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/profiles.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
import gdstk
|
|
9
|
+
|
|
10
|
+
from .api import CompareRunResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class AssemblyProfile:
|
|
15
|
+
name: str
|
|
16
|
+
gds_path: Path
|
|
17
|
+
expected_top: str | None = None
|
|
18
|
+
expected_reference_count: int | None = None
|
|
19
|
+
expected_layers: tuple[tuple[int, int], ...] = ()
|
|
20
|
+
expected_files: tuple[Path, ...] = ()
|
|
21
|
+
manifest_path: Path | None = None
|
|
22
|
+
manifest_required_columns: tuple[str, ...] = ()
|
|
23
|
+
manifest_expected_rows: int | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ProfileCheck:
|
|
28
|
+
name: str
|
|
29
|
+
passed: bool
|
|
30
|
+
observed: object
|
|
31
|
+
expected: object = None
|
|
32
|
+
message: str = ""
|
|
33
|
+
|
|
34
|
+
def to_json(self) -> dict[str, object]:
|
|
35
|
+
return {
|
|
36
|
+
"name": self.name,
|
|
37
|
+
"passed": self.passed,
|
|
38
|
+
"observed": self.observed,
|
|
39
|
+
"expected": self.expected,
|
|
40
|
+
"message": self.message,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ProfileResult:
|
|
46
|
+
profile_name: str
|
|
47
|
+
checks: tuple[ProfileCheck, ...] = field(default_factory=tuple)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def passed(self) -> bool:
|
|
51
|
+
return all(check.passed for check in self.checks)
|
|
52
|
+
|
|
53
|
+
def to_json(self) -> dict[str, object]:
|
|
54
|
+
return {
|
|
55
|
+
"profile_name": self.profile_name,
|
|
56
|
+
"passed": self.passed,
|
|
57
|
+
"checks": [check.to_json() for check in self.checks],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class CombinedProfileSummary:
|
|
63
|
+
geometry: CompareRunResult | None
|
|
64
|
+
assembly: ProfileResult | None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def passed(self) -> bool:
|
|
68
|
+
geometry_ok = True if self.geometry is None else self.geometry.exit_code == 0
|
|
69
|
+
assembly_ok = True if self.assembly is None else self.assembly.passed
|
|
70
|
+
return geometry_ok and assembly_ok
|
|
71
|
+
|
|
72
|
+
def render_markdown(self) -> str:
|
|
73
|
+
lines = ["### GDS Comparator Profile Summary", ""]
|
|
74
|
+
if self.geometry is not None:
|
|
75
|
+
lines.extend(
|
|
76
|
+
[
|
|
77
|
+
f"- Geometry status: {self.geometry.status.value}.",
|
|
78
|
+
f"- Geometry exit code: {self.geometry.exit_code}.",
|
|
79
|
+
]
|
|
80
|
+
)
|
|
81
|
+
if self.assembly is not None:
|
|
82
|
+
lines.append(f"- Assembly profile: {self.assembly.profile_name}.")
|
|
83
|
+
lines.append(f"- Assembly checks passed: {self.assembly.passed}.")
|
|
84
|
+
for check in self.assembly.checks:
|
|
85
|
+
status = "PASS" if check.passed else "FAIL"
|
|
86
|
+
lines.append(f" - {status} {check.name}: observed={check.observed!r} expected={check.expected!r}")
|
|
87
|
+
return "\n".join(lines) + "\n"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def check_assembly_profile(profile: AssemblyProfile) -> ProfileResult:
|
|
91
|
+
checks: list[ProfileCheck] = []
|
|
92
|
+
for path in profile.expected_files:
|
|
93
|
+
checks.append(ProfileCheck(name=f"file:{path.name}", passed=path.exists(), observed=str(path), expected="exists"))
|
|
94
|
+
if profile.gds_path.exists():
|
|
95
|
+
lib = gdstk.read_gds(str(profile.gds_path))
|
|
96
|
+
tops = lib.top_level()
|
|
97
|
+
top = _resolve_top(tops, profile.expected_top)
|
|
98
|
+
checks.append(ProfileCheck(name="top", passed=top is not None, observed=None if top is None else top.name, expected=profile.expected_top))
|
|
99
|
+
if top is not None:
|
|
100
|
+
if profile.expected_reference_count is not None:
|
|
101
|
+
observed = len(top.references)
|
|
102
|
+
checks.append(
|
|
103
|
+
ProfileCheck(
|
|
104
|
+
name="top_reference_count",
|
|
105
|
+
passed=observed == profile.expected_reference_count,
|
|
106
|
+
observed=observed,
|
|
107
|
+
expected=profile.expected_reference_count,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
if profile.expected_layers:
|
|
111
|
+
observed_layers = {(polygon.layer, polygon.datatype) for polygon in top.get_polygons(apply_repetitions=True)}
|
|
112
|
+
expected_layers = set(profile.expected_layers)
|
|
113
|
+
checks.append(
|
|
114
|
+
ProfileCheck(
|
|
115
|
+
name="expected_layers",
|
|
116
|
+
passed=expected_layers <= observed_layers,
|
|
117
|
+
observed=sorted(observed_layers),
|
|
118
|
+
expected=sorted(expected_layers),
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
checks.append(ProfileCheck(name="gds_path", passed=False, observed=str(profile.gds_path), expected="exists"))
|
|
123
|
+
if profile.manifest_path is not None:
|
|
124
|
+
checks.extend(_check_manifest(profile.manifest_path, profile.manifest_required_columns, profile.manifest_expected_rows))
|
|
125
|
+
return ProfileResult(profile.name, tuple(checks))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def combined_profile_summary(geometry: CompareRunResult | None = None, assembly: ProfileResult | None = None) -> CombinedProfileSummary:
|
|
129
|
+
return CombinedProfileSummary(geometry=geometry, assembly=assembly)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _resolve_top(tops: list[gdstk.Cell], expected_top: str | None) -> gdstk.Cell | None:
|
|
133
|
+
if expected_top is None:
|
|
134
|
+
return tops[0] if len(tops) == 1 else None
|
|
135
|
+
for top in tops:
|
|
136
|
+
if top.name == expected_top:
|
|
137
|
+
return top
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_manifest(path: Path, required_columns: Iterable[str], expected_rows: int | None) -> tuple[ProfileCheck, ...]:
|
|
142
|
+
if not path.exists():
|
|
143
|
+
return (ProfileCheck(name="manifest_path", passed=False, observed=str(path), expected="exists"),)
|
|
144
|
+
with path.open(newline="") as handle:
|
|
145
|
+
rows = list(csv.DictReader(handle))
|
|
146
|
+
checks = []
|
|
147
|
+
columns = set(rows[0]) if rows else set()
|
|
148
|
+
required = set(required_columns)
|
|
149
|
+
checks.append(ProfileCheck(name="manifest_columns", passed=required <= columns, observed=sorted(columns), expected=sorted(required)))
|
|
150
|
+
if expected_rows is not None:
|
|
151
|
+
checks.append(ProfileCheck(name="manifest_row_count", passed=len(rows) == expected_rows, observed=len(rows), expected=expected_rows))
|
|
152
|
+
return tuple(checks)
|
gdsdiff/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
gdsdiff/rect_coverage.py
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
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 .config import LayerSpec
|
|
9
|
+
from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff
|
|
10
|
+
from .geometry import PolygonBuffer, polygon_bbox, polygon_twice_area
|
|
11
|
+
from .polygon_components import _fragment_sort_key, _merge_layer_side_fragments
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RectCoverageUnsupported(RuntimeError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Rect = tuple[int, int, int, int]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RectCoverageStats:
|
|
23
|
+
component_count: int
|
|
24
|
+
cuda_task_count: int
|
|
25
|
+
old_rect_count: int
|
|
26
|
+
new_rect_count: int
|
|
27
|
+
dirty_rect_count: int
|
|
28
|
+
max_old_rects_per_component: int
|
|
29
|
+
max_new_rects_per_component: int
|
|
30
|
+
transferred_bytes: int
|
|
31
|
+
cuda_kernel_count: int
|
|
32
|
+
h2d_upload_s: float
|
|
33
|
+
gpu_xor_s: float
|
|
34
|
+
gpu_record_emit_s: float
|
|
35
|
+
accelerator: str = "cuda"
|
|
36
|
+
metal_task_count: int = 0
|
|
37
|
+
|
|
38
|
+
def to_json(self) -> dict[str, object]:
|
|
39
|
+
return {
|
|
40
|
+
"accelerator": self.accelerator,
|
|
41
|
+
"component_count": self.component_count,
|
|
42
|
+
"cuda_task_count": self.cuda_task_count,
|
|
43
|
+
"metal_task_count": self.metal_task_count,
|
|
44
|
+
"old_rect_count": self.old_rect_count,
|
|
45
|
+
"new_rect_count": self.new_rect_count,
|
|
46
|
+
"dirty_rect_count": self.dirty_rect_count,
|
|
47
|
+
"max_old_rects_per_component": self.max_old_rects_per_component,
|
|
48
|
+
"max_new_rects_per_component": self.max_new_rects_per_component,
|
|
49
|
+
"transferred_bytes": self.transferred_bytes,
|
|
50
|
+
"cuda_kernel_count": self.cuda_kernel_count,
|
|
51
|
+
"h2d_upload_s": self.h2d_upload_s,
|
|
52
|
+
"gpu_xor_s": self.gpu_xor_s,
|
|
53
|
+
"gpu_record_emit_s": self.gpu_record_emit_s,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class RectCoverageResult:
|
|
59
|
+
exact_result: ExactOracleResult
|
|
60
|
+
stats: RectCoverageStats
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class _RectComponent:
|
|
65
|
+
layer: LayerSpec
|
|
66
|
+
old_rects: tuple[Rect, ...]
|
|
67
|
+
new_rects: tuple[Rect, ...]
|
|
68
|
+
bbox: Rect
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_cuda_rectangle_coverage_diff(
|
|
72
|
+
old_buffer: PolygonBuffer,
|
|
73
|
+
new_buffer: PolygonBuffer,
|
|
74
|
+
*,
|
|
75
|
+
max_rects_per_component: int = 16,
|
|
76
|
+
max_fragments_per_component: int = 4096,
|
|
77
|
+
) -> RectCoverageResult:
|
|
78
|
+
old_by_layer = _rectangles_by_layer(old_buffer)
|
|
79
|
+
new_by_layer = _rectangles_by_layer(new_buffer)
|
|
80
|
+
layers = tuple(sorted(set(old_by_layer) | set(new_by_layer)))
|
|
81
|
+
components: list[_RectComponent] = []
|
|
82
|
+
dirty_rect_count = 0
|
|
83
|
+
for layer in layers:
|
|
84
|
+
old_rects = tuple(sorted(set(old_by_layer.get(layer, ()))))
|
|
85
|
+
new_rects = tuple(sorted(set(new_by_layer.get(layer, ()))))
|
|
86
|
+
dirty = tuple(sorted((set(old_rects) ^ set(new_rects))))
|
|
87
|
+
dirty_rect_count += len(dirty)
|
|
88
|
+
components.extend(_dirty_components(layer, old_rects, new_rects, dirty))
|
|
89
|
+
|
|
90
|
+
if not components:
|
|
91
|
+
return RectCoverageResult(
|
|
92
|
+
exact_result=ExactOracleResult(tuple(LayerExactDiff(layer=layer) for layer in layers)),
|
|
93
|
+
stats=RectCoverageStats(
|
|
94
|
+
component_count=0,
|
|
95
|
+
cuda_task_count=0,
|
|
96
|
+
old_rect_count=sum(len(rects) for rects in old_by_layer.values()),
|
|
97
|
+
new_rect_count=sum(len(rects) for rects in new_by_layer.values()),
|
|
98
|
+
dirty_rect_count=0,
|
|
99
|
+
max_old_rects_per_component=0,
|
|
100
|
+
max_new_rects_per_component=0,
|
|
101
|
+
transferred_bytes=0,
|
|
102
|
+
cuda_kernel_count=0,
|
|
103
|
+
h2d_upload_s=0.0,
|
|
104
|
+
gpu_xor_s=0.0,
|
|
105
|
+
gpu_record_emit_s=0.0,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
max_old = max(len(component.old_rects) for component in components)
|
|
110
|
+
max_new = max(len(component.new_rects) for component in components)
|
|
111
|
+
max_rects = max(max_old, max_new)
|
|
112
|
+
if max_rects > max_rects_per_component:
|
|
113
|
+
raise RectCoverageUnsupported(
|
|
114
|
+
f"rectangle coverage component exceeds CUDA rectangle limit: {max_rects} > {max_rects_per_component}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
from .backends.cuda_ctypes import CudaCtypesUnavailable, run_cuda_rect_set_exact_fragments
|
|
118
|
+
|
|
119
|
+
old_array = np.zeros((len(components), max_rects, 4), dtype=np.int64)
|
|
120
|
+
new_array = np.zeros((len(components), max_rects, 4), dtype=np.int64)
|
|
121
|
+
old_counts = np.zeros((len(components),), dtype=np.uint8)
|
|
122
|
+
new_counts = np.zeros((len(components),), dtype=np.uint8)
|
|
123
|
+
for row, component in enumerate(components):
|
|
124
|
+
old_counts[row] = len(component.old_rects)
|
|
125
|
+
new_counts[row] = len(component.new_rects)
|
|
126
|
+
for col, rect in enumerate(component.old_rects):
|
|
127
|
+
old_array[row, col] = rect
|
|
128
|
+
for col, rect in enumerate(component.new_rects):
|
|
129
|
+
new_array[row, col] = rect
|
|
130
|
+
|
|
131
|
+
transferred_bytes = int(old_array.nbytes + new_array.nbytes + old_counts.nbytes + new_counts.nbytes)
|
|
132
|
+
start = perf_counter()
|
|
133
|
+
try:
|
|
134
|
+
rects, counts, _areas = run_cuda_rect_set_exact_fragments(
|
|
135
|
+
old_array,
|
|
136
|
+
new_array,
|
|
137
|
+
old_counts,
|
|
138
|
+
new_counts,
|
|
139
|
+
max_fragments=max_fragments_per_component,
|
|
140
|
+
)
|
|
141
|
+
except CudaCtypesUnavailable as exc:
|
|
142
|
+
raise RectCoverageUnsupported(str(exc)) from exc
|
|
143
|
+
gpu_s = perf_counter() - start
|
|
144
|
+
|
|
145
|
+
diffs_by_layer: dict[LayerSpec, list[LayerExactDiff]] = {layer: [] for layer in layers}
|
|
146
|
+
emit_start = perf_counter()
|
|
147
|
+
for row, component in enumerate(components):
|
|
148
|
+
old_fragments = tuple(_fragment_from_rect(tuple(int(v) for v in rects[row, 0, index])) for index in range(int(counts[row, 0])))
|
|
149
|
+
new_fragments = tuple(_fragment_from_rect(tuple(int(v) for v in rects[row, 1, index])) for index in range(int(counts[row, 1])))
|
|
150
|
+
diffs_by_layer[component.layer].append(
|
|
151
|
+
LayerExactDiff(
|
|
152
|
+
layer=component.layer,
|
|
153
|
+
old_minus_new=old_fragments,
|
|
154
|
+
new_minus_old=new_fragments,
|
|
155
|
+
xor=old_fragments + new_fragments,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
emit_s = perf_counter() - emit_start
|
|
159
|
+
layer_diffs = tuple(_merge_layer_diffs(layer, diffs_by_layer[layer]) for layer in layers)
|
|
160
|
+
return RectCoverageResult(
|
|
161
|
+
exact_result=ExactOracleResult(layer_diffs),
|
|
162
|
+
stats=RectCoverageStats(
|
|
163
|
+
component_count=len(components),
|
|
164
|
+
cuda_task_count=len(components),
|
|
165
|
+
old_rect_count=sum(len(rects) for rects in old_by_layer.values()),
|
|
166
|
+
new_rect_count=sum(len(rects) for rects in new_by_layer.values()),
|
|
167
|
+
dirty_rect_count=dirty_rect_count,
|
|
168
|
+
max_old_rects_per_component=max_old,
|
|
169
|
+
max_new_rects_per_component=max_new,
|
|
170
|
+
transferred_bytes=transferred_bytes,
|
|
171
|
+
cuda_kernel_count=1,
|
|
172
|
+
h2d_upload_s=0.0,
|
|
173
|
+
gpu_xor_s=gpu_s,
|
|
174
|
+
gpu_record_emit_s=emit_s,
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_metal_rectangle_coverage_diff(
|
|
180
|
+
old_buffer: PolygonBuffer,
|
|
181
|
+
new_buffer: PolygonBuffer,
|
|
182
|
+
*,
|
|
183
|
+
max_rects_per_component: int = 16,
|
|
184
|
+
max_fragments_per_component: int = 4096,
|
|
185
|
+
) -> RectCoverageResult:
|
|
186
|
+
old_by_layer = _rectangles_by_layer(old_buffer)
|
|
187
|
+
new_by_layer = _rectangles_by_layer(new_buffer)
|
|
188
|
+
layers = tuple(sorted(set(old_by_layer) | set(new_by_layer)))
|
|
189
|
+
components: list[_RectComponent] = []
|
|
190
|
+
dirty_rect_count = 0
|
|
191
|
+
for layer in layers:
|
|
192
|
+
old_rects = tuple(sorted(set(old_by_layer.get(layer, ()))))
|
|
193
|
+
new_rects = tuple(sorted(set(new_by_layer.get(layer, ()))))
|
|
194
|
+
dirty = tuple(sorted((set(old_rects) ^ set(new_rects))))
|
|
195
|
+
dirty_rect_count += len(dirty)
|
|
196
|
+
components.extend(_dirty_components(layer, old_rects, new_rects, dirty))
|
|
197
|
+
|
|
198
|
+
if not components:
|
|
199
|
+
return RectCoverageResult(
|
|
200
|
+
exact_result=ExactOracleResult(tuple(LayerExactDiff(layer=layer) for layer in layers)),
|
|
201
|
+
stats=RectCoverageStats(
|
|
202
|
+
component_count=0,
|
|
203
|
+
cuda_task_count=0,
|
|
204
|
+
old_rect_count=sum(len(rects) for rects in old_by_layer.values()),
|
|
205
|
+
new_rect_count=sum(len(rects) for rects in new_by_layer.values()),
|
|
206
|
+
dirty_rect_count=0,
|
|
207
|
+
max_old_rects_per_component=0,
|
|
208
|
+
max_new_rects_per_component=0,
|
|
209
|
+
transferred_bytes=0,
|
|
210
|
+
cuda_kernel_count=0,
|
|
211
|
+
h2d_upload_s=0.0,
|
|
212
|
+
gpu_xor_s=0.0,
|
|
213
|
+
gpu_record_emit_s=0.0,
|
|
214
|
+
accelerator="metal",
|
|
215
|
+
metal_task_count=0,
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
max_old = max(len(component.old_rects) for component in components)
|
|
220
|
+
max_new = max(len(component.new_rects) for component in components)
|
|
221
|
+
max_rects = max(max_old, max_new)
|
|
222
|
+
if max_rects > max_rects_per_component:
|
|
223
|
+
raise RectCoverageUnsupported(
|
|
224
|
+
f"rectangle coverage component exceeds Metal rectangle limit: {max_rects} > {max_rects_per_component}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
from .backends.metal_ctypes import MetalCtypesUnavailable, run_metal_rect_set_exact_fragments
|
|
228
|
+
|
|
229
|
+
old_array = np.zeros((len(components), max_rects, 4), dtype=np.int64)
|
|
230
|
+
new_array = np.zeros((len(components), max_rects, 4), dtype=np.int64)
|
|
231
|
+
old_counts = np.zeros((len(components),), dtype=np.uint8)
|
|
232
|
+
new_counts = np.zeros((len(components),), dtype=np.uint8)
|
|
233
|
+
for row, component in enumerate(components):
|
|
234
|
+
old_counts[row] = len(component.old_rects)
|
|
235
|
+
new_counts[row] = len(component.new_rects)
|
|
236
|
+
for col, rect in enumerate(component.old_rects):
|
|
237
|
+
old_array[row, col] = rect
|
|
238
|
+
for col, rect in enumerate(component.new_rects):
|
|
239
|
+
new_array[row, col] = rect
|
|
240
|
+
|
|
241
|
+
transferred_bytes = int(old_array.nbytes + new_array.nbytes + old_counts.nbytes + new_counts.nbytes)
|
|
242
|
+
start = perf_counter()
|
|
243
|
+
try:
|
|
244
|
+
rects, counts, _areas = run_metal_rect_set_exact_fragments(
|
|
245
|
+
old_array,
|
|
246
|
+
new_array,
|
|
247
|
+
old_counts,
|
|
248
|
+
new_counts,
|
|
249
|
+
max_fragments=max_fragments_per_component,
|
|
250
|
+
)
|
|
251
|
+
except MetalCtypesUnavailable as exc:
|
|
252
|
+
raise RectCoverageUnsupported(str(exc)) from exc
|
|
253
|
+
gpu_s = perf_counter() - start
|
|
254
|
+
|
|
255
|
+
diffs_by_layer: dict[LayerSpec, list[LayerExactDiff]] = {layer: [] for layer in layers}
|
|
256
|
+
emit_start = perf_counter()
|
|
257
|
+
for row, component in enumerate(components):
|
|
258
|
+
old_fragments = tuple(_fragment_from_rect(tuple(int(v) for v in rects[row, 0, index])) for index in range(int(counts[row, 0])))
|
|
259
|
+
new_fragments = tuple(_fragment_from_rect(tuple(int(v) for v in rects[row, 1, index])) for index in range(int(counts[row, 1])))
|
|
260
|
+
diffs_by_layer[component.layer].append(
|
|
261
|
+
LayerExactDiff(
|
|
262
|
+
layer=component.layer,
|
|
263
|
+
old_minus_new=old_fragments,
|
|
264
|
+
new_minus_old=new_fragments,
|
|
265
|
+
xor=old_fragments + new_fragments,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
emit_s = perf_counter() - emit_start
|
|
269
|
+
layer_diffs = tuple(_merge_layer_diffs(layer, diffs_by_layer[layer]) for layer in layers)
|
|
270
|
+
return RectCoverageResult(
|
|
271
|
+
exact_result=ExactOracleResult(layer_diffs),
|
|
272
|
+
stats=RectCoverageStats(
|
|
273
|
+
component_count=len(components),
|
|
274
|
+
cuda_task_count=0,
|
|
275
|
+
old_rect_count=sum(len(rects) for rects in old_by_layer.values()),
|
|
276
|
+
new_rect_count=sum(len(rects) for rects in new_by_layer.values()),
|
|
277
|
+
dirty_rect_count=dirty_rect_count,
|
|
278
|
+
max_old_rects_per_component=max_old,
|
|
279
|
+
max_new_rects_per_component=max_new,
|
|
280
|
+
transferred_bytes=transferred_bytes,
|
|
281
|
+
cuda_kernel_count=0,
|
|
282
|
+
h2d_upload_s=0.0,
|
|
283
|
+
gpu_xor_s=gpu_s,
|
|
284
|
+
gpu_record_emit_s=emit_s,
|
|
285
|
+
accelerator="metal",
|
|
286
|
+
metal_task_count=len(components),
|
|
287
|
+
),
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _rectangles_by_layer(buffer: PolygonBuffer) -> dict[LayerSpec, tuple[Rect, ...]]:
|
|
292
|
+
by_layer: dict[LayerSpec, list[Rect]] = {}
|
|
293
|
+
for index, layer_id in enumerate(buffer.layer_ids):
|
|
294
|
+
if int(buffer.flags[index]) != 0:
|
|
295
|
+
continue
|
|
296
|
+
layer = buffer.layer_table.layers[int(layer_id)]
|
|
297
|
+
rect = _as_rect(tuple((int(x), int(y)) for x, y in buffer.polygon_vertices(index)))
|
|
298
|
+
if rect is None:
|
|
299
|
+
raise RectCoverageUnsupported("non-rectangle polygon present in rectangle coverage path")
|
|
300
|
+
by_layer.setdefault(layer, []).append(rect)
|
|
301
|
+
return {layer: tuple(rects) for layer, rects in by_layer.items()}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _as_rect(points: tuple[tuple[int, int], ...]) -> Rect | None:
|
|
305
|
+
if len(points) != 4:
|
|
306
|
+
return None
|
|
307
|
+
xs = sorted({point[0] for point in points})
|
|
308
|
+
ys = sorted({point[1] for point in points})
|
|
309
|
+
if len(xs) != 2 or len(ys) != 2:
|
|
310
|
+
return None
|
|
311
|
+
if set(points) != {(xs[0], ys[0]), (xs[0], ys[1]), (xs[1], ys[0]), (xs[1], ys[1])}:
|
|
312
|
+
return None
|
|
313
|
+
if xs[0] >= xs[1] or ys[0] >= ys[1]:
|
|
314
|
+
return None
|
|
315
|
+
return xs[0], ys[0], xs[1], ys[1]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _dirty_components(layer: LayerSpec, old_rects: tuple[Rect, ...], new_rects: tuple[Rect, ...], dirty: tuple[Rect, ...]) -> list[_RectComponent]:
|
|
319
|
+
if not dirty:
|
|
320
|
+
return []
|
|
321
|
+
candidates = tuple(sorted(set(old_rects) | set(new_rects)))
|
|
322
|
+
components: list[set[Rect]] = [{rect} for rect in dirty]
|
|
323
|
+
changed = True
|
|
324
|
+
while changed:
|
|
325
|
+
changed = False
|
|
326
|
+
for component in components:
|
|
327
|
+
bbox = _bbox_union(tuple(component))
|
|
328
|
+
for rect in candidates:
|
|
329
|
+
if rect not in component and _positive_overlap(bbox, rect):
|
|
330
|
+
component.add(rect)
|
|
331
|
+
changed = True
|
|
332
|
+
merged: list[set[Rect]] = []
|
|
333
|
+
while components:
|
|
334
|
+
component = components.pop(0)
|
|
335
|
+
merged_any = False
|
|
336
|
+
for other in merged:
|
|
337
|
+
if _positive_overlap(_bbox_union(tuple(component)), _bbox_union(tuple(other))):
|
|
338
|
+
other.update(component)
|
|
339
|
+
merged_any = True
|
|
340
|
+
changed = True
|
|
341
|
+
break
|
|
342
|
+
if not merged_any:
|
|
343
|
+
merged.append(component)
|
|
344
|
+
components = merged
|
|
345
|
+
out = []
|
|
346
|
+
old_set = set(old_rects)
|
|
347
|
+
new_set = set(new_rects)
|
|
348
|
+
for component in components:
|
|
349
|
+
old_component = tuple(sorted(rect for rect in component if rect in old_set))
|
|
350
|
+
new_component = tuple(sorted(rect for rect in component if rect in new_set))
|
|
351
|
+
out.append(_RectComponent(layer=layer, old_rects=old_component, new_rects=new_component, bbox=_bbox_union(tuple(component))))
|
|
352
|
+
return sorted(out, key=lambda item: (item.layer, item.bbox))
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _positive_overlap(left: Rect, right: Rect) -> bool:
|
|
356
|
+
return max(left[0], right[0]) < min(left[2], right[2]) and max(left[1], right[1]) < min(left[3], right[3])
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _bbox_union(rects: tuple[Rect, ...]) -> Rect:
|
|
360
|
+
return (
|
|
361
|
+
min(rect[0] for rect in rects),
|
|
362
|
+
min(rect[1] for rect in rects),
|
|
363
|
+
max(rect[2] for rect in rects),
|
|
364
|
+
max(rect[3] for rect in rects),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _fragment_from_rect(rect: Rect) -> DiffFragment:
|
|
369
|
+
x0, y0, x1, y1 = rect
|
|
370
|
+
points = ((x0, y0), (x1, y0), (x1, y1), (x0, y1))
|
|
371
|
+
return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _merge_layer_diffs(layer: LayerSpec, diffs: list[LayerExactDiff]) -> LayerExactDiff:
|
|
375
|
+
old_minus = tuple(fragment for diff in diffs for fragment in diff.old_minus_new)
|
|
376
|
+
new_minus = tuple(fragment for diff in diffs for fragment in diff.new_minus_old)
|
|
377
|
+
merged_old_minus = _merge_layer_side_fragments(tuple(sorted(old_minus, key=_fragment_sort_key)))
|
|
378
|
+
merged_new_minus = _merge_layer_side_fragments(tuple(sorted(new_minus, key=_fragment_sort_key)))
|
|
379
|
+
return LayerExactDiff(
|
|
380
|
+
layer=layer,
|
|
381
|
+
old_minus_new=merged_old_minus,
|
|
382
|
+
new_minus_old=merged_new_minus,
|
|
383
|
+
xor=tuple(sorted(merged_old_minus + merged_new_minus, key=_fragment_sort_key)),
|
|
384
|
+
)
|