Compare commits
4 Commits
94b2d0eddc
...
9ea4a29d2d
Author | SHA1 | Date | |
---|---|---|---|
9ea4a29d2d | |||
c1f573298e | |||
59bb5d901f | |||
02f17d2e51 |
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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]:
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
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
|
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() # лаяут первой трети
|
||||||
|
Loading…
x
Reference in New Issue
Block a user