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.
Files changed (85) hide show
  1. gdsdiff/__init__.py +148 -0
  2. gdsdiff/__main__.py +7 -0
  3. gdsdiff/_environment.py +12 -0
  4. gdsdiff/_version.py +34 -0
  5. gdsdiff/acceptance_evidence.py +530 -0
  6. gdsdiff/api.py +2955 -0
  7. gdsdiff/backends/__init__.py +26 -0
  8. gdsdiff/backends/base.py +77 -0
  9. gdsdiff/backends/boundary_loop_ctypes.py +191 -0
  10. gdsdiff/backends/cpu.py +20 -0
  11. gdsdiff/backends/cpu_ctypes.py +255 -0
  12. gdsdiff/backends/cuda.py +40 -0
  13. gdsdiff/backends/cuda_ctypes.py +1297 -0
  14. gdsdiff/backends/exact.py +424 -0
  15. gdsdiff/backends/geometry_ctypes.py +252 -0
  16. gdsdiff/backends/identity.py +53 -0
  17. gdsdiff/backends/metal.py +198 -0
  18. gdsdiff/backends/metal_ctypes.py +866 -0
  19. gdsdiff/backends/polygon_extract_ctypes.py +233 -0
  20. gdsdiff/backends/structural_ctypes.py +130 -0
  21. gdsdiff/boundary_loops.py +616 -0
  22. gdsdiff/cache.py +544 -0
  23. gdsdiff/canonicalize.py +176 -0
  24. gdsdiff/cli.py +352 -0
  25. gdsdiff/config.py +257 -0
  26. gdsdiff/cuda_setup.py +174 -0
  27. gdsdiff/design_history.py +65 -0
  28. gdsdiff/diagnostics.py +42 -0
  29. gdsdiff/diff_gds.py +66 -0
  30. gdsdiff/exact_diff_markdown.py +979 -0
  31. gdsdiff/exact_oracle.py +649 -0
  32. gdsdiff/extract.py +376 -0
  33. gdsdiff/gds_metadata.py +43 -0
  34. gdsdiff/geometry.py +112 -0
  35. gdsdiff/grid.py +143 -0
  36. gdsdiff/hierarchy.py +215 -0
  37. gdsdiff/hierarchy_extract.py +263 -0
  38. gdsdiff/inventory.py +153 -0
  39. gdsdiff/multiprocessing_support.py +39 -0
  40. gdsdiff/native.py +135 -0
  41. gdsdiff/native_cache.py +265 -0
  42. gdsdiff/native_src/cpu_prefilter.cpp +349 -0
  43. gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
  44. gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
  45. gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
  46. gdsdiff/native_src/gds_structural_scan.cpp +335 -0
  47. gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
  48. gdsdiff/native_src/gdsdiff/core.cpp +191 -0
  49. gdsdiff/native_src/gdsdiff/core.hpp +96 -0
  50. gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
  51. gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
  52. gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
  53. gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
  54. gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
  55. gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
  56. gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
  57. gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
  58. gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
  59. gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
  60. gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
  61. gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
  62. gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
  63. gdsdiff/policy.py +305 -0
  64. gdsdiff/polygon_components.py +860 -0
  65. gdsdiff/profiles.py +152 -0
  66. gdsdiff/py.typed +1 -0
  67. gdsdiff/rect_coverage.py +384 -0
  68. gdsdiff/report.py +354 -0
  69. gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
  70. gdsdiff/semantics.py +75 -0
  71. gdsdiff/source_edge_diff.py +1120 -0
  72. gdsdiff/spatial.py +71 -0
  73. gdsdiff/structural_guard.py +161 -0
  74. gdsdiff/surplus_coverage.py +255 -0
  75. gdsdiff/synthetic_suite.py +1643 -0
  76. gdsdiff/templates.py +709 -0
  77. gdsdiff/tiling.py +153 -0
  78. gdsdiff/windowed_exact.py +270 -0
  79. gdsdiff/windows.py +184 -0
  80. gdsdiff-0.1.0.dist-info/METADATA +135 -0
  81. gdsdiff-0.1.0.dist-info/RECORD +85 -0
  82. gdsdiff-0.1.0.dist-info/WHEEL +5 -0
  83. gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
  84. gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
  85. gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,530 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import threading
