diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..24cff02
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,24 @@
+name: CI
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+ merge_group:
+
+jobs:
+ quality:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v5
+
+ - name: Prepare Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.11"
+
+ - name: Install dependencies
+ run: pip install pre-commit
+
+ - name: Run pre-commit hooks
+ run: pre-commit run --all-files
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..ea5ae4c
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,24 @@
+# See https://pre-commit.com for more information
+# See https://pre-commit.com/hooks.html for more hooks
+repos:
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v6.0.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-yaml
+ - id: check-added-large-files
+ - repo: https://github.com/abravalheri/validate-pyproject
+ rev: v0.24.1
+ hooks:
+ - id: validate-pyproject
+ # Optional extra validations from SchemaStore:
+ additional_dependencies: [ "validate-pyproject-schema-store[all]" ]
+ - repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.13.1
+ hooks:
+ - id: ruff-check
+ types_or: [ python, pyi ]
+ args: [ --fix ]
+ - id: ruff-format
+ types_or: [ python, pyi ]
diff --git a/.zenodo.json b/.zenodo.json
index a5f6745..6c92c5b 100644
--- a/.zenodo.json
+++ b/.zenodo.json
@@ -20,4 +20,4 @@
"Biological Modeling",
"Parameter Estimation"
]
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index fd0d127..05e684b 100644
--- a/README.md
+++ b/README.md
@@ -52,17 +52,17 @@ The PEtabGUI provides a Python-based graphical user interface that simplifies
the creation, editing, and validation of PEtab parameter estimation problems.
- **Unified Environment**
- - Integrates all PEtab components (SBML/PySB models, conditions, observables,
- measurements, parameters, and visualization files).
- - Supports drag-and-drop import of YAML or individual component files.
- - Automatically resolves mismatches and converts matrix-format experimental data
+ - Integrates all PEtab components (SBML/PySB models, conditions, observables,
+ measurements, parameters, and visualization files).
+ - Supports drag-and-drop import of YAML or individual component files.
+ - Automatically resolves mismatches and converts matrix-format experimental data
into valid PEtab format.
- **Interactive and Intuitive Editing**
- - Dockable, resizable, and movable table widgets for each PEtab file.
- - Context-aware editing with combo-boxes, drop-downs, and multi-cell editing.
- - Automatic generation of missing observables/conditions with customizable defaults.
- - Real-time validation and plausibility checks with PEtab linting tools.
- - SBML view in both XML and human-readable Antimony syntax.
+ - Dockable, resizable, and movable table widgets for each PEtab file.
+ - Context-aware editing with combo-boxes, drop-downs, and multi-cell editing.
+ - Automatic generation of missing observables/conditions with customizable defaults.
+ - Real-time validation and plausibility checks with PEtab linting tools.
+ - SBML view in both XML and human-readable Antimony syntax.
- **Visualization and Simulation**
- Interactive plots linking measurement data with model simulations.
- Bidirectional highlighting between plots and tables.
@@ -71,6 +71,6 @@ the creation, editing, and validation of PEtab parameter estimation problems.
- Intelligent defaults for visualization with optional user customization.
- Ability to disable plotting for large models to maintain responsiveness.
- **Archiving and Export**
- - Export individual tables, the SBML model, or complete PEtab problems.
+ - Export individual tables, the SBML model, or complete PEtab problems.
- Save as directory structures or
[COMBINE archives](https://combinearchive.org) for reproducibility
diff --git a/docs/source/conf.py b/docs/source/conf.py
index d810715..517e4d4 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -10,30 +10,29 @@
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
-project = 'PEtab GUI'
-copyright = '2025, Paul Jonas Jost, Frank T. Bergmann'
-author = 'Paul Jonas Jost, Frank T. Bergmann'
-release = '0.1.3'
+project = "PEtab GUI"
+copyright = "2025, Paul Jonas Jost, Frank T. Bergmann"
+author = "Paul Jonas Jost, Frank T. Bergmann"
+release = "0.1.3"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
- 'sphinx.ext.napoleon', # For Google/Numpy style docstrings
- 'sphinx.ext.viewcode', # Add links to highlighted source code
- 'sphinx.ext.githubpages', # For publishing on GitHub Pages
- 'sphinx.ext.todo', # Support todo items
- 'sphinx.ext.mathjax', # For LaTeX math rendering
- 'myst_parser', # For Markdown support
- 'sphinx_copybutton', # To allow copying code snippets
- 'sphinx_design', # For better design elements
+ "sphinx.ext.napoleon", # For Google/Numpy style docstrings
+ "sphinx.ext.viewcode", # Add links to highlighted source code
+ "sphinx.ext.githubpages", # For publishing on GitHub Pages
+ "sphinx.ext.todo", # Support todo items
+ "sphinx.ext.mathjax", # For LaTeX math rendering
+ "myst_parser", # For Markdown support
+ "sphinx_copybutton", # To allow copying code snippets
+ "sphinx_design", # For better design elements
]
-templates_path = ['_templates']
+templates_path = ["_templates"]
exclude_patterns = []
-
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
diff --git a/example/data_matrix.csv b/example/data_matrix.csv
index d5a6f79..02a40d6 100644
--- a/example/data_matrix.csv
+++ b/example/data_matrix.csv
@@ -1,4 +1,4 @@
Time,obsA,obsB,obsC
3, 1, 2, 3
5, 4, 5, 6
-7, 7, 8, 9
\ No newline at end of file
+7, 7, 8, 9
diff --git a/example/dose_response.csv b/example/dose_response.csv
index e9170a4..c56d803 100644
--- a/example/dose_response.csv
+++ b/example/dose_response.csv
@@ -1,4 +1,4 @@
pApB,obsA,obsB,obsC
3, 1, 2, 3
5, 4, 5, 6
-7, 7, 8, 9
\ No newline at end of file
+7, 7, 8, 9
diff --git a/example/meas.tsv b/example/meas.tsv
index 22446c1..814cdb1 100644
--- a/example/meas.tsv
+++ b/example/meas.tsv
@@ -46,4 +46,4 @@ rSTAT5A_rel model1_data1 41.0627332148332 100.0 sd_rSTAT5A_rel model1_data1_rS
rSTAT5A_rel model1_data1 39.2358300289377 120.0 sd_rSTAT5A_rel model1_data1_rSTAT5A_rel
rSTAT5A_rel model1_data1 36.6194605442418 160.0 sd_rSTAT5A_rel model1_data1_rSTAT5A_rel
rSTAT5A_rel model1_data1 34.8937144010561 200.0 sd_rSTAT5A_rel model1_data1_rSTAT5A_rel
-rSTAT5A_rel model1_data1 32.2110771608676 240.0 sd_rSTAT5A_rel model1_data1_rSTAT5A_rel
\ No newline at end of file
+rSTAT5A_rel model1_data1 32.2110771608676 240.0 sd_rSTAT5A_rel model1_data1_rSTAT5A_rel
diff --git a/example/problem.yaml b/example/problem.yaml
index 26f179c..b24d457 100644
--- a/example/problem.yaml
+++ b/example/problem.yaml
@@ -8,4 +8,4 @@ problems:
observable_files:
- obs.tsv
sbml_files:
- - sbml_model.xml
\ No newline at end of file
+ - sbml_model.xml
diff --git a/pyproject.toml b/pyproject.toml
index d8a044a..5b3683f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -84,6 +84,16 @@ lint.ignore = [
"D100", # Ignore missing docstring in public modules
"D101", # Ignore missing docstring in public classes
"F401",
+ # FIXME: those are ignored for now, should be fixed eventually
+ "E501", # Ignore line too long
+ "ERA001", # Found commented-out code
+ "RET504", # Unnecessary assignment before `return`
+ "T201", # `print` found"
+ "SIM105", # Use `contextlib.suppress`
+ "S110", # `try`-`except`-`pass` detected, consider logging the exception
+ "A002", # Function argument shadowing a Python builtin
+ "E701", # Multiple statements on one line (colon)
+ "E741", # Ambiguous variable name
]
[tool.ruff.lint.per-file-ignores]
"*/__init__.py" = [
@@ -91,5 +101,9 @@ lint.ignore = [
"D400",
"D205",
]
+"docs/source/conf.py" = [
+ "A001", # Variable `copyright` is shadowing a Python builtin
+
+]
[tool.ruff.lint.pydocstyle]
convention = "pep257"
diff --git a/src/petab_gui/C.py b/src/petab_gui/C.py
index 9be7243..01ebaec 100644
--- a/src/petab_gui/C.py
+++ b/src/petab_gui/C.py
@@ -71,7 +71,7 @@
"yLabel": {"type": np.object_, "optional": True},
"yScale": {"type": np.object_, "optional": True},
"legendEntry": {"type": np.object_, "optional": True},
- }
+ },
}
CONFIG = {
diff --git a/src/petab_gui/app.py b/src/petab_gui/app.py
index 8d25239..9d18fd2 100644
--- a/src/petab_gui/app.py
+++ b/src/petab_gui/app.py
@@ -1,8 +1,8 @@
import argparse
import os
import sys
+from importlib.metadata import PackageNotFoundError, version
from importlib.resources import files
-from importlib.metadata import version, PackageNotFoundError
from pathlib import Path
from PySide6.QtCore import QEvent
@@ -48,7 +48,7 @@ class PEtabGuiApp(QApplication):
Inherits from QApplication and sets up the MVC components.
"""
- def __init__(self, file: str|Path = None):
+ def __init__(self, file: str | Path = None):
"""Initialize the PEtab GUI application.
Sets up the model, view, and controller components.
@@ -125,12 +125,10 @@ def main():
"--version",
action="version",
version=f"%(prog)s {pkg_version}",
- help="Show version number and exit"
+ help="Show version number and exit",
)
parser.add_argument(
- "petab_yaml",
- nargs="?",
- help="Path to the PEtab YAML file"
+ "petab_yaml", nargs="?", help="Path to the PEtab YAML file"
)
args = parser.parse_args()
diff --git a/src/petab_gui/commands.py b/src/petab_gui/commands.py
index e3f75cc..621409c 100644
--- a/src/petab_gui/commands.py
+++ b/src/petab_gui/commands.py
@@ -141,7 +141,9 @@ def redo(self):
df = self.model._data_frame
if self.add_mode:
- position = 0 if df.empty else df.shape[0] - 1 # insert *before* the auto-row
+ position = (
+ 0 if df.empty else df.shape[0] - 1
+ ) # insert *before* the auto-row
self.model.beginInsertRows(
QModelIndex(), position, position + len(self.row_indices) - 1
)
@@ -264,12 +266,9 @@ def _apply_changes(self, use_new: bool):
else:
df[col] = df[col].astype(dtype)
- rows = [
- df.index.get_loc(row_key) for (row_key, _) in self.changes
- ]
+ rows = [df.index.get_loc(row_key) for (row_key, _) in self.changes]
cols = [
- df.columns.get_loc(col) + col_offset
- for (_, col) in self.changes
+ df.columns.get_loc(col) + col_offset for (_, col) in self.changes
]
top_left = self.model.index(min(rows), min(cols))
diff --git a/src/petab_gui/controllers/logger_controller.py b/src/petab_gui/controllers/logger_controller.py
index e50fd0a..c091f96 100644
--- a/src/petab_gui/controllers/logger_controller.py
+++ b/src/petab_gui/controllers/logger_controller.py
@@ -18,8 +18,8 @@ def __init__(self, views):
self.logger_level = 1
self.log_message("Welcome to PEtab-GUI!", color="green")
self.log_message(
- 'If you need help, click Help in the menu,'
- ' enter the Help Mode (click question mark in toolbar) or visit '
+ "If you need help, click Help in the menu,"
+ " enter the Help Mode (click question mark in toolbar) or visit "
'the documentation.',
color="green",
diff --git a/src/petab_gui/controllers/mother_controller.py b/src/petab_gui/controllers/mother_controller.py
index 3417193..6ad0def 100644
--- a/src/petab_gui/controllers/mother_controller.py
+++ b/src/petab_gui/controllers/mother_controller.py
@@ -261,9 +261,7 @@ def setup_connections(self):
self.visualization_controller,
self.simulation_controller,
]:
- controller.overwritten_df.connect(
- self._schedule_plot_update
- )
+ controller.overwritten_df.connect(self._schedule_plot_update)
def setup_actions(self):
"""Setup actions for the main controller."""
@@ -302,9 +300,7 @@ def setup_actions(self):
actions["save_single_table"] = QAction(
qta.icon("mdi6.table-arrow-down"), "Save This Table", self.view
)
- actions["save_single_table"].triggered.connect(
- self.save_single_table
- )
+ actions["save_single_table"].triggered.connect(self.save_single_table)
# Find + Replace
actions["find"] = QAction(qta.icon("mdi6.magnify"), "Find", self.view)
actions["find"].setShortcut(QKeySequence.Find)
@@ -341,8 +337,9 @@ def setup_actions(self):
actions["delete_row"].triggered.connect(self.delete_rows)
# add/delete column
actions["add_column"] = QAction(
- qta.icon("mdi6.table-column-plus-after"), "Add Column...",
- self.view
+ qta.icon("mdi6.table-column-plus-after"),
+ "Add Column...",
+ self.view,
)
actions["add_column"].triggered.connect(self.add_column)
actions["delete_column"] = QAction(
@@ -381,8 +378,14 @@ def setup_actions(self):
filter_layout.addWidget(self.filter_input)
for table_n, table_name in zip(
["m", "p", "o", "c", "v", "s"],
- ["measurement", "parameter", "observable", "condition",
- "visualization", "simulation"],
+ [
+ "measurement",
+ "parameter",
+ "observable",
+ "condition",
+ "visualization",
+ "simulation",
+ ],
strict=False,
):
tool_button = QToolButton()
@@ -405,8 +408,14 @@ def setup_actions(self):
self.filter_input.textChanged.connect(self.filter_table)
# show/hide elements
- for element in ["measurement", "observable", "parameter",
- "condition", "visualization", "simulation"]:
+ for element in [
+ "measurement",
+ "observable",
+ "parameter",
+ "condition",
+ "visualization",
+ "simulation",
+ ]:
actions[f"show_{element}"] = QAction(
f"{element.capitalize()} Table", self.view
)
@@ -790,21 +799,13 @@ def open_omex_and_load_files(self, omex_path=None):
self.measurement_controller.overwrite_df(
combine_archive.measurement_df
)
- self.observable_controller.overwrite_df(
- combine_archive.observable_df
- )
- self.condition_controller.overwrite_df(
- combine_archive.condition_df
- )
- self.parameter_controller.overwrite_df(
- combine_archive.parameter_df
- )
+ self.observable_controller.overwrite_df(combine_archive.observable_df)
+ self.condition_controller.overwrite_df(combine_archive.condition_df)
+ self.parameter_controller.overwrite_df(combine_archive.parameter_df)
self.visualization_controller.overwrite_df(
combine_archive.visualization_df
)
- self.sbml_controller.overwrite_sbml(
- sbml_model = combine_archive.model
- )
+ self.sbml_controller.overwrite_sbml(sbml_model=combine_archive.model)
def new_file(self):
"""Empty all tables. In case of unsaved changes, ask to save."""
@@ -981,7 +982,6 @@ def open_settings(self):
settings_dialog = SettingsDialog(table_columns, self.view)
settings_dialog.exec()
-
def find(self):
"""Create a find replace bar if it is non existent."""
if self.view.find_replace_bar is None:
@@ -1001,7 +1001,7 @@ def init_plotter(self):
self.simulation_controller.proxy_model,
self.condition_controller.proxy_model,
self.visualization_controller.proxy_model,
- self.model
+ self.model,
)
self.plotter = self.view.plot_dock
self.plotter.highlighter.click_callback = self._on_plot_point_clicked
@@ -1021,10 +1021,7 @@ def _on_plot_point_clicked(self, x, y, label, data_type):
def column_index(name):
for col in range(proxy.columnCount()):
- if (
- proxy.headerData(col, Qt.Horizontal)
- == name
- ):
+ if proxy.headerData(col, Qt.Horizontal) == name:
return col
raise ValueError(f"Column '{name}' not found.")
@@ -1052,11 +1049,13 @@ def _on_table_selection_changed(self, selected, deselected):
self.plotter.highlight_from_selection(selected_rows)
def _on_simulation_selection_changed(self, selected, deselected):
- selected_rows = get_selected(self.simulation_controller.view.table_view)
+ selected_rows = get_selected(
+ self.simulation_controller.view.table_view
+ )
self.plotter.highlight_from_selection(
selected_rows,
proxy=self.simulation_controller.proxy_model,
- y_axis_col="simulation"
+ y_axis_col="simulation",
)
def simulate(self):
@@ -1069,16 +1068,21 @@ def simulate(self):
from basico.petab import PetabSimulator
# report current basico / COPASI version
- self.logger.log_message(f"Simulate with basico: {basico.__version__}, COPASI: {basico.COPASI.__version__}", color="green")
+ self.logger.log_message(
+ f"Simulate with basico: {basico.__version__}, COPASI: {basico.COPASI.__version__}",
+ color="green",
+ )
import tempfile
# create temp directory in temp folder:
with tempfile.TemporaryDirectory() as temp_dir:
# settings is only current solution statistic for now:
- settings = {'method' : {'name': basico.PE.CURRENT_SOLUTION}}
+ settings = {"method": {"name": basico.PE.CURRENT_SOLUTION}}
# create simulator
- simulator = PetabSimulator(petab_problem, settings=settings, working_dir=temp_dir)
+ simulator = PetabSimulator(
+ petab_problem, settings=settings, working_dir=temp_dir
+ )
# simulate
sim_df = simulator.simulate()
@@ -1093,6 +1097,7 @@ def _schedule_plot_update(self):
def _toggle_whats_this_mode(self, on: bool):
"""Enable/disable click-to-help mode by installing/removing the global filter.
+
On enter: show a short instruction bubble.
"""
app = QApplication.instance()
@@ -1105,17 +1110,14 @@ def _toggle_whats_this_mode(self, on: bool):
except Exception:
pass
app.removeEventFilter(self._whats_this_filter)
- self.logger.log_message(
- "Enden the Help mode.",
- color="blue"
- )
+ self.logger.log_message("Enden the Help mode.", color="blue")
return
# install filter
app.installEventFilter(self._whats_this_filter)
QApplication.setOverrideCursor(Qt.WhatsThisCursor)
self.logger.log_message(
"Started the Help mode. Click on any widget to see its help.",
- color="blue"
+ color="blue",
)
self._show_help_welcome()
@@ -1129,15 +1131,15 @@ def _show_help_welcome(self):
msg.setWindowTitle("Help mode")
msg.setTextFormat(Qt.RichText)
msg.setText(
- "Welcome to help mode
"
- "
"
- "- Click any widget, tab, or column header to see its help.
"
- "- Click the same item again or press Esc to close the bubble.
"
- "- Press Esc with no bubble, or toggle the ? button, to exit.
"
- "
"
- )
+ "Welcome to help mode
"
+ ""
+ "- Click any widget, tab, or column header to see its help.
"
+ "- Click the same item again or press Esc to close the bubble.
"
+ "- Press Esc with no bubble, or toggle the ? button, to exit.
"
+ "
"
+ )
dont = QCheckBox("Don't show again")
msg.setCheckBox(dont)
msg.exec()
if dont.isChecked():
- settings.setValue("help_mode/welcome_disabled", True)
+ settings.setValue("help_mode/welcome_disabled", True)
diff --git a/src/petab_gui/controllers/sbml_controller.py b/src/petab_gui/controllers/sbml_controller.py
index 54b62b0..d2292e6 100644
--- a/src/petab_gui/controllers/sbml_controller.py
+++ b/src/petab_gui/controllers/sbml_controller.py
@@ -151,12 +151,10 @@ def _overwrite_sbml_with_model(self, sbml_model: SbmlModel):
"SBML model successfully overwritten.", color="green"
)
-
def clear_model(self):
- """Clear the model in case the user wants to start a new problem"""
+ """Clear the model in case the user wants to start a new problem."""
self.model.antimony_text = DEFAULT_ANTIMONY_TEXT
self.model.convert_antimony_to_sbml()
self.view.sbml_text_edit.setPlainText(self.model.sbml_text)
self.view.antimony_text_edit.setPlainText(self.model.antimony_text)
self.overwritten_model.emit()
-
diff --git a/src/petab_gui/controllers/table_controllers.py b/src/petab_gui/controllers/table_controllers.py
index aadd597..0c99b0c 100644
--- a/src/petab_gui/controllers/table_controllers.py
+++ b/src/petab_gui/controllers/table_controllers.py
@@ -158,7 +158,9 @@ def open_table(self, file_path=None, separator=None, mode="overwrite"):
return
try:
if self.model.table_type in [
- "measurement", "visualization", "simulation"
+ "measurement",
+ "visualization",
+ "simulation",
]:
new_df = pd.read_csv(file_path, sep=separator)
else:
@@ -570,7 +572,9 @@ def save_table(self, file_name):
if not file_name.endswith((".tsv", ".csv")):
file_name += ".tsv"
try:
- save_petab_table(self.model.get_df(), file_name, self.model.table_type)
+ save_petab_table(
+ self.model.get_df(), file_name, self.model.table_type
+ )
except Exception as e:
QMessageBox.critical(
self.view,
@@ -709,30 +713,34 @@ def process_data_matrix_file(self, file_name, mode, separator=None):
if cond_dialog.exec():
conditions = cond_dialog.get_inputs()
condition_id = conditions.get("simulationConditionId", "")
- preeq_id = conditions.get("preequilibrationConditionId", "")
+ preeq_id = conditions.get(
+ "preequilibrationConditionId", ""
+ )
else:
return
else:
dose_col_sel, time_choice, preeq_id = (
- self._resolve_dose_and_time(data_matrix))
+ self._resolve_dose_and_time(data_matrix)
+ )
if not dose_col_sel or time_choice is None:
self.logger.log_message(
"While uploading file as a data matrix: time column "
"found and no dose/time selection made.",
- color="red"
+ color="red",
)
return
df_proc = data_matrix.copy()
- if (isinstance(time_choice, str)
- and time_choice.strip().lower() == "inf"):
- df_proc["time"] = "inf"
+ if (
+ isinstance(time_choice, str)
+ and time_choice.strip().lower() == "inf"
+ ):
+ df_proc["time"] = "inf"
else:
try:
- df_proc["time"] = float(time_choice)
+ df_proc["time"] = float(time_choice)
except Exception:
self.logger.log_message(
- f"Invalid time value: {time_choice}",
- color="red"
+ f"Invalid time value: {time_choice}", color="red"
)
return
# No fixed condition_id in dose-response; it's built per-row
@@ -763,24 +771,39 @@ def _detect_time_column(self, df) -> str | None:
def _rank_dose_candidates(self, df) -> list[str]:
"""Lightweight ranking of dose-like columns (regex + numeric + cardinality)."""
- patt = re.compile(r"\b(dose|conc|concentration|drug|compound|stim|input|u\d+)\b", re.IGNORECASE)
+ patt = re.compile(
+ r"\b(dose|conc|concentration|drug|compound|stim|input|u\d+)\b",
+ re.IGNORECASE,
+ )
scores = {}
- for col in df.columns:
+ # FIXME: https://github.com/PaulJonasJost/PEtab_GUI/issues/159
+ for col in df.columns: # noqa: B007
s = 0.0
- if patt.search(col or ""): s += 2.0
+ if patt.search(col or ""):
+ s += 2.0
try:
- if df[col].dtype.kind in "if": s += 1.0
+ if df[col].dtype.kind in "if":
+ s += 1.0
uniq = df[col].nunique(dropna=True)
- if 2 <= uniq <= 30: s += 0.8
- if np.all(pd.to_numeric(df[col], errors="coerce").fillna(0) >= 0): s += 0.3
+ if 2 <= uniq <= 30:
+ s += 0.8
+ if np.all(pd.to_numeric(df[col], errors="coerce").fillna(0) >= 0):
+ s += 0.3
ser = pd.to_numeric(df[col], errors="coerce").dropna()
if len(ser) >= 5:
diffs = np.diff(ser.values)
- if np.mean(diffs >= 0) >= 0.7: s += 0.2
+ if np.mean(diffs >= 0) >= 0.7:
+ s += 0.2
except Exception:
pass
scores[col] = s
- return [c for c, _ in sorted(scores.items(), key=lambda x: (-x[1], df[x[0]].nunique(dropna=True)))]
+ return [
+ c
+ for c, _ in sorted(
+ scores.items(),
+ key=lambda x: (-x[1], df[x[0]].nunique(dropna=True)),
+ )
+ ]
def _resolve_dose_and_time(self, df) -> tuple[str | None, str | None, str]:
"""Open dialog with ranked dose suggestions and time choices (incl. steady state)."""
@@ -790,11 +813,11 @@ def _resolve_dose_and_time(self, df) -> tuple[str | None, str | None, str]:
last_dose = settings.value(f"dose/last_choice/{header_key}", "", str)
suggested = self._rank_dose_candidates(df)
if last_dose and last_dose in df.columns:
- suggested = [last_dose] + [s for s in suggested if s != last_dose]
+ suggested = [last_dose] + [s for s in suggested if s != last_dose]
dlg = DoseTimeDialog(
columns=list(df.columns),
dose_suggested=suggested,
- parent=self.view if hasattr(self, "view") else None
+ parent=self.view if hasattr(self, "view") else None,
)
if dlg.exec():
dose_col, time_text, preeq_id = dlg.get_result()
@@ -814,8 +837,11 @@ def _format_dose_value(self, v) -> str:
return str(v).strip().replace(" ", "_")
def populate_tables_from_data_matrix(
- self, data_matrix,
- condition_id, preeq_id: str = "", dose_col: str | None = None
+ self,
+ data_matrix,
+ condition_id,
+ preeq_id: str = "",
+ dose_col: str | None = None,
):
"""Populate the measurement table from the data matrix."""
# Build per-row condition IDs if dose_col provided
@@ -831,13 +857,15 @@ def populate_tables_from_data_matrix(
if col == "time":
continue
if dose_col and col == dose_col:
- continue
+ continue
observable_id = col
self.model.relevant_id_changed.emit(
observable_id, "", "observable"
)
if condition_ids is None:
- self.model.relevant_id_changed.emit(condition_id, "", "condition")
+ self.model.relevant_id_changed.emit(
+ condition_id, "", "condition"
+ )
if preeq_id:
self.model.relevant_id_changed.emit(preeq_id, "", "condition")
self.add_measurement_rows(
@@ -853,9 +881,9 @@ def add_measurement_rows(
data_matrix,
observable_id,
condition_id: str = "",
- preeq_id: str = "",
- condition_ids: Sequence[str] | None = None,
- ):
+ preeq_id: str = "",
+ condition_ids: Sequence[str] | None = None,
+ ):
"""Adds multiple rows to the measurement table."""
# check number of rows and signal row insertion
rows = data_matrix.shape[0]
@@ -866,7 +894,11 @@ def add_measurement_rows(
) # Fills the table with empty rows
top_left = self.model.createIndex(current_rows, 0)
for i_row, (_, row) in enumerate(data_matrix.iterrows()):
- cid = condition_ids[i_row] if condition_ids is not None else condition_id
+ cid = (
+ condition_ids[i_row]
+ if condition_ids is not None
+ else condition_id
+ )
self.model.fill_row(
i_row + current_rows,
data={
@@ -1281,5 +1313,5 @@ def __init__(
model=model,
logger=logger,
undo_stack=undo_stack,
- mother_controller=mother_controller
+ mother_controller=mother_controller,
)
diff --git a/src/petab_gui/controllers/utils.py b/src/petab_gui/controllers/utils.py
index d93f663..e5f1920 100644
--- a/src/petab_gui/controllers/utils.py
+++ b/src/petab_gui/controllers/utils.py
@@ -37,8 +37,10 @@ class _WhatsThisClickHelp(QObject):
def __init__(self, action):
super().__init__()
self.action = action
- self._has_bubble = False # whether a bubble is currently visible
- self._last = None # ("kind", widget, extra) to toggle on repeated clicks
+ self._has_bubble = False # whether a bubble is currently visible
+ self._last = (
+ None # ("kind", widget, extra) to toggle on repeated clicks
+ )
def _exit_mode(self):
"""Uncheck action and remove filter."""
@@ -66,8 +68,10 @@ def eventFilter(self, _obj, ev):
return False
# Left-click: toggle/show help at the clicked target
- if ev.type() == QEvent.MouseButtonPress and (ev.buttons() & Qt.LeftButton):
- QWhatsThis.hideText() # always close any previous bubble
+ if ev.type() == QEvent.MouseButtonPress and (
+ ev.buttons() & Qt.LeftButton
+ ):
+ QWhatsThis.hideText() # always close any previous bubble
w = QApplication.widgetAt(QCursor.pos())
# Clicking the toolbar "?" button itself -> exit help mode
@@ -94,7 +98,11 @@ def eventFilter(self, _obj, ev):
if self._last == ("tab", tabbar, i):
self._has_bubble = False
return True
- text = tabbar.tabToolTip(i) or tabbar.tabText(i) or "No help available."
+ text = (
+ tabbar.tabToolTip(i)
+ or tabbar.tabText(i)
+ or "No help available."
+ )
QWhatsThis.showText(QCursor.pos(), text, tabbar)
self._last = ("tab", tabbar, i)
self._has_bubble = True
@@ -115,10 +123,18 @@ def eventFilter(self, _obj, ev):
if self._last == ("header", hdr, sec):
self._has_bubble = False
return True
- text = (view.model().headerData(sec, hdr.orientation(), Qt.WhatsThisRole)
- or view.model().headerData(sec, hdr.orientation(), Qt.ToolTipRole)
- or view.model().headerData(sec, hdr.orientation(), Qt.DisplayRole)
- or "No help available.")
+ text = (
+ view.model().headerData(
+ sec, hdr.orientation(), Qt.WhatsThisRole
+ )
+ or view.model().headerData(
+ sec, hdr.orientation(), Qt.ToolTipRole
+ )
+ or view.model().headerData(
+ sec, hdr.orientation(), Qt.DisplayRole
+ )
+ or "No help available."
+ )
QWhatsThis.showText(QCursor.pos(), text, hdr)
self._last = ("header", hdr, sec)
self._has_bubble = True
@@ -135,7 +151,11 @@ def eventFilter(self, _obj, ev):
if self._last == ("cell", view, (idx.row(), idx.column())):
self._has_bubble = False
return True
- text = (idx.data(Qt.WhatsThisRole) or idx.data(Qt.ToolTipRole) or "No help available.")
+ text = (
+ idx.data(Qt.WhatsThisRole)
+ or idx.data(Qt.ToolTipRole)
+ or "No help available."
+ )
QWhatsThis.showText(QCursor.pos(), text, w)
self._last = ("cell", view, (idx.row(), idx.column()))
self._has_bubble = True
@@ -171,24 +191,26 @@ def wrapper(
except Exception as e:
err_msg = filtered_error(e)
err_msg = html.escape(err_msg)
- if (additional_error_check and "Missing parameter(s)" in
- err_msg):
- match = re.search(r"\{(.+?)}", err_msg)
- missing_params = {
- s.strip(" '") for s in match.group(1).split(",")
- }
- remain = {
- p
- for p in missing_params
- if p not in self.model._data_frame.index
- }
- if not remain:
- return True
- err_msg = re.sub(
- r"\{.*?}",
- "{" + ", ".join(sorted(remain)) + "}",
- err_msg,
- )
+ if (
+ additional_error_check
+ and "Missing parameter(s)" in err_msg
+ ):
+ match = re.search(r"\{(.+?)}", err_msg)
+ missing_params = {
+ s.strip(" '") for s in match.group(1).split(",")
+ }
+ remain = {
+ p
+ for p in missing_params
+ if p not in self.model._data_frame.index
+ }
+ if not remain:
+ return True
+ err_msg = re.sub(
+ r"\{.*?}",
+ "{" + ", ".join(sorted(remain)) + "}",
+ err_msg,
+ )
msg = "PEtab linter failed"
if row_name is not None and col_name is not None:
msg = f"{msg} at ({row_name}, {col_name}): {err_msg}"
@@ -308,9 +330,8 @@ def clear_recent_files(self):
self.save_recent_files()
self.update_tool_bar_menu()
-def save_petab_table(
- df: pd.DataFrame, filename: str | Path, table_type: str
-):
+
+def save_petab_table(df: pd.DataFrame, filename: str | Path, table_type: str):
"""Save a PEtab table to a file. Function used based on table type."""
if table_type == "condition":
petab.write_condition_df(df, filename)
diff --git a/src/petab_gui/models/pandas_table_model.py b/src/petab_gui/models/pandas_table_model.py
index 32124c2..4ba208f 100644
--- a/src/petab_gui/models/pandas_table_model.py
+++ b/src/petab_gui/models/pandas_table_model.py
@@ -207,7 +207,9 @@ def headerData(self, section, orientation, role=Qt.DisplayRole):
if section == 0 and self._has_named_index:
col_label = self._data_frame.index.name
else:
- col_label = self._data_frame.columns[section - self.column_offset]
+ col_label = self._data_frame.columns[
+ section - self.column_offset
+ ]
if role == Qt.ToolTipRole:
tooltip_header = header_tip(self.table_type, col_label)
return tooltip_header
diff --git a/src/petab_gui/models/petab_model.py b/src/petab_gui/models/petab_model.py
index e9bb685..06a1702 100644
--- a/src/petab_gui/models/petab_model.py
+++ b/src/petab_gui/models/petab_model.py
@@ -140,8 +140,7 @@ def save(self, directory: str | Path):
self.current_petab_problem.to_files_generic(prefix_path=directory)
def save_as_omex(self, file_name: str):
- """Save the PEtab model as an OMEX file.
- """
+ """Save the PEtab model as an OMEX file."""
with tempfile.TemporaryDirectory() as temp_dir:
self.save(temp_dir)
petab.create_combine_archive(
diff --git a/src/petab_gui/models/sbml_model.py b/src/petab_gui/models/sbml_model.py
index 6b8b656..5cd8ac4 100644
--- a/src/petab_gui/models/sbml_model.py
+++ b/src/petab_gui/models/sbml_model.py
@@ -33,7 +33,7 @@ def __init__(self, sbml_model: petab.models.Model, parent=None):
self.antimony_text = sbmlToAntimony(self.sbml_text)
else:
self.antimony_text = DEFAULT_ANTIMONY_TEXT
- with QSignalBlocker(self) as blocker:
+ with QSignalBlocker(self):
self.convert_antimony_to_sbml()
self.model_id = self._get_model_id()
@@ -67,7 +67,7 @@ def get_current_sbml_model(self):
def _get_model_id(self):
"""Extract the model ID from the SBML text."""
- document = libsbml.readSBMLFromString(self.sbml_text)
+ document = libsbml.readSBMLFromString(self.sbml_text)
model = document.getModel()
model_id = model.getIdAttribute() or "New_File"
return model_id
diff --git a/src/petab_gui/models/tooltips.py b/src/petab_gui/models/tooltips.py
index ecdf775..396b3ea 100644
--- a/src/petab_gui/models/tooltips.py
+++ b/src/petab_gui/models/tooltips.py
@@ -1,4 +1,5 @@
"""Tooltips for table view columns."""
+
import numpy as np
# Tooltips
@@ -6,8 +7,11 @@
_CELL_TIPS: dict[str, dict[str, str]] = {}
-def register_tips(table: str, header: dict[str, str] | None = None,
- cell: dict[str, str] | None = None) -> None:
+def register_tips(
+ table: str,
+ header: dict[str, str] | None = None,
+ cell: dict[str, str] | None = None,
+) -> None:
"""Register tooltips for a given table."""
if header:
_HEADER_TIPS.setdefault(table, {}).update(header)
@@ -16,7 +20,8 @@ def register_tips(table: str, header: dict[str, str] | None = None,
# Measurement Tooltips
-register_tips("measurement",
+register_tips(
+ "measurement",
header={
"observableId": "ID from Observables; the output being measured.",
"preequilibrationConditionId": "Condition for preequilibration; empty = none.",
@@ -42,7 +47,8 @@ def register_tips(table: str, header: dict[str, str] | None = None,
)
# Observable Tooltips
-register_tips("observable",
+register_tips(
+ "observable",
header={
"observableId": "Unique ID; letters, digits, underscores; not starting with digit. Referenced in Measurements.",
"observableName": "Optional display name; not used for identification.",
@@ -62,7 +68,8 @@ def register_tips(table: str, header: dict[str, str] | None = None,
)
# Parameter Tooltips
-register_tips("parameter",
+register_tips(
+ "parameter",
header={
"parameterId": "Unique ID; must match SBML parameter, condition override, or observable/noise parameter.",
"parameterName": "Optional label for plotting; may differ from SBML name.",
@@ -92,7 +99,8 @@ def register_tips(table: str, header: dict[str, str] | None = None,
)
# Condition Tooltips
-register_tips("condition",
+register_tips(
+ "condition",
header={
"conditionId": "Unique ID; letters/digits/underscores; not starting with digit. Referenced by Measurements.",
"conditionName": "Optional human-readable name for reports/plots.",
@@ -102,12 +110,13 @@ def register_tips(table: str, header: dict[str, str] | None = None,
"conditionId": "Enter a valid, unique identifier.",
"conditionName": "Optional label.",
"*": "User-defined column. Provide numeric value or SBML/parameter ID. "
- "Species IDs = initial amount/concentration (NaN = keep preeq/initial). "
- "Compartment IDs = initial size.",
+ "Species IDs = initial amount/concentration (NaN = keep preeq/initial). "
+ "Compartment IDs = initial size.",
},
)
-register_tips("visualization",
+register_tips(
+ "visualization",
header={
"plotId": "Plot ID; datasets with same ID share axes.",
"plotName": "Optional plot display name.",
@@ -142,7 +151,7 @@ def register_tips(table: str, header: dict[str, str] | None = None,
},
)
-_default_tip = "User-defined column; no specific structure enforced.",
+_default_tip = ("User-defined column; no specific structure enforced.",)
def header_tip(table: str, column: str) -> str:
@@ -240,4 +249,3 @@ def cell_tip(table: str, column: str) -> str:
"• Edit SBML (XML) and Antimony side-by-side.
"
"• Use Forward Changes buttons to sync; see logger for errors."
)
-
diff --git a/src/petab_gui/settings_manager.py b/src/petab_gui/settings_manager.py
index 5c5bd1b..d20c542 100644
--- a/src/petab_gui/settings_manager.py
+++ b/src/petab_gui/settings_manager.py
@@ -62,27 +62,29 @@ def set_value(self, key, value):
def load_ui_settings(self, main_window):
"""Load UI-related settings such as main window and dock states."""
# Restore main window geometry and state
- main_window.restoreGeometry(self.get_value(
- "main_window/geometry", main_window.saveGeometry()
- ))
+ main_window.restoreGeometry(
+ self.get_value("main_window/geometry", main_window.saveGeometry())
+ )
main_window.restoreState(
self.get_value("main_window/state", main_window.saveState())
)
# Restore dock widget visibility
for dock, _ in main_window.dock_visibility.items():
- dock.setVisible(self.get_value(
- f"docks/{dock.objectName()}", True, value_type=bool
- ))
+ dock.setVisible(
+ self.get_value(
+ f"docks/{dock.objectName()}", True, value_type=bool
+ )
+ )
main_window.data_tab.restoreGeometry(
self.get_value(
"data_tab/geometry", main_window.data_tab.saveGeometry()
)
)
- main_window.data_tab.restoreState(self.get_value(
- "data_tab/state", main_window.data_tab.saveState()
- ))
+ main_window.data_tab.restoreState(
+ self.get_value("data_tab/state", main_window.data_tab.saveState())
+ )
def save_ui_settings(self, main_window):
"""Save UI-related settings such as main window and dock states."""
diff --git a/src/petab_gui/stylesheet.css b/src/petab_gui/stylesheet.css
index 7b5bab8..96557ef 100644
--- a/src/petab_gui/stylesheet.css
+++ b/src/petab_gui/stylesheet.css
@@ -138,4 +138,3 @@ QDockWidget::close-button:hover {
background-color: #ffcccc; /* Light red */
border-radius: 5px;
}
-
diff --git a/src/petab_gui/views/logger.py b/src/petab_gui/views/logger.py
index 46eb717..d89afe0 100644
--- a/src/petab_gui/views/logger.py
+++ b/src/petab_gui/views/logger.py
@@ -3,8 +3,8 @@
Contains logger widget as well as two helper buttons.
"""
-from PySide6.QtWidgets import QHBoxLayout, QTextBrowser, QWidget
from PySide6.QtCore import Qt
+from PySide6.QtWidgets import QHBoxLayout, QTextBrowser, QWidget
class Logger(QWidget):
diff --git a/src/petab_gui/views/other_views.py b/src/petab_gui/views/other_views.py
index eac486a..cb5480b 100644
--- a/src/petab_gui/views/other_views.py
+++ b/src/petab_gui/views/other_views.py
@@ -1,4 +1,5 @@
"""Collection of other views aside from the main ones."""
+
from PySide6.QtWidgets import (
QComboBox,
QDialog,
@@ -13,19 +14,29 @@
class DoseTimeDialog(QDialog):
"""Pick dose and time (or steady state)."""
- def __init__(self, columns: list[str], dose_suggested: list[str], parent=None):
+ def __init__(
+ self, columns: list[str], dose_suggested: list[str], parent=None
+ ):
super().__init__(parent)
self.setWindowTitle("Select Dose and Time")
- order = [c for c in dose_suggested if c in columns] + [c for c in columns if c not in dose_suggested]
+ order = [c for c in dose_suggested if c in columns] + [
+ c for c in columns if c not in dose_suggested
+ ]
self._dose = QComboBox(self)
self._dose.addItems(order)
self._time = QLineEdit(self)
- self._time.setPlaceholderText("Enter constant time (e.g. 0, 5, 12.5). Use 'inf' for steady state")
+ self._time.setPlaceholderText(
+ "Enter constant time (e.g. 0, 5, 12.5). Use 'inf' for steady state"
+ )
self._preeq_edit = QLineEdit(self)
- self._preeq_edit.setPlaceholderText("Optional preequilibrationConditionId")
+ self._preeq_edit.setPlaceholderText(
+ "Optional preequilibrationConditionId"
+ )
self._dose_lbl = QLabel("Dose column:", self)
self._time_lbl = QLabel("Time:", self)
- self._preeq_lbl = QLabel("Preequilibration condition (optional):", self)
+ self._preeq_lbl = QLabel(
+ "Preequilibration condition (optional):", self
+ )
ok = QPushButton("OK", self)
ok.clicked.connect(self.accept)
cancel = QPushButton("Cancel", self)
diff --git a/src/petab_gui/views/simple_plot_view.py b/src/petab_gui/views/simple_plot_view.py
index a3e0759..fcce794 100644
--- a/src/petab_gui/views/simple_plot_view.py
+++ b/src/petab_gui/views/simple_plot_view.py
@@ -77,8 +77,7 @@ def __init__(self, parent=None):
self.observable_to_subplot = {}
def initialize(
- self, meas_proxy, sim_proxy, cond_proxy, vis_proxy,
- petab_model
+ self, meas_proxy, sim_proxy, cond_proxy, vis_proxy, petab_model
):
self.meas_proxy = meas_proxy
self.cond_proxy = cond_proxy
@@ -125,44 +124,60 @@ def plot_it(self):
conditions_df,
measurements_df,
simulations_df,
- group_by
+ group_by,
)
worker.signals.finished.connect(self._render_on_main_thread)
QThreadPool.globalInstance().start(worker)
def _render_on_main_thread(self, payload):
import petab.v1.visualize as petab_vis
+
# GUI-thread plotting
- plt.close('all')
- meas_df = payload.get('meas_df')
- cond_df = payload.get('cond_df')
- if meas_df is None or meas_df.empty or cond_df is None or cond_df.empty:
+ plt.close("all")
+ meas_df = payload.get("meas_df")
+ cond_df = payload.get("cond_df")
+ if (
+ meas_df is None
+ or meas_df.empty
+ or cond_df is None
+ or cond_df.empty
+ ):
self._update_tabs(None)
return
- sim_df = payload.get('sim_df')
- group_by = payload.get('group_by')
- if group_by == 'vis_df':
- vis_df = payload.get('vis_df')
+ sim_df = payload.get("sim_df")
+ group_by = payload.get("group_by")
+ if group_by == "vis_df":
+ vis_df = payload.get("vis_df")
if vis_df is not None and not vis_df.empty:
try:
- petab_vis.plot_with_vis_spec(vis_df, cond_df, meas_df, sim_df)
+ petab_vis.plot_with_vis_spec(
+ vis_df, cond_df, meas_df, sim_df
+ )
fig = plt.gcf()
self._update_tabs(fig)
return
except Exception as e:
- print(f'Invalid Visualisation DF: {e}')
+ print(f"Invalid Visualisation DF: {e}")
# fallback to observable grouping
- plt.close('all')
+ plt.close("all")
petab_vis.plot_without_vis_spec(
- cond_df, measurements_df=meas_df, simulations_df=sim_df, group_by='observable'
+ cond_df,
+ measurements_df=meas_df,
+ simulations_df=sim_df,
+ group_by="observable",
)
else:
- plt.close('all')
+ plt.close("all")
petab_vis.plot_without_vis_spec(
- cond_df, measurements_df=meas_df, simulations_df=sim_df, group_by=group_by
+ cond_df,
+ measurements_df=meas_df,
+ simulations_df=sim_df,
+ group_by=group_by,
)
fig = plt.gcf()
- fig.subplots_adjust(left=0.12, bottom=0.15, right=0.95, top=0.9, wspace=0.3, hspace=0.4)
+ fig.subplots_adjust(
+ left=0.12, bottom=0.15, right=0.95, top=0.9, wspace=0.3, hspace=0.4
+ )
self._update_tabs(fig)
def _update_tabs(self, fig: plt.Figure):
@@ -239,7 +254,9 @@ def _update_tabs(self, fig: plt.Figure):
# Plot residuals if necessary
self.plot_residuals()
- def highlight_from_selection(self, selected_rows: list[int], proxy=None, y_axis_col="measurement"):
+ def highlight_from_selection(
+ self, selected_rows: list[int], proxy=None, y_axis_col="measurement"
+ ):
proxy = proxy or self.meas_proxy
if not proxy:
return
@@ -296,15 +313,15 @@ def plot_residuals(self):
plot_goodness_of_fit,
plot_residuals_vs_simulation,
)
+
fig_res, axes = plt.subplots(
- 1, 2,
- sharey=True, constrained_layout=True, width_ratios=[2, 1]
+ 1, 2, sharey=True, constrained_layout=True, width_ratios=[2, 1]
)
try:
plot_residuals_vs_simulation(
problem,
simulations_df,
- axes = axes,
+ axes=axes,
)
create_plot_tab(fig_res, self, "Residuals vs Simulation")
except ValueError as e:
@@ -314,7 +331,7 @@ def plot_residuals(self):
plot_goodness_of_fit(
problem,
simulations_df,
- ax = axes_fit,
+ ax=axes_fit,
)
# fig_fit.tight_layout()
create_plot_tab(fig_fit, self, "Goodness of Fit")
@@ -322,8 +339,10 @@ def plot_residuals(self):
class MeasurementHighlighter:
def __init__(self):
- self.highlight_scatters = defaultdict(list) # (subplot index) → scatter artist
- self.point_index_map = {} # (subplot index, observableId, x, y) → row index
+ self.highlight_scatters = defaultdict(
+ list
+ ) # (subplot index) → scatter artist
+ self.point_index_map = {} # (subplot index, observableId, x, y) → row index
self.click_callback = None
def clear_highlight(self):
@@ -331,7 +350,7 @@ def clear_highlight(self):
def register_subplot(self, ax, subplot_idx):
scatter = ax.scatter(
- [], [], s=80, edgecolors='black', facecolors='none', zorder=5
+ [], [], s=80, edgecolors="black", facecolors="none", zorder=5
)
self.highlight_scatters[subplot_idx].append(scatter)
@@ -390,7 +409,7 @@ class ToolbarOptionManager(QObject):
def __new__(cls):
if cls._instance is None:
- cls._instance = super(ToolbarOptionManager, cls).__new__(cls)
+ cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
@@ -427,7 +446,9 @@ def __init__(self, canvas, parent):
)
for grp, action in self.groupy_by_options.items():
action.setCheckable(True)
- action.triggered.connect(lambda _, grp=grp: self.manager.set_option(grp))
+ action.triggered.connect(
+ lambda _, grp=grp: self.manager.set_option(grp)
+ )
self.settings_menu.addAction(action)
self.manager.option_changed.connect(self.update_checked_state)
self.update_checked_state(self.manager.get_option())
@@ -441,9 +462,7 @@ def update_checked_state(self, selected_option):
def create_plot_tab(
- figure,
- plotter: MeasurementPlotter,
- plot_title: str = "New Plot"
+ figure, plotter: MeasurementPlotter, plot_title: str = "New Plot"
) -> FigureCanvas:
"""Create a new tab with the given figure and plotter."""
canvas = FigureCanvas(figure)
diff --git a/src/petab_gui/views/utils.py b/src/petab_gui/views/utils.py
index 2b052d3..6752c3e 100644
--- a/src/petab_gui/views/utils.py
+++ b/src/petab_gui/views/utils.py
@@ -9,11 +9,8 @@ def proxy_to_dataframe(proxy_model):
headers = [proxy_model.headerData(c, Qt.Horizontal) for c in range(cols)]
data = []
- for r in range(rows-1):
- row = {
- headers[c]: proxy_model.index(r, c).data()
- for c in range(cols)
- }
+ for r in range(rows - 1):
+ row = {headers[c]: proxy_model.index(r, c).data() for c in range(cols)}
for key, value in row.items():
if isinstance(value, str) and value == "":
row[key] = None
diff --git a/src/petab_gui/views/whats_this.py b/src/petab_gui/views/whats_this.py
index 00ce136..4a5d7ee 100644
--- a/src/petab_gui/views/whats_this.py
+++ b/src/petab_gui/views/whats_this.py
@@ -1,439 +1,440 @@
"""What's This? Dialog desciptions of the widgets."""
+
WHATS_THIS = {
- "tabs": {
- "data_tables": (
- "Data Tables
"
- ""
- "- Edit PEtab tables: Measurement, Observable, Parameter, Condition, Visualization.
"
- "- Hover headers for definitions; right-click for context actions.
"
- "- Keep IDs consistent across tables (e.g.,
observableId, condition IDs). "
- "- Import/export to manage files; validation highlights issues.
"
- "
"
- ),
- "sbml_model": (
- "SBML Model
"
- ""
- "- Edit SBML (XML) and Antimony side-by-side.
"
- "- Use the Forward Changes buttons to convert/sync between views.
"
- "- Errors and warnings appear in the logger panel below.
"
- "- Some constructs may not round-trip perfectly; keep the canonical SBML copy.
"
- "
"
- ),
- },
- "sbml_view": {
- "sbml_editor": (
- "SBML editor (XML)
"
- ""
- "- Paste or edit valid SBML (L2/L3); keep namespaces intact.
"
- "- Click Forward → Antimony to generate Antimony.
"
- "- Prefer this pane for full SBML feature coverage.
"
- "- Conversion issues are reported in the logger.
"
- "
"
- ),
- "antimony_editor": (
- "Antimony editor
"
- ""
- "- Human-readable model syntax compiled to SBML.
"
- "- Click Forward → SBML to regenerate XML.
"
- "- Use for quick edits; verify semantics after conversion.
"
- "- Syntax/convert errors will appear in the logger.
"
- "
"
- ),
- },
- "tables": {
- "measurement": {
- "table": (
- "Measurement table
"
- ""
- "- Each row is a data point with time, value, and linked IDs.
"
- "observableId must exist in Observable; "
- "condition IDs must exist in Condition. "
- "- Use
'inf' for steady-state times. "
- "- Override placeholders
observableParameter{n}_{observableId} "
- "and noise parameters noiseParameter{n}_{observableId} when defined. "
- "
"
- ),
- "columns": {
- "observableId": (
- ""
- "- Reference to an observable defined in the Observable table.
"
- "- Must match an existing
observableId. "
- "
"
- ),
- "preequilibrationConditionId": (
- ""
- "- Condition used for pre-equilibration; empty = none.
"
- "- Must be a valid condition ID if provided.
"
- "
"
- ),
- "simulationConditionId": (
- ""
- "- Condition used for simulation parameters (required).
"
- "- Must be a valid condition ID.
"
- "
"
- ),
- "time": (
- ""
- "- Numeric time in SBML units, or
'inf' for steady state. "
- "- Use a consistent unit system across data and model.
"
- "
"
- ),
- "measurement": (
- ""
- "- Observed numeric value in the same scale/units as the model output.
"
- "- Leave blank for missing values if supported by your workflow.
"
- "
"
- ),
- "observableParameters": (
- ""
- "- Overrides for placeholders defined in the observable formula.
"
- "- Provide
n semicolon-separated values/names for "
- "observableParameter{n}_{observableId}; empty if none. "
- "
"
- ),
- "noiseParameters": (
- ""
- "- Noise std-dev (or parameter names);
NaN if σ is a model parameter. "
- "- Same rules as
observableParameters for lists and naming. "
- "
"
- ),
- "datasetId": (
- ""
- "- Grouping key for plotting (datasets share style/axes).
"
- "- Optional; defaults to per-row if omitted.
"
- "
"
- ),
- "replicateId": (
- ""
- "- Label to distinguish replicates within a dataset.
"
- "- Enables error bars/replicate plotting modes.
"
- "
"
- ),
- },
- },
- "simulation": {
- "table": (
- "Simulation table
"
- ""
- "- Holds simulated outputs aligned to measurement definitions.
"
- "- Same IDs as Measurement (observable/conditions/time) for comparison.
"
- "- Populated by simulator/export; typically read-only.
"
- "
"
- ),
- "columns": {
- "observableId": (
- ""
- "- Observable whose simulation is reported.
"
- "- Must match an
observableId in Observable. "
- "
"
- ),
- "preequilibrationConditionId": (
- ""
- "- Preequilibration condition used during simulation; empty = none.
"
- "- Must be a valid condition ID if set.
"
- "
"
- ),
- "simulationConditionId": (
- ""
- "- Condition used to set simulation parameters (required).
"
- "- Must be a valid condition ID.
"
- "
"
- ),
- "time": (
- ""
- "- Time point for the simulated value (numeric or
'inf'). "
- "- Use same units as the model.
"
- "
"
- ),
- "simulation": (
- ""
- "- Simulated numeric value (same scale/units as measurement).
"
- "- Used for plotting and residuals.
"
- "
"
- ),
- "observableParameters": (
- ""
- "- Parameters used to evaluate observable placeholders, if applicable.
"
- "- Semicolon-separated values/names; mirrors Measurement rules.
"
- "
"
- ),
- "noiseParameters": (
- ""
- "- Noise parameters applied for simulation/plotting modes.
"
- "- Numeric values or names; may be empty.
"
- "
"
- ),
- "datasetId": (
- ""
- "- Dataset grouping to match plotted series.
"
- "- Optional; align with Measurement for overlays.
"
- "
"
- ),
- "replicateId": (
- ""
- "- Replicate label, if simulations are per-replicate.
"
- "- Usually empty unless replicates are simulated explicitly.
"
- "
"
- ),
- },
- },
- "observable": {
- "table": (
- "Observable table
"
- ""
- "- Defines how model states/expressions map to measured outputs.
"
- "- May introduce placeholders
observableParameter{n}_{observableId} "
- "that are overridden per-measurement. "
- "- Noise model can be numeric σ or a formula; distribution optional.
"
- "
"
- ),
- "columns": {
- "observableId": (
- ""
- "- Unique identifier (letters/digits/underscores; not starting with a digit).
"
- "- Referenced by
measurement.observableId. "
- "
"
- ),
- "observableName": (
- ""
- "- Optional display name for reports/plots.
"
- "- Not used for identification.
"
- "
"
- ),
- "observableFormula": (
- ""
- "- Expression using SBML symbols/parameters (e.g., species ID).
"
- "- May define
observableParameter{n}_{observableId} placeholders. "
- "
"
- ),
- "observableTransformation": (
- ""
- "- Transformation for objective:
lin, log, or log10. "
- "- Defaults to
lin; data and outputs assumed linear if not set. "
- "
"
- ),
- "noiseFormula": (
- ""
- "- Numeric σ (implies normal) or formula for complex noise.
"
- "- May include
noiseParameter{n}_{observableId}; "
- "values provided in Measurement. "
- "
"
- ),
- "noiseDistribution": (
- ""
- "normal (σ = std dev) or laplace (σ = scale). "
- "- Log-variants via
observableTransformation = log/log10. "
- "
"
- ),
- },
- },
- "parameter": {
- "table": (
- "Parameter table
"
- ""
- "- Declares parameters, estimation flag, and bounds (linear space).
"
- "parameterId must match SBML or overrides used elsewhere. "
- "- Optional priors for initialization and/or objective.
"
- "
"
- ),
- "columns": {
- "parameterId": (
- ""
- "- Must match an SBML parameter, a condition override, or names used in measurements.
"
- "- Unique within this table.
"
- "
"
- ),
- "parameterName": (
- ""
- "- Optional display name for plots/reports.
"
- "- May differ from SBML name.
"
- "
"
- ),
- "parameterScale": (
- ""
- "- Estimation scale:
lin, log, or log10. "
- "- Affects optimization scaling, not storage format.
"
- "
"
- ),
- "lowerBound": (
- ""
- "- Numeric lower bound in linear space.
"
- "- Optional if
estimate==0. "
- "
"
- ),
- "upperBound": (
- ""
- "- Numeric upper bound in linear space.
"
- "- Optional if
estimate==0. "
- "
"
- ),
- "nominalValue": (
- ""
- "- Value used when fixed (
estimate==0), in linear space. "
- "- Optional otherwise.
"
- "
"
- ),
- "estimate": (
- ""
- "1 = estimated; 0 = fixed to nominal value. "
- "- Controls inclusion in the optimization vector.
"
- "
"
+ "tabs": {
+ "data_tables": (
+ "Data Tables
"
+ ""
+ "- Edit PEtab tables: Measurement, Observable, Parameter, Condition, Visualization.
"
+ "- Hover headers for definitions; right-click for context actions.
"
+ "- Keep IDs consistent across tables (e.g.,
observableId, condition IDs). "
+ "- Import/export to manage files; validation highlights issues.
"
+ "
"
+ ),
+ "sbml_model": (
+ "SBML Model
"
+ ""
+ "- Edit SBML (XML) and Antimony side-by-side.
"
+ "- Use the Forward Changes buttons to convert/sync between views.
"
+ "- Errors and warnings appear in the logger panel below.
"
+ "- Some constructs may not round-trip perfectly; keep the canonical SBML copy.
"
+ "
"
),
- "initializationPriorType": (
- ""
- "- Prior for initial point sampling (e.g.,
uniform, normal, "
- "parameterScaleUniform). "
- "- Defaults to
parameterScaleUniform. "
- "
"
- ),
- "initializationPriorParameters": (
- ""
- "- Semicolon-separated numeric parameters; default
lowerBound;upperBound. "
- "- Linear scale unless using parameter-scale priors.
"
- "
"
- ),
- "objectivePriorType": (
- ""
- "- Prior contributing to the objective; same options as initialization prior.
"
- "- Optional; omit for unregularized fits.
"
- "
"
- ),
- "objectivePriorParameters": (
- ""
- "- Semicolon-separated numeric parameters; see initialization prior for formats.
"
- "- Scale rules mirror the chosen prior type.
"
- "
"
- ),
- },
},
- "condition": {
- "table": (
- "Condition table
"
- ""
- "- Defines simulation/experimental conditions referenced by other tables.
"
- "- User-defined columns must be SBML IDs (parameter/species/compartment).
"
- "- Species values act as initial conditions;
"
- "
"
- ),
- "columns": {
- "conditionId": (
- ""
- "- Unique identifier (letters/digits/underscores; not starting with a digit).
"
- "- Referenced by Measurement and Simulation.
"
- "
"
+ "sbml_view": {
+ "sbml_editor": (
+ "SBML editor (XML)
"
+ ""
+ "- Paste or edit valid SBML (L2/L3); keep namespaces intact.
"
+ "- Click Forward → Antimony to generate Antimony.
"
+ "- Prefer this pane for full SBML feature coverage.
"
+ "- Conversion issues are reported in the logger.
"
+ "
"
+ ),
+ "antimony_editor": (
+ "Antimony editor
"
+ ""
+ "- Human-readable model syntax compiled to SBML.
"
+ "- Click Forward → SBML to regenerate XML.
"
+ "- Use for quick edits; verify semantics after conversion.
"
+ "- Syntax/convert errors will appear in the logger.
"
+ "
"
),
- "conditionName": (
- ""
- "- Optional human-readable name for reports/plots.
"
- "- Does not affect model execution.
"
- "
"
- ),
- "*": (
- ""
- "- User-defined column. Must be an SBML ID: parameter, species, or compartment.
"
- "- Numbers or IDs allowed; species = initial amount/concentration "
- "(
NaN keeps preeq/initial), compartments = initial size. "
- "
"
- ),
- },
},
- "visualization": {
- "table": (
- "Visualization table
"
- ""
- "- Groups datasets into plots and configures axes/scales.
"
- "plotId collects series into the same axes. "
- "- Choose simulation/data types; set labels and offsets.
"
- "
"
- ),
- "columns": {
- "plotId": (
- ""
- "- Plot grouping key; identical IDs share the same axes.
"
- "- Required for multi-series plots.
"
- "
"
- ),
- "plotName": (
- ""
- "- Optional display name for the plot.
"
- "- Used in UIs/exports; not an ID.
"
- "
"
- ),
- "plotTypeSimulation": (
- ""
- "LinePlot | BarPlot | ScatterPlot. "
- "- Default is
LinePlot. "
- "
"
- ),
- "plotTypeData": (
- ""
- "MeanAndSD | MeanAndSEM | replicate | provided. "
- "- Default is
MeanAndSD. "
- "
"
- ),
- "datasetId": (
- ""
- "- Includes datasets (from Measurement) in this plot.
"
- "- Optional; multiple IDs → multiple series.
"
- "
"
- ),
- "xValues": (
- ""
- "- Independent variable:
time (default) or parameter/state ID. "
- "- Values appear as x-axis ticks.
"
- "
"
- ),
- "xOffset": (
- ""
- "- Numeric offset applied to x values (default 0).
"
- "- Use to disambiguate overlapping series.
"
- "
"
- ),
- "xLabel": (
- ""
- "- Custom x-axis label; defaults to
xValues. "
- "- Use units where helpful.
"
- "
"
- ),
- "xScale": (
- ""
- "lin | log | log10 | order (LinePlot only). "
- "- Default is
lin; order places points equidistantly. "
- "
"
- ),
- "yValues": (
- ""
- "- Observable ID to plot on the y-axis.
"
- "- Must match
measurement.observableId for overlays. "
- "
"
- ),
- "yOffset": (
- ""
- "- Numeric offset applied to y values (default 0).
"
- "- Use for stacked/shifted visuals.
"
- "
"
- ),
- "yLabel": (
- ""
- "- Custom y-axis label; defaults to
yValues. "
- "- Include units where applicable.
"
- "
"
- ),
- "yScale": (
- ""
- "lin | log | log10. "
- "- Default is
lin. "
- "
"
- ),
- "legendEntry": (
- ""
- "- Legend text; defaults to
datasetId. "
- "- Use concise, descriptive names.
"
- "
"
- ),
- },
+ "tables": {
+ "measurement": {
+ "table": (
+ "Measurement table
"
+ ""
+ "- Each row is a data point with time, value, and linked IDs.
"
+ "observableId must exist in Observable; "
+ "condition IDs must exist in Condition. "
+ "- Use
'inf' for steady-state times. "
+ "- Override placeholders
observableParameter{n}_{observableId} "
+ "and noise parameters noiseParameter{n}_{observableId} when defined. "
+ "
"
+ ),
+ "columns": {
+ "observableId": (
+ ""
+ "- Reference to an observable defined in the Observable table.
"
+ "- Must match an existing
observableId. "
+ "
"
+ ),
+ "preequilibrationConditionId": (
+ ""
+ "- Condition used for pre-equilibration; empty = none.
"
+ "- Must be a valid condition ID if provided.
"
+ "
"
+ ),
+ "simulationConditionId": (
+ ""
+ "- Condition used for simulation parameters (required).
"
+ "- Must be a valid condition ID.
"
+ "
"
+ ),
+ "time": (
+ ""
+ "- Numeric time in SBML units, or
'inf' for steady state. "
+ "- Use a consistent unit system across data and model.
"
+ "
"
+ ),
+ "measurement": (
+ ""
+ "- Observed numeric value in the same scale/units as the model output.
"
+ "- Leave blank for missing values if supported by your workflow.
"
+ "
"
+ ),
+ "observableParameters": (
+ ""
+ "- Overrides for placeholders defined in the observable formula.
"
+ "- Provide
n semicolon-separated values/names for "
+ "observableParameter{n}_{observableId}; empty if none. "
+ "
"
+ ),
+ "noiseParameters": (
+ ""
+ "- Noise std-dev (or parameter names);
NaN if σ is a model parameter. "
+ "- Same rules as
observableParameters for lists and naming. "
+ "
"
+ ),
+ "datasetId": (
+ ""
+ "- Grouping key for plotting (datasets share style/axes).
"
+ "- Optional; defaults to per-row if omitted.
"
+ "
"
+ ),
+ "replicateId": (
+ ""
+ "- Label to distinguish replicates within a dataset.
"
+ "- Enables error bars/replicate plotting modes.
"
+ "
"
+ ),
+ },
+ },
+ "simulation": {
+ "table": (
+ "Simulation table
"
+ ""
+ "- Holds simulated outputs aligned to measurement definitions.
"
+ "- Same IDs as Measurement (observable/conditions/time) for comparison.
"
+ "- Populated by simulator/export; typically read-only.
"
+ "
"
+ ),
+ "columns": {
+ "observableId": (
+ ""
+ "- Observable whose simulation is reported.
"
+ "- Must match an
observableId in Observable. "
+ "
"
+ ),
+ "preequilibrationConditionId": (
+ ""
+ "- Preequilibration condition used during simulation; empty = none.
"
+ "- Must be a valid condition ID if set.
"
+ "
"
+ ),
+ "simulationConditionId": (
+ ""
+ "- Condition used to set simulation parameters (required).
"
+ "- Must be a valid condition ID.
"
+ "
"
+ ),
+ "time": (
+ ""
+ "- Time point for the simulated value (numeric or
'inf'). "
+ "- Use same units as the model.
"
+ "
"
+ ),
+ "simulation": (
+ ""
+ "- Simulated numeric value (same scale/units as measurement).
"
+ "- Used for plotting and residuals.
"
+ "
"
+ ),
+ "observableParameters": (
+ ""
+ "- Parameters used to evaluate observable placeholders, if applicable.
"
+ "- Semicolon-separated values/names; mirrors Measurement rules.
"
+ "
"
+ ),
+ "noiseParameters": (
+ ""
+ "- Noise parameters applied for simulation/plotting modes.
"
+ "- Numeric values or names; may be empty.
"
+ "
"
+ ),
+ "datasetId": (
+ ""
+ "- Dataset grouping to match plotted series.
"
+ "- Optional; align with Measurement for overlays.
"
+ "
"
+ ),
+ "replicateId": (
+ ""
+ "- Replicate label, if simulations are per-replicate.
"
+ "- Usually empty unless replicates are simulated explicitly.
"
+ "
"
+ ),
+ },
+ },
+ "observable": {
+ "table": (
+ "Observable table
"
+ ""
+ "- Defines how model states/expressions map to measured outputs.
"
+ "- May introduce placeholders
observableParameter{n}_{observableId} "
+ "that are overridden per-measurement. "
+ "- Noise model can be numeric σ or a formula; distribution optional.
"
+ "
"
+ ),
+ "columns": {
+ "observableId": (
+ ""
+ "- Unique identifier (letters/digits/underscores; not starting with a digit).
"
+ "- Referenced by
measurement.observableId. "
+ "
"
+ ),
+ "observableName": (
+ ""
+ "- Optional display name for reports/plots.
"
+ "- Not used for identification.
"
+ "
"
+ ),
+ "observableFormula": (
+ ""
+ "- Expression using SBML symbols/parameters (e.g., species ID).
"
+ "- May define
observableParameter{n}_{observableId} placeholders. "
+ "
"
+ ),
+ "observableTransformation": (
+ ""
+ "- Transformation for objective:
lin, log, or log10. "
+ "- Defaults to
lin; data and outputs assumed linear if not set. "
+ "
"
+ ),
+ "noiseFormula": (
+ ""
+ "- Numeric σ (implies normal) or formula for complex noise.
"
+ "- May include
noiseParameter{n}_{observableId}; "
+ "values provided in Measurement. "
+ "
"
+ ),
+ "noiseDistribution": (
+ ""
+ "normal (σ = std dev) or laplace (σ = scale). "
+ "- Log-variants via
observableTransformation = log/log10. "
+ "
"
+ ),
+ },
+ },
+ "parameter": {
+ "table": (
+ "Parameter table
"
+ ""
+ "- Declares parameters, estimation flag, and bounds (linear space).
"
+ "parameterId must match SBML or overrides used elsewhere. "
+ "- Optional priors for initialization and/or objective.
"
+ "
"
+ ),
+ "columns": {
+ "parameterId": (
+ ""
+ "- Must match an SBML parameter, a condition override, or names used in measurements.
"
+ "- Unique within this table.
"
+ "
"
+ ),
+ "parameterName": (
+ ""
+ "- Optional display name for plots/reports.
"
+ "- May differ from SBML name.
"
+ "
"
+ ),
+ "parameterScale": (
+ ""
+ "- Estimation scale:
lin, log, or log10. "
+ "- Affects optimization scaling, not storage format.
"
+ "
"
+ ),
+ "lowerBound": (
+ ""
+ "- Numeric lower bound in linear space.
"
+ "- Optional if
estimate==0. "
+ "
"
+ ),
+ "upperBound": (
+ ""
+ "- Numeric upper bound in linear space.
"
+ "- Optional if
estimate==0. "
+ "
"
+ ),
+ "nominalValue": (
+ ""
+ "- Value used when fixed (
estimate==0), in linear space. "
+ "- Optional otherwise.
"
+ "
"
+ ),
+ "estimate": (
+ ""
+ "1 = estimated; 0 = fixed to nominal value. "
+ "- Controls inclusion in the optimization vector.
"
+ "
"
+ ),
+ "initializationPriorType": (
+ ""
+ "- Prior for initial point sampling (e.g.,
uniform, normal, "
+ "parameterScaleUniform). "
+ "- Defaults to
parameterScaleUniform. "
+ "
"
+ ),
+ "initializationPriorParameters": (
+ ""
+ "- Semicolon-separated numeric parameters; default
lowerBound;upperBound. "
+ "- Linear scale unless using parameter-scale priors.
"
+ "
"
+ ),
+ "objectivePriorType": (
+ ""
+ "- Prior contributing to the objective; same options as initialization prior.
"
+ "- Optional; omit for unregularized fits.
"
+ "
"
+ ),
+ "objectivePriorParameters": (
+ ""
+ "- Semicolon-separated numeric parameters; see initialization prior for formats.
"
+ "- Scale rules mirror the chosen prior type.
"
+ "
"
+ ),
+ },
+ },
+ "condition": {
+ "table": (
+ "Condition table
"
+ ""
+ "- Defines simulation/experimental conditions referenced by other tables.
"
+ "- User-defined columns must be SBML IDs (parameter/species/compartment).
"
+ "- Species values act as initial conditions;
"
+ "
"
+ ),
+ "columns": {
+ "conditionId": (
+ ""
+ "- Unique identifier (letters/digits/underscores; not starting with a digit).
"
+ "- Referenced by Measurement and Simulation.
"
+ "
"
+ ),
+ "conditionName": (
+ ""
+ "- Optional human-readable name for reports/plots.
"
+ "- Does not affect model execution.
"
+ "
"
+ ),
+ "*": (
+ ""
+ "- User-defined column. Must be an SBML ID: parameter, species, or compartment.
"
+ "- Numbers or IDs allowed; species = initial amount/concentration "
+ "(
NaN keeps preeq/initial), compartments = initial size. "
+ "
"
+ ),
+ },
+ },
+ "visualization": {
+ "table": (
+ "Visualization table
"
+ ""
+ "- Groups datasets into plots and configures axes/scales.
"
+ "plotId collects series into the same axes. "
+ "- Choose simulation/data types; set labels and offsets.
"
+ "
"
+ ),
+ "columns": {
+ "plotId": (
+ ""
+ "- Plot grouping key; identical IDs share the same axes.
"
+ "- Required for multi-series plots.
"
+ "
"
+ ),
+ "plotName": (
+ ""
+ "- Optional display name for the plot.
"
+ "- Used in UIs/exports; not an ID.
"
+ "
"
+ ),
+ "plotTypeSimulation": (
+ ""
+ "LinePlot | BarPlot | ScatterPlot. "
+ "- Default is
LinePlot. "
+ "
"
+ ),
+ "plotTypeData": (
+ ""
+ "MeanAndSD | MeanAndSEM | replicate | provided. "
+ "- Default is
MeanAndSD. "
+ "
"
+ ),
+ "datasetId": (
+ ""
+ "- Includes datasets (from Measurement) in this plot.
"
+ "- Optional; multiple IDs → multiple series.
"
+ "
"
+ ),
+ "xValues": (
+ ""
+ "- Independent variable:
time (default) or parameter/state ID. "
+ "- Values appear as x-axis ticks.
"
+ "
"
+ ),
+ "xOffset": (
+ ""
+ "- Numeric offset applied to x values (default 0).
"
+ "- Use to disambiguate overlapping series.
"
+ "
"
+ ),
+ "xLabel": (
+ ""
+ "- Custom x-axis label; defaults to
xValues. "
+ "- Use units where helpful.
"
+ "
"
+ ),
+ "xScale": (
+ ""
+ "lin | log | log10 | order (LinePlot only). "
+ "- Default is
lin; order places points equidistantly. "
+ "
"
+ ),
+ "yValues": (
+ ""
+ "- Observable ID to plot on the y-axis.
"
+ "- Must match
measurement.observableId for overlays. "
+ "
"
+ ),
+ "yOffset": (
+ ""
+ "- Numeric offset applied to y values (default 0).
"
+ "- Use for stacked/shifted visuals.
"
+ "
"
+ ),
+ "yLabel": (
+ ""
+ "- Custom y-axis label; defaults to
yValues. "
+ "- Include units where applicable.
"
+ "
"
+ ),
+ "yScale": (
+ ""
+ "lin | log | log10. "
+ "- Default is
lin. "
+ "
"
+ ),
+ "legendEntry": (
+ ""
+ "- Legend text; defaults to
datasetId. "
+ "- Use concise, descriptive names.
"
+ "
"
+ ),
+ },
+ },
},
- },
}