Coverage for klayout_pex/klayout/lvsdb_extractor.py: 70%
222 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-12 13:45 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-12 13:45 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/martinjankoehler/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
24from __future__ import annotations
26from dataclasses import dataclass
27from functools import cached_property
28import tempfile
29from typing import *
31from rich.pretty import pprint
33import klayout.db as kdb
35import klayout_pex_protobuf.tech_pb2 as tech_pb2
36from ..log import (
37 console,
38 debug,
39 info,
40 warning,
41 error,
42 rule
43)
45from ..tech_info import TechInfo
48GDSPair = Tuple[int, int]
50LayerIndexMap = Dict[int, int] # maps layer indexes of LVSDB to annotated_layout
51LVSDBRegions = Dict[int, kdb.Region] # maps layer index of annotated_layout to LVSDB region
54@dataclass
55class KLayoutExtractedLayerInfo:
56 index: int
57 lvs_layer_name: str # NOTE: this can be computed, so gds_pair is preferred
58 gds_pair: GDSPair
59 region: kdb.Region
62@dataclass
63class KLayoutMergedExtractedLayerInfo:
64 source_layers: List[KLayoutExtractedLayerInfo]
65 gds_pair: GDSPair
68@dataclass
69class KLayoutDeviceTerminal:
70 id: int
71 name: str
72 regions_by_layer_name: Dict[str, kdb.Region]
73 net_name: str
75 # internal data access
76 net_terminal_ref: Optional[kdb.NetTerminalRef]
77 net: Optional[kdb.Net]
80@dataclass
81class KLayoutDeviceTerminalList:
82 terminals: List[KLayoutDeviceTerminal]
85@dataclass
86class KLayoutDeviceInfo:
87 id: str
88 name: str # expanded name
89 class_name: str
90 abstract_name: str
92 terminals: KLayoutDeviceTerminalList
93 params: Dict[str, str]
95 # internal data access
96 device: kdb.Device
99@dataclass
100class KLayoutExtractionContext:
101 lvsdb: kdb.LayoutToNetlist
102 tech: TechInfo
103 dbu: float
104 layer_index_map: LayerIndexMap
105 lvsdb_regions: LVSDBRegions
106 cell_mapping: kdb.CellMapping
107 annotated_top_cell: kdb.Cell
108 annotated_layout: kdb.Layout
109 extracted_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo]
110 unnamed_layers: List[KLayoutExtractedLayerInfo]
112 @classmethod
113 def prepare_extraction(cls,
114 lvsdb: kdb.LayoutToNetlist,
115 top_cell: str,
116 tech: TechInfo,
117 blackbox_devices: bool) -> KLayoutExtractionContext:
118 dbu = lvsdb.internal_layout().dbu
119 annotated_layout = kdb.Layout()
120 annotated_layout.dbu = dbu
121 top_cell = annotated_layout.create_cell(top_cell)
123 # CellMapping
124 # mapping of internal layout to target layout for the circuit mapping
125 # https://www.klayout.de/doc-qt5/code/class_CellMapping.html
126 # ---
127 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
128 # Creates a cell mapping for copying shapes from the internal layout to the given target layout
129 cm = lvsdb.cell_mapping_into(annotated_layout, # target layout
130 top_cell,
131 not blackbox_devices) # with_device_cells
133 lvsdb_regions, layer_index_map = cls.build_LVS_layer_map(annotated_layout=annotated_layout,
134 lvsdb=lvsdb,
135 tech=tech,
136 blackbox_devices=blackbox_devices)
138 # NOTE: GDS only supports integer properties to GDS,
139 # as GDS does not support string keys,
140 # like OASIS does.
141 net_name_prop = "net"
143 # Build a full hierarchical representation of the nets
144 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method14
145 # hier_mode = None
146 hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_Flatten
147 # hier_mode = kdb.LayoutToNetlist.BuildNetHierarchyMode.BNH_SubcircuitCells
149 lvsdb.build_all_nets(
150 cmap=cm, # mapping of internal layout to target layout for the circuit mapping
151 target=annotated_layout, # target layout
152 lmap=lvsdb_regions, # maps: target layer index => net regions
153 hier_mode=hier_mode, # hier mode
154 netname_prop=net_name_prop, # property name to which to attach the net name
155 circuit_cell_name_prefix="CIRCUIT_", # NOTE: generates a cell for each circuit
156 net_cell_name_prefix=None, # NOTE: this would generate a cell for each net
157 device_cell_name_prefix=None # NOTE: this would create a cell for each device (e.g. transistor)
158 )
160 extracted_layers, unnamed_layers = cls.nonempty_extracted_layers(lvsdb=lvsdb,
161 tech=tech,
162 annotated_layout=annotated_layout,
163 layer_index_map=layer_index_map,
164 blackbox_devices=blackbox_devices)
166 return KLayoutExtractionContext(
167 lvsdb=lvsdb,
168 tech=tech,
169 dbu=dbu,
170 annotated_top_cell=top_cell,
171 layer_index_map=layer_index_map,
172 lvsdb_regions=lvsdb_regions,
173 cell_mapping=cm,
174 annotated_layout=annotated_layout,
175 extracted_layers=extracted_layers,
176 unnamed_layers=unnamed_layers
177 )
179 @staticmethod
180 def build_LVS_layer_map(annotated_layout: kdb.Layout,
181 lvsdb: kdb.LayoutToNetlist,
182 tech: TechInfo,
183 blackbox_devices: bool) -> Tuple[LVSDBRegions, LayerIndexMap]:
184 # NOTE: currently, the layer numbers are auto-assigned
185 # by the sequence they occur in the LVS script, hence not well defined!
186 # build a layer map for the layers that correspond to original ones.
188 # https://www.klayout.de/doc-qt5/code/class_LayerInfo.html
189 lvsdb_regions: LVSDBRegions = {}
190 layer_index_map: LayerIndexMap = {}
192 if not hasattr(lvsdb, "layer_indexes"):
193 raise Exception("Needs at least KLayout version 0.29.2")
195 for layer_index in lvsdb.layer_indexes():
196 lname = lvsdb.layer_name(layer_index)
198 computed_layer_info = tech.computed_layer_info_by_name.get(lname, None)
199 if computed_layer_info and blackbox_devices:
200 match computed_layer_info.kind:
201 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
202 continue
203 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
204 continue
206 gds_pair = tech.gds_pair_for_computed_layer_name.get(lname, None)
207 if not gds_pair:
208 li = lvsdb.internal_layout().get_info(layer_index)
209 if li != kdb.LayerInfo():
210 gds_pair = (li.layer, li.datatype)
212 if gds_pair is not None:
213 annotated_layer_index = annotated_layout.layer() # creates new index each time!
214 # Creates a new internal layer! because multiple layers with the same gds_pair are possible!
215 annotated_layout.set_info(annotated_layer_index, kdb.LayerInfo(*gds_pair))
216 region = lvsdb.layer_by_index(layer_index)
217 lvsdb_regions[annotated_layer_index] = region
218 layer_index_map[layer_index] = annotated_layer_index
220 return lvsdb_regions, layer_index_map
222 @staticmethod
223 def nonempty_extracted_layers(lvsdb: kdb.LayoutToNetlist,
224 tech: TechInfo,
225 annotated_layout: kdb.Layout,
226 layer_index_map: LayerIndexMap,
227 blackbox_devices: bool) -> Tuple[Dict[GDSPair, KLayoutMergedExtractedLayerInfo], List[KLayoutExtractedLayerInfo]]:
228 # https://www.klayout.de/doc-qt5/code/class_LayoutToNetlist.html#method18
229 nonempty_layers: Dict[GDSPair, KLayoutMergedExtractedLayerInfo] = {}
231 unnamed_layers: List[KLayoutExtractedLayerInfo] = []
232 lvsdb_layer_indexes = lvsdb.layer_indexes()
233 for idx, ln in enumerate(lvsdb.layer_names()):
234 li = lvsdb_layer_indexes[idx]
235 if li not in layer_index_map:
236 continue
237 li = layer_index_map[li]
238 layer = kdb.Region(annotated_layout.top_cell().begin_shapes_rec(li))
239 layer.enable_properties()
240 if layer.count() >= 1:
241 computed_layer_info = tech.computed_layer_info_by_name.get(ln, None)
242 if not computed_layer_info:
243 warning(f"Unable to find info about extracted LVS layer '{ln}'")
244 gds_pair = (1000 + idx, 20)
245 linfo = KLayoutExtractedLayerInfo(
246 index=idx,
247 lvs_layer_name=ln,
248 gds_pair=gds_pair,
249 region=layer
250 )
251 unnamed_layers.append(linfo)
252 continue
254 if blackbox_devices:
255 match computed_layer_info.kind:
256 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_RESISTOR:
257 continue
258 case tech_pb2.ComputedLayerInfo.Kind.KIND_DEVICE_CAPACITOR:
259 continue
261 gds_pair = (computed_layer_info.layer_info.drw_gds_pair.layer,
262 computed_layer_info.layer_info.drw_gds_pair.datatype)
264 linfo = KLayoutExtractedLayerInfo(
265 index=idx,
266 lvs_layer_name=ln,
267 gds_pair=gds_pair,
268 region=layer
269 )
271 entry = nonempty_layers.get(gds_pair, None)
272 if entry:
273 entry.source_layers.append(linfo)
274 else:
275 nonempty_layers[gds_pair] = KLayoutMergedExtractedLayerInfo(
276 source_layers=[linfo],
277 gds_pair=gds_pair,
278 )
280 return nonempty_layers, unnamed_layers
282 def top_cell_bbox(self) -> kdb.Box:
283 b1: kdb.Box = self.annotated_layout.top_cell().bbox()
284 b2: kdb.Box = self.lvsdb.internal_layout().top_cell().bbox()
285 if b1.area() > b2.area():
286 return b1
287 else:
288 return b2
290 def shapes_of_net(self, gds_pair: GDSPair, net: kdb.Net) -> Optional[kdb.Region]:
291 lyr = self.extracted_layers.get(gds_pair, None)
292 if not lyr:
293 return None
295 shapes = kdb.Region()
296 shapes.enable_properties()
298 def add_shapes_from_region(source_region: kdb.Region):
299 iter, transform = source_region.begin_shapes_rec()
300 while not iter.at_end():
301 shape = iter.shape()
302 net_name = shape.property('net')
303 if net_name == net.name:
304 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
305 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
306 shape.polygon)
307 iter.next()
309 match len(lyr.source_layers):
310 case 0:
311 raise AssertionError('Internal error: Empty list of source_layers')
312 case _:
313 for sl in lyr.source_layers:
314 add_shapes_from_region(sl.region)
316 return shapes
318 def shapes_of_layer(self, gds_pair: GDSPair) -> Optional[kdb.Region]:
319 lyr = self.extracted_layers.get(gds_pair, None)
320 if not lyr:
321 return None
323 shapes: kdb.Region
325 match len(lyr.source_layers):
326 case 0:
327 raise AssertionError('Internal error: Empty list of source_layers')
328 case 1:
329 shapes = lyr.source_layers[0].region
330 case _:
331 # NOTE: currently a bug, for now use polygon-per-polygon workaround
332 # shapes = kdb.Region()
333 # for sl in lyr.source_layers:
334 # shapes += sl.region
335 shapes = kdb.Region()
336 shapes.enable_properties()
337 for sl in lyr.source_layers:
338 iter, transform = sl.region.begin_shapes_rec()
339 while not iter.at_end():
340 p = kdb.PolygonWithProperties(iter.shape().polygon, {'net': iter.shape().property('net')})
341 shapes.insert(transform * # NOTE: this is a global/initial iterator-wide transformation
342 iter.trans() * # NOTE: this is local during the iteration (due to sub hierarchy)
343 p)
344 iter.next()
346 return shapes
348 def pins_of_layer(self, gds_pair: GDSPair) -> kdb.Region:
349 pin_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].pin_gds_pair
350 pin_gds_pair = pin_gds_pair.layer, pin_gds_pair.datatype
351 lyr = self.extracted_layers.get(pin_gds_pair, None)
352 if lyr is None:
353 return kdb.Region()
354 if len(lyr.source_layers) != 1:
355 raise NotImplementedError(f"currently only supporting 1 pin layer mapping, "
356 f"but got {len(lyr.source_layers)}")
357 return lyr.source_layers[0].region
359 def labels_of_layer(self, gds_pair: GDSPair) -> kdb.Texts:
360 labels_gds_pair = self.tech.layer_info_by_gds_pair[gds_pair].label_gds_pair
361 labels_gds_pair = labels_gds_pair.layer, labels_gds_pair.datatype
363 lay: kdb.Layout = self.lvsdb.internal_layout()
364 label_layer_idx = lay.find_layer(labels_gds_pair) # sky130 layer dt = 5
365 if label_layer_idx is None:
366 return kdb.Texts()
368 sh_it = lay.begin_shapes(self.lvsdb.internal_top_cell(), label_layer_idx)
369 labels: kdb.Texts = kdb.Texts(sh_it)
370 return labels
372 @cached_property
373 def top_circuit(self) -> kdb.Circuit:
374 return self.lvsdb.netlist().top_circuit()
376 @cached_property
377 def devices_by_name(self) -> Dict[str, KLayoutDeviceInfo]:
378 dd = {}
380 for d in self.top_circuit.each_device():
381 # https://www.klayout.de/doc-qt5/code/class_Device.html
382 d: kdb.Device
384 param_defs = d.device_class().parameter_definitions()
385 params_by_name = {pd.name: d.parameter(pd.id()) for pd in param_defs}
387 terminals: List[KLayoutDeviceTerminal] = []
389 for td in d.device_class().terminal_definitions():
390 n: kdb.Net = d.net_for_terminal(td.id())
391 if n is None:
392 warning(f"Skipping terminal {td.name} of device {d.expanded_name()} ({d.device_class().name}) "
393 f"is not connected to any net")
394 terminals.append(
395 KLayoutDeviceTerminal(
396 id=td.id(),
397 name=td.name,
398 regions_by_layer_name={},
399 net_name='',
400 net_terminal_ref=None,
401 net=None
402 )
403 )
404 continue
406 for nt in n.each_terminal():
407 nt: kdb.NetTerminalRef
409 if nt.device().expanded_name() != d.expanded_name():
410 continue
411 if nt.terminal_id() != td.id():
412 continue
414 shapes_by_lyr_idx = self.lvsdb.shapes_of_terminal(nt)
416 def layer_name(idx: int) -> str:
417 lyr_info: kdb.LayerInfo = self.annotated_layout.layer_infos()[self.layer_index_map[idx]]
418 return self.tech.canonical_layer_name_by_gds_pair[lyr_info.layer, lyr_info.datatype]
420 shapes_by_lyr_name = {layer_name(idx): shapes for idx, shapes in shapes_by_lyr_idx.items()}
422 terminals.append(
423 KLayoutDeviceTerminal(
424 id=td.id(),
425 name=td.name,
426 regions_by_layer_name=shapes_by_lyr_name,
427 net_name=n.name,
428 net_terminal_ref=nt,
429 net=n
430 )
431 )
433 dd[d.expanded_name()] = KLayoutDeviceInfo(
434 id=d.id(),
435 name=d.expanded_name(),
436 class_name=d.device_class().name,
437 abstract_name=d.device_abstract.name,
438 params=params_by_name,
439 terminals=KLayoutDeviceTerminalList(terminals=terminals),
440 device=d
441 )
443 return dd