diff --git a/doc/example/example_visualization.ipynb b/doc/example/example_visualization.ipynb index 17118e31..7a55bf48 100644 --- a/doc/example/example_visualization.ipynb +++ b/doc/example/example_visualization.ipynb @@ -264,4 +264,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/doc/example/example_visualization_without_visspec.ipynb b/doc/example/example_visualization_without_visspec.ipynb index abd53cc7..6200b928 100644 --- a/doc/example/example_visualization_without_visspec.ipynb +++ b/doc/example/example_visualization_without_visspec.ipynb @@ -168,4 +168,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/petab/C.py b/petab/C.py index ea56cf33..8e63f00f 100644 --- a/petab/C.py +++ b/petab/C.py @@ -274,3 +274,6 @@ RESIDUAL = 'residual' #: NOISE_VALUE = 'noiseValue' + +# separator for multiple parameter values (bounds, observableParameters, ...) +PARAMETER_SEPARATOR = ';' diff --git a/petab/core.py b/petab/core.py index bfab25d5..30256ca6 100644 --- a/petab/core.py +++ b/petab/core.py @@ -120,8 +120,8 @@ def flatten_timepoint_specific_output_overrides( replacement_id = '' for field in possible_groupvars: if field in groupvars: - val = str(groupvar[groupvars.index(field) - ]).replace(';', '_').replace('.', '_') + val = str(groupvar[groupvars.index(field)])\ + .replace(PARAMETER_SEPARATOR, '_').replace('.', '_') if replacement_id == '': replacement_id = val elif val != '': diff --git a/petab/lint.py b/petab/lint.py index e13f540b..e4e9c683 100644 --- a/petab/lint.py +++ b/petab/lint.py @@ -7,13 +7,13 @@ from typing import Optional, Iterable, Any from collections import Counter -import libsbml import numpy as np import pandas as pd import sympy as sp import petab -from . import (core, parameters, sbml, measurements) +from . import (core, parameters, measurements) +from .models import Model from .C import * # noqa: F403 logger = logging.getLogger(__name__) @@ -85,12 +85,12 @@ def assert_no_leading_trailing_whitespace( def check_condition_df( - df: pd.DataFrame, sbml_model: Optional[libsbml.Model] = None) -> None: + df: pd.DataFrame, model: Optional[Model] = None) -> None: """Run sanity checks on PEtab condition table Arguments: df: PEtab condition DataFrame - sbml_model: SBML Model for additional checking of parameter IDs + model: Model for additional checking of parameter IDs Raises: AssertionError: in case of problems @@ -117,16 +117,14 @@ def check_condition_df( assert_no_leading_trailing_whitespace( df[column_name].values, column_name) - if sbml_model is not None: + if model is not None: + allowed_cols = set(model.get_valid_ids_for_condition_table()) for column_name in df.columns: if column_name != CONDITION_NAME \ - and sbml_model.getParameter(column_name) is None \ - and sbml_model.getSpecies(column_name) is None \ - and sbml_model.getCompartment(column_name) is None: + and column_name not in allowed_cols: raise AssertionError( "Condition table contains column for unknown entity '" - f"{column_name}'. Column names must match parameter, " - "species or compartment IDs specified in the SBML model.") + f"{column_name}'.") def check_measurement_df(df: pd.DataFrame, @@ -189,7 +187,7 @@ def check_measurement_df(df: pd.DataFrame, def check_parameter_df( df: pd.DataFrame, - sbml_model: Optional[libsbml.Model] = None, + model: Optional[Model] = None, observable_df: Optional[pd.DataFrame] = None, measurement_df: Optional[pd.DataFrame] = None, condition_df: Optional[pd.DataFrame] = None) -> None: @@ -197,7 +195,7 @@ def check_parameter_df( Arguments: df: PEtab condition DataFrame - sbml_model: SBML Model for additional checking of parameter IDs + model: Model for additional checking of parameter IDs observable_df: PEtab observable table for additional checks measurement_df: PEtab measurement table for additional checks condition_df: PEtab condition table for additional checks @@ -247,10 +245,10 @@ def check_parameter_df( check_parameter_bounds(df) assert_parameter_prior_type_is_valid(df) - if sbml_model and measurement_df is not None \ + if model and measurement_df is not None \ and condition_df is not None: assert_all_parameters_present_in_parameter_df( - df, sbml_model, observable_df, measurement_df, condition_df) + df, model, observable_df, measurement_df, condition_df) def check_observable_df(observable_df: pd.DataFrame) -> None: @@ -306,7 +304,7 @@ def check_observable_df(observable_df: pd.DataFrame) -> None: def assert_all_parameters_present_in_parameter_df( parameter_df: pd.DataFrame, - sbml_model: libsbml.Model, + model: Model, observable_df: pd.DataFrame, measurement_df: pd.DataFrame, condition_df: pd.DataFrame) -> None: @@ -315,7 +313,7 @@ def assert_all_parameters_present_in_parameter_df( Arguments: parameter_df: PEtab parameter DataFrame - sbml_model: PEtab SBML Model + model: model observable_df: PEtab observable table measurement_df: PEtab measurement table condition_df: PEtab condition table @@ -325,11 +323,11 @@ def assert_all_parameters_present_in_parameter_df( """ required = parameters.get_required_parameters_for_parameter_table( - sbml_model=sbml_model, condition_df=condition_df, + model=model, condition_df=condition_df, observable_df=observable_df, measurement_df=measurement_df) allowed = parameters.get_valid_parameters_for_parameter_table( - sbml_model=sbml_model, condition_df=condition_df, + model=model, condition_df=condition_df, observable_df=observable_df, measurement_df=measurement_df) actual = set(parameter_df.index) @@ -539,7 +537,9 @@ def assert_parameter_prior_parameters_are_valid( continue # parse parameters try: - pars = tuple([float(val) for val in pars_str.split(';')]) + pars = tuple( + float(val) for val in pars_str.split(PARAMETER_SEPARATOR) + ) except ValueError: raise AssertionError( f"Could not parse prior parameters '{pars_str}'.") @@ -547,8 +547,8 @@ def assert_parameter_prior_parameters_are_valid( if len(pars) != 2: raise AssertionError( f"The prior parameters '{pars}' do not contain the " - "expected number of entries (currently 'par1;par2' " - "for all prior types).") + "expected number of entries (currently 'par1" + f"{PARAMETER_SEPARATOR}par2' for all prior types).") def assert_parameter_estimate_is_boolean(parameter_df: pd.DataFrame) -> None: @@ -764,13 +764,11 @@ def lint_problem(problem: 'petab.Problem') -> bool: errors_occurred = False # Run checks on individual files - if problem.sbml_model is not None: - logger.info("Checking SBML model...") - errors_occurred |= not sbml.is_sbml_consistent( - problem.sbml_model.getSBMLDocument()) - sbml.log_sbml_errors(problem.sbml_model.getSBMLDocument()) + if problem.model is not None: + logger.info("Checking model...") + errors_occurred |= not problem.model.is_valid() else: - logger.warning("SBML model not available. Skipping.") + logger.warning("Model not available. Skipping.") if problem.measurement_df is not None: logger.info("Checking measurement table...") @@ -790,7 +788,7 @@ def lint_problem(problem: 'petab.Problem') -> bool: if problem.condition_df is not None: logger.info("Checking condition table...") try: - check_condition_df(problem.condition_df, problem.sbml_model) + check_condition_df(problem.condition_df, problem.model) except AssertionError as e: logger.error(e) errors_occurred = True @@ -804,9 +802,9 @@ def lint_problem(problem: 'petab.Problem') -> bool: except AssertionError as e: logger.error(e) errors_occurred = True - if problem.sbml_model is not None: + if problem.model is not None: for obs_id in problem.observable_df.index: - if problem.sbml_model.getElementBySId(obs_id): + if problem.model.has_entity_with_id(obs_id): logger.error(f"Observable ID {obs_id} shadows model " "entity.") errors_occurred = True @@ -816,7 +814,7 @@ def lint_problem(problem: 'petab.Problem') -> bool: if problem.parameter_df is not None: logger.info("Checking parameter table...") try: - check_parameter_df(problem.parameter_df, problem.sbml_model, + check_parameter_df(problem.parameter_df, problem.model, problem.observable_df, problem.measurement_df, problem.condition_df) except AssertionError as e: @@ -825,11 +823,11 @@ def lint_problem(problem: 'petab.Problem') -> bool: else: logger.warning("Parameter table not available. Skipping.") - if problem.sbml_model is not None and problem.condition_df is not None \ + if problem.model is not None and problem.condition_df is not None \ and problem.parameter_df is not None: try: assert_model_parameters_in_condition_or_parameter_table( - problem.sbml_model, + problem.model, problem.condition_df, problem.parameter_df ) @@ -840,7 +838,7 @@ def lint_problem(problem: 'petab.Problem') -> bool: if errors_occurred: logger.error('Not OK') elif problem.measurement_df is None or problem.condition_df is None \ - or problem.sbml_model is None or problem.parameter_df is None \ + or problem.model is None or problem.parameter_df is None \ or problem.observable_df is None: logger.warning('Not all files of the PEtab problem definition could ' 'be checked.') @@ -851,46 +849,54 @@ def lint_problem(problem: 'petab.Problem') -> bool: def assert_model_parameters_in_condition_or_parameter_table( - sbml_model: libsbml.Model, + model: Model, condition_df: pd.DataFrame, parameter_df: pd.DataFrame) -> None: - """Model parameters that are targets of AssignmentRule must not be present - in parameter table or in condition table columns. Other parameters must - only be present in either in parameter table or condition table columns. - Check that. + """Model parameters that are rule targets must not be present in the + parameter table. Other parameters must only be present in either in + parameter table or condition table columns. Check that. Arguments: parameter_df: PEtab parameter DataFrame - sbml_model: PEtab SBML Model + model: PEtab model condition_df: PEtab condition table Raises: AssertionError: in case of problems """ - - for parameter in sbml_model.getListOfParameters(): - parameter_id = parameter.getId() - - if parameter_id.startswith('observableParameter'): - continue - if parameter_id.startswith('noiseParameter'): - continue - - is_assignee = \ - sbml_model.getAssignmentRuleByVariable(parameter_id) is not None - in_parameter_df = parameter_id in parameter_df.index - in_condition_df = parameter_id in condition_df.columns - - if is_assignee and (in_parameter_df or in_condition_df): - raise AssertionError(f"Model parameter '{parameter_id}' is target " - "of AssignmentRule, and thus, must not be " - "present in condition table or in parameter " - "table.") - - if in_parameter_df and in_condition_df: - raise AssertionError(f"Model parameter '{parameter_id}' present " - "in both condition table and parameter " - "table.") + allowed_in_condition_cols = set(model.get_valid_ids_for_condition_table()) + allowed_in_parameter_table = \ + set(model.get_valid_parameters_for_parameter_table()) + entities_in_condition_table = set(condition_df.columns) - {CONDITION_NAME} + entities_in_parameter_table = set(parameter_df.index.values) + + disallowed_in_condition = { + x for x in (entities_in_condition_table - allowed_in_condition_cols) + # we only check model entities here, not output parameters + if model.has_entity_with_id(x) + } + if disallowed_in_condition: + is_or_are = "is" if len(disallowed_in_condition) == 1 else "are" + raise AssertionError(f"{disallowed_in_condition} {is_or_are} not " + "allowed to occur in condition table " + "columns.") + + disallowed_in_parameters = { + x for x in (entities_in_parameter_table - allowed_in_parameter_table) + # we only check model entities here, not output parameters + if model.has_entity_with_id(x) + } + + if disallowed_in_parameters: + is_or_are = "is" if len(disallowed_in_parameters) == 1 else "are" + raise AssertionError(f"{disallowed_in_parameters} {is_or_are} not " + "allowed to occur in the parameters table.") + + in_both = entities_in_condition_table & entities_in_parameter_table + if in_both: + is_or_are = "is" if len(in_both) == 1 else "are" + raise AssertionError(f"{in_both} {is_or_are} present in both " + "the condition table and the parameter table.") def assert_measurement_conditions_present_in_condition_table( diff --git a/petab/measurements.py b/petab/measurements.py index d7e1c954..72cc2ce0 100644 --- a/petab/measurements.py +++ b/petab/measurements.py @@ -154,7 +154,7 @@ def get_unique_parameters(series): def split_parameter_replacement_list( list_string: Union[str, numbers.Number], - delim: str = ';') -> List[Union[str, numbers.Number]]: + delim: str = PARAMETER_SEPARATOR) -> List[Union[str, numbers.Number]]: """ Split values in observableParameters and noiseParameters in measurement table. diff --git a/petab/models/__init__.py b/petab/models/__init__.py new file mode 100644 index 00000000..b4bc9601 --- /dev/null +++ b/petab/models/__init__.py @@ -0,0 +1,5 @@ +MODEL_TYPE_SBML = 'sbml' + +known_model_types = {MODEL_TYPE_SBML} + +from .model import Model # noqa F401 diff --git a/petab/models/model.py b/petab/models/model.py new file mode 100644 index 00000000..ae2ad3b8 --- /dev/null +++ b/petab/models/model.py @@ -0,0 +1,130 @@ +"""PEtab model abstraction""" +from __future__ import annotations + +import abc +from pathlib import Path +from typing import Any, Iterable, Tuple + +from . import MODEL_TYPE_SBML, known_model_types + + +class Model(abc.ABC): + """Base class for wrappers for any PEtab-supported model type""" + + def __init__(self): + ... + + @staticmethod + @abc.abstractmethod + def from_file(filepath_or_buffer: Any) -> Model: + """Load the model from the given path/URL + + :param filepath_or_buffer: URL or path of the model + :returns: A ``Model`` instance holding the given model + """ + ... + + @abc.abstractmethod + def to_file(self, filename: [str, Path]): + """Save the model to the given file + + :param filename: Destination filename + """ + ... + + @classmethod + @property + @abc.abstractmethod + def type_id(cls): + ... + + @abc.abstractmethod + def get_parameter_value(self, id_: str) -> float: + """Get a parameter value + + :param id_: ID of the parameter whose value is to be returned + :raises ValueError: If no parameter with the given ID exists + :returns: The value of the given parameter as specified in the model + """ + ... + + @abc.abstractmethod + def get_free_parameter_ids_with_values( + self + ) -> Iterable[Tuple[str, float]]: + """Get free model parameters along with their values + + Returns: + Iterator over tuples of (parameter_id, parameter_value) + """ + ... + + @abc.abstractmethod + def has_entity_with_id(self, entity_id) -> bool: + """Check if there is a model entity with the given ID + + :param entity_id: ID to check for + :returns: ``True``, if there is an entity with the given ID, + ``False`` otherwise + """ + ... + + @abc.abstractmethod + def get_valid_parameters_for_parameter_table(self) -> Iterable[str]: + """Get IDs of all parameters that are allowed to occur in the PEtab + parameters table + + :returns: Iterator over parameter IDs + """ + ... + + @abc.abstractmethod + def get_valid_ids_for_condition_table(self) -> Iterable[str]: + """Get IDs of all model entities that are allowed to occur as columns + in the PEtab conditions table. + + :returns: Iterator over model entity IDs + """ + ... + + @abc.abstractmethod + def symbol_allowed_in_observable_formula(self, id_: str) -> bool: + """Check if the given ID is allowed to be used in observable and noise + formulas + + :returns: ``True``, if allowed, ``False`` otherwise + """ + + ... + + @abc.abstractmethod + def is_valid(self) -> bool: + """Validate this model + + :returns: `True` if the model is valid, `False` if there are errors in + this model + """ + ... + + @abc.abstractmethod + def is_state_variable(self, id_: str) -> bool: + """Check whether the given ID corresponds to a model state variable""" + ... + + +def model_factory(filepath_or_buffer: Any, model_language: str) -> Model: + """Create a PEtab model instance from the given model + + :param filepath_or_buffer: Path/URL of the model + :param model_language: PEtab model language ID for the given model + :returns: A :py:class:`Model` instance representing the given model + """ + if model_language == MODEL_TYPE_SBML: + from .sbml_model import SbmlModel + return SbmlModel.from_file(filepath_or_buffer) + + if model_language in known_model_types: + raise NotImplementedError( + f"Unsupported model format: {model_language}") + + raise ValueError(f"Unknown model format: {model_language}") diff --git a/petab/models/sbml_model.py b/petab/models/sbml_model.py new file mode 100644 index 00000000..3dcabe2f --- /dev/null +++ b/petab/models/sbml_model.py @@ -0,0 +1,119 @@ +"""Functions for handling SBML models""" + +import itertools +from pathlib import Path +from typing import Iterable, Optional, Tuple + +import libsbml + +from . import MODEL_TYPE_SBML +from .model import Model +from ..sbml import (get_sbml_model, is_sbml_consistent, load_sbml_from_string, + log_sbml_errors, write_sbml) + + +class SbmlModel(Model): + """PEtab wrapper for SBML models""" + + type_id = MODEL_TYPE_SBML + + def __init__( + self, + sbml_model: libsbml.Model = None, + sbml_reader: libsbml.SBMLReader = None, + sbml_document: libsbml.SBMLDocument = None, + ): + super().__init__() + + self.sbml_reader: Optional[libsbml.SBMLReader] = sbml_reader + self.sbml_document: Optional[libsbml.SBMLDocument] = sbml_document + self.sbml_model: Optional[libsbml.Model] = sbml_model + + def __getstate__(self): + """Return state for pickling""" + state = self.__dict__.copy() + + # libsbml stuff cannot be serialized directly + if self.sbml_model: + sbml_document = self.sbml_model.getSBMLDocument() + sbml_writer = libsbml.SBMLWriter() + state['sbml_string'] = sbml_writer.writeSBMLToString(sbml_document) + + exclude = ['sbml_reader', 'sbml_document', 'sbml_model'] + for key in exclude: + state.pop(key) + + return state + + def __setstate__(self, state): + """Set state after unpickling""" + # load SBML model from pickled string + sbml_string = state.pop('sbml_string', None) + if sbml_string: + self.sbml_reader, self.sbml_document, self.sbml_model = \ + load_sbml_from_string(sbml_string) + + self.__dict__.update(state) + + @staticmethod + def from_file(filepath_or_buffer): + sbml_reader, sbml_document, sbml_model = get_sbml_model( + filepath_or_buffer) + return SbmlModel( + sbml_model=sbml_model, + sbml_reader=sbml_reader, + sbml_document=sbml_document, + ) + + def to_file(self, filename: [str, Path]): + write_sbml(self.sbml_document or self.sbml_model.getSBMLDocument(), + filename) + + def get_parameter_value(self, id_: str) -> float: + parameter = self.sbml_model.getParameter(id_) + if not parameter: + raise ValueError(f"Parameter {id_} does not exist.") + return parameter.getValue() + + def get_free_parameter_ids_with_values( + self + ) -> Iterable[Tuple[str, float]]: + return ( + (p.getId(), p.getValue()) + for p in self.sbml_model.getListOfParameters() + if self.sbml_model.getAssignmentRuleByVariable(p.getId()) is None + ) + + def has_entity_with_id(self, entity_id) -> bool: + return self.sbml_model.getElementBySId(entity_id) is not None + + def get_valid_parameters_for_parameter_table(self) -> Iterable[str]: + # All parameters except rule-targets + disallowed_set = { + ar.getVariable() for ar in self.sbml_model.getListOfRules() + } + + return (p.getId() for p in self.sbml_model.getListOfParameters() + if p.getId() not in disallowed_set) + + def get_valid_ids_for_condition_table(self) -> Iterable[str]: + return ( + x.getId() for x in itertools.chain( + self.sbml_model.getListOfParameters(), + self.sbml_model.getListOfSpecies(), + self.sbml_model.getListOfCompartments() + ) + ) + + def symbol_allowed_in_observable_formula(self, id_: str) -> bool: + return self.sbml_model.getElementBySId(id_) or id_ == 'time' + + def is_valid(self) -> bool: + valid = is_sbml_consistent(self.sbml_model.getSBMLDocument()) + log_sbml_errors(self.sbml_model.getSBMLDocument()) + return valid + + def is_state_variable(self, id_: str) -> bool: + return (self.sbml_model.getSpecies(id_) is not None + or self.sbml_model.getCompartment(id_) is not None + or self.sbml_model.getRuleByVariable(id_) is not None) diff --git a/petab/observables.py b/petab/observables.py index 67fa1e4d..05fa142f 100644 --- a/petab/observables.py +++ b/petab/observables.py @@ -5,24 +5,26 @@ from pathlib import Path from typing import List, Union -import libsbml import pandas as pd import sympy as sp from . import core, lint from .C import * # noqa: F403 +from .models import Model -__all__ = ['create_observable_df', - 'get_formula_placeholders', - 'get_observable_df', - 'get_output_parameters', - 'get_placeholders', - 'write_observable_df'] +__all__ = [ + 'create_observable_df', + 'get_formula_placeholders', + 'get_observable_df', + 'get_output_parameters', + 'get_placeholders', + 'write_observable_df' +] def get_observable_df( observable_file: Union[str, pd.DataFrame, Path, None] -) -> pd.DataFrame: +) -> Union[pd.DataFrame, None]: """ Read the provided observable file into a ``pandas.Dataframe``. @@ -67,18 +69,18 @@ def write_observable_df(df: pd.DataFrame, filename: Union[str, Path]) -> None: def get_output_parameters( observable_df: pd.DataFrame, - sbml_model: libsbml.Model, + model: Model, observables: bool = True, noise: bool = True, ) -> List[str]: """Get output parameters Returns IDs of parameters used in observable and noise formulas that are - not defined in the SBML model. + not defined in the model. Arguments: observable_df: PEtab observable table - sbml_model: SBML model + model: The underlying model observables: Include parameters from observableFormulas noise: Include parameters from noiseFormulas @@ -97,7 +99,7 @@ def get_output_parameters( key=lambda symbol: symbol.name) for free_sym in free_syms: sym = str(free_sym) - if sbml_model.getElementBySId(sym) is None and sym != 'time': + if not model.symbol_allowed_in_observable_formula(sym): output_parameters[sym] = None return list(output_parameters.keys()) diff --git a/petab/parameter_mapping.py b/petab/parameter_mapping.py index 3679e506..6a309011 100644 --- a/petab/parameter_mapping.py +++ b/petab/parameter_mapping.py @@ -5,16 +5,17 @@ import numbers import os import re -from typing import Tuple, Dict, Union, Any, List, Optional, Iterable +import warnings +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union import libsbml import numpy as np import pandas as pd -from . import lint, measurements, sbml, core, observables, parameters -from . import ENV_NUM_THREADS +from . import ENV_NUM_THREADS, core, lint, measurements, observables, \ + parameters from .C import * # noqa: F403 - +from .models import Model logger = logging.getLogger(__name__) __all__ = ['get_optimization_to_simulation_parameter_mapping', @@ -51,10 +52,11 @@ def get_optimization_to_simulation_parameter_mapping( warn_unmapped: Optional[bool] = True, scaled_parameters: bool = False, fill_fixed_parameters: bool = True, - allow_timepoint_specific_numeric_noise_parameters: bool = False + allow_timepoint_specific_numeric_noise_parameters: bool = False, + model: Model = None, ) -> List[ParMappingDictQuadruple]: """ - Create list of mapping dicts from PEtab-problem to SBML parameters. + Create list of mapping dicts from PEtab-problem to model parameters. Mapping can be performed in parallel. The number of threads is controlled by the environment variable with the name of @@ -64,8 +66,9 @@ def get_optimization_to_simulation_parameter_mapping( condition_df, measurement_df, parameter_df, observable_df: The dataframes in the PEtab format. sbml_model: - The sbml model with observables and noise specified according to - the PEtab format. + The SBML model (deprecated) + model: + The model. simulation_conditions: Table of simulation conditions as created by ``petab.get_simulation_conditions``. @@ -100,6 +103,16 @@ def get_optimization_to_simulation_parameter_mapping( If no preequilibration condition is defined, the respective dicts will be empty. ``NaN`` is used where no mapping exists. """ + if sbml_model: + warnings.warn("Passing a model via the `sbml_model` argument is " + "deprecated, use `model=petab.models.sbml_model." + "SbmlModel(...)` instead.", DeprecationWarning, + stacklevel=2) + from petab.models.sbml_model import SbmlModel + if model: + raise ValueError("Arguments `model` and `sbml_model` are " + "mutually exclusive.") + model = SbmlModel(sbml_model=sbml_model) # Ensure inputs are okay _perform_mapping_checks( @@ -111,12 +124,11 @@ def get_optimization_to_simulation_parameter_mapping( simulation_conditions = measurements.get_simulation_conditions( measurement_df) - simulation_parameters = sbml.get_model_parameters(sbml_model, - with_values=True) - # Add output parameters that are not already defined in the SBML model + simulation_parameters = dict(model.get_free_parameter_ids_with_values()) + # Add output parameters that are not already defined in the model if observable_df is not None: output_parameters = observables.get_output_parameters( - observable_df=observable_df, sbml_model=sbml_model) + observable_df=observable_df, model=model) for par_id in output_parameters: simulation_parameters[par_id] = np.nan @@ -129,7 +141,7 @@ def get_optimization_to_simulation_parameter_mapping( _map_condition, _map_condition_arg_packer( simulation_conditions, measurement_df, condition_df, - parameter_df, sbml_model, simulation_parameters, warn_unmapped, + parameter_df, model, simulation_parameters, warn_unmapped, scaled_parameters, fill_fixed_parameters, allow_timepoint_specific_numeric_noise_parameters)) return list(mapping) @@ -141,7 +153,7 @@ def get_optimization_to_simulation_parameter_mapping( _map_condition, _map_condition_arg_packer( simulation_conditions, measurement_df, condition_df, - parameter_df, sbml_model, simulation_parameters, warn_unmapped, + parameter_df, model, simulation_parameters, warn_unmapped, scaled_parameters, fill_fixed_parameters, allow_timepoint_specific_numeric_noise_parameters)) return list(mapping) @@ -152,7 +164,7 @@ def _map_condition_arg_packer( measurement_df, condition_df, parameter_df, - sbml_model, + model, simulation_parameters, warn_unmapped, scaled_parameters, @@ -162,7 +174,7 @@ def _map_condition_arg_packer( """Helper function to pack extra arguments for _map_condition""" for _, condition in simulation_conditions.iterrows(): yield(condition, measurement_df, condition_df, parameter_df, - sbml_model, simulation_parameters, warn_unmapped, + model, simulation_parameters, warn_unmapped, scaled_parameters, fill_fixed_parameters, allow_timepoint_specific_numeric_noise_parameters) @@ -174,7 +186,7 @@ def _map_condition(packed_args): :py:func:`get_optimization_to_simulation_parameter_mapping`. """ - (condition, measurement_df, condition_df, parameter_df, sbml_model, + (condition, measurement_df, condition_df, parameter_df, model, simulation_parameters, warn_unmapped, scaled_parameters, fill_fixed_parameters, allow_timepoint_specific_numeric_noise_parameters) = packed_args @@ -199,7 +211,7 @@ def _map_condition(packed_args): condition_id=condition[PREEQUILIBRATION_CONDITION_ID], is_preeq=True, cur_measurement_df=cur_measurement_df, - sbml_model=sbml_model, + model=model, condition_df=condition_df, parameter_df=parameter_df, simulation_parameters=simulation_parameters, @@ -214,7 +226,7 @@ def _map_condition(packed_args): condition_id=condition[SIMULATION_CONDITION_ID], is_preeq=False, cur_measurement_df=cur_measurement_df, - sbml_model=sbml_model, + model=model, condition_df=condition_df, parameter_df=parameter_df, simulation_parameters=simulation_parameters, @@ -232,14 +244,15 @@ def get_parameter_mapping_for_condition( condition_id: str, is_preeq: bool, cur_measurement_df: Optional[pd.DataFrame], - sbml_model: libsbml.Model, - condition_df: pd.DataFrame, + sbml_model: libsbml.Model = None, + condition_df: pd.DataFrame = None, parameter_df: pd.DataFrame = None, simulation_parameters: Optional[Dict[str, str]] = None, warn_unmapped: bool = True, scaled_parameters: bool = False, fill_fixed_parameters: bool = True, allow_timepoint_specific_numeric_noise_parameters: bool = False, + model: Model = None, ) -> Tuple[ParMappingDict, ScaleMappingDict]: """ Create dictionary of parameter value and parameter scale mappings from @@ -258,8 +271,9 @@ def get_parameter_mapping_for_condition( parameter_df: PEtab parameter DataFrame sbml_model: - The sbml model with observables and noise specified according to - the PEtab format used to retrieve simulation parameter IDs. + The SBML model (deprecated) + model: + The model. simulation_parameters: Model simulation parameter IDs mapped to parameter values (output of ``petab.sbml.get_model_parameters(.., with_values=True)``). @@ -286,6 +300,17 @@ def get_parameter_mapping_for_condition( Second dictionary mapping model parameter IDs to their scale. ``NaN`` is used where no mapping exists. """ + if sbml_model: + warnings.warn("Passing a model via the `sbml_model` argument is " + "deprecated, use `model=petab.models.sbml_model." + "SbmlModel(...)` instead.", DeprecationWarning, + stacklevel=2) + from petab.models.sbml_model import SbmlModel + if model: + raise ValueError("Arguments `model` and `sbml_model` are " + "mutually exclusive.") + model = SbmlModel(sbml_model=sbml_model) + if cur_measurement_df is not None: _perform_mapping_checks( cur_measurement_df, @@ -293,11 +318,11 @@ def get_parameter_mapping_for_condition( allow_timepoint_specific_numeric_noise_parameters) if simulation_parameters is None: - simulation_parameters = sbml.get_model_parameters(sbml_model, - with_values=True) + simulation_parameters = dict( + model.get_free_parameter_ids_with_values()) # NOTE: order matters here - the former is overwritten by the latter: - # SBML model < condition table < measurement < table parameter table + # model < condition table < measurement < table parameter table # initialize mapping dicts # for the case of matching simulation and optimization parameter vector @@ -314,7 +339,7 @@ def get_parameter_mapping_for_condition( handle_missing_overrides(par_mapping, warn=warn_unmapped) _apply_condition_parameters(par_mapping, scale_mapping, condition_id, - condition_df, sbml_model) + condition_df, model) _apply_parameter_table(par_mapping, scale_mapping, parameter_df, scaled_parameters, fill_fixed_parameters) @@ -386,7 +411,7 @@ def _apply_condition_parameters(par_mapping: ParMappingDict, scale_mapping: ScaleMappingDict, condition_id: str, condition_df: pd.DataFrame, - sbml_model: libsbml.Model) -> None: + model: Model) -> None: """Replace parameter IDs in parameter mapping dictionary by condition table parameter values (in-place). @@ -400,11 +425,7 @@ def _apply_condition_parameters(par_mapping: ParMappingDict, continue # Species, compartments, and rule targets are handled elsewhere - if sbml_model.getSpecies(overridee_id) is not None: - continue - if sbml_model.getCompartment(overridee_id) is not None: - continue - if sbml_model.getRuleByVariable(overridee_id) is not None: + if model.is_state_variable(overridee_id): continue par_mapping[overridee_id] = core.to_float_if_float( @@ -414,14 +435,14 @@ def _apply_condition_parameters(par_mapping: ParMappingDict, and np.isnan(par_mapping[overridee_id]): # NaN in the condition table for an entity without time derivative # indicates that the model value should be used - parameter = sbml_model.getParameter(overridee_id) - if parameter: - par_mapping[overridee_id] = parameter.getValue() - else: + try: + par_mapping[overridee_id] = \ + model.get_parameter_value(overridee_id) + except ValueError as e: raise NotImplementedError( "Not sure how to handle NaN in condition table for " f"{overridee_id}." - ) + ) from e scale_mapping[overridee_id] = LIN diff --git a/petab/parameters.py b/petab/parameters.py index 8dbf441f..45f6febf 100644 --- a/petab/parameters.py +++ b/petab/parameters.py @@ -1,9 +1,10 @@ """Functions operating on the PEtab parameter table""" import numbers +import warnings from collections import OrderedDict from pathlib import Path -from typing import Dict, Iterable, List, Set, Tuple, Union +from typing import Dict, Iterable, List, Set, Tuple, Union, Optional import libsbml import numpy as np @@ -11,14 +12,13 @@ from . import conditions, core, lint, measurements, observables from .C import * # noqa: F403 +from .models import Model __all__ = ['create_parameter_df', 'get_optimization_parameter_scaling', 'get_optimization_parameters', 'get_parameter_df', 'get_priors_from_df', - 'get_required_parameters_for_parameter_table', - 'get_valid_parameters_for_parameter_table', 'map_scale', 'map_unscale', 'normalize_parameter_df', @@ -130,14 +130,17 @@ def get_optimization_parameter_scaling( return dict(zip(estimated_df.index, estimated_df[PARAMETER_SCALE])) -def create_parameter_df(sbml_model: libsbml.Model, - condition_df: pd.DataFrame, - observable_df: pd.DataFrame, - measurement_df: pd.DataFrame, - include_optional: bool = False, - parameter_scale: str = LOG10, - lower_bound: Iterable = None, - upper_bound: Iterable = None) -> pd.DataFrame: +def create_parameter_df( + sbml_model: Optional[libsbml.Model] = None, + condition_df: Optional[pd.DataFrame] = None, + observable_df: Optional[pd.DataFrame] = None, + measurement_df: Optional[pd.DataFrame] = None, + model: Optional[Model] = None, + include_optional: bool = False, + parameter_scale: str = LOG10, + lower_bound: Iterable = None, + upper_bound: Iterable = None +) -> pd.DataFrame: """Create a new PEtab parameter table All table entries can be provided as string or list-like with length @@ -145,6 +148,7 @@ def create_parameter_df(sbml_model: libsbml.Model, Arguments: sbml_model: SBML Model + model: PEtab model condition_df: PEtab condition DataFrame observable_df: PEtab observable DataFrame measurement_df: PEtab measurement DataFrame @@ -152,7 +156,7 @@ def create_parameter_df(sbml_model: libsbml.Model, required to be present in the parameter table. If set to True, this returns all parameters that are allowed to be present in the parameter table (i.e. also including parameters specified in the - SBML model). + model). parameter_scale: parameter scaling lower_bound: lower bound for parameter value upper_bound: upper bound for parameter value @@ -160,14 +164,23 @@ def create_parameter_df(sbml_model: libsbml.Model, Returns: The created parameter DataFrame """ - + if sbml_model: + warnings.warn("Passing a model via the `sbml_model` argument is " + "deprecated, use `model=petab.models.sbml_model." + "SbmlModel(...)` instead.", DeprecationWarning, + stacklevel=2) + from petab.models.sbml_model import SbmlModel + if model: + raise ValueError("Arguments `model` and `sbml_model` are " + "mutually exclusive.") + model = SbmlModel(sbml_model=sbml_model) if include_optional: parameter_ids = list(get_valid_parameters_for_parameter_table( - sbml_model=sbml_model, condition_df=condition_df, + model=model, condition_df=condition_df, observable_df=observable_df, measurement_df=measurement_df)) else: parameter_ids = list(get_required_parameters_for_parameter_table( - sbml_model=sbml_model, condition_df=condition_df, + model=model, condition_df=condition_df, observable_df=observable_df, measurement_df=measurement_df)) df = pd.DataFrame( @@ -186,12 +199,11 @@ def create_parameter_df(sbml_model: libsbml.Model, }) df.set_index([PARAMETER_ID], inplace=True) - # For SBML model parameters, set nominal values as defined in the model + # For model parameters, set nominal values as defined in the model for parameter_id in df.index: try: - parameter = sbml_model.getParameter(parameter_id) - if parameter: - df.loc[parameter_id, NOMINAL_VALUE] = parameter.getValue() + df.loc[parameter_id, NOMINAL_VALUE] = \ + model.get_parameter_value(parameter_id) except ValueError: # parameter was introduced as condition-specific override and # is potentially not present in the model @@ -200,7 +212,7 @@ def create_parameter_df(sbml_model: libsbml.Model, def get_required_parameters_for_parameter_table( - sbml_model: libsbml.Model, + model: Model, condition_df: pd.DataFrame, observable_df: pd.DataFrame, measurement_df: pd.DataFrame) -> Set[str]: @@ -208,7 +220,7 @@ def get_required_parameters_for_parameter_table( Get set of parameters which need to go into the parameter table Arguments: - sbml_model: PEtab SBML model + model: PEtab model condition_df: PEtab condition table observable_df: PEtab observable table measurement_df: PEtab measurement table @@ -217,7 +229,7 @@ def get_required_parameters_for_parameter_table( Set of parameter IDs which PEtab requires to be present in the parameter table. That is all {observable,noise}Parameters from the measurement table as well as all parametric condition table overrides - that are not defined in the SBML model. + that are not defined in the model. """ # use ordered dict as proxy for ordered set @@ -240,24 +252,24 @@ def append_overrides(overrides): for kwargs in [dict(observables=True, noise=False), dict(observables=False, noise=True)]: output_parameters = observables.get_output_parameters( - observable_df, sbml_model, **kwargs) + observable_df, model, **kwargs) placeholders = observables.get_placeholders( observable_df, **kwargs) for p in output_parameters: - if p not in placeholders and sbml_model.getParameter(p) is None: + if p not in placeholders: parameter_ids[p] = None # Add condition table parametric overrides unless already defined in the - # SBML model + # model for p in conditions.get_parametric_overrides(condition_df): - if sbml_model.getParameter(p) is None: + if not model.has_entity_with_id(p): parameter_ids[p] = None return parameter_ids.keys() def get_valid_parameters_for_parameter_table( - sbml_model: libsbml.Model, + model: Model, condition_df: pd.DataFrame, observable_df: pd.DataFrame, measurement_df: pd.DataFrame) -> Set[str]: @@ -265,7 +277,7 @@ def get_valid_parameters_for_parameter_table( Get set of parameters which may be present inside the parameter table Arguments: - sbml_model: PEtab SBML model + model: PEtab model condition_df: PEtab condition table observable_df: PEtab observable table measurement_df: PEtab measurement table @@ -274,38 +286,32 @@ def get_valid_parameters_for_parameter_table( Set of parameter IDs which PEtab allows to be present in the parameter table. """ - - # - grab all model parameters + # - grab all allowed model parameters # - grab all output parameters defined in {observable,noise}Formula # - grab all parameters from measurement table # - grab all parametric overrides from condition table # - remove parameters for which condition table columns exist - # - remove observables assigment targets - # - remove sigma assignment targets # - remove placeholder parameters # (only partial overrides are not supported) placeholders = set(observables.get_placeholders(observable_df)) - # exclude rule targets - assignment_targets = {ar.getVariable() - for ar in sbml_model.getListOfRules()} - # must not go into parameter table blackset = set() # collect assignment targets blackset |= placeholders - blackset |= assignment_targets blackset |= set(condition_df.columns.values) - {CONDITION_NAME} - # use ordered dict as proxy for ordered set + # don't use sets here, to have deterministic ordering, + # e.g. for creating parameter tables parameter_ids = OrderedDict.fromkeys( - p.getId() for p in sbml_model.getListOfParameters() - if p.getId() not in blackset) + p for p in model.get_valid_parameters_for_parameter_table() + if p not in blackset + ) # add output parameters from observables table output_parameters = observables.get_output_parameters( - observable_df, sbml_model) + observable_df=observable_df, model=model) for p in output_parameters: if p not in blackset: parameter_ids[p] = None @@ -358,8 +364,10 @@ def get_priors_from_df(parameter_df: pd.DataFrame, if core.is_empty(pars_str): lb, ub = map_scale([row[LOWER_BOUND], row[UPPER_BOUND]], [row[PARAMETER_SCALE]] * 2) - pars_str = f'{lb};{ub}' - prior_pars = tuple([float(entry) for entry in pars_str.split(';')]) + pars_str = f'{lb}{PARAMETER_SEPARATOR}{ub}' + prior_pars = tuple( + float(entry) for entry in pars_str.split(PARAMETER_SEPARATOR) + ) # add parameter scale and bounds, as this may be needed par_scale = row[PARAMETER_SCALE] @@ -488,6 +496,6 @@ def normalize_parameter_df(parameter_df: pd.DataFrame) -> pd.DataFrame: and row[prior_type_col] == PARAMETER_SCALE_UNIFORM: lb, ub = map_scale([row[LOWER_BOUND], row[UPPER_BOUND]], [row[PARAMETER_SCALE]] * 2) - df.loc[irow, prior_par_col] = f'{lb};{ub}' + df.loc[irow, prior_par_col] = f'{lb}{PARAMETER_SEPARATOR}{ub}' return df diff --git a/petab/problem.py b/petab/problem.py index f5238036..9d258f82 100644 --- a/petab/problem.py +++ b/petab/problem.py @@ -1,17 +1,25 @@ """PEtab Problem class""" +from __future__ import annotations import os import tempfile from pathlib import Path, PurePosixPath -from typing import Dict, Iterable, List, Optional, Union +from typing import Dict, Iterable, List, Optional, Union, TYPE_CHECKING from urllib.parse import unquote, urlparse, urlunparse +from warnings import warn -import libsbml import pandas as pd from . import (conditions, core, format_version, measurements, observables, parameter_mapping, parameters, sampling, sbml, yaml) from .C import * # noqa: F403 +from .models import MODEL_TYPE_SBML +from .models.model import Model, model_factory +from .models.sbml_model import SbmlModel + +if TYPE_CHECKING: + import libsbml + __all__ = ['Problem'] @@ -20,7 +28,7 @@ class Problem: """ PEtab parameter estimation problem as defined by - - SBML model + - model - condition table - measurement table - parameter table @@ -34,15 +42,17 @@ class Problem: parameter_df: PEtab parameter table observable_df: PEtab observable table visualization_df: PEtab visualization table - sbml_reader: Stored to keep object alive. - sbml_document: Stored to keep object alive. - sbml_model: PEtab SBML model + model: The underlying model + sbml_reader: Stored to keep object alive (deprecated). + sbml_document: Stored to keep object alive (deprecated). + sbml_model: PEtab SBML model (deprecated) """ def __init__(self, sbml_model: libsbml.Model = None, sbml_reader: libsbml.SBMLReader = None, sbml_document: libsbml.SBMLDocument = None, + model: Model = None, condition_df: pd.DataFrame = None, measurement_df: pd.DataFrame = None, parameter_df: pd.DataFrame = None, @@ -55,35 +65,40 @@ def __init__(self, self.visualization_df: Optional[pd.DataFrame] = visualization_df self.observable_df: Optional[pd.DataFrame] = observable_df - self.sbml_reader: Optional[libsbml.SBMLReader] = sbml_reader - self.sbml_document: Optional[libsbml.SBMLDocument] = sbml_document - self.sbml_model: Optional[libsbml.Model] = sbml_model - - def __getstate__(self): - """Return state for pickling""" - state = self.__dict__.copy() - - # libsbml stuff cannot be serialized directly - if self.sbml_model: - sbml_document = self.sbml_model.getSBMLDocument() - sbml_writer = libsbml.SBMLWriter() - state['sbml_string'] = sbml_writer.writeSBMLToString(sbml_document) - - exclude = ['sbml_reader', 'sbml_document', 'sbml_model'] - for key in exclude: - state.pop(key) - - return state - - def __setstate__(self, state): - """Set state after unpickling""" - # load SBML model from pickled string - sbml_string = state.pop('sbml_string', None) - if sbml_string: - self.sbml_reader, self.sbml_document, self.sbml_model = \ - sbml.load_sbml_from_string(sbml_string) - - self.__dict__.update(state) + if any((sbml_model, sbml_document, sbml_reader),): + warn("Passing `sbml_model`, `sbml_document`, or `sbml_reader` " + "to petab.Problem is deprecated and will be removed in a " + "future version. Use `model=petab.models.SbmlModel(...)` " + "instead.", DeprecationWarning, stacklevel=2) + if model: + raise ValueError("Must only provide one of (`sbml_model`, " + "`sbml_document`, `sbml_reader`) or `model`.") + + model = SbmlModel( + sbml_model=sbml_model, + sbml_reader=sbml_reader, + sbml_document=sbml_document) + + self.model: Optional[Model] = model + + def __getattr__(self, name): + # For backward-compatibility, allow access to SBML model related + # attributes now stored in self.model + if name in {'sbml_model', 'sbml_reader', 'sbml_document'}: + return getattr(self.model, name) if self.model else None + raise AttributeError(f"'{self.__class__.__name__}' object has no " + f"attribute '{name}'") + + def __setattr__(self, name, value): + # For backward-compatibility, allow access to SBML model related + # attributes now stored in self.model + if name in {'sbml_model', 'sbml_reader', 'sbml_document'}: + if self.model: + setattr(self.model, name, value) + else: + self.model = SbmlModel(**{name: value}) + else: + super().__setattr__(name, value) @staticmethod def from_files( @@ -97,7 +112,7 @@ def from_files( visualization_files: Union[str, Path, Iterable[Union[str, Path]]] = None, observable_files: Union[str, Path, - Iterable[Union[str, Path]]] = None + Iterable[Union[str, Path]]] = None, ) -> 'Problem': """ Factory method to load model and tables from files. @@ -110,44 +125,40 @@ def from_files( visualization_files: PEtab visualization tables observable_files: PEtab observables tables """ + warn("petab.Problem.from_files is deprecated and will be removed in a " + "future version. Use `petab.Problem.from_yaml instead.", + DeprecationWarning, stacklevel=2) - sbml_model = sbml_document = sbml_reader = None - condition_df = measurement_df = parameter_df = visualization_df = None - observable_df = None + model = model_factory(sbml_file, MODEL_TYPE_SBML) \ + if sbml_file else None - if condition_file: - condition_df = core.concat_tables(condition_file, - conditions.get_condition_df) + condition_df = core.concat_tables( + condition_file, conditions.get_condition_df) \ + if condition_file else None - if measurement_file: - # If there are multiple tables, we will merge them - measurement_df = core.concat_tables( - measurement_file, measurements.get_measurement_df) - - if parameter_file: - parameter_df = parameters.get_parameter_df(parameter_file) + # If there are multiple tables, we will merge them + measurement_df = core.concat_tables( + measurement_file, measurements.get_measurement_df) \ + if measurement_file else None - if sbml_file: - sbml_reader, sbml_document, sbml_model = \ - sbml.get_sbml_model(sbml_file) + parameter_df = parameters.get_parameter_df(parameter_file) \ + if parameter_file else None - if visualization_files: - # If there are multiple tables, we will merge them - visualization_df = core.concat_tables( - visualization_files, core.get_visualization_df) + # If there are multiple tables, we will merge them + visualization_df = core.concat_tables( + visualization_files, core.get_visualization_df) \ + if visualization_files else None - if observable_files: - # If there are multiple tables, we will merge them - observable_df = core.concat_tables( - observable_files, observables.get_observable_df) + # If there are multiple tables, we will merge them + observable_df = core.concat_tables( + observable_files, observables.get_observable_df) \ + if observable_files else None - return Problem(condition_df=condition_df, + return Problem(model=model, + condition_df=condition_df, measurement_df=measurement_df, parameter_df=parameter_df, observable_df=observable_df, - sbml_model=sbml_model, - sbml_document=sbml_document, - sbml_reader=sbml_reader, visualization_df=visualization_df) @staticmethod @@ -206,26 +217,53 @@ def from_yaml(yaml_config: Union[Dict, Path, str]) -> 'Problem': 'Support for multiple models is not yet implemented.') if isinstance(yaml_config[PARAMETER_FILE], list): - parameter_file = [ - get_path(f) for f in yaml_config[PARAMETER_FILE] + parameter_df = [ + parameters.get_parameter_df(get_path(f)) + for f in yaml_config[PARAMETER_FILE] ] else: - parameter_file = get_path(yaml_config[PARAMETER_FILE]) \ + parameter_df = parameters.get_parameter_df( + get_path(yaml_config[PARAMETER_FILE])) \ if yaml_config[PARAMETER_FILE] else None - return Problem.from_files( - sbml_file=get_path(problem0[SBML_FILES][0]) - if problem0[SBML_FILES] else None, - measurement_file=[get_path(f) - for f in problem0[MEASUREMENT_FILES]], - condition_file=[get_path(f) - for f in problem0[CONDITION_FILES]], - parameter_file=parameter_file, - visualization_files=[ - get_path(f) for f in problem0.get(VISUALIZATION_FILES, [])], - observable_files=[ - get_path(f) for f in problem0.get(OBSERVABLE_FILES, [])] - ) + model = model_factory(get_path(problem0[SBML_FILES][0]), + MODEL_TYPE_SBML) \ + if problem0[SBML_FILES] else None + + measurement_files = [ + get_path(f) for f in problem0.get(MEASUREMENT_FILES, [])] + # If there are multiple tables, we will merge them + measurement_df = core.concat_tables( + measurement_files, measurements.get_measurement_df) \ + if measurement_files else None + + condition_files = [ + get_path(f) for f in problem0.get(CONDITION_FILES, [])] + # If there are multiple tables, we will merge them + condition_df = core.concat_tables( + condition_files, conditions.get_condition_df) \ + if condition_files else None + + visualization_files = [ + get_path(f) for f in problem0.get(VISUALIZATION_FILES, [])] + # If there are multiple tables, we will merge them + visualization_df = core.concat_tables( + visualization_files, core.get_visualization_df) \ + if visualization_files else None + + observable_files = [ + get_path(f) for f in problem0.get(OBSERVABLE_FILES, [])] + # If there are multiple tables, we will merge them + observable_df = core.concat_tables( + observable_files, observables.get_observable_df) \ + if observable_files else None + + return Problem(condition_df=condition_df, + measurement_df=measurement_df, + parameter_df=parameter_df, + observable_df=observable_df, + model=model, + visualization_df=visualization_df) @staticmethod def from_combine(filename: Union[Path, str]) -> 'Problem': @@ -298,7 +336,10 @@ def to_files_generic( if getattr(self, f'{table_name}_df') is not None: filenames[f'{table_name}_file'] = f'{table_name}s.tsv' - if self.sbml_document is not None: + if self.model: + if not isinstance(self.model, SbmlModel): + raise NotImplementedError("Saving non-SBML models is " + "currently not supported.") filenames['sbml_file'] = 'model.xml' filenames['yaml_file'] = 'problem.yaml' @@ -319,6 +360,7 @@ def to_files(self, yaml_file: Union[None, str, Path] = None, prefix_path: Union[None, str, Path] = None, relative_paths: bool = True, + model_file: Union[None, str, Path] = None, ) -> None: """ Write PEtab tables to files for this problem @@ -331,6 +373,7 @@ def to_files(self, Arguments: sbml_file: SBML model destination + model_file: Model destination condition_file: Condition table destination measurement_file: Measurement table destination parameter_file: Parameter table destination @@ -350,6 +393,17 @@ def to_files(self, ValueError: If a destination was provided for a non-existing entity. """ + if sbml_file: + warn("The `sbml_file` argument is deprecated and will be " + "removed in a future version. Use `model_file` instead.", + DeprecationWarning, stacklevel=2) + + if model_file: + raise ValueError("Must provide either `sbml_file` or " + "`model_file` argument, but not both.") + + model_file = sbml_file + if prefix_path is not None: prefix_path = Path(prefix_path) @@ -358,7 +412,7 @@ def add_prefix(path0: Union[None, str, Path]) -> str: return path0 return str(prefix_path / path0) - sbml_file = add_prefix(sbml_file) + model_file = add_prefix(model_file) condition_file = add_prefix(condition_file) measurement_file = add_prefix(measurement_file) parameter_file = add_prefix(parameter_file) @@ -366,12 +420,8 @@ def add_prefix(path0: Union[None, str, Path]) -> str: visualization_file = add_prefix(visualization_file) yaml_file = add_prefix(yaml_file) - if sbml_file: - if self.sbml_document is not None: - sbml.write_sbml(self.sbml_document, sbml_file) - else: - raise ValueError("Unable to save SBML model with no " - "sbml_doc set.") + if model_file: + self.model.to_file(model_file) def error(name: str) -> ValueError: return ValueError(f"Unable to save non-existent {name} table") @@ -436,6 +486,10 @@ def get_optimization_parameter_scales(self): def get_model_parameters(self): """See :py:func:`petab.sbml.get_model_parameters`""" + warn("petab.Problem.get_model_parameters is deprecated and will be " + "removed in a future version.", + DeprecationWarning, stacklevel=2) + return sbml.get_model_parameters(self.sbml_model) def get_observable_ids(self): @@ -655,13 +709,13 @@ def get_optimization_to_simulation_parameter_mapping( """ See get_simulation_to_optimization_parameter_mapping. """ - return parameter_mapping\ + return parameter_mapping \ .get_optimization_to_simulation_parameter_mapping( - self.condition_df, - self.measurement_df, - self.parameter_df, - self.observable_df, - self.sbml_model, + condition_df=self.condition_df, + measurement_df=self.measurement_df, + parameter_df=self.parameter_df, + observable_df=self.observable_df, + model=self.model, warn_unmapped=warn_unmapped, scaled_parameters=scaled_parameters, allow_timepoint_specific_numeric_noise_parameters= # noqa: E251,E501 @@ -674,10 +728,10 @@ def create_parameter_df(self, *args, **kwargs): See :py:func:`create_parameter_df`. """ return parameters.create_parameter_df( - self.sbml_model, - self.condition_df, - self.observable_df, - self.measurement_df, + model=self.model, + condition_df=self.condition_df, + observable_df=self.observable_df, + measurement_df=self.measurement_df, *args, **kwargs) def sample_parameter_startpoints(self, n_starts: int = 100): diff --git a/tests/test_lint.py b/tests/test_lint.py index 85ea6626..f1935add 100644 --- a/tests/test_lint.py +++ b/tests/test_lint.py @@ -211,11 +211,12 @@ def test_assert_no_leading_trailing_whitespace(): def test_assert_model_parameters_in_condition_or_parameter_table(): import simplesbml + from petab.models.sbml_model import SbmlModel ss_model = simplesbml.SbmlModel() ss_model.addParameter('parameter1', 0.0) ss_model.addParameter('noiseParameter1_', 0.0) ss_model.addParameter('observableParameter1_', 0.0) - sbml_model = ss_model.model + sbml_model = SbmlModel(sbml_model=ss_model.model) lint.assert_model_parameters_in_condition_or_parameter_table( sbml_model, pd.DataFrame(columns=['parameter1']), pd.DataFrame() @@ -405,8 +406,9 @@ def test_assert_measurement_conditions_present_in_condition_table(): def test_check_condition_df(): """Check that we correctly detect errors in condition table""" import simplesbml + from petab.models.sbml_model import SbmlModel ss_model = simplesbml.SbmlModel() - + model = SbmlModel(sbml_model=ss_model.model) condition_df = pd.DataFrame(data={ CONDITION_ID: ['condition1'], 'p1': [nan], @@ -415,29 +417,29 @@ def test_check_condition_df(): # parameter missing in model with pytest.raises(AssertionError): - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) # fix: ss_model.addParameter('p1', 1.0) - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) # species missing in model condition_df['s1'] = [3.0] with pytest.raises(AssertionError): - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) # fix: ss_model.addSpecies("[s1]", 1.0) - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) # compartment missing in model condition_df['c2'] = [4.0] with pytest.raises(AssertionError): - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) # fix: ss_model.addCompartment(comp_id='c2', vol=1.0) - lint.check_condition_df(condition_df, ss_model.model) + lint.check_condition_df(condition_df, model) def test_check_ids(): diff --git a/tests/test_observables.py b/tests/test_observables.py index d18c20a4..4492e8a1 100644 --- a/tests/test_observables.py +++ b/tests/test_observables.py @@ -65,6 +65,7 @@ def test_get_output_parameters(): """Test measurements.get_output_parameters.""" # sbml model import simplesbml + from petab.models.sbml_model import SbmlModel ss_model = simplesbml.SbmlModel() ss_model.addParameter('fixedParameter1', 1.0) ss_model.addParameter('observable_1', 1.0) @@ -78,7 +79,7 @@ def test_get_output_parameters(): }).set_index(OBSERVABLE_ID) output_parameters = petab.get_output_parameters( - observable_df, ss_model.model) + observable_df, SbmlModel(sbml_model=ss_model.model)) assert output_parameters == ['offset', 'scaling'] diff --git a/tests/test_parameter_mapping.py b/tests/test_parameter_mapping.py index bc3c57b9..4446c2ac 100644 --- a/tests/test_parameter_mapping.py +++ b/tests/test_parameter_mapping.py @@ -1,10 +1,13 @@ -import numpy as np import os +from math import nan + +import numpy as np import pandas as pd import petab -from petab.parameter_mapping import _apply_parameter_table from petab.C import * -from math import nan +from petab.models.sbml_model import SbmlModel +from petab.parameter_mapping import _apply_parameter_table + # import fixtures pytest_plugins = [ @@ -62,8 +65,9 @@ def test_no_condition_specific(condition_df_2_conditions): 'fixedParameter1': LIN} )] + model = SbmlModel(sbml_model=ss_model.model) actual = petab.get_optimization_to_simulation_parameter_mapping( - sbml_model=ss_model.model, + model=model, measurement_df=measurement_df, condition_df=condition_df, ) @@ -102,7 +106,7 @@ def test_no_condition_specific(condition_df_2_conditions): ] actual = petab.get_optimization_to_simulation_parameter_mapping( - sbml_model=ss_model.model, + model=model, measurement_df=measurement_df, condition_df=condition_df, parameter_df=parameter_df @@ -136,7 +140,7 @@ def test_no_condition_specific(condition_df_2_conditions): ] actual = petab.get_optimization_to_simulation_parameter_mapping( - sbml_model=ss_model.model, + model=model, measurement_df=measurement_df, condition_df=condition_df, parameter_df=parameter_df, @@ -171,7 +175,7 @@ def test_no_condition_specific(condition_df_2_conditions): ] actual = petab.get_optimization_to_simulation_parameter_mapping( - sbml_model=ss_model.model, + model=model, measurement_df=measurement_df, condition_df=condition_df, parameter_df=parameter_df, @@ -189,6 +193,7 @@ def test_all_override(condition_df_2_conditions): ss_model = simplesbml.SbmlModel() ss_model.addParameter('dynamicParameter1', 0.0) ss_model.addParameter('dynamicParameter2', 0.0) + model = SbmlModel(sbml_model=ss_model.model) measurement_df = pd.DataFrame(data={ OBSERVABLE_ID: ['obs1', 'obs2', 'obs1', 'obs2'], @@ -254,7 +259,9 @@ def test_all_override(condition_df_2_conditions): actual = petab.get_optimization_to_simulation_parameter_mapping( measurement_df=measurement_df, condition_df=condition_df, - sbml_model=ss_model.model, parameter_df=parameter_df) + model=model, + parameter_df=parameter_df + ) assert actual == expected # For one case we test parallel execution, which must yield the same @@ -263,7 +270,9 @@ def test_all_override(condition_df_2_conditions): actual = petab.get_optimization_to_simulation_parameter_mapping( measurement_df=measurement_df, condition_df=condition_df, - sbml_model=ss_model.model, parameter_df=parameter_df) + model=model, + parameter_df=parameter_df + ) assert actual == expected @staticmethod diff --git a/tests/test_petab.py b/tests/test_petab.py index 0e0c4e00..e7b1eb9e 100644 --- a/tests/test_petab.py +++ b/tests/test_petab.py @@ -1,6 +1,7 @@ import copy import pickle import tempfile +import warnings from io import StringIO from math import nan from pathlib import Path @@ -9,9 +10,11 @@ import libsbml import numpy as np import pandas as pd -import petab import pytest + +import petab from petab.C import * +from petab.models.sbml_model import SbmlModel from yaml import safe_load @@ -204,6 +207,7 @@ def test_create_parameter_df( ss_model.addSpecies('[x1]', 1.0) ss_model.addParameter('fixedParameter1', 2.0) ss_model.addParameter('p0', 3.0) + model = SbmlModel(sbml_model=ss_model.model) observable_df = pd.DataFrame(data={ OBSERVABLE_ID: ['obs1', 'obs2'], @@ -220,16 +224,28 @@ def test_create_parameter_df( NOISE_PARAMETERS: ['p3;p4', 'p5'] }) - parameter_df = petab.create_parameter_df( - ss_model.model, - condition_df_2_conditions, - observable_df, - measurement_df) - # first model parameters, then row by row noise and sigma overrides expected = ['p3', 'p4', 'p1', 'p2', 'p5'] - actual = parameter_df.index.values.tolist() - assert actual == expected + + # Test old API with passing libsbml.Model directly + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + parameter_df = petab.create_parameter_df( + ss_model.model, + condition_df_2_conditions, + observable_df, + measurement_df) + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert parameter_df.index.values.tolist() == expected + + parameter_df = petab.create_parameter_df( + model=model, + condition_df=condition_df_2_conditions, + observable_df=observable_df, + measurement_df=measurement_df + ) + assert parameter_df.index.values.tolist() == expected # test with condition parameter override: condition_df_2_conditions.loc['condition2', 'fixedParameter1'] \ @@ -237,10 +253,11 @@ def test_create_parameter_df( expected = ['p3', 'p4', 'p1', 'p2', 'p5', 'overrider'] parameter_df = petab.create_parameter_df( - ss_model.model, - condition_df_2_conditions, - observable_df, - measurement_df) + model=model, + condition_df=condition_df_2_conditions, + observable_df=observable_df, + measurement_df=measurement_df, + ) actual = parameter_df.index.values.tolist() assert actual == expected @@ -248,10 +265,10 @@ def test_create_parameter_df( expected = ['p0', 'p3', 'p4', 'p1', 'p2', 'p5', 'overrider'] parameter_df = petab.create_parameter_df( - ss_model.model, - condition_df_2_conditions, - observable_df, - measurement_df, + model=model, + condition_df=condition_df_2_conditions, + observable_df=observable_df, + measurement_df=measurement_df, include_optional=True) actual = parameter_df.index.values.tolist() assert actual == expected