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

472 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +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, field 

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 

48from ..types import GDSPair, LayerName, LVSLayerName, NetName 

49import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2 

50import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2 

51import klayout_pex_protobuf.kpex.layout.pin_pb2 as pin_pb2 

52import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2 

53import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2 

54 

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# the property of the shapes of the plates of a device capacitor (see KLayoutExtractionContext.shapes_of_layer) 

60DEVICE_CAPACITOR_PLATE_PROPERTY = 'device_capacitor_plate' 

61 

62 

63class LVSDBError(Exception): 

64 """ 

65 The LVS database lacks what the extraction depends on. 

66 """ 

67 pass 

68 

69 

70def unique_name(name: str, present_names: Set[str], separator: str = '$') -> str: 

71 """ 

72 The name, or if it's present, the name with a number (e.g. vss$1), 

73 like KLayout's tl::unique_name, which its SPICE writer names the nets with, 

74 but regardless of case, as SPICE reads names (ngspice joins the nets a and A) 

75 

76 :param present_names: the names taken, case-folded 

77 """ 

78 if name.casefold() not in present_names: 

79 return name 

80 j = 0 

81 m = 1 << 30 

82 while m > 0: 

83 j += m 

84 if f"{name}{separator}{j}".casefold() not in present_names: 

85 j -= m 

86 m >>= 1 

87 return f"{name}{separator}{j + 1}" 

88 

89 

90def device_list(device_names: List[str]) -> str: 

91 """ 

92 The first device names, for a message (e.g. 'M1, M2, M3 and 7 more') 

93 """ 

94 listed = ', '.join(device_names[:3]) 

95 return listed if len(device_names) <= 3 else f"{listed} and {len(device_names) - 3} more" 

96 

97 

98@dataclass 

99class KLayoutExtractedLayerInfo: 

100 index: int 

101 lvs_layer_name: LVSLayerName # NOTE: this can be computed, so gds_pair is preferred 

102 gds_pair: GDSPair 

103 region: kdb.Region 

104 

105 

106@dataclass 

107class KLayoutMergedExtractedLayerInfo: 

108 source_layers: List[KLayoutExtractedLayerInfo] 

109 gds_pair: GDSPair 

110 

111 

112@dataclass 

113class KLayoutNonemptyExtractedLayers: 

114 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] 

115 unnamed_layers: List[KLayoutExtractedLayerInfo] # not in the tech info 

116 unmodeled_layers: List[str] # unnamed layers the tech info has no layer for 

117 

118 

119@dataclass 

120class KLayoutExtractionContext: 

121 lvsdb: kdb.LayoutToNetlist 

122 tech: TechInfo 

123 dbu: float 

124 layer_index_map: LayerIndexMap 

125 lvsdb_regions: LVSDBRegions 

126 cell_mapping: kdb.CellMapping 

127 annotated_top_cell: kdb.Cell 

128 annotated_layout: kdb.Layout 

129 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] 

130 unnamed_layers: List[KLayoutExtractedLayerInfo] 

131 # unnamed layers the tech info has no layer for (derived from the same original layer) 

132 unmodeled_layers: List[str] 

133 # the devices are black-boxed (--blackbox), their models have their capacitances (see ComputedLayerInfo.Kind) 

134 blackbox_devices: bool 

135 # (device abstract cell index, terminal ID) -> shapes by LVS layer index, see shapes_of_terminal 

136 terminal_shapes_by_abstract: Dict[Tuple[int, int], Dict[int, kdb.Region]] = field(default_factory=dict, 

137 init=False, repr=False) 

138 

139 @classmethod 

140 def prepare_extraction(cls, 

141 lvsdb: kdb.LayoutToNetlist, 

142 top_cell: str, 

143 tech: TechInfo, 

144 blackbox_devices: bool) -> KLayoutExtractionContext: 

145 # NOTE: e.g. the abstract of a device the LVS script removed from the netlist is left as a top cell 

146 internal_top_cell = lvsdb.internal_top_cell() 

147 extra_top_cells = [c.name for c in lvsdb.internal_layout().top_cells() 

148 if c.cell_index() != internal_top_cell.cell_index()] 

149 if extra_top_cells: 

150 warning(f"Ignoring the LVS database top cells besides {internal_top_cell.name}: " 

151 f"{', '.join(extra_top_cells)}") 

152 

153 cls.make_net_names_unique(lvsdb.netlist()) 

154 

155 dbu = lvsdb.internal_layout().dbu 

156 annotated_layout = kdb.Layout() 

157 annotated_layout.dbu = dbu 

158 top_cell = annotated_layout.create_cell(top_cell) 

159 

