"""
AMICI
-----
The AMICI Python module provides functionality for importing SBML or PySB
models and turning them into C++ Python extensions.
"""
import contextlib
import importlib
import os
import re
import sys
from pathlib import Path
from types import ModuleType as ModelModule
from typing import Any, Callable, Union
def _get_amici_path():
"""
Determine package installation path, or, if used directly from git
repository, get repository root
"""
basedir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
if os.path.exists(os.path.join(basedir, ".git")):
return os.path.abspath(basedir)
return os.path.dirname(__file__)
def _get_commit_hash():
"""Get commit hash from file"""
basedir = os.path.dirname(os.path.dirname(os.path.dirname(amici_path)))
commitfile = next(
(
file
for file in [
os.path.join(basedir, ".git", "FETCH_HEAD"),
os.path.join(basedir, ".git", "ORIG_HEAD"),
]
if os.path.isfile(file)
),
None,
)
if commitfile:
with open(commitfile) as f:
return str(re.search(r"^([\w]*)", f.read().strip()).group())
return "unknown"
def _imported_from_setup() -> bool:
"""Check whether this module is imported from `setup.py`"""
from inspect import currentframe, getouterframes
from os import sep
# in case we are imported from setup.py, this will be the AMICI package
# root directory (otherwise it is most likely the Python library directory,
# we are not interested in)
package_root = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
for frame in getouterframes(currentframe(), context=0):
# Need to compare the full path, in case a user tries to import AMICI
# from a module `*setup.py`. Will still cause trouble if some package
# requires the AMICI extension during its installation, but seems
# unlikely...
frame_path = os.path.realpath(os.path.expanduser(frame.filename))
if frame_path == os.path.join(
package_root, "setup.py"
) or frame_path.endswith(f"{sep}setuptools{sep}build_meta.py"):
return True
return False
# Initialize AMICI paths
#: absolute root path of the amici repository or Python package
amici_path = _get_amici_path()
#: absolute path of the amici swig directory
amiciSwigPath = os.path.join(amici_path, "swig")
#: absolute path of the amici source directory
amiciSrcPath = os.path.join(amici_path, "src")
#: absolute root path of the amici module
amiciModulePath = os.path.dirname(__file__)
#: boolean indicating if this is the full package with swig interface or
# the raw package without extension
has_clibs: bool = any(
os.path.isfile(os.path.join(amici_path, wrapper))
for wrapper in ["amici.py", "amici_without_hdf5.py"]
)
#: boolean indicating if amici was compiled with hdf5 support
hdf5_enabled: bool = False
# Get version number from file
with open(os.path.join(amici_path, "version.txt")) as f:
__version__ = f.read().strip()
__commit__ = _get_commit_hash()
# Import SWIG module and swig-dependent submodules if required and available
if not _imported_from_setup():
if has_clibs:
from . import amici
from .amici import *
# has to be done before importing readSolverSettingsFromHDF5
# from .swig_wrappers
hdf5_enabled = "readSolverSettingsFromHDF5" in dir()
# These modules require the swig interface and other dependencies
from .numpy import ExpDataView, ReturnDataView # noqa: F401
from .pandas import *
from .swig_wrappers import *
# These modules don't require the swig interface
from typing import Protocol, runtime_checkable
from .de_export import DEExporter # noqa: F401
from .sbml_import import ( # noqa: F401
SbmlImporter,
assignmentRules2observables,
)
[docs]
@runtime_checkable
class ModelModule(Protocol): # noqa: F811
"""Type of AMICI-generated model modules.
To enable static type checking."""
[docs]
def getModel(self) -> amici.Model:
"""Create a model instance."""
...
[docs]
def get_model(self) -> amici.Model:
"""Create a model instance."""
...
AmiciModel = Union[amici.Model, amici.ModelPtr]
[docs]
class add_path:
"""Context manager for temporarily changing PYTHONPATH"""
[docs]
def __init__(self, path: Union[str, Path]):
self.path: str = str(path)
def __enter__(self):
if self.path:
sys.path.insert(0, self.path)
def __exit__(self, exc_type, exc_value, traceback):
with contextlib.suppress(ValueError):
sys.path.remove(self.path)
[docs]
def import_model_module(
module_name: str, module_path: Union[Path, str]
) -> ModelModule:
"""
Import Python module of an AMICI model
:param module_name:
Name of the python package of the model
:param module_path:
Absolute or relative path of the package directory
:return:
The model module
"""
module_path = str(module_path)
# ensure we will find the newly created module
importlib.invalidate_caches()
if not os.path.isdir(module_path):
raise ValueError(f"module_path '{module_path}' is not a directory.")
module_path = os.path.abspath(module_path)
# module already loaded?
if module_name in sys.modules:
# if a module with that name is already in sys.modules, we remove it,
# along with all other modules from that package. otherwise, there
# will be trouble if two different models with the same name are to
# be imported.
del sys.modules[module_name]
# collect first, don't delete while iterating
to_unload = {
loaded_module_name
for loaded_module_name in sys.modules.keys()
if loaded_module_name.startswith(f"{module_name}.")
}
for m in to_unload:
del sys.modules[m]
with add_path(module_path):
return importlib.import_module(module_name)
[docs]
class AmiciVersionError(RuntimeError):
"""Error thrown if an AMICI model is loaded that is incompatible with
the installed AMICI base package"""
pass
def _get_default_argument(func: Callable, arg: str) -> Any:
"""Get the default value of the given argument in the given function."""
import inspect
signature = inspect.signature(func)
if (
default := signature.parameters[arg].default
) is not inspect.Parameter.empty:
return default
raise ValueError(f"No default value for argument {arg} of {func}.")