Skip to content

Commit 7231eba

Browse files
authored
Add validation for visualization files (#184)
Some basic validation of visualization files. Closes #8, closes #1.
1 parent a0817db commit 7231eba

File tree

5 files changed

+181
-1
lines changed

5 files changed

+181
-1
lines changed

petab/C.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@
234234
#: Supported plot types
235235
PLOT_TYPES_SIMULATION = [LINE_PLOT, BAR_PLOT, SCATTER_PLOT]
236236

237+
#: Supported xScales
238+
X_SCALES = [LIN, LOG, LOG10]
239+
240+
#: Supported yScales
241+
Y_SCALES = [LIN, LOG, LOG10]
242+
237243

238244
#:
239245
MEAN_AND_SD = 'MeanAndSD'

petab/lint.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,13 @@ def lint_problem(problem: 'petab.Problem') -> bool:
842842
logger.error(e)
843843
errors_occurred = True
844844

845+
if problem.visualization_df is not None:
846+
logger.info("Checking visualization table...")
847+
from petab.visualize.lint import validate_visualization_df
848+
errors_occurred |= validate_visualization_df(problem)
849+
else:
850+
logger.warning("Visualization table not available. Skipping.")
851+
845852
if errors_occurred:
846853
logger.error('Not OK')
847854
elif problem.measurement_df is None or problem.condition_df is None \

petab/petablint.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def parse_cli_args():
5050
help='Conditions table')
5151
parser.add_argument('-p', '--parameters', dest='parameter_file_name',
5252
help='Parameter table')
53+
parser.add_argument('--vis', '--visualizations',
54+
dest='visualization_file_name',
55+
help='Visualization table')
5356

