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

222 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-12 13:45 +0000

1# 

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

3# SPDX-FileCopyrightText: 2024 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/martinjankoehler/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 dataclasses import dataclass 

27from functools import cached_property 

28import tempfile 

29from typing import * 

30 

31from rich.pretty import pprint 

32 

33import klayout.db as kdb 

34 

35import klayout_pex_protobuf.tech_pb2 as tech_pb2 

36from ..log import ( 

37 console, 

38 debug, 

39 info, 

40 warning, 

41 error, 

42 rule 

43) 

44 

45from ..tech_info import TechInfo 

46 

47 

48GDSPair = Tuple[int, int] 

49 

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

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

52 

53 

54@dataclass 

55class KLayoutExtractedLayerInfo: 

56 index: int 

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

58 gds_pair: GDSPair 

59 region: kdb.Region 

60 

61 

62@dataclass 

63class KLayoutMergedExtractedLayerInfo: 

64 source_layers: List[KLayoutExtractedLayerInfo] 

65 gds_pair: GDSPair 

66 

67 

68@dataclass 

69class KLayoutDeviceTerminal: 

70 id: int 

71 name: str 

72 regions_by_layer_name: Dict[str, kdb.Region] 

73 net_name: str 

74 

75 # internal data access 

76 net_terminal_ref: Optional[kdb.NetTerminalRef] 

77 net: Optional[kdb.Net] 

78 

79 

80@dataclass 

81class KLayoutDeviceTerminalList: 

82 terminals: List[KLayoutDeviceTerminal] 

83 

84 

85@dataclass 

86class KLayoutDeviceInfo: 

87 id: str 

88 name: str # expanded name 

89 class_name: str 

90 abstract_name: str 

91 

92 terminals: KLayoutDeviceTerminalList 

93 params: Dict[str, str] 

94 

95 # internal data access 

96 device: kdb.Device 

97 

98 

99@dataclass 

100class KLayoutExtractionContext: 

101 lvsdb: kdb.LayoutToNetlist 

102 tech: TechInfo 

103 dbu: float 

104 layer_index_map: LayerIndexMap 

105 lvsdb_regions: LVSDBRegions 

106 cell_mapping: kdb.CellMapping 

107 annotated_top_cell: kdb.Cell 

108 annotated_layout: kdb.Layout 

109 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] 

110 unnamed_layers: List[KLayoutExtractedLayerInfo] 

111 

112 @classmethod 

113 def prepare_extraction(cls, 

114 lvsdb: kdb.LayoutToNetlist, 

115 top_cell: str, 

116 tech: TechInfo, 

117 blackbox_devices: bool) -> KLayoutExtractionContext: 

118 dbu = lvsdb.internal_layout().dbu 

119 annotated_layout = kdb.Layout() 

120 annotated_layout.dbu = dbu 

121 top_cell = annotated_layout.create_cell(top_cell) 

122 

123 # CellMapping 

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

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

126 # --- 

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

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

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

130 top_cell, 

131 not blackbox_devices) # with_device_cells 

132 

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

134 lvsdb=lvsdb, 

135 tech=tech, 

136 blackbox_devices=blackbox_devices) 

137 

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

139 # as GDS does not support string keys, 

140 # like OASIS does. 

141 net_name_prop = "net" 

142 

143 # Build a full hierarchical representation of the nets 

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

145 # hier_mode = None 

146 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten 

147 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells 

148 

149 lvsdb.build_all_nets( 

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

151 target=annotated_layout, # target layout 

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

153 hier_mode=hier_mode, # hier mode 

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

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

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

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

158 ) 

159 

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

161 tech=tech, 

162 annotated_layout=annotated_layout, 

163 layer_index_map=layer_index_map, 

164 blackbox_devices=blackbox_devices) 

165 

166 return KLayoutExtractionContext( 

167 lvsdb=lvsdb, 

168 tech=tech, 

169 dbu=dbu, 

170 annotated_top_cell=top_cell, 

171 layer_index_map=layer_index_map, 

172 lvsdb_regions=lvsdb_regions, 

173 cell_mapping=cm, 

174 annotated_layout=annotated_layout, 

175 extracted_layers=extracted_layers, 

176 unnamed_layers=unnamed_layers 

177 ) 

178 

179 @staticmethod 

180 def build_LVS_layer_map(annotated_layout: kdb.Layout, 

181 lvsdb: kdb.LayoutToNetlist, 

182 tech: TechInfo, 

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

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

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

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

187 

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

189 lvsdb_regions: LVSDBRegions = {} 

190 layer_index_map: LayerIndexMap = {} 

191 

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

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

194 

195 for layer_index in lvsdb.layer_indexes(): 

196 lname = lvsdb.layer_name(layer_index) 

197 

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

199 if computed_layer_info and blackbox_devices: 

200 match computed_layer_info.kind: 

201 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

202 continue 

203 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

204 continue 

205 

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

207 if not gds_pair: 

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

209 if li != kdb.LayerInfo(): 

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

211 

212 if gds_pair is not None: 

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

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

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

216 region = lvsdb.layer_by_index(layer_index) 

217 lvsdb_regions[annotated_layer_index] = region 

218 layer_index_map[layer_index] = annotated_layer_index 

219 

220 return lvsdb_regions, layer_index_map 

221 

222 @staticmethod 

223 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist, 

224 tech: TechInfo, 

225 annotated_layout: kdb.Layout, 

226 layer_index_map: LayerIndexMap, 

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

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

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

230 

231 unnamed_layers: List[KLayoutExtractedLayerInfo] = [] 

232 lvsdb_layer_indexes = lvsdb.layer_indexes() 

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

234 li = lvsdb_layer_indexes[idx] 

235 if li not in layer_index_map: 

236 continue 

237 li = layer_index_map[li] 

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

239 layer.enable_properties() 

240 if layer.count() >= 1: 

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

242 if not computed_layer_info: 

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

244 gds_pair = (1000 + idx, 20) 

245 linfo = KLayoutExtractedLayerInfo( 

246 index=idx, 

247 lvs_layer_name=ln, 

248 gds_pair=gds_pair, 

249 region=layer 

250 ) 

251 unnamed_layers.append(linfo) 

252 continue 

253 

254 if blackbox_devices: 

255 match computed_layer_info.kind: 

256 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

257 continue 

258 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

259 continue 

260 

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

262 computed_layer_info.layer_info.drw_gds_pair.datatype) 

