Skip to content

Commit dffb2c8

Browse files
New Plotter (#91)
* New Plotter that uses the petab visualisations. Supports highlighting, has a singular subplot per plot and supports reverse highlighting. Changes can be bunched and will be processed in the background! * Merge cleanup * added table functionality * SMall changes * Add visualization and simulation table handling Introduced support for visualization and simulation tables, including new docks, controllers, table models, and YAML integration. Adjusted layout and visibility handling for these features and updated relevant actions and signals to ensure proper integration with the existing system. * works fine as visible dataframes. Needs integration into visualization file * Simulation df integrated into visualization.
1 parent 2a475d3 commit dffb2c8

File tree

12 files changed

+610
-59
lines changed

12 files changed

+610
-59
lines changed

src/petab_gui/C.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@
1414
"datasetId": {"type": np.object_, "optional": True},
1515
"replicateId": {"type": np.object_, "optional": True},
1616
},
17+
"simulation": {
18+
"observableId": {"type": np.object_, "optional": False},
19+
"preequilibrationConditionId": {"type": np.object_, "optional": True},
20+
"simulationConditionId": {"type": np.object_, "optional": False},
21+
"time": {"type": np.float64, "optional": False},
22+
"simulation": {"type": np.float64, "optional": False},
23+
"observableParameters": {"type": np.object_, "optional": True},
24+
"noiseParameters": {"type": np.object_, "optional": True},
25+
"datasetId": {"type": np.object_, "optional": True},
26+
"replicateId": {"type": np.object_, "optional": True},
27+
},
1728
"observable": {
1829
"observableId": {"type": np.object_, "optional": False},
1930
"observableName": {"type": np.object_, "optional": True},
@@ -42,6 +53,25 @@
4253
"conditionId": {"type": np.object_, "optional": False},
4354
"conditionName": {"type": np.object_, "optional": False},
4455
},
56+
"visualization": {
57+
"plotId": {"type": np.object_, "optional": False},
58+
"plotName": {"type": np.object_, "optional": True},
59+
"plotTypeSimulation": {
60+
"type": np.object_,
61+
"optional": True,
62+
},
63+
"plotTypeData": {"type": np.object_, "optional": True},
64+
"datasetId": {"type": np.object_, "optional": True},
65+
"xValues": {"type": np.object_, "optional": True},
66+
"xOffset": {"type": np.float64, "optional": True},
67+
"xLabel": {"type": np.object_, "optional": True},
68+
"xScale": {"type": np.object_, "optional": True},
69+
"yValues": {"type": np.object_, "optional": True},
70+
"yOffset": {"type": np.float64, "optional": True},
71+
"yLabel": {"type": np.object_, "optional": True},
72+
"yScale": {"type": np.object_, "optional": True},
73+
"legendEntry": {"type": np.object_, "optional": True},
74+
}
4575
}
4676