8
+ from contextlib import contextmanager
9
+ from dataclasses import dataclass
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from time import perf_counter
13
+ from typing import Any
14
+
15
+ from .api import CompareRunResult, compare_gds_files
16
+ from .cache import load_or_build_cached_geometry
17
+ from .config import CompareConfig
18
+ from .native import discover_native_capabilities
19
+
20
+ SCHEMA_VERSION = "gdsdiff-acceptance-evidence-v1"
21
+ TOP_NAME_OVERRIDES = {
22
+ "medium": "medium_compare_top",
23
+ "equal_count": "equal_count_top",
24
+ }
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class AcceptanceCorpusPair:
29
+ name: str
30
+ before: Path
31
+ after: Path
32
+ top: str | None = None
33
+
34
+
35
+ def default_acceptance_corpus(repo_root: str | Path) -> tuple[AcceptanceCorpusPair, ...]:
36
+ repo_root = Path(repo_root)
37
+ test_root = repo_root / "references" / "testGDS"
38
+ if not test_root.exists():
39
+ return ()
40
+ pairs: list[AcceptanceCorpusPair] = []
41
+ for before in sorted(test_root.rglob("*_before.gds")):
42
+ stem = before.name[: -len("_before.gds")]
43
+ after = before.with_name(f"{stem}_after.gds")
44
+ if not after.exists():
45
+ continue
46
+ relative_parent = before.parent.relative_to(test_root)
47
+ name = stem if stem else before.parent.name
48
+ if name == before.parent.name or name.startswith(f"{before.parent.name}"):
49
+ pair_name = name
50
+ else:
51
+ pair_name = "_".join((*relative_parent.parts, stem))
52
+ top = TOP_NAME_OVERRIDES.get(pair_name) or TOP_NAME_OVERRIDES.get(stem) or TOP_NAME_OVERRIDES.get(before.parent.name)
53
+ pairs.append(AcceptanceCorpusPair(pair_name, before, after, top=top))
54
+ return tuple(pairs)
55
+
56
+
57
+ def run_acceptance_evidence(
58
+ pairs: tuple[AcceptanceCorpusPair, ...],
59
+ output_dir: str | Path,
60
+ *,
61
+ min_speedup: float = 100.0,
62
+ tile_size: int = 256,
63
+ fast_backend: str = "cuda",
64
+ monitor_cpu: bool = False,
65
+ monitor_interval_s: float = 0.1,
66
+ force_geometry_prefilter: bool = False,
67
+ geometry_cache_dir: str | Path | None = None,
68
+ warm_geometry_cache: bool = False,
69
+ structural_guard: bool = False,
70
+ native_geometry_guard: bool = False,
71
+ ) -> dict[str, Any]:
72
+ output_dir = Path(output_dir)
73
+ output_dir.mkdir(parents=True, exist_ok=True)
74
+ started = datetime.now(timezone.utc).isoformat()
75
+ rows = []
76
+ for pair in pairs:
77
+ row = _run_pair(
78
+ pair,
79
+ output_dir,
80
+ min_speedup=min_speedup,
81
+ tile_size=tile_size,
82
+ fast_backend=fast_backend,
83
+ monitor_cpu=monitor_cpu,
84
+ monitor_interval_s=monitor_interval_s,
85
+ force_geometry_prefilter=force_geometry_prefilter,
86
+ geometry_cache_dir=geometry_cache_dir,
87
+ warm_geometry_cache=warm_geometry_cache,
88
+ structural_guard=structural_guard,
89
+ native_geometry_guard=native_geometry_guard,
90
+ )
91
+ rows.append(row)
92
+ result = {
93
+ "schema_version": SCHEMA_VERSION,
94
+ "started": started,
95
+ "min_speedup": min_speedup,
96
+ "tile_size": tile_size,
97
+ "fast_backend": fast_backend,
98
+ "monitor_cpu": monitor_cpu,
99
+ "monitor_interval_s": monitor_interval_s,
100
+ "force_geometry_prefilter": force_geometry_prefilter,
101
+ "geometry_cache_dir": str(geometry_cache_dir) if geometry_cache_dir is not None else None,
102
+ "warm_geometry_cache": warm_geometry_cache,
103
+ "structural_guard": structural_guard,
104
+ "native_geometry_guard_requested": native_geometry_guard,
105
+ "capabilities": discover_native_capabilities().to_json(),
106
+ "results": rows,
107
+ "all_pairs_pass_speed_status_gate": all(row["passes_speed_status_gate"] for row in rows),
108
+ "all_pairs_have_exact_polygon_evidence": all(row["exact_polygon_evidence_complete"] for row in rows),
109
+ "all_monitored_cpu_runs_populated_all_cores_ge_50pct": all(
110
+ row["cpu_windowed_all_cores_ge_50pct_seen"] for row in rows
111
+ )
112
+ if monitor_cpu
113
+ else None,
114
+ "notes": [
115
+ "fast_backend timing is status evidence, not polygon-level evidence, unless exact_polygon_evidence_source says otherwise.",
116
+ "exact_diff_markdown is generated by the full exact gdstk oracle and is the vertex-level evidence artifact.",
117
+ ],
118
+ }
119
+ validate_acceptance_evidence(result)
120
+ output_path = output_dir / "acceptance_evidence.json"
121
+ output_path.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
122
+ return result | {"output_path": str(output_path)}
123
+
124
+
125
+ def validate_acceptance_evidence(result: dict[str, Any]) -> None:
126
+ required = {
127
+ "schema_version",
128
+ "started",
129
+ "min_speedup",
130
+ "tile_size",
131
+ "fast_backend",
132
+ "capabilities",
133
+ "results",
134
+ "all_pairs_pass_speed_status_gate",
135
+ "all_pairs_have_exact_polygon_evidence",
136
+ }
137
+ missing = sorted(required - set(result))
138
+ if missing:
139
+ raise ValueError(f"acceptance evidence missing fields: {', '.join(missing)}")
140
+ if result["schema_version"] != SCHEMA_VERSION:
141
+ raise ValueError("unsupported acceptance evidence schema")
142
+ if not isinstance(result["results"], list):
143
+ raise ValueError("acceptance evidence results must be a list")
144
+ for row in result["results"]:
145
+ _validate_row(row)
146
+
147
+
148
+ def _validate_row(row: dict[str, Any]) -> None:
149
+ required = {
150
+ "pair",
151
+ "before",
152
+ "after",
153
+ "cpu_windowed_wall_s",
154
+ "fast_wall_s",
155
+ "exact_wall_s",
156
+ "speedup_vs_cpu_windowed",
157
+ "cpu_windowed_status",
158
+ "fast_status",
159
+ "exact_status",
160
+ "fast_status_matches_exact",
161
+ "exact_polygon_evidence_complete",
162
+ "passes_speed_status_gate",
163
+ "exact_diff_markdown",
164
+ "exact_policy_sha256",
165
+ }
166
+ missing = sorted(required - set(row))
167
+ if missing:
168
+ raise ValueError(f"acceptance evidence row missing fields: {', '.join(missing)}")
169
+ for field in ("cpu_windowed_wall_s", "fast_wall_s", "exact_wall_s", "speedup_vs_cpu_windowed"):
170
+ if row[field] < 0:
171
+ raise ValueError(f"{field} must be nonnegative")
172
+
173
+
174
+ def _run_pair(
175
+ pair: AcceptanceCorpusPair,
176
+ output_dir: Path,
177
+ *,
178
+ min_speedup: float,
179
+ tile_size: int,
180
+ fast_backend: str,
181
+ monitor_cpu: bool,
182
+ monitor_interval_s: float,
183
+ force_geometry_prefilter: bool,
184
+ geometry_cache_dir: str | Path | None,
185
+ warm_geometry_cache: bool,
186
+ structural_guard: bool,
187
+ native_geometry_guard: bool,
188
+ ) -> dict[str, Any]:
189
+ exact_markdown_path = output_dir / f"{pair.name}_exact_diff.md"
190
+ if monitor_cpu:
191
+ with _forced_multicore_thresholds(), _CpuMonitor(interval_s=monitor_interval_s) as monitor:
192
+ cpu_windowed, cpu_windowed_wall = _timed_compare(
193
+ CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
194
+ exact=False,
195
+ command=("acceptance-evidence", pair.name, "cpu-windowed"),
196
+ )
197
+ cpu_monitor = monitor.summary()
198
+ else:
199
+ cpu_windowed, cpu_windowed_wall = _timed_compare(
200
+ CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
201
+ exact=False,
202
+ command=("acceptance-evidence", pair.name, "cpu-windowed"),
203
+ )
204
+ cpu_monitor = {"available": False, "reason": "monitoring disabled"}
205
+ if fast_backend == "cpu":
206
+ verified, verified_wall = _timed_compare(
207
+ CompareConfig(pair.before, pair.after, top=pair.top, backend="cpu", tile_size=tile_size),
208
+ exact=True,
209
+ exact_diff_markdown_path=exact_markdown_path,
210
+ command=("acceptance-evidence", pair.name, "cpu-exact-smoke"),
211
+ )
212
+ fast_status = verified.status.value
213
+ fast_wall = verified_wall
214
+ exact_wall = verified_wall
215
+ else:
216
+ cache_warm_s = 0.0
217
+ if warm_geometry_cache and geometry_cache_dir is not None:
218
+ cache_warm_s = _warm_geometry_cache(pair, geometry_cache_dir, tile_size=tile_size, backend=fast_backend)
219
+ verified, verified_wall = _timed_compare(
220
+ CompareConfig(
221
+ pair.before,
222
+ pair.after,
223
+ top=pair.top,
224
+ backend=fast_backend,
225
+ tile_size=tile_size,
226
+ geometry_cache_dir=geometry_cache_dir,
227
+ ),
228
+ exact=False,
229
+ exact_diff_markdown_path=exact_markdown_path,
230
+ verify_exact_after_fast=True,
231
+ use_fast_shortcuts=not force_geometry_prefilter,
232
+ use_structural_guard=structural_guard,
233
+ use_native_geometry_guard=native_geometry_guard,
234
+ command=("acceptance-evidence", pair.name, f"{fast_backend}-fast-plus-exact"),
235
+ )
236
+ fast_status = str(verified.report.exact_fallback.get("fast_status", verified.status.value))
237
+ fast_wall = float(
238
+ verified.timings.get(
239
+ "fast_status_s",
240
+ verified.timings.get("geometry_status_s", verified.timings.get("total_s", verified_wall)),
241
+ )
242
+ )
243
+ exact_wall = float(verified.timings.get("exact_verification_s", verified_wall))
244
+ speedup = cpu_windowed_wall / fast_wall if fast_wall > 0 else float("inf")
245
+ exact_policy_sha = _policy_sha256(verified)
246
+ all_cores_ge_50_seen = _all_cores_ge_50_seen(cpu_monitor)
247
+ return {
248
+ "pair": pair.name,
249
+ "before": str(pair.before),
250
+ "after": str(pair.after),
251
+ "top": pair.top,
252
+ "cpu_windowed_wall_s": cpu_windowed_wall,
253
+ "fast_wall_s": fast_wall,
254
+ "exact_wall_s": exact_wall,
255
+ "verified_total_wall_s": verified_wall,
256
+ "speedup_vs_cpu_windowed": speedup,
257
+ "cpu_windowed_status": cpu_windowed.status.value,
258
+ "cpu_windowed_timings": dict(cpu_windowed.timings),
259
+ "cpu_windowed_cpu_monitor": cpu_monitor,
260
+ "cpu_windowed_all_cores_ge_50pct_seen": all_cores_ge_50_seen,
261
+ "fast_status": fast_status,
262
+ "exact_status": verified.status.value,
263
+ "cpu_windowed_exact_verification_complete": cpu_windowed.report.exact_verification_complete,
264
+ "fast_exact_verification_complete": verified.report.exact_verification_complete,
265
+ "exact_verification_complete": verified.report.exact_verification_complete,
266
+ "fast_status_matches_exact": fast_status == verified.status.value,
267
+ "cpu_windowed_status_matches_exact": cpu_windowed.status == verified.status,
268
+ "exact_polygon_evidence_complete": verified.report.exact_verification_complete,
269
+ "exact_polygon_evidence_source": "fast-comparator-run-with-full-gdstk-oracle",
270
+ "passes_speed_status_gate": speedup >= min_speedup and fast_status == verified.status.value and verified.report.exact_verification_complete,
271
+ "exact_diff_markdown": str(exact_markdown_path),
272
+ "exact_policy_sha256": exact_policy_sha,
273
+ "exact_diff_markdown_sha256": _file_sha256(exact_markdown_path),
274
+ "artifacts": {
275
+ "exact_diff_markdown": str(exact_markdown_path),
276
+ },
277
+ "verified_report_backend": verified.report.backend,
278
+ "verified_report_exact_fallback": verified.report.exact_fallback,
279
+ "verified_timings": dict(verified.timings),
280
+ "force_geometry_prefilter": force_geometry_prefilter,
281
+ "geometry_cache_dir": str(geometry_cache_dir) if geometry_cache_dir is not None else None,
282
+ "warm_geometry_cache": warm_geometry_cache,
283
+ "geometry_cache_warm_s": cache_warm_s if fast_backend != "cpu" else 0.0,
284
+ "structural_guard": structural_guard,
285
+ "native_geometry_guard_requested": native_geometry_guard,
286
+ "native_geometry_guard_used": bool(verified.report.backend.get("native_geometry_guard")),
287
+ }
288
+
289
+
290
+ def _timed_compare(config: CompareConfig, **kwargs) -> tuple[CompareRunResult, float]:
291
+ start = perf_counter()
292
+ result = compare_gds_files(config, **kwargs)
293
+ return result, perf_counter() - start
294
+
295
+
296
+ def _warm_geometry_cache(pair: AcceptanceCorpusPair, geometry_cache_dir: str | Path, *, tile_size: int, backend: str) -> float:
297
+ start = perf_counter()
298
+ for path in (pair.before, pair.after):
299
+ load_or_build_cached_geometry(
300
+ cache_dir=geometry_cache_dir,
301
+ path=path,
302
+ top_name=pair.top,
303
+ layers="all",
304
+ meters_per_dbu=None,
305
+ path_policy="gdstk",
306
+ )
307
+ compare_gds_files(
308
+ CompareConfig(
309
+ pair.before,
310
+ pair.after,
311
+ top=pair.top,
312
+ backend=backend,
313
+ tile_size=tile_size,
314
+ geometry_cache_dir=geometry_cache_dir,
315
+ ),
316
+ exact=False,
317
+ verify_exact_after_fast=False,
318
+ use_fast_shortcuts=False,
319
+ command=("acceptance-evidence", pair.name, "warm-geometry-cache"),
320
+ )
321
+ return perf_counter() - start
322
+
323
+
324
+ class _CpuMonitor:
325
+ def __init__(self, *, interval_s: float = 0.1) -> None:
326
+ self._interval_s = max(interval_s, 0.01)
327
+ self._stop = threading.Event()
328
+ self._thread: threading.Thread | None = None
329
+ self._samples: list[tuple[float, ...]] = []
330
+ self._available = True
331
+ self._reason = ""
332
+
333
+ def __enter__(self) -> "_CpuMonitor":
334
+ try:
335
+ initial = _read_cpu_counters()
336
+ except OSError as exc:
337
+ self._available = False
338
+ self._reason = str(exc)
339
+ return self
340
+ if not initial:
341
+ self._available = False
342
+ self._reason = "no per-core CPU counters found"
343
+ return self
344
+ self._thread = threading.Thread(target=self._run, args=(initial,), daemon=True)
345
+ self._thread.start()
346
+ return self
347
+
348
+ def __exit__(self, exc_type, exc, tb) -> None:
349
+ self._stop.set()
350
+ if self._thread is not None:
351
+ self._thread.join(timeout=max(self._interval_s * 4, 0.25))
352
+
353
+ def summary(self) -> dict[str, Any]:
354
+ if not self._available:
355
+ return {"available": False, "reason": self._reason}
356
+ if not self._samples:
357
+ return {"available": True, "sample_count": 0, "reason": "no completed CPU samples"}
358
+ logical_cpu_count = len(self._samples[0])
359
+ return {
360
+ "available": True,
361
+ "sample_count": len(self._samples),
362
+ "logical_cpu_count": logical_cpu_count,
363
+ "max_cores_ge_25pct": max(_count_at_least(sample, 25.0) for sample in self._samples),
364
+ "max_cores_ge_50pct": max(_count_at_least(sample, 50.0) for sample in self._samples),
365
+ "max_cores_ge_75pct": max(_count_at_least(sample, 75.0) for sample in self._samples),
366
+ "mean_cores_ge_50pct": sum(_count_at_least(sample, 50.0) for sample in self._samples) / len(self._samples),
367
+ "max_mean_busy_pct": max(sum(sample) / len(sample) for sample in self._samples),
368
+ "mean_busy_pct": sum(sum(sample) / len(sample) for sample in self._samples) / len(self._samples),
369
+ "max_core_busy_pct": max(max(sample) for sample in self._samples),
370
+ "interval_s": self._interval_s,
371
+ }
372
+
373
+ def _run(self, previous: list[tuple[int, int]]) -> None:
374
+ while not self._stop.wait(self._interval_s):
375
+ try:
376
+ current = _read_cpu_counters()
377
+ except OSError as exc:
378
+ self._available = False
379
+ self._reason = str(exc)
380
+ return
381
+ if len(current) != len(previous):
382
+ self._available = False
383
+ self._reason = "CPU counter count changed during monitoring"
384
+ return
385
+ sample = tuple(_busy_percent(old, new) for old, new in zip(previous, current))
386
+ self._samples.append(sample)
387
+ previous = current
388
+
389
+
390
+ def _read_cpu_counters() -> list[tuple[int, int]]:
391
+ path = Path("/proc/stat")
392
+ rows: list[tuple[int, int]] = []
393
+ with path.open("r", encoding="utf-8") as handle:
394
+ for line in handle:
395
+ parts = line.split()
396
+ if not parts:
397
+ continue
398
+ name = parts[0]
399
+ if not name.startswith("cpu") or name == "cpu" or not name[3:].isdigit():
400
+ continue
401
+ values = [int(value) for value in parts[1:]]
402
+ idle = values[3] + (values[4] if len(values) > 4 else 0)
403
+ total = sum(values)
404
+ rows.append((total, idle))
405
+ return rows
406
+
407
+
408
+ def _busy_percent(old: tuple[int, int], new: tuple[int, int]) -> float:
409
+ old_total, old_idle = old
410
+ new_total, new_idle = new
411
+ total_delta = new_total - old_total
412
+ if total_delta <= 0:
413
+ return 0.0
414
+ idle_delta = new_idle - old_idle
415
+ return max(0.0, min(100.0, 100.0 * (total_delta - idle_delta) / total_delta))
416
+
417
+
418
+ def _count_at_least(sample: tuple[float, ...], threshold: float) -> int:
419
+ return sum(1 for value in sample if value >= threshold)
420
+
421
+
422
+ def _all_cores_ge_50_seen(summary: dict[str, Any]) -> bool:
423
+ if not summary.get("available"):
424
+ return False
425
+ logical_cpu_count = summary.get("logical_cpu_count")
426
+ max_cores = summary.get("max_cores_ge_50pct")
427
+ return isinstance(logical_cpu_count, int) and isinstance(max_cores, int) and max_cores >= logical_cpu_count
428
+
429
+
430
+ @contextmanager
431
+ def _forced_multicore_thresholds():
432
+ names = (
433
+ "GDSDIFF_PARALLEL_MIN_POLYGONS",
434
+ "GDSDIFF_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
435
+ "GDS_GPU_COMPARE_PARALLEL_MIN_POLYGONS",
436
+ "GDS_GPU_COMPARE_PARALLEL_EXTRACT_PAIR_MIN_BYTES",
437
+ )
438
+ previous = {name: os.environ.get(name) for name in names}
439
+ try:
440
+ for name in names:
441
+ os.environ[name] = "1"
442
+ yield
443
+ finally:
444
+ for name, value in previous.items():
445
+ if value is None:
446
+ os.environ.pop(name, None)
447
+ else:
448
+ os.environ[name] = value
449
+
450
+
451
+ def _policy_sha256(result: CompareRunResult) -> str:
452
+ payload = json.dumps(result.policy_result.to_json(), sort_keys=True, separators=(",", ":")).encode("utf-8")
453
+ return hashlib.sha256(payload).hexdigest()
454
+
455
+
456
+ def _file_sha256(path: Path) -> str:
457
+ digest = hashlib.sha256()
458
+ with path.open("rb") as handle:
459
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
460
+ digest.update(chunk)
461
+ return digest.hexdigest()
462
+
463
+
464
+ def build_parser() -> argparse.ArgumentParser:
465
+ parser = argparse.ArgumentParser(description="Run GDS comparator acceptance evidence over the reference corpus.")
466
+ parser.add_argument("--repo-root", type=Path, default=Path.cwd())
467
+ parser.add_argument("--output-dir", type=Path, required=True)
468
+ parser.add_argument("--pair", action="append", metavar="NAME:BEFORE:AFTER[:TOP]")
469
+ parser.add_argument("--min-speedup", type=float, default=100.0)
470
+ parser.add_argument("--tile-size", type=int, default=256)
471
+ parser.add_argument("--fast-backend", default="cuda")
472
+ parser.add_argument("--monitor-cpu", action="store_true")
473
+ parser.add_argument("--monitor-interval-s", type=float, default=0.1)
474
+ parser.add_argument("--force-geometry-prefilter", action="store_true")
475
+ parser.add_argument("--geometry-cache-dir", type=Path)
476
+ parser.add_argument("--warm-geometry-cache", action="store_true")
477
+ parser.add_argument("--structural-guard", action="store_true")
478
+ parser.add_argument("--native-geometry-guard", action="store_true")
479
+ return parser
480
+
481
+
482
+ def main(argv: list[str] | None = None) -> int:
483
+ parser = build_parser()
484
+ args = parser.parse_args(argv)
485
+ if args.min_speedup < 0:
486
+ parser.error("--min-speedup must be nonnegative")
487
+ if args.tile_size <= 0:
488
+ parser.error("--tile-size must be positive")
489
+ if args.monitor_interval_s <= 0:
490
+ parser.error("--monitor-interval-s must be positive")
491
+ pairs = _parse_pairs(args.pair) if args.pair else default_acceptance_corpus(args.repo_root)
492
+ if not pairs:
493
+ parser.error("no acceptance corpus pairs found")
494
+ result = run_acceptance_evidence(
495
+ pairs,
496
+ args.output_dir,
497
+ min_speedup=args.min_speedup,
498
+ tile_size=args.tile_size,
499
+ fast_backend=args.fast_backend,
500
+ monitor_cpu=args.monitor_cpu,
501
+ monitor_interval_s=args.monitor_interval_s,
502
+ force_geometry_prefilter=args.force_geometry_prefilter,
503
+ geometry_cache_dir=args.geometry_cache_dir,
504
+ warm_geometry_cache=args.warm_geometry_cache,
505
+ structural_guard=args.structural_guard,
506
+ native_geometry_guard=args.native_geometry_guard,
507
+ )
508
+ print(result["output_path"])
509
+ return 0 if result["all_pairs_pass_speed_status_gate"] else 1
510
+
511
+
512
+ def _parse_pairs(values: list[str]) -> tuple[AcceptanceCorpusPair, ...]:
513
+ pairs: list[AcceptanceCorpusPair] = []
514
+ for value in values:
515
+ parts = value.split(":")
516
+ if len(parts) not in {3, 4}:
517
+ raise ValueError("--pair must have form NAME:BEFORE:AFTER[:TOP]")
518
+ pairs.append(
519
+ AcceptanceCorpusPair(
520
+ name=parts[0],
521
+ before=Path(parts[1]),
522
+ after=Path(parts[2]),
523
+ top=parts[3] if len(parts) == 4 and parts[3] else None,
524
+ )
525
+ )
526
+ return tuple(pairs)
527
+
528
+
529
+ if __name__ == "__main__":
530
+ raise SystemExit(main())