Coverage for klayout_pex/klayout/lvsdb_extractor.py: 95%
397 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +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
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
48import klayout_pex_protobuf.kpex.geometry.shapes_pb2 as shapes_pb2
49import klayout_pex_protobuf.kpex.layout.device_pb2 as device_pb2
50import klayout_pex_protobuf.kpex.layout.pin_pb2 as pin_pb2
51import klayout_pex_protobuf.kpex.layout.location_pb2 as location_pb2
52import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
54GDSPair = Tuple[int, int]
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 """
75 if name not in present_names:
76 return name
77 j = 0
78 m = 1 << 30
79 while m > 0:
80 j += m
81 if f"{name}{separator}{j}" not in present_names:
82 j -= m
83 m >>= 1
84 return f"{name}{separator}{j + 1}"
87@dataclass
88class KLayoutExtractedLayerInfo:
89 index: int
90 lvs_layer_name: str # NOTE: this can be computed, so gds_pair is preferred
91 gds_pair: GDSPair
92 region: kdb.Region
95@dataclass
96class KLayoutMergedExtractedLayerInfo:
97 source_layers: List[KLayoutExtractedLayerInfo]
98 gds_pair: GDSPair
101@dataclass
102class KLayoutNonemptyExtractedLayers:
103 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo]
104 unnamed_layers: List[KLayoutExtractedLayerInfo] # not in the tech info
105 unmodeled_layers: List[str] # unnamed layers the tech info has no layer for
108@dataclass
109class KLayoutExtractionContext:
110 lvsdb: kdb.LayoutToNetlist
111 tech: TechInfo
112 dbu: float
113 layer_index_map: LayerIndexMap
114 lvsdb_regions: LVSDBRegions
115 cell_mapping: kdb.CellMapping
116 annotated_top_cell: kdb.Cell
117 annotated_layout: kdb.Layout
118 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo]
119 unnamed_layers: List[KLayoutExtractedLayerInfo]
120 # unnamed layers the tech info has no layer for (derived from the same original layer)
121 unmodeled_layers: List[str]
122 # the devices are black-boxed (--blackbox), their models have their capacitances (see ComputedLayerInfo.Kind)
123 blackbox_devices: bool
125 @classmethod
126 def prepare_extraction(cls,
127 lvsdb: kdb.LayoutToNetlist,
128 top_cell: str,
129 tech: TechInfo,
130 blackbox_devices: bool) -> KLayoutExtractionContext:
131 # NOTE: e.g. the abstract of a device the LVS script removed from the netlist is left as a top cell
132 internal_top_cell = lvsdb.internal_top_cell()
133 extra_top_cells = [c.name for c in lvsdb.internal_layout().top_cells()
134 if c.cell_index() != internal_top_cell.cell_index()]
135 if extra_top_cells:
136 warning(f"Ignoring the LVS database top cells besides {internal_top_cell.name}: "
137 f"{', '.join(extra_top_cells)}")
139 cls.make_net_names_unique(lvsdb.netlist())
141 dbu = lvsdb.internal_layout().dbu
142 annotated_layout = kdb.Layout()
143 annotated_layout.dbu = dbu
144 top_cell = annotated_layout.create_cell(top_cell)
146 # CellMapping
147 # mapping of internal layout to target layout for the circuit mapping
148 # https://www.klayout.de/doc-qt5/code/class_CellMapping.html
149 # ---
150 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
151 # Creates a cell mapping for copying shapes from the internal layout to the given target layout
152 cm = lvsdb.cell_mapping_into(annotated_layout, # target layout
153 top_cell,
154 not blackbox_devices) # with_device_cells
156 lvsdb_regions, layer_index_map = cls.build_LVS_layer_map(annotated_layout=annotated_layout,
157 lvsdb=lvsdb,
158 tech=tech,
159 blackbox_devices=blackbox_devices)
161 # NOTE: GDS only supports integer properties to GDS,
162 # as GDS does not support string keys,
163 # like OASIS does.
164 net_name_prop = "net"
166 # Build a full hierarchical representation of the nets
167 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method14
168 # hier_mode = None
169 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten
170 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells
172 lvsdb.build_all_nets(
173 cmap=cm, # mapping of internal layout to target layout for the circuit mapping
174 target=annotated_layout, # target layout
175 lmap=lvsdb_regions, # maps: target layer index => net regions
176 hier_mode=hier_mode, # hier mode
177 netname_prop=net_name_prop, # property name to which to attach the net name
178 circuit_cell_name_prefix="CIRCUIT_", # NOTE: generates a cell for each circuit
179 net_cell_name_prefix=None, # NOTE: this would generate a cell for each net
180 device_cell_name_prefix=None # NOTE: this would create a cell for each device (e.g. transistor)
181 )
183 nonempty_layers = cls.nonempty_extracted_layers(lvsdb=lvsdb,
184 tech=tech,
185 annotated_layout=annotated_layout,
186 layer_index_map=layer_index_map,
187 blackbox_devices=blackbox_devices)
189 return KLayoutExtractionContext(
190 lvsdb=lvsdb,
191 tech=tech,
192 dbu=dbu,
193 annotated_top_cell=top_cell,
194 layer_index_map=layer_index_map,
195 lvsdb_regions=lvsdb_regions,
196 cell_mapping=cm,
197 annotated_layout=annotated_layout,
198 extracted_layers=nonempty_layers.extracted_layers,
199 unnamed_layers=nonempty_layers.unnamed_layers,
200 unmodeled_layers=nonempty_layers.unmodeled_layers,
201 blackbox_devices=blackbox_devices
202 )
204 @staticmethod
205 def make_net_names_unique(netlist: kdb.Netlist):
206 """
207 Names each net of a circuit uniquely, as KLayout's SPICE writer does (e.g. vss, vss$1)
209 NOTE: nets can share a name, e.g. the metal islands of a supply, joined only in the parent
210 (unless the LVS script connects them implicitly), but the extraction identifies a net by its name,
211 and the annotated layout has the name on the shapes of the net (#211 §10)
213 NOTE: a net without a label gets its expanded name (e.g. $2) as its name, as KLayout 0.30.4 reads it
214 from the LVSDB without one (newer versions with the expanded name)
215 """
216 for circuit in netlist.each_circuit():
217 names: Set[str] = set()
218 for net in list(circuit.each_net()):
219 name = unique_name(net.expanded_name(), names)
220 names.add(name)
221 if name != net.name:
222 net.name = name
224 @staticmethod
225 def build_LVS_layer_map(annotated_layout: kdb.Layout,
226 lvsdb: kdb.LayoutToNetlist,
227 tech: TechInfo,
228 blackbox_devices: bool) -> Tuple[LVSDBRegions, LayerIndexMap]:
229 # NOTE: currently, the layer numbers are auto-assigned
230 # by the sequence they occur in the LVS script, hence not well defined!
231 # build a layer map for the layers that correspond to original ones.
233 # https://www.klayout.de/doc-qt5/code/class_LayerInfo.html
234 lvsdb_regions: LVSDBRegions = {}
235 layer_index_map: LayerIndexMap = {}
237 if not hasattr(lvsdb, "layer_indexes"):
238 raise Exception("Needs at least KLayout version 0.29.2")
240 for layer_index in lvsdb.layer_indexes():
241 lname = lvsdb.layer_name(layer_index)
243 computed_layer_info = tech.computed_layer_info_by_name.get(lname, None)
244 if computed_layer_info and blackbox_devices:
245 match computed_layer_info.kind:
246 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
247 continue
248 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
249 continue
251 gds_pair = tech.gds_pair_for_computed_layer_name.get(lname, None)
252 if not gds_pair:
253 li = lvsdb.internal_layout().get_info(layer_index)
254 if li != kdb.LayerInfo():
255 gds_pair = (li.layer, li.datatype)
257 if gds_pair is not None:
258 annotated_layer_index = annotated_layout.layer() # creates new index each time!
259 # Creates a new internal layer! because multiple layers with the same gds_pair are possible!
260 annotated_layout.set_info(annotated_layer_index, kdb.LayerInfo(*gds_pair))
261 region = lvsdb.layer_by_index(layer_index)
262 lvsdb_regions[annotated_layer_index] = region
263 layer_index_map[layer_index] = annotated_layer_index
265 return lvsdb_regions, layer_index_map
267 @staticmethod
268 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist,
269 tech: TechInfo,
270 annotated_layout: kdb.Layout,
271 layer_index_map: LayerIndexMap,
272 blackbox_devices: bool) -> KLayoutNonemptyExtractedLayers:
273 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
274 nonempty_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] = {}
276 unnamed_layers: List[KLayoutExtractedLayerInfo] = []
277 original_gds_pair_by_unnamed_layer_name: Dict[str, GDSPair] = {}
278 lvsdb_layer_indexes = lvsdb.layer_indexes()
279 for idx, ln in enumerate(lvsdb.layer_names()):
280 li = lvsdb_layer_indexes[idx]
281 if li not in layer_index_map:
282 continue
283 li = layer_index_map[li]
284 layer = kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li))
285 layer.enable_properties()
286 if layer.count() >= 1:
287 computed_layer_info = tech.computed_layer_info_by_name.get(ln, None)
288 if not computed_layer_info:
289 gds_pair = (1000 + idx, 20)
290 linfo = KLayoutExtractedLayerInfo(
291 index=idx,
292 lvs_layer_name=ln,
293 gds_pair=gds_pair,
294 region=layer
295 )
296 unnamed_layers.append(linfo)
297 original_info = annotated_layout.get_info(li)
298 original_gds_pair_by_unnamed_layer_name[ln] = (original_info.layer, original_info.datatype)
299 continue
301 if blackbox_devices:
302 match computed_layer_info.kind:
303 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
304 continue
305 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
306 continue
308 gds_pair = (computed_layer_info.layer_info.drw_gds_pair.layer,
309 computed_layer_info.layer_info.drw_gds_pair.datatype)
311 linfo = KLayoutExtractedLayerInfo(
312 index=idx,
313 lvs_layer_name=ln,
314 gds_pair=gds_pair,
315 region=layer
316 )
318 entry = nonempty_layers.get(gds_pair, None)
319 if entry:
320 entry.source_layers.append(linfo)
321 else:
322 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo(
323 source_layers=[linfo],
324 gds_pair=gds_pair,
325 )
327 unmodeled_layers = KLayoutExtractionContext.check_unnamed_layers(
328 lvsdb=lvsdb,
329 tech=tech,
330 annotated_layout=annotated_layout,
331 layer_index_map=layer_index_map,
332 unnamed_layers=unnamed_layers,
333 original_gds_pair_by_unnamed_layer_name=original_gds_pair_by_unnamed_layer_name
334 )
336 return KLayoutNonemptyExtractedLayers(extracted_layers=nonempty_layers,
337 unnamed_layers=unnamed_layers,
338 unmodeled_layers=unmodeled_layers)
340 @staticmethod
341 def check_unnamed_layers(lvsdb: kdb.LayoutToNetlist,
342 tech: TechInfo,
343 annotated_layout: kdb.Layout,
344 layer_index_map: LayerIndexMap,
345 unnamed_layers: List[KLayoutExtractedLayerInfo],
346 original_gds_pair_by_unnamed_layer_name: Dict[str, GDSPair]) -> List[str]:
347 """
348 An LVS layer the tech info doesn't know is left out of the extraction.
349 That's fine where the tech info has layers derived from the same original layer
350 that cover it (e.g. the raw contact layer, and the contacts to diffusion and poly).
352 :return: the layers the tech info has no derived layer for, which the LVS deck defines
353 """
354 lvsdb_layer_index_by_name = {lvsdb.layer_name(li): li for li in lvsdb.layer_indexes()}
356 def region(lvs_layer_name: str) -> kdb.Region:
357 li = layer_index_map.get(lvsdb_layer_index_by_name[lvs_layer_name], None)
358 if li is None:
359 return kdb.Region()
360 return kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li))
362 unmodeled_layers: List[str] = []
363 partly_covered_layers: List[str] = []
365 for ul in unnamed_layers:
366 gds_pair = original_gds_pair_by_unnamed_layer_name[ul.lvs_layer_name]
367 original_layer = tech.layer_info_by_gds_pair.get(gds_pair, None)
368 description = f"{ul.lvs_layer_name} ({original_layer.name if original_layer else '%d/%d' % gds_pair})"
370 # NOTE: the LVS deck may not define the layers the tech info derives (e.g. differently named ones)
371 derived_layer_names = [] if original_layer is None else [
372 name for name, info in tech.computed_layer_info_by_name.items()
373 if info.original_layer_name == original_layer.name and name in lvsdb_layer_index_by_name
374 ]
375 if not derived_layer_names:
376 unmodeled_layers.append(f"{description}: the tech info has no layer derived from it")
377 continue
379 covered = kdb.Region()
380 for name in derived_layer_names:
381 covered += region(name)
382 uncovered = ul.region.dup()
383 uncovered.remove_properties()
384 uncovered -= covered
385 if uncovered.is_empty():
386 debug(f"The LVS layer {description} is covered by {', '.join(derived_layer_names)}")
387 else:
388 partly_covered_layers.append(f"{description}: {uncovered.count()} of {ul.region.count()} shapes "
389 f"are not within {', '.join(derived_layer_names)}")
391 if unmodeled_layers or partly_covered_layers:
392 warning("The extraction leaves out these LVS layers, as the tech info doesn't know them:\n" +
393 '\n'.join(f" - {layer}" for layer in unmodeled_layers + partly_covered_layers))
395 return unmodeled_layers
397 def top_cell_bbox(self) -> kdb.Box:
398 b1: kdb.Box = self.annotated_layout.top_cell().bbox()
399 b2: kdb.Box = self.lvsdb.internal_top_cell().bbox()
400 if b1.area() > b2.area():
401 return b1
402 else:
403 return b2
405 def shapes_of_net(self, gds_pair: GDSPair, net: kdb.Net | str) -> Optional[kdb.Region]:
406 lyr = self.extracted_layers.get(gds_pair, None)
407 if not lyr:
408 return None
410 shapes = kdb.Region()
411 shapes.enable_properties()
413 requested_net_name = net.name if isinstance(net, kdb.Net) else net
415 def add_shapes_from_region(source_region: kdb.Region):
416 iter, transform = source_region.begin_shapes_rec()
417 while not iter.at_end():
418 shape = iter.shape()
419 net_name = shape.property('net')
420 if net_name == requested_net_name:
421 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
422 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
423 shape.polygon)
424 iter.next()
426 match len(lyr.source_layers):
427 case 0:
428 raise AssertionError('Internal error: Empty list of source_layers')
429 case _:
430 for sl in lyr.source_layers:
431 add_shapes_from_region(sl.region)
433 return shapes
435 def shapes_of_layer(self,
436 gds_pair: GDSPair,
437 mark_device_capacitor_plates: bool = False) -> Optional[kdb.Region]:
438 """
439 :param mark_device_capacitor_plates: give the shapes of the layers of KIND_DEVICE_CAPACITOR_PLATE
440 (e.g. the fingers of a MOM cap) the property
441 DEVICE_CAPACITOR_PLATE_PROPERTY, so that they are kept apart
442 from the other shapes of their nets
443 """
444 lyr = self.extracted_layers.get(gds_pair, None)
445 if not lyr:
446 return None
448 def is_plate(sl: KLayoutExtractedLayerInfo) -> bool:
449 return mark_device_capacitor_plates and \
450 self.tech.computed_layer_info_by_name[sl.lvs_layer_name].kind == \
451 tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR_PLATE
453 shapes: kdb.Region
455 match len(lyr.source_layers):
456 case 0:
457 raise AssertionError('Internal error: Empty list of source_layers')
458 case 1 if not is_plate(lyr.source_layers[0]):
459 shapes = lyr.source_layers[0].region
460 case _:
461 # NOTE: currently a bug, for now use polygon-per-polygon workaround
462 # shapes = kdb.Region()
463 # for sl in lyr.source_layers:
464 # shapes += sl.region
465 shapes = kdb.Region()
466 shapes.enable_properties()
467 for sl in lyr.source_layers:
468 plate_properties = {DEVICE_CAPACITOR_PLATE_PROPERTY: True} if is_plate(sl) else {}
469 iter, transform = sl.region.begin_shapes_rec()
470 while not iter.at_end():
471 p = kdb.PolygonWithProperties(iter.shape().polygon,
472 {'net': iter.shape().property('net'), **plate_properties})
473 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
474 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
475 p)
476 iter.next()
478 return shapes
480 def pins_of_layer(self, gds_pair: GDSPair) -> kdb.Region:
481 pin_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].pin_gds_pair
482 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype
483 # NOTE: the pin layer can have several LVS layers, where it's the drawn layer
484 # (e.g. gf180mcuD, without pin layers, with Metal4 split under the MIM caps)
485 pins = self.shapes_of_layer(pin_gds_pair)
486 if pins is None:
487 return kdb.Region()
488 return pins
490 def labels_of_layer(self, gds_pair: GDSPair) -> kdb.Texts:
491 labels_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].label_gds_pair
492 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype
494 lay: kdb.Layout = self.annotated_layout
495 label_layer_idx = lay.find_layer(labels_gds_pair) # sky130 layer dt = 5
496 if label_layer_idx is None:
497 return kdb.Texts()
499 sh_it = lay.begin_shapes(self.lvsdb.internal_top_cell(), label_layer_idx)
500 labels: kdb.Texts = kdb.Texts(sh_it)
501 return labels
503 @cached_property
504 def top_circuit(self) -> kdb.Circuit:
505 return self.lvsdb.netlist().top_circuit()
507 @cached_property
508 def substrate_net_name(self) -> str:
509 """
510 The net the capacitances to the substrate go to: the substrate net of the LVS netlist,
511 by a name of the tech info (e.g. sky130_gnd), or by a shape on its LVS layers,
512 when a label names it otherwise (e.g. VSS, through the taps), else the first name,
513 for a port of its own (e.g. a metal test pattern without taps or transistors)
514 """
515 substrate = self.tech.tech.substrate
516 for name in substrate.net_names:
517 if self.top_circuit.net_by_name(name) is not None:
518 return name
520 lvs_layer_names = set(self.lvsdb.layer_names())
521 for lvs_layer_name in substrate.lvs_layer_names:
522 if lvs_layer_name not in lvs_layer_names:
523 continue
524 region = self.lvsdb.layer_by_name(lvs_layer_name)
525 if region.is_empty():
526 continue
527 # NOTE: a point within the first shape, the vertices of a trapezoid surround their average
528 trapezoid = next(iter(region.each())).decompose_trapezoids()[0]
529 points = list(trapezoid.each_point())
530 point = kdb.Point(sum(p.x for p in points) // len(points), sum(p.y for p in points) // len(points))
531 subcircuit_path: List[kdb.SubCircuit] = []
532 net = self.lvsdb.probe_net(region, point, subcircuit_path)
533 # NOTE: the net of the shape may be within a subcircuit, the substrate is a pin of it
534 for subcircuit in reversed(subcircuit_path):
535 pins = list(net.each_pin()) if net is not None else []
536 net = subcircuit.net_for_pin(pins[0].pin_id()) if pins else None
537 if net is not None:
538 return net.expanded_name()
540 return substrate.net_names[0] if substrate.net_names else self.tech.internal_substrate_layer_name
542 @cached_property
543 def devices_by_name(self) -> Dict[str, device_pb2.Device]:
544 # NOTE: a device the LVS script created, rather than extracted, has no abstract,
545 # so no terminal geometry to connect it to the resistor network
546 # (e.g. a model mapping that replaces extracted devices with ones of another class)
547 created_devices = [f"{d.expanded_name()} ({d.device_class().name})"
548 for d in self.top_circuit.each_device() if d.device_abstract is None]
549 if created_devices:
550 raise LVSDBError(f"The LVS database has devices without layout geometry, "
551 f"which the resistance extraction depends on: {', '.join(created_devices)}. "
552 f"They were created by the LVS script rather than extracted from the layout, "
553 f"e.g. to replace extracted devices with another device class")
555 dd = {}
557 shapes_converter = ShapesConverter(dbu=self.dbu)
559 # (device class, terminal, LVS layer) -> device names
560 devices_by_unknown_terminal_layer: Dict[Tuple[str, str, str], List[str]] = defaultdict(list)
562 for d_kly in self.top_circuit.each_device():
563 # https://www.klayout.de/doc-qt5/code/class_Device.html
564 d_kly: kdb.Device
566 d = device_pb2.Device()
567 d.id = d_kly.id()
568 d.device_name = d_kly.expanded_name()
569 d.device_class_name = d_kly.device_class().name
570 d.device_abstract_name = d_kly.device_abstract.name
572 for pd in d_kly.device_class().parameter_definitions():
573 p = d.parameters.add()
574 p.id = pd.id()
575 p.name = pd.name
576 p.value = d_kly.parameter(pd.id())
578 for td in d_kly.device_class().terminal_definitions():
579 n: kdb.Net = d_kly.net_for_terminal(td.id())
580 net_name = n.name or f"${n.cluster_id}"
581 if n is None:
582 warning(f"Skipping terminal {td.name} of device {d.name} ({d.device_class}) "
583 f"is not connected to any net")
584 terminal = d.terminals.add()
585 terminal.id = td.id()
586 terminal.name = td.name
587 terminal.net_name = '' # TODO
588 continue
590 for nt in n.each_terminal():
591 nt: kdb.NetTerminalRef
593 if nt.device().expanded_name() != d_kly.expanded_name():
594 continue
595 if nt.terminal_id() != td.id():
596 continue
598 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt)
600 terminal = d.terminals.add()
601 terminal.device_id = d.id
602 terminal.terminal_id = td.id()
603 terminal.name = td.name
604 terminal.net_name = net_name
606 def add_region(gds_pair: GDSPair, region: kdb.Region):
607 region_by_layer = terminal.region_by_layer.add()
608 # NOTE: the annotated layout has a layer for each LVS layer, so several for a GDS pair
609 # (e.g. poly_con and poly_vpp), but the resistance extraction has the wires
610 # of a GDS pair on one of them, so the terminal must be on that one to be a port
611 region_by_layer.layer.id = self.annotated_layout.layer(*gds_pair)
612 region_by_layer.layer.canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[gds_pair]
613 shapes_converter.klayout_region_to_pb(region, region_by_layer.region)
615 for idx, shapes in shapes_by_lyr_idx.items():
616 lyr_idx = self.layer_index_map.get(idx, None)
617 if lyr_idx is None:
618 # NOTE: the tech info has no layer for the LVS layer of the terminal
619 # (e.g. the ports of a MOM cap, the metal pins within its marker),
620 # so the terminal is where it overlaps a conductor of its net,
621 # which LVS connected it to (e.g. the metal of the pins)
622 gds_pair_and_region = self.terminal_region_on_net(shapes, net_name)
623 if gds_pair_and_region is None:
624 key = (d.device_class_name, td.name, self.lvsdb.layer_name(idx))
625 devices_by_unknown_terminal_layer[key].append(d.device_name)
626 else:
627 add_region(*gds_pair_and_region)
628 continue
630 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[lyr_idx]
631 add_region((lyr_info.layer, lyr_info.datatype), shapes)
633 dd[d.device_name] = d
635 # NOTE: a terminal without a node stays on its net, which a node of the resistance network carries
636 # (#211 §6), so it misses only the resistance to where it is
637 if devices_by_unknown_terminal_layer:
638 def device_list(device_names: List[str]) -> str:
639 listed = ', '.join(device_names[:3])
640 return listed if len(device_names) <= 3 else f"{listed} and {len(device_names) - 3} more"
642 warning("The resistance network has no nodes for these device terminals, "
643 "as the tech info has no layer for their LVS layer, and they overlap no conductor of their net "
644 "(e.g. a terminal on the substrate or a well):\n" +
645 '\n'.join(f" - {device_class} terminal {terminal} on LVS layer {lvs_layer}: "
646 f"{device_list(device_names)}"
647 for (device_class, terminal, lvs_layer), device_names
648 in sorted(devices_by_unknown_terminal_layer.items())))
650 return dd
652 def terminal_region_on_net(self,
653 terminal_shapes: kdb.Region,
654 net_name: str) -> Optional[Tuple[GDSPair, kdb.Region]]:
655 """
656 :return: the part of the terminal shapes on the conductor of its net they overlap most,
657 the lowest of equals, or None if they overlap no conductor of their net
659 NOTE: one conductor, as the ports of a terminal are one node, which would short the resistance
660 between the conductors (e.g. a pin on the top metal of a MOM cap, over the fingers below)
661 """
662 lvsdb_layer_indexes = self.lvsdb.layer_indexes()
663 bbox = terminal_shapes.bbox()
664 best: Optional[Tuple[float, GDSPair, kdb.Region]] = None
665 for gds_pair in self.tech.process_conductor_gds_pairs:
666 lyr = self.extracted_layers.get(gds_pair, None)
667 if lyr is None:
668 continue
669 net_shapes = kdb.Region()
670 for sl in lyr.source_layers:
671 computed_layer_info = self.tech.computed_layer_info_by_name[sl.lvs_layer_name]
672 if computed_layer_info.kind == tech_pb2.ComputedLayerInfo.Kind.KIND_PIN:
673 continue
674 annotated_layer_index = self.layer_index_map[lvsdb_layer_indexes[sl.index]]
675 # NOTE: the shapes near the terminal only, the net may be huge (e.g. a supply)
676 iter = self.annotated_top_cell.begin_shapes_rec_touching(annotated_layer_index, bbox)
677 while not iter.at_end():
678 shape = iter.shape()
679 if shape.property('net') == net_name and not shape.is_text():
680 net_shapes.insert(iter.trans() * shape.polygon)
681 iter.next()
682 overlap = terminal_shapes & net_shapes
683 area = overlap.area()
684 if area > 0 and (best is None or area > best[0]):
685 best = (area, gds_pair, overlap)
686 return None if best is None else (best[1], best[2])
688 @cached_property
689 def pins_pb2_by_layer(self) -> Dict[GDSPair, List[pin_pb2.Pin]]:
690 d = defaultdict(list)
692 for lvs_gds_pair, lyr_info in self.extracted_layers.items():
693 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
694 # NOTE: LVS GDS Pair differs from real GDS Pair,
695 # as in some cases we want to split a layer into different regions (ptap vs ntap, cap vs ncap)
696 # so invent new datatype numbers, like adding 100 to the real GDS datatype
697 gds_pair = self.tech.gds_pair_for_layer_name.get(canonical_layer_name, None)
698 if gds_pair is None:
699 continue
700 if gds_pair not in self.tech.layer_info_by_gds_pair:
701 continue
703 for lyr in lyr_info.source_layers:
704 klayout_index = self.annotated_layout.layer(*lyr.gds_pair)
706 pins = self.pins_of_layer(gds_pair)
707 labels = self.labels_of_layer(gds_pair)
709 pin_labels: kdb.Texts = labels & pins
710 for l in pin_labels:
711 l: kdb.Text
712 # NOTE: because we want more like a point as a junction
713 # and folx create huge pins (covering the whole metal)
714 # we create our own "mini squares"
715 # (ResistorExtractor will subtract the pins from the metal polygons,
716 # so in the extreme case the polygons could become empty)
718 pin = pin_pb2.Pin()
719 pin.label = l.string
721 pos = l.position()
723 # is there more elegant / faster way to do this?
724 for p in pins:
725 p: kdb.PolygonWithProperties
726 if p.inside(pos):
727 pin.net_name = p.property('net')
728 break
730 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lyr.gds_pair]
731 lvs_layer_name = self.tech.computed_layer_info_by_gds_pair[lyr.gds_pair].layer_info.name
732 pin.layer.id = klayout_index
733 pin.layer.canonical_layer_name = canonical_layer_name
734 pin.layer.lvs_layer_name = lvs_layer_name
736 pin.label_point.x = pos.x
737 pin.label_point.y = pos.y
738 pin.label_point.net = pin.net_name
740 d[gds_pair].append(pin)
742 return d