class Base_Plot(QtCore.QObject): def __init__(self, parent, widget, mpl_layout): super().__init__(parent) self.parent = parent self.widget = widget self.mpl_layout = mpl_layout self.fig = mplfigure.Figure() mpl.scale.register_scale(AbsoluteLogScale) mpl.scale.register_scale(BiSymmetricLogScale) # Set plot variables self.x_zoom_constraint = False self.y_zoom_constraint = False self.create_canvas() self.NavigationToolbar(self.canvas, self.widget, coordinates=True) # AutoScale self.autoScale = [True, True] # Connect Signals self._draw_event_signal = self.canvas.mpl_connect('draw_event', self._draw_event) self.canvas.mpl_connect('button_press_event', lambda event: self.click(event)) self.canvas.mpl_connect('key_press_event', lambda event: self.key_press(event)) # self.canvas.mpl_connect('key_release_event', lambda event: self.key_release(event)) self._draw_event() def create_canvas(self): self.canvas = FigureCanvas(self.fig) self.mpl_layout.addWidget(self.canvas) self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus) self.canvas.draw() # Set scales scales = {'linear': True, 'log': 0, 'abslog': 0, 'bisymlog': 0} for ax in self.ax: ax.scale = {'x': scales, 'y': deepcopy(scales)} ax.ticklabel_format(scilimits=(-4, 4), useMathText=True) # Get background for ax in self.ax: ax.background = self.canvas.copy_from_bbox(ax.bbox) def _find_calling_axes(self, event): for axes in self.ax: # identify calling axis if axes == event or (hasattr(event, 'inaxes') and event.inaxes == axes): return axes def set_xlim(self, axes, x): if not self.autoScale[0]: return # obey autoscale right click option if axes.get_xscale() in ['linear']: # range = np.abs(np.max(x) - np.min(x)) # min = np.min(x) - range*0.05 # if min < 0: # min = 0 # xlim = [min, np.max(x) + range*0.05] xlim = [np.min(x), np.max(x)] if 'log' in axes.get_xscale(): abs_x = np.abs(x) abs_x = abs_x[np.nonzero(abs_x)] # exclude 0's if axes.get_xscale() in ['log', 'abslog', 'bisymlog']: min_data = np.ceil(np.log10(np.min(abs_x))) max_data = np.floor(np.log10(np.max(abs_x))) xlim = [10**(min_data-1), 10**(max_data+1)] if np.isnan(xlim).any() or np.isinf(xlim).any(): pass elif xlim != axes.get_xlim(): # if xlim changes axes.set_xlim(xlim) def set_ylim(self, axes, y): if not self.autoScale[1]: return # obey autoscale right click option min_data = np.array(y)[np.isfinite(y)].min() max_data = np.array(y)[np.isfinite(y)].max() if min_data == max_data: min_data -= 10**-1 max_data += 10**-1 if axes.get_yscale() == 'linear': range = np.abs(max_data - min_data) ylim = [min_data - range*0.1, max_data + range*0.1] elif axes.get_yscale() in ['log', 'abslog']: abs_y = np.abs(y) abs_y = abs_y[np.nonzero(abs_y)] # exclude 0's abs_y = abs_y[np.isfinite(abs_y)] # exclude nan, inf if abs_y.size == 0: # if no data, assign ylim = [10**-7, 10**-1] else: min_data = np.ceil(np.log10(np.min(abs_y))) max_data = np.floor(np.log10(np.max(abs_y))) ylim = [10**(min_data-1), 10**(max_data+1)] elif axes.get_yscale() == 'bisymlog': min_sign = np.sign(min_data) max_sign = np.sign(max_data) if min_sign > 0: min_data = np.ceil(np.log10(np.abs(min_data))) elif min_data == 0 or max_data == 0: pass else: min_data = np.floor(np.log10(np.abs(min_data))) if max_sign > 0: max_data = np.floor(np.log10(np.abs(max_data))) elif min_data == 0 or max_data == 0: pass else: max_data = np.ceil(np.log10(np.abs(max_data))) # TODO: ylim could be incorrect for neg/neg, checked for pos/pos, pos/neg ylim = [min_sign*10**(min_data-min_sign), max_sign*10**(max_data+max_sign)] if ylim != axes.get_ylim(): # if ylim changes, update axes.set_ylim(ylim) def update_xylim(self, axes, xlim=[], ylim=[]): data = self._get_data(axes) # on creation, there is no data, don't update if np.shape(data['x'])[0] < 2 or np.shape(data['y'])[0] < 2: return for (axis, lim) in zip(['x', 'y'], [xlim, ylim]): # Set Limits if len(lim) == 0: eval('self.set_' + axis + 'lim(axes, data["' + axis + '"])') else: eval('axes.set_' + axis + 'lim(lim)') # If bisymlog, also update scaling, C if eval('axes.get_' + axis + 'scale()') == 'bisymlog': self._set_scale(axis, 'bisymlog', axes) ''' # TODO: Do this some day, probably need to create annotation during canvas creation # Move exponent exp_loc = {'x': (.89, .01), 'y': (.01, .96)} eval(f'axes.get_{axis}axis().get_offset_text().set_visible(False)') ax_max = eval(f'max(axes.get_{axis}ticks())') oom = np.floor(np.log10(ax_max)).astype(int) axes.annotate(fr'$\times10^{oom}$', xy=exp_loc[axis], xycoords='axes fraction') ''' self._draw_event() # force a draw def _get_data(self, axes): # NOT Generic # get experimental data for axes data = {'x': [], 'y': []} if 'exp_data' in axes.item: data_plot = axes.item['exp_data'].get_offsets().T if np.shape(data_plot)[1] > 1: data['x'] = data_plot[0,:] data['y'] = data_plot[1,:] # append sim_x if it exists if 'sim_data' in axes.item and hasattr(axes.item['sim_data'], 'raw_data'): if axes.item['sim_data'].raw_data.size > 0: data['x'] = np.append(data['x'], axes.item['sim_data'].raw_data[:,0]) elif 'weight' in axes.item: data['x'] = axes.item['weight'].get_xdata() data['y'] = axes.item['weight'].get_ydata() elif any(key in axes.item for key in ['density', 'qq_data', 'sim_data']): name = np.intersect1d(['density', 'qq_data'], list(axes.item.keys()))[0] for n, coord in enumerate(['x', 'y']): xyrange = np.array([]) for item in axes.item[name]: if name == 'qq_data': coordData = item.get_offsets() if coordData.size == 0: continue else: coordData = coordData[:,n] elif name == 'density': coordData = eval('item.get_' + coord + 'data()') coordData = np.array(coordData)[np.isfinite(coordData)] if coordData.size == 0: continue xyrange = np.append(xyrange, [coordData.min(), coordData.max()]) xyrange = np.reshape(xyrange, (-1,2)) data[coord] = [np.min(xyrange[:,0]), np.max(xyrange[:,1])] return data def _set_scale(self, coord, type, event, update_xylim=False): def RoundToSigFigs(x, p): x = np.asarray(x) x_positive = np.where(np.isfinite(x) & (x != 0), np.abs(x), 10**(p-1)) mags = 10 ** (p - 1 - np.floor(np.log10(x_positive))) return np.round(x * mags) / mags # find correct axes axes = self._find_calling_axes(event) # for axes in self.ax: # if axes == event or (hasattr(event, 'inaxes') and event.inaxes == axes): # break # Set scale menu boolean if coord == 'x': shared_axes = axes.get_shared_x_axes().get_siblings(axes) else: shared_axes = axes.get_shared_y_axes().get_siblings(axes) for shared in shared_axes: shared.scale[coord] = dict.fromkeys(shared.scale[coord], False) # sets all types: False shared.scale[coord][type] = True # set selected type: True # Apply selected scale if type == 'linear': str = 'axes.set_{:s}scale("{:s}")'.format(coord, 'linear') elif type == 'log': str = 'axes.set_{0:s}scale("{1:s}", nonpos{0:s}="mask")'.format(coord, 'log') elif type == 'abslog': str = 'axes.set_{:s}scale("{:s}")'.format(coord, 'abslog') elif type == 'bisymlog': # default string to evaluate str = 'axes.set_{0:s}scale("{1:s}")'.format(coord, 'bisymlog') data = self._get_data(axes)[coord] if len(data) != 0: finite_data = np.array(data)[np.isfinite(data)] # ignore nan and inf min_data = finite_data.min() max_data = finite_data.max() if min_data != max_data: # if zero is within total range, find largest pos or neg range if np.sign(max_data) != np.sign(min_data): pos_data = finite_data[finite_data>=0] pos_range = pos_data.max() - pos_data.min() neg_data = finite_data[finite_data<=0] neg_range = neg_data.max() - neg_data.min() C = np.max([pos_range, neg_range]) else: C = np.abs(max_data-min_data) C /= 1E3 # scaling factor TODO: debating between 100, 500 and 1000 C = RoundToSigFigs(C, 1) # round to 1 significant figure str = 'axes.set_{0:s}scale("{1:s}", C={2:e})'.format(coord, 'bisymlog', C) eval(str) if type == 'linear' and coord == 'x': formatter = MathTextSciSIFormatter(useOffset=False, useMathText=True) axes.xaxis.set_major_formatter(formatter) elif type == 'linear' and coord == 'y': formatter = mpl.ticker.ScalarFormatter(useOffset=False, useMathText=True) formatter.set_powerlimits([-3, 4]) axes.yaxis.set_major_formatter(formatter) if update_xylim: self.update_xylim(axes) def _animate_items(self, bool=True): for axis in self.ax: if axis.get_legend() is not None: axis.get_legend().set_animated(bool) for item in axis.item.values(): if isinstance(item, list): for subItem in item: if isinstance(subItem, dict): subItem['line'].set_animated(bool) else: subItem.set_animated(bool) else: item.set_animated(bool) def _draw_items_artist(self): for axis in self.ax: # restore background first (needed for twinned plots) self.canvas.restore_region(axis.background) for axis in self.ax: for item in axis.item.values(): if isinstance(item, list): for subItem in item: if isinstance(subItem, dict): axis.draw_artist(subItem['line']) else: axis.draw_artist(subItem) else: axis.draw_artist(item) if axis.get_legend() is not None: axis.draw_artist(axis.get_legend()) self.canvas.update() # self.canvas.flush_events() # unnecessary? def set_background(self): self.canvas.draw_idle() # for when shock changes for axis in self.ax: # axis.background = self.canvas.copy_from_bbox(axis.bbox) axis.background = self.canvas.copy_from_bbox(self.fig.bbox) def _draw_event(self, event=None): # After redraw (new/resizing window), obtain new background self._animate_items(True) self.set_background() self._draw_items_artist() # self.canvas.draw_idle() # unnecessary? def clear_plot(self, ignore=[], draw=True): for axis in self.ax: if axis.get_legend() is not None: axis.get_legend().remove() for item in axis.item.values(): if hasattr(item, 'set_offsets'): # clears all data points if 'scatter' not in ignore: item.set_offsets(([np.nan, np.nan])) elif hasattr(item, 'set_xdata') and hasattr(item, 'set_ydata'): if 'line' not in ignore: item.set_xdata([np.nan, np.nan]) # clears all lines item.set_ydata([np.nan, np.nan]) elif hasattr(item, 'set_text'): # clears all text boxes if 'text' not in ignore: item.set_text('') if draw: self._draw_event() def click(self, event): if event.button == 3: # if right click if self.toolbar._active is None: self._popup_menu(event) # if self.toolbar._active is 'ZOOM': # if zoom is on, turn off # self.toolbar.press_zoom(event) # cancels current zooom # self.toolbar.zoom() # turns zoom off elif event.dblclick: # if double right click, go to default view self.toolbar.home() def key_press(self, event): if event.key == 'escape': if self.toolbar._active is 'ZOOM': # if zoom is on, turn off self.toolbar.zoom() # turns zoom off elif self.toolbar._active is 'PAN': self.toolbar.pan() # elif event.key == 'shift': elif event.key == 'x': # Does nothing, would like to make sticky constraint zoom/pan self.x_zoom_constraint = not self.x_zoom_constraint elif event.key == 'y': # Does nothing, would like to make sticky constraint zoom/pan self.y_zoom_constraint = not self.y_zoom_constraint elif event.key in ['s', 'l', 'L', 'k']: pass else: key_press_handler(event, self.canvas, self.toolbar) # def key_release(self, event): # print(event.key, 'released') def NavigationToolbar(self, *args, **kwargs): ## Add toolbar ## self.toolbar = CustomNavigationToolbar(self.canvas, self.widget, coordinates=True) self.mpl_layout.addWidget(self.toolbar) def _popup_menu(self, event): axes = self._find_calling_axes(event) # find axes calling right click if axes is None: return pos = self.parent.mapFromGlobal(QtGui.QCursor().pos()) popup_menu = QMenu(self.parent) xScaleMenu = popup_menu.addMenu('x-scale') yScaleMenu = popup_menu.addMenu('y-scale') for coord in ['x', 'y']: menu = eval(coord + 'ScaleMenu') for type in axes.scale[coord].keys(): action = QAction(type, menu, checkable=True) if axes.scale[coord][type]: # if it's checked action.setEnabled(False) else: action.setEnabled(True) menu.addAction(action) action.setChecked(axes.scale[coord][type]) fcn = lambda event, coord=coord, type=type: self._set_scale(coord, type, axes, True) action.triggered.connect(fcn) # Create menu for AutoScale options X Y All popup_menu.addSeparator() autoscale_options = ['AutoScale X', 'AutoScale Y', 'AutoScale All'] for n, text in enumerate(autoscale_options): action = QAction(text, menu, checkable=True) if n < len(self.autoScale): action.setChecked(self.autoScale[n]) else: action.setChecked(all(self.autoScale)) popup_menu.addAction(action) action.toggled.connect(lambda event, n=n: self._setAutoScale(n, event, axes)) popup_menu.exec_(self.parent.mapToGlobal(pos)) def _setAutoScale(self, choice, event, axes): if choice == len(self.autoScale): for n in range(len(self.autoScale)): self.autoScale[n] = event else: self.autoScale[choice] = event if event: # if something toggled true, update limits self.update_xylim(axes)
class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super(MainWindow, self).__init__() self.setWindowTitle('Data Analysis for NSOR project') self.setWindowIcon( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\window_icon.png')) ''' q actions that are intend to be in menu or toolbar ''' openFile = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\open_file.png'), '&Open File...', self) openFile.setShortcut('Ctrl+O') openFile.setStatusTip('Open the data file') openFile.triggered.connect(self.open_file) exitProgram = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\exit_program.png'), '&Exit', self) exitProgram.setShortcut("Ctrl+W") exitProgram.setStatusTip('Close the Program') exitProgram.triggered.connect(self.exit_program) renewData = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\renew.png'), '&Renew', self) renewData.setShortcut("Ctrl+R") renewData.setStatusTip('Reload the original data') renewData.triggered.connect(self.renew_data) self.verticalZoom = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\vertical_zoom.png'), '&Vertical Zoom', self) self.verticalZoom.setShortcut("Ctrl+Shift+V") self.verticalZoom.setStatusTip('Zoom in the vertical direction') self.verticalZoom.setCheckable(True) self.verticalZoom.toggled.connect(self.vzoom) self.horizontalZoom = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\horizontal_zoom.png'), '&Horizontal Zoom', self) self.horizontalZoom.setShortcut("Ctrl+Shift+H") self.horizontalZoom.setStatusTip('Zoom in the horizaontal direction') self.horizontalZoom.setCheckable(True) self.horizontalZoom.toggled.connect(self.hzoom) self.moveCursor = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\move_cursor.png'), '&Move Cursor', self) self.moveCursor.setShortcut("Ctrl+M") self.moveCursor.setStatusTip('Move cursors') self.moveCursor.setCheckable(True) self.moveCursor.toggled.connect(self.move_cursor) self.autoAxis = {} self.autoAxis['time_x'] = MyQAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_time_x.png'), '&Auto X axis (time)', 'time_x', self) self.autoAxis['time_y'] = MyQAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_time_y.png'), '&Auto Y axis (time)', 'time_y', self) self.autoAxis['freq_x'] = MyQAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_freq_x.png'), '&Auto X axis (freq)', 'freq_x', self) self.autoAxis['freq_y'] = MyQAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_freq_y.png'), '&Auto Y axis (freq)', 'freq_y', self) editParameters = QAction('&Edit Parameter', self) editParameters.setShortcut('Ctrl+E') editParameters.setStatusTip('open and edit the parameter file') editParameters.triggered.connect(self.edit_parameters) saveParameters = QAction('&Save Parameter', self) saveParameters.setShortcut('Ctrl+S') saveParameters.setStatusTip('save the parameters on screen to file') saveParameters.triggered.connect(self.save_parameters) self.data_type = QComboBox() self.data_type.setStatusTip( 'bin for legacy data recorded from labview program, big endian coded binary data, npy for numpy type data' ) self.data_type.addItems(['bin', '.npy']) ''' setting menubar ''' mainMenu = self.menuBar() #create a menuBar fileMenu = mainMenu.addMenu('&File') #add a submenu to the menu bar fileMenu.addAction( openFile) # add what happens when this menu is interacted fileMenu.addSeparator() fileMenu.addAction(exitProgram) # add an exit menu parameterMenu = mainMenu.addMenu('&Parameter') parameterMenu.addAction(editParameters) parameterMenu.addAction(saveParameters) ''' setting toolbar ''' self.toolbar = self.addToolBar( 'nsor_toolbar') #add a tool bar to the window if app.desktop().screenGeometry().height() == 2160: self.toolbar.setIconSize(QSize(100, 100)) else: self.toolbar.setIconSize(QSize(60, 60)) self.toolbar.addAction( openFile) # add what happens when this tool is interacted self.toolbar.addWidget(self.data_type) self.toolbar.addAction(renewData) self.toolbar.addSeparator() self.toolbar.addAction(self.verticalZoom) self.toolbar.addAction(self.horizontalZoom) self.toolbar.addAction(self.moveCursor) self.toolbar.addSeparator() for key, item in self.autoAxis.items(): self.autoAxis[key].setStatusTip( f'set {key} axis to size automatically') self.autoAxis[key].btnToggled.connect(self.auto_axis) self.toolbar.addAction(self.autoAxis[key]) self.statusBar() #create a status bar ''' setting matplotlib ''' if app.desktop().screenGeometry().height() == 2160: matplotlib.rcParams.update({'font.size': 28}) else: matplotlib.rcParams.update({'font.size': 14}) self.canvas = FigureCanvas(Figure(figsize=(40, 9))) self.fig = self.canvas.figure ''' setting axis as dictionary, containing two axes of time and freq ax['time'] ax['freq'] also initiate the vertical lines vline['time_l'] vline['time_r'] vline['freq_l'] vline['freq_r'] ''' self.ax = {} self.vline = {} self.ax['time'] = self.fig.add_subplot(121) self.ax['freq'] = self.fig.add_subplot(122) for axis in self.ax.values(): if app.desktop().screenGeometry().height() == 2160: axis.tick_params(pad=20) elif app.desktop().screenGeometry().height() == 1080: axis.tick_params(pad=10) # axis.ticklabel_format(style='sci', axis='y', scilimits=(0,0)) self.fourier_lb = QLabel("Ready", self) self.parameters = read_parameter(PARAMETER_FILE) ''' setting edits and labels as dictionary, representing all time and freq edits "file_name" is excluded "time_x_limit" "time_y_limit" "freq_x_limit" "freq_y_imit" "time_cursor" "freq_cursor" ''' self.edits = {} labels = {} for key, value in self.parameters.items(): if type(value) == list: val = str(value[0]) + ' ' + str(value[1]) if key == 'file_name': continue labels[key] = QLabel(key.replace('_', ' ').title(), self) self.edits[key] = MyLineEdit(key, val, self) self.edits[key].setStatusTip(f'{key}') self.edits[key].textModified.connect(self.limit_and_cursor) if key[0:4] == 'freq': self.edits[key].setFixedWidth(250) if 'cursor' in key: self.vline[key[0:4] + '_l'] = self.ax[key[0:4]].axvline( float(value[0]), c='red') self.vline[key[0:4] + '_r'] = self.ax[key[0:4]].axvline( float(value[1]), c='red') self.vline[key[0:4] + '_l'].set_animated(True) self.vline[key[0:4] + '_r'].set_animated(True) self.integral_label = QLabel('Peak Intensity: \n0', self) self.zeroPadPower = QComboBox(self) self.zeroPadPower.addItems(['x1', 'x2', 'x4', 'x8']) self.zeroPadPower.setStatusTip('This sets the zerofilling of the data') self.zeroPadPower.activated[str].connect(self.zero_padding) ''' phase stuff ''' self.toolbar.addSeparator() first_order_phase_check = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_phase_check.png'), '&First order on', self) first_order_phase_check.setStatusTip('Check to enbale 1st order phase') first_order_phase_check.setShortcut('Ctrl+F') first_order_phase_check.toggled.connect(self.first_order_phase_check) first_order_phase_check.setCheckable(True) self.toolbar.addAction(first_order_phase_check) auto_phase_btn = QAction( QIcon(BASE_FOLDER + r'\pyqt_analysis\icons\auto_phase_btn.png'), '&Auto Phase', self) auto_phase_btn.setStatusTip('Auto phase the peak (0th order only)') auto_phase_btn.setShortcut('Ctrl+A') auto_phase_btn.triggered.connect(self.auto_phase) self.toolbar.addAction(auto_phase_btn) self.zeroth_slider = QSlider(self) self.zeroth_slider.setMinimum(0) self.zeroth_slider.setMaximum(360) self.zeroth_slider.setValue(0) self.zeroth_slider.setTickInterval(1) self.zeroth_slider.valueChanged.connect(self.zeroth_order_phase) self.zeroth_slider.sliderReleased.connect(self.slider_released) self.first_slider = QSlider(self) self.first_slider.setMinimum(0) self.first_slider.setMaximum(360) self.first_slider.setValue(0) self.first_slider.hide() self.first_slider.valueChanged.connect(self.first_order_phase) self.phase_info = QLabel('Current Phase: \n0th: 0\n1st: 0 \nInt: 0', self) ''' setting layout ''' self._main = QWidget() self.setCentralWidget(self._main) layout1 = QHBoxLayout(self._main) layout2 = QVBoxLayout() layout3 = QVBoxLayout() layout4 = QVBoxLayout() layout5 = QHBoxLayout() for key in labels.keys(): if key[0:4] == 'time': layout2.addWidget(labels[key]) layout2.addWidget(self.edits[key]) elif key[0:4] == 'freq': layout4.addWidget(labels[key]) layout4.addWidget(self.edits[key]) layout4.addWidget(self.integral_label) layout4.addWidget(self.phase_info) layout4.addLayout(layout5) layout5.addWidget(self.zeroth_slider) layout5.addWidget(self.first_slider) layout2.addWidget(self.zeroPadPower) layout1.addLayout(layout2) layout2.addStretch(1) layout1.addLayout(layout3) layout3.addWidget(self.canvas) layout3.addWidget(self.fourier_lb) layout1.addLayout(layout4) # layout4.addStretch(1) self.threadpool = QThreadPool() #Multithreading ''' ################################################################################ phase ''' def slider_released(self): self.canvas.draw() key = 'freq' self.ax[key[0:4]].ticklabel_format( style='sci', axis='both', scilimits=(0, 0)) # format the tick label of the axes for k in self.ax.keys(): self.ax[k].draw_artist(self.vline[k + '_l']) self.ax[k].draw_artist(self.vline[k + '_r']) def first_order_phase_check(self, toggle_state): if toggle_state: self.first_slider.show() else: self.first_slider.setValue(0) self.first_slider.hide() def auto_phase(self): try: reft = self.data['freq_y'].real[self.csL:self.csR] imft = self.data['freq_y'].imag[self.csL:self.csR] intensity_int = np.array([]) for angle in range(360): phi = angle / 360 * 2 * pi intensity_int = np.append( intensity_int, np.sum(np.cos(phi) * reft + np.sin(phi) * imft)) best_angle = intensity_int.argmax() best_phi = best_angle / 360 * 2 * pi self.zeroth_slider.setValue(best_angle) self.data['freq_real'] = self.data['freq_y'].real*np.cos(best_phi) + \ self.data['freq_y'].imag*np.sin(best_phi) self.draw_phased_data() except AttributeError: dlg = QMessageBox.warning(self, 'WARNING', 'No original data available!', QMessageBox.Ok) def zeroth_order_phase(self, value): phi = value / 360 * 2 * pi try: reft = self.data['freq_y'].real[self.csL:self.csR] imft = self.data['freq_y'].imag[self.csL:self.csR] self.data['freq_real'] = self.data['freq_y'].real*np.cos(phi) + \ self.data['freq_y'].imag*np.sin(phi) intensity = np.sum(np.cos(phi) * reft + np.sin(phi) * imft) str = self.phase_info.text() str_lst = str.split('\n') intensity_str = "{:.5f}".format(intensity * 2) self.phase_info.setText(f'Current Phase: \n0th: {value}\n' + str_lst[2] + f'\nInt: {intensity_str}') self.draw_phased_data() self.canvas.blit(self.ax['freq'].bbox) except AttributeError: dlg = QMessageBox.warning(self, 'WARNING', 'No original data available!', QMessageBox.Ok) def first_order_phase(self, value): intensity = 0 str = self.phase_info.text() str_lst = str.split('\n') intensity_str = "{:.5f}".format(intensity * 2) self.phase_info.setText('Current Phase: \n' + str_lst[1] + f'\n1st: {value}' + f'\nInt: {intensity_str}') def draw_phased_data(self): key = 'freq' self.ax[key].clear() self.ax[key].plot(self.data[key + '_x'], self.data[key + '_real']) cs_value = [ float(x) for x in self.edits[key + '_cursor'].text().split(' ') ] self.vline[key + '_l'].set_xdata([cs_value[0], cs_value[0]]) self.vline[key + '_r'].set_xdata([cs_value[1], cs_value[1]]) lm_value = [ float(x) for x in self.edits[key + '_x_limit'].text().split(' ') ] self.ax[key].set_xlim(lm_value[0], lm_value[1]) self.canvas.draw() self.ax[key].ticklabel_format( style='sci', axis='both', scilimits=(0, 0)) # format the tick label of the axes for k in self.ax.keys(): self.ax[k].draw_artist(self.vline[k + '_l']) self.ax[k].draw_artist(self.vline[k + '_r']) ''' ################################################################################ some less complicated slot ''' def edit_parameters(self): os.startfile(PARAMETER_FILE) def save_parameters(self): for key in self.parameters.keys(): if key == 'file_name': continue str = self.edits[key].text() self.parameters[key] = str.split(' ') save_parameter(PARAMETER_FILE, **self.parameters) def auto_axis(self, key): ''' auto scale the axis ''' if key != 'time_y': self.ax[key[0:4]].autoscale(axis=key[5]) else: try: average = np.mean(np.abs(self.data['time_y'])) self.ax['time'].set_ylim(-2 * average, 2 * average) except AttributeError: self.ax[key[0:4]].autoscale(axis=key[5]) self.canvas.draw() self.ax[key[0:4]].ticklabel_format( style='sci', axis='both', scilimits=(0, 0)) # format the tick label of the axes for k in self.ax.keys(): self.ax[k].draw_artist(self.vline[k + '_l']) self.ax[k].draw_artist(self.vline[k + '_r']) ''' ################################################################################ browse the figure calculate based on cursors ''' def limit_and_cursor(self, key, text): ''' respond to the change of text in the edits ''' try: value = [float(x) for x in text.split(' ')] if 'limit' in key: if 'x' in key: self.ax[key[0:4]].set_xlim(value[0], value[1]) elif 'y' in key: self.ax[key[0:4]].set_ylim(value[0], value[1]) elif 'cursor' in key: self.vline[key[0:4] + '_l'].set_xdata([value[0], value[0]]) self.vline[key[0:4] + '_r'].set_xdata([value[1], value[1]]) try: cs1 = np.argmin( np.abs(self.data[key[0:4] + '_x'] - value[0]) ) # finding the index corresponding to the time stamp cs2 = np.argmin( np.abs(self.data[key[0:4] + '_x'] - value[1])) if cs1 > cs2: self.cursor_operation(key, cs2, cs1) else: self.cursor_operation(key, cs1, cs2) except AttributeError: dlg = QMessageBox.warning(self, 'WARNING', 'No original data available!', QMessageBox.Ok) self.canvas.draw() self.ax[key[0:4]].ticklabel_format( style='sci', axis='both', scilimits=(0, 0)) # format the tick label of the axes for k in self.ax.keys(): self.ax[k].draw_artist(self.vline[k + '_l']) self.ax[k].draw_artist(self.vline[k + '_r']) except ValueError: dlg = QMessageBox.warning(self, 'WARNING', 'Input only number', QMessageBox.Ok) def cursor_operation(self, key, csL, csR): self.csL = csL self.csR = csR if 'time' in key: self.zero_padding(self.zeroPadPower.currentText(), [csL, csR]) elif 'freq' in key: intensity = (np.sum(self.data['freq_y'].real)**2 + np.sum(self.data['freq_y'].imag)**2)**(1 / 2) intensity_str = "{:.5f}".format(intensity) self.integral_label.setText( f'Peak Intensity: \n{intensity_str}') # def cursor_lines_in_axis(self, ax): if ax == self.ax['time']: line1 = self.vline['time_l'] line2 = self.vline['time_r'] else: line1 = self.vline['freq_l'] line2 = self.vline['freq_r'] return line1, line2 def move_cursor(self, state): def on_press(event): if self.in_ax: if self.current_line != None: if event.button == 1: ax = event.inaxes self.last_ax = ax self.c_lock = True self.x0 = event.xdata self.current_line.set_xdata([event.xdata, event.xdata]) line1, line2 = self.cursor_lines_in_axis(ax) self.canvas.draw() self.background = self.canvas.copy_from_bbox(ax.bbox) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) def on_motion(event): ax = event.inaxes if ax != None: line1, line2 = self.cursor_lines_in_axis(ax) if self.c_lock: self.current_line.set_xdata([event.xdata, event.xdata]) self.canvas.restore_region(self.background) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) if self.x0 > event.xdata: self.c_side = 'left' else: self.c_side = 'right' else: if abs(event.xdata - line1.get_xdata()[0]) / self.xrange <= 0.02: if self.cursor == 'arrow': QApplication.setOverrideCursor(Qt.CrossCursor) self.current_line = line1 self.cursor = 'cross' elif abs(event.xdata - line2.get_xdata()[0]) / self.xrange <= 0.02: if self.cursor == 'arrow': QApplication.setOverrideCursor(Qt.CrossCursor) self.cursor = 'cross' self.current_line = line2 else: if self.cursor == 'cross': QApplication.restoreOverrideCursor() self.cursor = 'arrow' self.current_line = None def on_release(event): if self.c_lock: self.background = None self.c_lock = False ax = event.inaxes if ax != self.last_ax: ax = self.last_ax limit = ax.get_xlim() if self.c_side == 'left': event.xdata = limit[0] else: event.xdata = limit[1] line1, line2 = self.cursor_lines_in_axis(ax) str1 = "{:.5E}".format(line1.get_xdata()[0]) str2 = "{:.5E}".format(line2.get_xdata()[0]) if line2.get_xdata()[0] < line1.get_xdata()[0]: str1, str2 = str2, str1 if ax == self.ax['freq']: self.edits['freq_cursor'].setText(str1 + ' ' + str2) else: self.edits['time_cursor'].setText(str1 + ' ' + str2) def move_in_ax(event): self.in_ax = True ax = event.inaxes xmin, xmax = ax.get_xlim() self.xrange = xmax - xmin def move_out_ax(event): self.out_ax = False if state: self.cursor = 'arrow' self.verticalZoom.setChecked(False) self.horizontalZoom.setChecked(False) self.c_lock = False self.c_onpick = False self.c_cid_press = self.canvas.mpl_connect('button_press_event', on_press) self.c_cid_release = self.canvas.mpl_connect( 'button_release_event', on_release) self.c_cid_motion = self.canvas.mpl_connect( 'motion_notify_event', on_motion) self.c_in_ax = self.canvas.mpl_connect('axes_enter_event', move_in_ax) self.c_out_ax = self.canvas.mpl_connect('axes_leave_event', move_out_ax) else: self.canvas.mpl_disconnect(self.c_cid_press) self.canvas.mpl_disconnect(self.c_cid_release) self.canvas.mpl_disconnect(self.c_cid_motion) self.canvas.mpl_disconnect(self.c_in_ax) self.canvas.mpl_disconnect(self.c_out_ax) def vzoom(self, state): def on_press(event): if self.in_ax: ax = event.inaxes line1, line2 = self.cursor_lines_in_axis(ax) try: if event.button == 1: self.vlock = True self.last_ax = ax ymin, ymax = ax.get_ylim() self.yrange = ymax - ymin xmin, xmax = ax.get_xlim() self.xrange = xmax - xmin self.y0 = event.ydata self.top_ln, = ax.plot([ event.xdata - self.xrange * 0.02, event.xdata + self.xrange * 0.02 ], [event.ydata, event.ydata]) self.btm_ln, = ax.plot([ event.xdata - self.xrange * 0.02, event.xdata + self.xrange * 0.02 ], [event.ydata, event.ydata]) self.vzoom_ln, = ax.plot([event.xdata, event.xdata], [event.ydata, event.ydata]) self.top_ln.set_color('m') self.btm_ln.set_color('m') self.vzoom_ln.set_color('m') # print(self.right_ln.get_xdata(), self.right_ln.get_ydata()) self.btm_ln.set_animated(True) self.vzoom_ln.set_animated(True) self.canvas.draw() self.background = self.canvas.copy_from_bbox(ax.bbox) ax.draw_artist(self.vzoom_ln) ax.draw_artist(self.btm_ln) line1, line2 = self.cursor_lines_in_axis(ax) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) else: self.top_ln.remove() self.vzoom_ln.remove() self.btm_ln.remove() self.canvas.draw() self.background = None self.vlock = False ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) except: print('no') def on_release(event): if self.vlock: try: self.top_ln.remove() self.vzoom_ln.remove() self.btm_ln.remove() self.canvas.draw() self.background = None self.vlock = False ax = event.inaxes if ax != self.last_ax: ax = self.last_ax limit = ax.get_ylim() if self.vside == 'btm': event.ydata = limit[0] else: event.ydata = limit[1] if self.y0 > event.ydata: self.y0, event.ydata = event.ydata, self.y0 str1 = "{:.5E}".format(self.y0) str2 = "{:.5E}".format(event.ydata) if ax == self.ax['freq']: self.edits['freq_y_limit'].setText(str1 + ' ' + str2) else: self.edits['time_y_limit'].setText(str1 + ' ' + str2) except: print('no') def on_motion(event): if self.vlock: ax = event.inaxes if ax != None: self.btm_ln.set_ydata([event.ydata, event.ydata]) self.vzoom_ln.set_ydata([self.y0, event.ydata]) self.canvas.restore_region(self.background) ax.draw_artist(self.vzoom_ln) ax.draw_artist(self.btm_ln) line1, line2 = self.cursor_lines_in_axis(ax) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) if self.y0 > event.ydata: self.vside = 'btm' else: self.vside = 'top' def move_in_ax(event): self.in_ax = True def move_out_ax(event): self.out_ax = False if state: self.horizontalZoom.setChecked(False) self.moveCursor.setChecked(False) self.vlock = False self.vcid_press = self.canvas.mpl_connect('button_press_event', on_press) self.vcid_release = self.canvas.mpl_connect( 'button_release_event', on_release) self.vcid_motion = self.canvas.mpl_connect('motion_notify_event', on_motion) self.vin_ax = self.canvas.mpl_connect('axes_enter_event', move_in_ax) self.vout_ax = self.canvas.mpl_connect('axes_leave_event', move_out_ax) else: self.canvas.mpl_disconnect(self.vcid_press) self.canvas.mpl_disconnect(self.vcid_release) self.canvas.mpl_disconnect(self.vcid_motion) self.canvas.mpl_disconnect(self.vin_ax) self.canvas.mpl_disconnect(self.vout_ax) def hzoom(self, state): def on_press(event): if self.in_ax: ax = event.inaxes line1, line2 = self.cursor_lines_in_axis(ax) try: if event.button == 1: self.hlock = True self.last_ax = ax ymin, ymax = ax.get_ylim() self.yrange = ymax - ymin xmin, xmax = ax.get_xlim() self.xrange = xmax - xmin self.x0 = event.xdata self.left_ln, = ax.plot([event.xdata, event.xdata], [ event.ydata - self.yrange * 0.02, event.ydata + self.yrange * 0.02 ]) self.right_ln, = ax.plot([event.xdata, event.xdata], [ event.ydata - self.yrange * 0.02, event.ydata + self.yrange * 0.02 ]) self.hzoom_ln, = ax.plot([event.xdata, event.xdata], [event.ydata, event.ydata]) self.left_ln.set_color('m') self.right_ln.set_color('m') self.hzoom_ln.set_color('m') # print(self.right_ln.get_xdata(), self.right_ln.get_ydata()) self.right_ln.set_animated(True) self.hzoom_ln.set_animated(True) self.canvas.draw() self.background = self.canvas.copy_from_bbox(ax.bbox) ax.draw_artist(self.hzoom_ln) ax.draw_artist(self.right_ln) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) else: self.left_ln.remove() self.hzoom_ln.remove() self.right_ln.remove() self.canvas.draw() self.background = None self.hlock = False ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) except: print('no') def on_motion(event): if self.hlock: ax = event.inaxes if ax != None: self.right_ln.set_xdata([event.xdata, event.xdata]) self.hzoom_ln.set_xdata([self.x0, event.xdata]) self.canvas.restore_region(self.background) ax.draw_artist(self.hzoom_ln) ax.draw_artist(self.right_ln) line1, line2 = self.cursor_lines_in_axis(ax) ax.draw_artist(line1) ax.draw_artist(line2) self.canvas.blit(ax.bbox) if self.x0 > event.xdata: self.hside = 'left' else: self.hside = 'right' def on_release(event): if self.hlock: try: self.left_ln.remove() self.hzoom_ln.remove() self.right_ln.remove() self.canvas.draw() self.background = None self.hlock = False ax = event.inaxes if ax != self.last_ax: ax = self.last_ax limit = ax.get_xlim() if self.hside == 'left': event.xdata = limit[0] else: event.xdata = limit[1] if self.x0 > event.xdata: self.x0, event.xdata = event.xdata, self.x0 # ax.set_xlim(self.x0, event.xdata) str1 = "{:.5E}".format(self.x0) str2 = "{:.5E}".format(event.xdata) if ax == self.ax['freq']: self.edits['freq_x_limit'].setText(str1 + ' ' + str2) else: self.edits['time_x_limit'].setText(str1 + ' ' + str2) except: print('no') def move_in_ax(event): self.in_ax = True def move_out_ax(event): self.out_ax = False if state: self.moveCursor.setChecked(False) self.verticalZoom.setChecked(False) self.hlock = False self.hcid_press = self.canvas.mpl_connect('button_press_event', on_press) self.hcid_release = self.canvas.mpl_connect( 'button_release_event', on_release) self.hcid_motion = self.canvas.mpl_connect('motion_notify_event', on_motion) self.hin_ax = self.canvas.mpl_connect('axes_enter_event', move_in_ax) self.hout_ax = self.canvas.mpl_connect('axes_leave_event', move_out_ax) else: self.canvas.mpl_disconnect(self.hcid_press) self.canvas.mpl_disconnect(self.hcid_release) self.canvas.mpl_disconnect(self.hcid_motion) self.canvas.mpl_disconnect(self.hin_ax) self.canvas.mpl_disconnect(self.hout_ax) ''' ################################################################################ Multithreading fft calculation ''' def fourier_multithreading(self, time_sig): self.fourier_lb.setText('Waiting...') fourier_worker = FourierWorker(time_sig, self.f_max) fourier_worker.signals.data.connect(self.set_fourier) fourier_worker.signals.finished.connect(self.fourier_finished) self.threadpool.start(fourier_worker) def set_fourier(self, data): self.data['freq_x'] = data[0] self.data['freq_y'] = data[1] self.draw('freq') self.edits['freq_x_limit'].returnPressed.emit() self.edits['freq_cursor'].returnPressed.emit() def fourier_finished(self): self.fourier_lb.setText('Ready') ''' ################################################################################ make zerofilling work ''' def zero_padding(self, pad_power, value=[]): if value == []: value = [float(val) for val in self.parameters['time_cursor']] cs1 = np.argmin(np.abs( self.data['time_x'] - value[0])) # finding the index corresponding to the time stamp cs2 = np.argmin(np.abs(self.data['time_x'] - value[1])) else: cs1 = value[0] cs2 = value[1] try: time_data = self.data['time_y'][cs1:cs2] pad_power = int(pad_power[1:]) x = np.ceil(np.log2(len(self.data['time_y']))) n = 2**(pad_power - 1) l = int(2**x * n) time_sig = np.pad(time_data, (0, l - len(time_data)), 'constant') self.fourier_multithreading(time_sig) except AttributeError: dlg = QMessageBox.warning(self, 'WARNING', 'No original data available!', QMessageBox.Ok) self.zeroPadPower.setCurrentIndex(0) ''' ################################################################################ other miscellaneous function ''' def renew_data(self): try: self.data['time_x'] = self.data['raw_x'] self.data['time_y'] = self.data['raw_y'] self.draw('time') self.zeroPadPower.setCurrentIndex(0) except AttributeError: dlg = QMessageBox.warning(self, 'WARNING', 'No original data available!', QMessageBox.Ok) def exit_program(self): choice = QMessageBox.question( self, 'Exiting', 'Are you sure about exit?', QMessageBox.Yes | QMessageBox.No) #Set a QMessageBox when called if choice == QMessageBox.Yes: # give actions when answered the question sys.exit() def open_file(self): ''' open file and assign data to a dictionary self.Data self.data['raw_x'] self.data['raw_y'] above two are the original data self.data['time_x'] self.data['time_y'] self.data['freq_x'] self.data['freq_y'] ''' dlg = QFileDialog() dlg.setDirectory(read_parameter(PARAMETER_FILE)['file_name']) if dlg.exec_(): file_name = dlg.selectedFiles()[0] save_parameter(PARAMETER_FILE, **{"file_name": file_name}) if str(self.data_type.currentText()) == 'bin': raw_data = np.fromfile(file_name, '>f8') elif str(self.data_type.currentText()) == '.npy': raw_data = np.load(file_name) self.data = {} self.data['raw_x'] = raw_data[::2] self.data['raw_y'] = raw_data[1::2] self.data['time_x'] = self.data['raw_x'] self.data['time_y'] = self.data['raw_y'] dt = self.data['time_x'][1] - self.data['time_x'][0] self.f_max = 1 / (2 * dt) self.fourier_multithreading(self.data['time_y']) self.edits['time_cursor'].returnPressed.emit() self.draw('time') ''' ################################################################################ ''' def draw(self, key): self.ax[key].clear() if key == 'time': self.ax[key].plot(self.data[key + '_x'], self.data[key + '_y']) elif key == 'freq': self.ax[key].plot(self.data[key + '_x'], np.abs(self.data[key + '_y'])) value = [ float(x) for x in self.edits[key + '_cursor'].text().split(' ') ] self.vline[key + '_l'].set_xdata([value[0], value[0]]) self.vline[key + '_r'].set_xdata([value[1], value[1]]) self.canvas.draw() self.ax[key].ticklabel_format( style='sci', axis='both', scilimits=(0, 0)) # format the tick label of the axes self.ax[key].draw_artist(self.vline[key + '_l']) self.ax[key].draw_artist(self.vline[key + '_r']) self.canvas.blit(self.ax[key].bbox)