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
|
@@ -0,0 +1,1643 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, Iterable, Sequence
|
|
11
|
+
|
|
12
|
+
import gdstk
|
|
13
|
+
|
|
14
|
+
from .api import compare_gds_files
|
|
15
|
+
from .config import CompareConfig, LayerSpec
|
|
16
|
+
from .exact_oracle import run_full_exact_oracle
|
|
17
|
+
from .extract import extract_gds_polygons
|
|
18
|
+
from .native import discover_native_capabilities
|
|
19
|
+
|
|
20
|
+
LayerTuple = tuple[int, int]
|
|
21
|
+
BBox = tuple[float, float, float, float]
|
|
22
|
+
|
|
23
|
+
_UNIT = 1e-6
|
|
24
|
+
_PRECISION = 1e-9
|
|
25
|
+
_DBU_PER_UM = int(round(_UNIT / _PRECISION))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ScaleProfile:
|
|
30
|
+
name: str
|
|
31
|
+
sparse_grid_n: int
|
|
32
|
+
mixed_count: int
|
|
33
|
+
hollow_grid_n: int
|
|
34
|
+
hierarchy_cols: int
|
|
35
|
+
hierarchy_rows: int
|
|
36
|
+
reference_cols: int
|
|
37
|
+
reference_rows: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
SCALE_PROFILES: dict[str, ScaleProfile] = {
|
|
41
|
+
"smoke": ScaleProfile(
|
|
42
|
+
name="smoke",
|
|
43
|
+
sparse_grid_n=6,
|
|
44
|
+
mixed_count=24,
|
|
45
|
+
hollow_grid_n=4,
|
|
46
|
+
hierarchy_cols=4,
|
|
47
|
+
hierarchy_rows=3,
|
|
48
|
+
reference_cols=5,
|
|
49
|
+
reference_rows=4,
|
|
50
|
+
),
|
|
51
|
+
"benchmark": ScaleProfile(
|
|
52
|
+
name="benchmark",
|
|
53
|
+
sparse_grid_n=110,
|
|
54
|
+
mixed_count=900,
|
|
55
|
+
hollow_grid_n=28,
|
|
56
|
+
hierarchy_cols=60,
|
|
57
|
+
hierarchy_rows=24,
|
|
58
|
+
reference_cols=80,
|
|
59
|
+
reference_rows=80,
|
|
60
|
+
),
|
|
61
|
+
"stress": ScaleProfile(
|
|
62
|
+
name="stress",
|
|
63
|
+
sparse_grid_n=300,
|
|
64
|
+
mixed_count=5000,
|
|
65
|
+
hollow_grid_n=80,
|
|
66
|
+
hierarchy_cols=180,
|
|
67
|
+
hierarchy_rows=80,
|
|
68
|
+
reference_cols=250,
|
|
69
|
+
reference_rows=200,
|
|
70
|
+
),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class SyntheticEdit:
|
|
76
|
+
operation_id: str
|
|
77
|
+
shape_id: str
|
|
78
|
+
operation: str
|
|
79
|
+
expected_direction: str
|
|
80
|
+
before_layer: LayerTuple | None = None
|
|
81
|
+
after_layer: LayerTuple | None = None
|
|
82
|
+
before_bbox_um: BBox | None = None
|
|
83
|
+
after_bbox_um: BBox | None = None
|
|
84
|
+
notes: str = ""
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def coverage_changed(self) -> bool:
|
|
88
|
+
return self.expected_direction != "inventory_only"
|
|
89
|
+
|
|
90
|
+
def changed_layers(self) -> tuple[LayerTuple, ...]:
|
|
91
|
+
if not self.coverage_changed:
|
|
92
|
+
return ()
|
|
93
|
+
layers: list[LayerTuple] = []
|
|
94
|
+
if self.before_layer is not None:
|
|
95
|
+
layers.append(self.before_layer)
|
|
96
|
+
if self.after_layer is not None and self.after_layer not in layers:
|
|
97
|
+
layers.append(self.after_layer)
|
|
98
|
+
return tuple(layers)
|
|
99
|
+
|
|
100
|
+
def to_json(self) -> dict[str, object]:
|
|
101
|
+
return {
|
|
102
|
+
"operation_id": self.operation_id,
|
|
103
|
+
"shape_id": self.shape_id,
|
|
104
|
+
"operation": self.operation,
|
|
105
|
+
"expected_direction": self.expected_direction,
|
|
106
|
+
"before_layer": list(self.before_layer) if self.before_layer is not None else None,
|
|
107
|
+
"after_layer": list(self.after_layer) if self.after_layer is not None else None,
|
|
108
|
+
"before_bbox_um": list(self.before_bbox_um) if self.before_bbox_um is not None else None,
|
|
109
|
+
"after_bbox_um": list(self.after_bbox_um) if self.after_bbox_um is not None else None,
|
|
110
|
+
"notes": self.notes,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class SyntheticCaseBuild:
|
|
116
|
+
before: gdstk.Library
|
|
117
|
+
after: gdstk.Library
|
|
118
|
+
edits: tuple[SyntheticEdit, ...]
|
|
119
|
+
equivalent_expected: bool = False
|
|
120
|
+
notes: str = ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class SyntheticCaseDefinition:
|
|
125
|
+
name: str
|
|
126
|
+
title: str
|
|
127
|
+
builder: Callable[[ScaleProfile], SyntheticCaseBuild]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class SyntheticCaseRecord:
|
|
132
|
+
name: str
|
|
133
|
+
title: str
|
|
134
|
+
scale: str
|
|
135
|
+
before_path: Path
|
|
136
|
+
after_path: Path
|
|
137
|
+
manifest_path: Path
|
|
138
|
+
changes_path: Path
|
|
139
|
+
before_polygon_count: int
|
|
140
|
+
after_polygon_count: int
|
|
141
|
+
before_vertex_count: int
|
|
142
|
+
after_vertex_count: int
|
|
143
|
+
expected_changed_layers: tuple[LayerTuple, ...]
|
|
144
|
+
equivalent_expected: bool
|
|
145
|
+
edits: tuple[SyntheticEdit, ...]
|
|
146
|
+
|
|
147
|
+
def to_json(self) -> dict[str, object]:
|
|
148
|
+
return {
|
|
149
|
+
"name": self.name,
|
|
150
|
+
"title": self.title,
|
|
151
|
+
"scale": self.scale,
|
|
152
|
+
"paths": {
|
|
153
|
+
"before_gds": str(self.before_path),
|
|
154
|
+
"after_gds": str(self.after_path),
|
|
155
|
+
"manifest": str(self.manifest_path),
|
|
156
|
+
"changes": str(self.changes_path),
|
|
157
|
+
},
|
|
158
|
+
"counts": {
|
|
159
|
+
"before_polygons": self.before_polygon_count,
|
|
160
|
+
"after_polygons": self.after_polygon_count,
|
|
161
|
+
"before_vertices": self.before_vertex_count,
|
|
162
|
+
"after_vertices": self.after_vertex_count,
|
|
163
|
+
},
|
|
164
|
+
"expected_changed_layers": [list(layer) for layer in self.expected_changed_layers],
|
|
165
|
+
"equivalent_expected": self.equivalent_expected,
|
|
166
|
+
"edits": [edit.to_json() for edit in self.edits],
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class SyntheticBenchmarkRun:
|
|
172
|
+
case_name: str
|
|
173
|
+
scale: str
|
|
174
|
+
mode: str
|
|
175
|
+
status: str
|
|
176
|
+
exit_code: int
|
|
177
|
+
wall_s: float
|
|
178
|
+
total_s: float | None
|
|
179
|
+
changed_layers: tuple[LayerTuple, ...]
|
|
180
|
+
audit_match: bool | None
|
|
181
|
+
json_report_path: Path
|
|
182
|
+
exact_diff_markdown_path: Path
|
|
183
|
+
stdout_path: Path
|
|
184
|
+
stderr_path: Path
|
|
185
|
+
command_path: Path
|
|
186
|
+
error: str | None = None
|
|
187
|
+
|
|
188
|
+
def to_json(self) -> dict[str, object]:
|
|
189
|
+
return {
|
|
190
|
+
"case_name": self.case_name,
|
|
191
|
+
"scale": self.scale,
|
|
192
|
+
"mode": self.mode,
|
|
193
|
+
"status": self.status,
|
|
194
|
+
"exit_code": self.exit_code,
|
|
195
|
+
"wall_s": self.wall_s,
|
|
196
|
+
"total_s": self.total_s,
|
|
197
|
+
"changed_layers": [list(layer) for layer in self.changed_layers],
|
|
198
|
+
"audit_match": self.audit_match,
|
|
199
|
+
"artifacts": {
|
|
200
|
+
"json_report": str(self.json_report_path),
|
|
201
|
+
"exact_diff_markdown": str(self.exact_diff_markdown_path),
|
|
202
|
+
"stdout": str(self.stdout_path),
|
|
203
|
+
"stderr": str(self.stderr_path),
|
|
204
|
+
"command": str(self.command_path),
|
|
205
|
+
},
|
|
206
|
+
"error": self.error,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def build_synthetic_suite(
|
|
211
|
+
output_dir: str | Path,
|
|
212
|
+
*,
|
|
213
|
+
scale: str = "benchmark",
|
|
214
|
+
case_names: Iterable[str] | None = None,
|
|
215
|
+
) -> tuple[SyntheticCaseRecord, ...]:
|
|
216
|
+
profile = _profile(scale)
|
|
217
|
+
selected = set(case_names) if case_names is not None else None
|
|
218
|
+
output_dir = Path(output_dir)
|
|
219
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
records: list[SyntheticCaseRecord] = []
|
|
221
|
+
for definition in SYNTHETIC_CASES:
|
|
222
|
+
if selected is not None and definition.name not in selected:
|
|
223
|
+
continue
|
|
224
|
+
records.append(_write_case(output_dir, definition, profile))
|
|
225
|
+
suite_manifest = {
|
|
226
|
+
"suite": "gdsdiff-synthetic-readiness-corpus",
|
|
227
|
+
"scale": profile.name,
|
|
228
|
+
"case_count": len(records),
|
|
229
|
+
"cases": [record.to_json() for record in records],
|
|
230
|
+
}
|
|
231
|
+
(output_dir / "suite_manifest.json").write_text(_json_dumps(suite_manifest), encoding="utf-8")
|
|
232
|
+
return tuple(records)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def benchmark_synthetic_records(
|
|
236
|
+
records: Sequence[SyntheticCaseRecord],
|
|
237
|
+
*,
|
|
238
|
+
run_cpu: bool = True,
|
|
239
|
+
run_metal: bool = True,
|
|
240
|
+
run_metal_audit: bool = True,
|
|
241
|
+
stress_cpu_case_limit: int = 2,
|
|
242
|
+
) -> tuple[SyntheticBenchmarkRun, ...]:
|
|
243
|
+
rows: list[SyntheticBenchmarkRun] = []
|
|
244
|
+
stress_cpu_count = 0
|
|
245
|
+
for record in records:
|
|
246
|
+
allow_cpu = run_cpu
|
|
247
|
+
allow_metal_audit = run_metal_audit
|
|
248
|
+
if record.scale == "stress":
|
|
249
|
+
allow_cpu = run_cpu and stress_cpu_count < stress_cpu_case_limit
|
|
250
|
+
allow_metal_audit = run_metal_audit and stress_cpu_count < stress_cpu_case_limit
|
|
251
|
+
if allow_cpu:
|
|
252
|
+
stress_cpu_count += 1
|
|
253
|
+
if allow_cpu:
|
|
254
|
+
rows.append(_benchmark_one(record, mode="cpu"))
|
|
255
|
+
if run_metal:
|
|
256
|
+
rows.append(_benchmark_one(record, mode="metal"))
|
|
257
|
+
if run_metal and allow_metal_audit:
|
|
258
|
+
rows.append(_benchmark_one(record, mode="metal_audit"))
|
|
259
|
+
return tuple(rows)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def write_benchmark_summary(
|
|
263
|
+
output_dir: str | Path,
|
|
264
|
+
records: Sequence[SyntheticCaseRecord],
|
|
265
|
+
runs: Sequence[SyntheticBenchmarkRun],
|
|
266
|
+
) -> Path:
|
|
267
|
+
output_dir = Path(output_dir)
|
|
268
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
run_json_path = output_dir / "benchmark_runs.json"
|
|
270
|
+
run_json_path.write_text(_json_dumps([run.to_json() for run in runs]), encoding="utf-8")
|
|
271
|
+
summary_path = output_dir / "benchmark_summary.md"
|
|
272
|
+
summary_path.write_text(_benchmark_markdown(records, runs, run_json_path), encoding="utf-8")
|
|
273
|
+
return summary_path
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _write_case(output_dir: Path, definition: SyntheticCaseDefinition, profile: ScaleProfile) -> SyntheticCaseRecord:
|
|
277
|
+
case_dir = output_dir / definition.name
|
|
278
|
+
case_dir.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
built = definition.builder(profile)
|
|
280
|
+
before_path = case_dir / "before.gds"
|
|
281
|
+
after_path = case_dir / "after.gds"
|
|
282
|
+
built.before.write_gds(before_path)
|
|
283
|
+
built.after.write_gds(after_path)
|
|
284
|
+
before_stats = _library_stats(built.before)
|
|
285
|
+
after_stats = _library_stats(built.after)
|
|
286
|
+
expected_changed_layers = _expected_changed_layers(built.edits)
|
|
287
|
+
manifest_path = case_dir / "manifest.json"
|
|
288
|
+
changes_path = case_dir / "changes.md"
|
|
289
|
+
record = SyntheticCaseRecord(
|
|
290
|
+
name=definition.name,
|
|
291
|
+
title=definition.title,
|
|
292
|
+
scale=profile.name,
|
|
293
|
+
before_path=before_path,
|
|
294
|
+
after_path=after_path,
|
|
295
|
+
manifest_path=manifest_path,
|
|
296
|
+
changes_path=changes_path,
|
|
297
|
+
before_polygon_count=before_stats["polygon_count"],
|
|
298
|
+
after_polygon_count=after_stats["polygon_count"],
|
|
299
|
+
before_vertex_count=before_stats["vertex_count"],
|
|
300
|
+
after_vertex_count=after_stats["vertex_count"],
|
|
301
|
+
expected_changed_layers=expected_changed_layers,
|
|
302
|
+
equivalent_expected=built.equivalent_expected,
|
|
303
|
+
edits=built.edits,
|
|
304
|
+
)
|
|
305
|
+
manifest = record.to_json() | {
|
|
306
|
+
"unit_m": _UNIT,
|
|
307
|
+
"precision_m": _PRECISION,
|
|
308
|
+
"dbu_per_um": _DBU_PER_UM,
|
|
309
|
+
"notes": built.notes,
|
|
310
|
+
}
|
|
311
|
+
manifest_path.write_text(_json_dumps(manifest), encoding="utf-8")
|
|
312
|
+
changes_path.write_text(_changes_markdown(record, built.notes), encoding="utf-8")
|
|
313
|
+
return record
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _benchmark_one(record: SyntheticCaseRecord, *, mode: str) -> SyntheticBenchmarkRun:
|
|
317
|
+
case_dir = record.before_path.parent
|
|
318
|
+
artifact_dir = case_dir / "gdsdiff" / mode
|
|
319
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
320
|
+
json_report_path = artifact_dir / "report.json"
|
|
321
|
+
exact_diff_markdown_path = artifact_dir / "exact_diff.md"
|
|
322
|
+
stdout_path = artifact_dir / "stdout.txt"
|
|
323
|
+
stderr_path = artifact_dir / "stderr.txt"
|
|
324
|
+
command_path = artifact_dir / "command.json"
|
|
325
|
+
if mode == "cpu":
|
|
326
|
+
config = CompareConfig(
|
|
327
|
+
record.before_path,
|
|
328
|
+
record.after_path,
|
|
329
|
+
backend="cpu",
|
|
330
|
+
evidence_engine="gdstk-oracle",
|
|
331
|
+
)
|
|
332
|
+
audit = False
|
|
333
|
+
elif mode == "metal":
|
|
334
|
+
config = CompareConfig(
|
|
335
|
+
record.before_path,
|
|
336
|
+
record.after_path,
|
|
337
|
+
backend="metal",
|
|
338
|
+
evidence_engine="gpu-polygon",
|
|
339
|
+
)
|
|
340
|
+
audit = False
|
|
341
|
+
elif mode == "metal_audit":
|
|
342
|
+
config = CompareConfig(
|
|
343
|
+
record.before_path,
|
|
344
|
+
record.after_path,
|
|
345
|
+
backend="metal",
|
|
346
|
+
evidence_engine="gpu-polygon",
|
|
347
|
+
)
|
|
348
|
+
audit = True
|
|
349
|
+
else:
|
|
350
|
+
raise ValueError(f"unknown benchmark mode: {mode!r}")
|
|
351
|
+
|
|
352
|
+
command_payload = {
|
|
353
|
+
"mode": mode,
|
|
354
|
+
"config": config.to_json_dict(),
|
|
355
|
+
"audit_cpu_oracle": audit,
|
|
356
|
+
"json_report_path": str(json_report_path),
|
|
357
|
+
"exact_diff_markdown_path": str(exact_diff_markdown_path),
|
|
358
|
+
"synthetic_case": record.name,
|
|
359
|
+
"synthetic_scale": record.scale,
|
|
360
|
+
}
|
|
361
|
+
start = time.perf_counter()
|
|
362
|
+
try:
|
|
363
|
+
result = compare_gds_files(
|
|
364
|
+
config,
|
|
365
|
+
json_report_path=json_report_path,
|
|
366
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
367
|
+
audit_cpu_oracle=audit,
|
|
368
|
+
command=("gdsdiff.synthetic_suite", "benchmark", record.scale, record.name, mode),
|
|
369
|
+
)
|
|
370
|
+
wall_s = time.perf_counter() - start
|
|
371
|
+
payload = result.report.to_json_dict()
|
|
372
|
+
stdout_path.write_text(
|
|
373
|
+
"\n".join(
|
|
374
|
+
[
|
|
375
|
+
f"status={payload.get('status')}",
|
|
376
|
+
f"evidence_status={payload.get('evidence_status')}",
|
|
377
|
+
f"differences_found={payload.get('differences_found')}",
|
|
378
|
+
]
|
|
379
|
+
)
|
|
380
|
+
+ "\n",
|
|
381
|
+
encoding="utf-8",
|
|
382
|
+
)
|
|
383
|
+
stderr_path.write_text("\n".join(str(item) for item in payload.get("warnings", ())) + "\n", encoding="utf-8")
|
|
384
|
+
row = SyntheticBenchmarkRun(
|
|
385
|
+
case_name=record.name,
|
|
386
|
+
scale=record.scale,
|
|
387
|
+
mode=mode,
|
|
388
|
+
status=str(payload.get("status")),
|
|
389
|
+
exit_code=result.exit_code,
|
|
390
|
+
wall_s=wall_s,
|
|
391
|
+
total_s=_float_or_none(payload.get("timings", {}).get("total_s") if isinstance(payload.get("timings"), dict) else None),
|
|
392
|
+
changed_layers=_changed_layers_from_report(payload),
|
|
393
|
+
audit_match=_audit_match_from_report(payload),
|
|
394
|
+
json_report_path=json_report_path,
|
|
395
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
396
|
+
stdout_path=stdout_path,
|
|
397
|
+
stderr_path=stderr_path,
|
|
398
|
+
command_path=command_path,
|
|
399
|
+
)
|
|
400
|
+
except Exception as exc:
|
|
401
|
+
wall_s = time.perf_counter() - start
|
|
402
|
+
stdout_path.write_text("", encoding="utf-8")
|
|
403
|
+
stderr_path.write_text(f"{type(exc).__name__}: {exc}\n", encoding="utf-8")
|
|
404
|
+
row = SyntheticBenchmarkRun(
|
|
405
|
+
case_name=record.name,
|
|
406
|
+
scale=record.scale,
|
|
407
|
+
mode=mode,
|
|
408
|
+
status="error",
|
|
409
|
+
exit_code=2,
|
|
410
|
+
wall_s=wall_s,
|
|
411
|
+
total_s=None,
|
|
412
|
+
changed_layers=(),
|
|
413
|
+
audit_match=None,
|
|
414
|
+
json_report_path=json_report_path,
|
|
415
|
+
exact_diff_markdown_path=exact_diff_markdown_path,
|
|
416
|
+
stdout_path=stdout_path,
|
|
417
|
+
stderr_path=stderr_path,
|
|
418
|
+
command_path=command_path,
|
|
419
|
+
error=f"{type(exc).__name__}: {exc}",
|
|
420
|
+
)
|
|
421
|
+
command_path.write_text(_json_dumps(command_payload | {"result": row.to_json()}), encoding="utf-8")
|
|
422
|
+
return row
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _benchmark_markdown(
|
|
426
|
+
records: Sequence[SyntheticCaseRecord],
|
|
427
|
+
runs: Sequence[SyntheticBenchmarkRun],
|
|
428
|
+
run_json_path: Path,
|
|
429
|
+
) -> str:
|
|
430
|
+
by_case_mode = {(run.scale, run.case_name, run.mode): run for run in runs}
|
|
431
|
+
lines = [
|
|
432
|
+
"# GDSDiff Synthetic Readiness Benchmark",
|
|
433
|
+
"",
|
|
434
|
+
f"- Host capabilities: `{json.dumps(discover_native_capabilities().to_json(), sort_keys=True)}`",
|
|
435
|
+
f"- Run JSON: `{run_json_path}`",
|
|
436
|
+
"",
|
|
437
|
+
"| Scale | Case | Before polys | After polys | Changed layers | CPU wall s | Metal wall s | Speedup | Metal audit | Artifacts |",
|
|
438
|
+
"|---|---:|---:|---:|---|---:|---:|---:|---|---|",
|
|
439
|
+
]
|
|
440
|
+
for record in records:
|
|
441
|
+
cpu = by_case_mode.get((record.scale, record.name, "cpu"))
|
|
442
|
+
metal = by_case_mode.get((record.scale, record.name, "metal"))
|
|
443
|
+
metal_audit = by_case_mode.get((record.scale, record.name, "metal_audit"))
|
|
444
|
+
cpu_wall = cpu.wall_s if cpu is not None and cpu.error is None else None
|
|
445
|
+
metal_wall = metal.wall_s if metal is not None and metal.error is None else None
|
|
446
|
+
speedup = (cpu_wall / metal_wall) if cpu_wall and metal_wall and metal_wall > 0 else None
|
|
447
|
+
layers = _format_layers(record.expected_changed_layers)
|
|
448
|
+
audit = (
|
|
449
|
+
"match"
|
|
450
|
+
if metal_audit is not None and metal_audit.audit_match is True
|
|
451
|
+
else "mismatch"
|
|
452
|
+
if metal_audit is not None and metal_audit.audit_match is False
|
|
453
|
+
else "not-run"
|
|
454
|
+
if metal_audit is None
|
|
455
|
+
else metal_audit.status
|
|
456
|
+
)
|
|
457
|
+
artifact_bits = []
|
|
458
|
+
if cpu is not None:
|
|
459
|
+
artifact_bits.append(f"cpu `{cpu.json_report_path}`")
|
|
460
|
+
if metal is not None:
|
|
461
|
+
artifact_bits.append(f"metal `{metal.json_report_path}`")
|
|
462
|
+
if metal_audit is not None:
|
|
463
|
+
artifact_bits.append(f"audit `{metal_audit.json_report_path}`")
|
|
464
|
+
lines.append(
|
|
465
|
+
"| {scale} | `{case}` | {before} | {after} | {layers} | {cpu_wall} | {metal_wall} | {speedup} | {audit} | {artifacts} |".format(
|
|
466
|
+
scale=record.scale,
|
|
467
|
+
case=record.name,
|
|
468
|
+
before=record.before_polygon_count,
|
|
469
|
+
after=record.after_polygon_count,
|
|
470
|
+
layers=layers or "none",
|
|
471
|
+
cpu_wall=_format_seconds(cpu_wall),
|
|
472
|
+
metal_wall=_format_seconds(metal_wall),
|
|
473
|
+
speedup=f"{speedup:.2f}x" if speedup is not None else "n/a",
|
|
474
|
+
audit=audit,
|
|
475
|
+
artifacts="<br>".join(artifact_bits) if artifact_bits else "none",
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
lines.append("")
|
|
479
|
+
return "\n".join(lines)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _changes_markdown(record: SyntheticCaseRecord, notes: str) -> str:
|
|
483
|
+
lines = [
|
|
484
|
+
f"# {record.title}",
|
|
485
|
+
"",
|
|
486
|
+
f"- Case: `{record.name}`",
|
|
487
|
+
f"- Scale: `{record.scale}`",
|
|
488
|
+
f"- Before GDS: `{record.before_path}`",
|
|
489
|
+
f"- After GDS: `{record.after_path}`",
|
|
490
|
+
f"- Expected equivalent coverage: `{str(record.equivalent_expected).lower()}`",
|
|
491
|
+
f"- Expected changed layers: `{_format_layers(record.expected_changed_layers) or 'none'}`",
|
|
492
|
+
f"- Before polygons/vertices: `{record.before_polygon_count}` / `{record.before_vertex_count}`",
|
|
493
|
+
f"- After polygons/vertices: `{record.after_polygon_count}` / `{record.after_vertex_count}`",
|
|
494
|
+
]
|
|
495
|
+
if notes:
|
|
496
|
+
lines.extend(["", "## Notes", "", notes])
|
|
497
|
+
lines.extend(["", "## Intended Changes", ""])
|
|
498
|
+
for edit in record.edits:
|
|
499
|
+
lines.extend(
|
|
500
|
+
[
|
|
501
|
+
f"### {edit.operation_id}",
|
|
502
|
+
"",
|
|
503
|
+
f"- shape_id: `{edit.shape_id}`",
|
|
504
|
+
f"- operation: `{edit.operation}`",
|
|
505
|
+
f"- expected_direction: `{edit.expected_direction}`",
|
|
506
|
+
f"- before_layer: `{_format_layer(edit.before_layer) if edit.before_layer else 'none'}`",
|
|
507
|
+
f"- after_layer: `{_format_layer(edit.after_layer) if edit.after_layer else 'none'}`",
|
|
508
|
+
f"- before_bbox_um: `{edit.before_bbox_um}`",
|
|
509
|
+
f"- after_bbox_um: `{edit.after_bbox_um}`",
|
|
510
|
+
f"- notes: {edit.notes}",
|
|
511
|
+
"",
|
|
512
|
+
]
|
|
513
|
+
)
|
|
514
|
+
return "\n".join(lines)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _case_rect_add_remove_shift(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
518
|
+
layer = (1, 0)
|
|
519
|
+
old, old_top = _library()
|
|
520
|
+
new, new_top = _library()
|
|
521
|
+
stable = _rect(0, 0, 10, 10, layer)
|
|
522
|
+
old_removed = _rect(20, 0, 30, 10, layer)
|
|
523
|
+
old_shift = _rect(40, 0, 50, 10, layer)
|
|
524
|
+
new_shift = _rect(43, 0, 53, 10, layer)
|
|
525
|
+
added = _rect(60, 0, 70, 10, layer)
|
|
526
|
+
_add(old_top, stable, old_removed, old_shift)
|
|
527
|
+
_add(new_top, _rect(0, 0, 10, 10, layer), new_shift, added)
|
|
528
|
+
edits = (
|
|
529
|
+
SyntheticEdit("rect_remove_001", "removed_rect", "remove", "coverage_before_only", before_layer=layer, before_bbox_um=_bbox(old_removed)),
|
|
530
|
+
SyntheticEdit("rect_shift_001", "shifted_rect", "shift", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_shift), after_bbox_um=_bbox(new_shift)),
|
|
531
|
+
SyntheticEdit("rect_add_001", "added_rect", "add", "coverage_after_only", after_layer=layer, after_bbox_um=_bbox(added)),
|
|
532
|
+
)
|
|
533
|
+
return SyntheticCaseBuild(old, new, edits, notes="Simple axis-aligned add, remove, and shift operations.")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _case_overlap_rect_unions(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
537
|
+
layer = (2, 0)
|
|
538
|
+
old, old_top = _library()
|
|
539
|
+
new, new_top = _library()
|
|
540
|
+
old_a = _rect(0, 0, 20, 20, layer)
|
|
541
|
+
old_b = _rect(10, 0, 30, 20, layer)
|
|
542
|
+
new_a = _rect(0, 0, 20, 20, layer)
|
|
543
|
+
new_b = _rect(15, 0, 35, 20, layer)
|
|
544
|
+
new_c = _rect(5, 10, 25, 30, layer)
|
|
545
|
+
_add(old_top, old_a, old_b)
|
|
546
|
+
_add(new_top, new_a, new_b, new_c)
|
|
547
|
+
edits = (
|
|
548
|
+
SyntheticEdit("overlap_shift_001", "overlap_rect_b", "shift_overlapping_rect", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_b), after_bbox_um=_bbox(new_b), notes="The shifted rectangle overlaps a stable neighbor."),
|
|
549
|
+
SyntheticEdit("overlap_add_001", "overlap_rect_c", "add_overlapping_rect", "coverage_after_only", after_layer=layer, after_bbox_um=_bbox(new_c)),
|
|
550
|
+
)
|
|
551
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _case_circle_breakpoints(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
555
|
+
layer = (3, 0)
|
|
556
|
+
old, old_top = _library()
|
|
557
|
+
new, new_top = _library()
|
|
558
|
+
old_circle = _ellipse_polygon(0, 0, 10, 10, 32, layer)
|
|
559
|
+
new_circle = _ellipse_polygon(0, 0, 10, 10, 48, layer)
|
|
560
|
+
old_ellipse = _ellipse_polygon(35, 0, 13, 8, 40, layer)
|
|
561
|
+
new_ellipse = _ellipse_polygon(35, 0, 10.5, 8, 40, layer)
|
|
562
|
+
_add(old_top, old_circle, old_ellipse)
|
|
563
|
+
_add(new_top, new_circle, new_ellipse)
|
|
564
|
+
edits = (
|
|
565
|
+
SyntheticEdit("circle_breakpoint_001", "circle_same_radius", "change_curve_breakpoints", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_circle), after_bbox_um=_bbox(new_circle), notes="Same radius with different polygonal approximation."),
|
|
566
|
+
SyntheticEdit("ellipse_thin_001", "ellipse_thinned", "thin_curve", "coverage_before_only", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_ellipse), after_bbox_um=_bbox(new_ellipse)),
|
|
567
|
+
)
|
|
568
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _case_star_concave(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
572
|
+
layer = (4, 0)
|
|
573
|
+
old, old_top = _library()
|
|
574
|
+
new, new_top = _library()
|
|
575
|
+
old_star = _star_polygon(0, 0, 12, 5, 5, 0.0, layer)
|
|
576
|
+
new_star = _star_polygon(0, 0, 12, 5, 5, 0.2, layer)
|
|
577
|
+
old_removed = _star_polygon(35, 0, 10, 4, 7, 0.1, layer)
|
|
578
|
+
new_added = _star_polygon(70, 0, 11, 4, 6, -0.3, layer)
|
|
579
|
+
_add(old_top, old_star, old_removed)
|
|
580
|
+
_add(new_top, new_star, new_added)
|
|
581
|
+
edits = (
|
|
582
|
+
SyntheticEdit("star_rotate_001", "five_point_star", "rotate_concave_polygon", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_star), after_bbox_um=_bbox(new_star)),
|
|
583
|
+
SyntheticEdit("star_remove_001", "seven_point_star", "remove_concave_polygon", "coverage_before_only", before_layer=layer, before_bbox_um=_bbox(old_removed)),
|
|
584
|
+
SyntheticEdit("star_add_001", "six_point_star", "add_concave_polygon", "coverage_after_only", after_layer=layer, after_bbox_um=_bbox(new_added)),
|
|
585
|
+
)
|
|
586
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _case_hollow_rect_circle(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
590
|
+
layer = (5, 0)
|
|
591
|
+
old, old_top = _library()
|
|
592
|
+
new, new_top = _library()
|
|
593
|
+
old_ring = _rect_ring(0, 0, 34, 34, 9, 9, 25, 25, layer)
|
|
594
|
+
new_ring = _rect_ring(0, 0, 34, 34, 7, 7, 27, 27, layer)
|
|
595
|
+
old_donut = _ellipse_ring(70, 17, 16, 16, 7, 7, 48, layer)
|
|
596
|
+
new_donut = _ellipse_ring(70, 17, 16, 16, 9, 5, 64, layer)
|
|
597
|
+
_add(old_top, *old_ring, *old_donut)
|
|
598
|
+
_add(new_top, *new_ring, *new_donut)
|
|
599
|
+
edits = (
|
|
600
|
+
SyntheticEdit("hollow_rect_001", "rectangular_hole", "resize_inner_hole", "coverage_before_only", before_layer=layer, after_layer=layer, before_bbox_um=(7, 7, 27, 27), after_bbox_um=(7, 7, 27, 27)),
|
|
601
|
+
SyntheticEdit("hollow_circle_001", "circular_hole", "resize_and_refracture_inner_hole", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=(63, 10, 77, 24), after_bbox_um=(61, 12, 79, 22)),
|
|
602
|
+
)
|
|
603
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _case_nested_holes_bridge(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
607
|
+
layer = (6, 0)
|
|
608
|
+
old, old_top = _library()
|
|
609
|
+
new, new_top = _library()
|
|
610
|
+
outer = _rect(0, 0, 90, 42, layer)
|
|
611
|
+
holes_old = [_rect(10, 10, 22, 30, layer), _rect(38, 10, 50, 30, layer), _rect(66, 10, 78, 30, layer)]
|
|
612
|
+
holes_new = [_rect(10, 10, 22, 30, layer), _rect(66, 10, 78, 30, layer), _rect(42, 0, 46, 42, layer)]
|
|
613
|
+
old_polys = _boolean(outer, holes_old, "not", layer)
|
|
614
|
+
new_polys = _boolean(_rect(0, 0, 90, 42, layer), holes_new, "not", layer)
|
|
615
|
+
_add(old_top, *old_polys)
|
|
616
|
+
_add(new_top, *new_polys)
|
|
617
|
+
edits = (
|
|
618
|
+
SyntheticEdit("nested_fill_001", "middle_hole", "fill_inner_hole", "coverage_after_only", before_layer=layer, after_layer=layer, before_bbox_um=(38, 10, 50, 30), after_bbox_um=(38, 10, 50, 30)),
|
|
619
|
+
SyntheticEdit("nested_bridge_cut_001", "vertical_slot", "add_bridge_cut", "coverage_before_only", before_layer=layer, after_layer=layer, before_bbox_um=(42, 0, 46, 42), after_bbox_um=(42, 0, 46, 42)),
|
|
620
|
+
)
|
|
621
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _case_thin_slivers_near_collinear(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
625
|
+
layer = (7, 0)
|
|
626
|
+
old, old_top = _library()
|
|
627
|
+
new, new_top = _library()
|
|
628
|
+
old_sliver = _rect(0, 0, 100, 0.2, layer)
|
|
629
|
+
new_sliver = _rect(0, 0, 80, 0.2, layer)
|
|
630
|
+
old_poly = gdstk.Polygon([(0, 10), (50, 10.001), (100, 10), (100, 12), (0, 12)], layer=layer[0], datatype=layer[1])
|
|
631
|
+
new_poly = gdstk.Polygon([(0, 10), (40, 10.001), (100, 10.15), (100, 12), (0, 12)], layer=layer[0], datatype=layer[1])
|
|
632
|
+
added = _rect(120, 0, 160, 0.15, layer)
|
|
633
|
+
_add(old_top, old_sliver, old_poly)
|
|
634
|
+
_add(new_top, new_sliver, new_poly, added)
|
|
635
|
+
edits = (
|
|
636
|
+
SyntheticEdit("sliver_shorten_001", "thin_rectangle", "shorten_thin_polygon", "coverage_before_only", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_sliver), after_bbox_um=_bbox(new_sliver)),
|
|
637
|
+
SyntheticEdit("sliver_collinear_001", "near_collinear_polygon", "perturb_near_collinear_vertex", "coverage_before_only", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_poly), after_bbox_um=_bbox(new_poly)),
|
|
638
|
+
SyntheticEdit("sliver_add_001", "added_sliver", "add_thin_polygon", "coverage_after_only", after_layer=layer, after_bbox_um=_bbox(added)),
|
|
639
|
+
)
|
|
640
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _case_layer_datatype_moves(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
644
|
+
old_layer_a = (8, 0)
|
|
645
|
+
old_layer_b = (8, 1)
|
|
646
|
+
new_layer_a = (9, 0)
|
|
647
|
+
new_layer_b = (8, 2)
|
|
648
|
+
old, old_top = _library()
|
|
649
|
+
new, new_top = _library()
|
|
650
|
+
rect_a_old = _rect(0, 0, 12, 12, old_layer_a)
|
|
651
|
+
rect_b_old = _rect(30, 0, 42, 12, old_layer_b)
|
|
652
|
+
rect_a_new = _rect(0, 0, 12, 12, new_layer_a)
|
|
653
|
+
rect_b_new = _rect(30, 0, 42, 12, new_layer_b)
|
|
654
|
+
_add(old_top, rect_a_old, rect_b_old)
|
|
655
|
+
_add(new_top, rect_a_new, rect_b_new)
|
|
656
|
+
edits = (
|
|
657
|
+
SyntheticEdit("layer_move_001", "layer_moved_rect", "move_layer", "coverage_both", before_layer=old_layer_a, after_layer=new_layer_a, before_bbox_um=_bbox(rect_a_old), after_bbox_um=_bbox(rect_a_new)),
|
|
658
|
+
SyntheticEdit("datatype_move_001", "datatype_moved_rect", "move_datatype", "coverage_both", before_layer=old_layer_b, after_layer=new_layer_b, before_bbox_um=_bbox(rect_b_old), after_bbox_um=_bbox(rect_b_new)),
|
|
659
|
+
)
|
|
660
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _case_same_coverage_refracturing(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
664
|
+
layer = (10, 0)
|
|
665
|
+
old, old_top = _library()
|
|
666
|
+
new, new_top = _library()
|
|
667
|
+
old_rect = _rect(0, 0, 40, 20, layer)
|
|
668
|
+
left = _rect(0, 0, 20, 20, layer)
|
|
669
|
+
right = _rect(20, 0, 40, 20, layer)
|
|
670
|
+
old_top.add(old_rect)
|
|
671
|
+
_add(new_top, left, right)
|
|
672
|
+
edits = (
|
|
673
|
+
SyntheticEdit("refracture_001", "split_rectangle", "split_equivalent_coverage", "inventory_only", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_rect), after_bbox_um=_bbox(old_rect), notes="Coverage is intentionally identical; polygon inventory changes only."),
|
|
674
|
+
)
|
|
675
|
+
return SyntheticCaseBuild(old, new, edits, equivalent_expected=True)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _case_touching_edges_points(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
679
|
+
layer = (11, 0)
|
|
680
|
+
old, old_top = _library()
|
|
681
|
+
new, new_top = _library()
|
|
682
|
+
old_left = _rect(0, 0, 10, 10, layer)
|
|
683
|
+
old_right = _rect(10, 0, 20, 10, layer)
|
|
684
|
+
old_corner = _rect(30, 0, 40, 10, layer)
|
|
685
|
+
new_left = _rect(0, 0, 10, 10, layer)
|
|
686
|
+
new_right = _rect(10.2, 0, 20.2, 10, layer)
|
|
687
|
+
new_corner = _rect(39.8, 9.8, 49.8, 19.8, layer)
|
|
688
|
+
_add(old_top, old_left, old_right, old_corner)
|
|
689
|
+
_add(new_top, new_left, new_right, new_corner)
|
|
690
|
+
edits = (
|
|
691
|
+
SyntheticEdit("touch_gap_001", "edge_touching_pair", "open_small_gap", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=(10, 0, 20, 10), after_bbox_um=(10.2, 0, 20.2, 10)),
|
|
692
|
+
SyntheticEdit("touch_point_overlap_001", "corner_touching_square", "move_to_tiny_overlap", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_corner), after_bbox_um=_bbox(new_corner)),
|
|
693
|
+
)
|
|
694
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _case_cell_references_arrays(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
698
|
+
layer = (12, 0)
|
|
699
|
+
old = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
700
|
+
old_child = old.new_cell("leaf")
|
|
701
|
+
old_child.add(_rect(0, 0, 8, 8, layer))
|
|
702
|
+
old_top = old.new_cell("top")
|
|
703
|
+
old_top.add(gdstk.Reference(old_child, (0, 0), columns=profile.hierarchy_cols, rows=profile.hierarchy_rows, spacing=(16, 16)))
|
|
704
|
+
|
|
705
|
+
new = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
706
|
+
new_child = new.new_cell("leaf")
|
|
707
|
+
new_child.add(_rect(0, 0, 8, 8, layer))
|
|
708
|
+
new_shifted = new.new_cell("leaf_shifted")
|
|
709
|
+
new_shifted.add(_rect(1.5, 0, 9.5, 8, layer))
|
|
710
|
+
new_top = new.new_cell("top")
|
|
711
|
+
new_top.add(gdstk.Reference(new_child, (0, 0), columns=max(1, profile.hierarchy_cols - 1), rows=profile.hierarchy_rows, spacing=(16, 16)))
|
|
712
|
+
new_top.add(gdstk.Reference(new_shifted, ((profile.hierarchy_cols - 1) * 16, 0), rows=profile.hierarchy_rows, spacing=(16, 16)))
|
|
713
|
+
target_y1 = (profile.hierarchy_rows - 1) * 16 + 8
|
|
714
|
+
old_bbox = ((profile.hierarchy_cols - 1) * 16, 0, (profile.hierarchy_cols - 1) * 16 + 8, target_y1)
|
|
715
|
+
new_bbox = ((profile.hierarchy_cols - 1) * 16 + 1.5, 0, (profile.hierarchy_cols - 1) * 16 + 9.5, target_y1)
|
|
716
|
+
edits = (
|
|
717
|
+
SyntheticEdit("hierarchy_array_shift_001", "last_array_column", "shift_referenced_array_column", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=old_bbox, after_bbox_um=new_bbox, notes="Only the final array column uses a shifted leaf cell."),
|
|
718
|
+
)
|
|
719
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _case_sparse_large_rect_grid(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
723
|
+
layer = (13, 0)
|
|
724
|
+
old, old_top = _library()
|
|
725
|
+
new, new_top = _library()
|
|
726
|
+
n = profile.sparse_grid_n
|
|
727
|
+
removed = {(1, 1), (n // 2, n // 2)}
|
|
728
|
+
shifted = {(n - 2, n - 2)}
|
|
729
|
+
added_positions = [(n + 2, 0), (n + 3, 1)]
|
|
730
|
+
for ix in range(n):
|
|
731
|
+
for iy in range(n):
|
|
732
|
+
poly = _grid_rect(ix, iy, layer)
|
|
733
|
+
old_top.add(poly)
|
|
734
|
+
if (ix, iy) in removed:
|
|
735
|
+
continue
|
|
736
|
+
if (ix, iy) in shifted:
|
|
737
|
+
new_top.add(_grid_rect(ix, iy, layer, dx=0.7))
|
|
738
|
+
else:
|
|
739
|
+
new_top.add(_grid_rect(ix, iy, layer))
|
|
740
|
+
for pos in added_positions:
|
|
741
|
+
new_top.add(_grid_rect(pos[0], pos[1], layer))
|
|
742
|
+
edits = (
|
|
743
|
+
SyntheticEdit("sparse_remove_001", "grid_rect_1_1", "remove_grid_rect", "coverage_before_only", before_layer=layer, before_bbox_um=_grid_bbox(1, 1)),
|
|
744
|
+
SyntheticEdit("sparse_remove_002", "grid_rect_mid", "remove_grid_rect", "coverage_before_only", before_layer=layer, before_bbox_um=_grid_bbox(n // 2, n // 2)),
|
|
745
|
+
SyntheticEdit("sparse_shift_001", "grid_rect_last", "shift_grid_rect", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_grid_bbox(n - 2, n - 2), after_bbox_um=_grid_bbox(n - 2, n - 2, dx=0.7)),
|
|
746
|
+
SyntheticEdit("sparse_add_001", "grid_rect_extra_0", "add_grid_rect", "coverage_after_only", after_layer=layer, after_bbox_um=_grid_bbox(n + 2, 0)),
|
|
747
|
+
SyntheticEdit("sparse_add_002", "grid_rect_extra_1", "add_grid_rect", "coverage_after_only", after_layer=layer, after_bbox_um=_grid_bbox(n + 3, 1)),
|
|
748
|
+
)
|
|
749
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _case_mixed_complex_polygons(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
753
|
+
layer = (14, 0)
|
|
754
|
+
old, old_top = _library()
|
|
755
|
+
new, new_top = _library()
|
|
756
|
+
count = profile.mixed_count
|
|
757
|
+
changed = {3, count // 3, (2 * count) // 3}
|
|
758
|
+
removed_index = count // 2
|
|
759
|
+
for index in range(count):
|
|
760
|
+
x = (index % 40) * 18.0
|
|
761
|
+
y = (index // 40) * 18.0
|
|
762
|
+
kind = index % 3
|
|
763
|
+
old_poly = _mixed_shape(kind, x, y, index, layer, changed=False)
|
|
764
|
+
old_top.add(old_poly)
|
|
765
|
+
if index == removed_index:
|
|
766
|
+
continue
|
|
767
|
+
new_top.add(_mixed_shape(kind, x, y, index, layer, changed=index in changed))
|
|
768
|
+
added = _star_polygon(-35, -35, 8, 3, 6, 0.2, layer)
|
|
769
|
+
new_top.add(added)
|
|
770
|
+
edits = tuple(
|
|
771
|
+
SyntheticEdit(
|
|
772
|
+
f"mixed_change_{slot:03d}",
|
|
773
|
+
f"mixed_shape_{slot}",
|
|
774
|
+
"modify_complex_polygon",
|
|
775
|
+
"coverage_both",
|
|
776
|
+
before_layer=layer,
|
|
777
|
+
after_layer=layer,
|
|
778
|
+
before_bbox_um=_bbox(_mixed_shape(slot % 3, (slot % 40) * 18.0, (slot // 40) * 18.0, slot, layer, changed=False)),
|
|
779
|
+
after_bbox_um=_bbox(_mixed_shape(slot % 3, (slot % 40) * 18.0, (slot // 40) * 18.0, slot, layer, changed=True)),
|
|
780
|
+
)
|
|
781
|
+
for slot in sorted(changed)
|
|
782
|
+
) + (
|
|
783
|
+
SyntheticEdit("mixed_remove_001", f"mixed_shape_{removed_index}", "remove_complex_polygon", "coverage_before_only", before_layer=layer, before_bbox_um=_bbox(_mixed_shape(removed_index % 3, (removed_index % 40) * 18.0, (removed_index // 40) * 18.0, removed_index, layer, changed=False))),
|
|
784
|
+
SyntheticEdit("mixed_add_001", "mixed_added_star", "add_complex_polygon", "coverage_after_only", after_layer=layer, after_bbox_um=_bbox(added)),
|
|
785
|
+
)
|
|
786
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _case_hollow_grid_many(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
790
|
+
layer = (15, 0)
|
|
791
|
+
old, old_top = _library()
|
|
792
|
+
new, new_top = _library()
|
|
793
|
+
n = profile.hollow_grid_n
|
|
794
|
+
changed = (n // 2, n // 2)
|
|
795
|
+
removed = (max(0, n - 2), 1)
|
|
796
|
+
for ix in range(n):
|
|
797
|
+
for iy in range(n):
|
|
798
|
+
x = ix * 18.0
|
|
799
|
+
y = iy * 18.0
|
|
800
|
+
old_ring = _rect_ring(x, y, x + 12, y + 12, x + 4, y + 4, x + 8, y + 8, layer)
|
|
801
|
+
for poly in old_ring:
|
|
802
|
+
old_top.add(poly)
|
|
803
|
+
if (ix, iy) == removed:
|
|
804
|
+
continue
|
|
805
|
+
if (ix, iy) == changed:
|
|
806
|
+
new_ring = _rect_ring(x, y, x + 12, y + 12, x + 3, y + 3, x + 9, y + 9, layer)
|
|
807
|
+
else:
|
|
808
|
+
new_ring = _rect_ring(x, y, x + 12, y + 12, x + 4, y + 4, x + 8, y + 8, layer)
|
|
809
|
+
for poly in new_ring:
|
|
810
|
+
new_top.add(poly)
|
|
811
|
+
edits = (
|
|
812
|
+
SyntheticEdit("hollow_grid_resize_001", "central_ring_hole", "resize_grid_ring_hole", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=(changed[0] * 18 + 4, changed[1] * 18 + 4, changed[0] * 18 + 8, changed[1] * 18 + 8), after_bbox_um=(changed[0] * 18 + 3, changed[1] * 18 + 3, changed[0] * 18 + 9, changed[1] * 18 + 9)),
|
|
813
|
+
SyntheticEdit("hollow_grid_remove_001", "removed_ring", "remove_hollow_ring", "coverage_before_only", before_layer=layer, before_bbox_um=(removed[0] * 18, removed[1] * 18, removed[0] * 18 + 12, removed[1] * 18 + 12)),
|
|
814
|
+
)
|
|
815
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _case_multi_layer_mixed_edits(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
819
|
+
layers = ((16, 0), (16, 1), (17, 0), (18, 2))
|
|
820
|
+
old, old_top = _library()
|
|
821
|
+
new, new_top = _library()
|
|
822
|
+
edits: list[SyntheticEdit] = []
|
|
823
|
+
for idx, layer in enumerate(layers):
|
|
824
|
+
for j in range(8):
|
|
825
|
+
x = idx * 80 + j * 8
|
|
826
|
+
rect = _rect(x, 0, x + 5, 5, layer)
|
|
827
|
+
old_top.add(rect)
|
|
828
|
+
if idx == 0 and j == 2:
|
|
829
|
+
edits.append(SyntheticEdit("multi_remove_001", "layer16_removed", "remove_layer_shape", "coverage_before_only", before_layer=layer, before_bbox_um=_bbox(rect)))
|
|
830
|
+
continue
|
|
831
|
+
if idx == 1 and j == 3:
|
|
832
|
+
moved_layer = (19, 0)
|
|
833
|
+
new_rect = _rect(x, 0, x + 5, 5, moved_layer)
|
|
834
|
+
new_top.add(new_rect)
|
|
835
|
+
edits.append(SyntheticEdit("multi_layer_move_001", "datatype_shape_moved_layer", "move_to_new_layer", "coverage_both", before_layer=layer, after_layer=moved_layer, before_bbox_um=_bbox(rect), after_bbox_um=_bbox(new_rect)))
|
|
836
|
+
continue
|
|
837
|
+
if idx == 2 and j == 4:
|
|
838
|
+
new_rect = _rect(x + 1, 0, x + 6, 5, layer)
|
|
839
|
+
new_top.add(new_rect)
|
|
840
|
+
edits.append(SyntheticEdit("multi_shift_001", "layer17_shifted", "shift_layer_shape", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(rect), after_bbox_um=_bbox(new_rect)))
|
|
841
|
+
continue
|
|
842
|
+
new_top.add(_rect(x, 0, x + 5, 5, layer))
|
|
843
|
+
added = _ellipse_polygon(360, 0, 5, 5, 24, layers[-1])
|
|
844
|
+
new_top.add(added)
|
|
845
|
+
edits.append(SyntheticEdit("multi_add_001", "layer18_added_circle", "add_layer_shape", "coverage_after_only", after_layer=layers[-1], after_bbox_um=_bbox(added)))
|
|
846
|
+
return SyntheticCaseBuild(old, new, tuple(edits))
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _case_block_like_hierarchy_stress(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
850
|
+
layer = (20, 0)
|
|
851
|
+
old = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
852
|
+
old_block = old.new_cell("block")
|
|
853
|
+
_add(old_block, _rect(0, 0, 8, 8, layer), _rect(10, 0, 18, 8, layer), _star_polygon(9, 16, 5, 2, 5, 0.0, layer))
|
|
854
|
+
old_top = old.new_cell("top")
|
|
855
|
+
|
|
856
|
+
new = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
857
|
+
new_block = new.new_cell("block")
|
|
858
|
+
_add(new_block, _rect(0, 0, 8, 8, layer), _rect(10, 0, 18, 8, layer), _star_polygon(9, 16, 5, 2, 5, 0.0, layer))
|
|
859
|
+
new_block_modified = new.new_cell("block_modified")
|
|
860
|
+
_add(new_block_modified, _rect(0, 0, 8, 8, layer), _rect(11.5, 0, 19.5, 8, layer), _star_polygon(9, 16, 5, 2, 5, 0.25, layer))
|
|
861
|
+
new_top = new.new_cell("top")
|
|
862
|
+
|
|
863
|
+
cols = profile.reference_cols
|
|
864
|
+
rows = profile.reference_rows
|
|
865
|
+
modified = {(cols // 2, rows // 2), (max(0, cols - 3), max(0, rows - 2))}
|
|
866
|
+
for ix in range(cols):
|
|
867
|
+
for iy in range(rows):
|
|
868
|
+
origin = (ix * 28, iy * 28)
|
|
869
|
+
old_top.add(gdstk.Reference(old_block, origin))
|
|
870
|
+
new_top.add(gdstk.Reference(new_block_modified if (ix, iy) in modified else new_block, origin))
|
|
871
|
+
edits = tuple(
|
|
872
|
+
SyntheticEdit(
|
|
873
|
+
f"block_modify_{idx:03d}",
|
|
874
|
+
f"block_ref_{ix}_{iy}",
|
|
875
|
+
"modify_referenced_block",
|
|
876
|
+
"coverage_both",
|
|
877
|
+
before_layer=layer,
|
|
878
|
+
after_layer=layer,
|
|
879
|
+
before_bbox_um=(ix * 28, iy * 28, ix * 28 + 20, iy * 28 + 22),
|
|
880
|
+
after_bbox_um=(ix * 28, iy * 28, ix * 28 + 20, iy * 28 + 22),
|
|
881
|
+
notes="One referenced block instance uses shifted metal and a rotated star.",
|
|
882
|
+
)
|
|
883
|
+
for idx, (ix, iy) in enumerate(sorted(modified), start=1)
|
|
884
|
+
)
|
|
885
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _case_adversarial_nested_rings_islands(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
889
|
+
layer = (21, 0)
|
|
890
|
+
old, old_top = _library()
|
|
891
|
+
new, new_top = _library()
|
|
892
|
+
old_outer = _rect(0, 0, 90, 70, layer)
|
|
893
|
+
old_holes = (
|
|
894
|
+
_rect(10, 10, 25, 25, layer),
|
|
895
|
+
_rect(35, 10, 50, 25, layer),
|
|
896
|
+
_rect(20, 40, 70, 58, layer),
|
|
897
|
+
)
|
|
898
|
+
new_holes = (
|
|
899
|
+
_rect(10, 10, 25, 25, layer),
|
|
900
|
+
_rect(32, 7, 54, 28, layer),
|
|
901
|
+
_rect(20, 40, 70, 58, layer),
|
|
902
|
+
)
|
|
903
|
+
island = _rect(41, 15, 46, 20, layer)
|
|
904
|
+
inner_ring_old = _rect_ring(30, 42, 60, 56, 38, 46, 52, 52, layer)
|
|
905
|
+
inner_ring_new = _rect_ring(30, 42, 60, 56, 40, 48, 50, 51, layer)
|
|
906
|
+
_add(old_top, *_boolean(old_outer, old_holes, "not", layer), *inner_ring_old)
|
|
907
|
+
_add(new_top, *_boolean(_rect(0, 0, 90, 70, layer), new_holes, "not", layer), island, *inner_ring_new)
|
|
908
|
+
edits = (
|
|
909
|
+
SyntheticEdit(
|
|
910
|
+
"adv_nested_hole_expand_001",
|
|
911
|
+
"expanded_rect_hole",
|
|
912
|
+
"expand_inner_hole_inside_outer_ring",
|
|
913
|
+
"coverage_before_only",
|
|
914
|
+
before_layer=layer,
|
|
915
|
+
after_layer=layer,
|
|
916
|
+
before_bbox_um=(32, 7, 54, 28),
|
|
917
|
+
after_bbox_um=(32, 7, 54, 28),
|
|
918
|
+
notes="The hole is enlarged, creating before-only ring fragments fully contained by the outer polygon.",
|
|
919
|
+
),
|
|
920
|
+
SyntheticEdit(
|
|
921
|
+
"adv_nested_island_add_001",
|
|
922
|
+
"island_inside_hole",
|
|
923
|
+
"add_island_inside_hole",
|
|
924
|
+
"coverage_after_only",
|
|
925
|
+
after_layer=layer,
|
|
926
|
+
after_bbox_um=_bbox(island),
|
|
927
|
+
notes="A disconnected island is added inside a void.",
|
|
928
|
+
),
|
|
929
|
+
SyntheticEdit(
|
|
930
|
+
"adv_nested_inner_ring_fill_001",
|
|
931
|
+
"nested_ring_hole",
|
|
932
|
+
"shrink_nested_ring_hole",
|
|
933
|
+
"coverage_after_only",
|
|
934
|
+
before_layer=layer,
|
|
935
|
+
after_layer=layer,
|
|
936
|
+
before_bbox_um=(38, 46, 52, 52),
|
|
937
|
+
after_bbox_um=(38, 46, 52, 52),
|
|
938
|
+
notes="The inner ring hole shrinks, producing after-only coverage inside nested topology.",
|
|
939
|
+
),
|
|
940
|
+
)
|
|
941
|
+
return SyntheticCaseBuild(old, new, edits, notes="Nested ring, multi-hole, and island-in-hole topology.")
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _case_adversarial_hole_touching_boundary(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
945
|
+
layer = (22, 0)
|
|
946
|
+
old, old_top = _library()
|
|
947
|
+
new, new_top = _library()
|
|
948
|
+
old_outer = _rect(0, 0, 60, 42, layer)
|
|
949
|
+
old_hole = _rect(12, 10, 28, 30, layer)
|
|
950
|
+
new_edge_cut = _rect(0, 10, 28, 30, layer)
|
|
951
|
+
old_diamond = _diamond_polygon(46, 21, 7, layer)
|
|
952
|
+
new_diamond = _diamond_polygon(53, 21, 7, layer)
|
|
953
|
+
_add(old_top, *_boolean(old_outer, (old_hole, old_diamond), "not", layer))
|
|
954
|
+
_add(new_top, *_boolean(_rect(0, 0, 60, 42, layer), (new_edge_cut, new_diamond), "not", layer))
|
|
955
|
+
edits = (
|
|
956
|
+
SyntheticEdit(
|
|
957
|
+
"adv_hole_edge_touch_001",
|
|
958
|
+
"rect_hole_to_edge_notch",
|
|
959
|
+
"grow_hole_until_it_touches_outer_edge",
|
|
960
|
+
"coverage_before_only",
|
|
961
|
+
before_layer=layer,
|
|
962
|
+
after_layer=layer,
|
|
963
|
+
before_bbox_um=(0, 10, 12, 30),
|
|
964
|
+
after_bbox_um=(0, 10, 12, 30),
|
|
965
|
+
notes="A true hole becomes a notch connected to the outer boundary.",
|
|
966
|
+
),
|
|
967
|
+
SyntheticEdit(
|
|
968
|
+
"adv_hole_point_touch_001",
|
|
969
|
+
"diamond_hole_to_boundary_point",
|
|
970
|
+
"move_hole_until_one_vertex_touches_boundary",
|
|
971
|
+
"coverage_both",
|
|
972
|
+
before_layer=layer,
|
|
973
|
+
after_layer=layer,
|
|
974
|
+
before_bbox_um=(39, 14, 53, 28),
|
|
975
|
+
after_bbox_um=(46, 14, 60, 28),
|
|
976
|
+
notes="The moved diamond cutout has a point on the outer boundary.",
|
|
977
|
+
),
|
|
978
|
+
)
|
|
979
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _case_adversarial_containment_no_edge_crossing(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
983
|
+
layer = (23, 0)
|
|
984
|
+
old, old_top = _library()
|
|
985
|
+
new, new_top = _library()
|
|
986
|
+
small_old = _rect(0, 0, 10, 10, layer)
|
|
987
|
+
grown_new = _rect(-5, -5, 15, 15, layer)
|
|
988
|
+
large_old = _rect(35, 0, 75, 35, layer)
|
|
989
|
+
shrunk_new = _rect(48, 9, 62, 24, layer)
|
|
990
|
+
_add(old_top, small_old, large_old)
|
|
991
|
+
_add(new_top, grown_new, shrunk_new)
|
|
992
|
+
edits = (
|
|
993
|
+
SyntheticEdit(
|
|
994
|
+
"adv_containment_grow_001",
|
|
995
|
+
"small_rect_grown_around_old",
|
|
996
|
+
"grow_polygon_fully_containing_before_polygon",
|
|
997
|
+
"coverage_after_only",
|
|
998
|
+
before_layer=layer,
|
|
999
|
+
after_layer=layer,
|
|
1000
|
+
before_bbox_um=_bbox(small_old),
|
|
1001
|
+
after_bbox_um=_bbox(grown_new),
|
|
1002
|
+
notes="The before polygon is fully contained by the after polygon, with no boundary crossing.",
|
|
1003
|
+
),
|
|
1004
|
+
SyntheticEdit(
|
|
1005
|
+
"adv_containment_shrink_001",
|
|
1006
|
+
"large_rect_shrunk_inside_old",
|
|
1007
|
+
"shrink_polygon_fully_inside_before_polygon",
|
|
1008
|
+
"coverage_before_only",
|
|
1009
|
+
before_layer=layer,
|
|
1010
|
+
after_layer=layer,
|
|
1011
|
+
before_bbox_um=_bbox(large_old),
|
|
1012
|
+
after_bbox_um=_bbox(shrunk_new),
|
|
1013
|
+
notes="The after polygon is fully contained by the before polygon.",
|
|
1014
|
+
),
|
|
1015
|
+
)
|
|
1016
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def _case_adversarial_reversed_winding_equivalent(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1020
|
+
layer = (24, 0)
|
|
1021
|
+
old, old_top = _library()
|
|
1022
|
+
new, new_top = _library()
|
|
1023
|
+
points = ((0, 0), (34, 0), (34, 18), (18, 18), (18, 30), (0, 30))
|
|
1024
|
+
old_poly = gdstk.Polygon(points, layer=layer[0], datatype=layer[1])
|
|
1025
|
+
new_poly = gdstk.Polygon(tuple(reversed(points)), layer=layer[0], datatype=layer[1])
|
|
1026
|
+
old_top.add(old_poly)
|
|
1027
|
+
new_top.add(new_poly)
|
|
1028
|
+
edits = (
|
|
1029
|
+
SyntheticEdit(
|
|
1030
|
+
"adv_winding_reverse_001",
|
|
1031
|
+
"same_polygon_reversed_winding",
|
|
1032
|
+
"reverse_vertex_winding_without_changing_coverage",
|
|
1033
|
+
"inventory_only",
|
|
1034
|
+
before_layer=layer,
|
|
1035
|
+
after_layer=layer,
|
|
1036
|
+
before_bbox_um=_bbox(old_poly),
|
|
1037
|
+
after_bbox_um=_bbox(new_poly),
|
|
1038
|
+
notes="Coverage is identical; only source vertex order changes.",
|
|
1039
|
+
),
|
|
1040
|
+
)
|
|
1041
|
+
return SyntheticCaseBuild(old, new, edits, equivalent_expected=True)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def _case_adversarial_duplicate_polygons_equivalent(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1045
|
+
layer = (25, 0)
|
|
1046
|
+
old, old_top = _library()
|
|
1047
|
+
new, new_top = _library()
|
|
1048
|
+
rect = _rect(0, 0, 30, 20, layer)
|
|
1049
|
+
old_top.add(rect)
|
|
1050
|
+
old_top.add(_rect(0, 0, 30, 20, layer))
|
|
1051
|
+
_add(new_top, _rect(0, 0, 15, 20, layer), _rect(15, 0, 30, 20, layer))
|
|
1052
|
+
edits = (
|
|
1053
|
+
SyntheticEdit(
|
|
1054
|
+
"adv_duplicate_refracture_001",
|
|
1055
|
+
"duplicate_rect_to_split_rect",
|
|
1056
|
+
"remove_duplicate_and_refracture_without_changing_coverage",
|
|
1057
|
+
"inventory_only",
|
|
1058
|
+
before_layer=layer,
|
|
1059
|
+
after_layer=layer,
|
|
1060
|
+
before_bbox_um=_bbox(rect),
|
|
1061
|
+
after_bbox_um=_bbox(rect),
|
|
1062
|
+
notes="Duplicate coincident source polygons and split target polygons describe identical coverage.",
|
|
1063
|
+
),
|
|
1064
|
+
)
|
|
1065
|
+
return SyntheticCaseBuild(old, new, edits, equivalent_expected=True)
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
def _case_adversarial_partial_collinear_overlap(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1069
|
+
layer = (26, 0)
|
|
1070
|
+
old, old_top = _library()
|
|
1071
|
+
new, new_top = _library()
|
|
1072
|
+
base_old = _rect(0, 0, 60, 5, layer)
|
|
1073
|
+
tab_old = _rect(12, 5, 42, 18, layer)
|
|
1074
|
+
tab_new = _rect(12, 5.001, 42, 18.001, layer)
|
|
1075
|
+
_add(old_top, base_old, tab_old, _rect(72, 0, 92, 20, layer))
|
|
1076
|
+
_add(new_top, _rect(0, 0, 60, 5, layer), tab_new, _rect(72, 0, 92, 20, layer))
|
|
1077
|
+
edits = (
|
|
1078
|
+
SyntheticEdit(
|
|
1079
|
+
"adv_collinear_gap_001",
|
|
1080
|
+
"tab_with_partially_collinear_edge",
|
|
1081
|
+
"open_one_dbu_gap_along_partially_collinear_edge",
|
|
1082
|
+
"coverage_both",
|
|
1083
|
+
before_layer=layer,
|
|
1084
|
+
after_layer=layer,
|
|
1085
|
+
before_bbox_um=_bbox(tab_old),
|
|
1086
|
+
after_bbox_um=_bbox(tab_new),
|
|
1087
|
+
notes="A tab sharing part of a base rectangle edge moves by one DBU.",
|
|
1088
|
+
),
|
|
1089
|
+
)
|
|
1090
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _case_adversarial_one_dbu_touch_tjunction(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1094
|
+
layer = (27, 0)
|
|
1095
|
+
old, old_top = _library()
|
|
1096
|
+
new, new_top = _library()
|
|
1097
|
+
corner_a = _rect(0, 0, 10, 10, layer)
|
|
1098
|
+
corner_b_old = _rect(10, 10, 20, 20, layer)
|
|
1099
|
+
corner_b_new = _rect(10.001, 10.001, 20.001, 20.001, layer)
|
|
1100
|
+
tee_stem_old = _rect(42, 0, 46, 18, layer)
|
|
1101
|
+
tee_bar = _rect(32, 18, 56, 24, layer)
|
|
1102
|
+
tee_stem_new = _rect(42, 0, 46.001, 18, layer)
|
|
1103
|
+
_add(old_top, corner_a, corner_b_old, tee_stem_old, tee_bar)
|
|
1104
|
+
_add(new_top, _rect(0, 0, 10, 10, layer), corner_b_new, tee_stem_new, _rect(32, 18, 56, 24, layer))
|
|
1105
|
+
edits = (
|
|
1106
|
+
SyntheticEdit(
|
|
1107
|
+
"adv_point_touch_gap_001",
|
|
1108
|
+
"corner_touch_pair",
|
|
1109
|
+
"open_one_dbu_gap_from_point_touch",
|
|
1110
|
+
"coverage_both",
|
|
1111
|
+
before_layer=layer,
|
|
1112
|
+
after_layer=layer,
|
|
1113
|
+
before_bbox_um=_bbox(corner_b_old),
|
|
1114
|
+
after_bbox_um=_bbox(corner_b_new),
|
|
1115
|
+
),
|
|
1116
|
+
SyntheticEdit(
|
|
1117
|
+
"adv_tjunction_overlap_001",
|
|
1118
|
+
"t_junction_stem",
|
|
1119
|
+
"extend_t_junction_stem_by_one_dbu",
|
|
1120
|
+
"coverage_after_only",
|
|
1121
|
+
before_layer=layer,
|
|
1122
|
+
after_layer=layer,
|
|
1123
|
+
before_bbox_um=_bbox(tee_stem_old),
|
|
1124
|
+
after_bbox_um=(46, 0, 46.001, 18),
|
|
1125
|
+
),
|
|
1126
|
+
)
|
|
1127
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _case_adversarial_one_dbu_sliver_grid(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1131
|
+
layer = (28, 0)
|
|
1132
|
+
old, old_top = _library()
|
|
1133
|
+
new, new_top = _library()
|
|
1134
|
+
n = max(6, min(profile.sparse_grid_n, 180))
|
|
1135
|
+
removed = {1, n // 2}
|
|
1136
|
+
extended = n - 2
|
|
1137
|
+
for index in range(n):
|
|
1138
|
+
y = index * 0.01
|
|
1139
|
+
old_sliver = _rect(0, y, 20, y + 0.001, layer)
|
|
1140
|
+
old_top.add(old_sliver)
|
|
1141
|
+
if index in removed:
|
|
1142
|
+
continue
|
|
1143
|
+
if index == extended:
|
|
1144
|
+
new_top.add(_rect(0, y, 22, y + 0.001, layer))
|
|
1145
|
+
else:
|
|
1146
|
+
new_top.add(_rect(0, y, 20, y + 0.001, layer))
|
|
1147
|
+
edits = (
|
|
1148
|
+
SyntheticEdit("adv_sliver_remove_001", "one_dbu_sliver_1", "remove_one_dbu_sliver", "coverage_before_only", before_layer=layer, before_bbox_um=(0, 0.01, 20, 0.011)),
|
|
1149
|
+
SyntheticEdit("adv_sliver_remove_002", "one_dbu_sliver_mid", "remove_one_dbu_sliver", "coverage_before_only", before_layer=layer, before_bbox_um=(0, (n // 2) * 0.01, 20, (n // 2) * 0.01 + 0.001)),
|
|
1150
|
+
SyntheticEdit("adv_sliver_extend_001", "one_dbu_sliver_extended", "extend_one_dbu_sliver", "coverage_after_only", before_layer=layer, after_layer=layer, before_bbox_um=(20, extended * 0.01, 22, extended * 0.01 + 0.001), after_bbox_um=(20, extended * 0.01, 22, extended * 0.01 + 0.001)),
|
|
1151
|
+
)
|
|
1152
|
+
return SyntheticCaseBuild(old, new, edits, notes=f"Scale-aware one-DBU sliver set with {n} source slivers.")
|
|
1153
|
+
|
|
1154
|
+
|
|
1155
|
+
def _case_adversarial_large_coordinates_tiny_delta(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1156
|
+
layer = (29, 0)
|
|
1157
|
+
old, old_top = _library()
|
|
1158
|
+
new, new_top = _library()
|
|
1159
|
+
base_x = 1_000_000.0
|
|
1160
|
+
base_y = -900_000.0
|
|
1161
|
+
old_rect = _rect(base_x, base_y, base_x + 25, base_y + 12, layer)
|
|
1162
|
+
new_rect = _rect(base_x + 0.001, base_y, base_x + 25.001, base_y + 12, layer)
|
|
1163
|
+
old_far = _rect(-1_000_000.0, 900_000.0, -999_980.0, 900_020.0, layer)
|
|
1164
|
+
new_far = _rect(-1_000_000.0, 900_000.001, -999_980.0, 900_020.001, layer)
|
|
1165
|
+
_add(old_top, old_rect, old_far)
|
|
1166
|
+
_add(new_top, new_rect, new_far)
|
|
1167
|
+
edits = (
|
|
1168
|
+
SyntheticEdit("adv_large_coord_shift_001", "large_positive_coordinate_rect", "shift_large_coordinate_rect_by_one_dbu", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_rect), after_bbox_um=_bbox(new_rect)),
|
|
1169
|
+
SyntheticEdit("adv_large_coord_shift_002", "large_negative_coordinate_rect", "shift_large_negative_coordinate_rect_by_one_dbu", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_far), after_bbox_um=_bbox(new_far)),
|
|
1170
|
+
)
|
|
1171
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def _case_adversarial_snap_rounding_equivalent(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1175
|
+
layer = (30, 0)
|
|
1176
|
+
old, old_top = _library()
|
|
1177
|
+
new, new_top = _library()
|
|
1178
|
+
old_rect = _rect(0, 0, 10, 10, layer)
|
|
1179
|
+
new_rect = _rect(0.0004, 0.0004, 10.0004, 10.0004, layer)
|
|
1180
|
+
old_top.add(old_rect)
|
|
1181
|
+
new_top.add(new_rect)
|
|
1182
|
+
edits = (
|
|
1183
|
+
SyntheticEdit(
|
|
1184
|
+
"adv_snap_rounding_001",
|
|
1185
|
+
"sub_dbu_shifted_rect",
|
|
1186
|
+
"shift_below_gds_grid_resolution",
|
|
1187
|
+
"inventory_only",
|
|
1188
|
+
before_layer=layer,
|
|
1189
|
+
after_layer=layer,
|
|
1190
|
+
before_bbox_um=_bbox(old_rect),
|
|
1191
|
+
after_bbox_um=_bbox(new_rect),
|
|
1192
|
+
notes="The geometric source coordinates differ, but GDS quantization snaps both to the same DBU grid.",
|
|
1193
|
+
),
|
|
1194
|
+
)
|
|
1195
|
+
return SyntheticCaseBuild(old, new, edits, equivalent_expected=True)
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
def _case_adversarial_curve_single_vertex_bump(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1199
|
+
layer = (31, 0)
|
|
1200
|
+
old, old_top = _library()
|
|
1201
|
+
new, new_top = _library()
|
|
1202
|
+
old_curve = _ellipse_polygon(0, 0, 16, 12, 48, layer)
|
|
1203
|
+
points = [(float(x), float(y)) for x, y in old_curve.points]
|
|
1204
|
+
x, y = points[7]
|
|
1205
|
+
points[7] = (x + 1.25, y - 0.75)
|
|
1206
|
+
new_curve = gdstk.Polygon(points, layer=layer[0], datatype=layer[1])
|
|
1207
|
+
_add(old_top, old_curve, _ellipse_polygon(45, 0, 10, 10, 64, layer))
|
|
1208
|
+
_add(new_top, new_curve, _ellipse_polygon(45, 0, 10, 10, 64, layer))
|
|
1209
|
+
edits = (
|
|
1210
|
+
SyntheticEdit(
|
|
1211
|
+
"adv_curve_vertex_bump_001",
|
|
1212
|
+
"single_vertex_bumped_ellipse",
|
|
1213
|
+
"move_one_curve_breakpoint",
|
|
1214
|
+
"coverage_both",
|
|
1215
|
+
before_layer=layer,
|
|
1216
|
+
after_layer=layer,
|
|
1217
|
+
before_bbox_um=_bbox(old_curve),
|
|
1218
|
+
after_bbox_um=_bbox(new_curve),
|
|
1219
|
+
),
|
|
1220
|
+
)
|
|
1221
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _case_adversarial_dense_sparse_curve_overlap(_profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1225
|
+
layer = (32, 0)
|
|
1226
|
+
old, old_top = _library()
|
|
1227
|
+
new, new_top = _library()
|
|
1228
|
+
dense = _ellipse_polygon(0, 0, 20, 20, 128, layer)
|
|
1229
|
+
sparse = _ellipse_polygon(0, 0, 20, 20, 20, layer, angle=math.pi / 20)
|
|
1230
|
+
old_overlap = _ellipse_polygon(42, 0, 16, 10, 96, layer)
|
|
1231
|
+
new_overlap = _ellipse_polygon(42.001, 0, 16, 10, 24, layer)
|
|
1232
|
+
_add(old_top, dense, old_overlap)
|
|
1233
|
+
_add(new_top, sparse, new_overlap)
|
|
1234
|
+
edits = (
|
|
1235
|
+
SyntheticEdit("adv_curve_refracture_001", "dense_to_sparse_circle", "replace_dense_curve_with_sparse_curve", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(dense), after_bbox_um=_bbox(sparse)),
|
|
1236
|
+
SyntheticEdit("adv_curve_overlap_001", "overlapping_curve_changed_breakpoints", "shift_and_refracture_overlapping_curve", "coverage_both", before_layer=layer, after_layer=layer, before_bbox_um=_bbox(old_overlap), after_bbox_um=_bbox(new_overlap)),
|
|
1237
|
+
)
|
|
1238
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def _case_adversarial_hierarchy_transforms(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1242
|
+
layer = (33, 0)
|
|
1243
|
+
old = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
1244
|
+
old_leaf = old.new_cell("leaf")
|
|
1245
|
+
_add(old_leaf, _rect(0, 0, 8, 4, layer), _star_polygon(4, 10, 4, 1.5, 5, 0.0, layer))
|
|
1246
|
+
old_top = old.new_cell("top")
|
|
1247
|
+
old_top.add(gdstk.Reference(old_leaf, (0, 0), columns=max(2, profile.hierarchy_cols // 2), rows=2, spacing=(20, 24)))
|
|
1248
|
+
old_top.add(gdstk.Reference(old_leaf, (120, 0), rotation=math.pi / 2, magnification=2.0))
|
|
1249
|
+
old_top.add(gdstk.Reference(old_leaf, (170, 40), rotation=math.pi / 4, magnification=1.5, x_reflection=True))
|
|
1250
|
+
|
|
1251
|
+
new = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
1252
|
+
new_leaf = new.new_cell("leaf")
|
|
1253
|
+
_add(new_leaf, _rect(0, 0, 8, 4, layer), _star_polygon(4, 10, 4, 1.5, 5, 0.0, layer))
|
|
1254
|
+
new_leaf_shifted = new.new_cell("leaf_shifted")
|
|
1255
|
+
_add(new_leaf_shifted, _rect(1, 0, 9, 4, layer), _star_polygon(4, 10, 4, 1.5, 5, 0.2, layer))
|
|
1256
|
+
new_top = new.new_cell("top")
|
|
1257
|
+
new_top.add(gdstk.Reference(new_leaf, (0, 0), columns=max(2, profile.hierarchy_cols // 2), rows=2, spacing=(20, 24)))
|
|
1258
|
+
new_top.add(gdstk.Reference(new_leaf_shifted, (120, 0), rotation=math.pi / 2, magnification=2.0))
|
|
1259
|
+
new_top.add(gdstk.Reference(new_leaf, (170, 40), rotation=math.pi / 4, magnification=1.5, x_reflection=True))
|
|
1260
|
+
edits = (
|
|
1261
|
+
SyntheticEdit(
|
|
1262
|
+
"adv_hierarchy_transform_001",
|
|
1263
|
+
"rotated_magnified_reference",
|
|
1264
|
+
"modify_child_used_by_rotated_magnified_reference",
|
|
1265
|
+
"coverage_both",
|
|
1266
|
+
before_layer=layer,
|
|
1267
|
+
after_layer=layer,
|
|
1268
|
+
before_bbox_um=(108, -2, 124, 24),
|
|
1269
|
+
after_bbox_um=(108, -2, 124, 24),
|
|
1270
|
+
notes="Only a rotated and magnified reference changes; stable array and reflected reference remain unchanged.",
|
|
1271
|
+
),
|
|
1272
|
+
)
|
|
1273
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
def _case_adversarial_array_pitch_one_dbu(profile: ScaleProfile) -> SyntheticCaseBuild:
|
|
1277
|
+
layer = (34, 0)
|
|
1278
|
+
old = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
1279
|
+
old_leaf = old.new_cell("leaf")
|
|
1280
|
+
old_leaf.add(_rect(0, 0, 4, 4, layer))
|
|
1281
|
+
old_top = old.new_cell("top")
|
|
1282
|
+
rows = max(3, min(profile.hierarchy_rows, 20))
|
|
1283
|
+
cols = max(3, min(profile.hierarchy_cols, 20))
|
|
1284
|
+
old_top.add(gdstk.Reference(old_leaf, (0, 0), columns=cols, rows=rows, spacing=(8, 8)))
|
|
1285
|
+
|
|
1286
|
+
new = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
1287
|
+
new_leaf = new.new_cell("leaf")
|
|
1288
|
+
new_leaf.add(_rect(0, 0, 4, 4, layer))
|
|
1289
|
+
new_top = new.new_cell("top")
|
|
1290
|
+
new_top.add(gdstk.Reference(new_leaf, (0, 0), columns=cols, rows=max(1, rows - 1), spacing=(8, 8)))
|
|
1291
|
+
new_top.add(gdstk.Reference(new_leaf, (0, (rows - 1) * 8 + 0.001), columns=cols, rows=1, spacing=(8, 8)))
|
|
1292
|
+
y0 = (rows - 1) * 8
|
|
1293
|
+
edits = (
|
|
1294
|
+
SyntheticEdit(
|
|
1295
|
+
"adv_array_pitch_001",
|
|
1296
|
+
"last_array_row",
|
|
1297
|
+
"shift_array_row_by_one_dbu",
|
|
1298
|
+
"coverage_both",
|
|
1299
|
+
before_layer=layer,
|
|
1300
|
+
after_layer=layer,
|
|
1301
|
+
before_bbox_um=(0, y0, (cols - 1) * 8 + 4, y0 + 4),
|
|
1302
|
+
after_bbox_um=(0, y0 + 0.001, (cols - 1) * 8 + 4, y0 + 4.001),
|
|
1303
|
+
),
|
|
1304
|
+
)
|
|
1305
|
+
return SyntheticCaseBuild(old, new, edits)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
BASE_SYNTHETIC_CASES: tuple[SyntheticCaseDefinition, ...] = (
|
|
1309
|
+
SyntheticCaseDefinition("rect_add_remove_shift", "Rectangle Add Remove Shift", _case_rect_add_remove_shift),
|
|
1310
|
+
SyntheticCaseDefinition("overlap_rect_unions", "Overlapping Rectangle Unions", _case_overlap_rect_unions),
|
|
1311
|
+
SyntheticCaseDefinition("circle_breakpoints", "Circle And Ellipse Breakpoints", _case_circle_breakpoints),
|
|
1312
|
+
SyntheticCaseDefinition("star_concave", "Concave Star Polygons", _case_star_concave),
|
|
1313
|
+
SyntheticCaseDefinition("hollow_rect_circle", "Hollow Rectangle And Donut", _case_hollow_rect_circle),
|
|
1314
|
+
SyntheticCaseDefinition("nested_holes_bridge", "Nested Holes And Bridge Cuts", _case_nested_holes_bridge),
|
|
1315
|
+
SyntheticCaseDefinition("thin_slivers_near_collinear", "Thin Slivers And Near-Collinear Vertices", _case_thin_slivers_near_collinear),
|
|
1316
|
+
SyntheticCaseDefinition("layer_datatype_moves", "Layer And Datatype Moves", _case_layer_datatype_moves),
|
|
1317
|
+
SyntheticCaseDefinition("same_coverage_refracturing", "Same-Coverage Refracturing", _case_same_coverage_refracturing),
|
|
1318
|
+
SyntheticCaseDefinition("touching_edges_points", "Touching Edges And Points", _case_touching_edges_points),
|
|
1319
|
+
SyntheticCaseDefinition("cell_references_arrays", "Cell References And Arrays", _case_cell_references_arrays),
|
|
1320
|
+
SyntheticCaseDefinition("sparse_large_rect_grid", "Sparse Large Rectangle Grid", _case_sparse_large_rect_grid),
|
|
1321
|
+
SyntheticCaseDefinition("mixed_complex_polygons", "Mixed Complex Polygon Set", _case_mixed_complex_polygons),
|
|
1322
|
+
SyntheticCaseDefinition("hollow_grid_many", "Many Hollow Grid Rings", _case_hollow_grid_many),
|
|
1323
|
+
SyntheticCaseDefinition("multi_layer_mixed_edits", "Multi-Layer Mixed Edits", _case_multi_layer_mixed_edits),
|
|
1324
|
+
SyntheticCaseDefinition("block_like_hierarchy_stress", "Block-Like Hierarchy Stress", _case_block_like_hierarchy_stress),
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
ADVERSARIAL_SYNTHETIC_CASES: tuple[SyntheticCaseDefinition, ...] = (
|
|
1329
|
+
SyntheticCaseDefinition("adversarial_nested_rings_islands", "Adversarial Nested Rings And Islands", _case_adversarial_nested_rings_islands),
|
|
1330
|
+
SyntheticCaseDefinition("adversarial_hole_touching_boundary", "Adversarial Hole Touching Boundary", _case_adversarial_hole_touching_boundary),
|
|
1331
|
+
SyntheticCaseDefinition("adversarial_containment_no_edge_crossing", "Adversarial Containment Without Edge Crossing", _case_adversarial_containment_no_edge_crossing),
|
|
1332
|
+
SyntheticCaseDefinition("adversarial_reversed_winding_equivalent", "Adversarial Reversed Winding Equivalent Coverage", _case_adversarial_reversed_winding_equivalent),
|
|
1333
|
+
SyntheticCaseDefinition("adversarial_duplicate_polygons_equivalent", "Adversarial Duplicate Polygons Equivalent Coverage", _case_adversarial_duplicate_polygons_equivalent),
|
|
1334
|
+
SyntheticCaseDefinition("adversarial_partial_collinear_overlap", "Adversarial Partial Collinear Overlap", _case_adversarial_partial_collinear_overlap),
|
|
1335
|
+
SyntheticCaseDefinition("adversarial_one_dbu_touch_tjunction", "Adversarial One-DBU Touch And T-Junction", _case_adversarial_one_dbu_touch_tjunction),
|
|
1336
|
+
SyntheticCaseDefinition("adversarial_one_dbu_sliver_grid", "Adversarial One-DBU Sliver Grid", _case_adversarial_one_dbu_sliver_grid),
|
|
1337
|
+
SyntheticCaseDefinition("adversarial_large_coordinates_tiny_delta", "Adversarial Large Coordinates Tiny Delta", _case_adversarial_large_coordinates_tiny_delta),
|
|
1338
|
+
SyntheticCaseDefinition("adversarial_snap_rounding_equivalent", "Adversarial Snap Rounding Equivalent Coverage", _case_adversarial_snap_rounding_equivalent),
|
|
1339
|
+
SyntheticCaseDefinition("adversarial_curve_single_vertex_bump", "Adversarial Curve Single Vertex Bump", _case_adversarial_curve_single_vertex_bump),
|
|
1340
|
+
SyntheticCaseDefinition("adversarial_dense_sparse_curve_overlap", "Adversarial Dense Sparse Curve Overlap", _case_adversarial_dense_sparse_curve_overlap),
|
|
1341
|
+
SyntheticCaseDefinition("adversarial_hierarchy_transforms", "Adversarial Hierarchy Transforms", _case_adversarial_hierarchy_transforms),
|
|
1342
|
+
SyntheticCaseDefinition("adversarial_array_pitch_one_dbu", "Adversarial Array Pitch One DBU", _case_adversarial_array_pitch_one_dbu),
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
|
|
1346
|
+
SYNTHETIC_CASES: tuple[SyntheticCaseDefinition, ...] = BASE_SYNTHETIC_CASES + ADVERSARIAL_SYNTHETIC_CASES
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _library() -> tuple[gdstk.Library, gdstk.Cell]:
|
|
1350
|
+
lib = gdstk.Library(unit=_UNIT, precision=_PRECISION)
|
|
1351
|
+
return lib, lib.new_cell("top")
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def _profile(scale: str) -> ScaleProfile:
|
|
1355
|
+
try:
|
|
1356
|
+
return SCALE_PROFILES[scale]
|
|
1357
|
+
except KeyError as exc:
|
|
1358
|
+
raise ValueError(f"unknown synthetic suite scale {scale!r}; expected one of {', '.join(SCALE_PROFILES)}") from exc
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def _rect(x0: float, y0: float, x1: float, y1: float, layer: LayerTuple) -> gdstk.Polygon:
|
|
1362
|
+
return gdstk.rectangle((x0, y0), (x1, y1), layer=layer[0], datatype=layer[1])
|
|
1363
|
+
|
|
1364
|
+
|
|
1365
|
+
def _grid_rect(ix: int, iy: int, layer: LayerTuple, *, dx: float = 0.0, dy: float = 0.0) -> gdstk.Polygon:
|
|
1366
|
+
x0, y0, x1, y1 = _grid_bbox(ix, iy, dx=dx, dy=dy)
|
|
1367
|
+
return _rect(x0, y0, x1, y1, layer)
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def _grid_bbox(ix: int, iy: int, *, dx: float = 0.0, dy: float = 0.0) -> BBox:
|
|
1371
|
+
x = ix * 6.0 + dx
|
|
1372
|
+
y = iy * 6.0 + dy
|
|
1373
|
+
return (x, y, x + 3.0, y + 3.0)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
def _ellipse_polygon(
|
|
1377
|
+
cx: float,
|
|
1378
|
+
cy: float,
|
|
1379
|
+
rx: float,
|
|
1380
|
+
ry: float,
|
|
1381
|
+
point_count: int,
|
|
1382
|
+
layer: LayerTuple,
|
|
1383
|
+
*,
|
|
1384
|
+
angle: float = 0.0,
|
|
1385
|
+
) -> gdstk.Polygon:
|
|
1386
|
+
points = []
|
|
1387
|
+
cos_a = math.cos(angle)
|
|
1388
|
+
sin_a = math.sin(angle)
|
|
1389
|
+
for index in range(point_count):
|
|
1390
|
+
theta = 2 * math.pi * index / point_count
|
|
1391
|
+
x = rx * math.cos(theta)
|
|
1392
|
+
y = ry * math.sin(theta)
|
|
1393
|
+
points.append((cx + cos_a * x - sin_a * y, cy + sin_a * x + cos_a * y))
|
|
1394
|
+
return gdstk.Polygon(points, layer=layer[0], datatype=layer[1])
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def _star_polygon(
|
|
1398
|
+
cx: float,
|
|
1399
|
+
cy: float,
|
|
1400
|
+
outer_radius: float,
|
|
1401
|
+
inner_radius: float,
|
|
1402
|
+
point_count: int,
|
|
1403
|
+
angle: float,
|
|
1404
|
+
layer: LayerTuple,
|
|
1405
|
+
) -> gdstk.Polygon:
|
|
1406
|
+
points = []
|
|
1407
|
+
for index in range(point_count * 2):
|
|
1408
|
+
radius = outer_radius if index % 2 == 0 else inner_radius
|
|
1409
|
+
theta = angle + math.pi * index / point_count
|
|
1410
|
+
points.append((cx + radius * math.cos(theta), cy + radius * math.sin(theta)))
|
|
1411
|
+
return gdstk.Polygon(points, layer=layer[0], datatype=layer[1])
|
|
1412
|
+
|
|
1413
|
+
|
|
1414
|
+
def _diamond_polygon(cx: float, cy: float, radius: float, layer: LayerTuple) -> gdstk.Polygon:
|
|
1415
|
+
return gdstk.Polygon(
|
|
1416
|
+
((cx, cy + radius), (cx + radius, cy), (cx, cy - radius), (cx - radius, cy)),
|
|
1417
|
+
layer=layer[0],
|
|
1418
|
+
datatype=layer[1],
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def _rect_ring(
|
|
1423
|
+
ox0: float,
|
|
1424
|
+
oy0: float,
|
|
1425
|
+
ox1: float,
|
|
1426
|
+
oy1: float,
|
|
1427
|
+
ix0: float,
|
|
1428
|
+
iy0: float,
|
|
1429
|
+
ix1: float,
|
|
1430
|
+
iy1: float,
|
|
1431
|
+
layer: LayerTuple,
|
|
1432
|
+
) -> tuple[gdstk.Polygon, ...]:
|
|
1433
|
+
return _boolean(_rect(ox0, oy0, ox1, oy1, layer), [_rect(ix0, iy0, ix1, iy1, layer)], "not", layer)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _ellipse_ring(
|
|
1437
|
+
cx: float,
|
|
1438
|
+
cy: float,
|
|
1439
|
+
outer_rx: float,
|
|
1440
|
+
outer_ry: float,
|
|
1441
|
+
inner_rx: float,
|
|
1442
|
+
inner_ry: float,
|
|
1443
|
+
point_count: int,
|
|
1444
|
+
layer: LayerTuple,
|
|
1445
|
+
) -> tuple[gdstk.Polygon, ...]:
|
|
1446
|
+
outer = _ellipse_polygon(cx, cy, outer_rx, outer_ry, point_count, layer)
|
|
1447
|
+
inner = _ellipse_polygon(cx, cy, inner_rx, inner_ry, point_count, layer)
|
|
1448
|
+
return _boolean(outer, [inner], "not", layer)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _boolean(
|
|
1452
|
+
subject: gdstk.Polygon | Sequence[gdstk.Polygon],
|
|
1453
|
+
clip: Sequence[gdstk.Polygon],
|
|
1454
|
+
operation: str,
|
|
1455
|
+
layer: LayerTuple,
|
|
1456
|
+
) -> tuple[gdstk.Polygon, ...]:
|
|
1457
|
+
result = gdstk.boolean(subject, clip, operation, precision=1e-3, layer=layer[0], datatype=layer[1])
|
|
1458
|
+
return tuple(result or ())
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
def _mixed_shape(kind: int, x: float, y: float, index: int, layer: LayerTuple, *, changed: bool) -> gdstk.Polygon:
|
|
1462
|
+
if kind == 0:
|
|
1463
|
+
delta = 0.7 if changed else 0.0
|
|
1464
|
+
return _rect(x + delta, y, x + 8 + delta, y + 5, layer)
|
|
1465
|
+
if kind == 1:
|
|
1466
|
+
radius = 5.5 if changed else 5.0
|
|
1467
|
+
return _ellipse_polygon(x + 5, y + 5, radius, 3.5, 20 + (index % 5), layer, angle=0.1 * (index % 7))
|
|
1468
|
+
angle = 0.4 if changed else 0.0
|
|
1469
|
+
return _star_polygon(x + 5, y + 5, 5.5, 2.0, 5 + (index % 3), angle, layer)
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
def _add(cell: gdstk.Cell, *items: gdstk.Polygon | gdstk.Reference) -> None:
|
|
1473
|
+
for item in items:
|
|
1474
|
+
cell.add(item)
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
def _bbox(poly: gdstk.Polygon) -> BBox:
|
|
1478
|
+
points = poly.points
|
|
1479
|
+
xs = [float(point[0]) for point in points]
|
|
1480
|
+
ys = [float(point[1]) for point in points]
|
|
1481
|
+
return (min(xs), min(ys), max(xs), max(ys))
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
def _bbox_dbu(bbox_um: BBox) -> tuple[int, int, int, int]:
|
|
1485
|
+
return tuple(int(round(value * _DBU_PER_UM)) for value in bbox_um) # type: ignore[return-value]
|
|
1486
|
+
|
|
1487
|
+
|
|
1488
|
+
def _bboxes_intersect(a: tuple[int, int, int, int], b: tuple[int, int, int, int]) -> bool:
|
|
1489
|
+
return a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def _library_stats(lib: gdstk.Library) -> dict[str, int]:
|
|
1493
|
+
top = _top_cell(lib)
|
|
1494
|
+
polygons = top.get_polygons(apply_repetitions=True, include_paths=True)
|
|
1495
|
+
return {
|
|
1496
|
+
"polygon_count": len(polygons),
|
|
1497
|
+
"vertex_count": sum(len(polygon.points) for polygon in polygons),
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
def _top_cell(lib: gdstk.Library) -> gdstk.Cell:
|
|
1502
|
+
for cell in lib.cells:
|
|
1503
|
+
if cell.name == "top":
|
|
1504
|
+
return cell
|
|
1505
|
+
tops = lib.top_level()
|
|
1506
|
+
if len(tops) != 1:
|
|
1507
|
+
raise ValueError(f"synthetic library expected one top cell, found {len(tops)}")
|
|
1508
|
+
return tops[0]
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
def _expected_changed_layers(edits: Sequence[SyntheticEdit]) -> tuple[LayerTuple, ...]:
|
|
1512
|
+
layers = {layer for edit in edits for layer in edit.changed_layers()}
|
|
1513
|
+
return tuple(sorted(layers))
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def _changed_layers_from_report(payload: dict[str, object]) -> tuple[LayerTuple, ...]:
|
|
1517
|
+
layers = []
|
|
1518
|
+
for item in payload.get("layers", ()):
|
|
1519
|
+
if not isinstance(item, dict):
|
|
1520
|
+
continue
|
|
1521
|
+
if str(item.get("xor_area_dbu2", "0")) == "0" and str(item.get("old_minus_new_area_dbu2", "0")) == "0" and str(item.get("new_minus_old_area_dbu2", "0")) == "0":
|
|
1522
|
+
continue
|
|
1523
|
+
layers.append((int(item["layer"]), int(item["datatype"])))
|
|
1524
|
+
return tuple(sorted(layers))
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def _audit_match_from_report(payload: dict[str, object]) -> bool | None:
|
|
1528
|
+
exact_fallback = payload.get("exact_fallback")
|
|
1529
|
+
if not isinstance(exact_fallback, dict):
|
|
1530
|
+
return None
|
|
1531
|
+
audit = exact_fallback.get("cpu_oracle_audit")
|
|
1532
|
+
if not isinstance(audit, dict):
|
|
1533
|
+
return None
|
|
1534
|
+
match = audit.get("match")
|
|
1535
|
+
return bool(match) if match is not None else None
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
def _float_or_none(value: object) -> float | None:
|
|
1539
|
+
if value is None:
|
|
1540
|
+
return None
|
|
1541
|
+
try:
|
|
1542
|
+
return float(value)
|
|
1543
|
+
except (TypeError, ValueError):
|
|
1544
|
+
return None
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
def _format_layer(layer: LayerTuple) -> str:
|
|
1548
|
+
return f"{layer[0]}:{layer[1]}"
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def _format_layers(layers: Iterable[LayerTuple]) -> str:
|
|
1552
|
+
return ",".join(_format_layer(layer) for layer in layers)
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
def _format_seconds(value: float | None) -> str:
|
|
1556
|
+
return "n/a" if value is None else f"{value:.4f}"
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _json_dumps(payload: object) -> str:
|
|
1560
|
+
return json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def validate_synthetic_case_with_cpu_oracle(record: SyntheticCaseRecord) -> dict[str, object]:
|
|
1564
|
+
old = extract_gds_polygons(record.before_path).buffer
|
|
1565
|
+
new = extract_gds_polygons(record.after_path).buffer
|
|
1566
|
+
result = run_full_exact_oracle(old, new)
|
|
1567
|
+
by_layer = {layer.layer: layer for layer in result.layers}
|
|
1568
|
+
changed_layers = tuple(sorted(layer for layer, diff in by_layer.items() if diff.xor_twice_area))
|
|
1569
|
+
expected_layers = tuple(LayerSpec(*layer) for layer in record.expected_changed_layers)
|
|
1570
|
+
missing_layers = tuple(layer for layer in expected_layers if layer not in changed_layers)
|
|
1571
|
+
missing_windows: list[dict[str, object]] = []
|
|
1572
|
+
for edit in record.edits:
|
|
1573
|
+
if not edit.coverage_changed:
|
|
1574
|
+
continue
|
|
1575
|
+
for layer_key, bbox_um, side in _validation_sides(edit):
|
|
1576
|
+
if layer_key is None or bbox_um is None:
|
|
1577
|
+
continue
|
|
1578
|
+
diff = by_layer.get(LayerSpec(*layer_key))
|
|
1579
|
+
if diff is None:
|
|
1580
|
+
missing_windows.append({"operation_id": edit.operation_id, "layer": layer_key, "side": side, "reason": "layer missing"})
|
|
1581
|
+
continue
|
|
1582
|
+
fragments = diff.old_minus_new if side == "before" else diff.new_minus_old
|
|
1583
|
+
bbox_dbu = _bbox_dbu(bbox_um)
|
|
1584
|
+
if fragments and not any(_bboxes_intersect(fragment.bbox, bbox_dbu) for fragment in fragments):
|
|
1585
|
+
missing_windows.append({"operation_id": edit.operation_id, "layer": layer_key, "side": side, "bbox_dbu": bbox_dbu})
|
|
1586
|
+
return {
|
|
1587
|
+
"equivalent": result.equivalent,
|
|
1588
|
+
"expected_equivalent": record.equivalent_expected,
|
|
1589
|
+
"changed_layers": [[layer.layer, layer.datatype] for layer in changed_layers],
|
|
1590
|
+
"expected_changed_layers": [list(layer) for layer in record.expected_changed_layers],
|
|
1591
|
+
"missing_layers": [[layer.layer, layer.datatype] for layer in missing_layers],
|
|
1592
|
+
"missing_windows": missing_windows,
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
|
|
1596
|
+
def _validation_sides(edit: SyntheticEdit) -> tuple[tuple[LayerTuple | None, BBox | None, str], ...]:
|
|
1597
|
+
if edit.expected_direction == "coverage_before_only":
|
|
1598
|
+
return ((edit.before_layer, edit.before_bbox_um, "before"),)
|
|
1599
|
+
if edit.expected_direction == "coverage_after_only":
|
|
1600
|
+
return ((edit.after_layer, edit.after_bbox_um, "after"),)
|
|
1601
|
+
if edit.expected_direction == "coverage_both":
|
|
1602
|
+
return (
|
|
1603
|
+
(edit.before_layer, edit.before_bbox_um, "before"),
|
|
1604
|
+
(edit.after_layer, edit.after_bbox_um, "after"),
|
|
1605
|
+
)
|
|
1606
|
+
return ()
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
1610
|
+
parser = argparse.ArgumentParser(description="Generate and optionally benchmark synthetic gdsdiff readiness GDS pairs.")
|
|
1611
|
+
parser.add_argument("output_dir", type=Path)
|
|
1612
|
+
parser.add_argument("--scale", action="append", choices=tuple(SCALE_PROFILES), help="Scale profile to generate. May be repeated.")
|
|
1613
|
+
parser.add_argument("--case", action="append", dest="cases", help="Generate only the named case. May be repeated.")
|
|
1614
|
+
parser.add_argument("--benchmark", action="store_true", help="Run CPU and Metal gdsdiff comparisons after generation.")
|
|
1615
|
+
parser.add_argument("--no-cpu", action="store_true", help="Skip CPU oracle benchmark runs.")
|
|
1616
|
+
parser.add_argument("--no-metal", action="store_true", help="Skip Metal benchmark runs.")
|
|
1617
|
+
parser.add_argument("--no-metal-audit", action="store_true", help="Skip Metal runs with CPU audit.")
|
|
1618
|
+
parser.add_argument("--stress-cpu-case-limit", type=int, default=2)
|
|
1619
|
+
args = parser.parse_args(argv)
|
|
1620
|
+
|
|
1621
|
+
scales = tuple(args.scale or ("benchmark",))
|
|
1622
|
+
all_records: list[SyntheticCaseRecord] = []
|
|
1623
|
+
for scale in scales:
|
|
1624
|
+
scale_dir = args.output_dir / scale if len(scales) > 1 else args.output_dir
|
|
1625
|
+
all_records.extend(build_synthetic_suite(scale_dir, scale=scale, case_names=args.cases))
|
|
1626
|
+
|
|
1627
|
+
if args.benchmark:
|
|
1628
|
+
runs = benchmark_synthetic_records(
|
|
1629
|
+
all_records,
|
|
1630
|
+
run_cpu=not args.no_cpu,
|
|
1631
|
+
run_metal=not args.no_metal,
|
|
1632
|
+
run_metal_audit=not args.no_metal_audit,
|
|
1633
|
+
stress_cpu_case_limit=args.stress_cpu_case_limit,
|
|
1634
|
+
)
|
|
1635
|
+
summary = write_benchmark_summary(args.output_dir, all_records, runs)
|
|
1636
|
+
print(f"benchmark_summary={summary}")
|
|
1637
|
+
for record in all_records:
|
|
1638
|
+
print(f"{record.scale}/{record.name}: {record.before_path} {record.after_path}")
|
|
1639
|
+
return 0
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
if __name__ == "__main__":
|
|
1643
|
+
raise SystemExit(main(sys.argv[1:]))
|