Coverage for klayout_pex/klayout/lvsdb_extractor.py: 56%
260 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 15:43 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-04 15:43 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/iic-jku/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
24from __future__ import annotations
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
60@dataclass
61class KLayoutExtractedLayerInfo:
62 index: int
63 lvs_layer_name: str # NOTE: this can be computed, so gds_pair is preferred
64 gds_pair: GDSPair
65 region: kdb.Region
68@dataclass
69class KLayoutMergedExtractedLayerInfo:
70 source_layers: List[KLayoutExtractedLayerInfo]
71 gds_pair: GDSPair
74@dataclass
75class KLayoutExtractionContext:
76 lvsdb: kdb.LayoutToNetlist
77 tech: TechInfo
78 dbu: float
79 layer_index_map: LayerIndexMap
80 lvsdb_regions: LVSDBRegions
81 cell_mapping: kdb.CellMapping
82 annotated_top_cell: kdb.Cell
83 annotated_layout: kdb.Layout
84 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo]
85 unnamed_layers: List[KLayoutExtractedLayerInfo]
87 @classmethod
88 def prepare_extraction(cls,
89 lvsdb: kdb.LayoutToNetlist,
90 top_cell: str,
91 tech: TechInfo,
92 blackbox_devices: bool) -> KLayoutExtractionContext:
93 dbu = lvsdb.internal_layout().dbu
94 annotated_layout = kdb.Layout()
95 annotated_layout.dbu = dbu
96 top_cell = annotated_layout.create_cell(top_cell)
98 # CellMapping
99 # mapping of internal layout to target layout for the circuit mapping
100 # https://www.klayout.de/doc-qt5/code/class_CellMapping.html
101 # ---
102 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
103 # Creates a cell mapping for copying shapes from the internal layout to the given target layout
104 cm = lvsdb.cell_mapping_into(annotated_layout, # target layout
105 top_cell,
106 not blackbox_devices) # with_device_cells
108 lvsdb_regions, layer_index_map = cls.build_LVS_layer_map(annotated_layout=annotated_layout,
109 lvsdb=lvsdb,
110 tech=tech,
111 blackbox_devices=blackbox_devices)
113 # NOTE: GDS only supports integer properties to GDS,
114 # as GDS does not support string keys,
115 # like OASIS does.
116 net_name_prop = "net"
118 # Build a full hierarchical representation of the nets
119 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method14
120 # hier_mode = None
121 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten
122 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells
124 lvsdb.build_all_nets(
125 cmap=cm, # mapping of internal layout to target layout for the circuit mapping
126 target=annotated_layout, # target layout
127 lmap=lvsdb_regions, # maps: target layer index => net regions
128 hier_mode=hier_mode, # hier mode
129 netname_prop=net_name_prop, # property name to which to attach the net name
130 circuit_cell_name_prefix="CIRCUIT_", # NOTE: generates a cell for each circuit
131 net_cell_name_prefix=None, # NOTE: this would generate a cell for each net
132 device_cell_name_prefix=None # NOTE: this would create a cell for each device (e.g. transistor)
133 )
135 extracted_layers, unnamed_layers = cls.nonempty_extracted_layers(lvsdb=lvsdb,
136 tech=tech,
137 annotated_layout=annotated_layout,
138 layer_index_map=layer_index_map,
139 blackbox_devices=blackbox_devices)
141 return KLayoutExtractionContext(
142 lvsdb=lvsdb,
143 tech=tech,
144 dbu=dbu,
145 annotated_top_cell=top_cell,
146 layer_index_map=layer_index_map,
147 lvsdb_regions=lvsdb_regions,
148 cell_mapping=cm,
149 annotated_layout=annotated_layout,
150 extracted_layers=extracted_layers,
151 unnamed_layers=unnamed_layers
152 )
154 @staticmethod
155 def build_LVS_layer_map(annotated_layout: kdb.Layout,
156 lvsdb: kdb.LayoutToNetlist,
157 tech: TechInfo,
158 blackbox_devices: bool) -> Tuple[LVSDBRegions, LayerIndexMap]:
159 # NOTE: currently, the layer numbers are auto-assigned
160 # by the sequence they occur in the LVS script, hence not well defined!
161 # build a layer map for the layers that correspond to original ones.
163 # https://www.klayout.de/doc-qt5/code/class_LayerInfo.html
164 lvsdb_regions: LVSDBRegions = {}
165 layer_index_map: LayerIndexMap = {}
167 if not hasattr(lvsdb, "layer_indexes"):
168 raise Exception("Needs at least KLayout version 0.29.2")
170 for layer_index in lvsdb.layer_indexes():
171 lname = lvsdb.layer_name(layer_index)
173 computed_layer_info = tech.computed_layer_info_by_name.get(lname, None)
174 if computed_layer_info and blackbox_devices:
175 match computed_layer_info.kind:
176 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
177 continue
178 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
179 continue
181 gds_pair = tech.gds_pair_for_computed_layer_name.get(lname, None)
182 if not gds_pair:
183 li = lvsdb.internal_layout().get_info(layer_index)
184 if li != kdb.LayerInfo():
185 gds_pair = (li.layer, li.datatype)
187 if gds_pair is not None:
188 annotated_layer_index = annotated_layout.layer() # creates new index each time!
189 # Creates a new internal layer! because multiple layers with the same gds_pair are possible!
190 annotated_layout.set_info(annotated_layer_index, kdb.LayerInfo(*gds_pair))
191 region = lvsdb.layer_by_index(layer_index)
192 lvsdb_regions[annotated_layer_index] = region
193 layer_index_map[layer_index] = annotated_layer_index
195 return lvsdb_regions, layer_index_map
197 @staticmethod
198 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist,
199 tech: TechInfo,
200 annotated_layout: kdb.Layout,
201 layer_index_map: LayerIndexMap,
202 blackbox_devices: bool) -> Tuple[Dict[GDSPair, KLayoutMergedExtractedLayerInfo], List[KLayoutExtractedLayerInfo]]:
203 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
204 nonempty_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] = {}
206 unnamed_layers: List[KLayoutExtractedLayerInfo] = []
207 lvsdb_layer_indexes = lvsdb.layer_indexes()
208 for idx, ln in enumerate(lvsdb.layer_names()):
209 li = lvsdb_layer_indexes[idx]
210 if li not in layer_index_map:
211 continue
212 li = layer_index_map[li]
213 layer = kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li))
214 layer.enable_properties()
215 if layer.count() >= 1:
216 computed_layer_info = tech.computed_layer_info_by_name.get(ln, None)
217 if not computed_layer_info:
218 warning(f"Unable to find info about extracted LVS layer '{ln}'")
219 gds_pair = (1000 + idx, 20)
220 linfo = KLayoutExtractedLayerInfo(
221 index=idx,
222 lvs_layer_name=ln,
223 gds_pair=gds_pair,
224 region=layer
225 )
226 unnamed_layers.append(linfo)
227 continue
229 if blackbox_devices:
230 match computed_layer_info.kind:
231 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
232 continue
233 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
234 continue
236 gds_pair = (computed_layer_info.layer_info.drw_gds_pair.layer,
237 computed_layer_info.layer_info.drw_gds_pair.datatype)
239 linfo = KLayoutExtractedLayerInfo(
240 index=idx,
241 lvs_layer_name=ln,
242 gds_pair=gds_pair,
243 region=layer
244 )
246 entry = nonempty_layers.get(gds_pair, None)
247 if entry:
248 entry.source_layers.append(linfo)
249 else:
250 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo(
251 source_layers=[linfo],
252 gds_pair=gds_pair,
253 )
255 return nonempty_layers, unnamed_layers
257 def top_cell_bbox(self) -> kdb.Box:
258 b1: kdb.Box = self.annotated_layout.top_cell().bbox()
259 b2: kdb.Box = self.lvsdb.internal_layout().top_cell().bbox()
260 if b1.area() > b2.area():
261 return b1
262 else:
263 return b2
265 def shapes_of_net(self, gds_pair: GDSPair, net: kdb.Net | str) -> Optional[kdb.Region]:
266 lyr = self.extracted_layers.get(gds_pair, None)
267 if not lyr:
268 return None
270 shapes = kdb.Region()
271 shapes.enable_properties()
273 requested_net_name = net.name if isinstance(net, kdb.Net) else net
275 def add_shapes_from_region(source_region: kdb.Region):
276 iter, transform = source_region.begin_shapes_rec()
277 while not iter.at_end():
278 shape = iter.shape()
279 net_name = shape.property('net')
280 if net_name == requested_net_name:
281 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
282 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
283 shape.polygon)
284 iter.next()
286 match len(lyr.source_layers):
287 case 0:
288 raise AssertionError('Internal error: Empty list of source_layers')
289 case _:
290 for sl in lyr.source_layers:
291 add_shapes_from_region(sl.region)
293 return shapes
295 def shapes_of_layer(self, gds_pair: GDSPair) -> Optional[kdb.Region]:
296 lyr = self.extracted_layers.get(gds_pair, None)
297 if not lyr:
298 return None
300 shapes: kdb.Region
302 match len(lyr.source_layers):
303 case 0:
304 raise AssertionError('Internal error: Empty list of source_layers')
305 case 1:
306 shapes = lyr.source_layers[0].region
307 case _:
308 # NOTE: currently a bug, for now use polygon-per-polygon workaround
309 # shapes = kdb.Region()
310 # for sl in lyr.source_layers:
311 # shapes += sl.region
312 shapes = kdb.Region()
313 shapes.enable_properties()
314 for sl in lyr.source_layers:
315 iter, transform = sl.region.begin_shapes_rec()
316 while not iter.at_end():
317 p = kdb.PolygonWithProperties(iter.shape().polygon, {'net': iter.shape().property('net')})
318 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
319 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
320 p)
321 iter.next()
323 return shapes
325 def pins_of_layer(self, gds_pair: GDSPair) -> kdb.Region:
326 pin_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].pin_gds_pair
327 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype
328 lyr = self.extracted_layers.get(pin_gds_pair, None)
329 if lyr is None:
330 return kdb.Region()
331 if len(lyr.source_layers) != 1:
332 raise NotImplementedError(f"currently only supporting 1 pin layer mapping, "
333 f"but got {len(lyr.source_layers)}")
334 return lyr.source_layers[0].region
336 def labels_of_layer(self, gds_pair: GDSPair) -> kdb.Texts:
337 labels_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].label_gds_pair
338 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype
340 lay: kdb.Layout = self.annotated_layout
341 label_layer_idx = lay.find_layer(labels_gds_pair) # sky130 layer dt = 5
342 if label_layer_idx is None:
343 return kdb.Texts()
345 sh_it = lay.begin_shapes(self.lvsdb.internal_top_cell(), label_layer_idx)
346 labels: kdb.Texts = kdb.Texts(sh_it)
347 return labels
349 @cached_property
350 def top_circuit(self) -> kdb.Circuit:
351 return self.lvsdb.netlist().top_circuit()
353 @cached_property
354 def devices_by_name(self) -> Dict[str, device_pb2.Device]:
355 dd = {}
357 shapes_converter = ShapesConverter(dbu=self.dbu)
359 for d_kly in self.top_circuit.each_device():
360 # https://www.klayout.de/doc-qt5/code/class_Device.html
361 d_kly: kdb.Device
363 d = device_pb2.Device()
364 d.id = d_kly.id()
365 d.device_name = d_kly.expanded_name()
366 d.device_class_name = d_kly.device_class().name
367 d.device_abstract_name = d_kly.device_abstract.name
369 for pd in d_kly.device_class().parameter_definitions():
370 p = d.parameters.add()
371 p.id = pd.id()
372 p.name = pd.name
373 p.value = d_kly.parameter(pd.id())
375 for td in d_kly.device_class().terminal_definitions():
376 n: kdb.Net = d_kly.net_for_terminal(td.id())
377 if n is None:
378 warning(f"Skipping terminal {td.name} of device {d.name} ({d.device_class}) "
379 f"is not connected to any net")
380 terminal = d.terminals.add()
381 terminal.id = td.id()
382 terminal.name = td.name
383 terminal.net_name = '' # TODO
384 continue
386 for nt in n.each_terminal():
387 nt: kdb.NetTerminalRef
389 if nt.device().expanded_name() != d_kly.expanded_name():
390 continue
391 if nt.terminal_id() != td.id():
392 continue
394 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt)
396 terminal = d.terminals.add()
397 terminal.device_id = d.id
398 terminal.terminal_id = td.id()
399 terminal.name = td.name
400 terminal.net_name = n.name
402 for idx, shapes in shapes_by_lyr_idx.items():
403 lyr_idx = self.layer_index_map[idx]
404 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[lyr_idx]
406 region_by_layer = terminal.region_by_layer.add()
407 region_by_layer.layer.id = lyr_idx
408 region_by_layer.layer.canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lyr_info.layer, lyr_info.datatype]
410 shapes_converter.klayout_region_to_pb(shapes, region_by_layer.region)
412 dd[d.device_name] = d
414 return dd
416 @cached_property
417 def pins_pb2_by_layer(self) -> Dict[GDSPair, List[pin_pb2.Pin]]:
418 d = defaultdict(list)
420 for lvs_gds_pair, lyr_info in self.extracted_layers.items():
421 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lvs_gds_pair]
422 # NOTE: LVS GDS Pair differs from real GDS Pair,
423 # as in some cases we want to split a layer into different regions (ptap vs ntap, cap vs ncap)
424 # so invent new datatype numbers, like adding 100 to the real GDS datatype
425 gds_pair = self.tech.gds_pair_for_layer_name.get(canonical_layer_name, None)
426 if gds_pair is None:
427 continue
428 if gds_pair not in self.tech.layer_info_by_gds_pair:
429 continue
431 for lyr in lyr_info.source_layers:
432 klayout_index = self.annotated_layout.layer(*lyr.gds_pair)
434 pins = self.pins_of_layer(gds_pair)
435 labels = self.labels_of_layer(gds_pair)
437 pin_labels: kdb.Texts = labels & pins
438 for l in pin_labels:
439 l: kdb.Text
440 # NOTE: because we want more like a point as a junction
441 # and folx create huge pins (covering the whole metal)
442 # we create our own "mini squares"
443 # (ResistorExtractor will subtract the pins from the metal polygons,
444 # so in the extreme case the polygons could become empty)
446 pin = pin_pb2.Pin()
447 pin.label = l.string
449 pos = l.position()
451 # is there more elegant / faster way to do this?
452 for p in pins:
453 p: kdb.PolygonWithProperties
454 if p.inside(pos):
455 pin.net_name = p.property('net')
456 break
458 canonical_layer_name = self.tech.canonical_layer_name_by_gds_pair[lyr.gds_pair]
459 lvs_layer_name = self.tech.computed_layer_info_by_gds_pair[lyr.gds_pair].layer_info.name
460 pin.layer.id = klayout_index
461 pin.layer.canonical_layer_name = canonical_layer_name
462 pin.layer.lvs_layer_name = lvs_layer_name
464 pin.label_point.x = pos.x
465 pin.label_point.y = pos.y
466 pin.label_point.net = pin.net_name
468 d[gds_pair].append(pin)
470 return d