chore: во все модули добавлены docstring

This commit is contained in:
Andrew 2025-02-11 16:37:39 +03:00
parent ba0f8818bb
commit 78d34343d5
11 changed files with 1468 additions and 748 deletions

View File

@ -1,6 +1,6 @@
from __future__ import annotations
import os
from typing import Optional, Union
from typing import Optional, Union, List, Tuple, Dict, Any
from dataclasses import dataclass, field
from cachetools import LRUCache
@ -11,53 +11,67 @@ from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
from OptAlgorithm import OptAlgorithm
from utils.qt_settings import dark_style
# ========= Дата-классы для хранения данных =========
@dataclass
class KukaTXT:
"""
Класс для представления текстовых данных Кука.
"""
time: float = 0
endtime: float = 0
#module: str
func: str = ""
type_: str = ""
signal: str = ""
#line: int = 0
#point_name: str = ""
#point_coord: dict = field(default_factory=lambda: {})
#blending: str = ""
#blending_param: float = 0
#velocities: dict = field(default_factory=lambda: {})
#accelerarions: dict = field(default_factory=lambda: {})
#base: dict = field(default_factory=lambda: {})
#tool: dict = field(default_factory=lambda: {})
#ipo_mode: str = ""
#motion_mode: str = ""
#load: dict = field(default_factory=lambda: {})
#load_a3: dict = field(default_factory=lambda: {})
# Дополнительные поля (закомментированы) можно добавить при необходимости
# module: str
# line: int = 0
# point_name: str = ""
# ...
@dataclass
class KukaDataHead:
"""
Класс, описывающий заголовок данных Кука.
"""
rob_ID: int = 0
filename: str = ""
channels: dict = field(default_factory= lambda: {})
channels: dict = field(default_factory=dict)
@dataclass
class PlotItems:
regions: dict = field(default_factory= lambda: {})
curves: dict = field(default_factory= lambda: {})
qt_items: dict = field(default_factory= lambda: {})
"""
Класс для хранения элементов графика: регионы, кривые и QT-виджеты.
"""
regions: dict = field(default_factory=dict)
curves: dict = field(default_factory=dict)
qt_items: dict = field(default_factory=dict)
@dataclass
class PointPassport:
timeframe: list = field(default_factory= lambda: [])
events: dict = field(default_factory= lambda: {})
ideal_data: dict = field(default_factory= lambda: {})
useful_data: dict = field(default_factory= lambda: {})
"""
Паспорт точки, содержащий временной интервал, события,
идеальные данные и полезную информацию для этой точки.
"""
timeframe: List[Any] = field(default_factory=list)
events: Dict = field(default_factory=dict)
ideal_data: Dict = field(default_factory=dict)
useful_data: Dict = field(default_factory=dict)
@dataclass
class UsefulGraphData:
"""
Полезные данные для графика.
:param client_time: Время, рассчитанное для клиента.
:param range_ME: Диапазон для ME.
:param k_hardness: Коэффициент твердости.
"""
client_time: float = 0
range_ME: float = 0
k_hardness: float = 0
@ -65,20 +79,37 @@ class UsefulGraphData:
@dataclass
class GraphicPassport:
"""
Графический паспорт, объединяющий DataFrame с данными,
список паспортов точек и полезные данные для построения графика.
"""
dataframe: pd.DataFrame = pd.DataFrame({})
points_pocket: list[PointPassport] = field(default_factory= lambda: [])
points_pocket: List[PointPassport] = field(default_factory=list)
useful_data: UsefulGraphData = UsefulGraphData()
@dataclass
class Settings:
operator: dict = field(default_factory= lambda: {})
system: dict = field(default_factory= lambda: {})
filter: dict = field(default_factory= lambda: {})
"""
Настройки приложения, разделённые на категории:
- operator: настройки, связанные с оператором,
- system: системные настройки,
- filter: настройки фильтрации трейса клиента для определения событий.
"""
operator: Dict = field(default_factory=dict)
system: Dict = field(default_factory=dict)
filter: Dict = field(default_factory=dict)
# ========= Базовые классы для взаимодействия компонентов =========
class BaseMediator:
"""
Базовый класс медиатора для организации взаимодействия между компонентами приложения.
Медиатор связывает такие модули, как конвертер данных, формирователь паспортов,
виджет построения графиков, контроллер, файловый менеджер и процессор трассировки.
"""
def __init__(self,
converter: BaseDataConverter,
passport_former: BasePointPassportFormer,
@ -100,32 +131,61 @@ class BaseMediator:
self._trace_processor.mediator = self
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseRawTraceProcessor],
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget], pd.DataFrame]):
source: Union[
BaseDirectoryMonitor,
BaseDataConverter,
BasePointPassportFormer,
BasePlotWidget,
BaseRawTraceProcessor
],
data: Union[
List[str],
List[pd.DataFrame],
List[list],
List[QWidget],
pd.DataFrame
]) -> None:
"""
Получает уведомление от компонента-источника и распределяет данные по соответствующим модулям.
"""
...
def prerender_TCW(self, source:Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data:Union[str, list[str], list[pd.DataFrame, dict]]) -> None:
def prerender_TCW(self,
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data: Union[str, List[str], List[pd.DataFrame, dict]]) -> None:
"""
Выполняет операции для отображения предварительных данных клиента.
"""
...
def render_TCW(self, source:Union[BaseController, BaseRawTraceProcessor],
data:Union[str, list[pd.DataFrame, dict]]) -> None:
def render_TCW(self,
source: Union[BaseController, BaseRawTraceProcessor],
data: Union[str, List[pd.DataFrame, dict]]) -> None:
"""
Выполняет операции для отображения обработанных данных клиента и ТСК.
"""
...
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим работы.
"""
...
class BaseDirectoryMonitor:
class BaseDirectoryMonitor:
"""
Базовый класс для мониторинга директории.
Использует QTimer для периодической проверки содержимого директории.
"""
update_timer = QTimer()
def __init__(self,
file_manager: Optional[BaseFileManager] = None):
super().__init__()
self._directory_path = None
self._update_time = None
self.isActive = False
self._files = []
def __init__(self, file_manager: Optional[BaseFileManager] = None):
self._directory_path: Optional[str] = None
self._update_time: Optional[float] = None
self.isActive: bool = False
self._files: List[str] = []
self._file_manager = file_manager
@property
@ -137,7 +197,7 @@ class BaseDirectoryMonitor:
return self._update_time
@property
def files(self) -> list[str]:
def files(self) -> List[str]:
return self._files
@property
@ -148,23 +208,38 @@ class BaseDirectoryMonitor:
def file_manager(self, file_manager: BaseFileManager) -> None:
self._file_manager = file_manager
def init_state(self):
def init_state(self) -> None:
"""
Инициализирует состояние мониторинга, считывая список файлов из директории.
"""
files = os.listdir(self._directory_path)
self._files = files
def start(self):
def start(self) -> None:
"""
Запускает мониторинг директории, устанавливая флаг активности и запуская таймер.
"""
self.isActive = True
self.update_timer.start(int(self._update_time))
def stop(self):
def stop(self) -> None:
"""
Останавливает мониторинг директории.
"""
self.isActive = False
self.update_timer.stop()
def pause(self):
def pause(self) -> None:
"""
Приостанавливает мониторинг директории (останавливает таймер).
"""
self.update_timer.stop()
class BaseDataConverter:
"""
Базовый класс для конвертации данных.
"""
def __init__(self, mediator: Optional[BaseMediator] = None):
self._mediator = mediator
@ -176,38 +251,44 @@ 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:
@ -221,46 +302,80 @@ class BasePlotWidget:
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
def build(self, data: List[pd.DataFrame]) -> List[QWidget]:
"""
Строит графики на основе списка DataFrame.
"""
...
def build_raw_trace(self, data:pd.DataFrame) -> None:
def build_raw_trace(self, data: pd.DataFrame) -> None:
"""
Строит график трейсов клиента с предварительным разбиением на этапы.
"""
...
def set_mode(self, mode:int) -> None:
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим отображения графиков.
"""
...
class BaseController(QObject):
def __init__(self,
mediator: Optional[BaseMediator] = None,
file_manager: Optional[BaseFileManager] = None):
"""
Базовый класс контроллера, отвечающий за обновление интерфейса и взаимодействие с медиатором.
"""
def __init__(self, mediator: Optional[BaseMediator] = None, file_manager: Optional[BaseFileManager] = None):
super().__init__()
self._mediator = mediator
self._file_manager = file_manager
def send_widgets(self, widgets: list[QWidget]) -> None:
def send_widgets(self, widgets: List[QWidget]) -> None:
"""
Отправляет список виджетов для дальнейшей обработки.
"""
...
def update_settings(self, settings: list[dict]) -> None:
def update_settings(self, settings: List[Dict]) -> None:
"""
Обновляет настройки приложения.
"""
...
def set_working_mode(self, mode:int) -> None:
def set_working_mode(self, mode: int) -> None:
"""
Устанавливает рабочий режим приложения.
"""
...
def open_file(self, filepath: str) -> None:
"""
Открывает файл по заданному пути.
"""
...
def open_dir(self, dirpath:str) -> None:
def open_dir(self, dirpath: str) -> None:
"""
Открывает директорию.
"""
...
def update_plots(self) -> None:
"""
Обновляет графики.
"""
...
def update_status(self, msg:str) -> None:
def update_status(self, msg: str) -> None:
"""
Обновляет статусное сообщение в интерфейсе.
"""
...
def update_progress(self, progress:int) -> None:
def update_progress(self, progress: int) -> None:
"""
Обновляет значение прогресс-бара.
"""
...
@property
@ -281,14 +396,14 @@ class BaseController(QObject):
class BaseFileManager:
def __init__(self,
mediator: Optional[BaseMediator] = None,
monitor: Optional[BaseDirectoryMonitor] = None):
"""
Базовый класс файлового менеджера.
"""
def __init__(self, mediator: Optional[BaseMediator] = None, monitor: Optional[BaseDirectoryMonitor] = None):
self._mediator = mediator
self._monitor = monitor
self._paths_library:set = set()
self._mode = 0
self._paths_library: set = set()
self._mode: int = 0
@property
def paths_library(self) -> set:
@ -311,97 +426,150 @@ class BaseFileManager:
self._monitor = monitor
def replot_all(self) -> None:
"""
Переотрисовывает все данные, используя текущую библиотеку путей.
"""
...
def open_custom_file(self, path:str) -> None:
def open_custom_file(self, path: str) -> None:
"""
Открывает пользовательский файл по заданному пути.
"""
...
def open_raw_traces_dir(self, path:str) -> None:
def open_raw_traces_dir(self, path: str) -> None:
"""
Открывает директорию с сырыми трассировочными данными.
"""
...
def set_mode(self, num:int) -> None:
def set_mode(self, num: int) -> None:
"""
Устанавливает режим работы файлового менеджера.
"""
...
def update_monitor_settings(self, settings:list[dict]) -> None:
def update_monitor_settings(self, settings: List[Dict]) -> None:
"""
Обновляет настройки мониторинга директории.
"""
...
def add_new_paths(self, paths:list[str]):
def add_new_paths(self, paths: List[str]) -> None:
"""
Добавляет новые пути в библиотеку, если их ранее не было.
"""
...
class BaseIdealDataBuilder(OptAlgorithm):
"""
Базовый класс для построения идеальных данных, расширяющий функциональность OptAlgorithm.
При инициализации на основе настроек устанавливаются множитель времени и время сварки.
"""
def __init__(self, settings: Settings):
self.mul = settings.system['time_capture']
self.welding_time = settings.operator['time_wielding']
super().__init__(settings.system, settings.operator)
def get_closingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа закрытия.
"""
...
def get_compressionDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сжатия.
"""
...
def get_openingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа открытия.
"""
...
def get_tmovementDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения.
"""
...
def get_weldingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сварки.
"""
...
def get_oncomingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения (oncomming).
"""
...
def get_ideal_timings(self) -> list[float, float, float, float]:
def get_ideal_timings(self) -> Tuple[float, float, float, float]:
"""
Получает идеальные временные интервалы для этапов.
"""
...
def get_cycle_time(self) -> float:
"""
Вычисляет общее время цикла как сумму идеальных временных интервалов.
"""
result = sum(self.get_ideal_timings())
return result
class BaseMainWindow(QMainWindow):
"""
Базовое главное окно приложения.
"""
def __init__(self):
super().__init__()
self.resize(200,200)
self.resize(200, 200)
# Создаем центральный виджет и устанавливаем его
self._central_widget = QWidget()
self.setCentralWidget(self._central_widget)
# Устанавливаем основной вертикальный макет для центрального виджета
self._central_layout = QVBoxLayout()
self._central_widget.setLayout(self._central_layout)
self.set_style(self)
...
def set_style(self, object: Union[QTabWidget, QWidget, QMainWindow]) -> None:
object.setStyleSheet(dark_style)
def set_style(self, obj: Union[QTabWidget, QWidget, QMainWindow]) -> None:
"""
Устанавливает стиль для переданного объекта.
"""
obj.setStyleSheet(dark_style)
class BasePointPassportFormer:
"""
Базовый класс для формирования паспортов точек.
def __init__(self,
mediator: Optional[BaseMediator] = None):
Содержит базовые настройки, список этапов, кэш для идеальных данных и набор параметров для алгоритма.
"""
def __init__(self, mediator: Optional[BaseMediator] = None):
self._mediator = mediator
self._clear_stage = "Welding"
self._settings = Settings
self._stages = [
self._clear_stage: str = "Welding"
self._settings = Settings()
self._stages: List[str] = [
"Closing",
"Squeeze",
"Welding",
"Relief",
"Oncomming"
]
self._tesla_stages = [
self._tesla_stages: List[str] = [
"Tesla squeeze",
"Tesla closing",
"Tesla welding",
"Tesla oncomming_relief"
]
self._ideal_data_cashe = LRUCache(maxsize=1000)
self._ideal_data_cache = LRUCache(maxsize=1000)
self._OptAlgorithm_operator_params = [
"dist_open_start_1",
"dist_open_start_2",
@ -416,7 +584,8 @@ class BasePointPassportFormer:
"object_thickness",
"force_target",
"force_capture",
"time_wielding"]
"time_wielding"
]
self._OptAlgorithm_system_params = [
"a_max_1",
"v_max_1",
@ -433,15 +602,25 @@ class BasePointPassportFormer:
"position_start_1",
"position_start_2",
"k_prop",
"time_capture"]
"time_capture"
]
def form_passports(self, data: list[pd.DataFrame]) -> None:
def form_passports(self, data: List[pd.DataFrame]) -> None:
"""
Формирует паспорта для набора данных.
"""
...
def form_customer_passport(self, data: list[pd.DataFrame, dict]) -> None:
def form_customer_passport(self, data: List[Union[pd.DataFrame, dict]]) -> None:
"""
Формирует паспорт заказчика на основе DataFrame и событий.
"""
...
def update_settings(self, params: list) -> None:
def update_settings(self, params: List) -> None:
"""
Обновляет настройки для формирования паспортов.
"""
...
@property
@ -449,7 +628,7 @@ class BasePointPassportFormer:
return self._opt_algorithm
@opt_algorithm.setter
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder):
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder) -> None:
self._opt_algorithm = opt_algorithm
@property
@ -462,28 +641,39 @@ class BasePointPassportFormer:
class BaseRawTraceProcessor:
"""
Базовый класс для обработки сырых трассировочных данных.
"""
def __init__(self,
dataparser:BaseKukaDataParser,
textparser:BaseKukaTextParser,
data_detector:BaseTraceStageDetector,
text_detector:BaseTextStageDetector,
mediator:Optional[BaseMediator] = None):
dataparser: BaseKukaDataParser,
textparser: BaseKukaTextParser,
data_detector: BaseTraceStageDetector,
text_detector: BaseTextStageDetector,
mediator: Optional[BaseMediator] = None):
self._mediator = mediator
self._dataparser = dataparser
self._textparser = textparser
self._data_detector = data_detector
self._text_detector = text_detector
self._trace_df = None
self._text_data = None
self._trace_df: Optional[pd.DataFrame] = None
self._text_data: Optional[Any] = None
def prerender(self, data:list[str]) -> None:
def prerender(self, data: List[str]) -> None:
"""
Выполняет предварительный рендеринг данных.
"""
...
def final_render(self) -> None:
"""
Выполняет финальный рендеринг данных.
"""
...
def update_settings(self, data:Settings) -> None:
def update_settings(self, data: Settings) -> None:
"""
Обновляет настройки обработки трассировочных данных.
"""
...
@property
@ -496,38 +686,57 @@ class BaseRawTraceProcessor:
class BaseKukaDataParser:
"""
Базовый класс для парсинга данных Кука.
"""
def __init__(self):
self._ch_name = None
self._ch_name: Optional[str] = None
def parse(self, head_path: str) -> pd.DataFrame:
"""
Парсит файл заголовка и возвращает DataFrame с данными.
"""
...
class BaseKukaTextParser:
"""
Базовый класс для парсинга текстовых данных Кука.
"""
def __init__(self):
self._in_msg = None
self._datapacks = None
def parse(self, path:str ) -> list[KukaTXT]:
def parse(self, path: str) -> List[KukaTXT]:
"""
Парсит текстовый файл и возвращает список объектов KukaTXT.
"""
...
class BaseTraceStageDetector:
def __init__(self, parent:BaseRawTraceProcessor = None):
"""
Базовый класс для детекции стадий на основе данных трассировки.
"""
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
self._parent = parent
def detect_stages(self, df: pd.DataFrame) -> list:
def detect_stages(self, df: pd.DataFrame) -> List:
"""
Детектирует стадии процесса по данным DataFrame.
"""
...
class BaseTextStageDetector:
def __init__(self, parent:BaseRawTraceProcessor = None):
"""
Базовый класс для детекции сварочных стадий на основе текстовых данных.
"""
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
self._parent = parent
def detect_welding(self, data:list[KukaTXT]) -> list:
def detect_welding(self, data: List[KukaTXT]) -> List:
"""
Детектирует этап сварки по текстовым данным.
"""
...

