Compare commits
4 Commits
94b2d0eddc
...
9ea4a29d2d
Author | SHA1 | Date | |
---|---|---|---|
9ea4a29d2d | |||
c1f573298e | |||
59bb5d901f | |||
02f17d2e51 |
@ -1,22 +1,60 @@
|
||||
# PyQT graph plotter
|
||||
|
||||
## Package interface
|
||||
## Интерфейс пакетов
|
||||
|
||||
- `graph_widget`
|
||||
|
||||
<Артёмка>
|
||||
|
||||
- `parser`
|
||||
|
||||
[parser/README.md](./parser/README.md)
|
||||
|
||||
- `plotter_dialog`
|
||||
|
||||
```python
|
||||
from plotter_dialog import PlotterDialog
|
||||
from plotter_dialog import PlotterDialog, FUNCTION_NAMES
|
||||
|
||||
PlotterDialog(
|
||||
variable_full_names: dict[str, str] # Variable button and tooltip captions
|
||||
function_full_names: dict[str, str] # Same for function
|
||||
variable_values: dict[str, numpy.ndarray] # Values to be substituted for variables
|
||||
variable_values: dict[str, np.ndarray] = {} # Значения для подстановки в переменные
|
||||
variable_full_names: dict[str, str] = {} # Надписи для кнопок переменных и подсказок для них
|
||||
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
|
||||
python -m venv .venv
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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]:
|
||||
|
@ -12,38 +12,9 @@ def main():
|
||||
|
||||
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(
|
||||
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_full_names={key: key.upper() for key in variables},
|
||||
)
|
||||
dlg.show()
|
||||
|
||||
|
27
PyQt-Plotter-Dialog/plotter_dialog/constants.py
Normal file
27
PyQt-Plotter-Dialog/plotter_dialog/constants.py
Normal 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": "Максимум",
|
||||
}
|
@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (
|
||||
import numpy as np
|
||||
|
||||
from .button_group import ButtonGroup
|
||||
from .constants import FUNCTION_NAMES
|
||||
from .graph_requester import GraphRequester
|
||||
|
||||
from parser import Parser
|
||||
@ -26,9 +27,9 @@ class PlotterDialog(QDialog):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
variable_full_names: dict[str, str],
|
||||
function_full_names: dict[str, str],
|
||||
variable_values: dict[str, np.ndarray],
|
||||
variable_values: dict[str, np.ndarray] = {},
|
||||
variable_full_names: dict[str, str] = {},
|
||||
function_full_names: dict[str, str] = FUNCTION_NAMES,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
@ -44,7 +45,6 @@ class PlotterDialog(QDialog):
|
||||
|
||||
scrollWidget = QWidget()
|
||||
|
||||
# self.scroll.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
|
||||
|
||||
self.inputs_layout = QVBoxLayout() # лаяут первой трети
|
||||
|
Loading…
x
Reference in New Issue
Block a user