Coverage for klayout_pex/netlist/expression/nodes.py: 100%

104 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""" 

26Expressions of netlist parameters (e.g. w*nf), as trees of their operations, and their evaluation. 

27 

28The tree doesn't depend on how a netlist flavour writes the expression (see syntax.py), and its operators 

29compute as in C: e.g. % is the remainder with the sign of the dividend, a comparison is 1 or 0. 

30 

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

32""" 

33 

34from __future__ import annotations 

35from abc import ABC, abstractmethod 

36from dataclasses import dataclass 

37from enum import Enum 

38import math 

39import operator 

40from typing import * 

41 

42 

43class ExpressionError(Exception): 

44 """ 

45 An expression without value, e.g. of a parameter that is not defined, or with a division by zero 

46 """ 

47 pass 

48 

49 

50class Scope(Protocol): 

51 """ 

52 The values of the parameters an expression refers to 

53 """ 

54 

55 def value(self, name: str) -> float: 

56 """ 

57 :raises ExpressionError: if the scope has no parameter of that name, or it has no value 

58 """ 

59 ... 

60 

61 

62def _checked(name: str, function: Callable[..., float], *arguments: float) -> float: 

63 """ 

64 :raises ExpressionError: if the function has no finite value for the arguments, e.g. sqrt(-1) 

65 """ 

66 try: 

67 result = function(*arguments) 

68 except ZeroDivisionError: 

69 raise ExpressionError("division by zero") from None 

70 except (ArithmeticError, ValueError): 

71 result = math.nan 

72 if not math.isfinite(result): 

73 raise ExpressionError(f"{name}({', '.join(f'{a:g}' for a in arguments)}) has no finite value") 

74 return float(result) 

75 

76 

77class UnaryOperator(Enum): 

78 NEGATIVE = 'negative' 

79 POSITIVE = 'positive' 

80 NOT = 'not' 

81 

82 

83class BinaryOperator(Enum): 

84 OR = 'or' 

85 AND = 'and' 

86 EQUAL = 'equal' 

87 NOT_EQUAL = 'not equal' 

88 LESS = 'less' 

89 LESS_EQUAL = 'less or equal' 

90 GREATER = 'greater' 

91 GREATER_EQUAL = 'greater or equal' 

92 ADD = 'add' 

93 SUBTRACT = 'subtract' 

94 MULTIPLY = 'multiply' 

95 DIVIDE = 'divide' 

96 REMAINDER = 'remainder' 

97 POWER = 'power' 

98 

99 

100_UNARY_FUNCTIONS: Dict[UnaryOperator, Callable[[float], float]] = { 

101 UnaryOperator.NEGATIVE: operator.neg, 

102 UnaryOperator.POSITIVE: operator.pos, 

103 UnaryOperator.NOT: lambda x: float(not x), 

104} 

105 

106# NOTE: but AND and OR, which evaluate their right operand only if needed 

107_BINARY_FUNCTIONS: Dict[BinaryOperator, Callable[[float, float], float]] = { 

108 BinaryOperator.EQUAL: lambda a, b: float(a == b), 

109 BinaryOperator.NOT_EQUAL: lambda a, b: float(a != b), 

110 BinaryOperator.LESS: lambda a, b: float(a < b), 

111 BinaryOperator.LESS_EQUAL: lambda a, b: float(a <= b), 

112 BinaryOperator.GREATER: lambda a, b: float(a > b), 

113 BinaryOperator.GREATER_EQUAL: lambda a, b: float(a >= b), 

114 BinaryOperator.ADD: operator.add, 

115 BinaryOperator.SUBTRACT: operator.sub, 

116 BinaryOperator.MULTIPLY: operator.mul, 

117 BinaryOperator.DIVIDE: operator.truediv, 

118 BinaryOperator.REMAINDER: math.fmod, 

119 BinaryOperator.POWER: math.pow, 

120} 

121 

122 

123@dataclass(frozen=True) 

124class ExpressionFunction: 

125 """ 

126 A function that expressions call, e.g. sqrt 

127 """ 

128 name: str 

129 arity: int 

130 implementation: Callable[..., float] 

131 

132 def __call__(self, *arguments: float) -> float: 

133 """ 

134 :raises ExpressionError: if the function has no finite value for the arguments, e.g. sqrt(-1) 

135 """ 

136 return _checked(self.name, self.implementation, *arguments) 

137 

138 

139class Node(ABC): 

140 """ 

141 An operation of an expression, or a number, or a parameter 

142 """ 

143 

144 @abstractmethod 

145 def evaluate(self, scope: Scope) -> float: 

146 """ 

147 :raises ExpressionError: if the expression has no value in the scope 

148 """ 

149 ... 

150 

151 

152@dataclass(frozen=True) 

153class Number(Node): 

154 value: float 

155 

156 def evaluate(self, scope: Scope) -> float: 

157 return self.value 

158 

159 

160@dataclass(frozen=True) 

161class Parameter(Node): 

162 name: str # as the scope names it (e.g. lowercase for a flavour that ignores case) 

163 

164 def evaluate(self, scope: Scope) -> float: 

165 return scope.value(self.name) 

166 

167 

168@dataclass(frozen=True) 

169class UnaryOperation(Node): 

170 operator: UnaryOperator 

171 operand: Node 

172 

173 def evaluate(self, scope: Scope) -> float: 

174 return _UNARY_FUNCTIONS[self.operator](self.operand.evaluate(scope)) 

175 

176 

177@dataclass(frozen=True) 

178class BinaryOperation(Node): 

179 operator: BinaryOperator 

180 left: Node 

181 right: Node 

182 

183 def evaluate(self, scope: Scope) -> float: 

184 left = self.left.evaluate(scope) 

185 match self.operator: 

186 case BinaryOperator.AND: 

187 return float(bool(left) and bool(self.right.evaluate(scope))) 

188 case BinaryOperator.OR: 

189 return float(bool(left) or bool(self.right.evaluate(scope))) 

190 return _checked(self.operator.value, _BINARY_FUNCTIONS[self.operator], left, self.right.evaluate(scope)) 

191 

192 

193@dataclass(frozen=True) 

194class Conditional(Node): 

195 """ 

196 condition ? if_true : if_false, which evaluates only the operand it takes 

197 """ 

198 condition: Node 

199 if_true: Node 

200 if_false: Node 

201 

202 def evaluate(self, scope: Scope) -> float: 

203 return (self.if_true if self.condition.evaluate(scope) else self.if_false).evaluate(scope) 

204 

205 

206@dataclass(frozen=True) 

207class FunctionCall(Node): 

208 function: ExpressionFunction 

209 arguments: Tuple[Node, ...] 

210 

211 def evaluate(self, scope: Scope) -> float: 

212 return self.function(*(a.evaluate(scope) for a in self.arguments)) 

213 

214 

215@dataclass(frozen=True) 

216class Expression: 

217 """ 

218 An expression as a netlist writes it (text) and as tree of its operations (root) 

219 """ 

220 text: str 

221 root: Node 

222 

223 def evaluate(self, scope: Scope) -> float: 

224 """ 

225 :raises ExpressionError: if the expression has no value in the scope 

226 """ 

227 return self.root.evaluate(scope) 

228 

229 def __str__(self) -> str: 

230 return self.text 

231 

232 

233NumberOrExpression = Union[float, Expression] 

234"""A value of a netlist, e.g. of a parameter"""