diff --git a/src/base/base.py b/src/base/base.py index 863be46..ff2fab7 100644 --- a/src/base/base.py +++ b/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: - + """ + Базовый класс для мониторинга директории. + + Использует 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,43 +251,49 @@ 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 - "Welding": [128, 128, 128, 100], # Gray - "Relief": [34, 139, 34, 100], # Forest Green - "Oncomming": [255, 165, 0, 100] # Orange - } + "Closing": [220, 20, 60, 100], # Crimson + "Squeeze": [30, 144, 255, 100], # Dodger Blue + "Welding": [128, 128, 128, 100], # Gray + "Relief": [34, 139, 34, 100], # Forest Green + "Oncomming": [255, 165, 0, 100] # Orange + } 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: return self._controller - + @property def mediator(self) -> BaseMediator: return self._mediator @@ -221,48 +302,82 @@ 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 def mediator(self) -> BaseMediator: return self._mediator @@ -270,7 +385,7 @@ class BaseController(QObject): @mediator.setter def mediator(self, mediator: BaseMediator) -> None: self._mediator = mediator - + @property def file_manager(self) -> BaseFileManager: return self._file_manager @@ -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: @@ -308,100 +423,153 @@ class BaseFileManager: @monitor.setter def monitor(self, monitor: BaseDirectoryMonitor) -> None: - self._monitor = monitor + self._monitor = monitor def replot_all(self) -> None: - ... - - def open_custom_file(self, path:str) -> None: + """ + Переотрисовывает все данные, используя текущую библиотеку путей. + """ ... - def open_raw_traces_dir(self, path:str) -> None: - ... - - def set_mode(self, num:int) -> None: - ... - - def update_monitor_settings(self, settings:list[dict]) -> None: + def open_custom_file(self, path: str) -> None: + """ + Открывает пользовательский файл по заданному пути. + """ ... - def add_new_paths(self, paths:list[str]): + def open_raw_traces_dir(self, path: str) -> None: + """ + Открывает директорию с сырыми трассировочными данными. + """ + ... + + def set_mode(self, num: int) -> None: + """ + Устанавливает режим работы файлового менеджера. + """ + ... + + def update_monitor_settings(self, settings: List[Dict]) -> None: + """ + Обновляет настройки мониторинга директории. + """ + ... + + 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,23 +602,33 @@ class BasePointPassportFormer: "position_start_1", "position_start_2", "k_prop", - "time_capture"] - - def form_passports(self, data: list[pd.DataFrame]) -> None: + "time_capture" + ] + + 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 def opt_algorithm(self) -> BaseIdealDataBuilder: 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): + """ + Базовый класс для обработки сырых трассировочных данных. + """ + def __init__(self, + 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 - - def prerender(self, data:list[str]) -> None: - ... - - def final_render(self) -> None: + self._trace_df: Optional[pd.DataFrame] = None + self._text_data: Optional[Any] = None + + def prerender(self, data: List[str]) -> None: + """ + Выполняет предварительный рендеринг данных. + """ ... - def update_settings(self, data:Settings) -> None: + def final_render(self) -> 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: - ... \ No newline at end of file + + def detect_welding(self, data: List[KukaTXT]) -> List: + """ + Детектирует этап сварки по текстовым данным. + """ + ... diff --git a/src/controller/controller.py b/src/controller/controller.py index 7bf91bc..d779cb0 100644 --- a/src/controller/controller.py +++ b/src/controller/controller.py @@ -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: - filepath, tab = data - pixmap = QPixmap(tab.size()) - tab.render(pixmap) - pixmap.save(filepath) + def save_file(self, data: Tuple[str, QTabWidget]) -> None: + """ + Сохраняет снимок содержимого вкладки в указанный файл. - - \ No newline at end of file + :param data: Кортеж, содержащий путь к файлу и QTabWidget для сохранения. + """ + filepath, tab = data + # Создаем QPixmap с размером, равным размеру вкладки + pixmap = QPixmap(tab.size()) + # Рендерим содержимое вкладки в QPixmap + tab.render(pixmap) + # Сохраняем изображение по указанному пути + pixmap.save(filepath) diff --git a/src/controller/converter.py b/src/controller/converter.py index 5367ede..dde1d96 100644 --- a/src/controller/converter.py +++ b/src/controller/converter.py @@ -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 @@ -25,9 +40,15 @@ class DataConverter(BaseDataConverter): except Exception as e: logger.error(f"_replace_bool - Непредвиденная ошибка: {e}") 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) diff --git a/src/controller/file_manager.py b/src/controller/file_manager.py index 3c69c8c..8076dbc 100644 --- a/src/controller/file_manager.py +++ b/src/controller/file_manager.py @@ -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: - self._paths_library.add(path) + Если текущий режим не равен 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} не существует.") - + logger.warning(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() - - def add_new_paths(self, paths:list) -> None: - paths_set = set(paths) - new = self._paths_library.difference(paths_set) - self._paths_library.update(new) - self._mediator.notify(list(new)) + if self._monitor.isActive: + self._monitor.start() - def open_raw_traces_dir(self, path:str) -> None: + def add_new_paths(self, paths: list) -> None: + """ + Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали, + и уведомляет медиатора о новых путях. + + :param paths: Список новых путей. + """ + paths_set = set(paths) + 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: + """ + Открывает указанную директорию, ищет в ней файлы с расширениями .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() @@ -73,39 +133,57 @@ class FileManager(BaseFileManager): txt_file = full_path if dat_file and txt_file: 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} не существует.") - + logger.error(f"Путь {self._directory_path} не существует.") + raise FileNotFoundError(f"Путь {self._directory_path} не существует.") + self._files = [ - os.path.join(self._directory_path, file) + os.path.join(self._directory_path, file) for file in os.listdir(self._directory_path) if file.lower().endswith('.csv') - ] + ] self.update_timer.timeout.connect(self._monitor) 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}") self._file_manager.add_new_paths(new_files) self._files = current_files if not current_files: - self._files = [] \ No newline at end of file + self._files = [] diff --git a/src/controller/mediator.py b/src/controller/mediator.py index c49e78c..993de0e 100644 --- a/src/controller/mediator.py +++ b/src/controller/mediator.py @@ -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: - - if issubclass(source.__class__, BaseController): + + def prerender_TCW( + self, + source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor], + data: Union[str, list[str], list[pd.DataFrame], dict] + ) -> None: + """ + Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления. + + :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: - - if issubclass(source.__class__, BaseController): + def render_TCW( + self, + source: Union[BaseController, BaseRawTraceProcessor], + data: Union[str, list[pd.DataFrame], dict] = None + ) -> None: + """ + Выполняет финальный рендеринг TCW. + + :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) - - - diff --git a/src/controller/passport_former.py b/src/controller/passport_former.py index 6e6db34..7739edd 100644 --- a/src/controller/passport_former.py +++ b/src/controller/passport_former.py @@ -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): + """ + Класс для формирования паспортов (графических и точечных) по данным трассировки. + + Основные возможности: + - Формирование паспортов для каждого 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: + 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) - - def update_settings(self, settings: Settings): + self._mediator.notify(self, passports) + + 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) - - def _modify_coord_settings(self, ME_coords:tuple) -> None: + self._mediator.notify(self, result) + + 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]]: """ - Формирует списки времен начала и окончания событий для указанного сигнала. + Формирует списки времён начала и окончания событий для указанного сигнала. + + Если у первого события не определено время начала, оно принимается за 0. + Если число стартов события больше числа финишей, последним финишем считается конец времён. + + :param signal: Имя столбца-сигнала. + :param times: Series с временными метками. + :param df: DataFrame с данными. + :return: Кортеж из двух списков: (список времён начала, список времён окончания). """ - start_idx, finish_idx = PassportFormer._find_indexes(signal, dataframe) - + 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 - - def _generate_events(self, - times: pd.Series, - dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]: + return (start_list, end_list) + + 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 - - def _build_ideal_data(self, - idealDataBuilder: Optional[BaseIdealDataBuilder] = None, - point_settings: Settings = None) -> dict: + return (events, point_quantity) + + 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( @@ -145,18 +203,21 @@ class PassportFormer(BasePointPassportFormer): except Exception as e: 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 = { @@ -227,26 +306,37 @@ class PassportFormer(BasePointPassportFormer): except Exception as e: 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 = { @@ -257,15 +347,21 @@ class PassportFormer(BasePointPassportFormer): except Exception as e: 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()} @@ -273,11 +369,21 @@ class PassportFormer(BasePointPassportFormer): except Exception as e: logger.error(f"_form_point_events - Ошибка при формировании событий для точки {idx}: {e}") return None, None - + 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, diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 03363c8..e8c64c7 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,2 +1,2 @@ from .plotter import PlotWidget -from .settings_window import settingsWindow +from .settings_window import SettingsWindow diff --git a/src/gui/main_gui.py b/src/gui/main_gui.py index 7ef654b..a96fd4b 100644 --- a/src/gui/main_gui.py +++ b/src/gui/main_gui.py @@ -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: diff --git a/src/gui/plotter.py b/src/gui/plotter.py index 1be52ab..98739fc 100644 --- a/src/gui/plotter.py +++ b/src/gui/plotter.py @@ -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,68 +82,86 @@ 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) + 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"] - case 2: + case 2: self._plt_channels = self._plt_structures["Report Editor"] - case 3: - self._plt_channels = self._plt_structures["Client Trace Watcher"] + 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,131 +179,170 @@ 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 - + if not self._ideal_mode: dataframe_headers = graphic_passport.dataframe.columns.tolist() point_steps = len(graphic_passport.points_pocket) - else: + else: dataframe_headers = [] point_steps = 1 - - self._datapack = [ - graphic_passport.dataframe, - dataframe_headers, - graphic_passport.useful_data, - graphic_passport.points_pocket, - widget_steps, - point_steps - ] - def generate_plot_item(self, - widget_num: int, - channel: str, - description: dict[str, Any], - pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]: - - dataframe, dataframe_headers, useful_data, points_pocket, widget_steps, point_steps = self._datapack + # Используем словарь для более понятной организации данных + 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], + pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]: + """ + Генерирует PlotItem для заданного канала с добавлением регионов, компенсаций и идеальных данных. + + :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: + 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,60 +518,86 @@ 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) rect_item.setBrush(pg.mkBrush('grey')) 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 + self.setFixedHeight(100) + # Извлекаем данные из каждого элемента основного графика и даунсэмплируем их 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) diff --git a/src/gui/report_gui.py b/src/gui/report_gui.py index a903ffb..1489d0e 100644 --- a/src/gui/report_gui.py +++ b/src/gui/report_gui.py @@ -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() diff --git a/src/gui/settings_window.py b/src/gui/settings_window.py index 782ac5a..60e3f78 100644 --- a/src/gui/settings_window.py +++ b/src/gui/settings_window.py @@ -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): - """ - Окно настроек для редактирования параметров. - :param path: Путь к файлу настроек (JSON). - :param name: Название набора настроек. - :param upd_func: Функция обновления (коллбэк). - """ +class SettingsWindow(QWidget): + """ + Окно настроек для редактирования параметров. + + Загружает и сохраняет параметры из JSON-файла, отображает их в таблице, + позволяет редактировать значения и расширять число параметров. + + :param path: Путь к JSON-файлу с настройками. + :param name: Название набора настроек. + :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", @@ -204,17 +234,24 @@ class SystemSettings(settingsWindow): "time_capture": "Calculated points per sec", "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 + # Для системных настроек расширение столбцов не требуется. + 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", @@ -233,33 +270,43 @@ class OperatorSettings(settingsWindow): "Tesla welding": "Client welding time, sec", "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" - } - super().__init__(path, name, upd_func, assosiated_names) + "force_decrease": "ME force falling speed in Relief" + } + super().__init__(path, name, upd_func, associated_names) self._num_points.setVisible(False) def _expand(self): - pass + # Для настроек фильтра расширение столбцов не требуется. + 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() - -