View File

@ -1,3 +1,4 @@
from typing import Tuple
from PyQt5.QtWidgets import QWidget, QTabWidget
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import pyqtSignal
@ -6,43 +7,96 @@ from base.base import BaseController, Settings
class Controller(BaseController):
"""
Контроллер приложения.
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
медиатор и т.д.
"""
signal_widgets = pyqtSignal(list)
signal_progress_bar = pyqtSignal(int)
signal_status_text = pyqtSignal(str)
def set_working_mode(self, mode:int) -> None:
def set_working_mode(self, mode: int) -> None:
"""
Устанавливает рабочий режим приложения через медиатор.
:param mode: Режим работы (целое число).
"""
self._mediator.set_mode(mode)
def update_plots(self) -> None:
"""
Инициирует переотрисовку графиков через файловый менеджер.
"""
self._file_manager.replot_all()
def send_widgets(self, widgets: list[QWidget]) -> None:
"""
Отправляет список виджетов для дальнейшего использования.
:param widgets: Список виджетов.
"""
self.signal_widgets.emit(widgets)
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки приложения, передавая их через медиатор.
:param settings: Объект настроек.
"""
self._mediator.notify(self, settings)
def update_status(self, msg:str) -> None:
def update_status(self, msg: str) -> None:
"""
Обновляет статусное сообщение в интерфейсе.
:param msg: Текст сообщения.
"""
self.signal_status_text.emit(msg)
def update_progress(self, progress:int) -> None:
def update_progress(self, progress: int) -> None:
"""
Обновляет значение прогресс-бара.
:param progress: Значение прогресса (от 0 до 100).
"""
self.signal_progress_bar.emit(progress)
def open_file(self, filepath: str) -> None:
"""
Открывает файл по указанному пути через файловый менеджер.
:param filepath: Путь к файлу.
"""
self._file_manager.open_custom_file(filepath)
def open_dir(self, dirpath:str) -> None:
def open_dir(self, dirpath: str) -> None:
"""
Инициирует предварительный рендеринг данных из директории.
:param dirpath: Путь к директории.
"""
self._mediator.prerender_TCW(self, dirpath)
def build_TCW_for_client(self):
def build_TCW_for_client(self) -> None:
"""
Выполняет финальный рендеринг TCW (Trace Control Widget) для клиента.
"""
self._mediator.render_TCW(self)
def save_file(self, data:list[str, QTabWidget]) -> None:
def save_file(self, data: Tuple[str, QTabWidget]) -> None:
"""
Сохраняет снимок содержимого вкладки в указанный файл.
:param data: Кортеж, содержащий путь к файлу и QTabWidget для сохранения.
"""
filepath, tab = data
# Создаем QPixmap с размером, равным размеру вкладки
pixmap = QPixmap(tab.size())
# Рендерим содержимое вкладки в QPixmap
tab.render(pixmap)
# Сохраняем изображение по указанному пути
pixmap.save(filepath)

View File

@ -8,14 +8,29 @@ pd.set_option('future.no_silent_downcasting', True)
class DataConverter(BaseDataConverter):
"""
Класс для преобразования данных из CSV-файлов.
Основные задачи:
- Исправление заголовков столбцов по заданному шаблону.
- Преобразование булевых значений в целочисленные.
- Обработка ошибок при конвертации и уведомление через медиатора.
"""
@staticmethod
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
try:
bool_columns = dataframe.columns[dataframe.isin([True, False]).all()]
dataframe = dataframe.astype({col: int for col in bool_columns})
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
"""
Преобразует столбцы, содержащие булевы значения, в целочисленные.
return dataframe
:param df: DataFrame с исходными данными.
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
"""
try:
# Находим столбцы, в которых все значения являются True или False
bool_columns = df.columns[df.isin([True, False]).all()]
# Приводим найденные столбцы к типу int
df = df.astype({col: int for col in bool_columns})
return df
except AttributeError as e:
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
return None
@ -27,7 +42,13 @@ class DataConverter(BaseDataConverter):
return None
@staticmethod
def _fix_headers(dataframe: pd.DataFrame) -> pd.DataFrame:
def _fix_headers(df: pd.DataFrame) -> pd.DataFrame:
"""
Приводит заголовки столбцов DataFrame к корректным именам согласно заранее заданному списку.
:param df: DataFrame с исходными заголовками.
:return: DataFrame с исправленными заголовками или None, если произошла ошибка.
"""
correct_columns = [
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
@ -37,15 +58,17 @@ class DataConverter(BaseDataConverter):
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
]
try:
# Создаем словарь соответствий: ключ - имя в нижнем регистре, значение - корректное имя
correct_mapping = {name.lower(): name for name in correct_columns}
new_columns = []
for col in dataframe.columns:
# Для каждого столбца заменяем имя, если оно присутствует в корректном отображении
for col in df.columns:
fixed_col = correct_mapping.get(col.lower(), col)
new_columns.append(fixed_col)
dataframe.columns = new_columns
# Удаляем повторяющиеся столбцы, оставляя только первое вхождение
dataframe = dataframe.loc[:, ~dataframe.columns.duplicated()]
return dataframe
df.columns = new_columns
# Удаляем повторяющиеся столбцы, оставляя первое вхождение
df = df.loc[:, ~df.columns.duplicated()]
return df
except AttributeError as e:
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
return None
@ -54,9 +77,18 @@ class DataConverter(BaseDataConverter):
return None
def convert_data(self, files: list[str]) -> None:
"""
Считывает данные из списка CSV-файлов, исправляет заголовки и преобразует булевые столбцы,
после чего уведомляет медиатора о завершении преобразования.
:param files: Список путей к CSV-файлам.
"""
try:
# Считываем данные из файлов (если путь не пустой, иначе None)
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
# Применяем исправление заголовков к каждому DataFrame
renamed_dataframes = list(map(self._fix_headers, dataframes))
# Преобразуем булевые значения для каждого DataFrame
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
except FileNotFoundError as e:
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
@ -68,4 +100,5 @@ class DataConverter(BaseDataConverter):
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
converted_dataframes = [None]
finally:
# Передаем результат медиатору
self._mediator.notify(self, converted_dataframes)

View File

@ -6,64 +6,124 @@ from base.base import BaseDirectoryMonitor, BaseFileManager, Settings
class FileManager(BaseFileManager):
"""
Менеджер файлов для работы с библиотекой путей.
Основные функции:
- Переотправка (replot) списка найденных файлов.
- Добавление нового пути (например, выбранного пользователем).
- Переключение режимов работы (создание отчёта, онлайн-мониторинг, работа с трейсами клиента).
- Обновление настроек мониторинга.
- Поиск файлов в директории с трейсами: поиск файлов .dat и .txt.
"""
def replot_all(self) -> None:
if self._paths_library is not None:
if self._mode != 3: self._mediator.notify(self, list(self._paths_library))
else: self.open_raw_traces_dir(list(self._paths_library)[0])
"""
Переотправляет все пути из библиотеки.
def open_custom_file(self, path:str) -> None:
Если текущий режим не равен 3, уведомляет медиатора со списком путей.
Если режим равен 3, открывает директорию с трейсами.
"""
if self._paths_library is not None:
if self._mode != 3:
self._mediator.notify(self, list(self._paths_library))
else:
self.open_raw_traces_dir(list(self._paths_library)[0])
def open_custom_file(self, path: str) -> None:
"""
Добавляет указанный путь в библиотеку и уведомляет медиатора.
:param path: Путь к файлу, выбранному пользователем.
"""
self._paths_library.add(path)
# Передаем уведомление с указанным путем (если путь пустой, передаем пустую строку)
self._mediator.notify(self, [path if path else ''])
def set_mode(self, num:int) -> None:
def set_mode(self, num: int) -> None:
"""
Устанавливает режим работы менеджера файлов.
Режимы:
1 - Режим создания отчёта.
2 - Режим онлайн-мониторинга папки.
3 - Режим работы с трейсами клиента.
:param num: Целое число, определяющее режим работы.
"""
match num:
case 1: # Режим создания отчета
case 1:
# Режим создания отчёта
self._monitor.stop()
self._paths_library.clear()
self._paths_library.add('')
self._mediator.notify(self, list(self._paths_library))
self._mode = 1
case 2: # Режим онлайн-мониторинга папки
case 2:
# Режим онлайн-мониторинга папки
self._monitor.init_state()
self._monitor.start()
self._mode = 2
case 3: # Режим работы с трейсами клиента
case 3:
# Режим работы с трейсами клиента
self._monitor.stop()
self._paths_library.clear()
self._mode = 3
def update_monitor_settings(self, settings:Settings) -> None:
def update_monitor_settings(self, settings: Settings) -> None:
"""
Обновляет настройки мониторинга директории.
Из объекта настроек извлекается путь к директории и интервал обновления.
Выполняется проверка существования директории и корректности интервала обновления.
При необходимости приостанавливается или возобновляется монитор.
:param settings: Объект Settings с системными настройками.
"""
directory_path = settings.system['trace_storage_path'][0]
update_time = settings.system['monitor_update_period'][0]
if not os.path.exists(directory_path):
logger.warning(f"Путь {directory_path} не существует.")
#raise FileNotFoundError(f"Путь {directory_path} не существует.")
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()
if self._monitor.isActive:
self._monitor.start()
def add_new_paths(self, paths:list) -> None:
def add_new_paths(self, paths: list) -> None:
"""
Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали,
и уведомляет медиатора о новых путях.
:param paths: Список новых путей.
"""
paths_set = set(paths)
new = self._paths_library.difference(paths_set)
self._paths_library.update(new)
self._mediator.notify(list(new))
new_paths = self._paths_library.difference(paths_set)
self._paths_library.update(new_paths)
self._mediator.notify(list(new_paths))
def open_raw_traces_dir(self, path:str) -> None:
def open_raw_traces_dir(self, path: str) -> None:
"""
Открывает указанную директорию, ищет в ней файлы с расширениями .dat и .txt,
и при их наличии инициирует предварительный рендеринг через медиатора.
:param path: Путь к директории с трейсами.
"""
self._paths_library.clear()
self._paths_library.add(path)
dat_file, txt_file = None, None
# Перебираем содержимое директории
for entry in os.listdir(path):
full_path = os.path.join(path, entry)
if os.path.isfile(full_path):
_, ext = os.path.splitext(entry)
ext = ext.lower()
@ -75,33 +135,51 @@ class FileManager(BaseFileManager):
break
if dat_file and txt_file:
# Уведомляем медиатора для запуска предварительного рендеринга трейс-контента
self._mediator.prerender_TCW(self, [dat_file, txt_file])
class DirectoryMonitor(BaseDirectoryMonitor):
"""
Мониторинг директории для поиска CSV-файлов.
Инициализирует состояние, подключает таймер обновления и отслеживает появление новых файлов.
При обнаружении новых файлов уведомляет менеджер файлов.
"""
def init_state(self):
"""
Инициализирует состояние мониторинга директории.
Проверяет, существует ли директория. Если директория не существует, генерирует ошибку.
Собирает список файлов с расширением .csv и подключает метод мониторинга к таймеру.
"""
if not os.path.exists(self._directory_path):
logger.error(f"Путь {self._directory_path} не существует.")
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
logger.error(f"Путь {self._directory_path} не существует.")
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
self._files = [
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}")

View File

@ -13,65 +13,115 @@ from base.base import (
GraphicPassport,
Settings,
BaseRawTraceProcessor
)
)
class Mediator(BaseMediator):
"""
Медиатор для организации взаимодействия между различными компонентами приложения.
def notify(self,
source: Union[BaseFileManager, BaseDataConverter, BasePointPassportFormer, BasePlotWidget, BaseController, BaseRawTraceProcessor],
data: Union[list[str], list[pd.DataFrame], list[GraphicPassport], list[QWidget], Settings, pd.DataFrame]) -> None:
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
"""
if issubclass(source.__class__, BaseFileManager):
def notify(
self,
source: Union[
BaseFileManager,
BaseDataConverter,
BasePointPassportFormer,
BasePlotWidget,
BaseController,
BaseRawTraceProcessor
],
data: Union[
list[str],
list[pd.DataFrame],
list[GraphicPassport],
list[QWidget],
Settings,
pd.DataFrame
]
) -> None:
"""
Принимает уведомление от компонента-источника и направляет данные в соответствующий модуль.
:param source: Компонент, вызвавший уведомление.
:param data: Передаваемые данные (могут быть строками, DataFrame, графическими паспортами, виджетами, Settings).
"""
# Если источник менеджер файлов: обновляем статус и запускаем конвертацию CSV
if isinstance(source, BaseFileManager):
self._controller.update_status("CSV found! Calculating...")
self._converter.convert_data(data)
if issubclass(source.__class__, BaseDataConverter):
# Если источник конвертер данных: обновляем прогресс и формируем паспорта
if isinstance(source, BaseDataConverter):
self._controller.update_progress(1)
self._passport_former.form_passports(data)
if issubclass(source.__class__, BasePointPassportFormer):
# Если источник формирователь паспортов точек: обновляем прогресс и строим графический паспорт
if isinstance(source, BasePointPassportFormer):
self._controller.update_progress(10)
self._plot.build(data)
if issubclass(source.__class__, BasePlotWidget):
# Если источник виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
if isinstance(source, BasePlotWidget):
self._controller.update_progress(100)
self._controller.send_widgets(data)
if issubclass(source.__class__, BaseController):
# Если источник контроллер: обновляем настройки у различных модулей
if isinstance(source, BaseController):
self._file_manager.update_monitor_settings(data)
self._passport_former.update_settings(data)
self._trace_processor.update_settings(data)
def prerender_TCW(self, source:Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data:Union[str, list[str], list[pd.DataFrame, dict]]) -> None:
def prerender_TCW(
self,
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data: Union[str, list[str], list[pd.DataFrame], dict]
) -> None:
"""
Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления.
if issubclass(source.__class__, BaseController):
:param source: Компонент, инициирующий предварительный рендеринг.
:param data: Передаваемые данные, могут быть строкой или списком путей/DataFrame и словарём.
"""
if isinstance(source, BaseController):
self._controller.update_progress(5)
self._file_manager.open_raw_traces_dir(data)
if issubclass(source.__class__, BaseFileManager):
if isinstance(source, BaseFileManager):
self._controller.update_progress(15)
self._trace_processor.prerender(data)
if issubclass(source.__class__, BaseRawTraceProcessor):
if isinstance(source, BaseRawTraceProcessor):
self._controller.update_progress(40)
self._plot.build_raw_trace(data)
def render_TCW(self, source:Union[BaseController, BaseRawTraceProcessor],
data:Union[str, list[pd.DataFrame, dict]] = None) -> None:
def render_TCW(
self,
source: Union[BaseController, BaseRawTraceProcessor],
data: Union[str, list[pd.DataFrame], dict] = None
) -> None:
"""
Выполняет финальный рендеринг TCW.
if issubclass(source.__class__, BaseController):
:param source: Компонент, инициирующий финальный рендеринг.
:param data: Передаваемые данные (опционально).
"""
if isinstance(source, BaseController):
self._controller.update_progress(5)
self._trace_processor.final_render()
if issubclass(source.__class__, BaseRawTraceProcessor):
if isinstance(source, BaseRawTraceProcessor):
self._controller.update_progress(5)
self._passport_former.form_customer_passport(data)
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим работы для графиков и файлового менеджера.
:param mode: Целое число, определяющее режим работы.
"""
self._plot.set_mode(mode)
self._file_manager.set_mode(mode)