160 # CellMapping 

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

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

163 # --- 

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

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

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

167 top_cell, 

168 not blackbox_devices) # with_device_cells 

169 

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

171 lvsdb=lvsdb, 

172 tech=tech, 

173 blackbox_devices=blackbox_devices) 

174 

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

176 # as GDS does not support string keys, 

177 # like OASIS does. 

178 net_name_prop = "net" 

179 

180 # Build a full hierarchical representation of the nets 

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

182 # hier_mode = None 

183 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten 

184 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells 

185 

186 lvsdb.build_all_nets( 

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

188 target=annotated_layout, # target layout 

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

190 hier_mode=hier_mode, # hier mode 

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

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

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

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

195 ) 

196 

197 nonempty_layers = cls.nonempty_extracted_layers(lvsdb=lvsdb, 

198 tech=tech, 

199 annotated_layout=annotated_layout, 

200 layer_index_map=layer_index_map, 

201 blackbox_devices=blackbox_devices) 

202 

203 context = KLayoutExtractionContext( 

204 lvsdb=lvsdb, 

205 tech=tech, 

206 dbu=dbu, 

207 annotated_top_cell=top_cell, 

208 layer_index_map=layer_index_map, 

209 lvsdb_regions=lvsdb_regions, 

210 cell_mapping=cm, 

211 annotated_layout=annotated_layout, 

212 extracted_layers=nonempty_layers.extracted_layers, 

213 unnamed_layers=nonempty_layers.unnamed_layers, 

214 unmodeled_layers=nonempty_layers.unmodeled_layers, 

215 blackbox_devices=blackbox_devices 

216 ) 

217 context.make_pins_of_pin_labels() 

218 return context 

219 

220 def make_pins_of_pin_labels(self): 

221 """ 

222 Gives each net of the top circuit with a label on a pin shape a pin, if it has none, 

223 as KLayout's make_top_level_pins makes none for a net without devices (e.g. a wire between two pins) 

224 """ 

225 circuit = self.top_circuit 

226 net_names = {pin.net_name for pins in self.pins_pb2_by_layer.values() for pin in pins if pin.net_name} 

227 for name in sorted(net_names): 

228 net = circuit.net_by_name(name) 

229 if net is not None and net.pin_count() == 0: 

230 circuit.connect_pin(circuit.create_pin(name), net) 

231 

232 @staticmethod 

233 def make_net_names_unique(netlist: kdb.Netlist): 

234 """ 

235 Names each net of a circuit uniquely, as KLayout's SPICE writer does (e.g. vss, vss$1), 

236 but regardless of case, as SPICE reads names (e.g. A, a$1) 

237 

238 NOTE: nets can share a name, e.g. the metal islands of a supply, joined only in the parent 

239 (unless the LVS script connects them implicitly), but the extraction identifies a net by its name, 

240 and the annotated layout has the name on the shapes of the net (#211 §10) 

241 

242 NOTE: a net without a label gets its expanded name (e.g. $2) as its name, as KLayout 0.30.4 reads it 

243 from the LVSDB without one (newer versions with the expanded name) 

244 """ 

245 for circuit in netlist.each_circuit(): 

246 names: Set[str] = set() # NOTE: case-folded 

247 for net in list(circuit.each_net()): 

248 name = unique_name(net.expanded_name(), names) 

249 names.add(name.casefold()) 

250 if name != net.name: 

251 net.name = name 

252 

253 @staticmethod 

254 def build_LVS_layer_map(annotated_layout: kdb.Layout, 

255 lvsdb: kdb.LayoutToNetlist, 

256 tech: TechInfo, 

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

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

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

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

261 

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

263 lvsdb_regions: LVSDBRegions = {} 

264 layer_index_map: LayerIndexMap = {} 

265 

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

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

268 

269 for layer_index in lvsdb.layer_indexes(): 

270 lname = lvsdb.layer_name(layer_index) 

271 

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

273 if computed_layer_info and blackbox_devices: 

274 match computed_layer_info.kind: 

275 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

276 continue 

277 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

278 continue 

279 

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

281 if not gds_pair: 

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

283 if li != kdb.LayerInfo(): 

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

285 

286 if gds_pair is not None: 

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

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

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

290 region = lvsdb.layer_by_index(layer_index) 

291 lvsdb_regions[annotated_layer_index] = region 

292 layer_index_map[layer_index] = annotated_layer_index 

293 

294 return lvsdb_regions, layer_index_map 

295 

296 @staticmethod 

297 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist, 

298 tech: TechInfo, 

299 annotated_layout: kdb.Layout, 

300 layer_index_map: LayerIndexMap, 

301 blackbox_devices: bool) -> KLayoutNonemptyExtractedLayers: 

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

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

