Compare commits

...

4 Commits

8 changed files with 172 additions and 54 deletions

View File

@ -1,22 +1,60 @@
# PyQT graph plotter # PyQT graph plotter
## Package interface ## Интерфейс пакетов
- `graph_widget`
<Артёмка>
- `parser`
[parser/README.md](./parser/README.md)
- `plotter_dialog`
```python ```python
from plotter_dialog import PlotterDialog from plotter_dialog import PlotterDialog, FUNCTION_NAMES
PlotterDialog( PlotterDialog(
variable_full_names: dict[str, str] # Variable button and tooltip captions variable_values: dict[str, np.ndarray] = {} # Значения для подстановки в переменные
function_full_names: dict[str, str] # Same for function variable_full_names: dict[str, str] = {} # Надписи для кнопок переменных и подсказок для них
variable_values: dict[str, numpy.ndarray] # Values to be substituted for variables function_full_names: dict[str, str] = FUNCTION_NAMES # То же самое для функций
) )
FUNCTION_NAMES = {
"abs": "Модуль",
"acos": "Арккосинус",
"acosh": "Гиперболический арккосинус",
"acot": "Арккотангенс",
"asin": "Арксинус",
"asinh": "Гиперболический арксинус",
"atan": "Арктангенс",
"avg": "Среднее",
"cos": "Косинус",
"cosh": "Гиперболический косинус",
"cot": "Котангенс",
"exp": "Экспонента (e^x)",
"lg": "Десятичный логарифм",
"ln": "Натуральный логарифм",
"log2": "Двоичный логарифм",
"max": "Максимум",
"min": "Минимум",
"prod": "Произведение",
"sgn": "Знак",
"sin": "Синус",
"sinh": "Гиперболический синус",
"sqrt": "Квадратный корень",
"sum": "Сумма",
"tanh": "Гиперболический тангенс",
"tan": "Тангенс",
}
``` ```
`variable_full_names` and `variable_values` must have same keys. `variable_full_names` и `variable_values` должны иметь одни и те же ключи.
## Demo running instructions ## Инструкции по запуску демо-версии
Run in project root directory: Выполнить в корневой папке:
```bash ```bash
python -m venv .venv python -m venv .venv

View File

