Coverage for klayout_pex/klayout/lvsdb_extractor.py: 56%

260 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-04 15:43 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX  

7# (see https://github.com/iic-jku/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24from __future__ import annotations 

25 

26from collections import defaultdict 

27from dataclasses import dataclass 

28from functools import cached_property 

29import tempfile 

30from typing import * 

31 

32from rich.pretty import pprint 

33 

34import klayout.db as kdb 

35 

36from ..log import ( 

37 console, 

38 debug, 

39 info, 

40 warning, 

41 error, 

42 rule 

43) 

44 

45from .shapes_pb2_converter import ShapesConverter 

46 

47from ..tech_info import TechInfo 

48import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2 

49import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2 

50import klayout_pex_protobuf.kpex.layout.pin_pb2 as pin_pb2 

51import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2 

52import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

53 

54GDSPair = Tuple[int, int] 

55 

56LayerIndexMap = Dict[int, int] # maps layer indexes of LVSDB to annotated_layout 

57LVSDBRegions = Dict[int, kdb.Region] # maps layer index of annotated_layout to LVSDB region 

58 

59 

60@dataclass 

61class KLayoutExtractedLayerInfo: 

62 index: int 

63 lvs_layer_name: str # NOTE: this can be computed, so gds_pair is preferred 

64 gds_pair: GDSPair 

65 region: kdb.Region 

66 

67 

68@dataclass 

69class KLayoutMergedExtractedLayerInfo: 

70 source_layers: List[KLayoutExtractedLayerInfo] 

71 gds_pair: GDSPair 

72 

73 

74@dataclass 

75class KLayoutExtractionContext: 

76 lvsdb: kdb.LayoutToNetlist 

77 tech: TechInfo 

78 dbu: float 

79 layer_index_map: LayerIndexMap 

80 lvsdb_regions: LVSDBRegions 

81 cell_mapping: kdb.CellMapping 

82 annotated_top_cell: kdb.Cell 

83 annotated_layout: kdb.Layout 

84 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] 

85 unnamed_layers: List[KLayoutExtractedLayerInfo] 

86 

87 @classmethod 

88 def prepare_extraction(cls, 

89 lvsdb: kdb.LayoutToNetlist, 

90 top_cell: str, 

91 tech: TechInfo, 

92 blackbox_devices: bool) -> KLayoutExtractionContext: 

93 dbu = lvsdb.internal_layout().dbu 

94 annotated_layout = kdb.Layout() 

95 annotated_layout.dbu = dbu 

96 top_cell = annotated_layout.create_cell(top_cell) 

97 

98 # CellMapping 

99 # mapping of internal layout to target layout for the circuit mapping 

100 # https://www.klayout.de/doc-qt5/code/class_CellMapping.html 

101 # --- 

102 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18 

103 # Creates a cell mapping for copying shapes from the internal layout to the given target layout 

104 cm = lvsdb.cell_mapping_into(annotated_layout, # target layout 

105 top_cell, 

106 not blackbox_devices) # with_device_cells 

107 

108 lvsdb_regions, layer_index_map = cls.build_LVS_layer_map(annotated_layout=annotated_layout, 

109 lvsdb=lvsdb, 

110 tech=tech, 

111 blackbox_devices=blackbox_devices) 

112 

113 # NOTE: GDS only supports integer properties to GDS, 

114 # as GDS does not support string keys, 

115 # like OASIS does. 

116 net_name_prop = "net" 

117 

118 # Build a full hierarchical representation of the nets 

119 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method14 

120 # hier_mode = None 

121 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten 

122 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells 

123 

124 lvsdb.build_all_nets( 

125 cmap=cm, # mapping of internal layout to target layout for the circuit mapping 

126 target=annotated_layout, # target layout 

127 lmap=lvsdb_regions, # maps: target layer index => net regions 

128 hier_mode=hier_mode, # hier mode 

129 netname_prop=net_name_prop, # property name to which to attach the net name 

130 circuit_cell_name_prefix="CIRCUIT_", # NOTE: generates a cell for each circuit 

131 net_cell_name_prefix=None, # NOTE: this would generate a cell for each net 

132 device_cell_name_prefix=None # NOTE: this would create a cell for each device (e.g. transistor) 

133 ) 

134 

135 extracted_layers, unnamed_layers = cls.nonempty_extracted_layers(lvsdb=lvsdb, 

136 tech=tech, 

137 annotated_layout=annotated_layout, 

138 layer_index_map=layer_index_map, 

139 blackbox_devices=blackbox_devices) 

