diff --git a/PyQt-Plotter-Dialog/parser/expression.py b/PyQt-Plotter-Dialog/parser/expression.py index 8563d76..ebb59d0 100644 --- a/PyQt-Plotter-Dialog/parser/expression.py +++ b/PyQt-Plotter-Dialog/parser/expression.py @@ -1,3 +1,7 @@ +""" +Contains classes for expression tree representation and evaluation +""" + import abc from collections.abc import Callable, Mapping @@ -5,6 +9,13 @@ from .types import FunctionType, OperatorType, ValueType class Expression(abc.ABC): + """ + Abstract base class for a single parsed expression as a tree data + structure. It also defines its public function for triggering + evaluation. Each child class sets `_evaluator` property to a + function that accepts variables values and produces numpy array. + """ + _evaluator: Callable[[Mapping[str, ValueType]], ValueType] def evaluate(self, variables: Mapping[str, ValueType]): @@ -12,6 +23,12 @@ class Expression(abc.ABC): class ValueExpression(Expression): + """ + This expression accepts variable name, numpy array or scalar number + and evaluates to either constant or variable value, corresponding + to its name. + """ + def __init__(self, a: str | ValueType): self.__debug_a = a @@ -25,6 +42,12 @@ class ValueExpression(Expression): class UnaryExpression(Expression): + """ + This expression accepts function with one argument and `Expression` + (value, unary or binary) and that function on expression, passed into it. + It is applied for named functions like `exp(a)` + """ + def __init__(self, function: FunctionType, a: Expression): self.__debug_f = function.__name__ self.__debug_a = repr(a) @@ -36,6 +59,12 @@ class UnaryExpression(Expression): class BinaryExpression(Expression): + """ + This expression is similar to `UnaryExpression`, but accepts function + with two arguments and two expressions. + It is applied for math operators like `a - b` + """ + def __init__( self, function: OperatorType, diff --git a/PyQt-Plotter-Dialog/parser/operation.py b/PyQt-Plotter-Dialog/parser/operation.py index c2a1b3b..ecdeb01 100644 --- a/PyQt-Plotter-Dialog/parser/operation.py +++ b/PyQt-Plotter-Dialog/parser/operation.py @@ -5,6 +5,9 @@ import numpy as np from .types import FunctionType, OperatorType, ValueType +# Additional functions that are not defined in numpy + + def acot(x: ValueType): return np.arctan(1 / x) @@ -13,6 +16,9 @@ def cot(x: ValueType): return 1 / np.tan(x) +# Function and operation names to evaluators mapping + + functions: dict[str, FunctionType] = { "abs": np.abs, "acos": np.arccos, @@ -69,21 +75,39 @@ priorities: dict[str, int] = { @dataclass class Operation: + """ + Base class for math operation token (function, brace, operator). + It stores the way it is evaluated, evaluation priority and number + of arguments it supports. + """ + evaluator: (FunctionType | OperatorType | str) priority: int size: int class FunctionOperation(Operation): + """ + `Operator` class factory that represents function + """ + def __init__(self, name: str): super().__init__(functions[name], priorities["f"], 1) class BraceOperation(Operation): + """ + `Operator` class factory that represents brace + """ + def __init__(self, name: str): super().__init__(name, priorities[name], 0) class OperatorOperation(Operation): + """ + `Operator` class factory that represents binary operator + """ + def __init__(self, name: str): super().__init__(operators[name], priorities[name], 2) diff --git a/PyQt-Plotter-Dialog/parser/parser.py b/PyQt-Plotter-Dialog/parser/parser.py index 4619e53..c0be8d0 100644 --- a/PyQt-Plotter-Dialog/parser/parser.py +++ b/PyQt-Plotter-Dialog/parser/parser.py @@ -12,17 +12,32 @@ from .constants import CONSTANTS class Parser: + """ + Class that accepts math expression in its constructor, + parses it and provides `evaluate` method to substitue + variables values to it + """ + def __init__(self, input_expr: str): self.input_expr = input_expr self.variables_names: set[str] = set() - self.tokenize() - self.parse() + self._tokenize() + self._parse() - def tokenize(self): + def _tokenize(self): + """ + Uses `Tokenizer` class for math expression splitting + """ self.tokens = Tokenizer(self.input_expr) - def parse(self): + def _parse(self): + """ + Generates an evaluation tree from tokens by utilizing two + stacks - one for values and one for operations. Values and + operations are placed into corresponding stacks, and when + possible, assempled into tree node. + """ self.val_stack: list[Expression] = [] self.op_stack: list[Operation] = [] @@ -43,7 +58,7 @@ class Parser: while len(self.op_stack) > 0 and not ( self.op_stack[-1].size == 0 and self.op_stack[-1].priority == 0 ): # until next in stack is lbrace - self.do_one() + self._do_one() self.op_stack.pop() # pop lbrace elif t_type == Token.Operator: @@ -52,22 +67,22 @@ class Parser: while ( len(self.op_stack) > 0 and self.op_stack[-1].priority > t_priority ): - self.do_one() + self._do_one() self.op_stack.append(OperatorOperation(t_val)) while len(self.op_stack) > 0: - self.do_one() + self._do_one() self._evaluator = self.val_stack[0].evaluate self.__debug_expr = repr(self.val_stack) - def evaluate(self, variables: dict[str, ValueType]): - variables |= CONSTANTS - return self._evaluator(variables) - - def do_one(self): + def _do_one(self): + """ + Assembles one operation into `Expression` tree node that is stored + on value stack. + """ op = self.op_stack.pop() if op.size == 1: @@ -77,5 +92,13 @@ class Parser: b, a = self.val_stack.pop(), self.val_stack.pop() # inversed pop order self.val_stack.append(BinaryExpression(op.evaluator, a, b)) + def evaluate(self, variables: dict[str, ValueType]): + """ + Evaluates supplied to constructor expression with provided + variables values + """ + variables |= CONSTANTS + return self._evaluator(variables) + def __repr__(self): return self.__debug_expr diff --git a/PyQt-Plotter-Dialog/parser/tokenizer.py b/PyQt-Plotter-Dialog/parser/tokenizer.py index 6250ce8..4395e3b 100644 --- a/PyQt-Plotter-Dialog/parser/tokenizer.py +++ b/PyQt-Plotter-Dialog/parser/tokenizer.py @@ -8,6 +8,12 @@ from .types import Token, TokenType @dataclass class Tokenizer: + """ + Implements an iterator that yields `Token`s one by one. + It's grammatics is context-sensitive, but respects only a + single previous token. + """ + expression: str def __iter__(self) -> Generator[TokenType, None, None]: