diff --git a/rascal2/settings.py b/rascal2/settings.py index c620740..699500f 100644 --- a/rascal2/settings.py +++ b/rascal2/settings.py @@ -123,6 +123,9 @@ class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True live_recalculate: bool = Field( default=True, title=SettingsGroups.General, description="Auto-run simulation when parameter values change." ) + show_stop_calculation_warning: bool = Field( + default=True, title=SettingsGroups.General, description="Warn when Bayesian calculation is stopped manually." + ) clear_terminal: bool = Field( default=True, title=SettingsGroups.Terminal, description="Clear Terminal when Run Starts" diff --git a/rascal2/ui/presenter.py b/rascal2/ui/presenter.py index 6b2d0c4..ec9fbcb 100644 --- a/rascal2/ui/presenter.py +++ b/rascal2/ui/presenter.py @@ -168,7 +168,13 @@ def export_fits(self): def interrupt_terminal(self): """Send an interrupt signal to the RAT runner.""" - self.runner.interrupt() + if self.model.controls.procedure in [rat.utils.enums.Procedures.Simplex, rat.utils.enums.Procedures.DE]: + self.model.controls.sendStopEvent() + else: + if not self.view.show_confirm_stop_calculation_dialog(): + return + if self.runner.process.is_alive(): + self.runner.interrupt() def quick_run(self, project=None): """Run rat calculation with calculate procedure on the given project. @@ -206,6 +212,7 @@ def run(self): # hide bayes plots button so users can't open plots during run self.view.plot_widget.bayes_plots_button.setVisible(False) + self.model.controls.initialise_IPC() rat_inputs = rat.inputs.make_input(self.model.project, self.model.controls) display_on = self.model.controls.display != rat.utils.enums.Display.Off @@ -226,6 +233,7 @@ def handle_results(self): ) ) self.view.handle_results(self.runner.results) + self.model.controls.delete_IPC() def handle_interrupt(self): """Handle a RAT run being interrupted.""" @@ -234,6 +242,7 @@ def handle_interrupt(self): else: self.view.logging.error("RAT run failed with exception.\n", exc_info=self.runner.error) self.view.handle_results() + self.model.controls.delete_IPC() def handle_event(self): """Handle event data produced by the RAT run.""" diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index c4c1190..f849ebc 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -495,6 +495,36 @@ def show_confirm_dialog(self, title: str, message: str) -> bool: return reply == QtWidgets.QMessageBox.StandardButton.Ok + def show_confirm_stop_calculation_dialog(self) -> bool: + """Ask the user to confirm stopping the calculation. + + Returns + ------- + bool + Whether the confirmation was affirmative. + """ + if not self.settings.show_stop_calculation_warning: + return True + + message_box = QtWidgets.QMessageBox(self) + message_box.setWindowTitle("Confirm Stop?") + message_box.setText("For Bayesian procedures, stopping will lose all progress. Confirm Stop?") + message_box.setIcon(QtWidgets.QMessageBox.Icon.Question) + + yes_button = message_box.addButton("Stop", QtWidgets.QMessageBox.ButtonRole.YesRole) + no_button = message_box.addButton(QtWidgets.QMessageBox.StandardButton.Cancel) + message_box.setDefaultButton(no_button) + + no_show_check_box = QtWidgets.QCheckBox("Do not show this again") + message_box.setCheckBox(no_show_check_box) + no_show_check_box.toggled.connect(lambda val: setattr(self.settings, "show_stop_calculation_warning", not val)) + + # Make this save to general settings + + message_box.exec() + + return message_box.clickedButton() == yes_button + def show_unsaved_dialog(self, message: str) -> UnsavedReply: """Warn the user of unsaved changes, and ask whether to save those changes. diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index 7c4aa7a..8945f47 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -420,10 +420,6 @@ def plot(self, project: ratapi.Project, results: ratapi.outputs.Results | ratapi results : Union[ratapi.outputs.Results, ratapi.outputs.BayesResults] The calculation results. """ - if project is None or results is None: - self.clear() - return - data = ratapi.events.PlotEventData() data.modelType = project.model @@ -456,6 +452,7 @@ def plot_event(self, data: ratapi.events.PlotEventData | None = None): return show_legend = self.show_legend.isChecked() if self.current_plot_data.contrastNames else False + self.figure.clear() self.update_figure_size() self.figure.tight_layout() ratapi.plotting.plot_ref_sld_helper( diff --git a/tests/ui/test_presenter.py b/tests/ui/test_presenter.py index 80aa9e3..352a427 100644 --- a/tests/ui/test_presenter.py +++ b/tests/ui/test_presenter.py @@ -81,12 +81,31 @@ def test_controls_validation_error(presenter, param, value): @patch("rascal2.ui.presenter.RATRunner") def test_run_and_interrupt(mock_runner, mock_inputs, presenter): """Test that the runner can be started and interrupted.""" + assert presenter.model.controls._IPCFilePath == "" presenter.run() - presenter.interrupt_terminal() - mock_inputs.assert_called_once() presenter.runner.start.assert_called_once() - presenter.runner.interrupt.assert_called_once() + assert presenter.model.controls._IPCFilePath != "" + + presenter.view.show_confirm_stop_calculation_dialog = MagicMock(return_value=False) + presenter.interrupt_terminal() + presenter.runner.interrupt.assert_not_called() + + presenter.view.show_confirm_stop_calculation_dialog = MagicMock(return_value=True) + presenter.runner.process.is_alive.return_value = False + presenter.interrupt_terminal() + presenter.runner.interrupt.assert_not_called() + + presenter.runner.process.is_alive.return_value = True + presenter.interrupt_terminal() + presenter.runner.interrupt.assert_called() + + with open(presenter.model.controls._IPCFilePath) as f: + assert f.readline() == "\x00" + presenter.model.controls.procedure = "simplex" + presenter.interrupt_terminal() + with open(presenter.model.controls._IPCFilePath) as f: + assert f.readline() == "\x01" @patch("rascal2.core.commands.SaveCalculationOutputs")