diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..87c86463fa --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +f59fed72eb4d0cedf6abf2a96fe95087ce61478a diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d3e0189bdb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-patch", "version-update:semver-minor"] diff --git a/.github/workflows/deploy_branch.yml b/.github/workflows/deploy_branch.yml index ff1be3145d..24d77d8118 100644 --- a/.github/workflows/deploy_branch.yml +++ b/.github/workflows/deploy_branch.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/deploy_protected.yml b/.github/workflows/deploy_protected.yml index 881a4eb77a..e98209c97d 100644 --- a/.github/workflows/deploy_protected.yml +++ b/.github/workflows/deploy_protected.yml @@ -29,14 +29,14 @@ jobs: fi dockerhub: - name: Deploy Dockerhub + name: Deploy Docker Hub needs: [check-secret] if: needs.check-secret.outputs.secrets-defined == 'true' runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Publish to Registry - uses: elgohr/Publish-Docker-Github-Action@v4 + uses: elgohr/Publish-Docker-Github-Action@v5 with: name: dweindl/amici username: ${{ secrets.DOCKER_USERNAME }} diff --git a/.github/workflows/deploy_release.yml b/.github/workflows/deploy_release.yml index de7a073cc2..b18c31ff2a 100644 --- a/.github/workflows/deploy_release.yml +++ b/.github/workflows/deploy_release.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] environment: name: pypi @@ -39,7 +39,7 @@ jobs: - name: Remove direct dependencies from setup.cfg # Remove any "git+https"-based dependencies that are not supported # by PyPI and are only required for testing. - run: sed -i '/git+https/d' python/sdist/setup.cfg + run: sed -i '/git+https/d' python/sdist/pyproject.toml - name: sdist run: scripts/buildSdist.sh @@ -74,3 +74,29 @@ jobs: -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/${DOWNSTREAM_REPOSITORY}/actions/workflows/${WORKFLOW_FILE}/dispatches \ -d "{\"ref\": \"dev\", \"inputs\": {\"simulatorVersion\": \"${PACKAGE_VERSION}\", \"simulatorVersionLatest\": \"true\"}}" + + dockerhub: + name: Release to Docker Hub + runs-on: ubuntu-22.04 + + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + - uses: actions/checkout@v4 + - run: git archive -o container/amici.tar.gz --format=tar.gz HEAD + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Publish to Registry + uses: elgohr/Publish-Docker-Github-Action@v5 + with: + name: dweindl/amici + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + workdir: container/ + dockerfile: Dockerfile + tag_semver: true + platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/test_benchmark_collection_models.yml b/.github/workflows/test_benchmark_collection_models.yml index ab29938a12..023fe077e6 100644 --- a/.github/workflows/test_benchmark_collection_models.yml +++ b/.github/workflows/test_benchmark_collection_models.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] extract_subexpressions: ["true", "false"] env: AMICI_EXTRACT_CSE: ${{ matrix.extract_subexpressions }} diff --git a/.github/workflows/test_doc.yml b/.github/workflows/test_doc.yml index a4fe10de22..5fcf490eb7 100644 --- a/.github/workflows/test_doc.yml +++ b/.github/workflows/test_doc.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test_install.yml b/.github/workflows/test_install.yml index 0c624a33e5..9e1717d962 100644 --- a/.github/workflows/test_install.yml +++ b/.github/workflows/test_install.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} @@ -50,7 +50,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} @@ -83,7 +83,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test_performance.yml b/.github/workflows/test_performance.yml index c02245224c..6b66698d10 100644 --- a/.github/workflows/test_performance.yml +++ b/.github/workflows/test_performance.yml @@ -23,7 +23,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test_petab_test_suite.yml b/.github/workflows/test_petab_test_suite.yml index b6465d554b..7e9a93c494 100644 --- a/.github/workflows/test_petab_test_suite.yml +++ b/.github/workflows/test_petab_test_suite.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: - python-version: [3.9] + python-version: ["3.11"] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test_pypi.yml b/.github/workflows/test_pypi.yml index fecda3edb0..1e91019c6f 100644 --- a/.github/workflows/test_pypi.yml +++ b/.github/workflows/test_pypi.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] os: [ubuntu-22.04, macos-latest] runs-on: ${{ matrix.os }} diff --git a/.github/workflows/test_python_cplusplus.yml b/.github/workflows/test_python_cplusplus.yml index f729d47867..5714a37b21 100644 --- a/.github/workflows/test_python_cplusplus.yml +++ b/.github/workflows/test_python_cplusplus.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] steps: - name: Cache @@ -117,7 +117,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.10" ] steps: - name: Set up Python ${{ matrix.python-version }} @@ -193,7 +193,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] steps: - name: Set up Python ${{ matrix.python-version }} @@ -233,7 +233,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" - uses: actions/checkout@v4 - run: git fetch --prune --unshallow @@ -282,7 +282,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.11" - uses: actions/checkout@v4 - run: git fetch --prune --unshallow diff --git a/.github/workflows/test_python_ver_matrix.yml b/.github/workflows/test_python_ver_matrix.yml index 414daadccc..01d455b18a 100644 --- a/.github/workflows/test_python_ver_matrix.yml +++ b/.github/workflows/test_python_ver_matrix.yml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] experimental: [false] steps: diff --git a/.github/workflows/test_sbml_semantic_test_suite.yml b/.github/workflows/test_sbml_semantic_test_suite.yml index 3c0b3bd149..69c78d44b4 100644 --- a/.github/workflows/test_sbml_semantic_test_suite.yml +++ b/.github/workflows/test_sbml_semantic_test_suite.yml @@ -29,7 +29,7 @@ jobs: matrix: cases: ["1-250", "251-500", "501-750", "751-1000", "1000-1250", "1251-"] - python-version: [ "3.9" ] + python-version: [ "3.11" ] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/test_valgrind.yml b/.github/workflows/test_valgrind.yml index 034945d461..b3f893647f 100644 --- a/.github/workflows/test_valgrind.yml +++ b/.github/workflows/test_valgrind.yml @@ -18,7 +18,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] env: ENABLE_AMICI_DEBUGGING: "TRUE" @@ -57,7 +57,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] env: ENABLE_AMICI_DEBUGGING: "TRUE" diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml index fbd91ce73a..8b5b3b89f7 100644 --- a/.github/workflows/test_windows.yml +++ b/.github/workflows/test_windows.yml @@ -27,7 +27,7 @@ jobs: strategy: matrix: - python-version: [ "3.9" ] + python-version: [ "3.11" ] steps: - name: Set up Python ${{ matrix.python-version }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2d00e00c1..449f94ca79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-merge-conflict @@ -12,7 +12,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.11 + rev: v0.4.1 hooks: # Run the linter. - id: ruff @@ -28,10 +28,9 @@ repos: - python/sdist/pyproject.toml - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.15.2 hooks: - id: pyupgrade - args: ["--py39-plus"] - additional_dependencies: [pyupgrade==3.15.0] + args: ["--py310-plus"] exclude: '^(ThirdParty|models)/' diff --git a/CHANGELOG.md b/CHANGELOG.md index e658b12593..1819436908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,42 @@ ## v0.X Series +### v0.25.0 (2024-05-TBD) + +This release requires Python >= 3.10. + +**Fixes** +* Fixed a bug in event handling that could lead to incorrect simulation + results for models with events that assign to compartments *and* have + additional event assignments + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2428 +* SBML import: handle `useValuesFromTriggerTime` attribute on events. + This attribute was previously ignored. It is possible that now AMICI fails + to import models that it previously imported successfully. For cases where + `useValuesFromTriggerTime=True` made a difference, AMICI might have produced + incorrect results before. + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2429 +* Faster code generation for models with events if they don't have + state-dependent triggers + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2417 +* Most warnings now come with a more informative code location + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2421 +* `amici.ExpData` was changed so that `isinstance(edata, amici.ExpData)` works + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2396 + +**Features** +* Event-assignments to compartments are now supported. Previously, this only + worked for compartments that were rate rule targets. + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2425 +* Releases are now deployed to Docker Hub + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2413 + +**Full Changelog**: https://github.com/AMICI-dev/AMICI/compare/v0.24.0...v0.25.0 + ### v0.24.0 (2024-04-22) This will be the last release supporting Python 3.9. -Future releases will require Python 3.10. +Future releases will require Python>=3.10. **Fixes** @@ -27,7 +59,7 @@ Future releases will require Python 3.10. the correct SUNDIALS installation by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2397 -* **Features** +**Features** * Optionally include measurements in `plot_observable_trajectories` by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2381 diff --git a/documentation/conf.py b/documentation/conf.py index 25c6dab647..a0dcf658fb 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -558,15 +558,15 @@ def fix_typehints(sig: str) -> str: sig = sig.replace("sunindextype", "int") sig = sig.replace("H5::H5File", "object") - # remove const - sig = sig.replace(" const ", r" ") - sig = re.sub(r" const$", r"", sig) + # remove const / const& + sig = sig.replace(" const&? ", r" ") + sig = re.sub(r" const&?$", r"", sig) # remove pass by reference sig = re.sub(r" &(,|\))", r"\1", sig) sig = re.sub(r" &$", r"", sig) - # turn gsl_spans and pointers int Iterables + # turn gsl_spans and pointers into Iterables sig = re.sub(r"([\w.]+) \*", r"Iterable[\1]", sig) sig = re.sub(r"gsl::span< ([\w.]+) >", r"Iterable[\1]", sig) diff --git a/documentation/gfx/amici_workflow.png b/documentation/gfx/amici_workflow.png index 75f904ff7d..9eab380580 100644 Binary files a/documentation/gfx/amici_workflow.png and b/documentation/gfx/amici_workflow.png differ diff --git a/documentation/gfx/amici_workflow.svg b/documentation/gfx/amici_workflow.svg index ee1a86fccc..0d6ea41c64 100644 --- a/documentation/gfx/amici_workflow.svg +++ b/documentation/gfx/amici_workflow.svg @@ -7,9 +7,9 @@ viewBox="0 0 294.54358 107.54766" version="1.1" id="svg11110" - inkscape:version="1.1.1 (1:1.1+202109281946+c3084ef5ed)" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" sodipodi:docname="amici_workflow.svg" - inkscape:export-filename="/home/dweindl/src/AMICI-devel/documentation/gfx/amici_workflow.png" + inkscape:export-filename="amici_workflow.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -570,13 +570,13 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="3.959798" - inkscape:cx="83.085046" - inkscape:cy="80.180858" + inkscape:cx="76.771593" + inkscape:cy="80.433396" inkscape:document-units="mm" inkscape:current-layer="g22231" showgrid="false" inkscape:window-width="1920" - inkscape:window-height="974" + inkscape:window-height="1110" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -584,7 +584,9 @@ fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" - inkscape:pagecheckerboard="0" /> + inkscape:pagecheckerboard="0" + inkscape:showpageshadow="2" + inkscape:deskcolor="#d1d1d1" /> @@ -1498,7 +1500,7 @@ id="tspan20651" x="-16.819941" y="40.732136" - style="stroke-width:0.264583px">>= 3.9 + style="stroke-width:0.264583px">>= 3.10 =3.9 +* Python>=3.10 * :term:`SWIG`>=3.0 * CBLAS compatible BLAS library (e.g., OpenBLAS, CBLAS, Atlas, Accelerate, Intel MKL) diff --git a/documentation/python_interface.rst b/documentation/python_interface.rst index a919e925c4..2926abf963 100644 --- a/documentation/python_interface.rst +++ b/documentation/python_interface.rst @@ -26,7 +26,7 @@ AMICI can import :term:`SBML` models via the Status of SBML support in Python-AMICI ++++++++++++++++++++++++++++++++++++++ -Python-AMICI currently **passes 1247 out of the 1821 (~68%) test cases** from +Python-AMICI currently **passes 1252 out of the 1821 (~68%) test cases** from the semantic `SBML Test Suite `_ (`current status `_). diff --git a/python/sdist/amici/__init__.py b/python/sdist/amici/__init__.py index cd7bcb0500..942b669fa2 100644 --- a/python/sdist/amici/__init__.py +++ b/python/sdist/amici/__init__.py @@ -13,7 +13,8 @@ import sys from pathlib import Path from types import ModuleType as ModelModule -from typing import Any, Callable, Union +from typing import Any +from collections.abc import Callable def _get_amici_path(): @@ -134,13 +135,11 @@ def get_model(self) -> amici.Model: """Create a model instance.""" ... - AmiciModel = Union[amici.Model, amici.ModelPtr] - class add_path: """Context manager for temporarily changing PYTHONPATH""" - def __init__(self, path: Union[str, Path]): + def __init__(self, path: str | Path): self.path: str = str(path) def __enter__(self): @@ -153,7 +152,7 @@ def __exit__(self, exc_type, exc_value, traceback): def import_model_module( - module_name: str, module_path: Union[Path, str] + module_name: str, module_path: Path | str ) -> ModelModule: """ Import Python module of an AMICI model diff --git a/python/sdist/amici/_codegen/cxx_functions.py b/python/sdist/amici/_codegen/cxx_functions.py index 7831ed97c2..5fd5ead94a 100644 --- a/python/sdist/amici/_codegen/cxx_functions.py +++ b/python/sdist/amici/_codegen/cxx_functions.py @@ -1,4 +1,5 @@ """Info about C++ functions in the generated model code.""" + from __future__ import annotations from dataclasses import dataclass diff --git a/python/sdist/amici/_codegen/model_class.py b/python/sdist/amici/_codegen/model_class.py index e6366c1dfd..d24884ca89 100644 --- a/python/sdist/amici/_codegen/model_class.py +++ b/python/sdist/amici/_codegen/model_class.py @@ -1,4 +1,5 @@ """Function for generating the ``amici::Model`` subclass for an amici model.""" + from __future__ import annotations from .cxx_functions import functions, multiobs_functions diff --git a/python/sdist/amici/_codegen/template.py b/python/sdist/amici/_codegen/template.py index 34f3391ed6..2a099ba907 100644 --- a/python/sdist/amici/_codegen/template.py +++ b/python/sdist/amici/_codegen/template.py @@ -1,7 +1,7 @@ """Functions to apply template substitution to files.""" + from pathlib import Path from string import Template -from typing import Union class TemplateAmici(Template): @@ -17,8 +17,8 @@ class TemplateAmici(Template): def apply_template( - source_file: Union[str, Path], - target_file: Union[str, Path], + source_file: str | Path, + target_file: str | Path, template_data: dict[str, str], ) -> None: """ diff --git a/python/sdist/amici/antimony_import.py b/python/sdist/amici/antimony_import.py index 545a2654bd..ad269f11fe 100644 --- a/python/sdist/amici/antimony_import.py +++ b/python/sdist/amici/antimony_import.py @@ -3,11 +3,11 @@ https://antimony.sourceforge.net/ https://tellurium.readthedocs.io/en/latest/antimony.html """ + from pathlib import Path -from typing import Union -def antimony2sbml(ant_model: Union[str, Path]) -> str: +def antimony2sbml(ant_model: str | Path) -> str: """Convert Antimony model to SBML. :param ant_model: Antimony model as string or path to file @@ -46,7 +46,7 @@ def antimony2sbml(ant_model: Union[str, Path]) -> str: return sbml_str -def antimony2amici(ant_model: Union[str, Path], *args, **kwargs): +def antimony2amici(ant_model: str | Path, *args, **kwargs): """Convert Antimony model to AMICI model. Converts the Antimony model provided as string of file to SBML and then imports it into AMICI. diff --git a/python/sdist/amici/bngl_import.py b/python/sdist/amici/bngl_import.py index 960413dbe6..8624ca15f1 100644 --- a/python/sdist/amici/bngl_import.py +++ b/python/sdist/amici/bngl_import.py @@ -5,7 +5,6 @@ in the :term:`BNGL` format. """ - from pysb.importers.bngl import model_from_bngl from .pysb_import import pysb2amici diff --git a/python/sdist/amici/compile.py b/python/sdist/amici/compile.py index 6c4a336afc..eb3668bfbb 100644 --- a/python/sdist/amici/compile.py +++ b/python/sdist/amici/compile.py @@ -2,18 +2,18 @@ Functionality for building the C++ extensions of an amici-created model package. """ + import subprocess import sys -from typing import Optional, Union from pathlib import Path import os def build_model_extension( - package_dir: Union[str, Path], - verbose: Optional[Union[bool, int]] = False, - compiler: Optional[str] = None, - extra_msg: Optional[str] = None, + package_dir: str | Path, + verbose: bool | int | None = False, + compiler: str | None = None, + extra_msg: str | None = None, ) -> None: """ Compile the model extension of an amici-created model package. diff --git a/python/sdist/amici/conserved_quantities_demartino.py b/python/sdist/amici/conserved_quantities_demartino.py index 4f2d326b54..cf651241d1 100644 --- a/python/sdist/amici/conserved_quantities_demartino.py +++ b/python/sdist/amici/conserved_quantities_demartino.py @@ -2,7 +2,6 @@ import math import random import sys -from typing import Optional, Union from collections.abc import MutableSequence, Sequence from .logging import get_logger @@ -21,8 +20,8 @@ def compute_moiety_conservation_laws( num_species: int, num_reactions: int, max_num_monte_carlo: int = 20, - rng_seed: Union[None, bool, int] = False, - species_names: Optional[Sequence[str]] = None, + rng_seed: None | bool | int = False, + species_names: Sequence[str] | None = None, ) -> tuple[list[list[int]], list[list[float]]]: """Compute moiety conservation laws. @@ -116,7 +115,7 @@ def _output( int_matched: list[int], species_indices: list[list[int]], species_coefficients: list[list[float]], - species_names: Optional[Sequence[str]] = None, + species_names: Sequence[str] | None = None, verbose: bool = False, log_level: int = logging.DEBUG, ): @@ -140,7 +139,7 @@ def log(*args, **kwargs): # print all conserved quantities if verbose: for i, (coefficients, engaged_species_idxs) in enumerate( - zip(species_coefficients, species_indices) + zip(species_coefficients, species_indices, strict=True) ): if not engaged_species_idxs: continue @@ -149,7 +148,7 @@ def log(*args, **kwargs): "species:" ) for species_idx, coefficient in zip( - engaged_species_idxs, coefficients + engaged_species_idxs, coefficients, strict=True ): name = ( species_names[species_idx] @@ -958,12 +957,12 @@ def _reduce( k2 = order[j] column: list[float] = [0] * num_species for species_idx, coefficient in zip( - cls_species_idxs[k1], cls_coefficients[k1] + cls_species_idxs[k1], cls_coefficients[k1], strict=True ): column[species_idx] = coefficient ok1 = True for species_idx, coefficient in zip( - cls_species_idxs[k2], cls_coefficients[k2] + cls_species_idxs[k2], cls_coefficients[k2], strict=True ): column[species_idx] -= coefficient if column[species_idx] < -_MIN: diff --git a/python/sdist/amici/conserved_quantities_rref.py b/python/sdist/amici/conserved_quantities_rref.py index b16053ab08..85dbd2b9b1 100644 --- a/python/sdist/amici/conserved_quantities_rref.py +++ b/python/sdist/amici/conserved_quantities_rref.py @@ -1,12 +1,12 @@ """Find conserved quantities deterministically""" -from typing import Literal, Optional, Union +from typing import Literal import numpy as np def rref( - mat: np.array, round_ndigits: Optional[Union[Literal[False], int]] = None + mat: np.array, round_ndigits: Literal[False] | int | None = None ) -> np.array: """ Bring matrix ``mat`` to reduced row echelon form diff --git a/python/sdist/amici/custom_commands.py b/python/sdist/amici/custom_commands.py index 46abfe3290..36fcd17605 100644 --- a/python/sdist/amici/custom_commands.py +++ b/python/sdist/amici/custom_commands.py @@ -17,13 +17,17 @@ class AmiciInstall(install): """Custom `install` command to handle extra arguments""" - print("running AmiciInstall") - # Passing --no-clibs allows to install the Python-only part of AMICI user_options = install.user_options + [ ("no-clibs", None, "Don't build AMICI C++ extension"), ] + def run(self): + """Setuptools entry-point""" + print(f"running {self.__class__.__name__}") + + super().run() + def initialize_options(self): super().initialize_options() self.no_clibs = False diff --git a/python/sdist/amici/cxxcodeprinter.py b/python/sdist/amici/cxxcodeprinter.py index fb98b0aca0..69060eb00a 100644 --- a/python/sdist/amici/cxxcodeprinter.py +++ b/python/sdist/amici/cxxcodeprinter.py @@ -1,8 +1,8 @@ """C++ code generation""" + import itertools import os import re -from typing import Optional from collections.abc import Sequence from collections.abc import Iterable @@ -49,7 +49,7 @@ def __init__(self): else: self._fpoptimizer = None - def doprint(self, expr: sp.Expr, assign_to: Optional[str] = None) -> str: + def doprint(self, expr: sp.Expr, assign_to: str | None = None) -> str: if self._fpoptimizer: if isinstance(expr, list): expr = list(map(self._fpoptimizer, expr)) @@ -124,7 +124,7 @@ def _get_sym_lines_symbols( equations: sp.Matrix, variable: str, indent_level: int, - indices: Optional[Sequence[int]] = None, + indices: Sequence[int] | None = None, ) -> list[str]: """ Generate C++ code for where array elements are directly replaced with @@ -179,7 +179,9 @@ def format_regular_line(symbol, math, index): # we need toposort to handle the dependencies of extracted # subexpressions expr_dict = dict( - itertools.chain(zip(symbols, reduced_exprs), replacements) + itertools.chain( + zip(symbols, reduced_exprs, strict=True), replacements + ) ) sorted_symbols = toposort( { @@ -192,7 +194,7 @@ def format_regular_line(symbol, math, index): } ) symbol_to_idx = { - sym: idx for idx, sym in zip(indices, symbols) + sym: idx for idx, sym in zip(indices, symbols, strict=True) } def format_line(symbol: sp.Symbol): @@ -217,7 +219,9 @@ def format_line(symbol: sp.Symbol): return [ format_regular_line(sym, math, index) - for index, sym, math in zip(indices, symbols, equations) + for index, sym, math in zip( + indices, symbols, equations, strict=True + ) if math not in [0, 0.0] ] @@ -230,8 +234,8 @@ def print_bool(expr) -> str: def get_switch_statement( condition: str, cases: dict[int, list[str]], - indentation_level: Optional[int] = 0, - indentation_step: Optional[str] = " " * 4, + indentation_level: int | None = 0, + indentation_step: str | None = " " * 4, ): """ Generate code for a C++ switch statement. @@ -296,8 +300,8 @@ def csc_matrix( matrix: sp.Matrix, rownames: list[sp.Symbol], colnames: list[sp.Symbol], - identifier: Optional[int] = 0, - pattern_only: Optional[bool] = False, + identifier: int | None = 0, + pattern_only: bool | None = False, ) -> tuple[list[int], list[int], sp.Matrix, list[str], sp.Matrix]: """ Generates the sparse symbolic identifiers, symbolic identifiers, diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index fea9325ab2..6b1392a3d1 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -9,6 +9,7 @@ :py:func:`amici.sbml_import.SbmlImporter.sbml2amici` and :py:func:`amici.petab_import.import_model`. """ + from __future__ import annotations import copy import logging @@ -716,7 +717,9 @@ def _get_function_body( for ipar in range(self.model.num_par()): expressions = [] for index, formula in zip( - self.model._x0_fixedParameters_idx, equations[:, ipar] + self.model._x0_fixedParameters_idx, + equations[:, ipar], + strict=True, ): if not formula.is_zero: expressions.extend( @@ -734,7 +737,7 @@ def _get_function_body( elif function == "x0_fixedParameters": for index, formula in zip( - self.model._x0_fixedParameters_idx, equations + self.model._x0_fixedParameters_idx, equations, strict=True ): lines.append( f" if(std::find(reinitialization_state_idxs.cbegin(), " @@ -1140,36 +1143,36 @@ def _write_model_header_cpp(self) -> None: ] = impl continue - tpl_data[ - f"{func_name.upper()}_DEF" - ] = get_function_extern_declaration( - func_name, self.model_name, self.model.is_ode() + tpl_data[f"{func_name.upper()}_DEF"] = ( + get_function_extern_declaration( + func_name, self.model_name, self.model.is_ode() + ) ) - tpl_data[ - f"{func_name.upper()}_IMPL" - ] = get_model_override_implementation( - func_name, self.model_name, self.model.is_ode() + tpl_data[f"{func_name.upper()}_IMPL"] = ( + get_model_override_implementation( + func_name, self.model_name, self.model.is_ode() + ) ) if func_name in sparse_functions: - tpl_data[ - f"{func_name.upper()}_COLPTRS_DEF" - ] = get_sunindex_extern_declaration( - func_name, self.model_name, "colptrs" + tpl_data[f"{func_name.upper()}_COLPTRS_DEF"] = ( + get_sunindex_extern_declaration( + func_name, self.model_name, "colptrs" + ) ) - tpl_data[ - f"{func_name.upper()}_COLPTRS_IMPL" - ] = get_sunindex_override_implementation( - func_name, self.model_name, "colptrs" + tpl_data[f"{func_name.upper()}_COLPTRS_IMPL"] = ( + get_sunindex_override_implementation( + func_name, self.model_name, "colptrs" + ) ) - tpl_data[ - f"{func_name.upper()}_ROWVALS_DEF" - ] = get_sunindex_extern_declaration( - func_name, self.model_name, "rowvals" + tpl_data[f"{func_name.upper()}_ROWVALS_DEF"] = ( + get_sunindex_extern_declaration( + func_name, self.model_name, "rowvals" + ) ) - tpl_data[ - f"{func_name.upper()}_ROWVALS_IMPL" - ] = get_sunindex_override_implementation( - func_name, self.model_name, "rowvals" + tpl_data[f"{func_name.upper()}_ROWVALS_IMPL"] = ( + get_sunindex_override_implementation( + func_name, self.model_name, "rowvals" + ) ) if self.model.num_states_solver() == self.model.num_states_rdata(): diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index ea2807df52..0e48bf3af8 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -1,4 +1,5 @@ """Symbolic differential equation model.""" + from __future__ import annotations import contextlib @@ -6,7 +7,8 @@ import itertools import re from itertools import chain -from typing import Callable, TYPE_CHECKING +from typing import TYPE_CHECKING +from collections.abc import Callable from collections.abc import Sequence import numpy as np @@ -394,7 +396,9 @@ def states(self) -> list[State]: def _process_sbml_rate_of(self) -> None: """Substitute any SBML-rateOf constructs in the model equations""" rate_of_func = sp.core.function.UndefinedFunction("rateOf") - species_sym_to_xdot = dict(zip(self.sym("x"), self.sym("xdot"))) + species_sym_to_xdot = dict( + zip(self.sym("x"), self.sym("xdot"), strict=True) + ) species_sym_to_idx = {x: i for i, x in enumerate(self.sym("x"))} def get_rate(symbol: sp.Symbol): @@ -425,7 +429,7 @@ def get_rate(symbol: sp.Symbol): if made_substitutions: # substitute in topological order subs = toposort_symbols( - dict(zip(self.sym("xdot"), self.eq("xdot"))) + dict(zip(self.sym("xdot"), self.eq("xdot"), strict=True)) ) self._eqs["xdot"] = smart_subs_dict(self.eq("xdot"), subs) @@ -467,6 +471,7 @@ def get_rate(symbol: sp.Symbol): zip( self.sym("w")[self.num_cons_law() :, :], self.eq("w")[self.num_cons_law() :, :], + strict=True, ) ) ) @@ -475,6 +480,7 @@ def get_rate(symbol: sp.Symbol): zip( self.sym("w")[: self.num_cons_law(), :], self.eq("w")[: self.num_cons_law(), :], + strict=True, ) ) | w_sorted @@ -1025,7 +1031,9 @@ def static_indices(self, name: str) -> list[int]: # of non-zeros entries of the sparse matrix self._static_indices[name] = [ i - for i, (expr, row_idx) in enumerate(zip(sparseeq, rowvals)) + for i, (expr, row_idx) in enumerate( + zip(sparseeq, rowvals, strict=True) + ) # derivative of a static expression is static if row_idx in static_indices_w # constant expressions @@ -1394,6 +1402,8 @@ def _compute_equation(self, name: str) -> None: if not s.has_conservation_law() ), self.sym("dx"), + # dx contains extra elements for algebraic states + strict=False, ) ] + [eq.get_val() for eq in self._algebraic_equations] @@ -1551,14 +1561,18 @@ def _compute_equation(self, name: str) -> None: self._eqs[name] = smart_jacobian(self.eq("root"), time_symbol) elif name == "drootdt_total": - # backsubstitution of optimized right-hand side terms into RHS - # calling subs() is costly. Due to looping over events though, the - # following lines are only evaluated if a model has events - w_sorted = toposort_symbols(dict(zip(self.sym("w"), self.eq("w")))) - tmp_xdot = smart_subs_dict(self.eq("xdot"), w_sorted) self._eqs[name] = self.eq("drootdt") - if self.num_states_solver(): - self._eqs[name] += smart_multiply(self.eq("drootdx"), tmp_xdot) + # backsubstitution of optimized right-hand side terms into RHS + # calling subs() is costly. We can skip it if we don't have any + # state-dependent roots. + if self.num_states_solver() and not smart_is_zero_matrix( + drootdx := self.eq("drootdx") + ): + w_sorted = toposort_symbols( + dict(zip(self.sym("w"), self.eq("w"), strict=True)) + ) + tmp_xdot = smart_subs_dict(self.eq("xdot"), w_sorted) + self._eqs[name] += smart_multiply(drootdx, tmp_xdot) elif name == "deltax": # fill boluses for Heaviside functions, as empty state updates @@ -1584,7 +1598,7 @@ def _compute_equation(self, name: str) -> None: for event_obs in self._event_observables ] for (iz, ie), event_obs in zip( - enumerate(z2event), self._event_observables + enumerate(z2event), self._event_observables, strict=True ): event_observables[ie - 1][iz] = event_obs.get_val() @@ -1725,7 +1739,7 @@ def _compute_equation(self, name: str) -> None: syms_x = self.sym("x") syms_yz = self.sym(name.removeprefix("sigma")) xs_in_sigma = {} - for sym_yz, eq_yz in zip(syms_yz, self._eqs[name]): + for sym_yz, eq_yz in zip(syms_yz, self._eqs[name], strict=True): yz_free_syms = eq_yz.free_symbols if tmp := {x for x in syms_x if x in yz_free_syms}: xs_in_sigma[sym_yz] = tmp @@ -2227,6 +2241,7 @@ def _collect_heaviside_roots( zip( [expr.get_id() for expr in self._expressions], [expr.get_val() for expr in self._expressions], + strict=True, ) ) ) diff --git a/python/sdist/amici/de_model_components.py b/python/sdist/amici/de_model_components.py index eeeabb35db..bc93f44b87 100644 --- a/python/sdist/amici/de_model_components.py +++ b/python/sdist/amici/de_model_components.py @@ -1,7 +1,8 @@ """Objects for AMICI's internal differential equation model representation""" + import abc import numbers -from typing import Optional, SupportsFloat, Union +from typing import SupportsFloat import sympy as sp @@ -45,7 +46,7 @@ def __init__( self, identifier: sp.Symbol, name: str, - value: Union[SupportsFloat, numbers.Number, sp.Expr], + value: SupportsFloat | numbers.Number | sp.Expr, ): """ Create a new ModelQuantity instance. @@ -165,7 +166,7 @@ def __init__( self._ncoeff: sp.Expr = coefficients[state_id] super().__init__(identifier, name, value) - def get_ncoeff(self, state_id) -> Union[sp.Expr, int, float]: + def get_ncoeff(self, state_id) -> sp.Expr | int | float: """ Computes the normalized coefficient a_i/a_j where i is the index of the provided state_id and j is the index of the state that is @@ -216,7 +217,7 @@ class State(ModelQuantity): Base class for differential and algebraic model states """ - _conservation_law: Optional[ConservationLaw] = None + _conservation_law: ConservationLaw | None = None def get_x_rdata(self): """ @@ -323,7 +324,7 @@ def __init__( """ super().__init__(identifier, name, init) self._dt = cast_to_sym(dt, "dt") - self._conservation_law: Union[ConservationLaw, None] = None + self._conservation_law: ConservationLaw | None = None def set_conservation_law(self, law: ConservationLaw) -> None: """ @@ -394,17 +395,16 @@ class Observable(ModelQuantity): function or residuals """ - _measurement_symbol: Union[sp.Symbol, None] = None + _measurement_symbol: sp.Symbol | None = None def __init__( self, identifier: sp.Symbol, name: str, value: sp.Expr, - measurement_symbol: Optional[sp.Symbol] = None, - transformation: Optional[ - ObservableTransformation - ] = ObservableTransformation.LIN, + measurement_symbol: sp.Symbol | None = None, + transformation: None + | (ObservableTransformation) = ObservableTransformation.LIN, ): """ Create a new Observable instance. @@ -459,8 +459,8 @@ def __init__( name: str, value: sp.Expr, event: sp.Symbol, - measurement_symbol: Optional[sp.Symbol] = None, - transformation: Optional[ObservableTransformation] = "lin", + measurement_symbol: sp.Symbol | None = None, + transformation: ObservableTransformation | None = "lin", ): """ Create a new EventObservable instance. @@ -668,8 +668,8 @@ def __init__( identifier: sp.Symbol, name: str, value: sp.Expr, - state_update: Union[sp.Expr, None], - initial_value: Optional[bool] = True, + state_update: sp.Expr | None, + initial_value: bool | None = True, ): """ Create a new Event instance. diff --git a/python/sdist/amici/debugging/__init__.py b/python/sdist/amici/debugging/__init__.py index 81663d17b9..55b24e7b18 100644 --- a/python/sdist/amici/debugging/__init__.py +++ b/python/sdist/amici/debugging/__init__.py @@ -1,4 +1,5 @@ """Functions for debugging AMICI simulation failures.""" + import amici import numpy as np diff --git a/python/sdist/amici/gradient_check.py b/python/sdist/amici/gradient_check.py index c5ddb03749..019f091e86 100644 --- a/python/sdist/amici/gradient_check.py +++ b/python/sdist/amici/gradient_check.py @@ -6,7 +6,6 @@ """ import copy -from typing import Optional from collections.abc import Sequence import numpy as np @@ -31,9 +30,9 @@ def check_finite_difference( edata: ExpData, ip: int, fields: list[str], - atol: Optional[float] = 1e-4, - rtol: Optional[float] = 1e-4, - epsilon: Optional[float] = 1e-3, + atol: float | None = 1e-4, + rtol: float | None = 1e-4, + epsilon: float | None = 1e-3, ) -> None: """ Checks the computed sensitivity based derivatives against a finite @@ -138,10 +137,10 @@ def check_finite_difference( def check_derivatives( model: Model, solver: Solver, - edata: Optional[ExpData] = None, - atol: Optional[float] = 1e-4, - rtol: Optional[float] = 1e-4, - epsilon: Optional[float] = 1e-3, + edata: ExpData | None = None, + atol: float | None = 1e-4, + rtol: float | None = 1e-4, + epsilon: float | None = 1e-3, check_least_squares: bool = True, skip_zero_pars: bool = False, ) -> None: @@ -249,8 +248,8 @@ def _check_close( atol: float, rtol: float, field: str, - ip: Optional[int] = None, - verbose: Optional[bool] = True, + ip: int | None = None, + verbose: bool | None = True, ) -> None: """ Compares computed values against expected values and provides rich diff --git a/python/sdist/amici/import_utils.py b/python/sdist/amici/import_utils.py index 029c2cc6de..1a0dc782db 100644 --- a/python/sdist/amici/import_utils.py +++ b/python/sdist/amici/import_utils.py @@ -1,16 +1,16 @@ """Miscellaneous functions related to model import, independent of any specific - model format""" +model format""" + import enum import itertools as itt import numbers import sys from typing import ( Any, - Callable, - Optional, SupportsFloat, Union, ) +from collections.abc import Callable from collections.abc import Iterable, Sequence import sympy as sp @@ -69,7 +69,7 @@ class ObservableTransformation(str, enum.Enum): def noise_distribution_to_observable_transformation( - noise_distribution: Union[str, Callable], + noise_distribution: str | Callable, ) -> ObservableTransformation: """ Parse noise distribution string and extract observable transformation @@ -90,7 +90,7 @@ def noise_distribution_to_observable_transformation( def noise_distribution_to_cost_function( - noise_distribution: Union[str, Callable], + noise_distribution: str | Callable, ) -> Callable[[str], str]: """ Parse noise distribution string to a cost function definition amici can @@ -258,7 +258,7 @@ def _get_str_symbol_identifiers(str_symbol: str) -> tuple: def smart_subs_dict( sym: sp.Expr, subs: SymbolDef, - field: Optional[str] = None, + field: str | None = None, reverse: bool = True, ) -> sp.Expr: """ @@ -315,7 +315,7 @@ def smart_subs(element: sp.Expr, old: sp.Symbol, new: sp.Expr) -> sp.Expr: def toposort_symbols( - symbols: SymbolDef, field: Optional[str] = None + symbols: SymbolDef, field: str | None = None ) -> SymbolDef: """ Topologically sort symbol definitions according to their interdependency @@ -420,8 +420,8 @@ def _parse_special_functions(sym: sp.Expr, toplevel: bool = True) -> sp.Expr: def _denest_piecewise( - args: Sequence[Union[sp.Expr, sp.logic.boolalg.Boolean, bool]], -) -> tuple[Union[sp.Expr, sp.logic.boolalg.Boolean, bool]]: + args: Sequence[sp.Expr | sp.logic.boolalg.Boolean | bool], +) -> tuple[sp.Expr | sp.logic.boolalg.Boolean | bool]: """ Denest piecewise functions that contain piecewise as condition @@ -567,7 +567,7 @@ def grouper( def _check_unsupported_functions( - sym: sp.Expr, expression_type: str, full_sym: Optional[sp.Expr] = None + sym: sp.Expr, expression_type: str, full_sym: sp.Expr | None = None ): """ Recursively checks the symbolic expression for unsupported symbolic @@ -619,7 +619,7 @@ def _check_unsupported_functions( def cast_to_sym( - value: Union[SupportsFloat, sp.Expr, BooleanAtom], input_name: str + value: SupportsFloat | sp.Expr | BooleanAtom, input_name: str ) -> sp.Expr: """ Typecasts the value to :py:class:`sympy.Float` if possible, and ensures the @@ -647,7 +647,7 @@ def cast_to_sym( return value -def generate_measurement_symbol(observable_id: Union[str, sp.Symbol]): +def generate_measurement_symbol(observable_id: str | sp.Symbol): """ Generates the appropriate measurement symbol for the provided observable @@ -662,7 +662,7 @@ def generate_measurement_symbol(observable_id: Union[str, sp.Symbol]): return symbol_with_assumptions(f"m{observable_id}") -def generate_regularization_symbol(observable_id: Union[str, sp.Symbol]): +def generate_regularization_symbol(observable_id: str | sp.Symbol): """ Generates the appropriate regularization symbol for the provided observable @@ -678,7 +678,7 @@ def generate_regularization_symbol(observable_id: Union[str, sp.Symbol]): def generate_flux_symbol( - reaction_index: int, name: Optional[str] = None + reaction_index: int, name: str | None = None ) -> sp.Symbol: """ Generate identifier symbol for a reaction flux. diff --git a/python/sdist/amici/logging.py b/python/sdist/amici/logging.py index df39c4a219..1f5ae1f175 100644 --- a/python/sdist/amici/logging.py +++ b/python/sdist/amici/logging.py @@ -27,14 +27,14 @@ "CRITICAL": logging.CRITICAL, } -from typing import Callable, Optional, Union +from collections.abc import Callable def _setup_logger( - level: Optional[int] = logging.WARNING, - console_output: Optional[bool] = True, - file_output: Optional[bool] = False, - capture_warnings: Optional[bool] = False, + level: int | None = logging.WARNING, + console_output: bool | None = True, + file_output: bool | None = False, + capture_warnings: bool | None = False, ) -> logging.Logger: """ Set up a new :class:`logging.Logger` for AMICI logging. @@ -118,7 +118,7 @@ def _setup_logger( return log -def set_log_level(logger: logging.Logger, log_level: Union[int, bool]) -> None: +def set_log_level(logger: logging.Logger, log_level: int | bool) -> None: if log_level is not None and log_level is not False: if isinstance(log_level, bool): log_level = logging.DEBUG @@ -134,8 +134,8 @@ def set_log_level(logger: logging.Logger, log_level: Union[int, bool]) -> None: def get_logger( - logger_name: Optional[str] = BASE_LOGGER_NAME, - log_level: Optional[int] = None, + logger_name: str | None = BASE_LOGGER_NAME, + log_level: int | None = None, **kwargs, ) -> logging.Logger: """ @@ -175,7 +175,8 @@ def get_logger( elif kwargs: warnings.warn( "AMICI logger already exists, ignoring keyword " - "arguments to setup_logger" + "arguments to setup_logger", + stacklevel=2, ) logger = logging.getLogger(logger_name) diff --git a/python/sdist/amici/numpy.py b/python/sdist/amici/numpy.py index f40d0f4c6e..9aa03fc2dd 100644 --- a/python/sdist/amici/numpy.py +++ b/python/sdist/amici/numpy.py @@ -37,7 +37,7 @@ class is memory efficient as copies of the underlying C++ objects is _field_names: list[str] = [] _field_dimensions: dict[str, list[int]] = dict() - def __getitem__(self, item: str) -> Union[np.ndarray, float]: + def __getitem__(self, item: str) -> np.ndarray | float: """ Access to field names, copies data from C++ object into numpy array, reshapes according to field dimensions and stores values in @@ -76,7 +76,7 @@ def __missing__(self, key: str) -> None: """ raise KeyError(f"Unknown field name {key}.") - def __getattr__(self, item) -> Union[np.ndarray, float]: + def __getattr__(self, item) -> np.ndarray | float: """ Attribute accessor for field names @@ -245,7 +245,7 @@ class ReturnDataView(SwigPtrView): "t_last", ] - def __init__(self, rdata: Union[ReturnDataPtr, ReturnData]): + def __init__(self, rdata: ReturnDataPtr | ReturnData): """ Constructor @@ -309,7 +309,7 @@ def __init__(self, rdata: Union[ReturnDataPtr, ReturnData]): def __getitem__( self, item: str - ) -> Union[np.ndarray, ReturnDataPtr, ReturnData, float]: + ) -> np.ndarray | ReturnDataPtr | ReturnData | float: """ Access fields by name.s @@ -391,7 +391,7 @@ class ExpDataView(SwigPtrView): "fixedParametersPresimulation", ] - def __init__(self, edata: Union[ExpDataPtr, ExpData]): + def __init__(self, edata: ExpDataPtr | ExpData): """ Constructor @@ -429,7 +429,7 @@ def __init__(self, edata: Union[ExpDataPtr, ExpData]): def _field_as_numpy( field_dimensions: dict[str, list[int]], field: str, data: SwigPtrView -) -> Union[np.ndarray, float, None]: +) -> np.ndarray | float | None: """ Convert data object field to numpy array with dimensions according to specified field dimensions diff --git a/python/sdist/amici/pandas.py b/python/sdist/amici/pandas.py index b776d2d5ef..e83e524e08 100644 --- a/python/sdist/amici/pandas.py +++ b/python/sdist/amici/pandas.py @@ -7,7 +7,7 @@ import copy import math -from typing import Optional, SupportsFloat, Union +from typing import SupportsFloat, Union import amici import numpy as np @@ -70,7 +70,7 @@ def _process_rdata_list(rdata_list: ReturnDatas) -> list[amici.ReturnDataView]: def getDataObservablesAsDataFrame( - model: AmiciModel, edata_list: ExpDatas, by_id: Optional[bool] = False + model: AmiciModel, edata_list: ExpDatas, by_id: bool | None = False ) -> pd.DataFrame: """ Write Observables from experimental data as DataFrame. @@ -123,7 +123,7 @@ def getSimulationObservablesAsDataFrame( model: amici.Model, edata_list: ExpDatas, rdata_list: ReturnDatas, - by_id: Optional[bool] = False, + by_id: bool | None = False, ) -> pd.DataFrame: """ Write Observables from simulation results as DataFrame. @@ -155,7 +155,7 @@ def getSimulationObservablesAsDataFrame( # aggregate records dicts = [] - for edata, rdata in zip(edata_list, rdata_list): + for edata, rdata in zip(edata_list, rdata_list, strict=True): for i_time, timepoint in enumerate(rdata["t"]): datadict = { "time": timepoint, @@ -181,7 +181,7 @@ def getSimulationStatesAsDataFrame( model: amici.Model, edata_list: ExpDatas, rdata_list: ReturnDatas, - by_id: Optional[bool] = False, + by_id: bool | None = False, ) -> pd.DataFrame: """ Get model state according to lists of ReturnData and ExpData. @@ -212,7 +212,7 @@ def getSimulationStatesAsDataFrame( # aggregate records dicts = [] - for edata, rdata in zip(edata_list, rdata_list): + for edata, rdata in zip(edata_list, rdata_list, strict=True): for i_time, timepoint in enumerate(rdata["t"]): datadict = { "time": timepoint, @@ -237,7 +237,7 @@ def get_expressions_as_dataframe( model: amici.Model, edata_list: ExpDatas, rdata_list: ReturnDatas, - by_id: Optional[bool] = False, + by_id: bool | None = False, ) -> pd.DataFrame: """ Get values of model expressions from lists of ReturnData as DataFrame. @@ -268,7 +268,7 @@ def get_expressions_as_dataframe( # aggregate records dicts = [] - for edata, rdata in zip(edata_list, rdata_list): + for edata, rdata in zip(edata_list, rdata_list, strict=True): for i_time, timepoint in enumerate(rdata["t"]): datadict = { "time": timepoint, @@ -293,7 +293,7 @@ def getResidualsAsDataFrame( model: amici.Model, edata_list: ExpDatas, rdata_list: ReturnDatas, - by_id: Optional[bool] = False, + by_id: bool | None = False, ) -> pd.DataFrame: """ Convert a list of ReturnData and ExpData to pandas DataFrame with @@ -626,8 +626,8 @@ def _get_names_or_ids( def _get_specialized_fixed_parameters( model: AmiciModel, - condition: Union[dict[str, SupportsFloat], pd.Series], - overwrite: Union[dict[str, SupportsFloat], pd.Series], + condition: dict[str, SupportsFloat] | pd.Series, + overwrite: dict[str, SupportsFloat] | pd.Series, by_id: bool, ) -> list[float]: """ @@ -661,7 +661,7 @@ def constructEdataFromDataFrame( df: pd.DataFrame, model: AmiciModel, condition: pd.Series, - by_id: Optional[bool] = False, + by_id: bool | None = False, ) -> amici.amici.ExpData: """ Constructs an ExpData instance according to the provided Model @@ -778,7 +778,7 @@ def constructEdataFromDataFrame( def getEdataFromDataFrame( - model: AmiciModel, df: pd.DataFrame, by_id: Optional[bool] = False + model: AmiciModel, df: pd.DataFrame, by_id: bool | None = False ) -> list[amici.amici.ExpData]: """ Constructs a ExpData instances according to the provided Model and diff --git a/python/sdist/amici/parameter_mapping.py b/python/sdist/amici/parameter_mapping.py index b39d54c87e..dc369b448b 100644 --- a/python/sdist/amici/parameter_mapping.py +++ b/python/sdist/amici/parameter_mapping.py @@ -27,6 +27,7 @@ warnings.warn( "Importing amici.parameter_mapping is deprecated. Use `amici.petab.parameter_mapping` instead.", DeprecationWarning, + stacklevel=2, ) __all__ = [ diff --git a/python/sdist/amici/petab/conditions.py b/python/sdist/amici/petab/conditions.py index 34dd44cbcb..c0b702b69d 100644 --- a/python/sdist/amici/petab/conditions.py +++ b/python/sdist/amici/petab/conditions.py @@ -1,4 +1,5 @@ """PEtab conditions to AMICI ExpDatas.""" + import logging import numbers import warnings @@ -66,9 +67,12 @@ def fill_in_parameters( "The following problem parameters were not used: " + str(unused_parameters), RuntimeWarning, + stacklevel=2, ) - for edata, mapping_for_condition in zip(edatas, parameter_mapping): + for edata, mapping_for_condition in zip( + edatas, parameter_mapping, strict=True + ): fill_in_parameters_for_condition( edata, problem_parameters, @@ -218,7 +222,7 @@ def create_parameterized_edatas( problem_parameters: dict[str, numbers.Number], scaled_parameters: bool = False, parameter_mapping: ParameterMapping = None, - simulation_conditions: Union[pd.DataFrame, dict] = None, + simulation_conditions: pd.DataFrame | dict = None, ) -> list[amici.ExpData]: """Create list of :class:amici.ExpData objects with parameters filled in. @@ -284,7 +288,7 @@ def create_parameterized_edatas( def create_edata_for_condition( - condition: Union[dict, pd.Series], + condition: dict | pd.Series, measurement_df: pd.DataFrame, amici_model: AmiciModel, petab_problem: petab.Problem, @@ -369,7 +373,7 @@ def create_edata_for_condition( def create_edatas( amici_model: AmiciModel, petab_problem: petab.Problem, - simulation_conditions: Union[pd.DataFrame, dict] = None, + simulation_conditions: pd.DataFrame | dict = None, ) -> list[amici.ExpData]: """Create list of :class:`amici.amici.ExpData` objects for PEtab problem. @@ -515,7 +519,7 @@ def _get_measurements_and_sigmas( if isinstance( measurement.get(NOISE_PARAMETERS, None), numbers.Number ): - sigma_y[ - time_ix_for_obs_ix[observable_ix], observable_ix - ] = measurement[NOISE_PARAMETERS] + sigma_y[time_ix_for_obs_ix[observable_ix], observable_ix] = ( + measurement[NOISE_PARAMETERS] + ) return y, sigma_y diff --git a/python/sdist/amici/petab/import_helpers.py b/python/sdist/amici/petab/import_helpers.py index 3caf951ace..29cbff07e6 100644 --- a/python/sdist/amici/petab/import_helpers.py +++ b/python/sdist/amici/petab/import_helpers.py @@ -2,12 +2,12 @@ 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 @@ -30,9 +30,7 @@ def get_observation_model( observable_df: pd.DataFrame, -) -> tuple[ - dict[str, dict[str, str]], dict[str, str], dict[str, Union[str, float]] -]: +) -> tuple[dict[str, dict[str, str]], dict[str, str], dict[str, str | float]]: """ Get observables, sigmas, and noise distributions from PEtab observation table in a format suitable for @@ -128,7 +126,7 @@ def petab_scale_to_amici_scale(scale_str: str) -> int: raise ValueError(f"Invalid parameter scale {scale_str}") -def _create_model_name(folder: Union[str, Path]) -> str: +def _create_model_name(folder: str | Path) -> str: """ Create a name for the model. Just re-use the last part of the folder. @@ -136,9 +134,7 @@ def _create_model_name(folder: Union[str, Path]) -> str: return os.path.split(os.path.normpath(folder))[-1] -def _can_import_model( - model_name: str, model_output_dir: Union[str, Path] -) -> bool: +def _can_import_model(model_name: str, model_output_dir: str | Path) -> bool: """ Check whether a module of that name can already be imported. """ diff --git a/python/sdist/amici/petab/parameter_mapping.py b/python/sdist/amici/petab/parameter_mapping.py index 369ad13a96..54930073da 100644 --- a/python/sdist/amici/petab/parameter_mapping.py +++ b/python/sdist/amici/petab/parameter_mapping.py @@ -385,7 +385,7 @@ def create_parameter_mapping( parameter_mapping = ParameterMapping() for (_, condition), prelim_mapping_for_condition in zip( - simulation_conditions.iterrows(), prelim_parameter_mapping + simulation_conditions.iterrows(), prelim_parameter_mapping, strict=True ): mapping_for_condition = create_parameter_mapping_for_condition( prelim_mapping_for_condition, condition, petab_problem, amici_model diff --git a/python/sdist/amici/petab/petab_import.py b/python/sdist/amici/petab/petab_import.py index cb896b39e3..0e63496d75 100644 --- a/python/sdist/amici/petab/petab_import.py +++ b/python/sdist/amici/petab/petab_import.py @@ -9,7 +9,6 @@ import os import shutil from pathlib import Path -from typing import Union from warnings import warn import amici @@ -34,7 +33,7 @@ def import_petab_problem( petab_problem: petab.Problem, - model_output_dir: Union[str, Path, None] = None, + model_output_dir: str | Path | None = None, model_name: str = None, compile_: bool = None, non_estimated_parameters_as_constants=True, diff --git a/python/sdist/amici/petab/petab_problem.py b/python/sdist/amici/petab/petab_problem.py index 8ea177ad03..618b8b5247 100644 --- a/python/sdist/amici/petab/petab_problem.py +++ b/python/sdist/amici/petab/petab_problem.py @@ -1,6 +1,6 @@ """PEtab-problem based simulations.""" + import copy -from typing import Optional, Union import amici import pandas as pd @@ -36,10 +36,10 @@ class PetabProblem: def __init__( self, petab_problem: petab.Problem, - amici_model: Optional[amici.Model] = None, - problem_parameters: Optional[dict[str, float]] = None, + amici_model: amici.Model | None = None, + problem_parameters: dict[str, float] | None = None, scaled_parameters: bool = False, - simulation_conditions: Union[pd.DataFrame, list[dict]] = None, + simulation_conditions: pd.DataFrame | list[dict] = None, store_edatas: bool = True, ): self._petab_problem = copy.deepcopy(petab_problem) @@ -63,9 +63,9 @@ def __init__( if ( preeq_id := PREEQUILIBRATION_CONDITION_ID ) in self._simulation_conditions: - self._simulation_conditions[ - preeq_id - ] = self._simulation_conditions[preeq_id].fillna("") + self._simulation_conditions[preeq_id] = ( + self._simulation_conditions[preeq_id].fillna("") + ) if problem_parameters is None: # Use PEtab nominal values as default @@ -102,7 +102,10 @@ def set_parameters( :param scaled_parameters: Whether the provided parameters are on PEtab `parameterScale` or not. """ - if scaled_parameters != self._scaled_parameters and self._parameter_mapping is not None: + if ( + scaled_parameters != self._scaled_parameters + and self._parameter_mapping is not None + ): # redo parameter mapping if scale changed self._parameter_mapping = create_parameter_mapping( petab_problem=self._petab_problem, diff --git a/python/sdist/amici/petab/pysb_import.py b/python/sdist/amici/petab/pysb_import.py index 8c67bb0785..f5b1c84dbf 100644 --- a/python/sdist/amici/petab/pysb_import.py +++ b/python/sdist/amici/petab/pysb_import.py @@ -8,7 +8,6 @@ import logging import re from pathlib import Path -from typing import Optional, Union import petab import pysb @@ -56,6 +55,7 @@ def _add_observation_model( petab_problem.observable_df.index, petab_problem.observable_df[OBSERVABLE_FORMULA], petab_problem.observable_df[NOISE_FORMULA], + strict=True, ): obs_symbol = sp.sympify(observable_formula, locals=local_syms) if observable_id in pysb_model.expressions.keys(): @@ -165,9 +165,9 @@ def _add_initialization_variables( @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, + model_output_dir: str | Path | None = None, + verbose: bool | int | None = True, + model_name: str | None = None, **kwargs, ) -> None: """ diff --git a/python/sdist/amici/petab/sbml_import.py b/python/sdist/amici/petab/sbml_import.py index 2484d57a7a..2c43c3adc4 100644 --- a/python/sdist/amici/petab/sbml_import.py +++ b/python/sdist/amici/petab/sbml_import.py @@ -4,7 +4,7 @@ import tempfile from itertools import chain from pathlib import Path -from typing import Optional, Union +from typing import Union from warnings import warn import amici @@ -31,17 +31,17 @@ @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, + condition_table: str | Path | pd.DataFrame | None = None, + observable_table: str | Path | pd.DataFrame | None = None, + measurement_table: str | Path | pd.DataFrame | None = 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, + model_name: str | None = None, + model_output_dir: str | Path | None = None, + verbose: bool | int | None = True, allow_reinit_fixpar_initcond: bool = True, validate: bool = True, non_estimated_parameters_as_constants=True, - output_parameter_defaults: Optional[dict[str, float]] = None, + output_parameter_defaults: dict[str, float] | None = None, discard_sbml_annotations: bool = False, **kwargs, ) -> amici.SbmlImporter: @@ -545,7 +545,7 @@ def _get_fixed_parameters_sbml( def _create_model_output_dir_name( - sbml_model: "libsbml.Model", model_name: Optional[str] = None + sbml_model: "libsbml.Model", model_name: str | None = None ) -> Path: """ Find a folder for storing the compiled amici model. diff --git a/python/sdist/amici/petab/simulations.py b/python/sdist/amici/petab/simulations.py index 0f9aae3bfd..e80e40a94b 100644 --- a/python/sdist/amici/petab/simulations.py +++ b/python/sdist/amici/petab/simulations.py @@ -3,9 +3,10 @@ Functionality related to running simulations or evaluating the objective function as defined by a PEtab problem. """ + import copy import logging -from typing import Any, Optional, Union +from typing import Any from collections.abc import Sequence import amici @@ -72,12 +73,12 @@ def simulate_petab( petab_problem: petab.Problem, amici_model: AmiciModel, - solver: Optional[amici.Solver] = None, - problem_parameters: Optional[dict[str, float]] = None, - simulation_conditions: Union[pd.DataFrame, dict] = None, + solver: amici.Solver | None = None, + problem_parameters: dict[str, float] | None = None, + simulation_conditions: pd.DataFrame | dict = None, edatas: list[AmiciExpData] = None, parameter_mapping: ParameterMapping = None, - scaled_parameters: Optional[bool] = False, + scaled_parameters: bool | None = False, log_level: int = logging.WARNING, num_threads: int = 1, failfast: bool = True, @@ -171,6 +172,7 @@ def simulate_petab( zip( petab_problem.x_ids, petab_problem.x_nominal_scaled, + strict=True, ) ) # depending on `fill_fixed_parameters` for parameter mapping, the @@ -262,11 +264,11 @@ def simulate_petab( def aggregate_sllh( amici_model: AmiciModel, rdatas: Sequence[amici.ReturnDataView], - parameter_mapping: Optional[ParameterMapping], + parameter_mapping: ParameterMapping | None, edatas: list[AmiciExpData], petab_scale: bool = True, petab_problem: petab.Problem = None, -) -> Union[None, dict[str, float]]: +) -> None | dict[str, float]: """ Aggregate likelihood gradient for all conditions, according to PEtab parameter mapping. @@ -310,7 +312,7 @@ def aggregate_sllh( ) for condition_parameter_mapping, edata, rdata in zip( - parameter_mapping, edatas, rdatas + parameter_mapping, edatas, rdatas, strict=True ): for sllh_parameter_index, condition_parameter_sllh in enumerate( rdata.sllh @@ -432,7 +434,9 @@ def rdatas_to_measurement_df( observable_ids = model.getObservableIds() rows = [] # iterate over conditions - for (_, condition), rdata in zip(simulation_conditions.iterrows(), rdatas): + for (_, condition), rdata in zip( + simulation_conditions.iterrows(), rdatas, strict=True + ): # current simulation matrix y = rdata.y # time array used in rdata diff --git a/python/sdist/amici/petab/simulator.py b/python/sdist/amici/petab/simulator.py index a5f50112cc..9c655b1483 100644 --- a/python/sdist/amici/petab/simulator.py +++ b/python/sdist/amici/petab/simulator.py @@ -11,7 +11,7 @@ import inspect import sys -from typing import Callable +from collections.abc import Callable import pandas as pd import petab diff --git a/python/sdist/amici/petab/util.py b/python/sdist/amici/petab/util.py index 742f7bdfe3..e30b829a04 100644 --- a/python/sdist/amici/petab/util.py +++ b/python/sdist/amici/petab/util.py @@ -1,6 +1,7 @@ """Various helper functions for working with PEtab problems.""" + import re -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import libsbml import pandas as pd @@ -15,9 +16,9 @@ def get_states_in_condition_table( petab_problem: petab.Problem, - condition: Union[dict, pd.Series] = None, + condition: dict | pd.Series = None, return_patterns: bool = False, -) -> dict[str, tuple[Union[float, str, None], Union[float, str, None]]]: +) -> dict[str, tuple[float | str | None, float | str | None]]: """Get states and their initial condition as specified in the condition table. Returns: Dictionary: ``stateId -> (initial condition simulation, initial condition preequilibration)`` diff --git a/python/sdist/amici/petab_import.py b/python/sdist/amici/petab_import.py index d5c67753ac..1bcadaa1d2 100644 --- a/python/sdist/amici/petab_import.py +++ b/python/sdist/amici/petab_import.py @@ -7,11 +7,13 @@ .. deprecated:: 0.21.0 Use :mod:`amici.petab` instead. """ + import warnings warnings.warn( "Importing amici.petab_import is deprecated. Use `amici.petab` instead.", DeprecationWarning, + stacklevel=2, ) from .petab.import_helpers import ( # noqa # pylint: disable=unused-import diff --git a/python/sdist/amici/petab_import_pysb.py b/python/sdist/amici/petab_import_pysb.py index 595018f208..4d73a4bab3 100644 --- a/python/sdist/amici/petab_import_pysb.py +++ b/python/sdist/amici/petab_import_pysb.py @@ -4,6 +4,7 @@ .. deprecated:: 0.21.0 Use :mod:`amici.petab.pysb_import` instead. """ + import warnings from .petab.pysb_import import * # noqa: F401, F403 @@ -13,6 +14,7 @@ warnings.warn( "Importing amici.petab_import_pysb is deprecated. Use `amici.petab.pysb_import` instead.", DeprecationWarning, + stacklevel=2, ) __all__ = [ diff --git a/python/sdist/amici/petab_objective.py b/python/sdist/amici/petab_objective.py index 01724b7a7d..6d0dc44d84 100644 --- a/python/sdist/amici/petab_objective.py +++ b/python/sdist/amici/petab_objective.py @@ -12,6 +12,7 @@ warnings.warn( f"Importing {__name__} is deprecated. Use `amici.petab.simulations` instead.", DeprecationWarning, + stacklevel=2, ) from .petab.conditions import fill_in_parameters # noqa: F401 diff --git a/python/sdist/amici/petab_simulate.py b/python/sdist/amici/petab_simulate.py index 2dd25a8e4a..5f81d02a93 100644 --- a/python/sdist/amici/petab_simulate.py +++ b/python/sdist/amici/petab_simulate.py @@ -11,6 +11,7 @@ warnings.warn( f"Importing {__name__} is deprecated. Use `amici.petab.simulator` instead.", DeprecationWarning, + stacklevel=2, ) from .petab.simulator import PetabSimulator # noqa: F401 diff --git a/python/sdist/amici/petab_util.py b/python/sdist/amici/petab_util.py index ff202bf2e0..cf12a7411d 100644 --- a/python/sdist/amici/petab_util.py +++ b/python/sdist/amici/petab_util.py @@ -15,6 +15,7 @@ warnings.warn( f"Importing {__name__} is deprecated. Use `amici.petab.util` instead.", DeprecationWarning, + stacklevel=2, ) __all__ = [ diff --git a/python/sdist/amici/plotting.py b/python/sdist/amici/plotting.py index 19dbe05f89..3067d26bc5 100644 --- a/python/sdist/amici/plotting.py +++ b/python/sdist/amici/plotting.py @@ -3,7 +3,7 @@ -------- Plotting related functions """ -from typing import Optional, Union + from collections.abc import Iterable, Sequence import matplotlib.pyplot as plt @@ -19,8 +19,8 @@ def plot_state_trajectories( rdata: ReturnDataView, - state_indices: Optional[Sequence[int]] = None, - ax: Optional[Axes] = None, + state_indices: Sequence[int] | None = None, + ax: Axes | None = None, model: Model = None, prefer_names: bool = True, marker=None, @@ -65,7 +65,7 @@ def plot_state_trajectories( else: labels = np.asarray(rdata.ptr.state_ids)[list(state_indices)] - for ix, label in zip(state_indices, labels): + for ix, label in zip(state_indices, labels, strict=True): ax.plot(rdata["t"], rdata["x"][:, ix], marker=marker, label=label) ax.set_xlabel("$t$") @@ -76,12 +76,12 @@ def plot_state_trajectories( def plot_observable_trajectories( rdata: ReturnDataView, - observable_indices: Optional[Iterable[int]] = None, - ax: Optional[Axes] = None, + observable_indices: Iterable[int] | None = None, + ax: Axes | None = None, model: Model = None, prefer_names: bool = True, marker=None, - edata: Union[amici.ExpData, amici.ExpDataView] = None, + edata: amici.ExpData | amici.ExpDataView = None, ) -> None: """ Plot observable trajectories. @@ -131,7 +131,7 @@ def plot_observable_trajectories( else: labels = np.asarray(rdata.ptr.observable_ids)[list(observable_indices)] - for iy, label in zip(observable_indices, labels): + for iy, label in zip(observable_indices, labels, strict=True): (l,) = ax.plot( rdata["t"], rdata["y"][:, iy], marker=marker, label=label ) @@ -175,7 +175,7 @@ def plot_jacobian(rdata: ReturnDataView): def plot_expressions( - exprs: Union[Sequence[StrOrExpr], StrOrExpr], rdata: ReturnDataView + exprs: Sequence[StrOrExpr] | StrOrExpr, rdata: ReturnDataView ) -> None: """Plot the given expressions evaluated on the given simulation outputs. diff --git a/python/sdist/amici/pysb_import.py b/python/sdist/amici/pysb_import.py index 94aad595d9..1a21fef1ca 100644 --- a/python/sdist/amici/pysb_import.py +++ b/python/sdist/amici/pysb_import.py @@ -12,10 +12,9 @@ from pathlib import Path from typing import ( Any, - Callable, - Optional, Union, ) +from collections.abc import Callable from collections.abc import Iterable import numpy as np @@ -53,12 +52,12 @@ def pysb2amici( model: pysb.Model, - output_dir: Optional[Union[str, Path]] = None, + output_dir: str | Path | None = None, observables: list[str] = None, constant_parameters: list[str] = None, sigmas: dict[str, str] = None, - noise_distributions: Optional[dict[str, Union[str, Callable]]] = None, - verbose: Union[int, bool] = False, + noise_distributions: dict[str, str | Callable] | None = None, + verbose: int | bool = False, assume_pow_positivity: bool = False, compiler: str = None, compute_conservation_laws: bool = True, @@ -68,7 +67,7 @@ def pysb2amici( # See https://github.com/AMICI-dev/AMICI/pull/1672 cache_simplify: bool = False, generate_sensitivity_code: bool = True, - model_name: Optional[str] = None, + model_name: str | None = None, ): r""" Generate AMICI C++ files for the provided model. @@ -194,13 +193,13 @@ def ode_model_from_pysb_importer( constant_parameters: list[str] = None, observables: list[str] = None, sigmas: dict[str, str] = None, - noise_distributions: Optional[dict[str, Union[str, Callable]]] = None, + noise_distributions: dict[str, str | Callable] | None = None, compute_conservation_laws: bool = True, simplify: Callable = sp.powsimp, # Do not enable by default without testing. # See https://github.com/AMICI-dev/AMICI/pull/1672 cache_simplify: bool = False, - verbose: Union[int, bool] = False, + verbose: int | bool = False, ) -> DEModel: """ Creates an :class:`amici.DEModel` instance from a :class:`pysb.Model` @@ -441,7 +440,7 @@ def _process_pysb_expressions( ode_model: DEModel, observables: list[str], sigmas: dict[str, str], - noise_distributions: Optional[dict[str, Union[str, Callable]]] = None, + noise_distributions: dict[str, str | Callable] | None = None, ) -> None: r""" Converts pysb expressions/observables into Observables (with @@ -506,7 +505,7 @@ def _add_expression( ode_model: DEModel, observables: list[str], sigmas: dict[str, str], - noise_distributions: Optional[dict[str, Union[str, Callable]]] = None, + noise_distributions: dict[str, str | Callable] | None = None, ): """ Adds expressions to the ODE model given and adds observables/sigmas if @@ -564,7 +563,11 @@ def _add_expression( cost_fun_expr = sp.sympify( cost_fun_str, locals=dict( - zip(_get_str_symbol_identifiers(name), (y, my, sigma)) + zip( + _get_str_symbol_identifiers(name), + (y, my, sigma), + strict=True, + ) ), ) ode_model.add_component( @@ -621,7 +624,7 @@ def _process_pysb_observables( ode_model: DEModel, observables: list[str], sigmas: dict[str, str], - noise_distributions: Optional[dict[str, Union[str, Callable]]] = None, + noise_distributions: dict[str, str | Callable] | None = None, ) -> None: """ Converts :class:`pysb.core.Observable` into @@ -1349,7 +1352,7 @@ def has_fixed_parameter_ic( def extract_monomers( - complex_patterns: Union[pysb.ComplexPattern, list[pysb.ComplexPattern]], + complex_patterns: pysb.ComplexPattern | list[pysb.ComplexPattern], ) -> list[str]: """ Constructs a list of monomer names contained in complex patterns. @@ -1415,8 +1418,8 @@ def _get_unconserved_monomers( def _get_changed_stoichiometries( - reactants: Union[pysb.ComplexPattern, list[pysb.ComplexPattern]], - products: Union[pysb.ComplexPattern, list[pysb.ComplexPattern]], + reactants: pysb.ComplexPattern | list[pysb.ComplexPattern], + products: pysb.ComplexPattern | list[pysb.ComplexPattern], ) -> set[str]: """ Constructs the set of monomer names which have different @@ -1444,7 +1447,7 @@ def _get_changed_stoichiometries( return changed_stoichiometries -def pysb_model_from_path(pysb_model_file: Union[str, Path]) -> pysb.Model: +def pysb_model_from_path(pysb_model_file: str | Path) -> pysb.Model: """Load a pysb model module and return the :class:`pysb.Model` instance :param pysb_model_file: Full or relative path to the PySB model module diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 8f7f67b02f..61ce9a0ee1 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -4,6 +4,7 @@ This module provides all necessary functionality to import a model specified in the `Systems Biology Markup Language (SBML) `_. """ + import copy import itertools as itt import logging @@ -15,10 +16,9 @@ from pathlib import Path from typing import ( Any, - Callable, - Optional, Union, ) +from collections.abc import Callable from collections.abc import Iterable, Sequence import libsbml as sbml @@ -134,7 +134,7 @@ class SbmlImporter: def __init__( self, - sbml_source: Union[str, Path, sbml.Model], + sbml_source: str | Path | sbml.Model, show_sbml_warnings: bool = False, from_file: bool = True, discard_annotations: bool = False, @@ -177,7 +177,7 @@ def __init__( # Long and short names for model components self.symbols: dict[SymbolId, dict[sp.Symbol, dict[str, Any]]] = {} - self._local_symbols: dict[str, Union[sp.Expr, sp.Function]] = {} + self._local_symbols: dict[str, sp.Expr | sp.Function] = {} self.compartments: SymbolicFormula = {} self.compartment_assignment_rules: SymbolicFormula = {} self.species_assignment_rules: SymbolicFormula = {} @@ -269,21 +269,21 @@ def _reset_symbols(self) -> None: def sbml2amici( self, model_name: str, - output_dir: Union[str, Path] = None, + output_dir: str | Path = None, observables: dict[str, dict[str, str]] = None, event_observables: dict[str, dict[str, str]] = None, constant_parameters: Iterable[str] = None, - sigmas: dict[str, Union[str, float]] = None, - event_sigmas: dict[str, Union[str, float]] = None, - noise_distributions: dict[str, Union[str, Callable]] = None, - event_noise_distributions: dict[str, Union[str, Callable]] = None, - verbose: Union[int, bool] = logging.ERROR, + sigmas: dict[str, str | float] = None, + event_sigmas: dict[str, str | float] = None, + noise_distributions: dict[str, str | Callable] = None, + event_noise_distributions: dict[str, str | Callable] = None, + verbose: int | bool = logging.ERROR, assume_pow_positivity: bool = False, compiler: str = None, allow_reinit_fixpar_initcond: bool = True, compile: bool = True, compute_conservation_laws: bool = True, - simplify: Optional[Callable] = _default_simplify, + simplify: Callable | None = _default_simplify, cache_simplify: bool = False, log_as_log10: bool = True, generate_sensitivity_code: bool = True, @@ -442,7 +442,8 @@ def sbml2amici( if not has_clibs: warnings.warn( "AMICI C++ extensions have not been built. " - "Generated model code, but unable to compile." + "Generated model code, but unable to compile.", + stacklevel=2, ) exporter.compile_model() @@ -451,13 +452,13 @@ def _build_ode_model( observables: dict[str, dict[str, str]] = None, event_observables: dict[str, dict[str, str]] = None, constant_parameters: Iterable[str] = None, - sigmas: dict[str, Union[str, float]] = None, - event_sigmas: dict[str, Union[str, float]] = None, - noise_distributions: dict[str, Union[str, Callable]] = None, - event_noise_distributions: dict[str, Union[str, Callable]] = None, - verbose: Union[int, bool] = logging.ERROR, + sigmas: dict[str, str | float] = None, + event_sigmas: dict[str, str | float] = None, + noise_distributions: dict[str, str | Callable] = None, + event_noise_distributions: dict[str, str | Callable] = None, + verbose: int | bool = logging.ERROR, compute_conservation_laws: bool = True, - simplify: Optional[Callable] = _default_simplify, + simplify: Callable | None = _default_simplify, cache_simplify: bool = False, log_as_log10: bool = True, hardcode_symbols: Sequence[str] = None, @@ -556,9 +557,18 @@ def _build_ode_model( dxdt = smart_multiply( self.stoichiometric_matrix, MutableDenseMatrix(fluxes) ) + # dxdt has algebraic states at the end + assert dxdt.shape[0] - len(self.symbols[SymbolId.SPECIES]) == len( + self.symbols.get(SymbolId.ALGEBRAIC_STATE, []) + ), ( + self.symbols.get(SymbolId.SPECIES), + dxdt, + self.symbols.get(SymbolId.ALGEBRAIC_STATE), + ) + # correct time derivatives for compartment changes for ix, ((species_id, species), formula) in enumerate( - zip(self.symbols[SymbolId.SPECIES].items(), dxdt) + zip(self.symbols[SymbolId.SPECIES].items(), dxdt, strict=False) ): # rate rules and amount species don't need to be updated if "dt" in species: @@ -604,7 +614,7 @@ def _build_ode_model( # add fluxes as expressions, this needs to happen after base # expressions from symbols have been parsed - for flux_id, flux in zip(fluxes, self.flux_vector): + for flux_id, flux in zip(fluxes, self.flux_vector, strict=True): # replace splines inside fluxes flux = flux.subs(spline_subs) ode_model.add_component( @@ -944,6 +954,7 @@ def _process_species(self) -> None: } self._convert_event_assignment_parameter_targets_to_species() + self._convert_event_assignment_compartment_targets_to_species() self._process_species_initial() self._process_rate_rules() @@ -981,7 +992,9 @@ def _process_species_initial(self): self.symbols[SymbolId.SPECIES], "init" ) for species, rateof_dummies in zip( - self.symbols[SymbolId.SPECIES].values(), all_rateof_dummies + self.symbols[SymbolId.SPECIES].values(), + all_rateof_dummies, + strict=True, ): species["init"] = _dummy_to_rateof( smart_subs_dict(species["init"], sorted_species, "init"), @@ -1043,7 +1056,7 @@ def add_d_dt( self, d_dt: sp.Expr, variable: sp.Symbol, - variable0: Union[float, sp.Expr], + variable0: float | sp.Expr, name: str, ) -> None: """ @@ -1552,6 +1565,36 @@ def _convert_event_assignment_parameter_targets_to_species(self): "dt": sp.Float(0), } + def _convert_event_assignment_compartment_targets_to_species(self): + """Find compartments that are event assignment targets and convert + those compartments to species.""" + for event in self.sbml.getListOfEvents(): + for event_assignment in event.getListOfEventAssignments(): + if event_assignment.getMath() is None: + # Ignore event assignments with no change in value. + continue + variable = symbol_with_assumptions( + event_assignment.getVariable() + ) + if variable not in self.compartments: + continue + if variable in self.symbols[SymbolId.SPECIES]: + # Compartments with rate rules are already present as + # species + continue + + self.symbols[SymbolId.SPECIES][variable] = { + "name": str(variable), + "init": self.compartments[variable], + # 'compartment': None, # can ignore for amounts + "constant": False, + "amount": True, + # 'conversion_factor': 1.0, # can be ignored + "index": len(self.symbols[SymbolId.SPECIES]), + "dt": sp.Float(0), + } + del self.compartments[variable] + @log_execution_time("processing SBML events", logger) def _process_events(self) -> None: """Process SBML events.""" @@ -1582,6 +1625,10 @@ def get_empty_bolus_value() -> sp.Float: species_def["compartment"] ].append(species) + # Currently, all event assignment targets must exist in + # self.symbols[SymbolId.SPECIES] + state_vector = list(self.symbols[SymbolId.SPECIES].keys()) + for ievent, event in enumerate(events): # get the event id (which is optional unfortunately) event_id = event.getId() @@ -1594,14 +1641,12 @@ def get_empty_bolus_value() -> sp.Float: trigger_sym = self._sympy_from_sbml_math(trigger_sbml) trigger = _parse_event_trigger(trigger_sym) - # Currently, all event assignment targets must exist in - # self.symbols[SymbolId.SPECIES] - state_vector = list(self.symbols[SymbolId.SPECIES].keys()) - # parse the boluses / event assignments bolus = [get_empty_bolus_value() for _ in state_vector] event_assignments = event.getListOfEventAssignments() - compartment_event_assignments = set() + compartment_event_assignments: set[tuple[sp.Symbol, sp.Expr]] = ( + set() + ) for event_assignment in event_assignments: variable_sym = symbol_with_assumptions( event_assignment.getVariable() @@ -1619,7 +1664,7 @@ def get_empty_bolus_value() -> sp.Float: "Could not process event assignment for " f"{str(variable_sym)}. AMICI currently only allows " "event assignments to species; parameters; or, " - "compartments with rate rules, at the moment." + "compartments." ) try: # Try working with the formula now to detect errors @@ -1632,7 +1677,7 @@ def get_empty_bolus_value() -> sp.Float: "expressions as event assignments." ) if variable_sym in concentration_species_by_compartment: - compartment_event_assignments.add(variable_sym) + compartment_event_assignments.add((variable_sym, formula)) for ( comp, @@ -1640,15 +1685,15 @@ def get_empty_bolus_value() -> sp.Float: ) in self.compartment_assignment_rules.items(): if variable_sym not in assignment.free_symbols: continue - compartment_event_assignments.add(comp) + compartment_event_assignments.add((comp, formula)) # Update the concentration of species with concentration units # in compartments that were affected by the event assignments. - for compartment_sym in compartment_event_assignments: + for compartment_sym, formula in compartment_event_assignments: for species_sym in concentration_species_by_compartment[ compartment_sym ]: - # If the species was not affected by an event assignment + # If the species was not affected by an event assignment, # then the old value should be updated. if ( bolus[state_vector.index(species_sym)] @@ -1691,18 +1736,93 @@ def get_empty_bolus_value() -> sp.Float: " algebraic rules." ) + # Store `useValuesFromTriggerTime` attribute for checking later + # Since we assume valid in SBML models here, this attribute is + # either given (mandatory in L3), or defaults to True (L2) + use_trig_val = ( + event.getUseValuesFromTriggerTime() + if event.isSetUseValuesFromTriggerTime() + else True + ) + self.symbols[SymbolId.EVENT][event_sym] = { "name": event_id, "value": trigger, "state_update": sp.MutableDenseMatrix(bolus), "initial_value": initial_value, + "use_values_from_trigger_time": use_trig_val, } + # Check `useValuesFromTriggerTime` attribute + # AMICI does not support events with + # `useValuesFromTriggerTime=true`, unless + # 1) there is only a single event + # 2) there are multiple events, but they are guaranteed to not + # trigger at the same time + # 3) event assignments from events triggering at the same time + # are independent + # in these cases, the attribute value doesn't matter, as long + # as we don't support delays. + # We can't check this in `check_event_support` without already + # processing all trigger expressions, so we do it here + + # are there any events with `useValuesFromTriggerTime=true`? + if len(self.symbols[SymbolId.EVENT]) <= 1 or not any( + event["use_values_from_trigger_time"] + for event in self.symbols[SymbolId.EVENT].values() + ): + return + + # check if events are guaranteed to not trigger at the same time + trigger_times = [ + sp.solve(event["value"], sbml_time_symbol) + for event_sym, event in self.symbols[SymbolId.EVENT].items() + ] + # for now, we only check for single/fixed/unique time points, but there + # are probably other cases we could cover + if all(len(ts) == 1 and ts[0].is_Number for ts in trigger_times): + trigger_times = [ts[0] for ts in trigger_times] + if len(trigger_times) == len(set(trigger_times)): + # all trigger times are unique + return + + # If all events assign to different species, we are fine. This is the + # case if the list of assigned-to variables across all events contains + # only unique values. + assigned_to_species = [ + variable + for event in self.symbols[SymbolId.EVENT].values() + for variable, update in zip(state_vector, event["state_update"]) + if not update.is_zero + ] + if len(assigned_to_species) == len(set(assigned_to_species)): + return + + # if all assignments are absolute (not referring to other non-constant + # model entities), we are fine. + if all( + update.is_zero or (update + variable).is_Number + for event in self.symbols[SymbolId.EVENT].values() + for variable, update in zip(state_vector, event["state_update"]) + if not update.is_zero + ): + return + + raise SBMLException( + "Events with `useValuesFromTriggerTime=true` are not " + "supported when there are multiple events.\n" + "If it is guaranteed that 1) events do not trigger at the same " + "time, or 2) different event assignments do not affect the same " + "entities, or 3) event assignments do not depend on the " + "pre-event state, then you can set " + "`useValuesFromTriggerTime=false` and retry." + ) + @log_execution_time("processing SBML observables", logger) def _process_observables( self, - observables: Union[dict[str, dict[str, str]], None], - sigmas: dict[str, Union[str, float]], + observables: dict[str, dict[str, str]] | None, + sigmas: dict[str, str | float], noise_distributions: dict[str, str], ) -> None: """ @@ -1768,7 +1888,7 @@ def _process_observables( def _process_event_observables( self, event_observables: dict[str, dict[str, str]], - event_sigmas: dict[str, Union[str, float]], + event_sigmas: dict[str, str | float], event_noise_distributions: dict[str, str], ) -> None: """ @@ -1827,7 +1947,8 @@ def _process_event_observables( f'Event observable {eo["name"]} uses `t` in ' "it's formula which is not the time variable. " "For the time variable, please use `time` " - "instead!" + "instead!", + stacklevel=1, ) # check for nesting of observables (unsupported) @@ -1884,7 +2005,7 @@ def _generate_default_observables(self): def _process_log_likelihood( self, - sigmas: dict[str, Union[str, float]], + sigmas: dict[str, str | float], noise_distributions: dict[str, str], events: bool = False, event_reg: bool = False, @@ -1945,6 +2066,7 @@ def _process_log_likelihood( for (obs_id, obs), (sigma_id, sigma) in zip( self.symbols[obs_symbol].items(), self.symbols[sigma_symbol].items(), + strict=True, ): symbol = symbol_with_assumptions(f"J{obs_id}") dist = noise_distributions.get(str(obs_id), "normal") @@ -1955,6 +2077,7 @@ def _process_log_likelihood( zip( _get_str_symbol_identifiers(obs_id), (obs_id, obs["measurement_symbol"], sigma_id), + strict=True, ) ), ) @@ -2036,8 +2159,8 @@ def _process_species_references(self): ) def _make_initial( - self, sym_math: Union[sp.Expr, None, float] - ) -> Union[sp.Expr, None, float]: + self, sym_math: sp.Expr | None | float + ) -> sp.Expr | None | float: """ Transforms an expression to its value at the initial time point by replacing species by their initial values. @@ -2144,7 +2267,8 @@ def _get_conservation_laws_demartino( "Conservation laws for non-constant species in " "combination with parameterized stoichiometric " "coefficients are not currently supported " - "and will be turned off." + "and will be turned off.", + stacklevel=1, ) return [] @@ -2173,9 +2297,9 @@ def _get_conservation_laws_demartino( len(cls_coefficients), len(ode_model._differential_states) ) for i_cl, (cl, coefficients) in enumerate( - zip(cls_state_idxs, cls_coefficients) + zip(cls_state_idxs, cls_coefficients, strict=True) ): - for i, c in zip(cl, coefficients): + for i, c in zip(cl, coefficients, strict=True): A[i_cl, i] = sp.Rational(c) rref, pivots = A.rref() @@ -2221,7 +2345,8 @@ def _get_conservation_laws_rref( "Conservation laws for non-constant species in " "combination with parameterized stoichiometric " "coefficients are not currently supported " - "and will be turned off." + "and will be turned off.", + stacklevel=1, ) return [] @@ -2319,7 +2444,10 @@ def _add_conservation_for_non_constant_species( "coefficients": { state_id: coeff * compartment for state_id, coeff, compartment in zip( - state_ids, coefficients, compartment_sizes + state_ids, + coefficients, + compartment_sizes, + strict=True, ) }, } @@ -2488,7 +2616,7 @@ def _clean_reserved_symbols(self) -> None: def _sympy_from_sbml_math( self, var_or_math: [sbml.SBase, str] - ) -> Union[sp.Expr, float, None]: + ) -> sp.Expr | float | None: """ Sympify Math of SBML variables with all sanity checks and transformations @@ -2541,7 +2669,7 @@ def _sympy_from_sbml_math( def _get_element_initial_assignment( self, element_id: str - ) -> Union[sp.Expr, None]: + ) -> sp.Expr | None: """ Extract value of sbml variable according to its initial assignment @@ -2926,7 +3054,7 @@ def _get_list_of_species_references( ] -def replace_logx(math_str: Union[str, float, None]) -> Union[str, float, None]: +def replace_logx(math_str: str | float | None) -> str | float | None: """ Replace logX(.) by log(., X) since sympy cannot parse the former @@ -2961,7 +3089,7 @@ def _collect_event_assignment_parameter_targets( def _check_unsupported_functions_sbml( - sym: sp.Expr, expression_type: str, full_sym: Optional[sp.Expr] = None + sym: sp.Expr, expression_type: str, full_sym: sp.Expr | None = None ): try: _check_unsupported_functions(sym, expression_type, full_sym) @@ -2979,8 +3107,8 @@ def _parse_special_functions_sbml( def _validate_observables( - observables: Union[dict[str, dict[str, str]], None], - sigmas: dict[str, Union[str, float]], + observables: dict[str, dict[str, str]] | None, + sigmas: dict[str, str | float], noise_distributions: dict[str, str], events: bool = False, ) -> None: @@ -3032,7 +3160,8 @@ def _non_const_conservation_laws_supported(sbml_model: sbml.Model) -> bool: warnings.warn( "Conservation laws for non-constant species in " "models with RateRules are currently not supported " - "and will be turned off." + "and will be turned off.", + stacklevel=1, ) return False @@ -3044,7 +3173,8 @@ def _non_const_conservation_laws_supported(sbml_model: sbml.Model) -> bool: warnings.warn( "Conservation laws for non-constant species in " "models with Species-AssignmentRules are currently not " - "supported and will be turned off." + "supported and will be turned off.", + stacklevel=1, ) return False diff --git a/python/sdist/amici/setup.template.py b/python/sdist/amici/setup.template.py index 599c4df49b..80fd3fde2a 100644 --- a/python/sdist/amici/setup.template.py +++ b/python/sdist/amici/setup.template.py @@ -1,4 +1,5 @@ """AMICI model package setup""" + import os import sys from pathlib import Path @@ -71,8 +72,7 @@ def get_extension() -> CMakeExtension: ext_modules=[MODEL_EXT], packages=find_namespace_packages(), install_requires=["amici==TPL_AMICI_VERSION"], - extras_require={"wurlitzer": ["wurlitzer"]}, - python_requires=">=3.9", + python_requires=">=3.10", package_data={}, zip_safe=False, classifiers=CLASSIFIERS, diff --git a/python/sdist/amici/splines.py b/python/sdist/amici/splines.py index ea0cd0e06d..5d423a19e5 100644 --- a/python/sdist/amici/splines.py +++ b/python/sdist/amici/splines.py @@ -5,6 +5,7 @@ annotations from/to SBML files and for adding such splines to the AMICI C++ code. """ + from __future__ import annotations from typing import TYPE_CHECKING @@ -13,9 +14,9 @@ from numbers import Real from typing import ( Any, - Callable, Union, ) + from collections.abc import Callable from collections.abc import Sequence from . import sbml_import @@ -593,7 +594,9 @@ def check_if_valid(self, importer: sbml_import.SbmlImporter) -> None: importer.symbols[SymbolId.FIXED_PARAMETER][fp]["value"] for fp in fixed_parameters ] - subs = dict(zip(fixed_parameters, fixed_parameters_values)) + subs = dict( + zip(fixed_parameters, fixed_parameters_values, strict=True) + ) nodes_values = [sp.simplify(x.subs(subs)) for x in self.nodes] for x in nodes_values: assert x.is_Number @@ -1092,7 +1095,7 @@ def add_to_sbml_model( # It makes no sense to give a single nominal value: # grid values must all be different raise TypeError("x_nominal must be a Sequence!") - for _x, _val in zip(self.nodes, x_nominal): + for _x, _val in zip(self.nodes, x_nominal, strict=True): if _x.is_Symbol and not model.getParameter(_x.name): add_parameter( model, _x.name, value=_val, units=x_units @@ -1115,7 +1118,7 @@ def add_to_sbml_model( else: y_constant = len(self.values_at_nodes) * [y_constant] for _y, _val, _const in zip( - self.values_at_nodes, y_nominal, y_constant + self.values_at_nodes, y_nominal, y_constant, strict=True ): if _y.is_Symbol and not model.getParameter(_y.name): add_parameter( diff --git a/python/sdist/amici/swig.py b/python/sdist/amici/swig.py index 5ba8017005..e0c518957f 100644 --- a/python/sdist/amici/swig.py +++ b/python/sdist/amici/swig.py @@ -1,4 +1,5 @@ """Functions related to SWIG or SWIG-generated code""" + from __future__ import annotations import ast import contextlib diff --git a/python/sdist/amici/swig_wrappers.py b/python/sdist/amici/swig_wrappers.py index 72f850118c..2e90891df3 100644 --- a/python/sdist/amici/swig_wrappers.py +++ b/python/sdist/amici/swig_wrappers.py @@ -1,14 +1,18 @@ """Convenience wrappers for the swig interface""" + import logging -import sys import warnings -from contextlib import contextmanager, suppress -from typing import Any, Optional, Union -from collections.abc import Sequence +from typing import Any import amici import amici.amici as amici_swig - +from amici.amici import ( + _get_ptr, + AmiciExpData, + AmiciExpDataVector, + AmiciModel, + AmiciSolver, +) from . import numpy from .logging import get_logger @@ -18,76 +22,17 @@ __all__ = [ "runAmiciSimulation", "runAmiciSimulations", - "ExpData", "readSolverSettingsFromHDF5", "writeSolverSettingsToHDF5", "set_model_settings", "get_model_settings", - "AmiciModel", - "AmiciSolver", - "AmiciExpData", - "AmiciReturnData", - "AmiciExpDataVector", ] -AmiciModel = Union["amici.Model", "amici.ModelPtr"] -AmiciSolver = Union["amici.Solver", "amici.SolverPtr"] -AmiciExpData = Union["amici.ExpData", "amici.ExpDataPtr"] -AmiciReturnData = Union["amici.ReturnData", "amici.ReturnDataPtr"] -AmiciExpDataVector = Union["amici.ExpDataPtrVector", Sequence[AmiciExpData]] - - -try: - from wurlitzer import sys_pipes -except ModuleNotFoundError: - sys_pipes = suppress - - -@contextmanager -def _capture_cstdout(): - """Redirect C/C++ stdout to python stdout if python stdout is redirected, - e.g. in ipython notebook""" - if sys.stdout == sys.__stdout__: - yield - else: - with sys_pipes(): - yield - - -def _get_ptr( - obj: Union[AmiciModel, AmiciExpData, AmiciSolver, AmiciReturnData], -) -> Union[ - "amici_swig.Model", - "amici_swig.ExpData", - "amici_swig.Solver", - "amici_swig.ReturnData", -]: - """ - Convenience wrapper that returns the smart pointer pointee, if applicable - - :param obj: - Potential smart pointer - - :returns: - Non-smart pointer - """ - if isinstance( - obj, - ( - amici_swig.ModelPtr, - amici_swig.ExpDataPtr, - amici_swig.SolverPtr, - amici_swig.ReturnDataPtr, - ), - ): - return obj.get() - return obj - def runAmiciSimulation( model: AmiciModel, solver: AmiciSolver, - edata: Optional[AmiciExpData] = None, + edata: AmiciExpData | None = None, ) -> "numpy.ReturnDataView": """ Convenience wrapper around :py:func:`amici.amici.runAmiciSimulation` @@ -115,45 +60,19 @@ def runAmiciSimulation( warnings.warn( "Adjoint sensitivity analysis for models with discontinuous right hand sides (events/piecewise functions) has not been thoroughly tested." "Sensitivities might be wrong. Tracked at https://github.com/AMICI-dev/AMICI/issues/18. " - "Adjoint sensitivity analysis may work if the location of the discontinuity is not parameter-dependent, but we still recommend testing accuracy of gradients." + "Adjoint sensitivity analysis may work if the location of the discontinuity is not parameter-dependent, but we still recommend testing accuracy of gradients.", + stacklevel=1, ) - with _capture_cstdout(): - rdata = amici_swig.runAmiciSimulation( - _get_ptr(solver), _get_ptr(edata), _get_ptr(model) - ) + rdata = amici_swig.runAmiciSimulation( + _get_ptr(solver), _get_ptr(edata), _get_ptr(model) + ) _log_simulation(rdata) if solver.getReturnDataReportingMode() == amici.RDataReporting.full: _ids_and_names_to_rdata(rdata, model) return numpy.ReturnDataView(rdata) -def ExpData(*args) -> "amici_swig.ExpData": - """ - Convenience wrapper for :py:class:`amici.amici.ExpData` constructors - - :param args: arguments - - :returns: ExpData Instance - """ - if not args: - return amici_swig.ExpData() - - if isinstance(args[0], numpy.ReturnDataView): - return amici_swig.ExpData(_get_ptr(args[0]["ptr"]), *args[1:]) - - if isinstance(args[0], (amici_swig.ExpData, amici_swig.ExpDataPtr)): - # the *args[:1] should be empty, but by the time you read this, - # the constructor signature may have changed, and you are glad this - # wrapper did not break. - return amici_swig.ExpData(_get_ptr(args[0]), *args[1:]) - - if isinstance(args[0], (amici_swig.Model, amici_swig.ModelPtr)): - return amici_swig.ExpData(_get_ptr(args[0])) - - return amici_swig.ExpData(*args) - - def runAmiciSimulations( model: AmiciModel, solver: AmiciSolver, @@ -182,18 +101,18 @@ def runAmiciSimulations( warnings.warn( "Adjoint sensitivity analysis for models with discontinuous right hand sides (events/piecewise functions) has not been thoroughly tested. " "Sensitivities might be wrong. Tracked at https://github.com/AMICI-dev/AMICI/issues/18. " - "Adjoint sensitivity analysis may work if the location of the discontinuity is not parameter-dependent, but we still recommend testing accuracy of gradients." + "Adjoint sensitivity analysis may work if the location of the discontinuity is not parameter-dependent, but we still recommend testing accuracy of gradients.", + stacklevel=1, ) - with _capture_cstdout(): - edata_ptr_vector = amici_swig.ExpDataPtrVector(edata_list) - rdata_ptr_list = amici_swig.runAmiciSimulations( - _get_ptr(solver), - edata_ptr_vector, - _get_ptr(model), - failfast, - num_threads, - ) + edata_ptr_vector = amici_swig.ExpDataPtrVector(edata_list) + rdata_ptr_list = amici_swig.runAmiciSimulations( + _get_ptr(solver), + edata_ptr_vector, + _get_ptr(model), + failfast, + num_threads, + ) for rdata in rdata_ptr_list: _log_simulation(rdata) if solver.getReturnDataReportingMode() == amici.RDataReporting.full: @@ -203,7 +122,7 @@ def runAmiciSimulations( def readSolverSettingsFromHDF5( - file: str, solver: AmiciSolver, location: Optional[str] = "solverSettings" + file: str, solver: AmiciSolver, location: str | None = "solverSettings" ) -> None: """ Convenience wrapper for :py:func:`amici.readSolverSettingsFromHDF5` @@ -217,8 +136,8 @@ def readSolverSettingsFromHDF5( def writeSolverSettingsToHDF5( solver: AmiciSolver, - file: Union[str, object], - location: Optional[str] = "solverSettings", + file: str | object, + location: str | None = "solverSettings", ) -> None: """ Convenience wrapper for :py:func:`amici.amici.writeSolverSettingsToHDF5` diff --git a/python/sdist/amici/sympy_utils.py b/python/sdist/amici/sympy_utils.py index bc20c49dc4..9794fadef0 100644 --- a/python/sdist/amici/sympy_utils.py +++ b/python/sdist/amici/sympy_utils.py @@ -1,7 +1,9 @@ """Functionality for working with sympy objects.""" + import os from itertools import starmap -from typing import Union, Any, Callable +from typing import Any +from collections.abc import Callable import contextlib import sympy as sp import logging @@ -111,9 +113,9 @@ def smart_jacobian( @log_execution_time("running smart_multiply", logger) def smart_multiply( - x: Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix], + x: sp.MutableDenseMatrix | sp.MutableSparseMatrix, y: sp.MutableDenseMatrix, -) -> Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix]: +) -> sp.MutableDenseMatrix | sp.MutableSparseMatrix: """ Wrapper around symbolic multiplication with some additional checks that reduce computation time for large matrices @@ -136,7 +138,7 @@ def smart_multiply( def smart_is_zero_matrix( - x: Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix], + x: sp.MutableDenseMatrix | sp.MutableSparseMatrix, ) -> bool: """A faster implementation of sympy's is_zero_matrix @@ -182,7 +184,11 @@ def _parallel_applyfunc(obj: sp.Matrix, func: Callable) -> sp.Matrix: elif isinstance(obj, sp.SparseMatrix): dok = obj.todok() mapped = p.map(func, dok.values()) - dok = {k: v for k, v in zip(dok.keys(), mapped) if v != 0} + dok = { + k: v + for k, v in zip(dok.keys(), mapped, strict=True) + if v != 0 + } return obj._new(obj.rows, obj.cols, dok) else: raise ValueError(f"Unsupported matrix type {type(obj)}") diff --git a/python/sdist/amici/testing.py b/python/sdist/amici/testing.py index 8d4a73fbe1..40b791d0d8 100644 --- a/python/sdist/amici/testing.py +++ b/python/sdist/amici/testing.py @@ -1,4 +1,5 @@ """Test support functions""" + import os import sys from tempfile import TemporaryDirectory diff --git a/python/sdist/bin b/python/sdist/bin deleted file mode 120000 index 353d1e9f9e..0000000000 --- a/python/sdist/bin +++ /dev/null @@ -1 +0,0 @@ -../bin/ \ No newline at end of file diff --git a/python/sdist/pyproject.toml b/python/sdist/pyproject.toml index 91b8484af6..10a3b05374 100644 --- a/python/sdist/pyproject.toml +++ b/python/sdist/pyproject.toml @@ -1,6 +1,8 @@ +# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/ +# https://setuptools.pypa.io/en/latest/userguide/index.html [build-system] requires = [ - "setuptools>=40.6.3", + "setuptools>=61", "wheel", # oldest-supported-numpy helps us to pin numpy here to the lowest supported # version to have ABI-compatibility with the numpy version in the runtime @@ -13,10 +15,119 @@ requires = [ ] build-backend = "setuptools.build_meta" +[project] +name = "amici" +dynamic = ["version"] +description = "Advanced multi-language Interface to CVODES and IDAS" +requires-python = ">=3.10" +dependencies = [ + "cmake-build-extension==0.5.1", + "sympy>=1.9", + "numpy>=1.19.3; python_version=='3.9'", + "numpy>=1.21.4; python_version>='3.10'", + "numpy>=1.23.2; python_version=='3.11'", + "numpy>=1.26.2; python_version=='3.12'", + "numpy; python_version>='3.13'", + "python-libsbml", + "pandas>=2.0.2", + "pyarrow", + "toposort", + "setuptools>=48", + "mpmath", +] +license = {text = "BSD 3-Clause License"} +authors = [ + {name = "Fabian Froehlich", email = "froehlichfab@gmail.com"}, + {name = "Daniel Weindl", email = "sci@danielweindl.de"}, + {name = "Jan Hasenauer"}, + {name = "AMICI contributors"}, +] +maintainers = [ + {name = "Fabian Froehlich", email = "froehlichfab@gmail.com"}, + {name = "Daniel Weindl", email = "sci@danielweindl.de"}, +] +readme = "README.md" +keywords =["differential equations", "simulation", "ode", "cvodes", + "systems biology", "sensitivity analysis", "sbml", "pysb", "petab"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: C++", + "Topic :: Scientific/Engineering :: Bio-Informatics", +] + +[project.optional-dependencies] +# Don't include any URLs here - they are not supported by PyPI: +# HTTPError: 400 Bad Request from https://upload.pypi.org/legacy/ +# Invalid value for requires_dist. Error: Can't have direct dependency: ... +petab = ["petab>=0.2.9"] +pysb = ["pysb>=1.13.1"] +test = [ + "benchmark_models_petab @ git+https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab.git@master#subdirectory=src/python", + "h5py", + "pytest", + "pytest-cov", + "pytest-rerunfailures", + "coverage", + "shyaml", + "antimony>=2.13", + # see https://github.com/sys-bio/antimony/issues/92 + # unsupported x86_64 / x86_64h + "antimony!=2.14; platform_system=='Darwin' and platform_machine in 'x86_64h'", + "scipy", + "pooch" +] +vis =[ + "matplotlib", + "seaborn", +] +examples =[ + "jupyter", + "scipy", +] + +[project.scripts] +# amici_import_petab.py is kept for backwards compatibility +amici_import_petab = "amici.petab.cli.import_petab:_main" +"amici_import_petab.py" = "amici.petab.cli.import_petab:_main" + +[project.urls] +Homepage = "https://github.com/AMICI-dev/AMICI" +Documentation = "https://amici.readthedocs.io/en/latest/" +Repository = "https://github.com/AMICI-dev/AMICI.git" +"Bug Tracker" = "https://github.com/AMICI-dev/AMICI/issues" + +# TODO: consider using setuptools_scm +#[tool.setuptools_scm] +## https://setuptools-scm.readthedocs.io/en/latest/ +#root = "../.." + +[tool.setuptools.package-data] +amici = [ + "amici/include/amici/*", + "src/*template*", + "swig/*", + "libs/*", + "setup.py.template", +] + +[tool.setuptools.exclude-package-data] +"*" = ["README.txt"] + +[tool.setuptools.dynamic] +version = {attr = "amici.__version__"} + [tool.black] line-length = 79 [tool.ruff] line-length = 79 -ignore = ["E402", "F403", "F405", "E741"] extend-include = ["*.ipynb"] + +[tool.ruff.lint] +extend-select = ["B028"] +ignore = ["E402", "F403", "F405", "E741"] diff --git a/python/sdist/setup.cfg b/python/sdist/setup.cfg deleted file mode 100644 index d34d42f98f..0000000000 --- a/python/sdist/setup.cfg +++ /dev/null @@ -1,91 +0,0 @@ -[metadata] -name = amici -description = Advanced multi-language Interface to CVODES and IDAS -version = file: amici/version.txt -license = BSD 3-Clause License -url = https://github.com/AMICI-dev/AMICI -keywords = differential equations, simulation, ode, cvodes, systems biology, sensitivity analysis, sbml, pysb, petab -author = Fabian Froehlich, Jan Hasenauer, Daniel Weindl and Paul Stapor -author_email = fabian_froehlich@hms.harvard.edu -project_urls = - Bug Reports = https://github.com/AMICI-dev/AMICI/issues - Source = https://github.com/AMICI-dev/AMICI - Documentation = https://amici.readthedocs.io/en/latest/ -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Science/Research - License :: OSI Approved :: BSD License - Operating System :: POSIX :: Linux - Operating System :: MacOS :: MacOS X - Programming Language :: Python - Programming Language :: C++ - Topic :: Scientific/Engineering :: Bio-Informatics - -[options] -packages = find_namespace: -package_dir = - amici = amici -python_requires = >=3.9 -install_requires = - cmake-build-extension==0.5.1 - sympy>=1.9 - numpy>=1.19.3; python_version=='3.9' - numpy>=1.21.4; python_version>='3.10' - numpy>=1.23.2; python_version=='3.11' - numpy; python_version>='3.12' - python-libsbml - pandas>=2.0.2 - pyarrow - wurlitzer - toposort - setuptools>=48 - mpmath -include_package_data = True -zip_safe = False - -[options.extras_require] -# Don't include any URLs here - they are not supported by PyPI: -# HTTPError: 400 Bad Request from https://upload.pypi.org/legacy/ -# Invalid value for requires_dist. Error: Can't have direct dependency: ... -petab = petab>=0.2.9 -pysb = pysb>=1.13.1 -test = - benchmark_models_petab @ git+https://github.com/Benchmarking-Initiative/Benchmark-Models-PEtab.git@master#subdirectory=src/python - h5py - pytest - pytest-cov - pytest-rerunfailures - coverage - shyaml - antimony>=2.13 - # see https://github.com/sys-bio/antimony/issues/92 - # unsupported x86_64 / x86_64h - antimony!=2.14; platform_system=='Darwin' and platform_machine in 'x86_64h' - scipy - pooch -vis = - matplotlib - seaborn -examples = - jupyter - scipy - -[options.package_data] -amici = - amici/include/amici/* - src/*template* - swig/* - libs/* - setup.py.template - -[options.exclude_package_data] -* = - README.txt - - -[options.entry_points] - -; amici_import_petab.py is kept for backwards compatibility -console_scripts = - amici_import_petab = amici.petab.cli.import_petab:_main - amici_import_petab.py = amici.petab.cli.import_petab:_main diff --git a/python/sdist/setup.py b/python/sdist/setup.py index 2f44a18342..83bf33237a 100755 --- a/python/sdist/setup.py +++ b/python/sdist/setup.py @@ -9,6 +9,7 @@ - swig>=3.0 - Optional: hdf5 libraries and headers """ + import os import sys from pathlib import Path @@ -158,14 +159,6 @@ def get_extensions(): def main(): - # Readme as long package description to go on PyPi - # (https://pypi.org/project/amici/) - with open( - os.path.join(os.path.dirname(__file__), "README.md"), - encoding="utf-8", - ) as fh: - long_description = fh.read() - ext_modules = get_extensions() # handle parallel building @@ -185,8 +178,6 @@ def main(): "develop": AmiciDevelop, "build_py": AmiciBuildPy, }, - long_description=long_description, - long_description_content_type="text/markdown", ext_modules=ext_modules, ) diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 1da7cb31b3..d8d882fcfd 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,4 +1,5 @@ """pytest configuration file""" + import copy import importlib import os diff --git a/python/tests/petab/test_petab_problem.py b/python/tests/petab/test_petab_problem.py index 5a8a299bb9..736d895f8f 100644 --- a/python/tests/petab/test_petab_problem.py +++ b/python/tests/petab/test_petab_problem.py @@ -81,7 +81,7 @@ def test_amici_petab_problem_pregenerate_equals_on_demand(): app_store_false.set_parameters(parameter_update, scaled_parameters=True) for edata_store_true, edata_store_false in zip( - app_store_true.get_edatas(), app_store_false.get_edatas() + app_store_true.get_edatas(), app_store_false.get_edatas(), strict=True ): assert edata_store_true is not edata_store_false assert edata_store_true == edata_store_false diff --git a/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py b/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py index 1a39d4a846..4c40f7e815 100644 --- a/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py +++ b/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py @@ -3,7 +3,6 @@ http://bionetgen.org/index.php/Egfr_simple """ - from pysb import * Model() diff --git a/python/tests/splines_utils.py b/python/tests/splines_utils.py index 29024d3b73..19fd84eee6 100644 --- a/python/tests/splines_utils.py +++ b/python/tests/splines_utils.py @@ -6,9 +6,10 @@ import math import os +import platform import uuid from tempfile import mkdtemp -from typing import Any, Optional, Union +from typing import Any from collections.abc import Sequence import amici @@ -45,7 +46,7 @@ def evaluate_spline( def integrate_spline( spline: AbstractSpline, - params: Union[dict, None], + params: dict | None, tt: Sequence[float], initial_value: float = 0, ): @@ -119,12 +120,12 @@ def species_to_index(name) -> int: def create_petab_problem( splines: list[AbstractSpline], params_true: dict, - initial_values: Optional[np.ndarray] = None, + initial_values: np.ndarray | None = None, use_reactions: bool = False, measure_upsample: int = 6, sigma: float = 1.0, t_extrapolate: float = 0.25, - folder: Optional[str] = None, + folder: str | None = None, model_name: str = "test_splines", ): """ @@ -216,7 +217,7 @@ def create_petab_problem( zz_true = np.array( [ integrate_spline(spline, params_true, tt_obs, iv) - for (spline, iv) in zip(splines, initial_values) + for (spline, iv) in zip(splines, initial_values, strict=True) ], dtype=float, ) @@ -283,9 +284,9 @@ def simulate_splines( params_true, initial_values=None, *, - folder: Optional[str] = None, + folder: str | None = None, keep_temporary: bool = False, - benchmark: Union[bool, int] = False, + benchmark: bool | int = False, rtol: float = 1e-12, atol: float = 1e-12, maxsteps: int = 500_000, @@ -480,7 +481,7 @@ def compute_ground_truth( x_true_sym = sp.Matrix( [ integrate_spline(spline, None, times, iv) - for (spline, iv) in zip(splines, initial_values) + for (spline, iv) in zip(splines, initial_values, strict=True) ] ).transpose() groundtruth = { @@ -505,8 +506,8 @@ def check_splines( discard_annotations: bool = False, use_adjoint: bool = False, skip_sensitivity: bool = False, - debug: Union[bool, str] = False, - parameter_lists: Optional[Sequence[Sequence[int]]] = None, + debug: bool | str = False, + parameter_lists: Sequence[Sequence[int]] | None = None, llh_rtol: float = 1e-8, sllh_atol: float = 1e-8, x_rtol: float = 1e-11, @@ -515,7 +516,7 @@ def check_splines( w_atol: float = 1e-11, sx_rtol: float = 1e-10, sx_atol: float = 1e-10, - groundtruth: Optional[Union[str, dict[str, Any]]] = None, + groundtruth: str | dict[str, Any] | None = None, **kwargs, ): """ @@ -604,7 +605,7 @@ def param_by_name(id): x_true_sym = sp.Matrix( [ integrate_spline(spline, None, tt, iv) - for (spline, iv) in zip(splines, initial_values) + for (spline, iv) in zip(splines, initial_values, strict=True) ] ).transpose() x_true = np.asarray(x_true_sym.subs(params_true), dtype=float) @@ -716,13 +717,7 @@ def param_by_name(id): if sllh_atol is None: sllh_atol = np.finfo(float).eps sllh_err_abs = abs(sllh).max() - if ( - sllh_err_abs > sllh_atol and debug is not True - ) or debug == "print": - print(f"sllh_atol={sllh_atol}") - print(f"sllh_err_abs = {sllh_err_abs}") - if not debug: - assert sllh_err_abs <= sllh_atol + assert sllh_err_abs <= sllh_atol, f"{sllh_err_abs=} {sllh_atol=}" else: assert sllh is None @@ -771,8 +766,8 @@ def check_splines_full( check_piecewise: bool = True, check_forward: bool = True, check_adjoint: bool = True, - folder: Optional[str] = None, - groundtruth: Optional[Union[dict, str]] = "compute", + folder: str | None = None, + groundtruth: dict | str | None = "compute", return_groundtruth: bool = False, **kwargs, ): @@ -896,7 +891,7 @@ def example_spline_1( yy = list(sp.symbols(f"y{idx}_0:{len(yy_true)}")) if fixed_values is None: - params = dict(zip(yy, yy_true)) + params = dict(zip(yy, yy_true, strict=True)) elif fixed_values == "all": params = {} for i in range(len(yy_true)): @@ -917,11 +912,11 @@ def example_spline_1( extrapolate=extrapolate, ) - if os.name == "nt": + if os.name == "nt" or platform.system() == "Darwin": tols = ( dict(llh_rtol=1e-15, x_rtol=1e-8, x_atol=1e-7), dict(llh_rtol=1e-15, x_rtol=1e-8, x_atol=1e-7), - dict(llh_rtol=1e-15, sllh_atol=5e-8, x_rtol=1e-8, x_atol=1e-7), + dict(llh_rtol=1e-15, sllh_atol=5e-7, x_rtol=1e-8, x_atol=1e-7), ) else: tols = ( @@ -939,7 +934,7 @@ def example_spline_2(idx: int = 0): xx = UniformGrid(0, 25, number_of_nodes=len(yy_true)) yy = list(sp.symbols(f"y{idx}_0:{len(yy_true) - 1}")) yy.append(yy[0]) - params = dict(zip(yy, yy_true)) + params = dict(zip(yy, yy_true, strict=True)) spline = CubicHermiteSpline( f"y{idx}", nodes=xx, @@ -960,7 +955,7 @@ def example_spline_3(idx: int = 0): yy_true = [0.0, 2.0, 5.0, 6.0, 5.0, 4.0, 2.0, 3.0, 4.0, 6.0] xx = UniformGrid(0, 25, number_of_nodes=len(yy_true)) yy = list(sp.symbols(f"y{idx}_0:{len(yy_true)}")) - params = dict(zip(yy, yy_true)) + params = dict(zip(yy, yy_true, strict=True)) spline = CubicHermiteSpline( f"y{idx}", nodes=xx, diff --git a/python/tests/test_conserved_quantities_demartino.py b/python/tests/test_conserved_quantities_demartino.py index ca40946db3..94d51a606d 100644 --- a/python/tests/test_conserved_quantities_demartino.py +++ b/python/tests/test_conserved_quantities_demartino.py @@ -1,4 +1,5 @@ """Tests for conservation laws / conserved moieties""" + import os from time import perf_counter @@ -822,7 +823,7 @@ def test_cl_detect_execution_time(data_demartino2014): # <5s on modern hardware, but leave some slack max_time_seconds = 40 if "GITHUB_ACTIONS" in os.environ else 10 - runtime = np.Inf + runtime = np.inf for _ in range(max_tries): runtime = compute_moiety_conservation_laws_demartino2014( diff --git a/python/tests/test_edata.py b/python/tests/test_edata.py index 27c67de61e..fab49c160e 100644 --- a/python/tests/test_edata.py +++ b/python/tests/test_edata.py @@ -1,4 +1,5 @@ """Tests related to amici.ExpData via Python""" + import amici import numpy as np from amici.testing import skip_on_valgrind diff --git a/python/tests/test_events.py b/python/tests/test_events.py index 065cdeb126..d16877fd2e 100644 --- a/python/tests/test_events.py +++ b/python/tests/test_events.py @@ -1,4 +1,5 @@ """Tests for SBML events, including piecewise expressions.""" + from copy import deepcopy import amici @@ -14,6 +15,7 @@ create_amici_model, create_sbml_model, ) +from numpy.testing import assert_allclose @pytest.fixture( @@ -745,3 +747,85 @@ def test_handling_of_fixed_time_point_event_triggers(): assert (rdata.x[(rdata.ts >= 3)] == 3).all() check_derivatives(amici_model, amici_solver, edata=None) + + +def test_multiple_event_assignment_with_compartment(): + """see https://github.com/AMICI-dev/AMICI/issues/2426""" + ant_model = """ + model test_events_multiple_assignments + compartment event_target = 1 + event_target' = 0 + species species_in_event_target in event_target = 1 + unrelated = 2 + + # use different order of event assignments for the two events + at (time > 5): unrelated = 4, event_target = 10 + at (time > 10): event_target = 1, unrelated = 2 + end + """ + # watch out for too long path names on windows ... + module_name = "tst_mltple_ea_w_cmprtmnt" + with TemporaryDirectory(prefix=module_name, delete=False) as outdir: + antimony2amici( + ant_model, + model_name=module_name, + output_dir=outdir, + verbose=True, + ) + model_module = amici.import_model_module( + module_name=module_name, module_path=outdir + ) + amici_model = model_module.getModel() + assert amici_model.ne == 2 + assert amici_model.ne_solver == 0 + assert amici_model.nx_rdata == 3 + amici_model.setTimepoints(np.linspace(0, 15, 16)) + amici_solver = amici_model.getSolver() + rdata = amici.runAmiciSimulation(amici_model, amici_solver) + assert rdata.status == amici.AMICI_SUCCESS + idx_event_target = amici_model.getStateIds().index("event_target") + idx_unrelated = amici_model.getStateIds().index("unrelated") + idx_species_in_event_target = amici_model.getStateIds().index( + "species_in_event_target" + ) + + assert_allclose( + rdata.x[(rdata.ts < 5) & (rdata.ts > 10), idx_event_target], + 1, + rtol=0, + atol=1e-15, + ) + assert_allclose( + rdata.x[(5 < rdata.ts) & (rdata.ts < 10), idx_event_target], + 10, + rtol=0, + atol=1e-15, + ) + assert_allclose( + rdata.x[(rdata.ts < 5) & (rdata.ts > 10), idx_unrelated], + 2, + rtol=0, + atol=1e-15, + ) + assert_allclose( + rdata.x[(5 < rdata.ts) & (rdata.ts < 10), idx_unrelated], + 4, + rtol=0, + atol=1e-15, + ) + assert_allclose( + rdata.x[ + (rdata.ts < 5) & (rdata.ts > 10), idx_species_in_event_target + ], + 1, + rtol=0, + atol=1e-15, + ) + assert_allclose( + rdata.x[ + (5 < rdata.ts) & (rdata.ts < 10), idx_species_in_event_target + ], + 0.1, + rtol=0, + atol=1e-15, + ) diff --git a/python/tests/test_heavisides.py b/python/tests/test_heavisides.py index c3bea26a0c..26de4f575e 100644 --- a/python/tests/test_heavisides.py +++ b/python/tests/test_heavisides.py @@ -1,4 +1,5 @@ """Tests for SBML events, including piecewise expressions.""" + import numpy as np import pytest from util import ( diff --git a/python/tests/test_petab_objective.py b/python/tests/test_petab_objective.py index 5d29ad88ff..07770ac413 100755 --- a/python/tests/test_petab_objective.py +++ b/python/tests/test_petab_objective.py @@ -40,10 +40,7 @@ def test_simulate_petab_sensitivities(lotka_volterra): amici_solver.setMaxSteps(int(1e5)) problem_parameters = dict( - zip( - petab_problem.x_ids, - petab_problem.x_nominal, - ) + zip(petab_problem.x_ids, petab_problem.x_nominal, strict=True) ) results = {} diff --git a/python/tests/test_petab_simulate.py b/python/tests/test_petab_simulate.py index a8240bff33..e1da7768e3 100644 --- a/python/tests/test_petab_simulate.py +++ b/python/tests/test_petab_simulate.py @@ -1,4 +1,5 @@ """Tests for petab_simulate.py.""" + import tempfile from pathlib import Path diff --git a/python/tests/test_rdata.py b/python/tests/test_rdata.py index 8e0f78655e..80ec3b80e6 100644 --- a/python/tests/test_rdata.py +++ b/python/tests/test_rdata.py @@ -1,4 +1,5 @@ """Test amici.ReturnData(View)-related functionality""" + import amici import numpy as np import pytest diff --git a/python/tests/test_sbml_import.py b/python/tests/test_sbml_import.py index aaab5688cc..4936a3c901 100644 --- a/python/tests/test_sbml_import.py +++ b/python/tests/test_sbml_import.py @@ -1,4 +1,5 @@ """Tests related to amici.sbml_import""" + import os import re from numbers import Number @@ -758,7 +759,7 @@ def test_constraints(): # in practice assert np.any(rdata.x < 0) - amici_solver.setRelativeTolerance(1e-14) + amici_solver.setRelativeTolerance(1e-13) amici_solver.setConstraints( [Constraint.non_negative, Constraint.non_negative] ) diff --git a/python/tests/test_splines.py b/python/tests/test_splines.py index a7fe01e84a..66104b824b 100644 --- a/python/tests/test_splines.py +++ b/python/tests/test_splines.py @@ -62,7 +62,7 @@ def test_multiple_splines(**kwargs): tols = [] for t0, t1, t2, t3, t4, t5 in zip( - tols0, tols1, tols2, tols3, tols4, tols5 + tols0, tols1, tols2, tols3, tols4, tols5, strict=True ): keys = set().union( t0.keys(), t1.keys(), t2.keys(), t3.keys(), t4.keys(), t5.keys() diff --git a/python/tests/test_splines_python.py b/python/tests/test_splines_python.py index 4c4de5ccfc..539fb1dd4d 100644 --- a/python/tests/test_splines_python.py +++ b/python/tests/test_splines_python.py @@ -215,8 +215,11 @@ def test_SplineNonUniformPeriodicExtrapolation(): @skip_on_valgrind def check_gradient(spline, t, params, params_values, expected, rel_tol=1e-9): value = spline.evaluate(t) - subs = {pname: pvalue for (pname, pvalue) in zip(params, params_values)} - for p, exp in zip(params, expected): + subs = { + pname: pvalue + for (pname, pvalue) in zip(params, params_values, strict=True) + } + for p, exp in zip(params, expected, strict=True): assert math.isclose( float(value.diff(p).subs(subs)), exp, rel_tol=rel_tol ) diff --git a/python/tests/test_splines_short.py b/python/tests/test_splines_short.py index 59e54a3279..43b0ccd294 100644 --- a/python/tests/test_splines_short.py +++ b/python/tests/test_splines_short.py @@ -44,7 +44,7 @@ def test_two_splines(**kwargs): tols1 = (tols1, tols1, tols1) tols = [] - for t0, t1 in zip(tols0, tols1): + for t0, t1 in zip(tols0, tols1, strict=True): keys = set().union(t0.keys(), t1.keys()) t = { key: max( diff --git a/python/tests/util.py b/python/tests/util.py index dde10eb454..0f368a4f6f 100644 --- a/python/tests/util.py +++ b/python/tests/util.py @@ -1,4 +1,5 @@ """Tests for SBML events, including piecewise expressions.""" + import sys import tempfile from pathlib import Path @@ -95,7 +96,7 @@ def create_sbml_model( event = model.createEvent() event.setId(event_id) event.setName(event_id) - event.setUseValuesFromTriggerTime(True) + event.setUseValuesFromTriggerTime(False) trigger = event.createTrigger() trigger.setMath(libsbml.parseL3Formula(event_def["trigger"])) trigger.setPersistent(True) @@ -108,7 +109,7 @@ def create_event_assignment(target, assignment): if isinstance(event_def["target"], list): for event_target, event_assignment in zip( - event_def["target"], event_def["assignment"] + event_def["target"], event_def["assignment"], strict=True ): create_event_assignment(event_target, event_assignment) diff --git a/python/tests/valgrind-python.supp b/python/tests/valgrind-python.supp index 01bc776aec..dab3c06dc4 100644 --- a/python/tests/valgrind-python.supp +++ b/python/tests/valgrind-python.supp @@ -878,3 +878,90 @@ fun:loadAntimonyString ... } + +{ + Python + Memcheck:Cond + fun:maybe_small_long + ... +} + +{ + Python + Memcheck:Value8 + fun:medium_value + ... +} + +{ + Python + Memcheck:Value8 + fun:Py_INCREF + ... +} +{ + Python + Memcheck:Value8 + fun:Py_DECREF + ... +} +{ + Python + Memcheck:Value8 + fun:Py_SIZE + ... +} +{ + Python + Memcheck:Value8 + fun:Py_TYPE + ... +} +{ + Python + Memcheck:Value8 + fun:type_call + ... +} + +{ + Python + Memcheck:Value8 + fun:_PyEval_EvalFrameDefault + ... +} + + +{ + Python + Memcheck:Value8 + fun:PyType_HasFeature + ... +} + +{ + Python + Memcheck:Value8 + fun:unpack_indices + ... +} + +{ + Python + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:PyFloat_FromDouble + fun:fill_time + ... +} +{ + Python + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:gc_alloc + ... + fun:Py_CompileStringObject + ... +} diff --git a/swig/CMakeLists.txt b/swig/CMakeLists.txt index 73faccda7e..7b7baf9be9 100644 --- a/swig/CMakeLists.txt +++ b/swig/CMakeLists.txt @@ -20,6 +20,7 @@ find_package( Python3 COMPONENTS Interpreter Development NumPy REQUIRED) +message(STATUS "Found numpy ${Python3_NumPy_VERSION} include dir ${Python3_NumPy_INCLUDE_DIRS}") set(AMICI_INTERFACE_LIST ${CMAKE_CURRENT_SOURCE_DIR}/amici.i ${CMAKE_CURRENT_SOURCE_DIR}/edata.i @@ -122,7 +123,6 @@ if(WIN32) POST_BUILD COMMAND dumpbin "/DEPENDENTS" "$" COMMENT "Dumping extension dependencies.") - message("Dependencies: ${dump}") endif() install(FILES ${CMAKE_CURRENT_BINARY_DIR}/amici.py $ diff --git a/swig/amici.i b/swig/amici.i index 46a58f8365..48f99cd7a5 100644 --- a/swig/amici.i +++ b/swig/amici.i @@ -341,6 +341,8 @@ def __repr__(self): // Handle AMICI_DLL_DIRS environment variable %pythonbegin %{ +from __future__ import annotations + import sys import os @@ -353,7 +355,8 @@ if sys.platform == 'win32' and (dll_dirs := os.environ.get('AMICI_DLL_DIRS')): // import additional types for typehints // also import np for use in __repr__ functions %pythonbegin %{ -from typing import TYPE_CHECKING, Iterable, Sequence +from typing import TYPE_CHECKING, Iterable, Union +from collections.abc import Sequence import numpy as np if TYPE_CHECKING: import numpy @@ -361,6 +364,37 @@ if TYPE_CHECKING: %pythoncode %{ +AmiciModel = Union[Model, ModelPtr] +AmiciSolver = Union[Solver, SolverPtr] +AmiciExpData = Union[ExpData, ExpDataPtr] +AmiciReturnData = Union[ReturnData, ReturnDataPtr] +AmiciExpDataVector = Union[ExpDataPtrVector, Sequence[AmiciExpData]] + + +def _get_ptr( + obj: AmiciModel | AmiciExpData | AmiciSolver | AmiciReturnData, +) -> Model | ExpData | Solver | ReturnData: + """ + Convenience wrapper that returns the smart pointer pointee, if applicable + + :param obj: + Potential smart pointer + + :returns: + Non-smart pointer + """ + if isinstance( + obj, + ( + ModelPtr, + ExpDataPtr, + SolverPtr, + ReturnDataPtr, + ), + ): + return obj.get() + return obj + __all__ = [ x @@ -368,4 +402,5 @@ __all__ = [ if not x.startswith('_') and x not in {"np", "sys", "os", "numpy", "IntEnum", "enum", "pi", "TYPE_CHECKING", "Iterable", "Sequence"} ] + %} diff --git a/swig/edata.i b/swig/edata.i index f2f7d0da8a..0a8a01e3c9 100644 --- a/swig/edata.i +++ b/swig/edata.i @@ -8,6 +8,24 @@ using namespace amici; %ignore ConditionContext; +%feature("pythonprepend") amici::ExpData::ExpData %{ + """ + Convenience wrapper for :py:class:`amici.amici.ExpData` constructors + + :param args: arguments + + :returns: ExpData Instance + """ + if args: + from amici.numpy import ReturnDataView + + # Get the raw pointer if necessary + if isinstance(args[0], (ExpData, ExpDataPtr, Model, ModelPtr)): + args = (_get_ptr(args[0]), *args[1:]) + elif isinstance(args[0], ReturnDataView): + args = (_get_ptr(args[0]["ptr"]), *args[1:]) +%} + // ExpData.__repr__ %pythoncode %{ def _edata_repr(self: "ExpData"): diff --git a/tests/benchmark-models/evaluate_benchmark.py b/tests/benchmark-models/evaluate_benchmark.py index 0c6e2e4122..0fe80f6625 100644 --- a/tests/benchmark-models/evaluate_benchmark.py +++ b/tests/benchmark-models/evaluate_benchmark.py @@ -3,6 +3,7 @@ """ Aggregate computation times from different benchmarks and plot """ + import os import matplotlib.pyplot as plt diff --git a/tests/benchmark-models/test_petab_benchmark.py b/tests/benchmark-models/test_petab_benchmark.py index 753c88e500..976ff3dc05 100644 --- a/tests/benchmark-models/test_petab_benchmark.py +++ b/tests/benchmark-models/test_petab_benchmark.py @@ -1,4 +1,5 @@ """Tests for simulate_petab on PEtab benchmark problems.""" + import os from pathlib import Path diff --git a/tests/benchmark-models/test_petab_model.py b/tests/benchmark-models/test_petab_model.py index 8f52a341c4..da3d37b5fb 100755 --- a/tests/benchmark-models/test_petab_model.py +++ b/tests/benchmark-models/test_petab_model.py @@ -3,6 +3,7 @@ """ Simulate a PEtab problem and compare results to reference values """ + import argparse import contextlib import importlib diff --git a/tests/cpp/steadystate/tests1.cpp b/tests/cpp/steadystate/tests1.cpp index e939a84ff2..109cc9eecb 100644 --- a/tests/cpp/steadystate/tests1.cpp +++ b/tests/cpp/steadystate/tests1.cpp @@ -1,7 +1,6 @@ #include "testfunctions.h" #include "wrapfunctions.h" -#include #include @@ -175,7 +174,8 @@ TEST(ExampleSteadystate, SensitivityForwardErrorNewt) TEST(ExampleSteadystate, SensitivityForwardDense) { - amici::simulateVerifyWrite("/model_steadystate/sensiforwarddense/"); + amici::simulateVerifyWrite("/model_steadystate/sensiforwarddense/", + 1e-9, TEST_RTOL); } TEST(ExampleSteadystate, SensiFwdNewtonPreeq) diff --git a/version.txt b/version.txt index 2094a100ca..d21d277be5 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.24.0 +0.25.0