class FittingPlotPresenter(object): def __init__(self, parent, model=None, view=None): if view is None: self.view = FittingPlotView(parent) else: self.view = view if model is None: self.model = FittingPlotModel() else: self.model = model self.workspace_added_observer = GenericObserverWithArgPassing(self.add_workspace_to_plot) self.workspace_removed_observer = GenericObserverWithArgPassing(self.remove_workspace_from_plot) self.all_workspaces_removed_observer = GenericObserver(self.clear_plot) self.fit_all_started_observer = GenericObserverWithArgPassing(self.do_fit_all) self.fit_all_done_notifier = GenericObservable() def add_workspace_to_plot(self, ws): axes = self.view.get_axes() for ax in axes: self.model.add_workspace_to_plot(ws, ax, PLOT_KWARGS) self.view.update_figure() def remove_workspace_from_plot(self, ws): for ax in self.view.get_axes(): self.model.remove_workspace_from_plot(ws, ax) self.view.remove_ws_from_fitbrowser(ws) self.view.update_figure() def clear_plot(self): for ax in self.view.get_axes(): self.model.remove_all_workspaces_from_plot(ax) self.view.clear_figure() self.view.update_fitbrowser() def do_fit_all(self, ws_list, do_sequential=True): fitprop_list = [] prev_fitprop = self.view.read_fitprop_from_browser() for ws in ws_list: logger.notice(f'Starting to fit workspace {ws}') fitprop = deepcopy(prev_fitprop) # update I/O workspace name fitprop['properties']['Output'] = ws fitprop['properties']['InputWorkspace'] = ws # do fit fit_output = Fit(**fitprop['properties']) # update results fitprop['status'] = fit_output.OutputStatus funcstr = str(fit_output.Function.fun) fitprop['properties']['Function'] = funcstr if "success" in fitprop['status'].lower() and do_sequential: # update function in prev fitprop to use for next workspace prev_fitprop['properties']['Function'] = funcstr # update last fit in fit browser and save setup self.view.update_browser(fit_output.OutputStatus, funcstr, ws) # append a deep copy to output list (will be initial parameters if not successful) fitprop_list.append(fitprop) logger.notice('Sequential fitting finished.') self.fit_all_done_notifier.notify_subscribers(fitprop_list)
def test_that_disable_observer_calls_on_view_when_triggered(self): disable_notifier = GenericObservable() disable_notifier.add_subscriber(self.presenter.disable_tab_observer) self.view.setEnabled = mock.MagicMock() disable_notifier.notify_subscribers() self.view.setEnabled.assert_called_once_with(False)
def test_that_enable_observer_will_disable_the_view_when_there_is_zero_datasets(self): mock_model_number_of_datasets = mock.PropertyMock(return_value=0) type(self.model).number_of_datasets = mock_model_number_of_datasets enable_notifier = GenericObservable() enable_notifier.add_subscriber(self.presenter.enable_tab_observer) self.view.setEnabled = mock.MagicMock() enable_notifier.notify_subscribers() mock_model_number_of_datasets.assert_called_once_with() self.view.setEnabled.assert_called_once_with(False)
def test_that_enable_observer_calls_on_view_when_triggered(self): self.view.setEnabled(True) self.view.disable_widget() for widget in self.view.children(): if str(widget.objectName()) in ['cancel_calculate_phase_table_button']: continue self.assertFalse(widget.isEnabled()) enable_notifier = GenericObservable() enable_notifier.add_subscriber(self.presenter.enable_tab_observer) enable_notifier.notify_subscribers() for widget in self.view.children(): if str(widget.objectName()) in ['cancel_calculate_phase_table_button']: continue self.assertTrue(widget.isEnabled())
class ResultsContext: def __init__(self): self._result_table_names: list = [] self.ADS_observer = MuonADSObserver(self.remove, self.clear, self.replaced) self.remove_observable = GenericObservable() self.clear_observable = GenericObservable() self.replace_observable = GenericObservable() def remove(self, workspace): self.remove_observable.notify_subscribers(workspace) def clear(self): self.clear_observable.notify_subscribers() def replaced(self, workspace): self.replace_observable.notify_subscribers(workspace) @property def result_table_names(self) -> list: """Returns the names of the results tables loaded into the model fitting tab.""" return self._result_table_names @result_table_names.setter def result_table_names(self, table_names: list) -> None: """Sets the names of the results tables loaded into the model fitting tab.""" self._result_table_names = table_names def add_result_table(self, table_name: str) -> None: """Add a results table to the stored list of results tables.""" if table_name not in self._result_table_names and check_if_workspace_exist( table_name): self._result_table_names.append(table_name) def remove_workspace_by_name(self, workspace_name: str) -> None: """Removes a results table after and ADS deletion event.""" if workspace_name in self._result_table_names: self._result_table_names.remove(workspace_name)
class PlotFreqFitPanePresenter(PlotFitPanePresenter): def __init__(self, view, model, context, fitting_context, figure_presenter): super().__init__(view, model, context, fitting_context, figure_presenter) self._data_type = [FREQ, FIELD] self.context.frequency_context.x_label = FREQ self._sort_by = [""] self.update_view() self._view.hide_plot_raw() self._view.hide_tiled_by() self.update_freq_units = GenericObservable() self.update_maxent_plot = GenericObservable() self.update_fit_pane_observer = GenericObserver(self._update_fit_pane) def handle_data_type_changed(self): """ Handles the data type being changed in the view by plotting the workspaces corresponding to the new data type """ self.context.frequency_context.x_label = self._view.get_plot_type() self._figure_presenter.set_plot_range(self.context.frequency_context.range()) # need to add observable for switching units cannot reuse it as it causes a loop self.update_maxent_plot.notify_subscribers() # need to send signal out to update stuff => dont undate here # the slot will update the plot when the fit list updates self.update_freq_units.notify_subscribers() def handle_rebin_options_changed(self): # there is no way to rebin the data -> do nothing return def _update_fit_pane(self): if self.context.frequency_context.unit() == GAUSS: self._view.set_plot_type(FIELD) else: self._view.set_plot_type(FREQ) self._figure_presenter.set_plot_range(self.context.frequency_context.range()) self.update_freq_units.notify_subscribers()
class SeqFittingTabPresenter(object): def __init__(self, view, model, context): self.view = view self.model = model self.context = context self.fit_function = None self.selected_rows = [] self.calculation_thread = None self.fitting_calculation_model = None self.view.set_data_type_options( self.context.data_type_options_for_sequential()) self.fit_parameter_changed_notifier = GenericObservable() self.sequential_fit_finished_notifier = GenericObservable() self.view.set_slot_for_display_data_type_changed( self.handle_selected_workspaces_changed) # Observers self.selected_workspaces_observer = GenericObserver( self.handle_selected_workspaces_changed) self.fit_type_changed_observer = GenericObserver( self.handle_selected_workspaces_changed) self.fit_function_updated_observer = GenericObserver( self.handle_fit_function_updated) self.fit_parameter_updated_observer = GenericObserver( self.handle_fit_function_parameter_changed) self.fit_parameter_changed_in_view = GenericObserverWithArgPassing( self.handle_updated_fit_parameter_in_table) self.selected_sequential_fit_notifier = GenericObservable() self.disable_tab_observer = GenericObserver( lambda: self.view.setEnabled(False)) self.enable_tab_observer = GenericObserver( lambda: self.view.setEnabled(self.model.number_of_datasets > 0)) def create_thread(self, callback): self.fitting_calculation_model = ThreadModelWrapperWithOutput(callback) return thread_model.ThreadModel(self.fitting_calculation_model) def handle_fit_function_updated(self): parameters = self.model.get_fit_function_parameters() if not parameters: self.view.fit_table.clear_fit_parameters() self.view.fit_table.reset_fit_quality() else: parameter_values = self._get_fit_function_parameter_values_from_fitting_model( ) self.view.fit_table.set_parameters_and_values( parameters, parameter_values) def _get_fit_function_parameter_values_from_fitting_model(self): display_type = self.view.selected_data_type() parameter_values = [ self.model.get_all_fit_function_parameter_values_for(fit_function) for row, fit_function in enumerate( self.model.get_all_fit_functions_for(display_type)) ] if len(parameter_values) != self.view.fit_table.get_number_of_fits(): parameter_values *= self.view.fit_table.get_number_of_fits() return parameter_values def handle_fit_function_parameter_changed(self): self.view.fit_table.reset_fit_quality() display_type = self.view.selected_data_type() for row, fit_function in enumerate( self.model.get_all_fit_functions_for(display_type)): parameter_values = self.model.get_all_fit_function_parameter_values_for( fit_function) self.view.fit_table.set_parameter_values_for_row( row, parameter_values) def handle_selected_workspaces_changed(self): display_type = self.view.selected_data_type() workspace_names, runs, groups_and_pairs = self.model.get_runs_groups_and_pairs_for_fits( display_type) self.view.fit_table.set_fit_workspaces(workspace_names, runs, groups_and_pairs) self.handle_fit_function_updated() def handle_fit_selected_pressed(self): self.selected_rows = self.view.fit_table.get_selected_rows() self.handle_sequential_fit_requested() def handle_sequential_fit_pressed(self): self.view.fit_table.clear_fit_selection() self.selected_rows = [ i for i in range(self.view.fit_table.get_number_of_fits()) ] self.handle_sequential_fit_requested() def handle_fit_started(self): self.view.seq_fit_button.setEnabled(False) self.view.fit_selected_button.setEnabled(False) self.view.fit_table.block_signals(True) def handle_fit_error(self, error): self.view.warning_popup(error) self.view.fit_selected_button.setEnabled(True) self.view.seq_fit_button.setEnabled(True) self.view.fit_table.block_signals(False) def handle_sequential_fit_requested(self): workspace_names = [ self.get_workspaces_for_row_in_fit_table(row) for row in self.selected_rows ] if not self.validate_sequential_fit(workspace_names): return parameter_values = [ self.view.fit_table.get_fit_parameter_values_from_row(row) for row in self.selected_rows ] calculation_function = functools.partial( self.model.perform_sequential_fit, workspace_names, parameter_values, self.view.use_initial_values_for_fits()) self.calculation_thread = self.create_thread(calculation_function) self.calculation_thread.threadWrapperSetUp( on_thread_start_callback=self.handle_fit_started, on_thread_end_callback=self.handle_seq_fit_finished, on_thread_exception_callback=self.handle_fit_error) self.calculation_thread.start() def handle_seq_fit_finished(self): if self.fitting_calculation_model.result is None: return fit_functions, fit_statuses, fit_chi_squareds = self.fitting_calculation_model.result for fit_function, fit_status, fit_chi_squared, row in zip( fit_functions, fit_statuses, fit_chi_squareds, self.selected_rows): parameter_values = self.model.get_all_fit_function_parameter_values_for( fit_function) self.view.fit_table.set_parameter_values_for_row( row, parameter_values) self.view.fit_table.set_fit_quality(row, fit_status, fit_chi_squared) self.view.seq_fit_button.setEnabled(True) self.view.fit_selected_button.setEnabled(True) self.view.fit_table.block_signals(False) # if no row is selected (select the last) if len(self.view.fit_table.get_selected_rows()) == 0: self.view.fit_table.set_selection_to_last_row() else: self.handle_fit_selected_in_table() self.sequential_fit_finished_notifier.notify_subscribers() def handle_updated_fit_parameter_in_table(self, index): copy_param = self.view.copy_values_for_fits() if copy_param: self.view.fit_table.set_parameter_values_for_column( index.column(), index.data()) self._update_parameter_values_in_fitting_model_for_all_rows( self.view.fit_table.get_number_of_fits()) else: self._update_parameter_values_in_fitting_model_for_row(index.row()) self.fit_parameter_changed_notifier.notify_subscribers() def _update_parameter_values_in_fitting_model_for_all_rows( self, num_of_rows): for row in range(num_of_rows): self._update_parameter_values_in_fitting_model_for_row(row) def validate_sequential_fit(self, workspace_names): message = self.model.validate_sequential_fit(workspace_names) if message != "": self.view.warning_popup(message) return message == "" @staticmethod def _flatten_workspace_names(workspaces: list) -> list: return [ workspace for fit_workspaces in workspaces for workspace in fit_workspaces ] def _update_parameter_values_in_fitting_model_for_row(self, row): workspaces = self.get_workspaces_for_row_in_fit_table(row) parameter_values = self.view.fit_table.get_fit_parameter_values_from_row( row) self.model.update_ws_fit_function_parameters(workspaces, parameter_values) def handle_fit_selected_in_table(self): rows = self.view.fit_table.get_selected_rows() fit_information = [] for i, row in enumerate(rows): workspaces = self.get_workspaces_for_row_in_fit_table(row) fit = self.context.fitting_context.find_fit_for_input_workspace_list_and_function( workspaces, self.model.function_name) fit_information += [ FitPlotInformation(input_workspaces=workspaces, fit=fit) ] self.selected_sequential_fit_notifier.notify_subscribers( fit_information) def get_workspaces_for_row_in_fit_table(self, row): return self.view.fit_table.get_workspace_names_from_row(row).split("/")
class FittingTabPresenter(object): def __init__(self, view, model, context): self.view = view self.model = model self.context = context self._selected_data = [] self._start_x = [self.view.start_time] self._end_x = [self.view.end_time] self._grppair_index = {} self._fit_status = [None] self._fit_chi_squared = [0.0] self._fit_function = [None] self._tf_asymmetry_mode = False self._fit_function_cache = [None] self._plot_type = None self._number_of_fits_cached = 0 self._multi_domain_function = None self.manual_selection_made = False self.automatically_update_fit_name = True self.thread_success = True self.fitting_calculation_model = None self.update_selected_workspace_list_for_fit() self.fit_function_changed_notifier = GenericObservable() self.fit_parameter_changed_notifier = GenericObservable() self.fit_type_changed_notifier = GenericObservable() self.selected_single_fit_notifier = GenericObservable() self.gui_context_observer = GenericObserverWithArgPassing( self.handle_gui_changes_made) self.selected_group_pair_observer = GenericObserver( self.handle_selected_group_pair_changed) self.selected_plot_type_observer = GenericObserverWithArgPassing( self.handle_selected_plot_type_changed) self.input_workspace_observer = GenericObserver( self.handle_new_data_loaded) self.disable_tab_observer = GenericObserver(self.disable_view) self.enable_tab_observer = GenericObserver(self.enable_view) self.instrument_changed_observer = GenericObserver( self.instrument_changed) self.update_view_from_model_observer = GenericObserverWithArgPassing( self.update_view_from_model) self.initialise_model_options() self.double_pulse_observer = GenericObserverWithArgPassing( self.handle_double_pulse_set) self.model.context.gui_context.add_non_calc_subscriber( self.double_pulse_observer) self.view.setEnabled(False) self.enable_editing_notifier = GenericObservable() self.disable_editing_notifier = GenericObservable() def disable_view(self): self.view.setEnabled(False) def enable_view(self): if self.selected_data: self.view.setEnabled(True) @property def selected_data(self): return self._selected_data @selected_data.setter def selected_data(self, selected_data): if self._selected_data == selected_data: return self._selected_data = selected_data self.clear_and_reset_gui_state() @property def start_x(self): return self._start_x @property def end_x(self): return self._end_x # Respond to changes on view def handle_select_fit_data_clicked(self): selected_data, dialog_return = WorkspaceSelectorView.get_selected_data( self.context.data_context.current_runs, self.context.data_context.instrument, self.selected_data, self.view.fit_to_raw, self._plot_type, self.context, self.view) if dialog_return: self.selected_data = selected_data self.manual_selection_made = True def handle_new_data_loaded(self): self.manual_selection_made = False self.view.plot_guess_checkbox.setChecked(False) self.update_selected_workspace_list_for_fit() self.model.create_ws_fit_function_map() if self.selected_data: self.view.setEnabled(True) def handle_gui_changes_made(self, changed_values): for key in changed_values.keys(): if key in ['FirstGoodDataFromFile', 'FirstGoodData']: self.reset_start_time_to_first_good_data_value() def handle_selected_group_pair_changed(self): self.update_selected_workspace_list_for_fit() self._update_stored_fit_functions() def handle_selected_plot_type_changed(self, plot_type): self._plot_type = plot_type self.update_selected_workspace_list_for_fit() def handle_display_workspace_changed(self): current_index = self.view.get_index_for_start_end_times() self.view.start_time = self.start_x[current_index] self.view.end_time = self.end_x[current_index] self.view.function_browser.setCurrentDataset(current_index) self._update_stored_fit_functions() self.update_fit_status_information_in_view() self.handle_plot_guess_changed( ) # update the guess (use the selected workspace as data for the guess) self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) def handle_use_rebin_changed(self): if not self.view.fit_to_raw and not self.context._do_rebin(): self.view.fit_to_raw = True self.view.warning_popup('No rebin options specified') return if not self.manual_selection_made: self.update_selected_workspace_list_for_fit() else: self.selected_data = self.context.get_list_of_binned_or_unbinned_workspaces_from_equivalents( self.selected_data) self.context.fitting_context.fit_raw = self.view.fit_to_raw self.update_model_from_view(fit_to_raw=self.view.fit_to_raw) def handle_fit_type_changed(self): self.view.undo_fit_button.setEnabled(False) if self.view.is_simul_fit(): self.view.workspace_combo_box_label.setText( 'Display parameters for') self.view.enable_simul_fit_options() self.view.switch_to_simultaneous() self._update_stored_fit_functions() self.update_fit_specifier_list() else: self.selected_data = self.get_workspace_selected_list() self.view.workspace_combo_box_label.setText('Select Workspace') self.view.switch_to_single() self._update_stored_fit_functions() self.view.disable_simul_fit_options() self.update_model_from_view(fit_function=self._fit_function[0], fit_type=self._get_fit_type(), fit_by=self.view.simultaneous_fit_by) self.fit_type_changed_notifier.notify_subscribers() self.fit_function_changed_notifier.notify_subscribers() # Send the workspaces to be plotted self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) self.handle_plot_guess_changed() def handle_plot_guess_changed(self): index = self.view.get_index_for_start_end_times() workspaces = self.get_fit_input_workspaces() self.model.change_plot_guess(self.view.plot_guess, workspaces, index) def handle_fit_clicked(self): if len(self.selected_data) < 1: self.view.warning_popup('No data selected to fit') return self.perform_fit() def handle_started(self): self.disable_editing_notifier.notify_subscribers() self.thread_success = True def handle_finished(self): self.enable_editing_notifier.notify_subscribers() if not self.thread_success: return fit_function, fit_status, fit_chi_squared = self.fitting_calculation_model.result if any([not fit_function, not fit_status, not fit_chi_squared]): return if self.view.is_simul_fit(): self._fit_function[0] = fit_function self._fit_status = [fit_status] * len(self.start_x) self._fit_chi_squared = [fit_chi_squared] * len(self.start_x) else: current_index = self.view.get_index_for_start_end_times() self._fit_function[current_index] = fit_function self._fit_status[current_index] = fit_status self._fit_chi_squared[current_index] = fit_chi_squared self.update_fit_status_information_in_view() self.view.undo_fit_button.setEnabled(True) self.view.plot_guess_checkbox.setChecked(False) # Send the workspaces to be plotted self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) # Update parameter values in sequential tab. parameter_values = self.model.get_fit_function_parameter_values( fit_function) self.model.update_ws_fit_function_parameters( self.get_fit_input_workspaces(), parameter_values) self.fit_parameter_changed_notifier.notify_subscribers() def handle_error(self, error): self.enable_editing_notifier.notify_subscribers() self.thread_success = False self.view.warning_popup(error) def handle_start_x_updated(self): value = self.view.start_time index = self.view.get_index_for_start_end_times() self.update_start_x(index, value) self.update_model_from_view(startX=value) def handle_end_x_updated(self): value = self.view.end_time index = self.view.get_index_for_start_end_times() self.update_end_x(index, value) self.update_model_from_view(endX=value) def handle_minimiser_changed(self): self.update_model_from_view(minimiser=self.view.minimizer) def handle_evaluation_type_changed(self): self.update_model_from_view(evaluation_type=self.view.evaluation_type) def handle_fit_name_changed_by_user(self): self.automatically_update_fit_name = False self.model.function_name = self.view.function_name def handle_function_structure_changed(self): if not self._fit_function[0]: self.handle_display_workspace_changed() self.view.plot_guess_checkbox.setChecked(False) if self._tf_asymmetry_mode: self.view.warning_popup( 'Cannot change function structure during tf asymmetry mode') self.view.function_browser.blockSignals(True) self.view.function_browser.setFunction( str(self._fit_function[ self.view.get_index_for_start_end_times()])) self.view.function_browser.blockSignals(False) return if not self.view.fit_object: if self.view.is_simul_fit(): self._fit_function = [None] else: self._fit_function = [None] * len(self.selected_data)\ if self.selected_data else [None] self.model.clear_fit_information() self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) else: self._fit_function = [ func.clone() for func in self._get_fit_function() ] self.clear_fit_information() if self.automatically_update_fit_name: name = self._get_fit_function()[0] self.view.function_name = self.model.get_function_name(name) self.model.function_name = self.view.function_name self.update_model_from_view( fit_function=self._fit_function[0], global_parameters=self.view.get_global_parameters()) self.fit_function_changed_notifier.notify_subscribers() def handle_double_pulse_set(self, updated_variables): if 'DoublePulseEnabled' in updated_variables: self.view.tf_asymmetry_mode = False def handle_tf_asymmetry_mode_changed(self): def calculate_tf_fit_function(original_fit_function): tf_asymmetry_parameters = self.get_parameters_for_tf_function_calculation( original_fit_function) try: tf_function = self.model.convert_to_tf_function( tf_asymmetry_parameters) except RuntimeError: self.view.warning_popup( 'The input function was not of the form N*(1+f)+A*exp(-lambda*t)' ) return tf_asymmetry_parameters['InputFunction'] return tf_function self.view.undo_fit_button.setEnabled(False) self.view.plot_guess_checkbox.setChecked(False) groups_only = self.check_workspaces_are_tf_asymmetry_compliant( self.selected_data) if (not groups_only and self.view.tf_asymmetry_mode ) or not self.view.fit_object and self.view.tf_asymmetry_mode: self.view.tf_asymmetry_mode = False self.view.warning_popup( 'Can only fit groups in tf asymmetry mode and need a function defined' ) return if self._tf_asymmetry_mode == self.view.tf_asymmetry_mode: return self._tf_asymmetry_mode = self.view.tf_asymmetry_mode global_parameters = self.view.get_global_parameters() if self._tf_asymmetry_mode: self.view.select_workspaces_to_fit_button.setEnabled(False) new_global_parameters = [ str('f0.f1.f1.' + item) for item in global_parameters ] if self.automatically_update_fit_name: self.view.function_name += ',TFAsymmetry' self.model.function_name = self.view.function_name else: new_global_parameters = [item[9:] for item in global_parameters] if self.automatically_update_fit_name: self.view.function_name = self.view.function_name.replace( ',TFAsymmetry', '') self.model.function_name = self.view.function_name if not self.view.is_simul_fit(): for index, fit_function in enumerate(self._fit_function): fit_function = fit_function if fit_function else self.view.fit_object.clone( ) new_function = calculate_tf_fit_function(fit_function) self._fit_function[index] = new_function.clone() self.view.function_browser.blockSignals(True) self.view.function_browser.clear() self.view.function_browser.setFunction( str(self._fit_function[ self.view.get_index_for_start_end_times()])) self.view.function_browser.setGlobalParameters( new_global_parameters) self.view.function_browser.blockSignals(False) else: new_function = calculate_tf_fit_function(self.view.fit_object) self._fit_function = [new_function.clone()] self.view.function_browser.blockSignals(True) self.view.function_browser.clear() self.view.function_browser.setFunction(str(self._fit_function[0])) self.view.function_browser.setGlobalParameters( new_global_parameters) self.view.function_browser.blockSignals(False) self.update_fit_status_information_in_view() self.handle_display_workspace_changed() self.update_model_from_view( fit_function=self._fit_function[0], tf_asymmetry_mode=self.view.tf_asymmetry_mode) self.fit_function_changed_notifier.notify_subscribers() def get_parameters_for_tf_function_calculation(self, fit_function): mode = 'Construct' if self.view.tf_asymmetry_mode else 'Extract' workspace_list = self.selected_data if self.view.is_simul_fit() else [ self.view.display_workspace ] return { 'InputFunction': fit_function, 'WorkspaceList': workspace_list, 'Mode': mode, 'CopyTies': False } def handle_function_parameter_changed(self): if not self.view.is_simul_fit(): index = self.view.get_index_for_start_end_times() fit_function = self._get_fit_function()[index] self._fit_function[index] = self._get_fit_function()[index] else: fit_function = self._get_fit_function()[0] self._fit_function = self._get_fit_function() parameter_values = self.model.get_fit_function_parameter_values( fit_function) self.model.update_ws_fit_function_parameters( self.get_fit_input_workspaces(), parameter_values) self.fit_parameter_changed_notifier.notify_subscribers() self.model.update_plot_guess(self.get_fit_input_workspaces(), self.view.get_index_for_start_end_times()) def handle_undo_fit_clicked(self): self._fit_function = self._fit_function_cache self._fit_function = self._fit_function_cache self.clear_fit_information() self.update_fit_status_information_in_view() self.view.undo_fit_button.setEnabled(False) self.context.fitting_context.remove_latest_fit() self._number_of_fits_cached = 0 self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) def handle_fit_by_changed(self): self.manual_selection_made = False # reset manual selection flag self.update_selected_workspace_list_for_fit() self.view.simul_fit_by_specifier.setEnabled(True) self.view.select_workspaces_to_fit_button.setEnabled(False) self.update_model_from_view(fit_function=self._fit_function[0], fit_by=self.view.simultaneous_fit_by) self.fit_type_changed_notifier.notify_subscribers() self.fit_function_changed_notifier.notify_subscribers() # Send the workspaces to be plotted self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) def handle_fit_specifier_changed(self): self.selected_data = self.get_workspace_selected_list() # Send the workspaces to be plotted self.selected_single_fit_notifier.notify_subscribers( self.get_selected_fit_workspaces()) # Perform fit def perform_fit(self): if not self.view.fit_object: return self._fit_function_cache = [ func.clone() for func in self._fit_function ] try: workspaces = self.get_fit_input_workspaces() self._number_of_fits_cached += 1 calculation_function = functools.partial( self.model.evaluate_single_fit, workspaces) self.calculation_thread = self.create_thread(calculation_function) self.calculation_thread.threadWrapperSetUp(self.handle_started, self.handle_finished, self.handle_error) self.calculation_thread.start() except ValueError as error: self.view.warning_popup(error) # Query view and update model. def clear_and_reset_gui_state(self): self.view.set_datasets_in_function_browser(self.selected_data) self._fit_status = [None] * len( self.selected_data) if self.selected_data else [None] self._fit_chi_squared = [0.0] * len( self.selected_data) if self.selected_data else [0.0] if self.view.fit_object: self._fit_function = [ func.clone() for func in self._get_fit_function() ] else: self._fit_function = [None] * len( self.selected_data) if self.selected_data else [None] self.view.undo_fit_button.setEnabled(False) self.reset_start_time_to_first_good_data_value() self.view.update_displayed_data_combo_box(self.selected_data) self.update_model_from_view(fit_function=self._fit_function[0]) self.update_fit_status_information_in_view() def clear_fit_information(self): self._fit_status = [None] * len( self.selected_data) if self.selected_data else [None] self._fit_chi_squared = [0.0] * len( self.selected_data) if self.selected_data else [0.0] self.update_fit_status_information_in_view() self.view.undo_fit_button.setEnabled(False) def update_selected_workspace_list_for_fit(self): if self.view.is_simul_fit(): if self.manual_selection_made: return # if it is a manual selection then the data should not change self.update_fit_specifier_list() self.selected_data = self.get_workspace_selected_list() def set_display_workspace(self, workspace_name): self.view.display_workspace = workspace_name self.handle_display_workspace_changed() def get_workspace_selected_list(self): if isinstance(self.context, FrequencyDomainAnalysisContext): freq = self.context._frequency_context.plot_type else: freq = 'None' selected_runs, selected_groups_and_pairs = self._get_selected_runs_and_groups_for_fitting( ) selected_workspaces = [] for grp_and_pair in selected_groups_and_pairs: selected_workspaces += self.context.get_names_of_workspaces_to_fit( runs=selected_runs, group_and_pair=grp_and_pair, phasequad=False, rebin=not self.view.fit_to_raw, freq=freq) selected_workspaces = list( set(self._check_data_exists(selected_workspaces))) if len(selected_workspaces) > 1: # sort the list to preserve order selected_workspaces.sort(key=self.model.workspace_list_sorter) return selected_workspaces def update_fit_specifier_list(self): if self.view.simultaneous_fit_by == "Run": # extract runs from run list of lists, which is in the format [ [run,...,runs],[runs],...,[runs] ] flattened_run_list = [ str(item) for sublist in self.context.data_context.current_runs for item in sublist ] flattened_run_list.sort() simul_choices = flattened_run_list elif self.view.simultaneous_fit_by == "Group/Pair": simul_choices = self._get_selected_groups_and_pairs() else: simul_choices = [] self.view.setup_fit_by_specifier(simul_choices) def _update_stored_fit_functions(self): if self.view.is_simul_fit(): if self.view.fit_object: self._fit_function = [self.view.fit_object.clone()] else: self._fit_function = [None] else: # we need to convert stored function into equivalent function if self.view.fit_object: # make sure there is a fit function in the browser if isinstance(self.view.fit_object, MultiDomainFunction): equiv_fit_function = self.view.fit_object.createEquivalentFunctions( ) single_domain_fit_functions = [ func.clone() for func in equiv_fit_function ] else: single_domain_fit_functions = [ self.view.fit_object.clone() ] self._fit_function = single_domain_fit_functions else: self._fit_function = [None] * len(self._start_x) def _get_fit_function(self): if self.view.is_simul_fit(): return [self.view.fit_object ] # return the fit function stored in the browser else: # we need to convert stored function into equiv if self.view.fit_object: # make sure thers a fit function in the browser if isinstance(self.view.fit_object, MultiDomainFunction): equiv_fit_funtion = self.view.fit_object.createEquivalentFunctions( ) single_domain_fit_function = equiv_fit_funtion else: single_domain_fit_function = [self.view.fit_object] return single_domain_fit_function else: return [None] * len(self._start_x) def _current_fit_function(self): return self._fit_function[self._fit_function_index()] def _fit_function_index(self): if self.view.is_simul_fit(): return 0 # if we are doing a single simultaneous fit return 0 else: # else fitting on one of the display workspaces return self.view.get_index_for_start_end_times() def update_start_x(self, index, value): self._start_x[index] = value def update_end_x(self, index, value): self._end_x[index] = value def create_thread(self, callback): self.fitting_calculation_model = ThreadModelWrapperWithOutput(callback) return thread_model.ThreadModel(self.fitting_calculation_model) def retrieve_first_good_data_from_run_name(self, workspace_name): try: run = [float(re.search('[0-9]+', workspace_name).group())] except AttributeError: return 0.0 return self.context.first_good_data(run) def reset_start_time_to_first_good_data_value(self): self._start_x = [self.retrieve_first_good_data_from_run_name(run_name) for run_name in self.selected_data] if \ self.selected_data else [0.0] self._end_x = [self.view.end_time] * len( self.selected_data) if self.selected_data else [15.0] self.view.start_time = self.start_x[0] if 0 < len( self.start_x) else 0.0 self.view.end_time = self.end_x[0] if 0 < len(self.end_x) else 15.0 def update_fit_status_information_in_view(self): current_index = self._fit_function_index() self.view.update_with_fit_outputs(self._fit_function[current_index], self._fit_status[current_index], self._fit_chi_squared[current_index]) self.view.update_global_fit_state(self._fit_status) def update_view_from_model(self, workspace_removed=None): if workspace_removed: self.selected_data = [ item for item in self.selected_data if item != workspace_removed ] else: self.selected_data = [] def update_model_from_view(self, **kwargs): self.model.update_model_fit_options(**kwargs) def initialise_model_options(self): fitting_options = { "fit_function": self._fit_function[0], "startX": self.start_x[0], "endX": self.end_x[0], "minimiser": self.view.minimizer, "evaluation_type": self.view.evaluation_type, "fit_to_raw": self.view.fit_to_raw, "fit_type": self._get_fit_type(), "fit_by": self.view.simultaneous_fit_by, "global_parameters": self.view.get_global_parameters(), "tf_asymmetry_mode": self.view.tf_asymmetry_mode } self.model.update_model_fit_options(**fitting_options) def _get_fit_type(self): if self.view.is_simul_fit(): fit_type = "Simul" else: fit_type = "Single" return fit_type def get_fit_input_workspaces(self): if self.view.is_simul_fit(): return self.selected_data else: return [self.view.display_workspace] def get_selected_fit_workspaces(self): if self.selected_data: if self._get_fit_type() == "Single": fit = self.context.fitting_context.find_fit_for_input_workspace_list_and_function( [self.view.display_workspace], self.model.function_name) return [ FitPlotInformation( input_workspaces=[self.view.display_workspace], fit=fit) ] else: fit = self.context.fitting_context.find_fit_for_input_workspace_list_and_function( self.selected_data, self.model.function_name) return [ FitPlotInformation(input_workspaces=self.selected_data, fit=fit) ] else: return [] def instrument_changed(self): self.view.tf_asymmetry_mode = False def _get_selected_groups_and_pairs(self): return self.context.group_pair_context.selected_groups + self.context.group_pair_context.selected_pairs def _get_selected_runs_and_groups_for_fitting(self): runs = 'All' groups_and_pairs = self._get_selected_groups_and_pairs() if self.view.is_simul_fit(): if self.view.simultaneous_fit_by == "Run": runs = self.view.simultaneous_fit_by_specifier elif self.view.simultaneous_fit_by == "Group/Pair": groups_and_pairs = [self.view.simultaneous_fit_by_specifier] return runs, groups_and_pairs @staticmethod def check_workspaces_are_tf_asymmetry_compliant(workspace_list): non_compliant_workspaces = [ item for item in workspace_list if 'Group' not in item ] return False if non_compliant_workspaces else True @staticmethod def _check_data_exists(guess_selection): return [ item for item in guess_selection if AnalysisDataService.doesExist(item) ]
class BasicFittingPresenter: """ The BasicFittingPresenter holds a BasicFittingView and BasicFittingModel. """ def __init__(self, view: BasicFittingView, model: BasicFittingModel): """Initialize the BasicFittingPresenter. Sets up the slots and event observers.""" self.view = view self.model = model self.initialize_model_options() self.thread_success = True self.enable_editing_notifier = GenericObservable() self.disable_editing_notifier = GenericObservable() self.fitting_calculation_model = None self.remove_plot_guess_notifier = GenericObservable() self.update_plot_guess_notifier = GenericObservable() self.fit_function_changed_notifier = GenericObservable() self.fit_parameter_changed_notifier = GenericObservable() self.selected_fit_results_changed = GenericObservable() self.input_workspace_observer = GenericObserver( self.handle_new_data_loaded) self.gui_context_observer = GenericObserverWithArgPassing( self.handle_gui_changes_made) self.update_view_from_model_observer = GenericObserverWithArgPassing( self.handle_ads_clear_or_remove_workspace_event) self.instrument_changed_observer = GenericObserver( self.handle_instrument_changed) self.selected_group_pair_observer = GenericObserver( self.handle_selected_group_pair_changed) self.double_pulse_observer = GenericObserverWithArgPassing( self.handle_pulse_type_changed) self.sequential_fit_finished_observer = GenericObserver( self.handle_sequential_fit_finished) self.fit_parameter_updated_observer = GenericObserver( self.update_fit_function_in_view_from_model) self.fsg_model = None self.fsg_view = None self.fsg_presenter = None self.view.set_slot_for_fit_generator_clicked( self.handle_fit_generator_clicked) self.view.set_slot_for_fit_button_clicked(self.handle_fit_clicked) self.view.set_slot_for_undo_fit_clicked(self.handle_undo_fit_clicked) self.view.set_slot_for_plot_guess_changed( self.handle_plot_guess_changed) self.view.set_slot_for_fit_name_changed( self.handle_function_name_changed_by_user) self.view.set_slot_for_dataset_changed( self.handle_dataset_name_changed) self.view.set_slot_for_covariance_matrix_clicked( self.handle_covariance_matrix_clicked) self.view.set_slot_for_function_structure_changed( self.handle_function_structure_changed) self.view.set_slot_for_function_parameter_changed( lambda function_index, parameter: self. handle_function_parameter_changed(function_index, parameter)) self.view.set_slot_for_function_attribute_changed( lambda attribute: self.handle_function_attribute_changed(attribute )) self.view.set_slot_for_start_x_updated(self.handle_start_x_updated) self.view.set_slot_for_end_x_updated(self.handle_end_x_updated) self.view.set_slot_for_exclude_range_state_changed( self.handle_exclude_range_state_changed) self.view.set_slot_for_exclude_start_x_updated( self.handle_exclude_start_x_updated) self.view.set_slot_for_exclude_end_x_updated( self.handle_exclude_end_x_updated) self.view.set_slot_for_minimizer_changed(self.handle_minimizer_changed) self.view.set_slot_for_evaluation_type_changed( self.handle_evaluation_type_changed) self.view.set_slot_for_use_raw_changed(self.handle_use_rebin_changed) def initialize_model_options(self) -> None: """Initialise the model with the default fitting options.""" self.model.minimizer = self.view.minimizer self.model.evaluation_type = self.view.evaluation_type self.model.fit_to_raw = self.view.fit_to_raw def handle_ads_clear_or_remove_workspace_event(self, _: str = None) -> None: """Handle when there is a clear or remove workspace event in the ADS.""" self.update_and_reset_all_data() if self.model.number_of_datasets == 0: self.view.disable_view() else: self.enable_editing_notifier.notify_subscribers() def handle_gui_changes_made(self, changed_values: dict) -> None: """Handle when the good data checkbox is changed in the home tab.""" for key in changed_values.keys(): if key in ["FirstGoodDataFromFile", "FirstGoodData"]: self.reset_start_xs_and_end_xs() def handle_new_data_loaded(self) -> None: """Handle when new data has been loaded into the interface.""" self.update_and_reset_all_data() self.view.plot_guess, self.model.plot_guess = False, False self.clear_undo_data() if self.model.number_of_datasets == 0: self.view.disable_view() else: self.enable_editing_notifier.notify_subscribers() def handle_instrument_changed(self) -> None: """Handles when an instrument is changed and switches to normal fitting mode. Overridden by child.""" self.update_and_reset_all_data() self.clear_undo_data() self.model.remove_all_fits_from_context() def handle_selected_group_pair_changed(self) -> None: """Update the displayed workspaces when the selected group/pairs change in grouping tab.""" self.update_and_reset_all_data() def handle_pulse_type_changed(self, updated_variables: dict) -> None: """Handles when double pulse mode is switched on and switches to normal fitting mode.""" if "DoublePulseEnabled" in updated_variables: self.update_and_reset_all_data() def handle_plot_guess_changed(self) -> None: """Handle when plot guess is ticked or un-ticked.""" self.model.plot_guess = self.view.plot_guess self.update_plot_guess() def handle_undo_fit_clicked(self) -> None: """Handle when undo fit is clicked.""" self.model.undo_previous_fit() self.view.set_number_of_undos(self.model.number_of_undos()) self.update_fit_function_in_view_from_model() self.update_fit_statuses_and_chi_squared_in_view_from_model() self.update_covariance_matrix_button() self.update_plot_fit() self.update_plot_guess() def handle_fit_clicked(self) -> None: """Handle when the fit button is clicked.""" if self.model.number_of_datasets < 1: self.view.warning_popup("No data selected for fitting.") return if not self.view.fit_object: return self.model.save_current_fit_function_to_undo_data() self._perform_fit() def handle_started(self) -> None: """Handle when fitting has started.""" self.disable_editing_notifier.notify_subscribers() self.thread_success = True def handle_finished(self) -> None: """Handle when fitting has finished.""" self.enable_editing_notifier.notify_subscribers() if not self.thread_success: return fit_result = self.fitting_calculation_model.result if fit_result is None: return fit_function, fit_status, fit_chi_squared = fit_result if any([ not fit_function, not fit_status, fit_chi_squared != 0.0 and not fit_chi_squared ]): return self.handle_fitting_finished(fit_function, fit_status, fit_chi_squared) self.view.set_number_of_undos(self.model.number_of_undos()) self.view.plot_guess, self.model.plot_guess = False, False def handle_fitting_finished(self, fit_function, fit_status, chi_squared) -> None: """Handle when fitting is finished.""" self.update_fit_statuses_and_chi_squared_in_model( fit_status, chi_squared) self.update_fit_function_in_model(fit_function) self.update_fit_statuses_and_chi_squared_in_view_from_model() self.update_covariance_matrix_button() self.update_fit_function_in_view_from_model() self.update_plot_fit() self.fit_parameter_changed_notifier.notify_subscribers() def handle_error(self, error: str) -> None: """Handle when an error occurs while fitting.""" self.enable_editing_notifier.notify_subscribers() self.thread_success = False self.view.warning_popup(error) def handle_fit_generator_clicked(self) -> None: """Handle when the Fit Generator button has been clicked.""" fitting_mode = FittingMode.SIMULTANEOUS if self.model.simultaneous_fitting_mode else FittingMode.SEQUENTIAL self._open_fit_script_generator_interface( self.model.dataset_names, fitting_mode, self._get_fit_browser_options()) def handle_dataset_name_changed(self) -> None: """Handle when the display workspace combo box is changed.""" self.model.current_dataset_index = self.view.current_dataset_index self.update_fit_statuses_and_chi_squared_in_view_from_model() self.update_covariance_matrix_button() self.update_fit_function_in_view_from_model() self.update_start_and_end_x_in_view_from_model() self.update_plot_fit() self.update_plot_guess() def handle_covariance_matrix_clicked(self) -> None: """Handle when the Covariance Matrix button is clicked.""" covariance_matrix = self.model.current_normalised_covariance_matrix() if covariance_matrix is not None: self.view.show_normalised_covariance_matrix( covariance_matrix.workspace, covariance_matrix.workspace_name) def handle_function_name_changed_by_user(self) -> None: """Handle when the fit name is changed by the user.""" self.model.function_name_auto_update = False self.model.function_name = self.view.function_name def handle_minimizer_changed(self) -> None: """Handle when a minimizer is changed.""" self.model.minimizer = self.view.minimizer def handle_evaluation_type_changed(self) -> None: """Handle when the evaluation type is changed.""" self.model.evaluation_type = self.view.evaluation_type def handle_function_structure_changed(self) -> None: """Handle when the function structure is changed.""" self.update_fit_functions_in_model_from_view() self.automatically_update_function_name() if self.model.get_active_fit_function() is None: self.clear_undo_data() self.update_plot_fit() self.reset_fit_status_and_chi_squared_information() self.update_plot_guess() self.fit_function_changed_notifier.notify_subscribers() # Required to update the function browser to display the errors when first adding a function. self.view.set_current_dataset_index(self.model.current_dataset_index) def handle_function_parameter_changed(self, function_index: str, parameter: str) -> None: """Handle when the value of a parameter in a function is changed.""" full_parameter = f"{function_index}{parameter}" self.model.update_parameter_value( full_parameter, self.view.parameter_value(full_parameter)) self.update_plot_guess() self.fit_function_changed_notifier.notify_subscribers() self.fit_parameter_changed_notifier.notify_subscribers() def handle_function_attribute_changed(self, attribute: str) -> None: """Handle when the value of a attribute in a function is changed.""" self.model.update_attribute_value(attribute, self.view.attribute_value(attribute)) def handle_start_x_updated(self) -> None: """Handle when the start X is changed.""" new_start_x, new_end_x = check_start_x_is_valid( self.model.current_dataset_name, self.view.start_x, self.view.end_x, self.model.current_start_x) self.update_start_and_end_x_in_view_and_model(new_start_x, new_end_x) def handle_end_x_updated(self) -> None: """Handle when the end X is changed.""" new_start_x, new_end_x = check_end_x_is_valid( self.model.current_dataset_name, self.view.start_x, self.view.end_x, self.model.current_end_x) self.update_start_and_end_x_in_view_and_model(new_start_x, new_end_x) def handle_exclude_range_state_changed(self) -> None: """Handles when Exclude Range is ticked or unticked.""" self.model.exclude_range = self.view.exclude_range self.view.set_exclude_start_and_end_x_visible(self.model.exclude_range) def handle_exclude_start_x_updated(self) -> None: """Handle when the exclude start X is changed.""" exclude_start_x, exclude_end_x = check_exclude_start_x_is_valid( self.view.start_x, self.view.end_x, self.view.exclude_start_x, self.view.exclude_end_x, self.model.current_exclude_start_x) self.update_exclude_start_and_end_x_in_view_and_model( exclude_start_x, exclude_end_x) def handle_exclude_end_x_updated(self) -> None: """Handle when the exclude end X is changed.""" exclude_start_x, exclude_end_x = check_exclude_end_x_is_valid( self.view.start_x, self.view.end_x, self.view.exclude_start_x, self.view.exclude_end_x, self.model.current_exclude_end_x) self.update_exclude_start_and_end_x_in_view_and_model( exclude_start_x, exclude_end_x) def handle_use_rebin_changed(self) -> None: """Handle the Fit to raw data checkbox state change.""" if self._check_rebin_options(): self.model.fit_to_raw = self.view.fit_to_raw def handle_sequential_fit_finished(self) -> None: """Handles when a sequential fit has been performed and has finished executing in the sequential fitting tab.""" self.update_fit_function_in_view_from_model() self.update_fit_statuses_and_chi_squared_in_view_from_model() def clear_undo_data(self) -> None: """Clear all the previously saved undo fit functions and other data.""" self.model.clear_undo_data() self.view.set_number_of_undos(self.model.number_of_undos()) def reset_fit_status_and_chi_squared_information(self) -> None: """Clear the fit status and chi squared information in the view and model.""" self.model.reset_fit_statuses_and_chi_squared() self.update_fit_statuses_and_chi_squared_in_view_from_model() def reset_start_xs_and_end_xs(self) -> None: """Reset the start Xs and end Xs using the data stored in the context.""" self.model.reset_start_xs_and_end_xs() self.view.start_x = self.model.current_start_x self.view.end_x = self.model.current_end_x self.view.exclude_start_x = self.model.current_exclude_start_x self.view.exclude_end_x = self.model.current_exclude_end_x def set_selected_dataset(self, dataset_name: str) -> None: """Sets the workspace to be displayed in the view programmatically.""" # Triggers handle_dataset_name_changed which updates the model self.view.current_dataset_name = dataset_name def set_current_dataset_index(self, dataset_index: int) -> None: """Set the current dataset index in the model and view.""" self.model.current_dataset_index = dataset_index self.view.set_current_dataset_index(dataset_index) def automatically_update_function_name(self) -> None: """Updates the function name used within the outputted fit workspaces.""" self.model.automatically_update_function_name() self.view.function_name = self.model.function_name def update_and_reset_all_data(self) -> None: """Updates the various data displayed in the fitting widget. Resets and clears previous fit information.""" # Triggers handle_dataset_name_changed self.update_dataset_names_in_view_and_model() def update_dataset_names_in_view_and_model(self) -> None: """Updates the datasets currently displayed.""" self.model.dataset_names = self.model.get_workspace_names_to_display_from_context( ) self.view.set_datasets_in_function_browser(self.model.dataset_names) self.view.update_dataset_name_combo_box(self.model.dataset_names) self.model.current_dataset_index = self.view.current_dataset_index def update_fit_statuses_and_chi_squared_in_model( self, fit_status: str, chi_squared: float) -> None: """Updates the fit status and chi squared stored in the model. This is used after a fit.""" self.model.current_fit_status = fit_status self.model.current_chi_squared = chi_squared def update_fit_function_in_model(self, fit_function: IFunction) -> None: """Updates the fit function stored in the model. This is used after a fit.""" self.model.current_single_fit_function = fit_function def update_fit_function_in_view_from_model(self) -> None: """Updates the parameters of a fit function shown in the view.""" self.view.set_current_dataset_index(self.model.current_dataset_index) self.view.update_fit_function(self.model.get_active_fit_function()) def update_fit_functions_in_model_from_view(self) -> None: """Updates the fit functions stored in the model using the view.""" self.update_single_fit_functions_in_model() self.fit_function_changed_notifier.notify_subscribers() def update_single_fit_functions_in_model(self) -> None: """Updates the single fit functions in the model using the view.""" self.model.single_fit_functions = self._get_single_fit_functions_from_view( ) def update_fit_statuses_and_chi_squared_in_view_from_model(self) -> None: """Updates the local and global fit status and chi squared in the view.""" self.view.update_local_fit_status_and_chi_squared( self.model.current_fit_status, self.model.current_chi_squared) self.view.update_global_fit_status(self.model.fit_statuses, self.model.current_dataset_index) def update_covariance_matrix_button(self) -> None: """Updates the covariance matrix button to be enabled if a covariance matrix exists for the selected data.""" self.view.set_covariance_button_enabled( self.model.has_normalised_covariance_matrix()) def update_start_and_end_x_in_view_from_model(self) -> None: """Updates the start and end x in the view using the current values in the model.""" self.view.start_x = self.model.current_start_x self.view.end_x = self.model.current_end_x self.view.exclude_start_x = self.model.current_exclude_start_x self.view.exclude_end_x = self.model.current_exclude_end_x def update_exclude_start_and_end_x_in_view_and_model( self, exclude_start_x: float, exclude_end_x: float) -> None: """Updates the current exclude start and end X in the view and model.""" self.view.exclude_start_x, self.view.exclude_end_x = exclude_start_x, exclude_end_x self.model.current_exclude_start_x, self.model.current_exclude_end_x = exclude_start_x, exclude_end_x def update_start_and_end_x_in_view_and_model(self, start_x: float, end_x: float) -> None: """Updates the start and end x in the model using the provided values.""" self.view.start_x, self.view.end_x = start_x, end_x self.model.current_start_x, self.model.current_end_x = start_x, end_x self.update_plot_guess() def update_plot_guess(self) -> None: """Updates the guess plot using the current dataset and function.""" self.remove_plot_guess_notifier.notify_subscribers() self.model.update_plot_guess() self.update_plot_guess_notifier.notify_subscribers() def update_plot_fit(self) -> None: """Updates the fit results on the plot using the currently active fit results.""" self.selected_fit_results_changed.notify_subscribers( self.model.get_active_fit_results()) def _get_single_fit_functions_from_view(self) -> list: """Returns the fit functions corresponding to each domain as a list.""" if self.view.fit_object: if isinstance(self.view.fit_object, MultiDomainFunction): return [ function.clone() for function in self.view.fit_object.createEquivalentFunctions() ] return [self.view.fit_object] return [None] * self.view.number_of_datasets() def _get_fit_browser_options(self) -> dict: """Returns the fitting options to use in the Fit Script Generator interface.""" return { "Minimizer": self.model.minimizer, "Evaluation Type": self.model.evaluation_type } def _perform_fit(self) -> None: """Perform the fit in a thread.""" try: self.calculation_thread = self._create_fitting_thread( self.model.perform_fit) self.calculation_thread.threadWrapperSetUp(self.handle_started, self.handle_finished, self.handle_error) self.calculation_thread.start() except ValueError as error: self.view.warning_popup(error) def _create_fitting_thread(self, callback) -> ThreadModel: """Create a thread for fitting.""" self.fitting_calculation_model = ThreadModelWrapperWithOutput(callback) return ThreadModel(self.fitting_calculation_model) def _open_fit_script_generator_interface(self, workspaces: list, fitting_mode: FittingMode, fit_options: dict) -> None: """Open the Fit Script Generator interface.""" self.fsg_model = FitScriptGeneratorModel() self.fsg_view = FitScriptGeneratorView(self.view, fitting_mode, fit_options) self.fsg_view.setWindowFlag(Qt.Window) self.fsg_presenter = FitScriptGeneratorPresenter( self.fsg_view, self.fsg_model, workspaces, self.view.start_x, self.view.end_x) self.fsg_presenter.openFitScriptGenerator() def _check_rebin_options(self) -> bool: """Check that a rebin was indeed requested in the fitting tab or in the context.""" if not self.view.fit_to_raw and not self.model.do_rebin: self.view.fit_to_raw = True self.view.warning_popup("No rebin options specified.") return False return True
class EAGroupingTablePresenter(object): def __init__(self, view, model): self._view = view self._model = model self._view.on_remove_group_button_clicked( self.handle_remove_group_button_clicked) self._view.on_user_changes_group_name(self.validate_group_name) self._view.on_table_data_changed(self.handle_data_change) self.selected_group_changed_notifier = GenericObservable() self._dataChangedNotifier = lambda: 0 self.rebin_notifier = GenericObservable() self.data_changed_notifier = GenericObservable() def notify_data_changed(self): self.data_changed_notifier.notify_subscribers() self._dataChangedNotifier() def _is_edited_name_duplicated(self, new_name): is_name_column_being_edited = self._view.grouping_table.currentColumn( ) == 0 is_name_not_unique = new_name in self._model.group_names return is_name_column_being_edited and is_name_not_unique def validate_group_name(self, text): if self._is_edited_name_duplicated(text): self._view.warning_popup("Groups must have unique names") return False if not re.match(run_utils.valid_name_regex, text): self._view.warning_popup( "Group names should only contain digits, characters and _") return False return True def disable_editing(self): self._view.disable_editing() def enable_editing(self): self._view.enable_editing() def add_group_to_view(self, group, state): self._view.disable_updates() assert isinstance(group, EAGroup) entry = [ str(group.name), str(group.run_number), group.detector, state, str(group.rebin_index), group.rebin_option ] self._view.add_entry_to_table(entry) self._view.enable_updates() def handle_remove_group_button_clicked(self): group_names = self._view.get_selected_group_names() if not group_names: self.remove_last_row_in_view_and_model() else: self.remove_selected_rows_in_view_and_model(group_names) self.notify_data_changed() def remove_selected_rows_in_view_and_model(self, group_names): self._view.remove_selected_groups() for group_name in group_names: self._model.remove_group_from_analysis(group_name) self._model.remove_groups_by_name(group_names) self.update_view_from_model() def remove_last_row_in_view_and_model(self): if self._view.num_rows() > 0: name = self._view.get_table_contents()[-1][0] self._view.remove_last_row() self._model.remove_group_from_analysis(name) self._model.remove_groups_by_name([name]) def handle_data_change(self, row, col): changed_item = self._view.get_table_item(row, col) workspace_name = self._view.get_table_item( row, INVERSE_GROUP_TABLE_COLUMNS['workspace_name']).text() to_analyse_changed = self.handle_to_analyse_column_changed( col, changed_item, workspace_name) self.handle_rebin_column_changed(col, row, changed_item) self.handle_rebin_option_column_changed(col, changed_item, workspace_name) self.handle_update(to_analyse_changed) def handle_to_analyse_column_changed(self, col, changed_item, workspace_name): to_analyse_changed = False if col == INVERSE_GROUP_TABLE_COLUMNS['to_analyse']: to_analyse_changed = True self.to_analyse_data_checkbox_changed(changed_item.checkState(), workspace_name) return to_analyse_changed def handle_rebin_column_changed(self, col, row, changed_item): if col == INVERSE_GROUP_TABLE_COLUMNS['rebin']: if changed_item.text() == REBIN_FIXED_OPTION: self._view.rebin_fixed_chosen(row) elif changed_item.text() == REBIN_VARIABLE_OPTION: self._view.rebin_variable_chosen(row) elif changed_item.text() == REBIN_NONE_OPTION: self._view.rebin_none_chosen(row) def handle_rebin_option_column_changed(self, col, changed_item, workspace_name): if col == INVERSE_GROUP_TABLE_COLUMNS['rebin_options']: params = changed_item.text().split(":") if len(params) == 2: if params[0] == "Steps": """ param[1] is a string contain a float with the units KeV at the end of the string so units must be removed """ self._model.handle_rebin(name=workspace_name, rebin_type="Fixed", rebin_param=float( params[1][0:-3])) if params[0] == "Bin Boundaries": if len(params[1]) >= 1: self._model.handle_rebin(name=workspace_name, rebin_type="Variable", rebin_param=params[1]) def handle_update(self, update_model): if update_model: # Reset the view back to model values and exit early as the changes are invalid. self.notify_data_changed() return def update_model_from_view(self): table = self._view.get_table_contents() self._model.clear_groups() for entry in table: group = self._model.group_context.create_EAGroup( group_name=str(entry[0]), detector=str(entry[2]), run_number=str(entry[1])) if entry[4]: group.rebin_index = str(entry[4]) else: group.rebin_index = 0 group.rebin_option = str(entry[5]) self._model.add_group_from_table(group) def update_view_from_model(self): self._view.disable_updates() self._view.clear() for group in self._model.groups: if self._view.num_rows() >= MAXIMUM_NUMBER_OF_GROUPS: self._view.warning_popup( "Cannot add more than {} groups.".format( MAXIMUM_NUMBER_OF_GROUPS)) break else: to_analyse = True if group.name in self._model.selected_groups else False self.add_group_to_view(group, to_analyse) self._view.enable_updates() def to_analyse_data_checkbox_changed(self, state, group_name): group_added = True if state == 2 else False if group_added: self._model.add_group_to_analysis(group_name) else: self._model.remove_group_from_analysis(group_name) group_info = {'is_added': group_added, 'name': group_name} self.selected_group_changed_notifier.notify_subscribers(group_info) def plot_default_case(self): """ Detector 3 should be plotted by default and if not present Detector 1 should be plotted, if neither is present then Detector 4 and finally Detector 2 """ index_sorted_by_detectors = {} for row in range(self._view.num_rows()): group_name = self._view.get_table_item(row, 0).text() group = self._model._groups[group_name] detector = group.detector if detector in index_sorted_by_detectors: index_sorted_by_detectors[detector].append(row) else: index_sorted_by_detectors[detector] = [row] for detector in DEFAULT_DETECTOR_PLOTTED_ORDER: if detector not in index_sorted_by_detectors: continue index_list = index_sorted_by_detectors[detector] if index_list: for index in index_list: self._view.set_to_analyse_state(index, True) return
class SeqFittingTabPresenter(object): def __init__(self, view, model, context): self.view = view self.model = model self.context = context self.fit_function = None self.selected_rows = [] self.calculation_thread = None self.fitting_calculation_model = None # Observers self.selected_workspaces_observer = GenericObserver( self.handle_selected_workspaces_changed) self.fit_type_changed_observer = GenericObserver( self.handle_selected_workspaces_changed) self.fit_function_updated_observer = GenericObserver( self.handle_fit_function_updated) self.fit_parameter_updated_observer = GenericObserver( self.handle_fit_function_parameter_changed) self.fit_parameter_changed_in_view = GenericObserverWithArgPassing( self.handle_updated_fit_parameter_in_table) self.selected_sequential_fit_notifier = GenericObservable() self.disable_tab_observer = GenericObserver( lambda: self.view.setEnabled(False)) self.enable_tab_observer = GenericObserver( lambda: self.view.setEnabled(True)) def create_thread(self, callback): self.fitting_calculation_model = ThreadModelWrapperWithOutput(callback) return thread_model.ThreadModel(self.fitting_calculation_model) def handle_fit_function_updated(self): if self.model.fit_function is None: self.view.fit_table.clear_fit_parameters() self.view.fit_table.reset_fit_quality() self.model.clear_fit_information() return parameter_values = [] number_of_parameters = self.model.fit_function.nParams() parameters = [ self.model.fit_function.parameterName(i) for i in range(number_of_parameters) ] # get parameters for each fit for row in range(self.view.fit_table.get_number_of_fits()): ws_names = self.get_workspaces_for_row_in_fit_table(row) fit_function = self.model.get_ws_fit_function(ws_names) parameter_values.append( self.model.get_fit_function_parameter_values(fit_function)) self.view.fit_table.set_parameters_and_values(parameters, parameter_values) def handle_fit_function_parameter_changed(self): self.view.fit_table.reset_fit_quality() fit_functions = self.model.stored_fit_functions for row in range(self.view.fit_table.get_number_of_fits()): parameter_values = self.model.get_fit_function_parameter_values( fit_functions[row]) self.view.fit_table.set_parameter_values_for_row( row, parameter_values) def handle_selected_workspaces_changed(self): runs, groups_and_pairs = self.model.get_runs_groups_and_pairs_for_fits( ) self.view.fit_table.set_fit_workspaces(runs, groups_and_pairs) self.model.create_ws_fit_function_map() self.handle_fit_function_updated() def handle_fit_selected_pressed(self): self.selected_rows = self.view.fit_table.get_selected_rows() self.handle_sequential_fit_requested() def handle_sequential_fit_pressed(self): self.view.fit_table.clear_fit_selection() self.selected_rows = [ i for i in range(self.view.fit_table.get_number_of_fits()) ] self.handle_sequential_fit_requested() def handle_fit_started(self): self.view.seq_fit_button.setEnabled(False) self.view.fit_selected_button.setEnabled(False) self.view.fit_table.block_signals(True) def handle_fit_error(self, error): self.view.warning_popup(error) self.view.fit_selected_button.setEnabled(True) self.view.seq_fit_button.setEnabled(True) self.view.fit_table.block_signals(False) def handle_sequential_fit_requested(self): if self.model.fit_function is None or len(self.selected_rows) == 0: return workspace_names = [] for row in self.selected_rows: workspace_names += [self.get_workspaces_for_row_in_fit_table(row)] calculation_function = functools.partial( self.model.evaluate_sequential_fit, workspace_names, self.view.use_initial_values_for_fits()) self.calculation_thread = self.create_thread(calculation_function) self.calculation_thread.threadWrapperSetUp( on_thread_start_callback=self.handle_fit_started, on_thread_end_callback=self.handle_seq_fit_finished, on_thread_exception_callback=self.handle_fit_error) self.calculation_thread.start() def handle_seq_fit_finished(self): if self.fitting_calculation_model.result is None: return fit_functions, fit_statuses, fit_chi_squareds = self.fitting_calculation_model.result for fit_function, fit_status, fit_chi_squared, row in zip( fit_functions, fit_statuses, fit_chi_squareds, self.selected_rows): parameter_values = self.model.get_fit_function_parameter_values( fit_function) self.view.fit_table.set_parameter_values_for_row( row, parameter_values) self.view.fit_table.set_fit_quality(row, fit_status, fit_chi_squared) self.view.seq_fit_button.setEnabled(True) self.view.fit_selected_button.setEnabled(True) self.view.fit_table.block_signals(False) # if no row is selected (select the last) if len(self.view.fit_table.get_selected_rows()) == 0: self.view.fit_table.set_selection_to_last_row() else: self.handle_fit_selected_in_table() def handle_updated_fit_parameter_in_table(self, index): row = index.row() workspaces = self.get_workspaces_for_row_in_fit_table(row) parameter_values = self.view.fit_table.get_fit_parameter_values_from_row( row) self.model.update_ws_fit_function_parameters(workspaces, parameter_values) def handle_fit_selected_in_table(self): rows = self.view.fit_table.get_selected_rows() fit_information = [] for i, row in enumerate(rows): workspaces = self.get_workspaces_for_row_in_fit_table(row) fit = self.context.fitting_context.find_fit_for_input_workspace_list_and_function( workspaces, self.model.function_name) fit_information += [ FitPlotInformation(input_workspaces=workspaces, fit=fit) ] self.selected_sequential_fit_notifier.notify_subscribers( fit_information) def get_workspaces_for_row_in_fit_table(self, row): runs, group_and_pairs = self.view.fit_table.get_workspace_info_from_row( row) separated_runs = runs.split(';') separated_group_and_pairs = group_and_pairs.split(';') workspace_names = self.model.get_fit_workspace_names_from_groups_and_runs( separated_runs, separated_group_and_pairs) return workspace_names
class PairingTablePresenter(object): def __init__(self, view, model): self._view = view self._model = model self._view.on_add_pair_button_clicked(self.handle_add_pair_button_checked_state) self._view.on_remove_pair_button_clicked(self.handle_remove_pair_button_clicked) self._view.on_user_changes_pair_name(self.validate_pair_name) self._view.on_user_changes_alpha(self.validate_alpha) self._view.on_guess_alpha_clicked(self.handle_guess_alpha_clicked) self._view.on_table_data_changed(self.handle_data_change) self.selected_pair_changed_notifier = GenericObservable() self._dataChangedNotifier = lambda: 0 self._on_alpha_clicked = lambda: 0 self._on_guess_alpha_requested = lambda pair_name, group1, group2: 0 # notify if Guess Alpha clicked for any table entries self.guessAlphaNotifier = PairingTablePresenter.GuessAlphaNotifier(self) def show(self): self._view.show() def on_data_changed(self, notifier): self._dataChangedNotifier = notifier def notify_data_changed(self): self._dataChangedNotifier() def disable_editing(self): self._view.disable_editing() def enable_editing(self): self._view.enable_editing() def handle_guess_alpha_clicked(self, row): table_row = self._view.get_table_contents()[row] pair_name = table_row[0] group1 = table_row[2] group2 = table_row[3] self.guessAlphaNotifier.notify_subscribers([pair_name, group1, group2]) def handle_data_change(self, row, col): table = self._view.get_table_contents() changed_item = self._view.get_table_item(row, col) changed_item_text = self._view.get_table_item_text(row, col) pair_name = self._view.get_table_item_text(row, 0) update_model = True if pair_columns[col] == 'pair_name' and not self.validate_pair_name(changed_item_text): update_model = False if pair_columns[col] == 'group_1': if changed_item_text == self._view.get_table_item_text(row, pair_columns.index('group_2')): table[row][pair_columns.index('group_2')] = self._model.pairs[row].forward_group if pair_columns[col] == 'group_2': if changed_item_text == self._view.get_table_item_text(row, pair_columns.index('group_1')): table[row][pair_columns.index('group_1')] = self._model.pairs[row].backward_group if pair_columns[col] == 'alpha': if not self.validate_alpha(changed_item_text): update_model = False else: rounded_item = '{:.3f}'.format(float(changed_item_text)) if '{:.3f}'.format( float(changed_item_text)) != '0.000' \ else '{:.3g}'.format(float(changed_item_text)) table[row][col] = rounded_item if pair_columns[col] == 'to_analyse': update_model = False self.to_analyse_data_checkbox_changed(changed_item.checkState(), row, pair_name) if update_model: self.update_model_from_view(table) if col != 1: self.update_view_from_model() self.notify_data_changed() def update_model_from_view(self, table=None): if not table: table = self._view.get_table_contents() self._model.clear_pairs() for entry in table: periods = self._model.get_periods(str(entry[2]))+self._model.get_periods(str(entry[3])) pair = MuonPair(pair_name=str(entry[0]), backward_group_name=str(entry[3]), forward_group_name=str(entry[2]), alpha=float(entry[4]), periods = periods) self._model.add_pair(pair) def update_view_from_model(self): self._view.disable_updates() self._view.clear() for pair in self._model.pairs: if isinstance(pair, MuonPair): to_analyse = True if pair.name in self._model.selected_pairs else False forward_group_periods = self._model._context.group_pair_context[pair.forward_group].periods backward_group_periods = self._model._context.group_pair_context[pair.backward_group].periods forward_period_warning = self._model.validate_periods_list(forward_group_periods) backward_period_warning = self._model.validate_periods_list(backward_group_periods) if forward_period_warning==RowValid.invalid_for_all_runs or backward_period_warning == RowValid.invalid_for_all_runs: display_period_warning = RowValid.invalid_for_all_runs elif forward_period_warning==RowValid.valid_for_some_runs or backward_period_warning == RowValid.valid_for_some_runs: display_period_warning = RowValid.valid_for_some_runs else: display_period_warning = RowValid.valid_for_all_runs color = row_colors[display_period_warning] tool_tip = row_tooltips[display_period_warning] self.add_pair_to_view(pair, to_analyse, color, tool_tip) self._view.enable_updates() def update_group_selections(self): groups = self._model.group_names + [diff.name for diff in self._model.get_diffs("group")] self._view.update_group_selections(groups) def to_analyse_data_checkbox_changed(self, state, row, pair_name): pair_added = True if state == 2 else False if pair_added: self._model.add_pair_to_analysis(pair_name) else: self._model.remove_pair_from_analysis(pair_name) pair_info = {'is_added': pair_added, 'name': pair_name} self.selected_pair_changed_notifier.notify_subscribers(pair_info) def plot_default_case(self): for row in range(self._view.num_rows()): self._view.set_to_analyse_state_quietly(row, True) pair_name = self._view.get_table_item(row, 0).text() self._model.add_pair_to_analysis(pair_name) # ------------------------------------------------------------------------------------------------------------------ # Add / Remove pairs # ------------------------------------------------------------------------------------------------------------------ def add_pair(self, pair): """Add a pair to the model and view""" if self._view.num_rows() > 19: self._view.warning_popup("Cannot add more than 20 pairs.") return self.add_pair_to_model(pair) self.update_view_from_model() def add_pair_to_model(self, pair): self._model.add_pair(pair) def add_pair_to_view(self, pair, to_analyse=False, color=row_colors[RowValid.valid_for_all_runs], tool_tip=row_tooltips[RowValid.valid_for_all_runs]): self._view.disable_updates() self.update_group_selections() assert isinstance(pair, MuonPair) entry = [str(pair.name), to_analyse, str(pair.forward_group), str(pair.backward_group), str(pair.alpha)] self._view.add_entry_to_table(entry, color, tool_tip) self._view.enable_updates() """ This is required to strip out the boolean value the clicked method of QButton emits by default. """ def handle_add_pair_button_checked_state(self): self.handle_add_pair_button_clicked() def handle_add_pair_button_clicked(self, group_1='', group_2=''): if len(self._model.group_names) == 0 or len(self._model.group_names) == 1: self._view.warning_popup("At least two groups are required to create a pair") else: new_pair_name = self._view.enter_pair_name() if new_pair_name is None: return elif new_pair_name in self._model.group_and_pair_names: self._view.warning_popup("Groups and pairs must have unique names") elif self.validate_pair_name(new_pair_name): group1 = self._model.group_names[0] if not group_1 else group_1 group2 = self._model.group_names[1] if not group_2 else group_2 periods = self._model.get_periods(group1)+self._model.get_periods(group2) pair = MuonPair(pair_name=str(new_pair_name), forward_group_name=group1, backward_group_name=group2, alpha=1.0, periods=periods) self.add_pair(pair) self.notify_data_changed() def handle_remove_pair_button_clicked(self): pair_names = self._view.get_selected_pair_names_and_indexes() if not pair_names: self.remove_last_row_in_view_and_model() else: self.remove_selected_rows_in_view_and_model(pair_names) self.notify_data_changed() def remove_selected_rows_in_view_and_model(self, pair_names): safe_to_rm = [] warnings = "" for name, index in pair_names: used_by = self._model.check_if_used_by_diff(name) if used_by: warnings+=used_by+"\n" else: safe_to_rm.append([index, name]) for index, name in reversed(safe_to_rm): self._model.remove_pair_from_analysis(name) self._view.remove_pair_by_index(index) self._model.remove_pairs_by_name([name for index, name in safe_to_rm]) if warnings: self._view.warning_popup(warnings) def remove_last_row_in_view_and_model(self): if self._view.num_rows() > 0: name = self._view.get_table_contents()[-1][0] warning = self._model.check_if_used_by_diff(name) if warning: self._view.warning_popup(warning) else: self._view.remove_last_row() self._model.remove_pair_from_analysis(name) self._model.remove_pairs_by_name([name]) # ------------------------------------------------------------------------------------------------------------------ # Table entry validation # ------------------------------------------------------------------------------------------------------------------ def _is_edited_name_duplicated(self, new_name): is_name_column_being_edited = self._view.pairing_table.currentColumn() == 0 is_name_unique = (sum( [new_name == name for name in self._model.group_and_pair_names]) == 0) return is_name_column_being_edited and not is_name_unique def validate_pair_name(self, text): if self._is_edited_name_duplicated(text): self._view.warning_popup("Groups and pairs must have unique names") return False if not re.match(valid_name_regex, text): self._view.warning_popup("Pair names should only contain digits, characters and _") return False return True def validate_alpha(self, alpha_text): if not re.match(valid_alpha_regex, alpha_text) or float(alpha_text) <= 0.0: self._view.warning_popup("Alpha must be > 0") return False return True # ------------------------------------------------------------------------------------------------------------------ # Observer / Observable # ------------------------------------------------------------------------------------------------------------------ class GuessAlphaNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, arg=["", "", ""]): Observable.notify_subscribers(self, arg)
class GroupingTablePresenter(object): def __init__(self, view, model): self._view = view self._model = model self._view.on_add_group_button_clicked( self.handle_add_group_button_clicked) self._view.on_remove_group_button_clicked( self.handle_remove_group_button_clicked) self._view.on_user_changes_group_name(self.validate_group_name) self._view.on_user_changes_detector_IDs(self.validate_detector_ids) self._view.on_table_data_changed(self.handle_data_change) self._view.on_user_changes_min_range_source( self.first_good_data_checkbox_changed) self._view.on_user_changes_max_range_source( self.from_file_checkbox_changed) self._view.on_user_changes_group_range_min_text_edit( self.handle_group_range_min_updated) self._view.on_user_changes_group_range_max_text_edit( self.handle_group_range_max_updated) self.selected_group_changed_notifier = GenericObservable() self._dataChangedNotifier = lambda: 0 def show(self): self._view.show() def on_data_changed(self, notifier): self._dataChangedNotifier = notifier def notify_data_changed(self): self._dataChangedNotifier() def _is_edited_name_duplicated(self, new_name): is_name_column_being_edited = self._view.grouping_table.currentColumn( ) == 0 is_name_unique = True if new_name in self._model.group_and_pair_names: is_name_unique = False return is_name_column_being_edited and not is_name_unique def validate_group_name(self, text): if self._is_edited_name_duplicated(text): self._view.warning_popup("Groups and pairs must have unique names") return False if not re.match(run_utils.valid_name_regex, text): self._view.warning_popup( "Group names should only contain digits, characters and _") return False return True def validate_detector_ids(self, text): try: if re.match(run_utils.run_string_regex, text) and \ self._validate_detector_ids_list(run_utils.run_string_to_list(text, False)): return True except OverflowError: pass self._view.warning_popup("Invalid detector list.") return False def _validate_detector_ids_list(self, detector_ids: list) -> bool: return detector_ids and max( detector_ids) <= self._model._data.num_detectors and min( detector_ids) > 0 def validate_periods(self, period_text): try: period_list = run_utils.run_string_to_list(period_text) except IndexError: # An IndexError thrown here implies that the input string is not a valid # number list. return RowValid.invalid_for_all_runs return self._model.validate_periods_list(period_list) def disable_editing(self): self._view.disable_editing() def enable_editing(self): self._view.enable_editing() def add_group(self, group): """Adds a group to the model and view""" try: if self._view.num_rows() >= maximum_number_of_groups: self._view.warning_popup( "Cannot add more than {} groups.".format( maximum_number_of_groups)) return self.add_group_to_model(group) if len(self._model.group_names + self._model.pair_names) == 1: self._model.add_group_to_analysis(group.name) self.update_view_from_model() self.notify_data_changed() except ValueError as error: self._view.warning_popup(error) def add_group_to_model(self, group): self._model.add_group(group) def add_group_to_view(self, group, state, color, tooltip): self._view.disable_updates() assert isinstance(group, MuonGroup) entry = [ str(group.name), run_utils.run_list_to_string(group.periods), state, run_utils.run_list_to_string(group.detectors, False), str(group.n_detectors) ] self._view.add_entry_to_table(entry, color, tooltip) self._view.enable_updates() def handle_add_group_button_clicked(self): new_group_name = self._view.enter_group_name() if new_group_name is None: return if new_group_name in self._model.group_and_pair_names: self._view.warning_popup("Groups and pairs must have unique names") elif self.validate_group_name(new_group_name): group = MuonGroup(group_name=str(new_group_name), detector_ids=[1]) self.add_group(group) def handle_remove_group_button_clicked(self): group_names = self._view.get_selected_group_names_and_indexes() if not group_names: self.remove_last_row_in_view_and_model() else: self.remove_selected_rows_in_view_and_model(group_names) self.notify_data_changed() def remove_selected_rows_in_view_and_model(self, group_names): safe_to_rm = [] warnings = "" for name, index in group_names: used_by = self._model.check_group_in_use(name) if used_by: warnings += used_by + "\n" else: safe_to_rm.append([index, name]) for index, name in reversed(safe_to_rm): self._model.remove_group_from_analysis(name) self._view.remove_group_by_index(index) self._model.remove_groups_by_name([name for _, name in safe_to_rm]) if warnings: self._view.warning_popup(warnings) def remove_last_row_in_view_and_model(self): if self._view.num_rows() > 0: name = self._view.get_table_contents()[-1][0] used_by = self._model.check_group_in_use(name) if used_by: self._view.warning_popup(used_by) else: self._view.remove_last_row() self._model.remove_group_from_analysis(name) self._model.remove_groups_by_name([name]) def handle_data_change(self, row, col): changed_item = self._view.get_table_item(row, col) group_name = self._view.get_table_item( row, inverse_group_table_columns['group_name']).text() update_model = True if col == inverse_group_table_columns[ 'group_name'] and not self.validate_group_name( changed_item.text()): update_model = False if col == inverse_group_table_columns[ 'detector_ids'] and not self.validate_detector_ids( changed_item.text()): update_model = False if col == inverse_group_table_columns['to_analyse']: update_model = False self.to_analyse_data_checkbox_changed(changed_item.checkState(), row, group_name) if col == inverse_group_table_columns[ 'periods'] and self.validate_periods( changed_item.text()) == RowValid.invalid_for_all_runs: self._view.warning_popup( "One or more of the periods specified missing from all runs") update_model = False if not update_model: # Reset the view back to model values and exit early as the changes are invalid. self.update_view_from_model() return try: self.update_model_from_view() except ValueError as error: self._view.warning_popup(error) # if the column containing the "to_analyse" flag is changed, then we don't need to update anything group related if col != inverse_group_table_columns['to_analyse']: self.update_view_from_model() self.notify_data_changed() def update_model_from_view(self): table = self._view.get_table_contents() self._model.clear_groups() for entry in table: detector_list = run_utils.run_string_to_list( str(entry[inverse_group_table_columns['detector_ids']]), False) periods = run_utils.run_string_to_list( str(entry[inverse_group_table_columns['periods']])) group = MuonGroup(group_name=str(entry[0]), detector_ids=detector_list, periods=periods) self._model.add_group(group) def update_view_from_model(self): self._view.disable_updates() self._view.clear() self._remove_groups_with_invalid_detectors() for group in self._model.groups: to_analyse = True if group.name in self._model.selected_groups else False display_period_warning = self._model.validate_periods_list( group.periods) color = row_colors[display_period_warning] tool_tip = row_tooltips[display_period_warning] self.add_group_to_view(group, to_analyse, color, tool_tip) if self._view.group_range_use_last_data.isChecked(): self._view.group_range_max.setText( str(self._model.get_last_data_from_file())) if self._view.group_range_use_first_good_data.isChecked(): self._view.group_range_min.setText( str(self._model.get_first_good_data_from_file())) self._view.enable_updates() def _remove_groups_with_invalid_detectors(self): invalid_groups = [ group.name for group in self._model.groups if not self._validate_detector_ids_list(group.detectors) ] self._model.remove_groups_by_name(invalid_groups) def to_analyse_data_checkbox_changed(self, state, row, group_name): group_added = True if state == 2 else False if group_added: self._model.add_group_to_analysis(group_name) else: self._model.remove_group_from_analysis(group_name) group_info = {'is_added': group_added, 'name': group_name} self.selected_group_changed_notifier.notify_subscribers(group_info) def plot_default_case(self): for row in range(self._view.num_rows()): self._view.set_to_analyse_state_quietly(row, True) group_name = self._view.get_table_item(row, 0).text() self._model.add_group_to_analysis(group_name) def first_good_data_checkbox_changed(self): if self._view.group_range_use_first_good_data.isChecked(): self._view.group_range_min.setText( str(self._model.get_first_good_data_from_file())) self._view.group_range_min.setEnabled(False) if 'GroupRangeMin' in self._model._context.gui_context: # Remove variable from model if value from file is to be used self._model._context.gui_context.pop('GroupRangeMin') self._model._context.gui_context.update_and_send_signal() else: self._view.group_range_min.setEnabled(True) self._model._context.gui_context.update_and_send_signal( GroupRangeMin=float(self._view.group_range_min.text())) def from_file_checkbox_changed(self): if self._view.group_range_use_last_data.isChecked(): self._view.group_range_max.setText( str(self._model.get_last_data_from_file())) self._view.group_range_max.setEnabled(False) if 'GroupRangeMax' in self._model._context.gui_context: # Remove variable from model if value from file is to be used self._model._context.gui_context.pop('GroupRangeMax') self._model._context.gui_context.update_and_send_signal() else: self._view.group_range_max.setEnabled(True) self._model._context.gui_context.update_and_send_signal( GroupRangeMax=float(self._view.group_range_max.text())) def handle_group_range_min_updated(self): range_min_new = float(self._view.group_range_min.text()) range_max_current = self._model._context.gui_context['GroupRangeMax'] if 'GroupRangeMax' in \ self._model._context.gui_context \ else self._model.get_last_data_from_file() if range_min_new < range_max_current: self._model._context.gui_context.update_and_send_signal( GroupRangeMin=range_min_new) else: self._view.group_range_min.setText( str(self._model._context.gui_context['GroupRangeMin'])) self._view.warning_popup( 'Minimum of group asymmetry range must be less than maximum') def handle_group_range_max_updated(self): range_max_new = float(self._view.group_range_max.text()) range_min_current = self._model._context.gui_context['GroupRangeMin'] if 'GroupRangeMin' in \ self._model._context.gui_context \ else self._model.get_first_good_data_from_file() if range_max_new > range_min_current: self._model._context.gui_context.update_and_send_signal( GroupRangeMax=range_max_new) else: self._view.group_range_max.setText( str(self._model._context.gui_context['GroupRangeMax'])) self._view.warning_popup( 'Maximum of group asymmetry range must be greater than minimum' )
class CorrectionsPresenter(QObject): """ The CorrectionsPresenter has a CorrectionsView and CorrectionsModel. """ def __init__(self, view: CorrectionsView, model: CorrectionsModel, context: MuonContext): """Initialize the CorrectionsPresenter. Sets up the slots and event observers.""" super(CorrectionsPresenter, self).__init__() self.view = view self.model = model self.thread_success = True self.dead_time_model = DeadTimeCorrectionsModel( model, context.data_context, context.corrections_context) self.dead_time_presenter = DeadTimeCorrectionsPresenter( self.view.dead_time_view, self.dead_time_model, self) self.background_model = BackgroundCorrectionsModel(model, context) self.background_presenter = BackgroundCorrectionsPresenter( self.view.background_view, self.background_model, self) self.initialize_model_options() self.view.set_slot_for_run_selector_changed( self.handle_run_selector_changed) self.update_view_from_model_observer = GenericObserverWithArgPassing( self.handle_ads_clear_or_remove_workspace_event) self.instrument_changed_observer = GenericObserver( self.handle_instrument_changed) self.load_observer = GenericObserver(self.handle_runs_loaded) self.group_change_observer = GenericObserver( self.handle_groups_changed) self.pre_process_and_counts_calculated_observer = GenericObserver( self.handle_pre_process_and_counts_calculated) self.enable_editing_notifier = GenericObservable() self.disable_editing_notifier = GenericObservable() self.set_tab_warning_notifier = GenericObservable() self.perform_corrections_notifier = GenericObservable() self.asymmetry_pair_and_diff_calculations_finished_notifier = GenericObservable( ) def initialize_model_options(self) -> None: """Initialise the model with the default fitting options.""" self.dead_time_presenter.initialize_model_options() self.background_presenter.initialize_model_options() def handle_ads_clear_or_remove_workspace_event(self, _: str = None) -> None: """Handle when there is a clear or remove workspace event in the ADS.""" self.dead_time_presenter.handle_ads_clear_or_remove_workspace_event() self.handle_runs_loaded() def handle_instrument_changed(self) -> None: """User changes the selected instrument.""" QMetaObject.invokeMethod(self, "_handle_instrument_changed") @Slot() def _handle_instrument_changed(self) -> None: """Handles when new run numbers are loaded from the GUI thread.""" self.dead_time_presenter.handle_instrument_changed() self.background_presenter.handle_instrument_changed() def handle_runs_loaded(self) -> None: """Handles when new run numbers are loaded. QMetaObject is required so its executed on the GUI thread.""" QMetaObject.invokeMethod(self, "_handle_runs_loaded") @Slot() def _handle_runs_loaded(self) -> None: """Handles when new run numbers are loaded from the GUI thread.""" self.view.update_run_selector_combo_box( self.model.run_number_strings()) self.model.set_current_run_string(self.view.current_run_string()) if self.model.number_of_run_strings == 0: self.view.disable_view() else: self.view.enable_view() def handle_groups_changed(self) -> None: """Handles when the selected groups have changed in the grouping tab.""" self.background_presenter.handle_groups_changed() def handle_run_selector_changed(self) -> None: """Handles when the run selector is changed.""" self.model.set_current_run_string(self.view.current_run_string()) self.dead_time_presenter.handle_run_selector_changed() self.background_presenter.handle_run_selector_changed() def handle_pre_process_and_counts_calculated(self) -> None: """Handles when MuonPreProcess and counts workspaces have been calculated.""" self.dead_time_presenter.handle_pre_process_and_counts_calculated() self.background_presenter.handle_pre_process_and_counts_calculated() def handle_thread_calculation_started(self) -> None: """Handles when a calculation on a thread has started.""" self.disable_editing_notifier.notify_subscribers() self.thread_success = True def handle_background_corrections_for_all_finished(self) -> None: """Handle when the background corrections for all has finished.""" self.enable_editing_notifier.notify_subscribers() if not self.thread_success: return self.background_presenter.handle_background_corrections_for_all_finished( ) corrected_runs_and_groups = self.thread_model_wrapper.result if corrected_runs_and_groups is not None: runs, groups = corrected_runs_and_groups self._perform_asymmetry_pairs_and_diffs_calculation(runs, groups) def handle_asymmetry_pairs_and_diffs_calc_finished(self) -> None: """Handle when the calculation of Asymmetry, Pairs and Diffs has finished finished.""" self.enable_editing_notifier.notify_subscribers() self.asymmetry_pair_and_diff_calculations_finished_notifier.notify_subscribers( ) def handle_thread_error(self, error: str) -> None: """Handle when an error occurs while doing calculations on a thread.""" self.disable_editing_notifier.notify_subscribers() self.thread_success = False self.view.warning_popup(error) def current_run_string(self) -> str: """Returns the currently selected run string.""" return self.model.current_run_string() def _perform_asymmetry_pairs_and_diffs_calculation(self, *args) -> None: """Calculate the Asymmetry workspaces, Pairs and Diffs on a thread after background corrections are complete.""" try: self.calculation_thread = self.create_calculation_thread( self._calculate_asymmetry_pairs_and_diffs, *args) self.calculation_thread.threadWrapperSetUp( self.handle_thread_calculation_started, self.handle_asymmetry_pairs_and_diffs_calc_finished, self.handle_thread_error) self.calculation_thread.start() except ValueError as error: self.view.warning_popup(error) def _calculate_asymmetry_pairs_and_diffs(self, runs: list, groups: list) -> None: """Calculates the Asymmetry workspaces, Pairs and Diffs only for the provided runs and groups.""" # Calculates the Asymmetry workspaces for the corresponding runs and groups that have just been corrected self.model.calculate_asymmetry_workspaces_for(runs, groups) # Calculates the Pair Asymmetry workspaces for pairs formed from one or more groups which have been corrected pairs = self.model.calculate_pairs_for(runs, groups) # Calculates the Diff Asymmetry workspaces formed from one or more groups/pairs which have been corrected self.model.calculate_diffs_for(runs, groups + pairs) def create_calculation_thread(self, callback, *args) -> ThreadModel: """Create a thread for calculations.""" self.thread_model_wrapper = ThreadModelWrapperWithOutput( callback, *args) return ThreadModel(self.thread_model_wrapper) def warning_popup(self, message: str) -> None: """Displays a warning message.""" self.view.warning_popup(message) def set_tab_warning(self, message: str) -> None: """Sets a warning message as the tooltip of the corrections tab.""" self.view.set_tab_warning(message)
class ModelFittingPresenter(BasicFittingPresenter): """ The ModelFittingPresenter has a ModelFittingView and ModelFittingModel and derives from BasicFittingPresenter. """ def __init__(self, view: ModelFittingView, model: ModelFittingModel): """Initialize the ModelFittingPresenter. Sets up the slots and event observers.""" super(ModelFittingPresenter, self).__init__(view, model) self.parameter_combination_thread_success = True self.update_override_tick_labels_notifier = GenericObservable() self.update_plot_x_range_notifier = GenericObservable() self.results_table_created_observer = GenericObserverWithArgPassing( self.handle_new_results_table_created) self.view.set_slot_for_results_table_changed( self.handle_results_table_changed) self.view.set_slot_for_selected_x_changed( self.handle_selected_x_changed) self.view.set_slot_for_selected_y_changed( self.handle_selected_y_changed) def handle_new_results_table_created(self, new_results_table_name: str) -> None: """Handles when a new results table is created and added to the results context.""" if new_results_table_name not in self.model.result_table_names: self.model.result_table_names = self.model.result_table_names + [ new_results_table_name ] self.view.add_results_table_name(new_results_table_name) elif self.model.current_result_table_name == new_results_table_name: self.handle_results_table_changed() def handle_results_table_changed(self) -> None: """Handles when the selected results table has changed, and discovers the possible X's and Y's.""" self.model.current_result_table_index = self.view.current_result_table_index self._create_parameter_combination_workspaces( self.handle_parameter_combinations_finished) def handle_selected_x_changed(self) -> None: """Handles when the selected X parameter is changed.""" x_parameter = self.view.x_parameter() if x_parameter == self.view.y_parameter(): self.view.set_selected_y_parameter( self.model.get_first_y_parameter_not(x_parameter)) self.update_selected_parameter_combination_workspace() def handle_selected_y_changed(self) -> None: """Handles when the selected Y parameter is changed.""" y_parameter = self.view.y_parameter() if y_parameter == self.view.x_parameter(): self.view.set_selected_x_parameter( self.model.get_first_x_parameter_not(y_parameter)) self.update_selected_parameter_combination_workspace() def handle_parameter_combinations_started(self) -> None: """Handle when the creation of matrix workspaces starts for all the different parameter combinations.""" self.disable_editing_notifier.notify_subscribers() self.parameter_combination_thread_success = True def handle_parameter_combinations_finished(self) -> None: """Handle when the creation of matrix workspaces finishes for all the different parameter combinations.""" self.enable_editing_notifier.notify_subscribers() if not self.parameter_combination_thread_success: return self.handle_parameter_combinations_created_successfully() def handle_parameter_combinations_finished_before_fit(self) -> None: """Handle when the creation of the parameter combinations finishes, and then performs a fit.""" self.handle_parameter_combinations_finished() super().handle_fit_clicked() def handle_parameter_combinations_created_successfully(self) -> None: """Handles when the parameter combination workspaces have been created successfully.""" self.view.set_datasets_in_function_browser(self.model.dataset_names) self.view.update_dataset_name_combo_box(self.model.dataset_names, emit_signal=False) # Initially, the y parameters should be updated before the x parameters. self.view.update_y_parameters(self.model.y_parameters()) # Triggers handle_selected_x_changed self.view.update_x_parameters(self.model.x_parameters(), emit_signal=True) def handle_parameter_combinations_error(self, error: str) -> None: """Handle when an error occurs while creating workspaces for the different parameter combinations.""" self.disable_editing_notifier.notify_subscribers() self.parameter_combination_thread_success = False self.view.warning_popup(error) def handle_dataset_name_changed(self) -> None: """Handle when the hidden dataset workspace combo box is changed.""" self.model.current_dataset_index = self.view.current_dataset_index self.automatically_update_function_name() self.update_fit_statuses_and_chi_squared_in_view_from_model() self.update_covariance_matrix_button() self.update_fit_function_in_view_from_model() self.update_start_and_end_x_in_view_from_model() self.update_plot_fit() self.update_plot_guess() def handle_function_structure_changed(self) -> None: """Handle when the function structure is changed.""" self.update_fit_functions_in_model_from_view() self.automatically_update_function_name() if self.model.get_active_fit_function() is None: self.clear_current_fit_function_for_undo() self.update_plot_fit() self.reset_fit_status_and_chi_squared_information() self.update_plot_guess() self.fit_function_changed_notifier.notify_subscribers() # Required to update the function browser to display the errors when first adding a function. self.view.set_current_dataset_index(self.model.current_dataset_index) def handle_fit_clicked(self) -> None: """Handle when the fit button is clicked.""" current_dataset_name = self.model.current_dataset_name if current_dataset_name is not None and not check_if_workspace_exist( current_dataset_name): self._create_parameter_combination_workspaces( self.handle_parameter_combinations_finished_before_fit) else: super().handle_fit_clicked() def update_dataset_names_in_view_and_model(self) -> None: """Updates the results tables currently displayed.""" self.model.result_table_names = self.model.get_workspace_names_to_display_from_context( ) if self.model.result_table_names != self.view.result_table_names(): # Triggers handle_results_table_changed self.view.update_result_table_names(self.model.result_table_names) def update_fit_functions_in_model_from_view(self) -> None: """Update the fit function in the model only for the currently selected dataset.""" self.model.current_single_fit_function = self.view.current_fit_function( ) def update_selected_parameter_combination_workspace(self) -> None: """Updates the selected parameter combination based on the selected X and Y parameters.""" dataset_name = self.model.parameter_combination_workspace_name( self.view.x_parameter(), self.view.y_parameter()) if dataset_name is not None: self.model.current_dataset_index = self.model.dataset_names.index( dataset_name) self.view.current_dataset_name = dataset_name def update_plot_fit(self) -> None: """Updates the fit results on the plot using the currently active fit results.""" x_tick_labels, y_tick_labels = self.model.get_override_x_and_y_tick_labels( self.view.x_parameter(), self.view.y_parameter()) self.update_override_tick_labels_notifier.notify_subscribers( [x_tick_labels, y_tick_labels]) x_lower, x_upper = self.model.x_limits_of_workspace( self.model.current_dataset_name) self.update_plot_x_range_notifier.notify_subscribers( [x_lower, x_upper]) self.selected_fit_results_changed.notify_subscribers( self.model.get_active_fit_results()) def reset_fit_status_and_chi_squared_information(self) -> None: """Reset the fit status and chi squared only for the currently selected dataset.""" self.model.reset_current_fit_status_and_chi_squared() self.update_fit_statuses_and_chi_squared_in_view_from_model() def clear_current_fit_function_for_undo(self) -> None: """Clear the cached fit function for the currently selected dataset.""" self.model.clear_undo_data_for_current_dataset_index() self.view.set_number_of_undos(self.model.number_of_undos()) def _create_parameter_combination_workspaces(self, finished_callback) -> None: """Creates a matrix workspace for each possible parameter combination to be used for fitting.""" try: self.parameter_combination_thread = self._create_parameter_combinations_thread( self.model.create_x_and_y_parameter_combination_workspaces) self.parameter_combination_thread.threadWrapperSetUp( self.handle_parameter_combinations_started, finished_callback, self.handle_parameter_combinations_error) self.parameter_combination_thread.start() except ValueError as error: self.view.warning_popup(error) def _create_parameter_combinations_thread(self, callback) -> ThreadModel: """Create a thread for fitting.""" self.parameter_combinations_creator = ThreadModelWrapperWithOutput( callback) return ThreadModel(self.parameter_combinations_creator)
class DifferenceTablePresenter(object): def __init__(self, view, model, group_or_pair): self._view = view self._model = model self._group_or_pair = group_or_pair self._view.on_add_diff_button_clicked(self.handle_add_diff_button_checked_state) self._view.on_remove_diff_button_clicked(self.handle_remove_diff_button_clicked) self._view.on_user_changes_diff_name(self.validate_diff_name) self._view.on_table_data_changed(self.handle_data_change) self.selected_diff_changed_notifier = GenericObservable() self._dataChangedNotifier = lambda: 0 if group_or_pair == 'pair': self._view.set_table_headers_pairs() else: self._view.set_table_headers_groups() def show(self): self._view.show() def on_data_changed(self, notifier): self._dataChangedNotifier = notifier def notify_data_changed(self): self._dataChangedNotifier() def disable_editing(self): self._view.disable_editing() def enable_editing(self): self._view.enable_editing() def handle_data_change(self, row, col): table = self._view.get_table_contents() changed_item = self._view.get_table_item(row, col) changed_item_text = self._view.get_table_item_text(row, col) diff_name = self._view.get_table_item_text(row, 0) update_model = True if diff_columns[col] == 'diff_name' and not self.validate_diff_name(changed_item_text): update_model = False if diff_columns[col] == 'group_1': if changed_item_text == self._view.get_table_item_text(row, diff_columns.index('group_2')): table[row][diff_columns.index('group_2')] = self._model.get_diffs(self._group_or_pair)[row].positive if diff_columns[col] == 'group_2': if changed_item_text == self._view.get_table_item_text(row, diff_columns.index('group_1')): table[row][diff_columns.index('group_1')] = self._model.get_diffs(self._group_or_pair)[row].negative if diff_columns[col] == 'to_analyse': update_model = False self.to_analyse_data_checkbox_changed(changed_item.checkState(), row, diff_name) if update_model: self.update_model_from_view(table) if col != 1: self.update_view_from_model() self.notify_data_changed() def update_model_from_view(self, table=None): if not table: table = self._view.get_table_contents() self._model.clear_diffs(self._group_or_pair) for entry in table: periods = self._model._context.group_pair_context[entry[2]].periods + self._model._context.group_pair_context[entry[3]].periods diff = MuonDiff(diff_name=str(entry[0]), positive=str(entry[2]), negative=str(entry[3]), group_or_pair = self._group_or_pair, periods=periods) self._model.add_diff(diff) def update_view_from_model(self): self._view.disable_updates() self._view.clear() for diff in self._model.diffs: if isinstance(diff, MuonDiff) and diff.group_or_pair == self._group_or_pair: to_analyse = True if diff.name in self._model.selected_diffs else False positive_periods = self._model._context.group_pair_context[diff.positive].periods negative_periods = self._model._context.group_pair_context[diff.negative].periods forward_period_warning = self._model.validate_periods_list(positive_periods) backward_period_warning = self._model.validate_periods_list(negative_periods) if forward_period_warning==RowValid.invalid_for_all_runs or backward_period_warning == RowValid.invalid_for_all_runs: display_period_warning = RowValid.invalid_for_all_runs elif forward_period_warning==RowValid.valid_for_some_runs or backward_period_warning == RowValid.valid_for_some_runs: display_period_warning = RowValid.valid_for_some_runs else: display_period_warning = RowValid.valid_for_all_runs color = row_colors[display_period_warning] tool_tip = row_tooltips[display_period_warning] self.add_diff_to_view(diff, to_analyse, color, tool_tip) self._view.enable_updates() def update_group_selections(self): groups = self._model.get_names(self._group_or_pair) self._view.update_group_selections(groups) def to_analyse_data_checkbox_changed(self, state, row, diff_name): diff_added = True if state == 2 else False if diff_added: self._model.add_diff_to_analysis(diff_name) else: self._model.remove_diff_from_analysis(diff_name) diff_info = {'is_added': diff_added, 'name': diff_name} self.selected_diff_changed_notifier.notify_subscribers(diff_info) def plot_default_case(self): for row in range(self._view.num_rows()): self._view.set_to_analyse_state_quietly(row, True) diff_name = self._view.get_table_item(row, 0).text() self._model.add_diff_to_analysis(diff_name) # ------------------------------------------------------------------------------------------------------------------ # Add / Remove diffs # ------------------------------------------------------------------------------------------------------------------ def add_diff(self, diff): """Add a diff to the model and view""" if self._view.num_rows() > 19: self._view.warning_popup("Cannot add more than 20 diffs.") return self.add_diff_to_model(diff) self.update_view_from_model() def add_diff_to_model(self, diff): self._model.add_diff(diff) def add_diff_to_view(self, diff, to_analyse=False, color=row_colors[RowValid.valid_for_all_runs], tool_tip=row_tooltips[RowValid.valid_for_all_runs]): self._view.disable_updates() self.update_group_selections() assert isinstance(diff, MuonDiff) entry = [str(diff.name), to_analyse, str(diff.positive), str(diff.negative)] self._view.add_entry_to_table(entry, color, tool_tip) self._view.enable_updates() """ This is required to strip out the boolean value the clicked method of QButton emits by default. """ def handle_add_diff_button_checked_state(self): self.handle_add_diff_button_clicked() def handle_add_diff_button_clicked(self, group_1='', group_2=''): if len(self._model.get_names(self._group_or_pair)) == 0 or len(self._model.get_names(self._group_or_pair)) == 1: self._view.warning_popup("At least two groups/pairs are required to create a diff") else: new_diff_name = self._view.enter_diff_name() if new_diff_name is None: return elif new_diff_name in self._model.group_and_pair_names: self._view.warning_popup("Groups and diffs must have unique names") elif self.validate_diff_name(new_diff_name): group1 = self._model.get_names(self._group_or_pair)[0] if not group_1 else group_1 group2 = self._model.get_names(self._group_or_pair)[1] if not group_2 else group_2 periods = self._model._context.group_pair_context[group1].periods + self._model._context.group_pair_context[group2].periods diff = MuonDiff(diff_name=str(new_diff_name), positive=group1, negative=group2, group_or_pair = self._group_or_pair, periods=periods) self.add_diff(diff) self.notify_data_changed() def handle_remove_diff_button_clicked(self): diff_names = self._view.get_selected_diff_names() if not diff_names: self.remove_last_row_in_view_and_model() else: self._view.remove_selected_diffs() for diff_name in diff_names: self._model.remove_diff_from_analysis(diff_name) self._model.remove_diffs_by_name(diff_names) self.notify_data_changed() def remove_last_row_in_view_and_model(self): if self._view.num_rows() > 0: name = self._view.get_table_contents()[-1][0] self._view.remove_last_row() self._model.remove_diff_from_analysis(name) self._model.remove_diffs_by_name([name]) # ------------------------------------------------------------------------------------------------------------------ # Table entry validation # ------------------------------------------------------------------------------------------------------------------ def _is_edited_name_duplicated(self, new_name): is_name_column_being_edited = self._view.diff_table.currentColumn() == 0 is_name_unique = (sum( [new_name == name for name in self._model.group_and_pair_names]) == 0) return is_name_column_being_edited and not is_name_unique def validate_diff_name(self, text): if self._is_edited_name_duplicated(text): self._view.warning_popup("Groups and diffs must have unique names") return False if not re.match(valid_name_regex, text): self._view.warning_popup("diff names should only contain digits, characters and _") return False return True
class MaxEntPresenter(object): """ This class links the MaxEnt model to the GUI """ def __init__(self, view, load): self.view = view self.load = load self.thread = None # set data self.getWorkspaceNames() # connect self.view.maxEntButtonSignal.connect(self.handleMaxEntButton) self.view.cancelSignal.connect(self.cancel) self.phase_table_observer = GenericObserver( self.update_phase_table_options) self.calculation_finished_notifier = GenericObservable() self.calculation_started_notifier = GenericObservable() @property def widget(self): return self.view def runChanged(self): self.getWorkspaceNames() def clear(self): self.view.addItems([]) self.view.update_phase_table_combo([]) # functions def getWorkspaceNames(self): final_options = self.load.getGroupedWorkspaceNames() self.view.addItems(final_options) start = int( math.ceil( math.log(self.load.data_context.num_points) / math.log(2.0))) values = [str(2**k) for k in range(start, 21)] self.view.addNPoints(values) def cancel(self): if self.maxent_alg is not None: self.maxent_alg.cancel() # turn on button def activate(self): self.view.activateCalculateButton() # turn off button def deactivate(self): self.view.deactivateCalculateButton() def createThread(self): self.maxent_alg = mantid.AlgorithmManager.create("MuonMaxent") calculation_function = functools.partial(self.calculate_maxent, self.maxent_alg) self._maxent_calculation_model = ThreadModelWrapper( calculation_function) return thread_model.ThreadModel(self._maxent_calculation_model) # constructs the inputs for the MaxEnt algorithms # then executes them (see maxent_model to see the order # of execution def handleMaxEntButton(self): # put this on its own thread so not to freeze Mantid self.thread = self.createThread() self.thread.threadWrapperSetUp(self.deactivate, self.handleFinished, self.handle_error) self.calculation_started_notifier.notify_subscribers() self.thread.start() # kills the thread at end of execution def handleFinished(self): self.activate() self.calculation_finished_notifier.notify_subscribers() def handle_error(self, error): self.activate() self.view.warning_popup(error) def calculate_maxent(self, alg): maxent_parameters = self.get_parameters_for_maxent_calculation() base_name = get_maxent_workspace_name( maxent_parameters['InputWorkspace']) maxent_workspace = run_MuonMaxent(maxent_parameters, alg, base_name) self.add_maxent_workspace_to_ADS(maxent_parameters['InputWorkspace'], maxent_workspace, alg) def get_parameters_for_maxent_calculation(self): inputs = {} inputs['InputWorkspace'] = self.view.input_workspace run = [float(re.search('[0-9]+', inputs['InputWorkspace']).group())] if self.view.phase_table != 'Construct': inputs['InputPhaseTable'] = self.view.phase_table if self.load.dead_time_table(run): inputs['InputDeadTimeTable'] = self.load.dead_time_table(run) inputs['FirstGoodTime'] = self.load.first_good_data(run) inputs['LastGoodTime'] = self.load.last_good_data(run) inputs['Npts'] = self.view.num_points inputs['InnerIterations'] = self.view.inner_iterations inputs['OuterIterations'] = self.view.outer_iterations inputs['DoublePulse'] = self.view.double_pulse inputs['Factor'] = self.view.lagrange_multiplier inputs['MaxField'] = self.view.maximum_field inputs['DefaultLevel'] = self.view.maximum_entropy_constant inputs['FitDeadTime'] = self.view.fit_dead_times return inputs def update_phase_table_options(self): phase_table_list = self.load.phase_context.get_phase_table_list( self.load.data_context.instrument) phase_table_list.insert(0, 'Construct') self.view.update_phase_table_combo(phase_table_list) def add_maxent_workspace_to_ADS(self, input_workspace, maxent_workspace, alg): run = re.search('[0-9]+', input_workspace).group() base_name = get_maxent_workspace_name(input_workspace) directory = get_maxent_workspace_group_name( base_name, self.load.data_context.instrument, self.load.workspace_suffix) muon_workspace_wrapper = MuonWorkspaceWrapper(directory + base_name) muon_workspace_wrapper.show() maxent_output_options = self.get_maxent_output_options() self.load._frequency_context.add_maxEnt(run, maxent_workspace) self.add_optional_outputs_to_ADS(alg, maxent_output_options, base_name, directory) def get_maxent_output_options(self): output_options = {} output_options['OutputPhaseTable'] = self.view.output_phase_table output_options['OutputDeadTimeTable'] = self.view.output_dead_times output_options[ 'ReconstructedSpectra'] = self.view.output_reconstructed_spectra output_options[ 'PhaseConvergenceTable'] = self.view.output_phase_convergence return output_options def add_optional_outputs_to_ADS(self, alg, output_options, base_name, directory): for key in output_options: if output_options[key]: output = alg.getProperty(key).valueAsStr self.load.ads_observer.observeRename(False) wrapped_workspace = MuonWorkspaceWrapper(output) wrapped_workspace.show(directory + base_name + optional_output_suffixes[key]) self.load.ads_observer.observeRename(True) def update_view_from_model(self): self.getWorkspaceNames()
class FittingDataPresenter(object): def __init__(self, model, view): self.model = model self.view = view self.worker = None self.iplot = [] self.row_numbers = TwoWayRowDict( ) # {ws_name: table_row} and {table_row: ws_name} self.plotted = set() # List of plotted workspace names # Connect view signals to local methods self.view.set_on_load_clicked(self.on_load_clicked) self.view.set_enable_load_button_connection(self._enable_load_button) self.view.set_enable_inspect_bg_button_connection( self._enable_inspect_bg_button) self.view.set_on_remove_selected_clicked( self._remove_selected_tracked_workspaces) self.view.set_on_remove_all_clicked( self._remove_all_tracked_workspaces) self.view.set_on_plotBG_clicked(self._plotBG) self.view.set_on_seq_fit_clicked(self._start_seq_fit) self.view.set_on_serial_fit_clicked(self._start_serial_fit) self.view.set_on_table_cell_changed(self._handle_table_cell_changed) self.view.set_on_bank_changed(self._update_file_filter) self.view.set_on_xunit_changed(self._update_file_filter) self.view.set_table_selection_changed(self._handle_selection_changed) # Observable Setup self.plot_added_notifier = GenericObservable() self.plot_removed_notifier = GenericObservable() self.all_plots_removed_notifier = GenericObservable() self.fit_all_started_notifier = GenericObservable() # Observers self.fit_observer = GenericObserverWithArgPassing(self.fit_completed) # self.fit_enabled_observer = GenericObserverWithArgPassing( self.set_fit_enabled) self.fit_all_done_observer = GenericObserverWithArgPassing( self.fit_completed) self.focus_run_observer = GenericObserverWithArgPassing( self.view.set_default_files) def set_fit_enabled(self, fit_enabled): self.view.set_fit_buttons_enabled(fit_enabled) def fit_completed(self, fit_props): self.model.update_fit(fit_props) def _start_seq_fit(self): ws_list = self.model.get_ws_sorted_by_primary_log() self.fit_all_started_notifier.notify_subscribers(ws_list, do_sequential=True) def _start_serial_fit(self): ws_list = self.model.get_ws_list() self.fit_all_started_notifier.notify_subscribers(ws_list, do_sequential=False) def _update_file_filter(self, bank, xunit): self.view.update_file_filter(bank, xunit) def on_load_clicked(self): if self._validate(): filenames = self._get_filenames() self._start_load_worker(filenames) def remove_workspace(self, ws_name): if ws_name in self.get_loaded_workspaces(): removed = self.get_loaded_workspaces().pop(ws_name) self.plot_removed_notifier.notify_subscribers(removed) self.plotted.discard(ws_name) self.model.remove_log_rows([self.row_numbers[ws_name]]) self.model.update_log_workspace_group() self._repopulate_table() elif ws_name in self.model.get_log_workspaces_name(): self.model.update_log_workspace_group() def rename_workspace(self, old_name, new_name): if old_name in self.get_loaded_workspaces(): self.model.update_workspace_name(old_name, new_name) if old_name in self.plotted: self.plotted.remove(old_name) self.plotted.add(new_name) self._repopulate_table() self.model.update_log_workspace_group() # so matches new table def clear_workspaces(self): self.get_loaded_workspaces().clear() self.get_bgsub_workspaces().clear() self.get_bg_params().clear() self.model.set_log_workspaces_none() self.plotted.clear() self.row_numbers.clear() self._repopulate_table() def replace_workspace(self, name, workspace): if name in self.get_loaded_workspaces(): self.get_loaded_workspaces()[name] = workspace if name in self.plotted: self.all_plots_removed_notifier.notify_subscribers() self._repopulate_table() def get_loaded_workspaces(self): return self.model.get_loaded_workspaces() def get_bgsub_workspaces(self): return self.model.get_bgsub_workspaces() def get_bg_params(self): return self.model.get_bg_params() def restore_table( self ): # used when the interface is being restored from a save or crash self._repopulate_table() def _start_load_worker(self, filenames): """ Load one to many files into mantid that are tracked by the interface. :param filenames: Comma separated list of filenames to load """ self.worker = AsyncTask( self.model.load_files, (filenames, ), error_cb=self._on_worker_error, finished_cb=self._emit_enable_load_button_signal, success_cb=self._on_worker_success) self.worker.start() def _on_worker_error(self, _): logger.error("Error occurred when loading files.") self._emit_enable_load_button_signal() def _on_worker_success(self, _): wsnames = self.model.get_last_added() if self.view.get_add_to_plot(): self.plotted.update(wsnames) self._repopulate_table() # subtract background - has to be done post repopulation, can't change default in _add_row_to_table [ self.view.set_item_checkstate(self.row_numbers[wsname], 3, True) for wsname in wsnames ] def _repopulate_table(self): """ Populate the table with the information from the loaded workspaces. Will also handle any workspaces that need to be plotted. """ self._remove_all_table_rows() self.row_numbers.clear() self.all_plots_removed_notifier.notify_subscribers() workspaces = self.get_loaded_workspaces() for i, name in enumerate(workspaces): try: run_no = self.model.get_sample_log_from_ws(name, "run_number") bank = self.model.get_sample_log_from_ws(name, "bankid") if bank == 0: bank = "cropped" checked = name in self.plotted or name + "_bgsub" in self.plotted if name in self.model.get_bg_params(): self._add_row_to_table(name, i, run_no, bank, checked, *self.model.get_bg_params()[name]) else: self._add_row_to_table(name, i, run_no, bank, checked) except RuntimeError: self._add_row_to_table(name, i) self._handle_table_cell_changed(i, 2) def _remove_selected_tracked_workspaces(self): row_numbers = self._remove_selected_table_rows() self.model.remove_log_rows(row_numbers) for row_no in row_numbers: ws_name = self.row_numbers.pop(row_no) removed = self.get_loaded_workspaces().pop(ws_name) self.plot_removed_notifier.notify_subscribers(removed) self.plotted.discard(ws_name) self._repopulate_table() def _remove_all_tracked_workspaces(self): self.clear_workspaces() self.model.clear_logs() self._remove_all_table_rows() def _plotBG(self): # make external figure row_numbers = self.view.get_selected_rows() for row in row_numbers: ws_name = self.row_numbers[row] self.model.plot_background_figure(ws_name) def _handle_table_cell_changed(self, row, col): if row in self.row_numbers: ws_name = self.row_numbers[row] is_plotted = self.view.get_item_checked(row, 2) is_sub = self.view.get_item_checked(row, 3) if col == 2: # Plot check box if is_sub: ws = self.model.get_bgsub_workspaces()[ws_name] ws_name += "_bgsub" else: ws = self.model.get_loaded_workspaces()[ws_name] if self.view.get_item_checked(row, col): # Plot Box is checked self.plot_added_notifier.notify_subscribers(ws) self.plotted.add(ws_name) else: # Plot box is unchecked self.plot_removed_notifier.notify_subscribers(ws) self.plotted.discard(ws_name) elif col == 3: # subtract bg col self.model.update_bgsub_status(ws_name, is_sub) if is_sub or is_plotted: # this ensures the sub ws isn't made on load bg_params = self.view.read_bg_params_from_table(row) self.model.create_or_update_bgsub_ws(ws_name, bg_params) self._update_plotted_ws_with_sub_state(ws_name, is_sub) elif col > 3: if is_sub: # bg params changed - revaluate background bg_params = self.view.read_bg_params_from_table(row) self.model.create_or_update_bgsub_ws(ws_name, bg_params) def _update_plotted_ws_with_sub_state(self, ws_name, is_sub): ws = self.model.get_loaded_workspaces()[ws_name] ws_bgsub = self.model.get_bgsub_workspaces()[ws_name] if ws_name in self.plotted and is_sub: self.plot_removed_notifier.notify_subscribers(ws) self.plotted.discard(ws_name) self.plot_added_notifier.notify_subscribers(ws_bgsub) self.plotted.add(ws_name + "_bgsub") elif ws_name + "_bgsub" in self.plotted and not is_sub: self.plot_removed_notifier.notify_subscribers(ws_bgsub) self.plotted.discard(ws_name + "_bgsub") self.plot_added_notifier.notify_subscribers(ws) self.plotted.add(ws_name) def _handle_selection_changed(self): enable = True if not self.view.get_selected_rows(): enable = False self._enable_inspect_bg_button(enable) def _enable_load_button(self, enabled): self.view.set_load_button_enabled(enabled) def _emit_enable_load_button_signal(self): self.view.sig_enable_load_button.emit(True) def _enable_inspect_bg_button(self, enabled): self.view.set_inspect_bg_button_enabled(enabled) def _get_filenames(self): return self.view.get_filenames_to_load() def _is_searching(self): return self.view.is_searching() def _files_are_valid(self): return self.view.get_filenames_valid() def _validate(self): if self._is_searching(): create_error_message( self.view, "Mantid is searching for files. Please wait.") return False elif not self._files_are_valid(): create_error_message(self.view, "Entered files are not valid.") return False return True def _add_row_to_table(self, ws_name, row, run_no=None, bank=None, checked=False, bgsub=False, niter=100, xwindow=None, SG=True): words = ws_name.split("_") # find xwindow from ws xunit if not specified if not xwindow: ws = self.model.get_loaded_workspaces()[ws_name] if ws.getAxis(0).getUnit().unitID() == "TOF": xwindow = 1000 else: xwindow = 0.05 if run_no is not None and bank is not None: self.view.add_table_row(run_no, bank, checked, bgsub, niter, xwindow, SG) elif len(words) == 4 and words[2] == "bank": logger.notice( "No sample logs present, determining information from workspace name." ) self.view.add_table_row(words[1], words[3], checked, bgsub, niter, xwindow, SG) else: logger.warning( "The workspace '{}' was not in the correct naming format. Files should be named in the following way: " "INSTRUMENT_RUNNUMBER_bank_BANK. Using workspace name as identifier." .format(ws_name)) self.view.add_table_row(ws_name, "N/A", checked, bgsub, niter, xwindow, SG) self.row_numbers[ws_name] = row def _remove_table_row(self, row_no): self.view.remove_table_row(row_no) def _remove_selected_table_rows(self): return self.view.remove_selected() def _remove_all_table_rows(self): self.view.remove_all()
class FittingDataPresenter(object): def __init__(self, model, view): self.model = model self.view = view self.worker = None self.row_numbers = TwoWayRowDict( ) # {ws_name: table_row} and {table_row: ws_name} self.plotted = set() # List of plotted workspace names # Connect view signals to local methods self.view.set_on_load_clicked(self.on_load_clicked) self.view.set_enable_load_button_connection(self._enable_load_button) self.view.set_enable_inspect_bg_button_connection( self._enable_inspect_bg_button) self.view.set_on_remove_selected_clicked( self._remove_selected_tracked_workspaces) self.view.set_on_remove_all_clicked( self._remove_all_tracked_workspaces) self.view.set_on_plotBG_clicked(self._plotBG) self.view.set_on_table_cell_changed(self._handle_table_cell_changed) self.view.set_on_xunit_changed(self._log_xunit_change) self.view.set_table_selection_changed(self._handle_selection_changed) # Observable Setup self.plot_added_notifier = GenericObservable() self.plot_removed_notifier = GenericObservable() self.all_plots_removed_notifier = GenericObservable() def _log_xunit_change(self, xunit): logger.notice( "Subsequent files will be loaded with the x-axis unit:\t{}".format( xunit)) def on_load_clicked(self, xunit): if self._validate(): filenames = self._get_filenames() self._start_load_worker(filenames, xunit) def remove_workspace(self, ws_name): if ws_name in self.get_loaded_workspaces(): removed = self.get_loaded_workspaces().pop(ws_name) self.plot_removed_notifier.notify_subscribers(removed) self.plotted.discard(ws_name) self._repopulate_table() self.model.repopulate_logs() # so matches new table elif ws_name in self.model.get_log_workspaces_name(): logger.warning( 'Deleting the log workspace may cause unexpected errors.') def rename_workspace(self, old_name, new_name): if old_name in self.get_loaded_workspaces(): self.model.update_workspace_name(old_name, new_name) if old_name in self.plotted: self.plotted.remove(old_name) self.plotted.add(new_name) self._repopulate_table() self.model.repopulate_logs() # so matches new table def clear_workspaces(self): self.get_loaded_workspaces().clear() self.plotted.clear() self.row_numbers.clear() self._repopulate_table() def replace_workspace(self, name, workspace): if name in self.get_loaded_workspaces(): self.get_loaded_workspaces()[name] = workspace if name in self.plotted: self.all_plots_removed_notifier.notify_subscribers() self._repopulate_table() def get_loaded_workspaces(self): return self.model.get_loaded_workspaces() def _start_load_worker(self, filenames, xunit): """ Load one to many files into mantid that are tracked by the interface. :param filenames: Comma separated list of filenames to load """ self.worker = AsyncTask( self.model.load_files, (filenames, xunit), error_cb=self._on_worker_error, finished_cb=self._emit_enable_load_button_signal, success_cb=self._on_worker_success) self.worker.start() def _on_worker_error(self, _): logger.error("Error occurred when loading files.") self._emit_enable_load_button_signal() def _on_worker_success(self, _): if self.view.get_add_to_plot(): self.plotted.update(self.model.get_last_added()) self._repopulate_table() def _repopulate_table(self): """ Populate the table with the information from the loaded workspaces. Will also handle any workspaces that need to be plotted. """ self._remove_all_table_rows() self.row_numbers.clear() self.all_plots_removed_notifier.notify_subscribers() workspaces = self.get_loaded_workspaces() for i, name in enumerate(workspaces): try: run_no = self.model.get_sample_log_from_ws(name, "run_number") bank = self.model.get_sample_log_from_ws(name, "bankid") if bank == 0: bank = "cropped" checked = name in self.plotted if name in self.model.get_bg_params(): self._add_row_to_table(name, i, run_no, bank, checked, *self.model.get_bg_params()[name]) else: self._add_row_to_table(name, i, run_no, bank, checked) except RuntimeError: self._add_row_to_table(name, i) self._handle_table_cell_changed(i, 2) def _remove_selected_tracked_workspaces(self): row_numbers = self._remove_selected_table_rows() self.model.remove_log_rows(row_numbers) for row_no in row_numbers: ws_name = self.row_numbers.pop(row_no) removed = self.get_loaded_workspaces().pop(ws_name) self.plot_removed_notifier.notify_subscribers(removed) self.plotted.discard(ws_name) self._repopulate_table() self.model.repopulate_logs() def _remove_all_tracked_workspaces(self): self.clear_workspaces() self.model.clear_logs() self._remove_all_table_rows() def _plotBG(self): # make external figure row_numbers = self.view.get_selected_rows() for row in row_numbers: if self.view.get_item_checked(row, 3): # background has been subtracted from workspace ws_name = self.row_numbers[row] self.model.plot_background_figure(ws_name) def _handle_table_cell_changed(self, row, col): if row in self.row_numbers: ws_name = self.row_numbers[row] if col == 2: # Plot check box ws = self.model.get_loaded_workspaces()[ws_name] if self.view.get_item_checked(row, col): # Plot Box is checked self.plot_added_notifier.notify_subscribers(ws) self.plotted.add(ws_name) else: # Plot box is unchecked self.plot_removed_notifier.notify_subscribers(ws) self.plotted.discard(ws_name) elif col == 3: # subtract bg if self.view.get_item_checked(row, col): # subtract bg box checked bg_params = self.view.read_bg_params_from_table(row) self.model.do_background_subtraction(ws_name, bg_params) elif self.model.get_background_workspaces()[ws_name]: # box unchecked and bg exists: self.model.undo_background_subtraction(ws_name) elif col > 3: if self.view.get_item_checked(row, 3): # bg params changed - revaluate background bg_params = self.view.read_bg_params_from_table(row) self.model.do_background_subtraction(ws_name, bg_params) def _handle_selection_changed(self): rows = self.view.get_selected_rows() enabled = False for row in rows: if self.view.get_item_checked(row, 3): enabled = True self._enable_inspect_bg_button(enabled) def _enable_load_button(self, enabled): self.view.set_load_button_enabled(enabled) def _emit_enable_load_button_signal(self): self.view.sig_enable_load_button.emit(True) def _enable_inspect_bg_button(self, enabled): self.view.set_inspect_bg_button_enabled(enabled) def _get_filenames(self): return self.view.get_filenames_to_load() def _is_searching(self): return self.view.is_searching() def _files_are_valid(self): return self.view.get_filenames_valid() def _validate(self): if self._is_searching(): create_error_message( self.view, "Mantid is searching for files. Please wait.") return False elif not self._files_are_valid(): create_error_message(self.view, "Entered files are not valid.") return False return True def _add_row_to_table(self, ws_name, row, run_no=None, bank=None, checked=False, bgsub=False, niter=100, xwindow=None, SG=True): words = ws_name.split("_") # find xwindow from ws xunit if not specified if not xwindow: ws = self.model.get_loaded_workspaces()[ws_name] if ws.getAxis(0).getUnit().unitID() == "TOF": xwindow = 1000 else: xwindow = 0.05 if run_no is not None and bank is not None: self.view.add_table_row(run_no, bank, checked, bgsub, niter, xwindow, SG) self.row_numbers[ws_name] = row elif len(words) == 4 and words[2] == "bank": logger.notice( "No sample logs present, determining information from workspace name." ) self.view.add_table_row(words[1], words[3], checked, bgsub, niter, xwindow, SG) self.row_numbers[ws_name] = row else: logger.warning( "The workspace '{}' was not in the correct naming format. Files should be named in the following way: " "INSTRUMENT_RUNNUMBER_bank_BANK. Using workspace name as identifier." .format(ws_name)) self.view.add_table_row(ws_name, "N/A", checked, bgsub, niter, xwindow, SG) self.row_numbers[ws_name] = row def _remove_table_row(self, row_no): self.view.remove_table_row(row_no) def _remove_selected_table_rows(self): return self.view.remove_selected() def _remove_all_table_rows(self): self.view.remove_all()
class EAGroupingTabPresenter(object): """ The grouping tab presenter is responsible for synchronizing the group table. """ @staticmethod def string_to_list(text): return run_string_to_list(text) def __init__(self, view, model, grouping_table_widget=None): self._view = view self._model = model self.grouping_table_widget = grouping_table_widget self._view.set_description_text(self.text_for_description()) # monitors for loaded data changing self.loadObserver = GenericObserver(self.handle_new_data_loaded) self.instrumentObserver = GenericObserver(self.on_clear_requested) # notifiers self.groupingNotifier = GenericObservable() self.enable_editing_notifier = GenericObservable() self.disable_editing_notifier = GenericObservable() self.calculation_finished_notifier = GenericObservable() self.message_observer = GenericObserverWithArgPassing( self._view.display_warning_box) self.gui_variables_observer = GenericObserver( self.handle_update_all_clicked) self.enable_observer = GenericObserver(self.enable_editing) self.disable_observer = GenericObserver(self.disable_editing) self.disable_tab_observer = GenericObserver( self.disable_editing_without_notifying_subscribers) self.enable_tab_observer = GenericObserver( self.enable_editing_without_notifying_subscribers) self.update_view_from_model_observer = GenericObserver( self.update_view_from_model) def update_view_from_model(self): self.grouping_table_widget.update_view_from_model() def show(self): self._view.show() def text_for_description(self): """ Generate the text for the description edit at the top of the widget. """ text = "\u03BCx: exp2k : file type .dat" return text def update_description_text(self, description_text=''): if not description_text: description_text = self.text_for_description() self._view.set_description_text(description_text) def disable_editing(self): self.grouping_table_widget.disable_editing() self.disable_editing_notifier.notify_subscribers() def enable_editing(self): self.grouping_table_widget.enable_editing() self.enable_editing_notifier.notify_subscribers() def disable_editing_without_notifying_subscribers(self): self.grouping_table_widget.disable_editing() def enable_editing_without_notifying_subscribers(self): self.grouping_table_widget.enable_editing() def error_callback(self, error_message): self.enable_editing() self._view.display_warning_box(error_message) def handle_update_finished(self): self.enable_editing() self.groupingNotifier.notify_subscribers() self.calculation_finished_notifier.notify_subscribers() def on_clear_requested(self): self._model.clear() self.grouping_table_widget.update_view_from_model() self.update_description_text() def handle_new_data_loaded(self): if self._model.is_data_loaded(): self.update_view_from_model() self.update_description_text() self.plot_default_groups() else: self.on_clear_requested() def plot_default_groups(self): # if we have no groups selected, generate a default plot if len(self._model.selected_groups) == 0: self.grouping_table_widget.plot_default_case() def handle_update_all_clicked(self): self.update_thread = self.create_update_thread() self.update_thread.threadWrapperSetUp(self.disable_editing, self.handle_update_finished, self.error_callback) self.update_thread.start() def create_update_thread(self): self._update_model = ThreadModelWrapper(self.calculate_all_data) return thread_model.ThreadModel(self._update_model) def calculate_all_data(self): self._model.show_all_groups()
class GeneralFittingPresenter(BasicFittingPresenter): """ The GeneralFittingPresenter has a GeneralFittingView and GeneralFittingModel and derives from BasicFittingPresenter. """ def __init__(self, view: GeneralFittingView, model: GeneralFittingModel): """Initialize the GeneralFittingPresenter. Sets up the slots and event observers.""" super(GeneralFittingPresenter, self).__init__(view, model) self.fitting_mode_changed_notifier = GenericObservable() self.simultaneous_fit_by_specifier_changed = GenericObservable() self.model.context.gui_context.add_non_calc_subscriber(self.double_pulse_observer) self.view.set_slot_for_fitting_mode_changed(self.handle_fitting_mode_changed) self.view.set_slot_for_simultaneous_fit_by_changed(self.handle_simultaneous_fit_by_changed) self.view.set_slot_for_simultaneous_fit_by_specifier_changed(self.handle_simultaneous_fit_by_specifier_changed) self.update_and_reset_all_data() def initialize_model_options(self) -> None: """Returns the fitting options to be used when initializing the model.""" super().initialize_model_options() self.model.simultaneous_fit_by = self.view.simultaneous_fit_by self.model.simultaneous_fit_by_specifier = self.view.simultaneous_fit_by_specifier self.model.global_parameters = self.view.global_parameters def handle_fitting_mode_changed(self) -> None: """Handle when the fitting mode is changed to or from simultaneous fitting.""" self.model.simultaneous_fitting_mode = self.view.simultaneous_fitting_mode self.switch_fitting_mode_in_view() self.update_fit_functions_in_model_from_view() # Triggers handle_dataset_name_changed self.update_dataset_names_in_view_and_model() self.automatically_update_function_name() self.reset_fit_status_and_chi_squared_information() self.clear_undo_data() self.fitting_mode_changed_notifier.notify_subscribers() self.fit_function_changed_notifier.notify_subscribers() def handle_simultaneous_fit_by_changed(self) -> None: """Handle when the simultaneous fit by combo box is changed.""" self.model.simultaneous_fit_by = self.view.simultaneous_fit_by # Triggers handle_simultaneous_fit_by_specifier_changed self.update_simultaneous_fit_by_specifiers_in_view() def handle_simultaneous_fit_by_specifier_changed(self) -> None: """Handle when the simultaneous fit by specifier combo box is changed.""" self.model.simultaneous_fit_by_specifier = self.view.simultaneous_fit_by_specifier # Triggers handle_dataset_name_changed self.update_dataset_names_in_view_and_model() self.reset_fit_status_and_chi_squared_information() self.clear_undo_data() self.simultaneous_fit_by_specifier_changed.notify_subscribers() def switch_fitting_mode_in_view(self) -> None: """Switches the fitting mode by updating the relevant labels and checkboxes in the view.""" if self.model.simultaneous_fitting_mode: self.view.switch_to_simultaneous() else: self.view.switch_to_single() def update_and_reset_all_data(self) -> None: """Updates the various data displayed in the fitting widget. Resets and clears previous fit information.""" # Triggers handle_simultaneous_fit_by_specifier_changed self.update_simultaneous_fit_by_specifiers_in_view() def update_fit_statuses_and_chi_squared_in_model(self, fit_status: str, chi_squared: float) -> None: """Updates the fit status and chi squared stored in the model. This is used after a fit.""" if self.model.simultaneous_fitting_mode: self.model.fit_statuses = [fit_status] * self.model.number_of_datasets self.model.chi_squared = [chi_squared] * self.model.number_of_datasets else: super().update_fit_statuses_and_chi_squared_in_model(fit_status, chi_squared) def update_fit_function_in_model(self, fit_function: IFunction) -> None: """Updates the fit function stored in the model. This is used after a fit.""" if self.model.simultaneous_fitting_mode: self.model.simultaneous_fit_function = fit_function else: super().update_fit_function_in_model(fit_function) def update_simultaneous_fit_by_specifiers_in_view(self) -> None: """Updates the entries in the simultaneous fit by specifier combo box.""" self.view.setup_fit_by_specifier(self.model.get_simultaneous_fit_by_specifiers_to_display_from_context()) def update_fit_function_in_view_from_model(self) -> None: """Updates the parameters of a fit function shown in the view.""" self.view.set_current_dataset_index(self.model.current_dataset_index) self.view.update_fit_function(self.model.get_active_fit_function(), self.model.global_parameters) def update_fit_functions_in_model_from_view(self) -> None: """Updates the fit functions stored in the model using the view.""" self.model.global_parameters = self.view.global_parameters if self.model.simultaneous_fitting_mode: self.model.clear_single_fit_functions() self.update_simultaneous_fit_function_in_model() else: self.model.clear_simultaneous_fit_function() self.update_single_fit_functions_in_model() def update_simultaneous_fit_function_in_model(self) -> None: """Updates the simultaneous fit function in the model using the view.""" self.model.simultaneous_fit_function = self.view.fit_object
class EngineeringDiffractionPresenter(object): def __init__(self): self.calibration_presenter = None self.focus_presenter = None self.fitting_presenter = None self.settings_presenter = None self.doc_folder = "diffraction" self.doc = "Engineering Diffraction" # Setup observers self.calibration_observer = CalibrationObserver(self) # Setup observables self.statusbar_observable = GenericObservable() self.savedir_observable = GenericObservable() # the following setup functions should only be called from the view, this ensures both that the presenter object # itself doesn't own the view (vice versa in this instance) and that the 'child' tabs of the presenter can be mocked # /subbed in for other purposes i.e. testing, agnostic of the view def setup_calibration(self, view): cal_model = CalibrationModel() cal_view = CalibrationView(parent=view.tabs) self.calibration_presenter = CalibrationPresenter(cal_model, cal_view) view.tabs.addTab(cal_view, "Calibration") def setup_calibration_notifier(self): self.calibration_presenter.calibration_notifier.add_subscriber( self.focus_presenter.calibration_observer) self.calibration_presenter.calibration_notifier.add_subscriber( self.calibration_observer) def setup_focus(self, view): focus_model = FocusModel() focus_view = FocusView() self.focus_presenter = FocusPresenter(focus_model, focus_view) view.tabs.addTab(focus_view, "Focus") def setup_fitting(self, view): fitting_view = FittingView() self.fitting_presenter = FittingPresenter(fitting_view) self.focus_presenter.add_focus_subscriber( self.fitting_presenter.data_widget.presenter.focus_run_observer) view.tabs.addTab(fitting_view, "Fitting") def setup_settings(self, view): settings_model = SettingsModel() settings_view = SettingsView(view) settings_presenter = SettingsPresenter(settings_model, settings_view) settings_presenter.load_settings_from_file_or_default() self.settings_presenter = settings_presenter self.setup_savedir_notifier(view) def setup_savedir_notifier(self, view): self.settings_presenter.savedir_notifier.add_subscriber( view.savedir_observer) def handle_close(self): self.fitting_presenter.data_widget.ads_observer.unsubscribe() self.fitting_presenter.data_widget.view.saveSettings() self.fitting_presenter.plot_widget.view.ensure_fit_dock_closed() def open_help_window(self): InterfaceManager().showCustomInterfaceHelp(self.doc, self.doc_folder) def open_settings(self): self.settings_presenter.show() def update_calibration(self, calibration): instrument = calibration.get_instrument() sample_no = path_handling.get_run_number_from_path( calibration.get_sample(), instrument) self.statusbar_observable.notify_subscribers( f"CeO2: {sample_no}, Instrument: {instrument}") @staticmethod def get_saved_rb_number() -> str: rb_number = get_setting(output_settings.INTERFACES_SETTINGS_GROUP, output_settings.ENGINEERING_PREFIX, "rb_number") return rb_number @staticmethod def set_saved_rb_number(rb_number) -> None: set_setting(output_settings.INTERFACES_SETTINGS_GROUP, output_settings.ENGINEERING_PREFIX, "rb_number", rb_number)
class PlotWidgetPresenterCommon(HomeTabSubWidget): def __init__(self, view: PlotWidgetViewInterface, model: PlotWidgetModel, context, figure_presenter: PlottingCanvasPresenterInterface, get_selected_fit_workspaces, external_plotting_view=None, external_plotting_model=None): """ :param view: A reference to the QWidget object for plotting :param model: A reference to a model which contains the plotting logic :param context: A reference to the Muon context object :param figure_presenter: A reference to a figure presenter, which is used as an interface to plotting :param external_plotting_view: A reference to an external plotting_view - used for mocking :param external_plotting_model: A reference to an external plotting model - used for mocking """ if not isinstance(figure_presenter, PlottingCanvasPresenterInterface): raise TypeError("Parameter figure_presenter must be of type PlottingCanvasPresenterInterface") self._view = view self._model = model self.context = context self._get_selected_fit_workspaces = get_selected_fit_workspaces # figure presenter - the common presenter talks to this through an interface self._figure_presenter = figure_presenter self._external_plotting_view = external_plotting_view if external_plotting_view else ExternalPlottingView() self._external_plotting_model = external_plotting_model if external_plotting_model else ExternalPlottingModel() # gui observers self._setup_gui_observers() self._setup_view_connections() self.update_view_from_model() self.data_plot_range = self.context.default_data_plot_range self.fitting_plot_range = self.context.default_fitting_plot_range self.data_plot_tiled_state = None self._view.hide_plot_diff() def update_view_from_model(self): """"Updates the view based on data in the model """ plot_types = self._model.get_plot_types() self._view.setup_plot_type_options(plot_types) tiled_types = self._model.get_tiled_by_types() self._view.setup_tiled_by_options(tiled_types) def _setup_gui_observers(self): """"Setup GUI observers, e.g fit observers""" self.workspace_deleted_from_ads_observer = GenericObserverWithArgPassing(self.handle_workspace_deleted_from_ads) self.workspace_replaced_in_ads_observer = GenericObserverWithArgPassing(self.handle_workspace_replaced_in_ads) self.added_group_or_pair_observer = GenericObserverWithArgPassing( self.handle_added_or_removed_group_or_pair_to_plot) self.instrument_observer = GenericObserver(self.handle_instrument_changed) self.plot_selected_fit_observer = GenericObserverWithArgPassing(self.handle_plot_selected_fits) self.plot_guess_observer = GenericObserver(self.handle_plot_guess_changed) self.rebin_options_set_observer = GenericObserver(self.handle_rebin_options_changed) self.plot_type_changed_notifier = GenericObservable() def _setup_view_connections(self): self._view.on_plot_tiled_checkbox_changed(self.handle_plot_tiled_state_changed) self._view.on_tiled_by_type_changed(self.handle_tiled_by_type_changed) self._view.on_plot_type_changed(self.handle_plot_type_changed) self._view.on_external_plot_pressed(self.handle_external_plot_requested) self._view.on_rebin_options_changed(self.handle_use_raw_workspaces_changed) self._view.on_plot_mode_changed(self.handle_plot_mode_changed_by_user) def handle_data_updated(self, autoscale=False): """ Handles the group and pairs calculation finishing by plotting the loaded groups and pairs. """ if self._view.is_tiled_plot(): tiled_by = self._view.tiled_by() keys = self._model.create_tiled_keys(tiled_by) self._figure_presenter.create_tiled_plot(keys, tiled_by) self.plot_all_selected_data(autoscale=autoscale, hold_on=False) def update_plot(self, autoscale=False): if self.context.gui_context['PlotMode'] == PlotMode.Data: self.handle_data_updated(autoscale=autoscale) elif self.context.gui_context['PlotMode'] == PlotMode.Fitting: # Plot the displayed workspace self.handle_plot_selected_fits( self._get_selected_fit_workspaces(), autoscale ) def handle_plot_mode_changed(self, plot_mode : PlotMode): if isinstance(self.context, FrequencyDomainAnalysisContext): self.handle_plot_mode_changed_for_frequency_domain_analysis(plot_mode) else: self.handle_plot_mode_changed_for_muon_analysis(plot_mode) def handle_plot_mode_changed_for_muon_analysis(self, plot_mode : PlotMode): if plot_mode == self.context.gui_context['PlotMode']: return self.context.gui_context['PlotMode'] = plot_mode self._view.set_plot_mode(str(plot_mode)) if plot_mode == PlotMode.Data: self._view.enable_plot_type_combo() self._view.hide_plot_diff() self.update_plot() self.fitting_plot_range = self._figure_presenter.get_plot_x_range() self._figure_presenter.set_plot_range(self.data_plot_range) elif plot_mode == PlotMode.Fitting: self._view.disable_plot_type_combo() self._view.show_plot_diff() self.update_plot() self.data_plot_range = self._figure_presenter.get_plot_x_range() self._figure_presenter.set_plot_range(self.fitting_plot_range) self._figure_presenter.autoscale_y_axes() def handle_plot_mode_changed_for_frequency_domain_analysis(self, plot_mode : PlotMode): if plot_mode == self.context.gui_context['PlotMode']: return self.context.gui_context['PlotMode'] = plot_mode self._view.set_plot_mode(str(plot_mode)) if plot_mode == PlotMode.Data: self._view.enable_plot_type_combo() self._view.hide_plot_diff() self._view.enable_tile_plotting_options() self._view.enable_plot_raw_option() self._view.set_is_tiled_plot(self.data_plot_tiled_state) self.update_plot() self.fitting_plot_range = self._figure_presenter.get_plot_x_range() self._figure_presenter.set_plot_range(self.data_plot_range) elif plot_mode == PlotMode.Fitting: self._view.disable_plot_type_combo() self._view.show_plot_diff() self._view.disable_tile_plotting_options() self._view.disable_plot_raw_option() self.data_plot_tiled_state = self._view.is_tiled_plot() self._view.set_is_tiled_plot(False) self.update_plot() self.data_plot_range = self._figure_presenter.get_plot_x_range() self._figure_presenter.set_plot_range(self.fitting_plot_range) self._figure_presenter.autoscale_y_axes() def handle_plot_mode_changed_by_user(self): plot_mode = PlotMode.Data if str(PlotMode.Data) == self._view.get_plot_mode() else PlotMode.Fitting self.handle_plot_mode_changed(plot_mode) def handle_workspace_deleted_from_ads(self, workspace: Workspace2D): """ Handles a workspace being deleted from ads by removing the workspace from the plot :param workspace: workspace 2D object """ workspace_name = workspace.name() plotted_workspaces, _ = self._figure_presenter.get_plotted_workspaces_and_indices() if workspace_name in plotted_workspaces: self._figure_presenter.remove_workspace_from_plot(workspace) def handle_workspace_replaced_in_ads(self, workspace: Workspace2D): """ Handles the use raw workspaces being changed (e.g rebinned) in the ADS. :param workspace: workspace 2D object """ workspace_name = workspace.name() plotted_workspaces, _ = self._figure_presenter.get_plotted_workspaces_and_indices() if workspace_name in plotted_workspaces: self._figure_presenter.replace_workspace_in_plot(workspace) def handle_plot_type_changed(self): """ Handles the plot type being changed in the view by plotting the workspaces corresponding to the new plot type """ if self._check_if_counts_and_groups_selected(): return self.plot_all_selected_data(autoscale=True, hold_on=False) self.plot_type_changed_notifier.notify_subscribers(self._view.get_plot_type()) def handle_plot_tiled_state_changed(self): """ Handles the tiled plots checkbox being changed in the view. This leads to two behaviors: If switching to tiled plot, create a new figure based on the number of tiles and replot the data If switching from a tiled plot, create a new single figure and replot the data """ if self._view.is_tiled_plot(): tiled_by = self._view.tiled_by() keys = self._model.create_tiled_keys(tiled_by) self._figure_presenter.convert_plot_to_tiled_plot(keys, tiled_by) else: self._figure_presenter.convert_plot_to_single_plot() def handle_tiled_by_type_changed(self): """ Handles the tiled type changing, this will cause the tiles (and the key for each tile) to change. This is handled by generating the new keys and replotting the data based on these new tiles. """ if not self._view.is_tiled_plot(): return tiled_by = self._view.tiled_by() keys = self._model.create_tiled_keys(tiled_by) self._figure_presenter.convert_plot_to_tiled_plot(keys, tiled_by) def handle_rebin_options_changed(self): """ Handles rebin options changed on the home tab """ if self.context._do_rebin(): self._view.set_raw_checkbox_state(False) else: self._view.set_raw_checkbox_state(True) def handle_use_raw_workspaces_changed(self): """ Handles plot raw changed in view """ if not self._view.is_raw_plot() and not self.context._do_rebin(): self._view.set_raw_checkbox_state(True) self._view.warning_popup('No rebin options specified') return workspace_list, indices = self._model.get_workspace_list_and_indices_to_plot(self._view.is_raw_plot(), self._view.get_plot_type()) self._figure_presenter.plot_workspaces(workspace_list, indices, hold_on=False, autoscale=False) def handle_added_or_removed_group_or_pair_to_plot(self, group_pair_info: Dict): """ Handles a group or pair being added or removed from the grouping widget analysis table :param group_pair_info: A dictionary containing information on the removed group/pair """ is_added = group_pair_info["is_added"] name = group_pair_info["name"] if is_added: self.handle_added_group_or_pair_to_plot(name) else: self.handle_removed_group_or_pair_to_plot(name) def handle_added_group_or_pair_to_plot(self, group_or_pair_name: str): """ Handles a group or pair being added from the view :param group_or_pair_name: The group or pair name that was added to the analysis """ self._check_if_counts_and_groups_selected() # if tiled by group, we will need to recreate the tiles if self._view.is_tiled_plot() and self._view.tiled_by() == self._model.tiled_by_group: tiled_by = self._view.tiled_by() keys = self._model.create_tiled_keys(tiled_by) self._figure_presenter.create_tiled_plot(keys, tiled_by) self.plot_all_selected_data(autoscale=False, hold_on=False) return workspace_list, indices = self._model.get_workspace_and_indices_for_group_or_pair( group_or_pair_name, is_raw=self._view.is_raw_plot(), plot_type=self._view.get_plot_type()) self._figure_presenter.plot_workspaces(workspace_list, indices, hold_on=True, autoscale=False) def handle_removed_group_or_pair_to_plot(self, group_or_pair_name: str): """ Handles a group or pair being removed in grouping widget analysis table :param group_or_pair_name: The group or pair name that was removed from the analysis """ # tiled by group, we will need to recreate the tiles if self._view.is_tiled_plot() and self._view.tiled_by() == self._model.tiled_by_group: tiled_by = self._view.tiled_by() keys = self._model.create_tiled_keys(tiled_by) self._figure_presenter.create_tiled_plot(keys, tiled_by) self.plot_all_selected_data(hold_on=False, autoscale=False) return workspace_list, indices = self._model.get_workspace_and_indices_for_group_or_pair( group_or_pair_name, is_raw=True, plot_type=self._view.get_plot_type()) self._figure_presenter.remove_workspace_names_from_plot(workspace_list) def handle_external_plot_requested(self): """ Handles an external plot request """ axes = self._figure_presenter.get_plot_axes() external_fig = self._external_plotting_view.create_external_plot_window(axes) data = self._external_plotting_model.get_plotted_workspaces_and_indices_from_axes(axes) self._external_plotting_view.plot_data(external_fig, data) self._external_plotting_view.copy_axes_setup(external_fig, axes) self._external_plotting_view.show(external_fig) def handle_instrument_changed(self): """ Handles the instrument being changed by creating a blank plot window """ self._figure_presenter.create_single_plot() def handle_plot_selected_fits(self, fit_information_list: List[FitPlotInformation], autoscale=False): """Plots a list of selected fit workspaces (obtained from fit and seq fit tabs). :param fit_information_list: List of named tuples each entry of the form (fit, input_workspaces) """ workspace_list = [] indices = [] raw = self._view.is_raw_plot() with_diff = self._view.is_plot_diff() if fit_information_list: for fit_information in fit_information_list: fit = fit_information.fit fit_workspaces, fit_indices = self._model.get_fit_workspace_and_indices(fit,with_diff) workspace_list += self.match_raw_selection(fit_information.input_workspaces,raw) + fit_workspaces indices += [0] * len(fit_information.input_workspaces) + fit_indices self._figure_presenter.plot_workspaces(workspace_list, indices, hold_on=False, autoscale=autoscale) def match_raw_selection(self, workspace_names, plot_raw): ws_list = [] workspace_list = workspace_names if type(workspace_names) != list: workspace_list = [workspace_names] for workspace_name in workspace_list: fit_raw_data = self.context.fitting_context.fit_raw # binned data but want raw plot if plot_raw and not fit_raw_data: ws_list.append(remove_rebin_from_name(workspace_name)) # raw data but want binned plot elif not plot_raw and fit_raw_data: ws_list.append(add_rebin_to_name(workspace_name)) else: ws_list.append(workspace_name) return ws_list def handle_plot_guess_changed(self): if self.context.fitting_context.guess_ws is None: return if self.context.fitting_context.plot_guess: self._figure_presenter.plot_guess_workspace(self.context.fitting_context.guess_ws) else: self._figure_presenter.remove_workspace_names_from_plot([self.context.fitting_context.guess_ws]) def plot_all_selected_data(self, autoscale, hold_on): """Plots all selected run data e.g runs and groups :param autoscale: Whether to autoscale the graph :param hold_on: Whether to keep previous plots """ workspace_list, indices = self._model.get_workspace_list_and_indices_to_plot(self._view.is_raw_plot(), self._view.get_plot_type()) self._figure_presenter.plot_workspaces(workspace_list, indices, hold_on=hold_on, autoscale=autoscale) def _check_if_counts_and_groups_selected(self): if len(self.context.group_pair_context.selected_pairs) != 0 and \ self._view.get_plot_type() == self._model.counts_plot: self._view.set_plot_type(self._model.asymmetry_plot) self._view.warning_popup( 'Pair workspaces have no counts workspace, plotting Asymmetry') return True return False
class GroupingTablePresenter(object): def __init__(self, view, model): self._view = view self._model = model self._view.on_add_group_button_clicked( self.handle_add_group_button_clicked) self._view.on_remove_group_button_clicked( self.handle_remove_group_button_clicked) self._view.on_user_changes_group_name(self.validate_group_name) self._view.on_user_changes_detector_IDs(self.validate_detector_ids) self._view.on_table_data_changed(self.handle_data_change) self._view.on_user_changes_min_range_source( self.first_good_data_checkbox_changed) self._view.on_user_changes_max_range_source( self.from_file_checkbox_changed) self._view.on_user_changes_group_range_min_text_edit( self.handle_group_range_min_updated) self._view.on_user_changes_group_range_max_text_edit( self.handle_group_range_max_updated) self.selected_group_changed_notifier = GenericObservable() self._dataChangedNotifier = lambda: 0 def show(self): self._view.show() def on_data_changed(self, notifier): self._dataChangedNotifier = notifier def notify_data_changed(self): self._dataChangedNotifier() def _is_edited_name_duplicated(self, new_name): is_name_column_being_edited = self._view.grouping_table.currentColumn( ) == 0 is_name_unique = True if new_name in self._model.group_and_pair_names: is_name_unique = False return is_name_column_being_edited and not is_name_unique def validate_group_name(self, text): if self._is_edited_name_duplicated(text): self._view.warning_popup("Groups and pairs must have unique names") return False if not re.match(run_utils.valid_name_regex, text): self._view.warning_popup( "Group names should only contain digits, characters and _") return False return True def validate_detector_ids(self, text): try: if re.match(run_utils.run_string_regex, text) and run_utils.run_string_to_list(text, False) and \ max(run_utils.run_string_to_list(text, False)) <= self._model._data.num_detectors \ and min(run_utils.run_string_to_list(text, False)) > 0: return True except OverflowError: pass self._view.warning_popup("Invalid detector list.") return False def disable_editing(self): self._view.disable_editing() def enable_editing(self): self._view.enable_editing() def add_group(self, group): """Adds a group to the model and view""" try: if self._view.num_rows() >= maximum_number_of_groups: self._view.warning_popup( "Cannot add more than {} groups.".format( maximum_number_of_groups)) return # self.add_group_to_view(group) self.add_group_to_model(group) self.update_view_from_model() self._view.notify_data_changed() self.notify_data_changed() except ValueError as error: self._view.warning_popup(error) def add_group_to_model(self, group): self._model.add_group(group) def add_group_to_view(self, group, state): self._view.disable_updates() assert isinstance(group, MuonGroup) entry = [ str(group.name), state, run_utils.run_list_to_string(group.detectors, False), str(group.n_detectors) ] self._view.add_entry_to_table(entry) self._view.enable_updates() def handle_add_group_button_clicked(self): new_group_name = self._view.enter_group_name() if new_group_name is None: return if new_group_name in self._model.group_and_pair_names: self._view.warning_popup("Groups and pairs must have unique names") elif self.validate_group_name(new_group_name): group = MuonGroup(group_name=str(new_group_name), detector_ids=[1]) self.add_group(group) def handle_remove_group_button_clicked(self): group_names = self._view.get_selected_group_names() if not group_names: self.remove_last_row_in_view_and_model() else: self.remove_selected_rows_in_view_and_model(group_names) self._view.notify_data_changed() self.notify_data_changed() def remove_selected_rows_in_view_and_model(self, group_names): self._view.remove_selected_groups() for group_name in group_names: self._model.remove_group_from_analysis(group_name) self._model.remove_groups_by_name(group_names) def remove_last_row_in_view_and_model(self): if self._view.num_rows() > 0: name = self._view.get_table_contents()[-1][0] self._view.remove_last_row() self._model.remove_group_from_analysis(name) self._model.remove_groups_by_name([name]) def handle_data_change(self, row, col): changed_item = self._view.get_table_item(row, col) group_name = self._view.get_table_item(row, 0).text() update_model = True if col == 0 and not self.validate_group_name(changed_item.text()): update_model = False if col == 2 and not self.validate_detector_ids(changed_item.text()): update_model = False if col == 1: update_model = False self.to_analyse_data_checkbox_changed(changed_item.checkState(), row, group_name) if update_model: try: self.update_model_from_view() except ValueError as error: self._view.warning_popup(error) # if the column containing the "to_analyse" flag is changed, then we don't need to update anything group related if col != 1: self.update_view_from_model() self._view.notify_data_changed() self.notify_data_changed() def update_model_from_view(self): table = self._view.get_table_contents() self._model.clear_groups() for entry in table: detector_list = run_utils.run_string_to_list(str(entry[2]), False) group = MuonGroup(group_name=str(entry[0]), detector_ids=detector_list) self._model.add_group(group) def update_view_from_model(self): self._view.disable_updates() self._view.clear() for group in self._model.groups: to_analyse = True if group.name in self._model.selected_groups else False self.add_group_to_view(group, to_analyse) if self._view.group_range_use_last_data.isChecked(): self._view.group_range_max.setText( str(self._model.get_last_data_from_file())) if self._view.group_range_use_first_good_data.isChecked(): self._view.group_range_min.setText( str(self._model.get_first_good_data_from_file())) self._view.enable_updates() def to_analyse_data_checkbox_changed(self, state, row, group_name): group_added = True if state == 2 else False if group_added: self._model.add_group_to_analysis(group_name) else: self._model.remove_group_from_analysis(group_name) group_info = {'is_added': group_added, 'name': group_name} self.selected_group_changed_notifier.notify_subscribers(group_info) def plot_default_case(self): for row in range(self._view.num_rows()): self._view.set_to_analyse_state_quietly(row, True) group_name = self._view.get_table_item(row, 0).text() self._model.add_group_to_analysis(group_name) def first_good_data_checkbox_changed(self): if self._view.group_range_use_first_good_data.isChecked(): self._view.group_range_min.setText( str(self._model.get_first_good_data_from_file())) self._view.group_range_min.setEnabled(False) if 'GroupRangeMin' in self._model._context.gui_context: # Remove variable from model if value from file is to be used self._model._context.gui_context.pop('GroupRangeMin') self._model._context.gui_context.update_and_send_signal() else: self._view.group_range_min.setEnabled(True) self._model._context.gui_context.update_and_send_signal( GroupRangeMin=float(self._view.group_range_min.text())) def from_file_checkbox_changed(self): if self._view.group_range_use_last_data.isChecked(): self._view.group_range_max.setText( str(self._model.get_last_data_from_file())) self._view.group_range_max.setEnabled(False) if 'GroupRangeMax' in self._model._context.gui_context: # Remove variable from model if value from file is to be used self._model._context.gui_context.pop('GroupRangeMax') self._model._context.gui_context.update_and_send_signal() else: self._view.group_range_max.setEnabled(True) self._model._context.gui_context.update_and_send_signal( GroupRangeMax=float(self._view.group_range_max.text())) def handle_group_range_min_updated(self): range_min_new = float(self._view.group_range_min.text()) range_max_current = self._model._context.gui_context['GroupRangeMax'] if 'GroupRangeMax' in \ self._model._context.gui_context \ else self._model.get_last_data_from_file() if range_min_new < range_max_current: self._model._context.gui_context.update_and_send_signal( GroupRangeMin=range_min_new) else: self._view.group_range_min.setText( str(self._model._context.gui_context['GroupRangeMin'])) self._view.warning_popup( 'Minimum of group asymmetry range must be less than maximum') def handle_group_range_max_updated(self): range_max_new = float(self._view.group_range_max.text()) range_min_current = self._model._context.gui_context['GroupRangeMin'] if 'GroupRangeMin' in \ self._model._context.gui_context \ else self._model.get_first_good_data_from_file() if range_max_new > range_min_current: self._model._context.gui_context.update_and_send_signal( GroupRangeMax=range_max_new) else: self._view.group_range_max.setText( str(self._model._context.gui_context['GroupRangeMax'])) self._view.warning_popup( 'Maximum of group asymmetry range must be greater than minimum' )
class PlotToolbar(MantidNavigationToolbar): toolitems = ( MantidStandardNavigationTools.HOME, MantidStandardNavigationTools.BACK, MantidStandardNavigationTools.FORWARD, MantidStandardNavigationTools.SEPARATOR, MantidStandardNavigationTools.PAN, MantidStandardNavigationTools.ZOOM, MantidStandardNavigationTools().SEPARATOR, MantidNavigationTool('Show major','Show major gridlines','mdi.grid-large','show_major_gridlines'), MantidNavigationTool('Show minor','Show minor gridlines','mdi.grid','show_minor_gridlines' ), MantidStandardNavigationTools.SEPARATOR, MantidStandardNavigationTools.CONFIGURE, MantidStandardNavigationTools.SAVE, MantidStandardNavigationTools.SEPARATOR, MantidNavigationTool('Show/hide legend', 'Toggles the legend on/off', None, 'toggle_legend'), ) def __init__(self, figure_canvas, parent=None): super().__init__(figure_canvas, parent) self.is_major_grid_on = False self.is_minor_grid_on = False self.uncheck_autoscale_notifier = GenericObservable() self.enable_autoscale_notifier = GenericObservable() self.disable_autoscale_notifier = GenericObservable() self.range_changed_notifier = GenericObservable() # Adjust icon size or they are too small in PyQt5 by default dpi_ratio = QtWidgets.QApplication.instance().desktop().physicalDpiX() / 100 self.setIconSize(QtCore.QSize(24 * dpi_ratio, 24 * dpi_ratio)) def toggle_legend(self): for ax in self.canvas.figure.get_axes(): if ax.get_legend() is not None: ax.get_legend().set_visible(not ax.get_legend().get_visible()) self.canvas.figure.tight_layout() self.canvas.draw() def show_major_gridlines(self): if self.is_major_grid_on: for ax in self.canvas.figure.get_axes(): ax.grid(False) self.is_major_grid_on = False else: for ax in self.canvas.figure.get_axes(): ax.grid(True , color='black') self.is_major_grid_on = True self.canvas.draw() def show_minor_gridlines(self): if self.is_minor_grid_on: for ax in self.canvas.figure.get_axes(): ax.grid(which='minor') self.is_minor_grid_on = False else: for ax in self.canvas.figure.get_axes(): ax.minorticks_on() ax.grid(which='minor') self.is_minor_grid_on = True self.canvas.draw() def reset_gridline_flags(self): self.is_minor_grid_on = False self.is_major_grid_on = False def zoom(self, *args): """Activate zoom to rect mode.""" if self._active == 'ZOOM': self._active = None self.enable_autoscale_notifier.notify_subscribers() self.range_changed_notifier.notify_subscribers() else: self.uncheck_autoscale_notifier.notify_subscribers() self.disable_autoscale_notifier.notify_subscribers() self._active = 'ZOOM' if self._idPress is not None: self._idPress = self.canvas.mpl_disconnect(self._idPress) self.mode = '' if self._idRelease is not None: self._idRelease = self.canvas.mpl_disconnect(self._idRelease) self.mode = '' if self._active: self._idPress = self.canvas.mpl_connect('button_press_event', self.press_zoom) self._idRelease = self.canvas.mpl_connect('button_release_event', self.release_zoom) self.mode = 'zoom rect' self.canvas.widgetlock(self) else: self.canvas.widgetlock.release(self) for axes in self.canvas.figure.get_axes(): axes.set_navigate_mode(self._active) self._update_buttons_checked() self.set_message(self.mode) def pan(self, *args): """Activate the pan/zoom tool. pan with left button, zoom with right""" # set the pointer icon and button press funcs to the # appropriate callbacks if self._active == 'PAN': self._active = None self.enable_autoscale_notifier.notify_subscribers() self.range_changed_notifier.notify_subscribers() else: self.uncheck_autoscale_notifier.notify_subscribers() self.disable_autoscale_notifier.notify_subscribers() self._active = 'PAN' if self._idPress is not None: self._idPress = self.canvas.mpl_disconnect(self._idPress) self.mode = '' if self._idRelease is not None: self._idRelease = self.canvas.mpl_disconnect(self._idRelease) self.mode = '' if self._active: self._idPress = self.canvas.mpl_connect( 'button_press_event', self.press_pan) self._idRelease = self.canvas.mpl_connect( 'button_release_event', self.release_pan) self.mode = 'pan/zoom' self.canvas.widgetlock(self) else: self.canvas.widgetlock.release(self) for axes in self.canvas.figure.get_axes(): axes.set_navigate_mode(self._active) self._update_buttons_checked() self.set_message(self.mode) def home(self, *args): """Restore the original view.""" self._nav_stack.home() self.set_history_buttons() self._update_view()
class GroupingTabPresenter(object): """ The grouping tab presenter is responsible for synchronizing the group and pair tables. It also maintains functionality which covers both groups/pairs ; e.g. loading/saving/updating data. """ @staticmethod def string_to_list(text): return run_string_to_list(text) def __init__(self, view, model, grouping_table_widget=None, pairing_table_widget=None, diff_table=None, parent=None): self._view = view self._model = model self.grouping_table_widget = grouping_table_widget self.pairing_table_widget = pairing_table_widget self.diff_table = diff_table self.period_info_widget = MuonPeriodInfo() self._view.set_description_text('') self._view.on_add_pair_requested(self.add_pair_from_grouping_table) self._view.on_clear_grouping_button_clicked(self.on_clear_requested) self._view.on_load_grouping_button_clicked( self.handle_load_grouping_from_file) self._view.on_save_grouping_button_clicked( self.handle_save_grouping_file) self._view.on_default_grouping_button_clicked( self.handle_default_grouping_button_clicked) self._view.on_period_information_button_clicked( self.handle_period_information_button_clicked) # monitors for loaded data changing self.loadObserver = GroupingTabPresenter.LoadObserver(self) self.instrumentObserver = GroupingTabPresenter.InstrumentObserver(self) # notifiers self.groupingNotifier = GroupingTabPresenter.GroupingNotifier(self) self.grouping_table_widget.on_data_changed(self.group_table_changed) self.diff_table.on_data_changed(self.diff_table_changed) self.pairing_table_widget.on_data_changed(self.pair_table_changed) self.enable_editing_notifier = GroupingTabPresenter.EnableEditingNotifier( self) self.disable_editing_notifier = GroupingTabPresenter.DisableEditingNotifier( self) self.counts_calculation_finished_notifier = GenericObservable() self.guessAlphaObserver = GroupingTabPresenter.GuessAlphaObserver(self) self.pairing_table_widget.guessAlphaNotifier.add_subscriber( self.guessAlphaObserver) self.message_observer = GroupingTabPresenter.MessageObserver(self) self.gui_variables_observer = GroupingTabPresenter.GuiVariablesChangedObserver( self) self.enable_observer = GroupingTabPresenter.EnableObserver(self) self.disable_observer = GroupingTabPresenter.DisableObserver(self) self.disable_tab_observer = GenericObserver( self.disable_editing_without_notifying_subscribers) self.enable_tab_observer = GenericObserver( self.enable_editing_without_notifying_subscribers) self.update_view_from_model_observer = GenericObserver( self.update_view_from_model) def update_view_from_model(self): self.grouping_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.pairing_table_widget.update_view_from_model() def show(self): self._view.show() def text_for_description(self): """ Generate the text for the description edit at the top of the widget. """ instrument = self._model.instrument n_detectors = self._model.num_detectors main_field = self._model.main_field_direction text = "{}, {} detectors".format(instrument, n_detectors) if main_field: text += ", main field : {} to muon polarization".format(main_field) return text def update_description_text(self, description_text=''): if not description_text: description_text = self.text_for_description() self._view.set_description_text(description_text) def update_description_text_to_empty(self): self._view.set_description_text('') def add_pair_from_grouping_table(self, group_name1="", group_name2=""): """ If user requests to add a pair from the grouping table. """ self.pairing_table_widget.handle_add_pair_button_clicked( group_name1, group_name2) def handle_guess_alpha(self, pair_name, group1_name, group2_name): """ Calculate alpha for the pair for which "Guess Alpha" button was clicked. """ if len(self._model._data.current_runs) > 1: run, index, ok_clicked = RunSelectionDialog.get_run( self._model._data.current_runs, self._model._data.instrument, self._view) if not ok_clicked: return run_to_use = self._model._data.current_runs[index] else: run_to_use = self._model._data.current_runs[0] try: ws1 = self._model.get_group_workspace(group1_name, run_to_use) ws2 = self._model.get_group_workspace(group2_name, run_to_use) except KeyError: self._view.display_warning_box( 'Group workspace not found, try updating all and then recalculating.' ) return ws = algorithm_utils.run_AppendSpectra(ws1, ws2) new_alpha = algorithm_utils.run_AlphaCalc({ "InputWorkspace": ws, "ForwardSpectra": [0], "BackwardSpectra": [1] }) self._model.update_pair_alpha(pair_name, new_alpha) self.pairing_table_widget.update_view_from_model() self.handle_update_all_clicked() def handle_load_grouping_from_file(self): # Only XML format file_filter = file_utils.filter_for_extensions(["xml"]) filename = self._view.show_file_browser_and_return_selection( file_filter, [""]) if filename == '': return groups, pairs, diffs, description, default = xml_utils.load_grouping_from_XML( filename) self._model.clear() for group in groups: try: self._model.add_group(group) except ValueError as error: self._view.display_warning_box(str(error)) for pair in pairs: try: if pair.forward_group in self._model.group_names and pair.backward_group in self._model.group_names: self._model.add_pair(pair) except ValueError as error: self._view.display_warning_box(str(error)) for diff in diffs: try: if diff.positive in self._model.group_names and diff.negative in self._model.group_names: self._model.add_diff(diff) elif diff.positive in self._model.pair_names and diff.negative in self._model.pair_names: self._model.add_diff(diff) except ValueError as error: self._view.display_warning_box(str(error)) # Sets the default from file if it exists, if not selected groups/pairs are set on the logic # Select all pairs if there are any pairs otherwise select all groups. if default: if default in self._model.group_names: self._model.add_group_to_analysis(default) elif default in self._model.pair_names: self._model.add_pair_to_analysis(default) self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.update_description_text(description) self._model._context.group_pair_context.selected = default self.plot_default_groups_or_pairs() self.groupingNotifier.notify_subscribers() self.handle_update_all_clicked() def disable_editing(self): self._view.set_buttons_enabled(False) self.grouping_table_widget.disable_editing() self.diff_table.disable_editing() self.pairing_table_widget.disable_editing() self.disable_editing_notifier.notify_subscribers() def enable_editing(self, result=None): self._view.set_buttons_enabled(True) self.grouping_table_widget.enable_editing() self.diff_table.enable_editing() self.pairing_table_widget.enable_editing() self.enable_editing_notifier.notify_subscribers() def disable_editing_without_notifying_subscribers(self): self._view.set_buttons_enabled(False) self.grouping_table_widget.disable_editing() self.diff_table.disable_editing() self.pairing_table_widget.disable_editing() def enable_editing_without_notifying_subscribers(self): self._view.set_buttons_enabled(True) self.grouping_table_widget.enable_editing() self.diff_table.enable_editing() self.pairing_table_widget.enable_editing() def calculate_all_data(self): self._model.calculate_all_data() def handle_update_all_clicked(self): self.update_thread = self.create_update_thread() self.update_thread.threadWrapperSetUp(self.disable_editing, self.handle_update_finished, self.error_callback) self.update_thread.start() def error_callback(self, error_message): self.enable_editing() self._view.display_warning_box(error_message) def handle_update_finished(self): self.enable_editing() self.groupingNotifier.notify_subscribers() self.counts_calculation_finished_notifier.notify_subscribers() def handle_default_grouping_button_clicked(self): status = self._model.reset_groups_and_pairs_to_default() if status == "failed": self._view.display_warning_box( "The default may depend on the instrument. Please load a run.") return self._model.reset_selected_groups_and_pairs() self.grouping_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.update_description_text() self.groupingNotifier.notify_subscribers() self.handle_update_all_clicked() self.plot_default_groups_or_pairs() def on_clear_requested(self): self._model.clear() self.grouping_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.update_description_text_to_empty() self.groupingNotifier.notify_subscribers() def handle_new_data_loaded(self): self.period_info_widget.clear() if self._model.is_data_loaded(): self._model._context.show_raw_data() self.update_view_from_model() self.update_description_text() self.handle_update_all_clicked() self.plot_default_groups_or_pairs() if self.period_info_widget.isVisible(): self._add_period_info_to_widget() else: self.on_clear_requested() def handle_save_grouping_file(self): filename = self._view.show_file_save_browser_and_return_selection() if filename != "": xml_utils.save_grouping_to_XML( self._model.groups, self._model.pairs, self._model.diffs, filename, description=self._view.get_description_text()) def create_update_thread(self): self._update_model = ThreadModelWrapper(self.calculate_all_data) return thread_model.ThreadModel(self._update_model) def plot_default_groups_or_pairs(self): # if we have no pairs or groups selected, generate a default plot if len(self._model.selected_groups_and_pairs) == 0: if len(self._model.pairs ) > 0: # if we have pairs - then plot all pairs self.pairing_table_widget.plot_default_case() else: # else plot groups self.grouping_table_widget.plot_default_case() def handle_period_information_button_clicked(self): if self._model.is_data_loaded() and self.period_info_widget.isEmpty(): self._add_period_info_to_widget() self.period_info_widget.show() self.period_info_widget.raise_() def closePeriodInfoWidget(self): self.period_info_widget.close() def _add_period_info_to_widget(self): try: self.period_info_widget.addInfo( self._model._data.current_workspace) runs = self._model._data.current_runs runs_string = "" for run_list in runs: for run in run_list: if runs_string: runs_string += ", " runs_string += str(run) self.period_info_widget.setWidgetTitleRuns(self._model.instrument + runs_string) except RuntimeError: self._view.display_warning_box( "Could not read period info from the current workspace") # ------------------------------------------------------------------------------------------------------------------ # Observer / Observable # ------------------------------------------------------------------------------------------------------------------ def group_table_changed(self): self.pairing_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.handle_update_all_clicked() def pair_table_changed(self): self.grouping_table_widget.update_view_from_model() self.diff_table.update_view_from_model() self.handle_update_all_clicked() def diff_table_changed(self): self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.handle_update_all_clicked() class LoadObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_new_data_loaded() class InstrumentObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.on_clear_requested() class GuessAlphaObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_guess_alpha(arg[0], arg[1], arg[2]) class GuiVariablesChangedObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_update_all_clicked() class GroupingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs) class MessageObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer._view.display_warning_box(arg) class EnableObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.enable_editing() class DisableObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.disable_editing() class DisableEditingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs) class EnableEditingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs)
class FFTPresenter(object): """ This class links the FFT model to the GUI """ def __init__(self, view, alg, load): self.view = view self.alg = alg self.load = load self.thread = None # set data self.getWorkspaceNames() # connect self.view.tableClickSignal.connect(self.tableClicked) self.view.buttonSignal.connect(self.handleButton) self.calculation_finished_notifier = GenericObservable() self.view.setup_raw_checkbox_changed(self.handle_use_raw_data_changed) def cancel(self): if self.thread is not None: self.thread.cancel() def runChanged(self): self.getWorkspaceNames() @property def widget(self): return self.view # turn on button def activate(self): self.view.activateButton() # turn off button def deactivate(self): self.view.deactivateButton() def getWorkspaceNames(self): # get current values original_Re_name = self.view.workspace original_Im_name = self.view.imaginary_workspace final_options = self.load.get_workspace_names_for_FFT_analysis(self.view.use_raw_data) # update view self.view.addItems(final_options) # make intelligent guess of what user wants current_group_pair = self.load.group_pair_context[self.load.group_pair_context.selected] Re_name_to_use = None Im_name_to_use = None default_name = None # will need to check this exists before using it if current_group_pair: default_name = current_group_pair.get_asymmetry_workspace_names( self.load.data_context.current_runs) # if the original selection is available we should use it if original_Re_name in final_options: Re_name_to_use = original_Re_name elif default_name: Re_name_to_use = default_name[0] self.view.workspace = Re_name_to_use if original_Im_name in final_options: Im_name_to_use = original_Im_name elif default_name: Im_name_to_use = default_name[0] self.view.imaginary_workspace=Im_name_to_use return def handle_use_raw_data_changed(self): if not self.view.use_raw_data and not self.load._do_rebin(): self.view.set_raw_checkbox_state(True) self.view.warning_popup('No rebin options specified') return self.getWorkspaceNames() def tableClicked(self, row, col): if row == self.view.getImBoxRow() and col == 1: self.view.changedHideUnTick( self.view.getImBox(), self.view.getImBoxRow() + 1) elif row == self.view.getShiftBoxRow() and col == 1: self.view.changed( self.view.getShiftBox(), self.view.getShiftBoxRow() + 1) def createThread(self): self._phasequad_calculation_model = ThreadModelWrapper(self.calculate_FFT) return thread_model.ThreadModel(self._phasequad_calculation_model) # constructs the inputs for the FFT algorithms # then executes them (see fft_model to see the order # of execution def handleButton(self): # put this on its own thread so not to freeze Mantid self.thread = self.createThread() self.thread.threadWrapperSetUp(self.deactivate, self.handleFinished, self.handle_error) self.thread.start() def handle_error(self, error): self.view.activateButton() self.view.warning_popup(error) def get_pre_inputs(self): pre_inputs = self._get_generic_apodiazation_and_padding_inputs() pre_inputs['InputWorkspace'] = self.view.workspace return pre_inputs def get_imaginary_inputs(self): pre_inputs = self._get_generic_apodiazation_and_padding_inputs() pre_inputs['InputWorkspace'] = self.view.imaginary_workspace return pre_inputs def _get_generic_apodiazation_and_padding_inputs(self): pre_inputs = {} pre_inputs['InputWorkspace'] = self.view.imaginary_workspace pre_inputs["ApodizationFunction"] = self.view.apodization_function pre_inputs["DecayConstant"] = self.view.decay_constant pre_inputs["NegativePadding"] = self.view.negative_padding pre_inputs["Padding"] = self.view.padding_value return pre_inputs def get_fft_inputs(self, real_workspace, imaginary_workspace, imanginary=0): FFTInputs = {} FFTInputs["AcceptXRoundingErrors"] = True FFTInputs['Real'] = 0 FFTInputs['InputWorkspace'] = real_workspace FFTInputs['Transform'] = 'Forward' FFTInputs['AutoShift'] = self.view.auto_shift if self.view.imaginary_data: FFTInputs['InputImagWorkspace'] = imaginary_workspace FFTInputs['Imaginary'] = imanginary return FFTInputs # kills the thread at end of execution def handleFinished(self): self.activate() self.calculation_finished_notifier.notify_subscribers(self._output_workspace_name) def calculate_FFT(self): imaginary_workspace_index = 0 real_workspace_padding_parameters = self.get_pre_inputs() imaginary_workspace_padding_parameters = self.get_imaginary_inputs() real_workspace_input = run_PaddingAndApodization(real_workspace_padding_parameters, '__real') if self.view.imaginary_data: imaginary_workspace_input = run_PaddingAndApodization(imaginary_workspace_padding_parameters, '__Imag') else: imaginary_workspace_input = None imaginary_workspace_padding_parameters['InputWorkspace'] = "" fft_parameters = self.get_fft_inputs(real_workspace_input, imaginary_workspace_input, imaginary_workspace_index) frequency_domain_workspace = convert_to_field(run_FFT(fft_parameters)) self.add_fft_workspace_to_ADS(real_workspace_padding_parameters['InputWorkspace'], imaginary_workspace_padding_parameters['InputWorkspace'], frequency_domain_workspace) def add_fft_workspace_to_ADS(self, input_workspace, imaginary_input_workspace, fft_workspace_label): run = re.search('[0-9]+', input_workspace).group() fft_workspace = mantid.AnalysisDataService.retrieve(fft_workspace_label) Im_run = "" if imaginary_input_workspace != "": Im_run = re.search('[0-9]+', imaginary_input_workspace).group() fft_workspace_name = get_fft_workspace_name(input_workspace, imaginary_input_workspace) directory = get_fft_workspace_group_name(fft_workspace_name, self.load.data_context.instrument, self.load.workspace_suffix) Re = get_group_or_pair_from_name(input_workspace) Im = get_group_or_pair_from_name(imaginary_input_workspace) shift = 3 if fft_workspace.getNumberHistograms() == 6 else 0 spectra = {"_" + FREQUENCY_EXTENSIONS["RE"]: 0 + shift, "_" + FREQUENCY_EXTENSIONS["IM"]: 1 + shift, "_" + FREQUENCY_EXTENSIONS["MOD"]: 2 + shift} for spec_type in list(spectra.keys()): extracted_ws = extract_single_spec(fft_workspace, spectra[spec_type], fft_workspace_name + spec_type) self.load._frequency_context.add_FFT(fft_workspace_name + spec_type, run, Re, Im_run, Im) muon_workspace_wrapper = MuonWorkspaceWrapper(extracted_ws) muon_workspace_wrapper.show(directory + fft_workspace_name + spec_type) # This is a small hack to get the output name to a location where it can be part of the calculation finished # signal. self._output_workspace_name = fft_workspace_name + '_mod' def update_view_from_model(self): self.getWorkspaceNames()
class HomePlotWidgetPresenter(HomeTabSubWidget): def __init__(self, view, model, context): """ :param view: A reference to the QWidget object for plotting :param model: A refence to a model which contains the plotting logic :param context: A reference to the Muon context object """ self._view = view self._model = model self.context = context self._view.on_plot_button_clicked(self.handle_data_updated) self._view.on_rebin_options_changed(self.handle_use_raw_workspaces_changed) self._view.on_plot_type_changed(self.handle_plot_type_changed) self.input_workspace_observer = GenericObserver(self.handle_data_updated) self.fit_observer = GenericObserver(self.handle_fit_completed) self.group_pair_observer = GenericObserver(self.handle_group_pair_to_plot_changed) self.plot_type_observer = GenericObserver(self.handle_group_pair_to_plot_changed) self.rebin_options_set_observer = GenericObserver(self.handle_rebin_options_set) self.plot_guess_observer = GenericObserver(self.handle_plot_guess_changed) self.plot_type_changed_notifier = GenericObservable() self._force_redraw = False if self.context._frequency_context: for ext in FREQUENCY_EXTENSIONS.keys(): self._view.addItem(FREQ_PLOT_TYPE+FREQUENCY_EXTENSIONS[ext]) self._view.addItem(FREQ_PLOT_TYPE+"All") self.instrument_observer = GenericObserver(self.handle_instrument_changed) self.keep = False def show(self): """ Calls show on the view QtWidget """ self._view.show() def update_view_from_model(self): """ This is required in order for this presenter to match the base class interface """ pass def handle_data_updated(self): """ Handles the group, pair calculation finishing. Checks whether the list of workspaces has changed before doing anything as workspaces being modified in place is handled by the ADS handler observer. """ if self._model.plotted_workspaces == self.get_workspaces_to_plot( self.context.group_pair_context.selected, self._view.if_raw(), self._view.get_selected()): # If the workspace names have not changed the ADS observer should # handle any updates return self.plot_standard_workspaces() def handle_use_raw_workspaces_changed(self): """ Handles the use raw workspaces being changed. If """ if not self._view.if_raw() and not self.context._do_rebin(): self._view.set_raw_checkbox_state(True) self._view.warning_popup('No rebin options specified') return self.plot_standard_workspaces() def handle_plot_type_changed(self): """ Handles the plot type being changed on the view """ current_group_pair = self.context.group_pair_context[ self.context.group_pair_context.selected] current_plot_type = self._view.get_selected() if isinstance(current_group_pair, MuonPair) and current_plot_type == COUNTS_PLOT_TYPE: self._view.plot_selector.blockSignals(True) self._view.plot_selector.setCurrentText(ASYMMETRY_PLOT_TYPE) self._view.plot_selector.blockSignals(False) self._view.warning_popup( 'Pair workspaces have no counts workspace') return # force the plot to update self._force_redraw = True if self.context._frequency_context: self.context._frequency_context.plot_type = self._view.get_selected()[len(FREQ_PLOT_TYPE):] self.plot_type_changed_notifier.notify_subscribers() self.plot_standard_workspaces() def handle_group_pair_to_plot_changed(self): """ Handles the selected group pair being changed on the view """ if self.context.group_pair_context.selected == self._model.plotted_group: return self._model.plotted_group = self.context.group_pair_context.selected if not self._model.plot_figure: return self.plot_standard_workspaces() def plot_standard_workspaces(self): """ Plots the standard list of workspaces in a new plot window, closing any existing plot windows. :return: """ workspace_list = self.get_workspaces_to_plot( self.context.group_pair_context.selected, self._view.if_raw(), self._view.get_selected()) self._model.plot(workspace_list, self.get_plot_title(), self.get_domain(), self._force_redraw, self.context.window_title) self._force_redraw = False self._model.plotted_workspaces_inverse_binning = {workspace: self.context.group_pair_context.get_equivalent_group_pair(workspace) for workspace in workspace_list if self.context.group_pair_context.get_equivalent_group_pair(workspace)} if self.context.fitting_context.fit_list: self.handle_fit_completed() self.handle_plot_guess_changed() def handle_fit_completed(self): """ When a new fit is done adds the fit to the plotted workspaces if appropriate :return: """ if self._model.plot_figure is None: return if self.context.fitting_context.number_of_fits <= 1: for workspace_name in self._model.plotted_fit_workspaces: self._model.remove_workpace_from_plot(workspace_name) if self.context.fitting_context.fit_list: current_fit = self.context.fitting_context.fit_list[-1] combined_ws_list = self._model.plotted_workspaces + list(self._model.plotted_workspaces_inverse_binning.values()) list_of_output_workspaces_to_plot = [output for output, input in zip(current_fit.output_workspace_names, current_fit.input_workspaces) if input in combined_ws_list] list_of_output_workspaces_to_plot = list_of_output_workspaces_to_plot if list_of_output_workspaces_to_plot\ else [current_fit.output_workspace_names[-1]] else: list_of_output_workspaces_to_plot = [] for workspace_name in list_of_output_workspaces_to_plot: self._model.add_workspace_to_plot(workspace_name, 1, workspace_name + ': Fit') self._model.add_workspace_to_plot(workspace_name, 2, workspace_name + ': Diff') self._model.force_redraw() def get_workspaces_to_plot(self, current_group_pair, is_raw, plot_type): """ :param current_group_pair: The group/pair currently selected :param is_raw: Whether to use raw or rebinned data :param plot_type: Whether to plot counts or asymmetry :return: a list of workspace names """ if FREQ_PLOT_TYPE in plot_type: return self.get_freq_workspaces_to_plot(current_group_pair, plot_type) else: return self.get_time_workspaces_to_plot(current_group_pair, is_raw, plot_type) def get_freq_workspaces_to_plot(self, current_group_pair, plot_type): """ :param current_group_pair: The group/pair currently selected :param plot_type: Whether to plot counts or asymmetry :return: a list of workspace names """ try: runs = "" seperator = "" for run in self.context.data_context.current_runs: runs += seperator + str(run[0]) seperator = ", " workspace_list = self.context.get_names_of_frequency_domain_workspaces_to_fit( runs, current_group_pair, True, plot_type[len(FREQ_PLOT_TYPE):]) return workspace_list except AttributeError: return [] def get_time_workspaces_to_plot(self,current_group_pair, is_raw, plot_type): """ :param current_group_pair: The group/pair currently selected :param is_raw: Whether to use raw or rebinned data :param plot_type: Whether to plot counts or asymmetry :return: a list of workspace names """ try: if is_raw: workspace_list = self.context.group_pair_context[current_group_pair].get_asymmetry_workspace_names( self.context.data_context.current_runs) else: workspace_list = self.context.group_pair_context[current_group_pair].get_asymmetry_workspace_names_rebinned( self.context.data_context.current_runs) if plot_type == COUNTS_PLOT_TYPE: workspace_list = [item.replace(ASYMMETRY_PLOT_TYPE, COUNTS_PLOT_TYPE) for item in workspace_list if ASYMMETRY_PLOT_TYPE in item] return workspace_list except AttributeError: return [] def get_plot_title(self): """ Generates a title for the plot based on current instrument group and run numbers :return: """ flattened_run_list = [ item for sublist in self.context.data_context.current_runs for item in sublist] return self.context.data_context.instrument + ' ' + run_list_to_string(flattened_run_list) + ' ' + \ self.context.group_pair_context.selected def handle_rebin_options_set(self): if self.context._do_rebin(): self._view.set_raw_checkbox_state(False) else: self._view.set_raw_checkbox_state(True) def get_domain(self): if FREQ_PLOT_TYPE in self._view.get_selected(): return "Frequency" else: return "Time" def handle_instrument_changed(self): if self._model.plot_figure is not None: from matplotlib import pyplot as plt plt.close(self._model.plot_figure) def handle_plot_guess_changed(self): if self._model.plot_figure is None: return for guess in [ws for ws in self._model.plotted_fit_workspaces if '_guess' in ws]: self._model.remove_workpace_from_plot(guess) if self.context.fitting_context.plot_guess and self.context.fitting_context.guess_ws is not None: self._model.add_workspace_to_plot(self.context.fitting_context.guess_ws, workspace_index=1, label='Fit Function Guess') self._model.force_redraw()
class FocusPresenter(object): def __init__(self, model, view): self.model = model self.view = view self.worker = None self.calibration_observer = CalibrationObserver(self) # Observable Setup self.focus_run_notifier = GenericObservable() # Connect view signals to local methods. self.view.set_on_focus_clicked(self.on_focus_clicked) self.view.set_enable_controls_connection( self.set_focus_controls_enabled) # Variables from other GUI tabs. self.current_calibration = CalibrationInfo() self.instrument = "ENGINX" self.rb_num = None last_van_path = get_setting(output_settings.INTERFACES_SETTINGS_GROUP, output_settings.ENGINEERING_PREFIX, "last_vanadium_run") if last_van_path: self.view.set_van_file_text_with_search(last_van_path) def add_focus_subscriber(self, obs): self.focus_run_notifier.add_subscriber(obs) def on_focus_clicked(self): if not self._validate(): return regions_dict = self.current_calibration.create_focus_roi_dictionary() focus_paths = self.view.get_focus_filenames() van_path = self.view.get_vanadium_filename() if self._number_of_files_warning(focus_paths): self.start_focus_worker(focus_paths, van_path, self.view.get_plot_output(), self.rb_num, regions_dict) van_run = self.view.get_vanadium_run() set_setting(output_settings.INTERFACES_SETTINGS_GROUP, output_settings.ENGINEERING_PREFIX, "last_vanadium_run", van_run) def start_focus_worker(self, focus_paths: list, van_path: str, plot_output: bool, rb_num: str, regions_dict: dict) -> None: """ Focus data in a separate thread to stop the main GUI from hanging. :param focus_paths: List of paths to the files containing the data to focus. :param plot_output: True if the output should be plotted. :param rb_num: The RB Number from the main window (often an experiment id) :param regions_dict: Dictionary containing the regions to focus over, mapping region_name -> grouping_ws_name """ self.worker = AsyncTask(self.model.focus_run, (focus_paths, van_path, plot_output, self.instrument, rb_num, regions_dict), error_cb=self._on_worker_error, finished_cb=self._on_worker_success) self.set_focus_controls_enabled(False) self.worker.start() def _on_worker_success(self): self.emit_enable_button_signal() self.focus_run_notifier.notify_subscribers( self.model.get_last_focused_files()) def set_instrument_override(self, instrument): instrument = INSTRUMENT_DICT[instrument] self.view.set_instrument_override(instrument) self.instrument = instrument def set_rb_num(self, rb_num): self.rb_num = rb_num def _validate(self): """ Ensure that the worker is ready to be started. :return: True if the worker can be started safely. """ if self.view.is_searching(): create_error_message( self.view, "Mantid is searching for data files. Please wait.") return False if not self.view.get_focus_valid(): create_error_message(self.view, "Check run numbers/path is valid.") return False if not self.view.get_vanadium_valid(): create_error_message(self.view, "Check vanadium run number/path is valid.") return False if not self.current_calibration.is_valid(): create_error_message( self.view, "Create or Load a calibration via the Calibration tab before focusing." ) return False if self.current_calibration.get_instrument() != self.instrument: create_error_message( self.view, "Please make sure the selected instrument matches instrument for the current calibration.\n" "The instrument for the current calibration is: " + self.current_calibration.get_instrument()) return False return True def _number_of_files_warning(self, paths): if len( paths ) > 10: # Just a guess on the warning for now. May change in future. response = QMessageBox.warning( self.view, 'Engineering Diffraction - Warning', 'You are attempting to focus {} workspaces. This may take some time.\n\n Would you like to continue?' .format(len(paths)), QMessageBox.Ok | QMessageBox.Cancel) return response == QMessageBox.Ok else: return True def _on_worker_error(self, error_info): logger.error(str(error_info)) self.emit_enable_button_signal() def set_focus_controls_enabled(self, enabled): self.view.set_focus_button_enabled(enabled) self.view.set_plot_output_enabled(enabled) def emit_enable_button_signal(self): self.view.sig_enable_controls.emit(True) def update_calibration(self, calibration): """ Update the current calibration following an call from a CalibrationNotifier :param calibration: The new current calibration. """ self.current_calibration = calibration region_text = calibration.get_roi_text() self.view.set_region_display_text(region_text)
class GroupingTabPresenter(object): """ The grouping tab presenter is responsible for synchronizing the group and pair tables. It also maintains functionality which covers both groups/pairs ; e.g. loading/saving/updating data. """ @staticmethod def string_to_list(text): return run_string_to_list(text) def __init__(self, view, model, grouping_table_widget=None, pairing_table_widget=None): self._view = view self._model = model self.grouping_table_widget = grouping_table_widget self.pairing_table_widget = pairing_table_widget # Synchronize the two tables self._view.on_grouping_table_changed( self.pairing_table_widget.update_view_from_model) self._view.on_pairing_table_changed( self.grouping_table_widget.update_view_from_model) self._view.set_description_text(self.text_for_description()) self._view.on_add_pair_requested(self.add_pair_from_grouping_table) self._view.on_clear_grouping_button_clicked(self.on_clear_requested) self._view.on_load_grouping_button_clicked( self.handle_load_grouping_from_file) self._view.on_save_grouping_button_clicked( self.handle_save_grouping_file) self._view.on_default_grouping_button_clicked( self.handle_default_grouping_button_clicked) # multi period self._view.on_summed_periods_changed(self.handle_periods_changed) self._view.on_subtracted_periods_changed(self.handle_periods_changed) # monitors for loaded data changing self.loadObserver = GroupingTabPresenter.LoadObserver(self) self.instrumentObserver = GroupingTabPresenter.InstrumentObserver(self) # notifiers self.groupingNotifier = GroupingTabPresenter.GroupingNotifier(self) self.grouping_table_widget.on_data_changed(self.group_table_changed) self.pairing_table_widget.on_data_changed(self.pair_table_changed) self.enable_editing_notifier = GroupingTabPresenter.EnableEditingNotifier( self) self.disable_editing_notifier = GroupingTabPresenter.DisableEditingNotifier( self) self.calculation_finished_notifier = GenericObservable() self.guessAlphaObserver = GroupingTabPresenter.GuessAlphaObserver(self) self.pairing_table_widget.guessAlphaNotifier.add_subscriber( self.guessAlphaObserver) self.message_observer = GroupingTabPresenter.MessageObserver(self) self.gui_variables_observer = GroupingTabPresenter.GuiVariablesChangedObserver( self) self.enable_observer = GroupingTabPresenter.EnableObserver(self) self.disable_observer = GroupingTabPresenter.DisableObserver(self) self.update_view_from_model_observer = GenericObserver( self.update_view_from_model) def update_view_from_model(self): self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.hide_multiperiod_widget_if_data_single_period() n_periods = self._model.number_of_periods() self._view.set_period_number_in_period_label(n_periods) def show(self): self._view.show() def text_for_description(self): """ Generate the text for the description edit at the top of the widget. """ instrument = self._model.instrument n_detectors = self._model.num_detectors main_field = self._model.main_field_direction text = "{} , {} detectors, main field : {} to muon polarization".format( instrument, n_detectors, main_field) return text def update_description_text(self, description_text=''): if not description_text: description_text = self.text_for_description() self._view.set_description_text(description_text) def add_pair_from_grouping_table(self, group_name1="", group_name2=""): """ If user requests to add a pair from the grouping table. """ self.pairing_table_widget.handle_add_pair_button_clicked( group_name1, group_name2) def handle_guess_alpha(self, pair_name, group1_name, group2_name): """ Calculate alpha for the pair for which "Guess Alpha" button was clicked. """ if len(self._model._data.current_runs) > 1: run, index, ok_clicked = RunSelectionDialog.get_run( self._model._data.current_runs, self._model._data.instrument, self._view) if not ok_clicked: return run_to_use = self._model._data.current_runs[index] else: run_to_use = self._model._data.current_runs[0] try: ws1 = self._model.get_group_workspace(group1_name, run_to_use) ws2 = self._model.get_group_workspace(group2_name, run_to_use) except KeyError: self._view.display_warning_box( 'Group workspace not found, try updating all and then recalculating.' ) return ws = algorithm_utils.run_AppendSpectra(ws1, ws2) new_alpha = algorithm_utils.run_AlphaCalc({ "InputWorkspace": ws, "ForwardSpectra": [0], "BackwardSpectra": [1] }) self._model.update_pair_alpha(pair_name, new_alpha) self.pairing_table_widget.update_view_from_model() self.handle_update_all_clicked() def handle_load_grouping_from_file(self): # Only XML format file_filter = file_utils.filter_for_extensions(["xml"]) filename = self._view.show_file_browser_and_return_selection( file_filter, [""]) if filename == '': return groups, pairs, description, default = xml_utils.load_grouping_from_XML( filename) self._model.clear() for group in groups: try: self._model.add_group(group) except ValueError as error: self._view.display_warning_box(str(error)) for pair in pairs: if pair.forward_group in self._model.group_names and pair.backward_group in self._model.group_names: self._model.add_pair(pair) self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.update_description_text(description) self._model._context.group_pair_context.selected = default self.groupingNotifier.notify_subscribers() self.handle_update_all_clicked() def disable_editing(self): self._view.set_buttons_enabled(False) self.grouping_table_widget.disable_editing() self.pairing_table_widget.disable_editing() self.disable_editing_notifier.notify_subscribers() def enable_editing(self, result=None): self._view.set_buttons_enabled(True) self.grouping_table_widget.enable_editing() self.pairing_table_widget.enable_editing() self.enable_editing_notifier.notify_subscribers() def calculate_all_data(self): self._model.show_all_groups_and_pairs() def handle_update_all_clicked(self): self.update_thread = self.create_update_thread() self.update_thread.threadWrapperSetUp(self.disable_editing, self.handle_update_finished, self.error_callback) self.update_thread.start() def error_callback(self, error_message): self.enable_editing_notifier.notify_subscribers() self._view.display_warning_box(error_message) def handle_update_finished(self): self.enable_editing() self.groupingNotifier.notify_subscribers() self.calculation_finished_notifier.notify_subscribers() def handle_default_grouping_button_clicked(self): self._model.reset_groups_and_pairs_to_default() self._model.reset_selected_groups_and_pairs() self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.update_description_text() self.groupingNotifier.notify_subscribers() self.handle_update_all_clicked() self.plot_default_groups_or_pairs() def on_clear_requested(self): self._model.clear() self.grouping_table_widget.update_view_from_model() self.pairing_table_widget.update_view_from_model() self.update_description_text() def handle_new_data_loaded(self): if self._model.is_data_loaded(): self._model._context.show_raw_data() self.update_view_from_model() self.update_description_text() self.handle_update_all_clicked() self.plot_default_groups_or_pairs() else: self.on_clear_requested() def hide_multiperiod_widget_if_data_single_period(self): if self._model.is_data_multi_period(): self._view.multi_period_widget_hidden(False) else: self._view.multi_period_widget_hidden(True) def update_period_edits(self): summed_periods = self._model.get_summed_periods() subtracted_periods = self._model.get_subtracted_periods() self._view.set_summed_periods(",".join( [str(p) for p in summed_periods])) self._view.set_subtracted_periods(",".join( [str(p) for p in subtracted_periods])) def handle_periods_changed(self): self._view.summed_period_edit.blockSignals(True) self._view.subtracted_period_edit.blockSignals(True) summed = self.string_to_list(self._view.get_summed_periods()) subtracted = self.string_to_list(self._view.get_subtracted_periods()) subtracted = [i for i in subtracted if i not in summed] n_periods = self._model.number_of_periods() bad_periods = [period for period in summed if (period > n_periods) or period == 0] + \ [period for period in subtracted if (period > n_periods) or period == 0] if len(bad_periods) > 0: self._view.display_warning_box( "The following periods are invalid : " + ",".join([str(period) for period in bad_periods])) summed = [ p for p in summed if (p <= n_periods) and p > 0 and p not in bad_periods ] if not summed: summed = [1] subtracted = [ p for p in subtracted if (p <= n_periods) and p > 0 and p not in bad_periods ] self._model.update_periods(summed, subtracted) self.update_period_edits() self._view.summed_period_edit.blockSignals(False) self._view.subtracted_period_edit.blockSignals(False) def handle_save_grouping_file(self): filename = self._view.show_file_save_browser_and_return_selection() if filename != "": xml_utils.save_grouping_to_XML( self._model.groups, self._model.pairs, filename, description=self._view.get_description_text()) def create_update_thread(self): self._update_model = ThreadModelWrapper(self.calculate_all_data) return thread_model.ThreadModel(self._update_model) def plot_default_groups_or_pairs(self): # if we have no pairs or groups selected, generate a default plot if len(self._model.selected_pairs + self._model.selected_groups) == 0: if len(self._model.pairs ) > 0: # if we have pairs - then plot all pairs self.pairing_table_widget.plot_default_case() else: # else plot groups self.grouping_table_widget.plot_default_case() # ------------------------------------------------------------------------------------------------------------------ # Observer / Observable # ------------------------------------------------------------------------------------------------------------------ def group_table_changed(self): self.handle_update_all_clicked() def pair_table_changed(self): self.handle_update_all_clicked() class LoadObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_new_data_loaded() class InstrumentObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.on_clear_requested() class GuessAlphaObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_guess_alpha(arg[0], arg[1], arg[2]) class GuiVariablesChangedObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.handle_update_all_clicked() class GroupingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs) class MessageObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer._view.display_warning_box(arg) class EnableObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.enable_editing() class DisableObserver(Observer): def __init__(self, outer): Observer.__init__(self) self.outer = outer def update(self, observable, arg): self.outer.disable_editing() class DisableEditingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs) class EnableEditingNotifier(Observable): def __init__(self, outer): Observable.__init__(self) self.outer = outer # handle to containing class def notify_subscribers(self, *args, **kwargs): Observable.notify_subscribers(self, *args, **kwargs)