140 

141 return KLayoutExtractionContext( 

142 lvsdb=lvsdb, 

143 tech=tech, 

144 dbu=dbu, 

145 annotated_top_cell=top_cell, 

146 layer_index_map=layer_index_map, 

147 lvsdb_regions=lvsdb_regions, 

148 cell_mapping=cm, 

149 annotated_layout=annotated_layout, 

150 extracted_layers=extracted_layers, 

151 unnamed_layers=unnamed_layers 

152 ) 

153 

154 @staticmethod 

155 def build_LVS_layer_map(annotated_layout: kdb.Layout, 

156 lvsdb: kdb.LayoutToNetlist, 

157 tech: TechInfo, 

158 blackbox_devices: bool) -> Tuple[LVSDBRegions, LayerIndexMap]: 

159 # NOTE: currently, the layer numbers are auto-assigned 

160 # by the sequence they occur in the LVS script, hence not well defined! 

161 # build a layer map for the layers that correspond to original ones. 

162 

163 # https://www.klayout.de/doc-qt5/code/class_LayerInfo.html 

164 lvsdb_regions: LVSDBRegions = {} 

165 layer_index_map: LayerIndexMap = {} 

166 

167 if not hasattr(lvsdb, "layer_indexes"): 

168 raise Exception("Needs at least KLayout version 0.29.2") 

169 

170 for layer_index in lvsdb.layer_indexes(): 

171 lname = lvsdb.layer_name(layer_index) 

172 

173 computed_layer_info = tech.computed_layer_info_by_name.get(lname, None) 

174 if computed_layer_info and blackbox_devices: 

175 match computed_layer_info.kind: 

176 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

177 continue 

178 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

179 continue 

180 

181 gds_pair = tech.gds_pair_for_computed_layer_name.get(lname, None) 

182 if not gds_pair: 

183 li = lvsdb.internal_layout().get_info(layer_index) 

184 if li != kdb.LayerInfo(): 

185 gds_pair = (li.layer, li.datatype) 

186 

187 if gds_pair is not None: 

188 annotated_layer_index = annotated_layout.layer() # creates new index each time! 

189 # Creates a new internal layer! because multiple layers with the same gds_pair are possible! 

190 annotated_layout.set_info(annotated_layer_index, kdb.LayerInfo(*gds_pair)) 

191 region = lvsdb.layer_by_index(layer_index) 

192 lvsdb_regions[annotated_layer_index] = region 

193 layer_index_map[layer_index] = annotated_layer_index 

194 

195 return lvsdb_regions, layer_index_map 

196 

197 @staticmethod 

198 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist, 

199 tech: TechInfo, 

200 annotated_layout: kdb.Layout, 

201 layer_index_map: LayerIndexMap, 

202 blackbox_devices: bool) -> Tuple[Dict[GDSPair, KLayoutMergedExtractedLayerInfo], List[KLayoutExtractedLayerInfo]]: 

203 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18 

204 nonempty_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] = {} 

205 

206 unnamed_layers: List[KLayoutExtractedLayerInfo] = [] 

207 lvsdb_layer_indexes = lvsdb.layer_indexes() 

208 for idx, ln in enumerate(lvsdb.layer_names()): 

209 li = lvsdb_layer_indexes[idx] 

210 if li not in layer_index_map: 

211 continue 

212 li = layer_index_map[li] 

213 layer = kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li)) 

214 layer.enable_properties() 

215 if layer.count() >= 1: 

216 computed_layer_info = tech.computed_layer_info_by_name.get(ln, None) 

217 if not computed_layer_info: 

218 warning(f"Unable to find info about extracted LVS layer '{ln}'") 

219 gds_pair = (1000 + idx, 20) 

220 linfo = KLayoutExtractedLayerInfo( 

221 index=idx, 

222 lvs_layer_name=ln, 

223 gds_pair=gds_pair, 

224 region=layer 

225 ) 

226 unnamed_layers.append(linfo) 

227 continue 

228 

229 if blackbox_devices: 

230 match computed_layer_info.kind: 

231 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

232 continue 

233 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

234 continue 

235 

236 gds_pair = (computed_layer_info.layer_info.drw_gds_pair.layer, 

237 computed_layer_info.layer_info.drw_gds_pair.datatype) 

238 

239 linfo = KLayoutExtractedLayerInfo( 

240 index=idx, 

241 lvs_layer_name=ln, 

242 gds_pair=gds_pair, 

243 region=layer 

244 ) 

