Skip to content

Commit e3a8f82

Browse files
dweindldilpath
andauthored
Add abstraction for (SBML) models (#133)
Add abstraction for models. This helps to keep libsbml code closely together and to potentially accommodate non-SBML models in the future(PEtab-dev/PEtab#538). Wraps `libsbml.Model` using `petab.models.Model` and replaces the respective function arguments. In the (what I consider) most relevant function for downstream use, the `sbml_model` argument is kept, but deprecated. Co-authored-by: Dilan Pathirana <[email protected]>
1 parent 9967ce1 commit e3a8f82

17 files changed

+673
-296
lines changed

doc/example/example_visualization.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,4 +264,4 @@
264264
},
265265
"nbformat": 4,
266266
"nbformat_minor": 2
267-
}
267+
}

doc/example/example_visualization_without_visspec.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,4 @@
168168
},
169169
"nbformat": 4,
170170
"nbformat_minor": 2
171-
}
171+
}

petab/C.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,6 @@
274274
RESIDUAL = 'residual'
275275
#:
276276
NOISE_VALUE = 'noiseValue'
277+
278+
# separator for multiple parameter values (bounds, observableParameters, ...)
279+
PARAMETER_SEPARATOR = ';'

petab/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ def flatten_timepoint_specific_output_overrides(
120120
replacement_id = ''
121121
for field in possible_groupvars:
122122
if field in groupvars:
123-
val = str(groupvar[groupvars.index(field)
124-
]).replace(';', '_').replace('.', '_')
123+
val = str(groupvar[groupvars.index(field)])\
124+
.replace(PARAMETER_SEPARATOR, '_').replace('.', '_')
125125
if replacement_id == '':
126126
replacement_id = val
127127
elif val != '':

petab/lint.py

Lines changed: 70 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from typing import Optional, Iterable, Any
88
from collections import Counter
99

10-
import libsbml
1110
import numpy as np
1211
import pandas as pd
1312
import sympy as sp
1413

1514
import petab
16-
from . import (core, parameters, sbml, measurements)
15+
from . import (core, parameters, measurements)
16+
from .models import Model
1717
from .C import * # noqa: F403
1818

1919
logger = logging.getLogger(__name__)
@@ -85,12 +85,12 @@ def assert_no_leading_trailing_whitespace(
8585

8686

8787
def check_condition_df(
88-
df: pd.DataFrame, sbml_model: Optional[libsbml.Model] = None) -> None:
88+
df: pd.DataFrame, model: Optional[Model] = None) -> None:
8989
"""Run sanity checks on PEtab condition table
9090
9191
Arguments:
9292
df: PEtab condition DataFrame
93-
sbml_model: SBML Model for additional checking of parameter IDs
93+
model: Model for additional checking of parameter IDs
9494
9595
Raises:
9696
AssertionError: in case of problems
@@ -117,16 +117,14 @@ def check_condition_df(
117117
assert_no_leading_trailing_whitespace(
118118
df[column_name].values, column_name)
119119

120-
if sbml_model is not None:
120+
if model is not None:
121+
allowed_cols = set(model.get_valid_ids_for_condition_table())
121122
for column_name in df.columns:
122123
if column_name != CONDITION_NAME \
123-
and sbml_model.getParameter(column_name) is None \
124-
and sbml_model.getSpecies(column_name) is None \
125-
and sbml_model.getCompartment(column_name) is None:
124+
and column_name not in allowed_cols:
126125
raise AssertionError(
127126
"Condition table contains column for unknown entity '"
128-
f"{column_name}'. Column names must match parameter, "
129-
"species or compartment IDs specified in the SBML model.")
127+
f"{column_name}'.")
130128

131129

132130
def check_measurement_df(df: pd.DataFrame,
@@ -189,15 +187,15 @@ def check_measurement_df(df: pd.DataFrame,
189187

190188
def check_parameter_df(
191189
df: pd.DataFrame,
192-
sbml_model: Optional[libsbml.Model] = None,
190+
model: Optional[Model] = None,
193191
observable_df: Optional[pd.DataFrame] = None,
194192
measurement_df: Optional[pd.DataFrame] = None,
195193
condition_df: Optional[pd.DataFrame] = None) -> None:
196194
"""Run sanity checks on PEtab parameter table
197195
198196
Arguments:
199197
df: PEtab condition DataFrame
200-
sbml_model: SBML Model for additional checking of parameter IDs
198+
model: Model for additional checking of parameter IDs
201199
observable_df: PEtab observable table for additional checks
202200
measurement_df: PEtab measurement table for additional checks
203201
condition_df: PEtab condition table for additional checks
@@ -247,10 +245,10 @@ def check_parameter_df(
247245
check_parameter_bounds(df)
248246
assert_parameter_prior_type_is_valid(df)
249247

250-
if sbml_model and measurement_df is not None \
248+
if model and measurement_df is not None \
251249
and condition_df is not None:
252250
assert_all_parameters_present_in_parameter_df(
253-
df, sbml_model, observable_df, measurement_df, condition_df)
251+
df, model, observable_df, measurement_df, condition_df)
254252

255253

256254
def check_observable_df(observable_df: pd.DataFrame) -> None:
@@ -306,7 +304,7 @@ def check_observable_df(observable_df: pd.DataFrame) -> None:
306304

307305
def assert_all_parameters_present_in_parameter_df(
308306
parameter_df: pd.DataFrame,
309-
sbml_model: libsbml.Model,
307+
model: Model,
310308
observable_df: pd.DataFrame,
311309
measurement_df: pd.DataFrame,
312310
condition_df: pd.DataFrame) -> None:
@@ -315,7 +313,7 @@ def assert_all_parameters_present_in_parameter_df(
315313
316314
Arguments:
317315
parameter_df: PEtab parameter DataFrame
318-
sbml_model: PEtab SBML Model
316+
model: model
319317
observable_df: PEtab observable table
320318
measurement_df: PEtab measurement table
321319
condition_df: PEtab condition table
@@ -325,11 +323,11 @@ def assert_all_parameters_present_in_parameter_df(
325323
"""
326324

327325
required = parameters.get_required_parameters_for_parameter_table(
328-
sbml_model=sbml_model, condition_df=condition_df,
326+
model=model, condition_df=condition_df,
329327
observable_df=observable_df, measurement_df=measurement_df)
330328

331329
allowed = parameters.get_valid_parameters_for_parameter_table(
332-
sbml_model=sbml_model, condition_df=condition_df,
330+
model=model, condition_df=condition_df,
333331
observable_df=observable_df, measurement_df=measurement_df)
334332

335333
actual = set(parameter_df.index)
@@ -539,16 +537,18 @@ def assert_parameter_prior_parameters_are_valid(
539537
continue
540538
# parse parameters
541539
try:
542-
pars = tuple([float(val) for val in pars_str.split(';')])
540+
pars = tuple(
541+
float(val) for val in pars_str.split(PARAMETER_SEPARATOR)
542+
)
543543
except ValueError:
544544
raise AssertionError(
545545
f"Could not parse prior parameters '{pars_str}'.")
546546
# all distributions take 2 parameters
547547
if len(pars) != 2:
548548
raise AssertionError(
549549
f"The prior parameters '{pars}' do not contain the "
550-
"expected number of entries (currently 'par1;par2' "
551-
"for all prior types).")
550+
"expected number of entries (currently 'par1"
551+
f"{PARAMETER_SEPARATOR}par2' for all prior types).")
552552

553553

554554
def assert_parameter_estimate_is_boolean(parameter_df: pd.DataFrame) -> None:
@@ -764,13 +764,11 @@ def lint_problem(problem: 'petab.Problem') -> bool:
764764
errors_occurred = False
765765

766766
# Run checks on individual files
767-
if problem.sbml_model is not None:
768-
logger.info("Checking SBML model...")
769-
errors_occurred |= not sbml.is_sbml_consistent(
770-
problem.sbml_model.getSBMLDocument())
771-
sbml.log_sbml_errors(problem.sbml_model.getSBMLDocument())
767+
if problem.model is not None:
768+
logger.info("Checking model...")
769+
errors_occurred |= not problem.model.is_valid()
772770
else:
773-
logger.warning("SBML model not available. Skipping.")
771+
logger.warning("Model not available. Skipping.")
774772

775773
if problem.measurement_df is not None:
776774
logger.info("Checking measurement table...")
@@ -790,7 +788,7 @@ def lint_problem(problem: 'petab.Problem') -> bool:
790788
if problem.condition_df is not None:
791789
logger.info("Checking condition table...")
792790
try:
793-
check_condition_df(problem.condition_df, problem.sbml_model)
791+
check_condition_df(problem.condition_df, problem.model)
794792
except AssertionError as e:
795793
logger.error(e)
796794
errors_occurred = True
@@ -804,9 +802,9 @@ def lint_problem(problem: 'petab.Problem') -> bool:
804802
except AssertionError as e:
805803
logger.error(e)
806804
errors_occurred = True
807-
if problem.sbml_model is not None:
805+
if problem.model is not None:
808806
for obs_id in problem.observable_df.index:
809-
if problem.sbml_model.getElementBySId(obs_id):
807+
if problem.model.has_entity_with_id(obs_id):
810808
logger.error(f"Observable ID {obs_id} shadows model "
811809
"entity.")
812810
errors_occurred = True
@@ -816,7 +814,7 @@ def lint_problem(problem: 'petab.Problem') -> bool:
816814
if problem.parameter_df is not None:
817815
logger.info("Checking parameter table...")
818816
try:
819-
check_parameter_df(problem.parameter_df, problem.sbml_model,
817+
check_parameter_df(problem.parameter_df, problem.model,
820818
problem.observable_df,
821819
problem.measurement_df, problem.condition_df)
822820
except AssertionError as e:
@@ -825,11 +823,11 @@ def lint_problem(problem: 'petab.Problem') -> bool:
825823
else:
826824
logger.warning("Parameter table not available. Skipping.")
827825

828-
if problem.sbml_model is not None and problem.condition_df is not None \
826+
if problem.model is not None and problem.condition_df is not None \
829827
and problem.parameter_df is not None:
830828
try:
831829
assert_model_parameters_in_condition_or_parameter_table(
832-
problem.sbml_model,
830+
problem.model,
833831
problem.condition_df,
834832
problem.parameter_df
835833
)
@@ -840,7 +838,7 @@ def lint_problem(problem: 'petab.Problem') -> bool:
840838
if errors_occurred:
841839
logger.error('Not OK')
842840
elif problem.measurement_df is None or problem.condition_df is None \
843-
or problem.sbml_model is None or problem.parameter_df is None \
841+
or problem.model is None or problem.parameter_df is None \
844842
or problem.observable_df is None:
845843
logger.warning('Not all files of the PEtab problem definition could '
846844
'be checked.')
@@ -851,46 +849,54 @@ def lint_problem(problem: 'petab.Problem') -> bool:
851849

852850

853851
def assert_model_parameters_in_condition_or_parameter_table(
854-
sbml_model: libsbml.Model,
852+
model: Model,
855853
condition_df: pd.DataFrame,
856854
parameter_df: pd.DataFrame) -> None:
857-
"""Model parameters that are targets of AssignmentRule must not be present
858-
in parameter table or in condition table columns. Other parameters must
859-
only be present in either in parameter table or condition table columns.
860-
Check that.
855+
"""Model parameters that are rule targets must not be present in the
856+
parameter table. Other parameters must only be present in either in
857+
parameter table or condition table columns. Check that.
861858
862859
Arguments:
863860
parameter_df: PEtab parameter DataFrame
864-
sbml_model: PEtab SBML Model
861+
model: PEtab model
865862
condition_df: PEtab condition table
866863
867864
Raises:
868865
AssertionError: in case of problems
869866
"""
870-
871-
for parameter in sbml_model.getListOfParameters():
872-
parameter_id = parameter.getId()
873-
874-
if parameter_id.startswith('observableParameter'):
875-
continue
876-
if parameter_id.startswith('noiseParameter'):
877-
continue
878-
879-
is_assignee = \
880-
sbml_model.getAssignmentRuleByVariable(parameter_id) is not None
881-
in_parameter_df = parameter_id in parameter_df.index
882-
in_condition_df = parameter_id in condition_df.columns
883-
884-
if is_assignee and (in_parameter_df or in_condition_df):
885-
raise AssertionError(f"Model parameter '{parameter_id}' is target "
886-
"of AssignmentRule, and thus, must not be "
887-
"present in condition table or in parameter "
888-
"table.")
889-
890-
if in_parameter_df and in_condition_df:
891-
raise AssertionError(f"Model parameter '{parameter_id}' present "
892-
"in both condition table and parameter "
893-
"table.")
867+
allowed_in_condition_cols = set(model.get_valid_ids_for_condition_table())
868+
allowed_in_parameter_table = \
869+
set(model.get_valid_parameters_for_parameter_table())
870+
entities_in_condition_table = set(condition_df.columns) - {CONDITION_NAME}
871+
entities_in_parameter_table = set(parameter_df.index.values)
872+
873+
disallowed_in_condition = {
874+
x for x in (entities_in_condition_table - allowed_in_condition_cols)
875+
# we only check model entities here, not output parameters
876+
if model.has_entity_with_id(x)
877+
}
878+
if disallowed_in_condition:
879+
is_or_are = "is" if len(disallowed_in_condition) == 1 else "are"
880+
raise AssertionError(f"{disallowed_in_condition} {is_or_are} not "
881+
"allowed to occur in condition table "
882+
"columns.")
883+
884+
disallowed_in_parameters = {
885+
x for x in (entities_in_parameter_table - allowed_in_parameter_table)
886+
# we only check model entities here, not output parameters
887+
if model.has_entity_with_id(x)
888+
}
889+
890+
if disallowed_in_parameters:
891+
is_or_are = "is" if len(disallowed_in_parameters) == 1 else "are"
892+
raise AssertionError(f"{disallowed_in_parameters} {is_or_are} not "
893+
"allowed to occur in the parameters table.")
894+
895+
in_both = entities_in_condition_table & entities_in_parameter_table
896+
if in_both:
897+
is_or_are = "is" if len(in_both) == 1 else "are"
898+
raise AssertionError(f"{in_both} {is_or_are} present in both "
899+
"the condition table and the parameter table.")
894900

895901

896902
def assert_measurement_conditions_present_in_condition_table(

petab/measurements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ def get_unique_parameters(series):
154154

155155
def split_parameter_replacement_list(
156156
list_string: Union[str, numbers.Number],
157-
delim: str = ';') -> List[Union[str, numbers.Number]]:
157+
delim: str = PARAMETER_SEPARATOR) -> List[Union[str, numbers.Number]]:
158158
"""
159159
Split values in observableParameters and noiseParameters in measurement
160160
table.

petab/models/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
MODEL_TYPE_SBML = 'sbml'
2+
3+
known_model_types = {MODEL_TYPE_SBML}
4+
5+
from .model import Model # noqa F401

0 commit comments

Comments
 (0)