304 

305 unnamed_layers: List[KLayoutExtractedLayerInfo] = [] 

306 original_gds_pair_by_unnamed_layer_name: Dict[str, GDSPair] = {} 

307 lvsdb_layer_indexes = lvsdb.layer_indexes() 

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

309 li = lvsdb_layer_indexes[idx] 

310 if li not in layer_index_map: 

311 continue 

312 li = layer_index_map[li] 

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

314 layer.enable_properties() 

315 if layer.count() >= 1: 

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

317 if not computed_layer_info: 

318 gds_pair = (1000 + idx, 20) 

319 linfo = KLayoutExtractedLayerInfo( 

320 index=idx, 

321 lvs_layer_name=ln, 

322 gds_pair=gds_pair, 

323 region=layer 

324 ) 

325 unnamed_layers.append(linfo) 

326 original_info = annotated_layout.get_info(li) 

327 original_gds_pair_by_unnamed_layer_name[ln] = (original_info.layer, original_info.datatype) 

328 continue 

329 

330 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_INTERNAL: 

331 continue # NOTE: the device model has its resistance and capacitance 

332 

333 if blackbox_devices: 

334 match computed_layer_info.kind: 

335 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR: 

336 continue 

337 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR: 

338 continue 

339 

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

341 computed_layer_info.layer_info.drw_gds_pair.datatype) 

342 

343 linfo = KLayoutExtractedLayerInfo( 

344 index=idx, 

345 lvs_layer_name=ln, 

346 gds_pair=gds_pair, 

347 region=layer 

348 ) 

349 

350 entry = nonempty_layers.get(gds_pair, None) 

351 if entry: 

352 entry.source_layers.append(linfo) 

353 else: 

354 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo( 

355 source_layers=[linfo], 

356 gds_pair=gds_pair, 

357 ) 

358 

359 unmodeled_layers = KLayoutExtractionContext.check_unnamed_layers( 

360 lvsdb=lvsdb, 

361 tech=tech, 

362 annotated_layout=annotated_layout, 

363 layer_index_map=layer_index_map, 

364 unnamed_layers=unnamed_layers, 

365 original_gds_pair_by_unnamed_layer_name=original_gds_pair_by_unnamed_layer_name 

366 ) 

367 

368 return KLayoutNonemptyExtractedLayers(extracted_layers=nonempty_layers, 

369 unnamed_layers=unnamed_layers, 

370 unmodeled_layers=unmodeled_layers) 

371 

372 @staticmethod 

373 def check_unnamed_layers(lvsdb: kdb.LayoutToNetlist, 

374 tech: TechInfo, 

375 annotated_layout: kdb.Layout, 

376 layer_index_map: LayerIndexMap, 

377 unnamed_layers: List[KLayoutExtractedLayerInfo], 

378 original_gds_pair_by_unnamed_layer_name: Dict[str, GDSPair]) -> List[str]: 

379 """ 

380 An LVS layer the tech info doesn't know is left out of the extraction. 

381 That's fine where the tech info has layers derived from the same original layer 

382 that cover it (e.g. the raw contact layer, and the contacts to diffusion and poly). 

383 

384 :return: the layers the tech info has no derived layer for, which the LVS deck defines 

385 """ 

386 lvsdb_layer_index_by_name = {lvsdb.layer_name(li): li for li in lvsdb.layer_indexes()} 

387 

388 def region(lvs_layer_name: str) -> kdb.Region: 

389 li = layer_index_map.get(lvsdb_layer_index_by_name[lvs_layer_name], None) 

390 if li is None: 

391 return kdb.Region() 

392 return kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li)) 

393 

394 unmodeled_layers: List[str] = [] 

395 partly_covered_layers: List[str] = [] 

396 

397 for ul in unnamed_layers: 

398 gds_pair = original_gds_pair_by_unnamed_layer_name[ul.lvs_layer_name] 

399 original_layer = tech.layer_info_by_gds_pair.get(gds_pair, None) 

400 description = f"{ul.lvs_layer_name} ({original_layer.name if original_layer else '%d/%d' % gds_pair})" 

401 

402 # NOTE: the LVS deck may not define the layers the tech info derives (e.g. differently named ones) 

403 derived_layer_names = [] if original_layer is None else [ 

404 name for name, info in tech.computed_layer_info_by_name.items() 

405 if info.original_layer_name == original_layer.name and name in lvsdb_layer_index_by_name 

406 ] 

407 if not derived_layer_names: 

408 unmodeled_layers.append(f"{description}: the tech info has no layer derived from it") 

409 continue 

