diff --git a/src/petab_gui/C.py b/src/petab_gui/C.py
index e7acf26..eb412af 100644
--- a/src/petab_gui/C.py
+++ b/src/petab_gui/C.py
@@ -67,6 +67,7 @@
MIN_COLUMN = "use column min"
MAX_COLUMN = "use column max"
MODE = "use most frequent"
+SBML_LOOK = "sbml value"
STRATEGIES_DEFAULT = [COPY_FROM, USE_DEFAULT, NO_DEFAULT]
STRATEGIES_DEFAULT_EXT = STRATEGIES_DEFAULT + [MODE]
STRATEGIES_DEFAULT_ALL = STRATEGIES_DEFAULT_EXT + [MIN_COLUMN, MAX_COLUMN]
@@ -77,6 +78,7 @@
MIN_COLUMN: "Use the minimum value of the column",
MAX_COLUMN: "Use the maximum value of the column",
MODE: "Use the most frequent value of the column",
+ SBML_LOOK: "Use the value from the SBML model",
}
SOURCE_COLUMN = "source_column"
DEFAULT_VALUE = "default_value"
@@ -96,7 +98,7 @@
"parameterScale": [USE_DEFAULT, NO_DEFAULT, MODE],
"lowerBound": [MIN_COLUMN, MAX_COLUMN, USE_DEFAULT, NO_DEFAULT, MODE],
"upperBound": [MAX_COLUMN, MAX_COLUMN, USE_DEFAULT, NO_DEFAULT, MODE],
- "nominalValue": [USE_DEFAULT, NO_DEFAULT],
+ "nominalValue": [USE_DEFAULT, NO_DEFAULT, SBML_LOOK],
"estimate": [USE_DEFAULT, NO_DEFAULT, MODE],
}
ALLOWED_STRATEGIES_COND = {
@@ -157,6 +159,9 @@
"estimate": {
"strategy": USE_DEFAULT, DEFAULT_VALUE: 1
},
+ "nominalValue": {
+ "strategy": SBML_LOOK
+ },
}
DEFAULT_COND_CONFIG = {
"conditionId": {
diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py
index 34336d2..2563c1a 100644
--- a/src/petab_gui/commands.py
+++ b/src/petab_gui/commands.py
@@ -99,7 +99,7 @@ def redo(self):
position = df.shape[0] - 1 # insert *before* the auto-row
self.model.beginInsertRows(QModelIndex(), position, position + len(self.row_indices) - 1)
for i, idx in enumerate(self.row_indices):
- df.loc[idx] = [""] * df.shape[1]
+ df.loc[idx] = [np.nan] * df.shape[1]
self.model.endInsertRows()
else:
self.model.beginRemoveRows(QModelIndex(), min(self.row_indices), max(self.row_indices))
diff --git a/src/petab_gui/controllers/default_handler.py b/src/petab_gui/controllers/default_handler.py
index 3c98772..1454f93 100644
--- a/src/petab_gui/controllers/default_handler.py
+++ b/src/petab_gui/controllers/default_handler.py
@@ -5,11 +5,11 @@
from collections import Counter
from ..C import (COPY_FROM, USE_DEFAULT, NO_DEFAULT, MIN_COLUMN, MAX_COLUMN,
- MODE, DEFAULT_VALUE, SOURCE_COLUMN, STRATEGIES_DEFAULT)
+ MODE, DEFAULT_VALUE, SOURCE_COLUMN, SBML_LOOK)
class DefaultHandlerModel:
- def __init__(self, model, config):
+ def __init__(self, model, config, sbml_model = None):
"""
Initialize the handler for the model.
:param model: The PandasTable Model containing the Data.
@@ -20,12 +20,21 @@ def __init__(self, model, config):
self.model = model._data_frame
self.config = config
self.model_index = self.model.index.name
+ self._sbml_model = sbml_model
- def get_default(self, column_name, row_index=None):
+ def get_default(
+ self,
+ column_name,
+ row_index=None,
+ par_scale=None,
+ changed: dict | None = None,
+ ):
"""
Get the default value for a column based on its strategy.
:param column_name: The name of the column to compute the default for.
:param row_index: Optional index of the row (needed for some strategies).
+ :param par_scale: Optional parameter scale (needed for some strategies).
+ :param changed: Optional tuple containing the column name and index of the changed cell.
:return: The computed default value.
"""
source_column = column_name
@@ -40,40 +49,64 @@ def get_default(self, column_name, row_index=None):
default_value = column_config.get(DEFAULT_VALUE, "")
if strategy == USE_DEFAULT:
+ if self.model.dtypes[column_name] == float:
+ return float(default_value)
return default_value
elif strategy == NO_DEFAULT:
return ""
elif strategy == MIN_COLUMN:
- return self._min_column(column_name)
+ return self._min_column(column_name, par_scale)
elif strategy == MAX_COLUMN:
- return self._max_column(column_name)
+ return self._max_column(column_name, par_scale)
elif strategy == COPY_FROM:
- return self._copy_column(column_name, column_config, row_index)
+ return self._copy_column(
+ column_name, column_config, row_index, changed
+ )
elif strategy == MODE:
column_config[SOURCE_COLUMN] = source_column
return self._majority_vote(column_name, column_config)
+ elif strategy == SBML_LOOK:
+ return self._sbml_lookup(row_index)
else:
raise ValueError(f"Unknown strategy '{strategy}' for column '{column_name}'.")
- def _min_column(self, column_name):
- if column_name in self.model:
- column_data = self.model[column_name].replace("", np.nan).dropna()
- if not column_data.empty:
- return column_data.min()
- return ""
+ def _min_column(self, column_name, par_scale=None):
+ if column_name not in self.model:
+ return ""
+ column_data = self.model[column_name].replace("", np.nan).dropna()
+ if column_name in ["upperBound", "lowerBound"]:
+ column_data = column_data.loc[
+ self.model["parameterScale"] == par_scale
+ ]
+ if not column_data.empty:
+ return column_data.min()
- def _max_column(self, column_name):
- if column_name in self.model:
- column_data = self.model[column_name].replace("", np.nan).dropna()
- if not column_data.empty:
- return column_data.max()
- return ""
+ def _max_column(self, column_name, par_scale=None):
+ if column_name not in self.model:
+ return ""
+ column_data = self.model[column_name].replace("", np.nan).dropna()
+ if column_name in ["upperBound", "lowerBound"]:
+ column_data = column_data.loc[
+ self.model["parameterScale"] == par_scale
+ ]
+ if not column_data.empty:
+ return column_data.max()
- def _copy_column(self, column_name, config, row_index):
+ def _copy_column(
+ self,
+ column_name,
+ config,
+ row_index,
+ changed: dict | None = None
+ ):
+ """Copy the value from another column in the same row."""
source_column = config.get(SOURCE_COLUMN, column_name)
source_column_valid = (
source_column in self.model or source_column == self.model_index
)
+ if changed:
+ if source_column in changed.keys():
+ return changed[source_column]
if source_column and source_column_valid and row_index is not None:
prefix = config.get("prefix", "")
if row_index in self.model.index:
@@ -100,3 +133,20 @@ def _majority_vote(self, column_name, config):
value_counts = Counter(valid_values)
return value_counts.most_common(1)[0][0]
return ""
+
+ def _sbml_lookup(self, row_key):
+ """Use the most frequent value in the column as the default.
+
+ Defaults to last used value in case of a tie.
+ """
+ if self._sbml_model is None:
+ return 1
+ if row_key is None:
+ return 1
+ curr_model = self._sbml_model.get_current_sbml_model()
+ if curr_model is None:
+ return 1
+ parameters = curr_model.get_valid_parameters_for_parameter_table()
+ if row_key not in list(parameters):
+ return 1
+ return curr_model.get_parameter_value(row_key)
diff --git a/src/petab_gui/controllers/logger_controller.py b/src/petab_gui/controllers/logger_controller.py
index 9fd7f96..e791f51 100644
--- a/src/petab_gui/controllers/logger_controller.py
+++ b/src/petab_gui/controllers/logger_controller.py
@@ -32,7 +32,8 @@ def log_message(self, message, color="black", loglevel=1):
return
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
full_message = \
- f"[{timestamp}]\t {message}"
+ (f"[{timestamp}]\t "
+ f"{message}")
for view in self.views:
view.logger.append(full_message)
diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py
index 2f1471f..605803b 100644
--- a/src/petab_gui/controllers/mother_controller.py
+++ b/src/petab_gui/controllers/mother_controller.py
@@ -186,6 +186,10 @@ def setup_connections(self):
settings_manager.new_log_message.connect(
self.logger.log_message
)
+ # Update Parameter SBML Model
+ self.sbml_controller.overwritten_model.connect(
+ self.parameter_controller.update_handler_sbml
+ )
def setup_actions(self):
"""Setup actions for the main controller."""
diff --git a/src/petab_gui/controllers/sbml_controller.py b/src/petab_gui/controllers/sbml_controller.py
index 217e03c..1cfdfeb 100644
--- a/src/petab_gui/controllers/sbml_controller.py
+++ b/src/petab_gui/controllers/sbml_controller.py
@@ -34,6 +34,7 @@ def __init__(
The main controller of the application. Needed for signal
forwarding.
"""
+ super().__init__()
self.view = view
self.model = model
self.logger = logger
@@ -119,7 +120,8 @@ def overwrite_sbml(self, file_path=None):
self.view.antimony_text_edit.setPlainText(
self.model.antimony_text
)
- # self.overwritten_model.emit() # Deactivated for now. Discuss!
+
+ self.overwritten_model.emit()
self.logger.log_message(
"SBML model successfully opened and overwritten.",
color="green"
diff --git a/src/petab_gui/controllers/table_controllers.py b/src/petab_gui/controllers/table_controllers.py
index c3f8a27..ed771d6 100644
--- a/src/petab_gui/controllers/table_controllers.py
+++ b/src/petab_gui/controllers/table_controllers.py
@@ -978,6 +978,9 @@ def update_handler_model(self):
"""Update the handler model."""
self.model.default_handler.model = self.model._data_frame
+ def update_handler_sbml(self):
+ self.model.default_handler._sbml_model = self.mother_controller.model.sbml
+
def setup_completers(self):
"""Set completers for the parameter table."""
table_view = self.view.table_view
diff --git a/src/petab_gui/controllers/utils.py b/src/petab_gui/controllers/utils.py
index 7669022..a608604 100644
--- a/src/petab_gui/controllers/utils.py
+++ b/src/petab_gui/controllers/utils.py
@@ -6,6 +6,7 @@
import functools
import pandas as pd
import re
+import html
from ..settings_manager import settings_manager
from ..C import COMMON_ERRORS
@@ -23,6 +24,7 @@ def wrapper(self, row_data: pd.DataFrame = None, row_name:
return True
except Exception as e:
err_msg = filtered_error(e)
+ err_msg = html.escape(err_msg)
if additional_error_check:
if "Missing parameter(s)" in err_msg:
match = re.search(r"\{(.+?)\}", err_msg)
diff --git a/src/petab_gui/models/pandas_table_model.py b/src/petab_gui/models/pandas_table_model.py
index 83e1de4..dbcd7fe 100644
--- a/src/petab_gui/models/pandas_table_model.py
+++ b/src/petab_gui/models/pandas_table_model.py
@@ -222,22 +222,22 @@ def _set_data_single(self, index, value):
# Special ID emitters
if column_name == "observableId":
- self._push_change_and_notify(row, column, column_name, old_value, value)
- self.relevant_id_changed.emit(value, old_value, "observable")
if fill_with_defaults:
- self.get_default_values(index)
+ self.get_default_values(index, {column_name: value})
+ self.relevant_id_changed.emit(value, old_value, "observable")
+ self._push_change_and_notify(row, column, column_name, old_value, value)
return True
if column_name in ["conditionId", "simulationConditionId", "preequilibrationConditionId"]:
if fill_with_defaults:
- self.get_default_values(index)
- self._push_change_and_notify(row, column, column_name, old_value, value)
+ self.get_default_values(index, {column_name: value})
self.relevant_id_changed.emit(value, old_value, "condition")
+ self._push_change_and_notify(row, column, column_name, old_value, value)
return True
# Default value setting
if fill_with_defaults:
- self.get_default_values(index)
+ self.get_default_values(index, {column_name: value})
self._push_change_and_notify(row, column, column_name, old_value, value)
return True
@@ -249,9 +249,9 @@ def _push_change_and_notify(
(self._data_frame.index[row], column_name): (old_value, new_value)
}
self.undo_stack.push(ModifyDataFrameCommand(self, change))
- self.cell_needs_validation.emit(row, column)
self.dataChanged.emit(self.index(row, column), self.index(row, column),
[Qt.DisplayRole])
+ self.cell_needs_validation.emit(row, column)
self.something_changed.emit(True)
def clear_cells(self, selected):
@@ -266,8 +266,15 @@ def handle_named_index(self, index, value):
"""Handle the named index column."""
pass
- def get_default_values(self, index):
- """Return the default values for a the row in a new index."""
+ def get_default_values(self, index, changed: dict | None = None):
+ """Return the default values for the row in a new index.
+
+ Parameters
+ ----------
+ index: QModelIndex, index where the first change occurs
+ changed:
+ the changes made to the DataFrame, that have not yet been registered
+ """
pass
def replace_text(self, old_text: str, new_text: str):
@@ -517,7 +524,10 @@ def endResetModel(self):
"""Override endResetModel to reset the default handler."""
super().endResetModel()
self.config = settings_manager.get_table_defaults(self.table_type)
- self.default_handler = DefaultHandlerModel(self, self.config)
+ sbml_model = self.default_handler._sbml_model
+ self.default_handler = DefaultHandlerModel(
+ self, self.config, sbml_model=sbml_model
+ )
def fill_row(self, row_position: int, data: dict):
"""Fill a row with data.
@@ -556,7 +566,10 @@ def fill_row(self, row_position: int, data: dict):
self.undo_stack.push(ModifyDataFrameCommand(
self, changes, "Fill values"
))
- self.fill_defaults.emit(self.index(row_position, 0))
+ self.get_default_values(
+ self.index(row_position, 0),
+ changed=changes,
+ )
class IndexedPandasTableModel(PandasTableModel):
@@ -573,7 +586,7 @@ def __init__(self, data_frame, allowed_columns, table_type, parent=None):
self._has_named_index = True
self.column_offset = 1
- def get_default_values(self, index):
+ def get_default_values(self, index, changed: dict | None = None):
"""Return the default values for a the row in a new index."""
row_idx = index.row()
df = self._data_frame
@@ -588,21 +601,38 @@ def get_default_values(self, index):
columns_with_index = [df.index.name] if df.index.name else []
columns_with_index += list(df.columns)
+ # ensure parameterScale is before lowerBound and upperBound (potential)
+ if "parameterScale" in columns_with_index:
+ columns_with_index.remove("parameterScale")
+ columns_with_index.insert(1, "parameterScale")
for colname in columns_with_index:
+ if changed and colname in changed.keys():
+ continue
if colname == df.index.name:
# Generate default index name if empty
- default_value = self.default_handler.get_default(colname, row_key)
+ default_value = self.default_handler.get_default(
+ colname,
+ row_key,
+ changed = changed
+ )
if (
not row_key
or f"new_{self.table_type}" in row_key
- ):
+ ) and bool(default_value):
rename_needed = True
new_index = default_value
+ elif colname in ["upperBound", "lowerBound"]:
+ par_scale = changes[(row_key, "parameterScale")][1] if\
+ (row_key, "parameterScale") in changes \
+ else changed["parameterScale"]
+ default_value = self.default_handler.get_default(
+ colname, row_key, par_scale
+ )
+ changes[(row_key, colname)] = ("", default_value)
else:
- if df.at[row_key, colname] == "":
- default_value = self.default_handler.get_default(colname, row_key)
- changes[(row_key, colname)] = ("", default_value)
+ default_value = self.default_handler.get_default(colname, row_key)
+ changes[(row_key, colname)] = ("", default_value)
commands = []
if changes:
@@ -679,7 +709,7 @@ def __init__(self, data_frame, parent=None):
parent=parent
)
- def get_default_values(self, index):
+ def get_default_values(self, index, changed: dict | None = None):
"""Fill missing values in a row without modifying the index."""
row = index.row()
df = self._data_frame
@@ -690,9 +720,10 @@ def get_default_values(self, index):
changes = {}
for colname in df.columns:
- if df.at[row_key, colname] == "":
- default = self.default_handler.get_default(colname, row_key)
- changes[(row_key, colname)] = ("", default)
+ if colname in changed.keys():
+ continue
+ default = self.default_handler.get_default(colname, row_key)
+ changes[(row_key, colname)] = ("", default)
command = ModifyDataFrameCommand(
self, changes, "Fill default values"
)
@@ -757,13 +788,17 @@ def __init__(self, data_frame, parent=None):
class ParameterModel(IndexedPandasTableModel):
"""Table model for the parameter data."""
- def __init__(self, data_frame, parent=None):
+ def __init__(self, data_frame, parent=None, sbml_model=None):
super().__init__(
data_frame=data_frame,
allowed_columns=COLUMNS["parameter"],
table_type="parameter",
parent=parent
)
+ self.default_handler = DefaultHandlerModel(
+ self, self.config,
+ sbml_model=sbml_model
+ )
class ConditionModel(IndexedPandasTableModel):
diff --git a/src/petab_gui/models/petab_model.py b/src/petab_gui/models/petab_model.py
index be0509d..565bea2 100644
--- a/src/petab_gui/models/petab_model.py
+++ b/src/petab_gui/models/petab_model.py
@@ -46,6 +46,9 @@ def __init__(
if petab_problem is None:
petab_problem = petab.Problem()
self.problem = petab_problem
+ self.sbml = SbmlViewerModel(
+ sbml_model=self.problem.model,
+ )
self.measurement = MeasurementModel(
data_frame=self.problem.measurement_df,
)
@@ -54,13 +57,11 @@ def __init__(
)
self.parameter = ParameterModel(
data_frame=self.problem.parameter_df,
+ sbml_model=self.sbml
)
self.condition = ConditionModel(
data_frame=self.problem.condition_df,
)
- self.sbml = SbmlViewerModel(
- sbml_model=self.problem.model,
- )
@property
def models(self):