View File

@ -1,99 +1,149 @@
from typing import Optional, Any
from typing import Optional, Any, Tuple, List, Dict
import numpy as np
import pandas as pd
from loguru import logger
from base.base import BasePointPassportFormer, BaseIdealDataBuilder, PointPassport, GraphicPassport, Settings, UsefulGraphData
from base.base import (
BasePointPassportFormer,
BaseIdealDataBuilder,
PointPassport,
GraphicPassport,
Settings,
UsefulGraphData
)
class PassportFormer(BasePointPassportFormer):
"""
Класс для формирования паспортов (графических и точечных) по данным трассировки.
def form_passports(self, data: list[pd.DataFrame]) -> None:
Основные возможности:
- Формирование паспортов для каждого DataFrame.
- Формирование паспорта заказчика на основе DataFrame и событий.
- Генерация событий по DataFrame.
- Построение идеальных данных для каждой точки.
- Формирование графического паспорта, включающего полезные данные и перечень паспортов точек.
Внимание: многие атрибуты (например, self._mediator, self._stages, self._clear_stage,
self._settings, self._ideal_data_cache, self._OptAlgorithm_operator_params,
self._OptAlgorithm_system_params) предполагается задавать извне (например, в базовом классе).
"""
def form_passports(self, data: List[pd.DataFrame]) -> None:
"""
Формирует паспорта для каждого DataFrame из списка.
В случае ошибки логируется сообщение и возвращается пустой список.
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
:param data: Список DataFrame с данными трассировки.
"""
try:
return_data = [self._build_from_df_only(dataframe) for dataframe in data]
passports = [self._build_from_df_only(df) for df in data]
except Exception as e:
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
return_data = []
passports = []
finally:
self._mediator.notify(self, return_data)
self._mediator.notify(self, passports)
def update_settings(self, settings: Settings):
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки формирования паспортов.
:param settings: Объект настроек.
"""
self._settings = settings
def form_customer_passport(self, data: list[pd.DataFrame, dict]) -> None:
def form_customer_passport(self, data: Tuple[pd.DataFrame, Dict, Tuple]) -> None:
"""
Формирует паспорт заказчика на основе DataFrame и словаря событий.
В случае ошибки логируется сообщение и возвращается пустой список.
Ожидается, что data содержит:
- DataFrame с данными,
- словарь событий,
- кортеж координат ME (например, (part_pos, open_pos)).
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
:param data: Кортеж из (DataFrame, события, ME_coords).
"""
try:
dataframe, events, ME_coords = data
point_quantity = len(events["Squeeze"][0])
self._modify_coord_settings(ME_coords)
data_passport = self._form_graphic_passport(dataframe, events, point_quantity)
return_data = [data_passport]
customer_passport = self._form_graphic_passport(dataframe, events, point_quantity)
result = [customer_passport]
except Exception as e:
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
return_data = []
result = []
finally:
self._mediator.notify(self, return_data)
self._mediator.notify(self, result)
def _modify_coord_settings(self, ME_coords:tuple) -> None:
def _modify_coord_settings(self, ME_coords: Tuple[List[float], List[float]]) -> None:
"""
Модифицирует настройки координат на основе переданных данных ME.
:param ME_coords: Кортеж из двух списков: (part_pos, open_pos).
"""
part_pos, open_pos = ME_coords
self._settings.operator["distance_l_2"] = open_pos
l1 = self._settings.operator["distance_l_1"]
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range (len(part_pos))]
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
@staticmethod
def _find_indexes(signal: str, dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
def _find_indexes(signal: str, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
"""
Находит индексы начала и конца этапа для указанного сигнала.
Находит индексы начала и окончания этапа для указанного сигнала.
:param signal: Имя столбца-сигнала.
:param df: DataFrame с данными.
:return: Кортеж из массивов индексов начала и окончания этапа.
"""
stage_diff = np.diff(dataframe[signal])
stage_diff = np.diff(df[signal])
start_idx = np.where(stage_diff == 1)
finish_idx = np.where(stage_diff == -1)
return start_idx[0], finish_idx[0]
@staticmethod
def _find_events(signal: str,
times: pd.Series,
dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
def _find_events(signal: str, times: pd.Series, df: pd.DataFrame) -> Tuple[List[float], List[float]]:
"""
Формирует списки времен начала и окончания событий для указанного сигнала.
"""
start_idx, finish_idx = PassportFormer._find_indexes(signal, dataframe)
Формирует списки времён начала и окончания событий для указанного сигнала.
Если у первого события не определено время начала, оно принимается за 0.
Если число стартов события больше числа финишей, последним финишем считается конец времён.
:param signal: Имя столбца-сигнала.
:param times: Series с временными метками.
:param df: DataFrame с данными.
:return: Кортеж из двух списков: (список времён начала, список времён окончания).
"""
start_idx, finish_idx = PassportFormer._find_indexes(signal, df)
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
start_idx = np.insert(start_idx, 0, 0)
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
if len(start_list) - len(end_list) == 1:
end_list.append(float(times.iloc[-1]))
return start_list, end_list
return (start_list, end_list)
def _generate_events(self,
times: pd.Series,
dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
def _generate_events(self, times: pd.Series, df: pd.DataFrame) -> Tuple[Dict[str, List[List[float]]], int]:
"""
Генерирует словарь событий для каждого этапа и определяет количество точек.
Если для основного этапа (_clear_stage) не найдено ни одного события,
логируется ошибка и возвращается пустой список.
Генерирует словарь событий для каждого этапа, используя временные метки и данные.
Также определяет общее количество точек (на основе количества событий основного этапа).
Если для основного этапа (self._clear_stage) не найдено ни одного события, возвращается пустой словарь и 0 точек.
:param times: Серия временных меток.
:param df: DataFrame с данными.
:return: Кортеж (словарь событий, количество точек).
"""
events = {}
point_quantity = 0
if self._clear_stage in self._stages:
start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
start_list, end_list = self._find_events(self._clear_stage, times, df)
point_quantity = len(start_list)
if point_quantity == 0:
logger.error("_generate_events - Не найдены события для этапа '{}'.", self._clear_stage)
return {}, 0 # Возвращаем пустой словарь и 0 точек
logger.error(f"_generate_events - Не найдены события для этапа '{self._clear_stage}'.")
return {}, 0
for stage in self._stages:
s_list, e_list = self._find_events(stage, times, dataframe)
s_list, e_list = self._find_events(stage, times, df)
temp = min(len(s_list), len(e_list))
if temp < point_quantity:
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
@ -101,34 +151,42 @@ class PassportFormer(BasePointPassportFormer):
s_list += [0] * (point_quantity - temp)
e_list += [1] * (point_quantity - temp)
events[stage] = [s_list, e_list]
return events, point_quantity
return (events, point_quantity)
def _build_ideal_data(self,
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
point_settings: Settings = None) -> dict:
def _build_ideal_data(self, idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
point_settings: Optional[Settings] = None) -> Dict:
"""
Строит идеальные данные с использованием переданного билдера.
Строит идеальные данные с использованием билдера.
:param idealDataBuilder: Класс-билдер для генерации идеальных данных.
:param point_settings: Настройки для точки.
:return: Словарь с идеальными данными для этапов.
"""
try:
self.opt_algorithm = idealDataBuilder(point_settings)
self._opt_algorithm = idealDataBuilder(point_settings)
stage_ideals = {
"Closing": self._opt_algorithm.get_closingDF(),
"Squeeze": self._opt_algorithm.get_compressionDF(),
"Welding": self._opt_algorithm.get_weldingDF(),
"Relief": self._opt_algorithm.get_openingDF(),
"Oncomming": self._opt_algorithm.get_oncomingDF(),
"Ideal cycle": self._opt_algorithm.get_cycle_time(),
"Ideal timings": self._opt_algorithm.get_ideal_timings()
"Closing": self.opt_algorithm.get_closingDF(),
"Squeeze": self.opt_algorithm.get_compressionDF(),
"Welding": self.opt_algorithm.get_weldingDF(),
"Relief": self.opt_algorithm.get_openingDF(),
"Oncomming": self.opt_algorithm.get_oncomingDF(),
"Ideal cycle": self.opt_algorithm.get_cycle_time(),
"Ideal timings": self.opt_algorithm.get_ideal_timings()
}
return stage_ideals
except Exception as e:
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
return {}
def _generate_cache_key(self,
point_settings: Settings) -> tuple[tuple[tuple[str, Any], ...], tuple[tuple[str, Any], ...]]:
def _generate_cache_key(self, point_settings: Settings) -> Tuple[Tuple[Tuple[str, Any], ...], Tuple[Tuple[str, Any], ...]]:
"""
Преобразует point_settings в хешируемый ключ для кэша.
Преобразует настройки точки в хешируемый ключ для кэша.
Использует только те параметры оператора и системы, которые присутствуют в
соответствующих наборах (self._OptAlgorithm_operator_params и self._OptAlgorithm_system_params).
:param point_settings: Объект настроек для точки.
:return: Кортеж из двух frozenset с параметрами.
"""
try:
operator_tuple = frozenset(
@ -146,17 +204,20 @@ class PassportFormer(BasePointPassportFormer):
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
return ((), ())
def _build_from_df_only(self, dataframe: pd.DataFrame) -> GraphicPassport:
def _build_from_df_only(self, df: pd.DataFrame) -> Optional[GraphicPassport]:
"""
Строит GraphicPassport на основе одного DataFrame.
Если DataFrame содержит необходимые столбцы, события генерируются;
иначе используется альтернативное определение point_quantity.
Если DataFrame содержит необходимые столбцы (определяемые в self._stages),
генерируются события; иначе используется альтернативное определение количества точек.
Если количество точек равно нулю, логируется ошибка и возвращается None.
:param df: DataFrame с данными.
:return: Объект GraphicPassport или None в случае ошибки.
"""
try:
if (dataframe is not None and
set(self._stages).issubset(set(dataframe.columns.tolist()))):
events, point_quantity = self._generate_events(dataframe["time"], dataframe)
if df is not None and set(self._stages).issubset(set(df.columns.tolist())):
events, _ = self._generate_events(df["time"], df)
point_quantity = len(events["Welding"][0])
if point_quantity == 0:
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
@ -166,22 +227,34 @@ class PassportFormer(BasePointPassportFormer):
key = list(self._settings.operator.keys())[0]
point_quantity = len(self._settings.operator[key])
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
passport = self._form_graphic_passport(dataframe, events, point_quantity)
passport = self._form_graphic_passport(df, events, point_quantity)
return passport
except Exception as e:
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
return None
def _form_graphic_passport(self,
dataframe: pd.DataFrame,
events: dict,
point_quantity: int) -> GraphicPassport:
def _form_graphic_passport(self, df: pd.DataFrame, events: Dict, point_quantity: int) -> Optional[GraphicPassport]:
"""
Формирует графический паспорт с полезными данными и списком PointPassport.
Формирует графический паспорт, включающий полезные данные и список PointPassport.
Для каждой из точек:
- Извлекаются настройки оператора для данной точки.
- Формируется временной интервал и события.
- Рассчитываются идеальные и полезные данные.
- Создаётся объект PointPassport, который добавляется в список.
:param df: DataFrame с данными.
:param events: Словарь событий, сгенерированных для этапов.
:param point_quantity: Количество точек.
:return: Объект GraphicPassport или None в случае ошибки.
"""
try:
system_settings = {key: value[0] for key, value in self._settings.system.items()}
graphic_passport = GraphicPassport(dataframe, [], self._form_graphic_useful_data(system_settings))
graphic_passport = GraphicPassport(
df,
[],
self._form_graphic_useful_data(system_settings)
)
for i in range(point_quantity):
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
@ -197,9 +270,12 @@ class PassportFormer(BasePointPassportFormer):
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
return None
def _form_graphic_useful_data(self, system_settings: dict) -> dict:
def _form_graphic_useful_data(self, system_settings: Dict) -> UsefulGraphData:
"""
Формирует словарь полезных данных для графика.
Формирует словарь полезных данных для графического паспорта.
:param system_settings: Словарь системных настроек.
:return: Объект UsefulGraphData.
"""
try:
tesla_time = sum(self._settings.operator.get("Tesla summary time", []))
@ -211,11 +287,14 @@ class PassportFormer(BasePointPassportFormer):
return useful_data
except Exception as e:
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
return {}
return UsefulGraphData()
def _form_point_useful_data(self, operator_settings: dict) -> dict:
def _form_point_useful_data(self, operator_settings: Dict) -> Dict:
"""
Формирует полезные данные для отдельной точки.
:param operator_settings: Словарь настроек оператора для точки.
:return: Словарь с полезными данными (толщина, позиция детали, сила) или пустой словарь.
"""
try:
useful_data = {
@ -228,25 +307,36 @@ class PassportFormer(BasePointPassportFormer):
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
return {}
def _form_point_ideal_data(self, point_settings: Settings) -> dict:
def _form_point_ideal_data(self, point_settings: Settings) -> Dict:
"""
Формирует идеальные данные для точки с использованием кэша.
Формирует идеальные данные для отдельной точки с использованием кэша.
Генерируется кэш-ключ из настроек точки, затем либо извлекаются ранее рассчитанные
данные, либо вычисляются новые с помощью билдера.
:param point_settings: Настройки точки.
:return: Словарь с идеальными данными или пустой словарь в случае ошибки.
"""
try:
cache_key = self._generate_cache_key(point_settings)
ideal_data = self._ideal_data_cashe.get(
ideal_data = self._ideal_data_cache.get(
cache_key,
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
)
self._ideal_data_cashe[cache_key] = ideal_data
self._ideal_data_cache[cache_key] = ideal_data
return ideal_data
except Exception as e:
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
return {}
def _get_operator_settings_part(self, idx: int) -> dict:
def _get_operator_settings_part(self, idx: int) -> Dict:
"""
Извлекает часть настроек оператора для конкретного индекса.
Если индекс выходит за пределы списка, используется первый элемент.
:param idx: Индекс точки.
:return: Словарь настроек оператора для данной точки.
"""
try:
operator_settings = {
@ -258,14 +348,20 @@ class PassportFormer(BasePointPassportFormer):
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
return {}
def _form_point_events(self, events: dict, idx: int) -> list[list, dict]:
def _form_point_events(self, events: Dict, idx: int) -> Tuple[Optional[List[float]], Optional[Dict[str, List[float]]]]:
"""
Формирует временной интервал и события для отдельной точки.
Если событий нет, возвращает (None, None).
:param events: Словарь с событиями для всех этапов.
:param idx: Индекс точки.
:return: Кортеж (timeframe, point_events) или (None, None) в случае ошибки.
"""
try:
timeframe, point_events = None, None
if events is not None:
# Если первое событие основного этапа начинается с 0, сдвигаем индекс
idx_shift = idx + 1 if events[self._stages[-1]][0][0] == 0 else idx
timeframe = [events[self._stages[0]][0][idx], events[self._stages[-1]][1][idx_shift]]
point_events = {key: [value[0][idx], value[1][idx]] for key, value in events.items()}
@ -276,8 +372,18 @@ class PassportFormer(BasePointPassportFormer):
class IdealDataBuilder(BaseIdealDataBuilder):
"""
Класс для построения идеальных данных по этапам.
Реализует методы получения DataFrame для различных этапов:
закрытия, сжатия, открытия, движения, сварки,
а также метод получения идеальных временных интервалов.
"""
def get_closingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа закрытия.
"""
try:
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
except Exception as e:
@ -285,6 +391,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
return pd.DataFrame()
def get_compressionDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сжатия.
"""
try:
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
except Exception as e:
@ -292,6 +401,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
return pd.DataFrame()
def get_openingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа открытия.
"""
try:
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
except Exception as e:
@ -299,6 +411,9 @@ class IdealDataBuilder(BaseIdealDataBuilder):
return pd.DataFrame()
def get_oncomingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения.
"""
try:
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
except Exception as e:
@ -306,14 +421,21 @@ class IdealDataBuilder(BaseIdealDataBuilder):
return pd.DataFrame()
def get_weldingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сварки.
Используется функция calcPhaseGrow с небольшим сдвигом времени.
Значения масштабируются (умножаются на 1000) для перевода в нужные единицы.
"""
try:
data = []
# Используем небольшое смещение для расчёта сварки
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
points_num = 5
for i in range (points_num+1):
for i in range(points_num + 1):
data.append({
"time": self.welding_time*i/points_num,
"time": self.welding_time * i / points_num,
"Position FE": X1,
"Position ME": X2,
"Rotor Speed FE": V1,
@ -325,7 +447,11 @@ class IdealDataBuilder(BaseIdealDataBuilder):
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
return pd.DataFrame()
def get_ideal_timings(self) -> list[float]:
def get_ideal_timings(self) -> List[float]:
"""
Получает список идеальных временных интервалов для этапов.
Список включает: tclose, tgrow, welding_time, getMarkOpen(), tmovement.
"""
try:
data = self.Ts
ideal_timings = [
@ -342,21 +468,30 @@ class IdealDataBuilder(BaseIdealDataBuilder):
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
"""
Получает данные до указанного времени с шагом, определяемым mul.
Получает данные до указанного времени (end_timestamp) с шагом, определяемым параметром mul.
Для каждого шага рассчитываются значения с использованием переданной функции func.
В конце добавляется строка с точным значением end_timestamp.
:param end_timestamp: Время окончания получения данных.
:param func: Функция для расчёта значений в зависимости от времени.
:return: DataFrame с рассчитанными данными.
"""
try:
data = []
# Генерируем данные с шагом 1/mul
for i in range(0, int(end_timestamp * self.mul) + 1):
time = i / self.mul
X1, X2, V1, V2, F = func(time)
time_val = i / self.mul
X1, X2, V1, V2, F = func(time_val)
data.append({
"time": time,
"time": time_val,
"Position FE": X1 * 1000,
"Position ME": X2 * 1000,
"Rotor Speed FE": V1 * 1000,
"Rotor Speed ME": V2 * 1000,
"Force": F
})
# Добавляем финальную строку с end_timestamp
X1, X2, V1, V2, F = func(end_timestamp)
data.append({
"time": end_timestamp,

View File

@ -1,2 +1,2 @@
from .plotter import PlotWidget
from .settings_window import settingsWindow
from .settings_window import SettingsWindow

View File

@ -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:

View File

@ -1,31 +1,32 @@
import copy
import traceback
import sys
from typing import Optional, Tuple, Callable, List, Any
from typing import Optional, Tuple, Callable, List, Any, Dict
from dataclasses import dataclass, field
from PyQt5.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QGraphicsRectItem,
QSpacerItem,
QSizePolicy
)
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGraphicsRectItem, QSpacerItem, QSizePolicy
)
from PyQt5.QtCore import Qt
from loguru import logger
import pyqtgraph as pg
import pandas as pd
import numpy as np
from base.base import BasePlotWidget, GraphicPassport, PlotItems, PointPassport, UsefulGraphData, BaseController
from base.base import (
BasePlotWidget, GraphicPassport, PlotItems, PointPassport,
UsefulGraphData, BaseController
)
from utils.json_tools import read_json
from utils import qt_settings as qts
# =============================================================================
# Дата-класс для хранения временных характеристик канала
# =============================================================================
@dataclass
class ChannelTimings():
class ChannelTimings:
shift: float = 0
TWC_time: float = 0.0
ideal_time: float = 0.0
@ -33,28 +34,45 @@ class ChannelTimings():
TWC_start: float = 0.0
TWC_end: float = 0.0
worst_performance: float = 2
worst_timeframe: list = field(default_factory=lambda: [0, 0])
worst_timeframe: List[float] = field(default_factory=lambda: [0, 0])
# =============================================================================
# Класс PlotWidget построение графических виджетов на основе графических паспортов
# =============================================================================
class PlotWidget(BasePlotWidget):
"""
Виджет построения графиков. На основе полученных графических паспортов
создаёт набор виджетов (для отображения нескольких наборов данных),
"""
def __init__(self, controller:BaseController):
def __init__(self, controller: BaseController):
"""
Инициализирует PlotWidget, читая параметры структуры графиков из JSON.
"""
super().__init__(controller=controller)
self._plt_structures = read_json("params/plot_structure_params.json")
self._plt_channels = None
self._plt_channels = None # Будет установлен в зависимости от режима
def build(self, data: list[GraphicPassport]) -> None:
def build(self, data: List[GraphicPassport]) -> None:
"""
Создает набор виджетов по предоставленному списку данных.
Создает набор виджетов на основе списка графических паспортов.
При ошибке выводится информационная метка об ошибке.
:param data: Список объектов GraphicPassport с данными для построения графиков.
"""
try:
self._datalen = len(data)
widgets_datapack = [self._build_widget(data_sample) for self._datastep, data_sample in enumerate(data)]
widgets_datapack = []
# Проходим по каждому графическому паспорту и обновляем текущий индекс
for idx, passport in enumerate(data):
self._datastep = idx # Обновляем текущий шаг для обновления статуса
widget = self._build_widget(passport)
widgets_datapack.append(widget)
except (KeyError, ValueError, TypeError) as e:
logger.error(f"Возникла проблема при сборке данных графика: {e}")
logger.error(f"Ошибка при сборке данных графика: {e}")
logger.debug(traceback.format_exc())
error_label = QLabel("Произошла ошибка при формировании графиков. Пожалуйста, проверьте корректность данных.")
error_label = QLabel("Произошла ошибка при формировании графиков. Проверьте корректность данных.")
widgets_datapack = [error_label]
except Exception as e:
logger.error(f"Непредвиденная ошибка при формировании графиков: {e}")
@ -64,18 +82,24 @@ class PlotWidget(BasePlotWidget):
finally:
self._mediator.notify(self, widgets_datapack)
def build_raw_trace(self, data:list[pd.DataFrame, dict]) -> None:
def build_raw_trace(self, data: Tuple[pd.DataFrame, dict]) -> None:
"""
Создаёт один виджет с одним графиком, где представлены все данные
Создаёт виджет с одним графиком, отображающим все данные сырой трассировки.
:param data: Кортеж из DataFrame с трассировочными данными и словаря событий.
"""
container_widget, container_layout, pyqt_container = self._generate_widget_container()
plot = self._build_raw_plotitem(data, pyqt_container)
container_layout.addWidget(plot)
container_widget.setProperty("pyqt_container", pyqt_container)
self._mediator.notify(self, [container_widget])
def set_mode(self, mode:int) -> None:
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим построения графиков, выбирая соответствующий набор каналов.
:param mode: Режим работы (1 Online Path Scanner, 2 Report Editor, 3 Client Trace Watcher).
"""
match mode:
case 1:
self._plt_channels = self._plt_structures["Online Path Scanner"]
@ -84,48 +108,60 @@ class PlotWidget(BasePlotWidget):
case 3:
self._plt_channels = self._plt_structures["Client Trace Watcher"]
def _build_raw_plotitem(self,
data:list[pd.DataFrame, dict],
pyqt_container:PlotItems) -> pg.GraphicsLayoutWidget:
def _build_raw_plotitem(self, data: Tuple[pd.DataFrame, dict],
pyqt_container: PlotItems) -> pg.GraphicsLayoutWidget:
"""
Строит график необработанных трейсов клиента. Добавляет линии для каждого канала и области для событий.
:param data: Кортеж (dataframe, events)
:param pyqt_container: Контейнер для хранения ссылок на QT-объекты графиков.
:return: Виджет GraphicsLayoutWidget с построенным графиком.
"""
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
dataframe, events = data
channels = dataframe.columns.to_list()
channels = dataframe.columns.tolist()
# Добавляем линии для каждого канала
for i, channel in enumerate(channels):
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen = qts.colors[i], fast = True)
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen=qts.colors[i], fast=True)
legend.addItem(plot, channel)
pyqt_container.curves["real"].setdefault(channel, {})
pyqt_container.curves["real"][channel] = plot
pyqt_container.curves.setdefault("real", {})[channel] = plot
for i in range(len(events["Squeeze"][0])):
# Для каждого события этапа Squeeze добавляем регионы
for i in range(len(events.get("Squeeze", [[], []])[0])):
point_events = {}
for key, item in events.items():
point_events[key] = [item[0][i], item[1][i]]
PlotItemGenerator._add_stage_regions(self._stage_colors, plot_item, point_events, pyqt_container.regions,20)
PlotItemGenerator._add_stage_regions(self._stage_colors, plot_item, point_events, pyqt_container.regions, 20)
plot_layout = pg.GraphicsLayoutWidget()
plot_layout.addItem(plot_item)
return plot_layout
def _build_performance_label(self,
timings: ChannelTimings,
qt_items: dict) -> QWidget:
def _build_performance_label(self, timings: ChannelTimings, qt_items: Dict) -> QWidget:
"""
Добавляет QLabel с информацией о производительности.
Создает QLabel с информацией о производительности (сокращение длительности, идеальное значение, КДИП).
:param timings: Объект ChannelTimings с рассчитанными временными характеристиками.
:param qt_items: Словарь для сохранения ссылок на созданные виджеты.
:return: Виджет с меткой производительности.
"""
tesla_TWC = round((1 - timings.TWC_time/timings.client_time)*100, 2) if timings.client_time else 0.0
tesla_ideal = round((1 - timings.ideal_time/timings.client_time)*100, 2) if timings.client_time else 0.0
TWC_ideal = round((timings.ideal_time/timings.TWC_time)*100, 2) if timings.TWC_time else 0.0
tesla_TWC = round((1 - timings.TWC_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
tesla_ideal = round((1 - timings.ideal_time / timings.client_time) * 100, 2) if timings.client_time else 0.0
TWC_ideal = round((timings.ideal_time / timings.TWC_time) * 100, 2) if timings.TWC_time else 0.0
label_widget = QWidget()
label_layout = QHBoxLayout(label_widget)
start_label = QLabel("Сокращение длительности: ")
real_label = QLabel(f"фактическое = {tesla_TWC} % ")
if not tesla_TWC or not timings.TWC_time: real_label.setVisible(False)
if not tesla_TWC or not timings.TWC_time:
real_label.setVisible(False)
ideal_label = QLabel(f"идеальное = {tesla_ideal} % ")
if not tesla_ideal: ideal_label.setVisible(False)
if not tesla_ideal:
ideal_label.setVisible(False)
kdip_label = QLabel(f"КДИП = {TWC_ideal}% ")
if not TWC_ideal: kdip_label.setVisible(False)
if not TWC_ideal:
kdip_label.setVisible(False)
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
@ -143,80 +179,114 @@ class PlotWidget(BasePlotWidget):
def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget:
"""
Собирает графический виджет для одного набора данных.
Генерирует контейнер, создаёт CustomPlotLayout для построения графика,
добавляет, при необходимости, метку производительности.
:param graphic_passport: Объект GraphicPassport с данными для построения графика.
:return: Готовый QWidget для отображения.
"""
container_widget, container_layout, pyqt_container = self._generate_widget_container()
plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self)
plot_layout.build(pyqt_container, self._plt_channels)
if plot_layout.property("performance"):
perf_widget = self._build_performance_label(
plot_layout.property("performance"),
pyqt_container.qt_items
)
perf_widget = self._build_performance_label(plot_layout.property("performance"), pyqt_container.qt_items)
container_layout.addWidget(perf_widget)
container_layout.addWidget(plot_layout)
container_widget.setProperty("pyqt_container", pyqt_container)
return container_widget
@staticmethod
def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]:
"""
Создает контейнер для виджета графика, его макет и объект PlotItems для хранения ссылок.
:return: Кортеж (контейнер, вертикальный макет, PlotItems).
"""
container_widget = QWidget()
container_layout = QVBoxLayout(container_widget)
pyqt_container = PlotItems({"real":{}, "ideal":{}}, {"real":{}, "ideal":{}}, {})
return (container_widget, container_layout, pyqt_container)
pyqt_container = PlotItems({"real": {}, "ideal": {}}, {"real": {}, "ideal": {}}, {})
return container_widget, container_layout, pyqt_container
def _update_status(self, widgsteps:int, pointsteps:int, cur_widg:int, cur_point:int):
def _update_status(self, widget_steps: int, point_steps: int, cur_widget: int, cur_point: int) -> None:
"""
Вычисляет текущее значение прогресса и обновляет его через контроллер.
:param widget_steps: Общее число шагов по виджетам.
:param point_steps: Общее число точек в виджете.
:param cur_widget: Текущий номер виджета.
:param cur_point: Текущий номер точки внутри виджета.
"""
if self._datalen:
sycle_start = self._datastep/self._datalen*100 + 1
period1 = 99/self._datalen
cycle_progress = self._datastep / self._datalen * 100 + 1
period1 = 99 / self._datalen
else:
sycle_start = 1
cycle_progress = 1
period1 = 100
period2 = period1/widgsteps if widgsteps != 0 else period1
period3 = period2/pointsteps if pointsteps != 0 else period2
period2 = period1 / widget_steps if widget_steps != 0 else period1
period3 = period2 / point_steps if point_steps != 0 else period2
progress = int(sycle_start + period2*cur_widg + period3*cur_point)
progress = int(cycle_progress + period2 * cur_widget + period3 * cur_point)
self._controller.update_progress(progress)
# =============================================================================
# Класс CustomPlotLayout кастомное размещение графических элементов
# =============================================================================
class CustomPlotLayout(pg.GraphicsLayoutWidget):
def __init__(self,
graphic_passport: GraphicPassport,
widget_steps: int,
colors: dict,
parent: PlotWidget = None) -> None:
"""
Кастомный виджет для размещения графиков, построенных с помощью PlotItemGenerator.
Он формирует графики по каналам, связывает их между собой (XLink)
и добавляет навигатор для общего обзора.
"""
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
super().__init__()
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
self.setProperty("performance", None)
def build(self, pyqt_container:PlotItems, plt_channels:dict) -> None:
def build(self, pyqt_container: PlotItems, plt_channels: Dict) -> None:
"""
Строит графики для каждого канала на основе параметров из plt_channels.
Затем добавляет навигатор (NavigatorPlot) для синхронизации обзора.
:param pyqt_container: Контейнер для хранения ссылок на графические объекты.
:param plt_channels: Словарь с описаниями каналов и их настройками.
"""
main_plot = None
for widget_num, (channel, description) in enumerate(plt_channels.items()):
plot_item, plot_timings = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container)
if widget_num == 0:
main_plot = plot_item
else:
plot_item.setXLink(main_plot)
if description["Settings"].get("performance", False):
self.setProperty("performance", plot_timings)
self.addItem(plot_item, widget_num, 0)
# Если задана производительность, получаем объект ChannelTimings
timings = ChannelTimings()
if self.property('performance'):timings:ChannelTimings = self.property('performance')
if self.property('performance'):
timings = self.property('performance')
navigator = NavigatorPlot(timings.worst_timeframe, main_plot)
if navigator is not None:
self.addItem(navigator, widget_num+1, 0)
self.addItem(navigator, widget_num + 1, 0)
# =============================================================================
# Класс PlotItemGenerator генерация элементов графика и добавление регионов
# =============================================================================
class PlotItemGenerator:
"""
Генератор графических элементов (PlotItem) для построения графиков.
def __init__(self,
graphic_passport: GraphicPassport,
widget_steps: int,
colors: dict, parent:PlotWidget = None) -> None:
На основе графического паспорта создаются линии, регионы для этапов, добавляются
идеальные сигналы и рассчитываются показатели производительности.
"""
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
self._stage_colors = colors
self._ideal_mode = graphic_passport.dataframe is None
self._parent = parent
@ -228,46 +298,51 @@ class PlotItemGenerator:
dataframe_headers = []
point_steps = 1
self._datapack = [
graphic_passport.dataframe,
dataframe_headers,
graphic_passport.useful_data,
graphic_passport.points_pocket,
widget_steps,
point_steps
]
# Используем словарь для более понятной организации данных
self._datapack: Dict[str, Any] = {
"dataframe": graphic_passport.dataframe,
"headers": dataframe_headers,
"useful_data": graphic_passport.useful_data,
"points_pocket": graphic_passport.points_pocket,
"widget_steps": widget_steps,
"point_steps": point_steps
}
def generate_plot_item(self,
widget_num: int,
channel: str,
description: dict[str, Any],
pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]:
def generate_plot_item(self, widget_num: int, channel: str, description: Dict[str, Any],
pyqt_container: PlotItems) -> Tuple[pg.PlotItem, ChannelTimings]:
"""
Генерирует PlotItem для заданного канала с добавлением регионов, компенсаций и идеальных данных.
dataframe, dataframe_headers, useful_data, points_pocket, widget_steps, point_steps = self._datapack
:param widget_num: Номер текущего виджета.
:param channel: Имя канала.
:param description: Словарь настроек для данного канала.
:param pyqt_container: Контейнер для хранения ссылок на объекты графиков.
:return: Кортеж из созданного PlotItem и объекта ChannelTimings с рассчитанными параметрами.
"""
dp = self._datapack
dataframe = dp["dataframe"]
useful_data = dp["useful_data"]
points_pocket = dp["points_pocket"]
widget_steps = dp["widget_steps"]
point_steps = dp["point_steps"]
# Инициализируем PlotItem и легенду
plot_item, legend = self._init_plot_item(title=channel)
settings:dict = description["Settings"]
settings: Dict = description["Settings"]
timings = ChannelTimings()
timings.client_time = useful_data.client_time
ideal_df = pd.DataFrame({})
# TODO: рассчитать корректный параметр range
# При необходимости зеркальное отражение данных для ME
if settings.get("mirror ME", False) and not self._ideal_mode:
dataframe = self._shift_data(
"ME",
description["Real_signals"],
dataframe,
lambda x: useful_data.range_ME-x
)
dataframe = self._shift_data("ME", description["Real_signals"], dataframe,
lambda x: useful_data.range_ME - x)
# Итерация по точкам
for cur_point, data in enumerate(points_pocket):
point_data: PointPassport = data
# Итерация по точкам паспорта
for cur_point, point_data in enumerate(points_pocket):
ideal_data = copy.deepcopy(point_data.ideal_data)
is_last = (cur_point == len(points_pocket) - 1)
is_first = (cur_point == 0)
if self._ideal_mode:
timings, point_data.events, point_data.timeframe = self._generate_synthetic_events(timings, ideal_data)
else:
@ -276,43 +351,45 @@ class PlotItemGenerator:
k_hardness = useful_data.k_hardness
signals = description["Real_signals"]
dataframe = self._apply_force_compensation(force, k_hardness, dataframe, point_data.timeframe, signals)
if settings.get("stages", False):
self._add_stage_regions(self._stage_colors, plot_item, point_data.events, pyqt_container.regions, 75)
if settings.get("force accuracy", False):
force = point_data.useful_data["force"]
self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item)
if settings.get("ideals", False) and settings.get("mirror ME", False):
for stage in point_data.events.keys():
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage], lambda x: useful_data.range_ME-x)
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage],
lambda x: useful_data.range_ME - x)
if settings.get("workpiece", False):
self._add_workpiece(point_data, plot_item)
if settings.get("ideals", False):
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events, pyqt_container.regions, 100)
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events,
pyqt_container.regions, 100)
ideal_df = self._modify_ideal_df(ideal_df, ideal_data, point_data.events)
if settings.get("performance", False):
timings = self._calc_performance(timings, point_data, ideal_data, is_first, is_last)
# Обновляем статус через родительский PlotWidget
self._parent._update_status(widget_steps, point_steps, widget_num, cur_point)
# Добавляем реальные сигналы
# Добавляем идеальные сигналы (если указано в настройках)
if settings.get("ideals", False):
self._add_signals(plot_item, ideal_df, description["Ideal_signals"], legend, pyqt_container.curves["ideal"])
# Добавляем реальные сигналы, если не включен режим идеала
if not self._ideal_mode:
self._add_signals(plot_item, dataframe, description["Real_signals"], legend, pyqt_container.curves["real"])
return (plot_item, timings)
return plot_item, timings
@staticmethod
def _shift_data(valid_str: str,
signals: list[dict],
dataframe: pd.DataFrame,
func: Callable) -> pd.DataFrame:
def _shift_data(valid_str: str, signals: List[Dict], dataframe: pd.DataFrame, func: Callable) -> pd.DataFrame:
"""
Применяет заданную функцию (например, смещение) к столбцам, удовлетворяющим условию.
:param valid_str: Подстрока, по которой определяется, к каким сигналам применять функцию.
:param signals: Список описаний сигналов.
:param dataframe: DataFrame с данными.
:param func: Функция, применяемая к значениям.
:return: Измененный DataFrame.
"""
keys = dataframe.keys()
for signal in signals:
if valid_str in signal["name"] and signal["name"] in keys:
@ -320,60 +397,73 @@ class PlotItemGenerator:
return dataframe
@staticmethod
def _init_plot_item(title: str) -> tuple[pg.PlotItem, pg.LegendItem]:
def _init_plot_item(title: str) -> Tuple[pg.PlotItem, pg.LegendItem]:
"""
Инициализирует PlotItem с заданным заголовком, включает сетку и легенду.
:param title: Заголовок графика.
:return: Кортеж (PlotItem, LegendItem).
"""
plot_item = pg.PlotItem(title=title)
# Подписываемся на изменение диапазона оси X для автоматической даунсэмплинга
plot_item.sigXRangeChanged.connect(lambda: PlotItemGenerator._update_plots_downsample(plot_item))
# Оптимизация отображения графиков
plot_item.showGrid(x=True, y=True)
plot_item.setClipToView(True)
legend = plot_item.addLegend(offset=(70, 20))
return plot_item, legend
@staticmethod
def _create_stage_region(colors:dict,
stage: str,
start_timestamp: float,
finish_timestamp: float,
def _create_stage_region(colors: Dict, stage: str, start_timestamp: float, finish_timestamp: float,
transparency: int) -> Optional[pg.LinearRegionItem]:
"""
Создает регион для определённого этапа, если заданы временные рамки.
Создает регион (LinearRegionItem) для заданного этапа, если заданы границы.
:param colors: Словарь с цветами для этапов.
:param stage: Имя этапа.
:param start_timestamp: Время начала этапа.
:param finish_timestamp: Время окончания этапа.
:param transparency: Значение прозрачности (alpha).
:return: Объект LinearRegionItem или None.
"""
if start_timestamp is not None and finish_timestamp is not None:
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
color = colors.get(stage, [100, 100, 100, 100])
# Устанавливаем кисть с заданной прозрачностью
region.setBrush(pg.mkBrush(color[:3] + [transparency]))
return region
return None
@staticmethod
def _add_stage_regions(colors:dict,
plot_item: pg.PlotItem,
point_events: dict,
reg_items: dict,
transparency: int = 75) -> None:
def _add_stage_regions(colors: Dict, plot_item: pg.PlotItem, point_events: Dict, reg_items: Dict, transparency: int = 75) -> None:
"""
Добавляет регионы для реальных этапов
Добавляет регионы для реальных этапов на PlotItem.
:param colors: Словарь с цветами для этапов.
:param plot_item: PlotItem, на который добавляются регионы.
:param point_events: Словарь событий с временными рамками.
:param reg_items: Словарь для хранения созданных регионов.
:param transparency: Прозрачность регионов.
"""
stages = point_events.keys()
#if all(stage in dataframe_headers for stage in stages):
for stage in stages:
start_t, end_t = point_events[stage]
for stage, times in point_events.items():
start_t, end_t = times
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
if region is not None:
region.setZValue(-20)
plot_item.addItem(region)
reg_items["real"].setdefault(stage, [])
reg_items["real"][stage].append(region)
reg_items.setdefault("real", {}).setdefault(stage, []).append(region)
@staticmethod
def _add_ideal_stage_regions(colors: dict,
plot_item: pg.PlotItem,
ideal_data: dict[str, Any],
point_events: dict[str, list[float]],
reg_items: dict,
transparency: int = 125) -> None:
def _add_ideal_stage_regions(colors: Dict, plot_item: pg.PlotItem, ideal_data: Dict[str, Any],
point_events: Dict[str, List[float]], reg_items: Dict, transparency: int = 125) -> None:
"""
Добавляет регионы для идеальных этапов.
:param colors: Словарь с цветами.
:param plot_item: PlotItem для добавления регионов.
:param ideal_data: Словарь с идеальными данными, включая "Ideal timings".
:param point_events: События с временными рамками.
:param reg_items: Словарь для хранения созданных регионов.
:param transparency: Прозрачность регионов.
"""
ideal_timings = ideal_data["Ideal timings"]
stages = list(point_events.keys())
@ -384,15 +474,19 @@ class PlotItemGenerator:
if region:
region.setZValue(-10)
plot_item.addItem(region)
reg_items["ideal"].setdefault(stage, [])
reg_items["ideal"][stage].append(region)
reg_items.setdefault("ideal", {}).setdefault(stage, []).append(region)
@staticmethod
def _modify_ideal_df(ideal_df: pd.DataFrame,
ideal_data: dict[str, Any],
point_events: dict[str, list[float]]) -> None:
def _modify_ideal_df(ideal_df: pd.DataFrame, ideal_data: Dict[str, Any], point_events: Dict[str, List[float]]) -> pd.DataFrame:
"""
Добавляет идеальные сигналы для каждого этапа.
Добавляет идеальные сигналы для каждого этапа к DataFrame.
Если ideal_df не пуст, добавляет разделитель между сигналами.
:param ideal_df: DataFrame с идеальными данными.
:param ideal_data: Словарь с идеальными данными для этапов.
:param point_events: События с временными рамками.
:return: Обновленный DataFrame с идеальными сигналами.
"""
for stage in point_events.keys():
if not ideal_df.empty:
@ -400,20 +494,23 @@ class PlotItemGenerator:
separator_row = {col: np.nan if col != "time" else last_time + 0.01 for col in ideal_df.columns}
separator_df = pd.DataFrame([separator_row])
worker_df = ideal_data[stage].copy(deep=True)
worker_df["time"] = worker_df["time"]+point_events[stage][0]
worker_df["time"] = worker_df["time"] + point_events[stage][0]
ideal_df = pd.concat([ideal_df, separator_df, worker_df], ignore_index=True)
else:
ideal_df = ideal_data[stage].copy()
return ideal_df
@staticmethod
def _add_signals(plot_item: pg.PlotItem,
dataframe: pd.DataFrame,
real_signals: list[dict[str, Any]],
legend: pg.LegendItem,
curve_items: dict) -> None:
def _add_signals(plot_item: pg.PlotItem, dataframe: pd.DataFrame, real_signals: List[Dict[str, Any]],
legend: pg.LegendItem, curve_items: Dict) -> None:
"""
Добавляет реальные сигналы из dataframe на виджет.
Добавляет сигналы из DataFrame на PlotItem.
:param plot_item: PlotItem для добавления сигналов.
:param dataframe: DataFrame с данными.
:param real_signals: Список описаний сигналов, содержащих имя, ручку (pen) и т.д.
:param legend: Легенда для PlotItem.
:param curve_items: Словарь для хранения созданных объектов графиков.
"""
dataframe_headers = dataframe.columns.tolist()
for signal in real_signals:
@ -421,41 +518,60 @@ class PlotItemGenerator:
plot = plot_item.plot(dataframe["time"], dataframe[signal["name"]], pen=signal["pen"], fast=True)
plot.setZValue(0)
legend.addItem(plot, signal["name"])
curve_items.setdefault(signal["name"], {})
curve_items.setdefault(signal["name"], {}) # гарантируем, что ключ существует
curve_items[signal["name"]] = plot
@staticmethod
def _update_plots_downsample(plot_item:pg.PlotItem):
def _update_plots_downsample(plot_item: pg.PlotItem):
"""
Настраивает даунсэмплинг данных на PlotItem в зависимости от видимого диапазона по оси X.
:param plot_item: PlotItem, для которого устанавливается даунсэмплинг.
"""
visible_range = plot_item.getViewBox().viewRange()[0]
diapason = visible_range[1] - visible_range[0]
if diapason >= 30: plot_item.setDownsampling(ds=50, auto=True, mode='peak')
elif diapason >= 10: plot_item.setDownsampling(ds=20, auto=True, mode='peak')
elif diapason >=4: plot_item.setDownsampling(ds=10, auto=True, mode='peak')
else: plot_item.setDownsampling(ds=1, auto=True, mode='peak')
if diapason >= 30:
plot_item.setDownsampling(ds=50, auto=True, mode='peak')
elif diapason >= 10:
plot_item.setDownsampling(ds=20, auto=True, mode='peak')
elif diapason >= 4:
plot_item.setDownsampling(ds=10, auto=True, mode='peak')
else:
plot_item.setDownsampling(ds=1, auto=True, mode='peak')
@staticmethod
def _add_force_accuracy_region(event:list,
force: float,
plot_item:pg.PlotItem) -> None:
def _add_force_accuracy_region(event: List[float], force: float, plot_item: pg.PlotItem) -> None:
"""
Добавляет прямоугольную область для отображения точности силы на PlotItem.
:param event: Список [начало, конец] события.
:param force: Значение силы.
:param plot_item: PlotItem для добавления прямоугольника.
"""
modifier = 0.05
x1 = event[0]
dx = event[1] - x1
y1 = force*(1-modifier)
dy = force*(2*modifier)
y1 = force * (1 - modifier)
dy = force * (2 * modifier)
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
rect_item.setZValue(-5)
rect_item.setBrush(pg.mkBrush((0,255,0, 50)))
rect_item.setBrush(pg.mkBrush((0, 255, 0, 50)))
rect_item.setPen(pg.mkPen('black', width=0))
plot_item.addItem(rect_item)
@staticmethod
def _add_workpiece(point_data:PointPassport,
plot_item: pg.PlotItem) -> None:
def _add_workpiece(point_data: PointPassport, plot_item: pg.PlotItem) -> None:
"""
Добавляет область, обозначающую заготовку, на PlotItem.
:param point_data: Объект PointPassport с данными точки.
:param plot_item: PlotItem для добавления области.
"""
x1 = point_data.events["Closing"][0]
dx = point_data.events["Relief"][1] - x1
y1 = point_data.useful_data["part_pos"]*1000
dy = point_data.useful_data["thickness"]*1000
y1 = point_data.useful_data["part_pos"] * 1000
dy = point_data.useful_data["thickness"] * 1000
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
rect_item.setZValue(-5)
@ -463,18 +579,25 @@ class PlotItemGenerator:
rect_item.setPen(pg.mkPen('black', width=3))
plot_item.addItem(rect_item)
def _calc_performance(self,
timings:ChannelTimings,
point_data: PointPassport,
ideal_data:dict,
is_first:bool,
is_last:bool) -> ChannelTimings:
@staticmethod
def _calc_performance(timings: ChannelTimings, point_data: PointPassport, ideal_data: Dict,
is_first: bool, is_last: bool) -> ChannelTimings:
"""
Рассчитывает показатели производительности для текущей точки.
:param timings: Текущие временные показатели (ChannelTimings).
:param point_data: Объект PointPassport для текущей точки.
:param ideal_data: Идеальные данные для текущей точки.
:param is_first: True, если точка первая.
:param is_last: True, если точка последняя.
:return: Обновленный объект ChannelTimings.
"""
if is_first:
if not self._ideal_mode:
if not PlotItemGenerator._parent_ideal_mode():
timings.TWC_start = point_data.events["Closing"][0]
ideal_delta = ideal_data["Ideal cycle"]
elif is_last:
if not self._ideal_mode:
if not PlotItemGenerator._parent_ideal_mode():
timings.TWC_end = point_data.events["Relief"][1]
timings.TWC_time = timings.TWC_end - timings.TWC_start
timings.worst_timeframe = [timings.TWC_start, timings.TWC_end]
@ -483,63 +606,85 @@ class PlotItemGenerator:
ideal_delta = ideal_data["Ideal cycle"]
timings.ideal_time += ideal_delta
#curr_perf = ideal_delta/TWC_delta if TWC_delta != 0 else 1
if False: #curr_perf < timings.worst_performance:
timings.worst_performance = curr_perf
timings.worst_timeframe = point_data.timeframe
# При желании можно добавить сравнение с текущей производительностью
# Если (ideal_delta / TWC_delta) < timings.worst_performance, обновляем worst_performance и worst_timeframe
return timings
@staticmethod
def _generate_synthetic_events(timings:ChannelTimings,
ideal_data:dict) -> Tuple[ChannelTimings, dict, list[float]]:
timings.worst_timeframe = point_timeframe = [timings.shift, timings.shift+ ideal_data["Ideal cycle"]]
def _generate_synthetic_events(timings: ChannelTimings, ideal_data: Dict) -> Tuple[ChannelTimings, Dict, List[float]]:
"""
Генерирует синтетические события для случая, когда данные отсутствуют.
:param timings: Объект ChannelTimings.
:param ideal_data: Словарь с идеальными данными, содержащий "Ideal cycle" и "Ideal timings".
:return: Кортеж (обновленные timings, сгенерированные события, временной интервал точки).
"""
point_timeframe = [timings.shift, timings.shift + ideal_data["Ideal cycle"]]
point_events = {}
keys = list(ideal_data.keys())
shift = 0
for i, time in enumerate(ideal_data["Ideal timings"]):
point_events[keys[i]] = [timings.shift+shift, timings.shift+time+shift]
point_events[keys[i]] = [timings.shift + shift, timings.shift + time + shift]
shift += time
timings.shift +=ideal_data["Ideal cycle"]
return (timings, point_events, point_timeframe)
timings.shift += ideal_data["Ideal cycle"]
return timings, point_events, point_timeframe
@staticmethod
def _apply_force_compensation(force: float,
k_hardness: float,
dataframe:pd.DataFrame,
point_timeframe:list,
real_signals:list[dict]) -> pd.DataFrame:
F_comp = - force/k_hardness
def _apply_force_compensation(force: float, k_hardness: float, dataframe: pd.DataFrame,
point_timeframe: List[float], real_signals: List[Dict]) -> pd.DataFrame:
"""
Применяет компенсацию силы к данным в заданном интервале времени.
:param force: Значение силы.
:param k_hardness: Коэффициент твердости.
:param dataframe: DataFrame с данными.
:param point_timeframe: Временной интервал, для которого производится компенсация.
:param real_signals: Список описаний реальных сигналов.
:return: Измененный DataFrame с примененной компенсацией.
"""
F_comp = -force / k_hardness
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs], lambda x: x + F_comp)
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs],
lambda x: x + F_comp)
return dataframe
@staticmethod
def _parent_ideal_mode() -> bool:
"""
Вспомогательный метод-заглушка для определения режима идеальных данных.
(При необходимости можно реализовать проверку из родительского класса)
"""
# Здесь можно вернуть значение из родительского контекста, если требуется
return False
# =============================================================================
# Класс NavigatorPlot навигатор для графика, позволяющий управлять областью просмотра
# =============================================================================
class NavigatorPlot(pg.PlotItem):
def __init__(self,
time_region:tuple[float, float],
main_plot: pg.PlotItem):
"""
График-навигатор, отображающий уменьшенную копию данных основного графика,
позволяющий синхронизировать область просмотра основного графика с регионом навигатора.
"""
def __init__(self, time_region: Tuple[float, float], main_plot: pg.PlotItem):
"""
Инициализирует NavigatorPlot, синхронизируя его с основным графиком.
:param time_region: Временной интервал, который должен быть выделен.
:param main_plot: Основной PlotItem, с которым будет синхронизация.
"""
super().__init__()
self._init_navigator(time_region, main_plot)
self._init_syncranisation(main_plot)
@staticmethod
def _downsample_data(x:list, y:list, max_points=5000):
"""
Понижает разрешение данных до заданного количества точек для улучшения производительности навигатора.
"""
if len(x) > max_points:
factor = len(x) // max_points
x_downsampled = x[::factor]
y_downsampled = y[::factor]
return x_downsampled, y_downsampled
return x, y
@staticmethod
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem,
region: pg.LinearRegionItem) -> None:
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem, region: pg.LinearRegionItem) -> None:
"""
Синхронизирует область просмотра основного графика с регионом навигатора.
:param main_plot: Основной график.
:param region: Регион навигатора.
"""
x_min, x_max = region.getRegion()
if main_plot:
@ -548,51 +693,55 @@ class NavigatorPlot(pg.PlotItem):
main_plot.blockSignals(False)
@staticmethod
def _sync_navigator_with_main(main_plot: pg.PlotItem, region:pg.LinearRegionItem):
def _sync_navigator_with_main(main_plot: pg.PlotItem, region: pg.LinearRegionItem):
"""
Синхронизирует регион навигатора с областью просмотра основного графика.
Синхронизирует регион навигатора с текущей областью просмотра основного графика.
:param main_plot: Основной график.
:param region: Регион навигатора.
"""
if region:
x_min, x_max = main_plot
region.blockSignals(True) # Предотвращаем рекурсию
x_min, x_max = main_plot.getViewBox().viewRange()[0]
region.blockSignals(True)
region.setRegion([x_min, x_max])
region.blockSignals(False)
def _init_syncranisation(self, main_plot: pg.PlotItem) -> None:
"""
Связывает изменения навигатора и других графиков друг с другом
Настраивает синхронизацию между навигатором и основным графиком.
"""
self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
self.ROI_region.sigRegionChanged.connect(
lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
)
)
main_plot.sigXRangeChanged.connect(
lambda _, plot=main_plot, region=self.ROI_region: self._sync_navigator_with_main(main_plot=plot, region=region)
)
lambda: self._sync_navigator_with_main(main_plot, self.ROI_region)
)
def _init_navigator(self,
time_region:tuple[float, float],
main_plot: pg.PlotItem) -> None:
def _init_navigator(self, time_region: Tuple[float, float], main_plot: pg.PlotItem) -> None:
"""
Создаёт график-навигатор, отображающий все данные в уменьшенном масштабе.
Создает график-навигатор, отображающий уменьшенную копию данных.
:param time_region: Временной интервал, который необходимо выделить.
:param main_plot: Основной график, из которого будут извлекаться данные.
"""
self.setTitle("Navigator")
self.setFixedHeight(100)
# Получение кривых из main_plot
# Извлекаем данные из каждого элемента основного графика и даунсэмплируем их
for curve in main_plot.listDataItems():
# Извлекаем данные из кривой
x, y = curve.getData()
curve_name = curve.opts.get("name", None)
signal_pen = curve.opts.get("pen", None)
x_downsampled, y_downsampled = self._downsample_data(x, y, max_points=1000)
self.plot(x_downsampled, y_downsampled, pen=signal_pen, name=curve_name)
self.ROI_region = pg.LinearRegionItem(movable=True, brush=pg.mkBrush(0, 0, 255, 100), pen=pg.mkPen(width=4))
maxBound = 50
if len(x) !=0: maxBound = x[-1]
self.ROI_region.setBounds([0, maxBound])
plot = self.plot(x, y, pen=signal_pen, name=curve_name)
#plot.setDownsampling(ds = 10, auto = True, method = 'peak')
# Создаем регион для выделения области
self.ROI_region = pg.LinearRegionItem(
movable=True,
brush=pg.mkBrush(0, 0, 255, 100),
pen=pg.mkPen(width=4)
)
max_bound = x[-1] if x.any() else 50
self.ROI_region.setBounds([0, max_bound])
self.ROI_region.setRegion(time_region)
self.addItem(self.ROI_region)
self.getViewBox().setLimits(xMin=0, xMax=maxBound)
self.getViewBox().setLimits(xMin=0, xMax=max_bound)

View File

@ -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()

View File

@ -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
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
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()