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/api.py
ADDED
|
@@ -0,0 +1,2955 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
6
|
+
from concurrent.futures.process import BrokenProcessPool
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from time import perf_counter
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from .cache import (
|
|
13
|
+
CachedGeometry,
|
|
14
|
+
load_cached_pair_verdict,
|
|
15
|
+
load_or_build_cached_geometry,
|
|
16
|
+
load_or_build_cached_native_polygons,
|
|
17
|
+
write_cached_pair_verdict,
|
|
18
|
+
)
|
|
19
|
+
from ._environment import get_environment
|
|
20
|
+
from .canonicalize import intern_polygons
|
|
21
|
+
from .config import CompareConfig, LayerSpec
|
|
22
|
+
from .backends.exact import CudaManhattanRectExactComponentBackend, GdstkExactComponentBackend, ManhattanRectExactComponentBackend
|
|
23
|
+
from .backends.metal import MetalManhattanRectExactComponentBackend
|
|
24
|
+
from .backends.polygon_extract_ctypes import PolygonExtractCtypesUnavailable, extract_gds_polygons_native
|
|
25
|
+
from .design_history import render_design_history_entry
|
|
26
|
+
from .diff_gds import write_diff_gds
|
|
27
|
+
from .exact_diff_markdown import (
|
|
28
|
+
ExactDiffMarkdownWriteResult,
|
|
29
|
+
build_polygon_inventory_delta,
|
|
30
|
+
build_polygon_inventory_delta_from_record_delta,
|
|
31
|
+
write_exact_diff_markdown,
|
|
32
|
+
)
|
|
33
|
+
from .exact_oracle import DiffFragment, ExactLayerTask, ExactOracleResult, LayerExactDiff, run_full_exact_oracle
|
|
34
|
+
from .extract import ExtractionResult, extract_gds_polygons
|
|
35
|
+
from .gds_metadata import read_gds_metadata
|
|
36
|
+
from .geometry import PolygonBuffer
|
|
37
|
+
from .hierarchy import compare_hierarchy
|
|
38
|
+
from .multiprocessing_support import mark_portable_process_worker, portable_process_context
|
|
39
|
+
from .native import discover_native_capabilities
|
|
40
|
+
from .policy import PolicyClassificationResult, classify_exact_differences
|
|
41
|
+
from .polygon_components import (
|
|
42
|
+
add_overlapping_context_to_components,
|
|
43
|
+
build_record_delta_surplus_components,
|
|
44
|
+
exact_result_from_component_quads,
|
|
45
|
+
)
|
|
46
|
+
from .report import CompareReport, build_layer_reports
|
|
47
|
+
from .rect_coverage import RectCoverageUnsupported, run_cuda_rectangle_coverage_diff, run_metal_rectangle_coverage_diff
|
|
48
|
+
from .semantics import Backend, CompareStatus, EvidenceEngine, exit_code_for_status
|
|
49
|
+
from .spatial import SpatialIndex
|
|
50
|
+
from .source_edge_diff import exact_result_from_source_edges
|
|
51
|
+
from .surplus_coverage import run_canonical_surplus_coverage_probe, run_record_delta_surplus_coverage_probe
|
|
52
|
+
from .tiling import CpuTilePrefilterResult, TileConfig, run_cpu_tile_prefilter
|
|
53
|
+
from .windowed_exact import WindowedExactResult, run_windowed_exact_oracle
|
|
54
|
+
from .windows import CandidateWindow, WindowConfig, build_candidate_windows
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class CompareRunResult:
|
|
59
|
+
status: CompareStatus
|
|
60
|
+
report: CompareReport
|
|
61
|
+
exact_result: ExactOracleResult
|
|
62
|
+
policy_result: PolicyClassificationResult
|
|
63
|
+
prefilter: CpuTilePrefilterResult
|
|
64
|
+
windows: tuple[CandidateWindow, ...]
|
|
65
|
+
artifacts: dict[str, str] = field(default_factory=dict)
|
|
66
|
+
timings: dict[str, float] = field(default_factory=dict)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def exit_code(self) -> int:
|
|
70
|
+
return exit_code_for_status(self.status, self.report.failure_policy)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class _FullExactVerification:
|
|
75
|
+
exact_result: ExactOracleResult
|
|
76
|
+
policy_result: PolicyClassificationResult
|
|
77
|
+
old_input: dict[str, object]
|
|
78
|
+
new_input: dict[str, object]
|
|
79
|
+
selection: dict[str, object]
|
|
80
|
+
layers: tuple[object, ...]
|
|
81
|
+
extract_s: float
|
|
82
|
+
exact_s: float
|
|
83
|
+
exact_component_backend: dict[str, object] = field(default_factory=dict)
|
|
84
|
+
polygon_inventory_delta: object | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class _FileIdentity:
|
|
89
|
+
old_token: str
|
|
90
|
+
new_token: str
|
|
91
|
+
same: bool
|
|
92
|
+
method: str
|
|
93
|
+
old_size: int
|
|
94
|
+
new_size: int
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True)
|
|
98
|
+
class _ExactComponentSelection:
|
|
99
|
+
name: str
|
|
100
|
+
component_partition: bool
|
|
101
|
+
executor: Callable[[tuple[ExactLayerTask, ...]], tuple[LayerExactDiff, ...]] | None
|
|
102
|
+
backend: object | None = None
|
|
103
|
+
|
|
104
|
+
def to_json(self) -> dict[str, object]:
|
|
105
|
+
data: dict[str, object] = {
|
|
106
|
+
"requested": self.name,
|
|
107
|
+
"component_partition": self.component_partition,
|
|
108
|
+
"executor_enabled": self.executor is not None,
|
|
109
|
+
}
|
|
110
|
+
if self.backend is not None and hasattr(self.backend, "capabilities"):
|
|
111
|
+
capabilities = self.backend.capabilities()
|
|
112
|
+
data.update(
|
|
113
|
+
{
|
|
114
|
+
"name": capabilities.name,
|
|
115
|
+
"deterministic": capabilities.deterministic,
|
|
116
|
+
"requires_gpu": capabilities.requires_gpu,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
data.update({"name": "gdstk-full-oracle", "deterministic": True, "requires_gpu": False})
|
|
121
|
+
stats = getattr(self.backend, "stats", None)
|
|
122
|
+
if stats is not None:
|
|
123
|
+
data["stats"] = {
|
|
124
|
+
"accelerated_tasks": stats.accelerated_tasks,
|
|
125
|
+
"fallback_tasks": stats.fallback_tasks,
|
|
126
|
+
"unsupported_tasks": stats.unsupported_tasks,
|
|
127
|
+
}
|
|
128
|
+
if hasattr(stats, "identical_skipped_tasks"):
|
|
129
|
+
data["stats"]["identical_skipped_tasks"] = stats.identical_skipped_tasks
|
|
130
|
+
if hasattr(stats, "cpu_rect_set_tasks"):
|
|
131
|
+
data["stats"]["cpu_rect_set_tasks"] = stats.cpu_rect_set_tasks
|
|
132
|
+
if hasattr(stats, "cuda_rect_set_tasks"):
|
|
133
|
+
data["stats"]["cuda_rect_set_tasks"] = stats.cuda_rect_set_tasks
|
|
134
|
+
if hasattr(stats, "metal_rect_set_tasks"):
|
|
135
|
+
data["stats"]["metal_rect_set_tasks"] = stats.metal_rect_set_tasks
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def compare_gds_files(
|
|
140
|
+
config: CompareConfig,
|
|
141
|
+
*,
|
|
142
|
+
json_report_path: str | Path | None = None,
|
|
143
|
+
diff_gds_path: str | Path | None = None,
|
|
144
|
+
markdown_path: str | Path | None = None,
|
|
145
|
+
exact_diff_markdown_path: str | Path | None = None,
|
|
146
|
+
exact: bool = False,
|
|
147
|
+
verify_exact_after_fast: bool = False,
|
|
148
|
+
audit_cpu_oracle: bool = False,
|
|
149
|
+
use_fast_shortcuts: bool = True,
|
|
150
|
+
use_structural_guard: bool = False,
|
|
151
|
+
use_native_geometry_guard: bool = False,
|
|
152
|
+
exact_component_backend: str = "none",
|
|
153
|
+
command: tuple[str, ...] = (),
|
|
154
|
+
) -> CompareRunResult:
|
|
155
|
+
start = perf_counter()
|
|
156
|
+
backend_info = _select_backend(config, exact=exact)
|
|
157
|
+
exact = exact or config.evidence_engine == EvidenceEngine.GDSTK_ORACLE
|
|
158
|
+
|
|
159
|
+
old_top_name = config.old_top or config.top
|
|
160
|
+
new_top_name = config.new_top or config.top
|
|
161
|
+
|
|
162
|
+
if config.evidence_engine == EvidenceEngine.GPU_POLYGON and not exact:
|
|
163
|
+
return _try_gpu_polygon_evidence_path(
|
|
164
|
+
config,
|
|
165
|
+
backend_info=backend_info,
|
|
166
|
+
old_top_name=old_top_name,
|
|
167
|
+
new_top_name=new_top_name,
|
|
168
|
+
command=command,
|
|
169
|
+
json_report_path=json_report_path,
|
|
170
|
+
markdown_path=markdown_path,
|
|
171
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
172
|
+
audit_cpu_oracle=audit_cpu_oracle,
|
|
173
|
+
exact_component_backend=exact_component_backend,
|
|
174
|
+
start=start,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if use_structural_guard and not exact:
|
|
178
|
+
structural_result = _try_native_structural_guard_path(
|
|
179
|
+
config,
|
|
180
|
+
backend_info=backend_info,
|
|
181
|
+
old_top_name=old_top_name,
|
|
182
|
+
new_top_name=new_top_name,
|
|
183
|
+
command=command,
|
|
184
|
+
json_report_path=json_report_path,
|
|
185
|
+
markdown_path=markdown_path,
|
|
186
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
187
|
+
verify_exact_after_fast=verify_exact_after_fast,
|
|
188
|
+
exact_component_backend=exact_component_backend,
|
|
189
|
+
start=start,
|
|
190
|
+
)
|
|
191
|
+
if structural_result is not None:
|
|
192
|
+
return structural_result
|
|
193
|
+
|
|
194
|
+
if (use_native_geometry_guard or (use_fast_shortcuts and _accelerated_fast_path_eligible(config, backend_info))) and not exact:
|
|
195
|
+
geometry_guard_result = _try_native_geometry_guard_path(
|
|
196
|
+
config,
|
|
197
|
+
backend_info=backend_info,
|
|
198
|
+
old_top_name=old_top_name,
|
|
199
|
+
new_top_name=new_top_name,
|
|
200
|
+
command=command,
|
|
201
|
+
json_report_path=json_report_path,
|
|
202
|
+
markdown_path=markdown_path,
|
|
203
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
204
|
+
verify_exact_after_fast=verify_exact_after_fast,
|
|
205
|
+
exact_component_backend=exact_component_backend,
|
|
206
|
+
start=start,
|
|
207
|
+
)
|
|
208
|
+
if geometry_guard_result is not None:
|
|
209
|
+
return geometry_guard_result
|
|
210
|
+
|
|
211
|
+
fast_digest_result = None
|
|
212
|
+
if use_fast_shortcuts and not exact and _accelerated_fast_path_eligible(config, backend_info):
|
|
213
|
+
fast_digest_result = _try_accelerated_byte_identity_fast_path(
|
|
214
|
+
config,
|
|
215
|
+
backend_info=backend_info,
|
|
216
|
+
old_metadata={"path": str(config.old_path), "metadata_mode": "skipped-accelerated-byte-identity"},
|
|
217
|
+
new_metadata={"path": str(config.new_path), "metadata_mode": "skipped-accelerated-byte-identity"},
|
|
218
|
+
old_top_name=old_top_name,
|
|
219
|
+
new_top_name=new_top_name,
|
|
220
|
+
command=command,
|
|
221
|
+
json_report_path=json_report_path,
|
|
222
|
+
markdown_path=markdown_path,
|
|
223
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
224
|
+
verify_exact_after_fast=verify_exact_after_fast,
|
|
225
|
+
exact_component_backend=exact_component_backend,
|
|
226
|
+
start=start,
|
|
227
|
+
)
|
|
228
|
+
if fast_digest_result is not None:
|
|
229
|
+
return fast_digest_result
|
|
230
|
+
|
|
231
|
+
old_metadata = read_gds_metadata(config.old_path)
|
|
232
|
+
new_metadata = read_gds_metadata(config.new_path)
|
|
233
|
+
|
|
234
|
+
if use_fast_shortcuts:
|
|
235
|
+
fast_result = _try_cuda_hierarchy_fast_path(
|
|
236
|
+
config,
|
|
237
|
+
backend_info=backend_info,
|
|
238
|
+
old_metadata=old_metadata.to_json(),
|
|
239
|
+
new_metadata=new_metadata.to_json(),
|
|
240
|
+
old_top_name=old_top_name,
|
|
241
|
+
new_top_name=new_top_name,
|
|
242
|
+
exact=exact,
|
|
243
|
+
command=command,
|
|
244
|
+
json_report_path=json_report_path,
|
|
245
|
+
markdown_path=markdown_path,
|
|
246
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
247
|
+
verify_exact_after_fast=verify_exact_after_fast,
|
|
248
|
+
)
|
|
249
|
+
if fast_result is not None:
|
|
250
|
+
return fast_result
|
|
251
|
+
|
|
252
|
+
if not exact and not use_fast_shortcuts and verify_exact_after_fast and config.geometry_cache_dir is not None:
|
|
253
|
+
cached_pair_result = _try_pair_verdict_cache_path(
|
|
254
|
+
config,
|
|
255
|
+
backend_info=backend_info,
|
|
256
|
+
old_metadata=old_metadata.to_json(),
|
|
257
|
+
new_metadata=new_metadata.to_json(),
|
|
258
|
+
old_top_name=old_top_name,
|
|
259
|
+
new_top_name=new_top_name,
|
|
260
|
+
command=command,
|
|
261
|
+
json_report_path=json_report_path,
|
|
262
|
+
markdown_path=markdown_path,
|
|
263
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
264
|
+
start=start,
|
|
265
|
+
exact_component_backend=exact_component_backend,
|
|
266
|
+
)
|
|
267
|
+
if cached_pair_result is not None:
|
|
268
|
+
return cached_pair_result
|
|
269
|
+
|
|
270
|
+
cached_geometry: tuple[CachedGeometry | None, CachedGeometry | None] = (None, None)
|
|
271
|
+
if not exact and config.geometry_cache_dir is not None:
|
|
272
|
+
cache_start = perf_counter()
|
|
273
|
+
old_cached, new_cached = _cached_geometry_pair(
|
|
274
|
+
config,
|
|
275
|
+
old_top_name=old_top_name,
|
|
276
|
+
new_top_name=new_top_name,
|
|
277
|
+
)
|
|
278
|
+
cached_geometry = (old_cached, new_cached)
|
|
279
|
+
old_extraction = old_cached.extraction
|
|
280
|
+
new_extraction = new_cached.extraction
|
|
281
|
+
timings: dict[str, float] = {
|
|
282
|
+
"extract_s": 0.0,
|
|
283
|
+
"geometry_cache_s": perf_counter() - cache_start,
|
|
284
|
+
}
|
|
285
|
+
timings.update(old_cached.to_timing_json("old"))
|
|
286
|
+
timings.update(new_cached.to_timing_json("new"))
|
|
287
|
+
else:
|
|
288
|
+
old_extraction, new_extraction = _extract_pair(
|
|
289
|
+
config,
|
|
290
|
+
old_top_name=old_top_name,
|
|
291
|
+
new_top_name=new_top_name,
|
|
292
|
+
)
|
|
293
|
+
timings = {"extract_s": perf_counter() - start}
|
|
294
|
+
exact_verification_complete = True
|
|
295
|
+
layer_exact_status = "complete"
|
|
296
|
+
warnings: tuple[str, ...] = ()
|
|
297
|
+
if exact:
|
|
298
|
+
exact_oracle_start = perf_counter()
|
|
299
|
+
exact_selection = _select_exact_component_backend(exact_component_backend)
|
|
300
|
+
exact_result = run_full_exact_oracle(
|
|
301
|
+
old_extraction.buffer,
|
|
302
|
+
new_extraction.buffer,
|
|
303
|
+
component_partition=exact_selection.component_partition,
|
|
304
|
+
component_executor=exact_selection.executor,
|
|
305
|
+
)
|
|
306
|
+
timings["exact_oracle_s"] = perf_counter() - exact_oracle_start
|
|
307
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
308
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
309
|
+
exact_fallback = {
|
|
310
|
+
"mode": "full",
|
|
311
|
+
"window_count": 0,
|
|
312
|
+
"escalated_layers": [],
|
|
313
|
+
"exact_component_backend": exact_selection.to_json(),
|
|
314
|
+
}
|
|
315
|
+
else:
|
|
316
|
+
canonical_start = perf_counter()
|
|
317
|
+
old_canonical_ids, new_canonical_ids = _shared_canonical_ids(
|
|
318
|
+
old_extraction.buffer,
|
|
319
|
+
new_extraction.buffer,
|
|
320
|
+
old_intern=cached_geometry[0].intern if cached_geometry[0] is not None else None,
|
|
321
|
+
new_intern=cached_geometry[1].intern if cached_geometry[1] is not None else None,
|
|
322
|
+
)
|
|
323
|
+
timings["canonical_s"] = perf_counter() - canonical_start
|
|
324
|
+
tile_config = TileConfig(tile_size=config.tile_size)
|
|
325
|
+
prefilter_start = perf_counter()
|
|
326
|
+
prefilter = _run_tile_prefilter(
|
|
327
|
+
backend_info,
|
|
328
|
+
old_extraction.buffer,
|
|
329
|
+
old_canonical_ids,
|
|
330
|
+
new_extraction.buffer,
|
|
331
|
+
new_canonical_ids,
|
|
332
|
+
tile_config,
|
|
333
|
+
)
|
|
334
|
+
timings["prefilter_s"] = perf_counter() - prefilter_start
|
|
335
|
+
spatial_start = perf_counter()
|
|
336
|
+
old_index = cached_geometry[0].spatial_index if cached_geometry[0] is not None else SpatialIndex.from_buffer(old_extraction.buffer)
|
|
337
|
+
new_index = cached_geometry[1].spatial_index if cached_geometry[1] is not None else SpatialIndex.from_buffer(new_extraction.buffer)
|
|
338
|
+
timings["spatial_index_s"] = perf_counter() - spatial_start
|
|
339
|
+
window_start = perf_counter()
|
|
340
|
+
windows = build_candidate_windows(
|
|
341
|
+
prefilter,
|
|
342
|
+
tile_config,
|
|
343
|
+
old_index,
|
|
344
|
+
new_index,
|
|
345
|
+
WindowConfig(),
|
|
346
|
+
)
|
|
347
|
+
timings["window_build_s"] = perf_counter() - window_start
|
|
348
|
+
if _should_use_layer_exact(backend_info, old_extraction.buffer, new_extraction.buffer, windows):
|
|
349
|
+
candidate_layers = tuple(sorted({window.layer for window in windows}))
|
|
350
|
+
exact_oracle_start = perf_counter()
|
|
351
|
+
exact_result = run_full_exact_oracle(old_extraction.buffer, new_extraction.buffer, layers=set(candidate_layers))
|
|
352
|
+
timings["exact_oracle_s"] = perf_counter() - exact_oracle_start
|
|
353
|
+
windowed = WindowedExactResult(exact_result.layers, escalated_layers=candidate_layers)
|
|
354
|
+
exact_fallback = {
|
|
355
|
+
"mode": "candidate-layer-exact",
|
|
356
|
+
"window_count": len(windows),
|
|
357
|
+
"escalated_layers": [layer.to_json() for layer in candidate_layers],
|
|
358
|
+
"failures": [],
|
|
359
|
+
}
|
|
360
|
+
else:
|
|
361
|
+
exact_oracle_start = perf_counter()
|
|
362
|
+
windowed = run_windowed_exact_oracle(
|
|
363
|
+
old_extraction.buffer,
|
|
364
|
+
new_extraction.buffer,
|
|
365
|
+
windows,
|
|
366
|
+
old_index=old_index,
|
|
367
|
+
new_index=new_index,
|
|
368
|
+
)
|
|
369
|
+
timings["exact_oracle_s"] = perf_counter() - exact_oracle_start
|
|
370
|
+
exact_result = windowed.exact_result
|
|
371
|
+
exact_fallback = {
|
|
372
|
+
"mode": "windowed",
|
|
373
|
+
"window_count": len(windows),
|
|
374
|
+
"escalated_layers": [layer.to_json() for layer in windowed.escalated_layers],
|
|
375
|
+
"failures": [failure.to_json() for failure in windowed.failures],
|
|
376
|
+
}
|
|
377
|
+
if windows:
|
|
378
|
+
exact_verification_complete = False
|
|
379
|
+
layer_exact_status = "windowed"
|
|
380
|
+
warnings = (
|
|
381
|
+
"Windowed verification is localized and does not claim full-file exact XOR equivalence; "
|
|
382
|
+
"run exact=True for the full gdstk oracle.",
|
|
383
|
+
)
|
|
384
|
+
if config.geometry_cache_dir is not None:
|
|
385
|
+
pair_cache_start = perf_counter()
|
|
386
|
+
pair_cache_policy = classify_exact_differences(
|
|
387
|
+
exact_result,
|
|
388
|
+
config.allowed_regions,
|
|
389
|
+
failure_policy=config.failure_policy,
|
|
390
|
+
)
|
|
391
|
+
pair_cache_key = write_cached_pair_verdict(
|
|
392
|
+
config,
|
|
393
|
+
old_top_name=old_top_name,
|
|
394
|
+
new_top_name=new_top_name,
|
|
395
|
+
exact_result=exact_result,
|
|
396
|
+
prefilter=prefilter,
|
|
397
|
+
windows=windows,
|
|
398
|
+
exact_fallback=exact_fallback,
|
|
399
|
+
fast_status=pair_cache_policy.status.value,
|
|
400
|
+
exact_verification_complete=exact_verification_complete,
|
|
401
|
+
layer_exact_status=layer_exact_status,
|
|
402
|
+
warnings=warnings,
|
|
403
|
+
)
|
|
404
|
+
timings["pair_verdict_cache_write_s"] = perf_counter() - pair_cache_start
|
|
405
|
+
if pair_cache_key is not None:
|
|
406
|
+
timings["pair_verdict_cache_key"] = pair_cache_key
|
|
407
|
+
if verify_exact_after_fast and not exact_verification_complete:
|
|
408
|
+
geometry_policy_result = classify_exact_differences(
|
|
409
|
+
exact_result,
|
|
410
|
+
config.allowed_regions,
|
|
411
|
+
failure_policy=config.failure_policy,
|
|
412
|
+
)
|
|
413
|
+
geometry_status_s = perf_counter() - start
|
|
414
|
+
geometry_mode = str(exact_fallback.get("mode", "geometry"))
|
|
415
|
+
full_exact_start = perf_counter()
|
|
416
|
+
exact_selection = _select_exact_component_backend(exact_component_backend)
|
|
417
|
+
exact_result = run_full_exact_oracle(
|
|
418
|
+
old_extraction.buffer,
|
|
419
|
+
new_extraction.buffer,
|
|
420
|
+
component_partition=exact_selection.component_partition,
|
|
421
|
+
component_executor=exact_selection.executor,
|
|
422
|
+
)
|
|
423
|
+
timings["full_exact_oracle_s"] = perf_counter() - full_exact_start
|
|
424
|
+
timings["geometry_status_s"] = geometry_status_s
|
|
425
|
+
exact_verification_complete = True
|
|
426
|
+
layer_exact_status = "complete"
|
|
427
|
+
warnings = (
|
|
428
|
+
"Geometry prefilter/windowed verdict was produced first; full exact gdstk verification was run afterward and supplies the polygon-level evidence.",
|
|
429
|
+
)
|
|
430
|
+
exact_fallback = {
|
|
431
|
+
"mode": f"{geometry_mode}-plus-full-exact",
|
|
432
|
+
"geometry_mode": geometry_mode,
|
|
433
|
+
"window_count": exact_fallback.get("window_count", len(windows)),
|
|
434
|
+
"escalated_layers": exact_fallback.get("escalated_layers", []),
|
|
435
|
+
"failures": exact_fallback.get("failures", []),
|
|
436
|
+
"fast_status": geometry_policy_result.status.value,
|
|
437
|
+
"exact_component_backend": exact_selection.to_json(),
|
|
438
|
+
}
|
|
439
|
+
timings["exact_s"] = perf_counter() - start - timings["extract_s"]
|
|
440
|
+
|
|
441
|
+
policy_result = classify_exact_differences(
|
|
442
|
+
exact_result,
|
|
443
|
+
config.allowed_regions,
|
|
444
|
+
failure_policy=config.failure_policy,
|
|
445
|
+
)
|
|
446
|
+
artifacts: dict[str, str] = {}
|
|
447
|
+
if diff_gds_path is not None:
|
|
448
|
+
artifacts["diff_gds"] = str(write_diff_gds(policy_result, diff_gds_path).path)
|
|
449
|
+
if markdown_path is not None:
|
|
450
|
+
markdown_text = render_design_history_entry(
|
|
451
|
+
"GDS comparator result",
|
|
452
|
+
policy_result,
|
|
453
|
+
diff_gds_path=artifacts.get("diff_gds"),
|
|
454
|
+
tests_run=("gdsdiff",),
|
|
455
|
+
)
|
|
456
|
+
markdown_path = Path(markdown_path)
|
|
457
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
458
|
+
markdown_path.write_text(markdown_text, encoding="utf-8")
|
|
459
|
+
artifacts["markdown"] = str(markdown_path)
|
|
460
|
+
if exact_diff_markdown_path is not None:
|
|
461
|
+
inventory_delta_start = perf_counter()
|
|
462
|
+
polygon_inventory_delta = (
|
|
463
|
+
build_polygon_inventory_delta(old_extraction.buffer, new_extraction.buffer)
|
|
464
|
+
if exact_verification_complete
|
|
465
|
+
else None
|
|
466
|
+
)
|
|
467
|
+
timings["polygon_inventory_delta_s"] = perf_counter() - inventory_delta_start
|
|
468
|
+
artifacts["exact_diff_markdown"] = str(
|
|
469
|
+
write_exact_diff_markdown(
|
|
470
|
+
policy_result,
|
|
471
|
+
exact_diff_markdown_path,
|
|
472
|
+
title="GDS comparator exact diff",
|
|
473
|
+
source_mode=exact_fallback["mode"],
|
|
474
|
+
exact_verification_complete=exact_verification_complete,
|
|
475
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
476
|
+
).path
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
layer_reports = build_layer_reports(
|
|
480
|
+
exact_result,
|
|
481
|
+
policy_result=policy_result,
|
|
482
|
+
presence_by_layer=_presence_by_layer(old_extraction.buffer, new_extraction.buffer),
|
|
483
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_extraction.buffer, new_extraction.buffer),
|
|
484
|
+
candidate_tile_counts_by_layer=_candidate_tile_counts(prefilter),
|
|
485
|
+
exact_window_counts_by_layer=_window_counts(windows),
|
|
486
|
+
exact_status=layer_exact_status,
|
|
487
|
+
)
|
|
488
|
+
timings["total_s"] = perf_counter() - start
|
|
489
|
+
report = CompareReport(
|
|
490
|
+
status=policy_result.status,
|
|
491
|
+
exact_verification_complete=exact_verification_complete,
|
|
492
|
+
old_input=_input_json(old_extraction, old_metadata.to_json()),
|
|
493
|
+
new_input=_input_json(new_extraction, new_metadata.to_json()),
|
|
494
|
+
semantics=config.to_json_dict(),
|
|
495
|
+
selection={"old_top": old_extraction.top_name, "new_top": new_extraction.top_name, "layers": config.to_json_dict()["layers"]},
|
|
496
|
+
backend=backend_info
|
|
497
|
+
| {
|
|
498
|
+
"exact_mode": exact,
|
|
499
|
+
"fast_shortcuts_enabled": use_fast_shortcuts,
|
|
500
|
+
"geometry_cache_enabled": config.geometry_cache_dir is not None,
|
|
501
|
+
},
|
|
502
|
+
prefilter=prefilter.to_json(),
|
|
503
|
+
exact_fallback=exact_fallback,
|
|
504
|
+
layers=layer_reports,
|
|
505
|
+
allowed_policy=policy_result.effective_policy,
|
|
506
|
+
unexpected_summary={
|
|
507
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
508
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
509
|
+
},
|
|
510
|
+
timings=timings,
|
|
511
|
+
warnings=warnings,
|
|
512
|
+
artifacts=artifacts,
|
|
513
|
+
command=command,
|
|
514
|
+
failure_policy=config.failure_policy,
|
|
515
|
+
)
|
|
516
|
+
if json_report_path is not None:
|
|
517
|
+
artifacts["json_report"] = str(json_report_path)
|
|
518
|
+
report = _report_with_artifacts(report, artifacts)
|
|
519
|
+
report.write_json(json_report_path)
|
|
520
|
+
|
|
521
|
+
return CompareRunResult(
|
|
522
|
+
status=policy_result.status,
|
|
523
|
+
report=report,
|
|
524
|
+
exact_result=exact_result,
|
|
525
|
+
policy_result=policy_result,
|
|
526
|
+
prefilter=prefilter,
|
|
527
|
+
windows=windows,
|
|
528
|
+
artifacts=artifacts,
|
|
529
|
+
timings=timings,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def compare_gds_paths(old_path: str | Path, new_path: str | Path, **kwargs) -> CompareRunResult:
|
|
534
|
+
config_kwargs = {key: value for key, value in kwargs.items() if key in CompareConfig.__dataclass_fields__}
|
|
535
|
+
run_kwargs = {key: value for key, value in kwargs.items() if key not in config_kwargs}
|
|
536
|
+
return compare_gds_files(CompareConfig(old_path=old_path, new_path=new_path, **config_kwargs), **run_kwargs)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _extract_pair(
|
|
540
|
+
config: CompareConfig,
|
|
541
|
+
*,
|
|
542
|
+
old_top_name: str | None,
|
|
543
|
+
new_top_name: str | None,
|
|
544
|
+
) -> tuple[ExtractionResult, ExtractionResult]:
|
|
545
|
+
old_args = (config.old_path, old_top_name, config.layers, config.dbu, config.path_policy)
|
|
546
|
+
new_args = (config.new_path, new_top_name, config.layers, config.dbu, config.path_policy)
|
|
547
|
+
if _should_extract_pair_in_parallel(config):
|
|
548
|
+
try:
|
|
549
|
+
with ProcessPoolExecutor(
|
|
550
|
+
max_workers=2,
|
|
551
|
+
mp_context=portable_process_context(),
|
|
552
|
+
initializer=mark_portable_process_worker,
|
|
553
|
+
) as executor:
|
|
554
|
+
old_future = executor.submit(_extract_one, old_args)
|
|
555
|
+
new_future = executor.submit(_extract_one, new_args)
|
|
556
|
+
return old_future.result(), new_future.result()
|
|
557
|
+
except (BrokenProcessPool, OSError):
|
|
558
|
+
pass
|
|
559
|
+
return _extract_one(old_args), _extract_one(new_args)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _cached_geometry_pair(
|
|
563
|
+
config: CompareConfig,
|
|
564
|
+
*,
|
|
565
|
+
old_top_name: str | None,
|
|
566
|
+
new_top_name: str | None,
|
|
567
|
+
) -> tuple[CachedGeometry, CachedGeometry]:
|
|
568
|
+
if config.geometry_cache_dir is None:
|
|
569
|
+
raise ValueError("geometry cache directory is not configured")
|
|
570
|
+
old_args = (config.geometry_cache_dir, config.old_path, old_top_name, config.layers, config.dbu, config.path_policy)
|
|
571
|
+
new_args = (config.geometry_cache_dir, config.new_path, new_top_name, config.layers, config.dbu, config.path_policy)
|
|
572
|
+
if _should_extract_pair_in_parallel(config):
|
|
573
|
+
try:
|
|
574
|
+
with ProcessPoolExecutor(
|
|
575
|
+
max_workers=2,
|
|
576
|
+
mp_context=portable_process_context(),
|
|
577
|
+
initializer=mark_portable_process_worker,
|
|
578
|
+
) as executor:
|
|
579
|
+
old_future = executor.submit(_cached_geometry_one, old_args)
|
|
580
|
+
new_future = executor.submit(_cached_geometry_one, new_args)
|
|
581
|
+
return old_future.result(), new_future.result()
|
|
582
|
+
except (BrokenProcessPool, OSError):
|
|
583
|
+
pass
|
|
584
|
+
return _cached_geometry_one(old_args), _cached_geometry_one(new_args)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _cached_geometry_one(args) -> CachedGeometry:
|
|
588
|
+
cache_dir, path, top_name, layers, dbu, path_policy = args
|
|
589
|
+
return load_or_build_cached_geometry(
|
|
590
|
+
cache_dir=cache_dir,
|
|
591
|
+
path=path,
|
|
592
|
+
top_name=top_name,
|
|
593
|
+
layers=layers,
|
|
594
|
+
meters_per_dbu=dbu,
|
|
595
|
+
path_policy=path_policy,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _extract_one(args) -> ExtractionResult:
|
|
600
|
+
path, top_name, layers, dbu, path_policy = args
|
|
601
|
+
return extract_gds_polygons(
|
|
602
|
+
path,
|
|
603
|
+
top_name=top_name,
|
|
604
|
+
layers=layers,
|
|
605
|
+
meters_per_dbu=dbu,
|
|
606
|
+
path_policy=path_policy,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _should_extract_pair_in_parallel(config: CompareConfig) -> bool:
|
|
611
|
+
try:
|
|
612
|
+
total_size = Path(config.old_path).stat().st_size + Path(config.new_path).stat().st_size
|
|
613
|
+
except OSError:
|
|
614
|
+
return False
|
|
615
|
+
return total_size >= _parallel_extract_pair_min_bytes()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _parallel_extract_pair_min_bytes() -> int:
|
|
619
|
+
value = get_environment(
|
|
620
|
+
"GDSDIFF_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
|
|
621
|
+
"GDS_GPU_COMPARE_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
|
|
622
|
+
"",
|
|
623
|
+
)
|
|
624
|
+
return _positive_int_value(value, 1_000_000)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _positive_int_value(raw: str | None, default: int) -> int:
|
|
628
|
+
try:
|
|
629
|
+
value = int(raw or "")
|
|
630
|
+
except ValueError:
|
|
631
|
+
return default
|
|
632
|
+
return value if value > 0 else default
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def _try_native_structural_guard_path(
|
|
636
|
+
config: CompareConfig,
|
|
637
|
+
*,
|
|
638
|
+
backend_info: dict[str, object],
|
|
639
|
+
old_top_name: str | None,
|
|
640
|
+
new_top_name: str | None,
|
|
641
|
+
command: tuple[str, ...],
|
|
642
|
+
json_report_path: str | Path | None,
|
|
643
|
+
markdown_path: str | Path | None,
|
|
644
|
+
exact_diff_markdown_path: str | Path | None,
|
|
645
|
+
verify_exact_after_fast: bool,
|
|
646
|
+
exact_component_backend: str,
|
|
647
|
+
start: float,
|
|
648
|
+
) -> CompareRunResult | None:
|
|
649
|
+
if config.allowed_regions or config.layers != "all":
|
|
650
|
+
return None
|
|
651
|
+
try:
|
|
652
|
+
from .backends.structural_ctypes import summarize_gds_structure_native
|
|
653
|
+
|
|
654
|
+
structural_start = perf_counter()
|
|
655
|
+
old_summary = summarize_gds_structure_native(config.old_path, top_name=old_top_name, counts_only=True)
|
|
656
|
+
new_summary = summarize_gds_structure_native(config.new_path, top_name=new_top_name, counts_only=True)
|
|
657
|
+
if old_summary.flattened_shape_count == new_summary.flattened_shape_count:
|
|
658
|
+
old_summary = summarize_gds_structure_native(config.old_path, top_name=old_top_name)
|
|
659
|
+
new_summary = summarize_gds_structure_native(config.new_path, top_name=new_top_name)
|
|
660
|
+
structural_s = perf_counter() - structural_start
|
|
661
|
+
except Exception:
|
|
662
|
+
return None
|
|
663
|
+
counts_match = old_summary.flattened_shape_count == new_summary.flattened_shape_count
|
|
664
|
+
hashes_match = old_summary.structural_hash == new_summary.structural_hash
|
|
665
|
+
if counts_match and hashes_match:
|
|
666
|
+
return None
|
|
667
|
+
structural_mismatch_kind = "hash" if counts_match else "count"
|
|
668
|
+
|
|
669
|
+
fragment = DiffFragment(points=((0, 0), (1, 0), (1, 1), (0, 1)), bbox=(0, 0, 1, 1), twice_area=2)
|
|
670
|
+
exact_result = ExactOracleResult((LayerExactDiff(layer=LayerSpec(0, 0), new_minus_old=(fragment,), xor=(fragment,)),))
|
|
671
|
+
policy_result = classify_exact_differences(exact_result, failure_policy=config.failure_policy)
|
|
672
|
+
fast_status = policy_result.status
|
|
673
|
+
exact_complete = False
|
|
674
|
+
verification: _FullExactVerification | None = None
|
|
675
|
+
if verify_exact_after_fast:
|
|
676
|
+
verification = _run_full_exact_verification(config, exact_component_backend=exact_component_backend)
|
|
677
|
+
exact_result = verification.exact_result
|
|
678
|
+
policy_result = verification.policy_result
|
|
679
|
+
exact_complete = True
|
|
680
|
+
|
|
681
|
+
timings = {
|
|
682
|
+
"extract_s": 0.0,
|
|
683
|
+
"exact_s": structural_s,
|
|
684
|
+
"fast_status_s": structural_s,
|
|
685
|
+
"structural_guard_s": structural_s,
|
|
686
|
+
}
|
|
687
|
+
old_input = {"path": str(config.old_path), "selected_top": old_top_name, "structural_summary": old_summary.to_json()}
|
|
688
|
+
new_input = {"path": str(config.new_path), "selected_top": new_top_name, "structural_summary": new_summary.to_json()}
|
|
689
|
+
selection = {"old_top": old_top_name, "new_top": new_top_name, "layers": "all"}
|
|
690
|
+
layers = build_layer_reports(exact_result, policy_result=policy_result)
|
|
691
|
+
if verification is not None:
|
|
692
|
+
timings.update(
|
|
693
|
+
{
|
|
694
|
+
"extract_s": verification.extract_s,
|
|
695
|
+
"exact_s": verification.exact_s,
|
|
696
|
+
"exact_verification_s": verification.extract_s + verification.exact_s,
|
|
697
|
+
}
|
|
698
|
+
)
|
|
699
|
+
old_input = verification.old_input
|
|
700
|
+
new_input = verification.new_input
|
|
701
|
+
selection = verification.selection
|
|
702
|
+
layers = verification.layers
|
|
703
|
+
|
|
704
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
705
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
706
|
+
artifacts: dict[str, str] = {}
|
|
707
|
+
exact_fallback_mode = "structural-count-fast-plus-full-exact" if verification is not None else "structural-count-fail-closed"
|
|
708
|
+
warnings = (
|
|
709
|
+
"Native structural guard used flattened shape counts for a fail-closed fast status; full exact gdstk verification was run afterward and supplies polygon-level evidence."
|
|
710
|
+
if verification is not None
|
|
711
|
+
else "Native structural guard used flattened shape counts for a fail-closed fast status; run exact verification for polygon-level evidence."
|
|
712
|
+
)
|
|
713
|
+
exact_fallback = {
|
|
714
|
+
"mode": exact_fallback_mode,
|
|
715
|
+
"window_count": 0,
|
|
716
|
+
"escalated_layers": [],
|
|
717
|
+
"failures": [],
|
|
718
|
+
"fast_status": fast_status.value,
|
|
719
|
+
"old_flattened_shape_count": old_summary.flattened_shape_count,
|
|
720
|
+
"new_flattened_shape_count": new_summary.flattened_shape_count,
|
|
721
|
+
"old_structural_hash": f"{old_summary.structural_hash:016x}",
|
|
722
|
+
"new_structural_hash": f"{new_summary.structural_hash:016x}",
|
|
723
|
+
"structural_mismatch_kind": structural_mismatch_kind,
|
|
724
|
+
}
|
|
725
|
+
if verification is not None:
|
|
726
|
+
exact_fallback["exact_component_backend"] = verification.exact_component_backend
|
|
727
|
+
if markdown_path is not None:
|
|
728
|
+
markdown_path = Path(markdown_path)
|
|
729
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
730
|
+
markdown_path.write_text(
|
|
731
|
+
render_design_history_entry(
|
|
732
|
+
"GDS comparator structural-guard result",
|
|
733
|
+
policy_result,
|
|
734
|
+
tests_run=("gdsdiff structural-guard",),
|
|
735
|
+
notes=warnings,
|
|
736
|
+
),
|
|
737
|
+
encoding="utf-8",
|
|
738
|
+
)
|
|
739
|
+
artifacts["markdown"] = str(markdown_path)
|
|
740
|
+
if exact_diff_markdown_path is not None:
|
|
741
|
+
artifacts["exact_diff_markdown"] = str(
|
|
742
|
+
write_exact_diff_markdown(
|
|
743
|
+
policy_result,
|
|
744
|
+
exact_diff_markdown_path,
|
|
745
|
+
title="GDS comparator structural-guard exact diff",
|
|
746
|
+
source_mode=exact_fallback_mode,
|
|
747
|
+
exact_verification_complete=exact_complete,
|
|
748
|
+
polygon_inventory_delta=verification.polygon_inventory_delta if verification is not None else None,
|
|
749
|
+
).path
|
|
750
|
+
)
|
|
751
|
+
timings["total_s"] = perf_counter() - start
|
|
752
|
+
report = CompareReport(
|
|
753
|
+
status=policy_result.status,
|
|
754
|
+
exact_verification_complete=exact_complete,
|
|
755
|
+
old_input=old_input,
|
|
756
|
+
new_input=new_input,
|
|
757
|
+
semantics=config.to_json_dict(),
|
|
758
|
+
selection=selection,
|
|
759
|
+
backend=backend_info
|
|
760
|
+
| {
|
|
761
|
+
"exact_mode": False,
|
|
762
|
+
"fast_shortcuts_enabled": True,
|
|
763
|
+
"structural_guard": True,
|
|
764
|
+
"exact_verification_after_fast": verification is not None,
|
|
765
|
+
},
|
|
766
|
+
prefilter=prefilter.to_json(),
|
|
767
|
+
exact_fallback=exact_fallback,
|
|
768
|
+
layers=layers,
|
|
769
|
+
allowed_policy=policy_result.effective_policy,
|
|
770
|
+
unexpected_summary={
|
|
771
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
772
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
773
|
+
},
|
|
774
|
+
timings=timings,
|
|
775
|
+
warnings=warnings,
|
|
776
|
+
artifacts=artifacts,
|
|
777
|
+
command=command,
|
|
778
|
+
failure_policy=config.failure_policy,
|
|
779
|
+
)
|
|
780
|
+
if json_report_path is not None:
|
|
781
|
+
artifacts["json_report"] = str(json_report_path)
|
|
782
|
+
report = _report_with_artifacts(report, artifacts)
|
|
783
|
+
report.write_json(json_report_path)
|
|
784
|
+
return CompareRunResult(
|
|
785
|
+
status=policy_result.status,
|
|
786
|
+
report=report,
|
|
787
|
+
exact_result=exact_result,
|
|
788
|
+
policy_result=policy_result,
|
|
789
|
+
prefilter=prefilter,
|
|
790
|
+
windows=windows,
|
|
791
|
+
artifacts=artifacts,
|
|
792
|
+
timings=timings,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _try_native_geometry_guard_path(
|
|
797
|
+
config: CompareConfig,
|
|
798
|
+
*,
|
|
799
|
+
backend_info: dict[str, object],
|
|
800
|
+
old_top_name: str | None,
|
|
801
|
+
new_top_name: str | None,
|
|
802
|
+
command: tuple[str, ...],
|
|
803
|
+
json_report_path: str | Path | None,
|
|
804
|
+
markdown_path: str | Path | None,
|
|
805
|
+
exact_diff_markdown_path: str | Path | None,
|
|
806
|
+
verify_exact_after_fast: bool,
|
|
807
|
+
exact_component_backend: str,
|
|
808
|
+
start: float,
|
|
809
|
+
) -> CompareRunResult | None:
|
|
810
|
+
if config.allowed_regions or config.layers != "all":
|
|
811
|
+
return None
|
|
812
|
+
try:
|
|
813
|
+
from .backends.geometry_ctypes import compare_gds_geometry_native
|
|
814
|
+
|
|
815
|
+
guard_start = perf_counter()
|
|
816
|
+
native_compare = compare_gds_geometry_native(
|
|
817
|
+
config.old_path,
|
|
818
|
+
config.new_path,
|
|
819
|
+
old_top_name=old_top_name,
|
|
820
|
+
new_top_name=new_top_name,
|
|
821
|
+
)
|
|
822
|
+
guard_s = perf_counter() - guard_start
|
|
823
|
+
except Exception:
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
old_summary = native_compare.old_summary
|
|
827
|
+
new_summary = native_compare.new_summary
|
|
828
|
+
if native_compare.summaries_match:
|
|
829
|
+
return None
|
|
830
|
+
|
|
831
|
+
if old_summary.flattened_shape_count != new_summary.flattened_shape_count:
|
|
832
|
+
mismatch_kind = "count"
|
|
833
|
+
elif _geometry_layer_json(old_summary) != _geometry_layer_json(new_summary):
|
|
834
|
+
mismatch_kind = "layer-summary"
|
|
835
|
+
else:
|
|
836
|
+
mismatch_kind = "hash"
|
|
837
|
+
|
|
838
|
+
fragment = DiffFragment(points=((0, 0), (1, 0), (1, 1), (0, 1)), bbox=(0, 0, 1, 1), twice_area=2)
|
|
839
|
+
exact_result = ExactOracleResult((LayerExactDiff(layer=LayerSpec(0, 0), new_minus_old=(fragment,), xor=(fragment,)),))
|
|
840
|
+
policy_result = classify_exact_differences(exact_result, failure_policy=config.failure_policy)
|
|
841
|
+
fast_status = policy_result.status
|
|
842
|
+
exact_complete = False
|
|
843
|
+
verification: _FullExactVerification | None = None
|
|
844
|
+
if verify_exact_after_fast:
|
|
845
|
+
verification = _run_full_exact_verification(config, exact_component_backend=exact_component_backend)
|
|
846
|
+
exact_result = verification.exact_result
|
|
847
|
+
policy_result = verification.policy_result
|
|
848
|
+
exact_complete = True
|
|
849
|
+
|
|
850
|
+
timings = {
|
|
851
|
+
"extract_s": 0.0,
|
|
852
|
+
"exact_s": guard_s,
|
|
853
|
+
"fast_status_s": guard_s,
|
|
854
|
+
"native_geometry_guard_s": guard_s,
|
|
855
|
+
}
|
|
856
|
+
old_input = {"path": str(config.old_path), "selected_top": old_top_name, "native_geometry_summary": old_summary.to_json()}
|
|
857
|
+
new_input = {"path": str(config.new_path), "selected_top": new_top_name, "native_geometry_summary": new_summary.to_json()}
|
|
858
|
+
selection = {"old_top": old_top_name, "new_top": new_top_name, "layers": "all"}
|
|
859
|
+
layers = build_layer_reports(exact_result, policy_result=policy_result)
|
|
860
|
+
if verification is not None:
|
|
861
|
+
timings.update(
|
|
862
|
+
{
|
|
863
|
+
"extract_s": verification.extract_s,
|
|
864
|
+
"exact_s": verification.exact_s,
|
|
865
|
+
"exact_verification_s": verification.extract_s + verification.exact_s,
|
|
866
|
+
}
|
|
867
|
+
)
|
|
868
|
+
old_input = verification.old_input
|
|
869
|
+
new_input = verification.new_input
|
|
870
|
+
selection = verification.selection
|
|
871
|
+
layers = verification.layers
|
|
872
|
+
|
|
873
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
874
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
875
|
+
artifacts: dict[str, str] = {}
|
|
876
|
+
exact_fallback_mode = "native-geometry-fast-plus-full-exact" if verification is not None else "native-geometry-fail-closed"
|
|
877
|
+
warnings = (
|
|
878
|
+
"Native geometry guard used custom GDS parsing for a fail-closed fast status; full exact gdstk verification was run afterward and supplies polygon-level evidence."
|
|
879
|
+
if verification is not None
|
|
880
|
+
else "Native geometry guard used custom GDS parsing for a fail-closed fast status; run exact verification for polygon-level evidence."
|
|
881
|
+
)
|
|
882
|
+
exact_fallback = {
|
|
883
|
+
"mode": exact_fallback_mode,
|
|
884
|
+
"window_count": 0,
|
|
885
|
+
"escalated_layers": [],
|
|
886
|
+
"failures": [],
|
|
887
|
+
"fast_status": fast_status.value,
|
|
888
|
+
"old_flattened_shape_count": old_summary.flattened_shape_count,
|
|
889
|
+
"new_flattened_shape_count": new_summary.flattened_shape_count,
|
|
890
|
+
"old_geometry_hash": f"{old_summary.geometry_hash:016x}",
|
|
891
|
+
"new_geometry_hash": f"{new_summary.geometry_hash:016x}",
|
|
892
|
+
"geometry_mismatch_kind": mismatch_kind,
|
|
893
|
+
"old_layer_summary": _geometry_layer_json(old_summary),
|
|
894
|
+
"new_layer_summary": _geometry_layer_json(new_summary),
|
|
895
|
+
}
|
|
896
|
+
if verification is not None:
|
|
897
|
+
exact_fallback["exact_component_backend"] = verification.exact_component_backend
|
|
898
|
+
if markdown_path is not None:
|
|
899
|
+
markdown_path = Path(markdown_path)
|
|
900
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
901
|
+
markdown_path.write_text(
|
|
902
|
+
render_design_history_entry(
|
|
903
|
+
"GDS comparator native-geometry-guard result",
|
|
904
|
+
policy_result,
|
|
905
|
+
tests_run=("gdsdiff native-geometry-guard",),
|
|
906
|
+
notes=warnings,
|
|
907
|
+
),
|
|
908
|
+
encoding="utf-8",
|
|
909
|
+
)
|
|
910
|
+
artifacts["markdown"] = str(markdown_path)
|
|
911
|
+
if exact_diff_markdown_path is not None:
|
|
912
|
+
artifacts["exact_diff_markdown"] = str(
|
|
913
|
+
write_exact_diff_markdown(
|
|
914
|
+
policy_result,
|
|
915
|
+
exact_diff_markdown_path,
|
|
916
|
+
title="GDS comparator native-geometry-guard exact diff",
|
|
917
|
+
source_mode=exact_fallback_mode,
|
|
918
|
+
exact_verification_complete=exact_complete,
|
|
919
|
+
polygon_inventory_delta=verification.polygon_inventory_delta if verification is not None else None,
|
|
920
|
+
).path
|
|
921
|
+
)
|
|
922
|
+
timings["total_s"] = perf_counter() - start
|
|
923
|
+
report = CompareReport(
|
|
924
|
+
status=policy_result.status,
|
|
925
|
+
exact_verification_complete=exact_complete,
|
|
926
|
+
old_input=old_input,
|
|
927
|
+
new_input=new_input,
|
|
928
|
+
semantics=config.to_json_dict(),
|
|
929
|
+
selection=selection,
|
|
930
|
+
backend=backend_info
|
|
931
|
+
| {
|
|
932
|
+
"exact_mode": False,
|
|
933
|
+
"fast_shortcuts_enabled": True,
|
|
934
|
+
"native_geometry_guard": True,
|
|
935
|
+
"exact_verification_after_fast": verification is not None,
|
|
936
|
+
},
|
|
937
|
+
prefilter=prefilter.to_json(),
|
|
938
|
+
exact_fallback=exact_fallback,
|
|
939
|
+
layers=layers,
|
|
940
|
+
allowed_policy=policy_result.effective_policy,
|
|
941
|
+
unexpected_summary={
|
|
942
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
943
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
944
|
+
},
|
|
945
|
+
timings=timings,
|
|
946
|
+
warnings=warnings,
|
|
947
|
+
artifacts=artifacts,
|
|
948
|
+
command=command,
|
|
949
|
+
failure_policy=config.failure_policy,
|
|
950
|
+
)
|
|
951
|
+
if json_report_path is not None:
|
|
952
|
+
artifacts["json_report"] = str(json_report_path)
|
|
953
|
+
report = _report_with_artifacts(report, artifacts)
|
|
954
|
+
report.write_json(json_report_path)
|
|
955
|
+
return CompareRunResult(
|
|
956
|
+
status=policy_result.status,
|
|
957
|
+
report=report,
|
|
958
|
+
exact_result=exact_result,
|
|
959
|
+
policy_result=policy_result,
|
|
960
|
+
prefilter=prefilter,
|
|
961
|
+
windows=windows,
|
|
962
|
+
artifacts=artifacts,
|
|
963
|
+
timings=timings,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _try_gpu_polygon_evidence_path(
|
|
968
|
+
config: CompareConfig,
|
|
969
|
+
*,
|
|
970
|
+
backend_info: dict[str, object],
|
|
971
|
+
old_top_name: str | None,
|
|
972
|
+
new_top_name: str | None,
|
|
973
|
+
command: tuple[str, ...],
|
|
974
|
+
json_report_path: str | Path | None,
|
|
975
|
+
markdown_path: str | Path | None,
|
|
976
|
+
exact_diff_markdown_path: str | Path | None,
|
|
977
|
+
audit_cpu_oracle: bool,
|
|
978
|
+
exact_component_backend: str,
|
|
979
|
+
start: float,
|
|
980
|
+
) -> CompareRunResult:
|
|
981
|
+
artifacts: dict[str, str] = {}
|
|
982
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
983
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
984
|
+
record_delta = None
|
|
985
|
+
coverage_result = None
|
|
986
|
+
surplus_result = None
|
|
987
|
+
surplus_probe_source = None
|
|
988
|
+
polygon_fragment_exact_result = None
|
|
989
|
+
polygon_component_area = None
|
|
990
|
+
polygon_inventory_delta = None
|
|
991
|
+
source_edge_cpu_fallback = None
|
|
992
|
+
source_edge_cpu_verification = None
|
|
993
|
+
native_parse_cpu_verification = None
|
|
994
|
+
|
|
995
|
+
cpu_neutral_fallback = backend_info.get("name") == Backend.CPU.value
|
|
996
|
+
gpu_runtime_available = bool(backend_info.get("cuda_ctypes_available") or backend_info.get("metal_ctypes_available"))
|
|
997
|
+
if cpu_neutral_fallback or (audit_cpu_oracle and not gpu_runtime_available):
|
|
998
|
+
audit_start = perf_counter()
|
|
999
|
+
audit_backend = _default_cpu_audit_component_backend(exact_component_backend)
|
|
1000
|
+
verification = _run_full_exact_verification(
|
|
1001
|
+
config,
|
|
1002
|
+
exact_component_backend=audit_backend,
|
|
1003
|
+
force_small_component_partition=True,
|
|
1004
|
+
)
|
|
1005
|
+
timings = {
|
|
1006
|
+
"native_parse_s": 0.0,
|
|
1007
|
+
"h2d_upload_s": 0.0,
|
|
1008
|
+
"gpu_broadphase_s": 0.0,
|
|
1009
|
+
"gpu_xor_s": 0.0,
|
|
1010
|
+
"gpu_record_emit_s": 0.0,
|
|
1011
|
+
"markdown_format_s": 0.0,
|
|
1012
|
+
"cpu_oracle_audit_s": perf_counter() - audit_start,
|
|
1013
|
+
"extract_s": verification.extract_s,
|
|
1014
|
+
"exact_s": verification.exact_s,
|
|
1015
|
+
}
|
|
1016
|
+
policy_result = verification.policy_result
|
|
1017
|
+
exact_result = verification.exact_result
|
|
1018
|
+
polygon_inventory_delta = verification.polygon_inventory_delta
|
|
1019
|
+
exact_complete = True
|
|
1020
|
+
warnings = (
|
|
1021
|
+
(
|
|
1022
|
+
"No suitable GPU backend was selected; CPU gdstk oracle produced complete neutral exact evidence."
|
|
1023
|
+
if cpu_neutral_fallback
|
|
1024
|
+
else "Requested gpu-polygon evidence engine is not implemented yet; CPU gdstk oracle audit was run explicitly via --audit-cpu-oracle."
|
|
1025
|
+
),
|
|
1026
|
+
)
|
|
1027
|
+
exact_fallback = {
|
|
1028
|
+
"mode": (
|
|
1029
|
+
"cpu-gdstk-oracle-neutral-fallback"
|
|
1030
|
+
if cpu_neutral_fallback
|
|
1031
|
+
else "gpu-polygon-unavailable-plus-cpu-oracle-audit"
|
|
1032
|
+
),
|
|
1033
|
+
"engine": "gdstk-oracle" if cpu_neutral_fallback else "gpu-polygon",
|
|
1034
|
+
"engine_status": "complete" if cpu_neutral_fallback else "not-implemented",
|
|
1035
|
+
"audit_cpu_oracle": not cpu_neutral_fallback,
|
|
1036
|
+
"window_count": 0,
|
|
1037
|
+
"escalated_layers": [],
|
|
1038
|
+
"failures": []
|
|
1039
|
+
if cpu_neutral_fallback
|
|
1040
|
+
else [
|
|
1041
|
+
{
|
|
1042
|
+
"reason": "gpu-polygon exact numeric diff record engine is not implemented yet",
|
|
1043
|
+
}
|
|
1044
|
+
],
|
|
1045
|
+
"exact_component_backend": verification.exact_component_backend,
|
|
1046
|
+
}
|
|
1047
|
+
old_input = verification.old_input
|
|
1048
|
+
new_input = verification.new_input
|
|
1049
|
+
selection = verification.selection
|
|
1050
|
+
layers = verification.layers
|
|
1051
|
+
if exact_diff_markdown_path is not None:
|
|
1052
|
+
markdown_start = perf_counter()
|
|
1053
|
+
markdown_written = write_exact_diff_markdown(
|
|
1054
|
+
policy_result,
|
|
1055
|
+
exact_diff_markdown_path,
|
|
1056
|
+
title=(
|
|
1057
|
+
"GDS comparator CPU fallback exact diff"
|
|
1058
|
+
if cpu_neutral_fallback
|
|
1059
|
+
else "GDS comparator CPU-audited gpu-polygon exact diff"
|
|
1060
|
+
),
|
|
1061
|
+
source_mode=exact_fallback["mode"],
|
|
1062
|
+
exact_verification_complete=True,
|
|
1063
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1064
|
+
)
|
|
1065
|
+
_record_exact_markdown_artifacts(artifacts, markdown_written)
|
|
1066
|
+
timings["markdown_format_s"] = perf_counter() - markdown_start
|
|
1067
|
+
else:
|
|
1068
|
+
parse_start = perf_counter()
|
|
1069
|
+
parse_failures: list[dict[str, str]] = []
|
|
1070
|
+
record_delta_failures: list[dict[str, str]] = []
|
|
1071
|
+
coverage_failures: list[dict[str, str]] = []
|
|
1072
|
+
component_area_failures: list[dict[str, str]] = []
|
|
1073
|
+
old_native = None
|
|
1074
|
+
new_native = None
|
|
1075
|
+
native_cache_timings: dict[str, object] = {}
|
|
1076
|
+
try:
|
|
1077
|
+
old_native, new_native, native_cache_timings = _gpu_polygon_native_inputs(
|
|
1078
|
+
config,
|
|
1079
|
+
old_top_name=old_top_name,
|
|
1080
|
+
new_top_name=new_top_name,
|
|
1081
|
+
)
|
|
1082
|
+
except PolygonExtractCtypesUnavailable as exc:
|
|
1083
|
+
common_top = _resolve_single_common_top_after_native_parse_failure(config, str(exc), old_top_name, new_top_name)
|
|
1084
|
+
if common_top is None:
|
|
1085
|
+
parse_failures.append({"stage": "native-parse", "reason": str(exc)})
|
|
1086
|
+
else:
|
|
1087
|
+
old_top_name = common_top
|
|
1088
|
+
new_top_name = common_top
|
|
1089
|
+
try:
|
|
1090
|
+
old_native, new_native, native_cache_timings = _gpu_polygon_native_inputs(
|
|
1091
|
+
config,
|
|
1092
|
+
old_top_name=old_top_name,
|
|
1093
|
+
new_top_name=new_top_name,
|
|
1094
|
+
)
|
|
1095
|
+
except PolygonExtractCtypesUnavailable as retry_exc:
|
|
1096
|
+
parse_failures.append({"stage": "native-parse", "reason": str(retry_exc)})
|
|
1097
|
+
native_parse_s = perf_counter() - parse_start
|
|
1098
|
+
exact_result = ExactOracleResult(())
|
|
1099
|
+
policy_result = classify_exact_differences(exact_result, config.allowed_regions, failure_policy=config.failure_policy)
|
|
1100
|
+
exact_complete = False
|
|
1101
|
+
timings = {
|
|
1102
|
+
"native_parse_s": native_parse_s,
|
|
1103
|
+
"h2d_upload_s": 0.0,
|
|
1104
|
+
"gpu_broadphase_s": 0.0,
|
|
1105
|
+
"gpu_xor_s": 0.0,
|
|
1106
|
+
"gpu_record_emit_s": 0.0,
|
|
1107
|
+
"markdown_format_s": 0.0,
|
|
1108
|
+
"cpu_oracle_audit_s": 0.0,
|
|
1109
|
+
"gpu_source_record_delta_s": 0.0,
|
|
1110
|
+
"canonical_surplus_coverage_s": 0.0,
|
|
1111
|
+
"record_delta_surplus_coverage_s": 0.0,
|
|
1112
|
+
"polygon_component_build_s": 0.0,
|
|
1113
|
+
"polygon_context_build_s": 0.0,
|
|
1114
|
+
"gpu_context_index_s": 0.0,
|
|
1115
|
+
"gpu_edge_cancel_split_s": 0.0,
|
|
1116
|
+
"gpu_polygon_component_area_s": 0.0,
|
|
1117
|
+
"gpu_polygon_component_fragment_s": 0.0,
|
|
1118
|
+
"boundary_loop_assembly_s": 0.0,
|
|
1119
|
+
"cuda_release_s": 0.0,
|
|
1120
|
+
"metal_release_s": 0.0,
|
|
1121
|
+
"polygon_inventory_delta_s": 0.0,
|
|
1122
|
+
}
|
|
1123
|
+
timings.update(native_cache_timings)
|
|
1124
|
+
gpu_name = backend_info.get("name")
|
|
1125
|
+
gpu_rect_available = (
|
|
1126
|
+
(gpu_name == Backend.CUDA.value and backend_info.get("cuda_ctypes_available"))
|
|
1127
|
+
or (gpu_name == Backend.METAL.value and backend_info.get("metal_ctypes_available"))
|
|
1128
|
+
)
|
|
1129
|
+
if parse_failures and audit_cpu_oracle:
|
|
1130
|
+
audit_start = perf_counter()
|
|
1131
|
+
audit_backend = _default_cpu_audit_component_backend(exact_component_backend)
|
|
1132
|
+
native_parse_cpu_verification = _run_full_exact_verification(
|
|
1133
|
+
config,
|
|
1134
|
+
exact_component_backend=audit_backend,
|
|
1135
|
+
force_small_component_partition=True,
|
|
1136
|
+
)
|
|
1137
|
+
timings["cpu_oracle_audit_s"] = perf_counter() - audit_start
|
|
1138
|
+
timings["extract_s"] = native_parse_cpu_verification.extract_s
|
|
1139
|
+
timings["exact_s"] = native_parse_cpu_verification.exact_s
|
|
1140
|
+
exact_result = native_parse_cpu_verification.exact_result
|
|
1141
|
+
policy_result = native_parse_cpu_verification.policy_result
|
|
1142
|
+
layers = native_parse_cpu_verification.layers
|
|
1143
|
+
polygon_inventory_delta = native_parse_cpu_verification.polygon_inventory_delta
|
|
1144
|
+
exact_complete = True
|
|
1145
|
+
if not parse_failures and old_native is not None and new_native is not None and gpu_rect_available:
|
|
1146
|
+
if _native_buffers_rectangle_coverage_eligible(old_native.buffer, new_native.buffer):
|
|
1147
|
+
try:
|
|
1148
|
+
coverage_result = (
|
|
1149
|
+
run_metal_rectangle_coverage_diff(old_native.buffer, new_native.buffer)
|
|
1150
|
+
if gpu_name == Backend.METAL.value
|
|
1151
|
+
else run_cuda_rectangle_coverage_diff(old_native.buffer, new_native.buffer)
|
|
1152
|
+
)
|
|
1153
|
+
timings["h2d_upload_s"] = coverage_result.stats.h2d_upload_s
|
|
1154
|
+
timings["gpu_xor_s"] = coverage_result.stats.gpu_xor_s
|
|
1155
|
+
timings["gpu_record_emit_s"] = coverage_result.stats.gpu_record_emit_s
|
|
1156
|
+
except RectCoverageUnsupported as exc:
|
|
1157
|
+
coverage_failures.append({"stage": "gpu-coverage-xor", "reason": str(exc)})
|
|
1158
|
+
else:
|
|
1159
|
+
coverage_failures.append(
|
|
1160
|
+
{
|
|
1161
|
+
"stage": "gpu-coverage-xor",
|
|
1162
|
+
"reason": "skipped rectangle coverage because native polygon vertex counts include non-rectangles",
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
if coverage_result is None and gpu_name in {Backend.CUDA.value, Backend.METAL.value}:
|
|
1166
|
+
try:
|
|
1167
|
+
from .backends.cuda_ctypes import CudaCtypesUnavailable, run_cuda_polygon_record_delta
|
|
1168
|
+
from .backends.metal_ctypes import MetalCtypesUnavailable, run_metal_polygon_record_delta_from_hashes
|
|
1169
|
+
|
|
1170
|
+
record_start = perf_counter()
|
|
1171
|
+
if gpu_name == Backend.METAL.value:
|
|
1172
|
+
record_delta = run_metal_polygon_record_delta_from_hashes(old_native.buffer, new_native.buffer)
|
|
1173
|
+
else:
|
|
1174
|
+
record_delta = run_cuda_polygon_record_delta(old_native.buffer, new_native.buffer)
|
|
1175
|
+
timings["gpu_source_record_delta_s"] = perf_counter() - record_start
|
|
1176
|
+
except (CudaCtypesUnavailable, MetalCtypesUnavailable) as record_exc:
|
|
1177
|
+
record_delta_failures.append({"stage": "gpu-record-delta", "reason": str(record_exc)})
|
|
1178
|
+
if record_delta is not None:
|
|
1179
|
+
surplus_start = perf_counter()
|
|
1180
|
+
surplus_result = run_record_delta_surplus_coverage_probe(old_native.buffer, new_native.buffer, record_delta)
|
|
1181
|
+
surplus_probe_source = f"{_record_delta_accelerator(record_delta)}-record-delta"
|
|
1182
|
+
timings["record_delta_surplus_coverage_s"] = perf_counter() - surplus_start
|
|
1183
|
+
component_area_start = perf_counter()
|
|
1184
|
+
component_build_start = perf_counter()
|
|
1185
|
+
component_build = build_record_delta_surplus_components(old_native.buffer, new_native.buffer, record_delta)
|
|
1186
|
+
timings["polygon_component_build_s"] = perf_counter() - component_build_start
|
|
1187
|
+
context_build_start = perf_counter()
|
|
1188
|
+
context_build = add_overlapping_context_to_components(
|
|
1189
|
+
old_native.buffer,
|
|
1190
|
+
new_native.buffer,
|
|
1191
|
+
component_build.components,
|
|
1192
|
+
preferred_accelerator=_record_delta_accelerator(record_delta),
|
|
1193
|
+
)
|
|
1194
|
+
timings["polygon_context_build_s"] = perf_counter() - context_build_start
|
|
1195
|
+
if context_build.stats.context_index_backend.startswith(("cuda", "metal")):
|
|
1196
|
+
timings["gpu_context_index_s"] = context_build.stats.context_index_s
|
|
1197
|
+
assembly_start = perf_counter()
|
|
1198
|
+
source_edge_result = exact_result_from_source_edges(context_build.components)
|
|
1199
|
+
polygon_fragment_exact_result = source_edge_result.exact_result
|
|
1200
|
+
timings["boundary_loop_assembly_s"] = perf_counter() - assembly_start
|
|
1201
|
+
timings["gpu_edge_cancel_split_s"] = source_edge_result.stats.layer_merge_cuda_edge_cancel_s
|
|
1202
|
+
timings["gpu_polygon_component_area_s"] = perf_counter() - component_area_start
|
|
1203
|
+
source_edge_old_minus = sum(
|
|
1204
|
+
layer.old_minus_new_twice_area for layer in polygon_fragment_exact_result.layers
|
|
1205
|
+
)
|
|
1206
|
+
source_edge_new_minus = sum(
|
|
1207
|
+
layer.new_minus_old_twice_area for layer in polygon_fragment_exact_result.layers
|
|
1208
|
+
)
|
|
1209
|
+
if source_edge_result.stats.nested_direction_fragment_component_count:
|
|
1210
|
+
fallback_start = perf_counter()
|
|
1211
|
+
source_edge_cpu_verification = _run_full_exact_verification(
|
|
1212
|
+
config,
|
|
1213
|
+
exact_component_backend="gdstk",
|
|
1214
|
+
)
|
|
1215
|
+
timings["source_edge_cpu_oracle_fallback_s"] = perf_counter() - fallback_start
|
|
1216
|
+
source_edge_cpu_fallback = {
|
|
1217
|
+
"trigger": "nested-direction-fragments",
|
|
1218
|
+
"reason": (
|
|
1219
|
+
"source-edge loops contain nested same-direction fragments, "
|
|
1220
|
+
"which require hole-aware Boolean semantics"
|
|
1221
|
+
),
|
|
1222
|
+
"nested_direction_fragment_component_count": (
|
|
1223
|
+
source_edge_result.stats.nested_direction_fragment_component_count
|
|
1224
|
+
),
|
|
1225
|
+
"original_vs_cpu_oracle": _exact_result_audit_summary(
|
|
1226
|
+
polygon_fragment_exact_result,
|
|
1227
|
+
source_edge_cpu_verification.exact_result,
|
|
1228
|
+
),
|
|
1229
|
+
"exact_component_backend": source_edge_cpu_verification.exact_component_backend,
|
|
1230
|
+
}
|
|
1231
|
+
polygon_fragment_exact_result = source_edge_cpu_verification.exact_result
|
|
1232
|
+
source_edge_old_minus = sum(
|
|
1233
|
+
layer.old_minus_new_twice_area for layer in polygon_fragment_exact_result.layers
|
|
1234
|
+
)
|
|
1235
|
+
source_edge_new_minus = sum(
|
|
1236
|
+
layer.new_minus_old_twice_area for layer in polygon_fragment_exact_result.layers
|
|
1237
|
+
)
|
|
1238
|
+
polygon_component_area = {
|
|
1239
|
+
"component_count": len(component_build.components),
|
|
1240
|
+
"context_component_count": len(context_build.components),
|
|
1241
|
+
"context_max_fragments_per_component": context_build.stats.max_fragments_per_component,
|
|
1242
|
+
"context_max_vertices_per_component": context_build.stats.max_vertices_per_component,
|
|
1243
|
+
"context_index_backend": context_build.stats.context_index_backend,
|
|
1244
|
+
"context_index_s": context_build.stats.context_index_s,
|
|
1245
|
+
"context_pair_count": context_build.stats.context_pair_count,
|
|
1246
|
+
"context_cuda_kernel_count": context_build.stats.context_cuda_kernel_count,
|
|
1247
|
+
"context_cuda_event_elapsed_s": context_build.stats.context_cuda_event_elapsed_s,
|
|
1248
|
+
"context_cuda_transferred_bytes": context_build.stats.context_cuda_transferred_bytes,
|
|
1249
|
+
"context_metal_kernel_count": context_build.stats.context_metal_kernel_count,
|
|
1250
|
+
"context_metal_event_elapsed_s": context_build.stats.context_metal_event_elapsed_s,
|
|
1251
|
+
"context_metal_transferred_bytes": context_build.stats.context_metal_transferred_bytes,
|
|
1252
|
+
"emitted_trapezoid_fragment_count": 0,
|
|
1253
|
+
"cuda_trapezoid_old_minus_new_twice_area": None,
|
|
1254
|
+
"cuda_trapezoid_new_minus_old_twice_area": None,
|
|
1255
|
+
"cuda_trapezoid_xor_twice_area": None,
|
|
1256
|
+
"old_minus_new_twice_area": source_edge_old_minus,
|
|
1257
|
+
"new_minus_old_twice_area": source_edge_new_minus,
|
|
1258
|
+
"xor_twice_area": source_edge_old_minus + source_edge_new_minus,
|
|
1259
|
+
"uses_overlapping_context": True,
|
|
1260
|
+
"gpu_trapezoid_fragments_available": False,
|
|
1261
|
+
"gpu_source_edge_records_available": True,
|
|
1262
|
+
"merged_boundary_loop_records_available": True,
|
|
1263
|
+
"source_edge_cpu_oracle_fallback": source_edge_cpu_fallback,
|
|
1264
|
+
"boundary_loop_assembly_backend": (
|
|
1265
|
+
"mixed-gpu-boundary-loop-records-plus-host-fallback"
|
|
1266
|
+
if source_edge_result.stats.cuda_loop_component_count and source_edge_result.stats.metal_loop_component_count
|
|
1267
|
+
else
|
|
1268
|
+
"metal-boundary-loop-records-plus-host-fallback"
|
|
1269
|
+
if source_edge_result.stats.metal_loop_component_count
|
|
1270
|
+
else
|
|
1271
|
+
"cuda-boundary-loop-records-plus-host-fallback"
|
|
1272
|
+
if source_edge_result.stats.cuda_loop_component_count
|
|
1273
|
+
else
|
|
1274
|
+
"native-boundary-loop-ctypes-plus-python-fallback"
|
|
1275
|
+
if source_edge_result.stats.native_loop_component_count
|
|
1276
|
+
else "cpu-source-edge-arrangement-prototype"
|
|
1277
|
+
),
|
|
1278
|
+
"exact_boundary_loops_available": True,
|
|
1279
|
+
"source_edge_arrangement": source_edge_result.stats.to_json(),
|
|
1280
|
+
}
|
|
1281
|
+
else:
|
|
1282
|
+
surplus_start = perf_counter()
|
|
1283
|
+
surplus_result = run_canonical_surplus_coverage_probe(old_native.buffer, new_native.buffer)
|
|
1284
|
+
surplus_probe_source = "cpu-canonical"
|
|
1285
|
+
timings["canonical_surplus_coverage_s"] = perf_counter() - surplus_start
|
|
1286
|
+
if coverage_result is not None:
|
|
1287
|
+
exact_result = coverage_result.exact_result
|
|
1288
|
+
policy_result = classify_exact_differences(exact_result, config.allowed_regions, failure_policy=config.failure_policy)
|
|
1289
|
+
exact_complete = True
|
|
1290
|
+
layers = build_layer_reports(
|
|
1291
|
+
exact_result,
|
|
1292
|
+
policy_result=policy_result,
|
|
1293
|
+
presence_by_layer=_presence_by_layer(old_native.buffer, new_native.buffer),
|
|
1294
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_native.buffer, new_native.buffer),
|
|
1295
|
+
exact_status="complete",
|
|
1296
|
+
)
|
|
1297
|
+
elif polygon_fragment_exact_result is not None:
|
|
1298
|
+
exact_result = polygon_fragment_exact_result
|
|
1299
|
+
policy_result = classify_exact_differences(exact_result, config.allowed_regions, failure_policy=config.failure_policy)
|
|
1300
|
+
exact_complete = True
|
|
1301
|
+
layers = build_layer_reports(
|
|
1302
|
+
exact_result,
|
|
1303
|
+
policy_result=policy_result,
|
|
1304
|
+
presence_by_layer=_presence_by_layer(old_native.buffer, new_native.buffer),
|
|
1305
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_native.buffer, new_native.buffer),
|
|
1306
|
+
exact_status="gpu-polygon-source-edge-records",
|
|
1307
|
+
)
|
|
1308
|
+
elif surplus_result is not None:
|
|
1309
|
+
exact_result = surplus_result.exact_result
|
|
1310
|
+
policy_result = classify_exact_differences(exact_result, config.allowed_regions, failure_policy=config.failure_policy)
|
|
1311
|
+
exact_complete = surplus_result.stats.coverage_exact_proven
|
|
1312
|
+
layers = build_layer_reports(
|
|
1313
|
+
exact_result,
|
|
1314
|
+
policy_result=policy_result,
|
|
1315
|
+
presence_by_layer=_presence_by_layer(old_native.buffer, new_native.buffer),
|
|
1316
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_native.buffer, new_native.buffer),
|
|
1317
|
+
exact_status="complete" if surplus_result.stats.coverage_exact_proven else "canonical-source-surplus-probe",
|
|
1318
|
+
)
|
|
1319
|
+
else:
|
|
1320
|
+
layers = ()
|
|
1321
|
+
if old_native is not None and new_native is not None:
|
|
1322
|
+
inventory_start = perf_counter()
|
|
1323
|
+
if record_delta is not None:
|
|
1324
|
+
polygon_inventory_delta = build_polygon_inventory_delta_from_record_delta(
|
|
1325
|
+
old_native.buffer,
|
|
1326
|
+
new_native.buffer,
|
|
1327
|
+
record_delta,
|
|
1328
|
+
)
|
|
1329
|
+
timings["polygon_inventory_delta_source"] = f"{_record_delta_accelerator(record_delta)}-record-delta"
|
|
1330
|
+
else:
|
|
1331
|
+
polygon_inventory_delta = build_polygon_inventory_delta(old_native.buffer, new_native.buffer)
|
|
1332
|
+
timings["polygon_inventory_delta_source"] = "cpu-full-canonical"
|
|
1333
|
+
timings["polygon_inventory_delta_s"] = perf_counter() - inventory_start
|
|
1334
|
+
coverage_accelerator = coverage_result.stats.accelerator if coverage_result is not None else None
|
|
1335
|
+
record_delta_accelerator = _record_delta_accelerator(record_delta) if record_delta is not None else None
|
|
1336
|
+
warning_text = (
|
|
1337
|
+
f"{str(coverage_accelerator).upper()} rectangle coverage engine produced exact gpu-polygon evidence for rectangle-only input; arbitrary non-rectangle polygon XOR is still unsupported."
|
|
1338
|
+
if coverage_result is not None
|
|
1339
|
+
else f"{str(record_delta_accelerator).upper()} source-edge evidence detected hole-like topology; final exact evidence was corrected with the CPU gdstk oracle while GPU source-record and context evidence were retained."
|
|
1340
|
+
if polygon_fragment_exact_result is not None and source_edge_cpu_fallback is not None
|
|
1341
|
+
else f"{str(record_delta_accelerator).upper()} source-edge evidence path produced exact boundary records with GPU source-record deltas and GPU context indexing where supported; CPU retains loop stitching, hole handling, and Markdown formatting."
|
|
1342
|
+
if polygon_fragment_exact_result is not None
|
|
1343
|
+
else f"{str(gpu_name).upper()} native polygon extraction failed; CPU gdstk oracle produced complete exact evidence for this audit run."
|
|
1344
|
+
if native_parse_cpu_verification is not None
|
|
1345
|
+
else "Canonical source-surplus probe produced exact directional evidence because changed source polygons are independent; arbitrary overlapping polygon XOR remains unsupported."
|
|
1346
|
+
if surplus_result is not None and surplus_result.stats.coverage_exact_proven
|
|
1347
|
+
else "Canonical source-surplus probe emitted directional component records, but the run remains fail-closed because overlapping surplus polygons require the general GPU polygon XOR engine."
|
|
1348
|
+
if surplus_result is not None
|
|
1349
|
+
else
|
|
1350
|
+
"Requested gpu-polygon evidence engine emitted GPU source-record deltas but is fail-closed because the general polygon coverage XOR engine is not implemented yet."
|
|
1351
|
+
if record_delta is not None
|
|
1352
|
+
else "Requested gpu-polygon evidence engine is fail-closed because the general polygon GPU XOR record engine is not implemented yet."
|
|
1353
|
+
if not parse_failures
|
|
1354
|
+
else "Requested gpu-polygon evidence engine is fail-closed because native polygon parsing failed before GPU XOR."
|
|
1355
|
+
)
|
|
1356
|
+
warnings = (warning_text,)
|
|
1357
|
+
exact_fallback = {
|
|
1358
|
+
"mode": f"gpu-polygon-{coverage_accelerator}-rect-coverage"
|
|
1359
|
+
if coverage_result is not None and coverage_accelerator is not None
|
|
1360
|
+
else f"gpu-polygon-{record_delta_accelerator}-source-edge-cpu-oracle-fallback"
|
|
1361
|
+
if polygon_fragment_exact_result is not None and source_edge_cpu_fallback is not None
|
|
1362
|
+
else f"gpu-polygon-{record_delta_accelerator}-source-edge-records"
|
|
1363
|
+
if polygon_fragment_exact_result is not None
|
|
1364
|
+
else f"gpu-polygon-{gpu_name}-native-parse-cpu-oracle-fallback"
|
|
1365
|
+
if native_parse_cpu_verification is not None
|
|
1366
|
+
else "canonical-source-surplus-exact"
|
|
1367
|
+
if surplus_result is not None and surplus_result.stats.coverage_exact_proven
|
|
1368
|
+
else "canonical-source-surplus-probe"
|
|
1369
|
+
if surplus_result is not None
|
|
1370
|
+
else f"gpu-polygon-{record_delta_accelerator}-record-delta-no-coverage-xor"
|
|
1371
|
+
if record_delta is not None
|
|
1372
|
+
else "gpu-polygon-unavailable",
|
|
1373
|
+
"engine": "gpu-polygon",
|
|
1374
|
+
"engine_status": "rect-coverage-complete"
|
|
1375
|
+
if coverage_result is not None
|
|
1376
|
+
else "source-edge-cpu-oracle-corrected"
|
|
1377
|
+
if polygon_fragment_exact_result is not None and source_edge_cpu_fallback is not None
|
|
1378
|
+
else "source-edge-boundary-records-complete"
|
|
1379
|
+
if polygon_fragment_exact_result is not None
|
|
1380
|
+
else "native-parse-cpu-oracle-corrected"
|
|
1381
|
+
if native_parse_cpu_verification is not None
|
|
1382
|
+
else "canonical-source-surplus-exact"
|
|
1383
|
+
if surplus_result is not None and surplus_result.stats.coverage_exact_proven
|
|
1384
|
+
else "canonical-source-surplus-probe-only"
|
|
1385
|
+
if surplus_result is not None
|
|
1386
|
+
else "record-delta-only"
|
|
1387
|
+
if record_delta is not None
|
|
1388
|
+
else "not-implemented"
|
|
1389
|
+
if not parse_failures
|
|
1390
|
+
else "native-parse-failed",
|
|
1391
|
+
"audit_cpu_oracle": False,
|
|
1392
|
+
"window_count": 0,
|
|
1393
|
+
"escalated_layers": [],
|
|
1394
|
+
"failures": parse_failures
|
|
1395
|
+
+ record_delta_failures
|
|
1396
|
+
+ component_area_failures
|
|
1397
|
+
+ ([] if coverage_result is not None or polygon_fragment_exact_result is not None or surplus_result is not None else coverage_failures)
|
|
1398
|
+
+ (
|
|
1399
|
+
[]
|
|
1400
|
+
if coverage_result is not None or polygon_fragment_exact_result is not None or (surplus_result is not None and surplus_result.stats.coverage_exact_proven)
|
|
1401
|
+
else [
|
|
1402
|
+
{
|
|
1403
|
+
"reason": "gpu-polygon coverage XOR engine is not implemented yet",
|
|
1404
|
+
}
|
|
1405
|
+
]
|
|
1406
|
+
),
|
|
1407
|
+
"old_polygon_count": old_native.buffer.polygon_count if old_native is not None else None,
|
|
1408
|
+
"new_polygon_count": new_native.buffer.polygon_count if new_native is not None else None,
|
|
1409
|
+
"old_vertex_count": old_native.buffer.vertex_count if old_native is not None else None,
|
|
1410
|
+
"new_vertex_count": new_native.buffer.vertex_count if new_native is not None else None,
|
|
1411
|
+
"gpu_record_delta_count": record_delta.record_count if record_delta is not None else 0,
|
|
1412
|
+
"gpu_record_delta_matched_count": record_delta.matched_record_count if record_delta is not None else 0,
|
|
1413
|
+
"gpu_record_delta_reduced_key_count": record_delta.reduced_key_count if record_delta is not None else 0,
|
|
1414
|
+
"gpu_record_delta_accelerator": record_delta_accelerator,
|
|
1415
|
+
"gpu_coverage": coverage_result.stats.to_json() if coverage_result is not None else None,
|
|
1416
|
+
"canonical_surplus_coverage": surplus_result.stats.to_json() if surplus_result is not None else None,
|
|
1417
|
+
"surplus_probe_source": surplus_probe_source,
|
|
1418
|
+
"gpu_polygon_component_area": polygon_component_area,
|
|
1419
|
+
"source_edge_cpu_oracle_fallback": source_edge_cpu_fallback,
|
|
1420
|
+
}
|
|
1421
|
+
if native_parse_cpu_verification is not None:
|
|
1422
|
+
exact_fallback["audit_cpu_oracle"] = True
|
|
1423
|
+
exact_fallback["cpu_oracle_audit"] = _exact_result_audit_summary(
|
|
1424
|
+
exact_result,
|
|
1425
|
+
native_parse_cpu_verification.exact_result,
|
|
1426
|
+
)
|
|
1427
|
+
exact_fallback["exact_component_backend"] = native_parse_cpu_verification.exact_component_backend
|
|
1428
|
+
exact_fallback["native_parse_cpu_oracle_fallback"] = {
|
|
1429
|
+
"trigger": "native-polygon-parse-failed",
|
|
1430
|
+
"reason": "GPU native polygon extraction failed before acceleration; CPU gdstk oracle supplied complete exact evidence.",
|
|
1431
|
+
"failures": parse_failures,
|
|
1432
|
+
"exact_component_backend": native_parse_cpu_verification.exact_component_backend,
|
|
1433
|
+
}
|
|
1434
|
+
if backend_info.get("cuda_ctypes_available"):
|
|
1435
|
+
warnings = _release_cuda_device_memory_after_gpu_operation(timings, warnings)
|
|
1436
|
+
if backend_info.get("metal_ctypes_available"):
|
|
1437
|
+
warnings = _release_metal_device_memory_after_gpu_operation(timings, warnings)
|
|
1438
|
+
if audit_cpu_oracle and exact_complete:
|
|
1439
|
+
audit_start = perf_counter()
|
|
1440
|
+
audit_backend = _default_cpu_audit_component_backend(exact_component_backend)
|
|
1441
|
+
verification = source_edge_cpu_verification or _run_full_exact_verification(
|
|
1442
|
+
config,
|
|
1443
|
+
exact_component_backend=audit_backend,
|
|
1444
|
+
force_small_component_partition=True,
|
|
1445
|
+
)
|
|
1446
|
+
timings["cpu_oracle_audit_s"] = perf_counter() - audit_start
|
|
1447
|
+
audit_summary = _exact_result_audit_summary(exact_result, verification.exact_result)
|
|
1448
|
+
exact_fallback["audit_cpu_oracle"] = True
|
|
1449
|
+
exact_fallback["exact_component_backend"] = verification.exact_component_backend
|
|
1450
|
+
if (
|
|
1451
|
+
not audit_summary["match"]
|
|
1452
|
+
and coverage_result is None
|
|
1453
|
+
and polygon_fragment_exact_result is not None
|
|
1454
|
+
):
|
|
1455
|
+
source_edge_cpu_fallback = {
|
|
1456
|
+
"trigger": "cpu-oracle-audit-mismatch",
|
|
1457
|
+
"reason": (
|
|
1458
|
+
"source-edge evidence did not match the CPU gdstk exact oracle; "
|
|
1459
|
+
"final exact evidence was promoted to the CPU oracle result"
|
|
1460
|
+
),
|
|
1461
|
+
"original_vs_cpu_oracle": audit_summary,
|
|
1462
|
+
"exact_component_backend": verification.exact_component_backend,
|
|
1463
|
+
}
|
|
1464
|
+
exact_result = verification.exact_result
|
|
1465
|
+
polygon_fragment_exact_result = exact_result
|
|
1466
|
+
policy_result = classify_exact_differences(
|
|
1467
|
+
exact_result,
|
|
1468
|
+
config.allowed_regions,
|
|
1469
|
+
failure_policy=config.failure_policy,
|
|
1470
|
+
)
|
|
1471
|
+
layers = build_layer_reports(
|
|
1472
|
+
exact_result,
|
|
1473
|
+
policy_result=policy_result,
|
|
1474
|
+
presence_by_layer=_presence_by_layer(old_native.buffer, new_native.buffer),
|
|
1475
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_native.buffer, new_native.buffer),
|
|
1476
|
+
exact_status="gpu-polygon-source-edge-cpu-oracle-fallback",
|
|
1477
|
+
)
|
|
1478
|
+
if polygon_component_area is not None:
|
|
1479
|
+
polygon_component_area = dict(polygon_component_area)
|
|
1480
|
+
polygon_component_area["source_edge_cpu_oracle_fallback"] = source_edge_cpu_fallback
|
|
1481
|
+
exact_fallback["gpu_polygon_component_area"] = polygon_component_area
|
|
1482
|
+
exact_fallback["mode"] = f"gpu-polygon-{record_delta_accelerator}-source-edge-cpu-oracle-fallback"
|
|
1483
|
+
exact_fallback["engine_status"] = "source-edge-cpu-oracle-corrected"
|
|
1484
|
+
exact_fallback["source_edge_cpu_oracle_fallback"] = source_edge_cpu_fallback
|
|
1485
|
+
exact_fallback["cpu_oracle_audit_original_mismatch"] = audit_summary
|
|
1486
|
+
audit_summary = _exact_result_audit_summary(exact_result, verification.exact_result)
|
|
1487
|
+
warnings = warnings + (
|
|
1488
|
+
"CPU gdstk audit corrected source-edge GPU evidence; original mismatch is recorded in exact_fallback.cpu_oracle_audit_original_mismatch.",
|
|
1489
|
+
)
|
|
1490
|
+
exact_fallback["cpu_oracle_audit"] = audit_summary
|
|
1491
|
+
if not audit_summary["match"]:
|
|
1492
|
+
warnings = warnings + ("CPU gdstk audit did not match GPU evidence; see exact_fallback.cpu_oracle_audit.",)
|
|
1493
|
+
old_input = _native_polygon_input_json(config.old_path, old_top_name, old_native)
|
|
1494
|
+
new_input = _native_polygon_input_json(config.new_path, new_top_name, new_native)
|
|
1495
|
+
selection = {"old_top": old_top_name, "new_top": new_top_name, "layers": config.to_json_dict()["layers"]}
|
|
1496
|
+
if exact_diff_markdown_path is not None:
|
|
1497
|
+
markdown_start = perf_counter()
|
|
1498
|
+
if coverage_result is not None:
|
|
1499
|
+
markdown_written = write_exact_diff_markdown(
|
|
1500
|
+
policy_result,
|
|
1501
|
+
exact_diff_markdown_path,
|
|
1502
|
+
title=f"GDS comparator gpu-polygon {str(coverage_accelerator).upper()} rectangle coverage diff",
|
|
1503
|
+
source_mode=exact_fallback["mode"],
|
|
1504
|
+
exact_verification_complete=True,
|
|
1505
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1506
|
+
)
|
|
1507
|
+
_record_exact_markdown_artifacts(artifacts, markdown_written)
|
|
1508
|
+
elif polygon_fragment_exact_result is not None:
|
|
1509
|
+
markdown_written = write_exact_diff_markdown(
|
|
1510
|
+
policy_result,
|
|
1511
|
+
exact_diff_markdown_path,
|
|
1512
|
+
title=f"GDS comparator gpu-polygon {str(record_delta_accelerator).upper()} source-edge diff",
|
|
1513
|
+
source_mode=exact_fallback["mode"],
|
|
1514
|
+
exact_verification_complete=True,
|
|
1515
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1516
|
+
)
|
|
1517
|
+
if polygon_component_area is not None:
|
|
1518
|
+
_append_gpu_polygon_component_area_markdown(markdown_written.path, polygon_component_area)
|
|
1519
|
+
_record_exact_markdown_artifacts(artifacts, markdown_written)
|
|
1520
|
+
elif surplus_result is not None:
|
|
1521
|
+
markdown_written = write_exact_diff_markdown(
|
|
1522
|
+
policy_result,
|
|
1523
|
+
exact_diff_markdown_path,
|
|
1524
|
+
title="GDS comparator canonical source-surplus diff",
|
|
1525
|
+
source_mode=exact_fallback["mode"],
|
|
1526
|
+
exact_verification_complete=surplus_result.stats.coverage_exact_proven,
|
|
1527
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1528
|
+
)
|
|
1529
|
+
if polygon_component_area is not None:
|
|
1530
|
+
_append_gpu_polygon_component_area_markdown(markdown_written.path, polygon_component_area)
|
|
1531
|
+
_record_exact_markdown_artifacts(artifacts, markdown_written)
|
|
1532
|
+
elif native_parse_cpu_verification is not None:
|
|
1533
|
+
markdown_written = write_exact_diff_markdown(
|
|
1534
|
+
policy_result,
|
|
1535
|
+
exact_diff_markdown_path,
|
|
1536
|
+
title="GDS comparator GPU native-parse CPU oracle fallback diff",
|
|
1537
|
+
source_mode=exact_fallback["mode"],
|
|
1538
|
+
exact_verification_complete=True,
|
|
1539
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1540
|
+
)
|
|
1541
|
+
_record_exact_markdown_artifacts(artifacts, markdown_written)
|
|
1542
|
+
elif record_delta is not None:
|
|
1543
|
+
_write_gpu_polygon_record_delta_markdown(exact_diff_markdown_path, warnings[0], record_delta)
|
|
1544
|
+
artifacts["exact_diff_markdown"] = str(exact_diff_markdown_path)
|
|
1545
|
+
else:
|
|
1546
|
+
_write_gpu_polygon_unavailable_markdown(exact_diff_markdown_path, warnings[0])
|
|
1547
|
+
artifacts["exact_diff_markdown"] = str(exact_diff_markdown_path)
|
|
1548
|
+
timings["markdown_format_s"] = perf_counter() - markdown_start
|
|
1549
|
+
|
|
1550
|
+
if markdown_path is not None:
|
|
1551
|
+
markdown_path = Path(markdown_path)
|
|
1552
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1553
|
+
markdown_path.write_text(
|
|
1554
|
+
"\n".join(
|
|
1555
|
+
[
|
|
1556
|
+
"### GDS comparator gpu-polygon evidence result",
|
|
1557
|
+
"",
|
|
1558
|
+
f"- Status: {'cpu-audited' if audit_cpu_oracle else 'unresolved'}.",
|
|
1559
|
+
f"- Note: {warnings[0]}",
|
|
1560
|
+
]
|
|
1561
|
+
)
|
|
1562
|
+
+ "\n",
|
|
1563
|
+
encoding="utf-8",
|
|
1564
|
+
)
|
|
1565
|
+
artifacts["markdown"] = str(markdown_path)
|
|
1566
|
+
|
|
1567
|
+
timings["total_s"] = perf_counter() - start
|
|
1568
|
+
audit_summary = exact_fallback.get("cpu_oracle_audit") if isinstance(exact_fallback, dict) else None
|
|
1569
|
+
unsupported_feature_count = _gpu_polygon_unsupported_feature_count(exact_fallback, exact_complete=exact_complete)
|
|
1570
|
+
status = _neutral_compare_status(
|
|
1571
|
+
policy_result,
|
|
1572
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1573
|
+
exact_complete=exact_complete,
|
|
1574
|
+
unsupported_feature_count=unsupported_feature_count,
|
|
1575
|
+
audit_match=(audit_summary or {}).get("match") if isinstance(audit_summary, dict) else None,
|
|
1576
|
+
)
|
|
1577
|
+
neutral_fields = _neutral_evidence_report_fields(
|
|
1578
|
+
status,
|
|
1579
|
+
policy_result,
|
|
1580
|
+
polygon_inventory_delta=polygon_inventory_delta,
|
|
1581
|
+
exact_complete=exact_complete,
|
|
1582
|
+
unsupported_feature_count=unsupported_feature_count,
|
|
1583
|
+
vertex_sidecar_path=artifacts.get("vertex_sidecar"),
|
|
1584
|
+
)
|
|
1585
|
+
report = CompareReport(
|
|
1586
|
+
status=status,
|
|
1587
|
+
exact_verification_complete=exact_complete,
|
|
1588
|
+
old_input=old_input,
|
|
1589
|
+
new_input=new_input,
|
|
1590
|
+
semantics=config.to_json_dict(),
|
|
1591
|
+
selection=selection,
|
|
1592
|
+
backend=backend_info
|
|
1593
|
+
| {
|
|
1594
|
+
"exact_mode": False,
|
|
1595
|
+
"evidence_engine_requested": config.evidence_engine.value,
|
|
1596
|
+
"evidence_engine_selected": "gdstk-oracle" if cpu_neutral_fallback else "gpu-polygon",
|
|
1597
|
+
"gpu_polygon_available": (
|
|
1598
|
+
False if cpu_neutral_fallback else coverage_result is not None or polygon_fragment_exact_result is not None
|
|
1599
|
+
),
|
|
1600
|
+
"cpu_neutral_fallback": cpu_neutral_fallback,
|
|
1601
|
+
"gpu_record_delta_available": record_delta is not None,
|
|
1602
|
+
"canonical_surplus_coverage_available": surplus_result is not None,
|
|
1603
|
+
"canonical_surplus_coverage_exact": (
|
|
1604
|
+
surplus_result.stats.coverage_exact_proven if surplus_result is not None else False
|
|
1605
|
+
),
|
|
1606
|
+
"gpu_coverage_engine": (
|
|
1607
|
+
f"{coverage_result.stats.accelerator}-rect-coverage" if coverage_result is not None else None
|
|
1608
|
+
),
|
|
1609
|
+
"metal_coverage_engine": "metal-rect-coverage"
|
|
1610
|
+
if coverage_result is not None and coverage_result.stats.accelerator == "metal"
|
|
1611
|
+
else None,
|
|
1612
|
+
"audit_cpu_oracle": audit_cpu_oracle,
|
|
1613
|
+
"cuda_kernel_count": (
|
|
1614
|
+
(
|
|
1615
|
+
record_delta.cuda_kernel_count
|
|
1616
|
+
if record_delta is not None and _record_delta_accelerator(record_delta) == "cuda"
|
|
1617
|
+
else 0
|
|
1618
|
+
)
|
|
1619
|
+
+ (
|
|
1620
|
+
coverage_result.stats.cuda_kernel_count
|
|
1621
|
+
if coverage_result is not None and coverage_result.stats.accelerator == "cuda"
|
|
1622
|
+
else 0
|
|
1623
|
+
)
|
|
1624
|
+
+ int(
|
|
1625
|
+
polygon_component_area.get("context_cuda_kernel_count", 0)
|
|
1626
|
+
if polygon_component_area is not None
|
|
1627
|
+
else 0
|
|
1628
|
+
)
|
|
1629
|
+
+ (
|
|
1630
|
+
1
|
|
1631
|
+
if polygon_fragment_exact_result is not None and record_delta is not None and _record_delta_accelerator(record_delta) == "cuda"
|
|
1632
|
+
else 0
|
|
1633
|
+
)
|
|
1634
|
+
),
|
|
1635
|
+
"metal_kernel_count": (
|
|
1636
|
+
(
|
|
1637
|
+
record_delta.metal_kernel_count
|
|
1638
|
+
if record_delta is not None and _record_delta_accelerator(record_delta) == "metal"
|
|
1639
|
+
else 0
|
|
1640
|
+
)
|
|
1641
|
+
+ (1 if coverage_result is not None and coverage_result.stats.accelerator == "metal" else 0)
|
|
1642
|
+
+ int(
|
|
1643
|
+
polygon_component_area.get("context_metal_kernel_count", 0)
|
|
1644
|
+
if polygon_component_area is not None
|
|
1645
|
+
else 0
|
|
1646
|
+
)
|
|
1647
|
+
+ (
|
|
1648
|
+
1
|
|
1649
|
+
if polygon_fragment_exact_result is not None and record_delta is not None and _record_delta_accelerator(record_delta) == "metal"
|
|
1650
|
+
else 0
|
|
1651
|
+
)
|
|
1652
|
+
),
|
|
1653
|
+
"cuda_event_elapsed_s": (
|
|
1654
|
+
record_delta.cuda_event_elapsed_s
|
|
1655
|
+
if record_delta is not None and _record_delta_accelerator(record_delta) == "cuda"
|
|
1656
|
+
else 0.0
|
|
1657
|
+
),
|
|
1658
|
+
"metal_event_elapsed_s": (
|
|
1659
|
+
record_delta.metal_event_elapsed_s
|
|
1660
|
+
if record_delta is not None and _record_delta_accelerator(record_delta) == "metal"
|
|
1661
|
+
else 0.0
|
|
1662
|
+
),
|
|
1663
|
+
"transferred_bytes": (
|
|
1664
|
+
(record_delta.transferred_bytes if record_delta is not None else 0)
|
|
1665
|
+
+ (coverage_result.stats.transferred_bytes if coverage_result is not None else 0)
|
|
1666
|
+
+ int(polygon_component_area.get("transferred_bytes", 0) if polygon_component_area is not None else 0)
|
|
1667
|
+
),
|
|
1668
|
+
},
|
|
1669
|
+
prefilter=prefilter.to_json(),
|
|
1670
|
+
exact_fallback=exact_fallback,
|
|
1671
|
+
layers=layers,
|
|
1672
|
+
allowed_policy=policy_result.effective_policy,
|
|
1673
|
+
unexpected_summary={
|
|
1674
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
1675
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
1676
|
+
},
|
|
1677
|
+
timings=timings,
|
|
1678
|
+
warnings=warnings,
|
|
1679
|
+
errors=() if audit_cpu_oracle else warnings,
|
|
1680
|
+
artifacts=artifacts,
|
|
1681
|
+
**neutral_fields,
|
|
1682
|
+
command=command,
|
|
1683
|
+
failure_policy=config.failure_policy,
|
|
1684
|
+
)
|
|
1685
|
+
if json_report_path is not None:
|
|
1686
|
+
artifacts["json_report"] = str(json_report_path)
|
|
1687
|
+
report = _report_with_artifacts(report, artifacts)
|
|
1688
|
+
report.write_json(json_report_path)
|
|
1689
|
+
return CompareRunResult(
|
|
1690
|
+
status=CompareStatus.coerce(status),
|
|
1691
|
+
report=report,
|
|
1692
|
+
exact_result=exact_result,
|
|
1693
|
+
policy_result=policy_result,
|
|
1694
|
+
prefilter=prefilter,
|
|
1695
|
+
windows=windows,
|
|
1696
|
+
artifacts=artifacts,
|
|
1697
|
+
timings=timings,
|
|
1698
|
+
)
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
def _gpu_polygon_native_inputs(
|
|
1702
|
+
config: CompareConfig,
|
|
1703
|
+
*,
|
|
1704
|
+
old_top_name: str | None,
|
|
1705
|
+
new_top_name: str | None,
|
|
1706
|
+
):
|
|
1707
|
+
if config.geometry_cache_dir is None:
|
|
1708
|
+
old_extract_args = (config.old_path, old_top_name, config.layers)
|
|
1709
|
+
new_extract_args = (config.new_path, new_top_name, config.layers)
|
|
1710
|
+
if _should_extract_pair_in_parallel(config):
|
|
1711
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
1712
|
+
old_future = executor.submit(_extract_native_one, old_extract_args)
|
|
1713
|
+
new_future = executor.submit(_extract_native_one, new_extract_args)
|
|
1714
|
+
return old_future.result(), new_future.result(), {}
|
|
1715
|
+
return (
|
|
1716
|
+
_extract_native_one(old_extract_args),
|
|
1717
|
+
_extract_native_one(new_extract_args),
|
|
1718
|
+
{},
|
|
1719
|
+
)
|
|
1720
|
+
old_cache_args = (config.geometry_cache_dir, config.old_path, old_top_name, config.layers)
|
|
1721
|
+
new_cache_args = (config.geometry_cache_dir, config.new_path, new_top_name, config.layers)
|
|
1722
|
+
if _should_extract_pair_in_parallel(config):
|
|
1723
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
1724
|
+
old_future = executor.submit(_cached_native_polygon_one, old_cache_args)
|
|
1725
|
+
new_future = executor.submit(_cached_native_polygon_one, new_cache_args)
|
|
1726
|
+
old_cached = old_future.result()
|
|
1727
|
+
new_cached = new_future.result()
|
|
1728
|
+
timings: dict[str, object] = {}
|
|
1729
|
+
timings.update(old_cached.to_timing_json("old"))
|
|
1730
|
+
timings.update(new_cached.to_timing_json("new"))
|
|
1731
|
+
return old_cached.extraction, new_cached.extraction, timings
|
|
1732
|
+
old_cached = _cached_native_polygon_one(old_cache_args)
|
|
1733
|
+
new_cached = _cached_native_polygon_one(new_cache_args)
|
|
1734
|
+
timings: dict[str, object] = {}
|
|
1735
|
+
timings.update(old_cached.to_timing_json("old"))
|
|
1736
|
+
timings.update(new_cached.to_timing_json("new"))
|
|
1737
|
+
return old_cached.extraction, new_cached.extraction, timings
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def _native_buffers_rectangle_coverage_eligible(*buffers: PolygonBuffer) -> bool:
|
|
1741
|
+
for buffer in buffers:
|
|
1742
|
+
if buffer.polygon_count == 0:
|
|
1743
|
+
continue
|
|
1744
|
+
active = buffer.flags == 0
|
|
1745
|
+
if not bool(active.any()):
|
|
1746
|
+
continue
|
|
1747
|
+
vertex_counts = buffer.polygon_offsets[1:] - buffer.polygon_offsets[:-1]
|
|
1748
|
+
if not bool((vertex_counts[active] == 4).all()):
|
|
1749
|
+
return False
|
|
1750
|
+
return True
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
def _extract_native_one(args):
|
|
1754
|
+
path, top_name, layers = args
|
|
1755
|
+
return extract_gds_polygons_native(path, top_name=top_name, layers=layers)
|
|
1756
|
+
|
|
1757
|
+
|
|
1758
|
+
def _cached_native_polygon_one(args):
|
|
1759
|
+
cache_dir, path, top_name, layers = args
|
|
1760
|
+
return load_or_build_cached_native_polygons(
|
|
1761
|
+
cache_dir=cache_dir,
|
|
1762
|
+
path=path,
|
|
1763
|
+
top_name=top_name,
|
|
1764
|
+
layers=layers,
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
def _default_cpu_audit_component_backend(exact_component_backend: str) -> str:
|
|
1769
|
+
return "gdstk" if exact_component_backend == "none" else exact_component_backend
|
|
1770
|
+
|
|
1771
|
+
|
|
1772
|
+
def _record_delta_accelerator(record_delta) -> str:
|
|
1773
|
+
return str(getattr(record_delta, "accelerator", "cuda"))
|
|
1774
|
+
|
|
1775
|
+
|
|
1776
|
+
def _release_cuda_device_memory_after_gpu_operation(timings: dict[str, float], warnings: tuple[str, ...]) -> tuple[str, ...]:
|
|
1777
|
+
release_start = perf_counter()
|
|
1778
|
+
try:
|
|
1779
|
+
from .backends.cuda_ctypes import CudaCtypesUnavailable, release_cuda_device_memory
|
|
1780
|
+
|
|
1781
|
+
release_cuda_device_memory()
|
|
1782
|
+
except CudaCtypesUnavailable as exc:
|
|
1783
|
+
return warnings + (f"CUDA device memory release failed after gpu-polygon operation: {exc}",)
|
|
1784
|
+
finally:
|
|
1785
|
+
timings["cuda_release_s"] = perf_counter() - release_start
|
|
1786
|
+
return warnings
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def _release_metal_device_memory_after_gpu_operation(timings: dict[str, float], warnings: tuple[str, ...]) -> tuple[str, ...]:
|
|
1790
|
+
release_start = perf_counter()
|
|
1791
|
+
try:
|
|
1792
|
+
from .backends.metal_ctypes import MetalCtypesUnavailable, release_metal_device_memory
|
|
1793
|
+
|
|
1794
|
+
release_metal_device_memory()
|
|
1795
|
+
except MetalCtypesUnavailable as exc:
|
|
1796
|
+
return warnings + (f"Metal device memory release failed after gpu-polygon operation: {exc}",)
|
|
1797
|
+
finally:
|
|
1798
|
+
timings["metal_release_s"] = perf_counter() - release_start
|
|
1799
|
+
return warnings
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
def _exact_result_audit_summary(gpu_result: ExactOracleResult, cpu_result: ExactOracleResult) -> dict[str, object]:
|
|
1803
|
+
gpu_layers = _exact_result_layer_audit_map(gpu_result)
|
|
1804
|
+
cpu_layers = _exact_result_layer_audit_map(cpu_result)
|
|
1805
|
+
mismatches = []
|
|
1806
|
+
for layer in sorted(set(gpu_layers) | set(cpu_layers)):
|
|
1807
|
+
gpu_layer = gpu_layers.get(layer)
|
|
1808
|
+
cpu_layer = cpu_layers.get(layer)
|
|
1809
|
+
if gpu_layer != cpu_layer:
|
|
1810
|
+
mismatches.append(
|
|
1811
|
+
{
|
|
1812
|
+
"layer": [layer[0], layer[1]],
|
|
1813
|
+
"gpu": gpu_layer,
|
|
1814
|
+
"cpu": cpu_layer,
|
|
1815
|
+
}
|
|
1816
|
+
)
|
|
1817
|
+
return {
|
|
1818
|
+
"oracle": "gdstk-full-exact",
|
|
1819
|
+
"match": not mismatches,
|
|
1820
|
+
"mismatch_count": len(mismatches),
|
|
1821
|
+
"mismatches": mismatches[:20],
|
|
1822
|
+
"truncated_mismatch_count": max(0, len(mismatches) - 20),
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
|
|
1826
|
+
def _exact_result_layer_audit_map(result: ExactOracleResult) -> dict[tuple[int, int], dict[str, object]]:
|
|
1827
|
+
layers = {}
|
|
1828
|
+
for layer in result.layers:
|
|
1829
|
+
old_fragments = tuple(
|
|
1830
|
+
_audit_fragment_json(fragment)
|
|
1831
|
+
for fragment in sorted(layer.old_minus_new, key=_audit_fragment_sort_key)
|
|
1832
|
+
)
|
|
1833
|
+
new_fragments = tuple(
|
|
1834
|
+
_audit_fragment_json(fragment)
|
|
1835
|
+
for fragment in sorted(layer.new_minus_old, key=_audit_fragment_sort_key)
|
|
1836
|
+
)
|
|
1837
|
+
if not old_fragments and not new_fragments:
|
|
1838
|
+
continue
|
|
1839
|
+
layers[(layer.layer.layer, layer.layer.datatype)] = {
|
|
1840
|
+
"old_minus_new_twice_area": sum(fragment.twice_area for fragment in layer.old_minus_new),
|
|
1841
|
+
"new_minus_old_twice_area": sum(fragment.twice_area for fragment in layer.new_minus_old),
|
|
1842
|
+
"xor_twice_area": sum(fragment.twice_area for fragment in layer.xor),
|
|
1843
|
+
"old_minus_new_count": len(old_fragments),
|
|
1844
|
+
"new_minus_old_count": len(new_fragments),
|
|
1845
|
+
"xor_count": len(layer.xor),
|
|
1846
|
+
"old_minus_new_fragments": old_fragments,
|
|
1847
|
+
"new_minus_old_fragments": new_fragments,
|
|
1848
|
+
}
|
|
1849
|
+
return layers
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def _audit_fragment_sort_key(fragment: DiffFragment) -> tuple[tuple[int, int, int, int], int, int]:
|
|
1853
|
+
return fragment.bbox, fragment.twice_area, len(fragment.points)
|
|
1854
|
+
|
|
1855
|
+
|
|
1856
|
+
def _audit_fragment_json(fragment: DiffFragment) -> dict[str, object]:
|
|
1857
|
+
points = _audit_canonical_points(fragment.points)
|
|
1858
|
+
return {
|
|
1859
|
+
"bbox": list(fragment.bbox),
|
|
1860
|
+
"twice_area": fragment.twice_area,
|
|
1861
|
+
"vertex_count": len(points),
|
|
1862
|
+
"points_sha256": _audit_points_hash(points),
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def _audit_canonical_points(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
|
|
1867
|
+
cleaned = _audit_clean_loop_points(points)
|
|
1868
|
+
if not cleaned:
|
|
1869
|
+
return ()
|
|
1870
|
+
candidates: list[tuple[tuple[int, int], ...]] = []
|
|
1871
|
+
for sequence in (cleaned, tuple(reversed(cleaned))):
|
|
1872
|
+
candidates.extend(sequence[index:] + sequence[:index] for index in range(len(sequence)))
|
|
1873
|
+
return min(candidates)
|
|
1874
|
+
|
|
1875
|
+
|
|
1876
|
+
def _audit_clean_loop_points(points: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
|
|
1877
|
+
cleaned: list[tuple[int, int]] = []
|
|
1878
|
+
for point in points:
|
|
1879
|
+
if not cleaned or cleaned[-1] != point:
|
|
1880
|
+
cleaned.append(point)
|
|
1881
|
+
if len(cleaned) > 1 and cleaned[0] == cleaned[-1]:
|
|
1882
|
+
cleaned.pop()
|
|
1883
|
+
changed = True
|
|
1884
|
+
while changed and len(cleaned) >= 3:
|
|
1885
|
+
changed = False
|
|
1886
|
+
next_points: list[tuple[int, int]] = []
|
|
1887
|
+
for index, point in enumerate(cleaned):
|
|
1888
|
+
previous = cleaned[index - 1]
|
|
1889
|
+
following = cleaned[(index + 1) % len(cleaned)]
|
|
1890
|
+
if _audit_collinear(previous, point, following):
|
|
1891
|
+
changed = True
|
|
1892
|
+
continue
|
|
1893
|
+
next_points.append(point)
|
|
1894
|
+
cleaned = next_points
|
|
1895
|
+
return tuple(cleaned)
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _audit_collinear(
|
|
1899
|
+
left: tuple[int, int],
|
|
1900
|
+
middle: tuple[int, int],
|
|
1901
|
+
right: tuple[int, int],
|
|
1902
|
+
) -> bool:
|
|
1903
|
+
return (middle[0] - left[0]) * (right[1] - middle[1]) == (middle[1] - left[1]) * (right[0] - middle[0])
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def _audit_points_hash(points: tuple[tuple[int, int], ...]) -> str:
|
|
1907
|
+
payload = ";".join(f"{x},{y}" for x, y in points).encode("ascii")
|
|
1908
|
+
return hashlib.sha256(payload).hexdigest()
|
|
1909
|
+
|
|
1910
|
+
|
|
1911
|
+
def _resolve_single_common_top_after_native_parse_failure(
|
|
1912
|
+
config: CompareConfig,
|
|
1913
|
+
reason: str,
|
|
1914
|
+
old_top_name: str | None,
|
|
1915
|
+
new_top_name: str | None,
|
|
1916
|
+
) -> str | None:
|
|
1917
|
+
if old_top_name is not None or new_top_name is not None or "expected exactly one top cell" not in reason:
|
|
1918
|
+
return None
|
|
1919
|
+
try:
|
|
1920
|
+
old_metadata = read_gds_metadata(config.old_path)
|
|
1921
|
+
new_metadata = read_gds_metadata(config.new_path)
|
|
1922
|
+
except Exception:
|
|
1923
|
+
return None
|
|
1924
|
+
common_tops = sorted(set(old_metadata.top_names) & set(new_metadata.top_names))
|
|
1925
|
+
if len(common_tops) != 1:
|
|
1926
|
+
return None
|
|
1927
|
+
return common_tops[0]
|
|
1928
|
+
|
|
1929
|
+
|
|
1930
|
+
def _write_gpu_polygon_unavailable_markdown(path: str | Path, warning: str) -> None:
|
|
1931
|
+
path = Path(path)
|
|
1932
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1933
|
+
path.write_text(
|
|
1934
|
+
"\n".join(
|
|
1935
|
+
[
|
|
1936
|
+
"# GDS comparator gpu-polygon exact diff",
|
|
1937
|
+
"",
|
|
1938
|
+
"## Comparator Evidence",
|
|
1939
|
+
"",
|
|
1940
|
+
"- status: `unresolved`",
|
|
1941
|
+
"- source_mode: `gpu-polygon-unavailable`",
|
|
1942
|
+
"- exact_verification_complete: `false`",
|
|
1943
|
+
"- expected_area_dbu2: `0`",
|
|
1944
|
+
"- unexpected_area_dbu2: `0`",
|
|
1945
|
+
"- layer_count: `0`",
|
|
1946
|
+
"- component_count: `0`",
|
|
1947
|
+
"- polygon_inventory_delta_count: `not-recorded`",
|
|
1948
|
+
"",
|
|
1949
|
+
"## Exactness Warning",
|
|
1950
|
+
"",
|
|
1951
|
+
warning,
|
|
1952
|
+
"",
|
|
1953
|
+
"## Agent Minimal Read",
|
|
1954
|
+
"",
|
|
1955
|
+
"- coverage_changed: `unknown`",
|
|
1956
|
+
"- coverage_xor_area_dbu2: `unknown`",
|
|
1957
|
+
"- inventory_changed: `unknown`",
|
|
1958
|
+
"- component_count: `0`",
|
|
1959
|
+
"- note: `gpu-polygon numeric diff records are not available yet; rerun with --audit-cpu-oracle for CPU gdstk evidence`",
|
|
1960
|
+
"",
|
|
1961
|
+
]
|
|
1962
|
+
),
|
|
1963
|
+
encoding="utf-8",
|
|
1964
|
+
)
|
|
1965
|
+
|
|
1966
|
+
|
|
1967
|
+
def _write_gpu_polygon_record_delta_markdown(path: str | Path, warning: str, record_delta, *, row_limit: int = 200) -> None:
|
|
1968
|
+
path = Path(path)
|
|
1969
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1970
|
+
accelerator = _record_delta_accelerator(record_delta)
|
|
1971
|
+
shown = min(record_delta.record_count, row_limit)
|
|
1972
|
+
omitted = max(0, record_delta.record_count - shown)
|
|
1973
|
+
lines = [
|
|
1974
|
+
"# GDS comparator gpu-polygon exact diff",
|
|
1975
|
+
"",
|
|
1976
|
+
"## Comparator Evidence",
|
|
1977
|
+
"",
|
|
1978
|
+
"- status: `unresolved`",
|
|
1979
|
+
f"- source_mode: `gpu-polygon-{accelerator}-record-delta-no-coverage-xor`",
|
|
1980
|
+
"- exact_verification_complete: `false`",
|
|
1981
|
+
"- coverage_xor_available: `false`",
|
|
1982
|
+
f"- gpu_record_delta_accelerator: `{accelerator}`",
|
|
1983
|
+
"- gpu_record_delta_available: `true`",
|
|
1984
|
+
f"- gpu_record_delta_count: `{record_delta.record_count}`",
|
|
1985
|
+
f"- matched_source_record_count: `{record_delta.matched_record_count}`",
|
|
1986
|
+
f"- reduced_source_record_key_count: `{record_delta.reduced_key_count}`",
|
|
1987
|
+
f"- gpu_kernel_count: `{getattr(record_delta, accelerator + '_kernel_count', 0)}`",
|
|
1988
|
+
f"- gpu_event_elapsed_s: `{getattr(record_delta, accelerator + '_event_elapsed_s', 0.0):.9f}`",
|
|
1989
|
+
f"- transferred_bytes: `{record_delta.transferred_bytes}`",
|
|
1990
|
+
"",
|
|
1991
|
+
"## Exactness Warning",
|
|
1992
|
+
"",
|
|
1993
|
+
warning,
|
|
1994
|
+
"",
|
|
1995
|
+
"## Agent Minimal Read",
|
|
1996
|
+
"",
|
|
1997
|
+
"- coverage_changed: `unknown`",
|
|
1998
|
+
"- coverage_xor_area_dbu2: `unknown`",
|
|
1999
|
+
f"- source_polygon_records_changed: `{'true' if record_delta.record_count else 'false'}`",
|
|
2000
|
+
f"- source_record_delta_rows: `{record_delta.record_count}`",
|
|
2001
|
+
"- note: `these rows are GPU-emitted source polygon record deltas; they are not yet coverage XOR components`",
|
|
2002
|
+
"",
|
|
2003
|
+
"## GPU Source Record Delta",
|
|
2004
|
+
"",
|
|
2005
|
+
"| direction | count | layer | datatype | bbox_dbu | vertex_count | flags | hash0 | hash1 |",
|
|
2006
|
+
"|---|---:|---:|---:|---|---:|---:|---|---|",
|
|
2007
|
+
]
|
|
2008
|
+
for index in range(shown):
|
|
2009
|
+
direction = "old_minus_new" if int(record_delta.directions[index]) == 1 else "new_minus_old"
|
|
2010
|
+
bbox = [int(value) for value in record_delta.bboxes[index]]
|
|
2011
|
+
hash0 = f"{int(record_delta.hashes[index][0]):016x}"
|
|
2012
|
+
hash1 = f"{int(record_delta.hashes[index][1]):016x}"
|
|
2013
|
+
lines.append(
|
|
2014
|
+
"| {direction} | {count} | {layer} | {datatype} | `{bbox}` | {vertex_count} | {flags} | `{hash0}` | `{hash1}` |".format(
|
|
2015
|
+
direction=direction,
|
|
2016
|
+
count=int(record_delta.counts[index]),
|
|
2017
|
+
layer=int(record_delta.layers[index]),
|
|
2018
|
+
datatype=int(record_delta.datatypes[index]),
|
|
2019
|
+
bbox=bbox,
|
|
2020
|
+
vertex_count=int(record_delta.vertex_counts[index]),
|
|
2021
|
+
flags=int(record_delta.flags[index]),
|
|
2022
|
+
hash0=hash0,
|
|
2023
|
+
hash1=hash1,
|
|
2024
|
+
)
|
|
2025
|
+
)
|
|
2026
|
+
if omitted:
|
|
2027
|
+
lines.extend(
|
|
2028
|
+
[
|
|
2029
|
+
"",
|
|
2030
|
+
"## Omitted Rows",
|
|
2031
|
+
"",
|
|
2032
|
+
f"- omitted_gpu_record_delta_rows: `{omitted}`",
|
|
2033
|
+
"- note: `rerun with a larger row limit after the coverage-XOR engine is available if full source-record inventory is needed`",
|
|
2034
|
+
]
|
|
2035
|
+
)
|
|
2036
|
+
lines.append("")
|
|
2037
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
2038
|
+
|
|
2039
|
+
|
|
2040
|
+
def _append_gpu_polygon_component_area_markdown(path: str | Path, component_area: dict[str, object]) -> None:
|
|
2041
|
+
path = Path(path)
|
|
2042
|
+
gpu_trapezoid = bool(component_area.get("gpu_trapezoid_fragments_available", False))
|
|
2043
|
+
gpu_source_edge = bool(component_area.get("gpu_source_edge_records_available", False))
|
|
2044
|
+
exact_boundary = bool(component_area.get("exact_boundary_loops_available", False))
|
|
2045
|
+
lines = [
|
|
2046
|
+
"",
|
|
2047
|
+
"## GPU Polygon Evidence",
|
|
2048
|
+
"",
|
|
2049
|
+
f"- gpu_trapezoid_fragments_available: `{str(gpu_trapezoid).lower()}`",
|
|
2050
|
+
f"- gpu_source_edge_records_available: `{str(gpu_source_edge).lower()}`",
|
|
2051
|
+
f"- merged_boundary_loop_records_available: `{str(bool(component_area.get('merged_boundary_loop_records_available', False))).lower()}`",
|
|
2052
|
+
f"- boundary_loop_assembly_backend: `{component_area.get('boundary_loop_assembly_backend', 'none')}`",
|
|
2053
|
+
f"- exact_boundary_loops_available: `{str(exact_boundary).lower()}`",
|
|
2054
|
+
"- note: `default CUDA evidence uses GPU context indexing and GPU edge cancel/split when supported, then CPU formats compact exact boundary records and Markdown`",
|
|
2055
|
+
f"- component_count: `{component_area.get('component_count', 0)}`",
|
|
2056
|
+
f"- context_component_count: `{component_area.get('context_component_count', 0)}`",
|
|
2057
|
+
f"- context_max_fragments_per_component: `{component_area.get('context_max_fragments_per_component', 0)}`",
|
|
2058
|
+
f"- context_max_vertices_per_component: `{component_area.get('context_max_vertices_per_component', 0)}`",
|
|
2059
|
+
f"- emitted_trapezoid_fragment_count: `{component_area.get('emitted_trapezoid_fragment_count', 0)}`",
|
|
2060
|
+
f"- old_minus_new_twice_area_dbu2: `{component_area.get('old_minus_new_twice_area', 0)}`",
|
|
2061
|
+
f"- new_minus_old_twice_area_dbu2: `{component_area.get('new_minus_old_twice_area', 0)}`",
|
|
2062
|
+
f"- xor_twice_area_dbu2: `{component_area.get('xor_twice_area', 0)}`",
|
|
2063
|
+
"",
|
|
2064
|
+
]
|
|
2065
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
2066
|
+
handle.write("\n".join(lines))
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
def _native_polygon_input_json(path: str | Path, top_name: str | None, extraction) -> dict[str, object]:
|
|
2070
|
+
data: dict[str, object] = {
|
|
2071
|
+
"path": str(path),
|
|
2072
|
+
"selected_top": top_name,
|
|
2073
|
+
"input_mode": "native-polygon-array",
|
|
2074
|
+
}
|
|
2075
|
+
if extraction is None:
|
|
2076
|
+
data["native_polygon_parse_complete"] = False
|
|
2077
|
+
return data
|
|
2078
|
+
data.update(
|
|
2079
|
+
{
|
|
2080
|
+
"native_polygon_parse_complete": True,
|
|
2081
|
+
"polygon_count": extraction.buffer.polygon_count,
|
|
2082
|
+
"vertex_count": extraction.buffer.vertex_count,
|
|
2083
|
+
"layers": [layer.to_json() for layer in extraction.buffer.layer_table.layers],
|
|
2084
|
+
}
|
|
2085
|
+
)
|
|
2086
|
+
return data
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
def _geometry_layer_json(summary) -> tuple[dict[str, object], ...]:
|
|
2090
|
+
return tuple(layer.to_json() for layer in summary.layers)
|
|
2091
|
+
|
|
2092
|
+
|
|
2093
|
+
def _try_pair_verdict_cache_path(
|
|
2094
|
+
config: CompareConfig,
|
|
2095
|
+
*,
|
|
2096
|
+
backend_info: dict[str, object],
|
|
2097
|
+
old_metadata: dict[str, object],
|
|
2098
|
+
new_metadata: dict[str, object],
|
|
2099
|
+
old_top_name: str | None,
|
|
2100
|
+
new_top_name: str | None,
|
|
2101
|
+
command: tuple[str, ...],
|
|
2102
|
+
json_report_path: str | Path | None,
|
|
2103
|
+
markdown_path: str | Path | None,
|
|
2104
|
+
exact_diff_markdown_path: str | Path | None,
|
|
2105
|
+
start: float,
|
|
2106
|
+
exact_component_backend: str,
|
|
2107
|
+
) -> CompareRunResult | None:
|
|
2108
|
+
cached = load_cached_pair_verdict(config, old_top_name=old_top_name, new_top_name=new_top_name)
|
|
2109
|
+
if cached is None:
|
|
2110
|
+
return None
|
|
2111
|
+
cache_status_start = perf_counter()
|
|
2112
|
+
geometry_status_s = perf_counter() - cache_status_start + cached.load_s
|
|
2113
|
+
verification = _run_full_exact_verification(config, exact_component_backend=exact_component_backend)
|
|
2114
|
+
exact_result = verification.exact_result
|
|
2115
|
+
policy_result = verification.policy_result
|
|
2116
|
+
timings = {
|
|
2117
|
+
"extract_s": verification.extract_s,
|
|
2118
|
+
"exact_s": verification.exact_s,
|
|
2119
|
+
"fast_status_s": geometry_status_s,
|
|
2120
|
+
"geometry_status_s": geometry_status_s,
|
|
2121
|
+
"pair_verdict_cache_hit": True,
|
|
2122
|
+
"pair_verdict_cache_load_s": cached.load_s,
|
|
2123
|
+
"pair_verdict_cache_key": cached.cache_key,
|
|
2124
|
+
"exact_verification_s": verification.extract_s + verification.exact_s,
|
|
2125
|
+
"total_s": perf_counter() - start,
|
|
2126
|
+
}
|
|
2127
|
+
artifacts: dict[str, str] = {}
|
|
2128
|
+
warnings = (
|
|
2129
|
+
"Cached full-geometry prefilter/windowed verdict was reused by file-content cache key; full exact gdstk verification was run afterward and supplies the polygon-level evidence.",
|
|
2130
|
+
)
|
|
2131
|
+
exact_fallback = dict(cached.exact_fallback)
|
|
2132
|
+
exact_fallback["mode"] = f"{exact_fallback.get('mode', 'geometry')}-cache-plus-full-exact"
|
|
2133
|
+
exact_fallback["fast_status"] = cached.fast_status
|
|
2134
|
+
exact_fallback["pair_verdict_cache_hit"] = True
|
|
2135
|
+
exact_fallback["pair_verdict_cache_key"] = cached.cache_key
|
|
2136
|
+
exact_fallback["exact_component_backend"] = verification.exact_component_backend
|
|
2137
|
+
layers = verification.layers
|
|
2138
|
+
if markdown_path is not None:
|
|
2139
|
+
markdown_path = Path(markdown_path)
|
|
2140
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2141
|
+
markdown_path.write_text(
|
|
2142
|
+
render_design_history_entry(
|
|
2143
|
+
"GDS comparator cached geometry-prefilter result",
|
|
2144
|
+
policy_result,
|
|
2145
|
+
tests_run=("gdsdiff geometry-cache",),
|
|
2146
|
+
notes=warnings,
|
|
2147
|
+
),
|
|
2148
|
+
encoding="utf-8",
|
|
2149
|
+
)
|
|
2150
|
+
artifacts["markdown"] = str(markdown_path)
|
|
2151
|
+
if exact_diff_markdown_path is not None:
|
|
2152
|
+
artifacts["exact_diff_markdown"] = str(
|
|
2153
|
+
write_exact_diff_markdown(
|
|
2154
|
+
policy_result,
|
|
2155
|
+
exact_diff_markdown_path,
|
|
2156
|
+
title="GDS comparator cached geometry-prefilter exact diff",
|
|
2157
|
+
source_mode=str(exact_fallback["mode"]),
|
|
2158
|
+
exact_verification_complete=True,
|
|
2159
|
+
polygon_inventory_delta=verification.polygon_inventory_delta,
|
|
2160
|
+
).path
|
|
2161
|
+
)
|
|
2162
|
+
report = CompareReport(
|
|
2163
|
+
status=policy_result.status,
|
|
2164
|
+
exact_verification_complete=True,
|
|
2165
|
+
old_input=verification.old_input,
|
|
2166
|
+
new_input=verification.new_input,
|
|
2167
|
+
semantics=config.to_json_dict(),
|
|
2168
|
+
selection=verification.selection,
|
|
2169
|
+
backend=backend_info
|
|
2170
|
+
| {
|
|
2171
|
+
"exact_mode": False,
|
|
2172
|
+
"fast_shortcuts_enabled": False,
|
|
2173
|
+
"geometry_cache_enabled": True,
|
|
2174
|
+
"pair_verdict_cache_hit": True,
|
|
2175
|
+
},
|
|
2176
|
+
prefilter=CpuTilePrefilterResult(candidate_tiles=()).to_json(),
|
|
2177
|
+
exact_fallback=exact_fallback,
|
|
2178
|
+
layers=layers,
|
|
2179
|
+
allowed_policy=policy_result.effective_policy,
|
|
2180
|
+
unexpected_summary={
|
|
2181
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
2182
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
2183
|
+
},
|
|
2184
|
+
timings=timings,
|
|
2185
|
+
warnings=warnings,
|
|
2186
|
+
artifacts=artifacts,
|
|
2187
|
+
command=command,
|
|
2188
|
+
failure_policy=config.failure_policy,
|
|
2189
|
+
)
|
|
2190
|
+
if json_report_path is not None:
|
|
2191
|
+
artifacts["json_report"] = str(json_report_path)
|
|
2192
|
+
report = _report_with_artifacts(report, artifacts)
|
|
2193
|
+
report.write_json(json_report_path)
|
|
2194
|
+
return CompareRunResult(
|
|
2195
|
+
status=policy_result.status,
|
|
2196
|
+
report=report,
|
|
2197
|
+
exact_result=exact_result,
|
|
2198
|
+
policy_result=policy_result,
|
|
2199
|
+
prefilter=CpuTilePrefilterResult(candidate_tiles=()),
|
|
2200
|
+
windows=(),
|
|
2201
|
+
artifacts=artifacts,
|
|
2202
|
+
timings=timings,
|
|
2203
|
+
)
|
|
2204
|
+
|
|
2205
|
+
|
|
2206
|
+
def _try_cuda_hierarchy_fast_path(
|
|
2207
|
+
config: CompareConfig,
|
|
2208
|
+
*,
|
|
2209
|
+
backend_info: dict[str, object],
|
|
2210
|
+
old_metadata: dict[str, object],
|
|
2211
|
+
new_metadata: dict[str, object],
|
|
2212
|
+
old_top_name: str | None,
|
|
2213
|
+
new_top_name: str | None,
|
|
2214
|
+
exact: bool,
|
|
2215
|
+
command: tuple[str, ...],
|
|
2216
|
+
json_report_path: str | Path | None,
|
|
2217
|
+
markdown_path: str | Path | None,
|
|
2218
|
+
exact_diff_markdown_path: str | Path | None,
|
|
2219
|
+
verify_exact_after_fast: bool,
|
|
2220
|
+
) -> CompareRunResult | None:
|
|
2221
|
+
if exact or not _accelerated_fast_path_eligible(config, backend_info):
|
|
2222
|
+
return None
|
|
2223
|
+
start = perf_counter()
|
|
2224
|
+
digest_result = _try_accelerated_byte_identity_fast_path(
|
|
2225
|
+
config,
|
|
2226
|
+
backend_info=backend_info,
|
|
2227
|
+
old_metadata=old_metadata,
|
|
2228
|
+
new_metadata=new_metadata,
|
|
2229
|
+
old_top_name=old_top_name,
|
|
2230
|
+
new_top_name=new_top_name,
|
|
2231
|
+
command=command,
|
|
2232
|
+
json_report_path=json_report_path,
|
|
2233
|
+
markdown_path=markdown_path,
|
|
2234
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
2235
|
+
verify_exact_after_fast=verify_exact_after_fast,
|
|
2236
|
+
exact_component_backend="none",
|
|
2237
|
+
start=start,
|
|
2238
|
+
)
|
|
2239
|
+
if digest_result is not None:
|
|
2240
|
+
return digest_result
|
|
2241
|
+
try:
|
|
2242
|
+
import gdstk
|
|
2243
|
+
|
|
2244
|
+
old_lib = gdstk.read_gds(str(config.old_path))
|
|
2245
|
+
new_lib = gdstk.read_gds(str(config.new_path))
|
|
2246
|
+
hierarchy = compare_hierarchy(old_lib, new_lib, old_top=old_top_name, new_top=new_top_name)
|
|
2247
|
+
except Exception:
|
|
2248
|
+
return None
|
|
2249
|
+
top = config.top or old_top_name or new_top_name
|
|
2250
|
+
warnings = (
|
|
2251
|
+
"CUDA hierarchy fast path used; geometry differences are detected by canonical hierarchy digest and reported fail-closed.",
|
|
2252
|
+
)
|
|
2253
|
+
if hierarchy.identical:
|
|
2254
|
+
exact_result = ExactOracleResult(())
|
|
2255
|
+
else:
|
|
2256
|
+
fragment = DiffFragment(points=((0, 0), (1, 0), (1, 1), (0, 1)), bbox=(0, 0, 1, 1), twice_area=2)
|
|
2257
|
+
exact_result = ExactOracleResult((LayerExactDiff(layer=LayerSpec(0, 0), new_minus_old=(fragment,), xor=(fragment,)),))
|
|
2258
|
+
policy_result = classify_exact_differences(exact_result, failure_policy=config.failure_policy)
|
|
2259
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
2260
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
2261
|
+
timings = {"extract_s": 0.0, "exact_s": perf_counter() - start, "total_s": perf_counter() - start}
|
|
2262
|
+
artifacts: dict[str, str] = {}
|
|
2263
|
+
layer_reports = build_layer_reports(exact_result, policy_result=policy_result)
|
|
2264
|
+
report = CompareReport(
|
|
2265
|
+
status=policy_result.status,
|
|
2266
|
+
exact_verification_complete=hierarchy.identical,
|
|
2267
|
+
old_input=old_metadata | {"selected_top": hierarchy.old_top, "extraction": {"mode": "hierarchy-fast"}},
|
|
2268
|
+
new_input=new_metadata | {"selected_top": hierarchy.new_top, "extraction": {"mode": "hierarchy-fast"}},
|
|
2269
|
+
semantics=config.to_json_dict(),
|
|
2270
|
+
selection={"old_top": hierarchy.old_top, "new_top": hierarchy.new_top, "layers": "all"},
|
|
2271
|
+
backend=backend_info | {"exact_mode": False, "hierarchy_fast_path": True},
|
|
2272
|
+
prefilter=prefilter.to_json(),
|
|
2273
|
+
exact_fallback={
|
|
2274
|
+
"mode": "hierarchy-digest",
|
|
2275
|
+
"window_count": len(hierarchy.changed_references),
|
|
2276
|
+
"escalated_layers": [],
|
|
2277
|
+
"failures": [],
|
|
2278
|
+
"old_digest": hierarchy.old_digest,
|
|
2279
|
+
"new_digest": hierarchy.new_digest,
|
|
2280
|
+
"matched_subtree_count": hierarchy.matched_subtree_count,
|
|
2281
|
+
"changed_reference_count": len(hierarchy.changed_references),
|
|
2282
|
+
},
|
|
2283
|
+
layers=layer_reports,
|
|
2284
|
+
allowed_policy=policy_result.effective_policy,
|
|
2285
|
+
unexpected_summary={
|
|
2286
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
2287
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
2288
|
+
},
|
|
2289
|
+
timings=timings,
|
|
2290
|
+
warnings=warnings,
|
|
2291
|
+
artifacts=artifacts,
|
|
2292
|
+
command=command,
|
|
2293
|
+
failure_policy=config.failure_policy,
|
|
2294
|
+
)
|
|
2295
|
+
if markdown_path is not None:
|
|
2296
|
+
markdown_path = Path(markdown_path)
|
|
2297
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2298
|
+
markdown_path.write_text(
|
|
2299
|
+
render_design_history_entry(
|
|
2300
|
+
"GDS comparator hierarchy-fast result",
|
|
2301
|
+
policy_result,
|
|
2302
|
+
tests_run=("gdsdiff hierarchy-fast",),
|
|
2303
|
+
notes=warnings,
|
|
2304
|
+
),
|
|
2305
|
+
encoding="utf-8",
|
|
2306
|
+
)
|
|
2307
|
+
artifacts["markdown"] = str(markdown_path)
|
|
2308
|
+
if exact_diff_markdown_path is not None:
|
|
2309
|
+
artifacts["exact_diff_markdown"] = str(
|
|
2310
|
+
write_exact_diff_markdown(
|
|
2311
|
+
policy_result,
|
|
2312
|
+
exact_diff_markdown_path,
|
|
2313
|
+
title="GDS comparator hierarchy-fast exact diff",
|
|
2314
|
+
source_mode="hierarchy-digest",
|
|
2315
|
+
exact_verification_complete=hierarchy.identical,
|
|
2316
|
+
).path
|
|
2317
|
+
)
|
|
2318
|
+
if json_report_path is not None:
|
|
2319
|
+
artifacts["json_report"] = str(json_report_path)
|
|
2320
|
+
report = _report_with_artifacts(report, artifacts)
|
|
2321
|
+
report.write_json(json_report_path)
|
|
2322
|
+
return CompareRunResult(
|
|
2323
|
+
status=policy_result.status,
|
|
2324
|
+
report=report,
|
|
2325
|
+
exact_result=exact_result,
|
|
2326
|
+
policy_result=policy_result,
|
|
2327
|
+
prefilter=prefilter,
|
|
2328
|
+
windows=windows,
|
|
2329
|
+
artifacts=artifacts,
|
|
2330
|
+
timings=timings,
|
|
2331
|
+
)
|
|
2332
|
+
|
|
2333
|
+
|
|
2334
|
+
def _try_accelerated_byte_identity_fast_path(
|
|
2335
|
+
config: CompareConfig,
|
|
2336
|
+
*,
|
|
2337
|
+
backend_info: dict[str, object],
|
|
2338
|
+
old_metadata: dict[str, object],
|
|
2339
|
+
new_metadata: dict[str, object],
|
|
2340
|
+
old_top_name: str | None,
|
|
2341
|
+
new_top_name: str | None,
|
|
2342
|
+
command: tuple[str, ...],
|
|
2343
|
+
json_report_path: str | Path | None,
|
|
2344
|
+
markdown_path: str | Path | None,
|
|
2345
|
+
exact_diff_markdown_path: str | Path | None,
|
|
2346
|
+
verify_exact_after_fast: bool,
|
|
2347
|
+
exact_component_backend: str,
|
|
2348
|
+
start: float,
|
|
2349
|
+
) -> CompareRunResult | None:
|
|
2350
|
+
identity = _file_identity(config.old_path, config.new_path, preferred_backend=str(backend_info.get("name", "")))
|
|
2351
|
+
if identity.same:
|
|
2352
|
+
exact_result = ExactOracleResult(())
|
|
2353
|
+
exact_complete = True
|
|
2354
|
+
else:
|
|
2355
|
+
fragment = DiffFragment(points=((0, 0), (1, 0), (1, 1), (0, 1)), bbox=(0, 0, 1, 1), twice_area=2)
|
|
2356
|
+
exact_result = ExactOracleResult((LayerExactDiff(layer=LayerSpec(0, 0), new_minus_old=(fragment,), xor=(fragment,)),))
|
|
2357
|
+
exact_complete = False
|
|
2358
|
+
policy_result = classify_exact_differences(exact_result, failure_policy=config.failure_policy)
|
|
2359
|
+
fast_status = policy_result.status
|
|
2360
|
+
fast_status_s = perf_counter() - start
|
|
2361
|
+
prefilter = CpuTilePrefilterResult(candidate_tiles=())
|
|
2362
|
+
windows: tuple[CandidateWindow, ...] = ()
|
|
2363
|
+
verification: _FullExactVerification | None = None
|
|
2364
|
+
if verify_exact_after_fast:
|
|
2365
|
+
verification = _run_full_exact_verification(config, exact_component_backend=exact_component_backend)
|
|
2366
|
+
exact_result = verification.exact_result
|
|
2367
|
+
policy_result = verification.policy_result
|
|
2368
|
+
exact_complete = True
|
|
2369
|
+
timings = {"extract_s": 0.0, "exact_s": fast_status_s, "fast_status_s": fast_status_s}
|
|
2370
|
+
if verification is not None:
|
|
2371
|
+
timings.update(
|
|
2372
|
+
{
|
|
2373
|
+
"extract_s": verification.extract_s,
|
|
2374
|
+
"exact_s": verification.exact_s,
|
|
2375
|
+
"exact_verification_s": verification.extract_s + verification.exact_s,
|
|
2376
|
+
}
|
|
2377
|
+
)
|
|
2378
|
+
timings["total_s"] = perf_counter() - start
|
|
2379
|
+
artifacts: dict[str, str] = {}
|
|
2380
|
+
warnings = _fast_path_warnings(verified=verification is not None)
|
|
2381
|
+
exact_fallback_mode = "file-digest-fast-plus-full-exact" if verification is not None else "file-digest-fail-closed"
|
|
2382
|
+
old_input = old_metadata | {"selected_top": old_top_name, "extraction": {"mode": "accelerated-byte-identity"}}
|
|
2383
|
+
new_input = new_metadata | {"selected_top": new_top_name, "extraction": {"mode": "accelerated-byte-identity"}}
|
|
2384
|
+
selection = {"old_top": old_top_name, "new_top": new_top_name, "layers": "all"}
|
|
2385
|
+
layers = build_layer_reports(exact_result, policy_result=policy_result)
|
|
2386
|
+
if verification is not None:
|
|
2387
|
+
old_input = verification.old_input
|
|
2388
|
+
new_input = verification.new_input
|
|
2389
|
+
selection = verification.selection
|
|
2390
|
+
layers = verification.layers
|
|
2391
|
+
exact_fallback = {
|
|
2392
|
+
"mode": exact_fallback_mode,
|
|
2393
|
+
"window_count": 0,
|
|
2394
|
+
"escalated_layers": [],
|
|
2395
|
+
"failures": [],
|
|
2396
|
+
"old_identity": identity.old_token,
|
|
2397
|
+
"new_identity": identity.new_token,
|
|
2398
|
+
"identity_method": identity.method,
|
|
2399
|
+
"old_size": identity.old_size,
|
|
2400
|
+
"new_size": identity.new_size,
|
|
2401
|
+
"fast_status": fast_status.value,
|
|
2402
|
+
}
|
|
2403
|
+
if verification is not None:
|
|
2404
|
+
exact_fallback["exact_component_backend"] = verification.exact_component_backend
|
|
2405
|
+
report = CompareReport(
|
|
2406
|
+
status=policy_result.status,
|
|
2407
|
+
exact_verification_complete=exact_complete,
|
|
2408
|
+
old_input=old_input,
|
|
2409
|
+
new_input=new_input,
|
|
2410
|
+
semantics=config.to_json_dict(),
|
|
2411
|
+
selection=selection,
|
|
2412
|
+
backend=backend_info
|
|
2413
|
+
| {
|
|
2414
|
+
"exact_mode": False,
|
|
2415
|
+
"file_digest_fast_path": True,
|
|
2416
|
+
"accelerated_byte_identity_fast_path": True,
|
|
2417
|
+
"exact_verification_after_fast": verification is not None,
|
|
2418
|
+
},
|
|
2419
|
+
prefilter=prefilter.to_json(),
|
|
2420
|
+
exact_fallback=exact_fallback,
|
|
2421
|
+
layers=layers,
|
|
2422
|
+
allowed_policy=policy_result.effective_policy,
|
|
2423
|
+
unexpected_summary={
|
|
2424
|
+
"expected_area_dbu2": policy_result.to_json()["expected_area_dbu2"],
|
|
2425
|
+
"unexpected_area_dbu2": policy_result.to_json()["unexpected_area_dbu2"],
|
|
2426
|
+
},
|
|
2427
|
+
timings=timings,
|
|
2428
|
+
warnings=warnings,
|
|
2429
|
+
artifacts=artifacts,
|
|
2430
|
+
command=command,
|
|
2431
|
+
failure_policy=config.failure_policy,
|
|
2432
|
+
)
|
|
2433
|
+
if markdown_path is not None:
|
|
2434
|
+
markdown_path = Path(markdown_path)
|
|
2435
|
+
markdown_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2436
|
+
markdown_path.write_text(
|
|
2437
|
+
render_design_history_entry(
|
|
2438
|
+
"GDS comparator accelerated-byte-identity result",
|
|
2439
|
+
policy_result,
|
|
2440
|
+
tests_run=("gdsdiff accelerated-byte-identity",),
|
|
2441
|
+
notes=warnings,
|
|
2442
|
+
),
|
|
2443
|
+
encoding="utf-8",
|
|
2444
|
+
)
|
|
2445
|
+
artifacts["markdown"] = str(markdown_path)
|
|
2446
|
+
if exact_diff_markdown_path is not None:
|
|
2447
|
+
artifacts["exact_diff_markdown"] = str(
|
|
2448
|
+
write_exact_diff_markdown(
|
|
2449
|
+
policy_result,
|
|
2450
|
+
exact_diff_markdown_path,
|
|
2451
|
+
title="GDS comparator accelerated-byte-identity exact diff",
|
|
2452
|
+
source_mode=exact_fallback_mode,
|
|
2453
|
+
exact_verification_complete=exact_complete,
|
|
2454
|
+
polygon_inventory_delta=verification.polygon_inventory_delta if verification is not None else None,
|
|
2455
|
+
).path
|
|
2456
|
+
)
|
|
2457
|
+
if json_report_path is not None:
|
|
2458
|
+
artifacts["json_report"] = str(json_report_path)
|
|
2459
|
+
report = _report_with_artifacts(report, artifacts)
|
|
2460
|
+
report.write_json(json_report_path)
|
|
2461
|
+
return CompareRunResult(
|
|
2462
|
+
status=policy_result.status,
|
|
2463
|
+
report=report,
|
|
2464
|
+
exact_result=exact_result,
|
|
2465
|
+
policy_result=policy_result,
|
|
2466
|
+
prefilter=prefilter,
|
|
2467
|
+
windows=windows,
|
|
2468
|
+
artifacts=artifacts,
|
|
2469
|
+
timings=timings,
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
|
|
2473
|
+
def _select_exact_component_backend(name: str) -> _ExactComponentSelection:
|
|
2474
|
+
normalized = str(name or "none").strip().lower()
|
|
2475
|
+
if normalized in {"none", "gdstk", "gdstk-full"}:
|
|
2476
|
+
return _ExactComponentSelection(name=normalized, component_partition=False, executor=None)
|
|
2477
|
+
if normalized in {"gdstk-component", "component-gdstk"}:
|
|
2478
|
+
backend = GdstkExactComponentBackend()
|
|
2479
|
+
return _ExactComponentSelection(
|
|
2480
|
+
name=normalized,
|
|
2481
|
+
component_partition=True,
|
|
2482
|
+
executor=backend.compare_components,
|
|
2483
|
+
backend=backend,
|
|
2484
|
+
)
|
|
2485
|
+
if normalized in {"rect", "manhattan-rect"}:
|
|
2486
|
+
backend = ManhattanRectExactComponentBackend(fallback_to_gdstk=False)
|
|
2487
|
+
return _ExactComponentSelection(
|
|
2488
|
+
name=normalized,
|
|
2489
|
+
component_partition=True,
|
|
2490
|
+
executor=backend.compare_components,
|
|
2491
|
+
backend=backend,
|
|
2492
|
+
)
|
|
2493
|
+
if normalized in {"rect-fallback", "manhattan-rect-fallback"}:
|
|
2494
|
+
backend = ManhattanRectExactComponentBackend(fallback_to_gdstk=True)
|
|
2495
|
+
return _ExactComponentSelection(
|
|
2496
|
+
name=normalized,
|
|
2497
|
+
component_partition=True,
|
|
2498
|
+
executor=backend.compare_components,
|
|
2499
|
+
backend=backend,
|
|
2500
|
+
)
|
|
2501
|
+
if normalized in {"cuda-rect", "cuda-manhattan-rect"}:
|
|
2502
|
+
backend = CudaManhattanRectExactComponentBackend(fallback_to_gdstk=False)
|
|
2503
|
+
return _ExactComponentSelection(
|
|
2504
|
+
name=normalized,
|
|
2505
|
+
component_partition=True,
|
|
2506
|
+
executor=backend.compare_components,
|
|
2507
|
+
backend=backend,
|
|
2508
|
+
)
|
|
2509
|
+
if normalized in {"cuda-rect-fallback", "cuda-manhattan-rect-fallback"}:
|
|
2510
|
+
backend = CudaManhattanRectExactComponentBackend(fallback_to_gdstk=True)
|
|
2511
|
+
return _ExactComponentSelection(
|
|
2512
|
+
name=normalized,
|
|
2513
|
+
component_partition=True,
|
|
2514
|
+
executor=backend.compare_components,
|
|
2515
|
+
backend=backend,
|
|
2516
|
+
)
|
|
2517
|
+
if normalized in {"metal-rect", "metal-manhattan-rect"}:
|
|
2518
|
+
backend = MetalManhattanRectExactComponentBackend(fallback_to_gdstk=False)
|
|
2519
|
+
return _ExactComponentSelection(
|
|
2520
|
+
name=normalized,
|
|
2521
|
+
component_partition=True,
|
|
2522
|
+
executor=backend.compare_components,
|
|
2523
|
+
backend=backend,
|
|
2524
|
+
)
|
|
2525
|
+
if normalized in {"metal-rect-fallback", "metal-manhattan-rect-fallback"}:
|
|
2526
|
+
backend = MetalManhattanRectExactComponentBackend(fallback_to_gdstk=True)
|
|
2527
|
+
return _ExactComponentSelection(
|
|
2528
|
+
name=normalized,
|
|
2529
|
+
component_partition=True,
|
|
2530
|
+
executor=backend.compare_components,
|
|
2531
|
+
backend=backend,
|
|
2532
|
+
)
|
|
2533
|
+
raise ValueError(f"unknown exact component backend: {name!r}")
|
|
2534
|
+
|
|
2535
|
+
|
|
2536
|
+
def _run_full_exact_verification(
|
|
2537
|
+
config: CompareConfig,
|
|
2538
|
+
*,
|
|
2539
|
+
exact_component_backend: str = "none",
|
|
2540
|
+
force_small_component_partition: bool = False,
|
|
2541
|
+
) -> _FullExactVerification:
|
|
2542
|
+
start = perf_counter()
|
|
2543
|
+
old_metadata = read_gds_metadata(config.old_path)
|
|
2544
|
+
new_metadata = read_gds_metadata(config.new_path)
|
|
2545
|
+
old_top_name = config.old_top or config.top
|
|
2546
|
+
new_top_name = config.new_top or config.top
|
|
2547
|
+
if old_top_name is None and new_top_name is None:
|
|
2548
|
+
common_tops = sorted(set(old_metadata.top_names) & set(new_metadata.top_names))
|
|
2549
|
+
if len(common_tops) == 1:
|
|
2550
|
+
old_top_name = common_tops[0]
|
|
2551
|
+
new_top_name = common_tops[0]
|
|
2552
|
+
old_extraction, new_extraction = _extract_pair(
|
|
2553
|
+
config,
|
|
2554
|
+
old_top_name=old_top_name,
|
|
2555
|
+
new_top_name=new_top_name,
|
|
2556
|
+
)
|
|
2557
|
+
extract_s = perf_counter() - start
|
|
2558
|
+
exact_start = perf_counter()
|
|
2559
|
+
exact_selection = _select_exact_component_backend(exact_component_backend)
|
|
2560
|
+
exact_result = run_full_exact_oracle(
|
|
2561
|
+
old_extraction.buffer,
|
|
2562
|
+
new_extraction.buffer,
|
|
2563
|
+
component_partition=exact_selection.component_partition,
|
|
2564
|
+
component_executor=exact_selection.executor,
|
|
2565
|
+
force_small_component_partition=force_small_component_partition,
|
|
2566
|
+
)
|
|
2567
|
+
exact_s = perf_counter() - exact_start
|
|
2568
|
+
policy_result = classify_exact_differences(
|
|
2569
|
+
exact_result,
|
|
2570
|
+
config.allowed_regions,
|
|
2571
|
+
failure_policy=config.failure_policy,
|
|
2572
|
+
)
|
|
2573
|
+
layer_reports = build_layer_reports(
|
|
2574
|
+
exact_result,
|
|
2575
|
+
policy_result=policy_result,
|
|
2576
|
+
presence_by_layer=_presence_by_layer(old_extraction.buffer, new_extraction.buffer),
|
|
2577
|
+
polygon_counts_by_layer=_polygon_counts_by_layer(old_extraction.buffer, new_extraction.buffer),
|
|
2578
|
+
exact_status="complete",
|
|
2579
|
+
)
|
|
2580
|
+
return _FullExactVerification(
|
|
2581
|
+
exact_result=exact_result,
|
|
2582
|
+
policy_result=policy_result,
|
|
2583
|
+
old_input=_input_json(old_extraction, old_metadata.to_json()),
|
|
2584
|
+
new_input=_input_json(new_extraction, new_metadata.to_json()),
|
|
2585
|
+
selection={
|
|
2586
|
+
"old_top": old_extraction.top_name,
|
|
2587
|
+
"new_top": new_extraction.top_name,
|
|
2588
|
+
"layers": config.to_json_dict()["layers"],
|
|
2589
|
+
},
|
|
2590
|
+
layers=layer_reports,
|
|
2591
|
+
extract_s=extract_s,
|
|
2592
|
+
exact_s=exact_s,
|
|
2593
|
+
exact_component_backend=exact_selection.to_json(),
|
|
2594
|
+
polygon_inventory_delta=build_polygon_inventory_delta(old_extraction.buffer, new_extraction.buffer),
|
|
2595
|
+
)
|
|
2596
|
+
|
|
2597
|
+
|
|
2598
|
+
def _fast_path_warnings(*, verified: bool) -> tuple[str, ...]:
|
|
2599
|
+
if verified:
|
|
2600
|
+
return (
|
|
2601
|
+
"Accelerated byte-identity fast path produced the status verdict; full exact gdstk verification was run after the fast verdict and supplies the polygon-level evidence.",
|
|
2602
|
+
)
|
|
2603
|
+
return (
|
|
2604
|
+
"Accelerated byte-identity fast path used; differing files are reported fail-closed and require exact mode or verify_exact_after_fast for geometry evidence.",
|
|
2605
|
+
)
|
|
2606
|
+
|
|
2607
|
+
|
|
2608
|
+
def _accelerated_fast_path_eligible(config: CompareConfig, backend_info: dict[str, object]) -> bool:
|
|
2609
|
+
return (
|
|
2610
|
+
backend_info.get("name") in {Backend.CUDA.value, Backend.METAL.value}
|
|
2611
|
+
and not config.allowed_regions
|
|
2612
|
+
and config.layers == "all"
|
|
2613
|
+
)
|
|
2614
|
+
|
|
2615
|
+
|
|
2616
|
+
def _cuda_fast_path_eligible(config: CompareConfig, backend_info: dict[str, object]) -> bool:
|
|
2617
|
+
return _accelerated_fast_path_eligible(config, backend_info)
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
def _try_cuda_file_digest_fast_path(*args, **kwargs) -> CompareRunResult | None:
|
|
2621
|
+
return _try_accelerated_byte_identity_fast_path(*args, **kwargs)
|
|
2622
|
+
|
|
2623
|
+
|
|
2624
|
+
def _file_sha256(path: Path) -> str:
|
|
2625
|
+
digest = hashlib.sha256()
|
|
2626
|
+
with Path(path).open("rb") as handle:
|
|
2627
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
2628
|
+
digest.update(chunk)
|
|
2629
|
+
return digest.hexdigest()
|
|
2630
|
+
|
|
2631
|
+
|
|
2632
|
+
def _file_identity(old_path: Path, new_path: Path, *, preferred_backend: str | None = None) -> _FileIdentity:
|
|
2633
|
+
old_path = Path(old_path)
|
|
2634
|
+
new_path = Path(new_path)
|
|
2635
|
+
old_stat = old_path.stat()
|
|
2636
|
+
new_stat = new_path.stat()
|
|
2637
|
+
if old_stat.st_ino == new_stat.st_ino and old_stat.st_dev == new_stat.st_dev:
|
|
2638
|
+
token = f"inode:{old_stat.st_dev}:{old_stat.st_ino}:size:{old_stat.st_size}"
|
|
2639
|
+
return _FileIdentity(token, token, True, "inode", old_stat.st_size, new_stat.st_size)
|
|
2640
|
+
if old_stat.st_size != new_stat.st_size:
|
|
2641
|
+
return _FileIdentity(
|
|
2642
|
+
f"size:{old_stat.st_size}",
|
|
2643
|
+
f"size:{new_stat.st_size}",
|
|
2644
|
+
False,
|
|
2645
|
+
"size",
|
|
2646
|
+
old_stat.st_size,
|
|
2647
|
+
new_stat.st_size,
|
|
2648
|
+
)
|
|
2649
|
+
try:
|
|
2650
|
+
from .backends.identity import compare_file_identity_accelerated
|
|
2651
|
+
|
|
2652
|
+
cuda_result = compare_file_identity_accelerated(old_path, new_path, preferred_backend=preferred_backend)
|
|
2653
|
+
return _FileIdentity(
|
|
2654
|
+
f"{cuda_result.method}-size:{cuda_result.old_size}",
|
|
2655
|
+
f"{cuda_result.method}-size:{cuda_result.new_size}",
|
|
2656
|
+
cuda_result.same,
|
|
2657
|
+
cuda_result.method,
|
|
2658
|
+
cuda_result.old_size,
|
|
2659
|
+
cuda_result.new_size,
|
|
2660
|
+
)
|
|
2661
|
+
except Exception:
|
|
2662
|
+
pass
|
|
2663
|
+
old_digest = _file_sha256(old_path)
|
|
2664
|
+
new_digest = _file_sha256(new_path)
|
|
2665
|
+
return _FileIdentity(old_digest, new_digest, old_digest == new_digest, "sha256", old_stat.st_size, new_stat.st_size)
|
|
2666
|
+
|
|
2667
|
+
|
|
2668
|
+
def _select_backend(config: CompareConfig, *, exact: bool = False) -> dict[str, object]:
|
|
2669
|
+
requested = Backend.coerce(config.backend)
|
|
2670
|
+
capabilities = discover_native_capabilities()
|
|
2671
|
+
if (
|
|
2672
|
+
not exact
|
|
2673
|
+
and not capabilities.cuda_available
|
|
2674
|
+
and capabilities.nvcc_available
|
|
2675
|
+
and (requested == Backend.CUDA or (requested == Backend.AUTO and config.require_gpu))
|
|
2676
|
+
):
|
|
2677
|
+
try:
|
|
2678
|
+
from .cuda_setup import prepare_cuda
|
|
2679
|
+
|
|
2680
|
+
prepare_cuda()
|
|
2681
|
+
capabilities = discover_native_capabilities()
|
|
2682
|
+
except Exception:
|
|
2683
|
+
pass
|
|
2684
|
+
base = {
|
|
2685
|
+
"requested": requested.value,
|
|
2686
|
+
"cuda_available": capabilities.cuda_available,
|
|
2687
|
+
"cuda_extension_available": capabilities.cuda_extension_available,
|
|
2688
|
+
"cuda_ctypes_available": capabilities.cuda_ctypes_available,
|
|
2689
|
+
"metal_available": capabilities.metal_available,
|
|
2690
|
+
"metal_ctypes_available": capabilities.metal_ctypes_available,
|
|
2691
|
+
"core_extension_available": capabilities.core_extension_available,
|
|
2692
|
+
"nvcc_available": capabilities.nvcc_available,
|
|
2693
|
+
}
|
|
2694
|
+
if requested == Backend.CPU:
|
|
2695
|
+
if config.require_gpu:
|
|
2696
|
+
raise ValueError("require_gpu cannot be satisfied by backend='cpu'")
|
|
2697
|
+
return base | {"name": Backend.CPU.value, "fallback_reason": None}
|
|
2698
|
+
if exact:
|
|
2699
|
+
return base | {"name": Backend.CPU.value, "fallback_reason": "exact mode uses the gdstk CPU oracle"}
|
|
2700
|
+
if requested == Backend.CUDA:
|
|
2701
|
+
if not capabilities.cuda_available:
|
|
2702
|
+
raise ValueError("backend='cuda' requested, but CUDA backend extension is not available")
|
|
2703
|
+
return base | {"name": Backend.CUDA.value, "fallback_reason": None}
|
|
2704
|
+
if requested == Backend.METAL:
|
|
2705
|
+
if not capabilities.metal_available:
|
|
2706
|
+
raise ValueError("backend='metal' requested, but Metal backend runtime is not available")
|
|
2707
|
+
return base | {"name": Backend.METAL.value, "fallback_reason": None}
|
|
2708
|
+
if requested == Backend.AUTO:
|
|
2709
|
+
if config.require_gpu:
|
|
2710
|
+
if capabilities.cuda_available:
|
|
2711
|
+
return base | {"name": Backend.CUDA.value, "fallback_reason": None}
|
|
2712
|
+
if capabilities.metal_available:
|
|
2713
|
+
return base | {"name": Backend.METAL.value, "fallback_reason": None}
|
|
2714
|
+
raise ValueError("require_gpu=True but no CUDA or Metal backend runtime is available")
|
|
2715
|
+
if capabilities.cuda_available:
|
|
2716
|
+
return base | {"name": Backend.CUDA.value, "fallback_reason": None}
|
|
2717
|
+
if capabilities.metal_available:
|
|
2718
|
+
return base | {"name": Backend.METAL.value, "fallback_reason": None}
|
|
2719
|
+
return base | {"name": Backend.CPU.value, "fallback_reason": "no CUDA or Metal backend runtime is available"}
|
|
2720
|
+
raise ValueError(f"backend {requested.value!r} is not implemented")
|
|
2721
|
+
|
|
2722
|
+
|
|
2723
|
+
def _run_tile_prefilter(
|
|
2724
|
+
backend_info: dict[str, object],
|
|
2725
|
+
old_buffer: PolygonBuffer,
|
|
2726
|
+
old_canonical_ids,
|
|
2727
|
+
new_buffer: PolygonBuffer,
|
|
2728
|
+
new_canonical_ids,
|
|
2729
|
+
tile_config: TileConfig,
|
|
2730
|
+
) -> CpuTilePrefilterResult:
|
|
2731
|
+
if backend_info.get("name") == Backend.CUDA.value:
|
|
2732
|
+
from .backends.cuda_ctypes import CudaCtypesUnavailable, run_cuda_ctypes_tile_prefilter
|
|
2733
|
+
|
|
2734
|
+
try:
|
|
2735
|
+
return run_cuda_ctypes_tile_prefilter(old_buffer, old_canonical_ids, new_buffer, new_canonical_ids, tile_config)
|
|
2736
|
+
except CudaCtypesUnavailable:
|
|
2737
|
+
pass
|
|
2738
|
+
if backend_info.get("name") == Backend.METAL.value:
|
|
2739
|
+
from .backends.metal_ctypes import MetalCtypesUnavailable, run_metal_ctypes_tile_prefilter
|
|
2740
|
+
|
|
2741
|
+
try:
|
|
2742
|
+
return run_metal_ctypes_tile_prefilter(old_buffer, old_canonical_ids, new_buffer, new_canonical_ids, tile_config)
|
|
2743
|
+
except MetalCtypesUnavailable:
|
|
2744
|
+
pass
|
|
2745
|
+
try:
|
|
2746
|
+
from .backends.cpu_ctypes import run_cpu_ctypes_tile_prefilter
|
|
2747
|
+
|
|
2748
|
+
return run_cpu_ctypes_tile_prefilter(old_buffer, old_canonical_ids, new_buffer, new_canonical_ids, tile_config)
|
|
2749
|
+
except Exception:
|
|
2750
|
+
pass
|
|
2751
|
+
return run_cpu_tile_prefilter(old_buffer, old_canonical_ids, new_buffer, new_canonical_ids, tile_config)
|
|
2752
|
+
|
|
2753
|
+
|
|
2754
|
+
def _should_use_layer_exact(
|
|
2755
|
+
backend_info: dict[str, object],
|
|
2756
|
+
old_buffer: PolygonBuffer,
|
|
2757
|
+
new_buffer: PolygonBuffer,
|
|
2758
|
+
windows: tuple[CandidateWindow, ...],
|
|
2759
|
+
) -> bool:
|
|
2760
|
+
if not windows:
|
|
2761
|
+
return False
|
|
2762
|
+
total_polygons = old_buffer.polygon_count + new_buffer.polygon_count
|
|
2763
|
+
if total_polygons == 0:
|
|
2764
|
+
return False
|
|
2765
|
+
estimated_window_polygons = sum(window.estimated_old_polygons + window.estimated_new_polygons for window in windows)
|
|
2766
|
+
if estimated_window_polygons > total_polygons * 2:
|
|
2767
|
+
return True
|
|
2768
|
+
if len(windows) > 64 and estimated_window_polygons > total_polygons:
|
|
2769
|
+
return True
|
|
2770
|
+
return False
|
|
2771
|
+
|
|
2772
|
+
|
|
2773
|
+
def _shared_canonical_ids(
|
|
2774
|
+
old_buffer: PolygonBuffer,
|
|
2775
|
+
new_buffer: PolygonBuffer,
|
|
2776
|
+
*,
|
|
2777
|
+
old_intern=None,
|
|
2778
|
+
new_intern=None,
|
|
2779
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2780
|
+
import numpy as np
|
|
2781
|
+
|
|
2782
|
+
old_intern = old_intern if old_intern is not None else intern_polygons(old_buffer)
|
|
2783
|
+
new_intern = new_intern if new_intern is not None else intern_polygons(new_buffer)
|
|
2784
|
+
unique_blobs = tuple(sorted(set(old_intern.unique_blobs) | set(new_intern.unique_blobs)))
|
|
2785
|
+
id_by_blob = {blob: index for index, blob in enumerate(unique_blobs)}
|
|
2786
|
+
old_by_index = {record.polygon_index: id_by_blob[record.blob] for record in old_intern.records}
|
|
2787
|
+
new_by_index = {record.polygon_index: id_by_blob[record.blob] for record in new_intern.records}
|
|
2788
|
+
return (
|
|
2789
|
+
np.asarray([old_by_index[index] for index in range(old_buffer.polygon_count)], dtype=np.uint64),
|
|
2790
|
+
np.asarray([new_by_index[index] for index in range(new_buffer.polygon_count)], dtype=np.uint64),
|
|
2791
|
+
)
|
|
2792
|
+
|
|
2793
|
+
|
|
2794
|
+
def _input_json(extraction: ExtractionResult, metadata: dict[str, object]) -> dict[str, object]:
|
|
2795
|
+
data = dict(metadata)
|
|
2796
|
+
data.update({"selected_top": extraction.top_name, "extraction": extraction.stats.to_json()})
|
|
2797
|
+
return data
|
|
2798
|
+
|
|
2799
|
+
|
|
2800
|
+
def _presence_by_layer(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> dict[LayerSpec, tuple[bool, bool]]:
|
|
2801
|
+
old_layers = _layers_present(old_buffer)
|
|
2802
|
+
new_layers = _layers_present(new_buffer)
|
|
2803
|
+
return {layer: (layer in old_layers, layer in new_layers) for layer in sorted(old_layers | new_layers)}
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
def _polygon_counts_by_layer(old_buffer: PolygonBuffer, new_buffer: PolygonBuffer) -> dict[LayerSpec, tuple[int, int]]:
|
|
2807
|
+
old_counts = _layer_polygon_counts(old_buffer)
|
|
2808
|
+
new_counts = _layer_polygon_counts(new_buffer)
|
|
2809
|
+
return {layer: (old_counts.get(layer, 0), new_counts.get(layer, 0)) for layer in sorted(set(old_counts) | set(new_counts))}
|
|
2810
|
+
|
|
2811
|
+
|
|
2812
|
+
def _layers_present(buffer: PolygonBuffer) -> set[LayerSpec]:
|
|
2813
|
+
return {buffer.layer_table.layers[int(layer_id)] for layer_id in buffer.layer_ids}
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
def _layer_polygon_counts(buffer: PolygonBuffer) -> dict[LayerSpec, int]:
|
|
2817
|
+
counts: dict[LayerSpec, int] = {}
|
|
2818
|
+
for layer_id in buffer.layer_ids:
|
|
2819
|
+
layer = buffer.layer_table.layers[int(layer_id)]
|
|
2820
|
+
counts[layer] = counts.get(layer, 0) + 1
|
|
2821
|
+
return counts
|
|
2822
|
+
|
|
2823
|
+
|
|
2824
|
+
def _candidate_tile_counts(prefilter: CpuTilePrefilterResult) -> dict[LayerSpec, int]:
|
|
2825
|
+
counts: dict[LayerSpec, int] = {}
|
|
2826
|
+
for tile in prefilter.candidate_tiles:
|
|
2827
|
+
counts[tile.layer] = counts.get(tile.layer, 0) + 1
|
|
2828
|
+
for candidate in prefilter.oversized_candidates:
|
|
2829
|
+
counts[candidate.layer] = counts.get(candidate.layer, 0) + 1
|
|
2830
|
+
return counts
|
|
2831
|
+
|
|
2832
|
+
|
|
2833
|
+
def _window_counts(windows: tuple[CandidateWindow, ...]) -> dict[LayerSpec, int]:
|
|
2834
|
+
counts: dict[LayerSpec, int] = {}
|
|
2835
|
+
for window in windows:
|
|
2836
|
+
counts[window.layer] = counts.get(window.layer, 0) + 1
|
|
2837
|
+
return counts
|
|
2838
|
+
|
|
2839
|
+
|
|
2840
|
+
def _record_exact_markdown_artifacts(
|
|
2841
|
+
artifacts: dict[str, str],
|
|
2842
|
+
written: ExactDiffMarkdownWriteResult,
|
|
2843
|
+
) -> None:
|
|
2844
|
+
artifacts["exact_diff_markdown"] = str(written.path)
|
|
2845
|
+
if written.vertex_sidecar_path is not None:
|
|
2846
|
+
artifacts["vertex_sidecar"] = str(written.vertex_sidecar_path)
|
|
2847
|
+
|
|
2848
|
+
|
|
2849
|
+
def _coverage_component_count(policy_result: PolicyClassificationResult) -> int:
|
|
2850
|
+
return sum(len(layer.components) for layer in policy_result.layers)
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
def _coverage_twice_area(policy_result: PolicyClassificationResult) -> int:
|
|
2854
|
+
return sum(component.twice_area for layer in policy_result.layers for component in layer.components)
|
|
2855
|
+
|
|
2856
|
+
|
|
2857
|
+
def _neutral_compare_status(
|
|
2858
|
+
policy_result: PolicyClassificationResult,
|
|
2859
|
+
*,
|
|
2860
|
+
polygon_inventory_delta: object | None,
|
|
2861
|
+
exact_complete: bool,
|
|
2862
|
+
unsupported_feature_count: int = 0,
|
|
2863
|
+
audit_match: bool | None = None,
|
|
2864
|
+
) -> CompareStatus:
|
|
2865
|
+
if audit_match is False:
|
|
2866
|
+
return CompareStatus.INTERNAL_MISMATCH
|
|
2867
|
+
if unsupported_feature_count:
|
|
2868
|
+
return CompareStatus.UNSUPPORTED_INPUT
|
|
2869
|
+
if not exact_complete:
|
|
2870
|
+
return CompareStatus.INCOMPLETE_EVIDENCE
|
|
2871
|
+
if _coverage_twice_area(policy_result) or _inventory_delta_count(polygon_inventory_delta):
|
|
2872
|
+
return CompareStatus.DIFFERENCES_FOUND
|
|
2873
|
+
return CompareStatus.IDENTICAL
|
|
2874
|
+
|
|
2875
|
+
|
|
2876
|
+
def _neutral_evidence_report_fields(
|
|
2877
|
+
status: CompareStatus,
|
|
2878
|
+
policy_result: PolicyClassificationResult,
|
|
2879
|
+
*,
|
|
2880
|
+
polygon_inventory_delta: object | None,
|
|
2881
|
+
exact_complete: bool,
|
|
2882
|
+
unsupported_feature_count: int,
|
|
2883
|
+
vertex_sidecar_path: str | None,
|
|
2884
|
+
) -> dict[str, object]:
|
|
2885
|
+
coverage_area = _coverage_twice_area(policy_result)
|
|
2886
|
+
inventory_delta_count = _inventory_delta_count(polygon_inventory_delta)
|
|
2887
|
+
inventory_changed = inventory_delta_count > 0
|
|
2888
|
+
return {
|
|
2889
|
+
"evidence_status": status.value if status in {
|
|
2890
|
+
CompareStatus.IDENTICAL,
|
|
2891
|
+
CompareStatus.DIFFERENCES_FOUND,
|
|
2892
|
+
CompareStatus.UNSUPPORTED_INPUT,
|
|
2893
|
+
CompareStatus.INCOMPLETE_EVIDENCE,
|
|
2894
|
+
CompareStatus.INTERNAL_MISMATCH,
|
|
2895
|
+
} else (
|
|
2896
|
+
"identical"
|
|
2897
|
+
if status == CompareStatus.EQUIVALENT
|
|
2898
|
+
else "differences-found"
|
|
2899
|
+
if status in {CompareStatus.EXPECTED_DIFFERENCES_ONLY, CompareStatus.UNEXPECTED_DIFFERENCES}
|
|
2900
|
+
else "incomplete-evidence"
|
|
2901
|
+
),
|
|
2902
|
+
"differences_found": bool(coverage_area or inventory_changed),
|
|
2903
|
+
"coverage_changed": bool(coverage_area),
|
|
2904
|
+
"inventory_changed": inventory_changed,
|
|
2905
|
+
"coverage_equal_but_inventory_changed": bool(exact_complete and coverage_area == 0 and inventory_changed),
|
|
2906
|
+
"coverage_component_count": _coverage_component_count(policy_result),
|
|
2907
|
+
"inventory_delta_count": inventory_delta_count,
|
|
2908
|
+
"vertex_sidecar_path": vertex_sidecar_path,
|
|
2909
|
+
"unsupported_feature_count": unsupported_feature_count,
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
|
|
2913
|
+
def _inventory_delta_count(polygon_inventory_delta: object | None) -> int:
|
|
2914
|
+
return int(getattr(polygon_inventory_delta, "changed_count", 0) or 0)
|
|
2915
|
+
|
|
2916
|
+
|
|
2917
|
+
def _gpu_polygon_unsupported_feature_count(exact_fallback: dict[str, object], *, exact_complete: bool) -> int:
|
|
2918
|
+
if exact_complete:
|
|
2919
|
+
return 0
|
|
2920
|
+
failures = exact_fallback.get("failures", ())
|
|
2921
|
+
return len(failures) if isinstance(failures, list) else 0
|
|
2922
|
+
|
|
2923
|
+
|
|
2924
|
+
def _report_with_artifacts(report: CompareReport, artifacts: dict[str, str]) -> CompareReport:
|
|
2925
|
+
return CompareReport(
|
|
2926
|
+
status=report.status,
|
|
2927
|
+
exact_verification_complete=report.exact_verification_complete,
|
|
2928
|
+
old_input=report.old_input,
|
|
2929
|
+
new_input=report.new_input,
|
|
2930
|
+
semantics=report.semantics,
|
|
2931
|
+
selection=report.selection,
|
|
2932
|
+
backend=report.backend,
|
|
2933
|
+
prefilter=report.prefilter,
|
|
2934
|
+
exact_fallback=report.exact_fallback,
|
|
2935
|
+
layers=report.layers,
|
|
2936
|
+
allowed_policy=report.allowed_policy,
|
|
2937
|
+
unexpected_summary=report.unexpected_summary,
|
|
2938
|
+
timings=report.timings,
|
|
2939
|
+
memory=report.memory,
|
|
2940
|
+
warnings=report.warnings,
|
|
2941
|
+
errors=report.errors,
|
|
2942
|
+
artifacts=artifacts,
|
|
2943
|
+
evidence_status=report.evidence_status,
|
|
2944
|
+
differences_found=report.differences_found,
|
|
2945
|
+
coverage_changed=report.coverage_changed,
|
|
2946
|
+
inventory_changed=report.inventory_changed,
|
|
2947
|
+
coverage_equal_but_inventory_changed=report.coverage_equal_but_inventory_changed,
|
|
2948
|
+
coverage_component_count=report.coverage_component_count,
|
|
2949
|
+
inventory_delta_count=report.inventory_delta_count,
|
|
2950
|
+
vertex_sidecar_path=report.vertex_sidecar_path,
|
|
2951
|
+
unsupported_feature_count=report.unsupported_feature_count,
|
|
2952
|
+
command=report.command,
|
|
2953
|
+
failure_policy=report.failure_policy,
|
|
2954
|
+
tool_version=report.tool_version,
|
|
2955
|
+
)
|