Compare commits

..

No commits in common. "9ea4a29d2d1992b3f63d3c427e93b9ba8a34048b" and "94b2d0eddce8608961d61c19eee4df006d40812d" have entirely different histories.

8 changed files with 54 additions and 172 deletions

View File

@ -1,60 +1,22 @@
# 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, FUNCTION_NAMES from plotter_dialog import PlotterDialog
PlotterDialog( PlotterDialog(
variable_values: dict[str, np.ndarray] = {} # Значения для подстановки в переменные variable_full_names: dict[str, str] # Variable button and tooltip captions
variable_full_names: dict[str, str] = {} # Надписи для кнопок переменных и подсказок для них function_full_names: dict[str, str] # Same for function
function_full_names: dict[str, str] = FUNCTION_NAMES # То же самое для функций variable_values: dict[str, numpy.ndarray] # Values to be substituted for variables
) )
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` и `variable_values` должны иметь одни и те же ключи. `variable_full_names` and `variable_values` must have same keys.
## Инструкции по запуску демо-версии ## Demo running instructions
Выполнить в корневой папке: Run in project root directory:
```bash ```bash
python -m venv .venv python -m venv .venv

View File

@ -1,7 +1,3 @@
"""
Contains classes for expression tree representation and evaluation
"""
import abc import abc
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
@ -9,13 +5,6 @@ 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]):
@ -23,12 +12,6 @@ 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
@ -42,12 +25,6 @@ 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)
@ -59,12 +36,6 @@ 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,9 +5,6 @@ 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)
@ -16,9 +13,6 @@ 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,
@ -75,39 +69,21 @@ 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,32 +12,17 @@ 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] = []
@ -58,7 +43,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:
@ -67,22 +52,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 _do_one(self): def evaluate(self, variables: dict[str, ValueType]):
""" variables |= CONSTANTS
Assembles one operation into `Expression` tree node that is stored return self._evaluator(variables)
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:
@ -92,13 +77,5 @@ 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,12 +8,6 @@ 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,9 +12,38 @@ 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_values={key: np.sort(np.random.random(10)) * 10 for key in variables},
variable_full_names={key: key.upper() for key in variables}, 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},
) )
dlg.show() dlg.show()

View File

@ -1,27 +0,0 @@
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,7 +14,6 @@ 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
@ -27,9 +26,9 @@ class PlotterDialog(QDialog):
def __init__( def __init__(
self, self,
variable_values: dict[str, np.ndarray] = {}, variable_full_names: dict[str, str],
variable_full_names: dict[str, str] = {}, function_full_names: dict[str, str],
function_full_names: dict[str, str] = FUNCTION_NAMES, variable_values: dict[str, np.ndarray],
): ):
super().__init__() super().__init__()
@ -45,6 +44,7 @@ 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() # лаяут первой трети