410 

411 covered = kdb.Region() 

412 for name in derived_layer_names: 

413 covered += region(name) 

414 uncovered = ul.region.dup() 

415 uncovered.remove_properties() 

416 uncovered -= covered 

417 if uncovered.is_empty(): 

418 debug(f"The LVS layer {description} is covered by {', '.join(derived_layer_names)}") 

419 else: 

420 partly_covered_layers.append(f"{description}: {uncovered.count()} of {ul.region.count()} shapes " 

421 f"are not within {', '.join(derived_layer_names)}") 

422 

423 if unmodeled_layers or partly_covered_layers: 

424 warning("The extraction leaves out these LVS layers, as the tech info doesn't know them:\n" + 

425 '\n'.join(f" - {layer}" for layer in unmodeled_layers + partly_covered_layers)) 

426 

427 return unmodeled_layers 

428 

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

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

431 b2: kdb.Box = self.lvsdb.internal_top_cell().bbox() 

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

433 return b1 

434 else: 

435 return b2 

436 

437 @cached_property 

438 def shapes_by_net_name_by_lvs_layer(self) -> Dict[LVSLayerName, Dict[NetName, kdb.Region]]: 

439 """ 

440 The shapes of each net on each extracted LVS layer 

441 (several of them can share a GDS pair, e.g. sky130A met3_cap and met3_ncap) 

442 

443 NOTE: in one pass over the shapes of a layer, rather than one for each net, 

444 which is quadratic in the size of the layout (e.g. for the resistance extraction of each net) 

445 """ 

446 shapes_by_net_name_by_lvs_layer: Dict[LVSLayerName, Dict[NetName, kdb.Region]] = {} 

447 

448 for lyr in self.extracted_layers.values(): 

449 if not lyr.source_layers: 

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

451 for sl in lyr.source_layers: 

452 shapes_by_net_name: Dict[NetName, kdb.Region] = {} 

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

454 while not iter.at_end(): 

455 shape = iter.shape() 

456 net_name = shape.property('net') 

457 shapes = shapes_by_net_name.get(net_name, None) 

458 if shapes is None: 

459 shapes = kdb.Region() 

460 shapes.enable_properties() 

461 shapes_by_net_name[net_name] = shapes 

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

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

464 shape.polygon) 

465 iter.next() 

466 shapes_by_net_name_by_lvs_layer[sl.lvs_layer_name] = shapes_by_net_name 

467 

468 return shapes_by_net_name_by_lvs_layer 

469 

470 def source_layers(self, 

471 gds_pair: GDSPair, 

472 lvs_layer_names: Optional[Collection[LVSLayerName]] = None) \ 

473 -> List[KLayoutExtractedLayerInfo]: 

474 """ 

475 The extracted LVS layers of a GDS pair 

476 

477 :param lvs_layer_names: only these of them, default is all 

478 """ 

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

480 if not lyr: 

481 return [] 

482 if lvs_layer_names is None: 

483 return lyr.source_layers 

484 return [sl for sl in lyr.source_layers if sl.lvs_layer_name in lvs_layer_names] 

485 

486 def shapes_of_net(self, 

487 gds_pair: GDSPair, 

488 net: kdb.Net | NetName, 

489 lvs_layer_names: Optional[Collection[LVSLayerName]] = None) -> Optional[kdb.Region]: 

490 """ 

491 :param lvs_layer_names: only the shapes of these of the GDS pair's LVS layers, default is all of them 

492 (e.g. a layer of the process stack sharing its GDS pair with another one) 

493 :return: a copy, as the caller may change it, None if the GDS pair has none of the LVS layers 

494 """ 

495 source_layers = self.source_layers(gds_pair, lvs_layer_names) 

496 if not source_layers: 

497 return None 

498 

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

500 

501 shapes = kdb.Region() 

502 shapes.enable_properties() 

503 for sl in source_layers: 

504 net_shapes = self.shapes_by_net_name_by_lvs_layer[sl.lvs_layer_name].get(requested_net_name, None) 

505 if net_shapes is not None: 

506 shapes += net_shapes 

507 return shapes 

508 

509 def net_shapes_by_conductor_layer(self, net_name: NetName) -> Dict[LayerName, kdb.Region]: 

510 """ 

511 :return: the shapes of the net by layer of the process stack (its diffusion and metal layers), 

512 and by layer of its wells 

513 """ 

514 LP = tech_pb2.LayerInfo.Purpose 

515 

516 def is_well(layer_info: KLayoutExtractedLayerInfo) -> bool: 

