From 8c3fe3ece2737b7c998fcbac42e553583fc66e91 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Sat, 1 Nov 2025 15:21:48 +0100 Subject: [PATCH] Centralize default model path generation Currently, there are several places where default model directory paths are generated. Centralize that. Running the test suite or building the documentation creates model directories all over the place. Reduce that. Allow overriding the model base dir via environment variable. Closes #2994. --- CHANGELOG.md | 3 ++ python/sdist/amici/__init__.py | 47 +++++++++++++++++++ python/sdist/amici/de_export.py | 3 +- python/sdist/amici/petab/petab_import.py | 17 ++----- python/sdist/amici/petab/sbml_import.py | 27 ----------- python/sdist/amici/pysb_import.py | 2 +- python/sdist/amici/sbml_import.py | 5 +- python/tests/conftest.py | 6 ++- tests/benchmark_models/conftest.py | 4 +- .../benchmark_models/test_petab_benchmark.py | 5 +- tests/petab_test_suite/test_petab_suite.py | 2 - 11 files changed, 70 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3305b344df..b5955204a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,9 @@ See also our [versioning policy](https://amici.readthedocs.io/en/latest/versioni * The import function `sbml2amici`, `pysb2amici`, and `antimony2amici` now return an instance of the generated model class if called with `compile=True` (default). +* The default directory for model import changed, and a base directory + can now be specified via the `AMICI_MODELS_ROOT` environment variable. + See `amici.get_model_dir` for details. ## v0.X Series diff --git a/python/sdist/amici/__init__.py b/python/sdist/amici/__init__.py index 7bdf4ea5ed..84f907230a 100644 --- a/python/sdist/amici/__init__.py +++ b/python/sdist/amici/__init__.py @@ -76,6 +76,53 @@ def _imported_from_setup() -> bool: return False +def get_model_root_dir() -> Path: + """Get the default root directory for AMICI models. + + :return: + The model root directory. + This defaults to `{base_dir}/{amici_version}`. + If the environment variable `AMICI_MODELS_ROOT` is set, + it is used as `base_dir`, otherwise `amici_models` in the current + working directory. + """ + try: + base_dir = Path(os.environ["AMICI_MODELS_ROOT"]) + except KeyError: + base_dir = Path("amici_models") + + return base_dir / __version__ + + +def get_model_dir(model_id: str | None = None, jax: bool = False) -> Path: + """Get the default directory for the model with the given ID. + + :param model_id: + The model ID. + :param jax: + Whether to get the model directory for a JAX model. + If `True`, a suffix `_jax` is appended to the `model_id`. + :return: + The model directory. + This defaults to `{root_dir}/{model_id}`, where `root_dir` is + determined via :func:`get_model_root_dir`. + If `model_id` is `None`, a temporary directory is created in + `{base_dir}/{amici_version}` and returned. + """ + base_dir = get_model_root_dir() + + suffix = "_jax" if jax else "" + + if model_id is None: + import tempfile + + return Path( + tempfile.mkdtemp(dir=base_dir / __version__), suffix=suffix + ) + + return base_dir / __version__ / (model_id + suffix) + + # Initialize AMICI paths #: absolute root path of the amici repository or Python package amici_path = _get_amici_path() diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index d87638423d..05498c30fc 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -30,6 +30,7 @@ amiciModulePath, amiciSrcPath, amiciSwigPath, + get_model_dir, splines, ) from ._codegen.cxx_functions import ( @@ -1311,7 +1312,7 @@ def set_paths(self, output_dir: str | Path | None = None) -> None: """ if output_dir is None: - output_dir = os.path.join(os.getcwd(), f"amici-{self.model_name}") + output_dir = get_model_dir(self.model_name) self.model_path = os.path.abspath(output_dir) self.model_swig_path = os.path.join(self.model_path, "swig") diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index ac49894685..28d1b02033 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -55,7 +55,7 @@ def import_petab_problem( :param model_output_dir: Directory to write the model code to. It will be created if it doesn't - exist. Defaults to current directory. + exist. Defaults to :func:`amici.get_model_dir`. :param model_name: Name of the generated model module. Defaults to the ID of the model @@ -99,20 +99,11 @@ def import_petab_problem( # 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, jax=jax - ) + model_output_dir = amici.get_model_dir(model_name, jax=jax).absolute() else: - model_output_dir = os.path.abspath(model_output_dir) + model_output_dir = Path(model_output_dir).absolute() - # create folder - if not os.path.exists(model_output_dir): - os.makedirs(model_output_dir) + model_output_dir.mkdir(parents=True, exist_ok=True) # check if compilation necessary if compile_ or ( diff --git a/python/sdist/amici/petab/sbml_import.py b/python/sdist/amici/petab/sbml_import.py index 801cb6227f..26936de986 100644 --- a/python/sdist/amici/petab/sbml_import.py +++ b/python/sdist/amici/petab/sbml_import.py @@ -2,7 +2,6 @@ import math import os import re -import tempfile from _collections import OrderedDict from itertools import chain from pathlib import Path @@ -604,29 +603,3 @@ def _get_fixed_parameters_sbml( continue return list(sorted(fixed_parameters)) - - -def _create_model_output_dir_name( - sbml_model: "libsbml.Model", - model_name: str | None = None, - jax: bool = False, -) -> 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 - suffix = "_jax" if jax else "" - if model_name: - return BASE_DIR / (model_name + suffix) - - # try sbml model id - if sbml_model_id := sbml_model.getId(): - return BASE_DIR / (sbml_model_id + suffix) - - # create random folder name - return Path(tempfile.mkdtemp(dir=BASE_DIR)) diff --git a/python/sdist/amici/pysb_import.py b/python/sdist/amici/pysb_import.py index 727e5c59c6..4587bbb572 100644 --- a/python/sdist/amici/pysb_import.py +++ b/python/sdist/amici/pysb_import.py @@ -251,7 +251,7 @@ def pysb2amici( constant_parameters = [] model_name = model_name or model.name - + output_dir = output_dir or amici.get_model_dir(model_name) set_log_level(logger, verbose) ode_model = ode_model_from_pysb_importer( model, diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 4824f38de7..f3717aa21f 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -28,7 +28,7 @@ import amici -from . import has_clibs +from . import get_model_dir, has_clibs from .constants import SymbolId from .de_export import ( DEExporter, @@ -317,6 +317,7 @@ def sbml2amici( :param output_dir: Directory where the generated model package will be stored. + Defaults to :func:`amici.get_model_dir`. :param constant_parameters: list of SBML Ids identifying constant parameters @@ -398,6 +399,8 @@ def sbml2amici( hardcode_symbols=hardcode_symbols, ) + output_dir = output_dir or get_model_dir(model_name) + exporter = DEExporter( ode_model, model_name=model_name, diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 07654edc9c..3e32bd42ee 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -2,6 +2,7 @@ import copy import importlib +import os import sys from pathlib import Path @@ -12,7 +13,8 @@ pytest_plugins = ["amici.testing.fixtures"] -EXAMPLES_DIR = Path(__file__).parents[2] / "doc" / "examples" +REPO_ROOT = Path(__file__).parents[2] +EXAMPLES_DIR = REPO_ROOT / "doc" / "examples" TEST_DIR = Path(__file__).parent MODEL_STEADYSTATE_SCALED_XML = ( EXAMPLES_DIR / "getting_started" / "model_steadystate_scaled.xml" @@ -21,6 +23,8 @@ EXAMPLES_DIR / "example_presimulation" / "model_presimulation.xml" ) +os.environ.setdefault("AMICI_MODELS_ROOT", str(REPO_ROOT.absolute())) + @pytest.fixture(scope="session") def sbml_example_presimulation_module(): diff --git a/tests/benchmark_models/conftest.py b/tests/benchmark_models/conftest.py index 09ba3eec3b..7918275179 100644 --- a/tests/benchmark_models/conftest.py +++ b/tests/benchmark_models/conftest.py @@ -5,6 +5,7 @@ import benchmark_models_petab import petab.v1 as petab import pytest +from amici import get_model_root_dir from amici.petab.petab_import import import_petab_problem from petab.v1.lint import measurement_table_has_timepoint_specific_mappings @@ -14,8 +15,7 @@ from test_petab_benchmark import problems -repo_root = script_dir.parent.parent -benchmark_outdir = repo_root / "test_bmc" +benchmark_outdir = get_model_root_dir() / "test_bmc" @pytest.fixture(scope="session", params=problems, ids=problems) diff --git a/tests/benchmark_models/test_petab_benchmark.py b/tests/benchmark_models/test_petab_benchmark.py index 242b45a5ec..0a5ddfb9bc 100644 --- a/tests/benchmark_models/test_petab_benchmark.py +++ b/tests/benchmark_models/test_petab_benchmark.py @@ -20,7 +20,7 @@ import petab.v1 as petab import pytest import yaml -from amici import SensitivityMethod +from amici import SensitivityMethod, get_model_root_dir from amici.adapters.fiddy import simulate_petab_to_cached_functions from amici.logging import get_logger from amici.petab.petab_import import import_petab_problem @@ -44,8 +44,7 @@ ) script_dir = Path(__file__).parent.absolute() -repo_root = script_dir.parent.parent -benchmark_outdir = repo_root / "test_bmc" +benchmark_outdir = get_model_root_dir() / "test_bmc" debug_path = script_dir / "debug" if debug: debug_path.mkdir(exist_ok=True, parents=True) diff --git a/tests/petab_test_suite/test_petab_suite.py b/tests/petab_test_suite/test_petab_suite.py index fae48528f8..d74e870664 100755 --- a/tests/petab_test_suite/test_petab_suite.py +++ b/tests/petab_test_suite/test_petab_suite.py @@ -63,10 +63,8 @@ def _test_case(case, model_type, version, jax): model_name = ( f"petab_{model_type}_test_case_{case}_{version.replace('.', '_')}" ) - model_output_dir = f"amici_models/{model_name}" + ("_jax" if jax else "") imported = import_petab_problem( petab_problem=problem, - model_output_dir=model_output_dir, model_name=model_name, compile_=True, jax=jax,