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