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