Source code for pyccapt.control.gui.gui_visualization

import sys
import time

import numpy as np
import pyqtgraph as pg
import pyqtgraph.exporters

# from numba import njit
from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import QTimer

# Local module and scripts
from pyccapt.control.core import live_calibration, runtime, tof2mc_simple
from pyccapt.control.devices import initialize_devices
from pyccapt.control.gui import tooltips


[docs] class Ui_Visualization(object): def __init__(self, variables, conf, x_plot, y_plot, t_plot, main_v_dc_plot): """ Constructor for the Visualization UI class. Args: variables (object): Global experiment variables. conf (dict): Configuration settings. x_plot (multiprocessing.Array): Array for storing the x-axis values of the mass spectrum. y_plot (multiprocessing.Array): Array for storing the y-axis values of the mass spectrum. t_plot (multiprocessing.Array): Array for storing the time values of the mass spectrum. main_v_dc_plot (multiprocessing.Array): Array for storing the main voltage values of the mass spectrum. """ self.path_meta = None self.num_hit_display = 0 self.bins_detector = (256, 256) detector_diameter = conf["detector_diameter"] detector_diameter = detector_diameter / 2 self.range = [[-detector_diameter, detector_diameter], [-detector_diameter, detector_diameter]] self.hist_fdm, xedges, yedges = np.histogram2d([], [], bins=self.bins_detector, range=self.range) self.index_hist_mc = None self.index_hist_tof = None self.max_tof_val = None self.max_mc_val = None self.last_100_thousand_det_x_heatmap = np.array([]) self.last_100_thousand_det_y_heatmap = np.array([]) self.last_100_thousand_t = np.array([]) self.last_100_thousand_v = np.array([]) self.last_100_thousand_det_x = np.array([]) self.last_100_thousand_det_y = np.array([]) self.length_events = 0 self.styles = None self.num_event_mc_tof = None self.mc_tof_last_events_flag = False self.change_detection_rate_range = False self.start_time_metadata = 0 self.start_main_exp = 0 self.index_plot_start = 0 self.variables = variables self.conf = conf self.x_plot = x_plot self.y_plot = y_plot self.t_plot = t_plot self.main_v_dc_plot = main_v_dc_plot self.counter_source = '' self.index_plot_save = 0 self.index_plot = 0 self.index_wait_on_plot_start = 0 self.index_auto_scale_graph = 0 self.heatmap_fdm_switch_flag = 'heatmap' self.bins_mc = np.arange(0, self.conf["max_mass"] + self.conf['bin_size'], self.conf['bin_size']) self.bins_tof = np.arange(0, self.conf["max_tof"] + self.conf['bin_size'], self.conf['bin_size']) # Two parallel cumulative histograms per axis: one binned with # the live calibration applied, one with raw values. We always # update both so that toggling the "Uncalibrate" button is # purely a display swap and never loses prior events. self.hist_mc = np.zeros(len(self.bins_mc) - 1) self.hist_tof = np.zeros(len(self.bins_tof) - 1) self.hist_mc_uncalib = np.zeros(len(self.bins_mc) - 1) self.hist_tof_uncalib = np.zeros(len(self.bins_tof) - 1) self.update_timer = QTimer() # Create a QTimer for updating graphs self.update_timer.timeout.connect(self.update_graphs) # Connect it to the update_graphs slot # ----- Live calibration state ------------------------------------- # Default: show CALIBRATED data (uncalibrated_mode = False). # The "Uncalibrate" toggle in the GUI flips this flag. Calibration # parameters are refit by a background QThread (see # pyccapt.control.core.live_calibration) and atomically swapped # in here on the GUI thread. Until the first successful fit # self._calib_params stays None and the apply path falls back # to the raw uncalibrated formulas. self.uncalibrated_mode = False self._calib_params = None self._calib_worker = None self._calib_status_text = "calibrating..." # Lock that protects every write to / read of the # ``last_100_thousand_*`` ring buffer. The GUI thread mutates # these arrays in update_graphs_helper while the calibration # worker thread reads them via _calibration_snapshot. NumPy # array assignments are NOT atomic at the array level (a # concatenate -> realloc may race with a copy), so an explicit # RLock is the correct fix even though Python's GIL alone # usually papers over the race in practice. import threading as _threading self._buffer_lock = _threading.RLock() self.visualization_window = None # Inâ™ itialize the attribute
[docs] def setupUi(self, Visualization): """ Setup the UI for the Visualization window. Args: Visualization (QMainWindow): Visualization window. Return: None """ Visualization.setObjectName("Visualization") Visualization.resize(822, 647) self.gridLayout_6 = QtWidgets.QGridLayout(Visualization) self.gridLayout_6.setObjectName("gridLayout_6") self.gridLayout_5 = QtWidgets.QGridLayout() self.gridLayout_5.setObjectName("gridLayout_5") self.gridLayout_4 = QtWidgets.QGridLayout() self.gridLayout_4.setObjectName("gridLayout_4") self.label_200 = QtWidgets.QLabel(parent=Visualization) font = QtGui.QFont() font.setBold(True) self.label_200.setFont(font) self.label_200.setObjectName("label_200") self.gridLayout_4.addWidget(self.label_200, 0, 0, 1, 1) self.voltage = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.voltage.sizePolicy().hasHeightForWidth()) self.voltage.setSizePolicy(sizePolicy) self.voltage.setMinimumSize(QtCore.QSize(100, 20)) self.voltage.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.voltage.setObjectName("voltage") self.gridLayout_4.addWidget(self.voltage, 0, 1, 1, 1) spacerItem = QtWidgets.QSpacerItem( 26, 17, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.gridLayout_4.addItem(spacerItem, 0, 2, 1, 1) #### # self.vdc_time = QtWidgets.QGraphicsView(parent=Visualization) self.vdc_time = pg.PlotWidget(parent=Visualization) self.vdc_time.setBackground('w') #### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.vdc_time.sizePolicy().hasHeightForWidth()) self.vdc_time.setSizePolicy(sizePolicy) self.vdc_time.setMinimumSize(QtCore.QSize(250, 250)) self.vdc_time.setStyleSheet( "QWidget{\n" " border: 0.5px solid gray;\n" " }\n" " " ) self.vdc_time.setObjectName("vdc_time") self.gridLayout_4.addWidget(self.vdc_time, 1, 0, 1, 3) self.dc_hold = QtWidgets.QPushButton(parent=Visualization) self.dc_hold.setMinimumSize(QtCore.QSize(100, 20)) self.dc_hold.setMaximumSize(QtCore.QSize(100, 16777215)) self.dc_hold.setObjectName("dc_hold") self.gridLayout_4.addWidget(self.dc_hold, 2, 0, 1, 2) self.gridLayout_5.addLayout(self.gridLayout_4, 0, 0, 1, 1) self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") self.label_201 = QtWidgets.QLabel(parent=Visualization) font = QtGui.QFont() font.setBold(True) self.label_201.setFont(font) self.label_201.setObjectName("label_201") self.gridLayout.addWidget(self.label_201, 0, 0, 1, 1) self.detection_rate = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.detection_rate.sizePolicy().hasHeightForWidth()) self.detection_rate.setSizePolicy(sizePolicy) self.detection_rate.setMinimumSize(QtCore.QSize(100, 20)) self.detection_rate.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.detection_rate.setObjectName("detection_rate") self.gridLayout.addWidget(self.detection_rate, 0, 1, 1, 1) spacerItem1 = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.gridLayout.addItem(spacerItem1, 0, 2, 1, 1) #### # self.detection_rate_viz = QtWidgets.QGraphicsView(parent=Visualization) self.detection_rate_viz = pg.PlotWidget(parent=Visualization) self.detection_rate_viz.setBackground('w') #### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.detection_rate_viz.sizePolicy().hasHeightForWidth()) self.detection_rate_viz.setSizePolicy(sizePolicy) self.detection_rate_viz.setMinimumSize(QtCore.QSize(250, 250)) self.detection_rate_viz.setStyleSheet( "QWidget{\n" " border: 0.5px solid gray;\n" " }\n" " " ) self.detection_rate_viz.setObjectName("detection_rate_viz") self.gridLayout.addWidget(self.detection_rate_viz, 1, 0, 1, 3) self.detection_rate_range_switch = QtWidgets.QPushButton(parent=Visualization) self.detection_rate_range_switch.setMinimumSize(QtCore.QSize(0, 20)) self.detection_rate_range_switch.setMaximumSize(QtCore.QSize(100, 16777215)) self.detection_rate_range_switch.setObjectName("detection_rate_range_switch") self.gridLayout.addWidget(self.detection_rate_range_switch, 2, 0, 1, 1) self.gridLayout_5.addLayout(self.gridLayout, 0, 1, 1, 1) self.gridLayout_3 = QtWidgets.QGridLayout() self.gridLayout_3.setObjectName("gridLayout_3") self.label_206 = QtWidgets.QLabel(parent=Visualization) font = QtGui.QFont() font.setBold(True) self.label_206.setFont(font) self.label_206.setObjectName("label_206") self.gridLayout_3.addWidget(self.label_206, 0, 0, 1, 1) self.hitmap_count = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.hitmap_count.sizePolicy().hasHeightForWidth()) self.hitmap_count.setSizePolicy(sizePolicy) self.hitmap_count.setMinimumSize(QtCore.QSize(100, 20)) self.hitmap_count.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.hitmap_count.setObjectName("hitmap_count") self.gridLayout_3.addWidget(self.hitmap_count, 0, 1, 1, 1) spacerItem2 = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.gridLayout_3.addItem(spacerItem2, 0, 2, 1, 1) ### # self.detector_heatmap = QtWidgets.QGraphicsView(parent=Visualization) self.detector_heatmap = pg.PlotWidget(parent=Visualization) self.detector_heatmap.setBackground('w') ### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.detector_heatmap.sizePolicy().hasHeightForWidth()) self.detector_heatmap.setSizePolicy(sizePolicy) self.detector_heatmap.setMinimumSize(QtCore.QSize(250, 250)) self.detector_heatmap.setStyleSheet( "QWidget{\n" " border: 0.5px solid gray;\n" " }\n" " " ) self.detector_heatmap.setObjectName("detector_heatmap") self.gridLayout_3.addWidget(self.detector_heatmap, 1, 0, 1, 3) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.reset_heatmap_v = QtWidgets.QPushButton(parent=Visualization) self.reset_heatmap_v.setMinimumSize(QtCore.QSize(0, 20)) self.reset_heatmap_v.setMaximumSize(QtCore.QSize(60, 16777215)) self.reset_heatmap_v.setObjectName("reset_heatmap_v") self.horizontalLayout_2.addWidget(self.reset_heatmap_v) self.hitmap_plot_size = QtWidgets.QDoubleSpinBox(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.hitmap_plot_size.sizePolicy().hasHeightForWidth()) self.hitmap_plot_size.setSizePolicy(sizePolicy) self.hitmap_plot_size.setMinimumSize(QtCore.QSize(0, 20)) self.hitmap_plot_size.setStyleSheet( "QDoubleSpinBox{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.hitmap_plot_size.setObjectName("hitmap_plot_size") self.horizontalLayout_2.addWidget(self.hitmap_plot_size) self.hit_displayed = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.hit_displayed.sizePolicy().hasHeightForWidth()) self.hit_displayed.setSizePolicy(sizePolicy) self.hit_displayed.setMinimumSize(QtCore.QSize(50, 20)) self.hit_displayed.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.hit_displayed.setObjectName("hit_displayed") self.horizontalLayout_2.addWidget(self.hit_displayed) # Hitmap and FDM are now SEPARATE panels (see gridLayout_3b # below). The old heatmap_fdm_switch toggle is no longer # needed; we keep a hidden stub so any external code that # still references the attribute doesn't crash. self.heatmap_fdm_switch = QtWidgets.QPushButton(parent=Visualization) self.heatmap_fdm_switch.setVisible(False) self.gridLayout_3.addLayout(self.horizontalLayout_2, 2, 0, 1, 3) self.gridLayout_5.addLayout(self.gridLayout_3, 0, 2, 1, 1) # ------------------------------------------------------------------ # FDM-only panel - mirrors the hitmap panel above but always shows # the field-desorption map. Header has the live ion-count used in # the current FDM; bottom field is the max ion count that will be # accumulated before the histogram resets and starts over. # ------------------------------------------------------------------ self.gridLayout_3b = QtWidgets.QGridLayout() self.gridLayout_3b.setObjectName("gridLayout_3b") self.label_fdm_header = QtWidgets.QLabel(parent=Visualization) font = QtGui.QFont() font.setBold(True) self.label_fdm_header.setFont(font) self.label_fdm_header.setText("FDM") self.gridLayout_3b.addWidget(self.label_fdm_header, 0, 0, 1, 1) self.fdm_count = QtWidgets.QLineEdit(parent=Visualization) sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) self.fdm_count.setSizePolicy(sp) self.fdm_count.setMinimumSize(QtCore.QSize(100, 20)) self.fdm_count.setStyleSheet("QLineEdit{background: rgb(223,223,233)}") self.fdm_count.setReadOnly(True) self.fdm_count.setText("0") self.fdm_count.setObjectName("fdm_count") self.gridLayout_3b.addWidget(self.fdm_count, 0, 1, 1, 1) self.gridLayout_3b.addItem( QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum), 0, 2, 1, 1, ) self.detector_fdm = pg.PlotWidget(parent=Visualization) self.detector_fdm.setBackground('w') sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sp.setHorizontalStretch(1) sp.setVerticalStretch(1) self.detector_fdm.setSizePolicy(sp) self.detector_fdm.setMinimumSize(QtCore.QSize(250, 250)) self.detector_fdm.setStyleSheet("QWidget{border: 0.5px solid gray;}") self.detector_fdm.setObjectName("detector_fdm") self.gridLayout_3b.addWidget(self.detector_fdm, 1, 0, 1, 3) # Bottom row: [Last Events toggle] [N input] # When the toggle is OFF (default), the FDM accumulates ions # forever and the N field is ignored. When ON, only the last N # ions are used to build the FDM (sliding window). self.fdm_bottom_row = QtWidgets.QHBoxLayout() self.fdm_last_events_switch = QtWidgets.QPushButton(parent=Visualization) self.fdm_last_events_switch.setMinimumSize(QtCore.QSize(0, 20)) self.fdm_last_events_switch.setMaximumSize(QtCore.QSize(120, 16777215)) self.fdm_last_events_switch.setText("Last Events") self.fdm_last_events_switch.setCheckable(True) self.fdm_last_events_switch.setObjectName("fdm_last_events_switch") self.fdm_bottom_row.addWidget(self.fdm_last_events_switch) self.fdm_max_ions = QtWidgets.QLineEdit(parent=Visualization) self.fdm_max_ions.setMinimumSize(QtCore.QSize(100, 20)) self.fdm_max_ions.setStyleSheet("QLineEdit{background: rgb(223,223,233)}") self.fdm_max_ions.setText("1000000") self.fdm_max_ions.setObjectName("fdm_max_ions") self.fdm_bottom_row.addWidget(self.fdm_max_ions) self.gridLayout_3b.addLayout(self.fdm_bottom_row, 2, 0, 1, 3) self.gridLayout_5.addLayout(self.gridLayout_3b, 0, 3, 1, 1) self.gridLayout_2 = QtWidgets.QGridLayout() self.gridLayout_2.setObjectName("gridLayout_2") self.label_207 = QtWidgets.QLabel(parent=Visualization) self.label_207.setMinimumSize(QtCore.QSize(0, 25)) font = QtGui.QFont() font.setBold(True) self.label_207.setFont(font) self.label_207.setObjectName("label_207") self.gridLayout_2.addWidget(self.label_207, 0, 0, 1, 1) #### # self.histogram = QtWidgets.QGraphicsView(parent=Visualization) self.histogram = pg.PlotWidget(parent=Visualization) self.histogram.setBackground('w') #### sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.histogram.sizePolicy().hasHeightForWidth()) self.histogram.setSizePolicy(sizePolicy) self.histogram.setMinimumSize(QtCore.QSize(750, 150)) self.histogram.setStyleSheet( "QWidget{\n" " border: 0.5px solid gray;\n" " }\n" " " ) self.histogram.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.histogram.setObjectName("histogram") self.gridLayout_2.addWidget(self.histogram, 1, 0, 1, 1) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.spectrum_switch = QtWidgets.QPushButton(parent=Visualization) self.spectrum_switch.setMinimumSize(QtCore.QSize(0, 20)) self.spectrum_switch.setMaximumSize(QtCore.QSize(60, 16777215)) self.spectrum_switch.setObjectName("spectrum_switch") self.horizontalLayout.addWidget(self.spectrum_switch) # "Uncalibrate" toggle, sits right next to the mc/tof switch. # Default state = NOT pressed = show calibrated spectrum. # Pressed (green) = bypass corrections, show raw mc/tof. self.uncalibrate_switch = QtWidgets.QPushButton(parent=Visualization) self.uncalibrate_switch.setMinimumSize(QtCore.QSize(0, 20)) self.uncalibrate_switch.setMaximumSize(QtCore.QSize(100, 16777215)) self.uncalibrate_switch.setObjectName("uncalibrate_switch") self.horizontalLayout.addWidget(self.uncalibrate_switch) # Small status label that surfaces what the live-calibration # worker is doing ("calibrating…", "no clear peak", "R²=0.81…"). self.calib_status_label = QtWidgets.QLabel(parent=Visualization) self.calib_status_label.setMinimumSize(QtCore.QSize(120, 20)) font_status = QtGui.QFont() font_status.setItalic(True) font_status.setPointSize(8) self.calib_status_label.setFont(font_status) self.calib_status_label.setObjectName("calib_status_label") self.horizontalLayout.addWidget(self.calib_status_label) self.spectrum_last_events_switch = QtWidgets.QPushButton(parent=Visualization) self.spectrum_last_events_switch.setMinimumSize(QtCore.QSize(0, 20)) self.spectrum_last_events_switch.setMaximumSize(QtCore.QSize(100, 16777215)) self.spectrum_last_events_switch.setObjectName("spectrum_last_events_switch") self.horizontalLayout.addWidget(self.spectrum_last_events_switch) self.num_last_events = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.num_last_events.sizePolicy().hasHeightForWidth()) self.num_last_events.setSizePolicy(sizePolicy) self.num_last_events.setMinimumSize(QtCore.QSize(100, 20)) self.num_last_events.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.num_last_events.setObjectName("num_last_events") self.horizontalLayout.addWidget(self.num_last_events) self.label_208 = QtWidgets.QLabel(parent=Visualization) self.label_208.setMinimumSize(QtCore.QSize(0, 25)) font = QtGui.QFont() font.setBold(True) self.label_208.setFont(font) self.label_208.setObjectName("label_208") self.horizontalLayout.addWidget(self.label_208) self.max_mc = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.max_mc.sizePolicy().hasHeightForWidth()) self.max_mc.setSizePolicy(sizePolicy) self.max_mc.setMinimumSize(QtCore.QSize(100, 20)) self.max_mc.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.max_mc.setObjectName("max_mc") self.horizontalLayout.addWidget(self.max_mc) self.label_209 = QtWidgets.QLabel(parent=Visualization) self.label_209.setMinimumSize(QtCore.QSize(0, 25)) font = QtGui.QFont() font.setBold(True) self.label_209.setFont(font) self.label_209.setObjectName("label_209") self.horizontalLayout.addWidget(self.label_209) self.max_tof = QtWidgets.QLineEdit(parent=Visualization) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.max_tof.sizePolicy().hasHeightForWidth()) self.max_tof.setSizePolicy(sizePolicy) self.max_tof.setMinimumSize(QtCore.QSize(100, 20)) self.max_tof.setStyleSheet( "QLineEdit{\n" " background: rgb(223,223,233)\n" " }\n" " " ) self.max_tof.setObjectName("max_tof") self.horizontalLayout.addWidget(self.max_tof) spacerItem3 = QtWidgets.QSpacerItem( 40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum ) self.horizontalLayout.addItem(spacerItem3) self.gridLayout_2.addLayout(self.horizontalLayout, 2, 0, 1, 1) self.Error = QtWidgets.QLabel(parent=Visualization) self.Error.setMinimumSize(QtCore.QSize(800, 30)) font = QtGui.QFont() font.setPointSize(10) font.setBold(True) font.setStrikeOut(False) self.Error.setFont(font) self.Error.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.Error.setWordWrap(True) self.Error.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse) self.Error.setObjectName("Error") self.gridLayout_2.addWidget(self.Error, 3, 0, 1, 1) self.gridLayout_5.addLayout(self.gridLayout_2, 1, 0, 1, 4) self.gridLayout_6.addLayout(self.gridLayout_5, 0, 0, 1, 1) self.retranslateUi(Visualization) QtCore.QMetaObject.connectSlotsByName(Visualization) tooltips.apply_tooltips(self, tooltips.VISUALIZATION_TOOLTIPS) Visualization.setTabOrder(self.voltage, self.detection_rate) Visualization.setTabOrder(self.detection_rate, self.hitmap_count) Visualization.setTabOrder(self.hitmap_count, self.dc_hold) Visualization.setTabOrder(self.dc_hold, self.detection_rate_range_switch) Visualization.setTabOrder(self.detection_rate_range_switch, self.reset_heatmap_v) Visualization.setTabOrder(self.reset_heatmap_v, self.hitmap_plot_size) Visualization.setTabOrder(self.hitmap_plot_size, self.hit_displayed) Visualization.setTabOrder(self.hit_displayed, self.fdm_last_events_switch) Visualization.setTabOrder(self.fdm_last_events_switch, self.fdm_max_ions) Visualization.setTabOrder(self.fdm_max_ions, self.spectrum_switch) Visualization.setTabOrder(self.spectrum_switch, self.spectrum_last_events_switch) Visualization.setTabOrder(self.spectrum_last_events_switch, self.num_last_events) Visualization.setTabOrder(self.num_last_events, self.max_mc) Visualization.setTabOrder(self.max_mc, self.max_tof) Visualization.setTabOrder(self.max_tof, self.vdc_time) Visualization.setTabOrder(self.vdc_time, self.detection_rate_viz) Visualization.setTabOrder(self.detection_rate_viz, self.detector_heatmap) Visualization.setTabOrder(self.detector_heatmap, self.histogram) ### # Start the update timer with a 500 ms interval (2 times per second) self.update_timer.start(500) # High Voltage visualization ################ self.x_vdc = [i * 0.5 for i in range(200)] # 100 time points self.y_vdc = [0.0] * 200 # 200 data points, all initialized to 0.0 self.y_vdc[:] = [np.nan] * len(self.y_vdc) pen_vdc = pg.mkPen(color=(255, 0, 0), width=3) self.data_line_vdc = self.vdc_time.plot(self.x_vdc, self.y_vdc, pen=pen_vdc) self.vdc_time.plotItem.setMouseEnabled(x=False) # Only allow zoom in Y-axis # Add Axis Labels self.styles = {"color": "#f00", "font-size": "12px"} self.vdc_time.setLabel("left", "High Voltage", units='V', **self.styles) self.vdc_time.setLabel("bottom", "Time (s)", **self.styles) # Add grid self.vdc_time.showGrid(x=True, y=True) # Add Range self.vdc_time.setXRange(0, 100) self.vdc_time.setYRange(0, 15000) # Detection Visualization ######################### self.x_dtec = [i * 0.5 for i in range(200)] # 100 time points self.y_dtec = [0.0] * 200 # 200 data points, all initialized to 0.0 self.y_dtec[:] = [np.nan] * len(self.y_vdc) pen_dtec = pg.mkPen(color=(255, 0, 0), width=3) self.data_line_dtec = self.detection_rate_viz.plot(self.x_dtec, self.y_dtec, pen=pen_dtec) # Add Axis Labels self.detection_rate_viz.setLabel("left", "Detection rate (%)", **self.styles) self.detection_rate_viz.setLabel("bottom", "Time (s)", **self.styles) # Add grid self.detection_rate_viz.showGrid(x=True, y=True) self.detection_rate_viz.plotItem.setMouseEnabled(x=False) # Only allow zoom in Y-axis # Add Range self.detection_rate_viz.setXRange(0, 100) self.detection_rate_viz.setYRange(0, 100) # detector heatmep ##################### self.scatter = pg.ScatterPlotItem(size=self.hitmap_plot_size.value(), brush='black') self.detector_circle = QtWidgets.QGraphicsEllipseItem(-40, -40, 80, 80) # x, y, width, height self.detector_circle.setPen(pg.mkPen(color=(255, 0, 0), width=2)) self.detector_heatmap.addItem(self.detector_circle) self.detector_heatmap.setLabel("left", "X_det", units='mm', **self.styles) self.detector_heatmap.setLabel("bottom", "Y_det", units='mm', **self.styles) # FDM panel - one detector circle per plot (Qt items can't be # shared between two PlotWidgets) plus matching axis labels. self.detector_circle_fdm = QtWidgets.QGraphicsEllipseItem(-40, -40, 80, 80) self.detector_circle_fdm.setPen(pg.mkPen(color=(255, 0, 0), width=2)) self.detector_fdm.addItem(self.detector_circle_fdm) self.detector_fdm.setLabel("left", "X_det", units='mm', **self.styles) self.detector_fdm.setLabel("bottom", "Y_det", units='mm', **self.styles) self.detector_fdm.getViewBox().setAspectLocked(True) # Per-FDM ion counter - resets when fdm_max_ions is exceeded # (only relevant when the Last-Events toggle is OFF). self._fdm_event_count = 0 # Last-Events mode state. When the toggle is on, the FDM is # rebuilt every tick from a sliding window of the most recent # fdm_max_ions (x, y) hits stored in these two arrays. self._fdm_use_last_events = False self._fdm_window_x = np.array([], dtype=np.float32) self._fdm_window_y = np.array([], dtype=np.float32) self._original_fdm_button_style = self.fdm_last_events_switch.styleSheet() self.fdm_last_events_switch.clicked.connect(self._fdm_last_events_toggle) # Histogram ######################### # Add Axis Labels self.histogram.plotItem.setMouseEnabled(y=False) # Only allow zoom in X-axis self.histogram.setLabel("left", "Event Counts", **self.styles) self.histogram.setLogMode(y=True) if self.conf["visualization"] == "tof": self.histogram.setLabel("bottom", "Time", units='ns', **self.styles) elif self.conf["visualization"] == "mc": self.histogram.setLabel("bottom", "m/c", units='Da', **self.styles) self.visualization_window = Visualization # Assign the attribute when setting up the UI self.reset_heatmap_v.clicked.connect(self.reset_heatmap) self.histogram.addLegend(offset=(-10, 10)) self.original_button_style = self.detection_rate_range_switch.styleSheet() self.detection_rate_range_switch.clicked.connect(self.detection_rate_range) self.spectrum_switch.clicked.connect(self.spectrum_switch_mc_tof) self.uncalibrate_switch.clicked.connect(self.uncalibrate_clicked) self.spectrum_last_events_switch.clicked.connect(self.spectrum_last_events) # Start the background calibration worker. It snapshots the # 100 000-event ring buffer every refit_interval_s, fits new # parameters off the GUI thread, and emits parameters_updated # when ready. The render path picks up the new params on the # next tick. Disable via config: live_calibration_refit_interval_s = 0 self._start_live_calibration_worker() self.num_last_events.editingFinished.connect(self.parameters_changes) self.max_mc.editingFinished.connect(self.parameters_changes) self.max_tof.editingFinished.connect(self.parameters_changes) self.num_event_mc_tof = int(self.num_last_events.text()) # heatmap_fdm_switch is now hidden - hitmap and FDM are always # rendered side-by-side in their own panels, no toggle needed. self.num_event_mc_tof = int(self.num_last_events.text()) self.max_mc_val = int(self.max_mc.text()) self.max_tof_val = int(self.max_tof.text()) self.index_hist_tof = np.where(self.bins_tof == self.max_tof_val)[0][0] self.index_hist_mc = np.where(self.bins_mc == self.max_mc_val)[0][0] self.dc_hold.clicked.connect(self.dc_hold_clicked) self.hitmap_count.setReadOnly(True) self.voltage.setReadOnly(True) self.detection_rate.setReadOnly(True) self.hit_displayed.editingFinished.connect(self.parameters_changes) # Create a QTimer to hide the warning message after 8 seconds self.timer = QtCore.QTimer() self.timer.timeout.connect(self.hideMessage) self.hitmap_plot_size.setValue(1.0) self.hitmap_plot_size.setSingleStep(0.1) self.hitmap_plot_size.setDecimals(1)
[docs] def retranslateUi(self, Visualization): """ Set the text of the widgets Args: Visualization: The main window Return: None """ _translate = QtCore.QCoreApplication.translate ### # Visualization.setWindowTitle(_translate("Visualization", "Form")) Visualization.setWindowTitle(_translate("Visualization", "PyCCAPT Visualization")) Visualization.setWindowIcon(QtGui.QIcon('./files/logo.png')) ### self.label_200.setText(_translate("Visualization", "Voltage")) self.voltage.setText(_translate("Visualization", "0")) self.dc_hold.setText(_translate("Visualization", "Hold DC Voltage")) self.label_201.setText(_translate("Visualization", "Detection Rate")) self.detection_rate.setText(_translate("Visualization", "0")) self.detection_rate_range_switch.setText(_translate("Visualization", "Short Range")) self.label_206.setText(_translate("Visualization", "Detector")) self.hitmap_count.setText(_translate("Visualization", "0")) self.reset_heatmap_v.setText(_translate("Visualization", "Reset")) self.hit_displayed.setText(_translate("Visualization", "2000")) # heatmap_fdm_switch is hidden but we still set its text in case # any external code reads it. self.heatmap_fdm_switch.setText(_translate("Visualization", "Hitmap/FDM")) self.label_207.setText(_translate("Visualization", "Spectrum")) self.spectrum_switch.setText(_translate("Visualization", "mc/tof")) self.uncalibrate_switch.setText(_translate("Visualization", "Uncalibrate")) self.calib_status_label.setText(_translate("Visualization", "live cal: calibrating…")) self.spectrum_last_events_switch.setText(_translate("Visualization", "Last Events")) self.num_last_events.setText(_translate("Visualization", "10000")) self.label_208.setText(_translate("Visualization", "Max mc (Da)")) self.max_mc.setText(_translate("Visualization", "400")) self.label_209.setText(_translate("Visualization", "Max tof (ns)")) self.max_tof.setText(_translate("Visualization", "5000")) self.Error.setText(_translate("Visualization", "<html><head/><body><p><br/></p></body></html>"))
[docs] def dc_hold_clicked(self): """ Hold the DC voltage Args: None Return: None """ if self.variables.start_flag or self.variables.last_screen_shot: if not self.variables.vdc_hold: self.variables.vdc_hold = True self.dc_hold.setStyleSheet("QPushButton{\nbackground: rgb(0, 255, 26)\n}") elif self.variables.vdc_hold: self.variables.vdc_hold = False self.dc_hold.setStyleSheet(self.original_button_style)
[docs] def heatmap_fdm_switch_change(self): """No-op kept for backward compatibility. Hitmap and FDM are now rendered side-by-side in their own panels (detector_heatmap + detector_fdm) every refresh - there is no longer anything to toggle. Any external code that still clicks the (now-hidden) heatmap_fdm_switch button just lands here harmlessly. """ return
def _fdm_last_events_toggle(self): """Switch the FDM between accumulate-all and sliding-window modes. Default (button up) - the FDM histogram accumulates every ion for the lifetime of the experiment. The fdm_max_ions field below is ignored. Toggled on (button green) - the FDM is rebuilt every refresh from a sliding window of the most recent fdm_max_ions hits. Switching modes resets the window so the next render starts from a clean slate. """ self._fdm_use_last_events = self.fdm_last_events_switch.isChecked() if self._fdm_use_last_events: self.fdm_last_events_switch.setStyleSheet("QPushButton{background: rgb(0, 255, 26)}") else: self.fdm_last_events_switch.setStyleSheet(self._original_fdm_button_style) # Reset everything so the next render starts clean in the new mode. self._fdm_window_x = np.array([], dtype=np.float32) self._fdm_window_y = np.array([], dtype=np.float32) self.hist_fdm[:] = 0.0 self._fdm_event_count = 0 self.fdm_count.setText("0") self.detector_fdm.clear() self.detector_fdm.addItem(self.detector_circle_fdm)
[docs] def reset_heatmap(self): """ Reset the heatmap Args: None Return: None """ # with self.variables.lock_setup_parameters: if not self.variables.reset_heatmap: self.variables.reset_heatmap = True
[docs] def detection_rate_range(self): """ Change the time range of the detection rate Args: None Return: None """ self.change_detection_rate_range = not self.change_detection_rate_range if self.change_detection_rate_range: self.detection_rate_range_switch.setStyleSheet("QPushButton{\nbackground: rgb(0, 255, 26)\n}") else: self.detection_rate_range_switch.setStyleSheet(self.original_button_style)
[docs] def update_graphs_helper( self, ): """ Update the graphs Args: None Return: None """ if self.index_plot_start == 0: self.num_hit_display = int(float(self.hit_displayed.text())) self.start_main_exp = time.time() self.start_time = time.time() self.start_time_metadata = time.time() self.index_plot_start += 1 self.hitmap_count.setText(str(0)) self.variables.elapsed_time = time.time() - self.start_time # with self.variables.lock_statistics: if self.index_wait_on_plot_start <= 16: if self.index_wait_on_plot_start == 0: self.counter_source = self.variables.counter_source self.index_wait_on_plot_start += 1 # V_dc and V_p current_voltage = self.variables.specimen_voltage_plot if self.index_plot < len(self.y_vdc): self.y_vdc[self.index_plot] = int(current_voltage) # Add a new value. else: x_vdc_last = self.x_vdc[-1] self.x_vdc.append(x_vdc_last + 0.5) # Add a new value 1 higher than the last. self.y_vdc.append(int(current_voltage)) # set the value of the voltage with two decimal places self.voltage.setText(str("{:.2f}".format(current_voltage))) # Set the maximum number of data points to display max_display_points = 200 # Downsample the data if needed if len(self.x_vdc) > max_display_points: step = len(self.x_vdc) // max_display_points x_vdc_downsampled = self.x_vdc[::step] y_vdc_downsampled = self.y_vdc[::step] self.data_line_vdc.setData(x_vdc_downsampled, y_vdc_downsampled) else: self.data_line_vdc.setData(self.x_vdc, self.y_vdc) # Detection Rate Visualization # with self.variables.lock_statistics: current_detection_rate = self.variables.detection_rate_current_plot if self.index_plot < len(self.y_dtec): self.y_dtec[self.index_plot] = current_detection_rate # Add a new value. else: # self.x_dtec = self.x_dtec[1:] # Remove the first element. x_dtec_last = self.x_dtec[-1] self.x_dtec.append(x_dtec_last + 0.5) # Add a new value 1 higher than the last. self.y_dtec.append(current_detection_rate) self.detection_rate.setText(str("{:.2f}".format(current_detection_rate))) # self.data_line_dtec.setData(self.x_dtec, self.y_dtec) # Set the maximum number of data points to display max_display_points = 200 # Downsample the data if needed if len(self.x_dtec) > max_display_points and not self.change_detection_rate_range: step = len(self.x_dtec) // max_display_points x_dtec_downsampled = self.x_dtec[::step] y_dtec_downsampled = self.y_dtec[::step] self.data_line_dtec.setData(x_dtec_downsampled, y_dtec_downsampled) elif len(self.x_dtec) > max_display_points and self.change_detection_rate_range: x_dtec_downsampled = self.x_dtec[-max_display_points:] y_dtec_downsampled = self.y_dtec[-max_display_points:] self.data_line_dtec.setData(x_dtec_downsampled, y_dtec_downsampled) else: self.data_line_dtec.setData(self.x_dtec, self.y_dtec) # Increase the index # with self.variables.lock_statistics: self.index_plot += 1 # mass spectrum if self.counter_source == 'TDC' and self.variables.total_ions > 0 and self.index_wait_on_plot_start > 16: # Drain all four ring buffers in one shot (zero-copy NumPy # slices, no IPC). Each call returns every sample produced # since the last call and trims the four arrays to the # minimum length so they remain aligned per-ion if one buffer # happens to lag the others by a tick. xx = self.x_plot.read_all() yy = self.y_plot.read_all() tt = self.t_plot.read_all() main_v_dc_dld = self.main_v_dc_plot.read_all() n = min(len(xx), len(yy), len(tt), len(main_v_dc_dld)) if n == 0: xx = np.array([]) yy = np.array([]) tt = np.array([]) main_v_dc_dld = np.array([]) else: xx = xx[:n] yy = yy[:n] tt = tt[:n] main_v_dc_dld = main_v_dc_dld[:n] # self.length_events += len(self.tt) self.length_events += len(tt) # All ring-buffer writes go through the lock so the # background calibration worker's snapshot can never see a # half-updated buffer (e.g. concatenated v_dc but pre-trim # t / x / y after the 100 k cap kicks in). with self._buffer_lock: if len(self.last_100_thousand_v) == 0: self.last_100_thousand_det_x_heatmap = xx self.last_100_thousand_det_y_heatmap = yy mask_t = tt < self.conf["max_tof"] self.last_100_thousand_v = main_v_dc_dld[mask_t] self.last_100_thousand_det_x = xx[mask_t] self.last_100_thousand_det_y = yy[mask_t] self.last_100_thousand_t = tt[mask_t] else: self.last_100_thousand_det_x_heatmap = np.concatenate((self.last_100_thousand_det_x_heatmap, xx)) self.last_100_thousand_det_y_heatmap = np.concatenate((self.last_100_thousand_det_y_heatmap, yy)) mask_t = tt < self.conf["max_tof"] self.last_100_thousand_v = np.concatenate((self.last_100_thousand_v, main_v_dc_dld[mask_t])) self.last_100_thousand_det_x = np.concatenate((self.last_100_thousand_det_x, xx[mask_t])) self.last_100_thousand_det_y = np.concatenate((self.last_100_thousand_det_y, yy[mask_t])) self.last_100_thousand_t = np.concatenate((self.last_100_thousand_t, tt[mask_t])) if len(self.last_100_thousand_v) > 100000: self.last_100_thousand_v = self.last_100_thousand_v[-100000:] self.last_100_thousand_det_x = self.last_100_thousand_det_x[-100000:] self.last_100_thousand_det_x_heatmap = self.last_100_thousand_det_x_heatmap[-100000:] self.last_100_thousand_det_y = self.last_100_thousand_det_y[-100000:] self.last_100_thousand_det_y_heatmap = self.last_100_thousand_det_y_heatmap[-100000:] self.last_100_thousand_t = self.last_100_thousand_t[-100000:] try: if self.variables.pulse_mode == 'Voltage': t_0 = self.conf["t_0_voltage"] elif self.variables.pulse_mode == 'Laser' or self.variables.pulse_mode == 'VoltageLaser': t_0 = self.conf["t_0_laser"] # The toggle only chooses which histogram is *displayed*. # Both calibrated and uncalibrated accumulators are # updated below so flipping the button never loses prior # events. use_calibration_display = not self.uncalibrated_mode and self._calib_params is not None def _get_tof_mc(t_arr, v_arr, x_arr, y_arr, use_cal): """Return (tof, mc) arrays. ``use_cal`` applies live cal if available.""" if use_cal and self._calib_params is not None: corrected = live_calibration.apply_corrections( t_arr, v_arr, x_arr, y_arr, self._calib_params, ) if corrected is not None: return corrected # (t_corr, mc_corr) # Raw fallback: simple geometry-only mc, t unchanged. mc_raw = tof2mc_simple.tof_2_mc( t_arr, t_0, v_arr, x_arr, y_arr, flightPathLength=self.conf["flight_path_length"], ) return t_arr, mc_raw if self.mc_tof_last_events_flag and self.conf["visualization"] == "tof": t_le = self.last_100_thousand_t[-self.num_event_mc_tof :] v_le = self.last_100_thousand_v[-self.num_event_mc_tof :] x_le = self.last_100_thousand_det_x[-self.num_event_mc_tof :] y_le = self.last_100_thousand_det_y[-self.num_event_mc_tof :] tt_last_events, _ = _get_tof_mc(t_le, v_le, x_le, y_le, use_calibration_display) hist_tof_last_events, _ = np.histogram(tt_last_events, bins=self.bins_tof) elif self.mc_tof_last_events_flag and self.conf["visualization"] == "mc": t_le = self.last_100_thousand_t[-self.num_event_mc_tof :] v_le = self.last_100_thousand_v[-self.num_event_mc_tof :] x_le = self.last_100_thousand_det_x[-self.num_event_mc_tof :] y_le = self.last_100_thousand_det_y[-self.num_event_mc_tof :] _, mc_last_events = _get_tof_mc(t_le, v_le, x_le, y_le, use_calibration_display) hist_mc_last_events, _ = np.histogram(mc_last_events, bins=self.bins_mc) # Cumulative histograms always come from the new tick's # events only -- no need to re-bin the whole ring # buffer. We bin the same events *twice* (once with the # live calibration applied, once raw) so each # accumulator independently holds the complete history # of every event in its own bin space. When no # calibration params have arrived yet, the two outputs # are identical and we reuse the result. batch_t = tt[mask_t] batch_v = main_v_dc_dld[mask_t] batch_x = xx[mask_t] batch_y = yy[mask_t] tof_evt_calib, mc_evt_calib = _get_tof_mc(batch_t, batch_v, batch_x, batch_y, True) if self._calib_params is None: tof_evt_uncalib, mc_evt_uncalib = tof_evt_calib, mc_evt_calib else: tof_evt_uncalib, mc_evt_uncalib = _get_tof_mc( batch_t, batch_v, batch_x, batch_y, False ) hist_tof_calib_batch, _ = np.histogram(tof_evt_calib, bins=self.bins_tof) self.hist_tof += hist_tof_calib_batch hist_mc_calib_batch, _ = np.histogram(mc_evt_calib, bins=self.bins_mc) self.hist_mc += hist_mc_calib_batch hist_tof_uncalib_batch, _ = np.histogram(tof_evt_uncalib, bins=self.bins_tof) self.hist_tof_uncalib += hist_tof_uncalib_batch hist_mc_uncalib_batch, _ = np.histogram(mc_evt_uncalib, bins=self.bins_mc) self.hist_mc_uncalib += hist_mc_uncalib_batch # Pick which cumulative series to display this tick. cumul_hist_tof = self.hist_tof_uncalib if self.uncalibrated_mode else self.hist_tof cumul_hist_mc = self.hist_mc_uncalib if self.uncalibrated_mode else self.hist_mc self.histogram.clear() if self.conf["visualization"] == "tof" and not self.mc_tof_last_events_flag: hist = np.copy(cumul_hist_tof[: self.index_hist_tof]) hist[hist == 0] = 1 # Avoid log(0) error bins = self.bins_tof[: self.index_hist_tof + 1] self.histogram.plot( bins, hist, stepMode="center", fillLevel=0, fillOutline=True, brush='black', name="num events: %s" % self.length_events, ) elif self.conf["visualization"] == "mc" and not self.mc_tof_last_events_flag: hist = np.copy(cumul_hist_mc[: self.index_hist_mc]) hist[hist == 0] = 1 # Avoid log(0) error bins = self.bins_mc[: self.index_hist_mc + 1] self.histogram.plot( bins, hist, stepMode="center", fillLevel=0, fillOutline=True, brush='black', name="num events: %s" % self.length_events, ) elif self.conf["visualization"] == "tof" and self.mc_tof_last_events_flag: # remobe the bins bigger than the max_tof hist = np.copy(hist_tof_last_events[: self.index_hist_tof]) hist[hist == 0] = 1 # Avoid log(0) error bins = self.bins_tof[: self.index_hist_tof + 1] self.histogram.plot( bins, hist, stepMode="center", fillLevel=0, fillOutline=True, brush='black', name="num events: %s" % self.length_events, ) elif self.conf["visualization"] == "mc" and self.mc_tof_last_events_flag: # remobe the bins bigger than the max_mc hist = np.copy(hist_mc_last_events[: self.index_hist_mc]) hist[hist == 0] = 1 # Avoid log(0) error bins = self.bins_mc[: self.index_hist_mc + 1] self.histogram.plot( bins, hist, stepMode="center", fillLevel=0, fillOutline=True, brush='black', name="num events: %s" % self.length_events, ) except Exception as e: print( f"{initialize_devices.bcolors.FAIL}Error: Cannot plot Histogram correctly{initialize_devices.bcolors.ENDC}" ) print(e) # Hitmap and FDM are now rendered every tick into two # separate panels (detector_heatmap + detector_fdm). The # heatmap_fdm_switch toggle is gone. hist, xedges, yedges = np.histogram2d( xx * 10, yy * 10, bins=self.bins_detector, range=self.range, ) # --- Hitmap (left panel) ------------------------------------- if self.variables.reset_heatmap: self.variables.reset_heatmap = False self.last_100_thousand_det_x_heatmap = np.array([]) self.last_100_thousand_det_y_heatmap = np.array([]) x_last_events = self.last_100_thousand_det_x_heatmap[:] y_last_events = self.last_100_thousand_det_y_heatmap[:] self.scatter.setSize(self.hitmap_plot_size.value()) x = (x_last_events * 10)[-self.num_hit_display :] y = (y_last_events * 10)[-self.num_hit_display :] self.hitmap_count.setText(str(len(x))) self.scatter.clear() self.scatter.setData(x=x, y=y) self.detector_heatmap.clear() self.detector_heatmap.addItem(self.scatter) self.detector_heatmap.addItem(self.detector_circle) # --- FDM (right panel) --------------------------------------- # Two modes, switched by the Last Events toggle button: # * OFF (default): accumulate every ion into hist_fdm # forever; fdm_count = total ions seen. # * ON : keep a sliding window of the most recent # fdm_max ions and rebuild hist_fdm from # that window every refresh. try: fdm_max = max(1, int(float(self.fdm_max_ions.text()))) except (ValueError, AttributeError): fdm_max = 1_000_000 new_events = int(np.sum(hist)) if self._fdm_use_last_events: # Append the new chunk's per-ion (x_mm, y_mm) into the # sliding window, then trim to the last fdm_max entries. self._fdm_window_x = np.concatenate((self._fdm_window_x, (xx * 10).astype(np.float32)))[-fdm_max:] self._fdm_window_y = np.concatenate((self._fdm_window_y, (yy * 10).astype(np.float32)))[-fdm_max:] win_hist, _, _ = np.histogram2d( self._fdm_window_x, self._fdm_window_y, bins=self.bins_detector, range=self.range, ) self.hist_fdm = np.log10(win_hist + 1) self._fdm_event_count = int(self._fdm_window_x.size) else: # Plain accumulate-forever path. self.hist_fdm += np.log10(hist + 1) self._fdm_event_count += new_events self.fdm_count.setText(str(self._fdm_event_count)) img_fdm = pg.ImageItem() img_fdm.setImage(np.copy(self.hist_fdm)) img_fdm.setRect( QtCore.QRectF( xedges[0], yedges[0], xedges[-1] - xedges[0], yedges[-1] - yedges[0], ) ) lut = pg.colormap.get('viridis').getLookupTable(start=0.0, stop=1.0, nPts=256) img_fdm.setLookupTable(lut) self.detector_fdm.clear() self.detector_fdm.addItem(img_fdm) self.detector_fdm.addItem(self.detector_circle_fdm) self.detector_fdm.getViewBox().setAspectLocked(True)
[docs] def update_graphs( self, ): """ Update the graphs Args: None Return: None """ if self.variables.plot_clear_flag: self.x_vdc = [i * 0.5 for i in range(200)] # 100 time points self.y_vdc = [0.0] * 200 # 200 data points, all initialized to 0.0 self.y_vdc[:] = [np.nan] * len(self.y_vdc) self.vdc_time.clear() pen_vdc = pg.mkPen(color=(255, 0, 0), width=3) self.data_line_vdc = self.vdc_time.plot(self.x_vdc, self.y_vdc, pen=pen_vdc) self.x_dtec = [i * 0.5 for i in range(200)] # 100 time points self.y_dtec = [0.0] * 200 # 200 data points, all initialized to 0.0 self.y_dtec[:] = [np.nan] * len(self.y_vdc) self.detection_rate_viz.clear() pen_dtec = pg.mkPen(color=(255, 0, 0), width=3) self.data_line_dtec = self.detection_rate_viz.plot(self.x_dtec, self.y_dtec, pen=pen_dtec) self.histogram.clear() self.detector_heatmap.clear() self.detector_heatmap.addItem(self.detector_circle) # Reset the FDM panel too. self.detector_fdm.clear() self.detector_fdm.addItem(self.detector_circle_fdm) self.hist_fdm[:] = 0.0 self._fdm_event_count = 0 self._fdm_window_x = np.array([], dtype=np.float32) self._fdm_window_y = np.array([], dtype=np.float32) self.fdm_count.setText("0") self.variables.plot_clear_flag = False self.index_plot = 0 self.index_plot_start = 0 self.index_plot_save = 0 self.start_time_metadata = 0 self.variables.detection_rate_current_plot = 0 self.last_100_thousand_det_x_heatmap = np.array([]) self.last_100_thousand_det_x = np.array([]) self.last_100_thousand_det_y_heatmap = np.array([]) self.last_100_thousand_det_y = np.array([]) self.last_100_thousand_t = np.array([]) self.last_100_thousand_v = np.array([]) self.length_events = 0 self.hist_fdm, xedges, yedges = np.histogram2d([], [], bins=self.bins_detector, range=self.range) self._fdm_event_count = 0 self._fdm_window_x = np.array([], dtype=np.float32) self._fdm_window_y = np.array([], dtype=np.float32) self.fdm_count.setText("0") self.hist_mc = np.zeros(len(self.bins_mc) - 1) self.hist_tof = np.zeros(len(self.bins_tof) - 1) self.hist_mc_uncalib = np.zeros(len(self.bins_mc) - 1) self.hist_tof_uncalib = np.zeros(len(self.bins_tof) - 1) if self.index_auto_scale_graph == 30: self.vdc_time.enableAutoRange(axis='x') self.histogram.enableAutoRange(axis='y') self.detection_rate_viz.enableAutoRange(axis='x') self.detection_rate_viz.enableAutoRange(axis='y') self.detector_heatmap.enableAutoRange(axis='x') self.detector_heatmap.enableAutoRange(axis='y') self.index_auto_scale_graph = 0 # with self.variables.lock_statistics and self.variables.lock_setup_parameters: if self.variables.start_flag and self.variables.flag_visualization_start: self.index_auto_scale_graph += 1 self.update_graphs_helper() # save plots to the file if time.time() - self.start_time_metadata >= self.variables.save_meta_interval_visualization: self.path_meta = self.variables.path_meta exporter = pg.exporters.ImageExporter(self.vdc_time.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.variables.path_meta + '/visualization_v_dc_p_%s.png' % self.index_plot_save) exporter = pg.exporters.ImageExporter(self.detection_rate_viz.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.path_meta + '/visualization_detection_rate_%s.png' % self.index_plot_save) # Hitmap and FDM are now separate panels - export both. exporter = pg.exporters.ImageExporter(self.detector_heatmap.plotItem) exporter.params['width'] = 1000 exporter.params['height'] = 800 exporter.export(self.path_meta + '/visualization_detector_hitmap_%s.png' % self.index_plot_save) exporter = pg.exporters.ImageExporter(self.detector_fdm.plotItem) exporter.params['width'] = 1000 exporter.params['height'] = 800 exporter.export(self.path_meta + '/visualization_detector_fdm_%s.png' % self.index_plot_save) exporter = pg.exporters.ImageExporter(self.histogram.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.path_meta + '/visualization_mc_tof_%s.png' % self.index_plot_save) screenshot = QtWidgets.QApplication.primaryScreen().grabWindow(self.visualization_window.winId()) screenshot.save(self.path_meta + '/visualization_screenshot_%s.png' % self.index_plot_save, 'png') self.start_time_metadata = time.time() # Increase the index self.index_plot_save += 1 elif self.variables.last_screen_shot: self.path_meta = self.variables.path_meta if self.variables.vdc_hold: self.dc_hold.click() # (No more heatmap_fdm_switch click - both views are always # rendered into their own panels.) if self.mc_tof_last_events_flag: self.spectrum_last_events_switch.click() if self.change_detection_rate_range: self.detection_rate_range_switch.click() if self.conf["visualization"] == "tof": self.spectrum_switch.click() self.update_graphs_helper() exporter = pg.exporters.ImageExporter(self.vdc_time.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.path_meta + '/visualization_v_dc_p_final.png') exporter = pg.exporters.ImageExporter(self.detection_rate_viz.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.path_meta + '/visualization_detection_rate_final.png') # Hitmap panel exporter = pg.exporters.ImageExporter(self.detector_heatmap.plotItem) exporter.params['width'] = 1000 exporter.params['height'] = 800 exporter.export(self.path_meta + '/visualization_detector_hitmap_final.png') # FDM panel exporter = pg.exporters.ImageExporter(self.detector_fdm.plotItem) exporter.params['width'] = 1000 exporter.params['height'] = 800 exporter.export(self.path_meta + '/visualization_detector_fdm_final.png') exporter = pg.exporters.ImageExporter(self.histogram.plotItem) exporter.params['width'] = 1000 # Set the width of the image exporter.params['height'] = 800 # Set the height of the image exporter.export(self.path_meta + '/visualization_mc_tof_final.png') screenshot = QtWidgets.QApplication.primaryScreen().grabWindow(self.visualization_window.winId()) screenshot.save(self.path_meta + '/visualization_screenshot_final.png', 'png') self.variables.last_screen_shot = False
[docs] def spectrum_switch_mc_tof(self): """ Switch between mass spectrum and time of flight spectrum Args: None Return: None """ if self.conf["visualization"] == "tof": self.conf["visualization"] = "mc" self.histogram.setLabel("bottom", "m/c", units='Da', **self.styles) elif self.conf["visualization"] == "mc": self.conf["visualization"] = "tof" self.histogram.setLabel("bottom", "Time", units='ns', **self.styles) # The TOF and MC pipelines are different (TOF: prescale + vol + # bowl; MC: bowl_init + vol + bowl_final). Restart the worker # so it refits for the new mode and clear the cached params / # cumulative histograms so the displayed spectrum doesn't mix # bins from one mode's fit with another. self._calib_params = None self._reset_cumulative_histograms() self._stop_live_calibration_worker() self._set_calib_status_text("calibrating…") self._start_live_calibration_worker()
# ---------------------------------------------------------------- live cal
[docs] def uncalibrate_clicked(self): """Toggle "Uncalibrate" state on the live spectrum. Default = NOT pressed = show CALIBRATED data. Pressed (green) = bypass corrections, show raw mc/tof. """ self.uncalibrated_mode = not self.uncalibrated_mode if self.uncalibrated_mode: self.uncalibrate_switch.setStyleSheet("QPushButton{background: rgb(0, 255, 26)}") else: self.uncalibrate_switch.setStyleSheet(self.original_button_style)
# NOTE: we deliberately do NOT clear the cumulative histograms # here. Both calibrated and uncalibrated accumulators are kept # in parallel by the update loop, so toggling the button just # swaps the displayed series — every event hit so far stays # visible. Only "Last Events" (and a session restart) prunes # what's shown. def _reset_cumulative_histograms(self, *, calib=True, uncalib=True): """Clear cumulative histograms. ``calib`` clears the calibrated series; ``uncalib`` the raw series. By default both are cleared, which is what callers like the TOF↔MC switch and the session re-init want. The new-calibration-parameters callback passes ``uncalib=False`` because the raw bins are unaffected by parameter changes. """ try: if calib: self.hist_tof.fill(0) self.hist_mc.fill(0) if uncalib: self.hist_tof_uncalib.fill(0) self.hist_mc_uncalib.fill(0) except Exception: pass def _calibration_snapshot(self): """Snapshot callback handed to the LiveCalibrationWorker. Returns the 100 000-event ring buffer's contents as plain numpy arrays, or ``None`` when there is not yet enough data. Runs on the worker thread; never touches Qt widgets. Takes the same ``_buffer_lock`` as the writer in update_graphs_helper so the snapshot is guaranteed consistent across the four arrays even when the GUI thread is mid-concatenate. """ try: with self._buffer_lock: t = self.last_100_thousand_t v = self.last_100_thousand_v x = self.last_100_thousand_det_x y = self.last_100_thousand_det_y if t is None or t.size == 0: return None # Lengths can desync briefly across the four arrays # while update_graphs_helper concatenates one at a time. # The lock above already prevents that, but trim to the # common length defensively in case any future code # path bypasses the lock. n = min(len(t), len(v), len(x), len(y)) if n == 0: return None return t[-n:].copy(), v[-n:].copy(), x[-n:].copy(), y[-n:].copy() except Exception: return None def _start_live_calibration_worker(self): """Spin up the background fitter, unless disabled in config.""" try: interval = float(self.conf.get("live_calibration_refit_interval_s", 15.0)) except (TypeError, ValueError): interval = 15.0 if interval <= 0: # Operator disabled live calibration entirely. self._set_calib_status_text("disabled") return # Tell the worker which t_0 to use by hinting at the active pulse mode. try: pulse_mode = str(getattr(self.variables, "pulse_mode", "")).strip() self.conf["_active_pulse_mode_is_laser"] = pulse_mode in {"Laser", "VoltageLaser"} except Exception: self.conf["_active_pulse_mode_is_laser"] = False # Pick the calibration mode that matches the active visualization # view. The two pipelines are different (see live_calibration # docstring): TOF starts with a sqrt(V/V̄) prescaling, MC starts # with a bowl-only initial step. self.conf["live_calibration_mode"] = "mc" if self.conf.get("visualization", "tof") == "mc" else "tof" self._calib_worker = live_calibration.LiveCalibrationWorker( self._calibration_snapshot, self.conf, ) self._calib_worker.parameters_updated.connect(self._on_calibration_params_updated) self._calib_worker.status_changed.connect(self._on_calibration_status_changed) self._calib_worker.start() def _on_calibration_params_updated(self, params): """GUI-thread slot that atomically swaps in new calibration params.""" self._calib_params = params if params is not None: self._set_calib_status_text( f"calibrated (R²={params.fit_quality:.2f}, n={params.num_events_used})", ok=True, ) # Old calibrated bins were computed under the previous # parameters; clear *only* the calibrated accumulator so # the displayed spectrum reflects events binned with the # new calibration. The uncalibrated accumulator's bin # meanings don't depend on calibration params, so it keeps # the full event history. self._reset_cumulative_histograms(uncalib=False) def _on_calibration_status_changed(self, text): """GUI-thread slot for the worker's human-readable status.""" self._set_calib_status_text(text) def _set_calib_status_text(self, text, *, ok=False): self._calib_status_text = text try: color = "#0a7d20" if ok else "#666666" self.calib_status_label.setText(f"live cal: {text}") self.calib_status_label.setStyleSheet(f"QLabel{{color:{color};}}") except Exception: pass def _stop_live_calibration_worker(self): """Stop the background fitter cleanly; called from .stop().""" worker = self._calib_worker self._calib_worker = None if worker is None: return try: worker.stop() worker.wait(2000) # ms except Exception: pass
[docs] def spectrum_last_events(self): """ Display the last events in the mass spectrum Args: None Return: None """ self.mc_tof_last_events_flag = not self.mc_tof_last_events_flag if self.mc_tof_last_events_flag: self.spectrum_last_events_switch.setStyleSheet("QPushButton{\nbackground: rgb(0, 255, 26)\n}") else: self.spectrum_last_events_switch.setStyleSheet(self.original_button_style)
[docs] def parameters_changes(self): """ Change the parameters for the mass spectrum Args: None Return: None """ if self.num_last_events.text().isdigit(): num_last_event_tmp = int(self.num_last_events.text()) if num_last_event_tmp > 100000: self.num_last_events_val = 100000 self.num_last_events.setText("100000") else: self.num_event_mc_tof = num_last_event_tmp if self.max_mc.text().isdigit(): max_mc_tmp = int(self.max_mc.text()) if max_mc_tmp > self.conf["max_mass"]: self.max_mc_val = self.conf["max_mass"] self.max_mc.setText(str(self.conf["max_mass"])) self.index_hist_mc = np.where(self.bins_mc == self.max_mc_val)[0][0] else: self.max_mc_val = max_mc_tmp self.index_hist_mc = np.where(self.bins_mc == self.max_mc_val)[0][0] if self.max_tof.text().isdigit(): max_tof_tmp = int(self.max_tof.text()) if max_tof_tmp > self.conf["max_tof"]: self.max_tof_val = self.conf["max_tof"] self.max_tof.setText(str(self.conf["max_tof"])) self.index_hist_tof = np.where(self.bins_tof == self.max_tof_val)[0][0] else: self.max_tof_val = max_tof_tmp self.index_hist_tof = np.where(self.bins_tof == self.max_tof_val)[0][0] if self.hit_displayed.text().isdigit(): if int(float(self.hit_displayed.text())) > 100000: self.error_message("Maximum possible number is 100000") _translate = QtCore.QCoreApplication.translate self.hit_displayed.setText(_translate("PyCCAPT", "100000")) else: self.num_hit_display = int(float(self.hit_displayed.text()))
[docs] def error_message(self, message): """ Display an error message and start a timer to hide it after 8 seconds Args: message (str): Error message to display Return: None """ _translate = QtCore.QCoreApplication.translate self.Error.setText( _translate( "OXCART", "<html><head/><body><p><span style=\" color:#ff0000;\">" + message + "</span></p></body></html>" ) ) self.timer.start(8000)
[docs] def hideMessage( self, ): """ Hide the message and stop the timer Args: None Return: None """ # Hide the message and stop the timer _translate = QtCore.QCoreApplication.translate self.Error.setText( _translate("OXCART", "<html><head/><body><p><span style=\" color:#ff0000;\"></span></p></body></html>") ) self.timer.stop()
[docs] def stop(self): """ Stop any background activity Args: None Return: None """ # Stop the live-calibration QThread cleanly so the visualization # subprocess can exit. The worker's ``stop()`` sets a flag that # its ``run()`` loop checks every 200 ms; ``wait(2000)`` gives it # up to 2 s to actually exit. self._stop_live_calibration_worker()
[docs] def efficient_histogram(viz, bin_size): bins = np.arange(np.min(viz), np.max(viz) + bin_size, bin_size) hist, edges = np.histogram(viz, bins=bins) hist[hist == 0] = 1 # Avoid log(0) return hist, edges
[docs] class VisualizationWindow(QtWidgets.QWidget): """ Widget for the Visualization window. """ closed = QtCore.pyqtSignal() # Define a custom closed signal def __init__(self, variables, gui_visualization, visualization_close_event, command_queue, *args, **kwargs): """ Constructor for the VisualizationWindow class. Args: variables: Shared variables. gui_visualization: Instance of the Visualization. visualization_close_event: multiprocessing.Event signalled by this window when closed by the user. command_queue: multiprocessing.Queue of typed string commands from the main GUI ("show", "show_front", "hide"). """ super().__init__(*args, **kwargs) self.gui_visualization = gui_visualization self.variables = variables self.command_queue = command_queue self.visualization_close_event = visualization_close_event # Diagnostic: log the first few QTimer ticks + every command we # receive to files/logs/visualization_subprocess.log so we can # tell whether the timer fires and the queue is being drained. self._diag_ticks_logged = 0 self._diag_log_path = None try: from pyccapt.control.core import runtime as _runtime self._diag_log_path = _runtime.project_path("files", "logs", "visualization_subprocess.log") except Exception: pass # Start hidden - check_if_should() below brings the window up the # first time a "show" command arrives on the queue. self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.check_if_should) self.timer.start(500)
[docs] def closeEvent(self, event): """ Don't actually close - hide the window so the subprocess stays alive and the next "open" from the main GUI is instant. Using hide() (not showMinimized) avoids leaving a leftover minimised stub in the taskbar / desktop. """ event.ignore() self.hide() self.visualization_close_event.set()
[docs] def check_if_should(self): """Drain the command queue and dispatch each message in order.""" # Diagnostic: confirm the QTimer is actually firing (first 3 ticks # only, to avoid filling the log). if self._diag_ticks_logged < 3 and self._diag_log_path is not None: try: import datetime as _dt with open(self._diag_log_path, "a", encoding="utf-8") as fh: fh.write(f"[{_dt.datetime.now().isoformat()}] timer tick #{self._diag_ticks_logged + 1}\n") except Exception: pass self._diag_ticks_logged += 1 raise_to_front = False make_visible = False hide = False drained_msgs = [] while True: try: msg = self.command_queue.get_nowait() except Exception: break drained_msgs.append(msg) if msg == "show": make_visible = True elif msg == "show_front": make_visible = True raise_to_front = True elif msg == "hide": hide = True if drained_msgs and self._diag_log_path is not None: try: import datetime as _dt with open(self._diag_log_path, "a", encoding="utf-8") as fh: fh.write(f"[{_dt.datetime.now().isoformat()}] received: {drained_msgs}\n") except Exception: pass if hide and not make_visible: self.hide() return if not make_visible: return # Always call show() + showNormal() unconditionally. After a # previous closeEvent->hide() Qt may not honour a single show() # call on every platform; the explicit showNormal() also brings # the window out of a minimised state if it's been there. We # deliberately do NOT toggle setWindowFlags() - that hides the # widget on Windows (Qt docs). self.show() self.showNormal() self.raise_() if raise_to_front: self.activateWindow()
[docs] def setWindowStyleFusion(self): # Set the Fusion style QtWidgets.QApplication.setStyle("Fusion")
[docs] def run_visualization_window( variables, conf, visualization_closed_event, visualization_command_queue, x_plot, y_plot, t_plot, main_v_dc_plot ): """ Run the Cameras window in a separate process. Args: variables: Shared variables. conf: Configuration dictionary. visualization_closed_event: Event for the Visualization window closed. visualization_win_front: Event for the Visualization window front. x_plot: x plot y_plot: y plot t_plot: t plot main_v_dc_plot: main v dc plot Return: None """ # Every subprocess startup writes a one-line breadcrumb to the log. # If the visualization subprocess never gets that far the file stays # empty and we know the unpickling of the Process args failed before # this body even ran. Crashes inside this body land in the same # file with a full traceback. import os import traceback import datetime as _dt log_path = None try: log_path = runtime.project_path("files", "logs", "visualization_subprocess.log") log_path.parent.mkdir(parents=True, exist_ok=True) with open(log_path, "a", encoding="utf-8") as fh: fh.write(f"[{_dt.datetime.now().isoformat()}] pid={os.getpid()} startup\n") except Exception: pass try: app = QtWidgets.QApplication(sys.argv) app.setStyle('Fusion') app.setQuitOnLastWindowClosed(False) gui_visualization = Ui_Visualization(variables, conf, x_plot, y_plot, t_plot, main_v_dc_plot) Cameras_alignment = VisualizationWindow( variables, gui_visualization, visualization_closed_event, visualization_command_queue, flags=QtCore.Qt.WindowType.Tool, ) gui_visualization.setupUi(Cameras_alignment) try: if log_path is not None: with open(log_path, "a", encoding="utf-8") as fh: fh.write(f"[{_dt.datetime.now().isoformat()}] setupUi finished, entering app.exec()\n") except Exception: pass sys.exit(app.exec()) except Exception: try: if log_path is not None: with open(log_path, "a", encoding="utf-8") as fh: fh.write(f"[{_dt.datetime.now().isoformat()}] CRASH:\n") traceback.print_exc(file=fh) except Exception: pass traceback.print_exc() raise
if __name__ == "__main__": try: conf, _ = runtime.load_project_config() except Exception as exc: print('Can not load the configuration file') print(exc) sys.exit() shared = runtime.create_shared_context(conf) app = QtWidgets.QApplication(sys.argv) app.setStyle('Fusion') Visualization = QtWidgets.QWidget() ui = Ui_Visualization( shared.variables, conf, shared.x_plot, shared.y_plot, shared.t_plot, shared.main_v_dc_plot, ) ui.setupUi(Visualization) Visualization.show() sys.exit(app.exec())