-
Notifications
You must be signed in to change notification settings - Fork 187
/
difftest.py
332 lines (274 loc) · 11.6 KB
/
difftest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
"""GDS regression test. Inspired by lytest."""
import filecmp
import pathlib
import shutil
import gdsfactory as gf
from gdsfactory.config import CONF, PATH, logger
from gdsfactory.name import clean_name
class GeometryDifference(Exception):
pass
PathType = pathlib.Path | str
def diff(
ref_file: PathType,
run_file: PathType,
xor: bool = True,
test_name: str = "",
ignore_sliver_differences: bool | None = None,
ignore_cell_name_differences: bool | None = None,
ignore_label_differences: bool | None = None,
show: bool = True,
) -> bool:
"""Returns True if files are different, prints differences and shows them in klayout.
Args:
ref_file: reference (old) file.
run_file: run (new) file.
xor: runs xor on every layer between ref and run files.
test_name: prefix for the new cell.
ignore_sliver_differences: if True, ignores any sliver differences in the XOR result. If None (default), defers to the value set in CONF.difftest_ignore_sliver_differences
ignore_cell_name_differences: if True, ignores any cell name differences. If None (default), defers to the value set in CONF.difftest_ignore_cell_name_differences
ignore_label_differences: if True, ignores any label differences when run in XOR mode. If None (default) defers to the value set in CONF.difftest_ignore_label_differences
show: shows diff in klayout.
"""
try:
from kfactory import KCell, kdb
except ImportError as e:
print(
"You can install `pip install gdsfactory[kfactory]` for using maskprep. "
"And make sure you use python >= 3.10"
)
raise e
ref = read_top_cell(ref_file)
run = read_top_cell(run_file)
if ignore_sliver_differences is None:
ignore_sliver_differences = CONF.difftest_ignore_sliver_differences
if ignore_cell_name_differences is None:
ignore_cell_name_differences = CONF.difftest_ignore_cell_name_differences
if ignore_label_differences is None:
ignore_label_differences = CONF.difftest_ignore_label_differences
if ref.kcl.dbu != run.kcl.dbu:
raise ValueError(
f"dbu is different in ref {ref.kcl.dbu} and run {run.kcl.dbu} files"
)
equivalent = True
ld = kdb.LayoutDiff()
a_regions: dict[int, kdb.Region] = {}
a_texts: dict[int, kdb.Texts] = {}
b_regions: dict[int, kdb.Region] = {}
b_texts: dict[int, kdb.Texts] = {}
def get_region(key, regions: dict[int, kdb.Region]) -> kdb.Region:
if key not in regions:
reg = kdb.Region()
regions[key] = reg
return reg
else:
return regions[key]
def get_texts(key, texts_dict: dict[int, kdb.Texts]) -> kdb.Texts:
if key not in texts_dict:
texts = kdb.Texts()
texts_dict[key] = texts
return texts
else:
return texts_dict[key]
def polygon_diff_a(anotb: kdb.Polygon, prop_id: int):
get_region(ld.layer_index_a(), a_regions).insert(anotb)
def polygon_diff_b(bnota: kdb.Polygon, prop_id: int):
get_region(ld.layer_index_b(), b_regions).insert(bnota)
def cell_diff_a(cell: kdb.Cell):
nonlocal equivalent
print(f"{cell.name} only in old")
if not ignore_cell_name_differences:
equivalent = False
def cell_diff_b(cell: kdb.Cell):
nonlocal equivalent
print(f"{cell.name} only in new")
if not ignore_cell_name_differences:
equivalent = False
def text_diff_a(anotb: kdb.Text, prop_id: int):
print("Text only in old")
get_texts(ld.layer_index_a(), a_texts).insert(anotb)
def text_diff_b(bnota: kdb.Text, prop_id: int):
print("Text only in new")
get_texts(ld.layer_index_b(), b_texts).insert(bnota)
ld.on_cell_in_a_only = lambda anotb: cell_diff_a(anotb)
ld.on_cell_in_b_only = lambda anotb: cell_diff_b(anotb)
ld.on_text_in_a_only = lambda anotb, prop_id: text_diff_a(anotb, prop_id)
ld.on_text_in_b_only = lambda anotb, prop_id: text_diff_b(anotb, prop_id)
ld.on_polygon_in_a_only = lambda anotb, prop_id: polygon_diff_a(anotb, prop_id)
ld.on_polygon_in_b_only = lambda anotb, prop_id: polygon_diff_b(anotb, prop_id)
if ignore_cell_name_differences:
ld.on_cell_name_differs = lambda anotb: print(f"cell name differs {anotb.name}")
equal = ld.compare(
ref._kdb_cell,
run._kdb_cell,
kdb.LayoutDiff.SmartCellMapping | kdb.LayoutDiff.Verbose,
1,
)
else:
equal = ld.compare(ref._kdb_cell, run._kdb_cell, kdb.LayoutDiff.Verbose, 1)
if not ignore_label_differences:
if a_texts or b_texts:
equivalent = False
if not equal:
c = KCell(f"{test_name}_difftest")
refdiff = KCell(f"{test_name}_old")
rundiff = KCell(f"{test_name}_new")
# TODO: add suffix new and old
refdiff.copy_tree(ref._kdb_cell)
rundiff.copy_tree(run._kdb_cell)
_ = c << refdiff
_ = c << rundiff
if xor:
print("Running XOR on differences...")
# assume equivalence until we find XOR differences, determined significant by the settings
diff = KCell(f"{test_name}_xor")
for layer in c.kcl.layer_infos():
# exists in both
if layer in run.kcl.layer_infos() and layer in ref.kcl.layer_infos():
layer_ref = ref.layer(layer)
layer_run = run.layer(layer)
region_run = kdb.Region(run.begin_shapes_rec(layer_run))
region_ref = kdb.Region(ref.begin_shapes_rec(layer_ref))
region_diff = region_run ^ region_ref
if not region_diff.is_empty():
layer_id = c.layer(layer)
region_xor = region_ref ^ region_run
diff.shapes(layer_id).insert(region_xor)
xor_w_tolerance = region_xor.sized(-1)
is_sliver = xor_w_tolerance.is_empty()
message = f"{test_name}: XOR difference on layer {layer}"
if is_sliver:
message += " (sliver)"
if not ignore_sliver_differences:
equivalent = False
else:
equivalent = False
print(message)
# only in run
elif layer in run.kcl.layer_infos():
layer_id = run.layer(layer)
region = kdb.Region(run.begin_shapes_rec(layer_id))
diff.shapes(c.kcl.layer(layer)).insert(region)
print(f"{test_name}: layer {layer} only exists in updated cell")
equivalent = False
# only in ref
elif layer in ref.kcl.layer_infos():
layer_id = ref.layer(layer)
region = kdb.Region(ref.begin_shapes_rec(layer_id))
diff.shapes(c.kcl.layer(layer)).insert(region)
print(f"{test_name}: layer {layer} missing from updated cell")
equivalent = False
_ = c << diff
if equivalent:
print("No significant XOR differences between layouts!")
else:
# if no additional xor verificaiton, the two files are not equivalent
equivalent = False
if show:
c.show()
if equivalent:
return False
else:
return True
return False
def difftest(
component: gf.Component,
test_name: gf.Component | None = None,
dirpath: pathlib.Path = PATH.gds_ref,
xor: bool = True,
dirpath_run: pathlib.Path = PATH.gds_run,
ignore_sliver_differences: bool | None = None,
) -> None:
"""Avoids GDS regressions tests on the GeometryDifference.
If files are the same it returns None. If files are different runs XOR
between new component and the GDS reference stored in dirpath and
raises GeometryDifference if there are differences and show differences in KLayout.
If it runs for the fist time it just stores the GDS reference.
Args:
component: to test if it has changed.
test_name: used to store the GDS file.
dirpath: directory where reference files are stored.
xor: runs XOR.
dirpath_run: directory to store gds file generated by the test.
ignore_sliver_differences: if True, ignores any sliver differences in the XOR result. If None (default), defers to the value set in CONF.difftest_ignore_sliver_differences
"""
test_name = test_name or (
f"{component.function_name}_{component.name}"
if hasattr(component, "function_name")
and component.name != component.function_name
else f"{component.name}"
)
filename = f"{test_name}.gds"
dirpath_ref = dirpath
dirpath_ref.mkdir(exist_ok=True, parents=True)
dirpath_run.mkdir(exist_ok=True, parents=True)
ref_file = dirpath_ref / f"{clean_name(test_name)}.gds"
run_file = dirpath_run / filename
component = gf.get_component(component)
run_file = component.write_gds(gdspath=run_file)
if not ref_file.exists():
shutil.copy(run_file, ref_file)
raise AssertionError(
f"Reference GDS file for {test_name!r} not found. Writing to {ref_file!r}"
)
if filecmp.cmp(ref_file, run_file, shallow=False):
return
if diff(
ref_file=ref_file,
run_file=run_file,
xor=xor,
test_name=test_name,
ignore_sliver_differences=ignore_sliver_differences,
):
print(
f"\ngds_run {filename!r} changed from gds_ref {str(ref_file)!r}\n"
"You can check the differences in Klayout GUI or run XOR with\n"
f"gf gds-diff --xor {ref_file} {run_file}\n"
)
try:
overwrite(ref_file, run_file)
except OSError as exc:
raise GeometryDifference(
"\n"
f"{filename!r} changed from reference {str(ref_file)!r}. "
"Run `pytest -s` to step and check differences in klayout GUI."
) from exc
def overwrite(ref_file, run_file):
val = input("Save current GDS as the new reference (Y)? [Y/n]")
if val.upper().startswith("N"):
raise GeometryDifference
logger.info(f"deleting file {str(ref_file)!r}")
ref_file.unlink()
shutil.copy(run_file, ref_file)
raise GeometryDifference
def read_top_cell(arg0):
from kfactory import KCLayout
kcl = KCLayout(name=str(arg0))
kcl.read(arg0)
return kcl[kcl.top_cell().name]
if __name__ == "__main__":
# print([i.name for i in c.get_dependencies()])
# c.show()
# c.name = "mzi"
c = gf.components.straight(length=20, layer=(1, 0))
c.name = "a:demo"
c.show()
difftest(c, "straight", dirpath=PATH.cwd)
# component = gf.components.mzi()
# test_name = "mzi"
# filename = f"{test_name}.gds"
# dirpath = PATH.cwd
# dirpath_ref = dirpath / "gds_ref"
# dirpath_run = GDSDIR_TEMP
# ref_file = dirpath_ref / f"{test_name}.gds"
# run_file = dirpath_run / filename
# run = gf.get_component(component)
# run_file = run.write_gds(gdspath=run_file)
# if not ref_file.exists():
# component.write_gds(gdspath=ref_file)
# raise AssertionError(
# f"Reference GDS file for {test_name!r} not found. Writing to {ref_file!r}"
# )
# ref = read_top_cell(ref_file)
# run = read_top_cell(run_file)
# ld = kdb.LayoutDiff()
# print(ld.compare(ref._kdb_cell, run._kdb_cell))