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/config.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from decimal import Decimal, InvalidOperation
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, Literal
|
|
7
|
+
|
|
8
|
+
from .semantics import Backend, DifferenceDirection, EvidenceEngine, FailurePolicy, PathPolicy
|
|
9
|
+
|
|
10
|
+
LayerSelector = Literal["all"] | tuple["LayerSpec", ...]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _decimal_or_none(value) -> Decimal | None:
|
|
14
|
+
if value is None:
|
|
15
|
+
return None
|
|
16
|
+
if isinstance(value, Decimal):
|
|
17
|
+
result = value
|
|
18
|
+
else:
|
|
19
|
+
try:
|
|
20
|
+
result = Decimal(str(value))
|
|
21
|
+
except (InvalidOperation, ValueError) as exc:
|
|
22
|
+
raise ValueError(f"invalid decimal value: {value!r}") from exc
|
|
23
|
+
if not result.is_finite() or result <= 0:
|
|
24
|
+
raise ValueError(f"decimal value must be finite and positive: {value!r}")
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, order=True)
|
|
29
|
+
class LayerSpec:
|
|
30
|
+
layer: int
|
|
31
|
+
datatype: int = 0
|
|
32
|
+
|
|
33
|
+
def __post_init__(self) -> None:
|
|
34
|
+
if not isinstance(self.layer, int) or not isinstance(self.datatype, int):
|
|
35
|
+
raise TypeError("layer and datatype must be integers")
|
|
36
|
+
if self.layer < 0 or self.datatype < 0:
|
|
37
|
+
raise ValueError("layer and datatype must be nonnegative")
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def parse(cls, value: str | tuple[int, int] | "LayerSpec") -> "LayerSpec":
|
|
41
|
+
if isinstance(value, cls):
|
|
42
|
+
return value
|
|
43
|
+
if isinstance(value, tuple):
|
|
44
|
+
if len(value) != 2:
|
|
45
|
+
raise ValueError("layer tuple must be (layer, datatype)")
|
|
46
|
+
return cls(int(value[0]), int(value[1]))
|
|
47
|
+
parts = str(value).strip().split(":")
|
|
48
|
+
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
49
|
+
raise ValueError(f"layer spec must have form L:D, got {value!r}")
|
|
50
|
+
return cls(int(parts[0]), int(parts[1]))
|
|
51
|
+
|
|
52
|
+
def to_json(self) -> dict[str, int]:
|
|
53
|
+
return {"layer": self.layer, "datatype": self.datatype}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_layer_selector(value: str | LayerSpec | Iterable[str | tuple[int, int] | LayerSpec]) -> LayerSelector:
|
|
57
|
+
if value == "all":
|
|
58
|
+
return "all"
|
|
59
|
+
if isinstance(value, str):
|
|
60
|
+
parts = [part.strip() for part in value.split(",") if part.strip()]
|
|
61
|
+
if not parts:
|
|
62
|
+
raise ValueError("layer selector cannot be empty")
|
|
63
|
+
layers = tuple(sorted({LayerSpec.parse(part) for part in parts}))
|
|
64
|
+
elif isinstance(value, LayerSpec):
|
|
65
|
+
layers = (value,)
|
|
66
|
+
else:
|
|
67
|
+
layers = tuple(sorted({LayerSpec.parse(part) for part in value}))
|
|
68
|
+
if not layers:
|
|
69
|
+
raise ValueError("layer selector cannot be empty")
|
|
70
|
+
return layers
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class GridSpec:
|
|
75
|
+
meters_per_dbu: Decimal | str | float | None = None
|
|
76
|
+
source_old_precision_m: Decimal | str | float | None = None
|
|
77
|
+
source_new_precision_m: Decimal | str | float | None = None
|
|
78
|
+
selection_mode: str = "auto"
|
|
79
|
+
rounding_mode: str = "nearest-ties-away-from-zero"
|
|
80
|
+
max_coordinate: int | None = None
|
|
81
|
+
max_snap_error_dbu: Decimal | str | float | None = None
|
|
82
|
+
|
|
83
|
+
def __post_init__(self) -> None:
|
|
84
|
+
object.__setattr__(self, "meters_per_dbu", _decimal_or_none(self.meters_per_dbu))
|
|
85
|
+
object.__setattr__(self, "source_old_precision_m", _decimal_or_none(self.source_old_precision_m))
|
|
86
|
+
object.__setattr__(self, "source_new_precision_m", _decimal_or_none(self.source_new_precision_m))
|
|
87
|
+
object.__setattr__(self, "max_snap_error_dbu", _decimal_or_none(self.max_snap_error_dbu))
|
|
88
|
+
if self.rounding_mode != "nearest-ties-away-from-zero":
|
|
89
|
+
raise ValueError("unsupported rounding mode")
|
|
90
|
+
if self.max_coordinate is not None and self.max_coordinate <= 0:
|
|
91
|
+
raise ValueError("max_coordinate must be positive when set")
|
|
92
|
+
|
|
93
|
+
def to_json(self) -> dict[str, object]:
|
|
94
|
+
return {
|
|
95
|
+
"meters_per_dbu": str(self.meters_per_dbu) if self.meters_per_dbu is not None else None,
|
|
96
|
+
"source_old_precision_m": str(self.source_old_precision_m)
|
|
97
|
+
if self.source_old_precision_m is not None
|
|
98
|
+
else None,
|
|
99
|
+
"source_new_precision_m": str(self.source_new_precision_m)
|
|
100
|
+
if self.source_new_precision_m is not None
|
|
101
|
+
else None,
|
|
102
|
+
"selection_mode": self.selection_mode,
|
|
103
|
+
"rounding_mode": self.rounding_mode,
|
|
104
|
+
"max_coordinate": self.max_coordinate,
|
|
105
|
+
"max_snap_error_dbu": str(self.max_snap_error_dbu) if self.max_snap_error_dbu is not None else None,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class AllowedRegion:
|
|
111
|
+
layers: tuple[LayerSpec, ...]
|
|
112
|
+
direction: DifferenceDirection | str = DifferenceDirection.BOTH
|
|
113
|
+
bbox: tuple[int, int, int, int] | None = None
|
|
114
|
+
whole_layer: bool = False
|
|
115
|
+
halo_dbu: int = 0
|
|
116
|
+
label: str = ""
|
|
117
|
+
reason: str = ""
|
|
118
|
+
|
|
119
|
+
def __post_init__(self) -> None:
|
|
120
|
+
layers = tuple(sorted({LayerSpec.parse(layer) for layer in self.layers}))
|
|
121
|
+
if not layers:
|
|
122
|
+
raise ValueError("allowed region must list at least one layer")
|
|
123
|
+
object.__setattr__(self, "layers", layers)
|
|
124
|
+
object.__setattr__(self, "direction", DifferenceDirection.coerce(self.direction))
|
|
125
|
+
if self.halo_dbu < 0:
|
|
126
|
+
raise ValueError("allowed region halo_dbu must be nonnegative")
|
|
127
|
+
if self.whole_layer:
|
|
128
|
+
if self.bbox is not None:
|
|
129
|
+
raise ValueError("whole-layer allowed region cannot also set bbox")
|
|
130
|
+
elif self.bbox is None:
|
|
131
|
+
raise ValueError("allowed region requires bbox unless whole_layer is true")
|
|
132
|
+
else:
|
|
133
|
+
x0, y0, x1, y1 = self.bbox
|
|
134
|
+
if x0 >= x1 or y0 >= y1:
|
|
135
|
+
raise ValueError("allowed region bbox must satisfy x0 < x1 and y0 < y1")
|
|
136
|
+
|
|
137
|
+
def to_json(self) -> dict[str, object]:
|
|
138
|
+
return {
|
|
139
|
+
"layers": [layer.to_json() for layer in self.layers],
|
|
140
|
+
"direction": self.direction.value,
|
|
141
|
+
"bbox": list(self.bbox) if self.bbox is not None else None,
|
|
142
|
+
"whole_layer": self.whole_layer,
|
|
143
|
+
"halo_dbu": self.halo_dbu,
|
|
144
|
+
"label": self.label,
|
|
145
|
+
"reason": self.reason,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def parse_allowed_region(value: str) -> AllowedRegion:
|
|
150
|
+
"""Parse ``L:D,DIRECTION,X0,Y0,X1,Y1[,HALO]`` into an allowance."""
|
|
151
|
+
|
|
152
|
+
parts = [part.strip() for part in value.split(",")]
|
|
153
|
+
if len(parts) not in {6, 7}:
|
|
154
|
+
raise ValueError("allowed region must have form L:D,DIRECTION,X0,Y0,X1,Y1[,HALO]")
|
|
155
|
+
layer = LayerSpec.parse(parts[0])
|
|
156
|
+
direction = DifferenceDirection.coerce(parts[1])
|
|
157
|
+
x0, y0, x1, y1 = (int(part) for part in parts[2:6])
|
|
158
|
+
halo = int(parts[6]) if len(parts) == 7 else 0
|
|
159
|
+
return AllowedRegion(
|
|
160
|
+
layers=(layer,),
|
|
161
|
+
direction=direction,
|
|
162
|
+
bbox=(x0, y0, x1, y1),
|
|
163
|
+
halo_dbu=halo,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def parse_allowed_whole_layer(value: str) -> AllowedRegion:
|
|
168
|
+
"""Parse ``L:D,DIRECTION`` into a whole-layer allowance."""
|
|
169
|
+
|
|
170
|
+
parts = [part.strip() for part in value.split(",")]
|
|
171
|
+
if len(parts) != 2:
|
|
172
|
+
raise ValueError("whole-layer allowance must have form L:D,DIRECTION")
|
|
173
|
+
return AllowedRegion(
|
|
174
|
+
layers=(LayerSpec.parse(parts[0]),),
|
|
175
|
+
direction=DifferenceDirection.coerce(parts[1]),
|
|
176
|
+
whole_layer=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(frozen=True)
|
|
181
|
+
class CompareConfig:
|
|
182
|
+
old_path: Path | str
|
|
183
|
+
new_path: Path | str
|
|
184
|
+
top: str | None = None
|
|
185
|
+
old_top: str | None = None
|
|
186
|
+
new_top: str | None = None
|
|
187
|
+
require_top_name_match: bool = False
|
|
188
|
+
layers: str | LayerSpec | Iterable[str | tuple[int, int] | LayerSpec] = "all"
|
|
189
|
+
dbu: Decimal | str | float | None = None
|
|
190
|
+
precision: Decimal | str | float | None = None
|
|
191
|
+
path_policy: PathPolicy | str = PathPolicy.GDSTK
|
|
192
|
+
backend: Backend | str = Backend.AUTO
|
|
193
|
+
evidence_engine: EvidenceEngine | str = EvidenceEngine.AUTO
|
|
194
|
+
tile_size: int = 1024
|
|
195
|
+
allowed_regions: tuple[AllowedRegion, ...] = field(default_factory=tuple)
|
|
196
|
+
allow_empty_selected_layer: bool = False
|
|
197
|
+
strict: bool = True
|
|
198
|
+
require_gpu: bool = False
|
|
199
|
+
failure_policy: FailurePolicy | str = FailurePolicy.FAIL_ON_UNEXPECTED
|
|
200
|
+
memory_budget_bytes: int | None = None
|
|
201
|
+
geometry_cache_dir: Path | str | None = None
|
|
202
|
+
|
|
203
|
+
def __post_init__(self) -> None:
|
|
204
|
+
object.__setattr__(self, "old_path", Path(self.old_path))
|
|
205
|
+
object.__setattr__(self, "new_path", Path(self.new_path))
|
|
206
|
+
if self.geometry_cache_dir is not None:
|
|
207
|
+
object.__setattr__(self, "geometry_cache_dir", Path(self.geometry_cache_dir))
|
|
208
|
+
if self.top and (self.old_top or self.new_top):
|
|
209
|
+
raise ValueError("top cannot be combined with old_top or new_top")
|
|
210
|
+
if bool(self.old_top) != bool(self.new_top):
|
|
211
|
+
raise ValueError("old_top and new_top must be supplied together")
|
|
212
|
+
layers = parse_layer_selector(self.layers)
|
|
213
|
+
object.__setattr__(self, "layers", layers)
|
|
214
|
+
dbu = _decimal_or_none(self.dbu)
|
|
215
|
+
precision = _decimal_or_none(self.precision)
|
|
216
|
+
if dbu is not None and precision is not None and dbu != precision:
|
|
217
|
+
raise ValueError("explicit dbu and precision must match in MVP semantics")
|
|
218
|
+
object.__setattr__(self, "dbu", dbu)
|
|
219
|
+
object.__setattr__(self, "precision", precision)
|
|
220
|
+
object.__setattr__(self, "path_policy", PathPolicy.coerce(self.path_policy))
|
|
221
|
+
backend = Backend.coerce(self.backend)
|
|
222
|
+
if self.require_gpu and backend == Backend.CPU:
|
|
223
|
+
raise ValueError("require_gpu cannot be used with backend='cpu'")
|
|
224
|
+
object.__setattr__(self, "backend", backend)
|
|
225
|
+
object.__setattr__(self, "evidence_engine", EvidenceEngine.coerce(self.evidence_engine))
|
|
226
|
+
object.__setattr__(self, "failure_policy", FailurePolicy.coerce(self.failure_policy))
|
|
227
|
+
if self.tile_size <= 0:
|
|
228
|
+
raise ValueError("tile_size must be positive")
|
|
229
|
+
if self.memory_budget_bytes is not None and self.memory_budget_bytes <= 0:
|
|
230
|
+
raise ValueError("memory_budget_bytes must be positive when set")
|
|
231
|
+
object.__setattr__(self, "allowed_regions", tuple(self.allowed_regions))
|
|
232
|
+
|
|
233
|
+
def to_json_dict(self) -> dict[str, object]:
|
|
234
|
+
data = {
|
|
235
|
+
"old_path": str(self.old_path),
|
|
236
|
+
"new_path": str(self.new_path),
|
|
237
|
+
"top": self.top,
|
|
238
|
+
"old_top": self.old_top,
|
|
239
|
+
"new_top": self.new_top,
|
|
240
|
+
"require_top_name_match": self.require_top_name_match,
|
|
241
|
+
"layers": "all" if self.layers == "all" else [layer.to_json() for layer in self.layers],
|
|
242
|
+
"dbu": str(self.dbu) if self.dbu is not None else None,
|
|
243
|
+
"precision": str(self.precision) if self.precision is not None else None,
|
|
244
|
+
"path_policy": self.path_policy.value,
|
|
245
|
+
"backend": self.backend.value,
|
|
246
|
+
"evidence_engine": self.evidence_engine.value,
|
|
247
|
+
"tile_size": self.tile_size,
|
|
248
|
+
"allowed_regions": [region.to_json() for region in self.allowed_regions],
|
|
249
|
+
"allow_empty_selected_layer": self.allow_empty_selected_layer,
|
|
250
|
+
"strict": self.strict,
|
|
251
|
+
"require_gpu": self.require_gpu,
|
|
252
|
+
"failure_policy": self.failure_policy.value,
|
|
253
|
+
"memory_budget_bytes": self.memory_budget_bytes,
|
|
254
|
+
}
|
|
255
|
+
if self.geometry_cache_dir is not None:
|
|
256
|
+
data["geometry_cache_dir"] = str(self.geometry_cache_dir)
|
|
257
|
+
return data
|
gdsdiff/cuda_setup.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import asdict, dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Sequence
|
|
12
|
+
|
|
13
|
+
from .native import nvcc_architecture_flags, resolve_nvcc_path
|
|
14
|
+
from .native_cache import native_cache_dir
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CudaPreparationError(RuntimeError):
|
|
18
|
+
"""Raised when the optional CUDA backend cannot be prepared and verified."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class CudaToolchainReport:
|
|
23
|
+
available: bool
|
|
24
|
+
platform: str
|
|
25
|
+
machine: str
|
|
26
|
+
nvcc_path: str | None
|
|
27
|
+
nvcc_version: str | None
|
|
28
|
+
host_compiler: str | None
|
|
29
|
+
driver_version: str | None
|
|
30
|
+
devices: tuple[str, ...]
|
|
31
|
+
architectures: tuple[str, ...]
|
|
32
|
+
compile_flags: tuple[str, ...]
|
|
33
|
+
cache_path: str
|
|
34
|
+
reason: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_json(self) -> dict[str, object]:
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class CudaPreparationReport:
|
|
42
|
+
ready: bool
|
|
43
|
+
built: bool
|
|
44
|
+
reused: bool
|
|
45
|
+
verified: bool
|
|
46
|
+
library_path: str
|
|
47
|
+
toolchain: CudaToolchainReport
|
|
48
|
+
|
|
49
|
+
def to_json(self) -> dict[str, object]:
|
|
50
|
+
return asdict(self)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def inspect_cuda_toolchain() -> CudaToolchainReport:
|
|
54
|
+
"""Inspect CUDA without importing, compiling, or loading the backend."""
|
|
55
|
+
|
|
56
|
+
nvcc = resolve_nvcc_path()
|
|
57
|
+
version = _command_output((nvcc, "--version")) if nvcc else None
|
|
58
|
+
smi = _command_output(
|
|
59
|
+
(
|
|
60
|
+
"nvidia-smi",
|
|
61
|
+
"--query-gpu=name,compute_cap,driver_version",
|
|
62
|
+
"--format=csv,noheader,nounits",
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
devices = tuple(line.strip() for line in (smi or "").splitlines() if line.strip())
|
|
66
|
+
driver = None
|
|
67
|
+
if devices:
|
|
68
|
+
parts = [part.strip() for part in devices[0].split(",")]
|
|
69
|
+
driver = parts[-1] if len(parts) >= 3 else None
|
|
70
|
+
architectures = tuple(
|
|
71
|
+
part.strip() for part in os.environ.get("GDSDIFF_CUDA_ARCHITECTURES", "").replace(";", ",").split(",") if part.strip()
|
|
72
|
+
)
|
|
73
|
+
available = nvcc is not None and bool(devices)
|
|
74
|
+
reason = None
|
|
75
|
+
if nvcc is None:
|
|
76
|
+
reason = "nvcc was not found in PATH, CUDA_HOME, /usr/local/cuda, or /opt/cuda"
|
|
77
|
+
elif not devices:
|
|
78
|
+
reason = "no NVIDIA GPU/driver was visible through nvidia-smi"
|
|
79
|
+
return CudaToolchainReport(
|
|
80
|
+
available=available,
|
|
81
|
+
platform=platform.system(),
|
|
82
|
+
machine=platform.machine(),
|
|
83
|
+
nvcc_path=nvcc,
|
|
84
|
+
nvcc_version=version,
|
|
85
|
+
host_compiler=_which("g++"),
|
|
86
|
+
driver_version=driver,
|
|
87
|
+
devices=devices,
|
|
88
|
+
architectures=architectures,
|
|
89
|
+
compile_flags=nvcc_architecture_flags(),
|
|
90
|
+
cache_path=str(native_cache_dir()),
|
|
91
|
+
reason=reason,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def prepare_cuda() -> CudaPreparationReport:
|
|
96
|
+
"""Build the CUDA backend in the user cache and run a device smoke test."""
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
toolchain = inspect_cuda_toolchain()
|
|
100
|
+
except (OSError, ValueError) as exc:
|
|
101
|
+
raise CudaPreparationError(str(exc)) from exc
|
|
102
|
+
if not toolchain.available:
|
|
103
|
+
raise CudaPreparationError(toolchain.reason or "CUDA toolchain is unavailable")
|
|
104
|
+
try:
|
|
105
|
+
from .backends.cuda_ctypes import compare_files_cuda, prepared_cuda_library_path
|
|
106
|
+
|
|
107
|
+
library = prepared_cuda_library_path()
|
|
108
|
+
existed = library.is_file()
|
|
109
|
+
with tempfile.TemporaryDirectory(prefix="gdsdiff-cuda-smoke-") as directory:
|
|
110
|
+
left = Path(directory) / "left.bin"
|
|
111
|
+
right = Path(directory) / "right.bin"
|
|
112
|
+
left.write_bytes(b"gdsdiff-cuda-smoke")
|
|
113
|
+
right.write_bytes(b"gdsdiff-cuda-smoke")
|
|
114
|
+
result = compare_files_cuda(left, right)
|
|
115
|
+
if not result.same:
|
|
116
|
+
raise CudaPreparationError("CUDA smoke comparison returned a mismatch")
|
|
117
|
+
except CudaPreparationError:
|
|
118
|
+
raise
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
raise CudaPreparationError(str(exc)) from exc
|
|
121
|
+
if not library.is_file():
|
|
122
|
+
raise CudaPreparationError("CUDA preparation did not publish the expected native library")
|
|
123
|
+
built = not existed
|
|
124
|
+
return CudaPreparationReport(
|
|
125
|
+
ready=True,
|
|
126
|
+
built=built,
|
|
127
|
+
reused=not built,
|
|
128
|
+
verified=True,
|
|
129
|
+
library_path=str(library),
|
|
130
|
+
toolchain=toolchain,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
135
|
+
import argparse
|
|
136
|
+
|
|
137
|
+
parser = argparse.ArgumentParser(prog="gdsdiff-prepare-cuda", description="Build and verify the optional gdsdiff CUDA backend.")
|
|
138
|
+
parser.add_argument("--json", action="store_true", help="Emit a machine-readable report.")
|
|
139
|
+
args = parser.parse_args(argv)
|
|
140
|
+
try:
|
|
141
|
+
report = prepare_cuda()
|
|
142
|
+
except CudaPreparationError as exc:
|
|
143
|
+
if args.json:
|
|
144
|
+
print(json.dumps({"ready": False, "error": str(exc)}, indent=2, sort_keys=True))
|
|
145
|
+
else:
|
|
146
|
+
print(f"gdsdiff-prepare-cuda: {exc}", file=sys.stderr)
|
|
147
|
+
return 2
|
|
148
|
+
if args.json:
|
|
149
|
+
print(json.dumps(report.to_json(), indent=2, sort_keys=True))
|
|
150
|
+
else:
|
|
151
|
+
action = "built" if report.built else "reused"
|
|
152
|
+
print(f"CUDA backend ready ({action}): {report.library_path}")
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _command_output(command: tuple[str | None, ...]) -> str | None:
|
|
157
|
+
if not command or command[0] is None:
|
|
158
|
+
return None
|
|
159
|
+
try:
|
|
160
|
+
completed = subprocess.run(command, check=False, capture_output=True, text=True, timeout=10)
|
|
161
|
+
except (OSError, subprocess.SubprocessError):
|
|
162
|
+
return None
|
|
163
|
+
output = (completed.stdout or completed.stderr).strip()
|
|
164
|
+
return output if completed.returncode == 0 and output else None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _which(command: str) -> str | None:
|
|
168
|
+
import shutil
|
|
169
|
+
|
|
170
|
+
return shutil.which(command)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import date
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .exact_oracle import format_twice_area
|
|
8
|
+
from .policy import PolicyClassificationResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class DesignHistoryEntry:
|
|
13
|
+
title: str
|
|
14
|
+
policy_result: PolicyClassificationResult
|
|
15
|
+
diff_gds_path: str | Path | None = None
|
|
16
|
+
tests_run: tuple[str, ...] = field(default_factory=tuple)
|
|
17
|
+
notes: tuple[str, ...] = field(default_factory=tuple)
|
|
18
|
+
entry_date: date | None = None
|
|
19
|
+
|
|
20
|
+
def render_markdown(self) -> str:
|
|
21
|
+
entry_date = self.entry_date or date.today()
|
|
22
|
+
lines = [
|
|
23
|
+
f"### {entry_date.isoformat()} - {self.title}",
|
|
24
|
+
"",
|
|
25
|
+
f"- Status: {self.policy_result.status.value}.",
|
|
26
|
+
f"- Expected diff area dbu^2: {format_twice_area(self.policy_result.expected_twice_area)}.",
|
|
27
|
+
f"- Unexpected diff area dbu^2: {format_twice_area(self.policy_result.unexpected_twice_area)}.",
|
|
28
|
+
]
|
|
29
|
+
if self.diff_gds_path is not None:
|
|
30
|
+
lines.append(f"- Diff GDS: `{self.diff_gds_path}`.")
|
|
31
|
+
if self.tests_run:
|
|
32
|
+
lines.append("- Tests run:")
|
|
33
|
+
lines.extend(f" - `{test}`" for test in self.tests_run)
|
|
34
|
+
if self.notes:
|
|
35
|
+
lines.append("- Notes:")
|
|
36
|
+
lines.extend(f" - {note}" for note in self.notes)
|
|
37
|
+
lines.append("- Layer summary:")
|
|
38
|
+
for layer in self.policy_result.layers:
|
|
39
|
+
lines.append(
|
|
40
|
+
" - "
|
|
41
|
+
f"{layer.layer.layer}:{layer.layer.datatype} expected="
|
|
42
|
+
f"{format_twice_area(layer.expected_twice_area)} unexpected="
|
|
43
|
+
f"{format_twice_area(layer.unexpected_twice_area)} components="
|
|
44
|
+
f"{len(layer.components)}"
|
|
45
|
+
)
|
|
46
|
+
return "\n".join(lines) + "\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render_design_history_entry(
|
|
50
|
+
title: str,
|
|
51
|
+
policy_result: PolicyClassificationResult,
|
|
52
|
+
*,
|
|
53
|
+
diff_gds_path: str | Path | None = None,
|
|
54
|
+
tests_run: tuple[str, ...] = (),
|
|
55
|
+
notes: tuple[str, ...] = (),
|
|
56
|
+
entry_date: date | None = None,
|
|
57
|
+
) -> str:
|
|
58
|
+
return DesignHistoryEntry(
|
|
59
|
+
title=title,
|
|
60
|
+
policy_result=policy_result,
|
|
61
|
+
diff_gds_path=diff_gds_path,
|
|
62
|
+
tests_run=tests_run,
|
|
63
|
+
notes=notes,
|
|
64
|
+
entry_date=entry_date,
|
|
65
|
+
).render_markdown()
|
gdsdiff/diagnostics.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Sequence
|
|
7
|
+
|
|
8
|
+
from .native import discover_native_capabilities
|
|
9
|
+
from .native_cache import ENV_NATIVE_CACHE_DIR, LEGACY_ENV_NATIVE_CACHE_DIR, native_cache_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def backend_diagnostics() -> dict[str, Any]:
|
|
13
|
+
"""Return CPU-safe runtime and optional-backend diagnostics."""
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
"python": {
|
|
17
|
+
"implementation": platform.python_implementation(),
|
|
18
|
+
"version": platform.python_version(),
|
|
19
|
+
},
|
|
20
|
+
"platform": {
|
|
21
|
+
"system": platform.system(),
|
|
22
|
+
"machine": platform.machine(),
|
|
23
|
+
},
|
|
24
|
+
"native_cache": {
|
|
25
|
+
"path": str(native_cache_dir()),
|
|
26
|
+
"environment": ENV_NATIVE_CACHE_DIR,
|
|
27
|
+
"legacy_environment": LEGACY_ENV_NATIVE_CACHE_DIR,
|
|
28
|
+
},
|
|
29
|
+
"capabilities": discover_native_capabilities().to_json(),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
34
|
+
if argv:
|
|
35
|
+
print("gdsdiff-diagnostics does not accept arguments", file=sys.stderr)
|
|
36
|
+
return 2
|
|
37
|
+
print(json.dumps(backend_diagnostics(), indent=2, sort_keys=True))
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
raise SystemExit(main())
|
gdsdiff/diff_gds.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import gdstk
|
|
7
|
+
|
|
8
|
+
from .policy import ClassifiedComponent, PolicyClassificationResult
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class DiffGdsWriteResult:
|
|
13
|
+
path: Path
|
|
14
|
+
component_count: int
|
|
15
|
+
category_cells: tuple[str, ...]
|
|
16
|
+
|
|
17
|
+
def to_json(self) -> dict[str, object]:
|
|
18
|
+
return {
|
|
19
|
+
"path": str(self.path),
|
|
20
|
+
"component_count": self.component_count,
|
|
21
|
+
"category_cells": list(self.category_cells),
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_diff_gds(
|
|
26
|
+
policy_result: PolicyClassificationResult,
|
|
27
|
+
path: str | Path,
|
|
28
|
+
*,
|
|
29
|
+
unit: float = 1e-6,
|
|
30
|
+
precision: float = 1e-9,
|
|
31
|
+
top_cell_name: str = "GDS_GPU_COMPARE_DIFF",
|
|
32
|
+
) -> DiffGdsWriteResult:
|
|
33
|
+
path = Path(path)
|
|
34
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
lib = gdstk.Library(unit=unit, precision=precision)
|
|
36
|
+
top = lib.new_cell(top_cell_name)
|
|
37
|
+
cells: dict[str, gdstk.Cell] = {}
|
|
38
|
+
component_count = 0
|
|
39
|
+
for component in _iter_components(policy_result):
|
|
40
|
+
cell_name = _category_cell_name(component)
|
|
41
|
+
cell = cells.get(cell_name)
|
|
42
|
+
if cell is None:
|
|
43
|
+
cell = lib.new_cell(cell_name)
|
|
44
|
+
cells[cell_name] = cell
|
|
45
|
+
top.add(gdstk.Reference(cell))
|
|
46
|
+
cell.add(
|
|
47
|
+
gdstk.Polygon(
|
|
48
|
+
component.points,
|
|
49
|
+
layer=component.layer.layer,
|
|
50
|
+
datatype=component.layer.datatype,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
component_count += 1
|
|
54
|
+
lib.write_gds(path)
|
|
55
|
+
return DiffGdsWriteResult(path=path, component_count=component_count, category_cells=tuple(sorted(cells)))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _iter_components(policy_result: PolicyClassificationResult) -> tuple[ClassifiedComponent, ...]:
|
|
59
|
+
components = []
|
|
60
|
+
for layer in policy_result.layers:
|
|
61
|
+
components.extend(layer.components)
|
|
62
|
+
return tuple(sorted(components, key=lambda component: (component.classification, component.direction.value, component.layer, component.bbox)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _category_cell_name(component: ClassifiedComponent) -> str:
|
|
66
|
+
return f"{component.classification}_{component.direction.value}".upper().replace("-", "_")
|