Coverage for klayout_pex/tech_info.py: 89%
278 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#! /usr/bin/env python3
2#
3# --------------------------------------------------------------------------------
4# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
5# Johannes Kepler University, Institute for Integrated Circuits.
6#
7# This file is part of KPEX
8# (see https://github.com/iic-jku/klayout-pex).
9#
10# This program is free software: you can redistribute it and/or modify
11# it under the terms of the GNU General Public License as published by
12# the Free Software Foundation, either version 3 of the License, or
13# (at your option) any later version.
14#
15# This program is distributed in the hope that it will be useful,
16# but WITHOUT ANY WARRANTY; without even the implied warranty of
17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18# GNU General Public License for more details.
19#
20# You should have received a copy of the GNU General Public License
21# along with this program. If not, see <http://www.gnu.org/licenses/>.
22# SPDX-License-Identifier: GPL-3.0-or-later
23# --------------------------------------------------------------------------------
24#
26from __future__ import annotations # allow class type hints within same class
27from typing import *
28from collections import Counter
29from functools import cached_property
30import google.protobuf.json_format
32from .device_models import DeviceModels
33from .util.multiple_choice import MultipleChoicePattern
34from .log import (
35 warning
36)
38import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
39import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2
40import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2
42class TechDefError(Exception):
43 """A defect in the technology definition itself, not in a layout."""
46class TechInfo:
47 """Helper class for Protocol Buffer tech_pb2.Technology"""
49 LVSLayerName = str
50 CanonicalLayerName = str
51 GDSPair = Tuple[int, int]
53 @staticmethod
54 def duplicate_names(tech: tech_pb2.Technology) -> List[str]:
55 """
56 Report every name one of the technology's namespaces declares twice.
58 Each of them is looked up by name, and the process stack's is also the
59 PEX25D profile namespace, so a repeat adds nothing: it replaces or
60 drops whatever it collides with. sky130A declaring 'capild' once per
61 MiM-cap variant cost the second film, and left the dielectric above it
62 wrapping the metal three levels down.
63 """
64 namespaces: Dict[str, List[Tuple[str, str]]] = {
65 'process stack': [],
66 'layer': [],
67 'LVS computed layer': [],
68 'device model mapping': [],
69 }
71 for lyr in tech.process_stack.layers:
72 namespaces['process stack'].append((lyr.name, 'layer'))
73 parameters = lyr.WhichOneof('parameters')
74 # Only some layer kinds can carry a contact, and an unset one has
75 # no name, so the name is the test rather than the layer type.
76 contact = getattr(getattr(lyr, parameters), 'contact_above', None) \
77 if parameters else None
78 if contact is not None and contact.name:
79 namespaces['process stack'].append((contact.name, 'contact'))
81 namespaces['layer'] += [(lyr.name, 'layer') for lyr in tech.layers]
82 namespaces['LVS computed layer'] += [(lyr.layer_info.name, 'layer')
83 for lyr in tech.lvs_computed_layers]
84 namespaces['device model mapping'] += [(dm.lvs_device_class_name, 'LVS device class')
85 for dm in tech.device_models.device_model_mappings]
87 problems: List[str] = []
88 for namespace, declarations in namespaces.items():
89 counts = Counter(name for name, _ in declarations)
90 for name, count in sorted(counts.items()):
91 if count == 1:
92 continue
93 kinds = sorted({kind for n, kind in declarations if n == name})
94 problems.append(
95 f"the {namespace} namespace declares '{name}' {count} times "
96 f"(as {', '.join(kinds)}), so all but the first declaration "
97 f"are dropped")
98 return problems
100 @staticmethod
101 def parse_tech_def(jsonpb_path: str) -> tech_pb2.Technology:
102 with open(jsonpb_path, 'r') as f:
103 contents = f.read()
104 tech = google.protobuf.json_format.Parse(contents, tech_pb2.Technology())
106 # Checked here rather than where a name is used: by then the collision
107 # has already happened, and what it cost is no longer visible.
108 problems = TechInfo.duplicate_names(tech)
109 if problems:
110 raise TechDefError(
111 f"Names have to be unique, but in {jsonpb_path}"
112 + ''.join(f"\n - {p}" for p in problems))
113 return tech
115 @classmethod
116 def from_json(cls,
117 jsonpb_path: str,
118 dielectric_filter: Optional[MultipleChoicePattern]) -> TechInfo:
119 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path)
120 return TechInfo(tech=tech,
121 dielectric_filter=dielectric_filter)
123 def __init__(self,
124 tech: tech_pb2.Technology,
125 dielectric_filter: Optional[MultipleChoicePattern]):
126 self.tech = tech
127 self.dielectric_filter = dielectric_filter or MultipleChoicePattern(pattern='all')
129 @cached_property
130 def gds_pair_for_computed_layer_name(self) -> Dict[LVSLayerName, GDSPair]:
131 return {lyr.layer_info.name: (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype)
132 for lyr in self.tech.lvs_computed_layers}
134 @cached_property
135 def computed_layer_info_by_name(self) -> Dict[LVSLayerName, tech_pb2.ComputedLayerInfo]:
136 return {lyr.layer_info.name: lyr for lyr in self.tech.lvs_computed_layers}
138 @cached_property
139 def device_models(self) -> DeviceModels:
140 return DeviceModels(self.tech.device_models)
142 @cached_property
143 def computed_layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.ComputedLayerInfo]:
144 return {
145 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr
146 for lyr in self.tech.lvs_computed_layers
147 }
149 @cached_property
150 def canonical_layer_name_by_gds_pair(self) -> Dict[GDSPair, CanonicalLayerName]:
151 return {
152 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr.original_layer_name
153 for lyr in self.tech.lvs_computed_layers
154 }
156 @cached_property
157 def layer_info_by_name(self) -> Dict[CanonicalLayerName, tech_pb2.LayerInfo]:
158 return {lyr.name: lyr for lyr in self.tech.layers}
160 @cached_property
161 def pin_layer_mapping_for_drw_gds_pair(self) -> Dict[GDSPair, tech_pb2.PinLayerMapping]:
162 return {
163 (m.drw_gds_layer, m.drw_gds_datatype): (m.pin_gds_layer, m.pin_gds_datatype)
164 for m in self.tech.pin_layer_mappings
165 }
167 @cached_property
168 def gds_pair_for_layer_name(self) -> Dict[CanonicalLayerName, GDSPair]:
169 return {lyr.name: (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype) for lyr in self.tech.layers}
171 @cached_property
172 def layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.LayerInfo]:
173 return {(lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): lyr for lyr in self.tech.layers}
175 @cached_property
176 def process_stack_layer_by_name(self) -> Dict[LVSLayerName, process_stack_pb2.ProcessStackInfo.LayerInfo]:
177 return {lyr.name: lyr for lyr in self.tech.process_stack.layers}
179 @cached_property
180 def process_stack_layer_by_gds_pair(self) -> Dict[GDSPair, process_stack_pb2.ProcessStackInfo.LayerInfo]:
181 return {
182 (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): self.process_stack_layer_by_name[lyr.name]
183 for lyr in self.tech.process_stack.layers
184 }
186 @cached_property
187 def process_substrate_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
188 return list(
189 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE,
190 self.tech.process_stack.layers)
191 )[0]
193 @cached_property
194 def process_conductor_gds_pairs(self) -> List[GDSPair]:
195 """
196 The GDS pairs of the conductors of the process stack (its diffusion and metal layers), from the bottom up
197 """
198 gds_pairs: List[TechInfo.GDSPair] = []
199 for lyr in self.tech.process_stack.layers:
200 if lyr.layer_type not in (process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION,
201 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL):
202 continue
203 gds_pair = self.gds_pair_for_computed_layer_name.get(lyr.name, None) or \
204 self.gds_pair_for_layer_name.get(lyr.name, None)
205 if gds_pair is not None and gds_pair not in gds_pairs:
206 gds_pairs.append(gds_pair)
207 return gds_pairs
209 @cached_property
210 def process_diffusion_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
211 return list(
212 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION,
213 self.tech.process_stack.layers)
214 )
216 @cached_property
217 def process_diffusion_layer_names(self) -> List[CanonicalLayerName]:
218 """
219 The canonical names of the diffusion layers, which the capacitances are looked up by
220 (e.g. nsdm and psdm, of the LVS layers nsd and psd)
221 """
222 gds_pairs = [self.gds_pair(lyr.name) for lyr in self.process_diffusion_layers]
223 return [self.canonical_layer_name_by_gds_pair[gds_pair] for gds_pair in gds_pairs if gds_pair is not None]
225 @cached_property
226 def device_capacitance_layer_names(self) -> Set[CanonicalLayerName]:
227 """
228 The layers between which the diffusion has the capacitances of the devices (see is_device_capacitance)
229 """
230 gate_poly_layer_name = self.canonical_layer_name_by_gds_pair.get(self.gds_pair(self.gate_poly_layer.name), None)
231 return {self.internal_substrate_layer_name, gate_poly_layer_name, *self.process_diffusion_layer_names}
233 def is_device_capacitance(self, layer_name1: CanonicalLayerName, layer_name2: CanonicalLayerName) -> bool:
234 """
235 Whether the capacitance between the layers is the one of a device, which its model has,
236 rather than a parasitic one: between the diffusion and the substrate or another diffusion (the junctions,
237 e.g. by the AS, AD, PS and PD of a transistor), or the gate poly (the overlap of the gate)
238 """
239 return (layer_name1 in self.process_diffusion_layer_names or layer_name2 in self.process_diffusion_layer_names) \
240 and layer_name1 in self.device_capacitance_layer_names \
241 and layer_name2 in self.device_capacitance_layer_names
243 @cached_property
244 def gate_poly_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
245 return self.process_metal_layers[0]
247 @cached_property
248 def field_oxide_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
249 return list(
250 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_FIELD_OXIDE,
251 self.tech.process_stack.layers)
252 )[0]
254 @cached_property
255 def process_metal_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
256 return list(
257 filter(lambda lyr: lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL,
258 self.tech.process_stack.layers)
259 )
261 @cached_property
262 def filtered_dielectric_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
263 layers = []
264 for pl in self.tech.process_stack.layers:
265 match pl.layer_type:
266 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \
267 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
268 if self.dielectric_filter.is_included(pl.name):
269 layers.append(pl)
270 return layers
272 @cached_property
273 def dielectric_by_name(self) -> Dict[str, float]:
274 diel_by_name = {}
275 for pl in self.filtered_dielectric_layers:
276 match pl.layer_type:
277 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
278 diel_by_name[pl.name] = pl.simple_dielectric_layer.dielectric_k
279 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
280 diel_by_name[pl.name] = pl.conformal_dielectric_layer.dielectric_k
281 return diel_by_name
283 def conformal_dielectric_wrapping(self, layer_name: str) \
284 -> Optional[process_stack_pb2.ProcessStackInfo.LayerInfo]:
285 """
286 The conformal dielectric anchored on ``layer_name``, if any.
288 Films form a chain: a metal is wrapped by a film, which may itself be
289 wrapped by the next one. Calling this repeatedly walks that chain.
290 """
291 found_layers: List[process_stack_pb2.ProcessStackInfo.LayerInfo] = []
292 for lyr in self.filtered_dielectric_layers:
293 match lyr.layer_type:
294 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
295 if lyr.conformal_dielectric_layer.reference == layer_name:
296 found_layers.append(lyr)
297 case _:
298 continue
300 if len(found_layers) == 0:
301 return None
302 if len(found_layers) >= 2:
303 raise Exception(f"found multiple conformal dielectric layers wrapping {layer_name}")
304 return found_layers[0]
306 def simple_dielectric_above_metal(self, layer_name: str) -> Tuple[Optional[process_stack_pb2.ProcessStackInfo.LayerInfo], float]:
307 """
308 Returns a tuple of the dielectric layer and it's (maximum) height.
309 Maximum would be the case where no metal and other dielectrics are present.
310 """
311 found_layer: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
312 diel_lyr: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
313 for lyr in self.tech.process_stack.layers:
314 if lyr.name == layer_name:
315 found_layer = lyr
316 elif found_layer:
317 if not diel_lyr and lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
318 if not self.dielectric_filter.is_included(lyr.name):
319 return None, 0.0
320 diel_lyr = lyr
321 # search for next metal or end of stack
322 if lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL:
323 return diel_lyr, lyr.metal_layer.z - found_layer.metal_layer.z
324 return diel_lyr, 5.0 # air TODO
326 @cached_property
327 def contact_above_metal_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
328 d = {}
329 for lyr in self.process_metal_layers:
330 contact = lyr.metal_layer.contact_above
331 via_gds_pair = self.gds_pair(contact)
332 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair]
333 d[lyr.name] = canonical_via_name
334 return d
336 @cached_property
337 def contact_by_device_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
338 d = {}
339 LT = process_stack_pb2.ProcessStackInfo.LayerType
340 for lyr in self.tech.process_stack.layers:
341 match lyr.layer_type:
342 case LT.LAYER_TYPE_NWELL:
343 d[lyr.name] = lyr.nwell_layer.contact_above
345 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
346 d[lyr.name] = lyr.diffusion_layer.contact_above
347 return d
349 @cached_property
350 def contact_by_contact_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
351 d = {}
352 LT = process_stack_pb2.ProcessStackInfo.LayerType
353 for lyr in self.tech.process_stack.layers:
354 match lyr.layer_type:
355 case LT.LAYER_TYPE_NWELL:
356 d[lyr.nwell_layer.contact_above.name] = lyr.nwell_layer.contact_above
358 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
359 d[lyr.diffusion_layer.contact_above.name] = lyr.diffusion_layer.contact_above
361 case LT.LAYER_TYPE_METAL:
362 d[lyr.metal_layer.contact_above.name] = lyr.metal_layer.contact_above
363 return d
365 def gds_pair(self, layer_name) -> Optional[GDSPair]:
366 gds_pair = self.gds_pair_for_computed_layer_name.get(layer_name, None)
367 if not gds_pair:
368 gds_pair = self.gds_pair_for_layer_name.get(layer_name, None)
369 if not gds_pair:
370 warning(f"Can't find GDS pair for layer {layer_name}")
371 return None
372 return gds_pair
374 @cached_property
375 def bottom_and_top_layer_name_by_via_computed_layer_name(self) -> Dict[str, Tuple[str, str]]:
376 # NOTE: vias under the same name can be used in multiple situations
377 # e.g. in sky130A, via3 has two (bot, top) cases: {(met3, met4), (met3, cmim)},
378 # therefore the canonical name must not be used,
379 # but really the LVS computed name, that is also used in the process stack
380 #
381 # the metal layers however are canonical!
383 d = {}
384 for metal_layer in self.process_metal_layers:
385 layer_name = metal_layer.name
386 gds_pair = self.gds_pair(layer_name)
388 if metal_layer.metal_layer.HasField('contact_above'):
389 contact = metal_layer.metal_layer.contact_above
390 d[contact.name] = (contact.layer_below, contact.metal_above)
392 return d
393 #--------------------------------
395 @cached_property
396 def layer_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.LayerResistance]:
397 return {r.layer_name: r for r in self.tech.process_parasitics.resistance.layers}
399 @cached_property
400 def contact_resistance_by_device_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ContactResistance]:
401 return {r.device_layer_name: r for r in self.tech.process_parasitics.resistance.contacts}
403 @cached_property
404 def via_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ViaResistance]:
405 return {r.via_name: r for r in self.tech.process_parasitics.resistance.vias}
407 @staticmethod
408 def milliohm_to_ohm(milliohm: float) -> float:
409 # NOTE: tech_pb2 has mΩ/µm^2
410 # RExtractorTech.Conductor.resistance is in Ω/µm^2
411 return milliohm / 1000.0
413 @staticmethod
414 def milliohm_by_cnt_to_ohm_by_square_for_contact(
415 contact: process_stack_pb2.ProcessStackInfo.Contact,
416 contact_resistance: process_parasitics_pb2.ResistanceInfo.ContactResistance) -> float:
417 # NOTE: ContactResistance ... mΩ/CNT
418 #
419 ohm_by_square = contact_resistance.resistance / 1000.0 * contact.width ** 2
420 return ohm_by_square
422 @staticmethod
423 def milliohm_by_cnt_to_ohm_by_square_for_via(
424 contact: process_stack_pb2.ProcessStackInfo.Contact,
425 via_resistance: process_parasitics_pb2.ResistanceInfo.ViaResistance) -> float:
426 ohm_by_square = via_resistance.resistance / 1000.0 * contact.width ** 2
427 return ohm_by_square
429 #--------------------------------
431 @cached_property
432 def substrate_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance]:
433 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.substrates}
435 @cached_property
436 def overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance]]:
437 """
438 usage: dict[top_layer_name][bottom_layer_name]
439 """
441 def convert_substrate_to_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
442 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance:
443 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance()
444 oc.top_layer_name = sc.layer_name
445 oc.bottom_layer_name = self.internal_substrate_layer_name
446 oc.capacitance = sc.area_capacitance
447 return oc
449 d = {
450 ln: {
451 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc)
452 } for ln, sc in self.substrate_cap_by_layer_name.items()
453 }
455 d2 = {
456 oc.top_layer_name: {
457 oc_bot.bottom_layer_name: oc_bot
458 for oc_bot in self.tech.process_parasitics.capacitance.overlaps if oc_bot.top_layer_name == oc.top_layer_name
459 }
460 for oc in self.tech.process_parasitics.capacitance.overlaps
461 }
463 for k1, ve in d2.items():
464 for k2, v in ve.items():
465 if k1 not in d:
466 d[k1] = {k2: v}
467 else:
468 d[k1][k2] = v
469 return d
471 @cached_property
472 def sidewall_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance]:
473 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.sidewalls}
475 @property
476 def internal_substrate_layer_name(self) -> str:
477 return 'VSUBS'
479 @cached_property
480 def side_overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance]]:
481 """
482 usage: dict[in_layer_name][out_layer_name]
483 """
485 def convert_substrate_to_side_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
486 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance:
487 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance()
488 soc.in_layer_name = sc.layer_name
489 soc.out_layer_name = self.internal_substrate_layer_name
490 soc.capacitance = sc.perimeter_capacitance
491 return soc
493 d = {
494 ln: {
495 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc)
496 } for ln, sc in self.substrate_cap_by_layer_name.items()
497 }
499 d2 = {
500 oc.in_layer_name: {
501 oc_bot.out_layer_name: oc_bot
502 for oc_bot in self.tech.process_parasitics.capacitance.sideoverlaps if oc_bot.in_layer_name == oc.in_layer_name
503 }
504 for oc in self.tech.process_parasitics.capacitance.sideoverlaps
505 }
507 for k1, ve in d2.items():
508 for k2, v in ve.items():
509 d[k1][k2] = v
511 return d