517 return any(self.tech.computed_layer_info_by_name[source_layer.lvs_layer_name].layer_info.purpose 

518 in (LP.PURPOSE_NWELL, LP.PURPOSE_PWELL, LP.PURPOSE_DNWELL) 

519 for source_layer in layer_info.source_layers 

520 if source_layer.lvs_layer_name in self.tech.computed_layer_info_by_name) 

521 

522 well_gds_pairs = [gds_pair for gds_pair, layer_info in self.extracted_layers.items() if is_well(layer_info)] 

523 

524 shapes_by_layer: Dict[LayerName, kdb.Region] = {} 

525 for gds_pair in [*self.tech.process_conductor_gds_pairs, *well_gds_pairs]: 

526 shapes = self.shapes_of_net(gds_pair, net_name) 

527 if shapes is not None and not shapes.is_empty(): 

528 shapes_by_layer[self.tech.canonical_layer_name_by_gds_pair[gds_pair]] = shapes 

529 return shapes_by_layer 

530 

531 def shapes_of_layer(self, 

532 gds_pair: GDSPair, 

533 mark_device_capacitor_plates: bool = False, 

534 lvs_layer_names: Optional[Collection[LVSLayerName]] = None) -> Optional[kdb.Region]: 

535 """ 

536 :param mark_device_capacitor_plates: give the shapes of the layers of KIND_DEVICE_CAPACITOR_PLATE 

537 (e.g. the fingers of a MOM cap) the property 

538 DEVICE_CAPACITOR_PLATE_PROPERTY, so that they are kept apart 

539 from the other shapes of their nets 

540 :param lvs_layer_names: only the shapes of these of the GDS pair's LVS layers, default is all of them 

541 (e.g. a layer of the process stack sharing its GDS pair with another one) 

542 """ 

543 source_layers = self.source_layers(gds_pair, lvs_layer_names) 

544 if not source_layers: 

545 return None 

546 

547 def is_plate(sl: KLayoutExtractedLayerInfo) -> bool: 

548 return mark_device_capacitor_plates and \ 

549 self.tech.computed_layer_info_by_name[sl.lvs_layer_name].kind == \ 

550 tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR_PLATE 

551 

552 shapes: kdb.Region 

553 

554 match len(source_layers): 

555 case 1 if not is_plate(source_layers[0]): 

556 shapes = source_layers[0].region 

557 case _: 

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

559 # shapes = kdb.Region() 

560 # for sl in source_layers: 

561 # shapes += sl.region 

562 shapes = kdb.Region() 

563 shapes.enable_properties() 

564 for sl in source_layers: 

565 plate_properties = {DEVICE_CAPACITOR_PLATE_PROPERTY: True} if is_plate(sl) else {} 

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

567 while not iter.at_end(): 

568 p = kdb.PolygonWithProperties(iter.shape().polygon, 

569 {'net': iter.shape().property('net'), **plate_properties}) 

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

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

572 p) 

573 iter.next() 

574 

575 return shapes 

576 

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

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

579 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype 

580 # NOTE: the pin layer can have several LVS layers, where it's the drawn layer 

581 # (e.g. gf180mcuD, without pin layers, with Metal4 split under the MIM caps) 

582 pins = self.shapes_of_layer(pin_gds_pair) 

583 if pins is None: 

584 return kdb.Region() 

585 return pins 

586 

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

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

589 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype 

590 

591 lay: kdb.Layout = self.annotated_layout 

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

593 if label_layer_idx is None: 

594 return kdb.Texts() 

595 

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

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

598 return labels 

599 

600 @cached_property 

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

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

603 

604 @cached_property 

605 def substrate_net_name(self) -> str: 

606 """ 

607 The net the capacitances to the substrate go to: the substrate net of the LVS netlist, 

608 by a name of the tech info (e.g. sky130_gnd), or by a shape on its LVS layers, 

609 when a label names it otherwise (e.g. VSS, through the taps), else the first name, 

610 for a port of its own (e.g. a metal test pattern without taps or transistors) 

611 """ 

612 substrate = self.tech.tech.substrate 

613 for name in substrate.net_names: 

614 if self.top_circuit.net_by_name(name) is not None: 

615 return name 

616 

617 lvs_layer_names = set(self.lvsdb.layer_names()) 

618 for lvs_layer_name in substrate.lvs_layer_names: 

619 if lvs_layer_name not in lvs_layer_names: 

620 continue 

621 region = self.lvsdb.layer_by_name(lvs_layer_name) 

622 if region.is_empty(): 

623 continue 

624 # NOTE: a point within the first shape, the vertices of a trapezoid surround their average 

625 trapezoid = next(iter(region.each())).decompose_trapezoids()[0] 

626 points = list(trapezoid.each_point()) 

