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
« 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
26from collections import defaultdict
27from dataclasses import dataclass, field
28from functools import cached_property
29import tempfile
30from typing import *
32from rich.pretty import pprint
34import klayout.db as kdb
36from ..log import (
37 console,
38 debug,
39 info,
40 warning,
41 error,
42 rule
43)
45from .shapes_pb2_converter import ShapesConverter
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
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
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'
63class LVSDBError(Exception):
64 """
65 The LVS database lacks what the extraction depends on.
66 """
67 pass
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)
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}"
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"
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
106@dataclass
107class KLayoutMergedExtractedLayerInfo:
108 source_layers: List[KLayoutExtractedLayerInfo]
109 gds_pair: GDSPair
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
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)
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)}")
153 cls.make_net_names_unique(lvsdb.netlist())
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)
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
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)
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"
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
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 )
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)
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
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)
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)
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)
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
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.
262 # https://www.klayout.de/doc-qt5/code/class_LayerInfo.html
263 lvsdb_regions: LVSDBRegions = {}
264 layer_index_map: LayerIndexMap = {}
266 if not hasattr(lvsdb, "layer_indexes"):
267 raise Exception("Needs at least KLayout version 0.29.2")
269 for layer_index in lvsdb.layer_indexes():
270 lname = lvsdb.layer_name(layer_index)
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
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)
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
294 return lvsdb_regions, layer_index_map
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] = {}
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
330 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_INTERNAL:
331 continue # NOTE: the device model has its resistance and capacitance
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
340 gds_pair = (computed_layer_info.layer_info.drw_gds_pair.layer,
341 computed_layer_info.layer_info.drw_gds_pair.datatype)
343 linfo = KLayoutExtractedLayerInfo(
344 index=idx,
345 lvs_layer_name=ln,
346 gds_pair=gds_pair,
347 region=layer
348 )
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 )
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 )
368 return KLayoutNonemptyExtractedLayers(extracted_layers=nonempty_layers,
369 unnamed_layers=unnamed_layers,
370 unmodeled_layers=unmodeled_layers)
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).
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()}
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))
394 unmodeled_layers: List[str] = []
395 partly_covered_layers: List[str] = []
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})"
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
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)}")
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))
427 return unmodeled_layers
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
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)
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]] = {}
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
468 return shapes_by_net_name_by_lvs_layer
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
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]
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
499 requested_net_name = net.name if isinstance(net, kdb.Net) else net
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
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
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)
522 well_gds_pairs = [gds_pair for gds_pair, layer_info in self.extracted_layers.items() if is_well(layer_info)]
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
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
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
552 shapes: kdb.Region
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()
575 return shapes
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
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
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()
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
600 @cached_property
601 def top_circuit(self) -> kdb.Circuit:
602 return self.lvsdb.netlist().top_circuit()
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
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()
637 return substrate.net_names[0] if substrate.net_names else self.tech.internal_substrate_layer_name
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)
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
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)
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()
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
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()}
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")
707 dd = {}
709 shapes_converter = ShapesConverter(dbu=self.dbu)
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)
716 terminal_refs = self.terminal_refs
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
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
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())
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}"
744 for nt in terminal_refs[(d_kly.id(), td.id())]:
745 shapes_by_lyr_idx = self.shapes_of_terminal(d_kly, nt)
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
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)
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
777 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[lyr_idx]
778 add_region((lyr_info.layer, lyr_info.datatype), shapes)
780 dd[d.device_name] = d
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())))
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())))
801 return dd
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
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
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])
847 return largest_overlap(terminal_shapes) or largest_overlap(terminal_shapes.sized(1))
849 @cached_property
850 def pins_pb2_by_layer(self) -> Dict[GDSPair, List[pin_pb2.Pin]]:
851 d = defaultdict(list)
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
864 for lyr in lyr_info.source_layers:
865 klayout_index = self.annotated_layout.layer(*lyr.gds_pair)
867 pins = self.pins_of_layer(gds_pair)
868 labels = self.labels_of_layer(gds_pair)
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}))
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)
886 pin = pin_pb2.Pin()
887 pin.label = l.string
889 pos = l.position()
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
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
905 pin.label_point.x = pos.x
906 pin.label_point.y = pos.y
907 pin.label_point.net = pin.net_name
909 d[gds_pair].append(pin)
911 return d