245 

246 entry = nonempty_layers.get(gds_pair, None) 

247 if entry: 

248 entry.source_layers.append(linfo) 

249 else: 

250 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo( 

251 source_layers=[linfo], 

252 gds_pair=gds_pair, 

253 ) 

254 

255 return nonempty_layers, unnamed_layers 

256 

257 def top_cell_bbox(self) -> kdb.Box: 

258 b1: kdb.Box = self.annotated_layout.top_cell().bbox() 

259 b2: kdb.Box = self.lvsdb.internal_layout().top_cell().bbox() 

260 if b1.area() > b2.area(): 

261 return b1 

262 else: 

263 return b2 

264 

265 def shapes_of_net(self, gds_pair: GDSPair, net: kdb.Net | str) -> Optional[kdb.Region]: 

266 lyr = self.extracted_layers.get(gds_pair, None) 

267 if not lyr: 

268 return None 

269 

270 shapes = kdb.Region() 

271 shapes.enable_properties() 

272 

273 requested_net_name = net.name if isinstance(net, kdb.Net) else net 

274 

275 def add_shapes_from_region(source_region: kdb.Region): 

276 iter, transform = source_region.begin_shapes_rec() 

277 while not iter.at_end(): 

278 shape = iter.shape() 

279 net_name = shape.property('net') 

280 if net_name == requested_net_name: 

281 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation 

282 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy) 

283 shape.polygon) 

284 iter.next() 

285 

286 match len(lyr.source_layers): 

287 case 0: 

288 raise AssertionError('Internal error: Empty list of source_layers') 

289 case _: 

290 for sl in lyr.source_layers: 

291 add_shapes_from_region(sl.region) 

292 

293 return shapes 

294 

295 def shapes_of_layer(self, gds_pair: GDSPair) -> Optional[kdb.Region]: 

296 lyr = self.extracted_layers.get(gds_pair, None) 

297 if not lyr: 

298 return None 

299 

300 shapes: kdb.Region 

301 

302 match len(lyr.source_layers): 

303 case 0: 

304 raise AssertionError('Internal error: Empty list of source_layers') 

305 case 1: 

306 shapes = lyr.source_layers[0].region 

307 case _: 

308 # NOTE: currently a bug, for now use polygon-per-polygon workaround 

309 # shapes = kdb.Region() 

310 # for sl in lyr.source_layers: 

311 # shapes += sl.region 

312 shapes = kdb.Region() 

313 shapes.enable_properties() 

314 for sl in lyr.source_layers: 

315 iter, transform = sl.region.begin_shapes_rec() 

316 while not iter.at_end(): 

317 p = kdb.PolygonWithProperties(iter.shape().polygon, {'net': iter.shape().property('net')}) 

318 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation 

319 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy) 

320 p) 

321 iter.next() 

322 

323 return shapes 

324 

325 def pins_of_layer(self, gds_pair: GDSPair) -> kdb.Region: 

326 pin_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].pin_gds_pair 

327 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype 

328 lyr = self.extracted_layers.get(pin_gds_pair, None) 

329 if lyr is None: 

330 return kdb.Region() 

331 if len(lyr.source_layers) != 1: 

332 raise NotImplementedError(f"currently only supporting 1 pin layer mapping, " 

333 f"but got {len(lyr.source_layers)}") 

334 return lyr.source_layers[0].region 

335 

336 def labels_of_layer(self, gds_pair: GDSPair) -> kdb.Texts: 

337 labels_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].label_gds_pair 

338 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype 

339 

340 lay: kdb.Layout = self.annotated_layout 

341 label_layer_idx = lay.find_layer(labels_gds_pair) # sky130 layer dt = 5 

342 if label_layer_idx is None: 

343 return kdb.Texts() 

344 

345 sh_it = lay.begin_shapes(self.lvsdb.internal_top_cell(), label_layer_idx) 

346 labels: kdb.Texts = kdb.Texts(sh_it) 

347 return labels 

348 

349 @cached_property 

350 def top_circuit(self) -> kdb.Circuit: 

351 return self.lvsdb.netlist().top_circuit() 

352 

353 @cached_property 

354 def devices_by_name(self) -> Dict[str, device_pb2.Device]: 

355 dd = {} 

356 

357 shapes_converter = ShapesConverter(dbu=self.dbu) 

358 

359 for d_kly in self.top_circuit.each_device(): 

