gdsdiff 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gdsdiff/__init__.py +148 -0
- gdsdiff/__main__.py +7 -0
- gdsdiff/_environment.py +12 -0
- gdsdiff/_version.py +34 -0
- gdsdiff/acceptance_evidence.py +530 -0
- gdsdiff/api.py +2955 -0
- gdsdiff/backends/__init__.py +26 -0
- gdsdiff/backends/base.py +77 -0
- gdsdiff/backends/boundary_loop_ctypes.py +191 -0
- gdsdiff/backends/cpu.py +20 -0
- gdsdiff/backends/cpu_ctypes.py +255 -0
- gdsdiff/backends/cuda.py +40 -0
- gdsdiff/backends/cuda_ctypes.py +1297 -0
- gdsdiff/backends/exact.py +424 -0
- gdsdiff/backends/geometry_ctypes.py +252 -0
- gdsdiff/backends/identity.py +53 -0
- gdsdiff/backends/metal.py +198 -0
- gdsdiff/backends/metal_ctypes.py +866 -0
- gdsdiff/backends/polygon_extract_ctypes.py +233 -0
- gdsdiff/backends/structural_ctypes.py +130 -0
- gdsdiff/boundary_loops.py +616 -0
- gdsdiff/cache.py +544 -0
- gdsdiff/canonicalize.py +176 -0
- gdsdiff/cli.py +352 -0
- gdsdiff/config.py +257 -0
- gdsdiff/cuda_setup.py +174 -0
- gdsdiff/design_history.py +65 -0
- gdsdiff/diagnostics.py +42 -0
- gdsdiff/diff_gds.py +66 -0
- gdsdiff/exact_diff_markdown.py +979 -0
- gdsdiff/exact_oracle.py +649 -0
- gdsdiff/extract.py +376 -0
- gdsdiff/gds_metadata.py +43 -0
- gdsdiff/geometry.py +112 -0
- gdsdiff/grid.py +143 -0
- gdsdiff/hierarchy.py +215 -0
- gdsdiff/hierarchy_extract.py +263 -0
- gdsdiff/inventory.py +153 -0
- gdsdiff/multiprocessing_support.py +39 -0
- gdsdiff/native.py +135 -0
- gdsdiff/native_cache.py +265 -0
- gdsdiff/native_src/cpu_prefilter.cpp +349 -0
- gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
- gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
- gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
- gdsdiff/native_src/gds_structural_scan.cpp +335 -0
- gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
- gdsdiff/native_src/gdsdiff/core.cpp +191 -0
- gdsdiff/native_src/gdsdiff/core.hpp +96 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
- gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
- gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
- gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
- gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
- gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
- gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
- gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
- gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
- gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
- gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
- gdsdiff/policy.py +305 -0
- gdsdiff/polygon_components.py +860 -0
- gdsdiff/profiles.py +152 -0
- gdsdiff/py.typed +1 -0
- gdsdiff/rect_coverage.py +384 -0
- gdsdiff/report.py +354 -0
- gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
- gdsdiff/semantics.py +75 -0
- gdsdiff/source_edge_diff.py +1120 -0
- gdsdiff/spatial.py +71 -0
- gdsdiff/structural_guard.py +161 -0
- gdsdiff/surplus_coverage.py +255 -0
- gdsdiff/synthetic_suite.py +1643 -0
- gdsdiff/templates.py +709 -0
- gdsdiff/tiling.py +153 -0
- gdsdiff/windowed_exact.py +270 -0
- gdsdiff/windows.py +184 -0
- gdsdiff-0.1.0.dist-info/METADATA +135 -0
- gdsdiff-0.1.0.dist-info/RECORD +85 -0
- gdsdiff-0.1.0.dist-info/WHEEL +5 -0
- gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
- gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
- gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from time import perf_counter
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .boundary_loops import assemble_boundary_loops
|
|
9
|
+
from .config import LayerSpec
|
|
10
|
+
from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff
|
|
11
|
+
from .geometry import PolygonBuffer, polygon_bbox, polygon_twice_area
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ComponentPolygon:
|
|
16
|
+
side: str
|
|
17
|
+
source_index: int
|
|
18
|
+
fragment: DiffFragment
|
|
19
|
+
role: str = "surplus"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class PolygonComponent:
|
|
24
|
+
layer: LayerSpec
|
|
25
|
+
polygons: tuple[ComponentPolygon, ...]
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def old_count(self) -> int:
|
|
29
|
+
return sum(1 for polygon in self.polygons if polygon.side == "old")
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def new_count(self) -> int:
|
|
33
|
+
return sum(1 for polygon in self.polygons if polygon.side == "new")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def bbox(self) -> tuple[int, int, int, int]:
|
|
37
|
+
return _bbox_union(tuple(polygon.fragment.bbox for polygon in self.polygons))
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def vertex_count(self) -> int:
|
|
41
|
+
return sum(len(polygon.fragment.points) for polygon in self.polygons)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def has_positive_overlap(self) -> bool:
|
|
45
|
+
fragments = tuple(polygon.fragment for polygon in self.polygons)
|
|
46
|
+
return _has_positive_bbox_overlap(fragments)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class PolygonComponentStats:
|
|
51
|
+
component_count: int
|
|
52
|
+
overlap_component_count: int
|
|
53
|
+
max_fragments_per_component: int
|
|
54
|
+
max_vertices_per_component: int
|
|
55
|
+
context_index_backend: str = "none"
|
|
56
|
+
context_index_s: float = 0.0
|
|
57
|
+
context_pair_count: int = 0
|
|
58
|
+
context_cuda_kernel_count: int = 0
|
|
59
|
+
context_cuda_event_elapsed_s: float = 0.0
|
|
60
|
+
context_cuda_transferred_bytes: int = 0
|
|
61
|
+
context_metal_kernel_count: int = 0
|
|
62
|
+
context_metal_event_elapsed_s: float = 0.0
|
|
63
|
+
context_metal_transferred_bytes: int = 0
|
|
64
|
+
|
|
65
|
+
def to_json(self) -> dict[str, int | float | str]:
|
|
66
|
+
return {
|
|
67
|
+
"component_count": self.component_count,
|
|
68
|
+
"overlap_component_count": self.overlap_component_count,
|
|
69
|
+
"max_fragments_per_component": self.max_fragments_per_component,
|
|
70
|
+
"max_vertices_per_component": self.max_vertices_per_component,
|
|
71
|
+
"context_index_backend": self.context_index_backend,
|
|
72
|
+
"context_index_s": self.context_index_s,
|
|
73
|
+
"context_pair_count": self.context_pair_count,
|
|
74
|
+
"context_cuda_kernel_count": self.context_cuda_kernel_count,
|
|
75
|
+
"context_cuda_event_elapsed_s": self.context_cuda_event_elapsed_s,
|
|
76
|
+
"context_cuda_transferred_bytes": self.context_cuda_transferred_bytes,
|
|
77
|
+
"context_metal_kernel_count": self.context_metal_kernel_count,
|
|
78
|
+
"context_metal_event_elapsed_s": self.context_metal_event_elapsed_s,
|
|
79
|
+
"context_metal_transferred_bytes": self.context_metal_transferred_bytes,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class PolygonComponentBuildResult:
|
|
85
|
+
components: tuple[PolygonComponent, ...]
|
|
86
|
+
stats: PolygonComponentStats
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class LayerSideMergeStats:
|
|
91
|
+
input_fragment_count: int = 0
|
|
92
|
+
output_fragment_count: int = 0
|
|
93
|
+
group_count: int = 0
|
|
94
|
+
assembled_group_count: int = 0
|
|
95
|
+
cuda_edge_cancel_s: float = 0.0
|
|
96
|
+
cuda_edge_cancel_kernel_count: int = 0
|
|
97
|
+
cuda_edge_cancel_event_elapsed_s: float = 0.0
|
|
98
|
+
cuda_edge_cancel_transferred_bytes: int = 0
|
|
99
|
+
native_edge_cancel_s: float = 0.0
|
|
100
|
+
cuda_loop_trace_s: float = 0.0
|
|
101
|
+
metal_loop_trace_s: float = 0.0
|
|
102
|
+
metal_loop_kernel_count: int = 0
|
|
103
|
+
metal_loop_event_elapsed_s: float = 0.0
|
|
104
|
+
metal_loop_transferred_bytes: int = 0
|
|
105
|
+
native_loop_trace_s: float = 0.0
|
|
106
|
+
python_loop_trace_s: float = 0.0
|
|
107
|
+
hole_stitch_s: float = 0.0
|
|
108
|
+
|
|
109
|
+
def to_json(self) -> dict[str, int | float]:
|
|
110
|
+
return {
|
|
111
|
+
"input_fragment_count": self.input_fragment_count,
|
|
112
|
+
"output_fragment_count": self.output_fragment_count,
|
|
113
|
+
"group_count": self.group_count,
|
|
114
|
+
"assembled_group_count": self.assembled_group_count,
|
|
115
|
+
"cuda_edge_cancel_s": self.cuda_edge_cancel_s,
|
|
116
|
+
"cuda_edge_cancel_kernel_count": self.cuda_edge_cancel_kernel_count,
|
|
117
|
+
"cuda_edge_cancel_event_elapsed_s": self.cuda_edge_cancel_event_elapsed_s,
|
|
118
|
+
"cuda_edge_cancel_transferred_bytes": self.cuda_edge_cancel_transferred_bytes,
|
|
119
|
+
"native_edge_cancel_s": self.native_edge_cancel_s,
|
|
120
|
+
"cuda_loop_trace_s": self.cuda_loop_trace_s,
|
|
121
|
+
"metal_loop_trace_s": self.metal_loop_trace_s,
|
|
122
|
+
"metal_loop_kernel_count": self.metal_loop_kernel_count,
|
|
123
|
+
"metal_loop_event_elapsed_s": self.metal_loop_event_elapsed_s,
|
|
124
|
+
"metal_loop_transferred_bytes": self.metal_loop_transferred_bytes,
|
|
125
|
+
"native_loop_trace_s": self.native_loop_trace_s,
|
|
126
|
+
"python_loop_trace_s": self.python_loop_trace_s,
|
|
127
|
+
"hole_stitch_s": self.hole_stitch_s,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class LayerSideMergeResult:
|
|
133
|
+
fragments: tuple[DiffFragment, ...]
|
|
134
|
+
stats: LayerSideMergeStats
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True)
|
|
138
|
+
class _LayerBBoxIndex:
|
|
139
|
+
indices: np.ndarray
|
|
140
|
+
sorted_indices: np.ndarray
|
|
141
|
+
sorted_x0: np.ndarray
|
|
142
|
+
sorted_bboxes: np.ndarray
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(frozen=True)
|
|
146
|
+
class _ContextOverlapPairRun:
|
|
147
|
+
component_indices: np.ndarray
|
|
148
|
+
polygon_indices: np.ndarray
|
|
149
|
+
wall_s: float = 0.0
|
|
150
|
+
cuda_kernel_count: int = 0
|
|
151
|
+
cuda_event_elapsed_s: float = 0.0
|
|
152
|
+
metal_kernel_count: int = 0
|
|
153
|
+
metal_event_elapsed_s: float = 0.0
|
|
154
|
+
transferred_bytes: int = 0
|
|
155
|
+
accelerator: str = "cpu"
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def pair_count(self) -> int:
|
|
159
|
+
return int(len(self.component_indices))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def build_record_delta_surplus_components(
|
|
163
|
+
old_buffer: PolygonBuffer,
|
|
164
|
+
new_buffer: PolygonBuffer,
|
|
165
|
+
record_delta,
|
|
166
|
+
) -> PolygonComponentBuildResult:
|
|
167
|
+
by_layer: dict[LayerSpec, list[ComponentPolygon]] = {}
|
|
168
|
+
for row in range(record_delta.record_count):
|
|
169
|
+
direction = int(record_delta.directions[row])
|
|
170
|
+
count = int(record_delta.counts[row])
|
|
171
|
+
representative_index = int(record_delta.representative_indices[row])
|
|
172
|
+
if direction == 1:
|
|
173
|
+
layer = old_buffer.layer_table.layers[int(old_buffer.layer_ids[representative_index])]
|
|
174
|
+
polygon = ComponentPolygon(
|
|
175
|
+
side="old",
|
|
176
|
+
source_index=representative_index,
|
|
177
|
+
fragment=_fragment_from_buffer(old_buffer, representative_index),
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
layer = new_buffer.layer_table.layers[int(new_buffer.layer_ids[representative_index])]
|
|
181
|
+
polygon = ComponentPolygon(
|
|
182
|
+
side="new",
|
|
183
|
+
source_index=representative_index,
|
|
184
|
+
fragment=_fragment_from_buffer(new_buffer, representative_index),
|
|
185
|
+
)
|
|
186
|
+
by_layer.setdefault(layer, []).extend(polygon for _ in range(count))
|
|
187
|
+
|
|
188
|
+
components: list[PolygonComponent] = []
|
|
189
|
+
for layer in sorted(by_layer):
|
|
190
|
+
layer_polygons = tuple(by_layer[layer])
|
|
191
|
+
for component_indices in _polygon_components(layer_polygons):
|
|
192
|
+
components.append(
|
|
193
|
+
PolygonComponent(
|
|
194
|
+
layer=layer,
|
|
195
|
+
polygons=tuple(layer_polygons[index] for index in component_indices),
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
components_tuple = tuple(sorted(components, key=_component_sort_key))
|
|
199
|
+
return PolygonComponentBuildResult(
|
|
200
|
+
components=components_tuple,
|
|
201
|
+
stats=_component_stats(components_tuple),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def add_overlapping_context_to_components(
|
|
206
|
+
old_buffer: PolygonBuffer,
|
|
207
|
+
new_buffer: PolygonBuffer,
|
|
208
|
+
components: tuple[PolygonComponent, ...],
|
|
209
|
+
*,
|
|
210
|
+
preferred_accelerator: str | None = None,
|
|
211
|
+
) -> PolygonComponentBuildResult:
|
|
212
|
+
context_start = perf_counter()
|
|
213
|
+
old_fragment_cache: dict[int, DiffFragment] = {}
|
|
214
|
+
new_fragment_cache: dict[int, DiffFragment] = {}
|
|
215
|
+
old_source_indices = [
|
|
216
|
+
{polygon.source_index for polygon in component.polygons if polygon.side == "old"} for component in components
|
|
217
|
+
]
|
|
218
|
+
new_source_indices = [
|
|
219
|
+
{polygon.source_index for polygon in component.polygons if polygon.side == "new"} for component in components
|
|
220
|
+
]
|
|
221
|
+
old_pair_run = _try_accelerated_context_pairs(components, old_buffer, preferred_accelerator=preferred_accelerator)
|
|
222
|
+
new_pair_run = _try_accelerated_context_pairs(components, new_buffer, preferred_accelerator=preferred_accelerator)
|
|
223
|
+
if old_pair_run is not None and new_pair_run is not None:
|
|
224
|
+
expanded_polygons = [list(component.polygons) for component in components]
|
|
225
|
+
_append_context_pairs(
|
|
226
|
+
expanded_polygons,
|
|
227
|
+
old_pair_run,
|
|
228
|
+
old_source_indices,
|
|
229
|
+
side="old",
|
|
230
|
+
buffer=old_buffer,
|
|
231
|
+
fragment_cache=old_fragment_cache,
|
|
232
|
+
)
|
|
233
|
+
_append_context_pairs(
|
|
234
|
+
expanded_polygons,
|
|
235
|
+
new_pair_run,
|
|
236
|
+
new_source_indices,
|
|
237
|
+
side="new",
|
|
238
|
+
buffer=new_buffer,
|
|
239
|
+
fragment_cache=new_fragment_cache,
|
|
240
|
+
)
|
|
241
|
+
expanded_tuple = tuple(
|
|
242
|
+
sorted(
|
|
243
|
+
(
|
|
244
|
+
PolygonComponent(layer=component.layer, polygons=tuple(sorted(polygons, key=_polygon_sort_key)))
|
|
245
|
+
for component, polygons in zip(components, expanded_polygons)
|
|
246
|
+
),
|
|
247
|
+
key=_component_sort_key,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
accelerator = old_pair_run.accelerator
|
|
251
|
+
return PolygonComponentBuildResult(
|
|
252
|
+
components=expanded_tuple,
|
|
253
|
+
stats=_component_stats(
|
|
254
|
+
expanded_tuple,
|
|
255
|
+
context_index_backend=f"{accelerator}-context-overlap-pairs",
|
|
256
|
+
context_index_s=old_pair_run.wall_s + new_pair_run.wall_s,
|
|
257
|
+
context_pair_count=old_pair_run.pair_count + new_pair_run.pair_count,
|
|
258
|
+
context_cuda_kernel_count=old_pair_run.cuda_kernel_count + new_pair_run.cuda_kernel_count,
|
|
259
|
+
context_cuda_event_elapsed_s=old_pair_run.cuda_event_elapsed_s + new_pair_run.cuda_event_elapsed_s,
|
|
260
|
+
context_cuda_transferred_bytes=(
|
|
261
|
+
old_pair_run.transferred_bytes + new_pair_run.transferred_bytes if accelerator == "cuda" else 0
|
|
262
|
+
),
|
|
263
|
+
context_metal_kernel_count=old_pair_run.metal_kernel_count + new_pair_run.metal_kernel_count,
|
|
264
|
+
context_metal_event_elapsed_s=old_pair_run.metal_event_elapsed_s + new_pair_run.metal_event_elapsed_s,
|
|
265
|
+
context_metal_transferred_bytes=(
|
|
266
|
+
old_pair_run.transferred_bytes + new_pair_run.transferred_bytes if accelerator == "metal" else 0
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
old_layer_indices = _indices_by_layer_id(old_buffer)
|
|
271
|
+
new_layer_indices = _indices_by_layer_id(new_buffer)
|
|
272
|
+
old_layer_ids = old_buffer.layer_table.index_by_layer
|
|
273
|
+
new_layer_ids = new_buffer.layer_table.index_by_layer
|
|
274
|
+
expanded: list[PolygonComponent] = []
|
|
275
|
+
for component_index, component in enumerate(components):
|
|
276
|
+
old_indices = old_source_indices[component_index]
|
|
277
|
+
new_indices = new_source_indices[component_index]
|
|
278
|
+
polygons = list(component.polygons)
|
|
279
|
+
component_bbox = component.bbox
|
|
280
|
+
old_layer_id = old_layer_ids.get(component.layer)
|
|
281
|
+
new_layer_id = new_layer_ids.get(component.layer)
|
|
282
|
+
for index in _overlapping_layer_indices(old_buffer, old_layer_indices, old_layer_id, component_bbox):
|
|
283
|
+
if index not in old_indices:
|
|
284
|
+
polygons.append(
|
|
285
|
+
ComponentPolygon(
|
|
286
|
+
side="old",
|
|
287
|
+
source_index=index,
|
|
288
|
+
fragment=_cached_fragment(old_buffer, old_fragment_cache, index),
|
|
289
|
+
role="context",
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
for index in _overlapping_layer_indices(new_buffer, new_layer_indices, new_layer_id, component_bbox):
|
|
293
|
+
if index not in new_indices:
|
|
294
|
+
polygons.append(
|
|
295
|
+
ComponentPolygon(
|
|
296
|
+
side="new",
|
|
297
|
+
source_index=index,
|
|
298
|
+
fragment=_cached_fragment(new_buffer, new_fragment_cache, index),
|
|
299
|
+
role="context",
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
expanded.append(PolygonComponent(layer=component.layer, polygons=tuple(sorted(polygons, key=_polygon_sort_key))))
|
|
303
|
+
expanded_tuple = tuple(sorted(expanded, key=_component_sort_key))
|
|
304
|
+
return PolygonComponentBuildResult(
|
|
305
|
+
components=expanded_tuple,
|
|
306
|
+
stats=_component_stats(
|
|
307
|
+
expanded_tuple,
|
|
308
|
+
context_index_backend="cpu-layer-bbox-index",
|
|
309
|
+
context_index_s=perf_counter() - context_start,
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _merge_touching_components_by_layer(components: tuple[PolygonComponent, ...]) -> tuple[PolygonComponent, ...]:
|
|
315
|
+
by_layer: dict[LayerSpec, list[PolygonComponent]] = {}
|
|
316
|
+
for component in components:
|
|
317
|
+
by_layer.setdefault(component.layer, []).append(component)
|
|
318
|
+
merged: list[PolygonComponent] = []
|
|
319
|
+
for layer in sorted(by_layer):
|
|
320
|
+
layer_components = tuple(sorted(by_layer[layer], key=_component_sort_key))
|
|
321
|
+
for group in _component_bbox_contact_groups(layer_components):
|
|
322
|
+
polygons = {}
|
|
323
|
+
for index in group:
|
|
324
|
+
for polygon in layer_components[index].polygons:
|
|
325
|
+
polygons[(polygon.side, polygon.source_index, polygon.role, polygon.fragment.bbox)] = polygon
|
|
326
|
+
merged.append(
|
|
327
|
+
PolygonComponent(
|
|
328
|
+
layer=layer,
|
|
329
|
+
polygons=tuple(sorted(polygons.values(), key=_polygon_sort_key)),
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
return tuple(sorted(merged, key=_component_sort_key))
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _component_bbox_contact_groups(components: tuple[PolygonComponent, ...]) -> tuple[tuple[int, ...], ...]:
|
|
336
|
+
remaining = set(range(len(components)))
|
|
337
|
+
groups: list[tuple[int, ...]] = []
|
|
338
|
+
while remaining:
|
|
339
|
+
seed = remaining.pop()
|
|
340
|
+
group = {seed}
|
|
341
|
+
stack = [seed]
|
|
342
|
+
while stack:
|
|
343
|
+
left = stack.pop()
|
|
344
|
+
touched = [
|
|
345
|
+
right
|
|
346
|
+
for right in remaining
|
|
347
|
+
if _bbox_contact(components[left].bbox, components[right].bbox)
|
|
348
|
+
]
|
|
349
|
+
for right in touched:
|
|
350
|
+
remaining.remove(right)
|
|
351
|
+
group.add(right)
|
|
352
|
+
stack.append(right)
|
|
353
|
+
groups.append(tuple(sorted(group)))
|
|
354
|
+
return tuple(sorted(groups))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def exact_result_from_component_quads(
|
|
358
|
+
components: tuple[PolygonComponent, ...],
|
|
359
|
+
quads: np.ndarray,
|
|
360
|
+
fragment_areas: np.ndarray,
|
|
361
|
+
counts: np.ndarray,
|
|
362
|
+
component_areas: np.ndarray | None = None,
|
|
363
|
+
) -> ExactOracleResult:
|
|
364
|
+
by_layer: dict[LayerSpec, dict[str, list[DiffFragment]]] = {}
|
|
365
|
+
for component_index, component in enumerate(components):
|
|
366
|
+
layer_bucket = by_layer.setdefault(component.layer, {"old": [], "new": []})
|
|
367
|
+
one_sided = _one_sided_nonoverlap_fragments(component)
|
|
368
|
+
if one_sided is not None:
|
|
369
|
+
side, fragments = one_sided
|
|
370
|
+
layer_bucket[side].extend(fragments)
|
|
371
|
+
continue
|
|
372
|
+
for side_index, side in enumerate(("old", "new")):
|
|
373
|
+
fragments: list[DiffFragment] = []
|
|
374
|
+
for fragment_index in range(int(counts[component_index, side_index])):
|
|
375
|
+
points = tuple((int(x), int(y)) for x, y in quads[component_index, side_index, fragment_index])
|
|
376
|
+
if len(set(points)) < 3:
|
|
377
|
+
continue
|
|
378
|
+
twice_area = int(fragment_areas[component_index, side_index, fragment_index])
|
|
379
|
+
if twice_area == 0:
|
|
380
|
+
continue
|
|
381
|
+
fragments.append(DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=twice_area))
|
|
382
|
+
if fragments:
|
|
383
|
+
assembled = assemble_boundary_loops(tuple(fragments)).fragments
|
|
384
|
+
target_area = (
|
|
385
|
+
int(component_areas[component_index, side_index])
|
|
386
|
+
if component_areas is not None
|
|
387
|
+
else sum(fragment.twice_area for fragment in fragments)
|
|
388
|
+
)
|
|
389
|
+
layer_bucket[side].extend(_preserve_total_area(assembled, target_area))
|
|
390
|
+
layer_diffs = []
|
|
391
|
+
for layer in sorted(by_layer):
|
|
392
|
+
old_minus_new = _merge_layer_side_fragments(tuple(sorted(by_layer[layer]["old"], key=_fragment_sort_key)))
|
|
393
|
+
new_minus_old = _merge_layer_side_fragments(tuple(sorted(by_layer[layer]["new"], key=_fragment_sort_key)))
|
|
394
|
+
layer_diffs.append(
|
|
395
|
+
LayerExactDiff(
|
|
396
|
+
layer=layer,
|
|
397
|
+
old_minus_new=old_minus_new,
|
|
398
|
+
new_minus_old=new_minus_old,
|
|
399
|
+
xor=tuple(sorted(old_minus_new + new_minus_old, key=_fragment_sort_key)),
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
return ExactOracleResult(tuple(layer_diffs))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _fragment_from_buffer(buffer: PolygonBuffer, index: int) -> DiffFragment:
|
|
406
|
+
points = tuple((int(x), int(y)) for x, y in buffer.polygon_vertices(index))
|
|
407
|
+
return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points))
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _cached_fragment(buffer: PolygonBuffer, cache: dict[int, DiffFragment], index: int) -> DiffFragment:
|
|
411
|
+
fragment = cache.get(index)
|
|
412
|
+
if fragment is None:
|
|
413
|
+
fragment = _fragment_from_buffer(buffer, index)
|
|
414
|
+
cache[index] = fragment
|
|
415
|
+
return fragment
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _one_sided_nonoverlap_fragments(component: PolygonComponent) -> tuple[str, tuple[DiffFragment, ...]] | None:
|
|
419
|
+
sides = {polygon.side for polygon in component.polygons}
|
|
420
|
+
if len(sides) != 1:
|
|
421
|
+
return None
|
|
422
|
+
fragments = tuple(polygon.fragment for polygon in component.polygons)
|
|
423
|
+
if _has_bbox_contact(fragments):
|
|
424
|
+
return None
|
|
425
|
+
side = next(iter(sides))
|
|
426
|
+
return side, tuple(sorted(fragments, key=_fragment_sort_key))
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _preserve_total_area(fragments: tuple[DiffFragment, ...], target_twice_area: int) -> tuple[DiffFragment, ...]:
|
|
430
|
+
if not fragments:
|
|
431
|
+
return ()
|
|
432
|
+
current = sum(fragment.twice_area for fragment in fragments)
|
|
433
|
+
delta = target_twice_area - current
|
|
434
|
+
if delta == 0:
|
|
435
|
+
return fragments
|
|
436
|
+
largest_index = max(range(len(fragments)), key=lambda index: (fragments[index].twice_area, fragments[index].bbox))
|
|
437
|
+
adjusted = list(fragments)
|
|
438
|
+
largest = adjusted[largest_index]
|
|
439
|
+
if largest.twice_area + delta <= 0:
|
|
440
|
+
return fragments
|
|
441
|
+
adjusted[largest_index] = DiffFragment(points=largest.points, bbox=largest.bbox, twice_area=largest.twice_area + delta)
|
|
442
|
+
return tuple(sorted(adjusted, key=_fragment_sort_key))
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _indices_by_layer_id(buffer: PolygonBuffer) -> dict[int, _LayerBBoxIndex]:
|
|
446
|
+
if buffer.polygon_count == 0:
|
|
447
|
+
return {}
|
|
448
|
+
order = np.argsort(buffer.layer_ids, kind="stable")
|
|
449
|
+
sorted_layer_ids = buffer.layer_ids[order]
|
|
450
|
+
split_points = np.flatnonzero(sorted_layer_ids[1:] != sorted_layer_ids[:-1]) + 1
|
|
451
|
+
buckets = np.split(order.astype(np.int64, copy=False), split_points)
|
|
452
|
+
layer_ids = np.split(sorted_layer_ids, split_points)
|
|
453
|
+
return {int(ids[0]): _build_layer_bbox_index(buffer, bucket) for ids, bucket in zip(layer_ids, buckets) if len(ids)}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _build_layer_bbox_index(buffer: PolygonBuffer, indices: np.ndarray) -> _LayerBBoxIndex:
|
|
457
|
+
layer_bboxes = buffer.bboxes[indices]
|
|
458
|
+
order = np.argsort(layer_bboxes[:, 0], kind="stable")
|
|
459
|
+
sorted_indices = indices[order].astype(np.int64, copy=False)
|
|
460
|
+
sorted_bboxes = layer_bboxes[order].astype(np.int64, copy=False)
|
|
461
|
+
sorted_x0 = sorted_bboxes[:, 0]
|
|
462
|
+
return _LayerBBoxIndex(indices=indices, sorted_indices=sorted_indices, sorted_x0=sorted_x0, sorted_bboxes=sorted_bboxes)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _overlapping_layer_indices(
|
|
466
|
+
buffer: PolygonBuffer,
|
|
467
|
+
indices_by_layer: dict[int, _LayerBBoxIndex],
|
|
468
|
+
layer_id: int | None,
|
|
469
|
+
bbox: tuple[int, int, int, int],
|
|
470
|
+
) -> tuple[int, ...]:
|
|
471
|
+
if layer_id is None:
|
|
472
|
+
return ()
|
|
473
|
+
layer_index = indices_by_layer.get(layer_id)
|
|
474
|
+
if layer_index is None or len(layer_index.indices) == 0:
|
|
475
|
+
return ()
|
|
476
|
+
stop = int(np.searchsorted(layer_index.sorted_x0, bbox[2], side="left"))
|
|
477
|
+
if stop == 0:
|
|
478
|
+
return ()
|
|
479
|
+
indices = layer_index.sorted_indices[:stop]
|
|
480
|
+
bboxes = layer_index.sorted_bboxes[:stop]
|
|
481
|
+
mask = (
|
|
482
|
+
(bboxes[:, 2] > bbox[0])
|
|
483
|
+
& (np.maximum(bboxes[:, 1], bbox[1]) < np.minimum(bboxes[:, 3], bbox[3]))
|
|
484
|
+
)
|
|
485
|
+
return tuple(sorted(int(index) for index in indices[mask]))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _try_accelerated_context_pairs(
|
|
489
|
+
components: tuple[PolygonComponent, ...],
|
|
490
|
+
buffer: PolygonBuffer,
|
|
491
|
+
*,
|
|
492
|
+
preferred_accelerator: str | None,
|
|
493
|
+
) -> _ContextOverlapPairRun | None:
|
|
494
|
+
if preferred_accelerator == "cuda":
|
|
495
|
+
return _try_cuda_context_pairs(components, buffer)
|
|
496
|
+
if preferred_accelerator == "metal":
|
|
497
|
+
return _try_metal_context_pairs(components, buffer)
|
|
498
|
+
return _try_cuda_context_pairs(components, buffer) or _try_metal_context_pairs(components, buffer)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _try_cuda_context_pairs(
|
|
502
|
+
components: tuple[PolygonComponent, ...],
|
|
503
|
+
buffer: PolygonBuffer,
|
|
504
|
+
) -> _ContextOverlapPairRun | None:
|
|
505
|
+
if not components or buffer.polygon_count == 0:
|
|
506
|
+
return _ContextOverlapPairRun(
|
|
507
|
+
component_indices=np.empty((0,), dtype=np.uint64),
|
|
508
|
+
polygon_indices=np.empty((0,), dtype=np.uint64),
|
|
509
|
+
)
|
|
510
|
+
try:
|
|
511
|
+
from .backends.cuda_ctypes import CudaCtypesUnavailable, run_cuda_context_overlap_pairs
|
|
512
|
+
|
|
513
|
+
component_bboxes, component_layers, component_datatypes = _component_context_arrays(components)
|
|
514
|
+
polygon_layers, polygon_datatypes = _buffer_layer_datatype_arrays(buffer)
|
|
515
|
+
start = perf_counter()
|
|
516
|
+
pairs = run_cuda_context_overlap_pairs(
|
|
517
|
+
component_bboxes,
|
|
518
|
+
component_layers,
|
|
519
|
+
component_datatypes,
|
|
520
|
+
buffer.bboxes,
|
|
521
|
+
polygon_layers,
|
|
522
|
+
polygon_datatypes,
|
|
523
|
+
)
|
|
524
|
+
wall_s = perf_counter() - start
|
|
525
|
+
return _ContextOverlapPairRun(
|
|
526
|
+
component_indices=pairs.component_indices,
|
|
527
|
+
polygon_indices=pairs.polygon_indices,
|
|
528
|
+
wall_s=wall_s,
|
|
529
|
+
cuda_kernel_count=pairs.cuda_kernel_count,
|
|
530
|
+
cuda_event_elapsed_s=pairs.cuda_event_elapsed_s,
|
|
531
|
+
transferred_bytes=pairs.transferred_bytes,
|
|
532
|
+
accelerator="cuda",
|
|
533
|
+
)
|
|
534
|
+
except (CudaCtypesUnavailable, ImportError, OSError, ValueError, OverflowError):
|
|
535
|
+
return None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _try_metal_context_pairs(
|
|
539
|
+
components: tuple[PolygonComponent, ...],
|
|
540
|
+
buffer: PolygonBuffer,
|
|
541
|
+
) -> _ContextOverlapPairRun | None:
|
|
542
|
+
if not components or buffer.polygon_count == 0:
|
|
543
|
+
return _ContextOverlapPairRun(
|
|
544
|
+
component_indices=np.empty((0,), dtype=np.uint64),
|
|
545
|
+
polygon_indices=np.empty((0,), dtype=np.uint64),
|
|
546
|
+
accelerator="metal",
|
|
547
|
+
)
|
|
548
|
+
try:
|
|
549
|
+
from .backends.metal_ctypes import MetalCtypesUnavailable, run_metal_context_overlap_pairs
|
|
550
|
+
|
|
551
|
+
component_bboxes, component_layers, component_datatypes = _component_context_arrays(components)
|
|
552
|
+
polygon_layers, polygon_datatypes = _buffer_layer_datatype_arrays(buffer)
|
|
553
|
+
start = perf_counter()
|
|
554
|
+
pairs = run_metal_context_overlap_pairs(
|
|
555
|
+
component_bboxes,
|
|
556
|
+
component_layers,
|
|
557
|
+
component_datatypes,
|
|
558
|
+
buffer.bboxes,
|
|
559
|
+
polygon_layers,
|
|
560
|
+
polygon_datatypes,
|
|
561
|
+
)
|
|
562
|
+
wall_s = perf_counter() - start
|
|
563
|
+
return _ContextOverlapPairRun(
|
|
564
|
+
component_indices=pairs.component_indices,
|
|
565
|
+
polygon_indices=pairs.polygon_indices,
|
|
566
|
+
wall_s=wall_s,
|
|
567
|
+
metal_kernel_count=pairs.metal_kernel_count,
|
|
568
|
+
metal_event_elapsed_s=pairs.metal_event_elapsed_s,
|
|
569
|
+
transferred_bytes=pairs.transferred_bytes,
|
|
570
|
+
accelerator="metal",
|
|
571
|
+
)
|
|
572
|
+
except (MetalCtypesUnavailable, ImportError, OSError, ValueError, OverflowError):
|
|
573
|
+
return None
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def _component_context_arrays(
|
|
577
|
+
components: tuple[PolygonComponent, ...],
|
|
578
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
579
|
+
bboxes = np.asarray([component.bbox for component in components], dtype=np.int64).reshape((-1, 4))
|
|
580
|
+
layers = np.asarray([component.layer.layer for component in components], dtype=np.int32)
|
|
581
|
+
datatypes = np.asarray([component.layer.datatype for component in components], dtype=np.int32)
|
|
582
|
+
return (
|
|
583
|
+
np.ascontiguousarray(bboxes, dtype=np.int64),
|
|
584
|
+
np.ascontiguousarray(layers, dtype=np.int32),
|
|
585
|
+
np.ascontiguousarray(datatypes, dtype=np.int32),
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _buffer_layer_datatype_arrays(buffer: PolygonBuffer) -> tuple[np.ndarray, np.ndarray]:
|
|
590
|
+
layer_values = np.asarray([layer.layer for layer in buffer.layer_table.layers], dtype=np.int32)
|
|
591
|
+
datatype_values = np.asarray([layer.datatype for layer in buffer.layer_table.layers], dtype=np.int32)
|
|
592
|
+
return (
|
|
593
|
+
np.ascontiguousarray(layer_values[buffer.layer_ids], dtype=np.int32),
|
|
594
|
+
np.ascontiguousarray(datatype_values[buffer.layer_ids], dtype=np.int32),
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _append_context_pairs(
|
|
599
|
+
expanded_polygons: list[list[ComponentPolygon]],
|
|
600
|
+
pair_run: _ContextOverlapPairRun,
|
|
601
|
+
source_indices: list[set[int]],
|
|
602
|
+
*,
|
|
603
|
+
side: str,
|
|
604
|
+
buffer: PolygonBuffer,
|
|
605
|
+
fragment_cache: dict[int, DiffFragment],
|
|
606
|
+
) -> None:
|
|
607
|
+
for component_index_raw, polygon_index_raw in zip(pair_run.component_indices, pair_run.polygon_indices):
|
|
608
|
+
component_index = int(component_index_raw)
|
|
609
|
+
polygon_index = int(polygon_index_raw)
|
|
610
|
+
if polygon_index in source_indices[component_index]:
|
|
611
|
+
continue
|
|
612
|
+
expanded_polygons[component_index].append(
|
|
613
|
+
ComponentPolygon(
|
|
614
|
+
side=side,
|
|
615
|
+
source_index=polygon_index,
|
|
616
|
+
fragment=_cached_fragment(buffer, fragment_cache, polygon_index),
|
|
617
|
+
role="context",
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _polygon_components(polygons: tuple[ComponentPolygon, ...]) -> tuple[tuple[int, ...], ...]:
|
|
623
|
+
remaining = set(range(len(polygons)))
|
|
624
|
+
components: list[tuple[int, ...]] = []
|
|
625
|
+
while remaining:
|
|
626
|
+
seed = remaining.pop()
|
|
627
|
+
component = {seed}
|
|
628
|
+
stack = [seed]
|
|
629
|
+
while stack:
|
|
630
|
+
left = stack.pop()
|
|
631
|
+
touched = [
|
|
632
|
+
right
|
|
633
|
+
for right in remaining
|
|
634
|
+
if _positive_overlap(polygons[left].fragment.bbox, polygons[right].fragment.bbox)
|
|
635
|
+
]
|
|
636
|
+
for right in touched:
|
|
637
|
+
remaining.remove(right)
|
|
638
|
+
component.add(right)
|
|
639
|
+
stack.append(right)
|
|
640
|
+
components.append(tuple(sorted(component)))
|
|
641
|
+
return tuple(sorted(components))
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _component_stats(
|
|
645
|
+
components: tuple[PolygonComponent, ...],
|
|
646
|
+
*,
|
|
647
|
+
context_index_backend: str = "none",
|
|
648
|
+
context_index_s: float = 0.0,
|
|
649
|
+
context_pair_count: int = 0,
|
|
650
|
+
context_cuda_kernel_count: int = 0,
|
|
651
|
+
context_cuda_event_elapsed_s: float = 0.0,
|
|
652
|
+
context_cuda_transferred_bytes: int = 0,
|
|
653
|
+
context_metal_kernel_count: int = 0,
|
|
654
|
+
context_metal_event_elapsed_s: float = 0.0,
|
|
655
|
+
context_metal_transferred_bytes: int = 0,
|
|
656
|
+
) -> PolygonComponentStats:
|
|
657
|
+
return PolygonComponentStats(
|
|
658
|
+
component_count=len(components),
|
|
659
|
+
overlap_component_count=sum(1 for component in components if component.has_positive_overlap),
|
|
660
|
+
max_fragments_per_component=max((len(component.polygons) for component in components), default=0),
|
|
661
|
+
max_vertices_per_component=max((component.vertex_count for component in components), default=0),
|
|
662
|
+
context_index_backend=context_index_backend,
|
|
663
|
+
context_index_s=context_index_s,
|
|
664
|
+
context_pair_count=context_pair_count,
|
|
665
|
+
context_cuda_kernel_count=context_cuda_kernel_count,
|
|
666
|
+
context_cuda_event_elapsed_s=context_cuda_event_elapsed_s,
|
|
667
|
+
context_cuda_transferred_bytes=context_cuda_transferred_bytes,
|
|
668
|
+
context_metal_kernel_count=context_metal_kernel_count,
|
|
669
|
+
context_metal_event_elapsed_s=context_metal_event_elapsed_s,
|
|
670
|
+
context_metal_transferred_bytes=context_metal_transferred_bytes,
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _merge_layer_side_fragments(fragments: tuple[DiffFragment, ...]) -> tuple[DiffFragment, ...]:
|
|
675
|
+
return _merge_layer_side_fragments_with_stats(fragments).fragments
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _merge_layer_side_fragments_with_stats(fragments: tuple[DiffFragment, ...]) -> LayerSideMergeResult:
|
|
679
|
+
if len(fragments) < 2:
|
|
680
|
+
return LayerSideMergeResult(
|
|
681
|
+
fragments=fragments,
|
|
682
|
+
stats=LayerSideMergeStats(input_fragment_count=len(fragments), output_fragment_count=len(fragments)),
|
|
683
|
+
)
|
|
684
|
+
if len(fragments) > 512 or sum(len(fragment.points) for fragment in fragments) > 50_000:
|
|
685
|
+
return LayerSideMergeResult(
|
|
686
|
+
fragments=fragments,
|
|
687
|
+
stats=LayerSideMergeStats(input_fragment_count=len(fragments), output_fragment_count=len(fragments)),
|
|
688
|
+
)
|
|
689
|
+
merged: list[DiffFragment] = []
|
|
690
|
+
group_count = 0
|
|
691
|
+
assembled_group_count = 0
|
|
692
|
+
cuda_edge_cancel_s = 0.0
|
|
693
|
+
cuda_edge_cancel_kernel_count = 0
|
|
694
|
+
cuda_edge_cancel_event_elapsed_s = 0.0
|
|
695
|
+
cuda_edge_cancel_transferred_bytes = 0
|
|
696
|
+
native_edge_cancel_s = 0.0
|
|
697
|
+
cuda_loop_trace_s = 0.0
|
|
698
|
+
metal_loop_trace_s = 0.0
|
|
699
|
+
metal_loop_kernel_count = 0
|
|
700
|
+
metal_loop_event_elapsed_s = 0.0
|
|
701
|
+
metal_loop_transferred_bytes = 0
|
|
702
|
+
native_loop_trace_s = 0.0
|
|
703
|
+
python_loop_trace_s = 0.0
|
|
704
|
+
hole_stitch_s = 0.0
|
|
705
|
+
for group in _fragment_bbox_components(fragments):
|
|
706
|
+
group_count += 1
|
|
707
|
+
if len(group) == 1:
|
|
708
|
+
merged.append(_canonicalize_single_fragment(group[0]))
|
|
709
|
+
continue
|
|
710
|
+
target_area = sum(fragment.twice_area for fragment in group)
|
|
711
|
+
assembly = assemble_boundary_loops(group)
|
|
712
|
+
assembled = assembly.fragments
|
|
713
|
+
assembled_group_count += 1
|
|
714
|
+
cuda_edge_cancel_s += assembly.stats.cuda_edge_cancel_s
|
|
715
|
+
cuda_edge_cancel_kernel_count += assembly.stats.cuda_edge_cancel_kernel_count
|
|
716
|
+
cuda_edge_cancel_event_elapsed_s += assembly.stats.cuda_edge_cancel_event_elapsed_s
|
|
717
|
+
cuda_edge_cancel_transferred_bytes += assembly.stats.cuda_edge_cancel_transferred_bytes
|
|
718
|
+
native_edge_cancel_s += assembly.stats.native_edge_cancel_s
|
|
719
|
+
cuda_loop_trace_s += assembly.stats.cuda_loop_trace_s
|
|
720
|
+
metal_loop_trace_s += assembly.stats.metal_loop_trace_s
|
|
721
|
+
metal_loop_kernel_count += assembly.stats.metal_loop_kernel_count
|
|
722
|
+
metal_loop_event_elapsed_s += assembly.stats.metal_loop_event_elapsed_s
|
|
723
|
+
metal_loop_transferred_bytes += assembly.stats.metal_loop_transferred_bytes
|
|
724
|
+
native_loop_trace_s += assembly.stats.native_loop_trace_s
|
|
725
|
+
python_loop_trace_s += assembly.stats.python_loop_trace_s
|
|
726
|
+
hole_stitch_s += assembly.stats.hole_stitch_s
|
|
727
|
+
if not assembled:
|
|
728
|
+
merged.extend(group)
|
|
729
|
+
else:
|
|
730
|
+
merged.extend(_preserve_total_area(assembled, target_area))
|
|
731
|
+
output = tuple(sorted(merged, key=_fragment_sort_key))
|
|
732
|
+
return LayerSideMergeResult(
|
|
733
|
+
fragments=output,
|
|
734
|
+
stats=LayerSideMergeStats(
|
|
735
|
+
input_fragment_count=len(fragments),
|
|
736
|
+
output_fragment_count=len(output),
|
|
737
|
+
group_count=group_count,
|
|
738
|
+
assembled_group_count=assembled_group_count,
|
|
739
|
+
cuda_edge_cancel_s=cuda_edge_cancel_s,
|
|
740
|
+
cuda_edge_cancel_kernel_count=cuda_edge_cancel_kernel_count,
|
|
741
|
+
cuda_edge_cancel_event_elapsed_s=cuda_edge_cancel_event_elapsed_s,
|
|
742
|
+
cuda_edge_cancel_transferred_bytes=cuda_edge_cancel_transferred_bytes,
|
|
743
|
+
native_edge_cancel_s=native_edge_cancel_s,
|
|
744
|
+
cuda_loop_trace_s=cuda_loop_trace_s,
|
|
745
|
+
metal_loop_trace_s=metal_loop_trace_s,
|
|
746
|
+
metal_loop_kernel_count=metal_loop_kernel_count,
|
|
747
|
+
metal_loop_event_elapsed_s=metal_loop_event_elapsed_s,
|
|
748
|
+
metal_loop_transferred_bytes=metal_loop_transferred_bytes,
|
|
749
|
+
native_loop_trace_s=native_loop_trace_s,
|
|
750
|
+
python_loop_trace_s=python_loop_trace_s,
|
|
751
|
+
hole_stitch_s=hole_stitch_s,
|
|
752
|
+
),
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _canonicalize_single_fragment(fragment: DiffFragment) -> DiffFragment:
|
|
757
|
+
points = _clean_loop_points(fragment.points)
|
|
758
|
+
if len(points) < 3:
|
|
759
|
+
return fragment
|
|
760
|
+
twice_area = polygon_twice_area(points)
|
|
761
|
+
if twice_area != fragment.twice_area:
|
|
762
|
+
return fragment
|
|
763
|
+
return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=twice_area)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _clean_loop_points(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
|
|
767
|
+
cleaned: list[tuple[int, int]] = []
|
|
768
|
+
for point in points:
|
|
769
|
+
if not cleaned or cleaned[-1] != point:
|
|
770
|
+
cleaned.append(point)
|
|
771
|
+
if len(cleaned) > 1 and cleaned[0] == cleaned[-1]:
|
|
772
|
+
cleaned.pop()
|
|
773
|
+
changed = True
|
|
774
|
+
while changed and len(cleaned) >= 3:
|
|
775
|
+
changed = False
|
|
776
|
+
next_points: list[tuple[int, int]] = []
|
|
777
|
+
for index, point in enumerate(cleaned):
|
|
778
|
+
previous = cleaned[index - 1]
|
|
779
|
+
following = cleaned[(index + 1) % len(cleaned)]
|
|
780
|
+
if _collinear(previous, point, following):
|
|
781
|
+
changed = True
|
|
782
|
+
continue
|
|
783
|
+
next_points.append(point)
|
|
784
|
+
cleaned = next_points
|
|
785
|
+
return tuple(cleaned)
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _collinear(
|
|
789
|
+
left: tuple[int, int],
|
|
790
|
+
middle: tuple[int, int],
|
|
791
|
+
right: tuple[int, int],
|
|
792
|
+
) -> bool:
|
|
793
|
+
return (middle[0] - left[0]) * (right[1] - middle[1]) == (middle[1] - left[1]) * (right[0] - middle[0])
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _fragment_bbox_components(fragments: tuple[DiffFragment, ...]) -> tuple[tuple[DiffFragment, ...], ...]:
|
|
797
|
+
remaining = set(range(len(fragments)))
|
|
798
|
+
components: list[tuple[DiffFragment, ...]] = []
|
|
799
|
+
while remaining:
|
|
800
|
+
seed = remaining.pop()
|
|
801
|
+
component = {seed}
|
|
802
|
+
stack = [seed]
|
|
803
|
+
while stack:
|
|
804
|
+
left = stack.pop()
|
|
805
|
+
touched = [
|
|
806
|
+
right
|
|
807
|
+
for right in remaining
|
|
808
|
+
if _bbox_contact(fragments[left].bbox, fragments[right].bbox)
|
|
809
|
+
]
|
|
810
|
+
for right in touched:
|
|
811
|
+
remaining.remove(right)
|
|
812
|
+
component.add(right)
|
|
813
|
+
stack.append(right)
|
|
814
|
+
components.append(tuple(fragments[index] for index in sorted(component)))
|
|
815
|
+
return tuple(sorted(components, key=lambda group: (_bbox_union(tuple(fragment.bbox for fragment in group)), len(group))))
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _has_positive_bbox_overlap(fragments: tuple[DiffFragment, ...]) -> bool:
|
|
819
|
+
for left_index, left in enumerate(fragments):
|
|
820
|
+
for right in fragments[left_index + 1 :]:
|
|
821
|
+
if _positive_overlap(left.bbox, right.bbox):
|
|
822
|
+
return True
|
|
823
|
+
return False
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
def _has_bbox_contact(fragments: tuple[DiffFragment, ...]) -> bool:
|
|
827
|
+
for left_index, left in enumerate(fragments):
|
|
828
|
+
for right in fragments[left_index + 1 :]:
|
|
829
|
+
if _bbox_contact(left.bbox, right.bbox):
|
|
830
|
+
return True
|
|
831
|
+
return False
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _positive_overlap(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
|
|
835
|
+
return max(left[0], right[0]) < min(left[2], right[2]) and max(left[1], right[1]) < min(left[3], right[3])
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _bbox_contact(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
|
|
839
|
+
return max(left[0], right[0]) <= min(left[2], right[2]) and max(left[1], right[1]) <= min(left[3], right[3])
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _bbox_union(bboxes: tuple[tuple[int, int, int, int], ...]) -> tuple[int, int, int, int]:
|
|
843
|
+
return (
|
|
844
|
+
min(bbox[0] for bbox in bboxes),
|
|
845
|
+
min(bbox[1] for bbox in bboxes),
|
|
846
|
+
max(bbox[2] for bbox in bboxes),
|
|
847
|
+
max(bbox[3] for bbox in bboxes),
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _component_sort_key(component: PolygonComponent) -> tuple[int, int, tuple[int, int, int, int], int, int]:
|
|
852
|
+
return component.layer.layer, component.layer.datatype, component.bbox, component.old_count, component.new_count
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def _polygon_sort_key(polygon: ComponentPolygon) -> tuple[str, str, int, tuple[int, int, int, int]]:
|
|
856
|
+
return polygon.side, polygon.role, polygon.source_index, polygon.fragment.bbox
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _fragment_sort_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]]:
|
|
860
|
+
return fragment.bbox, fragment.twice_area, fragment.points
|