class DeviceWidget(QLabel): """ Colored Symbol for Lightpath Display See :func:`.symbol_for_device` for more information on how the proper symbol for the provided device is determined. Parameters ---------- device: ophyd.Device Object that will have a drawing created for it. """ clicked = pyqtSignal() def __init__(self, device, parent=None): super().__init__(parent=parent) # Grab the symbol of the device # NOTE: The symbol will not actually be created until setColor is # called. We want to avoid unnecessary widget creation and there is no # point in drawing a widget until we know more about the state of the # device self.symbol = symbol_for_device(device) # Default UI settings for conformity self.setMinimumSize(10, 10) self.setMaximumSize(50, 50) def setColor(self, color): """ Set the color of the QIcon contained in the widget """ try: icon = qta.icon(self.symbol, color=color) # Capture any errors loading icons except Exception: logger.exception("Unable to load icon %r", self.symbol) return # Set the proper pixmap self.setPixmap(icon.pixmap(self.width(), self.height())) def mousePressEvent(self, evt): """Catch mousePressEvent to emit "`clicked`" pyqtSignal""" # Push MouseEvent through super().mousePressEvent(evt) # Emit click self.clicked.emit()
class Detector2DView(mpl2dgraphicsview.Mpl2dGraphicsView): """ Customized 2D detector view """ class MousePress(object): RELEASED = 0 LEFT = 1 RIGHT = 3 newROIDefinedSignal = pyqtSignal(int, int, int, int) # return coordinate of the def __init__(self, parent): """ :param parent: :return: """ mpl2dgraphicsview.Mpl2dGraphicsView.__init__(self, parent) # connect the mouse motion to interact with the canvas self._myCanvas.mpl_connect('button_press_event', self.on_mouse_press_event) self._myCanvas.mpl_connect('button_release_event', self.on_mouse_release_event) self._myCanvas.mpl_connect('motion_notify_event', self.on_mouse_motion) # class variables self._myPolygon = None # matplotlib.patches.Polygon # class status variables self._roiSelectMode = False # region of interest. None or 2 tuple of 2-tuple for upper left corner and lower right corner # mouse positions as start and end self._roiStart = None self._roiEnd = None # mouse self._mousePressed = Detector2DView.MousePress.RELEASED # mouse position and resolution self._currX = 0. self._currY = 0. self._resolutionX = 0.005 self._resolutionY = 0.005 # parent window self._myParentWindow = None return def clear_canvas(self): """ clear canvas (override base class) :return: """ # clear the current record self._myPolygon = None # reset mouse selection ROI # set self._roiStart = None self._roiEnd = None # call base class super(Detector2DView, self).clear_canvas() return def enter_roi_mode(self, roi_state): """ Enter or leave the region of interest (ROI) selection mode :return: """ assert isinstance(roi_state, bool), 'ROI mode state {} must be a boolean but not a {}.' \ ''.format(roi_state, type(roi_state)) # set self._roiSelectMode = roi_state if roi_state: # new in add-ROI mode self.remove_roi() else: # reset roi start and roi end self._roiStart = None self._roiEnd = None return def integrate_roi_linear(self, exp_number, scan_number, pt_number, output_dir): """ integrate the 2D data inside region of interest along both axis-0 and axis-1 individually. and the result (as 1D data) will be saved to ascii file. the X values will be the corresponding pixel index either along axis-0 or axis-1 :return: """ def save_to_file(base_file_name, axis, array1d, start_index): """ save the result (1D data) to an ASCII file :param base_file_name: :param axis: :param array1d: :param start_index: :return: """ file_name = '{0}_axis_{1}.dat'.format(base_file_name, axis) wbuf = '' vec_x = np.arange(len(array1d)) + start_index for x, d in zip(vec_x, array1d): wbuf += '{0} \t{1}\n'.format(x, d) ofile = open(file_name, 'w') ofile.write(wbuf) ofile.close() return matrix = self.array2d assert isinstance( matrix, np.ndarray), 'A matrix must be an ndarray but not {0}.'.format( type(matrix)) # get region of interest if self._roiStart is None: self._roiStart = (0, 0) if self._roiEnd is None: self._roiEnd = matrix.shape ll_row = min(self._roiStart[0], self._roiEnd[0]) ll_col = min(self._roiStart[1], self._roiEnd[1]) ur_row = max(self._roiStart[0], self._roiEnd[0]) ur_col = max(self._roiStart[1], self._roiEnd[1]) #roi_matrix = matrix[ll_col:ur_col, ll_row:ur_row] #sum_0 = roi_matrix.sum(0) #sum_1 = roi_matrix.sum(1) roi_matrix = matrix[ll_col:ur_col, ll_row:ur_row] sum_0 = roi_matrix.sum(0) sum_1 = roi_matrix.sum(1) # write to file base_name = os.path.join( output_dir, 'Exp{0}_Scan{1}_Pt{2}'.format(exp_number, scan_number, pt_number)) save_to_file(base_name, 0, sum_0, ll_row) save_to_file(base_name, 1, sum_1, ll_col) message = 'Integrated values are saved to {0}...'.format(base_name) return message @property def is_roi_selection_drawn(self): """ whether ROI is drawn :return: """ is_drawn = not (self._myPolygon is None) return is_drawn def get_roi(self): """ :return: A list for polygon0 """ assert self._roiStart is not None assert self._roiEnd is not None # rio start is upper left, roi end is lower right lower_left_x = min(self._roiStart[0], self._roiEnd[0]) lower_left_y = min(self._roiStart[1], self._roiEnd[1]) lower_left = lower_left_x, lower_left_y # ROI upper right upper_right_x = max(self._roiStart[0], self._roiEnd[0]) upper_right_y = max(self._roiStart[1], self._roiEnd[1]) upper_right = upper_right_x, upper_right_y return lower_left, upper_right def plot_detector_counts(self, raw_det_data, title=None): """ plot detector counts as 2D plot :param raw_det_data: :return: """ x_min = 0 x_max = raw_det_data.shape[0] y_min = 0 y_max = raw_det_data.shape[1] count_plot = self.add_plot_2d(raw_det_data, x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, hold_prev_image=False) if title is None: title = 'No Title' self.set_title(title) if self._myPolygon is not None: print('[DB...BAT...] Add PATCH') self._myCanvas.add_patch(self._myPolygon) else: print('[DB...BAT...] NO PATCH') print('[DB...BAT...AFTER] ROI Rect: {0}. 2D plot: {1}'.format( self._myPolygon, count_plot)) return def plot_roi(self): """ Plot region of interest (as rectangular) to the canvas from the region set from :return: """ # check assert self._roiStart is not None, 'Starting point of region-of-interest cannot be None' assert self._roiEnd is not None, 'Ending point of region-of-interest cannot be None' # create a vertex list of a rectangular vertex_array = np.ndarray(shape=(4, 2)) # upper left corner vertex_array[0][0] = self._roiStart[0] vertex_array[0][1] = self._roiStart[1] # lower right corner vertex_array[2][0] = self._roiEnd[0] vertex_array[2][1] = self._roiEnd[1] # upper right corner vertex_array[1][0] = self._roiEnd[0] vertex_array[1][1] = self._roiStart[1] # lower left corner vertex_array[3][0] = self._roiStart[0] vertex_array[3][1] = self._roiEnd[1] # register if self._myPolygon is not None: self._myPolygon.remove() self._myPolygon = None self._myPolygon = self._myCanvas.plot_polygon(vertex_array, fill=False, color='w') return def remove_roi(self): """ Remove the rectangular for region of interest :return: """ print('[DB...BAT] Try to remove ROI {0}'.format(self._myPolygon)) if self._myPolygon is not None: # polygon is of type matplotlib.patches.Polygon self._myPolygon.remove() self._myPolygon = None # FUTURE-TO-DO: this should be replaced by some update() method of canvas self._myCanvas._flush() self._roiStart = None self._roiEnd = None else: print('[NOTICE] Polygon is None. Nothing to remove') return def on_mouse_motion(self, event): """ Event handing as mouse is moving :param event: :return: """ # skip if the mouse cursor is still outside of the canvas if event.xdata is None or event.ydata is None: return # check: _currX and _currY must be specified assert self._currX is not None and self._currY is not None # operation if the displacement is too small if abs(event.xdata - self._currX) < self.resolutionX() and abs( event.ydata - self._currY) < self.resolutionY(): return if self._mousePressed == Detector2DView.MousePress.RELEASED: # No operation if mouse is not pressed pass elif self._mousePressed == Detector2DView.MousePress.RIGHT: # No operation if mouse' right button is pressed pass elif self._mousePressed == Detector2DView.MousePress.LEFT: if self._roiSelectMode is True: # in ROI selection mode, update the size self.update_roi_poly(event.xdata, event.ydata) # update current mouse' position self._currX = event.xdata self._currY = event.ydata return def on_mouse_press_event(self, event): """ :param event: :return: """ # return if the cursor position is out of canvas if event.xdata is None or event.ydata is None: return # update mouse' position self._currX = event.xdata self._currY = event.ydata # update mouse' pressed state if event.button == 1: self._mousePressed = Detector2DView.MousePress.LEFT elif event.button == 3: self._mousePressed = Detector2DView.MousePress.RIGHT # do something? if self._roiSelectMode is True and self._mousePressed == Detector2DView.MousePress.LEFT: # start to select a region self._roiStart = (self._currX, self._currY) return def on_mouse_release_event(self, event): """ :param event: :return: """ # return without any operation if mouse cursor is out side of canvas if event.xdata is None or event.ydata is None: return # update mouse' position self._currX = event.xdata self._currY = event.ydata # update button prev_mouse_pressed = self._mousePressed self._mousePressed = Detector2DView.MousePress.RELEASED # do something if self._roiSelectMode and prev_mouse_pressed == Detector2DView.MousePress.LEFT: # end the ROI selection mode self.update_roi_poly(self._currX, self._currY) # send a signal to parent such that a rew ROI is defined self.newROIDefinedSignal.emit(self._roiStart[0], self._roiStart[1], self._roiEnd[0], self._roiEnd[1]) # END-IF return def resolutionX(self): """ :return: """ return (self.x_max - self.x_min) * self._resolutionX def resolutionY(self): """ :return: """ return (self.y_max - self.y_min) * self._resolutionY def set_parent_window(self, parent_window): """ Set the parent window for synchronizing the operation :param parent_window: :return: """ assert parent_window is not None, 'Parent window cannot be None' self._myParentWindow = parent_window self.newROIDefinedSignal.connect(self._myParentWindow.evt_new_roi) return def set_roi(self, lower_left_corner, upper_right_corner, plot=True): """ set ROI to class variables :param lower_left_corner: :param upper_right_corner: :param plot: if True, then plot ROI :return: """ # check inputs assert len(lower_left_corner) == 2, 'Lower left corner row/col coordinate {0} must have 2 items.' \ ''.format(lower_left_corner) assert len(upper_right_corner) == 2, 'Upper right corner row/col coordinate {0} must have 2 items.' \ ''.format(upper_right_corner) # set lower left corner and upper right corner self._roiStart = lower_left_corner self._roiEnd = upper_right_corner # plot if plot: self.plot_roi() return def update_roi_poly(self, cursor_x, cursor_y): """Update region of interest. It is to (1) remove the original polygon (2) draw a new polygon :return: """ # check assert isinstance( cursor_x, float), 'Cursor x coordination {0} must be a float.'.format( cursor_x) assert isinstance( cursor_y, float), 'Cursor y coordination {0} must be a float.'.format( cursor_y) # remove the original polygon if self._myPolygon is not None: self._myPolygon.remove() self._myPolygon = None # self.canvas._flush() # set RIO end self._roiEnd = [cursor_x, cursor_y] # plot the new polygon self.plot_roi() # # update: no need to do this! # if self._myPolygon is not None: # self._myParentWindow.do_apply_roi() return
class IntegrateSinglePtIntensityWindow(QMainWindow): """ Main window widget to set up parameters to optimize """ # establish signal for communicating from App2 to App1 - must be defined before the constructor scanIntegratedSignal = pyqtSignal(dict, name='SinglePtIntegrated') def __init__(self, parent=None): """ Initialization :param parent: :return: """ # init super(IntegrateSinglePtIntensityWindow, self).__init__(parent) assert parent is not None, 'Parent window cannot be None to set' self._parent_window = parent self._controller = parent.controller # connect signal handler self.scanIntegratedSignal.connect( self._parent_window.process_single_pt_scan_intensity) # init UI ui_path = "SinglePtIntegrationWindow.ui" self.ui = load_ui(__file__, ui_path, baseinstance=self) self._promote_widgets() # initialize widgets self.ui.tableView_summary.setup() self.ui.graphicsView_integration1DView.set_parent_window(self) # define event handlers for widgets self.ui.pushButton_integrteDetectorCounts.clicked.connect( self.do_integrate_detector_counts) self.ui.pushButton_load2thetaSigmaFile.clicked.connect( self.menu_load_gauss_sigma_file) self.ui.pushButton_exportIntensityToFile.clicked.connect( self.do_save_intensity) self.ui.pushButton_exportIntensityToTable.clicked.connect( self.do_export_intensity_to_parent) # self.ui.pushButton_refreshROI.clicked.connect(self.do_refresh_roi) self.ui.pushButton_retrieveFWHM.clicked.connect(self.do_retrieve_fwhm) self.ui.pushButton_integratePeaks.clicked.connect( self.do_integrate_single_pt) self.ui.pushButton_plot.clicked.connect(self.do_plot_integrated_pt) self.ui.pushButton_exportToMovie.clicked.connect( self.do_export_to_movie) # TODO - 20180809 - Implement the following...calling change_scan_number self.ui.pushButton_rewindPlot.clicked.connect( self.do_plot_previous_scan) self.ui.pushButton_forwardPlot.clicked.connect(self.do_plot_next_scan) self.ui.actionDefine_2theta_FWHM_Function.triggered.connect( self.do_define_2theta_fwhm_function) # menu bar self.ui.menuQuit.triggered.connect(self.do_close) self.ui.actionSelect_All.triggered.connect(self.menu_table_select_all) self.ui.actionDe_select_All.triggered.connect( self.menu_table_select_none) self.ui.actionLoad_Gaussian_Sigma_File.triggered.connect( self.menu_load_gauss_sigma_file) self.ui.actionLoad_Peak_Info_File.triggered.connect( self.do_load_peak_integration_table) self.ui.actionRefresh_ROI_List.triggered.connect(self.do_refresh_roi) # class variable self._working_dir = self._controller.get_working_directory() self._exp_number = None self._roiMutex = False # other things to do self.do_refresh_roi() return def _promote_widgets(self): graphicsView_integration1DView_layout = QVBoxLayout() self.ui.frame_graphicsView_integration1DView.setLayout( graphicsView_integration1DView_layout) self.ui.graphicsView_integration1DView = SinglePtIntegrationView(self) graphicsView_integration1DView_layout.addWidget( self.ui.graphicsView_integration1DView) tableView_summary_layout = QVBoxLayout() self.ui.frame_tableView_summary.setLayout(tableView_summary_layout) self.ui.tableView_summary = SinglePtIntegrationTable(self) tableView_summary_layout.addWidget(self.ui.tableView_summary) return def do_close(self): """ Quit the window :return: """ self.close() return def do_define_2theta_fwhm_function(self): """ pop out a dialog for user to input the 2theta-FWHM formula :return: """ formula = guiutility.get_value_from_dialog( parent=self, title='Input 2theta-FWHM function', details='Example: y = 4.0 * x**2 - 1.2 * x + 1./x]=\n' 'where y is FWHM and x is 2theta', label_name='Equation: ') if formula is None: # return if user cancels operation return print('[DB...BAT] User input 2theta formula: {}'.format(formula)) state, error_message = self._controller.check_2theta_fwhm_formula( formula) if not state: guiutility.show_message(self, message=error_message, message_type='error') return return def do_export_intensity_to_parent(self): """ export the integrated intensity to parent window's peak processing table :return: """ # collect all scan/pt from table. value including intensity and ROI intensity_dict = self.ui.tableView_summary.get_peak_intensities() # add to table including calculate peak center in Q-space self.scanIntegratedSignal.emit(intensity_dict) return # TESTME - 20180727 - Complete it! def do_export_to_movie(self): """ export the complete list of single-pt experiment to a movie :return: """ # find out the directory to save the PNG files for making a move movie_dir = self._controller.get_working_directory() roi_name = str(self.ui.comboBox_roiList.currentText()) direction = str( self.ui.comboBox_integrateDirection.currentText()).lower() movie_dir = os.path.join(movie_dir, '{}_{}'.format(roi_name, direction)) os.mkdir(movie_dir) # go through each line to plot and save the data num_rows = self.ui.tableView_summary.rowCount() file_list_str = '' for i_row in range(num_rows): # get run number and set to plot scan_number = self.ui.tableView_summary.get_scan_number(i_row) self.ui.lineEdit_Scan.setText('{}'.format(scan_number)) png_file_name = os.path.join( movie_dir, 'Scan{0:04d}_ROI{1}_{2}.png'.format(scan_number, roi_name, direction)) self.do_plot_integrated_pt(show_plot=False, save_plot_to=png_file_name) file_list_str += '{}\n'.format(png_file_name) # END-IF # write out png_list_file = open(os.path.join(movie_dir, 'MoviePNGList.txt'), 'w') png_list_file.write(file_list_str) png_list_file.close() # prompt how to make a movie command_linux = 'ffmpeg -framerate 8 -pattern_type glob -i "*.png" -r 30 test.mp4' guiutility.show_message(self, command_linux) return def do_integrate_detector_counts(self): """ sum over the (selected) scan's detector counts by ROI :return: """ # get ROI roi_name = str(self.ui.comboBox_roiList.currentText()) if roi_name is None or roi_name == '': guiutility.show_message( 'A region-of-interest must be chosen in order to integrate detector counts.' ) return # integration direction and fit direction = str( self.ui.comboBox_integrateDirection.currentText()).lower() fit_gaussian = self.ui.checkBox_fitPeaks.isChecked() num_rows = self.ui.tableView_summary.rowCount() print('[DB...BAT] Number of rows = {}'.format(num_rows)) scan_number_list = list() for row_number in range(num_rows): # integrate counts on detector scan_number = self.ui.tableView_summary.get_scan_number(row_number) scan_number_list.append(scan_number) # END-FOR print('[DB...BAT] Scan numbers: {}'.format(scan_number_list)) peak_height_dict = self._controller.integrate_single_pt_scans_detectors_counts( self._exp_number, scan_number_list, roi_name, direction, fit_gaussian) # set the value to the row to table for row_number in range(self.ui.tableView_summary.rowCount()): scan_number = self.ui.tableView_summary.get_scan_number(row_number) pt_number = 1 peak_height = peak_height_dict[scan_number] self.ui.tableView_summary.set_peak_height(scan_number, pt_number, peak_height, roi_name) # END-FOR (row_number) return def do_integrate_single_pt(self): """ integrate the 2D data inside region of interest along both axis-0 and axis-1 individually. and the result (as 1D data) will be saved to ascii file. the X values will be the corresponding pixel index either along axis-0 or axis-1 :return: """ # get ROI roi_name = str(self.ui.comboBox_roiList.currentText()) if roi_name is None or roi_name == '': guiutility.show_message( 'A region-of-interest must be chosen in order to integrate detector counts.' ) return for row_number in range(self.ui.tableView_summary.rowCount()): # integrate counts on detector scan_number = self.ui.tableView_summary.get_scan_number(row_number) pt_number = self.ui.tableView_summary.get_pt_number(row_number) # calculate peak intensity ref_fwhm = self.ui.tableView_summary.get_fwhm(row_number) intensity = self._controller.calculate_intensity_single_pt( self._exp_number, scan_number, pt_number, roi_name, ref_fwhm=ref_fwhm, is_fwhm=False) # add to table self.ui.tableView_summary.set_intensity(scan_number, pt_number, intensity) # END-FOR return # TESTME - Load a previously save integrated peaks file # Question: What kind of peak integrtion table??? Need to find out and well documented! def do_load_peak_integration_table(self): """ load peak integration table CSV file saved from peak integration table :return: """ # get table file name table_file = QFileDialog.getOpenFileName(self, 'Peak Integration Table', self._working_dir) if not table_file: return if isinstance(table_file, tuple): table_file = table_file[0] if not os.path.exists(table_file): return # load status, error_msg = self._controller.load_peak_integration_table( table_file) if not status: raise RuntimeError(error_msg) def do_plot_integrated_pt(self, show_plot=True, save_plot_to=None): """ plot integrated Pt with model and raw data 1. selection include: 2-theta FWHM Model, Summed Single Pt. Counts (horizontal), Summed Single Pt. Counts (vertical) from comboBox_plotType :return: """ plot_type = str(self.ui.comboBox_plotType.currentText()) # reset the canvas self.ui.graphicsView_integration1DView.clear_all_lines() if plot_type == '2-theta FWHM Model': self.plot_2theta_fwhm_model() else: # plot summed single pt scan self.plot_summed_single_pt_scan_counts( is_vertical_summed=plot_type.lower().count('vertical'), figure_file=save_plot_to) return def do_plot_previous_scan(self): """ plot previous scan if not in 2theta FWHM model :return: """ plot_type = str(self.ui.comboBox_plotType.currentText()) if plot_type == '2-theta FWHM Model': return scan_number_str = str(self.ui.lineEdit_Scan.text()).strip() if scan_number_str == '': return scan_number = int(scan_number_str) row_number = self.ui.tableView_summary.get_row_number_by_scan( scan_number) if row_number == 0: row_number = self.ui.tableView_summary.rowCount() - 1 scan_number = self.ui.tableView_summary.get_scan_number(row_number) self.ui.lineEdit_Scan.setText(scan_number) self.do_plot_integrated_pt() return def do_plot_next_scan(self): """ plot next scan if not in 2theta FWHM model :return: """ plot_type = str(self.ui.comboBox_plotType.currentText()) if plot_type == '2-theta FWHM Model': return scan_number_str = str(self.ui.lineEdit_Scan.text()).strip() if scan_number_str == '': return scan_number = int(scan_number_str) row_number = self.ui.tableView_summary.get_row_number_by_scan( scan_number) if row_number == self.ui.tableView_summary.rowCount() - 1: row_number = 0 scan_number = self.ui.tableView_summary.get_scan_number(row_number) self.ui.lineEdit_Scan.setText(scan_number) self.do_plot_integrated_pt() return def plot_summed_single_pt_scan_counts(self, is_vertical_summed, figure_file=None, pop_error=False): """ plot single pt scanned counts :param is_vertical_summed: :param figure_file: :param pop_error: :return: """ # get scan number scan_num_str = str(self.ui.lineEdit_Scan.text()).strip() if len(scan_num_str) == 0: scan_number = self.ui.tableView_summary.get_scan_number(0) self.ui.lineEdit_Scan.setText('{}'.format(scan_number)) else: scan_number = int(scan_num_str) roi_name = str(self.ui.comboBox_roiList.currentText()) if is_vertical_summed: direction = 'vertical' else: direction = 'horizontal' # get data: pt number is always 1 as it is a single Pt. measurement model_y = None if self.ui.checkBox_fitPeaks.isChecked(): try: vec_x, model_y = self._controller.get_single_scan_pt_model( self._exp_number, scan_number, pt_number=1, roi_name=roi_name, integration_direction=direction) except RuntimeError as run_err: err_msg = 'Unable to get single-pt scan model for {} {} {} due to {}' \ ''.format(self._exp_number, scan_number, roi_name, run_err) if pop_error: raise RuntimeError(err_msg) else: print(err_msg) # END-TRY-EXCEPT # END-IF # get original data vec_x, vec_y = self._controller.get_single_scan_pt_summed( self._exp_number, scan_number, pt_number=1, roi_name=roi_name, integration_direction=direction) # plot self.ui.graphicsView_integration1DView.add_observed_data( vec_x, vec_y, label='Summed (raw) counts', update_plot=False) if model_y is not None: self.ui.graphicsView_integration1DView.add_fit_data( vec_x, model_y, label='Gaussian model', update_plot=True) # title self.ui.graphicsView_integration1DView.set_title( 'Scan {} Pt {} {} Integration.' ''.format(scan_number, 1, direction)) # save plot? if figure_file is not None: self.ui.graphicsView_integration1DView.canvas.save_figure( figure_file) return def plot_2theta_fwhm_model(self): """ plot the loaded 2theta-FWHM model :return: """ # TODO - 20180815 - Need to parse the range from self.ui.lineEdit_Scan # default two_theta_range = 10, 2.0, 110 # start, resolution, stop vec_2theta, vec_fwhm, vec_model = self._controller.get_2theta_fwhm_data( two_theta_range[0], two_theta_range[1], two_theta_range[2]) self.ui.graphicsView_integration1DView.plot_2theta_model( vec_2theta, vec_fwhm, vec_model) return def do_refresh_roi(self): """ refresh ROI list from parent :return: """ roi_list = self._controller.get_region_of_interest_list() # add ROI self._roiMutex = True self.ui.comboBox_roiList.clear() for roi_name in sorted(roi_list): self.ui.comboBox_roiList.addItem(roi_name) self._roiMutex = False return def do_save_intensity(self): """ save intensity to file :return: """ # get output file out_file_name = QFileDialog.getSaveFileName( self, 'File to save integrated intensities', self._working_dir) if not out_file_name: return if isinstance(out_file_name, tuple): out_file_name = out_file_name[0] self.ui.tableView_summary.save_intensities_to_file(out_file_name) def do_retrieve_fwhm(self): """ Get FWHM from integrated 'STRONG' peaks according to 2theta value :return: """ row_number = self.ui.tableView_summary.rowCount() error_messages = '' for row_index in range(row_number): # check whether FWHM value is set up fwhm_i = self.ui.tableView_summary.get_fwhm(row_index) if fwhm_i is not None and fwhm_i > 1.E-10: continue # use interpolation to curve two_theta = self.ui.tableView_summary.get_two_theta(row_index) try: gauss_sigma = self._controller.calculate_peak_integration_sigma( two_theta) scan_number = self.ui.tableView_summary.get_scan_number( row_index) pt_number = 1 roi_name = self.ui.tableView_summary.get_region_of_interest_name( row_index) self.ui.tableView_summary.set_gaussian_sigma( row_index, gauss_sigma) self._controller.set_single_measure_peak_width( self._exp_number, scan_number, pt_number, roi_name, gauss_sigma, is_fhwm=False) except RuntimeError as err: # error! error_messages += 'Unable to calculate sigma of row {0} due to {1}\n'.format( row_index, err) continue # END-IF-ELSE # show error message if necessary if len(error_messages) > 0: guiutility.show_message(self, error_messages) def menu_load_gauss_sigma_file(self): """ load a Gaussian sigma curve for interpolation or matching :return: """ # get the column ascii file name file_filter = 'Data Files (*.dat);;All Files (*.*)' twotheta_sigma_file_name = QFileDialog.getOpenFileName( self, self._working_dir, '2theta Gaussian-Sigma File', file_filter) if not twotheta_sigma_file_name: return if isinstance(twotheta_sigma_file_name, tuple): twotheta_sigma_file_name = twotheta_sigma_file_name[0] # set the file to controller try: vec_x, vec_y = self._controller.import_2theta_gauss_sigma_file( twotheta_sigma_file_name) self.ui.graphicsView_integration1DView.plot_2theta_model( vec_x, vec_y) except RuntimeError as run_err: guiutility.show_message(self, str(run_err)) def menu_table_select_all(self): """ select all rows in table :return: """ self.ui.tableView_summary.select_all_rows(True) def menu_table_select_none(self): """ de-select all rows in the able :return: """ self.ui.tableView_summary.select_all_rows(False) def add_scans(self, scan_pt_list): """ add scans' information to table, i.e., add line :param scan_pt_list: :return: """ # check input assert isinstance(scan_pt_list, list), 'Scan-Pt-Infos {} must be a list but not a {}.' \ ''.format(scan_pt_list, type(scan_pt_list)) # sort the scans scan_pt_list = sorted(scan_pt_list) for scan_pt_info in scan_pt_list: scan_number, pt_number, hkl, two_theta = scan_pt_info self.ui.tableView_summary.add_scan_pt(scan_number, pt_number, hkl, two_theta) # END-FOR return def add_scan(self, scan_number, pt_number, hkl_str, two_theta): """ add single scan :param scan_number: :param pt_number: :param hkl_str: :param two_theta: :return: """ self.ui.tableView_summary.add_scan_pt(scan_number, pt_number, hkl_str, two_theta) def change_scan_number(self, increment): """ change the scan number in the :param increment: :return: """ # FIXME - 20180809 - This behaviors weird... Need debugging output - TODO # get the list of scan number from the table, in future, a real-time updated list shall be used. run_number_list = list() for irow in range(self.ui.tableView_summary.rowCount()): run_number_list.append( self.ui.tableView_summary.get_scan_number(irow)) curr_scan = int(self.ui.lineEdit_Scan.text()) try: curr_scan_index = run_number_list.index(curr_scan) except IndexError: curr_scan_index = 0 next_scan_index = curr_scan_index + increment next_scan_index = (next_scan_index + len(run_number_list)) % len(run_number_list) # set self.ui.lineEdit_Scan.setText('{}'.format( run_number_list[next_scan_index])) return def set_experiment(self, exp_number): """ set experiment number to this window for convenience :param exp_number: :return: """ assert isinstance(exp_number, int) and exp_number > 0, 'Experiment number {} (of type {} now) must be a ' \ 'positive integer'.format(exp_number, type(exp_number)) self._exp_number = exp_number return
class OptimizeLatticeWindow(QMainWindow): """ Main window widget to set up parameters to optimize """ # establish signal for communicating from App2 to App1 - must be defined before the constructor mySignal = pyqtSignal(int) def __init__(self, parent=None): """ Initialization :param parent: :return: """ # init QMainWindow.__init__(self, parent) ui_path = "OptimizeLattice.ui" self.ui = load_ui(__file__, ui_path, baseinstance=self) # initialize widgets self.ui.comboBox_unitCellTypes.addItems([ 'Cubic', 'Tetragonal', 'Orthorhombic', 'Hexagonal', 'Rhombohedral', 'Monoclinic', 'Triclinic' ]) self.ui.comboBox_ubSource.addItems( ['Tab - Calculate UB Matrix', 'Tab - Accepted UB Matrix']) self.ui.lineEdit_tolerance.setText('0.12') # define event handling self.ui.pushButton_Ok.clicked.connect(self.do_ok) self.ui.pushButton_cancel.clicked.connect(self.do_quit) if parent is not None: # connect to the method to refine UB matrix by constraining lattice parameters self.mySignal.connect(parent.refine_ub_lattice) # flag to trace back its previous step self._prevIndexByFFT = False return def do_ok(self): """ User decide to go on and then send a signal to parent :return: """ tolerance = self.get_tolerance() if tolerance is None: raise RuntimeError('Tolerance cannot be left blank!') # set up a hand-shaking signal signal_value = 1000 self.mySignal.emit(signal_value) # quit self.do_quit() return def do_quit(self): """ Quit the window :return: """ self.close() return def get_unit_cell_type(self): """ Get the tolerance :return: """ unit_cell_type = str(self.ui.comboBox_unitCellTypes.currentText()) return unit_cell_type def get_tolerance(self): """ Get the tolerance for refining UB matrix with unit cell type. :return: """ tol_str = str(self.ui.lineEdit_tolerance.text()).strip() if len(tol_str) == 0: # blank: return None tol = None else: tol = float(tol_str) return tol def get_ub_source(self): """ Get the index of the tab where the UB matrix comes from :return: """ source = str(self.ui.comboBox_ubSource.currentText()) if source == 'Tab - Calculate UB Matrix': tab_index = 3 else: tab_index = 4 return tab_index def set_prev_ub_refine_method(self, use_fft=False): """ :param use_fft: :return: """ self._prevIndexByFFT = use_fft return
class MergePeaksThread(QThread): """A thread to integrate peaks """ # signal to report state: (1) scan, (2) message mergeMsgSignal = pyqtSignal(int, str) saveMsgSignal = pyqtSignal(int, str) def __init__(self, main_window, exp_number, scan_number_list, md_file_list): """Initialization :param main_window: :param exp_number: :param scan_number_list: list of tuples for scan as (scan number, pt number list, state as merged) :param md_file_list: """ # check assert main_window is not None, 'Main window cannot be None' assert isinstance(exp_number, int), 'Experiment number must be an integer.' assert isinstance(scan_number_list, list), 'Scan (info) tuple list {0} must be a list but not {1}.' \ ''.format(scan_number_list, type(scan_number_list)) assert isinstance(md_file_list, list) or md_file_list is None, 'Output MDWorkspace file name list {0} ' \ 'must be either a list or None but not {1}.' \ ''.format(md_file_list, type(md_file_list)) if md_file_list is not None and len(scan_number_list) != len( md_file_list): raise RuntimeError( 'If MD file list is not None, then it must have the same size ({0}) as the ' 'scans ({1}) to merge.'.format(len(md_file_list), len(scan_number_list))) # start thread QThread.__init__(self) # set values self._mainWindow = main_window self._expNumber = exp_number self._scanNumberList = scan_number_list[:] self._outputMDFileList = None if md_file_list is not None: self._outputMDFileList = md_file_list[:] # other about preprocessed options self._checkPreprocessedScans = False self._preProcessedDir = None self._redoMerge = True # link signals self.mergeMsgSignal.connect(self._mainWindow.update_merge_value) self.saveMsgSignal.connect(self._mainWindow.update_file_name) return def __del__(self): """Delete signal :return: """ self.wait() return def run(self): """Execute the thread! i.e., merging the scans :return: """ if self._outputMDFileList is None or len(self._outputMDFileList) == 0: save_file = False else: save_file = True for index, scan_number in enumerate(self._scanNumberList): # set up merging parameters pt_number_list = list() # emit signal for run start (mode 0) # self.peakMergeSignal.emit(scan_number, 'In merging') self.mergeMsgSignal.emit(scan_number, 'Being merged') # merge if not merged merged_ws_name = None out_file_name = 'No File To Save' try: status, ret_tup = self._mainWindow.controller.merge_pts_in_scan( exp_no=self._expNumber, scan_no=scan_number, pt_num_list=pt_number_list, rewrite=self._redoMerge, preprocessed_dir=self._preProcessedDir) if status: merged_ws_name = str(ret_tup[0]) error_message = '' else: error_message = str(ret_tup) # save if save_file: out_file_name = self._outputMDFileList[index] self._mainWindow.controller.save_merged_scan( exp_number=self._expNumber, scan_number=scan_number, pt_number_list=pt_number_list, merged_ws_name=merged_ws_name, output=out_file_name) # END-IF-ELSE except RuntimeError as run_err: # error status = False error_message = 'Failed: {0}'.format(run_err) # continue to if status: # successfully merge peak assert merged_ws_name is not None, 'Impossible situation' self.mergeMsgSignal.emit(scan_number, merged_ws_name) self.saveMsgSignal.emit(scan_number, out_file_name) else: # merging error self.mergeMsgSignal.emit(scan_number, error_message) continue # END-IF return def set_pre_process_options(self, option_to_use, pre_process_dir): """ set the pre-process options :param option_to_use: :param pre_process_dir: :return: """ # check assert isinstance(option_to_use, bool), 'Option to use pre-process must be a boolean but not a {0}.' \ ''.format(type(option_to_use)) self._checkPreprocessedScans = option_to_use if self._checkPreprocessedScans: assert isinstance(pre_process_dir, str), 'Directory {0} to store preprocessed data must be a string ' \ 'but not a {1).'.format(pre_process_dir, type(pre_process_dir)) if os.path.exists(pre_process_dir) is False: raise RuntimeError( 'Directory {0} does not exist.'.format(pre_process_dir)) self._preProcessedDir = pre_process_dir # END-IF return def set_rewrite(self, flag): """ set the flag to re-merge the scan regardless whether the target workspace is in memory or a pre-processed MD workspace does exist. :param flag: :return: """ assert isinstance( flag, bool ), 'Re-merge/re-write flag must be a boolean but not a {0}'.format( type(flag)) self._redoMerge = flag return
class AddPeaksThread(QThread): """ A QThread class to add peaks to Mantid to calculate UB matrix """ # signal for a peak is added: int_0 = experiment number, int_1 = scan number peakAddedSignal = pyqtSignal(int, int) # signal for status: int_0 = experiment number, int_1 = scan number, int_2 = progress (0...) peakStatusSignal = pyqtSignal(int, int, int) # signal for final error report: int_0 = experiment number, str_1 = error message peakAddedErrorSignal = pyqtSignal(int, str) def __init__(self, main_window, exp_number, scan_number_list): """ Initialization :param main_window: :param exp_number: :param scan_number_list: """ # check assert main_window is not None, 'Main window cannot be None' assert isinstance(exp_number, int), 'Experiment number must be an integer.' assert isinstance(scan_number_list, list), 'Scan number list must be a list but not %s.' \ '' % str(type(scan_number_list)) # init thread super(AddPeaksThread, self).__init__() # set values self._mainWindow = main_window self._expNumber = exp_number self._scanNumberList = scan_number_list # connect to the updateTextEdit slot defined in app1.py self.peakAddedSignal.connect(self._mainWindow.update_peak_added_info) self.peakStatusSignal.connect( self._mainWindow.update_adding_peaks_status) self.peakAddedErrorSignal.connect( self._mainWindow.report_peak_addition) return def __del__(self): """ Delete signal :return: """ self.wait() return def run(self): """ method for thread is running :return: """ # declare list of failed failed_list = list() # loop over all scan numbers for index, scan_number in enumerate(self._scanNumberList): # update state self.peakStatusSignal.emit(self._expNumber, scan_number, index) # merge peak status, err_msg = self._mainWindow.controller.merge_pts_in_scan( self._expNumber, scan_number, [], False, self._mainWindow.controller.pre_processed_dir) # continue to the next scan if there is something wrong if status is False: failed_list.append((scan_number, err_msg)) continue # find peak self._mainWindow.controller.find_peak(self._expNumber, scan_number) # get PeakInfo peak_info = self._mainWindow.controller.get_peak_info( self._expNumber, scan_number) assert isinstance(peak_info, r4c.PeakProcessRecord) # send signal to main window for peak being added self.peakAddedSignal.emit(self._expNumber, scan_number) # END-FOR # send signal with unphysical scan number to flag the end of operation. self.peakStatusSignal.emit(self._expNumber, -1, len(self._scanNumberList)) # construct a final error message for main GUI # TEST: Exp 423 Scan 82 if len(failed_list) > 0: failed_scans_str = 'Unable to merge scans: ' sum_error_str = '' for fail_tup in failed_list: failed_scans_str += '%d, ' % fail_tup[0] sum_error_str += '%s\n' % fail_tup[1] # END-FOR self.peakAddedErrorSignal.emit( self._expNumber, failed_scans_str + '\n' + sum_error_str) # END-IF return
class IntegratePeaksThread(QThread): """ A thread to integrate peaks """ # signal to emit before a merge/integration status: exp number, scan number, progress, mode peakMergeSignal = pyqtSignal(int, int, float, list, int) # signal to report state: (1) experiment, (2) scan, (3) mode, (4) message mergeMsgSignal = pyqtSignal(int, int, int, str) def __init__(self, main_window, exp_number, scan_tuple_list, mask_det, mask_name, norm_type, num_pt_bg_left, num_pt_bg_right, scale_factor=1.000): """ :param main_window: :param exp_number: :param scan_tuple_list: list of tuples for scan as (scan number, pt number list, state as merged) :param mask_det: :param mask_name: :param norm_type: type of normalization :param num_pt_bg_left: number of Pt in the left :param num_pt_bg_right: number of Pt for background in the right """ # start thread QThread.__init__(self) # check assert main_window is not None, 'Main window cannot be None' assert isinstance(exp_number, int), 'Experiment number must be an integer.' assert isinstance(scan_tuple_list, list), 'Scan (info) tuple list must be a list but not %s.' \ '' % str(type(scan_tuple_list)) assert isinstance(mask_det, bool), 'Parameter mask_det must be a boolean but not %s.' \ '' % str(type(mask_det)) assert isinstance( mask_name, str), 'Name of mask must be a string but not %s.' % str( type(mask_name)) assert isinstance(norm_type, str), 'Normalization type must be a string but not %s.' \ '' % str(type(norm_type)) assert isinstance(num_pt_bg_left, int) and num_pt_bg_left >= 0,\ 'Number of Pt at left for background {0} must be non-negative integers but not of type {1}.' \ ''.format(num_pt_bg_left, type(num_pt_bg_left)) assert isinstance(num_pt_bg_right, int) and num_pt_bg_right >= 0,\ 'Number of Pt at right for background {0} must be non-negative integers but not of type {1}.' \ ''.format(num_pt_bg_right, type(num_pt_bg_right)) # set values self._mainWindow = main_window self._expNumber = exp_number self._scanTupleList = scan_tuple_list[:] self._maskDetector = mask_det self._normalizeType = norm_type self._selectedMaskName = mask_name self._numBgPtLeft = num_pt_bg_left self._numBgPtRight = num_pt_bg_right self._scaleFactor = scale_factor # other about preprocessed options self._checkPreprocessedScans = True # link signals self.peakMergeSignal.connect(self._mainWindow.update_merge_value) self.mergeMsgSignal.connect(self._mainWindow.update_merge_message) return def __del__(self): """ Delete signal :return: """ self.wait() return def run(self): """ Execute the thread! :return: """ for index, scan_tup in enumerate(self._scanTupleList): # check assert isinstance(scan_tup, tuple) and len(scan_tup) == 3 scan_number, pt_number_list, merged = scan_tup # emit signal for run start (mode 0) mode = int(0) self.peakMergeSignal.emit(self._expNumber, scan_number, float(index), [0., 0., 0.], mode) # merge if not merged if merged is False: merged_ws_name = 'X' try: pre_dir = self._mainWindow.controller.pre_processed_dir status, ret_tup = \ self._mainWindow.controller.merge_pts_in_scan(exp_no=self._expNumber, scan_no=scan_number, pt_num_list=pt_number_list, rewrite=False, preprocessed_dir=pre_dir) if status: merged_ws_name = str(ret_tup[0]) error_message = '' else: error_message = str(ret_tup) except RuntimeError as run_err: status = False error_message = str(run_err) # continue to if status: # successfully merge peak assert isinstance(merged_ws_name, str), 'Merged workspace %s must be a string but not %s.' \ '' % (str(merged_ws_name), type(merged_ws_name)) self.mergeMsgSignal.emit(self._expNumber, scan_number, 1, merged_ws_name) else: self.mergeMsgSignal.emit(self._expNumber, scan_number, 0, error_message) continue # self._mainWindow.ui.tableWidget_mergeScans.set_status(scan_number, 'Merged') else: # merged pass # END-IF # calculate peak center try: # status, ret_obj = self._mainWindow.controller.calculate_peak_center(self._expNumber, scan_number, # pt_number_list) status, ret_obj = self._mainWindow.controller.find_peak( self._expNumber, scan_number, pt_number_list) except RuntimeError as run_err: status = False ret_obj = 'RuntimeError: %s.' % str(run_err) except AssertionError as ass_err: status = False ret_obj = 'AssertionError: %s.' % str(ass_err) if status: center_i = ret_obj # 3-tuple else: error_msg = 'Unable to find peak for exp %d scan %d: %s.' % ( self._expNumber, scan_number, str(ret_obj)) # no need... self._mainWindow.controller.set_peak_intensity(self._expNumber, scan_number, 0.) self._mainWindow.ui.tableWidget_mergeScans.set_peak_intensity( None, scan_number, 0., False) self._mainWindow.ui.tableWidget_mergeScans.set_status( scan_number, error_msg) continue # check given mask workspace if self._maskDetector: self._mainWindow.controller.check_generate_mask_workspace( self._expNumber, scan_number, self._selectedMaskName, check_throw=True) bkgd_pt_list = (self._numBgPtLeft, self._numBgPtRight) # integrate peak try: status, ret_obj = self._mainWindow.controller.integrate_scan_peaks( exp=self._expNumber, scan=scan_number, peak_radius=1.0, peak_centre=center_i, merge_peaks=False, use_mask=self._maskDetector, normalization=self._normalizeType, mask_ws_name=self._selectedMaskName, scale_factor=self._scaleFactor, background_pt_tuple=bkgd_pt_list) except ValueError as val_err: status = False ret_obj = 'Unable to integrate scan {0} due to {1}.'.format( scan_number, str(val_err)) except RuntimeError as run_err: status = False ret_obj = 'Unable to integrate scan {0}: {1}.'.format( scan_number, run_err) # handle integration error if status: # get PT dict pt_dict = ret_obj assert isinstance(pt_dict, dict), 'dictionary must' self.set_integrated_peak_info(scan_number, pt_dict) # information setup include # - lorentz correction factor # - peak integration dictionary # - motor information: peak_info_obj.set_motor(motor_name, motor_step, motor_std_dev) else: # integration failed error_msg = str(ret_obj) self.mergeMsgSignal.emit(self._expNumber, scan_number, 0, error_msg) continue intensity1 = pt_dict['simple intensity'] peak_centre = self._mainWindow.controller.get_peak_info( self._expNumber, scan_number).get_peak_centre() # emit signal to main app for peak intensity value mode = 1 # center_i self.peakMergeSignal.emit(self._expNumber, scan_number, float(intensity1), list(peak_centre), mode) # END-FOR # terminate the process mode = int(2) self.peakMergeSignal.emit(self._expNumber, -1, len(self._scanTupleList), [0, 0, 0], mode) # self._mainWindow.ui.tableWidget_mergeScans.select_all_rows(False) return def set_integrated_peak_info(self, scan_number, peak_integration_dict): """ set the integrated peak information including * calculate Lorentz correction * add the integration result dictionary * add motor step information :return: """ # get peak information peak_info_obj = self._mainWindow.controller.get_peak_info( self._expNumber, scan_number) # get Q-vector of the peak center and calculate |Q| from it peak_center_q = peak_info_obj.get_peak_centre_v3d().norm() # get wave length wavelength = self._mainWindow.controller.get_wave_length( self._expNumber, [scan_number]) # get motor step (choose from omega, phi and chi) try: motor_move_tup = self._mainWindow.controller.get_motor_step( self._expNumber, scan_number) motor_name, motor_step, motor_std_dev = motor_move_tup except RuntimeError as run_err: return str(run_err) except AssertionError as ass_err: return str(ass_err) # calculate lorentz correction # TODO/FIXME/NOW2 : peak center Q shall be from calculation! lorentz_factor = peak_integration_utility.calculate_lorentz_correction_factor( peak_center_q, wavelength, motor_step) peak_info_obj.lorentz_correction_factor = lorentz_factor # set motor peak_info_obj.set_motor(motor_name, motor_step, motor_std_dev) # set peak integration dictionary peak_info_obj.set_integration(peak_integration_dict) return
class Worker(QThread): """ 使用示例: # 在activity 类中声明一个func thread self.delete_file_func = Worker() # 为线程信号绑定槽 self.delete_file_func.thread_signal.connect(....) # 正常通信频道 self.delete_file_func.thread_error_signal.connect(....) # 异常通信频道 """ # 正常通信信号 __success = pyqtSignal(Response) # 异常通信信号 __error = pyqtSignal(Response) # 终止信号 __end = pyqtSignal(Response) # 是否在运行 __is_working = False @property def success(self): return self.__success @property def error(self): return self.__error @property def end(self): return self.__end @property def running(self): return self.__is_working def strike(self, response=None): """ 手动激发 :param response: :return: """ # noinspection PyUnresolvedReferences self.end.emit(response or Response()) def __init__(self): super().__init__() self.func = None self._id = uuid.uuid4().hex self.func_args = [] self.func_kwargs = {} # noinspection PyUnresolvedReferences self.success.connect(lambda: None) # noinspection PyUnresolvedReferences self.error.connect(lambda: None) def set_func(self, func, task_id=None, *args, **kwargs): self.func_args = list(args) self.func_kwargs = kwargs self.func = func self._id = task_id logging.info("设置线程 func={} arg={} kwargs={}".format( func.__name__, args, kwargs)) def kill(self): self.__is_working = False self.terminate() def run(self): self.__is_working = True response = Response() response.task_id = self._id if not self.func: return try: result = self.func(*self.func_args, **self.func_kwargs) response.data = result self.thread_signal.emit(response) except Exception as e: response.code = 1 response.data = e # noinspection PyUnresolvedReferences self.error.emit(response) logging.exception( "执行失败 func={} arg={} kwargs={}, error={}".format( self.func.__name__, self.func_args, self.func_kwargs, e)) finally: # noinspection PyUnresolvedReferences self.end.emit(response) self.__is_working = False
class MyNavigation(QObject): mpl_mv = pyqtSignal(float, float, name='mpl_move') padWidth = 2 def __init__(self, ax, mplwidget=None): super(MyNavigation, self).__init__() self.mplwidget = mplwidget self.ax = ax self.figure = ax.figure self.canvas = ax.figure.canvas self.zoomer = None self.mode = "normal" self.drag = False self.interval = None self.distance = None self.curve = myCurve(self.ax) self.canvas.mpl_connect("button_press_event", self.on_press) self.canvas.mpl_connect("button_release_event", self.on_release) self.canvas.mpl_connect("motion_notify_event", self.on_move) self.canvas.mpl_connect("scroll_event", self.on_scroll) self.canvas.mpl_connect("axes_leave_event", self.on_leave_axes) self.canvas.mpl_connect("axes_enter_event", self.on_enter_axes) def on_enter_axes(self, event): if event.inaxes == self.ax and self.mplwidget: if self.mode == "zoom": self.mplwidget.setCursor(Qt.CrossCursor) if self.mode == "pan": self.mplwidget.setCursor(Qt.OpenHandCursor) if self.mode == "interval": self.mplwidget.setCursor(Qt.SizeHorCursor) if self.mode == "curve": self.mplwidget.setCursor(Qt.CrossCursor) def on_leave_axes(self, event): if event.inaxes == self.ax and self.mplwidget: self.mplwidget.setCursor(Qt.ArrowCursor) def on_press(self, event): if event.inaxes != self.ax: return self.startx, self.starty = event.xdata, event.ydata if event.button == 1 and self.mode == "zoom": self.bg = self.canvas.copy_from_bbox(self.ax.bbox) self.zoomer = patches.Rectangle((self.startx, self.starty), 0, 0, fill=False, ls="dashed", transform=self.ax.transData) self.zoomer.set_animated(True) self.ax.add_patch(self.zoomer) self.ax.draw_artist(self.zoomer) self.canvas.blit(self.ax.bbox) if event.button == 1 and self.mode == "pan": if self.mplwidget: self.mplwidget.setCursor(Qt.ClosedHandCursor) self.pcx_startx, self.pcx_starty = event.x, event.y self.bg = self.canvas.copy_from_bbox( self.ax.bbox.padded(-self.padWidth)) self.b = self.bg.get_extents() for l in self.ax.lines: l.set_animated(True) self.ax.patch.set_animated(True) self.canvas.restore_region(self.bg) self.canvas.blit(self.ax.bbox.padded(-self.padWidth)) self.drag = True if event.button == 1 and self.mode == "interval": self.remove_interval() self.bg = self.canvas.copy_from_bbox(self.ax.bbox) self.interval = self.ax.axvspan(self.startx, self.startx, fc="gray", alpha=0.2) self.interval.set_animated(True) self.drag = True if event.button == 1 and self.mode == "distance": self.bg = self.canvas.copy_from_bbox(self.ax.bbox) self.distance, = self.ax.plot([self.startx, self.startx], [self.starty, self.starty], "black", ls="-.") self.txt = self.ax.text(self.startx, self.starty, "(%8.3e,%8.3e)\n%8.3e" % (0, 0, 0), ha='center') self.txt.set_animated(True) self.distance.set_animated(True) self.canvas.restore_region(self.bg) self.ax.draw_artist(self.txt) self.canvas.blit(self.ax.bbox) self.drag = True if event.button == 1 and self.mode == "curve": self.curve.add_point((event.xdata, event.ydata)) self.curve.l.set_animated(True) self.canvas.draw() self.bg = self.canvas.copy_from_bbox(self.ax.bbox) self.drag = True self.canvas.restore_region(self.bg) self.ax.draw_artist(self.curve.l) self.canvas.blit(self.ax.bbox) def on_move(self, event): if event.inaxes != self.ax: return self.curx, self.cury = event.xdata, event.ydata if self.mplwidget: self.mpl_mv.emit(self.curx, self.cury) if self.zoomer is not None: w = event.xdata - self.startx h = event.ydata - self.starty self.zoomer.set_width(w) self.zoomer.set_height(h) self.canvas.restore_region(self.bg) self.ax.draw_artist(self.zoomer) self.canvas.blit(self.ax.bbox) if self.mode == "pan" and self.drag: dx = event.x - self.pcx_startx dy = event.y - self.pcx_starty self.bg.set_x(self.b[0] + dx) self.bg.set_y(self.b[1] - dy) self.ax.draw_artist(self.ax.patch) self.canvas.restore_region(self.bg) self.canvas.blit(self.ax.bbox.padded(-self.padWidth)) if self.mode == "interval" and self.drag: xy = self.interval.get_xy() xy[2, 0] = event.xdata xy[3, 0] = event.xdata self.interval.set_xy(xy) self.interval.set_xy(xy) self.canvas.restore_region(self.bg) self.ax.draw_artist(self.interval) self.canvas.blit(self.ax.bbox) if self.mode == "distance" and self.drag: self.distance.set_xdata([self.startx, self.curx]) self.distance.set_ydata([self.starty, self.cury]) self.txt.set_x(0.5 * (self.startx + self.curx)) self.txt.set_y(0.5 * (self.starty + self.cury)) dx = abs(self.startx - self.curx) dy = abs(self.starty - self.cury) self.txt.set_text("(%8.3e,%8.3e)\n%8.3e" % (dx, dy, np.sqrt(dx * dx + dy * dy))) self.canvas.restore_region(self.bg) self.ax.draw_artist(self.distance) self.ax.draw_artist(self.txt) self.canvas.blit(self.ax.bbox) if self.mode == "curve" and self.drag: self.curve.change_point((self.curx, self.cury)) self.canvas.restore_region(self.bg) self.ax.draw_artist(self.curve.l) self.canvas.blit(self.ax.bbox) def on_release(self, event): if event.inaxes == self.ax: self.curx, self.cury = event.xdata, event.ydata if self.zoomer is not None: x1 = self.startx x2 = self.startx + self.zoomer.get_width() y1 = self.starty y2 = self.starty + self.zoomer.get_height() if x2 < x1: x2, x1 = x1, x2 if y2 < y1: y2, y1 = y1, y2 self.zoomer.remove() self.zoomer = None if x1 != x2 and y1 != y2: self.ax.set_xlim(x1, x2) self.ax.set_ylim(y1, y2) self.canvas.draw() if self.mode == "zoom" and event.button == 3 and event.inaxes == self.ax: self.ax.autoscale() self.canvas.draw() if self.mode == "pan" and self.drag: if self.mplwidget: self.mplwidget.setCursor(Qt.OpenHandCursor) for l in self.ax.lines: l.set_animated(False) self.ax.patch.set_animated(False) self.drag = False dx = self.curx - self.startx dy = self.cury - self.starty b = self.ax.get_xlim() self.ax.set_xlim(b[0] - dx, b[1] - dx) b = self.ax.get_ylim() self.ax.set_ylim(b[0] - dy, b[1] - dy) self.canvas.draw() if self.mode == "interval" and self.drag and event.inaxes == self.ax: self.interval.set_animated(False) x1, x2 = self.get_interval() if x1 == x2: self.remove_interval() self.canvas.draw() self.drag = False if self.mode == "curve" and self.drag and event.inaxes == self.ax: self.curve.change_point((self.curx, self.cury)) self.curve.l.set_animated(False) self.canvas.draw() self.drag = False if self.mode == "distance" and self.drag and event.inaxes == self.ax: self.distance.remove() self.txt.remove() del self.txt del self.distance self.canvas.draw() self.drag = False def on_scroll(self, event): if self.mode != 'zoom': return scale = -1 if event.button == "up": scale = 1 b = self.ax.get_xbound() self.ax.set_xlim(b[0] - scale * 0.1 * (b[1] - b[0]), b[1] + scale * 0.1 * (b[1] - b[0])) b = self.ax.get_ybound() self.ax.set_ylim(b[0] - scale * 0.1 * (b[1] - b[0]), b[1] + scale * 0.1 * (b[1] - b[0])) self.canvas.draw() def get_interval(self): if self.interval: x1 = self.interval.xy[0, 0] x2 = self.interval.xy[2, 0] if x2 < x1: x1, x2 = x2, x1 return (x1, x2) else: return None def remove_interval(self): if self.interval: self.interval.remove() self.canvas.draw() del self.interval self.interval = None def apply_curve_to_line(self, linenum): if len(self.ax.get_lines()) < linenum + 1: return if len(self.curve.points) < 2: return c = self.ax.get_lines()[linenum] xs, ys = list( zip(*np.sort(np.array(self.curve.points, dtype=[("x", float), ("y", float)]), order="x"))) n1 = np.searchsorted(c.get_xdata(), xs[0]) n2 = np.searchsorted(c.get_xdata(), xs[-1]) xx = c.get_xdata()[n1:n2] yy = np.interp(xx, xs, ys) zz = c.get_ydata() zz[n1:n2] = yy c.set_ydata(zz) self.canvas.draw_idle() def pinch_line_to_curve(self, linenum): if len(self.ax.get_lines()) < linenum + 1: return if len(self.curve.points) < 2: return c = self.ax.get_lines()[linenum] xxs, yys = list( zip(*np.sort(np.array(self.curve.points, dtype=[("x", float), ("y", float)]), order="x"))) n1 = np.searchsorted(c.get_xdata(), xxs[0]) n2 = np.searchsorted(c.get_xdata(), xxs[-1]) xs = c.get_xdata() ys = c.get_ydata() def f(x): return np.interp(x, xxs, yys) for n in range(n1 + 1, n2): ss = ys[n] - f(xs[n]) ys[n] = 0.5 * ss + f(xs[n]) c.set_data(xs, ys) self.canvas.draw_idle() def interpolate_line_on_interval(self, linenum, inteprolation_degree): if len(self.ax.get_lines()) < linenum + 1: return c = self.ax.get_lines()[linenum] xs = c.get_xdata() ys = c.get_ydata() if self.get_interval() is not None: x1, x2 = self.get_interval() n1 = np.array(xs).searchsorted(x1) n2 = np.array(xs).searchsorted(x2) else: n1 = 0 n2 = len(xs) if n1 == n2: return xs = xs[n1:n2] ys = ys[n1:n2] p = np.polyfit(xs, ys, inteprolation_degree) ys = np.polyval(p, xs) self.curve.points = list(zip(xs, ys)) self.canvas.draw_idle()