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/tiling.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from .config import LayerSpec
|
|
6
|
+
from .geometry import PolygonBuffer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
OLD_SIDE = 1
|
|
10
|
+
NEW_SIDE = 2
|
|
11
|
+
BOTH_SIDES = OLD_SIDE | NEW_SIDE
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, order=True)
|
|
15
|
+
class TileKey:
|
|
16
|
+
layer: LayerSpec
|
|
17
|
+
tx: int
|
|
18
|
+
ty: int
|
|
19
|
+
|
|
20
|
+
def to_json(self) -> dict[str, int]:
|
|
21
|
+
return {
|
|
22
|
+
"layer": self.layer.layer,
|
|
23
|
+
"datatype": self.layer.datatype,
|
|
24
|
+
"tx": self.tx,
|
|
25
|
+
"ty": self.ty,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class TileConfig:
|
|
31
|
+
tile_size: int
|
|
32
|
+
max_tiles_per_polygon: int = 4096
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if self.tile_size <= 0:
|
|
36
|
+
raise ValueError("tile_size must be positive")
|
|
37
|
+
if self.max_tiles_per_polygon <= 0:
|
|
38
|
+
raise ValueError("max_tiles_per_polygon must be positive")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class OversizedPolygon:
|
|
43
|
+
layer: LayerSpec
|
|
44
|
+
canonical_id: int
|
|
45
|
+
side_mask: int
|
|
46
|
+
tile_bbox: tuple[int, int, int, int]
|
|
47
|
+
|
|
48
|
+
def to_json(self) -> dict[str, object]:
|
|
49
|
+
return {
|
|
50
|
+
"layer": self.layer.to_json(),
|
|
51
|
+
"canonical_id": self.canonical_id,
|
|
52
|
+
"side_mask": self.side_mask,
|
|
53
|
+
"tile_bbox": list(self.tile_bbox),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class CpuTilePrefilterResult:
|
|
59
|
+
candidate_tiles: tuple[TileKey, ...]
|
|
60
|
+
oversized_candidates: tuple[OversizedPolygon, ...] = field(default_factory=tuple)
|
|
61
|
+
assignment_count: int = 0
|
|
62
|
+
collapsed_pair_count: int = 0
|
|
63
|
+
oversized_matched_count: int = 0
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def candidate_tile_count(self) -> int:
|
|
67
|
+
return len(self.candidate_tiles)
|
|
68
|
+
|
|
69
|
+
def to_json(self) -> dict[str, object]:
|
|
70
|
+
return {
|
|
71
|
+
"candidate_tiles": [tile.to_json() for tile in self.candidate_tiles],
|
|
72
|
+
"candidate_tile_count": len(self.candidate_tiles),
|
|
73
|
+
"oversized_candidates": [candidate.to_json() for candidate in self.oversized_candidates],
|
|
74
|
+
"assignment_count": self.assignment_count,
|
|
75
|
+
"collapsed_pair_count": self.collapsed_pair_count,
|
|
76
|
+
"oversized_matched_count": self.oversized_matched_count,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def floor_div(value: int, divisor: int) -> int:
|
|
81
|
+
if divisor <= 0:
|
|
82
|
+
raise ValueError("divisor must be positive")
|
|
83
|
+
return value // divisor
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def bbox_tile_range(bbox: tuple[int, int, int, int], tile_size: int) -> tuple[int, int, int, int]:
|
|
87
|
+
if tile_size <= 0:
|
|
88
|
+
raise ValueError("tile_size must be positive")
|
|
89
|
+
x0, y0, x1, y1 = bbox
|
|
90
|
+
return (
|
|
91
|
+
floor_div(x0, tile_size),
|
|
92
|
+
floor_div(y0, tile_size),
|
|
93
|
+
floor_div(x1, tile_size),
|
|
94
|
+
floor_div(y1, tile_size),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_cpu_tile_prefilter(
|
|
99
|
+
old_buffer: PolygonBuffer,
|
|
100
|
+
old_canonical_ids,
|
|
101
|
+
new_buffer: PolygonBuffer,
|
|
102
|
+
new_canonical_ids,
|
|
103
|
+
config: TileConfig,
|
|
104
|
+
) -> CpuTilePrefilterResult:
|
|
105
|
+
pair_side_masks: dict[tuple[TileKey, int], int] = {}
|
|
106
|
+
oversized_side_masks: dict[tuple[LayerSpec, int, tuple[int, int, int, int]], int] = {}
|
|
107
|
+
assignment_count = 0
|
|
108
|
+
oversized_matched_count = 0
|
|
109
|
+
|
|
110
|
+
for buffer, ids, side in ((old_buffer, old_canonical_ids, OLD_SIDE), (new_buffer, new_canonical_ids, NEW_SIDE)):
|
|
111
|
+
if len(ids) != buffer.polygon_count:
|
|
112
|
+
raise ValueError("canonical id vector length must match polygon count")
|
|
113
|
+
for index in range(buffer.polygon_count):
|
|
114
|
+
layer = buffer.layer_table.layers[int(buffer.layer_ids[index])]
|
|
115
|
+
canonical_id = int(ids[index])
|
|
116
|
+
tile_bbox = bbox_tile_range(tuple(int(value) for value in buffer.bboxes[index]), config.tile_size)
|
|
117
|
+
tx0, ty0, tx1, ty1 = tile_bbox
|
|
118
|
+
span_x = tx1 - tx0 + 1
|
|
119
|
+
span_y = ty1 - ty0 + 1
|
|
120
|
+
if span_x <= 0 or span_y <= 0:
|
|
121
|
+
raise ValueError("invalid tile span")
|
|
122
|
+
count = span_x * span_y
|
|
123
|
+
if count > config.max_tiles_per_polygon:
|
|
124
|
+
key = (layer, canonical_id, tile_bbox)
|
|
125
|
+
oversized_side_masks[key] = oversized_side_masks.get(key, 0) | side
|
|
126
|
+
continue
|
|
127
|
+
for tx in range(tx0, tx1 + 1):
|
|
128
|
+
for ty in range(ty0, ty1 + 1):
|
|
129
|
+
key = (TileKey(layer, tx, ty), canonical_id)
|
|
130
|
+
pair_side_masks[key] = pair_side_masks.get(key, 0) | side
|
|
131
|
+
assignment_count += 1
|
|
132
|
+
|
|
133
|
+
candidate_tiles = {
|
|
134
|
+
tile_key
|
|
135
|
+
for (tile_key, _canonical_id), side_mask in pair_side_masks.items()
|
|
136
|
+
if side_mask != BOTH_SIDES
|
|
137
|
+
}
|
|
138
|
+
oversized_candidates = []
|
|
139
|
+
for (layer, canonical_id, tile_bbox), side_mask in oversized_side_masks.items():
|
|
140
|
+
if side_mask == BOTH_SIDES:
|
|
141
|
+
oversized_matched_count += 1
|
|
142
|
+
else:
|
|
143
|
+
oversized_candidates.append(
|
|
144
|
+
OversizedPolygon(layer=layer, canonical_id=canonical_id, side_mask=side_mask, tile_bbox=tile_bbox)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return CpuTilePrefilterResult(
|
|
148
|
+
candidate_tiles=tuple(sorted(candidate_tiles)),
|
|
149
|
+
oversized_candidates=tuple(sorted(oversized_candidates, key=lambda item: (item.layer, item.canonical_id, item.tile_bbox))),
|
|
150
|
+
assignment_count=assignment_count,
|
|
151
|
+
collapsed_pair_count=len(pair_side_masks),
|
|
152
|
+
oversized_matched_count=oversized_matched_count,
|
|
153
|
+
)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
import gdstk
|
|
7
|
+
|
|
8
|
+
from .config import LayerSpec
|
|
9
|
+
from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff, run_full_exact_oracle
|
|
10
|
+
from .geometry import PolygonBuffer, polygon_bbox, polygon_twice_area
|
|
11
|
+
from .grid import snap_to_grid
|
|
12
|
+
from .multiprocessing_support import fork_context
|
|
13
|
+
from .spatial import BBox, SpatialIndex
|
|
14
|
+
from .windows import CandidateWindow
|
|
15
|
+
|
|
16
|
+
_WORKER_OLD_BUFFER = None
|
|
17
|
+
_WORKER_NEW_BUFFER = None
|
|
18
|
+
_WORKER_PRECISION = 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class WindowedExactFailure:
|
|
23
|
+
window: CandidateWindow
|
|
24
|
+
reason: str
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> dict[str, object]:
|
|
27
|
+
return {"window": self.window.to_json(), "reason": self.reason}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class WindowedExactResult:
|
|
32
|
+
layers: tuple[LayerExactDiff, ...]
|
|
33
|
+
escalated_layers: tuple[LayerSpec, ...] = field(default_factory=tuple)
|
|
34
|
+
failures: tuple[WindowedExactFailure, ...] = field(default_factory=tuple)
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def equivalent(self) -> bool:
|
|
38
|
+
return all(layer.xor_twice_area == 0 for layer in self.layers)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def exact_result(self) -> ExactOracleResult:
|
|
42
|
+
return ExactOracleResult(self.layers)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_windowed_exact_oracle(
|
|
46
|
+
old_buffer: PolygonBuffer,
|
|
47
|
+
new_buffer: PolygonBuffer,
|
|
48
|
+
windows: tuple[CandidateWindow, ...] | list[CandidateWindow],
|
|
49
|
+
*,
|
|
50
|
+
old_index: SpatialIndex | None = None,
|
|
51
|
+
new_index: SpatialIndex | None = None,
|
|
52
|
+
precision: float = 1.0,
|
|
53
|
+
max_polygons_per_window: int = 200_000,
|
|
54
|
+
) -> WindowedExactResult:
|
|
55
|
+
old_index = old_index or SpatialIndex.from_buffer(old_buffer)
|
|
56
|
+
new_index = new_index or SpatialIndex.from_buffer(new_buffer)
|
|
57
|
+
|
|
58
|
+
layers_from_windows = tuple(sorted({window.layer for window in windows}))
|
|
59
|
+
direction_points: dict[LayerSpec, dict[str, list[tuple[tuple[int, int], ...]]]] = {
|
|
60
|
+
layer: {"old_minus_new": [], "new_minus_old": []} for layer in layers_from_windows
|
|
61
|
+
}
|
|
62
|
+
escalated_layers: set[LayerSpec] = set()
|
|
63
|
+
failures: list[WindowedExactFailure] = []
|
|
64
|
+
window_tasks = []
|
|
65
|
+
|
|
66
|
+
for window in windows:
|
|
67
|
+
old_indices = old_index.query(window.layer, window.verification_bbox)
|
|
68
|
+
new_indices = new_index.query(window.layer, window.verification_bbox)
|
|
69
|
+
window_tasks.append((window, old_indices, new_indices, max_polygons_per_window))
|
|
70
|
+
|
|
71
|
+
task_results = _run_window_tasks(old_buffer, new_buffer, window_tasks, precision=precision)
|
|
72
|
+
for window, old_minus, new_minus, failure_reason in task_results:
|
|
73
|
+
if failure_reason is not None:
|
|
74
|
+
escalated_layers.add(window.layer)
|
|
75
|
+
failures.append(WindowedExactFailure(window, failure_reason))
|
|
76
|
+
|
|
77
|
+
for window, old_minus, new_minus, failure_reason in task_results:
|
|
78
|
+
if failure_reason is not None or window.layer in escalated_layers:
|
|
79
|
+
continue
|
|
80
|
+
direction_points[window.layer]["old_minus_new"].extend(old_minus)
|
|
81
|
+
direction_points[window.layer]["new_minus_old"].extend(new_minus)
|
|
82
|
+
|
|
83
|
+
full_layer_diffs = _full_diffs_by_layer(old_buffer, new_buffer, precision=precision) if escalated_layers else {}
|
|
84
|
+
layer_diffs: list[LayerExactDiff] = []
|
|
85
|
+
for layer in layers_from_windows:
|
|
86
|
+
if layer in escalated_layers:
|
|
87
|
+
layer_diffs.append(
|
|
88
|
+
full_layer_diffs.get(
|
|
89
|
+
layer,
|
|
90
|
+
LayerExactDiff(layer=layer),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
continue
|
|
94
|
+
old_minus = _union_fragments(direction_points[layer]["old_minus_new"], layer, precision=precision)
|
|
95
|
+
new_minus = _union_fragments(direction_points[layer]["new_minus_old"], layer, precision=precision)
|
|
96
|
+
xor = _union_fragments([fragment.points for fragment in old_minus] + [fragment.points for fragment in new_minus], layer, precision=precision)
|
|
97
|
+
layer_diffs.append(
|
|
98
|
+
LayerExactDiff(
|
|
99
|
+
layer=layer,
|
|
100
|
+
old_minus_new=old_minus,
|
|
101
|
+
new_minus_old=new_minus,
|
|
102
|
+
xor=xor,
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return WindowedExactResult(
|
|
107
|
+
layers=tuple(sorted(layer_diffs, key=lambda diff: diff.layer)),
|
|
108
|
+
escalated_layers=tuple(sorted(escalated_layers)),
|
|
109
|
+
failures=tuple(failures),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_window_tasks(
|
|
114
|
+
old_buffer: PolygonBuffer,
|
|
115
|
+
new_buffer: PolygonBuffer,
|
|
116
|
+
window_tasks,
|
|
117
|
+
*,
|
|
118
|
+
precision: float,
|
|
119
|
+
):
|
|
120
|
+
if not window_tasks:
|
|
121
|
+
return ()
|
|
122
|
+
worker_count = _resolve_window_worker_count(len(window_tasks))
|
|
123
|
+
if worker_count <= 1:
|
|
124
|
+
return tuple(_compute_window_task((old_buffer, new_buffer, precision, *task)) for task in window_tasks)
|
|
125
|
+
context = fork_context()
|
|
126
|
+
if context is None:
|
|
127
|
+
return tuple(_compute_window_task((old_buffer, new_buffer, precision, *task)) for task in window_tasks)
|
|
128
|
+
|
|
129
|
+
global _WORKER_OLD_BUFFER, _WORKER_NEW_BUFFER, _WORKER_PRECISION
|
|
130
|
+
_WORKER_OLD_BUFFER = old_buffer
|
|
131
|
+
_WORKER_NEW_BUFFER = new_buffer
|
|
132
|
+
_WORKER_PRECISION = precision
|
|
133
|
+
try:
|
|
134
|
+
try:
|
|
135
|
+
with context.Pool(processes=worker_count) as pool:
|
|
136
|
+
return tuple(pool.map(_compute_window_task_worker, window_tasks, chunksize=1))
|
|
137
|
+
except OSError:
|
|
138
|
+
return tuple(_compute_window_task((old_buffer, new_buffer, precision, *task)) for task in window_tasks)
|
|
139
|
+
finally:
|
|
140
|
+
_WORKER_OLD_BUFFER = None
|
|
141
|
+
_WORKER_NEW_BUFFER = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _resolve_window_worker_count(window_count: int) -> int:
|
|
145
|
+
if window_count <= 1:
|
|
146
|
+
return 1
|
|
147
|
+
return max(1, min(os.cpu_count() or 1, window_count))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _compute_window_task_worker(task):
|
|
151
|
+
if _WORKER_OLD_BUFFER is None or _WORKER_NEW_BUFFER is None:
|
|
152
|
+
raise RuntimeError("windowed exact worker globals are not initialized")
|
|
153
|
+
return _compute_window_task((_WORKER_OLD_BUFFER, _WORKER_NEW_BUFFER, _WORKER_PRECISION, *task))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _compute_window_task(task):
|
|
157
|
+
old_buffer, new_buffer, precision, window, old_indices, new_indices, max_polygons_per_window = task
|
|
158
|
+
if len(old_indices) + len(new_indices) > max_polygons_per_window:
|
|
159
|
+
return window, (), (), "window polygon threshold exceeded"
|
|
160
|
+
try:
|
|
161
|
+
old_minus, new_minus = _compute_window_directional_fragments(
|
|
162
|
+
old_buffer,
|
|
163
|
+
old_indices,
|
|
164
|
+
new_buffer,
|
|
165
|
+
new_indices,
|
|
166
|
+
window,
|
|
167
|
+
precision=precision,
|
|
168
|
+
)
|
|
169
|
+
except (RuntimeError, ValueError, TypeError) as exc:
|
|
170
|
+
return window, (), (), f"gdstk boolean failed: {exc}"
|
|
171
|
+
return window, old_minus, new_minus, None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _compute_window_directional_fragments(
|
|
175
|
+
old_buffer: PolygonBuffer,
|
|
176
|
+
old_indices: tuple[int, ...],
|
|
177
|
+
new_buffer: PolygonBuffer,
|
|
178
|
+
new_indices: tuple[int, ...],
|
|
179
|
+
window: CandidateWindow,
|
|
180
|
+
*,
|
|
181
|
+
precision: float,
|
|
182
|
+
) -> tuple[tuple[tuple[tuple[int, int], ...], ...], tuple[tuple[tuple[int, int], ...], ...]]:
|
|
183
|
+
origin = window.verification_bbox[0], window.verification_bbox[1]
|
|
184
|
+
old_polys = _buffer_indices_to_gdstk(old_buffer, old_indices, window.layer, origin)
|
|
185
|
+
new_polys = _buffer_indices_to_gdstk(new_buffer, new_indices, window.layer, origin)
|
|
186
|
+
verification_rect = _bbox_rectangle(window.verification_bbox, window.layer, origin)
|
|
187
|
+
core_rect = _bbox_rectangle(window.core_bbox, window.layer, origin)
|
|
188
|
+
|
|
189
|
+
old_clipped = gdstk.boolean(old_polys, [verification_rect], "and", precision=precision) or []
|
|
190
|
+
new_clipped = gdstk.boolean(new_polys, [verification_rect], "and", precision=precision) or []
|
|
191
|
+
old_minus = gdstk.boolean(old_clipped, new_clipped, "not", precision=precision) or []
|
|
192
|
+
new_minus = gdstk.boolean(new_clipped, old_clipped, "not", precision=precision) or []
|
|
193
|
+
old_core = gdstk.boolean(old_minus, [core_rect], "and", precision=precision) or []
|
|
194
|
+
new_core = gdstk.boolean(new_minus, [core_rect], "and", precision=precision) or []
|
|
195
|
+
return _global_points_from_gdstk(old_core, origin), _global_points_from_gdstk(new_core, origin)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _buffer_indices_to_gdstk(
|
|
199
|
+
buffer: PolygonBuffer,
|
|
200
|
+
indices: tuple[int, ...],
|
|
201
|
+
layer: LayerSpec,
|
|
202
|
+
origin: tuple[int, int],
|
|
203
|
+
) -> list[gdstk.Polygon]:
|
|
204
|
+
ox, oy = origin
|
|
205
|
+
polygons = []
|
|
206
|
+
for index in indices:
|
|
207
|
+
points = tuple((int(x) - ox, int(y) - oy) for x, y in buffer.polygon_vertices(index))
|
|
208
|
+
if len(points) >= 3:
|
|
209
|
+
polygons.append(gdstk.Polygon(points, layer=layer.layer, datatype=layer.datatype))
|
|
210
|
+
return polygons
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _bbox_rectangle(bbox: BBox, layer: LayerSpec, origin: tuple[int, int]) -> gdstk.Polygon:
|
|
214
|
+
ox, oy = origin
|
|
215
|
+
x0, y0, x1, y1 = bbox
|
|
216
|
+
return gdstk.rectangle(
|
|
217
|
+
(x0 - ox, y0 - oy),
|
|
218
|
+
(x1 + 1 - ox, y1 + 1 - oy),
|
|
219
|
+
layer=layer.layer,
|
|
220
|
+
datatype=layer.datatype,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _global_points_from_gdstk(polygons: list[gdstk.Polygon], origin: tuple[int, int]) -> tuple[tuple[tuple[int, int], ...], ...]:
|
|
225
|
+
ox, oy = origin
|
|
226
|
+
result = []
|
|
227
|
+
for polygon in polygons:
|
|
228
|
+
points = tuple((snap_to_grid(point[0], 1) + ox, snap_to_grid(point[1], 1) + oy) for point in polygon.points)
|
|
229
|
+
if len(points) >= 3:
|
|
230
|
+
result.append(points)
|
|
231
|
+
return tuple(result)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _union_fragments(
|
|
235
|
+
polygons: list[tuple[tuple[int, int], ...]],
|
|
236
|
+
layer: LayerSpec,
|
|
237
|
+
*,
|
|
238
|
+
precision: float,
|
|
239
|
+
) -> tuple[DiffFragment, ...]:
|
|
240
|
+
if not polygons:
|
|
241
|
+
return ()
|
|
242
|
+
origin = _points_origin(polygons)
|
|
243
|
+
ox, oy = origin
|
|
244
|
+
local = [gdstk.Polygon([(x - ox, y - oy) for x, y in points], layer=layer.layer, datatype=layer.datatype) for points in polygons]
|
|
245
|
+
merged = gdstk.boolean(local, [], "or", precision=precision) or []
|
|
246
|
+
return _fragments_from_gdstk(merged, origin)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _fragments_from_gdstk(polygons: list[gdstk.Polygon], origin: tuple[int, int]) -> tuple[DiffFragment, ...]:
|
|
250
|
+
ox, oy = origin
|
|
251
|
+
fragments = []
|
|
252
|
+
for polygon in polygons:
|
|
253
|
+
points = tuple((snap_to_grid(point[0], 1) + ox, snap_to_grid(point[1], 1) + oy) for point in polygon.points)
|
|
254
|
+
if len(points) < 3:
|
|
255
|
+
continue
|
|
256
|
+
fragments.append(DiffFragment(points=points, bbox=polygon_bbox(points), twice_area=polygon_twice_area(points)))
|
|
257
|
+
return tuple(sorted(fragments, key=lambda fragment: (fragment.bbox, fragment.points)))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _points_origin(polygons: list[tuple[tuple[int, int], ...]]) -> tuple[int, int]:
|
|
261
|
+
return min(point[0] for polygon in polygons for point in polygon), min(point[1] for polygon in polygons for point in polygon)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _full_diffs_by_layer(
|
|
265
|
+
old_buffer: PolygonBuffer,
|
|
266
|
+
new_buffer: PolygonBuffer,
|
|
267
|
+
*,
|
|
268
|
+
precision: float,
|
|
269
|
+
) -> dict[LayerSpec, LayerExactDiff]:
|
|
270
|
+
return {layer_diff.layer: layer_diff for layer_diff in run_full_exact_oracle(old_buffer, new_buffer, precision=precision).layers}
|
gdsdiff/windows.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .config import LayerSpec
|
|
6
|
+
from .spatial import BBox, SpatialIndex, expand_bbox
|
|
7
|
+
from .tiling import CpuTilePrefilterResult, TileConfig, TileKey
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class CandidateWindow:
|
|
12
|
+
layer: LayerSpec
|
|
13
|
+
core_bbox: BBox
|
|
14
|
+
verification_bbox: BBox
|
|
15
|
+
source_candidate_count: int
|
|
16
|
+
estimated_old_polygons: int
|
|
17
|
+
estimated_new_polygons: int
|
|
18
|
+
reason: str = "tile mismatch"
|
|
19
|
+
|
|
20
|
+
def to_json(self) -> dict[str, object]:
|
|
21
|
+
return {
|
|
22
|
+
"layer": self.layer.to_json(),
|
|
23
|
+
"core_bbox": list(self.core_bbox),
|
|
24
|
+
"verification_bbox": list(self.verification_bbox),
|
|
25
|
+
"source_candidate_count": self.source_candidate_count,
|
|
26
|
+
"estimated_old_polygons": self.estimated_old_polygons,
|
|
27
|
+
"estimated_new_polygons": self.estimated_new_polygons,
|
|
28
|
+
"reason": self.reason,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class WindowConfig:
|
|
34
|
+
halo_dbu: int = 2
|
|
35
|
+
max_tiles_per_window: int = 4096
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
if self.halo_dbu < 0:
|
|
39
|
+
raise ValueError("halo_dbu must be nonnegative")
|
|
40
|
+
if self.max_tiles_per_window <= 0:
|
|
41
|
+
raise ValueError("max_tiles_per_window must be positive")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_candidate_windows(
|
|
45
|
+
prefilter: CpuTilePrefilterResult,
|
|
46
|
+
tile_config: TileConfig,
|
|
47
|
+
old_index: SpatialIndex,
|
|
48
|
+
new_index: SpatialIndex,
|
|
49
|
+
window_config: WindowConfig | None = None,
|
|
50
|
+
) -> tuple[CandidateWindow, ...]:
|
|
51
|
+
window_config = window_config or WindowConfig()
|
|
52
|
+
windows: list[CandidateWindow] = []
|
|
53
|
+
windows.extend(
|
|
54
|
+
_windows_from_tiles(
|
|
55
|
+
prefilter.candidate_tiles,
|
|
56
|
+
tile_config,
|
|
57
|
+
old_index,
|
|
58
|
+
new_index,
|
|
59
|
+
window_config,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
for oversized in prefilter.oversized_candidates:
|
|
63
|
+
tx0, ty0, tx1, ty1 = oversized.tile_bbox
|
|
64
|
+
core = _tile_rect_to_bbox(tx0, ty0, tx1, ty1, tile_config.tile_size)
|
|
65
|
+
windows.append(
|
|
66
|
+
_make_window(
|
|
67
|
+
oversized.layer,
|
|
68
|
+
core,
|
|
69
|
+
source_count=(tx1 - tx0 + 1) * (ty1 - ty0 + 1),
|
|
70
|
+
old_index=old_index,
|
|
71
|
+
new_index=new_index,
|
|
72
|
+
window_config=window_config,
|
|
73
|
+
reason="oversized polygon",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
return tuple(sorted(windows, key=lambda window: (window.layer, window.core_bbox, window.reason)))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _windows_from_tiles(
|
|
80
|
+
tiles: tuple[TileKey, ...],
|
|
81
|
+
tile_config: TileConfig,
|
|
82
|
+
old_index: SpatialIndex,
|
|
83
|
+
new_index: SpatialIndex,
|
|
84
|
+
window_config: WindowConfig,
|
|
85
|
+
) -> list[CandidateWindow]:
|
|
86
|
+
grouped: dict[LayerSpec, list[TileKey]] = {}
|
|
87
|
+
for tile in tiles:
|
|
88
|
+
grouped.setdefault(tile.layer, []).append(tile)
|
|
89
|
+
windows: list[CandidateWindow] = []
|
|
90
|
+
for layer, layer_tiles in grouped.items():
|
|
91
|
+
rects = _merge_tiles_to_rectangles(layer_tiles, window_config.max_tiles_per_window)
|
|
92
|
+
for tx0, ty0, tx1, ty1, count in rects:
|
|
93
|
+
core = _tile_rect_to_bbox(tx0, ty0, tx1, ty1, tile_config.tile_size)
|
|
94
|
+
windows.append(
|
|
95
|
+
_make_window(
|
|
96
|
+
layer,
|
|
97
|
+
core,
|
|
98
|
+
source_count=count,
|
|
99
|
+
old_index=old_index,
|
|
100
|
+
new_index=new_index,
|
|
101
|
+
window_config=window_config,
|
|
102
|
+
reason="tile mismatch",
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
return windows
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _merge_tiles_to_rectangles(tiles: list[TileKey], max_tiles_per_window: int) -> list[tuple[int, int, int, int, int]]:
|
|
109
|
+
rows: dict[int, list[int]] = {}
|
|
110
|
+
for tile in tiles:
|
|
111
|
+
rows.setdefault(tile.ty, []).append(tile.tx)
|
|
112
|
+
row_runs: list[tuple[int, int, int]] = []
|
|
113
|
+
for ty, txs in rows.items():
|
|
114
|
+
for x0, x1 in _runs(sorted(set(txs))):
|
|
115
|
+
for split_x0, split_x1 in _split_run(x0, x1, max_tiles_per_window):
|
|
116
|
+
row_runs.append((ty, split_x0, split_x1))
|
|
117
|
+
row_runs.sort()
|
|
118
|
+
rectangles: list[tuple[int, int, int, int, int]] = []
|
|
119
|
+
active: dict[tuple[int, int], tuple[int, int, int, int, int]] = {}
|
|
120
|
+
for ty, x0, x1 in row_runs:
|
|
121
|
+
key = (x0, x1)
|
|
122
|
+
prev = active.pop(key, None)
|
|
123
|
+
width = x1 - x0 + 1
|
|
124
|
+
if prev is not None and prev[3] + 1 == ty and prev[4] + width <= max_tiles_per_window:
|
|
125
|
+
active[key] = (x0, prev[1], x1, ty, prev[4] + width)
|
|
126
|
+
else:
|
|
127
|
+
if prev is not None:
|
|
128
|
+
rectangles.append(prev)
|
|
129
|
+
active[key] = (x0, ty, x1, ty, width)
|
|
130
|
+
rectangles.extend(active.values())
|
|
131
|
+
return sorted(rectangles)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _runs(values: list[int]) -> list[tuple[int, int]]:
|
|
135
|
+
if not values:
|
|
136
|
+
return []
|
|
137
|
+
result = []
|
|
138
|
+
start = previous = values[0]
|
|
139
|
+
for value in values[1:]:
|
|
140
|
+
if value == previous + 1:
|
|
141
|
+
previous = value
|
|
142
|
+
continue
|
|
143
|
+
result.append((start, previous))
|
|
144
|
+
start = previous = value
|
|
145
|
+
result.append((start, previous))
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _split_run(x0: int, x1: int, max_width: int) -> list[tuple[int, int]]:
|
|
150
|
+
if max_width <= 0:
|
|
151
|
+
raise ValueError("max_width must be positive")
|
|
152
|
+
result = []
|
|
153
|
+
current = x0
|
|
154
|
+
while current <= x1:
|
|
155
|
+
end = min(x1, current + max_width - 1)
|
|
156
|
+
result.append((current, end))
|
|
157
|
+
current = end + 1
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _tile_rect_to_bbox(tx0: int, ty0: int, tx1: int, ty1: int, tile_size: int) -> BBox:
|
|
162
|
+
return tx0 * tile_size, ty0 * tile_size, (tx1 + 1) * tile_size - 1, (ty1 + 1) * tile_size - 1
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _make_window(
|
|
166
|
+
layer: LayerSpec,
|
|
167
|
+
core: BBox,
|
|
168
|
+
*,
|
|
169
|
+
source_count: int,
|
|
170
|
+
old_index: SpatialIndex,
|
|
171
|
+
new_index: SpatialIndex,
|
|
172
|
+
window_config: WindowConfig,
|
|
173
|
+
reason: str,
|
|
174
|
+
) -> CandidateWindow:
|
|
175
|
+
verification = expand_bbox(core, window_config.halo_dbu)
|
|
176
|
+
return CandidateWindow(
|
|
177
|
+
layer=layer,
|
|
178
|
+
core_bbox=core,
|
|
179
|
+
verification_bbox=verification,
|
|
180
|
+
source_candidate_count=source_count,
|
|
181
|
+
estimated_old_polygons=len(old_index.query(layer, verification)),
|
|
182
|
+
estimated_new_polygons=len(new_index.query(layer, verification)),
|
|
183
|
+
reason=reason,
|
|
184
|
+
)
|