From 9dfdb1e8b02d09a6200f7ebc69812648063757c4 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Tue, 21 Nov 2023 21:34:40 +0100 Subject: [PATCH] Refactor: PEtab code to sub-package (cont.) --- python/sdist/amici/parameter_mapping.py | 7 + python/sdist/amici/petab/import_helpers.py | 272 ++++++ python/sdist/amici/petab/petab_import.py | 150 ++++ python/sdist/amici/petab/pysb_import.py | 274 ++++++ python/sdist/amici/petab/sbml_import.py | 553 +++++++++++++ python/sdist/amici/petab_import.py | 914 +-------------------- python/sdist/amici/petab_import_pysb.py | 279 +------ python/sdist/amici/petab_objective.py | 5 +- python/sdist/amici/petab_util.py | 6 +- python/tests/test_petab_objective.py | 4 +- 10 files changed, 1296 insertions(+), 1168 deletions(-) create mode 100644 python/sdist/amici/petab/import_helpers.py create mode 100644 python/sdist/amici/petab/petab_import.py create mode 100644 python/sdist/amici/petab/pysb_import.py create mode 100644 python/sdist/amici/petab/sbml_import.py diff --git a/python/sdist/amici/parameter_mapping.py b/python/sdist/amici/parameter_mapping.py index dd961f43c1..e27b210c46 100644 --- a/python/sdist/amici/parameter_mapping.py +++ b/python/sdist/amici/parameter_mapping.py @@ -1,4 +1,6 @@ # some extra imports for backward-compatibility +import warnings + from .petab.conditions import ( # noqa # pylint: disable=unused-import fill_in_parameters, fill_in_parameters_for_condition, @@ -15,3 +17,8 @@ unscale_parameter, unscale_parameters_dict, ) + +warnings.warn( + "Importing amici.parameter_mapping is deprecated. Use `amici.petab.parameter_mapping` instead.", + DeprecationWarning, +) diff --git a/python/sdist/amici/petab/import_helpers.py b/python/sdist/amici/petab/import_helpers.py new file mode 100644 index 0000000000..3caf951ace --- /dev/null +++ b/python/sdist/amici/petab/import_helpers.py @@ -0,0 +1,272 @@ +"""General helper functions for PEtab import. + +Functions for PEtab import that are independent of the model format. +""" +import importlib +import logging +import os +import re +from pathlib import Path +from typing import Union + +import amici +import pandas as pd +import petab +import sympy as sp +from petab.C import ( + CONDITION_NAME, + ESTIMATE, + NOISE_DISTRIBUTION, + NOISE_FORMULA, + OBSERVABLE_FORMULA, + OBSERVABLE_NAME, + OBSERVABLE_TRANSFORMATION, +) +from petab.parameters import get_valid_parameters_for_parameter_table +from sympy.abc import _clash + +logger = logging.getLogger(__name__) + + +def get_observation_model( + observable_df: pd.DataFrame, +) -> tuple[ + dict[str, dict[str, str]], dict[str, str], dict[str, Union[str, float]] +]: + """ + Get observables, sigmas, and noise distributions from PEtab observation + table in a format suitable for + :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. + + :param observable_df: + PEtab observables table + + :return: + Tuple of dicts with observables, noise distributions, and sigmas. + """ + if observable_df is None: + return {}, {}, {} + + observables = {} + sigmas = {} + + nan_pat = r"^[nN]a[nN]$" + for _, observable in observable_df.iterrows(): + oid = str(observable.name) + # need to sanitize due to https://github.com/PEtab-dev/PEtab/issues/447 + name = re.sub(nan_pat, "", str(observable.get(OBSERVABLE_NAME, ""))) + formula_obs = re.sub(nan_pat, "", str(observable[OBSERVABLE_FORMULA])) + formula_noise = re.sub(nan_pat, "", str(observable[NOISE_FORMULA])) + observables[oid] = {"name": name, "formula": formula_obs} + sigmas[oid] = formula_noise + + # PEtab does currently not allow observables in noiseFormula and AMICI + # cannot handle states in sigma expressions. Therefore, where possible, + # replace species occurring in error model definition by observableIds. + replacements = { + sp.sympify(observable["formula"], locals=_clash): sp.Symbol( + observable_id + ) + for observable_id, observable in observables.items() + } + for observable_id, formula in sigmas.items(): + repl = sp.sympify(formula, locals=_clash).subs(replacements) + sigmas[observable_id] = str(repl) + + noise_distrs = petab_noise_distributions_to_amici(observable_df) + + return observables, noise_distrs, sigmas + + +def petab_noise_distributions_to_amici( + observable_df: pd.DataFrame, +) -> dict[str, str]: + """ + Map from the petab to the amici format of noise distribution + identifiers. + + :param observable_df: + PEtab observable table + + :return: + dictionary of observable_id => AMICI noise-distributions + """ + amici_distrs = {} + for _, observable in observable_df.iterrows(): + amici_val = "" + + if ( + OBSERVABLE_TRANSFORMATION in observable + and isinstance(observable[OBSERVABLE_TRANSFORMATION], str) + and observable[OBSERVABLE_TRANSFORMATION] + ): + amici_val += observable[OBSERVABLE_TRANSFORMATION] + "-" + + if ( + NOISE_DISTRIBUTION in observable + and isinstance(observable[NOISE_DISTRIBUTION], str) + and observable[NOISE_DISTRIBUTION] + ): + amici_val += observable[NOISE_DISTRIBUTION] + else: + amici_val += "normal" + amici_distrs[observable.name] = amici_val + + return amici_distrs + + +def petab_scale_to_amici_scale(scale_str: str) -> int: + """Convert PEtab parameter scaling string to AMICI scaling integer""" + + if scale_str == petab.LIN: + return amici.ParameterScaling_none + if scale_str == petab.LOG: + return amici.ParameterScaling_ln + if scale_str == petab.LOG10: + return amici.ParameterScaling_log10 + + raise ValueError(f"Invalid parameter scale {scale_str}") + + +def _create_model_name(folder: Union[str, Path]) -> str: + """ + Create a name for the model. + Just re-use the last part of the folder. + """ + return os.path.split(os.path.normpath(folder))[-1] + + +def _can_import_model( + model_name: str, model_output_dir: Union[str, Path] +) -> bool: + """ + Check whether a module of that name can already be imported. + """ + # try to import (in particular checks version) + try: + with amici.add_path(model_output_dir): + model_module = importlib.import_module(model_name) + except ModuleNotFoundError: + return False + + # no need to (re-)compile + return hasattr(model_module, "getModel") + + +def get_fixed_parameters( + petab_problem: petab.Problem, + non_estimated_parameters_as_constants=True, +) -> list[str]: + """ + Determine, set and return fixed model parameters. + + Non-estimated parameters and parameters specified in the condition table + are turned into constants (unless they are overridden). + Only global SBML parameters are considered. Local parameters are ignored. + + :param petab_problem: + The PEtab problem instance + + :param non_estimated_parameters_as_constants: + Whether parameters marked as non-estimated in PEtab should be + considered constant in AMICI. Setting this to ``True`` will reduce + model size and simulation times. If sensitivities with respect to those + parameters are required, this should be set to ``False``. + + :return: + list of IDs of parameters which are to be considered constant. + """ + # if we have a parameter table, all parameters that are allowed to be + # listed in the parameter table, but are not marked as estimated, can be + # turned into AMICI constants + # due to legacy API, we might not always have a parameter table, though + fixed_parameters = set() + if petab_problem.parameter_df is not None: + all_parameters = get_valid_parameters_for_parameter_table( + model=petab_problem.model, + condition_df=petab_problem.condition_df, + observable_df=petab_problem.observable_df + if petab_problem.observable_df is not None + else pd.DataFrame(columns=petab.OBSERVABLE_DF_REQUIRED_COLS), + measurement_df=petab_problem.measurement_df + if petab_problem.measurement_df is not None + else pd.DataFrame(columns=petab.MEASUREMENT_DF_REQUIRED_COLS), + ) + if non_estimated_parameters_as_constants: + estimated_parameters = petab_problem.parameter_df.index.values[ + petab_problem.parameter_df[ESTIMATE] == 1 + ] + else: + # don't treat parameter table parameters as constants + estimated_parameters = petab_problem.parameter_df.index.values + fixed_parameters = set(all_parameters) - set(estimated_parameters) + + # Column names are model parameter IDs, compartment IDs or species IDs. + # Thereof, all parameters except for any overridden ones should be made + # constant. + # (Could potentially still be made constant, but leaving them might + # increase model reusability) + + # handle parameters in condition table + condition_df = petab_problem.condition_df + if condition_df is not None: + logger.debug(f"Condition table: {condition_df.shape}") + + # remove overridden parameters (`object`-type columns) + fixed_parameters.update( + p + for p in condition_df.columns + # get rid of conditionName column + if p != CONDITION_NAME + # there is no parametric override + # TODO: could check if the final overriding parameter is estimated + # or not, but for now, we skip the parameter if there is any kind + # of overriding + if condition_df[p].dtype != "O" + # p is a parameter + and not petab_problem.model.is_state_variable(p) + ) + + # Ensure mentioned parameters exist in the model. Remove additional ones + # from list + for fixed_parameter in fixed_parameters.copy(): + # check global parameters + if not petab_problem.model.has_entity_with_id(fixed_parameter): + # TODO: could still exist as an output parameter? + logger.warning( + f"Column '{fixed_parameter}' used in condition " + "table but not entity with the corresponding ID " + "exists. Ignoring." + ) + fixed_parameters.remove(fixed_parameter) + + return list(sorted(fixed_parameters)) + + +def check_model( + amici_model: amici.Model, + petab_problem: petab.Problem, +) -> None: + """Check that the model is consistent with the PEtab problem.""" + if petab_problem.parameter_df is None: + return + + amici_ids_free = set(amici_model.getParameterIds()) + amici_ids = amici_ids_free | set(amici_model.getFixedParameterIds()) + + petab_ids_free = set( + petab_problem.parameter_df.loc[ + petab_problem.parameter_df[ESTIMATE] == 1 + ].index + ) + + amici_ids_free_required = petab_ids_free.intersection(amici_ids) + + if not amici_ids_free_required.issubset(amici_ids_free): + raise ValueError( + "The available AMICI model does not support estimating the " + "following parameters. Please recompile the model and ensure " + "that these parameters are not treated as constants. Deleting " + "the current model might also resolve this. Parameters: " + f"{amici_ids_free_required.difference(amici_ids_free)}" + ) diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py new file mode 100644 index 0000000000..b9cbb1a433 --- /dev/null +++ b/python/sdist/amici/petab/petab_import.py @@ -0,0 +1,150 @@ +""" +PEtab Import +------------ +Import a model in the :mod:`petab` (https://github.com/PEtab-dev/PEtab) format +into AMICI. +""" + +import logging +import os +import shutil +from pathlib import Path +from typing import Union + +import amici +import petab +from petab.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML + +from ..logging import get_logger +from .import_helpers import _can_import_model, _create_model_name, check_model +from .sbml_import import import_model_sbml + +try: + from .pysb_import import import_model_pysb +except ModuleNotFoundError: + # pysb not available + import_model_pysb = None + +logger = get_logger(__name__, logging.WARNING) + + +def import_petab_problem( + petab_problem: petab.Problem, + model_output_dir: Union[str, Path, None] = None, + model_name: str = None, + force_compile: bool = False, + non_estimated_parameters_as_constants=True, + **kwargs, +) -> "amici.Model": + """ + Import model from petab problem. + + :param petab_problem: + A petab problem containing all relevant information on the model. + + :param model_output_dir: + Directory to write the model code to. Will be created if doesn't + exist. Defaults to current directory. + + :param model_name: + Name of the generated model. If model file name was provided, + this defaults to the file name without extension, otherwise + the model ID will be used. + + :param force_compile: + Whether to compile the model even if the target folder is not empty, + or the model exists already. + + :param non_estimated_parameters_as_constants: + Whether parameters marked as non-estimated in PEtab should be + considered constant in AMICI. Setting this to ``True`` will reduce + model size and simulation times. If sensitivities with respect to those + parameters are required, this should be set to ``False``. + + :param kwargs: + Additional keyword arguments to be passed to + :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. + + :return: + The imported model. + """ + if petab_problem.model.type_id not in (MODEL_TYPE_SBML, MODEL_TYPE_PYSB): + raise NotImplementedError( + "Unsupported model type " + petab_problem.model.type_id + ) + + if petab_problem.mapping_df is not None: + # It's partially supported. Remove at your own risk... + raise NotImplementedError( + "PEtab v2.0.0 mapping tables are not yet supported." + ) + + model_name = model_name or petab_problem.model.model_id + + if petab_problem.model.type_id == MODEL_TYPE_PYSB and model_name is None: + model_name = petab_problem.pysb_model.name + elif model_name is None and model_output_dir: + model_name = _create_model_name(model_output_dir) + + # generate folder and model name if necessary + if model_output_dir is None: + if petab_problem.model.type_id == MODEL_TYPE_PYSB: + raise ValueError("Parameter `model_output_dir` is required.") + + from .sbml_import import _create_model_output_dir_name + + model_output_dir = _create_model_output_dir_name( + petab_problem.sbml_model, model_name + ) + else: + model_output_dir = os.path.abspath(model_output_dir) + + # create folder + if not os.path.exists(model_output_dir): + os.makedirs(model_output_dir) + + # check if compilation necessary + if force_compile or not _can_import_model(model_name, model_output_dir): + # check if folder exists + if os.listdir(model_output_dir) and not force_compile: + raise ValueError( + f"Cannot compile to {model_output_dir}: not empty. " + "Please assign a different target or set `force_compile`." + ) + + # remove folder if exists + if os.path.exists(model_output_dir): + shutil.rmtree(model_output_dir) + + logger.info(f"Compiling model {model_name} to {model_output_dir}.") + # compile the model + if petab_problem.model.type_id == MODEL_TYPE_PYSB: + import_model_pysb( + petab_problem, + model_name=model_name, + model_output_dir=model_output_dir, + **kwargs, + ) + else: + import_model_sbml( + petab_problem=petab_problem, + model_name=model_name, + model_output_dir=model_output_dir, + non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, + **kwargs, + ) + + # import model + model_module = amici.import_model_module(model_name, model_output_dir) + model = model_module.getModel() + check_model(amici_model=model, petab_problem=petab_problem) + + logger.info( + f"Successfully loaded model {model_name} " f"from {model_output_dir}." + ) + + return model + + +# for backwards compatibility +import_model = import_model_sbml diff --git a/python/sdist/amici/petab/pysb_import.py b/python/sdist/amici/petab/pysb_import.py new file mode 100644 index 0000000000..51f53037af --- /dev/null +++ b/python/sdist/amici/petab/pysb_import.py @@ -0,0 +1,274 @@ +""" +PySB-PEtab Import +----------------- +Import a model in the PySB-adapted :mod:`petab` +(https://github.com/PEtab-dev/PEtab) format into AMICI. +""" + +import logging +import re +from pathlib import Path +from typing import Optional, Union + +import petab +import pysb +import pysb.bng +import sympy as sp +from petab.C import CONDITION_NAME, NOISE_FORMULA, OBSERVABLE_FORMULA +from petab.models.pysb_model import PySBModel + +from ..logging import get_logger, log_execution_time, set_log_level +from . import PREEQ_INDICATOR_ID +from .import_helpers import ( + get_fixed_parameters, + petab_noise_distributions_to_amici, +) +from .util import get_states_in_condition_table + +logger = get_logger(__name__, logging.WARNING) + + +def _add_observation_model( + pysb_model: pysb.Model, petab_problem: petab.Problem +): + """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 pysb_model.components + if isinstance(comp, sp.Symbol) + } + for formula in [ + *petab_problem.observable_df[OBSERVABLE_FORMULA], + *petab_problem.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) + 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( + petab_problem.observable_df.index, + petab_problem.observable_df[OBSERVABLE_FORMULA], + petab_problem.observable_df[NOISE_FORMULA], + ): + obs_symbol = sp.sympify(observable_formula, locals=local_syms) + if observable_id in pysb_model.expressions.keys(): + obs_expr = pysb_model.expressions[observable_id] + else: + obs_expr = pysb.Expression(observable_id, obs_symbol) + 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) + pysb_model.add_component(sigma_expr) + local_syms[sigma_id] = sigma_expr + + +def _add_initialization_variables( + pysb_model: pysb.Model, petab_problem: petab.Problem +): + """Add initialization variables to the PySB model to support initial + conditions specified in the PEtab condition table. + + To parameterize initial states, we currently need initial assignments. + If they occur in the condition table, we create a new parameter + initial_${speciesID}. Feels dirty and should be changed (see also #924). + """ + + initial_states = get_states_in_condition_table(petab_problem) + fixed_parameters = [] + if initial_states: + # add preequilibration indicator variable + # NOTE: would only be required if we actually have preequilibration + # adding it anyways. can be optimized-out later + if PREEQ_INDICATOR_ID in [c.name for c in pysb_model.components]: + raise AssertionError( + "Model already has a component with ID " + f"{PREEQ_INDICATOR_ID}. Cannot handle " + "species and compartments in condition table " + "then." + ) + preeq_indicator = pysb.Parameter(PREEQ_INDICATOR_ID) + pysb_model.add_component(preeq_indicator) + # Can only reset parameters after preequilibration if they are fixed. + fixed_parameters.append(PREEQ_INDICATOR_ID) + logger.debug( + "Adding preequilibration indicator constant " + f"{PREEQ_INDICATOR_ID}" + ) + logger.debug(f"Adding initial assignments for {initial_states.keys()}") + + for assignee_id in initial_states: + init_par_id_preeq = f"initial_{assignee_id}_preeq" + init_par_id_sim = f"initial_{assignee_id}_sim" + for init_par_id in [init_par_id_preeq, init_par_id_sim]: + if init_par_id in [c.name for c in pysb_model.components]: + raise ValueError( + "Cannot create parameter for initial assignment " + f"for {assignee_id} because an entity named " + f"{init_par_id} exists already in the model." + ) + p = pysb.Parameter(init_par_id) + pysb_model.add_component(p) + + species_idx = int(re.match(r"__s(\d+)$", assignee_id)[1]) + # use original model here since that's what was used to generate + # the ids in initial_states + species_pattern = petab_problem.model.model.species[species_idx] + + # species pattern comes from the _original_ model, but we only want + # to modify pysb_model, so we have to reconstitute the pattern using + # pysb_model + for c in pysb_model.components: + globals()[c.name] = c + species_pattern = pysb.as_complex_pattern(eval(str(species_pattern))) + + from pysb.pattern import match_complex_pattern + + formula = pysb.Expression( + f"initial_{assignee_id}_formula", + preeq_indicator * pysb_model.parameters[init_par_id_preeq] + + (1 - preeq_indicator) * pysb_model.parameters[init_par_id_sim], + ) + pysb_model.add_component(formula) + + for initial in pysb_model.initials: + if match_complex_pattern( + initial.pattern, species_pattern, exact=True + ): + logger.debug( + "The PySB model has an initial defined for species " + f"{assignee_id}, but this species also has an initial " + "value defined in the PEtab condition table. The SBML " + "initial assignment will be overwritten to handle " + "preequilibration and initial values specified by the " + "PEtab problem." + ) + initial.value = formula + break + else: + # No initial in the pysb model, so add one + init = pysb.Initial(species_pattern, formula) + pysb_model.add_component(init) + + return fixed_parameters + + +@log_execution_time("Importing PEtab model", logger) +def import_model_pysb( + petab_problem: petab.Problem, + model_output_dir: Optional[Union[str, Path]] = None, + verbose: Optional[Union[bool, int]] = True, + model_name: Optional[str] = None, + **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 model_name: + Name of the generated model module + + :param kwargs: + Additional keyword arguments to be passed to + :meth:`amici.pysb_import.pysb2amici`. + """ + set_log_level(logger, verbose) + + logger.info("Importing model ...") + + if not isinstance(petab_problem.model, PySBModel): + raise ValueError("Not a PySB model") + + # need to create a copy here as we don't want to modify the original + pysb.SelfExporter.cleanup() + og_export = pysb.SelfExporter.do_export + pysb.SelfExporter.do_export = False + pysb_model = pysb.Model( + base=petab_problem.model.model, + name=petab_problem.model.model_id, + ) + + _add_observation_model(pysb_model, petab_problem) + # generate species for the _original_ model + pysb.bng.generate_equations(petab_problem.model.model) + fixed_parameters = _add_initialization_variables(pysb_model, petab_problem) + pysb.SelfExporter.do_export = og_export + + # check condition table for supported features, important to use pysb_model + # here, as we want to also cover output parameters + model_parameters = [p.name for p in pysb_model.parameters] + condition_species_parameters = get_states_in_condition_table( + petab_problem, return_patterns=True + ) + for x in petab_problem.condition_df.columns: + if x == CONDITION_NAME: + continue + + x = petab.mapping.resolve_mapping(petab_problem.mapping_df, x) + + # parameters + if x in model_parameters: + continue + + # species/pattern + if x in condition_species_parameters: + continue + + raise NotImplementedError( + "For PySB PEtab import, only model parameters and species, but " + "not compartments are allowed in the condition table. Offending " + f"column: {x}" + ) + + constant_parameters = ( + get_fixed_parameters(petab_problem) + fixed_parameters + ) + + if petab_problem.observable_df is None: + observables = None + sigmas = None + noise_distrs = None + else: + observables = [ + expr.name + for expr in pysb_model.expressions + if expr.name in petab_problem.observable_df.index + ] + + sigmas = {obs_id: f"{obs_id}_sigma" for obs_id in observables} + + noise_distrs = petab_noise_distributions_to_amici( + petab_problem.observable_df + ) + + from amici.pysb_import import pysb2amici + + pysb2amici( + model=pysb_model, + output_dir=model_output_dir, + model_name=model_name, + verbose=True, + observables=observables, + sigmas=sigmas, + constant_parameters=constant_parameters, + noise_distributions=noise_distrs, + **kwargs, + ) diff --git a/python/sdist/amici/petab/sbml_import.py b/python/sdist/amici/petab/sbml_import.py new file mode 100644 index 0000000000..935a529c9b --- /dev/null +++ b/python/sdist/amici/petab/sbml_import.py @@ -0,0 +1,553 @@ +import logging +import math +import os +import tempfile +from itertools import chain +from pathlib import Path +from typing import Optional, Union +from warnings import warn + +import amici +import libsbml +import pandas as pd +import petab +import sympy as sp +from _collections import OrderedDict +from amici.logging import log_execution_time, set_log_level +from petab.models import MODEL_TYPE_SBML +from sympy.abc import _clash + +from . import PREEQ_INDICATOR_ID +from .import_helpers import ( + check_model, + get_fixed_parameters, + get_observation_model, +) +from .util import get_states_in_condition_table + +logger = logging.getLogger(__name__) + + +@log_execution_time("Importing PEtab model", logger) +def import_model_sbml( + sbml_model: Union[str, Path, "libsbml.Model"] = None, + condition_table: Optional[Union[str, Path, pd.DataFrame]] = None, + observable_table: Optional[Union[str, Path, pd.DataFrame]] = None, + measurement_table: Optional[Union[str, Path, pd.DataFrame]] = None, + petab_problem: petab.Problem = None, + model_name: Optional[str] = None, + model_output_dir: Optional[Union[str, Path]] = None, + verbose: Optional[Union[bool, int]] = True, + allow_reinit_fixpar_initcond: bool = True, + validate: bool = True, + non_estimated_parameters_as_constants=True, + output_parameter_defaults: Optional[dict[str, float]] = None, + discard_sbml_annotations: bool = False, + **kwargs, +) -> amici.SbmlImporter: + """ + Create AMICI model from PEtab problem + + :param sbml_model: + PEtab SBML model or SBML file name. + Deprecated, pass ``petab_problem`` instead. + + :param condition_table: + PEtab condition table. If provided, parameters from there will be + turned into AMICI constant parameters (i.e. parameters w.r.t. which + no sensitivities will be computed). + Deprecated, pass ``petab_problem`` instead. + + :param observable_table: + PEtab observable table. Deprecated, pass ``petab_problem`` instead. + + :param measurement_table: + PEtab measurement table. Deprecated, pass ``petab_problem`` instead. + + :param petab_problem: + PEtab problem. + + :param model_name: + Name of the generated model. If model file name was provided, + this defaults to the file name without extension, otherwise + the SBML model ID will be used. + + :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 allow_reinit_fixpar_initcond: + See :class:`amici.de_export.ODEExporter`. Must be enabled if initial + states are to be reset after preequilibration. + + :param validate: + Whether to validate the PEtab problem + + :param non_estimated_parameters_as_constants: + Whether parameters marked as non-estimated in PEtab should be + considered constant in AMICI. Setting this to ``True`` will reduce + model size and simulation times. If sensitivities with respect to those + parameters are required, this should be set to ``False``. + + :param output_parameter_defaults: + Optional default parameter values for output parameters introduced in + the PEtab observables table, in particular for placeholder parameters. + dictionary mapping parameter IDs to default values. + + :param discard_sbml_annotations: + Discard information contained in AMICI SBML annotations (debug). + + :param kwargs: + Additional keyword arguments to be passed to + :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. + + :return: + The created :class:`amici.sbml_import.SbmlImporter` instance. + """ + from petab.models.sbml_model import SbmlModel + + set_log_level(logger, verbose) + + logger.info("Importing model ...") + + if any([sbml_model, condition_table, observable_table, measurement_table]): + warn( + "The `sbml_model`, `condition_table`, `observable_table`, and " + "`measurement_table` arguments are deprecated and will be " + "removed in a future version. Use `petab_problem` instead.", + DeprecationWarning, + stacklevel=2, + ) + if petab_problem: + raise ValueError( + "Must not pass a `petab_problem` argument in " + "combination with any of `sbml_model`, " + "`condition_table`, `observable_table`, or " + "`measurement_table`." + ) + + petab_problem = petab.Problem( + model=SbmlModel(sbml_model) + if isinstance(sbml_model, libsbml.Model) + else SbmlModel.from_file(sbml_model), + condition_df=petab.get_condition_df(condition_table), + observable_df=petab.get_observable_df(observable_table), + ) + + if petab_problem.observable_df is None: + raise NotImplementedError( + "PEtab import without observables table " + "is currently not supported." + ) + + assert isinstance(petab_problem.model, SbmlModel) + + if validate: + logger.info("Validating PEtab problem ...") + petab.lint_problem(petab_problem) + + # Model name from SBML ID or filename + if model_name is None: + if not (model_name := petab_problem.model.sbml_model.getId()): + if not isinstance(sbml_model, (str, Path)): + raise ValueError( + "No `model_name` was provided and no model " + "ID was specified in the SBML model." + ) + model_name = os.path.splitext(os.path.split(sbml_model)[-1])[0] + + if model_output_dir is None: + model_output_dir = os.path.join( + os.getcwd(), f"{model_name}-amici{amici.__version__}" + ) + + logger.info( + f"Model name is '{model_name}'.\n" + f"Writing model code to '{model_output_dir}'." + ) + + # Create a copy, because it will be modified by SbmlImporter + sbml_doc = petab_problem.model.sbml_model.getSBMLDocument().clone() + sbml_model = sbml_doc.getModel() + + show_model_info(sbml_model) + + sbml_importer = amici.SbmlImporter( + sbml_model, + discard_annotations=discard_sbml_annotations, + ) + sbml_model = sbml_importer.sbml + + allow_n_noise_pars = ( + not petab.lint.observable_table_has_nontrivial_noise_formula( + petab_problem.observable_df + ) + ) + if ( + petab_problem.measurement_df is not None + and petab.lint.measurement_table_has_timepoint_specific_mappings( + petab_problem.measurement_df, + allow_scalar_numeric_noise_parameters=allow_n_noise_pars, + ) + ): + raise ValueError( + "AMICI does not support importing models with timepoint specific " + "mappings for noise or observable parameters. Please flatten " + "the problem and try again." + ) + + if petab_problem.observable_df is not None: + observables, noise_distrs, sigmas = get_observation_model( + petab_problem.observable_df + ) + else: + observables = noise_distrs = sigmas = None + + logger.info(f"Observables: {len(observables)}") + logger.info(f"Sigmas: {len(sigmas)}") + + if len(sigmas) != len(observables): + raise AssertionError( + f"Number of provided observables ({len(observables)}) and sigmas " + f"({len(sigmas)}) do not match." + ) + + # TODO: adding extra output parameters is currently not supported, + # so we add any output parameters to the SBML model. + # this should be changed to something more elegant + # + formulas = chain( + (val["formula"] for val in observables.values()), sigmas.values() + ) + output_parameters = OrderedDict() + for formula in formulas: + # we want reproducible parameter ordering upon repeated import + free_syms = sorted( + sp.sympify(formula, locals=_clash).free_symbols, + 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" + and sym not in observables + ): + output_parameters[sym] = None + logger.debug( + "Adding output parameters to model: " + f"{list(output_parameters.keys())}" + ) + output_parameter_defaults = output_parameter_defaults or {} + if extra_pars := ( + set(output_parameter_defaults) - set(output_parameters.keys()) + ): + raise ValueError( + f"Default output parameter values were given for {extra_pars}, " + "but they those are not output parameters." + ) + + for par in output_parameters.keys(): + _add_global_parameter( + sbml_model=sbml_model, + parameter_id=par, + value=output_parameter_defaults.get(par, 0.0), + ) + # + + # TODO: to parameterize initial states or compartment sizes, we currently + # need initial assignments. if they occur in the condition table, we + # create a new parameter initial_${speciesOrCompartmentID}. + # feels dirty and should be changed (see also #924) + # + + initial_states = get_states_in_condition_table(petab_problem) + fixed_parameters = [] + if initial_states: + # add preequilibration indicator variable + # NOTE: would only be required if we actually have preequilibration + # adding it anyways. can be optimized-out later + if sbml_model.getParameter(PREEQ_INDICATOR_ID) is not None: + raise AssertionError( + "Model already has a parameter with ID " + f"{PREEQ_INDICATOR_ID}. Cannot handle " + "species and compartments in condition table " + "then." + ) + indicator = sbml_model.createParameter() + indicator.setId(PREEQ_INDICATOR_ID) + indicator.setName(PREEQ_INDICATOR_ID) + # Can only reset parameters after preequilibration if they are fixed. + fixed_parameters.append(PREEQ_INDICATOR_ID) + logger.debug( + "Adding preequilibration indicator " + f"constant {PREEQ_INDICATOR_ID}" + ) + logger.debug(f"Adding initial assignments for {initial_states.keys()}") + for assignee_id in initial_states: + init_par_id_preeq = f"initial_{assignee_id}_preeq" + init_par_id_sim = f"initial_{assignee_id}_sim" + for init_par_id in [init_par_id_preeq, init_par_id_sim]: + if sbml_model.getElementBySId(init_par_id) is not None: + raise ValueError( + "Cannot create parameter for initial assignment " + f"for {assignee_id} because an entity named " + f"{init_par_id} exists already in the model." + ) + init_par = sbml_model.createParameter() + init_par.setId(init_par_id) + init_par.setName(init_par_id) + assignment = sbml_model.getInitialAssignment(assignee_id) + if assignment is None: + assignment = sbml_model.createInitialAssignment() + assignment.setSymbol(assignee_id) + else: + logger.debug( + "The SBML model has an initial assignment defined " + f"for model entity {assignee_id}, but this entity " + "also has an initial value defined in the PEtab " + "condition table. The SBML initial assignment will " + "be overwritten to handle preequilibration and " + "initial values specified by the PEtab problem." + ) + formula = ( + f"{PREEQ_INDICATOR_ID} * {init_par_id_preeq} " + f"+ (1 - {PREEQ_INDICATOR_ID}) * {init_par_id_sim}" + ) + math_ast = libsbml.parseL3Formula(formula) + assignment.setMath(math_ast) + # + + fixed_parameters.extend( + get_fixed_parameters( + petab_problem=petab_problem, + non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, + ) + ) + + logger.debug(f"Fixed parameters are {fixed_parameters}") + logger.info(f"Overall fixed parameters: {len(fixed_parameters)}") + logger.info( + "Variable parameters: " + + str(len(sbml_model.getListOfParameters()) - len(fixed_parameters)) + ) + + # Create Python module from SBML model + sbml_importer.sbml2amici( + model_name=model_name, + output_dir=model_output_dir, + observables=observables, + constant_parameters=fixed_parameters, + sigmas=sigmas, + allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, + noise_distributions=noise_distrs, + verbose=verbose, + **kwargs, + ) + + if kwargs.get( + "compile", + amici._get_default_argument(sbml_importer.sbml2amici, "compile"), + ): + # check that the model extension was compiled successfully + model_module = amici.import_model_module(model_name, model_output_dir) + model = model_module.getModel() + check_model(amici_model=model, petab_problem=petab_problem) + + return sbml_importer + + +def show_model_info(sbml_model: "libsbml.Model"): + """Log some model quantities""" + + logger.info(f"Species: {len(sbml_model.getListOfSpecies())}") + logger.info( + "Global parameters: " + str(len(sbml_model.getListOfParameters())) + ) + logger.info(f"Reactions: {len(sbml_model.getListOfReactions())}") + + +# TODO - remove?! +def species_to_parameters( + species_ids: list[str], sbml_model: "libsbml.Model" +) -> list[str]: + """ + Turn a SBML species into parameters and replace species references + inside the model instance. + + :param species_ids: + list of SBML species ID to convert to parameters with the same ID as + the replaced species. + + :param sbml_model: + SBML model to modify + + :return: + list of IDs of species which have been converted to parameters + """ + transformables = [] + + for species_id in species_ids: + species = sbml_model.getSpecies(species_id) + + if species.getHasOnlySubstanceUnits(): + logger.warning( + f"Ignoring {species.getId()} which has only substance units." + " Conversion not yet implemented." + ) + continue + + if math.isnan(species.getInitialConcentration()): + logger.warning( + f"Ignoring {species.getId()} which has no initial " + "concentration. Amount conversion not yet implemented." + ) + continue + + transformables.append(species_id) + + # Must not remove species while iterating over getListOfSpecies() + for species_id in transformables: + species = sbml_model.removeSpecies(species_id) + par = sbml_model.createParameter() + par.setId(species.getId()) + par.setName(species.getName()) + par.setConstant(True) + par.setValue(species.getInitialConcentration()) + par.setUnits(species.getUnits()) + + # Remove from reactants and products + for reaction in sbml_model.getListOfReactions(): + for species_id in transformables: + # loop, since removeX only removes one instance + while reaction.removeReactant(species_id): + # remove from reactants + pass + while reaction.removeProduct(species_id): + # remove from products + pass + while reaction.removeModifier(species_id): + # remove from modifiers + pass + + return transformables + + +def _add_global_parameter( + sbml_model: libsbml.Model, + parameter_id: str, + parameter_name: str = None, + constant: bool = False, + units: str = "dimensionless", + value: float = 0.0, +) -> libsbml.Parameter: + """Add new global parameter to SBML model + + Arguments: + sbml_model: SBML model + parameter_id: ID of the new parameter + parameter_name: Name of the new parameter + constant: Is parameter constant? + units: SBML unit ID + value: parameter value + + Returns: + The created parameter + """ + if parameter_name is None: + parameter_name = parameter_id + + p = sbml_model.createParameter() + p.setId(parameter_id) + p.setName(parameter_name) + p.setConstant(constant) + p.setValue(value) + p.setUnits(units) + return p + + +def _get_fixed_parameters_sbml( + petab_problem: petab.Problem, + non_estimated_parameters_as_constants=True, +) -> list[str]: + """ + Determine, set and return fixed model parameters. + + Non-estimated parameters and parameters specified in the condition table + are turned into constants (unless they are overridden). + Only global SBML parameters are considered. Local parameters are ignored. + + :param petab_problem: + The PEtab problem instance + + :param non_estimated_parameters_as_constants: + Whether parameters marked as non-estimated in PEtab should be + considered constant in AMICI. Setting this to ``True`` will reduce + model size and simulation times. If sensitivities with respect to those + parameters are required, this should be set to ``False``. + + :return: + list of IDs of parameters which are to be considered constant. + """ + if not petab_problem.model.type_id == MODEL_TYPE_SBML: + raise ValueError("Not an SBML model.") + # initial concentrations for species or initial compartment sizes in + # condition table will need to be turned into fixed parameters + + # if there is no initial assignment for that species, we'd need + # to create one. to avoid any naming collision right away, we don't + # allow that for now + + # we can't handle them yet + compartments = [ + col + for col in petab_problem.condition_df + if petab_problem.model.sbml_model.getCompartment(col) is not None + ] + if compartments: + raise NotImplementedError( + "Can't handle initial compartment sizes " + "at the moment. Consider creating an " + f"initial assignment for {compartments}" + ) + + fixed_parameters = get_fixed_parameters( + petab_problem, non_estimated_parameters_as_constants + ) + + # exclude targets of rules or initial assignments + sbml_model = petab_problem.model.sbml_model + for fixed_parameter in fixed_parameters.copy(): + # check global parameters + if sbml_model.getInitialAssignmentBySymbol( + fixed_parameter + ) or sbml_model.getRuleByVariable(fixed_parameter): + fixed_parameters.remove(fixed_parameter) + + return list(sorted(fixed_parameters)) + + +def _create_model_output_dir_name( + sbml_model: "libsbml.Model", model_name: Optional[str] = None +) -> Path: + """ + Find a folder for storing the compiled amici model. + If possible, use the sbml model id, otherwise create a random folder. + The folder will be located in the `amici_models` subfolder of the current + folder. + """ + BASE_DIR = Path("amici_models").absolute() + BASE_DIR.mkdir(exist_ok=True) + # try model_name + if model_name: + return BASE_DIR / model_name + + # try sbml model id + if sbml_model_id := sbml_model.getId(): + return BASE_DIR / sbml_model_id + + # create random folder name + return Path(tempfile.mkdtemp(dir=BASE_DIR)) diff --git a/python/sdist/amici/petab_import.py b/python/sdist/amici/petab_import.py index a33b419345..1edc82a221 100644 --- a/python/sdist/amici/petab_import.py +++ b/python/sdist/amici/petab_import.py @@ -4,895 +4,25 @@ Import a model in the :mod:`petab` (https://github.com/PEtab-dev/PEtab) format into AMICI. """ -import importlib -import logging -import math -import os -import re -import shutil -import tempfile -from itertools import chain -from pathlib import Path -from typing import Dict, List, Optional, Tuple, Union -from warnings import warn - -import amici -import libsbml -import pandas as pd -import petab -import sympy as sp -from _collections import OrderedDict -from amici.logging import get_logger, log_execution_time, set_log_level -from petab.C import * -from petab.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML -from petab.parameters import get_valid_parameters_for_parameter_table -from sympy.abc import _clash - -from .petab import PREEQ_INDICATOR_ID -from .petab.util import get_states_in_condition_table - -try: - from amici.petab_import_pysb import import_model_pysb -except ModuleNotFoundError: - # pysb not available - import_model_pysb = None - -logger = get_logger(__name__, logging.WARNING) - - -def _add_global_parameter( - sbml_model: libsbml.Model, - parameter_id: str, - parameter_name: str = None, - constant: bool = False, - units: str = "dimensionless", - value: float = 0.0, -) -> libsbml.Parameter: - """Add new global parameter to SBML model - - Arguments: - sbml_model: SBML model - parameter_id: ID of the new parameter - parameter_name: Name of the new parameter - constant: Is parameter constant? - units: SBML unit ID - value: parameter value - - Returns: - The created parameter - """ - if parameter_name is None: - parameter_name = parameter_id - - p = sbml_model.createParameter() - p.setId(parameter_id) - p.setName(parameter_name) - p.setConstant(constant) - p.setValue(value) - p.setUnits(units) - return p - - -def get_fixed_parameters( - petab_problem: petab.Problem, - non_estimated_parameters_as_constants=True, -) -> List[str]: - """ - Determine, set and return fixed model parameters. - - Non-estimated parameters and parameters specified in the condition table - are turned into constants (unless they are overridden). - Only global SBML parameters are considered. Local parameters are ignored. - - :param petab_problem: - The PEtab problem instance - - :param non_estimated_parameters_as_constants: - Whether parameters marked as non-estimated in PEtab should be - considered constant in AMICI. Setting this to ``True`` will reduce - model size and simulation times. If sensitivities with respect to those - parameters are required, this should be set to ``False``. - - :return: - List of IDs of parameters which are to be considered constant. - """ - if petab_problem.model.type_id == MODEL_TYPE_SBML: - # initial concentrations for species or initial compartment sizes in - # condition table will need to be turned into fixed parameters - - # if there is no initial assignment for that species, we'd need - # to create one. to avoid any naming collision right away, we don't - # allow that for now - - # we can't handle them yet - compartments = [ - col - for col in petab_problem.condition_df - if petab_problem.model.sbml_model.getCompartment(col) is not None - ] - if compartments: - raise NotImplementedError( - "Can't handle initial compartment sizes " - "at the moment. Consider creating an " - f"initial assignment for {compartments}" - ) - - # if we have a parameter table, all parameters that are allowed to be - # listed in the parameter table, but are not marked as estimated, can be - # turned into AMICI constants - # due to legacy API, we might not always have a parameter table, though - fixed_parameters = set() - if petab_problem.parameter_df is not None: - all_parameters = get_valid_parameters_for_parameter_table( - model=petab_problem.model, - condition_df=petab_problem.condition_df, - observable_df=petab_problem.observable_df - if petab_problem.observable_df is not None - else pd.DataFrame(columns=petab.OBSERVABLE_DF_REQUIRED_COLS), - measurement_df=petab_problem.measurement_df - if petab_problem.measurement_df is not None - else pd.DataFrame(columns=petab.MEASUREMENT_DF_REQUIRED_COLS), - ) - if non_estimated_parameters_as_constants: - estimated_parameters = petab_problem.parameter_df.index.values[ - petab_problem.parameter_df[ESTIMATE] == 1 - ] - else: - # don't treat parameter table parameters as constants - estimated_parameters = petab_problem.parameter_df.index.values - fixed_parameters = set(all_parameters) - set(estimated_parameters) - - # Column names are model parameter IDs, compartment IDs or species IDs. - # Thereof, all parameters except for any overridden ones should be made - # constant. - # (Could potentially still be made constant, but leaving them might - # increase model reusability) - - # handle parameters in condition table - condition_df = petab_problem.condition_df - if condition_df is not None: - logger.debug(f"Condition table: {condition_df.shape}") - - # remove overridden parameters (`object`-type columns) - fixed_parameters.update( - p - for p in condition_df.columns - # get rid of conditionName column - if p != CONDITION_NAME - # there is no parametric override - # TODO: could check if the final overriding parameter is estimated - # or not, but for now, we skip the parameter if there is any kind - # of overriding - if condition_df[p].dtype != "O" - # p is a parameter - and not petab_problem.model.is_state_variable(p) - ) - - # Ensure mentioned parameters exist in the model. Remove additional ones - # from list - for fixed_parameter in fixed_parameters.copy(): - # check global parameters - if not petab_problem.model.has_entity_with_id(fixed_parameter): - # TODO: could still exist as an output parameter? - logger.warning( - f"Column '{fixed_parameter}' used in condition " - "table but not entity with the corresponding ID " - "exists. Ignoring." - ) - fixed_parameters.remove(fixed_parameter) - - if petab_problem.model.type_id == MODEL_TYPE_SBML: - # exclude targets of rules or initial assignments - sbml_model = petab_problem.model.sbml_model - for fixed_parameter in fixed_parameters.copy(): - # check global parameters - if sbml_model.getInitialAssignmentBySymbol( - fixed_parameter - ) or sbml_model.getRuleByVariable(fixed_parameter): - fixed_parameters.remove(fixed_parameter) - - return list(sorted(fixed_parameters)) - - -def species_to_parameters( - species_ids: List[str], sbml_model: "libsbml.Model" -) -> List[str]: - """ - Turn a SBML species into parameters and replace species references - inside the model instance. - - :param species_ids: - List of SBML species ID to convert to parameters with the same ID as - the replaced species. - - :param sbml_model: - SBML model to modify - - :return: - List of IDs of species which have been converted to parameters - """ - transformables = [] - - for species_id in species_ids: - species = sbml_model.getSpecies(species_id) - - if species.getHasOnlySubstanceUnits(): - logger.warning( - f"Ignoring {species.getId()} which has only substance units." - " Conversion not yet implemented." - ) - continue - - if math.isnan(species.getInitialConcentration()): - logger.warning( - f"Ignoring {species.getId()} which has no initial " - "concentration. Amount conversion not yet implemented." - ) - continue - - transformables.append(species_id) - - # Must not remove species while iterating over getListOfSpecies() - for species_id in transformables: - species = sbml_model.removeSpecies(species_id) - par = sbml_model.createParameter() - par.setId(species.getId()) - par.setName(species.getName()) - par.setConstant(True) - par.setValue(species.getInitialConcentration()) - par.setUnits(species.getUnits()) - - # Remove from reactants and products - for reaction in sbml_model.getListOfReactions(): - for species_id in transformables: - # loop, since removeX only removes one instance - while reaction.removeReactant(species_id): - # remove from reactants - pass - while reaction.removeProduct(species_id): - # remove from products - pass - while reaction.removeModifier(species_id): - # remove from modifiers - pass - - return transformables - - -def import_petab_problem( - petab_problem: petab.Problem, - model_output_dir: Union[str, Path, None] = None, - model_name: str = None, - force_compile: bool = False, - non_estimated_parameters_as_constants=True, - **kwargs, -) -> "amici.Model": - """ - Import model from petab problem. - - :param petab_problem: - A petab problem containing all relevant information on the model. - - :param model_output_dir: - Directory to write the model code to. Will be created if doesn't - exist. Defaults to current directory. - - :param model_name: - Name of the generated model. If model file name was provided, - this defaults to the file name without extension, otherwise - the model ID will be used. - - :param force_compile: - Whether to compile the model even if the target folder is not empty, - or the model exists already. - - :param non_estimated_parameters_as_constants: - Whether parameters marked as non-estimated in PEtab should be - considered constant in AMICI. Setting this to ``True`` will reduce - model size and simulation times. If sensitivities with respect to those - parameters are required, this should be set to ``False``. - - :param kwargs: - Additional keyword arguments to be passed to - :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. - - :return: - The imported model. - """ - if petab_problem.model.type_id not in (MODEL_TYPE_SBML, MODEL_TYPE_PYSB): - raise NotImplementedError( - "Unsupported model type " + petab_problem.model.type_id - ) - - if petab_problem.mapping_df is not None: - # It's partially supported. Remove at your own risk... - raise NotImplementedError( - "PEtab v2.0.0 mapping tables are not yet supported." - ) - - model_name = model_name or petab_problem.model.model_id - - if petab_problem.model.type_id == MODEL_TYPE_PYSB and model_name is None: - model_name = petab_problem.pysb_model.name - elif model_name is None and model_output_dir: - model_name = _create_model_name(model_output_dir) - - # generate folder and model name if necessary - if model_output_dir is None: - if petab_problem.model.type_id == MODEL_TYPE_PYSB: - raise ValueError("Parameter `model_output_dir` is required.") - - model_output_dir = _create_model_output_dir_name( - petab_problem.sbml_model, model_name - ) - else: - model_output_dir = os.path.abspath(model_output_dir) - - # create folder - if not os.path.exists(model_output_dir): - os.makedirs(model_output_dir) - - # check if compilation necessary - if force_compile or not _can_import_model(model_name, model_output_dir): - # check if folder exists - if os.listdir(model_output_dir) and not force_compile: - raise ValueError( - f"Cannot compile to {model_output_dir}: not empty. " - "Please assign a different target or set `force_compile`." - ) - - # remove folder if exists - if os.path.exists(model_output_dir): - shutil.rmtree(model_output_dir) - - logger.info(f"Compiling model {model_name} to {model_output_dir}.") - # compile the model - if petab_problem.model.type_id == MODEL_TYPE_PYSB: - import_model_pysb( - petab_problem, - model_name=model_name, - model_output_dir=model_output_dir, - **kwargs, - ) - else: - import_model_sbml( - petab_problem=petab_problem, - model_name=model_name, - model_output_dir=model_output_dir, - non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, - **kwargs, - ) - - # import model - model_module = amici.import_model_module(model_name, model_output_dir) - model = model_module.getModel() - check_model(amici_model=model, petab_problem=petab_problem) - - logger.info( - f"Successfully loaded model {model_name} " f"from {model_output_dir}." - ) - - return model - - -def check_model( - amici_model: amici.Model, - petab_problem: petab.Problem, -) -> None: - """Check that the model is consistent with the PEtab problem.""" - if petab_problem.parameter_df is None: - return - - amici_ids_free = set(amici_model.getParameterIds()) - amici_ids = amici_ids_free | set(amici_model.getFixedParameterIds()) - - petab_ids_free = set( - petab_problem.parameter_df.loc[ - petab_problem.parameter_df[ESTIMATE] == 1 - ].index - ) - - amici_ids_free_required = petab_ids_free.intersection(amici_ids) - - if not amici_ids_free_required.issubset(amici_ids_free): - raise ValueError( - "The available AMICI model does not support estimating the " - "following parameters. Please recompile the model and ensure " - "that these parameters are not treated as constants. Deleting " - "the current model might also resolve this. Parameters: " - f"{amici_ids_free_required.difference(amici_ids_free)}" - ) - - -def _create_model_output_dir_name( - sbml_model: "libsbml.Model", model_name: Optional[str] = None -) -> Path: - """ - Find a folder for storing the compiled amici model. - If possible, use the sbml model id, otherwise create a random folder. - The folder will be located in the `amici_models` subfolder of the current - folder. - """ - BASE_DIR = Path("amici_models").absolute() - BASE_DIR.mkdir(exist_ok=True) - # try model_name - if model_name: - return BASE_DIR / model_name - - # try sbml model id - if sbml_model_id := sbml_model.getId(): - return BASE_DIR / sbml_model_id - - # create random folder name - return Path(tempfile.mkdtemp(dir=BASE_DIR)) - - -def _create_model_name(folder: Union[str, Path]) -> str: - """ - Create a name for the model. - Just re-use the last part of the folder. - """ - return os.path.split(os.path.normpath(folder))[-1] - - -def _can_import_model( - model_name: str, model_output_dir: Union[str, Path] -) -> bool: - """ - Check whether a module of that name can already be imported. - """ - # try to import (in particular checks version) - try: - with amici.add_path(model_output_dir): - model_module = importlib.import_module(model_name) - except ModuleNotFoundError: - return False - - # no need to (re-)compile - return hasattr(model_module, "getModel") - - -@log_execution_time("Importing PEtab model", logger) -def import_model_sbml( - sbml_model: Union[str, Path, "libsbml.Model"] = None, - condition_table: Optional[Union[str, Path, pd.DataFrame]] = None, - observable_table: Optional[Union[str, Path, pd.DataFrame]] = None, - measurement_table: Optional[Union[str, Path, pd.DataFrame]] = None, - petab_problem: petab.Problem = None, - model_name: Optional[str] = None, - model_output_dir: Optional[Union[str, Path]] = None, - verbose: Optional[Union[bool, int]] = True, - allow_reinit_fixpar_initcond: bool = True, - validate: bool = True, - non_estimated_parameters_as_constants=True, - output_parameter_defaults: Optional[Dict[str, float]] = None, - discard_sbml_annotations: bool = False, - **kwargs, -) -> amici.SbmlImporter: - """ - Create AMICI model from PEtab problem - - :param sbml_model: - PEtab SBML model or SBML file name. - Deprecated, pass ``petab_problem`` instead. - - :param condition_table: - PEtab condition table. If provided, parameters from there will be - turned into AMICI constant parameters (i.e. parameters w.r.t. which - no sensitivities will be computed). - Deprecated, pass ``petab_problem`` instead. - - :param observable_table: - PEtab observable table. Deprecated, pass ``petab_problem`` instead. - - :param measurement_table: - PEtab measurement table. Deprecated, pass ``petab_problem`` instead. - - :param petab_problem: - PEtab problem. - - :param model_name: - Name of the generated model. If model file name was provided, - this defaults to the file name without extension, otherwise - the SBML model ID will be used. - - :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 allow_reinit_fixpar_initcond: - See :class:`amici.de_export.ODEExporter`. Must be enabled if initial - states are to be reset after preequilibration. - - :param validate: - Whether to validate the PEtab problem - - :param non_estimated_parameters_as_constants: - Whether parameters marked as non-estimated in PEtab should be - considered constant in AMICI. Setting this to ``True`` will reduce - model size and simulation times. If sensitivities with respect to those - parameters are required, this should be set to ``False``. - - :param output_parameter_defaults: - Optional default parameter values for output parameters introduced in - the PEtab observables table, in particular for placeholder parameters. - Dictionary mapping parameter IDs to default values. - - :param discard_sbml_annotations: - Discard information contained in AMICI SBML annotations (debug). - - :param kwargs: - Additional keyword arguments to be passed to - :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. - - :return: - The created :class:`amici.sbml_import.SbmlImporter` instance. - """ - from petab.models.sbml_model import SbmlModel - - set_log_level(logger, verbose) - - logger.info("Importing model ...") - - if any([sbml_model, condition_table, observable_table, measurement_table]): - warn( - "The `sbml_model`, `condition_table`, `observable_table`, and " - "`measurement_table` arguments are deprecated and will be " - "removed in a future version. Use `petab_problem` instead.", - DeprecationWarning, - stacklevel=2, - ) - if petab_problem: - raise ValueError( - "Must not pass a `petab_problem` argument in " - "combination with any of `sbml_model`, " - "`condition_table`, `observable_table`, or " - "`measurement_table`." - ) - - petab_problem = petab.Problem( - model=SbmlModel(sbml_model) - if isinstance(sbml_model, libsbml.Model) - else SbmlModel.from_file(sbml_model), - condition_df=petab.get_condition_df(condition_table), - observable_df=petab.get_observable_df(observable_table), - ) - - if petab_problem.observable_df is None: - raise NotImplementedError( - "PEtab import without observables table " - "is currently not supported." - ) - - assert isinstance(petab_problem.model, SbmlModel) - - if validate: - logger.info("Validating PEtab problem ...") - petab.lint_problem(petab_problem) - - # Model name from SBML ID or filename - if model_name is None: - if not (model_name := petab_problem.model.sbml_model.getId()): - if not isinstance(sbml_model, (str, Path)): - raise ValueError( - "No `model_name` was provided and no model " - "ID was specified in the SBML model." - ) - model_name = os.path.splitext(os.path.split(sbml_model)[-1])[0] - - if model_output_dir is None: - model_output_dir = os.path.join( - os.getcwd(), f"{model_name}-amici{amici.__version__}" - ) - - logger.info( - f"Model name is '{model_name}'.\n" - f"Writing model code to '{model_output_dir}'." - ) - - # Create a copy, because it will be modified by SbmlImporter - sbml_doc = petab_problem.model.sbml_model.getSBMLDocument().clone() - sbml_model = sbml_doc.getModel() - - show_model_info(sbml_model) - - sbml_importer = amici.SbmlImporter( - sbml_model, - discard_annotations=discard_sbml_annotations, - ) - sbml_model = sbml_importer.sbml - - allow_n_noise_pars = ( - not petab.lint.observable_table_has_nontrivial_noise_formula( - petab_problem.observable_df - ) - ) - if ( - petab_problem.measurement_df is not None - and petab.lint.measurement_table_has_timepoint_specific_mappings( - petab_problem.measurement_df, - allow_scalar_numeric_noise_parameters=allow_n_noise_pars, - ) - ): - raise ValueError( - "AMICI does not support importing models with timepoint specific " - "mappings for noise or observable parameters. Please flatten " - "the problem and try again." - ) - - if petab_problem.observable_df is not None: - observables, noise_distrs, sigmas = get_observation_model( - petab_problem.observable_df - ) - else: - observables = noise_distrs = sigmas = None - - logger.info(f"Observables: {len(observables)}") - logger.info(f"Sigmas: {len(sigmas)}") - - if len(sigmas) != len(observables): - raise AssertionError( - f"Number of provided observables ({len(observables)}) and sigmas " - f"({len(sigmas)}) do not match." - ) - - # TODO: adding extra output parameters is currently not supported, - # so we add any output parameters to the SBML model. - # this should be changed to something more elegant - # - formulas = chain( - (val["formula"] for val in observables.values()), sigmas.values() - ) - output_parameters = OrderedDict() - for formula in formulas: - # we want reproducible parameter ordering upon repeated import - free_syms = sorted( - sp.sympify(formula, locals=_clash).free_symbols, - 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" - and sym not in observables - ): - output_parameters[sym] = None - logger.debug( - "Adding output parameters to model: " - f"{list(output_parameters.keys())}" - ) - output_parameter_defaults = output_parameter_defaults or {} - if extra_pars := ( - set(output_parameter_defaults) - set(output_parameters.keys()) - ): - raise ValueError( - f"Default output parameter values were given for {extra_pars}, " - "but they those are not output parameters." - ) - - for par in output_parameters.keys(): - _add_global_parameter( - sbml_model=sbml_model, - parameter_id=par, - value=output_parameter_defaults.get(par, 0.0), - ) - # - - # TODO: to parameterize initial states or compartment sizes, we currently - # need initial assignments. if they occur in the condition table, we - # create a new parameter initial_${speciesOrCompartmentID}. - # feels dirty and should be changed (see also #924) - # - - initial_states = get_states_in_condition_table(petab_problem) - fixed_parameters = [] - if initial_states: - # add preequilibration indicator variable - # NOTE: would only be required if we actually have preequilibration - # adding it anyways. can be optimized-out later - if sbml_model.getParameter(PREEQ_INDICATOR_ID) is not None: - raise AssertionError( - "Model already has a parameter with ID " - f"{PREEQ_INDICATOR_ID}. Cannot handle " - "species and compartments in condition table " - "then." - ) - indicator = sbml_model.createParameter() - indicator.setId(PREEQ_INDICATOR_ID) - indicator.setName(PREEQ_INDICATOR_ID) - # Can only reset parameters after preequilibration if they are fixed. - fixed_parameters.append(PREEQ_INDICATOR_ID) - logger.debug( - "Adding preequilibration indicator " - f"constant {PREEQ_INDICATOR_ID}" - ) - logger.debug(f"Adding initial assignments for {initial_states.keys()}") - for assignee_id in initial_states: - init_par_id_preeq = f"initial_{assignee_id}_preeq" - init_par_id_sim = f"initial_{assignee_id}_sim" - for init_par_id in [init_par_id_preeq, init_par_id_sim]: - if sbml_model.getElementBySId(init_par_id) is not None: - raise ValueError( - "Cannot create parameter for initial assignment " - f"for {assignee_id} because an entity named " - f"{init_par_id} exists already in the model." - ) - init_par = sbml_model.createParameter() - init_par.setId(init_par_id) - init_par.setName(init_par_id) - assignment = sbml_model.getInitialAssignment(assignee_id) - if assignment is None: - assignment = sbml_model.createInitialAssignment() - assignment.setSymbol(assignee_id) - else: - logger.debug( - "The SBML model has an initial assignment defined " - f"for model entity {assignee_id}, but this entity " - "also has an initial value defined in the PEtab " - "condition table. The SBML initial assignment will " - "be overwritten to handle preequilibration and " - "initial values specified by the PEtab problem." - ) - formula = ( - f"{PREEQ_INDICATOR_ID} * {init_par_id_preeq} " - f"+ (1 - {PREEQ_INDICATOR_ID}) * {init_par_id_sim}" - ) - math_ast = libsbml.parseL3Formula(formula) - assignment.setMath(math_ast) - # - - fixed_parameters.extend( - get_fixed_parameters( - petab_problem=petab_problem, - non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, - ) - ) - - logger.debug(f"Fixed parameters are {fixed_parameters}") - logger.info(f"Overall fixed parameters: {len(fixed_parameters)}") - logger.info( - "Variable parameters: " - + str(len(sbml_model.getListOfParameters()) - len(fixed_parameters)) - ) - - # Create Python module from SBML model - sbml_importer.sbml2amici( - model_name=model_name, - output_dir=model_output_dir, - observables=observables, - constant_parameters=fixed_parameters, - sigmas=sigmas, - allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, - noise_distributions=noise_distrs, - verbose=verbose, - **kwargs, - ) - - if kwargs.get( - "compile", - amici._get_default_argument(sbml_importer.sbml2amici, "compile"), - ): - # check that the model extension was compiled successfully - model_module = amici.import_model_module(model_name, model_output_dir) - model = model_module.getModel() - check_model(amici_model=model, petab_problem=petab_problem) - - return sbml_importer - - -# for backwards compatibility -import_model = import_model_sbml - - -def get_observation_model( - observable_df: pd.DataFrame, -) -> Tuple[ - Dict[str, Dict[str, str]], Dict[str, str], Dict[str, Union[str, float]] -]: - """ - Get observables, sigmas, and noise distributions from PEtab observation - table in a format suitable for - :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. - - :param observable_df: - PEtab observables table - - :return: - Tuple of dicts with observables, noise distributions, and sigmas. - """ - if observable_df is None: - return {}, {}, {} - - observables = {} - sigmas = {} - - nan_pat = r"^[nN]a[nN]$" - for _, observable in observable_df.iterrows(): - oid = str(observable.name) - # need to sanitize due to https://github.com/PEtab-dev/PEtab/issues/447 - name = re.sub(nan_pat, "", str(observable.get(OBSERVABLE_NAME, ""))) - formula_obs = re.sub(nan_pat, "", str(observable[OBSERVABLE_FORMULA])) - formula_noise = re.sub(nan_pat, "", str(observable[NOISE_FORMULA])) - observables[oid] = {"name": name, "formula": formula_obs} - sigmas[oid] = formula_noise - - # PEtab does currently not allow observables in noiseFormula and AMICI - # cannot handle states in sigma expressions. Therefore, where possible, - # replace species occurring in error model definition by observableIds. - replacements = { - sp.sympify(observable["formula"], locals=_clash): sp.Symbol( - observable_id - ) - for observable_id, observable in observables.items() - } - for observable_id, formula in sigmas.items(): - repl = sp.sympify(formula, locals=_clash).subs(replacements) - sigmas[observable_id] = str(repl) - - noise_distrs = petab_noise_distributions_to_amici(observable_df) - - return observables, noise_distrs, sigmas - - -def petab_noise_distributions_to_amici( - observable_df: pd.DataFrame, -) -> Dict[str, str]: - """ - Map from the petab to the amici format of noise distribution - identifiers. - - :param observable_df: - PEtab observable table - - :return: - Dictionary of observable_id => AMICI noise-distributions - """ - amici_distrs = {} - for _, observable in observable_df.iterrows(): - amici_val = "" - - if ( - OBSERVABLE_TRANSFORMATION in observable - and isinstance(observable[OBSERVABLE_TRANSFORMATION], str) - and observable[OBSERVABLE_TRANSFORMATION] - ): - amici_val += observable[OBSERVABLE_TRANSFORMATION] + "-" - - if ( - NOISE_DISTRIBUTION in observable - and isinstance(observable[NOISE_DISTRIBUTION], str) - and observable[NOISE_DISTRIBUTION] - ): - amici_val += observable[NOISE_DISTRIBUTION] - else: - amici_val += "normal" - amici_distrs[observable.name] = amici_val - - return amici_distrs - - -def petab_scale_to_amici_scale(scale_str: str) -> int: - """Convert PEtab parameter scaling string to AMICI scaling integer""" - - if scale_str == petab.LIN: - return amici.ParameterScaling_none - if scale_str == petab.LOG: - return amici.ParameterScaling_ln - if scale_str == petab.LOG10: - return amici.ParameterScaling_log10 - - raise ValueError(f"Invalid parameter scale {scale_str}") - - -def show_model_info(sbml_model: "libsbml.Model"): - """Log some model quantities""" - - logger.info(f"Species: {len(sbml_model.getListOfSpecies())}") - logger.info( - "Global parameters: " + str(len(sbml_model.getListOfParameters())) - ) - logger.info(f"Reactions: {len(sbml_model.getListOfReactions())}") +import warnings + +warnings.warn( + "Importing amici.petab_import is deprecated. Use `amici.petab` instead.", + DeprecationWarning, +) + +from .petab.import_helpers import ( + get_fixed_parameters, + get_observation_model, + petab_noise_distributions_to_amici, + petab_scale_to_amici_scale, +) + +# DEPRECATED - DON'T ADD ANYTHING NEW HERE +from .petab.petab_import import ( + check_model, + import_model, + import_model_sbml, + import_petab_problem, +) +from .petab.sbml_import import species_to_parameters diff --git a/python/sdist/amici/petab_import_pysb.py b/python/sdist/amici/petab_import_pysb.py index ccff072261..df3ed07631 100644 --- a/python/sdist/amici/petab_import_pysb.py +++ b/python/sdist/amici/petab_import_pysb.py @@ -1,275 +1,10 @@ -""" -PySB-PEtab Import ------------------ -Import a model in the PySB-adapted :mod:`petab` -(https://github.com/PEtab-dev/PEtab) format into AMICI. -""" +import warnings -import logging -import re -from pathlib import Path -from typing import Optional, Union +from .petab.pysb_import import * # noqa: F401, F403 -import petab -import pysb -import pysb.bng -import sympy as sp -from petab.C import CONDITION_NAME, NOISE_FORMULA, OBSERVABLE_FORMULA -from petab.models.pysb_model import PySBModel +# DEPRECATED - DON'T ADD ANYTHING NEW HERE -from .logging import get_logger, log_execution_time, set_log_level -from .petab import PREEQ_INDICATOR_ID -from .petab.util import get_states_in_condition_table - -logger = get_logger(__name__, logging.WARNING) - - -def _add_observation_model( - pysb_model: pysb.Model, petab_problem: petab.Problem -): - """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 pysb_model.components - if isinstance(comp, sp.Symbol) - } - for formula in [ - *petab_problem.observable_df[OBSERVABLE_FORMULA], - *petab_problem.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) - 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( - petab_problem.observable_df.index, - petab_problem.observable_df[OBSERVABLE_FORMULA], - petab_problem.observable_df[NOISE_FORMULA], - ): - obs_symbol = sp.sympify(observable_formula, locals=local_syms) - if observable_id in pysb_model.expressions.keys(): - obs_expr = pysb_model.expressions[observable_id] - else: - obs_expr = pysb.Expression(observable_id, obs_symbol) - 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) - pysb_model.add_component(sigma_expr) - local_syms[sigma_id] = sigma_expr - - -def _add_initialization_variables( - pysb_model: pysb.Model, petab_problem: petab.Problem -): - """Add initialization variables to the PySB model to support initial - conditions specified in the PEtab condition table. - - To parameterize initial states, we currently need initial assignments. - If they occur in the condition table, we create a new parameter - initial_${speciesID}. Feels dirty and should be changed (see also #924). - """ - - initial_states = get_states_in_condition_table(petab_problem) - fixed_parameters = [] - if initial_states: - # add preequilibration indicator variable - # NOTE: would only be required if we actually have preequilibration - # adding it anyways. can be optimized-out later - if PREEQ_INDICATOR_ID in [c.name for c in pysb_model.components]: - raise AssertionError( - "Model already has a component with ID " - f"{PREEQ_INDICATOR_ID}. Cannot handle " - "species and compartments in condition table " - "then." - ) - preeq_indicator = pysb.Parameter(PREEQ_INDICATOR_ID) - pysb_model.add_component(preeq_indicator) - # Can only reset parameters after preequilibration if they are fixed. - fixed_parameters.append(PREEQ_INDICATOR_ID) - logger.debug( - "Adding preequilibration indicator constant " - f"{PREEQ_INDICATOR_ID}" - ) - logger.debug(f"Adding initial assignments for {initial_states.keys()}") - - for assignee_id in initial_states: - init_par_id_preeq = f"initial_{assignee_id}_preeq" - init_par_id_sim = f"initial_{assignee_id}_sim" - for init_par_id in [init_par_id_preeq, init_par_id_sim]: - if init_par_id in [c.name for c in pysb_model.components]: - raise ValueError( - "Cannot create parameter for initial assignment " - f"for {assignee_id} because an entity named " - f"{init_par_id} exists already in the model." - ) - p = pysb.Parameter(init_par_id) - pysb_model.add_component(p) - - species_idx = int(re.match(r"__s(\d+)$", assignee_id)[1]) - # use original model here since that's what was used to generate - # the ids in initial_states - species_pattern = petab_problem.model.model.species[species_idx] - - # species pattern comes from the _original_ model, but we only want - # to modify pysb_model, so we have to reconstitute the pattern using - # pysb_model - for c in pysb_model.components: - globals()[c.name] = c - species_pattern = pysb.as_complex_pattern(eval(str(species_pattern))) - - from pysb.pattern import match_complex_pattern - - formula = pysb.Expression( - f"initial_{assignee_id}_formula", - preeq_indicator * pysb_model.parameters[init_par_id_preeq] - + (1 - preeq_indicator) * pysb_model.parameters[init_par_id_sim], - ) - pysb_model.add_component(formula) - - for initial in pysb_model.initials: - if match_complex_pattern( - initial.pattern, species_pattern, exact=True - ): - logger.debug( - "The PySB model has an initial defined for species " - f"{assignee_id}, but this species also has an initial " - "value defined in the PEtab condition table. The SBML " - "initial assignment will be overwritten to handle " - "preequilibration and initial values specified by the " - "PEtab problem." - ) - initial.value = formula - break - else: - # No initial in the pysb model, so add one - init = pysb.Initial(species_pattern, formula) - pysb_model.add_component(init) - - return fixed_parameters - - -@log_execution_time("Importing PEtab model", logger) -def import_model_pysb( - petab_problem: petab.Problem, - model_output_dir: Optional[Union[str, Path]] = None, - verbose: Optional[Union[bool, int]] = True, - model_name: Optional[str] = None, - **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 model_name: - Name of the generated model module - - :param kwargs: - Additional keyword arguments to be passed to - :meth:`amici.pysb_import.pysb2amici`. - """ - set_log_level(logger, verbose) - - logger.info("Importing model ...") - - if not isinstance(petab_problem.model, PySBModel): - raise ValueError("Not a PySB model") - - # need to create a copy here as we don't want to modify the original - pysb.SelfExporter.cleanup() - og_export = pysb.SelfExporter.do_export - pysb.SelfExporter.do_export = False - pysb_model = pysb.Model( - base=petab_problem.model.model, - name=petab_problem.model.model_id, - ) - - _add_observation_model(pysb_model, petab_problem) - # generate species for the _original_ model - pysb.bng.generate_equations(petab_problem.model.model) - fixed_parameters = _add_initialization_variables(pysb_model, petab_problem) - pysb.SelfExporter.do_export = og_export - - # check condition table for supported features, important to use pysb_model - # here, as we want to also cover output parameters - model_parameters = [p.name for p in pysb_model.parameters] - condition_species_parameters = get_states_in_condition_table( - petab_problem, return_patterns=True - ) - for x in petab_problem.condition_df.columns: - if x == CONDITION_NAME: - continue - - x = petab.mapping.resolve_mapping(petab_problem.mapping_df, x) - - # parameters - if x in model_parameters: - continue - - # species/pattern - if x in condition_species_parameters: - continue - - raise NotImplementedError( - "For PySB PEtab import, only model parameters and species, but " - "not compartments are allowed in the condition table. Offending " - f"column: {x}" - ) - - from .petab_import import ( - get_fixed_parameters, - petab_noise_distributions_to_amici, - ) - - constant_parameters = ( - get_fixed_parameters(petab_problem) + fixed_parameters - ) - - if petab_problem.observable_df is None: - observables = None - sigmas = None - noise_distrs = None - else: - observables = [ - expr.name - for expr in pysb_model.expressions - if expr.name in petab_problem.observable_df.index - ] - - sigmas = {obs_id: f"{obs_id}_sigma" for obs_id in observables} - - noise_distrs = petab_noise_distributions_to_amici( - petab_problem.observable_df - ) - - from amici.pysb_import import pysb2amici - - pysb2amici( - model=pysb_model, - output_dir=model_output_dir, - model_name=model_name, - verbose=True, - observables=observables, - sigmas=sigmas, - constant_parameters=constant_parameters, - noise_distributions=noise_distrs, - **kwargs, - ) +warnings.warn( + "Importing amici.petab_import_pysb is deprecated. Use `amici.petab.pysb_import` instead.", + DeprecationWarning, +) diff --git a/python/sdist/amici/petab_objective.py b/python/sdist/amici/petab_objective.py index 8fbb191bae..94bea6eb5f 100644 --- a/python/sdist/amici/petab_objective.py +++ b/python/sdist/amici/petab_objective.py @@ -16,9 +16,9 @@ from . import AmiciExpData, AmiciModel from .logging import get_logger, log_execution_time -from .parameter_mapping import ParameterMapping # some extra imports for backward-compatibility +# DEPRECATED: remove in 1.0 from .petab.conditions import ( # noqa # pylint: disable=unused-import create_edata_for_condition, create_edatas, @@ -26,6 +26,7 @@ fill_in_parameters, ) from .petab.parameter_mapping import ( # noqa # pylint: disable=unused-import + ParameterMapping, create_parameter_mapping, create_parameter_mapping_for_condition, ) @@ -33,6 +34,8 @@ get_states_in_condition_table, ) +# END DEPRECATED + try: import pysb except ImportError: diff --git a/python/sdist/amici/petab_util.py b/python/sdist/amici/petab_util.py index 6baf41d455..963beca3df 100644 --- a/python/sdist/amici/petab_util.py +++ b/python/sdist/amici/petab_util.py @@ -1,7 +1,11 @@ """Various helper functions for working with PEtab problems.""" + +# THIS FILE IS TO BE REMOVED - DON'T ADD ANYTHING HERE! + import warnings -from .petab.util import PREEQ_INDICATOR_ID, get_states_in_condition_table +from .petab import PREEQ_INDICATOR_ID # noqa: F401 +from .petab.util import get_states_in_condition_table # noqa: F401 warnings.warn( f"Importing {__name__} is deprecated. Use `amici.petab.util` instead.", diff --git a/python/tests/test_petab_objective.py b/python/tests/test_petab_objective.py index e31e693d11..f037bc989c 100755 --- a/python/tests/test_petab_objective.py +++ b/python/tests/test_petab_objective.py @@ -4,12 +4,12 @@ from pathlib import Path import amici -import amici.petab_import import amici.petab_objective import numpy as np import pandas as pd import petab import pytest +from amici.petab.petab_import import import_petab_problem from amici.petab_objective import SLLH # Absolute and relative tolerances for finite difference gradient checks. @@ -32,7 +32,7 @@ def lotka_volterra() -> petab.Problem: def test_simulate_petab_sensitivities(lotka_volterra): petab_problem = lotka_volterra - amici_model = amici.petab_import.import_petab_problem(petab_problem) + amici_model = import_petab_problem(petab_problem) amici_solver = amici_model.getSolver() amici_solver.setSensitivityOrder(amici.SensitivityOrder_first)