chore: во все модули добавлены docstring
This commit is contained in:
parent
ba0f8818bb
commit
78d34343d5
409
src/base/base.py
409
src/base/base.py
@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
import os
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union, List, Tuple, Dict, Any
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from cachetools import LRUCache
|
from cachetools import LRUCache
|
||||||
@ -11,53 +11,67 @@ from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
|
|||||||
from OptAlgorithm import OptAlgorithm
|
from OptAlgorithm import OptAlgorithm
|
||||||
from utils.qt_settings import dark_style
|
from utils.qt_settings import dark_style
|
||||||
|
|
||||||
|
|
||||||
|
# ========= Дата-классы для хранения данных =========
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class KukaTXT:
|
class KukaTXT:
|
||||||
|
"""
|
||||||
|
Класс для представления текстовых данных Кука.
|
||||||
|
"""
|
||||||
time: float = 0
|
time: float = 0
|
||||||
endtime: float = 0
|
endtime: float = 0
|
||||||
#module: str
|
|
||||||
func: str = ""
|
func: str = ""
|
||||||
type_: str = ""
|
type_: str = ""
|
||||||
signal: str = ""
|
signal: str = ""
|
||||||
|
# Дополнительные поля (закомментированы) можно добавить при необходимости
|
||||||
|
# module: str
|
||||||
# line: int = 0
|
# line: int = 0
|
||||||
# point_name: str = ""
|
# point_name: str = ""
|
||||||
#point_coord: dict = field(default_factory=lambda: {})
|
# ...
|
||||||
#blending: str = ""
|
|
||||||
#blending_param: float = 0
|
|
||||||
#velocities: dict = field(default_factory=lambda: {})
|
|
||||||
#accelerarions: dict = field(default_factory=lambda: {})
|
|
||||||
#base: dict = field(default_factory=lambda: {})
|
|
||||||
#tool: dict = field(default_factory=lambda: {})
|
|
||||||
#ipo_mode: str = ""
|
|
||||||
#motion_mode: str = ""
|
|
||||||
#load: dict = field(default_factory=lambda: {})
|
|
||||||
#load_a3: dict = field(default_factory=lambda: {})
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class KukaDataHead:
|
class KukaDataHead:
|
||||||
|
"""
|
||||||
|
Класс, описывающий заголовок данных Кука.
|
||||||
|
"""
|
||||||
rob_ID: int = 0
|
rob_ID: int = 0
|
||||||
filename: str = ""
|
filename: str = ""
|
||||||
channels: dict = field(default_factory= lambda: {})
|
channels: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlotItems:
|
class PlotItems:
|
||||||
regions: dict = field(default_factory= lambda: {})
|
"""
|
||||||
curves: dict = field(default_factory= lambda: {})
|
Класс для хранения элементов графика: регионы, кривые и QT-виджеты.
|
||||||
qt_items: dict = field(default_factory= lambda: {})
|
"""
|
||||||
|
regions: dict = field(default_factory=dict)
|
||||||
|
curves: dict = field(default_factory=dict)
|
||||||
|
qt_items: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PointPassport:
|
class PointPassport:
|
||||||
timeframe: list = field(default_factory= lambda: [])
|
"""
|
||||||
events: dict = field(default_factory= lambda: {})
|
Паспорт точки, содержащий временной интервал, события,
|
||||||
ideal_data: dict = field(default_factory= lambda: {})
|
идеальные данные и полезную информацию для этой точки.
|
||||||
useful_data: dict = field(default_factory= lambda: {})
|
"""
|
||||||
|
timeframe: List[Any] = field(default_factory=list)
|
||||||
|
events: Dict = field(default_factory=dict)
|
||||||
|
ideal_data: Dict = field(default_factory=dict)
|
||||||
|
useful_data: Dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UsefulGraphData:
|
class UsefulGraphData:
|
||||||
|
"""
|
||||||
|
Полезные данные для графика.
|
||||||
|
|
||||||
|
:param client_time: Время, рассчитанное для клиента.
|
||||||
|
:param range_ME: Диапазон для ME.
|
||||||
|
:param k_hardness: Коэффициент твердости.
|
||||||
|
"""
|
||||||
client_time: float = 0
|
client_time: float = 0
|
||||||
range_ME: float = 0
|
range_ME: float = 0
|
||||||
k_hardness: float = 0
|
k_hardness: float = 0
|
||||||
@ -65,20 +79,37 @@ class UsefulGraphData:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GraphicPassport:
|
class GraphicPassport:
|
||||||
|
"""
|
||||||
|
Графический паспорт, объединяющий DataFrame с данными,
|
||||||
|
список паспортов точек и полезные данные для построения графика.
|
||||||
|
"""
|
||||||
dataframe: pd.DataFrame = pd.DataFrame({})
|
dataframe: pd.DataFrame = pd.DataFrame({})
|
||||||
points_pocket: list[PointPassport] = field(default_factory= lambda: [])
|
points_pocket: List[PointPassport] = field(default_factory=list)
|
||||||
useful_data: UsefulGraphData = UsefulGraphData()
|
useful_data: UsefulGraphData = UsefulGraphData()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Settings:
|
class Settings:
|
||||||
operator: dict = field(default_factory= lambda: {})
|
"""
|
||||||
system: dict = field(default_factory= lambda: {})
|
Настройки приложения, разделённые на категории:
|
||||||
filter: dict = field(default_factory= lambda: {})
|
- operator: настройки, связанные с оператором,
|
||||||
|
- system: системные настройки,
|
||||||
|
- filter: настройки фильтрации трейса клиента для определения событий.
|
||||||
|
"""
|
||||||
|
operator: Dict = field(default_factory=dict)
|
||||||
|
system: Dict = field(default_factory=dict)
|
||||||
|
filter: Dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
# ========= Базовые классы для взаимодействия компонентов =========
|
||||||
|
|
||||||
class BaseMediator:
|
class BaseMediator:
|
||||||
|
"""
|
||||||
|
Базовый класс медиатора для организации взаимодействия между компонентами приложения.
|
||||||
|
|
||||||
|
Медиатор связывает такие модули, как конвертер данных, формирователь паспортов,
|
||||||
|
виджет построения графиков, контроллер, файловый менеджер и процессор трассировки.
|
||||||
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
converter: BaseDataConverter,
|
converter: BaseDataConverter,
|
||||||
passport_former: BasePointPassportFormer,
|
passport_former: BasePointPassportFormer,
|
||||||
@ -100,32 +131,61 @@ class BaseMediator:
|
|||||||
self._trace_processor.mediator = self
|
self._trace_processor.mediator = self
|
||||||
|
|
||||||
def notify(self,
|
def notify(self,
|
||||||
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseRawTraceProcessor],
|
source: Union[
|
||||||
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget], pd.DataFrame]):
|
BaseDirectoryMonitor,
|
||||||
|
BaseDataConverter,
|
||||||
|
BasePointPassportFormer,
|
||||||
|
BasePlotWidget,
|
||||||
|
BaseRawTraceProcessor
|
||||||
|
],
|
||||||
|
data: Union[
|
||||||
|
List[str],
|
||||||
|
List[pd.DataFrame],
|
||||||
|
List[list],
|
||||||
|
List[QWidget],
|
||||||
|
pd.DataFrame
|
||||||
|
]) -> None:
|
||||||
|
"""
|
||||||
|
Получает уведомление от компонента-источника и распределяет данные по соответствующим модулям.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def prerender_TCW(self, source:Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
def prerender_TCW(self,
|
||||||
data:Union[str, list[str], list[pd.DataFrame, dict]]) -> None:
|
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
||||||
|
data: Union[str, List[str], List[pd.DataFrame, dict]]) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет операции для отображения предварительных данных клиента.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def render_TCW(self, source:Union[BaseController, BaseRawTraceProcessor],
|
def render_TCW(self,
|
||||||
data:Union[str, list[pd.DataFrame, dict]]) -> None:
|
source: Union[BaseController, BaseRawTraceProcessor],
|
||||||
|
data: Union[str, List[pd.DataFrame, dict]]) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет операции для отображения обработанных данных клиента и ТСК.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_mode(self, mode: int) -> None:
|
def set_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим работы.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
class BaseDirectoryMonitor:
|
|
||||||
|
|
||||||
|
class BaseDirectoryMonitor:
|
||||||
|
"""
|
||||||
|
Базовый класс для мониторинга директории.
|
||||||
|
|
||||||
|
Использует QTimer для периодической проверки содержимого директории.
|
||||||
|
"""
|
||||||
update_timer = QTimer()
|
update_timer = QTimer()
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self, file_manager: Optional[BaseFileManager] = None):
|
||||||
file_manager: Optional[BaseFileManager] = None):
|
self._directory_path: Optional[str] = None
|
||||||
super().__init__()
|
self._update_time: Optional[float] = None
|
||||||
self._directory_path = None
|
self.isActive: bool = False
|
||||||
self._update_time = None
|
self._files: List[str] = []
|
||||||
self.isActive = False
|
|
||||||
self._files = []
|
|
||||||
self._file_manager = file_manager
|
self._file_manager = file_manager
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -137,7 +197,7 @@ class BaseDirectoryMonitor:
|
|||||||
return self._update_time
|
return self._update_time
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def files(self) -> list[str]:
|
def files(self) -> List[str]:
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -148,23 +208,38 @@ class BaseDirectoryMonitor:
|
|||||||
def file_manager(self, file_manager: BaseFileManager) -> None:
|
def file_manager(self, file_manager: BaseFileManager) -> None:
|
||||||
self._file_manager = file_manager
|
self._file_manager = file_manager
|
||||||
|
|
||||||
def init_state(self):
|
def init_state(self) -> None:
|
||||||
|
"""
|
||||||
|
Инициализирует состояние мониторинга, считывая список файлов из директории.
|
||||||
|
"""
|
||||||
files = os.listdir(self._directory_path)
|
files = os.listdir(self._directory_path)
|
||||||
self._files = files
|
self._files = files
|
||||||
|
|
||||||
def start(self):
|
def start(self) -> None:
|
||||||
|
"""
|
||||||
|
Запускает мониторинг директории, устанавливая флаг активности и запуская таймер.
|
||||||
|
"""
|
||||||
self.isActive = True
|
self.isActive = True
|
||||||
self.update_timer.start(int(self._update_time))
|
self.update_timer.start(int(self._update_time))
|
||||||
|
|
||||||
def stop(self):
|
def stop(self) -> None:
|
||||||
|
"""
|
||||||
|
Останавливает мониторинг директории.
|
||||||
|
"""
|
||||||
self.isActive = False
|
self.isActive = False
|
||||||
self.update_timer.stop()
|
self.update_timer.stop()
|
||||||
|
|
||||||
def pause(self):
|
def pause(self) -> None:
|
||||||
|
"""
|
||||||
|
Приостанавливает мониторинг директории (останавливает таймер).
|
||||||
|
"""
|
||||||
self.update_timer.stop()
|
self.update_timer.stop()
|
||||||
|
|
||||||
|
|
||||||
class BaseDataConverter:
|
class BaseDataConverter:
|
||||||
|
"""
|
||||||
|
Базовый класс для конвертации данных.
|
||||||
|
"""
|
||||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
|
|
||||||
@ -176,20 +251,22 @@ class BaseDataConverter:
|
|||||||
def mediator(self, mediator: BaseMediator) -> None:
|
def mediator(self, mediator: BaseMediator) -> None:
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
|
|
||||||
def convert_data(self, files: list[str]) -> None:
|
def convert_data(self, files: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Конвертирует данные из списка файлов.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class BasePlotWidget:
|
class BasePlotWidget:
|
||||||
def __init__(self,
|
"""
|
||||||
mediator: Optional[BaseMediator] = None,
|
Базовый класс для виджета построения графиков.
|
||||||
controller: BaseController = None):
|
"""
|
||||||
super().__init__()
|
def __init__(self, mediator: Optional[BaseMediator] = None, controller: BaseController = None):
|
||||||
self._mediator: BaseMediator = mediator
|
self._mediator: BaseMediator = mediator
|
||||||
self._controller: BaseController = controller
|
self._controller: BaseController = controller
|
||||||
self._datalen: int = 0
|
self._datalen: int = 0
|
||||||
self._datastep: int = 0
|
self._datastep: int = 0
|
||||||
|
|
||||||
self._stage_colors = {
|
self._stage_colors = {
|
||||||
"Closing": [220, 20, 60, 100], # Crimson
|
"Closing": [220, 20, 60, 100], # Crimson
|
||||||
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
||||||
@ -200,14 +277,18 @@ class BasePlotWidget:
|
|||||||
self._plt_channels = None
|
self._plt_channels = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def set_style(object_: Union[QTabWidget, QWidget]) -> None:
|
def set_style(obj: Union[QTabWidget, QWidget]) -> None:
|
||||||
object_.setStyleSheet(
|
"""
|
||||||
|
Устанавливает стиль для переданного объекта.
|
||||||
|
"""
|
||||||
|
obj.setStyleSheet(
|
||||||
"""QLabel {
|
"""QLabel {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-family: "Segoe UI", sans-serif;
|
font-family: "Segoe UI", sans-serif;
|
||||||
}""")
|
}"""
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def controller(self) -> BaseController:
|
def controller(self) -> BaseController:
|
||||||
@ -221,46 +302,80 @@ class BasePlotWidget:
|
|||||||
def mediator(self, mediator: BaseMediator) -> None:
|
def mediator(self, mediator: BaseMediator) -> None:
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
|
|
||||||
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
|
def build(self, data: List[pd.DataFrame]) -> List[QWidget]:
|
||||||
|
"""
|
||||||
|
Строит графики на основе списка DataFrame.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def build_raw_trace(self, data: pd.DataFrame) -> None:
|
def build_raw_trace(self, data: pd.DataFrame) -> None:
|
||||||
|
"""
|
||||||
|
Строит график трейсов клиента с предварительным разбиением на этапы.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_mode(self, mode: int) -> None:
|
def set_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим отображения графиков.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
class BaseController(QObject):
|
|
||||||
|
|
||||||
def __init__(self,
|
class BaseController(QObject):
|
||||||
mediator: Optional[BaseMediator] = None,
|
"""
|
||||||
file_manager: Optional[BaseFileManager] = None):
|
Базовый класс контроллера, отвечающий за обновление интерфейса и взаимодействие с медиатором.
|
||||||
|
"""
|
||||||
|
def __init__(self, mediator: Optional[BaseMediator] = None, file_manager: Optional[BaseFileManager] = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
self._file_manager = file_manager
|
self._file_manager = file_manager
|
||||||
|
|
||||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
def send_widgets(self, widgets: List[QWidget]) -> None:
|
||||||
|
"""
|
||||||
|
Отправляет список виджетов для дальнейшей обработки.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_settings(self, settings: list[dict]) -> None:
|
def update_settings(self, settings: List[Dict]) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки приложения.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_working_mode(self, mode: int) -> None:
|
def set_working_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает рабочий режим приложения.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def open_file(self, filepath: str) -> None:
|
def open_file(self, filepath: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает файл по заданному пути.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def open_dir(self, dirpath: str) -> None:
|
def open_dir(self, dirpath: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает директорию.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_plots(self) -> None:
|
def update_plots(self) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет графики.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_status(self, msg: str) -> None:
|
def update_status(self, msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет статусное сообщение в интерфейсе.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_progress(self, progress: int) -> None:
|
def update_progress(self, progress: int) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет значение прогресс-бара.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -281,14 +396,14 @@ class BaseController(QObject):
|
|||||||
|
|
||||||
|
|
||||||
class BaseFileManager:
|
class BaseFileManager:
|
||||||
|
"""
|
||||||
def __init__(self,
|
Базовый класс файлового менеджера.
|
||||||
mediator: Optional[BaseMediator] = None,
|
"""
|
||||||
monitor: Optional[BaseDirectoryMonitor] = None):
|
def __init__(self, mediator: Optional[BaseMediator] = None, monitor: Optional[BaseDirectoryMonitor] = None):
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
self._paths_library: set = set()
|
self._paths_library: set = set()
|
||||||
self._mode = 0
|
self._mode: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def paths_library(self) -> set:
|
def paths_library(self) -> set:
|
||||||
@ -311,97 +426,150 @@ class BaseFileManager:
|
|||||||
self._monitor = monitor
|
self._monitor = monitor
|
||||||
|
|
||||||
def replot_all(self) -> None:
|
def replot_all(self) -> None:
|
||||||
|
"""
|
||||||
|
Переотрисовывает все данные, используя текущую библиотеку путей.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def open_custom_file(self, path: str) -> None:
|
def open_custom_file(self, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает пользовательский файл по заданному пути.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def open_raw_traces_dir(self, path: str) -> None:
|
def open_raw_traces_dir(self, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает директорию с сырыми трассировочными данными.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_mode(self, num: int) -> None:
|
def set_mode(self, num: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим работы файлового менеджера.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_monitor_settings(self, settings:list[dict]) -> None:
|
def update_monitor_settings(self, settings: List[Dict]) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки мониторинга директории.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def add_new_paths(self, paths:list[str]):
|
def add_new_paths(self, paths: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Добавляет новые пути в библиотеку, если их ранее не было.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class BaseIdealDataBuilder(OptAlgorithm):
|
class BaseIdealDataBuilder(OptAlgorithm):
|
||||||
|
"""
|
||||||
|
Базовый класс для построения идеальных данных, расширяющий функциональность OptAlgorithm.
|
||||||
|
|
||||||
|
При инициализации на основе настроек устанавливаются множитель времени и время сварки.
|
||||||
|
"""
|
||||||
def __init__(self, settings: Settings):
|
def __init__(self, settings: Settings):
|
||||||
self.mul = settings.system['time_capture']
|
self.mul = settings.system['time_capture']
|
||||||
self.welding_time = settings.operator['time_wielding']
|
self.welding_time = settings.operator['time_wielding']
|
||||||
super().__init__(settings.system, settings.operator)
|
super().__init__(settings.system, settings.operator)
|
||||||
|
|
||||||
def get_closingDF(self) -> pd.DataFrame:
|
def get_closingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа закрытия.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_compressionDF(self) -> pd.DataFrame:
|
def get_compressionDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа сжатия.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_openingDF(self) -> pd.DataFrame:
|
def get_openingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа открытия.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_tmovementDF(self) -> pd.DataFrame:
|
def get_tmovementDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа движения.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_weldingDF(self) -> pd.DataFrame:
|
def get_weldingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа сварки.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_oncomingDF(self) -> pd.DataFrame:
|
def get_oncomingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа движения (oncomming).
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_ideal_timings(self) -> list[float, float, float, float]:
|
def get_ideal_timings(self) -> Tuple[float, float, float, float]:
|
||||||
|
"""
|
||||||
|
Получает идеальные временные интервалы для этапов.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_cycle_time(self) -> float:
|
def get_cycle_time(self) -> float:
|
||||||
|
"""
|
||||||
|
Вычисляет общее время цикла как сумму идеальных временных интервалов.
|
||||||
|
"""
|
||||||
result = sum(self.get_ideal_timings())
|
result = sum(self.get_ideal_timings())
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class BaseMainWindow(QMainWindow):
|
class BaseMainWindow(QMainWindow):
|
||||||
|
"""
|
||||||
|
Базовое главное окно приложения.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.resize(200, 200)
|
self.resize(200, 200)
|
||||||
# Создаем центральный виджет и устанавливаем его
|
# Создаем центральный виджет и устанавливаем его
|
||||||
self._central_widget = QWidget()
|
self._central_widget = QWidget()
|
||||||
self.setCentralWidget(self._central_widget)
|
self.setCentralWidget(self._central_widget)
|
||||||
|
|
||||||
# Устанавливаем основной вертикальный макет для центрального виджета
|
# Устанавливаем основной вертикальный макет для центрального виджета
|
||||||
self._central_layout = QVBoxLayout()
|
self._central_layout = QVBoxLayout()
|
||||||
self._central_widget.setLayout(self._central_layout)
|
self._central_widget.setLayout(self._central_layout)
|
||||||
self.set_style(self)
|
self.set_style(self)
|
||||||
...
|
...
|
||||||
|
|
||||||
def set_style(self, object: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
def set_style(self, obj: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
||||||
object.setStyleSheet(dark_style)
|
"""
|
||||||
|
Устанавливает стиль для переданного объекта.
|
||||||
|
"""
|
||||||
|
obj.setStyleSheet(dark_style)
|
||||||
|
|
||||||
|
|
||||||
class BasePointPassportFormer:
|
class BasePointPassportFormer:
|
||||||
|
"""
|
||||||
|
Базовый класс для формирования паспортов точек.
|
||||||
|
|
||||||
def __init__(self,
|
Содержит базовые настройки, список этапов, кэш для идеальных данных и набор параметров для алгоритма.
|
||||||
mediator: Optional[BaseMediator] = None):
|
"""
|
||||||
|
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||||
self._mediator = mediator
|
self._mediator = mediator
|
||||||
self._clear_stage = "Welding"
|
self._clear_stage: str = "Welding"
|
||||||
self._settings = Settings
|
self._settings = Settings()
|
||||||
self._stages = [
|
self._stages: List[str] = [
|
||||||
"Closing",
|
"Closing",
|
||||||
"Squeeze",
|
"Squeeze",
|
||||||
"Welding",
|
"Welding",
|
||||||
"Relief",
|
"Relief",
|
||||||
"Oncomming"
|
"Oncomming"
|
||||||
]
|
]
|
||||||
self._tesla_stages = [
|
self._tesla_stages: List[str] = [
|
||||||
"Tesla squeeze",
|
"Tesla squeeze",
|
||||||
"Tesla closing",
|
"Tesla closing",
|
||||||
"Tesla welding",
|
"Tesla welding",
|
||||||
"Tesla oncomming_relief"
|
"Tesla oncomming_relief"
|
||||||
]
|
]
|
||||||
self._ideal_data_cashe = LRUCache(maxsize=1000)
|
self._ideal_data_cache = LRUCache(maxsize=1000)
|
||||||
self._OptAlgorithm_operator_params = [
|
self._OptAlgorithm_operator_params = [
|
||||||
"dist_open_start_1",
|
"dist_open_start_1",
|
||||||
"dist_open_start_2",
|
"dist_open_start_2",
|
||||||
@ -416,7 +584,8 @@ class BasePointPassportFormer:
|
|||||||
"object_thickness",
|
"object_thickness",
|
||||||
"force_target",
|
"force_target",
|
||||||
"force_capture",
|
"force_capture",
|
||||||
"time_wielding"]
|
"time_wielding"
|
||||||
|
]
|
||||||
self._OptAlgorithm_system_params = [
|
self._OptAlgorithm_system_params = [
|
||||||
"a_max_1",
|
"a_max_1",
|
||||||
"v_max_1",
|
"v_max_1",
|
||||||
@ -433,15 +602,25 @@ class BasePointPassportFormer:
|
|||||||
"position_start_1",
|
"position_start_1",
|
||||||
"position_start_2",
|
"position_start_2",
|
||||||
"k_prop",
|
"k_prop",
|
||||||
"time_capture"]
|
"time_capture"
|
||||||
|
]
|
||||||
|
|
||||||
def form_passports(self, data: list[pd.DataFrame]) -> None:
|
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||||
|
"""
|
||||||
|
Формирует паспорта для набора данных.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def form_customer_passport(self, data: list[pd.DataFrame, dict]) -> None:
|
def form_customer_passport(self, data: List[Union[pd.DataFrame, dict]]) -> None:
|
||||||
|
"""
|
||||||
|
Формирует паспорт заказчика на основе DataFrame и событий.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_settings(self, params: list) -> None:
|
def update_settings(self, params: List) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки для формирования паспортов.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -449,7 +628,7 @@ class BasePointPassportFormer:
|
|||||||
return self._opt_algorithm
|
return self._opt_algorithm
|
||||||
|
|
||||||
@opt_algorithm.setter
|
@opt_algorithm.setter
|
||||||
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder):
|
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder) -> None:
|
||||||
self._opt_algorithm = opt_algorithm
|
self._opt_algorithm = opt_algorithm
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -462,7 +641,9 @@ class BasePointPassportFormer:
|
|||||||
|
|
||||||
|
|
||||||
class BaseRawTraceProcessor:
|
class BaseRawTraceProcessor:
|
||||||
|
"""
|
||||||
|
Базовый класс для обработки сырых трассировочных данных.
|
||||||
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
dataparser: BaseKukaDataParser,
|
dataparser: BaseKukaDataParser,
|
||||||
textparser: BaseKukaTextParser,
|
textparser: BaseKukaTextParser,
|
||||||
@ -474,16 +655,25 @@ class BaseRawTraceProcessor:
|
|||||||
self._textparser = textparser
|
self._textparser = textparser
|
||||||
self._data_detector = data_detector
|
self._data_detector = data_detector
|
||||||
self._text_detector = text_detector
|
self._text_detector = text_detector
|
||||||
self._trace_df = None
|
self._trace_df: Optional[pd.DataFrame] = None
|
||||||
self._text_data = None
|
self._text_data: Optional[Any] = None
|
||||||
|
|
||||||
def prerender(self, data:list[str]) -> None:
|
def prerender(self, data: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет предварительный рендеринг данных.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def final_render(self) -> None:
|
def final_render(self) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет финальный рендеринг данных.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def update_settings(self, data: Settings) -> None:
|
def update_settings(self, data: Settings) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки обработки трассировочных данных.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -496,38 +686,57 @@ class BaseRawTraceProcessor:
|
|||||||
|
|
||||||
|
|
||||||
class BaseKukaDataParser:
|
class BaseKukaDataParser:
|
||||||
|
"""
|
||||||
|
Базовый класс для парсинга данных Кука.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._ch_name = None
|
self._ch_name: Optional[str] = None
|
||||||
|
|
||||||
def parse(self, head_path: str) -> pd.DataFrame:
|
def parse(self, head_path: str) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Парсит файл заголовка и возвращает DataFrame с данными.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class BaseKukaTextParser:
|
class BaseKukaTextParser:
|
||||||
|
"""
|
||||||
|
Базовый класс для парсинга текстовых данных Кука.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._in_msg = None
|
self._in_msg = None
|
||||||
self._datapacks = None
|
self._datapacks = None
|
||||||
|
|
||||||
def parse(self, path:str ) -> list[KukaTXT]:
|
def parse(self, path: str) -> List[KukaTXT]:
|
||||||
|
"""
|
||||||
|
Парсит текстовый файл и возвращает список объектов KukaTXT.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTraceStageDetector:
|
class BaseTraceStageDetector:
|
||||||
|
"""
|
||||||
def __init__(self, parent:BaseRawTraceProcessor = None):
|
Базовый класс для детекции стадий на основе данных трассировки.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
|
|
||||||
def detect_stages(self, df: pd.DataFrame) -> list:
|
def detect_stages(self, df: pd.DataFrame) -> List:
|
||||||
|
"""
|
||||||
|
Детектирует стадии процесса по данным DataFrame.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
class BaseTextStageDetector:
|
class BaseTextStageDetector:
|
||||||
|
"""
|
||||||
def __init__(self, parent:BaseRawTraceProcessor = None):
|
Базовый класс для детекции сварочных стадий на основе текстовых данных.
|
||||||
|
"""
|
||||||
|
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
|
|
||||||
def detect_welding(self, data:list[KukaTXT]) -> list:
|
def detect_welding(self, data: List[KukaTXT]) -> List:
|
||||||
|
"""
|
||||||
|
Детектирует этап сварки по текстовым данным.
|
||||||
|
"""
|
||||||
...
|
...
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
from typing import Tuple
|
||||||
from PyQt5.QtWidgets import QWidget, QTabWidget
|
from PyQt5.QtWidgets import QWidget, QTabWidget
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap
|
||||||
from PyQt5.QtCore import pyqtSignal
|
from PyQt5.QtCore import pyqtSignal
|
||||||
@ -6,43 +7,96 @@ from base.base import BaseController, Settings
|
|||||||
|
|
||||||
|
|
||||||
class Controller(BaseController):
|
class Controller(BaseController):
|
||||||
|
"""
|
||||||
|
Контроллер приложения.
|
||||||
|
|
||||||
|
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
|
||||||
|
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
|
||||||
|
медиатор и т.д.
|
||||||
|
"""
|
||||||
|
|
||||||
signal_widgets = pyqtSignal(list)
|
signal_widgets = pyqtSignal(list)
|
||||||
signal_progress_bar = pyqtSignal(int)
|
signal_progress_bar = pyqtSignal(int)
|
||||||
signal_status_text = pyqtSignal(str)
|
signal_status_text = pyqtSignal(str)
|
||||||
|
|
||||||
def set_working_mode(self, mode: int) -> None:
|
def set_working_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает рабочий режим приложения через медиатор.
|
||||||
|
|
||||||
|
:param mode: Режим работы (целое число).
|
||||||
|
"""
|
||||||
self._mediator.set_mode(mode)
|
self._mediator.set_mode(mode)
|
||||||
|
|
||||||
def update_plots(self) -> None:
|
def update_plots(self) -> None:
|
||||||
|
"""
|
||||||
|
Инициирует переотрисовку графиков через файловый менеджер.
|
||||||
|
"""
|
||||||
self._file_manager.replot_all()
|
self._file_manager.replot_all()
|
||||||
|
|
||||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
def send_widgets(self, widgets: list[QWidget]) -> None:
|
||||||
|
"""
|
||||||
|
Отправляет список виджетов для дальнейшего использования.
|
||||||
|
|
||||||
|
:param widgets: Список виджетов.
|
||||||
|
"""
|
||||||
self.signal_widgets.emit(widgets)
|
self.signal_widgets.emit(widgets)
|
||||||
|
|
||||||
def update_settings(self, settings: Settings) -> None:
|
def update_settings(self, settings: Settings) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки приложения, передавая их через медиатор.
|
||||||
|
|
||||||
|
:param settings: Объект настроек.
|
||||||
|
"""
|
||||||
self._mediator.notify(self, settings)
|
self._mediator.notify(self, settings)
|
||||||
|
|
||||||
def update_status(self, msg: str) -> None:
|
def update_status(self, msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет статусное сообщение в интерфейсе.
|
||||||
|
|
||||||
|
:param msg: Текст сообщения.
|
||||||
|
"""
|
||||||
self.signal_status_text.emit(msg)
|
self.signal_status_text.emit(msg)
|
||||||
|
|
||||||
def update_progress(self, progress: int) -> None:
|
def update_progress(self, progress: int) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет значение прогресс-бара.
|
||||||
|
|
||||||
|
:param progress: Значение прогресса (от 0 до 100).
|
||||||
|
"""
|
||||||
self.signal_progress_bar.emit(progress)
|
self.signal_progress_bar.emit(progress)
|
||||||
|
|
||||||
def open_file(self, filepath: str) -> None:
|
def open_file(self, filepath: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает файл по указанному пути через файловый менеджер.
|
||||||
|
|
||||||
|
:param filepath: Путь к файлу.
|
||||||
|
"""
|
||||||
self._file_manager.open_custom_file(filepath)
|
self._file_manager.open_custom_file(filepath)
|
||||||
|
|
||||||
def open_dir(self, dirpath: str) -> None:
|
def open_dir(self, dirpath: str) -> None:
|
||||||
|
"""
|
||||||
|
Инициирует предварительный рендеринг данных из директории.
|
||||||
|
|
||||||
|
:param dirpath: Путь к директории.
|
||||||
|
"""
|
||||||
self._mediator.prerender_TCW(self, dirpath)
|
self._mediator.prerender_TCW(self, dirpath)
|
||||||
|
|
||||||
def build_TCW_for_client(self):
|
def build_TCW_for_client(self) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет финальный рендеринг TCW (Trace Control Widget) для клиента.
|
||||||
|
"""
|
||||||
self._mediator.render_TCW(self)
|
self._mediator.render_TCW(self)
|
||||||
|
|
||||||
def save_file(self, data:list[str, QTabWidget]) -> None:
|
def save_file(self, data: Tuple[str, QTabWidget]) -> None:
|
||||||
|
"""
|
||||||
|
Сохраняет снимок содержимого вкладки в указанный файл.
|
||||||
|
|
||||||
|
:param data: Кортеж, содержащий путь к файлу и QTabWidget для сохранения.
|
||||||
|
"""
|
||||||
filepath, tab = data
|
filepath, tab = data
|
||||||
|
# Создаем QPixmap с размером, равным размеру вкладки
|
||||||
pixmap = QPixmap(tab.size())
|
pixmap = QPixmap(tab.size())
|
||||||
|
# Рендерим содержимое вкладки в QPixmap
|
||||||
tab.render(pixmap)
|
tab.render(pixmap)
|
||||||
|
# Сохраняем изображение по указанному пути
|
||||||
pixmap.save(filepath)
|
pixmap.save(filepath)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -8,14 +8,29 @@ pd.set_option('future.no_silent_downcasting', True)
|
|||||||
|
|
||||||
|
|
||||||
class DataConverter(BaseDataConverter):
|
class DataConverter(BaseDataConverter):
|
||||||
|
"""
|
||||||
|
Класс для преобразования данных из CSV-файлов.
|
||||||
|
|
||||||
|
Основные задачи:
|
||||||
|
- Исправление заголовков столбцов по заданному шаблону.
|
||||||
|
- Преобразование булевых значений в целочисленные.
|
||||||
|
- Обработка ошибок при конвертации и уведомление через медиатора.
|
||||||
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
|
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
try:
|
"""
|
||||||
bool_columns = dataframe.columns[dataframe.isin([True, False]).all()]
|
Преобразует столбцы, содержащие булевы значения, в целочисленные.
|
||||||
dataframe = dataframe.astype({col: int for col in bool_columns})
|
|
||||||
|
|
||||||
return dataframe
|
:param df: DataFrame с исходными данными.
|
||||||
|
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Находим столбцы, в которых все значения являются True или False
|
||||||
|
bool_columns = df.columns[df.isin([True, False]).all()]
|
||||||
|
# Приводим найденные столбцы к типу int
|
||||||
|
df = df.astype({col: int for col in bool_columns})
|
||||||
|
return df
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||||
return None
|
return None
|
||||||
@ -27,7 +42,13 @@ class DataConverter(BaseDataConverter):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _fix_headers(dataframe: pd.DataFrame) -> pd.DataFrame:
|
def _fix_headers(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Приводит заголовки столбцов DataFrame к корректным именам согласно заранее заданному списку.
|
||||||
|
|
||||||
|
:param df: DataFrame с исходными заголовками.
|
||||||
|
:return: DataFrame с исправленными заголовками или None, если произошла ошибка.
|
||||||
|
"""
|
||||||
correct_columns = [
|
correct_columns = [
|
||||||
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
|
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
|
||||||
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
|
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
|
||||||
@ -37,15 +58,17 @@ class DataConverter(BaseDataConverter):
|
|||||||
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
|
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
|
||||||
]
|
]
|
||||||
try:
|
try:
|
||||||
|
# Создаем словарь соответствий: ключ - имя в нижнем регистре, значение - корректное имя
|
||||||
correct_mapping = {name.lower(): name for name in correct_columns}
|
correct_mapping = {name.lower(): name for name in correct_columns}
|
||||||
new_columns = []
|
new_columns = []
|
||||||
for col in dataframe.columns:
|
# Для каждого столбца заменяем имя, если оно присутствует в корректном отображении
|
||||||
|
for col in df.columns:
|
||||||
fixed_col = correct_mapping.get(col.lower(), col)
|
fixed_col = correct_mapping.get(col.lower(), col)
|
||||||
new_columns.append(fixed_col)
|
new_columns.append(fixed_col)
|
||||||
dataframe.columns = new_columns
|
df.columns = new_columns
|
||||||
# Удаляем повторяющиеся столбцы, оставляя только первое вхождение
|
# Удаляем повторяющиеся столбцы, оставляя первое вхождение
|
||||||
dataframe = dataframe.loc[:, ~dataframe.columns.duplicated()]
|
df = df.loc[:, ~df.columns.duplicated()]
|
||||||
return dataframe
|
return df
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||||
return None
|
return None
|
||||||
@ -54,9 +77,18 @@ class DataConverter(BaseDataConverter):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def convert_data(self, files: list[str]) -> None:
|
def convert_data(self, files: list[str]) -> None:
|
||||||
|
"""
|
||||||
|
Считывает данные из списка CSV-файлов, исправляет заголовки и преобразует булевые столбцы,
|
||||||
|
после чего уведомляет медиатора о завершении преобразования.
|
||||||
|
|
||||||
|
:param files: Список путей к CSV-файлам.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
|
# Считываем данные из файлов (если путь не пустой, иначе None)
|
||||||
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
|
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
|
||||||
|
# Применяем исправление заголовков к каждому DataFrame
|
||||||
renamed_dataframes = list(map(self._fix_headers, dataframes))
|
renamed_dataframes = list(map(self._fix_headers, dataframes))
|
||||||
|
# Преобразуем булевые значения для каждого DataFrame
|
||||||
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
|
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
|
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
|
||||||
@ -68,4 +100,5 @@ class DataConverter(BaseDataConverter):
|
|||||||
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
|
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
|
||||||
converted_dataframes = [None]
|
converted_dataframes = [None]
|
||||||
finally:
|
finally:
|
||||||
|
# Передаем результат медиатору
|
||||||
self._mediator.notify(self, converted_dataframes)
|
self._mediator.notify(self, converted_dataframes)
|
||||||
|
|||||||
@ -6,64 +6,124 @@ from base.base import BaseDirectoryMonitor, BaseFileManager, Settings
|
|||||||
|
|
||||||
|
|
||||||
class FileManager(BaseFileManager):
|
class FileManager(BaseFileManager):
|
||||||
|
"""
|
||||||
|
Менеджер файлов для работы с библиотекой путей.
|
||||||
|
|
||||||
|
Основные функции:
|
||||||
|
- Переотправка (replot) списка найденных файлов.
|
||||||
|
- Добавление нового пути (например, выбранного пользователем).
|
||||||
|
- Переключение режимов работы (создание отчёта, онлайн-мониторинг, работа с трейсами клиента).
|
||||||
|
- Обновление настроек мониторинга.
|
||||||
|
- Поиск файлов в директории с трейсами: поиск файлов .dat и .txt.
|
||||||
|
"""
|
||||||
|
|
||||||
def replot_all(self) -> None:
|
def replot_all(self) -> None:
|
||||||
|
"""
|
||||||
|
Переотправляет все пути из библиотеки.
|
||||||
|
|
||||||
|
Если текущий режим не равен 3, уведомляет медиатора со списком путей.
|
||||||
|
Если режим равен 3, открывает директорию с трейсами.
|
||||||
|
"""
|
||||||
if self._paths_library is not None:
|
if self._paths_library is not None:
|
||||||
if self._mode != 3: self._mediator.notify(self, list(self._paths_library))
|
if self._mode != 3:
|
||||||
else: self.open_raw_traces_dir(list(self._paths_library)[0])
|
self._mediator.notify(self, list(self._paths_library))
|
||||||
|
else:
|
||||||
|
self.open_raw_traces_dir(list(self._paths_library)[0])
|
||||||
|
|
||||||
def open_custom_file(self, path: str) -> None:
|
def open_custom_file(self, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Добавляет указанный путь в библиотеку и уведомляет медиатора.
|
||||||
|
|
||||||
|
:param path: Путь к файлу, выбранному пользователем.
|
||||||
|
"""
|
||||||
self._paths_library.add(path)
|
self._paths_library.add(path)
|
||||||
|
# Передаем уведомление с указанным путем (если путь пустой, передаем пустую строку)
|
||||||
self._mediator.notify(self, [path if path else ''])
|
self._mediator.notify(self, [path if path else ''])
|
||||||
|
|
||||||
def set_mode(self, num: int) -> None:
|
def set_mode(self, num: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим работы менеджера файлов.
|
||||||
|
|
||||||
|
Режимы:
|
||||||
|
1 - Режим создания отчёта.
|
||||||
|
2 - Режим онлайн-мониторинга папки.
|
||||||
|
3 - Режим работы с трейсами клиента.
|
||||||
|
|
||||||
|
:param num: Целое число, определяющее режим работы.
|
||||||
|
"""
|
||||||
match num:
|
match num:
|
||||||
case 1: # Режим создания отчета
|
case 1:
|
||||||
|
# Режим создания отчёта
|
||||||
self._monitor.stop()
|
self._monitor.stop()
|
||||||
self._paths_library.clear()
|
self._paths_library.clear()
|
||||||
self._paths_library.add('')
|
self._paths_library.add('')
|
||||||
self._mediator.notify(self, list(self._paths_library))
|
self._mediator.notify(self, list(self._paths_library))
|
||||||
self._mode = 1
|
self._mode = 1
|
||||||
|
case 2:
|
||||||
case 2: # Режим онлайн-мониторинга папки
|
# Режим онлайн-мониторинга папки
|
||||||
self._monitor.init_state()
|
self._monitor.init_state()
|
||||||
self._monitor.start()
|
self._monitor.start()
|
||||||
self._mode = 2
|
self._mode = 2
|
||||||
|
case 3:
|
||||||
case 3: # Режим работы с трейсами клиента
|
# Режим работы с трейсами клиента
|
||||||
self._monitor.stop()
|
self._monitor.stop()
|
||||||
self._paths_library.clear()
|
self._paths_library.clear()
|
||||||
self._mode = 3
|
self._mode = 3
|
||||||
|
|
||||||
def update_monitor_settings(self, settings: Settings) -> None:
|
def update_monitor_settings(self, settings: Settings) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки мониторинга директории.
|
||||||
|
|
||||||
|
Из объекта настроек извлекается путь к директории и интервал обновления.
|
||||||
|
Выполняется проверка существования директории и корректности интервала обновления.
|
||||||
|
При необходимости приостанавливается или возобновляется монитор.
|
||||||
|
|
||||||
|
:param settings: Объект Settings с системными настройками.
|
||||||
|
"""
|
||||||
directory_path = settings.system['trace_storage_path'][0]
|
directory_path = settings.system['trace_storage_path'][0]
|
||||||
update_time = settings.system['monitor_update_period'][0]
|
update_time = settings.system['monitor_update_period'][0]
|
||||||
|
|
||||||
if not os.path.exists(directory_path):
|
if not os.path.exists(directory_path):
|
||||||
logger.warning(f"Путь {directory_path} не существует.")
|
logger.warning(f"Путь {directory_path} не существует.")
|
||||||
|
# Можно раскомментировать raise, если требуется прерывание работы
|
||||||
# raise FileNotFoundError(f"Путь {directory_path} не существует.")
|
# raise FileNotFoundError(f"Путь {directory_path} не существует.")
|
||||||
|
|
||||||
if update_time <= 0.01:
|
if update_time <= 0.01:
|
||||||
logger.warning(f"Интервал между проверками папки слишком мал: {update_time}")
|
logger.warning(f"Интервал между проверками папки слишком мал: {update_time}")
|
||||||
|
|
||||||
if self._monitor.isActive: self._monitor.pause()
|
if self._monitor.isActive:
|
||||||
|
self._monitor.pause()
|
||||||
self._monitor._directory_path = directory_path
|
self._monitor._directory_path = directory_path
|
||||||
self._monitor._update_time = update_time
|
self._monitor._update_time = update_time
|
||||||
if self._monitor.isActive: self._monitor.start()
|
if self._monitor.isActive:
|
||||||
|
self._monitor.start()
|
||||||
|
|
||||||
def add_new_paths(self, paths: list) -> None:
|
def add_new_paths(self, paths: list) -> None:
|
||||||
|
"""
|
||||||
|
Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали,
|
||||||
|
и уведомляет медиатора о новых путях.
|
||||||
|
|
||||||
|
:param paths: Список новых путей.
|
||||||
|
"""
|
||||||
paths_set = set(paths)
|
paths_set = set(paths)
|
||||||
new = self._paths_library.difference(paths_set)
|
new_paths = self._paths_library.difference(paths_set)
|
||||||
self._paths_library.update(new)
|
self._paths_library.update(new_paths)
|
||||||
self._mediator.notify(list(new))
|
self._mediator.notify(list(new_paths))
|
||||||
|
|
||||||
def open_raw_traces_dir(self, path: str) -> None:
|
def open_raw_traces_dir(self, path: str) -> None:
|
||||||
|
"""
|
||||||
|
Открывает указанную директорию, ищет в ней файлы с расширениями .dat и .txt,
|
||||||
|
и при их наличии инициирует предварительный рендеринг через медиатора.
|
||||||
|
|
||||||
|
:param path: Путь к директории с трейсами.
|
||||||
|
"""
|
||||||
self._paths_library.clear()
|
self._paths_library.clear()
|
||||||
self._paths_library.add(path)
|
self._paths_library.add(path)
|
||||||
dat_file, txt_file = None, None
|
dat_file, txt_file = None, None
|
||||||
|
|
||||||
|
# Перебираем содержимое директории
|
||||||
for entry in os.listdir(path):
|
for entry in os.listdir(path):
|
||||||
full_path = os.path.join(path, entry)
|
full_path = os.path.join(path, entry)
|
||||||
|
|
||||||
if os.path.isfile(full_path):
|
if os.path.isfile(full_path):
|
||||||
_, ext = os.path.splitext(entry)
|
_, ext = os.path.splitext(entry)
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
@ -75,14 +135,25 @@ class FileManager(BaseFileManager):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if dat_file and txt_file:
|
if dat_file and txt_file:
|
||||||
|
# Уведомляем медиатора для запуска предварительного рендеринга трейс-контента
|
||||||
self._mediator.prerender_TCW(self, [dat_file, txt_file])
|
self._mediator.prerender_TCW(self, [dat_file, txt_file])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryMonitor(BaseDirectoryMonitor):
|
class DirectoryMonitor(BaseDirectoryMonitor):
|
||||||
|
"""
|
||||||
|
Мониторинг директории для поиска CSV-файлов.
|
||||||
|
|
||||||
|
Инициализирует состояние, подключает таймер обновления и отслеживает появление новых файлов.
|
||||||
|
При обнаружении новых файлов уведомляет менеджер файлов.
|
||||||
|
"""
|
||||||
|
|
||||||
def init_state(self):
|
def init_state(self):
|
||||||
|
"""
|
||||||
|
Инициализирует состояние мониторинга директории.
|
||||||
|
|
||||||
|
Проверяет, существует ли директория. Если директория не существует, генерирует ошибку.
|
||||||
|
Собирает список файлов с расширением .csv и подключает метод мониторинга к таймеру.
|
||||||
|
"""
|
||||||
if not os.path.exists(self._directory_path):
|
if not os.path.exists(self._directory_path):
|
||||||
logger.error(f"Путь {self._directory_path} не существует.")
|
logger.error(f"Путь {self._directory_path} не существует.")
|
||||||
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
||||||
@ -97,11 +168,18 @@ class DirectoryMonitor(BaseDirectoryMonitor):
|
|||||||
logger.info("Monitor initiated!")
|
logger.info("Monitor initiated!")
|
||||||
|
|
||||||
def _monitor(self):
|
def _monitor(self):
|
||||||
|
"""
|
||||||
|
Метод, вызываемый по таймеру, для проверки появления новых CSV-файлов.
|
||||||
|
|
||||||
|
Если обнаружены новые файлы, уведомляет менеджер файлов и обновляет список файлов.
|
||||||
|
Если в директории отсутствуют файлы, сбрасывает список.
|
||||||
|
"""
|
||||||
current_files = [
|
current_files = [
|
||||||
os.path.join(self._directory_path, file)
|
os.path.join(self._directory_path, file)
|
||||||
for file in os.listdir(self._directory_path)
|
for file in os.listdir(self._directory_path)
|
||||||
if file.lower().endswith('.csv')
|
if file.lower().endswith('.csv')
|
||||||
]
|
]
|
||||||
|
# Определяем файлы, которых еще не было в предыдущем списке
|
||||||
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
||||||
if new_files:
|
if new_files:
|
||||||
logger.info(f"New files detected: {new_files}")
|
logger.info(f"New files detected: {new_files}")
|
||||||
|
|||||||
@ -17,61 +17,111 @@ from base.base import (
|
|||||||
|
|
||||||
|
|
||||||
class Mediator(BaseMediator):
|
class Mediator(BaseMediator):
|
||||||
|
"""
|
||||||
|
Медиатор для организации взаимодействия между различными компонентами приложения.
|
||||||
|
|
||||||
def notify(self,
|
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
|
||||||
source: Union[BaseFileManager, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseController, BaseRawTraceProcessor],
|
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
|
||||||
data: Union[list[str], list[pd.DataFrame], list[GraphicPassport], list[QWidget], Settings, pd.DataFrame]) -> None:
|
"""
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseFileManager):
|
def notify(
|
||||||
|
self,
|
||||||
|
source: Union[
|
||||||
|
BaseFileManager,
|
||||||
|
BaseDataConverter,
|
||||||
|
BasePointPassportFormer,
|
||||||
|
BasePlotWidget,
|
||||||
|
BaseController,
|
||||||
|
BaseRawTraceProcessor
|
||||||
|
],
|
||||||
|
data: Union[
|
||||||
|
list[str],
|
||||||
|
list[pd.DataFrame],
|
||||||
|
list[GraphicPassport],
|
||||||
|
list[QWidget],
|
||||||
|
Settings,
|
||||||
|
pd.DataFrame
|
||||||
|
]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Принимает уведомление от компонента-источника и направляет данные в соответствующий модуль.
|
||||||
|
|
||||||
|
:param source: Компонент, вызвавший уведомление.
|
||||||
|
:param data: Передаваемые данные (могут быть строками, DataFrame, графическими паспортами, виджетами, Settings).
|
||||||
|
"""
|
||||||
|
# Если источник – менеджер файлов: обновляем статус и запускаем конвертацию CSV
|
||||||
|
if isinstance(source, BaseFileManager):
|
||||||
self._controller.update_status("CSV found! Calculating...")
|
self._controller.update_status("CSV found! Calculating...")
|
||||||
self._converter.convert_data(data)
|
self._converter.convert_data(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseDataConverter):
|
# Если источник – конвертер данных: обновляем прогресс и формируем паспорта
|
||||||
|
if isinstance(source, BaseDataConverter):
|
||||||
self._controller.update_progress(1)
|
self._controller.update_progress(1)
|
||||||
self._passport_former.form_passports(data)
|
self._passport_former.form_passports(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BasePointPassportFormer):
|
# Если источник – формирователь паспортов точек: обновляем прогресс и строим графический паспорт
|
||||||
|
if isinstance(source, BasePointPassportFormer):
|
||||||
self._controller.update_progress(10)
|
self._controller.update_progress(10)
|
||||||
self._plot.build(data)
|
self._plot.build(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BasePlotWidget):
|
# Если источник – виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
|
||||||
|
if isinstance(source, BasePlotWidget):
|
||||||
self._controller.update_progress(100)
|
self._controller.update_progress(100)
|
||||||
self._controller.send_widgets(data)
|
self._controller.send_widgets(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseController):
|
# Если источник – контроллер: обновляем настройки у различных модулей
|
||||||
|
if isinstance(source, BaseController):
|
||||||
self._file_manager.update_monitor_settings(data)
|
self._file_manager.update_monitor_settings(data)
|
||||||
self._passport_former.update_settings(data)
|
self._passport_former.update_settings(data)
|
||||||
self._trace_processor.update_settings(data)
|
self._trace_processor.update_settings(data)
|
||||||
|
|
||||||
def prerender_TCW(self, source:Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
def prerender_TCW(
|
||||||
data:Union[str, list[str], list[pd.DataFrame, dict]]) -> None:
|
self,
|
||||||
|
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
||||||
|
data: Union[str, list[str], list[pd.DataFrame], dict]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления.
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseController):
|
:param source: Компонент, инициирующий предварительный рендеринг.
|
||||||
|
:param data: Передаваемые данные, могут быть строкой или списком путей/DataFrame и словарём.
|
||||||
|
"""
|
||||||
|
if isinstance(source, BaseController):
|
||||||
self._controller.update_progress(5)
|
self._controller.update_progress(5)
|
||||||
self._file_manager.open_raw_traces_dir(data)
|
self._file_manager.open_raw_traces_dir(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseFileManager):
|
if isinstance(source, BaseFileManager):
|
||||||
self._controller.update_progress(15)
|
self._controller.update_progress(15)
|
||||||
self._trace_processor.prerender(data)
|
self._trace_processor.prerender(data)
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseRawTraceProcessor):
|
if isinstance(source, BaseRawTraceProcessor):
|
||||||
self._controller.update_progress(40)
|
self._controller.update_progress(40)
|
||||||
self._plot.build_raw_trace(data)
|
self._plot.build_raw_trace(data)
|
||||||
|
|
||||||
def render_TCW(self, source:Union[BaseController, BaseRawTraceProcessor],
|
def render_TCW(
|
||||||
data:Union[str, list[pd.DataFrame, dict]] = None) -> None:
|
self,
|
||||||
|
source: Union[BaseController, BaseRawTraceProcessor],
|
||||||
|
data: Union[str, list[pd.DataFrame], dict] = None
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Выполняет финальный рендеринг TCW.
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseController):
|
:param source: Компонент, инициирующий финальный рендеринг.
|
||||||
|
:param data: Передаваемые данные (опционально).
|
||||||
|
"""
|
||||||
|
if isinstance(source, BaseController):
|
||||||
self._controller.update_progress(5)
|
self._controller.update_progress(5)
|
||||||
self._trace_processor.final_render()
|
self._trace_processor.final_render()
|
||||||
|
|
||||||
if issubclass(source.__class__, BaseRawTraceProcessor):
|
if isinstance(source, BaseRawTraceProcessor):
|
||||||
self._controller.update_progress(5)
|
self._controller.update_progress(5)
|
||||||
self._passport_former.form_customer_passport(data)
|
self._passport_former.form_customer_passport(data)
|
||||||
|
|
||||||
def set_mode(self, mode: int) -> None:
|
def set_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим работы для графиков и файлового менеджера.
|
||||||
|
|
||||||
|
:param mode: Целое число, определяющее режим работы.
|
||||||
|
"""
|
||||||
self._plot.set_mode(mode)
|
self._plot.set_mode(mode)
|
||||||
self._file_manager.set_mode(mode)
|
self._file_manager.set_mode(mode)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,99 +1,149 @@
|
|||||||
from typing import Optional, Any
|
from typing import Optional, Any, Tuple, List, Dict
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from base.base import BasePointPassportFormer, BaseIdealDataBuilder, PointPassport, GraphicPassport, Settings, UsefulGraphData
|
from base.base import (
|
||||||
|
BasePointPassportFormer,
|
||||||
|
BaseIdealDataBuilder,
|
||||||
|
PointPassport,
|
||||||
|
GraphicPassport,
|
||||||
|
Settings,
|
||||||
|
UsefulGraphData
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PassportFormer(BasePointPassportFormer):
|
class PassportFormer(BasePointPassportFormer):
|
||||||
|
"""
|
||||||
|
Класс для формирования паспортов (графических и точечных) по данным трассировки.
|
||||||
|
|
||||||
def form_passports(self, data: list[pd.DataFrame]) -> None:
|
Основные возможности:
|
||||||
|
- Формирование паспортов для каждого DataFrame.
|
||||||
|
- Формирование паспорта заказчика на основе DataFrame и событий.
|
||||||
|
- Генерация событий по DataFrame.
|
||||||
|
- Построение идеальных данных для каждой точки.
|
||||||
|
- Формирование графического паспорта, включающего полезные данные и перечень паспортов точек.
|
||||||
|
|
||||||
|
Внимание: многие атрибуты (например, self._mediator, self._stages, self._clear_stage,
|
||||||
|
self._settings, self._ideal_data_cache, self._OptAlgorithm_operator_params,
|
||||||
|
self._OptAlgorithm_system_params) предполагается задавать извне (например, в базовом классе).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||||
"""
|
"""
|
||||||
Формирует паспорта для каждого DataFrame из списка.
|
Формирует паспорта для каждого DataFrame из списка.
|
||||||
В случае ошибки логируется сообщение и возвращается пустой список.
|
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||||
|
|
||||||
|
:param data: Список DataFrame с данными трассировки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
return_data = [self._build_from_df_only(dataframe) for dataframe in data]
|
passports = [self._build_from_df_only(df) for df in data]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
|
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
|
||||||
return_data = []
|
passports = []
|
||||||
finally:
|
finally:
|
||||||
self._mediator.notify(self, return_data)
|
self._mediator.notify(self, passports)
|
||||||
|
|
||||||
def update_settings(self, settings: Settings):
|
def update_settings(self, settings: Settings) -> None:
|
||||||
|
"""
|
||||||
|
Обновляет настройки формирования паспортов.
|
||||||
|
|
||||||
|
:param settings: Объект настроек.
|
||||||
|
"""
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
|
|
||||||
def form_customer_passport(self, data: list[pd.DataFrame, dict]) -> None:
|
def form_customer_passport(self, data: Tuple[pd.DataFrame, Dict, Tuple]) -> None:
|
||||||
"""
|
"""
|
||||||
Формирует паспорт заказчика на основе DataFrame и словаря событий.
|
Формирует паспорт заказчика на основе DataFrame и словаря событий.
|
||||||
В случае ошибки логируется сообщение и возвращается пустой список.
|
Ожидается, что data содержит:
|
||||||
|
- DataFrame с данными,
|
||||||
|
- словарь событий,
|
||||||
|
- кортеж координат ME (например, (part_pos, open_pos)).
|
||||||
|
|
||||||
|
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||||
|
|
||||||
|
:param data: Кортеж из (DataFrame, события, ME_coords).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
dataframe, events, ME_coords = data
|
dataframe, events, ME_coords = data
|
||||||
point_quantity = len(events["Squeeze"][0])
|
point_quantity = len(events["Squeeze"][0])
|
||||||
self._modify_coord_settings(ME_coords)
|
self._modify_coord_settings(ME_coords)
|
||||||
data_passport = self._form_graphic_passport(dataframe, events, point_quantity)
|
customer_passport = self._form_graphic_passport(dataframe, events, point_quantity)
|
||||||
return_data = [data_passport]
|
result = [customer_passport]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
|
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
|
||||||
return_data = []
|
result = []
|
||||||
finally:
|
finally:
|
||||||
self._mediator.notify(self, return_data)
|
self._mediator.notify(self, result)
|
||||||
|
|
||||||
def _modify_coord_settings(self, ME_coords:tuple) -> None:
|
def _modify_coord_settings(self, ME_coords: Tuple[List[float], List[float]]) -> None:
|
||||||
|
"""
|
||||||
|
Модифицирует настройки координат на основе переданных данных ME.
|
||||||
|
|
||||||
|
:param ME_coords: Кортеж из двух списков: (part_pos, open_pos).
|
||||||
|
"""
|
||||||
part_pos, open_pos = ME_coords
|
part_pos, open_pos = ME_coords
|
||||||
self._settings.operator["distance_l_2"] = open_pos
|
self._settings.operator["distance_l_2"] = open_pos
|
||||||
l1 = self._settings.operator["distance_l_1"]
|
l1 = self._settings.operator["distance_l_1"]
|
||||||
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
|
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_indexes(signal: str, dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
|
def _find_indexes(signal: str, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
"""
|
"""
|
||||||
Находит индексы начала и конца этапа для указанного сигнала.
|
Находит индексы начала и окончания этапа для указанного сигнала.
|
||||||
|
|
||||||
|
:param signal: Имя столбца-сигнала.
|
||||||
|
:param df: DataFrame с данными.
|
||||||
|
:return: Кортеж из массивов индексов начала и окончания этапа.
|
||||||
"""
|
"""
|
||||||
stage_diff = np.diff(dataframe[signal])
|
stage_diff = np.diff(df[signal])
|
||||||
start_idx = np.where(stage_diff == 1)
|
start_idx = np.where(stage_diff == 1)
|
||||||
finish_idx = np.where(stage_diff == -1)
|
finish_idx = np.where(stage_diff == -1)
|
||||||
return start_idx[0], finish_idx[0]
|
return start_idx[0], finish_idx[0]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _find_events(signal: str,
|
def _find_events(signal: str, times: pd.Series, df: pd.DataFrame) -> Tuple[List[float], List[float]]:
|
||||||
times: pd.Series,
|
|
||||||
dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
|
|
||||||
"""
|
"""
|
||||||
Формирует списки времен начала и окончания событий для указанного сигнала.
|
Формирует списки времён начала и окончания событий для указанного сигнала.
|
||||||
"""
|
|
||||||
start_idx, finish_idx = PassportFormer._find_indexes(signal, dataframe)
|
|
||||||
|
|
||||||
|
Если у первого события не определено время начала, оно принимается за 0.
|
||||||
|
Если число стартов события больше числа финишей, последним финишем считается конец времён.
|
||||||
|
|
||||||
|
:param signal: Имя столбца-сигнала.
|
||||||
|
:param times: Series с временными метками.
|
||||||
|
:param df: DataFrame с данными.
|
||||||
|
:return: Кортеж из двух списков: (список времён начала, список времён окончания).
|
||||||
|
"""
|
||||||
|
start_idx, finish_idx = PassportFormer._find_indexes(signal, df)
|
||||||
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
|
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
|
||||||
start_idx = np.insert(start_idx, 0, 0)
|
start_idx = np.insert(start_idx, 0, 0)
|
||||||
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
|
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
|
||||||
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
|
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
|
||||||
if len(start_list) - len(end_list) == 1:
|
if len(start_list) - len(end_list) == 1:
|
||||||
end_list.append(float(times.iloc[-1]))
|
end_list.append(float(times.iloc[-1]))
|
||||||
return start_list, end_list
|
return (start_list, end_list)
|
||||||
|
|
||||||
def _generate_events(self,
|
def _generate_events(self, times: pd.Series, df: pd.DataFrame) -> Tuple[Dict[str, List[List[float]]], int]:
|
||||||
times: pd.Series,
|
|
||||||
dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
|
|
||||||
"""
|
"""
|
||||||
Генерирует словарь событий для каждого этапа и определяет количество точек.
|
Генерирует словарь событий для каждого этапа, используя временные метки и данные.
|
||||||
Если для основного этапа (_clear_stage) не найдено ни одного события,
|
Также определяет общее количество точек (на основе количества событий основного этапа).
|
||||||
логируется ошибка и возвращается пустой список.
|
|
||||||
|
Если для основного этапа (self._clear_stage) не найдено ни одного события, возвращается пустой словарь и 0 точек.
|
||||||
|
|
||||||
|
:param times: Серия временных меток.
|
||||||
|
:param df: DataFrame с данными.
|
||||||
|
:return: Кортеж (словарь событий, количество точек).
|
||||||
"""
|
"""
|
||||||
events = {}
|
events = {}
|
||||||
point_quantity = 0
|
point_quantity = 0
|
||||||
if self._clear_stage in self._stages:
|
if self._clear_stage in self._stages:
|
||||||
start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
|
start_list, end_list = self._find_events(self._clear_stage, times, df)
|
||||||
point_quantity = len(start_list)
|
point_quantity = len(start_list)
|
||||||
if point_quantity == 0:
|
if point_quantity == 0:
|
||||||
logger.error("_generate_events - Не найдены события для этапа '{}'.", self._clear_stage)
|
logger.error(f"_generate_events - Не найдены события для этапа '{self._clear_stage}'.")
|
||||||
return {}, 0 # Возвращаем пустой словарь и 0 точек
|
return {}, 0
|
||||||
|
|
||||||
for stage in self._stages:
|
for stage in self._stages:
|
||||||
s_list, e_list = self._find_events(stage, times, dataframe)
|
s_list, e_list = self._find_events(stage, times, df)
|
||||||
temp = min(len(s_list), len(e_list))
|
temp = min(len(s_list), len(e_list))
|
||||||
if temp < point_quantity:
|
if temp < point_quantity:
|
||||||
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
|
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
|
||||||
@ -101,34 +151,42 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
s_list += [0] * (point_quantity - temp)
|
s_list += [0] * (point_quantity - temp)
|
||||||
e_list += [1] * (point_quantity - temp)
|
e_list += [1] * (point_quantity - temp)
|
||||||
events[stage] = [s_list, e_list]
|
events[stage] = [s_list, e_list]
|
||||||
return events, point_quantity
|
return (events, point_quantity)
|
||||||
|
|
||||||
def _build_ideal_data(self,
|
def _build_ideal_data(self, idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
||||||
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
point_settings: Optional[Settings] = None) -> Dict:
|
||||||
point_settings: Settings = None) -> dict:
|
|
||||||
"""
|
"""
|
||||||
Строит идеальные данные с использованием переданного билдера.
|
Строит идеальные данные с использованием билдера.
|
||||||
|
|
||||||
|
:param idealDataBuilder: Класс-билдер для генерации идеальных данных.
|
||||||
|
:param point_settings: Настройки для точки.
|
||||||
|
:return: Словарь с идеальными данными для этапов.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.opt_algorithm = idealDataBuilder(point_settings)
|
self._opt_algorithm = idealDataBuilder(point_settings)
|
||||||
stage_ideals = {
|
stage_ideals = {
|
||||||
"Closing": self._opt_algorithm.get_closingDF(),
|
"Closing": self.opt_algorithm.get_closingDF(),
|
||||||
"Squeeze": self._opt_algorithm.get_compressionDF(),
|
"Squeeze": self.opt_algorithm.get_compressionDF(),
|
||||||
"Welding": self._opt_algorithm.get_weldingDF(),
|
"Welding": self.opt_algorithm.get_weldingDF(),
|
||||||
"Relief": self._opt_algorithm.get_openingDF(),
|
"Relief": self.opt_algorithm.get_openingDF(),
|
||||||
"Oncomming": self._opt_algorithm.get_oncomingDF(),
|
"Oncomming": self.opt_algorithm.get_oncomingDF(),
|
||||||
"Ideal cycle": self._opt_algorithm.get_cycle_time(),
|
"Ideal cycle": self.opt_algorithm.get_cycle_time(),
|
||||||
"Ideal timings": self._opt_algorithm.get_ideal_timings()
|
"Ideal timings": self.opt_algorithm.get_ideal_timings()
|
||||||
}
|
}
|
||||||
return stage_ideals
|
return stage_ideals
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
|
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _generate_cache_key(self,
|
def _generate_cache_key(self, point_settings: Settings) -> Tuple[Tuple[Tuple[str, Any], ...], Tuple[Tuple[str, Any], ...]]:
|
||||||
point_settings: Settings) -> tuple[tuple[tuple[str, Any], ...], tuple[tuple[str, Any], ...]]:
|
|
||||||
"""
|
"""
|
||||||
Преобразует point_settings в хешируемый ключ для кэша.
|
Преобразует настройки точки в хешируемый ключ для кэша.
|
||||||
|
|
||||||
|
Использует только те параметры оператора и системы, которые присутствуют в
|
||||||
|
соответствующих наборах (self._OptAlgorithm_operator_params и self._OptAlgorithm_system_params).
|
||||||
|
|
||||||
|
:param point_settings: Объект настроек для точки.
|
||||||
|
:return: Кортеж из двух frozenset с параметрами.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
operator_tuple = frozenset(
|
operator_tuple = frozenset(
|
||||||
@ -146,17 +204,20 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
|
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
|
||||||
return ((), ())
|
return ((), ())
|
||||||
|
|
||||||
def _build_from_df_only(self, dataframe: pd.DataFrame) -> GraphicPassport:
|
def _build_from_df_only(self, df: pd.DataFrame) -> Optional[GraphicPassport]:
|
||||||
"""
|
"""
|
||||||
Строит GraphicPassport на основе одного DataFrame.
|
Строит GraphicPassport на основе одного DataFrame.
|
||||||
Если DataFrame содержит необходимые столбцы, события генерируются;
|
|
||||||
иначе используется альтернативное определение point_quantity.
|
Если DataFrame содержит необходимые столбцы (определяемые в self._stages),
|
||||||
|
генерируются события; иначе используется альтернативное определение количества точек.
|
||||||
Если количество точек равно нулю, логируется ошибка и возвращается None.
|
Если количество точек равно нулю, логируется ошибка и возвращается None.
|
||||||
|
|
||||||
|
:param df: DataFrame с данными.
|
||||||
|
:return: Объект GraphicPassport или None в случае ошибки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if (dataframe is not None and
|
if df is not None and set(self._stages).issubset(set(df.columns.tolist())):
|
||||||
set(self._stages).issubset(set(dataframe.columns.tolist()))):
|
events, _ = self._generate_events(df["time"], df)
|
||||||
events, point_quantity = self._generate_events(dataframe["time"], dataframe)
|
|
||||||
point_quantity = len(events["Welding"][0])
|
point_quantity = len(events["Welding"][0])
|
||||||
if point_quantity == 0:
|
if point_quantity == 0:
|
||||||
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
|
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
|
||||||
@ -166,22 +227,34 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
key = list(self._settings.operator.keys())[0]
|
key = list(self._settings.operator.keys())[0]
|
||||||
point_quantity = len(self._settings.operator[key])
|
point_quantity = len(self._settings.operator[key])
|
||||||
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
|
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
|
||||||
passport = self._form_graphic_passport(dataframe, events, point_quantity)
|
passport = self._form_graphic_passport(df, events, point_quantity)
|
||||||
return passport
|
return passport
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
|
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _form_graphic_passport(self,
|
def _form_graphic_passport(self, df: pd.DataFrame, events: Dict, point_quantity: int) -> Optional[GraphicPassport]:
|
||||||
dataframe: pd.DataFrame,
|
|
||||||
events: dict,
|
|
||||||
point_quantity: int) -> GraphicPassport:
|
|
||||||
"""
|
"""
|
||||||
Формирует графический паспорт с полезными данными и списком PointPassport.
|
Формирует графический паспорт, включающий полезные данные и список PointPassport.
|
||||||
|
|
||||||
|
Для каждой из точек:
|
||||||
|
- Извлекаются настройки оператора для данной точки.
|
||||||
|
- Формируется временной интервал и события.
|
||||||
|
- Рассчитываются идеальные и полезные данные.
|
||||||
|
- Создаётся объект PointPassport, который добавляется в список.
|
||||||
|
|
||||||
|
:param df: DataFrame с данными.
|
||||||
|
:param events: Словарь событий, сгенерированных для этапов.
|
||||||
|
:param point_quantity: Количество точек.
|
||||||
|
:return: Объект GraphicPassport или None в случае ошибки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
system_settings = {key: value[0] for key, value in self._settings.system.items()}
|
system_settings = {key: value[0] for key, value in self._settings.system.items()}
|
||||||
graphic_passport = GraphicPassport(dataframe, [], self._form_graphic_useful_data(system_settings))
|
graphic_passport = GraphicPassport(
|
||||||
|
df,
|
||||||
|
[],
|
||||||
|
self._form_graphic_useful_data(system_settings)
|
||||||
|
)
|
||||||
|
|
||||||
for i in range(point_quantity):
|
for i in range(point_quantity):
|
||||||
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
|
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
|
||||||
@ -197,9 +270,12 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
|
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _form_graphic_useful_data(self, system_settings: dict) -> dict:
|
def _form_graphic_useful_data(self, system_settings: Dict) -> UsefulGraphData:
|
||||||
"""
|
"""
|
||||||
Формирует словарь полезных данных для графика.
|
Формирует словарь полезных данных для графического паспорта.
|
||||||
|
|
||||||
|
:param system_settings: Словарь системных настроек.
|
||||||
|
:return: Объект UsefulGraphData.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tesla_time = sum(self._settings.operator.get("Tesla summary time", []))
|
tesla_time = sum(self._settings.operator.get("Tesla summary time", []))
|
||||||
@ -211,11 +287,14 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
return useful_data
|
return useful_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
|
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
|
||||||
return {}
|
return UsefulGraphData()
|
||||||
|
|
||||||
def _form_point_useful_data(self, operator_settings: dict) -> dict:
|
def _form_point_useful_data(self, operator_settings: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
Формирует полезные данные для отдельной точки.
|
Формирует полезные данные для отдельной точки.
|
||||||
|
|
||||||
|
:param operator_settings: Словарь настроек оператора для точки.
|
||||||
|
:return: Словарь с полезными данными (толщина, позиция детали, сила) или пустой словарь.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
useful_data = {
|
useful_data = {
|
||||||
@ -228,25 +307,36 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
|
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _form_point_ideal_data(self, point_settings: Settings) -> dict:
|
def _form_point_ideal_data(self, point_settings: Settings) -> Dict:
|
||||||
"""
|
"""
|
||||||
Формирует идеальные данные для точки с использованием кэша.
|
Формирует идеальные данные для отдельной точки с использованием кэша.
|
||||||
|
|
||||||
|
Генерируется кэш-ключ из настроек точки, затем либо извлекаются ранее рассчитанные
|
||||||
|
данные, либо вычисляются новые с помощью билдера.
|
||||||
|
|
||||||
|
:param point_settings: Настройки точки.
|
||||||
|
:return: Словарь с идеальными данными или пустой словарь в случае ошибки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
cache_key = self._generate_cache_key(point_settings)
|
cache_key = self._generate_cache_key(point_settings)
|
||||||
ideal_data = self._ideal_data_cashe.get(
|
ideal_data = self._ideal_data_cache.get(
|
||||||
cache_key,
|
cache_key,
|
||||||
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
|
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
|
||||||
)
|
)
|
||||||
self._ideal_data_cashe[cache_key] = ideal_data
|
self._ideal_data_cache[cache_key] = ideal_data
|
||||||
return ideal_data
|
return ideal_data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
|
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_operator_settings_part(self, idx: int) -> dict:
|
def _get_operator_settings_part(self, idx: int) -> Dict:
|
||||||
"""
|
"""
|
||||||
Извлекает часть настроек оператора для конкретного индекса.
|
Извлекает часть настроек оператора для конкретного индекса.
|
||||||
|
|
||||||
|
Если индекс выходит за пределы списка, используется первый элемент.
|
||||||
|
|
||||||
|
:param idx: Индекс точки.
|
||||||
|
:return: Словарь настроек оператора для данной точки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
operator_settings = {
|
operator_settings = {
|
||||||
@ -258,14 +348,20 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
|
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _form_point_events(self, events: dict, idx: int) -> list[list, dict]:
|
def _form_point_events(self, events: Dict, idx: int) -> Tuple[Optional[List[float]], Optional[Dict[str, List[float]]]]:
|
||||||
"""
|
"""
|
||||||
Формирует временной интервал и события для отдельной точки.
|
Формирует временной интервал и события для отдельной точки.
|
||||||
|
|
||||||
Если событий нет, возвращает (None, None).
|
Если событий нет, возвращает (None, None).
|
||||||
|
|
||||||
|
:param events: Словарь с событиями для всех этапов.
|
||||||
|
:param idx: Индекс точки.
|
||||||
|
:return: Кортеж (timeframe, point_events) или (None, None) в случае ошибки.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
timeframe, point_events = None, None
|
timeframe, point_events = None, None
|
||||||
if events is not None:
|
if events is not None:
|
||||||
|
# Если первое событие основного этапа начинается с 0, сдвигаем индекс
|
||||||
idx_shift = idx + 1 if events[self._stages[-1]][0][0] == 0 else idx
|
idx_shift = idx + 1 if events[self._stages[-1]][0][0] == 0 else idx
|
||||||
timeframe = [events[self._stages[0]][0][idx], events[self._stages[-1]][1][idx_shift]]
|
timeframe = [events[self._stages[0]][0][idx], events[self._stages[-1]][1][idx_shift]]
|
||||||
point_events = {key: [value[0][idx], value[1][idx]] for key, value in events.items()}
|
point_events = {key: [value[0][idx], value[1][idx]] for key, value in events.items()}
|
||||||
@ -276,8 +372,18 @@ class PassportFormer(BasePointPassportFormer):
|
|||||||
|
|
||||||
|
|
||||||
class IdealDataBuilder(BaseIdealDataBuilder):
|
class IdealDataBuilder(BaseIdealDataBuilder):
|
||||||
|
"""
|
||||||
|
Класс для построения идеальных данных по этапам.
|
||||||
|
|
||||||
|
Реализует методы получения DataFrame для различных этапов:
|
||||||
|
закрытия, сжатия, открытия, движения, сварки,
|
||||||
|
а также метод получения идеальных временных интервалов.
|
||||||
|
"""
|
||||||
|
|
||||||
def get_closingDF(self) -> pd.DataFrame:
|
def get_closingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа закрытия.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -285,6 +391,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def get_compressionDF(self) -> pd.DataFrame:
|
def get_compressionDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа сжатия.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -292,6 +401,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def get_openingDF(self) -> pd.DataFrame:
|
def get_openingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа открытия.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -299,6 +411,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def get_oncomingDF(self) -> pd.DataFrame:
|
def get_oncomingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа движения.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -306,8 +421,15 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def get_weldingDF(self) -> pd.DataFrame:
|
def get_weldingDF(self) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Получает DataFrame для этапа сварки.
|
||||||
|
|
||||||
|
Используется функция calcPhaseGrow с небольшим сдвигом времени.
|
||||||
|
Значения масштабируются (умножаются на 1000) для перевода в нужные единицы.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = []
|
data = []
|
||||||
|
# Используем небольшое смещение для расчёта сварки
|
||||||
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
|
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
|
||||||
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
|
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
|
||||||
points_num = 5
|
points_num = 5
|
||||||
@ -325,7 +447,11 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
|
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
def get_ideal_timings(self) -> list[float]:
|
def get_ideal_timings(self) -> List[float]:
|
||||||
|
"""
|
||||||
|
Получает список идеальных временных интервалов для этапов.
|
||||||
|
Список включает: tclose, tgrow, welding_time, getMarkOpen(), tmovement.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
data = self.Ts
|
data = self.Ts
|
||||||
ideal_timings = [
|
ideal_timings = [
|
||||||
@ -342,21 +468,30 @@ class IdealDataBuilder(BaseIdealDataBuilder):
|
|||||||
|
|
||||||
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
|
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Получает данные до указанного времени с шагом, определяемым mul.
|
Получает данные до указанного времени (end_timestamp) с шагом, определяемым параметром mul.
|
||||||
|
|
||||||
|
Для каждого шага рассчитываются значения с использованием переданной функции func.
|
||||||
|
В конце добавляется строка с точным значением end_timestamp.
|
||||||
|
|
||||||
|
:param end_timestamp: Время окончания получения данных.
|
||||||
|
:param func: Функция для расчёта значений в зависимости от времени.
|
||||||
|
:return: DataFrame с рассчитанными данными.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = []
|
data = []
|
||||||
|
# Генерируем данные с шагом 1/mul
|
||||||
for i in range(0, int(end_timestamp * self.mul) + 1):
|
for i in range(0, int(end_timestamp * self.mul) + 1):
|
||||||
time = i / self.mul
|
time_val = i / self.mul
|
||||||
X1, X2, V1, V2, F = func(time)
|
X1, X2, V1, V2, F = func(time_val)
|
||||||
data.append({
|
data.append({
|
||||||
"time": time,
|
"time": time_val,
|
||||||
"Position FE": X1 * 1000,
|
"Position FE": X1 * 1000,
|
||||||
"Position ME": X2 * 1000,
|
"Position ME": X2 * 1000,
|
||||||
"Rotor Speed FE": V1 * 1000,
|
"Rotor Speed FE": V1 * 1000,
|
||||||
"Rotor Speed ME": V2 * 1000,
|
"Rotor Speed ME": V2 * 1000,
|
||||||
"Force": F
|
"Force": F
|
||||||
})
|
})
|
||||||
|
# Добавляем финальную строку с end_timestamp
|
||||||
X1, X2, V1, V2, F = func(end_timestamp)
|
X1, X2, V1, V2, F = func(end_timestamp)
|
||||||
data.append({
|
data.append({
|
||||||
"time": end_timestamp,
|
"time": end_timestamp,
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
from .plotter import PlotWidget
|
from .plotter import PlotWidget
|
||||||
from .settings_window import settingsWindow
|
from .settings_window import SettingsWindow
|
||||||
|
|||||||
@ -70,7 +70,7 @@ class MainWindow(BaseMainWindow):
|
|||||||
self.menu.action_scanning.triggered.connect(self._init_seekingUI)
|
self.menu.action_scanning.triggered.connect(self._init_seekingUI)
|
||||||
self.menu.action_report.triggered.connect(self._init_raportUI)
|
self.menu.action_report.triggered.connect(self._init_raportUI)
|
||||||
self.menu.action_client.triggered.connect(self._init_client_UI)
|
self.menu.action_client.triggered.connect(self._init_client_UI)
|
||||||
self.menu.action_view_settings.triggered.connect(lambda: self._on_tab_changed(0))
|
self.menu.action_view_settings.triggered.connect(lambda: self._on_tab_changed)
|
||||||
self.menu.setup(self)
|
self.menu.setup(self)
|
||||||
|
|
||||||
def _init_status_bar(self) -> None:
|
def _init_status_bar(self) -> None:
|
||||||
@ -81,13 +81,13 @@ class MainWindow(BaseMainWindow):
|
|||||||
self._tab_widget = CustomTabWidget()
|
self._tab_widget = CustomTabWidget()
|
||||||
self._tab_widget.currentChanged.connect(self._on_tab_changed)
|
self._tab_widget.currentChanged.connect(self._on_tab_changed)
|
||||||
|
|
||||||
def _on_tab_changed(self, index):
|
def _on_tab_changed(self):
|
||||||
tab = self._tab_widget.currentWidget()
|
tab = self._tab_widget.currentWidget()
|
||||||
if tab:
|
if tab:
|
||||||
reg_items = tab.property("reg_items")
|
reg_items = tab.property("reg_items")
|
||||||
curve_items = tab.property("curve_items")
|
curve_items = tab.property("curve_items")
|
||||||
qt_items = tab.property("qt_items")
|
qt_items = tab.property("qt_items")
|
||||||
self.repSettings.build(index, reg_items, curve_items, qt_items)
|
self.repSettings.build(reg_items, curve_items, qt_items)
|
||||||
|
|
||||||
def _clear(self) -> None:
|
def _clear(self) -> None:
|
||||||
if self.layout() is not None:
|
if self.layout() is not None:
|
||||||
@ -139,9 +139,9 @@ class MainWindow(BaseMainWindow):
|
|||||||
|
|
||||||
def _transfer_settings(self) -> None:
|
def _transfer_settings(self) -> None:
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
settings.system = self.sysSettings.getParams()
|
settings.system = self.sysSettings.get_params()
|
||||||
settings.operator = self.operSettings.getParams()
|
settings.operator = self.operSettings.get_params()
|
||||||
settings.filter = self.filterSettings.getParams()
|
settings.filter = self.filterSettings.get_params()
|
||||||
self.signal_settings.emit(settings)
|
self.signal_settings.emit(settings)
|
||||||
|
|
||||||
def _upd_settings(self) -> None:
|
def _upd_settings(self) -> None:
|
||||||
|
|||||||
@ -1,17 +1,12 @@
|
|||||||
import copy
|
import copy
|
||||||
import traceback
|
import traceback
|
||||||
import sys
|
import sys
|
||||||
from typing import Optional, Tuple, Callable, List, Any
|
from typing import Optional, Tuple, Callable, List, Any, Dict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QVBoxLayout,
|
QGraphicsRectItem, QSpacerItem, QSizePolicy
|
||||||
QHBoxLayout,
|
|
||||||
QLabel,
|
|
||||||
QGraphicsRectItem,
|
|
||||||
QSpacerItem,
|
|
||||||
QSizePolicy
|
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@ -19,13 +14,19 @@ import pyqtgraph as pg
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from base.base import BasePlotWidget, GraphicPassport, PlotItems, PointPassport, UsefulGraphData, BaseController
|
from base.base import (
|
||||||
|
BasePlotWidget, GraphicPassport, PlotItems, PointPassport,
|
||||||
|
UsefulGraphData, BaseController
|
||||||
|
)
|
||||||
from utils.json_tools import read_json
|
from utils.json_tools import read_json
|
||||||
from utils import qt_settings as qts
|
from utils import qt_settings as qts
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Дата-класс для хранения временных характеристик канала
|
||||||
|
# =============================================================================
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChannelTimings():
|
class ChannelTimings:
|
||||||
shift: float = 0
|
shift: float = 0
|
||||||
TWC_time: float = 0.0
|
TWC_time: float = 0.0
|
||||||
ideal_time: float = 0.0
|
ideal_time: float = 0.0
|
||||||
@ -33,28 +34,45 @@ class ChannelTimings():
|
|||||||
TWC_start: float = 0.0
|
TWC_start: float = 0.0
|
||||||
TWC_end: float = 0.0
|
TWC_end: float = 0.0
|
||||||
worst_performance: float = 2
|
worst_performance: float = 2
|
||||||
worst_timeframe: list = field(default_factory=lambda: [0, 0])
|
worst_timeframe: List[float] = field(default_factory=lambda: [0, 0])
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Класс PlotWidget – построение графических виджетов на основе графических паспортов
|
||||||
|
# =============================================================================
|
||||||
class PlotWidget(BasePlotWidget):
|
class PlotWidget(BasePlotWidget):
|
||||||
|
"""
|
||||||
|
Виджет построения графиков. На основе полученных графических паспортов
|
||||||
|
создаёт набор виджетов (для отображения нескольких наборов данных),
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, controller: BaseController):
|
def __init__(self, controller: BaseController):
|
||||||
|
"""
|
||||||
|
Инициализирует PlotWidget, читая параметры структуры графиков из JSON.
|
||||||
|
"""
|
||||||
super().__init__(controller=controller)
|
super().__init__(controller=controller)
|
||||||
self._plt_structures = read_json("params/plot_structure_params.json")
|
self._plt_structures = read_json("params/plot_structure_params.json")
|
||||||
self._plt_channels = None
|
self._plt_channels = None # Будет установлен в зависимости от режима
|
||||||
|
|
||||||
def build(self, data: list[GraphicPassport]) -> None:
|
def build(self, data: List[GraphicPassport]) -> None:
|
||||||
"""
|
"""
|
||||||
Создает набор виджетов по предоставленному списку данных.
|
Создает набор виджетов на основе списка графических паспортов.
|
||||||
|
При ошибке выводится информационная метка об ошибке.
|
||||||
|
|
||||||
|
:param data: Список объектов GraphicPassport с данными для построения графиков.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self._datalen = len(data)
|
self._datalen = len(data)
|
||||||
widgets_datapack = [self._build_widget(data_sample) for self._datastep, data_sample in enumerate(data)]
|
widgets_datapack = []
|
||||||
|
# Проходим по каждому графическому паспорту и обновляем текущий индекс
|
||||||
|
for idx, passport in enumerate(data):
|
||||||
|
self._datastep = idx # Обновляем текущий шаг для обновления статуса
|
||||||
|
widget = self._build_widget(passport)
|
||||||
|
widgets_datapack.append(widget)
|
||||||
except (KeyError, ValueError, TypeError) as e:
|
except (KeyError, ValueError, TypeError) as e:
|
||||||
logger.error(f"Возникла проблема при сборке данных графика: {e}")
|
logger.error(f"Ошибка при сборке данных графика: {e}")
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
error_label = QLabel("Произошла ошибка при формировании графиков. Пожалуйста, проверьте корректность данных.")
|
error_label = QLabel("Произошла ошибка при формировании графиков. Проверьте корректность данных.")
|
||||||
widgets_datapack = [error_label]
|
widgets_datapack = [error_label]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Непредвиденная ошибка при формировании графиков: {e}")
|
logger.error(f"Непредвиденная ошибка при формировании графиков: {e}")
|
||||||
@ -64,18 +82,24 @@ class PlotWidget(BasePlotWidget):
|
|||||||
finally:
|
finally:
|
||||||
self._mediator.notify(self, widgets_datapack)
|
self._mediator.notify(self, widgets_datapack)
|
||||||
|
|
||||||
def build_raw_trace(self, data:list[pd.DataFrame, dict]) -> None:
|
def build_raw_trace(self, data: Tuple[pd.DataFrame, dict]) -> None:
|
||||||
"""
|
"""
|
||||||
Создаёт один виджет с одним графиком, где представлены все данные
|
Создаёт виджет с одним графиком, отображающим все данные сырой трассировки.
|
||||||
|
|
||||||
|
:param data: Кортеж из DataFrame с трассировочными данными и словаря событий.
|
||||||
"""
|
"""
|
||||||
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
||||||
plot = self._build_raw_plotitem(data, pyqt_container)
|
plot = self._build_raw_plotitem(data, pyqt_container)
|
||||||
container_layout.addWidget(plot)
|
container_layout.addWidget(plot)
|
||||||
container_widget.setProperty("pyqt_container", pyqt_container)
|
container_widget.setProperty("pyqt_container", pyqt_container)
|
||||||
|
|
||||||
self._mediator.notify(self, [container_widget])
|
self._mediator.notify(self, [container_widget])
|
||||||
|
|
||||||
def set_mode(self, mode: int) -> None:
|
def set_mode(self, mode: int) -> None:
|
||||||
|
"""
|
||||||
|
Устанавливает режим построения графиков, выбирая соответствующий набор каналов.
|
||||||
|
|
||||||
|
:param mode: Режим работы (1 – Online Path Scanner, 2 – Report Editor, 3 – Client Trace Watcher).
|
||||||
|
"""
|
||||||
match mode:
|
match mode:
|
||||||
case 1:
|
case 1:
|
||||||
self._plt_channels = self._plt_structures["Online Path Scanner"]
|
self._plt_channels = self._plt_structures["Online Path Scanner"]
|
||||||
@ -84,20 +108,26 @@ class PlotWidget(BasePlotWidget):
|
|||||||
case 3:
|
case 3:
|
||||||
self._plt_channels = self._plt_structures["Client Trace Watcher"]
|
self._plt_channels = self._plt_structures["Client Trace Watcher"]
|
||||||
|
|
||||||
def _build_raw_plotitem(self,
|
def _build_raw_plotitem(self, data: Tuple[pd.DataFrame, dict],
|
||||||
data:list[pd.DataFrame, dict],
|
|
||||||
pyqt_container: PlotItems) -> pg.GraphicsLayoutWidget:
|
pyqt_container: PlotItems) -> pg.GraphicsLayoutWidget:
|
||||||
|
"""
|
||||||
|
Строит график необработанных трейсов клиента. Добавляет линии для каждого канала и области для событий.
|
||||||
|
|
||||||
|
:param data: Кортеж (dataframe, events)
|
||||||
|
:param pyqt_container: Контейнер для хранения ссылок на QT-объекты графиков.
|
||||||
|
:return: Виджет GraphicsLayoutWidget с построенным графиком.
|
||||||
|
"""
|
||||||
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
|
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
|
||||||
dataframe, events = data
|
dataframe, events = data
|
||||||
channels = dataframe.columns.to_list()
|
channels = dataframe.columns.tolist()
|
||||||
|
# Добавляем линии для каждого канала
|
||||||
for i, channel in enumerate(channels):
|
for i, channel in enumerate(channels):
|
||||||
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen=qts.colors[i], fast=True)
|
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen=qts.colors[i], fast=True)
|
||||||
legend.addItem(plot, channel)
|
legend.addItem(plot, channel)
|
||||||
pyqt_container.curves["real"].setdefault(channel, {})
|
pyqt_container.curves.setdefault("real", {})[channel] = plot
|
||||||
pyqt_container.curves["real"][channel] = plot
|
|
||||||
|
|
||||||
for i in range(len(events["Squeeze"][0])):
|
# Для каждого события этапа Squeeze добавляем регионы
|
||||||
|
for i in range(len(events.get("Squeeze", [[], []])[0])):
|
||||||
point_events = {}
|
point_events = {}
|
||||||
for key, item in events.items():
|
for key, item in events.items():
|
||||||
point_events[key] = [item[0][i], item[1][i]]
|
point_events[key] = [item[0][i], item[1][i]]
|
||||||
@ -105,27 +135,33 @@ class PlotWidget(BasePlotWidget):
|
|||||||
|
|
||||||
plot_layout = pg.GraphicsLayoutWidget()
|
plot_layout = pg.GraphicsLayoutWidget()
|
||||||
plot_layout.addItem(plot_item)
|
plot_layout.addItem(plot_item)
|
||||||
|
|
||||||
return plot_layout
|
return plot_layout
|
||||||
|
|
||||||
def _build_performance_label(self,
|
def _build_performance_label(self, timings: ChannelTimings, qt_items: Dict) -> QWidget:
|
||||||
timings: ChannelTimings,
|
|
||||||
qt_items: dict) -> QWidget:
|
|
||||||
"""
|
"""
|
||||||
Добавляет QLabel с информацией о производительности.
|
Создает QLabel с информацией о производительности (сокращение длительности, идеальное значение, КДИП).
|
||||||
|
|
||||||
|
:param timings: Объект ChannelTimings с рассчитанными временными характеристиками.
|
||||||
|
:param qt_items: Словарь для сохранения ссылок на созданные виджеты.
|
||||||
|
:return: Виджет с меткой производительности.
|
||||||
"""
|
"""
|
||||||
tesla_TWC = round((1 - timings.TWC_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
|
tesla_TWC = round((1 - timings.TWC_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
|
||||||
tesla_ideal = round((1 - timings.ideal_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
|
tesla_ideal = round((1 - timings.ideal_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
|
||||||
TWC_ideal = round((timings.ideal_time / timings.TWC_time) * 100, 2) if timings.TWC_time else 0.0
|
TWC_ideal = round((timings.ideal_time / timings.TWC_time) * 100, 2) if timings.TWC_time else 0.0
|
||||||
|
|
||||||
label_widget = QWidget()
|
label_widget = QWidget()
|
||||||
label_layout = QHBoxLayout(label_widget)
|
label_layout = QHBoxLayout(label_widget)
|
||||||
start_label = QLabel("Сокращение длительности: ")
|
start_label = QLabel("Сокращение длительности: ")
|
||||||
real_label = QLabel(f"фактическое = {tesla_TWC} % ")
|
real_label = QLabel(f"фактическое = {tesla_TWC} % ")
|
||||||
if not tesla_TWC or not timings.TWC_time: real_label.setVisible(False)
|
if not tesla_TWC or not timings.TWC_time:
|
||||||
|
real_label.setVisible(False)
|
||||||
ideal_label = QLabel(f"идеальное = {tesla_ideal} % ")
|
ideal_label = QLabel(f"идеальное = {tesla_ideal} % ")
|
||||||
if not tesla_ideal: ideal_label.setVisible(False)
|
if not tesla_ideal:
|
||||||
|
ideal_label.setVisible(False)
|
||||||
kdip_label = QLabel(f"КДИП = {TWC_ideal}% ")
|
kdip_label = QLabel(f"КДИП = {TWC_ideal}% ")
|
||||||
if not TWC_ideal: kdip_label.setVisible(False)
|
if not TWC_ideal:
|
||||||
|
kdip_label.setVisible(False)
|
||||||
|
|
||||||
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
|
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
|
||||||
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
|
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
|
||||||
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
|
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
|
||||||
@ -143,80 +179,114 @@ class PlotWidget(BasePlotWidget):
|
|||||||
def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget:
|
def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget:
|
||||||
"""
|
"""
|
||||||
Собирает графический виджет для одного набора данных.
|
Собирает графический виджет для одного набора данных.
|
||||||
|
|
||||||
|
Генерирует контейнер, создаёт CustomPlotLayout для построения графика,
|
||||||
|
добавляет, при необходимости, метку производительности.
|
||||||
|
|
||||||
|
:param graphic_passport: Объект GraphicPassport с данными для построения графика.
|
||||||
|
:return: Готовый QWidget для отображения.
|
||||||
"""
|
"""
|
||||||
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
container_widget, container_layout, pyqt_container = self._generate_widget_container()
|
||||||
plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self)
|
plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self)
|
||||||
plot_layout.build(pyqt_container, self._plt_channels)
|
plot_layout.build(pyqt_container, self._plt_channels)
|
||||||
|
|
||||||
if plot_layout.property("performance"):
|
if plot_layout.property("performance"):
|
||||||
perf_widget = self._build_performance_label(
|
perf_widget = self._build_performance_label(plot_layout.property("performance"), pyqt_container.qt_items)
|
||||||
plot_layout.property("performance"),
|
|
||||||
pyqt_container.qt_items
|
|
||||||
)
|
|
||||||
container_layout.addWidget(perf_widget)
|
container_layout.addWidget(perf_widget)
|
||||||
|
|
||||||
container_layout.addWidget(plot_layout)
|
container_layout.addWidget(plot_layout)
|
||||||
container_widget.setProperty("pyqt_container", pyqt_container)
|
container_widget.setProperty("pyqt_container", pyqt_container)
|
||||||
return container_widget
|
return container_widget
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]:
|
def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]:
|
||||||
|
"""
|
||||||
|
Создает контейнер для виджета графика, его макет и объект PlotItems для хранения ссылок.
|
||||||
|
|
||||||
|
:return: Кортеж (контейнер, вертикальный макет, PlotItems).
|
||||||
|
"""
|
||||||
container_widget = QWidget()
|
container_widget = QWidget()
|
||||||
container_layout = QVBoxLayout(container_widget)
|
container_layout = QVBoxLayout(container_widget)
|
||||||
pyqt_container = PlotItems({"real": {}, "ideal": {}}, {"real": {}, "ideal": {}}, {})
|
pyqt_container = PlotItems({"real": {}, "ideal": {}}, {"real": {}, "ideal": {}}, {})
|
||||||
return (container_widget, container_layout, pyqt_container)
|
return container_widget, container_layout, pyqt_container
|
||||||
|
|
||||||
def _update_status(self, widgsteps:int, pointsteps:int, cur_widg:int, cur_point:int):
|
def _update_status(self, widget_steps: int, point_steps: int, cur_widget: int, cur_point: int) -> None:
|
||||||
|
"""
|
||||||
|
Вычисляет текущее значение прогресса и обновляет его через контроллер.
|
||||||
|
|
||||||
|
:param widget_steps: Общее число шагов по виджетам.
|
||||||
|
:param point_steps: Общее число точек в виджете.
|
||||||
|
:param cur_widget: Текущий номер виджета.
|
||||||
|
:param cur_point: Текущий номер точки внутри виджета.
|
||||||
|
"""
|
||||||
if self._datalen:
|
if self._datalen:
|
||||||
sycle_start = self._datastep/self._datalen*100 + 1
|
cycle_progress = self._datastep / self._datalen * 100 + 1
|
||||||
period1 = 99 / self._datalen
|
period1 = 99 / self._datalen
|
||||||
else:
|
else:
|
||||||
sycle_start = 1
|
cycle_progress = 1
|
||||||
period1 = 100
|
period1 = 100
|
||||||
|
|
||||||
period2 = period1/widgsteps if widgsteps != 0 else period1
|
period2 = period1 / widget_steps if widget_steps != 0 else period1
|
||||||
period3 = period2/pointsteps if pointsteps != 0 else period2
|
period3 = period2 / point_steps if point_steps != 0 else period2
|
||||||
|
|
||||||
progress = int(sycle_start + period2*cur_widg + period3*cur_point)
|
progress = int(cycle_progress + period2 * cur_widget + period3 * cur_point)
|
||||||
self._controller.update_progress(progress)
|
self._controller.update_progress(progress)
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Класс CustomPlotLayout – кастомное размещение графических элементов
|
||||||
|
# =============================================================================
|
||||||
class CustomPlotLayout(pg.GraphicsLayoutWidget):
|
class CustomPlotLayout(pg.GraphicsLayoutWidget):
|
||||||
def __init__(self,
|
"""
|
||||||
graphic_passport: GraphicPassport,
|
Кастомный виджет для размещения графиков, построенных с помощью PlotItemGenerator.
|
||||||
widget_steps: int,
|
|
||||||
colors: dict,
|
Он формирует графики по каналам, связывает их между собой (XLink)
|
||||||
parent: PlotWidget = None) -> None:
|
и добавляет навигатор для общего обзора.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
|
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
|
||||||
self.setProperty("performance", None)
|
self.setProperty("performance", None)
|
||||||
|
|
||||||
def build(self, pyqt_container:PlotItems, plt_channels:dict) -> None:
|
def build(self, pyqt_container: PlotItems, plt_channels: Dict) -> None:
|
||||||
|
"""
|
||||||
|
Строит графики для каждого канала на основе параметров из plt_channels.
|
||||||
|
Затем добавляет навигатор (NavigatorPlot) для синхронизации обзора.
|
||||||
|
|
||||||
|
:param pyqt_container: Контейнер для хранения ссылок на графические объекты.
|
||||||
|
:param plt_channels: Словарь с описаниями каналов и их настройками.
|
||||||
|
"""
|
||||||
|
main_plot = None
|
||||||
for widget_num, (channel, description) in enumerate(plt_channels.items()):
|
for widget_num, (channel, description) in enumerate(plt_channels.items()):
|
||||||
plot_item, plot_timings = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container)
|
plot_item, plot_timings = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container)
|
||||||
if widget_num == 0:
|
if widget_num == 0:
|
||||||
main_plot = plot_item
|
main_plot = plot_item
|
||||||
else:
|
else:
|
||||||
plot_item.setXLink(main_plot)
|
plot_item.setXLink(main_plot)
|
||||||
|
|
||||||
if description["Settings"].get("performance", False):
|
if description["Settings"].get("performance", False):
|
||||||
self.setProperty("performance", plot_timings)
|
self.setProperty("performance", plot_timings)
|
||||||
|
|
||||||
self.addItem(plot_item, widget_num, 0)
|
self.addItem(plot_item, widget_num, 0)
|
||||||
|
# Если задана производительность, получаем объект ChannelTimings
|
||||||
timings = ChannelTimings()
|
timings = ChannelTimings()
|
||||||
if self.property('performance'):timings:ChannelTimings = self.property('performance')
|
if self.property('performance'):
|
||||||
|
timings = self.property('performance')
|
||||||
navigator = NavigatorPlot(timings.worst_timeframe, main_plot)
|
navigator = NavigatorPlot(timings.worst_timeframe, main_plot)
|
||||||
if navigator is not None:
|
if navigator is not None:
|
||||||
self.addItem(navigator, widget_num + 1, 0)
|
self.addItem(navigator, widget_num + 1, 0)
|
||||||
|
|
||||||
class PlotItemGenerator:
|
|
||||||
|
|
||||||
def __init__(self,
|
# =============================================================================
|
||||||
graphic_passport: GraphicPassport,
|
# Класс PlotItemGenerator – генерация элементов графика и добавление регионов
|
||||||
widget_steps: int,
|
# =============================================================================
|
||||||
colors: dict, parent:PlotWidget = None) -> None:
|
class PlotItemGenerator:
|
||||||
|
"""
|
||||||
|
Генератор графических элементов (PlotItem) для построения графиков.
|
||||||
|
|
||||||
|
На основе графического паспорта создаются линии, регионы для этапов, добавляются
|
||||||
|
идеальные сигналы и рассчитываются показатели производительности.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
|
||||||
self._stage_colors = colors
|
self._stage_colors = colors
|
||||||
self._ideal_mode = graphic_passport.dataframe is None
|
self._ideal_mode = graphic_passport.dataframe is None
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
@ -228,46 +298,51 @@ class PlotItemGenerator:
|
|||||||
dataframe_headers = []
|
dataframe_headers = []
|
||||||
point_steps = 1
|
point_steps = 1
|
||||||
|
|
||||||
self._datapack = [
|
# Используем словарь для более понятной организации данных
|
||||||
graphic_passport.dataframe,
|
self._datapack: Dict[str, Any] = {
|
||||||
dataframe_headers,
|
"dataframe": graphic_passport.dataframe,
|
||||||
graphic_passport.useful_data,
|
"headers": dataframe_headers,
|
||||||
graphic_passport.points_pocket,
|
"useful_data": graphic_passport.useful_data,
|
||||||
widget_steps,
|
"points_pocket": graphic_passport.points_pocket,
|
||||||
point_steps
|
"widget_steps": widget_steps,
|
||||||
]
|
"point_steps": point_steps
|
||||||
|
}
|
||||||
|
|
||||||
def generate_plot_item(self,
|
def generate_plot_item(self, widget_num: int, channel: str, description: Dict[str, Any],
|
||||||
widget_num: int,
|
|
||||||
channel: str,
|
|
||||||
description: dict[str, Any],
|
|
||||||
pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]:
|
pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]:
|
||||||
|
"""
|
||||||
|
Генерирует PlotItem для заданного канала с добавлением регионов, компенсаций и идеальных данных.
|
||||||
|
|
||||||
dataframe, dataframe_headers, useful_data, points_pocket, widget_steps, point_steps = self._datapack
|
:param widget_num: Номер текущего виджета.
|
||||||
|
:param channel: Имя канала.
|
||||||
|
:param description: Словарь настроек для данного канала.
|
||||||
|
:param pyqt_container: Контейнер для хранения ссылок на объекты графиков.
|
||||||
|
:return: Кортеж из созданного PlotItem и объекта ChannelTimings с рассчитанными параметрами.
|
||||||
|
"""
|
||||||
|
dp = self._datapack
|
||||||
|
dataframe = dp["dataframe"]
|
||||||
|
useful_data = dp["useful_data"]
|
||||||
|
points_pocket = dp["points_pocket"]
|
||||||
|
widget_steps = dp["widget_steps"]
|
||||||
|
point_steps = dp["point_steps"]
|
||||||
|
|
||||||
|
# Инициализируем PlotItem и легенду
|
||||||
plot_item, legend = self._init_plot_item(title=channel)
|
plot_item, legend = self._init_plot_item(title=channel)
|
||||||
settings:dict = description["Settings"]
|
settings: Dict = description["Settings"]
|
||||||
timings = ChannelTimings()
|
timings = ChannelTimings()
|
||||||
timings.client_time = useful_data.client_time
|
timings.client_time = useful_data.client_time
|
||||||
|
|
||||||
ideal_df = pd.DataFrame({})
|
ideal_df = pd.DataFrame({})
|
||||||
|
|
||||||
# TODO: рассчитать корректный параметр range
|
# При необходимости – зеркальное отражение данных для ME
|
||||||
if settings.get("mirror ME", False) and not self._ideal_mode:
|
if settings.get("mirror ME", False) and not self._ideal_mode:
|
||||||
dataframe = self._shift_data(
|
dataframe = self._shift_data("ME", description["Real_signals"], dataframe,
|
||||||
"ME",
|
lambda x: useful_data.range_ME - x)
|
||||||
description["Real_signals"],
|
|
||||||
dataframe,
|
|
||||||
lambda x: useful_data.range_ME-x
|
|
||||||
)
|
|
||||||
|
|
||||||
# Итерация по точкам
|
# Итерация по точкам паспорта
|
||||||
for cur_point, data in enumerate(points_pocket):
|
for cur_point, point_data in enumerate(points_pocket):
|
||||||
point_data: PointPassport = data
|
|
||||||
ideal_data = copy.deepcopy(point_data.ideal_data)
|
ideal_data = copy.deepcopy(point_data.ideal_data)
|
||||||
is_last = (cur_point == len(points_pocket) - 1)
|
is_last = (cur_point == len(points_pocket) - 1)
|
||||||
is_first = (cur_point == 0)
|
is_first = (cur_point == 0)
|
||||||
|
|
||||||
if self._ideal_mode:
|
if self._ideal_mode:
|
||||||
timings, point_data.events, point_data.timeframe = self._generate_synthetic_events(timings, ideal_data)
|
timings, point_data.events, point_data.timeframe = self._generate_synthetic_events(timings, ideal_data)
|
||||||
else:
|
else:
|
||||||
@ -276,43 +351,45 @@ class PlotItemGenerator:
|
|||||||
k_hardness = useful_data.k_hardness
|
k_hardness = useful_data.k_hardness
|
||||||
signals = description["Real_signals"]
|
signals = description["Real_signals"]
|
||||||
dataframe = self._apply_force_compensation(force, k_hardness, dataframe, point_data.timeframe, signals)
|
dataframe = self._apply_force_compensation(force, k_hardness, dataframe, point_data.timeframe, signals)
|
||||||
|
|
||||||
if settings.get("stages", False):
|
if settings.get("stages", False):
|
||||||
self._add_stage_regions(self._stage_colors, plot_item, point_data.events, pyqt_container.regions, 75)
|
self._add_stage_regions(self._stage_colors, plot_item, point_data.events, pyqt_container.regions, 75)
|
||||||
|
|
||||||
if settings.get("force accuracy", False):
|
if settings.get("force accuracy", False):
|
||||||
force = point_data.useful_data["force"]
|
force = point_data.useful_data["force"]
|
||||||
self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item)
|
self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item)
|
||||||
|
|
||||||
if settings.get("ideals", False) and settings.get("mirror ME", False):
|
if settings.get("ideals", False) and settings.get("mirror ME", False):
|
||||||
for stage in point_data.events.keys():
|
for stage in point_data.events.keys():
|
||||||
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage], lambda x: useful_data.range_ME-x)
|
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage],
|
||||||
|
lambda x: useful_data.range_ME - x)
|
||||||
if settings.get("workpiece", False):
|
if settings.get("workpiece", False):
|
||||||
self._add_workpiece(point_data, plot_item)
|
self._add_workpiece(point_data, plot_item)
|
||||||
|
|
||||||
if settings.get("ideals", False):
|
if settings.get("ideals", False):
|
||||||
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events, pyqt_container.regions, 100)
|
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events,
|
||||||
|
pyqt_container.regions, 100)
|
||||||
ideal_df = self._modify_ideal_df(ideal_df, ideal_data, point_data.events)
|
ideal_df = self._modify_ideal_df(ideal_df, ideal_data, point_data.events)
|
||||||
|
|
||||||
if settings.get("performance", False):
|
if settings.get("performance", False):
|
||||||
timings = self._calc_performance(timings, point_data, ideal_data, is_first, is_last)
|
timings = self._calc_performance(timings, point_data, ideal_data, is_first, is_last)
|
||||||
|
# Обновляем статус через родительский PlotWidget
|
||||||
self._parent._update_status(widget_steps, point_steps, widget_num, cur_point)
|
self._parent._update_status(widget_steps, point_steps, widget_num, cur_point)
|
||||||
|
|
||||||
# Добавляем реальные сигналы
|
# Добавляем идеальные сигналы (если указано в настройках)
|
||||||
if settings.get("ideals", False):
|
if settings.get("ideals", False):
|
||||||
self._add_signals(plot_item, ideal_df, description["Ideal_signals"], legend, pyqt_container.curves["ideal"])
|
self._add_signals(plot_item, ideal_df, description["Ideal_signals"], legend, pyqt_container.curves["ideal"])
|
||||||
|
# Добавляем реальные сигналы, если не включен режим идеала
|
||||||
if not self._ideal_mode:
|
if not self._ideal_mode:
|
||||||
self._add_signals(plot_item, dataframe, description["Real_signals"], legend, pyqt_container.curves["real"])
|
self._add_signals(plot_item, dataframe, description["Real_signals"], legend, pyqt_container.curves["real"])
|
||||||
return (plot_item, timings)
|
return plot_item, timings
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _shift_data(valid_str: str,
|
def _shift_data(valid_str: str, signals: List[Dict], dataframe: pd.DataFrame, func: Callable) -> pd.DataFrame:
|
||||||
signals: list[dict],
|
"""
|
||||||
dataframe: pd.DataFrame,
|
Применяет заданную функцию (например, смещение) к столбцам, удовлетворяющим условию.
|
||||||
func: Callable) -> pd.DataFrame:
|
|
||||||
|
:param valid_str: Подстрока, по которой определяется, к каким сигналам применять функцию.
|
||||||
|
:param signals: Список описаний сигналов.
|
||||||
|
:param dataframe: DataFrame с данными.
|
||||||
|
:param func: Функция, применяемая к значениям.
|
||||||
|
:return: Измененный DataFrame.
|
||||||
|
"""
|
||||||
keys = dataframe.keys()
|
keys = dataframe.keys()
|
||||||
for signal in signals:
|
for signal in signals:
|
||||||
if valid_str in signal["name"] and signal["name"] in keys:
|
if valid_str in signal["name"] and signal["name"] in keys:
|
||||||
@ -320,60 +397,73 @@ class PlotItemGenerator:
|
|||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _init_plot_item(title: str) -> tuple[pg.PlotItem, pg.LegendItem]:
|
def _init_plot_item(title: str) -> Tuple[pg.PlotItem, pg.LegendItem]:
|
||||||
|
"""
|
||||||
|
Инициализирует PlotItem с заданным заголовком, включает сетку и легенду.
|
||||||
|
|
||||||
|
:param title: Заголовок графика.
|
||||||
|
:return: Кортеж (PlotItem, LegendItem).
|
||||||
|
"""
|
||||||
plot_item = pg.PlotItem(title=title)
|
plot_item = pg.PlotItem(title=title)
|
||||||
|
# Подписываемся на изменение диапазона оси X для автоматической даунсэмплинга
|
||||||
plot_item.sigXRangeChanged.connect(lambda: PlotItemGenerator._update_plots_downsample(plot_item))
|
plot_item.sigXRangeChanged.connect(lambda: PlotItemGenerator._update_plots_downsample(plot_item))
|
||||||
# Оптимизация отображения графиков
|
|
||||||
plot_item.showGrid(x=True, y=True)
|
plot_item.showGrid(x=True, y=True)
|
||||||
plot_item.setClipToView(True)
|
plot_item.setClipToView(True)
|
||||||
legend = plot_item.addLegend(offset=(70, 20))
|
legend = plot_item.addLegend(offset=(70, 20))
|
||||||
return plot_item, legend
|
return plot_item, legend
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_stage_region(colors:dict,
|
def _create_stage_region(colors: Dict, stage: str, start_timestamp: float, finish_timestamp: float,
|
||||||
stage: str,
|
|
||||||
start_timestamp: float,
|
|
||||||
finish_timestamp: float,
|
|
||||||
transparency: int) -> Optional[pg.LinearRegionItem]:
|
transparency: int) -> Optional[pg.LinearRegionItem]:
|
||||||
"""
|
"""
|
||||||
Создает регион для определённого этапа, если заданы временные рамки.
|
Создает регион (LinearRegionItem) для заданного этапа, если заданы границы.
|
||||||
|
|
||||||
|
:param colors: Словарь с цветами для этапов.
|
||||||
|
:param stage: Имя этапа.
|
||||||
|
:param start_timestamp: Время начала этапа.
|
||||||
|
:param finish_timestamp: Время окончания этапа.
|
||||||
|
:param transparency: Значение прозрачности (alpha).
|
||||||
|
:return: Объект LinearRegionItem или None.
|
||||||
"""
|
"""
|
||||||
if start_timestamp is not None and finish_timestamp is not None:
|
if start_timestamp is not None and finish_timestamp is not None:
|
||||||
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
|
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
|
||||||
color = colors.get(stage, [100, 100, 100, 100])
|
color = colors.get(stage, [100, 100, 100, 100])
|
||||||
|
# Устанавливаем кисть с заданной прозрачностью
|
||||||
region.setBrush(pg.mkBrush(color[:3] + [transparency]))
|
region.setBrush(pg.mkBrush(color[:3] + [transparency]))
|
||||||
return region
|
return region
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_stage_regions(colors:dict,
|
def _add_stage_regions(colors: Dict, plot_item: pg.PlotItem, point_events: Dict, reg_items: Dict, transparency: int = 75) -> None:
|
||||||
plot_item: pg.PlotItem,
|
|
||||||
point_events: dict,
|
|
||||||
reg_items: dict,
|
|
||||||
transparency: int = 75) -> None:
|
|
||||||
"""
|
"""
|
||||||
Добавляет регионы для реальных этапов
|
Добавляет регионы для реальных этапов на PlotItem.
|
||||||
|
|
||||||
|
:param colors: Словарь с цветами для этапов.
|
||||||
|
:param plot_item: PlotItem, на который добавляются регионы.
|
||||||
|
:param point_events: Словарь событий с временными рамками.
|
||||||
|
:param reg_items: Словарь для хранения созданных регионов.
|
||||||
|
:param transparency: Прозрачность регионов.
|
||||||
"""
|
"""
|
||||||
stages = point_events.keys()
|
for stage, times in point_events.items():
|
||||||
#if all(stage in dataframe_headers for stage in stages):
|
start_t, end_t = times
|
||||||
for stage in stages:
|
|
||||||
start_t, end_t = point_events[stage]
|
|
||||||
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
|
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
|
||||||
if region is not None:
|
if region is not None:
|
||||||
region.setZValue(-20)
|
region.setZValue(-20)
|
||||||
plot_item.addItem(region)
|
plot_item.addItem(region)
|
||||||
reg_items["real"].setdefault(stage, [])
|
reg_items.setdefault("real", {}).setdefault(stage, []).append(region)
|
||||||
reg_items["real"][stage].append(region)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_ideal_stage_regions(colors: dict,
|
def _add_ideal_stage_regions(colors: Dict, plot_item: pg.PlotItem, ideal_data: Dict[str, Any],
|
||||||
plot_item: pg.PlotItem,
|
point_events: Dict[str, List[float]], reg_items: Dict, transparency: int = 125) -> None:
|
||||||
ideal_data: dict[str, Any],
|
|
||||||
point_events: dict[str, list[float]],
|
|
||||||
reg_items: dict,
|
|
||||||
transparency: int = 125) -> None:
|
|
||||||
"""
|
"""
|
||||||
Добавляет регионы для идеальных этапов.
|
Добавляет регионы для идеальных этапов.
|
||||||
|
|
||||||
|
:param colors: Словарь с цветами.
|
||||||
|
:param plot_item: PlotItem для добавления регионов.
|
||||||
|
:param ideal_data: Словарь с идеальными данными, включая "Ideal timings".
|
||||||
|
:param point_events: События с временными рамками.
|
||||||
|
:param reg_items: Словарь для хранения созданных регионов.
|
||||||
|
:param transparency: Прозрачность регионов.
|
||||||
"""
|
"""
|
||||||
ideal_timings = ideal_data["Ideal timings"]
|
ideal_timings = ideal_data["Ideal timings"]
|
||||||
stages = list(point_events.keys())
|
stages = list(point_events.keys())
|
||||||
@ -384,15 +474,19 @@ class PlotItemGenerator:
|
|||||||
if region:
|
if region:
|
||||||
region.setZValue(-10)
|
region.setZValue(-10)
|
||||||
plot_item.addItem(region)
|
plot_item.addItem(region)
|
||||||
reg_items["ideal"].setdefault(stage, [])
|
reg_items.setdefault("ideal", {}).setdefault(stage, []).append(region)
|
||||||
reg_items["ideal"][stage].append(region)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _modify_ideal_df(ideal_df: pd.DataFrame,
|
def _modify_ideal_df(ideal_df: pd.DataFrame, ideal_data: Dict[str, Any], point_events: Dict[str, List[float]]) -> pd.DataFrame:
|
||||||
ideal_data: dict[str, Any],
|
|
||||||
point_events: dict[str, list[float]]) -> None:
|
|
||||||
"""
|
"""
|
||||||
Добавляет идеальные сигналы для каждого этапа.
|
Добавляет идеальные сигналы для каждого этапа к DataFrame.
|
||||||
|
|
||||||
|
Если ideal_df не пуст, добавляет разделитель между сигналами.
|
||||||
|
|
||||||
|
:param ideal_df: DataFrame с идеальными данными.
|
||||||
|
:param ideal_data: Словарь с идеальными данными для этапов.
|
||||||
|
:param point_events: События с временными рамками.
|
||||||
|
:return: Обновленный DataFrame с идеальными сигналами.
|
||||||
"""
|
"""
|
||||||
for stage in point_events.keys():
|
for stage in point_events.keys():
|
||||||
if not ideal_df.empty:
|
if not ideal_df.empty:
|
||||||
@ -407,13 +501,16 @@ class PlotItemGenerator:
|
|||||||
return ideal_df
|
return ideal_df
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_signals(plot_item: pg.PlotItem,
|
def _add_signals(plot_item: pg.PlotItem, dataframe: pd.DataFrame, real_signals: List[Dict[str, Any]],
|
||||||
dataframe: pd.DataFrame,
|
legend: pg.LegendItem, curve_items: Dict) -> None:
|
||||||
real_signals: list[dict[str, Any]],
|
|
||||||
legend: pg.LegendItem,
|
|
||||||
curve_items: dict) -> None:
|
|
||||||
"""
|
"""
|
||||||
Добавляет реальные сигналы из dataframe на виджет.
|
Добавляет сигналы из DataFrame на PlotItem.
|
||||||
|
|
||||||
|
:param plot_item: PlotItem для добавления сигналов.
|
||||||
|
:param dataframe: DataFrame с данными.
|
||||||
|
:param real_signals: Список описаний сигналов, содержащих имя, ручку (pen) и т.д.
|
||||||
|
:param legend: Легенда для PlotItem.
|
||||||
|
:param curve_items: Словарь для хранения созданных объектов графиков.
|
||||||
"""
|
"""
|
||||||
dataframe_headers = dataframe.columns.tolist()
|
dataframe_headers = dataframe.columns.tolist()
|
||||||
for signal in real_signals:
|
for signal in real_signals:
|
||||||
@ -421,22 +518,36 @@ class PlotItemGenerator:
|
|||||||
plot = plot_item.plot(dataframe["time"], dataframe[signal["name"]], pen=signal["pen"], fast=True)
|
plot = plot_item.plot(dataframe["time"], dataframe[signal["name"]], pen=signal["pen"], fast=True)
|
||||||
plot.setZValue(0)
|
plot.setZValue(0)
|
||||||
legend.addItem(plot, signal["name"])
|
legend.addItem(plot, signal["name"])
|
||||||
curve_items.setdefault(signal["name"], {})
|
curve_items.setdefault(signal["name"], {}) # гарантируем, что ключ существует
|
||||||
curve_items[signal["name"]] = plot
|
curve_items[signal["name"]] = plot
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _update_plots_downsample(plot_item: pg.PlotItem):
|
def _update_plots_downsample(plot_item: pg.PlotItem):
|
||||||
|
"""
|
||||||
|
Настраивает даунсэмплинг данных на PlotItem в зависимости от видимого диапазона по оси X.
|
||||||
|
|
||||||
|
:param plot_item: PlotItem, для которого устанавливается даунсэмплинг.
|
||||||
|
"""
|
||||||
visible_range = plot_item.getViewBox().viewRange()[0]
|
visible_range = plot_item.getViewBox().viewRange()[0]
|
||||||
diapason = visible_range[1] - visible_range[0]
|
diapason = visible_range[1] - visible_range[0]
|
||||||
if diapason >= 30: plot_item.setDownsampling(ds=50, auto=True, mode='peak')
|
if diapason >= 30:
|
||||||
elif diapason >= 10: plot_item.setDownsampling(ds=20, auto=True, mode='peak')
|
plot_item.setDownsampling(ds=50, auto=True, mode='peak')
|
||||||
elif diapason >=4: plot_item.setDownsampling(ds=10, auto=True, mode='peak')
|
elif diapason >= 10:
|
||||||
else: plot_item.setDownsampling(ds=1, auto=True, mode='peak')
|
plot_item.setDownsampling(ds=20, auto=True, mode='peak')
|
||||||
|
elif diapason >= 4:
|
||||||
|
plot_item.setDownsampling(ds=10, auto=True, mode='peak')
|
||||||
|
else:
|
||||||
|
plot_item.setDownsampling(ds=1, auto=True, mode='peak')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_force_accuracy_region(event:list,
|
def _add_force_accuracy_region(event: List[float], force: float, plot_item: pg.PlotItem) -> None:
|
||||||
force: float,
|
"""
|
||||||
plot_item:pg.PlotItem) -> None:
|
Добавляет прямоугольную область для отображения точности силы на PlotItem.
|
||||||
|
|
||||||
|
:param event: Список [начало, конец] события.
|
||||||
|
:param force: Значение силы.
|
||||||
|
:param plot_item: PlotItem для добавления прямоугольника.
|
||||||
|
"""
|
||||||
modifier = 0.05
|
modifier = 0.05
|
||||||
x1 = event[0]
|
x1 = event[0]
|
||||||
dx = event[1] - x1
|
dx = event[1] - x1
|
||||||
@ -450,8 +561,13 @@ class PlotItemGenerator:
|
|||||||
plot_item.addItem(rect_item)
|
plot_item.addItem(rect_item)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _add_workpiece(point_data:PointPassport,
|
def _add_workpiece(point_data: PointPassport, plot_item: pg.PlotItem) -> None:
|
||||||
plot_item: pg.PlotItem) -> None:
|
"""
|
||||||
|
Добавляет область, обозначающую заготовку, на PlotItem.
|
||||||
|
|
||||||
|
:param point_data: Объект PointPassport с данными точки.
|
||||||
|
:param plot_item: PlotItem для добавления области.
|
||||||
|
"""
|
||||||
x1 = point_data.events["Closing"][0]
|
x1 = point_data.events["Closing"][0]
|
||||||
dx = point_data.events["Relief"][1] - x1
|
dx = point_data.events["Relief"][1] - x1
|
||||||
y1 = point_data.useful_data["part_pos"] * 1000
|
y1 = point_data.useful_data["part_pos"] * 1000
|
||||||
@ -463,18 +579,25 @@ class PlotItemGenerator:
|
|||||||
rect_item.setPen(pg.mkPen('black', width=3))
|
rect_item.setPen(pg.mkPen('black', width=3))
|
||||||
plot_item.addItem(rect_item)
|
plot_item.addItem(rect_item)
|
||||||
|
|
||||||
def _calc_performance(self,
|
@staticmethod
|
||||||
timings:ChannelTimings,
|
def _calc_performance(timings: ChannelTimings, point_data: PointPassport, ideal_data: Dict,
|
||||||
point_data: PointPassport,
|
is_first: bool, is_last: bool) -> ChannelTimings:
|
||||||
ideal_data:dict,
|
"""
|
||||||
is_first:bool,
|
Рассчитывает показатели производительности для текущей точки.
|
||||||
is_last:bool) -> ChannelTimings:
|
|
||||||
|
:param timings: Текущие временные показатели (ChannelTimings).
|
||||||
|
:param point_data: Объект PointPassport для текущей точки.
|
||||||
|
:param ideal_data: Идеальные данные для текущей точки.
|
||||||
|
:param is_first: True, если точка первая.
|
||||||
|
:param is_last: True, если точка последняя.
|
||||||
|
:return: Обновленный объект ChannelTimings.
|
||||||
|
"""
|
||||||
if is_first:
|
if is_first:
|
||||||
if not self._ideal_mode:
|
if not PlotItemGenerator._parent_ideal_mode():
|
||||||
timings.TWC_start = point_data.events["Closing"][0]
|
timings.TWC_start = point_data.events["Closing"][0]
|
||||||
ideal_delta = ideal_data["Ideal cycle"]
|
ideal_delta = ideal_data["Ideal cycle"]
|
||||||
elif is_last:
|
elif is_last:
|
||||||
if not self._ideal_mode:
|
if not PlotItemGenerator._parent_ideal_mode():
|
||||||
timings.TWC_end = point_data.events["Relief"][1]
|
timings.TWC_end = point_data.events["Relief"][1]
|
||||||
timings.TWC_time = timings.TWC_end - timings.TWC_start
|
timings.TWC_time = timings.TWC_end - timings.TWC_start
|
||||||
timings.worst_timeframe = [timings.TWC_start, timings.TWC_end]
|
timings.worst_timeframe = [timings.TWC_start, timings.TWC_end]
|
||||||
@ -483,17 +606,20 @@ class PlotItemGenerator:
|
|||||||
ideal_delta = ideal_data["Ideal cycle"]
|
ideal_delta = ideal_data["Ideal cycle"]
|
||||||
|
|
||||||
timings.ideal_time += ideal_delta
|
timings.ideal_time += ideal_delta
|
||||||
#curr_perf = ideal_delta/TWC_delta if TWC_delta != 0 else 1
|
# При желании можно добавить сравнение с текущей производительностью
|
||||||
|
# Если (ideal_delta / TWC_delta) < timings.worst_performance, обновляем worst_performance и worst_timeframe
|
||||||
if False: #curr_perf < timings.worst_performance:
|
|
||||||
timings.worst_performance = curr_perf
|
|
||||||
timings.worst_timeframe = point_data.timeframe
|
|
||||||
return timings
|
return timings
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _generate_synthetic_events(timings:ChannelTimings,
|
def _generate_synthetic_events(timings: ChannelTimings, ideal_data: Dict) -> Tuple[ChannelTimings, Dict, List[float]]:
|
||||||
ideal_data:dict) -> Tuple[ChannelTimings, dict, list[float]]:
|
"""
|
||||||
timings.worst_timeframe = point_timeframe = [timings.shift, timings.shift+ ideal_data["Ideal cycle"]]
|
Генерирует синтетические события для случая, когда данные отсутствуют.
|
||||||
|
|
||||||
|
:param timings: Объект ChannelTimings.
|
||||||
|
:param ideal_data: Словарь с идеальными данными, содержащий "Ideal cycle" и "Ideal timings".
|
||||||
|
:return: Кортеж (обновленные timings, сгенерированные события, временной интервал точки).
|
||||||
|
"""
|
||||||
|
point_timeframe = [timings.shift, timings.shift + ideal_data["Ideal cycle"]]
|
||||||
point_events = {}
|
point_events = {}
|
||||||
keys = list(ideal_data.keys())
|
keys = list(ideal_data.keys())
|
||||||
shift = 0
|
shift = 0
|
||||||
@ -501,45 +627,64 @@ class PlotItemGenerator:
|
|||||||
point_events[keys[i]] = [timings.shift + shift, timings.shift + time + shift]
|
point_events[keys[i]] = [timings.shift + shift, timings.shift + time + shift]
|
||||||
shift += time
|
shift += time
|
||||||
timings.shift += ideal_data["Ideal cycle"]
|
timings.shift += ideal_data["Ideal cycle"]
|
||||||
return (timings, point_events, point_timeframe)
|
return timings, point_events, point_timeframe
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _apply_force_compensation(force: float,
|
def _apply_force_compensation(force: float, k_hardness: float, dataframe: pd.DataFrame,
|
||||||
k_hardness: float,
|
point_timeframe: List[float], real_signals: List[Dict]) -> pd.DataFrame:
|
||||||
dataframe:pd.DataFrame,
|
"""
|
||||||
point_timeframe:list,
|
Применяет компенсацию силы к данным в заданном интервале времени.
|
||||||
real_signals:list[dict]) -> pd.DataFrame:
|
|
||||||
|
:param force: Значение силы.
|
||||||
|
:param k_hardness: Коэффициент твердости.
|
||||||
|
:param dataframe: DataFrame с данными.
|
||||||
|
:param point_timeframe: Временной интервал, для которого производится компенсация.
|
||||||
|
:param real_signals: Список описаний реальных сигналов.
|
||||||
|
:return: Измененный DataFrame с примененной компенсацией.
|
||||||
|
"""
|
||||||
F_comp = -force / k_hardness
|
F_comp = -force / k_hardness
|
||||||
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
|
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
|
||||||
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs], lambda x: x + F_comp)
|
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs],
|
||||||
|
lambda x: x + F_comp)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parent_ideal_mode() -> bool:
|
||||||
|
"""
|
||||||
|
Вспомогательный метод-заглушка для определения режима идеальных данных.
|
||||||
|
(При необходимости можно реализовать проверку из родительского класса)
|
||||||
|
"""
|
||||||
|
# Здесь можно вернуть значение из родительского контекста, если требуется
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Класс NavigatorPlot – навигатор для графика, позволяющий управлять областью просмотра
|
||||||
|
# =============================================================================
|
||||||
class NavigatorPlot(pg.PlotItem):
|
class NavigatorPlot(pg.PlotItem):
|
||||||
def __init__(self,
|
"""
|
||||||
time_region:tuple[float, float],
|
График-навигатор, отображающий уменьшенную копию данных основного графика,
|
||||||
main_plot: pg.PlotItem):
|
позволяющий синхронизировать область просмотра основного графика с регионом навигатора.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, time_region: Tuple[float, float], main_plot: pg.PlotItem):
|
||||||
|
"""
|
||||||
|
Инициализирует NavigatorPlot, синхронизируя его с основным графиком.
|
||||||
|
|
||||||
|
:param time_region: Временной интервал, который должен быть выделен.
|
||||||
|
:param main_plot: Основной PlotItem, с которым будет синхронизация.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._init_navigator(time_region, main_plot)
|
self._init_navigator(time_region, main_plot)
|
||||||
self._init_syncranisation(main_plot)
|
self._init_syncranisation(main_plot)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _downsample_data(x:list, y:list, max_points=5000):
|
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem, region: pg.LinearRegionItem) -> None:
|
||||||
"""
|
|
||||||
Понижает разрешение данных до заданного количества точек для улучшения производительности навигатора.
|
|
||||||
"""
|
|
||||||
if len(x) > max_points:
|
|
||||||
factor = len(x) // max_points
|
|
||||||
x_downsampled = x[::factor]
|
|
||||||
y_downsampled = y[::factor]
|
|
||||||
return x_downsampled, y_downsampled
|
|
||||||
return x, y
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem,
|
|
||||||
region: pg.LinearRegionItem) -> None:
|
|
||||||
"""
|
"""
|
||||||
Синхронизирует область просмотра основного графика с регионом навигатора.
|
Синхронизирует область просмотра основного графика с регионом навигатора.
|
||||||
|
|
||||||
|
:param main_plot: Основной график.
|
||||||
|
:param region: Регион навигатора.
|
||||||
"""
|
"""
|
||||||
x_min, x_max = region.getRegion()
|
x_min, x_max = region.getRegion()
|
||||||
if main_plot:
|
if main_plot:
|
||||||
@ -550,49 +695,53 @@ class NavigatorPlot(pg.PlotItem):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _sync_navigator_with_main(main_plot: pg.PlotItem, region: pg.LinearRegionItem):
|
def _sync_navigator_with_main(main_plot: pg.PlotItem, region: pg.LinearRegionItem):
|
||||||
"""
|
"""
|
||||||
Синхронизирует регион навигатора с областью просмотра основного графика.
|
Синхронизирует регион навигатора с текущей областью просмотра основного графика.
|
||||||
|
|
||||||
|
:param main_plot: Основной график.
|
||||||
|
:param region: Регион навигатора.
|
||||||
"""
|
"""
|
||||||
if region:
|
if region:
|
||||||
x_min, x_max = main_plot
|
x_min, x_max = main_plot.getViewBox().viewRange()[0]
|
||||||
region.blockSignals(True) # Предотвращаем рекурсию
|
region.blockSignals(True)
|
||||||
region.setRegion([x_min, x_max])
|
region.setRegion([x_min, x_max])
|
||||||
region.blockSignals(False)
|
region.blockSignals(False)
|
||||||
|
|
||||||
def _init_syncranisation(self, main_plot: pg.PlotItem) -> None:
|
def _init_syncranisation(self, main_plot: pg.PlotItem) -> None:
|
||||||
"""
|
"""
|
||||||
Связывает изменения навигатора и других графиков друг с другом
|
Настраивает синхронизацию между навигатором и основным графиком.
|
||||||
"""
|
"""
|
||||||
self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
||||||
self.ROI_region.sigRegionChanged.connect(
|
self.ROI_region.sigRegionChanged.connect(
|
||||||
lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
|
||||||
)
|
)
|
||||||
main_plot.sigXRangeChanged.connect(
|
main_plot.sigXRangeChanged.connect(
|
||||||
lambda _, plot=main_plot, region=self.ROI_region: self._sync_navigator_with_main(main_plot=plot, region=region)
|
lambda: self._sync_navigator_with_main(main_plot, self.ROI_region)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _init_navigator(self,
|
def _init_navigator(self, time_region: Tuple[float, float], main_plot: pg.PlotItem) -> None:
|
||||||
time_region:tuple[float, float],
|
|
||||||
main_plot: pg.PlotItem) -> None:
|
|
||||||
"""
|
"""
|
||||||
Создаёт график-навигатор, отображающий все данные в уменьшенном масштабе.
|
Создает график-навигатор, отображающий уменьшенную копию данных.
|
||||||
|
|
||||||
|
:param time_region: Временной интервал, который необходимо выделить.
|
||||||
|
:param main_plot: Основной график, из которого будут извлекаться данные.
|
||||||
"""
|
"""
|
||||||
self.setTitle("Navigator")
|
self.setTitle("Navigator")
|
||||||
self.setFixedHeight(100)
|
self.setFixedHeight(100)
|
||||||
# Получение кривых из main_plot
|
# Извлекаем данные из каждого элемента основного графика и даунсэмплируем их
|
||||||
for curve in main_plot.listDataItems():
|
for curve in main_plot.listDataItems():
|
||||||
# Извлекаем данные из кривой
|
|
||||||
x, y = curve.getData()
|
x, y = curve.getData()
|
||||||
curve_name = curve.opts.get("name", None)
|
curve_name = curve.opts.get("name", None)
|
||||||
signal_pen = curve.opts.get("pen", None)
|
signal_pen = curve.opts.get("pen", None)
|
||||||
x_downsampled, y_downsampled = self._downsample_data(x, y, max_points=1000)
|
plot = self.plot(x, y, pen=signal_pen, name=curve_name)
|
||||||
self.plot(x_downsampled, y_downsampled, pen=signal_pen, name=curve_name)
|
#plot.setDownsampling(ds = 10, auto = True, method = 'peak')
|
||||||
|
# Создаем регион для выделения области
|
||||||
self.ROI_region = pg.LinearRegionItem(movable=True, brush=pg.mkBrush(0, 0, 255, 100), pen=pg.mkPen(width=4))
|
self.ROI_region = pg.LinearRegionItem(
|
||||||
maxBound = 50
|
movable=True,
|
||||||
if len(x) !=0: maxBound = x[-1]
|
brush=pg.mkBrush(0, 0, 255, 100),
|
||||||
self.ROI_region.setBounds([0, maxBound])
|
pen=pg.mkPen(width=4)
|
||||||
|
)
|
||||||
|
max_bound = x[-1] if x.any() else 50
|
||||||
|
self.ROI_region.setBounds([0, max_bound])
|
||||||
self.ROI_region.setRegion(time_region)
|
self.ROI_region.setRegion(time_region)
|
||||||
self.addItem(self.ROI_region)
|
self.addItem(self.ROI_region)
|
||||||
self.getViewBox().setLimits(xMin=0, xMax=maxBound)
|
self.getViewBox().setLimits(xMin=0, xMax=max_bound)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,32 +11,23 @@ from loguru import logger
|
|||||||
class ReportSettings(QtWidgets.QWidget):
|
class ReportSettings(QtWidgets.QWidget):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
#self._tab_cashe = LRUCache(maxsize=1000)
|
|
||||||
|
|
||||||
def build(self, index, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
def build(self, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
||||||
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
|
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
|
||||||
try:
|
try:
|
||||||
self._clear()
|
self._clear()
|
||||||
param_tree = ParameterTree()
|
param_tree = ParameterTree()
|
||||||
layout = self.layout()
|
layout = self.layout()
|
||||||
layout.addWidget(param_tree)
|
layout.addWidget(param_tree)
|
||||||
"""if index in self._tab_cashe:
|
|
||||||
body = self._tab_cashe[index]
|
|
||||||
else:
|
|
||||||
body= [
|
body= [
|
||||||
self._generate_reg_params(reg_items),
|
ReportSettings._generate_reg_params(reg_items),
|
||||||
self._generate_curve_params(curve_items)
|
ReportSettings._generate_curve_params(curve_items),
|
||||||
]
|
ReportSettings._generate_qt_params(qt_items)
|
||||||
self._tab_cashe[index] = body"""
|
|
||||||
body= [
|
|
||||||
self._generate_reg_params(reg_items),
|
|
||||||
self._generate_curve_params(curve_items),
|
|
||||||
self._generate_qt_params(qt_items)
|
|
||||||
]
|
]
|
||||||
# Добавляем параметры в дерево
|
# Добавляем параметры в дерево
|
||||||
params = Parameter.create(name='params', type='group', children=body)
|
params = Parameter.create(name='params', type='group', children=body)
|
||||||
params.sigTreeStateChanged.connect(
|
params.sigTreeStateChanged.connect(
|
||||||
lambda: self._update_settings(reg_items, curve_items, qt_items, params)
|
lambda: ReportSettings._update_settings(reg_items, curve_items, qt_items, params)
|
||||||
)
|
)
|
||||||
param_tree.setParameters(params, showTop=False)
|
param_tree.setParameters(params, showTop=False)
|
||||||
except:
|
except:
|
||||||
@ -59,69 +50,52 @@ class ReportSettings(QtWidgets.QWidget):
|
|||||||
else:
|
else:
|
||||||
self.setLayout(QtWidgets.QVBoxLayout())
|
self.setLayout(QtWidgets.QVBoxLayout())
|
||||||
|
|
||||||
def _generate_qt_params(self, qt_items: dict) -> dict:
|
@staticmethod
|
||||||
|
def _generate_qt_params(qt_items: dict) -> dict:
|
||||||
"""Создает qt элементы"""
|
"""Создает qt элементы"""
|
||||||
res = {'name': 'Qt elements', 'type': 'group', 'children':[
|
res = {'name': 'Qt elements', 'type': 'group', 'children':[
|
||||||
{'name': key, 'type': 'group', 'children': self._create_qt_samples(item)} for key, item in qt_items.items()
|
{'name': key, 'type': 'group', 'children': ReportSettings._create_qt_samples(item)} for key, item in qt_items.items()
|
||||||
]}
|
]}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _create_qt_samples(self, item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
@staticmethod
|
||||||
|
def _create_qt_samples(item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
||||||
visibility = item.isVisible()
|
visibility = item.isVisible()
|
||||||
return [
|
return [
|
||||||
{'name': 'Visibility', 'type': 'bool', 'value': visibility}
|
{'name': 'Visibility', 'type': 'bool', 'value': visibility}
|
||||||
]
|
]
|
||||||
|
|
||||||
def _generate_reg_params(self,
|
@staticmethod
|
||||||
reg_items: dict) -> dict:
|
def _generate_reg_params(reg_items: dict) -> dict:
|
||||||
|
|
||||||
"""Созадет реальные и идеальные секторы"""
|
"""Созадет реальные и идеальные секторы"""
|
||||||
|
|
||||||
res = {'name': 'Sectors', 'type': 'group', 'children': [
|
res = {'name': 'Sectors', 'type': 'group', 'children': [
|
||||||
{'name': 'Real sectors', 'type': 'group', 'children': self._create_samples(reg_items["real"])},
|
{'name': 'Real sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["real"])},
|
||||||
{'name': 'Ideal sectors', 'type': 'group', 'children': self._create_samples(reg_items["ideal"])},
|
{'name': 'Ideal sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["ideal"])},
|
||||||
]}
|
]}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _generate_curve_params(self,
|
@staticmethod
|
||||||
curve_items: dict) -> dict:
|
def _generate_curve_params(curve_items: dict) -> dict:
|
||||||
|
|
||||||
"""Создает реальные и идеальные линии графиков"""
|
"""Создает реальные и идеальные линии графиков"""
|
||||||
|
|
||||||
res = {'name': 'Plots', 'type': 'group', 'children': [
|
res = {'name': 'Plots', 'type': 'group', 'children': [
|
||||||
{'name': 'Real plots', 'type': 'group', 'children': self._create_samples(curve_items["real"])},
|
{'name': 'Real plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["real"])},
|
||||||
{'name': 'Ideal plots', 'type': 'group', 'children': self._create_samples(curve_items["ideal"])},
|
{'name': 'Ideal plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["ideal"])},
|
||||||
]}
|
]}
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _create_ideal_curves(self,
|
@staticmethod
|
||||||
curve: dict) -> list[dict]:
|
def _create_samples(sector: dict) -> list[dict]:
|
||||||
|
|
||||||
"""Создает секторы с этапами циклограммы"""
|
|
||||||
|
|
||||||
res = []
|
|
||||||
for key, item in curve.items():
|
|
||||||
param = {'name': key, 'type': 'group', 'children': self._create_samples(item)}
|
|
||||||
res.append(param)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _create_samples(self,
|
|
||||||
sector: dict) -> list[dict]:
|
|
||||||
|
|
||||||
"""Создает список представленных элементов с их параметрами"""
|
"""Создает список представленных элементов с их параметрами"""
|
||||||
|
|
||||||
res = []
|
res = []
|
||||||
for key, item in sector.items():
|
for key, item in sector.items():
|
||||||
sample = item[0] if type(item) == list else item
|
sample = item[0] if type(item) == list else item
|
||||||
param = {'name': key, 'type': 'group', 'children': self._create_settings(sample)}
|
param = {'name': key, 'type': 'group', 'children': ReportSettings._create_settings(sample)}
|
||||||
res.append(param)
|
res.append(param)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _create_settings(self,
|
@staticmethod
|
||||||
item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
def _create_settings(item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
||||||
|
|
||||||
"""Получает настройки для элемента"""
|
"""Получает настройки для элемента"""
|
||||||
|
|
||||||
if type(item) == pg.LinearRegionItem:
|
if type(item) == pg.LinearRegionItem:
|
||||||
pen = item.lines[0].pen
|
pen = item.lines[0].pen
|
||||||
brush = item.brush
|
brush = item.brush
|
||||||
@ -140,14 +114,12 @@ class ReportSettings(QtWidgets.QWidget):
|
|||||||
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
|
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
|
||||||
]
|
]
|
||||||
|
|
||||||
def _update_settings(self,
|
@staticmethod
|
||||||
reg_items: dict,
|
def _update_settings(reg_items: dict,
|
||||||
curve_items: dict,
|
curve_items: dict,
|
||||||
qt_items: dict,
|
qt_items: dict,
|
||||||
params: Parameter) -> None:
|
params: Parameter) -> None:
|
||||||
|
|
||||||
"""Задает параметры элементов в соответствии с paramTree"""
|
"""Задает параметры элементов в соответствии с paramTree"""
|
||||||
|
|
||||||
real_sectors = params.child("Sectors").child("Real sectors")
|
real_sectors = params.child("Sectors").child("Real sectors")
|
||||||
ideal_sectors = params.child("Sectors").child("Ideal sectors")
|
ideal_sectors = params.child("Sectors").child("Ideal sectors")
|
||||||
|
|
||||||
@ -156,19 +128,17 @@ class ReportSettings(QtWidgets.QWidget):
|
|||||||
|
|
||||||
qt_settings = params.child("Qt elements")
|
qt_settings = params.child("Qt elements")
|
||||||
|
|
||||||
self._set_sector_settings(reg_items["real"], real_sectors)
|
ReportSettings._set_sector_settings(reg_items["real"], real_sectors)
|
||||||
self._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
ReportSettings._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
||||||
|
|
||||||
self._set_plot_settings(curve_items["real"], real_plots)
|
ReportSettings._set_plot_settings(curve_items["real"], real_plots)
|
||||||
self._set_plot_settings(curve_items["ideal"], ideal_plots)
|
ReportSettings._set_plot_settings(curve_items["ideal"], ideal_plots)
|
||||||
self._set_qt_settings(qt_items, qt_settings)
|
ReportSettings._set_qt_settings(qt_items, qt_settings)
|
||||||
|
|
||||||
def _set_sector_settings(self,
|
@staticmethod
|
||||||
sectors: dict,
|
def _set_sector_settings(sectors: dict,
|
||||||
settings: Parameter) -> None:
|
settings: Parameter) -> None:
|
||||||
|
|
||||||
"""Задает параметры секторов в соответствии с настройками"""
|
"""Задает параметры секторов в соответствии с настройками"""
|
||||||
|
|
||||||
for key, item in sectors.items():
|
for key, item in sectors.items():
|
||||||
sample = settings.child(key)
|
sample = settings.child(key)
|
||||||
line_color = sample.child("Line color").value()
|
line_color = sample.child("Line color").value()
|
||||||
@ -184,12 +154,10 @@ class ReportSettings(QtWidgets.QWidget):
|
|||||||
reg.lines[1].setPen(pen)
|
reg.lines[1].setPen(pen)
|
||||||
reg.setBrush(brush)
|
reg.setBrush(brush)
|
||||||
|
|
||||||
def _set_plot_settings(self,
|
@staticmethod
|
||||||
curves: dict,
|
def _set_plot_settings(curves: dict,
|
||||||
settings: Parameter) -> None:
|
settings: Parameter) -> None:
|
||||||
|
|
||||||
"""Задает параметры кривых в соответствии с настройками"""
|
"""Задает параметры кривых в соответствии с настройками"""
|
||||||
|
|
||||||
for key, item in curves.items():
|
for key, item in curves.items():
|
||||||
sample = settings.child(key)
|
sample = settings.child(key)
|
||||||
line_color = sample.child("Line color").value()
|
line_color = sample.child("Line color").value()
|
||||||
@ -204,12 +172,10 @@ class ReportSettings(QtWidgets.QWidget):
|
|||||||
item.setVisible(visibility)
|
item.setVisible(visibility)
|
||||||
item.setPen(pen)
|
item.setPen(pen)
|
||||||
|
|
||||||
def _set_qt_settings(self,
|
@staticmethod
|
||||||
qt_items: dict,
|
def _set_qt_settings(qt_items: dict,
|
||||||
settings: Parameter) -> None:
|
settings: Parameter) -> None:
|
||||||
|
|
||||||
"""Задает параметры Qt элементов в соответствии с настройками"""
|
"""Задает параметры Qt элементов в соответствии с настройками"""
|
||||||
|
|
||||||
for key, item in qt_items.items():
|
for key, item in qt_items.items():
|
||||||
sample = settings.child(key)
|
sample = settings.child(key)
|
||||||
visibility = sample.child("Visibility").value()
|
visibility = sample.child("Visibility").value()
|
||||||
|
|||||||
@ -1,40 +1,44 @@
|
|||||||
from typing import Callable, Optional, Any
|
from typing import Callable, Optional, Any
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (QWidget, QPushButton,
|
from PyQt5.QtWidgets import (
|
||||||
QLineEdit, QHBoxLayout,
|
QWidget, QPushButton, QLineEdit, QHBoxLayout, QVBoxLayout, QLabel,
|
||||||
QVBoxLayout, QLabel,
|
QTableWidget, QTableWidgetItem, QStyledItemDelegate
|
||||||
QTableWidget, QTableWidgetItem,
|
)
|
||||||
QStyledItemDelegate)
|
|
||||||
from PyQt5.QtGui import QIntValidator, QDoubleValidator
|
from PyQt5.QtGui import QIntValidator, QDoubleValidator
|
||||||
|
|
||||||
from utils.json_tools import read_json, write_json
|
from utils.json_tools import read_json, write_json
|
||||||
from utils import qt_settings as qts
|
from utils import qt_settings as qts
|
||||||
|
|
||||||
|
|
||||||
class settingsWindow(QWidget):
|
class SettingsWindow(QWidget):
|
||||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None], names: dict):
|
|
||||||
"""
|
"""
|
||||||
Окно настроек для редактирования параметров.
|
Окно настроек для редактирования параметров.
|
||||||
:param path: Путь к файлу настроек (JSON).
|
|
||||||
|
Загружает и сохраняет параметры из JSON-файла, отображает их в таблице,
|
||||||
|
позволяет редактировать значения и расширять число параметров.
|
||||||
|
|
||||||
|
:param path: Путь к JSON-файлу с настройками.
|
||||||
:param name: Название набора настроек.
|
:param name: Название набора настроек.
|
||||||
:param upd_func: Функция обновления (коллбэк).
|
:param upd_func: Функция-коллбэк для обновления после сохранения.
|
||||||
|
:param associated_names: Словарь отображения ключей настроек в читаемые имена.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, path: str, name: str, upd_func: Callable[[], None], associated_names: dict):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._settingsPath = path
|
self._settings_path = path
|
||||||
self._name = name
|
self._name = name
|
||||||
self._data: dict[str, list[Any]] = {}
|
self._data: dict[str, list[Any]] = {}
|
||||||
self._upd_func = upd_func
|
self._upd_func = upd_func
|
||||||
|
self._associated_names = associated_names
|
||||||
|
|
||||||
self._num_points: Optional[QLineEdit] = None
|
self._num_points: Optional[QLineEdit] = None
|
||||||
self._param_table: Optional[QTableWidget] = None
|
self._param_table: Optional[QTableWidget] = None
|
||||||
self._assosiated_names = names
|
|
||||||
|
|
||||||
self.load_settings()
|
self.load_settings()
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
|
||||||
def load_settings(self) -> None:
|
def load_settings(self) -> None:
|
||||||
"""Загружает настройки из JSON-файла."""
|
"""Загружает настройки из JSON-файла."""
|
||||||
data = read_json(self._settingsPath)
|
data = read_json(self._settings_path)
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
self._data = data
|
self._data = data
|
||||||
else:
|
else:
|
||||||
@ -42,17 +46,19 @@ class settingsWindow(QWidget):
|
|||||||
|
|
||||||
def write_settings(self) -> None:
|
def write_settings(self) -> None:
|
||||||
"""Записывает текущие настройки в JSON-файл."""
|
"""Записывает текущие настройки в JSON-файл."""
|
||||||
write_json(self._settingsPath, self._data)
|
write_json(self._settings_path, self._data)
|
||||||
|
|
||||||
def getParams(self) -> dict:
|
def get_params(self) -> dict:
|
||||||
"""Возвращает текущий словарь параметров."""
|
"""Возвращает текущий словарь параметров."""
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
def _init_ui(self) -> None:
|
def _init_ui(self) -> None:
|
||||||
"""Инициализирует UI: кнопки, поля ввода, таблицу."""
|
"""Инициализирует пользовательский интерфейс: кнопки, поля ввода, таблицу."""
|
||||||
|
# Кнопки управления
|
||||||
save_button = QPushButton("Save")
|
save_button = QPushButton("Save")
|
||||||
restore_button = QPushButton("Restore")
|
restore_button = QPushButton("Restore")
|
||||||
|
|
||||||
|
# Поле для ввода количества точек сварки
|
||||||
self._num_points = QLineEdit()
|
self._num_points = QLineEdit()
|
||||||
self._num_points.setPlaceholderText("Enter the number of welding points")
|
self._num_points.setPlaceholderText("Enter the number of welding points")
|
||||||
self._num_points.setValidator(QIntValidator())
|
self._num_points.setValidator(QIntValidator())
|
||||||
@ -62,13 +68,16 @@ class settingsWindow(QWidget):
|
|||||||
control_layout.addWidget(restore_button)
|
control_layout.addWidget(restore_button)
|
||||||
control_layout.addWidget(self._num_points)
|
control_layout.addWidget(self._num_points)
|
||||||
|
|
||||||
|
# Подключение сигналов к слотам
|
||||||
save_button.pressed.connect(self._save)
|
save_button.pressed.connect(self._save)
|
||||||
restore_button.pressed.connect(self._restore)
|
restore_button.pressed.connect(self._restore)
|
||||||
self._num_points.editingFinished.connect(self._expand)
|
self._num_points.editingFinished.connect(self._expand)
|
||||||
|
|
||||||
|
# Таблица для отображения параметров
|
||||||
self._param_table = QTableWidget()
|
self._param_table = QTableWidget()
|
||||||
self._populate_table()
|
self._populate_table()
|
||||||
|
|
||||||
|
# Основной вертикальный макет
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
header = QLabel(self._name)
|
header = QLabel(self._name)
|
||||||
layout.addWidget(header)
|
layout.addWidget(header)
|
||||||
@ -78,14 +87,18 @@ class settingsWindow(QWidget):
|
|||||||
self.setStyleSheet(qts.dark_style)
|
self.setStyleSheet(qts.dark_style)
|
||||||
|
|
||||||
def _populate_table(self) -> None:
|
def _populate_table(self) -> None:
|
||||||
"""Заполняет таблицу значениями из self._data."""
|
"""
|
||||||
# Если нет данных для заполнения
|
Заполняет таблицу значениями из self._data.
|
||||||
|
|
||||||
|
Если данных нет, таблица очищается. Для каждого параметра устанавливается
|
||||||
|
делегат в зависимости от типа данных (int, float, str).
|
||||||
|
"""
|
||||||
if not self._data:
|
if not self._data:
|
||||||
self._param_table.setRowCount(0)
|
self._param_table.setRowCount(0)
|
||||||
self._param_table.setColumnCount(0)
|
self._param_table.setColumnCount(0)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Предполагаем, что у всех ключей одинаковая длина списков параметров.
|
# Определяем количество столбцов на основе длины списка первого ключа
|
||||||
first_key = next(iter(self._data), None)
|
first_key = next(iter(self._data), None)
|
||||||
if first_key is None:
|
if first_key is None:
|
||||||
self._param_table.setRowCount(0)
|
self._param_table.setRowCount(0)
|
||||||
@ -95,26 +108,31 @@ class settingsWindow(QWidget):
|
|||||||
column_count = len(self._data[first_key])
|
column_count = len(self._data[first_key])
|
||||||
self._param_table.setRowCount(len(self._data))
|
self._param_table.setRowCount(len(self._data))
|
||||||
self._param_table.setColumnCount(column_count)
|
self._param_table.setColumnCount(column_count)
|
||||||
headers = [self._assosiated_names[key] for key in self._data.keys()]
|
headers = [self._associated_names.get(key, key) for key in self._data.keys()]
|
||||||
self._param_table.setVerticalHeaderLabels(headers)
|
self._param_table.setVerticalHeaderLabels(headers)
|
||||||
|
|
||||||
|
# Создаем делегаты для различных типов данных
|
||||||
int_delegate = ValidatorDelegate(data_type='int', parent=self._param_table)
|
int_delegate = ValidatorDelegate(data_type='int', parent=self._param_table)
|
||||||
float_delegate = ValidatorDelegate(data_type='float', parent=self._param_table)
|
float_delegate = ValidatorDelegate(data_type='float', parent=self._param_table)
|
||||||
str_delegate = ValidatorDelegate(data_type='str', parent=self._param_table)
|
str_delegate = ValidatorDelegate(data_type='str', parent=self._param_table)
|
||||||
|
|
||||||
|
# Заполняем таблицу и устанавливаем делегаты для каждой строки
|
||||||
for i, (_, items) in enumerate(self._data.items()):
|
for i, (_, items) in enumerate(self._data.items()):
|
||||||
for j, item in enumerate(items):
|
for j, item in enumerate(items):
|
||||||
self._param_table.setItem(i, j, QTableWidgetItem(str(item)))
|
self._param_table.setItem(i, j, QTableWidgetItem(str(item)))
|
||||||
if isinstance(item, int):
|
# Используем тип последнего элемента строки для выбора делегата
|
||||||
|
if isinstance(items[-1], int):
|
||||||
self._param_table.setItemDelegateForRow(i, int_delegate)
|
self._param_table.setItemDelegateForRow(i, int_delegate)
|
||||||
elif isinstance(item, float):
|
elif isinstance(items[-1], float):
|
||||||
self._param_table.setItemDelegateForRow(i, float_delegate)
|
self._param_table.setItemDelegateForRow(i, float_delegate)
|
||||||
else:
|
else:
|
||||||
self._param_table.setItemDelegateForRow(i, str_delegate)
|
self._param_table.setItemDelegateForRow(i, str_delegate)
|
||||||
|
|
||||||
|
|
||||||
def _save(self) -> None:
|
def _save(self) -> None:
|
||||||
"""Сохраняет текущие параметры из таблицы в self._data и вызывает _upd_func()."""
|
"""
|
||||||
|
Сохраняет текущие параметры из таблицы в self._data,
|
||||||
|
записывает их в JSON-файл и вызывает функцию обновления.
|
||||||
|
"""
|
||||||
new_data = {}
|
new_data = {}
|
||||||
col_count = self._param_table.columnCount()
|
col_count = self._param_table.columnCount()
|
||||||
for i, key in enumerate(self._data.keys()):
|
for i, key in enumerate(self._data.keys()):
|
||||||
@ -124,7 +142,7 @@ class settingsWindow(QWidget):
|
|||||||
if cell_item is None:
|
if cell_item is None:
|
||||||
continue
|
continue
|
||||||
param_str = cell_item.text()
|
param_str = cell_item.text()
|
||||||
# Если ключ не trace_storage_path, конвертируем в float
|
# Для ключа 'trace_storage_path' оставляем строковое значение
|
||||||
if key != "trace_storage_path":
|
if key != "trace_storage_path":
|
||||||
try:
|
try:
|
||||||
param = float(param_str)
|
param = float(param_str)
|
||||||
@ -133,7 +151,6 @@ class settingsWindow(QWidget):
|
|||||||
else:
|
else:
|
||||||
param = param_str
|
param = param_str
|
||||||
row_data.append(param)
|
row_data.append(param)
|
||||||
|
|
||||||
new_data[key] = row_data
|
new_data[key] = row_data
|
||||||
|
|
||||||
self._data = new_data
|
self._data = new_data
|
||||||
@ -146,7 +163,13 @@ class settingsWindow(QWidget):
|
|||||||
self._populate_table()
|
self._populate_table()
|
||||||
|
|
||||||
def _expand(self) -> None:
|
def _expand(self) -> None:
|
||||||
"""Расширяет количество столбцов таблицы в зависимости от введённого значения."""
|
"""
|
||||||
|
Расширяет количество столбцов таблицы согласно введённому значению.
|
||||||
|
|
||||||
|
Количество столбцов таблицы
|
||||||
|
устанавливается равным заданному значению, а новые ячейки заполняются последним
|
||||||
|
известным значением для соответствующего параметра.
|
||||||
|
"""
|
||||||
if not self._num_points:
|
if not self._num_points:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -166,24 +189,31 @@ class settingsWindow(QWidget):
|
|||||||
|
|
||||||
self._param_table.setColumnCount(desired_columns)
|
self._param_table.setColumnCount(desired_columns)
|
||||||
|
|
||||||
# Новые столбцы заполняем последним известным параметром для каждого ключа
|
# Заполнение новых столбцов последним значением для каждого параметра
|
||||||
for i, (key, items) in enumerate(self._data.items()):
|
for i, (key, items) in enumerate(self._data.items()):
|
||||||
# Если нет данных, пропускаем
|
|
||||||
if not items:
|
if not items:
|
||||||
continue
|
continue
|
||||||
last_value = str(items[-1])
|
last_value = str(items[-1])
|
||||||
for col in range(prev_columns, desired_columns):
|
for col in range(prev_columns, desired_columns):
|
||||||
self._param_table.setItem(i, col, QTableWidgetItem(last_value))
|
self._param_table.setItem(i, col, QTableWidgetItem(last_value))
|
||||||
# Добавляем новый элемент также в self._data для консистентности
|
|
||||||
# После этого можно будет сохранить при нажатии Save
|
|
||||||
# Дополним также и в self._data
|
|
||||||
additional_count = desired_columns - prev_columns
|
additional_count = desired_columns - prev_columns
|
||||||
self._data[key].extend([float(last_value) if key != "trace_storage_path" else last_value] * additional_count)
|
# Приведение типа: для ключа 'trace_storage_path' оставляем строку, иначе преобразуем к float
|
||||||
|
additional_values = (
|
||||||
|
[float(last_value)] * additional_count
|
||||||
|
if key != "trace_storage_path"
|
||||||
|
else [last_value] * additional_count
|
||||||
|
)
|
||||||
|
self._data[key].extend(additional_values)
|
||||||
|
|
||||||
|
|
||||||
class SystemSettings(settingsWindow):
|
class SystemSettings(SettingsWindow):
|
||||||
def __init__(self, path, name, upd_func):
|
"""
|
||||||
assosiated_names = {
|
Настройки системы.
|
||||||
|
|
||||||
|
Окно для редактирования параметров, связанных с работой системы.
|
||||||
|
"""
|
||||||
|
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||||
|
associated_names = {
|
||||||
"trace_storage_path": "Trace path",
|
"trace_storage_path": "Trace path",
|
||||||
"monitor_update_period": "Monitoring period",
|
"monitor_update_period": "Monitoring period",
|
||||||
"a_max_1": "Max lin accel FE, m/s^2",
|
"a_max_1": "Max lin accel FE, m/s^2",
|
||||||
@ -205,15 +235,22 @@ class SystemSettings(settingsWindow):
|
|||||||
"UML_time_scaler": "UML_time_scaler",
|
"UML_time_scaler": "UML_time_scaler",
|
||||||
"Range ME, mm": "Range ME, mm"
|
"Range ME, mm": "Range ME, mm"
|
||||||
}
|
}
|
||||||
super().__init__(path, name, upd_func, assosiated_names)
|
super().__init__(path, name, upd_func, associated_names)
|
||||||
self._num_points.setVisible(False)
|
self._num_points.setVisible(False)
|
||||||
|
|
||||||
def _expand(self):
|
def _expand(self):
|
||||||
|
# Для системных настроек расширение столбцов не требуется.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class OperatorSettings(settingsWindow):
|
|
||||||
def __init__(self, path, name, upd_func):
|
class OperatorSettings(SettingsWindow):
|
||||||
assosiated_names = {
|
"""
|
||||||
|
Настройки оператора.
|
||||||
|
|
||||||
|
Окно для редактирования параметров, связанных с работой оператора.
|
||||||
|
"""
|
||||||
|
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||||
|
associated_names = {
|
||||||
"distance_h_start_1": "Closing start dist FE, m",
|
"distance_h_start_1": "Closing start dist FE, m",
|
||||||
"distance_h_start_2": "Closing start dist ME, m",
|
"distance_h_start_2": "Closing start dist ME, m",
|
||||||
"distance_s_1": "Rob movement start dist FE, m",
|
"distance_s_1": "Rob movement start dist FE, m",
|
||||||
@ -234,13 +271,17 @@ class OperatorSettings(settingsWindow):
|
|||||||
"Tesla oncomming_relief": "Client moving to next point time, sec",
|
"Tesla oncomming_relief": "Client moving to next point time, sec",
|
||||||
"Tesla summary time": "Client summary time, sec"
|
"Tesla summary time": "Client summary time, sec"
|
||||||
}
|
}
|
||||||
super().__init__(path, name, upd_func, assosiated_names)
|
super().__init__(path, name, upd_func, associated_names)
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FilterSettings(settingsWindow):
|
class FilterSettings(SettingsWindow):
|
||||||
def __init__(self, path, name, upd_func):
|
"""
|
||||||
assosiated_names = {
|
Настройки фильтра.
|
||||||
|
|
||||||
|
Окно для редактирования параметров фильтрации.
|
||||||
|
"""
|
||||||
|
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||||
|
associated_names = {
|
||||||
"act_pos_decrease": "ME max pos change in Relief, mm",
|
"act_pos_decrease": "ME max pos change in Relief, mm",
|
||||||
"act_vel_min": "Minimum for ME speed in Closing mm/sec",
|
"act_vel_min": "Minimum for ME speed in Closing mm/sec",
|
||||||
"act_vel_close": "Maximum for ME speed in Squeeze mm/sec",
|
"act_vel_close": "Maximum for ME speed in Squeeze mm/sec",
|
||||||
@ -252,14 +293,20 @@ class FilterSettings(settingsWindow):
|
|||||||
"force_increase": "ME force rising speed in Squeeze",
|
"force_increase": "ME force rising speed in Squeeze",
|
||||||
"force_decrease": "ME force falling speed in Relief"
|
"force_decrease": "ME force falling speed in Relief"
|
||||||
}
|
}
|
||||||
super().__init__(path, name, upd_func, assosiated_names)
|
super().__init__(path, name, upd_func, associated_names)
|
||||||
self._num_points.setVisible(False)
|
self._num_points.setVisible(False)
|
||||||
|
|
||||||
def _expand(self):
|
def _expand(self):
|
||||||
|
# Для настроек фильтра расширение столбцов не требуется.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ValidatorDelegate(QStyledItemDelegate):
|
class ValidatorDelegate(QStyledItemDelegate):
|
||||||
|
"""
|
||||||
|
Валидация для ввода в ячейках таблицы.
|
||||||
|
|
||||||
|
В зависимости от типа данных ('int', 'float' или 'str') устанавливает соответствующий валидатор.
|
||||||
|
"""
|
||||||
def __init__(self, data_type='str', parent=None):
|
def __init__(self, data_type='str', parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.data_type = data_type
|
self.data_type = data_type
|
||||||
@ -278,11 +325,10 @@ class ValidatorDelegate(QStyledItemDelegate):
|
|||||||
return editor
|
return editor
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
# Для демонстрации создаем окно настроек с фиктивной функцией обновления и пустым словарем имен
|
||||||
app = pg.mkQApp("Parameter Tree Example")
|
app = pg.mkQApp("Parameter Tree Example")
|
||||||
window = settingsWindow('params\operator_params.json', 'operator')
|
window = SettingsWindow('params/operator_params.json', 'operator', lambda: None, {})
|
||||||
|
window.show()
|
||||||
app.exec()
|
app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user