Source code for amici.petab_import_pysb

"""
PySB-PEtab Import
-----------------
Import a model in the PySB-adapted :mod:`petab`
(https://github.com/PEtab-dev/PEtab) format into AMICI.
"""

import logging
import os
from itertools import chain
from typing import List, Dict, Union, Optional, Tuple, Iterable

import libsbml
import petab
import pysb
import sympy as sp
from petab.C import (CONDITION_NAME, OBSERVABLE_TRANSFORMATION, LIN,
                     OBSERVABLE_FORMULA, NOISE_FORMULA, FORMAT_VERSION,
                     PARAMETER_FILE, SBML_FILES, CONDITION_FILES,
                     MEASUREMENT_FILES, VISUALIZATION_FILES, OBSERVABLE_FILES)

from . import petab_import
from .logging import get_logger, log_execution_time, set_log_level

logger = get_logger(__name__, logging.WARNING)


[docs]class PysbPetabProblem(petab.Problem): """Representation of a PySB-model-based PEtab problem This class extends :class:`petab.Problem` with a PySB model. The model is augmented with the observation model based on the PEtab observable table. For now, a dummy SBML model is created which allows used the existing SBML-PEtab API. :ivar pysb_model: PySB model instance from of this PEtab problem. """
[docs] def __init__(self, pysb_model: 'pysb.Model' = None, *args, **kwargs): """ Constructor :param pysb_model: PySB model instance for this PEtab problem :param args: See :meth:`petab.Problem.__init__` :param kwargs: See :meth:`petab.Problem.__init__` """ flatten = kwargs.pop('flatten', False) super().__init__(*args, **kwargs) if flatten: petab.flatten_timepoint_specific_output_overrides(self) self.pysb_model: 'pysb.Model' = pysb_model self._add_observation_model() if self.pysb_model is not None: self.sbml_document, self.sbml_model = \ create_dummy_sbml( self.pysb_model, observable_ids=self.observable_df.index.values if self.observable_df is not None else None )
def _add_observation_model(self): """Extend PySB model by observation model as defined in the PEtab observables table""" # add any required output parameters local_syms = {sp.Symbol.__str__(comp): comp for comp in self.pysb_model.components if isinstance(comp, sp.Symbol)} for formula in [*self.observable_df[OBSERVABLE_FORMULA], *self.observable_df[NOISE_FORMULA]]: sym = sp.sympify(formula, locals=local_syms) for s in sym.free_symbols: if not isinstance(s, pysb.Component): p = pysb.Parameter(str(s), 1.0, _export=False) self.pysb_model.add_component(p) local_syms[sp.Symbol.__str__(p)] = p # add observables and sigmas to pysb model for (observable_id, observable_formula, noise_formula) \ in zip(self.observable_df.index, self.observable_df[OBSERVABLE_FORMULA], self.observable_df[NOISE_FORMULA]): obs_symbol = sp.sympify(observable_formula, locals=local_syms) if observable_id in self.pysb_model.expressions.keys(): obs_expr = self.pysb_model.expressions[observable_id] else: obs_expr = pysb.Expression(observable_id, obs_symbol, _export=False) self.pysb_model.add_component(obs_expr) local_syms[observable_id] = obs_expr sigma_id = f"{observable_id}_sigma" sigma_symbol = sp.sympify( noise_formula, locals=local_syms ) sigma_expr = pysb.Expression(sigma_id, sigma_symbol, _export=False) self.pysb_model.add_component(sigma_expr) local_syms[sigma_id] = sigma_expr
[docs] @staticmethod def from_files(condition_file: str = None, measurement_file: Union[str, Iterable[str]] = None, parameter_file: Union[str, List[str]] = None, visualization_files: Union[str, Iterable[str]] = None, observable_files: Union[str, Iterable[str]] = None, pysb_model_file: str = None, flatten: bool = False) -> 'PysbPetabProblem': """ Factory method to load model and tables from files. :param condition_file: PEtab condition table :param measurement_file: PEtab measurement table :param parameter_file: PEtab parameter table :param visualization_files: PEtab visualization tables :param observable_files: PEtab observables tables :param pysb_model_file: PySB model file :param flatten: Flatten the petab problem :return: Petab Problem """ condition_df = measurement_df = parameter_df = visualization_df = None observable_df = None if condition_file: condition_df = petab.conditions.get_condition_df(condition_file) if measurement_file: # If there are multiple tables, we will merge them measurement_df = petab.core.concat_tables( measurement_file, petab.measurements.get_measurement_df) if parameter_file: parameter_df = petab.parameters.get_parameter_df(parameter_file) if visualization_files: # If there are multiple tables, we will merge them visualization_df = petab.core.concat_tables( visualization_files, petab.core.get_visualization_df) if observable_files: # If there are multiple tables, we will merge them observable_df = petab.core.concat_tables( observable_files, petab.observables.get_observable_df) from amici.pysb_import import pysb_model_from_path return PysbPetabProblem( pysb_model=pysb_model_from_path( pysb_model_file=pysb_model_file), condition_df=condition_df, measurement_df=measurement_df, parameter_df=parameter_df, observable_df=observable_df, visualization_df=visualization_df, flatten=flatten )
[docs] @staticmethod def from_yaml(yaml_config: Union[Dict, str], flatten: bool = False) -> 'PysbPetabProblem': """ Factory method to load model and tables as specified by YAML file. NOTE: The PySB model is currently expected in the YAML file under ``sbml_files``. :param yaml_config: PEtab configuration as dictionary or YAML file name :param flatten: Flatten the petab problem :return: Petab Problem """ from petab.yaml import (load_yaml, is_composite_problem, assert_single_condition_and_sbml_file) if isinstance(yaml_config, str): path_prefix = os.path.dirname(yaml_config) yaml_config = load_yaml(yaml_config) else: path_prefix = "" if is_composite_problem(yaml_config): raise ValueError('petab.Problem.from_yaml() can only be used for ' 'yaml files comprising a single model. ' 'Consider using ' 'petab.CompositeProblem.from_yaml() instead.') if yaml_config[FORMAT_VERSION] != petab.__format_version__: raise ValueError("Provided PEtab files are of unsupported version" f"{yaml_config[FORMAT_VERSION]}. Expected " f"{petab.__format_version__}.") problem0 = yaml_config['problems'][0] assert_single_condition_and_sbml_file(problem0) if isinstance(yaml_config[PARAMETER_FILE], list): parameter_file = [ os.path.join(path_prefix, f) for f in yaml_config[PARAMETER_FILE] ] else: parameter_file = os.path.join( path_prefix, yaml_config[PARAMETER_FILE]) return PysbPetabProblem.from_files( pysb_model_file=os.path.join( path_prefix, problem0[SBML_FILES][0]), measurement_file=[os.path.join(path_prefix, f) for f in problem0[MEASUREMENT_FILES]], condition_file=os.path.join( path_prefix, problem0[CONDITION_FILES][0]), parameter_file=parameter_file, visualization_files=[ os.path.join(path_prefix, f) for f in problem0.get(VISUALIZATION_FILES, [])], observable_files=[ os.path.join(path_prefix, f) for f in problem0.get(OBSERVABLE_FILES, [])], flatten=flatten )
[docs]def create_dummy_sbml( pysb_model: 'pysb.Model', observable_ids: Optional[Iterable[str]] = None ) -> Tuple['libsbml.Model', 'libsbml.SBMLDocument']: """Create SBML dummy model for to use PySB models with PEtab. Model must at least contain PEtab problem parameter and noise parameters for observables. :param pysb_model: PySB model :param observable_ids: Observable IDs :return: A dummy SBML model and document. """ import libsbml document = libsbml.SBMLDocument(3, 1) dummy_sbml_model = document.createModel() dummy_sbml_model.setTimeUnits("second") dummy_sbml_model.setExtentUnits("mole") dummy_sbml_model.setSubstanceUnits('mole') # mandatory if there are species c = dummy_sbml_model.createCompartment() c.setId('dummy_compartment') c.setConstant(False) # parameters are required for parameter mapping for parameter in pysb_model.parameters: p = dummy_sbml_model.createParameter() p.setId(parameter.name) p.setConstant(True) p.setValue(0.0) # noise parameters are required for every observable for observable_id in observable_ids: p = dummy_sbml_model.createParameter() p.setId(f"noiseParameter1_{observable_id}") p.setConstant(True) p.setValue(0.0) # pysb observables and expressions are required in case they occur in # the observableFormula or noiseFormula. # as this code is only temporary and not performance-critical, we just add # all of them. we just need an sbml entity with the same ID. sbml species # seem to be the simplest, as parameters would interfere with parameter # mapping later on for component in chain(pysb_model.expressions, pysb_model.observables): s = dummy_sbml_model.createSpecies() s.setId(component.name) s.setInitialAmount(0.0) s.setHasOnlySubstanceUnits(False) s.setBoundaryCondition(False) s.setCompartment('dummy_compartment') s.setConstant(False) return document, dummy_sbml_model
[docs]@log_execution_time('Importing PEtab model', logger) def import_model_pysb( petab_problem: PysbPetabProblem, model_output_dir: Optional[str] = None, verbose: Optional[Union[bool, int]] = True, **kwargs ) -> None: """ Create AMICI model from PySB-PEtab problem :param petab_problem: PySB PEtab problem :param model_output_dir: Directory to write the model code to. Will be created if doesn't exist. Defaults to current directory. :param verbose: Print/log extra information. :param kwargs: Additional keyword arguments to be passed to :meth:`amici.pysb_import.pysb2amici`. """ set_log_level(logger, verbose) logger.info(f"Importing model ...") observable_table = petab_problem.observable_df pysb_model = petab_problem.pysb_model # For pysb, we only allow parameters in the condition table # those must be pysb model parameters (either natively, or output # parameters from measurement or condition table that have been added in # PysbPetabProblem) model_parameters = [p.name for p in pysb_model.parameters] for x in petab_problem.condition_df.columns: if x == CONDITION_NAME: continue if x not in model_parameters: raise NotImplementedError( "For PySB PEtab import, only model parameters, but no states " "or compartments are allowed in the condition table." f"Offending column: {x}" ) constant_parameters = petab_import.get_fixed_parameters( petab_problem.sbml_model, petab_problem.condition_df) if observable_table is None: observables = None sigmas = None else: observables = [expr.name for expr in pysb_model.expressions if expr.name in observable_table.index] sigmas = {obs_id: f"{obs_id}_sigma" for obs_id in observables} noise_distrs = petab_import.petab_noise_distributions_to_amici( observable_table) from amici.pysb_import import pysb2amici pysb2amici(pysb_model, model_output_dir, verbose=True, observables=observables, sigmas=sigmas, constant_parameters=constant_parameters, noise_distributions=noise_distrs, **kwargs)