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/extract.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from fractions import Fraction
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .config import LayerSpec, parse_layer_selector
|
|
13
|
+
from ._environment import get_environment
|
|
14
|
+
from .gds_metadata import decimal_from_number
|
|
15
|
+
from .geometry import LayerTable, PolygonBuffer, normalize_polygon_points, polygon_bbox, polygon_flags
|
|
16
|
+
from .grid import DEFAULT_COORDINATE_SAFETY_LIMIT, snap_to_grid
|
|
17
|
+
from .inventory import select_top_cell
|
|
18
|
+
from .multiprocessing_support import fork_context
|
|
19
|
+
from .semantics import PathPolicy
|
|
20
|
+
|
|
21
|
+
_WORKER_RAW_POLYGONS = None
|
|
22
|
+
_WORKER_SELECTED_LAYERS = None
|
|
23
|
+
_WORKER_GRID = None
|
|
24
|
+
_WORKER_UNIT = None
|
|
25
|
+
_WORKER_FAST_SCALE = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ExtractionStats:
|
|
30
|
+
raw_polygon_count: int
|
|
31
|
+
kept_polygon_count: int
|
|
32
|
+
vertex_count: int
|
|
33
|
+
max_snap_error_dbu: Decimal
|
|
34
|
+
|
|
35
|
+
def to_json(self) -> dict[str, object]:
|
|
36
|
+
return {
|
|
37
|
+
"raw_polygon_count": self.raw_polygon_count,
|
|
38
|
+
"kept_polygon_count": self.kept_polygon_count,
|
|
39
|
+
"vertex_count": self.vertex_count,
|
|
40
|
+
"max_snap_error_dbu": str(self.max_snap_error_dbu),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class ExtractionResult:
|
|
46
|
+
path: Path
|
|
47
|
+
top_name: str
|
|
48
|
+
buffer: PolygonBuffer
|
|
49
|
+
stats: ExtractionStats
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extract_gds_polygons(
|
|
53
|
+
path: str | Path,
|
|
54
|
+
*,
|
|
55
|
+
top_name: str | None = None,
|
|
56
|
+
layers: str | LayerSpec | Iterable[str | tuple[int, int] | LayerSpec] = "all",
|
|
57
|
+
meters_per_dbu: Decimal | str | float | None = None,
|
|
58
|
+
path_policy: PathPolicy | str = PathPolicy.GDSTK,
|
|
59
|
+
) -> ExtractionResult:
|
|
60
|
+
import gdstk
|
|
61
|
+
|
|
62
|
+
path = Path(path)
|
|
63
|
+
path_policy = PathPolicy.coerce(path_policy)
|
|
64
|
+
if path_policy == PathPolicy.REPORT_ONLY:
|
|
65
|
+
raise ValueError("report-only path policy cannot produce a passing extraction")
|
|
66
|
+
lib = gdstk.read_gds(str(path))
|
|
67
|
+
top = select_top_cell(lib, top_name=top_name)
|
|
68
|
+
if path_policy == PathPolicy.REJECT and top.get_paths():
|
|
69
|
+
raise ValueError("selected geometry contains paths and path_policy='reject'")
|
|
70
|
+
grid = decimal_from_number(meters_per_dbu) if meters_per_dbu is not None else decimal_from_number(lib.precision)
|
|
71
|
+
unit = decimal_from_number(lib.unit)
|
|
72
|
+
raw_polygons = top.get_polygons(apply_repetitions=True, include_paths=True)
|
|
73
|
+
selected_layers = _selected_layer_set(layers, raw_polygons)
|
|
74
|
+
layer_table = LayerTable(tuple(sorted(selected_layers)))
|
|
75
|
+
layer_index = layer_table.index_by_layer
|
|
76
|
+
|
|
77
|
+
records, max_error = _extract_polygon_records(raw_polygons, selected_layers, grid, unit)
|
|
78
|
+
|
|
79
|
+
all_vertices: list[tuple[int, int]] = []
|
|
80
|
+
offsets: list[int] = [0]
|
|
81
|
+
layer_ids: list[int] = []
|
|
82
|
+
bboxes: list[tuple[int, int, int, int]] = []
|
|
83
|
+
flags: list[int] = []
|
|
84
|
+
source_ids: list[int] = []
|
|
85
|
+
|
|
86
|
+
for source_id, spec, normalized, bbox, polygon_flag in records:
|
|
87
|
+
all_vertices.extend(normalized)
|
|
88
|
+
offsets.append(len(all_vertices))
|
|
89
|
+
layer_ids.append(layer_index[spec])
|
|
90
|
+
bboxes.append(bbox)
|
|
91
|
+
flags.append(polygon_flag)
|
|
92
|
+
source_ids.append(source_id)
|
|
93
|
+
|
|
94
|
+
buffer = PolygonBuffer(
|
|
95
|
+
vertices_xy=np.asarray(all_vertices, dtype=np.int64).reshape((-1, 2)),
|
|
96
|
+
polygon_offsets=np.asarray(offsets, dtype=np.uint64),
|
|
97
|
+
layer_ids=np.asarray(layer_ids, dtype=np.uint32),
|
|
98
|
+
bboxes=np.asarray(bboxes, dtype=np.int64).reshape((-1, 4)),
|
|
99
|
+
flags=np.asarray(flags, dtype=np.uint32),
|
|
100
|
+
source_ids=np.asarray(source_ids, dtype=np.uint64),
|
|
101
|
+
layer_table=layer_table,
|
|
102
|
+
)
|
|
103
|
+
return ExtractionResult(
|
|
104
|
+
path=path,
|
|
105
|
+
top_name=top.name,
|
|
106
|
+
buffer=buffer,
|
|
107
|
+
stats=ExtractionStats(
|
|
108
|
+
raw_polygon_count=len(raw_polygons),
|
|
109
|
+
kept_polygon_count=buffer.polygon_count,
|
|
110
|
+
vertex_count=buffer.vertex_count,
|
|
111
|
+
max_snap_error_dbu=max_error,
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _selected_layer_set(layers, raw_polygons: list[gdstk.Polygon]) -> set[LayerSpec]:
|
|
117
|
+
if layers == "all":
|
|
118
|
+
return {LayerSpec(polygon.layer, polygon.datatype) for polygon in raw_polygons}
|
|
119
|
+
parsed = parse_layer_selector(layers)
|
|
120
|
+
if parsed == "all":
|
|
121
|
+
return {LayerSpec(polygon.layer, polygon.datatype) for polygon in raw_polygons}
|
|
122
|
+
return set(parsed)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _extract_polygon_records(
|
|
126
|
+
raw_polygons: list[gdstk.Polygon],
|
|
127
|
+
selected_layers: set[LayerSpec],
|
|
128
|
+
grid: Decimal,
|
|
129
|
+
unit: Decimal,
|
|
130
|
+
) -> tuple[
|
|
131
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
132
|
+
Decimal,
|
|
133
|
+
]:
|
|
134
|
+
fast_scale = _fast_integer_snap_scale(unit, grid)
|
|
135
|
+
if fast_scale is not None:
|
|
136
|
+
return _extract_polygon_records_fast(raw_polygons, selected_layers, fast_scale)
|
|
137
|
+
|
|
138
|
+
worker_count = _resolve_extract_worker_count(len(raw_polygons))
|
|
139
|
+
if worker_count <= 1:
|
|
140
|
+
return _extract_polygon_records_serial(raw_polygons, selected_layers, grid, unit)
|
|
141
|
+
|
|
142
|
+
chunk_count = max(worker_count, worker_count * 8)
|
|
143
|
+
chunk_size = max(1, (len(raw_polygons) + chunk_count - 1) // chunk_count)
|
|
144
|
+
chunks = tuple((start, min(start + chunk_size, len(raw_polygons))) for start in range(0, len(raw_polygons), chunk_size))
|
|
145
|
+
context = fork_context()
|
|
146
|
+
if context is None:
|
|
147
|
+
return _extract_polygon_records_serial(raw_polygons, selected_layers, grid, unit)
|
|
148
|
+
|
|
149
|
+
global _WORKER_RAW_POLYGONS, _WORKER_SELECTED_LAYERS, _WORKER_GRID, _WORKER_UNIT
|
|
150
|
+
_WORKER_RAW_POLYGONS = raw_polygons
|
|
151
|
+
_WORKER_SELECTED_LAYERS = selected_layers
|
|
152
|
+
_WORKER_GRID = grid
|
|
153
|
+
_WORKER_UNIT = unit
|
|
154
|
+
try:
|
|
155
|
+
try:
|
|
156
|
+
with context.Pool(processes=worker_count) as pool:
|
|
157
|
+
chunk_results = pool.map(_extract_polygon_records_worker, chunks, chunksize=1)
|
|
158
|
+
except OSError:
|
|
159
|
+
return _extract_polygon_records_serial(raw_polygons, selected_layers, grid, unit)
|
|
160
|
+
finally:
|
|
161
|
+
_WORKER_RAW_POLYGONS = None
|
|
162
|
+
_WORKER_SELECTED_LAYERS = None
|
|
163
|
+
_WORKER_GRID = None
|
|
164
|
+
_WORKER_UNIT = None
|
|
165
|
+
|
|
166
|
+
records = []
|
|
167
|
+
max_error = Decimal(0)
|
|
168
|
+
for chunk_records, chunk_max_error in chunk_results:
|
|
169
|
+
records.extend(chunk_records)
|
|
170
|
+
max_error = max(max_error, chunk_max_error)
|
|
171
|
+
records.sort(key=lambda record: record[0])
|
|
172
|
+
return records, max_error
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _fast_integer_snap_scale(unit: Decimal, grid: Decimal) -> int | None:
|
|
176
|
+
if get_environment("GDSDIFF_EXTRACT_DECIMAL", "GDS_GPU_COMPARE_EXTRACT_DECIMAL") == "1":
|
|
177
|
+
return None
|
|
178
|
+
try:
|
|
179
|
+
ratio = Fraction(unit) / Fraction(grid)
|
|
180
|
+
except (ArithmeticError, ValueError):
|
|
181
|
+
return None
|
|
182
|
+
if ratio.denominator != 1:
|
|
183
|
+
return None
|
|
184
|
+
scale = ratio.numerator
|
|
185
|
+
if scale <= 0:
|
|
186
|
+
return None
|
|
187
|
+
return scale
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _extract_polygon_records_fast(
|
|
191
|
+
raw_polygons: list[gdstk.Polygon],
|
|
192
|
+
selected_layers: set[LayerSpec],
|
|
193
|
+
scale: int,
|
|
194
|
+
) -> tuple[
|
|
195
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
196
|
+
Decimal,
|
|
197
|
+
]:
|
|
198
|
+
worker_count = _resolve_extract_worker_count(len(raw_polygons))
|
|
199
|
+
if worker_count <= 1:
|
|
200
|
+
return _extract_polygon_records_fast_from_range(raw_polygons, selected_layers, scale, 0, len(raw_polygons))
|
|
201
|
+
|
|
202
|
+
chunk_count = max(worker_count, worker_count * 8)
|
|
203
|
+
chunk_size = max(1, (len(raw_polygons) + chunk_count - 1) // chunk_count)
|
|
204
|
+
chunks = tuple((start, min(start + chunk_size, len(raw_polygons))) for start in range(0, len(raw_polygons), chunk_size))
|
|
205
|
+
context = fork_context()
|
|
206
|
+
if context is None:
|
|
207
|
+
return _extract_polygon_records_fast_from_range(raw_polygons, selected_layers, scale, 0, len(raw_polygons))
|
|
208
|
+
|
|
209
|
+
global _WORKER_RAW_POLYGONS, _WORKER_SELECTED_LAYERS, _WORKER_FAST_SCALE
|
|
210
|
+
_WORKER_RAW_POLYGONS = raw_polygons
|
|
211
|
+
_WORKER_SELECTED_LAYERS = selected_layers
|
|
212
|
+
_WORKER_FAST_SCALE = scale
|
|
213
|
+
try:
|
|
214
|
+
try:
|
|
215
|
+
with context.Pool(processes=worker_count) as pool:
|
|
216
|
+
chunk_results = pool.map(_extract_polygon_records_fast_worker, chunks, chunksize=1)
|
|
217
|
+
except OSError:
|
|
218
|
+
return _extract_polygon_records_fast_from_range(raw_polygons, selected_layers, scale, 0, len(raw_polygons))
|
|
219
|
+
finally:
|
|
220
|
+
_WORKER_RAW_POLYGONS = None
|
|
221
|
+
_WORKER_SELECTED_LAYERS = None
|
|
222
|
+
_WORKER_FAST_SCALE = None
|
|
223
|
+
|
|
224
|
+
records = []
|
|
225
|
+
max_error = Decimal(0)
|
|
226
|
+
for chunk_records, chunk_max_error in chunk_results:
|
|
227
|
+
records.extend(chunk_records)
|
|
228
|
+
max_error = max(max_error, chunk_max_error)
|
|
229
|
+
records.sort(key=lambda record: record[0])
|
|
230
|
+
return records, max_error
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _extract_polygon_records_fast_worker(
|
|
234
|
+
chunk: tuple[int, int],
|
|
235
|
+
) -> tuple[
|
|
236
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
237
|
+
Decimal,
|
|
238
|
+
]:
|
|
239
|
+
if _WORKER_RAW_POLYGONS is None or _WORKER_SELECTED_LAYERS is None or _WORKER_FAST_SCALE is None:
|
|
240
|
+
raise RuntimeError("fast extraction worker globals are not initialized")
|
|
241
|
+
start, stop = chunk
|
|
242
|
+
return _extract_polygon_records_fast_from_range(
|
|
243
|
+
_WORKER_RAW_POLYGONS,
|
|
244
|
+
_WORKER_SELECTED_LAYERS,
|
|
245
|
+
_WORKER_FAST_SCALE,
|
|
246
|
+
start,
|
|
247
|
+
stop,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _extract_polygon_records_fast_from_range(
|
|
252
|
+
raw_polygons: list[gdstk.Polygon],
|
|
253
|
+
selected_layers: set[LayerSpec],
|
|
254
|
+
scale: int,
|
|
255
|
+
start: int,
|
|
256
|
+
stop: int,
|
|
257
|
+
) -> tuple[
|
|
258
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
259
|
+
Decimal,
|
|
260
|
+
]:
|
|
261
|
+
records = []
|
|
262
|
+
max_error = 0.0
|
|
263
|
+
float_scale = float(scale)
|
|
264
|
+
for source_id in range(start, stop):
|
|
265
|
+
polygon = raw_polygons[source_id]
|
|
266
|
+
spec = LayerSpec(polygon.layer, polygon.datatype)
|
|
267
|
+
if spec not in selected_layers:
|
|
268
|
+
continue
|
|
269
|
+
points = np.asarray(polygon.points, dtype=np.float64)
|
|
270
|
+
if points.size == 0:
|
|
271
|
+
continue
|
|
272
|
+
scaled = points * float_scale
|
|
273
|
+
snapped = _round_half_away_from_zero(scaled)
|
|
274
|
+
if snapped.size and np.max(np.abs(snapped)) > DEFAULT_COORDINATE_SAFETY_LIMIT:
|
|
275
|
+
raise OverflowError("coordinate exceeds configured safety limit")
|
|
276
|
+
max_error = max(max_error, float(np.max(np.abs(scaled - snapped))))
|
|
277
|
+
snapped_points = tuple((int(x), int(y)) for x, y in snapped.astype(np.int64, copy=False))
|
|
278
|
+
normalized = normalize_polygon_points(snapped_points)
|
|
279
|
+
if not normalized:
|
|
280
|
+
continue
|
|
281
|
+
records.append((source_id, spec, normalized, polygon_bbox(normalized), polygon_flags(normalized)))
|
|
282
|
+
return records, Decimal(str(max_error))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _round_half_away_from_zero(values: np.ndarray) -> np.ndarray:
|
|
286
|
+
return np.copysign(np.floor(np.abs(values) + 0.5), values)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _resolve_extract_worker_count(raw_polygon_count: int) -> int:
|
|
290
|
+
if raw_polygon_count < _parallel_min_polygons():
|
|
291
|
+
return 1
|
|
292
|
+
return max(1, os.cpu_count() or 1)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _parallel_min_polygons() -> int:
|
|
296
|
+
try:
|
|
297
|
+
value = int(
|
|
298
|
+
get_environment(
|
|
299
|
+
"GDSDIFF_PARALLEL_MIN_POLYGONS",
|
|
300
|
+
"GDS_GPU_COMPARE_PARALLEL_MIN_POLYGONS",
|
|
301
|
+
"",
|
|
302
|
+
)
|
|
303
|
+
or ""
|
|
304
|
+
)
|
|
305
|
+
except ValueError:
|
|
306
|
+
return 30_000
|
|
307
|
+
return value if value > 0 else 30_000
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _extract_polygon_records_serial(
|
|
311
|
+
raw_polygons: list[gdstk.Polygon],
|
|
312
|
+
selected_layers: set[LayerSpec],
|
|
313
|
+
grid: Decimal,
|
|
314
|
+
unit: Decimal,
|
|
315
|
+
) -> tuple[
|
|
316
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
317
|
+
Decimal,
|
|
318
|
+
]:
|
|
319
|
+
return _extract_polygon_records_from_range(raw_polygons, selected_layers, grid, unit, 0, len(raw_polygons))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _extract_polygon_records_worker(
|
|
323
|
+
chunk: tuple[int, int],
|
|
324
|
+
) -> tuple[
|
|
325
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
326
|
+
Decimal,
|
|
327
|
+
]:
|
|
328
|
+
if (
|
|
329
|
+
_WORKER_RAW_POLYGONS is None
|
|
330
|
+
or _WORKER_SELECTED_LAYERS is None
|
|
331
|
+
or _WORKER_GRID is None
|
|
332
|
+
or _WORKER_UNIT is None
|
|
333
|
+
):
|
|
334
|
+
raise RuntimeError("extraction worker globals are not initialized")
|
|
335
|
+
start, stop = chunk
|
|
336
|
+
return _extract_polygon_records_from_range(
|
|
337
|
+
_WORKER_RAW_POLYGONS,
|
|
338
|
+
_WORKER_SELECTED_LAYERS,
|
|
339
|
+
_WORKER_GRID,
|
|
340
|
+
_WORKER_UNIT,
|
|
341
|
+
start,
|
|
342
|
+
stop,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _extract_polygon_records_from_range(
|
|
347
|
+
raw_polygons: list[gdstk.Polygon],
|
|
348
|
+
selected_layers: set[LayerSpec],
|
|
349
|
+
grid: Decimal,
|
|
350
|
+
unit: Decimal,
|
|
351
|
+
start: int,
|
|
352
|
+
stop: int,
|
|
353
|
+
) -> tuple[
|
|
354
|
+
list[tuple[int, LayerSpec, tuple[tuple[int, int], ...], tuple[int, int, int, int], int]],
|
|
355
|
+
Decimal,
|
|
356
|
+
]:
|
|
357
|
+
records = []
|
|
358
|
+
max_error = Decimal(0)
|
|
359
|
+
for source_id in range(start, stop):
|
|
360
|
+
polygon = raw_polygons[source_id]
|
|
361
|
+
spec = LayerSpec(polygon.layer, polygon.datatype)
|
|
362
|
+
if spec not in selected_layers:
|
|
363
|
+
continue
|
|
364
|
+
snapped_points: list[tuple[int, int]] = []
|
|
365
|
+
for point in polygon.points:
|
|
366
|
+
x_physical = decimal_from_number(float(point[0])) * unit
|
|
367
|
+
y_physical = decimal_from_number(float(point[1])) * unit
|
|
368
|
+
x = snap_to_grid(x_physical, grid)
|
|
369
|
+
y = snap_to_grid(y_physical, grid)
|
|
370
|
+
max_error = max(max_error, abs(x_physical / grid - Decimal(x)), abs(y_physical / grid - Decimal(y)))
|
|
371
|
+
snapped_points.append((x, y))
|
|
372
|
+
normalized = normalize_polygon_points(snapped_points)
|
|
373
|
+
if not normalized:
|
|
374
|
+
continue
|
|
375
|
+
records.append((source_id, spec, normalized, polygon_bbox(normalized), polygon_flags(normalized)))
|
|
376
|
+
return records, max_error
|
gdsdiff/gds_metadata.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def decimal_from_number(value: int | float | str | Decimal) -> Decimal:
|
|
9
|
+
if isinstance(value, Decimal):
|
|
10
|
+
return value
|
|
11
|
+
return Decimal(str(value))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class GdsMetadata:
|
|
16
|
+
path: Path
|
|
17
|
+
unit_m: Decimal
|
|
18
|
+
precision_m: Decimal
|
|
19
|
+
cell_count: int
|
|
20
|
+
top_names: tuple[str, ...]
|
|
21
|
+
|
|
22
|
+
def to_json(self) -> dict[str, object]:
|
|
23
|
+
return {
|
|
24
|
+
"path": str(self.path),
|
|
25
|
+
"unit_m": str(self.unit_m),
|
|
26
|
+
"precision_m": str(self.precision_m),
|
|
27
|
+
"cell_count": self.cell_count,
|
|
28
|
+
"top_names": list(self.top_names),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_gds_metadata(path: str | Path) -> GdsMetadata:
|
|
33
|
+
import gdstk
|
|
34
|
+
|
|
35
|
+
path = Path(path)
|
|
36
|
+
lib = gdstk.read_gds(str(path))
|
|
37
|
+
return GdsMetadata(
|
|
38
|
+
path=path,
|
|
39
|
+
unit_m=decimal_from_number(lib.unit),
|
|
40
|
+
precision_m=decimal_from_number(lib.precision),
|
|
41
|
+
cell_count=len(lib.cells),
|
|
42
|
+
top_names=tuple(cell.name for cell in lib.top_level()),
|
|
43
|
+
)
|
gdsdiff/geometry.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .config import LayerSpec
|
|
8
|
+
|
|
9
|
+
FLAG_FEWER_THAN_THREE_POINTS = 1 << 0
|
|
10
|
+
FLAG_ZERO_AREA = 1 << 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class LayerTable:
|
|
15
|
+
layers: tuple[LayerSpec, ...]
|
|
16
|
+
|
|
17
|
+
def __post_init__(self) -> None:
|
|
18
|
+
if len(set(self.layers)) != len(self.layers):
|
|
19
|
+
raise ValueError("LayerTable cannot contain duplicate layers")
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def index_by_layer(self) -> dict[LayerSpec, int]:
|
|
23
|
+
return {layer: index for index, layer in enumerate(self.layers)}
|
|
24
|
+
|
|
25
|
+
def to_json(self) -> list[dict[str, int]]:
|
|
26
|
+
return [layer.to_json() for layer in self.layers]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PolygonBuffer:
|
|
31
|
+
vertices_xy: np.ndarray
|
|
32
|
+
polygon_offsets: np.ndarray
|
|
33
|
+
layer_ids: np.ndarray
|
|
34
|
+
bboxes: np.ndarray
|
|
35
|
+
flags: np.ndarray
|
|
36
|
+
source_ids: np.ndarray
|
|
37
|
+
layer_table: LayerTable
|
|
38
|
+
polygon_hashes: np.ndarray | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
if self.vertices_xy.dtype != np.int64 or self.vertices_xy.ndim != 2 or self.vertices_xy.shape[1] != 2:
|
|
42
|
+
raise TypeError("vertices_xy must be an int64 array with shape (N, 2)")
|
|
43
|
+
if self.polygon_offsets.dtype != np.uint64 or self.polygon_offsets.ndim != 1:
|
|
44
|
+
raise TypeError("polygon_offsets must be a uint64 vector")
|
|
45
|
+
if self.layer_ids.dtype != np.uint32 or self.layer_ids.ndim != 1:
|
|
46
|
+
raise TypeError("layer_ids must be a uint32 vector")
|
|
47
|
+
if self.bboxes.dtype != np.int64 or self.bboxes.shape != (self.polygon_count, 4):
|
|
48
|
+
raise TypeError("bboxes must be an int64 array with shape (N_polygons, 4)")
|
|
49
|
+
if self.flags.dtype != np.uint32 or self.flags.shape != (self.polygon_count,):
|
|
50
|
+
raise TypeError("flags must be a uint32 vector with one entry per polygon")
|
|
51
|
+
if self.source_ids.dtype != np.uint64 or self.source_ids.shape != (self.polygon_count,):
|
|
52
|
+
raise TypeError("source_ids must be a uint64 vector with one entry per polygon")
|
|
53
|
+
if self.polygon_hashes is not None and (
|
|
54
|
+
self.polygon_hashes.dtype != np.uint64 or self.polygon_hashes.shape != (self.polygon_count, 2)
|
|
55
|
+
):
|
|
56
|
+
raise TypeError("polygon_hashes must be a uint64 array with shape (N_polygons, 2)")
|
|
57
|
+
if len(self.polygon_offsets) != self.polygon_count + 1:
|
|
58
|
+
raise ValueError("polygon_offsets length must be polygon_count + 1")
|
|
59
|
+
if len(self.polygon_offsets) and int(self.polygon_offsets[0]) != 0:
|
|
60
|
+
raise ValueError("polygon_offsets must start at zero")
|
|
61
|
+
if len(self.polygon_offsets) and int(self.polygon_offsets[-1]) != self.vertex_count:
|
|
62
|
+
raise ValueError("final polygon offset must equal vertex count")
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def polygon_count(self) -> int:
|
|
66
|
+
return len(self.layer_ids)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def vertex_count(self) -> int:
|
|
70
|
+
return len(self.vertices_xy)
|
|
71
|
+
|
|
72
|
+
def polygon_vertices(self, index: int) -> np.ndarray:
|
|
73
|
+
start = int(self.polygon_offsets[index])
|
|
74
|
+
end = int(self.polygon_offsets[index + 1])
|
|
75
|
+
return self.vertices_xy[start:end]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def normalize_polygon_points(points: list[tuple[int, int]]) -> tuple[tuple[int, int], ...]:
|
|
79
|
+
if len(points) > 1 and points[0] == points[-1]:
|
|
80
|
+
points = points[:-1]
|
|
81
|
+
normalized: list[tuple[int, int]] = []
|
|
82
|
+
for point in points:
|
|
83
|
+
if not normalized or normalized[-1] != point:
|
|
84
|
+
normalized.append(point)
|
|
85
|
+
if len(normalized) > 1 and normalized[0] == normalized[-1]:
|
|
86
|
+
normalized.pop()
|
|
87
|
+
return tuple(normalized)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def polygon_bbox(points: tuple[tuple[int, int], ...]) -> tuple[int, int, int, int]:
|
|
91
|
+
xs = [point[0] for point in points]
|
|
92
|
+
ys = [point[1] for point in points]
|
|
93
|
+
return min(xs), min(ys), max(xs), max(ys)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def polygon_twice_area(points: tuple[tuple[int, int], ...]) -> int:
|
|
97
|
+
if len(points) < 3:
|
|
98
|
+
return 0
|
|
99
|
+
area = 0
|
|
100
|
+
for index, point in enumerate(points):
|
|
101
|
+
next_point = points[(index + 1) % len(points)]
|
|
102
|
+
area += point[0] * next_point[1] - next_point[0] * point[1]
|
|
103
|
+
return abs(area)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def polygon_flags(points: tuple[tuple[int, int], ...]) -> int:
|
|
107
|
+
flags = 0
|
|
108
|
+
if len(set(points)) < 3:
|
|
109
|
+
flags |= FLAG_FEWER_THAN_THREE_POINTS
|
|
110
|
+
if polygon_twice_area(points) == 0:
|
|
111
|
+
flags |= FLAG_ZERO_AREA
|
|
112
|
+
return flags
|
gdsdiff/grid.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from fractions import Fraction
|
|
6
|
+
from math import gcd, lcm
|
|
7
|
+
|
|
8
|
+
from .config import GridSpec
|
|
9
|
+
from .gds_metadata import GdsMetadata, decimal_from_number
|
|
10
|
+
|
|
11
|
+
INT64_LIMIT = 2**63 - 1
|
|
12
|
+
DEFAULT_COORDINATE_SAFETY_LIMIT = 2**62
|
|
13
|
+
MAX_AUTO_GRID_DENOMINATOR = 10**18
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class GridResolution:
|
|
18
|
+
grid: GridSpec
|
|
19
|
+
ratio_residual: Decimal
|
|
20
|
+
method: str
|
|
21
|
+
|
|
22
|
+
def to_json(self) -> dict[str, object]:
|
|
23
|
+
data = self.grid.to_json()
|
|
24
|
+
data["ratio_residual"] = str(self.ratio_residual)
|
|
25
|
+
data["method"] = self.method
|
|
26
|
+
return data
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _fraction_from_decimal(value: Decimal) -> Fraction:
|
|
30
|
+
return Fraction(value)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _decimal_from_fraction(value: Fraction) -> Decimal:
|
|
34
|
+
return Decimal(value.numerator) / Decimal(value.denominator)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _rational_gcd(left: Fraction, right: Fraction) -> Fraction:
|
|
38
|
+
numerator = gcd(abs(left.numerator), abs(right.numerator))
|
|
39
|
+
denominator = lcm(left.denominator, right.denominator)
|
|
40
|
+
return Fraction(numerator, denominator)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _integer_related(left: Decimal, right: Decimal) -> bool:
|
|
44
|
+
larger = max(left, right)
|
|
45
|
+
smaller = min(left, right)
|
|
46
|
+
ratio = larger / smaller
|
|
47
|
+
return ratio == ratio.to_integral_value()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_comparison_grid(
|
|
51
|
+
old: GdsMetadata,
|
|
52
|
+
new: GdsMetadata,
|
|
53
|
+
*,
|
|
54
|
+
dbu: Decimal | str | float | None = None,
|
|
55
|
+
precision: Decimal | str | float | None = None,
|
|
56
|
+
max_denominator: int = MAX_AUTO_GRID_DENOMINATOR,
|
|
57
|
+
) -> GridResolution:
|
|
58
|
+
explicit_dbu = decimal_from_number(dbu) if dbu is not None else None
|
|
59
|
+
explicit_precision = decimal_from_number(precision) if precision is not None else None
|
|
60
|
+
if explicit_dbu is not None and explicit_precision is not None and explicit_dbu != explicit_precision:
|
|
61
|
+
raise ValueError("explicit dbu and precision must match")
|
|
62
|
+
explicit = explicit_dbu if explicit_dbu is not None else explicit_precision
|
|
63
|
+
if explicit is not None:
|
|
64
|
+
_validate_positive_decimal(explicit, "comparison grid")
|
|
65
|
+
return GridResolution(
|
|
66
|
+
grid=GridSpec(
|
|
67
|
+
meters_per_dbu=explicit,
|
|
68
|
+
source_old_precision_m=old.precision_m,
|
|
69
|
+
source_new_precision_m=new.precision_m,
|
|
70
|
+
selection_mode="explicit",
|
|
71
|
+
),
|
|
72
|
+
ratio_residual=Decimal(0),
|
|
73
|
+
method="explicit",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
old_precision = old.precision_m
|
|
77
|
+
new_precision = new.precision_m
|
|
78
|
+
_validate_positive_decimal(old_precision, "old precision")
|
|
79
|
+
_validate_positive_decimal(new_precision, "new precision")
|
|
80
|
+
|
|
81
|
+
if old_precision == new_precision:
|
|
82
|
+
selected = old_precision
|
|
83
|
+
method = "equal-source-precision"
|
|
84
|
+
elif _integer_related(old_precision, new_precision):
|
|
85
|
+
selected = min(old_precision, new_precision)
|
|
86
|
+
method = "integer-related-source-precision"
|
|
87
|
+
else:
|
|
88
|
+
old_fraction = _fraction_from_decimal(old_precision)
|
|
89
|
+
new_fraction = _fraction_from_decimal(new_precision)
|
|
90
|
+
selected_fraction = _rational_gcd(old_fraction, new_fraction)
|
|
91
|
+
if selected_fraction.denominator > max_denominator:
|
|
92
|
+
raise ValueError("automatic grid denominator exceeds limit; pass explicit dbu")
|
|
93
|
+
selected = _decimal_from_fraction(selected_fraction)
|
|
94
|
+
method = "rational-common-subgrid"
|
|
95
|
+
|
|
96
|
+
return GridResolution(
|
|
97
|
+
grid=GridSpec(
|
|
98
|
+
meters_per_dbu=selected,
|
|
99
|
+
source_old_precision_m=old_precision,
|
|
100
|
+
source_new_precision_m=new_precision,
|
|
101
|
+
selection_mode="auto",
|
|
102
|
+
),
|
|
103
|
+
ratio_residual=Decimal(0),
|
|
104
|
+
method=method,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def snap_to_grid(value_m: Decimal | str | float | int, meters_per_dbu: Decimal | str | float) -> int:
|
|
109
|
+
value = decimal_from_number(value_m)
|
|
110
|
+
q = decimal_from_number(meters_per_dbu)
|
|
111
|
+
_validate_positive_decimal(q, "meters_per_dbu")
|
|
112
|
+
scaled = value / q
|
|
113
|
+
sign = -1 if scaled < 0 else 1
|
|
114
|
+
magnitude = abs(scaled)
|
|
115
|
+
whole = int(magnitude)
|
|
116
|
+
fraction = magnitude - whole
|
|
117
|
+
if fraction >= Decimal("0.5"):
|
|
118
|
+
whole += 1
|
|
119
|
+
snapped = sign * whole
|
|
120
|
+
validate_int64_coordinate(snapped)
|
|
121
|
+
return snapped
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def max_snap_error_dbu(values_m: list[Decimal], meters_per_dbu: Decimal) -> Decimal:
|
|
125
|
+
if not values_m:
|
|
126
|
+
return Decimal(0)
|
|
127
|
+
errors = []
|
|
128
|
+
for value in values_m:
|
|
129
|
+
snapped = Decimal(snap_to_grid(value, meters_per_dbu))
|
|
130
|
+
errors.append(abs(value / meters_per_dbu - snapped))
|
|
131
|
+
return max(errors)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_int64_coordinate(value: int, *, safety_limit: int = DEFAULT_COORDINATE_SAFETY_LIMIT) -> None:
|
|
135
|
+
if abs(value) > INT64_LIMIT:
|
|
136
|
+
raise OverflowError("coordinate exceeds signed int64 range")
|
|
137
|
+
if abs(value) > safety_limit:
|
|
138
|
+
raise OverflowError("coordinate exceeds configured safety limit")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _validate_positive_decimal(value: Decimal, name: str) -> None:
|
|
142
|
+
if not value.is_finite() or value <= 0:
|
|
143
|
+
raise ValueError(f"{name} must be finite and positive")
|