627 point = kdb.Point(sum(p.x for p in points) // len(points), sum(p.y for p in points) // len(points)) 

628 subcircuit_path: List[kdb.SubCircuit] = [] 

629 net = self.lvsdb.probe_net(region, point, subcircuit_path) 

630 # NOTE: the net of the shape may be within a subcircuit, the substrate is a pin of it 

631 for subcircuit in reversed(subcircuit_path): 

632 pins = list(net.each_pin()) if net is not None else [] 

633 net = subcircuit.net_for_pin(pins[0].pin_id()) if pins else None 

634 if net is not None: 

635 return net.expanded_name() 

636 

637 return substrate.net_names[0] if substrate.net_names else self.tech.internal_substrate_layer_name 

638 

639 @cached_property 

640 def terminal_refs(self) -> Dict[Tuple[int, int], List[kdb.NetTerminalRef]]: 

641 """ 

642 The references of the device terminals by (device ID, terminal ID) 

643 

644 NOTE: in one pass over the nets, rather than one over the terminals of its net for each terminal, 

645 which is quadratic in the size of a net (e.g. the bulk terminals on a supply) 

646 """ 

647 terminal_refs: Dict[Tuple[int, int], List[kdb.NetTerminalRef]] = defaultdict(list) 

648 for net in self.top_circuit.each_net(): 

649 for nt in net.each_terminal(): 

650 terminal_refs[(nt.device().id(), nt.terminal_id())].append(nt) 

651 return terminal_refs 

652 

653 @cached_property 

654 def mos_channels(self) -> kdb.Region: 

655 """ 

656 The channels of the MOS transistors, the shapes of their gate terminals on the gate poly 

657 (over the active area), whose capacitances (of the gate oxide, the overlap and the fringe) 

658 the transistor models have (#290) 

659 

660 NOTE: by the shapes on the gate poly, as the device class doesn't tell 

661 (e.g. sky130A reads VPP caps with 4 terminals as MOS4, whose terminals are on the metal) 

662 """ 

663 gate_poly_gds_pair = self.tech.gds_pair(self.tech.gate_poly_layer.name) 

664 channels = kdb.Region() 

665 for device in self.top_circuit.each_device(): 

666 device_class = device.device_class() 

667 if not isinstance(device_class, kdb.DeviceClassMOS3Transistor): 

668 continue 

669 for nt in self.terminal_refs[(device.id(), device_class.terminal_id('G'))]: 

670 for layer_index, shapes in self.shapes_of_terminal(device, nt).items(): 

671 if self.tech.gds_pair_for_computed_layer_name.get(self.lvsdb.layer_name(layer_index)) == \ 

672 gate_poly_gds_pair: 

673 channels += shapes 

674 return channels.merged() 

675 

676 def shapes_of_terminal(self, device: kdb.Device, nt: kdb.NetTerminalRef) -> Dict[int, kdb.Region]: 

677 """ 

678 The shapes of a device terminal by LVS layer index, like LayoutToNetlist.shapes_of_terminal 

679 

680 NOTE: LayoutToNetlist.shapes_of_terminal takes long (e.g. 0.1 s for each terminal of gcd, sky130A), 

681 so the shapes of each terminal of a device abstract are taken once, and moved to each device of it, 

682 but for a device of several abstracts (combined devices), or a non-orthogonal or magnifying placement 

683 """ 

684 trans = kdb.ICplxTrans(device.trans, self.dbu) 

685 if any(True for _ in device.each_combined_abstract()) or not trans.is_ortho() or trans.is_mag(): 

686 return self.lvsdb.shapes_of_terminal(nt) 

687 key = (device.device_abstract.cell_index(), nt.terminal_id()) 

688 shapes_by_lyr_idx = self.terminal_shapes_by_abstract.get(key, None) 

689 if shapes_by_lyr_idx is None: 

690 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt, trans.inverted()) # of the abstract 

691 self.terminal_shapes_by_abstract[key] = shapes_by_lyr_idx 

692 return {idx: shapes.transformed(trans) for idx, shapes in shapes_by_lyr_idx.items()} 

693 

694 @cached_property 

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

696 # NOTE: a device the LVS script created, rather than extracted, has no abstract, 

697 # so no terminal geometry to connect it to the resistor network 

698 # (e.g. a model mapping that replaces extracted devices with ones of another class) 

699 created_devices = [f"{d.expanded_name()} ({d.device_class().name})" 

700 for d in self.top_circuit.each_device() if d.device_abstract is None] 

701 if created_devices: 