5457
group = parser.add_mutually_exclusive_group()
5558
group.add_argument('-y', '--yaml', dest='yaml_file_name',
@@ -109,14 +112,18 @@ def main():
109112
logger.debug(f'\tMeasurement table: {args.measurement_file_name}')
110113
if args.parameter_file_name:
111114
logger.debug(f'\tParameter table: {args.parameter_file_name}')
115+
if args.visualization_file_name:
116+
logger.debug('\tVisualization table: '
117+
f'{args.visualization_file_name}')
112118

113119
try:
114120
problem = petab.Problem.from_files(
115121
sbml_file=args.sbml_file_name,
116122
condition_file=args.condition_file_name,
117123
measurement_file=args.measurement_file_name,
118124
parameter_file=args.parameter_file_name,
119-
observable_files=args.observable_file_name
125+
observable_files=args.observable_file_name,
126+
visualization_files=args.visualization_file_name,
120127
)
121128
except FileNotFoundError as e:
122129
logger.error(e)

petab/visualize/lint.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Validation of PEtab visualization files"""
2+
import logging
3+
4+
import pandas as pd
5+
6+
from .. import C, Problem
7+
from ..C import VISUALIZATION_DF_REQUIRED_COLS
8+
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def validate_visualization_df(
14+
problem: Problem
15+
) -> bool:
16+
"""Validate visualization table
17+
18+
Arguments:
19+
problem: The PEtab problem containing a visualization table
20+
21+
Returns:
22+
``True`` if errors occurred, ``False`` otherwise
23+
"""
24+
vis_df = problem.visualization_df
25+
if vis_df is None or vis_df.empty:
26+
return False
27+
28+
errors = False
29+
30+
if missing_req_cols := (set(VISUALIZATION_DF_REQUIRED_COLS)
31+
- set(vis_df.columns)):
32+
logger.error(f"Missing required columns {missing_req_cols} "
33+
"in visualization table.")
34+
errors = True
35+
36+
# Set all unspecified optional values to their defaults to simplify
37+
# validation
38+
vis_df = vis_df.copy()
39+
_apply_defaults(vis_df)
40+
41+
if unknown_types := (set(vis_df[C.PLOT_TYPE_SIMULATION].unique())
42+
- set(C.PLOT_TYPES_SIMULATION)):
43+
logger.error(f"Unknown {C.PLOT_TYPE_SIMULATION}: {unknown_types}. "
44+
f"Must be one of {C.PLOT_TYPES_SIMULATION}")
45+
errors = True
46+
47+
if unknown_types := (set(vis_df[C.PLOT_TYPE_DATA].unique())
48+
- set(C.PLOT_TYPES_DATA)):
49+
logger.error(f"Unknown {C.PLOT_TYPE_DATA}: {unknown_types}. "
50+
f"Must be one of {C.PLOT_TYPES_DATA}")
51+
errors = True
52+
53+
if unknown_scale := (set(vis_df[C.X_SCALE].unique())
54+
- set(C.X_SCALES)):
55+
logger.error(f"Unknown {C.X_SCALE}: {unknown_scale}. "
56+
f"Must be one of {C.X_SCALES}")
57+
errors = True
58+
59+
if any(
60+
(vis_df[C.X_SCALE] == 'order')
61+
& (vis_df[C.PLOT_TYPE_SIMULATION] != C.LINE_PLOT)
62+
):
63+
logger.error(f"{C.X_SCALE}=order is only allowed with "
64+
f"{C.PLOT_TYPE_SIMULATION}={C.LINE_PLOT}.")
65+
errors = True
66+
67+
if unknown_scale := (set(vis_df[C.Y_SCALE].unique())
68+
- set(C.Y_SCALES)):
69+
logger.error(f"Unknown {C.Y_SCALE}: {unknown_scale}. "
70+
f"Must be one of {C.Y_SCALES}")
71+
errors = True
72+
73+
if problem.condition_df is not None:
74+
# check for ambiguous values
75+
reserved_names = {C.TIME, "condition"}
76+
for reserved_name in reserved_names:
77+
if reserved_name in problem.condition_df \
78+
and reserved_name in vis_df[C.X_VALUES]:
79+
logger.error(f"Ambiguous value for `{C.X_VALUES}`: "
80+
f"`{reserved_name}` has a special meaning as "
81+
f"`{C.X_VALUES}`, but there exists also a model "
82+
"entity with that name.")
83+
errors = True
84+
85+
# check xValues exist in condition table
86+
for xvalue in set(vis_df[C.X_VALUES].unique()) - reserved_names:
87+
if xvalue not in problem.condition_df:
88+
logger.error(f"{C.X_VALUES} was set to `{xvalue}`, but no "
89+
"such column exists in the conditions table.")
90+
errors = True
91+
92+
if problem.observable_df is not None:
93+
# yValues must be an observable
94+
for yvalue in vis_df[C.Y_VALUES].unique():
95+
if yvalue not in problem.observable_df.index:
96+
logger.error(
97+
f"{C.Y_VALUES} was set to `{yvalue}`, but no such "
98+
"observable exists in the observables table."
99+
)
100+
errors = True
101+
102+
return errors
103+
104+
105+
def _apply_defaults(vis_df: pd.DataFrame):
106+
"""
107+
Set default values.
108+
109+
Adds default values to the given visualization table where no value was
110+
specified.
111+
"""
112+
def set_default(column: str, value):
113+
if column not in vis_df:
114+
vis_df[column] = value
115+
elif value is not None:
116+
vis_df[column].fillna(value)
117+
118+
set_default(C.PLOT_NAME, "")
119+
set_default(C.PLOT_TYPE_SIMULATION, C.LINE_PLOT)
120+
set_default(C.PLOT_TYPE_DATA, C.MEAN_AND_SD)
121+
set_default(C.DATASET_ID, None)
122+
set_default(C.X_VALUES, C.TIME)
123+
set_default(C.X_OFFSET, 0)
124+
set_default(C.X_LABEL, vis_df[C.X_VALUES])
125+
set_default(C.X_SCALE, C.LIN)
126+
set_default(C.Y_VALUES, None)
127+
set_default(C.Y_OFFSET, 0)
128+
set_default(C.Y_LABEL, vis_df[C.Y_VALUES])
129+
set_default(C.Y_SCALE, C.LIN)
130+
set_default(C.LEGEND_ENTRY, vis_df[C.DATASET_ID])

tests/test_visualization.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import matplotlib.pyplot as plt
77
import pytest
88

9+
import petab
910
from petab.C import *
1011
from petab.visualize import plot_with_vis_spec, plot_without_vis_spec
1112
from petab.visualize.plotting import VisSpecParser
13+
from petab.visualize.lint import validate_visualization_df
1214

1315
# Avoid errors when plotting without X server
1416
plt.switch_backend('agg')
@@ -135,6 +137,12 @@ def test_visualization_with_vis_and_sim(data_file_Isensee,
135137
condition_file_Isensee,
136138
vis_spec_file_Isensee,
137139
simulation_file_Isensee):
140+
validate_visualization_df(
141+
petab.Problem(
142+
condition_df=petab.get_condition_df(condition_file_Isensee),
143+
visualization_df=petab.get_visualization_df(vis_spec_file_Isensee),
144+
)
145+
)
138146
plot_with_vis_spec(vis_spec_file_Isensee, condition_file_Isensee,
139147
data_file_Isensee, simulation_file_Isensee)
140148

@@ -366,3 +374,25 @@ def test_cli():
366374
"-o", temp_dir
367375
]
368376
subprocess.run(args, check=True)
377+
378+
379+
@pytest.mark.parametrize(
380+
"vis_file",
381+
(
382+
"vis_spec_file_Isensee",
383+
"vis_spec_file_Isensee_replicates",
384+
"vis_spec_file_Isensee_scatterplot",
385+
"visu_file_Fujita_wo_dsid_wo_yvalues",
386+
"visu_file_Fujita_all_obs_with_diff_settings",
387+
"visu_file_Fujita_empty",
388+
"visu_file_Fujita_minimal",
389+
"visu_file_Fujita_replicates",
390+
"visu_file_Fujita_small",
391+
)
392+
)
393+
def test_validate(vis_file, request):
394+
"""Check that all test files pass validation."""
395+
vis_file = request.getfixturevalue(vis_file)
396+
assert False is validate_visualization_df(
397+
petab.Problem(visualization_df=petab.get_visualization_df(vis_file))
398+
)

0 commit comments

Comments
 (0)