4777
CONFIG = {

src/petab_gui/commands.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,19 @@ def redo(self):
141141
df = self.model._data_frame
142142

143143
if self.add_mode:
144-
position = df.shape[0] - 1 # insert *before* the auto-row
144+
position = 0 if df.empty else df.shape[0] - 1 # insert *before* the auto-row
145145
self.model.beginInsertRows(
146146
QModelIndex(), position, position + len(self.row_indices) - 1
147147
)
148+
# save dtypes
149+
dtypes = df.dtypes.copy()
148150
for _i, idx in enumerate(self.row_indices):
149151
df.loc[idx] = [np.nan] * df.shape[1]
152+
# set dtypes
153+
if np.any(dtypes != df.dtypes):
154+
for col, dtype in dtypes.items():
155+
if dtype != df.dtypes[col]:
156+
df[col] = df[col].astype(dtype)
150157
self.model.endInsertRows()
151158
else:
152159
self.model.beginRemoveRows(

src/petab_gui/controllers/mother_controller.py

Lines changed: 130 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222

2323
from ..models import PEtabModel
2424
from ..settings_manager import SettingsDialog, settings_manager
25-
from ..utils import CaptureLogHandler, process_file
25+
from ..utils import (
26+
CaptureLogHandler,
27+
get_selected,
28+
process_file,
29+
)
2630
from ..views import TaskBar
2731
from .logger_controller import LoggerController
2832
from .sbml_controller import SbmlController
@@ -31,6 +35,7 @@
3135
MeasurementController,
3236
ObservableController,
3337
ParameterController,
38+
VisualizationController,
3439
)
3540
from .utils import (
3641
RecentFilesManager,
@@ -91,6 +96,20 @@ def __init__(self, view, model: PEtabModel):
9196
self.undo_stack,
9297
self,
9398
)
99+
self.visualization_controller = VisualizationController(
100+
self.view.visualization_dock,
101+
self.model.visualization,
102+
self.logger,
103+
self.undo_stack,
104+
self,
105+
)
106+
self.simulation_controller = MeasurementController(
107+
self.view.simulation_dock,
108+
self.model.simulation,
109+
self.logger,
110+
self.undo_stack,
111+
self,
112+
)
94113
self.sbml_controller = SbmlController(
95114
self.view.sbml_viewer, self.model.sbml, self.logger, self
96115
)
@@ -100,6 +119,8 @@ def __init__(self, view, model: PEtabModel):
100119
self.parameter_controller,
101120
self.condition_controller,
102121
self.sbml_controller,
122+
self.visualization_controller,
123+
self.simulation_controller,
103124
]
104125
# Recent Files
105126
self.recent_files_manager = RecentFilesManager(max_files=10)
@@ -109,6 +130,8 @@ def __init__(self, view, model: PEtabModel):
109130
"observable": False,
110131
"parameter": False,
111132
"condition": False,
133+
"visualization": False,
134+
"simulation": False,
112135
}
113136
self.sbml_checkbox_states = {"sbml": False, "antimony": False}
114137
self.unsaved_changes = False
@@ -120,13 +143,15 @@ def __init__(self, view, model: PEtabModel):
120143
self.setup_connections()
121144
self.setup_task_bar()
122145
self.setup_context_menu()
146+
self.plotter = None
147+
self.init_plotter()
123148

124149
def setup_context_menu(self):
125150
"""Sets up context menus for the tables."""
126-
self.measurement_controller.setup_context_menu(self.actions)
127-
self.observable_controller.setup_context_menu(self.actions)
128-
self.parameter_controller.setup_context_menu(self.actions)
129-
self.condition_controller.setup_context_menu(self.actions)
151+
for controller in self.controllers:
152+
if controller == self.sbml_controller:
153+
continue
154+
controller.setup_context_menu(self.actions)
130155

131156
def setup_task_bar(self):
132157
"""Create shortcuts for the main window."""
@@ -169,9 +194,11 @@ def setup_connections(self):
169194
)
170195
# Maybe Move to a Plot Model
171196
self.view.measurement_dock.table_view.selectionModel().selectionChanged.connect(
172-
self.handle_selection_changed
197+
self._on_table_selection_changed
198+
)
199+
self.view.simulation_dock.table_view.selectionModel().selectionChanged.connect(
200+
self._on_simulation_selection_changed
173201
)
174-
self.model.measurement.dataChanged.connect(self.handle_data_changed)
175202
# Unsaved Changes
176203
self.model.measurement.something_changed.connect(
177204
self.unsaved_changes_change
@@ -185,6 +212,12 @@ def setup_connections(self):
185212
self.model.condition.something_changed.connect(
186213
self.unsaved_changes_change
187214
)
215+
self.model.visualization.something_changed.connect(
216+
self.unsaved_changes_change
217+
)
218+
self.model.simulation.something_changed.connect(
219+
self.unsaved_changes_change
220+
)
188221
self.model.sbml.something_changed.connect(self.unsaved_changes_change)
189222
# Visibility
190223
self.sync_visibility_with_actions()
@@ -198,6 +231,14 @@ def setup_connections(self):
198231
self.sbml_controller.overwritten_model.connect(
199232
self.parameter_controller.update_handler_sbml
200233
)
234+
# overwrite signals
235+
for controller in [
236+
# self.measurement_controller,
237+
self.condition_controller
238+
]:
239+
controller.overwritten_df.connect(
240+
self.init_plotter
241+
)
201242

202243
def setup_actions(self):
203244
"""Setup actions for the main controller."""
@@ -301,8 +342,9 @@ def setup_actions(self):
301342
self.filter_input.setPlaceholderText("Filter...")
302343
filter_layout.addWidget(self.filter_input)
303344
for table_n, table_name in zip(
304-
["m", "p", "o", "c"],
305-
["measurement", "parameter", "observable", "condition"],
345+
["m", "p", "o", "c", "v", "s"],
346+
["measurement", "parameter", "observable", "condition",
347+
"visualization", "simulation"],
306348
strict=False,
307349
):
308350
tool_button = QToolButton()
@@ -325,7 +367,8 @@ def setup_actions(self):
325367
self.filter_input.textChanged.connect(self.filter_table)
326368

327369
# show/hide elements
328-
for element in ["measurement", "observable", "parameter", "condition"]:
370+
for element in ["measurement", "observable", "parameter",
371+
"condition", "visualization", "simulation"]:
329372
actions[f"show_{element}"] = QAction(
330373
f"{element.capitalize()} Table", self.view
331374
)
@@ -396,6 +439,8 @@ def sync_visibility_with_actions(self):
396439
"condition": self.view.condition_dock,
397440
"logger": self.view.logger_dock,
398441
"plot": self.view.plot_dock,
442+
"visualization": self.view.visualization_dock,
443+
"simulation": self.view.simulation_dock,
399444
}
400445

