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
« 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#
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
35NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it
36"""
38from __future__ import annotations
39from dataclasses import dataclass, replace
40from functools import partial
41from itertools import chain
42from typing import *
44from .expression.nodes import ExpressionError, NumberOrExpression
45from .expression.scope import ParameterScope
46from .spice_netlist import SpiceElement, SpiceNetlist, SpiceSubcircuit
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
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)
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)
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())
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]
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
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)
101 # --------------------------------------------------------------------------------
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)
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)
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
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)