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/spatial.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .config import LayerSpec
|
|
8
|
+
from .geometry import PolygonBuffer
|
|
9
|
+
|
|
10
|
+
BBox = tuple[int, int, int, int]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SpatialIndex:
|
|
15
|
+
entries_by_layer: dict[LayerSpec, tuple[tuple[BBox, int], ...]]
|
|
16
|
+
bbox_arrays_by_layer: dict[LayerSpec, np.ndarray] | None = None
|
|
17
|
+
index_arrays_by_layer: dict[LayerSpec, np.ndarray] | None = None
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_buffer(cls, buffer: PolygonBuffer) -> "SpatialIndex":
|
|
21
|
+
entries: dict[LayerSpec, list[tuple[BBox, int]]] = {}
|
|
22
|
+
for index in range(buffer.polygon_count):
|
|
23
|
+
layer = buffer.layer_table.layers[int(buffer.layer_ids[index])]
|
|
24
|
+
bbox = tuple(int(value) for value in buffer.bboxes[index])
|
|
25
|
+
entries.setdefault(layer, []).append((bbox, index))
|
|
26
|
+
sorted_entries = {layer: tuple(sorted(items, key=lambda item: (item[0], item[1]))) for layer, items in entries.items()}
|
|
27
|
+
bbox_arrays = {
|
|
28
|
+
layer: np.asarray([item[0] for item in items], dtype=np.int64).reshape((-1, 4))
|
|
29
|
+
for layer, items in sorted_entries.items()
|
|
30
|
+
}
|
|
31
|
+
index_arrays = {
|
|
32
|
+
layer: np.asarray([item[1] for item in items], dtype=np.int64)
|
|
33
|
+
for layer, items in sorted_entries.items()
|
|
34
|
+
}
|
|
35
|
+
return cls(sorted_entries, bbox_arrays_by_layer=bbox_arrays, index_arrays_by_layer=index_arrays)
|
|
36
|
+
|
|
37
|
+
def query(self, layer: LayerSpec, bbox: BBox) -> tuple[int, ...]:
|
|
38
|
+
if self.bbox_arrays_by_layer is not None and self.index_arrays_by_layer is not None:
|
|
39
|
+
bboxes = self.bbox_arrays_by_layer.get(layer)
|
|
40
|
+
indices = self.index_arrays_by_layer.get(layer)
|
|
41
|
+
if bboxes is None or indices is None or len(bboxes) == 0:
|
|
42
|
+
return ()
|
|
43
|
+
mask = (
|
|
44
|
+
(bboxes[:, 0] <= bbox[2])
|
|
45
|
+
& (bboxes[:, 2] >= bbox[0])
|
|
46
|
+
& (bboxes[:, 1] <= bbox[3])
|
|
47
|
+
& (bboxes[:, 3] >= bbox[1])
|
|
48
|
+
)
|
|
49
|
+
return tuple(int(index) for index in indices[np.nonzero(mask)[0]])
|
|
50
|
+
return tuple(index for candidate_bbox, index in self.entries_by_layer.get(layer, ()) if bboxes_intersect(candidate_bbox, bbox))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def bboxes_intersect(left: BBox, right: BBox) -> bool:
|
|
54
|
+
return left[0] <= right[2] and left[2] >= right[0] and left[1] <= right[3] and left[3] >= right[1]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bbox_union(bboxes: list[BBox] | tuple[BBox, ...]) -> BBox:
|
|
58
|
+
if not bboxes:
|
|
59
|
+
raise ValueError("bbox_union requires at least one bbox")
|
|
60
|
+
return (
|
|
61
|
+
min(bbox[0] for bbox in bboxes),
|
|
62
|
+
min(bbox[1] for bbox in bboxes),
|
|
63
|
+
max(bbox[2] for bbox in bboxes),
|
|
64
|
+
max(bbox[3] for bbox in bboxes),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def expand_bbox(bbox: BBox, amount: int) -> BBox:
|
|
69
|
+
if amount < 0:
|
|
70
|
+
raise ValueError("expansion amount must be nonnegative")
|
|
71
|
+
return bbox[0] - amount, bbox[1] - amount, bbox[2] + amount, bbox[3] + amount
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import struct
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StructuralGuardUnavailable(ValueError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CellStructure:
|
|
14
|
+
name: str
|
|
15
|
+
local_shape_count: int = 0
|
|
16
|
+
references: tuple[tuple[str, int], ...] = field(default_factory=tuple)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class StructuralSummary:
|
|
21
|
+
path: Path
|
|
22
|
+
selected_top: str
|
|
23
|
+
cell_count: int
|
|
24
|
+
flattened_shape_count: int
|
|
25
|
+
reachable_cell_count: int
|
|
26
|
+
|
|
27
|
+
def to_json(self) -> dict[str, object]:
|
|
28
|
+
return {
|
|
29
|
+
"path": str(self.path),
|
|
30
|
+
"selected_top": self.selected_top,
|
|
31
|
+
"cell_count": self.cell_count,
|
|
32
|
+
"flattened_shape_count": self.flattened_shape_count,
|
|
33
|
+
"reachable_cell_count": self.reachable_cell_count,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def summarize_gds_structure(path: str | Path, *, top_name: str | None = None) -> StructuralSummary:
|
|
38
|
+
path = Path(path)
|
|
39
|
+
cells = _parse_cells(path)
|
|
40
|
+
if not cells:
|
|
41
|
+
raise StructuralGuardUnavailable("no cells found")
|
|
42
|
+
top = _select_top(cells, top_name)
|
|
43
|
+
visiting: set[str] = set()
|
|
44
|
+
seen: set[str] = set()
|
|
45
|
+
count = _flattened_shape_count(top, cells, visiting, seen)
|
|
46
|
+
return StructuralSummary(
|
|
47
|
+
path=path,
|
|
48
|
+
selected_top=top,
|
|
49
|
+
cell_count=len(cells),
|
|
50
|
+
flattened_shape_count=count,
|
|
51
|
+
reachable_cell_count=len(seen),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _parse_cells(path: Path) -> dict[str, CellStructure]:
|
|
56
|
+
cells: dict[str, CellStructure] = {}
|
|
57
|
+
current_name: str | None = None
|
|
58
|
+
local_shape_count = 0
|
|
59
|
+
references: list[tuple[str, int]] = []
|
|
60
|
+
element_kind: int | None = None
|
|
61
|
+
pending_sname: str | None = None
|
|
62
|
+
pending_multiplier = 1
|
|
63
|
+
|
|
64
|
+
data = path.read_bytes()
|
|
65
|
+
offset = 0
|
|
66
|
+
size = len(data)
|
|
67
|
+
while offset + 4 <= size:
|
|
68
|
+
record_len, record_type, _data_type = struct.unpack_from(">HBB", data, offset)
|
|
69
|
+
if record_len < 4 or offset + record_len > size:
|
|
70
|
+
raise StructuralGuardUnavailable("invalid GDS record length")
|
|
71
|
+
payload = data[offset + 4 : offset + record_len]
|
|
72
|
+
offset += record_len
|
|
73
|
+
|
|
74
|
+
if record_type == 0x05: # BGNSTR
|
|
75
|
+
current_name = None
|
|
76
|
+
local_shape_count = 0
|
|
77
|
+
references = []
|
|
78
|
+
elif record_type == 0x06: # STRNAME
|
|
79
|
+
current_name = _decode_gds_string(payload)
|
|
80
|
+
elif record_type in (0x08, 0x09): # BOUNDARY, PATH
|
|
81
|
+
element_kind = record_type
|
|
82
|
+
elif record_type in (0x0A, 0x0B): # SREF, AREF
|
|
83
|
+
element_kind = record_type
|
|
84
|
+
pending_sname = None
|
|
85
|
+
pending_multiplier = 1
|
|
86
|
+
elif record_type == 0x12 and element_kind in (0x0A, 0x0B): # SNAME
|
|
87
|
+
pending_sname = _decode_gds_string(payload)
|
|
88
|
+
elif record_type == 0x13 and element_kind == 0x0B: # COLROW
|
|
89
|
+
if len(payload) < 4:
|
|
90
|
+
raise StructuralGuardUnavailable("invalid AREF COLROW record")
|
|
91
|
+
columns, rows = struct.unpack_from(">hh", payload, 0)
|
|
92
|
+
if columns <= 0 or rows <= 0:
|
|
93
|
+
raise StructuralGuardUnavailable("invalid AREF repetition")
|
|
94
|
+
pending_multiplier = int(columns) * int(rows)
|
|
95
|
+
elif record_type == 0x11: # ENDEL
|
|
96
|
+
if element_kind in (0x08, 0x09):
|
|
97
|
+
local_shape_count += 1
|
|
98
|
+
elif element_kind in (0x0A, 0x0B):
|
|
99
|
+
if pending_sname is None:
|
|
100
|
+
raise StructuralGuardUnavailable("reference missing SNAME")
|
|
101
|
+
references.append((pending_sname, pending_multiplier))
|
|
102
|
+
element_kind = None
|
|
103
|
+
pending_sname = None
|
|
104
|
+
pending_multiplier = 1
|
|
105
|
+
elif record_type == 0x07: # ENDSTR
|
|
106
|
+
if current_name is None:
|
|
107
|
+
raise StructuralGuardUnavailable("cell missing STRNAME")
|
|
108
|
+
cells[current_name] = CellStructure(
|
|
109
|
+
name=current_name,
|
|
110
|
+
local_shape_count=local_shape_count,
|
|
111
|
+
references=tuple(references),
|
|
112
|
+
)
|
|
113
|
+
current_name = None
|
|
114
|
+
local_shape_count = 0
|
|
115
|
+
references = []
|
|
116
|
+
|
|
117
|
+
if offset != size:
|
|
118
|
+
raise StructuralGuardUnavailable("trailing partial GDS record")
|
|
119
|
+
return cells
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _select_top(cells: dict[str, CellStructure], top_name: str | None) -> str:
|
|
123
|
+
if top_name is not None:
|
|
124
|
+
if top_name not in cells:
|
|
125
|
+
raise StructuralGuardUnavailable(f"top cell {top_name!r} not found")
|
|
126
|
+
return top_name
|
|
127
|
+
referenced = {child for cell in cells.values() for child, _multiplier in cell.references}
|
|
128
|
+
tops = sorted(set(cells) - referenced)
|
|
129
|
+
if len(tops) != 1:
|
|
130
|
+
raise StructuralGuardUnavailable(f"expected exactly one top cell, found {len(tops)}")
|
|
131
|
+
return tops[0]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _flattened_shape_count(
|
|
135
|
+
name: str,
|
|
136
|
+
cells: dict[str, CellStructure],
|
|
137
|
+
visiting: set[str],
|
|
138
|
+
seen: set[str],
|
|
139
|
+
memo: dict[str, int] | None = None,
|
|
140
|
+
) -> int:
|
|
141
|
+
memo = memo if memo is not None else {}
|
|
142
|
+
if name in memo:
|
|
143
|
+
seen.add(name)
|
|
144
|
+
return memo[name]
|
|
145
|
+
if name in visiting:
|
|
146
|
+
raise StructuralGuardUnavailable("cell reference cycle detected")
|
|
147
|
+
cell = cells.get(name)
|
|
148
|
+
if cell is None:
|
|
149
|
+
raise StructuralGuardUnavailable(f"unresolved reference to {name!r}")
|
|
150
|
+
visiting.add(name)
|
|
151
|
+
seen.add(name)
|
|
152
|
+
total = cell.local_shape_count
|
|
153
|
+
for child_name, multiplier in cell.references:
|
|
154
|
+
total += multiplier * _flattened_shape_count(child_name, cells, visiting, seen, memo)
|
|
155
|
+
visiting.remove(name)
|
|
156
|
+
memo[name] = total
|
|
157
|
+
return total
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _decode_gds_string(payload: bytes) -> str:
|
|
161
|
+
return payload.rstrip(b"\0").decode("ascii")
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from time import perf_counter
|
|
6
|
+
|
|
7
|
+
from .canonicalize import intern_polygons
|
|
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 build_record_delta_surplus_components
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class SurplusCoverageStats:
|
|
16
|
+
old_surplus_count: int
|
|
17
|
+
new_surplus_count: int
|
|
18
|
+
changed_layer_count: int
|
|
19
|
+
conservative_overlap_layer_count: int
|
|
20
|
+
component_count: int
|
|
21
|
+
overlap_component_count: int
|
|
22
|
+
max_fragments_per_component: int
|
|
23
|
+
max_vertices_per_component: int
|
|
24
|
+
coverage_exact_proven: bool
|
|
25
|
+
canonicalize_s: float
|
|
26
|
+
record_emit_s: float
|
|
27
|
+
|
|
28
|
+
def to_json(self) -> dict[str, object]:
|
|
29
|
+
return {
|
|
30
|
+
"old_surplus_count": self.old_surplus_count,
|
|
31
|
+
"new_surplus_count": self.new_surplus_count,
|
|
32
|
+
"changed_layer_count": self.changed_layer_count,
|
|
33
|
+
"conservative_overlap_layer_count": self.conservative_overlap_layer_count,
|
|
34
|
+
"component_count": self.component_count,
|
|
35
|
+
"overlap_component_count": self.overlap_component_count,
|
|
36
|
+
"max_fragments_per_component": self.max_fragments_per_component,
|
|
37
|
+
"max_vertices_per_component": self.max_vertices_per_component,
|
|
38
|
+
"coverage_exact_proven": self.coverage_exact_proven,
|
|
39
|
+
"canonicalize_s": self.canonicalize_s,
|
|
40
|
+
"record_emit_s": self.record_emit_s,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SurplusCoverageResult:
|
|
46
|
+
exact_result: ExactOracleResult
|
|
47
|
+
stats: SurplusCoverageStats
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_canonical_surplus_coverage_probe(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> SurplusCoverageResult:
|
|
51
|
+
"""Emit directional component records from canonical source-polygon surplus.
|
|
52
|
+
|
|
53
|
+
This is a coverage probe, not the final arbitrary-polygon Boolean engine. It is exact only
|
|
54
|
+
when the surplus polygons are conservatively proven independent by positive bbox overlap.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
canonicalize_start = perf_counter()
|
|
58
|
+
old_intern = intern_polygons(old_buffer)
|
|
59
|
+
new_intern = intern_polygons(new_buffer)
|
|
60
|
+
canonicalize_s = perf_counter() - canonicalize_start
|
|
61
|
+
|
|
62
|
+
old_by_key = _records_by_key(old_buffer, old_intern.records)
|
|
63
|
+
new_by_key = _records_by_key(new_buffer, new_intern.records)
|
|
64
|
+
|
|
65
|
+
emit_start = perf_counter()
|
|
66
|
+
old_surplus: dict[LayerSpec, list[DiffFragment]] = defaultdict(list)
|
|
67
|
+
new_surplus: dict[LayerSpec, list[DiffFragment]] = defaultdict(list)
|
|
68
|
+
for key in sorted(set(old_by_key) | set(new_by_key), key=_key_sort):
|
|
69
|
+
old_records = old_by_key.get(key, ())
|
|
70
|
+
new_records = new_by_key.get(key, ())
|
|
71
|
+
delta = len(old_records) - len(new_records)
|
|
72
|
+
if delta > 0:
|
|
73
|
+
layer = key[0]
|
|
74
|
+
old_surplus[layer].extend(_fragment_from_buffer(old_buffer, record.polygon_index) for record in old_records[:delta])
|
|
75
|
+
elif delta < 0:
|
|
76
|
+
layer = key[0]
|
|
77
|
+
new_surplus[layer].extend(_fragment_from_buffer(new_buffer, record.polygon_index) for record in new_records[: -delta])
|
|
78
|
+
|
|
79
|
+
layers = tuple(sorted(set(old_surplus) | set(new_surplus)))
|
|
80
|
+
layer_diffs = tuple(
|
|
81
|
+
LayerExactDiff(
|
|
82
|
+
layer=layer,
|
|
83
|
+
old_minus_new=tuple(sorted(old_surplus.get(layer, ()), key=_fragment_sort_key)),
|
|
84
|
+
new_minus_old=tuple(sorted(new_surplus.get(layer, ()), key=_fragment_sort_key)),
|
|
85
|
+
xor=tuple(
|
|
86
|
+
sorted(
|
|
87
|
+
tuple(old_surplus.get(layer, ())) + tuple(new_surplus.get(layer, ())),
|
|
88
|
+
key=_fragment_sort_key,
|
|
89
|
+
)
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
for layer in layers
|
|
93
|
+
)
|
|
94
|
+
overlap_layers = tuple(
|
|
95
|
+
layer
|
|
96
|
+
for layer in layers
|
|
97
|
+
if _has_positive_bbox_overlap(tuple(old_surplus.get(layer, ())) + tuple(new_surplus.get(layer, ())))
|
|
98
|
+
)
|
|
99
|
+
component_stats = _component_stats(old_surplus, new_surplus)
|
|
100
|
+
record_emit_s = perf_counter() - emit_start
|
|
101
|
+
return SurplusCoverageResult(
|
|
102
|
+
exact_result=ExactOracleResult(layer_diffs),
|
|
103
|
+
stats=SurplusCoverageStats(
|
|
104
|
+
old_surplus_count=sum(len(fragments) for fragments in old_surplus.values()),
|
|
105
|
+
new_surplus_count=sum(len(fragments) for fragments in new_surplus.values()),
|
|
106
|
+
changed_layer_count=len(layers),
|
|
107
|
+
conservative_overlap_layer_count=len(overlap_layers),
|
|
108
|
+
component_count=component_stats["component_count"],
|
|
109
|
+
overlap_component_count=component_stats["overlap_component_count"],
|
|
110
|
+
max_fragments_per_component=component_stats["max_fragments_per_component"],
|
|
111
|
+
max_vertices_per_component=component_stats["max_vertices_per_component"],
|
|
112
|
+
coverage_exact_proven=len(overlap_layers) == 0,
|
|
113
|
+
canonicalize_s=canonicalize_s,
|
|
114
|
+
record_emit_s=record_emit_s,
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def run_record_delta_surplus_coverage_probe(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer, record_delta) -> SurplusCoverageResult:
|
|
120
|
+
"""Emit source-surplus component records from GPU record-delta rows.
|
|
121
|
+
|
|
122
|
+
The CUDA record-delta path uses canonical orientation-insensitive hashes and returns a
|
|
123
|
+
representative source polygon index for each changed key. This removes the full CPU
|
|
124
|
+
canonicalization pass, but changed coverage is not marked exact until the true polygon
|
|
125
|
+
Boolean engine exists.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
emit_start = perf_counter()
|
|
129
|
+
component_result = build_record_delta_surplus_components(old_buffer, new_buffer, record_delta)
|
|
130
|
+
old_surplus: dict[LayerSpec, list[DiffFragment]] = defaultdict(list)
|
|
131
|
+
new_surplus: dict[LayerSpec, list[DiffFragment]] = defaultdict(list)
|
|
132
|
+
for component in component_result.components:
|
|
133
|
+
for polygon in component.polygons:
|
|
134
|
+
if polygon.side == "old":
|
|
135
|
+
old_surplus[component.layer].append(polygon.fragment)
|
|
136
|
+
else:
|
|
137
|
+
new_surplus[component.layer].append(polygon.fragment)
|
|
138
|
+
|
|
139
|
+
layers = tuple(sorted(set(old_surplus) | set(new_surplus)))
|
|
140
|
+
layer_diffs = tuple(
|
|
141
|
+
LayerExactDiff(
|
|
142
|
+
layer=layer,
|
|
143
|
+
old_minus_new=tuple(sorted(old_surplus.get(layer, ()), key=_fragment_sort_key)),
|
|
144
|
+
new_minus_old=tuple(sorted(new_surplus.get(layer, ()), key=_fragment_sort_key)),
|
|
145
|
+
xor=tuple(
|
|
146
|
+
sorted(
|
|
147
|
+
tuple(old_surplus.get(layer, ())) + tuple(new_surplus.get(layer, ())),
|
|
148
|
+
key=_fragment_sort_key,
|
|
149
|
+
)
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
for layer in layers
|
|
153
|
+
)
|
|
154
|
+
overlap_layers = tuple(
|
|
155
|
+
layer
|
|
156
|
+
for layer in layers
|
|
157
|
+
if _has_positive_bbox_overlap(tuple(old_surplus.get(layer, ())) + tuple(new_surplus.get(layer, ())))
|
|
158
|
+
)
|
|
159
|
+
record_emit_s = perf_counter() - emit_start
|
|
160
|
+
changed_count = sum(len(fragments) for fragments in old_surplus.values()) + sum(
|
|
161
|
+
len(fragments) for fragments in new_surplus.values()
|
|
162
|
+
)
|
|
163
|
+
return SurplusCoverageResult(
|
|
164
|
+
exact_result=ExactOracleResult(layer_diffs),
|
|
165
|
+
stats=SurplusCoverageStats(
|
|
166
|
+
old_surplus_count=sum(len(fragments) for fragments in old_surplus.values()),
|
|
167
|
+
new_surplus_count=sum(len(fragments) for fragments in new_surplus.values()),
|
|
168
|
+
changed_layer_count=len(layers),
|
|
169
|
+
conservative_overlap_layer_count=len(overlap_layers),
|
|
170
|
+
component_count=component_result.stats.component_count,
|
|
171
|
+
overlap_component_count=component_result.stats.overlap_component_count,
|
|
172
|
+
max_fragments_per_component=component_result.stats.max_fragments_per_component,
|
|
173
|
+
max_vertices_per_component=component_result.stats.max_vertices_per_component,
|
|
174
|
+
coverage_exact_proven=changed_count == 0,
|
|
175
|
+
canonicalize_s=0.0,
|
|
176
|
+
record_emit_s=record_emit_s,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
def _records_by_key(buffer: PolygonBuffer, records) -> dict[tuple[LayerSpec, bytes], tuple[object, ...]]:
|
|
180
|
+
by_key: dict[tuple[LayerSpec, bytes], list[object]] = defaultdict(list)
|
|
181
|
+
for record in records:
|
|
182
|
+
layer = buffer.layer_table.layers[int(buffer.layer_ids[record.polygon_index])]
|
|
183
|
+
by_key[(layer, record.blob)].append(record)
|
|
184
|
+
return {key: tuple(records) for key, records in by_key.items()}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _fragment_from_buffer(buffer: PolygonBuffer, index: int) -> DiffFragment:
|
|
188
|
+
points = tuple((int(x), int(y)) for x, y in buffer.polygon_vertices(index))
|
|
189
|
+
return DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points))
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _has_positive_bbox_overlap(fragments: tuple[DiffFragment, ...]) -> bool:
|
|
193
|
+
for left_index, left in enumerate(fragments):
|
|
194
|
+
for right in fragments[left_index + 1 :]:
|
|
195
|
+
if _positive_overlap(left.bbox, right.bbox):
|
|
196
|
+
return True
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _component_stats(
|
|
201
|
+
old_surplus: dict[LayerSpec, list[DiffFragment]],
|
|
202
|
+
new_surplus: dict[LayerSpec, list[DiffFragment]],
|
|
203
|
+
) -> dict[str, int]:
|
|
204
|
+
component_count = 0
|
|
205
|
+
overlap_component_count = 0
|
|
206
|
+
max_fragments = 0
|
|
207
|
+
max_vertices = 0
|
|
208
|
+
for layer in sorted(set(old_surplus) | set(new_surplus)):
|
|
209
|
+
fragments = tuple(old_surplus.get(layer, ())) + tuple(new_surplus.get(layer, ()))
|
|
210
|
+
if not fragments:
|
|
211
|
+
continue
|
|
212
|
+
components = _fragment_components(fragments)
|
|
213
|
+
component_count += len(components)
|
|
214
|
+
for component in components:
|
|
215
|
+
max_fragments = max(max_fragments, len(component))
|
|
216
|
+
max_vertices = max(max_vertices, sum(len(fragments[index].points) for index in component))
|
|
217
|
+
if _has_positive_bbox_overlap(tuple(fragments[index] for index in component)):
|
|
218
|
+
overlap_component_count += 1
|
|
219
|
+
return {
|
|
220
|
+
"component_count": component_count,
|
|
221
|
+
"overlap_component_count": overlap_component_count,
|
|
222
|
+
"max_fragments_per_component": max_fragments,
|
|
223
|
+
"max_vertices_per_component": max_vertices,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _fragment_components(fragments: tuple[DiffFragment, ...]) -> tuple[tuple[int, ...], ...]:
|
|
228
|
+
remaining = set(range(len(fragments)))
|
|
229
|
+
components: list[tuple[int, ...]] = []
|
|
230
|
+
while remaining:
|
|
231
|
+
seed = remaining.pop()
|
|
232
|
+
component = {seed}
|
|
233
|
+
stack = [seed]
|
|
234
|
+
while stack:
|
|
235
|
+
left = stack.pop()
|
|
236
|
+
touched = [right for right in remaining if _positive_overlap(fragments[left].bbox, fragments[right].bbox)]
|
|
237
|
+
for right in touched:
|
|
238
|
+
remaining.remove(right)
|
|
239
|
+
component.add(right)
|
|
240
|
+
stack.append(right)
|
|
241
|
+
components.append(tuple(sorted(component)))
|
|
242
|
+
return tuple(sorted(components))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _positive_overlap(left: tuple[int, int, int, int], right: tuple[int, int, int, int]) -> bool:
|
|
246
|
+
return max(left[0], right[0]) < min(left[2], right[2]) and max(left[1], right[1]) < min(left[3], right[3])
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _key_sort(key: tuple[LayerSpec, bytes]) -> tuple[int, int, bytes]:
|
|
250
|
+
layer, blob = key
|
|
251
|
+
return layer.layer, layer.datatype, blob
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _fragment_sort_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, tuple[tuple[int, int], ...]]:
|
|
255
|
+
return fragment.bbox, fragment.twice_area, fragment.points
|