From b1a1b99d5d30f606eb7c5cf69df5a10115999aed Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Thu, 25 Sep 2025 20:20:07 +0200 Subject: [PATCH 1/5] Temporarily shut off plotting during data changes. Closes #192 --- src/petab_gui/controllers/mother_controller.py | 11 +++++++++-- src/petab_gui/models/pandas_table_model.py | 4 ++++ src/petab_gui/views/simple_plot_view.py | 9 +++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py index 7ee6e5a..3eb8790 100644 --- a/src/petab_gui/controllers/mother_controller.py +++ b/src/petab_gui/controllers/mother_controller.py @@ -155,11 +155,11 @@ def __init__(self, view, model: PEtabModel): self.actions = self.setup_actions() self.view.setup_toolbar(self.actions) + self.plotter = None + self.init_plotter() self.setup_connections() self.setup_task_bar() self.setup_context_menu() - self.plotter = None - self.init_plotter() @property def window_title(self): @@ -204,6 +204,13 @@ def setup_connections(self): ], ) ) + # Plotting Disable Temporarily + for controller in self.controllers: + if controller == self.sbml_controller: + continue + controller.model.plotting_needs_break.connect( + self.plotter.disable_plotting + ) # Add new condition or observable self.model.measurement.relevant_id_changed.connect( lambda x, y, z: self.observable_controller.maybe_add_observable( diff --git a/src/petab_gui/models/pandas_table_model.py b/src/petab_gui/models/pandas_table_model.py index 310e020..806c49b 100644 --- a/src/petab_gui/models/pandas_table_model.py +++ b/src/petab_gui/models/pandas_table_model.py @@ -45,6 +45,7 @@ class PandasTableModel(QAbstractTableModel): cell_needs_validation = Signal(int, int) # row, column something_changed = Signal(bool) inserted_row = Signal(QModelIndex) + plotting_needs_break = Signal(bool) def __init__( self, @@ -310,6 +311,7 @@ def setData( if is_invalid(value) or value == "": value = None + self.plotting_needs_break.emit(True) # Temp disable plotting multi_row_change = False if check_multi: # check whether multiple rows but only one column is selected @@ -318,6 +320,7 @@ def setData( self.undo_stack.beginMacro("Set data") success = self._set_data_single(index, value) self.undo_stack.endMacro() + self.plotting_needs_break.emit(False) return success # multiple rows but only one column is selected all_set = [] @@ -325,6 +328,7 @@ def setData( for index in selected: all_set.append(self._set_data_single(index, value)) self.undo_stack.endMacro() + self.plotting_needs_break.emit(False) return all(all_set) def _set_data_single(self, index, value): diff --git a/src/petab_gui/views/simple_plot_view.py b/src/petab_gui/views/simple_plot_view.py index fcce794..896221a 100644 --- a/src/petab_gui/views/simple_plot_view.py +++ b/src/petab_gui/views/simple_plot_view.py @@ -75,6 +75,7 @@ def __init__(self, parent=None): self.update_timer.setSingleShot(True) self.update_timer.timeout.connect(self.plot_it) self.observable_to_subplot = {} + self.no_plotting_rn = False def initialize( self, meas_proxy, sim_proxy, cond_proxy, vis_proxy, petab_model @@ -104,6 +105,8 @@ def initialize( self.plot_it() def plot_it(self): + if self.no_plotting_rn: + return if not self.meas_proxy or not self.cond_proxy: return if not self.isVisible(): @@ -336,6 +339,12 @@ def plot_residuals(self): # fig_fit.tight_layout() create_plot_tab(fig_fit, self, "Goodness of Fit") + def disable_plotting(self, disable: bool): + """Set self.no_plotting_rn to enable/disable plotting.""" + self.no_plotting_rn = disable + if not self.no_plotting_rn: + self._debounced_plot() + class MeasurementHighlighter: def __init__(self): From 5390c7564a6691b085a256d31e5ddbc51626e84e Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Thu, 25 Sep 2025 20:29:37 +0200 Subject: [PATCH 2/5] Rename yValues when observable is renamed --- .../controllers/mother_controller.py | 7 +++ .../controllers/table_controllers.py | 44 ++++++++++--------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py index 3eb8790..0626505 100644 --- a/src/petab_gui/controllers/mother_controller.py +++ b/src/petab_gui/controllers/mother_controller.py @@ -194,6 +194,13 @@ def setup_connections(self): column_names="observableId", ) ) + self.observable_controller.observable_2be_renamed.connect( + partial( + self.visualization_controller.rename_value, + column_names="yValues", + ) + ) + # Maybe TODO: add renaming dataset id? # Rename Condition self.condition_controller.condition_2be_renamed.connect( partial( diff --git a/src/petab_gui/controllers/table_controllers.py b/src/petab_gui/controllers/table_controllers.py index 2b76d18..0156c59 100644 --- a/src/petab_gui/controllers/table_controllers.py +++ b/src/petab_gui/controllers/table_controllers.py @@ -588,30 +588,10 @@ def save_table(self, file_name): f"Failed to save table: {str(e)}", ) - -class MeasurementController(TableController): - """Controller of the Measurement table.""" - - @linter_wrapper - def check_petab_lint( - self, - row_data: pd.DataFrame = None, - row_name: str = None, - col_name: str = None, - ): - """Check a number of rows of the model with petablint.""" - if row_data is None: - row_data = self.model.get_df() - observable_df = self.mother_controller.model.observable.get_df() - return petab.check_measurement_df( - row_data, - observable_df=observable_df, - ) - def rename_value( self, old_id: str, new_id: str, column_names: str | list[str] ): - """Rename the values in the measurement_df. + """Rename the values in the dataframe. Triggered by changes in the original observable_df or condition_df id. @@ -621,6 +601,8 @@ def rename_value( The old id, which was changed. new_id: The new id. + column_names: + The column or list of columns in which the id should be changed. """ if not isinstance(column_names, list): column_names = [column_names] @@ -647,6 +629,26 @@ def rename_value( # Emit change signal self.model.something_changed.emit(True) + +class MeasurementController(TableController): + """Controller of the Measurement table.""" + + @linter_wrapper + def check_petab_lint( + self, + row_data: pd.DataFrame = None, + row_name: str = None, + col_name: str = None, + ): + """Check a number of rows of the model with petablint.""" + if row_data is None: + row_data = self.model.get_df() + observable_df = self.mother_controller.model.observable.get_df() + return petab.check_measurement_df( + row_data, + observable_df=observable_df, + ) + def copy_noise_parameters( self, observable_id: str, condition_id: str | None = None ) -> str: From 4af69ec5f1b281c5ab8672f0c201a2ac68c3a9c6 Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Mon, 29 Sep 2025 12:42:55 +0200 Subject: [PATCH 3/5] Only check datatype for columns, not index. Fixes #180 --- src/petab_gui/controllers/default_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/petab_gui/controllers/default_handler.py b/src/petab_gui/controllers/default_handler.py index 6ccacd2..99428dc 100644 --- a/src/petab_gui/controllers/default_handler.py +++ b/src/petab_gui/controllers/default_handler.py @@ -66,7 +66,9 @@ def get_default( default_value = column_config.get(DEFAULT_VALUE, "") if strategy == USE_DEFAULT: - if np.issubdtype(self.model.dtypes[column_name], np.floating): + if column_name != self.model.index.name and np.issubdtype( + self.model.dtypes[column_name], np.floating + ): return float(default_value) return default_value if strategy == NO_DEFAULT: From 50795570b5f8e005e0a6f2b9edfaff4ef30e5574 Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Mon, 29 Sep 2025 13:10:17 +0200 Subject: [PATCH 4/5] Make values redoable --- src/petab_gui/commands.py | 43 +++++++++++++++++++ .../controllers/table_controllers.py | 27 ++---------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py index 621409c..60a3e01 100644 --- a/src/petab_gui/commands.py +++ b/src/petab_gui/commands.py @@ -318,3 +318,46 @@ def _apply(self, src, dst): self.model.dataChanged.emit( self.model_index, self.model_index, [Qt.DisplayRole] ) + + +class RenameValueCommand(QUndoCommand): + """Command to rename values in specified columns.""" + + def __init__( + self, model, old_id: str, new_id: str, column_names: str | list[str] + ): + super().__init__(f"Rename value {old_id} → {new_id}") + self.model = model + self.old_id = old_id + self.new_id = new_id + self.column_names = ( + column_names if isinstance(column_names, list) else [column_names] + ) + self.changes = {} # {(row_idx, col_name): (old_val, new_val)} + + df = self.model._data_frame + for col_name in self.column_names: + mask = df[col_name].eq(self.old_id) + for row_idx in df.index[mask]: + self.changes[(row_idx, col_name)] = (self.old_id, self.new_id) + + def redo(self): + self._apply_changes(use_new=True) + + def undo(self): + self._apply_changes(use_new=False) + + def _apply_changes(self, use_new: bool): + df = self.model._data_frame + for (row_idx, col_name), (old_val, new_val) in self.changes.items(): + df.at[row_idx, col_name] = new_val if use_new else old_val + + if self.changes: + rows = [df.index.get_loc(row) for (row, _) in self.changes] + cols = [df.columns.get_loc(col) + 1 for (_, col) in self.changes] + top_left = self.model.index(min(rows), min(cols)) + bottom_right = self.model.index(max(rows), max(cols)) + self.model.dataChanged.emit( + top_left, bottom_right, [Qt.DisplayRole, Qt.EditRole] + ) + self.model.something_changed.emit(True) diff --git a/src/petab_gui/controllers/table_controllers.py b/src/petab_gui/controllers/table_controllers.py index 0156c59..7e38e0a 100644 --- a/src/petab_gui/controllers/table_controllers.py +++ b/src/petab_gui/controllers/table_controllers.py @@ -18,6 +18,7 @@ ) from ..C import COLUMN, INDEX +from ..commands import RenameValueCommand from ..models.pandas_table_model import ( PandasTableFilterProxy, PandasTableModel, @@ -604,30 +605,8 @@ def rename_value( column_names: The column or list of columns in which the id should be changed. """ - if not isinstance(column_names, list): - column_names = [column_names] - - for col_name in column_names: - # Find occurrences - mask = self.model._data_frame[col_name].eq(old_id) - if not mask.any(): - continue - - self.model._data_frame.loc[mask, col_name] = new_id - first_row, last_row = ( - mask.idxmax(), - mask[::-1].idxmax(), - ) - top_left = self.model.index(first_row, 1) - bottom_right = self.model.index( - last_row, self.model.columnCount() - 1 - ) - self.model.dataChanged.emit( - top_left, bottom_right, [Qt.DisplayRole, Qt.EditRole] - ) - - # Emit change signal - self.model.something_changed.emit(True) + command = RenameValueCommand(self.model, old_id, new_id, column_names) + self.undo_stack.push(command) class MeasurementController(TableController): From 4f4748bb58da0a9375b7df6f93bccd524a03454f Mon Sep 17 00:00:00 2001 From: PaulJonasJost Date: Mon, 29 Sep 2025 13:14:44 +0200 Subject: [PATCH 5/5] Aligning default values --- src/petab_gui/settings_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/petab_gui/settings_manager.py b/src/petab_gui/settings_manager.py index d20c542..9bb899a 100644 --- a/src/petab_gui/settings_manager.py +++ b/src/petab_gui/settings_manager.py @@ -286,7 +286,7 @@ def save_current_settings(self): def default_col_config(self): """Return default config for new columns.""" - return {"strategy": NO_DEFAULT} + return settings_manager.get_table_defaults(self.table_name) class SettingsDialog(QDialog): @@ -332,7 +332,8 @@ def init_general_page(self): # Header header = QLabel("Profile") desc = QLabel( - "These information can be automatically used when saving a COMBINE archive." + "These information can be automatically used when saving " + "a COMBINE archive." ) desc.setWordWrap(True)