Ejemplo n.º 1
0
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())
Ejemplo n.º 5
0
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()
Ejemplo n.º 7
0
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("/")
Ejemplo n.º 8
0
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)
        ]
Ejemplo n.º 9
0
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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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'
            )
Ejemplo n.º 14
0
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)
Ejemplo n.º 16
0
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
Ejemplo n.º 17
0
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()
Ejemplo n.º 18
0
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()
Ejemplo n.º 19
0
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()
Ejemplo n.º 21
0
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
Ejemplo n.º 22
0
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)
Ejemplo n.º 23
0
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'
            )
Ejemplo n.º 25
0
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)
Ejemplo n.º 27
0
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()
Ejemplo n.º 29
0
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)
Ejemplo n.º 30
0
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)