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
« 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"""
26Expressions of netlist parameters (e.g. w*nf), as trees of their operations, and their evaluation.
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.
31NOTE: imports neither KLayout nor generated protobuf code, so that format-only paths can use it
32"""
34from __future__ import annotations
35from abc import ABC, abstractmethod
36from dataclasses import dataclass
37from enum import Enum
38import math
39import operator
40from typing import *
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
50class Scope(Protocol):
51 """
52 The values of the parameters an expression refers to
53 """
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 ...
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)
77class UnaryOperator(Enum):
78 NEGATIVE = 'negative'
79 POSITIVE = 'positive'
80 NOT = 'not'
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'
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}
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}
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]
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)
139class Node(ABC):
140 """
141 An operation of an expression, or a number, or a parameter
142 """
144 @abstractmethod
145 def evaluate(self, scope: Scope) -> float:
146 """
147 :raises ExpressionError: if the expression has no value in the scope
148 """
149 ...
152@dataclass(frozen=True)
153class Number(Node):
154 value: float
156 def evaluate(self, scope: Scope) -> float:
157 return self.value
160@dataclass(frozen=True)
161class Parameter(Node):
162 name: str # as the scope names it (e.g. lowercase for a flavour that ignores case)
164 def evaluate(self, scope: Scope) -> float:
165 return scope.value(self.name)
168@dataclass(frozen=True)
169class UnaryOperation(Node):
170 operator: UnaryOperator
171 operand: Node
173 def evaluate(self, scope: Scope) -> float:
174 return _UNARY_FUNCTIONS[self.operator](self.operand.evaluate(scope))
177@dataclass(frozen=True)
178class BinaryOperation(Node):
179 operator: BinaryOperator
180 left: Node
181 right: Node
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))
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
202 def evaluate(self, scope: Scope) -> float:
203 return (self.if_true if self.condition.evaluate(scope) else self.if_false).evaluate(scope)
206@dataclass(frozen=True)
207class FunctionCall(Node):
208 function: ExpressionFunction
209 arguments: Tuple[Node, ...]
211 def evaluate(self, scope: Scope) -> float:
212 return self.function(*(a.evaluate(scope) for a in self.arguments))
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
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)
229 def __str__(self) -> str:
230 return self.text
233NumberOrExpression = Union[float, Expression]
234"""A value of a netlist, e.g. of a parameter"""