Coverage for klayout_pex/tech_info.py: 88%
253 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-17 19:08 +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 .util.multiple_choice import MultipleChoicePattern
33from .log import (
34 warning
35)
37import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
38import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2
39import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2
41class TechDefError(Exception):
42 """A defect in the technology definition itself, not in a layout."""
45class TechInfo:
46 """Helper class for Protocol Buffer tech_pb2.Technology"""
48 LVSLayerName = str
49 CanonicalLayerName = str
50 GDSPair = Tuple[int, int]
52 @staticmethod
53 def duplicate_names(tech: tech_pb2.Technology) -> List[str]:
54 """
55 Report every name one of the technology's namespaces declares twice.
57 Each of them is looked up by name, and the process stack's is also the
58 PEX25D profile namespace, so a repeat adds nothing: it replaces or
59 drops whatever it collides with. sky130A declaring 'capild' once per
60 MiM-cap variant cost the second film, and left the dielectric above it
61 wrapping the metal three levels down.
62 """
63 namespaces: Dict[str, List[Tuple[str, str]]] = {
64 'process stack': [],
65 'layer': [],
66 'LVS computed layer': [],
67 }
69 for lyr in tech.process_stack.layers:
70 namespaces['process stack'].append((lyr.name, 'layer'))
71 parameters = lyr.WhichOneof('parameters')
72 # Only some layer kinds can carry a contact, and an unset one has
73 # no name, so the name is the test rather than the layer type.
74 contact = getattr(getattr(lyr, parameters), 'contact_above', None) \
75 if parameters else None
76 if contact is not None and contact.name:
77 namespaces['process stack'].append((contact.name, 'contact'))
79 namespaces['layer'] += [(lyr.name, 'layer') for lyr in tech.layers]
80 namespaces['LVS computed layer'] += [(lyr.layer_info.name, 'layer')
81 for lyr in tech.lvs_computed_layers]
83 problems: List[str] = []
84 for namespace, declarations in namespaces.items():
85 counts = Counter(name for name, _ in declarations)
86 for name, count in sorted(counts.items()):
87 if count == 1:
88 continue
89 kinds = sorted({kind for n, kind in declarations if n == name})
90 problems.append(
91 f"the {namespace} namespace declares '{name}' {count} times "
92 f"(as {', '.join(kinds)}), so all but the first declaration "
93 f"are dropped")
94 return problems
96 @staticmethod
97 def parse_tech_def(jsonpb_path: str) -> tech_pb2.Technology:
98 with open(jsonpb_path, 'r') as f:
99 contents = f.read()
100 tech = google.protobuf.json_format.Parse(contents, tech_pb2.Technology())
102 # Checked here rather than where a name is used: by then the collision
103 # has already happened, and what it cost is no longer visible.
104 problems = TechInfo.duplicate_names(tech)
105 if problems:
106 raise TechDefError(
107 f"Names have to be unique, but in {jsonpb_path}"
108 + ''.join(f"\n - {p}" for p in problems))
109 return tech
111 @classmethod
112 def from_json(cls,
113 jsonpb_path: str,
114 dielectric_filter: Optional[MultipleChoicePattern]) -> TechInfo:
115 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path)
116 return TechInfo(tech=tech,
117 dielectric_filter=dielectric_filter)
119 def __init__(self,
120 tech: tech_pb2.Technology,
121 dielectric_filter: Optional[MultipleChoicePattern]):
122 self.tech = tech
123 self.dielectric_filter = dielectric_filter or MultipleChoicePattern(pattern='all')
125 @cached_property
126 def gds_pair_for_computed_layer_name(self) -> Dict[LVSLayerName, GDSPair]:
127 return {lyr.layer_info.name: (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype)
128 for lyr in self.tech.lvs_computed_layers}
130 @cached_property
131 def computed_layer_info_by_name(self) -> Dict[LVSLayerName, tech_pb2.ComputedLayerInfo]:
132 return {lyr.layer_info.name: lyr for lyr in self.tech.lvs_computed_layers}
134 @cached_property
135 def computed_layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.ComputedLayerInfo]:
136 return {
137 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr
138 for lyr in self.tech.lvs_computed_layers
139 }
141 @cached_property
142 def canonical_layer_name_by_gds_pair(self) -> Dict[GDSPair, CanonicalLayerName]:
143 return {
144 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr.original_layer_name
145 for lyr in self.tech.lvs_computed_layers
146 }
148 @cached_property
149 def layer_info_by_name(self) -> Dict[CanonicalLayerName, tech_pb2.LayerInfo]:
150 return {lyr.name: lyr for lyr in self.tech.layers}
152 @cached_property
153 def pin_layer_mapping_for_drw_gds_pair(self) -> Dict[GDSPair, tech_pb2.PinLayerMapping]:
154 return {
155 (m.drw_gds_layer, m.drw_gds_datatype): (m.pin_gds_layer, m.pin_gds_datatype)
156 for m in self.tech.pin_layer_mappings
157 }
159 @cached_property
160 def gds_pair_for_layer_name(self) -> Dict[CanonicalLayerName, GDSPair]:
161 return {lyr.name: (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype) for lyr in self.tech.layers}
163 @cached_property
164 def layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.LayerInfo]:
165 return {(lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): lyr for lyr in self.tech.layers}
167 @cached_property
168 def process_stack_layer_by_name(self) -> Dict[LVSLayerName, process_stack_pb2.ProcessStackInfo.LayerInfo]:
169 return {lyr.name: lyr for lyr in self.tech.process_stack.layers}
171 @cached_property
172 def process_stack_layer_by_gds_pair(self) -> Dict[GDSPair, process_stack_pb2.ProcessStackInfo.LayerInfo]:
173 return {
174 (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): self.process_stack_layer_by_name[lyr.name]
175 for lyr in self.tech.process_stack.layers
176 }
178 @cached_property
179 def process_substrate_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
180 return list(
181 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE,
182 self.tech.process_stack.layers)
183 )[0]
185 @cached_property
186 def process_diffusion_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
187 return list(
188 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION,
189 self.tech.process_stack.layers)
190 )
192 @cached_property
193 def gate_poly_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
194 return self.process_metal_layers[0]
196 @cached_property
197 def field_oxide_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
198 return list(
199 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_FIELD_OXIDE,
200 self.tech.process_stack.layers)
201 )[0]
203 @cached_property
204 def process_metal_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
205 return list(
206 filter(lambda lyr: lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL,
207 self.tech.process_stack.layers)
208 )
210 @cached_property
211 def filtered_dielectric_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
212 layers = []
213 for pl in self.tech.process_stack.layers:
214 match pl.layer_type:
215 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \
216 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
217 if self.dielectric_filter.is_included(pl.name):
218 layers.append(pl)
219 return layers
221 @cached_property
222 def dielectric_by_name(self) -> Dict[str, float]:
223 diel_by_name = {}
224 for pl in self.filtered_dielectric_layers:
225 match pl.layer_type:
226 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
227 diel_by_name[pl.name] = pl.simple_dielectric_layer.dielectric_k
228 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
229 diel_by_name[pl.name] = pl.conformal_dielectric_layer.dielectric_k
230 return diel_by_name
232 def conformal_dielectric_wrapping(self, layer_name: str) \
233 -> Optional[process_stack_pb2.ProcessStackInfo.LayerInfo]:
234 """
235 The conformal dielectric anchored on ``layer_name``, if any.
237 Films form a chain: a metal is wrapped by a film, which may itself be
238 wrapped by the next one. Calling this repeatedly walks that chain.
239 """
240 found_layers: List[process_stack_pb2.ProcessStackInfo.LayerInfo] = []
241 for lyr in self.filtered_dielectric_layers:
242 match lyr.layer_type:
243 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
244 if lyr.conformal_dielectric_layer.reference == layer_name:
245 found_layers.append(lyr)
246 case _:
247 continue
249 if len(found_layers) == 0:
250 return None
251 if len(found_layers) >= 2:
252 raise Exception(f"found multiple conformal dielectric layers wrapping {layer_name}")
253 return found_layers[0]
255 def simple_dielectric_above_metal(self, layer_name: str) -> Tuple[Optional[process_stack_pb2.ProcessStackInfo.LayerInfo], float]:
256 """
257 Returns a tuple of the dielectric layer and it's (maximum) height.
258 Maximum would be the case where no metal and other dielectrics are present.
259 """
260 found_layer: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
261 diel_lyr: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
262 for lyr in self.tech.process_stack.layers:
263 if lyr.name == layer_name:
264 found_layer = lyr
265 elif found_layer:
266 if not diel_lyr and lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
267 if not self.dielectric_filter.is_included(lyr.name):
268 return None, 0.0
269 diel_lyr = lyr
270 # search for next metal or end of stack
271 if lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL:
272 return diel_lyr, lyr.metal_layer.z - found_layer.metal_layer.z
273 return diel_lyr, 5.0 # air TODO
275 @cached_property
276 def contact_above_metal_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
277 d = {}
278 for lyr in self.process_metal_layers:
279 contact = lyr.metal_layer.contact_above
280 via_gds_pair = self.gds_pair(contact)
281 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair]
282 d[lyr.name] = canonical_via_name
283 return d
285 @cached_property
286 def contact_by_device_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
287 d = {}
288 LT = process_stack_pb2.ProcessStackInfo.LayerType
289 for lyr in self.tech.process_stack.layers:
290 match lyr.layer_type:
291 case LT.LAYER_TYPE_NWELL:
292 d[lyr.name] = lyr.nwell_layer.contact_above
294 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
295 d[lyr.name] = lyr.diffusion_layer.contact_above
296 return d
298 @cached_property
299 def contact_by_contact_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
300 d = {}
301 LT = process_stack_pb2.ProcessStackInfo.LayerType
302 for lyr in self.tech.process_stack.layers:
303 match lyr.layer_type:
304 case LT.LAYER_TYPE_NWELL:
305 d[lyr.nwell_layer.contact_above.name] = lyr.nwell_layer.contact_above
307 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
308 d[lyr.diffusion_layer.contact_above.name] = lyr.diffusion_layer.contact_above
310 case LT.LAYER_TYPE_METAL:
311 d[lyr.metal_layer.contact_above.name] = lyr.metal_layer.contact_above
312 return d
314 def gds_pair(self, layer_name) -> Optional[GDSPair]:
315 gds_pair = self.gds_pair_for_computed_layer_name.get(layer_name, None)
316 if not gds_pair:
317 gds_pair = self.gds_pair_for_layer_name.get(layer_name, None)
318 if not gds_pair:
319 warning(f"Can't find GDS pair for layer {layer_name}")
320 return None
321 return gds_pair
323 @cached_property
324 def bottom_and_top_layer_name_by_via_computed_layer_name(self) -> Dict[str, Tuple[str, str]]:
325 # NOTE: vias under the same name can be used in multiple situations
326 # e.g. in sky130A, via3 has two (bot, top) cases: {(met3, met4), (met3, cmim)},
327 # therefore the canonical name must not be used,
328 # but really the LVS computed name, that is also used in the process stack
329 #
330 # the metal layers however are canonical!
332 d = {}
333 for metal_layer in self.process_metal_layers:
334 layer_name = metal_layer.name
335 gds_pair = self.gds_pair(layer_name)
337 if metal_layer.metal_layer.HasField('contact_above'):
338 contact = metal_layer.metal_layer.contact_above
339 d[contact.name] = (contact.layer_below, contact.metal_above)
341 return d
342 #--------------------------------
344 @cached_property
345 def layer_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.LayerResistance]:
346 return {r.layer_name: r for r in self.tech.process_parasitics.resistance.layers}
348 @cached_property
349 def contact_resistance_by_device_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ContactResistance]:
350 return {r.device_layer_name: r for r in self.tech.process_parasitics.resistance.contacts}
352 @cached_property
353 def via_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ViaResistance]:
354 return {r.via_name: r for r in self.tech.process_parasitics.resistance.vias}
356 @staticmethod
357 def milliohm_to_ohm(milliohm: float) -> float:
358 # NOTE: tech_pb2 has mΩ/µm^2
359 # RExtractorTech.Conductor.resistance is in Ω/µm^2
360 return milliohm / 1000.0
362 @staticmethod
363 def milliohm_by_cnt_to_ohm_by_square_for_contact(
364 contact: process_stack_pb2.ProcessStackInfo.Contact,
365 contact_resistance: process_parasitics_pb2.ResistanceInfo.ContactResistance) -> float:
366 # NOTE: ContactResistance ... mΩ/CNT
367 #
368 ohm_by_square = contact_resistance.resistance / 1000.0 * contact.width ** 2
369 return ohm_by_square
371 @staticmethod
372 def milliohm_by_cnt_to_ohm_by_square_for_via(
373 contact: process_stack_pb2.ProcessStackInfo.Contact,
374 via_resistance: process_parasitics_pb2.ResistanceInfo.ViaResistance) -> float:
375 ohm_by_square = via_resistance.resistance / 1000.0 * contact.width ** 2
376 return ohm_by_square
378 #--------------------------------
380 @cached_property
381 def substrate_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance]:
382 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.substrates}
384 @cached_property
385 def overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance]]:
386 """
387 usage: dict[top_layer_name][bottom_layer_name]
388 """
390 def convert_substrate_to_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
391 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance:
392 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance()
393 oc.top_layer_name = sc.layer_name
394 oc.bottom_layer_name = self.internal_substrate_layer_name
395 oc.capacitance = sc.area_capacitance
396 return oc
398 d = {
399 ln: {
400 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc)
401 } for ln, sc in self.substrate_cap_by_layer_name.items()
402 }
404 d2 = {
405 oc.top_layer_name: {
406 oc_bot.bottom_layer_name: oc_bot
407 for oc_bot in self.tech.process_parasitics.capacitance.overlaps if oc_bot.top_layer_name == oc.top_layer_name
408 }
409 for oc in self.tech.process_parasitics.capacitance.overlaps
410 }
412 for k1, ve in d2.items():
413 for k2, v in ve.items():
414 if k1 not in d:
415 d[k1] = {k2: v}
416 else:
417 d[k1][k2] = v
418 return d
420 @cached_property
421 def sidewall_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance]:
422 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.sidewalls}
424 @property
425 def internal_substrate_layer_name(self) -> str:
426 return 'VSUBS'
428 @cached_property
429 def side_overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance]]:
430 """
431 usage: dict[in_layer_name][out_layer_name]
432 """
434 def convert_substrate_to_side_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
435 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance:
436 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance()
437 soc.in_layer_name = sc.layer_name
438 soc.out_layer_name = self.internal_substrate_layer_name
439 soc.capacitance = sc.perimeter_capacitance
440 return soc
442 d = {
443 ln: {
444 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc)
445 } for ln, sc in self.substrate_cap_by_layer_name.items()
446 }
448 d2 = {
449 oc.in_layer_name: {
450 oc_bot.out_layer_name: oc_bot
451 for oc_bot in self.tech.process_parasitics.capacitance.sideoverlaps if oc_bot.in_layer_name == oc.in_layer_name
452 }
453 for oc in self.tech.process_parasitics.capacitance.sideoverlaps
454 }
456 for k1, ve in d2.items():
457 for k2, v in ve.items():
458 d[k1][k2] = v
460 return d