702 raise LVSDBError(f"The LVS database has devices without layout geometry, " 

703 f"which the resistance extraction depends on: {', '.join(created_devices)}. " 

704 f"They were created by the LVS script rather than extracted from the layout, " 

705 f"e.g. to replace extracted devices with another device class") 

706 

707 dd = {} 

708 

709 shapes_converter = ShapesConverter(dbu=self.dbu) 

710 

711 # (device class, terminal, LVS layer) -> device names 

712 devices_by_unknown_terminal_layer: Dict[Tuple[str, str, str], List[str]] = defaultdict(list) 

713 # (device class, terminal) -> device names 

714 devices_by_unconnected_terminal: Dict[Tuple[str, str], List[str]] = defaultdict(list) 

715 

716 terminal_refs = self.terminal_refs 

717 

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

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

720 d_kly: kdb.Device 

721 

722 d = device_pb2.Device() 

723 d.id = d_kly.id() 

724 d.device_name = d_kly.expanded_name() 

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

726 d.device_abstract_name = d_kly.device_abstract.name 

727 

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

729 p = d.parameters.add() 

730 p.id = pd.id() 

731 p.name = pd.name 

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

733 

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

735 n: Optional[kdb.Net] = d_kly.net_for_terminal(td.id()) 

736 if n is None: 

737 # NOTE: the LVS netlist leaves the terminal unconnected, as its shapes touch no conductor 

738 # (e.g. the bulk of a sky130A varactor without a ptap ring around it), 

739 # so the RC netlist leaves it unconnected too 

740 devices_by_unconnected_terminal[(d.device_class_name, td.name)].append(d.device_name) 

741 continue 

742 net_name = n.name or f"${n.cluster_id}" 

743 

744 for nt in terminal_refs[(d_kly.id(), td.id())]: 

745 shapes_by_lyr_idx = self.shapes_of_terminal(d_kly, nt) 

746 

747 terminal = d.terminals.add() 

748 terminal.device_id = d.id 

749 terminal.terminal_id = td.id() 

750 terminal.name = td.name 

751 terminal.net_name = net_name 

752 

753 def add_region(gds_pair: GDSPair, region: kdb.Region): 

754 region_by_layer = terminal.region_by_layer.add() 

755 # NOTE: the annotated layout has a layer for each LVS layer, so several for a GDS pair 

756 # (e.g. poly_con and poly_vpp), but the resistance extraction has the wires 

757 # of a GDS pair on one of them, so the terminal must be on that one to be a port 

758 region_by_layer.layer.id = self.annotated_layout.layer(*gds_pair) 

759 region_by_layer.layer.canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[gds_pair] 

760 shapes_converter.klayout_region_to_pb(region, region_by_layer.region) 

761 

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

763 lyr_idx = self.layer_index_map.get(idx, None) 

764 if lyr_idx is None: 

765 # NOTE: the tech info has no layer for the LVS layer of the terminal 

766 # (e.g. the ports of a MOM cap, the metal pins within its marker), 

767 # so the terminal is where it overlaps or abuts a conductor of its net, 

768 # which LVS connected it to (e.g. the metal of the pins) 

769 gds_pair_and_region = self.terminal_region_on_net(shapes, net_name) 

770 if gds_pair_and_region is None: 

771 key = (d.device_class_name, td.name, self.lvsdb.layer_name(idx)) 

772 devices_by_unknown_terminal_layer[key].append(d.device_name) 

773 else: 

774 add_region(*gds_pair_and_region) 

775 continue 

776 

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

778 add_region((lyr_info.layer, lyr_info.datatype), shapes) 

779 

780 dd[d.device_name] = d 

781 

782 if devices_by_unconnected_terminal: 

783 warning("These device terminals are unconnected in the LVS netlist, " 

784 "so they stay unconnected in the extracted netlist:\n" + 

785 '\n'.join(f" - {device_class} terminal {terminal}: {device_list(device_names)}" 

786 for (device_class, terminal), device_names 

787 in sorted(devices_by_unconnected_terminal.items()))) 

788 

789 # NOTE: a terminal without a node stays on its net, which a node of the resistance network carries 

790 # (#211 §6), so it misses only the resistance to where it is 

791 if devices_by_unknown_terminal_layer: 

792 warning("The resistance network has no nodes for these device terminals, " 

793 "as the tech info has no layer for their LVS layer, " 

794 "and they neither overlap nor abut a conductor of their net " 

795 "(e.g. a terminal on the substrate or a well):\n" + 

796 '\n'.join(f" - {device_class} terminal {terminal} on LVS layer {lvs_layer}: " 

797 f"{device_list(device_names)}" 

798 for (device_class, terminal, lvs_layer), device_names 

799 in sorted(devices_by_unknown_terminal_layer.items()))) 