@ -1,3 +1,7 @@
"""
Contains classes for expression tree representation and evaluation
"""
import abc import abc
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
@ -5,6 +9,13 @@ from .types import FunctionType, OperatorType, ValueType
class Expression(abc.ABC): 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] _evaluator: Callable[[Mapping[str, ValueType]], ValueType]
def evaluate(self, variables: Mapping[str, ValueType]): def evaluate(self, variables: Mapping[str, ValueType]):
@ -12,6 +23,12 @@ class Expression(abc.ABC):
class ValueExpression(Expression): 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): def __init__(self, a: str | ValueType):
self.__debug_a = a self.__debug_a = a
@ -25,6 +42,12 @@ class ValueExpression(Expression):
class UnaryExpression(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): def __init__(self, function: FunctionType, a: Expression):
self.__debug_f = function.__name__ self.__debug_f = function.__name__
self.__debug_a = repr(a) self.__debug_a = repr(a)
@ -36,6 +59,12 @@ class UnaryExpression(Expression):
class BinaryExpression(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__( def __init__(
self, self,
function: OperatorType, function: OperatorType,

View File

@ -5,6 +5,9 @@ import numpy as np
from .types import FunctionType, OperatorType, ValueType from .types import FunctionType, OperatorType, ValueType
# Additional functions that are not defined in numpy
def acot(x: ValueType): def acot(x: ValueType):
return np.arctan(1 / x) return np.arctan(1 / x)
@ -13,6 +16,9 @@ def cot(x: ValueType):
return 1 / np.tan(x) return 1 / np.tan(x)
# Function and operation names to evaluators mapping
functions: dict[str, FunctionType] = { functions: dict[str, FunctionType] = {
"abs": np.abs, "abs": np.abs,
"acos": np.arccos, "acos": np.arccos,
@ -69,21 +75,39 @@ priorities: dict[str, int] = {
@dataclass @dataclass
class Operation: 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) evaluator: (FunctionType | OperatorType | str)
priority: int priority: int
size: int size: int
class FunctionOperation(Operation): class FunctionOperation(Operation):
"""
`Operator` class factory that represents function
"""
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(functions[name], priorities["f"], 1) super().__init__(functions[name], priorities["f"], 1)
class BraceOperation(Operation): class BraceOperation(Operation):
"""
`Operator` class factory that represents brace
"""
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(name, priorities[name], 0) super().__init__(name, priorities[name], 0)
class OperatorOperation(Operation): class OperatorOperation(Operation):
"""
`Operator` class factory that represents binary operator
"""
def __init__(self, name: str): def __init__(self, name: str):
super().__init__(operators[name], priorities[name], 2) super().__init__(operators[name], priorities[name], 2)

View File

@ -12,17 +12,32 @@ from .constants import CONSTANTS
class Parser: 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): def __init__(self, input_expr: str):
self.input_expr = input_expr self.input_expr = input_expr
self.variables_names: set[str] = set() self.variables_names: set[str] = set()
self.tokenize() self._tokenize()
self.parse() self._parse()
def tokenize(self): def _tokenize(self):
"""
Uses `Tokenizer` class for math expression splitting
"""
self.tokens = Tokenizer(self.input_expr) 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.val_stack: list[Expression] = []
self.op_stack: list[Operation] = [] self.op_stack: list[Operation] = []
@ -43,7 +58,7 @@ class Parser:
while len(self.op_stack) > 0 and not ( while len(self.op_stack) > 0 and not (
self.op_stack[-1].size == 0 and self.op_stack[-1].priority == 0 self.op_stack[-1].size == 0 and self.op_stack[-1].priority == 0
): # until next in stack is lbrace ): # until next in stack is lbrace
self.do_one() self._do_one()
self.op_stack.pop() # pop lbrace self.op_stack.pop() # pop lbrace
elif t_type == Token.Operator: elif t_type == Token.Operator:
@ -52,22 +67,22 @@ class Parser:
while ( while (
len(self.op_stack) > 0 and self.op_stack[-1].priority > t_priority 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)) self.op_stack.append(OperatorOperation(t_val))
while len(self.op_stack) > 0: while len(self.op_stack) > 0:
self.do_one() self._do_one()
self._evaluator = self.val_stack[0].evaluate self._evaluator = self.val_stack[0].evaluate
self.__debug_expr = repr(self.val_stack) self.__debug_expr = repr(self.val_stack)
def evaluate(self, variables: dict[str, ValueType]): def _do_one(self):
variables |= CONSTANTS """
return self._evaluator(variables) Assembles one operation into `Expression` tree node that is stored
on value stack.
def do_one(self): """
op = self.op_stack.pop() op = self.op_stack.pop()
if op.size == 1: if op.size == 1:
@ -77,5 +92,13 @@ class Parser:
b, a = self.val_stack.pop(), self.val_stack.pop() # inversed pop order b, a = self.val_stack.pop(), self.val_stack.pop() # inversed pop order
self.val_stack.append(BinaryExpression(op.evaluator, a, b)) 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): def __repr__(self):
return self.__debug_expr return self.__debug_expr

View File

@ -8,6 +8,12 @@ from .types import Token, TokenType
@dataclass @dataclass
class Tokenizer: 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 expression: str
def __iter__(self) -> Generator[TokenType, None, None]: def __iter__(self) -> Generator[TokenType, None, None]:

View File

@ -12,38 +12,9 @@ def main():
variables = [chr(ord("a") + i) for i in range(10)] variables = [chr(ord("a") + i) for i in range(10)]
functions = [
"abs",
"acos",
"acosh",
"acot",
"asin",
"asinh",
"atan",
"avg",
"cos",
"cosh",
"cot",
"exp",
"lg",
"ln",
"log2",
"max",
"min",
"prod",
"sgn",
"sin",
"sinh",
"sqrt",
"sum",
"tanh",
"tan",
]
dlg = PlotterDialog( dlg = PlotterDialog(
variable_full_names={key: key.upper() for key in variables},
function_full_names={key: key.upper() for key in functions},
variable_values={key: np.sort(np.random.random(10)) * 10 for key in variables}, variable_values={key: np.sort(np.random.random(10)) * 10 for key in variables},
variable_full_names={key: key.upper() for key in variables},
) )
dlg.show() dlg.show()

View File

@ -0,0 +1,27 @@
FUNCTION_NAMES = {
"sin": "Синус",
"cos": "Косинус",
"tan": "Тангенс",
"cot": "Котангенс",
"abs": "Модуль",
"exp": "Экспонента (e^x)",
"sqrt": "Квадратный корень",
"ln": "Натуральный логарифм",
"lg": "Десятичный логарифм",
"log2": "Двоичный логарифм",
"sgn": "Знак",
"asin": "Арксинус",
"acos": "Арккосинус",
"atan": "Арктангенс",
"acot": "Арккотангенс",
"sinh": "Гиперболический синус",
"cosh": "Гиперболический косинус",
"tanh": "Гиперболический тангенс",
"acosh": "Гиперболический арккосинус",
"asinh": "Гиперболический арксинус",
"sum": "Сумма",
"prod": "Произведение",
"min": "Минимум",
"avg": "Среднее",
"max": "Максимум",
}

View File

@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (
import numpy as np import numpy as np
from .button_group import ButtonGroup from .button_group import ButtonGroup
from .constants import FUNCTION_NAMES
from .graph_requester import GraphRequester from .graph_requester import GraphRequester
from parser import Parser from parser import Parser
@ -26,9 +27,9 @@ class PlotterDialog(QDialog):
def __init__( def __init__(
self, self,
variable_full_names: dict[str, str], variable_values: dict[str, np.ndarray] = {},
function_full_names: dict[str, str], variable_full_names: dict[str, str] = {},
variable_values: dict[str, np.ndarray], function_full_names: dict[str, str] = FUNCTION_NAMES,
): ):
super().__init__() super().__init__()
@ -44,7 +45,6 @@ class PlotterDialog(QDialog):
scrollWidget = QWidget() scrollWidget = QWidget()
# self.scroll.setFrameShape(QFrame.NoFrame)
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.inputs_layout = QVBoxLayout() # лаяут первой трети self.inputs_layout = QVBoxLayout() # лаяут первой трети