263 

264 linfo = KLayoutExtractedLayerInfo( 

265 index=idx, 

266 lvs_layer_name=ln, 

267 gds_pair=gds_pair, 

268 region=layer 

269 ) 

270 

271 entry = nonempty_layers.get(gds_pair, None) 

272 if entry: 

273 entry.source_layers.append(linfo) 

274 else: 

275 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo( 

276 source_layers=[linfo], 

277 gds_pair=gds_pair, 

278 ) 

279 

280 return nonempty_layers, unnamed_layers 

281 

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

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

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

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

286 return b1 

287 else: 

288 return b2 

289 

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

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

292 if not lyr: 

293 return None 

294 

295 shapes = kdb.Region() 

296 shapes.enable_properties() 

297 

298 def add_shapes_from_region(source_region: kdb.Region): 

299 iter, transform = source_region.begin_shapes_rec() 

300 while not iter.at_end(): 

301 shape = iter.shape() 

302 net_name = shape.property('net') 

303 if net_name == net.name: 

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

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

306 shape.polygon) 

307 iter.next() 

308 

309 match len(lyr.source_layers): 

310 case 0: 

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

312 case _: 

313 for sl in lyr.source_layers: 

314 add_shapes_from_region(sl.region) 

315 

316 return shapes 

317 

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

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

320 if not lyr: 

321 return None 

322 

323 shapes: kdb.Region 

324 

325 match len(lyr.source_layers): 

326 case 0: 

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

328 case 1: 

329 shapes = lyr.source_layers[0].region 

330 case _: 

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

332 # shapes = kdb.Region() 

333 # for sl in lyr.source_layers: 

334 # shapes += sl.region 

335 shapes = kdb.Region() 

336 shapes.enable_properties() 

337 for sl in lyr.source_layers: 

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

339 while not iter.at_end(): 

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

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

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

343 p) 

344 iter.next() 

345 

346 return shapes 

347 

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

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

350 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype 

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

352 if lyr is None: 

353 return kdb.Region() 

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

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

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

357 return lyr.source_layers[0].region 

358 

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

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

361 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype 

362 

363 lay: kdb.Layout = self.lvsdb.internal_layout() 

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

365 if label_layer_idx is None: 

366 return kdb.Texts() 

367 

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

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

370 return labels 

371 

372 @cached_property 

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

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

375 

376 @cached_property 

377 def devices_by_name(self) -> Dict[str, KLayoutDeviceInfo]: 

378 dd = {} 

379 

380 for d in self.top_circuit.each_device(): 

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

382 d: kdb.Device 

383 

384 param_defs = d.device_class().parameter_definitions() 

385 params_by_name = {pd.name: d.parameter(pd.id()) for pd in param_defs} 

386 

387 terminals: List[KLayoutDeviceTerminal] = [] 

388 

389 for td in d.device_class().terminal_definitions(): 

390 n: kdb.Net = d.net_for_terminal(td.id()) 

391 if n is None: 

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

393 f"is not connected to any net") 

394 terminals.append( 

395 KLayoutDeviceTerminal( 

396 id=td.id(), 

397 name=td.name, 

398 regions_by_layer_name={}, 

399 net_name='', 

400 net_terminal_ref=None, 

401 net=None 

402 ) 

403 ) 

404 continue 

405 

406 for nt in n.each_terminal(): 

407 nt: kdb.NetTerminalRef 

408 

409 if nt.device().expanded_name() != d.expanded_name(): 

410 continue 

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

412 continue 

413 

414 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt) 

415 

416 def layer_name(idx: int) -> str: 

417 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[self.layer_index_map[idx]] 

418 return self.tech.canonical_layer_name_by_gds_pair[lyr_info.layer, lyr_info.datatype] 

419 

420 shapes_by_lyr_name = {layer_name(idx): shapes for idx, shapes in shapes_by_lyr_idx.items()} 

421 

422 terminals.append( 

423 KLayoutDeviceTerminal( 

424 id=td.id(), 

425 name=td.name, 

426 regions_by_layer_name=shapes_by_lyr_name, 

427 net_name=n.name, 

428 net_terminal_ref=nt, 

429 net=n 

430 ) 

431 ) 

432 

433 dd[d.expanded_name()] = KLayoutDeviceInfo( 

434 id=d.id(), 

435 name=d.expanded_name(), 

436 class_name=d.device_class().name, 

437 abstract_name=d.device_abstract.name, 

438 params=params_by_name, 

439 terminals=KLayoutDeviceTerminalList(terminals=terminals), 

440 device=d 

441 ) 

442 

443 return dd