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

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# 

24 

25from dataclasses import dataclass 

26from datetime import datetime 

27from pathlib import Path 

28from typing import * 

29 

30import klayout.db as kdb 

31 

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 

39 

40import klayout_pex_protobuf.kpex.tech.device_models_pb2 as device_models_pb2 

41 

42 

43@dataclass 

44class NetlistHeader: 

45 kpex_version: str 

46 extraction_engine: str 

47 tech: str 

48 date: datetime 

49 

50 

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__() 

61 

62 self.header = header 

63 self.device_models = device_models 

64 

65 self.spice_writer = kdb.NetlistSpiceWriter(self) 

66 self.spice_writer.use_net_names = True 

67 self.spice_writer.with_comments = False 

68 

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) 

75 

76 # -------------------------------------------------------------------------------- 

77 # NetlistSpiceWriterDelegate overwrites 

78 

79 def write_header(self, *args, **kwargs): 

80 header_date = self.header.date.strftime("%Y-%m-%d %H:%M:%S") 

81 

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"*********************************************************") 

89 

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]) 

105 

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))