diff --git a/backend/backend_cavity.py b/backend/backend_cavity.py index 3748b67..f25e101 100644 --- a/backend/backend_cavity.py +++ b/backend/backend_cavity.py @@ -1,8 +1,8 @@ -from collections import OrderedDict -from datetime import datetime -from typing import Dict +from collections import OrderedDict, defaultdict +from datetime import datetime from epics import caput +from typing import Dict from backend.fault import Fault, FaultCounter, PVInvalidError from lcls_tools.superconducting.sc_linac import Cavity @@ -18,9 +18,9 @@ class BackendCavity(Cavity): def __init__( - self, - cavity_num, - rack_object, + self, + cavity_num, + rack_object, ): super(BackendCavity, self).__init__( cavity_num=cavity_num, rack_object=rack_object @@ -75,7 +75,7 @@ def create_faults(self): ) if (cm_type == "1.3" and self.cryomodule.is_harmonic_linearizer) or ( - cm_type == "3.9" and not self.cryomodule.is_harmonic_linearizer + cm_type == "3.9" and not self.cryomodule.is_harmonic_linearizer ): continue pv = prefix + suffix @@ -119,14 +119,20 @@ def create_faults(self): ) def get_fault_counts( - self, start_time: datetime, end_time: datetime + self, start_time: datetime, end_time: datetime ) -> Dict[str, FaultCounter]: - result: Dict[str, FaultCounter] = {} - + result: Dict[str, FaultCounter] = defaultdict(FaultCounter) + + """ + Using max function to get the maximum fault or invalid count for duplicate TLCs + i.e. MGT tlc has three PVs associated with it (X, Y, and Q) but we + only want the fault and invalid count for whichever PV had the + greatest number of faults + """ for fault in self.faults.values(): - result[fault.pv.pvname] = fault.get_fault_count_over_time_range( + result[fault.tlc] = max(result[fault.tlc], fault.get_fault_count_over_time_range( start_time=start_time, end_time=end_time - ) + )) return result diff --git a/backend/fault.py b/backend/fault.py index c0f267d..62421ad 100644 --- a/backend/fault.py +++ b/backend/fault.py @@ -19,6 +19,10 @@ class FaultCounter: ok_count: int = 0 invalid_count: int = 0 + @property + def sum_fault_count(self): + return self.fault_count + self.invalid_count + @property def ratio_ok(self): try: @@ -26,6 +30,12 @@ def ratio_ok(self): except ZeroDivisionError: return 1 + def __gt__(self, other): + return self.sum_fault_count > other.sum_fault_count + + def __eq__(self, other): + return self.sum_fault_count == other.sum_fault_count + class PVInvalidError(Exception): def __init__(self, message): @@ -34,20 +44,20 @@ def __init__(self, message): class Fault: def __init__( - self, - tlc, - severity, - pv, - ok_value, - fault_value, - long_description, - short_description, - button_level, - button_command, - macros, - button_text, - button_macro, - action, + self, + tlc, + severity, + pv, + ok_value, + fault_value, + long_description, + short_description, + button_level, + button_command, + macros, + button_text, + button_macro, + action, ): self.tlc = tlc self.severity = int(severity) @@ -65,6 +75,8 @@ def __init__( self.pv: PV = PV(pv, connection_timeout=PV_TIMEOUT) def is_currently_faulted(self): + # returns "TRUE" if faulted + # returns "FALSE" if not faulted return self.is_faulted(self.pv) def is_faulted(self, obj: Union[PV, ArchiverValue]): @@ -79,10 +91,16 @@ class AlarmSeverity(DefaultIntEnum): if obj.severity == 3 or obj.status is None: raise PVInvalidError(self.pv.pvname) + # self.ok_value is the value stated in spreadsheet + # obj.value is the actual reading value from pv if self.ok_value is not None: + # return "TRUE" means they do NOT match + # return "FALSE" means is_okay, not faulted return obj.val != self.ok_value elif self.fault_value is not None: + # return "TRUE" means faulted + # return "FALSE" means not faulted return obj.val == self.fault_value else: @@ -99,7 +117,7 @@ def was_faulted(self, time: datetime): return self.is_faulted(archiver_value) def get_fault_count_over_time_range( - self, start_time: datetime, end_time: datetime + self, start_time: datetime, end_time: datetime ) -> FaultCounter: result = get_values_over_time_range( pv_list=[self.pv.pvname], start_time=start_time, end_time=end_time diff --git a/backend/runner.py b/backend/runner.py index 5c4ecbc..1e6151e 100644 --- a/backend/runner.py +++ b/backend/runner.py @@ -3,7 +3,7 @@ from backend.backend_cavity import BackendCavity from lcls_tools.common.controls.pyepics.utils import PV -from lcls_tools.superconducting.sc_linac import Machine, Cryomodule +from lcls_tools.superconducting.sc_linac import Cryomodule, Machine from lcls_tools.superconducting.sc_linac_utils import ALL_CRYOMODULES from utils.utils import DEBUG, BACKEND_SLEEP_TIME @@ -11,18 +11,18 @@ WATCHER_PV.put(0) DISPLAY_MACHINE = Machine(cavity_class=BackendCavity) - while True: start = datetime.now() for cryomoduleName in ALL_CRYOMODULES: cryomodule: Cryomodule = DISPLAY_MACHINE.cryomodules[cryomoduleName] for cavity in cryomodule.cavities.values(): cavity.run_through_faults() - if DEBUG: - delta = (datetime.now() - start).total_seconds() - sleep(BACKEND_SLEEP_TIME - delta if delta < BACKEND_SLEEP_TIME else 0) - try: - WATCHER_PV.put(WATCHER_PV.get() + 1) - except TypeError as e: - print(f"Write to watcher PV failed with error: {e}") + if DEBUG: + delta = (datetime.now() - start).total_seconds() + sleep(BACKEND_SLEEP_TIME - delta if delta < BACKEND_SLEEP_TIME else 0) + + try: + WATCHER_PV.put(WATCHER_PV.get() + 1) + except TypeError as e: + print(f"Write to watcher PV failed with error: {e}") diff --git a/frontend/decoder.py b/frontend/decoder.py index 076212a..b6a6b8a 100644 --- a/frontend/decoder.py +++ b/frontend/decoder.py @@ -1,7 +1,6 @@ -import sys from collections import OrderedDict -from dataclasses import dataclass +import sys from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QHBoxLayout, @@ -13,6 +12,7 @@ QApplication, QAbstractScrollArea, ) +from dataclasses import dataclass from pydm import Display from utils.utils import parse_csv @@ -23,8 +23,9 @@ @dataclass class Row: tlc: str - longDesc: str - genShortDesc: str + long_desc: str + gen_short_desc: str + corrective_action: str class DecoderDisplay(Display): @@ -35,8 +36,9 @@ def __init__(self, parent=None, args=None, macros=None): tlc = faultRowDict["Three Letter Code"] rows[tlc] = Row( tlc=tlc, - longDesc=faultRowDict["Long Description"], - genShortDesc=faultRowDict["Generic Short Description for Decoder"], + long_desc=faultRowDict["Long Description"], + gen_short_desc=faultRowDict["Generic Short Description for Decoder"], + corrective_action=faultRowDict["Recommended Corrective Actions"] ) sorted_fault_rows = OrderedDict( @@ -62,52 +64,66 @@ def __init__(self, parent=None, args=None, macros=None): # Long description header header_layout = QHBoxLayout() description_header_label = QLabel("Description") - description_header_label.setMinimumSize(200, 30) + description_header_label.setMinimumSize(100, 30) + description_header_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) description_header_label.setStyleSheet("text-decoration: underline") # Name (aka short description) header name_header_label = QLabel("Name") - name_header_label.setMinimumSize(200, 30) - name_header_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) + name_header_label.setMinimumSize(100, 30) + name_header_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) name_header_label.setStyleSheet("text-decoration: underline") # Three-Letter Code header code_header_label = QLabel("Code") code_header_label.setMinimumSize(30, 30) - code_header_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + code_header_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) code_header_label.setStyleSheet("text-decoration: underline") + # Corrective Action header + action_header_label = QLabel("Corrective Action") + action_header_label.setMinimumSize(100, 30) + action_header_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + action_header_label.setStyleSheet("text-decoration: underline") + header_layout.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) header_layout.addWidget(code_header_label) header_layout.addWidget(name_header_label) - header_layout.addWidget(description_header_label) + header_layout.addWidget(description_header_label, 2) + header_layout.addWidget(action_header_label, 2) header_layout.setSpacing(50) scroll_area_layout.addLayout(header_layout) for row in sorted_fault_rows.values(): horizontal_layout = QHBoxLayout() - description_label = QLabel(row.longDesc) - description_label.setMinimumSize(300, 50) + description_label = QLabel(row.long_desc) + description_label.setMinimumSize(100, 50) description_label.setSizePolicy( - QSizePolicy.MinimumExpanding, QSizePolicy.Minimum + QSizePolicy.Minimum, QSizePolicy.Minimum ) description_label.setWordWrap(True) code_label = QLabel(row.tlc) code_label.setMinimumSize(30, 30) - code_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum) + code_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) name_label = QLabel() - name_label.setText(row.genShortDesc) - name_label.setMinimumSize(200, 50) - name_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Maximum) + name_label.setText(row.gen_short_desc) + name_label.setMinimumSize(100, 50) + name_label.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) name_label.setWordWrap(True) + action_label = QLabel(row.corrective_action) + action_label.setMinimumSize(100, 50) + action_label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) + action_label.setWordWrap(True) + horizontal_layout.addWidget(code_label) horizontal_layout.addWidget(name_label) - horizontal_layout.addWidget(description_label) + horizontal_layout.addWidget(description_label, 2) + horizontal_layout.addWidget(action_label, 2) horizontal_layout.setSpacing(50) horizontal_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) diff --git a/frontend/fault_count_display.py b/frontend/fault_count_display.py new file mode 100644 index 0000000..68ce790 --- /dev/null +++ b/frontend/fault_count_display.py @@ -0,0 +1,131 @@ +import pyqtgraph as pg +from PyQt5.QtCore import QDateTime +from PyQt5.QtWidgets import ( + QVBoxLayout, + QHBoxLayout, + QComboBox, + QDateTimeEdit, + QPushButton, + QLabel, + QCheckBox +) +from pydm import Display +from typing import Dict + +from backend.backend_cavity import BackendCavity +from frontend.cavity_widget import RED_FILL_COLOR, PURPLE_FILL_COLOR, DARK_GRAY_COLOR +from lcls_tools.superconducting.sc_linac import Machine +from lcls_tools.superconducting.sc_linac_utils import ALL_CRYOMODULES + +DISPLAY_MACHINE = Machine(cavity_class=BackendCavity) + + +class FaultCountDisplay(Display): + def __init__(self): + super().__init__() + self.setWindowTitle("Fault Count Display") + + main_v_layout = QVBoxLayout() + input_h_layout = QHBoxLayout() + + self.plot_window = pg.plot() + self.plot_window.setBackground(DARK_GRAY_COLOR) + + main_v_layout.addLayout(input_h_layout) + main_v_layout.addWidget(self.plot_window) + self.setLayout(main_v_layout) + + cm_text = QLabel("CM:") + self.cm_combo_box = QComboBox() + cav_text = QLabel("Cav:") + self.cav_combo_box = QComboBox() + + end_date_time = QDateTime.currentDateTime() + intermediate_time = QDateTime.addSecs(end_date_time, -30 * 60) # 30 min + min_date_time = QDateTime.addYears(end_date_time, -3) # 3 years + + start_text = QLabel("Start:") + self.start_selector = QDateTimeEdit() + self.start_selector.setCalendarPopup(True) + + end_text = QLabel("End:") + self.end_selector = QDateTimeEdit() + self.end_selector.setCalendarPopup(True) + + self.start_selector.setMinimumDateTime(min_date_time) + self.start_selector.setDateTime(intermediate_time) + self.end_selector.setDateTime(end_date_time) + + self.pot_checkbox = QCheckBox(text="Check to remove POT fault counts from plot") + + self.plot_button = QPushButton() + self.plot_button.setText("Update Bar Chart") + + input_h_layout.addWidget(cm_text) + input_h_layout.addWidget(self.cm_combo_box) + input_h_layout.addWidget(cav_text) + input_h_layout.addWidget(self.cav_combo_box) + input_h_layout.addWidget(start_text) + input_h_layout.addWidget(self.start_selector) + input_h_layout.addWidget(end_text) + input_h_layout.addWidget(self.end_selector) + input_h_layout.addWidget(self.plot_button) + main_v_layout.addWidget(self.pot_checkbox) + + self.cm_combo_box.addItems(ALL_CRYOMODULES) + self.cav_combo_box.addItems([str(i) for i in range(1, 9)]) + + self.num_of_faults = [] + self.num_of_invalids = [] + self.y_data = None + + self.plot_button.clicked.connect(self.update_plot) + + def get_data(self): + cavity: BackendCavity = DISPLAY_MACHINE.cryomodules[self.cm_combo_box.currentText()].cavities[ + int(self.cav_combo_box.currentText())] + + self.num_of_faults = [] + self.num_of_invalids = [] + self.y_data = [] + + start = self.start_selector.dateTime().toPyDateTime() + end = self.end_selector.dateTime().toPyDateTime() + + """ + result is a dictionary with: + key = fault pv string + value = FaultCounter(fault_count=0, ok_count=1, invalid_count=0) <-- Example + """ + result: Dict[str, FaultCounter] = cavity.get_fault_counts( + start, end + ) + + for tlc, counter_obj in result.items(): + if self.pot_checkbox.isChecked() and tlc == 'POT': + continue + else: + self.y_data.append(tlc) + self.num_of_faults.append(counter_obj.fault_count) + self.num_of_invalids.append(counter_obj.invalid_count) + + def update_plot(self): + self.plot_window.clear() + self.get_data() + + ticks = [] + y_vals_ints = [] + for idy, y_val in enumerate(self.y_data): + ticks.append((idy, y_val)) + y_vals_ints.append(idy) + + # Create pyqt5graph bar graph for faults, then stack invalid faults on same bars + bargraph = pg.BarGraphItem(x0=0, y=y_vals_ints, height=0.6, width=self.num_of_faults, brush=RED_FILL_COLOR) + self.plot_window.addItem(bargraph) + bargraph = pg.BarGraphItem(x0=self.num_of_faults, y=y_vals_ints, height=0.6, width=self.num_of_invalids, + brush=PURPLE_FILL_COLOR) + + ax = self.plot_window.getAxis("left") + ax.setTicks([ticks]) + self.plot_window.showGrid(x=True, y=False, alpha=0.6) + self.plot_window.addItem(bargraph) diff --git a/utils/utils.py b/utils/utils.py index d24a01b..11b5ce3 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -1,6 +1,5 @@ import os from csv import DictReader - from typing import Dict, List DEBUG = False @@ -23,20 +22,20 @@ def parse_csv() -> List[Dict]: def display_hash( - rack: str, - fault_condition: str, - ok_condition: str, - tlc: str, - suffix: str, - prefix: str, + rack: str, + fault_condition: str, + ok_condition: str, + tlc: str, + suffix: str, + prefix: str, ): return ( - hash(rack) - ^ hash(fault_condition) - ^ hash(ok_condition) - ^ hash(tlc) - ^ hash(suffix) - ^ hash(prefix) + hash(rack) + ^ hash(fault_condition) + ^ hash(ok_condition) + ^ hash(tlc) + ^ hash(suffix) + ^ hash(prefix) )