Coverage for klayout_pex/tech_info.py: 40%
231 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +0000
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-08 18:54 +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/martinjankoehler/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 functools import cached_property
29import google.protobuf.json_format
31from .util.multiple_choice import MultipleChoicePattern
32from .log import (
33 warning
34)
36import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
37import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as process_stack_pb2
38import klayout_pex_protobuf.kpex.tech.process_parasitics_pb2 as process_parasitics_pb2
40class TechInfo:
41 """Helper class for Protocol Buffer tech_pb2.Technology"""
43 GDSPair = Tuple[int, int]
45 @staticmethod
46 def parse_tech_def(jsonpb_path: str) -> tech_pb2.Technology:
47 with open(jsonpb_path, 'r') as f:
48 contents = f.read()
49 tech = google.protobuf.json_format.Parse(contents, tech_pb2.Technology())
50 return tech
52 @classmethod
53 def from_json(cls,
54 jsonpb_path: str,
55 dielectric_filter: Optional[MultipleChoicePattern]) -> TechInfo:
56 tech = cls.parse_tech_def(jsonpb_path=jsonpb_path)
57 return TechInfo(tech=tech,
58 dielectric_filter=dielectric_filter)
60 def __init__(self,
61 tech: tech_pb2.Technology,
62 dielectric_filter: Optional[MultipleChoicePattern]):
63 self.tech = tech
64 self.dielectric_filter = dielectric_filter or MultipleChoicePattern(pattern='all')
66 @cached_property
67 def gds_pair_for_computed_layer_name(self) -> Dict[str, GDSPair]:
68 return {lyr.layer_info.name: (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype)
69 for lyr in self.tech.lvs_computed_layers}
71 @cached_property
72 def computed_layer_info_by_name(self) -> Dict[str, tech_pb2.ComputedLayerInfo]:
73 return {lyr.layer_info.name: lyr for lyr in self.tech.lvs_computed_layers}
75 @cached_property
76 def computed_layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.ComputedLayerInfo]:
77 return {
78 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr
79 for lyr in self.tech.lvs_computed_layers
80 }
82 @cached_property
83 def canonical_layer_name_by_gds_pair(self) -> Dict[GDSPair, str]:
84 return {
85 (lyr.layer_info.drw_gds_pair.layer, lyr.layer_info.drw_gds_pair.datatype): lyr.original_layer_name
86 for lyr in self.tech.lvs_computed_layers
87 }
89 @cached_property
90 def layer_info_by_name(self) -> Dict[str, tech_pb2.LayerInfo]:
91 return {lyr.name: lyr for lyr in self.tech.layers}
93 @cached_property
94 def pin_layer_mapping_for_drw_gds_pair(self) -> Dict[GDSPair, tech_pb2.PinLayerMapping]:
95 return {
96 (m.drw_gds_layer, m.drw_gds_datatype): (m.pin_gds_layer, m.pin_gds_datatype)
97 for m in self.tech.pin_layer_mappings
98 }
100 @cached_property
101 def gds_pair_for_layer_name(self) -> Dict[str, GDSPair]:
102 return {lyr.name: (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype) for lyr in self.tech.layers}
104 @cached_property
105 def layer_info_by_gds_pair(self) -> Dict[GDSPair, tech_pb2.LayerInfo]:
106 return {(lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): lyr for lyr in self.tech.layers}
108 @cached_property
109 def process_stack_layer_by_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.LayerInfo]:
110 return {lyr.name: lyr for lyr in self.tech.process_stack.layers}
112 @cached_property
113 def process_stack_layer_by_gds_pair(self) -> Dict[GDSPair, process_stack_pb2.ProcessStackInfo.LayerInfo]:
114 return {
115 (lyr.drw_gds_pair.layer, lyr.drw_gds_pair.datatype): self.process_stack_layer_by_name[lyr.name]
116 for lyr in self.tech.process_stack.layers
117 }
119 @cached_property
120 def process_substrate_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
121 return list(
122 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE,
123 self.tech.process_stack.layers)
124 )[0]
126 @cached_property
127 def process_diffusion_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
128 return list(
129 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_DIFFUSION,
130 self.tech.process_stack.layers)
131 )
133 @cached_property
134 def gate_poly_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
135 return self.process_metal_layers[0]
137 @cached_property
138 def field_oxide_layer(self) -> process_stack_pb2.ProcessStackInfo.LayerInfo:
139 return list(
140 filter(lambda lyr: lyr.layer_type is process_stack_pb2.ProcessStackInfo.LAYER_TYPE_FIELD_OXIDE,
141 self.tech.process_stack.layers)
142 )[0]
144 @cached_property
145 def process_metal_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
146 return list(
147 filter(lambda lyr: lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL,
148 self.tech.process_stack.layers)
149 )
151 @cached_property
152 def filtered_dielectric_layers(self) -> List[process_stack_pb2.ProcessStackInfo.LayerInfo]:
153 layers = []
154 for pl in self.tech.process_stack.layers:
155 match pl.layer_type:
156 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC | \
157 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC | \
158 process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC:
159 if self.dielectric_filter.is_included(pl.name):
160 layers.append(pl)
161 return layers
163 @cached_property
164 def dielectric_by_name(self) -> Dict[str, float]:
165 diel_by_name = {}
166 for pl in self.filtered_dielectric_layers:
167 match pl.layer_type:
168 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
169 diel_by_name[pl.name] = pl.simple_dielectric_layer.dielectric_k
170 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
171 diel_by_name[pl.name] = pl.conformal_dielectric_layer.dielectric_k
172 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC:
173 diel_by_name[pl.name] = pl.sidewall_dielectric_layer.dielectric_k
174 return diel_by_name
176 def sidewall_dielectric_layer(self, layer_name: str) -> Optional[process_stack_pb2.ProcessStackInfo.LayerInfo]:
177 found_layers: List[process_stack_pb2.ProcessStackInfo.LayerInfo] = []
178 for lyr in self.filtered_dielectric_layers:
179 match lyr.layer_type:
180 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIDEWALL_DIELECTRIC:
181 if lyr.sidewall_dielectric_layer.reference == layer_name:
182 found_layers.append(lyr)
183 case process_stack_pb2.ProcessStackInfo.LAYER_TYPE_CONFORMAL_DIELECTRIC:
184 if lyr.conformal_dielectric_layer.reference == layer_name:
185 found_layers.append(lyr)
186 case _:
187 continue
189 if len(found_layers) == 0:
190 return None
191 if len(found_layers) >= 2:
192 raise Exception(f"found multiple sidewall dielectric layers for {layer_name}")
193 return found_layers[0]
195 def simple_dielectric_above_metal(self, layer_name: str) -> Tuple[Optional[process_stack_pb2.ProcessStackInfo.LayerInfo], float]:
196 """
197 Returns a tuple of the dielectric layer and it's (maximum) height.
198 Maximum would be the case where no metal and other dielectrics are present.
199 """
200 found_layer: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
201 diel_lyr: Optional[process_stack_pb2.ProcessStackInfo.LayerInfo] = None
202 for lyr in self.tech.process_stack.layers:
203 if lyr.name == layer_name:
204 found_layer = lyr
205 elif found_layer:
206 if not diel_lyr and lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC:
207 if not self.dielectric_filter.is_included(lyr.name):
208 return None, 0.0
209 diel_lyr = lyr
210 # search for next metal or end of stack
211 if lyr.layer_type == process_stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL:
212 return diel_lyr, lyr.metal_layer.z - found_layer.metal_layer.z
213 return diel_lyr, 5.0 # air TODO
215 @cached_property
216 def contact_above_metal_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
217 d = {}
218 for lyr in self.process_metal_layers:
219 contact = lyr.metal_layer.contact_above
220 via_gds_pair = self.gds_pair(contact)
221 canonical_via_name = self.canonical_layer_name_by_gds_pair[via_gds_pair]
222 d[lyr.name] = canonical_via_name
223 return d
225 @cached_property
226 def contact_by_device_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
227 d = {}
228 LT = process_stack_pb2.ProcessStackInfo.LayerType
229 for lyr in self.tech.process_stack.layers:
230 match lyr.layer_type:
231 case LT.LAYER_TYPE_NWELL:
232 d[lyr.name] = lyr.nwell_layer.contact_above
234 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
235 d[lyr.name] = lyr.diffusion_layer.contact_above
236 return d
238 @cached_property
239 def contact_by_contact_lvs_layer_name(self) -> Dict[str, process_stack_pb2.ProcessStackInfo.Contact]:
240 d = {}
241 LT = process_stack_pb2.ProcessStackInfo.LayerType
242 for lyr in self.tech.process_stack.layers:
243 match lyr.layer_type:
244 case LT.LAYER_TYPE_NWELL:
245 d[lyr.nwell_layer.contact_above.name] = lyr.nwell_layer.contact_above
247 case LT.LAYER_TYPE_DIFFUSION: # nsdm or psdm
248 d[lyr.diffusion_layer.contact_above.name] = lyr.diffusion_layer.contact_above
250 case LT.LAYER_TYPE_METAL:
251 d[lyr.metal_layer.contact_above.name] = lyr.metal_layer.contact_above
252 return d
254 def gds_pair(self, layer_name) -> Optional[GDSPair]:
255 gds_pair = self.gds_pair_for_computed_layer_name.get(layer_name, None)
256 if not gds_pair:
257 gds_pair = self.gds_pair_for_layer_name.get(layer_name, None)
258 if not gds_pair:
259 warning(f"Can't find GDS pair for layer {layer_name}")
260 return None
261 return gds_pair
263 @cached_property
264 def bottom_and_top_layer_name_by_via_computed_layer_name(self) -> Dict[str, Tuple[str, str]]:
265 # NOTE: vias under the same name can be used in multiple situations
266 # e.g. in sky130A, via3 has two (bot, top) cases: {(met3, met4), (met3, cmim)},
267 # therefore the canonical name must not be used,
268 # but really the LVS computed name, that is also used in the process stack
269 #
270 # the metal layers however are canonical!
272 d = {}
273 for metal_layer in self.process_metal_layers:
274 layer_name = metal_layer.name
275 gds_pair = self.gds_pair(layer_name)
277 if metal_layer.metal_layer.HasField('contact_above'):
278 contact = metal_layer.metal_layer.contact_above
279 d[contact.name] = (contact.layer_below, contact.metal_above)
281 return d
282 #--------------------------------
284 @cached_property
285 def layer_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.LayerResistance]:
286 return {r.layer_name: r for r in self.tech.process_parasitics.resistance.layers}
288 @cached_property
289 def contact_resistance_by_device_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ContactResistance]:
290 return {r.device_layer_name: r for r in self.tech.process_parasitics.resistance.contacts}
292 @cached_property
293 def via_resistance_by_layer_name(self) -> Dict[str, process_parasitics_pb2.ResistanceInfo.ViaResistance]:
294 return {r.via_name: r for r in self.tech.process_parasitics.resistance.vias}
296 @staticmethod
297 def milliohm_to_ohm(milliohm: float) -> float:
298 # NOTE: tech_pb2 has mΩ/µm^2
299 # RExtractorTech.Conductor.resistance is in Ω/µm^2
300 return milliohm / 1000.0
302 @staticmethod
303 def milliohm_by_cnt_to_ohm_by_square_for_contact(
304 contact: process_stack_pb2.ProcessStackInfo.Contact,
305 contact_resistance: process_parasitics_pb2.ResistanceInfo.ContactResistance) -> float:
306 # NOTE: ContactResistance ... mΩ/CNT
307 #
308 ohm_by_square = contact_resistance.resistance / 1000.0 * contact.width ** 2
309 return ohm_by_square
311 @staticmethod
312 def milliohm_by_cnt_to_ohm_by_square_for_via(
313 contact: process_stack_pb2.ProcessStackInfo.Contact,
314 via_resistance: process_parasitics_pb2.ResistanceInfo.ViaResistance) -> float:
315 ohm_by_square = via_resistance.resistance / 1000.0 * contact.width ** 2
316 return ohm_by_square
318 #--------------------------------
320 @cached_property
321 def substrate_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance]:
322 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.substrates}
324 @cached_property
325 def overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance]]:
326 """
327 usage: dict[top_layer_name][bottom_layer_name]
328 """
330 def convert_substrate_to_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
331 -> process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance:
332 oc = process_parasitics_pb2.CapacitanceInfo.OverlapCapacitance()
333 oc.top_layer_name = sc.layer_name
334 oc.bottom_layer_name = self.internal_substrate_layer_name
335 oc.capacitance = sc.area_capacitance
336 return oc
338 d = {
339 ln: {
340 self.internal_substrate_layer_name: convert_substrate_to_overlap_cap(sc)
341 } for ln, sc in self.substrate_cap_by_layer_name.items()
342 }
344 d2 = {
345 oc.top_layer_name: {
346 oc_bot.bottom_layer_name: oc_bot
347 for oc_bot in self.tech.process_parasitics.capacitance.overlaps if oc_bot.top_layer_name == oc.top_layer_name
348 }
349 for oc in self.tech.process_parasitics.capacitance.overlaps
350 }
352 for k1, ve in d2.items():
353 for k2, v in ve.items():
354 if k1 not in d:
355 d[k1] = {k2: v}
356 else:
357 d[k1][k2] = v
358 return d
360 @cached_property
361 def sidewall_cap_by_layer_name(self) -> Dict[str, process_parasitics_pb2.CapacitanceInfo.SidewallCapacitance]:
362 return {sc.layer_name: sc for sc in self.tech.process_parasitics.capacitance.sidewalls}
364 @property
365 def internal_substrate_layer_name(self) -> str:
366 return 'VSUBS'
368 @cached_property
369 def side_overlap_cap_by_layer_names(self) -> Dict[str, Dict[str, process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance]]:
370 """
371 usage: dict[in_layer_name][out_layer_name]
372 """
374 def convert_substrate_to_side_overlap_cap(sc: process_parasitics_pb2.CapacitanceInfo.SubstrateCapacitance) \
375 -> process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance:
376 soc = process_parasitics_pb2.CapacitanceInfo.SideOverlapCapacitance()
377 soc.in_layer_name = sc.layer_name
378 soc.out_layer_name = self.internal_substrate_layer_name
379 soc.capacitance = sc.perimeter_capacitance
380 return soc
382 d = {
383 ln: {
384 self.internal_substrate_layer_name: convert_substrate_to_side_overlap_cap(sc)
385 } for ln, sc in self.substrate_cap_by_layer_name.items()
386 }
388 d2 = {
389 oc.in_layer_name: {
390 oc_bot.out_layer_name: oc_bot
391 for oc_bot in self.tech.process_parasitics.capacitance.sideoverlaps if oc_bot.in_layer_name == oc.in_layer_name
392 }
393 for oc in self.tech.process_parasitics.capacitance.sideoverlaps
394 }
396 for k1, ve in d2.items():
397 for k2, v in ve.items():
398 d[k1][k2] = v
400 return d