401446
for key, dock in dock_map.items():
@@ -558,6 +603,10 @@ def _open_file(self, actionable, file_path, sep, mode):
558603
self.parameter_controller.open_table(file_path, sep, mode)
559604
elif actionable == "condition":
560605
self.condition_controller.open_table(file_path, sep, mode)
606+
elif actionable == "visualization":
607+
self.visualization_controller.open_table(file_path, sep, mode)
608+
elif actionable == "simulation":
609+
self.simulation_controller.open_table(file_path, sep, mode)
561610
elif actionable == "data_matrix":
562611
self.measurement_controller.process_data_matrix_file(
563612
file_path, mode, sep
@@ -604,6 +653,14 @@ def open_yaml_and_load_files(self, yaml_path=None, mode="overwrite"):
604653
self.condition_controller.open_table(
605654
yaml_dir / yaml_content["problems"][0]["condition_files"][0]
606655
)
656+
# Visualization is optional
657+
vis_path = yaml_content["problems"][0].get("visualization_files")
658+
if vis_path:
659+
self.visualization_controller.open_table(
660+
yaml_dir / vis_path[0]
661+
)
662+
else:
663+
self.visualization_controller.clear_table()
607664
self.logger.log_message(
608665
"All files opened successfully from the YAML configuration.",
609666
color="green",
@@ -721,6 +778,10 @@ def active_controller(self):
721778
return self.parameter_controller
722779
if active_widget == self.view.condition_dock.table_view:
723780
return self.condition_controller
781+
if active_widget == self.view.visualization_dock.table_view:
782+
return self.visualization_controller
783+
if active_widget == self.view.simulation_dock.table_view:
784+
return self.simulation_controller
724785
return None
725786

726787
def delete_rows(self):
@@ -799,3 +860,62 @@ def replace(self):
799860
if self.view.find_replace_bar is None:
800861
self.view.create_find_replace_bar()
801862
self.view.toggle_replace()
863+
864+
def init_plotter(self):
865+
"""(Re-)initialize the plotter."""
866+
self.view.plot_dock.initialize(
867+
self.measurement_controller.proxy_model,
868+
self.simulation_controller.proxy_model,
869+
self.condition_controller.proxy_model,
870+
)
871+
self.plotter = self.view.plot_dock
872+
self.plotter.highlighter.click_callback = self._on_plot_point_clicked
873+
874+
def _on_plot_point_clicked(self, x, y, label):
875+
# Extract observable ID from label, if formatted like 'obsId (label)'
876+
meas_proxy = self.measurement_controller.proxy_model
877+
obs = label
878+
879+
x_axis_col = "time"
880+
y_axis_col = "measurement"
881+
observable_col = "observableId"
882+
883+
def column_index(name):
884+
for col in range(meas_proxy.columnCount()):
885+
if (
886+
meas_proxy.headerData(col, Qt.Horizontal)
887+
== name
888+
):
889+
return col
890+
raise ValueError(f"Column '{name}' not found.")
891+
892+
x_col = column_index(x_axis_col)
893+
y_col = column_index(y_axis_col)
894+
obs_col = column_index(observable_col)
895+
896+
for row in range(meas_proxy.rowCount()):
897+
row_obs = meas_proxy.index(row, obs_col).data()
898+
row_x = meas_proxy.index(row, x_col).data()
899+
row_y = meas_proxy.index(row, y_col).data()
900+
try:
901+
row_x, row_y = float(row_x), float(row_y)
902+
except ValueError:
903+
continue
904+
if row_obs == obs and row_x == x and row_y == y:
905+
self.measurement_controller.view.table_view.selectRow(row)
906+
break
907+
908+
def _on_table_selection_changed(self, selected, deselected):
909+
"""Highlight the cells selected in measurement table."""
910+
selected_rows = get_selected(
911+
self.measurement_controller.view.table_view
912+
)
913+
self.plotter.highlight_from_selection(selected_rows)
914+
915+
def _on_simulation_selection_changed(self, selected, deselected):
916+
selected_rows = get_selected(self.simulation_controller.view.table_view)
917+
self.plotter.highlight_from_selection(
918+
selected_rows,
919+
proxy=self.simulation_controller.proxy_model,
920+
y_axis_col="simulation"
921+
)

src/petab_gui/controllers/table_controllers.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def __init__(
6868
self.undo_stack = undo_stack
6969
self.model.undo_stack = undo_stack
7070
self.check_petab_lint_mode = True
71+
if model.table_type in ["simulation", "visualization"]:
72+
self.check_petab_lint_mode = False
7173
self.mother_controller = mother_controller
7274
self.view.table_view.setModel(self.proxy_model)
7375
self.setup_connections()
@@ -149,7 +151,9 @@ def open_table(self, file_path=None, separator=None, mode="overwrite"):
149151
if actionable in ["yaml", "sbml", "data_matrix", None]: # no table
150152
return
151153
try:
152-
if self.model.table_type == "measurement":
154+
if self.model.table_type in [
155+
"measurement", "visualization", "simulation"
156+
]:
153157
new_df = pd.read_csv(file_path, sep=separator)
154158
else:
155159
new_df = pd.read_csv(file_path, sep=separator, index_col=0)
@@ -175,8 +179,6 @@ def open_table(self, file_path=None, separator=None, mode="overwrite"):
175179
self.model.reset_invalid_cells()
176180

177181
def overwrite_df(self, new_df: pd.DataFrame):
178-
# TODO: Mother controller connects to overwritten_df signal. Set df
179-
# in petabProblem and unsaved changes to True
180182
"""Overwrite the DataFrame of the model with the data from the view."""
181183
self.proxy_model.setSourceModel(None)
182184
self.model.beginResetModel()
@@ -318,9 +320,10 @@ def copy_to_clipboard(self):
318320

319321
def paste_from_clipboard(self):
320322
"""Paste the clipboard content to the currently selected cells."""
323+
old_lint = self.check_petab_lint_mode
321324
self.check_petab_lint_mode = False
322325
self.view.paste_from_clipboard()
323-
self.check_petab_lint_mode = True
326+
self.check_petab_lint_mode = old_lint
324327
try:
325328
self.check_petab_lint()
326329
except Exception as e:
@@ -1134,3 +1137,27 @@ def check_petab_lint(
11341137
condition_df=condition_df,
11351138
model=sbml_model,
11361139
)
1140+
1141+
1142+
class VisualizationController(TableController):
1143+
"""Controller of the Visualization table."""
1144+
1145+
def __init__(
1146+
self,
1147+
view: TableViewer,
1148+
model: PandasTableModel,
1149+
logger,
1150+
undo_stack,
1151+
mother_controller,
1152+
):
1153+
"""Initialize the table controller.
1154+
1155+
See class:`TableController` for details.
1156+
"""
1157+
super().__init__(
1158+
view=view,
1159+
model=model,
1160+
logger=logger,
1161+
undo_stack=undo_stack,
1162+
mother_controller=mother_controller
1163+
)

0 commit comments

Comments
 (0)