Coverage for klayout_pex/klayout/netlist_printer.py: 100%
67 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#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 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/iic-jku/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#
25from dataclasses import dataclass
26from datetime import datetime
27from pathlib import Path
28from typing import *
30import klayout.db as kdb
32from ..device_models import DeviceModels, rectangle_sides
33from .parasitic_device_classes import (
34 PARASITIC_CAPACITOR_CLASS_NAME,
35 PARASITIC_DEVICE_CLASS_NAMES,
36 PARASITIC_RESISTOR_CLASS_NAME,
37)
38from ..util.unit_formatter import format_spice_number
40import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2
43@dataclass
44class NetlistHeader:
45 kpex_version: str
46 extraction_engine: str
47 tech: str
48 date: datetime
51class NetlistPrinter(kdb.NetlistSpiceWriterDelegate):
52 def __init__(self,
53 header: NetlistHeader,
54 device_models: DeviceModels):
55 """
56 :param header: written as comments at the top of the netlist
57 :param device_models: how to write the devices of the LVS netlist
58 (the parasitics KPEX added are written as they are)
59 """
60 super().__init__()
62 self.header = header
63 self.device_models = device_models
65 self.spice_writer = kdb.NetlistSpiceWriter(self)
66 self.spice_writer.use_net_names = True
67 self.spice_writer.with_comments = False
69 def write(self,
70 netlist: kdb.Netlist,
71 output_path: str | Path):
72 # NOTE: the final netlist, the netlist expanders may have removed (whitebox) devices
73 self.device_models.check_mappings(netlist, ignored_device_class_names=PARASITIC_DEVICE_CLASS_NAMES)
74 netlist.write(output_path, self.spice_writer)
76 # --------------------------------------------------------------------------------
77 # NetlistSpiceWriterDelegate overwrites
79 def write_header(self, *args, **kwargs):
80 header_date = self.header.date.strftime("%Y-%m-%d %H:%M:%S")
82 self.emit_line(f"*********************************************************")
83 self.emit_line(f"*** NGSPICE file created by KLayout-PEX {self.header.kpex_version}")
84 self.emit_line(f"*** -----------------------------------------------------")
85 self.emit_line(f"*** Extraction Engine: {self.header.extraction_engine}")
86 self.emit_line(f"*** Technology: {self.header.tech}")
87 self.emit_line(f"*** Date: {header_date}")
88 self.emit_line(f"*********************************************************")
90 def write_device(self, device: kdb.Device):
91 dc = device.device_class()
92 if dc.name == PARASITIC_CAPACITOR_CLASS_NAME:
93 c_farad = device.parameter('C')
94 net1 = self.net_to_string(device.net_for_terminal(0))
95 net2 = self.net_to_string(device.net_for_terminal(1))
96 self.emit_line(f"C{device.name} {net1} {net2} {format_spice_number(c_farad)}")
97 elif dc.name == PARASITIC_RESISTOR_CLASS_NAME:
98 # NOTE: without a model name (KLayout's writer adds the device class name, which ngspice rejects)
99 r_ohm = device.parameter('R')
100 net1 = self.net_to_string(device.net_for_terminal(0))
101 net2 = self.net_to_string(device.net_for_terminal(1))
102 self.emit_line(f"R{device.name} {net1} {net2} {r_ohm:.12g}")
103 else:
104 self.write_modeled_device(device, self.device_models.mapping_by_lvs_device_class_name[dc.name])
106 def write_modeled_device(self,
107 device: kdb.Device,
108 device_model_mapping: device_models_pb2.DeviceModelMapping):
109 dc = device.device_class()
110 items = [f"{device_model_mapping.spice_prefix}{self.format_name(device.expanded_name())}"]
111 items += [self.net_to_string(device.net_for_terminal(dc.terminal_id(t)))
112 for t in device_model_mapping.terminal_names]
113 items.append(self.format_name(device_model_mapping.model_name or dc.name))
114 for p in device_model_mapping.parameters:
115 factor = p.lvs_parameter_factor if p.HasField('lvs_parameter_factor') else 1.0
116 match p.WhichOneof('value'):
117 case 'lvs_parameter_name':
118 value = device.parameter(p.lvs_parameter_name) * factor
119 case 'lvs_area_perimeter_side':
120 side = p.lvs_area_perimeter_side
121 long_side, short_side = rectangle_sides(area=device.parameter(side.area_parameter_name),
122 perimeter=device.parameter(side.perimeter_parameter_name))
123 is_long = side.side == device_models_pb2.LVSAreaPerimeterSide.SIDE_LONG
124 value = (long_side if is_long else short_side) * factor
125 case 'constant':
126 value = p.constant
127 items.append(f"{p.name}={value:.12g}")
128 self.emit_line(' '.join(items))