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
gdsdiff/native.py ADDED
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import os
5
+ import shutil
6
+ from dataclasses import dataclass
7
+ from functools import lru_cache
8
+
9
+ from ._environment import get_environment
10
+ from .report import TOOL_VERSION
11
+
12
+ CUDA_ARCH_ENV = "GDSDIFF_CUDA_ARCHITECTURES"
13
+ LEGACY_CUDA_ARCH_ENV = "GDS_GPU_COMPARE_CUDA_ARCHITECTURES"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class NativeCapabilities:
18
+ tool_version: str
19
+ core_extension_available: bool
20
+ cuda_extension_available: bool
21
+ cuda_ctypes_available: bool
22
+ metal_ctypes_available: bool
23
+ cmake_available: bool
24
+ nvcc_available: bool
25
+ cuda_architectures: tuple[str, ...]
26
+ cuda_compile_flags: tuple[str, ...] = ()
27
+
28
+ @property
29
+ def cpu_available(self) -> bool:
30
+ return True
31
+
32
+ @property
33
+ def cuda_available(self) -> bool:
34
+ return self.cuda_extension_available or self.cuda_ctypes_available
35
+
36
+ @property
37
+ def metal_available(self) -> bool:
38
+ return self.metal_ctypes_available
39
+
40
+ def to_json(self) -> dict[str, object]:
41
+ return {
42
+ "tool_version": self.tool_version,
43
+ "cpu_available": self.cpu_available,
44
+ "core_extension_available": self.core_extension_available,
45
+ "cuda_extension_available": self.cuda_extension_available,
46
+ "cuda_ctypes_available": self.cuda_ctypes_available,
47
+ "cuda_available": self.cuda_available,
48
+ "metal_ctypes_available": self.metal_ctypes_available,
49
+ "metal_available": self.metal_available,
50
+ "cmake_available": self.cmake_available,
51
+ "nvcc_available": self.nvcc_available,
52
+ "cuda_architectures": list(self.cuda_architectures),
53
+ "cuda_compile_flags": list(self.cuda_compile_flags),
54
+ }
55
+
56
+
57
+ def discover_native_capabilities() -> NativeCapabilities:
58
+ """Inspect already prepared capabilities without compiling native code."""
59
+
60
+ return NativeCapabilities(
61
+ tool_version=TOOL_VERSION,
62
+ core_extension_available=_module_available("gdsdiff._gds_compare_core"),
63
+ cuda_extension_available=_module_available("gdsdiff._gds_compare_cuda"),
64
+ cuda_ctypes_available=_cuda_ctypes_available(),
65
+ metal_ctypes_available=_metal_ctypes_available(),
66
+ cmake_available=_which_tool("cmake", os.environ.get("PATH")) is not None,
67
+ nvcc_available=resolve_nvcc_path() is not None,
68
+ cuda_architectures=_cuda_architectures_from_env(),
69
+ cuda_compile_flags=nvcc_architecture_flags(),
70
+ )
71
+
72
+
73
+ @lru_cache(maxsize=16)
74
+ def _module_available(name: str) -> bool:
75
+ try:
76
+ return importlib.util.find_spec(name) is not None
77
+ except (ImportError, AttributeError, ValueError):
78
+ return False
79
+
80
+
81
+ @lru_cache(maxsize=16)
82
+ def _which_tool(name: str, path: str | None) -> str | None:
83
+ """Resolve a tool once per PATH value while honoring runtime PATH changes."""
84
+
85
+ return shutil.which(name, path=path)
86
+
87
+
88
+ def _cuda_ctypes_available() -> bool:
89
+ from .backends.cuda_ctypes import prepared_cuda_library_path
90
+
91
+ return prepared_cuda_library_path().is_file()
92
+
93
+
94
+ def _metal_ctypes_available() -> bool:
95
+ from .native_cache import native_cache_dir
96
+
97
+ return any(native_cache_dir().glob("_gds_compare_metal_ctypes-*.dylib"))
98
+
99
+
100
+ def resolve_nvcc_path() -> str | None:
101
+ """Return the CUDA compiler from PATH or conventional toolkit locations."""
102
+
103
+ resolved = _which_tool("nvcc", os.environ.get("PATH"))
104
+ if resolved:
105
+ return resolved
106
+ candidates = []
107
+ for variable in ("CUDA_HOME", "CUDA_PATH"):
108
+ if os.environ.get(variable):
109
+ candidates.append(os.path.join(os.environ[variable], "bin", "nvcc"))
110
+ candidates.extend(("/usr/local/cuda/bin/nvcc", "/opt/cuda/bin/nvcc"))
111
+ return next((candidate for candidate in candidates if os.path.isfile(candidate) and os.access(candidate, os.X_OK)), None)
112
+
113
+
114
+ def _cuda_architectures_from_env() -> tuple[str, ...]:
115
+ raw = get_environment(CUDA_ARCH_ENV, LEGACY_CUDA_ARCH_ENV, "") or ""
116
+ return tuple(part.strip() for part in raw.replace(";", ",").split(",") if part.strip())
117
+
118
+
119
+ def nvcc_architecture_flags() -> tuple[str, ...]:
120
+ """Translate configured CUDA architectures into native-SASS nvcc flags."""
121
+
122
+ configured_architectures = _cuda_architectures_from_env()
123
+ if not configured_architectures:
124
+ return ("-arch=native",)
125
+ flags = []
126
+ for configured in configured_architectures:
127
+ architecture = configured.lower()
128
+ for prefix in ("sm_", "compute_"):
129
+ if architecture.startswith(prefix):
130
+ architecture = architecture[len(prefix) :]
131
+ break
132
+ if not architecture.isdigit():
133
+ raise ValueError(f"invalid CUDA architecture {configured!r}; expected values such as '89' or '120'")
134
+ flags.append(f"--generate-code=arch=compute_{architecture},code=sm_{architecture}")
135
+ return tuple(flags)
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import ctypes
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ import uuid
12
+ from contextlib import contextmanager
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+ from typing import Callable, Iterable, Iterator
16
+
17
+
18
+ ENV_NATIVE_CACHE_DIR = "GDSDIFF_NATIVE_CACHE_DIR"
19
+ LEGACY_ENV_NATIVE_CACHE_DIR = "SCGDS_NATIVE_CACHE_DIR"
20
+
21
+
22
+ def cached_native_library_path(filename: str) -> Path:
23
+ return native_cache_dir() / filename
24
+
25
+
26
+ def content_addressed_library_path(
27
+ filename: str,
28
+ *,
29
+ sources: Iterable[Path],
30
+ build_key: Iterable[str] = (),
31
+ ) -> Path:
32
+ """Return a cache path keyed by source content and the build contract."""
33
+
34
+ source_paths = tuple(Path(source) for source in sources)
35
+ digest = hashlib.sha256(b"gdsdiff-native-cache-v1\0")
36
+ for item in native_build_fingerprint(tuple(build_key)):
37
+ digest.update(str(item).encode("utf-8"))
38
+ digest.update(b"\0")
39
+ for source in source_paths:
40
+ digest.update(source.name.encode("utf-8"))
41
+ digest.update(b"\0")
42
+ digest.update(source.read_bytes())
43
+ digest.update(b"\0")
44
+ source_hash = digest.hexdigest()[:20]
45
+ candidate = Path(filename)
46
+ suffix = candidate.suffix
47
+ stem = candidate.name[: -len(suffix)] if suffix else candidate.name
48
+ return native_cache_dir() / f"{stem}-{source_hash}{suffix}"
49
+
50
+
51
+ def ensure_cached_native_library(
52
+ filename: str,
53
+ *,
54
+ sources: Iterable[Path],
55
+ build_key: Iterable[str],
56
+ builder: Callable[[Path], None],
57
+ ) -> Path:
58
+ """Build and atomically publish a content-addressed native library."""
59
+
60
+ source_paths = tuple(Path(source) for source in sources)
61
+ path = content_addressed_library_path(filename, sources=source_paths, build_key=build_key)
62
+ if path.is_file():
63
+ return path
64
+ _ensure_private_cache_directory(path.parent)
65
+ lock_path = path.with_name(f".{path.name}.lock")
66
+ with _exclusive_cache_lock(lock_path):
67
+ if path.is_file():
68
+ return path
69
+ temporary = path.with_name(f".{path.stem}.{os.getpid()}.{uuid.uuid4().hex}{path.suffix}")
70
+ try:
71
+ builder(temporary)
72
+ if not temporary.is_file() or temporary.stat().st_size == 0:
73
+ raise RuntimeError(f"native builder did not create a non-empty library: {temporary}")
74
+ os.replace(temporary, path)
75
+ finally:
76
+ temporary.unlink(missing_ok=True)
77
+ return path
78
+
79
+
80
+ def native_cache_dir() -> Path:
81
+ configured = os.environ.get(ENV_NATIVE_CACHE_DIR) or os.environ.get(LEGACY_ENV_NATIVE_CACHE_DIR)
82
+ if configured:
83
+ base = Path(configured).expanduser()
84
+ elif sys.platform == "darwin":
85
+ base = Path.home() / "Library" / "Caches" / "gdsdiff" / "native"
86
+ elif os.name == "nt":
87
+ root = os.environ.get("LOCALAPPDATA")
88
+ base = Path(root).expanduser() if root else Path.home() / "AppData" / "Local"
89
+ base = base / "gdsdiff" / "native"
90
+ else:
91
+ root = os.environ.get("XDG_CACHE_HOME")
92
+ base = Path(root).expanduser() if root else Path.home() / ".cache"
93
+ base = base / "gdsdiff" / "native"
94
+ return base / _platform_tag()
95
+
96
+
97
+ def native_build_fingerprint(build_key: tuple[str, ...]) -> tuple[str, ...]:
98
+ """Augment a native build contract with compiler and device identity."""
99
+
100
+ if not build_key:
101
+ return build_key
102
+ compiler = build_key[0]
103
+ fingerprint = (*build_key, *_executable_fingerprint(compiler, os.environ.get("PATH")))
104
+ if Path(compiler).name.lower().startswith("nvcc") and "-arch=native" in build_key:
105
+ fingerprint = (*fingerprint, *_cuda_device_fingerprint(os.environ.get("CUDA_VISIBLE_DEVICES")))
106
+ return fingerprint
107
+
108
+
109
+ @lru_cache(maxsize=32)
110
+ def _executable_fingerprint(command: str, path: str | None) -> tuple[str, ...]:
111
+ resolved = shutil.which(command, path=path)
112
+ if resolved is None:
113
+ return (f"compiler={command}", "compiler_resolved=unavailable")
114
+ executable = Path(resolved).resolve()
115
+ try:
116
+ stat = executable.stat()
117
+ identity = f"{stat.st_size}:{stat.st_mtime_ns}"
118
+ except OSError:
119
+ identity = "stat-unavailable"
120
+ try:
121
+ completed = subprocess.run(
122
+ [str(executable), "--version"],
123
+ check=False,
124
+ capture_output=True,
125
+ text=True,
126
+ timeout=10,
127
+ )
128
+ version = (completed.stdout or completed.stderr).strip()
129
+ except (OSError, subprocess.SubprocessError):
130
+ version = "version-unavailable"
131
+ return (
132
+ f"compiler={command}",
133
+ f"compiler_resolved={executable}",
134
+ f"compiler_identity={identity}",
135
+ f"compiler_version={version}",
136
+ )
137
+
138
+
139
+ @lru_cache(maxsize=8)
140
+ def _cuda_device_fingerprint(visible_devices: str | None) -> tuple[str, ...]:
141
+ driver_devices = _cuda_driver_api_fingerprint()
142
+ if driver_devices is not None:
143
+ return (f"CUDA_VISIBLE_DEVICES={visible_devices or ''}", f"cuda_driver_devices={driver_devices}")
144
+ try:
145
+ completed = subprocess.run(
146
+ [
147
+ "nvidia-smi",
148
+ "--query-gpu=index,uuid,name,compute_cap,driver_version",
149
+ "--format=csv,noheader,nounits",
150
+ ],
151
+ check=False,
152
+ capture_output=True,
153
+ text=True,
154
+ timeout=10,
155
+ )
156
+ devices = completed.stdout.strip() if completed.returncode == 0 else "unavailable"
157
+ except (OSError, subprocess.SubprocessError):
158
+ devices = "unavailable"
159
+ return (f"CUDA_VISIBLE_DEVICES={visible_devices or ''}", f"cuda_devices={devices}")
160
+
161
+
162
+ def _cuda_driver_api_fingerprint() -> str | None:
163
+ """Read visible device identity without creating a CUDA context."""
164
+
165
+ try:
166
+ driver = ctypes.CDLL("libcuda.so.1")
167
+ driver.cuInit.argtypes = [ctypes.c_uint]
168
+ driver.cuInit.restype = ctypes.c_int
169
+ driver.cuDeviceGetCount.argtypes = [ctypes.POINTER(ctypes.c_int)]
170
+ driver.cuDeviceGetCount.restype = ctypes.c_int
171
+ driver.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
172
+ driver.cuDeviceGet.restype = ctypes.c_int
173
+ driver.cuDeviceComputeCapability.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int), ctypes.c_int]
174
+ driver.cuDeviceComputeCapability.restype = ctypes.c_int
175
+ driver.cuDeviceGetName.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
176
+ driver.cuDeviceGetName.restype = ctypes.c_int
177
+ driver.cuDriverGetVersion.argtypes = [ctypes.POINTER(ctypes.c_int)]
178
+ driver.cuDriverGetVersion.restype = ctypes.c_int
179
+ if driver.cuInit(0) != 0:
180
+ return None
181
+ count = ctypes.c_int()
182
+ version = ctypes.c_int()
183
+ if driver.cuDeviceGetCount(ctypes.byref(count)) != 0 or driver.cuDriverGetVersion(ctypes.byref(version)) != 0:
184
+ return None
185
+ devices = []
186
+ for ordinal in range(count.value):
187
+ device = ctypes.c_int()
188
+ major = ctypes.c_int()
189
+ minor = ctypes.c_int()
190
+ name = ctypes.create_string_buffer(256)
191
+ if driver.cuDeviceGet(ctypes.byref(device), ordinal) != 0:
192
+ return None
193
+ if driver.cuDeviceComputeCapability(ctypes.byref(major), ctypes.byref(minor), device.value) != 0:
194
+ return None
195
+ if driver.cuDeviceGetName(name, len(name), device.value) != 0:
196
+ return None
197
+ devices.append(f"{ordinal}:{name.value.decode('utf-8', errors='replace')}:sm_{major.value}{minor.value}")
198
+ return f"driver_api={version.value};" + ";".join(devices)
199
+ except (AttributeError, OSError, ValueError):
200
+ return None
201
+
202
+
203
+ def _ensure_private_cache_directory(path: Path) -> None:
204
+ path.mkdir(parents=True, exist_ok=True, mode=0o700)
205
+ if os.name == "nt":
206
+ return
207
+ if path.is_symlink():
208
+ raise PermissionError(f"native cache directory must not be a symbolic link: {path}")
209
+ stat = path.stat()
210
+ if hasattr(os, "getuid") and stat.st_uid != os.getuid():
211
+ raise PermissionError(f"native cache directory is not owned by the current user: {path}")
212
+ if stat.st_mode & 0o077:
213
+ path.chmod(stat.st_mode & ~0o077)
214
+
215
+
216
+ def _platform_tag() -> str:
217
+ system = platform.system().lower() or "unknown"
218
+ machine = platform.machine().lower() or "unknown"
219
+ python_tag = f"py{sys.version_info.major}{sys.version_info.minor}"
220
+ return f"{system}-{machine}-{python_tag}"
221
+
222
+
223
+ @contextmanager
224
+ def _exclusive_cache_lock(
225
+ path: Path,
226
+ *,
227
+ timeout_s: float = 300.0,
228
+ stale_after_s: float = 900.0,
229
+ ) -> Iterator[None]:
230
+ started = time.monotonic()
231
+ descriptor: int | None = None
232
+ while descriptor is None:
233
+ try:
234
+ descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
235
+ except (FileExistsError, PermissionError) as exc:
236
+ # Windows can report an existing O_EXCL lock as access denied when
237
+ # another process or thread still owns the directory entry.
238
+ if isinstance(exc, PermissionError) and sys.platform != "win32":
239
+ raise
240
+ try:
241
+ age_s = time.time() - path.stat().st_mtime
242
+ except FileNotFoundError:
243
+ continue
244
+ except PermissionError:
245
+ # A Windows sharing violation is itself evidence that the lock
246
+ # is live; do not misclassify it as a cache permission failure.
247
+ age_s = 0.0
248
+ if age_s > stale_after_s:
249
+ try:
250
+ path.unlink()
251
+ except FileNotFoundError:
252
+ pass
253
+ continue
254
+ if time.monotonic() - started >= timeout_s:
255
+ raise TimeoutError(f"timed out waiting for native cache lock: {path}")
256
+ time.sleep(0.05)
257
+ try:
258
+ os.write(descriptor, f"pid={os.getpid()}\n".encode("ascii"))
259
+ os.close(descriptor)
260
+ descriptor = None
261
+ yield
262
+ finally:
263
+ if descriptor is not None:
264
+ os.close(descriptor)
265
+ path.unlink(missing_ok=True)