Coverage for klayout_pex/netlist/spice_hierarchy.py: 100%

71 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-06 13:14 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024-2026 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 

25""" 

26The elements of a SPICE netlist in the hierarchy of its subcircuit calls, with their values evaluated 

27in the scopes of parameters as ngspice has them (as of ngspice-46): 

28 - a subcircuit call: its parameters, else the defaults of its subcircuit, and the .param lines 

29 of its subcircuit; the parameters of the call refer to this scope (e.g. ad='w*0.29' w=1u), 

30 but to the caller's parameter of their own name (e.g. w='w*2') 

31 - then the scope of its caller, e.g. of the call of the calling subcircuit, up to the .param lines 

32 of the top level 

33 - the values of another element refer to the scope of its subcircuit call 

34 

35NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it 

36""" 

37 

38from __future__ import annotations 

39from dataclasses import dataclass, replace 

40from functools import partial 

41from itertools import chain 

42from typing import * 

43 

44from .expression.nodes import ExpressionError, NumberOrExpression 

45from .expression.scope import ParameterScope 

46from .spice_netlist import SpiceElement, SpiceNetlist, SpiceSubcircuit 

47 

48 

49class SpiceEvaluationError(Exception): 

50 """ 

51 A value of the netlist without number, e.g. an expression of a parameter that is not defined 

52 """ 

53 pass 

54 

55 

56@dataclass(frozen=True) 

57class HierarchyElement: 

58 path: str # the names of the subcircuit calls and of the element, e.g. X1.XM2 

59 element: SpiceElement # with its value and parameters as numbers (see SpiceHierarchy.flat_elements) 

60 

61 

62class SpiceHierarchy: 

63 def __init__(self, netlist: SpiceNetlist): 

64 self.netlist = netlist 

65 self._subcircuit_by_key = {s.name.casefold(): s for s in netlist.subcircuits} 

66 self._top_scope = ParameterScope(netlist.parameters) 

67 

68 def subcircuit(self, name: str) -> Optional[SpiceSubcircuit]: 

69 """ 

70 The subcircuit of that name, in any case 

71 """ 

72 return self._subcircuit_by_key.get(name.casefold()) 

73 

74 def uncalled_subcircuits(self) -> List[SpiceSubcircuit]: 

75 """ 

76 The subcircuits no element calls, e.g. the cells of a schematic 

77 """ 

78 elements = chain(self.netlist.elements, *(s.elements for s in self.netlist.subcircuits)) 

79 called = {e.model.casefold() for e in elements if e.kind == 'X' and e.model is not None} 

80 return [s for s in self.netlist.subcircuits if s.name.casefold() not in called] 

81 

82 def flat_elements(self, 

83 subcircuit: Optional[SpiceSubcircuit] = None, 

84 parameter_names: Optional[Callable[[SpiceElement], Collection[str]]] = None 

85 ) -> Iterator[HierarchyElement]: 

86 """ 

87 The elements of the subcircuit (with the defaults of its parameters), or else of the top level, 

88 with the calls of the subcircuits of the netlist as their elements, recursively 

89 

90 :param parameter_names: the parameters to evaluate of an element, which then has only those, and no value, 

91 as the others may have no number here (e.g. of a .param of a model library 

92 the netlist doesn't include); by default all of them, and its value 

93 :raises SpiceEvaluationError: if a value has no number (e.g. of a parameter that is not defined), 

94 or a subcircuit calls itself 

95 """ 

96 if subcircuit is None: 

97 return self._flat_elements(self.netlist.elements, self._top_scope, '', (), parameter_names) 

98 return self._flat_elements(subcircuit.elements, self._call_scope(None, subcircuit, self._top_scope), 

99 '', (subcircuit.name.casefold(),), parameter_names) 

100 

101 # -------------------------------------------------------------------------------- 

102 

103 @staticmethod 

104 def _call_scope(call: Optional[SpiceElement], 

105 subcircuit: Optional[SpiceSubcircuit], 

106 caller_scope: ParameterScope) -> ParameterScope: 

107 """ 

108 :param call: None for the subcircuit of the top cell 

109 :param subcircuit: None for a call of a subcircuit the netlist doesn't define (e.g. a device of a PDK) 

110 """ 

111 definitions: Dict[str, NumberOrExpression] = {} 

112 if subcircuit is not None: 

113 definitions.update(subcircuit.parameters) 

114 if call is not None: 

115 definitions.update(call.parameters) 

116 if subcircuit is not None: 

117 definitions.update(subcircuit.local_parameters) 

118 return ParameterScope(definitions, parent=caller_scope) 

119 

120 def _flat_elements(self, 

121 elements: List[SpiceElement], 

122 scope: ParameterScope, 

123 path: str, 

124 calls: Tuple[str, ...], 

125 parameter_names: Optional[Callable[[SpiceElement], Collection[str]]] 

126 ) -> Iterator[HierarchyElement]: 

127 for element in elements: 

128 name = f"{path}{element.name}" 

129 if element.kind != 'X': 

130 yield HierarchyElement(path=name, element=self._evaluated(element, scope, name, parameter_names)) 

131 continue 

132 subcircuit = self.subcircuit(element.model) if element.model is not None else None 

133 call_scope = self._call_scope(element, subcircuit, scope) 

134 evaluated = self._evaluated(element, call_scope, name, parameter_names) 

135 if subcircuit is None: 

136 yield HierarchyElement(path=name, element=evaluated) 

137 continue 

138 key = subcircuit.name.casefold() 

139 if key in calls: 

140 raise SpiceEvaluationError(f"{name} calls the subcircuit {subcircuit.name} within itself") 

141 yield from self._flat_elements(subcircuit.elements, call_scope, f"{name}.", calls + (key,), 

142 parameter_names) 

143 

144 @staticmethod 

145 def _evaluated(element: SpiceElement, 

146 scope: ParameterScope, 

147 path: str, 

148 parameter_names: Optional[Callable[[SpiceElement], Collection[str]]]) -> SpiceElement: 

149 """ 

150 :param scope: of the element, or of the call for a subcircuit call (which has its parameters) 

151 :param parameter_names: see flat_elements 

152 """ 

153 def evaluate(what: str, value: NumberOrExpression, evaluation: Callable[[], float]) -> float: 

154 try: 

155 return evaluation() 

156 except ExpressionError as e: 

157 raise SpiceEvaluationError(f"the {what} {value} of {path}: {e}") from None 

158 

159 names = element.parameters if parameter_names is None else parameter_names(element) 

160 to_evaluate = {n: v for n, v in element.parameters.items() if n in names} 

161 if element.kind == 'X': 

162 parameters = {n: evaluate(f"parameter {n} =", v, partial(scope.value, n)) 

163 for n, v in to_evaluate.items()} 

164 else: 

165 parameters = {n: evaluate(f"parameter {n} =", v, partial(scope.evaluate, v)) 

166 for n, v in to_evaluate.items()} 

167 value = None 

168 if parameter_names is None and element.value is not None: 

169 value = evaluate('value', element.value, partial(scope.evaluate, element.value)) 

170 return replace(element, value=value, parameters=parameters)