800 

801 return dd 

802 

803 def terminal_region_on_net(self, 

804 terminal_shapes: kdb.Region, 

805 net_name: str) -> Optional[Tuple[GDSPair, kdb.Region]]: 

806 """ 

807 :return: the part of the terminal shapes on the conductor of its net they overlap most, 

808 the lowest of equals, else the strip of 1 dbu of the conductor of its net they abut most 

809 (e.g. the ports of an IHP inductor, within its marker, at the ends of their leads outside it, 

810 which LVS joins by a pin layer over both), or None if they neither overlap nor abut 

811 a conductor of their net 

812 

813 NOTE: one conductor, as the ports of a terminal are one node, which would short the resistance 

814 between the conductors (e.g. a pin on the top metal of a MOM cap, over the fingers below) 

815 """ 

816 lvsdb_layer_indexes = self.lvsdb.layer_indexes() 

817 bbox = terminal_shapes.bbox() 

818 net_shapes_by_gds_pair: Dict[GDSPair, kdb.Region] = {} 

819 for gds_pair in self.tech.process_conductor_gds_pairs: 

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

821 if lyr is None: 

822 continue 

823 net_shapes = kdb.Region() 

824 for sl in lyr.source_layers: 

825 computed_layer_info = self.tech.computed_layer_info_by_name[sl.lvs_layer_name] 

826 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_PIN: 

827 continue 

828 annotated_layer_index = self.layer_index_map[lvsdb_layer_indexes[sl.index]] 

829 # NOTE: the shapes near the terminal only, the net may be huge (e.g. a supply) 

830 iter = self.annotated_top_cell.begin_shapes_rec_touching(annotated_layer_index, bbox) 

831 while not iter.at_end(): 

832 shape = iter.shape() 

833 if shape.property('net') == net_name and not shape.is_text(): 

834 net_shapes.insert(iter.trans() * shape.polygon) 

835 iter.next() 

836 net_shapes_by_gds_pair[gds_pair] = net_shapes 

837 

838 def largest_overlap(shapes: kdb.Region) -> Optional[Tuple[GDSPair, kdb.Region]]: 

839 best: Optional[Tuple[float, GDSPair, kdb.Region]] = None 

840 for gds_pair, net_shapes in net_shapes_by_gds_pair.items(): 

841 overlap = shapes & net_shapes 

842 area = overlap.area() 

843 if area > 0 and (best is None or area > best[0]): 

844 best = (area, gds_pair, overlap) 

845 return None if best is None else (best[1], best[2]) 

846 

847 return largest_overlap(terminal_shapes) or largest_overlap(terminal_shapes.sized(1)) 

848 

849 @cached_property 

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

851 d = defaultdict(list) 

852 

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

854 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair] 

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

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

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

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

859 if gds_pair is None: 

860 continue 

861 if gds_pair not in self.tech.layer_info_by_gds_pair: 

862 continue 

863 

864 for lyr in lyr_info.source_layers: 

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

866 

867 pins = self.pins_of_layer(gds_pair) 

868 labels = self.labels_of_layer(gds_pair) 

869 

870 # NOTE: the pins at a label are found by their boxes, rather than by going through all pins 

871 # for each label, which is quadratic in the number of pins (e.g. the ones of standard cells) 

872 pin_polygons: List[kdb.PolygonWithProperties] = list(pins.each()) 

873 pin_boxes = kdb.Shapes() 

874 for pin_index, p in enumerate(pin_polygons): 

875 pin_boxes.insert(kdb.BoxWithProperties(p.bbox(), {'pin_index': pin_index})) 

876 

877 pin_labels: kdb.Texts = labels & pins 

878 for l in pin_labels: 

879 l: kdb.Text 

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

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

882 # we create our own "mini squares" 

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

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

885 

886 pin = pin_pb2.Pin() 

887 pin.label = l.string 

888 

889 pos = l.position() 

890 

891 # NOTE: the first pin the label is inside of 

892 for pin_index in sorted(shape.property('pin_index') 

893 for shape in pin_boxes.each_touching(kdb.Shapes.SAll, kdb.Box(pos, pos))): 

894 p = pin_polygons[pin_index] 

895 if p.inside(pos): 

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

897 break 

898 

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

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

901 pin.layer.id = klayout_index 

902 pin.layer.canonical_layer_name = canonical_layer_name 

903 pin.layer.lvs_layer_name = lvs_layer_name 

904 

905 pin.label_point.x = pos.x 

906 pin.label_point.y = pos.y 

907 pin.label_point.net = pin.net_name 

908 

909 d[gds_pair].append(pin) 

910 

911 return d