360 # https://www.klayout.de/doc-qt5/code/class_Device.html 

361 d_kly: kdb.Device 

362 

363 d = device_pb2.Device() 

364 d.id = d_kly.id() 

365 d.device_name = d_kly.expanded_name() 

366 d.device_class_name = d_kly.device_class().name 

367 d.device_abstract_name = d_kly.device_abstract.name 

368 

369 for pd in d_kly.device_class().parameter_definitions(): 

370 p = d.parameters.add() 

371 p.id = pd.id() 

372 p.name = pd.name 

373 p.value = d_kly.parameter(pd.id()) 

374 

375 for td in d_kly.device_class().terminal_definitions(): 

376 n: kdb.Net = d_kly.net_for_terminal(td.id()) 

377 if n is None: 

378 warning(f"Skipping terminal {td.name} of device {d.name} ({d.device_class}) " 

379 f"is not connected to any net") 

380 terminal = d.terminals.add() 

381 terminal.id = td.id() 

382 terminal.name = td.name 

383 terminal.net_name = '' # TODO 

384 continue 

385 

386 for nt in n.each_terminal(): 

387 nt: kdb.NetTerminalRef 

388 

389 if nt.device().expanded_name() != d_kly.expanded_name(): 

390 continue 

391 if nt.terminal_id() != td.id(): 

392 continue 

393 

394 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt) 

395 

396 terminal = d.terminals.add() 

397 terminal.device_id = d.id 

398 terminal.terminal_id = td.id() 

399 terminal.name = td.name 

400 terminal.net_name = n.name 

401 

402 for idx, shapes in shapes_by_lyr_idx.items(): 

403 lyr_idx = self.layer_index_map[idx] 

404 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[lyr_idx] 

405 

406 region_by_layer = terminal.region_by_layer.add() 

407 region_by_layer.layer.id = lyr_idx 

408 region_by_layer.layer.canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lyr_info.layer, lyr_info.datatype] 

409 

410 shapes_converter.klayout_region_to_pb(shapes, region_by_layer.region) 

411 

412 dd[d.device_name] = d 

413 

414 return dd 

415 

416 @cached_property 

417 def pins_pb2_by_layer(self) -> Dict[GDSPair, List[pin_pb2.Pin]]: 

418 d = defaultdict(list) 

419 

420 for lvs_gds_pair, lyr_info in self.extracted_layers.items(): 

421 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair] 

422 # NOTE: LVS GDS Pair differs from real GDS Pair, 

423 # as in some cases we want to split a layer into different regions (ptap vs ntap, cap vs ncap) 

424 # so invent new datatype numbers, like adding 100 to the real GDS datatype 

425 gds_pair = self.tech.gds_pair_for_layer_name.get(canonical_layer_name, None) 

426 if gds_pair is None: 

427 continue 

428 if gds_pair not in self.tech.layer_info_by_gds_pair: 

429 continue 

430 

431 for lyr in lyr_info.source_layers: 

432 klayout_index = self.annotated_layout.layer(*lyr.gds_pair) 

433 

434 pins = self.pins_of_layer(gds_pair) 

435 labels = self.labels_of_layer(gds_pair) 

436 

437 pin_labels: kdb.Texts = labels & pins 

438 for l in pin_labels: 

439 l: kdb.Text 

440 # NOTE: because we want more like a point as a junction 

441 # and folx create huge pins (covering the whole metal) 

442 # we create our own "mini squares" 

443 # (ResistorExtractor will subtract the pins from the metal polygons, 

444 # so in the extreme case the polygons could become empty) 

445 

446 pin = pin_pb2.Pin() 

447 pin.label = l.string 

448 

449 pos = l.position() 

450 

451 # is there more elegant / faster way to do this? 

452 for p in pins: 

453 p: kdb.PolygonWithProperties 

454 if p.inside(pos): 

455 pin.net_name = p.property('net') 

456 break 

457 

458 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lyr.gds_pair] 

459 lvs_layer_name = self.tech.computed_layer_info_by_gds_pair[lyr.gds_pair].layer_info.name 

460 pin.layer.id = klayout_index 

461 pin.layer.canonical_layer_name = canonical_layer_name 

462 pin.layer.lvs_layer_name = lvs_layer_name 

463 

464 pin.label_point.x = pos.x 

465 pin.label_point.y = pos.y 

466 pin.label_point.net = pin.net_name 

467 

468 d[gds_pair].append(pin) 

469 

470 return d