diff --git a/.coveragerc b/.coveragerc index 5c8d382e6f..d5f1748d1b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -16,4 +16,3 @@ exclude_lines = raise except: import - diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dbda62fd09..6b2aece452 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,3 @@ # default owners * @AMICI-dev/amici-maintainers - diff --git a/.github/workflows/test_benchmark_collection_models.yml b/.github/workflows/test_benchmark_collection_models.yml index 5dc3658ba1..9cfa50ea2a 100644 --- a/.github/workflows/test_benchmark_collection_models.yml +++ b/.github/workflows/test_benchmark_collection_models.yml @@ -61,7 +61,7 @@ jobs: git clone --depth 1 https://github.com/benchmarking-initiative/Benchmark-Models-PEtab.git \ && export BENCHMARK_COLLECTION="$(pwd)/Benchmark-Models-PEtab/Benchmark-Models/" \ && AMICI_PARALLEL_COMPILE=2 tests/benchmark-models/test_benchmark_collection.sh - + # run gradient checks - name: Run Gradient Checks run: | diff --git a/.github/workflows/test_petab_test_suite.yml b/.github/workflows/test_petab_test_suite.yml index 28a76678a7..70acd254df 100644 --- a/.github/workflows/test_petab_test_suite.yml +++ b/.github/workflows/test_petab_test_suite.yml @@ -43,8 +43,6 @@ jobs: libatlas-base-dev \ python3-venv - - run: pip3 install pysb petab - - name: Build BNGL run: | scripts/buildBNGL.sh @@ -59,6 +57,11 @@ jobs: run: | scripts/installAmiciSource.sh + - name: Install petab + run: | + source ./build/venv/bin/activate \ + && pip3 install wheel pytest shyaml pytest-cov pysb + # retrieve test models - name: Download and install PEtab test suite run: | diff --git a/.github/workflows/test_python_cplusplus.yml b/.github/workflows/test_python_cplusplus.yml index 6a60b0e6c9..3bc7f8c920 100644 --- a/.github/workflows/test_python_cplusplus.yml +++ b/.github/workflows/test_python_cplusplus.yml @@ -1,5 +1,11 @@ name: C++/Python Tests -on: [push, merge_group, workflow_dispatch] +on: + push: + merge_group: + workflow_dispatch: + pull_request: + branches: + - master jobs: build: @@ -91,11 +97,23 @@ jobs: source build/venv/bin/activate \ && pytest \ --ignore-glob=*petab* \ + --ignore-glob=*test_splines.py \ --cov=amici \ --cov-report=xml:"${AMICI_DIR}/build/coverage_py.xml" \ --cov-append \ ${AMICI_DIR}/python/tests + + - name: Python tests splines + if: ${{ github.base_ref == 'master' || github.event.merge_group.base_ref == 'master'}} + run: | + source build/venv/bin/activate \ + && pytest \ + --cov=amici \ + --cov-report=xml:"${AMICI_DIR}/build/coverage_py.xml" \ + --cov-append \ + ${AMICI_DIR}/python/tests/test_splines.py + - name: Install notebook dependencies run: | source build/venv/bin/activate \ diff --git a/.github/workflows/test_python_ver_matrix.yml b/.github/workflows/test_python_ver_matrix.yml index f84c3c5897..59dcf91041 100644 --- a/.github/workflows/test_python_ver_matrix.yml +++ b/.github/workflows/test_python_ver_matrix.yml @@ -63,4 +63,5 @@ jobs: run: | source build/venv/bin/activate \ && pip3 install git+https://github.com/pysb/pysb \ - && python3 -m pytest --ignore-glob=*petab* ${AMICI_DIR}/python/tests + && python3 -m pytest --ignore-glob=*petab* \ + --ignore-glob=*test_splines.py ${AMICI_DIR}/python/tests diff --git a/.github/workflows/test_windows.yml b/.github/workflows/test_windows.yml index c9dad451e6..53834c3000 100644 --- a/.github/workflows/test_windows.yml +++ b/.github/workflows/test_windows.yml @@ -5,12 +5,15 @@ on: workflow_dispatch: schedule: - cron: '48 4 * * *' + pull_request: + branches: + - master jobs: build: name: Tests Windows - runs-on: windows-2019 + runs-on: windows-latest env: AMICI_SKIP_CMAKE_TESTS: "TRUE" @@ -74,4 +77,14 @@ jobs: - run: python -m amici - name: Run Python tests - run: python -m pytest --ignore-glob=*petab* --ignore-glob=*special* python/tests + shell: bash + run: | + python -m pytest \ + --ignore-glob=*petab* \ + --ignore-glob=*special* \ + --ignore-glob=*test_splines.py \ + python/tests + + - name: Python tests splines + if: ${{ github.base_ref == 'master' || github.event.merge_group.base_ref == 'master'}} + run: python -m pytest python/tests/test_splines.py diff --git a/.gitignore b/.gitignore index cb888cde5c..60b9ff5031 100644 --- a/.gitignore +++ b/.gitignore @@ -42,7 +42,7 @@ models/model_robertson/build/* !models/model_calvetti models/model_calvetti/build/* -amici_models/* +amici_models/ simulate_model_*_hdf.m simulate_model_*.m diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..e0ea39c7c2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + name: isort (python) + args: ["--profile", "black", "--filter-files"] +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: check-added-large-files + - id: check-merge-conflict + - id: check-yaml + args: [--allow-multiple-documents] + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.11 +exclude: '^(ThirdParty|models)/' diff --git a/CHANGELOG.md b/CHANGELOG.md index ff722cd5d3..1a3e07b657 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,39 @@ -# Changelog +# Changelog ## v0.X Series +### v0.18.0 (2023-05-26) +Features: +* More efficient handling of splines in SBML models + by @paulstapor, @lcontento, @dweindl + in https://github.com/AMICI-dev/AMICI/pull/1515 +* Partial support of current PEtab2.0 draft, including support for PySB models + by @dweindl, @FFroehlich in https://github.com/AMICI-dev/AMICI/pull/1800 + +Fixes +* **Fixed incorrect forward sensitivities for models with events with** + **state-dependent trigger functions** + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2084 +* Model import: Don't create spl.h and sspl.h for models without splines + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2088 +* SBML import - faster processing of SpeciesReference IDs + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2094 +* Update swig ignores + by @FFroehlich in https://github.com/AMICI-dev/AMICI/pull/2098 +* CMake: Fixed choosing SWIG via `SWIG` env variable + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2100 +* CMake: Try FindBLAS if no other information was provided + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2104 +* Fixed cblas error for models without solver states in combination with + forward sensitivities + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2108 +* Fixed compilation error for models with events and xdot=0 + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2111 +* Fixed import error for models with events and 0 states + by @dweindl in https://github.com/AMICI-dev/AMICI/pull/2112 + +**Full Changelog**: https://github.com/AMICI-dev/AMICI/compare/v0.17.1...v0.18.0 + ### v0.17.1 (2023-05-10) This release fixes two bugs: @@ -244,7 +276,7 @@ Fixes * Added equality operator for ExpData by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1881 - + * Updated base image for Dockerfile to Ubuntu 22.04/Python 3.10 by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1896 @@ -272,7 +304,7 @@ Fixes #### Documentation: -* Update reference list +* Update reference list by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1874, https://github.com/AMICI-dev/AMICI/pull/1884 **Full Changelog**: @@ -492,7 +524,7 @@ https://github.com/AMICI-dev/AMICI/compare/v0.11.26...v0.11.27 ### v0.11.26 (2022-03-14) New features: -* Import of BioNetGenLanguage (BNGL) models by @FFroehlich in +* Import of BioNetGenLanguage (BNGL) models by @FFroehlich in https://github.com/AMICI-dev/AMICI/pull/1709 * Added support for observable-dependent sigmas by @dweindl, @FFroehlich in https://github.com/AMICI-dev/AMICI/pull/1692 @@ -572,26 +604,26 @@ Fixes: Features: * Added overload for Model::setParameterScale with vector by @dilpath in https://github.com/AMICI-dev/AMICI/pull/1614 -* Removed assert_fun argument from gradient checking, improve output +* Removed assert_fun argument from gradient checking, improve output by @dweindl, @FFroehlich in https://github.com/AMICI-dev/AMICI/pull/1609 * Added get_expressions_as_dataframe by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1621 -* Added `id` field to ExpData and ReturnData by @dweindl in +* Added `id` field to ExpData and ReturnData by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1622 -* Included condition id in dataframes by @dweindl in +* Included condition id in dataframes by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1623 Fixes: -* C++: Fixed SUNMatrixWrapper ctor for size 0 matrices by @dweindl in +* C++: Fixed SUNMatrixWrapper ctor for size 0 matrices by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1608 -* Python: Handle TemporaryDirectory cleanup failures on Windows by @dweindl in +* Python: Handle TemporaryDirectory cleanup failures on Windows by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1617 -* Python: pysb.Model.initial_conditions throws a DeprecationWarning by +* Python: pysb.Model.initial_conditions throws a DeprecationWarning by @PaulJonasJost in https://github.com/AMICI-dev/AMICI/pull/1620 * Fixed wrong array size in warnings by @dweindl in https://github.com/AMICI-dev/AMICI/pull/1624 -NOTE: AMICI 0.11.23 requires numpy<1.22.0 +NOTE: AMICI 0.11.23 requires numpy<1.22.0 **Full Changelog**: https://github.com/AMICI-dev/AMICI/compare/v0.11.22...v0.11.23 @@ -626,7 +658,7 @@ New: ### v0.11.20 (2021-11-12) -New: +New: * Changed parameter mappings such that unassigned values have non-nan default values. This fixes erroneous evaluation of `llh` as `NaN` in some situations (#1574) * Added support for Python 3.10 (#1555) @@ -760,7 +792,7 @@ Misc: Breaking changes: * AMICI requires Python>=3.7 -* Updated package installation (PEP517/518): +* Updated package installation (PEP517/518): Creating source distributions requires https://github.com/pypa/build (#1384) (but now handles all package building dependencies properly) @@ -794,7 +826,7 @@ Other: ### v0.11.12 (2021-01-26) -Features: +Features: * Add expression IDs and names to generated models (#1374) Fixes: @@ -882,7 +914,7 @@ Bugfix release that restores compatibility with sympy 1.7 * Overload python interface functions for amici.{Model,Solver,ExpData} and amici.{Model,Solver,ExpData}Ptr (#1271) #### C++ -* Fix and extend use of sparse matrix operations (#1230, #1240, #1244, #1247, #1271) +* Fix and extend use of sparse matrix operations (#1230, #1240, #1244, #1247, #1271) * **Fix application of maximal number of steps**, MaxNumStep parameter now limit total number of steps, not number of steps between output times. (#1267) #### Doc @@ -913,7 +945,7 @@ Bugfix release that restores compatibility with sympy 1.7 * Create sdist on GHA using swig4.0.1 (#1204) (Fixing broken pypi package) * Fix links after repository move * Speed-up swig build: disable all languages except python (#1211) -* Fix doc generation on readthedocs (#1196) +* Fix doc generation on readthedocs (#1196) ### v0.11.5 (2020-08-07) @@ -977,7 +1009,7 @@ Bugfix release that restores compatibility with sympy 1.7 #### Python * Upgrade to sympy 1.6.0, which is now required minimum version (#1098, #1103) -* Speed up model import +* Speed up model import * Speed-up computation of sx0, reduce file size (#1109) * Replace terribly slow sympy.MutableDenseMatrix.is_zero_matrix by custom implementation (#1104) * speedup dataframe creation in `get*AsDataFrame` (#1088) @@ -1103,11 +1135,11 @@ CI: ### v0.10.17 (2020-01-15) -- **added python 3.8 support, dropped python 3.6 support** (#898) +- **added python 3.8 support, dropped python 3.6 support** (#898) - Added logging functionality (#900) - Fixes PySB import (#879, #902) - Fixes symbolic processing (#899) -- Improved build scripts (#894, +- Improved build scripts (#894, - Improved petab support (#886, #888, #891) - CI related fixes (#865, #896) @@ -1130,15 +1162,15 @@ No other changes. **NOTE: For Python-imported SBML-models this release may compute incorrect sensitivities w.r.t. sigma. Bug introduced in 0.10.14, fixed in 0.10.15.** -Python: +Python: * Don't require use of ModelPtr.get to call ExpData(Model) * Fix import in generated model Python package * Setup AMICI standalone scripts as setuptools entrypoints * Simplify symbolic sensitivity expressions during Python SBML import Fixes Infs in the Jacobian when using Hill-functions with states of 0.0. -* Extended Newton solver #848 - The changes that allow performing Newton tests from the paper: +* Extended Newton solver #848 + The changes that allow performing Newton tests from the paper: G. T. Lines, Ł. Paszkowski, L. Schmiester, D. Weindl, P. Stapor, and J. Hasenauer. Efficient computation of steady states in large-scale ODE models of biochemical reaction networks. accepted for Proceedings of the 8th IFAC Conference on Foundations of Systems Biology in Engineering (FOSBE), Valencia, Spain, October 2019. * Use SWIG>=4.0 on travis to include PyDoc in sdist / pypi package (#841) * **Fix choice of likelihood formula; failed if observable names were not equal to observable IDs** @@ -1183,8 +1215,8 @@ Misc: ### v0.10.11 (2019-08-31) -* Fixed setting initial conditions for preequilibration (#784) -* Fixed species->parameter conversion during PEtab import (#782) +* Fixed setting initial conditions for preequilibration (#784) +* Fixed species->parameter conversion during PEtab import (#782) * Set correct Matlab include directories in CMake (#793) * Extended and updated documentation (#785, #787) * Fix various SBML import issues @@ -1216,7 +1248,7 @@ Detaills: * feature(python) Use MKL from environment modules to provide cblas * fix(python) Fix define_macros not being passed to setuptools for Extension * fix(python) Fix define_macros not being passed to setuptools for clibs - * Do not always add 'cblas' library since users may want to override that by a cblas-compatible library with a different name (closes #736) + * Do not always add 'cblas' library since users may want to override that by a cblas-compatible library with a different name (closes #736) * Update HDF5 path hints; use shared library if static is not available. * Check for HDF5_BASE from environment module * Fix system-dependent sundials library directory (Fixes #749) (#750) @@ -1240,7 +1272,7 @@ All: - Fix reuse of `Solver` instances (#541) C++: -- Check for correct AMICI version for model in CMake +- Check for correct AMICI version for model in CMake - Add reporting of computation times (#699) Python: @@ -1277,12 +1309,12 @@ Doc C++ - Fix missing source files in CMakeLists.txt (#658) - Set CMake policies to prevent warnings (Closes #676) (#677) -- Start using gsl::span instead of raw pointers (#393) (#678) +- Start using gsl::span instead of raw pointers (#393) (#678) Python - PySB parsing fix (#669) -- Fix failure to propagate BLAS_LIBS contents (#665) -- Require setuptools at setup (#673) +- Fix failure to propagate BLAS_LIBS contents (#665) +- Require setuptools at setup (#673) - Updated PEtab import to allow for different noise models @@ -1355,7 +1387,7 @@ Bugfixes: Maintenance: -- use newer CI images +- use newer CI images ### v0.9.4 (2019-02-11) @@ -1387,9 +1419,9 @@ Bugfixes: - fixes a critical bug in the newton solver - fixes multiple bugs in sbml import for degenerate models, empty stoichiometry assignments and conversion factors - improved error messages for sbml import -- #560 -- #557 -- #559 +- #560 +- #557 +- #559 ### v0.9.1 (2019-01-21) @@ -1418,7 +1450,7 @@ Features / improvements: - Allow more detailed finiteness checks (#514) Bugfixes: - - #491 + - #491 Maintenance: - Several improvements to travis log sizes and folding @@ -1475,7 +1507,7 @@ Maintenance: ### v0.7.11 (2018-10-15) - [python] Added numpy and python wrappers that provide a more user friendly python API -- [python] Enable import of SBML models with non-float assignment rules +- [python] Enable import of SBML models with non-float assignment rules - [python] Enable handling of exceptions in python - [python] Enable nativ python access to std::vector data-structures - [core] Provide an API for more fine-grained control over sensitivity tolerances and steady-state tolerances @@ -1555,7 +1587,7 @@ Features: Major bugfixes: - Fix python sbml model import / compilation error (undefined function) -- Fix model preequilibration +- Fix model preequilibration Minor fixes: - Various fixes for mingw compilation of python source distribution @@ -1585,8 +1617,8 @@ WARNING: Implement experimental support for python via swig. Python interface is now usable, but API will still receive some updates in the future. -WARNING: -- There is a bug in sensitivity computation for Python-generated models +WARNING: +- There is a bug in sensitivity computation for Python-generated models - Matlab C++ compilation will fail due to undefined M_PI -> Please use v0.7.0 diff --git a/CITATION.cff b/CITATION.cff index c5e6d8d6ff..d251658032 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,32 +1,32 @@ authors: - - + - family-names: "Fröhlich" given-names: "Fabian" orcid: "https://orcid.org/0000-0002-5360-4292" - - + - family-names: "Weindl" given-names: "Daniel" orcid: "https://orcid.org/0000-0001-9963-6057" - - + - family-names: "Schälte" given-names: "Yannik" orcid: "https://orcid.org/0000-0003-1293-820X" - - + - family-names: "Pathirana" given-names: "Dilan" orcid: "https://orcid.org/0000-0001-7000-2659" - - + - family-names: "Paszkowski" given-names: "Lukasz" - - + - family-names: "Lines" given-names: "Glenn Terje" orcid: "https://orcid.org/0000-0002-6294-1805" - - + - family-names: "Stapor" given-names: "Paul" orcid: "https://orcid.org/0000-0002-7567-3985" - - + - family-names: "Hasenauer" given-names: "Jan" orcid: "https://orcid.org/0000-0002-4935-3312" @@ -42,34 +42,34 @@ preferred-citation: start: 1 end: 1 authors: - - + - family-names: "Fröhlich" given-names: "Fabian" orcid: "https://orcid.org/0000-0002-5360-4292" - - + - family-names: "Weindl" given-names: "Daniel" orcid: "https://orcid.org/0000-0001-9963-6057" - - + - family-names: "Schälte" given-names: "Yannik" orcid: "https://orcid.org/0000-0003-1293-820X" - - + - family-names: "Pathirana" given-names: "Dilan" orcid: "https://orcid.org/0000-0001-7000-2659" - - + - family-names: "Paszkowski" given-names: "Lukasz" - - + - family-names: "Lines" given-names: "Glenn Terje" orcid: "https://orcid.org/0000-0002-6294-1805" - - + - family-names: "Stapor" given-names: "Paul" orcid: "https://orcid.org/0000-0002-7567-3985" - - + - family-names: "Hasenauer" given-names: "Jan" orcid: "https://orcid.org/0000-0002-4935-3312" diff --git a/CMakeLists.txt b/CMakeLists.txt index 8595530db6..cdf0ff8fed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,8 @@ endif() if(DEFINED ENV{SWIG}) message(STATUS "Setting SWIG_EXECUTABLE to $ENV{SWIG} ($SWIG)") + unset(SWIG_VERSION CACHE) + unset(SWIG_DIR CACHE) set(SWIG_EXECUTABLE $ENV{SWIG}) endif() @@ -151,12 +153,18 @@ if(${BLAS} STREQUAL "MKL" OR DEFINED ENV{MKLROOT}) CACHE STRING "") endif() elseif(NOT DEFINED ENV{BLAS_LIBS} AND NOT DEFINED ENV{BLAS_CFLAGS}) - set(BLAS_INCLUDE_DIRS - "" - CACHE STRING "") - set(BLAS_LIBRARIES - -lcblas - CACHE STRING "") + # if nothing is specified via environment variables, let's try FindBLAS + find_package(BLAS) + if(NOT BLAS_FOUND) + # Nothing specified by the user and FindBLAS didn't find anything; let's try + # if cblas is available on the system paths. + set(BLAS_INCLUDE_DIRS + "" + CACHE STRING "") + set(BLAS_LIBRARIES + -lcblas + CACHE STRING "") + endif() endif() add_compile_definitions(AMICI_BLAS_${BLAS}) @@ -175,6 +183,7 @@ add_custom_target( # Library source files set(AMICI_SRC_LIST src/symbolic_functions.cpp + src/splinefunctions.cpp src/cblas.cpp src/amici.cpp src/misc.cpp @@ -223,6 +232,7 @@ set(AMICI_SRC_LIST include/amici/solver.h include/amici/solver_idas.h include/amici/spline.h + include/amici/splinefunctions.h include/amici/steadystateproblem.h include/amici/sundials_linsol_wrapper.h include/amici/sundials_matrix_wrapper.h diff --git a/README.md b/README.md index 7531627e94..006ae200dc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Advanced Multilanguage Interface for CVODES and IDAS -## About +## About AMICI provides a multi-language (Python, C++, Matlab) interface for the [SUNDIALS](https://computing.llnl.gov/projects/sundials/) solvers @@ -143,8 +143,8 @@ When using AMICI in your project, please cite eprint = {https://academic.oup.com/bioinformatics/advance-article-pdf/doi/10.1093/bioinformatics/btab227/36866220/btab227.pdf}, } ``` - -When presenting work that employs AMICI, feel free to use one of the icons in + +When presenting work that employs AMICI, feel free to use one of the icons in [documentation/gfx/](https://github.com/AMICI-dev/AMICI/tree/master/documentation/gfx), which are available under a [CC0](https://github.com/AMICI-dev/AMICI/tree/master/documentation/gfx/LICENSE.md) diff --git a/binder/Dockerfile b/binder/Dockerfile index daa51b99d1..bda8f7e1af 100644 --- a/binder/Dockerfile +++ b/binder/Dockerfile @@ -48,4 +48,3 @@ RUN . ./.profile && python3 -m build --sdist python/sdist && \ scripts/buildBNGL.sh ENV BNGPATH="${HOME}/ThirdParty/BioNetGen-2.7.0" - diff --git a/binder/overview.ipynb b/binder/overview.ipynb index 1b780cebb3..9c4959f372 100644 --- a/binder/overview.ipynb +++ b/binder/overview.ipynb @@ -1,6 +1,7 @@ { "cells": [ { + "attachments": {}, "cell_type": "markdown", "id": "f7ebed12-4309-4c92-a54e-da80ccd2d5e7", "metadata": {}, @@ -34,6 +35,13 @@ "* [Interfacing JAX](../python/examples/example_jax/ExampleJax.ipynb)\n", "\n", " Provides guidance on how to combine AMICI with differential programming frameworks such as JAX.\n" + "* [Efficient spline interpolation](../python/examples/example_splines/ExampleSplines.ipynb)\n", + "\n", + " Shows how to add annotated spline formulas to existing SBML models in order to speed up AMICI's model import.\n", + "\n", + "* [A real-world application of splines](../python/examples/example_splines_swameye/ExampleSplinesSwameye2003.ipynb)\n", + "\n", + " An illustration of how to apply AMICI's spline functionalities to parameter estimation for a reaction network.\n" ] } ], diff --git a/cmake/version.cmake b/cmake/version.cmake index 3f7b054a03..8dfe45c3c6 100644 --- a/cmake/version.cmake +++ b/cmake/version.cmake @@ -3,7 +3,7 @@ if(Git_FOUND) execute_process( COMMAND sh -c - "${GIT_EXECUTABLE} describe --abbrev=4 --dirty=-dirty --always --tags | cut -c 2- | tr -d '\n' | sed s/-/./" + "'${GIT_EXECUTABLE}' describe --abbrev=4 --dirty=-dirty --always --tags | cut -c 2- | tr -d '\n' | sed s/-/./" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE PROJECT_VERSION_GIT) endif() diff --git a/container/README.md b/container/README.md index 5396681df4..f6b51719af 100644 --- a/container/README.md +++ b/container/README.md @@ -17,7 +17,7 @@ git archive -o container/amici.tar.gz --format=tar.gz HEAD cd container && docker build -t $USER/amici:latest . ``` Note that this will include files from the last commit, but no uncommitted -changes. +changes. ### Pull published image @@ -34,7 +34,7 @@ In the AMICI base directory run: ```bash # prepare amici files to be copied to the image # Note that this will include files from the last commit, but no uncommitted -# changes. +# changes. git archive -o container/amici.tar.gz --format=tar.gz HEAD # install spython if necessary test -x "$(command -v spython)" || pip install spython diff --git a/documentation/CI.md b/documentation/CI.md index 3fd72d99f9..34b08355dc 100644 --- a/documentation/CI.md +++ b/documentation/CI.md @@ -12,7 +12,7 @@ This includes the following steps: More details are provided in the sections below. The CI scripts and tests can be found in `tests/` and `scripts/`. Some of the -tests are integrated with CMake, see `make help` in the build directory. +tests are integrated with CMake, see `make help` in the build directory. ## C++ unit and integration tests @@ -61,7 +61,7 @@ obtained from the Python and C++ are compared to results saved in an HDF5 file (`tests/cpp/expectedResults.h5`). Settings and data for the test simulations are also specified in this file. -**Note:** The C++ code for the models is included in the repository under +**Note:** The C++ code for the models is included in the repository under `models/`. This code is to be updated whenever `amici::Model` changes. @@ -72,23 +72,23 @@ Regeneration of the model code has to be done whenever `amici::Model` or the Matlab model import routines change. This is done with - + tests/cpp/wrapTestModels.m **Note:** This is currently only possible from Matlab < R2018a. This should change as soon as 1) all second-order sensitivity code is ported to C++/Python, 2) a non-SBML import exists for Python and 3) support for events has been added for Python. - - + + ### Regenerating expected results To update test results, run `make test` in the build directory, -replace `tests/cpp/expectedResults.h5` by -`tests/cpp/writeResults.h5.bak` +replace `tests/cpp/expectedResults.h5` by +`tests/cpp/writeResults.h5.bak` [ONLY DO THIS AFTER TRIPLE CHECKING CORRECTNESS OF RESULTS] Before replacing the test results, confirm that only expected datasets have -changed, e.g. using +changed, e.g. using h5diff -v --relative 1e-8 tests/cpp/expectedResults.h5 tests/cpp/writeResults.h5.bak | less @@ -96,6 +96,6 @@ changed, e.g. using ## Adding/Updating tests To add new tests add a new corresponding python script (see, e.g., -`./tests/generateTestConfig/example_dirac.py`) and add it to and run +`./tests/generateTestConfig/example_dirac.py`) and add it to and run `tests/generateTestConfigurationForExamples.sh`. Then regenerate the expected test results (see above). diff --git a/documentation/ExampleSplines.ipynb b/documentation/ExampleSplines.ipynb new file mode 120000 index 0000000000..5512101de9 --- /dev/null +++ b/documentation/ExampleSplines.ipynb @@ -0,0 +1 @@ +../python/examples/example_splines/ExampleSplines.ipynb \ No newline at end of file diff --git a/documentation/ExampleSplinesSwameye2003.ipynb b/documentation/ExampleSplinesSwameye2003.ipynb new file mode 120000 index 0000000000..1a3b8aab63 --- /dev/null +++ b/documentation/ExampleSplinesSwameye2003.ipynb @@ -0,0 +1 @@ +../python/examples/example_splines_swameye/ExampleSplinesSwameye2003.ipynb \ No newline at end of file diff --git a/documentation/MATLAB_.md b/documentation/MATLAB_.md index 40ecc87c31..760a228df9 100644 --- a/documentation/MATLAB_.md +++ b/documentation/MATLAB_.md @@ -2,7 +2,7 @@ In the following we will give a detailed overview how to specify models in MATLAB and how to call the generated simulation files. -## Model Definition +## Model Definition This guide will guide the user on how to specify models in MATLAB. For example implementations see the examples in the matlab/examples directory. @@ -120,7 +120,7 @@ Specifying events is optional. Events are specified in terms of a trigger functi Events may depend on states, parameters and constants but __not__ on observables. -For more details about event support see https://doi.org/10.1093/bioinformatics/btw764 +For more details about event support see https://doi.org/10.1093/bioinformatics/btw764 ### Standard Deviation @@ -139,7 +139,7 @@ They can depend on time and parameters but must not depend on the states or obse ### Objective Function -By default, AMICI assumes a normal noise model and uses the corresponding negative log-likelihood +By default, AMICI assumes a normal noise model and uses the corresponding negative log-likelihood J = 1/2*sum(((y_i(t)-my_ti)/sigma_y_i)^2 + log(2*pi*sigma_y^2) @@ -193,7 +193,7 @@ Here for proof of concept: * Install the python package as described in the documentation * Ensure `pyversion` shows the correct python version (3.6 or 3.7) * Then, from within the AMICI `matlab/` directory: - + ``` sbml_importer = py.amici.SbmlImporter('../python/examples/example_steadystate/model_steadystate_scaled.xml') sbml_importer.sbml2amici('steadystate', 'steadystate_example_from_python') diff --git a/documentation/README.md b/documentation/README.md index ac5d1b12b7..af9f33320e 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -78,12 +78,12 @@ Graphics for documentation are kept in `documentation/gfx/`. for offline use. * Please stick to the limit of 80 characters per line for readability of raw - Markdown files where possible. + Markdown files where possible. However, note that some Markdown interpreters can handle line breaks within links and headings, whereas others cannot. Here, compatibility is preferred - over linebreaks. - + over linebreaks. + * Avoid trailing whitespace ## Maintaining the list of publications diff --git a/documentation/_templates/autosummary/class.rst b/documentation/_templates/autosummary/class.rst index 030e2a4c16..ed9e3761ef 100644 --- a/documentation/_templates/autosummary/class.rst +++ b/documentation/_templates/autosummary/class.rst @@ -36,5 +36,3 @@ {%- endfor %} {% endif %} {% endblock %} - - diff --git a/documentation/code_review_guide.md b/documentation/code_review_guide.md index 456b78709e..43d6e815c0 100644 --- a/documentation/code_review_guide.md +++ b/documentation/code_review_guide.md @@ -5,7 +5,7 @@ A guide for reviewing code and having your code reviewed by others. ## Everyone * Don't be too protective of your code -* Accept that, to a large extent, coding decisions are a matter of personal +* Accept that, to a large extent, coding decisions are a matter of personal preference * Don't get personal * Ask for clarification @@ -13,25 +13,25 @@ A guide for reviewing code and having your code reviewed by others. * Try to understand your counterpart's perspective * Clarify how strong you feel about each discussion point -## Reviewing code +## Reviewing code * If there are no objective advantages, don't force your style on others * Ask questions instead of making demands * Assume the author gave his best -* Mind the scope (many things are nice to have, but might be out of scope - of the current change - open a new issue) -* The goal is "good enough", not "perfect" +* Mind the scope (many things are nice to have, but might be out of scope + of the current change - open a new issue) +* The goal is "good enough", not "perfect" * Be constructive -* You do not always have to request changes +* You do not always have to request changes -## Having your code reviewed +## Having your code reviewed * Don't take it personal - the review is on the code, not on you * Code reviews take time, appreciate the reviewer's comments * Assume the reviewer did his best (but might still be wrong) -* Keep code changes small (e.g. separate wide reformatting from actual code +* Keep code changes small (e.g. separate wide reformatting from actual code changes to facility review) -* If the reviewer does not understand your code, probably many others won't +* If the reviewer does not understand your code, probably many others won't either ## Checklist @@ -42,10 +42,10 @@ A guide for reviewing code and having your code reviewed by others. * [ ] Meaningful identifiers are used * [ ] Corner-cases are covered, cases not covered fail loudly * [ ] The code can be expected to scale well (enough) -* [ ] The code is well documented (e.g., input, operation, output), but +* [ ] The code is well documented (e.g., input, operation, output), but without trivial comments * [ ] The code is [SOLID](https://en.wikipedia.org/wiki/SOLID) -* [ ] New code is added in the most meaningful place (i.e. matches the +* [ ] New code is added in the most meaningful place (i.e. matches the current architecture) * [ ] No magic numbers * [ ] No hard-coded values that should be user inputs diff --git a/documentation/conf.py b/documentation/conf.py index ec2460d38d..96209e4c31 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -10,17 +10,16 @@ import re import subprocess import sys -import typing +# need to import before setting typing.TYPE_CHECKING=True, fails otherwise +import amici import exhale.deploy import exhale_multiproject_monkeypatch import mock -from exhale import configs as exhale_configs -from sphinx.transforms.post_transforms import ReferencesResolver - -# need to import before setting typing.TYPE_CHECKING=True, fails otherwise import pandas as pd import sympy as sp +from exhale import configs as exhale_configs +from sphinx.transforms.post_transforms import ReferencesResolver exhale_multiproject_monkeypatch, pd, sp # to avoid removal of unused import @@ -33,10 +32,11 @@ def my_exhale_generate_doxygen(doxygen_input): # run mtocpp_post doxy_xml_dir = exhale_configs._doxygen_xml_output_directory - if 'matlab' in doxy_xml_dir: - print('Running mtocpp_post on ', doxy_xml_dir) - mtocpp_post = os.path.join(amici_dir, 'ThirdParty', 'mtocpp-master', - 'build', 'mtocpp_post') + if "matlab" in doxy_xml_dir: + print("Running mtocpp_post on ", doxy_xml_dir) + mtocpp_post = os.path.join( + amici_dir, "ThirdParty", "mtocpp-master", "build", "mtocpp_post" + ) subprocess.run([mtocpp_post, doxy_xml_dir]) # let exhale do its job @@ -48,27 +48,26 @@ def my_exhale_generate_doxygen(doxygen_input): # BEGIN Monkeypatch breathe -from breathe.renderer.sphinxrenderer import \ - DomainDirectiveFactory as breathe_DomainDirectiveFactory +from breathe.renderer.sphinxrenderer import ( + DomainDirectiveFactory as breathe_DomainDirectiveFactory, +) -old_breathe_DomainDirectiveFactory_create = \ - breathe_DomainDirectiveFactory.create +old_breathe_DomainDirectiveFactory_create = breathe_DomainDirectiveFactory.create def my_breathe_DomainDirectiveFactory_create(domain: str, args): - if domain != 'mat': + if domain != "mat": return old_breathe_DomainDirectiveFactory_create(domain, args) - from sphinxcontrib.matlab import MATLABDomain, MatClassmember + from sphinxcontrib.matlab import MatClassmember, MATLABDomain matlab_classes = {k: (v, k) for k, v in MATLABDomain.directives.items()} - matlab_classes['variable'] = (MatClassmember, 'attribute') + matlab_classes["variable"] = (MatClassmember, "attribute") cls, name = matlab_classes[args[0]] - return cls(domain + ':' + name, *args[1:]) + return cls(domain + ":" + name, *args[1:]) -breathe_DomainDirectiveFactory.create = \ - my_breathe_DomainDirectiveFactory_create +breathe_DomainDirectiveFactory.create = my_breathe_DomainDirectiveFactory_create # END Monkeypatch breathe @@ -76,21 +75,23 @@ def my_breathe_DomainDirectiveFactory_create(domain: str, args): def install_mtocpp(): """Install mtocpp (Matlab doxygen filter)""" - cmd = os.path.join(amici_dir, 'scripts', 'downloadAndBuildMtocpp.sh') - ret = subprocess.run(cmd, shell=True, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + cmd = os.path.join(amici_dir, "scripts", "downloadAndBuildMtocpp.sh") + ret = subprocess.run( + cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) if ret.returncode != 0: - print(ret.stdout.decode('utf-8')) - raise RuntimeError('downloadAndBuildMtocpp.sh failed') + print(ret.stdout.decode("utf-8")) + raise RuntimeError("downloadAndBuildMtocpp.sh failed") def install_doxygen(): """Get a more recent doxygen""" - version = '1.9.6' - doxygen_exe = os.path.join(amici_dir, 'ThirdParty', - f'doxygen-{version}', 'bin', 'doxygen') + version = "1.9.7" + doxygen_exe = os.path.join( + amici_dir, "ThirdParty", f"doxygen-{version}", "bin", "doxygen" + ) # to create a symlink to doxygen in a location that is already on PATH - some_dir_on_path = os.environ['PATH'].split(os.pathsep)[0] + some_dir_on_path = os.environ["PATH"].split(os.pathsep)[0] cmd = ( f"cd '{os.path.join(amici_dir, 'ThirdParty')}' " f"&& wget 'https://www.doxygen.nl/files/" @@ -99,10 +100,9 @@ def install_doxygen(): f"&& ln -sf '{doxygen_exe}' '{some_dir_on_path}'" ) subprocess.run(cmd, shell=True, check=True) - assert os.path.islink(os.path.join(some_dir_on_path, 'doxygen')) + assert os.path.islink(os.path.join(some_dir_on_path, "doxygen")) # verify it's available - res = subprocess.run(['doxygen', '--version'], - check=False, capture_output=True) + res = subprocess.run(["doxygen", "--version"], check=False, capture_output=True) print(res.stdout.decode(), res.stderr.decode()) assert version in res.stdout.decode() @@ -118,17 +118,12 @@ def install_doxygen(): # -- RTD custom build -------------------------------------------------------- # only execute those commands when running from RTD -if 'READTHEDOCS' in os.environ and os.environ['READTHEDOCS']: +if "READTHEDOCS" in os.environ and os.environ["READTHEDOCS"]: install_doxygen() # Required for matlab doxygen processing install_mtocpp() -# Install AMICI if not already present -typing.TYPE_CHECKING = True -import amici -typing.TYPE_CHECKING = False - # -- Project information ----------------------------------------------------- # The short X.Y version @@ -136,15 +131,15 @@ def install_doxygen(): # The full version, including alpha/beta/rc tags release = version -project = 'AMICI' -copyright = '2020, The AMICI developers' -author = 'The AMICI developers' -title = 'AMICI Documentation' +project = "AMICI" +copyright = "2020, The AMICI developers" +author = "The AMICI developers" +title = "AMICI Documentation" # -- Mock out some problematic modules------------------------------------- # Note that for sub-modules, all parent modules must be listed explicitly. -autodoc_mock_imports = ['_amici', 'amici._amici'] +autodoc_mock_imports = ["_amici", "amici._amici"] for mod_name in autodoc_mock_imports: sys.modules[mod_name] = mock.MagicMock() @@ -158,45 +153,39 @@ def install_doxygen(): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'readthedocs_ext.readthedocs', + "readthedocs_ext.readthedocs", # Required, e.g. for PEtab-derived classes where the base class has non-rst # docstrings - 'sphinx.ext.napoleon', - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.coverage', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary', - 'sphinx.ext.viewcode', - 'sphinx.ext.mathjax', - 'sphinxcontrib.matlab', - 'nbsphinx', - 'IPython.sphinxext.ipython_console_highlighting', - 'recommonmark', - 'sphinx_autodoc_typehints', - 'hoverxref.extension', - 'breathe', - 'exhale', + "sphinx.ext.napoleon", + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.coverage", + "sphinx.ext.intersphinx", + "sphinx.ext.autosummary", + "sphinx.ext.viewcode", + "sphinx.ext.mathjax", + "sphinxcontrib.matlab", + "nbsphinx", + "IPython.sphinxext.ipython_console_highlighting", + "recommonmark", + "sphinx_autodoc_typehints", + "hoverxref.extension", + "breathe", + "exhale", ] intersphinx_mapping = { - 'pysb': ('https://pysb.readthedocs.io/en/stable/', None), - 'petab': ( - 'https://petab.readthedocs.io/projects/libpetab-python/en/latest/', - None - ), - 'pandas': ('https://pandas.pydata.org/docs/', None), - 'numpy': ('https://numpy.org/devdocs/', None), - 'sympy': ('https://docs.sympy.org/latest/', None), - 'python': ('https://docs.python.org/3', None), + "pysb": ("https://pysb.readthedocs.io/en/stable/", None), + "petab": ("https://petab.readthedocs.io/projects/libpetab-python/en/latest/", None), + "pandas": ("https://pandas.pydata.org/docs/", None), + "numpy": ("https://numpy.org/devdocs/", None), + "sympy": ("https://docs.sympy.org/latest/", None), + "python": ("https://docs.python.org/3", None), } # Add notebooks prolog with binder links # get current git reference -ret = subprocess.run( - "git rev-parse HEAD".split(" "), - capture_output=True -) +ret = subprocess.run("git rev-parse HEAD".split(" "), capture_output=True) ref = ret.stdout.rstrip().decode() nbsphinx_prolog = ( f"{{% set {ref=} %}}" @@ -213,16 +202,16 @@ def install_doxygen(): ) # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -235,27 +224,27 @@ def install_doxygen(): # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . exclude_patterns = [ - '_build', - 'Thumbs.db', - '.DS_Store', - '**.ipynb_checkpoints', - 'numpy.py', - 'INSTALL.md', - 'MATLAB_.md', - 'CPP_.md', - 'gfx' + "_build", + "Thumbs.db", + ".DS_Store", + "**.ipynb_checkpoints", + "numpy.py", + "INSTALL.md", + "MATLAB_.md", + "CPP_.md", + "gfx", ] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # autodoc autodoc_default_options = { - 'special-members': '__init__', - 'inherited-members': True, + "special-members": "__init__", + "inherited-members": True, } # sphinx-autodoc-typehints @@ -265,17 +254,17 @@ def install_doxygen(): # hoverxref hoverxref_auto_ref = True -hoverxref_roles = ['term'] -hoverxref_domains = ['py'] +hoverxref_roles = ["term"] +hoverxref_domains = ["py"] hoverxref_role_types = { - 'hoverxref': 'tooltip', - 'ref': 'tooltip', - 'term': 'tooltip', - 'obj': 'tooltip', - 'func': 'tooltip', - 'mod': 'tooltip', - 'meth': 'tooltip', - 'class': 'tooltip', + "hoverxref": "tooltip", + "ref": "tooltip", + "term": "tooltip", + "obj": "tooltip", + "func": "tooltip", + "mod": "tooltip", + "meth": "tooltip", + "class": "tooltip", } # breathe settings @@ -302,47 +291,47 @@ def install_doxygen(): "verboseBuild": True, } -mtocpp_filter = os.path.join(amici_dir, 'matlab', 'mtoc', - 'config', 'mtocpp_filter.sh') +mtocpp_filter = os.path.join(amici_dir, "matlab", "mtoc", "config", "mtocpp_filter.sh") exhale_projects_args = { "AMICI_CPP": { - "exhaleDoxygenStdin": "\n".join([ - "INPUT = ../include/amici", - "BUILTIN_STL_SUPPORT = YES", - "PREDEFINED += EXHALE_DOXYGEN_SHOULD_SKIP_THIS", - "EXCLUDE += ../include/amici/interface_matlab.h", - "EXCLUDE += ../include/amici/returndata_matlab.h", - "EXCLUDE += ../include/amici/spline.h", - # amici::log collides with amici::${some_enum}::log - # potentially fixed in - # https://github.com/svenevs/exhale/commit/c924df2e139a09fbacd07587779c55fd0ee4e00b - # and can be un-excluded after the next exhale release - "EXCLUDE += ../include/amici/symbolic_functions.h", - ]), + "exhaleDoxygenStdin": "\n".join( + [ + "INPUT = ../include/amici", + "BUILTIN_STL_SUPPORT = YES", + "PREDEFINED += EXHALE_DOXYGEN_SHOULD_SKIP_THIS", + "EXCLUDE += ../include/amici/interface_matlab.h", + "EXCLUDE += ../include/amici/returndata_matlab.h", + "EXCLUDE += ../include/amici/spline.h", + # amici::log collides with amici::${some_enum}::log + # potentially fixed in + # https://github.com/svenevs/exhale/commit/c924df2e139a09fbacd07587779c55fd0ee4e00b + # and can be un-excluded after the next exhale release + "EXCLUDE += ../include/amici/symbolic_functions.h", + ] + ), "containmentFolder": "_exhale_cpp_api", "rootFileTitle": "AMICI C++ API", - "afterTitleDescription": - "AMICI C++ library functions", + "afterTitleDescription": "AMICI C++ library functions", }, # Third Party Project Includes "AMICI_Matlab": { - "exhaleDoxygenStdin": "\n".join([ - "INPUT = ../matlab", - "EXTENSION_MAPPING = .m=C++", - "FILTER_PATTERNS = " - f"*.m={mtocpp_filter}", - "EXCLUDE += ../matlab/examples", - "EXCLUDE += ../matlab/mtoc", - "EXCLUDE += ../matlab/SBMLimporter", - "EXCLUDE += ../matlab/auxiliary", - "EXCLUDE += ../matlab/tests", - "PREDEFINED += EXHALE_DOXYGEN_SHOULD_SKIP_THIS", - ]), + "exhaleDoxygenStdin": "\n".join( + [ + "INPUT = ../matlab", + "EXTENSION_MAPPING = .m=C++", + "FILTER_PATTERNS = " f"*.m={mtocpp_filter}", + "EXCLUDE += ../matlab/examples", + "EXCLUDE += ../matlab/mtoc", + "EXCLUDE += ../matlab/SBMLimporter", + "EXCLUDE += ../matlab/auxiliary", + "EXCLUDE += ../matlab/tests", + "PREDEFINED += EXHALE_DOXYGEN_SHOULD_SKIP_THIS", + ] + ), "containmentFolder": "_exhale_matlab_api", "rootFileTitle": "AMICI Matlab API", - "afterTitleDescription": - "AMICI Matlab library functions", - "lexerMapping": {r'.*\.m$': 'matlab'} + "afterTitleDescription": "AMICI Matlab library functions", + "lexerMapping": {r".*\.m$": "matlab"}, }, } # -- Options for HTML output ------------------------------------------------- @@ -350,7 +339,7 @@ def install_doxygen(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -378,7 +367,7 @@ def install_doxygen(): # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'AMICIdoc' +htmlhelp_basename = "AMICIdoc" # -- Options for LaTeX output ------------------------------------------------ @@ -386,15 +375,12 @@ def install_doxygen(): # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -404,18 +390,14 @@ def install_doxygen(): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'AMICI.tex', title, - author, 'manual'), + (master_doc, "AMICI.tex", title, author, "manual"), ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'amici', title, - [author], 1) -] +man_pages = [(master_doc, "amici", title, [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -423,78 +405,80 @@ def install_doxygen(): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'AMICI', title, - author, 'AMICI', 'Advanced Multilanguage Interface for CVODES and IDAS.', - 'Miscellaneous'), + ( + master_doc, + "AMICI", + title, + author, + "AMICI", + "Advanced Multilanguage Interface for CVODES and IDAS.", + "Miscellaneous", + ), ] # Custom processing routines for docstrings and signatures typemaps = { - 'std::vector< amici::realtype,std::allocator< amici::realtype > >': - 'DoubleVector', - 'std::vector< double,std::allocator< double > >': - 'DoubleVector', - 'std::vector< int,std::allocator< int > >': - 'IntVector', - 'std::vector< amici::ParameterScaling,std::allocator< ' - 'amici::ParameterScaling >': 'ParameterScalingVector', - 'std::vector< std::string,std::allocator< std::string > >': - 'StringVector', - 'std::vector< bool,std::allocator< bool > >': - 'BoolVector', - 'std::map< std::string,amici::realtype,std::less< std::string >,' - 'std::allocator< std::pair< std::string const,amici::realtype > > >': - 'StringDoubleMap', - 'std::vector< amici::ExpData *,std::allocator< amici::ExpData * > >': - 'ExpDataPtrVector', - 'std::vector< std::unique_ptr< amici::ReturnData >,std::allocator< ' - 'std::unique_ptr< amici::ReturnData > > >': - 'Iterable[ReturnData]', - 'std::unique_ptr< amici::ExpData >': - 'ExpData', - 'std::unique_ptr< amici::ReturnData >': - 'ReturnData', - 'std::unique_ptr< amici::Solver >': - 'Solver', - 'amici::realtype': - 'float', + "std::vector< amici::realtype,std::allocator< amici::realtype > >": "DoubleVector", + "std::vector< double,std::allocator< double > >": "DoubleVector", + "std::vector< int,std::allocator< int > >": "IntVector", + "std::vector< amici::ParameterScaling,std::allocator< " + "amici::ParameterScaling >": "ParameterScalingVector", + "std::vector< std::string,std::allocator< std::string > >": "StringVector", + "std::vector< bool,std::allocator< bool > >": "BoolVector", + "std::map< std::string,amici::realtype,std::less< std::string >," + "std::allocator< std::pair< std::string const,amici::realtype > > >": "StringDoubleMap", + "std::vector< amici::ExpData *,std::allocator< amici::ExpData * > >": "ExpDataPtrVector", + "std::vector< std::unique_ptr< amici::ReturnData >,std::allocator< " + "std::unique_ptr< amici::ReturnData > > >": "Iterable[ReturnData]", + "std::unique_ptr< amici::ExpData >": "ExpData", + "std::unique_ptr< amici::ReturnData >": "ReturnData", + "std::unique_ptr< amici::Solver >": "Solver", + "amici::realtype": "float", } vector_types = { - 'IntVector': ':class:`int`', - 'BoolVector': ':class:`bool`', - 'DoubleVector': ':class:`float`', - 'StringVector': ':class:`str`', - 'ExpDataPtrVector': ':class:`amici.amici.ExpData`', + "IntVector": ":class:`int`", + "BoolVector": ":class:`bool`", + "DoubleVector": ":class:`float`", + "StringVector": ":class:`str`", + "ExpDataPtrVector": ":class:`amici.amici.ExpData`", } def process_docstring(app, what, name, obj, options, lines): # only apply in the amici.amici module - if len(name.split('.')) < 2 or name.split('.')[1] != 'amici': + if len(name.split(".")) < 2 or name.split(".")[1] != "amici": return # add custom doc to swig generated classes - if len(name.split('.')) == 3 and name.split('.')[2] in \ - ['IntVector', 'BoolVector', 'DoubleVector', 'StringVector', - 'ExpDataPtrVector']: - cname = name.split('.')[2] + if len(name.split(".")) == 3 and name.split(".")[2] in [ + "IntVector", + "BoolVector", + "DoubleVector", + "StringVector", + "ExpDataPtrVector", + ]: + cname = name.split(".")[2] lines.append( - f'Swig-Generated class templating common python ' - f'types including :class:`Iterable` ' - f'[{vector_types[cname]}] ' - f'and ' - f':class:`numpy.array` [{vector_types[cname]}] to facilitate' - ' interfacing with C++ bindings.' + f"Swig-Generated class templating common python " + f"types including :class:`Iterable` " + f"[{vector_types[cname]}] " + f"and " + f":class:`numpy.array` [{vector_types[cname]}] to facilitate" + " interfacing with C++ bindings." ) return - if len(name.split('.')) == 3 and name.split('.')[2] in \ - ['ExpDataPtr', 'ReturnDataPtr', 'ModelPtr', 'SolverPtr']: - cname = name.split('.')[2] + if len(name.split(".")) == 3 and name.split(".")[2] in [ + "ExpDataPtr", + "ReturnDataPtr", + "ModelPtr", + "SolverPtr", + ]: + cname = name.split(".")[2] lines.append( - f'Swig-Generated class that implements smart pointers to ' + f"Swig-Generated class that implements smart pointers to " f'{cname.replace("Ptr", "")} as objects.' ) return @@ -505,9 +489,12 @@ def process_docstring(app, what, name, obj, options, lines): while len(lines): line = lines.pop(0) - if re.match(r':(type|rtype|param|return)', line) and \ - len(lines_clean) and lines_clean[-1] != '': - lines_clean.append('') + if ( + re.match(r":(type|rtype|param|return)", line) + and len(lines_clean) + and lines_clean[-1] != "" + ): + lines_clean.append("") lines_clean.append(line) lines.extend(lines_clean) @@ -517,14 +504,10 @@ def process_docstring(app, what, name, obj, options, lines): for old, new in typemaps.items(): lines[i] = lines[i].replace(old, new) lines[i] = re.sub( - r'amici::(Model|Solver|ExpData) ', - r':class:`amici\.amici\.\1\`', - lines[i] + r"amici::(Model|Solver|ExpData) ", r":class:`amici\.amici\.\1\`", lines[i] ) lines[i] = re.sub( - r'amici::(runAmiciSimulation[s]?)', - r':func:`amici\.amici\.\1`', - lines[i] + r"amici::(runAmiciSimulation[s]?)", r":func:`amici\.amici\.\1`", lines[i] ) @@ -535,44 +518,45 @@ def fix_typehints(sig: str) -> str: for old, new in typemaps.items(): sig = sig.replace(old, new) - sig = sig.replace('void', 'None') - sig = sig.replace('amici::realtype', 'float') - sig = sig.replace('std::string', 'str') - sig = sig.replace('double', 'float') - sig = sig.replace('long', 'int') - sig = sig.replace('char const *', 'str') - sig = sig.replace('amici::', '') - sig = sig.replace('sunindextype', 'int') - sig = sig.replace('H5::H5File', 'object') + sig = sig.replace("void", "None") + sig = sig.replace("amici::realtype", "float") + sig = sig.replace("std::string", "str") + sig = sig.replace("double", "float") + sig = sig.replace("long", "int") + sig = sig.replace("char const *", "str") + sig = sig.replace("amici::", "") + 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) + 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) + sig = re.sub(r" &(,|\))", r"\1", sig) + sig = re.sub(r" &$", r"", sig) # turn gsl_spans and pointers int Iterables - sig = re.sub(r'([\w.]+) \*', r'Iterable[\1]', sig) - sig = re.sub(r'gsl::span< ([\w.]+) >', r'Iterable[\1]', sig) + sig = re.sub(r"([\w.]+) \*", r"Iterable[\1]", sig) + sig = re.sub(r"gsl::span< ([\w.]+) >", r"Iterable[\1]", sig) # fix garbled output - sig = sig.replace(' >', '') + sig = sig.replace(" >", "") return sig -def process_signature(app, what: str, name: str, obj, options, signature, - return_annotation): +def process_signature( + app, what: str, name: str, obj, options, signature, return_annotation +): if signature is None: return # only apply in the amici.amici module - if name.split('.')[1] != 'amici': + if name.split(".")[1] != "amici": return signature = fix_typehints(signature) - if hasattr(obj, '__annotations__'): + if hasattr(obj, "__annotations__"): for ann in obj.__annotations__: obj.__annotations__[ann] = fix_typehints(obj.__annotations__[ann]) @@ -582,71 +566,92 @@ def process_signature(app, what: str, name: str, obj, options, signature, # this code fixes references in symlinked md files in documentation folder # link replacements must be in env.domains['std'].labels doclinks = { - 'documentation/development': '/development.md', - 'documentation/CI': '/ci.md', - 'documentation/code_review_guide': '/code_review_guide.md', + "documentation/development": "/development.md", + "documentation/CI": "/ci.md", + "documentation/code_review_guide": "/code_review_guide.md", } def process_missing_ref(app, env, node, contnode): - if not any(link in node['reftarget'] for link in doclinks): + if not any(link in node["reftarget"] for link in doclinks): return # speedup futile processing for old, new in doclinks.items(): - node['reftarget'] = node['reftarget'].replace(old, new) + node["reftarget"] = node["reftarget"].replace(old, new) cnode = node[0] - if 'refuri' in cnode: + if "refuri" in cnode: for old, new in doclinks.items(): - cnode['refuri'] = cnode['refuri'].replace(old, new) + cnode["refuri"] = cnode["refuri"].replace(old, new) - refdoc = node.get('refdoc', env.docname) + refdoc = node.get("refdoc", env.docname) resolver = ReferencesResolver(env.get_doctree(refdoc)) result = resolver.resolve_anyref(refdoc, node, cnode) return result def skip_member(app, what, name, obj, skip, options): - ignored = ['AbstractModel', 'CVodeSolver', 'IDASolver', 'Model_ODE', - 'Model_DAE', 'ConditionContext', 'checkSigmaPositivity', - 'createGroup', 'createGroup', 'equals', 'printErrMsgIdAndTxt', - 'wrapErrHandlerFn', 'printWarnMsgIdAndTxt', - 'AmiciApplication', 'writeReturnData', - 'writeReturnDataDiagnosis', 'attributeExists', 'locationExists', - 'createAndWriteDouble1DDataset', - 'createAndWriteDouble2DDataset', - 'createAndWriteDouble3DDataset', - 'createAndWriteInt1DDataset', 'createAndWriteInt2DDataset', - 'createAndWriteInt3DDataset', 'getDoubleDataset1D', - 'getDoubleDataset2D', 'getDoubleDataset3D', 'getIntDataset1D', - 'getIntScalarAttribute', 'getDoubleScalarAttribute', - 'stdVec2ndarray', 'SwigPyIterator', 'thisown'] + ignored = [ + "AbstractModel", + "CVodeSolver", + "IDASolver", + "Model_ODE", + "Model_DAE", + "ConditionContext", + "checkSigmaPositivity", + "createGroup", + "createGroup", + "equals", + "printErrMsgIdAndTxt", + "wrapErrHandlerFn", + "printWarnMsgIdAndTxt", + "AmiciApplication", + "writeReturnData", + "writeReturnDataDiagnosis", + "attributeExists", + "locationExists", + "createAndWriteDouble1DDataset", + "createAndWriteDouble2DDataset", + "createAndWriteDouble3DDataset", + "createAndWriteInt1DDataset", + "createAndWriteInt2DDataset", + "createAndWriteInt3DDataset", + "getDoubleDataset1D", + "getDoubleDataset2D", + "getDoubleDataset3D", + "getIntDataset1D", + "getIntScalarAttribute", + "getDoubleScalarAttribute", + "stdVec2ndarray", + "SwigPyIterator", + "thisown", + ] if name in ignored: return True - if name.startswith('_') and name != '__init__': + if name.startswith("_") and name != "__init__": return True # ignore various functions for std::vector<> types - if re.match(r'^`__. -New releases are created on Github and are automatically deployed to -`Zenodo `__ for +New releases are created on GitHub and are automatically deployed to +`Zenodo `__ for archiving and to obtain a digital object identifier (DOI) to make them citable. Furthermore, our `CI pipeline `__ will automatically create and deploy a new release on @@ -51,17 +51,11 @@ process described below: - Submit a pull request to the ``develop`` branch -- Make sure your code is documented appropriately - - - Run ``scripts/run-doxygen.sh`` to check completeness of your - documentation - -- Make sure your code is compatible with C++17, ``gcc`` and ``clang`` - (our CI pipeline will do this for you) +- Ensure all tests pass - When adding new functionality, please also provide test cases (see - ``tests/cpp/`` and - `documentation/CI.md `__) + ``tests/cpp/``, ``python/tests/``, + and `documentation/CI.md `__) - Write meaningful commit messages @@ -84,8 +78,8 @@ process described below: - Wait for feedback. If you do not receive feedback to your pull request within a week, please give us a friendly reminder. -Style guide -~~~~~~~~~~~ +Style/compatibility guide +~~~~~~~~~~~~~~~~~~~~~~~~~ General ^^^^^^^ @@ -105,12 +99,18 @@ Python - For the Python code we want to follow `PEP8 `__. Although this is not the case for all existing code, any new contributions should - do so. + do so. We use `black `__ + for code formatting. + + To run black as pre-commit hook, install the + `pre-commit `_ package + (e.g. ``pip install pre-commit``), and enable AMICI-hooks by running + ``pre-commit install`` from within the AMICI directory. - We use Python `type hints `__ for all functions (but not for class attributes, since they are not supported - by the current Python doxygen filter). In Python code type hints + by the current Python doxygen filter). In Python code, type hints should be used instead of doxygen ``@type``. For function docstrings, follow this format: @@ -135,15 +135,14 @@ Python C++ ^^^ -- We use C++14 +- We use C++17 -- We want to maintain compatibility with g++, clang and the Intel C++ +- We want to maintain compatibility with g++, clang, and the Intel C++ compiler -- For code formatting, we use the settings from ``.clang-format`` in - the root directory - -- *Details to be defined* +- For code formatting, we use ``clang-format`` and ``cmake-format``. They can + be invoked by ``make clang-format cmake-format`` from the CMake build + directory. Matlab ^^^^^^ diff --git a/documentation/gfx/logo_template.svg b/documentation/gfx/logo_template.svg index eb41200e30..f4bfb6169d 100644 --- a/documentation/gfx/logo_template.svg +++ b/documentation/gfx/logo_template.svg @@ -169,25 +169,25 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/documentation/implementation_discontinuities.rst b/documentation/implementation_discontinuities.rst index fc04bacce4..45e2d78aba 100644 --- a/documentation/implementation_discontinuities.rst +++ b/documentation/implementation_discontinuities.rst @@ -72,15 +72,15 @@ respective root function as argument. These will be automatically updated during events and take either 0 or 1 values as appropriate pre/post event limits. -In order to fully support SBML events and Piecewise functions, AMICI uses -the SUNDIALS functionality to only track zero crossings from negative to -positive. Accordingly, two root functions are necessary to keep track of -Heaviside functions and two Heaviside function helper variables will be -created, where one corresponds to the value of `Heaviside(...)` and one -to the value of `1-Heaviside(...)`. To ensure that Heaviside functions are -correctly evaluated at the beginning of the simulation, Heaviside functions -are implement as unit steps that evaluate to `1` at `0`. The arguments of -Heaviside functions are normalized such that respective properties of +In order to fully support SBML events and Piecewise functions, AMICI uses +the SUNDIALS functionality to only track zero crossings from negative to +positive. Accordingly, two root functions are necessary to keep track of +Heaviside functions and two Heaviside function helper variables will be +created, where one corresponds to the value of `Heaviside(...)` and one +to the value of `1-Heaviside(...)`. To ensure that Heaviside functions are +correctly evaluated at the beginning of the simulation, Heaviside functions +are implement as unit steps that evaluate to `1` at `0`. The arguments of +Heaviside functions are normalized such that respective properties of Piecewise functions are conserved for the first Heaviside function variable. Accordingly, the value of of the second helper variable is incorrect when simulation starts when the respective Heaviside function evaluates to zero diff --git a/documentation/python_examples.rst b/documentation/python_examples.rst index ec26e9f1c6..286ebf3ffd 100644 --- a/documentation/python_examples.rst +++ b/documentation/python_examples.rst @@ -17,3 +17,5 @@ Various example notebooks. example_errors.ipynb example_large_models/example_performance_optimization.ipynb ExampleJax.ipynb + ExampleSplines.ipynb + ExampleSplinesSwameye2003.ipynb diff --git a/documentation/python_modules.rst b/documentation/python_modules.rst index 4b23eb8061..5481865a7d 100644 --- a/documentation/python_modules.rst +++ b/documentation/python_modules.rst @@ -26,3 +26,5 @@ AMICI Python API amici.conserved_quantities_demartino amici.conserved_quantities_rref amici.numpy + amici.sbml_utils + amici.splines diff --git a/documentation/recreate_reference_list.py b/documentation/recreate_reference_list.py index 71cbdaa5a4..1dd1c13b4b 100755 --- a/documentation/recreate_reference_list.py +++ b/documentation/recreate_reference_list.py @@ -8,28 +8,29 @@ Requires pandoc """ -import biblib.bib -import biblib.messages -import biblib.algo import os -import sys import subprocess +import sys + +import biblib.algo +import biblib.bib +import biblib.messages def get_keys_by_year(bibfile): """Get bibtex entry keys as dict by year""" - with open(bibfile, 'r') as f: + with open(bibfile, "r") as f: db = biblib.bib.Parser().parse(f, log_fp=sys.stderr).get_entries() recoverer = biblib.messages.InputErrorRecoverer() by_year = {} for ent in db.values(): with recoverer: - if 'year' in ent: + if "year" in ent: try: - by_year[ent['year']].append(ent.key) + by_year[ent["year"]].append(ent.key) except KeyError: - by_year[ent['year']] = [ent.key] + by_year[ent["year"]] = [ent.key] else: print("Missing year for entry", ent.key) recoverer.reraise() @@ -39,15 +40,17 @@ def get_keys_by_year(bibfile): def get_sub_bibliography(year, by_year, bibfile): """Get HTML bibliography for the given year""" - entries = ','.join(['@' + x for x in by_year[year]]) - stdin_input = '---\n' \ - f'bibliography: {bibfile}\n' \ - f'nocite: "{entries}"\n...\n' \ - f'# {year}' - - out = subprocess.run(['pandoc', '--citeproc', '-f', 'markdown'], - input=stdin_input, capture_output=True, - encoding='utf-8') + entries = ",".join(["@" + x for x in by_year[year]]) + stdin_input = ( + "---\n" f"bibliography: {bibfile}\n" f'nocite: "{entries}"\n...\n' f"# {year}" + ) + + out = subprocess.run( + ["pandoc", "--citeproc", "-f", "markdown"], + input=stdin_input, + capture_output=True, + encoding="utf-8", + ) if out.returncode != 0: raise AssertionError(out.stderr) @@ -56,30 +59,33 @@ def get_sub_bibliography(year, by_year, bibfile): def main(): script_path = os.path.dirname(os.path.realpath(__file__)) - bibfile = os.path.join(script_path, 'amici_refs.bib') - outfile = os.path.join(script_path, 'references.md') + bibfile = os.path.join(script_path, "amici_refs.bib") + outfile = os.path.join(script_path, "references.md") by_year = get_keys_by_year(bibfile) num_total = sum(map(len, by_year.values())) - with open(outfile, 'w') as f: - f.write('# References\n\n') - f.write('List of publications using AMICI. ' - f'Total number is {num_total}.\n\n') - f.write('If you applied AMICI in your work and your publication is ' - 'missing, please let us know via a new GitHub issue.\n\n') + with open(outfile, "w") as f: + f.write("# References\n\n") f.write( -""" + "List of publications using AMICI. " f"Total number is {num_total}.\n\n" + ) + f.write( + "If you applied AMICI in your work and your publication is " + "missing, please let us know via a new GitHub issue.\n\n" + ) + f.write( + """ \n """ - ) + ) for year in reversed(sorted(by_year.keys())): cur_bib = get_sub_bibliography(year, by_year, bibfile) f.write(cur_bib) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/documentation/rtd_requirements.txt b/documentation/rtd_requirements.txt index b1f499ad10..64bc03e519 100644 --- a/documentation/rtd_requirements.txt +++ b/documentation/rtd_requirements.txt @@ -1,24 +1,23 @@ # NOTE: relative paths are expected to be relative to the repository root -sphinx==5.1.1 -mock>=4.0.3 -setuptools==65.5.1 +sphinx<7 +mock>=5.0.2 +setuptools==67.7.2 pysb>=1.11.0 -matplotlib==3.5.3 -pkgconfig>=1.5.5 -nbsphinx>=0.8.9 -nbformat==5.4.0 -recommonmark>=0.6.0 -sphinx_rtd_theme>=1.0.0 -petab[vis]>=0.1.20 -sphinx-autodoc-typehints==1.19.2 +matplotlib==3.7.1 +nbsphinx==0.9.1 +nbformat==5.8.0 +recommonmark>=0.7.1 +sphinx_rtd_theme>=1.2.0 +petab[vis]>=0.2.0 +sphinx-autodoc-typehints==1.23.0 git+https://github.com/readthedocs/sphinx-hoverxref@main -ipython==8.10.0 -breathe==4.34.0 +ipython==8.13.2 +breathe==4.35.0 #exhale>=0.3.5 -e git+https://github.com/mithro/sphinx-contrib-mithro#egg=sphinx-contrib-exhale-multiproject&subdirectory=sphinx-contrib-exhale-multiproject -sphinxcontrib-matlabdomain>=0.12.0 +sphinxcontrib-matlabdomain<0.19.0 sphinxcontrib-napoleon>=0.7 -pygments==2.13.0 +pygments==2.15.1 Jinja2==3.1.2 git+https://github.com/readthedocs/readthedocs-sphinx-ext ipykernel diff --git a/documentation/rtd_requirements2.txt b/documentation/rtd_requirements2.txt index 2b90e5cf66..5a39f8e683 100644 --- a/documentation/rtd_requirements2.txt +++ b/documentation/rtd_requirements2.txt @@ -1 +1 @@ -exhale>=0.3.5 +exhale>=0.3.6 diff --git a/include/amici/abstract_model.h b/include/amici/abstract_model.h index 2d703a93ba..bb824577b9 100644 --- a/include/amici/abstract_model.h +++ b/include/amici/abstract_model.h @@ -2,6 +2,7 @@ #define AMICI_ABSTRACT_MODEL_H #include "amici/defines.h" +#include "amici/splinefunctions.h" #include "amici/sundials_matrix_wrapper.h" #include "amici/vector.h" @@ -39,8 +40,10 @@ class AbstractModel { * @param dx time derivative of state (DAE only) * @param root array to which values of the root function will be written */ - virtual void froot(const realtype t, const AmiVector &x, - const AmiVector &dx, gsl::span root) = 0; + virtual void froot( + const realtype t, AmiVector const& x, AmiVector const& dx, + gsl::span root + ) = 0; /** * @brief Residual function @@ -50,8 +53,10 @@ class AbstractModel { * @param xdot array to which values of the residual function will be * written */ - virtual void fxdot(const realtype t, const AmiVector &x, - const AmiVector &dx, AmiVector &xdot) = 0; + virtual void fxdot( + const realtype t, AmiVector const& x, AmiVector const& dx, + AmiVector& xdot + ) = 0; /** * @brief Sensitivity Residual function @@ -64,9 +69,10 @@ class AbstractModel { * @param sxdot array to which values of the sensitivity residual function * will be written */ - virtual void fsxdot(const realtype t, const AmiVector &x, - const AmiVector &dx, int ip, const AmiVector &sx, - const AmiVector &sdx, AmiVector &sxdot) = 0; + virtual void fsxdot( + const realtype t, AmiVector const& x, AmiVector const& dx, int ip, + AmiVector const& sx, AmiVector const& sdx, AmiVector& sxdot + ) = 0; /** * @brief Residual function backward when running in steady state mode @@ -76,8 +82,10 @@ class AbstractModel { * @param xBdot array to which values of the residual function will be * written */ - virtual void fxBdot_ss(const realtype t, const AmiVector &xB, - const AmiVector &dxB, AmiVector &xBdot) = 0; + virtual void fxBdot_ss( + const realtype t, AmiVector const& xB, AmiVector const& dxB, + AmiVector& xBdot + ) = 0; /** * @brief Sparse Jacobian function backward, steady state case @@ -96,10 +104,10 @@ class AbstractModel { * @param dxB Vector with the adjoint derivative states * @param xBdot Vector with the adjoint state right hand side */ - virtual void writeSteadystateJB(const realtype t, realtype cj, - const AmiVector &x, const AmiVector &dx, - const AmiVector &xB, const AmiVector &dxB, - const AmiVector &xBdot) = 0; + virtual void writeSteadystateJB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot + ) = 0; /** * @brief Dense Jacobian function @@ -110,9 +118,10 @@ class AbstractModel { * @param xdot values of residual function (unused) * @param J dense matrix to which values of the jacobian will be written */ - virtual void fJ(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xdot, - SUNMatrix J) = 0; + virtual void + fJ(const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J) + = 0; /** * @brief Dense Jacobian function @@ -125,10 +134,11 @@ class AbstractModel { * @param xBdot Vector with the adjoint right hand side (unused) * @param JB dense matrix to which values of the jacobian will be written */ - virtual void fJB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &dxB, const AmiVector &xBdot, - SUNMatrix JB) = 0; + virtual void + fJB(const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB) + = 0; /** * @brief Sparse Jacobian function @@ -139,9 +149,10 @@ class AbstractModel { * @param xdot values of residual function (unused) * @param J sparse matrix to which values of the Jacobian will be written */ - virtual void fJSparse(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xdot, - SUNMatrix J) = 0; + virtual void fJSparse( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J + ) = 0; /** * @brief Sparse Jacobian function @@ -154,10 +165,11 @@ class AbstractModel { * @param xBdot Vector with the adjoint right hand side (unused) * @param JB dense matrix to which values of the jacobian will be written */ - virtual void fJSparseB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &dxB, const AmiVector &xBdot, - SUNMatrix JB) = 0; + virtual void fJSparseB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB + ) = 0; /** * @brief Diagonal Jacobian function @@ -167,8 +179,10 @@ class AbstractModel { * @param x state * @param dx time derivative of state (DAE only) */ - virtual void fJDiag(const realtype t, AmiVector &Jdiag, realtype cj, - const AmiVector &x, const AmiVector &dx) = 0; + virtual void fJDiag( + const realtype t, AmiVector& Jdiag, realtype cj, AmiVector const& x, + AmiVector const& dx + ) = 0; /** * @brief Model-specific sparse implementation of explicit parameter @@ -177,8 +191,9 @@ class AbstractModel { * @param x state * @param dx time derivative of state (DAE only) */ - virtual void fdxdotdp(const realtype t, const AmiVector &x, - const AmiVector &dx) = 0; + virtual void + fdxdotdp(const realtype t, AmiVector const& x, AmiVector const& dx) + = 0; /** * @brief Jacobian multiply function @@ -190,9 +205,10 @@ class AbstractModel { * @param nJv array to which result of multiplication will be written * @param cj scaling factor (inverse of timestep, DAE only) */ - virtual void fJv(const realtype t, const AmiVector &x, const AmiVector &dx, - const AmiVector &xdot, const AmiVector &v, AmiVector &nJv, - realtype cj) = 0; + virtual void + fJv(const realtype t, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, AmiVector const& v, AmiVector& nJv, realtype cj) + = 0; /** * @brief Returns the AMICI version that was used to generate the model @@ -213,8 +229,8 @@ class AbstractModel { * @param p parameter vector * @param k constant vector */ - virtual void fx0(realtype *x0, const realtype t, const realtype *p, - const realtype *k); + virtual void + fx0(realtype* x0, const realtype t, realtype const* p, realtype const* k); /** * @brief Function indicating whether reinitialization of states depending @@ -233,9 +249,10 @@ class AbstractModel { * @param reinitialization_state_idxs Indices of states to be reinitialized * based on provided constants / fixed parameters. */ - virtual void fx0_fixedParameters(realtype *x0, const realtype t, - const realtype *p, const realtype *k, - gsl::span reinitialization_state_idxs); + virtual void fx0_fixedParameters( + realtype* x0, const realtype t, realtype const* p, realtype const* k, + gsl::span reinitialization_state_idxs + ); /** * @brief Model-specific implementation of fsx0_fixedParameters @@ -248,10 +265,11 @@ class AbstractModel { * @param reinitialization_state_idxs Indices of states to be reinitialized * based on provided constants / fixed parameters. */ - virtual void fsx0_fixedParameters(realtype *sx0, const realtype t, - const realtype *x0, const realtype *p, - const realtype *k, int ip, - gsl::span reinitialization_state_idxs); + virtual void fsx0_fixedParameters( + realtype* sx0, const realtype t, realtype const* x0, realtype const* p, + realtype const* k, int ip, + gsl::span reinitialization_state_idxs + ); /** * @brief Model-specific implementation of fsx0 @@ -262,8 +280,10 @@ class AbstractModel { * @param k constant vector * @param ip sensitivity index */ - virtual void fsx0(realtype *sx0, const realtype t, const realtype *x0, - const realtype *p, const realtype *k, int ip); + virtual void fsx0( + realtype* sx0, const realtype t, realtype const* x0, realtype const* p, + realtype const* k, int ip + ); /** * @brief Initial value for time derivative of states (only necessary for @@ -272,7 +292,7 @@ class AbstractModel { * @param dx0 Vector to which the initial derivative states will be written * (only DAE) */ - virtual void fdx0(AmiVector &x0, AmiVector &dx0); + virtual void fdx0(AmiVector& x0, AmiVector& dx0); /** * @brief Model-specific implementation of fstau @@ -287,9 +307,11 @@ class AbstractModel { * @param ip sensitivity index * @param ie event index */ - virtual void fstau(realtype *stau, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *tcl, const realtype *sx, int ip, int ie); + virtual void fstau( + realtype* stau, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* tcl, + realtype const* sx, int ip, int ie + ); /** * @brief Model-specific implementation of fy @@ -301,9 +323,9 @@ class AbstractModel { * @param h Heaviside vector * @param w repeating elements vector */ - virtual void fy(realtype *y, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w); + virtual void + fy(realtype* y, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w); /** * @brief Model-specific implementation of fdydp (MATLAB-only) @@ -317,9 +339,11 @@ class AbstractModel { * @param w repeating elements vector * @param dwdp Recurring terms in xdot, parameter derivative */ - virtual void fdydp(realtype *dydp, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - int ip, const realtype *w, const realtype *dwdp); + virtual void fdydp( + realtype* dydp, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, int ip, realtype const* w, + realtype const* dwdp + ); /** * @brief Model-specific implementation of fdydp (Python) @@ -333,11 +357,16 @@ class AbstractModel { * @param w repeating elements vector * @param tcl total abundances for conservation laws * @param dtcldp Sensitivities of total abundances for conservation laws + * @param spl spline value vector + * @param sspl sensitivities of spline values vector w.r.t. parameters \f$ p + * \f$ */ - virtual void fdydp(realtype *dydp, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - int ip, const realtype *w, const realtype *tcl, - const realtype *dtcldp); + virtual void fdydp( + realtype* dydp, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, int ip, realtype const* w, + realtype const* tcl, realtype const* dtcldp, realtype const* spl, + realtype const* sspl + ); /** * @brief Model-specific implementation of fdydx @@ -350,9 +379,11 @@ class AbstractModel { * @param w repeating elements vector * @param dwdx Recurring terms in xdot, state derivative */ - virtual void fdydx(realtype *dydx, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w, const realtype *dwdx); + virtual void fdydx( + realtype* dydx, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* dwdx + ); /** * @brief Model-specific implementation of fz @@ -364,8 +395,9 @@ class AbstractModel { * @param k constant vector * @param h Heaviside vector */ - virtual void fz(realtype *z, int ie, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h); + virtual void + fz(realtype* z, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h); /** * @brief Model-specific implementation of fsz @@ -379,9 +411,10 @@ class AbstractModel { * @param sx current state sensitivity * @param ip sensitivity index */ - virtual void fsz(realtype *sz, int ie, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *sx, int ip); + virtual void + fsz(realtype* sz, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* sx, int ip); /** * @brief Model-specific implementation of frz @@ -394,8 +427,9 @@ class AbstractModel { * @param k constant vector * @param h Heaviside vector */ - virtual void frz(realtype *rz, int ie, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h); + virtual void + frz(realtype* rz, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h); /** * @brief Model-specific implementation of fsrz @@ -409,9 +443,11 @@ class AbstractModel { * @param h Heaviside vector * @param ip sensitivity index */ - virtual void fsrz(realtype *srz, int ie, const realtype t, - const realtype *x, const realtype *p, const realtype *k, - const realtype *h, const realtype *sx, int ip); + virtual void fsrz( + realtype* srz, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* sx, int ip + ); /** * @brief Model-specific implementation of fdzdp @@ -425,9 +461,10 @@ class AbstractModel { * @param h Heaviside vector * @param ip parameter index w.r.t. which the derivative is requested */ - virtual void fdzdp(realtype *dzdp, int ie, const realtype t, - const realtype *x, const realtype *p, const realtype *k, - const realtype *h, int ip); + virtual void fdzdp( + realtype* dzdp, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, int ip + ); /** * @brief Model-specific implementation of fdzdx @@ -440,9 +477,10 @@ class AbstractModel { * @param k constant vector * @param h Heaviside vector */ - virtual void fdzdx(realtype *dzdx, int ie, const realtype t, - const realtype *x, const realtype *p, const realtype *k, - const realtype *h); + virtual void fdzdx( + realtype* dzdx, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h + ); /** * @brief Model-specific implementation of fdrzdp @@ -456,9 +494,10 @@ class AbstractModel { * @param h Heaviside vector * @param ip parameter index w.r.t. which the derivative is requested */ - virtual void fdrzdp(realtype *drzdp, int ie, const realtype t, - const realtype *x, const realtype *p, const realtype *k, - const realtype *h, int ip); + virtual void fdrzdp( + realtype* drzdp, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, int ip + ); /** * @brief Model-specific implementation of fdrzdx @@ -470,9 +509,10 @@ class AbstractModel { * @param k constant vector * @param h Heaviside vector */ - virtual void fdrzdx(realtype *drzdx, int ie, const realtype t, - const realtype *x, const realtype *p, const realtype *k, - const realtype *h); + virtual void fdrzdx( + realtype* drzdx, int ie, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h + ); /** * @brief Model-specific implementation of fdeltax @@ -486,10 +526,11 @@ class AbstractModel { * @param xdot new model right hand side * @param xdot_old previous model right hand side */ - virtual void fdeltax(realtype *deltax, const realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, int ie, const realtype *xdot, - const realtype *xdot_old); + virtual void fdeltax( + realtype* deltax, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, int ie, + realtype const* xdot, realtype const* xdot_old + ); /** * @brief Model-specific implementation of fdeltasx @@ -508,13 +549,13 @@ class AbstractModel { * @param stau event-time sensitivity * @param tcl total abundances for conservation laws */ - virtual void fdeltasx(realtype *deltasx, const realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *w, int ip, int ie, - const realtype *xdot, const realtype *xdot_old, - const realtype *sx, const realtype *stau, - const realtype *tcl); + virtual void fdeltasx( + realtype* deltasx, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* w, int ip, int ie, realtype const* xdot, + realtype const* xdot_old, realtype const* sx, realtype const* stau, + realtype const* tcl + ); /** * @brief Model-specific implementation of fdeltaxB @@ -529,11 +570,11 @@ class AbstractModel { * @param xdot_old previous model right hand side * @param xB current adjoint state */ - virtual void fdeltaxB(realtype *deltaxB, const realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, int ie, - const realtype *xdot, const realtype *xdot_old, - const realtype *xB); + virtual void fdeltaxB( + realtype* deltaxB, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, int ie, + realtype const* xdot, realtype const* xdot_old, realtype const* xB + ); /** * @brief Model-specific implementation of fdeltaqB @@ -549,11 +590,11 @@ class AbstractModel { * @param xdot_old previous model right hand side * @param xB adjoint state */ - virtual void fdeltaqB(realtype *deltaqB, const realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, int ip, int ie, - const realtype *xdot, const realtype *xdot_old, - const realtype *xB); + virtual void fdeltaqB( + realtype* deltaqB, const realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, int ip, int ie, + realtype const* xdot, realtype const* xdot_old, realtype const* xB + ); /** * @brief Model-specific implementation of fsigmay @@ -563,8 +604,10 @@ class AbstractModel { * @param k constant vector * @param y model output at timepoint t */ - virtual void fsigmay(realtype *sigmay, const realtype t, const realtype *p, - const realtype *k, const realtype *y); + virtual void fsigmay( + realtype* sigmay, const realtype t, realtype const* p, + realtype const* k, realtype const* y + ); /** * @brief Model-specific implementation of fdsigmaydp @@ -575,9 +618,10 @@ class AbstractModel { * @param y model output at timepoint t * @param ip sensitivity index */ - virtual void fdsigmaydp(realtype *dsigmaydp, const realtype t, - const realtype *p, const realtype *k, - const realtype *y, int ip); + virtual void fdsigmaydp( + realtype* dsigmaydp, const realtype t, realtype const* p, + realtype const* k, realtype const* y, int ip + ); /** * @brief Model-specific implementation of fsigmay * @param dsigmaydy partial derivative of standard deviation of measurements @@ -587,10 +631,10 @@ class AbstractModel { * @param k constant vector * @param y model output at timepoint t */ - virtual void fdsigmaydy(realtype *dsigmaydy, const realtype t, - const realtype *p, const realtype *k, - const realtype *y); - + virtual void fdsigmaydy( + realtype* dsigmaydy, const realtype t, realtype const* p, + realtype const* k, realtype const* y + ); /** * @brief Model-specific implementation of fsigmaz @@ -599,8 +643,9 @@ class AbstractModel { * @param p parameter vector * @param k constant vector */ - virtual void fsigmaz(realtype *sigmaz, const realtype t, const realtype *p, - const realtype *k); + virtual void fsigmaz( + realtype* sigmaz, const realtype t, realtype const* p, realtype const* k + ); /** * @brief Model-specific implementation of fsigmaz @@ -611,8 +656,10 @@ class AbstractModel { * @param k constant vector * @param ip sensitivity index */ - virtual void fdsigmazdp(realtype *dsigmazdp, const realtype t, - const realtype *p, const realtype *k, int ip); + virtual void fdsigmazdp( + realtype* dsigmazdp, const realtype t, realtype const* p, + realtype const* k, int ip + ); /** * @brief Model-specific implementation of fJy @@ -624,9 +671,9 @@ class AbstractModel { * @param sigmay measurement standard deviation at timepoint * @param my measurements at timepoint */ - virtual void fJy(realtype *nllh, int iy, const realtype *p, - const realtype *k, const realtype *y, - const realtype *sigmay, const realtype *my); + virtual void + fJy(realtype* nllh, int iy, realtype const* p, realtype const* k, + realtype const* y, realtype const* sigmay, realtype const* my); /** * @brief Model-specific implementation of fJz @@ -638,9 +685,9 @@ class AbstractModel { * @param sigmaz event measurement standard deviation at timepoint * @param mz event measurements at timepoint */ - virtual void fJz(realtype *nllh, int iz, const realtype *p, - const realtype *k, const realtype *z, - const realtype *sigmaz, const realtype *mz); + virtual void + fJz(realtype* nllh, int iz, realtype const* p, realtype const* k, + realtype const* z, realtype const* sigmaz, realtype const* mz); /** * @brief Model-specific implementation of fJrz @@ -651,9 +698,10 @@ class AbstractModel { * @param z model event output at timepoint * @param sigmaz event measurement standard deviation at timepoint */ - virtual void fJrz(realtype *nllh, int iz, const realtype *p, - const realtype *k, const realtype *z, - const realtype *sigmaz); + virtual void fJrz( + realtype* nllh, int iz, realtype const* p, realtype const* k, + realtype const* z, realtype const* sigmaz + ); /** * @brief Model-specific implementation of fdJydy @@ -666,23 +714,24 @@ class AbstractModel { * @param sigmay measurement standard deviation at timepoint * @param my measurement at timepoint */ - virtual void fdJydy(realtype *dJydy, int iy, const realtype *p, - const realtype *k, const realtype *y, - const realtype *sigmay, const realtype *my); + virtual void fdJydy( + realtype* dJydy, int iy, realtype const* p, realtype const* k, + realtype const* y, realtype const* sigmay, realtype const* my + ); /** * @brief Model-specific implementation of fdJydy colptrs * @param dJydy sparse matrix to which colptrs will be written * @param index ytrue index */ - virtual void fdJydy_colptrs(SUNMatrixWrapper &dJydy, int index); + virtual void fdJydy_colptrs(SUNMatrixWrapper& dJydy, int index); /** * @brief Model-specific implementation of fdJydy rowvals * @param dJydy sparse matrix to which rowvals will be written * @param index `ytrue` index */ - virtual void fdJydy_rowvals(SUNMatrixWrapper &dJydy, int index); + virtual void fdJydy_rowvals(SUNMatrixWrapper& dJydy, int index); /** * @brief Model-specific implementation of fdJydsigma @@ -695,9 +744,10 @@ class AbstractModel { * @param sigmay measurement standard deviation at timepoint * @param my measurement at timepoint */ - virtual void fdJydsigma(realtype *dJydsigma, int iy, const realtype *p, - const realtype *k, const realtype *y, - const realtype *sigmay, const realtype *my); + virtual void fdJydsigma( + realtype* dJydsigma, int iy, realtype const* p, realtype const* k, + realtype const* y, realtype const* sigmay, realtype const* my + ); /** * @brief Model-specific implementation of fdJzdz @@ -710,9 +760,10 @@ class AbstractModel { * @param sigmaz event measurement standard deviation at timepoint * @param mz event measurement at timepoint */ - virtual void fdJzdz(realtype *dJzdz, int iz, const realtype *p, - const realtype *k, const realtype *z, - const realtype *sigmaz, const realtype *mz); + virtual void fdJzdz( + realtype* dJzdz, int iz, realtype const* p, realtype const* k, + realtype const* z, realtype const* sigmaz, realtype const* mz + ); /** * @brief Model-specific implementation of fdJzdsigma @@ -725,9 +776,10 @@ class AbstractModel { * @param sigmaz event measurement standard deviation at timepoint * @param mz event measurement at timepoint */ - virtual void fdJzdsigma(realtype *dJzdsigma, int iz, const realtype *p, - const realtype *k, const realtype *z, - const realtype *sigmaz, const realtype *mz); + virtual void fdJzdsigma( + realtype* dJzdsigma, int iz, realtype const* p, realtype const* k, + realtype const* z, realtype const* sigmaz, realtype const* mz + ); /** * @brief Model-specific implementation of fdJrzdz @@ -738,9 +790,10 @@ class AbstractModel { * @param rz model root output at timepoint * @param sigmaz event measurement standard deviation at timepoint */ - virtual void fdJrzdz(realtype *dJrzdz, int iz, const realtype *p, - const realtype *k, const realtype *rz, - const realtype *sigmaz); + virtual void fdJrzdz( + realtype* dJrzdz, int iz, realtype const* p, realtype const* k, + realtype const* rz, realtype const* sigmaz + ); /** * @brief Model-specific implementation of fdJrzdsigma @@ -752,9 +805,10 @@ class AbstractModel { * @param rz model root output at timepoint * @param sigmaz event measurement standard deviation at timepoint */ - virtual void fdJrzdsigma(realtype *dJrzdsigma, int iz, const realtype *p, - const realtype *k, const realtype *rz, - const realtype *sigmaz); + virtual void fdJrzdsigma( + realtype* dJrzdsigma, int iz, realtype const* p, realtype const* k, + realtype const* rz, realtype const* sigmaz + ); /** * @brief Model-specific implementation of fw @@ -765,10 +819,12 @@ class AbstractModel { * @param k constants vector * @param h Heaviside vector * @param tcl total abundances for conservation laws + * @param spl spline value vector */ - virtual void fw(realtype *w, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *tcl); + virtual void + fw(realtype* w, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* tcl, + realtype const* spl); /** * @brief Model-specific sparse implementation of dwdp @@ -781,23 +837,28 @@ class AbstractModel { * @param w vector with helper variables * @param tcl total abundances for conservation laws * @param stcl sensitivities of total abundances for conservation laws + * @param spl spline value vector + * @param sspl sensitivities of spline values vector w.r.t. parameters \f$ p + * \f$ */ - virtual void fdwdp(realtype *dwdp, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w, const realtype *tcl, - const realtype *stcl); + virtual void fdwdp( + realtype* dwdp, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* tcl, realtype const* stcl, realtype const* spl, + realtype const* sspl + ); /** * @brief Model-specific implementation for dwdp, column pointers * @param dwdp sparse matrix to which colptrs will be written */ - virtual void fdwdp_colptrs(SUNMatrixWrapper &dwdp); + virtual void fdwdp_colptrs(SUNMatrixWrapper& dwdp); /** * @brief Model-specific implementation for dwdp, row values * @param dwdp sparse matrix to which rowvals will be written */ - virtual void fdwdp_rowvals(SUNMatrixWrapper &dwdp); + virtual void fdwdp_rowvals(SUNMatrixWrapper& dwdp); /** * @brief Model-specific sensitivity implementation of dwdp @@ -810,12 +871,16 @@ class AbstractModel { * @param w vector with helper variables * @param tcl total abundances for conservation laws * @param stcl sensitivities of total abundances for conservation laws + * @param spl spline value vector + * @param sspl sensitivities of spline values vector * @param ip sensitivity parameter index */ - virtual void fdwdp(realtype *dwdp, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w, const realtype *tcl, - const realtype *stcl, int ip); + virtual void fdwdp( + realtype* dwdp, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* tcl, realtype const* stcl, realtype const* spl, + realtype const* sspl, int ip + ); /** * @brief Model-specific implementation of dwdx, data part @@ -827,22 +892,25 @@ class AbstractModel { * @param h Heaviside vector * @param w vector with helper variables * @param tcl total abundances for conservation laws + * @param spl spline value vector */ - virtual void fdwdx(realtype *dwdx, const realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w, const realtype *tcl); + virtual void fdwdx( + realtype* dwdx, const realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* tcl, realtype const* spl + ); /** * @brief Model-specific implementation for dwdx, column pointers * @param dwdx sparse matrix to which colptrs will be written */ - virtual void fdwdx_colptrs(SUNMatrixWrapper &dwdx); + virtual void fdwdx_colptrs(SUNMatrixWrapper& dwdx); /** * @brief Model-specific implementation for dwdx, row values * @param dwdx sparse matrix to which rowvals will be written */ - virtual void fdwdx_rowvals(SUNMatrixWrapper &dwdx); + virtual void fdwdx_rowvals(SUNMatrixWrapper& dwdx); /** * @brief Model-specific implementation of fdwdw, no w chainrule (Py) @@ -855,21 +923,23 @@ class AbstractModel { * @param w vector with helper variables * @param tcl Total abundances for conservation laws */ - virtual void fdwdw(realtype *dwdw, realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w, const realtype *tcl); + virtual void fdwdw( + realtype* dwdw, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* tcl + ); /** * @brief Model-specific implementation of fdwdw, colptrs part * @param dwdw sparse matrix to which colptrs will be written */ - virtual void fdwdw_colptrs(SUNMatrixWrapper &dwdw); + virtual void fdwdw_colptrs(SUNMatrixWrapper& dwdw); /** * @brief Model-specific implementation of fdwdw, rowvals part * @param dwdw sparse matrix to which rowvals will be written */ - virtual void fdwdw_rowvals(SUNMatrixWrapper &dwdw); + virtual void fdwdw_rowvals(SUNMatrixWrapper& dwdw); /** * @brief Compute dx_rdata / dx_solver @@ -879,21 +949,22 @@ class AbstractModel { * @param x State variables with conservation laws applied * @param tcl Total abundances for conservation laws */ - virtual void fdx_rdatadx_solver(realtype *dx_rdatadx_solver, - const realtype *x, const realtype *tcl, - const realtype *p, const realtype *k); + virtual void fdx_rdatadx_solver( + realtype* dx_rdatadx_solver, realtype const* x, realtype const* tcl, + realtype const* p, realtype const* k + ); /** * @brief Model-specific implementation of fdx_rdatadx_solver, colptrs part * @param dxrdatadxsolver sparse matrix to which colptrs will be written */ - virtual void fdx_rdatadx_solver_colptrs(SUNMatrixWrapper &dxrdatadxsolver); + virtual void fdx_rdatadx_solver_colptrs(SUNMatrixWrapper& dxrdatadxsolver); /** * @brief Model-specific implementation of fdx_rdatadx_solver, rowvals part * @param dxrdatadxsolver sparse matrix to which rowvals will be written */ - virtual void fdx_rdatadx_solver_rowvals(SUNMatrixWrapper &dxrdatadxsolver); + virtual void fdx_rdatadx_solver_rowvals(SUNMatrixWrapper& dxrdatadxsolver); /** * @brief Compute dx_rdata / dp @@ -904,9 +975,10 @@ class AbstractModel { * @param tcl Total abundances for conservation laws * @param ip Sensitivity index */ - virtual void fdx_rdatadp(realtype *dx_rdatadp, const realtype *x, - const realtype *tcl, const realtype *p, - const realtype *k, const int ip); + virtual void fdx_rdatadp( + realtype* dx_rdatadp, realtype const* x, realtype const* tcl, + realtype const* p, realtype const* k, int const ip + ); /** * @brief Compute dx_rdata / dtcl @@ -916,21 +988,22 @@ class AbstractModel { * @param x State variables with conservation laws applied * @param tcl Total abundances for conservation laws */ - virtual void fdx_rdatadtcl(realtype *dx_rdatadtcl, const realtype *x, - const realtype *tcl, const realtype *p, - const realtype *k); + virtual void fdx_rdatadtcl( + realtype* dx_rdatadtcl, realtype const* x, realtype const* tcl, + realtype const* p, realtype const* k + ); /** * @brief Model-specific implementation of fdx_rdatadtcl, colptrs part * @param dx_rdatadtcl sparse matrix to which colptrs will be written */ - virtual void fdx_rdatadtcl_colptrs(SUNMatrixWrapper &dx_rdatadtcl); + virtual void fdx_rdatadtcl_colptrs(SUNMatrixWrapper& dx_rdatadtcl); /** * @brief Model-specific implementation of fdx_rdatadtcl, rowvals part * @param dx_rdatadtcl sparse matrix to which rowvals will be written */ - virtual void fdx_rdatadtcl_rowvals(SUNMatrixWrapper &dx_rdatadtcl); + virtual void fdx_rdatadtcl_rowvals(SUNMatrixWrapper& dx_rdatadtcl); /** * @brief Compute dtotal_cl / dp @@ -940,9 +1013,10 @@ class AbstractModel { * @param k constant vector * @param ip Sensitivity index */ - virtual void fdtotal_cldp(realtype *dtotal_cldp, const realtype *x_rdata, - const realtype *p, const realtype *k, - const int ip); + virtual void fdtotal_cldp( + realtype* dtotal_cldp, realtype const* x_rdata, realtype const* p, + realtype const* k, int const ip + ); /** * @brief Compute dtotal_cl / dx_rdata @@ -952,24 +1026,59 @@ class AbstractModel { * @param k constant vector * @param tcl Total abundances for conservation laws */ - virtual void fdtotal_cldx_rdata(realtype *dtotal_cldx_rdata, - const realtype *x_rdata, const realtype *p, - const realtype *k, const realtype *tcl); + virtual void fdtotal_cldx_rdata( + realtype* dtotal_cldx_rdata, realtype const* x_rdata, realtype const* p, + realtype const* k, realtype const* tcl + ); /** * @brief Model-specific implementation of fdtotal_cldx_rdata, colptrs part * @param dtotal_cldx_rdata sparse matrix to which colptrs will be written */ - virtual void fdtotal_cldx_rdata_colptrs( - SUNMatrixWrapper &dtotal_cldx_rdata); + virtual void fdtotal_cldx_rdata_colptrs(SUNMatrixWrapper& dtotal_cldx_rdata + ); /** * @brief Model-specific implementation of fdtotal_cldx_rdata, rowvals part * @param dtotal_cldx_rdata sparse matrix to which rowvals will be written */ - virtual void fdtotal_cldx_rdata_rowvals( - SUNMatrixWrapper &dtotal_cldx_rdata); + virtual void fdtotal_cldx_rdata_rowvals(SUNMatrixWrapper& dtotal_cldx_rdata + ); + /** + * @brief Model-specific implementation of spline creation + * @param p parameter vector + * @param k constants vector + * @return Vector of splines used in the model + */ + virtual std::vector + fcreate_splines(realtype const* p, realtype const* k); + + /** + * @brief Model-specific implementation the parametric derivatives + * of spline node values + * @param dspline_valuesdp vector to which derivatives will be written + * @param p parameter vector + * @param k constants vector + * @param ip Sensitivity index + */ + virtual void fdspline_valuesdp( + realtype* dspline_valuesdp, realtype const* p, realtype const* k, + int const ip + ); + + /** + * @brief Model-specific implementation the parametric derivatives + * of slopevalues at spline nodes + * @param dspline_slopesdp vector to which derivatives will be written + * @param p parameter vector + * @param k constants vector + * @param ip Sensitivity index + */ + virtual void fdspline_slopesdp( + realtype* dspline_slopesdp, realtype const* p, realtype const* k, + int const ip + ); }; } // namespace amici diff --git a/include/amici/amici.h b/include/amici/amici.h index 1201b3b1b7..93b8daed9b 100644 --- a/include/amici/amici.h +++ b/include/amici/amici.h @@ -6,10 +6,8 @@ #include "amici/rdata.h" #include "amici/solver.h" - namespace amici { - /** * @brief Core integration routine. Initializes the solver and runs the forward * and backward problem. diff --git a/include/amici/backwardproblem.h b/include/amici/backwardproblem.h index e59c93ec60..1c26186c17 100644 --- a/include/amici/backwardproblem.h +++ b/include/amici/backwardproblem.h @@ -27,8 +27,9 @@ class BackwardProblem { * @param fwd pointer to corresponding forward problem * @param posteq pointer to postequilibration problem, can be nullptr */ - explicit BackwardProblem(const ForwardProblem &fwd, - const SteadystateProblem *posteq); + explicit BackwardProblem( + ForwardProblem const& fwd, SteadystateProblem const* posteq + ); /** * @brief Solve the backward problem. @@ -43,51 +44,39 @@ class BackwardProblem { * @brief Accessor for current time t * @return t */ - realtype gett() const { - return t_; - } + realtype gett() const { return t_; } /** * @brief Accessor for which * @return which */ - int getwhich() const { - return which; - } + int getwhich() const { return which; } /** * @brief Accessor for pointer to which * @return which */ - int *getwhichptr() { - return &which; - } + int* getwhichptr() { return &which; } /** * @brief Accessor for dJydx * @return dJydx */ - std::vector const& getdJydx() const { - return dJydx_; - } + std::vector const& getdJydx() const { return dJydx_; } /** * @brief Accessor for xB * @return xB */ - AmiVector const& getAdjointState() const { - return xB_; - } + AmiVector const& getAdjointState() const { return xB_; } /** * @brief Accessor for xQB * @return xQB */ - AmiVector const& getAdjointQuadrature() const { - return xQB_; - } + AmiVector const& getAdjointQuadrature() const { return xQB_; } -private: + private: /** * @brief Execute everything necessary for the handling of events * for the backward problem @@ -102,7 +91,6 @@ class BackwardProblem { */ void handleDataPointB(int it); - /** * @brief Compute the next timepoint to integrate to. * @@ -114,9 +102,9 @@ class BackwardProblem { */ realtype getTnext(int it); - Model *model_; - Solver *solver_; - const ExpData *edata_; + Model* model_; + Solver* solver_; + ExpData const* edata_; /** current time */ realtype t_; diff --git a/include/amici/cblas.h b/include/amici/cblas.h index bbfef955fe..d8f320c821 100644 --- a/include/amici/cblas.h +++ b/include/amici/cblas.h @@ -25,10 +25,11 @@ namespace amici { * @param Y vector Y * @param incY increment for entries of Y */ -void amici_dgemv(BLASLayout layout, BLASTranspose TransA, - int M, int N, double alpha, const double *A, - int lda, const double *X, int incX, - double beta, double *Y, int incY); +void amici_dgemv( + BLASLayout layout, BLASTranspose TransA, int M, int N, double alpha, + double const* A, int lda, double const* X, int incX, double beta, double* Y, + int incY +); /** * @brief CBLAS matrix matrix multiplication (dgemm) @@ -53,11 +54,11 @@ void amici_dgemv(BLASLayout layout, BLASTranspose TransA, * @param C matrix C * @param ldc leading dimension of C (>=M or >= N) */ -void amici_dgemm(BLASLayout layout, BLASTranspose TransA, - BLASTranspose TransB, int M, int N, - int K, double alpha, const double *A, - int lda, const double *B, int ldb, - double beta, double *C, int ldc); +void amici_dgemm( + BLASLayout layout, BLASTranspose TransA, BLASTranspose TransB, int M, int N, + int K, double alpha, double const* A, int lda, double const* B, int ldb, + double beta, double* C, int ldc +); /** * @brief Compute y = a*x + y @@ -68,7 +69,9 @@ void amici_dgemm(BLASLayout layout, BLASTranspose TransA, * @param y vector of length n*incy * @param incy y stride */ -void amici_daxpy(int n, double alpha, const double *x, int incx, double *y, int incy); +void amici_daxpy( + int n, double alpha, double const* x, int incx, double* y, int incy +); } // namespace amici diff --git a/include/amici/defines.h b/include/amici/defines.h index 4d5191e64d..df959f48d5 100644 --- a/include/amici/defines.h +++ b/include/amici/defines.h @@ -5,49 +5,49 @@ #define _USE_MATH_DEFINES #endif -#include #include +#include /* Math constants in case _USE_MATH_DEFINES is not supported */ #if defined(_USE_MATH_DEFINES) #if !defined(M_E) -#define M_E 2.71828182845904523536 +#define M_E 2.71828182845904523536 #endif #if !defined(M_LOG2E) -#define M_LOG2E 1.44269504088896340736 +#define M_LOG2E 1.44269504088896340736 #endif #if !defined(M_LOG10E) -#define M_LOG10E 0.434294481903251827651 +#define M_LOG10E 0.434294481903251827651 #endif #if !defined(M_LN2) -#define M_LN2 0.693147180559945309417 +#define M_LN2 0.693147180559945309417 #endif #if !defined(M_LN10) -#define M_LN10 2.30258509299404568402 +#define M_LN10 2.30258509299404568402 #endif #if !defined(M_PI) -#define M_PI 3.14159265358979323846 +#define M_PI 3.14159265358979323846 #endif #if !defined(M_PI_2) -#define M_PI_2 1.57079632679489661923 +#define M_PI_2 1.57079632679489661923 #endif #if !defined(M_PI_4) -#define M_PI_4 0.785398163397448309616 +#define M_PI_4 0.785398163397448309616 #endif #if !defined(M_1_PI) -#define M_1_PI 0.318309886183790671538 +#define M_1_PI 0.318309886183790671538 #endif #if !defined(M_2_PI) -#define M_2_PI 0.636619772367581343076 +#define M_2_PI 0.636619772367581343076 #endif #if !defined(M_2_SQRTPI) #define M_2_SQRTPI 1.12837916709551257390 #endif #if !defined(M_SQRT2) -#define M_SQRT2 1.41421356237309504880 +#define M_SQRT2 1.41421356237309504880 #endif #if !defined(M_SQRT1_2) -#define M_SQRT1_2 0.707106781186547524401 +#define M_SQRT1_2 0.707106781186547524401 #endif #endif @@ -55,7 +55,6 @@ namespace amici { constexpr double pi = M_PI; - // clang-format off constexpr int AMICI_ONEOUTPUT= 5; @@ -231,6 +230,24 @@ enum class RDataReporting { likelihood, }; +/** boundary conditions for splines */ +enum class SplineBoundaryCondition { + given = -1, + zeroDerivative = 0, + natural = 1, + naturalZeroDerivative = 2, + periodic = 3, +}; + +/** extrapolation methods for splines */ +enum class SplineExtrapolation { + noExtrapolation = -1, + constant = 0, + linear = 1, + polynomial = 2, + periodic = 3, +}; + // clang-format on } // namespace amici diff --git a/include/amici/edata.h b/include/amici/edata.h index dd6cad85ed..f8639ca2eb 100644 --- a/include/amici/edata.h +++ b/include/amici/edata.h @@ -59,8 +59,10 @@ class ExpData : public SimulationParameters { * @param ts Timepoints (dimension: nt) * @param fixedParameters Model constants (dimension: nk) */ - ExpData(int nytrue, int nztrue, int nmaxevent, std::vector ts, - std::vector fixedParameters); + ExpData( + int nytrue, int nztrue, int nmaxevent, std::vector ts, + std::vector fixedParameters + ); /** * @brief constructor that initializes timepoints and data from vectors @@ -77,11 +79,13 @@ class ExpData : public SimulationParameters { * @param observedEventsStdDev standard deviation of observed events/roots * (dimension: nmaxevents x nztrue, row-major) */ - ExpData(int nytrue, int nztrue, int nmaxevent, std::vector ts, - std::vector const &observedData, - std::vector const &observedDataStdDev, - std::vector const &observedEvents, - std::vector const &observedEventsStdDev); + ExpData( + int nytrue, int nztrue, int nmaxevent, std::vector ts, + std::vector const& observedData, + std::vector const& observedDataStdDev, + std::vector const& observedEvents, + std::vector const& observedEventsStdDev + ); /** * @brief constructor that initializes with Model @@ -159,7 +163,7 @@ class ExpData : public SimulationParameters { * * @return ExpData::ts */ - std::vector const &getTimepoints() const; + std::vector const& getTimepoints() const; /** * @brief Get timepoint for the given index @@ -201,7 +205,7 @@ class ExpData : public SimulationParameters { * * @return observed data (dimension: nt x nytrue, row-major) */ - std::vector const &getObservedData() const; + std::vector const& getObservedData() const; /** * @brief Get measurements for a given timepoint index. @@ -263,7 +267,7 @@ class ExpData : public SimulationParameters { * * @return standard deviation of observed data */ - std::vector const &getObservedDataStdDev() const; + std::vector const& getObservedDataStdDev() const; /** * @brief Get pointer to measurement standard deviations. @@ -306,7 +310,7 @@ class ExpData : public SimulationParameters { * * @return observed event data */ - std::vector const &getObservedEvents() const; + std::vector const& getObservedEvents() const; /** * @brief get function that returns a pointer to observed data at ieth @@ -372,7 +376,7 @@ class ExpData : public SimulationParameters { * * @return standard deviation of observed event data */ - std::vector const &getObservedEventsStdDev() const; + std::vector const& getObservedEventsStdDev() const; /** * @brief get function that returns a pointer to standard deviation of @@ -536,7 +540,7 @@ class ConditionContext : public ContextManager { void restore(); private: - Model *model_ = nullptr; + Model* model_ = nullptr; std::vector original_x0_; std::vector original_sx0_; std::vector original_parameters_; diff --git a/include/amici/forwardproblem.h b/include/amici/forwardproblem.h index 581361dc6c..dfe3bd8f22 100644 --- a/include/amici/forwardproblem.h +++ b/include/amici/forwardproblem.h @@ -2,14 +2,14 @@ #define AMICI_FORWARDPROBLEM_H #include "amici/defines.h" -#include "amici/vector.h" -#include "amici/model.h" #include "amici/misc.h" +#include "amici/model.h" +#include "amici/vector.h" #include +#include #include #include -#include namespace amici { @@ -18,7 +18,6 @@ class Solver; class SteadystateProblem; class FinalStateStorer; - /** * @brief The ForwardProblem class groups all functions for solving the * forward problem. @@ -30,8 +29,8 @@ class ForwardProblem { * @param edata pointer to ExpData instance * @param model pointer to Model instance * @param solver pointer to Solver instance - * @param preeq preequilibration with which to initialize the forward problem, - * pass nullptr for no initialization + * @param preeq preequilibration with which to initialize the forward + * problem, pass nullptr for no initialization */ ForwardProblem( ExpData const* edata, Model* model, Solver* solver, @@ -46,12 +45,14 @@ class ForwardProblem { /** * @brief Solve the forward problem. * - * If forward sensitivities are enabled this will also compute sensitivities. + * If forward sensitivities are enabled this will also compute + * sensitivities. */ void workForwardProblem(); /** - * @brief computes adjoint updates dJydx according to provided model and expdata + * @brief computes adjoint updates dJydx according to provided model and + * expdata * @param model Model instance * @param edata experimental data */ @@ -61,33 +62,25 @@ class ForwardProblem { * @brief Accessor for t * @return t */ - realtype getTime() const { - return t_; - } + realtype getTime() const { return t_; } /** * @brief Accessor for x * @return x */ - AmiVector const& getState() const { - return x_; - } + AmiVector const& getState() const { return x_; } /** * @brief Accessor for dx * @return dx */ - AmiVector const& getStateDerivative() const { - return dx_; - } + AmiVector const& getStateDerivative() const { return dx_; } /** * @brief Accessor for sx * @return sx */ - AmiVectorArray const& getStateSensitivity() const { - return sx_; - } + AmiVectorArray const& getStateSensitivity() const { return sx_; } /** * @brief Accessor for x_disc @@ -117,17 +110,13 @@ class ForwardProblem { * @brief Accessor for nroots * @return nroots */ - std::vector const& getNumberOfRoots() const { - return nroots_; - } + std::vector const& getNumberOfRoots() const { return nroots_; } /** * @brief Accessor for discs * @return discs */ - std::vector const& getDiscontinuities() const { - return discs_; - } + std::vector const& getDiscontinuities() const { return discs_; } /** * @brief Accessor for rootidx @@ -141,65 +130,49 @@ class ForwardProblem { * @brief Accessor for dJydx * @return dJydx */ - std::vector const& getDJydx() const { - return dJydx_; - } + std::vector const& getDJydx() const { return dJydx_; } /** * @brief Accessor for dJzdx * @return dJzdx */ - std::vector const& getDJzdx() const { - return dJzdx_; - } + std::vector const& getDJzdx() const { return dJzdx_; } /** * @brief Accessor for pointer to x * @return &x */ - AmiVector *getStatePointer() { - return &x_; - } + AmiVector* getStatePointer() { return &x_; } /** * @brief Accessor for pointer to dx * @return &dx */ - AmiVector *getStateDerivativePointer() { - return &dx_; - } + AmiVector* getStateDerivativePointer() { return &dx_; } /** * @brief accessor for pointer to sx * @return &sx */ - AmiVectorArray *getStateSensitivityPointer() { - return &sx_; - } + AmiVectorArray* getStateSensitivityPointer() { return &sx_; } /** * @brief Accessor for pointer to sdx * @return &sdx */ - AmiVectorArray *getStateDerivativeSensitivityPointer() { - return &sdx_; - } + AmiVectorArray* getStateDerivativeSensitivityPointer() { return &sdx_; } /** * @brief Accessor for it * @return it */ - int getCurrentTimeIteration() const { - return it_; - } + int getCurrentTimeIteration() const { return it_; } /** * @brief Returns final time point for which simulations are available * @return time point */ - realtype getFinalTime() const { - return final_state_.t; - } + realtype getFinalTime() const { return final_state_.t; } /** * @brief Returns maximal event index for which simulations are available @@ -213,9 +186,7 @@ class ForwardProblem { * @brief Returns maximal event index for which the timepoint is available * @return index */ - int getRootCounter() const { - return gsl::narrow(discs_.size()) - 1; - } + int getRootCounter() const { return gsl::narrow(discs_.size()) - 1; } /** * @brief Retrieves the carbon copy of the simulation state variables at @@ -258,16 +229,15 @@ class ForwardProblem { }; /** pointer to model instance */ - Model *model; + Model* model; /** pointer to solver instance */ - Solver *solver; + Solver* solver; /** pointer to experimental data instance */ ExpData const* edata; private: - void handlePresimulation(); /** @@ -278,8 +248,7 @@ class ForwardProblem { * @param initial_event initial event flag */ - void handleEvent(realtype *tlastroot, bool seflag, - bool initial_event); + void handleEvent(realtype* tlastroot, bool seflag, bool initial_event); /** * @brief Extract output information for events @@ -309,10 +278,10 @@ class ForwardProblem { * @param nmaxevent maximal number of events */ bool checkEventsToFill(int nmaxevent) const { - return std::any_of(nroots_.cbegin(), nroots_.cend(), - [nmaxevent](int curNRoots) { - return curNRoots < nmaxevent; - }); + return std::any_of( + nroots_.cbegin(), nroots_.cend(), + [nmaxevent](int curNRoots) { return curNRoots < nmaxevent; } + ); }; /** @@ -429,14 +398,14 @@ class ForwardProblem { std::vector stau_; /** storage for last found root */ - realtype tlastroot_ {0.0}; + realtype tlastroot_{0.0}; - /** flag to indicate whether solver was preeinitialized via preequilibration */ - bool preequilibrated_ {false}; + /** flag to indicate whether solver was preeinitialized via preequilibration + */ + bool preequilibrated_{false}; /** current iteration number for time index */ int it_; - }; /** @@ -448,8 +417,8 @@ class FinalStateStorer : public ContextManager { * @brief constructor, attaches problem pointer * @param fwd problem from which the simulation state is to be stored */ - explicit FinalStateStorer(ForwardProblem *fwd) : fwd_(fwd) { - } + explicit FinalStateStorer(ForwardProblem* fwd) + : fwd_(fwd) {} FinalStateStorer& operator=(FinalStateStorer const& other) = delete; @@ -457,11 +426,12 @@ class FinalStateStorer : public ContextManager { * @brief destructor, stores simulation state */ ~FinalStateStorer() { - if(fwd_) + if (fwd_) fwd_->final_state_ = fwd_->getSimulationState(); } + private: - ForwardProblem *fwd_; + ForwardProblem* fwd_; }; } // namespace amici diff --git a/include/amici/hdf5.h b/include/amici/hdf5.h index 8ed91da0ec..32cd4c925a 100644 --- a/include/amici/hdf5.h +++ b/include/amici/hdf5.h @@ -9,13 +9,12 @@ #include - /* Macros for enabling/disabling HDF5 error auto-printing * AMICI_H5_SAVE_ERROR_HANDLER and AMICI_H5_RESTORE_ERROR_HANDLER must be called * within the same context, otherwise the stack handler is lost. */ #define AMICI_H5_SAVE_ERROR_HANDLER \ - herr_t (*old_func)(void *); \ - void *old_client_data; \ + herr_t (*old_func)(void*); \ + void* old_client_data; \ H5Eget_auto1(&old_func, &old_client_data); \ H5Eset_auto1(NULL, NULL) @@ -39,7 +38,7 @@ namespace hdf5 { * @param hdf5filename File to open * @return File object */ -H5::H5File createOrOpenForWriting(std::string const &hdf5filename); +H5::H5File createOrOpenForWriting(std::string const& hdf5filename); /** * @brief Read solver options from HDF5 file. @@ -47,8 +46,9 @@ H5::H5File createOrOpenForWriting(std::string const &hdf5filename); * @param solver Solver to set options on * @param datasetPath Path inside the HDF5 file */ -void readSolverSettingsFromHDF5(const H5::H5File &file, Solver &solver, - std::string const &datasetPath); +void readSolverSettingsFromHDF5( + const H5::H5File& file, Solver& solver, std::string const& datasetPath +); /** * @brief Write solver options to HDF5 file. @@ -56,9 +56,10 @@ void readSolverSettingsFromHDF5(const H5::H5File &file, Solver &solver, * @param solver Solver to write options from * @param hdf5Location Path inside the HDF5 file */ -void writeSolverSettingsToHDF5(Solver const& solver, - std::string const& hdf5Filename, - std::string const& hdf5Location); +void writeSolverSettingsToHDF5( + Solver const& solver, std::string const& hdf5Filename, + std::string const& hdf5Location +); /** * @brief Write solver options to HDF5 file. @@ -66,9 +67,10 @@ void writeSolverSettingsToHDF5(Solver const& solver, * @param solver Solver to write options from * @param hdf5Location Path inside the HDF5 file */ -void writeSolverSettingsToHDF5(Solver const& solver, - H5::H5File const& file, - std::string const& hdf5Location); +void writeSolverSettingsToHDF5( + Solver const& solver, H5::H5File const& file, + std::string const& hdf5Location +); /** * @brief Read solver options from HDF5 file. @@ -76,8 +78,9 @@ void writeSolverSettingsToHDF5(Solver const& solver, * @param solver Solver to set options on * @param datasetPath Path inside the HDF5 file */ -void readSolverSettingsFromHDF5(std::string const &hdffile, Solver &solver, - std::string const &datasetPath); +void readSolverSettingsFromHDF5( + std::string const& hdffile, Solver& solver, std::string const& datasetPath +); /** * @brief Read model data from HDF5 file. @@ -85,8 +88,9 @@ void readSolverSettingsFromHDF5(std::string const &hdffile, Solver &solver, * @param model Model to set data on * @param datasetPath Path inside the HDF5 file */ -void readModelDataFromHDF5(std::string const &hdffile, Model &model, - std::string const &datasetPath); +void readModelDataFromHDF5( + std::string const& hdffile, Model& model, std::string const& datasetPath +); /** * @brief Read model data from HDF5 file. @@ -94,8 +98,9 @@ void readModelDataFromHDF5(std::string const &hdffile, Model &model, * @param model Model to set data on * @param datasetPath Path inside the HDF5 file */ -void readModelDataFromHDF5(H5::H5File const &file, Model &model, - std::string const &datasetPath); +void readModelDataFromHDF5( + H5::H5File const& file, Model& model, std::string const& datasetPath +); /** * @brief Write ReturnData to HDF5 file. @@ -104,8 +109,10 @@ void readModelDataFromHDF5(H5::H5File const &file, Model &model, * @param hdf5Location Full dataset path inside the HDF5 file (will be created) */ -void writeReturnData(const ReturnData &rdata, H5::H5File const &file, - const std::string &hdf5Location); +void writeReturnData( + ReturnData const& rdata, H5::H5File const& file, + std::string const& hdf5Location +); /** * @brief Write ReturnData to HDF5 file. @@ -114,8 +121,10 @@ void writeReturnData(const ReturnData &rdata, H5::H5File const &file, * @param hdf5Location Full dataset path inside the HDF5 file (will be created) */ -void writeReturnData(const ReturnData &rdata, std::string const &hdf5Filename, - const std::string &hdf5Location); +void writeReturnData( + ReturnData const& rdata, std::string const& hdf5Filename, + std::string const& hdf5Location +); /** * @brief Write ReturnData diagnosis data to HDF5 file. @@ -123,8 +132,10 @@ void writeReturnData(const ReturnData &rdata, std::string const &hdf5Filename, * @param file HDF5 file to write to * @param hdf5Location Full dataset path inside the HDF5 file (will be created) */ -void writeReturnDataDiagnosis(const ReturnData &rdata, H5::H5File const &file, - const std::string &hdf5Location); +void writeReturnDataDiagnosis( + ReturnData const& rdata, H5::H5File const& file, + std::string const& hdf5Location +); /** * @brief Create the given group and possibly parents. @@ -132,8 +143,10 @@ void writeReturnDataDiagnosis(const ReturnData &rdata, H5::H5File const &file, * @param groupPath Path to the group to be created * @param recursively Create intermediary groups */ -void createGroup(const H5::H5File &file, std::string const &groupPath, - bool recursively = true); +void createGroup( + const H5::H5File& file, std::string const& groupPath, + bool recursively = true +); /** * @brief Read AMICI ExpData data from HDF5 file. @@ -143,9 +156,10 @@ void createGroup(const H5::H5File &file, std::string const &groupPath, * @return ExpData created from data in the given location */ -std::unique_ptr readSimulationExpData(const std::string &hdf5Filename, - const std::string &hdf5Root, - const Model &model); +std::unique_ptr readSimulationExpData( + std::string const& hdf5Filename, std::string const& hdf5Root, + Model const& model +); /** * @brief Write AMICI experimental data to HDF5 file. @@ -154,8 +168,10 @@ std::unique_ptr readSimulationExpData(const std::string &hdf5Filename, * @param hdf5Location Path inside the HDF5 file to object having ExpData */ -void writeSimulationExpData(const ExpData &edata, H5::H5File const &file, - const std::string &hdf5Location); +void writeSimulationExpData( + ExpData const& edata, H5::H5File const& file, + std::string const& hdf5Location +); /** * @brief Check whether an attribute with the given name exists @@ -165,8 +181,10 @@ void writeSimulationExpData(const ExpData &edata, H5::H5File const &file, * @param attributeName Name of the attribute of interest * @return `true` if attribute exists, `false` otherwise */ -bool attributeExists(H5::H5File const &file, const std::string &optionsObject, - const std::string &attributeName); +bool attributeExists( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +); /** * @brief Check whether an attribute with the given name exists @@ -175,8 +193,9 @@ bool attributeExists(H5::H5File const &file, const std::string &optionsObject, * @param attributeName Name of the attribute of interest * @return `true` if attribute exists, `false` otherwise */ -bool attributeExists(H5::H5Object const &object, - const std::string &attributeName); +bool attributeExists( + H5::H5Object const& object, std::string const& attributeName +); /** * @brief Create and write to 1-dimensional native integer dataset. @@ -184,9 +203,10 @@ bool attributeExists(H5::H5Object const &object, * @param datasetName Name of dataset to create * @param buffer Data to write to dataset */ -void createAndWriteInt1DDataset(H5::H5File const &file, - std::string const &datasetName, - gsl::span buffer); +void createAndWriteInt1DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer +); /** * @brief Create and write to 2-dimensional native integer dataset. @@ -196,10 +216,10 @@ void createAndWriteInt1DDataset(H5::H5File const &file, * @param m Number of rows in buffer * @param n Number of columns buffer */ -void createAndWriteInt2DDataset(H5::H5File const &file, - std::string const &datasetName, - gsl::span buffer, hsize_t m, - hsize_t n); +void createAndWriteInt2DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n +); /** * @brief Create and write to 1-dimensional native double dataset. @@ -207,9 +227,10 @@ void createAndWriteInt2DDataset(H5::H5File const &file, * @param datasetName Name of dataset to create * @param buffer Data to write to dataset */ -void createAndWriteDouble1DDataset(H5::H5File const &file, - std::string const &datasetName, - gsl::span buffer); +void createAndWriteDouble1DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer +); /** * @brief Create and write to 2-dimensional native double dataset. @@ -220,10 +241,10 @@ void createAndWriteDouble1DDataset(H5::H5File const &file, * @param n Number of columns buffer */ -void createAndWriteDouble2DDataset(H5::H5File const &file, - std::string const &datasetName, - gsl::span buffer, hsize_t m, - hsize_t n); +void createAndWriteDouble2DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n +); /** * @brief Create and write to 3-dimensional native double dataset. @@ -235,10 +256,10 @@ void createAndWriteDouble2DDataset(H5::H5File const &file, * @param o Length of first dimension in buffer */ -void createAndWriteDouble3DDataset(H5::H5File const &file, - std::string const &datasetName, - gsl::span buffer, hsize_t m, - hsize_t n, hsize_t o); +void createAndWriteDouble3DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n, hsize_t o +); /** * @brief Read string attribute from HDF5 object. @@ -247,9 +268,10 @@ void createAndWriteDouble3DDataset(H5::H5File const &file, * @param attributeName Name of attribute to read * @return Attribute value */ -std::string getStringAttribute(H5::H5File const& file, - std::string const& optionsObject, - std::string const& attributeName); +std::string getStringAttribute( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +); /** * @brief Read scalar native double attribute from HDF5 object. @@ -258,9 +280,10 @@ std::string getStringAttribute(H5::H5File const& file, * @param attributeName Name of attribute to read * @return Attribute value */ -double getDoubleScalarAttribute(const H5::H5File &file, - const std::string &optionsObject, - const std::string &attributeName); +double getDoubleScalarAttribute( + const H5::H5File& file, std::string const& optionsObject, + std::string const& attributeName +); /** * @brief Read scalar native integer attribute from HDF5 object. @@ -270,9 +293,10 @@ double getDoubleScalarAttribute(const H5::H5File &file, * @return Attribute value */ -int getIntScalarAttribute(const H5::H5File &file, - const std::string &optionsObject, - const std::string &attributeName); +int getIntScalarAttribute( + const H5::H5File& file, std::string const& optionsObject, + std::string const& attributeName +); /** * @brief Read 1-dimensional native integer dataset from HDF5 file. @@ -280,8 +304,8 @@ int getIntScalarAttribute(const H5::H5File &file, * @param name Name of dataset to read * @return Data read */ -std::vector getIntDataset1D(const H5::H5File &file, - std::string const &name); +std::vector +getIntDataset1D(const H5::H5File& file, std::string const& name); /** * @brief Read 1-dimensional native double dataset from HDF5 file. @@ -290,8 +314,8 @@ std::vector getIntDataset1D(const H5::H5File &file, * @return Data read */ -std::vector getDoubleDataset1D(const H5::H5File &file, - std::string const &name); +std::vector +getDoubleDataset1D(const H5::H5File& file, std::string const& name); /** * @brief Read 2-dimensional native double dataset from HDF5 file. @@ -302,9 +326,9 @@ std::vector getDoubleDataset1D(const H5::H5File &file, * @return Flattened data (row-major) */ -std::vector getDoubleDataset2D(const H5::H5File &file, - std::string const &name, hsize_t &m, - hsize_t &n); +std::vector getDoubleDataset2D( + const H5::H5File& file, std::string const& name, hsize_t& m, hsize_t& n +); /** * @brief Read 3-dimensional native double dataset from HDF5 file. @@ -316,9 +340,10 @@ std::vector getDoubleDataset2D(const H5::H5File &file, * @return Flattened data (row-major) */ -std::vector getDoubleDataset3D(const H5::H5File &file, - std::string const &name, hsize_t &m, - hsize_t &n, hsize_t &o); +std::vector getDoubleDataset3D( + const H5::H5File& file, std::string const& name, hsize_t& m, hsize_t& n, + hsize_t& o +); /** * @brief Check if the given location (group, link or dataset) exists in the @@ -327,7 +352,7 @@ std::vector getDoubleDataset3D(const H5::H5File &file, * @param location Location to test for * @return `true` if exists, `false` otherwise */ -bool locationExists(std::string const &filename, std::string const &location); +bool locationExists(std::string const& filename, std::string const& location); /** * @brief Check if the given location (group, link or dataset) exists in the @@ -337,7 +362,7 @@ bool locationExists(std::string const &filename, std::string const &location); * @return `true` if exists, `false` otherwise */ -bool locationExists(H5::H5File const &file, std::string const &location); +bool locationExists(H5::H5File const& file, std::string const& location); } // namespace hdf5 } // namespace amici diff --git a/include/amici/interface_matlab.h b/include/amici/interface_matlab.h index 1471faf6d9..21fd89c412 100644 --- a/include/amici/interface_matlab.h +++ b/include/amici/interface_matlab.h @@ -3,8 +3,8 @@ #include -#include #include +#include namespace amici { @@ -17,14 +17,13 @@ namespace generic_model { extern std::unique_ptr getModel(); } // namespace generic_model - /** * @brief setModelData sets data from the matlab call to the model object * @param prhs: pointer to the array of input arguments * @param nrhs: number of elements in prhs * @param model: model to update */ -void setModelData(const mxArray *prhs[], int nrhs, Model& model); +void setModelData(mxArray const* prhs[], int nrhs, Model& model); /** * @brief setSolverOptions solver options from the matlab call to a solver @@ -33,7 +32,7 @@ void setModelData(const mxArray *prhs[], int nrhs, Model& model); * @param nrhs: number of elements in prhs * @param solver: solver to update */ -void setSolverOptions(const mxArray *prhs[], int nrhs, Solver& solver); +void setSolverOptions(mxArray const* prhs[], int nrhs, Solver& solver); /** * @brief setupReturnData initialises the return data struct @@ -41,8 +40,7 @@ void setSolverOptions(const mxArray *prhs[], int nrhs, Solver& solver); * @param nlhs number of elements in plhs * @return rdata: return data struct */ -ReturnDataMatlab *setupReturnData(mxArray *plhs[], int nlhs); - +ReturnDataMatlab* setupReturnData(mxArray* plhs[], int nlhs); /*! * @brief expDataFromMatlabCall parses the experimental data from the matlab @@ -53,19 +51,21 @@ ReturnDataMatlab *setupReturnData(mxArray *plhs[], int nlhs); * dimension checks * @return edata pointer to experimental data object */ -std::unique_ptr expDataFromMatlabCall(const mxArray *prhs[], - const Model &model); +std::unique_ptr +expDataFromMatlabCall(mxArray const* prhs[], Model const& model); -void amici_dgemv(BLASLayout layout, BLASTranspose TransA, - const int M, const int N, const double alpha, const double *A, - const int lda, const double *X, const int incX, - const double beta, double *Y, const int incY); +void amici_dgemv( + BLASLayout layout, BLASTranspose TransA, int const M, int const N, + double const alpha, double const* A, int const lda, double const* X, + int const incX, double const beta, double* Y, int const incY +); -void amici_dgemm(BLASLayout layout, BLASTranspose TransA, - BLASTranspose TransB, const int M, const int N, - const int K, const double alpha, const double *A, - const int lda, const double *B, const int ldb, - const double beta, double *C, const int ldc); +void amici_dgemm( + BLASLayout layout, BLASTranspose TransA, BLASTranspose TransB, int const M, + int const N, int const K, double const alpha, double const* A, + int const lda, double const* B, int const ldb, double const beta, double* C, + int const ldc +); } // namespace amici diff --git a/include/amici/logging.h b/include/amici/logging.h index 6447cf6054..0118bedd28 100644 --- a/include/amici/logging.h +++ b/include/amici/logging.h @@ -20,8 +20,7 @@ enum class LogSeverity { /** * @brief A logger, holding a list of error messages. */ -class Logger -{ +class Logger { public: Logger() = default; /** @@ -30,11 +29,9 @@ class Logger * @param identifier Short identifier for the logged event * @param message A more detailed message */ - void log( - LogSeverity severity, - std::string const& identifier, - std::string const& message - ); + void + log(LogSeverity severity, std::string const& identifier, + std::string const& message); #if SWIG_VERSION >= 0x040002 /** @@ -62,12 +59,10 @@ class Logger std::vector items; }; - /** * @brief A log item. */ -struct LogItem -{ +struct LogItem { /** * @brief Default ctor. */ @@ -80,14 +75,12 @@ struct LogItem * @param message */ LogItem( - LogSeverity severity, - std::string const& identifier, + LogSeverity severity, std::string const& identifier, std::string const& message - ): - severity(severity) - ,identifier(identifier) - ,message(message) - {}; + ) + : severity(severity) + , identifier(identifier) + , message(message){}; /** Severity level */ LogSeverity severity; diff --git a/include/amici/misc.h b/include/amici/misc.h index 88d28302d4..32dde8edcd 100644 --- a/include/amici/misc.h +++ b/include/amici/misc.h @@ -7,11 +7,11 @@ #include // SUNMatrixContent_Sparse #include -#include +#include +#include #include #include -#include -#include +#include #ifdef HAS_BOOST_CHRONO #include @@ -31,11 +31,11 @@ namespace amici { */ template -gsl::span slice(std::vector &data, int index, unsigned size) { +gsl::span slice(std::vector& data, int index, unsigned size) { if ((index + 1) * size > data.size()) throw std::out_of_range("requested slice is out of data range"); if (size > 0) - return gsl::make_span(&data.at(index*size), size); + return gsl::make_span(&data.at(index * size), size); return gsl::make_span(static_cast(nullptr), 0); } @@ -54,7 +54,7 @@ gsl::span slice(std::vector const& data, int index, unsigned size) { if ((index + 1) * size > data.size()) throw std::out_of_range("requested slice is out of data range"); if (size > 0) - return gsl::make_span(&data.at(index*size), size); + return gsl::make_span(&data.at(index * size), size); return gsl::make_span(static_cast(nullptr), 0); } @@ -66,18 +66,22 @@ gsl::span slice(std::vector const& data, int index, unsigned size) { * @param expected_size expected size of the buffer */ template -void checkBufferSize(gsl::span buffer, - typename gsl::span::index_type expected_size) { +void checkBufferSize( + gsl::span buffer, typename gsl::span::index_type expected_size +) { if (buffer.size() != expected_size) - throw AmiException("Incorrect buffer size! Was %u, expected %u.", - buffer.size(), expected_size); + throw AmiException( + "Incorrect buffer size! Was %u, expected %u.", buffer.size(), + expected_size + ); } /* TODO: templating writeSlice breaks implicit conversion between vector & span not sure whether this is fixable */ /** - * @brief local helper function to write computed slice to provided buffer (span) + * @brief local helper function to write computed slice to provided buffer + * (span) * @param slice computed value * @param buffer buffer to which values are to be written */ @@ -88,29 +92,35 @@ void writeSlice(const gsl::span slice, gsl::span buffer) { }; /** - * @brief local helper function to add the computed slice to provided buffer (span) + * @brief local helper function to add the computed slice to provided buffer + * (span) * @param slice computed value * @param buffer buffer to which values are to be added */ template void addSlice(const gsl::span slice, gsl::span buffer) { checkBufferSize(buffer, slice.size()); - std::transform(slice.begin(), slice.end(), buffer.begin(), buffer.begin(), - std::plus()); + std::transform( + slice.begin(), slice.end(), buffer.begin(), buffer.begin(), + std::plus() + ); }; /** - * @brief local helper function to write computed slice to provided buffer (vector) + * @brief local helper function to write computed slice to provided buffer + * (vector) * @param s computed value * @param b buffer to which values are to be written */ template void writeSlice(std::vector const& s, std::vector& b) { - writeSlice(gsl::make_span(s.data(), s.size()), - gsl::make_span(b.data(), b.size())); + writeSlice( + gsl::make_span(s.data(), s.size()), gsl::make_span(b.data(), b.size()) + ); }; /** - * @brief local helper function to write computed slice to provided buffer (vector/span) + * @brief local helper function to write computed slice to provided buffer + * (vector/span) * @param s computed value * @param b buffer to which values are to be written */ @@ -119,7 +129,8 @@ template void writeSlice(std::vector const& s, gsl::span b) { }; /** - * @brief local helper function to add the computed slice to provided buffer (vector/span) + * @brief local helper function to add the computed slice to provided buffer + * (vector/span) * @param s computed value * @param b buffer to which values are to be written */ @@ -128,7 +139,8 @@ template void addSlice(std::vector const& s, gsl::span b) { }; /** - * @brief local helper function to write computed slice to provided buffer (AmiVector/span) + * @brief local helper function to write computed slice to provided buffer + * (AmiVector/span) * @param s computed value * @param b buffer to which values are to be written */ @@ -149,16 +161,15 @@ void unscaleParameters( ); /** - * @brief Remove parameter scaling according to `scaling` - * - * @param scaledParameter scaled parameter - * @param scaling parameter scaling - * - * @return Unscaled parameter - */ + * @brief Remove parameter scaling according to `scaling` + * + * @param scaledParameter scaled parameter + * @param scaling parameter scaling + * + * @return Unscaled parameter + */ double getUnscaledParameter(double scaledParameter, ParameterScaling scaling); - /** * @brief Apply parameter scaling according to `scaling` * @param unscaledParameter @@ -167,7 +178,6 @@ double getUnscaledParameter(double scaledParameter, ParameterScaling scaling); */ double getScaledParameter(double unscaledParameter, ParameterScaling scaling); - /** * @brief Apply parameter scaling according to `scaling` * @param bufferUnscaled @@ -206,14 +216,13 @@ std::string printfToString(char const* fmt, va_list ap); * @brief Generic implementation for a context manager, explicitly deletes copy * and move operators for derived classes */ -class ContextManager{ +class ContextManager { public: ContextManager() = default; - ContextManager(ContextManager &other) = delete; - ContextManager(ContextManager &&other) = delete; + ContextManager(ContextManager& other) = delete; + ContextManager(ContextManager&& other) = delete; }; - /** * @brief Convert a flat index to a pair of row/column indices, * assuming row-major order. @@ -231,15 +240,14 @@ auto unravel_index(size_t flat_idx, size_t num_cols) * @param b * @return Whether the contents of the two spans are equal. */ -template -bool is_equal(T const& a, T const& b) { - if(a.size() != b.size()) +template bool is_equal(T const& a, T const& b) { + if (a.size() != b.size()) return false; auto a_data = a.data(); auto b_data = b.data(); - for(typename T::size_type i = 0; i < a.size(); ++i) { - if(a_data[i] != b_data[i] + for (typename T::size_type i = 0; i < a.size(); ++i) { + if (a_data[i] != b_data[i] && !(std::isnan(a_data[i]) && std::isnan(b_data[i]))) return false; } @@ -253,11 +261,13 @@ class CpuTimer { using time_point = boost::chrono::thread_clock::time_point; using d_seconds = boost::chrono::duration; using d_milliseconds = boost::chrono::duration; + public: /** * @brief Constructor */ - CpuTimer() : start_(clock::now()){} + CpuTimer() + : start_(clock::now()) {} /** * @brief Reset the timer @@ -269,8 +279,8 @@ class CpuTimer { * @return CPU time in seconds */ double elapsed_seconds() const { - return boost::chrono::duration_cast( - clock::now() - start_).count(); + return boost::chrono::duration_cast(clock::now() - start_) + .count(); } /** @@ -280,8 +290,11 @@ class CpuTimer { */ double elapsed_milliseconds() const { return boost::chrono::duration_cast( - clock::now() - start_).count(); + clock::now() - start_ + ) + .count(); } + private: /** Start time */ time_point start_; @@ -293,7 +306,8 @@ class CpuTimer { /** * @brief Constructor */ - CpuTimer() : start_(std::clock()){} + CpuTimer() + : start_(std::clock()) {} /** * @brief Reset the timer @@ -314,8 +328,10 @@ class CpuTimer { * @return CPU time in milliseconds */ double elapsed_milliseconds() const { - return static_cast(std::clock() - start_) * 1000.0 / CLOCKS_PER_SEC; + return static_cast(std::clock() - start_) * 1000.0 + / CLOCKS_PER_SEC; } + private: /** Start time */ std::clock_t start_; diff --git a/include/amici/model.h b/include/amici/model.h index a7103b361c..27f6b5a213 100644 --- a/include/amici/model.h +++ b/include/amici/model.h @@ -3,12 +3,13 @@ #include "amici/abstract_model.h" #include "amici/defines.h" -#include "amici/sundials_matrix_wrapper.h" -#include "amici/vector.h" -#include "amici/simulation_parameters.h" +#include "amici/logging.h" #include "amici/model_dimensions.h" #include "amici/model_state.h" -#include "amici/logging.h" +#include "amici/simulation_parameters.h" +#include "amici/splinefunctions.h" +#include "amici/sundials_matrix_wrapper.h" +#include "amici/vector.h" #include #include @@ -26,7 +27,7 @@ class Solver; namespace boost { namespace serialization { template -void serialize(Archive &ar, amici::Model &m, unsigned int version); +void serialize(Archive& ar, amici::Model& m, unsigned int version); } } // namespace boost @@ -94,7 +95,6 @@ enum class ModelQuantity { extern const std::map model_quantity_to_str; - /** * @brief The Model class represents an AMICI ODE/DAE model. * @@ -118,14 +118,14 @@ class Model : public AbstractModel, public ModelDimensions { * @param ndxdotdx_explicit Number of nonzero elements in `dxdotdx_explicit` * @param w_recursion_depth Recursion depth of fw */ - Model(ModelDimensions const& model_dimensions, - SimulationParameters simulation_parameters, - amici::SecondOrderMode o2mode, - std::vector idlist, - std::vector z2event, - bool pythonGenerated = false, - int ndxdotdp_explicit = 0, int ndxdotdx_explicit = 0, - int w_recursion_depth = 0); + Model( + ModelDimensions const& model_dimensions, + SimulationParameters simulation_parameters, + amici::SecondOrderMode o2mode, std::vector idlist, + std::vector z2event, bool pythonGenerated = false, + int ndxdotdp_explicit = 0, int ndxdotdx_explicit = 0, + int w_recursion_depth = 0 + ); /** Destructor. */ ~Model() override = default; @@ -135,13 +135,13 @@ class Model : public AbstractModel, public ModelDimensions { * @param other Object to copy from * @return */ - Model &operator=(Model const &other) = delete; + Model& operator=(Model const& other) = delete; /** * @brief Clone this instance. * @return The clone */ - virtual Model *clone() const = 0; + virtual Model* clone() const = 0; /** * @brief Serialize Model (see `boost::serialization::serialize`). @@ -150,8 +150,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param version Version number */ template - friend void boost::serialization::serialize(Archive &ar, Model &m, - unsigned int version); + friend void boost::serialization::serialize( + Archive& ar, Model& m, unsigned int version + ); /** * @brief Check equality of data members. @@ -159,7 +160,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param b Second model instance * @return Equality */ - friend bool operator==(const Model &a, const Model &b); + friend bool operator==(Model const& a, Model const& b); // Overloaded base class methods using AbstractModel::fdeltaqB; @@ -179,15 +180,26 @@ class Model : public AbstractModel, public ModelDimensions { using AbstractModel::fdsigmaydp; using AbstractModel::fdsigmaydy; using AbstractModel::fdsigmazdp; + using AbstractModel::fdtotal_cldp; + using AbstractModel::fdtotal_cldx_rdata; + using AbstractModel::fdtotal_cldx_rdata_colptrs; + using AbstractModel::fdtotal_cldx_rdata_rowvals; using AbstractModel::fdwdp; using AbstractModel::fdwdp_colptrs; using AbstractModel::fdwdp_rowvals; - using AbstractModel::fdwdx; - using AbstractModel::fdwdx_colptrs; - using AbstractModel::fdwdx_rowvals; using AbstractModel::fdwdw; using AbstractModel::fdwdw_colptrs; using AbstractModel::fdwdw_rowvals; + using AbstractModel::fdwdx; + using AbstractModel::fdwdx_colptrs; + using AbstractModel::fdwdx_rowvals; + using AbstractModel::fdx_rdatadp; + using AbstractModel::fdx_rdatadtcl; + using AbstractModel::fdx_rdatadtcl_colptrs; + using AbstractModel::fdx_rdatadtcl_rowvals; + using AbstractModel::fdx_rdatadx_solver; + using AbstractModel::fdx_rdatadx_solver_colptrs; + using AbstractModel::fdx_rdatadx_solver_rowvals; using AbstractModel::fdydp; using AbstractModel::fdydx; using AbstractModel::fdzdp; @@ -208,17 +220,6 @@ class Model : public AbstractModel, public ModelDimensions { using AbstractModel::fx0_fixedParameters; using AbstractModel::fy; using AbstractModel::fz; - using AbstractModel::fdx_rdatadx_solver; - using AbstractModel::fdx_rdatadx_solver_colptrs; - using AbstractModel::fdx_rdatadx_solver_rowvals; - using AbstractModel::fdx_rdatadp; - using AbstractModel::fdx_rdatadtcl; - using AbstractModel::fdx_rdatadtcl_colptrs; - using AbstractModel::fdx_rdatadtcl_rowvals; - using AbstractModel::fdtotal_cldx_rdata; - using AbstractModel::fdtotal_cldx_rdata_colptrs; - using AbstractModel::fdtotal_cldx_rdata_rowvals; - using AbstractModel::fdtotal_cldp; /** * @brief Initialize model properties. @@ -228,11 +229,13 @@ class Model : public AbstractModel, public ModelDimensions { * @param sdx Reference to time derivative of state sensitivities (DAE only) * @param computeSensitivities Flag indicating whether sensitivities are to * be computed - * @param roots_found boolean indicators indicating whether roots were found at t0 by this fun + * @param roots_found boolean indicators indicating whether roots were found + * at t0 by this fun */ - void initialize(AmiVector &x, AmiVector &dx, AmiVectorArray &sx, - AmiVectorArray &sdx, bool computeSensitivities, - std::vector &roots_found); + void initialize( + AmiVector& x, AmiVector& dx, AmiVectorArray& sx, AmiVectorArray& sdx, + bool computeSensitivities, std::vector& roots_found + ); /** * @brief Initialize model properties. @@ -241,21 +244,31 @@ class Model : public AbstractModel, public ModelDimensions { * @param xQB Adjoint quadratures * @param posteq Flag indicating whether postequilibration was performed */ - void initializeB(AmiVector &xB, AmiVector &dxB, AmiVector &xQB, - bool posteq) const; + void initializeB(AmiVector& xB, AmiVector& dxB, AmiVector& xQB, bool posteq) + const; /** * @brief Initialize initial states. * @param x State vector to be initialized */ - void initializeStates(AmiVector &x); + void initializeStates(AmiVector& x); /** * @brief Initialize initial state sensitivities. * @param sx Reference to state variable sensitivities * @param x Reference to state variables */ - void initializeStateSensitivities(AmiVectorArray &sx, const AmiVector &x); + void initializeStateSensitivities(AmiVectorArray& sx, AmiVector const& x); + + /** + * @brief Initialization of spline functions + */ + void initializeSplines(); + + /** + * @brief Initialization of spline sensitivity functions + */ + void initializeSplineSensitivities(); /** * @brief Initialize the Heaviside variables `h` at the initial time `t0`. @@ -264,10 +277,12 @@ class Model : public AbstractModel, public ModelDimensions { * * @param x Reference to state variables * @param dx Reference to time derivative of states (DAE only) - * @param roots_found boolean indicators indicating whether roots were found at t0 by this fun + * @param roots_found boolean indicators indicating whether roots were found + * at t0 by this fun */ - void initEvents(const AmiVector &x, const AmiVector &dx, - std::vector &roots_found); + void initEvents( + AmiVector const& x, AmiVector const& dx, std::vector& roots_found + ); /** * @brief Get number of parameters wrt to which sensitivities are computed. @@ -304,7 +319,7 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Get fixed parameters. * @return Pointer to constants array */ - const double *k() const; + double const* k() const; /** * @brief Get maximum number of events that may occur for each type. @@ -328,7 +343,7 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Get parameter scale for each parameter. * @return Vector of parameter scales */ - std::vector const &getParameterScale() const; + std::vector const& getParameterScale() const; /** * @brief Set parameter scale for each parameter. @@ -346,40 +361,40 @@ class Model : public AbstractModel, public ModelDimensions { * * @param pscaleVec Vector of parameter scales */ - void setParameterScale(const std::vector &pscaleVec); + void setParameterScale(std::vector const& pscaleVec); /** * @brief Get parameters with transformation according to parameter scale * applied. * @return Unscaled parameters */ - std::vector const &getUnscaledParameters() const; + std::vector const& getUnscaledParameters() const; /** * @brief Get parameter vector. * @return The user-set parameters (see also `Model::getUnscaledParameters`) */ - std::vector const &getParameters() const; + std::vector const& getParameters() const; /** * @brief Get value of first model parameter with the specified ID. * @param par_id Parameter ID * @return Parameter value */ - realtype getParameterById(std::string const &par_id) const; + realtype getParameterById(std::string const& par_id) const; /** * @brief Get value of first model parameter with the specified name. * @param par_name Parameter name * @return Parameter value */ - realtype getParameterByName(std::string const &par_name) const; + realtype getParameterByName(std::string const& par_name) const; /** * @brief Set the parameter vector. * @param p Vector of parameters */ - void setParameters(std::vector const &p); + void setParameters(std::vector const& p); /** * @brief Set model parameters according to the parameter IDs and mapped @@ -388,15 +403,16 @@ class Model : public AbstractModel, public ModelDimensions { * @param ignoreErrors Ignore errors such as parameter IDs in p which are * not model parameters */ - void setParameterById(std::map const &p, - bool ignoreErrors = false); + void setParameterById( + std::map const& p, bool ignoreErrors = false + ); /** * @brief Set value of first model parameter with the specified ID. * @param par_id Parameter ID * @param value Parameter value */ - void setParameterById(std::string const &par_id, realtype value); + void setParameterById(std::string const& par_id, realtype value); /** * @brief Set all values of model parameters with IDs matching the specified @@ -405,14 +421,14 @@ class Model : public AbstractModel, public ModelDimensions { * @param value Parameter value * @return Number of parameter IDs that matched the regex */ - int setParametersByIdRegex(std::string const &par_id_regex, realtype value); + int setParametersByIdRegex(std::string const& par_id_regex, realtype value); /** * @brief Set value of first model parameter with the specified name. * @param par_name Parameter name * @param value Parameter value */ - void setParameterByName(std::string const &par_name, realtype value); + void setParameterByName(std::string const& par_name, realtype value); /** * @brief Set model parameters according to the parameter name and mapped @@ -421,8 +437,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param ignoreErrors Ignore errors such as parameter names in p which are * not model parameters */ - void setParameterByName(std::map const &p, - bool ignoreErrors = false); + void setParameterByName( + std::map const& p, bool ignoreErrors = false + ); /** * @brief Set all values of all model parameters with names matching the @@ -431,22 +448,22 @@ class Model : public AbstractModel, public ModelDimensions { * @param value Parameter value * @return Number of fixed parameter names that matched the regex */ - int setParametersByNameRegex(std::string const &par_name_regex, - realtype value); + int + setParametersByNameRegex(std::string const& par_name_regex, realtype value); /** * @brief Get values of fixed parameters. * @return Vector of fixed parameters with same ordering as in * Model::getFixedParameterIds */ - std::vector const &getFixedParameters() const; + std::vector const& getFixedParameters() const; /** * @brief Get value of fixed parameter with the specified ID. * @param par_id Parameter ID * @return Parameter value */ - realtype getFixedParameterById(std::string const &par_id) const; + realtype getFixedParameterById(std::string const& par_id) const; /** * @brief Get value of fixed parameter with the specified name. @@ -457,20 +474,20 @@ class Model : public AbstractModel, public ModelDimensions { * @param par_name Parameter name * @return Parameter value */ - realtype getFixedParameterByName(std::string const &par_name) const; + realtype getFixedParameterByName(std::string const& par_name) const; /** * @brief Set values for constants. * @param k Vector of fixed parameters */ - void setFixedParameters(std::vector const &k); + void setFixedParameters(std::vector const& k); /** * @brief Set value of first fixed parameter with the specified ID. * @param par_id Fixed parameter id * @param value Fixed parameter value */ - void setFixedParameterById(std::string const &par_id, realtype value); + void setFixedParameterById(std::string const& par_id, realtype value); /** * @brief Set values of all fixed parameters with the ID matching the @@ -479,15 +496,16 @@ class Model : public AbstractModel, public ModelDimensions { * @param value Fixed parameter value * @return Number of fixed parameter IDs that matched the regex */ - int setFixedParametersByIdRegex(std::string const &par_id_regex, - realtype value); + int setFixedParametersByIdRegex( + std::string const& par_id_regex, realtype value + ); /** * @brief Set value of first fixed parameter with the specified name. * @param par_name Fixed parameter ID * @param value Fixed parameter value */ - void setFixedParameterByName(std::string const &par_name, realtype value); + void setFixedParameterByName(std::string const& par_name, realtype value); /** * @brief Set value of all fixed parameters with name matching the specified @@ -496,8 +514,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param value Fixed parameter value * @return Number of fixed parameter names that matched the regex */ - int setFixedParametersByNameRegex(std::string const &par_name_regex, - realtype value); + int setFixedParametersByNameRegex( + std::string const& par_name_regex, realtype value + ); /** * @brief Get the model name. @@ -660,7 +679,7 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Get the timepoint vector. * @return Timepoint vector */ - std::vector const &getTimepoints() const; + std::vector const& getTimepoints() const; /** * @brief Get simulation timepoint for time index `it`. @@ -673,7 +692,7 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Set the timepoint vector. * @param ts New timepoint vector */ - void setTimepoints(std::vector const &ts); + void setTimepoints(std::vector const& ts); /** * @brief Get simulation start time. @@ -692,14 +711,14 @@ class Model : public AbstractModel, public ModelDimensions { * non-negative. * @return Vector of flags */ - std::vector const &getStateIsNonNegative() const; + std::vector const& getStateIsNonNegative() const; /** * @brief Set flags indicating whether states should be treated as * non-negative. * @param stateIsNonNegative Vector of flags */ - void setStateIsNonNegative(std::vector const &stateIsNonNegative); + void setStateIsNonNegative(std::vector const& stateIsNonNegative); /** * @brief Set flags indicating that all states should be treated as @@ -711,15 +730,13 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Get the current model state. * @return Current model state */ - ModelState const &getModelState() const { - return state_; - }; + ModelState const& getModelState() const { return state_; }; /** * @brief Set the current model state. * @param state Model state */ - void setModelState(ModelState const &state) { + void setModelState(ModelState const& state) { if (gsl::narrow(state.unscaledParameters.size()) != np()) throw AmiException("Mismatch in parameter size"); if (gsl::narrow(state.fixedParameters.size()) != nk()) @@ -728,55 +745,51 @@ class Model : public AbstractModel, public ModelDimensions { throw AmiException("Mismatch in Heaviside size"); if (gsl::narrow(state.total_cl.size()) != ncl()) throw AmiException("Mismatch in conservation law size"); - if (gsl::narrow(state.stotal_cl.size()) != ncl() * np() ) + if (gsl::narrow(state.stotal_cl.size()) != ncl() * np()) throw AmiException("Mismatch in conservation law sensitivity size"); state_ = state; }; /** - * @brief Sets the estimated lower boundary for sigma_y. When :meth:`setAddSigmaResiduals` is - * activated, this lower boundary must ensure that log(sigma) + min_sigma > 0. + * @brief Sets the estimated lower boundary for sigma_y. When + * :meth:`setAddSigmaResiduals` is activated, this lower boundary must + * ensure that log(sigma) + min_sigma > 0. * @param min_sigma lower boundary */ - void setMinimumSigmaResiduals(double min_sigma) { - min_sigma_ = min_sigma; - } + void setMinimumSigmaResiduals(double min_sigma) { min_sigma_ = min_sigma; } /** * @brief Gets the specified estimated lower boundary for sigma_y. * @return lower boundary */ - realtype getMinimumSigmaResiduals() const { - return min_sigma_; - } + realtype getMinimumSigmaResiduals() const { return min_sigma_; } /** - * @brief Specifies whether residuals should be added to account for parameter dependent sigma. + * @brief Specifies whether residuals should be added to account for + * parameter dependent sigma. * - * If set to true, additional residuals of the form \f$ \sqrt{\log(\sigma) + C} \f$ will be added. - * This enables least-squares optimization for variables with Gaussian noise assumption and parameter - * dependent standard deviation sigma. The constant \f$ C \f$ can be set via + * If set to true, additional residuals of the form \f$ \sqrt{\log(\sigma) + + * C} \f$ will be added. This enables least-squares optimization for + * variables with Gaussian noise assumption and parameter dependent standard + * deviation sigma. The constant \f$ C \f$ can be set via * :meth:`setMinimumSigmaResiduals`. * * @param sigma_res if true, additional residuals are added */ - void setAddSigmaResiduals(bool sigma_res) { - sigma_res_ = sigma_res; - } + void setAddSigmaResiduals(bool sigma_res) { sigma_res_ = sigma_res; } /** - * @brief Checks whether residuals should be added to account for parameter dependent sigma. + * @brief Checks whether residuals should be added to account for parameter + * dependent sigma. * @return sigma_res */ - bool getAddSigmaResiduals() const { - return sigma_res_; - } + bool getAddSigmaResiduals() const { return sigma_res_; } /** * @brief Get the list of parameters for which sensitivities are computed. * @return List of parameter indices */ - std::vector const &getParameterList() const; + std::vector const& getParameterList() const; /** * @brief Get entry in parameter list by index. @@ -793,7 +806,7 @@ class Model : public AbstractModel, public ModelDimensions { * * @param plist List of parameter indices */ - void setParameterList(std::vector const &plist); + void setParameterList(std::vector const& plist); /** * @brief Get the initial states. @@ -805,7 +818,7 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Set the initial states. * @param x0 Initial state vector */ - void setInitialStates(std::vector const &x0); + void setInitialStates(std::vector const& x0); /** * @brief Return whether custom initial states have been set. @@ -824,7 +837,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx0 vector of initial state sensitivities with chainrule applied. * This could be a slice of ReturnData::sx or ReturnData::sx0 */ - void setInitialStateSensitivities(std::vector const &sx0); + void setInitialStateSensitivities(std::vector const& sx0); /** * @brief Return whether custom initial state sensitivities have been set. @@ -838,7 +851,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx0 Vector of initial state sensitivities without chainrule * applied. This could be the readin from a `model.sx0data` saved to HDF5. */ - void setUnscaledInitialStateSensitivities(std::vector const &sx0); + void setUnscaledInitialStateSensitivities(std::vector const& sx0); /** * @brief Set the mode how sensitivities are computed in the steadystate @@ -882,7 +895,8 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void getExpression(gsl::span w, const realtype t, const AmiVector &x); + void + getExpression(gsl::span w, const realtype t, AmiVector const& x); /** * @brief Get time-resolved observables. @@ -890,8 +904,8 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void getObservable(gsl::span y, const realtype t, - const AmiVector &x); + void + getObservable(gsl::span y, const realtype t, AmiVector const& x); /** * @brief Get scaling type for observable @@ -910,8 +924,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param sx State sensitivities */ - void getObservableSensitivity(gsl::span sy, const realtype t, - const AmiVector &x, const AmiVectorArray &sx); + void getObservableSensitivity( + gsl::span sy, const realtype t, AmiVector const& x, + AmiVectorArray const& sx + ); /** * @brief Get time-resolved observable standard deviations @@ -920,8 +936,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param edata Pointer to experimental data instance (optional, pass * `nullptr` to ignore) */ - void getObservableSigma(gsl::span sigmay, const int it, - const ExpData *edata); + void getObservableSigma( + gsl::span sigmay, int const it, ExpData const* edata + ); /** * @brief Sensitivity of time-resolved observable standard deviation. @@ -934,9 +951,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param edata Pointer to experimental data instance (optional, pass * `nullptr` to ignore) */ - void getObservableSigmaSensitivity(gsl::span ssigmay, - gsl::span sy, - const int it, const ExpData *edata); + void getObservableSigmaSensitivity( + gsl::span ssigmay, gsl::span sy, int const it, + ExpData const* edata + ); /** * @brief Add time-resolved measurement negative log-likelihood \f$ Jy \f$. @@ -945,12 +963,13 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void addObservableObjective(realtype &Jy, const int it, const AmiVector &x, - const ExpData &edata); + void addObservableObjective( + realtype& Jy, int const it, AmiVector const& x, ExpData const& edata + ); /** - * @brief Add sensitivity of time-resolved measurement negative log-likelihood - * \f$ Jy \f$. + * @brief Add sensitivity of time-resolved measurement negative + * log-likelihood \f$ Jy \f$. * * @param sllh First-order buffer (shape `nplist`) * @param s2llh Second-order buffer (shape `nJ - 1` x `nplist`, row-major) @@ -959,11 +978,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx State sensitivities * @param edata Experimental data */ - void addObservableObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int it, const AmiVector &x, - const AmiVectorArray &sx, - const ExpData &edata); + void addObservableObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const it, + AmiVector const& x, AmiVectorArray const& sx, ExpData const& edata + ); /** * @brief Add sensitivity of time-resolved measurement negative @@ -978,11 +996,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void addPartialObservableObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int it, - const AmiVector &x, - const ExpData &edata); + void addPartialObservableObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const it, + AmiVector const& x, ExpData const& edata + ); /** * @brief Get state sensitivity of the negative loglikelihood \f$ Jy \f$, @@ -993,9 +1010,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data instance */ - void getAdjointStateObservableUpdate(gsl::span dJydx, - const int it, const AmiVector &x, - const ExpData &edata); + void getAdjointStateObservableUpdate( + gsl::span dJydx, int const it, AmiVector const& x, + ExpData const& edata + ); /** * @brief Get event-resolved observables. @@ -1004,8 +1022,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Timepoint * @param x State variables */ - void getEvent(gsl::span z, const int ie, const realtype t, - const AmiVector &x); + void getEvent( + gsl::span z, int const ie, const realtype t, + AmiVector const& x + ); /** * @brief Get sensitivities of event-resolved observables. * @@ -1017,9 +1037,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param sx State sensitivities */ - void getEventSensitivity(gsl::span sz, const int ie, - const realtype t, const AmiVector &x, - const AmiVectorArray &sx); + void getEventSensitivity( + gsl::span sz, int const ie, const realtype t, + AmiVector const& x, AmiVectorArray const& sx + ); /** * @brief Get sensitivity of `z` at final timepoint. @@ -1029,7 +1050,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param sz Output buffer (shape `nz x nplist`, row-major) * @param ie Event index */ - void getUnobservedEventSensitivity(gsl::span sz, const int ie); + void getUnobservedEventSensitivity(gsl::span sz, int const ie); /** * @brief Get regularization for event-resolved observables. @@ -1038,8 +1059,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Timepoint * @param x State variables */ - void getEventRegularization(gsl::span rz, const int ie, - const realtype t, const AmiVector &x); + void getEventRegularization( + gsl::span rz, int const ie, const realtype t, + AmiVector const& x + ); /** * @brief Get sensitivities of regularization for event-resolved @@ -1053,10 +1076,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param sx State sensitivities */ - void getEventRegularizationSensitivity(gsl::span srz, - const int ie, const realtype t, - const AmiVector &x, - const AmiVectorArray &sx); + void getEventRegularizationSensitivity( + gsl::span srz, int const ie, const realtype t, + AmiVector const& x, AmiVectorArray const& sx + ); /** * @brief Get event-resolved observable standard deviations. * @param sigmaz Output buffer (shape `nz`) @@ -1066,9 +1089,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param edata Pointer to experimental data (optional, pass * `nullptr` to ignore) */ - void getEventSigma(gsl::span sigmaz, const int ie, - const int nroots, const realtype t, - const ExpData *edata); + void getEventSigma( + gsl::span sigmaz, int const ie, int const nroots, + const realtype t, ExpData const* edata + ); /** * @brief Get sensitivities of event-resolved observable standard @@ -1083,9 +1107,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param edata Pointer to experimental data (optional, pass * `nullptr` to ignore) */ - void getEventSigmaSensitivity(gsl::span ssigmaz, const int ie, - const int nroots, const realtype t, - const ExpData *edata); + void getEventSigmaSensitivity( + gsl::span ssigmaz, int const ie, int const nroots, + const realtype t, ExpData const* edata + ); /** * @brief Add event-resolved observable negative log-likelihood. @@ -1096,9 +1121,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void addEventObjective(realtype &Jz, const int ie, const int nroots, - const realtype t, const AmiVector &x, - const ExpData &edata); + void addEventObjective( + realtype& Jz, int const ie, int const nroots, const realtype t, + AmiVector const& x, ExpData const& edata + ); /** * @brief Add event-resolved observable negative log-likelihood. @@ -1109,10 +1135,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void addEventObjectiveRegularization(realtype &Jrz, const int ie, - const int nroots, const realtype t, - const AmiVector &x, - const ExpData &edata); + void addEventObjectiveRegularization( + realtype& Jrz, int const ie, int const nroots, const realtype t, + AmiVector const& x, ExpData const& edata + ); /** * @brief Add sensitivity of time-resolved measurement negative @@ -1130,12 +1156,11 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx State sensitivities * @param edata Experimental data */ - void addEventObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int ie, const int nroots, - const realtype t, const AmiVector &x, - const AmiVectorArray &sx, - const ExpData &edata); + void addEventObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const ie, + int const nroots, const realtype t, AmiVector const& x, + AmiVectorArray const& sx, ExpData const& edata + ); /** * @brief Add sensitivity of time-resolved measurement negative @@ -1152,12 +1177,11 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void addPartialEventObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int ie, const int nroots, - const realtype t, - const AmiVector &x, - const ExpData &edata); + void addPartialEventObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const ie, + int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief State sensitivity of the negative loglikelihood \f$ Jz \f$. @@ -1171,9 +1195,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void getAdjointStateEventUpdate(gsl::span dJzdx, const int ie, - const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata); + void getAdjointStateEventUpdate( + gsl::span dJzdx, int const ie, int const nroots, + const realtype t, AmiVector const& x, ExpData const& edata + ); /** * @brief Sensitivity of event timepoint, total derivative. @@ -1186,9 +1211,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param sx State sensitivities */ - void getEventTimeSensitivity(std::vector &stau, const realtype t, - const int ie, const AmiVector &x, - const AmiVectorArray &sx); + void getEventTimeSensitivity( + std::vector& stau, const realtype t, int const ie, + AmiVector const& x, AmiVectorArray const& sx + ); /** * @brief Update state variables after event. @@ -1198,8 +1224,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param xdot Current residual function values * @param xdot_old Value of residual function before event */ - void addStateEventUpdate(AmiVector &x, const int ie, const realtype t, - const AmiVector &xdot, const AmiVector &xdot_old); + void addStateEventUpdate( + AmiVector& x, int const ie, const realtype t, AmiVector const& xdot, + AmiVector const& xdot_old + ); /** * @brief Update state sensitivity after event. @@ -1212,12 +1240,11 @@ class Model : public AbstractModel, public ModelDimensions { * @param stau Timepoint sensitivity, to be computed with * `Model::getEventTimeSensitivity` */ - void addStateSensitivityEventUpdate(AmiVectorArray &sx, const int ie, - const realtype t, - const AmiVector &x_old, - const AmiVector &xdot, - const AmiVector &xdot_old, - const std::vector &stau); + void addStateSensitivityEventUpdate( + AmiVectorArray& sx, int const ie, const realtype t, + AmiVector const& x_old, AmiVector const& xdot, + AmiVector const& xdot_old, std::vector const& stau + ); /** * @brief Update adjoint state after event. @@ -1228,10 +1255,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param xdot Current residual function values * @param xdot_old Value of residual function before event */ - void addAdjointStateEventUpdate(AmiVector &xB, const int ie, - const realtype t, const AmiVector &x, - const AmiVector &xdot, - const AmiVector &xdot_old); + void addAdjointStateEventUpdate( + AmiVector& xB, int const ie, const realtype t, AmiVector const& x, + AmiVector const& xdot, AmiVector const& xdot_old + ); /** * @brief Update adjoint quadratures after event. @@ -1243,11 +1270,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param xdot Current residual function values * @param xdot_old Value of residual function before event */ - void addAdjointQuadratureEventUpdate(AmiVector xQB, const int ie, - const realtype t, const AmiVector &x, - const AmiVector &xB, - const AmiVector &xdot, - const AmiVector &xdot_old); + void addAdjointQuadratureEventUpdate( + AmiVector xQB, int const ie, const realtype t, AmiVector const& x, + AmiVector const& xB, AmiVector const& xdot, AmiVector const& xdot_old + ); /** * @brief Update the Heaviside variables `h` on event occurrences. @@ -1256,7 +1282,7 @@ class Model : public AbstractModel, public ModelDimensions { * it will give the right update to the Heaviside variables (zero if no root * was found) */ - void updateHeaviside(const std::vector &rootsfound); + void updateHeaviside(std::vector const& rootsfound); /** * @brief Updates the Heaviside variables `h` on event occurrences in the @@ -1265,8 +1291,7 @@ class Model : public AbstractModel, public ModelDimensions { * it will give the right update to the Heaviside variables (zero if no root * was found) */ - void updateHeavisideB(const int *rootsfound); - + void updateHeavisideB(int const* rootsfound); /** * @brief Check if the given array has only finite elements. @@ -1277,8 +1302,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param model_quantity The model quantity `array` corresponds to * @return */ - int checkFinite(gsl::span array, - ModelQuantity model_quantity) const; + int checkFinite( + gsl::span array, ModelQuantity model_quantity + ) const; /** * @brief Check if the given array has only finite elements. * @@ -1289,9 +1315,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param num_cols Number of columns of the non-flattened matrix * @return */ - int checkFinite(gsl::span array, - ModelQuantity model_quantity, - size_t num_cols) const; + int checkFinite( + gsl::span array, ModelQuantity model_quantity, + size_t num_cols + ) const; /** * @brief Check if the given array has only finite elements. @@ -1303,7 +1330,8 @@ class Model : public AbstractModel, public ModelDimensions { * @param t current timepoint * @return */ - int checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) const; + int + checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) const; /** * @brief Set whether the result of every call to `Model::f*` should be @@ -1323,21 +1351,21 @@ class Model : public AbstractModel, public ModelDimensions { * @brief Compute/get initial states. * @param x Output buffer. */ - void fx0(AmiVector &x); + void fx0(AmiVector& x); /** * @brief Set only those initial states that are specified via * fixed parameters. * @param x Output buffer. */ - void fx0_fixedParameters(AmiVector &x); + void fx0_fixedParameters(AmiVector& x); /** * @brief Compute/get initial value for initial state sensitivities. * @param sx Output buffer for state sensitivities * @param x State variables */ - void fsx0(AmiVectorArray &sx, const AmiVector &x); + void fsx0(AmiVectorArray& sx, AmiVector const& x); /** * @brief Get only those initial states sensitivities that are affected @@ -1345,7 +1373,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx Output buffer for state sensitivities * @param x State variables */ - void fsx0_fixedParameters(AmiVectorArray &sx, const AmiVector &x); + void fsx0_fixedParameters(AmiVectorArray& sx, AmiVector const& x); /** * @brief Compute sensitivity of derivative initial states sensitivities @@ -1362,7 +1390,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x_solver State variables with conservation laws applied * (solver returns this) */ - void fx_rdata(AmiVector &x_rdata, const AmiVector &x_solver); + void fx_rdata(AmiVector& x_rdata, AmiVector const& x_solver); /** * @brief Expand conservation law for state sensitivities. @@ -1373,15 +1401,17 @@ class Model : public AbstractModel, public ModelDimensions { * @param x_solver State variables with conservation laws * applied (solver returns this) */ - void fsx_rdata(AmiVectorArray &sx_rdata, const AmiVectorArray &sx_solver, - const AmiVector &x_solver); + void fsx_rdata( + AmiVectorArray& sx_rdata, AmiVectorArray const& sx_solver, + AmiVector const& x_solver + ); /** * @brief Set indices of states to be reinitialized based on provided * constants / fixed parameters * @param idxs Array of state indices */ - void setReinitializationStateIdxs(const std::vector &idxs); + void setReinitializationStateIdxs(std::vector const& idxs); /** * @brief Return indices of states to be reinitialized based on provided @@ -1397,13 +1427,13 @@ class Model : public AbstractModel, public ModelDimensions { * @brief getter for dxdotdp (matlab generated) * @return dxdotdp */ - const AmiVectorArray &get_dxdotdp() const; + AmiVectorArray const& get_dxdotdp() const; /** * @brief getter for dxdotdp (python generated) * @return dxdotdp */ - const SUNMatrixWrapper &get_dxdotdp_full() const; + SUNMatrixWrapper const& get_dxdotdp_full() const; /** * Flag indicating whether for @@ -1416,7 +1446,7 @@ class Model : public AbstractModel, public ModelDimensions { std::vector idlist; /** Logger */ - Logger *logger = nullptr; + Logger* logger = nullptr; protected: /** @@ -1426,8 +1456,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param buffer Output data slice * @param ie Event index */ - void writeSliceEvent(gsl::span slice, - gsl::span buffer, const int ie); + void writeSliceEvent( + gsl::span slice, gsl::span buffer, + int const ie + ); /** * @brief Write part of a sensitivity slice to a buffer according to @@ -1436,8 +1468,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param buffer output data slice * @param ie event index */ - void writeSensitivitySliceEvent(gsl::span slice, - gsl::span buffer, const int ie); + void writeSensitivitySliceEvent( + gsl::span slice, gsl::span buffer, + int const ie + ); /** * @brief Separate first and second order objective sensitivity information @@ -1446,17 +1480,19 @@ class Model : public AbstractModel, public ModelDimensions { * @param sllh First order buffer * @param s2llh Second order buffer */ - void writeLLHSensitivitySlice(const std::vector &dLLhdp, - std::vector &sllh, - std::vector &s2llh); + void writeLLHSensitivitySlice( + std::vector const& dLLhdp, std::vector& sllh, + std::vector& s2llh + ); /** * @brief Verify that the provided buffers have the expected size. * @param sllh first order buffer * @param s2llh second order buffer */ - void checkLLHBufferSize(const std::vector &sllh, - const std::vector &s2llh) const; + void checkLLHBufferSize( + std::vector const& sllh, std::vector const& s2llh + ) const; /** * @brief Set the nplist-dependent vectors to their proper sizes. @@ -1468,7 +1504,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fy(realtype t, const AmiVector &x); + void fy(realtype t, AmiVector const& x); /** * @brief Compute partial derivative of observables \f$ y \f$ w.r.t. model @@ -1476,7 +1512,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fdydp(realtype t, const AmiVector &x); + void fdydp(realtype t, AmiVector const& x); /** * @brief Compute partial derivative of observables \f$ y \f$ w.r.t. state @@ -1484,30 +1520,32 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fdydx(realtype t, const AmiVector &x); + void fdydx(realtype t, AmiVector const& x); /** * @brief Compute standard deviation of measurements. * @param it Timepoint index * @param edata Experimental data */ - void fsigmay(int it, const ExpData *edata); + void fsigmay(int it, ExpData const* edata); /** * @brief Compute partial derivative of standard deviation of measurements * w.r.t. model parameters. * @param it Timepoint index - * @param edata pointer to `amici::ExpData` data instance holding sigma values + * @param edata pointer to `amici::ExpData` data instance holding sigma + * values */ - void fdsigmaydp(int it, const ExpData *edata); + void fdsigmaydp(int it, ExpData const* edata); /** * @brief Compute partial derivative of standard deviation of measurements * w.r.t. model outputs. * @param it Timepoint index - * @param edata pointer to `amici::ExpData` data instance holding sigma values + * @param edata pointer to `amici::ExpData` data instance holding sigma + * values */ - void fdsigmaydy(int it, const ExpData *edata); + void fdsigmaydy(int it, ExpData const* edata); /** * @brief Compute negative log-likelihood of measurements \f$ y \f$. @@ -1517,7 +1555,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param y Simulated observable * @param edata Pointer to experimental data instance */ - void fJy(realtype &Jy, int it, const AmiVector &y, const ExpData &edata); + void fJy(realtype& Jy, int it, AmiVector const& y, ExpData const& edata); /** * @brief Compute partial derivative of time-resolved measurement negative @@ -1526,7 +1564,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x state variables * @param edata Pointer to experimental data */ - void fdJydy(int it, const AmiVector &x, const ExpData &edata); + void fdJydy(int it, AmiVector const& x, ExpData const& edata); /** * @brief Sensitivity of time-resolved measurement negative log-likelihood @@ -1535,7 +1573,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x state variables * @param edata pointer to experimental data instance */ - void fdJydsigma(int it, const AmiVector &x, const ExpData &edata); + void fdJydsigma(int it, AmiVector const& x, ExpData const& edata); /** * @brief Compute sensitivity of time-resolved measurement negative @@ -1544,7 +1582,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x state variables * @param edata pointer to experimental data instance */ - void fdJydp(const int it, const AmiVector &x, const ExpData &edata); + void fdJydp(int const it, AmiVector const& x, ExpData const& edata); /** * @brief Sensitivity of time-resolved measurement negative log-likelihood @@ -1553,7 +1591,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Pointer to experimental data instance */ - void fdJydx(const int it, const AmiVector &x, const ExpData &edata); + void fdJydx(int const it, AmiVector const& x, ExpData const& edata); /** * @brief Compute event-resolved output. @@ -1561,7 +1599,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fz(int ie, realtype t, const AmiVector &x); + void fz(int ie, realtype t, AmiVector const& x); /** * @brief Compute partial derivative of event-resolved output `z` w.r.t. @@ -1570,7 +1608,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t current timepoint * @param x current state */ - void fdzdp(int ie, realtype t, const AmiVector &x); + void fdzdp(int ie, realtype t, AmiVector const& x); /** * @brief Compute partial derivative of event-resolved output `z` w.r.t. @@ -1579,7 +1617,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fdzdx(int ie, realtype t, const AmiVector &x); + void fdzdx(int ie, realtype t, AmiVector const& x); /** * @brief Compute event root function of events. @@ -1590,7 +1628,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void frz(int ie, realtype t, const AmiVector &x); + void frz(int ie, realtype t, AmiVector const& x); /** * @brief Compute sensitivity of event-resolved root output w.r.t. model @@ -1599,16 +1637,16 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param x Current state */ - void fdrzdp(int ie, realtype t, const AmiVector &x); + void fdrzdp(int ie, realtype t, AmiVector const& x); /** - * @brief Compute sensitivity of event-resolved measurements \f$ rz \f$ w.r.t. - * model states `x`. + * @brief Compute sensitivity of event-resolved measurements \f$ rz \f$ + * w.r.t. model states `x`. * @param ie Event index * @param t Current timepoint * @param x Current state */ - void fdrzdx(int ie, realtype t, const AmiVector &x); + void fdrzdx(int ie, realtype t, AmiVector const& x); /** * @brief Compute standard deviation of events. @@ -1617,8 +1655,9 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param edata Experimental data */ - void fsigmaz(const int ie, const int nroots, const realtype t, - const ExpData *edata); + void fsigmaz( + int const ie, int const nroots, const realtype t, ExpData const* edata + ); /** * @brief Compute sensitivity of standard deviation of events measurements @@ -1628,7 +1667,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param t Current timepoint * @param edata Pointer to experimental data instance */ - void fdsigmazdp(int ie, int nroots, realtype t, const ExpData *edata); + void fdsigmazdp(int ie, int nroots, realtype t, ExpData const* edata); /** * @brief Compute negative log-likelihood of event-resolved measurements @@ -1638,8 +1677,8 @@ class Model : public AbstractModel, public ModelDimensions { * @param z Simulated event * @param edata Experimental data */ - void fJz(realtype &Jz, int nroots, const AmiVector &z, - const ExpData &edata); + void + fJz(realtype& Jz, int nroots, AmiVector const& z, ExpData const& edata); /** * @brief Compute partial derivative of event measurement negative @@ -1650,8 +1689,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void fdJzdz(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata); + void fdJzdz( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief Compute sensitivity of event measurement negative log-likelihood @@ -1662,8 +1703,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Pointer to experimental data instance */ - void fdJzdsigma(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata); + void fdJzdsigma( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief Compute sensitivity of event-resolved measurement negative @@ -1674,8 +1717,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Pointer to experimental data instance */ - void fdJzdp(const int ie, const int nroots, realtype t, const AmiVector &x, - const ExpData &edata); + void fdJzdp( + int const ie, int const nroots, realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief Compute sensitivity of event-resolved measurement negative @@ -1686,8 +1731,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void fdJzdx(const int ie, const int nroots, realtype t, const AmiVector &x, - const ExpData &edata); + void fdJzdx( + int const ie, int const nroots, realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief Compute regularization of negative log-likelihood with roots of @@ -1697,8 +1744,8 @@ class Model : public AbstractModel, public ModelDimensions { * @param rz Regularization variable * @param edata Experimental data */ - void fJrz(realtype &Jrz, int nroots, const AmiVector &rz, - const ExpData &edata); + void + fJrz(realtype& Jrz, int nroots, AmiVector const& rz, ExpData const& edata); /** * @brief Compute partial derivative of event measurement negative @@ -1709,8 +1756,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param x State variables * @param edata Experimental data */ - void fdJrzdz(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata); + void fdJrzdz( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata + ); /** * @brief Compute sensitivity of event measurement negative log-likelihood @@ -1721,36 +1770,50 @@ class Model : public AbstractModel, public ModelDimensions { * @param x state variables * @param edata pointer to experimental data instance */ - void fdJrzdsigma(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata); + void fdJrzdsigma( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata + ); + + /** + * @brief Spline functions + * @param t timepoint + */ + void fspl(realtype t); + + /** + * @brief Parametric derivatives of splines functions + * @param t timepoint + */ + void fsspl(realtype t); /** * @brief Compute recurring terms in xdot. * @param t Timepoint * @param x Array with the states */ - void fw(realtype t, const realtype *x); + void fw(realtype t, realtype const* x); /** * @brief Compute parameter derivative for recurring terms in xdot. * @param t Timepoint * @param x Array with the states */ - void fdwdp(realtype t, const realtype *x); + void fdwdp(realtype t, realtype const* x); /** * @brief Compute state derivative for recurring terms in xdot. * @param t Timepoint * @param x Array with the states */ - void fdwdx(realtype t, const realtype *x); + void fdwdx(realtype t, realtype const* x); /** * @brief Compute self derivative for recurring terms in xdot. * @param t Timepoint * @param x Array with the states */ - void fdwdw(realtype t, const realtype *x); + void fdwdw(realtype t, realtype const* x); /** * @brief Compute fx_rdata. @@ -1763,9 +1826,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param p parameter vector * @param k constant vector */ - virtual void fx_rdata(realtype *x_rdata, const realtype *x_solver, - const realtype *tcl, const realtype *p, - const realtype *k); + virtual void fx_rdata( + realtype* x_rdata, realtype const* x_solver, realtype const* tcl, + realtype const* p, realtype const* k + ); /** * @brief Compute fsx_solver. @@ -1783,11 +1847,11 @@ class Model : public AbstractModel, public ModelDimensions { * @param tcl Total abundances for conservation laws * @param ip Sensitivity index */ - virtual void fsx_rdata(realtype *sx_rdata, const realtype *sx_solver, - const realtype *stcl, const realtype *p, - const realtype *k, const realtype *x_solver, - const realtype *tcl, - const int ip); + virtual void fsx_rdata( + realtype* sx_rdata, realtype const* sx_solver, realtype const* stcl, + realtype const* p, realtype const* k, realtype const* x_solver, + realtype const* tcl, int const ip + ); /** * @brief Compute fx_solver. @@ -1797,7 +1861,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param x_solver State variables with conservation laws applied * @param x_rdata State variables with conservation laws expanded */ - virtual void fx_solver(realtype *x_solver, const realtype *x_rdata); + virtual void fx_solver(realtype* x_solver, realtype const* x_rdata); /** * @brief Compute fsx_solver. @@ -1809,7 +1873,7 @@ class Model : public AbstractModel, public ModelDimensions { * @param sx_solver State sensitivity variables with conservation laws * applied */ - virtual void fsx_solver(realtype *sx_solver, const realtype *sx_rdata); + virtual void fsx_solver(realtype* sx_solver, realtype const* sx_rdata); /** * @brief Compute ftotal_cl. @@ -1821,8 +1885,10 @@ class Model : public AbstractModel, public ModelDimensions { * @param p parameter vector * @param k constant vector */ - virtual void ftotal_cl(realtype *total_cl, const realtype *x_rdata, - const realtype *p, const realtype *k); + virtual void ftotal_cl( + realtype* total_cl, realtype const* x_rdata, realtype const* p, + realtype const* k + ); /** * @brief Compute fstotal_cl @@ -1839,10 +1905,11 @@ class Model : public AbstractModel, public ModelDimensions { * @param k constant vector * @param tcl Total abundances for conservation laws */ - virtual void fstotal_cl(realtype *stotal_cl, const realtype *sx_rdata, - const int ip, const realtype *x_rdata, - const realtype *p, const realtype *k, - const realtype *tcl); + virtual void fstotal_cl( + realtype* stotal_cl, realtype const* sx_rdata, int const ip, + realtype const* x_rdata, realtype const* p, realtype const* k, + realtype const* tcl + ); /** * @brief Compute non-negative state vector. @@ -1872,7 +1939,7 @@ class Model : public AbstractModel, public ModelDimensions { * @return State vector with negative values replaced by `0` according to * stateIsNonNegative */ - const realtype *computeX_pos(AmiVector const& x); + realtype const* computeX_pos(AmiVector const& x); /** All variables necessary for function evaluation */ ModelState state_; @@ -1882,6 +1949,9 @@ class Model : public AbstractModel, public ModelDimensions { */ ModelStateDerived derived_state_; + /** Storage for splines of the model */ + std::vector splines_; + /** index indicating to which event an event output belongs */ std::vector z2event_; @@ -1895,34 +1965,37 @@ class Model : public AbstractModel, public ModelDimensions { * be positive */ std::vector state_is_non_negative_; - /** Vector of booleans indicating the initial boolean value for every event trigger function. Events at t0 - * can only trigger if the initial value is set to `false`. Must be specified during model compilation by - * setting the `initialValue` attribute of an event trigger. */ + /** Vector of booleans indicating the initial boolean value for every event + * trigger function. Events at t0 can only trigger if the initial value is + * set to `false`. Must be specified during model compilation by setting the + * `initialValue` attribute of an event trigger. */ std::vector root_initial_values_; /** boolean indicating whether any entry in stateIsNonNegative is `true` */ - bool any_state_non_negative_ {false}; + bool any_state_non_negative_{false}; /** maximal number of events to track */ - int nmaxevent_ {10}; + int nmaxevent_{10}; /** * flag indicating whether steadystate sensitivities are to be computed * via FSA when steadyStateSimulation is used */ - SteadyStateSensitivityMode steadystate_sensitivity_mode_ {SteadyStateSensitivityMode::newtonOnly}; + SteadyStateSensitivityMode steadystate_sensitivity_mode_{ + SteadyStateSensitivityMode::newtonOnly}; /** * Indicates whether the result of every call to `Model::f*` should be * checked for finiteness */ - bool always_check_finite_ {false}; + bool always_check_finite_{false}; - /** indicates whether sigma residuals are to be added for every datapoint */ - bool sigma_res_ {false}; + /** indicates whether sigma residuals are to be added for every datapoint */ + bool sigma_res_{false}; - /** offset to ensure positivity of sigma residuals, only has an effect when `sigma_res_` is `true` */ - realtype min_sigma_ {50.0}; + /** offset to ensure positivity of sigma residuals, only has an effect when + * `sigma_res_` is `true` */ + realtype min_sigma_{50.0}; private: /** Sparse dwdp implicit temporary storage (shape `ndwdp`) */ @@ -1935,14 +2008,14 @@ class Model : public AbstractModel, public ModelDimensions { mutable std::vector dwdx_hierarchical_; /** Recursion */ - int w_recursion_depth_ {0}; + int w_recursion_depth_{0}; /** Simulation parameters, initial state, etc. */ SimulationParameters simulation_parameters_; }; -bool operator==(const Model &a, const Model &b); -bool operator==(const ModelDimensions &a, const ModelDimensions &b); +bool operator==(Model const& a, Model const& b); +bool operator==(ModelDimensions const& a, ModelDimensions const& b); } // namespace amici diff --git a/include/amici/model_dae.h b/include/amici/model_dae.h index b83d150940..dd16e74666 100644 --- a/include/amici/model_dae.h +++ b/include/amici/model_dae.h @@ -9,8 +9,8 @@ #include #include -#include #include +#include #include namespace amici { @@ -42,28 +42,32 @@ class Model_DAE : public Model { * @param ndxdotdx_explicit number of nonzero elements dxdotdx_explicit * @param w_recursion_depth Recursion depth of fw */ - Model_DAE(const ModelDimensions &model_dimensions, - SimulationParameters simulation_parameters, - const SecondOrderMode o2mode, - std::vector const &idlist, - std::vector const &z2event, const bool pythonGenerated=false, - const int ndxdotdp_explicit=0, const int ndxdotdx_explicit=0, - const int w_recursion_depth=0) - : Model(model_dimensions, simulation_parameters, - o2mode, idlist, z2event, pythonGenerated, - ndxdotdp_explicit, ndxdotdx_explicit, w_recursion_depth) { + Model_DAE( + ModelDimensions const& model_dimensions, + SimulationParameters simulation_parameters, + const SecondOrderMode o2mode, std::vector const& idlist, + std::vector const& z2event, bool const pythonGenerated = false, + int const ndxdotdp_explicit = 0, int const ndxdotdx_explicit = 0, + int const w_recursion_depth = 0 + ) + : Model( + model_dimensions, simulation_parameters, o2mode, idlist, z2event, + pythonGenerated, ndxdotdp_explicit, ndxdotdx_explicit, + w_recursion_depth + ) { derived_state_.M_ = SUNMatrixWrapper(nx_solver, nx_solver); auto M_nnz = static_cast( std::reduce(idlist.begin(), idlist.end()) ); - derived_state_.MSparse_ = SUNMatrixWrapper(nx_solver, nx_solver, - M_nnz, CSC_MAT); - derived_state_.dfdx_ = SUNMatrixWrapper(nx_solver, nx_solver, - 0, CSC_MAT); + derived_state_.MSparse_ + = SUNMatrixWrapper(nx_solver, nx_solver, M_nnz, CSC_MAT); + derived_state_.dfdx_ + = SUNMatrixWrapper(nx_solver, nx_solver, 0, CSC_MAT); } - void fJ(realtype t, realtype cj, const AmiVector &x, const AmiVector &dx, - const AmiVector &xdot, SUNMatrix J) override; + void + fJ(realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J) override; /** * @brief Jacobian of xdot with respect to states x @@ -74,12 +78,14 @@ class Model_DAE : public Model { * @param xdot Vector with the right hand side * @param J Matrix to which the Jacobian will be written **/ - void fJ(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, - const_N_Vector xdot, SUNMatrix J); + void + fJ(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector xdot, SUNMatrix J); - void fJB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, const AmiVector &dxB, - const AmiVector &xBdot, SUNMatrix JB) override; + void + fJB(const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB) override; /** * @brief Jacobian of xBdot with respect to adjoint state xB @@ -91,12 +97,14 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param JB Matrix to which the Jacobian will be written **/ - void fJB(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, SUNMatrix JB); + void + fJB(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector xB, const_N_Vector dxB, SUNMatrix JB); - void fJSparse(realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xdot, - SUNMatrix J) override; + void fJSparse( + realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J + ) override; /** * @brief J in sparse form (for sparse solvers from the SuiteSparse Package) @@ -106,16 +114,20 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states * @param J Matrix to which the Jacobian will be written */ - void fJSparse(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, - SUNMatrix J); + void fJSparse( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + SUNMatrix J + ); - void fJSparseB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &dxB, const AmiVector &xBdot, - SUNMatrix JB) override; + void fJSparseB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB + ) override; /** - * @brief JB in sparse form (for sparse solvers from the SuiteSparse Package) + * @brief JB in sparse form (for sparse solvers from the SuiteSparse + * Package) * @param t timepoint * @param cj scalar in Jacobian * @param x Vector with the states @@ -124,8 +136,10 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param JB Matrix to which the Jacobian will be written */ - void fJSparseB(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, SUNMatrix JB); + void fJSparseB( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector xB, const_N_Vector dxB, SUNMatrix JB + ); /** * @brief Diagonal of the Jacobian (for preconditioning) @@ -136,12 +150,15 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states **/ - void fJDiag(realtype t, AmiVector &JDiag, realtype cj, const AmiVector &x, - const AmiVector &dx) override; + void fJDiag( + realtype t, AmiVector& JDiag, realtype cj, AmiVector const& x, + AmiVector const& dx + ) override; - void fJv(realtype t, const AmiVector &x, const AmiVector &dx, - const AmiVector &xdot, const AmiVector &v, AmiVector &nJv, - realtype cj) override; + void + fJv(realtype t, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, AmiVector const& v, AmiVector& nJv, + realtype cj) override; /** * @brief Matrix vector product of J with a vector v (for iterative solvers) @@ -153,11 +170,13 @@ class Model_DAE : public Model { * @param Jv Vector to which the Jacobian vector product will be * written **/ - void fJv(realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector v, - N_Vector Jv, realtype cj); + void + fJv(realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector v, + N_Vector Jv, realtype cj); /** - * @brief Matrix vector product of JB with a vector v (for iterative solvers) + * @brief Matrix vector product of JB with a vector v (for iterative + *solvers) * @param t timepoint * @param x Vector with the states * @param dx Vector with the derivative states @@ -168,12 +187,15 @@ class Model_DAE : public Model { * @param cj scalar in Jacobian (inverse stepsize) **/ - void fJvB(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, - const_N_Vector vB, N_Vector JvB, realtype cj); + void fJvB( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector dxB, const_N_Vector vB, N_Vector JvB, realtype cj + ); - void froot(realtype t, const AmiVector &x, const AmiVector &dx, - gsl::span root) override; + void froot( + realtype t, AmiVector const& x, AmiVector const& dx, + gsl::span root + ) override; /** * @brief Event trigger function for events @@ -182,10 +204,14 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states * @param root array with root function values */ - void froot(realtype t, const_N_Vector x, const_N_Vector dx, gsl::span root); + void froot( + realtype t, const_N_Vector x, const_N_Vector dx, + gsl::span root + ); - void fxdot(realtype t, const AmiVector &x, const AmiVector &dx, - AmiVector &xdot) override; + void fxdot( + realtype t, AmiVector const& x, AmiVector const& dx, AmiVector& xdot + ) override; /** * @brief Residual function of the DAE @@ -205,8 +231,10 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param xBdot Vector with the adjoint right hand side */ - void fxBdot(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, N_Vector xBdot); + void fxBdot( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector dxB, N_Vector xBdot + ); /** * @brief Right hand side of integral equation for quadrature states qB @@ -217,22 +245,27 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param qBdot Vector with the adjoint quadrature right hand side */ - void fqBdot(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, - N_Vector qBdot); + void fqBdot( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector dxB, N_Vector qBdot + ); - void fxBdot_ss(const realtype t, const AmiVector &xB, - const AmiVector &dxB, AmiVector &xBdot) override; + void fxBdot_ss( + const realtype t, AmiVector const& xB, AmiVector const& dxB, + AmiVector& xBdot + ) override; /** - * @brief Implementation of fxBdot for steady state case at the N_Vector level + * @brief Implementation of fxBdot for steady state case at the N_Vector + * level * @param t timepoint * @param xB Vector with the adjoint state * @param dxB Vector with the adjoint derivative states * @param xBdot Vector with the adjoint right hand side */ - void fxBdot_ss(realtype t, const_N_Vector xB, const_N_Vector dxB, - N_Vector xBdot) const; + void fxBdot_ss( + realtype t, const_N_Vector xB, const_N_Vector dxB, N_Vector xBdot + ) const; /** * @brief Implementation of fqBdot for steady state at the N_Vector level @@ -241,8 +274,9 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param qBdot Vector with the adjoint quadrature right hand side */ - void fqBdot_ss(realtype t, const_N_Vector xB, const_N_Vector dxB, - N_Vector qBdot) const; + void fqBdot_ss( + realtype t, const_N_Vector xB, const_N_Vector dxB, N_Vector qBdot + ) const; /** * @brief Sparse Jacobian function backward, steady state case @@ -261,10 +295,10 @@ class Model_DAE : public Model { * @param dxB Vector with the adjoint derivative states * @param xBdot Vector with the adjoint state right hand side */ - void writeSteadystateJB(const realtype t, realtype cj, - const AmiVector &x, const AmiVector &dx, - const AmiVector &xB, const AmiVector &dxB, - const AmiVector &xBdot) override; + void writeSteadystateJB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot + ) override; /** * @brief Sensitivity of dx/dt wrt model parameters p @@ -273,16 +307,18 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states */ void fdxdotdp(realtype t, const const_N_Vector x, const const_N_Vector dx); - void fdxdotdp(const realtype t, const AmiVector &x, - const AmiVector &dx) override { + void fdxdotdp(const realtype t, AmiVector const& x, AmiVector const& dx) + override { fdxdotdp(t, x.getNVector(), dx.getNVector()); }; - void fsxdot(realtype t, const AmiVector &x, const AmiVector &dx, int ip, - const AmiVector &sx, const AmiVector &sdx, - AmiVector &sxdot) override; + void fsxdot( + realtype t, AmiVector const& x, AmiVector const& dx, int ip, + AmiVector const& sx, AmiVector const& sdx, AmiVector& sxdot + ) override; /** - * @brief Right hand side of differential equation for state sensitivities sx + * @brief Right hand side of differential equation for state sensitivities + * sx * @param t timepoint * @param x Vector with the states * @param dx Vector with the derivative states @@ -291,8 +327,10 @@ class Model_DAE : public Model { * @param sdx Vector with the derivative state sensitivities * @param sxdot Vector with the sensitivity right hand side */ - void fsxdot(realtype t, const_N_Vector x, const_N_Vector dx, int ip, - const_N_Vector sx, const_N_Vector sdx, N_Vector sxdot); + void fsxdot( + realtype t, const_N_Vector x, const_N_Vector dx, int ip, + const_N_Vector sx, const_N_Vector sdx, N_Vector sxdot + ); /** * @brief Mass matrix for DAE systems @@ -317,10 +355,11 @@ class Model_DAE : public Model { * @param w vector with helper variables * @param dwdx derivative of w wrt x **/ - virtual void fJSparse(SUNMatrixContent_Sparse JSparse, realtype t, - const realtype *x, const double *p, const double *k, - const realtype *h, realtype cj, const realtype *dx, - const realtype *w, const realtype *dwdx); + virtual void fJSparse( + SUNMatrixContent_Sparse JSparse, realtype t, realtype const* x, + double const* p, double const* k, realtype const* h, realtype cj, + realtype const* dx, realtype const* w, realtype const* dwdx + ); /** * @brief Model specific implementation for froot @@ -332,9 +371,10 @@ class Model_DAE : public Model { * @param h Heaviside vector * @param dx Vector with the derivative states **/ - virtual void froot(realtype *root, realtype t, const realtype *x, - const double *p, const double *k, const realtype *h, - const realtype *dx); + virtual void froot( + realtype* root, realtype t, realtype const* x, double const* p, + double const* k, realtype const* h, realtype const* dx + ); /** * @brief Model specific implementation for fxdot @@ -347,9 +387,11 @@ class Model_DAE : public Model { * @param w vector with helper variables * @param dx Vector with the derivative states **/ - virtual void fxdot(realtype *xdot, realtype t, const realtype *x, - const double *p, const double *k, const realtype *h, - const realtype *dx, const realtype *w) = 0; + virtual void fxdot( + realtype* xdot, realtype t, realtype const* x, double const* p, + double const* k, realtype const* h, realtype const* dx, + realtype const* w + ) = 0; /** * @brief Model specific implementation of fdxdotdp @@ -364,13 +406,15 @@ class Model_DAE : public Model { * @param w vector with helper variables * @param dwdp derivative of w wrt p */ - virtual void fdxdotdp(realtype *dxdotdp, realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, int ip, const realtype *dx, - const realtype *w, const realtype *dwdp); - + virtual void fdxdotdp( + realtype* dxdotdp, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, int ip, realtype const* dx, + realtype const* w, realtype const* dwdp + ); + /** - * @brief Model specific implementation of fdxdotdp_explicit, no w chainrule (Py) + * @brief Model specific implementation of fdxdotdp_explicit, no w chainrule + * (Py) * * @param dxdotdp_explicit partial derivative xdot wrt p * @param t timepoint @@ -381,27 +425,29 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states * @param w vector with helper variables */ - virtual void fdxdotdp_explicit(realtype *dxdotdp_explicit, realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *dx, const realtype *w); + virtual void fdxdotdp_explicit( + realtype* dxdotdp_explicit, realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* dx, realtype const* w + ); /** * @brief Model specific implementation of fdxdotdp_explicit, colptrs part * * @param dxdotdp sparse matrix to which colptrs will be written */ - virtual void fdxdotdp_explicit_colptrs(SUNMatrixWrapper &dxdotdp); + virtual void fdxdotdp_explicit_colptrs(SUNMatrixWrapper& dxdotdp); /** * @brief Model specific implementation of fdxdotdp_explicit, rowvals part * * @param dxdotdp sparse matrix to which rowvals will be written */ - virtual void fdxdotdp_explicit_rowvals(SUNMatrixWrapper &dxdotdp); + virtual void fdxdotdp_explicit_rowvals(SUNMatrixWrapper& dxdotdp); /** - * @brief Model specific implementation of fdxdotdx_explicit, no w chainrule (Py) + * @brief Model specific implementation of fdxdotdx_explicit, no w chainrule + * (Py) * * @param dxdotdx_explicit partial derivative xdot wrt x * @param t timepoint @@ -412,24 +458,25 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states * @param w vector with helper variables */ - virtual void fdxdotdx_explicit(realtype *dxdotdx_explicit, realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *dx, const realtype *w); + virtual void fdxdotdx_explicit( + realtype* dxdotdx_explicit, realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* dx, realtype const* w + ); /** * @brief Model specific implementation of fdxdotdx_explicit, colptrs part * * @param dxdotdx sparse matrix to which colptrs will be written */ - virtual void fdxdotdx_explicit_colptrs(SUNMatrixWrapper &dxdotdx); + virtual void fdxdotdx_explicit_colptrs(SUNMatrixWrapper& dxdotdx); /** * @brief Model specific implementation of fdxdotdx_explicit, rowvals part * * @param dxdotdx sparse matrix to which rowvals will be written */ - virtual void fdxdotdx_explicit_rowvals(SUNMatrixWrapper &dxdotdx); + virtual void fdxdotdx_explicit_rowvals(SUNMatrixWrapper& dxdotdx); /** * @brief Model specific implementation of fdxdotdw, data part @@ -442,22 +489,23 @@ class Model_DAE : public Model { * @param dx Vector with the derivative states * @param w vector with helper variables */ - virtual void fdxdotdw(realtype *dxdotdw, realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, const realtype *dx, - const realtype *w); + virtual void fdxdotdw( + realtype* dxdotdw, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* dx, + realtype const* w + ); /** * @brief Model specific implementation of fdxdotdw, colptrs part * @param dxdotdw sparse matrix to which colptrs will be written */ - virtual void fdxdotdw_colptrs(SUNMatrixWrapper &dxdotdw); + virtual void fdxdotdw_colptrs(SUNMatrixWrapper& dxdotdw); /** * @brief Model specific implementation of fdxdotdw, rowvals part * @param dxdotdw sparse matrix to which rowvals will be written */ - virtual void fdxdotdw_rowvals(SUNMatrixWrapper &dxdotdw); + virtual void fdxdotdw_rowvals(SUNMatrixWrapper& dxdotdw); /** * @brief Sensitivity of dx/dt wrt model parameters w @@ -465,8 +513,7 @@ class Model_DAE : public Model { * @param x Vector with the states * @param dx Vector with the derivative states */ - void fdxdotdw(realtype t, const_N_Vector x, - const_N_Vector dx); + void fdxdotdw(realtype t, const_N_Vector x, const_N_Vector dx); /** * @brief Model specific implementation of fM @@ -476,8 +523,9 @@ class Model_DAE : public Model { * @param p parameter vector * @param k constants vector */ - virtual void fM(realtype *M, const realtype t, const realtype *x, - const realtype *p, const realtype *k); + virtual void + fM(realtype* M, const realtype t, realtype const* x, realtype const* p, + realtype const* k); }; } // namespace amici diff --git a/include/amici/model_dimensions.h b/include/amici/model_dimensions.h index eb53756e44..f0679dbe36 100644 --- a/include/amici/model_dimensions.h +++ b/include/amici/model_dimensions.h @@ -31,6 +31,7 @@ struct ModelDimensions { * @param nz Number of event observables * @param nztrue Number of event observables of the non-augmented model * @param ne Number of events + * @param nspl Number of splines * @param nJ Number of objective functions * @param nw Number of repeating elements * @param ndwdx Number of nonzero elements in the `x` derivative of the @@ -54,24 +55,41 @@ struct ModelDimensions { * @param lbw Lower matrix bandwidth in the Jacobian */ ModelDimensions( - const int nx_rdata, const int nxtrue_rdata, const int nx_solver, - const int nxtrue_solver, const int nx_solver_reinit, const int np, - const int nk, const int ny, - const int nytrue, const int nz, const int nztrue, const int ne, - const int nJ, const int nw, const int ndwdx, const int ndwdp, - const int ndwdw, const int ndxdotdw, std::vector ndJydy, - const int ndxrdatadxsolver, const int ndxrdatadtcl, - const int ndtotal_cldx_rdata, - const int nnz, const int ubw, const int lbw) - : nx_rdata(nx_rdata), nxtrue_rdata(nxtrue_rdata), nx_solver(nx_solver), - nxtrue_solver(nxtrue_solver), nx_solver_reinit(nx_solver_reinit), - np(np), nk(nk), - ny(ny), nytrue(nytrue), nz(nz), nztrue(nztrue), - ne(ne), nw(nw), ndwdx(ndwdx), ndwdp(ndwdp), ndwdw(ndwdw), - ndxdotdw(ndxdotdw), ndJydy(std::move(ndJydy)), - ndxrdatadxsolver(ndxrdatadxsolver), ndxrdatadtcl(ndxrdatadtcl), - ndtotal_cldx_rdata(ndtotal_cldx_rdata), - nnz(nnz), nJ(nJ), ubw(ubw), lbw(lbw) { + int const nx_rdata, int const nxtrue_rdata, int const nx_solver, + int const nxtrue_solver, int const nx_solver_reinit, int const np, + int const nk, int const ny, int const nytrue, int const nz, + int const nztrue, int const ne, int const nspl, int const nJ, + int const nw, int const ndwdx, int const ndwdp, int const ndwdw, + int const ndxdotdw, std::vector ndJydy, int const ndxrdatadxsolver, + int const ndxrdatadtcl, int const ndtotal_cldx_rdata, int const nnz, + int const ubw, int const lbw + ) + : nx_rdata(nx_rdata) + , nxtrue_rdata(nxtrue_rdata) + , nx_solver(nx_solver) + , nxtrue_solver(nxtrue_solver) + , nx_solver_reinit(nx_solver_reinit) + , np(np) + , nk(nk) + , ny(ny) + , nytrue(nytrue) + , nz(nz) + , nztrue(nztrue) + , ne(ne) + , nspl(nspl) + , nw(nw) + , ndwdx(ndwdx) + , ndwdp(ndwdp) + , ndwdw(ndwdw) + , ndxdotdw(ndxdotdw) + , ndJydy(std::move(ndJydy)) + , ndxrdatadxsolver(ndxrdatadxsolver) + , ndxrdatadtcl(ndxrdatadtcl) + , ndtotal_cldx_rdata(ndtotal_cldx_rdata) + , nnz(nnz) + , nJ(nJ) + , ubw(ubw) + , lbw(lbw) { Expects(nxtrue_rdata >= 0); Expects(nxtrue_rdata <= nx_rdata); Expects(nxtrue_solver >= 0); @@ -86,6 +104,7 @@ struct ModelDimensions { Expects(nztrue >= 0); Expects(nztrue <= nz); Expects(ne >= 0); + Expects(nspl >= 0); Expects(nw >= 0); Expects(ndwdx >= 0); Expects(ndwdx <= nw * nx_solver); @@ -97,9 +116,9 @@ struct ModelDimensions { Expects(ndxrdatadxsolver >= 0); Expects(ndxrdatadxsolver <= nx_rdata * nx_solver); Expects(ndxrdatadtcl >= 0); - Expects(ndxrdatadtcl <= nx_rdata * (nx_rdata-nx_solver)); + Expects(ndxrdatadtcl <= nx_rdata * (nx_rdata - nx_solver)); Expects(ndtotal_cldx_rdata >= 0); - Expects(ndtotal_cldx_rdata <= (nx_rdata-nx_solver) * nx_rdata); + Expects(ndtotal_cldx_rdata <= (nx_rdata - nx_solver) * nx_rdata); Expects(nnz >= 0); Expects(nJ >= 0); Expects(ubw >= 0); @@ -145,6 +164,9 @@ struct ModelDimensions { /** Number of events */ int ne{0}; + /** numer of spline functions in the model */ + int nspl{0}; + /** Number of common expressions */ int nw{0}; @@ -152,22 +174,23 @@ struct ModelDimensions { * Number of nonzero elements in the `x` derivative of the * repeating elements */ - int ndwdx {0}; + int ndwdx{0}; /** * Number of nonzero elements in the `p` derivative of the * repeating elements */ - int ndwdp {0}; + int ndwdp{0}; /** * Number of nonzero elements in the `w` derivative of the * repeating elements */ - int ndwdw {0}; + int ndwdw{0}; - /** Number of nonzero elements in the \f$ w \f$ derivative of \f$ xdot \f$ */ - int ndxdotdw {0}; + /** Number of nonzero elements in the \f$ w \f$ derivative of \f$ xdot \f$ + */ + int ndxdotdw{0}; /** * Number of nonzero elements in the \f$ y \f$ derivative of @@ -175,10 +198,12 @@ struct ModelDimensions { */ std::vector ndJydy; - /** Number of nonzero elements in the \f$ x \f$ derivative of \f$ x_rdata \f$ */ + /** Number of nonzero elements in the \f$ x \f$ derivative of \f$ x_rdata + * \f$ */ int ndxrdatadxsolver{0}; - /** Number of nonzero elements in the \f$ tcl\f$ derivative of \f$ x_rdata \f$ */ + /** Number of nonzero elements in the \f$ tcl\f$ derivative of \f$ x_rdata + * \f$ */ int ndxrdatadtcl{0}; /** Number of nonzero elements in the \f$ x_rdata\f$ derivative of diff --git a/include/amici/model_ode.h b/include/amici/model_ode.h index a7c701a90c..91e0c9cd45 100644 --- a/include/amici/model_ode.h +++ b/include/amici/model_ode.h @@ -41,19 +41,23 @@ class Model_ODE : public Model { * @param ndxdotdx_explicit number of nonzero elements dxdotdx_explicit * @param w_recursion_depth Recursion depth of fw */ - Model_ODE(ModelDimensions const& model_dimensions, - SimulationParameters simulation_parameters, - const SecondOrderMode o2mode, - std::vector const &idlist, - std::vector const &z2event, const bool pythonGenerated=false, - const int ndxdotdp_explicit=0, const int ndxdotdx_explicit=0, - const int w_recursion_depth=0) - : Model(model_dimensions, simulation_parameters, - o2mode, idlist, z2event, pythonGenerated, - ndxdotdp_explicit, ndxdotdx_explicit, w_recursion_depth) {} - - void fJ(realtype t, realtype cj, const AmiVector &x, const AmiVector &dx, - const AmiVector &xdot, SUNMatrix J) override; + Model_ODE( + ModelDimensions const& model_dimensions, + SimulationParameters simulation_parameters, + const SecondOrderMode o2mode, std::vector const& idlist, + std::vector const& z2event, bool const pythonGenerated = false, + int const ndxdotdp_explicit = 0, int const ndxdotdx_explicit = 0, + int const w_recursion_depth = 0 + ) + : Model( + model_dimensions, simulation_parameters, o2mode, idlist, z2event, + pythonGenerated, ndxdotdp_explicit, ndxdotdx_explicit, + w_recursion_depth + ) {} + + void + fJ(realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J) override; /** * @brief Implementation of fJ at the N_Vector level @@ -68,25 +72,29 @@ class Model_ODE : public Model { **/ void fJ(realtype t, const_N_Vector x, const_N_Vector xdot, SUNMatrix J); - void fJB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, const AmiVector &dxB, - const AmiVector &xBdot, SUNMatrix JB) override; + void + fJB(const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB) override; /** - * @brief Implementation of fJB at the N_Vector level, this function provides - * an interface to the model specific routines for the solver implementation + * @brief Implementation of fJB at the N_Vector level, this function + *provides an interface to the model specific routines for the solver + *implementation * @param t timepoint * @param x Vector with the states * @param xB Vector with the adjoint states * @param xBdot Vector with the adjoint right hand side * @param JB Matrix to which the Jacobian will be written **/ - void fJB(realtype t, const_N_Vector x, const_N_Vector xB, - const_N_Vector xBdot, SUNMatrix JB); + void + fJB(realtype t, const_N_Vector x, const_N_Vector xB, const_N_Vector xBdot, + SUNMatrix JB); - void fJSparse(realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xdot, - SUNMatrix J) override; + void fJSparse( + realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, SUNMatrix J + ) override; /** * @brief Implementation of fJSparse at the N_Vector level, this function @@ -98,10 +106,11 @@ class Model_ODE : public Model { */ void fJSparse(realtype t, const_N_Vector x, SUNMatrix J); - void fJSparseB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &dxB, const AmiVector &xBdot, - SUNMatrix JB) override; + void fJSparseB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot, + SUNMatrix JB + ) override; /** * @brief Implementation of fJSparseB at the N_Vector level, this function @@ -113,12 +122,15 @@ class Model_ODE : public Model { * @param xBdot Vector with the adjoint right hand side * @param JB Matrix to which the Jacobian will be written */ - void fJSparseB(realtype t, const_N_Vector x, const_N_Vector xB, - const_N_Vector xBdot, SUNMatrix JB); + void fJSparseB( + realtype t, const_N_Vector x, const_N_Vector xB, const_N_Vector xBdot, + SUNMatrix JB + ); /** - * @brief Implementation of fJDiag at the N_Vector level, this function provides - * an interface to the model specific routines for the solver implementation + * @brief Implementation of fJDiag at the N_Vector level, this function + *provides an interface to the model specific routines for the solver + *implementation * @param t timepoint * @param JDiag Vector to which the Jacobian diagonal will be written * @param x Vector with the states @@ -133,12 +145,15 @@ class Model_ODE : public Model { * @param x Vector with the states * @param dx Vector with the derivative states **/ - void fJDiag(realtype t, AmiVector &JDiag, realtype cj, const AmiVector &x, - const AmiVector &dx) override; + void fJDiag( + realtype t, AmiVector& JDiag, realtype cj, AmiVector const& x, + AmiVector const& dx + ) override; - void fJv(realtype t, const AmiVector &x, const AmiVector &dx, - const AmiVector &xdot, const AmiVector &v, AmiVector &nJv, - realtype cj) override; + void + fJv(realtype t, AmiVector const& x, AmiVector const& dx, + AmiVector const& xdot, AmiVector const& v, AmiVector& nJv, + realtype cj) override; /** * @brief Implementation of fJv at the N_Vector level. @@ -158,11 +173,15 @@ class Model_ODE : public Model { * @param vB Vector with which the Jacobian is multiplied * @param JvB Vector to which the Jacobian vector product will be written **/ - void fJvB(const_N_Vector vB, N_Vector JvB, realtype t, const_N_Vector x, - const_N_Vector xB); + void fJvB( + const_N_Vector vB, N_Vector JvB, realtype t, const_N_Vector x, + const_N_Vector xB + ); - void froot(realtype t, const AmiVector &x, const AmiVector &dx, - gsl::span root) override; + void froot( + realtype t, AmiVector const& x, AmiVector const& dx, + gsl::span root + ) override; /** * @brief Implementation of froot at the N_Vector level @@ -174,8 +193,9 @@ class Model_ODE : public Model { */ void froot(realtype t, const_N_Vector x, gsl::span root); - void fxdot(realtype t, const AmiVector &x, const AmiVector &dx, - AmiVector &xdot) override; + void fxdot( + realtype t, AmiVector const& x, AmiVector const& dx, AmiVector& xdot + ) override; /** * @brief Implementation of fxdot at the N_Vector level, this function @@ -203,10 +223,13 @@ class Model_ODE : public Model { * @param xB Vector with the adjoint states * @param qBdot Vector with the adjoint quadrature right hand side */ - void fqBdot(realtype t, const_N_Vector x, const_N_Vector xB, N_Vector qBdot); + void + fqBdot(realtype t, const_N_Vector x, const_N_Vector xB, N_Vector qBdot); - void fxBdot_ss(const realtype t, const AmiVector &xB, - const AmiVector & /*dxB*/, AmiVector &xBdot) override; + void fxBdot_ss( + const realtype t, AmiVector const& xB, AmiVector const& /*dxB*/, + AmiVector& xBdot + ) override; /** * @brief Implementation of fxBdot for steady state at the N_Vector level @@ -217,7 +240,8 @@ class Model_ODE : public Model { void fxBdot_ss(realtype t, const_N_Vector xB, N_Vector xBdot) const; /** - * @brief Implementation of fqBdot for steady state case at the N_Vector level + * @brief Implementation of fqBdot for steady state case at the N_Vector + * level * @param t timepoint * @param xB Vector with the adjoint states * @param qBdot Vector with the adjoint quadrature right hand side @@ -241,14 +265,15 @@ class Model_ODE : public Model { * @param dxB Vector with the adjoint derivative states * @param xBdot Vector with the adjoint state right hand side */ - void writeSteadystateJB(const realtype t, realtype cj, - const AmiVector &x, const AmiVector &dx, - const AmiVector &xB, const AmiVector &dxB, - const AmiVector &xBdot) override; + void writeSteadystateJB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& xBdot + ) override; - void fsxdot(realtype t, const AmiVector &x, const AmiVector &dx, int ip, - const AmiVector &sx, const AmiVector &sdx, - AmiVector &sxdot) override; + void fsxdot( + realtype t, AmiVector const& x, AmiVector const& dx, int ip, + AmiVector const& sx, AmiVector const& sdx, AmiVector& sxdot + ) override; /** * @brief Implementation of fsxdot at the N_Vector level @@ -258,13 +283,13 @@ class Model_ODE : public Model { * @param sx Vector with the state sensitivities * @param sxdot Vector with the sensitivity right hand side */ - void fsxdot(realtype t, const_N_Vector x, int ip, const_N_Vector sx, - N_Vector sxdot); + void fsxdot( + realtype t, const_N_Vector x, int ip, const_N_Vector sx, N_Vector sxdot + ); std::unique_ptr getSolver() override; protected: - /** * @brief Model specific implementation for fJSparse (Matlab) * @param JSparse Matrix to which the Jacobian will be written @@ -276,10 +301,11 @@ class Model_ODE : public Model { * @param w vector with helper variables * @param dwdx derivative of w wrt x **/ - virtual void fJSparse(SUNMatrixContent_Sparse JSparse, realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *w, const realtype *dwdx); + virtual void fJSparse( + SUNMatrixContent_Sparse JSparse, realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* w, realtype const* dwdx + ); /** * @brief Model specific implementation for fJSparse, data only (Py) @@ -292,22 +318,23 @@ class Model_ODE : public Model { * @param w vector with helper variables * @param dwdx derivative of w wrt x **/ - virtual void fJSparse(realtype *JSparse, realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, const realtype *w, - const realtype *dwdx); + virtual void fJSparse( + realtype* JSparse, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w, + realtype const* dwdx + ); /** * @brief Model specific implementation for fJSparse, column pointers * @param JSparse sparse matrix to which colptrs will be written **/ - virtual void fJSparse_colptrs(SUNMatrixWrapper &JSparse); + virtual void fJSparse_colptrs(SUNMatrixWrapper& JSparse); /** * @brief Model specific implementation for fJSparse, row values * @param JSparse sparse matrix to which rowvals will be written **/ - virtual void fJSparse_rowvals(SUNMatrixWrapper &JSparse); + virtual void fJSparse_rowvals(SUNMatrixWrapper& JSparse); /** * @brief Model specific implementation for froot @@ -319,9 +346,10 @@ class Model_ODE : public Model { * @param h Heaviside vector * @param tcl total abundances for conservation laws **/ - virtual void froot(realtype *root, realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *tcl); + virtual void froot( + realtype* root, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* tcl + ); /** * @brief Model specific implementation for fxdot @@ -333,12 +361,14 @@ class Model_ODE : public Model { * @param h Heaviside vector * @param w vector with helper variables **/ - virtual void fxdot(realtype *xdot, realtype t, const realtype *x, - const realtype *p, const realtype *k, const realtype *h, - const realtype *w) = 0; + virtual void fxdot( + realtype* xdot, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w + ) = 0; /** - * @brief Model specific implementation of fdxdotdp, with w chainrule (Matlab) + * @brief Model specific implementation of fdxdotdp, with w chainrule + * (Matlab) * @param dxdotdp partial derivative xdot wrt p * @param t timepoint * @param x Vector with the states @@ -349,13 +379,15 @@ class Model_ODE : public Model { * @param w vector with helper variables * @param dwdp derivative of w wrt p */ - virtual void fdxdotdp(realtype *dxdotdp, realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, int ip, const realtype *w, - const realtype *dwdp); + virtual void fdxdotdp( + realtype* dxdotdp, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, int ip, realtype const* w, + realtype const* dwdp + ); /** - * @brief Model specific implementation of fdxdotdp_explicit, no w chainrule (Py) + * @brief Model specific implementation of fdxdotdp_explicit, no w chainrule + * (Py) * @param dxdotdp_explicit partial derivative xdot wrt p * @param t timepoint * @param x Vector with the states @@ -364,25 +396,27 @@ class Model_ODE : public Model { * @param h Heaviside vector * @param w vector with helper variables */ - virtual void fdxdotdp_explicit(realtype *dxdotdp_explicit, realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *w); + virtual void fdxdotdp_explicit( + realtype* dxdotdp_explicit, realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* w + ); /** * @brief Model specific implementation of fdxdotdp_explicit, colptrs part * @param dxdotdp sparse matrix to which colptrs will be written */ - virtual void fdxdotdp_explicit_colptrs(SUNMatrixWrapper &dxdotdp); + virtual void fdxdotdp_explicit_colptrs(SUNMatrixWrapper& dxdotdp); /** * @brief Model specific implementation of fdxdotdp_explicit, rowvals part * @param dxdotdp sparse matrix to which rowvals will be written */ - virtual void fdxdotdp_explicit_rowvals(SUNMatrixWrapper &dxdotdp); + virtual void fdxdotdp_explicit_rowvals(SUNMatrixWrapper& dxdotdp); /** - * @brief Model specific implementation of fdxdotdx_explicit, no w chainrule (Py) + * @brief Model specific implementation of fdxdotdx_explicit, no w chainrule + * (Py) * @param dxdotdx_explicit partial derivative xdot wrt x * @param t timepoint * @param x Vector with the states @@ -391,22 +425,23 @@ class Model_ODE : public Model { * @param h heavyside vector * @param w vector with helper variables */ - virtual void fdxdotdx_explicit(realtype *dxdotdx_explicit, realtype t, - const realtype *x, const realtype *p, - const realtype *k, const realtype *h, - const realtype *w); + virtual void fdxdotdx_explicit( + realtype* dxdotdx_explicit, realtype t, realtype const* x, + realtype const* p, realtype const* k, realtype const* h, + realtype const* w + ); /** * @brief Model specific implementation of fdxdotdx_explicit, colptrs part * @param dxdotdx sparse matrix to which colptrs will be written */ - virtual void fdxdotdx_explicit_colptrs(SUNMatrixWrapper &dxdotdx); + virtual void fdxdotdx_explicit_colptrs(SUNMatrixWrapper& dxdotdx); /** * @brief Model specific implementation of fdxdotdx_explicit, rowvals part * @param dxdotdx sparse matrix to which rowvals will be written */ - virtual void fdxdotdx_explicit_rowvals(SUNMatrixWrapper &dxdotdx); + virtual void fdxdotdx_explicit_rowvals(SUNMatrixWrapper& dxdotdx); /** * @brief Model specific implementation of fdxdotdw, data part @@ -418,21 +453,22 @@ class Model_ODE : public Model { * @param h Heaviside vector * @param w vector with helper variables */ - virtual void fdxdotdw(realtype *dxdotdw, realtype t, const realtype *x, - const realtype *p, const realtype *k, - const realtype *h, const realtype *w); + virtual void fdxdotdw( + realtype* dxdotdw, realtype t, realtype const* x, realtype const* p, + realtype const* k, realtype const* h, realtype const* w + ); /** * @brief Model specific implementation of fdxdotdw, colptrs part * @param dxdotdw sparse matrix to which colptrs will be written */ - virtual void fdxdotdw_colptrs(SUNMatrixWrapper &dxdotdw); + virtual void fdxdotdw_colptrs(SUNMatrixWrapper& dxdotdw); /** * @brief Model specific implementation of fdxdotdw, rowvals part * @param dxdotdw sparse matrix to which rowvals will be written */ - virtual void fdxdotdw_rowvals(SUNMatrixWrapper &dxdotdw); + virtual void fdxdotdw_rowvals(SUNMatrixWrapper& dxdotdw); /** * @brief Sensitivity of dx/dt wrt model parameters w @@ -447,7 +483,7 @@ class Model_ODE : public Model { */ void fdxdotdp(realtype t, const_N_Vector x); - void fdxdotdp(realtype t, const AmiVector &x, const AmiVector &dx) override; + void fdxdotdp(realtype t, AmiVector const& x, AmiVector const& dx) override; }; } // namespace amici diff --git a/include/amici/model_state.h b/include/amici/model_state.h index 099147ba98..c6c46df3de 100644 --- a/include/amici/model_state.h +++ b/include/amici/model_state.h @@ -2,15 +2,14 @@ #define AMICI_MODEL_STATE_H #include "amici/defines.h" -#include "amici/sundials_matrix_wrapper.h" -#include "amici/model_dimensions.h" #include "amici/misc.h" +#include "amici/model_dimensions.h" +#include "amici/sundials_matrix_wrapper.h" #include namespace amici { - /** * @brief Exchange format to store and transfer the state of the * model at a specific timepoint. @@ -44,18 +43,19 @@ struct ModelState { * (dimension: nplist) */ std::vector plist; + + /** temporary storage for spline values */ + std::vector spl_; }; -inline bool operator==(const ModelState &a, const ModelState &b) { - return is_equal(a.h, b.h) - && is_equal(a.total_cl, b.total_cl) +inline bool operator==(ModelState const& a, ModelState const& b) { + return is_equal(a.h, b.h) && is_equal(a.total_cl, b.total_cl) && is_equal(a.stotal_cl, b.stotal_cl) && is_equal(a.unscaledParameters, b.unscaledParameters) && is_equal(a.fixedParameters, b.fixedParameters) && a.plist == b.plist; } - /** * @brief Storage for `amici::Model` quantities computed based on * `amici::ModelState` for a specific timepoint. @@ -71,16 +71,20 @@ struct ModelStateDerived { */ explicit ModelStateDerived(ModelDimensions const& dim); - /** Sparse Jacobian (dimension: `nx_solver` x `nx_solver`, nnz: `amici::Model::nnz`) */ + /** Sparse Jacobian (dimension: `nx_solver` x `nx_solver`, nnz: + * `amici::Model::nnz`) */ SUNMatrixWrapper J_; - /** Sparse Backwards Jacobian (dimension: `nx_solver` x `nx_solver`, nnz:`amici::Model::nnz`) */ + /** Sparse Backwards Jacobian (dimension: `nx_solver` x `nx_solver`, + * nnz:`amici::Model::nnz`) */ SUNMatrixWrapper JB_; - /** Sparse dxdotdw temporary storage (dimension: `nx_solver` x `nw`, nnz: `ndxdotdw`) */ + /** Sparse dxdotdw temporary storage (dimension: `nx_solver` x `nw`, nnz: + * `ndxdotdw`) */ SUNMatrixWrapper dxdotdw_; - /** Sparse dwdx temporary storage (dimension: `nw` x `nx_solver`, nnz:`ndwdx`) */ + /** Sparse dwdx temporary storage (dimension: `nw` x `nx_solver`, + * nnz:`ndwdx`) */ SUNMatrixWrapper dwdx_; /** Sparse dwdp temporary storage (dimension: `nw` x `np`, nnz: `ndwdp`) */ @@ -88,11 +92,13 @@ struct ModelStateDerived { /** Dense Mass matrix (dimension: `nx_solver` x `nx_solver`) */ SUNMatrixWrapper M_; - - /** Sparse Mass matrix (dimension: `nx_solver` x `nx_solver`, nnz: `sum(amici::Model::idlist)`) */ + + /** Sparse Mass matrix (dimension: `nx_solver` x `nx_solver`, nnz: + * `sum(amici::Model::idlist)`) */ SUNMatrixWrapper MSparse_; - - /** JSparse intermediate matrix (dimension: `nx_solver` x `nx_solver`, nnz: dynamic) */ + + /** JSparse intermediate matrix (dimension: `nx_solver` x `nx_solver`, nnz: + * dynamic) */ SUNMatrixWrapper dfdx_; /** @@ -103,9 +109,9 @@ struct ModelStateDerived { SUNMatrixWrapper dxdotdp_full; /** - * Temporary storage of `dxdotdp_explicit` data across functions (Python only) - * (dimension: `nplist` x `nx_solver`, nnz: `ndxdotdp_explicit`, - * type `CSC_MAT`) + * Temporary storage of `dxdotdp_explicit` data across functions (Python + * only) (dimension: `nplist` x `nx_solver`, nnz: `ndxdotdp_explicit`, type + * `CSC_MAT`) */ SUNMatrixWrapper dxdotdp_explicit; @@ -118,8 +124,8 @@ struct ModelStateDerived { SUNMatrixWrapper dxdotdp_implicit; /** - * Temporary storage of `dxdotdx_explicit` data across functions (Python only) - * (dimension: `nplist` x `nx_solver`, nnz: `nxdotdotdx_explicit`, + * Temporary storage of `dxdotdx_explicit` data across functions (Python + * only) (dimension: `nplist` x `nx_solver`, nnz: `nxdotdotdx_explicit`, * type `CSC_MAT`) */ SUNMatrixWrapper dxdotdx_explicit; @@ -134,7 +140,8 @@ struct ModelStateDerived { /** * Temporary storage for `dx_rdatadx_solver` - * (dimension: `nx_rdata` x `nx_solver`, nnz: `ndxrdatadxsolver`, type: `CSC_MAT`) + * (dimension: `nx_rdata` x `nx_solver`, nnz: `ndxrdatadxsolver`, type: + * `CSC_MAT`) */ SUNMatrixWrapper dx_rdatadx_solver; @@ -155,7 +162,7 @@ struct ModelStateDerived { * Temporary storage of `dxdotdp` data across functions, Matlab only * (dimension: `nplist` x `nx_solver` , row-major) */ - AmiVectorArray dxdotdp {0, 0}; + AmiVectorArray dxdotdp{0, 0}; /** Sparse observable derivative of data likelihood, only used if * `pythonGenerated` == `true` (dimension `nytrue`, `nJ` x `ny`, row-major) @@ -250,7 +257,7 @@ struct ModelStateDerived { * (dimension: `nx_solver` x `nplist`, row-major) */ std::vector sx_; - + /** temporary storage for sy, * (dimension: `ny` x `nplist`, row-major) */ @@ -292,7 +299,8 @@ struct ModelStateDerived { */ std::vector dsigmazdp_; - /** temporary storage for change in x after event (dimension: `nx_solver`) */ + /** temporary storage for change in x after event (dimension: `nx_solver`) + */ std::vector deltax_; /** temporary storage for change in sx after event @@ -300,7 +308,8 @@ struct ModelStateDerived { */ std::vector deltasx_; - /** temporary storage for change in xB after event (dimension: `nx_solver`) */ + /** temporary storage for change in xB after event (dimension: `nx_solver`) + */ std::vector deltaxB_; /** temporary storage for change in qB after event @@ -308,17 +317,19 @@ struct ModelStateDerived { */ std::vector deltaqB_; + /** temporary storage for sensitivity values of splines */ + SUNMatrixWrapper sspl_; + /** temporary storage of positified state variables according to * stateIsNonNegative (dimension: `nx_solver`) */ - AmiVector x_pos_tmp_ {0}; + AmiVector x_pos_tmp_{0}; }; - /** - * @brief implements an exchange format to store and transfer the state of a simulation at a - * specific timepoint. + * @brief implements an exchange format to store and transfer the state of a + * simulation at a specific timepoint. */ -struct SimulationState{ +struct SimulationState { /** timepoint */ realtype t; /** state variables */ @@ -331,7 +342,6 @@ struct SimulationState{ ModelState state; }; - } // namespace amici #endif // AMICI_MODEL_STATE_H diff --git a/include/amici/newton_solver.h b/include/amici/newton_solver.h index f829bf95c9..2e8b2f6573 100644 --- a/include/amici/newton_solver.h +++ b/include/amici/newton_solver.h @@ -28,7 +28,7 @@ class NewtonSolver { * * @param model pointer to the model object */ - explicit NewtonSolver(const Model &model); + explicit NewtonSolver(Model const& model); /** * @brief Factory method to create a NewtonSolver based on linsolType @@ -38,7 +38,7 @@ class NewtonSolver { * @return solver NewtonSolver according to the specified linsolType */ static std::unique_ptr - getSolver(const Solver &simulationSolver, const Model &model); + getSolver(Solver const& simulationSolver, Model const& model); /** * @brief Computes the solution of one Newton iteration @@ -48,7 +48,7 @@ class NewtonSolver { * @param model pointer to the model instance * @param state current simulation state */ - void getStep(AmiVector &delta, Model &model, const SimulationState &state); + void getStep(AmiVector& delta, Model& model, SimulationState const& state); /** * @brief Computes steady state sensitivities @@ -57,8 +57,9 @@ class NewtonSolver { * @param model pointer to the model instance * @param state current simulation state */ - void computeNewtonSensis(AmiVectorArray &sx, Model &model, - const SimulationState &state); + void computeNewtonSensis( + AmiVectorArray& sx, Model& model, SimulationState const& state + ); /** * @brief Writes the Jacobian for the Newton iteration and passes it to the @@ -67,8 +68,8 @@ class NewtonSolver { * @param model pointer to the model instance * @param state current simulation state */ - virtual void prepareLinearSystem(Model &model, - const SimulationState &state) = 0; + virtual void prepareLinearSystem(Model& model, SimulationState const& state) + = 0; /** * Writes the Jacobian (JB) for the Newton iteration and passes it to the @@ -77,8 +78,9 @@ class NewtonSolver { * @param model pointer to the model instance * @param state current simulation state */ - virtual void prepareLinearSystemB(Model &model, - const SimulationState &state) = 0; + virtual void + prepareLinearSystemB(Model& model, SimulationState const& state) + = 0; /** * @brief Solves the linear system for the Newton step @@ -86,7 +88,7 @@ class NewtonSolver { * @param rhs containing the RHS of the linear system, will be * overwritten by solution to the linear system */ - virtual void solveLinearSystem(AmiVector &rhs) = 0; + virtual void solveLinearSystem(AmiVector& rhs) = 0; /** * @brief Reinitialize the linear solver @@ -102,8 +104,8 @@ class NewtonSolver { * @return boolean indicating whether the linear system is singular * (condition number < 1/machine precision) */ - virtual bool is_singular(Model &model, - const SimulationState &state) const = 0; + virtual bool is_singular(Model& model, SimulationState const& state) const + = 0; virtual ~NewtonSolver() = default; @@ -132,25 +134,25 @@ class NewtonSolverDense : public NewtonSolver { * * @param model model instance that provides problem dimensions */ - explicit NewtonSolverDense(const Model &model); + explicit NewtonSolverDense(Model const& model); - NewtonSolverDense(const NewtonSolverDense &) = delete; + NewtonSolverDense(NewtonSolverDense const&) = delete; - NewtonSolverDense &operator=(const NewtonSolverDense &other) = delete; + NewtonSolverDense& operator=(NewtonSolverDense const& other) = delete; ~NewtonSolverDense() override; - void solveLinearSystem(AmiVector &rhs) override; + void solveLinearSystem(AmiVector& rhs) override; - void prepareLinearSystem(Model &model, - const SimulationState &state) override; + void + prepareLinearSystem(Model& model, SimulationState const& state) override; - void prepareLinearSystemB(Model &model, - const SimulationState &state) override; + void + prepareLinearSystemB(Model& model, SimulationState const& state) override; void reinitialize() override; - bool is_singular(Model &model, const SimulationState &state) const override; + bool is_singular(Model& model, SimulationState const& state) const override; private: /** temporary storage of Jacobian */ @@ -173,23 +175,23 @@ class NewtonSolverSparse : public NewtonSolver { * * @param model model instance that provides problem dimensions */ - explicit NewtonSolverSparse(const Model &model); + explicit NewtonSolverSparse(Model const& model); - NewtonSolverSparse(const NewtonSolverSparse &) = delete; + NewtonSolverSparse(NewtonSolverSparse const&) = delete; - NewtonSolverSparse &operator=(const NewtonSolverSparse &other) = delete; + NewtonSolverSparse& operator=(NewtonSolverSparse const& other) = delete; ~NewtonSolverSparse() override; - void solveLinearSystem(AmiVector &rhs) override; + void solveLinearSystem(AmiVector& rhs) override; - void prepareLinearSystem(Model &model, - const SimulationState &state) override; + void + prepareLinearSystem(Model& model, SimulationState const& state) override; - void prepareLinearSystemB(Model &model, - const SimulationState &state) override; + void + prepareLinearSystemB(Model& model, SimulationState const& state) override; - bool is_singular(Model &model, const SimulationState &state) const override; + bool is_singular(Model& model, SimulationState const& state) const override; void reinitialize() override; diff --git a/include/amici/rdata.h b/include/amici/rdata.h index 5a647978da..98c512d4fe 100644 --- a/include/amici/rdata.h +++ b/include/amici/rdata.h @@ -2,10 +2,10 @@ #define AMICI_RDATA_H #include "amici/defines.h" -#include "amici/vector.h" -#include "amici/model.h" -#include "amici/misc.h" #include "amici/logging.h" +#include "amici/misc.h" +#include "amici/model.h" +#include "amici/vector.h" #include @@ -21,7 +21,7 @@ class SteadystateProblem; namespace boost { namespace serialization { template -void serialize(Archive &ar, amici::ReturnData &r, unsigned int version); +void serialize(Archive& ar, amici::ReturnData& r, unsigned int version); } } // namespace boost @@ -32,7 +32,7 @@ namespace amici { * * NOTE: multi-dimensional arrays are stored in row-major order (C-style) */ -class ReturnData: public ModelDimensions { +class ReturnData : public ModelDimensions { public: /** * @brief Default constructor @@ -54,17 +54,18 @@ class ReturnData: public ModelDimensions { * @param rdrm see amici::Solver::rdata_reporting * @param quadratic_llh whether model defines a quadratic nllh and * computing res, sres and FIM makes sense - * @param sigma_res indicates whether additional residuals are to be added for each sigma + * @param sigma_res indicates whether additional residuals are to be added + * for each sigma * @param sigma_offset offset to ensure real-valuedness of sigma residuals */ - ReturnData(std::vector ts, - ModelDimensions const& model_dimensions, - int nplist, int nmaxevent, int nt, - int newton_maxsteps, - std::vector pscale, SecondOrderMode o2mode, - SensitivityOrder sensi, SensitivityMethod sensi_meth, - RDataReporting rdrm, bool quadratic_llh, bool sigma_res, - realtype sigma_offset); + ReturnData( + std::vector ts, ModelDimensions const& model_dimensions, + int nplist, int nmaxevent, int nt, int newton_maxsteps, + std::vector pscale, SecondOrderMode o2mode, + SensitivityOrder sensi, SensitivityMethod sensi_meth, + RDataReporting rdrm, bool quadratic_llh, bool sigma_res, + realtype sigma_offset + ); /** * @brief constructor that uses information from model and solver to @@ -72,7 +73,7 @@ class ReturnData: public ModelDimensions { * @param solver solver instance * @param model model instance */ - ReturnData(Solver const &solver, const Model &model); + ReturnData(Solver const& solver, Model const& model); ~ReturnData() = default; @@ -82,17 +83,17 @@ class ReturnData: public ModelDimensions { * @param preeq simulated preequilibration problem, pass `nullptr` to ignore * @param fwd simulated forward problem, pass `nullptr` to ignore * @param bwd simulated backward problem, pass `nullptr` to ignore - * @param posteq simulated postequilibration problem, pass `nullptr` to ignore + * @param posteq simulated postequilibration problem, pass `nullptr` to + * ignore * @param model matching model instance * @param solver matching solver instance * @param edata matching experimental data */ - void processSimulationObjects(SteadystateProblem const *preeq, - ForwardProblem const *fwd, - BackwardProblem const *bwd, - SteadystateProblem const *posteq, - Model &model, Solver const &solver, - ExpData const *edata); + void processSimulationObjects( + SteadystateProblem const* preeq, ForwardProblem const* fwd, + BackwardProblem const* bwd, SteadystateProblem const* posteq, + Model& model, Solver const& solver, ExpData const* edata + ); /** * @brief Arbitrary (not necessarily unique) identifier. */ @@ -107,13 +108,15 @@ class ReturnData: public ModelDimensions { std::vector xdot; /** - * Jacobian of differential equation right hand side (shape `nx` x `nx`, row-major) + * Jacobian of differential equation right hand side (shape `nx` x `nx`, + * row-major) */ std::vector J; /** * w data from the model (recurring terms in xdot, for imported SBML models - * from python, this contains the flux vector) (shape `nt` x `nw`, row major) + * from python, this contains the flux vector) (shape `nt` x `nw`, row + * major) */ std::vector w; @@ -121,7 +124,8 @@ class ReturnData: public ModelDimensions { std::vector z; /** - * event output sigma standard deviation (shape `nmaxevent` x `nz`, row-major) + * event output sigma standard deviation (shape `nmaxevent` x `nz`, + * row-major) */ std::vector sigmaz; @@ -213,7 +217,8 @@ class ReturnData: public ModelDimensions { std::vector numnonlinsolvconvfails; /** - * number of linear solver convergence failures backward problem (shape `nt`) + * number of linear solver convergence failures backward problem (shape + * `nt`) */ std::vector numnonlinsolvconvfailsB; @@ -440,27 +445,30 @@ class ReturnData: public ModelDimensions { * @param version Version number */ template - friend void boost::serialization::serialize(Archive &ar, ReturnData &r, - unsigned int version); + friend void boost::serialization::serialize( + Archive& ar, ReturnData& r, unsigned int version + ); - /** boolean indicating whether residuals for standard deviations have been added */ + /** boolean indicating whether residuals for standard deviations have been + * added */ bool sigma_res; /** log messages */ std::vector messages; protected: - /** offset for sigma_residuals */ realtype sigma_offset; /** timepoint for model evaluation*/ realtype t_; - /** partial state vector, excluding states eliminated from conservation laws */ + /** partial state vector, excluding states eliminated from conservation laws + */ AmiVector x_solver_; - /** partial time derivative of state vector, excluding states eliminated from conservation laws */ + /** partial time derivative of state vector, excluding states eliminated + * from conservation laws */ AmiVector dx_solver_; /** partial sensitivity state vector array, excluding states eliminated from @@ -480,8 +488,8 @@ class ReturnData: public ModelDimensions { /** * @brief initializes storage for likelihood reporting mode - * @param quadratic_llh whether model defines a quadratic nllh and computing res, sres and FIM - * makes sense. + * @param quadratic_llh whether model defines a quadratic nllh and computing + * res, sres and FIM makes sense. */ void initializeLikelihoodReporting(bool quadratic_llh); @@ -497,7 +505,6 @@ class ReturnData: public ModelDimensions { */ void initializeFullReporting(bool enable_fim); - /** * @brief initialize values for chi2 and llh and derivatives * @param enable_chi2 whether chi2 values are to be computed @@ -509,8 +516,7 @@ class ReturnData: public ModelDimensions { * @param preeq SteadystateProblem for preequilibration * @param model Model instance to compute return values */ - void processPreEquilibration(SteadystateProblem const &preeq, - Model &model); + void processPreEquilibration(SteadystateProblem const& preeq, Model& model); /** * @brief extracts data from a preequilibration SteadystateProblem @@ -518,9 +524,9 @@ class ReturnData: public ModelDimensions { * @param model Model instance to compute return values * @param edata ExpData instance containing observable data */ - void processPostEquilibration(SteadystateProblem const &posteq, - Model &model, - ExpData const *edata); + void processPostEquilibration( + SteadystateProblem const& posteq, Model& model, ExpData const* edata + ); /** * @brief extracts results from forward problem @@ -528,10 +534,9 @@ class ReturnData: public ModelDimensions { * @param model model that was used for forward simulation * @param edata ExpData instance containing observable data */ - void processForwardProblem(ForwardProblem const &fwd, - Model &model, - ExpData const *edata); - + void processForwardProblem( + ForwardProblem const& fwd, Model& model, ExpData const* edata + ); /** * @brief extracts results from backward problem @@ -540,25 +545,26 @@ class ReturnData: public ModelDimensions { * @param preeq SteadystateProblem for preequilibration * @param model model that was used for forward/backward simulation */ - void processBackwardProblem(ForwardProblem const &fwd, - BackwardProblem const &bwd, - SteadystateProblem const *preeq, - Model &model); + void processBackwardProblem( + ForwardProblem const& fwd, BackwardProblem const& bwd, + SteadystateProblem const* preeq, Model& model + ); /** * @brief extracts results from solver * @param solver solver that was used for forward/backward simulation */ - void processSolver(Solver const &solver); + void processSolver(Solver const& solver); /** - * @brief Evaluates and stores the Jacobian and right hand side at final timepoint + * @brief Evaluates and stores the Jacobian and right hand side at final + * timepoint * @param problem forward problem or steadystate problem * @param model model that was used for forward/backward simulation */ template - void storeJacobianAndDerivativeInReturnData(T const &problem, Model &model) - { + void + storeJacobianAndDerivativeInReturnData(T const& problem, Model& model) { readSimulationState(problem.getFinalSimulationState(), model); AmiVector xdot(nx_solver); @@ -574,16 +580,17 @@ class ReturnData: public ModelDimensions { // CVODES uses colmajor, so we need to transform to rowmajor for (int ix = 0; ix < model.nx_solver; ix++) for (int jx = 0; jx < model.nx_solver; jx++) - this->J.at(ix * model.nx_solver + jx) = - J.data()[ix + model.nx_solver * jx]; + this->J.at(ix * model.nx_solver + jx) + = J.data()[ix + model.nx_solver * jx]; } } /** - * @brief sets member variables and model state according to provided simulation state + * @brief sets member variables and model state according to provided + * simulation state * @param state simulation state provided by Problem * @param model model that was used for forward/backward simulation */ - void readSimulationState(SimulationState const &state, Model &model); + void readSimulationState(SimulationState const& state, Model& model); /** * @brief Residual function @@ -591,14 +598,14 @@ class ReturnData: public ModelDimensions { * @param model model that was used for forward/backward simulation * @param edata ExpData instance containing observable data */ - void fres(int it, Model &model, const ExpData &edata); + void fres(int it, Model& model, ExpData const& edata); /** * @brief Chi-squared function * @param it time index * @param edata ExpData instance containing observable data */ - void fchi2(int it, const ExpData &edata); + void fchi2(int it, ExpData const& edata); /** * @brief Residual sensitivity function @@ -606,7 +613,7 @@ class ReturnData: public ModelDimensions { * @param model model that was used for forward/backward simulation * @param edata ExpData instance containing observable data */ - void fsres(int it, Model &model, const ExpData &edata); + void fsres(int it, Model& model, ExpData const& edata); /** * @brief Fisher information matrix function @@ -614,7 +621,7 @@ class ReturnData: public ModelDimensions { * @param model model that was used for forward/backward simulation * @param edata ExpData instance containing observable data */ - void fFIM(int it, Model &model, const ExpData &edata); + void fFIM(int it, Model& model, ExpData const& edata); /** * @brief Set likelihood, state variables, outputs and respective @@ -640,16 +647,17 @@ class ReturnData: public ModelDimensions { * the sensitivities of simulation results * @param model Model from which the ReturnData was obtained */ - void applyChainRuleFactorToSimulationResults(const Model &model); - + void applyChainRuleFactorToSimulationResults(Model const& model); /** * @brief Checks whether forward sensitivity analysis is performed * @return boolean indicator */ bool computingFSA() const { - return (sensi_meth == SensitivityMethod::forward && - sensi >= SensitivityOrder::first); + return ( + sensi_meth == SensitivityMethod::forward + && sensi >= SensitivityOrder::first + ); } /** @@ -659,7 +667,7 @@ class ReturnData: public ModelDimensions { * @param model model that was used in forward solve * @param edata ExpData instance carrying experimental data */ - void getDataOutput(int it, Model &model, ExpData const *edata); + void getDataOutput(int it, Model& model, ExpData const* edata); /** * @brief Extracts data information for forward sensitivity analysis, @@ -668,7 +676,7 @@ class ReturnData: public ModelDimensions { * @param model model that was used in forward solve * @param edata ExpData instance carrying experimental data */ - void getDataSensisFSA(int it, Model &model, ExpData const *edata); + void getDataSensisFSA(int it, Model& model, ExpData const* edata); /** * @brief Extracts output information for events, expects that x_solver_ @@ -679,8 +687,10 @@ class ReturnData: public ModelDimensions { * @param model model that was used in forward solve * @param edata ExpData instance carrying experimental data */ - void getEventOutput(realtype t, const std::vector rootidx, - Model &model, ExpData const *edata); + void getEventOutput( + realtype t, const std::vector rootidx, Model& model, + ExpData const* edata + ); /** * @brief Extracts event information for forward sensitivity analysis, @@ -690,8 +700,8 @@ class ReturnData: public ModelDimensions { * @param model model that was used in forward solve * @param edata ExpData instance carrying experimental data */ - void getEventSensisFSA(int ie, realtype t, Model &model, - ExpData const *edata); + void + getEventSensisFSA(int ie, realtype t, Model& model, ExpData const* edata); /** * @brief Updates contribution to likelihood from quadratures (xQB), @@ -702,20 +712,23 @@ class ReturnData: public ModelDimensions { * of preequilibration * @param xQB vector with quadratures from adjoint computation */ - void handleSx0Backward(const Model &model, SteadystateProblem const &preeq, - std::vector &llhS0, AmiVector &xQB) const; + void handleSx0Backward( + Model const& model, SteadystateProblem const& preeq, + std::vector& llhS0, AmiVector& xQB + ) const; /** * @brief Updates contribution to likelihood for initial state sensitivities - * (llhS0), if no preequilibration was run or if forward sensitivities were used + * (llhS0), if no preequilibration was run or if forward sensitivities were + * used * @param model model that was used for forward/backward simulation * @param llhS0 contribution to likelihood for initial state sensitivities * @param xB vector with final adjoint state * (excluding conservation laws) */ - void handleSx0Forward(const Model &model, - std::vector &llhS0, - AmiVector &xB) const; + void handleSx0Forward( + Model const& model, std::vector& llhS0, AmiVector& xB + ) const; }; /** @@ -729,9 +742,9 @@ class ModelContext : public ContextManager { * * @param model */ - explicit ModelContext(Model *model); + explicit ModelContext(Model* model); - ModelContext &operator=(const ModelContext &other) = delete; + ModelContext& operator=(ModelContext const& other) = delete; ~ModelContext(); @@ -743,11 +756,10 @@ class ModelContext : public ContextManager { void restore(); private: - Model *model_ {nullptr}; + Model* model_{nullptr}; ModelState original_state_; }; - } // namespace amici #endif /* _MY_RDATA */ diff --git a/include/amici/returndata_matlab.h b/include/amici/returndata_matlab.h index 7b82e8ae25..6a51db5264 100644 --- a/include/amici/returndata_matlab.h +++ b/include/amici/returndata_matlab.h @@ -14,7 +14,7 @@ namespace amici { * @param rdata ReturnDataObject * @return rdatamatlab ReturnDataObject stored as matlab compatible data */ -mxArray *getReturnDataMatlabFromAmiciCall(ReturnData const *rdata); +mxArray* getReturnDataMatlabFromAmiciCall(ReturnData const* rdata); /** * @brief allocates and initializes solution mxArray with the corresponding @@ -22,7 +22,7 @@ mxArray *getReturnDataMatlabFromAmiciCall(ReturnData const *rdata); * @param rdata ReturnDataObject * @return Solution mxArray */ -mxArray *initMatlabReturnFields(ReturnData const *rdata); +mxArray* initMatlabReturnFields(ReturnData const* rdata); /** * @brief allocates and initializes diagnosis mxArray with the corresponding @@ -30,7 +30,7 @@ mxArray *initMatlabReturnFields(ReturnData const *rdata); * @param rdata ReturnDataObject * @return Diagnosis mxArray */ -mxArray *initMatlabDiagnosisFields(ReturnData const *rdata); +mxArray* initMatlabDiagnosisFields(ReturnData const* rdata); /** * @brief initialize vector and attach to the field @@ -137,8 +137,8 @@ void checkFieldNames(char const** fieldNames, int const fieldCount); * @return Reordered vector */ template -std::vector reorder(std::vector const& input, - std::vector const& order); +std::vector +reorder(std::vector const& input, std::vector const& order); } // namespace amici diff --git a/include/amici/serialization.h b/include/amici/serialization.h index 3ba9ee7ffd..2695e38980 100644 --- a/include/amici/serialization.h +++ b/include/amici/serialization.h @@ -1,24 +1,24 @@ #ifndef AMICI_SERIALIZATION_H #define AMICI_SERIALIZATION_H -#include "amici/rdata.h" #include "amici/model.h" +#include "amici/rdata.h" #include "amici/solver.h" #include "amici/solver_cvodes.h" #include +#include #include #include -#include -#include -#include #include #include #include #include #include #include +#include +#include /** @file serialization.h Helper functions and forward declarations for * boost::serialization */ @@ -32,17 +32,17 @@ namespace serialization { * @param size Size of p */ template -void archiveVector(Archive &ar, T **p, int size) { +void archiveVector(Archive& ar, T** p, int size) { if (Archive::is_loading::value) { - if(*p != nullptr) - delete[] *p; - ar &size; + if (*p != nullptr) + delete[] * p; + ar& size; *p = size ? new T[size] : nullptr; } else { size = *p == nullptr ? 0 : size; - ar &size; + ar& size; } - ar &make_array(*p, size); + ar& make_array(*p, size); } #ifndef EXHALE_DOXYGEN_SHOULD_SKIP_THIS @@ -52,41 +52,41 @@ void archiveVector(Archive &ar, T **p, int size) { * @param s Solver instance to serialize */ template -void serialize(Archive &ar, amici::Solver &s, const unsigned int /*version*/) { - ar &s.sensi_; - ar &s.atol_; - ar &s.rtol_; - ar &s.atolB_; - ar &s.rtolB_; - ar &s.atol_fsa_; - ar &s.rtol_fsa_; - ar &s.quad_atol_; - ar &s.quad_rtol_; - ar &s.ss_tol_factor_; - ar &s.ss_atol_; - ar &s.ss_rtol_; - ar &s.ss_tol_sensi_factor_; - ar &s.ss_atol_sensi_; - ar &s.ss_rtol_sensi_; - ar &s.maxsteps_; - ar &s.maxstepsB_; - ar &s.newton_maxsteps_; - ar &s.newton_damping_factor_mode_; - ar &s.newton_damping_factor_lower_bound_; - ar &s.ism_; - ar &s.sensi_meth_; - ar &s.linsol_; - ar &s.interp_type_; - ar &s.lmm_; - ar &s.iter_; - ar &s.stldet_; - ar &s.ordering_; - ar &s.cpu_time_; - ar &s.cpu_timeB_; - ar &s.newton_step_steadystate_conv_; - ar &s.check_sensi_steadystate_conv_; - ar &s.rdata_mode_; - ar &s.maxtime_; +void serialize(Archive& ar, amici::Solver& s, unsigned int const /*version*/) { + ar& s.sensi_; + ar& s.atol_; + ar& s.rtol_; + ar& s.atolB_; + ar& s.rtolB_; + ar& s.atol_fsa_; + ar& s.rtol_fsa_; + ar& s.quad_atol_; + ar& s.quad_rtol_; + ar& s.ss_tol_factor_; + ar& s.ss_atol_; + ar& s.ss_rtol_; + ar& s.ss_tol_sensi_factor_; + ar& s.ss_atol_sensi_; + ar& s.ss_rtol_sensi_; + ar& s.maxsteps_; + ar& s.maxstepsB_; + ar& s.newton_maxsteps_; + ar& s.newton_damping_factor_mode_; + ar& s.newton_damping_factor_lower_bound_; + ar& s.ism_; + ar& s.sensi_meth_; + ar& s.linsol_; + ar& s.interp_type_; + ar& s.lmm_; + ar& s.iter_; + ar& s.stldet_; + ar& s.ordering_; + ar& s.cpu_time_; + ar& s.cpu_timeB_; + ar& s.newton_step_steadystate_conv_; + ar& s.check_sensi_steadystate_conv_; + ar& s.rdata_mode_; + ar& s.maxtime_; } /** @@ -95,14 +95,17 @@ void serialize(Archive &ar, amici::Solver &s, const unsigned int /*version*/) { * @param d Duration */ template -void serialize(Archive &ar, std::chrono::duration &d, const unsigned int /*version*/) { +void serialize( + Archive& ar, std::chrono::duration& d, + unsigned int const /*version*/ +) { Period tmp_period; if (Archive::is_loading::value) { - ar &tmp_period; + ar& tmp_period; d = std::chrono::duration(tmp_period); } else { tmp_period = d.count(); - ar &tmp_period; + ar& tmp_period; } } @@ -112,8 +115,10 @@ void serialize(Archive &ar, std::chrono::duration &d, const unsigne * @param s Solver instance to serialize */ template -void serialize(Archive &ar, amici::CVodeSolver &s, const unsigned int /*version*/) { - ar & static_cast(s); +void serialize( + Archive& ar, amici::CVodeSolver& s, unsigned int const /*version*/ +) { + ar& static_cast(s); } /** @@ -122,45 +127,46 @@ void serialize(Archive &ar, amici::CVodeSolver &s, const unsigned int /*version* * @param m Model instance to serialize */ template -void serialize(Archive &ar, amici::Model &m, const unsigned int /*version*/) { - ar &dynamic_cast(m); - ar &m.simulation_parameters_; - ar &m.o2mode; - ar &m.z2event_; - ar &m.idlist; - ar &m.state_.h; - ar &m.state_.unscaledParameters; - ar &m.state_.fixedParameters; - ar &m.state_.plist; - ar &m.x0data_; - ar &m.sx0data_; - ar &m.nmaxevent_; - ar &m.state_is_non_negative_; - ar &m.pythonGenerated; - ar &m.min_sigma_; - ar &m.sigma_res_; +void serialize(Archive& ar, amici::Model& m, unsigned int const /*version*/) { + ar& dynamic_cast(m); + ar& m.simulation_parameters_; + ar& m.o2mode; + ar& m.z2event_; + ar& m.idlist; + ar& m.state_.h; + ar& m.state_.unscaledParameters; + ar& m.state_.fixedParameters; + ar& m.state_.plist; + ar& m.x0data_; + ar& m.sx0data_; + ar& m.nmaxevent_; + ar& m.state_is_non_negative_; + ar& m.pythonGenerated; + ar& m.min_sigma_; + ar& m.sigma_res_; } - /** * @brief Serialize amici::SimulationParameters to boost archive * @param ar Archive * @param s amici::SimulationParameters instance to serialize */ template -void serialize(Archive &ar, amici::SimulationParameters &s, const unsigned int /*version*/) { - ar &s.fixedParameters; - ar &s.fixedParametersPreequilibration; - ar &s.fixedParametersPresimulation; - ar &s.parameters; - ar &s.x0; - ar &s.sx0; - ar &s.pscale; - ar &s.plist; - ar &s.ts_; - ar &s.tstart_; - ar &s.t_presim; - ar &s.reinitializeFixedParameterInitialStates; +void serialize( + Archive& ar, amici::SimulationParameters& s, unsigned int const /*version*/ +) { + ar& s.fixedParameters; + ar& s.fixedParametersPreequilibration; + ar& s.fixedParametersPresimulation; + ar& s.parameters; + ar& s.x0; + ar& s.sx0; + ar& s.pscale; + ar& s.plist; + ar& s.ts_; + ar& s.tstart_; + ar& s.t_presim; + ar& s.reinitializeFixedParameterInitialStates; } /** @@ -170,68 +176,69 @@ void serialize(Archive &ar, amici::SimulationParameters &s, const unsigned int / */ template -void serialize(Archive &ar, amici::ReturnData &r, const unsigned int /*version*/) { - ar &dynamic_cast(r); - ar &r.id; - ar &r.nx; - ar &r.nxtrue; - ar &r.nplist; - ar &r.nmaxevent; - ar &r.nt; - ar &r.newton_maxsteps; - ar &r.pscale; - ar &r.o2mode; - ar &r.sensi; - ar &r.sensi_meth; - - ar &r.ts; - ar &r.xdot; - ar &r.J; - ar &r.w; - ar &r.z & r.sigmaz; - ar &r.sz &r.ssigmaz; - ar &r.rz; - ar &r.srz; - ar &r.s2rz; - ar &r.x; - ar &r.sx; - ar &r.y & r.sigmay; - ar &r.sy & r.ssigmay; - - ar &r.numsteps; - ar &r.numstepsB; - ar &r.numrhsevals; - ar &r.numrhsevalsB; - ar &r.numerrtestfails; - ar &r.numerrtestfailsB; - ar &r.numnonlinsolvconvfails; - ar &r.numnonlinsolvconvfailsB; - ar &r.order; - ar &r.cpu_time; - ar &r.cpu_timeB; - ar &r.cpu_time_total; - ar &r.preeq_cpu_time; - ar &r.preeq_cpu_timeB; - ar &r.preeq_status; - ar &r.preeq_numsteps; - ar &r.preeq_wrms; - ar &r.preeq_t; - ar &r.posteq_cpu_time; - ar &r.posteq_cpu_timeB; - ar &r.posteq_status; - ar &r.posteq_numsteps; - ar &r.posteq_wrms; - ar &r.posteq_t; - ar &r.x0; - ar &r.sx0; - ar &r.llh; - ar &r.chi2; - ar &r.sllh; - ar &r.s2llh; - ar &r.status; +void serialize( + Archive& ar, amici::ReturnData& r, unsigned int const /*version*/ +) { + ar& dynamic_cast(r); + ar& r.id; + ar& r.nx; + ar& r.nxtrue; + ar& r.nplist; + ar& r.nmaxevent; + ar& r.nt; + ar& r.newton_maxsteps; + ar& r.pscale; + ar& r.o2mode; + ar& r.sensi; + ar& r.sensi_meth; + + ar& r.ts; + ar& r.xdot; + ar& r.J; + ar& r.w; + ar& r.z& r.sigmaz; + ar& r.sz& r.ssigmaz; + ar& r.rz; + ar& r.srz; + ar& r.s2rz; + ar& r.x; + ar& r.sx; + ar& r.y& r.sigmay; + ar& r.sy& r.ssigmay; + + ar& r.numsteps; + ar& r.numstepsB; + ar& r.numrhsevals; + ar& r.numrhsevalsB; + ar& r.numerrtestfails; + ar& r.numerrtestfailsB; + ar& r.numnonlinsolvconvfails; + ar& r.numnonlinsolvconvfailsB; + ar& r.order; + ar& r.cpu_time; + ar& r.cpu_timeB; + ar& r.cpu_time_total; + ar& r.preeq_cpu_time; + ar& r.preeq_cpu_timeB; + ar& r.preeq_status; + ar& r.preeq_numsteps; + ar& r.preeq_wrms; + ar& r.preeq_t; + ar& r.posteq_cpu_time; + ar& r.posteq_cpu_timeB; + ar& r.posteq_status; + ar& r.posteq_numsteps; + ar& r.posteq_wrms; + ar& r.posteq_t; + ar& r.x0; + ar& r.sx0; + ar& r.llh; + ar& r.chi2; + ar& r.sllh; + ar& r.s2llh; + ar& r.status; } - /** * @brief Serialize amici::ModelDimensions to boost archive * @param ar Archive @@ -239,29 +246,32 @@ void serialize(Archive &ar, amici::ReturnData &r, const unsigned int /*version*/ */ template -void serialize(Archive &ar, amici::ModelDimensions &m, const unsigned int /*version*/) { - ar &m.nx_rdata; - ar &m.nxtrue_rdata; - ar &m.nx_solver; - ar &m.nxtrue_solver; - ar &m.nx_solver_reinit; - ar &m.np; - ar &m.nk; - ar &m.ny; - ar &m.nytrue; - ar &m.nz; - ar &m.nztrue; - ar &m.ne; - ar &m.nw; - ar &m.ndwdx; - ar &m.ndwdp; - ar &m.ndwdw; - ar &m.ndxdotdw; - ar &m.ndJydy; - ar &m.nnz; - ar &m.nJ; - ar &m.ubw; - ar &m.lbw; +void serialize( + Archive& ar, amici::ModelDimensions& m, unsigned int const /*version*/ +) { + ar& m.nx_rdata; + ar& m.nxtrue_rdata; + ar& m.nx_solver; + ar& m.nxtrue_solver; + ar& m.nx_solver_reinit; + ar& m.np; + ar& m.nk; + ar& m.ny; + ar& m.nytrue; + ar& m.nz; + ar& m.nztrue; + ar& m.ne; + ar& m.nspl; + ar& m.nw; + ar& m.ndwdx; + ar& m.ndwdp; + ar& m.ndwdw; + ar& m.ndxdotdw; + ar& m.ndJydy; + ar& m.nnz; + ar& m.nJ; + ar& m.ubw; + ar& m.lbw; } #endif } // namespace serialization @@ -277,31 +287,31 @@ namespace amici { * * @return The object serialized as char */ -template -char *serializeToChar(T const& data, int *size) { +template char* serializeToChar(T const& data, int* size) { try { std::string serialized; - ::boost::iostreams::back_insert_device inserter(serialized); - ::boost::iostreams::stream<::boost::iostreams::back_insert_device> + ::boost::iostreams::back_insert_device inserter(serialized + ); + ::boost::iostreams::stream< + ::boost::iostreams::back_insert_device> s(inserter); ::boost::archive::binary_oarchive oar(s); oar << data; s.flush(); - char *charBuffer = new char[serialized.size()]; + char* charBuffer = new char[serialized.size()]; memcpy(charBuffer, serialized.data(), serialized.size()); if (size) *size = serialized.size(); return charBuffer; - } catch(boost::archive::archive_exception const& e) { + } catch (boost::archive::archive_exception const& e) { throw AmiException("Serialization to char failed: %s", e.what()); } } - /** * @brief Deserialize object that has been serialized using serializeToChar * @@ -311,8 +321,7 @@ char *serializeToChar(T const& data, int *size) { * @return The deserialized object */ -template -T deserializeFromChar(const char *buffer, int size) { +template T deserializeFromChar(char const* buffer, int size) { namespace ba = ::boost::archive; namespace bio = ::boost::iostreams; @@ -325,7 +334,7 @@ T deserializeFromChar(const char *buffer, int size) { // archive must be destroyed BEFORE returning ba::binary_iarchive iar(s); iar >> data; - } catch(ba::archive_exception const& e) { + } catch (ba::archive_exception const& e) { throw AmiException("Deserialization from char failed: %s", e.what()); } return data; @@ -339,8 +348,7 @@ T deserializeFromChar(const char *buffer, int size) { * @return The object serialized as string */ -template -std::string serializeToString(T const& data) { +template std::string serializeToString(T const& data) { namespace ba = ::boost::archive; namespace bio = ::boost::iostreams; @@ -352,7 +360,7 @@ std::string serializeToString(T const& data) { // archive must be destroyed BEFORE returning ba::binary_oarchive oar(os); oar << data; - } catch(ba::archive_exception const& e) { + } catch (ba::archive_exception const& e) { throw AmiException("Serialization to string failed: %s", e.what()); } @@ -367,21 +375,18 @@ std::string serializeToString(T const& data) { * @return The object serialized as std::vector */ -template -std::vector serializeToStdVec(T const& data) { +template std::vector serializeToStdVec(T const& data) { namespace ba = ::boost::archive; namespace bio = ::boost::iostreams; std::vector buffer; - bio::stream< - bio::back_insert_device< - std::vector>> os(buffer); + bio::stream>> os(buffer); - try{ + try { // archive must be destroyed BEFORE returning ba::binary_oarchive oar(os); oar << data; - } catch(ba::archive_exception const& e) { + } catch (ba::archive_exception const& e) { throw AmiException("Serialization to std::vector failed: %s", e.what()); } @@ -396,8 +401,7 @@ std::vector serializeToStdVec(T const& data) { * @return The deserialized object */ -template -T deserializeFromString(std::string const& serialized) { +template T deserializeFromString(std::string const& serialized) { namespace ba = ::boost::archive; namespace bio = ::boost::iostreams; @@ -405,18 +409,18 @@ T deserializeFromString(std::string const& serialized) { bio::stream> os(device); T deserialized; - try{ + try { // archive must be destroyed BEFORE returning ba::binary_iarchive iar(os); iar >> deserialized; - } catch(ba::archive_exception const& e) { - throw AmiException("Deserialization from std::string failed: %s", - e.what()); + } catch (ba::archive_exception const& e) { + throw AmiException( + "Deserialization from std::string failed: %s", e.what() + ); } return deserialized; } - } // namespace amici #endif // AMICI_SERIALIZATION_H diff --git a/include/amici/simulation_parameters.h b/include/amici/simulation_parameters.h index fe4bed9cf3..ca0e127c5c 100644 --- a/include/amici/simulation_parameters.h +++ b/include/amici/simulation_parameters.h @@ -11,7 +11,7 @@ namespace amici { * @brief Container for various simulation parameters. */ class SimulationParameters { -public: + public: SimulationParameters() = default; /** @@ -19,23 +19,21 @@ class SimulationParameters { * @param timepoints Timepoints for which simulation results are requested */ explicit SimulationParameters(std::vector timepoints) - : ts_(std::move(timepoints)) - { - } + : ts_(std::move(timepoints)) {} /** * @brief Constructor * @param fixedParameters Model constants * @param parameters Model parameters */ - SimulationParameters(std::vector fixedParameters, - std::vector parameters) - : fixedParameters(std::move(fixedParameters)), - parameters(std::move(parameters)), - pscale(std::vector(this->parameters.size(), - ParameterScaling::none)) - { - } + SimulationParameters( + std::vector fixedParameters, std::vector parameters + ) + : fixedParameters(std::move(fixedParameters)) + , parameters(std::move(parameters)) + , pscale(std::vector( + this->parameters.size(), ParameterScaling::none + )) {} /** * @brief Constructor @@ -44,17 +42,16 @@ class SimulationParameters { * @param plist Model parameter indices w.r.t. which sensitivities are to be * computed */ - SimulationParameters(std::vector fixedParameters, - std::vector parameters, - std::vector plist - ) - : fixedParameters(std::move(fixedParameters)), - parameters(std::move(parameters)), - pscale(std::vector(this->parameters.size(), - ParameterScaling::none)), - plist(std::move(plist)) - { - } + SimulationParameters( + std::vector fixedParameters, std::vector parameters, + std::vector plist + ) + : fixedParameters(std::move(fixedParameters)) + , parameters(std::move(parameters)) + , pscale(std::vector( + this->parameters.size(), ParameterScaling::none + )) + , plist(std::move(plist)) {} /** * @brief Constructor @@ -62,16 +59,16 @@ class SimulationParameters { * @param fixedParameters Model constants * @param parameters Model parameters */ - SimulationParameters(std::vector timepoints, - std::vector fixedParameters, - std::vector parameters) - : fixedParameters(std::move(fixedParameters)), - parameters(std::move(parameters)), - pscale(std::vector(this->parameters.size(), - ParameterScaling::none)), - ts_(std::move(timepoints)) - { - } + SimulationParameters( + std::vector timepoints, std::vector fixedParameters, + std::vector parameters + ) + : fixedParameters(std::move(fixedParameters)) + , parameters(std::move(parameters)) + , pscale(std::vector( + this->parameters.size(), ParameterScaling::none + )) + , ts_(std::move(timepoints)) {} /** * @brief Set reinitialization of all states based on model constants for @@ -83,7 +80,9 @@ class SimulationParameters { * * @param nx_rdata Number of states (Model::nx_rdata) */ - void reinitializeAllFixedParameterDependentInitialStatesForPresimulation(int nx_rdata); + void reinitializeAllFixedParameterDependentInitialStatesForPresimulation( + int nx_rdata + ); /** * @brief Set reinitialization of all states based on model constants for @@ -96,7 +95,9 @@ class SimulationParameters { * * @param nx_rdata Number of states (Model::nx_rdata) */ - void reinitializeAllFixedParameterDependentInitialStatesForSimulation(int nx_rdata); + void reinitializeAllFixedParameterDependentInitialStatesForSimulation( + int nx_rdata + ); /** * @brief Set reinitialization of all states based on model constants for @@ -169,7 +170,7 @@ class SimulationParameters { std::vector plist; /** starting time */ - realtype tstart_ {0.0}; + realtype tstart_{0.0}; /** * @brief Duration of pre-simulation. @@ -178,7 +179,7 @@ class SimulationParameters { * (model->t0 - t_presim) to model->t0 using the fixedParameters in * fixedParametersPresimulation */ - realtype t_presim {0.0}; + realtype t_presim{0.0}; /** * @brief Timepoints for which model state/outputs/... are requested @@ -191,7 +192,7 @@ class SimulationParameters { * @brief Flag indicating whether reinitialization of states depending on * fixed parameters is activated */ - bool reinitializeFixedParameterInitialStates {false}; + bool reinitializeFixedParameterInitialStates{false}; /** * @brief Indices of states to be reinitialized based on provided @@ -206,7 +207,7 @@ class SimulationParameters { std::vector reinitialization_state_idxs_sim; }; -bool operator==(const SimulationParameters &a, const SimulationParameters &b); +bool operator==(SimulationParameters const& a, SimulationParameters const& b); } // namespace amici diff --git a/include/amici/solver.h b/include/amici/solver.h index 46338410e1..120a963ba4 100644 --- a/include/amici/solver.h +++ b/include/amici/solver.h @@ -2,15 +2,15 @@ #define AMICI_SOLVER_H #include "amici/defines.h" -#include "amici/sundials_linsol_wrapper.h" -#include "amici/vector.h" #include "amici/logging.h" #include "amici/misc.h" +#include "amici/sundials_linsol_wrapper.h" +#include "amici/vector.h" +#include #include #include #include -#include namespace amici { @@ -26,9 +26,9 @@ class Solver; namespace boost { namespace serialization { template -void serialize(Archive &ar, amici::Solver &s, unsigned int version); +void serialize(Archive& ar, amici::Solver& s, unsigned int version); } -} // namespace boost::serialization +} // namespace boost namespace amici { @@ -47,7 +47,7 @@ namespace amici { class Solver { public: /** Type of what is passed to Sundials solvers as user_data */ - using user_data_type = std::pair; + using user_data_type = std::pair; /** * @brief Default constructor @@ -58,7 +58,7 @@ class Solver { * @brief Solver copy constructor * @param other */ - Solver(const Solver &other); + Solver(Solver const& other); virtual ~Solver() = default; @@ -66,7 +66,7 @@ class Solver { * @brief Clone this instance * @return The clone */ - virtual Solver *clone() const = 0; + virtual Solver* clone() const = 0; /** * @brief runs a forward simulation until the specified timepoint @@ -101,9 +101,10 @@ class Solver { * @param sdx0 initial derivative state sensitivities */ - void setup(realtype t0, Model *model, const AmiVector &x0, - const AmiVector &dx0, const AmiVectorArray &sx0, - const AmiVectorArray &sdx0) const; + void setup( + realtype t0, Model* model, AmiVector const& x0, AmiVector const& dx0, + AmiVectorArray const& sx0, AmiVectorArray const& sdx0 + ) const; /** * @brief Initializes the AMI memory object for the backwards problem @@ -115,8 +116,10 @@ class Solver { * @param xQB0 initial adjoint quadratures */ - void setupB(int *which, realtype tf, Model *model, const AmiVector &xB0, - const AmiVector &dxB0, const AmiVector &xQB0) const; + void setupB( + int* which, realtype tf, Model* model, AmiVector const& xB0, + AmiVector const& dxB0, AmiVector const& xQB0 + ) const; /** * @brief Initializes the ami memory for quadrature computation @@ -129,17 +132,19 @@ class Solver { * @param xQ0 initial quadrature vector */ - void setupSteadystate(const realtype t0, Model *model, const AmiVector &x0, - const AmiVector &dx0, const AmiVector &xB0, - const AmiVector &dxB0, const AmiVector &xQ0) const; + void setupSteadystate( + const realtype t0, Model* model, AmiVector const& x0, + AmiVector const& dx0, AmiVector const& xB0, AmiVector const& dxB0, + AmiVector const& xQ0 + ) const; /** - * @brief Reinitializes state and respective sensitivities (if necessary) according - * to changes in fixedParameters + * @brief Reinitializes state and respective sensitivities (if necessary) + * according to changes in fixedParameters * * @param model pointer to the model instance */ - void updateAndReinitStatesAndSensitivities(Model *model) const; + void updateAndReinitStatesAndSensitivities(Model* model) const; /** * getRootInfo extracts information which event occurred @@ -147,7 +152,7 @@ class Solver { * @param rootsfound array with flags indicating whether the respective * event occurred */ - virtual void getRootInfo(int *rootsfound) const = 0; + virtual void getRootInfo(int* rootsfound) const = 0; /** * @brief Calculates consistent initial conditions, assumes initial @@ -202,7 +207,8 @@ class Solver { * @brief Set sensitivity method for preequilibration * @param sensi_meth_preeq */ - void setSensitivityMethodPreequilibration(SensitivityMethod sensi_meth_preeq); + void setSensitivityMethodPreequilibration(SensitivityMethod sensi_meth_preeq + ); /** * @brief Disable forward sensitivity integration (used in steady state sim) @@ -637,8 +643,10 @@ class Solver { * @param sx state sensitivity * @param xQ quadrature */ - void writeSolution(realtype *t, AmiVector &x, AmiVector &dx, - AmiVectorArray &sx, AmiVector &xQ) const; + void writeSolution( + realtype* t, AmiVector& x, AmiVector& dx, AmiVectorArray& sx, + AmiVector& xQ + ) const; /** * @brief write solution from backward simulation @@ -648,29 +656,30 @@ class Solver { * @param xQB adjoint quadrature * @param which index of adjoint problem */ - void writeSolutionB(realtype *t, AmiVector &xB, AmiVector &dxB, - AmiVector &xQB, int which) const; + void writeSolutionB( + realtype* t, AmiVector& xB, AmiVector& dxB, AmiVector& xQB, int which + ) const; /** * @brief Access state solution at time t * @param t time * @return x or interpolated solution dky */ - const AmiVector &getState(realtype t) const; + AmiVector const& getState(realtype t) const; /** * @brief Access derivative state solution at time t * @param t time * @return dx or interpolated solution dky */ - const AmiVector &getDerivativeState(realtype t) const; + AmiVector const& getDerivativeState(realtype t) const; /** * @brief Access state sensitivity solution at time t * @param t time * @return (interpolated) solution sx */ - const AmiVectorArray &getStateSensitivity(realtype t) const; + AmiVectorArray const& getStateSensitivity(realtype t) const; /** * @brief Access adjoint solution at time t @@ -678,7 +687,7 @@ class Solver { * @param t time * @return (interpolated) solution xB */ - const AmiVector &getAdjointState(int which, realtype t) const; + AmiVector const& getAdjointState(int which, realtype t) const; /** * @brief Access adjoint derivative solution at time t @@ -686,7 +695,7 @@ class Solver { * @param t time * @return (interpolated) solution dxB */ - const AmiVector &getAdjointDerivativeState(int which, realtype t) const; + AmiVector const& getAdjointDerivativeState(int which, realtype t) const; /** * @brief Access adjoint quadrature solution at time t @@ -694,14 +703,14 @@ class Solver { * @param t time * @return (interpolated) solution xQB */ - const AmiVector &getAdjointQuadrature(int which, realtype t) const; + AmiVector const& getAdjointQuadrature(int which, realtype t) const; /** * @brief Access quadrature solution at time t * @param t time * @return (interpolated) solution xQ */ - const AmiVector &getQuadrature(realtype t) const; + AmiVector const& getQuadrature(realtype t) const; /** * @brief Reinitializes the states in the solver after an event occurrence @@ -710,8 +719,9 @@ class Solver { * @param yy0 initial state variables * @param yp0 initial derivative state variables (DAE only) */ - virtual void reInit(realtype t0, const AmiVector &yy0, - const AmiVector &yp0) const = 0; + virtual void + reInit(realtype t0, AmiVector const& yy0, AmiVector const& yp0) const + = 0; /** * @brief Reinitializes the state sensitivities in the solver after an @@ -720,8 +730,9 @@ class Solver { * @param yyS0 new state sensitivity * @param ypS0 new derivative state sensitivities (DAE only) */ - virtual void sensReInit(const AmiVectorArray &yyS0, - const AmiVectorArray &ypS0) const = 0; + virtual void + sensReInit(AmiVectorArray const& yyS0, AmiVectorArray const& ypS0) const + = 0; /** * @brief Switches off computation of state sensitivities without @@ -737,8 +748,10 @@ class Solver { * @param yyB0 new adjoint state * @param ypB0 new adjoint derivative state */ - virtual void reInitB(int which, realtype tB0, const AmiVector &yyB0, - const AmiVector &ypB0) const = 0; + virtual void reInitB( + int which, realtype tB0, AmiVector const& yyB0, AmiVector const& ypB0 + ) const + = 0; /** * @brief Reinitialize the adjoint states after an event occurrence @@ -746,7 +759,7 @@ class Solver { * @param which identifier of the backwards problem * @param yQB0 new adjoint quadrature state */ - virtual void quadReInitB(int which, const AmiVector &yQB0) const = 0; + virtual void quadReInitB(int which, AmiVector const& yQB0) const = 0; /** * @brief current solver timepoint @@ -789,8 +802,9 @@ class Solver { * @return flag */ bool computingFSA() const { - return getSensitivityOrder() >= SensitivityOrder::first && - getSensitivityMethod() == SensitivityMethod::forward && nplist() > 0; + return getSensitivityOrder() >= SensitivityOrder::first + && getSensitivityMethod() == SensitivityMethod::forward + && nplist() > 0; } /** @@ -798,8 +812,9 @@ class Solver { * @return flag */ bool computingASA() const { - return getSensitivityOrder() >= SensitivityOrder::first && - getSensitivityMethod() == SensitivityMethod::adjoint && nplist() > 0; + return getSensitivityOrder() >= SensitivityOrder::first + && getSensitivityMethod() == SensitivityMethod::adjoint + && nplist() > 0; } /** @@ -808,12 +823,14 @@ class Solver { void resetDiagnosis() const; /** - * @brief Stores diagnosis information from solver memory block for forward problem + * @brief Stores diagnosis information from solver memory block for forward + * problem */ void storeDiagnosis() const; /** - * @brief Stores diagnosis information from solver memory block for backward problem + * @brief Stores diagnosis information from solver memory block for backward + * problem * * @param which identifier of the backwards problem */ @@ -823,49 +840,37 @@ class Solver { * @brief Accessor ns * @return ns */ - std::vector const& getNumSteps() const { - return ns_; - } + std::vector const& getNumSteps() const { return ns_; } /** * @brief Accessor nsB * @return nsB */ - std::vector const& getNumStepsB() const { - return nsB_; - } + std::vector const& getNumStepsB() const { return nsB_; } /** * @brief Accessor nrhs * @return nrhs */ - std::vector const& getNumRhsEvals() const { - return nrhs_; - } + std::vector const& getNumRhsEvals() const { return nrhs_; } /** * @brief Accessor nrhsB * @return nrhsB */ - std::vector const& getNumRhsEvalsB() const { - return nrhsB_; - } + std::vector const& getNumRhsEvalsB() const { return nrhsB_; } /** * @brief Accessor netf * @return netf */ - std::vector const& getNumErrTestFails() const { - return netf_; - } + std::vector const& getNumErrTestFails() const { return netf_; } /** * @brief Accessor netfB * @return netfB */ - std::vector const& getNumErrTestFailsB() const { - return netfB_; - } + std::vector const& getNumErrTestFailsB() const { return netfB_; } /** * @brief Accessor nnlscf @@ -887,38 +892,44 @@ class Solver { * @brief Accessor order * @return order */ - std::vector const& getLastOrder() const { - return order_; - } + std::vector const& getLastOrder() const { return order_; } /** - * @brief Returns how convergence checks for steadystate computation are performed. If activated, - * convergence checks are limited to every 25 steps in the simulation solver to limit performance impact. - * @return boolean flag indicating newton step (true) or the right hand side (false) + * @brief Returns how convergence checks for steadystate computation are + * performed. If activated, convergence checks are limited to every 25 steps + * in the simulation solver to limit performance impact. + * @return boolean flag indicating newton step (true) or the right hand side + * (false) */ bool getNewtonStepSteadyStateCheck() const { return newton_step_steadystate_conv_; } /** - * @brief Returns how convergence checks for steadystate computation are performed. - * @return boolean flag indicating state and sensitivity equations (true) or only state variables (false). + * @brief Returns how convergence checks for steadystate computation are + * performed. + * @return boolean flag indicating state and sensitivity equations (true) or + * only state variables (false). */ bool getSensiSteadyStateCheck() const { return check_sensi_steadystate_conv_; } /** - * @brief Sets how convergence checks for steadystate computation are performed. - * @param flag boolean flag to pick newton step (true) or the right hand side (false, default) + * @brief Sets how convergence checks for steadystate computation are + * performed. + * @param flag boolean flag to pick newton step (true) or the right hand + * side (false, default) */ void setNewtonStepSteadyStateCheck(bool flag) { newton_step_steadystate_conv_ = flag; } /** - * @brief Sets for which variables convergence checks for steadystate computation are performed. - * @param flag boolean flag to pick state and sensitivity equations (true, default) or only state variables (false). + * @brief Sets for which variables convergence checks for steadystate + * computation are performed. + * @param flag boolean flag to pick state and sensitivity equations (true, + * default) or only state variables (false). */ void setSensiSteadyStateCheck(bool flag) { check_sensi_steadystate_conv_ = flag; @@ -931,8 +942,9 @@ class Solver { * @param version Version number */ template - friend void boost::serialization::serialize(Archive &ar, Solver &s, - unsigned int version); + friend void boost::serialization::serialize( + Archive& ar, Solver& s, unsigned int version + ); /** * @brief Check equality of data members excluding solver memory @@ -940,10 +952,10 @@ class Solver { * @param b * @return */ - friend bool operator==(const Solver &a, const Solver &b); + friend bool operator==(Solver const& a, Solver const& b); /** logger */ - Logger *logger = nullptr; + Logger* logger = nullptr; protected: /** @@ -972,7 +984,7 @@ class Solver { * checkpoints * @return status flag indicating success of execution */ - virtual int solveF(realtype tout, int itask, int *ncheckPtr) const = 0; + virtual int solveF(realtype tout, int itask, int* ncheckPtr) const = 0; /** * @brief reInitPostProcessF postprocessing of the solver memory after a @@ -1014,7 +1026,7 @@ class Solver { * * @param t timepoint for quadrature extraction */ - virtual void getQuad(realtype &t) const = 0; + virtual void getQuad(realtype& t) const = 0; /** * @brief Initializes the states at the specified initial timepoint @@ -1023,8 +1035,9 @@ class Solver { * @param x0 initial states * @param dx0 initial derivative states */ - virtual void init(realtype t0, const AmiVector &x0, - const AmiVector &dx0) const = 0; + virtual void + init(realtype t0, AmiVector const& x0, AmiVector const& dx0) const + = 0; /** * @brief Initializes the states at the specified initial timepoint @@ -1033,16 +1046,19 @@ class Solver { * @param x0 initial states * @param dx0 initial derivative states */ - virtual void initSteadystate(realtype t0, const AmiVector &x0, - const AmiVector &dx0) const = 0; + virtual void initSteadystate( + realtype t0, AmiVector const& x0, AmiVector const& dx0 + ) const + = 0; /** * @brief Initializes the forward sensitivities * @param sx0 initial states sensitivities * @param sdx0 initial derivative states sensitivities */ - virtual void sensInit1(const AmiVectorArray &sx0, - const AmiVectorArray &sdx0) const = 0; + virtual void + sensInit1(AmiVectorArray const& sx0, AmiVectorArray const& sdx0) const + = 0; /** * @brief Initialize the adjoint states at the specified final timepoint @@ -1052,8 +1068,10 @@ class Solver { * @param xB0 initial adjoint state * @param dxB0 initial adjoint derivative state */ - virtual void binit(int which, realtype tf, const AmiVector &xB0, - const AmiVector &dxB0) const = 0; + virtual void binit( + int which, realtype tf, AmiVector const& xB0, AmiVector const& dxB0 + ) const + = 0; /** * @brief Initialize the quadrature states at the specified final timepoint @@ -1061,7 +1079,7 @@ class Solver { * @param which identifier of the backwards problem * @param xQB0 initial adjoint quadrature state */ - virtual void qbinit(int which, const AmiVector &xQB0) const = 0; + virtual void qbinit(int which, AmiVector const& xQB0) const = 0; /** * @brief Initializes the rootfinding for events @@ -1074,7 +1092,7 @@ class Solver { * @brief Initalize non-linear solver for sensitivities * @param model Model instance */ - void initializeNonLinearSolverSens(const Model *model) const; + void initializeNonLinearSolverSens(Model const* model) const; /** * @brief Set the dense Jacobian function @@ -1151,7 +1169,7 @@ class Solver { * @param rtol relative tolerances * @param atol array of absolute tolerances for every sensitivity variable */ - virtual void setSensSStolerances(double rtol, const double *atol) const = 0; + virtual void setSensSStolerances(double rtol, double const* atol) const = 0; /** * SetSensErrCon specifies whether error control is also enforced for @@ -1202,7 +1220,8 @@ class Solver { * problem * * @param mxsteps number of steps - * @note in contrast to the SUNDIALS method, this sets the overall maximum, not the maximum between output times. + * @note in contrast to the SUNDIALS method, this sets the overall maximum, + * not the maximum between output times. */ virtual void setMaxNumSteps(long int mxsteps) const = 0; @@ -1212,7 +1231,8 @@ class Solver { * * @param which identifier of the backwards problem * @param mxstepsB number of steps - * @note in contrast to the SUNDIALS method, this sets the overall maximum, not the maximum between output times. + * @note in contrast to the SUNDIALS method, this sets the overall maximum, + * not the maximum between output times. */ virtual void setMaxNumStepsB(int which, long int mxstepsB) const = 0; @@ -1240,7 +1260,7 @@ class Solver { * * @param model model specification */ - virtual void setId(const Model *model) const = 0; + virtual void setId(Model const* model) const = 0; /** * @brief deactivates error control for algebraic components (DAE only) @@ -1257,8 +1277,10 @@ class Solver { * @param pbar parameter scaling constants * @param plist parameter index list */ - virtual void setSensParams(const realtype *p, const realtype *pbar, - const int *plist) const = 0; + virtual void setSensParams( + realtype const* p, realtype const* pbar, int const* plist + ) const + = 0; /** * @brief interpolates the (derivative of the) solution at the requested @@ -1316,7 +1338,7 @@ class Solver { * @brief initializes the quadratures * @param xQ0 vector with initial values for xQ */ - virtual void quadInit(const AmiVector &xQ0) const = 0; + virtual void quadInit(AmiVector const& xQ0) const = 0; /** * @brief Specifies solver method and initializes solver memory for the @@ -1324,7 +1346,7 @@ class Solver { * * @param which identifier of the backwards problem */ - virtual void allocateSolverB(int *which) const = 0; + virtual void allocateSolverB(int* which) const = 0; /** * @brief sets relative and absolute tolerances for the backward @@ -1334,8 +1356,9 @@ class Solver { * @param relTolB relative tolerances * @param absTolB absolute tolerances */ - virtual void setSStolerancesB(int which, realtype relTolB, - realtype absTolB) const = 0; + virtual void + setSStolerancesB(int which, realtype relTolB, realtype absTolB) const + = 0; /** * @brief sets relative and absolute tolerances for the quadrature @@ -1345,8 +1368,9 @@ class Solver { * @param reltolQB relative tolerances * @param abstolQB absolute tolerances */ - virtual void quadSStolerancesB(int which, realtype reltolQB, - realtype abstolQB) const = 0; + virtual void + quadSStolerancesB(int which, realtype reltolQB, realtype abstolQB) const + = 0; /** * @brief sets relative and absolute tolerances for the quadrature problem @@ -1354,8 +1378,8 @@ class Solver { * @param reltolQB relative tolerances * @param abstolQB absolute tolerances */ - virtual void quadSStolerances(realtype reltolQB, - realtype abstolQB) const = 0; + virtual void quadSStolerances(realtype reltolQB, realtype abstolQB) const + = 0; /** * @brief reports the number of solver steps @@ -1364,7 +1388,7 @@ class Solver { * forward or backward problem) * @param numsteps output array */ - virtual void getNumSteps(const void *ami_mem, long int *numsteps) const = 0; + virtual void getNumSteps(void const* ami_mem, long int* numsteps) const = 0; /** * @brief reports the number of right hand evaluations @@ -1373,8 +1397,9 @@ class Solver { * forward or backward problem) * @param numrhsevals output array */ - virtual void getNumRhsEvals(const void *ami_mem, - long int *numrhsevals) const = 0; + virtual void + getNumRhsEvals(void const* ami_mem, long int* numrhsevals) const + = 0; /** * @brief reports the number of local error test failures @@ -1383,8 +1408,9 @@ class Solver { * forward or backward problem) * @param numerrtestfails output array */ - virtual void getNumErrTestFails(const void *ami_mem, - long int *numerrtestfails) const = 0; + virtual void + getNumErrTestFails(void const* ami_mem, long int* numerrtestfails) const + = 0; /** * @brief reports the number of nonlinear convergence failures @@ -1393,9 +1419,10 @@ class Solver { * forward or backward problem) * @param numnonlinsolvconvfails output array */ - virtual void - getNumNonlinSolvConvFails(const void *ami_mem, - long int *numnonlinsolvconvfails) const = 0; + virtual void getNumNonlinSolvConvFails( + void const* ami_mem, long int* numnonlinsolvconvfails + ) const + = 0; /** * @brief Reports the order of the integration method during the @@ -1405,14 +1432,14 @@ class Solver { * forward or backward problem) * @param order output array */ - virtual void getLastOrder(const void *ami_mem, int *order) const = 0; + virtual void getLastOrder(void const* ami_mem, int* order) const = 0; /** * @brief Initializes and sets the linear solver for the forward problem * * @param model pointer to the model object */ - void initializeLinearSolver(const Model *model) const; + void initializeLinearSolver(Model const* model) const; /** * @brief Sets the non-linear solver @@ -1453,7 +1480,7 @@ class Solver { * @param which index of the backward problem */ - void initializeLinearSolverB(const Model *model, int which) const; + void initializeLinearSolverB(Model const* model, int which) const; /** * @brief Initializes the non-linear solver for the backward problem @@ -1466,7 +1493,7 @@ class Solver { * * @return user data model */ - virtual const Model *getModel() const = 0; + virtual Model const* getModel() const = 0; /** * @brief checks whether memory for the forward problem has been allocated @@ -1538,7 +1565,7 @@ class Solver { * @return A (void *) pointer to the CVODES memory allocated for the * backward problem. */ - virtual void *getAdjBmem(void *ami_mem, int which) const = 0; + virtual void* getAdjBmem(void* ami_mem, int which) const = 0; /** * @brief updates solver tolerances according to the currently specified @@ -1581,10 +1608,10 @@ class Solver { void applySensitivityTolerances() const; /** pointer to solver memory block */ - mutable std::unique_ptr> solver_memory_; + mutable std::unique_ptr> solver_memory_; /** pointer to solver memory block */ - mutable std::vector>> + mutable std::vector>> solver_memory_B_; /** Sundials user_data */ @@ -1592,27 +1619,27 @@ class Solver { /** internal sensitivity method flag used to select the sensitivity solution * method. Only applies for Forward Sensitivities. */ - InternalSensitivityMethod ism_ {InternalSensitivityMethod::simultaneous}; + InternalSensitivityMethod ism_{InternalSensitivityMethod::simultaneous}; /** specifies the linear multistep method. */ - LinearMultistepMethod lmm_ {LinearMultistepMethod::BDF}; + LinearMultistepMethod lmm_{LinearMultistepMethod::BDF}; /** * specifies the type of nonlinear solver iteration */ - NonlinearSolverIteration iter_ {NonlinearSolverIteration::newton}; + NonlinearSolverIteration iter_{NonlinearSolverIteration::newton}; /** interpolation type for the forward problem solution which * is then used for the backwards problem. */ - InterpolationType interp_type_ {InterpolationType::polynomial}; + InterpolationType interp_type_{InterpolationType::polynomial}; /** maximum number of allowed integration steps */ - long int maxsteps_ {10000}; + long int maxsteps_{10000}; /** Maximum CPU-time for integration in seconds */ - std::chrono::duration> maxtime_ {0}; + std::chrono::duration> maxtime_{0}; /** Time at which solver timer was started */ mutable CpuTimer simulation_timer_; @@ -1633,10 +1660,10 @@ class Solver { mutable std::unique_ptr non_linear_solver_sens_; /** flag indicating whether the forward solver has been called */ - mutable bool solver_was_called_F_ {false}; + mutable bool solver_was_called_F_{false}; /** flag indicating whether the backward solver has been called */ - mutable bool solver_was_called_B_ {false}; + mutable bool solver_was_called_B_{false}; /** * @brief sets that memory for the forward problem has been allocated @@ -1680,51 +1707,51 @@ class Solver { * @param sensi_meth new value for sensi_meth[_preeq] * @param preequilibration flag indicating preequilibration or simulation */ - void checkSensitivityMethod(const SensitivityMethod sensi_meth, - bool preequilibration) const; + void checkSensitivityMethod( + const SensitivityMethod sensi_meth, bool preequilibration + ) const; /** state (dimension: nx_solver) */ - mutable AmiVector x_ {0}; + mutable AmiVector x_{0}; /** state interface variable (dimension: nx_solver) */ - mutable AmiVector dky_ {0}; + mutable AmiVector dky_{0}; /** state derivative dummy (dimension: nx_solver) */ - mutable AmiVector dx_ {0}; + mutable AmiVector dx_{0}; /** state sensitivities interface variable (dimension: nx_solver x nplist) */ - mutable AmiVectorArray sx_ {0, 0}; + mutable AmiVectorArray sx_{0, 0}; /** state derivative sensitivities dummy (dimension: nx_solver x nplist) */ - mutable AmiVectorArray sdx_ {0, 0}; + mutable AmiVectorArray sdx_{0, 0}; /** adjoint state interface variable (dimension: nx_solver) */ - mutable AmiVector xB_ {0}; + mutable AmiVector xB_{0}; /** adjoint derivative dummy variable (dimension: nx_solver) */ - mutable AmiVector dxB_ {0}; + mutable AmiVector dxB_{0}; /** adjoint quadrature interface variable (dimension: nJ x nplist) */ - mutable AmiVector xQB_ {0}; + mutable AmiVector xQB_{0}; /** forward quadrature interface variable (dimension: nx_solver) */ - mutable AmiVector xQ_ {0}; + mutable AmiVector xQ_{0}; /** integration time of the forward problem */ - mutable realtype t_ {std::nan("")}; + mutable realtype t_{std::nan("")}; /** flag to force reInitPostProcessF before next call to solve */ - mutable bool force_reinit_postprocess_F_ {false}; + mutable bool force_reinit_postprocess_F_{false}; /** flag to force reInitPostProcessB before next call to solveB */ - mutable bool force_reinit_postprocess_B_ {false}; + mutable bool force_reinit_postprocess_B_{false}; /** flag indicating whether sensInit1 was called */ - mutable bool sens_initialized_ {false}; + mutable bool sens_initialized_{false}; private: - /** * @brief applies total number of steps for next solver call */ @@ -1735,106 +1762,106 @@ class Solver { */ void apply_max_num_steps_B() const; - /** method for sensitivity computation */ - SensitivityMethod sensi_meth_ {SensitivityMethod::forward}; + SensitivityMethod sensi_meth_{SensitivityMethod::forward}; /** method for sensitivity computation in preequilibration */ - SensitivityMethod sensi_meth_preeq_ {SensitivityMethod::forward}; + SensitivityMethod sensi_meth_preeq_{SensitivityMethod::forward}; /** flag controlling stability limit detection */ - booleantype stldet_ {true}; + booleantype stldet_{true}; /** state ordering */ - int ordering_ {static_cast(SUNLinSolKLU::StateOrdering::AMD)}; + int ordering_{static_cast(SUNLinSolKLU::StateOrdering::AMD)}; /** maximum number of allowed Newton steps for steady state computation */ - long int newton_maxsteps_ {0L}; + long int newton_maxsteps_{0L}; /** maximum number of allowed linear steps per Newton step for steady state * computation */ - long int newton_maxlinsteps_ {0L}; + long int newton_maxlinsteps_{0L}; /** Damping factor state used int the Newton method */ - NewtonDampingFactorMode newton_damping_factor_mode_ - {NewtonDampingFactorMode::on}; + NewtonDampingFactorMode newton_damping_factor_mode_{ + NewtonDampingFactorMode::on}; /** Lower bound of the damping factor. */ - realtype newton_damping_factor_lower_bound_ {1e-8}; + realtype newton_damping_factor_lower_bound_{1e-8}; /** linear solver specification */ - LinearSolver linsol_ {LinearSolver::KLU}; + LinearSolver linsol_{LinearSolver::KLU}; /** absolute tolerances for integration */ - realtype atol_ {1e-16}; + realtype atol_{1e-16}; /** relative tolerances for integration */ - realtype rtol_ {1e-8}; + realtype rtol_{1e-8}; /** absolute tolerances for forward sensitivity integration */ - realtype atol_fsa_ {NAN}; + realtype atol_fsa_{NAN}; /** relative tolerances for forward sensitivity integration */ - realtype rtol_fsa_ {NAN}; + realtype rtol_fsa_{NAN}; /** absolute tolerances for adjoint sensitivity integration */ - realtype atolB_ {NAN}; + realtype atolB_{NAN}; /** relative tolerances for adjoint sensitivity integration */ - realtype rtolB_ {NAN}; + realtype rtolB_{NAN}; /** absolute tolerances for backward quadratures */ - realtype quad_atol_ {1e-12}; + realtype quad_atol_{1e-12}; /** relative tolerances for backward quadratures */ - realtype quad_rtol_ {1e-8}; + realtype quad_rtol_{1e-8}; /** steady state simulation tolerance factor */ - realtype ss_tol_factor_ {1e2}; + realtype ss_tol_factor_{1e2}; /** absolute tolerances for steadystate computation */ - realtype ss_atol_ {NAN}; + realtype ss_atol_{NAN}; /** relative tolerances for steadystate computation */ - realtype ss_rtol_ {NAN}; + realtype ss_rtol_{NAN}; /** steady state sensitivity simulation tolerance factor */ - realtype ss_tol_sensi_factor_ {1e2}; + realtype ss_tol_sensi_factor_{1e2}; /** absolute tolerances for steadystate sensitivity computation */ - realtype ss_atol_sensi_ {NAN}; + realtype ss_atol_sensi_{NAN}; /** relative tolerances for steadystate sensitivity computation */ - realtype ss_rtol_sensi_ {NAN}; + realtype ss_rtol_sensi_{NAN}; - RDataReporting rdata_mode_ {RDataReporting::full}; + RDataReporting rdata_mode_{RDataReporting::full}; /** whether newton step should be used for convergence steps */ - bool newton_step_steadystate_conv_ {false}; + bool newton_step_steadystate_conv_{false}; - /** whether sensitivities should be checked for convergence to steadystate */ - bool check_sensi_steadystate_conv_ {true}; + /** whether sensitivities should be checked for convergence to steadystate + */ + bool check_sensi_steadystate_conv_{true}; /** CPU time, forward solve */ - mutable realtype cpu_time_ {0.0}; + mutable realtype cpu_time_{0.0}; /** CPU time, backward solve */ - mutable realtype cpu_timeB_ {0.0}; + mutable realtype cpu_timeB_{0.0}; /** maximum number of allowed integration steps for backward problem */ - long int maxstepsB_ {0L}; + long int maxstepsB_{0L}; /** flag indicating whether sensitivities are supposed to be computed */ - SensitivityOrder sensi_ {SensitivityOrder::none}; + SensitivityOrder sensi_{SensitivityOrder::none}; /** flag indicating whether init was called */ - mutable bool initialized_ {false}; + mutable bool initialized_{false}; /** flag indicating whether adjInit was called */ - mutable bool adj_initialized_ {false}; + mutable bool adj_initialized_{false}; /** flag indicating whether (forward) quadInit was called */ - mutable bool quad_initialized_ {false}; + mutable bool quad_initialized_{false}; /** vector of flags indicating whether binit was called for respective which */ @@ -1845,7 +1872,7 @@ class Solver { mutable std::vector initializedQB_{false}; /** number of checkpoints in the forward problem */ - mutable int ncheckPtr_ {0}; + mutable int ncheckPtr_{0}; /** number of integration steps forward problem (dimension: nt) */ mutable std::vector ns_; @@ -1856,7 +1883,8 @@ class Solver { /** number of right hand side evaluations forward problem (dimension: nt) */ mutable std::vector nrhs_; - /** number of right hand side evaluations backward problem (dimension: nt) */ + /** number of right hand side evaluations backward problem (dimension: nt) + */ mutable std::vector nrhsB_; /** number of error test failures forward problem (dimension: nt) */ @@ -1879,7 +1907,7 @@ class Solver { mutable std::vector order_; }; -bool operator==(const Solver &a, const Solver &b); +bool operator==(Solver const& a, Solver const& b); /** * @brief Extracts diagnosis information from solver memory block and @@ -1891,8 +1919,10 @@ bool operator==(const Solver &a, const Solver &b); * @param msg error message * @param eh_data amici::Solver as void* */ -void wrapErrHandlerFn(int error_code, const char *module, const char *function, - char *msg, void *eh_data); +void wrapErrHandlerFn( + int error_code, char const* module, char const* function, char* msg, + void* eh_data +); } // namespace amici diff --git a/include/amici/solver_cvodes.h b/include/amici/solver_cvodes.h index 1f457418d5..d6d1dcea24 100644 --- a/include/amici/solver_cvodes.h +++ b/include/amici/solver_cvodes.h @@ -18,9 +18,9 @@ class CVodeSolver; namespace boost { namespace serialization { template -void serialize(Archive &ar, amici::CVodeSolver &s, unsigned int version); +void serialize(Archive& ar, amici::CVodeSolver& s, unsigned int version); } -} // namespace boost::serialization +} // namespace boost namespace amici { @@ -38,25 +38,25 @@ class CVodeSolver : public Solver { * @brief Clone this instance * @return The clone */ - Solver *clone() const override; + Solver* clone() const override; - void reInit(realtype t0, const AmiVector &yy0, - const AmiVector &yp0) const override; + void reInit(realtype t0, AmiVector const& yy0, AmiVector const& yp0) + const override; - void sensReInit(const AmiVectorArray &yyS0, - const AmiVectorArray &ypS0) const override; + void sensReInit(AmiVectorArray const& yyS0, AmiVectorArray const& ypS0) + const override; void sensToggleOff() const override; - void reInitB(int which, realtype tB0, - const AmiVector &yyB0, const AmiVector &ypB0) const override; + void reInitB( + int which, realtype tB0, AmiVector const& yyB0, AmiVector const& ypB0 + ) const override; - void quadReInitB(int which, const AmiVector &yQB0) const override; + void quadReInitB(int which, AmiVector const& yQB0) const override; int solve(realtype tout, int itask) const override; - int solveF(realtype tout, int itask, - int *ncheckPtr) const override; + int solveF(realtype tout, int itask, int* ncheckPtr) const override; void solveB(realtype tBout, int itaskB) const override; @@ -64,18 +64,17 @@ class CVodeSolver : public Solver { void getSensDky(realtype t, int k) const override; - void getQuadDkyB(realtype t, int k, - int which) const override; + void getQuadDkyB(realtype t, int k, int which) const override; void getDkyB(realtype t, int k, int which) const override; - void getRootInfo(int *rootsfound) const override; + void getRootInfo(int* rootsfound) const override; void setStopTime(realtype tstop) const override; void turnOffRootFinding() const override; - const Model *getModel() const override; + Model const* getModel() const override; #if !defined(EXHALE_DOXYGEN_SHOULD_SKIP_THIS) using Solver::setLinearSolver; @@ -93,7 +92,6 @@ class CVodeSolver : public Solver { void setNonLinearSolverB(int which) const override; protected: - void calcIC(realtype tout1) const override; void calcICB(int which, realtype tout1) const override; @@ -104,7 +102,7 @@ class CVodeSolver : public Solver { void getQuadB(int which) const override; - void getQuad(realtype &t) const override; + void getQuad(realtype& t) const override; void getQuadDky(realtype t, int k) const override; @@ -119,15 +117,15 @@ class CVodeSolver : public Solver { * @param yout new state vector * @param tout anticipated next integration timepoint. */ - void reInitPostProcess(void *cv_mem, realtype *t, AmiVector *yout, - realtype tout) const; + void reInitPostProcess( + void* cv_mem, realtype* t, AmiVector* yout, realtype tout + ) const; void allocateSolver() const override; void setSStolerances(double rtol, double atol) const override; - void setSensSStolerances(double rtol, - const double *atol) const override; + void setSensSStolerances(double rtol, double const* atol) const override; void setSensErrCon(bool error_corr) const override; @@ -147,31 +145,33 @@ class CVodeSolver : public Solver { void setStabLimDetB(int which, int stldet) const override; - void setId(const Model *model) const override; + void setId(Model const* model) const override; void setSuppressAlg(bool flag) const override; /** - * @brief resetState reset the CVODES solver to restart integration after a rhs discontinuity. + * @brief resetState reset the CVODES solver to restart integration after a + * rhs discontinuity. * @param cv_mem pointer to CVODES solver memory object * @param y0 new state vector */ - void resetState(void *cv_mem, const_N_Vector y0) const; + void resetState(void* cv_mem, const_N_Vector y0) const; - void setSensParams(const realtype *p, const realtype *pbar, - const int *plist) const override; + void setSensParams( + realtype const* p, realtype const* pbar, int const* plist + ) const override; void adjInit() const override; - void quadInit(const AmiVector &xQ0) const override; + void quadInit(AmiVector const& xQ0) const override; - void allocateSolverB(int *which) const override; + void allocateSolverB(int* which) const override; - void setSStolerancesB(int which, realtype relTolB, - realtype absTolB) const override; + void setSStolerancesB(int which, realtype relTolB, realtype absTolB) + const override; - void quadSStolerancesB(int which, realtype reltolQB, - realtype abstolQB) const override; + void quadSStolerancesB(int which, realtype reltolQB, realtype abstolQB) + const override; void quadSStolerances(realtype reltolQ, realtype abstolQ) const override; @@ -181,21 +181,21 @@ class CVodeSolver : public Solver { void diagB(int which) const override; - void getNumSteps(const void *ami_mem, long int *numsteps) const override; + void getNumSteps(void const* ami_mem, long int* numsteps) const override; - void getNumRhsEvals(const void *ami_mem, - long int *numrhsevals) const override; + void + getNumRhsEvals(void const* ami_mem, long int* numrhsevals) const override; - void getNumErrTestFails(const void *ami_mem, - long int *numerrtestfails) const override; + void getNumErrTestFails(void const* ami_mem, long int* numerrtestfails) + const override; - void - getNumNonlinSolvConvFails(const void *ami_mem, - long int *numnonlinsolvconvfails) const override; + void getNumNonlinSolvConvFails( + void const* ami_mem, long int* numnonlinsolvconvfails + ) const override; - void getLastOrder(const void *ami_ami_mem, int *order) const override; + void getLastOrder(void const* ami_ami_mem, int* order) const override; - void *getAdjBmem(void *ami_mem, int which) const override; + void* getAdjBmem(void* ami_mem, int which) const override; /** * @brief Serialize amici::CVodeSolver to boost archive @@ -203,8 +203,8 @@ class CVodeSolver : public Solver { * @param s Solver instance to serialize */ template - friend void boost::serialization::serialize(Archive &ar, CVodeSolver &s, - unsigned int /*version*/); + friend void boost::serialization:: + serialize(Archive& ar, CVodeSolver& s, unsigned int /*version*/); /** * @brief Equality operator @@ -212,21 +212,23 @@ class CVodeSolver : public Solver { * @param b * @return Whether a and b are equal */ - friend bool operator==(const CVodeSolver &a, const CVodeSolver &b); + friend bool operator==(CVodeSolver const& a, CVodeSolver const& b); - void init(realtype t0, const AmiVector &x0, - const AmiVector &dx0) const override; + void + init(realtype t0, AmiVector const& x0, AmiVector const& dx0) const override; - void initSteadystate(const realtype t0, const AmiVector &x0, - const AmiVector &dx0) const override; + void initSteadystate( + const realtype t0, AmiVector const& x0, AmiVector const& dx0 + ) const override; - void sensInit1(const AmiVectorArray &sx0, const AmiVectorArray &sdx0) - const override; + void sensInit1(AmiVectorArray const& sx0, AmiVectorArray const& sdx0) + const override; - void binit(int which, realtype tf, const AmiVector &xB0, - const AmiVector &dxB0) const override; + void binit( + int which, realtype tf, AmiVector const& xB0, AmiVector const& dxB0 + ) const override; - void qbinit(int which, const AmiVector &xQB0) const override; + void qbinit(int which, AmiVector const& xQB0) const override; void rootInit(int ne) const override; diff --git a/include/amici/solver_idas.h b/include/amici/solver_idas.h index 331a1a6206..0dba1a9504 100644 --- a/include/amici/solver_idas.h +++ b/include/amici/solver_idas.h @@ -16,9 +16,9 @@ class IDASolver; namespace boost { namespace serialization { template -void serialize(Archive &ar, amici::IDASolver &s, unsigned int version); +void serialize(Archive& ar, amici::IDASolver& s, unsigned int version); } -} // namespace boost::serialization +} // namespace boost namespace amici { @@ -35,38 +35,38 @@ class IDASolver : public Solver { * @brief Clone this instance * @return The clone */ - Solver *clone() const override; + Solver* clone() const override; void reInitPostProcessF(realtype tnext) const override; void reInitPostProcessB(realtype tnext) const override; - void reInit(realtype t0, const AmiVector &yy0, - const AmiVector &yp0) const override; + void reInit(realtype t0, AmiVector const& yy0, AmiVector const& yp0) + const override; - void sensReInit(const AmiVectorArray &yyS0, - const AmiVectorArray &ypS0) const override; + void sensReInit(AmiVectorArray const& yyS0, AmiVectorArray const& ypS0) + const override; void sensToggleOff() const override; - void reInitB(int which, realtype tB0, - const AmiVector &yyB0, const AmiVector &ypB0) const override; + void reInitB( + int which, realtype tB0, AmiVector const& yyB0, AmiVector const& ypB0 + ) const override; - void quadReInitB(int which, const AmiVector &yQB0) const override; + void quadReInitB(int which, AmiVector const& yQB0) const override; - void quadSStolerancesB(int which, realtype reltolQB, - realtype abstolQB) const override; + void quadSStolerancesB(int which, realtype reltolQB, realtype abstolQB) + const override; void quadSStolerances(realtype reltolQ, realtype abstolQ) const override; int solve(realtype tout, int itask) const override; - int solveF(realtype tout, int itask, - int *ncheckPtr) const override; + int solveF(realtype tout, int itask, int* ncheckPtr) const override; void solveB(realtype tBout, int itaskB) const override; - void getRootInfo(int *rootsfound) const override; + void getRootInfo(int* rootsfound) const override; void getDky(realtype t, int k) const override; @@ -82,7 +82,7 @@ class IDASolver : public Solver { void getQuadDkyB(realtype t, int k, int which) const override; - void getQuad(realtype &t) const override; + void getQuad(realtype& t) const override; void getQuadDky(realtype t, int k) const override; @@ -94,7 +94,7 @@ class IDASolver : public Solver { void turnOffRootFinding() const override; - const Model *getModel() const override; + Model const* getModel() const override; void setLinearSolver() const override; @@ -115,16 +115,17 @@ class IDASolver : public Solver { * @param ypout new state derivative vector * @param tout anticipated next integration timepoint. */ - void reInitPostProcess(void *ida_mem, realtype *t, AmiVector *yout, - AmiVector *ypout, realtype tout) const; + void reInitPostProcess( + void* ida_mem, realtype* t, AmiVector* yout, AmiVector* ypout, + realtype tout + ) const; void allocateSolver() const override; - void setSStolerances(realtype rtol, - realtype atol) const override; + void setSStolerances(realtype rtol, realtype atol) const override; - void setSensSStolerances(realtype rtol, - const realtype *atol) const override; + void + setSensSStolerances(realtype rtol, realtype const* atol) const override; void setSensErrCon(bool error_corr) const override; @@ -144,66 +145,70 @@ class IDASolver : public Solver { void setStabLimDetB(int which, int stldet) const override; - void setId(const Model *model) const override; + void setId(Model const* model) const override; void setSuppressAlg(bool flag) const override; /** - * @brief resetState reset the IDAS solver to restart integration after a rhs discontinuity. + * @brief resetState reset the IDAS solver to restart integration after a + * rhs discontinuity. * @param ida_mem pointer to IDAS solver memory object * @param yy0 new state vector * @param yp0 new state derivative vector */ - void resetState(void *ida_mem, const_N_Vector yy0, - const_N_Vector yp0) const; + void + resetState(void* ida_mem, const_N_Vector yy0, const_N_Vector yp0) const; - void setSensParams(const realtype *p, const realtype *pbar, - const int *plist) const override; + void setSensParams( + realtype const* p, realtype const* pbar, int const* plist + ) const override; void adjInit() const override; - void quadInit(const AmiVector &xQ0) const override; + void quadInit(AmiVector const& xQ0) const override; - void allocateSolverB(int *which) const override; + void allocateSolverB(int* which) const override; - void setMaxNumStepsB(int which, - long int mxstepsB) const override; + void setMaxNumStepsB(int which, long int mxstepsB) const override; - void setSStolerancesB(int which, realtype relTolB, - realtype absTolB) const override; + void setSStolerancesB(int which, realtype relTolB, realtype absTolB) + const override; void diag() const override; void diagB(int which) const override; - void getNumSteps(const void *ami_mem, long int *numsteps) const override; + void getNumSteps(void const* ami_mem, long int* numsteps) const override; - void getNumRhsEvals(const void *ami_mem, - long int *numrhsevals) const override; + void + getNumRhsEvals(void const* ami_mem, long int* numrhsevals) const override; - void getNumErrTestFails(const void *ami_mem, - long int *numerrtestfails) const override; + void getNumErrTestFails(void const* ami_mem, long int* numerrtestfails) + const override; - void - getNumNonlinSolvConvFails(const void *ami_mem, - long int *numnonlinsolvconvfails) const override; + void getNumNonlinSolvConvFails( + void const* ami_mem, long int* numnonlinsolvconvfails + ) const override; - void getLastOrder(const void *ami_mem, int *order) const override; + void getLastOrder(void const* ami_mem, int* order) const override; - void *getAdjBmem(void *ami_mem, int which) const override; + void* getAdjBmem(void* ami_mem, int which) const override; - void init(realtype t0, const AmiVector &x0, - const AmiVector &dx0) const override; + void + init(realtype t0, AmiVector const& x0, AmiVector const& dx0) const override; - void initSteadystate(const realtype t0, const AmiVector &x0, - const AmiVector &dx0) const override; + void initSteadystate( + const realtype t0, AmiVector const& x0, AmiVector const& dx0 + ) const override; - void sensInit1(const AmiVectorArray &sx0, const AmiVectorArray &sdx0) const override; + void sensInit1(AmiVectorArray const& sx0, AmiVectorArray const& sdx0) + const override; - void binit(int which, realtype tf, - const AmiVector &xB0, const AmiVector &dxB0) const override; + void binit( + int which, realtype tf, AmiVector const& xB0, AmiVector const& dxB0 + ) const override; - void qbinit(int which, const AmiVector &xQB0) const override; + void qbinit(int which, AmiVector const& xQB0) const override; void rootInit(int ne) const override; diff --git a/include/amici/spline.h b/include/amici/spline.h index e0587ceac7..07a436e380 100644 --- a/include/amici/spline.h +++ b/include/amici/spline.h @@ -5,15 +5,19 @@ namespace amici { #ifndef EXHALE_DOXYGEN_SHOULD_SKIP_THIS -int spline(int n, int end1, int end2, double slope1, double slope2, double x[], - double y[], double b[], double c[], double d[]); +int spline( + int n, int end1, int end2, double slope1, double slope2, double x[], + double y[], double b[], double c[], double d[] +); #endif -double seval(int n, double u, double x[], double y[], double b[], double c[], - double d[]); +double seval( + int n, double u, double x[], double y[], double b[], double c[], double d[] +); -double sinteg(int n, double u, double x[], double y[], double b[], double c[], - double d[]); +double sinteg( + int n, double u, double x[], double y[], double b[], double c[], double d[] +); } // namespace amici diff --git a/include/amici/splinefunctions.h b/include/amici/splinefunctions.h new file mode 100644 index 0000000000..db4410de91 --- /dev/null +++ b/include/amici/splinefunctions.h @@ -0,0 +1,398 @@ +#ifndef AMICI_SPLINEFUNCTIONS_H +#define AMICI_SPLINEFUNCTIONS_H + +#include "amici/defines.h" + +#include + +#include + +namespace amici { + +class Model; +/** + * @brief AMICI spline base class. + * + * Instances of this class are created upon solver setup and the needed splines + * are set up (e.g., interpolation of the nodes is performed). + * Upon call to a spline function, only the evaluation of the spline polynomial + * is carried out. + */ +class AbstractSpline { + public: + /** default constructor */ + AbstractSpline() = default; + + /** + * @brief Common constructor for `AbstractSpline` instances. + * @param nodes the nodes defining the position at which the value of + * the spline is known + * (if `equidistant_spacing` is true, it must contain only the first and + * the last node; the other nodes will be automatically inserted, + * assuming they are uniformly spaced) + * @param node_values the values assumed by the spline at the nodes + * @param equidistant_spacing whether equidistant nodes are to be computed + * @param logarithmic_parametrization if true, the spline interpolation + * will occur in log-space in order to ensure positivity of the interpolant + * (which strictly speaking will no longer be a spline) + */ + AbstractSpline( + std::vector nodes, std::vector node_values, + bool equidistant_spacing, bool logarithmic_parametrization + ); + + virtual ~AbstractSpline() = default; + + /** + * @brief Compute the coefficients for all polynomial segments of this + * spline + */ + virtual void compute_coefficients() = 0; + + /** + * @brief Compute the coefficients for all polynomial segments of + * the derivatives of this spline with respect to the parameters + * @param nplist number of parameters + * @param spline_offset offset of this spline inside `dvaluesdp` + * and `dslopesdp` + * @param dvaluesdp derivatives of the spline values with respect to the + * parameters (for all splines in the model, not just this one) + * @param dslopesdp derivatives of the spline derivatives with respect + * to the parameters (for all splines in the model, not just this one) + * @remark The contents of `dvaluesdp` and `dslopesdp` may be modified + * by this function. + */ + virtual void compute_coefficients_sensi( + int nplist, int spline_offset, gsl::span dvaluesdp, + gsl::span dslopesdp + ) = 0; + + /** + * @brief Get the value of this spline at a given point + * @param t point at which the spline is to be evaluated + * @return value of the spline at `t` + */ + realtype get_value(const realtype t) const; + + /** + * @brief Get the value of this spline at a given point + * in the scale in which interpolation is carried out (e.g., log-scale) + * @param t point at which the spline is to be evaluated + * @return scaled value of the spline at `t` + */ + virtual realtype get_value_scaled(const realtype t) const = 0; + + /** + * @brief Get the value of this spline at a given node + * @param i index of the node at which the spline is to be evaluated + * @return value of the spline at the `i`-th node + */ + realtype get_node_value(int const i) const; + + /** + * @brief Get the value of this spline at a given node + * in the scale in which interpolation is carried out (e.g., log-scale) + * @param i index of the node at which the spline is to be evaluated + * @return scaled value of the spline at the `i`-th node + */ + realtype get_node_value_scaled(int const i) const; + + /** + * @brief Get the derivative of this spline with respect to a given + * parameter at a given point + * @param t point at which the sensitivity is to be evaluated + * @param ip index of the parameter + * @return sensitivity of the spline with respect to the `ip`th parameter + * at `t` + */ + realtype get_sensitivity(const realtype t, int const ip) const; + + /** + * @brief Get the derivative of this spline with respect to a given + * parameter at a given point + * @param t point at which the sensitivity is to be evaluated + * @param ip index of the parameter + * @param value value of the spline at the given time point. + * It is used e.g. when interpolation is carried out in log-space. + * If omitted it will be computed. + * @return sensitivity of the spline with respect to the `ip`th parameter + * at `t` + */ + realtype + get_sensitivity(const realtype t, int const ip, const realtype value) const; + + /** + * @brief Get the derivative of this spline with respect to a given + * parameter at a given point + * in the scale in which interpolation is carried out (e.g., log-scale) + * @param t point at which the sensitivity is to be evaluated + * @param ip index of the parameter + * @return scaled sensitivity of the spline with respect to the `ip`th + * parameter at `t` + */ + virtual realtype + get_sensitivity_scaled(const realtype t, int const ip) const + = 0; + + /** + * @brief Compute the limit value of the spline + * as the evaluation point tends to positive infinity. + */ + virtual void compute_final_value() = 0; + + /** + * @brief Compute the limit of the value of the sensitivity + * as the evaluation point tends to positive infinity. + * @param nplist number of parameters + * @param spline_offset offset of this spline inside `dspline_valuesdp` + * and `dspline_slopesdp` + * @param dspline_valuesdp derivatives of the spline values with respect to + * the parameters (for all splines in the model, not just this one) + * @param dspline_slopesdp derivatives of the spline derivatives with + * respect to the parameters (for all splines in the model, not just this + * one) + */ + virtual void compute_final_sensitivity( + int nplist, int spline_offset, gsl::span dspline_valuesdp, + gsl::span dspline_slopesdp + ) = 0; + + /** + * @brief Get the limit value of the spline + * as the evaluation point tends to positive infinity. + * @return limit value + */ + realtype get_final_value() const; + + /** + * @brief Get the limit value of the spline + * (in the scale in which interpolation is carried out) + * as the evaluation point tends to positive infinity. + * @return limit value + */ + realtype get_final_value_scaled() const; + + /** + * @brief Get the limit value of the sensitivity + * with respect to the given parameter + * as the evaluation point tends to positive infinity. + * @param ip parameter index + * @return limit value + */ + realtype get_final_sensitivity(int const ip) const; + + /** + * @brief Get the limit value of the sensitivity + * with respect to the given parameter + * (in the scale in which interpolation is carried out) + * as the evaluation point tends to positive infinity. + * @param ip parameter index + * @return limit value + */ + realtype get_final_sensitivity_scaled(int const ip) const; + + /** + * @brief Whether nodes are uniformly spaced + * @return boolean flag + */ + bool get_equidistant_spacing() const; + + /** + * @brief Whether spline interpolation is carried out in log-space + * @return boolean flag + */ + bool get_logarithmic_parametrization() const; + + /** + * @brief The number of interpolation nodes for this spline + * @return number of nodes + */ + int n_nodes() const { return static_cast(nodes_.size()); } + + protected: + /** + * @brief The nodes at which this spline is interpolated + */ + std::vector nodes_; + + /** + * @brief The values the spline assumes at the nodes + */ + std::vector node_values_; + + /** + * @brief Coefficients for each polynomial segment of the spline + */ + std::vector coefficients; + + /** + * @brief Polynomial coefficients for the extrapolating the spline values + */ + std::vector coefficients_extrapolate; + + /** + * @brief Coefficients for each polynomial segment of the sensitivities + * with respect to the parameters + */ + std::vector coefficients_sensi; + + /** + * @brief Polynomial coefficients for the extrapolating the sensitivities + */ + std::vector coefficients_extrapolate_sensi; + + /** + * @brief Set the limit value of the spline + * (in the scale in which interpolation is carried out) + * as the evaluation point tends to positive infinity. + * @param finalValue final value + */ + void set_final_value_scaled(realtype finalValue); + + /** + * @brief Set the limit value of the sensitivity + * (in the scale in which interpolation is carried out) + * as the evaluation point tends to positive infinity. + * @param finalSensitivity final value of the sensitivity + * for each parameter + */ + void set_final_sensitivity_scaled(std::vector finalSensitivity); + + private: + realtype final_value_scaled_; + + std::vector final_sensitivity_scaled_; + + bool equidistant_spacing_ = false; + + bool logarithmic_parametrization_ = false; + +}; // class SplineFunction + +/** + * @brief AMICI Hermite spline class. + * + * Instances of this class represent Hermite splines, + * which are uniquely determined by their nodes, + * the values at their nodes, the derivatives at their nodes + * (defaulting to finite difference approximations from the node values), + * boundary conditions and extrapolation conditions. + * Optionally, the spline can be defined in log-space in order + * to ensure positivity. + */ +class HermiteSpline : public AbstractSpline { + public: + HermiteSpline() = default; + + /** + * @brief Construct a `HermiteSpline`. + * @param nodes the nodes defining the position at which the value of + * the spline is known + * (if `equidistant_spacing` is true, it must contain only the first and + * the last node; the other nodes will be automatically inserted, + * assuming they are uniformly spaced) + * @param node_values the values assumed by the spline at the nodes + * @param node_values_derivative the derivatives of the spline at the nodes + * (if `node_derivative_by_FD` is true, it will resized and filled with + * finite difference approximations computed from `node_values`) + * @param firstNodeBC boundary condition at the first node + * @param lastNodeBC boundary condition at the last node + * @param firstNodeExtrapol extrapolation method on the left side + * @param lastNodeExtrapol extrapolation method on the right side + * @param node_derivative_by_FD whether derivatives are to be computed by + * finite differences + * @param equidistant_spacing whether equidistant nodes are to be computed + * @param logarithmic_parametrization if true, the spline interpolation + * will occur in log-space in order to ensure positivity of the interpolant + * (which strictly speaking will no longer be a spline) + */ + HermiteSpline( + std::vector nodes, std::vector node_values, + std::vector node_values_derivative, + SplineBoundaryCondition firstNodeBC, SplineBoundaryCondition lastNodeBC, + SplineExtrapolation firstNodeExtrapol, + SplineExtrapolation lastNodeExtrapol, bool node_derivative_by_FD, + bool equidistant_spacing, bool logarithmic_parametrization + ); + + void compute_coefficients() override; + + void compute_coefficients_sensi( + int nplist, int spline_offset, gsl::span dvaluesdp, + gsl::span dslopesdp + ) override; + + void compute_final_value() override; + + void compute_final_sensitivity( + int nplist, int spline_offset, gsl::span dspline_valuesdp, + gsl::span dspline_slopesdp + ) override; + + realtype get_value_scaled(const realtype t) const override; + + /** + * @brief Get the derivative of the spline at a given node + * @param i index of the node at which the spline is to be evaluated + * @return value of the derivative at the `i`-th node + */ + realtype get_node_derivative(int const i) const; + + /** + * @brief Get the derivative of the spline at a given node + * in the scale in which interpolation is carried out (e.g., log-scale) + * @param i index of the node at which the spline is to be evaluated + * @return scaled value of the derivative at the `i`-th node + */ + realtype get_node_derivative_scaled(int const i) const; + + realtype + get_sensitivity_scaled(const realtype t, int const ip) const override; + + /** + * @brief Whether derivatives of this spline are computed + * by finite differences + * @return boolean flag + */ + bool get_node_derivative_by_fd() const { return node_derivative_by_FD_; } + + private: + void compute_slope_sensitivities_by_fd( + int nplist, int spline_offset, int ip, gsl::span dvaluesdp, + gsl::span dslopesdp + ); + + void get_coeffs_sensi_lowlevel( + int ip, int i_node, int nplist, int n_spline_coefficients, + int spline_offset, realtype len, gsl::span dnodesdp, + gsl::span dslopesdp, gsl::span coeffs + ) const; + + void handle_inner_derivatives(); + + void handle_boundary_conditions(); + + void compute_coefficients_extrapolation(); + + void compute_coefficients_extrapolation_sensi( + int nplist, int spline_offset, gsl::span dspline_valuesdp, + gsl::span dspline_slopesdp + ); + + std::vector node_values_derivative_; + + SplineBoundaryCondition first_node_bc_ = SplineBoundaryCondition::given; + + SplineBoundaryCondition last_node_bc_ = SplineBoundaryCondition::given; + + SplineExtrapolation first_node_ep_ = SplineExtrapolation::linear; + + SplineExtrapolation last_node_ep_ = SplineExtrapolation::linear; + + bool node_derivative_by_FD_ = false; + +}; // class HermiteSpline + +} // namespace amici + +#endif /* AMICI_SPLINEFUNCTIONS_H */ diff --git a/include/amici/steadystateproblem.h b/include/amici/steadystateproblem.h index bb1b39e36e..b3af55c20a 100644 --- a/include/amici/steadystateproblem.h +++ b/include/amici/steadystateproblem.h @@ -2,9 +2,9 @@ #define AMICI_STEADYSTATEPROBLEM_H #include -#include -#include #include +#include +#include #include @@ -28,7 +28,7 @@ class SteadystateProblem { * @param solver Solver instance * @param model Model instance */ - explicit SteadystateProblem(const Solver &solver, const Model &model); + explicit SteadystateProblem(Solver const& solver, Model const& model); /** * @brief Handles steady state computation in the forward case: @@ -38,7 +38,7 @@ class SteadystateProblem { * @param model pointer to the model object * @param it integer with the index of the current time step */ - void workSteadyStateProblem(const Solver &solver, Model &model, int it); + void workSteadyStateProblem(Solver const& solver, Model& model, int it); /** * Integrates over the adjoint state backward in time by solving a linear @@ -48,37 +48,38 @@ class SteadystateProblem { * @param model pointer to the model object * @param bwd backward problem */ - void workSteadyStateBackwardProblem(const Solver &solver, Model &model, - const BackwardProblem *bwd); + void workSteadyStateBackwardProblem( + Solver const& solver, Model& model, BackwardProblem const* bwd + ); /** * @brief Returns the stored SimulationState * @return stored SimulationState */ - const SimulationState &getFinalSimulationState() const { return state_; }; + SimulationState const& getFinalSimulationState() const { return state_; }; /** * @brief Returns the quadratures from pre- or postequilibration * @return xQB Vector with quadratures */ - const AmiVector &getEquilibrationQuadratures() const { return xQB_; } + AmiVector const& getEquilibrationQuadratures() const { return xQB_; } /** * @brief Returns state at steadystate * @return x */ - const AmiVector &getState() const { return state_.x; }; + AmiVector const& getState() const { return state_.x; }; /** * @brief Returns state sensitivity at steadystate * @return sx */ - const AmiVectorArray &getStateSensitivity() const { return state_.sx; }; + AmiVectorArray const& getStateSensitivity() const { return state_.sx; }; /** * @brief Accessor for dJydx * @return dJydx */ - std::vector const &getDJydx() const { return dJydx_; } + std::vector const& getDJydx() const { return dJydx_; } /** * @brief Accessor for run_time of the forward problem @@ -96,7 +97,7 @@ class SteadystateProblem { * @brief Accessor for steady_state_status * @return steady_state_status */ - std::vector const &getSteadyStateStatus() const { + std::vector const& getSteadyStateStatus() const { return steady_state_status_; } @@ -116,7 +117,7 @@ class SteadystateProblem { * @brief Accessor for numsteps * @return numsteps */ - const std::vector &getNumSteps() const { return numsteps_; } + std::vector const& getNumSteps() const { return numsteps_; } /** * @brief Accessor for numstepsB @@ -130,19 +131,19 @@ class SteadystateProblem { * @param model Model instance * @param edata experimental data */ - void getAdjointUpdates(Model &model, const ExpData &edata); + void getAdjointUpdates(Model& model, ExpData const& edata); /** * @brief Return the adjoint state * @return xB adjoint state */ - AmiVector const &getAdjointState() const { return xB_; } + AmiVector const& getAdjointState() const { return xB_; } /** * @brief Accessor for xQB * @return xQB */ - AmiVector const &getAdjointQuadrature() const { return xQB_; } + AmiVector const& getAdjointQuadrature() const { return xQB_; } /** * @brief Accessor for hasQuadrature_ @@ -165,14 +166,14 @@ class SteadystateProblem { * @param model pointer to the model object * @param it integer with the index of the current time step */ - void findSteadyState(const Solver &solver, Model &model, int it); + void findSteadyState(Solver const& solver, Model& model, int it); /** * @brief Tries to determine the steady state by using Newton's method * @param model pointer to the model object * @param newton_retry bool flag indicating whether being relaunched */ - void findSteadyStateByNewtonsMethod(Model &model, bool newton_retry); + void findSteadyStateByNewtonsMethod(Model& model, bool newton_retry); /** * @brief Tries to determine the steady state by using forward simulation @@ -180,22 +181,22 @@ class SteadystateProblem { * @param model pointer to the model object * @param it integer with the index of the current time step */ - void findSteadyStateBySimulation(const Solver &solver, Model &model, - int it); + void + findSteadyStateBySimulation(Solver const& solver, Model& model, int it); /** * @brief Handles the computation of quadratures in adjoint mode * @param solver pointer to the solver object * @param model pointer to the model object */ - void computeSteadyStateQuadrature(const Solver &solver, Model &model); + void computeSteadyStateQuadrature(Solver const& solver, Model& model); /** * @brief Computes the quadrature in steady state backward mode by * solving the linear system defined by the backward Jacobian * @param model pointer to the model object */ - void getQuadratureByLinSolve(Model &model); + void getQuadratureByLinSolve(Model& model); /** * @brief Computes the quadrature in steady state backward mode by @@ -203,7 +204,7 @@ class SteadystateProblem { * @param solver pointer to the solver object * @param model pointer to the model object */ - void getQuadratureBySimulation(const Solver &solver, Model &model); + void getQuadratureBySimulation(Solver const& solver, Model& model); /** * @brief Stores state and throws an exception if equilibration failed @@ -215,8 +216,8 @@ class SteadystateProblem { * @param errorString const pointer to string with error message * @param status Entry of steady_state_status to be processed */ - void writeErrorString(std::string *errorString, - SteadyStateStatus status) const; + void + writeErrorString(std::string* errorString, SteadyStateStatus status) const; /** * @brief Checks depending on the status of the Newton solver, @@ -228,8 +229,10 @@ class SteadystateProblem { * @param context SteadyStateContext giving the situation for the flag * @return flag telling how to process state sensitivities */ - bool getSensitivityFlag(const Model &model, const Solver &solver, int it, - SteadyStateContext context); + bool getSensitivityFlag( + Model const& model, Solver const& solver, int it, + SteadyStateContext context + ); /** * @brief Computes the weighted root mean square of xdot @@ -242,8 +245,10 @@ class SteadystateProblem { * @param ewt error weight vector * @return root-mean-square norm */ - realtype getWrmsNorm(AmiVector const &x, AmiVector const &xdot, - realtype atol, realtype rtol, AmiVector &ewt) const; + realtype getWrmsNorm( + AmiVector const& x, AmiVector const& xdot, realtype atol, realtype rtol, + AmiVector& ewt + ) const; /** * @brief Checks convergence for state or adjoint quadratures, depending on @@ -252,14 +257,14 @@ class SteadystateProblem { * @param sensi_method sensitivity method * @return weighted root mean squared residuals of the RHS */ - realtype getWrms(Model &model, SensitivityMethod sensi_method); + realtype getWrms(Model& model, SensitivityMethod sensi_method); /** * @brief Checks convergence for state sensitivities * @param model Model instance * @return weighted root mean squared residuals of the RHS */ - realtype getWrmsFSA(Model &model); + realtype getWrmsFSA(Model& model); /** * @brief Runs the Newton solver iterations and checks for convergence @@ -267,7 +272,7 @@ class SteadystateProblem { * @param model pointer to the model object * @param newton_retry flag indicating if Newton solver is rerun */ - void applyNewtonsMethod(Model &model, bool newton_retry); + void applyNewtonsMethod(Model& model, bool newton_retry); /** * @brief Simulation is launched, if Newton solver or linear system solve @@ -276,8 +281,8 @@ class SteadystateProblem { * @param model pointer to the model object * @param backward flag indicating adjoint mode (including quadrature) */ - void runSteadystateSimulation(const Solver &solver, Model &model, - bool backward); + void + runSteadystateSimulation(Solver const& solver, Model& model, bool backward); /** * @brief Initialize CVodeSolver instance for preequilibration simulation @@ -287,10 +292,9 @@ class SteadystateProblem { * @param backward flag switching on quadratures computation * @return solver instance */ - std::unique_ptr createSteadystateSimSolver(const Solver &solver, - Model &model, - bool forwardSensis, - bool backward) const; + std::unique_ptr createSteadystateSimSolver( + Solver const& solver, Model& model, bool forwardSensis, bool backward + ) const; /** * @brief Initialize forward computation @@ -298,7 +302,7 @@ class SteadystateProblem { * @param solver pointer to the solver object * @param model pointer to the model object */ - void initializeForwardProblem(int it, const Solver &solver, Model &model); + void initializeForwardProblem(int it, Solver const& solver, Model& model); /** * @brief Initialize backward computation @@ -307,8 +311,9 @@ class SteadystateProblem { * @param bwd pointer to backward problem * @return flag indicating whether backward computation to be carried out */ - bool initializeBackwardProblem(const Solver &solver, Model &model, - const BackwardProblem *bwd); + bool initializeBackwardProblem( + Solver const& solver, Model& model, BackwardProblem const* bwd + ); /** * @brief Compute the backward quadratures, which contribute to the @@ -317,15 +322,15 @@ class SteadystateProblem { * @param yQ vector to be multiplied with dxdotdp * @param yQB resulting vector after multiplication */ - void computeQBfromQ(Model &model, const AmiVector &yQ, - AmiVector &yQB) const; + void + computeQBfromQ(Model& model, AmiVector const& yQ, AmiVector& yQB) const; /** * @brief Ensures state positivity, if requested and repeats convergence * check, if necessary * @param model pointer to the model object */ - bool makePositiveAndCheckConvergence(Model &model); + bool makePositiveAndCheckConvergence(Model& model); /** * @brief Updates the damping factor gamma that determines step size @@ -339,31 +344,31 @@ class SteadystateProblem { bool updateDampingFactor(bool step_successful); /** - * @brief Updates member variables to indicate that state_.x has been updated and xdot_, delta_, etc. - * need to be recomputed. + * @brief Updates member variables to indicate that state_.x has been + * updated and xdot_, delta_, etc. need to be recomputed. */ void flagUpdatedState(); /** - * @brief Retrieves simulation sensitivities from the provided solver and sets the corresponding flag - * to indicate they are up to date + * @brief Retrieves simulation sensitivities from the provided solver and + * sets the corresponding flag to indicate they are up to date * @param solver simulation solver instance */ - void updateSensiSimulation(const Solver &solver); + void updateSensiSimulation(Solver const& solver); /** - * @brief Computes the right hand side for the current state_.x and sets the corresponding flag to - * indicate xdot_ is up to date. + * @brief Computes the right hand side for the current state_.x and sets the + * corresponding flag to indicate xdot_ is up to date. * @param model model instance */ - void updateRightHandSide(Model &model); + void updateRightHandSide(Model& model); /** - * @brief Computes the newton step for the current state_.x and sets the corresponding flag to - * indicate delta_ is up to date. + * @brief Computes the newton step for the current state_.x and sets the + * corresponding flag to indicate delta_ is up to date. * @param model model instance */ - void getNewtonStep(Model &model); + void getNewtonStep(Model& model); /** newton step */ AmiVector delta_; @@ -444,17 +449,19 @@ class SteadystateProblem { /** damping factor lower bound */ realtype damping_factor_lower_bound_{1e-8}; /** whether newton step should be used for convergence steps */ - bool newton_step_conv_ {false}; - /** whether sensitivities should be checked for convergence to steadystate */ - bool check_sensi_conv_ {true}; + bool newton_step_conv_{false}; + /** whether sensitivities should be checked for convergence to steadystate + */ + bool check_sensi_conv_{true}; /** flag indicating whether xdot_ has been computed for the current state */ - bool xdot_updated_ {false}; - /** flag indicating whether delta_ has been computed for the current state */ - bool delta_updated_ {false}; - /** flag indicating whether simulation sensitivities have been retrieved for the current state */ - bool sensis_updated_ {false}; - + bool xdot_updated_{false}; + /** flag indicating whether delta_ has been computed for the current state + */ + bool delta_updated_{false}; + /** flag indicating whether simulation sensitivities have been retrieved for + * the current state */ + bool sensis_updated_{false}; }; } // namespace amici diff --git a/include/amici/sundials_linsol_wrapper.h b/include/amici/sundials_linsol_wrapper.h index c1f7dd0d05..613eb22156 100644 --- a/include/amici/sundials_linsol_wrapper.h +++ b/include/amici/sundials_linsol_wrapper.h @@ -43,27 +43,27 @@ class SUNLinSolWrapper { * @brief Copy constructor * @param other */ - SUNLinSolWrapper(const SUNLinSolWrapper &other) = delete; + SUNLinSolWrapper(SUNLinSolWrapper const& other) = delete; /** * @brief Move constructor * @param other */ - SUNLinSolWrapper(SUNLinSolWrapper &&other) noexcept; + SUNLinSolWrapper(SUNLinSolWrapper&& other) noexcept; /** * @brief Copy assignment * @param other * @return */ - SUNLinSolWrapper &operator=(const SUNLinSolWrapper &other) = delete; + SUNLinSolWrapper& operator=(SUNLinSolWrapper const& other) = delete; /** * @brief Move assignment * @param other * @return */ - SUNLinSolWrapper &operator=(SUNLinSolWrapper &&other) noexcept; + SUNLinSolWrapper& operator=(SUNLinSolWrapper&& other) noexcept; /** * @brief Returns the wrapped SUNLinSol. @@ -89,7 +89,7 @@ class SUNLinSolWrapper { * system matrix A. * @param A */ - void setup(const SUNMatrixWrapper& A) const; + void setup(SUNMatrixWrapper const& A) const; /** * @brief Solves a linear system A*x = b @@ -113,7 +113,7 @@ class SUNLinSolWrapper { * @param leniwLS output argument for size of integer workspace * @return workspace size */ - int space(long int *lenrwLS, long int *leniwLS) const; + int space(long int* lenrwLS, long int* leniwLS) const; /** * @brief Get the matrix A (matrix solvers only). @@ -130,10 +130,9 @@ class SUNLinSolWrapper { int initialize(); /** Wrapped solver */ - SUNLinearSolver solver_ {nullptr}; + SUNLinearSolver solver_{nullptr}; }; - /** * @brief SUNDIALS band direct solver. */ @@ -153,7 +152,7 @@ class SUNLinSolBand : public SUNLinSolWrapper { * @param ubw upper bandwidth of band matrix A * @param lbw lower bandwidth of band matrix A */ - SUNLinSolBand(AmiVector const &x, int ubw, int lbw); + SUNLinSolBand(AmiVector const& x, int ubw, int lbw); SUNMatrix getMatrix() const override; @@ -162,7 +161,6 @@ class SUNLinSolBand : public SUNLinSolWrapper { SUNMatrixWrapper A_; }; - /** * @brief SUNDIALS dense direct solver. */ @@ -172,7 +170,7 @@ class SUNLinSolDense : public SUNLinSolWrapper { * @brief Create dense solver * @param x A template for cloning vectors needed within the solver. */ - explicit SUNLinSolDense(AmiVector const &x); + explicit SUNLinSolDense(AmiVector const& x); SUNMatrix getMatrix() const override; @@ -181,18 +179,13 @@ class SUNLinSolDense : public SUNLinSolWrapper { SUNMatrixWrapper A_; }; - /** * @brief SUNDIALS KLU sparse direct solver. */ class SUNLinSolKLU : public SUNLinSolWrapper { public: /** KLU state reordering (different from SuperLUMT ordering!) */ - enum class StateOrdering { - AMD, - COLAMD, - natural - }; + enum class StateOrdering { AMD, COLAMD, natural }; /** * @brief Create KLU solver with given matrix @@ -208,8 +201,9 @@ class SUNLinSolKLU : public SUNLinSolWrapper { * @param sparsetype Sparse matrix type (CSC_MAT, CSR_MAT) * @param ordering */ - SUNLinSolKLU(AmiVector const &x, int nnz, int sparsetype, - StateOrdering ordering); + SUNLinSolKLU( + AmiVector const& x, int nnz, int sparsetype, StateOrdering ordering + ); SUNMatrix getMatrix() const override; @@ -239,7 +233,7 @@ class SUNLinSolKLU : public SUNLinSolWrapper { /** * @brief SUNDIALS SuperLUMT sparse direct solver. */ -class SUNLinSolSuperLUMT : public SUNLinSolWrapper { +class SUNLinSolSuperLUMT : public SUNLinSolWrapper { public: /** SuperLUMT ordering (different from KLU ordering!) */ enum class StateOrdering { @@ -268,8 +262,9 @@ class SUNLinSolSuperLUMT : public SUNLinSolWrapper { * @param sparsetype Sparse matrix type (CSC_MAT, CSR_MAT) * @param ordering */ - SUNLinSolSuperLUMT(AmiVector const &x, int nnz, int sparsetype, - StateOrdering ordering); + SUNLinSolSuperLUMT( + AmiVector const& x, int nnz, int sparsetype, StateOrdering ordering + ); /** * @brief Create SuperLUMT solver and matrix to operate on @@ -279,8 +274,10 @@ class SUNLinSolSuperLUMT : public SUNLinSolWrapper { * @param ordering * @param numThreads Number of threads to be used by SuperLUMT */ - SUNLinSolSuperLUMT(AmiVector const &x, int nnz, int sparsetype, - StateOrdering ordering, int numThreads); + SUNLinSolSuperLUMT( + AmiVector const& x, int nnz, int sparsetype, StateOrdering ordering, + int numThreads + ); SUNMatrix getMatrix() const override; @@ -320,7 +317,7 @@ class SUNLinSolPCG : public SUNLinSolWrapper { * @param ATimes * @return */ - int setATimes(void *A_data, ATimesFn ATimes); + int setATimes(void* A_data, ATimesFn ATimes); /** * @brief Sets function pointers for PSetup and PSolve routines inside @@ -331,7 +328,7 @@ class SUNLinSolPCG : public SUNLinSolWrapper { * @param Psol * @return */ - int setPreconditioner(void *P_data, PSetupFn Pset, PSolveFn Psol); + int setPreconditioner(void* P_data, PSetupFn Pset, PSolveFn Psol); /** * @brief Sets pointers to left/right scaling vectors for the linear @@ -363,7 +360,6 @@ class SUNLinSolPCG : public SUNLinSolWrapper { N_Vector getResid() const; }; - /** * @brief SUNDIALS scaled preconditioned Bi-CGStab (Bi-Conjugate Gradient * Stable method) (SPBCGS) solver. @@ -377,8 +373,9 @@ class SUNLinSolSPBCGS : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - explicit SUNLinSolSPBCGS(N_Vector x, int pretype = PREC_NONE, - int maxl = SUNSPBCGS_MAXL_DEFAULT); + explicit SUNLinSolSPBCGS( + N_Vector x, int pretype = PREC_NONE, int maxl = SUNSPBCGS_MAXL_DEFAULT + ); /** * @brief SUNLinSolSPBCGS @@ -387,8 +384,10 @@ class SUNLinSolSPBCGS : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - explicit SUNLinSolSPBCGS(AmiVector const &x, int pretype = PREC_NONE, - int maxl = SUNSPBCGS_MAXL_DEFAULT); + explicit SUNLinSolSPBCGS( + AmiVector const& x, int pretype = PREC_NONE, + int maxl = SUNSPBCGS_MAXL_DEFAULT + ); /** * @brief Sets the function pointer for ATimes @@ -397,7 +396,7 @@ class SUNLinSolSPBCGS : public SUNLinSolWrapper { * @param ATimes * @return */ - int setATimes(void *A_data, ATimesFn ATimes); + int setATimes(void* A_data, ATimesFn ATimes); /** * @brief Sets function pointers for PSetup and PSolve routines inside @@ -408,7 +407,7 @@ class SUNLinSolSPBCGS : public SUNLinSolWrapper { * @param Psol * @return */ - int setPreconditioner(void *P_data, PSetupFn Pset, PSolveFn Psol); + int setPreconditioner(void* P_data, PSetupFn Pset, PSolveFn Psol); /** * @brief Sets pointers to left/right scaling vectors for the linear @@ -440,7 +439,6 @@ class SUNLinSolSPBCGS : public SUNLinSolWrapper { N_Vector getResid() const; }; - /** * @brief SUNDIALS scaled preconditioned FGMRES (Flexible Generalized Minimal * Residual method) (SPFGMR) solver. @@ -454,7 +452,7 @@ class SUNLinSolSPFGMR : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - SUNLinSolSPFGMR(AmiVector const &x, int pretype, int maxl); + SUNLinSolSPFGMR(AmiVector const& x, int pretype, int maxl); /** * @brief Sets the function pointer for ATimes @@ -463,7 +461,7 @@ class SUNLinSolSPFGMR : public SUNLinSolWrapper { * @param ATimes * @return */ - int setATimes(void *A_data, ATimesFn ATimes); + int setATimes(void* A_data, ATimesFn ATimes); /** * @brief Sets function pointers for PSetup and PSolve routines inside @@ -474,7 +472,7 @@ class SUNLinSolSPFGMR : public SUNLinSolWrapper { * @param Psol * @return */ - int setPreconditioner(void *P_data, PSetupFn Pset, PSolveFn Psol); + int setPreconditioner(void* P_data, PSetupFn Pset, PSolveFn Psol); /** * @brief Sets pointers to left/right scaling vectors for the linear @@ -506,7 +504,6 @@ class SUNLinSolSPFGMR : public SUNLinSolWrapper { N_Vector getResid() const; }; - /** * @brief SUNDIALS scaled preconditioned GMRES (Generalized Minimal Residual * method) solver (SPGMR). @@ -520,8 +517,10 @@ class SUNLinSolSPGMR : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - explicit SUNLinSolSPGMR(AmiVector const &x, int pretype = PREC_NONE, - int maxl = SUNSPGMR_MAXL_DEFAULT); + explicit SUNLinSolSPGMR( + AmiVector const& x, int pretype = PREC_NONE, + int maxl = SUNSPGMR_MAXL_DEFAULT + ); /** * @brief Sets the function pointer for ATimes @@ -530,7 +529,7 @@ class SUNLinSolSPGMR : public SUNLinSolWrapper { * @param ATimes * @return */ - int setATimes(void *A_data, ATimesFn ATimes); + int setATimes(void* A_data, ATimesFn ATimes); /** * @brief Sets function pointers for PSetup and PSolve routines inside @@ -541,7 +540,7 @@ class SUNLinSolSPGMR : public SUNLinSolWrapper { * @param Psol * @return */ - int setPreconditioner(void *P_data, PSetupFn Pset, PSolveFn Psol); + int setPreconditioner(void* P_data, PSetupFn Pset, PSolveFn Psol); /** * @brief Sets pointers to left/right scaling vectors for the linear @@ -573,7 +572,6 @@ class SUNLinSolSPGMR : public SUNLinSolWrapper { N_Vector getResid() const; }; - /** * @brief SUNDIALS scaled preconditioned TFQMR (Transpose-Free Quasi-Minimal * Residual method) (SPTFQMR) solver. @@ -587,8 +585,9 @@ class SUNLinSolSPTFQMR : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - explicit SUNLinSolSPTFQMR(N_Vector x, int pretype = PREC_NONE, - int maxl = SUNSPTFQMR_MAXL_DEFAULT); + explicit SUNLinSolSPTFQMR( + N_Vector x, int pretype = PREC_NONE, int maxl = SUNSPTFQMR_MAXL_DEFAULT + ); /** * @brief Create SPTFQMR solver @@ -597,8 +596,10 @@ class SUNLinSolSPTFQMR : public SUNLinSolWrapper { * PREC_BOTH) * @param maxl Maximum number of solver iterations */ - explicit SUNLinSolSPTFQMR(AmiVector const &x, int pretype = PREC_NONE, - int maxl = SUNSPTFQMR_MAXL_DEFAULT); + explicit SUNLinSolSPTFQMR( + AmiVector const& x, int pretype = PREC_NONE, + int maxl = SUNSPTFQMR_MAXL_DEFAULT + ); /** * @brief Sets the function pointer for ATimes @@ -607,7 +608,7 @@ class SUNLinSolSPTFQMR : public SUNLinSolWrapper { * @param ATimes * @return */ - int setATimes(void *A_data, ATimesFn ATimes); + int setATimes(void* A_data, ATimesFn ATimes); /** * @brief Sets function pointers for PSetup and PSolve routines inside @@ -618,7 +619,7 @@ class SUNLinSolSPTFQMR : public SUNLinSolWrapper { * @param Psol * @return */ - int setPreconditioner(void *P_data, PSetupFn Pset, PSolveFn Psol); + int setPreconditioner(void* P_data, PSetupFn Pset, PSolveFn Psol); /** * @brief Sets pointers to left/right scaling vectors for the linear @@ -650,7 +651,6 @@ class SUNLinSolSPTFQMR : public SUNLinSolWrapper { N_Vector getResid() const; }; - /** * @brief A RAII wrapper for SUNNonLinearSolver structs which solve the * nonlinear system F (y) = 0 or G(y) = y. @@ -669,27 +669,27 @@ class SUNNonLinSolWrapper { * @brief Copy constructor * @param other */ - SUNNonLinSolWrapper(const SUNNonLinSolWrapper &other) = delete; + SUNNonLinSolWrapper(SUNNonLinSolWrapper const& other) = delete; /** * @brief Move constructor * @param other */ - SUNNonLinSolWrapper(SUNNonLinSolWrapper &&other) noexcept; + SUNNonLinSolWrapper(SUNNonLinSolWrapper&& other) noexcept; /** * @brief Copy assignment * @param other * @return */ - SUNNonLinSolWrapper &operator=(const SUNNonLinSolWrapper &other) = delete; + SUNNonLinSolWrapper& operator=(SUNNonLinSolWrapper const& other) = delete; /** * @brief Move assignment * @param other * @return */ - SUNNonLinSolWrapper &operator=(SUNNonLinSolWrapper &&other) noexcept; + SUNNonLinSolWrapper& operator=(SUNNonLinSolWrapper&& other) noexcept; /** * @brief Get the wrapped SUNNonlinearSolver @@ -709,7 +709,7 @@ class SUNNonLinSolWrapper { * @param mem the sundials integrator memory structure. * @return */ - int setup(N_Vector y, void *mem); + int setup(N_Vector y, void* mem); /** * @brief Solve the nonlinear system F (y) = 0 or G(y) = y. @@ -725,8 +725,10 @@ class SUNNonLinSolWrapper { * @param mem the sundials integrator memory structure. * @return */ - int Solve(N_Vector y0, N_Vector y, N_Vector w, realtype tol, - bool callLSetup, void *mem); + int Solve( + N_Vector y0, N_Vector y, N_Vector w, realtype tol, bool callLSetup, + void* mem + ); /** * @brief Set function to evaluate the nonlinear residual function F(y) = 0 @@ -793,7 +795,6 @@ class SUNNonLinSolWrapper { SUNNonlinearSolver solver = nullptr; }; - /** * @brief SUNDIALS Newton non-linear solver to solve F (y) = 0. */ @@ -820,10 +821,9 @@ class SUNNonLinSolNewton : public SUNNonLinSolWrapper { * @param SysFn * @return */ - int getSysFn(SUNNonlinSolSysFn *SysFn) const; + int getSysFn(SUNNonlinSolSysFn* SysFn) const; }; - /** * @brief SUNDIALS Fixed point non-linear solver to solve G(y) = y. */ @@ -852,7 +852,7 @@ class SUNNonLinSolFixedPoint : public SUNNonLinSolWrapper { * @param SysFn * @return */ - int getSysFn(SUNNonlinSolSysFn *SysFn) const; + int getSysFn(SUNNonlinSolSysFn* SysFn) const; }; } // namespace amici diff --git a/include/amici/sundials_matrix_wrapper.h b/include/amici/sundials_matrix_wrapper.h index dbdd8ad992..8d63eca5ea 100644 --- a/include/amici/sundials_matrix_wrapper.h +++ b/include/amici/sundials_matrix_wrapper.h @@ -7,8 +7,8 @@ #include -#include #include +#include #include @@ -33,8 +33,9 @@ class SUNMatrixWrapper { * @param NNZ Number of nonzeros * @param sparsetype Sparse type */ - SUNMatrixWrapper(sunindextype M, sunindextype N, sunindextype NNZ, - int sparsetype); + SUNMatrixWrapper( + sunindextype M, sunindextype N, sunindextype NNZ, int sparsetype + ); /** * @brief Create dense matrix. See SUNDenseMatrix in sunmatrix_dense.h @@ -59,8 +60,9 @@ class SUNMatrixWrapper { * @param droptol tolerance for dropping entries * @param sparsetype Sparse type */ - SUNMatrixWrapper(const SUNMatrixWrapper &A, realtype droptol, - int sparsetype); + SUNMatrixWrapper( + SUNMatrixWrapper const& A, realtype droptol, int sparsetype + ); /** * @brief Wrap existing SUNMatrix @@ -74,27 +76,27 @@ class SUNMatrixWrapper { * @brief Copy constructor * @param other */ - SUNMatrixWrapper(const SUNMatrixWrapper &other); + SUNMatrixWrapper(SUNMatrixWrapper const& other); /** * @brief Move constructor * @param other */ - SUNMatrixWrapper(SUNMatrixWrapper &&other); + SUNMatrixWrapper(SUNMatrixWrapper&& other); /** * @brief Copy assignment * @param other * @return */ - SUNMatrixWrapper &operator=(const SUNMatrixWrapper &other); + SUNMatrixWrapper& operator=(SUNMatrixWrapper const& other); /** * @brief Move assignment * @param other * @return */ - SUNMatrixWrapper &operator=(SUNMatrixWrapper &&other); + SUNMatrixWrapper& operator=(SUNMatrixWrapper&& other); /** * @brief Reallocate space for sparse matrix according to specified nnz @@ -103,17 +105,19 @@ class SUNMatrixWrapper { void reallocate(sunindextype nnz); /** - * @brief Reallocate space for sparse matrix to used space according to last entry in indexptrs + * @brief Reallocate space for sparse matrix to used space according to last + * entry in indexptrs */ void realloc(); /** * @brief Get the wrapped SUNMatrix * @return raw SunMatrix object - * @note Even though the returned matrix_ pointer is const qualified, matrix_->content will not be const. - * This is a shortcoming in the underlying C library, which we cannot address and it is not intended that - * any of those values are modified externally. If matrix_->content is manipulated, - * cpp:meth:SUNMatrixWrapper:`refresh` needs to be called. + * @note Even though the returned matrix_ pointer is const qualified, + * matrix_->content will not be const. This is a shortcoming in the + * underlying C library, which we cannot address and it is not intended that + * any of those values are modified externally. If matrix_->content is + * manipulated, cpp:meth:SUNMatrixWrapper:`refresh` needs to be called. */ SUNMatrix get() const; @@ -122,10 +126,12 @@ class SUNMatrixWrapper { * @return number of rows */ sunindextype rows() const { - assert(!matrix_ || - (matrix_id() == SUNMATRIX_SPARSE ? - num_rows_ == SM_ROWS_S(matrix_) : - num_rows_ == SM_ROWS_D(matrix_))); + assert( + !matrix_ + || (matrix_id() == SUNMATRIX_SPARSE + ? num_rows_ == SM_ROWS_S(matrix_) + : num_rows_ == SM_ROWS_D(matrix_)) + ); return num_rows_; } @@ -134,22 +140,26 @@ class SUNMatrixWrapper { * @return number of columns */ sunindextype columns() const { - assert(!matrix_ || - (matrix_id() == SUNMATRIX_SPARSE ? - num_columns_ == SM_COLUMNS_S(matrix_) : - num_columns_ == SM_COLUMNS_D(matrix_))); + assert( + !matrix_ + || (matrix_id() == SUNMATRIX_SPARSE + ? num_columns_ == SM_COLUMNS_S(matrix_) + : num_columns_ == SM_COLUMNS_D(matrix_)) + ); return num_columns_; } /** - * @brief Get the number of specified non-zero elements (sparse matrices only) + * @brief Get the number of specified non-zero elements (sparse matrices + * only) * @note value will be 0 before indexptrs are set. * @return number of nonzero entries */ sunindextype num_nonzeros() const; /** - * @brief Get the number of indexptrs that can be specified (sparse matrices only) + * @brief Get the number of indexptrs that can be specified (sparse matrices + * only) * @return number of indexptrs */ sunindextype num_indexptrs() const; @@ -164,20 +174,20 @@ class SUNMatrixWrapper { * @brief Get raw data of a sparse matrix * @return pointer to first data entry */ - realtype *data(); + realtype* data(); /** * @brief Get const raw data of a sparse matrix * @return pointer to first data entry */ - const realtype *data() const; + realtype const* data() const; /** * @brief Get data of a sparse matrix * @param idx data index * @return idx-th data entry */ - realtype get_data(sunindextype idx) const{ + realtype get_data(sunindextype idx) const { assert(matrix_); assert(matrix_id() == SUNMATRIX_SPARSE); assert(idx < capacity()); @@ -191,7 +201,7 @@ class SUNMatrixWrapper { * @param icol col * @return A(irow,icol) */ - realtype get_data(sunindextype irow, sunindextype icol) const{ + realtype get_data(sunindextype irow, sunindextype icol) const { assert(matrix_); assert(matrix_id() == SUNMATRIX_DENSE); assert(irow < rows()); @@ -256,7 +266,7 @@ class SUNMatrixWrapper { * @brief Set the index values of a sparse matrix * @param vals rows (CSC) or columns (CSR) for data entries */ - void set_indexvals(const gsl::span vals) { + void set_indexvals(const gsl::span vals) { assert(matrix_); assert(matrix_id() == SUNMATRIX_SPARSE); assert(gsl::narrow(vals.size()) == capacity()); @@ -280,7 +290,8 @@ class SUNMatrixWrapper { /** * @brief Set the index pointer of a sparse matrix * @param ptr_idx pointer index - * @param ptr data-index where the ptr_idx-th column (CSC) or row (CSR) starts + * @param ptr data-index where the ptr_idx-th column (CSC) or row (CSR) + * starts */ void set_indexptr(sunindextype ptr_idx, sunindextype ptr) { assert(matrix_); @@ -295,9 +306,10 @@ class SUNMatrixWrapper { /** * @brief Set the index pointers of a sparse matrix - * @param ptrs starting data-indices where the columns (CSC) or rows (CSR) start + * @param ptrs starting data-indices where the columns (CSC) or rows (CSR) + * start */ - void set_indexptrs(const gsl::span ptrs) { + void set_indexptrs(const gsl::span ptrs) { assert(matrix_); assert(matrix_id() == SUNMATRIX_SPARSE); assert(gsl::narrow(ptrs.size()) == num_indexptrs() + 1); @@ -332,19 +344,21 @@ class SUNMatrixWrapper { * @param b multiplication vector * @param alpha scalar coefficient for matrix */ - void multiply(AmiVector& c, AmiVector const& b, realtype alpha = 1.0) const { + void + multiply(AmiVector& c, AmiVector const& b, realtype alpha = 1.0) const { multiply(c.getNVector(), b.getNVector(), alpha); } - /** * @brief Perform matrix vector multiplication c += alpha * A*b * @param c output vector, may already contain values * @param b multiplication vector * @param alpha scalar coefficient */ - void multiply(gsl::span c, gsl::span b, - const realtype alpha = 1.0) const; + void multiply( + gsl::span c, gsl::span b, + const realtype alpha = 1.0 + ) const; /** * @brief Perform reordered matrix vector multiplication c += A[:,cols]*b @@ -353,10 +367,9 @@ class SUNMatrixWrapper { * @param cols int vector for column reordering * @param transpose bool transpose A before multiplication */ - void multiply(N_Vector c, - const_N_Vector b, - gsl::span cols, - bool transpose) const; + void multiply( + N_Vector c, const_N_Vector b, gsl::span cols, bool transpose + ) const; /** * @brief Perform reordered matrix vector multiplication c += A[:,cols]*b @@ -365,19 +378,19 @@ class SUNMatrixWrapper { * @param cols int vector for column reordering * @param transpose bool transpose A before multiplication */ - void multiply(gsl::span c, - gsl::span b, - gsl::span cols, - bool transpose) const; + void multiply( + gsl::span c, gsl::span b, + gsl::span cols, bool transpose + ) const; /** * @brief Perform matrix matrix multiplication C = A * B for sparse A, B, C * @param C output matrix, * @param B multiplication matrix - * @note will overwrite existing data, indexptrs, indexvals for C, but will use preallocated space for these vars + * @note will overwrite existing data, indexptrs, indexvals for C, but will + * use preallocated space for these vars */ - void sparse_multiply(SUNMatrixWrapper &C, - const SUNMatrixWrapper &B) const; + void sparse_multiply(SUNMatrixWrapper& C, SUNMatrixWrapper const& B) const; /** * @brief Perform sparse matrix matrix addition C = alpha * A + beta * B @@ -385,59 +398,72 @@ class SUNMatrixWrapper { * @param alpha scalar A * @param B addition matrix * @param beta scalar B - * @note will overwrite existing data, indexptrs, indexvals for C, but will use preallocated space for these vars + * @note will overwrite existing data, indexptrs, indexvals for C, but will + * use preallocated space for these vars */ - void sparse_add(const SUNMatrixWrapper &A, realtype alpha, - const SUNMatrixWrapper &B, realtype beta); + void sparse_add( + SUNMatrixWrapper const& A, realtype alpha, SUNMatrixWrapper const& B, + realtype beta + ); /** * @brief Perform matrix-matrix addition A = sum(mats(0)...mats(len(mats))) * @param mats vector of sparse matrices - * @note will overwrite existing data, indexptrs, indexvals for A, but will use preallocated space for these vars + * @note will overwrite existing data, indexptrs, indexvals for A, but will + * use preallocated space for these vars */ - void sparse_sum(const std::vector &mats); + void sparse_sum(std::vector const& mats); /** - * @brief Compute x = x + beta * A(:,k), where x is a dense vector and A(:,k) is sparse, and update - * the sparsity pattern for C(:,j) if applicable + * @brief Compute x = x + beta * A(:,k), where x is a dense vector and + * A(:,k) is sparse, and update the sparsity pattern for C(:,j) if + * applicable * * This function currently has two purposes: - * - perform parts of sparse matrix-matrix multiplication C(:,j)=A(:,k)*B(k,j) - * enabled by passing beta=B(k,j), x=C(:,j), C=C, w=sparsity of C(:,j) from B(k,0...j-1), nnz=nnz(C(:,0...j-1) - * - add the k-th column of the sparse matrix A multiplied by beta to the dense vector x. - * enabled by passing beta=*, x=x, C=nullptr, w=nullptr, nnz=* + * - perform parts of sparse matrix-matrix multiplication + * C(:,j)=A(:,k)*B(k,j) enabled by passing beta=B(k,j), x=C(:,j), C=C, + * w=sparsity of C(:,j) from B(k,0...j-1), nnz=nnz(C(:,0...j-1) + * - add the k-th column of the sparse matrix A multiplied by beta to the + * dense vector x. enabled by passing beta=*, x=x, C=nullptr, w=nullptr, + * nnz=* * * @param k column index * @param beta scaling factor - * @param w index workspace, (w[i] x, - const sunindextype mark, - SUNMatrixWrapper *C, sunindextype nnz) const; + sunindextype scatter( + const sunindextype k, const realtype beta, sunindextype* w, + gsl::span x, const sunindextype mark, SUNMatrixWrapper* C, + sunindextype nnz + ) const; /** - * @brief Compute transpose A' of sparse matrix A and writes it to the matrix C = alpha * A' + * @brief Compute transpose A' of sparse matrix A and writes it to the + * matrix C = alpha * A' * * @param C output matrix (sparse or dense) * @param alpha scalar multiplier - * @param blocksize blocksize for transposition. For full matrix transpose set to ncols/nrows + * @param blocksize blocksize for transposition. For full matrix transpose + * set to ncols/nrows */ - void transpose(SUNMatrixWrapper &C, const realtype alpha, - sunindextype blocksize) const; + void transpose( + SUNMatrixWrapper& C, const realtype alpha, sunindextype blocksize + ) const; /** * @brief Writes a sparse matrix A to a dense matrix D. * * @param D dense output matrix */ - void to_dense(SUNMatrixWrapper &D) const; + void to_dense(SUNMatrixWrapper& D) const; /** * @brief Writes the diagonal of sparse matrix A to a dense vector v. @@ -455,64 +481,64 @@ class SUNMatrixWrapper { * @brief Get matrix id * @return SUNMatrix_ID */ - SUNMatrix_ID matrix_id() const {return id_;}; + SUNMatrix_ID matrix_id() const { return id_; }; /** - * @brief Update internal cache, needs to be called after external manipulation of matrix_->content + * @brief Update internal cache, needs to be called after external + * manipulation of matrix_->content */ void refresh(); private: - /** * @brief SUNMatrix to which all methods are applied */ - SUNMatrix matrix_ {nullptr}; + SUNMatrix matrix_{nullptr}; /** * @brief cache for SUNMatrixGetId(matrix_) */ - SUNMatrix_ID id_ {SUNMATRIX_CUSTOM}; + SUNMatrix_ID id_{SUNMATRIX_CUSTOM}; /** * @brief cache for SUNMatrixGetId(matrix_) */ - int sparsetype_ {CSC_MAT}; + int sparsetype_{CSC_MAT}; /** * @brief cache for SM_INDEXPTRS_S(matrix_)[SM_NP_S(matrix_)] */ - sunindextype num_nonzeros_ {0}; + sunindextype num_nonzeros_{0}; /** * @brief cache for SM_NNZ_S(matrix_) */ - sunindextype capacity_ {0}; + sunindextype capacity_{0}; /** * @brief cache for SM_DATA_S(matrix_) */ - realtype *data_ {nullptr}; + realtype* data_{nullptr}; /** * @brief cache for SM_INDEXPTRS_S(matrix_) */ - sunindextype *indexptrs_ {nullptr}; + sunindextype* indexptrs_{nullptr}; /** * @brief cache for SM_INDEXVALS_S(matrix_) */ - sunindextype *indexvals_ {nullptr}; + sunindextype* indexvals_{nullptr}; /** * @brief cache for SM_ROWS_X(matrix_) */ - sunindextype num_rows_ {0}; + sunindextype num_rows_{0}; /** * @brief cache for SM_COLUMS_X(matrix_) */ - sunindextype num_columns_ {0}; + sunindextype num_columns_{0}; /** * @brief cache for SM_NP_S(matrix_) */ - sunindextype num_indexptrs_ {0}; + sunindextype num_indexptrs_{0}; /** * @brief call update_ptrs & update_size @@ -527,12 +553,12 @@ class SUNMatrixWrapper { */ void update_size(); /** - * @brief indicator whether this wrapper allocated matrix_ and is responsible for deallocation + * @brief indicator whether this wrapper allocated matrix_ and is + * responsible for deallocation */ bool ownmat = true; }; - /** * @brief Convert a flat index to a pair of row/column indices. * @param i flat index @@ -550,8 +576,7 @@ namespace gsl { * @param m SUNMatrix * @return Created span */ -inline span make_span(SUNMatrix m) -{ +inline span make_span(SUNMatrix m) { switch (SUNMatGetID(m)) { case SUNMATRIX_DENSE: return span(SM_DATA_D(m), SM_LDATA_D(m)); diff --git a/include/amici/symbolic_functions.h b/include/amici/symbolic_functions.h index 3cacc823be..0e8f558eba 100644 --- a/include/amici/symbolic_functions.h +++ b/include/amici/symbolic_functions.h @@ -114,8 +114,6 @@ double getNaN(); */ double sign(double x); - - /* legacy spline implementation in C (MATLAB only) */ /** diff --git a/include/amici/vector.h b/include/amici/vector.h index 79465776bb..b1b496c26e 100644 --- a/include/amici/vector.h +++ b/include/amici/vector.h @@ -1,8 +1,8 @@ #ifndef AMICI_VECTOR_H #define AMICI_VECTOR_H -#include #include +#include #include @@ -13,10 +13,10 @@ namespace amici { /** Since const N_Vector is not what we want */ -using const_N_Vector = - std::add_const_t> *; +using const_N_Vector + = std::add_const_t>*; -inline const realtype* N_VGetArrayPointerConst(const_N_Vector x) { +inline realtype const* N_VGetArrayPointerConst(const_N_Vector x) { return N_VGetArrayPointer(const_cast(x)); } @@ -36,9 +36,9 @@ class AmiVector { * @brief empty constructor * @param length number of elements in vector */ - explicit AmiVector(const long int length) - : vec_(static_cast(length), 0.0), - nvec_(N_VMake_Serial(length, vec_.data())) {} + explicit AmiVector(long int const length) + : vec_(static_cast(length), 0.0) + , nvec_(N_VMake_Serial(length, vec_.data())) {} /** Moves data from std::vector and constructs an nvec that points to the * data @@ -46,8 +46,9 @@ class AmiVector { * @param rvec vector from which the data will be moved */ explicit AmiVector(std::vector rvec) - : vec_(std::move(rvec)), - nvec_(N_VMake_Serial(gsl::narrow(vec_.size()), vec_.data())) {} + : vec_(std::move(rvec)) + , nvec_(N_VMake_Serial(gsl::narrow(vec_.size()), vec_.data()) + ) {} /** Copy data from gsl::span and constructs a vector * @brief constructor from gsl::span, @@ -60,16 +61,19 @@ class AmiVector { * @brief copy constructor * @param vold vector from which the data will be copied */ - AmiVector(const AmiVector &vold) : vec_(vold.vec_) { - nvec_ = - N_VMake_Serial(gsl::narrow(vold.vec_.size()), vec_.data()); + AmiVector(AmiVector const& vold) + : vec_(vold.vec_) { + nvec_ = N_VMake_Serial( + gsl::narrow(vold.vec_.size()), vec_.data() + ); } /** * @brief move constructor * @param other vector from which the data will be moved */ - AmiVector(AmiVector&& other) noexcept : nvec_(nullptr) { + AmiVector(AmiVector&& other) noexcept + : nvec_(nullptr) { vec_ = std::move(other.vec_); synchroniseNVector(); } @@ -84,17 +88,18 @@ class AmiVector { * @param other right hand side * @return left hand side */ - AmiVector &operator=(AmiVector const &other); + AmiVector& operator=(AmiVector const& other); /** * @brief operator *= (element-wise multiplication) * @param multiplier multiplier * @return result */ - AmiVector &operator*=(AmiVector const& multiplier) { - N_VProd(getNVector(), - const_cast(multiplier.getNVector()), - getNVector()); + AmiVector& operator*=(AmiVector const& multiplier) { + N_VProd( + getNVector(), const_cast(multiplier.getNVector()), + getNVector() + ); return *this; } @@ -103,10 +108,11 @@ class AmiVector { * @param divisor divisor * @return result */ - AmiVector &operator/=(AmiVector const& divisor) { - N_VDiv(getNVector(), - const_cast(divisor.getNVector()), - getNVector()); + AmiVector& operator/=(AmiVector const& divisor) { + N_VDiv( + getNVector(), const_cast(divisor.getNVector()), + getNVector() + ); return *this; } @@ -128,13 +134,13 @@ class AmiVector { * @brief data accessor * @return pointer to data array */ - realtype *data(); + realtype* data(); /** * @brief const data accessor * @return const pointer to data array */ - const realtype *data() const; + realtype const* data() const; /** * @brief N_Vector accessor @@ -152,7 +158,7 @@ class AmiVector { * @brief Vector accessor * @return Vector */ - std::vector const &getVector() const; + std::vector const& getVector() const; /** * @brief returns the length of the vector @@ -181,40 +187,38 @@ class AmiVector { * @param pos index of element * @return element */ - realtype &operator[](int pos); + realtype& operator[](int pos); /** * @brief accessor to data elements of the vector * @param pos index of element * @return element */ - realtype &at(int pos); + realtype& at(int pos); /** * @brief accessor to data elements of the vector * @param pos index of element * @return element */ - const realtype &at(int pos) const; + realtype const& at(int pos) const; /** * @brief copies data from another AmiVector * @param other data source */ - void copy(const AmiVector &other); + void copy(AmiVector const& other); /** * @brief Take absolute value (in-place) */ - void abs() { - N_VAbs(getNVector(), getNVector()); - }; + void abs() { N_VAbs(getNVector(), getNVector()); }; private: /** main data storage */ std::vector vec_; /** N_Vector, will be synchronized such that it points to data in vec */ - N_Vector nvec_ {nullptr}; + N_Vector nvec_{nullptr}; /** * @brief reconstructs nvec such that data pointer points to vec data array @@ -250,7 +254,7 @@ class AmiVectorArray { * @brief copy constructor * @param vaold object to copy from */ - AmiVectorArray(const AmiVectorArray &vaold); + AmiVectorArray(AmiVectorArray const& vaold); ~AmiVectorArray() = default; @@ -259,21 +263,21 @@ class AmiVectorArray { * @param other right hand side * @return left hand side */ - AmiVectorArray &operator=(AmiVectorArray const &other); + AmiVectorArray& operator=(AmiVectorArray const& other); /** * @brief accessor to data of AmiVector elements * @param pos index of AmiVector * @return pointer to data array */ - realtype *data(int pos); + realtype* data(int pos); /** * @brief const accessor to data of AmiVector elements * @param pos index of AmiVector * @return const pointer to data array */ - const realtype *data(int pos) const; + realtype const* data(int pos) const; /** * @brief accessor to elements of AmiVector elements @@ -281,7 +285,7 @@ class AmiVectorArray { * @param jpos outer index in AmiVectorArray * @return element */ - realtype &at(int ipos, int jpos); + realtype& at(int ipos, int jpos); /** * @brief const accessor to elements of AmiVector elements @@ -289,13 +293,13 @@ class AmiVectorArray { * @param jpos outer index in AmiVectorArray * @return element */ - const realtype &at(int ipos, int jpos) const; + realtype const& at(int ipos, int jpos) const; /** * @brief accessor to NVectorArray * @return N_VectorArray */ - N_Vector *getNVectorArray(); + N_Vector* getNVectorArray(); /** * @brief accessor to NVector element @@ -316,14 +320,14 @@ class AmiVectorArray { * @param pos index of AmiVector * @return AmiVector */ - AmiVector &operator[](int pos); + AmiVector& operator[](int pos); /** * @brief const accessor to AmiVector elements * @param pos index of AmiVector * @return const AmiVector */ - const AmiVector &operator[](int pos) const; + AmiVector const& operator[](int pos) const; /** * @brief length of AmiVectorArray @@ -341,13 +345,13 @@ class AmiVectorArray { * @param vec vector into which the AmiVectorArray will be flattened. Must * have length equal to number of elements. */ - void flatten_to_vector(std::vector &vec) const; + void flatten_to_vector(std::vector& vec) const; /** * @brief copies data from another AmiVectorArray * @param other data source */ - void copy(const AmiVectorArray &other); + void copy(AmiVectorArray const& other); private: /** main data storage */ @@ -368,11 +372,13 @@ class AmiVectorArray { * @param y another vector with same size as x * @param z result vector of same size as x and y */ -inline void linearSum(realtype a, AmiVector const& x, realtype b, - AmiVector const& y, AmiVector& z) { - N_VLinearSum(a, const_cast(x.getNVector()), - b, const_cast(y.getNVector()), - z.getNVector()); +inline void linearSum( + realtype a, AmiVector const& x, realtype b, AmiVector const& y, AmiVector& z +) { + N_VLinearSum( + a, const_cast(x.getNVector()), b, + const_cast(y.getNVector()), z.getNVector() + ); } /** @@ -382,21 +388,21 @@ inline void linearSum(realtype a, AmiVector const& x, realtype b, * @return dot product of x and y */ inline realtype dotProd(AmiVector const& x, AmiVector const& y) { - return N_VDotProd(const_cast(x.getNVector()), - const_cast(y.getNVector())); + return N_VDotProd( + const_cast(x.getNVector()), + const_cast(y.getNVector()) + ); } } // namespace amici - namespace gsl { /** * @brief Create span from N_Vector * @param nv * @return */ -inline span make_span(N_Vector nv) -{ +inline span make_span(N_Vector nv) { return span(N_VGetArrayPointer(nv), N_VGetLength_Serial(nv)); } } // namespace gsl diff --git a/matlab/@amidata/amidata.m b/matlab/@amidata/amidata.m index b9be294d27..85ff897fed 100644 --- a/matlab/@amidata/amidata.m +++ b/matlab/@amidata/amidata.m @@ -4,11 +4,11 @@ % classdef amidata < handle % AMIDATA provides a data container to pass experimental data to the - % simulation routine for likelihood computation. - % when any of the properties are updated, the class automatically - % checks consistency of dimension and updates related properties and + % simulation routine for likelihood computation. + % when any of the properties are updated, the class automatically + % checks consistency of dimension and updates related properties and % initialises them with NaNs - + properties % number of timepoints nt=0; @@ -37,12 +37,12 @@ % reinitialize states based on fixed parameters after preeq.? reinitializeStates = false; end - + methods function D = amidata(varargin) - % amidata creates an amidata container for experimental data + % amidata creates an amidata container for experimental data % with specified dimensions amidata. - % + % % AMIDATA(amidata) creates a copy of the input container % % AMIDATA(struct) tries to creates an amidata container from the @@ -62,15 +62,15 @@ % AMIDATA(nt,ny,nz,ne,nk) constructs an empty data container with % in the provided dimensions intialised with NaNs % - % + % % % Parameters: % varargin: % % Return values: % - - + + % initialisation via struct if isa(varargin{1},'amidata') if strcmp(class(varargin{1}),class(D)) @@ -112,7 +112,7 @@ end if(isfield(varargin{1},'Sigma_Z')) D.Sigma_Z = varargin{1}.Sigma_Z; - end + end if(isfield(varargin{1},'condition')) D.nk = numel(varargin{1}.condition); D.condition = varargin{1}.condition; @@ -131,7 +131,7 @@ else error('Assignment error: Value for field reinitializeStates must be logical.'); end - end + end elseif(nargin == 5) D.nt = varargin{1}; D.ny = varargin{2}; @@ -141,62 +141,62 @@ end end - + function set.nt(this,nt) this.nt = nt; this.t = 1:nt; this.Y = NaN; this.Sigma_Y = NaN; end - + function set.ny(this,ny) this.ny = ny; this.Y = NaN; this.Sigma_Y = NaN; end - + function set.nz(this,nz) this.nz = nz; this.Z = NaN; this.Sigma_Z = NaN; end - + function set.ne(this,ne) this.ne = ne; this.Z = NaN; this.Sigma_Z = NaN; end - + function set.nk(this,nk) this.nk = nk; this.condition = NaN(nk,1); end - + function set.t(this,value) assert(isnumeric(value),'AMICI:amimodel:t:numeric','t must have a numeric value!') assert(ismatrix(value),'AMICI:amimodel:t:ndims','t must be a two dimensional matrix!') assert(numel(value)==this.nt,'AMICI:amimodel:t:ndims',['t must have ' num2str(this.nt) ' (D.nt) elements!']) this.t = double(value(:)); end - + function set.condition(this,value) assert(isnumeric(value),'AMICI:amimodel:condition:numeric','condition must have a numeric value!') assert(ismatrix(value),'AMICI:amimodel:condition:ndims','condition must be a two dimensional matrix!') assert(numel(value)==this.nk,'AMICI:amimodel:condition:ndims',['condition must have ' num2str(this.nk) ' (D.nk) elements!']) this.condition = double(value(:)); end - + function set.conditionPreequilibration(this,value) assert(isnumeric(value),'AMICI:amimodel:condition:numeric','condition must have a numeric value!') assert(ismatrix(value),'AMICI:amimodel:condition:ndims','condition must be a two dimensional matrix!') assert(numel(value)==this.nk,'AMICI:amimodel:condition:ndims',['condition must have ' num2str(this.nk) ' (D.nk) elements!']) this.conditionPreequilibration = double(value(:)); end - + function set.Y(this,value) assert(ismatrix(value),'AMICI:amimodel:Y:ndims','Y must be a two dimensional matrix!') assert(all(all(or(isnumeric(value),isnan(value)))),'AMICI:amimodel:Y:numeric','Y must have a numeric value!') - + if(all(size(value)==[this.nt this.ny])) this.Y = double(value); elseif(all(size(value)==[this.nt 1])) @@ -209,7 +209,7 @@ error('AMICI:amimodel:Y:size',['Y must have size [' num2str(this.nt) ',' num2str(this.ny) '] ([D.nt,D.ny])!']) end end - + function set.Sigma_Y(this,value) assert(ismatrix(value),'AMICI:amimodel:Sigma_Y:ndims','Sigma_Y must be a two dimensional matrix!') assert(all(all(or(isnumeric(value),isnan(value)))),'AMICI:amimodel:Sigma_Y:numeric','Sigma_Y must have a numeric value!') @@ -225,7 +225,7 @@ error('AMICI:amimodel:Sigma_Y:size',['Sigma_Y must have size [' num2str(this.nt) ',' num2str(this.ny) '] ([D.nt,D.ny])!']) end end - + function set.Z(this,value) assert(ismatrix(value),'AMICI:amimodel:Z:ndims','Z must be a two dimensional matrix!') assert(all(all(or(isnumeric(value),isnan(value)))),'AMICI:amimodel:Z:numeric','Z must have a numeric value!') @@ -241,7 +241,7 @@ error('AMICI:amimodel:Z:size',['Z must have size [' num2str(this.ne) ',' num2str(this.nz) '] ([D.ne,D.nz])!']) end end - + function set.Sigma_Z(this,value) assert(ismatrix(value),'AMICI:amimodel:Sigma_Z:ndims','Sigma_Z must be a two dimensional matrix!') assert(all(all(or(isnumeric(value),isnan(value)))),'AMICI:amimodel:Sigma_Z:numeric','Sigma_Z must have a numeric value!') @@ -258,6 +258,5 @@ end end end - -end +end diff --git a/matlab/@amievent/amievent.m b/matlab/@amievent/amievent.m index 098a4e4372..1ce5d7f2cd 100644 --- a/matlab/@amievent/amievent.m +++ b/matlab/@amievent/amievent.m @@ -5,7 +5,7 @@ classdef amievent % AMIEVENT defines events which later on will be transformed into appropriate % C code - + properties ( GetAccess = 'public', SetAccess = 'private' ) % the trigger function activates the event on every zero crossing @type symbolic trigger = sym.empty(); @@ -17,7 +17,7 @@ % to speed up symbolic computations hflag = logical.empty(); end - + methods function AE = amievent(trigger,bolus,z) % amievent constructs an amievent object from the provided input. @@ -46,7 +46,7 @@ if(numel(AE.trigger)>1) error('The trigger function must be scalar.') end - + if(~isa(bolus,'sym')) if(isa(bolus,'double')) AE.bolus = sym(bolus(:)); @@ -56,7 +56,7 @@ else AE.bolus = bolus; end - + if(~isa(z,'sym')) if(isa(z,'double')) if(~isempty(z)) @@ -67,13 +67,12 @@ end else error('output function must be a symbolic expression') - end + end else AE.z = z; end end - + this = setHflag(this,hflag); end end - diff --git a/matlab/@amifun/amifun.m b/matlab/@amifun/amifun.m index 6743434cb8..e51c43b311 100644 --- a/matlab/@amifun/amifun.m +++ b/matlab/@amifun/amifun.m @@ -5,7 +5,7 @@ classdef amifun % AMIFUN defines functions which later on will be transformed into % appropriate C code - + properties ( GetAccess = 'public', SetAccess = 'public' ) % symbolic definition struct @type symbolic sym = sym([]); @@ -29,7 +29,7 @@ % with respect to parameters sensiflag = logical.empty(); end - + methods function AF = amifun(funstr,model) % amievent constructs an amifun object from the provided input. @@ -38,7 +38,7 @@ % funstr: name of the requested function % model: amimodel object which carries all symbolic % definitions to construct the function - % + % % % Return values: % AF: amifun object @@ -50,26 +50,25 @@ AF = AF.getCVar(); AF = AF.getSensiFlag(); end - + writeCcode_sensi(this,model,fid) - + writeCcode(this,model,fid) - + writeMcode(this,model) - + gccode(this,model,fid) - + [ this ] = getDeps(this,model) - + [ this ] = getArgs(this,model) - + [ this ] = getNVecs(this) - + [ this ] = getCVar(this) - + [ this ] = getSensiFlag(this) [ this, model ] = getSyms(this,model) end end - diff --git a/matlab/@amifun/gccode.m b/matlab/@amifun/gccode.m index a00724b3ba..5670f26886 100644 --- a/matlab/@amifun/gccode.m +++ b/matlab/@amifun/gccode.m @@ -8,17 +8,17 @@ % % Return values: % this: function definition object @type amifun - - + + if(any(any(any(this.sym~=0)))) - + % replace unknown partial derivatives if(model.maxflag) this.sym = subs(this.sym,sym('D([1], am_max)'),sym('D1max')); this.sym = subs(this.sym,sym('D([2], am_max)'),sym('D2max')); this.sym = subs(this.sym,sym('am_max'),sym('max')); end - + % If we have spline, we need to parse them to get derivatives if (model.splineflag) symstr = char(this.sym); @@ -31,7 +31,7 @@ else isDSpline = false; end - + if (isDSpline) [~, nCol] = size(this.sym); for iCol = 1 : nCol @@ -48,7 +48,7 @@ end end end - + cstr = ccode(this.sym); if(~strcmp(cstr(3:4),'t0')) if(any(strcmp(this.funstr,{'J','JB','JDiag','dJydsigma','dJydy','dJzdsigma','dJzdz','dJrzdsigma','dJrzdz','dydx','dzdx','drzdx','M','dfdx'}) )) @@ -60,13 +60,13 @@ else cstr = strrep(cstr,'t0',[this.cvar '_0']); end - + cstr = strrep(cstr,'log','amici::log'); % fix derivatives again (we cant do this before as this would yield % incorrect symbolic expressions cstr = regexprep(regexprep(cstr,'D([0-9]*)([\w]*)\(','D$2\($1,'),'DD([0-9]*)([\w]*)\(','DD$2\($1,'); cstr = strrep(strrep(cstr, 'DDspline', 'DDspline'), 'Dspline', 'Dspline'); - + if (model.splineflag) if (strfind(symstr, 'spline')) % The floating numbers after 't' must be converted to integers @@ -75,11 +75,11 @@ cstr = regexprep(cstr, '([D]*(spline|spline_pos))\((\w+)\,(\w+)\,t\,\w+\.\w+\,', ['amici::$1\($2\,$3\,t\,', num2str(nNodes), '\,']); end end - + if(numel(cstr)>1) - + % fix various function specific variable names/indexes - + cstr = regexprep(cstr,'var_x_([0-9]+)','x[$1]'); cstr = regexprep(cstr,'var_dx_([0-9]+)','dx[$1]'); cstr = regexprep(cstr,'var_sx_([0-9]+)','sx[$1]'); @@ -101,7 +101,7 @@ cstr = regexprep(cstr,'var_sx0_([0-9]+)','sx0[$1]'); cstr = regexprep(cstr,'var_sdx0_([0-9]+)','sdx0[$1]'); cstr = regexprep(cstr,'var_root_([0-9]+)', 'root[$1]'); - + cstr = regexprep(cstr,'var_p_([0-9]+)','p[$1]'); cstr = regexprep(cstr,'var_k_([0-9]+)','k[$1]'); cstr = regexprep(cstr,'h_([0-9]+)','h[$1]'); @@ -114,7 +114,7 @@ cstr = regexprep(cstr,'var_dwdp_([0-9]+)','dwdp[$1]'); cstr = regexprep(cstr,'tmp_J_([0-9]+)','J->data[$1]'); cstr = regexprep(cstr,'tmp_dxdotdp_([0-9]+)','dxdotdp[$1]'); - + cstr = regexprep(cstr,'var_y_([0-9]+)','y[$1]'); cstr = regexprep(cstr,'my_([0-9]+)','my[$1]'); cstr = regexprep(cstr,'var_z_([0-9]+)','z[$1]'); @@ -123,7 +123,7 @@ cstr = regexprep(cstr,'var_srz_([0-9]+)','srz[$1]'); cstr = regexprep(cstr,'var_sy_([0-9]+)','sy[$1]'); cstr = regexprep(cstr,'var_sz_([0-9]+)','sz[$1]'); - + cstr = regexprep(cstr,'var_dydx[_\[]*([0-9\+\*]+)[\]]*','dydx[$1]'); % matches both _... and [...] cstr = regexprep(cstr,'var_dzdx[_\[]*([0-9\+\*]+)[\]]*','dzdx[$1]'); cstr = regexprep(cstr,'var_drzdx[_\[]*([0-9\+\*]+)[\]]*','drzdx[$1]'); @@ -139,7 +139,7 @@ cstr = regexprep(cstr,'var_sigma_z_([0-9]+)','sigmaz[$1]'); cstr = regexprep(cstr,'var_dsigma_zdp_([0-9]+)',['dsigmazdp[$1]']); cstr = regexprep(cstr,'var_dsigma_ydp_([0-9]+)',['dsigmaydp[$1]']); - + cstr = regexprep(cstr,'var_dsdydp_([0-9]+)',['dsigmaydp[ip*' num2str(model.ny) ' + $1]']); cstr = regexprep(cstr,'var_dsdzdp_([0-9]+)',['dsigmazdp[ip*' num2str(model.nz) ' + $1]']); cstr = regexprep(cstr,'var_Jy_([0-9]+)','nllh[$1]'); @@ -153,7 +153,7 @@ cstr = regexprep(cstr,'var_dJrzdsigma[_\[]*([0-9\+\*]+)[\]]*','dJrzdsigma[$1]'); cstr = regexprep(cstr,'var_JDiag[_\[]*([0-9\+\*]+)[\]]*','JDiag[$1]'); end - + %% % print to file fprintf(fid,[cstr '\n']); diff --git a/matlab/@amifun/getArgs.m b/matlab/@amifun/getArgs.m index e59c10173c..afb50802e0 100644 --- a/matlab/@amifun/getArgs.m +++ b/matlab/@amifun/getArgs.m @@ -10,7 +10,7 @@ % Return values: % this: updated function definition object @type amifun % - + if(strcmp(model.wtype,'iw')) dx = ', const realtype *dx'; sdx = ', const realtype *sdx'; @@ -24,7 +24,7 @@ M = ''; cj = ''; end - + switch(this.funstr) case 'xdot' this.argstr = ['(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h' dx ', const realtype *w)']; @@ -108,15 +108,15 @@ case 'dJrzdsigma' this.argstr = '(double *dJrzdsigma, const int iz, const realtype *p, const realtype *k, const double *rz, const double *sigmaz)'; case 'w' - this.argstr = '(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl)'; + this.argstr = '(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl)'; case 'dwdp' - this.argstr = '(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl)'; + this.argstr = '(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl)'; case 'dwdx' - this.argstr = '(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl)'; + this.argstr = '(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl)'; case 'M' this.argstr = '(realtype *M, const realtype t, const realtype *x, const realtype *p, const realtype *k)'; otherwise %nothing end - + end diff --git a/matlab/@amifun/getCVar.m b/matlab/@amifun/getCVar.m index 323303925d..43d5ecc2ed 100644 --- a/matlab/@amifun/getCVar.m +++ b/matlab/@amifun/getCVar.m @@ -5,7 +5,7 @@ % % Return values: % this: updated function definition object @type amifun - + switch(this.funstr) case 'JSparse' this.cvar = 'var_JSparse'; @@ -17,4 +17,3 @@ this.cvar = ['var_' this.funstr]; end end - diff --git a/matlab/@amifun/getDeps.m b/matlab/@amifun/getDeps.m index 800f4ccb98..5249fde319 100644 --- a/matlab/@amifun/getDeps.m +++ b/matlab/@amifun/getDeps.m @@ -6,7 +6,7 @@ % % Return values: % this: updated function definition object @type amifun - + switch(this.funstr) case 'xdot' if(strcmp(model.wtype,'iw')) @@ -14,205 +14,205 @@ else this.deps = {'p','x','k'}; end - + case 'dfdx' this.deps = {'xdot','x','dwdx'}; - + case 'J' if(strcmp(model.wtype,'iw')) this.deps = {'dfdx','M','x','xdot'}; else this.deps = {'xdot','x','dwdx'}; end - + case 'dxdotdp' this.deps = {'xdot','p','dwdp'}; - + case 'sx0' - this.deps = {'x0','p'}; - + this.deps = {'x0','p'}; + case 'sdx0' - this.deps = {'dx0','p'}; - + this.deps = {'dx0','p'}; + case 'sxdot' if(strcmp(model.wtype,'iw')) this.deps = {'dfdx','M','dxdotdp','sdx','sx'}; else this.deps = {'J','dxdotdp','sx'}; end - + case 'dydx' this.deps = {'y','x'}; - + case 'dydp' this.deps = {'y','p'}; - + case 'sy' this.deps = {'dydp','dydx','sx'}; - + case 'Jv' this.deps = {'J'}; - + case 'JvB' this.deps = {'J'}; - + case 'xBdot' if(strcmp(model.wtype,'iw')) this.deps = {'J','M','xB','dxB'}; else this.deps = {'J','xB'}; end - + case 'qBdot' this.deps = {'dxdotdp','xB'}; - + case 'dsigma_ydp' this.deps = {'sigma_y','p'}; - + case 'dsigma_zdp' this.deps = {'sigma_z','p'}; - + case 'root' this.deps = {'x','k','p'}; - + case 'drootdp' this.deps = {'root','p','drootdx','sx'}; - + case 'drzdp' this.deps = {'rz','p',}; - + case 'drootdx' this.deps = {'root','x'}; - + case 'drzdx' this.deps = {'rz','x',}; - + case 'drootdt' % w is necessary for xdot_noopt this.deps = {'root','drootdx','xdot','w'}; - + case 'deltax' this.deps = {'x','k','p'}; - + case 'ddeltaxdp' this.deps = {'deltax','p'}; - + case 'ddeltaxdx' this.deps = {'deltax','x'}; - + case 'ddeltaxdt' this.deps = {'deltax'}; - + case 'deltasx' this.deps = {'deltax','deltaxdot','ddeltaxdx','ddeltaxdp','ddeltaxdt','dtaudp','xdot','sx','stau'}; - + case 'deltaqB' this.deps = {'ddeltaxdp','xB'}; - + case 'deltaxB' this.deps = {'deltax','dtaudp','xdot','xB','ddeltaxdx'}; - + case 'z' this.deps = {'x','k','p'}; - + case 'rz' this.deps = {'z','root'}; - + case 'srz' this.deps = {'rz','root','drootdx','drootdp','sx'}; - + case 'dzdp' this.deps = {'z','p','dtaudp'}; - + case 'dzdx' this.deps = {'z','x','dtaudx'}; - + case 'dzdt' % w is necessary for xdot_noopt this.deps = {'z','x','xdot','w'}; - + case 'sz' this.deps = {'dzdp','dzdx','dzdt','sx','dtaudp','stau'}; - + case 'sz_tf' this.deps = {'dzdp','dzdx','sx'}; - + case 'dtaudp' this.deps = {'drootdp','drootdt'}; - + case 'dtaudx' this.deps = {'drootdx','drootdt'}; - + case 'stau' this.deps = {'sroot','drootdt'}; - + case 'sroot' this.deps = {'drootdp','drootdx','sx'}; - + case 'x0' this.deps = {'p','k','x'}; - + case 'JBand' this.deps = {'J'}; - + case 'JBandB' this.deps = {'JB'}; - + case 'JSparse' this.deps = {'J'}; - + case 'y' this.deps = {'x','p','k'}; - + case 'sigma_y' this.deps = {'p','k'}; - + case 'sigma_z' this.deps = {'p','k'}; - + case 'rhs' this.deps = {'xdot'}; - + case 'dx0' this.deps = {'x','p','k'}; - + case 'M' this.deps = {'x','p','k'}; - + case 'x' this.deps = {}; - + case 'dx' this.deps = {}; - + case 'xB' this.deps = {}; - + case 'dxB' this.deps = {}; - + case 'k' this.deps = {}; - + case 'p' this.deps = {}; - + case 'sx' this.deps = {}; - + case 'sdx' this.deps = {}; - + case 'deltaxdot' this.deps = {'xdot'}; - + case 'Jy' this.deps = {'y','sigma_y'}; case 'dJydy' this.deps = {'Jy','y'}; case 'dJydsigma' this.deps = {'Jy','sigma_y'}; - + case 'Jz' this.deps = {'z','sigma_z'}; case 'dJzdz' @@ -225,14 +225,14 @@ this.deps = {'Jrz','x'}; case 'dJrzdsigma' this.deps = {'Jrz','sigma_z'}; - + case 'w' this.deps = {'xdot'}; case 'dwdp' this.deps = {'w','p'}; case 'dwdx' this.deps = {'w','x'}; - + case 's2root' this.deps = {'sroot'}; @@ -240,4 +240,3 @@ error(['unknown function string: ' this.funstr ]) end end - diff --git a/matlab/@amifun/getNVecs.m b/matlab/@amifun/getNVecs.m index 4b75133a0e..1eb6670c57 100644 --- a/matlab/@amifun/getNVecs.m +++ b/matlab/@amifun/getNVecs.m @@ -1,6 +1,6 @@ function this = getNVecs(this) % getfunargs populates the nvecs property with the names of the - % N_Vector elements which are required in the execution of the function + % N_Vector elements which are required in the execution of the function % (if applicable). the information is directly extracted from the % argument string % @@ -9,18 +9,18 @@ % Return values: % this: updated function definition object @type amifun % - + vecs = {'x,','dx,','sx,','*sx,','sdx,','xB,','dxB,',... '*sxdot,','sxdot,','xdot,','xBdot,','qBdot,',... 'x0,','dx0,','*sx0,','*sdx0,',... 'v,','vB,','JDiag,','Jv,','JvB,',... 'xdot_old,'}; - + this.nvecs = {}; for iv = 1:length(vecs) if strfind(this.argstr,['N_Vector ' vecs{iv}]) this.nvecs = [this.nvecs,vecs{iv}(1:(end-1))]; end end - -end \ No newline at end of file + +end diff --git a/matlab/@amifun/getSensiFlag.m b/matlab/@amifun/getSensiFlag.m index 9d646509cb..195f119184 100644 --- a/matlab/@amifun/getSensiFlag.m +++ b/matlab/@amifun/getSensiFlag.m @@ -5,66 +5,66 @@ % % Return values: % this: updated function definition object @type amifun - + switch(this.funstr) case 'dxdotdp' this.sensiflag = true; - + case 'sx0' - this.sensiflag = true; - + this.sensiflag = true; + case 'sdx0' this.sensiflag = true; - - + + case 'dydp' this.sensiflag = true; - + case 'sy' this.sensiflag = true; - + case 'qBdot' this.sensiflag = true; - + case 'dsigma_ydp' this.sensiflag = true; - + case 'dsigma_zdp' this.sensiflag = true; - + case 'drzdp' this.sensiflag = true; - + case 'ddeltaxdp' this.sensiflag = true; - + case 'deltasx' this.sensiflag = true; - + case 'deltaqB' this.sensiflag = true; - + case 'dzdp' this.sensiflag = true; - + case 'sz' this.sensiflag = true; - + case 'dtaudp' this.sensiflag = true; - + case 'stau' this.sensiflag = true; - + case 'sroot' this.sensiflag = true; - + case 'srz' this.sensiflag = true; - + case 'sx' this.sensiflag = true; - + case 'sdx' this.sensiflag = true; @@ -72,4 +72,3 @@ this.sensiflag = false; end end - diff --git a/matlab/@amifun/getSyms.m b/matlab/@amifun/getSyms.m index ad741a8185..77c23f75af 100644 --- a/matlab/@amifun/getSyms.m +++ b/matlab/@amifun/getSyms.m @@ -7,19 +7,19 @@ % Return values: % this: updated function definition object @type amifun % model: updated model definition object @type amimodel - + % store often used variables for ease of notation, dependencies should % ensure that these variables are defined - + persistent x p sx w ndw jacw - + nx = model.nx; nevent = model.nevent; np = model.np; nk = model.nk; nz = model.nz; ny = model.ny; - + fprintf([this.funstr ' | ']) switch(this.funstr) case 'x' @@ -32,7 +32,7 @@ % transform into symbolic expression this.sym = sym(xs); x = this.sym; - + case 'dx' % create cell array of same size dxs = cell(nx,1); @@ -42,7 +42,7 @@ end % transform into symbolic expression this.sym = sym(dxs); - + case 'p' % create cell array of same size ps = cell(np,1); @@ -53,7 +53,7 @@ % transform into symbolic expression this.sym = sym(ps); p = this.sym; - + case 'k' % create cell array of same size ks = cell(nk,1); @@ -63,7 +63,7 @@ end % transform into symbolic expression this.sym = sym(ks); - + case 'sx' % create cell array of same size sxs = cell(nx,1); @@ -74,7 +74,7 @@ % transform into symbolic expression this.sym = repmat(sym(sxs),[1,np]); sx = this.sym; - + case 'sdx' % create cell array of same size sdx = cell(nx,np); @@ -86,7 +86,7 @@ end % transform into symbolic expression this.sym = sym(sdx); - + case 'xB' % create cell array of same size xBs = cell(nx,1); @@ -96,7 +96,7 @@ end % transform into symbolic expression this.sym = sym(xBs); - + case 'dxB' % create cell array of same size dxBs = cell(nx,1); @@ -106,14 +106,14 @@ end % transform into symbolic expression this.sym = sym(dxBs); - + case 'y' this.sym = model.sym.y; % replace unify symbolic expression this = unifySyms(this,model); this = makeStrSymsFull(this); - - + + % activate splines for iy = 1:ny if(not(all([model.splineflag,model.minflag,model.maxflag]))) @@ -128,41 +128,41 @@ model.minflag = true; end end - end - + end + case 'x0' this.sym = model.sym.x0; % replace unify symbolic expression this = unifySyms(this,model); - + case 'dx0' this.sym = model.sym.dx0; % replace unify symbolic expression this = unifySyms(this,model); - + case 'sigma_y' this.sym = model.sym.sigma_y; this = makeStrSymsFull(this); % replace unify symbolic expression this = unifySyms(this,model); - + case 'sigma_z' this.sym = model.sym.sigma_z; this = makeStrSymsFull(this); % replace unify symbolic expression this = unifySyms(this,model); - + case 'M' this.sym = sym(model.sym.M); % replace unify symbolic expression this = unifySyms(this,model); this = makeStrSyms(this); - + case 'xdot' this.sym = model.sym.xdot; % replace unify symbolic expression this = unifySyms(this,model); - + if(strcmp(model.wtype,'iw')) if(size(this.sym,2)>size(this.sym,1)) this.sym = -transpose(model.fun.M.sym*model.fun.dx.sym)+this.sym; @@ -170,7 +170,7 @@ this.sym = -model.fun.M.sym*model.fun.dx.sym+this.sym; end end - + % create cell array of same size xdots = cell(nx,1); xdot_olds = cell(nx,1); @@ -181,7 +181,7 @@ end this.strsym = sym(xdots); this.strsym_old = sym(xdot_olds); - + % activate splines for ix = 1:nx if(not(all([model.splineflag,model.minflag,model.maxflag]))) @@ -197,7 +197,7 @@ end end end - + case 'w' optimize = getoptimized(optsym(model.fun.xdot.sym)); tmpxdot = sym(char(optimize(end))); @@ -215,10 +215,10 @@ end % model.nw = 0; % nw = 0; -% this.sym = sym(zeros(0,1)); - +% this.sym = sym(zeros(0,1)); + + - ws = cell(nw,1); ts = cell(nw,1); % fill cell array @@ -249,7 +249,7 @@ jacx = jacobian(model.fun.w.sym,x); this.sym = jacx; for idw = 1:ndw - this.sym = this.sym + (jacw^idw)*jacx; % this part is only to get the right nonzero entries + this.sym = this.sym + (jacw^idw)*jacx; % this part is only to get the right nonzero entries end % fill cell array idx_w = find(this.sym); @@ -270,13 +270,13 @@ this.sym = sym(zeros(0,nx)); this.strsym = sym(zeros(0,nx)); end - + case 'dwdp' if(length(model.fun.w.sym)>0) jacp = jacobian(model.fun.w.sym,p); this.sym = jacp; for idw = 1:ndw - this.sym = this.sym + (jacw^idw)*jacp; % this part is only to get the right nonzero entries + this.sym = this.sym + (jacw^idw)*jacp; % this part is only to get the right nonzero entries end % fill cell array idx_w = find(this.sym); @@ -295,11 +295,11 @@ this.sym = sym(zeros(0,nx)); this.strsym = sym(zeros(0,nx)); end - + case 'dfdx' this.sym=jacobian(model.fun.xdot.sym,x) + jacobian(model.fun.xdot.sym,w)*model.fun.dwdx.strsym; this = makeStrSyms(this); - + case 'J' if(strcmp(model.wtype,'iw')) syms cj @@ -313,15 +313,15 @@ this.sym_noopt = this.sym; end end - + this = makeStrSymsSparse(this); - - - + + + case 'JDiag' this.sym = diag(model.fun.J.sym); this = makeStrSyms(this); - + case 'dxdotdp' if(~isempty(w)) this.sym=jacobian(model.fun.xdot.sym,p) + jacobian(model.fun.xdot.sym,w)*model.fun.dwdp.strsym; @@ -330,7 +330,7 @@ this.sym=jacobian(model.fun.xdot.sym,p); this.sym_noopt = this.sym; end - + %% % build short strings for reuse of dxdotdp % create cell array of same size @@ -341,13 +341,13 @@ end % create full symbolic array this.strsym = sym(dxdotdps); - + case 'sx0' this.sym=jacobian(model.fun.x0.sym,p); - + case 'sdx0' this.sym=jacobian(model.fun.dx0.sym,p); - + case 'sxdot' if(np>0) if(strcmp(model.wtype,'iw')) @@ -358,19 +358,19 @@ else this.sym = sym(zeros(size(sx,1),0)); end - + case 'dydx' this.sym=jacobian(model.fun.y.sym,x); % create cell array of same sizex this.strsym = sym(zeros(ny,nx)); % fill cell array this = makeStrSyms(this); - + case 'dydp' this.sym=jacobian(model.fun.y.sym,p); % create cell array of same size this = makeStrSyms(this); - + case 'xBdot' if(strcmp(model.wtype,'iw')) syms t @@ -378,7 +378,7 @@ else this.sym = model.fun.JB.sym * model.fun.xB.sym; end - + case 'qBdot' % If we do second order adjoints, we have to augment if (model.nxtrue < nx) @@ -388,7 +388,7 @@ this.sym(ig,:) = ... -transpose(model.fun.xB.sym(1:model.nxtrue)) * model.fun.dxdotdp.sym((ig-1)*model.nxtrue+1 : ig*model.nxtrue, :) ... -transpose(model.fun.xB.sym((ig-1)*model.nxtrue+1 : ig*model.nxtrue)) * model.fun.dxdotdp.sym(1:model.nxtrue, :); - end + end else this.sym = -transpose(model.fun.xB.sym)*model.fun.dxdotdp.sym; end @@ -396,7 +396,7 @@ case 'dsigma_ydp' this.sym = jacobian(model.fun.sigma_y.sym,p); this = makeStrSyms(this); - + case 'dsigma_zdp' if(nz>0) this.sym = jacobian(model.fun.sigma_z.sym,p); @@ -404,7 +404,7 @@ this.sym = sym(zeros(model.nz,np)); end this = makeStrSyms(this); - + case 'root' if(nevent>0) this.sym = transpose([model.event.trigger]); @@ -426,20 +426,20 @@ end end end - + case 'drootdp' this.sym = jacobian(model.fun.root.sym,p); - + case 'drootdx' this.sym = jacobian(model.fun.root.sym,x); - + case 'drootdt' % noopt is important here to get derivatives right this.sym = diff(model.fun.root.sym,'t') + model.fun.drootdx.sym*model.fun.xdot.sym_noopt; - + case 'sroot' this.sym = model.fun.drootdp.sym + model.fun.drootdx.sym*sx; - + case 'srz' if(isfield(model.sym,'rz')) % user defined input or from augmentation this.sym = jacobian(model.fun.rz.sym,p) + jacobian(model.fun.rz.sym,x)*sx; @@ -448,7 +448,7 @@ this.sym(iz,:) = model.fun.sroot.sym(model.z2event(iz),:); end end - + case 's2root' switch(model.o2flag) case 1 @@ -459,25 +459,25 @@ vec = model.sym.k((end-np+1):end); end for ievent = 1:nevent - + this.sym(ievent,:,:) = (jacobian(model.fun.sroot.sym(ievent,:),p) + jacobian(model.fun.sroot.sym(ievent,:),x(1:model.nxtrue))*sx(1:model.nxtrue,:) + jacobian(model.fun.sroot.sym(ievent,:),x(1:model.nxtrue))*sx(1:model.nxtrue,:))*vec; for ix = 1:model.nxtrue this.sym(ievent,:,:) = this.sym(ievent,:,:) + model.fun.drootdx.sym(ievent,ix)*s2x(ix,:,:); end end - + case 'dtaudp' this.sym = sym(zeros(nevent,np)); for ievent = 1:nevent this.sym(ievent,:) = - model.fun.drootdp.sym(ievent,:)/model.fun.drootdt.sym(ievent); end - + case 'dtaudx' this.sym = sym(zeros(nevent,nx)); for ievent = 1:nevent this.sym(ievent,:) = - model.fun.drootdx.sym(ievent,:)/model.fun.drootdt.sym(ievent); end - + case 'stau' this.sym = sym(zeros(nevent,np)); for ievent = 1:nevent @@ -493,7 +493,7 @@ staus = sym(staus); % multiply this.strsym = staus; - + case 'deltax' if(nevent>0) this.sym = [model.event.bolus]; @@ -501,16 +501,16 @@ else this.sym = sym(zeros(0,1)); end - + case 'deltaxdot' this.sym = model.fun.xdot.strsym-model.fun.xdot.strsym_old; - + case 'ddeltaxdp' this.sym = sym(zeros(nx,nevent,np)); for ievent = 1:nevent this.sym(:,ievent,:) = jacobian(model.fun.deltax.sym(:,ievent),p); end - + case 'ddeltaxdx' this.sym = sym(zeros(nx,nevent,nx)); if(nx>0) @@ -518,27 +518,27 @@ this.sym(:,ievent,:) = jacobian(model.fun.deltax.sym(:,ievent),x); end end - + case 'ddeltaxdt' this.sym = diff(model.fun.deltax.sym,'t'); - + case 'deltasx' - + if(nevent>0) for ievent = 1:nevent - + % dtdp = (1/drdt)*drdp dtdp = model.fun.stau.strsym; % this 1 here is correct, we explicitely do not want ievent here as the actual stau_tmp will only have dimension np - + % if we are just non-differentiable and but not - % discontinuous we can ignore some of the terms! + % discontinuous we can ignore some of the terms! if(any(logical(model.fun.deltax.sym(:,ievent)~=0))) % dxdp = dx/dt*dt/dp + dx/dp dxdp = sym(zeros(nx,np)); for ix = 1:nx dxdp(ix,:) = model.fun.xdot.sym(ix)*dtdp + sx(ix,:); end - + this.sym(:,:,ievent) = ... + permute(model.fun.ddeltaxdx.sym(:,ievent,:),[1 3 2])*dxdp ... + model.fun.ddeltaxdt.sym(:,ievent)*dtdp ... @@ -553,7 +553,7 @@ end end end - + case 'deltaqB' if (model.nxtrue < nx) ng_tmp = round(nx / model.nxtrue); @@ -561,21 +561,21 @@ else this.sym = sym(zeros(np,nevent)); end - + for ievent = 1:nevent this.sym(1:np,ievent) = -transpose(model.fun.xB.sym)*squeeze(model.fun.ddeltaxdp.sym(:,ievent,:)); % This is just a very quick fix. Events in adjoint systems % have to be implemented in a way more rigorous way later % on... Some day... end - + case 'deltaxB' this.sym = sym(zeros(nx,nevent)); for ievent = 1:nevent this.sym(:,ievent) = -transpose(squeeze(model.fun.ddeltaxdx.sym(:,ievent,:)))*model.fun.xB.sym; end - - + + case 'z' if(nevent>0) this.sym = transpose([model.event.z]); @@ -594,7 +594,7 @@ end end this = makeStrSymsFull(this); - + case 'rz' this.sym = sym(zeros(size(model.fun.z.sym))); if(isfield(model.sym,'rz')) @@ -606,38 +606,38 @@ end this = unifySyms(this,model); this = makeStrSymsFull(this); - + case 'dzdp' this.sym = jacobian(model.fun.z.sym,p); - + for iz = 1:nz this.sym(iz,:) = this.sym(iz,:) + diff(model.fun.z.sym(iz),sym('t'))*model.fun.dtaudp.sym(model.z2event(iz),:); end % create cell array of same size this = makeStrSyms(this); - + case 'drzdp' this.sym = jacobian(model.fun.rz.sym,p); this = makeStrSyms(this); - + case 'dzdx' this.sym = jacobian(model.fun.z.sym,x); for iz = 1:nz this.sym(iz,:) = this.sym(iz,:)+ diff(model.fun.z.sym(iz),sym('t'))*model.fun.dtaudx.sym(model.z2event(iz),:); end this = makeStrSyms(this); - + case 'drzdx' this.sym = jacobian(model.fun.rz.sym,x); this = makeStrSyms(this); - + case 'dzdt' if(nz>0) this.sym = diff(model.fun.z.sym,'t')+jacobian(model.fun.z.sym,x(1:model.nxtrue))*model.fun.xdot.sym_noopt(1:model.nxtrue); else this.sym = sym.empty(); end - + case 'sz' this.sym = sym(zeros(nz,np)); tmpsym = sym(zeros(nz-model.nztrue,np)); @@ -659,15 +659,15 @@ % symmetrise it and add it later on. % also the dzdp part contains second order sensitivities and the drootdt part does not (this leads % to 1:model.nxtrue indexing) - - + + if(model.o2flag==1) tmpsym(iz-model.nztrue,:) = jacobian(1/model.fun.drootdt.sym(model.z2event(iz)),p)*model.fun.z.sym(iz)*model.fun.drootdt.sym(model.z2event(iz)) ... + jacobian(1/model.fun.drootdt.sym(model.z2event(iz)),x(1:model.nxtrue))*sx(1:model.nxtrue,:)*model.fun.z.sym(iz)*model.fun.drootdt.sym(model.z2event(iz)); else - error('sz for directional second order sensis was never implemented and I do not know how to, you are on your own here.'); + error('sz for directional second order sensis was never implemented and I do not know how to, you are on your own here.'); end - + this.sym(iz,:) = ... + jacobian(model.fun.z.sym(iz)*model.fun.drootdt.sym(model.z2event(iz)),p)/model.fun.drootdt.sym(model.z2event(iz)) ... + jacobian(model.fun.z.sym(iz)*model.fun.drootdt.sym(model.z2event(iz)),x)*sx/model.fun.drootdt.sym(model.z2event(iz)) ... @@ -687,7 +687,7 @@ % you might not believe this, but this matrix should (and hopefully will) actually be symmetric ;) end end - + % create cell array of same size szs = cell(nz,np); % fill cell array @@ -698,14 +698,14 @@ end % transform into symbolic expression this.strsym = sym(szs); - + case 'JBand' %do nothing case 'JBandB' %do nothing case 'JSparse' %do nothing - + case 'Jy' this.sym = model.sym.Jy; % replace unify symbolic expression @@ -760,7 +760,7 @@ for iz = 1 : model.nztrue this.sym(iz,:,:) = jacobian(model.fun.Jrz.sym(iz,:),model.fun.sigma_z.strsym); end - + otherwise error('unknown function name') end @@ -821,4 +821,4 @@ else out = in; end -end \ No newline at end of file +end diff --git a/matlab/@amifun/writeCcode.m b/matlab/@amifun/writeCcode.m index d74709e958..b03232cd95 100644 --- a/matlab/@amifun/writeCcode.m +++ b/matlab/@amifun/writeCcode.m @@ -94,4 +94,4 @@ function writeCcode(this,model,fid) end -end \ No newline at end of file +end diff --git a/matlab/@amifun/writeCcode_sensi.m b/matlab/@amifun/writeCcode_sensi.m index b9238d3fc5..c660a5e43b 100644 --- a/matlab/@amifun/writeCcode_sensi.m +++ b/matlab/@amifun/writeCcode_sensi.m @@ -5,10 +5,10 @@ function writeCcode_sensi(this,model,fid) % Parameters: % model: model defintion object @type amimodel % fid: file id in which the final expression is written @type fileid -% +% % Return values: % void - + np = model.np; ng = model.ng; @@ -69,7 +69,7 @@ function writeCcode_sensi(this,model,fid) end end end -else +else nonzero = this.sym ~=0; if(any(any(nonzero))) tmpfun = this; @@ -88,4 +88,4 @@ function writeCcode_sensi(this,model,fid) end end end -end \ No newline at end of file +end diff --git a/matlab/@amifun/writeMcode.m b/matlab/@amifun/writeMcode.m index d78af5e628..4b5fb126c9 100644 --- a/matlab/@amifun/writeMcode.m +++ b/matlab/@amifun/writeMcode.m @@ -20,8 +20,8 @@ function writeMcode(this,model) end this.sym_noopt = subs(this.sym_noopt,h_vars,h_rep); end - + ami_mfun(this.sym_noopt, 'file', fullfile(model.wrap_path,'models',... model.modelname,[ this.funstr '_',model.modelname,'.m']), ... 'vars', {'t',model.fun.x.sym,model.fun.p.sym,model.fun.k.sym},'varnames',{'t','x','p','k'}); -end \ No newline at end of file +end diff --git a/matlab/@amimodel/amimodel.m b/matlab/@amimodel/amimodel.m index cef415bdfe..1aed263556 100644 --- a/matlab/@amimodel/amimodel.m +++ b/matlab/@amimodel/amimodel.m @@ -4,7 +4,7 @@ % classdef amimodel < handle % AMIMODEL carries all model definitions including functions and events - + properties ( GetAccess = 'public', SetAccess = 'private' ) % symbolic definition struct @type struct sym = struct.empty(); @@ -82,7 +82,7 @@ % storage for flags determining recompilation of individual % functions cfun = struct.empty(); - % flag which identifies augmented models + % flag which identifies augmented models % 0 indicates no augmentation % 1 indicates augmentation by first order sensitivities (yields % second order sensitivities) @@ -90,7 +90,7 @@ % order sensitivities (yields hessian-vector product) o2flag = 0; end - + properties ( GetAccess = 'public', SetAccess = 'public' ) % vector that maps outputs to events z2event = double.empty(); @@ -108,7 +108,7 @@ % number of derivatives of derived variables w, dwdp @type int ndwdp = 0; end - + methods function AM = amimodel(symfun,modelname) % amimodel initializes the model object based on the provided @@ -138,17 +138,17 @@ else error('invalid input symfun') end - + if(isfield(model,'sym')) AM.sym = model.sym; else error('symbolic definitions missing in struct returned by symfun') end - - - + + + props = fields(model); - + for j = 1:length(props) if(~strcmp(props{j},'sym')) % we already checked for the sym field if(isfield(model,props{j})) @@ -174,7 +174,7 @@ end end end - + AM.modelname = modelname; % set path and create folder AM.wrap_path=fileparts(fileparts(fileparts(mfilename('fullpath')))); @@ -192,7 +192,7 @@ AM.nztrue = AM.nz; end AM.nevent = length(AM.event); - + % check whether we have a DAE or ODE if(isfield(AM.sym,'M')) AM.wtype = 'iw'; % DAE @@ -201,7 +201,7 @@ end end end - + function updateRHS(this,xdot) % updateRHS updates the private fun property .fun.xdot.sym % (right hand side of the differential equation) @@ -214,7 +214,7 @@ function updateRHS(this,xdot) this.fun.xdot.sym_noopt = this.fun.xdot.sym; this.fun.xdot.sym = xdot; end - + function updateModelName(this,modelname) % updateModelName updates the modelname % @@ -225,7 +225,7 @@ function updateModelName(this,modelname) % void this.modelname = modelname; end - + function updateWrapPath(this,wrap_path) % updateModelName updates the modelname % @@ -236,38 +236,37 @@ function updateWrapPath(this,wrap_path) % void this.wrap_path = wrap_path; end - + parseModel(this) - + generateC(this) - + generateRebuildM(this) compileC(this) - + generateM(this,amimodelo2) - + getFun(this,HTable,funstr) - + makeEvents(this) - + makeSyms(this) - + cflag = checkDeps(this,HTable,deps) - + HTable = loadOldHashes(this) - + modelo2 = augmento2(this) - + modelo2vec = augmento2vec(this) - + end - + methods(Static) compileAndLinkModel(modelname, modelSourceFolder, coptim, debug, funs, cfun) - + generateMatlabWrapper(nx, ny, np, nk, nz, o2flag, amimodelo2, wrapperFilename, modelname, pscale, forward, adjoint) end end - diff --git a/matlab/@amimodel/augmento2.m b/matlab/@amimodel/augmento2.m index baf51b70fe..e2a0852ebe 100644 --- a/matlab/@amimodel/augmento2.m +++ b/matlab/@amimodel/augmento2.m @@ -8,9 +8,9 @@ % Return values: % this: augmented system which contains symbolic definition of the % original system and its sensitivities @type amimodel - + syms Sx Sdot Sy S0 - + augmodel.nxtrue = length(this.sym.x); % number of states augmodel.nytrue = length(this.sym.y); % number of observables if(this.nevent>0) @@ -19,7 +19,7 @@ augmodel.nztrue = 0; end np = this.np; - + % augment states Sx = sym(zeros(length(this.sym.x),np)); for j = 1:length(this.sym.x) @@ -29,10 +29,10 @@ end end Sdot = jacobian(this.sym.xdot,this.sym.x)*Sx+jacobian(this.sym.xdot,this.sym.p); - + % augment output Sy = jacobian(this.sym.y,this.sym.x)*Sx+jacobian(this.sym.y,this.sym.p); - + % generate deltasx this.getFun([],'deltasx'); this.getFun([],'sz'); @@ -64,7 +64,7 @@ end augmodel.event(ievent) = amievent(this.event(ievent).trigger,bolusnew,znew); end - + % augment likelihood this.getFun([],'dsigma_ydp'); this.getFun([],'y'); @@ -79,7 +79,7 @@ SJy = jacobian(this.fun.Jy.sym,this.sym.p) ... + jacobian(this.fun.Jy.sym,this.fun.sigma_y.strsym)*this.fun.dsigma_ydp.sym ... + jacobian(this.fun.Jy.sym,this.fun.y.strsym)*aug_y_strsym; - + this.getFun([],'dsigma_zdp'); this.getFun([],'rz'); this.getFun([],'Jz'); @@ -92,7 +92,7 @@ SJz = jacobian(this.fun.Jz.sym,this.sym.p); if(~isempty(this.fun.sigma_z.strsym)) SJz = SJz + jacobian(this.fun.Jz.sym,this.fun.sigma_z.strsym)*this.fun.dsigma_zdp.sym ... - + jacobian(this.fun.Jz.sym,this.fun.z.strsym)*aug_z_strsym; + + jacobian(this.fun.Jz.sym,this.fun.z.strsym)*aug_z_strsym; end this.getFun([],'Jrz'); tmp = arrayfun(@(x) sym(['var_rz_' num2str(x)]),0:(augmodel.nztrue*(1+np)-1),'UniformOutput',false); @@ -104,14 +104,14 @@ SJrz = jacobian(this.fun.Jrz.sym,this.sym.p); if(~isempty(this.fun.sigma_z.strsym)) SJrz = SJrz + jacobian(this.fun.Jrz.sym,this.fun.sigma_z.strsym)*this.fun.dsigma_zdp.sym ... - + jacobian(this.fun.Jrz.sym,this.fun.rz.strsym)*aug_rz_strsym; + + jacobian(this.fun.Jrz.sym,this.fun.rz.strsym)*aug_rz_strsym; end - + % augment sigmas this.getFun([],'sigma_y'); this.getFun([],'sigma_z'); S0 = jacobian(this.sym.x0,this.sym.p); - + augmodel.sym.x = [this.sym.x;Sx(:)]; augmodel.sym.xdot = [this.sym.xdot;Sdot(:)]; augmodel.sym.f = augmodel.sym.xdot; @@ -127,7 +127,7 @@ augmodel.sym.k = this.sym.k; augmodel.sym.sigma_y = [transpose(this.sym.sigma_y(:)), reshape(transpose(this.fun.dsigma_ydp.sym), [1,numel(this.fun.dsigma_ydp.sym)])]; augmodel.sym.sigma_z = [transpose(this.sym.sigma_z(:)), reshape(transpose(this.fun.dsigma_zdp.sym), [1,numel(this.fun.dsigma_zdp.sym)])]; - + modelo2 = amimodel(augmodel,[this.modelname '_o2']); modelo2.o2flag = 1; modelo2.debug = this.debug; diff --git a/matlab/@amimodel/augmento2vec.m b/matlab/@amimodel/augmento2vec.m index 48df87e9c3..09ba3000ad 100644 --- a/matlab/@amimodel/augmento2vec.m +++ b/matlab/@amimodel/augmento2vec.m @@ -8,38 +8,38 @@ % Return values: % modelo2vec: augmented system which contains symbolic definition of the % original system and its sensitivities @type amimodel - + syms Sx Sdot Sy S0 - - + + augmodel.nxtrue = length(this.sym.x); % number of states augmodel.nytrue = length(this.sym.y); % number of observables augmodel.nztrue = this.nz; augmodel.coptim = this.coptim; augmodel.debug = this.debug; - + % multiplication vector (extension of kappa vecs = cell([length(this.sym.p),1]); for ivec = 1:length(this.sym.p) vecs{ivec} = sprintf('k_%i', length(this.sym.k) + ivec-1); end vec = sym(vecs); - + if(this.nevent>0) augmodel.nztrue = length([this.event.z]); % number of observables else augmodel.nztrue = 0; end np = this.np; - + % augment states sv = sym('sv',[length(this.sym.x),1]); Sdot = jacobian(this.sym.xdot,this.sym.x)*sv+jacobian(this.sym.xdot,this.sym.p)*vec; - + % augment output Sy = jacobian(this.sym.y,this.sym.x)*sv+jacobian(this.sym.y,this.sym.p)*vec; - + % generate deltasx this.getFun([],'deltasx'); for ievent = 1:this.nevent; @@ -60,7 +60,7 @@ augmodel.event(ievent) = amievent(this.event(ievent).trigger,bolusnew,znew); augmodel.event(ievent) = augmodel.event(ievent).setHflag([hflagold;zeros([numel(sv),1])]); end - + % augment likelihood this.getFun([],'dsigma_ydp'); this.getFun([],'y'); @@ -75,7 +75,7 @@ + jacobian(this.fun.Jy.sym,this.fun.sigma_y.strsym)*this.fun.dsigma_ydp.sym) ... * vec + jacobian(this.fun.Jy.sym,this.fun.y.strsym)*aug_y_strsym; this.getFun([],'dsigma_zdp'); - + this.getFun([],'dzdp'); this.getFun([],'Jz'); SJz = jacobian(this.fun.Jz.sym,this.sym.p); @@ -87,9 +87,9 @@ % augment sigmas this.getFun([],'sigma_y'); this.getFun([],'sigma_z'); - + S0 = jacobian(this.sym.x0,this.sym.p)*vec; - + augmodel.sym.x = [this.sym.x;sv]; augmodel.sym.xdot = [this.sym.xdot;Sdot]; augmodel.sym.f = augmodel.sym.xdot; @@ -101,7 +101,7 @@ augmodel.sym.p = this.sym.p; augmodel.sym.sigma_y = [this.sym.sigma_y, transpose(this.fun.dsigma_ydp.sym * vec)]; augmodel.sym.sigma_z = [this.sym.sigma_z, transpose(this.fun.dsigma_zdp.sym * vec)]; - + modelo2vec = amimodel(augmodel,[this.modelname '_o2vec']); modelo2vec.o2flag = 2; modelo2vec.debug = this.debug; diff --git a/matlab/@amimodel/checkDeps.m b/matlab/@amimodel/checkDeps.m index caebb514b7..77c40b7fb0 100644 --- a/matlab/@amimodel/checkDeps.m +++ b/matlab/@amimodel/checkDeps.m @@ -8,10 +8,10 @@ % deps: cell array with containing a list of dependencies @type cell % % Return values: - % cflag: boolean indicating whether any of the dependencies have + % cflag: boolean indicating whether any of the dependencies have % changed with respect to the hashes stored in HTable @type % bool - + if(~isempty(HTable)) cflags = zeros(length(deps),1); for id = 1:length(deps) @@ -47,4 +47,4 @@ end end end -end \ No newline at end of file +end diff --git a/matlab/@amimodel/compileAndLinkModel.m b/matlab/@amimodel/compileAndLinkModel.m index 261b8dd3bc..c75f575152 100644 --- a/matlab/@amimodel/compileAndLinkModel.m +++ b/matlab/@amimodel/compileAndLinkModel.m @@ -220,7 +220,7 @@ function compileAndLinkModel(modelname, modelSourceFolder, coptim, debug, funs, 'model', 'model_ode', 'model_dae', 'returndata_matlab', ... 'forwardproblem', 'steadystateproblem', 'backwardproblem', 'newton_solver', ... 'abstract_model', 'sundials_matrix_wrapper', 'sundials_linsol_wrapper', ... - 'vector' + 'vector', 'splinefunctions' }; % to be safe, recompile everything if headers have changed. otherwise % would need to check the full include hierarchy @@ -382,5 +382,3 @@ function updateFileHash(fileFolder,hashFolder,filename) str = regexprep(str,'[\s\.\-]','_'); versionstring = genvarname(str); % fix everything else we have missed end - - diff --git a/matlab/@amimodel/compileC.m b/matlab/@amimodel/compileC.m index 4056b77a3b..815e4e065c 100644 --- a/matlab/@amimodel/compileC.m +++ b/matlab/@amimodel/compileC.m @@ -3,6 +3,6 @@ function compileC(this) % % Return values: % void - + amimodel.compileAndLinkModel(this.modelname, fullfile(this.wrap_path,'models',this.modelname), this.coptim, this.debug, this.funs, this.cfun); -end +end diff --git a/matlab/@amimodel/generateC.m b/matlab/@amimodel/generateC.m index 39f9ab7b81..59eb5e37b4 100644 --- a/matlab/@amimodel/generateC.m +++ b/matlab/@amimodel/generateC.m @@ -163,6 +163,7 @@ function generateC(this) fprintf(fid,[' ' num2str(this.nz) ',\n']); fprintf(fid,[' ' num2str(this.nztrue) ',\n']); fprintf(fid,[' ' num2str(this.nevent) ',\n']); +fprintf(fid,[' 0,\n']); fprintf(fid,[' ' num2str(this.ng) ',\n']); fprintf(fid,[' ' num2str(this.nw) ',\n']); fprintf(fid,[' ' num2str(this.ndwdx) ',\n']); diff --git a/matlab/@amimodel/generateM.m b/matlab/@amimodel/generateM.m index be85528e80..ea8c531f83 100644 --- a/matlab/@amimodel/generateM.m +++ b/matlab/@amimodel/generateM.m @@ -23,4 +23,3 @@ function generateM(this, amimodelo2) end - diff --git a/matlab/@amimodel/generateMatlabWrapper.m b/matlab/@amimodel/generateMatlabWrapper.m index b19521db14..ae89270c8a 100644 --- a/matlab/@amimodel/generateMatlabWrapper.m +++ b/matlab/@amimodel/generateMatlabWrapper.m @@ -462,4 +462,3 @@ function generateMatlabWrapper(nx, ny, np, nk, nz, o2flag, amimodelo2, wrapperFi fclose(fid); end - diff --git a/matlab/@amimodel/generateRebuildM.m b/matlab/@amimodel/generateRebuildM.m index 56b213af70..07bf5bf02a 100644 --- a/matlab/@amimodel/generateRebuildM.m +++ b/matlab/@amimodel/generateRebuildM.m @@ -23,4 +23,3 @@ function generateRebuildM(this) fclose(fid); end - diff --git a/matlab/@amimodel/getFun.m b/matlab/@amimodel/getFun.m index 4d126c585c..12f405b329 100644 --- a/matlab/@amimodel/getFun.m +++ b/matlab/@amimodel/getFun.m @@ -8,11 +8,11 @@ function getFun(this,HTable,funstr) % % Return values: % void - + [wrap_path,~,~]=fileparts(fileparts(which('amiwrap.m'))); - + fun = amifun(funstr,this); - + if(~isfield(this.fun,funstr)) % check whether we already computed the respective fun if(~all(strcmp(fun.deps,funstr))) % prevent infinite loops @@ -35,12 +35,12 @@ function getFun(this,HTable,funstr) else cflag = 0; end - - + + if(cflag) fun = amifun(funstr,this); [fun,this] = fun.getSyms(this); this.fun(1).(funstr) = fun; end - -end \ No newline at end of file + +end diff --git a/matlab/@amimodel/loadOldHashes.m b/matlab/@amimodel/loadOldHashes.m index 088aa826ef..fd56c3361b 100644 --- a/matlab/@amimodel/loadOldHashes.m +++ b/matlab/@amimodel/loadOldHashes.m @@ -4,7 +4,7 @@ % Return values: % HTable: struct with hashes of symbolic definition from the previous % compilation @type struct - + [wrap_path,~,~]=fileparts(fileparts(which('amiwrap.m'))); try load(fullfile(wrap_path,'models',this.modelname,['hashes.mat'])) @@ -25,7 +25,7 @@ this.sparseidxB = sparseidxB; catch err end - + catch HTable = struct(); end @@ -47,7 +47,7 @@ end DHTable.Jy = ''; DHTable.Jz = ''; - + DHTable.generateC = ''; DHTable.makeSyms = ''; DHTable.makeEvents = ''; @@ -62,6 +62,6 @@ DHTable.writeCcode_sensi = ''; DHTable.tdata = ''; - + HTable = am_setdefault(HTable,DHTable); end diff --git a/matlab/@amimodel/makeEvents.m b/matlab/@amimodel/makeEvents.m index f283ff9204..db6025403b 100644 --- a/matlab/@amimodel/makeEvents.m +++ b/matlab/@amimodel/makeEvents.m @@ -69,10 +69,10 @@ function makeEvents( this ) tmp_bolus{ievent} = sym(zeros([nx,1])); end syms polydirac - + % initialise hflag hflags = zeros([nx,nevent]); - + % heaviside event_dependency = zeros(nevent); for ievent = 1:nevent @@ -91,14 +91,14 @@ function makeEvents( this ) end end end - + % check for loops if(any(any(event_dependency^(size(event_dependency,1))))) error('Found loop in trigger dependency. This can lead to the simulation getting stuck and is thus currently not supported. Please check your model definition!') end - + P = 1:size(event_dependency,1); - + % make matrix upper triangular, this is to ensure that we dont end % up with partially replaced trigger functions that we no longer recognise while(~isempty(find(triu(event_dependency(P,P))-event_dependency(P,P)))) @@ -115,9 +115,9 @@ function makeEvents( this ) trigger = trigger(P); bolus = bolus(P); z = z(P); - - - + + + for ix = 1:nx symchar = char(this.sym.xdot(ix)); symvariable = this.sym.xdot(ix); @@ -140,18 +140,18 @@ function makeEvents( this ) end end if(strfind(symchar,'heaviside')) - + for ievent = 1:nevent % remove the heaviside function and replace by h % variable which is updated upon event occurrence in the % solver - + % h variables only change for one sign change but heaviside - % needs updating for both, thus we should + % needs updating for both, thus we should symvariable = subs(symvariable,heaviside( trigger{ievent}),betterSym(['h_' num2str(ievent-1)'])); symvariable = subs(symvariable,heaviside(-trigger{ievent}),betterSym(['(1-h_' num2str(ievent-1) ')'])); % set hflag - + % we can check whether dividing cfp(2) by % trigger{ievent} reduced the length of the symbolic % expression. If it does, this suggests that @@ -172,7 +172,7 @@ function makeEvents( this ) % update xdot this.sym.xdot(ix) = symvariable; end - + % loop until we no longer found any dynamic heaviside functions in the triggers in the previous loop nheavy = 1; while nheavy>0 @@ -196,13 +196,13 @@ function makeEvents( this ) trigger{ievent} = betterSym(symchar); end end - + % compute dtriggerdt and constant trigger functions for ievent = 1:nevent dtriggerdt(ievent) = diff(trigger{ievent},sym('t')) + jacobian(trigger{ievent},this.sym.x)*this.sym.xdot(:); end triggeridx = logical(dtriggerdt~=0); - + % multiply by the dtriggerdt factor, this should stay here as we % want the xdot to be cleaned of any dirac functions ievent = 1; @@ -221,7 +221,7 @@ function makeEvents( this ) ievent = ievent+1; end end - + % update hflags according to bolus for ievent = 1:nevent if(any(double(bolus{ievent}~=0))) @@ -236,9 +236,9 @@ function makeEvents( this ) end end end - + this.event = amievent.empty(); - + % update events for ievent = 1:nevent this.event(ievent) = amievent(trigger{ievent},bolus{ievent}(:),z{ievent}); @@ -279,11 +279,10 @@ function makeEvents( this ) this.sym.Jrz = sym(zeros(size(this.sym.Jz))); for iz = 1:length([this.event.z]) tmp = subs(this.sym.Jz(iz,:),var_z,var_rz); - this.sym.Jrz(iz,:) = subs(tmp,mz,sym(zeros(size(mz)))); + this.sym.Jrz(iz,:) = subs(tmp,mz,sym(zeros(size(mz)))); end end this.sym.Jrz = subs(this.sym.Jrz,rz,var_rz); end - diff --git a/matlab/@amimodel/makeSyms.m b/matlab/@amimodel/makeSyms.m index 13f0226780..4f6f382e40 100644 --- a/matlab/@amimodel/makeSyms.m +++ b/matlab/@amimodel/makeSyms.m @@ -103,7 +103,7 @@ function makeSyms( this ) catch error('Could not transform model.sym.k into a symbolic variable, please check the definition!') end - + end if(isfield(this.sym,'root')) @@ -141,4 +141,3 @@ function makeSyms( this ) % error(['The symbolic variable ' char(symvars(find(svaridx,1))) ' is used in the differential equation right hand side but was not specified as parameter/state/constant!']); % end end - diff --git a/matlab/@amimodel/parseModel.m b/matlab/@amimodel/parseModel.m index beea83800f..167819bb76 100644 --- a/matlab/@amimodel/parseModel.m +++ b/matlab/@amimodel/parseModel.m @@ -148,13 +148,13 @@ function parseModel(this) fprintf('sparse | ') M = double(logical(this.fun.J.sym~=sym(zeros(size(this.fun.J.sym))))); this.sparseidx = find(M); - + [ubw,lbw] = ami_bandwidth(M); - + this.ubw = ubw; this.lbw = lbw; this.nnz = length(find(M(:))); - + I = arrayfun(@(x) find(M(:,x))-1,1:nx,'UniformOutput',false); this.rowvals = []; this.colptrs = []; @@ -163,7 +163,7 @@ function parseModel(this) this.rowvals = [this.rowvals; I{ix}]; end this.colptrs(ix+1) = length(this.rowvals); - + if(this.adjoint) if(isfield(this.fun,'JB')) fprintf('sparseB | ') @@ -186,7 +186,7 @@ function parseModel(this) this.getFun([], 'M'); this.id = double(any(this.fun.M.sym)); else - + end else this.id = zeros(1, nx); diff --git a/matlab/@amised/amised.m b/matlab/@amised/amised.m index 2d69c2bb27..a8f1be6400 100644 --- a/matlab/@amised/amised.m +++ b/matlab/@amised/amised.m @@ -4,7 +4,7 @@ % classdef amised < handle % AMISED is a container for SED-ML objects - + properties ( GetAccess = 'public', SetAccess = 'private' ) % amimodel from the specified model model = struct('event',[],'sym',[]); @@ -20,16 +20,16 @@ varsym = sym([]); % symbolic expressions for data datasym = sym([]); - + end - + properties ( GetAccess = 'public', SetAccess = 'public' ) - + end - + methods function ASED = amised(sedname) - %amised reads in an SEDML document using the JAVA binding of + %amised reads in an SEDML document using the JAVA binding of % of libSEDML % % Parameters: @@ -38,7 +38,7 @@ % Return values: % ASED: amised object which contains all the information from % the SEDML document - + % get models for imodel = 1:length(ASED.sedml.listOfModels.model) % get the model sbml @@ -115,11 +115,10 @@ ASED.varsym(idata,ivar) = sym(variable.Attributes.id); end ASED.datasym(idata) = sym(variable.Attributes.id); - + end - + end end end - diff --git a/matlab/@optsym/optsym.m b/matlab/@optsym/optsym.m index b825a34bae..6209712fe2 100644 --- a/matlab/@optsym/optsym.m +++ b/matlab/@optsym/optsym.m @@ -4,12 +4,12 @@ % classdef optsym0) stoichsymbols = [reactant_id{:},product_id{:}]; stoichmath = [tmp_rs,tmp_ps]; - + stoichidx = not(strcmp(stoichsymbols,'')); stoichsymbols = stoichsymbols(stoichidx); stoichmath = stoichmath(stoichidx); @@ -440,7 +440,7 @@ function importSBML(this,filename) error('Event priorities are currently not supported!'); end end - + try tmp = cellfun(@(x) sym(sanitizeString(x)),{model.event.trigger},'UniformOutput',false); this.trigger = [tmp{:}]; @@ -459,10 +459,10 @@ function importSBML(this,filename) for ievent = 1:length(this.trigger) tmp = cellfun(@(x) {x.variable},{model.event(ievent).eventAssignment},'UniformOutput',false); assignments = sym(cat(2,tmp{:})); - + tmp = cellfun(@(x) {x.math},{model.event(ievent).eventAssignment},'UniformOutput',false); assignments_math = cleanedsym(cat(2,tmp{:})); - + for iassign = 1:length(assignments) state_assign_idx = find(assignments(iassign)==this.state); param_assign_idx = find(assignments(iassign)==this.param); @@ -470,37 +470,37 @@ function importSBML(this,filename) bound_assign_idx = find(assignments(iassign)==boundary_sym); stoich_assign_idx = find(assignments(iassign)==stoichsymbols); vol_assign_idx = find(assignments(iassign)==compartments_sym); - + if(np>0 && ~isempty(param_assign_idx)) error('Assignments of parameters via events are currently not supported') this.param(param_assign_idx) = this.param(param_assign_idx)*heaviside(-this.trigger(ievent)) + assignments_math(iassign)*heaviside(this.trigger(ievent)); end - + if(nk>0 && ~isempty(cond_assign_idx)) error('Assignments of constants via events are currently not supported') conditions(cond_assign_idx) = conditions(cond_assign_idx)*heaviside(-this.trigger(ievent)) + assignments_math(iassign)*heaviside(this.trigger(ievent)); end - + if(length(boundaries)>0 && ~isempty(bound_assign_idx)) error('Assignments of boundary conditions via events are currently not supported') boundaries(bound_assign_idx) = conditions(bound_assign_idx)*heaviside(-this.trigger(ievent)) + assignments_math(iassign)*heaviside(this.trigger(ievent)); end - + if(length(stoichsymbols)>0 && ~isempty(stoich_assign_idx)) error('Assignments of stoichiometries via events are currently not supported') stoichmath(stoich_assign_idx) = stoichmath(stoich_assign_idx)*heaviside(-this.trigger(ievent)) + assignments_math(iassign)*heaviside(this.trigger(ievent)); end - + if(length(compartments_sym)>0 && ~isempty(vol_assign_idx)) error('Assignments of compartment volumes via events are currently not supported') end - + if(length(this.state)>0 && ~isempty(state_assign_idx)) - + this.bolus(state_assign_idx,ievent) = -this.state(state_assign_idx); addToBolus = sym(zeros(size(this.bolus(:,ievent)))); addToBolus(state_assign_idx) = assignments_math(iassign); - + this.bolus(:,ievent) = this.bolus(:,ievent) + addToBolus; end @@ -526,15 +526,15 @@ function importSBML(this,filename) tmpfun = cellfun(@(x) ['fun_' num2str(x)],num2cell(1:length(model.functionDefinition)),'UniformOutput',false); this.funmath = strrep(this.funmath,{model.functionDefinition.id},tmpfun); % replace helper functions - + checkIllegalFunctions(this.funmath); this.funmath = replaceLogicalFunctions(this.funmath); - + this.funmath = strrep(this.funmath,tmpfun,{model.functionDefinition.id}); this.funarg = cellfun(@(x,y) [y '(' strjoin(transpose(x(1:end-1)),',') ')'],lambdas,replaceReservedFunctionIDs({model.functionDefinition.id}),'UniformOutput',false); - + % make functions available in this file - + for ifun = 1:length(this.funmath) token = regexp(this.funarg(ifun),'\(([0-9\w\,]*)\)','tokens'); start = regexp(this.funarg(ifun),'\(([0-9\w\,]*)\)'); @@ -567,7 +567,7 @@ function importSBML(this,filename) if(ismember(initassignments_sym(iIA),this.param)) if(ismember(sym(model.time_symbol),symvar(initassignments_math(iIA)))) error('Time dependent initial assignments are currently not supported!') - end + end param_idx = find(initassignments_sym(iIA)==this.param); parameter_sym(param_idx) = []; parameter_val(param_idx) = []; @@ -579,7 +579,7 @@ function importSBML(this,filename) this.param = subs(this.param,initassignments_sym(iIA),initassignments_math(iIA)); rulemath = subs(rulemath,initassignments_sym(iIA),initassignments_math(iIA)); np = np-1; - end + end end applyRule(this,model,'param',rulevars,rulemath) @@ -760,7 +760,7 @@ function checkIllegalFunctions(str) if(isfield(y,'math')) expr = cleanedsym(y.math); else - expr = cleanedsym(); + expr = cleanedsym(); end end @@ -771,4 +771,3 @@ function checkIllegalFunctions(str) id = {x.species}; end end - diff --git a/matlab/SBMLimporter/@SBMLode/writeAMICI.m b/matlab/SBMLimporter/@SBMLode/writeAMICI.m index 422a805150..3550241cd7 100644 --- a/matlab/SBMLimporter/@SBMLode/writeAMICI.m +++ b/matlab/SBMLimporter/@SBMLode/writeAMICI.m @@ -7,10 +7,10 @@ function writeAMICI(this,modelname) % % Return values: % void - + fprintf('writing file ...\n') fid = fopen([modelname '_syms.m'],'w'); - + fprintf(fid,['function model = ' modelname '_syms()\n']); fprintf(fid,'\n'); if(strcmp(this.time_symbol,'')) @@ -20,7 +20,7 @@ function writeAMICI(this,modelname) end fprintf(fid,'\n'); fprintf(fid,'avogadro = 6.02214179e23;'); - + % fprintf(fid,'model.debug = true;\n'); writeDefinition('STATES','x','state',this,fid) writeDefinition('PARAMETERS','p','parameter',this,fid) @@ -54,7 +54,7 @@ function writeAMICI(this,modelname) fprintf(fid,'\n'); fprintf(fid,'end\n'); fprintf(fid,'\n'); - + for ifun = 1:length(this.funmath) fprintf(fid,['function r = ' this.funarg{ifun} '\n']); fprintf(fid,'\n'); @@ -62,12 +62,12 @@ function writeAMICI(this,modelname) fprintf(fid,'\n'); fprintf(fid,'end\n'); fprintf(fid,'\n'); - end - + end + for fun = {'factorial','cei','psi'} fprintUnsupportedFunctionError(fun{1},fid) end - + fclose(fid); end @@ -99,4 +99,4 @@ function fprintUnsupportedFunctionError(functionName,fid) fprintf(fid,'\n'); fprintf(fid,'end\n'); fprintf(fid,'\n'); -end \ No newline at end of file +end diff --git a/matlab/SBMLimporter/computeBracketLevel.m b/matlab/SBMLimporter/computeBracketLevel.m index ec1c6621db..5bbffb961b 100644 --- a/matlab/SBMLimporter/computeBracketLevel.m +++ b/matlab/SBMLimporter/computeBracketLevel.m @@ -11,7 +11,7 @@ % % Return values: % brl: bracket levels @type *int - + % compute bracket levels add one for each (, (before) remove 1 for each % ) (after) open = (cstr == '('); @@ -24,6 +24,5 @@ for ifun = 1:length(fun_startidx) brl(fun_startidx(ifun):(fun_endidx(ifun)-1)) = brl(fun_endidx(ifun)); end - -end +end diff --git a/matlab/amiwrap.m b/matlab/amiwrap.m index 838d95c102..6fbf8353ae 100644 --- a/matlab/amiwrap.m +++ b/matlab/amiwrap.m @@ -199,4 +199,3 @@ function amiwrap( varargin ) end warning(warningreset); end - diff --git a/matlab/auxiliary/CalcMD5/CalcMD5.c b/matlab/auxiliary/CalcMD5/CalcMD5.c index d84dcfb811..a078a23496 100644 --- a/matlab/auxiliary/CalcMD5/CalcMD5.c +++ b/matlab/auxiliary/CalcMD5/CalcMD5.c @@ -90,7 +90,7 @@ ** documentation and/or software. ** ********************************************************************** */ - + /* % $JRev: R5.00z V:025 Sum:/kHGslMmCpAS Date:17-Dec-2009 12:46:26 $ % $File: CalcMD5\CalcMD5.c $ @@ -209,33 +209,33 @@ void MD5Update(MD5_CTX *context, UCHAR *input, UINT inputLen) /* Compute number of bytes mod 64: */ index = (UINT)((context->count[0] >> 3) & 0x3F); - + /* Update number of bits: */ if ((context->count[0] += ((UINT32)inputLen << 3)) < ((UINT32)inputLen << 3)) { context->count[1]++; } context->count[1] += ((UINT32)inputLen >> 29); - + partLen = 64 - index; - + /* Transform as many times as possible: */ if (inputLen >= partLen) { int i; memcpy((POINTER)&context->buffer[index], (POINTER)input, partLen); MD5Transform(context->state, context->buffer); - + inputLenM63 = inputLen - 63; for (i = partLen; i < inputLenM63; i += 64) { MD5Transform(context->state, &input[i]); } - + /* Buffer remaining input: index = 0 */ memcpy((POINTER)&context->buffer[0], (POINTER)&input[i], inputLen - i); } else { /* Buffer remaining input: i = 0 */ memcpy((POINTER)&context->buffer[index], (POINTER)input, inputLen); } - + return; } @@ -254,13 +254,13 @@ void MD5Final(UCHAR digest[16], MD5_CTX *context) index = (UINT)((context->count[0] >> 3) & 0x3f); padLen = (index < 56) ? (56 - index) : (120 - index); MD5Update(context, PADDING, padLen); - + /* Append length before padding: */ MD5Update(context, bits, 8); - + /* Store state in digest: */ MD5Encode(digest, context->state, 4); - + /* Zero sensitive information: */ memset((POINTER)context, 0, sizeof(MD5_CTX)); } @@ -312,7 +312,7 @@ void MD5Transform(UINT32 state[4], UCHAR block[64]) (((UINT32)block[58]) << 16) | (((UINT32)block[59]) << 24); x[15] = ( (UINT32)block[60]) | (((UINT32)block[61]) << 8) | (((UINT32)block[62]) << 16) | (((UINT32)block[63]) << 24); - + /* Round 1 */ FF(a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */ FF(d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */ @@ -398,7 +398,7 @@ void MD5Transform(UINT32 state[4], UCHAR block[64]) void MD5Encode(UCHAR *output, UINT32 *input, UINT len) { UINT j; - + for (j = 0; j < len; j++) { *output++ = (UCHAR)( *input & 0xff); *output++ = (UCHAR)((*input >> 8) & 0xff); @@ -417,17 +417,17 @@ void MD5Char(mxChar *array, mwSize inputLen, UCHAR digest[16]) MD5_CTX context; UINT Chunk; UCHAR *bufferP, *bufferEnd = buffer + BUFFER_LEN, *arrayP; - + /* Limit length to 32 bit address, because I cannot test this function */ /* with 64 bit arrays currently (under construction): */ if (inputLen >> 31 != 0) { /* Detect sign-bit if mwSize is int */ mexErrMsgTxt("*** CalcMD5[mex]: Input > 2^31 byte not handled yet."); } - + arrayP = (UCHAR *) array; /* UCHAR *, not mxChar *!*/ - + MD5Init(&context); - + /* Copy chunks of input data - only the first byte of each mxChar: */ Chunk = inputLen / BUFFER_LEN; while (Chunk--) { @@ -436,10 +436,10 @@ void MD5Char(mxChar *array, mwSize inputLen, UCHAR digest[16]) *bufferP++ = *arrayP; arrayP += 2; } - + MD5Update(&context, buffer, BUFFER_LEN); } - + /* Last chunk: */ Chunk = inputLen % BUFFER_LEN; if (Chunk != 0) { @@ -449,12 +449,12 @@ void MD5Char(mxChar *array, mwSize inputLen, UCHAR digest[16]) *bufferP++ = *arrayP; arrayP += 2; } - + MD5Update(&context, buffer, Chunk); } - + MD5Final(digest, &context); - + return; } @@ -462,13 +462,13 @@ void MD5Char(mxChar *array, mwSize inputLen, UCHAR digest[16]) void MD5Array(UCHAR *array, mwSize inputLen, UCHAR digest[16]) { MD5_CTX context; - + /* Limit length to 32 bit address, because I cannot test this function */ /* with 64 bit arrays currently (under construction): */ if (inputLen >> 31 != 0) { /* Detect sign-bit if mwSize is signed int */ mexErrMsgTxt("*** CalcMD5[mex]: Input > 2^31 byte not handled yet."); } - + MD5Init(&context); MD5Update(&context, array, (UINT) inputLen); MD5Final(digest, &context); @@ -481,13 +481,13 @@ void MD5File(char *filename, UCHAR digest[16]) MD5_CTX context; int len; UINT32 allLen = 0; - + /* Open the file in binary mode: */ if ((FID = fopen(filename, "rb")) == NULL) { mexPrintf("*** Error for file: [%s]\n", filename); mexErrMsgTxt("*** CalcMD5[mex]: Cannot open file."); } - + MD5Init(&context); while ((len = fread(buffer, 1, BUFFER_LEN, FID)) != 0) { /* Limit length to 32 bit address, because I cannot test this function */ @@ -497,7 +497,7 @@ void MD5File(char *filename, UCHAR digest[16]) fclose(FID); mexErrMsgTxt("*** CalcMD5[mex]: Cannot handle files > 2.1GB yet."); } - + MD5Update(&context, buffer, (UINT) len); } MD5Final(digest, &context); @@ -509,7 +509,7 @@ void MD5File(char *filename, UCHAR digest[16]) void ToHex(const UCHAR digest[16], char *output, int LowerCase) { char *outputEnd; - + if (LowerCase) { for (outputEnd = output + 32; output < outputEnd; output += 2) { sprintf(output, "%02x", *(digest++)); @@ -519,7 +519,7 @@ void ToHex(const UCHAR digest[16], char *output, int LowerCase) sprintf(output, "%02X", *(digest++)); } } - + return; } @@ -535,7 +535,7 @@ void ToBase64(const UCHAR In[16], char *Out) int i; char *p; const UCHAR *s; - + p = Out; s = In; for (i = 0; i < 5; i++) { @@ -545,11 +545,11 @@ void ToBase64(const UCHAR In[16], char *Out) *p++ = B64[s[2] & 0x3F]; s += 3; } - + *p++ = B64[(*s >> 2) & 0x3F]; *p++ = B64[((*s & 0x3) << 4)]; *p = '\0'; - + return; } @@ -560,12 +560,12 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) /* - Define default values of optional arguments. */ /* - Forward input data to different calculators according to the input type. */ /* - Convert digest to output format. */ - + char *FileName, InType, hexOut[33], b64Out[23]; UCHAR digest[16], *digestP, OutType = 'h'; int isFile = false, isUnicode = false; double *outP, *outEnd; - + /* Check number of inputs and outputs: */ if (nrhs == 0 || nrhs > 3) { mexErrMsgTxt("*** CalcMD5[mex]: 1 to 3 inputs required."); @@ -573,27 +573,27 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) if (nlhs > 1) { mexErrMsgTxt("*** CalcMD5[mex]: Too many output arguments."); } - + /* If 2nd input starts with 'f', treat string in 1st argument as file name: */ if (nrhs >= 2 && mxGetNumberOfElements(prhs[1]) > 0) { if (mxIsChar(prhs[1]) == 0) { mexErrMsgTxt("*** CalcMD5[mex]: 2nd input must be a string."); } - + InType = (char) tolower(*(POINTER) mxGetData(prhs[1])); isFile = (InType == 'f'); isUnicode = (InType == 'u'); } /* Default otherwise! */ - + /* Output type - default: hex: */ if (nrhs == 3 && !mxIsEmpty(prhs[2])) { if (mxIsChar(prhs[2]) == 0) { mexErrMsgTxt("*** CalcMD5[mex]: 3rd input must be a string."); } - + OutType = *(POINTER) mxGetData(prhs[2]); /* Just 1st character */ } - + /* Calculate check sum: */ if (isFile) { if ((FileName = mxArrayToString(prhs[0])) == NULL) { @@ -601,21 +601,21 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) } MD5File(FileName, digest); mxFree(FileName); - + } else if (mxIsNumeric(prhs[0]) || isUnicode) { MD5Array((POINTER) mxGetData(prhs[0]), mxGetNumberOfElements(prhs[0]) * mxGetElementSize(prhs[0]), digest); - + } else if (mxIsChar(prhs[0])) { MD5Char((mxChar *) mxGetData(prhs[0]), mxGetNumberOfElements(prhs[0]), digest); - + } else { mexErrMsgTxt("*** CalcMD5[mex]: Input type not accepted."); } - + /* Create output: */ switch (OutType) { case 'H': @@ -623,7 +623,7 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) ToHex(digest, hexOut, OutType == 'h'); plhs[0] = mxCreateString(hexOut); break; - + case 'D': case 'd': /* DOUBLE with integer values: */ plhs[0] = mxCreateDoubleMatrix(1, 16, mxREAL); @@ -633,7 +633,7 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) *outP = (double) *digestP++; } break; - + case 'B': case 'b': /* Base64: */ /* strtobase64(b64Out, 26, digest, 16); // included in LCC3.8 */ @@ -641,10 +641,10 @@ void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) ToBase64(digest, b64Out); /* Locally implemented */ plhs[0] = mxCreateString(b64Out); break; - + default: mexErrMsgTxt("*** CalcMD5[mex]: Unknown output type."); } - + return; } diff --git a/matlab/auxiliary/CalcMD5/TestCalcMD5.m b/matlab/auxiliary/CalcMD5/TestCalcMD5.m index 5cc14e4d2f..ec09ecfc6b 100644 --- a/matlab/auxiliary/CalcMD5/TestCalcMD5.m +++ b/matlab/auxiliary/CalcMD5/TestCalcMD5.m @@ -54,7 +54,7 @@ function TestCalcMD5(doSpeed) error(['*** ', FuncName, ': Failed for string:', ... char(10), '[', TestData{iTest, 1}, ']']); end - + % Check file input: FID = fopen(TestFile, 'wb+'); if FID < 0 @@ -63,7 +63,7 @@ function TestCalcMD5(doSpeed) end fwrite(FID, TestData{iTest, 1}, 'uchar'); fclose(FID); - + Str2 = CalcMD5(TestFile, 'file'); if strcmpi(Str2, TestData{iTest, 2}) == 0 fprintf('\n'); @@ -83,14 +83,14 @@ function TestCalcMD5(doSpeed) upHexOut = CalcMD5(data, 'char', 'HEX'); decOut = CalcMD5(data, 'char', 'Dec'); b64Out = CalcMD5(data, 'char', 'Base64'); - + if not(strcmpi(lowHexOut, upHexOut) && ... isequal(sscanf(lowHexOut, '%2x'), decOut(:)) && ... isequal(Base64decode(b64Out), decOut)) fprintf('\n'); error(['*** ', FuncName, ': Different results for output types.']); end - + % Check unicode, if the data length is a multiple of 2: if rem(length(data), 2) == 0 doubleData = double(data); @@ -110,12 +110,12 @@ function TestCalcMD5(doSpeed) disp('== Test speed:'); disp('(Short data: mainly the overhead of calling the function)'); Delay = 2; - + for Len = [10, 100, 1000, 10000, 1e5, 1e6, 1e7] [Number, Unit] = UnitPrint(Len); fprintf(' Data length: %s %s:\n', Number, Unit); data = uint8(fix(rand(1, Len) * 256)); - + % Measure java time: iniTime = cputime; finTime = iniTime + Delay; @@ -129,7 +129,7 @@ function TestCalcMD5(doSpeed) javaLoopPerSec = javaLoop / (cputime - iniTime); [Number, Unit] = UnitPrint(javaLoopPerSec * Len); fprintf(' java: %6s %s/sec\n', Number, Unit); - + % Measure Mex time: iniTime = cputime; finTime = iniTime + Delay; @@ -142,7 +142,7 @@ function TestCalcMD5(doSpeed) [Number, Unit] = UnitPrint(mexLoopPerSec * Len); fprintf(' mex: %6s %s/sec: %.1f times faster\n', ... Number, Unit, mexLoopPerSec / javaLoopPerSec); - + % Compare the results: if ~isequal(javaHash(:), mexHash(:)) error(['*** ', FuncName, ': Different results from java and Mex.']); @@ -151,7 +151,7 @@ function TestCalcMD5(doSpeed) end fprintf('\nCalcMD5 seems to work well.\n'); - + return; % ****************************************************************************** diff --git a/matlab/auxiliary/am_setdefault.m b/matlab/auxiliary/am_setdefault.m index 6065734eb6..f96ac7461d 100755 --- a/matlab/auxiliary/am_setdefault.m +++ b/matlab/auxiliary/am_setdefault.m @@ -7,7 +7,7 @@ % % Return values: % robj: updated obj @type struct - + fieldlist = fieldnames(obj); for i = 1:length(fieldlist) dobj.(fieldlist{i}) = obj.(fieldlist{i}); diff --git a/matlab/auxiliary/betterSym.m b/matlab/auxiliary/betterSym.m index e3eec9baf9..cf93b74098 100644 --- a/matlab/auxiliary/betterSym.m +++ b/matlab/auxiliary/betterSym.m @@ -5,4 +5,4 @@ else csym = sym(str); end -end \ No newline at end of file +end diff --git a/matlab/auxiliary/getCommitHash.m b/matlab/auxiliary/getCommitHash.m index b33e820e49..c39f33f7a3 100644 --- a/matlab/auxiliary/getCommitHash.m +++ b/matlab/auxiliary/getCommitHash.m @@ -9,7 +9,7 @@ % commit_hash: extracted hash value @type char % branch: branch of the repository @type char % url: employed remote origin @type char - + try fid = fopen(fullfile(wrap_path,'..','.git','FETCH_HEAD')); str = fgetl(fid); @@ -25,11 +25,11 @@ fid = fopen(fullfile(wrap_path,'.git','ORIG_HEAD')); commit_hash = ['dev_' fgetl(fid)]; fclose(fid); - + fid = fopen(fullfile(wrap_path,'.git','HEAD')); branch = strrep(fgetl(fid),'ref: refs/heads/',''); fclose(fid); - + url = 'local'; end catch @@ -38,4 +38,3 @@ url = 'unknown repository'; end end - diff --git a/matlab/auxiliary/struct2xml/struct2xml.m b/matlab/auxiliary/struct2xml/struct2xml.m index 228acb99fb..7fa900ff0e 100644 --- a/matlab/auxiliary/struct2xml/struct2xml.m +++ b/matlab/auxiliary/struct2xml/struct2xml.m @@ -1,5 +1,5 @@ function varargout = struct2xml( s, varargin ) -%Convert a MATLAB structure into a xml file +%Convert a MATLAB structure into a xml file % [ ] = struct2xml( s, file ) % xml = struct2xml( s ) % @@ -26,7 +26,7 @@ % On-screen output functionality added by P. Orth, 01-12-2010 % Multiple space to single space conversion adapted for speed by T. Lohuis, 11-04-2011 % Val2str subfunction bugfix by H. Gsenger, 19-9-2011 - + if (nargin ~= 2) if(nargout ~= 1 || nargin ~= 1) error(['Supported function calls:' sprintf('\n')... @@ -46,17 +46,17 @@ file = [file '.xml']; end end - + if (~isstruct(s)) error([inputname(1) ' is not a structure']); end - + if (length(fieldnames(s)) > 1) error(['Error processing the structure:' sprintf('\n') 'There should be a single field in the main structure.']); end xmlname = fieldnames(s); xmlname = xmlname{1}; - + %substitute special characters xmlname_sc = xmlname; xmlname_sc = strrep(xmlname_sc,'_dash_','-'); @@ -77,35 +77,35 @@ xmlwrite(file,docNode); else varargout{1} = xmlwrite(docNode); - end + end end % ----- Subfunction parseStruct ----- function [] = parseStruct(s,docNode,curNode,pName) - + fnames = fieldnames(s); for i = 1:length(fnames) curfield = fnames{i}; - + %substitute special characters curfield_sc = curfield; curfield_sc = strrep(curfield_sc,'_dash_','-'); curfield_sc = strrep(curfield_sc,'_colon_',':'); curfield_sc = strrep(curfield_sc,'_dot_','.'); - + if (strcmp(curfield,'Attributes')) %Attribute data if (isstruct(s.(curfield))) attr_names = fieldnames(s.Attributes); for a = 1:length(attr_names) cur_attr = attr_names{a}; - + %substitute special characters cur_attr_sc = cur_attr; cur_attr_sc = strrep(cur_attr_sc,'_dash_','-'); cur_attr_sc = strrep(cur_attr_sc,'_colon_',':'); cur_attr_sc = strrep(cur_attr_sc,'_dot_','.'); - + [cur_str,succes] = val2str(s.Attributes.(cur_attr)); if (succes) curNode.setAttribute(cur_attr_sc,cur_str); @@ -161,10 +161,10 @@ %----- Subfunction val2str ----- function [str,succes] = val2str(val) - + succes = true; str = []; - + if (isempty(val)) return; %bugfix from H. Gsenger elseif (ischar(val)) @@ -174,16 +174,16 @@ else succes = false; end - + if (ischar(val)) %add line breaks to all lines except the last (for multiline strings) lines = size(val,1); val = [val char(sprintf('\n')*[ones(lines-1,1);0])]; - - %transpose is required since indexing (i.e., val(nonspace) or val(:)) produces a 1-D vector. + + %transpose is required since indexing (i.e., val(nonspace) or val(:)) produces a 1-D vector. %This should be row based (line based) and not column based. valt = val'; - + remove_multiple_white_spaces = true; if (remove_multiple_white_spaces) %remove multiple white spaces using isspace, suggestion of T. Lohuis diff --git a/matlab/auxiliary/structToHDF5Attribute.m b/matlab/auxiliary/structToHDF5Attribute.m index a692ed69eb..78b2f4096c 100644 --- a/matlab/auxiliary/structToHDF5Attribute.m +++ b/matlab/auxiliary/structToHDF5Attribute.m @@ -39,4 +39,4 @@ end end end -end \ No newline at end of file +end diff --git a/matlab/auxiliary/template.m b/matlab/auxiliary/template.m index 2d1ff3732e..24fd06ff86 100644 --- a/matlab/auxiliary/template.m +++ b/matlab/auxiliary/template.m @@ -1,13 +1,13 @@ classdef template < handle %TEMPLATE A class to replace strings in template files - + properties % strings in the template to be replaced templateStrings = {}; % strings for replacement templateReplacements = {}; end - + methods function replace(this, infile, outfile) % apply all provided template substitutions to infile and write @@ -22,17 +22,17 @@ function replace(this, infile, outfile) fclose(fin); fclose(fout); end - + function add(this, templateStr, replacementStr) % add a new template string and replacement nextIdx = numel(this.templateStrings); this.templateStrings{nextIdx + 1} = templateStr; this.templateReplacements{nextIdx + 1} = replacementStr; end - + function s = replaceStr(this, s) - % apply all provided template substitutions to s - + % apply all provided template substitutions to s + % do not use cellfun to guarantee order of replacements for n = 1:numel(this.templateStrings) s = strrep(s, this.templateStrings(n), this.templateReplacements(n)); @@ -40,6 +40,5 @@ function add(this, templateStr, replacementStr) end end end - -end +end diff --git a/matlab/auxiliary/xml2struct/xml2struct.m b/matlab/auxiliary/xml2struct/xml2struct.m index dcd85a207b..a7b93058f1 100644 --- a/matlab/auxiliary/xml2struct/xml2struct.m +++ b/matlab/auxiliary/xml2struct/xml2struct.m @@ -32,7 +32,7 @@ help xml2struct return end - + if isa(file, 'org.apache.xerces.dom.DeferredDocumentImpl') || isa(file, 'org.apache.xerces.dom.DeferredElementImpl') % input is a java xml object xDoc = file; @@ -44,7 +44,7 @@ if (isempty(strfind(file,'.xml'))) file = [file '.xml']; end - + if (exist(file,'file') == 0) error(['The file ' file ' could not be found']); end @@ -52,10 +52,10 @@ %read the xml file xDoc = xmlread(file); end - + %parse xDoc into a MATLAB structure s = parseChildNodes(xDoc); - + end % ----- Subfunction parseChildNodes ----- @@ -70,7 +70,7 @@ for count = 1:numChildNodes theChild = item(childNodes,count-1); [text,name,attr,childs,textflag] = getNodeData(theChild); - + if (~strcmp(name,'#text') && ~strcmp(name,'#comment') && ~strcmp(name,'#cdata_dash_section')) %XML allows the same elements to be defined multiple times, %put each in a different cell @@ -83,19 +83,19 @@ %add new element children.(name){index} = childs; if(~isempty(fieldnames(text))) - children.(name){index} = text; + children.(name){index} = text; end - if(~isempty(attr)) - children.(name){index}.('Attributes') = attr; + if(~isempty(attr)) + children.(name){index}.('Attributes') = attr; end else %add previously unknown (new) element to the structure children.(name) = childs; if(~isempty(text) && ~isempty(fieldnames(text))) - children.(name) = text; + children.(name) = text; end - if(~isempty(attr)) - children.(name).('Attributes') = attr; + if(~isempty(attr)) + children.(name).('Attributes') = attr; end end else @@ -105,25 +105,25 @@ elseif (strcmp(name, '#comment')) ptextflag = 'Comment'; end - - %this is the text in an element (i.e., the parentNode) + + %this is the text in an element (i.e., the parentNode) if (~isempty(regexprep(text.(textflag),'[\s]*',''))) if (~isfield(ptext,ptextflag) || isempty(ptext.(ptextflag))) ptext.(ptextflag) = text.(textflag); else %what to do when element data is as follows: %Text More text - + %put the text in different cells: % if (~iscell(ptext)) ptext = {ptext}; end % ptext{length(ptext)+1} = text; - + %just append the text ptext.(ptextflag) = [ptext.(ptextflag) text.(textflag)]; end end end - + end end end @@ -131,7 +131,7 @@ % ----- Subfunction getNodeData ----- function [text,name,attr,childs,textflag] = getNodeData(theNode) % Create structure of node info. - + %make sure name is allowed as structure name name = toCharArray(getNodeName(theNode))'; name = strrep(name, '-', '_dash_'); @@ -139,13 +139,13 @@ name = strrep(name, '.', '_dot_'); attr = parseAttributes(theNode); - if (isempty(fieldnames(attr))) - attr = []; + if (isempty(fieldnames(attr))) + attr = []; end - + %parse child nodes [childs,text,textflag] = parseChildNodes(theNode); - + if (isempty(fieldnames(childs)) && isempty(fieldnames(text))) %get the data of any childless nodes % faster than if any(strcmp(methods(theNode), 'getData')) @@ -153,7 +153,7 @@ % faster than text = char(getData(theNode)); text.(textflag) = toCharArray(getTextContent(theNode))'; end - + end % ----- Subfunction parseAttributes ----- @@ -172,7 +172,7 @@ %Suggestion of Adrian Wanner str = toCharArray(toString(item(theAttributes,count-1)))'; - k = strfind(str,'='); + k = strfind(str,'='); attr_name = str(1:(k(1)-1)); attr_name = strrep(attr_name, '-', '_dash_'); attr_name = strrep(attr_name, ':', '_colon_'); @@ -180,4 +180,4 @@ attributes.(attr_name) = str((k(1)+2):(end-1)); end end -end \ No newline at end of file +end diff --git a/matlab/examples/example_adjoint/example_adjoint.m b/matlab/examples/example_adjoint/example_adjoint.m index fc0584b2da..3e787c2110 100644 --- a/matlab/examples/example_adjoint/example_adjoint.m +++ b/matlab/examples/example_adjoint/example_adjoint.m @@ -42,17 +42,17 @@ function example_adjoint() errorbar(t,D.Y,D.Sigma_Y) hold on % plot(t,sol.y) - + xlabel('time t') ylabel('observable') title(['log-likelihood: ' num2str(sol.llh) ]) - + y = (p(2)*t + p(3)).*(t<2) + ( (2*p(2)+p(3)-p(2)/p(1))*exp(-p(1)*(t-2))+p(2)/p(1) ).*(t>=2); - - + + tfine = linspace(0,4,100001); xfine = (p(2)*tfine + 1).*(tfine<2) + ( (2*p(2)+p(3)-p(2)/p(1))*exp(-p(1)*(tfine-2))+p(2)/p(1) ).*(tfine>=2); - + mu = zeros(1,length(tfine)); for it = 1:length(t) if(t(it)<=2) @@ -69,9 +69,9 @@ function example_adjoint() ylabel('adjoint') xlabel('time t') xlim([min(t)-0.5,max(t)+0.5]) - + subplot(3,1,3) - + plot(fliplr(tfine),-cumsum(fliplr(-mu.*xfine.*(tfine>2)))*p(1)*log(10)*(t(end)/numel(tfine))) hold on plot(fliplr(tfine),-cumsum(fliplr(mu))*p(2)*log(10)*(t(end)/numel(tfine))) @@ -79,13 +79,13 @@ function example_adjoint() xlim([min(t)-0.5,max(t)+0.5]) ylabel('integral') xlabel('time t') - + legend('p1','p2','p3') - + grad(1,1) = -trapz(tfine,-mu.*xfine.*(tfine>2))*p(1)*log(10); grad(2,1) = -trapz(tfine,mu)*p(2)*log(10); grad(3,1) = -mu(1)*p(3)*log(10); - + plot(zeros(3,1),grad,'ko') end @@ -123,7 +123,7 @@ function example_adjoint() xlabel('analytic absolute value of gradient element') ylabel('computed absolute value of gradient element') set(gcf,'Position',[100 300 1200 500]) - + drawnow end diff --git a/matlab/examples/example_adjoint/model_adjoint_syms.m b/matlab/examples/example_adjoint/model_adjoint_syms.m index 8473fcc6d4..0eddd4d122 100644 --- a/matlab/examples/example_adjoint/model_adjoint_syms.m +++ b/matlab/examples/example_adjoint/model_adjoint_syms.m @@ -13,9 +13,9 @@ % PARAMETERS ( for these sensitivities will be computed ) % create parameter syms -syms p1 p2 p3 +syms p1 p2 p3 -% create parameter vector +% create parameter vector model.sym.p = [p1 p2 p3]; @@ -48,4 +48,4 @@ model.sym.y(1) = x1; -end \ No newline at end of file +end diff --git a/matlab/examples/example_adjoint_hessian/example_adjoint_hessian.m b/matlab/examples/example_adjoint_hessian/example_adjoint_hessian.m index 6133dc6560..91aa105bd5 100644 --- a/matlab/examples/example_adjoint_hessian/example_adjoint_hessian.m +++ b/matlab/examples/example_adjoint_hessian/example_adjoint_hessian.m @@ -91,4 +91,4 @@ success=0; end -end \ No newline at end of file +end diff --git a/matlab/examples/example_adjoint_hessian/model_adjoint_hessian_syms.m b/matlab/examples/example_adjoint_hessian/model_adjoint_hessian_syms.m index 05b06bd4d7..3648341dbb 100644 --- a/matlab/examples/example_adjoint_hessian/model_adjoint_hessian_syms.m +++ b/matlab/examples/example_adjoint_hessian/model_adjoint_hessian_syms.m @@ -13,9 +13,9 @@ % PARAMETERS ( for these sensitivities will be computed ) % create parameter syms -syms p1 p2 p3 +syms p1 p2 p3 -% create parameter vector +% create parameter vector model.sym.p = [p1 p2 p3]; @@ -48,4 +48,4 @@ model.sym.y(1) = x1; -end \ No newline at end of file +end diff --git a/matlab/examples/example_calvetti/example_calvetti.m b/matlab/examples/example_calvetti/example_calvetti.m index 334f977be0..e786677161 100755 --- a/matlab/examples/example_calvetti/example_calvetti.m +++ b/matlab/examples/example_calvetti/example_calvetti.m @@ -26,13 +26,13 @@ function example_calvetti() % ODE15S y0 = [k(1); k(3); k(5); 1; 1; 1;]; -M = [1 0 0 0 0 0 +M = [1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]; - + function [xdot] = dae_system(t,x,p,k,it) if it<3 h0 = 0; @@ -95,7 +95,7 @@ function example_calvetti() legend('error x1','error x2','error x3','error x4','error x5','error x6','Location','NorthEastOutside') legend boxoff ylabel('x') - + set(gcf,'Position',[100 300 1200 500]) end end diff --git a/matlab/examples/example_calvetti/model_calvetti_syms.m b/matlab/examples/example_calvetti/model_calvetti_syms.m index 5fc3661ebf..697e4c1340 100755 --- a/matlab/examples/example_calvetti/model_calvetti_syms.m +++ b/matlab/examples/example_calvetti/model_calvetti_syms.m @@ -6,28 +6,28 @@ %% % STATES -% create state syms +% create state syms syms V1 V2 V3 f1 f2 f3 % create state vector model.sym.x = [V1, V2, V3, f1, f2, f3]; %% % PARAMETERS ( for these sensitivities will be computed ) -% create parameter syms -% create parameter vector +% create parameter syms +% create parameter vector model.sym.p = [ ]; -%% +%% % CONSTANTS ( for these no sensitivities will be computed ) % this part is optional and can be ommited % create parameter syms syms V1ss R1ss V2ss R2ss V3ss R3ss -% create parameter vector +% create parameter vector model.sym.k = [V1ss, R1ss, V2ss, R2ss, V3ss, R3ss]; %% % SYSTEM EQUATIONS % create symbolic variable for time -syms t f0 +syms t f0 model.sym.xdot = sym(zeros(size(model.sym.x))); p1=1; p2=1-R1ss; @@ -72,4 +72,3 @@ model.sym.y(5)=f1; model.sym.y(6)=f2; end - diff --git a/matlab/examples/example_dirac/example_dirac.m b/matlab/examples/example_dirac/example_dirac.m index 937a590027..ec1dc38326 100644 --- a/matlab/examples/example_dirac/example_dirac.m +++ b/matlab/examples/example_dirac/example_dirac.m @@ -56,7 +56,7 @@ function example_dirac() hold on plot(t,X_ode45(:,ix),'--','Color',c_x(ix,:)) end - + legend('x1','x1_{ode45}','x2','x2_{ode15s}','Location','NorthEastOutside') legend boxoff xlabel('time t') @@ -68,7 +68,7 @@ function example_dirac() ylim([1e-10,1e0]) legend('error x1','error x2','Location','NorthEastOutside') legend boxoff - + subplot(2,2,3) plot(t,sol.y,'.-','Color',c_x(1,:)) hold on @@ -78,7 +78,7 @@ function example_dirac() xlabel('time t') ylabel('y') box on - + subplot(2,2,4) plot(t,abs(sol.y-X_ode45(:,2)),'--') set(gca,'YScale','log') @@ -130,7 +130,7 @@ function example_dirac() xlabel('time t') ylabel('x') box on - + subplot(length(options.sens_ind),2,ip*2) plot(t,abs(sol.sx(:,:,ip)-sx_fd(:,:,ip)),'r--') legend('error x1','error x2','Location','NorthEastOutside') @@ -143,7 +143,7 @@ function example_dirac() box on end set(gcf,'Position',[100 300 1200 500]) - + figure for ip = 1:length(options.sens_ind) subplot(length(options.sens_ind),2,ip*2-1) @@ -159,7 +159,7 @@ function example_dirac() xlabel('time t') ylabel('y') box on - + subplot(length(options.sens_ind),2,ip*2) plot(t,abs(sol.sy(:,:,ip)-sy_fd(:,:,ip)),'r--') legend('error y1','Location','NorthEastOutside') @@ -172,8 +172,8 @@ function example_dirac() box on end set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac/model_dirac_syms.m b/matlab/examples/example_dirac/model_dirac_syms.m index 6c89ab26e2..3c0c9dd80a 100644 --- a/matlab/examples/example_dirac/model_dirac_syms.m +++ b/matlab/examples/example_dirac/model_dirac_syms.m @@ -15,12 +15,12 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and % 'lin' (default). -model.param = 'log10'; +model.param = 'log10'; %% % SYSTEM EQUATIONS @@ -49,4 +49,4 @@ model.sym.y = sym(zeros(1,1)); model.sym.y(1) = x2; -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_adjoint/example_dirac_adjoint.m b/matlab/examples/example_dirac_adjoint/example_dirac_adjoint.m index f5f2b23da0..60c1d327f9 100644 --- a/matlab/examples/example_dirac_adjoint/example_dirac_adjoint.m +++ b/matlab/examples/example_dirac_adjoint/example_dirac_adjoint.m @@ -79,7 +79,7 @@ function example_dirac_adjoint() xlabel('adjoint sensitivity absolute value of gradient element') ylabel('computed absolute value of gradient element') set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_adjoint/example_model_5_paper.m b/matlab/examples/example_dirac_adjoint/example_model_5_paper.m index d3c0758f60..93aab4b315 100644 --- a/matlab/examples/example_dirac_adjoint/example_model_5_paper.m +++ b/matlab/examples/example_dirac_adjoint/example_model_5_paper.m @@ -63,7 +63,7 @@ syms xB1(t) xB2(t) eqn1B = diff(xB1) == p(1)*xB1 - p(3)*xB2; eqn3B = diff(xB2) == p(4)*xB2; -syms sigma my +syms sigma my x = sym('x',[2,1]); J = -0.5*((x(2) - my)/sigma)^2; dJdx = jacobian(J,x); @@ -241,9 +241,3 @@ syms xB1(t) xB2(t) xlim([-0.1,4.1]) set(gcf,'PaperPositionMode','auto','Position',[100 300 300 200]) print('-depsc','-r300',['sJ_asa']) - - - - - - diff --git a/matlab/examples/example_dirac_adjoint/model_dirac_adjoint_syms.m b/matlab/examples/example_dirac_adjoint/model_dirac_adjoint_syms.m index 488d97b901..ce2d21f207 100644 --- a/matlab/examples/example_dirac_adjoint/model_dirac_adjoint_syms.m +++ b/matlab/examples/example_dirac_adjoint/model_dirac_adjoint_syms.m @@ -16,7 +16,7 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and @@ -51,4 +51,4 @@ model.sym.y(1) = x2; -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_adjoint_hessVecProd/example_dirac_adjoint_hessVecProd.m b/matlab/examples/example_dirac_adjoint_hessVecProd/example_dirac_adjoint_hessVecProd.m index 25cbd23ffb..4c4a76c9b1 100644 --- a/matlab/examples/example_dirac_adjoint_hessVecProd/example_dirac_adjoint_hessVecProd.m +++ b/matlab/examples/example_dirac_adjoint_hessVecProd/example_dirac_adjoint_hessVecProd.m @@ -56,4 +56,3 @@ fprintf('Finite differences, HVP: \n'); disp(FD_HVP); - diff --git a/matlab/examples/example_dirac_adjoint_hessVecProd/model_dirac_adjoint_hessVecProd_syms.m b/matlab/examples/example_dirac_adjoint_hessVecProd/model_dirac_adjoint_hessVecProd_syms.m index b254bfcc5f..8f144cb4c9 100644 --- a/matlab/examples/example_dirac_adjoint_hessVecProd/model_dirac_adjoint_hessVecProd_syms.m +++ b/matlab/examples/example_dirac_adjoint_hessVecProd/model_dirac_adjoint_hessVecProd_syms.m @@ -16,7 +16,7 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and @@ -51,4 +51,4 @@ model.sym.y(1) = x2; -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_secondorder/example_dirac_secondorder.m b/matlab/examples/example_dirac_secondorder/example_dirac_secondorder.m index cba5ba56cc..fe9392ead6 100644 --- a/matlab/examples/example_dirac_secondorder/example_dirac_secondorder.m +++ b/matlab/examples/example_dirac_secondorder/example_dirac_secondorder.m @@ -87,7 +87,7 @@ function example_dirac_secondorder() end end set(gcf,'Position',[100 300 1200 500]) - + drawnow end diff --git a/matlab/examples/example_dirac_secondorder/model_dirac_secondorder_syms.m b/matlab/examples/example_dirac_secondorder/model_dirac_secondorder_syms.m index 657f2216a8..c2db18d77d 100644 --- a/matlab/examples/example_dirac_secondorder/model_dirac_secondorder_syms.m +++ b/matlab/examples/example_dirac_secondorder/model_dirac_secondorder_syms.m @@ -1,5 +1,5 @@ function [model] = model_dirac_secondorder_syms() - + %% % STATES @@ -15,7 +15,7 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and @@ -49,4 +49,4 @@ model.sym.y = sym(zeros(1,1)); model.sym.y(1) = x2; -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_secondorder_vectmult/example_dirac_secondorder_vectmult.m b/matlab/examples/example_dirac_secondorder_vectmult/example_dirac_secondorder_vectmult.m index 327e268faf..e7f4c9e3b3 100644 --- a/matlab/examples/example_dirac_secondorder_vectmult/example_dirac_secondorder_vectmult.m +++ b/matlab/examples/example_dirac_secondorder_vectmult/example_dirac_secondorder_vectmult.m @@ -64,7 +64,7 @@ function example_dirac_secondorder_vectmult() xlabel('time t') ylabel('x') box on - + subplot(4,2,ip*2) plot(t,abs(sol.s2x(:,:,ip)-s2x_fd(:,:,ip)),'r--') legend('error x1','error x2','Location','NorthEastOutside') @@ -77,7 +77,7 @@ function example_dirac_secondorder_vectmult() box on end set(gcf,'Position',[100 300 1200 500]) - + figure for ip = 1:4 subplot(4,2,ip*2-1) @@ -93,7 +93,7 @@ function example_dirac_secondorder_vectmult() xlabel('time t') ylabel('y') box on - + subplot(4,2,ip*2) plot(t,abs(sol.s2y(:,:,ip)-s2y_fd(:,:,ip)),'r--') legend('error y1','Location','NorthEastOutside') @@ -106,8 +106,8 @@ function example_dirac_secondorder_vectmult() box on end set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_dirac_secondorder_vectmult/model_dirac_secondorder_vectmult_syms.m b/matlab/examples/example_dirac_secondorder_vectmult/model_dirac_secondorder_vectmult_syms.m index cc366495f1..5f217f4dad 100644 --- a/matlab/examples/example_dirac_secondorder_vectmult/model_dirac_secondorder_vectmult_syms.m +++ b/matlab/examples/example_dirac_secondorder_vectmult/model_dirac_secondorder_vectmult_syms.m @@ -1,5 +1,5 @@ function [model] = model_dirac_secondorder_vectmult_syms() - + %% % STATES @@ -15,12 +15,12 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and % 'lin' (default). -model.param = 'log10'; +model.param = 'log10'; %% % SYSTEM EQUATIONS @@ -49,4 +49,4 @@ model.sym.y = sym(zeros(1,1)); model.sym.y(1) = x2; -end \ No newline at end of file +end diff --git a/matlab/examples/example_events/example_events.m b/matlab/examples/example_events/example_events.m index 1f5e2a47f4..8dfcdbb21e 100644 --- a/matlab/examples/example_events/example_events.m +++ b/matlab/examples/example_events/example_events.m @@ -64,7 +64,7 @@ function example_events() legend('error x1','error x2','error x3','Location','NorthEastOutside') legend boxoff ylabel('x') - + subplot(2,2,3) plot(t,sol.y,'.-','Color',c_x(1,:)) hold on @@ -74,7 +74,7 @@ function example_events() xlabel('time t') ylabel('y') box on - + subplot(2,2,4) plot(t,abs(sol.y-p(4)*sum(X_ode15s,2)),'--') set(gca,'YScale','log') @@ -83,7 +83,7 @@ function example_events() xlabel('time t') ylabel('y') box on - + set(gcf,'Position',[100 300 1200 500]) end @@ -125,7 +125,7 @@ function example_events() xlabel('time t') ylabel('sx') box on - + subplot(4,2,ip*2) plot(t,abs(sol.sx(:,:,ip)-sx_fd(:,:,ip)),'--') legend('error sx1','error sx2','error sx3','Location','NorthEastOutside') @@ -137,7 +137,7 @@ function example_events() box on end set(gcf,'Position',[100 300 1200 500]) - + figure for ip = 1:4 subplot(4,2,ip*2-1) @@ -152,7 +152,7 @@ function example_events() xlabel('time t') ylabel('sy') box on - + subplot(4,2,ip*2) plot(t,abs(sol.sy(:,:,ip)-sy_fd(:,:,ip)),'--') legend('error sy1','Location','NorthEastOutside') @@ -164,7 +164,7 @@ function example_events() box on end set(gcf,'Position',[100 300 1200 500]) - + figure for ip = 1:4 subplot(4,2,2*ip-1) @@ -177,7 +177,7 @@ function example_events() xlabel('event #') ylabel('sz') box on - + subplot(4,2,2*ip) bar(1:D.ne,sol.sz(1:D.ne,:,ip)-sz_fd(1:D.ne,:,ip),0.8) hold on @@ -189,7 +189,7 @@ function example_events() box on end set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_events/model_events_syms.m b/matlab/examples/example_events/model_events_syms.m index f98298c8dd..7bf9ac6d32 100644 --- a/matlab/examples/example_events/model_events_syms.m +++ b/matlab/examples/example_events/model_events_syms.m @@ -22,12 +22,12 @@ % create parameter syms syms p1 p2 p3 p4 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4]; % set the parametrisation of the problem options are 'log', 'log10' and % 'lin' (default). -model.param = 'log10'; +model.param = 'log10'; %% % CONSTANTS ( for these no sensitivities will be computed ) @@ -36,7 +36,7 @@ % create parameter syms syms k1 k2 k3 k4 -% create parameter vector +% create parameter vector model.sym.k = [k1 k2 k3 k4]; %% @@ -79,4 +79,4 @@ model.event(1) = amievent(am_ge(x2,x3),0,t); model.event(2) = amievent(am_ge(x1,x3),0,t); -end \ No newline at end of file +end diff --git a/matlab/examples/example_jakstat_adjoint/example_jakstat_adjoint.m b/matlab/examples/example_jakstat_adjoint/example_jakstat_adjoint.m index 55cd78dbe0..45a9002629 100644 --- a/matlab/examples/example_jakstat_adjoint/example_jakstat_adjoint.m +++ b/matlab/examples/example_jakstat_adjoint/example_jakstat_adjoint.m @@ -1,17 +1,17 @@ function example_jakstat_adjoint() - + % compile the model [exdir,~,~]=fileparts(which('example_jakstat_adjoint.m')); amiwrap('model_jakstat_adjoint','model_jakstat_adjoint_syms',exdir,1) - + num = xlsread(fullfile(exdir,'pnas_data_original.xls')); - + D.t = num(:,1); D.condition= [1.4,0.45]; D.Y = num(:,[2,4,6]); D.Sigma_Y = NaN(size(D.Y)); D = amidata(D); - + xi = [0.60 3 -0.95 @@ -29,10 +29,10 @@ function example_jakstat_adjoint() -0.5 0 -0.5]; - + options.sensi = 0; sol = simulate_model_jakstat_adjoint([],xi,[],D,options); - + if(usejava('jvm')) figure for iy = 1:3 @@ -54,7 +54,7 @@ function example_jakstat_adjoint() end set(gcf,'Position',[100 300 1200 500]) end - + % generate new xi_rand = xi + 0.1; options.sensi = 2; @@ -62,7 +62,7 @@ function example_jakstat_adjoint() sol = simulate_model_jakstat_adjoint([],xi_rand,[],D,options); options.sensi_meth = 'forward'; solf = simulate_model_jakstat_adjoint([],xi_rand,[],D,options); - + options.sensi = 1; eps = 1e-4; fd_grad = NaN(length(xi),1); @@ -73,7 +73,7 @@ function example_jakstat_adjoint() fd_grad(ip) = (psol.llh-sol.llh)/eps; fd_hess(:,ip) = (psol.sllh-sol.sllh)/eps; end - + if(usejava('jvm')) figure subplot(1,2,1) @@ -91,7 +91,7 @@ function example_jakstat_adjoint() axis square xlabel('absolute value forward sensitivity gradient entries') ylabel('absolute value gradient entries') - + subplot(1,2,2) plot(abs(solf.s2llh(:)),abs(fd_hess(:)),'rx') hold on @@ -107,11 +107,11 @@ function example_jakstat_adjoint() axis square xlabel('absolute value forward sensitivity hessian entries') ylabel('absolute value hessian entries') - + set(gcf,'Position',[100 300 1200 500]) end - - + + drawnow - + end diff --git a/matlab/examples/example_jakstat_adjoint/model_jakstat_adjoint_syms.m b/matlab/examples/example_jakstat_adjoint/model_jakstat_adjoint_syms.m index d1426d53be..a1e937e162 100644 --- a/matlab/examples/example_jakstat_adjoint/model_jakstat_adjoint_syms.m +++ b/matlab/examples/example_jakstat_adjoint/model_jakstat_adjoint_syms.m @@ -1,36 +1,36 @@ function [model] = model_jakstat_syms() - + %% % STATES - + syms STAT pSTAT pSTAT_pSTAT npSTAT_npSTAT nSTAT1 nSTAT2 nSTAT3 nSTAT4 nSTAT5 - + model.sym.x = [ STAT, pSTAT, pSTAT_pSTAT, npSTAT_npSTAT, nSTAT1, nSTAT2, nSTAT3, nSTAT4, nSTAT5 ... ]; %% % PARAMETERS - + syms p1 p2 p3 p4 init_STAT Omega_cyt Omega_nuc sp1 sp2 sp3 sp4 sp5 offset_tSTAT offset_pSTAT scale_tSTAT scale_pSTAT sigma_pSTAT sigma_tSTAT sigma_pEpoR - + model.sym.p = [p1,p2,p3,p4,init_STAT,sp1,sp2,sp3,sp4,sp5,offset_tSTAT,offset_pSTAT,scale_tSTAT,scale_pSTAT,sigma_pSTAT,sigma_tSTAT,sigma_pEpoR]; - + model.param = 'log10'; - + model.sym.k = [Omega_cyt,Omega_nuc]; - + %% % INPUT syms t % u(1) = spline_pos5(t, 0.0, sp1, 5.0, sp2, 10.0, sp3, 20.0, sp4, 60.0, sp5, 0, 0.0); u(1) = am_spline_pos(t, 5, 0.0, sp1, 5.0, sp2, 10.0, sp3, 20.0, sp4, 60.0, sp5, 0, 0.0); - + %% % SYSTEM EQUATIONS - + model.sym.xdot = sym(zeros(size(model.sym.x))); - + model.sym.xdot(1) = (Omega_nuc*p4*nSTAT5 - Omega_cyt*STAT*p1*u(1))/Omega_cyt; model.sym.xdot(2) = STAT*p1*u(1) - 2*p2*pSTAT^2; model.sym.xdot(3) = p2*pSTAT^2 - p3*pSTAT_pSTAT; @@ -40,30 +40,30 @@ model.sym.xdot(7) = p4*(nSTAT2 - nSTAT3); model.sym.xdot(8) = p4*(nSTAT3 - nSTAT4); model.sym.xdot(9) = p4*(nSTAT4 - nSTAT5); - + %% % INITIAL CONDITIONS - + model.sym.x0 = sym(zeros(size(model.sym.x))); - + model.sym.x0(1) = init_STAT; - + %% % OBSERVABLES - + model.sym.y = sym(zeros(3,1)); - + model.sym.y(1) = offset_pSTAT + scale_pSTAT/init_STAT*(pSTAT + 2*pSTAT_pSTAT); model.sym.y(2) = offset_tSTAT + scale_tSTAT/init_STAT*(STAT + pSTAT + 2*(pSTAT_pSTAT)); model.sym.y(3) = u(1); - + %% % SIGMA - + model.sym.sigma_y = sym(size(model.sym.y)); - + model.sym.sigma_y(1) = sigma_pSTAT; model.sym.sigma_y(2) = sigma_tSTAT; model.sym.sigma_y(3) = sigma_pEpoR; - -end \ No newline at end of file + +end diff --git a/matlab/examples/example_jakstat_adjoint_hvp/example_jakstat_adjoint_hvp.m b/matlab/examples/example_jakstat_adjoint_hvp/example_jakstat_adjoint_hvp.m index 2654e7bd93..2865fd7b22 100644 --- a/matlab/examples/example_jakstat_adjoint_hvp/example_jakstat_adjoint_hvp.m +++ b/matlab/examples/example_jakstat_adjoint_hvp/example_jakstat_adjoint_hvp.m @@ -1,16 +1,16 @@ function example_jakstat_adjoint_hvp() - + % compile the model [exdir,~,~]=fileparts(which('example_jakstat_adjoint_hvp.m')); amiwrap('model_jakstat_adjoint_hvp','model_jakstat_adjoint_hvp_syms',exdir,2) num = xlsread(fullfile(exdir,'pnas_data_original.xls')); - + D.t = num(:,1); D.condition= [1.4,0.45]; D.Y = num(:,[2,4,6]); D.Sigma_Y = NaN(size(D.Y)); D = amidata(D); - + xi = [0.60 3 -0.95 @@ -28,26 +28,26 @@ function example_jakstat_adjoint_hvp() -0.5 0 -0.5]; - - + + % generate new xi_rand = xi - 0.1; options.atol = 1e-12; options.rtol = 1e-12; - + % Get time for simulation tic; options.sensi = 0; sol0 = simulate_model_jakstat_adjoint_hvp([],xi_rand,[],D,options); t0 = toc; - + % Get time for usual evaluation tic; options.sensi = 1; options.sensi_meth = 'adjoint'; sol1 = simulate_model_jakstat_adjoint_hvp([],xi_rand,[],D,options); t1 = toc; - + % Get time for Finite Differences hvp = zeros(17,1); hvp_f = zeros(17,1); @@ -63,7 +63,7 @@ function example_jakstat_adjoint_hvp() hvp_f = hvp_f + (solp.sllh - sol2.sllh) / (delta); hvp_b = hvp_b + (sol2.sllh - solm.sllh) / (delta); t2 = toc; - + % Get time for Second order adjoints hvpasa = zeros(17,1); tic; @@ -81,7 +81,7 @@ function example_jakstat_adjoint_hvp() if(usejava('jvm')) figure(); - + subplot(1,2,1); bar([abs((sol.s2llh-hvp)./sol.s2llh),abs((sol.s2llh-hvp_f)./sol.s2llh),abs((sol.s2llh-hvp_b)./sol.s2llh),abs((sol.s2llh-solf.s2llh)./sol.s2llh)]) hold on @@ -95,13 +95,13 @@ function example_jakstat_adjoint_hvp() legend('FD_{central}','FD_{forward}','FD_{backward}','forward sensitivities','Orientation','horizontal') legend boxoff set(gcf,'Position',[100 300 1200 500]) - + subplot(1,2,2); hold on; bar([t0,t1,t2,t3]); xlabel('runtime [s]') set(gca,'XTick',1:4,'XTickLabel',{'ODE Integration', 'Gradient computation (ASA)', 'HVP from FD via 1st order ASA', 'HVP via 2nd order ASA'},'XTickLabelRotation',20); - + box on; hold off; end diff --git a/matlab/examples/example_jakstat_adjoint_hvp/model_jakstat_adjoint_hvp_syms.m b/matlab/examples/example_jakstat_adjoint_hvp/model_jakstat_adjoint_hvp_syms.m index b14eaacdb5..ab145a0472 100644 --- a/matlab/examples/example_jakstat_adjoint_hvp/model_jakstat_adjoint_hvp_syms.m +++ b/matlab/examples/example_jakstat_adjoint_hvp/model_jakstat_adjoint_hvp_syms.m @@ -1,36 +1,36 @@ function [model] = model_jakstat_adjoint_hvp_syms() - + %% % STATES - + syms STAT pSTAT pSTAT_pSTAT npSTAT_npSTAT nSTAT1 nSTAT2 nSTAT3 nSTAT4 nSTAT5 - + model.sym.x = [ STAT, pSTAT, pSTAT_pSTAT, npSTAT_npSTAT, nSTAT1, nSTAT2, nSTAT3, nSTAT4, nSTAT5 ... ]; - + %% % PARAMETERS - + syms p1 p2 p3 p4 init_STAT Omega_cyt Omega_nuc sp1 sp2 sp3 sp4 sp5 offset_tSTAT offset_pSTAT scale_tSTAT scale_pSTAT sigma_pSTAT sigma_tSTAT sigma_pEpoR - + model.sym.p = [p1,p2,p3,p4,init_STAT,sp1,sp2,sp3,sp4,sp5,offset_tSTAT,offset_pSTAT,scale_tSTAT,scale_pSTAT,sigma_pSTAT,sigma_tSTAT,sigma_pEpoR]; - + model.param = 'log10'; - + model.sym.k = [Omega_cyt,Omega_nuc]; - + %% % INPUT syms t u(1) = am_spline_pos(t, 5, 0, sp1, 5.0, sp2, 10.0, sp3, 20.0, sp4, 60.0, sp5, 0, 0); % u(1) = spline_pos5(t, 0, sp1, 5.0, sp2, 10.0, sp3, 20.0, sp4, 60.0, sp5, 0, 0); - + %% % SYSTEM EQUATIONS - + model.sym.xdot = sym(zeros(size(model.sym.x))); - + model.sym.xdot(1) = (Omega_nuc*p4*nSTAT5 - Omega_cyt*STAT*p1*u(1))/Omega_cyt; model.sym.xdot(2) = STAT*p1*u(1) - 2*p2*pSTAT^2; model.sym.xdot(3) = p2*pSTAT^2 - p3*pSTAT_pSTAT; @@ -40,30 +40,30 @@ model.sym.xdot(7) = p4*(nSTAT2 - nSTAT3); model.sym.xdot(8) = p4*(nSTAT3 - nSTAT4); model.sym.xdot(9) = p4*(nSTAT4 - nSTAT5); - + %% % INITIAL CONDITIONS - + model.sym.x0 = sym(zeros(size(model.sym.x))); model.sym.x0(1) = init_STAT; - + %% % OBSERVABLES - + model.sym.y = sym(zeros(3,1)); - + model.sym.y(1) = offset_pSTAT + scale_pSTAT/init_STAT*(pSTAT + 2*pSTAT_pSTAT); model.sym.y(2) = offset_tSTAT + scale_tSTAT/init_STAT*(STAT + pSTAT + 2*(pSTAT_pSTAT)); model.sym.y(3) = u(1); - + %% % SIGMA - + model.sym.sigma_y = sym(size(model.sym.y)); - + model.sym.sigma_y(1) = sigma_pSTAT; model.sym.sigma_y(2) = sigma_tSTAT; model.sym.sigma_y(3) = sigma_pEpoR; - -end \ No newline at end of file + +end diff --git a/matlab/examples/example_nested_events/example_nested_events.m b/matlab/examples/example_nested_events/example_nested_events.m index 1bd5ed9d5b..52d46b97a3 100644 --- a/matlab/examples/example_nested_events/example_nested_events.m +++ b/matlab/examples/example_nested_events/example_nested_events.m @@ -70,7 +70,7 @@ function example_events() legend('error x1','Location','NorthEastOutside') legend boxoff ylabel('x') - + set(gcf,'Position',[100 300 1200 300]) end @@ -111,7 +111,7 @@ function example_events() xlabel('time t') ylabel('sx') box on - + subplot(5,2,ip*2) plot(t,abs(sol.sx(:,:,ip)-sx_fd(:,:,ip)),'--') legend('error sx1','Location','NorthEastOutside') @@ -123,7 +123,7 @@ function example_events() box on end set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_nested_events/model_nested_events_syms.m b/matlab/examples/example_nested_events/model_nested_events_syms.m index 42a1b48ae1..1bfa941a35 100644 --- a/matlab/examples/example_nested_events/model_nested_events_syms.m +++ b/matlab/examples/example_nested_events/model_nested_events_syms.m @@ -1,4 +1,4 @@ -function [model] = model_nested_events_syms() +function [model] = model_nested_events_syms() %% CVODES OPTIONS % set the parametrisation of the problem options are 'log', 'log10' and 'lin' (default) @@ -61,4 +61,4 @@ model.sym.x0 = x0; model.sym.y = y; -end \ No newline at end of file +end diff --git a/matlab/examples/example_neuron/example_neuron.m b/matlab/examples/example_neuron/example_neuron.m index d05b0cfe65..603c1af0c6 100644 --- a/matlab/examples/example_neuron/example_neuron.m +++ b/matlab/examples/example_neuron/example_neuron.m @@ -34,7 +34,7 @@ function example_neuron() t = linspace(0,D.Z(end)-0.1,100); D.Z = D.Z + 0.5*randn(size(D.Z)); D.Z(3) = NaN; -D.Sigma_Z = 0.5*ones(size(D.Z)); +D.Sigma_Z = 0.5*ones(size(D.Z)); D.Z = D.Z + D.Sigma_Z.*randn(size(D.Z)); D.t = t; @@ -64,7 +64,7 @@ function example_neuron() hold on end stem(sol.z,zeros(size(sol.z))) - + legend('x1','x2','events','Location','NorthEastOutside') legend boxoff xlabel('time t') @@ -135,7 +135,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,4) plot(abs(sol.sz(:)),abs(sol.sz(:)-sz_fd(:)),'ro') hold on @@ -149,7 +149,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,2) hold on plot(abs(sol.srz(:)),abs(srz_fd(:)),'bo') @@ -164,7 +164,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,5) plot(abs(sol.srz(:)),abs(sol.srz(:)-srz_fd(:)),'ro') hold on @@ -178,7 +178,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,3) hold on plot(abs(sol.sllh),abs(sllh_fd),'ko') @@ -193,7 +193,7 @@ function example_neuron() title('abs llh sensitivity') box on axis square - + subplot(2,3,6) plot(abs(sol.sllh),abs(sol.sllh-sllh_fd),'ro') hold on @@ -208,7 +208,7 @@ function example_neuron() box on axis square set(gcf,'Position',[100 300 1200 500]) - + figure subplot(2,3,1) hold on @@ -224,7 +224,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,4) plot(abs(sol.s2z(:)),abs(sol.s2z(:)-s2z_fd(:)),'ro') hold on @@ -238,7 +238,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,2) hold on plot(abs(sol.s2rz(:)),abs(s2rz_fd(:)),'bo') @@ -253,7 +253,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,5) plot(abs(sol.s2rz(:)),abs(sol.s2rz(:)-s2rz_fd(:)),'ro') hold on @@ -267,7 +267,7 @@ function example_neuron() set(gca,'XScale','log') box on axis square - + subplot(2,3,3) hold on plot(abs(sol.s2llh),abs(s2llh_fd),'ko') @@ -282,7 +282,7 @@ function example_neuron() title('abs llh sensitivity') box on axis square - + subplot(2,3,6) plot(abs(sol.s2llh),abs(sol.s2llh-s2llh_fd),'ro') hold on @@ -297,8 +297,8 @@ function example_neuron() box on axis square set(gcf,'Position',[100 300 1200 500]) - + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_neuron/model_neuron_syms.m b/matlab/examples/example_neuron/model_neuron_syms.m index e5786fb8ee..fddb869464 100644 --- a/matlab/examples/example_neuron/model_neuron_syms.m +++ b/matlab/examples/example_neuron/model_neuron_syms.m @@ -1,33 +1,33 @@ function model = neuron_syms() - + model.param = 'log10'; - - syms a b c d - + + syms a b c d + p = [a b c d]; - + syms v0 I0 - + k = [v0,I0]; - + syms v u - + x = [v u]; - + syms I t - + I = I0; - + f(1) = 0.04*v^2 + 5*v + 140 - u + I ; f(2) = a*(b*v - u); y(1) = v; - - + + x0 = [v0,b*v0]; - + event = amievent(v-30,[-c-v,d],t); - + model.sym.p = p; model.sym.k = k; model.sym.x = x; @@ -35,6 +35,6 @@ model.sym.f = f; model.event = event; model.sym.x0 = x0; - - -end \ No newline at end of file + + +end diff --git a/matlab/examples/example_robertson/example_robertson.m b/matlab/examples/example_robertson/example_robertson.m index a5daeaa8b4..b6b2bc08fc 100644 --- a/matlab/examples/example_robertson/example_robertson.m +++ b/matlab/examples/example_robertson/example_robertson.m @@ -70,7 +70,7 @@ function example_robertson() legend('error x1','error x2','error x3','Location','NorthEastOutside') legend boxoff ylabel('x') - + set(gcf,'Position',[100 300 1200 500]) end @@ -111,7 +111,7 @@ function example_robertson() set(gca,'XScale','log') ylabel('sx') box on - + subplot(length(p),2,ip*2) plot(t,abs(sol.sy(:,:,ip)-sy_fd(:,:,ip)),'--') legend('error sy1','error sy2','error sy3','Location','NorthEastOutside') @@ -124,8 +124,8 @@ function example_robertson() box on end set(gcf,'Position',[100 300 1200 500]) - - + + drawnow end -end \ No newline at end of file +end diff --git a/matlab/examples/example_robertson/model_robertson_syms.m b/matlab/examples/example_robertson/model_robertson_syms.m index bf95c21212..9cca91e566 100644 --- a/matlab/examples/example_robertson/model_robertson_syms.m +++ b/matlab/examples/example_robertson/model_robertson_syms.m @@ -18,12 +18,12 @@ % create parameter syms syms p1 p2 p3 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3]; % set the parametrisation of the problem options are 'log', 'log10' and % 'lin' (default). -model.param = 'log10'; +model.param = 'log10'; %% @@ -33,7 +33,7 @@ % create parameter syms syms k1 -% create parameter vector +% create parameter vector model.sym.k = [k1]; %% @@ -65,4 +65,4 @@ model.sym.y = model.sym.x; model.sym.y(2) = 1e4*model.sym.x(2); -end \ No newline at end of file +end diff --git a/matlab/examples/example_steadystate/example_steadystate.m b/matlab/examples/example_steadystate/example_steadystate.m index 118b1f5cf9..a6503527ff 100644 --- a/matlab/examples/example_steadystate/example_steadystate.m +++ b/matlab/examples/example_steadystate/example_steadystate.m @@ -1,46 +1,46 @@ function example_steadystate %% % COMPILATION - + [exdir,~,~]=fileparts(which('example_steadystate.m')); % compile the model amiwrap('model_steadystate','model_steadystate_syms',exdir) - + %% % SIMULATION - + % time vector t = linspace(0,100,50); p = [1;0.5;0.4;2;0.1]; k = [0.1,0.4,0.7,1]; - + options = amioption(... 'sensi', 0, ... 'maxsteps', 1e4 ... ); - + % load mex into memory simulate_model_steadystate(t,log10(p),k,[],options); - + tic; sol = simulate_model_steadystate([t, inf],log10(p),k,[],options); display(['Time elapsed with cvodes: ' num2str(toc) ' seconds']); - + %% % ODE15S - + ode_system = @(t,x,p,k) [-2*p(1)*x(1)^2 - p(2)*x(1)*x(2) + 2*p(3)*x(2) + p(4)*x(3) + p(5); + p(1)*x(1)^2 - p(2)*x(1)*x(2) - p(3)*x(2) + p(4)*x(3); + p(2)*x(1)*x(2) - p(4)*x(3) - k(4)*x(3)]; options_ode15s = odeset('RelTol',options.rtol,'AbsTol',options.atol,'MaxStep',options.maxsteps); - + tic; [~, X_ode15s] = ode15s(@(t,x) ode_system(t,x,p,k),t,k(1:3),options_ode15s); disp(['Time elapsed with ode15s: ' num2str(toc) ' seconds']) - + %% % PLOTTING - + if(usejava('jvm')) figure('Name', 'Example SteadyState'); c_x = get(gca,'ColorOrder'); @@ -63,19 +63,19 @@ legend boxoff; set(gcf,'Position',[100 300 1200 500]); end - + %% % FORWARD SENSITIVITY ANALYSIS - + options.sensi = 1; options.sens_ind = [3,1,2,4]; sol = simulate_model_steadystate([t, inf],log10(p),k,[],options); - + %% % FINITE DIFFERENCES - + eps = 1e-3; - + xi = log10(p); sx_ffd = zeros(length(t)+1, 3, length(p)); sx_bfd = zeros(length(t)+1, 3, length(p)); @@ -91,7 +91,7 @@ sx_bfd(:,:,ip) = (sol.x - solm.x) / eps; sx_cfd(:,:,ip) = (solp.x - solm.x) / (2*eps); end - + %% % PLOTTING if(usejava('jvm')) @@ -122,10 +122,10 @@ box on; end set(gcf,'Position',[100 300 1200 500]); - + sxss = squeeze(sol.sx(length(t),:,:)); sxss_fd = squeeze(sx_cfd(length(t),:,options.sens_ind)); - + % Sensitivities for steady state figure('Name', 'Example SteadyState'); subplot(1,2,1); @@ -148,8 +148,8 @@ xlabel('Steady state sensitivities'); ylabel('finite differences'); box on; - - + + subplot(1,2,2); hold on; for ip = 1:4 @@ -172,10 +172,10 @@ set(gca,'YScale','log'); set(gcf,'Position',[100 300 1200 500]); end - + %% % XDOT FOR DIFFERENT TIME POINTS - + t = [10,25,100,250,1000]; options.sensi = 0; ssxdot = NaN(length(t), size(sol.x, 2)); @@ -187,13 +187,13 @@ % Compute steady state wihtout integration before sol = simulate_model_steadystate(inf,log10(p),k,[],options); - + % Test recapturing in the case of Newton solver failing options.newton_maxsteps = 4; options.maxsteps = 300; sol_newton_fail = simulate_model_steadystate(inf,log10(p),k,[],options); - + %% % PLOTTING if(usejava('jvm')) @@ -216,7 +216,7 @@ box on; set(gca,'YScale','log'); set(gca,'XScale','log'); - + subplot(1,3,3); hold on; bar(sol_newton_fail.diagnosis.posteq_numsteps([1, 3])); @@ -229,10 +229,8 @@ a = gca(); a.Children.BarWidth = 0.6; box on; - + set(gcf,'Position',[100 300 1200 500]); end - -end - +end diff --git a/matlab/examples/example_steadystate/model_steadystate_syms.m b/matlab/examples/example_steadystate/model_steadystate_syms.m index 713cd1ad6a..75b441cff6 100644 --- a/matlab/examples/example_steadystate/model_steadystate_syms.m +++ b/matlab/examples/example_steadystate/model_steadystate_syms.m @@ -18,12 +18,12 @@ % create parameter syms syms p1 p2 p3 p4 p5 -% create parameter vector +% create parameter vector model.sym.p = [p1,p2,p3,p4,p5]; % set the parametrisation of the problem options are 'log', 'log10' and % 'lin' (default). -model.param = 'log10'; +model.param = 'log10'; %% @@ -33,7 +33,7 @@ % create parameter syms syms k1 k2 k3 k4 -% create parameter vector +% create parameter vector model.sym.k = [k1 k2 k3 k4]; %% @@ -63,4 +63,4 @@ % OBSERVALES model.sym.y = model.sym.x; -end \ No newline at end of file +end diff --git a/matlab/installAMICI.m b/matlab/installAMICI.m index 9a6ccbc1c4..d3e40dd107 100644 --- a/matlab/installAMICI.m +++ b/matlab/installAMICI.m @@ -3,4 +3,4 @@ addpath(fullfile(amipath,'auxiliary')) addpath(fullfile(amipath,'auxiliary','CalcMD5')) addpath(fullfile(amipath,'symbolic')) -addpath(fullfile(amipath,'SBMLimporter')) \ No newline at end of file +addpath(fullfile(amipath,'SBMLimporter')) diff --git a/matlab/mtoc/MatlabDocMaker.m b/matlab/mtoc/MatlabDocMaker.m index cdbcfd7dd9..8160635238 100644 --- a/matlab/mtoc/MatlabDocMaker.m +++ b/matlab/mtoc/MatlabDocMaker.m @@ -23,7 +23,7 @@ % @change{1,5,dw,2013-12-03} Fixed default value selection for properties, % now not having set a description or logo does not cause an error to be % thrown. -% +% % @change{1,5,dw,2013-02-21} Fixed the callback for suggested direct documentation creation % after MatlabDocMaker.setup (Thanks to Aurelien Queffurust) % @@ -111,7 +111,7 @@ % % @type char @default 'Doxyfile.template' DOXYFILE_TEMPLATE = 'Doxyfile.template'; - + % File name for the latex extras style file processed by the MatlabDocMaker. % % Assumed to reside in the MatlabDocMaker.getConfigDirectory. @@ -119,7 +119,7 @@ % % @type char @default 'latexextras.template' LATEXEXTRAS_TEMPLATE = 'latexextras.template'; - + % File name the mtoc++ configuration file. % % Assumed to reside in the MatlabDocMaker.getConfigDirectory. @@ -139,17 +139,17 @@ % % Return values: % name: The project name @type char - + %error('Please replace this by returning your project name as string.'); % Example: name = 'AMICI'; end end - + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% End of user defined part. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - + methods(Static, Sealed) function dir = getOutputDirectory % Returns the directory where the applications source files @@ -159,7 +159,7 @@ % dir: The output directory @type char dir = MatlabDocMaker.getPref('outdir'); end - + function dir = getSourceDirectory % Returns the directory where the applications source files % reside @@ -168,7 +168,7 @@ % dir: The project source directory @type char dir = MatlabDocMaker.getPref('srcdir'); end - + function dir = getConfigDirectory % Returns the directory where the applications documentation % configuration files reside @@ -180,7 +180,7 @@ % dir: The documentation configuration directory @type char dir = MatlabDocMaker.getPref('confdir'); end - + function desc = getProjectDescription % Returns the short project description. % @@ -190,7 +190,7 @@ % See also: setProjectDescription desc = MatlabDocMaker.getPref('proj_desc', ''); end - + function setProjectDescription(value) % Sets the project description. % @@ -203,7 +203,7 @@ function setProjectDescription(value) end MatlabDocMaker.setPref('proj_desc', value); end - + function version = getProjectVersion % Returns the current version of the project. % @@ -216,7 +216,7 @@ function setProjectDescription(value) % See also: setProjectVersion version = MatlabDocMaker.getPref('proj_ver', '0'); end - + function setProjectVersion(value) % Sets the project version. % @@ -229,7 +229,7 @@ function setProjectVersion(value) end MatlabDocMaker.setPref('proj_ver', value); end - + function fullPath = getProjectLogo % Returns the logo image file for the project. Either an absolute path or a plain % filename. For the latter case the image file is assumed to reside inside the @@ -253,7 +253,7 @@ function setProjectVersion(value) end end end - + function setProjectLogo(value) % Sets the project logo. Set to '' to unset. % @@ -289,9 +289,9 @@ function setProjectLogo(value) MatlabDocMaker.setPref('proj_logo', value); end end - + methods(Static) - + function open % Opens the generated documentation. % @@ -308,7 +308,7 @@ function setProjectLogo(value) end end end - + function create(varargin) % Creates the Doxygen documentation % @@ -318,14 +318,14 @@ function create(varargin) % successful compilation @type logical @default false % latex: Set to true if `\text{\LaTeX}` output should be generated, too. @type logical % @default false - + %% Preparations ip = inputParser; ip.addParameter('open',false,@islogical); ip.addParameter('latex',false,@islogical); ip.parse(varargin{:}); genlatex = ip.Results.latex; - + % Check for correct setup cdir = MatlabDocMaker.getConfigDirectory; srcdir = MatlabDocMaker.getSourceDirectory; @@ -335,7 +335,7 @@ function create(varargin) if exist(doxyfile_in,'file') ~= 2 error('No doxygen configuration file template found at "%s"',doxyfile_in); end - + lstr = ''; if genlatex lstr = '(+Latex)'; @@ -344,7 +344,7 @@ function create(varargin) 'Sources: %s\nOutput to: %s\nCreating config files...'],lstr,... MatlabDocMaker.getProjectName,MatlabDocMaker.getProjectVersion,... srcdir,outdir); - + % Operation-system dependent strings strs = struct; if isunix @@ -356,17 +356,17 @@ function create(varargin) else error('Current platform not supported.'); end - + % Save current working dir and change into the KerMor home % directory; only from there all classes and packages are % detected properly. curdir = pwd; cd(srcdir); - + % Append the configuration file directory to the current PATH pathadd = [pathsep cdir]; setenv('PATH',[getenv('PATH') pathadd]); - + mtoc_conf = fullfile(cdir,MatlabDocMaker.MTOCPP_CONFIGFILE); filter = sprintf('%smtocpp',strs.silencer); if exist(mtoc_conf,'file') @@ -381,7 +381,7 @@ function create(varargin) end %% Creation part cdir = MatlabDocMaker.getConfigDirectory; - % Create "configured" filter script for inclusion in doxygen + % Create "configured" filter script for inclusion in doxygen filter = fullfile(cdir,strs.filter); f = fopen(filter,'w'); fprintf(f,'%smtocpp %s %s',strs.silencer,strs.farg,mtoc_conf); @@ -390,7 +390,7 @@ function create(varargin) unix(['chmod +x ' filter]); end end - + %% Prepare placeholders in the Doxyfile template m = {'_OutputDir_' strrep(outdir,'\','\\'); ... '_SourceDir_' strrep(MatlabDocMaker.getSourceDirectory,'\','\\');... @@ -401,11 +401,11 @@ function create(varargin) '_ProjectVersion_' MatlabDocMaker.getProjectVersion; ... '_MTOCFILTER_' strrep(filter,'\','\\'); ... }; - + % Check for latex extra stuff texin = fullfile(cdir,MatlabDocMaker.LATEXEXTRAS_TEMPLATE); latexextras = ''; - if exist(texin,'file') == 2 + if exist(texin,'file') == 2 latexstr = strrep(fileread(texin),'_ConfDir_',strrep(cdir,'\','/')); latexextras = fullfile(cdir,'latexextras.sty'); fid = fopen(latexextras,'w+'); fprintf(fid,'%s',latexstr); fclose(fid); @@ -418,7 +418,7 @@ function create(varargin) L = 'YES'; end m(end+1,:) = {'_GenLatex_',L}; - + % Check how to set the HAVE_DOT flag [s, ~] = system('dot -V'); if s == 0 @@ -428,12 +428,12 @@ function create(varargin) fprintf('no "dot" found...'); end m(end+1,:) = {'_HaveDot_',HD}; - + % Read, replace & write doxygen config file doxyfile = fullfile(cdir,'Doxyfile'); doxyconfstr = regexprep(fileread(doxyfile_in),m(:,1),m(:,2)); fid = fopen(doxyfile,'w'); fprintf(fid,'%s',doxyconfstr); fclose(fid); - + % Fix for unix systems where the MatLab installation uses older % GLIBSTD libraries than doxygen/mtoc++ ldpath = ''; @@ -443,7 +443,7 @@ function create(varargin) % Call doxygen fprintf('running doxygen with mtoc++ filter...'); [~,warn] = system(sprintf('%sdoxygen "%s" 1>%s',ldpath, doxyfile, strs.null)); - + % Postprocess fprintf('running mtoc++ postprocessor...'); [~,postwarn] = system(sprintf('%smtocpp_post "%s" 1>%s',ldpath,... @@ -451,7 +451,7 @@ function create(varargin) if ~isempty(postwarn) warn = [warn sprintf('mtoc++ postprocessor messages:\n') postwarn]; end - + % Create latex document if desired if genlatex oldd = pwd; @@ -460,7 +460,7 @@ function create(varargin) if exist(latexdir,'dir') == 7 if exist(fullfile(latexdir,'refman.tex'),'file') == 2 fprintf('compiling LaTeX output...'); - cd(latexdir); + cd(latexdir); [s, latexmsg] = system('make'); if s ~= 0 warn = [warn sprintf('LaTeX compiler output:\n') latexmsg]; @@ -472,7 +472,7 @@ function create(varargin) end cd(oldd); end - + % Tidy up fprintf('cleaning up...'); if isfield(strs,'filter') @@ -482,14 +482,14 @@ function create(varargin) delete(latexextras); end delete(doxyfile); - - %% Post generation phase + + %% Post generation phase cd(curdir); % Restore PATH to previous value curpath = getenv('PATH'); setenv('PATH',curpath(1:end-length(pathadd))); fprintf('done!\n'); - + % Process warnings showchars = 800; warn = strtrim(warn); @@ -514,21 +514,21 @@ function create(varargin) else fprintf('MatlabDocMaker finished with no warnings!\n'); end - + % Open index.html if wanted if ip.Results.open MatlabDocMaker.open; end end - + function setup % Runs the setup script for MatlabDocMaker and collects all % necessary paths in order for the documentation creation to % work properly. - + %% Validity checks fprintf('<<<< Welcome to the MatlabDocMaker setup for your project "%s"! >>>>\n',MatlabDocMaker.getProjectName); - + %% Setup directories % Source directory srcdir = MatlabDocMaker.getPref('srcdir',''); @@ -547,7 +547,7 @@ function create(varargin) srcdir = d; end MatlabDocMaker.setPref('srcdir',srcdir); - + % Config directory confdir = MatlabDocMaker.getPref('confdir',''); word = 'keep'; @@ -565,7 +565,7 @@ function create(varargin) confdir = d; end MatlabDocMaker.setPref('confdir',confdir); - + % Output directory outdir = MatlabDocMaker.getPref('outdir',''); word = 'keep'; @@ -583,7 +583,7 @@ function create(varargin) outdir = d; end MatlabDocMaker.setPref('outdir',outdir); - + %% Additional Project properties if isequal(lower(input(['Do you want to specify further project details?\n'... 'You can set them later using provided set methods. (Y)es/(N)o?: '],'s')),'y') @@ -591,7 +591,7 @@ function create(varargin) MatlabDocMaker.setPref('proj_desc',input('Please specify a short project description: ','s')); MatlabDocMaker.setProjectLogo; end - + %% Check for necessary and recommended tools hasall = true; setenv('PATH',[getenv('PATH') pathsep confdir]); @@ -601,7 +601,7 @@ function create(varargin) fprintf(' found %s\n',vers(1:end-1)); else fprintf(2,' not found!\n'); - hasall = false; + hasall = false; end fprintf('[Required] Checking for mtoc++... '); ldpath = ''; @@ -653,20 +653,20 @@ function create(varargin) end end end - + methods(Static, Access=private) function value = getProjPrefTag % Gets the tag for the MatLab preferences struct. - % + % % @change{0,7,dw,2013-04-02} Now also removing "~" and "-" characters from ProjectName tags for preferences. str = regexprep(strrep(strtrim(MatlabDocMaker.getProjectName),' ','_'),'[^\d\w]',''); value = sprintf('MatlabDocMaker_on_%s',str); end - + function value = getPref(name, default) if nargin < 2 def = []; - else + else def = default; end value = getpref(MatlabDocMaker.getProjPrefTag,name,def); @@ -674,7 +674,7 @@ function create(varargin) error('MatlabDocMaker preferences not found/set correctly. (Re-)Run the MatlabDocMaker.setup method.'); end end - + function value = setPref(name, value) setpref(MatlabDocMaker.getProjPrefTag,name,value); end diff --git a/matlab/mtoc/config/customdoxygen.css b/matlab/mtoc/config/customdoxygen.css index 685cdb5d00..8a2de47fbf 100644 --- a/matlab/mtoc/config/customdoxygen.css +++ b/matlab/mtoc/config/customdoxygen.css @@ -138,11 +138,11 @@ a.elRef { } a.code, a.code:visited, a.line, a.line:visited { - color: #4665A2; + color: #4665A2; } a.codeRef, a.codeRef:visited, a.lineRef, a.lineRef:visited { - color: #4665A2; + color: #4665A2; } /* @end */ @@ -294,7 +294,7 @@ p.formulaDsp { } img.formulaDsp { - + } img.formulaInl { @@ -352,20 +352,20 @@ span.charliteral { color: #008080 } -span.vhdldigit { - color: #ff00ff +span.vhdldigit { + color: #ff00ff } -span.vhdlchar { - color: #000000 +span.vhdlchar { + color: #000000 } -span.vhdlkeyword { - color: #700070 +span.vhdlkeyword { + color: #700070 } -span.vhdllogic { - color: #ff0000 +span.vhdllogic { + color: #ff0000 } blockquote { @@ -560,9 +560,9 @@ table.memberdecls { } .memdoc, dl.reflist dd { - border-bottom: 1px solid #A8B8D9; - border-left: 1px solid #A8B8D9; - border-right: 1px solid #A8B8D9; + border-bottom: 1px solid #A8B8D9; + border-left: 1px solid #A8B8D9; + border-right: 1px solid #A8B8D9; padding: 6px 10px 2px 10px; border-top-width: 0; background-image:url('nav_g.png'); @@ -613,18 +613,18 @@ dl.reflist dd { .params, .retval, .exception, .tparams { margin-left: 0px; padding-left: 0px; -} +} .params .paramname, .retval .paramname { font-weight: bold; vertical-align: top; } - + .params .paramtype { font-style: italic; vertical-align: top; -} - +} + .params .paramdir { font-family: "courier new",courier,monospace; vertical-align: top; @@ -876,8 +876,8 @@ table.fieldtable { .fieldtable td.fielddoc p:first-child { margin-top: 0px; -} - +} + .fieldtable td.fielddoc p:last-child { margin-bottom: 2px; } @@ -950,7 +950,7 @@ table.fieldtable { color: #283A5D; font-family: 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; text-shadow: 0px 1px 1px rgba(255, 255, 255, 0.9); - text-decoration: none; + text-decoration: none; } .navpath li.navelem a:hover @@ -979,7 +979,7 @@ div.summary padding-right: 5px; width: 50%; text-align: right; -} +} div.summary a { @@ -1091,12 +1091,12 @@ dl.section dd { vertical-align: bottom; border-collapse: separate; } - + #projectlogo img -{ +{ border: 0px none; } - + #projectalign { vertical-align: middle; @@ -1107,7 +1107,7 @@ dl.section dd { font-size: 200%; font-weight: bold; } - + #projectbrief { font-size: 100%; @@ -1206,7 +1206,7 @@ div.toc ul { list-style: none outside none; border: medium none; padding: 0px; -} +} div.toc li.level1 { margin-left: 0px; @@ -1522,5 +1522,3 @@ div#nav-path ul { border-left: 1px solid #d1d5da; border-right: 1px solid #d1d5da; } - - diff --git a/matlab/mtoc/config/latexextras.template b/matlab/mtoc/config/latexextras.template index 352d89e27e..5abd2c045c 100644 --- a/matlab/mtoc/config/latexextras.template +++ b/matlab/mtoc/config/latexextras.template @@ -1,7 +1,7 @@ % Additional LaTeX inclusions for mtoc++/doxygen tools % % Use the _ConfDir_ tag to insert the folder where this file resides. -% Thus you can include more custom latex files/styles/packages which reside in this folder +% Thus you can include more custom latex files/styles/packages which reside in this folder % Default packages \usepackage{amsmath} @@ -12,4 +12,4 @@ %\input{_ConfDirFwdSlash_/myexternalstyle.sty} \setcounter{tocdepth}{2} -%\uchyph=0 \ No newline at end of file +%\uchyph=0 diff --git a/matlab/mtoc/config/mtocpp.conf b/matlab/mtoc/config/mtocpp.conf index 2a792649a1..9be3840ae2 100644 --- a/matlab/mtoc/config/mtocpp.conf +++ b/matlab/mtoc/config/mtocpp.conf @@ -71,9 +71,9 @@ COPY_TYPIFIED_FIELD_DOCU := false; # By default their documentation strings are ignored. GENERATE_SUBFUNTION_DOCUMENTATION := true; -# Leave this ## there, it marks the end of variable definitions +# Leave this ## there, it marks the end of variable definitions # and switches the parser to mtoc++ rules! -## +## # ########################### mtoc++ rules ############################ @@ -102,6 +102,6 @@ GENERATE_SUBFUNTION_DOCUMENTATION := true; # # }; # } - + # add(doc) = """ docu for all functions !!! """; # add(extra) = """ extra comments: @ref s_rand !!!! """; diff --git a/matlab/symbolic/am_and.m b/matlab/symbolic/am_and.m index 6362b6e8a0..5d8f5a1e06 100644 --- a/matlab/symbolic/am_and.m +++ b/matlab/symbolic/am_and.m @@ -8,4 +8,4 @@ % Return values: % fun: logical value, negative for false, positive for true fun = am_min(a,b); -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_eq.m b/matlab/symbolic/am_eq.m index 6ede0e5609..a366dbbe40 100644 --- a/matlab/symbolic/am_eq.m +++ b/matlab/symbolic/am_eq.m @@ -2,9 +2,9 @@ % am_eq is currently a placeholder that simply produces an error message % % Parameters: -% varargin: elements for chain of equalities +% varargin: elements for chain of equalities % % Return values: % fun: logical value, negative for false, positive for true error('Logical operator ''eq'' is currently not supported!'); -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_ge.m b/matlab/symbolic/am_ge.m index 8225db7069..28ba88ff8b 100644 --- a/matlab/symbolic/am_ge.m +++ b/matlab/symbolic/am_ge.m @@ -14,4 +14,4 @@ if(nargin>2) fun = am_and(a-b,am_ge(varargin{2:end})); end -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_gt.m b/matlab/symbolic/am_gt.m index 3c076b3ab0..cd2b420334 100644 --- a/matlab/symbolic/am_gt.m +++ b/matlab/symbolic/am_gt.m @@ -14,4 +14,4 @@ if(nargin>2) fun = am_and(a-b,am_gt(varargin{2:end})); end -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_if.m b/matlab/symbolic/am_if.m index 6673023a68..729df34153 100644 --- a/matlab/symbolic/am_if.m +++ b/matlab/symbolic/am_if.m @@ -21,4 +21,4 @@ fun = falsepart; end end -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_le.m b/matlab/symbolic/am_le.m index 3016b7d6b2..49fda85721 100644 --- a/matlab/symbolic/am_le.m +++ b/matlab/symbolic/am_le.m @@ -14,4 +14,4 @@ if(nargin>2) fun = am_and(b-a,am_le(varargin{2:end})); end -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_lt.m b/matlab/symbolic/am_lt.m index 67ec79f601..53b3006bd9 100644 --- a/matlab/symbolic/am_lt.m +++ b/matlab/symbolic/am_lt.m @@ -14,4 +14,4 @@ if(nargin>2) fun = am_and(b-a,am_lt(varargin{2:end})); end -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_min.m b/matlab/symbolic/am_min.m index 068d2dc2d6..3059eccaff 100644 --- a/matlab/symbolic/am_min.m +++ b/matlab/symbolic/am_min.m @@ -8,4 +8,4 @@ % Return values: % fun: minimum of a and b fun = -am_max(-a,-b); -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_or.m b/matlab/symbolic/am_or.m index e948070061..0c5d484232 100644 --- a/matlab/symbolic/am_or.m +++ b/matlab/symbolic/am_or.m @@ -8,4 +8,4 @@ % Return values: % fun: logical value, negative for false, positive for true fun = am_max(a,b); -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_piecewise.m b/matlab/symbolic/am_piecewise.m index 7a9be67bff..07b2f6205d 100644 --- a/matlab/symbolic/am_piecewise.m +++ b/matlab/symbolic/am_piecewise.m @@ -10,4 +10,3 @@ % fun: return value, piece if condition is true, default if not fun = am_if(condition,piece,default); end - diff --git a/matlab/symbolic/am_spline_pos.m b/matlab/symbolic/am_spline_pos.m index 32775d48e6..de3f9e2c49 100644 --- a/matlab/symbolic/am_spline_pos.m +++ b/matlab/symbolic/am_spline_pos.m @@ -28,6 +28,6 @@ end str = strcat('(',strcat(strcat(str, char(varargin{n})), ')')); str = strrep(str, ' ', ''); - + splinefun = sym(strcat('spline_pos', str)); end diff --git a/matlab/symbolic/am_stepfun.m b/matlab/symbolic/am_stepfun.m index 53749f1d4c..3cc3052572 100644 --- a/matlab/symbolic/am_stepfun.m +++ b/matlab/symbolic/am_stepfun.m @@ -1,5 +1,5 @@ function fun = am_stepfun(t,tstart,vstart,tend,vend) -% am_stepfun is the amici implementation of the step function +% am_stepfun is the amici implementation of the step function % % Parameters: % t: input variable @type sym @@ -11,4 +11,4 @@ % Return values: % fun: 0 before tstart, vstart between tstart and tend and vend after tend fun = heaviside(t-tstart)*vstart - heaviside(t-tend)*(vstart-vend); -end \ No newline at end of file +end diff --git a/matlab/symbolic/am_xor.m b/matlab/symbolic/am_xor.m index 9f8b26a238..93514c6c50 100644 --- a/matlab/symbolic/am_xor.m +++ b/matlab/symbolic/am_xor.m @@ -9,4 +9,4 @@ % fun: logical value, negative for false, positive for true fun = am_and(am_or(a,b),-am_and(a,b)); -end \ No newline at end of file +end diff --git a/models/model_calvetti/dwdx.cpp b/models/model_calvetti/dwdx.cpp index f52276f407..9066d0e953 100644 --- a/models/model_calvetti/dwdx.cpp +++ b/models/model_calvetti/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_calvetti{ -void dwdx_model_calvetti(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_calvetti(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = 1.0/(x[0]*x[0]*x[0])*-2.0; dwdx[1] = k[1]*w[15]*dwdx[0]; dwdx[2] = dwdx[1]; diff --git a/models/model_calvetti/model_calvetti.h b/models/model_calvetti/model_calvetti.h index b7b77518b7..828be82728 100644 --- a/models/model_calvetti/model_calvetti.h +++ b/models/model_calvetti/model_calvetti.h @@ -19,11 +19,11 @@ extern void Jy_model_calvetti(double *nllh, const int iy, const realtype *p, con extern void M_model_calvetti(realtype *M, const realtype t, const realtype *x, const realtype *p, const realtype *k); extern void dJydsigma_model_calvetti(double *dJydsigma, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydy_model_calvetti(double *dJydy, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); -extern void dwdx_model_calvetti(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdx_model_calvetti(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dydx_model_calvetti(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void root_model_calvetti(realtype *root, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *dx); extern void sigmay_model_calvetti(double *sigmay, const realtype t, const realtype *p, const realtype *k, const realtype *y); -extern void w_model_calvetti(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_calvetti(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_calvetti(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_calvetti(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *dx, const realtype *w); extern void y_model_calvetti(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -45,6 +45,7 @@ class Model_model_calvetti : public amici::Model_DAE { 0, 0, 4, + 0, 1, 38, 53, @@ -134,11 +135,11 @@ class Model_model_calvetti : public amici::Model_DAE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_calvetti(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_calvetti(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *dx, const realtype *w, const realtype *dwdp) override { @@ -183,8 +184,8 @@ class Model_model_calvetti : public amici::Model_DAE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_calvetti(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_calvetti(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_calvetti/w.cpp b/models/model_calvetti/w.cpp index 771f367993..fb1aaef8e2 100644 --- a/models/model_calvetti/w.cpp +++ b/models/model_calvetti/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_calvetti{ -void w_model_calvetti(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_calvetti(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = 1.0/k[0]; w[1] = k[2]*k[2]; w[2] = 1.0/(x[1]*x[1]); diff --git a/models/model_dirac/model_dirac.h b/models/model_dirac/model_dirac.h index 6426c1dc50..7a762479b5 100644 --- a/models/model_dirac/model_dirac.h +++ b/models/model_dirac/model_dirac.h @@ -45,6 +45,7 @@ class Model_model_dirac : public amici::Model_ODE { 0, 0, 2, + 0, 1, 0, 0, @@ -132,10 +133,10 @@ class Model_model_dirac : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -182,7 +183,7 @@ class Model_model_dirac : public amici::Model_ODE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_events/model_events.h b/models/model_events/model_events.h index 1491efe87b..df4bb68ae7 100644 --- a/models/model_events/model_events.h +++ b/models/model_events/model_events.h @@ -59,6 +59,7 @@ class Model_model_events : public amici::Model_ODE { 2, 2, 6, + 0, 1, 0, 0, @@ -152,10 +153,10 @@ class Model_model_events : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -208,7 +209,7 @@ class Model_model_events : public amici::Model_ODE { sz_model_events(sz, ie, t, x, p, k, h, sx, ip); } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_jakstat_adjoint/dwdp.cpp b/models/model_jakstat_adjoint/dwdp.cpp index 431552d3c9..3213a319d9 100644 --- a/models/model_jakstat_adjoint/dwdp.cpp +++ b/models/model_jakstat_adjoint/dwdp.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint{ -void dwdp_model_jakstat_adjoint(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) { +void dwdp_model_jakstat_adjoint(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) { dwdp[0] = amici::Dspline_pos(4,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); dwdp[1] = amici::Dspline_pos(6,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); dwdp[2] = amici::Dspline_pos(8,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); diff --git a/models/model_jakstat_adjoint/dwdx.cpp b/models/model_jakstat_adjoint/dwdx.cpp index c9c14945c0..70a26b8a2c 100644 --- a/models/model_jakstat_adjoint/dwdx.cpp +++ b/models/model_jakstat_adjoint/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint{ -void dwdx_model_jakstat_adjoint(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_jakstat_adjoint(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = x[1]*2.0; } diff --git a/models/model_jakstat_adjoint/model_jakstat_adjoint.h b/models/model_jakstat_adjoint/model_jakstat_adjoint.h index 94655a8d16..fdac2a9f94 100644 --- a/models/model_jakstat_adjoint/model_jakstat_adjoint.h +++ b/models/model_jakstat_adjoint/model_jakstat_adjoint.h @@ -19,14 +19,14 @@ extern void Jy_model_jakstat_adjoint(double *nllh, const int iy, const realtype extern void dJydsigma_model_jakstat_adjoint(double *dJydsigma, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydy_model_jakstat_adjoint(double *dJydy, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dsigmaydp_model_jakstat_adjoint(double *dsigmaydp, const realtype t, const realtype *p, const realtype *k, const realtype *y, const int ip); -extern void dwdp_model_jakstat_adjoint(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl); -extern void dwdx_model_jakstat_adjoint(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdp_model_jakstat_adjoint(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl); +extern void dwdx_model_jakstat_adjoint(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dxdotdp_model_jakstat_adjoint(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydp_model_jakstat_adjoint(double *dydp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydx_model_jakstat_adjoint(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void sigmay_model_jakstat_adjoint(double *sigmay, const realtype t, const realtype *p, const realtype *k, const realtype *y); extern void sx0_model_jakstat_adjoint(realtype *sx0, const realtype t,const realtype *x0, const realtype *p, const realtype *k, const int ip); -extern void w_model_jakstat_adjoint(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_jakstat_adjoint(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_jakstat_adjoint(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_jakstat_adjoint(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); extern void y_model_jakstat_adjoint(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -48,6 +48,7 @@ class Model_model_jakstat_adjoint : public amici::Model_ODE { 0, 0, 0, + 0, 1, 2, 1, @@ -134,12 +135,12 @@ class Model_model_jakstat_adjoint : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { - dwdp_model_jakstat_adjoint(dwdp, t, x, p, k, h, w, tcl, stcl); + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { + dwdp_model_jakstat_adjoint(dwdp, t, x, p, k, h, w, tcl, stcl, spl, sspl); } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_jakstat_adjoint(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_jakstat_adjoint(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -186,8 +187,8 @@ class Model_model_jakstat_adjoint : public amici::Model_ODE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_jakstat_adjoint(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_jakstat_adjoint(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_jakstat_adjoint/w.cpp b/models/model_jakstat_adjoint/w.cpp index 942ba399f5..06238238ca 100644 --- a/models/model_jakstat_adjoint/w.cpp +++ b/models/model_jakstat_adjoint/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint{ -void w_model_jakstat_adjoint(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_jakstat_adjoint(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = amici::spline_pos(t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); w[1] = x[1]*x[1]; } diff --git a/models/model_jakstat_adjoint_o2/dwdp.cpp b/models/model_jakstat_adjoint_o2/dwdp.cpp index 3035ef9c7a..b3e591fcba 100644 --- a/models/model_jakstat_adjoint_o2/dwdp.cpp +++ b/models/model_jakstat_adjoint_o2/dwdp.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint_o2{ -void dwdp_model_jakstat_adjoint_o2(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) { +void dwdp_model_jakstat_adjoint_o2(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) { dwdp[0] = amici::Dspline_pos(4,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); dwdp[1] = amici::DDspline_pos(4,4,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); dwdp[2] = amici::DDspline_pos(6,4,t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); diff --git a/models/model_jakstat_adjoint_o2/dwdx.cpp b/models/model_jakstat_adjoint_o2/dwdx.cpp index 0c441f5c2a..3226a7535b 100644 --- a/models/model_jakstat_adjoint_o2/dwdx.cpp +++ b/models/model_jakstat_adjoint_o2/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint_o2{ -void dwdx_model_jakstat_adjoint_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_jakstat_adjoint_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = x[1]*2.0; dwdx[1] = 2.0; } diff --git a/models/model_jakstat_adjoint_o2/model_jakstat_adjoint_o2.h b/models/model_jakstat_adjoint_o2/model_jakstat_adjoint_o2.h index 8896929d3e..22ca276067 100644 --- a/models/model_jakstat_adjoint_o2/model_jakstat_adjoint_o2.h +++ b/models/model_jakstat_adjoint_o2/model_jakstat_adjoint_o2.h @@ -19,14 +19,14 @@ extern void Jy_model_jakstat_adjoint_o2(double *nllh, const int iy, const realty extern void dJydsigma_model_jakstat_adjoint_o2(double *dJydsigma, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydy_model_jakstat_adjoint_o2(double *dJydy, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dsigmaydp_model_jakstat_adjoint_o2(double *dsigmaydp, const realtype t, const realtype *p, const realtype *k, const realtype *y, const int ip); -extern void dwdp_model_jakstat_adjoint_o2(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl); -extern void dwdx_model_jakstat_adjoint_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdp_model_jakstat_adjoint_o2(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl); +extern void dwdx_model_jakstat_adjoint_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dxdotdp_model_jakstat_adjoint_o2(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydp_model_jakstat_adjoint_o2(double *dydp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydx_model_jakstat_adjoint_o2(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void sigmay_model_jakstat_adjoint_o2(double *sigmay, const realtype t, const realtype *p, const realtype *k, const realtype *y); extern void sx0_model_jakstat_adjoint_o2(realtype *sx0, const realtype t,const realtype *x0, const realtype *p, const realtype *k, const int ip); -extern void w_model_jakstat_adjoint_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_jakstat_adjoint_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_jakstat_adjoint_o2(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_jakstat_adjoint_o2(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); extern void y_model_jakstat_adjoint_o2(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -48,6 +48,7 @@ class Model_model_jakstat_adjoint_o2 : public amici::Model_ODE { 0, 0, 0, + 0, 18, 10, 2, @@ -134,12 +135,12 @@ class Model_model_jakstat_adjoint_o2 : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { - dwdp_model_jakstat_adjoint_o2(dwdp, t, x, p, k, h, w, tcl, stcl); + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { + dwdp_model_jakstat_adjoint_o2(dwdp, t, x, p, k, h, w, tcl, stcl, spl, sspl); } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_jakstat_adjoint_o2(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_jakstat_adjoint_o2(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -186,8 +187,8 @@ class Model_model_jakstat_adjoint_o2 : public amici::Model_ODE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_jakstat_adjoint_o2(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_jakstat_adjoint_o2(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_jakstat_adjoint_o2/w.cpp b/models/model_jakstat_adjoint_o2/w.cpp index 2b1e113f08..766860cfa0 100644 --- a/models/model_jakstat_adjoint_o2/w.cpp +++ b/models/model_jakstat_adjoint_o2/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_jakstat_adjoint_o2{ -void w_model_jakstat_adjoint_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_jakstat_adjoint_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = amici::spline_pos(t,5,0.0,p[5],5.0,p[6],1.0E1,p[7],2.0E1,p[8],6.0E1,p[9],0.0,0.0); w[1] = x[1]*x[1]; w[2] = 1.0/k[0]; diff --git a/models/model_nested_events/model_nested_events.h b/models/model_nested_events/model_nested_events.h index 98b9af04a1..9ff8f519fe 100644 --- a/models/model_nested_events/model_nested_events.h +++ b/models/model_nested_events/model_nested_events.h @@ -48,6 +48,7 @@ class Model_model_nested_events : public amici::Model_ODE { 0, 0, 4, + 0, 1, 0, 0, @@ -136,10 +137,10 @@ class Model_model_nested_events : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -187,7 +188,7 @@ class Model_model_nested_events : public amici::Model_ODE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_neuron/model_neuron.h b/models/model_neuron/model_neuron.h index 54fbaf3238..e8f6f5c21f 100644 --- a/models/model_neuron/model_neuron.h +++ b/models/model_neuron/model_neuron.h @@ -62,6 +62,7 @@ class Model_model_neuron : public amici::Model_ODE { 1, 1, 1, + 0, 1, 0, 0, @@ -158,10 +159,10 @@ class Model_model_neuron : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -214,7 +215,7 @@ class Model_model_neuron : public amici::Model_ODE { sz_model_neuron(sz, ie, t, x, p, k, h, sx, ip); } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_neuron_o2/dwdx.cpp b/models/model_neuron_o2/dwdx.cpp index 6ec3315194..a746d7549a 100644 --- a/models/model_neuron_o2/dwdx.cpp +++ b/models/model_neuron_o2/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_neuron_o2{ -void dwdx_model_neuron_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_neuron_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = 2.0/2.5E1; dwdx[1] = dwdx[0]; } diff --git a/models/model_neuron_o2/model_neuron_o2.h b/models/model_neuron_o2/model_neuron_o2.h index e677cb4a49..23df2b9b33 100644 --- a/models/model_neuron_o2/model_neuron_o2.h +++ b/models/model_neuron_o2/model_neuron_o2.h @@ -29,7 +29,7 @@ extern void deltasx_model_neuron_o2(double *deltasx, const realtype t, const rea extern void deltax_model_neuron_o2(double *deltax, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ie, const realtype *xdot, const realtype *xdot_old); extern void deltaxB_model_neuron_o2(double *deltaxB, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ie, const realtype *xdot, const realtype *xdot_old, const realtype *xB); extern void drzdx_model_neuron_o2(double *drzdx, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h); -extern void dwdx_model_neuron_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdx_model_neuron_o2(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dxdotdp_model_neuron_o2(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydx_model_neuron_o2(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void dzdx_model_neuron_o2(double *dzdx, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h); @@ -41,7 +41,7 @@ extern void srz_model_neuron_o2(double *srz, const int ie, const realtype t, con extern void stau_model_neuron_o2(double *stau, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *sx, const int ip, const int ie); extern void sx0_model_neuron_o2(realtype *sx0, const realtype t,const realtype *x0, const realtype *p, const realtype *k, const int ip); extern void sz_model_neuron_o2(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip); -extern void w_model_neuron_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_neuron_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_neuron_o2(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_neuron_o2(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); extern void y_model_neuron_o2(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -64,6 +64,7 @@ class Model_model_neuron_o2 : public amici::Model_ODE { 5, 1, 1, + 0, 5, 2, 2, @@ -160,11 +161,11 @@ class Model_model_neuron_o2 : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_neuron_o2(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_neuron_o2(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -217,8 +218,8 @@ class Model_model_neuron_o2 : public amici::Model_ODE { sz_model_neuron_o2(sz, ie, t, x, p, k, h, sx, ip); } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_neuron_o2(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_neuron_o2(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_neuron_o2/w.cpp b/models/model_neuron_o2/w.cpp index 4f49da479b..cbd2f0a25f 100644 --- a/models/model_neuron_o2/w.cpp +++ b/models/model_neuron_o2/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_neuron_o2{ -void w_model_neuron_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_neuron_o2(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = x[0]*(2.0/2.5E1); w[1] = w[0]+5.0; } diff --git a/models/model_robertson/dwdp.cpp b/models/model_robertson/dwdp.cpp index e7db2c1390..831c448cad 100644 --- a/models/model_robertson/dwdp.cpp +++ b/models/model_robertson/dwdp.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_robertson{ -void dwdp_model_robertson(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) { +void dwdp_model_robertson(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) { dwdp[0] = x[1]*x[2]; } diff --git a/models/model_robertson/dwdx.cpp b/models/model_robertson/dwdx.cpp index 8e25dc5184..5c300a54ec 100644 --- a/models/model_robertson/dwdx.cpp +++ b/models/model_robertson/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_robertson{ -void dwdx_model_robertson(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_robertson(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = p[1]*x[2]; dwdx[1] = p[1]*x[1]; } diff --git a/models/model_robertson/model_robertson.h b/models/model_robertson/model_robertson.h index 0c4ac22110..7f4377d785 100644 --- a/models/model_robertson/model_robertson.h +++ b/models/model_robertson/model_robertson.h @@ -19,12 +19,12 @@ extern void Jy_model_robertson(double *nllh, const int iy, const realtype *p, co extern void M_model_robertson(realtype *M, const realtype t, const realtype *x, const realtype *p, const realtype *k); extern void dJydsigma_model_robertson(double *dJydsigma, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydy_model_robertson(double *dJydy, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); -extern void dwdp_model_robertson(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl); -extern void dwdx_model_robertson(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdp_model_robertson(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl); +extern void dwdx_model_robertson(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dxdotdp_model_robertson(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *dx, const realtype *w, const realtype *dwdp); extern void dydx_model_robertson(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void sigmay_model_robertson(double *sigmay, const realtype t, const realtype *p, const realtype *k, const realtype *y); -extern void w_model_robertson(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_robertson(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_robertson(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_robertson(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *dx, const realtype *w); extern void y_model_robertson(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -46,6 +46,7 @@ class Model_model_robertson : public amici::Model_DAE { 0, 0, 0, + 0, 1, 1, 2, @@ -135,12 +136,12 @@ class Model_model_robertson : public amici::Model_DAE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { - dwdp_model_robertson(dwdp, t, x, p, k, h, w, tcl, stcl); + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { + dwdp_model_robertson(dwdp, t, x, p, k, h, w, tcl, stcl, spl, sspl); } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_robertson(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_robertson(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *dx, const realtype *w, const realtype *dwdp) override { @@ -185,8 +186,8 @@ class Model_model_robertson : public amici::Model_DAE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_robertson(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_robertson(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_robertson/w.cpp b/models/model_robertson/w.cpp index f5e90c8e42..6905b49c0e 100644 --- a/models/model_robertson/w.cpp +++ b/models/model_robertson/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_robertson{ -void w_model_robertson(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_robertson(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = p[1]*x[1]*x[2]; } diff --git a/models/model_steadystate/dwdp.cpp b/models/model_steadystate/dwdp.cpp index 20e9916687..154db2a72f 100644 --- a/models/model_steadystate/dwdp.cpp +++ b/models/model_steadystate/dwdp.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_steadystate{ -void dwdp_model_steadystate(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) { +void dwdp_model_steadystate(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) { dwdp[0] = x[2]; } diff --git a/models/model_steadystate/dwdx.cpp b/models/model_steadystate/dwdx.cpp index 34b1f15eb7..d447f2140d 100644 --- a/models/model_steadystate/dwdx.cpp +++ b/models/model_steadystate/dwdx.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_steadystate{ -void dwdx_model_steadystate(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) { +void dwdx_model_steadystate(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) { dwdx[0] = x[0]*2.0; dwdx[1] = p[3]; } diff --git a/models/model_steadystate/model_steadystate.h b/models/model_steadystate/model_steadystate.h index 313ad9f743..b61649f9c8 100644 --- a/models/model_steadystate/model_steadystate.h +++ b/models/model_steadystate/model_steadystate.h @@ -18,12 +18,12 @@ extern void JSparse_model_steadystate(SUNMatrixContent_Sparse JSparse, const rea extern void Jy_model_steadystate(double *nllh, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydsigma_model_steadystate(double *dJydsigma, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); extern void dJydy_model_steadystate(double *dJydy, const int iy, const realtype *p, const realtype *k, const double *y, const double *sigmay, const double *my); -extern void dwdp_model_steadystate(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl); -extern void dwdx_model_steadystate(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl); +extern void dwdp_model_steadystate(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl); +extern void dwdx_model_steadystate(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl); extern void dxdotdp_model_steadystate(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp); extern void dydx_model_steadystate(double *dydx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *dwdx); extern void sigmay_model_steadystate(double *sigmay, const realtype t, const realtype *p, const realtype *k, const realtype *y); -extern void w_model_steadystate(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl); +extern void w_model_steadystate(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl); extern void x0_model_steadystate(realtype *x0, const realtype t, const realtype *p, const realtype *k); extern void xdot_model_steadystate(realtype *xdot, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); extern void y_model_steadystate(double *y, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w); @@ -45,6 +45,7 @@ class Model_model_steadystate : public amici::Model_ODE { 0, 0, 0, + 0, 1, 2, 2, @@ -130,12 +131,12 @@ class Model_model_steadystate : public amici::Model_ODE { void fdsigmazdp(double *dsigmazdp, const realtype t, const realtype *p, const realtype *k, const int ip) override { } - void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl) override { - dwdp_model_steadystate(dwdp, t, x, p, k, h, w, tcl, stcl); + void fdwdp(realtype *dwdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *stcl, const realtype *spl, const realtype *sspl) override { + dwdp_model_steadystate(dwdp, t, x, p, k, h, w, tcl, stcl, spl, sspl); } - void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl) override { - dwdx_model_steadystate(dwdx, t, x, p, k, h, w, tcl); + void fdwdx(realtype *dwdx, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *w, const realtype *tcl, const realtype *spl) override { + dwdx_model_steadystate(dwdx, t, x, p, k, h, w, tcl, spl); } void fdxdotdp(realtype *dxdotdp, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const int ip, const realtype *w, const realtype *dwdp) override { @@ -180,8 +181,8 @@ class Model_model_steadystate : public amici::Model_ODE { void fsz(double *sz, const int ie, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *sx, const int ip) override { } - void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) override { - w_model_steadystate(w, t, x, p, k, h, tcl); + void fw(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) override { + w_model_steadystate(w, t, x, p, k, h, tcl, spl); } void fx0(realtype *x0, const realtype t, const realtype *p, const realtype *k) override { diff --git a/models/model_steadystate/w.cpp b/models/model_steadystate/w.cpp index c968a93c45..948d4529c2 100644 --- a/models/model_steadystate/w.cpp +++ b/models/model_steadystate/w.cpp @@ -10,7 +10,7 @@ namespace amici { namespace model_model_steadystate{ -void w_model_steadystate(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl) { +void w_model_steadystate(realtype *w, const realtype t, const realtype *x, const realtype *p, const realtype *k, const realtype *h, const realtype *tcl, const realtype *spl) { w[0] = p[3]*x[2]; w[1] = x[0]*x[0]; } diff --git a/python/benchmark/benchmark_pysb.py b/python/benchmark/benchmark_pysb.py index add85092ec..e3b505300e 100644 --- a/python/benchmark/benchmark_pysb.py +++ b/python/benchmark/benchmark_pysb.py @@ -5,20 +5,20 @@ times are averages of N_REPEATS simulations at reference values. """ +import importlib import os -import pysb +import sys +import timeit + import amici -from amici.pysb_import import pysb2amici +import matplotlib.pyplot as plt import numpy as np import pandas as pd -import matplotlib.pyplot as plt -import importlib -import timeit -import sys - +import pysb +from amici.pysb_import import pysb2amici from pysb.simulator import ScipyOdeSimulator -sys.path.insert(0, os.path.join('..', 'tests')) +sys.path.insert(0, os.path.join("..", "tests")) from test_pysb import pysb_models simulation_times = dict() @@ -32,37 +32,39 @@ simulation_times[example] = dict() with amici.add_path(os.path.dirname(pysb.examples.__file__)): - with amici.add_path(os.path.join(os.path.dirname(__file__), '..', - 'tests', 'pysb_test_models')): - + with amici.add_path( + os.path.join(os.path.dirname(__file__), "..", "tests", "pysb_test_models") + ): pysb.SelfExporter.cleanup() # reset pysb pysb.SelfExporter.do_export = True module = importlib.import_module(example) pysb_model = module.model - pysb_model.name = pysb_model.name.replace('pysb.examples.', '') + pysb_model.name = pysb_model.name.replace("pysb.examples.", "") # avoid naming clash for custom pysb models - pysb_model.name += '_amici' + pysb_model.name += "_amici" # pysb part tspan = np.linspace(0, 100, 101) sim = ScipyOdeSimulator( pysb_model, tspan=tspan, - integrator_options={'rtol': rtol, 'atol': atol}, + integrator_options={"rtol": rtol, "atol": atol}, + ) + time_pysb = ( + timeit.Timer("pysb_simres = sim.run()", globals={"sim": sim}).timeit( + number=N_REPEATS + ) + / N_REPEATS ) - time_pysb = timeit.Timer( - 'pysb_simres = sim.run()', - globals={'sim': sim} - ).timeit(number=N_REPEATS)/N_REPEATS - simulation_times[example]['pysb'] = time_pysb - print(f'PySB average simulation time {example}: {time_pysb}') + simulation_times[example]["pysb"] = time_pysb + print(f"PySB average simulation time {example}: {time_pysb}") # amici part outdir = pysb_model.name - if pysb_model.name in ['move_connected_amici']: + if pysb_model.name in ["move_connected_amici"]: compute_conservation_laws = False else: compute_conservation_laws = True @@ -71,11 +73,10 @@ pysb_model, outdir, compute_conservation_laws=compute_conservation_laws, - observables=list(pysb_model.observables.keys()) + observables=list(pysb_model.observables.keys()), ) - amici_model_module = amici.import_model_module(pysb_model.name, - outdir) + amici_model_module = amici.import_model_module(pysb_model.name, outdir) model_pysb = amici_model_module.getModel() @@ -85,27 +86,29 @@ solver.setMaxSteps(int(1e6)) solver.setAbsoluteTolerance(atol) solver.setRelativeTolerance(rtol) - time_amici = timeit.Timer( - 'rdata = amici.runAmiciSimulation(model, solver)', - globals={'model': model_pysb, 'solver': solver, - 'amici': amici} - ).timeit(number=N_REPEATS)/N_REPEATS - simulation_times[example]['amici'] = time_amici - print(f'AMICI average simulation time {example}: {time_amici}') + time_amici = ( + timeit.Timer( + "rdata = amici.runAmiciSimulation(model, solver)", + globals={"model": model_pysb, "solver": solver, "amici": amici}, + ).timeit(number=N_REPEATS) + / N_REPEATS + ) + simulation_times[example]["amici"] = time_amici + print(f"AMICI average simulation time {example}: {time_amici}") times = pd.DataFrame(simulation_times) -ax = times.T.plot(kind='scatter', x='pysb', y='amici') -ax.set_xscale('log') -ax.set_yscale('log') -ax.set_aspect('equal') +ax = times.T.plot(kind="scatter", x="pysb", y="amici") +ax.set_xscale("log") +ax.set_yscale("log") +ax.set_aspect("equal") xy_min = np.min([ax.get_xlim()[0], ax.get_ylim()[0]]) xy_max = np.max([ax.get_xlim()[1], ax.get_ylim()[1]]) ax.set_xlim([xy_min, xy_max]) ax.set_ylim([xy_min, xy_max]) -ax.set_ylabel('simulation time AMICI [s]') -ax.set_xlabel('simulation time PySB [s]') -ax.plot([xy_min, xy_max], [xy_min, xy_max], 'k:') +ax.set_ylabel("simulation time AMICI [s]") +ax.set_xlabel("simulation time PySB [s]") +ax.plot([xy_min, xy_max], [xy_min, xy_max], "k:") plt.tight_layout() -plt.savefig('benchmark_pysb.eps') +plt.savefig("benchmark_pysb.eps") diff --git a/python/examples/example_constant_species/ExampleEquilibrationLogic.ipynb b/python/examples/example_constant_species/ExampleEquilibrationLogic.ipynb index 40ecb9f7aa..7c8ceec6cd 100644 --- a/python/examples/example_constant_species/ExampleEquilibrationLogic.ipynb +++ b/python/examples/example_constant_species/ExampleEquilibrationLogic.ipynb @@ -1204,4 +1204,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/examples/example_petab/petab.ipynb b/python/examples/example_petab/petab.ipynb index e1861e5f13..27ee96e449 100644 --- a/python/examples/example_petab/petab.ipynb +++ b/python/examples/example_petab/petab.ipynb @@ -370,4 +370,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/examples/example_presimulation/createModelPresimulation.py b/python/examples/example_presimulation/createModelPresimulation.py index c2d373156c..4806b67647 100644 --- a/python/examples/example_presimulation/createModelPresimulation.py +++ b/python/examples/example_presimulation/createModelPresimulation.py @@ -1,60 +1,59 @@ -from pysb.core import ( - Rule, Parameter, Model, Monomer, Expression, Initial, Observable -) +import os import pysb.export -import os +from pysb.core import Expression, Initial, Model, Monomer, Observable, Parameter, Rule model = Model() -prot = Monomer('PROT', ['kin', 'drug', 'phospho'], {'phospho': ['u', 'p']}) -prot_0 = Parameter('PROT_0', 10) -Initial(prot(phospho='u', drug=None, kin=None), - Expression('initProt', prot_0)) - -drug = Monomer('DRUG', ['bound']) -drug_0 = Parameter('DRUG_0', 9) -Initial(drug(bound=None), - Expression('initDrug', drug_0)) - -kin = Monomer('KIN', ['bound']) -kin_0 = Parameter('KIN_0', 1) -Initial(kin(bound=None), - Expression('initKin', kin_0)) - -Rule('PROT_DRUG_bind', - drug(bound=None) + prot(phospho='u', drug=None, kin=None) | - drug(bound=1) % prot(phospho='u', drug=1, kin=None), - Parameter('kon_prot_drug', 0.1), - Parameter('koff_prot_drug', 0.1) - ) - -Rule('PROT_KIN_bind', - kin(bound=None) + prot(phospho='u', drug=None, kin=None) >> - kin(bound=1) % prot(phospho='u', drug=None, kin=1), - Parameter('kon_prot_kin', 0.1), - ) - -Rule('PROT_KIN_phospho', - kin(bound=1) % prot(phospho='u', drug=None, kin=1) >> - kin(bound=None) + prot(phospho='p', drug=None, kin=None), - Parameter('kphospho_prot_kin', 0.1) - ) - -Rule('PROT_dephospho', - prot(phospho='p', drug=None, kin=None) >> - prot(phospho='u', drug=None, kin=None), - Parameter('kdephospho_prot', 0.1) - ) - -pProt = Observable('pPROT', prot(phospho='p')) -tProt = Observable('tPROT', prot()) - -Expression('pPROT_obs', pProt/tProt) - -sbml_output = pysb.export.export(model, format='sbml') - -outfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'model_presimulation.xml') -with open(outfile, 'w') as f: +prot = Monomer("PROT", ["kin", "drug", "phospho"], {"phospho": ["u", "p"]}) +prot_0 = Parameter("PROT_0", 10) +Initial(prot(phospho="u", drug=None, kin=None), Expression("initProt", prot_0)) + +drug = Monomer("DRUG", ["bound"]) +drug_0 = Parameter("DRUG_0", 9) +Initial(drug(bound=None), Expression("initDrug", drug_0)) + +kin = Monomer("KIN", ["bound"]) +kin_0 = Parameter("KIN_0", 1) +Initial(kin(bound=None), Expression("initKin", kin_0)) + +Rule( + "PROT_DRUG_bind", + drug(bound=None) + prot(phospho="u", drug=None, kin=None) + | drug(bound=1) % prot(phospho="u", drug=1, kin=None), + Parameter("kon_prot_drug", 0.1), + Parameter("koff_prot_drug", 0.1), +) + +Rule( + "PROT_KIN_bind", + kin(bound=None) + prot(phospho="u", drug=None, kin=None) + >> kin(bound=1) % prot(phospho="u", drug=None, kin=1), + Parameter("kon_prot_kin", 0.1), +) + +Rule( + "PROT_KIN_phospho", + kin(bound=1) % prot(phospho="u", drug=None, kin=1) + >> kin(bound=None) + prot(phospho="p", drug=None, kin=None), + Parameter("kphospho_prot_kin", 0.1), +) + +Rule( + "PROT_dephospho", + prot(phospho="p", drug=None, kin=None) >> prot(phospho="u", drug=None, kin=None), + Parameter("kdephospho_prot", 0.1), +) + +pProt = Observable("pPROT", prot(phospho="p")) +tProt = Observable("tPROT", prot()) + +Expression("pPROT_obs", pProt / tProt) + +sbml_output = pysb.export.export(model, format="sbml") + +outfile = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "model_presimulation.xml" +) +with open(outfile, "w") as f: f.write(sbml_output) diff --git a/python/examples/example_splines/.gitignore b/python/examples/example_splines/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/python/examples/example_splines/.gitignore @@ -0,0 +1 @@ +/build diff --git a/python/examples/example_splines/ExampleSplines.ipynb b/python/examples/example_splines/ExampleSplines.ipynb new file mode 100644 index 0000000000..593c84e3b9 --- /dev/null +++ b/python/examples/example_splines/ExampleSplines.ipynb @@ -0,0 +1,1243 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# AMICI Python example \"splines\"\n", + "\n", + "This is an example showing how to add spline assignment rules to a pre-existing SBML model." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Utility functions" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import sys\n", + "import os\n", + "import libsbml\n", + "import amici\n", + "\n", + "import numpy as np\n", + "import sympy as sp\n", + "\n", + "from shutil import rmtree\n", + "from importlib import import_module\n", + "from uuid import uuid1\n", + "from tempfile import TemporaryDirectory\n", + "import matplotlib as mpl\n", + "from matplotlib import pyplot as plt\n", + "\n", + "# Choose build directory\n", + "BUILD_PATH = None # temporary folder\n", + "# BUILD_PATH = 'build' # specified folder for debugging\n", + "if BUILD_PATH is not None:\n", + " # Remove previous models\n", + " rmtree(BUILD_PATH, ignore_errors=True)\n", + " os.mkdir(BUILD_PATH)\n", + " \n", + "def simulate(sbml_model, parameters=None, *, model_name=None, **kwargs):\n", + " if model_name is None:\n", + " model_name = 'model_' + uuid1().hex\n", + " if BUILD_PATH is None:\n", + " with TemporaryDirectory() as build_dir:\n", + " return _simulate(sbml_model, parameters, build_dir=build_dir, model_name=model_name, **kwargs)\n", + " else:\n", + " build_dir = os.path.join(BUILD_PATH, model_name)\n", + " rmtree(build_dir, ignore_errors=True)\n", + " return _simulate(sbml_model, parameters, build_dir=build_dir, model_name=model_name, **kwargs)\n", + "\n", + "def _simulate(sbml_model, parameters, *, build_dir, model_name, T=1, discard_annotations=False, plot=True):\n", + " if parameters is None:\n", + " parameters = {}\n", + " # Build the model module from the SBML file\n", + " sbml_importer = amici.SbmlImporter(sbml_model, discard_annotations=discard_annotations)\n", + " sbml_importer.sbml2amici(model_name, build_dir)\n", + " # Import the model module\n", + " sys.path.insert(0, os.path.abspath(build_dir))\n", + " model_module = import_module(model_name)\n", + " # Setup simulation timepoints and parameters\n", + " model = model_module.getModel()\n", + " for (name, value) in parameters.items():\n", + " model.setParameterByName(name, value)\n", + " if isinstance(T, (int, float)):\n", + " T = np.linspace(0, T, 100)\n", + " model.setTimepoints([float(t) for t in T])\n", + " solver = model.getSolver()\n", + " solver.setSensitivityOrder(amici.SensitivityOrder.first)\n", + " solver.setSensitivityMethod(amici.SensitivityMethod.forward)\n", + " # Simulate\n", + " rdata = amici.runAmiciSimulation(model, solver)\n", + " # Plot results\n", + " if plot:\n", + " fig, ax = plt.subplots()\n", + " ax.plot(rdata['t'], rdata['x'])\n", + " ax.set_xlabel(\"time\")\n", + " ax.set_ylabel(\"concentration\")\n", + " return model, rdata" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### A simple SBML model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us consider the following SBML model:" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```xml\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " f \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This model corresponds to the simple ODE $\\dot{x} = f$ for a species $x$ and a parameter $f$.\n", + "\n", + "We can easily import and simulate this model in AMICI." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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pUvJz89n+PDhDBs2jEaNGrF27VpSU1O58847ufrqqxk5ciQA48ePZ+/evSxdupQWLVqwevVqbr75Znbt2kWHDh1q9X2KSM1QuRGROsff3x8PDw98fHzKp6L2799/0XqlpaW8+eabtGvXDoC77rqLf//732RkZNCwYUO6du3KgAED2LBhAyNHjiQtLY3333+ftLQ0WrRoAcBjjz3GunXreP/995kxY0btvUkRqTEqNyLisHx8fMqLDUBQUBBhYWE0bNiwwrLMzEwAdu3ahdVqpWPHjhWep7i4mKZNm9ZOaBGpcSo3IuKw3N3dK3xtsVguucxmswFw/vx5XF1d2b59O66urhXW+99CJCKOTeVGROokDw+PCgcCV4eoqCisViuZmZn079+/Wp9bROoOnS0lInVSWFgYP/30E8eOHSM7O7t89OVKdOzYkXvuuYcxY8awatUqjh49ytatW5k5cyZr1qyphtQiUheo3IhInfTYY4/h6upK165dadasGWlpadXyvO+//z5jxozh0UcfpVOnTgwdOpT//Oc/tGrVqlqeX0TMpysUi4iIiFPRyI2IiIg4FZUbERERcSoqNyIiIuJUVG5ERETEqajciIiIiFNRuRERERGnonIjIiIiTkXlRkRERJyKyo2IiIg4FZUbERERcSoqNyIiIuJU/n+Eyy8Cx5B7JQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "simulate('example_splines.xml', dict(f=1));" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### Adding a simple spline" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of using a constant parameter $f$, we want to use a smooth time-dependent function $f(t)$ whose value is known only at a finite number of time instants. The value of $f(t)$ outside such grid points needs to be smoothly interpolated. Several methods have been developed for this problem over the years; AMICI at the moment supports only [cubic Hermite splines](https://en.wikipedia.org/wiki/Cubic_Hermite_spline)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can add a spline function to an existing SBML model with the following code. The resulting time-dependent parameter $f(t)$ will assume values $(1, -0.5, 2)$ at the equally spaced points $(0, 0.5, 1)$ and smoothly vary elsewhere." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "AMICI encodes the spline as a SBML assignment rule for the parameter $f$. Such a rule consists of a piecewise-polynomial formula which can be interpreted in any SBML-compliant software. However, such very complex formulas are computationally inefficient; e.g., in AMICI they lead to very long model creation times. To solve such problem the code below adds AMICI-specific SBML annotations to the assignment rule which can be used by AMICI to recreate the correct interpolant without reading the inefficient piecewise formula." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create a spline object\n", + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol, # the spline function is evaluated at the current time point\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=3),\n", + " values_at_nodes=[1, -1, 2],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "spline value at 0.3 = -0.560000000000000\n", + "spline derivative at 0.3 = -4.60000000000000\n", + "spline integral between 0 and 1 = 0.0416666666666672\n" + ] + } + ], + "source": [ + "# This spline object can be evaluated at any point\n", + "# and so can its derivative/integral\n", + "print(f\"spline value at 0.3 = {spline.evaluate(0.3)}\")\n", + "print(f\"spline derivative at 0.3 = {spline.derivative(0.3)}\")\n", + "print(f\"spline integral between 0 and 1 = {spline.integrate(0.0, 1.0)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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i8VibjgRqbHczQ4gImIPnp6eBAcHmx1DRGo4fTQmIg7v9ttv56effuKNN97AYrFgsViYM2dOmY/G5syZQ0BAAIsWLaJNmzb4+Phw4403kpOTw9y5c2nevDl169blgQceoLi4uPSx8/Pzefjhh2ncuDG1a9eme/furFmzxpwXKiJVTmeERMThvfHGG+zfv58OHTrw3HPPAbB79+6/7ZeTk8Obb77J/PnzyczMZNiwYVx//fUEBASwZMkSDh06xA033EDv3r0ZPnw4AOPGjSMuLo758+cTEhLCwoULGThwIDt37iQ8PLxaX6eIVD0VIRFxeP7+/nh6euLj41P6cdjevXv/tl9hYSHvvPMOLVu2BODGG2/k448/JjU1lTp16tCuXTuuuOIKVq9ezfDhw0lISOCjjz4iISGBkJAQAB5++GGWLl3KRx99xEsvvVR9L1JE7EJFSERcho+PT2kJAggKCqJ58+bUqVOnzLZjx44BsHPnToqLi2ndunWZx8nPz6d+/frVE1pE7EpFSERchoeHR5mvLRbLWbfZbDYAsrKycHNzY+vWrbi5uZXZ78/lSUQcl4qQiDgFT0/PMhc5V4Xo6GiKi4s5duwYffv2rdLHFpGaQaPGRMQpNG/enE2bNvH7779z4sSJ0rM6F6N169bceuutjBo1igULFnD48GE2b97M1KlTWbx4cRWkFhGzqQiJiFN4+OGHcXNzo127dgQGBpKQkFAlj/vRRx8xatQoJk6cSJs2bRg6dCi//vorTZs2rZLHFxFzaWZpERERcVk6IyQiIiIuS0VIREREXJaKkIiIiLgsFSERERFxWSpCIiIi4rJUhERERMRlqQiJiIiIy1IREhEREZelIiQiIiIuS0VIREREXJaKkIiIiLis/wf0qBxzqVz86wAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot the spline\n", + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Load SBML model using libsbml\n", + "import libsbml\n", + "sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + "sbml_model = sbml_doc.getModel()\n", + "# We can add the spline assignment rule to the SBML model\n", + "spline.add_to_sbml_model(sbml_model)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Finally, we can simulate it in AMICI\n", + "model, rdata = simulate(sbml_model);" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Final value should be equal to the integral computed above\n", + "assert np.allclose(rdata['x'][-1], float(spline.integrate(0.0, 1.0)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following is the SBML code for the above model\n", + "```xml\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " f \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " time \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 1\n", + " \n", + " \n", + " -1\n", + " \n", + " \n", + " 2\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " ... piecewise representation of the spline ...\n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The spline annotation on its own can be accessed by" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\t\n", + "\t\t\n", + "\t\t\t time \n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t\n", + "\t\t\t0\n", + "\t\t\n", + "\t\t\n", + "\t\t\t1\n", + "\t\t\n", + "\t\t\n", + "\t\t\t\n", + "\t\t\t\t\n", + "\t\t\t\t1\n", + "\t\t\t\t2\n", + "\t\t\t\n", + "\t\t\n", + "\t\n", + "\t\n", + "\t\t\n", + "\t\t\t1\n", + "\t\t\n", + "\t\t\n", + "\t\t\t-1\n", + "\t\t\n", + "\t\t\n", + "\t\t\t2\n", + "\t\t\n", + "\t\n", + "\n", + "\n" + ] + } + ], + "source": [ + "print(spline.amici_annotation)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### Splines can be parametrized" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of constant values, SBML parameters can be used as spline values. These can also be automatically added to the model when adding the assignment rule." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=3),\n", + " values_at_nodes=sp.symbols('f0:3'),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(\n", + " sbml_model,\n", + " auto_add=True,\n", + " y_nominal=[1, -0.5, 2],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "parameters = dict(f0=-2, f1=1, f2=-2)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(parameters, xlabel='time');" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "model, rdata = simulate(sbml_model, parameters)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjcAAAGwCAYAAABVdURTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABrR0lEQVR4nO3dd3xT9f7H8VeSNuluKdABlC1bhiwBtyhucSIoIK6r4qJXr+IA1xW9+lMUEdzgQFDcgqCiqCiKLGUje7allO6VJuf3xymBympL25Om7+d95JGT05Pkk3Np8/Z7vsNmGIaBiIiISICwW12AiIiISFVSuBEREZGAonAjIiIiAUXhRkRERAKKwo2IiIgEFIUbERERCSgKNyIiIhJQgqwuoKZ5vV527dpFZGQkNpvN6nJERESkHAzDICcnh0aNGmG3H71tps6Fm127dpGUlGR1GSIiIlIJ27dvp0mTJkc9ps6Fm8jISMA8OVFRURZXIyIiIuWRnZ1NUlKS73v8aOpcuNl/KSoqKkrhRkREpJYpT5cSdSgWERGRgGJ5uJk4cSLNmzcnJCSE3r17s2jRoqMen5mZyciRI0lMTMTlctGmTRtmz55dQ9WKiIiIv7P0stSMGTNITk5m8uTJ9O7dm/HjxzNgwADWrVtHXFzcIccXFxdzzjnnEBcXx8yZM2ncuDFbt24lJiam5osXERERv2QzDMOw6s179+5Nz549efnllwFzmHZSUhJ33nknDzzwwCHHT548mWeffZa1a9cSHBxcqffMzs4mOjqarKyso/a58Xg8uN3uSr1HbRQcHIzD4bC6DBERkcMq7/c3WNhyU1xczJIlSxg9erRvn91up3///ixcuPCwz/niiy/o06cPI0eO5PPPP6dhw4YMGTKE+++//4hfzEVFRRQVFfkeZ2dnH7UuwzBISUkhMzOz4h+qlouJiSEhIUHz/4iISK1mWbhJT0/H4/EQHx9fZn98fDxr16497HM2bdrE999/z7XXXsvs2bPZsGEDt99+O263m7Fjxx72OePGjeOxxx4rd137g01cXBxhYWF14oveMAzy8/NJS0sDIDEx0eKKREREKq9WDQX3er3ExcXx2muv4XA46N69Ozt37uTZZ589YrgZPXo0ycnJvsf7x8kfjsfj8QWb+vXrV8tn8FehoaEApKWlERcXp0tUIiJSa1kWbho0aIDD4SA1NbXM/tTUVBISEg77nMTExEP6hrRv356UlBSKi4txOp2HPMflcuFyucpV0/4+NmFhYeX9GAFl/+d2u90KNyIiUmtZNhTc6XTSvXt35s2b59vn9XqZN28effr0Oexz+vXrx4YNG/B6vb5969evJzEx8bDBprLqwqWow6mrn1tERAKLpfPcJCcn8/rrrzN16lTWrFnDbbfdRl5eHiNGjABg2LBhZToc33bbbWRkZHD33Xezfv16Zs2axVNPPcXIkSOt+ggiIiLiZyztczNo0CD27NnDmDFjSElJoWvXrsyZM8fXyXjbtm1lVv5MSkpi7ty5jBo1is6dO9O4cWPuvvtu7r//fqs+goiIiPgZS+e5scLRxskXFhayefNmWrRoQUhIiEUVVo5hGPzrX/9i5syZ7Nu3j2XLltG1a9cKvUZt/vwiIhLYKjLPjeXLL0jVmDNnDlOmTOGrr75i9+7ddOrUqcJLW4iIiJSb1wvF+ZCfAXnpkJMKWTshcxtk77a0tFo1FFyObOPGjSQmJtK3b1+g4ktbiIhIHVaYDVk7IDcVctMgL83cztsLhVlQmAkFmea2Ow/cBVBSeOTXS+oNN35TU9UfQuHmGAzDoMDtseS9Q4Md5RrBdP311zN16lTAHPHUrFkz4uPjufnmm32dsydPnsysWbN46623Dru0hYiIBDDDgJzdkP437N0AezfCvi2Qtc1saSnMOv73sNnB5gC7AxxVN4K5MhRujqHA7aHDmLmWvPfqxwcQ5jz2/0UvvvgirVq14rXXXuOPP/7AZrPRuHHjCi1tISIiAaIgE1JXld5Wmre0tWaLy9GExEBkIkTEld7iIaw+hMaYPwuJNu9dERAcCsFhEBQCQS6wB4EfTSeicBMAoqOjiYyMxOFwkJCQwK5duyq8tIWIiNRCJcWQsgJ2Lim9LTZbZg7H5oB6zaF+a2hwgrkd0wxikiA6yQwtAULh5hhCgx2sfnyAZe8tIiLiU5wPO/6Arb/C1l9gx2IoKTj0uKgmkNAJ4jtBfEfzVq8FBFl7uaimKNwcg81mK9elIX9SmaUtRETED3m9kPIXbPzevG37DbzusseExkKTHtC4u3lrdBKE1631Ef+pdn1rS7kcvLTFwIEDgQNLW9xxxx3WFiciIkdXmA0b58G6r2HDd5C/t+zPoxpDs76lt37QoI1f9XfxBwo3ASo5OZnhw4fTo0cPevXqxfjx48ssbSEiIn4kJwXWfGkGms0/lW2dcUZAi9Og1VnmLbalwswxKNwEqGMtbSEiIhbL3QNrPodVn8GWBcBBCwbEtoJ2F0Cb88w5YxzBVlVZK2n5hYPU9eUH6vrnFxGpdsV5sHYWLJ8Gm38Ew3vgZ417QPuLod2F5mgmKaMiyy+o5UZERKQ6eb2w7VdY/gGs/gyKcw/8rFE36Hg5dBwIMU2tqjDgKNyIiIhUh7y9sPx9WDIFMjYe2B/TDLoMhi6DzP4zUuUUbkRERKqKYcC2hfDHm7DmC/AUm/udEdDxMug6BJr2UYfgaqZwIyIicrxKimDlJ/DbK+a8NPsldoUeI6DTlQE1A7C/U7gRERGprLx0s5XmjzfMlbTBXG+p89XQ4wazT43UOIUbERGRitq3FRa+DEvfPbD8QWQj6HUTdB8BYbHW1lfHKdyIiIiUV9oaWPACrJgJhsfcl9gV+t4JHS7VfDR+QuFGRETkWFJXw49Pw+rPD+xreQacMgpanK4Own7GbnUBUjUMw+CWW24hNjYWm83G8uXLrS5JRKT2S1sDHw6HSX0OBJv2l8DNP8Cwz82Ao2DjdxRuAsScOXOYMmUKX331Fbt37yY7O5uLL76YRo0aYbPZ+Oyzz6wuUUSk9sjYBB/fBK/0MSfeA/Oy020LYdC70PgkS8uTo9NlqQCxceNGEhMT6du3LwDLli2jS5cu3HDDDVx++eUWVyciUkvkpsFPz8Lit8BbYu5rfwmcfj8kdLK2Nik3hZsAcP311zN16lQAbDYbzZo1Y8uWLZx//vkWVyYiUksU5cKvE8ybO8/c1+osOHssNOpqaWlScQo3x2IY4M635r2Dw8p1LffFF1+kVatWvPbaa/zxxx84HI4aKE5EJAB4vfDnBzDvcchNMfc1Ogn6PwotT7e0NKk8hZtjcefDU42see8Hd4Ez/JiHRUdHExkZicPhICEhoQYKExEJAFt/hTmjYfdy83G95mao6TBQnYRrOYUbERGpW7J2wjcPwapPzceuKDjtXuh9KwS5rK1NqoTCzbEEh5ktKFa9t4iIVI2SYvh9Esx/xuxXY7PDScPhzIcgoqHV1UkVUrg5FputXJeGRETEj23+CWbdC+nrzMdJveGC5yCxs7V1SbVQuAlQubm5bNiwwfd48+bNLF++nNjYWJo2bWphZSIiNShvL8x9EP6abj4OawDnPgGdrwG7pnoLVAo3AWrx4sWceeaZvsfJyckADB8+nClTplhUlYhIDTEM+GuG2WG4IAOwQc+b4KyHITTG6uqkmtkMwzCsLqImZWdnEx0dTVZWFlFRUWV+VlhYyObNm2nRogUhISEWVWiduv75RSRAZGyGr0bBph/Mx3Ed4ZIJ0KS7tXXJcTna9/c/qeVGREQCg9cLf7wO3z1qTuMRFGLOLNz3Tq3WXcco3IiISO2XsQk+vwO2/mI+bn4qXPwi1G9lbV1iCYUbERGpvf7ZWhMcDuc8Bj1uVIfhOkzhRkREaqesnfDZreYwbzBbay592ZxpWOo0hRsREal9VsyEWclQmGVOeHrO42qtER+FGxERqT0KMmH2vbDiI/Nx4+5w2WvQoLWlZYl/UbgREZHaYdtv8PFNkLUdbA447T5zTSiNhJJ/ULgRERH/5vXAz8/D/HFgeKBeC7jiDWjSw+rKxE8p3IiIiP/K3gWf3AJbfjYfd74GLnwOXJHW1iV+TT2vAoRhGNxyyy3ExsZis9lYvny51SWJiByfv7+FSf3MYBMcDgMnw+WvKtjIMSncBIg5c+YwZcoUvvrqK3bv3s2XX35Jz549iYyMJC4ujoEDB7Ju3TqryxQROTZPCcx7HN6/0lwXKqEz/Osn6DrY6sqkllC4CRAbN24kMTGRvn37kpCQwC+//MLIkSP57bff+Pbbb3G73Zx77rnk5eVZXaqIyJHlpMK7A+Hn/zMf97wZbvpOo6GkQtTnJgBcf/31TJ06FQCbzUazZs3YsmVLmWOmTJlCXFwcS5Ys4bTTTrOgShGRY9jyC8wcAbmp4IyAS16CTldYXZXUQgo3x2AYBgUlBZa8d2hQKDab7ZjHvfjii7Rq1YrXXnuNP/74A4fDccgxWVlZAMTGxlZ5nSIix8UwYOFE+HaMORqqYXu4+h1o2MbqyqSW8otwM3HiRJ599llSUlLo0qULEyZMoFevXoc9dsqUKYwYMaLMPpfLRWFhYbXUVlBSQO9pvavltY/l9yG/ExYcdszjoqOjiYyMxOFwkJCQcMjPvV4v99xzD/369aNTp07VUaqISOUU58MXd8LKmebjE6+Gi8eDM9zSsqR2szzczJgxg+TkZCZPnkzv3r0ZP348AwYMYN26dcTFxR32OVFRUWU6x5andaMuGzlyJCtXrmTBggVWlyIickDGZphxHaSuBHsQDHgKet0C+psux8nycPP8889z8803+1pjJk+ezKxZs3jrrbd44IEHDvscm8122BaKwykqKqKoqMj3ODs7u0L1hQaF8vuQ3yv0nKoSGhR63K9xxx138NVXX/HTTz/RpEmTKqhKRKQKbJgHM2+AwkwIbwhXTYXm/ayuSgKEpeGmuLiYJUuWMHr0aN8+u91O//79Wbhw4RGfl5ubS7NmzfB6vZx00kk89dRTdOzY8bDHjhs3jscee6zSNdpstnJdGvI3hmFw55138umnnzJ//nxatGhhdUkiImb/mt8mwTcPgeGFxj1g0LsQ1cjqyiSAWDoUPD09HY/HQ3x8fJn98fHxpKSkHPY5bdu25a233uLzzz/nvffew+v10rdvX3bs2HHY40ePHk1WVpbvtn379ir/HP5o5MiRvPfee0ybNo3IyEhSUlJISUmhoMCaztEiIpQUwRd3wNzRZrDpeh2MmK1gI1XO8stSFdWnTx/69Onje9y3b1/at2/Pq6++yhNPPHHI8S6XC5fLVZMl+oVJkyYBcMYZZ5TZ//bbb3P99dfXfEEiUrflpsGMobD9N7DZ4dwn4eTb1b9GqoWl4aZBgwY4HA5SU1PL7E9NTS13n5rg4GC6devGhg0bqqPEWuOee+7hnnvu8T02DMO6YkREDpayEj64xlzN2xUNV70FrftbXZUEMEsvSzmdTrp37868efN8+7xeL/PmzSvTOnM0Ho+HFStWkJiYWF1liohIZf39Lbw1wAw2sa3g5nkKNlLtLL8slZyczPDhw+nRowe9evVi/Pjx5OXl+UZPDRs2jMaNGzNu3DgAHn/8cU4++WRat25NZmYmzz77LFu3buWmm26y8mOIiMg/LXodvv6P2b+m+almx+HQelZXJXWA5eFm0KBB7NmzhzFjxpCSkkLXrl2ZM2eOr5Pxtm3bsNsPNDDt27ePm2++mZSUFOrVq0f37t359ddf6dChg1UfQUREDub1wNyH4Hez7x9dr4WLxkOQ09KypO6wGXWsc0Z2djbR0dFkZWURFRVV5meFhYVs3ryZFi1aEBISYlGF1qnrn19EqkBxPnx8I6ybbT4+ewyckqyOw3Lcjvb9/U+Wt9z4ozqW93zq6ucWkSqSlw7TroadS8DhgssmaeFLsYTCzUGCg4MByM/PJzT0+GcHrm3y8/OBA+dBRKTc9m6E96+EjE1mv5rB06HpyVZXJXWUws1BHA4HMTExpKWlARAWFlYn1q0yDIP8/HzS0tKIiYk57KriIiJHtGOx2WKTvxdimsK1H2tFb7GUws0/7J9fZ3/AqUtiYmLKPb+QiAgA6+fCh8OhpAASu8CQjyAy/tjPE6lGCjf/YLPZSExMJC4uDrfbbXU5NSY4OFgtNiJSMcs/gM9HguEx5665aiq4IqyuSkTh5kgcDoe+7EVEjuTXCfDNw+Z252vg0pfBof564h8UbkREpPwMA74dA7++ZD7ucwec8wTYLZ3wXqQMhRsRESkfTwl8eTcsf898fM7j0O9ua2sSOQyFGxERObaSInNyvjVfgs0Bl7wE3a6zuiqRw1K4ERGRoyvOgxnXwcbvweGEK9+G9hdZXZXIESnciIjIkRVkmnPYbP8dgsNh8DRoeYbVVYkclcKNiIgcXu4eeO8ySFkBIdFw7UxI6mV1VSLHpHAjIiKHyt4N71wC6eshvCEM/RQSTrS6KpFyUbgREZGyMrfD1Ith32aIagzDvoAGra2uSqTcFG5EROSAjE0w9VLI2gYxzWD4F1CvudVViVSIwo2IiJjS/zZbbHJ2Q2wrGP4lRDe2uiqRClO4ERERSFtrBpu8NGjYDoZ9DpFaSFdqJ4UbEZG6Lm1NabDZA/EnwrDPILyB1VWJVJrCjYhIXZa62gw2+emQ0NlssQmLtboqkeOilc5EROqq1FUHgk1iFwUbCRhquRERqYt8wWYvJHY1L0WF1rO6KpEqoZYbEZG6xncpai806qZgIwFHLTciInXJnnXmzMP7W2yGfgahMRYXJVK11HIjIlJX7J/HJm+P2Xl46KcKNhKQFG5EROqCvRvNYJObCvGd1HlYAprCjYhIoNu35cDMww3bK9hIwFO4EREJZFk7YeolkL0TGrQx14rSBH0S4BRuREQCVW6a2Xk4cyvUa2GuFRURZ3VVItVO4UZEJBDlZ8A7l8LeDRCdZLbYaK0oqSMUbkREAk1BJrw7ENJWQ0SC2ccmpqnVVYnUGIUbEZFAUpwH066G3X9CWAOzxaZ+K6urEqlRCjciIoGipAhmXAfbf4eQaHMem4Ztra5KpMYp3IiIBAJPCXx8I2z8HoLD4dqPIbGz1VWJWELhRkSktvN64cu7YM2X4HDCNe9DUk+rqxKxjMKNiEhtZhgwdzQsfx9sDrhqCrQ60+qqRCylcCMiUpv9+Az8PtncHjgJ2l1obT0ifkDhRkSktvr9NZg/zty+4DnoMsjaekT8hMKNiEht9NdH8PV95vYZD0Kvm62tR8SPKNyIiNQ2f38Ln91qbve6BU7/j7X1iPgZhRsRkdpk2+8wYyh4S6DTlXDeM2CzWV2ViF9RuBERqS3S1pizD5cUQOtzzA7Edv0ZF/kn/VaIiNQGWTvgvSugMBOa9ISrp0KQ0+qqRPySwo2IiL/Lz4B3L4fsndCgLQz5EJzhVlcl4rcUbkRE/FlxPkwbBOnrILIRXPcxhMVaXZWIX/OLcDNx4kSaN29OSEgIvXv3ZtGiReV63vTp07HZbAwcOLB6CxQRsYKnBGaOgB2LzIUwr/sYYpKsrkrE71kebmbMmEFycjJjx45l6dKldOnShQEDBpCWlnbU523ZsoV7772XU089tYYqFRGpQYYBs0bB+jkQFAKDZ0B8B6urEqkVLA83zz//PDfffDMjRoygQ4cOTJ48mbCwMN56660jPsfj8XDttdfy2GOP0bJlyxqsVkSkhvz4DCx9B2x2uOJNaNbH6opEag1Lw01xcTFLliyhf//+vn12u53+/fuzcOHCIz7v8ccfJy4ujhtvvPGY71FUVER2dnaZm4iIX1v6TtllFdpfZG09IrWMpeEmPT0dj8dDfHx8mf3x8fGkpKQc9jkLFizgzTff5PXXXy/Xe4wbN47o6GjfLSlJ16tFxI+t/wa+vMfcPvXf0PPY/xEnImVZflmqInJychg6dCivv/46DRo0KNdzRo8eTVZWlu+2ffv2aq5SRKSSdi6Bj4aD4YEug+GsR6yuSKRWCrLyzRs0aIDD4SA1NbXM/tTUVBISEg45fuPGjWzZsoWLL77Yt8/r9QIQFBTEunXraNWqVZnnuFwuXC5XNVQvIlKFMjbD+1eDOx9anQ2XTNCyCiKVZGnLjdPppHv37sybN8+3z+v1Mm/ePPr0ObTzXLt27VixYgXLly/33S655BLOPPNMli9frktOIlI75WfA+1dCfjokdDZnH3YEW12VSK1lacsNQHJyMsOHD6dHjx706tWL8ePHk5eXx4gRIwAYNmwYjRs3Zty4cYSEhNCpU6cyz4+JiQE4ZL+ISK3gLoQPBsPeDRCdZM4+7Iq0uiqRWs3ycDNo0CD27NnDmDFjSElJoWvXrsyZM8fXyXjbtm3YtTCciAQirxc+/Rds/w1c0XDtRxCVaHVVIrWezTAMw+oialJ2djbR0dFkZWURFRVldTkiUpd98zD8OgHswTD0U2ihSUlFjqQi399qEhERscKi181gAzBwkoKNSBVSuBERqWnr5sDX/zG3z3oEOl9lbT0iAUbhRkSkJu3+E2beAIYXug01J+oTkSqlcCMiUlOydsK0QeDOg5ZnwkUvaC4bkWqgcCMiUhMKs2Ha1ZCzGxq211w2ItVI4UZEpLp5SmDmCEhdCRHxcO2HEBJtdVUiAUvhRkSkOhmG2Xl4w3cQHAaDp0NMU6urEgloCjciItXp98mw+E3ABpe/Do1PsroikYCncCMiUl3WfQ1zRpvb5z4B7S+yth6ROkLhRkSkOuz+C2beCBjQ/Xroc4fVFYnUGQo3IiJVLXv3QUO+z4ALntOQb5EapHAjIlKVivPhg2sgZxc0aANXaci3SE1TuBERqSr7V/nevRzC6sOQDyE0xuqqROochRsRkaryw5Ow5gtwOGHQ+xDbwuqKROokhRsRkarw53T4+f/M7YtfgmZ9rK1HpA5TuBEROV5bF8IXd5rbp/4bug62th6ROk7hRkTkeOzbAjOuBU8xtL8EznzY6opE6jyFGxGRyirMhmnXQP5eSOwCl00Gu/6silhNv4UiIpXh9cDHN8GeNRCRYK4Z5Qy3uioRQeFGRKRyvh0Df8+FoBAYPA2iGlldkYiUUrgREamope/CwpfN7YGToHF3a+sRkTIUbkREKmLrr/DVKHP79Aeg0+XW1iMih1C4EREpr31bYMZ14HVDx8vg9PutrkhEDkPhRkSkPIpy4IPBpSOjusKlr2hklIif0m+miMixeL3wyS2Qthoi4mHwB+AMs7oqETkChRsRkWP5/glYNxscLrhGI6NE/F2lwk1eXl5V1yEi4p/++hAWPG9uX/oyNOlhbT0ickyVCjfx8fHccMMNLFiwoKrrERHxHzuWwOd3mNunjILOV1tbj4iUS6XCzXvvvUdGRgZnnXUWbdq04emnn2bXrl1VXZuIiHWyd8H0IeApgjbnw1ljrK5IRMqpUuFm4MCBfPbZZ+zcuZNbb72VadOm0axZMy666CI++eQTSkpKqrpOEZGa4y6A6ddCbgo0bA9XvK6RUSK1yHH9tjZs2JDk5GT++usvnn/+eb777juuvPJKGjVqxJgxY8jPz6+qOkVEaoZhwBd3wa6lEFrPHBnlirS6KhGpgKDjeXJqaipTp05lypQpbN26lSuvvJIbb7yRHTt28Mwzz/Dbb7/xzTffVFWtIiLV75cXYcWHYHPA1e9AbAurKxKRCqpUuPnkk094++23mTt3Lh06dOD222/nuuuuIyYmxndM3759ad++fVXVKSJS/dbPhe8eNbfPfwZanGZpOSJSOZUKNyNGjOCaa67hl19+oWfPnoc9plGjRjz00EPHVZyISI3Zsw5m3ggY0H0E9LzJ6opEpJJshmEYFX1Sfn4+YWG1c3bO7OxsoqOjycrKIioqyupyRMQfFOyD18+CjE3QrB8M/QyCnFZXJSIHqcj3d6U6FEdGRpKWlnbI/r179+JwOCrzkiIi1vCUwMwbzGAT3dTsZ6NgI1KrVSrcHKmxp6ioCKdTfxREpBb5bixs/B6Cw2DwNAhvYHVFInKcKtTn5qWXXgLAZrPxxhtvEBER4fuZx+Php59+ol27dlVboYhIdVk+DRa+bG4PnAQJJ1pbj4hUiQqFmxdeeAEwW24mT55c5hKU0+mkefPmTJ48uWorFBGpDjsWw5d3m9un/Qc6DrS0HBGpOhUKN5s3bwbgzDPP5JNPPqFevXrVUpSISLXK3m3OQOwphrYXwhmjra5IRKpQpYaC//DDD1Vdh4hIzXAXwoyDlla4/FUtrSASYModbpKTk3niiScIDw8nOTn5qMc+//zzx12YiEiVMwz4ahTsXAIhMWYHYi2tIBJwyh1uli1bhtvt9m0fic1mO/6qRESqw2+T4M9pYLPDVVMgtqXVFYlINSh3uDn4UpQuS4lIrbPxB/imdNb0c/8Lrc60th4RqTaVutD83nvvacVvEak9MjbBR9eD4YWu18LJt1ldkYhUo0qFm1GjRhEXF8eQIUOYPXs2Ho/nuIqYOHEizZs3JyQkhN69e7No0aIjHvvJJ5/Qo0cPYmJiCA8Pp2vXrrz77rvH9f4iEsCKcuCDIVCYCY17wIXPgy6fiwS0SoWb3bt3M336dGw2G1dffTWJiYmMHDmSX3/9tcKvNWPGDJKTkxk7dixLly6lS5cuDBgw4LDLOwDExsby0EMPsXDhQv766y9GjBjBiBEjmDt3bmU+iogEMq8XPr0V9qyBiAQY9B4Eh1hdlYhUs0otnHmw/Px8Pv30U6ZNm8Z3331HkyZN2LhxY7mf37t3b3r27MnLL5uzhHq9XpKSkrjzzjt54IEHyvUaJ510EhdeeCFPPPHEIT8rKiqiqKjI9zg7O5ukpCQtnClSF8x/GuaPA4cTRnwNTXpYXZGIVFK1L5x5sLCwMAYMGMD555/PCSecwJYtW8r93OLiYpYsWUL//v0PFGS3079/fxYuXHjM5xuGwbx581i3bh2nnXbaYY8ZN24c0dHRvltSUlK56xORWmzNl2awAbjoBQUbkTqk0uEmPz+f999/nwsuuIDGjRszfvx4LrvsMlatWlXu10hPT8fj8RAfH19mf3x8PCkpKUd8XlZWFhERETidTi688EImTJjAOeecc9hjR48eTVZWlu+2ffv2ctcnIrVU6mr45F/mdu/boNt11tYjIjWqUjMUX3PNNXz11VeEhYVx9dVX88gjj9CnT5+qru2IIiMjWb58Obm5ucybN4/k5GRatmzJGWecccixLpcLl8tVY7WJiMXyM2D6YHDnQYvT4Nwnra5IRGpYpcKNw+Hgww8/ZMCAAWUWz6yoBg0a4HA4SE1NLbM/NTWVhISEIz7PbrfTunVrALp27cqaNWsYN27cYcONiNQhnhKYOQL2bYGYZnDVVHBU6s+ciNRilbostf9y1PEEGzBXEu/evTvz5s3z7fN6vcybN69CLUFer7dMp2ERqaO+HQOb5kNwOAz+AMJira5IRCxQ7v+keemll7jlllsICQnhpZdeOuqxd911V7kLSE5OZvjw4fTo0YNevXoxfvx48vLyGDFiBADDhg2jcePGjBtndgwcN24cPXr0oFWrVhQVFTF79mzeffddJk2aVO73FJEAtPwD+G2iuX3ZJIjvaG09ImKZcoebF154gWuvvZaQkBBeeOGFIx5ns9kqFG4GDRrEnj17GDNmDCkpKXTt2pU5c+b4Ohlv27YN+0Er9ubl5XH77bezY8cOQkNDadeuHe+99x6DBg0q93uKSIDZsQS+vNvcPu0/0OFSa+sREUsd9zw3tU1FxsmLSC2QkwKvnQk5u6DtBTDofbAf9ywXIuJnqn2em8cff/ywa0sVFBTw+OOPV+YlRUQqrqQIZgw1g02DtnDZqwo2IlK5lhuHw8Hu3buJi4srs3/v3r3ExcUd91pT1UktNyIBwjDgizth2bsQEg03/wD1W1ldlYhUk2pvuTEMA9thFp77888/iY3V6AQRqQF/vGEGG5sdrnxLwUZEfCo0AUS9evWw2WzYbDbatGlTJuB4PB5yc3O59dZbq7xIEZEyNv8MX99vbvd/DFr3P/rxIlKnVCjcjB8/HsMwuOGGG3jssceIjo72/czpdNK8efManalYROqgfVvhw2FgeODEq6HvnVZXJCJ+pkLhZvjw4QC0aNGCvn37EhwcXC1FiYgcVnEeTB8CBRmQ2BUueQkOc4lcROq2coeb7OxsXweebt26UVBQQEFBwWGPVUddEalyhgGf3QapKyE8Dq55H4JDra5KRPxQucNNvXr1fCOkYmJiDtuheH9HY38eLSUitdRPz8Hqz8EeDIPehegmVlckIn6q3OHm+++/942E+uGHH6qtIBGRQ6ydBT+Uru594XPQ9GRr6xERv1bucHP66acfdltEpFqlrYFPbjG3e94M3a+3tBwR8X+Vmudmzpw5LFiwwPd44sSJdO3alSFDhrBv374qK05E6rj8DPhgMBTnQrNT4LxxVlckIrVApcLNfffdR3Z2NgArVqwgOTmZCy64gM2bN5OcnFylBYpIHeUpgZkjYN9miG4KV08Fh0ZoisixVWgo+H6bN2+mQ4cOAHz88cdcfPHFPPXUUyxdupQLLrigSgsUkTrq20dg03wIDoPB0yC8gdUViUgtUamWG6fT6Vs487vvvuPcc88FIDY21teiIyJSacveh99eMbcvmwwJJ1pbj4jUKpVquTnllFNITk6mX79+LFq0iBkzZgCwfv16mjTR8EwROQ7bF8FX95jbp98PHS61tBwRqX0q1XLz8ssvExQUxMyZM5k0aRKNGzcG4Ouvv+a8886r0gJFpA7J3gUzrgNPMbS7CE5/wOqKRKQWshmGYVhdRE2qyJLpIlKD3AXw9vmwaxnEdYQbvwFXhNVViYifqMj3d6UuSwF4vV42bNhAWloaXq+3zM9OO+20yr6siNRFhgFf3GkGm9BYswOxgo2IVFKlws1vv/3GkCFD2Lp1K/9s+NHyCyJSYQtegBUfgT0Irn4H6jW3uiIRqcUqFW5uvfVWevTowaxZs0hMTDzsOlMiIuWy7muY97i5ff7/oMWp1tYjIrVepcLN33//zcyZM2ndunVV1yMidUnaGvj4JsCAHjdCzxutrkhEAkClRkv17t2bDRs2VHUtIlKX5GfAB9eYSys0PxXOf8bqikQkQFSq5ebOO+/k3//+NykpKZx44okEB5edEr1z585VUpyIBCiPGz4cBvu2QExTuEpLK4hI1anUUHC7/dAGH5vNhmEYft+hWEPBRfzAV8mw+E1wRphDvuM7Wl2RiPi5ah8Kvnnz5koVJiLCotfNYIMNLn9dwUZEqlylwk2zZs2qug4RqQs2/Qhf329unz0G2mmhXRGpepXqUAzw7rvv0q9fPxo1asTWrVsBGD9+PJ9//nmVFSciAWTvRrOfjeGBE6+GU0ZZXZGIBKhKhZtJkyaRnJzMBRdcQGZmpq+PTUxMDOPHj6/K+kQkEBRmwQeDoTATGneHSyaA5scSkWpSqXAzYcIEXn/9dR566CEcDodvf48ePVixYkWVFSciAcBTAh+NgPR1ENkIrpkGwSFWVyUiAaxS4Wbz5s1069btkP0ul4u8vLzjLkpEAsg3D8PGeRAUCoM/gMgEqysSkQBXqQ7FLVq0YPny5Yd0LJ4zZw7t27evksJEJAAsfht+n2RuX/4qNOpa6Zfyeg2KPV6KSry4PV48XqPM7XBzWthtYLfZcNjNW5DdRnCQnWC7nWCHuU/Lx4gEnkqFm+TkZEaOHElhYSGGYbBo0SI++OADxo0bxxtvvFHVNYpIbbT5J5h9LwAlpz9IWqNz2bsji8yCYvblu8nML2ZfnpvsQje5hSXkFLnJKSwhp7CEgmIP+e4SCoq9FBSXUFTipcRb4Sm5jslmA1eQnZBgh+8+JMhBiNNBWLCDMKeDUKeDcGcQ4a4gIlwOwl3mdmTI/luw7z46NJhwp0OBScRilQo3N910E6GhoTz88MPk5+czZMgQGjduzIsvvsg111xT1TWKiB/LKXSzPaOAXZkF7M4uJCWrAHfaBu7cdCuRRglf04/b5naEud9X6fs67DYcNht2O+b9PwKFAXgNs1XnwH3Z1zAMKHR7KXR7q7SuqJAgokKDiQkNJjrMSUxoMDFhwaX3TuqFB1MvzEm9MCex4U7qhTsVikSqUKXCTUFBAZdddhnXXnst+fn5rFy5kl9++YUmTZpUdX0i4gcy84vZlJ7H5j15bErPZUt6Ptsy8tm+L5/MfHeZY6PI5VPnWCLtOSz3tuSe4psB85JQbLj5ZR4dWvrlHh5MVGgwka4DLSARriDCnEGEOktbToIdhAQ7cAbZcQaZl5OcDnulgoDXa+D2einxGLg9XopLzMtcRSUeCt3mfUGxlwK3h/zi0hakYnM7t2j/fQm5heZ9ju/eTXZBCcWll8v25bvZl+9mawVqcwbZiS0NO/UjSu/DXdSPcFI/3En9CHO7Qem+MIUhkSOqVLi59NJLufzyy7n11lspLi7mkksuITg4mPT0dJ5//nluu+22qq5TRGpATqGbdSk5rEvNYX1KDutTc1mfmsPevOKjPi8mLJgm9UJpEhXMfenP0ypnN/khCRRf8D5fxifRMMJFdGgwdru1X8Z2uw2X3YGrUn/5js4wDIpKvGQVuMkucJNZ4CYr37zPzC8mM999yCW5ffnFZOQVU1RiBq2U7EJSsgvL9X4hwXYaRLioH+GiQbiTBhEuGkSW3pfeGkY6aRgRQlRokIKQ1CmV+hVfunQpL7zwAgAzZ84kPj6eZcuW8fHHHzNmzBiFG5FaIDO/mD93ZLFyZxardmWxelc2W/bmH/H4hKgQWjQIp0XDcFo2CCcpNoykemEkxYYSGRJsXuOZlQybF0NwOGHDP6JXYoca/ETWstlsZp+dYAfxUeUf6m4YBgVuDxl5ZuDZm1dERl4xe3OL2ZtXzN5c83F66XZ6bpHvUtqOfQXs2FdwzPdwOuzUj3DSMHJ/8HGWhh/XIfdRIQpCUvtVKtzk5+cTGRkJwDfffMPll1+O3W7n5JNP9s1WLCL+o8TjZW1KDou3ZPDnjiyWb89kc/rhp21IiAqhXWIkbeL33yJo1TCC8GM1d/z+Kix+C7DBFW9AYueq/yAByGazEeY0L8U1qVe+5+QVlbA3t5g9uUWlgaeY9NLgs39/ek4Re3KLyCk0L5ftzipkd9axW4WcDrsZfv4RhMyWIfNxw9LH/tAaJ3I4lQo3rVu35rPPPuOyyy5j7ty5jBplTqOelpamlbZF/ECh28PSbfv4Y/M+Fm/NYOnWfeQVew45rnn9ME5sEkPHRlGlt2hiw50Vf8O/v4W5o83tcx7XmlHVbP+Irab1w455bKHbw968YvbkmIFnfwhKzzX3HS4I7coqZFc5gtD+flQN9vcHKg1D9SNc1C/dv78PUYMIFyHBjmO+pkhVqFS4GTNmDEOGDGHUqFGcffbZ9OnTBzBbcQ43uZ+IVK8Sj5cVO7P4deNeft2YzuIt+ygqKTsCKDIkiO7N6tEtqR5dkqLp0iSGepUJMv+UusqcgdjwQreh0PfO439NqTIhwQ4ax4TSOCb0mMcWuj1lgk967sGBqLRFqLR1KKvATYnXIC2niLSconLVEuZ0lHaUdpZ2LjdDkTlqLLjM6LF6YWbHc4dahqQSbIZhVGryiJSUFHbv3k2XLl2w282JjhctWkRUVBTt2rWr0iKrUnZ2NtHR0WRlZamVSWq11OxCfly/hx/X7eHnv/eQXVhS5udxkS56t6xPr+b16NE8ljbxkVX/RZGTCm+cDVnbofmpcN0nEFQFgUn8XnGJl715RaTnmJfE9uaV3h90aSwjr9jXf6jYU/Hh9jYbvpF1+4fS1wtzEh0WTExo6b4wc8Rd9D9uwY5Krwstfqoi39+VDje1lcKN1FaGYbByZzbfrk7huzVprN6dXebnUSFB9GlVn36tG9C3VX1aNYyo3o6hxfkw9SLYuQTqt4Ybv4Ww2Op7P6m1DMMgt6jEDDqlYScjzwxE+/KKySgdObb/8b78YnL+EdYrKszpICokmKhQc5qBqIMmXIwICSLqoKkHfLeQ/ZM1mvdhwQ71KfIjFfn+roYBkSJSVdweLws37uWb1Sl8tzqtzDBhmw06N4nh9DYNOb1NQ7omxdRcE77XC5/dagab0How5EMFGzkim81WGiyCaVY/vFzPcXu8ZOaboefAvTmUPqvATWa+m6yC0iH2pfuyC92+UJRfOkdRSvYx3ugYwpyO0g7f5rxL4S5zO6R0BmtzLqYgQoLtvjmZQoLtuPZvB5Vul947HXZcwXZcpfM2OR3752+yE6TlQKqMwo2In3F7vPy6cS+z/9rN3NUpZSbJC3M6OL1NQ/q3j+eMtg2pH+Gypsjvn4DVn4M92Fzlu34ra+qQgBXssNMw0hyeXhEer0FOoRl2cgpLyC4NPdkFJb7wY06+6PZNwnjwxIy5RSXkFZX4ZrPeH5JqitNhTlQZHGQnqHQNtCCHjWC73VwfrTQE7V8rzdxnO7CGms2G/aDZu+2ls3fvX2cNG77HNmzYbJTebNgwt+HAz8zt0vujBK/9P/IYxSzMfZ6+Da7gsXMHVtdpOiaFGxE/4PUaLNm2j0+X7WT2it1lAk39cCfndozn3A4J9GlV3/oRJ8vegwXPm9uXvgzN+lpbj8hBHHYbMWFOYsIq3/fLMAwK3V7yis2gs3+W6vxiD3lFJaUzWHt8M1gXuD0U+m5eCoo9FJZ4KHJ7y9z7ZsR2e3yLwP6zY0ixx0uxB6jBQFV1DEIafUhw9HI+37WR+93nEhZ87BF91cEvws3EiRN59tlnSUlJoUuXLkyYMIFevXod9tjXX3+dd955h5UrVwLQvXt3nnrqqSMeL+LPNqTl8umyHXy2bBc7Mw9MxtYgwsmAjglceGIivVrEEuQvnSM3/gBf3m1un3YfdNFachJ4bDYboaWLpjaoxtZRo3TNs+LSpUCKS7y4vQYlHi9ujxd36TIhJV6DEo9BSenSIR6vQYnXvDe3vb5tcx01c101r2Hg9Rp4DPM/oAwMDAO8hvlzwzAfG1B6f+Dx/tT1z065h+ula5QetSrvC5bkLMOGnauaPmBZsAE/CDczZswgOTmZyZMn07t3b8aPH8+AAQNYt24dcXFxhxw/f/58Bg8eTN++fQkJCeGZZ57h3HPPZdWqVTRu3NiCTyBSMblFJXz15y4+XLydpdsyffsjXEGc1ymBgV0b06dVff8bApu2Bj4cBt4S6HQlnPmQ1RWJ1Go2m3lJKchh5zgamvzCTzt+4t157wHwQK/7GdL+EkvrsXy0VO/evenZsycvv/wyAF6vl6SkJO68804eeOCBYz7f4/FQr149Xn75ZYYNG3bM4zVaSqxgGAZLt2XywaJtzPprNwVus8nZYbdxRpuGXHZSY/q3j7f+ktORHDzku2kfGPY5BFnU30dE/MqmrE1cO+tact25XHHCFYztM7ZaOkbXmtFSxcXFLFmyhNGjR/v22e12+vfvz8KFC8v1Gvn5+bjdbmJjDz9So6ioiKKiAxNMZWcfZ9d5kQrILSrhs2U7ef/3baw5aOh2y4bhDOqRxGUnNSYusvzrEFmiOA8+GGQGm9hWZgdiBRsRAbKKsrj7+7vJdedyUtxJPNT7Ib8Y8WVpuElPT8fj8RAfH19mf3x8PGvXri3Xa9x///00atSI/v37H/bn48aN47HHHjvuWkUqYkNaLlN/3cKny3aSW2QOTXUF2bm4SyMG90ripKb1/OIPwDF5PfDJLbBrGYTGwrUfaci3iABQ4i3h/p/uZ0v2FhLCE3j+jOcJdgRbXRbgB31ujsfTTz/N9OnTmT9/PiEhh/+v39GjR5OcnOx7nJ2dTVJSUk2VKHWIYRj89Hc6by3YzI/r9/j2t2wYzrW9m3HlSU2IDvOPX/xyMQyYMxrWfgUOJwz+QEO+RcTnucXP8cuuXwgNCmXCWROoH1rf6pJ8LA03DRo0wOFwkJqaWmZ/amoqCQkJR33uc889x9NPP813331H585HXn3Y5XLhcqkJXapPUYmHT5fu5I0Fm9mQlguYcz70bx/P9X2b07dV/drRSvNPv70Ci141ty97FZqebG09IuI3Plz3Ie+veR+Ap055inax/rXskqXhxul00r17d+bNm8fAgQMBs0PxvHnzuOOOO474vP/973/897//Ze7cufTo0aOGqhUpK7vQzfu/beOtXzazp3ThwHCng6t7JnF93+blnonVL636DOaWjoY65wnodLml5YiI//h99+889ftTANzV7S76Nzt8txArWX5ZKjk5meHDh9OjRw969erF+PHjycvLY8SIEQAMGzaMxo0bM27cOACeeeYZxowZw7Rp02jevDkpKSkAREREEBERYdnnkLpjT04RbyzYxPu/bfP1p0mICuHGU1owqFcSUSG16NLT4Wz73exngwE9b9Yq3yLisyVrC6Pmj8JjeLio5UXcdOJNVpd0WJaHm0GDBrFnzx7GjBlDSkoKXbt2Zc6cOb5Oxtu2bfOtOg4wadIkiouLufLKK8u8ztixY3n00UdrsnSpY1KzC3n1x01MW7SVQre5wvEJcRH86/RWXNKlEc4gP5lo73ikb4APrgFPEbQ5H85/5sC86iJSp2UVZXHn93eSU5xDl4ZdeLTvo357yd3yeW5qmua5kYranVXApPkbmf7HdopLzFDTNSmGO85szVnt4gJn1eDcNHijP2RuhUYnwfVfgbMWX1oTkSrj9ri59btbWZSyiMTwRKZdOI0GoQ1qtIZaM8+NiD/bk1PEK/M38P7v23yhpkezetx19gmcekIDv/0vlkopyoX3rzKDTb3mMGSGgo2IAOZI0Md/e5xFKYsIDw5nwlkTajzYVJTCjcg/ZOW7efWnjbz9yxbfTMK9msdyT/8T6FNbRz4djccNHw2H3cshrD5c9wlEHLr0iYjUTW+ufJPPNnyG3Wbn2dOepW1sW6tLOiaFG5FShW4Pb/+yhVfmbyCn0Owo3KVJNP8+t23gtdTsZxjw5T2w4TsICoUhH2ouGxHxmbNlDi8ufRGA0b1Gc2qTUy2uqHwUbqTO83gNPl22k//7Zh27swoBaBsfyb/PbcM5HeIDM9Ts98NTsPw9sNnhqinQRFMriIjpzz1/8tDP5pQQ17W/jmvaXWNxReWncCN12k/r9/DU7DWsTckBoFF0CP8+ty0DuzX2v1W5q9ofb8JP/zO3L3oB2p5nbT0i4je2Z2/nru/vothbzBlJZ3Bvj3utLqlCFG6kTtq0J5cnZ63h+7VpAESGBHHHma0Z3re5/67MXZVWfw6z/m1un34/dL/e0nJExH9kFmZy27zbyCjMoH1se5459Rkc9tr1d1HhRuqUrAI3E+b9zZRft1DiNQiy2xjWpzl3ntWaeuFOq8urGVsWwMc3AYYZas4YbXVFIuInijxF3PXDXWzN3kpieCITz55IWHCY1WVVmMKN1Aler8FHS7bzzJx1ZOQVA3Bm24Y8fFEHWjWsQzNbp6yADwaDpxjaXQQXPq9J+kQEAK/h5cGfH2RZ2jIigyOZ1H8SDcMaWl1WpSjcSMBbuTOLRz5fybJtmQC0ahjOIxd14Iy2dWy4876t8N6VUJQNTfvCFW9CLWtqFpHq88KSF/hm6zcE2YN48awXaRVTe0dOKtxIwMrKd/N/367jvd+24jXMRS3v6d+G6/s1J9gRAEslVETuHnj3MshNgbiOMPgDCA6xuioR8RPvr3mfKaumAPBkvyfpmdDT2oKOk8KNBBzDMPjyr908/uUq0nPNS1AXd2nEQxe0JyG6Dn6hF2bDe5dDxkaIbgrXzYTQGKurEhE/MXfLXJ5Z9AwAd590Nxe2vNDiio6fwo0ElO0Z+Tzy+Urmr9sDQMuG4TxxaSf6tfbvqcKrjbvQ7GOT8heENYBhn0FUI6urEhE/8UfKH4z+eTQGBoPaDuLGTjdaXVKVULiRgFDi8TLl1y383zfrKXB7cDrsjDyzNbee0RJXUB3tV+IpgZk3wNYF4IyEoZ9o9mER8Vm/bz13f383bq+b/k37M7rX6ICZtFThRmq9dSk5/Gfmn/y5IwuAXi1ieeqyE2kdV4dGQf2TYcCXd8O6WeBwmX1sErtYXZWI+Indubu57dvbyHHncFLcSYw7dVytm8vmaBRupNZye7xMnr+Rl77/G7fHIDIkiIcuaM/VPZKwB/rswkdjGDD3oYOWVXgbWtSO9WBEpPplFmZy63e3klaQRqvoVrx01kuEBAVWf0SFG6mVVu/K5r6Zf7JqVzYA/dvH8d/LTiQ+KrB+QSvlx//BbxPN7Utehna1v3OgiFSNfHc+I+eNZFPWJuLC4ph8zmSiXdFWl1XlFG6kVinxeHn1p02M/249bo9BTFgwj17ckUu7NgqYa8XH5bdJMP8pc/u8p6HbtdbWIyJ+w+1xM2r+KP5K/4soZxSv9n+VhPAEq8uqFgo3UmtsSc8j+cPlLC2djO/cDvE8eVkn4iLVWgPA0ndhzgPm9hkPwsm3WVuPiPgNr+HloQUP8euuXwkNCuWV/q/Qul5rq8uqNgo34vcMw+D937fx31lrKHB7iHQF8eglHbn8pMZqrdlv1Wfw5V3mdp874PT/WFqOiPgPwzAY9/s4vt7yNUG2IJ4/43m6NAzsAQYKN+LX0nOLuO+jP/mhdN6aPi3r89zVXWgcE2pxZX5k/Vz4+EYwvNBtKJz7pNaLEhGfSX9OYvq66diw8d9T/sspjU+xuqRqp3AjfuvH9Xv494d/kp5bhDPIzv3ntWNE3+Z1eyTUP238AWYMBW8JdLwcLn5RwUZEfN5Z9Q6T/pwEwAO9HuCClhdYXFHNULgRv1NU4uHZOet4Y8FmANrGR/LS4G60TYi0uDI/s3UhTB8CniJoeyFc/poWwhQRn0/+/oRnFz8LwMiuIxnSfojFFdUchRvxK5v25HLHtGWs3m0O8R7epxmjL2hPSLC+tMvYuQTevwrc+dDqbHMuG0ew1VWJiJ+Ys2UOj/76KADDOwznX53/ZW1BNUzhRvzG58t38uAnK8gr9hAb7uTZKztzdvt4q8vyP7v/gncvh+IcaH4qDHoPglxWVyUifuLnHT/71ou64oQr+HePf9e5wRcKN2K5QreHR79YxfQ/tgNwcstYXrymmybkO5zUVfDOpVCYCU16mssqOMOsrkpE/MQfKX8wav4oSrwlnNf8PB45+ZE6F2xA4UYstiEtlzumLWVtSg42G9x51gncffYJONRp+FBpa2DqJVCQAY1Ogus+Bpf6IYmIaVnaMkbOG0mRp4jTmpzGU6c+FVDrRVWEwo1YZvaK3dz30Z/kFXtoEOFk/KBunHJCA6vL8k971pvBJj/dXABz6CcQEnhTpotI5axMX8nt391OQUkBJyeezPNnPE+wve72w1O4kRrn9nh55uu1vtFQvVvEMmFwN+J0Gerw0jfA1IshLw0SToShn0FoPaurEhE/sS5jHf/69l/kunPpHt+dl856CZejbvfDU7iRGpWWXcgd05axaEsGAP86vSX3nduWIIfd4sr81N6NMPUiyE2BuI4w9HMIi7W6KhHxExszN3LzNzeTXZxNl4ZdmHj2REKDNMmpwo3UmMVbMrjt/aXsySkiwhXEc1d14bxOgbloW5VI/xumlAabhu1h2OcQXt/qqkTET2zK3MSNc29kX9E+OtTvwKT+kwgPDre6LL+gcCM1Ytrv2xj7xUrcHoM28RFMvq47LRtGWF2W/9qz3rwUlZsCcR1g2BcQ0dDqqkTET2zK2sQNc29gb+Fe2tZry6v9XyXSqQEG+yncSLUqLvHy6JermPb7NgAuPDGRZ6/qTJhT//SOaM/60ktRqealqOFfQLg6WouIaXPWZm6ceyN7C/fSpl4bXj/3dWJCYqwuy6/oG0aqTVpOIbe/t5TFW/dhs8G957bl9jNa1ck5F8otbe2BzsPxncwWG12KEpFSW7K2cOPcG0kvSOeEeifwxrlvUC9EAwz+SeFGqsXKnVnc/M5idmcVEhkSxEvXdOPMdnFWl+XfUlaYE/Tl74X4E80WG3UeFpFS+4PNnoI9tI5prWBzFAo3UuXmrNzNqBl/UuD20LJhOG8M66H+Nceyc4m5pEJhJiR2haGfKtiIiM+mzE3c+I3ZYrM/2MSG6G/EkSjcSJUxDIOJP2zguW/WA3DqCQ14echJRIfW3YmkymXbb+YimEXZ0KQXXDdTE/SJiM/f+/7mpm9uIqMww3cpSsHm6BRupEoUuj3c//FffL58FwDX923Owxe21/w1x7L5J5h2DbjzoNkpMGS6llQQEZ91Geu46ZubyCzKpH1se1475zV1Hi4HhRs5bhl5xdz8zmKWbN2Hw27jsUs6ct3Jzawuy/+tnwsfDoOSQmh1Fgx6X4tgiojPqr2ruOWbW8guzqZT/U5MPmcy0S616paHwo0cl83peYx4exFb9uYTGRLE5Ou606+1hi0f04qZ8Om/wFsCbc6Hq6ZAsJafEBHT8rTl3PbdbeS6c+ncsDOT+0/WPDYVoHAjlbZocwa3vLuYzHw3TeqFMmVET1rH6ZfvmBa/DV+NAgw48SoYOAkc6pckIqaFuxZy9w93U1BSwElxJzHx7IlEODUooyIUbqRSPl++k/s++otij5cuSTG8MawHDSPr9kJt5fLLS/DtI+Z2jxvggv8Du/oliYjph20/8O8f/43b66Zfo368cOYLWiuqEhRupEIMw+C1nzYx7uu1AAzoGM/4Qd0IdTosrszPGQZ8/wT8/H/m4373QP9HQRMaikiprzd/zeifR+MxPJzd9Gz+d9r/cDqcVpdVKyncSLl5vQZPzFrN279sAeCGfi146ML2OOz6gj4qTwnMGgVL3zEfnz0GTv23tTWJiF/5aP1HPLHwCQwMLmp5EU/0e4Igu76iK0tnTsql0O3h3x/+yawVuwF46IL23HxaS4urqgXchfDxjbD2K7DZ4aIXoPv1VlclIn7CMAzeWPEGLy17CYCr21zNQyc/hN2my9XHQ+FGjimrwM0t7yzm980ZBDtsPHdVFy7t2tjqsvxfYRZ8MAS2LgCHC658E9pfbHVVIuInvIaX5xY/x7ur3wXg5hNv5s5ud2r9vSpgeTScOHEizZs3JyQkhN69e7No0aIjHrtq1SquuOIKmjdvjs1mY/z48TVXaB2VllPIoFcX8vvmDCJcQUwd0UvBpjyyd8PbF5rBxhUF132sYCMiPm6vm4cXPOwLNvf1uI+7TrpLwaaKWBpuZsyYQXJyMmPHjmXp0qV06dKFAQMGkJaWdtjj8/PzadmyJU8//TQJCQk1XG3ds21vPldOWsjalBwaRLiY8a+T6as5bI4tbS28eQ6kroDwOLh+FrQ41eqqRMRPFJQUMOqHUXy56UscNgf/PeW/DOs4zOqyAorNMAzDqjfv3bs3PXv25OWXXwbA6/WSlJTEnXfeyQMPPHDU5zZv3px77rmHe+65p0LvmZ2dTXR0NFlZWURFRVW29IC3Znc2w95axJ6cIpJiQ3nvxt40qx9udVn+b8svMH2weUmqfmu4dibEtrC6KhHxE/sK93HH93fw156/cDlcPHf6c5yRdIbVZdUKFfn+tqzPTXFxMUuWLGH06NG+fXa7nf79+7Nw4cIqe5+ioiKKiop8j7Ozs6vstQPVH1syuGHKH+QUltAuIZJ3buhFXJRmzz2mlZ+Ysw57iiGpNwyerpW9RcRnR84ObvvuNrZkbyHKGcXLZ79Mt7huVpcVkCy7LJWeno7H4yE+Pr7M/vj4eFJSUqrsfcaNG0d0dLTvlpSUVGWvHYh+XL+HoW/+Tk5hCT2a1WPGLX0UbI7FMMzJ+WaOMINNu4tg2OcKNiLis2bvGoZ+PZQt2VtIDE/k3fPfVbCpRpZ3KK5uo0ePJisry3fbvn271SX5rTkrU7hp6h8Uur2c0bYh797Ym+gwLQtwVB63uZTC/lmHe90CV78DwZpRVERMv+76lRFzR5BekE6bem149/x3aRmjqTSqk2WXpRo0aIDD4SA1NbXM/tTU1CrtLOxyuXC5tCzAsXy6bAf3fvQXHq/BhScm8sKgrjiDAj77Hp/CLPhwOGz6AbDBgKfg5Ns067CI+Hy8/mOe+O0JPIaHXgm9GH/meC2AWQMs+/ZyOp10796defPm+fZ5vV7mzZtHnz59rCqrTnr/960kf/gnHq/Bld2b8OI1CjbHtG8rvDnADDbBYXDNNOhzu4KNiADmHDbjl4zn0YWP4jE8XNTyIib1n6RgU0MsncQvOTmZ4cOH06NHD3r16sX48ePJy8tjxIgRAAwbNozGjRszbtw4wOyEvHr1at/2zp07Wb58OREREbRu3dqyz1GbvfHzJp6ctQaA4X2aMfbijti1nMLRbV8E04dA3h6ITDQ7DjfqanVVIuInijxFPLzgYeZsmQPArV1u5fYut2sOmxpkabgZNGgQe/bsYcyYMaSkpNC1a1fmzJnj62S8bds27AetmLxr1y66dTvQAeu5557jueee4/TTT2f+/Pk1XX6t98r8DfxvzjoAbjujFf8Z0Fa/fMeyfBp8ebfZcTj+RBgyA6I1qaGImPYW7OWeH+5h+Z7lBNmDeLTPo1za+lKry6pzLJ3nxgqa58b00ry/ef7b9QCM6t+Gu/ufYHFFfs7rgW/HwEJzTibaXQSXvQquCGvrEhG/sX7feu6cdye78nYR6Yxk/Bnj6ZXYy+qyAkatmOdGrGEYBs9/u54J328A4L4BbRl5pi7pHVVhFsy8ETZ8az4+7T9wxmiwq1+SiJh+3P4j//npP+SX5NM0sikTzp5Ay2iNiLKKwk0dYhgGz8xZx+QfNwJa2btc9qyD6dfC3r8hKBQGvgKdLre6KhHxE4ZhMHXVVJ5f8jwGBr0SevH8Gc8T7Yq2urQ6TeGmjjAMg6fnrOXVHzcBMPbiDozop2UBjmrNl/DprVCcC1GNzRFR6jgsIqWKPEU8vvBxvtj4BQBXtbmK0b1HE2zX/GBWU7ipA/a32OwPNk9c2pGhfZpbW5Q/83rgh//Cz/9nPm5+Klz5NkQ0tLYuEfEbKXkpjPphFCv3rsRus/Ofnv9hSLshGpThJxRuApxhGDw798ClqMcVbI4uPwM+uRk2fGc+PnkknPM4OPSrIiKmpalLGTV/FBmFGUS7onnu9Oc4OfFkq8uSg+gvdgAzDIPnvlnHK/PNYPPYJR0ZpmBzZDuWwEfXQ9Y2s3/NJROg81VWVyUifsIwDD5a/xHjFo2jxFtCm3ptePHMF2kS2cTq0uQfFG4C2AvfrmfiD2awGXtxB4b3bW5tQf7KMGDR6zD3QfC6IbaluT5UwolWVyYifqKwpJAnf3uSzzd+DsCA5gN4vO/jhAWHWVyZHI7CTYCa+MMGXiod7v3IReo8fERFOfDFXbDqE/Nx+0vg0pchRCMdRMS0PWc7yfOTWZuxFrvNzl3d7uKGTjeof40fU7gJQG/8vIln55ozD48+vx03nqJgc1i7/4SZN8DeDWAPgnOe0MKXIlLGj9t/ZPSC0eQU5xAbEsv/TvsfvRN7W12WHIPCTYB5d+EW31pRo/q34V+nt7K4Ij9kGPD7ZHPGYU+xOcz7yrehqf5giYipxFvCK8tf4fUVrwPQuWFn/u/0/yMhPMHiyqQ8FG4CyId/bOeRz1cBcPsZrbjrbM08fIi8vfD57bDeXNCOtheal6HCYq2tS0T8RmpeKv/56T8sTVsKwOB2g7mvx30EOzR/TW2hcBMgvvhzF/d/8hcAN/RrwX1aBPNQm+abk/Ll7AaHCwb8F3repMtQIuKzYOcCHvz5QfYV7SM8OJxH+zzKeS3Os7osqSCFmwAwb00qyTOWYxgwpHdTHrmovYLNwdyFMO9x+G2i+bj+CXDV2xoNJSI+bq+bicsm8ubKNwFoH9ue505/jqZRTS2uTCpD4aaW+3VjOre9v5QSr8HAro148tJOCjYHS1kBn9wCaavNx91HmC02znBr6xIRv7E9ezv3/3w/K9JXAHBN22u4t+e9uBwuiyuTylK4qcWWbdvHzVMXU1zi5ZwO8Tx7VRfsdgUbwFxCYeHL8P2TZqfh8IZwycvQVs3LInLAlxu/5MnfniS/JJ9IZySP9nmUc5ufa3VZcpwUbmqptSnZXP/2H+QVe+jXuj4TBncj2GG3uiz/kP43fHY77FhkPm57AVz8ktaGEhGfnOIc/vv7f5m1aRYAJ8WdxNOnPk1iRKLFlUlVULiphbbtzWfom4vIKnDTrWkMrw3tQUiww+qyrOf1wG+vmK01JYXgijIvQXUbqk7DIuLzR8ofPLzgYXbl7cJhc3Bbl9u46cSbcNj1dzRQKNzUMmk5hVz35u/sySmiXUIkU67vRbhL/zeyZz18cQds/9183Oosc22oaK35IiKmIk8RE5ZO4J3V72Bg0DiiMU+f+jRd47paXZpUMX0r1iJZBW6Gv/UH2zLySYoN5Z0behEdVsfnXSgpggUvwM//Z/atcUaarTUnDVNrjYj4rNm7hgcXPMiGTHNZmitOuIL7et5HeLAGFwQihZtaotDt4eapi1mzO5sGES7evaE3cVEhVpdlra0L4cu7Id1caoLW58BFL0BMkrV1iYjfcHvcvLHiDV5b8Rol3hLqh9Tnsb6PcXrS6VaXJtVI4aYWKPF4uWPaMhZtySDSFcTUG3rSvEEd/q+N/Axz3polb5uPwxvC+c9Ax8vVWiMiPqv3ruaRXx5h/b71APRv2p9H+jxCbIhmJA90Cjd+zjAMHvp0Jd+tScUZZOf14T3o2KiOrljt9cLy9+G7sZC/19zXbSic87iWTxARn2JPMZP/nMxbK9/CY3iIccXwYO8HOa/5eZoHrI5QuPFzL3y7nhmLt2O3wcuDu3Fyy/pWl2SN3X/CrHsPDO9u2A4ueA5anGptXSLiV5alLeOxXx9jY9ZGAM5tdi4P9n6Q+qF19G9nHaVw48fe/30rL31vdn57cuCJnNuxDq5Gm5cOPzxlXoIyvOCMgDMegN63ghaxE5FS2cXZvLjkRT5c/yEAsSGxPHzyw5zT7ByLKxMrKNz4qbmrUnjks5UA3H32CQzpXcfWNykphkWvwY//g6Isc1+nK+DcJyGqkbW1iYjfMAyD77Z9x7jfx7GnYA8Al59wOcndk4l21dFL+KJw448Wb8ngrg+W4TVgcK8k7ul/gtUl1RzDgLWz4NtHIGOTuS+hM5w3DpqfYm1tIuJXtmdvZ9yicfy882cAmkc1Z0yfMfRM6GlxZWI1hRs/s3FPLjdOXUxRiZf+7eN4oi4thLl1IXz3KGz/zXwcEQ9nj4Eug0Ezh4pIqcKSQt5a+RZvrniTYm8xQfYgbuh0A7d0vkWLXQqgcONX9uQUcf3b5rIKXZNimDD4JILqwnpRqath3mOwfo75OCgU+twOp4wCV6S1tYmI3zAMg592/MS4RePYmbsTgD6JfRjdezQtoltYXJ34E4UbP5FfXMJNU/9ge0YBzeqH8ebwHoQ6A7y1In0D/PQ/+OtDwACbA04aCqc/AFFavE5EDtiYuZFn/3iWX3b9AkBcWBz397yfc5qdU3dat6XcFG78gMdrcNcHy/hzRxb1woKZMqIX9SMCuGl170b46Vn4a4Y5Agqgw6Vw1iPQoA71LxKRY8oqyuKV5a8wY90MPIaHYHsw13W4jls730pYcJjV5YmfUrixmGEYPPrFKr5bk4YryM4bw3vSIlBnH96z3lwH6q8ZYHjMfW3OM4d2N+pmbW0i4lfcHjcfrv+QSX9OIqt0xORZSWdxb497SYrSEitydAo3Fnvj5828+9tWbDYYP6gr3ZvVs7qkqrdziRlq1nwFGOa+EwbAGfdD4+6WliYi/sUwDOZuncuLS15kR+4OAFrHtOb+XvdzcuLJFlcntYXCjYXmrEzhqa/XAPDQBe05/8QA6mdiGLBxHvzyImz+6cD+thfCqf+GJgo1IlLWHyl/8Pzi51m515zjq0FoA27vejuXtb6MILu+rqT89K/FIn/tyOSeGcswDBh6cjNuPCVAevoX58Gf0+H3Vw+s1m0PghOvhn53Q1w7a+sTEb+zMn0lE5ZN4NddvwIQFhTGiE4jGNZhmPrVSKUo3FhgZ2YBN05dTKHbyxltGzL24g61v7f/vi2w+G1YMgUKM819zkjodh30GQkxukYuImWt37eeicsm8v327wEIsgVxRZsruLXLrTQIbWBxdVKbKdzUsJxCNzdO+YM9OUW0S4hkwuButXcuG0+JOTfN4rdg4/f4+tPUa26u/dT1WgiJsrJCEfFDf+/7m9f+eo25W+ZiYGC32bmo5UXc1uU2mkQ2sbo8CQAKNzWoxOPlzg+WsTYlh4aRLt68vieRIbVw8ce9G+HPD2DZe5Cz+8D+lmdAr1vMEVCaUVhE/mFtxlpe++s1vt36rW/fuc3OZWTXkbSMaWlhZRJoFG5q0JOz1jB/3R5Cgu28ObwHjWNCrS6p/AoyYdUnsPwD2LHowP6wBualp+7DIVZ/nETkUMvTlvPmyjeZv30+ADZs9G/Wn391/hdtY9taWpsEJoWbGvLeb1uZ8usWAF64uiudm8RYWk+5FOWal51WfQp/fwueInO/zQ6tzoauQ6DdRRDktLZOEfE7XsPLzzt+5q2Vb7E0bSlghprzWpzHLSfeQut6rS2uUAKZwk0N+HVDOmO/WAXAvee28e8h34XZsOE7WP0ZrP8GSgoO/Cyug7mIZeerITLBshJFxH8VeYr4evPXTF01lQ2ZGwAIsgdxccuLGdFphNaAkhqhcFPNNu3J5bb3l+LxGgzs2oiRZ/rhf61kbjdbaNbNhs0/g9d94Gf1WkCny6HjZRDfCWr7qC4RqRbpBenMWDeDD9d9SEZhBgDhweFc3eZqrutwHXFhcRZXKHWJwk01ysp3c9PUxWQVuOnWNIanr+jsH0O+i/Nh26+w8QdzlFPa6rI/r98a2l0IHS+HxC4KNCJyWIZhsCJ9BdPXTufrLV9T4i0BICE8gcHtBnNVm6uIdEZaXKXURQo31aTE42XktKVsSs+jUXQIrw3tQUiwRSOI3AXmEghbf4UtC2Dbbwf6z4DZhyapN7Q9H9peoMUrReSo8t35zN48mw/XfciajDW+/V0bduW6DtdxdtOzNaOwWEr/+qrJf2evYcGGdMKcDt4Y3pOGkTW0yrdhQNYOM8zsXALbF8GupeApLntcVBNodSa0Osscwh0WWzP1iUitZBgGazLW8Onfn/LVpq/IdecC4LQ7Oa/FeVzT9hpObHiixVWKmBRuqsGHf2zn7V+2APD81V3o0KiaJrLzesw5Z1JXQuoq837nUshLO/TYiARo3g+a9YXmp0KDNrrcJCLHlFmYyazNs/j0709Zt2+db3/TyKZc3fZqLm11KTEhMdYVKHIYfhFuJk6cyLPPPktKSgpdunRhwoQJ9OrV64jHf/TRRzzyyCNs2bKFE044gWeeeYYLLrigBis+ssVbMnjosxUA3NP/BM7rdJwjowwD8vdC5lYzyKT/DXv/hr0bzO2SwkOfYw+C+I7mituNu5uBpl4LhRkRKZciTxE/7/iZrzZ9xU87fsJdOsgg2B5M/6b9GXjCQE5OPBm7rZbOri4Bz/JwM2PGDJKTk5k8eTK9e/dm/PjxDBgwgHXr1hEXd2jv+l9//ZXBgwczbtw4LrroIqZNm8bAgQNZunQpnTp1suATHLArs4Bb31uC22NwfqcE7jrrCH1XDMPsB1OYaU6OV7DPbG3J3X9Lheyd5iimrB1lh2P/U3CYOUQ7vqN5a9QNEk6E4Fo0QaCIWM7j9bAkdQmzN8/mmy3fkOPO8f2sfWx7BrYeyIUtLyTaFW1hlSLlYzMMw7CygN69e9OzZ09efvllALxeL0lJSdx555088MADhxw/aNAg8vLy+Oqrr3z7Tj75ZLp27crkyZMPOb6oqIiiogOdZ7Ozs0lKSiIrK4uoqKq7XLT27295ct4DlHgNguwQHRKEDcMMMoYHvF7z3vBg85SA4S3zfNtB9wffAGwG2IJDsDsjsLkisbkisbuisIVE43BFY7c7sNlsOGwObNgIsgdht9lx2Bzmvd1BkC0Ih92Bw+YgyB5EkD2ozHawPdh3H2wPJthh3jvtzgPbDicuhwun3enbdmiZBZFaq8RbwpLUJXyz5Ru+2/adbwg3QHxYPBe0vIALW1yoWYTFL2RnZxMdHV2u729LW26Ki4tZsmQJo0eP9u2z2+3079+fhQsXHvY5CxcuJDk5ucy+AQMG8Nlnnx32+HHjxvHYY49VWc1H8vPadfwZenCn3dK5YvYnFMf+B0FU/rTnm8O4i1Mh59hH14QgexAuh6vMLTQoFJfDRUhQCCFBIYQ6QgkNDiU06MAtLCjMvA8OIywozLwPDiM8KJzwYPMWGhTqH0PnRQJIYUkhv+3+jR+2/8D87fPLBJpoVzRnNz2bi1peRPf47rrsJLWWpeEmPT0dj8dDfHx8mf3x8fGsXbv2sM9JSUk57PEpKSmHPX706NFlwtD+lpuqdlmfc9g3dyXR4S7iokLN4dU2G4bNDg5n2VtQCASHYgQ5ARtG6WraBgb7G9IMw8D3P+PAvdfw+rY9hgcDc9/BN4/hwWN48HrN7f37SrwllHhLfNsew4Pb6/btd3vd5s3j9u0v9hRT7C2m2FOM2+umyFOE96BWp/3PzXPnVfk5tdvsZthxhhMRHEF4cDgRzggigyPNe2ckkcGR5n3pLcoZZd5c5r3ToaUhRNLy0/hl5y/8sP0HFu5aSKHnQF+9/YHm3Gbn0iuxF8H2WriYr8g/WN7nprq5XC5cruofht2gQVv+c+2hl8UCkS/0eIop9BT67otKisx7TxGFJYUUegopLCmkoKSAgpICCksKyS/JNx+7C3zbee488kvyzXt3Pvkl+b6gluPOKXPtv6JCg0KJdEYS7Yom2hlt3u+/OaOJccUQ44oh2hVNvZB6vm3N0SG1mdvjZvme5SzYuYBfdv5SZpQTmJPsnZl0JmcmnUmPhB4KNBJwLP0L3qBBAxwOB6mpqWX2p6amkpBw+LWLEhISKnS8VL39/XTCgsOq5fUNw/CFnlx3LvnufHLcOeQV55HjziG3OPfAfXGO75ZdnO275RbnYmD4glVa/mGGxx9FpDOS2JBYYlwx1HPVo16IeYsNiTW3Xeb2/schQSHVci5EysPj9bB231p+3/07i3YvYmnaUgoOGohgw0bH+h05tcmpnJl0Ju1i2+mSrwQ0S8ON0+mke/fuzJs3j4EDBwJmh+J58+Zxxx13HPY5ffr0Yd68edxzzz2+fd9++y19+vSpgYqlJthsNl8fnIY0rNRreA2vL/RkFWeRVZRFdlE2WUVZZBVnkVmUaW4XZbGvaJ95X7iP7OJsAN9zt7K1XO8XFhRmhp1QM/DUD6lv3ofWp35IfeqH1vftj3JFqS+DHJdiTzGr9q5iSeoSlqUtY1naMnKKy7ZwxobE0rdRX/o17kffRn2JDdFEnVJ3WN72npyczPDhw+nRowe9evVi/Pjx5OXlMWLECACGDRtG48aNGTduHAB33303p59+Ov/3f//HhRdeyPTp01m8eDGvvfaalR9D/IzdZvddfmpCk3I/r8RbQnZxNpmFmewr2se+wn0H7gv3kVGY4duXUZhBRmEGJd4S8kvyyc/NZ0fujmO+R5AtyBd8YkNjfeHn4PsGoQ2oH1qfGFeMglAdZxgGO3J3sGLPClakr2Bl+kpW711NsbfsrOMRwRH0iO9B78Te9ErsReuY1vq3I3WW5eFm0KBB7NmzhzFjxpCSkkLXrl2ZM2eOr9Pwtm3bsNsP/IL27duXadOm8fDDD/Pggw9ywgkn8Nlnn1k+x40EhiB7kO9yU3kYhkGuO5eMwgz2FuxlX+E+9hbu9T3OKMxgb+Fe33Z2cTYlRglpBWmkFRz7UpnD5iA2JJYGoQ3KBKEGIWb4OXg72hWtL7NazuP1sDVnK2v3rmXtvrWs3buWNRlryCzKPOTY2JBYusd3p1tcN06KO4m2sW3VV0yklOXz3NS0ioyTF6lqbo/bDDulgWdvwaHb6QXp7C3cS1ZRVoVeO8gWRL2Qeoe0Au2/JOZrLSrtJ6ROpNYp8ZawK3cXW7K3sCFzAxv2bWBD5gY2ZW2i6OBFbUsF24NpF9uOTg06cWKDE+ncsDNNI5uq34zUKbVmnhuRuibYEUxCeAIJ4cfuAO/2uMkozCC9MP2IQWhvwV7SC9PJKsqixChhT8Ee9hTsKVctUc6oMp2iy9y76hETcqAzdYwrRp2mK6iwpJBdubvYkbuDnbk72Zmzk605W9mavZXtOdsp8ZYc9nkhjhDaxLahXb12tKvfjvax7WlTr42mNRCpAIUbET8V7AgmPjye+PD4Yx67Pwj5ws8/7g/+WWZRJl7D6xtZtiV7S7nqCXGEEBNyYOh8jCuGaGc0Ua4o332UM8o339D+uYfCg8MD6nJJQUkBmYWZB1rZSs9zWn4aqXmppOSnkJqXyr6ifUd9HZfDRdOoprSObk2rmFa0rtea1jGtaRLRRDN/ixynwPmLI1KHVSQIeQ0vWUVZvg7R+28Hd5jOLDrQoTqzMJMSo4RCTyEpeSmk5B1+wsyjCQ0KJTI4knBnuG8W6rDgMN9M1AffQoJCCHGE+Jb4CAkKOWRZkP3Lhxy8zIjNZsPGgcs0BoZvIsv9t/1zNO2frLLYW+ybi2n/vEv57nxy3bnkufPIKc4h151LVtGBEXaHu2x0JBHBETSOaGzeIhuTFJlE86jmNI9qTnx4vPpIiVQThRuROsZus/vm7WlFq2Mev7/T9P4v98yiTN92dlG2b6h9VlHWIfMO7Z8Jd/98QxxlDdjaJMgeVGZUW/2Q+sSFxZkBM8y8JYQnEOWMUr8YEQso3IjIUdlsNt8lpqTIii1d4va4yXXn+iZezHPn+Waizis5MCP1/lmr94egYk8xRZ4i3+2fS4O4vW7fsiJew4vH6/EtY3Iwu81OkC0Iu91s4QmyBRHsMBeB3b8A7P6WooNbjyKd5hIfEcHmzXcZrvQ+PDhcoUXEjynciEi1CXYEU89hthKJiNQUXfAVERGRgKJwIyIiIgFF4UZEREQCisKNiIiIBBSFGxEREQkoCjciIiISUBRuREREJKAo3IiIiEhAUbgRERGRgKJwIyIiIgFF4UZEREQCisKNiIiIBBSFGxEREQkoCjciIiISUIKsLqCmGYYBQHZ2tsWViIiISHnt/97e/z1+NHUu3OTk5ACQlJRkcSUiIiJSUTk5OURHRx/1GJtRnggUQLxeL7t27SIyMhKbzValr52dnU1SUhLbt28nKiqqSl9bDtB5rhk6zzVD57nm6FzXjOo6z4ZhkJOTQ6NGjbDbj96rps613Njtdpo0aVKt7xEVFaVfnBqg81wzdJ5rhs5zzdG5rhnVcZ6P1WKznzoUi4iISEBRuBEREZGAonBThVwuF2PHjsXlclldSkDTea4ZOs81Q+e55uhc1wx/OM91rkOxiIiIBDa13IiIiEhAUbgRERGRgKJwIyIiIgFF4UZEREQCisJNBU2cOJHmzZsTEhJC7969WbRo0VGP/+ijj2jXrh0hISGceOKJzJ49u4Yqrd0qcp5ff/11Tj31VOrVq0e9evXo37//Mf9/EVNF/z3vN336dGw2GwMHDqzeAgNERc9zZmYmI0eOJDExEZfLRZs2bfS3oxwqep7Hjx9P27ZtCQ0NJSkpiVGjRlFYWFhD1dZOP/30ExdffDGNGjXCZrPx2WefHfM58+fP56STTsLlctG6dWumTJlS7XViSLlNnz7dcDqdxltvvWWsWrXKuPnmm42YmBgjNTX1sMf/8ssvhsPhMP73v/8Zq1evNh5++GEjODjYWLFiRQ1XXrtU9DwPGTLEmDhxorFs2TJjzZo1xvXXX29ER0cbO3bsqOHKa5eKnuf9Nm/ebDRu3Ng49dRTjUsvvbRmiq3FKnqei4qKjB49ehgXXHCBsWDBAmPz5s3G/PnzjeXLl9dw5bVLRc/z+++/b7hcLuP99983Nm/ebMydO9dITEw0Ro0aVcOV1y6zZ882HnroIeOTTz4xAOPTTz896vGbNm0ywsLCjOTkZGP16tXGhAkTDIfDYcyZM6da61S4qYBevXoZI0eO9D32eDxGo0aNjHHjxh32+Kuvvtq48MILy+zr3bu38a9//ata66ztKnqe/6mkpMSIjIw0pk6dWl0lBoTKnOeSkhKjb9++xhtvvGEMHz5c4aYcKnqeJ02aZLRs2dIoLi6uqRIDQkXP88iRI42zzjqrzL7k5GSjX79+1VpnIClPuPnPf/5jdOzYscy+QYMGGQMGDKjGygxDl6XKqbi4mCVLltC/f3/fPrvdTv/+/Vm4cOFhn7Nw4cIyxwMMGDDgiMdL5c7zP+Xn5+N2u4mNja2uMmu9yp7nxx9/nLi4OG688caaKLPWq8x5/uKLL+jTpw8jR44kPj6eTp068dRTT+HxeGqq7FqnMue5b9++LFmyxHfpatOmTcyePZsLLrigRmquK6z6HqxzC2dWVnp6Oh6Ph/j4+DL74+PjWbt27WGfk5KSctjjU1JSqq3O2q4y5/mf7r//fho1anTIL5QcUJnzvGDBAt58802WL19eAxUGhsqc502bNvH9999z7bXXMnv2bDZs2MDtt9+O2+1m7NixNVF2rVOZ8zxkyBDS09M55ZRTMAyDkpISbr31Vh588MGaKLnOONL3YHZ2NgUFBYSGhlbL+6rlRgLK008/zfTp0/n0008JCQmxupyAkZOTw9ChQ3n99ddp0KCB1eUENK/XS1xcHK+99hrdu3dn0KBBPPTQQ0yePNnq0gLK/Pnzeeqpp3jllVdYunQpn3zyCbNmzeKJJ56wujSpAmq5KacGDRrgcDhITU0tsz81NZWEhITDPichIaFCx0vlzvN+zz33HE8//TTfffcdnTt3rs4ya72KnueNGzeyZcsWLr74Yt8+r9cLQFBQEOvWraNVq1bVW3QtVJl/z4mJiQQHB+NwOHz72rdvT0pKCsXFxTidzmqtuTaqzHl+5JFHGDp0KDfddBMAJ554Inl5edxyyy089NBD2O36b/+qcKTvwaioqGprtQG13JSb0+mke/fuzJs3z7fP6/Uyb948+vTpc9jn9OnTp8zxAN9+++0Rj5fKnWeA//3vfzzxxBPMmTOHHj161ESptVpFz3O7du1YsWIFy5cv990uueQSzjzzTJYvX05SUlJNll9rVObfc79+/diwYYMvPAKsX7+exMREBZsjqMx5zs/PPyTA7A+UhpZcrDKWfQ9Wa3flADN9+nTD5XIZU6ZMMVavXm3ccsstRkxMjJGSkmIYhmEMHTrUeOCBB3zH//LLL0ZQUJDx3HPPGWvWrDHGjh2roeDlUNHz/PTTTxtOp9OYOXOmsXv3bt8tJyfHqo9QK1T0PP+TRkuVT0XP87Zt24zIyEjjjjvuMNatW2d89dVXRlxcnPHkk09a9RFqhYqe57FjxxqRkZHGBx98YGzatMn45ptvjFatWhlXX321VR+hVsjJyTGWLVtmLFu2zACM559/3li2bJmxdetWwzAM44EHHjCGDh3qO37/UPD77rvPWLNmjTFx4kQNBfdHEyZMMJo2bWo4nU6jV69exm+//eb72emnn24MHz68zPEffvih0aZNG8PpdBodO3Y0Zs2aVcMV104VOc/NmjUzgENuY8eOrfnCa5mK/ns+mMJN+VX0PP/6669G7969DZfLZbRs2dL473//a5SUlNRw1bVPRc6z2+02Hn30UaNVq1ZGSEiIkZSUZNx+++3Gvn37ar7wWuSHH3447N/b/ed2+PDhxumnn37Ic7p27Wo4nU6jZcuWxttvv13tddoMQ+1vIiIiEjjU50ZEREQCisKNiIiIBBSFGxEREQkoCjciIiISUBRuREREJKAo3IiIiEhAUbgRERGRgKJwIyIiIgFF4UZEaoX58+djs9nIzMy0uhQR8XOaoVhE/NIZZ5xB165dGT9+PADFxcVkZGQQHx+PzWaztjgR8WtBVhcgIlIeTqeThIQEq8sQkVpAl6VExO9cf/31/Pjjj7z44ovYbDZsNhtTpkwpc1lqypQpxMTE8NVXX9G2bVvCwsK48soryc/PZ+rUqTRv3px69epx11134fF4fK9dVFTEvffeS+PGjQkPD6d3797Mnz/fmg8qItVCLTci4ndefPFF1q9fT6dOnXj88ccBWLVq1SHH5efn89JLLzF9+nRycnK4/PLLueyyy4iJiWH27Nls2rSJK664gn79+jFo0CAA7rjjDlavXs306dNp1KgRn376Keeddx4rVqzghBNOqNHPKSLVQ+FGRPxOdHQ0TqeTsLAw36WotWvXHnKc2+1m0qRJtGrVCoArr7ySd999l9TUVCIiIujQoQNnnnkmP/zwA4MGDWLbtm28/fbbbNu2jUaNGgFw7733MmfOHN5++22eeuqpmvuQIlJtFG5EpNYKCwvzBRuA+Ph4mjdvTkRERJl9aWlpAKxYsQKPx0ObNm3KvE5RURH169evmaJFpNop3IhIrRUcHFzmsc1mO+w+r9cLQG5uLg6HgyVLluBwOMocd3AgEpHaTeFGRPyS0+ks0xG4KnTr1g2Px0NaWhqnnnpqlb62iPgPjZYSEb/UvHlzfv/9d7Zs2UJ6erqv9eV4tGnThmuvvZZhw4bxySefsHnzZhYtWsS4ceOYNWtWFVQtIv5A4UZE/NK9996Lw+GgQ4cONGzYkG3btlXJ67799tsMGzaMf//737Rt25aBAwfyxx9/0LRp0yp5fRGxnmYoFhERkYCilhsREREJKAo3IiIiElAUbkRERCSgKNyIiIhIQFG4ERERkYCicCMiIiIBReFGREREAorCjYiIiAQUhRsREREJKAo3IiIiElAUbkRERCSg/D80S+ZmCmJruAAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Sensitivities with respect to the spline values can be computed\n", + "fig, ax = plt.subplots()\n", + "ax.plot(rdata['t'], rdata.sx[:, 0], label=model.getParameterNames()[0])\n", + "ax.plot(rdata['t'], rdata.sx[:, 1], label=model.getParameterNames()[1])\n", + "ax.plot(rdata['t'], rdata.sx[:, 2], label=model.getParameterNames()[2])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"sensitivity\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "tags": [] + }, + "source": [ + "### Specifying derivatives, boundary conditions and extrapolation methods" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When derivatives are not specified in the `CubicHermiteSpline` constructor, they are computed automatically using finite differences and according to the boundary conditions. If their form is known a priori (e.g., they are known constants or functions of parameters), they can be passed explicitly to the spline constructor." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# A simple spline for which finite differencing would give a different result\n", + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=3),\n", + " values_at_nodes=[1.0, -1.0, 1.0],\n", + " derivatives_at_nodes=[10.0, -10.0, -10.0],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABaM0lEQVR4nO3dd3xT5eIG8OckadKdAt2DFkqhlBZaQKBsBAVEpgMRBVHxuq+CCxduvPzcezMURVBAlsjem7ZQtqWli066V9ok5/dH2miV0ZQkJ+P5fj753Nv0pH16QPL0Pe95X0EURRFERERETkgmdQAiIiIiqbAIERERkdNiESIiIiKnxSJERERETotFiIiIiJwWixARERE5LRYhIiIicloKqQPYOr1ejwsXLsDLywuCIEgdh4iIiFpAFEVUVlYiODgYMtnlx31YhK7iwoULCAsLkzoGERERtUJ2djZCQ0Mv+3kWoavw8vICYDiR3t7eEqchIiKilqioqEBYWJjxffxyWISuoulymLe3N4sQERGRnbnatBZOliYiIiKnxSJERERETotFiIiIiJwWixARERE5LRYhIiIiclosQkREROS0WISIiIjIabEIERERkdNiESIiIiKnxSJERERETotFiIiIiJwWixARERE5LW66Shan1elxsboeLnIZPFUKKBXs30REZBtYhMhs6rV6JGWVYk9aMU7nV6Kgog755XUortJAL/51nFIhg5dKgXaeSsSGqBEf5oPuoT7oGuQFlUIu3Q9AREROh0WIrklJdT1Wp+Ri55/F2J9+ETX1ukseJxNgLEP1Wj0uautxsboeZwuqsCIpFwDgIhfQr2M7TIgPwcjYQHiq+NeTiIgsSxBFUbz6Yc6roqICarUa5eXl8Pb2ljqOzfizoBLf7TmPFUk50Gj1xufbeSgxoJMveke0QZDaDYHerghQq+DroYJeFFFdr0OVRouqOi1yy2pwNLscx3LKcDSnHCXV9cav4+oiw40xgZjYMwRDovwgkwlS/JhERGSnWvr+zSJ0FSxCze09V4wvdqRj59ki43OxId64uXswBnbyRUyQd6tKiyiKyCiuxtpjeViVnIv04mrj57oGeeOJEVG4MSYAgsBCREREV8ciZCYsQgYFFXV4fe1JrD2WBwAQBODGmADcN7AjrotoY9aCIooijuWUY2VyLn45koMqjRYA0C3Y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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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wEABwBVvCxYsMH1wC9PR7+e/Yrlpv45e5z179gixsbGCWq0WgoODhbfeektoamoycWrL05Hr3NjYKLz22mtCSEiIYGtrKwQEBAhz584VLl++bPrgFmTbtm1X/Xnbcm1nzpwpjBgx4or3iYyMFFQqlRAcHCysWLGi03PKBIHjb0RERCQdnHNDREREksJyQ0RERJLCckNERESSwnJDREREksJyQ0RERJLCckNERESSwnJDREREksJyQ0RERJLCckNEFiElJQUymQzl5eViRyEiM8cdionILI0cORKRkZFYvHgxAECj0aCsrAxeXl6QyWTihiMis6YUOwARUXuoVCp4e3uLHYOILABvSxGR2bn//vuxfft2fPjhh5DJZJDJZFi5cmWb21IrV66Eq6srfvvtN/Tq1Qv29va4++67UVtbi1WrViEoKAhdunTBk08+Ca1W2/qxGxoa8Nxzz8HPzw8ODg6IjY1FSkqKOF8oEXUKjtwQkdn58MMPcebMGfTt2xdvvPEGAODkyZNXPK+2thYfffQRVq9ejaqqKkyePBmTJk2Cq6srNmzYgMzMTNx1110YMmQIEhISAACPP/44Tp06hdWrV8PX1xc///wzxo0bh+PHj6Nnz54m/TqJqHOw3BCR2XFxcYFKpYK9vX3rrajTp09f8bzGxkZ89tlnCAkJAQDcfffd+Oabb1BYWAhHR0eEh4dj1KhR2LZtGxISEpCbm4sVK1YgNzcXvr6+AIDnnnsOGzduxIoVK/D222+b7oskok7DckNEFsve3r612ACAl5cXgoKC4Ojo2OaxoqIiAMDx48eh1WoRGhra5uM0NDSga9eupglNRJ2O5YaILJaNjU2bP8tksqs+ptPpAADV1dVQKBRIS0uDQqFo87y/FiIismwsN0RkllQqVZuJwMYwYMAAaLVaFBUVYdiwYUb92ERkPrhaiojMUlBQEPbt24fs7GyUlJS0jr7ciNDQUEybNg0zZszA+vXrkZWVhf379yMpKQm///67EVITkTlguSEis/Tcc89BoVAgPDwcHh4eyM3NNcrHXbFiBWbMmIFnn30WvXr1wsSJE3HgwAF069bNKB+fiMTHHYqJiIhIUjhyQ0RERJLCckNERESSwnJDREREksJyQ0RERJLCckNERESSwnJDREREksJyQ0RERJLCckNERESSwnJDREREksJyQ0RERJLCckNERESS8v/klLgONLudJQAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Simulation\n", + "sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model)\n", + "simulate(sbml_model, T=1);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The spline annotation in this case is\n", + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\t\n", + "\t\t\n", + "\t\t\t10\n", + "\t\t\n", + "\t\t\n", + "\t\t\t-10\n", + "\t\t\n", + "\t\t\n", + "\t\t\t-10\n", + "\t\t\n", + "\t\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "The default boundary conditions depend on the extrapolation method (which defaults to no extrapolation). For example, below we have a spline with constant extrapolation." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=3),\n", + " values_at_nodes=[-2, 1, -1],\n", + " extrapolate=(None, 'constant'), # no extrapolation required on the left side\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGwCAYAAABFFQqPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABRMElEQVR4nO3deVxU5eIG8OcMMIMIAyj7IogboiiKqbiUpqVpKm6ZmVtWN8v6lbZom9lmddu7lmXlUpqpuaXmEomWuwjuqIgsAgMiwrAPzJzfHwMUKcrgzJxZnu/nM597Hc+B56TCw3ve876CKIoiiIiIiOyQTOoARERERFJhESIiIiK7xSJEREREdotFiIiIiOwWixARERHZLRYhIiIislssQkRERGS3HKUOYOl0Oh2ys7Ph5uYGQRCkjkNERESNIIoiiouLERAQAJms4XEfFqFbyM7ORnBwsNQxiIiIqAkyMzMRFBTU4O+zCN2Cm5sbAP1/SKVSKXEaIiIiagy1Wo3g4OC67+MNYRG6hdrbYUqlkkWIiIjIytxqWgsnSxMREZHdYhEiIiIiu8UiRERERHaLRYiIiIjsFosQERER2S0WISIiIrJbLEJERERkt1iEiIiIyG6xCBEREZHdYhEiIiIiu8UtNohsiU4LpO8HSnIBV18gpA8gc5A6FRGRxbKqEaG9e/dixIgRCAgIgCAI2Lhx4y3PiY+PR/fu3aFQKNC2bVssW7bM5DmJJHFmM/BpZ2D5/cAvM/T/+2ln/ftERHRDVlWESktL0bVrVyxatKhRx1+6dAnDhw/HwIEDkZSUhGeffRaPPvooduzYYeKkRGZ2ZjOwZgqgzq7/vjoH4popKEr4BRVVWmmyERFZMEEURVHqEE0hCAI2bNiA2NjYBo956aWXsHXrVpw6daruvQcffBCFhYXYvn17oz6PWq2Gu7s7ioqKuPs8WSadVj/y8+8SVPvbIqBCS/Sr/AzOcie0aC5Hi+ZyRAa6I7ZbIKJbeUImu/nuzERE1qax379teo7QgQMHMHjw4HrvDRkyBM8++2yD51RWVqKysrLu12q12lTxiIwjfX+DJQgAZAIQgKvoKUvGQU0EyjTluHytHCcuF2HloQwEeTbDqKgAjO4WiLY+bmYMTkQkPZsuQiqVCr6+vvXe8/X1hVqtRnl5OZo1a3bdOQsXLsSCBQvMFZHotmVkpKJVI477fmww8kIGoKBMgzx1BeLO5uG3UypcvlaORbsvYtHuixjdLRAvD+sIbzeFyXMTEVkCq5ojZA7z5s1DUVFR3SszM1PqSEQ3VFCqwZMrE/DijrxGHe/SIhChXs3RvZUnhnb2x3/Hd8XRVwfjfw91w+COPhAEYENiFgZ9FI+Vh9Kh01nlXXMiIoPY9IiQn58fcnNz672Xm5sLpVJ5w9EgAFAoFFAo+NMwWbbdyXl4Yd0J5JdUwknWEYWO3nCvzoeAG5UXAVAG6B+l/xdnJwfc3yUA93cJwInLhXh5w0mcylLjlQ2nsC7hMt4dHYmO/pwbR0S2y6ZHhGJiYhAXF1fvvV27diEmJkaiRES3p0xTjVc2nMT0ZUeQX1KJdj6u2PBUf3iM+Rj66c7/nvRc8+uh791yPaEuQR7Y9FQ/zB8RAVeFIxIzCjFq0T5sOdHw/CMiImtnVUWopKQESUlJSEpKAqB/PD4pKQkZGRkA9Le1pkyZUnf8E088gdTUVLz44otITk7Gl19+iTVr1uC5556TIj7RbbmQW4z7P/8LKw/p/74/0rc1fn26HzoHugMRI4EHVgBK//onKQP070eMbNTncJAJmN63NX6ffRcGdPCGplqHWasS8WV8Cqz0AVMiopuyqsfn4+PjMXDgwOvenzp1KpYtW4Zp06YhLS0N8fHx9c557rnncObMGQQFBeG1117DtGnTGv05+fg8WYKjaQWYsfwoisqr4Kd0xofju6JfO6/rDzTiytJanYi3t57B0n1pAIAH7wjGW7Gd4eRgVT8/EZGdauz3b6sqQlJgESKp7TqTi1mrjqGyWodurTzw3dQ70KK53Gyff9m+S3hzyxnoRKBfWy98+XB3KJ2dzPb5iYiaorHfv/mjHZEFW304A//54Sgqq3UYFO6DVY/2NmsJAoBpfVtjyZQecJE74K+UfDyy9AjKNVylmohsA4sQkYVatDsFc9efhE4EHugRhK8nR6OZXJoNVAd19MWa/8RA6eyIo+nXMHNlAqq0OkmyEBEZE4sQkQVauu8S/rvjHABg1sC2eH9sFzhKPDenc6A7lk6/A85OMsSfu4Ln1x7nWkNEZPVYhIgszKakLCz49QwAYPY97fH8kA4QBMvYCyw6pAUWPxwNR5mATUnZWPDraT5NRkRWjUWIyILsOX8Fc9YcBwBMjQnB03e3lTjR9QZ08MFHD3SFIADLD6Tj098vSB2JiKjJWISILERSZiFm/piAap2IEV0DMH9EJ4sZCfq3UVGBeHNkJwDAZ3EXuOgiEVktFiEiC5B6pQTTlx5GmUaL/u288NH4rpDJLLME1ZocE4on7moDAHhx3QlcyC2WOBERkeFYhIgkVlJZjcd/SMC1sip0DXLHVw9HQ+5oHf80n7+3Pfq0aYkyjRZP/JiAkspqqSMRERnEOr7aEtkoURTx0roTSMkrga9SgW+n3gFXhfXshezoIMPnE7vBT+mMi1dK8dK6E5w8TURWhUWISELf/XUJW0/mwMlBwJeTouHtppA6ksG8XBVYNKk7nBwEbD2Zg+/+uiR1JCKiRmMRIpLIodSrWPhbMgDg1eERiA7xlDhR00WHeOLV4REAgIW/JePwpQKJExERNQ6LEJEEctUVeGpVIrQ6EbFRAZgSEyJ1pNs2JSYEo6ICoNWJeO7nJBRXVEkdiYjolliEiMysWqvDUyuPIb+kEuF+bnh3TKTFPiZvCEEQ8O7oSLRq4YKswnK8veWs1JGIiG6JRYjIzL6Kv4ij6dfgpnDE4oej4SK3nsnRt9Jc4Yj/jusCQQB+PpqJ3cl5UkciIropFiEiMzqVVYTP4vQrMb8Z2wmhXs0lTmR8vcJa4pG+rQEAL/1yAoVlGokTERE1jEWIyEwqqrR47uckVOtE3NfZD7FRgVJHMpkXhnRAmHdz5BVXYv7m01LHISJqEIsQkZl8vOs8LuSVwMtVgbdjO9vEvKCGODs54OMHoiATgE1J2dh2MkfqSEREN8QiRGQGh1KvYsmfqQCA98ZEoqWr9a0XZKioYA88OUC/aeyrG0/hakmlxImIiK7HIkRkYiWV1Ziz9jhEEZjQIxiDI3yljmQ2zwxqh3A/NxSUavBezZpJRESWhEWIyMQWbjuLy9fKEeTZDK/e31HqOGYld5ThndGRAIC1CZdxJI0LLRKRZWERIjKhYxnXsOpwBgDgg3Fd4ObsJHEi84sO8cSDdwQDAF7dcApVWp3EiYiI/sYiRGQi1VodXl5/EqIIjIsOQp82XlJHksxLQ8Ph6eKEc7nFWL4/Teo4RER1WISITGTZ/jQkq4rh4eKEefeFSx1HUp7N5Zhb89/gk13nkVNULnEiIiI9FiEiE8guLMfHu84DAObdF24XT4ndyvjoYHRv5YFSjZbbbxCRxWARIjKBBb+eRplGix4hnhgfHSx1HIsgkwl4OzYSMgHYejIHe85fkToSERGLEJGx/X4mFztO58JRJuCd0ZGQyWx34URDRQQoMa2PfvuNBZtPc+I0EUmORYjIiMo12rotJR7tH4YOfm4SJ7I8z93TDi2by5GaX4pVhzKkjkNEdo5FiMiIvtmbiqzCcgR6NMMzg9pKHcciuTk74dl72gMAPv39PIrKqyRORET2jEWIyEhy1RVYvOciAGDesHC4yB0lTmS5Jt4RjLY+rrhWVoUvd6dIHYeI7BiLEJGR/HfHOZRXaREd4onhkf5Sx7Fojg4yvDxM/zj90n1pyCwokzgREdkrFiEiIziVVYRfjl0GALw6vKNN7yxvLAM7+KBv25bQaHV4fzv3ISMiabAIEd0mURTx1pYzEEVgVFQAurXylDqSVRAEAa8Mi4AgAFtO5OBYxjWpIxGRHWIRIrpNO8/k4tClAigcZXhxqH2vIG2oiAAlxnUPAgC8veUMRFGUOBER2RsWIaLboKnWYeE2/SrJj/UPQ6BHM4kTWZ8593ZAMycHHMsoxPZTKqnjEJGdYREiug0rDqQh7WoZvN0UeGJAG6njWCU/d2c82l+/yOJHu85Dq+OoEBGZD4sQURMVV1ThfzWPfs+5pz1cFXxcvqke7R8GpbMjUvJKsCkpS+o4RGRHWISImujbPy+hsKwKbbybY1x0kNRxrJp7M6e6EbVPf7/ArTeIyGxYhIiaoKBUg2//TAUAzL6nAxwd+E/pdk3rEwovVwUyCsqw5mim1HGIyE7wqzdREyzecxGlGi06BShxX2c/qePYBBe5I54aqB8V+iIuBRVVWokTEZE9YBEiMpCqqALL96cBAJ4f0oG7yxvRQ71aIcDdGSp1BX48mC51HCKyAyxCRAb64o8LqKzW4Y5QTwxo7y11HJuicHTA/w1uBwD4Mv4iSiqrJU5ERLaORYjIABlXy/DzEf38lefv7cCtNExgbPcgtPZqjoJSDZb+dUnqOERk41iEiAzw6e/nUa0TcWd7b/QKayl1HJvk6CDDszWjQt/8mQp1RZXEiYjIlrEIETXShdxibKhZ4+aFeztInMa2jegSgHY+riiuqMbyfWlSxyEiG8YiRNRI/9udAlEEhnTyRWSQu9RxbJpMJmDW3W0BAN/tu8S5QkRkMlZXhBYtWoTQ0FA4OzujV69eOHz4cIPHLlu2DIIg1Hs5OzubMS3Zikv5pfj1eDYA4Om720mcxj7c3yUAYV7NUVhWhR8O8AkyIjINqypCP//8M2bPno358+fj2LFj6Nq1K4YMGYK8vLwGz1EqlcjJyal7pafzCyoZbtHuFOhEYFC4DzoHcjTIHBz+MSq05M9UlGk4KkRExmdVRejjjz/GY489hunTpyMiIgKLFy+Gi4sLvv/++wbPEQQBfn5+dS9fX18zJiZbkFlQhg2J+rlBTw/iaJA5jewagJCWLigo1WDlwQyp4xCRDbKaIqTRaJCQkIDBgwfXvSeTyTB48GAcOHCgwfNKSkoQEhKC4OBgjBo1CqdPn77p56msrIRara73Ivv2ZfxFaHUi+rfzQlSwh9Rx7IqjgwxPDdCPCn29NxXlGq42TUTGZTVFKD8/H1qt9roRHV9fX6hUqhue06FDB3z//ffYtGkTfvzxR+h0OvTp0weXL19u8PMsXLgQ7u7uda/g4GCjXgdZl+zCcqxL0K8b9AxHgyQxunsgAj2aIb+kEj8d5qgQERmX1RShpoiJicGUKVMQFRWFu+66C+vXr4e3tze+/vrrBs+ZN28eioqK6l6Zmdz80Z59veciqrQieoe1wB2hLaSOY5ecHGR4aqB+VGjxnovcg4yIjMpqipCXlxccHByQm5tb7/3c3Fz4+TVu00snJyd069YNKSkpDR6jUCigVCrrvcg+5akr8FPNKtLP8EkxSY2NDkSAuzPyiiuxljvTE5ERWU0RksvliI6ORlxcXN17Op0OcXFxiImJadTH0Gq1OHnyJPz9/U0Vk2zIN3tToanWITrEEzFtuIq0lBSODvjPXfqd6b/em4pqrU7iRERkK6ymCAHA7NmzsWTJEixfvhxnz57FzJkzUVpaiunTpwMApkyZgnnz5tUd/+abb2Lnzp1ITU3FsWPH8PDDDyM9PR2PPvqoVJdAVqKwTINVNfNRZt3dlnuKWYAHegSjZXM5Ll8rx9aTOVLHISIb4Sh1AENMmDABV65cweuvvw6VSoWoqChs3769bgJ1RkYGZLK/u921a9fw2GOPQaVSwdPTE9HR0di/fz8iIiKkugSyEj8eTEeZRouO/kruMG8hmskdML1vKD7ceR5fxV/EyK4BLKhEdNsEURRFqUNYMrVaDXd3dxQVFXG+kJ2oqNKi3/t/IL9Eg88ejMKoqECpI1GNorIq9HkvDqUaLb6f1gN3h3NdMCK6scZ+/7aqW2NE5vDLscvIL9Eg0KMZhkVyPpklcXdxwqTeIQCAr+IvSpyGiGwBixDRP2h1IpbsTQUAPNq/NZwc+E/E0szo1xpyBxmOpF3DkbQCqeMQkZXjV3mif9h5WoW0q2XwcHHChDu4mKYl8lU6Y2y0/nblYo4KEdFtYhEiqiGKIhbv0X9jndw7BC5yq3qWwK48fmcbCAIQl5yHZBW3wSGipmMRIqpx6FIBjl8ugtxRhql9QqWOQzfR2qs5hnXWz9/iqBAR3Q4WIaIaX9eMBo2PDoKXq0LiNHQrMwfoF1j89UQOLl8rkzgNEVkrFiEiAOdUxdh97goEAXisf5jUcagROge6o2/bltDqRCzdlyZ1HCKyUixCRAC++0v/pNjQTn4I9WoucRpqrNrSuvpwBorKqyROQ0TWiEWI7F5+SSU2JmUD0D8yT9bjrvbe6ODrhlKNFqsOZUgdh4isEIsQ2b1VhzKgqdaha5A7urfylDoOGUAQBDx2p35UaOm+S9BUczNWIjIMixDZtcpqLX44mA4AeKRfa+5dZYVGdg2Ar1KBvOJKbD6eLXUcIrIyLEJk17aeyMGV4kr4KhW4rzO307BGckcZpvXR39JcsjcV3D6RiAzBIkR2SxRFfPfXJQDAlJhQyB35z8FaPdSrFZrLHXAutxh7L+RLHYeIrAi/8pPdOnypAKez1VA4yjCxZyup49BtcG/mhAdr/gy/2csFFomo8ViEyG59v08/GjSmexBaNJdLnIZu1/S+oXCQCdiXchWnsoqkjkNEVoJFiOxSxtUy7DyTCwB4pG+otGHIKII8XTA8Uj/Pq/aWJxHRrbAIkV1afiANogj0b+eFdr5uUschI6ldB+rX49nIVVdInIaIrAGLENmd0spqrDmSCUD/yDzZji5BHrgj1BPVOhErDqRJHYeIrACLENmdDYlZKK6sRmhLF9zVzlvqOGRkM/rpF1hceSgD5RqtxGmIyNKxCJFdEUURPxzQL6A4OSYUMhkXULQ190T4olULFxSWVeGXY5eljkNEFo5FiOzKoUsFOJdbjGZODhgXHSR1HDIBB5mAaX1CAeifDNTpuMAiETWMRYjsSu1oUGy3ALg3c5I4DZnKA3cEw03hiNQrpdhz/orUcYjIgrEIkd3IVVdgx2kVAGBy71Bpw5BJuSocMeGOYAB8lJ6Ibo5FiOzGqkMZqNaJuCPUExEBSqnjkIlN6xsKmQD8lZKPZJVa6jhEZKFYhMguaKp1WHU4A4B+kjTZviBPl7qNdL/nqBARNYBFiOzCjtMqXCmuhLebAkM7+Ukdh8ykdp2ojYnZyC+plDgNEVkiFiGyC7WTpCf2bMVd5u1IdIgnooI9oNHqsOpQhtRxiMgC8TsC2bxklRqH0wrgIBPwEHeZtzvTa/aS++FgOjTVOmnDEJHFYREim1c7GjSkky/83J0lTkPmNizSH75KBa4UV2LryWyp4xCRhWERIptWWlmNTUn6b34P9w6ROA1JwclBhsk1f/ZL96VBFLnAIhH9jUWIbNrm49koqaxGmFdzxIS1lDoOSWRiz1ZQOMpw4nIREtKvSR2HiCwIixDZtJWH/p4kLQjcV8xetXRVIDYqEIB+VIiIqBaLENmsE5cLcSpLDbmjDGO5r5jdm94vFACw/bQKWYXl0oYhIovBIkQ2q/Zx6WGd/dCiuVziNCS1cD8lYsJaQqsTseJAmtRxiMhCsAiRTVJXVNVNkn6oFydJk17tAourD2eiTFMtcRoisgQsQmSTNiVmobxKi3Y+rrgj1FPqOGQh7g73QasWLigqr8KGxCyp4xCRBWARIpsjiiJW1twWe6gXJ0nT3xxkAqb2CQUALOOj9EQEFiGyQccyCpGsKobCUYYx3ThJmuob3yMIzeUOuJBXgn0pV6WOQ0QSYxEim1M7SXpE1wC4uzhJnIYsjdLZCeNqniJctp+70hPZOxYhsilF5VXYcqJ2kjT3FaMbm1JzeywuOQ/pV0ulDUNEkmIRIpuyOSkLldU6hPu5oVuwh9RxyEK18XbFgA7eEEVg+f50qeMQkYRYhMimrD6SCQCYcEcwJ0nTTU2rGRVaezQTJZV8lJ7IXrEIkc04ebkIp7P1K0mP7hYodRyycHe280aYV3MUV1bjl4TLUschIomwCJHNWH1EP0n6vs5+8HDhStJ0czKZgGl9QwEAy/enQafjo/RE9ohFiGxCmaYam2tWkp5wR7DEachajOkeBDeFI1LzS7H3whWp4xCRBKyuCC1atAihoaFwdnZGr169cPjw4Zsev3btWoSHh8PZ2RmRkZHYtm2bmZKSOW07qUJxZTVCWrqgd+uWUschK+GqcMT4HvrizF3pieyTVRWhn3/+GbNnz8b8+fNx7NgxdO3aFUOGDEFeXt4Nj9+/fz8mTpyIGTNmIDExEbGxsYiNjcWpU6fMnJxM7eea22IP9AiGTMZJ0tR4U/uEQBCAPeev4OKVEqnjEJGZWVUR+vjjj/HYY49h+vTpiIiIwOLFi+Hi4oLvv//+hsd/9tlnGDp0KF544QV07NgRb731Frp3747//e9/Zk5OppSSV4IjadfgIBPqFsojaqyQls0xKNwHALBif5q0YYjI7KymCGk0GiQkJGDw4MF178lkMgwePBgHDhy44TkHDhyodzwADBkypMHjAaCyshJqtbreiyzbmqP6R+YHdvCBr9JZ4jRkjab10e9Kvy7hMtQVVRKnISJzspoilJ+fD61WC19f33rv+/r6QqVS3fAclUpl0PEAsHDhQri7u9e9goM58daSaap1dY8+P8hJ0tREfdu2RFsfV5RqtFh3lI/SE9kTqylC5jJv3jwUFRXVvTIzM6WORDcRdzYXV0s18HFTYEAHb6njkJUSBKFugcXlB/goPZE9sZoi5OXlBQcHB+Tm5tZ7Pzc3F35+fjc8x8/Pz6DjAUChUECpVNZ7keX6uea22LjoIDg6WM1fZ7JAY7oHQunsiPSrZdh97sYPYBCR7bGa7xxyuRzR0dGIi4ure0+n0yEuLg4xMTE3PCcmJqbe8QCwa9euBo8n66IqqsDe8/q1X2ofgSZqKhe5Ix7sqd+odxknTRPZDaspQgAwe/ZsLFmyBMuXL8fZs2cxc+ZMlJaWYvr06QCAKVOmYN68eXXH/9///R+2b9+Ojz76CMnJyXjjjTdw9OhRzJo1S6pLICNan3gZOhHoGdoCrb2aSx2HbMDk3iGQCcCfF/JxIbdY6jhEZAZWVYQmTJiADz/8EK+//jqioqKQlJSE7du3102IzsjIQE5OTt3xffr0wapVq/DNN9+ga9euWLduHTZu3IjOnTtLdQlkJKIoYm3NpNZxPfjIPBlHcAsXDO6o/3qy/ECatGGIyCwEURQ5K/Am1Go13N3dUVRUxPlCFuRoWgHGLT4AF7kDjrwyGM0VjlJHIhux/2I+HlpyCM2cHHDw5UFwb+YkdSQiaoLGfv+2qhEholq1o0HDIv1ZgsioYsJaItzPDeVVWqw5wqdGiWwdixBZnTJNNbac0G+w+gAnSZOR/ftRei0fpSeyaSxCZHW2nVShVKNFaEsX3BHqKXUcskGjogLh4eKEy9fK8fvZ3FufQERWi0WIrM7af6wdJAjcYJWMr5ncARNrHqVfuu+SxGmIyJRYhMiqpF8txaFLBRAEYCw3WCUTmtw7BA4yAQdTC3A2h3sOEtkqFiGyKutq9hXr384b/u7NJE5DtizAoxmGdtavQs9RISLbxSJEVkOrE+s2WB3P0SAyg0f6hgIANiZl42pJpbRhiMgkWITIauy/mI/sogoonR1xT4Sv1HHIDnRv5YkuQe7QVOvw0+EMqeMQkQk0qQhdvHgRr776KiZOnIi8PP3mhL/99htOnz5t1HBE/1Q7GjQyKgDOTg4SpyF7IAgCpteMCv1wMB1VWp20gYjI6AwuQnv27EFkZCQOHTqE9evXo6SkBABw/PhxzJ8/3+gBiQCguKIK20+rAABju/O2GJnP8MgAeLspkKuuxLaTObc+gYisisFFaO7cuXj77bexa9cuyOXyuvfvvvtuHDx40KjhiGr9dlKFiiodwrybIyrYQ+o4ZEfkjjI83CsEALB0X5q0YYjI6AwuQidPnsTo0aOve9/Hxwf5+flGCUX0b+uO6W+Lje3OtYPI/B7q1QpyBxmSMgtxLOOa1HGIyIgMLkIeHh71dnivlZiYiMDAQKOEIvqnzIIyHK5ZO2hMd/4dI/PzdlNgZFQAAOD7v/goPZEtMbgIPfjgg3jppZegUqkgCAJ0Oh327duH559/HlOmTDFFRrJzv9SMBvVt48W1g0gyj/RtDQD47ZQK2YXlEqchImMxuAi9++67CA8PR3BwMEpKShAREYE777wTffr0wauvvmqKjGTHRFHE+mNZAICx0RwNIulEBCgRE9YSWp2I5QfSpI5DREZicBGSy+VYsmQJLl68iC1btuDHH39EcnIyfvjhBzg48JFmMq4jadeQUVCG5nIHDOnkJ3UcsnOP9NOPCv10KAOlldUSpyEiY3Bs6omtWrVCq1atjJmF6Dq1awcNi/SHi7zJf12JjGJQuA9CW7og7WoZfjl2GVNiQqWORES3yeDvLI888shNf//7779vchiifyrXaLG1Zt0WbrBKlkAmEzC9b2vM33waS/el4eFeIZDJ+BQjkTUz+NbYtWvX6r3y8vLwxx9/YP369SgsLDRBRLJXO06rUFJZjSDPZugZ2kLqOEQAgHHRQXBzdsSl/FLsPpcndRwiuk0Gjwht2LDhuvd0Oh1mzpyJNm3aGCUUEfD302Jjugfxp26yGM0VjpjYsxW+2ZuK7/66hEEdue8dkTUzyqarMpkMs2fPxieffGKMD0eEXHUF9qXoF+gcy7WDyMJMiQmBTAD2X7yKM9lqqeMQ0W0w2u7zFy9eRHU1n6Ig49iUlAWdCESHeCKkZXOp4xDVE+Tpgvs6+wMAvt/HBRaJrJnBt8Zmz55d79eiKCInJwdbt27F1KlTjRaM7Fvt2kFcSZos1SP9WmPryRxsTsrGi0M7wMfNWepIRNQEBhehxMTEer+WyWTw9vbGRx99dMsnyoga40y2GsmqYsgdZLg/MkDqOEQ3FB3iiW6tPJCYUYgV+9Px/JAOUkcioiYwuAjt3r3bFDmI6mxI1E+SvjvcB+4uThKnIWrY4/3DMHPlMfx4KB1PDmzDta6IrJDR5ggRGYNWJ2JTUjYA3hYjy3dvJz+0auGCwrIqrKtZ/JOIrEujfnzp1q0bBKFxjy8fO3bstgKRfduXko+84kp4ujhhQAcfqeMQ3ZSDTMCMfvoFFr/76xIm9QqBA5d6ILIqjSpCsbGxJo5BpLchUT9J+v4uAZA7csCSLN/4HkH4eNd5pF8tw64zuRjamXviEVmTRhWh+fPnmzoHEUorq7H9lAoAb4uR9XCRO+Lh3q2waPdFfPtnKosQkZXhj9xkMbafUqG8SovWXs0RFewhdRyiRpsaEwq5gwxH06/hWMY1qeMQkQEMLkJarRYffvghevbsCT8/P7Ro0aLei6ipam+Lje4W2Og5aUSWwEfpjFFR+qUevv0zVeI0RGQIg4vQggUL8PHHH2PChAkoKirC7NmzMWbMGMhkMrzxxhsmiEj2QFVUgX0X9VtqjO7G22JkfR7tHwZAP7KZcbVM4jRE1FgGF6GVK1diyZIlmDNnDhwdHTFx4kR8++23eP3113Hw4EFTZCQ7sPl4FkQRuCPUE8EtXKSOQ2SwDn5uuKu9N3Qi8O1fHBUishYGFyGVSoXIyEgAgKurK4qKigAA999/P7Zu3WrcdGQ3NiTq1w4a3S1I4iRETfefO/WjQmuOZuJqSaXEaYioMQwuQkFBQcjJyQEAtGnTBjt37gQAHDlyBAqFwrjpyC4kq9Q4m6OG3EGG4ZH+UscharKYNi3RNcgdFVU6LN+fJnUcImoEg4vQ6NGjERcXBwB4+umn8dprr6Fdu3aYMmUK9xqjJtlYMxo0MNybW2qQVRMEAU/c1QYAsPxAOkorqyVORES3YvDGOO+9917d/58wYQJCQkKwf/9+tGvXDiNGjDBqOLJ9Op2ITUl/Py1GZO3u7eSH1l7NcSm/FD8dzqibRE1ElsngEaGKiop6v+7duzdmz57NEkRNcuhSAXKKKqB0duSWGmQTHGQCHq+ZK/TdX5egqdZJnIiIbsbgIuTj44OpU6di165d0On4D5xuz8aatYOGRfrD2clB4jRExjG6WyC83RTIKarA5uPZUschopswuAgtX74cZWVlGDVqFAIDA/Hss8/i6NGjpshGNq6iSottJ/UT72N5W4xsiLOTAx7p2xoAsHjPReh0osSJiKghTZosvXbtWuTm5uLdd9/FmTNn0Lt3b7Rv3x5vvvmmKTKSjfojOQ/FldUIcHdGz1CuSk62ZVLvVnBTOCIlrwRxyXlSxyGiBjR5rzE3NzdMnz4dO3fuxIkTJ9C8eXMsWLDAmNnIxtVuqTGqWyBkMm6pQbZF6eyESb1DAABfxadAFDkqRGSJmlyEKioqsGbNGsTGxqJ79+4oKCjACy+8YMxsZMOulWoQf07/UzKfFiNb9UjfUMgdZTiWUYiDqQVSxyGiGzC4CO3YsQNTp06Fr68vZs6cCV9fX+zcuRPp6en1Hq0nupmtJ3NQpRUR4a9Ee183qeMQmYSP0hkTegQDAL7444LEaYjoRpo0R6i8vBwrVqyASqXC119/jTvvvNMU2ciGbUzk2kFkH/5zVxgcZQL2X7yKhHSOChFZGoOLUG5uLtasWYNRo0bBycl8qwAXFBRg0qRJUCqV8PDwwIwZM1BSUnLTcwYMGABBEOq9nnjiCTMlpoZkFpThaPo1CAIwomuA1HGITCrI0wVju+v30PvijxSJ0xDRvxlchNzcpLmNMWnSJJw+fRq7du3Cli1bsHfvXjz++OO3PO+xxx5DTk5O3euDDz4wQ1q6mdp1VWLCWsLP3VniNESmN3NAG8gEIP7cFZy8XCR1HCL6hyZPljans2fPYvv27fj222/Rq1cv9OvXD1988QVWr16N7OybL1bm4uICPz+/updSqbzp8ZWVlVCr1fVeZDyiKNbdFouN4m0xsg+hXs0xqubvO+cKEVkWqyhCBw4cgIeHB3r06FH33uDBgyGTyXDo0KGbnrty5Up4eXmhc+fOmDdvHsrKym56/MKFC+Hu7l73Cg4ONso1kN6ZHDUu5JVA7ijD0Eg/qeMQmc2TA9pAEICdZ3KRrOIPWESWwiqKkEqlgo9P/X2oHB0d0aJFC6hUqgbPe+ihh/Djjz9i9+7dmDdvHn744Qc8/PDDN/1c8+bNQ1FRUd0rMzPTKNdAepuS9CN4g8J9oHTmTvNkP9r5uuG+zvryv2j3RYnTEFGtJhehlJQU7NixA+Xl5QDQpMXC5s6de91k5n+/kpOTmxoRjz/+OIYMGYLIyEhMmjQJK1aswIYNG3DxYsNfhBQKBZRKZb0XGYdWJ2JzTREaxdtiZIdmDWwHANhyIhsXr9z8YQ8iMg9HQ0+4evUqJkyYgD/++AOCIODChQsICwvDjBkz4OnpiY8++qjRH2vOnDmYNm3aTY8JCwuDn58f8vLqL1FfXV2NgoIC+Pk1/vZKr169AOhLXJs2bRp9HhnHoUtXoVJXwM3ZEQM6eEsdh8jsIgKUGNzRB7+fzcP//kjBJxOipI5EZPcMHhF67rnn4OjoiIyMDLi4uNS9P2HCBGzfvt2gj+Xt7Y3w8PCbvuRyOWJiYlBYWIiEhIS6c//44w/odLq6ctMYSUlJAAB/f3+DcpJxbErUjwYN68yd5sl+/d+g9gCAjUlZuJBbLHEaIjK4CO3cuRPvv/8+goKC6r3frl07pKenGy3YP3Xs2BFDhw7FY489hsOHD2Pfvn2YNWsWHnzwQQQE6NehycrKQnh4OA4fPgwAuHjxIt566y0kJCQgLS0NmzdvxpQpU3DnnXeiS5cuJslJDauo0mLbKf1O86O6ce0gsl+RQe64N8IXogh8+jufICOSmsFFqLS0tN5IUK2CggIoFAqjhLqRlStXIjw8HIMGDcKwYcPQr18/fPPNN3W/X1VVhXPnztU9FSaXy/H777/j3nvvRXh4OObMmYOxY8fi119/NVlGalj8uSsorqiGn9IZvVu3lDoOkaSeu0c/KrT1ZA7OZPMJMiIpGTxHqH///lixYgXeeustAIAgCNDpdPjggw8wcOBAowes1aJFC6xatarB3w8NDa03YTs4OBh79uwxWR4yzKYk/dpBI6MCuNM82b2O/krc38UfW07k4ONd5/Ht1B63PomITMLgIvTBBx9g0KBBOHr0KDQaDV588UWcPn0aBQUF2LdvnykykpVTV1QhLlk/2X1UFG+LEQHAs4PbY9vJHPx+NhfHMwvRNdhD6khEdsngW2OdO3fG+fPn0a9fP4waNQqlpaUYM2YMEhMT+SQW3dD2kypoqnVo5+OKCH8uR0AEAG19XBFbs+nwx7vOS5yGyH4ZPCIEAO7u7njllVeMnYVs1Kbj+ttio6ICIAi8LUZU6/8GtcOmpGzsOX8FR9MK0CO0hdSRiOxOk4pQYWEhDh8+jLy8POh0unq/N2XKFKMEI9uQp67A/otXAXARRaJ/C2nZHOOjg7D6SCY+3HkOqx+PkToSkd0xuAj9+uuvmDRpEkpKSqBUKuv9hC8IAosQ1bP5eDZEEYgO8URwi+ufNiSyd08Paof1x7JwMLUAe89fwZ3tudgokTkZPEdozpw5eOSRR1BSUoLCwkJcu3at7lVQUGCKjGTFNh+v3VKDk6SJbiTQoxke7h0CAHh321lodYZvV0RETWdwEcrKysIzzzxzw7WEiP4p9UoJTlwugoNMwLBIruZN1JCn724LN2dHJKuKsSExS+o4RHbF4CI0ZMgQHD161BRZyMbUjgb1a+sFL1fTLbZJZO08m8sxa2BbAMBHO8+hokorcSIi+2HwHKHhw4fjhRdewJkzZxAZGQknJ6d6vz9y5EijhSPrJYoiNtXsNB/LLTWIbmlqn1CsOJCOrMJyfL/vEp4c0FbqSER2QRD/uRxzI8hkDQ8iCYIArda2fpJRq9Vwd3dHUVERlEqugdNYJy4XYuT/9sHZSYajr94DV0WTHlAksisbEi/juZ+Pw03hiPgXBqAlR1KJmqyx378NvjWm0+kafNlaCaKmqx0NGtzRlyWIqJFGdQ1EpwAliiur8cUfKVLHIbILBhcholvR6kT8WjM/KJZrBxE1mkwm4OVhHQEAPx5MR1p+qcSJiGxfo35U//zzz/H444/D2dkZn3/++U2PfeaZZ4wSjKzXwdSryCuuhHszJ66JQmSgvm29MKCDN+LPXcHC387i68nckJXIlBpVhD755BNMmjQJzs7O+OSTTxo8ThAEFiGq22l+WKQ/5I4cdCQy1MvDOuLPC/nYcTqXiywSmVijitClS5du+P+J/q2iSovfTqkAcBFFoqZq7+uGaX1C8d1fl/DG5tP47dn+UDg6SB2LyCbxx3UyqvhzV1BcUQ1/d2f05AaSRE327OB28HJVIDW/FN/9xR9AiUylUSNCs2fPbvQH/Pjjj5schqzf5pqd5kd2DYBMxp3miZrKzdkJLw8Lx+w1x/FFXApiowIR4NFM6lhENqdRRSgxMbFRH+yfG7CS/SmuqMLvZ/MAACN5W4zoto3uFoifDmfgSNo1vLP1LBZN6i51JCKb06gitHv3blPnIBuw43QuNNU6tPFujgh/Lj5JdLsEQcCCkZ1x/xd/YuvJHDyUko++bb2kjkVkU25rjlBmZiYyMzONlYWsXO3TYqOiAjk6SGQkEQFKTK7ZnX7+5tPQVOskTkRkWwwuQtXV1Xjttdfg7u6O0NBQhIaGwt3dHa+++iqqqqpMkZGswJXiSuxLyQegnx9ERMYz+94OaNlcjpS8EnwZzxWniYzJ4CL09NNP45tvvsEHH3yAxMREJCYm4oMPPsB3333HNYTs2NYT2dCJQNdgD4R6NZc6DpFNcW/mhPkjOwEA/vdHCs7mqCVORGQ7DN4EatWqVVi9ejXuu+++uve6dOmC4OBgTJw4EV999ZVRA5J12FyzpcYojgYRmcSILv7YcjwbO8/k4oV1x7Hhyb5wcuAKKES3y+B/RQqFAqGhode937p1a8jlcmNkIiuTcbUMxzIKIROA+7v4Sx2HyCYJgoC3R3eGezMnnMpS4+s9F6WORGQTDC5Cs2bNwltvvYXKysq69yorK/HOO+9g1qxZRg1H1uHXE/rRoD5tvOCjdJY4DZHt8nFzxvwREQCAz+NScD63WOJERNbP4FtjiYmJiIuLQ1BQELp27QoAOH78ODQaDQYNGoQxY8bUHbt+/XrjJSWLJIoiNibWLKLItYOITG50t0BsPZGDuOQ8vLD2OH6Z2QeOvEVG1GQGFyEPDw+MHTu23nvBwcFGC0TWJVlVjAt5JZA7yjC0s5/UcYhsniAIeGd0JA5/sgfHLxfhmz9T8eSAtlLHIrJaBhehpUuXmiIHWalNSfrbYgM7eEPp7CRxGiL74OfujNfuj8CL607g453n0TusJbq38pQ6FpFVMng8tby8HGVlZXW/Tk9Px6effoqdO3caNRhZPp1OxK81T4vFRgVKnIbIvoyPDsLwLv6o1ol4elUiCss0UkciskoGF6FRo0ZhxYoVAIDCwkL07NkTH330EUaNGsVH5+1MQsY1ZBWWw03hiIHhPlLHIbIrgiDgvTGRCG3pgqzCcsxZcxyiKEodi8jqGFyEjh07hv79+wMA1q1bBz8/P6Snp2PFihX4/PPPjR6QLFftlhpDOvvB2clB4jRE9sfN2Qn/e6g75I4yxCXnYcmfqVJHIrI6BhehsrIyuLm5AQB27tyJMWPGQCaToXfv3khPTzd6QLJMVVodtp7IAQCM4tNiRJLpHOiO1+/XP1L//vZzSEgvkDgRkXUxuAi1bdsWGzduRGZmJnbs2IF7770XAJCXlwelkjuO24u/LuTjWlkVvFwViAlrKXUcIrs2qVcr3N/FH1qdiFmrEnG1pPLWJxERgCYUoddffx3PP/88QkND0atXL8TExADQjw5169bN6AHJMtXeFru/iz/XMCGSmCAIWDgmEq29miOnqAIzlh9FuUYrdSwiq2Dwd7Bx48YhIyMDR48exfbt2+veHzRoED755BOjhiPLVKapxs4zuQC4iCKRpXBzdsKSKdFwb+aEpMxCPP1TIqq1OqljEVm8Jv0o7+fnh27dukEm+/v0nj17Ijw83GjByHL9fjYPZRotgls0Q7dgD6njEFGNtj5u+G5qD8gdZfj9bC7e+PU0nyQjugXe0yCDba65LTaqayAEQZA4DRH9U4/QFvhsQhQEAfjxYAa+jOfmrEQ3wyJEBiks02DP+SsA+LQYkaW6L9K/7kmy/+44h3UJlyVORGS5WITIIL+dUqFKK6KjvxLtfN2kjkNEDZjetzUevzMMAPDCuuP44UCatIGILBSLEBmk9mkxjgYRWb65Q8MxqVcriCLw2qbT+GTXec4ZIvoXFiFqtJyichy6pF+sbURXFiEiSyeTCXg7tjP+b1A7AMBncRfw6sZT0OpYhohqsQhRo/16PBuiCPQMbYFAj2ZSxyGiRhAEAc/d0x5vjeoEQQBWHsrArFXHUFHFdYaIABYhMsCmJP1O81w7iMj6TI4JxRcTu8HJQcBvp1QY9tmfOJZxTepYRJJjEaJGSckrxulsNRxlAoZH+ksdh4ia4P4uAVj+SE/4uCmQml+KcV/tx3u/Jd96dEinBS79CZxcp/9fHUeTyHawCFGj1I4G3dXeG57N5RKnIaKm6tPGC7ueuwujuwVCJwKL91zEiC/+aniz1jObgU87A8vvB36Zof/fTzvr3yeyAVZThN555x306dMHLi4u8PDwaNQ5oiji9ddfh7+/P5o1a4bBgwfjwoULpg1qg0RR5G0xIhvi7uKETyZE4evJ0fByleNCXgnGfnUAoxbtwy8Jl/8eITqzGVgzBVBn1/8A6hz9+yxDZAOspghpNBqMHz8eM2fObPQ5H3zwAT7//HMsXrwYhw4dQvPmzTFkyBBUVFSYMKntScosREZBGVzkDrgnwlfqOERkJEM6+WHnc3dhfHQQnBwEHM8sxJy1x9HnvT/w3tZTqPj1BYi40RNmNe9tn8vbZGT1HKUO0FgLFiwAACxbtqxRx4uiiE8//RSvvvoqRo0aBQBYsWIFfH19sXHjRjz44IM3PK+yshKVlZV1v1ar1bcX3AbUjgbdG+ELF7nV/JUhokZo0VyO/47vipfuC8fPRzKx8mA6sosqkLTvNzjLVTc5UwTUWXjl069xSt7FbHnJNs0c0BZDO/tJ8rlt9rvapUuXoFKpMHjw4Lr33N3d0atXLxw4cKDBIrRw4cK60kVAtVaHLSf0RWhUVKDEaYjIVLxcFXhqYFv8584w/JGcB9W+s0D2rc8rzs/CcV2I6QOSTSso1Uj2uW22CKlU+p9kfH3r38rx9fWt+70bmTdvHmbPnl33a7VajeDgYNOEtAL7L15FfokGni5O6NfOS+o4RGRijg4y3NvJD3DpBSy/9fHThvRCrG8P0wcjm9bBTynZ55a0CM2dOxfvv//+TY85e/YswsPDzZQIUCgUUCgUZvt8lq72ttjwLv5wcrCaKWVEdLtC+gDKAP3E6BvOExIAZQC69x8OyBzMnY7IaCQtQnPmzMG0adNuekxYWFiTPrafn/5eY25uLvz9/173Jjc3F1FRUU36mPamokqLHaf1o2exvC1GZF9kDsDQ9/VPh0FA/TIk6P9n6HssQWT1JC1C3t7e8Pb2NsnHbt26Nfz8/BAXF1dXfNRqNQ4dOmTQk2f2LO5sHkoqqxHo0QzdW3lKHYeIzC1iJPDACmD7S/UfoVcG6EtQxEjpshEZidXMEcrIyEBBQQEyMjKg1WqRlJQEAGjbti1cXV0BAOHh4Vi4cCFGjx4NQRDw7LPP4u2330a7du3QunVrvPbaawgICEBsbKx0F2JFNtbsND8yKgAymSBxGiKSRMRIIHw4kL4fKMkFXH31t804EkQ2wmqK0Ouvv47ly/+eudetWzcAwO7duzFgwAAAwLlz51BUVFR3zIsvvojS0lI8/vjjKCwsRL9+/bB9+3Y4OzubNbs1KizTIP5cHgDeFiOyezIHoHV/qVMQmYQgiuKNZsFRDbVaDXd3dxQVFUGplG5Wu7mtOpSBlzecRLifG7Y/e6fUcYiIiAzS2O/ffAyIbmhTzW0xrh1ERES2jEWIrpNdWI5Dl/QbMHJvMSIismUsQnSdzcf1T4f0bN0CgR7NJE5DRERkOixCdJ2NifrbYpwkTUREto5FiOo5pypGsqoYTg4ChkVKswEeERGRubAIUT21awcN6OADDxe5xGmIiIhMi0WI6uh0IjbX7C3G22JERGQPWISoztH0a8gqLIerwhGDOvpIHYeIiMjkWISoTu3aQUM7+8HZicvnExGR7WMRIgCAplqHrSdzAACjuHYQERHZCRYhAgDsPX8FhWVV8HZToE8bL6njEBERmQWLEAEANtRuqdE1AA7caZ6IiOwEixBBXVGF38/kAgBiu/FpMSIish8sQoTtp1SorNahnY8rOgU0vEMvERGRrWERor+31OgWCEHgbTEiIrIfLEJ2LqeoHAdSrwLg02JERGR/WITs3OakbIiifqf5IE8XqeMQERGZFYuQndvAneaJiMiOsQjZsbM5aiSriiF3kGF4pL/UcYiIiMyORciO1e40PzDcG+4uThKnISIiMj8WITul04nYlKjfaX401w4iIiI7xSJkpw5eugqVugJKZ0cM6MCd5omIyD6xCNmp2rWDhnfx507zRERkt1iE7FBFlRa/nVQB4NNiRERk31iE7NCuM7korqxGoEcz3BHaQuo4REREkmERskPrj10GAIzpHggZd5onIiI7xiJkZ64UV2LvhXwAfFqMiIiIRcjO/Ho8G1qdiK7BHgjzdpU6DhERkaRYhOzM+sSa22IcDSIiImIRsicXcotxKksNR5mAEV250zwRERGLkB1ZX7N20IAOPmjRXC5xGiIiIumxCNkJnU6sW0RxTHfeFiMiIgJYhOzGwdSryCnSb6lxdzi31CAiIgJYhOzG+rotNQK4pQYREVENFiE7UK7R4reTOQB4W4yIiOifWITswI7TKpRqtAhu0Qw9QjyljkNERGQxWITswC+1W2p0C4IgcEsNIiKiWixCNi6nqBx/pei31BjbPUjiNERERJaFRcjGbUjMgigCPUNboFVLF6njEBERWRQWIRsmiiJ+SdDfFhsbzUnSRERE/8YiZMOOXy7CxSulcHaSYVikv9RxiIiILA6LkA2rHQ0a0skPbs5OEqchIiKyPCxCNqqyWovNx7MBcJI0ERFRQ1iEbNQfZ/NQVF4FP6Uz+rb1kjoOERGRRbKaIvTOO++gT58+cHFxgYeHR6POmTZtGgRBqPcaOnSoaYNaiNq1g0Z3D4SDjGsHERER3Yij1AEaS6PRYPz48YiJicF3333X6POGDh2KpUuX1v1aoVCYIp5FyS+pRPy5KwB4W4yIiOhmrKYILViwAACwbNkyg85TKBTw8/MzQSLLtSkpG9U6EV2DPdDWx1XqOERERBbLam6NNVV8fDx8fHzQoUMHzJw5E1evXr3p8ZWVlVCr1fVe1mZdzdNi47jBKhER0U3ZdBEaOnQoVqxYgbi4OLz//vvYs2cP7rvvPmi12gbPWbhwIdzd3etewcHBZkx8+05lFeFsjhpyBxlGdA2QOg4REZFFk7QIzZ0797rJzP9+JScnN/njP/jggxg5ciQiIyMRGxuLLVu24MiRI4iPj2/wnHnz5qGoqKjulZmZ2eTPL4Xa0aB7OvnCw0UucRoiIiLLJukcoTlz5mDatGk3PSYsLMxony8sLAxeXl5ISUnBoEGDbniMQqGw2gnVldVabEzKAgCMj+YkaSIioluRtAh5e3vD29vbbJ/v8uXLuHr1Kvz9bXO7id/P5KGwTL92UP925vvvSkREZK2sZo5QRkYGkpKSkJGRAa1Wi6SkJCQlJaGkpKTumPDwcGzYsAEAUFJSghdeeAEHDx5EWloa4uLiMGrUKLRt2xZDhgyR6jJMam2C/jbe2GiuHURERNQYVvP4/Ouvv47ly5fX/bpbt24AgN27d2PAgAEAgHPnzqGoqAgA4ODggBMnTmD58uUoLCxEQEAA7r33Xrz11ltWe+vrZlRFFdh7Xr920Lho65rgTUREJBVBFEVR6hCWTK1Ww93dHUVFRVAqlVLHadCX8Sn4YPs53BHqibVP9JE6DhERkaQa+/3bam6NUcNEUcTao/qnxcZzNIiIiKjRWIRsQEL6NVzKL4WL3AHDutjmRHAiIiJTYBGyAbWjQcMi/eGqsJppX0RERJJjEbJyZZpqbDmRDYBrBxERERmKRcjKbTupQqlGi5CWLujZuoXUcYiIiKwKi5CV+/lIBgD9aJAgcO0gIiIiQ7AIWbGUvBIcSbsGmQCM78GnxYiIiAzFImTF1hzVryR9d7gPfJXOEqchIiKyPixCVkpTrcMvNTvNT7ijlcRpiIiIrBOLkJWKO5uLq6Ua+LgpMLADN1glIiJqChYhK7X6iP622LjoIDg68I+RiIioKfgd1AplFZZj7wX9BqsPcJI0ERFRk7EIWaG1RzMhikBMWEuEejWXOg4REZHVYhGyMlrd3xusPtiTo0FERES3g0XIyvyVko+swnK4N3PCkE5+UschIiKyaixCVqZ2JenR3QLh7OQgcRoiIiLrxiJkRa4UV2Ln6VwAnCRNRERkDCxCVmRtQiaqdSKigj0QEaCUOg4REZHVYxGyEjqdiJ8O62+LTerFlaSJiIiMgUXISvyZko/MgnK4OTvi/i4BUschIiKyCSxCVmLVoXQAwNjuQWgm5yRpIiIiY2ARsgK56gr8fjYPAG+LERERGROLkBX4+UgmtDoRd4R6op2vm9RxiIiIbAaLkIXT6kSsrpskHSJxGiIiItvCImTh4s/lIbuoAp4uThjamStJExERGROLkIVbdUg/GjS2exBXkiYiIjIyFiELllVYjt3n9JOkJ3KSNBERkdGxCFmw1YczoBOBmLCWaOPtKnUcIiIim8MiZKE01Tr8dDgTAPBwb06SJiIiMgUWIQv126kc5JdUwlepwL2dfKWOQ0REZJNYhCzUigP6laQf6hkCJwf+MREREZkCv8NaoNPZRUhIvwZHmYCJPYOljkNERGSzWIQs0A81o0FDO/vBR+kscRoiIiLbxSJkYYrKqrAxKQsAMCUmVNowRERENo5FyMKsTchERZUO4X5uuCPUU+o4RERENo1FyILodCJ+OKi/LTYlJhSCIEiciIiIyLaxCFmQvReuIP1qGdycHRHbLUDqOERERDaPRciC1E6SHhcdBBe5o8RpiIiIbB+LkIVIv1qKP2r2FZvMlaSJiIjMgkXIQizbnwZRBAZ08EYY9xUjIiIyCxYhC1BcUYW1Ry8DAB7p21riNERERPaDRcgCrDl6GSWV1Wjr44r+7bykjkNERGQ3WIQkptWJWLb/EgD9aBAfmSciIjIfFiGJ7TqTi8yCcni6OGFM90Cp4xAREdkVPqMtBZ0WSN8PlOTi4O58yBCIh3q1grOTg9TJiIiI7IpVjAilpaVhxowZaN26NZo1a4Y2bdpg/vz50Gg0Nz2voqICTz31FFq2bAlXV1eMHTsWubm5ZkrdgDObgU87A8vvB36ZgTcKXsI+xTN4tOVpaXMRERHZIasoQsnJydDpdPj6669x+vRpfPLJJ1i8eDFefvnlm5733HPP4ddff8XatWuxZ88eZGdnY8yYMWZKfQNnNgNrpgDq7Hpv+wkF8NwyQ//7REREZDaCKIqi1CGa4r///S+++uorpKam3vD3i4qK4O3tjVWrVmHcuHEA9IWqY8eOOHDgAHr37t2oz6NWq+Hu7o6ioiIolcqmB9Zp9SNB/ypBfxMAZQDw7ElAxltkREREt6Ox37+tYkToRoqKitCiRYsGfz8hIQFVVVUYPHhw3Xvh4eFo1aoVDhw40OB5lZWVUKvV9V5Gkb7/JiUIAERAnaU/joiIiMzCKotQSkoKvvjiC/znP/9p8BiVSgW5XA4PD4967/v6+kKlUjV43sKFC+Hu7l73Cg4ONk7okkbOTWrscURERHTbJC1Cc+fOhSAIN30lJyfXOycrKwtDhw7F+PHj8dhjjxk907x581BUVFT3yszMNM4HdvU17nFERER02yR9fH7OnDmYNm3aTY8JCwur+//Z2dkYOHAg+vTpg2+++eam5/n5+UGj0aCwsLDeqFBubi78/PwaPE+hUEChUDQqv0FC+ujnAKlzANxoWlbNHKGQPsb/3ERERHRDkhYhb29veHt7N+rYrKwsDBw4ENHR0Vi6dClkspsPZkVHR8PJyQlxcXEYO3YsAODcuXPIyMhATEzMbWc3mMwBGPq+/qkxCKhfhmpWkx76HidKExERmZFVzBHKysrCgAED0KpVK3z44Ye4cuUKVCpVvbk+WVlZCA8Px+HDhwEA7u7umDFjBmbPno3du3cjISEB06dPR0xMTKOfGDO6iJHAAysApX/995UB+vcjRkqTi4iIyE5ZxcrSu3btQkpKClJSUhAUFFTv92qf/q+qqsK5c+dQVlZW93uffPIJZDIZxo4di8rKSgwZMgRffvmlWbNfJ2IkED68bmVpuPrqb4dxJIiIiMjsrHYdIXMx2jpCREREZDY2v44QERER0e1iESIiIiK7xSJEREREdotFiIiIiOwWixARERHZLRYhIiIislssQkRERGS3WISIiIjIbrEIERERkd2yii02pFS78LZarZY4CRERETVW7fftW22gwSJ0C8XFxQCA4OBgiZMQERGRoYqLi+Hu7t7g73OvsVvQ6XTIzs6Gm5sbBEEw2sdVq9UIDg5GZmam3exhZm/XzOu1bbxe28brtX6iKKK4uBgBAQGQyRqeCcQRoVuQyWTX7XhvTEql0mb+0jWWvV0zr9e28XptG6/Xut1sJKgWJ0sTERGR3WIRIiIiIrvFIiQRhUKB+fPnQ6FQSB3FbOztmnm9to3Xa9t4vfaDk6WJiIjIbnFEiIiIiOwWixARERHZLRYhIiIislssQkRERGS3WIRMaNGiRQgNDYWzszN69eqFw4cP3/T4tWvXIjw8HM7OzoiMjMS2bdvMlNR4DLnmJUuWoH///vD09ISnpycGDx58y/9GlsbQP+Naq1evhiAIiI2NNW1AIzP0egsLC/HUU0/B398fCoUC7du3t6q/14Ze76effooOHTqgWbNmCA4OxnPPPYeKigozpb09e/fuxYgRIxAQEABBELBx48ZbnhMfH4/u3btDoVCgbdu2WLZsmclzGouh17t+/Xrcc8898Pb2hlKpRExMDHbs2GGesEbQlD/fWvv27YOjoyOioqJMlk9KLEIm8vPPP2P27NmYP38+jh07hq5du2LIkCHIy8u74fH79+/HxIkTMWPGDCQmJiI2NhaxsbE4deqUmZM3naHXHB8fj4kTJ2L37t04cOAAgoODce+99yIrK8vMyZvG0OutlZaWhueffx79+/c3U1LjMPR6NRoN7rnnHqSlpWHdunU4d+4clixZgsDAQDMnbxpDr3fVqlWYO3cu5s+fj7Nnz+K7777Dzz//jJdfftnMyZumtLQUXbt2xaJFixp1/KVLlzB8+HAMHDgQSUlJePbZZ/Hoo49aTTkw9Hr37t2Le+65B9u2bUNCQgIGDhyIESNGIDEx0cRJjcPQ661VWFiIKVOmYNCgQSZKZgFEMomePXuKTz31VN2vtVqtGBAQIC5cuPCGxz/wwAPi8OHD673Xq1cv8T//+Y9JcxqTodf8b9XV1aKbm5u4fPlyU0U0qqZcb3V1tdinTx/x22+/FadOnSqOGjXKDEmNw9Dr/eqrr8SwsDBRo9GYK6JRGXq9Tz31lHj33XfXe2/27Nli3759TZrTFACIGzZsuOkxL774otipU6d6702YMEEcMmSICZOZRmOu90YiIiLEBQsWGD+QiRlyvRMmTBBfffVVcf78+WLXrl1NmksqHBEyAY1Gg4SEBAwePLjuPZlMhsGDB+PAgQM3POfAgQP1jgeAIUOGNHi8pWnKNf9bWVkZqqqq0KJFC1PFNJqmXu+bb74JHx8fzJgxwxwxjaYp17t582bExMTgqaeegq+vLzp37ox3330XWq3WXLGbrCnX26dPHyQkJNTdPktNTcW2bdswbNgws2Q2N2v/mnW7dDodiouLreLrVVMtXboUqampmD9/vtRRTIqbrppAfn4+tFotfH19673v6+uL5OTkG56jUqlueLxKpTJZTmNqyjX/20svvYSAgIDrvrhaoqZc719//YXvvvsOSUlJZkhoXE253tTUVPzxxx+YNGkStm3bhpSUFDz55JOoqqqy+C+sTbnehx56CPn5+ejXrx9EUUR1dTWeeOIJq7k1ZqiGvmap1WqUl5ejWbNmEiUzjw8//BAlJSV44IEHpI5iEhcuXMDcuXPx559/wtHRtqsCR4TIIrz33ntYvXo1NmzYAGdnZ6njGF1xcTEmT56MJUuWwMvLS+o4ZqHT6eDj44NvvvkG0dHRmDBhAl555RUsXrxY6mgmER8fj3fffRdffvkljh07hvXr12Pr1q146623pI5GRrZq1SosWLAAa9asgY+Pj9RxjE6r1eKhhx7CggUL0L59e6njmJxt1zyJeHl5wcHBAbm5ufXez83NhZ+f3w3P8fPzM+h4S9OUa6714Ycf4r333sPvv/+OLl26mDKm0Rh6vRcvXkRaWhpGjBhR955OpwMAODo64ty5c2jTpo1pQ9+Gpvz5+vv7w8nJCQ4ODnXvdezYESqVChqNBnK53KSZb0dTrve1117D5MmT8eijjwIAIiMjUVpaiscffxyvvPIKZDLb+rmzoa9ZSqXSpkeDVq9ejUcffRRr1661itHrpiguLsbRo0eRmJiIWbNmAdB/vRJFEY6Ojti5cyfuvvtuiVMaj239y7QQcrkc0dHRiIuLq3tPp9MhLi4OMTExNzwnJiam3vEAsGvXrgaPtzRNuWYA+OCDD/DWW29h+/bt6NGjhzmiGoWh1xseHo6TJ08iKSmp7jVy5Mi6J26Cg4PNGd9gTfnz7du3L1JSUuoKHwCcP38e/v7+Fl2CgKZdb1lZ2XVlp7YEija4paO1f81qip9++gnTp0/HTz/9hOHDh0sdx2SUSuV1X6+eeOIJdOjQAUlJSejVq5fUEY1L4snaNmv16tWiQqEQly1bJp45c0Z8/PHHRQ8PD1GlUomiKIqTJ08W586dW3f8vn37REdHR/HDDz8Uz549K86fP190cnIST548KdUlGMzQa37vvfdEuVwurlu3TszJyal7FRcXS3UJBjH0ev/N2p4aM/R6MzIyRDc3N3HWrFniuXPnxC1btog+Pj7i22+/LdUlGMTQ650/f77o5uYm/vTTT2Jqaqq4c+dOsU2bNuIDDzwg1SUYpLi4WExMTBQTExNFAOLHH38sJiYmiunp6aIoiuLcuXPFyZMn1x2fmpoquri4iC+88IJ49uxZcdGiRaKDg4O4fft2qS7BIIZe78qVK0VHR0dx0aJF9b5eFRYWSnUJBjH0ev/Nlp8aYxEyoS+++EJs1aqVKJfLxZ49e4oHDx6s+7277rpLnDp1ar3j16xZI7Zv316Uy+Vip06dxK1bt5o58e0z5JpDQkJEANe95s+fb/7gTWTon/E/WVsREkXDr3f//v1ir169RIVCIYaFhYnvvPOOWF1dbebUTWfI9VZVVYlvvPGG2KZNG9HZ2VkMDg4Wn3zySfHatWvmD94Eu3fvvuG/x9prnDp1qnjXXXddd05UVJQol8vFsLAwcenSpWbP3VSGXu9dd9110+MtXVP+fP/JlouQIIo2OGZLRERE1AicI0RERER2i0WIiIiI7BaLEBEREdktFiEiIiKyWyxCREREZLdYhIiIiMhusQgRERGR3WIRIiIiIrvFIkRENik+Ph6CIKCwsFDqKERkwbiyNBHZhAEDBiAqKgqffvopAECj0aCgoAC+vr4QBEHacERksRylDkBEZApyuRx+fn5SxyAiC8dbY0Rk9aZNm4Y9e/bgs88+gyAIEAQBy5Ytq3drbNmyZfDw8MCWLVvQoUMHuLi4YNy4cSgrK8Py5csRGhoKT09PPPPMM9BqtXUfu7KyEs8//zwCAwPRvHlz9OrVC/Hx8dJcKBEZHUeEiMjqffbZZzh//jw6d+6MN998EwBw+vTp644rKyvD559/jtWrV6O4uBhjxozB6NGj4eHhgW3btiE1NRVjx45F3759MWHCBADArFmzcObMGaxevRoBAQHYsGEDhg4dipMnT6Jdu3ZmvU4iMj4WISKyeu7u7pDL5XBxcam7HZacnHzdcVVVVfjqq6/Qpk0bAMC4cePwww8/IDc3F66uroiIiMDAgQOxe/duTJgwARkZGVi6dCkyMjIQEBAAAHj++eexfft2LF26FO+++675LpKITIJFiIjshouLS10JAgBfX1+EhobC1dW13nt5eXkAgJMnT0Kr1aJ9+/b1Pk5lZSVatmxpntBEZFIsQkRkN5ycnOr9WhCEG76n0+kAACUlJXBwcEBCQgIcHBzqHffP8kRE1otFiIhsglwurzfJ2Ri6desGrVaLvLw89O/f36gfm4gsA58aIyKbEBoaikOHDiEtLQ35+fl1ozq3o3379pg0aRKmTJmC9evX49KlSzh8+DAWLlyIrVu3GiE1EUmNRYiIbMLzzz8PBwcHREREwNvbGxkZGUb5uEuXLsWUKVMwZ84cdOjQAbGxsThy5AhatWpllI9PRNLiytJERERktzgiRERERHaLRYiIiIjsFosQERER2S0WISIiIrJbLEJERERkt1iEiIiIyG6xCBEREZHdYhEiIiIiu8UiRERERHaLRYiIiIjsFosQERER2a3/ByCRZq2UPqdtAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(xlabel='time', xlim=(0, 1.5));" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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leo6fPLnzSc4biksQkRERNRiFHIZ5o/uCAD46cA5ZBVWSJqHRYiIiIhaVL/2Hhje0RtanYi34tIkXYeMRYiIiIha3OsjI6BUyLAv+zK2pl2ULAeLEBEREbU4f3d7zBgQBAelHGVXpRs0bTYTKhIREZFleXpwMB7p0xZezraSZWARIiIiIknYKxWwV0pbRXhrjIiIiKwWixARERFZLRYhIiIislosQkRERGS1WISIiIjIarEIERERkdViESIiIiKrxSJEREREVotFiIiIiKwWixARERFZLRYhIiIislosQkRERGS1WISIiIjIanH1+dsQRREAUF5eLnESIiIiaqrrv7ev/x6/GRah26ioqAAA+Pv7S5yEiIiImquiogIuLi43/bkg3q4qWTmdTofz58/DyckJgiAY7H3Ly8vh7++PvLw8ODs7G+x9TZm1nTPP17LxfC0bz9f8iaKIiooK+Pn5QSa7+UggXhG6DZlMhjZt2hjt/Z2dnS3m/3RNZW3nzPO1bDxfy8bzNW+3uhJ0HQdLExERkdViESIiIiKrxSIkEZVKhfnz50OlUkkdpcVY2znzfC0bz9ey8XytBwdLExERkdXiFSEiIiKyWixCREREZLVYhIiIiMhqsQgRERGR1WIRMqKlS5ciMDAQtra2iIyMRFJS0i33X7NmDcLCwmBra4vOnTtj8+bNLZTUcJpzzt988w0GDBgANzc3uLm5ITo6+rb/jkxNc/+Mr1u1ahUEQcC4ceOMG9DAmnu+paWleOaZZ+Dr6wuVSoWQkBCz+v91c8/3k08+QWhoKOzs7ODv749Zs2ahpqamhdLemd27d2P06NHw8/ODIAj4/fffb3vMrl27cNddd0GlUqF9+/ZYsWKF0XMaSnPPd926dRg6dCg8PT3h7OyMPn36YOvWrS0T1gD0+fO9bt++fVAoFOjWrZvR8kmJRchIVq9ejRdffBHz58/HkSNH0LVrVwwfPhxFRUU33H///v148MEHMW3aNKSkpGDcuHEYN24cTpw40cLJ9dfcc961axcefPBB7Ny5E4mJifD398ewYcNQUFDQwsn109zzve7s2bOYM2cOBgwY0EJJDaO551tbW4uhQ4fi7NmzWLt2LbKysvDNN9+gdevWLZxcP809319++QVz587F/PnzkZGRge+++w6rV6/Gq6++2sLJ9VNVVYWuXbti6dKlTdr/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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model)\n", + "simulate(sbml_model, T=1.5);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The spline annotation in this case is\n", + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And here we have a periodic spline." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=3),\n", + " values_at_nodes=[-2, 1, -2], # first and last node must coincide\n", + " extrapolate='periodic',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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VqIoor+/H2VlZTAYxq4EYkboNAwGwykn3idSVlYW/1DiwM8rdvysYsfPKnb8rGLHzyp2yfqsxssESVgsTURERLrFQIiIiIh0i4GQTKxWKx544AFYrVa5m6IK/Lxix88qdvysYsfPKnb8rGKnhM+KxdJERESkW8wIERERkW4xECIiIiLdYiBEREREusVAiIiIiHSLgVASPfbYY6isrITNZsOSJUuwffv2cZ//wgsvoKamBjabDfPnz8frr7+eopYqQzyf18aNGyEIwogvm82WwtbK491338UXvvAFlJWVQRAEvPzyy6d9zZYtW3DWWWfBarVixowZ2LhxY9LbqQTxflZbtmw5pU8JgoC2trbUNFhG69evx7/9278hMzMTRUVFWLlyJQ4fPnza1+nxmjWRz0qv16vHH38cCxYsCG+WuHTpUvz9738f9zVy9CkGQknyxz/+EWvXrsUDDzyAnTt3YuHChVixYgU6OjpGff7777+P66+/Hrfccgt27dqFlStXYuXKldi3b1+KWy6PeD8vILQTaWtra/iroaEhhS2Wh9vtxsKFC/HYY4/F9Pz6+npcdtlluOiii7B7927ccccd+MpXvoI333wzyS2VX7yfleTw4cMj+lVRUVGSWqgcW7duxde//nV88MEH2LRpE3w+Hz73uc/B7XaP+Rq9XrMm8lkB+rxeTZ06FT/+8Y+xY8cOfPzxx/jMZz6DK6+8Evv37x/1+bL1KZGS4uyzzxa//vWvh/87EAiIZWVl4vr160d9/r//+7+Ll1122YjHlixZIn71q19NajuVIt7P6+mnnxazs7NT1DplAiC+9NJL4z7nnnvuEc8444wRj61atUpcsWJFElumPLF8Vu+8844IQOzt7U1Jm5Sso6NDBCBu3bp1zOfo/ZolieWz4vUqIjc3V/ztb3876r/J1aeYEUoCr9eLHTt2YPny5eHHDAYDli9fjm3bto36mm3bto14PgCsWLFizOdryUQ+LwBwuVyoqKhAeXn5uKMMPdNzv5qoM888E6WlpfjsZz+L9957T+7myMLhcAAA8vLyxnwO+1ZILJ8VwOtVIBDA888/D7fbjaVLl476HLn6FAOhJOjq6kIgEEBxcfGIx4uLi8esN2hra4vr+Voykc9r9uzZeOqpp/DXv/4Vv//97xEMBrFs2TKcOHEiFU1WjbH6ldPpxODgoEytUqbS0lI88cQT+Mtf/oK//OUvKC8vx4UXXoidO3fK3bSUCgaDuOOOO/DpT38a8+bNG/N5er5mSWL9rPR8vdq7dy8yMjJgtVrxta99DS+99BLmzp076nPl6lM8fZ5UaenSpSNGFcuWLcOcOXPw61//Gt///vdlbBmp1ezZszF79uzwfy9btgy1tbXYsGEDfve738nYstT6+te/jn379uFf//qX3E1RvFg/Kz1fr2bPno3du3fD4XDgz3/+M2688UZs3bp1zGBIDswIJUFBQQGMRiPa29tHPN7e3o6SkpJRX1NSUhLX87VkIp/XycxmMxYtWoRjx44lo4mqNVa/ysrKQlpamkytUo+zzz5bV31qzZo1ePXVV/HOO+9g6tSp4z5Xz9csIL7P6mR6ul5ZLBbMmDEDixcvxvr167Fw4UL84he/GPW5cvUpBkJJYLFYsHjxYmzevDn8WDAYxObNm8ecG126dOmI5wPApk2bxny+lkzk8zpZIBDA3r17UVpamqxmqpKe+1Ui7N69Wxd9ShRFrFmzBi+99BLefvttTJ8+/bSv0WvfmshndTI9X6+CwSA8Hs+o/yZbn0pqKbaOPf/886LVahU3btwoHjhwQLztttvEnJwcsa2tTRRFUfzSl74k3nvvveHnv/fee6LJZBJ/9rOfiQcPHhQfeOAB0Ww2i3v37pXrR0ipeD+vBx98UHzzzTfF2tpacceOHeJ1110n2mw2cf/+/XL9CCnR398v7tq1S9y1a5cIQHzkkUfEXbt2iQ0NDaIoiuK9994rfulLXwo/v66uTrTb7eLdd98tHjx4UHzsscdEo9EovvHGG3L9CCkT72e1YcMG8eWXXxaPHj0q7t27V/zWt74lGgwG8R//+IdcP0LK3H777WJ2dra4ZcsWsbW1Nfw1MDAQfg6vWSET+az0er269957xa1bt4r19fXinj17xHvvvVcUBEF86623RFFUTp9iIJREv/zlL8Vp06aJFotFPPvss8UPPvgg/G8XXHCBeOONN454/p/+9Cdx1qxZosViEc844wzxtddeS3GL5RXP53XHHXeEn1tcXCxeeuml4s6dO2VodWpJS7xP/pI+mxtvvFG84IILTnnNmWeeKVosFrGqqkp8+umnU95uOcT7Wf3kJz8Rq6urRZvNJubl5YkXXnih+Pbbb8vT+BQb7XMCMKKv8JoVMpHPSq/Xqy9/+ctiRUWFaLFYxMLCQvHiiy8OB0GiqJw+JYiiKCY350RERESkTKwRIiIiIt1iIERERES6xUCIiIiIdIuBEBEREekWAyEiIiLSLQZCREREpFsMhIiIiEi3GAgRERGRbjEQIiJN2rJlCwRBQF9fn9xNISIF487SRKQJF154Ic4880z8/Oc/BwB4vV709PSguLgYgiDI2zgiUiyT3A0gIkoGi8WCkpISuZtBRArHqTEiUr2bbroJW7duxS9+8QsIggBBELBx48YRU2MbN25ETk4OXn31VcyePRt2ux3XXHMNBgYG8Mwzz6CyshK5ubn45je/iUAgEP7eHo8H3/72tzFlyhSkp6djyZIl2LJlizw/KBElHDNCRKR6v/jFL3DkyBHMmzcPDz30EABg//79pzxvYGAAjz76KJ5//nn09/fjqquuwhe/+EXk5OTg9ddfR11dHa6++mp8+tOfxqpVqwAAa9aswYEDB/D888+jrKwML730Ei655BLs3bsXM2fOTOnPSUSJx0CIiFQvOzsbFosFdrs9PB126NChU57n8/nw+OOPo7q6GgBwzTXX4He/+x3a29uRkZGBuXPn4qKLLsI777yDVatWobGxEU8//TQaGxtRVlYGAPj2t7+NN954A08//TR+9KMfpe6HJKKkYCBERLpht9vDQRAAFBcXo7KyEhkZGSMe6+joAADs3bsXgUAAs2bNGvF9PB4P8vPzU9NoIkoqBkJEpBtms3nEfwuCMOpjwWAQAOByuWA0GrFjxw4YjcYRz4sOnohIvRgIEZEmWCyWEUXOibBo0SIEAgF0dHTgvPPOS+j3JiJl4KoxItKEyspKfPjhhzh+/Di6urrCWZ3JmDVrFm644QasXr0aL774Iurr67F9+3asX78er732WgJaTURyYyBERJrw7W9/G0ajEXPnzkVhYSEaGxsT8n2ffvpprF69GnfddRdmz56NlStX4qOPPsK0adMS8v2JSF7cWZqIiIh0ixkhIiIi0i0GQkRERKRbDISIiIhItxgIERERkW4xECIiIiLdYiBEREREusVAiIiIiHSLgRARERHpFgMhIiIi0i0GQkRERKRbDISIiIhIt/5/J0KTt3NqxlMAAAAASUVORK5CYII=\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(xlabel='time', xlim=(0, 3));" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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AWrqpeseYyFEREREJhcT2A7+KgdU1DZg79kiyXKwECIiIiKTu9Y95qJUoFBTI1kOQRRFUbK7WwCNRgOVSgW1Wg03Nzep4xAREVmNsqo6ONjJ4WAnN/hzt/T7W2HwOxMRERG1gLuTvdQR2DVGREREtouFEBEREdksFkJERERks1gIERERkc1iIUREREQ2i4UQERER2SwWQkRERGSzWAgRERGRzWIhRERERDaLhRARERHZLBZCREREZLNYCBEREZHNYiFERERENou7z9+GKIoAAI1GI3ESIiIiaqlr39vXvsdvhoXQbZSXlwMAAgMDJU5CRERErVVeXg6VSnXTnwvi7UolG6fT6ZCbmwtXV1cIgmCw59VoNAgMDER2djbc3NwM9rzWiu9Xy/G9ajm+Vy3H96rl+F61nDHfK1EUUV5eDn9/f8hkNx8JxBah25DJZOjQoYPRnt/NzY3/obQC36+W43vVcnyvWo7vVcvxvWo5Y71Xt2oJuoaDpYmIiMhmsRAiIiIim8VCSCJKpRJvvfUWlEql1FEsAt+vluN71XJ8r1qO71XL8b1qOXN4rzhYmoiIiGwWW4SIiIjIZrEQIiIiIpvFQoiIiIhsFgshIiIislkshIxo8eLFCA4OhoODA2JjYxEfH3/L81evXo3w8HA4ODggMjISW7ZsMVFS89Ca92v58uUQBKHZ4eDgYMK00ti3bx/uv/9++Pv7QxAErF+//rbX7NmzB7169YJSqUTnzp2xfPlyo+c0B619r/bs2XPdZ0oQBOTn55smsITmz5+PO+64A66urvD29sbYsWORnp5+2+ts8XeWPu+Vrf6++uqrr9CzZ8+mxRL79++PrVu33vIaKT5TLISM5JdffsErr7yCt956C4mJiYiKisKwYcNQWFh4w/MPHTqERx55BNOnT8eJEycwduxYjB07FmlpaSZOLo3Wvl9A40qkeXl5TcelS5dMmFgalZWViIqKwuLFi1t0fmZmJkaNGoW7774bSUlJmDlzJp566ils377dyEml19r36pr09PRmnytvb28jJTQfe/fuxQsvvIAjR45gx44dqK+vx3333YfKysqbXmOrv7P0ea8A2/x91aFDB3z44YdISEjA8ePHcc899+CBBx7AyZMnb3i+ZJ8pkYyib9++4gsvvND0b61WK/r7+4vz58+/4fkTJ04UR40a1eyx2NhY8dln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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model)\n", + "simulate(sbml_model, T=3);" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The spline annotation in this case is\n", + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can modify the spline's boundary conditions, for example requiring that the derivatives is zero." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4),\n", + " values_at_nodes=[-1, 2, 4, 2],\n", + " bc='zeroderivative',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Or we can impose natural boundary conditions." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4),\n", + " values_at_nodes=[-1, 2, 4, 2],\n", + " bc='natural',\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Even if all node values are positive, due to under-shooting a cubic Hermite spline can assume negative values. In certain settings (e.g., when the spline represents a chemical reaction rate) this should be avoided. A possible solution is to carry out the interpolation in log-space (the resulting function is no longer a spline, but it is still a smooth interpolant)." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=5),\n", + " values_at_nodes=[2, 0.05, 0.1, 2, 1],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# This spline assumes negative values!\n", + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=5),\n", + " values_at_nodes=[2, 0.05, 0.1, 2, 1],\n", + " logarithmic_parametrization=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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iLh7927L90aPHi3OnDmz1fnfffed2KNHD1GlUom9e/cW165d6+CKXZM117lz584igAtu8+fPd3zhLsba/8/nY7hpP2uv886dO8WhQ4eKarVa7Nq1q/jPf/5TbG5udnDVrsea69zU1CS+/PLLYrdu3UQvLy8xNjZW/Nvf/iaWl5c7vnAXsnXr1jbfb83XdubMmeLo0aMvuE9iYqKoUqnErl27ip9//rnd6xREke1vREREJB8cc0NERESywnBDREREssJwQ0RERLLCcENERESywnBDREREssJwQ0RERLLCcENERESywnBDREREssJwQ0QuITU1FYIgoKKiQupSiMjJcYViInJKY8aMQWJiIhYuXAgAaGxsRFlZGcLDwyEIgrTFEZFT85C6ACKi9lCpVIiIiJC6DCJyAeyWIiKnc88992Dbtm147733IAgCBEHA0qVLW3VLLV26FAEBAVizZg0SEhLg4+OD2267DbW1tfjiiy8QFxeHwMBAPP744zAYDJbHbmhowNNPP43o6Gj4+vpi6NChSE1NleaFEpFdsOWGiJzOe++9h5MnT6JPnz549dVXAQBHjhy54Lza2lq8//77WL58OaqqqjBt2jTccsstCAgIwLp165CdnY1bb70VI0aMwPTp0wEAs2fPxtGjR7F8+XJERUVh1apVuPHGG5GRkYH4+HiHvk4isg+GGyJyOlqtFiqVCj4+PpauqOPHj19wXlNTEz766CN069YNAHDbbbfhv//9L4qKiuDn54devXph7Nix2Lp1K6ZPn468vDx8/vnnyMvLQ1RUFADg6aefxvr16/H555/j9ddfd9yLJCK7YbghIpfl4+NjCTYAEB4ejri4OPj5+bU6VlxcDADIyMiAwWBAjx49Wj1OQ0MDgoODHVM0Edkdww0RuSxPT89WXwuC0OYxo9EIAKiuroZSqcSBAwegVCpbnXd+ICIi18ZwQ0ROSaVStRoIbAsDBgyAwWBAcXExRo4cadPHJiLnwdlSROSU4uLisGfPHpw6dQqlpaWW1pcr0aNHD9x1111ITk7GypUrkZOTg7179yIlJQVr1661QdVE5AwYbojIKT399NNQKpXo1asXQkNDkZeXZ5PH/fzzz5GcnIynnnoKCQkJmDp1Kvbt24dOnTrZ5PGJSHpcoZiIiIhkhS03REREJCsMN0RERCQrDDdEREQkKww3REREJCsMN0RERCQrDDdEREQkKww3REREJCsMN0RERCQrDDdEREQkKww3REREJCsMN0RERCQr/w/9GTPZftCcqwAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Instead of under-shooting we now have over-shooting,\n", + "# but at least the \"spline\" is always positive\n", + "spline.plot(xlabel='time');" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The spline annotation in this case is\n", + "```xml\n", + "\n", + "\t ... \n", + "\t ... \n", + "\t ... \n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Comparing model import time for the SBML-native piecewise implementation and the AMICI spline implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import seaborn as sns\n", + "import tempfile\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "nruns = 6 # number of replicates\n", + "num_nodes = [5, 10, 15, 20, 25, 30, 40] # benchmark model import for these node numbers\n", + "amici_only_nodes = [50, 75, 100, 125, 150, 175, 200, 225, 250] # for these node numbers, only benchmark the annotation-based implementation" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "# If running as a Github action, just do the minimal amount of work required to check whether the code is working\n", + "if os.getenv('GITHUB_ACTIONS') is not None:\n", + " nruns = 1\n", + " num_nodes = [4]\n", + " amici_only_nodes = [5]" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "df = None\n", + "for n in num_nodes + amici_only_nodes:\n", + " # Create model\n", + " spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='f',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=n),\n", + " values_at_nodes=np.random.rand(n),\n", + " )\n", + " sbml_doc = libsbml.SBMLReader().readSBML('example_splines.xml')\n", + " sbml_model = sbml_doc.getModel()\n", + " spline.add_to_sbml_model(sbml_model)\n", + " # Benchmark model creation\n", + " timings_amici = []\n", + " timings_piecewise = []\n", + " for _ in range(nruns):\n", + " with tempfile.TemporaryDirectory() as tmpdir:\n", + " t0 = time.perf_counter_ns()\n", + " amici.SbmlImporter(sbml_model).sbml2amici('benchmark', tmpdir)\n", + " dt = time.perf_counter_ns() - t0\n", + " timings_amici.append(dt / 1e9)\n", + " if n in num_nodes:\n", + " with tempfile.TemporaryDirectory() as tmpdir:\n", + " t0 = time.perf_counter_ns()\n", + " amici.SbmlImporter(sbml_model, discard_annotations=True).sbml2amici('benchmark', tmpdir)\n", + " dt = time.perf_counter_ns() - t0\n", + " timings_piecewise.append(dt / 1e9)\n", + " # Append benchmark data to dataframe\n", + " df_amici = pd.DataFrame(dict(num_nodes=n, time=timings_amici, use_annotations=True))\n", + " df_piecewise = pd.DataFrame(dict(num_nodes=n, time=timings_piecewise, use_annotations=False))\n", + " if df is None:\n", + " df = pd.concat([df_amici, df_piecewise], ignore_index=True, verify_integrity=True)\n", + " else:\n", + " df = pd.concat([df, df_amici, df_piecewise], ignore_index=True, verify_integrity=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "kwargs = dict(markersize=7.5)\n", + "df_avg = df.groupby(['use_annotations', 'num_nodes']).mean().reset_index()\n", + "fig, ax = plt.subplots(1, 1, figsize=(6.5, 3.5))\n", + "ax.plot(df_avg[np.logical_not(df_avg['use_annotations'])]['num_nodes'], df_avg[np.logical_not(df_avg['use_annotations'])]['time'], '.', label='MathML piecewise', **kwargs)\n", + "ax.plot(df_avg[df_avg['use_annotations']]['num_nodes'], df_avg[df_avg['use_annotations']]['time'], '.', label='AMICI annotations', **kwargs)\n", + "ax.set_ylabel('model import time (s)')\n", + "ax.set_xlabel('number of spline nodes')\n", + "ax.set_yscale('log')\n", + "ax.yaxis.set_major_formatter(mpl.ticker.FuncFormatter(lambda x, pos: f\"{x:.0f}\"))\n", + "ax.xaxis.set_ticks([10, 20, 30, 40, 60, 70, 80, 90, 110, 120, 130, 140, 160, 170, 180, 190, 210, 220, 230, 240, 260], minor=True)\n", + "ax.yaxis.set_ticks([20, 30, 40, 50, 60, 70, 80, 90, 200, 300, 400], ['20', '30', '40', '50', None, None, None, None, '200', '300', '400'], minor=True)\n", + "ax.legend()\n", + "ax.figure.tight_layout()\n", + "#ax.figure.savefig('benchmark_import.pdf')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/python/examples/example_splines/example_splines.xml b/python/examples/example_splines/example_splines.xml new file mode 100644 index 0000000000..2ff6a8d73f --- /dev/null +++ b/python/examples/example_splines/example_splines.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + f + + + + + diff --git a/python/examples/example_splines_swameye/ExampleSplinesSwameye2003.ipynb b/python/examples/example_splines_swameye/ExampleSplinesSwameye2003.ipynb new file mode 100644 index 0000000000..6ef92d4c49 --- /dev/null +++ b/python/examples/example_splines_swameye/ExampleSplinesSwameye2003.ipynb @@ -0,0 +1,1962 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c10af447-e4f1-46e1-bf60-910dc67f5d77", + "metadata": { + "tags": [] + }, + "source": [ + "# Spline implementation of JAK2-STAT5 signaling pathway\n", + "In this notebook a practical example of the usage of AMICI spline functionalities is shown.\n", + "The model under consideration is the JAK2-STAT5 signaling pathway ([Swameye et al., 2003](https://doi.org/10.1073/pnas.0237333100)),\n", + "in which the dynamics of the system depend on a measured input function (the quantity `pEpoR` in the model).\n", + "\n", + "Following the approach of ([Schelker et al., 2012](https://doi.org/10.1093/bioinformatics/bts393)), a continuous approximation of this input function is estimated together with the other parameters.\n", + "As in the original paper, we will use a spline with logarithmic parameterization in order to enforce the positivity constraint.\n", + "\n", + "The model of the signaling pathway will be implemented in SBML using AMICI's spline annotations, experimental data integrated using the PEtab format and parameter estimation will be carried out using the [pyPESTO](https://pypesto.readthedocs.io/) library." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7323a4fb", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install pypesto" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78dbeefe", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install fides" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5f94310d-d95c-4fb5-bd1e-a29bb1c92a30", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import os\n", + "import math\n", + "import logging\n", + "import contextlib\n", + "import multiprocessing\n", + "import copy\n", + "\n", + "import numpy as np\n", + "import sympy as sp\n", + "import pandas as pd\n", + "from matplotlib import pyplot as plt\n", + "\n", + "import libsbml\n", + "import amici\n", + "import petab\n", + "import pypesto\n", + "import pypesto.petab" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "02162090-1008-4eb7-b954-c10370aae9c5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Number of multi-starts for MAP estimation\n", + "n_starts = 150\n", + "# n_starts = 0 # when loading results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2a366ac", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Set default pypesto engine/optimizer\n", + "pypesto_optimizer = pypesto.optimize.FidesOptimizer(verbose=logging.WARNING)\n", + "pypesto_engine = pypesto.engine.MultiProcessEngine()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "21eca425", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# If running as a Github action, just do the minimal amount of work required to check whether the code is working\n", + "if os.getenv('GITHUB_ACTIONS') is not None:\n", + " n_starts = 15\n", + " pypesto_optimizer = pypesto.optimize.FidesOptimizer(verbose=logging.WARNING, options=dict(maxiter=10))\n", + " pypesto_engine = pypesto.engine.SingleCoreEngine()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "ff552b79-96a1-42b4-a5cb-a9f2143f10bb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# A dictionary to store different approaches for a final comparison\n", + "all_results = {}" + ] + }, + { + "cell_type": "markdown", + "id": "3ab1206d-f0dd-4b77-a668-be3076c28fa3", + "metadata": { + "tags": [] + }, + "source": [ + "## Spline approximation with few nodes, using finite differences for the derivatives\n", + "As a first attempt, we fix a small amount of nodes, create new parameters for the values of the splines at the nodes and let AMICI compute the derivative at the nodes by using finite differences." + ] + }, + { + "cell_type": "markdown", + "id": "5684adb0-4c66-48d3-9076-270839a77a5f", + "metadata": {}, + "source": [ + "### Creating the PEtab model" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "c796cf8b-d7e9-473a-a368-b8919ef93efe", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem name\n", + "name = \"Swameye_PNAS2003_5nodes_FD\"" + ] + }, + { + "cell_type": "markdown", + "id": "c684a2e4-e4e8-4f4a-aa06-c1c9378a3bce", + "metadata": {}, + "source": [ + "First, we create a spline to represent the input function `pEpoR`, parametrized by its values at the nodes.\n", + "Since the value of the input function reaches its steady state by the end of the experiment, we extrapolate constantly after that (useful if we need to simulate the model after the last spline node)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "68562fc1-7213-4dac-9177-61fba454f9d4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create spline for pEpoR\n", + "nodes = [0, 5, 10, 20, 60]\n", + "values_at_nodes = [sp.Symbol(f\"pEpoR_t{str(t).replace('.', '_dot_')}\") for t in nodes] # new parameter symbols for spline values\n", + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='pEpoR', # matches name of species in SBML model\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol, # the spline is evaluated at the current time\n", + " nodes=nodes,\n", + " values_at_nodes=values_at_nodes, # values at the nodes (in linear scale)\n", + " extrapolate=(None, \"constant\"), # because steady state is reached\n", + " bc=\"auto\", # automatically determined from extrapolate (bc at right end will be 'zero_derivative')\n", + " logarithmic_parametrization=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "4a3de7bd-3157-4950-a264-cf8ff8f2250e", + "metadata": {}, + "source": [ + "We can then add the spline to a skeleton SBML model based on the d2d implementation by (Schelker et al., 2012).\n", + "The skeleton SBML model defines a species `pEpoR` which interacts with the other species,\n", + "but has no reactions or rate rules of its own.\n", + "The code below creates an assignment rule for `pEpoR` using the spline formula, completing the model.\n", + "The parameters `pEpoR_t*` are automatically added to the SBML model too (using nominal values of `0.1` and declaring them to be constant)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "118feb66-b3d3-46d9-9b4b-5a3f43586a64", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add spline formula to SBML model\n", + "sbml_doc = libsbml.SBMLReader().readSBML(os.path.join('Swameye_PNAS2003', 'swameye2003_model.xml'))\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model, auto_add=True, y_nominal=0.1, y_constant=True)" + ] + }, + { + "cell_type": "markdown", + "id": "dedc2418-dc8b-4f54-84f3-22dc26f2846b", + "metadata": {}, + "source": [ + "A skeleton PEtab problem is provided, containing parameter bounds, observable definitions and experimental data.\n", + "Of particular relevance is the noise model used for the measurements of `pEpoR`, normal additive noise with standard deviation equal to `0.0274 + 0.1 * pEpoR`;\n", + "this is the same choice used in (Schelker et al., 2012), where it was estimated from experimental replicates.\n", + "\n", + "However, the parameters associated to the spline are to be added too.\n", + "The code below defines parameter bounds for them according to the PEtab format and then creates a full PEtab problem integrating them together with the edited SBML file.\n", + "The condition, measurement and observable PEtab tables do not require additional modification and can be used as they are." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5c2f1c0b-72fe-4df4-b25c-525e36f66514", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Extra parameters associated to the spline\n", + "spline_parameters_df = pd.DataFrame(\n", + " dict(parameterScale='log', lowerBound=0.001, upperBound=10, nominalValue=0.1, estimate=1),\n", + " index=pd.Series(list(map(str, values_at_nodes)), name=\"parameterId\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "f832bfe0-ddda-48fc-9a73-6e62db19b445", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create PEtab problem\n", + "petab_problem = petab.Problem(\n", + " sbml_model,\n", + " condition_df=petab.conditions.get_condition_df(os.path.join('Swameye_PNAS2003', 'swameye2003_conditions.tsv')),\n", + " measurement_df=petab.measurements.get_measurement_df(os.path.join('Swameye_PNAS2003', 'swameye2003_measurements.tsv')),\n", + " parameter_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_parameters.tsv'), spline_parameters_df],\n", + " petab.parameters.get_parameter_df\n", + " ),\n", + " observable_df=petab.observables.get_observable_df(os.path.join('Swameye_PNAS2003', 'swameye2003_observables.tsv')),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "c69bc77d-3efc-4ac5-bde4-c0e43219f560", + "metadata": {}, + "source": [ + "The resulting PEtab problem can be checked for errors and exported to disk if needed." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3c10e6ad-c891-43ff-b1d7-199943bdfa4b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether PEtab model is valid\n", + "assert not petab.lint_problem(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0f2ec7eb-b4af-46ac-a96e-6e484f774883", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save PEtab problem to disk\n", + "# import shutil\n", + "# shutil.rmtree(name, ignore_errors=True)\n", + "# os.mkdir(name)\n", + "# petab_problem.to_files_generic(prefix_path=name)" + ] + }, + { + "cell_type": "markdown", + "id": "7ebcbcc9-82d9-44cf-a0ad-2a80711cb382", + "metadata": {}, + "source": [ + "### Creating the pyPESTO problem\n", + "We can now create a pyPESTO problem directly from the PEtab problem.\n", + "Due to technical limitations in AMICI, currently the PEtab problem has to be \"flattened\" before it can be simulated from, but such operation is merely syntactical and thus does not change the essence of the model." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "5a46a020-c670-4f52-9bc7-a1dd74faedd0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem must be \"flattened\" to be used with AMICI\n", + "petab.core.flatten_timepoint_specific_output_overrides(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fab8bd39-eb26-4462-b158-0fae8a8ba027", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether simulation from the PEtab problem works\n", + "# import amici.petab_simulate\n", + "# simulator = amici.petab_simulate.PetabSimulator(petab_problem)\n", + "# simulator.simulate(noise=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1beb3a3e-6966-4d3e-acc5-546c1932df9f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Import PEtab problem into pyPESTO\n", + "pypesto_problem = pypesto.petab.PetabImporter(petab_problem, model_name=name).create_problem()" + ] + }, + { + "cell_type": "markdown", + "id": "715a8a22-878c-4c37-98e8-7f3898a2a2dc", + "metadata": {}, + "source": [ + "### Maximum Likelihood estimation\n", + "Using pyPESTO we can optimize for the parameter vector that maximizes the probability of observing the experimental data (maximum likelihood estimation).\n", + "\n", + "A multistart method with local gradient-based optimization is used and the results of each multistart can be visualized in a waterfall plot." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bf1e3a52-8f72-466e-b63c-2f15fbe59137", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Load existing results if available\n", + "if os.path.exists(f'{name}.h5'):\n", + " pypesto_result = pypesto.store.read_result(f'{name}.h5', problem=pypesto_problem)\n", + "else:\n", + " pypesto_result = None\n", + "# Overwrite\n", + "# pypesto_result = None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6e20c36-d6fe-4264-b366-c23e707246ff", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Parallel multistart optimization with pyPESTO and FIDES\n", + "if n_starts > 0:\n", + " if pypesto_result is None:\n", + " new_ids = [str(i) for i in range(n_starts)]\n", + " else:\n", + " last_id = max(int(i) for i in pypesto_result.optimize_result.id)\n", + " new_ids = [str(i) for i in range(last_id+1, last_id+n_starts+1)]\n", + " pypesto_result = pypesto.optimize.minimize(\n", + " pypesto_problem,\n", + " n_starts=n_starts,\n", + " ids=new_ids,\n", + " optimizer=pypesto_optimizer,\n", + " engine=pypesto_engine,\n", + " result=pypesto_result,\n", + " )\n", + " pypesto_result.optimize_result.sort()\n", + " if pypesto_result.optimize_result.x[0] is None:\n", + " raise Exception(\"All multistarts failed (n_starts is probably too small)! If this error occurred during CI, just run the workflow again.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "0592af7c-227a-4e52-b383-d43061a4c52f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save results to disk\n", + "# pypesto.store.write_result(pypesto_result, f'{name}.h5', overwrite=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "7762a31f-55e8-4e12-8107-3f3e6f0af5e5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Print result table\n", + "# pypesto_result.optimize_result.as_dataframe()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2cfbcf68-6d13-49eb-b18f-26d9d32e79ae", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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vq5du+K7775DQEAAbGwM83Xs7u6OjIwM9XZKSgru3r1rkGsRNUQcNUbUgK1duxYlJSWIiIjA0aNHkZaWhn379mHQoEFo2rSpRrNUZaZMmYJ9+/ZhxYoVSElJwdq1azWaxQBg3rx5+PLLL7Fw4UIkJibiwoUL2LZtG955551qz13T+8LDw9GmTRtERkbizz//xG+//aYxyq0ydnZ2mDx5MmbOnIl9+/YhKSkJkyZNwt27d/Hvf/9b49hFixYhJiYG58+fx/jx4+Hm5oaRI0cCeDA6rLCwEDExMcjJyTFYYpCZmYlRo0bh2WefRUREBDIzM5GZmYm//voLwINRf7du3cJzzz2HuLg4XLlyBfv378eECRP0lqgNGDAAa9euxZkzZ3Dq1Cm88sorGrU/RJaOiRBRA9a6dWucOnUKLVq0wNNPP42WLVvipZdeQv/+/REbGwsXF5dq39+rVy9s2LABH330EYKDg3HgwIEKCU5ERAR+/PFHHDhwAN27d0evXr2watUqdY1MVWp6n5WVFXbt2oV79+6hR48eePHFF2tM3ADggw8+wJNPPomxY8eia9euuHz5Mvbv3w9nZ+cKx02dOhUhISHIzMzEnj171P1nwsLC8Morr+CZZ56Bu7s7li1bVuN1a+PixYvIysrCF198AW9vb/Wre/fuAAAfHx8cO3YMSqUSgwcPRseOHTFt2jQ0adIEVlb6+Xr+8MMP4evri759++L555/HjBkz6jxXEpE5kQhdeloSEZm4w4cPo3///rh9+7ZRlwohooaBNUJERERksZgIERERkcVi0xgRERFZLNYIERERkcViIkREREQWi4kQERERWSwmQkRERGSxmAgRERGRxWIiRERERBaLiRARERFZLCZCREREZLGYCBEREZHF+n/XWu5nHsrgTAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the results of the multistarts\n", + "pypesto.visualize.waterfall(pypesto_result, size=[6.5, 3.5]);" + ] + }, + { + "cell_type": "markdown", + "id": "5b58b02a-d09c-4974-862a-0fbd29ce624b", + "metadata": {}, + "source": [ + "Below the maximum likelihood estimates for `pEpoR` and the other observables are plotted, together with the experimental measurements.\n", + "\n", + "To assess whether the noise model used in the observable is reasonable, we have also plotted 2-sigma error bands for `pEpoR`." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "6464be7c-1365-4bf6-8625-cc7cf5a9c988", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Functions for simulating observables given a parameter vector\n", + "def _simulate(x=None, *, problem=None, result=None, N=500, **kwargs):\n", + " if result is None:\n", + " result = pypesto_result\n", + " if problem is None:\n", + " problem = pypesto_problem\n", + " if x is None:\n", + " x = result.optimize_result.x[0]\n", + " if N is None:\n", + " objective = problem.objective\n", + " else:\n", + " objective = problem.objective.set_custom_timepoints(timepoints_global=np.linspace(0, 60, N))\n", + " if len(x) != len(problem.x_free_indices):\n", + " x = x[problem.x_free_indices]\n", + " simresult = objective(x, return_dict=True, **kwargs)\n", + " return problem, simresult['rdatas'][0]\n", + "\n", + "def simulate_pEpoR(x=None, **kwargs):\n", + " problem, rdata = _simulate(x, **kwargs)\n", + " assert problem.objective.amici_model.getObservableIds()[0].startswith('pEpoR')\n", + " return rdata['t'], rdata['y'][:, 0]\n", + "\n", + "def simulate_pSTAT5(x=None, **kwargs):\n", + " problem, rdata = _simulate(x, **kwargs)\n", + " assert problem.objective.amici_model.getObservableIds()[1].startswith('pSTAT5')\n", + " return rdata['t'], rdata['y'][:, 1]\n", + "\n", + "def simulate_tSTAT5(x=None, **kwargs):\n", + " problem, rdata = _simulate(x, **kwargs)\n", + " assert problem.objective.amici_model.getObservableIds()[-1].startswith('tSTAT5')\n", + " return rdata['t'], rdata['y'][:, -1]\n", + "\n", + "# Experimental data\n", + "df_measurements = petab.measurements.get_measurement_df(os.path.join('Swameye_PNAS2003', 'swameye2003_measurements.tsv'))\n", + "df_pEpoR = df_measurements[df_measurements['observableId'].str.startswith('pEpoR')]\n", + "df_pSTAT5 = df_measurements[df_measurements['observableId'].str.startswith('pSTAT5')]\n", + "df_tSTAT5 = df_measurements[df_measurements['observableId'].str.startswith('tSTAT5')]" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "1fca8d58-1d37-48cb-8880-e9bacf256117", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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DL169YJWq0VkZCRee+21OgVYkydPhlQqxfTp01FQUIC2bdti7969aNmyJQDA3d0d+/btw0cffYROnTqhbdu2+OabbzBw4MBa160294/UDodpZv1FMpkMEokEUqm02eb/pKWloWfPnkhPT0e3bt0q7D9w4AD69u2LHTt2IC4urtqyHj16hNjYWJhMJpw+fRrR0dGOqjaAx8Pcx40bh4SEBCQlJSEmJgY3b97EypUrcfToUaxdu7bCMPu1a9di/vz5CAkJwenTp61epLW8kpIShIWFwcvLq46vhJC60Wg0yMzMRHR0tEUSbWM2atQolJWVYc+ePc6uCmmgqvu7t+bzvVGN9iL2YZ6Yq127dhX2mUwmhIWFAUCVM22Xt3PnTphMJsTGxjo88AGAPn36YO3atbh58yb69++P1q1bo3///sjIyKg08AEed58GBwfj0aNHbGK2rQQCAUpLS2m9L0IIacQo+GmGgoODATyeSfRJGo2GXRXdnDtTFYZhsH37dgCPuzDrS58+ffD7779jx44dWLVqFXbs2IHTp09XObGiUChkF0Jcvnx5ndbqEolEUKlUtN4XIYQ0YhT8NEM9evRAVFQUvv766wotGEqlEuvWrauQO1OZixcv4v79+xCLxejbt68jq1wBj8dDXFwcEhMTERcXV2Ou0ZAhQxAVFYWioiKsX7/e5utyuVxwuVwa9k6IA2zcuJG6vEi9oOCnGeLxeFiyZAn279+PxMREdrTXmTNnMHToUJw8eZKdJLA6u3btAvC4JcbWIaL1xdXVlV309IcffqjTqC3zel807J0QQhonCn6aqQEDBmDnzp24evUq4uLi4Onpifj4eGRkZGD16tU1rs2l1+vZuXMGDBhQH1Wus/79+6NFixYoKyvDhg0bbC7H1dUVRqORhr2TBoFaIElzYq+/dwp+mrEBAwbgzp07OHHiBLZs2YJ9+/Zh//796NevX43nnjp1CqWlpfDz86swpXtDxePx2NyfH374oU6LlZqHvev1ejvVjhDrmFtmq1vxnJCmxpxvWdfpRpw6zw9xPh6Ph5deegkAkJOTU+uAwNwv/+abb9Z6CYyG4M0338TSpUtx7949bNy4EUlJSTaVIxQKUVJSAoVCAW9vbzvXkpCaubi4QCwWo7CwEK6urnabBJCQhohhGKhUKhQUFMDLy6vOc8rRPD8EwONurPv378PF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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "t, pEpoR = simulate_pEpoR()\n", + "sigma_pEpoR = 0.0274 + 0.1 * pEpoR\n", + "ax.fill_between(t, pEpoR - 2*sigma_pEpoR, pEpoR + 2*sigma_pEpoR, color='black', alpha=0.10, interpolate=True, label='2-sigma error bands')\n", + "ax.plot(t, pEpoR, color='black', label='MLE')\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "75c999c0-af7b-4ee2-9e14-52c1c7d2e5bc", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pSTAT5\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "t, pSTAT5 = simulate_pSTAT5()\n", + "ax.plot(t, pSTAT5, color='black', label='MLE')\n", + "ax.plot(df_pSTAT5['time'], df_pSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pSTAT5\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "226588b1-32b4-4aba-beac-fd70f0e79697", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for tSTAT5\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "t, tSTAT5 = simulate_tSTAT5()\n", + "ax.plot(t, tSTAT5, color='black', label='MLE')\n", + "ax.plot(df_tSTAT5['time'], df_tSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"tSTAT5\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "65bcf2fe-0189-4f92-bee1-d4229717c535", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Store results for later\n", + "all_results['5 nodes, FD'] = (pypesto_problem, pypesto_result)" + ] + }, + { + "cell_type": "markdown", + "id": "ec09ff5d-d370-4fac-ab90-6806a2d5945a", + "metadata": { + "tags": [] + }, + "source": [ + "## Spline approximation with many nodes, using finite differences for the derivatives\n", + "Five nodes is arguably not enough to represent all plausible input choices. Increasing the number of nodes would give the spline more freedom and it can be done with minimal changes to the example above. However, more degrees of freedom mean more chance of overfitting. Thus, following (Schelker et al., 2012), we will add a regularization term consisting in the squared L2 norm of the spline's curvature, which promotes smoother and less oscillating functions. The value for the regularization strength $\\lambda$ is chosen by comparing the sum of squared normalized residuals with its expected value, which can be computing by assuming it is roughly $\\chi^2$-distributed." + ] + }, + { + "cell_type": "markdown", + "id": "f0e8d4ef-33f4-4e4a-8754-539251aa4d11", + "metadata": {}, + "source": [ + "### Creating the PEtab model" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "9ea0c348-88d1-4d0e-845f-dd1dc1a43edc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem name\n", + "name = \"Swameye_PNAS2003_15nodes_FD\"" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "cefd1af3-dac6-4e0d-92a0-33016dce168d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create spline for pEpoR\n", + "nodes = [0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20, 25, 30, 35, 40, 50, 60]\n", + "values_at_nodes = [sp.Symbol(f\"pEpoR_t{str(t).replace('.', '_dot_')}\") for t in nodes]\n", + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='pEpoR',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=nodes,\n", + " values_at_nodes=values_at_nodes,\n", + " extrapolate=(None, \"constant\"),\n", + " bc=\"auto\",\n", + " logarithmic_parametrization=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "0851dd99-83e8-4e7c-8678-7f0914d2bf01", + "metadata": {}, + "source": [ + "The regularization term can be easily computed by symbolic manipulation of the spline expression using AMICI and SymPy. Since it is very commonly used, we already provide a function for it in AMICI. Note: we regularize the curvature of the spline, which for positivity-enforcing spline is the logarithm of the function.\n", + "\n", + "In order add the regularization term to the PEtab likelihood, a dummy observable has to be created." + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "525ed7b4-999d-40ad-9383-8f5c1157088f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Compute L2 norm of the curvature of pEpoR\n", + "regularization = spline.squared_L2_norm_of_curvature()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "46182f2e-a8a4-4a7f-8072-e8034ed09c1a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add a parameter for regularization strength\n", + "reg_parameters_df = pd.DataFrame(\n", + " dict(parameterScale='log10', lowerBound=1e-6, upperBound=1e6, nominalValue=1.0, estimate=0),\n", + " index=pd.Series(['regularization_strength'], name=\"parameterId\"),\n", + ")\n", + "# Encode regularization term as an additional observable\n", + "reg_observables_df = pd.DataFrame(\n", + " dict(observableFormula=f'sqrt({regularization})'.replace('**', '^'), observableTransformation='lin', noiseFormula='1/sqrt(regularization_strength)', noiseDistribution='normal'),\n", + " index=pd.Series(['regularization'], name=\"observableId\"),\n", + ")\n", + "# and correspoding measurement\n", + "reg_measurements_df = pd.DataFrame(\n", + " dict(observableId='regularization', simulationConditionId='condition1', measurement=0, time=0, observableTransformation='lin'),\n", + " index=pd.Series([0]),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "c8214d30-1cb8-4575-9fef-2e485cabf319", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add spline formula to SBML model\n", + "sbml_doc = libsbml.SBMLReader().readSBML(os.path.join('Swameye_PNAS2003', 'swameye2003_model.xml'))\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model, auto_add=True, y_nominal=0.1, y_constant=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "a75cf7db-918e-4048-a77b-082cab06dcb8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Extra parameters associated to the spline\n", + "spline_parameters_df = pd.DataFrame(\n", + " dict(parameterScale='log', lowerBound=0.001, upperBound=10, nominalValue=0.1, estimate=1),\n", + " index=pd.Series(list(map(str, values_at_nodes)), name=\"parameterId\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "4fdd82f8-adc5-471f-aa80-6856834e6913", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create PEtab problem\n", + "petab_problem = petab.Problem(\n", + " sbml_model,\n", + " condition_df=petab.conditions.get_condition_df(os.path.join('Swameye_PNAS2003', 'swameye2003_conditions.tsv')),\n", + " measurement_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_measurements.tsv'), reg_measurements_df],\n", + " petab.measurements.get_measurement_df\n", + " ).reset_index(drop=True),\n", + " parameter_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_parameters.tsv'), spline_parameters_df, reg_parameters_df],\n", + " petab.parameters.get_parameter_df\n", + " ),\n", + " observable_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_observables.tsv'), reg_observables_df],\n", + " petab.observables.get_observable_df\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "43e47476-8f75-4d63-a57d-09b9b55f086b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether PEtab model is valid\n", + "assert not petab.lint_problem(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "e4f9f7a8-dcab-431e-8f80-e10605bdb69c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save PEtab problem to disk\n", + "# import shutil\n", + "# shutil.rmtree(name, ignore_errors=True)\n", + "# os.mkdir(name)\n", + "# petab_problem.to_files_generic(prefix_path=name)" + ] + }, + { + "cell_type": "markdown", + "id": "784aebee-b501-4fa8-808e-ff6277c9432c", + "metadata": {}, + "source": [ + "### Creating the pyPESTO problem" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "19654099-93d0-469e-bb03-82f1d9319419", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem must be \"flattened\" to be used with AMICI\n", + "petab.core.flatten_timepoint_specific_output_overrides(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "fd8e3502-f9de-457e-9652-95bea94c0972", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether simulation from the PEtab problem works\n", + "# import amici.petab_simulate\n", + "# simulator = amici.petab_simulate.PetabSimulator(petab_problem)\n", + "# simulator.simulate(noise=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "a7104b34-d33c-46d8-afa5-1278070d6af1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Import PEtab problem into pyPESTO\n", + "pypesto_problem = pypesto.petab.PetabImporter(petab_problem, model_name=name).create_problem()" + ] + }, + { + "cell_type": "markdown", + "id": "0c890937-c9e8-4617-992f-65d39479ed55", + "metadata": {}, + "source": [ + "### Maximum Likelihood estimation\n", + "We will optimize the problem for different values of the regularization strength $\\lambda$, then compute the sum of squared normalized residuals for each of the resulting parameter vectors. The one for which such a value is nearest to its expected value of $15$ (the number of observations from the input function) will be chosen as the final estimate." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "116f6d10-9aba-4db7-88fb-f503b1b5d408", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Try different regularization strengths\n", + "regstrengths = np.asarray([1, 10, 40, 75, 150, 500])\n", + "if os.getenv('GITHUB_ACTIONS') is not None:\n", + " regstrengths = np.asarray([75])\n", + "regproblems = {}\n", + "regresults = {}\n", + "\n", + "for regstrength in regstrengths:\n", + " # Fix parameter in pypesto problem\n", + " name = f\"Swameye_PNAS2003_15nodes_FD_reg{regstrength}\"\n", + " pypesto_problem.fix_parameters(\n", + " pypesto_problem.x_names.index('regularization_strength'),\n", + " np.log10(regstrength) # parameter is specified as log10 scale in PEtab\n", + " )\n", + " regproblem = copy.deepcopy(pypesto_problem)\n", + "\n", + " # Load existing results if available\n", + " if os.path.exists(f'{name}.h5'):\n", + " regresult = pypesto.store.read_result(f'{name}.h5', problem=regproblem)\n", + " else:\n", + " regresult = None\n", + " # Overwrite\n", + " # regresult = None\n", + "\n", + " # Parallel multistart optimization with pyPESTO and FIDES\n", + " if n_starts > 0:\n", + " if regresult is None:\n", + " new_ids = [str(i) for i in range(n_starts)]\n", + " else:\n", + " last_id = max(int(i) for i in regresult.optimize_result.id)\n", + " new_ids = [str(i) for i in range(last_id+1, last_id+n_starts+1)]\n", + " regresult = pypesto.optimize.minimize(\n", + " regproblem,\n", + " n_starts=n_starts,\n", + " ids=new_ids,\n", + " optimizer=pypesto_optimizer,\n", + " engine=pypesto_engine,\n", + " result=regresult,\n", + " )\n", + " regresult.optimize_result.sort()\n", + " if regresult.optimize_result.x[0] is None:\n", + " raise Exception(\"All multistarts failed (n_starts is probably too small)! If this error occurred during CI, just run the workflow again.\")\n", + "\n", + " # Save results to disk\n", + " # pypesto.store.write_result(regresult, f'{name}.h5', overwrite=True)\n", + "\n", + " # Store result\n", + " regproblems[regstrength] = regproblem\n", + " regresults[regstrength] = regresult" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "5d3ef681-81f0-423d-9b78-18f3b3939adb", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target value is 15\n", + "Regularization strength: 1. Statistic is 6.794369874307712\n", + "Regularization strength: 10. Statistic is 8.435094498146606\n", + "Regularization strength: 40. Statistic is 11.83872830962955\n", + "Regularization strength: 75. Statistic is 15.030926511510327\n", + "Regularization strength: 150. Statistic is 19.971139477161476\n", + "Regularization strength: 500. Statistic is 32.44623424533765\n" + ] + } + ], + "source": [ + "# Compute sum of squared normalized residuals\n", + "print(f\"Target value is {len(df_pEpoR['time'])}\")\n", + "regstrengths = sorted(regproblems.keys())\n", + "stats = []\n", + "for regstrength in regstrengths:\n", + " t, pEpoR = simulate_pEpoR(N=None, problem=regproblems[regstrength], result=regresults[regstrength])\n", + " assert np.array_equal(df_pEpoR['time'], t[:-1])\n", + " pEpoR = pEpoR[:-1]\n", + " sigma_pEpoR = 0.0274 + 0.1 * pEpoR\n", + " stat = np.sum(((pEpoR - df_pEpoR['measurement']) / sigma_pEpoR)**2)\n", + " print(f\"Regularization strength: {regstrength}. Statistic is {stat}\")\n", + " stats.append(stat)\n", + "# Select best regularization strength\n", + "chosen_regstrength = regstrengths[np.abs(np.asarray(stats) - len(df_pEpoR['time'])).argmin()]" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "6e362c17-4222-48ce-8db1-9f9956078e2b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the results of the multistarts for a chosen regularization strength\n", + "ax = pypesto.visualize.waterfall(regresults[chosen_regstrength], size=[6.5, 3.5])\n", + "ax.set_title(f\"Waterfall plot (regularization strength = {chosen_regstrength})\")\n", + "ax.set_ylim(ax.get_ylim()[0], 100);" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "9a4fb86c-4b74-43f8-97d8-eba60529abaf", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, pEpoR = simulate_pEpoR(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, pEpoR, **kwargs)\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.set_xlim(-3.0, 63.0)\n", + "ax.set_ylim(-0.05299052022388704, 1.126290214024833)\n", + "ax.legend()\n", + "ax.figure.tight_layout()\n", + "# ax.set_ylabel(\"input function\")\n", + "# print(f\"xlim = {ax.get_xlim()}, ylim = {ax.get_ylim()}\")\n", + "# ax.figure.savefig('fit_15nodes_lambdas.pdf')" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "18f79f00-265a-4cb8-a462-e4606636612b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pSTAT5 (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, pSTAT5 = simulate_pSTAT5(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, pSTAT5, **kwargs)\n", + "ax.plot(df_pSTAT5['time'], df_pSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pSTAT5\");\n", + "#ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "ac882963-7714-4536-b974-da0b642d79a9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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du1asrKyupenpaURCASw2+09th/MbabX68nPz2fSpEksWbKECRMm8Pjjj0tbHcfWrVtpbm7m/PPPR6vVotVqWb9+PX/84x/RarWkpqZKm32BuLg4RowYwcGDB+V37DjS09MZM2ZMj2WjR4/uvlXYHz73Jfz0Ar1ez6RJk1izZk33skgkwpo1a5g2bVoUK+v/8vLySEtL69F2TqeTjRs3Dtq2UxSFO++8k1dffZUPPviAvLy8HusnTZqETqfr0Wbl5eXU1NQM2jb7b5FIBL/fL211HJdeeim7d+9mx44d3a8LLriAb37zm91/lzY7uc7OTg4dOkR6err8jh3HjBkzjhme48CBAwwZMgToJ5/7fdKtehB4+eWXFYPBoDz//PPK3r17le9///tKXFyc0tjYGO3Sos7lcinbt29Xtm/frgDKH/7wB2X79u3K4cOHFUVRlN/97ndKXFyc8tprrym7du1S5s+fr+Tl5SlerzfKlUfH7bffrthsNmXdunVKQ0ND98vj8XRv84Mf/EDJyclRPvjgA2XLli3KtGnTlGnTpkWx6ui57777lPXr1ytVVVXKrl27lPvuu09RqVTKu+++qyiKtNWp+PzTXooibfbf7rnnHmXdunVKVVWV8vHHHytz5sxRkpKSlObmZkVRpL3+26ZNmxStVqv89re/VSoqKpR//etfitlsVl588cXubaL9uS/hpxc98cQTSk5OjqLX65XJkycrGzZsiHZJ/cLatWsV4JjXzTffrChK12OPDzzwgJKamqoYDAbl0ksvVcrLy6NbdBQdr60A5bnnnuvexuv1Kj/84Q+V+Ph4xWw2K9dee63S0NAQvaKj6Lvf/a4yZMgQRa/XK8nJycqll17aHXwURdrqVPx3+JE26+n6669X0tPTFb1er2RmZirXX3+9cvDgwe710l7HeuONN5Rx48YpBoNBGTVqlPKXv/ylx/pof+6rFEVR+uYakxBCCCFE9EmfHyGEEEIMKhJ+hBBCCDGoSPgRQgghxKAi4UcIIYQQg4qEHyGEEEIMKhJ+hBBCCDGoSPgRQgghxKAi4UcIIYQQg4qEHyHEOWndunWoVKpjJpQUQogvIiM8CyHOCbNmzeK8887jscceAyAQCNDe3k5qaioqlSq6xQkhzinaaBcghBBfhl6vJy0tLdplCCHOQXLbSwjR733nO99h/fr1PP7446hUKlQqFc8//3yP217PP/88cXFxrFq1ipEjR2I2mykqKsLj8fDCCy+Qm5tLfHw8P/rRjwiHw93H9vv93HvvvWRmZhITE8OUKVNYt25ddE5UCNEn5MqPEKLfe/zxxzlw4ADjxo3j4YcfBmDPnj3HbOfxePjjH//Iyy+/jMvlYsGCBVx77bXExcWxevVqKisrue6665gxYwbXX389AHfeeSd79+7l5ZdfJiMjg1dffZUrrriC3bt3M3z48D49TyFE35DwI4To92w2G3q9HrPZ3H2ra//+/cdsFwwGefrppxk2bBgARUVF/POf/6SpqQmLxcKYMWOYPXs2a9eu5frrr6empobnnnuOmpoaMjIyALj33nt5++23ee6551i8eHHfnaQQos9I+BFCDBhms7k7+ACkpqaSm5uLxWLpsay5uRmA3bt3Ew6HGTFiRI/j+P1+EhMT+6ZoIUSfk/AjhBgwdDpdj59VKtVxl0UiEQA6OzvRaDRs3boVjUbTY7vPByYhxMAi4UcIcU7Q6/U9Oir3hokTJxIOh2lubmbmzJm9emwhRP8lT3sJIc4Jubm5bNy4kerqalpbW7uv3pyJESNG8M1vfpObbrqJ4uJiqqqq2LRpE0uWLOHNN9/shaqFEP2RhB8hxDnh3nvvRaPRMGbMGJKTk6mpqemV4z733HPcdNNN3HPPPYwcOZLCwkI2b95MTk5OrxxfCNH/yAjPQgghhBhU5MqPEEIIIQYVCT9CCCGEGFQk/AghhBBiUJHwI4QQQohBRcKPEEIIIQYVCT9CCCGEGFQk/AghhBBiUJHwI4QQQohBRcKPEEIIIQYVCT9CCCGEGFQk/AghhBBiUPn/dvf2grhNOkgAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for tSTAT5 (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, tSTAT5 = simulate_tSTAT5(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, tSTAT5, **kwargs)\n", + "ax.plot(df_tSTAT5['time'], df_tSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"tSTAT5\");\n", + "#ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "c4d6fe0b-335f-4e69-ba72-6cc2b2b1af70", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR (single regularization strength with noise model)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "t, pEpoR = simulate_pEpoR(problem=regproblems[chosen_regstrength], result=regresults[chosen_regstrength])\n", + "sigma_pEpoR = 0.0274 + 0.1 * pEpoR\n", + "ax.fill_between(t, pEpoR - 2*sigma_pEpoR, pEpoR + 2*sigma_pEpoR, color='black', alpha=0.10, interpolate=True, label='2-sigma error bands')\n", + "ax.plot(t, pEpoR, color='black', label='MLE')\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.set_title(f\"ML fit for regularization strength = {chosen_regstrength}\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "658cd182-8f00-4eb7-8d14-30b839cd9e38", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Store results for later\n", + "all_results['15 nodes, FD'] = (regproblems[chosen_regstrength], regresults[chosen_regstrength])" + ] + }, + { + "cell_type": "markdown", + "id": "3c33c2c6-b299-40a6-83b1-a3724d72dfc8", + "metadata": { + "tags": [] + }, + "source": [ + "## Spline approximation with few nodes, optimizing derivatives explicitly\n", + "An alternative way to achieve higher expressivity, while not increasing the number of nodes, is to optimize the derivatives of the spline at the nodes instead of computing them by finite differencing. The risk of overfitting is still present, so we will include regularization as in the above example." + ] + }, + { + "cell_type": "markdown", + "id": "8ab36ca3-4555-4fd0-9a98-8ed8fc899fee", + "metadata": {}, + "source": [ + "### Creating the PEtab model" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "ec220c5d-ec0b-44f3-bfae-129ffe3ee26b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem name\n", + "name = \"Swameye_PNAS2003_5nodes\"" + ] + }, + { + "cell_type": "markdown", + "id": "a3ef383a-c34c-40cb-bdd9-4012554ba0fe", + "metadata": {}, + "source": [ + "We now need to create additional parameters for the spline derivatives too." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "e686b762-b485-4624-bbd6-27220bdb9c03", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create spline for pEpoR\n", + "nodes = [0, 5, 10, 20, 60]\n", + "values_at_nodes = [sp.Symbol(f\"pEpoR_t{str(t).replace('.', '_dot_')}\") for t in nodes]\n", + "derivatives_at_nodes = [sp.Symbol(f\"derivative_pEpoR_t{str(t).replace('.', '_dot_')}\") for t in nodes[:-1]]\n", + "spline = amici.splines.CubicHermiteSpline(\n", + " sbml_id='pEpoR',\n", + " evaluate_at=amici.sbml_utils.amici_time_symbol,\n", + " nodes=nodes,\n", + " values_at_nodes=values_at_nodes,\n", + " derivatives_at_nodes=derivatives_at_nodes + [0], # last value is zero because steady state is reached\n", + " extrapolate=(None, \"constant\"),\n", + " bc=\"auto\",\n", + " logarithmic_parametrization=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "id": "8b0924bf-ec72-4631-a3db-3325945e3ccb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Compute L2 norm of the curvature of pEpoR\n", + "regularization = spline.squared_L2_norm_of_curvature()" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "id": "80ca3be0-a3b2-446d-96fc-edbf405215db", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add a parameter for regularization strength\n", + "reg_parameters_df = pd.DataFrame(\n", + " dict(parameterScale='log10', lowerBound=1e-6, upperBound=1e6, nominalValue=1.0, estimate=0),\n", + " index=pd.Series(['regularization_strength'], name=\"parameterId\"),\n", + ")\n", + "# Encode regularization term as an additional observable\n", + "reg_observables_df = pd.DataFrame(\n", + " dict(observableFormula=f'sqrt({regularization})'.replace('**', '^'), observableTransformation='lin', noiseFormula='1/sqrt(regularization_strength)', noiseDistribution='normal'),\n", + " index=pd.Series(['regularization'], name=\"observableId\"),\n", + ")\n", + "# and correspoding measurement\n", + "reg_measurements_df = pd.DataFrame(\n", + " dict(observableId='regularization', simulationConditionId='condition1', measurement=0, time=0, observableTransformation='lin'),\n", + " index=pd.Series([0]),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "6c9af000-624f-46a6-907b-fa1875a87986", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Add spline formula to SBML model\n", + "sbml_doc = libsbml.SBMLReader().readSBML(os.path.join('Swameye_PNAS2003', 'swameye2003_model.xml'))\n", + "sbml_model = sbml_doc.getModel()\n", + "spline.add_to_sbml_model(sbml_model, auto_add=True, y_nominal=0.1, y_constant=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "266ea27b-4eff-4fdf-98db-0009389cca81", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Derivative parameters must be added separately\n", + "for p in derivatives_at_nodes:\n", + " amici.sbml_utils.add_parameter(sbml_model, p, value=0.0, constant=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "223e47a3-6d7b-49af-94ef-f536aeea3e18", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Extra parameters associated to the spline\n", + "spline_parameters_df1 = pd.DataFrame(\n", + " dict(parameterScale='log', lowerBound=0.001, upperBound=10, nominalValue=0.1, estimate=1),\n", + " index=pd.Series(list(map(str, values_at_nodes)), name=\"parameterId\"),\n", + ")\n", + "spline_parameters_df2 = pd.DataFrame(\n", + " dict(parameterScale='lin', lowerBound=-0.666, upperBound=0.666, nominalValue=0.0, estimate=1),\n", + " index=pd.Series(list(map(str, derivatives_at_nodes)), name=\"parameterId\"),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "5afa675e-0f72-4ff4-b18a-538a8a5f1d4e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create PEtab problem\n", + "petab_problem = petab.Problem(\n", + " sbml_model,\n", + " condition_df=petab.conditions.get_condition_df(os.path.join('Swameye_PNAS2003', 'swameye2003_conditions.tsv')),\n", + " measurement_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_measurements.tsv'), reg_measurements_df],\n", + " petab.measurements.get_measurement_df\n", + " ).reset_index(drop=True),\n", + " parameter_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_parameters.tsv'), spline_parameters_df1, spline_parameters_df2, reg_parameters_df],\n", + " petab.parameters.get_parameter_df\n", + " ),\n", + " observable_df=petab.core.concat_tables(\n", + " [os.path.join('Swameye_PNAS2003', 'swameye2003_observables.tsv'), reg_observables_df],\n", + " petab.observables.get_observable_df\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "acb52953-ab44-4beb-9378-3ab8f743c1f8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether PEtab model is valid\n", + "assert not petab.lint_problem(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "2b4d2215-0551-4f9c-a092-c22259405582", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Save PEtab problem to disk\n", + "# import shutil\n", + "# shutil.rmtree(name, ignore_errors=True)\n", + "# os.mkdir(name)\n", + "# petab_problem.to_files_generic(prefix_path=name)" + ] + }, + { + "cell_type": "markdown", + "id": "5466e058-089c-4bc7-a7bb-1f88649fb29f", + "metadata": {}, + "source": [ + "### Creating the pyPESTO problem" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "6fc63a63-d5f3-45a3-b8a2-284e07b229e0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Problem must be \"flattened\" to be used with AMICI\n", + "petab.core.flatten_timepoint_specific_output_overrides(petab_problem)" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "4c55449b-b877-451d-8a40-0547c9f88e12", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Check whether simulation from the PEtab problem works\n", + "# import amici.petab_simulate\n", + "# simulator = amici.petab_simulate.PetabSimulator(petab_problem)\n", + "# simulator.simulate(noise=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "bbb28c44-11d3-4a74-996f-cc13870b091b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Import PEtab problem into pyPESTO\n", + "pypesto_problem = pypesto.petab.PetabImporter(petab_problem, model_name=name).create_problem()" + ] + }, + { + "cell_type": "markdown", + "id": "ca2f1ac5-4616-43bd-88ce-3e424461ccd2", + "metadata": {}, + "source": [ + "### Maximum Likelihood estimation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a745c509-6e28-4e98-aefc-12db835eb0ed", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Try different regularization strengths\n", + "regstrengths = np.asarray([1, 175, 500, 1000])\n", + "if os.getenv('GITHUB_ACTIONS') is not None:\n", + " regstrengths = np.asarray([175])\n", + "regproblems = {}\n", + "regresults = {}\n", + "\n", + "for regstrength in regstrengths:\n", + " # Fix parameter in pypesto problem\n", + " name = f\"Swameye_PNAS2003_5nodes_reg{regstrength}\"\n", + " pypesto_problem.fix_parameters(\n", + " pypesto_problem.x_names.index('regularization_strength'),\n", + " np.log10(regstrength) # parameter is specified as log10 scale in PEtab\n", + " )\n", + " regproblem = copy.deepcopy(pypesto_problem)\n", + "\n", + " # Load existing results if available\n", + " if os.path.exists(f'{name}.h5'):\n", + " regresult = pypesto.store.read_result(f'{name}.h5', problem=regproblem)\n", + " else:\n", + " regresult = None\n", + " # Overwrite\n", + " # regresult = None\n", + "\n", + " # Parallel multistart optimization with pyPESTO and FIDES\n", + " if n_starts > 0:\n", + " if regresult is None:\n", + " new_ids = [str(i) for i in range(n_starts)]\n", + " else:\n", + " last_id = max(int(i) for i in regresult.optimize_result.id)\n", + " new_ids = [str(i) for i in range(last_id+1, last_id+n_starts+1)]\n", + " regresult = pypesto.optimize.minimize(\n", + " regproblem,\n", + " n_starts=n_starts,\n", + " ids=new_ids,\n", + " optimizer=pypesto_optimizer,\n", + " engine=pypesto_engine,\n", + " result=regresult,\n", + " )\n", + " regresult.optimize_result.sort()\n", + " if regresult.optimize_result.x[0] is None:\n", + " raise Exception(\"All multistarts failed (n_starts is probably too small)! If this error occurred during CI, just run the workflow again.\")\n", + "\n", + " # Save results to disk\n", + " # pypesto.store.write_result(regresult, f'{name}.h5', overwrite=True)\n", + "\n", + " # Store result\n", + " regproblems[regstrength] = regproblem\n", + " regresults[regstrength] = regresult" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "efa1b2f7-347a-4fba-9e22-e375aeb06d30", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Target value is 15\n", + "Regularization strength: 1. Statistic is 9.638207938045252\n", + "Regularization strength: 175. Statistic is 15.115255701660317\n", + "Regularization strength: 500. Statistic is 19.156287450444093\n", + "Regularization strength: 1000. Statistic is 25.09224919998158\n" + ] + } + ], + "source": [ + "# Compute sum of squared normalized residuals\n", + "print(f\"Target value is {len(df_pEpoR['time'])}\")\n", + "regstrengths = sorted(regproblems.keys())\n", + "stats = []\n", + "for regstrength in regstrengths:\n", + " t, pEpoR = simulate_pEpoR(N=None, problem=regproblems[regstrength], result=regresults[regstrength])\n", + " assert np.array_equal(df_pEpoR['time'], t[:-1])\n", + " pEpoR = pEpoR[:-1]\n", + " sigma_pEpoR = 0.0274 + 0.1 * pEpoR\n", + " stat = np.sum(((pEpoR - df_pEpoR['measurement']) / sigma_pEpoR)**2)\n", + " print(f\"Regularization strength: {regstrength}. Statistic is {stat}\")\n", + " stats.append(stat)\n", + "# Select best regularization strength\n", + "chosen_regstrength = regstrengths[np.abs(np.asarray(stats) - len(df_pEpoR['time'])).argmin()]" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "07e7e94c-2017-424a-a7ab-cd42d9b454b4", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAFjCAYAAADRv2QOAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABYv0lEQVR4nO3dd1xTV/8H8E8AE5Q9BNmgaBEHKi5wCxa1bqvWWkXto1Zp1Tqe6mPrbJ21jorax7baWnerXdZdJ6J1V1wMcVQBBRkCAhrO7w8f8jOGkUBiEvi8X6+8XuTck3u/JyeQL+eee65ECCFAREREVAWZ6DsAIiIiIn1hIkRERERVFhMhIiIiqrKYCBEREVGVxUSIiIiIqiwmQkRERFRlMREiIiKiKouJEBEREVVZTISIiIioymIiRJVKSkoK3nzzTTg4OEAikWD58uVqv/bWrVuQSCTYsGGDomz27NmQSCRai6+4Y+ha9+7dMWrUqFd2vPLS9ntdxNvbG8OHD9f6fg31uFWNRCLB+++/r+8w9G7t2rXw9PREfn6+vkMxOkyECNu3b4dEIsGuXbtUtgUEBEAikeDw4cMq2zw9PREcHKzRsVavXq3TJODDDz/Evn37MH36dGzcuBFdu3bV2bFetZMnT2L27NnIyMhQ+zVRUVHYv38/PvroI90FVoWVp08MwebNmzX6J0HfjOV93rZtG9555x3UrVsXEokEHTt2LLbe8OHDIZFISnzcu3dPUbdjx47F1nn5b9vw4cNRUFCAr776SpdNrJTM9B0A6V/btm0BACdOnEDfvn0V5VlZWYiJiYGZmRmioqLQqVMnxba7d+/i7t27eOuttzQ61urVq+Ho6Kiz/5T//PNP9O7dG1OmTNHJ/vXp5MmTmDNnDoYPHw5bW1u1XrNkyRKEhITA19dXt8EZsBs3bsDERDf/85XWJ7o8bkVt3rwZMTExmDhxor5DUUt5Pvv6sGbNGpw7dw4tWrRAWlpaifXGjBmD0NBQpTIhBN577z14e3vDzc1NaZu7uzsWLFigVObq6qr03NzcHOHh4fjiiy/wwQcf6GR0tbJiIkRwdXWFj48PTpw4oVQeHR0NIQQGDBigsq3oeVESpU/Pnj1DYWEhpFIpHjx4YNB/KF+lBw8eYPfu3Vi7dm2ZdXNycmBhYfEKono1hBDIy8tD9erVIZPJ9BKDvo6rbXl5eZBKpQab1BmSjRs3ws3NDSYmJmjYsGGJ9YKCghAUFKRUduLECeTm5mLIkCEq9W1sbPDOO++UefyBAwdi8eLFOHz4MDp37qx5A6oofrIJwPOE5sKFC3jy5ImiLCoqCg0aNEC3bt1w6tQpFBYWKm2TSCRo06YNAGD9+vXo3LkznJycIJPJ4O/vjzVr1igdw9vbG1euXMHRo0cVw7svDh1nZGRg4sSJ8PDwgEwmg6+vLxYtWqR03KI5Np9//jmWL1+OOnXqQCaTYfXq1ZBIJBBCIDIyUrF/AHj06BGmTJmCRo0awdLSEtbW1ujWrRsuXbqktfevY8eOaNiwIc6dO4fg4GBUr14dPj4+aiUhwPORrHbt2sHCwgK2trbo3bs3rl27ptg+e/ZsTJ06FQDg4+OjaN+tW7dK3Ofu3bvx7Nkzlf88N2zYAIlEgqNHj2LcuHFwcnKCu7u7YvuePXsUsVhZWeGNN97AlStXVPa/Y8cO+Pv7w9zcHA0bNsSuXbswfPhweHt7K+ocOXIEEokER44cUXqtunOl1PlcAc8/Wz169MC+ffvQvHlzVK9eXXGK4OW5OqWdkih6P//++28MHz4ctWvXhrm5OWrVqoWRI0cq/ZdfVp8UN0fo5s2bGDBgAOzt7VGjRg20bt0au3fvVqpT9J5t374dn332Gdzd3WFubo6QkBDEx8eX+n4BwOPHjzFx4kR4e3tDJpPByckJXbp0wfnz5wE8/6zu3r0bt2/fVsRc1GdFx966dSs+/vhjuLm5oUaNGsjKygIAnD59Gl27doWNjQ1q1KiBDh06ICoqSun4RXO94uPjFSM4NjY2GDFiBHJzc5XqPnnyBOPHj4ejoyOsrKzQq1cv3Lt3DxKJBLNnz1brfS7y888/o2HDhpDJZGjQoAH27t1b5nulbR4eHuVOGDdv3gyJRIK333672O3Pnj1DdnZ2qfsIDAyEvb09fvnll3LFUFVxRIgAPE+ENm7ciNOnTyuSk6ioKAQHByM4OBiZmZmIiYlB48aNFdv8/Pzg4OAA4PmQcIMGDdCrVy+YmZnht99+w7hx41BYWIiIiAgAwPLly/HBBx/A0tISM2bMAAA4OzsDAHJzc9GhQwfcu3cPY8aMgaenJ06ePInp06cjKSlJZT7D+vXrkZeXh9GjR0Mmk6FZs2bYuHEjhg4dii5dumDYsGGKujdv3sTPP/+MAQMGwMfHBykpKfjqq6/QoUMHXL16VWWIubzS09PRvXt3DBw4EIMHD8b27dsxduxYSKVSjBw5ssTXHTx4EN26dUPt2rUxe/ZsPHnyBF9++SXatGmD8+fPw9vbG/369UNsbCy2bNmCZcuWwdHREQBQs2bNEvd78uRJODg4wMvLq9jt48aNQ82aNTFz5kzk5OQAeP4fbXh4OMLCwrBo0SLk5uZizZo1ikS56Atz9+7dGDRoEBo1aoQFCxYgPT0d7777rsqQfkWp87kqcuPGDQwePBhjxozBqFGj8NprrxW7z40bN6qUffzxx3jw4AEsLS0BAAcOHMDNmzcxYsQI1KpVC1euXMF///tfXLlyBadOnYJEItG4T1JSUhAcHIzc3FyMHz8eDg4O+O6779CrVy/8+OOPSqelAWDhwoUwMTHBlClTkJmZicWLF2PIkCE4ffp0qe/Ze++9hx9//BHvv/8+/P39kZaWhhMnTuDatWto1qwZZsyYgczMTPzzzz9YtmwZACjaXWTevHmQSqWYMmUK8vPzIZVK8eeff6Jbt24IDAzErFmzYGJiokhUjx8/jpYtWyrtY+DAgfDx8cGCBQtw/vx5fP3113BycsKiRYsUdYYPH47t27dj6NChaN26NY4ePYo33nhDaT/qvM8nTpzAzp07MW7cOFhZWWHlypXo378/7ty5o/gbVZLU1NRStxexsrLS2Sjf06dPsX37dgQHByv9I1EkNjYWFhYWKCgogLOzM0aNGoWZM2eiWrVqKnWbNWumkpxSGQSREOLKlSsCgJg3b54QQoinT58KCwsL8d133wkhhHB2dhaRkZFCCCGysrKEqampGDVqlOL1ubm5KvsMCwsTtWvXVipr0KCB6NChg0rdefPmCQsLCxEbG6tUPm3aNGFqairu3LkjhBAiMTFRABDW1tbiwYMHKvsBICIiIpTK8vLyhFwuVypLTEwUMplMzJ07V6kMgFi/fr2ibNasWUKdX5MOHToIAGLp0qWKsvz8fNGkSRPh5OQkCgoKSjxGUZ20tDRF2aVLl4SJiYkYNmyYomzJkiUCgEhMTCwzHiGEaNu2rQgMDFQpX79+vQAg2rZtK549e6Yof/z4sbC1tVXqVyGESE5OFjY2NkrljRo1Eu7u7uLx48eKsiNHjggAwsvLS1F2+PBhAUAcPnxYaZ/qvtfqfq68vLwEALF3716V+l5eXiI8PFylvMjixYsFAPH999+XetwtW7YIAOLYsWOKstL65OXjTpw4UQAQx48fV5Q9fvxY+Pj4CG9vb8VntOg9q1+/vsjPz1fUXbFihQAgLl++XGJbhBDCxsZG5XfgZW+88YZSPxUpOnbt2rWV3oPCwkJRt25dERYWJgoLCxXlubm5wsfHR3Tp0kVRVtSPI0eOVNp33759hYODg+L5uXPnBAAxceJEpXrDhw8XAMSsWbMUZaW9zwCEVCoV8fHxirJLly4JAOLLL78s9X0oer06jxc/q+oo6W9dcX777TcBQKxevVpl28iRI8Xs2bPFTz/9JL7//nvRq1cvAUAMHDiw2H2NHj1aVK9eXaNYqzqeGiMAQP369eHg4KCY+3Pp0iXk5OQorgoLDg5W/JcRHR0NuVyuND+oevXqip8zMzORmpqKDh064ObNm8jMzCzz+Dt27EC7du1gZ2eH1NRUxSM0NBRyuRzHjh1Tqt+/f/9SR0NeJJPJFMPVcrkcaWlpsLS0xGuvvaY4XaANZmZmGDNmjOK5VCrFmDFj8ODBA5w7d67Y1yQlJeHixYsYPnw47O3tFeWNGzdGly5d8Mcff5Q7nrS0NNjZ2ZW4fdSoUTA1NVU8P3DgADIyMjB48GClPjA1NUWrVq0UVw7ev38fly9fxrBhw5RGEjp06IBGjRqVO97iaPK58vHxQVhYmEb7P3z4MKZPn44PPvgAQ4cOLfa4eXl5SE1NRevWrQGg3J+ZP/74Ay1btlT6vbG0tMTo0aNx69YtXL16Van+iBEjIJVKFc/btWsH4PkIZ2lsbW1x+vRp3L9/v1xxAkB4eLjSe3Dx4kXExcXh7bffRlpamuKzkZOTg5CQEBw7dkzpFDbwfGTqRe3atUNaWpriNFvRqatx48Yp1fvggw80jjc0NBR16tRRPG/cuDGsra3LfK+A5597dR6afrY0sXnzZlSrVg0DBw5U2fbNN99g1qxZ6NevH4YOHYpffvkFo0aNwvbt23Hq1CmV+nZ2dnjy5InKaUgqGU+NEYDn8yaCg4MVf9CioqLg5OSkuNooODgYq1atAgBFQvTiH/SoqCjMmjUL0dHRKr+AmZmZsLGxKfX4cXFx+Pvvv0tMbh48eKD03MfHR+22FRYWYsWKFVi9ejUSExMhl8sV28oaNteEq6uryoTjevXqAXg+J6boi/RFt2/fBoBiT+PUr18f+/btq9BEZiFEidtefg/j4uIAoMRJltbW1koxF3clmq+vr1aTS00+V5p8JgDgn3/+waBBg9CmTRt88cUXStsePXqEOXPmYOvWrSqfPXUS++Lcvn0brVq1UimvX7++YvuLE2w9PT2V6hUltenp6aUeZ/HixQgPD4eHhwcCAwPRvXt3DBs2DLVr11Y71pI+G+Hh4SW+JjMzUynxLi1+a2tr3L59GyYmJirHKs8Vji8fq+h4Zb1XAFTm0L1q2dnZ+OWXXxAWFqb236PJkydj3bp1OHjwoMrflaLfeV41pj4mQqTQtm1b/Pbbb7h8+bJiflCR4OBgTJ06Fffu3cOJEyfg6uqq+MOakJCAkJAQ+Pn54YsvvoCHhwekUin++OMPLFu2TOU/xeIUFhaiS5cu+Pe//13s9qKEosiL/62WZf78+fjkk08wcuRIzJs3D/b29jAxMcHEiRPVis1YOTg4lPpF8PJ7WPRebNy4EbVq1VKpb2am+Z+Lkv4Yv5iMlkTTz5Umn4mCggK8+eabkMlk2L59u0rbBg4ciJMnT2Lq1Klo0qQJLC0tUVhYiK5du76yz8yLo3UvKi25BZ7H3q5dO+zatQv79+/HkiVLsGjRIuzcuRPdunVT69glfTaWLFmCJk2aFPual+cZlTf+8qjIsZKTk9U6ho2NjUafMXX9/PPPJV4tVhIPDw8AzxP2l6Wnp6NGjRo6ibWyYiJECi+uJxQVFaW0xkhgYCBkMhmOHDmC06dPo3v37optv/32G/Lz8/Hrr78q/WdW3CKMJX0x1qlTB9nZ2Tr57+zHH39Ep06d8M033yiVZ2RkKCZeasP9+/dVRm9iY2MBoNgJkAAUE5lv3Lihsu369etwdHRU7E/T//D8/Pzw008/qV2/6NSCk5NTqf1QFHNxVzC9XFY0CvDyQnhFo0ql0eRzpanx48fj4sWLOHbsmGLCfpH09HQcOnQIc+bMwcyZMxXlRaMiL9KkT7y8vErs56Lt2uLi4oJx48Zh3LhxePDgAZo1a4bPPvtMkQhp+lkq+mxYW1tr7XfUy8sLhYWFSExMRN26dRXlxX2udDm64eLiola99evX62T9s02bNsHS0hK9evVS+zVFp/yKG0FPTExUjDKSejhHiBSaN28Oc3NzbNq0Cffu3VMaESq6MisyMhI5OTlKp8WK/ht78b+vzMxMrF+/XuUYFhYWxa4OO3DgQERHR2Pfvn0q2zIyMvDs2bNyt8vU1FTlP8MdO3Yord6qDc+ePVNa1bVoldeaNWsiMDCw2Ne4uLigSZMm+O6775Tel5iYGOzfv18p4SxKiNRdXTcoKAjp6elqzZMAgLCwMFhbW2P+/Pl4+vSpyvaHDx8CeH4KsGHDhvj++++VLuc9evQoLl++rPQaLy8vmJqaqszxWr16dZnxaPK50sT69evx1VdfITIyUuVKp5KOC6DYlZg16ZPu3bvjr7/+QnR0tKIsJycH//3vf+Ht7Q1/f38NWlE8uVyucurOyckJrq6uSrdesLCw0OgUX2BgIOrUqYPPP/+82Eu4iz4bmiiac/PyZ+HLL79UqavpZ18T+pwj9PDhQxw8eBB9+/ZFjRo1VLZnZWWp3DJDCIFPP/0UAIqN6fz58xqv+F/VcUSIFKRSKVq0aIHjx49DJpOpfHkHBwdj6dKlAJTnB73++uuQSqXo2bMnxowZg+zsbKxbtw5OTk5ISkpS2kdgYCDWrFmDTz/9FL6+vnByckLnzp0xdepU/Prrr+jRoweGDx+OwMBA5OTk4PLly/jxxx9x69atco/e9OjRA3PnzsWIESMQHByMy5cvY9OmTRrNmVCHq6srFi1ahFu3bqFevXrYtm0bLl68iP/+97/FXuZaZMmSJejWrRuCgoLw7rvvKi6ft7GxUaylAkDRHzNmzMBbb72FatWqoWfPniXOH3rjjTdgZmaGgwcPYvTo0WXGb21tjTVr1mDo0KFo1qwZ3nrrLdSsWRN37tzB7t270aZNG8U8sfnz56N3795o06YNRowYgfT0dKxatQoNGzZU+qK0sbHBgAED8OWXX0IikaBOnTr4/fffVebdFEeTz5W6UlNTMW7cOPj7+0Mmk+GHH35Q2t63b19YW1ujffv2WLx4MZ4+fQo3Nzfs378fiYmJKvvTpE+mTZuGLVu2oFu3bhg/fjzs7e3x3XffITExET/99JNWFix8/Pgx3N3d8eabbyIgIACWlpY4ePAgzpw5o/jdLYp727ZtmDRpElq0aAFLS0v07NmzxP2amJjg66+/Rrdu3dCgQQOMGDECbm5uuHfvHg4fPgxra2v89ttvGsUaGBiI/v37Y/ny5UhLS1NcPl80ivriKJCmn31NaHMU+tixY4qk/+HDh8jJyVEkLe3bt0f79u2V6m/btg3Pnj0r8bTY+fPnMXjwYAwePBi+vr548uQJdu3ahaioKIwePRrNmjVTqn/u3Dk8evQIvXv31lqbqgR9Xa5Ghmn69OkCgAgODlbZtnPnTgFAWFlZKV12LYQQv/76q2jcuLEwNzcX3t7eYtGiReLbb79VueQ1OTlZvPHGG8LKykoAULq89PHjx2L69OnC19dXSKVS4ejoKIKDg8Xnn3+ucvn5kiVLio0fJVw+P3nyZOHi4iKqV68u2rRpI6Kjo0WHDh2Ujl/Ry+cbNGggzp49K4KCgoS5ubnw8vISq1atUqpX3DGEEOLgwYOiTZs2onr16sLa2lr07NlTXL16VeU48+bNE25ubsLExEStS+l79eolQkJClMqKLp8/c+ZMsa85fPiwCAsLEzY2NsLc3FzUqVNHDB8+XJw9e1ap3tatW4Wfn5+QyWSiYcOG4tdffxX9+/cXfn5+SvUePnwo+vfvL2rUqCHs7OzEmDFjRExMjFrvtbqfKy8vL/HGG28U254XL2Mvev9LehTt859//hF9+/YVtra2wsbGRgwYMEDcv39f5bJuIUruk+Iu209ISBBvvvmmsLW1Febm5qJly5bi999/V3n/AYgdO3YolZf02XlRfn6+mDp1qggICBBWVlbCwsJCBAQEqFyWnZ2dLd5++21ha2urtORBSccucuHCBdGvXz/h4OAgZDKZ8PLyEgMHDhSHDh1S1Cnqx4cPHyq9tuhz92K/5eTkiIiICGFvby8sLS1Fnz59xI0bNwQAsXDhQqXXl/Q+F/c7L0TZyyboQlHbi3u8/LkRQojWrVsLJycnlb+nRW7evCkGDBggvL29hbm5uahRo4YIDAwUa9euVVrGoMhHH30kPD09i91GJZMIoYOZa0RVTMeOHZGamoqYmBh9h6Lk+PHj6NixI65fv640D0NXmjRpgpo1a+LAgQM6PxZVThcvXkTTpk3xww8/aDSBuKrLz8+Ht7c3pk2bhgkTJug7HKPCOUJElVi7du3w+uuvY/HixVrd79OnT1XmbR05cgSXLl0q8Y7bRC978ZY+RZYvXw4TExOV00hUuvXr16NatWoq6zdR2Yx+ROju3bsYOnQoHjx4ADMzM3zyyScYMGCAvsOiKsZQR4R05datWwgNDcU777wDV1dXXL9+HWvXroWNjQ1iYmK0uj4TVV5z5szBuXPn0KlTJ5iZmWHPnj3Ys2cPRo8erXThAZEuGX0ilJSUhJSUFDRp0gTJyckIDAxU3JeF6FWpaolQZmYmRo8ejaioKDx8+BAWFhYICQnBwoULlVb4JSrNgQMHMGfOHFy9ehXZ2dnw9PTE0KFDMWPGjHKtW0VUHkafCL0sICAAv//+u2LBKSIiIqKS6H2O0LFjx9CzZ0+4urpCIpHg559/VqkTGRkJb29vmJubo1WrVvjrr7+K3de5c+cgl8uZBBEREZFa9J4I5eTkICAgAJGRkcVuL1rrYtasWTh//jwCAgIQFhamsg7Jo0ePMGzYMPz3v/99FWETERFRJWBQp8YkEgl27dqFPn36KMpatWqFFi1aKBZyKywshIeHBz744ANMmzYNwPPLBrt06YJRo0Yp3UG6OPn5+UordRYWFuLRo0dwcHDgTeqIiIgqASEEHj9+DFdX1zIXKzXo2WgFBQU4d+4cpk+frigzMTFBaGioYpl6IQSGDx+Ozp07l5kEAcCCBQswZ84cncVMREREhuHu3btwd3cvtY5BJ0KpqamQy+UqN0V0dnZW3KgwKioK27ZtQ+PGjRXzizZu3IhGjRoVu8/p06dj0qRJiueZmZnw9PTE3bt3YW1trZuGEBER6VhycjIuXrxY4vYmTZqgVq1ary4gPcrKyoKHhwesrKzKrGvQiZA62rZti8LCQrXry2QyyGQylXJra2smQkREZLSsra1hZWWFS5cuKS14am5ujgYNGsDFxUWP0emHOlNeDDoRcnR0hKmpKVJSUpTKU1JSqkxWS0REpK5atWrh4cOHyMvLg7OzMywsLDgHtgx6v2qsNFKpFIGBgTh06JCirLCwEIcOHUJQUJAeIyMiIjI8T58+BQBUr14dnp6ecHR0ZBJUBr2PCGVnZyM+Pl7xPDExERcvXoS9vT08PT0xadIkhIeHo3nz5mjZsiWWL1+OnJwcjBgxQo9RExERGZ6CggIAzwcSmACpR++J0NmzZ9GpUyfF86KJzOHh4diwYQMGDRqEhw8fYubMmUhOTkaTJk2wd+9elQnUmoqMjERkZCTkcnmF9kNERGQoXkyESD0GtY6QPmRlZcHGxgaZmZmcLE1EREYtOTkZDx48gIODA9zc3PQdjt5o8t1u0HOEiIiISH0cEdIcEyEiIqJKgomQ5pgIERERVRJMhDSn0WTpa9euYevWrTh+/Dhu376N3Nxc1KxZE02bNkVYWBj69+9f7GKFhoiTpYmIqDKRy+WKhRSZCKlPrcnS58+fx7///W+cOHECbdq0QcuWLeHq6orq1avj0aNHiImJwfHjx5GVlYV///vfmDhxotEkRJwsTURElcGTJ08QFxcHMzMz+Pv76zscvdLku12tEaH+/ftj6tSp+PHHH2Fra1tivejoaKxYsQJLly7Ff/7zH42CJiIiovLjabHyUSsRio2NRbVq1cqsFxQUhKCgIMXKlkRERPRqMBEqH7UmS6uTBFWkPhEREVUME6Hy0dpVYykpKZg7d662dkdEREQaYCJUPlpLhJKTkzFnzhxt7Y6IiIg0wESofNS+fP7vv/8udfuNGzcqHMyrxMvniYioshBCMBEqJ7XvNWZiYgKJRILiqheVSyQSo0ssePk8EREZu4KCAly/fh0SiQQNGzas8nee1/rl8wBgb2+PxYsXIyQkpNjtV65cQc+ePTWLlIiIiCrsxdGgqp4EaUrtRCgwMBD379+Hl5dXsdszMjKKHS0iIiIi3eJpsfJTOxF67733kJOTU+J2T09PrF+/XitBERERkfqYCJWf2olQ3759S91uZ2eH8PDwCgdEREREmsnPzwfARKg8ePd5IiIiI1d0RwcmQprTKBG6evUqxo0bh6ZNm8LFxQUuLi5o2rQpxo0bh6tXr+oqRiIiIioFR4TKT+1TY3v27EGfPn3QrFkz9O7dG87OzgCeryh94MABNGvWDL/88gvCwsJ0Fqw2cR0hIiKqDORyueK7jImQ5tReRyggIAC9e/cu8TYas2fPxs6dO8tceNHQcB0hIiIyZk+ePEFcXBzMzMzg7++v73AMgibf7WqfGouNjcWQIUNK3D548GDExcWpHyURERFVGK8Yqxi1EyFvb2/s3r27xO27d+8ucY0hIiIi0g0mQhWj9hyhuXPn4u2338aRI0cQGhqqNEfo0KFD2Lt3LzZv3qyzQImIiEgVJ0pXjNqJ0IABA+Dm5oaVK1di6dKlSE5OBgDUqlULQUFBOHLkCIKCgnQWKBEREaniiFDFqJ0IAUBwcDCCg4N1FQsRERFpiIlQxXBBRSIiIiMlhOBiihVUoURo3LhxSE1N1VYsREREpIGnT59CCAGJRIJq1arpOxyjVKFE6IcffkBWVpa2YnmlIiMj4e/vjxYtWug7FCIionJ58bSYRCLRczTGqUKJkJprMRqkiIgIXL16FWfOnNF3KEREROXCK8YqrsJzhJiBEhER6QcnSlecRleN+fj4KCU+T548QYcOHWBm9v+7uXnzpvaiIyIiohIxEao4jRKhDRs2KH4WQqB79+5YuHAh3NzctB0XERERlaEoEZLJZHqOxHhplAh16NBB6bmpqSlat26N2rVrazUoIiIiKltRIsQrxsqvQnOEOD+IiIhIP+RyOeRyOQCeGquIKnvVGBERkTErGg0yMzODqampnqMxXhqdGnvZ48ePtRUHERERaYATpbWDt9ggIiIyQlxDSDvKlQgdP34c77zzDoKCgnDv3j0AwMaNG3HixAmtBkdERETF44iQdmicCP30008ICwtD9erVceHCBUVGmpmZifnz52s9QCIiIlLFS+e1Q+NE6NNPP8XatWuxbt06pcv12rRpg/Pnz2s1OF3ivcaIiMiY8dJ57dA4Ebpx4wbat2+vUm5jY4OMjAxtxPRK8F5jRERkrIQQePr0KQCOCFWUxolQrVq1EB8fr1J+4sQJLqxIRET0Cjx9+hRCCEgkEqXbXJHmNE6ERo0ahQkTJuD06dOQSCS4f/8+Nm3ahClTpmDs2LG6iJGIiIhe8OIVY1zcuGI0TiOnTZuGwsJChISEIDc3F+3bt4dMJsOUKVPwwQcf6CJGIiIiegGvGNMeiSjn8tAFBQWIj49HdnY2/P39YWlpqe3YXomsrCzY2NggMzMT1tbW+g6HiIioTElJSXj48CEcHBx44/NiaPLdXu4Ti1KpFP7+/nj27BnPTxIREb1CvHRee9SeI7R3715cvnwZAFBYWIh58+bBzc0NMpkM7u7uWLhwIe89RkRE9Arw1Jj2qD2UM3HiRKxbtw4AsGjRIqxYsQIzZsxA/fr1cePGDSxYsAASiQQfffSRzoIlIiIiJkLapHYidOvWLXh5eQEANm/ejDVr1mDAgAEAgK5du8LX1xcTJ05kIkRERKRDcrkccrkcABMhbVD71Ji9vT3u378PAHj48CF8fX2VtterV09x3zEiIiLSjaJL583MzGBiwnunV5Ta72Dfvn3x2WefQS6Xo3fv3li9erXSnKAvv/wSTZo00UWMRERE9D88LaZdap8amz9/PkJDQ+Hn54egoCDs2LEDBw4cQL169RAfH49Hjx5h3759uoyViIioymMipF1qjwjZ2Njg5MmTmDx5MtLS0uDt7Q2ZTIaCggIMHjwYMTExaNWqlS5jJSIiqvJ46bx2lXtBRWMXGRmJyMhIyOVyxMbGckFFIiIyCjdv3kR2djY8PDxgZ2en73AMkiYLKlbZRKgIV5YmIiJjcv36dRQUFKBOnTqwsLDQdzgGSZPvdrVPjVlZWeHdd9/FyZMnKxwgERERaU4IwTlCWqZ2IpSTk4PTp0+jbdu2qF+/PpYuXYqHDx/qMjYiIiJ6QVESJJFIeHsrLdFoAYI///wTFy5cQGhoKObPnw93d3f0798fe/bs4e01iIiIdOzF0SCJRKLnaCoHjVdiCggIwJdffon79+9jw4YNyMzMRI8ePeDp6YmZM2fqIkYiIiICrxjTBbUToZczT5lMhsGDB+PgwYNISEjA8OHDsWHDBm3HR0RERP/D+UHap3YiVNqpL29vb8ybNw+3b9/WSlBERESkiomQ9qmdCM2aNQuWlpal1uH5SiIiIt1hIqR9ak85nzVrli7jICIiolIIIRQ3XGUipD0aX3uXnJyM06dPIzk5GQBQq1YttGrVCrVq1dJ6cERERMZICDlQcBYofAiY1ASkzSGRmFaonvzJaVhVu4hnhbaQVqv/KppRJaidCOXk5GDMmDHYunUrJBIJ7O3tAQCPHj2CEAKDBw/GV199hRo1augsWCIiIkMn8vZBZH0GFCb/f6FJLcB6BiTmYeWuZ1KYDA+b/xWmrYV4qR6Vj9pzhCZMmIC//voLu3fvRl5eHlJSUpCSkoK8vDz88ccf+OuvvzBhwgRdxkpERGTQRN4+iIzxyskNABSmQGSMh8jbp5N6VH5q32vMzs4Ou3fvRnBwcLHbo6Ki0KNHD6Snp2s1QF3jvcaIiEgbhJBDPOykmrQUbYcEMHGGsN0LSUYYUJiC4i4xUrceIAFMakFS889iT6dVZTq511hhYWGpk7OkUikKCwvVj5KIiKgyKThbYhIEABIISAqTkXJ7GSQlJjfq1wMEUJj0/LhUbmonQj169MDo0aNx4cIFlW0XLlzA2LFj0bNnT60GR0REZDQK1bv/ptQ0Rav11D0uFU/tRGjVqlVwdnZGYGAgHBwcUL9+fdSvXx8ODg5o3rw5nJycsGrVKl3GSkREZLhMaqpVzcGpqVbrqXtcKp7aV43Z2dlhz549uH79OqKjo5Uunw8KCoKfn5/OgiQiIjJ40ubPr/oqTAFQ3PTb53N6UGMIkLtee/WkzbXZiipH43WE/Pz8mPQQERG9RCIxBaxnPL+aSwDKN1t4/kRi/R9ITKQQ/6v3vFxUrB4nSleIxnefL0lSUhLu3Lmjrd0REREZHYl5GCS2K/FMOChvMKkFie1Kxbo/RfVg4qyVelR+al8+X5b69esjNjYWcrlcG7t7ZXj5PBERaZMQAjExl1DD7Co83WvATOqilZWl1alHz2ny3a7xqbGSfP/998jNzdXW7nQuMjISkZGRRpe4ERGRYZPL5RDCBDlPG8LUoiEkJiWffJFITAFZqzL3qW490pzWRoSMFUeEiIhIm/Ly8hAbGwtTU1M0aNBA3+FUSTpZUDE1NbXCgREREVV2z549AwCYmWntpAvpkNqJkLOzM0JCQrB582bk5+frMiYiIiKjxUTIuKidCAkhIJVKMWLECLi4uOCDDz7AxYsXdRgaERGR8Xn69CkAoFq1anqOhNSh0eXz3333He7du4cZM2bgzz//RGBgIAIDA7FmzRpkZWXpKkYiIiKjwREh46LxOkKOjo6YPHkyrly5ghMnTqBJkyb46KOP4OLigmHDhukiRiIiIqPBRMi4qJ0ISSSq978NCgrCN998g6SkJKxcuRIJCQlaDY6IiMjYMBEyLhrNESqJhYUF3n33XURFRWklKCIiImPFRMi4qJ0IrV+/HjY2NrqMhYiIyOgxETIuavdSeHi4LuMgIiIyekIIRSLEq8aMg9ZuukpERFTVPb+9xvOpJBwRMg7lSoSsra1x8+ZNlZ+JiIiqsqLRIFNT02IvMiLDU65E6MWJ01X8VmVEREQKnB9kfHhqjIiISEu4qrTxYSJERESkJRwRMj5MhIiIiLSEiZDxYSJERESkJUyEjA8TISIiIi1hImR8mAgRERFpCRMh41OuROidd96BtbW1ys9ERERVGa8aMz4SUcUXAsrKyoKNjQ0yMzOZ0BERUbkJIRATEwMhBOrXr89kSI80+W7XeERo7ty5yM3NVSl/8uQJ5s6dq+nuiIiIKgXeXsM4aZwIzZkzB9nZ2Srlubm5mDNnjlaCIiIiMja8vYZx0jgREkIU28GXLl2Cvb29VoIiIiIyNpwobZzU7i07OztIJBJIJBLUq1dPKRmSy+XIzs7Ge++9p5MgiYiIDB0nShsntROh5cuXQwiBkSNHYs6cObCxsVFsk0ql8Pb2RlBQkE6CJCIiMnQcETJOavdWeHg4AMDHxwdt2rRhRxMREb2AiZBx0niOUIcOHXD79m18/PHHGDx4MB48eAAA2LNnD65cuaL1ANXRt29f2NnZ4c0339TL8YmIiJgIGSeNE6GjR4+iUaNGOH36NHbu3Km4guzSpUuYNWuW1gNUx4QJE/D999/r5dhEREQAEyFjpXEiNG3aNHz66ac4cOAApFKporxz5844deqUVoNTV8eOHWFlZaWXYxMREQFMhIyVxonQ5cuX0bdvX5VyJycnpKamahzAsWPH0LNnT7i6ukIikeDnn39WqRMZGQlvb2+Ym5ujVatW+OuvvzQ+DhERkS7xqjHjpHEiZGtri6SkJJXyCxcuwM3NTeMAcnJyEBAQgMjIyGK3b9u2DZMmTcKsWbNw/vx5BAQEICwsTDE3iYiISN+EEJDL5QA4ImRsNE6E3nrrLXz00UdITk6GRCJBYWEhoqKiMGXKFAwbNkzjALp164ZPP/202FEmAPjiiy8watQojBgxAv7+/li7di1q1KiBb7/9VuNjERER6QJvr2G8NE6E5s+fDz8/P3h4eCA7Oxv+/v5o3749goOD8fHHH2s1uIKCApw7dw6hoaH/H7CJCUJDQxEdHV2ufebn5yMrK0vpQUREVBG8vYbx0jhtlUqlWLduHWbOnInLly8jOzsbTZs2Rd26dbUeXGpqKuRyOZydnZXKnZ2dcf36dcXz0NBQXLp0CTk5OXB3d8eOHTtKXNxxwYIFvCcaERFpVdH8II4GGZ9y95iHhwc8PDwgl8tx+fJlpKenw87OTpuxqe3gwYNq150+fTomTZqkeJ6VlQUPDw9dhEVERFVE0YgQJ0obH41PjU2cOBHffPMNgOfnRDt06IBmzZrBw8MDR44c0Wpwjo6OMDU1RUpKilJ5SkoKatWqVa59ymQyWFtbKz2IiIgqgpfOGy+NE6Eff/wRAQEBAIDffvsNN2/exPXr1/Hhhx9ixowZWg1OKpUiMDAQhw4dUpQVFhbi0KFDvK8ZEREZDCZCxkvjHktNTVWMxvzxxx8YOHAg6tWrh5EjR2LFihUaB5CdnY34+HjF88TERFy8eBH29vbw9PTEpEmTEB4ejubNm6Nly5ZYvnw5cnJyMGLECI2PRUREpAtMhIyXxj3m7OyMq1evwsXFBXv37sWaNWsAALm5uTA1NdU4gLNnz6JTp06K50Xzd8LDw7FhwwYMGjQIDx8+xMyZM5GcnIwmTZpg7969KhOoNRUZGYnIyEjFug9ERETlxUTIeElE0cIHapo9ezaWL18OFxcX5ObmIjY2FjKZDN9++y3WrVtX7sva9SUrKws2NjbIzMzkfCEiIiqXuLg4PHnyBN7e3vwuMQCafLdrnLrOnj0bjRo1wp07dzBgwADIZDIAz9dOmDZtWvkiJiIiMmK8vYbxUisRsre3R2xsLBwdHRVzgV6+yWl4eLhOAiQiIjJkvL2GcVPrqrGCggLFCszfffcd8vLydBoUERGRseDtNYybWj0WFBSEPn36IDAwEEIIjB8/HtWrVy+2Lu8BRkREVcmLq0rz9hrGR61E6IcffsCyZcuQkJAAAMjMzDT6USFeNUZERNrAK8aMm8ZXjfn4+ODs2bNwcHDQVUyvFK8aIyKiikhPT8fdu3dhaWmJ2rVr6zscgmbf7WrNEbK3t0dqaioAoFOnTpBKpRWPkoiIqBLgiJBx42RpIiKiCmAiZNw4WZqIiKgCmAgZN40nS0skkkoxWZqIiEgbuJiicVMrEXJ2dsbChQsBPJ8svXHjxkozWZqIiKgiOCJk3DTutcTERF3E8crx8nkiItIGJkLGTa3J0i87evQoevbsCV9fX/j6+qJXr144fvy4tmPTqYiICFy9ehVnzpzRdyhERGSkhBBMhIycxonQDz/8gNDQUNSoUQPjx49XTJwOCQnB5s2bdREjERGRQXrxrAITIeOk8YKK9evXx+jRo/Hhhx8qlX/xxRdYt24drl27ptUAdY0LKhIRUXk9efIEcXFxMDMzg7+/v77Dof/R+oKKL7p58yZ69uypUt6rV69KM3+IiIhIHTwtZvw0ToQ8PDxw6NAhlfKDBw/Cw8NDK0EREREZAyZCxk/jnps8eTLGjx+PixcvIjg4GAAQFRWFDRs2YMWKFVoPkIiIyFAxETJ+Gvfc2LFjUatWLSxduhTbt28H8Hze0LZt29C7d2+tB6grvHyeiIgqiomQ8dN4snRlw8nSRERUXnfv3kV6ejpq1aoFJycnfYdD/6P1ydJVPFciIiIqFm+vYfzUSoQaNGiArVu3oqCgoNR6cXFxGDt2rOJ2HERERJUZT40ZP7V67ssvv8RHH32EcePGoUuXLmjevDlcXV1hbm6O9PR0XL16FSdOnMCVK1fw/vvvY+zYsbqOm4iISO+YCBk/tXouJCQEZ8+exYkTJ7Bt2zZs2rQJt2/fxpMnT+Do6IimTZti2LBhGDJkCOzs7HQdMxERkd7x9hqVg0Y917ZtW7Rt21ZXsRARERkN3l6jcijXTVeJiIiquqKJ0mZmZpBIJHqOhsqLiRAREVE58LRY5cBEiIiIqByYCFUOVTYRioyMhL+/P1q0aKHvUIiIyAgxEaocuLI0V5YmIqJySEpKwsOHD+Ho6AhXV1d9h0Mv0PrK0i9LSEjAxx9/jMGDB+PBgwcAgD179uDKlSvl2R0REZHR4YhQ5aBxInT06FE0atQIp0+fxs6dO5GdnQ0AuHTpEmbNmqX1AImIiAwRb69ROWicCE2bNg2ffvopDhw4AKlUqijv3LkzTp06pdXgiIiIDBVHhCoHjROhy5cvo2/fvirlTk5OSE1N1UpQREREho6JUOWgcSJka2uLpKQklfILFy7Azc1NK0EREREZMt5eo/LQOBF666238NFHHyE5ORkSiQSFhYWIiorClClTMGzYMF3ESEREZFCKkiCAiZCx0zgRmj9/Pvz8/ODh4YHs7Gz4+/ujffv2CA4Oxscff6yLGImIiAzKi6NBvL2GcSv3OkJ37txBTEwMsrOz0bRpU9StW1fbsb0SXEeIiIg09fjxYyQmJsLc3Bz16tXTdzj0Ek2+28s9nufp6QlPT8/yvpyIiMhocX5Q5aFxD44cObLU7d9++225g3mVIiMjERkZCblcru9QiIjIyDARqjw07sH09HSl50+fPkVMTAwyMjLQuXNnrQWmaxEREYiIiFAMnxEREamLiVDloXEP7tq1S6WssLAQY8eORZ06dbQSFBERkSHjqtKVh1buPm9iYoJJkyZh2bJl2tgdERGRQeOIUOWhlUQIeH4j1hfXVSAiIqqsmAhVHhr34KRJk5SeCyGQlJSE3bt3Izw8XGuBERERGSomQpWHxj144cIFpecmJiaoWbMmli5dWuYVZURERMaOt9eoXDTuwcOHD+siDiIiIqPA22tULlqbI0RERFQV8PYalYtaqWzTpk3V7uzz589XKCAiIiJDxtNilYtavdinTx8dh0FERGQcmAhVLmr14qxZs3QdBxERkVFgIlS5cI4QERGRBopWlWYiVDlo3ItyuRzLli3D9u3bcefOHRQUFChtf/TokdaCIyIiMjRFI0K8vUbloPGI0Jw5c/DFF19g0KBByMzMxKRJk9CvXz+YmJhg9uzZOghRNyIjI+Hv748WLVroOxQiIjIiPDVWuUiEEEKTF9SpUwcrV67EG2+8ASsrK1y8eFFRdurUKWzevFlXsepE0d3nMzMzYW1tre9wiIjIwMXGxiIvLw8+Pj6wsrLSdzhUDE2+2zUeEUpOTkajRo0AAJaWlsjMzAQA9OjRA7t37y5HuERERMaDI0KVi8aJkLu7O5KSkgA8Hx3av38/AODMmTOQyWTajY6IiMiA8PYalY/GiVDfvn1x6NAhAMAHH3yATz75BHXr1sWwYcN4rzEiIqrUeHuNykfjXly4cKHi50GDBsHLywsnT55E3bp10bNnT60GR0REZEh4e43KR+NEKC8vD+bm5ornrVu3RuvWrbUaFBERkSHiabHKR+NTY05OTggPD8eBAwdQWFioi5iIiIgMEhOhykfjROi7775Dbm4uevfuDTc3N0ycOBFnz57VRWxEREQGQy6X4++jV3F+Twziz9yCXC7Xd0ikBRqvI1Tk8ePH+PHHH7Flyxb8+eefqF27Nt555x3MnDlT2zHqFNcRIjI8crkcMcevIy0pHQ4udmjYzg+mpqY6r2cs+6xs7TGG9+j4ztNYPXE9Uv9JU5Q5ujtg3PIRaNevVbH7Jf3R5Lu93InQi65evYohQ4bg77//NroMmYkQkWFR9wtH2/WMZZ+VrT3G8B4d33kacwd8Drz8bfm/udIzd0xhMmRgXkkilJeXh19//RWbN2/G3r174ezsjMGDBytdVWYMmAgRGQ51v3C0XU8Xx2Z7Ksd7FNy7Od7xiVBKlF6uW9PdARtvRpY44kSvnk4ToX379mHz5s34+eefYWZmhjfffBNDhgxB+/btKxS0vjARIjIMcrm8zC8cRzd7rLu6DKPqf4jUeyXc4FnDet/eWAEAGFlvgkHv01jas/by5xjTcArSSqvnao+vrnwOABjdYDLS7qWXWjfy0iKMa/xvpN2veD0HV3usvbwYAPBew6ml1rV3scW7SwdjyeA1xdd5wed/zkZAxwZl1qNXQ5Pvdo2nvfft2xc9evTA999/j+7du/Puu0SkFTHHr5ecBAGAAFL/eYT1c38o+cu4HPX2bH6+Or6h79NY2vP9Z1tKToKK6t17hIM7DgNAyUnQC3U3LdxecsKiYb20e4/w509Hnx+7jLqP7mfgwqHLJdd5QVpSKfsig6ZxIpSSksKbzBGR1qn7RfLonwyt1stOy1Grnr73aSztSb+XqVa9J5l5au8zIylLq/XyMvPVPraFpYVa9Rxc7NTeJxkWjS+fZxJERLqg7hdJw5b+Wq0X0LIxAlo2Nvh9Gkt7GrSor96xmzVAw2bqnUryD/TTar0GzfzRoJl6bW/zRis4ujso5g2pkAA1PRzQsJ16xybDo3EiRESkCw3b+an1hdNzXJhW6zVs56f1Y+tin5WtPcbyHjXu6I9xy0coyl6uAwBjl43gRGkjxkSIiAyCqampWl84Umk1rdYzNTXV+rF1sc/K1h5jeY9MTU3Rrl8rzNwxBY5uDkrVaro78NL5SkAr6wgZM141RmRYfvn6D2z85EdkpjxWlNX0cMDYZWWvFVOResayz8rWHmN5jwDNFmkk/Xol6wjFx8cjISEB7du3R/Xq1SGEMMo78TIRIjIsN27cwJPcJ8i5n4/8rKcGucqwvvdZ2dpjLO8RGQ+dJkJpaWkYNGgQ/vzzT0gkEsTFxaF27doYOXIk7OzssHTp0goF/6oxESIyHM+ePcPVq1cBAP7+/ryxJRGViybf7RrPEfrwww9hZmaGO3fuoEaNGoryQYMGYe/evZpHqyeRkZHw9/dHixYt9B0KEf1PTs7zy7TNzc2ZBBHRK6HxX5r9+/dj3759cHd3VyqvW7cubt++rbXAdC0iIgIRERGKrJGI9C87OxsAYGGh3totREQVpfGIUE5OjtJIUJFHjx5BJpNpJSgiqpqKRoQsLS31HAkRVRUaJ0Lt2rXD999/r3gukUhQWFiIxYsXo1OnTloNjoiqjmfPniEv7/lqwxwRIqJXReNTY4sXL0ZISAjOnj2LgoIC/Pvf/8aVK1fw6NEjREVF6SJGIqoCOD+IiPRB4xGhhg0bIjY2Fm3btkXv3r2Rk5ODfv364cKFC6hTp44uYiSiKoDzg4hIH8r1b5eNjQ1mzJih7ViIqArj/CAi0geNR4R8fX0xe/ZsxMXF6SIeIqqCOD+IiPRF40QoIiICu3fvxmuvvYYWLVpgxYoVSE5O1kVsRFRFFI0GyWQyzg8ioleqXAsqnjlzBtevX0f37t0RGRkJDw8PvP7660pXkxERqYunxYhIX8p99/l69ephzpw5iI2NxfHjx/Hw4UOMGDFCm7ERURXBidJEpC8VGoP+66+/sHnzZmzbtg1ZWVkYMGCAtuIioiqC84OISJ80ToRiY2OxadMmbNmyBYmJiejcuTMWLVqEfv36cVibiDSWm5sL4Pn8oGrVquk5GiKqajROhPz8/NCiRQtERETgrbfegrOzsy7iIqIqgqfFiEifNE6Ebty4gbp16+oiFiKqgjhRmoj0SePJ0kyCiEhb5HI5njx5AoAjQkSkH2qNCNnb2yM2NhaOjo6ws7ODRCIpse6jR4+0FhwRVW4vrh/E+UFEpA9qJULLli2DlZWV4ufSEiEiInVxfhAR6ZtECCH0HYQ+ZWVlwcbGBpmZmbC2ttZ3OERVSlxcHJ48eQJPT0/Y2trqOxwiqiQ0+W7XeI6QqakpHjx4oFKelpYGU1NTTXdHRFUU5wcRkSHQOBEqaQApPz8fUqm0wgERUdXA+UFEZAjUvnx+5cqVAACJRIKvv/5a6VJXuVyOY8eOwc/PT/sRElGlVJQIcTSIiPRJ7URo2bJlAJ6PCK1du1bpNJhUKoW3tzfWrl2r/QiJqFLiRGkiMgRqJ0KJiYkAgE6dOmHnzp2ws7PTWVBEVLm9OD+ICykSkT5pvLL04cOHdREHEVUhnB9ERIZC48nS/fv3x6JFi1TKFy9ezLvPE5FaOD+IiAyFxonQsWPH0L17d5Xybt264dixY1oJiogqNyZCRGQoNE6EsrOzi71Mvlq1asjKytJKUERUeXH9ICIyJBonQo0aNcK2bdtUyrdu3Qp/f3+tBEVElVdOTg6EEJBKpVx7jIj0TuPJ0p988gn69euHhIQEdO7cGQBw6NAhbNmyBTt27NB6gOr4/fffMXnyZBQWFuKjjz7Cv/71L73EQURlKzotxqvFiMgQlOteY7t378b8+fNx8eJFVK9eHY0bN8asWbPQoUMHXcRYqmfPnsHf3x+HDx+GjY0NAgMDcfLkSTg4OKj1ep3ca0wuB44fB5KSABcXoF07oKTbj6hbt7Ltk+0x7H3q8NjJFy4gx9oa9r17w87Rsfh9EhFVgEbf7cLIRUVFiT59+iieT5gwQWzevFnt12dmZgoAIjMzUzsB/fSTEO7uQgD//3B3f15e3rqVbZ9sj2Hv8xUdu9DNrfh9EhFVkCbf7eVKhNLT08W6devE9OnTRVpamhBCiHPnzol//vlH430dPXpU9OjRQ7i4uAgAYteuXSp1Vq1aJby8vIRMJhMtW7YUp0+fVmzbsWOHiIiIUDxfvHixWLJkidrH12oi9NNPQkgkyl8KwPMyiUT5j766dSvbPtkew96nvttDRKQFOk2ELl26JGrWrCl8fX2FmZmZSEhIEEIIMWPGDDF06FCNg/3jjz/EjBkzxM6dO4tNhLZu3SqkUqn49ttvxZUrV8SoUaOEra2tSElJEUIYUCL07Jnqf8Yv/vcrkQi5u7vIycoSOVlZQu7mJgrLqpuerl49Y9kn22PY+1SzXnZmpsjOzFSv7qNHpdYTEokQHh7Pf3+IiLREp4lQSEiImDp1qhBCCEtLS0UiFBUVJby8vDTdnXIwxSRCLVu2VEp05HK5cHV1FQsWLFAc9+VTY5s2bSrxGHl5eSIzM1PxuHv3rnYSocOHS0yCXnzEf/21iP/6a7Xq/jNlSqXaJ9tj2PvU57HF4cMV+/0jInqBJomQxpfPnzlzBmPGjFEpd3NzQ3Jysqa7K1VBQQHOnTuH0NBQRZmJiQlCQ0MRHR0NAGjZsiViYmJw7949ZGdnY8+ePQgLCytxnwsWLICNjY3i4eHhoZ1gk5LUqmaeng7z9HS16la/f79S7ZPt0dM+1fxsqlvPPCMD5hkZatWtoeY+1f39ISLSNo0TIZlMVuzCibGxsahZs6ZWgiqSmpoKuVwOZ2dnpXJnZ2dF0mVmZoalS5eiU6dOaNKkCSZPnlzqFWPTp09HZmam4nH37l3tBOviolY1t+bN4da8uVp17Vu0qFT7ZHv0tE8t13MLDIRbYKBade3U3Ke6vz9ERFqn6XDTu+++K/r06SMKCgqEpaWluHnzprh9+7Zo2rSpmDBhQnlGsBQA5VNj9+7dEwDEyZMnlepNnTpVtGzZskLHKqL1OULFTQoFlOdCqFs3P79y7ZPtMex96rs9RERaotM5QhkZGSI0NFTY2toKU1NT4eHhIapVqybat28vsrOzyxWwIpiXEqH8/HxhamqqMm9o2LBholevXhU6VhGdXDX28h/90q64KatuZdsn22PY+9R3e4iItEDnl88LIcTx48dFZGSkWLRokThw4EB5d6McDIqfLP3+++8rnsvlcuHm5qaYLF1Rr2QdIQ8P9ddgKa5uZdsn22PY+9R3e4iIKkiT73aJEELo67Qc8PwmrvHx8QCApk2b4osvvkCnTp1gb28PT09PbNu2DeHh4fjqq6/QsmVLLF++HNu3b8f169dV5g5pIjIyEpGRkZDL5YiNjeXK0pVo5WK2pxK0h4ioAjRZWVqtRGjlypUYPXo0zM3NsXLlylLrWlpaokGDBmjVqpVawR45cgSdOnVSKQ8PD8eGDRsAAKtWrcKSJUuQnJyMJk2aYOXKlWrvvyw6ucUGERER6Y3WEyEfHx+cPXsWDg4O8PHxKbVufn4+Hjx4gA8//BBLlizRLHI9YCJERERUuWg9EdLUgQMH8Pbbb+Phw4fa3rXWMREiIiKqXDT5btd4HSF1tG3bFh9//LEudk1ERESkNeVKhA4dOoQePXqgTp06qFOnDnr06IGDBw8qtlevXh0TJkzQWpBEREREuqBxIrR69Wp07doVVlZWmDBhAiZMmABra2t0794dkZGRuohRJyIjI+Hv748Waq7kS0RERJWPxnOE3N3dMW3aNLz//vtK5ZGRkZg/fz7u3bun1QB1jXOEiIiIKhedzhHKyMhA165dVcpff/11ZGZmaro7IiIiIr3ROBHq1asXdu3apVL+yy+/oEePHloJioiIiOhVMFOn0ouLKPr7++Ozzz7DkSNHEBQUBAA4deoUoqKiMHnyZN1ESURERKQDai+oqNbOJBLcvHmzwkG9SpwjREREVLlo8t2u1ohQYmKiVgIjIiIiMiTlXlAxNTUVqamp2ozlleLl80RERKRRIpSRkYGIiAg4OjrC2dkZzs7OcHR0xPvvv4+MjAwdhagbERERuHr1Ks6cOaPvUIiIiEhP1Do1BgCPHj1CUFAQ7t27hyFDhqB+/foAgKtXr2LDhg04dOgQTp48CTs7O50FS0RERKRNaidCc+fOhVQqRUJCApydnVW2vf7665g7dy6WLVum9SCJiIiIdEHtU2M///wzPv/8c5UkCABq1aqFxYsXF7u+EBEREZGhUjsRSkpKQoMGDUrc3rBhQyQnJ2slKCIiIqJXQe1EyNHREbdu3Spxe2JiIuzt7bURExEREdEroXYiFBYWhhkzZqCgoEBlW35+Pj755JNi70FGREREZKjUvvv8P//8g+bNm0MmkyEiIgJ+fn4QQuDatWtYvXo18vPzcfbsWXh4eOg6Zq2IjIxEZGQk5HI5YmNjubI0ERFRJaHJytJqJ0LA89Nf48aNw/79+1H0MolEgi5dumDVqlXw9fWtWOR6wFtsEBERVS5av8VGER8fH+zZswfp6emIi4sDAPj6+nJuEBERERkljRKhInZ2dmjZsqW2YyEiIiJ6pcp9rzEiIiIiY8dEiIiIiKosJkJERERUZTERIiIioiqLiRARERFVWVU2EYqMjIS/vz9atGih71CIiIhITzRaULEyyszMhK2tLe7evcsFFYmIiCqBrKwseHh4ICMjAzY2NqXWLdc6QpXJ48ePAcBobg1CRERE6nn8+HGZiVCVHxEqLCzE/fv3YWVlBYlEorX9FmWjlWmkqbK1ie0xbGyPYWN7DFtVb48QAo8fP4arqytMTEqfBVTlR4RMTEzg7u6us/1bW1tXig/hiypbm9gew8b2GDa2x7BV5faUNRJUpMpOliYiIiJiIkRERERVFhMhHZHJZJg1axZkMpm+Q9GaytYmtsewsT2Gje0xbGyP+qr8ZGkiIiKqujgiRERERFUWEyEiIiKqspgIERERUZXFRIiIiIiqLCZCOhIZGQlvb2+Ym5ujVatW+Ouvv/QdUrnMnj0bEolE6eHn56fvsNR27Ngx9OzZE66urpBIJPj555+VtgshMHPmTLi4uKB69eoIDQ1FXFycfoJVQ1ntGT58uEp/de3aVT/BqmHBggVo0aIFrKys4OTkhD59+uDGjRtKdfLy8hAREQEHBwdYWlqif//+SElJ0VPEpVOnPR07dlTpo/fee09PEZduzZo1aNy4sWIRu6CgIOzZs0ex3Zj6Bii7PcbUN8VZuHAhJBIJJk6cqCgztj56UXHt0UUfMRHSgW3btmHSpEmYNWsWzp8/j4CAAISFheHBgwf6Dq1cGjRogKSkJMXjxIkT+g5JbTk5OQgICEBkZGSx2xcvXoyVK1di7dq1OH36NCwsLBAWFoa8vLxXHKl6ymoPAHTt2lWpv7Zs2fIKI9TM0aNHERERgVOnTuHAgQN4+vQpXn/9deTk5CjqfPjhh/jtt9+wY8cOHD16FPfv30e/fv30GHXJ1GkPAIwaNUqpjxYvXqyniEvn7u6OhQsX4ty5czh79iw6d+6M3r1748qVKwCMq2+AstsDGE/fvOzMmTP46quv0LhxY6VyY+ujIiW1B9BBHwnSupYtW4qIiAjFc7lcLlxdXcWCBQv0GFX5zJo1SwQEBOg7DK0AIHbt2qV4XlhYKGrVqiWWLFmiKMvIyBAymUxs2bJFDxFq5uX2CCFEeHi46N27t17i0YYHDx4IAOLo0aNCiOf9Ua1aNbFjxw5FnWvXrgkAIjo6Wl9hqu3l9gghRIcOHcSECRP0F1QF2dnZia+//tro+6ZIUXuEMN6+efz4sahbt644cOCAUhuMtY9Kao8QuukjjghpWUFBAc6dO4fQ0FBFmYmJCUJDQxEdHa3HyMovLi4Orq6uqF27NoYMGYI7d+7oOyStSExMRHJyslJf2djYoFWrVkbbVwBw5MgRODk54bXXXsPYsWORlpam75DUlpmZCQCwt7cHAJw7dw5Pnz5V6iM/Pz94enoaRR+93J4imzZtgqOjIxo2bIjp06cjNzdXH+FpRC6XY+vWrcjJyUFQUJDR983L7SlijH0TERGBN954Q6kvAOP9/SmpPUW03UdV/qar2paamgq5XA5nZ2elcmdnZ1y/fl1PUZVfq1atsGHDBrz22mtISkrCnDlz0K5dO8TExMDKykrf4VVIcnIyABTbV0XbjE3Xrl3Rr18/+Pj4ICEhAf/5z3/QrVs3REdHw9TUVN/hlaqwsBATJ05EmzZt0LBhQwDP+0gqlcLW1laprjH0UXHtAYC3334bXl5ecHV1xd9//42PPvoIN27cwM6dO/UYbckuX76MoKAg5OXlwdLSErt27YK/vz8uXrxolH1TUnsA4+sbANi6dSvOnz+PM2fOqGwzxt+f0toD6KaPmAhRqbp166b4uXHjxmjVqhW8vLywfft2vPvuu3qMjIrz1ltvKX5u1KgRGjdujDp16uDIkSMICQnRY2Rli4iIQExMjFHNQStNSe0ZPXq04udGjRrBxcUFISEhSEhIQJ06dV51mGV67bXXcPHiRWRmZuLHH39EeHg4jh49qu+wyq2k9vj7+xtd39y9excTJkzAgQMHYG5uru9wKkyd9uiij3hqTMscHR1hamqqMis/JSUFtWrV0lNU2mNra4t69eohPj5e36FUWFF/VNa+AoDatWvD0dHR4Pvr/fffx++//47Dhw/D3d1dUV6rVi0UFBQgIyNDqb6h91FJ7SlOq1atAMBg+0gqlcLX1xeBgYFYsGABAgICsGLFCqPtm5LaUxxD75tz587hwYMHaNasGczMzGBmZoajR49i5cqVMDMzg7Ozs1H1UVntkcvlKq/RRh8xEdIyqVSKwMBAHDp0SFFWWFiIQ4cOKZ2HNlbZ2dlISEiAi4uLvkOpMB8fH9SqVUupr7KysnD69OlK0VcA8M8//yAtLc1g+0sIgffffx+7du3Cn3/+CR8fH6XtgYGBqFatmlIf3bhxA3fu3DHIPiqrPcW5ePEiABhsH72ssLAQ+fn5Rtc3JSlqT3EMvW9CQkJw+fJlXLx4UfFo3rw5hgwZovjZmPqorPYUd3pfK32k1anXJIQQYuvWrUImk4kNGzaIq1evitGjRwtbW1uRnJys79A0NnnyZHHkyBGRmJgooqKiRGhoqHB0dBQPHjzQd2hqefz4sbhw4YK4cOGCACC++OILceHCBXH79m0hhBALFy4Utra24pdffhF///236N27t/Dx8RFPnjzRc+TFK609jx8/FlOmTBHR0dEiMTFRHDx4UDRr1kzUrVtX5OXl6Tv0Yo0dO1bY2NiII0eOiKSkJMUjNzdXUee9994Tnp6e4s8//xRnz54VQUFBIigoSI9Rl6ys9sTHx4u5c+eKs2fPisTERPHLL7+I2rVri/bt2+s58uJNmzZNHD16VCQmJoq///5bTJs2TUgkErF//34hhHH1jRClt8fY+qYkL19VZWx99LIX26OrPmIipCNffvml8PT0FFKpVLRs2VKcOnVK3yGVy6BBg4SLi4uQSqXCzc1NDBo0SMTHx+s7LLUdPnxYAFB5hIeHCyGeX0L/ySefCGdnZyGTyURISIi4ceOGfoMuRWntyc3NFa+//rqoWbOmqFatmvDy8hKjRo0y6AS8uLYAEOvXr1fUefLkiRg3bpyws7MTNWrUEH379hVJSUn6C7oUZbXnzp07on379sLe3l7IZDLh6+srpk6dKjIzM/UbeAlGjhwpvLy8hFQqFTVr1hQhISGKJEgI4+obIUpvj7H1TUleToSMrY9e9mJ7dNVHEiGEKP94EhEREZHx4hwhIiIiqrKYCBEREVGVxUSIiIiIqiwmQkRERFRlMREiIiKiKouJEBEREVVZTISIiIioymIiRERKNmzYoHK36ldJIpHg559/1suxvb29sXz58grtY/bs2WjSpIlW4iEi3WMiRGTk7t69i5EjR8LV1RVSqRReXl6YMGEC0tLS9B2awSop2Ttz5ozS3a3LY8qUKUr3diIiw8ZEiMiI3bx5E82bN0dcXBy2bNmC+Ph4rF27VnGT30ePHpX42oKCAp3F9fTpU53tW5dq1qyJGjVqVGgflpaWcHBw0FJEqtTtN132L1FlwkSIyIhFRERAKpVi//796NChAzw9PdGtWzccPHgQ9+7dw4wZMxR1vb29MW/ePAwbNgzW1taKkY8NGzbA09MTNWrUQN++fYsdSfrll1/QrFkzmJubo3bt2pgzZw6ePXum2C6RSLBmzRr06tULFhYW+Oyzz9R6XVxcHNq3bw9zc3P4+/vjwIEDZbY5Pz8f48ePh5OTE8zNzdG2bVucOXNGsf3IkSOQSCTYvXs3GjduDHNzc7Ru3RoxMTGK7SNGjEBmZiYkEgkkEglmz56teI9ePDUmkUjw1VdfoUePHqhRowbq16+P6OhoxMfHo2PHjrCwsEBwcDASEhIUr3n51FjRMV58eHt7K7bHxMSgW7dusLS0hLOzM4YOHYrU1FTF9o4dO+L999/HxIkT4ejoiLCwsGLfl+HDh6NPnz747LPP4Orqitdee01x/JdPNdra2mLDhg0AgFu3bkEikWDnzp3o1KkTatSogYCAAERHR5fZF0SVQoXviEZEepGWliYkEomYP39+sdtHjRol7OzsRGFhoRBCCC8vL2FtbS0+//xzER8fL+Lj48WpU6eEiYmJWLRokbhx44ZYsWKFsLW1FTY2Nor9HDt2TFhbW4sNGzaIhIQEsX//fuHt7S1mz56tqANAODk5iW+//VYkJCSI27dvl/k6uVwuGjZsKEJCQsTFixfF0aNHRdOmTQUAsWvXrhLbPX78eOHq6ir++OMPceXKFREeHi7s7OxEWlqaEOL/b0xbv359sX//fvH333+LHj16CG9vb1FQUCDy8/PF8uXLhbW1teLu8I8fP1a8R8uWLVNql5ubm9i2bZu4ceOG6NOnj/D29hadO3cWe/fuFVevXhWtW7cWXbt2Vbxm1qxZIiAgQPH8xbvQx8fHC19fXzF06FAhhBDp6emiZs2aYvr06eLatWvi/PnzokuXLqJTp06K13fo0EFYWlqKqVOniuvXr4vr168X+76Eh4cLS0tLMXToUBETEyNiYmIUbXj5/bSxsVHcCDYxMVEAEH5+fuL3338XN27cEG+++abw8vIST58+LbEfiCoLJkJERurUqVOlJg1ffPGFACBSUlKEEM+/5Pv06aNUZ/DgwaJ79+5KZYMGDVJKhEJCQlSSrY0bNwoXFxfFcwBi4sSJSnXKet2+ffuEmZmZuHfvnmL7nj17Sm1Tdna2qFatmti0aZOirKCgQLi6uorFixcLIf4/Edq6dauiTlpamqhevbrYtm2bEEKI9evXK7WxSHGJ0Mcff6x4Hh0dLQCIb775RlG2ZcsWYW5urnj+ciJUpLCwUPTt21cEBgaK3NxcIYQQ8+bNE6+//rpSvbt37woA4saNG0KI54lQ06ZNi30/XhQeHi6cnZ1Ffn6+Urm6idDXX3+t2H7lyhUBQFy7dq3M4xIZO7NXPgRFRFolhFC7bvPmzZWeX7t2DX379lUqCwoKwt69exXPL126hKioKMXpLgCQy+XIy8tDbm6uYk7Ny/su63XXrl2Dh4cHXF1dlY5dmoSEBDx9+hRt2rRRlFWrVg0tW7bEtWvXVNpRxN7eHq+99ppKHXU0btxY8bOzszMAoFGjRkpleXl5yMrKgrW1dYn7+c9//oPo6GicPXsW1atXB/D8PTp8+DAsLS1V6ickJKBevXoAgMDAQLVibdSoEaRSqVp1X/ZiO11cXAAADx48gJ+fX7n2R2QsmAgRGSlfX19IJJJikxngeZJjZ2eHmjVrKsosLCw0Pk52djbmzJmDfv36qWwzNzcvcd/qvs7QVatWTfGzRCIpsaywsLDEffzwww9YtmwZjhw5Ajc3N0V5dnY2evbsiUWLFqm8pigZAdTvt+LqSSQSlWS5uMnsmraJqLJgIkRkpBwcHNClSxesXr0aH374oWKUAQCSk5OxadMmDBs2TPGlVpz69evj9OnTSmWnTp1Set6sWTPcuHEDvr6+GsVX1uvq16+Pu3fvIikpSfGl//KxX1anTh1IpVJERUXBy8sLwPMv9TNnzmDixIkq7fD09AQApKenIzY2FvXr1wcASKVSyOVyjdpTXtHR0fjXv/6Fr776Cq1bt1ba1qxZM/z000/w9vaGmZlu/hzXrFkTSUlJiudxcXHIzc3VybGIjBGvGiMyYqtWrUJ+fj7CwsJw7Ngx3L17F3v37kWXLl3g5uamdFqqOOPHj8fevXvx+eefIy4uDqtWrVI6LQYAM2fOxPfff485c+bgypUruHbtGrZu3YqPP/641H2X9brQ0FDUq1cP4eHhuHTpEo4fP650lVtxLCwsMHbsWEydOhV79+7F1atXMWrUKOTm5uLdd99Vqjt37lwcOnQIMTExGD58OBwdHdGnTx8Az68Oy87OxqFDh5CamqqzxCA5ORl9+/bFW2+9hbCwMCQnJyM5ORkPHz4E8Pyqv0ePHmHw4ME4c+YMEhISsG/fPowYMUJriVrnzp2xatUqXLhwAWfPnsV7772nNPpDVNUxESIyYnXr1sXZs2dRu3ZtDBw4EHXq1MHo0aPRqVMnREdHw97evtTXt27dGuvWrcOKFSsQEBCA/fv3qyQ4YWFh+P3337F//360aNECrVu3xrJlyxQjMiUp63UmJibYtWsXnjx5gpYtW+Jf//pXmYkbACxcuBD9+/fH0KFD0axZM8THx2Pfvn2ws7NTqTdhwgQEBgYiOTkZv/32m2L+THBwMN577z0MGjQINWvWxOLFi8s8bnlcv34dKSkp+O677+Di4qJ4tGjRAgDg6uqKqKgoyOVyvP7662jUqBEmTpwIW1tbmJho58/z0qVL4eHhgXbt2uHtt9/GlClTKrxWElFlIhGazLQkIjJwR44cQadOnZCenq7XW4UQkXHgiBARERFVWUyEiIiIqMriqTEiIiKqsjgiRERERFUWEyEiIiKqspgIERERUZXFRIiIiIiqLCZCREREVGUxESIiIqIqi4kQERERVVlMhIiIiKjKYiJEREREVdb/AZI+XqyR45C8AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Visualize the results of the multistarts for a chosen regularization strength\n", + "ax = pypesto.visualize.waterfall(regresults[chosen_regstrength], size=[6.5, 3.5])\n", + "ax.set_title(f\"Waterfall plot (regularization strength = {chosen_regstrength})\")\n", + "ax.set_ylim(ax.get_ylim()[0], 100);" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "bd083fae-d3f6-4de8-bb57-fa9f94ca3157", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAFUCAYAAAC0io2HAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACNyElEQVR4nOzdeVxU9frA8c+ZAYZ93xUBd1Tcc7fUzH1BMk0rzZ+VVlY3tXuzcqub5E3Lblm2b7fMMrPS0sokUXFfcUFFEEH2fYeZOb8/RqdIXFBgBJ7363VeMOd8zznPwRGe+a6KqqoqQgghhBCi0dBYOgAhhBBCCFG3JAEUQgghhGhkJAEUQgghhGhkJAEUQgghhGhkJAEUQgghhGhkJAEUQgghhGhkJAEUQgghhGhkJAEUQgghhGhkrCwdQF0zGo1cuHABJycnFEWxdDhCCCGEEDVCVVUKCgrw9/dHo7l6HV+jSwAvXLhAQECApcMQQgghhKgV58+fp2nTplct0+gSQCcnJ8D0w3F2drZwNEIIIYQQNSM/P5+AgABzrnM1jS4BvNTs6+zsLAmgEEIIIRqc6+niJoNAhBBCCCEaGUkAhRBCCCEaGUkAhRBCCCEamUbXB1AIIYQwGAxUVFRYOgwhqsXa2hqtVlsj15IEUAghRKOhqiqpqank5uZaOhQhboirqyu+vr43PZexJIBCCCEajUvJn7e3N/b29rIggKg3VFWluLiY9PR0APz8/G7qepIACiGEaBQMBoM5+fPw8LB0OEJUm52dHQDp6el4e3vfVHOwDAIRQgjRKFzq82dvb2/hSIS4cZfevzfbh1USQCGEEI2KNPuK+qym3r+SAAohhBBCNDKSAAohhBBCNDKSAAohhBD1wLPPPotOp2Py5MmWDkU0AJIACiGEEPXAvHnzWL58OatXr+bMmTM1fv1t27YxevRo/P39URSF9evX1/g9xK1DEkAhhBCiHnBxcWH69OloNBqOHj1a49cvKiqiU6dOrFy5ssavLW49Mg+gEEIIUU/o9Xrs7e2JiYlh3LhxNXrt4cOHM3z48Bq9prh1SQIohBCiUVJVlQqDapF7W2uVG5rO44UXXqCwsJCYmJgrllmyZAlLliy56nWOHz9Os2bNqn1/0XBIAiiEEKJRqjCorNxa833prsfjA1tiY1W9BHD//v2sWrWKkSNHXjUBnDlzJhMmTLjqtfz9/at1b9HwSAIohBBC3OKMRiMzZsxg1qxZ9OzZk/vvv5+Kigqsra0vK+vu7o67u7sFohT1iSSAQgghGiVrrcLjA1ta7N7V8eabb5KZmcmLL75IYmIiFRUVnDx5ktDQ0MvKShOwuB6SAAohhGiUFEWpdjOsJSQnJzN//nxWr16Ng4MDrVq1QqfTERMTU2UCKE3A4npIAiiEEELcwp588kmGDx/OyJEjAbCysiIkJOSK/QBvtAm4sLCw0vyC8fHxHDp0CHd3d6ktbIAkARRCCCFuURs2bOD333/nxIkTlfaHhoZedSDIjdi3bx8DBw40v549ezYAU6dO5ZNPPqnRewnLs+hE0Dcy63hkZCRdu3ZFp9PRsmVLeVPWEwaDgcjISFavXk1kZCQGg8HSIQkhxC1v1KhR5OTk4OvrW2n/Z599xvfff1+j9xowYACqql62yd/ZhsmiCWB1Zx2Pj49n5MiRDBw4kEOHDvGPf/yDhx56iM2bN9dypOJmrFu3jpYtWzJw4EAmT57MwIEDadmyJevWrbN0aEIIIUSjZNEEcPjw4fz73/++7tnMV61aRXBwMMuXLyckJIRZs2Yxfvx4Xn/99VqOVNyodevWMX78eEJDQ4mOjqagoIDo6GhCQ0MZP368JIFCCCGEBdSrtYCjo6MZPHhwpX1Dhw4lOjraQhGJqzEYDMyZM4dRo0axfv16evXqhaOjI7169WL9+vWMGjWKuXPnSnOwEEIIUcfqVQKYmpqKj49PpX0+Pj7k5+dTUlJS5TllZWXk5+dX2kTdiIqKIiEhgeeeew6NpvJbTaPRMG/ePOLj44mKirJQhEIIIUTjVK8SwBsRERGBi4uLeQsICLB0SI1GSkoKAB06dKjy+KX9l8oJIYQQom7UqwTQ19eXtLS0SvvS0tJwdnbGzs6uynPmzZtHXl6eeTt//nxdhCoAPz8/gCtOVXBp/6VyQgghhKgb9SoB7N27N1u2bKm079dff6V3795XPEen0+Hs7FxpE3Wjf//+BAUFsWTJEoxGY6VjRqORiIgIgoOD6d+/v4UiFEIIIRoniyaAhYWFHDp0iEOHDgF/zjqemJgImGrvpkyZYi4/c+ZMzp49yz//+U9OnjzJ22+/zddff83TTz9tifDFNWi1WpYvX86GDRsICwurNAo4LCyMDRs2sGzZMrRaraVDFUIIIRoVi64Ecq1Zx1NSUszJIEBwcDAbN27k6aef5o033qBp06Z88MEHDB06tM5jF9dn1Jgw3nj/cyIWPUefPn3M+4ODg1m7di3h4eEWjE4IIYRonBRVVVVLB1GX8vPzcXFxIS8vT5qDa9n57GI2xaRSWKbHaDBwNmYf5QVZDOrahgmjhkjNnxCiTpWWlhIfH09wcDC2traWDue6JSQkmGPu1q0bq1atuuLgOtHwXe19XJ0cp171ART1x/nsYtYfTKawTI+TrRUhTVzp0rMv7fqNIM2hBcdTCi0dohBC1As6nY4ePXqg0+nYsWMHY8eORa/X1/h9tm3bxogRI/Dy8kJRFBRFYdWqVZXKJCQkmI9VtS1atMhcdtGiRVcsVxvxi+qxaBOwaJhKKwxsiklFb1Rp7uXAiFA/rLUa9AYjkbEZHE3OY8vJNHTWGlr7OFk6XCGEuKX5+fmxe/duYmNjCQkJ4ezZs3zzzTdMmjSpRu9z4MABfv31V5o3b05mZmaVZXQ6HT179qy0Lzc3l9jYWHOsf+fp6UmLFi0q7VMUpYaiFjdKagBFjdt+OpPCMj1u9tbm5A/ASqvhzhBvOgW4oKrwy7FUMgvLLBytEELUD23atOH2228H4LPPPqvx6z/wwAPk5+ezefPmK5bx8/Nj165dlbZLK3S5ublx3333XXbOyJEjLztHugBZniSAokblFpdz7IJptZXB7XzMyd8liqIwoLU3zdztqTCobIpJxWBsVN1QhRDihqiqSlJSEgC//fYb6enpVZa7WtPrpS0hIeGy8zw8PK44p+6VZGVl8fHHHwPw6KOP4ujoeFmZb7/9Fjs7O/z8/Bg1ahQHDx6s1j1E7ZAmYFGj9sRnY1RVgjztaepmX2UZjUZhWAdfPos+R0ZBGfvP5dAj2B2A7Oxs8vPz8fHxqfYvIiGEqA5VVSkuLrbIve3t7avdDLp582bi4uIA0Ov1fPXVVzz55JOXlWvatOllzbR/p9PpqnXvK3n77bcpLi5Gp9PxxBNPXHZcq9Xi6+uLlZUVJ0+eZOPGjfz2229ER0fTpUuXGolB3BhJAEWNKa0wEJtaAEDPYI+rlnXQWTGgjRebYlLZcSqVres+49MP3+PEiROA6ZfG7bffzuzZsxk1alStxy6EaHyKi4urrLGqC4WFhTg4OFTrnHfeeQcwtaSoqsr//ve/KhPAhx56iIceeqhG4ryasrIyVq5cCcD999+Pr69vpeOTJ0/mySefxN3d9AF/8+bNDBs2zHzeBx98UOsxiiuTBFDUmBMp+eiNKp5OOvxcrj3FQltfJ7btP8ZLTz9E8pnj5v22traUlpaydetWtm7dSnh4OB9//LFM2yOEaLQSExPZuHEjAHPnzuXVV19l7969nDp1itatW1cq+8EHH1wzufruu+9uehnOzz77jLS0NBRFYc6cOZcd/3tcQ4cOxcPDg6ysrEpz/ArLkARQ1JjjKaa+f6FNXK6raeP48ePMnx5GRno69k6uPPPcCzw14/9wdXUlISGBt99+mzfeeIN169YRGxvLli1b8PHxqe3HEEI0Evb29hQWWmZKKnv7qrvIXMl7772HwWCgbdu2vPzyy3z88cdkZmbyv//9jxdffLFS2aSkJHbv3n3V65WV3dwAPFVVWb58OWAa5BESEnJZmaVLlzJp0iSaNWsGmJZuzcrKAiAoKOim7i9qgNrI5OXlqYCal5dn6VAalJyiMvW1X2LVFb+eUovL9Ncsf/78edXf318F1FYhHdQFX25TP4w6q1boDZXK7dmzR/Xz81MBtUOHDmpubm5tPYIQooErKSlRjx8/rpaUlFg6lGopLy9XfXx8VEB99dVXVVVV1SeeeEIF1ObNm9fYfb799lu1RYsWamBgoAqogOrl5aW2aNFCnTx5cqWy33//vbnMH3/8UeX1AgMDVUVR1GbNmqkhISGqoigqoDo4OKjHjh2rsbgbm6u9j6uT48goYFEjTqebPkUHuNthZ3P14f0VFRVMmDCBCxcu0K5dO7ZFbqVpkybklVRw6HxupbK33XYb27Ztw9/fn5iYGO69914MBkNtPYYQQtxy1q1bR1paGtbW1kyZMgUwLZkKcPbsWXbu3Fkj98nPzycuLo5z586Z92VkZBAXF0dycnKlssuWLQOgR48e5qlp/u65557jzjvvpKKigrNnzxIYGMh9993H/v37adeuXY3ELG6cLAUnasTqPYmk5pUyOMSH0KYuVy27cOFCXnzxRVxcXDh48CDBwcEcu5DHL8fSsLHSMK1vEPY2lXsnHDhwgH79+lFSUsKrr77K3Llza/NxhBANUH1dCk6Iv5Kl4MQto6TcQFp+KQDBXlcf1RYTE8OSJUsAePfddwkODgagnZ8zPs62lOuNRMdlXXZe165deeONNwB4/vnniYmJqclHEEIIIRoVSQDFTTuXXYSqgqeTDkfdlccVqarKk08+iV6vZ+zYsUyYMMF8TFEUbm/tCcDR5DwyCi7voPzQQw8xcuRIysvLuf/++ykvL6/5hxFCCCEaAUkAxU1LyDRNpBrkcfVRbRs2bGDr1q3odDpWrFhx2Ujhpm72tPZ2wL3wDGci/4e6+z3Y/S4cXgPn96DoS/nggw/w8PDg8OHDvP7667X2TEIIIURDJgmguCmqqnI+25QABrpfuflXVVUWLVoEwD/+8Y+qpwDIPc+ggu9pk/07pJ8gJ+MCFGdD9lk4swV2vY2vIZnXLk49sHjx4iqXMxJCCCHE1UkCKG5KXkkFhWV6tBoFP9crd6revHkzBw4cwN7evuoBHEn74dAX2FXk4ePhRrJzF7bYDELfcRK0HAwOnqAvh1O/8EA3JwYNuJ2SkpIqZ8EXQgghxNVJAihuSlJOCQC+zrZYa6/8dnr55ZcBmDlzJp6enpUPJu6G07+AqoJPe/yHPk2OT2+SFF8OFjhDwG3QfbopEdRoUTJi+eyZ0djZWPHjjz/y/fff19rzCSGEEA2RJIDipiTnmhLAJm52VywTFRXF9u3bsbGxuXy5oNQYiPvd9H1QPwgZjY2tPX1bmpLEPfHZ5BVXgEZjSgQ7TgStNU3sKvhs7kisNfDkk09SVFRUK88nhBBCNESSAIqbcuFiAujveuUE8NJgjWnTpuHv7//ngYI0iP3Z9H2znhDcHy4ODAnxc6KJmx3leiO/HE/FPF2lWyB0mgRWOsbe0ZVH7/Al6Xwi//nPf2r+4YQQQogGShJAccNKKwzkFlcA4OdSdf+/Cxcu8MMPPwBU7q9nNMDJDWDUg0cLaD6w0nmKojCknQ/WWoWknBIOJ+X9edClCXSciLWtHTPGD2ZUayteffVVkpKSavYBhRBCiAZKEkBxwy5N/uxqb42tddXLv3388ccYDAb69etXeemfxGgoTAdrO2gzwlzz91eu9jb0a+UFwPbTGZXnBnRpAu3CCAlpR1jPYLp4lvPcc8/V3MMJIYQQDZgkgOKGpeWbEjIf56pr/wwGA++//z4AjzzyyJ8HCtMhYYfp+1ZDQOdY5flG1YiPWwlWdkmcLz7Oe9GRrNmwji+//JLIyEgMbs1RWg9h6JChDG6uZceG/7Fv376ae0AhhLiFPPvss+h0OiZPnmzpUEQDIAmguGGXagB9nHVVHv/11185d+4cbm5ujB8//s8Dcb+DagTPVuAdctl5eqOeQ+mH+Pz453x35juMdsfZv2MNEQ/cy72j7+a+++5j4MCBtGzZknW7EvDvNozOnToxvp01C/75FI1seWshRCMxb948li9fzurVqzlz5kyNX3/RokUoilJpa9u27WXlVq5cSVBQELa2tvTs2ZM9e/bcUBlhWZIAihuWfrFJ1tup6hrADz/8EIAHHngAO7uLg0Sy402bRgst77ys6TezJJOvY79m54WdFFUUYaO1IX1PCr8v/QqfoECGR8zgvi9fYMaqp3APdmf8PfewLqaYAaPvxdnOGp+cvXy37tvae2ghhLAQFxcXpk+fjkaj4ejRo7Vyj/bt25OSkmLetm/fXun4mjVrmD17NgsXLuTAgQN06tSJoUOHkp6eXq0ywvIkARQ3pLhcT36JaQCIdxU1gPn5+WzYsAGABx980LRTVeHsVtP3/l3Azq3SOfF58Xx76ltyy3Kxt7JnYMBAprSdwpf/+ZJRo0axf+tuJg94hiZO3Snz8mfo8/fSvn97Zs15Cvsek+nRux+BLhq+e+NfGAyGWnt2IYSwFL1ej729PTExMbVyfSsrK3x9fc3b3+dtfe2113j44YeZNm0a7dq1Y9WqVdjb2/PRRx9Vq4ywPEkAxQ1Jv9j/z93BBp3V5QNAfvjhB0pLS2ndujWdO3c27cw4aZr6xcoGAvtUKp+Ql8CmhE0YVAMBTgFMbDuREI8Qdu7YSUJCAs899xyu9jom9WjOsJa9ae84kpJif9qPu52UxBRe+f5dQif/Ezs7W4KVJL79ZGVt/wiEEKLOvfDCCxQWFl41AVyyZAmOjo5X3RITE6s89/Tp0/j7+9O8eXPuu+++SuXKy8vZv38/gwcPNu/TaDQMHjyY6Ojo6y4jbg1Wlg5A1E/X6v/31VdfATBp0iQURTHV/p3baTrYtAfY/LlucFpRGr+c+wVVVWnt1ppBzQahUUyfTVJSUgDo0KEDAFqNwsC23ng42rD1pA22AW7A28QnJbC5axABA0Zx6ue1xHyzhLGT/g+dfdUDTIQQAlUFQ4Vl7q21rnL2g6vZv38/q1atYuTIkVdNAGfOnMmECROueq1Kc7Je1LNnTz755BPatGlDSkoKixcvpn///sTExODk5ERmZiYGgwEfH59K5/n4+HDy5EmA6yojbg2SAIobknax/59XFf3/srKy2Lx5MwD33nuvaWdOgmn0r9YKmnQzly0oL+Cn+J/QG/UEOAUwMGCgOfkD8PPzAyAmJoZevXqZ93ds6oqzrTX/XX0QAL2NG4XlhWgH9aQ0+ldsc3PZvOo5xsz+b40+txCiATFUQNRyy9y7/xxTa8h1MhqNzJgxg1mzZtGzZ0/uv/9+KioqsLa2vqysu7s77u7u1Q5p+PDh5u87duxIz549CQwM5Ouvv2b69OnVvp64tUkTsLgh6VepAVy3bh16vZ7OnTv/OYLs/G7TV7/OYGMPgKqq/J74OyX6EjztPBkaNBStpnJzcv/+/QkKCmLJkiUYjcZKx5q523Fk46d4+DYltN0UcgvsMVhpKBx+JyX2WmK3fEFxas2PlBNCiLr25ptvkpmZyYsvvkhoaCgVFRVXrFG7mSbgv3J1daV169bmEceenp5otVrS0tIqlUtLS8PX1/e6y4hbg9QAimorrTBQUKoHwMvp8gTw66+/Bv5S+1eQZhr5q2ig6W3mcoczDpNcmIyVxoohgUOw0V7+aVir1bJ8+XLGjx9PWFgY8+bNo0OHDsTExBAREcHvv/zMmx9+ToW1PdrSHlToDtKme29+ObqbDqdT2PbBcwx7brVp1LEQQvyV1tpUE2epe1+n5ORk5s+fz+rVq3FwcKBVq1bodDpiYmIIDQ29rPyNNgH/XWFhIXFxcTzwwAMA2NjY0K1bN7Zs2UJYWBhgqpncsmULs2bNuu4y4tYgCaCotqyicgCcbK0uGwCSm5tLZGQkAOHh4aadSXtNX73agJ2r6RolWexK2QVAX/++uNq6XvF+4eHhrF27ljlz5tCnz5+DR4KDg1m7di3h4eHsP5fNtlOZGPK7YOdxiKa3DSRO/yP2u36hz4nfcW5/180/uBCiYVGUajXDWsqTTz7J8OHDGTlyJGAaqRsSEnLFfoA32gQ8d+5cRo8eTWBgIBcuXGDhwoVotVomTZpkLjN79mymTp1K9+7d6dGjBytWrKCoqIhp06ZVq4ywPEkARbVlFZr6/3k4Xv6Lc9OmTej1ekJCQmjVqhVUlED6CdPBpt0BU9Pv9uTtGFUjgc6BtPNod9l1/i48PJyxY8cSFRVFSkoKfn5+9O/fH63WlIB2beZGdlEFMcl5lOd1pltnPZ/v34GtfxY7v3qVYfO6gX31fyEKIYQlbdiwgd9//50TJ05U2h8aGlrjU8EkJSUxadIksrKy8PLyol+/fuzatQsvLy9zmYkTJ5KRkcGCBQtITU2lc+fObNq0qdKgj+spIyxPEkBRbZdqAD0cLm/+/f777wEYO3asaUfaMTDqwdELnJsAcDbvLMmFyWgVLf2b9jeNEr4OWq2WAQMGVHlMURQGtvEivaCU9HywLrmNrr2GEbPtK75POULfg9/h1Fc6MQsh6pdRo0aRk5Nz2f7PPvusxu91afaGa5k1a9Y1m3Ovp4ywLBkEIqotu9CUALo7VK4BLC8v5+effwZgzJgxpikWLphG6eLXBRSFCmMFO5JN6wB38e6Cs41zjcVlpdUwMtQPGysN2QVWDAidRp7GgwxHDWv++AwyZUCIEEIIAZIAihuQVWRqAvZ0rFwDuG3bNvLy8vD29qZnz56QlwRFmaapX3zaA3Ak4wiFFYU4WjvSxadLjcfmam/D4BBTM0NCmh1jez5Icr6R37POEn9wNRhlhRAhhBBCEkBRLaUVBorKTEnU32sAf/jhBwBGjx6NRqOBlMOmA14hYG1LmaGMQ+mHAOjp1xNrzfWPgquONr5OtPZxwqiqNGkxjuJ8T4rLDXx8dBOl53bUyj2FEEKI+sTiCeDKlSsJCgrC1taWnj17smfPnquWX7FiBW3atMHOzo6AgACefvppSktL6yhakXlxAIiznTU2Vn++fVRVNa/9O2bMGNMEqxkX56jy6wiYav/KDGW46lxp5daqVuMc2NYLexstOSUGwm6fxdnkCs5cOM8vR/6HWppfq/cWQgghbnUWTQDXrFnD7NmzWbhwIQcOHKBTp04MHTqU9PT0Kst/+eWXPPvssyxcuJATJ07w4YcfsmbNGp577rk6jrzxyjYPAKlc+3fmzBni4+OxtrZm0KBBkHnalATauoBLAGWGMg5nmGoEb/O9rdJqH7XB3saKQW29AXBpO4Cy857kFFawPSGGmKNf1Oq9hRBCiFudRRPA1157jYcffphp06bRrl07Vq1ahb29PR999FGV5Xfu3Enfvn2ZPHkyQUFBDBkyhEmTJl2z1lDUnKwrDAC5tPRbv379cHR0NI3+BVPfP0XhSMYRyg3luNu608K1RZ3E2srHiba+TiiKhsFjHuPYznySkpKISvyD3KzTdRKDEEIIcSuyWAJYXl7O/v37GTx48J/BaDQMHjyY6OjoKs/p06cP+/fvNyd8Z8+e5aeffmLEiBF1ErP4yxQwjlUngEOHDoXyIsg+azrg04EKYwUxmab5qrr6dK312r+/uqONF7bWWprfNhhtcRMyzhVxLimRrYc/wqgar30BIYQQogGy2DyAmZmZGAyGyyaG9PHxueL6hpMnTyYzM5N+/fqhqip6vZ6ZM2detQm4rKyMsrIy8+v8fOn/dTOyL44A/uscgOXl5WzduhW4mACmnwTVCE6+4ODBqaxjlOhLcLR2pKVryzqN197Gir4tPdhyIp07Js7gx3f+iaf/eYpzozl5eB69uwyvNKG0EEII0RhYfBBIdURGRrJkyRLefvttDhw4wLp169i4cSMvvfTSFc+JiIjAxcXFvAUEBNRhxA3LlUYA79ixg6KiInx8fOjYsSOkX2r+7YCqqhxON/X96+jVsU5r/y7p4O+Cj7Mtof1HgJUX61cl89aiP3hu9n8YOHAgLVu2ZN26dXUelxBCCGEpFksAPT090Wq1pKWlVdqflpaGr69vlefMnz+fBx54gIceeojQ0FDGjRvHkiVLiIiIwGisujlv3rx55OXlmbfz58/X+LM0FjnFpuZfR51VpRHAl5p/hwwZgqaiCPKSTQe82nAu/xy5ZbnYaG2ua8m32qDRKNwZ4s2x6C1kZ6RTUa4yeqwnr34+joiPniY0NJTx48dLEiiEEKLRsFgCaGNjQ7du3diyZYt5n9FoZMuWLfTu3bvKc4qLi03zy/3FpaY7VVWrPEen0+Hs7FxpEzcmt7gCAFf7yvP3Ver/l3nKtNPZH2ydOZp5FIB27u2w0Vpu0XVPB2t+/vA/tOt5B+5+AWQfzyMvJxNnzxyWvPcSo0aNYu7cuRgMMlG0EOLW9Oyzz6LT6Zg8ebKlQxENgEWbgGfPns3777/Pp59+yokTJ3j00UcpKipi2rRpAEyZMoV58+aZy48ePZp33nmHr776ivj4eH799Vfmz5/P6NGjpQ9XLTIYDERGRvL1mq84c3g3Tro/f9bZ2dkcPmxq4r3zzjsh42IC6NWGvLI8zhecR0GhvWd7S4RuFhUVRWryeUZOeZyBE2ewO66C3F1nMJYXs/vMdzz9zNPEx8cTFRVl0TiFEOJK5s2bx/Lly1m9ejVnztT80pbbtm1j9OjR+Pv7oygK69evr7Lc9czfW1NlRO2xaAI4ceJEli1bxoIFC+jcuTOHDh1i06ZN5oEhiYmJpKSkmMu/8MILzJkzhxdeeIF27doxffp0hg4dyrvvvmupR2jw1q1bR8uWLRk4cCCLnp7B289MYfqI3ubm0j/++ANVVQkJCcHX3RlyE00nerbmWJapL2BTp6a46Fws9QgA5vfRmIE96T54LE6efkRtTaP4QgZl2XEUuuRUKieEELcaFxcXpk+fjkaj4ejRozV+/aKiIjp16sTKlSuvWOZ65u+tqTKilqmNTF5engqoeXl5lg7llvftt9+qiqKoo0ePVqOjo9UPfz+uPvnGGnXQkOGqoijqt99+qz7xxBMqoD766KOqeuGQqv6+RFX3fKBWGCrUD49+qK48uFKNy42z9KOoW7duVQF1+46d6kfbz6rhsxaoCqhzxvqpb307UZ3z5n0qoG7dutXSoQohaklJSYl6/PhxtaSkxNKh3LD8/HzV0dFRffHFF2v1PoD63XffXba/R48e6uOPP25+bTAYVH9/fzUiIqLGy4iqXe19XJ0cp16NAhZ1x2AwMGfOHEaNGsX69evp2bMnpYo1QSGdWf3Nt+Y+c5emfxk4cGCl5t+zeWcp1ZfiYO1AkHOQ5R7kov79+xMUFMTSVyLo28KDHkPvxsndi592pKE7n8svn0Xi1cSL3n2r7n8qhGh4VFWlwlBhkU29Qr/1a3nhhRcoLCwkJibmimWWLFmCo6PjVbfExMRq3/t65u+tqTKi9llsHkBxa4uKiiIhIYHVq1ej0WgoLtdTVmFEUcDdQce8efPo06ePufwd/XrBqdWmF55tOJ66A4B2Hu0sMvXL32m1WpYvX8748ePhkfvpGfZ/9BtzHz99soKlSyKJyyjjoYVjOJp1lO6+3S0drhCiDuiNet4/+r5F7v1w6MNYa62vXfAv9u/fz6pVqxg5cuRVE8CZM2cyYcKEq17L39+/WveG65u/t6bKiNonCaCo0qW+cB06dAAg5+IIYEedFVZajXk/QPv27fHWFIDRAPbu5FvbcKHwAgoKbd3b1n3wVxAeHs7atWuZM2cOP/44zrw/IbOMZ/+vB007O7L/wk5au7fG2UZGiwshbh1Go5EZM2Ywa9Ysevbsyf33309FRQXW1pcnke7u7ri7u1sgSlGfSAIoquTn5wdATEwMvXr1IvfiHIBu9jbm/ZcMHDgQMmNNL7zacCrHtM6uv6M/TjZOdRj1tYWHhzN27FiioqL4afdxtkVuZfemtSgpqfhrQrmQc47oC9EMDRpq6VCFELXMSmPFw6EPW+ze1fHmm2+SmZnJiy++SGJiIhUVFZw8eZLQ0NDLyi5ZsoQlS5Zc9XrHjx+nWbNm1YrheubvrakyovZZvm1O3JIu9ZlbsmQJRqPRPAegm4M1RqORiIgI8yfPgQPuMK/9q3q0IjbHlAy2cW9jmeCvQavVMmDAAObNeoiwmfOwdXDiy53n8U4qRClMJS7jGBcKL1g6TCFELVMUBWuttUU2RVGuO87k5GTmz5/PypUrcXBwoFWrVuh0uis2A8+cOZNDhw5ddbuRJuDrmb+3psqI2ic1gKJKf+0zFxYWxu33PEyFcxPOHY8n7J8r2bBhg7kT84AuLeDcabBxIE2rIa8sD2uNNS1cWlj4Ka7OzcGG7q2a0D/sAX794m02/byPQTOHcjzvPNuTtzO+9fhbov+iEKJxe/LJJxk+fDgjR44EwMrKipCQkCsmgDfaBFxYWFhpfsH4+HgOHTqEu7u7ubZw9uzZTJ06le7du9OjRw9WrFhRaf7emiwjapckgOKKKveZG2PeHxwczNNPP81rr71Gx44dcTdmmQ64Nyc2xzQSuLlL82p3cLaEns3dGRQ+lW3rPuGjyLPcM7IAG1+VzPzznMg+QXsPy05gLYRo3DZs2MDvv//OiRMnKu0PDQ296kCQG7Fv3z5Tl56LZs+eDcDUqVP55JNPANP8vRkZGSxYsIDU1FQ6d+5caf7emiwjapei3uhY9HoqPz8fFxcX8vLyZFm466TX65nz5hqyM9K4p38oI4cM4oknnuCdd97hqaeeYsWkdlCchT5kDJ9kRFNuKGd0i9EEOAVYOvTrsu1UBovmP8fWrz9gflhb7p4xnB1aPbZ+nbkv5D50Wp2lQxRC1IDS0lLi4+MJDg7G1tbW0uEIcUOu9j6uTo4j7VvimkoNENThNroNGsXIoYPRarXm+f/u6tcdirNA0ZCghXJDOY7WjjR1bGrhqK/fbUHu3DVhOlY2Ot77NRb75HzcSgsoLc5ib+peS4cnhBBC1DhJAMU15ZWYBoA42Vqj1ShkZGSY52rq1840WhiXpsQWJADQ2r11tTo4W5qdjZZBXVrRe8QE0opUvvh5D/10fpB7jqOZR8kpzbF0iEIIIUSNkgRQXFP+xQTQxc7Up2/HDtMkz+3bt8elIhOAUtcAzhecB6C1W2sLRHlzujRzY8TkR9BaW/P2zzEYUnIILK9ALS1gV8ouS4cnhBBC1ChJAMU15V0hAby9by/INS0nlGBjg1E14m7rjrtt/ZuA1MZKw1092tFjSDgZxSqf/byX3ra+KLmJxOfFy7QwQgghGhRJAMU1XSkBvOu2tmDUg60Lp0szAGjhemtP/XI1HZu6MuqBmWg0Wt744QDFadm0MwCl+ey8sPOG1+4UQgghbjWSAIprupQAOttZUVJSwr59+wDo3doTgBLXAJIKkwBo6drSMkHWAGuthmG9O9HtzjFkl6h88vN+btN5Y52fTHpxOqdzT1s6RCGEEKJGSAIorumvfQD37dtHRUUFvr4++FgVAhCv06GqKp52nrjZulky1JsW2sSFMVMfR1EUln8bTV5GNl0NVlCax+6U3eiNekuHKIQQQtw0SQDFVekNRgrLTEmPi521ufl36O09UMoKQKMlzlAE1O/m30ustRpG9u9K5ztGkFsKn2w+REedJw75qRSU5XM086ilQxRCCCFumiSA4qoKSvWoqmmQhJ21lu3btwMw9DbTSN9iR2+SilOB+t38+1eXagEBln61jdzsXHqpOijNZX/afkr0JRaOUAghhLg5kgCKqzL3/7O1QlVVdu7cCUCPVt4AxOtszc2/LjoXi8VZk6y0Gsbd2ZsOfQaTV6ry8S+HaG3timdhFuX6Mval7rN0iEIIcUtISEhAURQOHTpk6VBu2CeffIKrq2u1zmkIzy0JoLiqPweAWHPy5ElycnJwsLcnyMU00XMc5UDDqf27pL2/M2HTZgHw8ue/k51XQB/soCSHmKwYcktzLRugEMJiDAYDkZGRrF69msjISAwGg6VDspiAgABSUlLo0KGDpUNh0aJFdO7c2dJhXNGDDz5IWFiYpcMwkwRQXFV+6Z8DQC41/47o3xmtqqdEa0Wy3jQQpCH0//srK62Ge4bdQdvbbie/1Minvx2lqZUjgcX5qEajTA4tRCO1bt06WrZsycCBA5k8eTIDBw6kZcuWrFu3ztKh1bny8nK0Wi2+vr5YWVlZOhxRTZIAiqv66xyA5gEgF/v/Jdo5ogIeth4Npvn3r9r7uxA27QkAFn/yC7mFxfTGDqUkh7N5Z2VyaCEamXXr1jF+/HhCQ0OJjo6moKCA6OhoQkNDGT9+fK0lgUajkYiICIKDg7Gzs6NTp06sXbsWAFVVGTx4MEOHDjXPVZqdnU3Tpk1ZsGABAJGRkSiKwsaNG+nYsSO2trb06tWLmJiYSvfZvn07/fv3x87OjoCAAJ588kmKiorMx4OCgnjppZeYMmUKzs7OPPLII5c1hV661+bNm+nSpQt2dnYMGjSI9PR0fv75Z0JCQnB2dmby5MkUFxdf1zP+9bpbtmyhe/fu2Nvb06dPH2JjYwFTM+7ixYs5fPgwiqKgKAqffPIJAK+99hqhoaE4ODgQEBDAY489RmFhYbX+Dfbs2UOXLl2wtbWle/fuHDx4sNJxg8HA9OnTzfG3adOGN954w3x80aJFfPrpp3z//ffm+CIjIwH417/+RevWrbG3t6d58+bMnz+fioqKasV3Q9RGJi8vTwXUvLw8S4dSL/xvV4L62i+x6pn0ArV58+YqoB759J+q+vsS9ef9b6srD65Ud1/Ybekwa83RpFy1ZaeeKqCueGqcqv6+RI3cMk9deeAt9ZvYb1Sj0WjpEIUQ16mkpEQ9fvy4WlJSUu1z9Xq9GhQUpI4ePVo1GAyVjhkMBnX06NFqcHCwqtfraypcs3//+99q27Zt1U2bNqlxcXHqxx9/rOp0OjUyMlJVVVVNSkpS3dzc1BUrVqiqqqr33HOP2qNHD7WiokJVVVXdunWrCqghISHqL7/8oh45ckQdNWqUGhQUpJaXl6uqqqpnzpxRHRwc1Ndff109deqUumPHDrVLly7qgw8+aI4jMDBQdXZ2VpctW6aeOXNGPXPmjBofH68C6sGDByvdq1evXur27dvVAwcOqC1btlTvuOMOdciQIeqBAwfUbdu2qR4eHuorr7xy3c946bo9e/ZUIyMj1WPHjqn9+/dX+/Tpo6qqqhYXF6tz5sxR27dvr6akpKgpKSlqcXGxqqqq+vrrr6u///67Gh8fr27ZskVt06aN+uijj5rv/fHHH6suLi5X/PkXFBSoXl5e6uTJk9WYmBj1xx9/NP89vPTc5eXl6oIFC9S9e/eqZ8+eVf/3v/+p9vb26po1a8zXmDBhgjps2DBzfGVlZaqqqupLL72k7tixQ42Pj1d/+OEH1cfHR126dOkV47na+7g6OY4kgOKqVm49rb72S6x6LO6cCqg6K0Ut2bRILd/yb/XdfW+oKw+uVNOL0i0dZq3RG4zqM298oQKqs71Ozdu4UC36bbH63s5/qysPrlRPZZ+ydIhCiOt0MwngpQQkOjq6yuM7d+5UAXXr1q03GWVlpaWlqr29vbpz585K+6dPn65OmjTJ/Prrr79WbW1t1WeffVZ1cHBQT53683fTpdi/+uor876srCzVzs7OnKBMnz5dfeSRRyrdIyoqStVoNOafV2BgoBoWFlapzJUSwN9++81cJiIiQgXUuLg4874ZM2aoQ4cOve5nrOq6GzduVAFzfAsXLlQ7dep0pR+l2TfffKN6eHiYX18rAXz33XdVDw+PSu+bd955p9JzV+Xxxx9X7777bvPrqVOnqmPHjr1mfK+++qrarVu3Kx6vqQRQGu3FFZVWGCirMAJw4vABAO7s2hJbGxvitaDXWuFo7Yinnaclw6xVWo3CA+Ej+ObdLiQcP8hnW2OZNaIjXcrK2GOrsitlF8EuwVhp5L+SEA1ZSkoKwBUHO1zaf6lcTTlz5gzFxcXcddddlfaXl5fTpUsX8+t77rmH7777jldeeYV33nmHVq1aXXat3r17m793d3enTZs2nDhxAoDDhw9z5MgRvvjiC3MZVVUxGo3Ex8cTEhICQPfu3a8r7o4dO5q/9/HxMTdv/nXfnj17qvWMf7+un58fAOnp6TRr1uyKsfz2229ERERw8uRJ8vPz0ev1lJaWUlxcjL29/TWf5cSJE+am80v++rO8ZOXKlXz00UckJiZSUlJCeXn5dQ1KWbNmDf/973+Ji4ujsLAQvV6Ps7PzNc+7WfJXS1zRpRVA7G20HNi3F4DB3UyjfeOtTesCB7sEoyiKZQKsI5f6Aq545v94YdUPTBvcgU7W1hwrK6BAUYjJjKGzd2dLhymEqEWXko2YmBh69ep12fFL/ekulaspl/qqbdy4kSZNmlQ6ptPpzN8XFxezf/9+tFotp09Xf9nKwsJCZsyYwZNPPnnZsb8mVw4ODtd1PeuLfyMAFEWp9PrSPqPRaL43XPsZq7ouYL5OVRISEhg1ahSPPvooL7/8Mu7u7mzfvp3p06dTXl5+XQng9fjqq6+YO3cuy5cvp3fv3jg5OfHqq6+ye/fuq54XHR3Nfffdx+LFixk6dCguLi589dVXLF++vEbiuhpJAMUVXRoB7GxnbX4T92jlhVFVSVBM0x4EuwRbLL66otEoPDQpnLXvLSfp9DE+++MUj97Vlh6lFWzVqexL20cb9zbYWdlZOlQhRC3p378/QUFBLFmyhPXr16PR/DmG8q8DGPr371+j923Xrh06nY7ExETuuOOOK5abM2cOGo2Gn3/+mREjRjBy5EgGDRpUqcyuXbvMyVxOTg6nTp0y1+x17dqV48eP07Jl3U/pdb3PeC02NjaXTcmzf/9+jEYjy5cvN/+bff3119W6bkhICJ9//jmlpaXmWsBduyrPBLFjxw769OnDY489Zt4XFxd3zfh27txJYGAgzz//vHnfuXPnqhXfjZJRwOKK8ktNS8A52mjZu3cvTjbQws+VVGMxpdZ26LQ6/B39LRxl3Qjxc2bc/5k+Gc9buY5ig0IboxaPilLKDeXsT9tv4QiFELVJq9WyfPlyNmzYQFhYWKVRwGFhYWzYsIFly5ah1Wpr9L5OTk7MnTuXp59+mk8//ZS4uDgOHDjAm2++yaeffgqYas4++ugjvvjiC+666y6eeeYZpk6dSk5OTqVrvfjii2zZsoWYmBgefPBBPD09zfPS/etf/2Lnzp3MmjWLQ4cOcfr0ab7//ntmzZpVo89zo894PYKCgoiPj+fQoUNkZmZSVlZGy5Ytqaio4M033+Ts2bN8/vnnrFq1qlrxTZ48GUVRePjhhzl+/Dg//fQTy5Ytq1SmVatW7Nu3j82bN3Pq1Cnmz5/P3r17L4vvyJEjxMbGkpmZSUVFBa1atSIxMZGvvvqKuLg4/vvf//Ldd99VK74bJQmguKKCiwlgzoV48vPz6eBvj7eXN/FWVqC1Isg5CI3SON5CGo3CzCkT8A1qTV5+IV9uj0ejKPQpKQfVSExmDHlleZYOUwhRi8LDw1m7di1Hjx6lT58+ODs706dPH2JiYli7di3h4eG1ct+XXnqJ+fPnExERQUhICMOGDWPjxo0EBweTkZHB9OnTWbRoEV27dgVg8eLF+Pj4MHPmzErXeeWVV3jqqafo1q0bqamp/Pjjj9jY2ACmvnV//PEHp06don///nTp0oUFCxbg7183H/Kv9ozX6+6772bYsGEMHDgQLy8vVq9eTadOnXjttddYunQpHTp04IsvviAiIqJasTk6OvLjjz9y9OhRunTpwvPPP8/SpUsrlZkxYwbh4eFMnDiRnj17kpWVVak2EODhhx+mTZs2dO/eHS8vL3bs2MGYMWN4+umnmTVrFp07d2bnzp3Mnz+/WvHdKEVVL04c1Ejk5+fj4uJCXl5enXSyrM9+PHyBM+mF5B3+hcXPPMELd3fkxccm8oV1OflO3gwLGkZz1+bXvlADYTSqzPr3W7yz8Enc3V1J/uY5bBU9Pzo5ct5KS3PX5gwLGmbpMIUQV1BaWkp8fDzBwcGVOvRXl8FgICoqipSUFPz8/Ojfv3+N1/zVpMjISAYOHEhOTk61lzwTt56rvY+rk+M0juobcUMu1QCeOmqa8LJXax+yjKXkW+vQKloCnAIsGV6d02gUnnjoAbyaBpGdncvq6EQA+hSXoqgqZ3PPklqUauEohRC1TavVMmDAACZNmsSAAQNu6eRPiCuRBFBc0aVBIEcO7sPbQSG4qQ/xhiLQORHgFIC11voaV2h42vq5EPagqU/MP1d8RbnGFg+DnraYmlF2XtjJ9Vaqy3qiQgghLEUSQFGlcr2RknIDZSXFnDgWQ5CrhqZNmhJvpQVF0yhG/1ZFURSenjkNd58mZGZmsmaPac6vHoV5WKGQWpTK2byz17yOrCcqhKgrAwYMQFVVaf4VlUgCKKpUcLH2L+3scYxGI12D3VEcbcnUalFQCHQOtHCEltPW342xD5o69z7z2uforR1x0FfQRTHNJxV9IRqD8cq1eZZaT1QIIYS4RBJAUaVL/f9SzpgmN+3TrimJFQVg64KPgw/21jUzeWZ9pCgKc2fNwMXTh7TUVHMtYOf8TOw1NuSX5xOTFVPluQaDgTlz5jBq1CjWr19Pr169cHR0pFevXqxfv55Ro0Yxd+5caQ4WQghRqyQBFFW6lAAmnjyMj4NCcIAf54zFoHOkmdOVl9xpLEKauDPuwccB+McrH1Ju5Yi1vpzbLtYC7kvdR6m+9LLzoqKiSEhI4Lnnnqs0kSyARqNh3rx5xMfHExUVVfsPIYQQotGSBFBU6dIAkDPHDhHspsGvSROSLvb/C3IJsmxwtwBFUZj39CxTX8CMdD7+Ix6AkJwU3K2dKDOUcSDtwGXnWWo9USGEEOKvajwBLCkpqVb5lStXEhQUhK2tLT179jQvDn0lubm5PP744/j5+aHT6WjdujU//fTTzYQsqlBQWkFeVhoZqRdo7qYFbxf0OiccrB3wsPWwdHi3hFZ+rtw7cw4A/1z2ESUaBzSGcnorpiXhjmQeuWxy6L+uJ1qV2lpPVAghhPirGksAy8rKWL58ebVm7V6zZg2zZ89m4cKFHDhwgE6dOjF06FDS09OrLF9eXs5dd91FQkICa9euJTY2lvfff/+yxaPFzcsv0XPuxBEU4LbWvlxQSsHWhWZOzcwLcDd2iqIw97GH8GnWgvy8PFZuPg5As8wEmtp5Y1SN7E6pvBD4X9cT/fsC5rW5nqgQQgjxV9VKAMvKypg3bx7du3enT58+rF+/HoCPP/6Y4OBgVqxYwdNPP33d13vttdd4+OGHmTZtGu3atWPVqlXY29vz0UcfVVn+o48+Ijs7m/Xr19O3b1+CgoK444476NSpU3UeQ1yH/NIKEmMP4+uoENzsYv8/G0cCXRrv6N+qBHk5ct/j/wRg/hufk6/aoRgq6GO0QkHhTO6ZSpNDW2o9USFE4zVgwAD+8Y9/mF8HBQWxYsUKi8VzMxISElAUhUOHDlk6lHqvWgngggULeOeddwgKCiIhIYF77rmHRx55hNdff53XXnuNhIQE/vWvf13XtcrLy9m/fz+DBw/+MxiNhsGDBxMdHV3lOT/88AO9e/fm8ccfx8fHhw4dOrBkyZKrjpgsKysjPz+/0iauzmBUKSzTc+7kYYLdNLg29SPf2haNRkuAY+Na/eNaFEXhqen3EdC6A6XFxby23rRqimf6aVo7NgVM08L8dXJoS60nKoQQAHv37uWRRx6xdBjCwqqVAH7zzTd89tlnrF27ll9++QWDwYBer+fw4cPce++91aq1yMzMxGAw4OPjU2m/j48PqalVL6d19uxZ1q5di8Fg4KeffmL+/PksX76cf//731e8T0REBC4uLuYtIEASmGspLNNj0BtIOnWMIFcFo58r2LrSxLFJo1z941qaeTgw9cl5ACxZ9RVZRgcw6ulZWopW0ZJSlEJ8Xnylc8LDwzlz5gxbt27lyy+/ZOvWrZw+fVqSPyFuYYsWLeKll16q8thLL73EokWL6jagG+Tl5YW9feOdykuYVCsBTEpKolu3boBptKJOp+Ppp5+usz5hRqMRb29v3nvvPbp168bEiRN5/vnnWbVq1RXPmTdvHnl5eebt/PnzdRJrfVZQWkFq4hnKS4po5WVLnqPW1P/PWaZ/uZKZk8No2aknFRXlLPxiJwCOmXF0djT9zKJTotEb9ZXOkfVEhahftFotCxYsuCwJfOmll1iwYEGt/R9eu3YtoaGh2NnZ4eHhweDBgykqKgLgwQcfJCwsjMWLF+Pl5YWzszMzZ86kvLz8itf7exOwoih88MEHjBs3Dnt7e1q1asUPP/xQ6ZyYmBiGDx+Oo6MjPj4+PPDAA2RmZl7xHp988gmurq5s3ryZkJAQHB0dGTZsWKUZDoxGIy+++CJNmzZFp9PRuXNnNm3aVOk6e/bsoUuXLtja2tK9e3cOHjx42b2uFdvVfn6NWbUSQIPBgI2Njfm1lZUVjo6ON3RjT09PtFotaWlplfanpaXh6+tb5Tl+fn60bt260n+ykJAQUlNTr/hm1+l0ODs7V9rE1eWX6Ek8eQR/J4WmzfxJUcvBxoFAJ+n/d8nf1/H1ddbxyJznAXjn029INriCqtIlLxMHK3vyyvI4knHEskELIW7K/PnzefHFFyslgZeSvxdffJH58+fX+D1TUlKYNGkS//d//8eJEyeIjIwkPDy8UreSLVu2mI+tXr2adevWsXjx4mrdZ/HixUyYMIEjR44wYsQI7rvvPrKzswHT7BuDBg2iS5cu7Nu3j02bNpGWlsaECROues3i4mKWLVvG559/zrZt20hMTGTu3Lnm42+88QbLly9n2bJlHDlyhKFDhzJmzBhOnz4NQGFhIaNGjaJdu3bs37+fRYsWVTr/emK7np9fo6VWg6Io6ogRI9Rx48ap48aNU62srNQhQ4aYX1/arlePHj3UWbNmmV8bDAa1SZMmakRERJXl582bpwYGBqoGg8G8b8WKFaqfn9913zMvL08F1Ly8vOs+p7HZFZep9hx+j9o3QKt+vvh2deWmx9X/Hf+fpcO6ZXz77bdqUFCQCpi3oKAg9ZP/faV26D1IBdR7wkaoauRSVf19iXry7K/qyoMr1fcOv6cWlhdaOnwhGq2SkhL1+PHjaklJyU1d58UXX1QB1cbGRgXUF198sYYivNz+/ftVQE1ISKjy+NSpU1V3d3e1qKjIvO+dd95RHR0dzX8r77jjDvWpp54yHw8MDFRff/1182tAfeGFF8yvCwsLVUD9+eefVVVV1ZdeekkdMmRIpfueP39eBdTY2Ngq4/r4449VQD1z5ox538qVK1UfHx/za39/f/Xll1+udN5tt92mPvbYY6qqquq7776renh4VPr3euedd1RAPXjw4HXFdq2fX310tfdxdXKcatUATp06FW9vb3N/uvvvvx9/f/9KfexcXFyu+3qzZ8/m/fff59NPP+XEiRM8+uijFBUVMW3aNACmTJnCvHnzzOUfffRRsrOzeeqppzh16hQbN25kyZIlPP7449V5DHEN+aV6Ei8OADH6uoCtS6Ne+/evrraO77QHJtH39gEoGg3frP+Jo9mm2vLWGfH42HlTYaxg14VdFn4CIcTNmj9/PjY2NpSXl2NjY1MrNX+XdOrUiTvvvJPQ0FDuuece3n//fXJyci4r89c+fb1796awsLBaXZ46duxo/t7BwQFnZ2fzlGyHDx9m69atODo6mre2bdsCEBcXd8Vr2tvb06JFC/NrPz8/8zXz8/O5cOECffv2rXRO3759OXHiBAAnTpygY8eO2NraVnq2v7pWbNfz82usrKpT+OOPP67Rm0+cOJGMjAwWLFhAamqquf3/0sCQxMTESstlBQQEsHnzZp5++mk6duxIkyZNeOqpp6575LG4PunZuaQnniGgjxUlrnZg5yoJIJev43vpvXlpHd+wsDA2rfmYXsPuJvqnb5j+8mfseu0+NEWZ9PNqybcl6cTmxNLesz2+DlV3cxBC3Ppeeuklc/JXXl7OSy+9VGtJoFar5ddff2Xnzp388ssvvPnmmzz//PPs3r27WvPuXou1deUBfoqimOcqLSwsZPTo0SxduvSy8642aX1V11RruOn1WrHV1c+vPrrpiaCTkpJISkq64fNnzZrFuXPnKCsrY/fu3fTs2dN8LDIykk8++aRS+d69e7Nr1y5KS0uJi4vjueeek87zNezooYP4OajY+bhisNVhbeuCv4O/pcOyuOtZx/dcQjwjhg9HZ2fP3v0H+fWUqaOxT8ox2rq2Ml0nOUr6nwhRT/21z19ZWdllfQJrg6Io9O3bl8WLF3Pw4EFsbGz47rvvzMcPHz5caRWuXbt24ejoWGOzXnTt2pVjx44RFBREy5YtK20ODg43dE1nZ2f8/f3ZsWNHpf07duygXbt2gKmP/5EjRygt/XNd9V27KreiXE9s1/r5NVY3lABeGrnj4uJCYGAggYGBuLq68tJLL122uoGoX1RV5cTh/QS7arAO9gVbFwKcmqHVSJJ9vev4BrjYMGTyDAAeWvw+5Vp7KCugl0GDjdaGjOIMTmafrJughRA1pqoBH1UNDKlJu3fvZsmSJezbt4/ExETWrVtHRkYGISEh5jLl5eVMnz6d48eP89NPP7Fw4UJmzZp12QfVG/X444+TnZ3NpEmT2Lt3L3FxcWzevJlp06ZddR7ea3nmmWdYunQpa9asITY2lmeffZZDhw7x1FNPATB58mQUReHhhx82P9uyZcuqFdv1/Pwaq2o1AV/y/PPP8+GHH/LKK6+Y2++3b9/OokWLKC0t5eWXX67RIEXdKakwEH/iMJ1dNeDnKtO//MVf1/Ht1avXZccvreMbGNCEubPvIOr71SQlJfHptgQe7uuNffJBurfow86MQ+xK2UVz1+botLo6fQYhxI0zGAxVjva99PpmkqErcXZ2Ztu2baxYsYL8/HwCAwNZvnw5w4cPN5e58847adWqFbfffjtlZWVMmjSpRuckvFRT969//YshQ4ZQVlZGYGAgw4YNu6kk88knnyQvL485c+aQnp5Ou3bt+OGHH2jVytRa4ujoyI8//sjMmTPp0qUL7dq1Y+nSpdx9993XHdv1/PwaK0W9gbYof39/Vq1axZgxYyrt//7773nsscdITk6usQBrWn5+Pi4uLuTl5cmUMFVIzSulXctmPBKaj8vUQbiEDGRql8dwsL6xav6GxGAw0LJlS0JDQyv1AQRTrXhYWBgxMTGcPn0aIwozFrzOx0vm4uDoyIUfX8FZzcfg0441ShG5Zbl08upE3yZ9r3JHIURNKi0tJT4+nuDg4EoDC+qzBx98kNzcXPPSrKLhu9r7uDo5zg2l7tnZ2eZRNn/Vtm1b87xBon46fTYBp4pMytx0ODq74+kSKMnfRdVZx9daq+GpGf9H01btKSos5KU1u0FR0KYdp6+zaVTckcwjZJVkWfiphBBCNEY3lAB26tSJt95667L9b731Fp06dbrpoITlRO/eRbCrBqO/B1oHNxn9+zfVWcc3tKkrU/5hahp67b0vOFtsSqQDU44T7ByEqqpsS9omA0KEEELUuRvqA/if//yHkSNH8ttvv5nn5ImOjub8+fP89NNPNRqgqFsH9u0j0E2D6uMKtjL9S1XCw8MZO3YsUVFRpKSk4OfnR//+/S8bja7RKDwycRQ/fzOSg5Ebmfby/9gacQ+aglT6ebclSZNMSlEKJ7NPEuIhHZKFENX395kyhLheN1QDeMcdd3Dq1CnGjRtHbm4uubm5hIeHExsbS//+/Ws6RlGHThzeh4efNbYujtg6+uBt723pkG5J17uOb6CHA7PmLcLG1p5tO/ew6aRpWhin83u4zdM08Wp0SjQl+pIqzxdCCCFqww3VAIJpIIiM9m1Y9Ho9peePUNLKFj8XD5q5t0Gj1Mw0Ao1ZWN+OrL//cX784FWmLVhJwprnsSsvJrQoj5O27mSXZrPrwi4GNhto6VCFEEI0Ejf81z0nJ4dly5Yxffp0pk+fzvLly2UASD137Ngx/O3KKXazxdHTn0CXIEuH1CC4O9jwxJNP4t00mPS0DJZ/fxgAbfJB7nBvD8CJ7BOkFKZYMkwhGg3pdyvqs5p6/95QArht2zaCgoL473//S05ODjk5Ofz3v/8lODiYbdu21Uhgou5t3xlNUx8rcHfGxtGDAKeamUVeQP+2fkx4cgEAi/77GYmldqAa8Tt/gBB304j6bUnbMBhrfh4xIYTJpaXJiouLLRyJEDfu0vv370vtVdcNNQE//vjjTJw4kXfeecfc98lgMPDYY4/x+OOPc/To0ZsKSljG7uiduDSxQW/vSBOP1thaNYx5sm4FttZapt8bxh8/rObo9l94+D/f8POiMDT5F+jl1Zp4K1uySrM4mH6Q7r7dLR2uEA2SVqvF1dWV9PR0AOzt7VEUxcJRCXF9VFWluLiY9PR0XF1db3oZ3BtKAM+cOcPatWsr3Vyr1TJ79mw+++yzmwpIWE7y8V1YddTh6OhKoIxKrXEdm7gwbfYCnt0bxS+RO/khZiBh7RywOxdNv9aD+C1lJ/vS9tHctTnutu6WDleIBsnX1xfAnAQKUd+4urqa38c344YSwK5du3LixAnatGlTaf+JEydkHsB6Kj8/HzU3niInTzzdm0j/v1qg0ShMGNiVPx78B9+/G8GDz71JwrrFuOpLaJWZwGnnQM7ln2Nr4lbGtRonA3CEqAWKouDn54e3tzcVFRWWDkeIarG2tr7pmr9LbigBfPLJJ3nqqac4c+aMeU3UXbt2sXLlSl555RWOHDliLtuxY8caCVTUrn379uHbzAZ0tri4BUoNVC1p4mrHI489zoHIDZyPPcqcd3/jw8f6oaSf4I52o/mqKIW04jSOZh6lk5d8mBKitmi12hr7QypEfXRDawFfa/FnRVFQVRVFUWplceybIWsBV23pkn8Td+w19K386T98AdN6TrB0SA1Wcbmelz/bxCszx2E06Nn+8UL6BurAzo1jzXvzx4UdWGmsmNhmIi46F0uHK4QQop6oTo5zQzWA8fHxNxSYuHWdPbSNIk8b7OxcaO7ZztLhNGj2NlZMHNaPPRMe4rfVq5j8wruc/Gw2diU5tMtN54xjE5ILk4k8H8mYFmOkk7oQQogad0MJYGCgLA/WkKiqSkbKYaz8tTg4+tDCPcjSITV4HfxduP/RpzkctYnEpAT+/c0BXp7QESV5HwPajWZNcRrJhcnEZMYQ6hVq6XCFEEI0MNXqZf7YY49RWFhofr169WqKiorMr3NzcxkxYkTNRSfqxPnz57F2LgVFwdUtFHcHmf6ltmk0CsM6NWPCP14CYMmqr9ifYuou4RIfRW9v01QwOy/sJKc0x2JxCiGEaJiqlQC+++67lSbQnDFjBmlpaebXZWVlbN68ueaiE3Vib/R2bP1s0Nna4+jcATtr6RhdF3ycbbln9FD6jX0AgPB/rqTIoIWSXDrkZRDgFIBBNfBb4m8yQbQQQogaVa0E8O/jRWQ5nYbh2L5fKXWwRrFzwsclRPqc1aE+LTy474ln8Q5oTmJyKs98GImKinLhAIOcW6DT6sgozmBf2j5LhyqEEKIBkYnGBOeS96EqoNM1wcvB1dLhNCrWWg2jugZx/7OvotFa8c5XP7M1NhcAhzOR3OHXG4ADaQdILUq1YKRCCCEaEkkAG7mysjLyjabEws29M062N7e2oKi+pm72jLqzH0MfmAXAxOfeJbvEAKV5tEw/Q2u31qio/HbuN8oN5RaOVgghRENQ7VHACxYswN7eHoDy8nJefvllXFxMc5XJAtv1z8F9u9B6adFYW+Hm2w9n2xsaGC5uUt8WnpyZ+hgn9vxBwvGDzFixkTXPjkaTdoz+rYdywdqR/PJ8/kj6g8HNBkszvRBCiJtSrb/2t99+O7GxsebXffr04ezZs5eVEfXHnj3fY9Rq0Nm4oNMFSg2ghdhYaRjWsQnxzyxl+aNhrP11F5/1ac+Dd7RAF/c7d7UbyfrkSE7nnMbf0Z/2Hu0tHbIQQoh6rFoJYGRk5GX7Lg0EkRqJ+un0+b0A2Gv9URQNznZSA2gpAe723NWrE+eeXMiX//kX01/8iB6fP0s7fyf8zu2mV5PbiE7dw/ak7fjY++Bp52npkIUQQtRTN9wH8MMPP6RDhw7Y2tpia2tLhw4d+OCDD2oyNlEHMkvPA+Do2hlAagAtrF8rT4aFTaDnsPEYVZUxz35Afkk55KfQOT+HQOdADKqBzQmbpT+gEEKIG3ZDCeCCBQt46qmnGD16NN988w3ffPMNo0eP5umnn2bBggU1HaOoJafPxqC3KUcBvIKGoSjgqJMaQEuy1moY1sGPe55YgF/zNsQlZTDrrZ8xGo0oyfsYZOuPo7UjeWV5bD2/VaZiEkIIcUNuKAF85513eP/994mIiGDMmDGMGTOGiIgI3nvvPd5+++2ajlHUkj92foMCOKr2WDs1wVFnhVYjTfmW5uWkY2CHpkx94Q10dg58/vNu3t10BAC7M1sY4n0biqIQlxvHkcwjFo5WCCFEfXRDCWBFRQXdu3e/bH+3bt3Q6/U3HZSoGycTdwPgamNa29lZmn9vGV0CXOnZuQP3zo0A4PFlX7P92HkwVOB7Noo+PrcBpqXizhect2SoQggh6qEbSgAfeOAB3nnnncv2v/fee9x33303HZSofRXGCtKLEgHw9ekGIANAbiGKojC0vS/97hrJkPsfRwVGP/sh59KyoTiLjmlnaePaClVV+SXhF/LK8iwdshBCiHrkhv/if/jhh/zyyy/06tULgN27d5OYmMiUKVOYPXu2udxrr71281GKGpeQeozyknysyg34tR1OBjIA5FZjZ6NlVEd/Cqc8wYWzscTs/I1xL3zO1tcfwoU47rDvSo69N+nF6fwc/zPhrcKx0dpYOmwhhBD1wA3VAMbExNC1a1e8vLyIi4sjLi4OT09PunbtSkxMDAcPHuTgwYMcOnSohsMVNSV6/w8YjUY0eRocm7QCwEkmgb7l+LrYMijEh8n/XIpvUCsOxqUxbelaSstKsUo6wHDbJjhYO5Bdms2v537FqBotHbIQQoh64Ib+4m/durWm4xB1SFVVjiXuAcBdF0RBmQGQPoC3qtAmLqTk+fPQi6v47z/u5bsdscx79ydefXwMDme3MbTVIL7PPMC5/HNEJUVxe9PbZV5OIYQQVyVrATdCaUWp5OYmoTGotGzej4LSCkBqAG9ViqJwZ1tvQtu24qF/v4fOzoEV6/fx5te/oxoN+Mb9wV0enVFQOJZ1jIPpBy0dshBCiFucJICNUHzaQYrycrHNLad93+GUVZiaDaUP4K3LSqthVCc/2od24sH5/0WrteKZDyL54qftqIZymp/bRV+PUAB2pewiNjv2GlcUQgjRmEkC2AgdOLmV8vJySlINtAk1TedjZ6PFxkreDrcyexsrxnT2J7TX7Uyc8zIGFaav+Jl1v+xALS+iY9IROrua+nP+fv53zuadvcYVhRBCNFa3xF/8lStXEhQUhK2tLT179mTPnj3Xdd5XX32FoiiEhYXVboANSE5pDueSjqGo4ObQknLV9BaQ5t/6wdNRx6iOfvQcMo7xTy6m3ABTlv/ET1t3QWkevVPjaOPUzDw9zPl8mSNQCCHE5SyeAK5Zs4bZs2ezcOFCDhw4QKdOnRg6dCjp6elXPS8hIYG5c+fSv3//Ooq0YYjPOUNxdgoOBRUEdxxAfqlp4m4ZAFJ/BHo4MKyDL31H30vYo89TXAH3/edHfo7cCSXZDMxIorm9H0bVyM8JP3Oh8IKlQxZCCHGLsXgC+Nprr/Hwww8zbdo02rVrx6pVq7C3t+ejjz664jkGg4H77ruPxYsX07x58zqMtv6LTz1AXl4umswyuvUfSm5xOQAudpIA1ietfZy4s60Pt4+bwuiH/0leGUxauoHvfvodpTibu7JSaGbrhd6oZ+PZjSQVJFk6ZCGEELcQiyaA5eXl7N+/n8GDB5v3aTQaBg8eTHR09BXPe/HFF/H29mb69OnXvEdZWRn5+fmVtsaqqKKIuKTDlJWWkX2+gt59+pBbbBoB7GYvEwjXN6FNXejfypOB90wnfNYC8srg/97cwpffbUQpzmFodioBNm5UGCvYeHYj5/LPWTpkIYQQtwiLJoCZmZkYDAZ8fHwq7ffx8SE1NbXKc7Zv386HH37I+++/f133iIiIwMXFxbwFBATcdNz1VUJeArkpZ7Er0mPn0QYHBwdzDaCrvdQA1kfdg9zp38qTfmPuY/I//0OhXsvj7+/k3f99gzE/i+FZFwiydsagGvg5/mfO5srAECGEELdAE3B1FBQU8MADD/D+++/j6el5XefMmzePvLw883b+fOPtFB+XcZSinHQc88oI6nYnRqNKXompD6CLJID1Vvcgdwa08aL74LE8uOAtShU75n5xmCVvfUxBRhpDM5JpobHHqBrZnLCZoxlHLR2yEEIIC7Po0E9PT0+0Wi1paWmV9qelpeHr63tZ+bi4OBISEhg9erR5n9FomsPOysqK2NhYWrRoUekcnU6HTqerhejrlxJ9CcmZx8nNy0NNLGPE1EHkl1ZgVFWsNApOOhkFXJ91aeaGlUaDogzisWWf8+nix4n4OZG07A949pF7uMtoQOfswnGtSlRyFAXlBfT27y0rhgghRCNl0RpAGxsbunXrxpYtW8z7jEYjW7ZsoXfv3peVb9u2LUePHuXQoUPmbcyYMQwcOJBDhw416ubdazmbd5bi7GTUrAISUg307duXnIv9/1ztrSURaABCm7owupM/Ldp1YtYb39C0dUfei85hVsTHREVto39eDj1LSsCg51DGITYnbKbCUGHpsIUQQliAxat9Zs+ezdSpU+nevTs9evRgxYoVFBUVMW3aNACmTJlCkyZNiIiIwNbWlg4dOlQ639XVFeCy/aKys9mnyU9PxDm3HBvftri6uhKfmAOAqwwAaTBaeDlyT7emfH9Iw4z/fMZ3by1m4y/fkfTOrzwZf457wsfh5GLP77bWnM07S87pHIYFDcPN1s3SoQshhKhDFk8AJ06cSEZGBgsWLCA1NZXOnTuzadMm88CQxMRENJp61VXxllOiLyEp8zh5uTkoaWW07zkQgOwi0wAQdwdJABsSb2db7u0RwIYjKUyc+wrNO/Xiu5WLeeHbWM4kvs09o4YwNrQ1v+gKyVGNrD21lkHNBtHCtcW1Ly6EEKJBUFRVVS0dRF3Kz8/HxcWFvLw8nJ2dLR1OnTiWdYw/Dn3E0Y3rSNqYxPSl3xAWFsbXe8+TnFvC8FBf2vo2jp9FY2Iwquw4k8n+czmkJ8Xz1dK5pJyJYXgrK8J6tWDQyGHsd9FwwckLbF3o4NmBPv59sNJY/HOhEEKIG1CdHEd+0zcCZ3PiKMm+gDY5h7gclYEDB6KqKplFZYDUADZUWo3C7a29CHC3Z7ONlsdXrGHbuk/4+fO3iM08zcm4RPr36U7bnm056WhHjL6MlMIU7gq6C3dbd0uHL4QQohZJAtjAlehLSMo5Q25mCg7ZZXi07I6LiwuFZXrKKowoCrhLH8AGLdjTgam9g9h2OgPtPQ8R2ncI61e+yH93RRGTFk3vA4dofXsPctpnkeWWxtrSbPoF3EGIe4gMDhJCiAZKEsAGLj4vHrU4i4r0XC6kGxg4dggAWYWm2j83exustNLHsqGzs9EytL0v7fyc+d3Bhode/oDY/Tv4+ePl7I8+zojMbQTussfmjg7YtjhPZGEqSU37cEfAAHRamUZJCCEaGkkAG7gzuWdMCWBcCqezjDxy110AZBbKAJDGKMDdngd6BXIiNR8XuwG06tKbg5E/se7j5TjEpdDj7G6adjqJzW1xlGUkkJZ1mrvajsfX4fJ5OYUQQtRfkgA2YEUVRSTnJlCYdQFdagHJpXb06tULgIwCUw2gp6PU7jQ2Go1Ce38X2vo68+aH/2Pzp6+Tk3aBHCApX4/uVBaDTxSQ1v88nkEnyEg5xoAuU+ji30uahIUQooGQBLABO51zGrU4CyUjj+xsPZ1734m1tWnJt4yCUgB8nCUBrM8MBgNRUVGkpKTg5+dH//790Wq113Xu9+u/Y/aMqYwaNYoZT/4Pg0sAkbv2sXblv9l4+CjdstLoPKiMPckXSDp9hLO97mZEtxnY2zjU8lMJIYSobZIANmCnck5BcRZqQjonM42ETzE1/5brjWRdnAPQ29nWkiGKm7Bu3TrmzJlDQkKCeV9QUBDLly8nPDz8qucaDAbmzJnDqFGjWL9+vXmuzSGdA7l/zGCm3zeBU8cPY3PMh5be57igP8t3Kf/l8JFIHh4XQTPv9rX5aEIIIWqZ9P5voLJLs8ksSkMtzqbk5HlOZBoZPHgwAJmFZagqOOi0OMoawPXSunXrGD9+PKGhoURHR1NQUEB0dDShoaGMHz+edevWXfX8qKgoEhISeO655ypNtG5rraVroDsr//MiBTmZjJi5EOseEeTu10JuMbEn9jF/xTi+2fAK6sV1uIUQQtQ/kgA2UKdyTkFJDvY5xeTkVWDl4ktISAgA6Rf7//lI7V+99Pfau169euHo6EivXr1Yv349o0aNYu7cuRgMhiteIyUlBbjyEoqhoaEAtHDQs3Tuwzz25nbQToBUPSXFhXyz5b/868UBZKbG1/wDCiGEqHWSADZAqqpyOuc0FGdidT6bExkG7rpriLkDf2peCQDeTpIA1kdXqr0D0Gg0zJs3j/j4eKKioq54DT8/PwBiYmKqPH5pv5+fH56OOsZ0DWLl8tcYN+NXtPktUA0qCblnePrlO/jm0yXU5YJCBoOByMhIVq9eTWRk5FUTXSGEEFWTBLABSi1KpaAsD5uSfPJOJHAy08hdF6d/AUjONQ0A8XeVBLA+ulbt3aX9l8pVpX///gQFBbFkyRKMf2vKNRqNREREEBwcTP/+/c37Xe1tmDSwE2+88SsdQv6FXq+jTKvn2/1v8tRDt5EYH3ezj3ZN69ato2XLlgwcOJDJkyczcOBAWrZsec0mbyGEEJVJAtgAmZp/c/Epg4TENJILFYYOHQpAYZme/JIKFAV8XSQBrI+qU3t3JVqtluXLl7NhwwbCwsIq9SMMCwtjw4YNLFu2rMoRxd5Otix89B8s/uc2dE4dMGo1pDpdYMFzvXlv5au1Vht4s/0ehRBC/ElR67Lt5hZQnYWS66MKYwWfHvuU8rTjBB0+zTvvbSLbswc7duwA4FRaARuPpODlpOP+XoEWjlbcCIPBQMuWLQkNDa00ghdMtXdhYWHExMRw+vTpa04JU9VI4uDgYJYtW3bNkcQAeoOBt75fQfSe91BLC7ArqMA2rxXPr/iGZk2b3PAz/l1NPrMQQjRU1clxpAawgTmbe5ZyfRlOZQXkxCZwIsPAyJEjzceTc039/5q42lkqRHGTbqb27u/Cw8M5c+YMW7du5csvv2Tr1q2cPn36upI/ACutln+Ez+G5WV/h5N+WUmcbCr3PsmhqR9778KMaqw2siX6PQggh/iQJYANzIvsElOTQSrXl+Kl44nNVRo0aZT5+PrsYgKZukgDWZ+Hh4axdu5ajR4/Sp08fnJ2d6dOnDzExMaxdu/a6EzgwJZQDBgxg0qRJDBgw4IZq0Do17cJ/Zn1NaN8JqM6OFLe3Yd+6p5h8zxhSMnOqfb2/q4l+j0IIIf4kCWADkleWx4XCCyjFmdheyOPQhTL8mzQ1T+lRUFpBVmE5imJaE1bUbzdbe1fTPOw9mRsWQfi9L+DcJIjc1s64WG/noaGhrPkp8qZqA2ui36MQQog/ySzADciJ7BNgNNK0vJzk02eJSTcwatIo8/QviRdr/3ycbbG1ln5SDcGl2rtbha2VLeGdpuDv0ZyNkW+TcHQXPnZFrH9hBL9veZpl/16Ik51Nta/711HLVfUBrGrUshBCiCuTGsAGwqgaic2OhZJsQqyc2B9zisQ8tVL/v8QsUwLYTGr/RC3SKBr6BNzO1JGLuW3IvWiDfNH2dEK7/3XGDBvInuPVnzy6Jvs9CiGEkASwwUjMT6Soogjbklxs0gvZfiobBwcH8/JvBqNKfFYRAEGeDpYMVTQSrd3bcF+fOfQfNQO/Dh3I7eRGJ+fD/DO8O6+8v4YyffUmcK7Jfo9CCNHYSRNwA2Fq/tXTRm/k1MmTHE03MnLkSGxtTXP9JeeUUFZhxN5Gi58sASfqiJe9F+ND/w8ne29OeP1IrP1O2ttnseu1B7h3ZxRvLHuFZh6O13298PBwxo4dS1RUFCkpKfj5+dG/f3+p+RNCiGqSBLABKCgvICE/AYqzaGPlzDuHYkktVBk3bpy5TFxGIQDBng5oNIqFIhWNkb21PWNa342Hgw+OHn6c2vsbnnaJEPMe40bs46U3P2FY9zbX/b681fo9CiFEfSQJYANwPOs4qqriX16Bml3KlphUbGxsGDFiBABGo8rp9AIAWnpff22LEDVFq9Fye8AdeNp7YePgRpLb7yTZx3D7icM8O7E/+194l3/cNxonW2tLhyqEEI2CJID1nN6o53jWcdCXEapXOXHiBEfTjdx11zDzLODnc4opKjNgZ6Ml0EP6/wnLaefRDndbdzbpXHFy9SDWaS93OaSxafG9HN4/m6WLnqeFt5OlwxRCiAZPBoHUc3G5cZToS3AoLSDYyonfD5wiu0St1CH+RIqp9q+1jyNaaf4VFubr4Mv49g/QotM4Og0YRlnvlnTtaY8auZx77h7Hhn1nMBgb1QqVQghR56QGsJ47mnkUVJX2FXqys/P4fk8CVlZWjBkzBoDSCgOn00wJYIhfw1v7WNRPjjaOhLW+mz/sPLGydyXZbTueDsfxPLGNf9wzgCOL3+Ox8YNxta/+nIFCCCGuTRLAeiytKI304nQ05YW0M1qz69hJjqUbuWvoUDw9PQGIScrh5MFdqMU5nLQKxfv222XEpLglWGmsGNRsEJ72nuy0dcLJ2Y0El4OMPZrOZ8+EE3N4PvPnziLEz8XSoQohRIMjCWA9djTzKACt9GCn0bJ223EqjHDvvfcCsHbttzz65D/ITEkC4B0gKCiI5cuXy5xp4pagKAqdvDrhYevBLzYu2Dm5Eee+j1H28exeu5ApR/YyP+I1hncJwlorPVaEEKKmyG/UeqqooogzuWfAqKdDWSlpaWn8uO8cOp2OsLAw1q1bx4QJ9+AT2Iq5K78mKyeP6OhoQkNDGT9+POvWrbP0Iwhh1tSpKePb30eTNsNo2+dOrO7qTOdeTgSnbOSJicN4fW0kWYVllg5TCCEaDEkA66nDGYcxqkZ8DUZ8FBt2Hz1DUr5p6TcHBwfmzJlDl353Mm3R24QNGYi7qzO9evVi/fr1jBo1irlz52IwVG8lBiFqk7ONM2Gtw2nbZjTNug7Ed0RvvPp6MdIrgTceH8M/X32PmOQ8VFUGiAghxM2SBLAeKjOUmaZ+AbpUGFFVlU9/OQzApEmTiIqKIiEhgX7jH0ZnraVzgKv5XI1Gw7x584iPjycqKsoS4QtxRdYaawYFDOL2kIl4tB1Ay7tux3pgc+7tbGDn2/9g1pP/YOOhJMr1RkuHKoQQ9ZokgPXQscxjlBvKcUNLUGkpCYmJ/HI4GWdnZ0aOHEly8gUA/IJa0SnAFQdd5a6eHTp0ACAlJaXOYxfiWhRFoYNnB8I6TMGt1UBa9b8d62GdGNHbHquDn/Pk/WN4e+MeMgqkSVgIIW6UJID1jN6o50jGEQC66BUURWHjnjiKKkyDP+zs7CixNk33kp0UR/dA98uuERMTA4Cfn1/dBS5ENfk6+HJPu/vxbzGEZl374TKmFx36utPX6hgR00ew+J3VHE2SJmEhhLgRMgq4nonNiaVYX4yjxoZWhZmUlZfxxrc7AXjwwQcpLtdT6tEKd58m7PnuQ3QzK4/2NRqNREREEBwcTP/+/S3xCEJcNwdrB8a2GscOB29ibJywd3RC8TzMxL3JrHt5OmePH+Kxp5/hrva+6KxkeiMhhLheUgNYjxhVI4fSDwHQCR1aVWVf7AXOpBfTpk0bevXqxY4zWVQYFe7/x3y2bdlMWFgY0dHRFBQUEB0dTVhYGBs2bGDZsmUyH6CoF7QaLbc3vZ1B7Sbi1KIfzQf2w254eyb11pG6+U2eeXgy7/1ymPT8UkuHKoQQ9YbUANYjp3NOk1eWh05jQ7uCLAA+2HQIMNX+JWQVE5OcB8A/Z07hjtZezJkzhz59+pivERwczNq1a2UeQFHvtHVvi7utO5ttXdHqnEhzd2aA82FOH97Ji/83ioT5b/LAmDvp1NQFRZElD4UQ4moUtZF1oMnPz8fFxYW8vDycnevP0mgGo4GvYr8iryyPXrY+dE05RXpeMX7h/wZFw8nTZ9mapKeozECXZq4MaONtOs9gICoqipSUFPz8/Ojfv7/U/Il6rVRfyu+JW0hI3E5h0jFSog9TsDeVdSdUBk17jqnTH+audr7YWsv7XAjRuFQnx7klmoBXrlxJUFAQtra29OzZkz179lyx7Pvvv0///v1xc3PDzc2NwYMHX7V8QxGbE0teWR52VnaEFphq+b7efhqjCiNHjuREgTVFZQY8HG3o19LTfJ5Wq2XAgAFMmjSJAQMGSPIn6j1bK1uGB4+gT7uJOLXsRdCdfXEd0YYHelpx4PMXWTT7UT7ceoLUPGkSFkKIK7F4ArhmzRpmz57NwoULOXDgAJ06dWLo0KGkp6dXWT4yMpJJkyaxdetWoqOjCQgIYMiQISQnJ9dx5HVHb9SzL3UfAF0dmmJdmEa5wcArn20CYHD4/cSlF6LVKAxr74uVLJklGjhFUejs3Zlxof+Ha/PbCe7RE8ex3RgzyAn7Mz/x0iNh/Pfbrew/lyOjhIUQogoWbwLu2bMnt912G2+99RZgGqUaEBDAE088wbPPPnvN8w0GA25ubrz11ltMmTLlmuXrYxPwkYwjbE/ejoO1A/fpbbHKPstvxzO5a9ZrBAQG8Y/3fkZRNAxq602nv0z6LERjUKIv4bf4Xzh/LpLc5FhSdh4ma08mP8dZMXrWv7l7wgSGtpcmYSFEw1edHMeig0DKy8vZv38/8+bNM+/TaDQMHjyY6Ojo67pGcXExFRUVuLtfPt8dQFlZGWVlf04Ym5+ff3NB17EKYwUH0g4A0N25OVZntoOisPTLSNO+ofegKBpC/Jzp2NTFgpEKYRl2VnaMajmGA85N2WP7A/b2jmi8Ypi8+xwb3p7DqUPRJD29kHG3Ncff1c7S4QohxC3Bom2FmZmZGAwGfHx8Ku338fEhNTX1uq7xr3/9C39/fwYPHlzl8YiICFxcXMxbQEDATcddlw6lH6JYX4yTjRNt8zMBOJWr4bedB7CytqHL4HC8nHTcGeItIx9Fo6UoCt18uhHWeSYeLQfSqmdPHEZ0ZNRQZ6yPr+Olh8bwxte/sTchW5qEhRCCW6AP4M145ZVX+Oqrr/juu++wtbWtssy8efPIy8szb+fPn6/jKG9cYXkhB9MPAtDLLQRtRiwAy77eAUCXgaPw8PJkdEd/rKXfnxD4OfoxoeP/0br9PQR16In/sN60H+XHIO9kVj09nqXL32D9wWSKy/WWDlUIISzKok3Anp6eaLVa0tLSKu1PS0vD19f3qucuW7aMV155hd9++42OHTtesZxOp0On09VIvHVtV8ou9EY9fg5+tMxOBlUl3ejEh2s2AHDH3Q8yvIMfLvbWFo5UiFuHTqvjruDhBLq2YJv91zg4OnHG6ziTvM+x+X8vcepgNOeefYVxvdsQ6OFg6XCFEMIiLFptZGNjQ7du3diyZYt5n9FoZMuWLfTu3fuK5/3nP//hpZdeYtOmTXTv3r0uQq1zqUWpnMo5hYJCX7cQlIwTACxbfxCj0Uibbv0Yd2dvgj3lD5gQf6coCm3c2zCh+5M0bTuckG49cR/emSFj3PFM28pL00ey7NPv2RqbToXBaOlwhRCizll8JZDZs2czdepUunfvTo8ePVixYgVFRUVMmzYNgClTptCkSRMiIiIAWLp0KQsWLODLL78kKCjI3FfQ0dERR0dHiz1HTVJVle3J2wFo494G79TjoKrk6vx488OXAbhn2kx6BXtYMkwhbnkuOhfGtZ/CPrdWaGy/xcXFFaP3MZruS2P1ggc4ue8RJs18mlGdm+HtXHU3EiGEaIgsngBOnDiRjIwMFixYQGpqKp07d2bTpk3mgSGJiYloNH9WVL7zzjuUl5czfvz4StdZuHAhixYtqsvQa83J7JOkF6djrbGml1MwxH2FCjz3xV5KS4po2qItzz40AY1GBn0IcS1ajZaeTfsR4Nqc34+vxs7BidMeJxjrf46D295jwd4/OPWvZYTf2YvugW7y/0oI0ShYfB7AunarzwNYXFHM6pOrKTOU0duvF10unIDcRGIrvOly92xKCvNZ9fH/mPHgfZYOVYh6p8JQQfS5LcSc+p6M86eJjzlJ/t5sthw2MHDKHCY++AgjQptIv1ohRL1Ub+YBFJeLSo6izFCGp50nnTQOkJtIqVFh4dcHKCnMJ7hVGx6eMsnSYQpRL1lrrbm9+TCae7Zn69FPcXZxJdbzJGEByUR/u5SFu7Zy5l9LGduvI6FNXGRqJSFEgyUJ4C0kPi+euNw4FEXhdt++bPviNS5cSOKCLpgf1nwBwEsLF1RqEhdCVF9T5wAm9von0XE/Y2P/AykeZ1C9z5C1/zCvzRjJsYfmcfekBxjSzldqA4UQDZIkgLeIMkMZ25K2AZC7N5feo28j4XxKpTK+fn7ce+9ES4QnRINjo7XhjtZjae4dSuSRj3F1deOk20lGNM9g7+qFHIz8ieNzXmJ0v850CXCV2kAhRIMiVUm3iO3J2ymqKOLUtlM8/8jzhAa4EbniEd56dxVWNqZ5DNNSU/n+++8tHKkQDUuAa3Pu7buAPrf9H1179CGgZwi33eNNc+fDvDZjFP9e+hpr9iSSXVRu6VCFEKLGyCCQW8DpnNP8eu5XVIPK0ruX0jnYl+9eGMOhPEee+mQPO35czW233Yavry8xMTGcPn0arVYWtheipmUWphB56EMSEvcReyqW/HOZHN6aR7ljKJPnLmHMHbfRLdANrYwUFkLcgmQQSD2SX57PH0l/AKBJ0HD+3Hm+fmYkOSUGNmf5Ev3TNwC8+uqr2NjY0KdPH6KiohgwYIAFoxaiYfJ09CO873PENPkdZ5evOOd1Go37WQpOx/HO7LEcGPsodz84k6EdA2jqZm/pcIUQ4oZJAmhBRtXIb+d+o9xQjq+DLyXFBQC0DfQiujyYz95bidGgZ+TIkdxxxx0UFJiOp6SkXO2yQoiboFE0dAweTLBvV3Yc+QQPjx2cconlzoBMzu1/n+ce+I59jy8ibNQw+rfyxN5Gfo0KIeof+c1lQbtTdpNalIqN1oY7m93Jweh3Afj1RDa/FuUSu287NjY2vP766wDExMQA4OfnZ7GYhWgsnOzcGdZzNh2C7yLK42NOnj2MjUMcvulFbHx7Brt+HsyEx59jdJ8OtPd3lkEiQoh6RRJACzmdc5qD6QcBuKPpHbgUZdPfv4JAH1f+/fU+zid+B8DcuXNp1aoVRqORiIgIgoOD6d+/vyVDF6JRaeodysQ7X+VYwPfs8P6O2LiT6NySyYyPZvmTw9g1Zhb3TH2IO9v74+diZ+lwhRDiusggEAvILMlk3el16I16Ont1po9XJ9j3EZQVsmRLGs+//AYAXl5eHDlyhPj4eCIiItiwYQNr164lPDzcInEL0diVFGex9/DH7Ir7g1OnTpOTm8f5YyUkJXkyauqzhI8ZSZ+WnjjbytyBQoi6J4NAbmHFFcX8dPYn9EY9AU4B9PLrCTHroKyQbJzJ8gkG5b+gqmRkZJibe4ODgyX5E8LC7Ow9uL33XNoFD2GX7//YfXoPOl08vm2KiPr+abZ9/zHhjzzH6AG96Bboho2VzLQlhLg1SQJYhyoMFWxK2ERhRSEuOhfuCrwLTfJ+yDqDqtHyS3kXvnxtGqgqEydOZObMmaSkpODn50f//v1l6hchbhGevh0Z5bOUrs1/54+jq9l75gh2tsnk5sXx2RsT+f27kUx4eA7DbmtLW18nNDJtjBDiFiNNwHXEYDTwc8LPJOYnotPqGNdqHO4lBXD4K1CNxLv34aGIL/h9zft4enlx4vhxPD096yw+IcSNUStKiY9dz5Zj6zl05jjpGZlk5hg4faSC0C4PEH7fIwwMDaSFl4MMFBFC1Krq5DiSANYBVVX5LfE3TuecxkpjxZgWY/DV2sH+T6C8GINXO57ZnMvrcx8EYN26dYwbN65OYhNC1AxjcRYnYr5iy4nNHD4TS15BAalZBhJOKnTtNpXxk6czsENTAtxl/kAhRO2QBPAq6joBVFWVqOQoYjJjUBSFkcEjaWbvAwc/h8IMcPLhN7Un4cMGUpCdwSMzZvDuqlW1HpcQonYY8pI4eexrNh7ZxLFzcRQWl3AhS0/SaSu69ZjGvZP/jzvayYhhIUTNkwTwKuoyATSqRiLPR3Iy+yQKCncG3klrlxZw9BvIjgcbBwpCJtJ9cBinDu6iZZsQjhzcj52d/GEQor4zZMdx8thaNhz9lZjEsxSXlJKSa+DCOR3duk1lwj0P0reNH01c7aRpWAhRIyQBvIq6SgANRgO/n/+d0zmnUVAY1GwQbVxbwckNkHYMtFbQ+X7ueWIRaz97D1t7B/bu3k2HDu1rLSYhRB1TVYwZsZw8/h0bYn7jyPmzlJaWkVJg5EKSNW1bhTFx/KPcERpIkIe9JIJCiJsiCeBV1EUCqDfq+fXcr8TnxaMoCkMCh9DCpTnE/gQpR0DRQIe7eWvtVp549BEAVn26mhlT7q2VeIQQFqaqqJlniD/5Pd8f+In9F+IpLSsjvVAlJUXB328Qd4fNYtht7Wnp5SijhoUQN0QSwKuoiwSwqKKI705/R1FFEUODhhLkHAixP0PKYVPy124MP+2LZ/SYMRgNBu55+GnWvLtcPv0L0dCpKuQkkBb7Ez/u38CO5FMUlZSQU6pyIVvF3qEbwwY9Qtjt/Wnv74KttUz9JIS4fpIAXkVdNQHnleVRUF5AUwd/OLXpYvKnQMhodicUMnDQIEqKi+k+OIwf136Jr3QIF6JxyUumICGKzbu/4fdzMWQWFVBcoZKUr1Khb0rn0LE8MOpBbmvujYejztLRCiHqAUkAr6JORwHry+H495B1xpT8tR3JvqQyBg8eTF5eHm269+M/737BmK7NajcOIcStqyQXQ9JedkavZtOpfZwuykJvVEkrVMkuscXXrx/jBv8fw7t2o7mXE1ppHhZCXIEkgFdRZwlgWaFptG9BKmisoN0Y9p0r4K677iI3N5fgDt14NOIDHhnUHhd7WTdUiEZPXwYpR0g4spGfDm9hd0YCJaqB/HKV1CIFxa45oW2Gcu/gyfRqHoC7g42lIxZC3GIkAbyKOkkAS/Ph0BdQkgvWdhB6D7/tPcG4ceMoLCykdcfuPPjiu/Rq25SBbbxrJwYhRP2kqpB7jtJze4jc/jW/nz3COUMBehTSi1Ry9Tqc3Dtxx21jubvPcEKbeKOzkr6CQghJAK+qThJAoxFivoXiLOg4gS+++5lp06ZRUVFBr363M2bu6zg7OzOtbxD2NrIcsxANlcFgICoq6sbX9C4vgtQYzu75ni1HI9mTeY48a5UyA6QXGSmx8cDLtxt39RzL8E59CfHxxkqrqbXnEULc2iQBvIo6awLWl1NRVsw/X1jMihUrABh/zz0MmrGYUqOWvi096RHsXnv3F0JY1Lp165gzZw4JCQnmfUFBQSxfvpzw8PDqXUxVIe88FcmHObD9W/44vZ/jBekU22kprlBJL4YKO288fTpyZ49RDAntTUe/ppIMCtHISAJ4FXWVAKampjJx4kS2bdsGwLx58xg29Sn2J+bhbGfNlN6BWMsvZyEapHXr1jF+/HhGjRrFc889R4cOHYiJiWHJkiVs2LCBtWvXVj8JvMSgh+yz5J2OZm/Ut+w4d4KzFXmUOFhRoofMYpUKOy9c/drTq8MA7gzty21NW+Kos63ZhxRC3HIkAbyKukgAz58/T69evbhw4QJOTk58+umnDBgyks93ncNgVBndyZ+W3o61cm8hhGUZDAZatmxJaGgo69evR6P584Oe0WgkLCyMmJgYTp8+Xb3m4KroyyAjlqzYaA7u2kj0uRMklOZQ5GRNKQpZxSolWidsPINpG9ydQV0H0iOwNS3cmqDVSL9BIRoaSQCvoi4SQKPRSHh4OKdOnWLdunW0adOG7w4mcy6rmGBPB8Z29pdJn4VooCIjIxk4cCDR0dH06tXrsuPR0dH06dOHrVu3MmDAgJq7sb4css+SGbuTmO0/sj/hGGeKs8h3sqbcRkN+mUq+3gqc/XHxbUGXVj3oE3IbXfya4+/kg0aRFgkh6rvq5DgyAqEWaDQaPvvsMxRFwcnJiaNJeZzLKsZKo3BHay9J/oRowFJSUgDo0KFDlccv7b9UrsZY2YB3Wzy92zKg71QG5CZSmHiY2F2biDm5j5i8RHJsKigqO0fp+fPsPrOVP362Q3Hxx8OvBV3b9qBb8w6EeAfQxMkPOyuZnF6IhkwSwFpyKfPOK6lg2+kMAPq09MRN5u4SokHz8/MDICYmpsoawJiYmErlaoVGC+7BOLoH061zGN1K8zBkxhG391fiD27lRNIZ4guzKHSooLikhPKEs2w/+Qu/auxRHL1x9G5Ky2bt6NGmG+18mtHU2RcPWw+stbfGnKU3PbpaCCFNwLXJaFT59kASSTklNHG1Y3y3prLIuxANXJ32AbwRRiMUplKSeopTe7dw7uhOTiWfJak4m2IHK4odrCjTmUYXF+m1GO3c0Ln54erdjFbN2hAa2JYWHv74O/rgYedR5zWFNTq6WogGRpqALezSp9PfD8SSZbSnbece3NXOR5I/IRoBrVbL8uXLGT9+PGFhYcybN888CjgiIsI8CthiNVYaDTj7Y+fsT6fWA+hkNEJRBoXJJ4g78AfnT+zjQvIZ4vPTybM2UlpRRmlFBuXZMRw8upFo1RrV1gVrZy+cPf0JaNKC9kEhBLr54OfoibudO246N+ys7Gq8u8tfR1evXr260ujq8ePH39zoaiEaGakBrGFVfTptGhDIGytek19MQjQiVf0uCA4OZtmyZbf+74KyQoz5F0g8tpszB7eTFneEtMxkUkryKLZVKLOzosTeigobDWUGKNYrGKwcUOxc0Dm74+Thg593IG0DWxPg5ou3gyuuOlecbZxx0blga1X9KWlu+ZpVIW4BMgr4KmozAbz06XTYiBGEDJuKV7OWuJWlsfHzt29+7i8hRL3TYPqqqSqU5FCek8y543uJP7af9IQT5GWcI6Ugi0KtkTJbLeW2WtNXnRaDCiV60GvtUG0c0Nq7YOvohqOrJ16eTWjhH0ygpx8edi442TjhYOOAk7UTjjaO2GptL6s9tNjo6kakwbxfGzFJAK+ithLAv346/e6774i5UEBCVhGjO/oDqnw6FUI0PPpyjIVppJw5QuLJQ6TEnyDnQjyFuSlkFudQqDFQbqOhXKelQqel3EaD3lqDESjTg16xRrW2R6OzR2vrhI2DM/ZOrri4eOLnGUCgTxO8ndxx1jnyxw9/8OzMZzmVcgpPV0/sre2x0diYE8WCggKcnZ358ssvmTRpkmV/LvWQ9K1sGKQPoAVERUWRkJDA6tWr0Wq1dApwpWNTl4u/nBTmzZtHnz59iIqKkk+nQoiGwcoGjWsATboH0KT7yD/3Gw2oJTlkJMaSePIwKedOkXPhLMVZKZRmZpBdlk+hWkGFTkuFdQEVNhoqrDXobTSUWWlIB1JV2G0Ao9YG1cqW7BQ9AA8tmoRvK390Dk44OLjg7uSNt4cvBedzAci2yuZg+kF0Wh22Wlt0Vqavtla26LQ6rDTyZ+/vpG9l43RL1ACuXLmSV199ldTUVDp16sSbb75Jjx49rlj+m2++Yf78+SQkJNCqVSuWLl3KiBEjrutetVUDuHr1aiZPnkxBQQGOjpev8iGfToUQAtMo5PIC8jOSSDx9jKSzseSkJlKUdYHSvAzKi3MorsinoKKUMq2K/mJyWKFR+Oa7XFw8rOg7yg3V6i99AFWVHT/mkJ+lZ+CDzcBah6K1RrHWobW2QWtti7XOFmudHXY6BxzsXHB2cMPZyR0XJzdcndxwtnfAwdoWG63Nn5vGBmutNTaaP/dZa6yx0lg1mImzpW9lw1KvagDXrFnD7NmzWbVqFT179mTFihUMHTqU2NhYvL29Lyu/c+dOJk2aREREBKNGjeLLL78kLCyMAwcOXHHi1bpwS8z9JYQQtzqNBmxdcA5woUNAezoMqqKMoQJjaQFZKee4cC6OlPNnyc1Iwa5oH++s207M92Xc3sYFd4cKUnIL+eNUEampBob2sadpZi4GrYLBSnPxq4JBqzF9BUqBnL/fTgWDqqAqWtMcihotaKxQNFYoWi2K1hqN1hqNlWnTWlmj1dpgpbXB2kqHtZUtOmsdOmtbbG3ssdXZYaezx07ngL2dA/Z2jjjaOeFg74DOWoe1RotWo0WrmDaNRoOVYkoqtYrpmEa5uE+jMZfTKqb9GkVTYyOs/9p69dfkz/RPpZHWqwbM4jWAPXv25LbbbuOtt94CTJ84AgICeOKJJ3j22WcvKz9x4kSKiorYsGGDeV+vXr3o3Lkzq1atuub96qIPoHyKEkKI2lHl6OqgQBY8O5seHduQkXqB/Jx0inKzKcrPoaQwl7KiAipK8igvL6TCUILeUIKRcgxqBRUYMWoUjFoFw8WvRq2CUaNguPj1r/vUm8y7VEVBRUFFg3qxi5CKBhQFVTF9VRQNXNwURUHRXPpeY/oezcVE0JQ8KmhQUFAULcrFJFExJ4oXy2q0aBQtWkVBuViDaaXRcnJ/Ahs+jeSZ16dja2eLVqtBgxUarena+jIDC2Ys575Z4XTv19kUg6KgVbQoGtP1tRpTLCjKxftoUDQKGsUKjUYxJbYaLZq/HNdoTN+brqFcjE3zZ5mLz2x6PsV0PY3ptUa59Kwa899aRVHMSfGl75W/JMqme5jiBVA0CgoKKApac22uYr6eRtFw6Z9aczFGU3Hl4s9aMX9/6foXL2GmcPmbxVnnjLWm9iZUrzc1gOXl5ezfv5958+aZ92k0GgYPHkx0dHSV50RHRzN79uxK+4YOHcr69eurLF9WVkZZWZn5dX5+/s0HXoVbfu4vIYRoAMLDwxk7dmzNjFY1GtCXFVGUl0NBbhZ5eTkU5udSkJ9PYX4exYX5lBYXUFZSSHlJERUlhZSXFVFeUYJRX45RLcNorMBorMCg6lFVPQZFj6oaUDGiKkYMGDEqYNSAqlFMCaCCOZk0bQqqBvMxVVEwariYFJrK/p0KGC5uN6Og0PT3cX/0ejz8Ll+pKiulHICklEjKr/B3WVzu7zVrl/4Fpwxfzqiht0Y3MIsmgJmZmRgMBnx8fCrt9/Hx4eTJk1Wek5qaWmX51NTUKstHRESwePHimgn4GsLDw1m7di1z5syhT58+5v3BwcHSiVYIIWqIVqutmeZIjRYrO2dc7Jxx8Q2k6c1f8XJGI/qKMooLCygqLqS4qJCykhLKy0spLy2l/P/bu/+YqOs/DuDP8+BOEOUIlAMNsQKNTDRIdpGzkmXNNX82/rBJ9UfTcGrqyn6IDqc4m01tDrM2cJVRtrC0tBjCtfoqCupELcWkoOSHrkBE5c77vL5/IJ8v9wVT4I7PnZ/nY7u6+7zfd/fiKbvPi8+P+ziuw+m4DmdbGxxtbbhxwwln23XccDrhvOGA0+GAcuPmfWcbXC4HXC4nnC4HXC4XbrgcgOKCIq72xlNcUEQBRLm5TFGXiQgUKDcbVIGIIGKwC0dDWvDbwTY88FQEYBC0ty8KFBGU/acZg0MCcF/wIAz4p70hNUDaOxqBOl8AtG8Ek05bweRmI3Tzv52eI+qMjtdAp2UC9Q06bVhTmyq3ftjg9tyu4/jfe9+C3GZ3erfPNXTU2jO+dOyo5scAetubb77ptsXw8uXLuPfee732fh7965SIiPzbgAEIMAdhiDkIQ8K7HtfuC9Keaz8LuO7v+C57r2r/qtZ8A4aIqDdFUaC4XDfvuyACoGO8fXb7faW94e1oJ0Xk5jxAROmYiY6j4ETpdL/jyDhBp9foPN5pTqf5/1smbuPodD9qeIxHs+kLTRvAiIgIGI1GNDQ0uC1vaGiA1Wrt9jlWq7VH881mM8xms2cKvkMe++uUiIjIy3x971Xn4/uMRiMQ6L1j6PRE022RJpMJSUlJKC4uVpcpioLi4mLYbLZun2Oz2dzmA0BRUdEt5xMREdG/mzVrFs6dO4eSkhLs3LkTJSUlqKqq0rz5I+/RfBfw0qVLkZGRgeTkZEycOBGbNm1Ca2srXnrpJQDAvHnzMHz4cOTk5AAAFi9ejMmTJ2Pjxo2YNm0aCgoKUF5eju3bt2v5YxAREfk17r3SF80bwPT0dFy8eBFZWVmor6/H+PHjsX//fvVEj5qaGrevVHnsscewc+dOvPPOO3jrrbcQFxeH3bt3a/odgERERET+RPPvAexv3voeQCIiIiIt9aTH8Z3zkYmIiIioX7AB9KDVq1djzZo13Y6tWbMGq1ev7t+C/gVrJeZKROQ9vv4ZywbQg4xGI7Kysrr8g69ZswZZWVk+9V2ArJWYKxGR9/j8Z6zoTHNzswCQ5uZmr7x+dna2AJDs7OxuH/sS1krMlYjIe/r7M7YnPQ4bQC/o+Ac2mUw+vzJlrcRciYi8pz8/Y3vS4+juLODm5mZYLBbU1tZ69SzgoUOHwuFwwGQy4eLFi157H09grcRciYi8p78+Yzsud9vU1ITQ0NB/nau7BvDPP//06rWAiYiIiLRUW1uLESNG/Osc3TWAiqLgwoULGDx4sHptQU/asGED1q5di2XLlmHjxo3q/99++228/vrrHn+/vuiotaO2/3/sS/wpV3/CXL2v4y9yb+910Bvm6h3M1bP6+zNWRNDS0oLo6Gi3i2jcajJ5SOeDOzvvh/fFA+tvVZOv1+rrufoT5to/+uO4Yz1irt7BXD3H1z9jNb8U3N3E5XIhOzsbK1euxOXLl9XlK1euVMd9RedaO/P1Wn09V3/CXImIvMfXP2N1twu4v/CSc97BXL2DuXoPs/UO5uodzNU7fDFXfhG0l5jNZqxatQpms1nrUu4qzNU7mKv3MFvvYK7ewVy9wxdz5RZAIiIiIp3hFkAiIiIinWEDSERERKQzbACJiIiIdIYNoJds3boVsbGxGDhwIFJSUnD48GGtS/IrP/74I5577jlER0fDYDBg9+7dbuMigqysLERFRSEoKAhpaWmoqqrSplg/kpOTg0cffRSDBw/GsGHDMGPGDJw5c8ZtzvXr15GZmYnw8HCEhIRg9uzZaGho0Khi/5Cbm4tx48ZhyJAhGDJkCGw2G/bt26eOM1PPWL9+PQwGA5YsWaIuY7Y9t3r1ahgMBrfbmDFj1HFm2nt//fUXXnjhBYSHhyMoKAgPP/wwysvL1XFfWnexAfSCzz//HEuXLsWqVatw9OhRJCYmYurUqWhsbNS6NL/R2tqKxMREbN26tdvxDRs2YMuWLdi2bRvKysowaNAgTJ06FdevX+/nSv2L3W5HZmYmDh06hKKiIjidTjz99NNobW1V57z22mvYs2cPdu3aBbvdjgsXLmDWrFkaVu37RowYgfXr16OiogLl5eV46qmnMH36dJw6dQoAM/WEI0eO4IMPPsC4cePcljPb3nnooYdQV1en3n766Sd1jJn2zj///IPU1FQEBgZi3759OH36NDZu3IiwsDB1jk+tu7T8Fuq71cSJEyUzM1N97HK5JDo6WnJycjSsyn8BkMLCQvWxoihitVrl3XffVZc1NTWJ2WyWzz77TIMK/VdjY6MAELvdLiLtOQYGBsquXbvUOb/88osAkIMHD2pVpl8KCwuTjz76iJl6QEtLi8TFxUlRUZFMnjxZFi9eLCL8fe2tVatWSWJiYrdjzLT33njjDXn88cdvOe5r6y5uAfQwh8OBiooKpKWlqcsGDBiAtLQ0HDx4UMPK7h7V1dWor693yzg0NBQpKSnMuIeam5sBAPfccw8AoKKiAk6n0y3bMWPGICYmhtneIZfLhYKCArS2tsJmszFTD8jMzMS0adPcMgT4+9oXVVVViI6Oxn333Ye5c+eipqYGADPti2+++QbJycl4/vnnMWzYMEyYMAEffvihOu5r6y42gB526dIluFwuREZGui2PjIxEfX29RlXdXTpyZMZ9oygKlixZgtTUVIwdOxZAe7YmkwkWi8VtLrO9vcrKSoSEhMBsNmP+/PkoLCxEQkICM+2jgoICHD16FDk5OV3GmG3vpKSkID8/H/v370dubi6qq6sxadIktLS0MNM+OH/+PHJzcxEXF4fvv/8eCxYswKJFi7Bjxw4Avrfu4rWAiXQqMzMTJ0+edDv2h3pv9OjROH78OJqbm/Hll18iIyMDdrtd67L8Wm1tLRYvXoyioiIMHDhQ63LuGs8++6x6f9y4cUhJScHIkSPxxRdfICgoSMPK/JuiKEhOTsa6desAABMmTMDJkyexbds2ZGRkaFxdV9wC6GEREREwGo1dzphqaGiA1WrVqKq7S0eOzLj3Fi5ciL1796KkpAQjRoxQl1utVjgcDjQ1NbnNZ7a3ZzKZ8MADDyApKQk5OTlITEzE5s2bmWkfVFRUoLGxEY888ggCAgIQEBAAu92OLVu2ICAgAJGRkczWAywWC+Lj43Hu3Dn+vvZBVFQUEhIS3JY9+OCD6u51X1t3sQH0MJPJhKSkJBQXF6vLFEVBcXExbDabhpXdPUaNGgWr1eqW8eXLl1FWVsaMb0NEsHDhQhQWFuLAgQMYNWqU23hSUhICAwPdsj1z5gxqamqYbQ8pioK2tjZm2gdTpkxBZWUljh8/rt6Sk5Mxd+5c9T6z7bsrV67gt99+Q1RUFH9f+yA1NbXL12qdPXsWI0eOBOCD665+P+1EBwoKCsRsNkt+fr6cPn1aXnnlFbFYLFJfX691aX6jpaVFjh07JseOHRMA8t5778mxY8fkjz/+EBGR9evXi8Vika+//lpOnDgh06dPl1GjRsm1a9c0rty3LViwQEJDQ6W0tFTq6urU29WrV9U58+fPl5iYGDlw4ICUl5eLzWYTm82mYdW+b8WKFWK326W6ulpOnDghK1asEIPBID/88IOIMFNP6nwWsAiz7Y1ly5ZJaWmpVFdXy88//yxpaWkSEREhjY2NIsJMe+vw4cMSEBAga9eulaqqKvn0008lODhYPvnkE3WOL6272AB6yfvvvy8xMTFiMplk4sSJcujQIa1L8islJSUCoMstIyNDRNpPp1+5cqVERkaK2WyWKVOmyJkzZ7Qt2g90lykAycvLU+dcu3ZNXn31VQkLC5Pg4GCZOXOm1NXVaVe0H3j55Zdl5MiRYjKZZOjQoTJlyhS1+RNhpp70/w0gs+259PR0iYqKEpPJJMOHD5f09HQ5d+6cOs5Me2/Pnj0yduxYMZvNMmbMGNm+fbvbuC+tuwwiIv2/3ZGIiIiItMJjAImIiIh0hg0gERERkc6wASQiIiLSGTaARERERDrDBpCIiIhIZ9gAEhEREekMG0AiIiIinWEDSERERKQzbACJiHqhtLQUBoMBTU1NWpdCRNRjvBIIEdEdeOKJJzB+/Hhs2rQJAOBwOPD3338jMjISBoNB2+KIiHooQOsCiIj8kclkgtVq1boMIqJe4S5gIqLbePHFF2G327F582YYDAYYDAbk5+e77QLOz8+HxWLB3r17MXr0aAQHB2POnDm4evUqduzYgdjYWISFhWHRokVwuVzqa7e1tWH58uUYPnw4Bg0ahJSUFJSWlmrzgxKRbnALIBHRbWzevBlnz57F2LFjkZ2dDQA4depUl3lXr17Fli1bUFBQgJaWFsyaNQszZ86ExWLBd999h/Pnz2P27NlITU1Feno6AGDhwoU4ffo0CgoKEB0djcLCQjzzzDOorKxEXFxcv/6cRKQfbACJiG4jNDQUJpMJwcHB6m7fX3/9tcs8p9OJ3Nxc3H///QCAOXPm4OOPP0ZDQwNCQkKQkJCAJ598EiUlJUhPT0dNTQ3y8vJQU1OD6OhoAMDy5cuxf/9+5OXlYd26df33QxKRrrABJCLykODgYLX5A4DIyEjExsYiJCTEbVljYyMAoLKyEi6XC/Hx8W6v09bWhvDw8P4pmoh0iQ0gEZGHBAYGuj02GAzdLlMUBQBw5coVGI1GVFRUwGg0us3r3DQSEXkaG0AiojtgMpncTt7whAkTJsDlcqGxsRGTJk3y6GsTEf0bngVMRHQHYmNjUVZWht9//x2XLl1St+L1RXx8PObOnYt58+bhq6++QnV1NQ4fPoycnBx8++23HqiaiKh7bACJiO7A8uXLYTQakZCQgKFDh6KmpsYjr5uXl4d58+Zh2bJlGD16NGbMmIEjR44gJibGI69PRNQdXgmEiIiISGe4BZCIiIhIZ9gAEhEREekMG0AiIiIinWEDSERERKQzbACJiIiIdIYNIBEREZHOsAEkIiIi0hk2gEREREQ6wwaQiIiISGfYABIRERHpDBtAIiIiIp1hA0hERESkM/8Fz2oX/RLsssYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, pEpoR = simulate_pEpoR(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, pEpoR, **kwargs)\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.set_xlim(-3.0, 63.0)\n", + "ax.set_ylim(-0.05299052022388704, 1.126290214024833)\n", + "ax.legend()\n", + "ax.figure.tight_layout()\n", + "# ax.set_ylabel(\"input function\")\n", + "# ax.figure.savefig('fit_5nodes_lambdas.pdf')" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "fb75829f-ff65-4d92-b4e6-7a7670fc829a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pSTAT5 (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, pSTAT5 = simulate_pSTAT5(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, pSTAT5, **kwargs)\n", + "ax.plot(df_pSTAT5['time'], df_pSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pSTAT5\");\n", + "# ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "c68e4f72-bd06-48db-bebf-7079df51a616", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for tSTAT5 (all regularization strengths)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for regstrength in sorted(regproblems.keys()):\n", + " t, tSTAT5 = simulate_tSTAT5(problem=regproblems[regstrength], result=regresults[regstrength])\n", + " if regstrength == chosen_regstrength:\n", + " kwargs = dict(color='black', label=f'$\\\\mathbf{{\\\\lambda = {regstrength}}}$', zorder=2)\n", + " else:\n", + " kwargs = dict(label=f'$\\\\lambda = {regstrength}$', alpha=0.5)\n", + " ax.plot(t, tSTAT5, **kwargs)\n", + "ax.plot(df_tSTAT5['time'], df_tSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"tSTAT5\");\n", + "# ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "7c0ed0ba-8870-470e-8009-0c69dc4a48df", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj8AAAFjCAYAAADSCGomAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACcxklEQVR4nOzdd1yTV/s/8E8GWYS9RGW4KlpnseKodZQWRa3jcdsqarW27tXWWhdW0VatdTxa7YOjdRVr1Vbco1ZRWncdOEERZQgkIWQn9+8Pfrm/RAIECPt6v155KXfucRJCcuWc61yHwzAMA0IIIYSQWoJb2Q0ghBBCCKlIFPwQQgghpFah4IcQQgghtQoFP4QQQgipVSj4IYQQQkitQsEPIYQQQmoVCn4IIYQQUqtQ8EMIIYSQWoWCH0IIIYTUKhT8kBrp6NGjaNOmDUQiETgcDmQyGSIiIhAYGFim8z548ADvvfceXFxcwOFwcODAAbu0tzrZtm0bOBwOkpKS7Hrebt26oVu3bnY9Z1W+bm0TGBiIPn36VHYzCAFAwQ8pZ+YPSg6Hg/Pnzxe4n2EY+Pn5gcPhFHhj5HA4mDx5comvmZmZiSFDhkAsFmPDhg346aef4OjoWGA/lUqFRYsW4ezZszafe/To0fj333+xdOlS/PTTT2jXrl2J20cq3p07d7Bo0SK7B2zlLTY2FosWLarsZtisujzPx48fx7hx49CiRQvweLxCvxQtWrSIff+ydrtw4QK7b0REhNV9goKCKuhRkZLgV3YDSO0gEomwa9cuvPXWWxbb//zzTzx79gxCodBu1/rnn3+Qk5ODJUuWIDQ0lN2+ZcsWmEwm9meVSoXFixcDgE3f/NVqNS5evIh58+aVKigjRTt+/Hi5nfvOnTtYvHgxunXrVuCDrjyvW1axsbHYsGFDtQmAinqeq5Jdu3Zh7969eOONN1C3bt1C9xs4cCAaN25cYPuXX34JpVKJN99802K7UCjEjz/+aLHNxcXFPo0mdkXBD6kQ4eHhiImJwdq1a8Hn/9/LbteuXQgODsbLly/tdq309HQAgKurq8V2BweHMp03IyPD6nnLIjc312qvVGE0Gg0EAgG43JrTaatSqSCRSCAQCCrl+pV1XXszGAwwmUw15vGUp2XLlmHLli1wcHBAnz59cOvWLav7tWrVCq1atbLYlpycjGfPnuGjjz4q8Fzz+Xx88MEH5dZuYj815x2UVGnDhw9HZmYmTpw4wW7T6XTYt28fRowYYbfrdOvWDaNHjwYAvPnmm+BwOIiIiAAAi5yfpKQkeHl5AQAWL17MdlEX9g170aJFCAgIAADMmTMHHA7H4pvttWvX0KtXLzg7O0MqleKdd97BpUuXLM5hHgL8888/8emnn8Lb2xv169cv9LGcPXsWHA4He/bswVdffYV69epBIpFAoVAAAOLj49GzZ0+4uLhAIpGga9euFt3w+c/Trl07iEQiNGrUCD/88APbnW+WlJQEDoeDbdu2FTi+qOfF7ODBg+jduzfq1q0LoVCIRo0aYcmSJTAajRb7devWDS1atMCVK1fw9ttvQyKR4Msvv2Tvy98DFxgYWOhwg3mo8smTJ/j000/RtGlTiMVieHh4YPDgwRbDLtu2bcPgwYMBAN27dy9wDms5P+np6Rg3bhx8fHwgEonQunVrbN++3WIf83O2cuVKbN68GY0aNYJQKMSbb76Jf/75p8jnCwD0ej0WL16MJk2aQCQSwcPDA2+99Rb7NxIREYENGzYAgMVjf/Xaa9asYa99584dAEBCQgIGDRoEd3d3iEQitGvXDocOHbK4vvn1eOHCBcycORNeXl5wdHTEgAED2EDfzGQyYdGiRahbty4kEgm6d++OO3fuIDAwkP37Ku55Njt//jzat28PkUiEhg0bYseOHcU+V/ZWt27dUn8Z2r17NxiGwciRI63ebzQa2b9RUnVRzw+pEIGBgejYsSN2796NXr16AQCOHDkCuVyOYcOGYe3atXa5zrx589C0aVNs3rwZkZGRaNCgARo1alRgPy8vL2zcuBGffPIJBgwYgIEDBwJAgW95ZgMHDoSrqytmzJiB4cOHIzw8HFKpFABw+/ZtdOnSBc7Ozvjss8/g4OCAH374Ad26dcOff/6JkJAQi3N9+umn8PLywoIFC5Cbm1vsY1qyZAkEAgFmz54NrVYLgUCA06dPo1evXggODsbChQvB5XKxdetW9OjRA3/99Rfat28PIC8o69mzJ3x9fbF48WIYjUZERkaygZ+9bNu2DVKpFDNnzoRUKsXp06exYMECKBQKfPvttxb7ZmZmolevXhg2bBg++OAD+Pj4WD3nmjVroFQqLbZ99913uH79Ojw8PADkDXHGxcVh2LBhqF+/PpKSkrBx40Z069YNd+7cgUQiwdtvv42pU6di7dq1+PLLL9GsWTMAYP99lVqtRrdu3fDw4UNMnjwZDRo0QExMDCIiIiCTyTBt2jSL/Xft2oWcnBx8/PHH4HA4+OabbzBw4EA8fvy4yA/YRYsWISoqCh999BHat28PhUKBy5cv4+rVq3j33Xfx8ccf4/nz5zhx4gR++uknq+fYunUrNBoNJkyYAKFQCHd3d9y+fRudO3dGvXr18MUXX8DR0RG//PIL+vfvj19//RUDBgywOMeUKVPg5uaGhQsXIikpCWvWrMHkyZOxd+9edp+5c+fim2++Qd++fREWFoYbN24gLCwMGo2G3ceW5/nhw4cYNGgQxo0bh9GjRyM6OhoREREIDg7G66+/XuhzBQDZ2dkFgmlrJBIJJBJJsfuV1s6dO+Hn54e33367wH0qlQrOzs5QqVRwc3PD8OHDsWLFCva9glQhDCHlaOvWrQwA5p9//mHWr1/PODk5MSqVimEYhhk8eDDTvXt3hmEYJiAggOndu7fFsQCYSZMmlema+Y0ePZoJCAhgf87IyGAAMAsXLrTpvImJiQwA5ttvv7XY3r9/f0YgEDCPHj1itz1//pxxcnJi3n777QLteuuttxiDwVDs9c6cOcMAYBo2bMg+ZwzDMCaTiWnSpAkTFhbGmEwmdrtKpWIaNGjAvPvuu+y2vn37MhKJhElJSWG3PXjwgOHz+Uz+P3/zY9u6dWuBdrz6HJkfR2JiosW1X/Xxxx8zEomE0Wg07LauXbsyAJhNmzYV2L9r165M165dC30+fvnlFwYAExkZWeR1L168yABgduzYwW6LiYlhADBnzpwp9rpr1qxhADA///wzu02n0zEdO3ZkpFIpo1AoGIb5v+fMw8ODycrKYvc9ePAgA4D5/fffC30sDMMwrVu3LvCaf9WkSZMYa2/T5ms7Ozsz6enpFve98847TMuWLS2ed5PJxHTq1Ilp0qQJu838ewwNDbV4Hc2YMYPh8XiMTCZjGIZhUlNTGT6fz/Tv39/iOosWLWIAMKNHj2a3FfU8BwQEMACYc+fOsdvS09MZoVDIzJo1q8jnIf/xxd1s/Xs26927t8X7QlFu3brFAGA+++yzAvd98cUXzOeff87s3buX2b17NzN69GgGANO5c2dGr9eXqE2k/NGwF6kwQ4YMgVqtxh9//IGcnBz88ccfdh3yqgxGoxHHjx9H//790bBhQ3a7r68vRowYgfPnzxfoAh8/fjx4PJ7N1xg9ejTEYjH78/Xr1/HgwQOMGDECmZmZePnyJV6+fInc3Fy88847OHfuHEwmE4xGI06ePIn+/ftbJHU2btyY7X2zl/zty8nJwcuXL9GlSxeoVCokJCRY7CsUCjFmzJgSnf/OnTsYO3Ys+vXrh6+++srqdfV6PTIzM9G4cWO4urri6tWrpXossbGxqFOnDoYPH85uc3BwwNSpU6FUKvHnn39a7D906FC4ubmxP3fp0gUA8Pjx4yKv4+rqitu3b+PBgwelaicA/Oc//7HoxcvKysLp06cxZMgQ9vfw8uVLZGZmIiwsDA8ePEBKSorFOSZMmGAxBNqlSxcYjUY8efIEAHDq1CkYDAZ8+umnFsdNmTKlxO1t3rw5+/wAeT2wTZs2Lfa5AvJ6XE6cOFHsbdSoUSVul6127twJAFaHvKKiorB8+XIMGTIEw4YNw7Zt27B06VJcuHAB+/btK7c2kdKhYS9SYby8vBAaGopdu3ZBpVLBaDRi0KBBld2sMsnIyIBKpULTpk0L3NesWTOYTCYkJydbdOk3aNCgRNd4dX/zh6U5t8kauVwOjUYDtVptdbaKtW1lcfv2bXz11Vc4ffp0gWBPLpdb/FyvXr0SJeUqFAoMHDgQ9erVw44dOyw+qNVqNaKiorB161akpKSAYZhCr2urJ0+eoEmTJgWSys3DN+agwMzf39/iZ3MglJ2dXeR1IiMj0a9fP7z22mto0aIFevbsiQ8//LDQoVdrXn1tPHz4EAzDYP78+Zg/f77VY9LT01GvXj2b229+vK++Ztzd3S2CPlu8ei3z9Yp7rgCgc+fOJbqWvTEMg127dqFFixY2/45mzJiB+fPn4+TJkxg2bFg5t5CUBAU/pEKNGDEC48ePR2pqKnr16mXXmVPVRf7eitLsb56u/+2336JNmzZWj5FKpRb5GMXJH1DkZ0uOhUwmQ9euXeHs7IzIyEg0atQIIpEIV69exeeff25RXgAo+eOPiIjA8+fP8ffff8PZ2dnivilTpmDr1q2YPn06OnbsyBafHDZsWIHrlpfCevHyB2LWvP3223j06BEOHjyI48eP48cff8R3332HTZs24aOPPrLp2oW9NmbPno2wsDCrx7waxJS2/aVRlmtlZGTY9HqUSqXlkmNz4cIFPHnyBFFRUTYfY07Cz8rKsnt7SNlQ8EMq1IABA/Dxxx/j0qVLFgmVlaGwD/yS8PLygkQiwb179wrcl5CQAC6XCz8/vzJfJz9zArezs7NFHaNXeXt7QyQS4eHDhwXue3Wb+Ru8TCaz2P5qL4c1Z8+eRWZmJvbv32+RBJqYmFjsscVZvnw5Dhw4gP3791stFrdv3z6MHj0aq1atYrdpNJoCj6Mkv+uAgADcvHkTJpPJovfHPHxnnvVnD+7u7hgzZgzGjBkDpVKJt99+G4sWLWKDn5K+Rs1Drw4ODkW+NkrC/HgfPnxo0dOUmZlZoMfGHn9ThXnzzTdtej0uXLiwXOoi7dy5ExwOp0RD9eahR3tPMCBlR8EPqVBSqRQbN25EUlIS+vbtW6ltMc8IefWDsiR4PB7ee+89HDx4EElJSez097S0NLao46u9FWUVHByMRo0aYeXKlRgxYkSBb7kZGRnw8vICj8dDaGgoDhw4gOfPn7N5Pw8fPsSRI0csjnF2doanpyfOnTuH6dOns9v/+9//Ftse87f5/N/edTqdTccW5eTJk/jqq68wb9489O/fv9Brv9prsG7dugI9BOZaSrb8rsPDw3H8+HHs3buXzfsxGAxYt24dpFIpunbtWvIHY0VmZiY7aw3I+9to3LgxkpOTrbbbll5Sb29vdOvWDT/88AOmTJkCX19fi/vNr42SeOedd8Dn87Fx40a8++677Pb169cX2Lckz3NJ7dy5E2q1utj98ufe2Yter0dMTAzeeustq0N3Go0Ger0eTk5OFtuXLFkChmHQs2dPu7eJlA0FP6TCFZWr8qrLly/j66+/LrC9W7duBapFl5RYLEbz5s2xd+9evPbaa3B3d0eLFi3QokWLEp3n66+/xokTJ/DWW2/h008/BZ/Pxw8//ACtVotvvvmmTG20hsvl4scff0SvXr3w+uuvY8yYMahXrx5SUlJw5swZODs74/fffweQN536+PHj6Ny5Mz755BMYjUasX78eLVq0wPXr1y3O+9FHH2H58uX46KOP0K5dO5w7dw73798vtj2dOnWCm5sbRo8ejalTp4LD4eCnn34q87DJ8OHD4eXlhSZNmuDnn3+2uO/dd9+Fj48P+vTpg59++gkuLi5o3rw5Ll68iJMnT1oEFQDQpk0b8Hg8rFixAnK5HEKhED169IC3t3eB606YMAE//PADIiIicOXKFQQGBmLfvn24cOEC1qxZU+ADrrSaN2+Obt26ITg4GO7u7rh8+TL27dtnUT08ODgYADB16lSEhYWBx+MVmzuyYcMGvPXWW2jZsiXGjx+Phg0bIi0tDRcvXsSzZ89w48aNErXTx8cH06ZNw6pVq/D++++jZ8+euHHjBo4cOQJPT0+L3p6SPM8lZc+cn5s3b7J1jx4+fAi5XM6+z7Ru3brAF7Njx44hMzOz0No+qampaNu2LYYPH872UB47dgyxsbHo2bMn+vXrZ7e2EzuprGlmpHYobNr5qwqb6l7YbcmSJSW+5qtT3RmGYeLi4pjg4GBGIBAUO022sKnuDMMwV69eZcLCwhipVMpIJBKme/fuTFxcnE3tKox5qntMTIzV+69du8YMHDiQ8fDwYIRCIRMQEMAMGTKEOXXqlMV+p06dYtq2bcsIBAKmUaNGzI8//sjMmjWLEYlEFvupVCpm3LhxjIuLC+Pk5MQMGTKESU9Pt2mq+4ULF5gOHTowYrGYqVu3LvPZZ58xx44dKzDtuWvXrszrr79u9fG8OuW8qN+/+ZzZ2dnMmDFjGE9PT0YqlTJhYWFMQkICExAQYDEFm2EYZsuWLUzDhg0ZHo9ncQ5rU+zT0tLY8woEAqZly5YFygAU9Xoo7rXEMAzz9ddfM+3bt2dcXV0ZsVjMBAUFMUuXLmV0Oh27j8FgYKZMmcJ4eXkxHA6HnfZe1LUZhmEePXrEjBo1iqlTpw7j4ODA1KtXj+nTpw+zb98+dp/CXo/m113+35vBYGDmz5/P1KlThxGLxUyPHj2Yu3fvMh4eHszEiRMtji/sebb2N84wxZc4KA/mx27t9urrhmEYZtiwYYyDgwOTmZlp9XzZ2dnMBx98wDRu3JiRSCSMUChkXn/9dWbZsmUWv09SdXAYphyy2gghVVr//v3LPM2a1G4ymQxubm74+uuvMW/evMpuDiElQnV+CKnhXs2TePDgAWJjY21azJUQoOBrCMirwA3YtigwIVUN9fwQUsP5+voiIiICDRs2xJMnT7Bx40ZotVpcu3YNTZo0qezmkWpg27Zt2LZtG7usy/nz57F792689957OHbsWGU3j5ASo4RnQmq4nj17Yvfu3UhNTYVQKETHjh2xbNkyCnyIzVq1agU+n49vvvkGCoWCTYK2NhmBkOqAen4IIYQQUqtQzg8hhBBCahUKfgghhBBSq9S6nB+TyYTnz5/DycmpXEuxE0IIIaTiMAyDnJwc1K1bt8DCxK+qdcHP8+fP7b7WEiGEEEKqhuTkZNSvX7/IfWpd8GMuTZ+cnGz3NZcIIYQQUjkUCgX8/PxsWoKm1gU/5qEuZ2dnCn4IIYSQGsaWlBZKeCaEEEJIrULBDyGEEEJqFQp+CCGEEFKr1LqcH0IIqUlMJhN0Ol1lN4OQcufg4AAej2eXc1Vq8HPu3Dl8++23uHLlCl68eIHffvsN/fv3L3T//fv3Y+PGjbh+/Tq0Wi1ef/11LFq0CGFhYRXXaEIIqSJ0Oh0SExNhMpkquymEVAhXV1fUqVOnzHX6KjX4yc3NRevWrTF27FgMHDiw2P3PnTuHd999F8uWLYOrqyu2bt2Kvn37Ij4+Hm3btq2AFhNCSNXAMAxevHgBHo8HPz+/You6EVKdMQwDlUqF9PR0AICvr2+ZzlepwU+vXr3Qq1cvm/dfs2aNxc/Lli3DwYMH8fvvv1PwQwipVQwGA1QqFerWrQuJRFLZzSGk3InFYgBAeno6vL29yzQEVq1zfkwmE3JycuDu7l7oPlqtFlqtlv1ZoVBURNMIIaRcGY1GAIBAIKjklhBSccyBvl6vL1PwU637SVeuXAmlUokhQ4YUuk9UVBRcXFzYGy1tQQipSWiNQlKb2Ov1Xm2Dn127dmHx4sX45Zdf4O3tXeh+c+fOhVwuZ2/JyckV2EoC5H1DNX9LJYQQQipbtRz22rNnDz766CPExMQgNDS0yH2FQiGEQmEFtYzkp1QqIZfLoVarAeSN17q6usLR0bGSW0ZIzWU0Git09heXy7Xb9OPS4nA4xc4WJpYiIiIgk8lw4MCBCr/2tm3bMH36dMhksgq/tlm1C352796NsWPHYs+ePejdu3dlN4dYwTAMMjMzkZGRAQBs8KlQKKBQKODl5QV3d3eanUKInRmNRiQnJ1do3R+BQAA/Pz+bA6CoqCjs378fCQkJEIvF6NSpE1asWIGmTZuWug0vXryAm5tbqY8ntU+lBj9KpRIPHz5kf05MTMT169fh7u4Of39/zJ07FykpKdixYweAvKGu0aNH4/vvv0dISAhSU1MB5PUouLi4VMpjIJYYhsHLly+Rnp4OR0dHi2RMkUgErVaL1NRUGI1GeHt7U74CIXZkLnjI4/HA55f/27vBYIBOp4PJZLI5+Pnzzz8xadIkvPnmmzAYDPjyyy/x3nvv4c6dO6XuFa5Tp06pjquKGIaB0Wgs8PvT6XSlSm4v7XE1XaV+9b58+TLatm3LTlOfOXMm2rZtiwULFgDIi+afPn3K7r9582YYDAZMmjQJvr6+7G3atGmV0n5SkEKhQEZGRoHAx0woFMLJyQkZGRnIysqqhBYSUvPx+Xw4ODiU+600AdbRo0cRERGB119/Ha1bt8a2bdvw9OlTXLlypdBjdDodJk+eDF9fX4hEIgQEBCAqKoq9n8PhWAzfxMXFoU2bNhCJRGjXrh0OHDgADoeD69evAwDOnj0LDoeDY8eOoW3bthCLxejRowfS09Nx5MgRNGvWDM7OzhgxYgRUKpVF29966y24urrCw8MDffr0waNHj4p8vCaTCVFRUWjQoAHEYjFat26Nffv2sfeb23LkyBEEBwdDKBTi/Pnz6NatGyZPnozp06fD09OTLeb7559/on379hAKhfD19cUXX3wBg8HAnq+w4wqzePFieHl5wdnZGRMnTrToNSzu8SYlJYHD4WD//v3o3r07JBIJWrdujYsXL1pcY9u2bfD394dEIsGAAQOQmZlpcf+NGzfQvXt3ODk5wdnZGcHBwbh8+XKR7S6rSu356datGxiGKfT+bdu2Wfx89uzZ8m0QKROtVouMjAwIBIIiv2k4ODhAIpHg5cuXEIlElANESC0ml8sBoMiSJWvXrsWhQ4fwyy+/wN/fH8nJyYVOXlEoFOjbty/Cw8Oxa9cuPHnyBNOnT7e676JFi7B+/XpIJBIMGTIEQ4YMgVAoxK5du6BUKjFgwACsW7cOn3/+OYC8wrwzZ85Eq1atoFQqsWDBAgwYMADXr18vdBg/KioKP//8MzZt2oQmTZrg3Llz+OCDD+Dl5YWuXbuy+33xxRdYuXIlGjZsyA7hbd++HZ988gkuXLgAAEhJSUF4eDgiIiKwY8cOJCQkYPz48RCJRFi0aBF7rlePK8ypU6cgEolw9uxZJCUlYcyYMfDw8MDSpUtL9HjnzZuHlStXokmTJpg3bx6GDx+Ohw8fgs/nIz4+HuPGjUNUVBT69++Po0ePYuHChRbtGDlyJNq2bYuNGzeCx+Ph+vXrcHBwKLLtZcbUMnK5nAHAyOXyym5KjWIymZiUlBTm1q1bTEpKik23u3fvMklJSYzBYKjs5hNS7ajVaubOnTuMWq1mt+l0OubevXtMUlKSzX+HZbklJSUx9+7dY3Q6Xakeg9FoZHr37s107ty5yP2mTJnC9OjRgzGZTFbvB8D89ttvDMMwzMaNGxkPDw+L52XLli0MAObatWsMwzDMmTNnGADMyZMn2X2ioqIYAMyjR4/YbR9//DETFhZWaLsyMjIYAMy///5r9X6NRsNIJBImLi7OYvu4ceOY4cOHW7TlwIEDFvt07dqVadu2rcW2L7/8kmnatKnF87BhwwZGKpUyRqOx0OOsGT16NOPu7s7k5uay2zZu3GhxruIeb2JiIgOA+fHHH9l9bt++zQBg7t69yzAMwwwfPpwJDw+3OM/QoUMZFxcX9mcnJydm27ZtxbaZYay/7s1K8vlOGafELnJzcyGTyeDk5GTzMVKpFDk5OZWa8U8IqTyTJk3CrVu3sGfPHnbbxIkTIZVK2RuQNzPp+vXraNq0KaZOnYrjx48Xes579+6hVatWEIlE7Lb27dtb3bdVq1bs/318fCCRSNCwYUOLbeblFADgwYMHGD58OBo2bAhnZ2cEBgYCgEV6Rn4PHz6ESqXCu+++a/GYduzYUWC4rF27dgWODw4Otvj57t276Nixo0WuZOfOnaFUKvHs2bNCjytM69atLaqDd+zYEUqlku1Vs/Xx5n8ezctOmJ+3u3fvIiQkxGL/jh07Wvw8c+ZMfPTRRwgNDcXy5cuLHUq0h2o324tUPQzDIDs7G1wut0Q5AFwuF2KxGFlZWZBKpVSSgJBaZPLkyfjjjz9w7tw51K9fn90eGRmJ2bNnW+z7xhtvIDExEUeOHMHJkycxZMgQhIaGWuTOlEb+oRUOh1NgqIXD4ViUDejbty8CAgKwZcsW1K1bFyaTCS1atCh0dp1SqQQAHD58GPXq1bO479X3O2vD/6VNCbBXKoGtj/fV5xFAicotLFq0CCNGjMDhw4dx5MgRLFy4EHv27MGAAQPs8jisoeCHlFlubi5ycnJK1OtjZg5+ZDIZfHx8yqF1hJCqhGEYTJkyBb/99hvOnj2LBg0aWNzv7e1ttXCts7Mzhg4diqFDh2LQoEHo2bMnsrKyCuQKNW3aFD///DO0Wi0bYPzzzz9lbndmZibu3buHLVu2oEuXLgCA8+fPF3lM8+bNIRQK8fTpU4v8ntJq1qwZfv31VzAMwwYZFy5cgJOTk0UAaasbN25ArVaza2ZdunQJUqkUfn5+pXq8hbU5Pj7eYtulS5cK7Pfaa6/htddew4wZMzB8+HBs3bq1XIMfGvYiZcIwDGQyGTgcTqkLnUmlUmRnZ1vMqiCE1EyTJk3Czz//jF27dsHJyQmpqalITU1li6Fas3r1auzevRsJCQm4f/8+YmJiUKdOHbi6uhbYd8SIETCZTJgwYQLu3r2LY8eOYeXKlQDKtjSCm5sbPDw8sHnzZjx8+BCnT5/GzJkzizzGyckJs2fPxowZM7B9+3Y8evQIV69exbp167B9+/YSt+HTTz9FcnIypkyZgoSEBBw8eBALFy7EzJkzS1U3TafTYdy4cbhz5w5iY2OxcOFCTJ48GVwut1SP15qpU6fi6NGjWLlyJR48eID169fj6NGj7P1qtRqTJ0/G2bNn8eTJE1y4cAH//PMPmjVrVuJrlQT1/JAyUavVyMnJKVM3q0AggEqlQnZ2NsRiMdX+IaSM8k99rmrX2bhxI4C82b75bd26FREREVaPcXJywjfffIMHDx6Ax+PhzTffRGxsrNUPfGdnZ/z+++/45JNP0KZNG7Rs2RILFizAiBEjLPKASorL5WLPnj2YOnUqWrRogaZNm2Lt2rUFHserlixZAi8vL0RFReHx48dwdXXFG2+8gS+//LLEbahXrx5iY2MxZ84ctG7dGu7u7hg3bhy++uqrUj2md955B02aNMHbb78NrVaL4cOHs7PGSvt4X9WhQwds2bIFCxcuxIIFCxAaGoqvvvoKS5YsAQDweDxkZmZi1KhRSEtLg6enJwYOHIjFixeX6jHZisMwRcw1r4EUCgVcXFwgl8vh7Oxc2c2p9tLT05GRkVHm6qoGgwE5OTkICAhgkxwJIYXTaDRITExEgwYN2A/16lDhuTLs3LkTY8aMgVwuZ4d4SPVk7XVvVpLPd+r5IaWm1+uhUCjs8mbC5/PB4/GQnZ0NiURCS18QUgo8Hg9+fn61bm2vV+3YsQMNGzZEvXr1cOPGDXz++ecYMmQIBT6ERcEPKTWVSgWNRmO3NXUcHR2hUCjg6upaquRpQkheAFTVgpGKlpqaigULFiA1NRW+vr4YPHgwW7iPEICCH1JKDMNAoVDAwcHBbjk65vWIsrKy4OjoSL0/hJBS+eyzz/DZZ59VdjNIFUafLqRUtFotVCpVmRIIrXF0dIRSqURubq5dz0sIIYSYUfBDSkWtVkOv19t9/RVzocSsrKwKzVsghBBSe1DwQ0rMPORV1OKlZUG9P4QQQsoTBT+kxLRaLdRqtd2HvMzMs0dkMhlqWSUGQgghFYCCH1JiGo0GBoOhROt4lZSjoyNycnKo94cQQojdUfBDSiwnJ8fuuT6v4vF44HA4kMvl1PtDCCHErij4ISWi0+mgVqsrZAV2c92fotb8IYQQQkqKgh9SIhqNBjqdrtySnfPj8/lgGAZyubzcr0UIqTgRERHgcDiYOHFigfsmTZoEDofDrvMVERGB/v37F3quwMBAcDicArfly5eXU+tJTUDBDymR3NzcCq0eK5FIoFAooNFoKuyahJDy5+fnhz179lj07Go0GuzatQv+/v4lOldkZCRevHhhcZsyZYq9m0xqEAp+iM2MRiNUKlWF9PqYCQQCdg0xQkjN8cYbb8DPzw/79+9nt+3fvx/+/v5o27Ztic7l5OSEOnXqWNwcHR3t3WRSg9DyFsRmWq0WGo0GLi4uFXpdiUQCuVwOV1fXCg28CKlOGIaBSqWqlGtLJJJSLXMzduxYbN26FSNHjgQAREdHY8yYMTh79qydW0iIJQp+iM3MQ08VveaWSCRCVlYWcnJy4OHhUaHXJqS6UKlUkEqllXJtpVJZqp6WDz74AHPnzsWTJ08AABcuXMCePXtKHPx8/vnn+Oqrryy2HTlyBF26dClxm0jtQMEPsZlSqSz3Ke6FEYlEkMlkcHFxKdf6QoSQiuPl5YXevXtj27ZtYBgGvXv3hqenZ4nPM2fOHDZB2qxevXp2aiWpiehThNhEp9NBo9FU2rCTWCxGdnY2cnNzK3zYjZDqQCKRQKlUVtq1S2vs2LGYPHkyAGDDhg2lOoenpycaN25c6jaQ2oeCH2ITrVYLnU5Xad3qHA4HAoEAMpkMTk5OFT70RkhVx+FwqmWSb8+ePaHT6cDhcBAWFlbZzSG1BAU/xCZarbbSAw7ztPfKzG0ghNgXj8fD3bt32f9bI5fLcf36dYttHh4e8PPzA5BXdT41NdXifolEAmdnZ/s3mNQI9PWZFIthGCiVykqfaWUOvmjaOyE1i7Ozc5GBytmzZ9G2bVuL2+LFi9n7FyxYAF9fX4vbZ599VhFNJ9UUh6llCycpFAq4uLhALpfTtwIbabVaJCUlQSQSVVrCs5ler4darUZgYGC5rSpPSHWg0WiQmJiIBg0a0N8CqTWKet2X5PO9Unt+zp07h759+6Ju3brgcDg4cOBAscecPXsWb7zxBoRCIRo3boxt27aVeztrO51OB4PBUCDwMRqNiIuLw4EDBxAXFwej0VjubXFwcIDBYEBOTk65X4sQQkjNVKnBT25uLlq3bm1zhn9iYiJ69+6N7t274/r165g+fTo++ugjHDt2rJxbWrtZW1oiNjYWnTt3xuDBgzFp0iQMHjwYnTt3RmxsbLm3RywWQy6XQ6/Xl/u1CCGE1DyVGvz06tULX3/9NQYMGGDT/ps2bUKDBg2watUqNGvWDJMnT8agQYPw3XfflXNLay+GYZCbm2uxintsbCwmTJiAoKAgHDp0CPfv38ehQ4cQFBSECRMmlHsAJBaLodFokJubW67XIYQQUjNVq4TnixcvIjQ01GJbWFgYLl68WEktqvn0ej10Oh075GU0GhEZGYnQ0FBER0cjODgYjo6OCA4ORnR0NEJDQ7FkyZJyHwITCoXIzs6GyWQq1+sQQgipeapV8JOamgofHx+LbT4+PlAoFBYrA+en1WqhUCgsbsR2Op3OIviJj49HcnIypkyZUmDqO5fLxeTJk/H06VPEx8eXa7vEYjFUKlWlrWVECCGk+qpWwU9pREVFwcXFhb2Z60IQ22i1WosFC9PT0wEAQUFBVvc3bzfvV164XC64XC7kcnm5XocQQkjNU62Cnzp16iAtLc1iW1paGpydnSEWi60eM3fuXMjlcvaWnJxcEU2tMXJzcy1meXl7ewMAEhISrO5v3m7erzxJJBLk5ORYTcgmhBBCClOtgp+OHTvi1KlTFttOnDiBjh07FnqMUChkC2gVV0iLWDIYDNBqtRbBT0hICPz8/LBu3boC+TYmkwnr16+Hv78/QkJCyr19NO2dEEJIaVRq8KNUKnH9+nW2bHliYiKuX7+Op0+fAsjrtRk1ahS7/8SJE/H48WN89tlnSEhIwH//+1/88ssvmDFjRmU0v8Yzr+eVv7Izj8fDggULcPLkSYwdOxaXL1+GUqnE5cuXMXbsWJw8eRLz588vtEy9vdG0d0IIISVVqcHP5cuX2VLlADBz5ky0bdsWCxYsAAC8ePGCDYQAoEGDBjh8+DBOnDiB1q1bY9WqVfjxxx9pMbxyotPpwDCMRc4PAISHh2Pz5s1ISEhAv3790LRpU/Tr1w/37t3D5s2bER4eXmFtFIlENO2dEGIXSUlJ4HA4BdYRq062bdsGV1fXEh1TEx53SVXqwqbdunVDUatrWKve3K1bN1y7dq0cW0XM1Go1+HzrL5Hw8HCEhYUhPj4e6enp8Pb2RkhISIX1+JjlX+3d2dm50hdfJaS6MRqN+Ouvv/DixQv4+vqiS5cuFf53XFX4+fnhxYsX8PT0rOymYNGiRThw4ECVDUgiIiIgk8lsWpmhKqJV3YlVJpMJarW6yLW8eDweOnXqVIGtso5WeyekdPbv349Zs2YhKSmJ3RYYGIhVq1Zh4MCBldewSmAe4q9Tp05lN4VUAPqaTKzS6XTQ6/WVvpK7Lcy9PZT4TIjt9u/fj0GDBqFly5a4ePEicnJycPHiRbRs2RKDBg3C/v37y+W6JpMJUVFRaNCgAcRiMVq3bo19+/YByKsoHxoairCwMHZUICsrC/Xr12fTIc6ePQsOh4PDhw+jVatWEIlE6NChA27dumVxnfPnz6NLly4Qi8Xw8/PD1KlTLYbHAwMDsWTJEowaNQrOzs6YMGFCgeEf87WOHTuGtm3bQiwWo0ePHkhPT8eRI0fQrFkzODs7Y8SIERY1x4p6jPnPe+rUKbRr1w4SiQSdOnXCvXv3AOSNeixevBg3btwAh8MBh8NhR0JWr16Nli1bwtHREX5+fvj000+hVCpL9Dv4+++/0bZtW4hEIrRr167AaIrRaMS4cePY9jdt2hTff/89e/+iRYuwfft2HDx4kG3f2bNnAQCff/45XnvtNUgkEjRs2BDz58+vkjmZFPwQq8yLmZa1+9toNEKpVJZ7JWZz749Wqy3X6xBSExiNRsyaNQt9+vTBgQMH0KFDB0ilUnTo0AEHDhxAnz59MHv27HKp1B4VFYUdO3Zg06ZNuH37NmbMmIEPPvgAf/75JzgcDrZv345//vkHa9euBZA30aVevXps8GM2Z84crFq1Cv/88w+8vLzQt29f9kP20aNH6NmzJ/7zn//g5s2b2Lt3L86fP4/JkydbnGPlypVo3bo1rl27hvnz5xfa5kWLFmH9+vWIi4tDcnIyhgwZgjVr1mDXrl04fPgwjh8/jnXr1tn0GPObN28eVq1ahcuXL4PP52Ps2LEAgKFDh2LWrFl4/fXX8eLFC7x48QJDhw4FkPdlb+3atbh9+za2b9+O06dP47PPPrP5+VcqlejTpw+aN2+OK1euYNGiRZg9e7bFPiaTCfXr10dMTAzu3LmDBQsW4Msvv8Qvv/wCAJg9ezaGDBmCnj17su0zjwI4OTlh27ZtuHPnDr7//nts2bKlai5BxdQycrmcAcDI5fLKbkqVlpGRwdy6dYtJSUkp1S0mJoZ55513GJFIxABgJBIJExYWxuzfv7/U5yzu9u+//zIZGRmV/dQRUiHUajVz584dRq1Wl/jYM2fOMACYixcvWr0/Li6OAcCcOXOmjK20pNFoGIlEwsTFxVlsHzduHDN8+HD2519++YURiUTMF198wTg6OjL3798v0PY9e/aw2zIzMxmxWMzs3buXPd+ECRMsrvHXX38xXC6Xfb4CAgKY/v37W+yTmJjIAGCuXbtmca2TJ0+y+0RFRTEAmEePHrHbPv74YyYsLMzmx2jtvIcPH2YAsO1buHAh07p168KeSlZMTAzj4eHB/rx161bGxcWl0P1/+OEHxsPDw+J1s3HjRovHbc2kSZOY//znP+zPo0ePZvr161ds+7799lsmODi42P1sVdTrviSf75TzQ6zKzc0t1ZCXTqfD/Pnz8fPPP1tsV6lUOHbsGI4dO4YRI0YgMjKy0MKUpWWe9u7q6lpoojYhJG8mLQC0aNHC6v3m7eb97OXhw4dQqVR49913LbbrdDp21i8ADB48GL/99huWL1+OjRs3okmTJgXOlb++m7u7O5o2bYq7d+8CAG7cuIGbN29i586d7D4Mw8BkMiExMRHNmjUDALRr186mdrdq1Yr9v4+PDzukk3/b33//XaLH+Op5fX19AeRVx/f39y+0LSdPnkRUVBQSEhKgUChgMBig0WigUqkgkUiKfSx3795lhwvNrNXK27BhA6Kjo/H06VOo1WrodDq0adOm2PPv3bsXa9euxaNHj6BUKmEwGKpkfT36hCAFGAwG6HS6EgcQWq0WY8eOZcezR44ciYiICAQGBuLRo0fYsWMHdu3ahV27duHx48fYvn27XROURSIRsrOzoVKpquQfGyFVhfmD9tatW+jQoUOB+835M+b97MWcm3L48GHUq1fP4j6hUMj+X6VS4cqVK+DxeHjw4EGprvPxxx9j6tSpBe7LH1g4OjradL78Ez84HE6BiSAcDocd2rf1MVo7L4AiUwSSkpLQp08ffPLJJ1i6dCnc3d1x/vx5jBs3DjqdzqbgxxZ79uzB7NmzsWrVKnTs2BFOTk749ttvi12z8eLFixg5ciQWL16MsLAwuLi4YM+ePVi1apVd2mVPFPyQAsyLmZbkD4lhGMyYMQNnz56FWCzG5s2b0aNHD/b+Fi1a4JtvvkGfPn0wYcIEXLp0CRMnTsT27dvtNq3W/KYkk8ng5ORUoD4RISRPly5dEBgYiGXLluHAgQMWJSLyJ+t26dLFrtdt3rw5hEIhnj59iq5duxa636xZs8DlcnHkyBGEh4ejd+/eFu8nAHDp0iU2kMnOzsb9+/fZHp033ngDd+7cQePGje3aflvY+hiLIxAICuRcXblyBSaTCatWrWJ/Z+Y8HFs1a9YMP/30EzQaDdv7c+nSJYt9Lly4gE6dOuHTTz9ltz169KjY9sXFxSEgIADz5s1jtz158qRE7asolPBMCiisuGFRtm3bhoMHD4LP52Pr1q0F3qjM3n77bezevRsikQhnzpzBsmXL7NVsAHmJz0qlEmq12q7nJaQm4fF4WLVqFf744w/079/fYrZX//798ccff2DlypV2r/fj5OSE2bNnY8aMGdi+fTsePXqEq1evYt26ddi+fTuAvB6T6Oho7Ny5E++++y7mzJmD0aNHIzs72+JckZGROHXqFG7duoWIiAh4enqif//+APJmHMXFxWHy5Mm4fv06Hjx4gIMHDxZIeC4PtjxGWwQGBrKrHrx8+RJarRaNGzeGXq/HunXr8PjxY/z000/YtGlTido3YsQIcDgcjB8/Hnfu3EFsbCxWrlxpsU+TJk1w+fJlHDt2DPfv38f8+fPxzz//FGjfzZs3ce/ePbx8+RJ6vR5NmjTB06dPsWfPHjx69Ahr167Fb7/9VqL2VRQKfkgBWq22RG96ycnJbBCzYMGCYr8ttm3bls3+37RpE/7444/SN/YV5nbTtHdCijZw4EDs27cP//77Lzp16gRnZ2d06tQJt27dwr59+8qtzs+SJUswf/58REVFoVmzZujZsycOHz6MBg0aICMjA+PGjcOiRYvwxhtvAAAWL14MHx8fTJw40eI8y5cvx7Rp0xAcHIzU1FT8/vvvbJ5iq1at8Oeff+L+/fvo0qULu3JA3bp1y+UxleQx2uo///kPevbsie7du8PLywu7d+9G69atsXr1aqxYsQItWrTAzp07ERUVVaK2SaVS/P777/j333/Rtm1bzJs3DytWrLDY5+OPP8bAgQMxdOhQhISEIDMz06IXCADGjx+Ppk2bol27dvDy8sKFCxfw/vvvY8aMGZg8eTLatGmDuLi4ImfRVSYOwxRRYrkGUigUcHFxgVwup7wQKxiGQWJiIkwmk03DXgzD4IMPPsDZs2fRoUMHxMTE2FxlOSoqCuvXr4ebmxvOnDkDLy+vsjYfQF7wptfrERgYWC3qFBFSGhqNBomJiWjQoIFF8mpJVbcKz2fPnkX37t2RnZ1d4mUcSPVX1Ou+JJ/v1PNDLOj1ehgMhiIrO+d3+PBhnD17FkKhECtWrCjR8hKzZs1C8+bNkZ2djblz5xa51ElJCIVC6HQ6Wu+LEBvweDx069YNw4cPR7du3ap04EOIvVDwQyzo9Xro9XqbZnoZjUZ2rPjTTz8tcXKhQCDAmjVrwOfzceTIERw8eLBUbbbGPPOrPIq0EUIIqd4o+CEWdDodANiU7Hzw4EE8ePAArq6umDBhQqmu9/rrr2P69OkA8qqdZmRklOo8rxKJRFCr1RYl5wkh1Z95QWwa8iJlQcEPsaDRaGzq9jYYDFi9ejWAvOS4suRPTZ48GS1atIBMJsOSJUtKfZ78uFwueDwe5HK53YbTCCGE1AwU/BAWwzBQqVQ25fscOXIEiYmJcHNzY9ejKS0HBwesWLECHA4Hv/76K+Li4sp0PjPztHeNRmOX8xFCCKkZKPghLPNiprYEP1u3bgUAjBo1yi5Vmtu0aYMPP/wQAPDll1+yw29lwefzYTQaado7IYQQCxT8EJZ5pldxyc63b99GfHw8+Hw+G7CUhtFoRFxcHA4cOIC4uDjMnj0bnp6eePDgAX744YdSnzc/83pf5tWeCSGEEFregrBsDRC2bdsGAOjVq1ep1/6JjY1FZGQkkpOT2W1+fn54//33ER0dje+//x6DBg0q89pCIpEIWVlZyM3NpQRJQgghAKjnh+SjVquLTXZWKBTYv38/AGDMmDGluk5sbCwmTJiAoKAgHDp0CPfv38ehQ4cQFBSErVu3olGjRlCr1QWqjpaWUChEdnZ2kQsGEkIIqT0o+CEA8hYzVKvVxeb7HD58GBqNBk2aNEH79u1LfB2j0YjIyEiEhoYiOjoawcHBcHR0RHBwMKKjoxEaGsquihwTE4ObN2+W6vHkJxaLoVKpqOghITVAt27d2PIYQN4aU2vWrKm09pRFUlISOBwOrl+/XtlNqXUo+CEAbK/svG/fPgDAoEGDSrVqenx8PJKTkzFlypQC1aC5XC4mT56MtLQ0dn2wRYsWlXmqOpfLBZfLpWnvhNRA//zzT6nrjJHai4IfAsC2ZOfk5GRcunQJHA4HAwYMKNV10tPTAQBBQUFW7zdvf++99yASiRAfH4/Y2NhSXSs/R0dHmvZOSD6LFi0qtK7WkiVLsGjRooptUCl5eXnZtA4hIflR8EMA2Jbs/OuvvwIAOnXqhHr16pXqOt7e3gCAhIQEq/ebtwcFBbGrOC9duhRarbZU1zOjae+EWOLxeFiwYEGBAGjJkiVYsGBBua3xtW/fPrRs2RJisRgeHh4IDQ1lh6QjIiLQv39/LF68GF5eXnB2dsbEiROLLH3x6rAXh8PBjz/+iAEDBkAikaBJkyY4dOiQxTG3bt1Cr169IJVK4ePjgw8//BAvX74s9Brbtm2Dq6srjh07hmbNmkEqlaJnz5548eIFu4/JZEJkZCTq168PoVCINm3a4OjRoxbn+fvvv9G2bVuIRCK0a9cO165dK3Ct4tpW1PNHbEfBDwFQfGVnhmHYROdBgwaV+johISHw8/PDunXrCiQgm0wmrF+/Hv7+/ggJCcGnn34KHx8fPHnyhK0rVBbmae/2qCFESHU3f/58REZGWgRA5sAnMjIS8+fPt/s1X7x4geHDh2Ps2LG4e/cuzp49i4EDB1oMR586dYq9b/fu3di/fz8WL15coussXrwYQ4YMwc2bNxEeHo6RI0ciKysLACCTydCjRw+0bdsWly9fxtGjR5GWloYhQ4YUeU6VSoWVK1fip59+wrlz5/D06VPMnj2bvf/777/HqlWrsHLlSty8eRNhYWF4//338eDBAwCAUqlEnz590Lx5c1y5cgWLFi2yON6Wttny/BEbMbWMXC5nADByubyym1JlmEwm5uHDh8zDhw+ZlJQUq7eTJ08yABiBQMAkJCQUup8tty1btjAcDod59913mYMHDzL37t1jDh48yLz77rsMh8NhtmzZwu67evVqBgDj6urK3L17t0zXTUlJYf79918mKyursp9yQspMrVYzd+7cYdRqdZnOExkZyf5tA2AiIyPt1MKCrly5wgBgkpKSrN4/evRoxt3dncnNzWW3bdy4kZFKpYzRaGQYhmG6du3KTJs2jb0/ICCA+e6779ifATBfffUV+7NSqWQAMEeOHGEYhmGWLFnCvPfeexbXTU5OZgAw9+7ds9qurVu3MgCYhw8fsts2bNjA+Pj4sD/XrVuXWbp0qcVxb775JvPpp58yDMMwP/zwA+Ph4WHx+9q4cSMDgLl27ZpNbSvu+asNinrdl+TznXp+iE35Pua8m65du8LJyalM1wsPD8fmzZuRkJCAfv36oWnTpujXrx/u3buHzZs3Izw8nN130KBBeO211yCTybBp06YyXRf4v7o/tNo7IXnmz58PgUAAnU4HgUBQLj0+Zq1bt8Y777yDli1bYvDgwdiyZQuys7ML7JM/h6djx45QKpUWNcGK06pVK/b/jo6OcHZ2ZvMNb9y4gTNnzkAqlbI3c67ho0ePCj2nRCJBo0aN2J99fX3ZcyoUCjx//hydO3e2OKZz5864e/cuAODu3bto1aoVRCKRxWPLr7i22fL8EdtQ8ENsCn6OHDkCABaBSVmEh4fjwoULiImJwYYNGxATE4Pz588XOD+Px8Nnn30GANi8eXOZV30Xi8XQaDQ0Rk7I/7dkyRI28NHpdHZbXNgaHo+HEydO4MiRI2jevDnWrVuHpk2bIjEx0a7XeXXWKofDYYfZlUol+vbti+vXr1vcHjx4gLfffrtE52TsPNxUXNsq6vmrDSj4IdDr9WAYptCp648fP8bdu3fB5/Px3nvv2e26PB4PnTp1Qv/+/dGpU6dCc4569uyJtm3bQq1W4/vvvy/TNTkcDvh8PmQyGY2Tk1ovf46PVqstkANUHjgcDjp37ozFixfj2rVrEAgE+O2339j7b9y4AbVazf586dIlSKVS+Pn52eX6b7zxBm7fvo3AwEA0btzY4ubo6Fiqczo7O6Nu3bq4cOGCxfYLFy6gefPmAIBmzZrh5s2bFjNOL126VOK2Fff8EdtQ8EOKTXY2D3l17ty5UpaI4HA4mDt3LgDg559/xtOnT8t0PvNq7yqVyh7NI6RaspbcbC0J2p7i4+OxbNkyXL58GU+fPsX+/fuRkZGBZs2asfvodDqMGzcOd+7cQWxsLBYuXIjJkycXqAtWWpMmTUJWVhaGDx+Of/75B48ePcKxY8cwZsyYMg2Hz5kzBytWrMDevXtx7949fPHFF7h+/TqmTZsGABgxYgQ4HA7Gjx/PPraVK1eWqG22PH/ENpUe/GzYsAGBgYEQiUQICQnB33//XeT+a9asQdOmTSEWi+Hn54cZM2ZQ7ZYyYBgGKpWqyOKG5uma9hryKo3OnTuja9eu0Ov1Bd4wSsoc6CkUCns0jZBqyVxt/dUcH3MAVB55cc7Ozjh37hzCw8Px2muv4auvvsKqVavQq1cvdp933nkHTZo0wdtvv42hQ4fi/ffft2vNIXMPjdFoxHvvvYeWLVti+vTpcHV1LVOANXXqVMycOROzZs1Cy5YtcfToURw6dAhNmjQBAEilUvz+++/4999/0bZtW8ybN6/AEj7Ftc2W54/YhsNUYt//3r17MWrUKGzatAkhISFYs2YNYmJicO/ePbYeTH67du3C2LFjER0djU6dOuH+/fuIiIjAsGHDsHr1apuuqVAo4OLiArlcDmdnZ3s/pGpHr9cjMTERAoEAAoGgwP0vX75EmzZtwDAMrl69Ch8fn0poZZ6bN2+iV69e4HA4OHHiRJm+7eh0Omi1WgQEBFgkIBJSXWg0GiQmJqJBgwY15jUcEREBmUyGAwcOVHZTSBVV1Ou+JJ/vldrzs3r1aowfPx5jxoxB8+bNsWnTJkgkEkRHR1vdPy4uDp07d8aIESMQGBiI9957D8OHDy+2t4gUTq/XQ6/XF9rzc/r0aTAMg5YtW1Zq4APkzeDo06cPGIbBihUrEBcXhwMHDiAuLq7E31IFAgH0ej0VPSSEkFqo0oIfnU6HK1euIDQ09P8aw+UiNDQUFy9etHpMp06dcOXKFTbYefz4MWJjY4scjtFqtVAoFBY38n+KS3Y+efIkAFj8nirTnDlzwOVyceLECQwePBiTJk3C4MGD0blz5xIvgyGRSCCTyajoISGE1DKVFvy8fPkSRqOxQG+Cj48PUlNTrR4zYsQIREZG4q233oKDgwMaNWqEbt264csvvyz0OlFRUXBxcWFv9poxUFNotdpCx7n1ej3OnTsHIG8cviq4f/8+O2W1bdu2uH//Pg4dOoSgoCBMmDChRAGQSCSCVqul3h9Cqoht27bRkBepEJWe8FwSZ8+exbJly/Df//4XV69exf79+3H48OEiZyXMnTsXcrmcvZWkUFZtUFSy899//42cnBx4enqidevWFdyygswJmm+99RZ4PB6uXbuG27dvIzg4GNHR0QgNDcWSJUtKNAQmEokgk8lgMBjKseWEEEKqkkoLfjw9PcHj8ZCWlmaxPS0tDXXq1LF6zPz58/Hhhx/io48+QsuWLTFgwAAsW7YMUVFRBdaJMhMKhXB2dra4kTwGgwEajQaXL1+2mjtz+vRpAED37t3tNs20LOLj45GcnIzPPvsMw4YNAwB25heXy8XkyZPx9OlTxMfH23xOsVgMtVpNRQ9JtUX1qkhtYq/Xe6V9ogkEAgQHB+PUqVPsNpPJhFOnThUo+W2mUqkKfAibpy3TG0DJxcTEIDQ0FCNGjLCaO2Me8urevXtlNpNlLiUfFBSEadOmwcHBARcuXEBcXBy7Pf9+tuBwOBAIBMjOzi40gCakKjK/91HOGqlNzPXZiirPYovC1zOoADNnzsTo0aPRrl07tG/fHmvWrEFubi7GjBkDABg1ahTq1auHqKgoAEDfvn2xevVqtG3bFiEhIXj48CHmz5+Pvn37FlmkjxS0f/9+jBw5El27dsXGjRsRFBSEhIQErFu3DhMmTMDKlStx584dAMBbb71Vya3NYy5/kJCQgODgYIwYMQLbt2/HqlWr0LFjRyQkJFjsZyuJRAK5XI7c3Nwyr1tGSEXh8/mQSCTIyMiAg4NDleidJaS8mGvSpaenw9XVtcyf+ZVa5wcA1q9fj2+//Rapqalo06YN1q5di5CQEABAt27dEBgYiG3btgHIG6ZZunQpfvrpJ6SkpMDLywt9+/bF0qVLba48THV+8nJnGjdujKZNm+Lbb7+Fh4cHe5/JZMLYsWNx9epVZGZm4vXXX8fx48crsbX/x2g0onPnzggKCkJ0dDRSU1Px1ltvQavVYteuXdi6dSvu3buH8+fPl/gPIycnhy2cWdjMN0KqGp1Oh8TEROq1JLWGq6sr6tSpY/V9uiSf75Ue/FQ0Cn7yEse7d++OAwcOoFmzZpBKpRb3X758Gf369QMAfPLJJ/jqq68qo5lWxcbGYsKECQgNDcXkyZOxb98+/PTTT3BxcYFCoSiwKrytjEYjcnJy4O/vX+D5IKQqM5lMNPRFagUHB4civ9iW5PO9Uoe9SOV48eIFAKBBgwZWV3Jv2rQp+/8uXbpUWLtsER4ejs2bNyMyMpIN0ABALpdj+vTppV6Cg8fjgcPhQCaTwdHRkXp/SLXB5XJrTIVnQioKDRLXQr6+vgCAO3fuWE0aM8/ycnBwQPv27Su0bbYIDw/HhQsXEBMTgw0bNqBPnz4AgD///LNMie+Ojo5QKBS04CkhhNRwFPzUQl26dEFAQAB++OGHAj0cJpMJa9euBQC8+eabEIvFldHEYvF4PHTq1An9+/fH119/DbFYjGvXrlnMHiwpcy+YXC6n2YOEEFKDUfBTC/F4PERGRuLPP//E2LFjcfnyZSiVSly+fBljx45lZ029/fbbldxS23h5ebEzBFeuXFnm3h+5XA61Wm2v5hFCCKliKPippUJDQ7FmzRokJCSgX79+aNq0Kfr164eEhAQ2f6C6BD9AXmK2o6Mj/v333zLNTnNwcADDMJDL5XZsHSGEkKqEZnvVQgzD4PHjxwDyik3Gx8cjPT0d3t7e4PP5GDBgAFxdXXHz5s1qVT9p+fLlWLduHZo1a4bjx4+Xuu6JXq+HWq1GQEBAlR32I4QQYqkkn+/U81ML6fV6GAwG8Pl8i9yZTp064e+//wYAdOrUqVoFPgDw8ccfQyqV4u7duyVe4T0/BwcHmEwmyGQy+zWOEEJIlUHBTy2k1+thNBqtTnO/dOkSAKBDhw4V3awyc3Nzw/jx4wEAq1atKtECp6+i3B9CCKm5KPiphfR6PRiGKTDTy2AwsD0/1TH4AYDx48fD2dkZ9+/fx++//17q81DvDyGE1FwU/NRCOp3Oaj7MrVu3kJubCxcXF3aR0OrGxcUFEyZMAJDX+2MwGEp9LnPvD9X9IYSQmoWCn1pIpVJZLW5oHvJq3759tcv3ye+jjz6Cm5sbHj9+jP3795f6PPl7f2rZvABCCKnRKPipZQwGA/R6vdXg5uLFiwDsO+RlNBqh1Wqh1WrLlINTEk5OTpg0aRIA4LvvvivTukdSqZR6fwghpIah4KeW0ev10Ov1BXp+jEYjm+/TsWPHMl9Ho9EgKysLSqUSBoMBBoMBSqUS2dnZ0Gg0ZT5/cSIiIuDl5YWnT59i7969pT4Pn88HwzDU+0MIITUIBT+1jF6vh8lkKpDzc/fuXSgUCkilUrz++uulPr+5QKDBYECdOnUQGBiIBg0aoEGDBggMDISPjw8MBgNkMhlMJlNZH06hxGIxpkyZAgD4/vvvyxRwSaVSyGQy5Obm2qt5hBBCKhEFP7WMTqezumK5ecirffv2VqfA28JkMiE7OxsSiQR+fn7w9PSEWCwGn88Hn8+HWCyGp6cn/P394eTkBJlMVq5DYSNHjoSvry9evHiBnTt3lvo85npI2dnZ5RqwEUIIqRgU/NQyarW6yGTnkJCQUp3X3OPj4uKCunXrFlkZWSQSwdfXFx4eHpDL5eUWAIlEIkyfPh0AsG7dujLV7JFKpVAoFFAqlXZqHSGEkMpCwU8tYjQaodPpCvTsmEymMhc3lMvlcHR0RJ06dawGV6/i8/nw9vaGu7s75HJ5ufWoDB06FP7+/sjIyMC2bdtKfR4ulwsHBwdkZWVVWOI2IYSQ8kHBTy1inun1anDy8OFDyGQyiEQitGrVqsTnNfcm+fj42BT4mPF4PHh7e7NrsZRHQrGDgwNmzJgBANiwYQNycnJKfS5HR0colUooFAp7NY8QQkglsHvwQ8sBVF3mNb1eneZ++fJlAEDbtm0hEAhKdE6j0Qi1Wg0vL69SLQLK5/Ph4+MDsVhcpsCkKAMHDkSjRo2QnZ2NH3/8sdTn4XA4EIlEyMrKgl6vt2MLCSGEVCS7BT9arRarVq1CgwYN7HVKYmeFfWD/888/AIB27dqV+JwKhQJubm5wcXEpdbsEAgHq1KkDLpdbLsEzn8/HrFmzAACbN28u05IVEokEarWalr0ghJBqrETBj1arxdy5c9GuXTt06tQJBw4cAABs3boVDRo0wJo1a9ghBlL1qNVqqzO5zD0/JQ1+NBoNHBwc4OHhYXUGWUlIJBJ4e3tDq9WWS69K37590axZMygUCmzatKlM53J0dERWVhb1chJCSDVVouBnwYIF2LhxIwIDA5GUlITBgwdjwoQJ+O6777B69WokJSXh888/L6+2kjJgGMbqTK+srCw8fvwYABAcHFyi86lUKri7u0MkEtmljS4uLvDw8EBOTo7dE6C5XC5mz54NAPjxxx+Rnp5e6nMJhUIYjUZkZ2dT4UNCCKmGShT8xMTEYMeOHdi3bx+OHz8Oo9EIg8GAGzduYNiwYdV6PaiaTq/Xw2g0Fuj5Mff6NGnSBG5ubjafT6PRQCwWl2m461UcDgeenp5wcXEpl6TisLAwtG3bFmq1Gt99912ZzmUufEhT3wkhpPopUfDz7NkztnegRYsWEAqFmDFjRpmHPEj5Myc7Fxb8lGTIy9yL5O7uXqLZXbbg8Xjw8vKCg4OD3dfT4nA4mDdvHgBg165dbI9XSRiNRsTFxeGPP/7A5cuXkZ6eTlPfCSGkmilR8GM0Gi1mA/H5fEilUrs3itifXq8HwzAFAtXSBD/mXh8nJye7ttFMJBLB29sbOp3O7vk/HTt2xDvvvAODwYBvvvmmRMfGxsaic+fOGDx4MCZNmoTRo0eja9eu+Pnnn+3aRkIIIeWrROsYMAyDiIgICIVCAHkfghMnToSjo6PFfvv377dfC4ldaLXaAut56XQ63LhxA0DJgh+1Wo169eqVehkMWzg7O0OtVuPly5dwc3Oza+/i3Llzcfr0afz++++YOHEi2rRpU+wxsbGxmDBhAkJDQ7FhwwYEBQUhISEB33//PcaMGQOhUIhhw4bZrY2EEELKD4cpQcbmmDFjbNpv69atpW5QeVMoFGxRPWdn58puToVJSkqCwWCARCJht129ehV9+/aFq6srbt26ZVOAodFoYDKZEBAQYPchr1fp9Xo8e/YMOp3O7r1M06dPR0xMDDp16oRffvmlyMduNBrRuXNnBAUFITo62iKINJlM+PDDD/H48WM8evSoXANCQgghhSvJ53uJ3qmrclBDCldYZef8Q1629qyo1Wp4e3uXe+AD5FVn9vLyQnJyMrRaLdvjaA9z5szBoUOHEBcXhz///BPdunUrdN/4+HgkJydjw4YNBXrPuFwupk2bhgEDBuDo0aPo06eP3dpICCGkfJS5yOGzZ8/w7NmzUh+/YcMGBAYGQiQSISQkBH///XeR+8tkMkyaNAm+vr4QCoV47bXXEBsbW+rr1wZ6vR56vb7QZOc333zT5vPw+fxyy/WxRiqVwtPTE7m5uXadVl6vXj1EREQAAJYuXVrk1HrztPigoCCr9zdv3hxA3jIhOp3Obm0khBBSPkoV/JhMJkRGRsLFxQUBAQEICAiAq6srlixZUqL6LHv37sXMmTOxcOFCXL16Fa1bt0ZYWFihNVh0Oh3effddJCUlYd++fbh37x62bNmCevXqleZh1BrmZOf8vRYMw5Q42VmlUkEqldqtro+t3N3d4eTkZPfp75MnT4azszPu3LlTZJ6at7c3ACAhIcHq/ebtLi4uyMzMpNo/hBBSxZUq+Jk3bx7Wr1+P5cuX49q1a7h27RqWLVuGdevWYf78+TafZ/Xq1Rg/fjzGjBmD5s2bY9OmTZBIJIiOjra6f3R0NLKysnDgwAF07twZgYGB6Nq1K1q3bl2ah1FraLXaAsNaz549Q1paGvh8vk3Pn8lkgslksmtdH1vxeDx4enqCw+FAq9Xa7bzu7u6YPHkyACAqKqrQqfUhISHw8/PDunXrCgT3JpMJ69evh7+/P7p164bs7OxyW6OMEEKIfZQq+Nm+fTt+/PFHfPLJJ2jVqhVatWqFTz/9FFu2bMG2bdtsOodOp8OVK1cQGhr6f43hchEaGoqLFy9aPebQoUPo2LEjJk2aBB8fH7Ro0QLLli0rss6KVquFQqGwuNU2KpWq0Hyfli1b2rQgqUajgUgkskiYrkiOjo7lMvw1btw4+Pv7IzU1FRs3brS6D4/Hw4IFC3Dy5EmMHTsWly9fhlKpxOXLlzF27FicPHkS8+fPh1AohIODAzIyMmj4ixBCqrBSBT9ZWVlW8x+CgoKQlZVl0zlevnwJo9EIHx8fi+0+Pj5ITU21eszjx4+xb98+GI1GxMbGYv78+Vi1ahW+/vrrQq8TFRUFFxcX9ubn52dT+2oKc7JzYfk+ti5podFo4OrqWiDhtyK5ubnZffhLJBLhq6++AgD897//RUpKitX9wsPDsXnzZiQkJKBfv35o2rQp+vXrh3v37mHz5s0IDw8HkBekaTQaGv4ihJAqrFSfZK1bt8b69esLbF+/fn25DkGZTCZ4e3tj8+bNCA4OxtChQzFv3rwiF6qcO3cu5HI5e0tOTi639lVF5mTnV3t+SrKSu/n4V+s5VTR7DX+ZqzQfOHAAcXFxCAsLQ4cOHaDRaLB8+fJCjwsPD8eFCxcQExODDRs2ICYmBufPn2cDHzMnJydkZWVBLpeXuo2EEELKT6mKknzzzTfo3bs3Tp48iY4dOwIALl68iOTkZJtnXnl6eoLH4yEtLc1ie1paGurUqWP1GF9fXzg4OFisIdasWTOkpqZCp9NZVJ82EwqFdp0iXd2Y1/TK32OjVCpx9+5dALbN9FKr1XB0dKzwRGdrHB0d4eHhgbS0NAgEghIXP4yNjUVkZKRFEOzn54fRo0cjPj4e+/fvR0RERKE9YjweD506dSryGnw+HyKRCBkZGRAKhTYNKxJCCKk4per56dq1K+7fv48BAwZAJpNBJpNh4MCBuHfvHrp06WLTOQQCAYKDg3Hq1Cl2m8lkwqlTp9iA6lWdO3fGw4cPLZJO79+/D19fX6uBD8nLrXo1QLh27RpMJhPq169faKBpxjAMDAZDpSQ6F8bNzQ1SqbTEi4qaqzQHBQXh0KFDuH//Pg4dOoSgoCAsXbqUDWoWLlxY5lXlxWIx9Ho9MjIyaO0vQgipYkpdjrZu3bpYunRpmS4+c+ZMjB49Gu3atUP79u2xZs0a5ObmspWkR40ahXr16iEqKgoA8Mknn2D9+vWYNm0apkyZggcPHmDZsmWYOnVqmdpRkxWV7GzLkJe5uGBV6r3g8/nw9PTE06dPrQ7pWWM0GhEZGYnQ0FCLKs3BwcGIjo7G2LFjcefOHUgkEly7dg0HDhzAwIEDy9ROZ2dnyGQyCIVCeHt70wLAhBBSRZQ6+MnOzsb//vc/dvikefPmGDNmDNzd3W0+x9ChQ5GRkYEFCxYgNTUVbdq0wdGjR9kk6KdPn1oM1/j5+eHYsWOYMWMGWrVqhXr16mHatGn4/PPPS/swajSDwQCdTldo8GPLkJdGoymX1dvLSiqVwsPDAxkZGTat/VVclebJkyejX79+GD58OHbv3o2lS5ciLCysTHlOXC4XTk5OyMzMhEgkqlK9Z4QQUpuVatjr3LlzCAwMxNq1a5GdnY3s7GysXbsWDRo0wLlz50p0rsmTJ+PJkyfQarWIj49HSEgIe9/Zs2cLTJ3v2LEjLl26BI1Gg0ePHuHLL7+0yAEi/0ev1xcIfkwmE65cuQKg+J4fhmHAMAykUmm5trO03N3dIRaLC63Pk19xVZrN29u3b89Off/uu+/K3EYHBwcIhUKkpaXZ1E5CCCHlr1TBz6RJkzB06FAkJiZi//792L9/Px4/foxhw4Zh0qRJ9m4jKSW9Xg+TyWTR03H//n3k5ORAIpEUGgiYmWv7VKUhr/zMa3/pdDoYDIYi97W1SnP9+vXZ0glbtmwpdP+SEIvFMBqNSEtLo/o/hBBSBZQq+Hn48CFmzZpl0ePC4/Ewc+ZMPHz40G6NI2Wj1WoLDPGYp7i/8cYbxa5ArtVq4eTkVKV71pycnODm5lZsVWVbqzSHhITgnXfeQa9evWAwGPDll1/apV6Ps7MzcnNzkZ6eTgnQhBBSyUoV/Lzxxhtsrk9+d+/epaUmqpDc3NxSJzubA4TKquhsKw6HAw8PDwgEAqjV6kL3s7VKsznQW7x4McRiMeLj4xETE2OXdrq6ukImk+Hly5dUAJEQQipRqRKep06dimnTpuHhw4fo0KEDAODSpUvYsGEDli9fjps3b7L7tmrVyj4tJSVSWHFDW4OfqjjLqzBCoRCenp5ISUmBUCgstAq1uUpzZGQk+vXrx2739/e3qNIM5K36PnPmTCxduhRff/013n33Xbi5uZWpnVwuF87Oznj58iV4PB48PDxoBhghhFQCDlOKr6DFLXHA4XDAMAw4HE6V6+JXKBRwcXGBXC6Hs7NzZTen3KhUKiQmJsLV1ZX9gM3IyECbNm3A4XBw+/btImcfyWQyeHp6srkyVZ3JZEJKSgpycnLg6upa5L5GoxHx8fFIT0+Ht7c3QkJCrA7t6XQ6hIWF4f79+xg5ciS++eYbu7RVq9VCrVbD19e3zAEVIYSQPCX5fC9Vz09iYmKpGkYqjjmxNn/PgnmWV9OmTYsMfMyzvKr6kFd+XC4XHh4eyM3NZXutCmNLlWYgrxBnVFQU/vOf/2Dnzp3o168fOnfuXOa2CoVCMAyDFy9esMNhhBBCKk6pgp+AgAB7t4PYmUajKdBDZ+tiplV9lldhJBIJPD09kZqaWqqlL6zp0KEDPvzwQ/z000+YM2cOTp48aZegUCQSWQRAVAOIEEIqTokSnj/99FOLJQV2796N3Nxc9meZTFZgkUdS8RiGgUqlKrDkh63FDbVaLaRSaZWe5VUYV1dXODo6lnjpi6LMmzcPdevWxZMnT+w29AXkTYEXCAR4/vw5ZDKZ3c5LCCGkaCUKfn744QeLQm0ff/yxxcKkWq0Wx44ds1/rSKnodLoCyc5arZZNRC8q2ZlhGJhMpmo15JWfeekLo9EIvV5vl3M6OTlhxYoVAIAff/yRDSLtIX8AlJ2dTbPACCGkApQo+Hn1jZneqKsmc/CTv47Pv//+C61WCw8PDwQGBhZ5rFAorBIruJeWVCqFu7t7sbV/SqJHjx4YNGgQGIbB7NmzodFo7HZusVgMoVCIFy9eIDMzs8yLqhJCCClaqer8kKrN2kru+Ye8isqF0Wq1kEgkVW4tr5LgcDglWvrCVosWLYKXlxcePHiAb7/91m7nBfICIIlEgtTUVKSlpRVbsZoQQkjpUfBTA5VlJXeDwVBl1/IqCYFAAE9PT2g0GruVW3Bzc2Nzfn744QecP3/eLuc1EwgEcHZ2RmZmJp4/f27X3iVCCCH/p8SzvRYsWMDmg+h0OixdupSdqUILN1Y+g8EArVZrEfwwDGNT8GPOE6rOQ175OTs7w83NDXK53G7Tyd977z2MHDkSO3fuxLRp03Dy5Em71urh8/lwdXWFQqGATqeDl5cXnJ2dqRgiIYTYUYmKHHbr1s2mN+EzZ86UqVHlqaYXOVSpVEhKSoKzszM71f3Jkyfo1KkTBAIB7t69W2hwo1QqIRKJ4OfnV2M+bDUaDZKTk8Hlcu02dV+lUiEsLAyPHz9Gnz59sGnTpnJ5vlQqFXQ6Hdzd3eHu7l5g9h4hhJD/U25FDs+ePVtgmzl2qikfltWdTqcDwzAWNX7Mi5m2aNGiyF4dc09DTfpdikQieHh44Pnz50UufVESEokE69evx/vvv48//vgDMTExGDJkiB1aW/A6AoEAL1++RG5uLjw9PeHk5GSXx0AIIbVZqd9F//e//7EfpiKRCC1atMCPP/5oz7aRUlCr1QXq8/z9998A8lY2L4zRaASPx6sxQ175ubq6wsXFxa6zv1q3bo1Zs2YBAL766is8fPjQbufOj8/nw93dHQzD4NmzZ0hOTkZOTg7NtCSEkDIoVfCzYMECTJs2DX379kVMTAxiYmLQt29fzJgxAwsWLLB3G4mNTCaT1WRnc89PUcUNzVPci1oWoroyL33B5XKh1Wrtdt5JkyahY8eOyM3Nxfjx4y0KftqbRCKBi4sL1Go1kpOT8ezZM+Tm5lIQRAghpVCqhU29vLywdu1aDB8+3GL77t27MWXKFLx8+dJuDbS3mpzzo9FokJSUBEdHR7b3JysrCy1btgQA3Lx5Ex4eHlaPlclk8PLygpeXV4W1t6K9fPkSqampcHNzs9vQXkZGBsLCwpCWlob+/ftj/fr15T5saDQaoVQqwTAM3Nzc4ObmViN77AghpCRK8vleqp4fvV5vddZQcHAw1SepRDqdDgaDwWLYyzzLq3HjxoUGPuaFTKvbWl4l5ebmBqlUatfhLy8vL2zatAk8Hg8HDhzA9u3b7XbuwvB4PLi4uEAqlSIrKwtPnz5FZmam3ab0E0JITVeq4OfDDz/Exo0bC2zfvHkzRo4cWeZGkdKxtpipecirffv2hR6n0+kgEAhqfO8Bj8dje7bsOfzVvn17fPXVVwDyCiFeuXLFbucuCp/Ph5ubG/h8Pl68eIHk5GS7rmlGCCE1ValWdQfyEp6PHz+ODh06AADi4+Px9OlTjBo1CjNnzmT3W716ddlbSYrFMAyUSmWB6dDmZOei8n20Wi2cnJwslsOoqRwdHeHh4YG0tDS7rfwOAOPHj8fly5dx+PBhfPTRR/jjjz9Qr149u5y7OCKRCEKhEEqlEsnJyXB3d4eHh0et+H0SQkhplOrd8datW3jjjTcAAI8ePQIAeHp6wtPTE7du3WL3q0lTpqs6nU4HnU5nsSCpRqNhFzMtquenplR1tpWbmxtyc3ORk5Njt7wvDoeD1atX4/Hjx7h79y5Gjx6NAwcOVNjzyuFw4OTkBJ1Oh4yMDKhUKnh5edWq3yshhNiqVMFPVS5iWFtptVoYDAaLb/s3b96ETqeDt7c3AgICrB5nPqYmzvIqDJ/Ph5eXF5KTk9khP3uQSqXYvn07evfujbt37+KTTz7B1q1bK7QHRiAQwM3Nje0F8vT0hLu7e4HyB4QQUptRtbQawlq+T/4hr8J64bRaLTtsUpuYh7/Ms6bspV69eti2bRtEIhFOnz6NxYsX2+3ctjL3AolEIqSlpdE6YYQQ8goKfmqA4vJ9ikt2lkqltXKI0jz7S6FQ2PW8bdq0wbp16wAA0dHR+O9//2vX89tKKBTC1dUVOTk5SE5Ohlwup7pAhBACCn5qBHO+T/7gx2QysdPcCwt+GIYBh8Op8VPcC2Me/uJwOHad/QUA4eHhmD9/PgBg6dKl+Pnnn+16fltxuVy4urqCw+EgJSUF6enpNCWeEFLrUfBTA2i1WhiNRovckjt37kAul8PR0RHNmzcv9DiBQFDrhrzyc3R0hKenJ3Jzc2Eymex67okTJ2Ly5MkAgC+++AIHDhyw6/lLQiKRwNHRERkZGXj+/Lndgz1CCKlOKPipAdRqdYFhq7i4OAB563kVlnCr0+kgFotr/ZRoNzc3uLi42H34C8gLekaNGgWGYTB16lQcPHjQ7tewlYODA1xdXaFQKPDs2TOqCUQIqbWqRPCzYcMGBAYGQiQSISQkhM1VKc6ePXvA4XDQv3//8m1gFWYymaBUKgv03ly4cAEA0Llz50KPrW1T3AtjLn7o4OAAtVpt13NzOBwsXboUgwYNgtFoxOTJk/Hrr7/a9RolweVy4ebmBqPRiGfPniErK4vygAghtU6lBz979+7FzJkzsXDhQly9ehWtW7dGWFgY0tPTizwuKSkJs2fPRpcuXSqopVWTVqstkO9jMBhw6dIlAIUHP7VxintRRCIRvLy8oNFo7L5EC5fLxerVqzF8+HCYTCZMmzYNu3btsus1SkoqlUIgEOD58+eUB0QIqXUqPfhZvXo1xo8fjzFjxqB58+bYtGkTJBIJoqOjCz3GaDRi5MiRWLx4MRo2bFiBra16zPk++eu43Lx5E0qlEq6uroXm+9TkVdxLy8XFBe7u7lAoFHbvDeHxePjmm2/YIbA5c+bgu+++q9ReF5FIBGdnZzYPSKfTVVpbCCGkIlVq8KPT6XDlyhWEhoay27hcLkJDQ3Hx4sVCj4uMjIS3tzfGjRtX7DW0Wi0UCoXFrSbJzc0tkLNjHvLq2LFjocXtzEta1MYp7oXhcDjw9PSEo6NjueTDcLlcLFu2jE2CXrlyJebMmQO9Xm/3a9mKz+ezeUApKSlQqVSV1hZCCKkolRr8vHz5EkajET4+PhbbfXx8kJqaavWY8+fP43//+x+2bNli0zWioqLg4uLC3vz8/Mrc7qrCYDBArVYXqO9TXL6Pubehpi9kWhoODg7w8vICwzDlUhiQw+Fg7ty5iIqKApfLxe7duzFixAi8fPnS7teylXk6vEajQUpKSo37gkAIIa+q9GGvksjJycGHH36ILVu2wNPT06Zj5s6dC7lczt6Sk5PLuZUVx5zvk3/oSqvVsiu5Fxb81JZV3EtLKpXCy8sLKpWq3HJhRo0ahf/973+QSCSIi4tDz549ce3atXK5li04HA5cXFwAACkpKcjMzKREaEJIjVWpwY+npyd4PB7S0tIstqelpaFOnToF9n/06BGSkpLQt29f8Pl88Pl87NixA4cOHQKfz2cXWc1PKBTC2dnZ4lZTqNVqtlCh2dWrV6HRaODl5YUmTZpYPc68AGptn+JeFDc3N7i5uZVrVeT33nsPhw8fRsOGDfHixQsMHDgQmzdvtnu9oZJwdHSEUChEamoq0tPTK7UthBBSXio1+BEIBAgODsapU6fYbSaTCadOnULHjh0L7B8UFIR///0X169fZ2/vv/8+unfvjuvXr9eoIa3iFLakRf4hr8LyefR6PRwdHcu9jdUZl8uFl5cXJBJJudbDee211xAbG4tevXpBp9Nh8eLFGDp0KFJSUsrtmsURiUSQSqXIyMhAamqq3We/EUJIZav0Ya+ZM2diy5Yt2L59O7sSdm5uLsaMGQMgb3hg7ty5APLelFu0aGFxc3V1hZOTE1q0aGG31bmrA51OB41GU2h9n06dOlk9zjwzjGZ5FU8gELD5aPau/5Ofk5MTtmzZguXLl0MsFiMuLg49evTAli1bKi3wcHBwgIuLC7KysqgiNCGkxqn04Gfo0KFYuXIlFixYgDZt2uD69es4evQo+6Hz9OlTvHjxopJbWfWY69HkH7qSy+W4cuUKABRa/4imuJeMo6MjvL29oVaryzUQ4XA4+PDDD3H8+HEEBwdDqVRi0aJF6NmzJ1utu6LxeDy4ubkhJyeHZoIRQmoUDlPLshoVCgVcXFwgl8urdf5PSkoKcnJyLB7DH3/8gY8//hiNGjXCuXPnrB6XnZ0NLy8veHt7V1RTqz2GYZCeno6MjAy4urqCyy3f7wwmkwm7d+/GsmXLIJPJAABvvfUW5syZg3bt2pXrta1hGAYKhQIODg7w8fGBk5NThbeBEEKKU5LP90rv+SElp9froVKpCvTenDlzBgDQvXt3q8cxDAOGYWrtKu6lxeFw4OHhwf5RlTcul4uRI0fir7/+wujRo+Hg4IDz58+jX79+CAsLQ1RUFC5cuFBhVZnNM8FMJhNSUlIgk8loJhghpFqj4Kca0mg07IrsZgzD4OzZswCAHj16WD1Or9fTFPdS4vP58Pb2hkgkqrA6OO7u7li2bBn++usvdhjz1q1bWL9+PYYMGYK2bdvi999/r5C2AHklABwcHGgqPCGk2qPgpxpSqVTgcrkWs7nu3r2L1NRUiMVihISEWD1Oq9VCLBbDwcGhoppaowiFQtSpUwdcLrfU+S9GoxFxcXE4cOAA4uLibOq9+ffff3H+/Hl06dIF77//Pttzl5mZiYkTJ2LWrFkVlo8jFoshkUjYqfC0JhghpDqi4KeaMRqNVldxNw95derUqdCeHb1eT6u4l5GjoyN8fHyg0+lKPAMqNjYWnTt3xuDBgzFp0iQMHjwYnTt3RmxsbKHHGI1GREZGIjQ0FLt27cLGjRvxzz//YM6cOXB3dwcA7NmzB+3bt0d0dHSFrM8lFArh5OSEjIwMpKWl0VR4Qki1Q8FPNWMe8ios+ClsyMtkMoHL5dKQlx24uLjAx8cHubm5Nq/LFRsbiwkTJiAoKAiHDh3C/fv3cejQIQQFBWHChAmFBkDx8fFITk7GlClT2ERrNzc3TJ8+HX///TcmTpwIIC+Rff78+ejevTsOHTpU7kNS+afCv3jxghZFJYRUKxT8VDMqlQoMw1jMOMrJyWGXtCgs2dkcMNEUd/twd3eHl5cXcnJyih36yd97Ex0djeDgYDg6OiI4OBjR0dEIDQ3FkiVLrJ4nPT0dQF6Bz1eJxWLMmDEDQF7JCC8vLyQlJeGTTz5Bnz592NdEeeHxeBaLopZnLSRCCLEnCn6qEZPJBIVCUaD35q+//oLBYEDDhg0REBBg9VitVgupVFru07RrCw6HAy8vL3h4eEAulxe5DIS13hszLpeLyZMn4+nTp4iPjy9wrLkkQUJCgtVzm7cPGjQIFy5cwKxZsyCRSHD9+nX0798fM2fORGZmZmkfZrG4XC6cnJxw7tw5bNq0CUeOHKE8IEJIlUefhNWIWq22WtXZvDxIYb0+QF7gJJFIyrV9tQ2Xy4W3tzdcXV0hk8kKDYCK6r3Jv928X34hISHw8/PDunXrCpzfZDJh/fr18Pf3R0hICBwdHTFz5kxcuHABw4YNAwDs3bsXb7/9Nnbs2FEuQUlsbCzeeustREREYObMmQgPD0fDhg2xf/9+u1+LEELshYKfasQ8oyd/74HBYMDx48cB5C2UaY15FXca8rI/Ho8HHx8ftgaQtVwbW3tvrBWe5PF4WLBgAU6ePImxY8fi8uXLUCqVuHz5MsaOHYuTJ09i/vz54PF4FtdbtWoVDhw4gObNm0Mmk2Hu3Ll4//33cefOHXs8bADW85hiYmLQsGFDDBo0CL/++qvdrkUIIfZEFZ6rCaPRiKSkJACwKFIYFxeHwYMHw9XVFTdu3LC6UrtSqYRIJIK/v39FNbfW0ev1eP78OXJycuDm5mZRhsBoNKJz584ICgpCdHS0RfBqMpkwduxY3Lt3D+fPn7cIYvKLjY1FZGQkkpOT2W3+/v6YP38+wsPDC22XwWDA9u3b8e233yInJwd8Ph9Tp07FlClTyrQWXlGPSa1W46OPPsLjx4/x4MGDWrXmHiGk8lCF5xqosCGvo0ePAsjr9bEW+AB5PT+0JEH5cnBwgK+vL5ycnJCdnW3RA1Sa3ptXhYeH48KFC4iJicGGDRsQExOD8+fPFxn4AHnFGceNG4c///wTPXv2hMFgwOrVqxEeHo6bN2+W+vEWlcckFosxZcoUPH36FAcPHrR5RhwhhFQUCn6qidzcXACWQ14Mw+DIkSMAgF69elk9zmg00hT3CiIQCNgA6NUcoPDwcGzevBkJCQno168fmjZtin79+uHevXvYvHlzsUEMkBdEderUCf3790enTp2KDJZe5ePjgx9//BH//e9/4e7ujrt376JPnz6IioqCRqMp8WMtLo+pRYsWAIDExERaFZ4QUuVQ8FMNGAwG5OTkFFiT6+bNm3j+/DkkEgnefvttq8fSKu4VSyAQoG7dunB2drYaAJWm98ZeOBwO+vXrh7Nnz+L999+H0WjE+vXrER4ejtu3b5foXLbmMTVo0AC5ubl49uwZG8ATQkhlo+CnGlCpVFaHvMyF8Xr06FFoz45Wq4WzszNNca9A5iEwNzc3yGQyiwrIZem9sRcPDw9s3LgRP/74I7y8vHDv3j307t0bGzdutHlGmK2z0Dp06ABXV1fo9XqkpKRUyMKwhBBSHPpErAbMiar5k2iB/8v3KWzIi1Zxrzx8Ph916tSBh4cHFApFlayA3KtXL5w6dQphYWHQ6/X4+uuvMXToUDx79qzYY0uax2QOwFNSUvDy5UtaFJUQUqlotlcVp9Fo8OTJE4hEIosFSR88eIBu3bpBIBDg5s2bVhOaNRoNTCYTAgMDC02GJuXLZDIhMzMT6enpEIlEVTIQZRgGe/bswYIFC6BSqeDk5IRly5ZhwIABBQLuV5V0FppWq4VKpYKHhwe8vLwqpeeLEFIzleTznT4Rqzjz+lGvBjeHDh0CALz11luFzuTSarVwdXWlwKcScblceHp6gs/ns4uAVrWZdxwOB8OHD0fHjh0xZcoUXL16FVOmTMGJEyewfPlyuLi4FHpseHg4wsLCEB8fj/T0dHh7eyMkJKTQoEYoFILH4+Hly5fQ6/Xw9vamfDRCSIWjYa8qzGg0QiaTFcjnYRiGraA7cODAIo93dHQs1zaS4nE4HLi5uaF+/frg8/nIzs4ucjmMyhIYGIjffvsNs2fPBo/Hw6FDh/Dee+/h8uXLRR5X0jwmPp/PrglGidCEkMpAwU8VZk50fjX4uXr1KpKSkiCRSBAWFmb1WHNVZ5riXnVIpVLUr1+fnQlWFad/8/l8zJgxAwcPHkRAQACePXuGgQMHYu3atXZdHoPL5cLNzY1NhJbJZJQHRAipMBT8VGEKhQJcLrfATC1zr0/Pnj0hkUhgNBoRFxeHAwcOIC4uDkajEVqtFmKxmKrrVjFCoRB169aFj48PNBoNcnJyquSHftu2bXH06FH0798fRqMRK1aswPDhw5GammrX6zg7O4PH4+H58+fIyMigRVEJIRWCEp6rKLVajSdPnkAsFlskOuv1erRt2xbZ2dnYuXMnVCpVgYRTPz8/zJw5E2PGjCkyX4NULqVSiYyMDOTm5sLJycni91xVMAyDX375BfPmzYNarYa7uzu+++47hIaG2vU6Op0OSqUSrq6u8Pb2pqCdEFJitLxFDaBUKmEwGAp8IJ49exbZ2dnw8vJCTk5OgYUlDx06hKZNm2LmzJnsVHhSNZmHwby8vKBSqaBQKKpcLhCHw8HQoUNx9OhRvP7668jKysLo0aOxcOFCuw7bCQQCuLq6QiaTITk5mfKACCHlinp+qiC9Xo+kpCTweLwCOTuffPIJDh06hLFjx+LEiRNWF5aUy+WYOnUqHj16hAcPHtB04mpAqVQiMzMTOTk5EIlEkEgkld2kArRaLZYuXYr//e9/APKWsPjvf/+LRo0a2e0aDMNAqVQCyKsi7erqWux0e0IIAajnp9pTKpXQarUFAh+FQoHjx48DAF577bVCF5Y0Go2YM2cOEhMT8ddff1VYu0npmXuB6tevDw6Hg6ysLKjV6spulgWhUIjIyEhs27YNbm5uuHXrFnr27IlffvnFbnlLHA4HTk5O4PP5SElJQWpqKi2MSgixOwp+qhiDwYCsrCyrs7R+++03aDQavPbaa5BKpQAKLiyp0+ng4OCAN954AwDw4sWL8m80sQsejwdXV1f4+/ujTp06YBiGDYKqUgftu+++ixMnTqBjx45QqVSYMWMGpk6dipycHLtdQywWw9nZGZmZmUhJSYFKpbLbuQkhhIKfKkapVEKtVheoBMwwDH7++WcAwMiRI+Hj4wOg4MKSWq0WEokEDx48AAD4+vpWQKuJPTk4OMDT0xP+/v7s7y87Oxs5OTkW64RVJl9fX+zduxefffYZeDwe9u/fj549e+L69et2uwafz4ebmxvUajWSk5OrbH0kQkj1Q8FPFWI0GpGdnQ2hUFggz+HGjRu4c+cOhEIh/vOf/xS6sKRer4dUKkVUVBQaNGiALl26VPTDIHYiEAjg4eGBgIAA+Pv7QyKRQKVSISsrC7m5uZU+LZzH42HatGn49ddfUa9ePSQlJaFfv35Yv3693drG4XDg4uJiMQxWFddJI4RULxT8VCFKpRK5ublWk1137twJAOjduzfc3NysLiyZmZmJ27dv48MPP8Qff/yBlStXUrJzDcDn8+Hs7Iz69esjMDAQ9erVg4ODA5RKJbKzsyt9WOzNN9/E8ePH0bt3bxgMBkRFRWHo0KF4/vy53a4hFovh4uKC7OxsJCcn23WIjRBS+1SJ4GfDhg0IDAyESCRCSEgI/v7770L33bJlC7p06QI3Nze4ubkhNDS0yP2rC6PRiKysLKu9PjKZDL/99huAvCEvs/DwcGzevBkJCQno168fWrVqhWHDhuHOnTvYt29fkUtfkOqHw+FAJBLBzc0NAQEBCAgIYIc/K3tYzNXVFT/88ANWrVoFiUSCixcv4t1338Xhw4ftdg0ejwc3NzcYjUYkJycjPT29ygwDEkKql0oPfvbu3YuZM2di4cKFuHr1Klq3bo2wsDCkp6db3f/s2bMYPnw4zpw5g4sXL8LPzw/vvfceUlJSKrjl9pWTk1Nor8+ePXugVqvRrFkzhISEWNwXHh6OCxcu4JdffsGKFStw+PBhPHjwgAKfGo7D4UAikcDT09NiWEypVEImk1XK0BCHw8GwYcNw7NgxtG7dGjKZDBMmTMDs2bPtWrdHKpVCIpEgPT2d1gYjhJRKpdf5CQkJwZtvvon169cDAEwmE/z8/DBlyhR88cUXxR5vNBrh5uaG9evXY9SoUcXuXxXr/Oj1ejx9+hQMwxQIfgwGAzp37oxnz55h5cqVGD58uNVzmKdFBwQE0CrutRTDMFCr1VAoFFAoFNDr9RCLxZWyvptOp8OqVauwYcMGMAyDBg0aYMOGDWjdurXdrsEwDDv85enpyQ4HE0Jqp2pT50en0+HKlSsWpfK5XC5CQ0Nx8eJFm86hUqmg1+vh7u5eXs0sd3K53OoMLwA4fvw4nj17Bnd3d/Tv37/Qc2g0GrY+CqmdzL1BderUQUBAALy9vdnh1IquGSQQCDB37lzs3bsXderUQWJiIt5//31s2LDBbjO2OBwOnJ2dIRQKkZqaSpWhCSE2q9Tg5+XLlzAajWzegpmPj4/NCyh+/vnnqFu3bqFrDWm1WvabsPlWlWg0GmRlZUEikVitZLt582YAwAcffGA1OALyer84HA5b+4cQoVAILy8vBAQEwNfX16JmUEXq3LkzTp48ifDwcBgMBixbtgxDhw616zC1UCiEm5sbNBoNnj59SrlAhJBiVXrOT1ksX74ce/bswW+//VZo135UVBRcXFzYm5+fXwW3snDmDyS9Xm+1/fHx8fjnn38gEAgQERFR6Hk0Gg0kEkmhwRGpvfJPl88fBGk0mgprg5ubGzZv3oyVK1dCLBYjLi4OPXr0wJ49e+xaGdrZ2RlisRjp6el48uQJFApFlSoOSQipOio1+PH09ASPx0NaWprF9rS0NNSpU6fIY1euXInly5fj+PHjaNWqVaH7zZ07F3K5nL3lX/28suXk5EAmk8HJycnq/evWrQMADB06tEDvWH5arZbWQCJFcnBwgIeHB1s92jwcZs/FSYvC4XAwfPhwHD9+HMHBwVAqlZg1axZGjRplcy+vLQQCgcWMsBcvXlRooEcIqR4qNfgRCAQIDg7GqVOn2G0mkwmnTp1Cx44dCz3um2++wZIlS3D06FG0a9euyGsIhUI4Oztb3KoCvV6PjIwM8Pl8q3k6//77L86cOQMul4tPPvmk0PNotVoIhcIquRAmqXoEAgFbPdrb2xs6nQ7Z2dkVtn5Ww4YN8dtvv+Grr76CQCDA6dOn8c4772D//v127QWSSqVwcnJCdnY2nj59ipcvX9JQGCGEVenDXjNnzsSWLVuwfft23L17F5988glyc3MxZswYAMCoUaMwd+5cdv8VK1Zg/vz5iI6ORmBgIFJTU5GamsquBF0d5M+/cHR0tLrPmjVrAAD9+/dHQEBAoedSq9VwcnKCQCAoj6aSGkooFMLb2xsBAQFwd3eHSqWCXC6vkKrRPB4Pn3zyCY4ePYpWrVpBJpNhypQpGD9+PF6+fGm365iXx+Dz+UhNTcXTp08hl8tpiQxCSOUHP0OHDsXKlSuxYMECtGnTBtevX8fRo0fZYZ6nT59aLM65ceNG6HQ6DBo0CL6+vuxt5cqVlfUQSiwnJwcvX76Ek5OT1aEq83PA5XIxderUQs9j/qAqbNiMkOKIRCLUqVMH/v7+kEqlUCgUUCqVFZIr07RpUxw6dAizZ88Gn8/HkSNH0L17dxw8eNCu1zcXhjQPhaWkpCA3N5fygQipxSq9zk9Fq+w6P1qtFs+ePYPJZGJ7fYxGI+Lj45Geng5vb2+sW7cO586dw+DBg9keIGuUSiWEQiH8/f0p34eUmclkglKpRGZmJnJzcyEWiyssif7WrVuYPn067t69CwDo0aMHoqKiUL9+fbtex2QyIScnBwzDwNXVFW5ubjRRgJAaoiSf7xT8VCCTyYTnz59DLpfDzc0NABAbG4vIyMgCidg8Hg/nz5+Hv7+/1XMxDIPs7Gz4+fnBxcWl3NtOag+j0QiZTIasrCzodDo4OjpWyLCqTqfDhg0bsHbtWuh0OojFYsyZMwfjxo2ze/0qg8EApVIJLpcLV1dXuLq6VkoxSEKI/VSbIoe1CcMwyMzMhEwmY4OV2NhYTJgwAUFBQTh06BDu3r2Lhg0bAsj7ALp161ah59NoNBCLxYXmDBFSWjwej50Z5unpCY1GUyH5QAKBADNmzMCJEyfQoUMHqNVqREZGok+fPvj333/tei0+n88GPJmZmXjy5AnS0tJoZhghtQT1/FQQmUyG58+fQyKRQCAQwGg0onPnzggKCkJ0dDS4XC52796N2bNnw8nJCW+88QYSExNx/vx5qyX7s7Ky4OvrCw8Pjwp7DKR2ys3NRVZWFuRyOTuzsLyHWU0mE/bu3YslS5ZALpeDy+Vi3LhxmDVrVrnkuOl0OuTm5oLP57M1wWg4jJDqhXp+qhilUom0tDQIhUJ2+CA+Ph7JycmYMmUKuFwucnJysGLFCgDAjBkzMHPmTDx9+hTx8fEFzqfVaiEQCKiiM6kQjo6OqFevHvz8/MDlcpGdnV3uPSRcLhfDhw/Hn3/+if79+8NkMmHLli3o0qUL9u7da/cZW+b6QEKhkO0Jev78OSVGE1JDUfBTzlQqFVJTU8HhcCy+SZpXrQ8KCgKQN4U/IyMDDRo0wJgxY9jt1la3V6lUcHFxgVAorIBHQEheMOLi4oKAgADUqVMHBoMBMpms3GvneHl5YcOGDdi5cycaNmyIjIwMzJw5E++//z6uX79u9+uZgyCxWAyZTIYnT54gOTkZCoWiQsoAEEIqBgU/5UitVuPFixcwGAwFemm8vb0BAAkJCbh69Sq2bdsGAFi2bBkEAgESEhIs9jPT6/Xg8XhVplgjqV34fD5bJNHV1RVKpZKdPVWeunXrhlOnTmH+/PlwdHTEtWvX0KdPH8yePdvqF4SycnBwgKurK5ycnKBWq5GcnIykpCRkZmZWWFVsQkj5oZyfcqJSqfDixQvodDqrs7HMOT+vvfYaUlJSkJCQgEGDBuH777+HyWTC2LFjce/evQI5PzKZDG5ubvD19S23thNiC4Zh2KnxSqWywqbGp6WlYdmyZdi3bx8AQCKR4OOPP8bEiRPLbSiYYRio1WpoNBoIBAI4OTnByckJEokEXC59hySkKqCp7kWoiOBHqVQiNTUVer2+yGnosbGxGD9+PIC8QoUnTpxAWloa1q9fj5MnT2Lz5s0IDw9n99fr9VCr1fD396flLEiVYTQaIZfLkZmZCZ1OB6lUCgcHh3K/7uXLl7F48WJcvXoVQN5agTNmzMDIkSPL9fo6nQ4qlQoMw0AkEsHV1RUSiQQikYjqbRFSiSj4KUJ5Bz9yuZxdqLG4WSl///03Bg4cWGDIwN/fH/Pnz7cIfADq9SFVm1arZesDAXmv//LuFWEYBrGxsYiKikJiYiIAoEGDBvjss8/Qp0+fcr2+yWSCRqOBVqsFj8eDRCJhV5anfDxCKh4FP0Uoz+CHYRgkJiZCr9cXG/hkZ2ejV69eSE5OxqBBgzB06FC2wnNISEiB6e3mXp+AgACagkuqtMqYGq/X67Fz505899137PpgQUFBmD59Onr37l3uQZjBYIBGo4FOp4ODgwMbCIlEIggEAuoRIqQCUPBThIoIfhiGKTJAMZlMiIiIwKlTpxAQEIBjx47ZFCx5eHigTp06dm0zIeXBvIzEy5cvoVar2WGh8qZUKrF582Zs2bIFCoUCQN4aYjNmzKiQIAjIC8Q0Gg30ej0cHBwgFovh5OQEkUgEoVBIOUKElBMKfopQFYKf7777DitXroRQKMShQ4fQokWLIs+r1Wqh1+vh7+9PJfhJtWKeEp+VlQW9Xl9h+UByuRz/+9//CgRBn376Kd5///0KWa4DyAuEtFotdDodeDwehEIhGwiJRKIKeS4IqS0o+ClCZQc/v//+OyZOnAgAWLlyJYYPH17sebOysuDj4wMvLy+7tpeQiqLVapGVlQWZTAYAkEqlViuX25tcLkd0dDQ2b97MBkF16tTBRx99hJEjR1ZoyQij0cgGQiaTCQKBACKRCFKpFEKhEEKh0O5rmBFSm1DwU4TKDH4uX76MoUOHQqPRYNy4cYiMjCz2nCqVChwOB/7+/vQtkVRrDMNApVIhKysLCoUCDg4OcHR0rJB8GLlcjp9++gnR0dFIS0sDkBeAjRgxAuPGjbP76vHFYRgGer0eOp0Oer0eHA4HDg4OEAqFkEqlEAgEEAgEcHBwoHwhQmxEwU8RKiv4+ffffzFkyBAoFAqEhoYiOjq62G++JpMJMpkM9evXh6urq13bSkhlMZlMUCqVyMrKglKphEgkglgsrpAPea1WiwMHDmDTpk24f/8+gLzq1e+88w5GjRqFbt26VUpODsMwbCCk1+sB5BWUNAeI5qVxHBwcwOfzKSAixAoKfopQGcHP7du3MWTIEMhkMrRv3x4///yzTauxy+VySKVS1KtXj5IkSY1jNBqRk5ODzMxMqNXqCiuSCOT9rZ45cwabNm3ChQsX2O3+/v744IMPMGzYsEpfNNgcCOn1ehiNRrZ3iM/nQyKRQCgUwsHBgd1WEcOIhFRlFPwUoaKDn1u3bmHEiBHIzMxE27ZtsXv3bptWpdZqtdBqtVTQkNR4er0eCoUCWVlZ0Gq1bE9HRXn48CF27NiBffv2QS6XA8hb4ys0NBSDBw9Gt27dKixBuijmoTKDwQC9Xs8u7srj8dheIvPUej6fb3GjniJSG1DwU4SKDH7OnDmDjz/+GLm5uWjVqhX27NlTZMXn/OfJzs6mJGdSq+h0OsjlcmRnZ0On01V4EKRWq3Ho0CHs2LHDYtFUd3d39O/fH4MGDUKrVq2qXCBhNBrZ3iGDwcAGRVwuFzweDzwejw2MHBwc2G3moInL5VLPMqkRKPgpQkUFP7/99hu++OILdg2vLVu22BT4mNsoEolQv359mv1Bah1zpWjzqvGOjo6l6nkxGo2Ij48vsnhoYW7fvo1ff/0Vv/32m8XCqQ0bNkTv3r3Rp08fvP7661UuEMrPZDLBYDDAaDRa3ACAw+GwAZA5SDInWb+63RwcmW9V+TGT2o2CnyKUd/Bz584dLFq0iF10cdCgQfj2229tfvM2V4n18/OzKS+IkJpKo9FALpezQZA5z8UWsbGxiIyMRHJyMrvNz88PCxYsKLBsTFEMBgP++usv7Nu3D0ePHoVGo2HvCwgIQHh4OMLDw9GmTZtq13uSPyAymUwwmUzs/80BDofDYYMeHo8HDodjdUjt1eCosH8pcCLliYKfIpRn8HPr1i0MHDgQDx48AIfDwaxZszB9+nSb/+DNC0T6+vpWerIlIVVF/iBIr9cXOxwWGxuLCRMmIDQ0FFOmTEFQUBASEhKwbt06qwsG2yonJwenTp3C4cOHcfr0aYtAyMvLC927d0ePHj3QtWvXCq0fVJ4YhrEIisw/57+Z98v/PmctEAJg0ZOUv4cpf3BU3A1AkdvM/8//L6kdKPgpQnkFP+np6WjUqBGUSiW8vb2xfv16dO7cuUTnyMrKYhcurW7fIgkpbxqNBgqFAjKZDDqdzursMPMwc1BQEKKjoy3+jkwmE8aOHYt79+7h/PnzZZodpVKpcPr0aRw+fBinTp1Cbm4uex+Px0P79u3RtWtXdOrUCa1atapVNboYhmGDpPz/5r9Z25b/eKBg4FJUAJT/5/z7A2BfA/n/zb/vq/dbu+/V9hT2f1t+Lsm2st5Xln3L8zwcDqdcJhFQ8FOE8uz5mT9/Pv7880+sXLmyxEXTzEXf/Pz8qsTMEkKqKq1Wi5ycHMhkMmg0Gos6QXFxcRg8eDAOHTqE4ODgAsdevnwZ/fr1Q0xMDDp16mSX9uh0Ovz99984ffo0Tp8+jQcPHljc7+joiPbt26Njx47o1KkTWrZsSbl8pfBqoGQteHo1kHp1W2H/2nKf+QO/sP+X5ufCttlyn5mtgYg9AhZ7cXBwQL169ey+XBMFP0Uoz+DHYDAgKSkJHA6nRPVK1Go1DAYD6tevT3k+hNhIr9ezxRI1Gg34fD5OnDiBKVOm4P79+1b/lpRKJZo2bYoNGzagf//+5dKup0+f4vTp07hw4QLi4uLYJT3MHB0d0aZNG7Rt2xbBwcFo27ZtlZjVWZYEcVLxbP3oLutHvL1DBJPJBJVKhQYNGlRq8ENfP+zIPH5dkheLVquFRqNBvXr1KPAhpAQcHBzg5uYGZ2dn5ObmQiaTQSqVAsirqN6hQ4cCxyQkJAAAvL29y61d/v7+iIiIQEREBEwmE+7evYuLFy8iLi4Oly5dglwux4ULFyyKK9avXx9vvPEGWrVqhebNm6NZs2bl2sZX2StBnFSciurxsbeq0h7q+bETo9GIc+fO4caNG/Dy8sLbb79d7LcmvV6PnJwc1KlTBx4eHlXmRUFIdcQwDHJyctCiRQs0btwYa9asgUQigUgkApfLtWvOT2kZjUYkJCTg2rVr7O3+/ftWvzB5enqygVDz5s0RFBSERo0a2b0KdnkliBNijclkQk5OTqX3/FDwYwf79+/HrFmzkJSUxG4r7luTOfDx9vaGl5cXBT6E2Mn+/fsxaNAghIWFYdy4cfD398fDhw8RHR2N06dPV7kP85ycHFy/fh3Xrl3D7du3cffuXTx+/LjQHuS6deuiYcOGaNSokcW/devWLXFidUUliNdUNFRYchT8VBJ7Bz/mN9o+ffpg7ty5kEqluHfvHjZv3lzotyZz4OPl5QUvLy+a2UWInVn7QlK/fn3MmjUL4eHhEIvFVfpDSq1WIyEhAXfv3sWdO3dw584d3L9/H9nZ2YUew+VyUadOHdSvX9/i5ufnh3r16sHX17fAUjmVkSBeU9BQYelQ8FNJ7Bn8GI1GNG7cGC1btsSBAwfA4XDYCs9CodDqtyadTofc3Fx4enpS4ENIOTIajfjrr7/w4sUL+Pr6on379tDpdJDJZFCr1TAajRCLxRAKhdXm7zArKwuPHz/Go0eP8PjxY/aWmJgIrVZb7PFSqRReXl7w8fGBt7c3FAoFzp49i+XLl8Pf3x8eHh5wc3ODu7s7RCIRcnNzyz1BvDqiocLSo+Annw0bNuDbb79FamoqWrdujXXr1qF9+/aF7h8TE4P58+cjKSkJTZo0wYoVK2x+odkz+Dl79iy6d++OixcvokOHDgXW9nr1W5NarYZWq4W3tzfl+BBSSRiGgUajQW5uLuRyOTQaDTgcDoRCYbUKhPIzmUx4+fIlkpOT8ezZM6SkpBT4v0qlKtE5hUIhHB0dkZWVhebNmyMwMBBubm5wc3ODq6srpFIpnJyc4OjoCCcnJ/Zn8/9raskOGiosm6oS/FT6bK+9e/di5syZ2LRpE0JCQrBmzRqEhYXh3r17Vmc7xMXFYfjw4YiKikKfPn2wa9cu9O/fH1evXkWLFi0qtO0vXrwAgEKvGxQUBCCvAGJOTg4YhoGvry9cXV0p8CGkkphLUYjFYri7u0OtViM3Nxc5OTlQKBRgGAYikahaBUJcLhfe3t7w9va2OnzFMAyUSiXS09MtbqmpqdixYwdEIhG8vLyQlZWF7OxsGAwGaLVatjfJPPRWEgKBgA2IzInnYrG40H+tbRMKhXBwcICDgwMEAkGB/5vXI8u/rbxXsY+Pj0dycjI2bNhQ4PXB5XIxefJk9OvXD/Hx8TRUWIVVes9PSEgI3nzzTaxfvx5AXlTo5+eHKVOm4Isvviiw/9ChQ5Gbm4s//viD3dahQwe0adMGmzZtKvZ6ldHzEx0djbfffhs+Pj7sVFxCSNViNBqh0WigVqshl8uh1WrBMAwcHBzYD+GaKP8QzuTJk9G0aVNcu3YNGzZswIULFzBt2jQ0adIE2dnZ7E0ul0OpVLK3nJwcNoAsaQ9TecgfKL26kv2rK9rn/39h28xlTPh8Pp4/f45//vkH//nPf9iei/xLeRiNRvz000/o3r07GjdubHV9s/zroZn//+p5Xj2msPNYW9Yj//9t3ZZfWc5T3LmBvJy2kSNHwsfHp5DfYOlUm2EvnU4HiUSCffv2WYwnjx49GjKZDAcPHixwjL+/P2bOnInp06ez2xYuXIgDBw7gxo0bBfbP/+0FyHty/Pz8KiTnZ/To0bh37x7i4uJQp06dGtsNTEhNYzKZoNFooNFokJOTA41GA4PBAC6XC4FAUK16hWxhLXnX398f8+fPL3HuisFgQG5urkVgpFKp2MCyuH/z/1+n00Gv10Ov10Or1bL/f3U7qX4uX75stZeyLKrNsNfLly9hNBoLRH8+Pj5sMbJXpaamWt0/NTXV6v5RUVFYvHjx/2vv3oOiKv8/gL/XXRZFhCVRri5YoUYGmgSD5PgtmSidBkUbmtGR6g9Hg1FTpywTHRzFsbHxkoNZM+BURumEpqXlqGyX8QZq4iUviWEpkJWAXJfd5/eHnfPbhQVBV85Zzvs1s7N7znn27IcP8PDhnOc8xz0Bt6HX67F27VpMmzYNkydPxuLFi+Hr64vz589j8+bNsFgsKCgoQHh4eK/qKIl6uz59+sDHxwc+Pj4ICAhAS0sLmpubUV9fj4aGBtTW1sJutzudhvHk3/GJEyciJSXFLZdtGwwG+Pv7w9/f/wFE2p4QAjabzakoavtst9vR2trqdCd7x0dra6t8A1fpdUfrrFYrNm7ciEGDBjn90y7d6HXXrl34+++/MWPGDOh0uk7vcya9R3ot7aej93S0HykPjs/3ss7V8r3ut6P9S9+vtlce9jTFx/w8aG+//TYWLFggL0tHftwlLS0NO3bswMKFC51uZGo2m7Ft2za8/PLLbvssIup5joOh/fz8YLPZ0NzcLF+52djYKI8V0uv1cjHkaYNd9Xq9R45R0el0MBgMMBgMbp8AsiMRERGYNWsWfvnlF2RlZclXe33wwQe4evUqr/bqhOOAZyUpWvwEBgZCr9ejqqrKaX1VVRWCg4Ndvic4OLhb7aVO60FKS0tDamoqfvzxR1y8eBFhYWFISUnhzQuJeiG9Xi8fFTKZTE7FUENDgzyA2mazAbgz/sRgMMDLy8ujjw7R/5s4cSK2bNmCnJwcpKamyuvNZjMLHw+higHP8fHx2LhxI4A7VaHZbEZWVlaHA54bGhqwe/dued3YsWMRExPT4wOeiYjastvtaGlpkR/19fVOp190Op18VZI0GJc8E2d47j5e6v6fBQsWICMjA3FxcYiPj8e6detQX1+PV199FQAwc+ZMhIWFITc3FwAwb948jB8/HmvXrsWkSZNQWFiIkpISbNmyRckvg4gIwJ3xQn379pU79sDAQHmsiDQOpaGhAVarVT5CJJ0yk64skp45JYa6eeqpQlJB8ZOeno6//voL2dnZqKysxKhRo7Bv3z55UHNFRYXToeKxY8di27ZtePfdd/HOO+8gKioKO3fu7PE5foiIusrVmBRpIK3VakVraytaWlrQ2NiI1tZW+Vni6tJsFkZE907x0149jae9iEjNpKJIujrJccJBaVk6WiSR5q6R5o5xfE2kJjztRURE7UiFjKsLNTq6VFsaX2S1WuVl6TJq4M7lxY4T67WdYM/xQaQFLH6IiDyEVBh1RJoDRiqKpNdSIeR4ms1xHhtpuzR/jHRKTXrdlRmG27bjaTlSMxY/RES9hE6nu2uBJHGcZM9x8ry2y0IIp0LJsajqaBI+x8n3HD9PirFtzNJzV1539F5Xz3db19k+qXdj8UNEpEHdKZRccSxyXBU+nT0c399R8eR42q7tc9t9OMbk+Hy3dZ29lpYdi6G2y3fLT0dtOyoEu7rdVduutu/qvtyxv44+Qw3TAbD4ISKiblPq1FZ3budwtyKoozau2t3vuq5s68r27rbrqf10hzTXlZJY/BARkcfo6BQYUXdwaD8RERFpCosfIiIi0hQWP0RERKQpLH6IiIhIU1j8EBERkaaw+CEiIiJNYfFzn5YvX44VK1a43LZixQosX768ZwP6j1rjAtQdmxoxX0TkydTYh7H4uU96vR7Z2dntvrErVqxAdna2YjNZqjUuQN2xqRHzRUSeTJV9mNCYmpoaAUDU1NS4bZ85OTkCgMjJyXG5rBS1xuUqFjXFpkbMFxF5sp7ow7rz910nhAJzWyuopqYGJpMJ165dg5+fn9v2u2bNGqxcuRJGoxEtLS1YsmQJ3nzzTbftv7fFBag7NjVivojIkz3oPqy2thZDhgzBrVu34O/v32lbzRU/f/zxB4YMGaJ0GERERPQAXLt2DeHh4Z220VzxY7fbcf36dQwYMMCt94aRKlqJWv4rV/PRArXmTK2Yr+6T/hN095He3or56j7mrOsedB8mhEBdXR1CQ0PRp89dhjS77WSbhknnLpcsWeL0rPR4DDWPE1FrztSK+bo3D2KMX2/GfHUfc9Y1auvDWPzcJ8eCwvGXQOlCo6PPVzqutjGoKWdqxXzdO/5h6h7mq/uYs7tTYx9mcMehJi2z2WzIycnB0qVLUVtbK69funSpvF3puBwpHZf02WrMmVoxX0TkyVTZh/V4udWLNTU1iWXLlommpialQ/EYzFn3MF/dw3x1D/PVfcxZ96glX5ob8ExERETaxhmeiYiISFNY/BAREZGmsPghIiIiTWHxQ0RERJrC4seNNm3ahMjISPTt2xcJCQk4duyY0iGpwg8//IAXX3wRoaGh0Ol02Llzp9N2IQSys7MREhKCfv36ITk5GZcuXVImWBXIzc3FU089hQEDBmDw4MGYPHkyLly44NSmqakJmZmZGDhwIHx9fTF16lRUVVUpFLGy8vLyEBMTAz8/P/j5+SExMRF79+6VtzNXnVu9ejV0Oh3mz58vr2POnC1fvhw6nc7pMWLECHk789Xen3/+iRkzZmDgwIHo168fnnjiCZSUlMjble73Wfy4yRdffIEFCxZg2bJlOHHiBGJjY5GSkoLq6mqlQ1NcfX09YmNjsWnTJpfb16xZgw0bNmDz5s04evQo+vfvj5SUFDQ1NfVwpOpgsViQmZmJI0eOYP/+/bBarXjuuedQX18vt3njjTewe/dubN++HRaLBdevX0daWpqCUSsnPDwcq1evRmlpKUpKSvDss88iNTUVZ8+eBcBcdeb48eP48MMPERMT47SeOWvv8ccfx40bN+THTz/9JG9jvpz9+++/SEpKgpeXF/bu3Ytz585h7dq1CAgIkNso3u8reqF9LxIfHy8yMzPlZZvNJkJDQ0Vubq6CUakPAFFUVCQv2+12ERwcLN577z153a1bt4S3t7f4/PPPFYhQfaqrqwUAYbFYhBB38uPl5SW2b98utzl//rwAIA4fPqxUmKoSEBAgPv74Y+aqE3V1dSIqKkrs379fjB8/XsybN08IwZ8vV5YtWyZiY2NdbmO+2nvrrbfE008/3eF2NfT7PPLjBi0tLSgtLUVycrK8rk+fPkhOTsbhw4cVjEz9ysvLUVlZ6ZQ7f39/JCQkMHf/qampAQA89NBDAIDS0lJYrVannI0YMQJms1nzObPZbCgsLER9fT0SExOZq05kZmZi0qRJTrkB+PPVkUuXLiE0NBQPP/wwpk+fjoqKCgDMlytff/014uLi8NJLL2Hw4MEYPXo0PvroI3m7Gvp9Fj9ucPPmTdhsNgQFBTmtDwoKQmVlpUJReQYpP8yda3a7HfPnz0dSUhJGjhwJ4E7OjEYjTCaTU1st56ysrAy+vr7w9vbG7NmzUVRUhOjoaOaqA4WFhThx4gRyc3PbbWPO2ktISEBBQQH27duHvLw8lJeXY9y4cairq2O+XLhy5Qry8vIQFRWF7777DnPmzMHcuXOxdetWAOro93lvLyIVy8zMxJkzZ5zGF1B7w4cPx6lTp1BTU4MdO3YgIyMDFotF6bBU6dq1a5g3bx7279+Pvn37Kh2OR3jhhRfk1zExMUhISEBERAS+/PJL9OvXT8HI1MlutyMuLg6rVq0CAIwePRpnzpzB5s2bkZGRoXB0d/DIjxsEBgZCr9e3G91fVVWF4OBghaLyDFJ+mLv2srKysGfPHhw6dAjh4eHy+uDgYLS0tODWrVtO7bWcM6PRiEcffRRjxoxBbm4uYmNjsX79eubKhdLSUlRXV+PJJ5+EwWCAwWCAxWLBhg0bYDAYEBQUxJzdhclkwrBhw3D58mX+jLkQEhKC6Ohop3WPPfaYfKpQDf0+ix83MBqNGDNmDA4cOCCvs9vtOHDgABITExWMTP2GDh2K4OBgp9zV1tbi6NGjms2dEAJZWVkoKirCwYMHMXToUKftY8aMgZeXl1POLly4gIqKCs3mrC273Y7m5mbmyoUJEyagrKwMp06dkh9xcXGYPn26/Jo569zt27fx22+/ISQkhD9jLiQlJbWbnuPixYuIiIgAoJJ+v0eGVWtAYWGh8Pb2FgUFBeLcuXNi1qxZwmQyicrKSqVDU1xdXZ04efKkOHnypAAg3n//fXHy5Enx+++/CyGEWL16tTCZTGLXrl3i9OnTIjU1VQwdOlQ0NjYqHLky5syZI/z9/UVxcbG4ceOG/GhoaJDbzJ49W5jNZnHw4EFRUlIiEhMTRWJiooJRK2fx4sXCYrGI8vJycfr0abF48WKh0+nE999/L4RgrrrC8WovIZizthYuXCiKi4tFeXm5+Pnnn0VycrIIDAwU1dXVQgjmq61jx44Jg8EgVq5cKS5duiQ+++wz4ePjIz799FO5jdL9PosfN9q4caMwm83CaDSK+Ph4ceTIEaVDUoVDhw4JAO0eGRkZQog7lz0uXbpUBAUFCW9vbzFhwgRx4cIFZYNWkKtcARD5+flym8bGRvH666+LgIAA4ePjI6ZMmSJu3LihXNAKeu2110RERIQwGo1i0KBBYsKECXLhIwRz1RVtix/mzFl6eroICQkRRqNRhIWFifT0dHH58mV5O/PV3u7du8XIkSOFt7e3GDFihNiyZYvTdqX7fZ0QQvTMMSYiIiIi5XHMDxEREWkKix8iIiLSFBY/REREpCksfoiIiEhTWPwQERGRprD4ISIiIk1h8UNERESawuKHiDxScXExdDpdu3sqERHdDSc5JCKP8L///Q+jRo3CunXrAAAtLS34559/EBQUBJ1Op2xwRORRDEoHQER0L4xGo2bvmk1E94envYhI9V555RVYLBasX78eOp0OOp0OBQUFTqe9CgoKYDKZsGfPHgwfPhw+Pj6YNm0aGhoasHXrVkRGRiIgIABz586FzWaT993c3IxFixYhLCwM/fv3R0JCAoqLi5X5QomoR/DIDxGp3vr163Hx4kWMHDkSOTk5AICzZ8+2a9fQ0IANGzagsLAQdXV1SEtLw5QpU2AymfDtt9/iypUrmDp1KpKSkpCeng4AyMrKwrlz51BYWIjQ0FAUFRXh+eefR1lZGaKionr06ySinsHih4hUz9/fH0ajET4+PvKprl9//bVdO6vViry8PDzyyCMAgGnTpuGTTz5BVVUVfH19ER0djWeeeQaHDh1Ceno6KioqkJ+fj4qKCoSGhgIAFi1ahH379iE/Px+rVq3quS+SiHoMix8i6jV8fHzkwgcAgoKCEBkZCV9fX6d11dXVAICysjLYbDYMGzbMaT/Nzc0YOHBgzwRNRD2OxQ8R9RpeXl5OyzqdzuU6u90OALh9+zb0ej1KS0uh1+ud2jkWTETUu7D4ISKPYDQanQYqu8Po0aNhs9lQXV2NcePGuXXfRKRevNqLiDxCZGQkjh49iqtXr+LmzZvy0Zv7MWzYMEyfPh0zZ87EV199hfLychw7dgy5ubn45ptv3BA1EakRix8i8giLFi2CXq9HdHQ0Bg0ahIqKCrfsNz8/HzNnzsTChQsxfPhwTJ48GcePH4fZbHbL/olIfTjDMxEREWkKj/wQERGRprD4ISIiIk1h8UNERESawuKHiIiINIXFDxEREWkKix8iIiLSFBY/REREpCksfoiIiEhTWPwQERGRprD4ISIiIk1h8UNERESawuKHiIiINOX/AOxO4Toq8/C0AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR (single regularization strength with noise model)\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "t, pEpoR = simulate_pEpoR(problem=regproblems[chosen_regstrength], result=regresults[chosen_regstrength])\n", + "sigma_pEpoR = 0.0274 + 0.1 * pEpoR\n", + "ax.fill_between(t, pEpoR - 2*sigma_pEpoR, pEpoR + 2*sigma_pEpoR, color='black', alpha=0.10, interpolate=True, label='2-sigma error bands')\n", + "ax.plot(t, pEpoR, color='black', label='MLE')\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ylim1 = ax.get_ylim()[0]\n", + "ax.plot(nodes, len(nodes)*[ylim1], 'x', color='black', label='spline nodes', zorder=10, clip_on=False)\n", + "ax.set_ylim(ylim1, ax.get_ylim()[1])\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.set_title(f\"ML fit for regularization strength = {chosen_regstrength}\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "809c240f-e3ca-45ec-906e-0e87d10b46dd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Store results for later\n", + "all_results['5 nodes'] = (regproblems[chosen_regstrength], regresults[chosen_regstrength])" + ] + }, + { + "cell_type": "markdown", + "id": "6ae693f7-f3c1-43cd-8b0e-12b69b4ca6ef", + "metadata": {}, + "source": [ + "## Comparing the three approaches" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "71b57527-51f6-479a-8d6d-895e9b2f4d34", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pEpoR\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for (label, (problem, result)) in all_results.items():\n", + " t, pEpoR = simulate_pEpoR(problem=problem, result=result)\n", + " ax.plot(t, pEpoR, label=label)\n", + "ax.plot(df_pEpoR['time'], df_pEpoR['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pEpoR\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "214476c9-ecab-4201-a528-4507539b0b05", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for pSTAT5\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for (label, (problem, result)) in all_results.items():\n", + " t, pSTAT5 = simulate_pSTAT5(problem=problem, result=result)\n", + " ax.plot(t, pSTAT5, label=label)\n", + "ax.plot(df_pSTAT5['time'], df_pSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"pSTAT5\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "5776b49f-a3ba-401d-88a5-0a7674e4b14b", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Plot ML fit for tSTAT5\n", + "fig, ax = plt.subplots(figsize=(6.5, 3.5))\n", + "for (label, (problem, result)) in all_results.items():\n", + " t, tSTAT5 = simulate_tSTAT5(problem=problem, result=result)\n", + " ax.plot(t, tSTAT5, label=label)\n", + "ax.plot(df_tSTAT5['time'], df_tSTAT5['measurement'], 'o', color='black', markerfacecolor='none', label='experimental data')\n", + "ax.set_xlabel(\"time\")\n", + "ax.set_ylabel(\"tSTAT5\")\n", + "ax.legend();" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "c2aeab6e-828c-4748-b3c8-2494ae89ef43", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "### 5 nodes, FD\n", + "k1 = -0.012344171128634264\n", + "k2 = -1.11975626735931\n", + "k3 = 5.999999816644789\n", + "k4 = 0.22576351403212522\n", + "scale_tSTAT5 = -0.020792663448966672\n", + "scale_pSTAT5 = 0.1422550065768319\n", + "sigma_pEpoR_abs = -1.562249437179612\n", + "sigma_pEpoR_rel = -1.0\n", + "pEpoR_t0 = -2.6870875267006804\n", + "pEpoR_t5 = -0.7797622417853871\n", + "pEpoR_t10 = -0.11820562755751975\n", + "pEpoR_t20 = -0.9974218537437654\n", + "pEpoR_t60 = -6.90775527898212\n", + "\n", + "### 15 nodes, FD\n", + "k1 = 0.1543170078851364\n", + "k2 = -1.0042579083153138\n", + "k3 = -0.17925294344845363\n", + "k4 = 0.31486258696137254\n", + "scale_tSTAT5 = -0.03364700359730668\n", + "scale_pSTAT5 = 0.11013784140762342\n", + "sigma_pEpoR_abs = -1.562249437179612\n", + "sigma_pEpoR_rel = -1.0\n", + "pEpoR_t0 = -2.40456191981547\n", + "pEpoR_t2_dot_5 = -1.6438670641678346\n", + "pEpoR_t5_dot_0 = -0.80437214623219\n", + "pEpoR_t7_dot_5 = -0.1144993579909219\n", + "pEpoR_t10_dot_0 = -0.09849380649928209\n", + "pEpoR_t12_dot_5 = -0.30861764847405077\n", + "pEpoR_t15_dot_0 = -0.535565172217061\n", + "pEpoR_t17_dot_5 = -0.8808659628360864\n", + "pEpoR_t20 = -1.1184724117332843\n", + "pEpoR_t25 = -1.3245209689075161\n", + "pEpoR_t30 = -2.3651756746835053\n", + "pEpoR_t35 = -3.4734027477458524\n", + "pEpoR_t40 = -4.578132101040909\n", + "pEpoR_t50 = -5.7968417139258435\n", + "pEpoR_t60 = -6.268875988124801\n", + "regularization_strength = 1.8750612633917\n", + "\n", + "### 5 nodes\n", + "k1 = 0.2486924371230916\n", + "k2 = -0.9010429810987043\n", + "k3 = -0.3408591074551208\n", + "k4 = 0.3594353532480489\n", + "scale_tSTAT5 = -0.03395751814386045\n", + "scale_pSTAT5 = 0.1008121903144357\n", + "sigma_pEpoR_abs = -1.562249437179612\n", + "sigma_pEpoR_rel = -1.0\n", + "pEpoR_t0 = -2.8601663957890175\n", + "pEpoR_t5 = -0.7275787612811422\n", + "pEpoR_t10 = -0.08172482568007049\n", + "pEpoR_t20 = -1.02532663950965\n", + "pEpoR_t60 = -6.907755278982137\n", + "derivative_pEpoR_t0 = 0.026587630472163528\n", + "derivative_pEpoR_t5 = 0.17154606724507934\n", + "derivative_pEpoR_t10 = -0.05503215878900286\n", + "derivative_pEpoR_t20 = -0.016352876798592663\n", + "regularization_strength = 2.2430380486862944\n" + ] + } + ], + "source": [ + "# Compare parameter values\n", + "for (label, (problem, result)) in all_results.items():\n", + " print(f\"\\n### {label}\")\n", + " x = result.optimize_result.x[0]\n", + " if len(x) == len(problem.x_free_indices):\n", + " names = problem.x_names[problem.x_free_indices]\n", + " else:\n", + " names = problem.x_names\n", + " for (name, value) in zip(names, x):\n", + " print(f\"{name} = {value}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2ced065a-4b15-4403-91c6-6a46dc0b3e66", + "metadata": {}, + "source": [ + "## Bibliography\n", + "Schelker, M. et al. (2012). “Comprehensive estimation of input signals and dynamics in biochemical reaction networks”. In: Bioinformatics 28.18, pp. i529–i534. doi: [10.1093/bioinformatics/bts393](https://doi.org/10.1093/bioinformatics/bts393).\n", + "\n", + "Swameye, I. et al. (2003). “Identification of nucleocytoplasmic cycling as a remote sensor in cellular signaling by databased modeling”. In: Proceedings of the National Academy of Sciences 100.3, pp. 1028–1033. doi: [10.1073/pnas.0237333100](https://doi.org/10.1073/pnas.0237333100).\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_conditions.tsv b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_conditions.tsv new file mode 100644 index 0000000000..97ed387788 --- /dev/null +++ b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_conditions.tsv @@ -0,0 +1,2 @@ +conditionId conditionName +condition1 diff --git a/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_measurements.tsv b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_measurements.tsv new file mode 100644 index 0000000000..8813d0aae9 --- /dev/null +++ b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_measurements.tsv @@ -0,0 +1,47 @@ +observableId simulationConditionId measurement time noiseParameters observableTransformation +tSTAT5_au condition1 1.0000 0 0.084 lin +tSTAT5_au condition1 0.9275 2 0.046 lin +tSTAT5_au condition1 0.7923 4 0.038 lin +tSTAT5_au condition1 0.7778 6 0.032 lin +tSTAT5_au condition1 0.7053 8 0.033 lin +tSTAT5_au condition1 0.6522 10 0.037 lin +tSTAT5_au condition1 0.5894 12 0.039 lin +tSTAT5_au condition1 0.5894 14 0.040 lin +tSTAT5_au condition1 0.6377 16 0.030 lin +tSTAT5_au condition1 0.6425 18 0.028 lin +tSTAT5_au condition1 0.6908 20 0.030 lin +tSTAT5_au condition1 0.6908 25 0.031 lin +tSTAT5_au condition1 0.7585 30 0.032 lin +tSTAT5_au condition1 0.8068 40 0.040 lin +tSTAT5_au condition1 0.9275 50 0.046 lin +tSTAT5_au condition1 0.9710 60 0.082 lin +pSTAT5_au condition1 0.3315 2 0.050 lin +pSTAT5_au condition1 0.8645 4 0.066 lin +pSTAT5_au condition1 0.9635 6 0.070 lin +pSTAT5_au condition1 0.9279 8 0.065 lin +pSTAT5_au condition1 0.8162 10 0.051 lin +pSTAT5_au condition1 0.7553 12 0.053 lin +pSTAT5_au condition1 0.7680 14 0.051 lin +pSTAT5_au condition1 0.8416 16 0.040 lin +pSTAT5_au condition1 0.7680 18 0.040 lin +pSTAT5_au condition1 0.8010 20 0.048 lin +pSTAT5_au condition1 0.7832 25 0.052 lin +pSTAT5_au condition1 0.8086 30 0.054 lin +pSTAT5_au condition1 0.4888 40 0.055 lin +pSTAT5_au condition1 0.2782 50 0.044 lin +pSTAT5_au condition1 0.2553 60 0.071 lin +pEpoR_au condition1 0.01713 0 lin +pEpoR_au condition1 0.145 2 lin +pEpoR_au condition1 0.2442 4 lin +pEpoR_au condition1 0.7659 6 lin +pEpoR_au condition1 1 8 lin +pEpoR_au condition1 0.8605 10 lin +pEpoR_au condition1 0.7829 12 lin +pEpoR_au condition1 0.5705 14 lin +pEpoR_au condition1 0.6217 16 lin +pEpoR_au condition1 0.331 18 lin +pEpoR_au condition1 0.3388 20 lin +pEpoR_au condition1 0.3116 25 lin +pEpoR_au condition1 0.05062 30 lin +pEpoR_au condition1 0.02504 40 lin +pEpoR_au condition1 0.01163 50 lin diff --git a/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_model.xml b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_model.xml new file mode 100644 index 0000000000..1e8045a74c --- /dev/null +++ b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_model.xml @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cyt + k1 + STAT5 + pEpoR + + + + + + + + + + + + + + + + cyt + k2 + + + pSTAT5 + 2 + + + + + + + + + + + + + + + + + cyt + k3 + pSTAT5_pSTAT5 + + + + + + + + + + + + + + + + nuc + k4 + npSTAT5_npSTAT5 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_1 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_2 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_3 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_4 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_5 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_6 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_7 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_8 + + + + + + + + + + + + + + + + nuc + k4 + nSTAT5_9 + + + + + + + diff --git a/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_observables.tsv b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_observables.tsv new file mode 100644 index 0000000000..63d12243d7 --- /dev/null +++ b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_observables.tsv @@ -0,0 +1,4 @@ +observableId observableFormula observableTransformation noiseFormula noiseDistribution +tSTAT5_au scale_tSTAT5 * (STAT5 + pSTAT5 + 2 * pSTAT5_pSTAT5) lin noiseParameter1_tSTAT5_au normal +pSTAT5_au scale_pSTAT5 * (pSTAT5 + 2 * pSTAT5_pSTAT5) lin noiseParameter1_pSTAT5_au normal +pEpoR_au pEpoR lin sigma_pEpoR_abs + sigma_pEpoR_rel * pEpoR normal diff --git a/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_parameters.tsv b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_parameters.tsv new file mode 100644 index 0000000000..e3858d0119 --- /dev/null +++ b/python/examples/example_splines_swameye/Swameye_PNAS2003/swameye2003_parameters.tsv @@ -0,0 +1,9 @@ +parameterId parameterScale lowerBound upperBound nominalValue estimate +k1 log10 0.001 100000 1.95 1 +k2 log10 0.001 100000 0.11 1 +k3 log10 0.001 1000000 98400 1 +k4 log10 0.001 100000 1.49 1 +scale_tSTAT5 log10 0.01 100 0.95 1 +scale_pSTAT5 log10 0.01 100 1.25 1 +sigma_pEpoR_abs log10 0.001 1 0.0274 0 +sigma_pEpoR_rel log10 0.001 1 0.10 0 diff --git a/python/examples/example_steadystate/ExampleSteadystate.ipynb b/python/examples/example_steadystate/ExampleSteadystate.ipynb index 52f014dba8..0d9765e727 100644 --- a/python/examples/example_steadystate/ExampleSteadystate.ipynb +++ b/python/examples/example_steadystate/ExampleSteadystate.ipynb @@ -1975,4 +1975,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/python/examples/example_steadystate/model_steadystate_scaled.xml b/python/examples/example_steadystate/model_steadystate_scaled.xml index dc5ae669f7..980b7b72a3 100644 --- a/python/examples/example_steadystate/model_steadystate_scaled.xml +++ b/python/examples/example_steadystate/model_steadystate_scaled.xml @@ -196,4 +196,3 @@ - diff --git a/python/examples/example_steadystate/model_steadystate_scaled_without_observables.xml b/python/examples/example_steadystate/model_steadystate_scaled_without_observables.xml index 62c673e513..e55a2c7892 100644 --- a/python/examples/example_steadystate/model_steadystate_scaled_without_observables.xml +++ b/python/examples/example_steadystate/model_steadystate_scaled_without_observables.xml @@ -150,4 +150,3 @@ - diff --git a/python/examples/example_units/model_units.xml b/python/examples/example_units/model_units.xml index 9108ad78cf..09e2e40ac4 100644 --- a/python/examples/example_units/model_units.xml +++ b/python/examples/example_units/model_units.xml @@ -86,4 +86,4 @@ - \ No newline at end of file + diff --git a/python/sdist/amici/__init__.py b/python/sdist/amici/__init__.py index ec6756204a..7160acb475 100644 --- a/python/sdist/amici/__init__.py +++ b/python/sdist/amici/__init__.py @@ -14,7 +14,7 @@ import sys from pathlib import Path from types import ModuleType as ModelModule -from typing import Optional, Union, Callable, Any +from typing import Any, Callable, Optional, Union def _get_amici_path(): @@ -23,7 +23,7 @@ def _get_amici_path(): repository, get repository root """ basedir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - if os.path.exists(os.path.join(basedir, '.git')): + if os.path.exists(os.path.join(basedir, ".git")): return os.path.abspath(basedir) return os.path.dirname(__file__) @@ -33,24 +33,26 @@ def _get_commit_hash(): basedir = os.path.dirname(os.path.dirname(os.path.dirname(amici_path))) commitfile = next( ( - file for file in [ - os.path.join(basedir, '.git', 'FETCH_HEAD'), - os.path.join(basedir, '.git', 'ORIG_HEAD'), ] + file + for file in [ + os.path.join(basedir, ".git", "FETCH_HEAD"), + os.path.join(basedir, ".git", "ORIG_HEAD"), + ] if os.path.isfile(file) ), - None + None, ) if commitfile: with open(commitfile) as f: - return str(re.search(r'^([\w]*)', f.read().strip()).group()) - return 'unknown' + return str(re.search(r"^([\w]*)", f.read().strip()).group()) + return "unknown" def _imported_from_setup() -> bool: """Check whether this module is imported from `setup.py`""" - from inspect import getouterframes, currentframe + from inspect import currentframe, getouterframes from os import sep # in case we are imported from setup.py, this will be the AMICI package @@ -64,8 +66,8 @@ def _imported_from_setup() -> bool: # requires the AMICI extension during its installation, but seems # unlikely... frame_path = os.path.realpath(os.path.expanduser(frame.filename)) - if (frame_path == os.path.join(package_root, 'setup.py') - or frame_path.endswith(f"{sep}setuptools{sep}build_meta.py") + if frame_path == os.path.join(package_root, "setup.py") or frame_path.endswith( + f"{sep}setuptools{sep}build_meta.py" ): return True @@ -76,20 +78,22 @@ def _imported_from_setup() -> bool: #: absolute root path of the amici repository or Python package amici_path = _get_amici_path() #: absolute path of the amici swig directory -amiciSwigPath = os.path.join(amici_path, 'swig') +amiciSwigPath = os.path.join(amici_path, "swig") #: absolute path of the amici source directory -amiciSrcPath = os.path.join(amici_path, 'src') +amiciSrcPath = os.path.join(amici_path, "src") #: absolute root path of the amici module amiciModulePath = os.path.dirname(__file__) #: boolean indicating if this is the full package with swig interface or # the raw package without extension -has_clibs: bool = any(os.path.isfile(os.path.join(amici_path, wrapper)) - for wrapper in ['amici.py', 'amici_without_hdf5.py']) +has_clibs: bool = any( + os.path.isfile(os.path.join(amici_path, wrapper)) + for wrapper in ["amici.py", "amici_without_hdf5.py"] +) #: boolean indicating if amici was compiled with hdf5 support hdf5_enabled: bool = False # Get version number from file -with open(os.path.join(amici_path, 'version.txt')) as f: +with open(os.path.join(amici_path, "version.txt")) as f: __version__ = f.read().strip() __commit__ = _get_commit_hash() @@ -99,21 +103,20 @@ def _imported_from_setup() -> bool: if has_clibs: from . import amici from .amici import * + # has to be done before importing readSolverSettingsFromHDF5 # from .swig_wrappers - hdf5_enabled = 'readSolverSettingsFromHDF5' in dir() - from .swig_wrappers import * - + hdf5_enabled = "readSolverSettingsFromHDF5" in dir() # These modules require the swig interface and other dependencies - from .numpy import ReturnDataView, ExpDataView + from .numpy import ExpDataView, ReturnDataView from .pandas import * + from .swig_wrappers import * # These modules don't require the swig interface - from .sbml_import import SbmlImporter, assignmentRules2observables - from .de_export import DEModel, DEExporter - from typing import Protocol, runtime_checkable + from .de_export import DEExporter, DEModel + from .sbml_import import SbmlImporter, assignmentRules2observables @runtime_checkable class ModelModule(Protocol): @@ -146,8 +149,7 @@ def __exit__(self, exc_type, exc_value, traceback): def import_model_module( - module_name: str, - module_path: Optional[Union[Path, str]] = None + module_name: str, module_path: Optional[Union[Path, str]] = None ) -> ModelModule: """ Import Python module of an AMICI model @@ -177,9 +179,11 @@ def import_model_module( # be imported. del sys.modules[module_name] # collect first, don't delete while iterating - to_unload = {loaded_module_name for loaded_module_name in - sys.modules.keys() if - loaded_module_name.startswith(f"{module_name}.")} + to_unload = { + loaded_module_name + for loaded_module_name in sys.modules.keys() + if loaded_module_name.startswith(f"{module_name}.") + } for m in to_unload: del sys.modules[m] @@ -190,12 +194,14 @@ def import_model_module( class AmiciVersionError(RuntimeError): """Error thrown if an AMICI model is loaded that is incompatible with the installed AMICI base package""" + pass def _get_default_argument(func: Callable, arg: str) -> Any: """Get the default value of the given argument in the given function.""" import inspect + signature = inspect.signature(func) if (default := signature.parameters[arg].default) is not inspect.Parameter.empty: return default diff --git a/python/sdist/amici/__init__.template.py b/python/sdist/amici/__init__.template.py index bc83c1d4c9..c37ac0f962 100644 --- a/python/sdist/amici/__init__.template.py +++ b/python/sdist/amici/__init__.template.py @@ -1,20 +1,21 @@ """AMICI-generated module for model TPL_MODELNAME""" -import amici from pathlib import Path +import amici + # Ensure we are binary-compatible, see #556 -if 'TPL_AMICI_VERSION' != amici.__version__: +if "TPL_AMICI_VERSION" != amici.__version__: raise amici.AmiciVersionError( - f'Cannot use model `TPL_MODELNAME` in {Path(__file__).parent}, ' - 'generated with amici==TPL_AMICI_VERSION, ' - f'together with amici=={amici.__version__} ' - 'which is currently installed. To use this model, install ' - 'amici==TPL_AMICI_VERSION or re-import the model with the amici ' - 'version currently installed.' + f"Cannot use model `TPL_MODELNAME` in {Path(__file__).parent}, " + "generated with amici==TPL_AMICI_VERSION, " + f"together with amici=={amici.__version__} " + "which is currently installed. To use this model, install " + "amici==TPL_AMICI_VERSION or re-import the model with the amici " + "version currently installed." ) from .TPL_MODELNAME import * from .TPL_MODELNAME import getModel as get_model -__version__ = 'TPL_PACKAGE_VERSION' +__version__ = "TPL_PACKAGE_VERSION" diff --git a/python/sdist/amici/__main__.py b/python/sdist/amici/__main__.py index dac5230270..b8fbc77c0f 100644 --- a/python/sdist/amici/__main__.py +++ b/python/sdist/amici/__main__.py @@ -1,9 +1,11 @@ """Package-level entrypoint""" -from . import __version__, compiledWithOpenMP, has_clibs, hdf5_enabled import os import sys +from . import __version__, compiledWithOpenMP, has_clibs, hdf5_enabled + + def print_info(): """Displays information on the current AMICI installation. @@ -21,5 +23,6 @@ def print_info(): print(f"AMICI ({sys.platform}) version {__version__} ({','.join(features)})") -if __name__ == '__main__': + +if __name__ == "__main__": print_info() diff --git a/python/sdist/amici/bngl_import.py b/python/sdist/amici/bngl_import.py index 840e4a4229..960413dbe6 100644 --- a/python/sdist/amici/bngl_import.py +++ b/python/sdist/amici/bngl_import.py @@ -26,7 +26,7 @@ def bngl2amici(bngl_model: str, *args, **kwargs) -> None: see :func:`amici.pysb_import.pysb2amici` for additional arguments """ - if 'model' in kwargs: - raise ValueError('model argument not allowed') + if "model" in kwargs: + raise ValueError("model argument not allowed") pysb_model = model_from_bngl(bngl_model) pysb2amici(pysb_model, *args, **kwargs) diff --git a/python/sdist/amici/conserved_quantities_demartino.py b/python/sdist/amici/conserved_quantities_demartino.py index 28fe3f9e77..c579558f71 100644 --- a/python/sdist/amici/conserved_quantities_demartino.py +++ b/python/sdist/amici/conserved_quantities_demartino.py @@ -2,7 +2,7 @@ import math import random import sys -from typing import List, MutableSequence, Sequence, Tuple, Union, Optional +from typing import List, MutableSequence, Optional, Sequence, Tuple, Union from .logging import get_logger @@ -16,12 +16,12 @@ def compute_moiety_conservation_laws( - stoichiometric_list: Sequence[float], - 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, + stoichiometric_list: Sequence[float], + 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, ) -> Tuple[List[List[int]], List[List[float]]]: """Compute moiety conservation laws. @@ -47,16 +47,20 @@ def compute_moiety_conservation_laws( list of lists of corresponding coefficients. """ # compute semi-positive conservation laws - (kernel_dim, engaged_species, int_kernel_dim, conserved_moieties, - cls_species_idxs, cls_coefficients) = _kernel( - stoichiometric_list, num_species, num_reactions) + ( + kernel_dim, + engaged_species, + int_kernel_dim, + conserved_moieties, + cls_species_idxs, + cls_coefficients, + ) = _kernel(stoichiometric_list, num_species, num_reactions) # if the number of integer MCLs equals total MCLS no MC relaxation - done = (int_kernel_dim == kernel_dim) + done = int_kernel_dim == kernel_dim if not done: # construct interaction matrix - J, J2, fields = _fill(stoichiometric_list, engaged_species, - num_species) + J, J2, fields = _fill(stoichiometric_list, engaged_species, num_species) # seed random number generator if rng_seed is not False: @@ -66,68 +70,83 @@ def compute_moiety_conservation_laws( # maximum number of montecarlo search before starting relaxation while not done: yes, int_kernel_dim, conserved_moieties = _monte_carlo( - engaged_species, J, J2, fields, conserved_moieties, - int_kernel_dim, cls_species_idxs, cls_coefficients, - num_species, max_iter=max_num_monte_carlo + engaged_species, + J, + J2, + fields, + conserved_moieties, + int_kernel_dim, + cls_species_idxs, + cls_coefficients, + num_species, + max_iter=max_num_monte_carlo, ) # if the number of integer MCLs equals total MCLS then MC done - done = (int_kernel_dim == kernel_dim) + done = int_kernel_dim == kernel_dim timer = 0 if yes else timer + 1 if timer == max_num_monte_carlo: - done = _relax(stoichiometric_list, conserved_moieties, - num_reactions, num_species) + done = _relax( + stoichiometric_list, conserved_moieties, num_reactions, num_species + ) timer = 0 _reduce(int_kernel_dim, cls_species_idxs, cls_coefficients, num_species) - _output(int_kernel_dim, kernel_dim, engaged_species, cls_species_idxs, - cls_coefficients, species_names, verbose=True) + _output( + int_kernel_dim, + kernel_dim, + engaged_species, + cls_species_idxs, + cls_coefficients, + species_names, + verbose=True, + ) return cls_species_idxs[:int_kernel_dim], cls_coefficients[:int_kernel_dim] def _output( - int_kernel_dim: int, - kernel_dim: int, - int_matched: List[int], - species_indices: List[List[int]], - species_coefficients: List[List[float]], - species_names: Optional[Sequence[str]] = None, - verbose: bool = False, - log_level: int = logging.DEBUG + int_kernel_dim: int, + kernel_dim: int, + int_matched: List[int], + species_indices: List[List[int]], + species_coefficients: List[List[float]], + species_names: Optional[Sequence[str]] = None, + verbose: bool = False, + log_level: int = logging.DEBUG, ): """Log infos on identified conservation laws""" + def log(*args, **kwargs): logger.log(log_level, *args, **kwargs) - log(f"There are {int_kernel_dim} linearly independent conserved " - f"moieties, engaging {len(int_matched)} state variables.") + log( + f"There are {int_kernel_dim} linearly independent conserved " + f"moieties, engaging {len(int_matched)} state variables." + ) if int_kernel_dim == kernel_dim: log("They generate all the conservation laws") else: - log(f"They don't generate all the conservation laws, " + log( + f"They don't generate all the conservation laws, " f"{kernel_dim - int_kernel_dim} of them are not reducible to " - "moieties") + "moieties" + ) # print all conserved quantities if verbose: - for i, (coefficients, engaged_species_idxs) \ - in enumerate(zip(species_coefficients, species_indices)): + for i, (coefficients, engaged_species_idxs) in enumerate( + zip(species_coefficients, species_indices) + ): if not engaged_species_idxs: continue - log(f"Moiety number {i + 1} engages {len(engaged_species_idxs)} " - "species:") - for species_idx, coefficient \ - in zip(engaged_species_idxs, coefficients): - name = species_names[species_idx] if species_names \ - else species_idx + log( + f"Moiety number {i + 1} engages {len(engaged_species_idxs)} " "species:" + ) + for species_idx, coefficient in zip(engaged_species_idxs, coefficients): + name = species_names[species_idx] if species_names else species_idx log(f"\t{name}\t{coefficient}") -def _qsort( - k: int, - km: int, - order: MutableSequence[int], - pivots: Sequence[int] -) -> None: +def _qsort(k: int, km: int, order: MutableSequence[int], pivots: Sequence[int]) -> None: """Quicksort Recursive implementation of the quicksort algorithm @@ -169,11 +188,8 @@ def _qsort( def _kernel( - stoichiometric_list: Sequence[float], - num_species: int, - num_reactions: int -) -> Tuple[int, List[int], int, List[int], - List[List[int]], List[List[float]]]: + stoichiometric_list: Sequence[float], num_species: int, num_reactions: int +) -> Tuple[int, List[int], int, List[int], List[List[int]], List[List[float]]]: """ Kernel (left nullspace of :math:`S`) calculation by Gaussian elimination @@ -214,8 +230,7 @@ def _kernel( matrix2[i].append(1) order: List[int] = list(range(num_species)) - pivots = [matrix[i][0] if len(matrix[i]) else _MAX - for i in range(num_species)] + pivots = [matrix[i][0] if len(matrix[i]) else _MAX for i in range(num_species)] done = False while not done: @@ -225,14 +240,17 @@ def _kernel( min1 = _MAX if len(matrix[order[j]]) > 1: for i in range(len(matrix[order[j]])): - min1 = min(min1, abs(matrix2[order[j]][0] - / matrix2[order[j]][i])) + min1 = min( + min1, abs(matrix2[order[j]][0] / matrix2[order[j]][i]) + ) min2 = _MAX if len(matrix[order[j + 1]]) > 1: for i in range(len(matrix[order[j + 1]])): - min2 = min(min2, abs(matrix2[order[j + 1]][0] - / matrix2[order[j + 1]][i])) + min2 = min( + min2, + abs(matrix2[order[j + 1]][0] / matrix2[order[j + 1]][i]), + ) if min2 > min1: # swap @@ -271,8 +289,7 @@ def _kernel( kernel_dim = 0 for i in range(num_species): - done = all(matrix[i][j] >= num_reactions - for j in range(len(matrix[i]))) + done = all(matrix[i][j] >= num_reactions for j in range(len(matrix[i]))) if done and len(matrix[i]): for j in range(len(matrix[i])): RSolutions[kernel_dim].append(matrix[i][j] - num_reactions) @@ -292,8 +309,7 @@ def _kernel( if RSolutions2[i][j] * RSolutions2[i][0] < 0: ok2 = False if not matched or all( - cur_matched != RSolutions[i][j] for cur_matched in - matched + cur_matched != RSolutions[i][j] for cur_matched in matched ): matched.append(RSolutions[i][j]) if ok2 and len(RSolutions[i]): @@ -303,8 +319,8 @@ def _kernel( cls_coefficients[i2].append(abs(RSolutions2[i][j])) min_value = min(min_value, abs(RSolutions2[i][j])) if not int_matched or all( - cur_int_matched != cls_species_idxs[i2][j] - for cur_int_matched in int_matched + cur_int_matched != cls_species_idxs[i2][j] + for cur_int_matched in int_matched ): int_matched.append(cls_species_idxs[i2][j]) for j in range(len(cls_species_idxs[i2])): @@ -313,17 +329,21 @@ def _kernel( int_kernel_dim = i2 assert int_kernel_dim <= kernel_dim - assert len(cls_species_idxs) == len(cls_coefficients), \ - "Inconsistent number of conserved quantities in coefficients and " \ - "species" - return (kernel_dim, matched, int_kernel_dim, int_matched, cls_species_idxs, - cls_coefficients) + assert len(cls_species_idxs) == len(cls_coefficients), ( + "Inconsistent number of conserved quantities in coefficients and " "species" + ) + return ( + kernel_dim, + matched, + int_kernel_dim, + int_matched, + cls_species_idxs, + cls_coefficients, + ) def _fill( - stoichiometric_list: Sequence[float], - matched: Sequence[int], - num_species: int + stoichiometric_list: Sequence[float], matched: Sequence[int], num_species: int ) -> Tuple[List[List[int]], List[List[int]], List[int]]: """Construct interaction matrix @@ -381,12 +401,12 @@ def _fill( def _is_linearly_dependent( - vector: Sequence[float], - int_kernel_dim: int, - cls_species_idxs: Sequence[Sequence[int]], - cls_coefficients: Sequence[Sequence[float]], - matched: Sequence[int], - num_species: int + vector: Sequence[float], + int_kernel_dim: int, + cls_species_idxs: Sequence[Sequence[int]], + cls_coefficients: Sequence[Sequence[float]], + matched: Sequence[int], + num_species: int, ) -> bool: """Check for linear dependence between MCLs @@ -439,13 +459,16 @@ def _is_linearly_dependent( min1 = _MAX if len(matrix[order[j]]) > 1: for i in range(len(matrix[order[j]])): - min1 = min(min1, abs(matrix2[order[j]][0] - / matrix2[order[j]][i])) + min1 = min( + min1, abs(matrix2[order[j]][0] / matrix2[order[j]][i]) + ) min2 = _MAX if len(matrix[order[j + 1]]) > 1: for i in range(len(matrix[order[j + 1]])): - min2 = min(min2, abs(matrix2[order[j + 1]][0] - / matrix2[order[j + 1]][i])) + min2 = min( + min2, + abs(matrix2[order[j + 1]][0] / matrix2[order[j + 1]][i]), + ) if min2 > min1: # swap k2 = order[j + 1] @@ -476,18 +499,18 @@ def _is_linearly_dependent( def _monte_carlo( - matched: Sequence[int], - J: Sequence[Sequence[int]], - J2: Sequence[Sequence[float]], - fields: Sequence[float], - int_matched: MutableSequence[int], - int_kernel_dim: int, - cls_species_idxs: MutableSequence[MutableSequence[int]], - cls_coefficients: MutableSequence[MutableSequence[float]], - num_species: int, - initial_temperature: float = 1, - cool_rate: float = 1e-3, - max_iter: int = 10 + matched: Sequence[int], + J: Sequence[Sequence[int]], + J2: Sequence[Sequence[float]], + fields: Sequence[float], + int_matched: MutableSequence[int], + int_kernel_dim: int, + cls_species_idxs: MutableSequence[MutableSequence[int]], + cls_coefficients: MutableSequence[MutableSequence[float]], + num_species: int, + initial_temperature: float = 1, + cool_rate: float = 1e-3, + max_iter: int = 10, ) -> Tuple[bool, int, Sequence[int]]: """MonteCarlo simulated annealing for finding integer MCLs @@ -526,8 +549,7 @@ def _monte_carlo( considered otherwise the algorithm retries Monte Carlo up to max_iter """ dim = len(matched) - num = [int(2 * random.uniform(0, 1)) if len(J[i]) else 0 - for i in range(dim)] + num = [int(2 * random.uniform(0, 1)) if len(J[i]) else 0 for i in range(dim)] numtot = sum(num) def compute_h(): @@ -589,10 +611,9 @@ def compute_h(): # founds MCLS? need to check for linear independence if len(int_matched) and not _is_linearly_dependent( - num, int_kernel_dim, cls_species_idxs, - cls_coefficients, matched, num_species): - logger.debug( - "Found a moiety but it is linearly dependent... next.") + num, int_kernel_dim, cls_species_idxs, cls_coefficients, matched, num_species + ): + logger.debug("Found a moiety but it is linearly dependent... next.") return False, int_kernel_dim, int_matched # reduce by MC procedure @@ -607,10 +628,10 @@ def compute_h(): _reduce(int_kernel_dim, cls_species_idxs, cls_coefficients, num_species) min_value = 1000 for i in range(len(cls_species_idxs[int_kernel_dim - 1])): - if not len(int_matched) \ - or all(cur_int_matched - != cls_species_idxs[int_kernel_dim - 1][i] - for cur_int_matched in int_matched): + if not len(int_matched) or all( + cur_int_matched != cls_species_idxs[int_kernel_dim - 1][i] + for cur_int_matched in int_matched + ): int_matched.append(cls_species_idxs[int_kernel_dim - 1][i]) min_value = min(min_value, cls_coefficients[int_kernel_dim - 1][i]) @@ -619,18 +640,19 @@ def compute_h(): logger.debug( f"Found linearly independent moiety, now there are " - f"{int_kernel_dim} engaging {len(int_matched)} species") + f"{int_kernel_dim} engaging {len(int_matched)} species" + ) return True, int_kernel_dim, int_matched def _relax( - stoichiometric_list: Sequence[float], - int_matched: Sequence[int], - num_reactions: int, - num_species: int, - relaxation_max: float = 1e6, - relaxation_step: float = 1.9 + stoichiometric_list: Sequence[float], + int_matched: Sequence[int], + num_reactions: int, + num_species: int, + relaxation_max: float = 1e6, + relaxation_step: float = 1.9, ) -> bool: """Relaxation scheme for Monte Carlo final solution @@ -685,13 +707,16 @@ def _relax( min1 = _MAX if len(matrix[order[j]]) > 1: for i in range(len(matrix[order[j]])): - min1 = min(min1, abs(matrix2[order[j]][0] - / matrix2[order[j]][i])) + min1 = min( + min1, abs(matrix2[order[j]][0] / matrix2[order[j]][i]) + ) min2 = _MAX if len(matrix[order[j + 1]]) > 1: for i in range(len(matrix[order[j + 1]])): - min2 = min(min2, abs(matrix2[order[j + 1]][0] - / matrix2[order[j + 1]][i])) + min2 = min( + min2, + abs(matrix2[order[j + 1]][0] / matrix2[order[j + 1]][i]), + ) if min2 > min1: # swap k2 = order[j + 1] @@ -748,8 +773,9 @@ def _relax( for a in range(len(matrix[j])): row_k[matrix[j][a]] -= matrix2[j][a] * matrix2[k][i] # filter - matrix[k] = [row_idx for row_idx, row_val in enumerate(row_k) - if row_val != 0] + matrix[k] = [ + row_idx for row_idx, row_val in enumerate(row_k) if row_val != 0 + ] matrix2[k] = [row_val for row_val in row_k if row_val != 0] if len(matrix[k]) <= i: @@ -838,7 +864,7 @@ def _relax( # Motzkin relaxation alpha = -relaxation_step * cmin - fact = sum(val ** 2 for val in matrixb2[cmin_idx]) + fact = sum(val**2 for val in matrixb2[cmin_idx]) alpha /= fact alpha = max(1e-9 * _MIN, alpha) for j in range(len(matrixb[cmin_idx])): @@ -853,10 +879,10 @@ def _relax( def _reduce( - int_kernel_dim: int, - cls_species_idxs: MutableSequence[MutableSequence[int]], - cls_coefficients: MutableSequence[MutableSequence[float]], - num_species: int + int_kernel_dim: int, + cls_species_idxs: MutableSequence[MutableSequence[int]], + cls_coefficients: MutableSequence[MutableSequence[float]], + num_species: int, ) -> None: """Reducing the solution which has been found by the Monte Carlo process @@ -888,12 +914,14 @@ def _reduce( for j in range(i + 1, K): k2 = order[j] column: List[float] = [0] * num_species - for species_idx, coefficient \ - in zip(cls_species_idxs[k1], cls_coefficients[k1]): + for species_idx, coefficient in zip( + cls_species_idxs[k1], cls_coefficients[k1] + ): column[species_idx] = coefficient ok1 = True - for species_idx, coefficient \ - in zip(cls_species_idxs[k2], cls_coefficients[k2]): + for species_idx, coefficient in zip( + cls_species_idxs[k2], cls_coefficients[k2] + ): column[species_idx] -= coefficient if column[species_idx] < -_MIN: ok1 = False diff --git a/python/sdist/amici/conserved_quantities_rref.py b/python/sdist/amici/conserved_quantities_rref.py index 4c401293cf..46028e94b0 100644 --- a/python/sdist/amici/conserved_quantities_rref.py +++ b/python/sdist/amici/conserved_quantities_rref.py @@ -6,8 +6,7 @@ def rref( - mat: np.array, - round_ndigits: Optional[Union[Literal[False], int]] = None + mat: np.array, round_ndigits: Optional[Union[Literal[False], int]] = None ) -> np.array: """ Bring matrix ``mat`` to reduced row echelon form @@ -25,14 +24,15 @@ def rref( # no-op def _round(mat): return mat + else: if round_ndigits is None: # drop the least significant digit (more or less) - round_ndigits = - int(np.ceil(np.log10(np.spacing(1)))) + round_ndigits = -int(np.ceil(np.log10(np.spacing(1)))) def _round(mat): mat = np.round(mat, round_ndigits) - mat[np.abs(mat) <= 10**(-round_ndigits)] = 0 + mat[np.abs(mat) <= 10 ** (-round_ndigits)] = 0 return mat # create a copy that will be modified diff --git a/python/sdist/amici/constants.py b/python/sdist/amici/constants.py index 12acd87252..74b365889c 100644 --- a/python/sdist/amici/constants.py +++ b/python/sdist/amici/constants.py @@ -19,17 +19,18 @@ class SymbolId(str, enum.Enum): SymbolId.SPECIES], which is how the field should be accessed programmatically. """ - SPECIES = 'species' - ALGEBRAIC_STATE = 'algebraic_state' - ALGEBRAIC_EQUATION = 'algebraic_equation' - PARAMETER = 'parameter' - FIXED_PARAMETER = 'fixed_parameter' - OBSERVABLE = 'observable' - EXPRESSION = 'expression' - SIGMAY = 'sigmay' - LLHY = 'llhy' - EVENT = 'event' - EVENT_OBSERVABLE = 'event_observable' - SIGMAZ = 'sigmaz' - LLHZ = 'llhz' - LLHRZ = 'llhrz' + + SPECIES = "species" + ALGEBRAIC_STATE = "algebraic_state" + ALGEBRAIC_EQUATION = "algebraic_equation" + PARAMETER = "parameter" + FIXED_PARAMETER = "fixed_parameter" + OBSERVABLE = "observable" + EXPRESSION = "expression" + SIGMAY = "sigmay" + LLHY = "llhy" + EVENT = "event" + EVENT_OBSERVABLE = "event_observable" + SIGMAZ = "sigmaz" + LLHZ = "llhz" + LLHRZ = "llhrz" diff --git a/python/sdist/amici/custom_commands.py b/python/sdist/amici/custom_commands.py index d8db24c083..2e69800fc7 100644 --- a/python/sdist/amici/custom_commands.py +++ b/python/sdist/amici/custom_commands.py @@ -5,6 +5,7 @@ import sys from pathlib import Path +from amici.swig import fix_typehints from cmake_build_extension import BuildExtension, CMakeExtension from setuptools.command.build_py import build_py from setuptools.command.develop import develop @@ -12,16 +13,15 @@ from setuptools.command.install_lib import install_lib from setuptools.command.sdist import sdist -from amici.swig import fix_typehints - 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"), + ("no-clibs", None, "Don't build AMICI C++ extension"), ] def initialize_options(self): @@ -39,7 +39,7 @@ class AmiciDevelop(develop): # Passing --no-clibs allows to install the Python-only part of AMICI user_options = develop.user_options + [ - ('no-clibs', None, "Don't build AMICI C++ extension"), + ("no-clibs", None, "Don't build AMICI C++ extension"), ] def initialize_options(self): @@ -63,14 +63,21 @@ def run(self): """ print("running AmiciInstallLib") - if os.environ.get('ENABLE_AMICI_DEBUGGING') == 'TRUE' \ - and sys.platform == 'darwin': - search_dir = os.path.join(os.getcwd(), self.build_dir, 'amici') + if ( + os.environ.get("ENABLE_AMICI_DEBUGGING") == "TRUE" + and sys.platform == "darwin" + ): + search_dir = os.path.join(os.getcwd(), self.build_dir, "amici") for file in os.listdir(search_dir): - if file.endswith('.so'): - subprocess.run(['dsymutil', os.path.join(search_dir, file), - '-o', - os.path.join(search_dir, f'{file}.dSYM')]) + if file.endswith(".so"): + subprocess.run( + [ + "dsymutil", + os.path.join(search_dir, file), + "-o", + os.path.join(search_dir, f"{file}.dSYM"), + ] + ) # Continue with the actual installation super().run() @@ -96,8 +103,14 @@ def save_git_version(): """ with open(os.path.join("amici", "git_version.txt"), "w") as f: try: - cmd = ['git', 'describe', '--abbrev=4', '--dirty=-dirty', - '--always', '--tags'] + cmd = [ + "git", + "describe", + "--abbrev=4", + "--dirty=-dirty", + "--always", + "--tags", + ] subprocess.run(cmd, stdout=f) except Exception as e: print(e) @@ -120,10 +133,14 @@ def run(self): print(f"running {self.__class__.__name__}") # custom flag to build without extensions - no_clibs = 'develop' in self.distribution.command_obj \ - and self.get_finalized_command('develop').no_clibs - no_clibs |= 'install' in self.distribution.command_obj \ - and self.get_finalized_command('install').no_clibs + no_clibs = ( + "develop" in self.distribution.command_obj + and self.get_finalized_command("develop").no_clibs + ) + no_clibs |= ( + "install" in self.distribution.command_obj + and self.get_finalized_command("install").no_clibs + ) if no_clibs: # Nothing to build @@ -144,9 +161,7 @@ def run(self): return result - def build_extension( - self, ext: CMakeExtension - ) -> None: + def build_extension(self, ext: CMakeExtension) -> None: # put some structure into CMake output print("-" * 30, ext.name, "-" * 30, file=sys.stderr) @@ -156,8 +171,8 @@ def build_extension( build_dir = self.build_lib if self.inplace == 0 else os.getcwd() build_dir = Path(build_dir).absolute().as_posix() ext.cmake_configure_options = [ - x.replace("${build_dir}", build_dir) for x in - ext.cmake_configure_options] + x.replace("${build_dir}", build_dir) for x in ext.cmake_configure_options + ] super().build_extension(ext) diff --git a/python/sdist/amici/cxxcodeprinter.py b/python/sdist/amici/cxxcodeprinter.py index 1a5f106850..3055518c5b 100644 --- a/python/sdist/amici/cxxcodeprinter.py +++ b/python/sdist/amici/cxxcodeprinter.py @@ -2,10 +2,10 @@ import itertools import os import re -from typing import Dict, List, Optional, Tuple, Iterable +from typing import Dict, Iterable, List, Optional, Tuple import sympy as sp -from sympy.codegen.rewriting import optimize, Optimization +from sympy.codegen.rewriting import Optimization, optimize from sympy.printing.cxx import CXX11CodePrinter from sympy.utilities.iterables import numbered_symbols from toposort import toposort @@ -34,8 +34,11 @@ def __init__(self): super().__init__() # extract common subexpressions in matrix functions? - self.extract_cse = (os.getenv("AMICI_EXTRACT_CSE", "0").lower() - in ('1', 'on', 'true')) + self.extract_cse = os.getenv("AMICI_EXTRACT_CSE", "0").lower() in ( + "1", + "on", + "true", + ) # Floating-point optimizations # e.g., log(1 + x) --> logp1(x) @@ -54,7 +57,7 @@ def doprint(self, expr: sp.Expr, assign_to: Optional[str] = None) -> str: try: # floating point code = super().doprint(expr, assign_to) - code = re.sub(r'(^|\W)M_PI(\W|$)', r'\1amici::pi\2', code) + code = re.sub(r"(^|\W)M_PI(\W|$)", r"\1amici::pi\2", code) return code except TypeError as e: @@ -65,26 +68,28 @@ def doprint(self, expr: sp.Expr, assign_to: Optional[str] = None) -> str: def _print_min_max(self, expr, cpp_fun: str, sympy_fun): # C++ doesn't like mixing int and double for arguments for min/max, # therefore, we just always convert to float - arg0 = sp.Float(expr.args[0]) if expr.args[0].is_number \ - else expr.args[0] + arg0 = sp.Float(expr.args[0]) if expr.args[0].is_number else expr.args[0] if len(expr.args) == 1: return self._print(arg0) - return "%s%s(%s, %s)" % (self._ns, cpp_fun, self._print(arg0), - self._print(sympy_fun(*expr.args[1:]))) + return "%s%s(%s, %s)" % ( + self._ns, + cpp_fun, + self._print(arg0), + self._print(sympy_fun(*expr.args[1:])), + ) def _print_Min(self, expr): from sympy.functions.elementary.miscellaneous import Min + return self._print_min_max(expr, "min", Min) def _print_Max(self, expr): from sympy.functions.elementary.miscellaneous import Max + return self._print_min_max(expr, "max", Max) def _get_sym_lines_array( - self, - equations: sp.Matrix, - variable: str, - indent_level: int + self, equations: sp.Matrix, variable: str, indent_level: int ) -> List[str]: """ Generate C++ code for assigning symbolic terms in symbols to C++ array @@ -103,17 +108,13 @@ def _get_sym_lines_array( C++ code as list of lines """ return [ - ' ' * indent_level + f'{variable}[{index}] = ' - f'{self.doprint(math)};' + " " * indent_level + f"{variable}[{index}] = " f"{self.doprint(math)};" for index, math in enumerate(equations) if math not in [0, 0.0] ] def _get_sym_lines_symbols( - self, symbols: sp.Matrix, - equations: sp.Matrix, - variable: str, - indent_level: int + self, symbols: sp.Matrix, equations: sp.Matrix, variable: str, indent_level: int ) -> List[str]: """ Generate C++ code for where array elements are directly replaced with @@ -138,44 +139,47 @@ def _get_sym_lines_symbols( def format_regular_line(symbol, math, index): return ( - f'{indent}{self.doprint(symbol)} = {self.doprint(math)};' - f' // {variable}[{index}]'.replace('\n', '\n' + indent) + f"{indent}{self.doprint(symbol)} = {self.doprint(math)};" + f" // {variable}[{index}]".replace("\n", "\n" + indent) ) if self.extract_cse: # Extract common subexpressions cse_sym_prefix = "__amici_cse_" - symbol_generator = numbered_symbols( - cls=sp.Symbol, prefix=cse_sym_prefix) + symbol_generator = numbered_symbols(cls=sp.Symbol, prefix=cse_sym_prefix) replacements, reduced_exprs = sp.cse( equations, symbols=symbol_generator, - order='none', + order="none", list=False, ) if replacements: # we need toposort to handle the dependencies of extracted # subexpressions - expr_dict = dict(itertools.chain(zip(symbols, reduced_exprs), - replacements)) - sorted_symbols = toposort({ - identifier: { - s for s in definition.free_symbols - if s in expr_dict + expr_dict = dict( + itertools.chain(zip(symbols, reduced_exprs), replacements) + ) + sorted_symbols = toposort( + { + identifier: { + s for s in definition.free_symbols if s in expr_dict + } + for (identifier, definition) in expr_dict.items() } - for (identifier, definition) in expr_dict.items() - }) + ) symbol_to_idx = {sym: idx for idx, sym in enumerate(symbols)} def format_line(symbol: sp.Symbol): math = expr_dict[symbol] if str(symbol).startswith(cse_sym_prefix): - return f'{indent}const realtype ' \ - f'{self.doprint(symbol)} ' \ - f'= {self.doprint(math)};' + return ( + f"{indent}const realtype " + f"{self.doprint(symbol)} " + f"= {self.doprint(math)};" + ) elif math not in [0, 0.0]: - return format_regular_line( - symbol, math, symbol_to_idx[symbol]) + return format_regular_line(symbol, math, symbol_to_idx[symbol]) + return [ line for symbol_group in sorted_symbols @@ -190,15 +194,13 @@ def format_line(symbol: sp.Symbol): ] def csc_matrix( - self, - matrix: sp.Matrix, - rownames: List[sp.Symbol], - colnames: List[sp.Symbol], - identifier: Optional[int] = 0, - pattern_only: Optional[bool] = False - ) -> Tuple[ - List[int], List[int], sp.Matrix, List[str], sp.Matrix - ]: + self, + matrix: sp.Matrix, + rownames: List[sp.Symbol], + colnames: List[sp.Symbol], + identifier: Optional[int] = 0, + pattern_only: Optional[bool] = False, + ) -> Tuple[List[int], List[int], sp.Matrix, List[str], sp.Matrix]: """ Generates the sparse symbolic identifiers, symbolic identifiers, sparse matrix, column pointers and row values for a symbolic @@ -245,10 +247,9 @@ def csc_matrix( symbol_row_vals.append(row) idx += 1 - symbol_name = f'd{rownames[row].name}' \ - f'_d{colnames[col].name}' + symbol_name = f"d{rownames[row].name}" f"_d{colnames[col].name}" if identifier: - symbol_name += f'_{identifier}' + symbol_name += f"_{identifier}" symbol_list.append(symbol_name) if pattern_only: continue @@ -266,8 +267,7 @@ def csc_matrix( else: sparse_list = sp.Matrix(sparse_list) - return symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, \ - sparse_matrix + return symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, sparse_matrix @staticmethod def print_bool(expr) -> str: @@ -275,9 +275,12 @@ def print_bool(expr) -> str: return "true" if bool(expr) else "false" -def get_switch_statement(condition: str, cases: Dict[int, List[str]], - indentation_level: Optional[int] = 0, - indentation_step: Optional[str] = ' ' * 4): +def get_switch_statement( + condition: str, + cases: Dict[int, List[str]], + indentation_level: Optional[int] = 0, + indentation_step: Optional[str] = " " * 4, +): """ Generate code for switch statement @@ -307,14 +310,16 @@ def get_switch_statement(condition: str, cases: Dict[int, List[str]], indent2 = (indentation_level + 2) * indentation_step for expression, statements in cases.items(): if statements: - lines.extend([ - f'{indent1}case {expression}:', - *(f"{indent2}{statement}" for statement in statements), - f'{indent2}break;' - ]) + lines.extend( + [ + f"{indent1}case {expression}:", + *(f"{indent2}{statement}" for statement in statements), + f"{indent2}break;", + ] + ) if lines: - lines.insert(0, f'{indent0}switch({condition}) {{') - lines.append(indent0 + '}') + lines.insert(0, f"{indent0}switch({condition}) {{") + lines.append(indent0 + "}") return lines diff --git a/python/sdist/amici/de_export.py b/python/sdist/amici/de_export.py index 1be6efefcb..e7d1750184 100644 --- a/python/sdist/amici/de_export.py +++ b/python/sdist/amici/de_export.py @@ -21,38 +21,60 @@ from itertools import chain, starmap from pathlib import Path from string import Template -from typing import (Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, - Union) +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) import numpy as np import sympy as sp from sympy.matrices.dense import MutableDenseMatrix from sympy.matrices.immutable import ImmutableDenseMatrix -from . import (__commit__, __version__, amiciModulePath, amiciSrcPath, - amiciSwigPath, sbml_import) +from . import ( + __commit__, + __version__, + amiciModulePath, + amiciSrcPath, + amiciSwigPath, + splines, +) from .constants import SymbolId from .cxxcodeprinter import AmiciCxxCodePrinter, get_switch_statement -from .import_utils import (ObservableTransformation, generate_flux_symbol, - smart_subs_dict, strip_pysb, - symbol_with_assumptions, toposort_symbols, - SBMLException) -from .logging import get_logger, log_execution_time, set_log_level from .de_model import * +from .import_utils import ( + amici_time_symbol, + ObservableTransformation, + SBMLException, + generate_flux_symbol, + smart_subs_dict, + strip_pysb, + symbol_with_assumptions, + toposort_symbols, +) +from .logging import get_logger, log_execution_time, set_log_level +if TYPE_CHECKING: + from . import sbml_import # Template for model simulation main.cpp file -CXX_MAIN_TEMPLATE_FILE = os.path.join(amiciSrcPath, 'main.template.cpp') +CXX_MAIN_TEMPLATE_FILE = os.path.join(amiciSrcPath, "main.template.cpp") # Template for model/swig/CMakeLists.txt -SWIG_CMAKE_TEMPLATE_FILE = os.path.join(amiciSwigPath, - 'CMakeLists_model.cmake') +SWIG_CMAKE_TEMPLATE_FILE = os.path.join(amiciSwigPath, "CMakeLists_model.cmake") # Template for model/CMakeLists.txt -MODEL_CMAKE_TEMPLATE_FILE = os.path.join(amiciSrcPath, - 'CMakeLists.template.cmake') +MODEL_CMAKE_TEMPLATE_FILE = os.path.join(amiciSrcPath, "CMakeLists.template.cmake") -IDENTIFIER_PATTERN = re.compile(r'^[a-zA-Z_]\w*$') -DERIVATIVE_PATTERN = re.compile(r'^d(x_rdata|xdot|\w+?)d(\w+?)(?:_explicit)?$') +IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z_]\w*$") +DERIVATIVE_PATTERN = re.compile(r"^d(x_rdata|xdot|\w+?)d(\w+?)(?:_explicit)?$") @dataclass @@ -78,13 +100,14 @@ class _FunctionInfo: :ivar body: the actual function body. will be filled later """ - ode_arguments: str = '' - dae_arguments: str = '' - return_type: str = 'void' + + ode_arguments: str = "" + dae_arguments: str = "" + return_type: str = "void" assume_pow_positivity: bool = False sparse: bool = False generate_body: bool = True - body: str = '' + body: str = "" def arguments(self, ode: bool = True) -> str: """Get the arguments for the ODE or DAE function""" @@ -96,373 +119,343 @@ def arguments(self, ode: bool = True) -> str: # Information on a model-specific generated C++ function # prototype for generated C++ functions, keys are the names of functions functions = { - 'Jy': - _FunctionInfo( - 'realtype *Jy, const int iy, const realtype *p, ' - 'const realtype *k, const realtype *y, const realtype *sigmay, ' - 'const realtype *my' - ), - 'dJydsigma': - _FunctionInfo( - 'realtype *dJydsigma, const int iy, const realtype *p, ' - 'const realtype *k, const realtype *y, const realtype *sigmay, ' - 'const realtype *my' - ), - 'dJydy': - _FunctionInfo( - 'realtype *dJydy, const int iy, const realtype *p, ' - 'const realtype *k, const realtype *y, ' - 'const realtype *sigmay, const realtype *my', - sparse=True - ), - 'Jz': - _FunctionInfo( - 'realtype *Jz, const int iz, const realtype *p, const realtype *k, ' - 'const realtype *z, const realtype *sigmaz, const realtype *mz' - ), - 'dJzdsigma': - _FunctionInfo( - 'realtype *dJzdsigma, const int iz, const realtype *p, ' - 'const realtype *k, const realtype *z, const realtype *sigmaz, ' - 'const realtype *mz' - ), - 'dJzdz': - _FunctionInfo( - 'realtype *dJzdz, const int iz, const realtype *p, ' - 'const realtype *k, const realtype *z, const realtype *sigmaz, ' - 'const double *mz', - ), - 'Jrz': - _FunctionInfo( - 'realtype *Jrz, const int iz, const realtype *p, ' - 'const realtype *k, const realtype *rz, const realtype *sigmaz' - ), - 'dJrzdsigma': - _FunctionInfo( - 'realtype *dJrzdsigma, const int iz, const realtype *p, ' - 'const realtype *k, const realtype *rz, const realtype *sigmaz' - ), - 'dJrzdz': - _FunctionInfo( - 'realtype *dJrzdz, const int iz, const realtype *p, ' - 'const realtype *k, const realtype *rz, const realtype *sigmaz', - ), - 'root': - _FunctionInfo( - 'realtype *root, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *tcl' - ), - 'dwdp': - _FunctionInfo( - 'realtype *dwdp, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w, const realtype *tcl, const realtype *dtcldp', - assume_pow_positivity=True, sparse=True - ), - 'dwdx': - _FunctionInfo( - 'realtype *dwdx, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w, const realtype *tcl', - assume_pow_positivity=True, sparse=True - ), - 'dwdw': - _FunctionInfo( - 'realtype *dwdw, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w, const realtype *tcl', - assume_pow_positivity=True, sparse=True - ), - 'dxdotdw': - _FunctionInfo( - 'realtype *dxdotdw, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w', - 'realtype *dxdotdw, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *dx, const realtype *w', - assume_pow_positivity=True, sparse=True - ), - 'dxdotdx_explicit': - _FunctionInfo( - 'realtype *dxdotdx_explicit, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *w', - 'realtype *dxdotdx_explicit, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *dx, const realtype *w', - assume_pow_positivity=True, sparse=True - ), - 'dxdotdp_explicit': - _FunctionInfo( - 'realtype *dxdotdp_explicit, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *w', - 'realtype *dxdotdp_explicit, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *dx, const realtype *w', - assume_pow_positivity=True, sparse=True - ), - 'dydx': - _FunctionInfo( - 'realtype *dydx, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w, const realtype *dwdx', - ), - 'dydp': - _FunctionInfo( - 'realtype *dydp, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const int ip, const realtype *w, const realtype *tcl, ' - 'const realtype *dtcldp', - ), - 'dzdx': - _FunctionInfo( - 'realtype *dzdx, const int ie, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h', - ), - 'dzdp': - _FunctionInfo( - 'realtype *dzdp, const int ie, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const int ip', - ), - 'drzdx': - _FunctionInfo( - 'realtype *drzdx, const int ie, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h', - ), - 'drzdp': - _FunctionInfo( - 'realtype *drzdp, const int ie, const realtype t, ' - 'const realtype *x, const realtype *p, const realtype *k, ' - 'const realtype *h, const int ip', - ), - 'dsigmaydy': - _FunctionInfo( - 'realtype *dsigmaydy, const realtype t, const realtype *p, ' - 'const realtype *k, const realtype *y' - ), - 'dsigmaydp': - _FunctionInfo( - 'realtype *dsigmaydp, const realtype t, const realtype *p, ' - 'const realtype *k, const realtype *y, const int ip', - ), - 'sigmay': - _FunctionInfo( - 'realtype *sigmay, const realtype t, const realtype *p, ' - 'const realtype *k, const realtype *y', - ), - 'dsigmazdp': - _FunctionInfo( - 'realtype *dsigmazdp, const realtype t, const realtype *p,' - ' const realtype *k, const int ip', - ), - 'sigmaz': - _FunctionInfo( - 'realtype *sigmaz, const realtype t, const realtype *p, ' - 'const realtype *k', - ), - 'sroot': - _FunctionInfo( - 'realtype *stau, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *sx, const int ip, const int ie, ' - 'const realtype *tcl', - generate_body=False - ), - 'drootdt': - _FunctionInfo(generate_body=False), - 'drootdt_total': - _FunctionInfo(generate_body=False), - 'drootdp': - _FunctionInfo(generate_body=False), - 'drootdx': - _FunctionInfo(generate_body=False), - 'stau': - _FunctionInfo( - 'realtype *stau, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *tcl, const realtype *sx, const int ip, ' - 'const int ie' - ), - 'deltax': - _FunctionInfo( - 'double *deltax, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const int ie, const realtype *xdot, const realtype *xdot_old' - ), - 'ddeltaxdx': - _FunctionInfo(generate_body=False), - 'ddeltaxdt': - _FunctionInfo(generate_body=False), - 'ddeltaxdp': - _FunctionInfo(generate_body=False), - 'deltasx': - _FunctionInfo( - 'realtype *deltasx, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w, const int ip, const int ie, ' - 'const realtype *xdot, const realtype *xdot_old, ' - 'const realtype *sx, const realtype *stau, const realtype *tcl' - ), - 'w': - _FunctionInfo( - 'realtype *w, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *tcl', - assume_pow_positivity=True - ), - 'x0': - _FunctionInfo( - 'realtype *x0, const realtype t, const realtype *p, ' - 'const realtype *k' - ), - 'x0_fixedParameters': - _FunctionInfo( - 'realtype *x0_fixedParameters, const realtype t, ' - 'const realtype *p, const realtype *k, ' - 'gsl::span reinitialization_state_idxs', - ), - 'sx0': - _FunctionInfo( - 'realtype *sx0, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const int ip', - ), - 'sx0_fixedParameters': - _FunctionInfo( - 'realtype *sx0_fixedParameters, const realtype t, ' - 'const realtype *x0, const realtype *p, const realtype *k, ' - 'const int ip, gsl::span reinitialization_state_idxs', - ), - 'xdot': - _FunctionInfo( - 'realtype *xdot, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *w', - 'realtype *xdot, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h, ' - 'const realtype *dx, const realtype *w', - assume_pow_positivity=True - ), - 'xdot_old': - _FunctionInfo(generate_body=False), - 'y': - _FunctionInfo( - 'realtype *y, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, ' - 'const realtype *h, const realtype *w', - ), - 'x_rdata': - _FunctionInfo( - 'realtype *x_rdata, const realtype *x, const realtype *tcl, ' - 'const realtype *p, const realtype *k' - ), - 'total_cl': - _FunctionInfo( - 'realtype *total_cl, const realtype *x_rdata, ' - 'const realtype *p, const realtype *k' - ), - 'dtotal_cldp': - _FunctionInfo( - 'realtype *dtotal_cldp, const realtype *x_rdata, ' - 'const realtype *p, const realtype *k, const int ip' - ), - 'dtotal_cldx_rdata': - _FunctionInfo( - 'realtype *dtotal_cldx_rdata, const realtype *x_rdata, ' - 'const realtype *p, const realtype *k, const realtype *tcl', - sparse=True - ), - 'x_solver': - _FunctionInfo('realtype *x_solver, const realtype *x_rdata'), - 'dx_rdatadx_solver': - _FunctionInfo( - 'realtype *dx_rdatadx_solver, const realtype *x, ' - 'const realtype *tcl, const realtype *p, const realtype *k', - sparse=True - ), - 'dx_rdatadp': - _FunctionInfo( - 'realtype *dx_rdatadp, const realtype *x, ' - 'const realtype *tcl, const realtype *p, const realtype *k, ' - 'const int ip' - ), - 'dx_rdatadtcl': - _FunctionInfo( - 'realtype *dx_rdatadtcl, const realtype *x, ' - 'const realtype *tcl, const realtype *p, const realtype *k', - sparse=True - ), - 'z': - _FunctionInfo( - 'realtype *z, const int ie, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h' - ), - 'rz': - _FunctionInfo( - 'realtype *rz, const int ie, const realtype t, const realtype *x, ' - 'const realtype *p, const realtype *k, const realtype *h' - ), + "Jy": _FunctionInfo( + "realtype *Jy, const int iy, const realtype *p, " + "const realtype *k, const realtype *y, const realtype *sigmay, " + "const realtype *my" + ), + "dJydsigma": _FunctionInfo( + "realtype *dJydsigma, const int iy, const realtype *p, " + "const realtype *k, const realtype *y, const realtype *sigmay, " + "const realtype *my" + ), + "dJydy": _FunctionInfo( + "realtype *dJydy, const int iy, const realtype *p, " + "const realtype *k, const realtype *y, " + "const realtype *sigmay, const realtype *my", + sparse=True, + ), + "Jz": _FunctionInfo( + "realtype *Jz, const int iz, const realtype *p, const realtype *k, " + "const realtype *z, const realtype *sigmaz, const realtype *mz" + ), + "dJzdsigma": _FunctionInfo( + "realtype *dJzdsigma, const int iz, const realtype *p, " + "const realtype *k, const realtype *z, const realtype *sigmaz, " + "const realtype *mz" + ), + "dJzdz": _FunctionInfo( + "realtype *dJzdz, const int iz, const realtype *p, " + "const realtype *k, const realtype *z, const realtype *sigmaz, " + "const double *mz", + ), + "Jrz": _FunctionInfo( + "realtype *Jrz, const int iz, const realtype *p, " + "const realtype *k, const realtype *rz, const realtype *sigmaz" + ), + "dJrzdsigma": _FunctionInfo( + "realtype *dJrzdsigma, const int iz, const realtype *p, " + "const realtype *k, const realtype *rz, const realtype *sigmaz" + ), + "dJrzdz": _FunctionInfo( + "realtype *dJrzdz, const int iz, const realtype *p, " + "const realtype *k, const realtype *rz, const realtype *sigmaz", + ), + "root": _FunctionInfo( + "realtype *root, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *tcl" + ), + "dwdp": _FunctionInfo( + "realtype *dwdp, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w, const realtype *tcl, const realtype *dtcldp, " + "const realtype *spl, const realtype *sspl", + assume_pow_positivity=True, + sparse=True, + ), + "dwdx": _FunctionInfo( + "realtype *dwdx, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w, const realtype *tcl, const realtype *spl", + assume_pow_positivity=True, + sparse=True, + ), + "create_splines": _FunctionInfo( + "const realtype *p, const realtype *k", + return_type="std::vector", + ), + "spl": _FunctionInfo(generate_body=False), + "sspl": _FunctionInfo(generate_body=False), + "spline_values": _FunctionInfo( + "const realtype *p, const realtype *k", generate_body=False + ), + "spline_slopes": _FunctionInfo( + "const realtype *p, const realtype *k", generate_body=False + ), + "dspline_valuesdp": _FunctionInfo( + "realtype *dspline_valuesdp, const realtype *p, const realtype *k, const int ip" + ), + "dspline_slopesdp": _FunctionInfo( + "realtype *dspline_slopesdp, const realtype *p, const realtype *k, const int ip" + ), + "dwdw": _FunctionInfo( + "realtype *dwdw, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w, const realtype *tcl", + assume_pow_positivity=True, + sparse=True, + ), + "dxdotdw": _FunctionInfo( + "realtype *dxdotdw, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w", + "realtype *dxdotdw, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *dx, const realtype *w", + assume_pow_positivity=True, + sparse=True, + ), + "dxdotdx_explicit": _FunctionInfo( + "realtype *dxdotdx_explicit, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const realtype *w", + "realtype *dxdotdx_explicit, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const realtype *dx, const realtype *w", + assume_pow_positivity=True, + sparse=True, + ), + "dxdotdp_explicit": _FunctionInfo( + "realtype *dxdotdp_explicit, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const realtype *w", + "realtype *dxdotdp_explicit, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const realtype *dx, const realtype *w", + assume_pow_positivity=True, + sparse=True, + ), + "dydx": _FunctionInfo( + "realtype *dydx, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w, const realtype *dwdx", + ), + "dydp": _FunctionInfo( + "realtype *dydp, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const int ip, const realtype *w, const realtype *tcl, " + "const realtype *dtcldp, const realtype *spl, const realtype *sspl" + ), + "dzdx": _FunctionInfo( + "realtype *dzdx, const int ie, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h", + ), + "dzdp": _FunctionInfo( + "realtype *dzdp, const int ie, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const int ip", + ), + "drzdx": _FunctionInfo( + "realtype *drzdx, const int ie, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h", + ), + "drzdp": _FunctionInfo( + "realtype *drzdp, const int ie, const realtype t, " + "const realtype *x, const realtype *p, const realtype *k, " + "const realtype *h, const int ip", + ), + "dsigmaydy": _FunctionInfo( + "realtype *dsigmaydy, const realtype t, const realtype *p, " + "const realtype *k, const realtype *y" + ), + "dsigmaydp": _FunctionInfo( + "realtype *dsigmaydp, const realtype t, const realtype *p, " + "const realtype *k, const realtype *y, const int ip", + ), + "sigmay": _FunctionInfo( + "realtype *sigmay, const realtype t, const realtype *p, " + "const realtype *k, const realtype *y", + ), + "dsigmazdp": _FunctionInfo( + "realtype *dsigmazdp, const realtype t, const realtype *p," + " const realtype *k, const int ip", + ), + "sigmaz": _FunctionInfo( + "realtype *sigmaz, const realtype t, const realtype *p, " "const realtype *k", + ), + "sroot": _FunctionInfo( + "realtype *stau, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *sx, const int ip, const int ie, " + "const realtype *tcl", + generate_body=False, + ), + "drootdt": _FunctionInfo(generate_body=False), + "drootdt_total": _FunctionInfo(generate_body=False), + "drootdp": _FunctionInfo(generate_body=False), + "drootdx": _FunctionInfo(generate_body=False), + "stau": _FunctionInfo( + "realtype *stau, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *tcl, const realtype *sx, const int ip, " + "const int ie" + ), + "deltax": _FunctionInfo( + "double *deltax, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const int ie, const realtype *xdot, const realtype *xdot_old" + ), + "ddeltaxdx": _FunctionInfo(generate_body=False), + "ddeltaxdt": _FunctionInfo(generate_body=False), + "ddeltaxdp": _FunctionInfo(generate_body=False), + "deltasx": _FunctionInfo( + "realtype *deltasx, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w, const int ip, const int ie, " + "const realtype *xdot, const realtype *xdot_old, " + "const realtype *sx, const realtype *stau, const realtype *tcl" + ), + "w": _FunctionInfo( + "realtype *w, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, " + "const realtype *h, const realtype *tcl, const realtype *spl", + assume_pow_positivity=True, + ), + "x0": _FunctionInfo( + "realtype *x0, const realtype t, const realtype *p, " "const realtype *k" + ), + "x0_fixedParameters": _FunctionInfo( + "realtype *x0_fixedParameters, const realtype t, " + "const realtype *p, const realtype *k, " + "gsl::span reinitialization_state_idxs", + ), + "sx0": _FunctionInfo( + "realtype *sx0, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const int ip", + ), + "sx0_fixedParameters": _FunctionInfo( + "realtype *sx0_fixedParameters, const realtype t, " + "const realtype *x0, const realtype *p, const realtype *k, " + "const int ip, gsl::span reinitialization_state_idxs", + ), + "xdot": _FunctionInfo( + "realtype *xdot, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *w", + "realtype *xdot, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h, " + "const realtype *dx, const realtype *w", + assume_pow_positivity=True, + ), + "xdot_old": _FunctionInfo(generate_body=False), + "y": _FunctionInfo( + "realtype *y, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, " + "const realtype *h, const realtype *w", + ), + "x_rdata": _FunctionInfo( + "realtype *x_rdata, const realtype *x, const realtype *tcl, " + "const realtype *p, const realtype *k" + ), + "total_cl": _FunctionInfo( + "realtype *total_cl, const realtype *x_rdata, " + "const realtype *p, const realtype *k" + ), + "dtotal_cldp": _FunctionInfo( + "realtype *dtotal_cldp, const realtype *x_rdata, " + "const realtype *p, const realtype *k, const int ip" + ), + "dtotal_cldx_rdata": _FunctionInfo( + "realtype *dtotal_cldx_rdata, const realtype *x_rdata, " + "const realtype *p, const realtype *k, const realtype *tcl", + sparse=True, + ), + "x_solver": _FunctionInfo("realtype *x_solver, const realtype *x_rdata"), + "dx_rdatadx_solver": _FunctionInfo( + "realtype *dx_rdatadx_solver, const realtype *x, " + "const realtype *tcl, const realtype *p, const realtype *k", + sparse=True, + ), + "dx_rdatadp": _FunctionInfo( + "realtype *dx_rdatadp, const realtype *x, " + "const realtype *tcl, const realtype *p, const realtype *k, " + "const int ip" + ), + "dx_rdatadtcl": _FunctionInfo( + "realtype *dx_rdatadtcl, const realtype *x, " + "const realtype *tcl, const realtype *p, const realtype *k", + sparse=True, + ), + "z": _FunctionInfo( + "realtype *z, const int ie, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h" + ), + "rz": _FunctionInfo( + "realtype *rz, const int ie, const realtype t, const realtype *x, " + "const realtype *p, const realtype *k, const realtype *h" + ), } # list of sparse functions sparse_functions = [ - func_name for func_name, func_info in functions.items() - if func_info.sparse + func_name for func_name, func_info in functions.items() if func_info.sparse ] # list of nobody functions nobody_functions = [ - func_name for func_name, func_info in functions.items() + func_name + for func_name, func_info in functions.items() if not func_info.generate_body ] # list of sensitivity functions sensi_functions = [ - func_name for func_name, func_info in functions.items() - if 'const int ip' in func_info.arguments() + func_name + for func_name, func_info in functions.items() + if "const int ip" in func_info.arguments() ] # list of sensitivity functions sparse_sensi_functions = [ - func_name for func_name, func_info in functions.items() - if 'const int ip' not in func_info.arguments() - and func_name.endswith('dp') or func_name.endswith('dp_explicit') + func_name + for func_name, func_info in functions.items() + if "const int ip" not in func_info.arguments() + and func_name.endswith("dp") + or func_name.endswith("dp_explicit") ] # list of event functions event_functions = [ - func_name for func_name, func_info in functions.items() - if 'const int ie' in func_info.arguments() and - 'const int ip' not in func_info.arguments() + func_name + for func_name, func_info in functions.items() + if "const int ie" in func_info.arguments() + and "const int ip" not in func_info.arguments() ] event_sensi_functions = [ - func_name for func_name, func_info in functions.items() - if 'const int ie' in func_info.arguments() and - 'const int ip' in func_info.arguments() + func_name + for func_name, func_info in functions.items() + if "const int ie" in func_info.arguments() + and "const int ip" in func_info.arguments() ] # list of multiobs functions multiobs_functions = [ - func_name for func_name, func_info in functions.items() - if 'const int iy' in func_info.arguments() - or 'const int iz' in func_info.arguments() + func_name + for func_name, func_info in functions.items() + if "const int iy" in func_info.arguments() + or "const int iz" in func_info.arguments() ] # list of equations that have ids which may not be unique -non_unique_id_symbols = [ - 'x_rdata', 'y' -] +non_unique_id_symbols = ["x_rdata", "y"] # custom c++ function replacements CUSTOM_FUNCTIONS = [ - {'sympy': 'polygamma', - 'c++': 'boost::math::polygamma', - 'include': '#include ', - 'build_hint': 'Using polygamma requires libboost-math header files.' - }, - {'sympy': 'Heaviside', - 'c++': 'amici::heaviside'}, - {'sympy': 'DiracDelta', - 'c++': 'amici::dirac'} + { + "sympy": "polygamma", + "c++": "boost::math::polygamma", + "include": "#include ", + "build_hint": "Using polygamma requires libboost-math header files.", + }, + {"sympy": "Heaviside", "c++": "amici::heaviside"}, + {"sympy": "DiracDelta", "c++": "amici::dirac"}, ] # python log manager @@ -485,11 +478,10 @@ def var_in_function_signature(name: str, varname: str, ode: bool) -> bool: boolean indicating whether the variable occurs in the function signature """ - return name in functions \ - and re.search( - rf'const (realtype|double) \*{varname}[0]*(,|$)+', - functions[name].arguments(ode=ode) - ) + return name in functions and re.search( + rf"const (realtype|double) \*{varname}[0]*(,|$)+", + functions[name].arguments(ode=ode), + ) # defines the type of some attributes in DEModel @@ -507,14 +499,13 @@ def var_in_function_signature(name: str, varname: str, ode: bool) -> bool: SymbolId.LLHZ: LogLikelihoodZ, SymbolId.LLHRZ: LogLikelihoodRZ, SymbolId.EXPRESSION: Expression, - SymbolId.EVENT: Event + SymbolId.EVENT: Event, } -@log_execution_time('running smart_jacobian', logger) +@log_execution_time("running smart_jacobian", logger) def smart_jacobian( - eq: sp.MutableDenseMatrix, - sym_var: sp.MutableDenseMatrix + eq: sp.MutableDenseMatrix, sym_var: sp.MutableDenseMatrix ) -> sp.MutableSparseMatrix: """ Wrapper around symbolic jacobian with some additional checks that reduce @@ -547,24 +538,25 @@ def smart_jacobian( if (n_procs := int(os.environ.get("AMICI_IMPORT_NPROCS", 1))) == 1: # serial - return sp.MutableSparseMatrix(nrow, ncol, - dict(starmap(_jacobian_element, elements)) + return sp.MutableSparseMatrix( + nrow, ncol, dict(starmap(_jacobian_element, elements)) ) # parallel from multiprocessing import get_context + # "spawn" should avoid potential deadlocks occurring with fork # see e.g. https://stackoverflow.com/a/66113051 - ctx = get_context('spawn') + ctx = get_context("spawn") with ctx.Pool(n_procs) as p: mapped = p.starmap(_jacobian_element, elements) return sp.MutableSparseMatrix(nrow, ncol, dict(mapped)) -@log_execution_time('running smart_multiply', logger) +@log_execution_time("running smart_multiply", logger) def smart_multiply( - x: Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix], - y: sp.MutableDenseMatrix + x: Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix], + y: sp.MutableDenseMatrix, ) -> Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix]: """ Wrapper around symbolic multiplication with some additional checks that @@ -577,14 +569,19 @@ def smart_multiply( :return: product """ - if not x.shape[0] or not y.shape[1] or smart_is_zero_matrix(x) or \ - smart_is_zero_matrix(y): + if ( + not x.shape[0] + or not y.shape[1] + or smart_is_zero_matrix(x) + or smart_is_zero_matrix(y) + ): return sp.zeros(x.shape[0], y.shape[1]) return x.multiply(y) -def smart_is_zero_matrix(x: Union[sp.MutableDenseMatrix, - sp.MutableSparseMatrix]) -> bool: +def smart_is_zero_matrix( + x: Union[sp.MutableDenseMatrix, sp.MutableSparseMatrix] +) -> bool: """A faster implementation of sympy's is_zero_matrix Avoids repeated indexer type checks and double iteration to distinguish @@ -733,9 +730,12 @@ class DEModel: list of event indices for each event observable """ - def __init__(self, verbose: Optional[Union[bool, int]] = False, - simplify: Optional[Callable] = _default_simplify, - cache_simplify: bool = False): + def __init__( + self, + verbose: Optional[Union[bool, int]] = False, + simplify: Optional[Callable] = _default_simplify, + cache_simplify: bool = False, + ): """ Create a new DEModel instance. @@ -765,16 +765,23 @@ def __init__(self, verbose: Optional[Union[bool, int]] = False, self._expressions: List[Expression] = [] self._conservation_laws: List[ConservationLaw] = [] self._events: List[Event] = [] + self.splines = [] self._symboldim_funs: Dict[str, Callable[[], int]] = { - 'sx': self.num_states_solver, - 'v': self.num_states_solver, - 'vB': self.num_states_solver, - 'xB': self.num_states_solver, - 'sigmay': self.num_obs, - 'sigmaz': self.num_eventobs, + "sx": self.num_states_solver, + "v": self.num_states_solver, + "vB": self.num_states_solver, + "xB": self.num_states_solver, + "sigmay": self.num_obs, + "sigmaz": self.num_eventobs, } - self._eqs: Dict[str, Union[sp.Matrix, sp.SparseMatrix, - List[Union[sp.Matrix, sp.SparseMatrix]]]] = dict() + self._eqs: Dict[ + str, + Union[ + sp.Matrix, + sp.SparseMatrix, + List[Union[sp.Matrix, sp.SparseMatrix]], + ], + ] = dict() self._sparseeqs: Dict[str, Union[sp.Matrix, List[sp.Matrix]]] = dict() self._vals: Dict[str, List[sp.Expr]] = dict() self._names: Dict[str, List[str]] = dict() @@ -784,46 +791,48 @@ def __init__(self, verbose: Optional[Union[bool, int]] = False, self._rowvals: Dict[str, Union[List[int], List[List[int]]]] = dict() self._equation_prototype: Dict[str, Callable] = { - 'total_cl': self.conservation_laws, - 'x0': self.states, - 'y': self.observables, - 'Jy': self.log_likelihood_ys, - 'Jz': self.log_likelihood_zs, - 'Jrz': self.log_likelihood_rzs, - 'w': self.expressions, - 'root': self.events, - 'sigmay': self.sigma_ys, - 'sigmaz': self.sigma_zs + "total_cl": self.conservation_laws, + "x0": self.states, + "y": self.observables, + "Jy": self.log_likelihood_ys, + "Jz": self.log_likelihood_zs, + "Jrz": self.log_likelihood_rzs, + "w": self.expressions, + "root": self.events, + "sigmay": self.sigma_ys, + "sigmaz": self.sigma_zs, } self._variable_prototype: Dict[str, Callable] = { - 'tcl': self.conservation_laws, - 'x_rdata': self.states, - 'y': self.observables, - 'z': self.event_observables, - 'p': self.parameters, - 'k': self.constants, - 'w': self.expressions, - 'sigmay': self.sigma_ys, - 'sigmaz': self.sigma_zs, - 'h': self.events, + "tcl": self.conservation_laws, + "x_rdata": self.states, + "y": self.observables, + "z": self.event_observables, + "p": self.parameters, + "k": self.constants, + "w": self.expressions, + "sigmay": self.sigma_ys, + "sigmaz": self.sigma_zs, + "h": self.events, } self._value_prototype: Dict[str, Callable] = { - 'p': self.parameters, - 'k': self.constants, + "p": self.parameters, + "k": self.constants, } - self._total_derivative_prototypes: \ - Dict[str, Dict[str, Union[str, List[str]]]] = { - 'sroot': { - 'eq': 'root', - 'chainvars': ['x'], - 'var': 'p', - 'dxdz_name': 'sx', - }, + self._total_derivative_prototypes: Dict[ + str, Dict[str, Union[str, List[str]]] + ] = { + "sroot": { + "eq": "root", + "chainvars": ["x"], + "var": "p", + "dxdz_name": "sx", + }, } self._lock_total_derivative: List[str] = list() self._simplify: Callable = simplify if cache_simplify and simplify is not None: + def cached_simplify( expr: sp.Expr, _simplified: Dict[str, sp.Expr] = {}, @@ -851,6 +860,7 @@ def cached_simplify( if expr_str not in _simplified: _simplified[expr_str] = _simplify(expr) return _simplified[expr_str] + self._simplify = cached_simplify self._x0_fixedParameters_idx: Union[None, Sequence[int]] self._w_recursion_depth: int = 0 @@ -859,7 +869,7 @@ def cached_simplify( self._code_printer = AmiciCxxCodePrinter() for fun in CUSTOM_FUNCTIONS: - self._code_printer.known_functions[fun['sympy']] = fun['c++'] + self._code_printer.known_functions[fun["sympy"]] = fun["c++"] def differential_states(self) -> List[DifferentialState]: """Get all differential states.""" @@ -925,11 +935,9 @@ def states(self) -> List[State]: """Get all states.""" return self._differential_states + self._algebraic_states - @log_execution_time('importing SbmlImporter', logger) + @log_execution_time("importing SbmlImporter", logger) def import_from_sbml_importer( - self, - si: 'sbml_import.SbmlImporter', - compute_cls: Optional[bool] = True + self, si: "sbml_import.SbmlImporter", compute_cls: Optional[bool] = True ) -> None: """ Imports a model specification from a @@ -941,13 +949,31 @@ def import_from_sbml_importer( whether to compute conservation laws """ + # add splines as expressions to the model + # saved for later substituting into the fluxes + spline_subs = {} + + for ispl, spl in enumerate(si.splines): + spline_expr = spl.ode_model_symbol(si) + spline_subs[spl.sbml_id] = spline_expr + self.add_component( + Expression( + identifier=spl.sbml_id, + name=str(spl.sbml_id), + value=spline_expr, + ) + ) + self.splines = si.splines + # get symbolic expression from SBML importers symbols = copy.copy(si.symbols) # assemble fluxes and add them as expressions to the model assert len(si.flux_ids) == len(si.flux_vector) - fluxes = [generate_flux_symbol(ir, name=flux_id) - for ir, flux_id in enumerate(si.flux_ids)] + fluxes = [ + generate_flux_symbol(ir, name=flux_id) + for ir, flux_id in enumerate(si.flux_ids) + ] # correct time derivatives for compartment changes def transform_dxdt_to_concentration(species_id, dxdt): @@ -976,9 +1002,9 @@ def transform_dxdt_to_concentration(species_id, dxdt): # volume, respectively. species = si.symbols[SymbolId.SPECIES][species_id] - comp = species['compartment'] + comp = species["compartment"] if comp in si.symbols[SymbolId.SPECIES]: - dv_dt = si.symbols[SymbolId.SPECIES][comp]['dt'] + dv_dt = si.symbols[SymbolId.SPECIES][comp]["dt"] xdot = (dxdt - dv_dt * species_id) / comp return xdot elif comp in si.compartment_assignment_rules: @@ -986,24 +1012,23 @@ def transform_dxdt_to_concentration(species_id, dxdt): # we need to flatten out assignments in the compartment in # order to ensure that we catch all species dependencies - v = smart_subs_dict(v, si.symbols[SymbolId.EXPRESSION], - 'value') - dv_dt = v.diff(si.amici_time_symbol) + v = smart_subs_dict(v, si.symbols[SymbolId.EXPRESSION], "value") + dv_dt = v.diff(amici_time_symbol) # we may end up with a time derivative of the compartment # volume due to parameter rate rules - comp_rate_vars = [p for p in v.free_symbols - if p in si.symbols[SymbolId.SPECIES]] + comp_rate_vars = [ + p for p in v.free_symbols if p in si.symbols[SymbolId.SPECIES] + ] for var in comp_rate_vars: - dv_dt += \ - v.diff(var) * si.symbols[SymbolId.SPECIES][var]['dt'] + dv_dt += v.diff(var) * si.symbols[SymbolId.SPECIES][var]["dt"] dv_dx = v.diff(species_id) xdot = (dxdt - dv_dt * species_id) / (dv_dx * species_id + v) return xdot elif comp in si.symbols[SymbolId.ALGEBRAIC_STATE]: raise SBMLException( - f'Species {species_id} is in a compartment {comp} that is' - f' defined by an algebraic equation. This is not' - f' supported.' + f"Species {species_id} is in a compartment {comp} that is" + f" defined by an algebraic equation. This is not" + f" supported." ) else: v = si.compartments[comp] @@ -1014,44 +1039,41 @@ def transform_dxdt_to_concentration(species_id, dxdt): return dxdt / v # create dynamics without respecting conservation laws first - dxdt = smart_multiply(si.stoichiometric_matrix, - MutableDenseMatrix(fluxes)) - for ix, ((species_id, species), formula) in enumerate(zip( - symbols[SymbolId.SPECIES].items(), - dxdt - )): + dxdt = smart_multiply(si.stoichiometric_matrix, MutableDenseMatrix(fluxes)) + for ix, ((species_id, species), formula) in enumerate( + zip(symbols[SymbolId.SPECIES].items(), dxdt) + ): # rate rules and amount species don't need to be updated - if 'dt' in species: + if "dt" in species: continue - if species['amount']: - species['dt'] = formula + if species["amount"]: + species["dt"] = formula else: - species['dt'] = transform_dxdt_to_concentration(species_id, - formula) + species["dt"] = transform_dxdt_to_concentration(species_id, formula) # create all basic components of the DE model and add them. for symbol_name in symbols: # transform dict of lists into a list of dicts - args = ['name', 'identifier'] + args = ["name", "identifier"] if symbol_name == SymbolId.SPECIES: - args += ['dt', 'init'] + args += ["dt", "init"] elif symbol_name == SymbolId.ALGEBRAIC_STATE: - args += ['init'] + args += ["init"] else: - args += ['value'] + args += ["value"] if symbol_name == SymbolId.EVENT: - args += ['state_update', 'initial_value'] + args += ["state_update", "initial_value"] elif symbol_name == SymbolId.OBSERVABLE: - args += ['transformation'] + args += ["transformation"] elif symbol_name == SymbolId.EVENT_OBSERVABLE: - args += ['event'] + args += ["event"] comp_kwargs = [ { - 'identifier': var_id, - **{k: v for k, v in var.items() if k in args} + "identifier": var_id, + **{k: v for k, v in var.items() if k in args}, } for var_id, var in symbols[symbol_name].items() ] @@ -1062,11 +1084,11 @@ def transform_dxdt_to_concentration(species_id, dxdt): # add fluxes as expressions, this needs to happen after base # expressions from symbols have been parsed for flux_id, flux in zip(fluxes, si.flux_vector): - self.add_component(Expression( - identifier=flux_id, - name=str(flux_id), - value=flux - )) + # replace splines inside fluxes + flux = flux.subs(spline_subs) + self.add_component( + Expression(identifier=flux_id, name=str(flux_id), value=flux) + ) # process conservation laws if compute_cls: @@ -1075,13 +1097,13 @@ def transform_dxdt_to_concentration(species_id, dxdt): # fill in 'self._sym' based on prototypes and components in ode_model self.generate_basic_variables() self._has_quadratic_nllh = all( - llh['dist'] in ['normal', 'lin-normal', 'log-normal', - 'log10-normal'] + llh["dist"] in ["normal", "lin-normal", "log-normal", "log10-normal"] for llh in si.symbols[SymbolId.LLHY].values() ) - def add_component(self, component: ModelQuantity, - insert_first: Optional[bool] = False) -> None: + def add_component( + self, component: ModelQuantity, insert_first: Optional[bool] = False + ) -> None: """ Adds a new ModelQuantity to the model. @@ -1093,28 +1115,45 @@ def add_component(self, component: ModelQuantity, may refer to other components of the same type. """ if type(component) not in { - Observable, Expression, Parameter, Constant, DifferentialState, - AlgebraicState, AlgebraicEquation, - LogLikelihoodY, LogLikelihoodZ, LogLikelihoodRZ, - SigmaY, SigmaZ, ConservationLaw, Event, EventObservable + Observable, + Expression, + Parameter, + Constant, + DifferentialState, + AlgebraicState, + AlgebraicEquation, + LogLikelihoodY, + LogLikelihoodZ, + LogLikelihoodRZ, + SigmaY, + SigmaZ, + ConservationLaw, + Event, + EventObservable, }: - raise ValueError(f'Invalid component type {type(component)}') + raise ValueError(f"Invalid component type {type(component)}") component_list = getattr( - self, '_' + '_'.join( - s.lower() for s in re.split(r"([A-Z][^A-Z]+)", - type(component).__name__) if s - ) + 's' + self, + "_" + + "_".join( + s.lower() + for s in re.split(r"([A-Z][^A-Z]+)", type(component).__name__) + if s + ) + + "s", ) if insert_first: component_list.insert(0, component) else: component_list.append(component) - def add_conservation_law(self, - state: sp.Symbol, - total_abundance: sp.Symbol, - coefficients: Dict[sp.Symbol, sp.Expr]) -> None: + def add_conservation_law( + self, + state: sp.Symbol, + total_abundance: sp.Symbol, + coefficients: Dict[sp.Symbol, sp.Expr], + ) -> None: r""" Adds a new conservation law to the model. A conservation law is defined by the conserved quantity :math:`T = \sum_i(a_i * x_i)`, where @@ -1132,18 +1171,24 @@ def add_conservation_law(self, Dictionary of coefficients {x_i: a_i} """ try: - ix = next(filter(lambda is_s: is_s[1].get_id() == state, - enumerate(self._differential_states)))[0] + ix = next( + filter( + lambda is_s: is_s[1].get_id() == state, + enumerate(self._differential_states), + ) + )[0] except StopIteration: - raise ValueError(f'Specified state {state} was not found in the ' - f'model states.') + raise ValueError( + f"Specified state {state} was not found in the " f"model states." + ) state_id = self._differential_states[ix].get_id() # \sum_{i≠j}(a_i * x_i)/a_j - target_expression = sp.Add(*( - c_i*x_i for x_i, c_i in coefficients.items() if x_i != state - )) / coefficients[state] + target_expression = ( + sp.Add(*(c_i * x_i for x_i, c_i in coefficients.items() if x_i != state)) + / coefficients[state] + ) # x_j = T/a_j - \sum_{i≠j}(a_i * x_i)/a_j state_expr = total_abundance - target_expression @@ -1152,13 +1197,15 @@ def add_conservation_law(self, abundance_expr = target_expression + state_id self.add_component( - Expression(state_id, str(state_id), state_expr), - insert_first=True + Expression(state_id, str(state_id), state_expr), insert_first=True ) cl = ConservationLaw( - total_abundance, f'total_{state_id}', abundance_expr, - coefficients, state_id + total_abundance, + f"total_{state_id}", + abundance_expr, + coefficients, + state_id, ) self.add_component(cl) @@ -1180,7 +1227,7 @@ def num_states_rdata(self) -> int: :return: number of state variable symbols """ - return len(self.sym('x_rdata')) + return len(self.sym("x_rdata")) def num_states_solver(self) -> int: """ @@ -1189,7 +1236,7 @@ def num_states_solver(self) -> int: :return: number of state variable symbols """ - return len(self.sym('x')) + return len(self.sym("x")) def num_cons_law(self) -> int: """ @@ -1208,8 +1255,8 @@ def num_state_reinits(self) -> int: :return: number of state variable symbols with reinitialization """ - reinit_states = self.eq('x0_fixedParameters') - solver_states = self.eq('x_solver') + reinit_states = self.eq("x0_fixedParameters") + solver_states = self.eq("x_solver") return sum(ix in solver_states for ix in reinit_states) def num_obs(self) -> int: @@ -1219,7 +1266,7 @@ def num_obs(self) -> int: :return: number of observable symbols """ - return len(self.sym('y')) + return len(self.sym("y")) def num_eventobs(self) -> int: """ @@ -1228,7 +1275,7 @@ def num_eventobs(self) -> int: :return: number of event observable symbols """ - return len(self.sym('z')) + return len(self.sym("z")) def num_const(self) -> int: """ @@ -1237,7 +1284,7 @@ def num_const(self) -> int: :return: number of constant symbols """ - return len(self.sym('k')) + return len(self.sym("k")) def num_par(self) -> int: """ @@ -1246,7 +1293,7 @@ def num_par(self) -> int: :return: number of parameter symbols """ - return len(self.sym('p')) + return len(self.sym("p")) def num_expr(self) -> int: """ @@ -1255,7 +1302,7 @@ def num_expr(self) -> int: :return: number of expression symbols """ - return len(self.sym('w')) + return len(self.sym("w")) def num_events(self) -> int: """ @@ -1264,7 +1311,7 @@ def num_events(self) -> int: :return: number of event symbols (length of the root vector in AMICI) """ - return len(self.sym('h')) + return len(self.sym("h")) def sym(self, name: str) -> sp.Matrix: """ @@ -1297,7 +1344,7 @@ def sparsesym(self, name: str, force_generate: bool = True) -> List[str]: linearized Matrix containing the symbolic identifiers """ if name not in sparse_functions: - raise ValueError(f'{name} is not marked as sparse') + raise ValueError(f"{name} is not marked as sparse") if name not in self._sparsesyms and force_generate: self._generate_sparse_symbol(name) return self._sparsesyms.get(name, []) @@ -1315,7 +1362,7 @@ def eq(self, name: str) -> sp.Matrix: """ if name not in self._eqs: - dec = log_execution_time(f'computing {name}', logger) + dec = log_execution_time(f"computing {name}", logger) dec(self._compute_equation)(name) return self._eqs[name] @@ -1331,13 +1378,12 @@ def sparseeq(self, name) -> sp.Matrix: linearized matrix containing the symbolic formulas """ if name not in sparse_functions: - raise ValueError(f'{name} is not marked as sparse') + raise ValueError(f"{name} is not marked as sparse") if name not in self._sparseeqs: self._generate_sparse_symbol(name) return self._sparseeqs[name] - def colptrs(self, name: str) -> Union[List[sp.Number], - List[List[sp.Number]]]: + def colptrs(self, name: str) -> Union[List[sp.Number], List[List[sp.Number]]]: """ Returns (and constructs if necessary) the column pointers for a sparsified symbolic variable. @@ -1349,13 +1395,12 @@ def colptrs(self, name: str) -> Union[List[sp.Number], list containing the column pointers """ if name not in sparse_functions: - raise ValueError(f'{name} is not marked as sparse') + raise ValueError(f"{name} is not marked as sparse") if name not in self._sparseeqs: self._generate_sparse_symbol(name) return self._colptrs[name] - def rowvals(self, name: str) -> Union[List[sp.Number], - List[List[sp.Number]]]: + def rowvals(self, name: str) -> Union[List[sp.Number], List[List[sp.Number]]]: """ Returns (and constructs if necessary) the row values for a sparsified symbolic variable. @@ -1367,7 +1412,7 @@ def rowvals(self, name: str) -> Union[List[sp.Number], list containing the row values """ if name not in sparse_functions: - raise ValueError(f'{name} is not marked as sparse') + raise ValueError(f"{name} is not marked as sparse") if name not in self._sparseeqs: self._generate_sparse_symbol(name) return self._rowvals[name] @@ -1407,10 +1452,12 @@ def free_symbols(self) -> Set[sp.Basic]: Returns list of free symbols that appear in RHS and initial conditions. """ - return set(chain.from_iterable( - state.get_free_symbols() - for state in self.states() + self.algebraic_equations() - )) + return set( + chain.from_iterable( + state.get_free_symbols() + for state in self.states() + self.algebraic_equations() + ) + ) def _generate_symbol(self, name: str) -> None: """ @@ -1421,91 +1468,115 @@ def _generate_symbol(self, name: str) -> None: """ if name in self._variable_prototype: components = self._variable_prototype[name]() - self._syms[name] = sp.Matrix([ - comp.get_id() - for comp in components - ]) - if name == 'y': - self._syms['my'] = sp.Matrix([ - comp.get_measurement_symbol() - for comp in components - ]) - if name == 'z': - self._syms['mz'] = sp.Matrix([ - comp.get_measurement_symbol() - for comp in components - ]) - self._syms['rz'] = sp.Matrix([ - comp.get_regularization_symbol() - for comp in components - ]) + self._syms[name] = sp.Matrix([comp.get_id() for comp in components]) + if name == "y": + self._syms["my"] = sp.Matrix( + [comp.get_measurement_symbol() for comp in components] + ) + if name == "z": + self._syms["mz"] = sp.Matrix( + [comp.get_measurement_symbol() for comp in components] + ) + self._syms["rz"] = sp.Matrix( + [comp.get_regularization_symbol() for comp in components] + ) return - elif name == 'x': - self._syms[name] = sp.Matrix([ - state.get_id() - for state in self.states() - if not state.has_conservation_law() - ]) + elif name == "x": + self._syms[name] = sp.Matrix( + [ + state.get_id() + for state in self.states() + if not state.has_conservation_law() + ] + ) return - elif name == 'xdot': - self._syms[name] = sp.Matrix([ - f'd{x.get_id()}dt' if self.is_ode() else f'de_{ix}' - for ix, x in enumerate(self._differential_states) - if not x.has_conservation_law() - ] + [ - f'ae_{ix}' - for ix in range(len(self._algebraic_equations)) - ]) + elif name == "xdot": + self._syms[name] = sp.Matrix( + [ + f"d{x.get_id()}dt" if self.is_ode() else f"de_{ix}" + for ix, x in enumerate(self._differential_states) + if not x.has_conservation_law() + ] + + [f"ae_{ix}" for ix in range(len(self._algebraic_equations))] + ) return - elif name == 'dx': - self._syms[name] = sp.Matrix([ - f'd{state.get_id()}dt' - for state in self.states() - if not state.has_conservation_law() - ]) + elif name == "dx": + self._syms[name] = sp.Matrix( + [ + f"d{state.get_id()}dt" + for state in self.states() + if not state.has_conservation_law() + ] + ) return - elif name == 'sx0': - self._syms[name] = sp.Matrix([ - f's{state.get_id()}_0' - for state in self.states() - if not state.has_conservation_law() - ]) + elif name == "sx0": + self._syms[name] = sp.Matrix( + [ + f"s{state.get_id()}_0" + for state in self.states() + if not state.has_conservation_law() + ] + ) return - elif name == 'sx_rdata': - self._syms[name] = sp.Matrix([ - f'sx_rdata_{i}' - for i in range(len(self.states())) - ]) + elif name == "sx_rdata": + self._syms[name] = sp.Matrix( + [f"sx_rdata_{i}" for i in range(len(self.states()))] + ) return - elif name == 'dtcldp': + elif name == "dtcldp": # check, whether the CL consists of only one state. Then, # sensitivities drop out, otherwise generate symbols - self._syms[name] = sp.Matrix([ - [sp.Symbol(f's{strip_pysb(tcl.get_id())}__' - f'{strip_pysb(par.get_id())}', real=True) - for par in self._parameters] - if self.conservation_law_has_multispecies(tcl) - else [0] * self.num_par() - for tcl in self._conservation_laws - ]) + self._syms[name] = sp.Matrix( + [ + [ + sp.Symbol( + f"s{strip_pysb(tcl.get_id())}__" + f"{strip_pysb(par.get_id())}", + real=True, + ) + for par in self._parameters + ] + if self.conservation_law_has_multispecies(tcl) + else [0] * self.num_par() + for tcl in self._conservation_laws + ] + ) return - elif name == 'xdot_old': - length = len(self.eq('xdot')) + elif name == "xdot_old": + length = len(self.eq("xdot")) elif name in sparse_functions: self._generate_sparse_symbol(name) return elif name in self._symboldim_funs: length = self._symboldim_funs[name]() - elif name == 'stau': + elif name == "stau": length = self.eq(name)[0].shape[1] elif name in sensi_functions: length = self.eq(name).shape[0] + elif name == "spl": + # placeholders for the numeric spline values. + # Need to create symbols + self._syms[name] = sp.Matrix( + [[f"spl_{isp}" for isp in range(len(self.splines))]] + ) + return + elif name == "sspl": + # placeholders for spline sensitivities. Need to create symbols + self._syms[name] = sp.Matrix( + [ + [f"sspl_{isp}_{ip}" for ip in range(len(self._syms["p"]))] + for isp in range(len(self.splines)) + ] + ) + return else: length = len(self.eq(name)) - self._syms[name] = sp.Matrix([ - sp.Symbol(f'{name}{0 if name == "stau" else i}', real=True) - for i in range(length) - ]) + self._syms[name] = sp.Matrix( + [ + sp.Symbol(f'{name}{0 if name == "stau" else i}', real=True) + for i in range(length) + ] + ) def generate_basic_variables(self) -> None: """ @@ -1519,8 +1590,11 @@ def generate_basic_variables(self) -> None: for var in self._variable_prototype: if var not in self._syms: self._generate_symbol(var) + # symbols for spline values need to be created in addition + for var in ["spl", "sspl"]: + self._generate_symbol(var) - self._generate_symbol('x') + self._generate_symbol("x") def parse_events(self) -> None: """ @@ -1562,26 +1636,23 @@ def get_appearance_counts(self, idxs: List[int]) -> List[int]: list of counts for the states ordered according to the provided indices """ - free_symbols_dt = list(itertools.chain.from_iterable( - [ - str(symbol) - for symbol in state.get_dt().free_symbols - ] - for state in self.states() - )) + free_symbols_dt = list( + itertools.chain.from_iterable( + [str(symbol) for symbol in state.get_dt().free_symbols] + for state in self.states() + ) + ) - free_symbols_expr = list(itertools.chain.from_iterable( - [ - str(symbol) - for symbol in expr.get_val().free_symbols - ] - for expr in self._expressions - )) + free_symbols_expr = list( + itertools.chain.from_iterable( + [str(symbol) for symbol in expr.get_val().free_symbols] + for expr in self._expressions + ) + ) return [ free_symbols_dt.count(str(self._differential_states[idx].get_id())) - + - free_symbols_expr.count(str(self._differential_states[idx].get_id())) + + free_symbols_expr.count(str(self._differential_states[idx].get_id())) for idx in idxs ] @@ -1603,7 +1674,7 @@ def _generate_sparse_symbol(self, name: str) -> None: rownames = self.sym(eq) colnames = self.sym(var) - if name == 'dJydy': + if name == "dJydy": # One entry per y-slice self._colptrs[name] = [] self._rowvals[name] = [] @@ -1612,21 +1683,36 @@ def _generate_sparse_symbol(self, name: str) -> None: self._syms[name] = [] for iy in range(self.num_obs()): - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, \ - sparse_matrix = self._code_printer.csc_matrix( - matrix[iy, :], rownames=rownames, colnames=colnames, - identifier=iy) + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = self._code_printer.csc_matrix( + matrix[iy, :], + rownames=rownames, + colnames=colnames, + identifier=iy, + ) self._colptrs[name].append(symbol_col_ptrs) self._rowvals[name].append(symbol_row_vals) self._sparseeqs[name].append(sparse_list) self._sparsesyms[name].append(symbol_list) self._syms[name].append(sparse_matrix) else: - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, \ - sparse_matrix = self._code_printer.csc_matrix( - matrix, rownames=rownames, colnames=colnames, - pattern_only=name in nobody_functions - ) + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = self._code_printer.csc_matrix( + matrix, + rownames=rownames, + colnames=colnames, + pattern_only=name in nobody_functions, + ) self._colptrs[name] = symbol_col_ptrs self._rowvals[name] = symbol_row_vals @@ -1644,99 +1730,107 @@ def _compute_equation(self, name: str) -> None: # replacement ensures that we don't have to adapt name in abstract # model and keep backwards compatibility with matlab match_deriv = DERIVATIVE_PATTERN.match( - re.sub(r'dJ(y|z|rz)dsigma', r'dJ\1dsigma\1', name) - .replace('sigmarz', 'sigmaz') - .replace('dJrzdz', 'dJrzdrz') + re.sub(r"dJ(y|z|rz)dsigma", r"dJ\1dsigma\1", name) + .replace("sigmarz", "sigmaz") + .replace("dJrzdz", "dJrzdrz") ) - time_symbol = sp.Matrix([symbol_with_assumptions('t')]) + time_symbol = sp.Matrix([amici_time_symbol]) if name in self._equation_prototype: self._equation_from_components(name, self._equation_prototype[name]()) elif name in self._total_derivative_prototypes: args = self._total_derivative_prototypes[name] - args['name'] = name - self._lock_total_derivative += args['chainvars'] + args["name"] = name + self._lock_total_derivative += args["chainvars"] self._total_derivative(**args) - for cv in args['chainvars']: + for cv in args["chainvars"]: self._lock_total_derivative.remove(cv) - elif name == 'xdot': + elif name == "xdot": if self.is_ode(): - self._eqs[name] = sp.Matrix([ - state.get_dt() for state in self._differential_states - if not state.has_conservation_law() - ]) + self._eqs[name] = sp.Matrix( + [ + state.get_dt() + for state in self._differential_states + if not state.has_conservation_law() + ] + ) else: - self._eqs[name] = sp.Matrix([ - x.get_dt() - dx - for x, dx in zip( - (s for s in self._differential_states - if not s.has_conservation_law()), - self.sym('dx') - ) - ] + [ - eq.get_val() - for eq in self._algebraic_equations - ]) - - elif name == 'x_rdata': - self._eqs[name] = sp.Matrix([ - state.get_x_rdata() - for state in self.states() - ]) + self._eqs[name] = sp.Matrix( + [ + x.get_dt() - dx + for x, dx in zip( + ( + s + for s in self._differential_states + if not s.has_conservation_law() + ), + self.sym("dx"), + ) + ] + + [eq.get_val() for eq in self._algebraic_equations] + ) - elif name == 'x_solver': - self._eqs[name] = sp.Matrix([ - state.get_id() - for state in self.states() - if not state.has_conservation_law() - ]) + elif name == "x_rdata": + self._eqs[name] = sp.Matrix( + [state.get_x_rdata() for state in self.states()] + ) - elif name == 'sx_solver': - self._eqs[name] = sp.Matrix([ - self.sym('sx_rdata')[ix] - for ix, state in enumerate(self.states()) - if not state.has_conservation_law() - ]) + elif name == "x_solver": + self._eqs[name] = sp.Matrix( + [ + state.get_id() + for state in self.states() + if not state.has_conservation_law() + ] + ) - elif name == 'sx0': - self._derivative(name[1:], 'p', name=name) + elif name == "sx_solver": + self._eqs[name] = sp.Matrix( + [ + self.sym("sx_rdata")[ix] + for ix, state in enumerate(self.states()) + if not state.has_conservation_law() + ] + ) + + elif name == "sx0": + self._derivative(name[1:], "p", name=name) - elif name == 'sx0_fixedParameters': + elif name == "sx0_fixedParameters": # deltax = -x+x0_fixedParameters if x0_fixedParameters>0 else 0 # deltasx = -sx+dx0_fixed_parametersdx*sx+dx0_fixedParametersdp # if x0_fixedParameters>0 else 0 # sx0_fixedParameters = sx+deltasx = # dx0_fixed_parametersdx*sx+dx0_fixedParametersdp self._eqs[name] = smart_jacobian( - self.eq('x0_fixedParameters'), self.sym('p') + self.eq("x0_fixedParameters"), self.sym("p") ) dx0_fixed_parametersdx = smart_jacobian( - self.eq('x0_fixedParameters'), self.sym('x') + self.eq("x0_fixedParameters"), self.sym("x") ) if not smart_is_zero_matrix(dx0_fixed_parametersdx): if isinstance(self._eqs[name], ImmutableDenseMatrix): self._eqs[name] = MutableDenseMatrix(self._eqs[name]) - tmp = smart_multiply(dx0_fixed_parametersdx, self.sym('sx0')) + tmp = smart_multiply(dx0_fixed_parametersdx, self.sym("sx0")) for ip in range(self._eqs[name].shape[1]): self._eqs[name][:, ip] += tmp - elif name == 'x0_fixedParameters': - k = self.sym('k') + elif name == "x0_fixedParameters": + k = self.sym("k") self._x0_fixedParameters_idx = [ ix - for ix, eq in enumerate(self.eq('x0')) + for ix, eq in enumerate(self.eq("x0")) if any(sym in eq.free_symbols for sym in k) ] - eq = self.eq('x0') - self._eqs[name] = sp.Matrix([eq[ix] for ix in - self._x0_fixedParameters_idx]) + eq = self.eq("x0") + self._eqs[name] = sp.Matrix([eq[ix] for ix in self._x0_fixedParameters_idx]) - elif name == 'dtotal_cldx_rdata': - x_rdata = self.sym('x_rdata') + elif name == "dtotal_cldx_rdata": + x_rdata = self.sym("x_rdata") self._eqs[name] = sp.Matrix( [ [cl.get_ncoeff(xr) for xr in x_rdata] @@ -1744,18 +1838,17 @@ def _compute_equation(self, name: str) -> None: ] ) - elif name == 'dtcldx': + elif name == "dtcldx": # this is always zero - self._eqs[name] = \ - sp.zeros(self.num_cons_law(), self.num_states_solver()) + self._eqs[name] = sp.zeros(self.num_cons_law(), self.num_states_solver()) - elif name == 'dtcldp': + elif name == "dtcldp": # force symbols self._eqs[name] = self.sym(name) - elif name == 'dx_rdatadx_solver': + elif name == "dx_rdatadx_solver": if self.num_cons_law(): - x_solver = self.sym('x') + x_solver = self.sym("x") self._eqs[name] = sp.Matrix( [ [state.get_dx_rdata_dx_solver(xs) for xs in x_solver] @@ -1766,49 +1859,71 @@ def _compute_equation(self, name: str) -> None: # so far, dx_rdatadx_solver is only required for sx_rdata # in case of no conservation laws, C++ code will directly use # sx, we don't need this - self._eqs[name] = \ - sp.zeros(self.num_states_rdata(), - self.num_states_solver()) + self._eqs[name] = sp.zeros( + self.num_states_rdata(), self.num_states_solver() + ) - elif name == 'dx_rdatadp': + elif name == "dx_rdatadp": if self.num_cons_law(): - self._eqs[name] = smart_jacobian(self.eq('x_rdata'), - self.sym('p')) + self._eqs[name] = smart_jacobian(self.eq("x_rdata"), self.sym("p")) else: # so far, dx_rdatadp is only required for sx_rdata # in case of no conservation laws, C++ code will directly use # sx, we don't need this - self._eqs[name] = \ - sp.zeros(self.num_states_rdata(), - self.num_par()) + self._eqs[name] = sp.zeros(self.num_states_rdata(), self.num_par()) - elif name == 'dx_rdatadtcl': - self._eqs[name] = smart_jacobian(self.eq('x_rdata'), - self.sym('tcl')) + elif name == "dx_rdatadtcl": + self._eqs[name] = smart_jacobian(self.eq("x_rdata"), self.sym("tcl")) - elif name == 'dxdotdx_explicit': + elif name == "dxdotdx_explicit": # force symbols - self._derivative('xdot', 'x', name=name) + self._derivative("xdot", "x", name=name) - elif name == 'dxdotdp_explicit': + elif name == "dxdotdp_explicit": # force symbols - self._derivative('xdot', 'p', name=name) + self._derivative("xdot", "p", name=name) - elif name == 'drootdt': - self._eqs[name] = smart_jacobian(self.eq('root'), time_symbol) + elif name == "spl": + self._eqs[name] = self.sym(name) - elif name == 'drootdt_total': + elif name == "sspl": + # force symbols + self._eqs[name] = self.sym(name) + + elif name == "spline_values": + # force symbols + self._eqs[name] = sp.Matrix( + [y for spline in self.splines for y in spline.values_at_nodes] + ) + + elif name == "spline_slopes": + # force symbols + self._eqs[name] = sp.Matrix( + [ + d + for spline in self.splines + for d in ( + sp.zeros(len(spline.derivatives_at_nodes), 1) + if spline.derivatives_by_fd + else spline.derivatives_at_nodes + ) + ] + ) + + elif name == "drootdt": + 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') + 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) + self._eqs[name] += smart_multiply(self.eq("drootdx"), tmp_xdot) - elif name == 'deltax': + elif name == "deltax": # fill boluses for Heaviside functions, as empty state updates # would cause problems when writing the function file later event_eqs = [] @@ -1820,29 +1935,23 @@ def _compute_equation(self, name: str) -> None: self._eqs[name] = event_eqs - elif name == 'z': - event_observables = [ - sp.zeros(self.num_eventobs(), 1) - for _ in self._events - ] - event_ids = [ - e.get_id() for e in self._events - ] + elif name == "z": + event_observables = [sp.zeros(self.num_eventobs(), 1) for _ in self._events] + event_ids = [e.get_id() for e in self._events] # TODO: get rid of this stupid 1-based indexing as soon as we can # the matlab interface z2event = [ event_ids.index(event_obs.get_event()) + 1 for event_obs in self._event_observables ] - for (iz, ie), event_obs in zip(enumerate(z2event), - self._event_observables): - event_observables[ie-1][iz] = event_obs.get_val() + for (iz, ie), event_obs in zip(enumerate(z2event), self._event_observables): + event_observables[ie - 1][iz] = event_obs.get_val() self._eqs[name] = event_observables self._z2event = z2event - elif name in ['ddeltaxdx', 'ddeltaxdp', 'ddeltaxdt', 'dzdp', 'dzdx']: - if match_deriv[2] == 't': + elif name in ["ddeltaxdx", "ddeltaxdp", "ddeltaxdt", "dzdp", "dzdx"]: + if match_deriv[2] == "t": var = time_symbol else: var = self.sym(match_deriv[2]) @@ -1851,128 +1960,128 @@ def _compute_equation(self, name: str) -> None: smart_jacobian(self.eq(match_deriv[1])[ie], var) for ie in range(self.num_events()) ] - if name == 'dzdx': + if name == "dzdx": for ie in range(self.num_events()): - dtaudx = -self.eq('drootdx')[ie, :] / \ - self.eq('drootdt_total')[ie] + dtaudx = -self.eq("drootdx")[ie, :] / self.eq("drootdt_total")[ie] for iz in range(self.num_eventobs()): - if ie != self._z2event[iz]-1: + if ie != self._z2event[iz] - 1: continue - dzdt = sp.diff(self.eq('z')[ie][iz], time_symbol) + dzdt = sp.diff(self.eq("z")[ie][iz], time_symbol) self._eqs[name][ie][iz, :] += dzdt * dtaudx - elif name in ['rz', 'drzdx', 'drzdp']: + elif name in ["rz", "drzdx", "drzdp"]: eq_events = [] for ie in range(self.num_events()): val = sp.zeros( self.num_eventobs(), - 1 if name == 'rz' else len(self.sym(match_deriv[2])) + 1 if name == "rz" else len(self.sym(match_deriv[2])), ) # match event observables to root function for iz in range(self.num_eventobs()): - if ie == self._z2event[iz]-1: - val[iz, :] = self.eq(name.replace('rz', 'root'))[ie, :] + if ie == self._z2event[iz] - 1: + val[iz, :] = self.eq(name.replace("rz", "root"))[ie, :] eq_events.append(val) self._eqs[name] = eq_events - elif name == 'stau': + elif name == "stau": self._eqs[name] = [ - -self.eq('sroot')[ie, :] / self.eq('drootdt_total')[ie] - if not self.eq('drootdt_total')[ie].is_zero else - sp.zeros(*self.eq('sroot')[ie, :].shape) + -self.eq("sroot")[ie, :] / self.eq("drootdt_total")[ie] + if not self.eq("drootdt_total")[ie].is_zero + else sp.zeros(*self.eq("sroot")[ie, :].shape) for ie in range(self.num_events()) ] - elif name == 'deltasx': + elif name == "deltasx": + if self.num_states_solver() * self.num_par() == 0: + self._eqs[name] = [] + return + event_eqs = [] for ie, event in enumerate(self._events): - tmp_eq = sp.zeros(self.num_states_solver(), self.num_par()) # need to check if equations are zero since we are using # symbols - if not smart_is_zero_matrix(self.eq('stau')[ie]): + if not smart_is_zero_matrix(self.eq("stau")[ie]) \ + and not smart_is_zero_matrix(self.eq("xdot")): tmp_eq += smart_multiply( - (self.sym('xdot_old') - self.sym('xdot')), - self.sym('stau').T) + self.sym("xdot_old") - self.sym("xdot"), + self.sym("stau").T, + ) # only add deltax part if there is state update if event._state_update is not None: # partial derivative for the parameters - tmp_eq += self.eq('ddeltaxdp')[ie] + tmp_eq += self.eq("ddeltaxdp")[ie] # initial part of chain rule state variables - tmp_dxdp = self.sym('sx') * sp.ones(1, self.num_par()) + tmp_dxdp = self.sym("sx") * sp.ones(1, self.num_par()) # need to check if equations are zero since we are using # symbols - if not smart_is_zero_matrix(self.eq('stau')[ie]): + if not smart_is_zero_matrix(self.eq("stau")[ie]): # chain rule for the time point - tmp_eq += smart_multiply(self.eq('ddeltaxdt')[ie], - self.sym('stau').T) + tmp_eq += smart_multiply( + self.eq("ddeltaxdt")[ie], self.sym("stau").T + ) # additional part of chain rule state variables - # This part only works if we use self.eq('xdot') - # instead of self.sym('xdot'). Not immediately clear - # why that is. - tmp_dxdp += smart_multiply(self.eq('xdot'), - self.sym('stau').T) + tmp_dxdp += smart_multiply(self.sym("xdot_old"), self.sym("stau").T) # finish chain rule for the state variables - tmp_eq += smart_multiply(self.eq('ddeltaxdx')[ie], - tmp_dxdp) + tmp_eq += smart_multiply(self.eq("ddeltaxdx")[ie], tmp_dxdp) event_eqs.append(tmp_eq) self._eqs[name] = event_eqs - elif name == 'xdot_old': + elif name == "xdot_old": # force symbols self._eqs[name] = self.sym(name) - elif name == 'dwdx': - x = self.sym('x') - self._eqs[name] = sp.Matrix([ - [-cl.get_ncoeff(xs) for xs in x] - # the insert first in ode_model._add_conservation_law() means - # that we need to reverse the order here - for cl in reversed(self._conservation_laws) - ]).col_join(smart_jacobian(self.eq('w')[self.num_cons_law():, :], - x)) + elif name == "dwdx": + x = self.sym("x") + self._eqs[name] = sp.Matrix( + [ + [-cl.get_ncoeff(xs) for xs in x] + # the insert first in ode_model._add_conservation_law() means + # that we need to reverse the order here + for cl in reversed(self._conservation_laws) + ] + ).col_join(smart_jacobian(self.eq("w")[self.num_cons_law() :, :], x)) elif match_deriv: self._derivative(match_deriv[1], match_deriv[2], name) else: - raise ValueError(f'Unknown equation {name}') + raise ValueError(f"Unknown equation {name}") - if name == 'root': + if name == "root": # Events are processed after the model has been set up. # Equations are there, but symbols for roots must be added - self.sym('h') + self.sym("h") - if name in {'Jy', 'dydx'}: + if name in {"Jy", "dydx"}: # do not transpose if we compute the partial derivative as part of # a total derivative if not len(self._lock_total_derivative): self._eqs[name] = self._eqs[name].transpose() - if name in {'dzdx', 'drzdx'}: - self._eqs[name] = [ - e.T for e in self._eqs[name] - ] + if name in {"dzdx", "drzdx"}: + self._eqs[name] = [e.T for e in self._eqs[name]] if self._simplify: - dec = log_execution_time(f'simplifying {name}', logger) + dec = log_execution_time(f"simplifying {name}", logger) if isinstance(self._eqs[name], list): self._eqs[name] = [ dec(_parallel_applyfunc)(sub_eq, self._simplify) for sub_eq in self._eqs[name] ] else: - self._eqs[name] = dec(_parallel_applyfunc)(self._eqs[name], - self._simplify) + self._eqs[name] = dec(_parallel_applyfunc)( + self._eqs[name], self._simplify + ) def sym_names(self) -> List[str]: """ @@ -1998,25 +2107,23 @@ def _derivative(self, eq: str, var: str, name: str = None) -> None: name of resulting symbolic variable, default is ``d{eq}d{var}`` """ if not name: - name = f'd{eq}d{var}' + name = f"d{eq}d{var}" ignore_chainrule = { - ('xdot', 'p'): 'w', # has generic implementation in c++ code - ('xdot', 'x'): 'w', # has generic implementation in c++ code - ('w', 'w'): 'tcl', # dtcldw = 0 - ('w', 'x'): 'tcl', # dtcldx = 0 + ("xdot", "p"): "w", # has generic implementation in c++ code + ("xdot", "x"): "w", # has generic implementation in c++ code + ("w", "w"): "tcl", # dtcldw = 0 + ("w", "x"): "tcl", # dtcldx = 0 } # automatically detect chainrule chainvars = [ - cv for cv in ['w', 'tcl'] + cv + for cv in ["w", "tcl"] if var_in_function_signature(eq, cv, self.is_ode()) - and cv not in self._lock_total_derivative - and var != cv - and min(self.sym(cv).shape) - and ( - (eq, var) not in ignore_chainrule - or ignore_chainrule[(eq, var)] != cv - ) + and cv not in self._lock_total_derivative + and var != cv + and min(self.sym(cv).shape) + and ((eq, var) not in ignore_chainrule or ignore_chainrule[(eq, var)] != cv) ] if len(chainvars): self._lock_total_derivative += chainvars @@ -2026,7 +2133,7 @@ def _derivative(self, eq: str, var: str, name: str = None) -> None: return # partial derivative - sym_eq = self.eq(eq).transpose() if eq == 'Jy' else self.eq(eq) + sym_eq = self.eq(eq).transpose() if eq == "Jy" else self.eq(eq) sym_var = self.sym(var) @@ -2036,7 +2143,7 @@ def _derivative(self, eq: str, var: str, name: str = None) -> None: # compute recursion depth based on nilpotency of jacobian. computing # nilpotency can be done more efficiently on numerical sparsity pattern - if name == 'dwdw': + if name == "dwdw": nonzeros = np.asarray( derivative.applyfunc(lambda x: int(not x.is_zero)) ).astype(np.int64) @@ -2047,14 +2154,14 @@ def _derivative(self, eq: str, var: str, name: str = None) -> None: self._w_recursion_depth += 1 if self._w_recursion_depth > len(sym_eq): raise RuntimeError( - 'dwdw is not nilpotent. Something, somewhere went ' - 'terribly wrong. Please file a bug report at ' - 'https://github.com/AMICI-dev/AMICI/issues and ' - 'attach this model.' + "dwdw is not nilpotent. Something, somewhere went " + "terribly wrong. Please file a bug report at " + "https://github.com/AMICI-dev/AMICI/issues and " + "attach this model." ) - if name == 'dydw' and not smart_is_zero_matrix(derivative): - dwdw = self.eq('dwdw') + if name == "dydw" and not smart_is_zero_matrix(derivative): + dwdw = self.eq("dwdw") # h(k) = d{eq}dw*dwdw^k* (k=1) h = smart_multiply(derivative, dwdw) while not smart_is_zero_matrix(h): @@ -2062,9 +2169,15 @@ def _derivative(self, eq: str, var: str, name: str = None) -> None: # h(k+1) = d{eq}dw*dwdw^(k+1) = h(k)*dwdw h = smart_multiply(h, dwdw) - def _total_derivative(self, name: str, eq: str, chainvars: List[str], - var: str, dydx_name: str = None, - dxdz_name: str = None) -> None: + def _total_derivative( + self, + name: str, + eq: str, + chainvars: List[str], + var: str, + dydx_name: str = None, + dxdz_name: str = None, + ) -> None: """ Creates a new symbolic variable according to a total derivative using the chain rule @@ -2097,7 +2210,7 @@ def _total_derivative(self, name: str, eq: str, chainvars: List[str], # Dydz = dydx*dxdz + dydz # initialize with partial derivative dydz without chain rule - self._eqs[name] = self.sym_or_eq(name, f'd{eq}d{var}') + self._eqs[name] = self.sym_or_eq(name, f"d{eq}d{var}") if not isinstance(self._eqs[name], sp.Symbol): # if not a Symbol, create a copy using sympy API # NB deepcopy does not work safely, see sympy issue #7672 @@ -2105,19 +2218,17 @@ def _total_derivative(self, name: str, eq: str, chainvars: List[str], for chainvar in chainvars: if dydx_name is None: - dydx_name = f'd{eq}d{chainvar}' + dydx_name = f"d{eq}d{chainvar}" if dxdz_name is None: - dxdz_name = f'd{chainvar}d{var}' + dxdz_name = f"d{chainvar}d{var}" dydx = self.sym_or_eq(name, dydx_name) dxdz = self.sym_or_eq(name, dxdz_name) # Save time for large models if one multiplicand is zero, # which is not checked for by sympy - if not smart_is_zero_matrix(dydx) and not \ - smart_is_zero_matrix(dxdz): + if not smart_is_zero_matrix(dydx) and not smart_is_zero_matrix(dxdz): dydx_times_dxdz = smart_multiply(dydx, dxdz) - if dxdz.shape[1] == 1 and \ - self._eqs[name].shape[1] != dxdz.shape[1]: + if dxdz.shape[1] == 1 and self._eqs[name].shape[1] != dxdz.shape[1]: for iz in range(self._eqs[name].shape[1]): self._eqs[name][:, iz] += dydx_times_dxdz else: @@ -2143,15 +2254,22 @@ def sym_or_eq(self, name: str, varname: str) -> sp.Matrix: # within a column may differ from the initialization of symbols here, # so those are not safe to use. Not removing them from signature as # this would break backwards compatibility. - if var_in_function_signature(name, varname, self.is_ode()) \ - and varname not in ['dwdx', 'dwdp']: + if var_in_function_signature(name, varname, self.is_ode()) and varname not in [ + "dwdx", + "dwdp", + ]: return self.sym(varname) else: return self.eq(varname) - def _multiplication(self, name: str, x: str, y: str, - transpose_x: Optional[bool] = False, - sign: Optional[int] = 1): + def _multiplication( + self, + name: str, + x: str, + y: str, + transpose_x: Optional[bool] = False, + sign: Optional[int] = 1, + ): """ Creates a new symbolic variable according to a multiplication @@ -2172,7 +2290,7 @@ def _multiplication(self, name: str, x: str, y: str, defines the sign of the product, should be +1 or -1 """ if sign not in [-1, 1]: - raise TypeError(f'sign must be +1 or -1, was {sign}') + raise TypeError(f"sign must be +1 or -1, was {sign}") variables = { varname: self.sym(varname) @@ -2186,8 +2304,9 @@ def _multiplication(self, name: str, x: str, y: str, self._eqs[name] = sign * smart_multiply(xx, yy) - def _equation_from_components(self, name: str, - components: List[ModelQuantity]) -> None: + def _equation_from_components( + self, name: str, components: List[ModelQuantity] + ) -> None: """ Generates the formulas of a symbolic variable from the attributes @@ -2197,9 +2316,7 @@ def _equation_from_components(self, name: str, :param component: name of the attribute """ - self._eqs[name] = sp.Matrix( - [comp.get_val() for comp in components] - ) + self._eqs[name] = sp.Matrix([comp.get_val() for comp in components]) def get_conservation_laws(self) -> List[Tuple[sp.Symbol, sp.Expr]]: """Returns a list of states with conservation law set @@ -2224,10 +2341,9 @@ def _generate_value(self, name: str) -> None: if name in self._value_prototype: components = self._value_prototype[name]() else: - raise ValueError(f'No values for {name}') + raise ValueError(f"No values for {name}") - self._vals[name] = [comp.get_val() - for comp in components] + self._vals[name] = [comp.get_val() for comp in components] def _generate_name(self, name: str) -> None: """ @@ -2242,10 +2358,9 @@ def _generate_name(self, name: str) -> None: elif name in self._equation_prototype: components = self._equation_prototype[name]() else: - raise ValueError(f'No names for {name}') + raise ValueError(f"No names for {name}") - self._names[name] = [comp.get_name() - for comp in components] + self._names[name] = [comp.get_name() for comp in components] def state_has_fixed_parameter_initial_condition(self, ix: int) -> bool: """ @@ -2263,8 +2378,7 @@ def state_has_fixed_parameter_initial_condition(self, ix: int) -> bool: if not isinstance(ic, sp.Basic): return False return any( - fp in (c.get_id() for c in self._constants) - for fp in ic.free_symbols + fp in (c.get_id() for c in self._constants) for fp in ic.free_symbols ) def state_has_conservation_law(self, ix: int) -> bool: @@ -2312,8 +2426,7 @@ def state_is_constant(self, ix: int) -> bool: return state.get_dt() == 0.0 - def conservation_law_has_multispecies(self, - tcl: ConservationLaw) -> bool: + def conservation_law_has_multispecies(self, tcl: ConservationLaw) -> bool: """ Checks whether a conservation law has multiple species or it just defines one constant species @@ -2324,7 +2437,7 @@ def conservation_law_has_multispecies(self, :return: boolean indicating if conservation_law is not None """ - state_set = set(self.sym('x_rdata')) + state_set = set(self.sym("x_rdata")) n_species = len(state_set.intersection(tcl.get_val().free_symbols)) return n_species > 1 @@ -2342,7 +2455,7 @@ def _expr_is_time_dependent(self, expr: sp.Expr) -> bool: expr_syms = {str(sym) for sym in expr.free_symbols} # Check if the time variable is in the expression. - if 't' in expr_syms: + if "t" in expr_syms: return True # Check if any time-dependent states are in the expression. @@ -2353,9 +2466,9 @@ def _expr_is_time_dependent(self, expr: sp.Expr) -> bool: ) def _get_unique_root( - self, - root_found: sp.Expr, - roots: List[Event], + self, + root_found: sp.Expr, + roots: List[Event], ) -> Union[sp.Symbol, None]: """ Collects roots of Heaviside functions and events and stores them in @@ -2379,18 +2492,20 @@ def _get_unique_root( return root.get_id() # create an event for a new root function - root_symstr = f'Heaviside_{len(roots)}' - roots.append(Event( - identifier=sp.Symbol(root_symstr), - name=root_symstr, - value=root_found, - state_update=None, - )) + root_symstr = f"Heaviside_{len(roots)}" + roots.append( + Event( + identifier=sp.Symbol(root_symstr), + name=root_symstr, + value=root_found, + state_update=None, + ) + ) return roots[-1].get_id() def _collect_heaviside_roots( - self, - args: Sequence[sp.Expr], + self, + args: Sequence[sp.Expr], ) -> List[sp.Expr]: """ Recursively checks an expression for the occurrence of Heaviside @@ -2413,21 +2528,22 @@ def _collect_heaviside_roots( # substitute 'w' expressions into root expressions now, to avoid # rewriting 'root.cpp' and 'stau.cpp' headers # to include 'w.h' - w_sorted = toposort_symbols(dict(zip( - [expr.get_id() for expr in self._expressions], - [expr.get_val() for expr in self._expressions], - ))) - root_funs = [ - r.subs(w_sorted) - for r in root_funs - ] + w_sorted = toposort_symbols( + dict( + zip( + [expr.get_id() for expr in self._expressions], + [expr.get_val() for expr in self._expressions], + ) + ) + ) + root_funs = [r.subs(w_sorted) for r in root_funs] return root_funs def _process_heavisides( - self, - dxdt: sp.Expr, - roots: List[Event], + self, + dxdt: sp.Expr, + roots: List[Event], ) -> sp.Expr: """ Parses the RHS of a state variable, checks for Heaviside functions, @@ -2459,7 +2575,7 @@ def _process_heavisides( if tmp_new is None: continue # For Heavisides, we need to add the negative function as well - self._get_unique_root(sp.sympify(- tmp_old), roots) + self._get_unique_root(sp.sympify(-tmp_old), roots) heavisides.append((sp.Heaviside(tmp_old), tmp_new)) if heavisides: @@ -2522,15 +2638,15 @@ class DEExporter: """ def __init__( - self, - de_model: DEModel, - outdir: Optional[Union[Path, str]] = None, - verbose: Optional[Union[bool, int]] = False, - assume_pow_positivity: Optional[bool] = False, - compiler: Optional[str] = None, - allow_reinit_fixpar_initcond: Optional[bool] = True, - generate_sensitivity_code: Optional[bool] = True, - model_name: Optional[str] = 'model' + self, + de_model: DEModel, + outdir: Optional[Union[Path, str]] = None, + verbose: Optional[Union[bool, int]] = False, + assume_pow_positivity: Optional[bool] = False, + compiler: Optional[str] = None, + allow_reinit_fixpar_initcond: Optional[bool] = True, + generate_sensitivity_code: Optional[bool] = True, + model_name: Optional[str] = "model", ): """ Generate AMICI C++ files for the DE provided to the constructor. @@ -2539,7 +2655,7 @@ def __init__( DE model definition :param outdir: - see :meth:`amici.ode_export.DEExporter.set_paths` + see :meth:`amici.de_export.DEExporter.set_paths` :param verbose: verbosity level for logging, ``True``/``False`` default to @@ -2554,7 +2670,7 @@ def __init__( python extension :param allow_reinit_fixpar_initcond: - see :class:`amici.ode_export.DEExporter` + see :class:`amici.de_export.DEExporter` :param generate_sensitivity_code: specifies whether code required for sensitivity computation will be @@ -2569,8 +2685,8 @@ def __init__( self.assume_pow_positivity: bool = assume_pow_positivity self.compiler: str = compiler - self.model_path: str = '' - self.model_swig_path: str = '' + self.model_path: str = "" + self.model_swig_path: str = "" self.set_name(model_name) self.set_paths(outdir) @@ -2578,6 +2694,9 @@ def __init__( # Signatures and properties of generated model functions (see # include/amici/model.h for details) self.model: DEModel = de_model + self.model._code_printer.known_functions.update( + splines.spline_user_functions(self.model.splines, self._get_index("p")) + ) # To only generate a subset of functions, apply subselection here self.functions: Dict[str, _FunctionInfo] = copy.deepcopy(functions) @@ -2586,26 +2705,23 @@ def __init__( self._build_hints = set() self.generate_sensitivity_code: bool = generate_sensitivity_code - @log_execution_time('generating cpp code', logger) + @log_execution_time("generating cpp code", logger) def generate_model_code(self) -> None: """ Generates the native C++ code for the loaded model and a Matlab script that can be run to compile a mex file from the C++ code """ - with _monkeypatched(sp.Pow, '_eval_derivative', - _custom_pow_eval_derivative): - + with _monkeypatched(sp.Pow, "_eval_derivative", _custom_pow_eval_derivative): self._prepare_model_folder() self._generate_c_code() self._generate_m_code() - @log_execution_time('compiling cpp code', logger) + @log_execution_time("compiling cpp code", logger) def compile_model(self) -> None: """ Compiles the generated code it into a simulatable module """ - self._compile_c_code(compiler=self.compiler, - verbose=self.verbose) + self._compile_c_code(compiler=self.compiler, verbose=self.verbose) def _prepare_model_folder(self) -> None: """ @@ -2625,26 +2741,28 @@ def _generate_c_code(self) -> None: :attribute:`DEExporter.model`. """ for func_name, func_info in self.functions.items(): - if func_name in sensi_functions + sparse_sensi_functions and \ - not self.generate_sensitivity_code: + if ( + func_name in sensi_functions + sparse_sensi_functions + and not self.generate_sensitivity_code + ): continue if func_info.generate_body: - dec = log_execution_time(f'writing {func_name}.cpp', logger) + dec = log_execution_time(f"writing {func_name}.cpp", logger) dec(self._write_function_file)(func_name) if func_name in sparse_functions and func_info.body: - self._write_function_index(func_name, 'colptrs') - self._write_function_index(func_name, 'rowvals') + self._write_function_index(func_name, "colptrs") + self._write_function_index(func_name, "rowvals") for name in self.model.sym_names(): # only generate for those that have nontrivial implementation, # check for both basic variables (not in functions) and function # computed values - if (name in self.functions + if ( + name in self.functions and not self.functions[name].body - and name not in nobody_functions) \ - or (name not in self.functions and - len(self.model.sym(name)) == 0): + and name not in nobody_functions + ) or (name not in self.functions and len(self.model.sym(name)) == 0): continue self._write_index_files(name) @@ -2655,12 +2773,13 @@ def _generate_c_code(self) -> None: self._write_swig_files() self._write_module_setup() - shutil.copy(CXX_MAIN_TEMPLATE_FILE, - os.path.join(self.model_path, 'main.cpp')) + shutil.copy(CXX_MAIN_TEMPLATE_FILE, os.path.join(self.model_path, "main.cpp")) - def _compile_c_code(self, - verbose: Optional[Union[bool, int]] = False, - compiler: Optional[str] = None) -> None: + def _compile_c_code( + self, + verbose: Optional[Union[bool, int]] = False, + compiler: Optional[str] = None, + ) -> None: """ Compile the generated model code @@ -2673,44 +2792,48 @@ def _compile_c_code(self, """ # setup.py assumes it is run from within the model directory module_dir = self.model_path - script_args = [sys.executable, os.path.join(module_dir, 'setup.py')] + script_args = [sys.executable, os.path.join(module_dir, "setup.py")] if verbose: - script_args.append('--verbose') + script_args.append("--verbose") else: - script_args.append('--quiet') - - script_args.extend([ - 'build_ext', - f'--build-lib={module_dir}', - # This is generally not required, but helps to reduce the path - # length of intermediate build files, that may easily become - # problematic on Windows, due to its ridiculous 255-character path - # length limit. - f'--build-temp={Path(module_dir, "build")}', - ]) + script_args.append("--quiet") + + script_args.extend( + [ + "build_ext", + f"--build-lib={module_dir}", + # This is generally not required, but helps to reduce the path + # length of intermediate build files, that may easily become + # problematic on Windows, due to its ridiculous 255-character path + # length limit. + f'--build-temp={Path(module_dir, "build")}', + ] + ) if compiler is not None: - script_args.extend([f'--compiler={compiler}']) + script_args.extend([f"--compiler={compiler}"]) # distutils.core.run_setup looks nicer, but does not let us check the # result easily try: - result = subprocess.run(script_args, - cwd=module_dir, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - check=True) + result = subprocess.run( + script_args, + cwd=module_dir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ) except subprocess.CalledProcessError as e: - print(e.output.decode('utf-8')) + print(e.output.decode("utf-8")) print("Failed building the model extension.") if self._build_hints: print("Note:") - print('\n'.join(self._build_hints)) + print("\n".join(self._build_hints)) raise if verbose: - print(result.stdout.decode('utf-8')) + print(result.stdout.decode("utf-8")) def _generate_m_code(self) -> None: """ @@ -2726,26 +2849,42 @@ def _generate_m_code(self) -> None: o2flag = 0 lines = [ - '% This compile script was automatically created from' - ' Python SBML import.', - '% If mex compiler is set up within MATLAB, it can be run' - ' from MATLAB ', - '% in order to compile a mex-file from the Python' - ' generated C++ files.', - '', + "% This compile script was automatically created from" + " Python SBML import.", + "% If mex compiler is set up within MATLAB, it can be run" " from MATLAB ", + "% in order to compile a mex-file from the Python" " generated C++ files.", + "", f"modelName = '{self.model_name}';", "amimodel.compileAndLinkModel(modelName, '', [], [], [], []);", f"amimodel.generateMatlabWrapper({nxtrue_rdata}, " f"{nytrue}, {self.model.num_par()}, " f"{self.model.num_const()}, {nztrue}, {o2flag}, ...", " [], ['simulate_' modelName '.m'], modelName, ...", - " 'lin', 1, 1);" + " 'lin', 1, 1);", ] # write compile script (for mex) - compile_script = os.path.join(self.model_path, 'compileMexFile.m') - with open(compile_script, 'w') as fileout: - fileout.write('\n'.join(lines)) + compile_script = os.path.join(self.model_path, "compileMexFile.m") + with open(compile_script, "w") as fileout: + fileout.write("\n".join(lines)) + + def _get_index(self, name: str) -> Dict[sp.Symbol, int]: + """ + Compute indices for a symbolic array. + :param name: + key in self.model._syms for which to obtain the index. + :return: + a dictionary of symbol/index pairs. + """ + if name in self.model.sym_names(): + if name in sparse_functions: + symbols = self.model.sparsesym(name) + else: + symbols = self.model.sym(name).T + else: + raise ValueError(f"Unknown symbolic array: {name}") + + return {strip_pysb(symbol).name: index for index, symbol in enumerate(symbols)} def _write_index_files(self, name: str) -> None: """ @@ -2756,10 +2895,15 @@ def _write_index_files(self, name: str) -> None: be written """ if name not in self.model.sym_names(): - raise ValueError(f'Unknown symbolic array: {name}') + raise ValueError(f"Unknown symbolic array: {name}") - symbols = self.model.sparsesym(name) if name in sparse_functions \ + symbols = ( + self.model.sparsesym(name) + if name in sparse_functions else self.model.sym(name).T + ) + if not len(symbols): + return # flatten multiobs if isinstance(next(iter(symbols), None), list): @@ -2768,18 +2912,18 @@ def _write_index_files(self, name: str) -> None: lines = [] for index, symbol in enumerate(symbols): symbol_name = strip_pysb(symbol) - if str(symbol) == '0': + if str(symbol) == "0": continue - if str(symbol_name) == '': + if str(symbol_name) == "": raise ValueError(f'{name} contains a symbol called ""') - lines.append(f'#define {symbol_name} {name}[{index}]') - if name == 'stau': + lines.append(f"#define {symbol_name} {name}[{index}]") + if name == "stau": # we only need a single macro, as all entries have the same symbol break - filename = os.path.join(self.model_path, f'{name}.h') - with open(filename, 'w') as fileout: - fileout.write('\n'.join(lines)) + filename = os.path.join(self.model_path, f"{name}.h") + with open(filename, "w") as fileout: + fileout.write("\n".join(lines)) def _write_function_file(self, function: str) -> None: """ @@ -2794,10 +2938,14 @@ def _write_function_file(self, function: str) -> None: # need in subsequent steps if function in sparse_functions: equations = self.model.sparseeq(function) - elif not self.allow_reinit_fixpar_initcond \ - and function == 'sx0_fixedParameters': + elif ( + not self.allow_reinit_fixpar_initcond and function == "sx0_fixedParameters" + ): # Not required. Will create empty function body. equations = sp.Matrix() + elif function == "create_splines": + # nothing to do + pass else: equations = self.model.eq(function) @@ -2806,11 +2954,13 @@ def _write_function_file(self, function: str) -> None: '#include "amici/symbolic_functions.h"', '#include "amici/defines.h"', '#include "sundials/sundials_types.h"', - '', - '#include ', - '#include ', - '' + "", + "#include ", + "#include ", + "", ] + if function == "create_splines": + lines += ['#include "amici/splinefunctions.h"', "#include "] func_info = self.functions[function] @@ -2819,8 +2969,8 @@ def _write_function_file(self, function: str) -> None: # Unfortunately we cannot check for `self.functions[sym].body` # here since it may not have been generated yet. for sym in re.findall( - r'const (?:realtype|double) \*([\w]+)[0]*(?:,|$)', - func_info.arguments(self.model.is_ode()) + r"const (?:realtype|double) \*([\w]+)[0]*(?:,|$)", + func_info.arguments(self.model.is_ode()), ): if sym not in self.model.sym_names(): continue @@ -2838,56 +2988,60 @@ def _write_function_file(self, function: str) -> None: lines.append(f'#include "{sym}.h"') # include return symbols - if function in self.model.sym_names() and \ - function not in non_unique_id_symbols: + if function in self.model.sym_names() and function not in non_unique_id_symbols: lines.append(f'#include "{function}.h"') - lines.extend([ - '', - 'namespace amici {', - f'namespace model_{self.model_name} {{', - '', - f'{func_info.return_type} {function}_{self.model_name}' - f'({func_info.arguments(self.model.is_ode())}){{' - ]) + lines.extend( + [ + "", + "namespace amici {", + f"namespace model_{self.model_name} {{", + "", + f"{func_info.return_type} {function}_{self.model_name}" + f"({func_info.arguments(self.model.is_ode())}){{", + ] + ) # function body - body = self._get_function_body(function, equations) + if function == "create_splines": + body = self._get_create_splines_body() + else: + body = self._get_function_body(function, equations) if not body: return if self.assume_pow_positivity and func_info.assume_pow_positivity: - pow_rx = re.compile(r'(^|\W)std::pow\(') + pow_rx = re.compile(r"(^|\W)std::pow\(") body = [ # execute this twice to catch cases where the ending '(' would # be the starting (^|\W) for the following match - pow_rx.sub(r'\1amici::pos_pow(', - pow_rx.sub(r'\1amici::pos_pow(', line)) + pow_rx.sub(r"\1amici::pos_pow(", pow_rx.sub(r"\1amici::pos_pow(", line)) for line in body ] self.functions[function].body = body lines += body - lines.extend([ - '}', - '', - f'}} // namespace model_{self.model_name}', - '} // namespace amici\n', - ]) + lines.extend( + [ + "}", + "", + f"}} // namespace model_{self.model_name}", + "} // namespace amici\n", + ] + ) # check custom functions for fun in CUSTOM_FUNCTIONS: - if 'include' in fun and any(fun['c++'] in line for line in lines): - if 'build_hint' in fun: - self._build_hints.add(fun['build_hint']) - lines.insert(0, fun['include']) + if "include" in fun and any(fun["c++"] in line for line in lines): + if "build_hint" in fun: + self._build_hints.add(fun["build_hint"]) + lines.insert(0, fun["include"]) # if not body is None: - filename = os.path.join(self.model_path, - f'{function}.cpp') - with open(filename, 'w') as fileout: - fileout.write('\n'.join(lines)) + filename = os.path.join(self.model_path, f"{function}.cpp") + with open(filename, "w") as fileout: + fileout.write("\n".join(lines)) def _write_function_index(self, function: str, indextype: str) -> None: """ @@ -2900,32 +3054,34 @@ def _write_function_index(self, function: str, indextype: str) -> None: :param indextype: type of index {'colptrs', 'rowvals'} """ - if indextype == 'colptrs': + if indextype == "colptrs": values = self.model.colptrs(function) - setter = 'indexptrs' - elif indextype == 'rowvals': + setter = "indexptrs" + elif indextype == "rowvals": values = self.model.rowvals(function) - setter = 'indexvals' + setter = "indexvals" else: - raise ValueError('Invalid value for indextype, must be colptrs or ' - f'rowvals: {indextype}') + raise ValueError( + "Invalid value for indextype, must be colptrs or " + f"rowvals: {indextype}" + ) # function signature if function in multiobs_functions: - signature = f'(SUNMatrixWrapper &{function}, int index)' + signature = f"(SUNMatrixWrapper &{function}, int index)" else: - signature = f'(SUNMatrixWrapper &{function})' + signature = f"(SUNMatrixWrapper &{function})" lines = [ '#include "amici/sundials_matrix_wrapper.h"', '#include "sundials/sundials_types.h"', - '', - '#include ', - '#include ', - '', - 'namespace amici {', - f'namespace model_{self.model_name} {{', - '', + "", + "#include ", + "#include ", + "", + "namespace amici {", + f"namespace model_{self.model_name} {{", + "", ] # Generate static array with indices @@ -2938,23 +3094,30 @@ def _write_function_index(self, function: str, indextype: str) -> None: f"{len(values[0])}>, {len(values)}> " f"{static_array_name} = {{{{" ) - lines.extend([' {' - + ', '.join(map(str, index_vector)) + '}, ' - for index_vector in values]) + lines.extend( + [ + " {" + ", ".join(map(str, index_vector)) + "}, " + for index_vector in values + ] + ) lines.append("}};") else: # single index vector - lines.extend([ - "static constexpr std::array {static_array_name} = {{", - ' ' + ', '.join(map(str, values)), - "};" - ]) - - lines.extend([ - '', - f'void {function}_{indextype}_{self.model_name}{signature}{{', - ]) + lines.extend( + [ + "static constexpr std::array {static_array_name} = {{", + " " + ", ".join(map(str, values)), + "};", + ] + ) + + lines.extend( + [ + "", + f"void {function}_{indextype}_{self.model_name}{signature}{{", + ] + ) if len(values): if function in multiobs_functions: @@ -2968,24 +3131,21 @@ def _write_function_index(self, function: str, indextype: str) -> None: f"(gsl::make_span({static_array_name}));" ) - lines.extend([ - '}' - '', - f'}} // namespace model_{self.model_name}', - '} // namespace amici\n', - ]) + lines.extend( + [ + "}" "", + f"}} // namespace model_{self.model_name}", + "} // namespace amici\n", + ] + ) - filename = f'{function}_{indextype}.cpp' + filename = f"{function}_{indextype}.cpp" filename = os.path.join(self.model_path, filename) - with open(filename, 'w') as fileout: - fileout.write('\n'.join(lines)) + with open(filename, "w") as fileout: + fileout.write("\n".join(lines)) - def _get_function_body( - self, - function: str, - equations: sp.Matrix - ) -> List[str]: + def _get_function_body(self, function: str, equations: sp.Matrix) -> List[str]: """ Generate C++ code for body of function ``function``. @@ -3000,88 +3160,85 @@ def _get_function_body( """ lines = [] - if ( - len(equations) == 0 - or ( - isinstance(equations, (sp.Matrix, sp.ImmutableDenseMatrix)) - and min(equations.shape) == 0 - ) + if len(equations) == 0 or ( + isinstance(equations, (sp.Matrix, sp.ImmutableDenseMatrix)) + and min(equations.shape) == 0 ): # dJydy is a list return lines if not self.allow_reinit_fixpar_initcond and function in { - 'sx0_fixedParameters', - 'x0_fixedParameters', + "sx0_fixedParameters", + "x0_fixedParameters", }: return lines - if function == 'sx0_fixedParameters': + if function == "sx0_fixedParameters": # here we only want to overwrite values where x0_fixedParameters # was applied - lines.extend([ - # Keep list of indices of fixed parameters occurring in x0 - " static const std::array _x0_fixedParameters_idxs = {", - " " - + ', '.join(str(x) - for x in self.model._x0_fixedParameters_idx), - " };", - "", - # Set all parameters that are to be reset to 0, so that the - # switch statement below only needs to handle non-zero entries - # (which usually reduces file size and speeds up - # compilation significantly). - " for(auto idx: reinitialization_state_idxs) {", - " if(std::find(_x0_fixedParameters_idxs.cbegin(), " - "_x0_fixedParameters_idxs.cend(), idx) != " - "_x0_fixedParameters_idxs.cend())\n" - " sx0_fixedParameters[idx] = 0.0;", - " }" - ]) + lines.extend( + [ + # Keep list of indices of fixed parameters occurring in x0 + " static const std::array _x0_fixedParameters_idxs = {", + " " + + ", ".join(str(x) for x in self.model._x0_fixedParameters_idx), + " };", + "", + # Set all parameters that are to be reset to 0, so that the + # switch statement below only needs to handle non-zero entries + # (which usually reduces file size and speeds up + # compilation significantly). + " for(auto idx: reinitialization_state_idxs) {", + " if(std::find(_x0_fixedParameters_idxs.cbegin(), " + "_x0_fixedParameters_idxs.cend(), idx) != " + "_x0_fixedParameters_idxs.cend())\n" + " sx0_fixedParameters[idx] = 0.0;", + " }", + ] + ) cases = {} 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] ): if not formula.is_zero: - expressions.extend([ - f'if(std::find(' - 'reinitialization_state_idxs.cbegin(), ' - f'reinitialization_state_idxs.cend(), {index}) != ' - 'reinitialization_state_idxs.cend())', - f' {function}[{index}] = ' - f'{self.model._code_printer.doprint(formula)};' - ]) + expressions.extend( + [ + f"if(std::find(" + "reinitialization_state_idxs.cbegin(), " + f"reinitialization_state_idxs.cend(), {index}) != " + "reinitialization_state_idxs.cend())", + f" {function}[{index}] = " + f"{self.model._code_printer.doprint(formula)};", + ] + ) cases[ipar] = expressions - lines.extend(get_switch_statement('ip', cases, 1)) + lines.extend(get_switch_statement("ip", cases, 1)) - elif function == 'x0_fixedParameters': - for index, formula in zip( - self.model._x0_fixedParameters_idx, - equations - ): + elif function == "x0_fixedParameters": + for index, formula in zip(self.model._x0_fixedParameters_idx, equations): lines.append( - f' if(std::find(reinitialization_state_idxs.cbegin(), ' - f'reinitialization_state_idxs.cend(), {index}) != ' - 'reinitialization_state_idxs.cend())\n ' - f'{function}[{index}] = ' - f'{self.model._code_printer.doprint(formula)};' + f" if(std::find(reinitialization_state_idxs.cbegin(), " + f"reinitialization_state_idxs.cend(), {index}) != " + "reinitialization_state_idxs.cend())\n " + f"{function}[{index}] = " + f"{self.model._code_printer.doprint(formula)};" ) elif function in event_functions: cases = { ie: self.model._code_printer._get_sym_lines_array( - equations[ie], function, 0) + equations[ie], function, 0 + ) for ie in range(self.model.num_events()) if not smart_is_zero_matrix(equations[ie]) } - lines.extend(get_switch_statement('ie', cases, 1)) + lines.extend(get_switch_statement("ie", cases, 1)) elif function in event_sensi_functions: outer_cases = {} @@ -3089,185 +3246,274 @@ def _get_function_body( inner_lines = [] inner_cases = { ipar: self.model._code_printer._get_sym_lines_array( - inner_equations[:, ipar], function, 0) + inner_equations[:, ipar], function, 0 + ) for ipar in range(self.model.num_par()) if not smart_is_zero_matrix(inner_equations[:, ipar]) } - inner_lines.extend(get_switch_statement( - 'ip', inner_cases, 0)) + inner_lines.extend(get_switch_statement("ip", inner_cases, 0)) outer_cases[ie] = copy.copy(inner_lines) - lines.extend(get_switch_statement('ie', outer_cases, 1)) + lines.extend(get_switch_statement("ie", outer_cases, 1)) - elif function in sensi_functions \ - and equations.shape[1] == self.model.num_par(): + elif function in sensi_functions and equations.shape[1] == self.model.num_par(): cases = { ipar: self.model._code_printer._get_sym_lines_array( - equations[:, ipar], function, 0) + equations[:, ipar], function, 0 + ) for ipar in range(self.model.num_par()) if not smart_is_zero_matrix(equations[:, ipar]) } - lines.extend(get_switch_statement('ip', cases, 1)) + lines.extend(get_switch_statement("ip", cases, 1)) elif function in multiobs_functions: - if function == 'dJydy': + if function == "dJydy": cases = { iobs: self.model._code_printer._get_sym_lines_array( - equations[iobs], function, 0) + equations[iobs], function, 0 + ) for iobs in range(self.model.num_obs()) if not smart_is_zero_matrix(equations[iobs]) } else: cases = { iobs: self.model._code_printer._get_sym_lines_array( - equations[:, iobs], function, 0) + equations[:, iobs], function, 0 + ) for iobs in range(equations.shape[1]) if not smart_is_zero_matrix(equations[:, iobs]) } - if function.startswith(('Jz', 'dJz', 'Jrz', 'dJrz')): - iterator = 'iz' + if function.startswith(("Jz", "dJz", "Jrz", "dJrz")): + iterator = "iz" else: - iterator = 'iy' + iterator = "iy" lines.extend(get_switch_statement(iterator, cases, 1)) - elif function in self.model.sym_names() \ - and function not in non_unique_id_symbols: + elif ( + function in self.model.sym_names() and function not in non_unique_id_symbols + ): if function in sparse_functions: symbols = list(map(sp.Symbol, self.model.sparsesym(function))) else: symbols = self.model.sym(function) lines += self.model._code_printer._get_sym_lines_symbols( - symbols, equations, function, 4) + symbols, equations, function, 4 + ) else: lines += self.model._code_printer._get_sym_lines_array( - equations, function, 4) + equations, function, 4 + ) return [line for line in lines if line] + def _get_create_splines_body(self): + if not self.model.splines: + return [" return {};"] + + ind4 = " " * 4 + ind8 = " " * 8 + + body = ["return {"] + for ispl, spline in enumerate(self.model.splines): + if isinstance(spline.nodes, splines.UniformGrid): + nodes = f"{ind8}{{{spline.nodes.start}, {spline.nodes.stop}}}, " + else: + nodes = f"{ind8}{{{', '.join(map(str, spline.nodes))}}}, " + + # vector with the node values + values = f"{ind8}{{{', '.join(map(str, spline.values_at_nodes))}}}, " + # vector with the slopes + if spline.derivatives_by_fd: + slopes = f"{ind8}{{}}," + else: + slopes = ( + f"{ind8}{{{', '.join(map(str, spline.derivatives_at_nodes))}}}," + ) + + body.extend( + [ + f"{ind4}HermiteSpline(", + nodes, + values, + slopes, + ] + ) + + bc_to_cpp = { + None: "SplineBoundaryCondition::given, ", + "zeroderivative": "SplineBoundaryCondition::zeroDerivative, ", + "natural": "SplineBoundaryCondition::natural, ", + "zeroderivative+natural": "SplineBoundaryCondition::naturalZeroDerivative, ", + "periodic": "SplineBoundaryCondition::periodic, ", + } + for bc in spline.bc: + try: + body.append(ind8 + bc_to_cpp[bc]) + except KeyError: + raise ValueError( + f"Unknown boundary condition '{bc}' " "found in spline object" + ) + extrapolate_to_cpp = { + None: "SplineExtrapolation::noExtrapolation, ", + "polynomial": "SplineExtrapolation::polynomial, ", + "constant": "SplineExtrapolation::constant, ", + "linear": "SplineExtrapolation::linear, ", + "periodic": "SplineExtrapolation::periodic, ", + } + for extr in spline.extrapolate: + try: + body.append(ind8 + extrapolate_to_cpp[extr]) + except KeyError: + raise ValueError( + f"Unknown extrapolation '{extr}' " "found in spline object" + ) + line = ind8 + line += "true, " if spline.derivatives_by_fd else "false, " + line += ( + "true, " if isinstance(spline.nodes, splines.UniformGrid) else "false, " + ) + line += "true" if spline.logarithmic_parametrization else "false" + body.append(line) + body.append(f"{ind4}),") + + body.append("};") + return [" " + line for line in body] + def _write_wrapfunctions_cpp(self) -> None: """ Write model-specific 'wrapper' file (``wrapfunctions.cpp``). """ - template_data = {'MODELNAME': self.model_name} + template_data = {"MODELNAME": self.model_name} apply_template( - os.path.join(amiciSrcPath, 'wrapfunctions.template.cpp'), - os.path.join(self.model_path, 'wrapfunctions.cpp'), - template_data + os.path.join(amiciSrcPath, "wrapfunctions.template.cpp"), + os.path.join(self.model_path, "wrapfunctions.cpp"), + template_data, ) def _write_wrapfunctions_header(self) -> None: """ Write model-specific header file (``wrapfunctions.h``). """ - template_data = {'MODELNAME': str(self.model_name)} + template_data = {"MODELNAME": str(self.model_name)} apply_template( - os.path.join(amiciSrcPath, 'wrapfunctions.template.h'), - os.path.join(self.model_path, 'wrapfunctions.h'), - template_data + os.path.join(amiciSrcPath, "wrapfunctions.template.h"), + os.path.join(self.model_path, "wrapfunctions.h"), + template_data, ) def _write_model_header_cpp(self) -> None: """ Write model-specific header and cpp file (MODELNAME.{h,cpp}). """ - model_type = 'ODE' if self.model.is_ode() else 'DAE' + model_type = "ODE" if self.model.is_ode() else "DAE" tpl_data = { - 'MODEL_TYPE_LOWER': model_type.lower(), - 'MODEL_TYPE_UPPER': model_type, - 'MODELNAME': self.model_name, - 'NX_RDATA': self.model.num_states_rdata(), - 'NXTRUE_RDATA': self.model.num_states_rdata(), - 'NX_SOLVER': self.model.num_states_solver(), - 'NXTRUE_SOLVER': self.model.num_states_solver(), - 'NX_SOLVER_REINIT': self.model.num_state_reinits(), - 'NY': self.model.num_obs(), - 'NYTRUE': self.model.num_obs(), - 'NZ': self.model.num_eventobs(), - 'NZTRUE': self.model.num_eventobs(), - 'NEVENT': self.model.num_events(), - 'NOBJECTIVE': '1', - 'NW': len(self.model.sym('w')), - 'NDWDP': len(self.model.sparsesym( - 'dwdp', force_generate=self.generate_sensitivity_code - )), - 'NDWDX': len(self.model.sparsesym('dwdx')), - 'NDWDW': len(self.model.sparsesym('dwdw')), - 'NDXDOTDW': len(self.model.sparsesym('dxdotdw')), - 'NDXDOTDP_EXPLICIT': len(self.model.sparsesym( - 'dxdotdp_explicit', - force_generate=self.generate_sensitivity_code - )), - 'NDXDOTDX_EXPLICIT': len(self.model.sparsesym( - 'dxdotdx_explicit')), - 'NDJYDY': 'std::vector{%s}' - % ','.join(str(len(x)) - for x in self.model.sparsesym('dJydy')), - 'NDXRDATADXSOLVER': len(self.model.sparsesym('dx_rdatadx_solver')), - 'NDXRDATADTCL': len(self.model.sparsesym('dx_rdatadtcl')), - 'NDTOTALCLDXRDATA': len(self.model.sparsesym('dtotal_cldx_rdata')), - 'UBW': self.model.num_states_solver(), - 'LBW': self.model.num_states_solver(), - 'NP': self.model.num_par(), - 'NK': self.model.num_const(), - 'O2MODE': 'amici::SecondOrderMode::none', + "MODEL_TYPE_LOWER": model_type.lower(), + "MODEL_TYPE_UPPER": model_type, + "MODELNAME": self.model_name, + "NX_RDATA": self.model.num_states_rdata(), + "NXTRUE_RDATA": self.model.num_states_rdata(), + "NX_SOLVER": self.model.num_states_solver(), + "NXTRUE_SOLVER": self.model.num_states_solver(), + "NX_SOLVER_REINIT": self.model.num_state_reinits(), + "NY": self.model.num_obs(), + "NYTRUE": self.model.num_obs(), + "NZ": self.model.num_eventobs(), + "NZTRUE": self.model.num_eventobs(), + "NEVENT": self.model.num_events(), + "NOBJECTIVE": "1", + "NSPL": len(self.model.splines), + "NW": len(self.model.sym("w")), + "NDWDP": len( + self.model.sparsesym( + "dwdp", force_generate=self.generate_sensitivity_code + ) + ), + "NDWDX": len(self.model.sparsesym("dwdx")), + "NDWDW": len(self.model.sparsesym("dwdw")), + "NDXDOTDW": len(self.model.sparsesym("dxdotdw")), + "NDXDOTDP_EXPLICIT": len( + self.model.sparsesym( + "dxdotdp_explicit", + force_generate=self.generate_sensitivity_code, + ) + ), + "NDXDOTDX_EXPLICIT": len(self.model.sparsesym("dxdotdx_explicit")), + "NDJYDY": "std::vector{%s}" + % ",".join(str(len(x)) for x in self.model.sparsesym("dJydy")), + "NDXRDATADXSOLVER": len(self.model.sparsesym("dx_rdatadx_solver")), + "NDXRDATADTCL": len(self.model.sparsesym("dx_rdatadtcl")), + "NDTOTALCLDXRDATA": len(self.model.sparsesym("dtotal_cldx_rdata")), + "UBW": self.model.num_states_solver(), + "LBW": self.model.num_states_solver(), + "NP": self.model.num_par(), + "NK": self.model.num_const(), + "O2MODE": "amici::SecondOrderMode::none", # using code printer ensures proper handling of nan/inf - 'PARAMETERS': self.model._code_printer.doprint( - self.model.val('p'))[1:-1], - 'FIXED_PARAMETERS': self.model._code_printer.doprint( - self.model.val('k'))[1:-1], - 'PARAMETER_NAMES_INITIALIZER_LIST': - self._get_symbol_name_initializer_list('p'), - 'STATE_NAMES_INITIALIZER_LIST': - self._get_symbol_name_initializer_list('x_rdata'), - 'FIXED_PARAMETER_NAMES_INITIALIZER_LIST': - self._get_symbol_name_initializer_list('k'), - 'OBSERVABLE_NAMES_INITIALIZER_LIST': - self._get_symbol_name_initializer_list('y'), - 'OBSERVABLE_TRAFO_INITIALIZER_LIST': - '\n'.join( - f'ObservableScaling::{trafo.value}, // y[{idx}]' - for idx, trafo in enumerate( - self.model.get_observable_transformations() - ) - ), - 'EXPRESSION_NAMES_INITIALIZER_LIST': - self._get_symbol_name_initializer_list('w'), - 'PARAMETER_IDS_INITIALIZER_LIST': - self._get_symbol_id_initializer_list('p'), - 'STATE_IDS_INITIALIZER_LIST': - self._get_symbol_id_initializer_list('x_rdata'), - 'FIXED_PARAMETER_IDS_INITIALIZER_LIST': - self._get_symbol_id_initializer_list('k'), - 'OBSERVABLE_IDS_INITIALIZER_LIST': - self._get_symbol_id_initializer_list('y'), - 'EXPRESSION_IDS_INITIALIZER_LIST': - self._get_symbol_id_initializer_list('w'), - 'STATE_IDXS_SOLVER_INITIALIZER_LIST': - ', '.join( - str(idx) - for idx, state in enumerate(self.model.states()) - if not state.has_conservation_law() - ), - 'REINIT_FIXPAR_INITCOND': - AmiciCxxCodePrinter.print_bool( - self.allow_reinit_fixpar_initcond), - 'AMICI_VERSION_STRING': __version__, - 'AMICI_COMMIT_STRING': __commit__, - 'W_RECURSION_DEPTH': self.model._w_recursion_depth, - 'QUADRATIC_LLH': AmiciCxxCodePrinter.print_bool( - self.model._has_quadratic_nllh), - 'ROOT_INITIAL_VALUES': - ', '.join(map( + "PARAMETERS": self.model._code_printer.doprint(self.model.val("p"))[1:-1], + "FIXED_PARAMETERS": self.model._code_printer.doprint(self.model.val("k"))[ + 1:-1 + ], + "PARAMETER_NAMES_INITIALIZER_LIST": self._get_symbol_name_initializer_list( + "p" + ), + "STATE_NAMES_INITIALIZER_LIST": self._get_symbol_name_initializer_list( + "x_rdata" + ), + "FIXED_PARAMETER_NAMES_INITIALIZER_LIST": self._get_symbol_name_initializer_list( + "k" + ), + "OBSERVABLE_NAMES_INITIALIZER_LIST": self._get_symbol_name_initializer_list( + "y" + ), + "OBSERVABLE_TRAFO_INITIALIZER_LIST": "\n".join( + f"ObservableScaling::{trafo.value}, // y[{idx}]" + for idx, trafo in enumerate(self.model.get_observable_transformations()) + ), + "EXPRESSION_NAMES_INITIALIZER_LIST": self._get_symbol_name_initializer_list( + "w" + ), + "PARAMETER_IDS_INITIALIZER_LIST": self._get_symbol_id_initializer_list("p"), + "STATE_IDS_INITIALIZER_LIST": self._get_symbol_id_initializer_list( + "x_rdata" + ), + "FIXED_PARAMETER_IDS_INITIALIZER_LIST": self._get_symbol_id_initializer_list( + "k" + ), + "OBSERVABLE_IDS_INITIALIZER_LIST": self._get_symbol_id_initializer_list( + "y" + ), + "EXPRESSION_IDS_INITIALIZER_LIST": self._get_symbol_id_initializer_list( + "w" + ), + "STATE_IDXS_SOLVER_INITIALIZER_LIST": ", ".join( + str(idx) + for idx, state in enumerate(self.model.states()) + if not state.has_conservation_law() + ), + "REINIT_FIXPAR_INITCOND": AmiciCxxCodePrinter.print_bool( + self.allow_reinit_fixpar_initcond + ), + "AMICI_VERSION_STRING": __version__, + "AMICI_COMMIT_STRING": __commit__, + "W_RECURSION_DEPTH": self.model._w_recursion_depth, + "QUADRATIC_LLH": AmiciCxxCodePrinter.print_bool( + self.model._has_quadratic_nllh + ), + "ROOT_INITIAL_VALUES": ", ".join( + map( lambda event: AmiciCxxCodePrinter.print_bool( - event.get_initial_value()), - self.model.events())), - 'Z2EVENT': - ', '.join(map(str, self.model._z2event)), - 'ID': - ', '.join(( + event.get_initial_value() + ), + self.model.events(), + ) + ), + "Z2EVENT": ", ".join(map(str, self.model._z2event)), + "ID": ", ".join( + ( str(float(isinstance(s, DifferentialState))) for s in self.model.states() if not s.has_conservation_law() - )) + ) + ), } for func_name, func_info in self.functions.items(): @@ -3275,71 +3521,87 @@ def _write_model_header_cpp(self) -> None: continue if not func_info.body: - tpl_data[f'{func_name.upper()}_DEF'] = '' + tpl_data[f"{func_name.upper()}_DEF"] = "" - if func_name in sensi_functions + sparse_sensi_functions and \ - not self.generate_sensitivity_code: - impl = '' + if ( + func_name in sensi_functions + sparse_sensi_functions + and not self.generate_sensitivity_code + ): + impl = "" else: impl = get_model_override_implementation( - func_name, self.model_name, self.model.is_ode(), - nobody=True + func_name, + self.model_name, + self.model.is_ode(), + nobody=True, ) - tpl_data[f'{func_name.upper()}_IMPL'] = impl + tpl_data[f"{func_name.upper()}_IMPL"] = impl if func_name in sparse_functions: - for indexfield in ['colptrs', 'rowvals']: - if func_name in sparse_sensi_functions and \ - not self.generate_sensitivity_code: - impl = '' + for indexfield in ["colptrs", "rowvals"]: + if ( + func_name in sparse_sensi_functions + and not self.generate_sensitivity_code + ): + impl = "" else: impl = get_sunindex_override_implementation( - func_name, self.model_name, indexfield, - nobody=True + func_name, + self.model_name, + indexfield, + nobody=True, ) - tpl_data[f'{func_name.upper()}_{indexfield.upper()}_DEF'] \ - = '' - tpl_data[f'{func_name.upper()}_{indexfield.upper()}_IMPL'] \ - = impl + tpl_data[f"{func_name.upper()}_{indexfield.upper()}_DEF"] = "" + tpl_data[ + f"{func_name.upper()}_{indexfield.upper()}_IMPL" + ] = 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()}_IMPL'] = \ - get_model_override_implementation(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() + ) 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_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_IMPL'] = \ - get_sunindex_override_implementation( - func_name, self.model_name, 'rowvals') + 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()}_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" + ) if self.model.num_states_solver() == self.model.num_states_rdata(): - tpl_data['X_RDATA_DEF'] = '' - tpl_data['X_RDATA_IMPL'] = '' + tpl_data["X_RDATA_DEF"] = "" + tpl_data["X_RDATA_IMPL"] = "" tpl_data = {k: str(v) for k, v in tpl_data.items()} apply_template( - os.path.join(amiciSrcPath, 'model_header.template.h'), - os.path.join(self.model_path, f'{self.model_name}.h'), - tpl_data + os.path.join(amiciSrcPath, "model_header.template.h"), + os.path.join(self.model_path, f"{self.model_name}.h"), + tpl_data, ) apply_template( - os.path.join(amiciSrcPath, 'model.template.cpp'), - os.path.join(self.model_path, f'{self.model_name}.cpp'), - tpl_data + os.path.join(amiciSrcPath, "model.template.cpp"), + os.path.join(self.model_path, f"{self.model_name}.cpp"), + tpl_data, ) def _get_symbol_name_initializer_list(self, name: str) -> str: @@ -3353,7 +3615,7 @@ def _get_symbol_name_initializer_list(self, name: str) -> str: :return: Template initializer list of names """ - return '\n'.join( + return "\n".join( f'"{symbol}", // {name}[{idx}]' for idx, symbol in enumerate(self.model.name(name)) ) @@ -3369,59 +3631,74 @@ def _get_symbol_id_initializer_list(self, name: str) -> str: :return: Template initializer list of ids """ - return '\n'.join( + return "\n".join( f'"{self.model._code_printer.doprint(symbol)}", // {name}[{idx}]' for idx, symbol in enumerate(self.model.sym(name)) ) def _write_c_make_file(self): """Write CMake ``CMakeLists.txt`` file for this model.""" - sources = '\n'.join( - f + ' ' for f in os.listdir(self.model_path) - if f.endswith(('.cpp', '.h'),) and f != 'main.cpp' + sources = "\n".join( + f + " " + for f in os.listdir(self.model_path) + if f.endswith( + (".cpp", ".h"), + ) + and f != "main.cpp" ) - template_data = {'MODELNAME': self.model_name, - 'SOURCES': sources, - 'AMICI_VERSION': __version__} + template_data = { + "MODELNAME": self.model_name, + "SOURCES": sources, + "AMICI_VERSION": __version__, + } apply_template( MODEL_CMAKE_TEMPLATE_FILE, - Path(self.model_path, 'CMakeLists.txt'), - template_data + Path(self.model_path, "CMakeLists.txt"), + template_data, ) def _write_swig_files(self) -> None: """Write SWIG interface files for this model.""" Path(self.model_swig_path).mkdir(exist_ok=True) - template_data = {'MODELNAME': self.model_name} + template_data = {"MODELNAME": self.model_name} apply_template( - Path(amiciSwigPath, 'modelname.template.i'), - Path(self.model_swig_path, self.model_name + '.i'), - template_data + Path(amiciSwigPath, "modelname.template.i"), + Path(self.model_swig_path, self.model_name + ".i"), + template_data, + ) + shutil.copy( + SWIG_CMAKE_TEMPLATE_FILE, + Path(self.model_swig_path, "CMakeLists.txt"), ) - shutil.copy(SWIG_CMAKE_TEMPLATE_FILE, - Path(self.model_swig_path, 'CMakeLists.txt')) def _write_module_setup(self) -> None: """ Create a setuptools ``setup.py`` file for compile the model module. """ - template_data = {'MODELNAME': self.model_name, - 'AMICI_VERSION': __version__, - 'PACKAGE_VERSION': '0.1.0'} - apply_template(Path(amiciModulePath, 'setup.template.py'), - Path(self.model_path, 'setup.py'), - template_data) - apply_template(Path(amiciModulePath, 'MANIFEST.template.in'), - Path(self.model_path, 'MANIFEST.in'), {}) + template_data = { + "MODELNAME": self.model_name, + "AMICI_VERSION": __version__, + "PACKAGE_VERSION": "0.1.0", + } + apply_template( + Path(amiciModulePath, "setup.template.py"), + Path(self.model_path, "setup.py"), + template_data, + ) + apply_template( + Path(amiciModulePath, "MANIFEST.template.in"), + Path(self.model_path, "MANIFEST.in"), + {}, + ) # write __init__.py for the model module Path(self.model_path, self.model_name).mkdir(exist_ok=True) apply_template( - Path(amiciModulePath, '__init__.template.py'), - Path(self.model_path, self.model_name, '__init__.py'), - template_data + Path(amiciModulePath, "__init__.template.py"), + Path(self.model_path, self.model_name, "__init__.py"), + template_data, ) def set_paths(self, output_dir: Optional[Union[str, Path]] = None) -> None: @@ -3436,11 +3713,10 @@ def set_paths(self, output_dir: Optional[Union[str, Path]] = None) -> None: """ if output_dir is None: - output_dir = os.path.join(os.getcwd(), - f'amici-{self.model_name}') + output_dir = os.path.join(os.getcwd(), f"amici-{self.model_name}") self.model_path = os.path.abspath(output_dir) - self.model_swig_path = os.path.join(self.model_path, 'swig') + self.model_swig_path = os.path.join(self.model_path, "swig") def set_name(self, model_name: str) -> None: """ @@ -3454,7 +3730,8 @@ def set_name(self, model_name: str) -> None: raise ValueError( f"'{model_name}' is not a valid model name. " "Model name may only contain upper and lower case letters, " - "digits and underscores, and must not start with a digit.") + "digits and underscores, and must not start with a digit." + ) self.model_name = model_name @@ -3467,12 +3744,15 @@ class TemplateAmici(Template): :cvar delimiter: delimiter that identifies template variables """ - delimiter = 'TPL_' + + delimiter = "TPL_" -def apply_template(source_file: Union[str, Path], - target_file: Union[str, Path], - template_data: Dict[str, str]) -> None: +def apply_template( + source_file: Union[str, Path], + target_file: Union[str, Path], + template_data: Dict[str, str], +) -> None: """ Load source file, apply template substitution as provided in templateData and save as targetFile. @@ -3490,7 +3770,7 @@ def apply_template(source_file: Union[str, Path], with open(source_file) as filein: src = TemplateAmici(filein.read()) result = src.safe_substitute(template_data) - with open(target_file, 'w') as fileout: + with open(target_file, "w") as fileout: fileout.write(result) @@ -3509,11 +3789,10 @@ def get_function_extern_declaration(fun: str, name: str, ode: bool) -> str: C++ function definition string """ f = functions[fun] - return f'extern {f.return_type} {fun}_{name}({f.arguments(ode)});' + return f"extern {f.return_type} {fun}_{name}({f.arguments(ode)});" -def get_sunindex_extern_declaration(fun: str, name: str, - indextype: str) -> str: +def get_sunindex_extern_declaration(fun: str, name: str, indextype: str) -> str: """ Constructs the function declaration for an index function of a given function @@ -3530,14 +3809,16 @@ def get_sunindex_extern_declaration(fun: str, name: str, :return: C++ function declaration string """ - index_arg = ', int index' if fun in multiobs_functions else '' - return \ - f'extern void {fun}_{indextype}_{name}' \ - f'(SUNMatrixWrapper &{indextype}{index_arg});' + index_arg = ", int index" if fun in multiobs_functions else "" + return ( + f"extern void {fun}_{indextype}_{name}" + f"(SUNMatrixWrapper &{indextype}{index_arg});" + ) -def get_model_override_implementation(fun: str, name: str, ode: bool, - nobody: bool = False) -> str: +def get_model_override_implementation( + fun: str, name: str, ode: bool, nobody: bool = False +) -> str: """ Constructs ``amici::Model::*`` override implementation for a given function @@ -3554,17 +3835,19 @@ def get_model_override_implementation(fun: str, name: str, ode: bool, C++ function implementation string """ func_info = functions[fun] - body = "" if nobody \ - else '\n{ind8}{maybe_return}{fun}_{name}({eval_signature});{ind4}\n' \ - .format( - ind4=' ' * 4, - ind8=' ' * 8, + body = ( + "" + if nobody + else "\n{ind8}{maybe_return}{fun}_{name}({eval_signature});{ind4}\n".format( + ind4=" " * 4, + ind8=" " * 8, maybe_return="" if func_info.return_type == "void" else "return ", fun=fun, name=name, eval_signature=remove_argument_types(func_info.arguments(ode)), ) - return '{return_type} f{fun}({signature}) override {{{body}}}\n'.format( + ) + return "{return_type} f{fun}({signature}) override {{{body}}}\n".format( return_type=func_info.return_type, fun=fun, signature=func_info.arguments(ode), @@ -3572,9 +3855,9 @@ def get_model_override_implementation(fun: str, name: str, ode: bool, ) -def get_sunindex_override_implementation(fun: str, name: str, - indextype: str, - nobody: bool = False) -> str: +def get_sunindex_override_implementation( + fun: str, name: str, indextype: str, nobody: bool = False +) -> str: """ Constructs the ``amici::Model`` function implementation for an index function of a given function @@ -3594,24 +3877,24 @@ def get_sunindex_override_implementation(fun: str, name: str, :return: C++ function implementation string """ - index_arg = ', int index' if fun in multiobs_functions else '' - index_arg_eval = ', index' if fun in multiobs_functions else '' + index_arg = ", int index" if fun in multiobs_functions else "" + index_arg_eval = ", index" if fun in multiobs_functions else "" - impl = 'void f{fun}_{indextype}({signature}) override {{' + impl = "void f{fun}_{indextype}({signature}) override {{" if nobody: - impl += '}}\n' + impl += "}}\n" else: - impl += '{ind8}{fun}_{indextype}_{name}({eval_signature});\n{ind4}}}\n' + impl += "{ind8}{fun}_{indextype}_{name}({eval_signature});\n{ind4}}}\n" return impl.format( - ind4=' ' * 4, - ind8=' ' * 8, + ind4=" " * 4, + ind8=" " * 8, fun=fun, indextype=indextype, name=name, - signature=f'SUNMatrixWrapper &{indextype}{index_arg}', - eval_signature=f'{indextype}{index_arg_eval}', + signature=f"SUNMatrixWrapper &{indextype}{index_arg}", + eval_signature=f"{indextype}{index_arg_eval}", ) @@ -3632,19 +3915,19 @@ def remove_argument_types(signature: str) -> str: # # always add whitespace after type definition for cosmetic reasons known_types = [ - 'const realtype *', - 'const double *', - 'const realtype ', - 'double *', - 'realtype *', - 'const int ', - 'int ', - 'SUNMatrixContent_Sparse ', - 'gsl::span' + "const realtype *", + "const double *", + "const realtype ", + "double *", + "realtype *", + "const int ", + "int ", + "SUNMatrixContent_Sparse ", + "gsl::span", ] for type_str in known_types: - signature = signature.replace(type_str, '') + signature = signature.replace(type_str, "") return signature @@ -3708,8 +3991,7 @@ def _custom_pow_eval_derivative(self, s): return part1 + part2 return part1 + sp.Piecewise( - (self.base, sp.And(sp.Eq(self.base, 0), sp.Eq(dbase, 0))), - (part2, True) + (self.base, sp.And(sp.Eq(self.base, 0), sp.Eq(dbase, 0))), (part2, True) ) @@ -3718,22 +4000,21 @@ def _jacobian_element(i, j, eq_i, sym_var_j): return (i, j), eq_i.diff(sym_var_j) -def _parallel_applyfunc( - obj: sp.Matrix, - func: Callable -) -> sp.Matrix: +def _parallel_applyfunc(obj: sp.Matrix, func: Callable) -> sp.Matrix: """Parallel implementation of sympy's Matrix.applyfunc""" if (n_procs := int(os.environ.get("AMICI_IMPORT_NPROCS", 1))) == 1: # serial return obj.applyfunc(func) # parallel + from multiprocessing import get_context from pickle import PicklingError + from sympy.matrices.dense import DenseMatrix - from multiprocessing import get_context + # "spawn" should avoid potential deadlocks occurring with fork # see e.g. https://stackoverflow.com/a/66113051 - ctx = get_context('spawn') + ctx = get_context("spawn") with ctx.Pool(n_procs) as p: try: if isinstance(obj, DenseMatrix): diff --git a/python/sdist/amici/de_model.py b/python/sdist/amici/de_model.py index b5b3cb3419..c5363511e7 100644 --- a/python/sdist/amici/de_model.py +++ b/python/sdist/amici/de_model.py @@ -1,24 +1,36 @@ """Objects for AMICI's internal differential equation model representation""" import abc +import numbers +from typing import Dict, Optional, Set, SupportsFloat, Union import sympy as sp -import numbers -from typing import ( - Optional, Union, Dict, SupportsFloat, Set +from .import_utils import ( + RESERVED_SYMBOLS, + ObservableTransformation, + cast_to_sym, + generate_measurement_symbol, + generate_regularization_symbol, ) -from .import_utils import ObservableTransformation, \ - generate_measurement_symbol, generate_regularization_symbol,\ - RESERVED_SYMBOLS -from .import_utils import cast_to_sym - - __all__ = [ - 'ConservationLaw', 'Constant', 'Event', 'Expression', 'LogLikelihoodY', - 'LogLikelihoodZ', 'LogLikelihoodRZ', 'ModelQuantity', 'Observable', - 'Parameter', 'SigmaY', 'SigmaZ', 'DifferentialState', 'EventObservable', - 'AlgebraicState', 'AlgebraicEquation', 'State' + "ConservationLaw", + "Constant", + "Event", + "Expression", + "LogLikelihoodY", + "LogLikelihoodZ", + "LogLikelihoodRZ", + "ModelQuantity", + "Observable", + "Parameter", + "SigmaY", + "SigmaZ", + "DifferentialState", + "EventObservable", + "AlgebraicState", + "AlgebraicEquation", + "State", ] @@ -26,10 +38,13 @@ class ModelQuantity: """ Base class for model components """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: Union[SupportsFloat, numbers.Number, sp.Expr]): + + def __init__( + self, + identifier: sp.Symbol, + name: str, + value: Union[SupportsFloat, numbers.Number, sp.Expr], + ): """ Create a new ModelQuantity instance. @@ -44,22 +59,24 @@ def __init__(self, """ if not isinstance(identifier, sp.Symbol): - raise TypeError(f'identifier must be sympy.Symbol, was ' - f'{type(identifier)}') - - if str(identifier) in RESERVED_SYMBOLS or \ - (hasattr(identifier, 'name') and - identifier.name in RESERVED_SYMBOLS): - raise ValueError(f'Cannot add model quantity with name "{name}", ' - f'please rename.') + raise TypeError( + f"identifier must be sympy.Symbol, was " f"{type(identifier)}" + ) + + if str(identifier) in RESERVED_SYMBOLS or ( + hasattr(identifier, "name") and identifier.name in RESERVED_SYMBOLS + ): + raise ValueError( + f'Cannot add model quantity with name "{name}", ' f"please rename." + ) self._identifier: sp.Symbol = identifier if not isinstance(name, str): - raise TypeError(f'name must be str, was {type(name)}') + raise TypeError(f"name must be str, was {type(name)}") self._name: str = name - self._value: sp.Expr = cast_to_sym(value, 'value') + self._value: sp.Expr = cast_to_sym(value, "value") def __repr__(self) -> str: """ @@ -104,7 +121,7 @@ def set_val(self, val: sp.Expr): :return: value of the ModelQuantity """ - self._value = cast_to_sym(val, 'value') + self._value = cast_to_sym(val, "value") class ConservationLaw(ModelQuantity): @@ -113,12 +130,15 @@ class ConservationLaw(ModelQuantity): (weighted) sum of states """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr, - coefficients: Dict[sp.Symbol, sp.Expr], - state_id: sp.Symbol): + + def __init__( + self, + identifier: sp.Symbol, + name: str, + value: sp.Expr, + coefficients: Dict[sp.Symbol, sp.Expr], + state_id: sp.Symbol, + ): """ Create a new ConservationLaw instance. @@ -170,6 +190,7 @@ class AlgebraicEquation(ModelQuantity): """ An AlgebraicEquation defines an algebraic equation. """ + def __init__(self, identifier: str, value: sp.Expr): """ Create a new AlgebraicEquation instance. @@ -193,6 +214,7 @@ class State(ModelQuantity): """ Base class for differential and algebraic model states """ + _conservation_law: Optional[ConservationLaw] = None def get_x_rdata(self): @@ -235,10 +257,7 @@ class AlgebraicState(State): An AlgebraicState defines an entity that is algebraically determined """ - def __init__(self, - identifier: sp.Symbol, - name: str, - init: sp.Expr): + def __init__(self, identifier: sp.Symbol, name: str, init: sp.Expr): """ Create a new AlgebraicState instance. @@ -281,11 +300,8 @@ class DifferentialState(State): algebraic formula that defines the temporal derivative of this state """ - def __init__(self, - identifier: sp.Symbol, - name: str, - init: sp.Expr, - dt: sp.Expr): + + def __init__(self, identifier: sp.Symbol, name: str, init: sp.Expr, dt: sp.Expr): """ Create a new State instance. Extends :meth:`ModelQuantity.__init__` by ``dt`` @@ -303,7 +319,7 @@ def __init__(self, time derivative """ super(DifferentialState, self).__init__(identifier, name, init) - self._dt = cast_to_sym(dt, 'dt') + self._dt = cast_to_sym(dt, "dt") self._conservation_law: Union[ConservationLaw, None] = None def set_conservation_law(self, law: ConservationLaw) -> None: @@ -318,20 +334,20 @@ def set_conservation_law(self, law: ConservationLaw) -> None: constant over time """ if not isinstance(law, ConservationLaw): - raise TypeError(f'conservation law must have type ConservationLaw' - f', was {type(law)}') + raise TypeError( + f"conservation law must have type ConservationLaw" f", was {type(law)}" + ) self._conservation_law = law - def set_dt(self, - dt: sp.Expr) -> None: + def set_dt(self, dt: sp.Expr) -> None: """ Sets the time derivative :param dt: time derivative """ - self._dt = cast_to_sym(dt, 'dt') + self._dt = cast_to_sym(dt, "dt") def get_dt(self) -> sp.Expr: """ @@ -377,13 +393,14 @@ class Observable(ModelQuantity): _measurement_symbol: Union[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 + self, + identifier: sp.Symbol, + name: str, + value: sp.Expr, + measurement_symbol: Optional[sp.Symbol] = None, + transformation: Optional[ + ObservableTransformation + ] = ObservableTransformation.LIN, ): """ Create a new Observable instance. @@ -408,17 +425,13 @@ def __init__( def get_measurement_symbol(self) -> sp.Symbol: if self._measurement_symbol is None: - self._measurement_symbol = generate_measurement_symbol( - self.get_id() - ) + self._measurement_symbol = generate_measurement_symbol(self.get_id()) return self._measurement_symbol def get_regularization_symbol(self) -> sp.Symbol: if self._regularization_symbol is None: - self._regularization_symbol = generate_regularization_symbol( - self.get_id() - ) + self._regularization_symbol = generate_regularization_symbol(self.get_id()) return self._regularization_symbol @@ -432,13 +445,15 @@ class EventObservable(Observable): symbolic event identifier """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr, - event: sp.Symbol, - measurement_symbol: Optional[sp.Symbol] = None, - transformation: Optional[ObservableTransformation] = 'lin',): + def __init__( + self, + identifier: sp.Symbol, + name: str, + value: sp.Expr, + event: sp.Symbol, + measurement_symbol: Optional[sp.Symbol] = None, + transformation: Optional[ObservableTransformation] = "lin", + ): """ Create a new EventObservable instance. @@ -457,9 +472,9 @@ def __init__(self, :param event: Symbolic identifier of the corresponding event. """ - super(EventObservable, self).__init__(identifier, name, value, - measurement_symbol, - transformation) + super(EventObservable, self).__init__( + identifier, name, value, measurement_symbol, transformation + ) self._event: sp.Symbol = event def get_event(self) -> sp.Symbol: @@ -477,10 +492,8 @@ class Sigma(ModelQuantity): and measurements when computing residuals or objective functions, abbreviated by ``sigma{y,z}``. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr): + + def __init__(self, identifier: sp.Symbol, name: str, value: sp.Expr): """ Create a new Standard Deviation instance. @@ -520,10 +533,8 @@ class Expression(ModelQuantity): shorter model compilation times, but may also reduce model simulation time. Abbreviated by ``w``. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr): + + def __init__(self, identifier: sp.Symbol, name: str, value: sp.Expr): """ Create a new Expression instance. @@ -545,10 +556,7 @@ class Parameter(ModelQuantity): sensitivities may be computed, abbreviated by ``p``. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: numbers.Number): + def __init__(self, identifier: sp.Symbol, name: str, value: numbers.Number): """ Create a new Expression instance. @@ -571,10 +579,7 @@ class Constant(ModelQuantity): sensitivities cannot be computed, abbreviated by ``k``. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: numbers.Number): + def __init__(self, identifier: sp.Symbol, name: str, value: numbers.Number): """ Create a new Expression instance. @@ -598,10 +603,7 @@ class LogLikelihood(ModelQuantity): instances evaluated at all timepoints, abbreviated by ``Jy``. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr): + def __init__(self, identifier: sp.Symbol, name: str, value: sp.Expr): """ Create a new Expression instance. @@ -649,12 +651,14 @@ class Event(ModelQuantity): themselves, causing a reinitialization of the solver. """ - def __init__(self, - identifier: sp.Symbol, - name: str, - value: sp.Expr, - state_update: Union[sp.Expr, None], - initial_value: Optional[bool] = True): + def __init__( + self, + identifier: sp.Symbol, + name: str, + value: sp.Expr, + state_update: Union[sp.Expr, None], + initial_value: Optional[bool] = True, + ): """ Create a new Event instance. @@ -694,5 +698,6 @@ def __eq__(self, other): Check equality of events at the level of trigger/root functions, as we need to collect unique root functions for ``roots.cpp`` """ - return self.get_val() == other.get_val() and \ - (self.get_initial_value() == other.get_initial_value()) + return self.get_val() == other.get_val() and ( + self.get_initial_value() == other.get_initial_value() + ) diff --git a/python/sdist/amici/gradient_check.py b/python/sdist/amici/gradient_check.py index 76a17817c2..ee900fe902 100644 --- a/python/sdist/amici/gradient_check.py +++ b/python/sdist/amici/gradient_check.py @@ -5,25 +5,34 @@ computed sensitivities using finite difference approximations """ -from . import ( - runAmiciSimulation, SensitivityOrder, AMICI_SUCCESS, SensitivityMethod, - Model, Solver, ExpData, ReturnData, ParameterScaling) -import numpy as np import copy +from typing import Callable, List, Optional, Sequence + +import numpy as np -from typing import Callable, Optional, List, Sequence +from . import ( + AMICI_SUCCESS, + ExpData, + Model, + ParameterScaling, + ReturnData, + SensitivityMethod, + SensitivityOrder, + Solver, + runAmiciSimulation, +) def check_finite_difference( - x0: Sequence[float], - model: Model, - solver: Solver, - edata: ExpData, - ip: int, - fields: List[str], - atol: Optional[float] = 1e-4, - rtol: Optional[float] = 1e-4, - epsilon: Optional[float] = 1e-3 + x0: Sequence[float], + model: Model, + solver: Solver, + edata: ExpData, + ip: int, + fields: List[str], + atol: Optional[float] = 1e-4, + rtol: Optional[float] = 1e-4, + epsilon: Optional[float] = 1e-3, ) -> None: """ Checks the computed sensitivity based derivatives against a finite @@ -76,7 +85,7 @@ def check_finite_difference( if int(og_sensitivity_order) < int(SensitivityOrder.first): solver.setSensitivityOrder(SensitivityOrder.first) rdata = runAmiciSimulation(model, solver, edata) - if rdata['status'] != AMICI_SUCCESS: + if rdata["status"] != AMICI_SUCCESS: raise AssertionError(f"Simulation failed (status {rdata['status']}") # finite difference @@ -95,17 +104,17 @@ def check_finite_difference( # forward: model.setParameters(pf) rdataf = runAmiciSimulation(model, solver, edata) - if rdataf['status'] != AMICI_SUCCESS: + if rdataf["status"] != AMICI_SUCCESS: raise AssertionError(f"Simulation failed (status {rdataf['status']}") # backward: model.setParameters(pb) rdatab = runAmiciSimulation(model, solver, edata) - if rdatab['status'] != AMICI_SUCCESS: + if rdatab["status"] != AMICI_SUCCESS: raise AssertionError(f"Simulation failed (status {rdatab['status']}") for field in fields: - sensi_raw = rdata[f's{field}'] + sensi_raw = rdata[f"s{field}"] fd = (rdataf[field] - rdatab[field]) / (pf[ip] - pb[ip]) if len(sensi_raw.shape) == 1: sensi = sensi_raw[0] @@ -126,14 +135,14 @@ 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, - check_least_squares: bool = True, - skip_zero_pars: bool = False + model: Model, + solver: Solver, + edata: Optional[ExpData] = None, + atol: Optional[float] = 1e-4, + rtol: Optional[float] = 1e-4, + epsilon: Optional[float] = 1e-3, + check_least_squares: bool = True, + skip_zero_pars: bool = False, ) -> None: """ Finite differences check for likelihood gradient. @@ -172,50 +181,58 @@ def check_derivatives( rdata = runAmiciSimulation(model, solver, edata) solver.setSensitivityOrder(og_sens_order) - if rdata['status'] != AMICI_SUCCESS: + if rdata["status"] != AMICI_SUCCESS: raise AssertionError(f"Simulation failed (status {rdata['status']}") fields = [] - if solver.getSensitivityMethod() == SensitivityMethod.forward and \ - solver.getSensitivityOrder() <= SensitivityOrder.first: - fields.append('x') - - leastsquares_applicable = \ - solver.getSensitivityMethod() == SensitivityMethod.forward \ - and edata is not None - - if 'ssigmay' in rdata.keys() \ - and rdata['ssigmay'] is not None \ - and rdata['ssigmay'].any() and not model.getAddSigmaResiduals(): + if ( + solver.getSensitivityMethod() == SensitivityMethod.forward + and solver.getSensitivityOrder() <= SensitivityOrder.first + ): + fields.append("x") + + leastsquares_applicable = ( + solver.getSensitivityMethod() == SensitivityMethod.forward and edata is not None + ) + + if ( + "ssigmay" in rdata.keys() + and rdata["ssigmay"] is not None + and rdata["ssigmay"].any() + and not model.getAddSigmaResiduals() + ): leastsquares_applicable = False if check_least_squares and leastsquares_applicable: - fields += ['res', 'y'] + fields += ["res", "y"] - _check_results(rdata, 'FIM', np.dot(rdata['sres'].T, rdata['sres']), - atol=1e-8, rtol=1e-4) - _check_results(rdata, 'sllh', -np.dot(rdata['res'].T, rdata['sres']), - atol=1e-8, rtol=1e-4) + _check_results( + rdata, "FIM", np.dot(rdata["sres"].T, rdata["sres"]), atol=1e-8, rtol=1e-4 + ) + _check_results( + rdata, "sllh", -np.dot(rdata["res"].T, rdata["sres"]), atol=1e-8, rtol=1e-4 + ) if edata is not None: - fields.append('llh') + fields.append("llh") for ip, pval in enumerate(p): if pval == 0.0 and skip_zero_pars: continue - check_finite_difference(p, model, solver, edata, ip, fields, - atol=atol, rtol=rtol, epsilon=epsilon) + check_finite_difference( + p, model, solver, edata, ip, fields, atol=atol, rtol=rtol, epsilon=epsilon + ) def _check_close( - result: np.array, - expected: np.array, - atol: float, - rtol: float, - field: str, - ip: Optional[int] = None, - verbose: Optional[bool] = True, + result: np.array, + expected: np.array, + atol: float, + rtol: float, + field: str, + ip: Optional[int] = None, + verbose: Optional[bool] = True, ) -> None: """ Compares computed values against expected values and provides rich @@ -247,14 +264,16 @@ def _check_close( return if ip is None: - index_str = '' - check_type = 'Regression check' + index_str = "" + check_type = "Regression check" else: - index_str = f'at index ip={ip} ' - check_type = 'FD check' + index_str = f"at index ip={ip} " + check_type = "FD check" - lines = [f'{check_type} failed for {field} {index_str}for ' - f'{close.size - close.sum()} indices:'] + lines = [ + f"{check_type} failed for {field} {index_str}for " + f"{close.size - close.sum()} indices:" + ] if verbose: for idx in np.argwhere(~close): idx = tuple(idx) @@ -262,22 +281,17 @@ def _check_close( rr = result[idx] else: rr = result - lines.append( - f"\tat {idx}: Expected {expected[idx]}, got {rr}") + lines.append(f"\tat {idx}: Expected {expected[idx]}, got {rr}") adev = np.abs(result - expected) rdev = np.abs((result - expected) / (expected + atol)) - lines.append(f'max(adev): {adev.max()}, max(rdev): {rdev.max()}') + lines.append(f"max(adev): {adev.max()}, max(rdev): {rdev.max()}") raise AssertionError("\n".join(lines)) def _check_results( - rdata: ReturnData, - field: str, - expected: np.array, - atol: float, - rtol: float - ) -> None: + rdata: ReturnData, field: str, expected: np.array, atol: float, rtol: float +) -> None: """ Checks whether rdata[field] agrees with expected according to provided tolerances. @@ -303,5 +317,4 @@ def _check_results( if type(result) is float: result = np.array(result) - _check_close(result=result, expected=expected, - atol=atol, rtol=rtol, field=field) + _check_close(result=result, expected=expected, atol=atol, rtol=rtol, field=field) diff --git a/python/sdist/amici/import_utils.py b/python/sdist/amici/import_utils.py index 44e075ed0e..6c2e6b0e7e 100644 --- a/python/sdist/amici/import_utils.py +++ b/python/sdist/amici/import_utils.py @@ -4,15 +4,24 @@ import itertools as itt import numbers import sys -from typing import (Any, Callable, Dict, Iterable, Optional, Sequence, - SupportsFloat, Tuple, Union) +from typing import ( + Any, + Callable, + Dict, + Iterable, + Optional, + Sequence, + SupportsFloat, + Tuple, + Union, +) import sympy as sp from sympy.functions.elementary.piecewise import ExprCondPair from sympy.logic.boolalg import BooleanAtom from toposort import toposort -RESERVED_SYMBOLS = ['x', 'k', 'p', 'y', 'w', 'h', 't', 'AMICI_EMPTY_BOLUS'] +RESERVED_SYMBOLS = ["x", "k", "p", "y", "w", "h", "t", "AMICI_EMPTY_BOLUS"] try: import pysb @@ -35,25 +44,27 @@ def __init__(self, data): # error messages. That's convenient for doctests. s = "Circular dependencies exist among these items: {{{}}}".format( ", ".join( - "{!r}:{!r}".format(key, value) for key, value in sorted( - {str(k): v for k, v in data.items()}.items()) + "{!r}:{!r}".format(key, value) + for key, value in sorted({str(k): v for k, v in data.items()}.items()) ) ) super(CircularDependencyError, self).__init__(s) self.data = data -setattr(sys.modules["toposort"], "CircularDependencyError", - CircularDependencyError) +setattr(sys.modules["toposort"], "CircularDependencyError", CircularDependencyError) + +annotation_namespace = "https://github.com/AMICI-dev/AMICI" class ObservableTransformation(str, enum.Enum): """ Different modes of observable transformation. """ - LOG10 = 'log10' - LOG = 'log' - LIN = 'lin' + + LOG10 = "log10" + LOG = "log" + LIN = "lin" def noise_distribution_to_observable_transformation( @@ -69,16 +80,16 @@ def noise_distribution_to_observable_transformation( observable transformation """ if isinstance(noise_distribution, str): - if noise_distribution.startswith('log-'): + if noise_distribution.startswith("log-"): return ObservableTransformation.LOG - if noise_distribution.startswith('log10-'): + if noise_distribution.startswith("log10-"): return ObservableTransformation.LOG10 return ObservableTransformation.LIN def noise_distribution_to_cost_function( - noise_distribution: Union[str, Callable] + noise_distribution: Union[str, Callable] ) -> Callable[[str], str]: """ Parse noise distribution string to a cost function definition amici can @@ -186,38 +197,46 @@ def noise_distribution_to_cost_function( if isinstance(noise_distribution, Callable): return noise_distribution - if noise_distribution in ['normal', 'lin-normal']: - y_string = '0.5*log(2*pi*{sigma}**2) + 0.5*(({y} - {m}) / {sigma})**2' - elif noise_distribution == 'log-normal': - y_string = '0.5*log(2*pi*{sigma}**2*{m}**2) ' \ - '+ 0.5*((log({y}) - log({m})) / {sigma})**2' - elif noise_distribution == 'log10-normal': - y_string = '0.5*log(2*pi*{sigma}**2*{m}**2*log(10)**2) ' \ - '+ 0.5*((log({y}, 10) - log({m}, 10)) / {sigma})**2' - elif noise_distribution in ['laplace', 'lin-laplace']: - y_string = 'log(2*{sigma}) + Abs({y} - {m}) / {sigma}' - elif noise_distribution == 'log-laplace': - y_string = 'log(2*{sigma}*{m}) + Abs(log({y}) - log({m})) / {sigma}' - elif noise_distribution == 'log10-laplace': - y_string = 'log(2*{sigma}*{m}*log(10)) ' \ - '+ Abs(log({y}, 10) - log({m}, 10)) / {sigma}' - elif noise_distribution in ['binomial', 'lin-binomial']: + if noise_distribution in ["normal", "lin-normal"]: + y_string = "0.5*log(2*pi*{sigma}**2) + 0.5*(({y} - {m}) / {sigma})**2" + elif noise_distribution == "log-normal": + y_string = ( + "0.5*log(2*pi*{sigma}**2*{m}**2) " + "+ 0.5*((log({y}) - log({m})) / {sigma})**2" + ) + elif noise_distribution == "log10-normal": + y_string = ( + "0.5*log(2*pi*{sigma}**2*{m}**2*log(10)**2) " + "+ 0.5*((log({y}, 10) - log({m}, 10)) / {sigma})**2" + ) + elif noise_distribution in ["laplace", "lin-laplace"]: + y_string = "log(2*{sigma}) + Abs({y} - {m}) / {sigma}" + elif noise_distribution == "log-laplace": + y_string = "log(2*{sigma}*{m}) + Abs(log({y}) - log({m})) / {sigma}" + elif noise_distribution == "log10-laplace": + y_string = ( + "log(2*{sigma}*{m}*log(10)) " "+ Abs(log({y}, 10) - log({m}, 10)) / {sigma}" + ) + elif noise_distribution in ["binomial", "lin-binomial"]: # Binomial noise model parameterized via success probability p - y_string = '- log(Heaviside({y} - {m})) - loggamma({y}+1) ' \ - '+ loggamma({m}+1) + loggamma({y}-{m}+1) ' \ - '- {m} * log({sigma}) - ({y} - {m}) * log(1-{sigma})' - elif noise_distribution in ['negative-binomial', 'lin-negative-binomial']: + y_string = ( + "- log(Heaviside({y} - {m})) - loggamma({y}+1) " + "+ loggamma({m}+1) + loggamma({y}-{m}+1) " + "- {m} * log({sigma}) - ({y} - {m}) * log(1-{sigma})" + ) + elif noise_distribution in ["negative-binomial", "lin-negative-binomial"]: # Negative binomial noise model of the number of successes m # (data) before r=(1-sigma)/sigma * y failures occur, # with mean number of successes y (simulation), # parameterized via success probability p = sigma. - r = '{y} * (1-{sigma}) / {sigma}' - y_string = f'- loggamma({{m}}+{r}) + loggamma({{m}}+1) ' \ - f'+ loggamma({r}) - {r} * log(1-{{sigma}}) ' \ - f'- {{m}} * log({{sigma}})' + r = "{y} * (1-{sigma}) / {sigma}" + y_string = ( + f"- loggamma({{m}}+{r}) + loggamma({{m}}+1) " + f"+ loggamma({r}) - {r} * log(1-{{sigma}}) " + f"- {{m}} * log({{sigma}})" + ) else: - raise ValueError( - f"Cost identifier {noise_distribution} not recognized.") + raise ValueError(f"Cost identifier {noise_distribution} not recognized.") def nllh_y_string(str_symbol): y, m, sigma = _get_str_symbol_identifiers(str_symbol) @@ -232,10 +251,9 @@ def _get_str_symbol_identifiers(str_symbol: str) -> tuple: return y, m, sigma -def smart_subs_dict(sym: sp.Expr, - subs: SymbolDef, - field: Optional[str] = None, - reverse: bool = True) -> sp.Expr: +def smart_subs_dict( + sym: sp.Expr, subs: SymbolDef, field: Optional[str] = None, reverse: bool = True +) -> sp.Expr: """ Substitutes expressions completely flattening them out. Requires sorting of expressions with toposort. @@ -257,8 +275,7 @@ def smart_subs_dict(sym: sp.Expr, Substituted symbolic expression """ s = [ - (eid, expr[field] if field is not None else expr) - for eid, expr in subs.items() + (eid, expr[field] if field is not None else expr) for eid, expr in subs.items() ] if reverse: s.reverse() @@ -289,8 +306,7 @@ def smart_subs(element: sp.Expr, old: sp.Symbol, new: sp.Expr) -> sp.Expr: return element.subs(old, new) if element.has(old) else element -def toposort_symbols(symbols: SymbolDef, - field: Optional[str] = None) -> SymbolDef: +def toposort_symbols(symbols: SymbolDef, field: Optional[str] = None) -> SymbolDef: """ Topologically sort symbol definitions according to their interdependency @@ -303,16 +319,18 @@ def toposort_symbols(symbols: SymbolDef, :return: ordered symbol definitions """ - sorted_symbols = toposort({ - identifier: { - s for s in ( - definition[field] if field is not None else definition - ).free_symbols - if s in symbols + sorted_symbols = toposort( + { + identifier: { + s + for s in ( + definition[field] if field is not None else definition + ).free_symbols + if s in symbols + } + for identifier, definition in symbols.items() } - for identifier, definition - in symbols.items() - }) + ) return { s: symbols[s] for symbol_group in sorted_symbols @@ -331,40 +349,42 @@ def _parse_special_functions(sym: sp.Expr, toplevel: bool = True) -> sp.Expr: :param toplevel: as this is called recursively, are we in the top level expression? """ - args = tuple(arg if arg.__class__.__name__ == 'piecewise' - and sym.__class__.__name__ == 'piecewise' - else _parse_special_functions(arg, False) - for arg in sym.args) + args = tuple( + arg + if arg.__class__.__name__ == "piecewise" + and sym.__class__.__name__ == "piecewise" + else _parse_special_functions(arg, False) + for arg in sym.args + ) fun_mappings = { - 'times': sp.Mul, - 'xor': sp.Xor, - 'abs': sp.Abs, - 'min': sp.Min, - 'max': sp.Max, - 'ceil': sp.functions.ceiling, - 'floor': sp.functions.floor, - 'factorial': sp.functions.factorial, - 'arcsin': sp.functions.asin, - 'arccos': sp.functions.acos, - 'arctan': sp.functions.atan, - 'arccot': sp.functions.acot, - 'arcsec': sp.functions.asec, - 'arccsc': sp.functions.acsc, - 'arcsinh': sp.functions.asinh, - 'arccosh': sp.functions.acosh, - 'arctanh': sp.functions.atanh, - 'arccoth': sp.functions.acoth, - 'arcsech': sp.functions.asech, - 'arccsch': sp.functions.acsch, + "times": sp.Mul, + "xor": sp.Xor, + "abs": sp.Abs, + "min": sp.Min, + "max": sp.Max, + "ceil": sp.functions.ceiling, + "floor": sp.functions.floor, + "factorial": sp.functions.factorial, + "arcsin": sp.functions.asin, + "arccos": sp.functions.acos, + "arctan": sp.functions.atan, + "arccot": sp.functions.acot, + "arcsec": sp.functions.asec, + "arccsc": sp.functions.acsc, + "arcsinh": sp.functions.asinh, + "arccosh": sp.functions.acosh, + "arctanh": sp.functions.atanh, + "arccoth": sp.functions.acoth, + "arcsech": sp.functions.asech, + "arccsch": sp.functions.acsch, } if sym.__class__.__name__ in fun_mappings: return fun_mappings[sym.__class__.__name__](*args) - elif sym.__class__.__name__ == 'piecewise' \ - or isinstance(sym, sp.Piecewise): - if isinstance(sym, sp.Piecewise): + elif sym.__class__.__name__ == "piecewise" or isinstance(sym, sp.Piecewise): + if isinstance(sym, sp.Piecewise): # this is sympy piecewise, can't be nested denested_args = args else: @@ -372,7 +392,7 @@ def _parse_special_functions(sym: sp.Expr, toplevel: bool = True) -> sp.Expr: denested_args = _denest_piecewise(args) return _parse_piecewise_to_heaviside(denested_args) - if sym.__class__.__name__ == 'plus' and not sym.args: + if sym.__class__.__name__ == "plus" and not sym.args: return sp.Float(0.0) if isinstance(sym, (sp.Function, sp.Mul, sp.Add, sp.Pow)): @@ -388,7 +408,7 @@ 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]] + args: Sequence[Union[sp.Expr, sp.logic.boolalg.Boolean, bool]] ) -> Tuple[Union[sp.Expr, sp.logic.boolalg.Boolean, bool]]: """ Denest piecewise functions that contain piecewise as condition @@ -405,23 +425,19 @@ def _denest_piecewise( # handling of this case is explicitely disabled in # _parse_special_functions as keeping track of coeff/cond # arguments is tricky. Simpler to just parse them out here - if coeff.__class__.__name__ == 'piecewise': + if coeff.__class__.__name__ == "piecewise": coeff = _parse_special_functions(coeff, False) # we can have conditions that are piecewise function # returning True or False - if cond.__class__.__name__ == 'piecewise': + if cond.__class__.__name__ == "piecewise": # this keeps track of conditional that the previous # piece was picked previous_was_picked = sp.false # recursively denest those first - for sub_coeff, sub_cond in grouper( - _denest_piecewise(cond.args), 2, True - ): + for sub_coeff, sub_cond in grouper(_denest_piecewise(cond.args), 2, True): # flatten the individual pieces - pick_this = sp.And( - sp.Not(previous_was_picked), sub_cond - ) + pick_this = sp.And(sp.Not(previous_was_picked), sub_cond) if sub_coeff == sp.true: args_out.extend([coeff, pick_this]) previous_was_picked = pick_this @@ -463,7 +479,7 @@ def _parse_piecewise_to_heaviside(args: Iterable[sp.Expr]) -> sp.Expr: tmp = _parse_heaviside_trigger(trigger) formula += coeff * sp.simplify(not_condition * tmp) - not_condition *= (1-tmp) + not_condition *= 1 - tmp return formula @@ -478,7 +494,7 @@ def _parse_heaviside_trigger(trigger: sp.Expr) -> sp.Expr: """ if trigger.is_Relational: root = trigger.args[0] - trigger.args[1] - _check_unsupported_functions(root, 'sympy.Expression') + _check_unsupported_functions(root, "sympy.Expression") # normalize such that we always implement <, # this ensures that we can correctly evaluate the condition if @@ -500,21 +516,18 @@ def _parse_heaviside_trigger(trigger: sp.Expr) -> sp.Expr: # or(x,y) = not(and(not(x),not(y)) if isinstance(trigger, sp.Or): - return 1-sp.Mul(*[1-_parse_heaviside_trigger(arg) - for arg in trigger.args]) + return 1 - sp.Mul(*[1 - _parse_heaviside_trigger(arg) for arg in trigger.args]) if isinstance(trigger, sp.And): - return sp.Mul(*[_parse_heaviside_trigger(arg) - for arg in trigger.args]) + return sp.Mul(*[_parse_heaviside_trigger(arg) for arg in trigger.args]) raise RuntimeError( - 'AMICI can not parse piecewise/event trigger functions with argument ' - f'{trigger}.' + "AMICI can not parse piecewise/event trigger functions with argument " + f"{trigger}." ) -def grouper(iterable: Iterable, n: int, - fillvalue: Any = None) -> Iterable[Tuple[Any]]: +def grouper(iterable: Iterable, n: int, fillvalue: Any = None) -> Iterable[Tuple[Any]]: """ Collect data into fixed-length chunks or blocks @@ -535,9 +548,9 @@ def grouper(iterable: Iterable, n: int, return itt.zip_longest(*args, fillvalue=fillvalue) -def _check_unsupported_functions(sym: sp.Expr, - expression_type: str, - full_sym: Optional[sp.Expr] = None): +def _check_unsupported_functions( + sym: sp.Expr, expression_type: str, full_sym: Optional[sp.Expr] = None +): """ Recursively checks the symbolic expression for unsupported symbolic functions @@ -558,25 +571,37 @@ def _check_unsupported_functions(sym: sp.Expr, # sp.functions.floor applied to numbers should be simplified out and # thus pass this test unsupported_functions = ( - sp.functions.factorial, sp.functions.ceiling, sp.functions.floor, - sp.functions.sec, sp.functions.csc, sp.functions.cot, - sp.functions.asec, sp.functions.acsc, sp.functions.acot, - sp.functions.acsch, sp.functions.acoth, - sp.Mod, sp.core.function.UndefinedFunction + sp.functions.factorial, + sp.functions.ceiling, + sp.functions.floor, + sp.functions.sec, + sp.functions.csc, + sp.functions.cot, + sp.functions.asec, + sp.functions.acsc, + sp.functions.acot, + sp.functions.acsch, + sp.functions.acoth, + sp.Mod, + sp.core.function.UndefinedFunction, ) - if isinstance(sym.func, unsupported_functions) \ - or isinstance(sym, unsupported_functions): - raise RuntimeError(f'Encountered unsupported expression ' - f'"{sym.func}" of type ' - f'"{type(sym.func)}" as part of a ' - f'{expression_type}: "{full_sym}"!') + if isinstance(sym.func, unsupported_functions) or isinstance( + sym, unsupported_functions + ): + raise RuntimeError( + f"Encountered unsupported expression " + f'"{sym.func}" of type ' + f'"{type(sym.func)}" as part of a ' + f'{expression_type}: "{full_sym}"!' + ) for arg in list(sym.args): _check_unsupported_functions(arg, expression_type) -def cast_to_sym(value: Union[SupportsFloat, sp.Expr, BooleanAtom], - input_name: str) -> sp.Expr: +def cast_to_sym( + value: Union[SupportsFloat, sp.Expr, BooleanAtom], input_name: str +) -> sp.Expr: """ Typecasts the value to :py:class:`sympy.Float` if possible, and ensures the value is a symbolic expression. @@ -596,8 +621,9 @@ def cast_to_sym(value: Union[SupportsFloat, sp.Expr, BooleanAtom], value = sp.Float(float(bool(value))) if not isinstance(value, sp.Expr): - raise TypeError(f"Couldn't cast {input_name} to sympy.Expr, was " - f"{type(value)}") + raise TypeError( + f"Couldn't cast {input_name} to sympy.Expr, was " f"{type(value)}" + ) return value @@ -614,7 +640,7 @@ def generate_measurement_symbol(observable_id: Union[str, sp.Symbol]): """ if not isinstance(observable_id, str): observable_id = strip_pysb(observable_id) - return symbol_with_assumptions(f'm{observable_id}') + return symbol_with_assumptions(f"m{observable_id}") def generate_regularization_symbol(observable_id: Union[str, sp.Symbol]): @@ -629,13 +655,10 @@ def generate_regularization_symbol(observable_id: Union[str, sp.Symbol]): """ if not isinstance(observable_id, str): observable_id = strip_pysb(observable_id) - return symbol_with_assumptions(f'r{observable_id}') + return symbol_with_assumptions(f"r{observable_id}") -def generate_flux_symbol( - reaction_index: int, - name: Optional[str] = None -) -> sp.Symbol: +def generate_flux_symbol(reaction_index: int, name: Optional[str] = None) -> sp.Symbol: """ Generate identifier symbol for a reaction flux. This function will always return the same unique python object for a @@ -651,7 +674,7 @@ def generate_flux_symbol( if name is not None: return symbol_with_assumptions(name) - return symbol_with_assumptions(f'flux_r{reaction_index}') + return symbol_with_assumptions(f"flux_r{reaction_index}") def symbol_with_assumptions(name: str): @@ -685,3 +708,7 @@ def strip_pysb(symbol: sp.Basic) -> sp.Basic: else: # in this case we will use sympy specific transform anyways return symbol + + +sbml_time_symbol = symbol_with_assumptions("time") +amici_time_symbol = symbol_with_assumptions("t") diff --git a/python/sdist/amici/logging.py b/python/sdist/amici/logging.py index 2c03d4e8e9..5f548de7a1 100644 --- a/python/sdist/amici/logging.py +++ b/python/sdist/amici/logging.py @@ -4,34 +4,38 @@ This module provides custom logging functionality for other amici modules """ +import functools import logging +import os import platform import socket -import amici -import os -import warnings import time -import functools +import warnings +from inspect import currentframe, getouterframes -from inspect import getouterframes, currentframe +import amici -LOG_LEVEL_ENV_VAR = 'AMICI_LOG' -BASE_LOGGER_NAME = 'amici' +LOG_LEVEL_ENV_VAR = "AMICI_LOG" +BASE_LOGGER_NAME = "amici" # Supported values for LOG_LEVEL_ENV_VAR -NAMED_LOG_LEVELS = {'NOTSET': logging.NOTSET, - 'DEBUG': logging.DEBUG, - 'INFO': logging.INFO, - 'WARNING': logging.WARNING, - 'ERROR': logging.ERROR, - 'CRITICAL': logging.CRITICAL} - -from typing import Optional, Callable, Union - - -def _setup_logger(level: Optional[int] = logging.WARNING, - console_output: Optional[bool] = True, - file_output: Optional[bool] = False, - capture_warnings: Optional[bool] = True) -> logging.Logger: +NAMED_LOG_LEVELS = { + "NOTSET": logging.NOTSET, + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, +} + +from typing import Callable, Optional, Union + + +def _setup_logger( + level: Optional[int] = logging.WARNING, + console_output: Optional[bool] = True, + file_output: Optional[bool] = False, + capture_warnings: Optional[bool] = True, +) -> logging.Logger: """ Set up a new logging.Logger for AMICI logging @@ -67,20 +71,23 @@ def _setup_logger(level: Optional[int] = logging.WARNING, if level_name in NAMED_LOG_LEVELS.keys(): level = NAMED_LOG_LEVELS[level_name] else: - raise ValueError(f'Environment variable {LOG_LEVEL_ENV_VAR} ' - f'contains an invalid value "{level_name}".' - f' If set, its value must be one of ' - f'{", ".join(NAMED_LOG_LEVELS.keys())}' - f' (case-sensitive) or an integer log level.') + raise ValueError( + f"Environment variable {LOG_LEVEL_ENV_VAR} " + f'contains an invalid value "{level_name}".' + f" If set, its value must be one of " + f'{", ".join(NAMED_LOG_LEVELS.keys())}' + f" (case-sensitive) or an integer log level." + ) log.setLevel(level) # Remove default logging handler log.handlers = [] - log_fmt = logging.Formatter('%(asctime)s.%(msecs).3d - %(name)s - ' - '%(levelname)s - %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') + log_fmt = logging.Formatter( + "%(asctime)s.%(msecs).3d - %(name)s - " "%(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) if console_output: stream_handler = logging.StreamHandler() @@ -92,11 +99,11 @@ def _setup_logger(level: Optional[int] = logging.WARNING, file_handler.setFormatter(log_fmt) log.addHandler(file_handler) - log.info('Logging started on AMICI version %s', amici.__version__) + log.info("Logging started on AMICI version %s", amici.__version__) - log.debug('OS Platform: %s', platform.platform()) - log.debug('Python version: %s', platform.python_version()) - log.debug('Hostname: %s', socket.getfqdn()) + log.debug("OS Platform: %s", platform.platform()) + log.debug("Python version: %s", platform.python_version()) + log.debug("Hostname: %s", socket.getfqdn()) logging.captureWarnings(capture_warnings) @@ -108,17 +115,21 @@ def set_log_level(logger: logging.Logger, log_level: Union[int, bool]) -> None: if isinstance(log_level, bool): log_level = logging.DEBUG elif not isinstance(log_level, int): - raise ValueError('log_level must be a boolean, integer or None') + raise ValueError("log_level must be a boolean, integer or None") if logger.getEffectiveLevel() != log_level: - logger.debug('Changing log_level from %d to %d' % ( - logger.getEffectiveLevel(), log_level)) + logger.debug( + "Changing log_level from %d to %d" + % (logger.getEffectiveLevel(), log_level) + ) logger.setLevel(log_level) -def get_logger(logger_name: Optional[str] = BASE_LOGGER_NAME, - log_level: Optional[int] = None, - **kwargs) -> logging.Logger: +def get_logger( + logger_name: Optional[str] = BASE_LOGGER_NAME, + log_level: Optional[int] = None, + **kwargs, +) -> logging.Logger: """ Returns (if extistant) or creates an AMICI logger @@ -154,8 +165,9 @@ def get_logger(logger_name: Optional[str] = BASE_LOGGER_NAME, if BASE_LOGGER_NAME not in logging.Logger.manager.loggerDict.keys(): _setup_logger(**kwargs) elif kwargs: - warnings.warn('AMICI logger already exists, ignoring keyword ' - 'arguments to setup_logger') + warnings.warn( + "AMICI logger already exists, ignoring keyword " "arguments to setup_logger" + ) logger = logging.getLogger(logger_name) @@ -175,33 +187,41 @@ def log_execution_time(description: str, logger: logging.Logger) -> Callable: :param logger: Logger to which execution timing will be printed """ + def decorator_timer(func): @functools.wraps(func) def wrapper_timer(*args, **kwargs): - # append pluses to indicate recursion level recursion_level = sum( - frame.function == 'wrapper_timer' - and frame.filename == __file__ + frame.function == "wrapper_timer" and frame.filename == __file__ for frame in getouterframes(currentframe(), context=0) ) - recursion = '' + recursion = "" level = logging.INFO - level_length = len('INFO') + level_length = len("INFO") if recursion_level > 1: - recursion = '+' * (recursion_level - 1) + recursion = "+" * (recursion_level - 1) level = logging.DEBUG - level_length = len('DEBUG') + level_length = len("DEBUG") tstart = time.perf_counter() rval = func(*args, **kwargs) tend = time.perf_counter() - spacers = ' ' * max(59 - len(description) - len(logger.name) - - len(recursion) - level_length, 0) + spacers = " " * max( + 59 + - len(description) + - len(logger.name) + - len(recursion) + - level_length, + 0, + ) logger.log( - level, f'Finished {description}{spacers}{recursion} ({(tend - tstart):.2E}s)' + level, + f"Finished {description}{spacers}{recursion} ({(tend - tstart):.2E}s)", ) return rval + return wrapper_timer + return decorator_timer diff --git a/python/sdist/amici/numpy.py b/python/sdist/amici/numpy.py index ea83ac2984..91ca6449f6 100644 --- a/python/sdist/amici/numpy.py +++ b/python/sdist/amici/numpy.py @@ -4,13 +4,14 @@ This module provides views on C++ objects for efficient access. """ -import numpy as np -import copy import collections +import copy +from typing import Dict, Iterator, List, Literal, Union import amici -from . import ExpDataPtr, ReturnDataPtr, ExpData, ReturnData, Model -from typing import Union, List, Dict, Iterator, Literal +import numpy as np + +from . import ExpData, ExpDataPtr, Model, ReturnData, ReturnDataPtr class SwigPtrView(collections.abc.Mapping): @@ -41,23 +42,21 @@ def __getitem__(self, item: str) -> Union[np.ndarray, float]: :return: value """ if self._swigptr is None: - raise NotImplementedError('Cannot get items from abstract class.') + raise NotImplementedError("Cannot get items from abstract class.") - if item == 'ptr': + if item == "ptr": return self._swigptr if item in self._cache: return self._cache[item] - if item == 'id': + if item == "id": return getattr(self._swigptr, item) if item not in self._field_names: self.__missing__(item) - value = _field_as_numpy( - self._field_dimensions, item, self._swigptr - ) + value = _field_as_numpy(self._field_dimensions, item, self._swigptr) self._cache[item] = value return value @@ -67,7 +66,7 @@ def __missing__(self, key: str) -> None: :param key: field name """ - raise KeyError(f'Unknown field name {key}.') + raise KeyError(f"Unknown field name {key}.") def __getattr__(self, item) -> Union[np.ndarray, float]: """ @@ -147,7 +146,7 @@ def __repr__(self): :returns: string representation """ - return f'<{self.__class__.__name__}({self._swigptr})>' + return f"<{self.__class__.__name__}({self._swigptr})>" class ReturnDataView(SwigPtrView): @@ -157,17 +156,60 @@ class ReturnDataView(SwigPtrView): """ _field_names = [ - 'ts', 'x', 'x0', 'x_ss', 'sx', 'sx0', 'sx_ss', 'y', 'sigmay', - 'sy', 'ssigmay', 'z', 'rz', 'sigmaz', 'sz', 'srz', - 'ssigmaz', 'sllh', 's2llh', 'J', 'xdot', 'status', 'llh', - 'chi2', 'res', 'sres', 'FIM', 'w', 'preeq_wrms', 'preeq_t', - 'preeq_numsteps', 'preeq_numstepsB', 'preeq_status', 'preeq_cpu_time', - 'preeq_cpu_timeB', 'posteq_wrms', 'posteq_t', 'posteq_numsteps', - 'posteq_numstepsB', 'posteq_status', 'posteq_cpu_time', - 'posteq_cpu_timeB', 'numsteps', 'numrhsevals', - 'numerrtestfails', 'numnonlinsolvconvfails', 'order', 'cpu_time', - 'numstepsB', 'numrhsevalsB', 'numerrtestfailsB', - 'numnonlinsolvconvfailsB', 'cpu_timeB', 'cpu_time_total' + "ts", + "x", + "x0", + "x_ss", + "sx", + "sx0", + "sx_ss", + "y", + "sigmay", + "sy", + "ssigmay", + "z", + "rz", + "sigmaz", + "sz", + "srz", + "ssigmaz", + "sllh", + "s2llh", + "J", + "xdot", + "status", + "llh", + "chi2", + "res", + "sres", + "FIM", + "w", + "preeq_wrms", + "preeq_t", + "preeq_numsteps", + "preeq_numstepsB", + "preeq_status", + "preeq_cpu_time", + "preeq_cpu_timeB", + "posteq_wrms", + "posteq_t", + "posteq_numsteps", + "posteq_numstepsB", + "posteq_status", + "posteq_cpu_time", + "posteq_cpu_timeB", + "numsteps", + "numrhsevals", + "numerrtestfails", + "numnonlinsolvconvfails", + "order", + "cpu_time", + "numstepsB", + "numrhsevalsB", + "numerrtestfailsB", + "numnonlinsolvconvfailsB", + "cpu_timeB", + "cpu_time_total", ] def __init__(self, rdata: Union[ReturnDataPtr, ReturnData]): @@ -177,65 +219,63 @@ def __init__(self, rdata: Union[ReturnDataPtr, ReturnData]): :param rdata: pointer to the ``ReturnData`` instance """ if not isinstance(rdata, (ReturnDataPtr, ReturnData)): - raise TypeError(f'Unsupported pointer {type(rdata)}, must be' - f'amici.ExpDataPtr!') + raise TypeError( + f"Unsupported pointer {type(rdata)}, must be" f"amici.ExpDataPtr!" + ) self._field_dimensions = { - 'ts': [rdata.nt], - 'x': [rdata.nt, rdata.nx], - 'x0': [rdata.nx], - 'x_ss': [rdata.nx], - 'sx': [rdata.nt, rdata.nplist, rdata.nx], - 'sx0': [rdata.nplist, rdata.nx], - 'sx_ss': [rdata.nplist, rdata.nx], - + "ts": [rdata.nt], + "x": [rdata.nt, rdata.nx], + "x0": [rdata.nx], + "x_ss": [rdata.nx], + "sx": [rdata.nt, rdata.nplist, rdata.nx], + "sx0": [rdata.nplist, rdata.nx], + "sx_ss": [rdata.nplist, rdata.nx], # observables - 'y': [rdata.nt, rdata.ny], - 'sigmay': [rdata.nt, rdata.ny], - 'sy': [rdata.nt, rdata.nplist, rdata.ny], - 'ssigmay': [rdata.nt, rdata.nplist, rdata.ny], - + "y": [rdata.nt, rdata.ny], + "sigmay": [rdata.nt, rdata.ny], + "sy": [rdata.nt, rdata.nplist, rdata.ny], + "ssigmay": [rdata.nt, rdata.nplist, rdata.ny], # event observables - 'z': [rdata.nmaxevent, rdata.nz], - 'rz': [rdata.nmaxevent, rdata.nz], - 'sigmaz': [rdata.nmaxevent, rdata.nz], - 'sz': [rdata.nmaxevent, rdata.nplist, rdata.nz], - 'srz': [rdata.nmaxevent, rdata.nplist, rdata.nz], - 'ssigmaz': [rdata.nmaxevent, rdata.nplist, rdata.nz], - + "z": [rdata.nmaxevent, rdata.nz], + "rz": [rdata.nmaxevent, rdata.nz], + "sigmaz": [rdata.nmaxevent, rdata.nz], + "sz": [rdata.nmaxevent, rdata.nplist, rdata.nz], + "srz": [rdata.nmaxevent, rdata.nplist, rdata.nz], + "ssigmaz": [rdata.nmaxevent, rdata.nplist, rdata.nz], # objective function - 'sllh': [rdata.nplist], - 's2llh': [rdata.np, rdata.nplist], - - 'res': [rdata.nt * rdata.nytrue * - (2 if rdata.sigma_res else 1)], - 'sres': [rdata.nt * rdata.nytrue * - (2 if rdata.sigma_res else 1), rdata.nplist], - 'FIM': [rdata.nplist, rdata.nplist], - + "sllh": [rdata.nplist], + "s2llh": [rdata.np, rdata.nplist], + "res": [rdata.nt * rdata.nytrue * (2 if rdata.sigma_res else 1)], + "sres": [ + rdata.nt * rdata.nytrue * (2 if rdata.sigma_res else 1), + rdata.nplist, + ], + "FIM": [rdata.nplist, rdata.nplist], # diagnosis - 'J': [rdata.nx_solver, rdata.nx_solver], - 'w': [rdata.nt, rdata.nw], - 'xdot': [rdata.nx_solver], - 'preeq_numlinsteps': [rdata.newton_maxsteps, 2], - 'preeq_numsteps': [1, 3], - 'preeq_status': [1, 3], - 'posteq_numlinsteps': [rdata.newton_maxsteps, 2], - 'posteq_numsteps': [1, 3], - 'posteq_status': [1, 3], - 'numsteps': [rdata.nt], - 'numrhsevals': [rdata.nt], - 'numerrtestfails': [rdata.nt], - 'numnonlinsolvconvfails': [rdata.nt], - 'order': [rdata.nt], - 'numstepsB': [rdata.nt], - 'numrhsevalsB': [rdata.nt], - 'numerrtestfailsB': [rdata.nt], - 'numnonlinsolvconvfailsB': [rdata.nt], + "J": [rdata.nx_solver, rdata.nx_solver], + "w": [rdata.nt, rdata.nw], + "xdot": [rdata.nx_solver], + "preeq_numlinsteps": [rdata.newton_maxsteps, 2], + "preeq_numsteps": [1, 3], + "preeq_status": [1, 3], + "posteq_numlinsteps": [rdata.newton_maxsteps, 2], + "posteq_numsteps": [1, 3], + "posteq_status": [1, 3], + "numsteps": [rdata.nt], + "numrhsevals": [rdata.nt], + "numerrtestfails": [rdata.nt], + "numnonlinsolvconvfails": [rdata.nt], + "order": [rdata.nt], + "numstepsB": [rdata.nt], + "numrhsevalsB": [rdata.nt], + "numerrtestfailsB": [rdata.nt], + "numnonlinsolvconvfailsB": [rdata.nt], } super(ReturnDataView, self).__init__(rdata) - def __getitem__(self, item: str) -> Union[np.ndarray, ReturnDataPtr, - ReturnData, float]: + def __getitem__( + self, item: str + ) -> Union[np.ndarray, ReturnDataPtr, ReturnData, float]: """ Access fields by name.s @@ -245,20 +285,15 @@ def __getitem__(self, item: str) -> Union[np.ndarray, ReturnDataPtr, :returns: self[item] """ - if item == 'status': + if item == "status": return int(super().__getitem__(item)) - if item == 't': - item = 'ts' + if item == "t": + item = "ts" return super().__getitem__(item) - def by_id( - self, - entity_id: str, - field: str = None, - model: Model = None - ) -> np.array: + def by_id(self, entity_id: str, field: str = None, model: Model = None) -> np.array: """ Get the value of a given field for a named entity. @@ -273,17 +308,14 @@ def by_id( if field is None: field = _entity_type_from_id(entity_id, self, model) - if field in {'x', 'x0', 'x_ss', 'sx', 'sx0', 'sx_ss'}: + if field in {"x", "x0", "x_ss", "sx", "sx0", "sx_ss"}: ids = (model and model.getStateIds()) or self._swigptr.state_ids - elif field in {'w'}: - ids = (model and model.getExpressionIds()) \ - or self._swigptr.expression_ids - elif field in {'y', 'sy', 'sigmay'}: - ids = (model and model.getObservableIds()) \ - or self._swigptr.observable_ids - elif field in {'sllh'}: - ids = (model and model.getParameterIds()) \ - or self._swigptr.parameter_ids + elif field in {"w"}: + ids = (model and model.getExpressionIds()) or self._swigptr.expression_ids + elif field in {"y", "sy", "sigmay"}: + ids = (model and model.getObservableIds()) or self._swigptr.observable_ids + elif field in {"sllh"}: + ids = (model and model.getParameterIds()) or self._swigptr.parameter_ids else: raise NotImplementedError( f"Subsetting {field} by ID is not implemented or not possible." @@ -299,10 +331,13 @@ class ExpDataView(SwigPtrView): """ _field_names = [ - 'observedData', 'observedDataStdDev', 'observedEvents', - 'observedEventsStdDev', 'fixedParameters', - 'fixedParametersPreequilibration', - 'fixedParametersPresimulation' + "observedData", + "observedDataStdDev", + "observedEvents", + "observedEventsStdDev", + "fixedParameters", + "fixedParametersPreequilibration", + "fixedParametersPresimulation", ] def __init__(self, edata: Union[ExpDataPtr, ExpData]): @@ -312,22 +347,23 @@ def __init__(self, edata: Union[ExpDataPtr, ExpData]): :param edata: pointer to the ExpData instance """ if not isinstance(edata, (ExpDataPtr, ExpData)): - raise TypeError(f'Unsupported pointer {type(edata)}, must be' - f'amici.ExpDataPtr!') + raise TypeError( + f"Unsupported pointer {type(edata)}, must be" f"amici.ExpDataPtr!" + ) self._field_dimensions = { # observables - 'observedData': [edata.nt(), edata.nytrue()], - 'observedDataStdDev': [edata.nt(), edata.nytrue()], - + "observedData": [edata.nt(), edata.nytrue()], + "observedDataStdDev": [edata.nt(), edata.nytrue()], # event observables - 'observedEvents': [edata.nmaxevent(), edata.nztrue()], - 'observedEventsStdDev': [edata.nmaxevent(), edata.nztrue()], - + "observedEvents": [edata.nmaxevent(), edata.nztrue()], + "observedEventsStdDev": [edata.nmaxevent(), edata.nztrue()], # fixed parameters - 'fixedParameters': [len(edata.fixedParameters)], - 'fixedParametersPreequilibration': [ - len(edata.fixedParametersPreequilibration)], - 'fixedParametersPresimulation': [ - len(edata.fixedParametersPreequilibration)], + "fixedParameters": [len(edata.fixedParameters)], + "fixedParametersPreequilibration": [ + len(edata.fixedParametersPreequilibration) + ], + "fixedParametersPresimulation": [ + len(edata.fixedParametersPreequilibration) + ], } edata.observedData = edata.getObservedData() edata.observedDataStdDev = edata.getObservedDataStdDev() @@ -337,8 +373,7 @@ def __init__(self, edata: Union[ExpDataPtr, ExpData]): def _field_as_numpy( - field_dimensions: Dict[str, List[int]], - field: str, data: SwigPtrView + field_dimensions: Dict[str, List[int]], field: str, data: SwigPtrView ) -> Union[np.ndarray, float, None]: """ Convert data object field to numpy array with dimensions according to @@ -359,26 +394,26 @@ def _field_as_numpy( def _entity_type_from_id( - entity_id: str, - rdata: Union[amici.ReturnData, 'amici.ReturnDataView'] = None, - model: amici.Model = None, -) -> Literal['x', 'y', 'w', 'p', 'k']: + entity_id: str, + rdata: Union[amici.ReturnData, "amici.ReturnDataView"] = None, + model: amici.Model = None, +) -> Literal["x", "y", "w", "p", "k"]: """Guess the type of some entity by its ID.""" for entity_type, symbol in ( - ('State', 'x'), - ('Observable', 'y'), - ('Expression', 'w'), - ('Parameter', 'p'), - ('FixedParameter', 'k') + ("State", "x"), + ("Observable", "y"), + ("Expression", "w"), + ("Parameter", "p"), + ("FixedParameter", "k"), ): if model: - if entity_id in getattr(model, f'get{entity_type}Ids')(): + if entity_id in getattr(model, f"get{entity_type}Ids")(): return symbol else: if entity_id in getattr( - rdata if isinstance(rdata, amici.ReturnData) - else rdata._swigptr, - f'{entity_type.lower()}_ids'): + rdata if isinstance(rdata, amici.ReturnData) else rdata._swigptr, + f"{entity_type.lower()}_ids", + ): return symbol raise KeyError(f"Unknown symbol {entity_id}.") diff --git a/python/sdist/amici/pandas.py b/python/sdist/amici/pandas.py index 4842bbec47..dd240242af 100644 --- a/python/sdist/amici/pandas.py +++ b/python/sdist/amici/pandas.py @@ -5,31 +5,32 @@ between C++ objects from :mod:`amici.amici` and pandas DataFrames """ -import pandas as pd -import numpy as np -import math import copy +import math +from typing import Dict, List, Optional, SupportsFloat, Union -from typing import List, Union, Optional, Dict, SupportsFloat -from .numpy import ExpDataView import amici +import numpy as np +import pandas as pd + +from .numpy import ExpDataView __all__ = [ - 'get_expressions_as_dataframe', - 'getEdataFromDataFrame', - 'getDataObservablesAsDataFrame', - 'getSimulationObservablesAsDataFrame', - 'getSimulationStatesAsDataFrame', - 'getResidualsAsDataFrame' + "get_expressions_as_dataframe", + "getEdataFromDataFrame", + "getDataObservablesAsDataFrame", + "getSimulationObservablesAsDataFrame", + "getSimulationStatesAsDataFrame", + "getResidualsAsDataFrame", ] ExpDatas = Union[ - List[amici.amici.ExpData], List[amici.ExpDataPtr], - amici.amici.ExpData, amici.ExpDataPtr -] -ReturnDatas = Union[ - List[amici.ReturnDataView], amici.ReturnDataView + List[amici.amici.ExpData], + List[amici.ExpDataPtr], + amici.amici.ExpData, + amici.ExpDataPtr, ] +ReturnDatas = Union[List[amici.ReturnDataView], amici.ReturnDataView] AmiciModel = Union[amici.ModelPtr, amici.Model] @@ -69,9 +70,8 @@ def _process_rdata_list(rdata_list: ReturnDatas) -> List[amici.ReturnDataView]: def getDataObservablesAsDataFrame( - model: AmiciModel, - edata_list: ExpDatas, - by_id: Optional[bool] = False) -> pd.DataFrame: + model: AmiciModel, edata_list: ExpDatas, by_id: Optional[bool] = False +) -> pd.DataFrame: """ Write Observables from experimental data as DataFrame. @@ -101,16 +101,13 @@ def getDataObservablesAsDataFrame( for edata in edata_list: npdata = ExpDataView(edata) for i_time, timepoint in enumerate(edata.getTimepoints()): - datadict = { - 'time': timepoint, - 'datatype': 'data' - } + datadict = {"time": timepoint, "datatype": "data"} # add observables and noises - for i_obs, obs in enumerate(_get_names_or_ids( - model, 'Observable', by_id=by_id)): - datadict[obs] = npdata['observedData'][i_time, i_obs] - datadict[obs + '_std'] = \ - npdata['observedDataStdDev'][i_time, i_obs] + for i_obs, obs in enumerate( + _get_names_or_ids(model, "Observable", by_id=by_id) + ): + datadict[obs] = npdata["observedData"][i_time, i_obs] + datadict[obs + "_std"] = npdata["observedDataStdDev"][i_time, i_obs] # add conditions _fill_conditions_dict(datadict, model, edata, by_id=by_id) @@ -121,10 +118,10 @@ def getDataObservablesAsDataFrame( def getSimulationObservablesAsDataFrame( - model: amici.Model, - edata_list: ExpDatas, - rdata_list: ReturnDatas, - by_id: Optional[bool] = False + model: amici.Model, + edata_list: ExpDatas, + rdata_list: ReturnDatas, + by_id: Optional[bool] = False, ) -> pd.DataFrame: """ Write Observables from simulation results as DataFrame. @@ -157,16 +154,17 @@ def getSimulationObservablesAsDataFrame( # aggregate records dicts = [] for edata, rdata in zip(edata_list, rdata_list): - for i_time, timepoint in enumerate(rdata['t']): + for i_time, timepoint in enumerate(rdata["t"]): datadict = { - 'time': timepoint, - 'datatype': 'simulation', + "time": timepoint, + "datatype": "simulation", } # append simulations - for i_obs, obs in enumerate(_get_names_or_ids( - model, 'Observable', by_id=by_id)): - datadict[obs] = rdata['y'][i_time, i_obs] - datadict[obs + '_std'] = rdata['sigmay'][i_time, i_obs] + for i_obs, obs in enumerate( + _get_names_or_ids(model, "Observable", by_id=by_id) + ): + datadict[obs] = rdata["y"][i_time, i_obs] + datadict[obs + "_std"] = rdata["sigmay"][i_time, i_obs] # use edata to fill conditions columns _fill_conditions_dict(datadict, model, edata, by_id=by_id) @@ -178,10 +176,11 @@ def getSimulationObservablesAsDataFrame( def getSimulationStatesAsDataFrame( - model: amici.Model, - edata_list: ExpDatas, - rdata_list: ReturnDatas, - by_id: Optional[bool] = False) -> pd.DataFrame: + model: amici.Model, + edata_list: ExpDatas, + rdata_list: ReturnDatas, + by_id: Optional[bool] = False, +) -> pd.DataFrame: """ Get model state according to lists of ReturnData and ExpData. @@ -212,15 +211,16 @@ def getSimulationStatesAsDataFrame( # aggregate records dicts = [] for edata, rdata in zip(edata_list, rdata_list): - for i_time, timepoint in enumerate(rdata['t']): + for i_time, timepoint in enumerate(rdata["t"]): datadict = { - 'time': timepoint, + "time": timepoint, } # append states for i_state, state in enumerate( - _get_names_or_ids(model, 'State', by_id=by_id)): - datadict[state] = rdata['x'][i_time, i_state] + _get_names_or_ids(model, "State", by_id=by_id) + ): + datadict[state] = rdata["x"][i_time, i_state] # use data to fill condition columns _fill_conditions_dict(datadict, model, edata, by_id=by_id) @@ -232,10 +232,11 @@ def getSimulationStatesAsDataFrame( def get_expressions_as_dataframe( - model: amici.Model, - edata_list: ExpDatas, - rdata_list: ReturnDatas, - by_id: Optional[bool] = False) -> pd.DataFrame: + model: amici.Model, + edata_list: ExpDatas, + rdata_list: ReturnDatas, + by_id: Optional[bool] = False, +) -> pd.DataFrame: """ Get values of model expressions from lists of ReturnData as DataFrame. @@ -266,15 +267,16 @@ def get_expressions_as_dataframe( # aggregate records dicts = [] for edata, rdata in zip(edata_list, rdata_list): - for i_time, timepoint in enumerate(rdata['t']): + for i_time, timepoint in enumerate(rdata["t"]): datadict = { - 'time': timepoint, + "time": timepoint, } # append expressions for i_expr, expr in enumerate( - _get_names_or_ids(model, 'Expression', by_id=by_id)): - datadict[expr] = rdata['w'][i_time, i_expr] + _get_names_or_ids(model, "Expression", by_id=by_id) + ): + datadict[expr] = rdata["w"][i_time, i_expr] # use data to fill condition columns _fill_conditions_dict(datadict, model, edata, by_id=by_id) @@ -285,10 +287,12 @@ def get_expressions_as_dataframe( return pd.DataFrame.from_records(dicts, columns=cols) -def getResidualsAsDataFrame(model: amici.Model, - edata_list: ExpDatas, - rdata_list: ReturnDatas, - by_id: Optional[bool] = False) -> pd.DataFrame: +def getResidualsAsDataFrame( + model: amici.Model, + edata_list: ExpDatas, + rdata_list: ReturnDatas, + by_id: Optional[bool] = False, +) -> pd.DataFrame: """ Convert a list of ReturnData and ExpData to pandas DataFrame with residuals. @@ -315,10 +319,10 @@ def getResidualsAsDataFrame(model: amici.Model, rdata_list = _process_rdata_list(rdata_list) # create observable and simulation dataframes - df_edata = getDataObservablesAsDataFrame( - model, edata_list, by_id=by_id) + df_edata = getDataObservablesAsDataFrame(model, edata_list, by_id=by_id) df_rdata = getSimulationObservablesAsDataFrame( - model, edata_list, rdata_list, by_id=by_id) + model, edata_list, rdata_list, by_id=by_id + ) # get all column names using names or ids cols = _get_observable_cols(model, by_id=by_id) @@ -327,23 +331,24 @@ def getResidualsAsDataFrame(model: amici.Model, dicts = [] for row in df_rdata.index: datadict = { - 'time': df_rdata.loc[row]['time'], - 't_presim': df_rdata.loc[row]['t_presim'] + "time": df_rdata.loc[row]["time"], + "t_presim": df_rdata.loc[row]["t_presim"], } # iterate over observables - for obs in _get_names_or_ids(model, 'Observable', by_id=by_id): + for obs in _get_names_or_ids(model, "Observable", by_id=by_id): # compute residual and append to dict datadict[obs] = abs( - (df_edata.loc[row][obs] - df_rdata.loc[row][obs]) / - df_rdata.loc[row][obs + '_std']) + (df_edata.loc[row][obs] - df_rdata.loc[row][obs]) + / df_rdata.loc[row][obs + "_std"] + ) # iterate over fixed parameters - for par in _get_names_or_ids(model, 'FixedParameter', by_id=by_id): + for par in _get_names_or_ids(model, "FixedParameter", by_id=by_id): # fill in conditions datadict[par] = df_rdata.loc[row][par] - datadict[par + '_preeq'] = df_rdata.loc[row][par + '_preeq'] - datadict[par + '_presim'] = df_rdata.loc[row][par + '_presim'] + datadict[par + "_preeq"] = df_rdata.loc[row][par + "_preeq"] + datadict[par + "_presim"] = df_rdata.loc[row][par + "_presim"] # append to dataframe dicts.append(datadict) @@ -351,10 +356,12 @@ def getResidualsAsDataFrame(model: amici.Model, return pd.DataFrame.from_records(dicts, columns=cols) -def _fill_conditions_dict(datadict: Dict[str, float], - model: AmiciModel, - edata: amici.amici.ExpData, - by_id: bool) -> Dict[str, float]: +def _fill_conditions_dict( + datadict: Dict[str, float], + model: AmiciModel, + edata: amici.amici.ExpData, + by_id: bool, +) -> Dict[str, float]: """ Helper function that fills in condition parameters from model and edata. @@ -377,32 +384,30 @@ def _fill_conditions_dict(datadict: Dict[str, float], dictionary with filled condition parameters. """ - datadict['condition_id'] = edata.id - datadict['t_presim'] = edata.t_presim + datadict["condition_id"] = edata.id + datadict["t_presim"] = edata.t_presim for i_par, par in enumerate( - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)): + _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ): if len(edata.fixedParameters): datadict[par] = edata.fixedParameters[i_par] else: datadict[par] = model.getFixedParameters()[i_par] if len(edata.fixedParametersPreequilibration): - datadict[par + '_preeq'] = \ - edata.fixedParametersPreequilibration[i_par] + datadict[par + "_preeq"] = edata.fixedParametersPreequilibration[i_par] else: - datadict[par + '_preeq'] = np.nan + datadict[par + "_preeq"] = np.nan if len(edata.fixedParametersPresimulation): - datadict[par + '_presim'] = \ - edata.fixedParametersPresimulation[i_par] + datadict[par + "_presim"] = edata.fixedParametersPresimulation[i_par] else: - datadict[par + '_presim'] = np.nan + datadict[par + "_presim"] = np.nan return datadict -def _get_extended_observable_cols(model: AmiciModel, - by_id: bool) -> List[str]: +def _get_extended_observable_cols(model: AmiciModel, by_id: bool) -> List[str]: """ Construction helper for extended observable dataframe headers. @@ -416,20 +421,26 @@ def _get_extended_observable_cols(model: AmiciModel, :return: column names as list. """ - return \ - ['condition_id', 'time', 'datatype', 't_presim'] + \ - _get_names_or_ids(model, 'FixedParameter', by_id=by_id) + \ - [name + '_preeq' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - [name + '_presim' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - _get_names_or_ids(model, 'Observable', by_id=by_id) + \ - [name + '_std' for name in - _get_names_or_ids(model, 'Observable', by_id=by_id)] - - -def _get_observable_cols(model: AmiciModel, - by_id: bool) -> List[str]: + return ( + ["condition_id", "time", "datatype", "t_presim"] + + _get_names_or_ids(model, "FixedParameter", by_id=by_id) + + [ + name + "_preeq" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + [ + name + "_presim" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + _get_names_or_ids(model, "Observable", by_id=by_id) + + [ + name + "_std" + for name in _get_names_or_ids(model, "Observable", by_id=by_id) + ] + ) + + +def _get_observable_cols(model: AmiciModel, by_id: bool) -> List[str]: """ Construction helper for observable dataframe headers. @@ -443,18 +454,22 @@ def _get_observable_cols(model: AmiciModel, :return: column names as list. """ - return \ - ['condition_id', 'time', 't_presim'] + \ - _get_names_or_ids(model, 'FixedParameter', by_id=by_id) + \ - [name + '_preeq' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - [name + '_presim' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - _get_names_or_ids(model, 'Observable', by_id=by_id) - - -def _get_state_cols(model: AmiciModel, - by_id: bool) -> List[str]: + return ( + ["condition_id", "time", "t_presim"] + + _get_names_or_ids(model, "FixedParameter", by_id=by_id) + + [ + name + "_preeq" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + [ + name + "_presim" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + _get_names_or_ids(model, "Observable", by_id=by_id) + ) + + +def _get_state_cols(model: AmiciModel, by_id: bool) -> List[str]: """ Construction helper for state dataframe headers. @@ -468,14 +483,19 @@ def _get_state_cols(model: AmiciModel, :return: column names as list. """ - return \ - ['condition_id', 'time', 't_presim'] + \ - _get_names_or_ids(model, 'FixedParameter', by_id=by_id) + \ - [name + '_preeq' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - [name + '_presim' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - _get_names_or_ids(model, 'State', by_id=by_id) + return ( + ["condition_id", "time", "t_presim"] + + _get_names_or_ids(model, "FixedParameter", by_id=by_id) + + [ + name + "_preeq" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + [ + name + "_presim" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + _get_names_or_ids(model, "State", by_id=by_id) + ) def _get_expression_cols(model: AmiciModel, by_id: bool) -> List[str]: @@ -491,19 +511,22 @@ def _get_expression_cols(model: AmiciModel, by_id: bool) -> List[str]: :return: column names as list. """ - return \ - ['condition_id', 'time', 't_presim'] + \ - _get_names_or_ids(model, 'FixedParameter', by_id=by_id) + \ - [name + '_preeq' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - [name + '_presim' for name in - _get_names_or_ids(model, 'FixedParameter', by_id=by_id)] + \ - _get_names_or_ids(model, 'Expression', by_id=by_id) - - -def _get_names_or_ids(model: AmiciModel, - variable: str, - by_id: bool) -> List[str]: + return ( + ["condition_id", "time", "t_presim"] + + _get_names_or_ids(model, "FixedParameter", by_id=by_id) + + [ + name + "_preeq" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + [ + name + "_presim" + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] + + _get_names_or_ids(model, "Expression", by_id=by_id) + ) + + +def _get_names_or_ids(model: AmiciModel, variable: str, by_id: bool) -> List[str]: """ Obtains a unique list of identifiers for the specified variable. First tries model.getVariableNames and then uses model.getVariableIds. @@ -523,18 +546,22 @@ def _get_names_or_ids(model: AmiciModel, """ # check whether variable type permitted variable_options = [ - 'Parameter', 'FixedParameter', 'Observable', 'State', 'Expression' + "Parameter", + "FixedParameter", + "Observable", + "State", + "Expression", ] if variable not in variable_options: - raise ValueError('Variable must be in ' + str(variable_options)) + raise ValueError("Variable must be in " + str(variable_options)) # extract attributes - names = list(getattr(model, f'get{variable}Names')()) - ids = list(getattr(model, f'get{variable}Ids')()) + names = list(getattr(model, f"get{variable}Names")()) + ids = list(getattr(model, f"get{variable}Ids")()) # find out if model has names and ids - has_names = getattr(model, f'has{variable}Names')() - has_ids = getattr(model, f'has{variable}Ids')() + has_names = getattr(model, f"has{variable}Names")() + has_ids = getattr(model, f"has{variable}Ids")() # extract labels if not by_id and has_names and len(set(names)) == len(names): @@ -548,16 +575,18 @@ def _get_names_or_ids(model: AmiciModel, if by_id: msg = f"Model {variable} ids are not set." else: - msg = f"Model {variable} names are not unique and " \ - f"{variable} ids are not set." + msg = ( + f"Model {variable} names are not unique and " + f"{variable} ids are not set." + ) raise ValueError(msg) def _get_specialized_fixed_parameters( - model: AmiciModel, - condition: Union[Dict[str, SupportsFloat], pd.Series], - overwrite: Union[Dict[str, SupportsFloat], pd.Series], - by_id: bool + model: AmiciModel, + condition: Union[Dict[str, SupportsFloat], pd.Series], + overwrite: Union[Dict[str, SupportsFloat], pd.Series], + by_id: bool, ) -> List[float]: """ Copies values in condition and overwrites them according to key @@ -580,15 +609,17 @@ def _get_specialized_fixed_parameters( cond = copy.deepcopy(condition) for field in overwrite: cond[field] = overwrite[field] - return [float(cond[name]) for name in _get_names_or_ids( - model, 'FixedParameter', by_id=by_id)] + return [ + float(cond[name]) + for name in _get_names_or_ids(model, "FixedParameter", by_id=by_id) + ] def constructEdataFromDataFrame( - df: pd.DataFrame, - model: AmiciModel, - condition: pd.Series, - by_id: Optional[bool] = False + df: pd.DataFrame, + model: AmiciModel, + condition: pd.Series, + by_id: Optional[bool] = False, ) -> amici.amici.ExpData: """ Constructs an ExpData instance according to the provided Model @@ -619,68 +650,67 @@ def constructEdataFromDataFrame( edata = amici.ExpData(model.get()) # timepoints - df = df.sort_values(by='time', ascending=True) - edata.setTimepoints(df['time'].values.astype(float)) + df = df.sort_values(by="time", ascending=True) + edata.setTimepoints(df["time"].values.astype(float)) # get fixed parameters from condition overwrite_preeq = {} overwrite_presim = {} - for par in list(_get_names_or_ids(model, 'FixedParameter', by_id=by_id)): - if par + '_preeq' in condition.keys() \ - and not math.isnan(condition[par + '_preeq'].astype(float)): - overwrite_preeq[par] = condition[par + '_preeq'].astype(float) - if par + '_presim' in condition.keys() \ - and not math.isnan(condition[par + '_presim'].astype(float)): - overwrite_presim[par] = condition[par + '_presim'].astype(float) + for par in list(_get_names_or_ids(model, "FixedParameter", by_id=by_id)): + if par + "_preeq" in condition.keys() and not math.isnan( + condition[par + "_preeq"].astype(float) + ): + overwrite_preeq[par] = condition[par + "_preeq"].astype(float) + if par + "_presim" in condition.keys() and not math.isnan( + condition[par + "_presim"].astype(float) + ): + overwrite_presim[par] = condition[par + "_presim"].astype(float) # fill in fixed parameters - edata.fixedParameters = condition[ - _get_names_or_ids(model, 'FixedParameter', by_id=by_id) - ].astype(float).values + edata.fixedParameters = ( + condition[_get_names_or_ids(model, "FixedParameter", by_id=by_id)] + .astype(float) + .values + ) # fill in preequilibration parameters - if any([overwrite_preeq[key] != condition[key] for key in - overwrite_preeq]): - edata.fixedParametersPreequilibration = \ - _get_specialized_fixed_parameters( - model, condition, overwrite_preeq, by_id=by_id) - elif len(overwrite_preeq): - edata.fixedParametersPreequilibration = copy.deepcopy( - edata.fixedParameters + if any([overwrite_preeq[key] != condition[key] for key in overwrite_preeq]): + edata.fixedParametersPreequilibration = _get_specialized_fixed_parameters( + model, condition, overwrite_preeq, by_id=by_id ) + elif len(overwrite_preeq): + edata.fixedParametersPreequilibration = copy.deepcopy(edata.fixedParameters) # fill in presimulation parameters - if any([overwrite_presim[key] != condition[key] for key in - overwrite_presim.keys()]): + if any( + [overwrite_presim[key] != condition[key] for key in overwrite_presim.keys()] + ): edata.fixedParametersPresimulation = _get_specialized_fixed_parameters( model, condition, overwrite_presim, by_id=by_id ) elif len(overwrite_presim.keys()): - edata.fixedParametersPresimulation = copy.deepcopy( - edata.fixedParameters - ) + edata.fixedParametersPresimulation = copy.deepcopy(edata.fixedParameters) # fill in presimulation time - if 't_presim' in condition.keys(): - edata.t_presim = float(condition['t_presim']) + if "t_presim" in condition.keys(): + edata.t_presim = float(condition["t_presim"]) # fill in data and stds for obs_index, obs in enumerate( - _get_names_or_ids(model, 'Observable', by_id=by_id)): + _get_names_or_ids(model, "Observable", by_id=by_id) + ): if obs in df.keys(): edata.setObservedData(df[obs].values.astype(float), obs_index) - if obs + '_std' in df.keys(): + if obs + "_std" in df.keys(): edata.setObservedDataStdDev( - df[obs + '_std'].values.astype(float), obs_index + df[obs + "_std"].values.astype(float), obs_index ) return edata def getEdataFromDataFrame( - model: AmiciModel, - df: pd.DataFrame, - by_id: Optional[bool] = False + model: AmiciModel, df: pd.DataFrame, by_id: Optional[bool] = False ) -> List[amici.amici.ExpData]: """ Constructs a ExpData instances according to the provided Model and @@ -709,17 +739,16 @@ def getEdataFromDataFrame( # aggregate features that define a condition # fixed parameters - condition_parameters = _get_names_or_ids(model, 'FixedParameter', - by_id=by_id) + condition_parameters = _get_names_or_ids(model, "FixedParameter", by_id=by_id) # preeq and presim parameters - for par in _get_names_or_ids(model, 'FixedParameter', by_id=by_id): - if par + '_preeq' in df.columns: - condition_parameters.append(par + '_preeq') - if par + '_presim' in df.columns: - condition_parameters.append(par + '_presim') + for par in _get_names_or_ids(model, "FixedParameter", by_id=by_id): + if par + "_preeq" in df.columns: + condition_parameters.append(par + "_preeq") + if par + "_presim" in df.columns: + condition_parameters.append(par + "_presim") # presimulation time - if 't_presim' in df.columns: - condition_parameters.append('t_presim') + if "t_presim" in df.columns: + condition_parameters.append("t_presim") # drop duplicates to create final conditions conditions = df[condition_parameters].drop_duplicates() @@ -729,9 +758,7 @@ def getEdataFromDataFrame( selected = np.ones((len(df),), dtype=bool) for par_label, par in row.items(): if math.isnan(par): - selected = selected & np.isnan( - df[par_label].astype(float).values - ) + selected = selected & np.isnan(df[par_label].astype(float).values) else: selected = selected & (df[par_label] == par) edata_df = df[selected] diff --git a/python/sdist/amici/parameter_mapping.py b/python/sdist/amici/parameter_mapping.py index ed3eaa2b08..9f4d3b24dd 100644 --- a/python/sdist/amici/parameter_mapping.py +++ b/python/sdist/amici/parameter_mapping.py @@ -17,15 +17,14 @@ import numbers import warnings -from typing import Any, Dict, List, Union, Set from collections.abc import Sequence from itertools import chain +from typing import Any, Dict, List, Set, Union import amici import numpy as np from petab.C import * # noqa: F403 - SingleParameterMapping = Dict[str, Union[numbers.Number, str]] SingleScaleMapping = Dict[str, str] AmiciModel = Union[amici.Model, amici.ModelPtr] @@ -61,13 +60,13 @@ class ParameterMappingForCondition: """ def __init__( - self, - map_sim_var: SingleParameterMapping = None, - scale_map_sim_var: SingleScaleMapping = None, - map_preeq_fix: SingleParameterMapping = None, - scale_map_preeq_fix: SingleScaleMapping = None, - map_sim_fix: SingleParameterMapping = None, - scale_map_sim_fix: SingleScaleMapping = None, + self, + map_sim_var: SingleParameterMapping = None, + scale_map_sim_var: SingleScaleMapping = None, + map_preeq_fix: SingleParameterMapping = None, + scale_map_preeq_fix: SingleScaleMapping = None, + map_sim_fix: SingleParameterMapping = None, + scale_map_sim_fix: SingleScaleMapping = None, ): if map_sim_var is None: map_sim_var = {} @@ -94,22 +93,25 @@ def __init__( self.scale_map_sim_fix = scale_map_sim_fix def __repr__(self): - return (f"{self.__class__.__name__}(" - f"map_sim_var={repr(self.map_sim_var)}," - f"scale_map_sim_var={repr(self.scale_map_sim_var)}," - f"map_preeq_fix={repr(self.map_preeq_fix)}," - f"scale_map_preeq_fix={repr(self.scale_map_preeq_fix)}," - f"map_sim_fix={repr(self.map_sim_fix)}," - f"scale_map_sim_fix={repr(self.scale_map_sim_fix)})") + return ( + f"{self.__class__.__name__}(" + f"map_sim_var={repr(self.map_sim_var)}," + f"scale_map_sim_var={repr(self.scale_map_sim_var)}," + f"map_preeq_fix={repr(self.map_preeq_fix)}," + f"scale_map_preeq_fix={repr(self.scale_map_preeq_fix)}," + f"map_sim_fix={repr(self.map_sim_fix)}," + f"scale_map_sim_fix={repr(self.scale_map_sim_fix)})" + ) @property def free_symbols(self) -> Set[str]: """Get IDs of all (symbolic) parameters present in this mapping""" return { - p for p in chain( + p + for p in chain( self.map_sim_var.values(), self.map_preeq_fix.values(), - self.map_sim_fix.values() + self.map_sim_fix.values(), ) if isinstance(p, str) } @@ -124,10 +126,7 @@ class ParameterMapping(Sequence): List of parameter mappings for specific conditions. """ - def __init__( - self, - parameter_mappings: List[ParameterMappingForCondition] = None - ): + def __init__(self, parameter_mappings: List[ParameterMappingForCondition] = None): super().__init__() if parameter_mappings is None: parameter_mappings = [] @@ -137,7 +136,7 @@ def __iter__(self): yield from self.parameter_mappings def __getitem__( - self, item + self, item ) -> Union[ParameterMapping, ParameterMappingForCondition]: result = self.parameter_mappings[item] if isinstance(result, ParameterMappingForCondition): @@ -147,10 +146,7 @@ def __getitem__( def __len__(self): return len(self.parameter_mappings) - def append( - self, - parameter_mapping_for_condition: ParameterMappingForCondition - ): + def append(self, parameter_mapping_for_condition: ParameterMappingForCondition): """Append a condition specific parameter mapping.""" self.parameter_mappings.append(parameter_mapping_for_condition) @@ -164,11 +160,11 @@ def free_symbols(self) -> Set[str]: def fill_in_parameters( - edatas: List[amici.ExpData], - problem_parameters: Dict[str, numbers.Number], - scaled_parameters: bool, - parameter_mapping: ParameterMapping, - amici_model: AmiciModel + edatas: List[amici.ExpData], + problem_parameters: Dict[str, numbers.Number], + scaled_parameters: bool, + parameter_mapping: ParameterMapping, + amici_model: AmiciModel, ) -> None: """Fill fixed and dynamic parameters into the edatas (in-place). @@ -188,23 +184,31 @@ def fill_in_parameters( :param amici_model: AMICI model. """ - if unused_parameters := (set(problem_parameters.keys()) - - parameter_mapping.free_symbols): - warnings.warn("The following problem parameters were not used: " - + str(unused_parameters), RuntimeWarning) + if unused_parameters := ( + set(problem_parameters.keys()) - parameter_mapping.free_symbols + ): + warnings.warn( + "The following problem parameters were not used: " + str(unused_parameters), + RuntimeWarning, + ) for edata, mapping_for_condition in zip(edatas, parameter_mapping): fill_in_parameters_for_condition( - edata, problem_parameters, scaled_parameters, - mapping_for_condition, amici_model) + edata, + problem_parameters, + scaled_parameters, + mapping_for_condition, + amici_model, + ) def fill_in_parameters_for_condition( - edata: amici.ExpData, - problem_parameters: Dict[str, numbers.Number], - scaled_parameters: bool, - parameter_mapping: ParameterMappingForCondition, - amici_model: AmiciModel) -> None: + edata: amici.ExpData, + problem_parameters: Dict[str, numbers.Number], + scaled_parameters: bool, + parameter_mapping: ParameterMappingForCondition, + amici_model: AmiciModel, +) -> None: """Fill fixed and dynamic parameters into the edata for condition (in-place). @@ -244,6 +248,9 @@ def _get_par(model_par, value, mapping): # condition table overrides must have been handled already, # e.g. by the PEtab parameter mapping, but parameters from # InitialAssignments may still be present. + if mapping[value] == model_par: + # prevent infinite recursion + raise return _get_par(value, mapping[value], mapping) if model_par in problem_parameters: # user-provided @@ -254,12 +261,16 @@ def _get_par(model_par, value, mapping): # constant value return value - map_preeq_fix = {key: _get_par(key, val, map_preeq_fix) - for key, val in map_preeq_fix.items()} - map_sim_fix = {key: _get_par(key, val, map_sim_fix) - for key, val in map_sim_fix.items()} - map_sim_var = {key: _get_par(key, val, dict(map_sim_fix, **map_sim_var)) - for key, val in map_sim_var.items()} + map_preeq_fix = { + key: _get_par(key, val, map_preeq_fix) for key, val in map_preeq_fix.items() + } + map_sim_fix = { + key: _get_par(key, val, map_sim_fix) for key, val in map_sim_fix.items() + } + map_sim_var = { + key: _get_par(key, val, dict(map_sim_fix, **map_sim_var)) + for key, val in map_sim_var.items() + } # If necessary, (un)scale parameters if scaled_parameters: @@ -278,16 +289,18 @@ def _get_par(model_par, value, mapping): # variable parameters and parameter scale # parameter list from mapping dict - parameters = [map_sim_var[par_id] - for par_id in amici_model.getParameterIds()] + parameters = [map_sim_var[par_id] for par_id in amici_model.getParameterIds()] # scales list from mapping dict - scales = [petab_to_amici_scale(scale_map_sim_var[par_id]) - for par_id in amici_model.getParameterIds()] + scales = [ + petab_to_amici_scale(scale_map_sim_var[par_id]) + for par_id in amici_model.getParameterIds() + ] # plist plist = [ - ip for ip, par_id in enumerate(amici_model.getParameterIds()) + ip + for ip, par_id in enumerate(amici_model.getParameterIds()) if isinstance(parameter_mapping.map_sim_var[par_id], str) ] @@ -303,15 +316,17 @@ def _get_par(model_par, value, mapping): ########################################################################## # fixed parameters preequilibration if map_preeq_fix: - fixed_pars_preeq = [map_preeq_fix[par_id] - for par_id in amici_model.getFixedParameterIds()] + fixed_pars_preeq = [ + map_preeq_fix[par_id] for par_id in amici_model.getFixedParameterIds() + ] edata.fixedParametersPreequilibration = fixed_pars_preeq ########################################################################## # fixed parameters simulation if map_sim_fix: - fixed_pars_sim = [map_sim_fix[par_id] - for par_id in amici_model.getFixedParameterIds()] + fixed_pars_sim = [ + map_sim_fix[par_id] for par_id in amici_model.getFixedParameterIds() + ] edata.fixedParameters = fixed_pars_sim @@ -337,8 +352,7 @@ def amici_to_petab_scale(amici_scale: int) -> str: raise ValueError(f"AMICI scale not recognized: {amici_scale}") -def scale_parameter(value: numbers.Number, - petab_scale: str) -> numbers.Number: +def scale_parameter(value: numbers.Number, petab_scale: str) -> numbers.Number: """Bring parameter from linear scale to target scale. :param value: @@ -355,12 +369,12 @@ def scale_parameter(value: numbers.Number, return np.log10(value) if petab_scale == LOG: return np.log(value) - raise ValueError(f"Unknown parameter scale {petab_scale}. " - f"Must be from {(LIN, LOG, LOG10)}") + raise ValueError( + f"Unknown parameter scale {petab_scale}. " f"Must be from {(LIN, LOG, LOG10)}" + ) -def unscale_parameter(value: numbers.Number, - petab_scale: str) -> numbers.Number: +def unscale_parameter(value: numbers.Number, petab_scale: str) -> numbers.Number: """Bring parameter from scale to linear scale. :param value: @@ -377,13 +391,14 @@ def unscale_parameter(value: numbers.Number, return np.power(10, value) if petab_scale == LOG: return np.exp(value) - raise ValueError(f"Unknown parameter scale {petab_scale}. " - f"Must be from {(LIN, LOG, LOG10)}") + raise ValueError( + f"Unknown parameter scale {petab_scale}. " f"Must be from {(LIN, LOG, LOG10)}" + ) def scale_parameters_dict( - value_dict: Dict[Any, numbers.Number], - petab_scale_dict: Dict[Any, str]) -> None: + value_dict: Dict[Any, numbers.Number], petab_scale_dict: Dict[Any, str] +) -> None: """ Bring parameters from linear scale to target scale. @@ -405,8 +420,8 @@ def scale_parameters_dict( def unscale_parameters_dict( - value_dict: Dict[Any, numbers.Number], - petab_scale_dict: Dict[Any, str]) -> None: + value_dict: Dict[Any, numbers.Number], petab_scale_dict: Dict[Any, str] +) -> None: """ Bring parameters from target scale to linear scale. diff --git a/python/sdist/amici/petab_import.py b/python/sdist/amici/petab_import.py index bb41705e94..909bf250ae 100644 --- a/python/sdist/amici/petab_import.py +++ b/python/sdist/amici/petab_import.py @@ -7,47 +7,47 @@ import argparse import importlib import logging +import math import os import re import shutil import tempfile -from _collections import OrderedDict from itertools import chain from pathlib import Path from typing import Dict, List, Optional, Tuple, Union from warnings import warn +import amici import libsbml import pandas as pd import petab import sympy as sp +from _collections import OrderedDict +from amici.logging import get_logger, log_execution_time, set_log_level from petab.C import * +from petab.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML from petab.parameters import get_valid_parameters_for_parameter_table from sympy.abc import _clash -import amici -from amici.logging import get_logger, log_execution_time, set_log_level +from .petab_util import PREEQ_INDICATOR_ID, get_states_in_condition_table try: - from amici.petab_import_pysb import PysbPetabProblem, import_model_pysb + from amici.petab_import_pysb import import_model_pysb except ModuleNotFoundError: # pysb not available - PysbPetabProblem = None import_model_pysb = None logger = get_logger(__name__, logging.WARNING) -# ID of model parameter that is to be added to SBML model to indicate -# preequilibration -PREEQ_INDICATOR_ID = 'preequilibration_indicator' - -def _add_global_parameter(sbml_model: libsbml.Model, - parameter_id: str, - parameter_name: str = None, - constant: bool = False, - units: str = 'dimensionless', - value: float = 0.0) -> libsbml.Parameter: +def _add_global_parameter( + sbml_model: libsbml.Model, + parameter_id: str, + parameter_name: str = None, + constant: bool = False, + units: str = "dimensionless", + value: float = 0.0, +) -> libsbml.Parameter: """Add new global parameter to SBML model Arguments: @@ -74,8 +74,8 @@ def _add_global_parameter(sbml_model: libsbml.Model, def get_fixed_parameters( - petab_problem: petab.Problem, - non_estimated_parameters_as_constants=True, + petab_problem: petab.Problem, + non_estimated_parameters_as_constants=True, ) -> List[str]: """ Determine, set and return fixed model parameters. @@ -96,22 +96,26 @@ def get_fixed_parameters( :return: List of IDs of parameters which are to be considered constant. """ - # initial concentrations for species or initial compartment sizes in - # condition table will need to be turned into fixed parameters - - # if there is no initial assignment for that species, we'd need - # to create one. to avoid any naming collision right away, we don't - # allow that for now - - # we can't handle them yet - compartments = [ - col for col in petab_problem.condition_df - if petab_problem.sbml_model.getCompartment(col) is not None - ] - if compartments: - raise NotImplementedError("Can't handle initial compartment sizes " - "at the moment. Consider creating an " - f"initial assignment for {compartments}") + if petab_problem.model.type_id == MODEL_TYPE_SBML: + # initial concentrations for species or initial compartment sizes in + # condition table will need to be turned into fixed parameters + + # if there is no initial assignment for that species, we'd need + # to create one. to avoid any naming collision right away, we don't + # allow that for now + + # we can't handle them yet + compartments = [ + col + for col in petab_problem.condition_df + if petab_problem.model.sbml_model.getCompartment(col) is not None + ] + if compartments: + raise NotImplementedError( + "Can't handle initial compartment sizes " + "at the moment. Consider creating an " + f"initial assignment for {compartments}" + ) # if we have a parameter table, all parameters that are allowed to be # listed in the parameter table, but are not marked as estimated, can be @@ -130,17 +134,14 @@ def get_fixed_parameters( else pd.DataFrame(columns=petab.MEASUREMENT_DF_REQUIRED_COLS), ) if non_estimated_parameters_as_constants: - estimated_parameters = \ - petab_problem.parameter_df.index.values[ - petab_problem.parameter_df[ESTIMATE] == 1] + estimated_parameters = petab_problem.parameter_df.index.values[ + petab_problem.parameter_df[ESTIMATE] == 1 + ] else: # don't treat parameter table parameters as constants estimated_parameters = petab_problem.parameter_df.index.values fixed_parameters = set(all_parameters) - set(estimated_parameters) - sbml_model = petab_problem.sbml_model - condition_df = petab_problem.condition_df - # Column names are model parameter IDs, compartment IDs or species IDs. # Thereof, all parameters except for any overridden ones should be made # constant. @@ -148,47 +149,54 @@ def get_fixed_parameters( # increase model reusability) # handle parameters in condition table + condition_df = petab_problem.condition_df if condition_df is not None: - logger.debug(f'Condition table: {condition_df.shape}') + logger.debug(f"Condition table: {condition_df.shape}") # remove overridden parameters (`object`-type columns) fixed_parameters.update( - p for p in condition_df.columns + p + for p in condition_df.columns # get rid of conditionName column if p != CONDITION_NAME # there is no parametric override # TODO: could check if the final overriding parameter is estimated # or not, but for now, we skip the parameter if there is any kind # of overriding - if condition_df[p].dtype != 'O' - # p is a parameter - and sbml_model.getParameter(p) is not None - # but not a rule target - and sbml_model.getRuleByVariable(p) is None + if condition_df[p].dtype != "O" + # p is a parameter + and not petab_problem.model.is_state_variable(p) ) # Ensure mentioned parameters exist in the model. Remove additional ones # from list for fixed_parameter in fixed_parameters.copy(): # check global parameters - if not sbml_model.getParameter(fixed_parameter): - logger.warning(f"Parameter or species '{fixed_parameter}'" - " provided in condition table but not present in" - " model. Ignoring.") + if not petab_problem.model.has_entity_with_id(fixed_parameter): + # TODO: could still exist as an output parameter? + logger.warning( + f"Column '{fixed_parameter}' used in condition " + "table but not entity with the corresponding ID " + "exists. Ignoring." + ) fixed_parameters.remove(fixed_parameter) - # exclude targets of rules or initial assignments - for fixed_parameter in fixed_parameters.copy(): - # check global parameters - if sbml_model.getInitialAssignmentBySymbol(fixed_parameter)\ - or sbml_model.getRuleByVariable(fixed_parameter): - fixed_parameters.remove(fixed_parameter) + if petab_problem.model.type_id == MODEL_TYPE_SBML: + # exclude targets of rules or initial assignments + sbml_model = petab_problem.model.sbml_model + for fixed_parameter in fixed_parameters.copy(): + # check global parameters + if sbml_model.getInitialAssignmentBySymbol( + fixed_parameter + ) or sbml_model.getRuleByVariable(fixed_parameter): + fixed_parameters.remove(fixed_parameter) return list(sorted(fixed_parameters)) -def species_to_parameters(species_ids: List[str], - sbml_model: 'libsbml.Model') -> List[str]: +def species_to_parameters( + species_ids: List[str], sbml_model: "libsbml.Model" +) -> List[str]: """ Turn a SBML species into parameters and replace species references inside the model instance. @@ -211,13 +219,15 @@ def species_to_parameters(species_ids: List[str], if species.getHasOnlySubstanceUnits(): logger.warning( f"Ignoring {species.getId()} which has only substance units." - " Conversion not yet implemented.") + " Conversion not yet implemented." + ) continue if math.isnan(species.getInitialConcentration()): logger.warning( f"Ignoring {species.getId()} which has no initial " - "concentration. Amount conversion not yet implemented.") + "concentration. Amount conversion not yet implemented." + ) continue transformables.append(species_id) @@ -250,12 +260,13 @@ def species_to_parameters(species_ids: List[str], def import_petab_problem( - petab_problem: petab.Problem, - model_output_dir: Union[str, Path, None] = None, - model_name: str = None, - force_compile: bool = False, - non_estimated_parameters_as_constants = True, - **kwargs) -> 'amici.Model': + petab_problem: petab.Problem, + model_output_dir: Union[str, Path, None] = None, + model_name: str = None, + force_compile: bool = False, + non_estimated_parameters_as_constants=True, + **kwargs, +) -> "amici.Model": """ Import model from petab problem. @@ -288,24 +299,33 @@ def import_petab_problem( :return: The imported model. """ - # extract model name from pysb - if PysbPetabProblem and isinstance(petab_problem, PysbPetabProblem) \ - and model_name is None: + if petab_problem.model.type_id not in (MODEL_TYPE_SBML, MODEL_TYPE_PYSB): + raise NotImplementedError( + "Unsupported model type " + petab_problem.model.type_id + ) + + if petab_problem.mapping_df is not None: + # It's partially supported. Remove at your own risk... + raise NotImplementedError("PEtab v2.0.0 mapping tables are not yet supported.") + + model_name = model_name or petab_problem.model.model_id + + if petab_problem.model.type_id == MODEL_TYPE_PYSB and model_name is None: model_name = petab_problem.pysb_model.name + elif model_name is None and model_output_dir: + model_name = _create_model_name(model_output_dir) # generate folder and model name if necessary if model_output_dir is None: - if PysbPetabProblem and isinstance(petab_problem, PysbPetabProblem): + if petab_problem.model.type_id == MODEL_TYPE_PYSB: raise ValueError("Parameter `model_output_dir` is required.") - model_output_dir = \ - _create_model_output_dir_name(petab_problem.sbml_model, model_name) + model_output_dir = _create_model_output_dir_name( + petab_problem.sbml_model, model_name + ) else: model_output_dir = os.path.abspath(model_output_dir) - if model_name is None: - model_name = _create_model_name(model_output_dir) - # create folder if not os.path.exists(model_output_dir): os.makedirs(model_output_dir) @@ -316,7 +336,8 @@ def import_petab_problem( if os.listdir(model_output_dir) and not force_compile: raise ValueError( f"Cannot compile to {model_output_dir}: not empty. " - "Please assign a different target or set `force_compile`.") + "Please assign a different target or set `force_compile`." + ) # remove folder if exists if os.path.exists(model_output_dir): @@ -324,28 +345,28 @@ def import_petab_problem( logger.info(f"Compiling model {model_name} to {model_output_dir}.") # compile the model - if PysbPetabProblem and isinstance(petab_problem, PysbPetabProblem): + if petab_problem.model.type_id == MODEL_TYPE_PYSB: import_model_pysb( petab_problem, model_name=model_name, model_output_dir=model_output_dir, - **kwargs) + **kwargs, + ) else: import_model_sbml( petab_problem=petab_problem, model_name=model_name, model_output_dir=model_output_dir, - non_estimated_parameters_as_constants= - non_estimated_parameters_as_constants, - **kwargs) + non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, + **kwargs, + ) # import model model_module = amici.import_model_module(model_name, model_output_dir) model = model_module.getModel() check_model(amici_model=model, petab_problem=petab_problem) - logger.info(f"Successfully loaded model {model_name} " - f"from {model_output_dir}.") + logger.info(f"Successfully loaded model {model_name} " f"from {model_output_dir}.") return model @@ -361,23 +382,25 @@ def check_model( amici_ids_free = set(amici_model.getParameterIds()) amici_ids = amici_ids_free | set(amici_model.getFixedParameterIds()) - petab_ids_free = set(petab_problem.parameter_df.loc[ - petab_problem.parameter_df[ESTIMATE] == 1 - ].index) + petab_ids_free = set( + petab_problem.parameter_df.loc[petab_problem.parameter_df[ESTIMATE] == 1].index + ) amici_ids_free_required = petab_ids_free.intersection(amici_ids) if not amici_ids_free_required.issubset(amici_ids_free): raise ValueError( - 'The available AMICI model does not support estimating the ' - 'following parameters. Please recompile the model and ensure ' - 'that these parameters are not treated as constants. Deleting ' - 'the current model might also resolve this. Parameters: ' - f'{amici_ids_free_required.difference(amici_ids_free)}' + "The available AMICI model does not support estimating the " + "following parameters. Please recompile the model and ensure " + "that these parameters are not treated as constants. Deleting " + "the current model might also resolve this. Parameters: " + f"{amici_ids_free_required.difference(amici_ids_free)}" ) -def _create_model_output_dir_name(sbml_model: 'libsbml.Model', model_name: Optional[str] = None) -> Path: +def _create_model_output_dir_name( + sbml_model: "libsbml.Model", model_name: Optional[str] = None +) -> Path: """ Find a folder for storing the compiled amici model. If possible, use the sbml model id, otherwise create a random folder. @@ -406,10 +429,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: Union[str, Path]) -> bool: """ Check whether a module of that name can already be imported. """ @@ -424,21 +444,23 @@ def _can_import_model( return hasattr(model_module, "getModel") -@log_execution_time('Importing PEtab model', logger) +@log_execution_time("Importing PEtab model", logger) def import_model_sbml( - sbml_model: Union[str, Path, 'libsbml.Model'] = None, - condition_table: Optional[Union[str, Path, pd.DataFrame]] = None, - observable_table: Optional[Union[str, Path, pd.DataFrame]] = None, - measurement_table: Optional[Union[str, Path, pd.DataFrame]] = None, - petab_problem: petab.Problem = None, - model_name: Optional[str] = None, - model_output_dir: Optional[Union[str, Path]] = None, - verbose: Optional[Union[bool, int]] = True, - allow_reinit_fixpar_initcond: bool = True, - validate: bool = True, - non_estimated_parameters_as_constants=True, - output_parameter_defaults: Optional[Dict[str, float]] = None, - **kwargs) -> amici.SbmlImporter: + sbml_model: Union[str, Path, "libsbml.Model"] = None, + condition_table: Optional[Union[str, Path, pd.DataFrame]] = None, + observable_table: Optional[Union[str, Path, pd.DataFrame]] = None, + measurement_table: Optional[Union[str, Path, pd.DataFrame]] = None, + petab_problem: petab.Problem = None, + model_name: Optional[str] = None, + model_output_dir: Optional[Union[str, Path]] = None, + verbose: Optional[Union[bool, int]] = True, + allow_reinit_fixpar_initcond: bool = True, + validate: bool = True, + non_estimated_parameters_as_constants=True, + output_parameter_defaults: Optional[Dict[str, float]] = None, + discard_sbml_annotations: bool = False, + **kwargs, +) -> amici.SbmlImporter: """ Create AMICI model from PEtab problem @@ -474,7 +496,7 @@ def import_model_sbml( Print/log extra information. :param allow_reinit_fixpar_initcond: - See :class:`amici.ode_export.ODEExporter`. Must be enabled if initial + See :class:`amici.de_export.ODEExporter`. Must be enabled if initial states are to be reset after preequilibration. :param validate: @@ -491,6 +513,9 @@ def import_model_sbml( the PEtab observables table, in particular for placeholder parameters. Dictionary mapping parameter IDs to default values. + :param discard_sbml_annotations: + Discard information contained in AMICI SBML annotations (debug). + :param kwargs: Additional keyword arguments to be passed to :meth:`amici.sbml_import.SbmlImporter.sbml2amici`. @@ -505,15 +530,20 @@ def import_model_sbml( logger.info("Importing model ...") if any([sbml_model, condition_table, observable_table, measurement_table]): - warn("The `sbml_model`, `condition_table`, `observable_table`, and " - "`measurement_table` arguments are deprecated and will be " - "removed in a future version. Use `petab_problem` instead.", - DeprecationWarning, stacklevel=2) + warn( + "The `sbml_model`, `condition_table`, `observable_table`, and " + "`measurement_table` arguments are deprecated and will be " + "removed in a future version. Use `petab_problem` instead.", + DeprecationWarning, + stacklevel=2, + ) if petab_problem: - raise ValueError("Must not pass a `petab_problem` argument in " - "combination with any of `sbml_model`, " - "`condition_table`, `observable_table`, or " - "`measurement_table`.") + raise ValueError( + "Must not pass a `petab_problem` argument in " + "combination with any of `sbml_model`, " + "`condition_table`, `observable_table`, or " + "`measurement_table`." + ) petab_problem = petab.Problem( model=SbmlModel(sbml_model) @@ -524,8 +554,9 @@ def import_model_sbml( ) if petab_problem.observable_df is None: - raise NotImplementedError("PEtab import without observables table " - "is currently not supported.") + raise NotImplementedError( + "PEtab import without observables table " "is currently not supported." + ) assert isinstance(petab_problem.model, SbmlModel) @@ -537,8 +568,10 @@ def import_model_sbml( if model_name is None: if not (model_name := petab_problem.model.sbml_model.getId()): if not isinstance(sbml_model, (str, Path)): - raise ValueError("No `model_name` was provided and no model " - "ID was specified in the SBML model.") + raise ValueError( + "No `model_name` was provided and no model " + "ID was specified in the SBML model." + ) model_name = os.path.splitext(os.path.split(sbml_model)[-1])[0] if model_output_dir is None: @@ -546,8 +579,10 @@ def import_model_sbml( os.getcwd(), f"{model_name}-amici{amici.__version__}" ) - logger.info(f"Model name is '{model_name}'.\n" - f"Writing model code to '{model_output_dir}'.") + logger.info( + f"Model name is '{model_name}'.\n" + f"Writing model code to '{model_output_dir}'." + ) # Create a copy, because it will be modified by SbmlImporter sbml_doc = petab_problem.model.sbml_model.getSBMLDocument().clone() @@ -555,59 +590,69 @@ def import_model_sbml( show_model_info(sbml_model) - sbml_importer = amici.SbmlImporter(sbml_model) + sbml_importer = amici.SbmlImporter( + sbml_model, + discard_annotations=discard_sbml_annotations, + ) sbml_model = sbml_importer.sbml - allow_n_noise_pars = \ - not petab.lint.observable_table_has_nontrivial_noise_formula( - petab_problem.observable_df + allow_n_noise_pars = not petab.lint.observable_table_has_nontrivial_noise_formula( + petab_problem.observable_df + ) + if ( + petab_problem.measurement_df is not None + and petab.lint.measurement_table_has_timepoint_specific_mappings( + petab_problem.measurement_df, + allow_scalar_numeric_noise_parameters=allow_n_noise_pars, ) - if petab_problem.measurement_df is not None and \ - petab.lint.measurement_table_has_timepoint_specific_mappings( - petab_problem.measurement_df, - allow_scalar_numeric_noise_parameters=allow_n_noise_pars - ): + ): raise ValueError( - 'AMICI does not support importing models with timepoint specific ' - 'mappings for noise or observable parameters. Please flatten ' - 'the problem and try again.' + "AMICI does not support importing models with timepoint specific " + "mappings for noise or observable parameters. Please flatten " + "the problem and try again." ) if petab_problem.observable_df is not None: - observables, noise_distrs, sigmas = \ - get_observation_model(petab_problem.observable_df) + observables, noise_distrs, sigmas = get_observation_model( + petab_problem.observable_df + ) else: observables = noise_distrs = sigmas = None - logger.info(f'Observables: {len(observables)}') - logger.info(f'Sigmas: {len(sigmas)}') + logger.info(f"Observables: {len(observables)}") + logger.info(f"Sigmas: {len(sigmas)}") if len(sigmas) != len(observables): raise AssertionError( - f'Number of provided observables ({len(observables)}) and sigmas ' - f'({len(sigmas)}) do not match.') + f"Number of provided observables ({len(observables)}) and sigmas " + f"({len(sigmas)}) do not match." + ) # TODO: adding extra output parameters is currently not supported, # so we add any output parameters to the SBML model. # this should be changed to something more elegant # - formulas = chain((val['formula'] for val in observables.values()), - sigmas.values()) + formulas = chain((val["formula"] for val in observables.values()), sigmas.values()) output_parameters = OrderedDict() for formula in formulas: # we want reproducible parameter ordering upon repeated import - free_syms = sorted(sp.sympify(formula, locals=_clash).free_symbols, - key=lambda symbol: symbol.name) + free_syms = sorted( + sp.sympify(formula, locals=_clash).free_symbols, + key=lambda symbol: symbol.name, + ) for free_sym in free_syms: sym = str(free_sym) - if sbml_model.getElementBySId(sym) is None and sym != 'time' \ - and sym not in observables: + if ( + sbml_model.getElementBySId(sym) is None + and sym != "time" + and sym not in observables + ): output_parameters[sym] = None - logger.debug("Adding output parameters to model: " - f"{list(output_parameters.keys())}") + logger.debug( + "Adding output parameters to model: " f"{list(output_parameters.keys())}" + ) output_parameter_defaults = output_parameter_defaults or {} - if extra_pars := (set(output_parameter_defaults) - - set(output_parameters.keys())): + if extra_pars := (set(output_parameter_defaults) - set(output_parameters.keys())): raise ValueError( f"Default output parameter values were given for {extra_pars}, " "but they those are not output parameters." @@ -617,36 +662,38 @@ def import_model_sbml( _add_global_parameter( sbml_model=sbml_model, parameter_id=par, - value=output_parameter_defaults.get(par, 0.0) + value=output_parameter_defaults.get(par, 0.0), ) # # TODO: to parameterize initial states or compartment sizes, we currently # need initial assignments. if they occur in the condition table, we - # create a new parameter initial_${startOrCompartmentID}. + # create a new parameter initial_${speciesOrCompartmentID}. # feels dirty and should be changed (see also #924) # - initial_states = [col for col in petab_problem.condition_df - if element_is_state(sbml_model, col)] + initial_states = get_states_in_condition_table(petab_problem) fixed_parameters = [] if initial_states: # add preequilibration indicator variable # NOTE: would only be required if we actually have preequilibration # adding it anyways. can be optimized-out later if sbml_model.getParameter(PREEQ_INDICATOR_ID) is not None: - raise AssertionError("Model already has a parameter with ID " - f"{PREEQ_INDICATOR_ID}. Cannot handle " - "species and compartments in condition table " - "then.") + raise AssertionError( + "Model already has a parameter with ID " + f"{PREEQ_INDICATOR_ID}. Cannot handle " + "species and compartments in condition table " + "then." + ) indicator = sbml_model.createParameter() indicator.setId(PREEQ_INDICATOR_ID) indicator.setName(PREEQ_INDICATOR_ID) # Can only reset parameters after preequilibration if they are fixed. fixed_parameters.append(PREEQ_INDICATOR_ID) - logger.debug("Adding preequilibration indicator " - f"constant {PREEQ_INDICATOR_ID}") - logger.debug(f"Adding initial assignments for {initial_states}") + logger.debug( + "Adding preequilibration indicator " f"constant {PREEQ_INDICATOR_ID}" + ) + logger.debug(f"Adding initial assignments for {initial_states.keys()}") for assignee_id in initial_states: init_par_id_preeq = f"initial_{assignee_id}_preeq" init_par_id_sim = f"initial_{assignee_id}_sim" @@ -655,7 +702,8 @@ def import_model_sbml( raise ValueError( "Cannot create parameter for initial assignment " f"for {assignee_id} because an entity named " - f"{init_par_id} exists already in the model.") + f"{init_par_id} exists already in the model." + ) init_par = sbml_model.createParameter() init_par.setId(init_par_id) init_par.setName(init_par_id) @@ -664,14 +712,18 @@ def import_model_sbml( assignment = sbml_model.createInitialAssignment() assignment.setSymbol(assignee_id) else: - logger.debug('The SBML model has an initial assignment defined ' - f'for model entity {assignee_id}, but this entity ' - 'also has an initial value defined in the PEtab ' - 'condition table. The SBML initial assignment will ' - 'be overwritten to handle preequilibration and ' - 'initial values specified by the PEtab problem.') - formula = f'{PREEQ_INDICATOR_ID} * {init_par_id_preeq} ' \ - f'+ (1 - {PREEQ_INDICATOR_ID}) * {init_par_id_sim}' + logger.debug( + "The SBML model has an initial assignment defined " + f"for model entity {assignee_id}, but this entity " + "also has an initial value defined in the PEtab " + "condition table. The SBML initial assignment will " + "be overwritten to handle preequilibration and " + "initial values specified by the PEtab problem." + ) + formula = ( + f"{PREEQ_INDICATOR_ID} * {init_par_id_preeq} " + f"+ (1 - {PREEQ_INDICATOR_ID}) * {init_par_id_sim}" + ) math_ast = libsbml.parseL3Formula(formula) assignment.setMath(math_ast) # @@ -679,15 +731,16 @@ def import_model_sbml( fixed_parameters.extend( get_fixed_parameters( petab_problem=petab_problem, - non_estimated_parameters_as_constants= - non_estimated_parameters_as_constants, - )) + non_estimated_parameters_as_constants=non_estimated_parameters_as_constants, + ) + ) logger.debug(f"Fixed parameters are {fixed_parameters}") logger.info(f"Overall fixed parameters: {len(fixed_parameters)}") - logger.info("Variable parameters: " - + str(len(sbml_model.getListOfParameters()) - - len(fixed_parameters))) + logger.info( + "Variable parameters: " + + str(len(sbml_model.getListOfParameters()) - len(fixed_parameters)) + ) # Create Python module from SBML model sbml_importer.sbml2amici( @@ -699,10 +752,12 @@ def import_model_sbml( allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, noise_distributions=noise_distrs, verbose=verbose, - **kwargs) + **kwargs, + ) - if kwargs.get('compile', amici._get_default_argument( - sbml_importer.sbml2amici, 'compile')): + if kwargs.get( + "compile", amici._get_default_argument(sbml_importer.sbml2amici, "compile") + ): # check that the model extension was compiled successfully model_module = amici.import_model_module(model_name, model_output_dir) model = model_module.getModel() @@ -716,9 +771,8 @@ def import_model_sbml( def get_observation_model( - observable_df: pd.DataFrame, -) -> Tuple[Dict[str, Dict[str, str]], Dict[str, str], - Dict[str, Union[str, float]]]: + observable_df: pd.DataFrame, +) -> Tuple[Dict[str, Dict[str, str]], Dict[str, str], Dict[str, Union[str, float]]]: """ Get observables, sigmas, and noise distributions from PEtab observation table in a format suitable for @@ -730,29 +784,27 @@ def get_observation_model( :return: Tuple of dicts with observables, noise distributions, and sigmas. """ - if observable_df is None: return {}, {}, {} observables = {} sigmas = {} - nan_pat = r'^[nN]a[nN]$' + nan_pat = r"^[nN]a[nN]$" for _, observable in observable_df.iterrows(): oid = str(observable.name) # need to sanitize due to https://github.com/PEtab-dev/PEtab/issues/447 - name = re.sub(nan_pat, '', str(observable.get(OBSERVABLE_NAME, ''))) - formula_obs = re.sub(nan_pat, '', str(observable[OBSERVABLE_FORMULA])) - formula_noise = re.sub(nan_pat, '', str(observable[NOISE_FORMULA])) - observables[oid] = {'name': name, 'formula': formula_obs} + name = re.sub(nan_pat, "", str(observable.get(OBSERVABLE_NAME, ""))) + formula_obs = re.sub(nan_pat, "", str(observable[OBSERVABLE_FORMULA])) + formula_noise = re.sub(nan_pat, "", str(observable[NOISE_FORMULA])) + observables[oid] = {"name": name, "formula": formula_obs} sigmas[oid] = formula_noise # PEtab does currently not allow observables in noiseFormula and AMICI # cannot handle states in sigma expressions. Therefore, where possible, # replace species occurring in error model definition by observableIds. replacements = { - sp.sympify(observable['formula'], locals=_clash): - sp.Symbol(observable_id) + sp.sympify(observable["formula"], locals=_clash): sp.Symbol(observable_id) for observable_id, observable in observables.items() } for observable_id, formula in sigmas.items(): @@ -764,8 +816,7 @@ def get_observation_model( return observables, noise_distrs, sigmas -def petab_noise_distributions_to_amici(observable_df: pd.DataFrame - ) -> Dict[str, str]: +def petab_noise_distributions_to_amici(observable_df: pd.DataFrame) -> Dict[str, str]: """ Map from the petab to the amici format of noise distribution identifiers. @@ -778,19 +829,23 @@ def petab_noise_distributions_to_amici(observable_df: pd.DataFrame """ amici_distrs = {} for _, observable in observable_df.iterrows(): - amici_val = '' - - if OBSERVABLE_TRANSFORMATION in observable \ - and isinstance(observable[OBSERVABLE_TRANSFORMATION], str) \ - and observable[OBSERVABLE_TRANSFORMATION]: - amici_val += observable[OBSERVABLE_TRANSFORMATION] + '-' - - if NOISE_DISTRIBUTION in observable \ - and isinstance(observable[NOISE_DISTRIBUTION], str) \ - and observable[NOISE_DISTRIBUTION]: + amici_val = "" + + if ( + OBSERVABLE_TRANSFORMATION in observable + and isinstance(observable[OBSERVABLE_TRANSFORMATION], str) + and observable[OBSERVABLE_TRANSFORMATION] + ): + amici_val += observable[OBSERVABLE_TRANSFORMATION] + "-" + + if ( + NOISE_DISTRIBUTION in observable + and isinstance(observable[NOISE_DISTRIBUTION], str) + and observable[NOISE_DISTRIBUTION] + ): amici_val += observable[NOISE_DISTRIBUTION] else: - amici_val += 'normal' + amici_val += "normal" amici_distrs[observable.name] = amici_val return amici_distrs @@ -809,27 +864,12 @@ def petab_scale_to_amici_scale(scale_str: str) -> int: raise ValueError(f"Invalid parameter scale {scale_str}") -def show_model_info(sbml_model: 'libsbml.Model'): +def show_model_info(sbml_model: "libsbml.Model"): """Log some model quantities""" - logger.info(f'Species: {len(sbml_model.getListOfSpecies())}') - logger.info('Global parameters: ' - + str(len(sbml_model.getListOfParameters()))) - logger.info(f'Reactions: {len(sbml_model.getListOfReactions())}') - - -def element_is_state(sbml_model: libsbml.Model, sbml_id: str) -> bool: - """Does the element with ID `sbml_id` correspond to a state variable? - """ - if sbml_model.getCompartment(sbml_id) is not None: - return True - if sbml_model.getSpecies(sbml_id) is not None: - return True - if (rule := sbml_model.getRuleByVariable(sbml_id)) is not None \ - and rule.getTypeCode() == libsbml.SBML_RATE_RULE: - return True - - return False + logger.info(f"Species: {len(sbml_model.getListOfSpecies())}") + logger.info("Global parameters: " + str(len(sbml_model.getListOfParameters()))) + logger.info(f"Reactions: {len(sbml_model.getListOfReactions())}") def _parse_cli_args(): @@ -839,55 +879,89 @@ def _parse_cli_args(): :return: Parsed CLI arguments from :mod:`argparse`. """ - parser = argparse.ArgumentParser( - description='Import PEtab-format model into AMICI.') + description="Import PEtab-format model into AMICI." + ) # General options: - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', - help='More verbose output') - parser.add_argument('-o', '--output-dir', dest='model_output_dir', - help='Name of the model directory to create') - parser.add_argument('--no-compile', action='store_false', - dest='compile', - help='Only generate model code, do not compile') - parser.add_argument('--no-validate', action='store_false', - dest='validate', - help='Skip validation of PEtab files') - parser.add_argument('--flatten', dest='flatten', default=False, - action='store_true', - help='Flatten measurement specific overrides of ' - 'observable and noise parameters') - parser.add_argument('--no-sensitivities', dest='generate_sensitivity_code', - default=True, action='store_false', - help='Skip generation of sensitivity code') + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + help="More verbose output", + ) + parser.add_argument( + "-o", + "--output-dir", + dest="model_output_dir", + help="Name of the model directory to create", + ) + parser.add_argument( + "--no-compile", + action="store_false", + dest="compile", + help="Only generate model code, do not compile", + ) + parser.add_argument( + "--no-validate", + action="store_false", + dest="validate", + help="Skip validation of PEtab files", + ) + parser.add_argument( + "--flatten", + dest="flatten", + default=False, + action="store_true", + help="Flatten measurement specific overrides of " + "observable and noise parameters", + ) + parser.add_argument( + "--no-sensitivities", + dest="generate_sensitivity_code", + default=True, + action="store_false", + help="Skip generation of sensitivity code", + ) # Call with set of files - parser.add_argument('-s', '--sbml', dest='sbml_file_name', - help='SBML model filename') - parser.add_argument('-m', '--measurements', dest='measurement_file_name', - help='Measurement table') - parser.add_argument('-c', '--conditions', dest='condition_file_name', - help='Conditions table') - parser.add_argument('-p', '--parameters', dest='parameter_file_name', - help='Parameter table') - parser.add_argument('-b', '--observables', dest='observable_file_name', - help='Observable table') - - parser.add_argument('-y', '--yaml', dest='yaml_file_name', - help='PEtab YAML problem filename') - - parser.add_argument('-n', '--model-name', dest='model_name', - help='Name of the python module generated for the ' - 'model') + parser.add_argument( + "-s", "--sbml", dest="sbml_file_name", help="SBML model filename" + ) + parser.add_argument( + "-m", "--measurements", dest="measurement_file_name", help="Measurement table" + ) + parser.add_argument( + "-c", "--conditions", dest="condition_file_name", help="Conditions table" + ) + parser.add_argument( + "-p", "--parameters", dest="parameter_file_name", help="Parameter table" + ) + parser.add_argument( + "-b", "--observables", dest="observable_file_name", help="Observable table" + ) + + parser.add_argument( + "-y", "--yaml", dest="yaml_file_name", help="PEtab YAML problem filename" + ) + + parser.add_argument( + "-n", + "--model-name", + dest="model_name", + help="Name of the python module generated for the " "model", + ) args = parser.parse_args() - if not args.yaml_file_name \ - and not all((args.sbml_file_name, args.condition_file_name, - args.observable_file_name)): - parser.error('When not specifying a model name or YAML file, then ' - 'SBML, condition and observable file must be specified') + if not args.yaml_file_name and not all( + (args.sbml_file_name, args.condition_file_name, args.observable_file_name) + ): + parser.error( + "When not specifying a model name or YAML file, then " + "SBML, condition and observable file must be specified" + ) return args @@ -907,7 +981,8 @@ def _main(): condition_file=args.condition_file_name, measurement_file=args.measurement_file_name, parameter_file=args.parameter_file_name, - observable_files=args.observable_file_name) + observable_files=args.observable_file_name, + ) # Check for valid PEtab before potentially modifying it if args.validate: @@ -916,17 +991,19 @@ def _main(): if args.flatten: petab.flatten_timepoint_specific_output_overrides(pp) - import_model(model_name=args.model_name, - sbml_model=pp.sbml_model, - condition_table=pp.condition_df, - observable_table=pp.observable_df, - measurement_table=pp.measurement_df, - model_output_dir=args.model_output_dir, - compile=args.compile, - generate_sensitivity_code=args.generate_sensitivity_code, - verbose=args.verbose, - validate=False) - - -if __name__ == '__main__': + import_model( + model_name=args.model_name, + sbml_model=pp.sbml_model, + condition_table=pp.condition_df, + observable_table=pp.observable_df, + measurement_table=pp.measurement_df, + model_output_dir=args.model_output_dir, + compile=args.compile, + generate_sensitivity_code=args.generate_sensitivity_code, + verbose=args.verbose, + validate=False, + ) + + +if __name__ == "__main__": _main() diff --git a/python/sdist/amici/petab_import_pysb.py b/python/sdist/amici/petab_import_pysb.py index 4d2f146a74..63c1dd9681 100644 --- a/python/sdist/amici/petab_import_pysb.py +++ b/python/sdist/amici/petab_import_pysb.py @@ -6,314 +6,157 @@ """ import logging -import os -from itertools import chain +import re from pathlib import Path -from typing import Dict, Iterable, Optional, Union +from typing import Optional, Union import petab import pysb +import pysb.bng import sympy as sp -from petab.C import (CONDITION_FILES, CONDITION_NAME, FORMAT_VERSION, - MEASUREMENT_FILES, NOISE_FORMULA, OBSERVABLE_FILES, - OBSERVABLE_FORMULA, PARAMETER_FILE, SBML_FILES, - VISUALIZATION_FILES) -from petab.models.sbml_model import SbmlModel +from petab.C import CONDITION_NAME, NOISE_FORMULA, OBSERVABLE_FORMULA +from petab.models.pysb_model import PySBModel from .logging import get_logger, log_execution_time, set_log_level +from .petab_util import PREEQ_INDICATOR_ID, get_states_in_condition_table logger = get_logger(__name__, logging.WARNING) -class PysbPetabProblem(petab.Problem): - """Representation of a PySB-model-based PEtab problem +def _add_observation_model(pysb_model: pysb.Model, petab_problem: petab.Problem): + """Extend PySB model by observation model as defined in the PEtab + observables table""" + + # add any required output parameters + local_syms = { + sp.Symbol.__str__(comp): comp + for comp in pysb_model.components + if isinstance(comp, sp.Symbol) + } + for formula in [ + *petab_problem.observable_df[OBSERVABLE_FORMULA], + *petab_problem.observable_df[NOISE_FORMULA], + ]: + sym = sp.sympify(formula, locals=local_syms) + for s in sym.free_symbols: + if not isinstance(s, pysb.Component): + p = pysb.Parameter(str(s), 1.0) + pysb_model.add_component(p) + local_syms[sp.Symbol.__str__(p)] = p + + # add observables and sigmas to pysb model + for observable_id, observable_formula, noise_formula in zip( + petab_problem.observable_df.index, + petab_problem.observable_df[OBSERVABLE_FORMULA], + petab_problem.observable_df[NOISE_FORMULA], + ): + obs_symbol = sp.sympify(observable_formula, locals=local_syms) + if observable_id in pysb_model.expressions.keys(): + obs_expr = pysb_model.expressions[observable_id] + else: + obs_expr = pysb.Expression(observable_id, obs_symbol) + pysb_model.add_component(obs_expr) + local_syms[observable_id] = obs_expr - This class extends :class:`petab.Problem` with a PySB model. - The model is augmented with the observation model based on the PEtab - observable table. - For now, a dummy SBML model is created which allows used the existing - SBML-PEtab API. + sigma_id = f"{observable_id}_sigma" + sigma_symbol = sp.sympify(noise_formula, locals=local_syms) + sigma_expr = pysb.Expression(sigma_id, sigma_symbol) + pysb_model.add_component(sigma_expr) + local_syms[sigma_id] = sigma_expr - :ivar pysb_model: - PySB model instance from of this PEtab problem. - """ +def _add_initialization_variables(pysb_model: pysb.Model, petab_problem: petab.Problem): + """Add initialization variables to the PySB model to support initial + conditions specified in the PEtab condition table. - def __init__(self, pysb_model: 'pysb.Model' = None, *args, **kwargs): - """ - Constructor - - :param pysb_model: PySB model instance for this PEtab problem - :param args: See :meth:`petab.Problem.__init__` - :param kwargs: See :meth:`petab.Problem.__init__` - """ - flatten = kwargs.pop('flatten', False) - super().__init__(*args, **kwargs) - if flatten: - petab.flatten_timepoint_specific_output_overrides(self) - - self.pysb_model: 'pysb.Model' = pysb_model - self._add_observation_model() - - if self.pysb_model is not None: - self.model = \ - create_dummy_sbml( - self.pysb_model, - observable_ids=self.observable_df.index.values - if self.observable_df is not None else None - ) + To parameterize initial states, we currently need initial assignments. + If they occur in the condition table, we create a new parameter + initial_${speciesID}. Feels dirty and should be changed (see also #924). + """ - def _add_observation_model(self): - """Extend PySB model by observation model as defined in the PEtab - observables table""" - - # add any required output parameters - local_syms = {sp.Symbol.__str__(comp): comp for comp in - self.pysb_model.components if - isinstance(comp, sp.Symbol)} - for formula in [*self.observable_df[OBSERVABLE_FORMULA], - *self.observable_df[NOISE_FORMULA]]: - sym = sp.sympify(formula, locals=local_syms) - for s in sym.free_symbols: - if not isinstance(s, pysb.Component): - p = pysb.Parameter(str(s), 1.0, _export=False) - self.pysb_model.add_component(p) - local_syms[sp.Symbol.__str__(p)] = p - - # add observables and sigmas to pysb model - for (observable_id, observable_formula, noise_formula) \ - in zip(self.observable_df.index, - self.observable_df[OBSERVABLE_FORMULA], - self.observable_df[NOISE_FORMULA]): - obs_symbol = sp.sympify(observable_formula, locals=local_syms) - if observable_id in self.pysb_model.expressions.keys(): - obs_expr = self.pysb_model.expressions[observable_id] - else: - obs_expr = pysb.Expression(observable_id, obs_symbol, - _export=False) - self.pysb_model.add_component(obs_expr) - local_syms[observable_id] = obs_expr - - sigma_id = f"{observable_id}_sigma" - sigma_symbol = sp.sympify( - noise_formula, - locals=local_syms + initial_states = get_states_in_condition_table(petab_problem) + fixed_parameters = [] + if initial_states: + # add preequilibration indicator variable + # NOTE: would only be required if we actually have preequilibration + # adding it anyways. can be optimized-out later + if PREEQ_INDICATOR_ID in [c.name for c in pysb_model.components]: + raise AssertionError( + "Model already has a component with ID " + f"{PREEQ_INDICATOR_ID}. Cannot handle " + "species and compartments in condition table " + "then." ) - sigma_expr = pysb.Expression(sigma_id, sigma_symbol, _export=False) - self.pysb_model.add_component(sigma_expr) - local_syms[sigma_id] = sigma_expr - - @staticmethod - def from_files( - condition_file: - Union[str, Path, Iterable[Union[str, Path]]] = None, - measurement_file: - Union[str, Path, Iterable[Union[str, Path]]] = None, - parameter_file: - Union[str, Path, Iterable[Union[str, Path]]] = None, - visualization_files: - Union[str, Path, Iterable[Union[str, Path]]] = None, - observable_files: - Union[str, Path, Iterable[Union[str, Path]]] = None, - pysb_model_file: Union[str, Path] = None, - flatten: bool = False - ) -> 'PysbPetabProblem': - """ - Factory method to load model and tables from files. - - :param condition_file: - PEtab condition table - - :param measurement_file: - PEtab measurement table - - :param parameter_file: - PEtab parameter table - - :param visualization_files: - PEtab visualization tables - - :param observable_files: - PEtab observables tables - - :param pysb_model_file: - PySB model file - - :param flatten: - Flatten the petab problem - - :return: - Petab Problem - """ - - condition_df = measurement_df = parameter_df = visualization_df = None - observable_df = None - - if condition_file: - condition_df = petab.conditions.get_condition_df(condition_file) - - if measurement_file: - # If there are multiple tables, we will merge them - measurement_df = petab.core.concat_tables( - measurement_file, petab.measurements.get_measurement_df) - - if parameter_file: - parameter_df = petab.parameters.get_parameter_df(parameter_file) - - if visualization_files: - # If there are multiple tables, we will merge them - visualization_df = petab.core.concat_tables( - visualization_files, petab.core.get_visualization_df) - - if observable_files: - # If there are multiple tables, we will merge them - observable_df = petab.core.concat_tables( - observable_files, petab.observables.get_observable_df) - from amici.pysb_import import pysb_model_from_path - return PysbPetabProblem( - pysb_model=pysb_model_from_path( - pysb_model_file=pysb_model_file), - condition_df=condition_df, - measurement_df=measurement_df, - parameter_df=parameter_df, - observable_df=observable_df, - visualization_df=visualization_df, - flatten=flatten + preeq_indicator = pysb.Parameter(PREEQ_INDICATOR_ID) + pysb_model.add_component(preeq_indicator) + # Can only reset parameters after preequilibration if they are fixed. + fixed_parameters.append(PREEQ_INDICATOR_ID) + logger.debug( + "Adding preequilibration indicator constant " f"{PREEQ_INDICATOR_ID}" ) - - @staticmethod - def from_yaml(yaml_config: Union[Dict, Path, str], - flatten: bool = False) -> 'PysbPetabProblem': - """ - Factory method to load model and tables as specified by YAML file. - - NOTE: The PySB model is currently expected in the YAML file under - ``sbml_files``. - - :param yaml_config: - PEtab configuration as dictionary or YAML file name - - :param flatten: - Flatten the petab problem - - :return: - Petab Problem - """ - from petab.yaml import (load_yaml, is_composite_problem, - assert_single_condition_and_sbml_file) - if isinstance(yaml_config, (str, Path)): - path_prefix = os.path.dirname(yaml_config) - yaml_config = load_yaml(yaml_config) - else: - path_prefix = "" - - if is_composite_problem(yaml_config): - raise ValueError('petab.Problem.from_yaml() can only be used for ' - 'yaml files comprising a single model. ' - 'Consider using ' - 'petab.CompositeProblem.from_yaml() instead.') - - if yaml_config[FORMAT_VERSION] != petab.__format_version__: - raise ValueError("Provided PEtab files are of unsupported version" - f"{yaml_config[FORMAT_VERSION]}. Expected " - f"{petab.__format_version__}.") - - problem0 = yaml_config['problems'][0] - - assert_single_condition_and_sbml_file(problem0) - - if isinstance(yaml_config[PARAMETER_FILE], list): - parameter_file = [ - os.path.join(path_prefix, f) - for f in yaml_config[PARAMETER_FILE] - ] - else: - parameter_file = os.path.join( - path_prefix, yaml_config[PARAMETER_FILE]) - - return PysbPetabProblem.from_files( - pysb_model_file=os.path.join( - path_prefix, problem0[SBML_FILES][0]), - measurement_file=[os.path.join(path_prefix, f) - for f in problem0[MEASUREMENT_FILES]], - condition_file=os.path.join( - path_prefix, problem0[CONDITION_FILES][0]), - parameter_file=parameter_file, - visualization_files=[ - os.path.join(path_prefix, f) - for f in problem0.get(VISUALIZATION_FILES, [])], - observable_files=[ - os.path.join(path_prefix, f) - for f in problem0.get(OBSERVABLE_FILES, [])], - flatten=flatten + logger.debug(f"Adding initial assignments for {initial_states.keys()}") + + for assignee_id in initial_states: + init_par_id_preeq = f"initial_{assignee_id}_preeq" + init_par_id_sim = f"initial_{assignee_id}_sim" + for init_par_id in [init_par_id_preeq, init_par_id_sim]: + if init_par_id in [c.name for c in pysb_model.components]: + raise ValueError( + "Cannot create parameter for initial assignment " + f"for {assignee_id} because an entity named " + f"{init_par_id} exists already in the model." + ) + p = pysb.Parameter(init_par_id) + pysb_model.add_component(p) + + species_idx = int(re.match(r"__s(\d+)$", assignee_id)[1]) + # use original model here since that's what was used to generate + # the ids in initial_states + species_pattern = petab_problem.model.model.species[species_idx] + + # species pattern comes from the _original_ model, but we only want + # to modify pysb_model, so we have to reconstitute the pattern using + # pysb_model + for c in pysb_model.components: + globals()[c.name] = c + species_pattern = pysb.as_complex_pattern(eval(str(species_pattern))) + + from pysb.pattern import match_complex_pattern + + formula = pysb.Expression( + f"initial_{assignee_id}_formula", + preeq_indicator * pysb_model.parameters[init_par_id_preeq] + + (1 - preeq_indicator) * pysb_model.parameters[init_par_id_sim], ) + pysb_model.add_component(formula) + + for initial in pysb_model.initials: + if match_complex_pattern(initial.pattern, species_pattern, exact=True): + logger.debug( + "The PySB model has an initial defined for species " + f"{assignee_id}, but this species also has an initial " + "value defined in the PEtab condition table. The SBML " + "initial assignment will be overwritten to handle " + "preequilibration and initial values specified by the " + "PEtab problem." + ) + initial.value = formula + break + else: + # No initial in the pysb model, so add one + init = pysb.Initial(species_pattern, formula) + pysb_model.add_component(init) + return fixed_parameters -def create_dummy_sbml( - pysb_model: 'pysb.Model', - observable_ids: Optional[Iterable[str]] = None -) -> SbmlModel: - """Create SBML dummy model for to use PySB models with PEtab. - - Model must at least contain PEtab problem parameter and noise parameters - for observables. - :param pysb_model: PySB model - :param observable_ids: Observable IDs - :return: A dummy petab SBML model. - """ - import libsbml - - document = libsbml.SBMLDocument(3, 1) - dummy_sbml_model = document.createModel() - dummy_sbml_model.setTimeUnits("second") - dummy_sbml_model.setExtentUnits("mole") - dummy_sbml_model.setSubstanceUnits('mole') - - # mandatory if there are species - c = dummy_sbml_model.createCompartment() - c.setId('dummy_compartment') - c.setConstant(False) - - # parameters are required for parameter mapping - for parameter in pysb_model.parameters: - p = dummy_sbml_model.createParameter() - p.setId(parameter.name) - p.setConstant(True) - p.setValue(0.0) - - # noise parameters are required for every observable - for observable_id in observable_ids: - p = dummy_sbml_model.createParameter() - p.setId(f"noiseParameter1_{observable_id}") - p.setConstant(True) - p.setValue(0.0) - - # pysb observables and expressions are required in case they occur in - # the observableFormula or noiseFormula. - # as this code is only temporary and not performance-critical, we just add - # all of them. we just need an sbml entity with the same ID. sbml species - # seem to be the simplest, as parameters would interfere with parameter - # mapping later on - for component in chain(pysb_model.expressions, pysb_model.observables): - s = dummy_sbml_model.createSpecies() - s.setId(component.name) - s.setInitialAmount(0.0) - s.setHasOnlySubstanceUnits(False) - s.setBoundaryCondition(False) - s.setCompartment('dummy_compartment') - s.setConstant(False) - - return SbmlModel(sbml_model=dummy_sbml_model, sbml_document=document) - - -@log_execution_time('Importing PEtab model', logger) +@log_execution_time("Importing PEtab model", logger) def import_model_pysb( - petab_problem: PysbPetabProblem, - model_output_dir: Optional[Union[str, Path]] = None, - verbose: Optional[Union[bool, int]] = True, - model_name: Optional[str] = None, - **kwargs + petab_problem: petab.Problem, + model_output_dir: Optional[Union[str, Path]] = None, + verbose: Optional[Union[bool, int]] = True, + model_name: Optional[str] = None, + **kwargs, ) -> None: """ Create AMICI model from PySB-PEtab problem @@ -339,49 +182,79 @@ def import_model_pysb( logger.info("Importing model ...") - observable_table = petab_problem.observable_df - pysb_model = petab_problem.pysb_model + if not isinstance(petab_problem.model, PySBModel): + raise ValueError("Not a PySB model") + + # need to create a copy here as we don't want to modify the original + pysb.SelfExporter.cleanup() + og_export = pysb.SelfExporter.do_export + pysb.SelfExporter.do_export = False + pysb_model = pysb.Model( + base=petab_problem.model.model, + name=petab_problem.model.model_id, + ) + + _add_observation_model(pysb_model, petab_problem) + # generate species for the _original_ model + pysb.bng.generate_equations(petab_problem.model.model) + fixed_parameters = _add_initialization_variables(pysb_model, petab_problem) + pysb.SelfExporter.do_export = og_export - # For pysb, we only allow parameters in the condition table - # those must be pysb model parameters (either natively, or output - # parameters from measurement or condition table that have been added in - # PysbPetabProblem) + # check condition table for supported features, important to use pysb_model + # here, as we want to also cover output parameters model_parameters = [p.name for p in pysb_model.parameters] + condition_species_parameters = get_states_in_condition_table( + petab_problem, return_patterns=True + ) for x in petab_problem.condition_df.columns: if x == CONDITION_NAME: continue - if x not in model_parameters: - raise NotImplementedError( - "For PySB PEtab import, only model parameters, but no states " - "or compartments are allowed in the condition table." - f"Offending column: {x}" - ) + x = petab.mapping.resolve_mapping(petab_problem.mapping_df, x) - from .petab_import import ( - get_fixed_parameters, petab_noise_distributions_to_amici - ) - constant_parameters = get_fixed_parameters(petab_problem) + # parameters + if x in model_parameters: + continue - if observable_table is None: + # species/pattern + if x in condition_species_parameters: + continue + + raise NotImplementedError( + "For PySB PEtab import, only model parameters and species, but " + "not compartments are allowed in the condition table. Offending " + f"column: {x}" + ) + + from .petab_import import get_fixed_parameters, petab_noise_distributions_to_amici + + constant_parameters = get_fixed_parameters(petab_problem) + fixed_parameters + + if petab_problem.observable_df is None: observables = None sigmas = None noise_distrs = None else: - observables = [expr.name for expr in pysb_model.expressions - if expr.name in observable_table.index] + observables = [ + expr.name + for expr in pysb_model.expressions + if expr.name in petab_problem.observable_df.index + ] sigmas = {obs_id: f"{obs_id}_sigma" for obs_id in observables} - noise_distrs = petab_noise_distributions_to_amici(observable_table) + noise_distrs = petab_noise_distributions_to_amici(petab_problem.observable_df) from amici.pysb_import import pysb2amici - pysb2amici(model=pysb_model, - output_dir=model_output_dir, - model_name=model_name, - verbose=True, - observables=observables, - sigmas=sigmas, - constant_parameters=constant_parameters, - noise_distributions=noise_distrs, - **kwargs) + + pysb2amici( + model=pysb_model, + output_dir=model_output_dir, + model_name=model_name, + verbose=True, + observables=observables, + sigmas=sigmas, + constant_parameters=constant_parameters, + noise_distributions=noise_distrs, + **kwargs, + ) diff --git a/python/sdist/amici/petab_objective.py b/python/sdist/amici/petab_objective.py index dee9134289..f518724c82 100644 --- a/python/sdist/amici/petab_objective.py +++ b/python/sdist/amici/petab_objective.py @@ -8,58 +8,73 @@ import copy import logging import numbers -from typing import (Any, Collection, Dict, Iterator, List, Optional, Sequence, - Tuple, Union) +import re +from typing import ( + Any, + Collection, + Dict, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, +) +import amici import libsbml import numpy as np import pandas as pd import petab import sympy as sp +from amici.sbml_import import get_species_initial from petab.C import * # noqa: F403 +from petab.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML from sympy.abc import _clash -import amici -from amici.sbml_import import get_species_initial from . import AmiciExpData, AmiciModel from .logging import get_logger, log_execution_time -from .parameter_mapping import (ParameterMapping, ParameterMappingForCondition, - fill_in_parameters) -from .petab_import import PREEQ_INDICATOR_ID, element_is_state from .parameter_mapping import ( - fill_in_parameters, - ParameterMappingForCondition, ParameterMapping, + ParameterMappingForCondition, + fill_in_parameters, ) +from .petab_import import PREEQ_INDICATOR_ID +from .petab_util import get_states_in_condition_table + +try: + import pysb +except ImportError: + pysb = None logger = get_logger(__name__) # string constant definitions -LLH = 'llh' -SLLH = 'sllh' -FIM = 'fim' -S2LLH = 's2llh' -RES = 'res' -SRES = 'sres' -RDATAS = 'rdatas' -EDATAS = 'edatas' +LLH = "llh" +SLLH = "sllh" +FIM = "fim" +S2LLH = "s2llh" +RES = "res" +SRES = "sres" +RDATAS = "rdatas" +EDATAS = "edatas" -@log_execution_time('Simulating PEtab model', logger) +@log_execution_time("Simulating PEtab model", logger) 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, - edatas: List[AmiciExpData] = None, - parameter_mapping: ParameterMapping = None, - scaled_parameters: Optional[bool] = False, - log_level: int = logging.WARNING, - num_threads: int = 1, - failfast: bool = True, - scaled_gradients: bool = False, + 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, + edatas: List[AmiciExpData] = None, + parameter_mapping: ParameterMapping = None, + scaled_parameters: Optional[bool] = False, + log_level: int = logging.WARNING, + num_threads: int = 1, + failfast: bool = True, + scaled_gradients: bool = False, ) -> Dict[str, Any]: """Simulate PEtab model. @@ -129,10 +144,10 @@ def simulate_petab( # number of amici simulations will be number of unique # (preequilibrationConditionId, simulationConditionId) pairs. # Can be optimized by checking for identical condition vectors. - if simulation_conditions is None and parameter_mapping is None \ - and edatas is None: - simulation_conditions = \ + if simulation_conditions is None and parameter_mapping is None and edatas is None: + simulation_conditions = ( petab_problem.get_simulation_conditions_from_measurement_df() + ) # Get parameter mapping if parameter_mapping is None: @@ -140,7 +155,8 @@ def simulate_petab( petab_problem=petab_problem, simulation_conditions=simulation_conditions, scaled_parameters=scaled_parameters, - amici_model=amici_model) + amici_model=amici_model, + ) # Get edatas if edatas is None: @@ -148,7 +164,8 @@ def simulate_petab( edatas = create_edatas( amici_model=amici_model, petab_problem=petab_problem, - simulation_conditions=simulation_conditions) + simulation_conditions=simulation_conditions, + ) # Fill parameters in ExpDatas (in-place) fill_in_parameters( @@ -156,15 +173,20 @@ def simulate_petab( problem_parameters=problem_parameters, scaled_parameters=scaled_parameters, parameter_mapping=parameter_mapping, - amici_model=amici_model) + amici_model=amici_model, + ) # Simulate rdatas = amici.runAmiciSimulations( - amici_model, solver, edata_list=edatas, - num_threads=num_threads, failfast=failfast) + amici_model, + solver, + edata_list=edatas, + num_threads=num_threads, + failfast=failfast, + ) # Compute total llh - llh = sum(rdata['llh'] for rdata in rdatas) + llh = sum(rdata["llh"] for rdata in rdatas) # Compute total sllh sllh = None if solver.getSensitivityOrder() != amici.SensitivityOrder.none: @@ -207,12 +229,12 @@ def simulate_petab( def aggregate_sllh( - amici_model: AmiciModel, - rdatas: Sequence[amici.ReturnDataView], - parameter_mapping: Optional[ParameterMapping], - edatas: List[AmiciExpData], - petab_scale: bool = True, - petab_problem: petab.Problem = None, + amici_model: AmiciModel, + rdatas: Sequence[amici.ReturnDataView], + parameter_mapping: Optional[ParameterMapping], + edatas: List[AmiciExpData], + petab_scale: bool = True, + petab_problem: petab.Problem = None, ) -> Union[None, Dict[str, float]]: """ Aggregate likelihood gradient for all conditions, according to PEtab @@ -240,8 +262,7 @@ def aggregate_sllh( if petab_scale and petab_problem is None: raise ValueError( - 'Please provide the PEtab problem, when using ' - '`petab_scale=True`.' + "Please provide the PEtab problem, when using " "`petab_scale=True`." ) # Check for issues in all condition simulation results. @@ -252,14 +273,14 @@ def aggregate_sllh( # Condition simulation result does not provide SLLH. if rdata.sllh is None: raise ValueError( - 'The sensitivities of the likelihood for a condition were ' - 'not computed.' + "The sensitivities of the likelihood for a condition were " + "not computed." ) - for condition_parameter_mapping, edata, rdata in \ - zip(parameter_mapping, edatas, rdatas): - for sllh_parameter_index, condition_parameter_sllh in \ - enumerate(rdata.sllh): + for condition_parameter_mapping, edata, rdata in zip( + parameter_mapping, edatas, rdatas + ): + for sllh_parameter_index, condition_parameter_sllh in enumerate(rdata.sllh): # Get PEtab parameter ID # Use ExpData if it provides a parameter list, else default to # Model. @@ -268,10 +289,9 @@ def aggregate_sllh( else: model_parameter_index = amici_model.plist(sllh_parameter_index) model_parameter_id = model_parameter_ids[model_parameter_index] - petab_parameter_id = ( - condition_parameter_mapping - .map_sim_var[model_parameter_id] - ) + petab_parameter_id = condition_parameter_mapping.map_sim_var[ + model_parameter_id + ] # Initialize if petab_parameter_id not in accumulated_sllh: @@ -281,21 +301,18 @@ def aggregate_sllh( if petab_scale: # `ParameterMappingForCondition` objects provide the scale in # terms of `petab.C` constants already, not AMICI equivalents. - model_parameter_scale = ( - condition_parameter_mapping - .scale_map_sim_var[model_parameter_id] - ) - petab_parameter_scale = ( - petab_problem - .parameter_df - .loc[petab_parameter_id, PARAMETER_SCALE] - ) + model_parameter_scale = condition_parameter_mapping.scale_map_sim_var[ + model_parameter_id + ] + petab_parameter_scale = petab_problem.parameter_df.loc[ + petab_parameter_id, PARAMETER_SCALE + ] if model_parameter_scale != petab_parameter_scale: raise ValueError( - f'The scale of the parameter `{petab_parameter_id}` ' - 'differs between the AMICI model ' - f'({model_parameter_scale}) and the PEtab problem ' - f'({petab_parameter_scale}).' + f"The scale of the parameter `{petab_parameter_id}` " + "differs between the AMICI model " + f"({model_parameter_scale}) and the PEtab problem " + f"({petab_parameter_scale})." ) # Accumulate @@ -345,20 +362,18 @@ def rescale_sensitivity( scale[(LOG10, LOG)] = lambda s: scale[(LIN, LOG)](scale[(LOG10, LIN)](s)) if (old_scale, new_scale) not in scale: - raise NotImplementedError( - f"Old scale: {old_scale}. New scale: {new_scale}." - ) + raise NotImplementedError(f"Old scale: {old_scale}. New scale: {new_scale}.") return scale[(old_scale, new_scale)](sensitivity) def create_parameterized_edatas( - amici_model: AmiciModel, - petab_problem: petab.Problem, - problem_parameters: Dict[str, numbers.Number], - scaled_parameters: bool = False, - parameter_mapping: ParameterMapping = None, - simulation_conditions: Union[pd.DataFrame, Dict] = None, + amici_model: AmiciModel, + petab_problem: petab.Problem, + problem_parameters: Dict[str, numbers.Number], + scaled_parameters: bool = False, + parameter_mapping: ParameterMapping = None, + simulation_conditions: Union[pd.DataFrame, Dict] = None, ) -> List[amici.ExpData]: """Create list of :class:amici.ExpData objects with parameters filled in. @@ -389,8 +404,9 @@ def create_parameterized_edatas( # (preequilibrationConditionId, simulationConditionId) pairs. # Can be optimized by checking for identical condition vectors. if simulation_conditions is None: - simulation_conditions = \ + simulation_conditions = ( petab_problem.get_simulation_conditions_from_measurement_df() + ) # Get parameter mapping if parameter_mapping is None: @@ -398,13 +414,15 @@ def create_parameterized_edatas( petab_problem=petab_problem, simulation_conditions=simulation_conditions, scaled_parameters=scaled_parameters, - amici_model=amici_model) + amici_model=amici_model, + ) # Generate ExpData with all condition-specific information edatas = create_edatas( amici_model=amici_model, petab_problem=petab_problem, - simulation_conditions=simulation_conditions) + simulation_conditions=simulation_conditions, + ) # Fill parameters in ExpDatas (in-place) fill_in_parameters( @@ -412,17 +430,18 @@ def create_parameterized_edatas( problem_parameters=problem_parameters, scaled_parameters=scaled_parameters, parameter_mapping=parameter_mapping, - amici_model=amici_model) + amici_model=amici_model, + ) return edatas def create_parameter_mapping( - petab_problem: petab.Problem, - simulation_conditions: Union[pd.DataFrame, List[Dict]], - scaled_parameters: bool, - amici_model: AmiciModel, - **parameter_mapping_kwargs, + petab_problem: petab.Problem, + simulation_conditions: Union[pd.DataFrame, List[Dict]], + scaled_parameters: bool, + amici_model: AmiciModel, + **parameter_mapping_kwargs, ) -> ParameterMapping: """Generate AMICI specific parameter mapping. @@ -446,60 +465,167 @@ def create_parameter_mapping( List of the parameter mappings. """ if simulation_conditions is None: - simulation_conditions = \ + simulation_conditions = ( petab_problem.get_simulation_conditions_from_measurement_df() + ) if isinstance(simulation_conditions, list): simulation_conditions = pd.DataFrame(data=simulation_conditions) # Because AMICI globalizes all local parameters during model import, # we need to do that here as well to prevent parameter mapping errors # (PEtab does currently not care about SBML LocalParameters) - if petab_problem.sbml_document: - converter_config = libsbml.SBMLLocalParameterConverter() \ - .getDefaultProperties() - petab_problem.sbml_document.convert(converter_config) - else: - logger.debug("No petab_problem.sbml_document is set. Cannot convert " - "SBML LocalParameters. If the model contains " - "LocalParameters, parameter mapping will fail.") + if petab_problem.model.type_id == MODEL_TYPE_SBML: + if petab_problem.sbml_document: + converter_config = ( + libsbml.SBMLLocalParameterConverter().getDefaultProperties() + ) + petab_problem.sbml_document.convert(converter_config) + else: + logger.debug( + "No petab_problem.sbml_document is set. Cannot " + "convert SBML LocalParameters. If the model contains " + "LocalParameters, parameter mapping will fail." + ) default_parameter_mapping_kwargs = { "warn_unmapped": False, "scaled_parameters": scaled_parameters, - "allow_timepoint_specific_numeric_noise_parameters": - not petab.lint.observable_table_has_nontrivial_noise_formula( - petab_problem.observable_df), + "allow_timepoint_specific_numeric_noise_parameters": not petab.lint.observable_table_has_nontrivial_noise_formula( + petab_problem.observable_df + ), } if parameter_mapping_kwargs is None: parameter_mapping_kwargs = {} - prelim_parameter_mapping = \ - petab.get_optimization_to_simulation_parameter_mapping( - condition_df=petab_problem.condition_df, - measurement_df=petab_problem.measurement_df, - parameter_df=petab_problem.parameter_df, - observable_df=petab_problem.observable_df, - model=petab_problem.model, - **dict(default_parameter_mapping_kwargs, - **parameter_mapping_kwargs) - ) + prelim_parameter_mapping = petab.get_optimization_to_simulation_parameter_mapping( + condition_df=petab_problem.condition_df, + measurement_df=petab_problem.measurement_df, + parameter_df=petab_problem.parameter_df, + observable_df=petab_problem.observable_df, + mapping_df=petab_problem.mapping_df, + model=petab_problem.model, + **dict(default_parameter_mapping_kwargs, **parameter_mapping_kwargs), + ) parameter_mapping = ParameterMapping() - for (_, condition), prelim_mapping_for_condition in \ - zip(simulation_conditions.iterrows(), prelim_parameter_mapping): + for (_, condition), prelim_mapping_for_condition in zip( + simulation_conditions.iterrows(), prelim_parameter_mapping + ): mapping_for_condition = create_parameter_mapping_for_condition( - prelim_mapping_for_condition, condition, petab_problem, - amici_model) + prelim_mapping_for_condition, condition, petab_problem, amici_model + ) parameter_mapping.append(mapping_for_condition) return parameter_mapping +def _get_initial_state_sbml( + petab_problem: petab.Problem, element_id: str +) -> Union[float, sp.Basic]: + element = petab_problem.sbml_model.getElementBySId(element_id) + type_code = element.getTypeCode() + initial_assignment = petab_problem.sbml_model.getInitialAssignmentBySymbol( + element_id + ) + if initial_assignment: + initial_assignment = sp.sympify( + libsbml.formulaToL3String(initial_assignment.getMath()), locals=_clash + ) + if type_code == libsbml.SBML_SPECIES: + value = ( + get_species_initial(element) + if initial_assignment is None + else initial_assignment + ) + elif type_code == libsbml.SBML_PARAMETER: + value = element.getValue() if initial_assignment is None else initial_assignment + elif type_code == libsbml.SBML_COMPARTMENT: + value = element.getSize() if initial_assignment is None else initial_assignment + else: + raise NotImplementedError( + f"Don't know what how to handle {element_id} in " "condition table." + ) + return value + + +def _get_initial_state_pysb( + petab_problem: petab.Problem, element_id: str +) -> Union[float, sp.Symbol]: + species_idx = int(re.match(r"__s(\d+)$", element_id)[1]) + species_pattern = petab_problem.model.model.species[species_idx] + from pysb.pattern import match_complex_pattern + + value = next( + ( + initial.value + for initial in petab_problem.model.model.initials + if match_complex_pattern(initial.pattern, species_pattern, exact=True) + ), + 0.0, + ) + if isinstance(value, pysb.Parameter): + if value.name in petab_problem.parameter_df.index: + value = value.name + else: + value = value.value + + return value + + +def _set_initial_state( + petab_problem, + condition_id, + element_id, + init_par_id, + par_map, + scale_map, + value, +): + value = petab.to_float_if_float(value) + if pd.isna(value): + if petab_problem.model.type_id == MODEL_TYPE_SBML: + value = _get_initial_state_sbml(petab_problem, element_id) + elif petab_problem.model.type_id == MODEL_TYPE_PYSB: + value = _get_initial_state_pysb(petab_problem, element_id) + + try: + value = float(value) + except (ValueError, TypeError): + if sp.nsimplify(value).is_Atom and ( + pysb is None or not isinstance(value, pysb.Component) + ): + # Get rid of multiplication with one + value = sp.nsimplify(value) + else: + raise NotImplementedError( + "Cannot handle non-trivial initial state " + f"expression for {element_id}: {value}" + ) + # this should be a parameter ID + value = str(value) + logger.debug( + f"The species {element_id} has no initial value " + f"defined for the condition {condition_id} in " + "the PEtab conditions table. The initial value is " + f"now set to {value}, which is the initial value " + "defined in the SBML model." + ) + par_map[init_par_id] = value + if isinstance(value, float): + # numeric initial state + scale_map[init_par_id] = petab.LIN + else: + # parametric initial state + scale_map[init_par_id] = petab_problem.parameter_df[PARAMETER_SCALE].get( + value, petab.LIN + ) + + def create_parameter_mapping_for_condition( - parameter_mapping_for_condition: petab.ParMappingDictQuadruple, - condition: Union[pd.Series, Dict], - petab_problem: petab.Problem, - amici_model: AmiciModel + parameter_mapping_for_condition: petab.ParMappingDictQuadruple, + condition: Union[pd.Series, Dict], + petab_problem: petab.Problem, + amici_model: AmiciModel, ) -> ParameterMappingForCondition: """Generate AMICI specific parameter mapping for condition. @@ -518,20 +644,26 @@ def create_parameter_mapping_for_condition( preequilibration, fixed simulation, and variable simulation parameters, and then the respective scalings. """ - (condition_map_preeq, condition_map_sim, condition_scale_map_preeq, - condition_scale_map_sim) = parameter_mapping_for_condition + ( + condition_map_preeq, + condition_map_sim, + condition_scale_map_preeq, + condition_scale_map_sim, + ) = parameter_mapping_for_condition logger.debug(f"PEtab mapping: {parameter_mapping_for_condition}") - if len(condition_map_preeq) != len(condition_scale_map_preeq) \ - or len(condition_map_sim) != len(condition_scale_map_sim): - raise AssertionError("Number of parameters and number of parameter " - "scales do not match.") - if len(condition_map_preeq) \ - and len(condition_map_preeq) != len(condition_map_sim): + if len(condition_map_preeq) != len(condition_scale_map_preeq) or len( + condition_map_sim + ) != len(condition_scale_map_sim): + raise AssertionError( + "Number of parameters and number of parameter " "scales do not match." + ) + if len(condition_map_preeq) and len(condition_map_preeq) != len(condition_map_sim): logger.debug(f"Preequilibration parameter map: {condition_map_preeq}") logger.debug(f"Simulation parameter map: {condition_map_sim}") - raise AssertionError("Number of parameters for preequilbration " - "and simulation do not match.") + raise AssertionError( + "Number of parameters for preequilbration " "and simulation do not match." + ) ########################################################################## # initial states @@ -545,11 +677,9 @@ def create_parameter_mapping_for_condition( # ExpData.x0, but in the case of preequilibration this would not allow for # resetting initial states. - states_in_condition_table = [ - col for col in petab_problem.condition_df - if element_is_state(petab_problem.sbml_model, col) - ] - if states_in_condition_table: + if states_in_condition_table := get_states_in_condition_table( + petab_problem, condition + ): # set indicator fixed parameter for preeq # (we expect here, that this parameter was added during import and # that it was not added by the user with a different meaning...) @@ -560,69 +690,21 @@ def create_parameter_mapping_for_condition( condition_map_sim[PREEQ_INDICATOR_ID] = 0.0 condition_scale_map_sim[PREEQ_INDICATOR_ID] = LIN - def _set_initial_state(condition_id, element_id, init_par_id, - par_map, scale_map): - value = petab.to_float_if_float( - petab_problem.condition_df.loc[condition_id, element_id]) - if pd.isna(value): - element = petab_problem.sbml_model.getElementBySId(element_id) - type_code = element.getTypeCode() - initial_assignment = petab_problem.sbml_model\ - .getInitialAssignmentBySymbol(element_id) - if initial_assignment: - initial_assignment = sp.sympify( - libsbml.formulaToL3String(initial_assignment.getMath()), - locals=_clash - ) - if type_code == libsbml.SBML_SPECIES: - value = get_species_initial(element) \ - if initial_assignment is None else initial_assignment - elif type_code == libsbml.SBML_PARAMETER: - value = element.getValue()\ - if initial_assignment is None else initial_assignment - elif type_code == libsbml.SBML_COMPARTMENT: - value = element.getSize()\ - if initial_assignment is None else initial_assignment - else: - raise NotImplementedError( - f"Don't know what how to handle {element_id} in " - "condition table.") - - try: - value = float(value) - except (ValueError, TypeError): - if sp.nsimplify(value).is_Atom: - # Get rid of multiplication with one - value = sp.nsimplify(value) - else: - raise NotImplementedError( - "Cannot handle non-trivial initial state " - f"expression for {element_id}: {value}") - # this should be a parameter ID - value = str(value) - logger.debug(f'The species {element_id} has no initial value ' - f'defined for the condition {condition_id} in ' - 'the PEtab conditions table. The initial value is ' - f'now set to {value}, which is the initial value ' - 'defined in the SBML model.') - par_map[init_par_id] = value - if isinstance(value, float): - # numeric initial state - scale_map[init_par_id] = petab.LIN - else: - # parametric initial state - scale_map[init_par_id] = \ - petab_problem.parameter_df[PARAMETER_SCALE]\ - .get(value, petab.LIN) - - for element_id in states_in_condition_table: + for element_id, (value, preeq_value) in states_in_condition_table.items(): # for preequilibration - init_par_id = f'initial_{element_id}_preeq' - if condition.get(PREEQUILIBRATION_CONDITION_ID): - condition_id = condition[PREEQUILIBRATION_CONDITION_ID] + init_par_id = f"initial_{element_id}_preeq" + if ( + condition_id := condition.get(PREEQUILIBRATION_CONDITION_ID) + ) is not None: _set_initial_state( - condition_id, element_id, init_par_id, condition_map_preeq, - condition_scale_map_preeq) + petab_problem, + condition_id, + element_id, + init_par_id, + condition_map_preeq, + condition_scale_map_preeq, + preeq_value, + ) else: # need to set dummy value for preeq parameter anyways, as it # is expected below (set to 0, not nan, because will be @@ -632,10 +714,16 @@ def _set_initial_state(condition_id, element_id, init_par_id, # for simulation condition_id = condition[SIMULATION_CONDITION_ID] - init_par_id = f'initial_{element_id}_sim' + init_par_id = f"initial_{element_id}_sim" _set_initial_state( - condition_id, element_id, init_par_id, condition_map_sim, - condition_scale_map_sim) + petab_problem, + condition_id, + element_id, + init_par_id, + condition_map_sim, + condition_scale_map_sim, + value, + ) ########################################################################## # separate fixed and variable AMICI parameters, because we may have @@ -646,31 +734,34 @@ def _set_initial_state(condition_id, element_id, init_par_id, variable_par_ids = amici_model.getParameterIds() fixed_par_ids = amici_model.getFixedParameterIds() - condition_map_preeq_var, condition_map_preeq_fix = \ - _subset_dict(condition_map_preeq, variable_par_ids, fixed_par_ids) + condition_map_preeq_var, condition_map_preeq_fix = _subset_dict( + condition_map_preeq, variable_par_ids, fixed_par_ids + ) - condition_scale_map_preeq_var, condition_scale_map_preeq_fix = \ - _subset_dict(condition_scale_map_preeq, variable_par_ids, fixed_par_ids) + condition_scale_map_preeq_var, condition_scale_map_preeq_fix = _subset_dict( + condition_scale_map_preeq, variable_par_ids, fixed_par_ids + ) - condition_map_sim_var, condition_map_sim_fix = \ - _subset_dict(condition_map_sim, variable_par_ids, fixed_par_ids) + condition_map_sim_var, condition_map_sim_fix = _subset_dict( + condition_map_sim, variable_par_ids, fixed_par_ids + ) - condition_scale_map_sim_var, condition_scale_map_sim_fix = \ - _subset_dict(condition_scale_map_sim, variable_par_ids, fixed_par_ids) + condition_scale_map_sim_var, condition_scale_map_sim_fix = _subset_dict( + condition_scale_map_sim, variable_par_ids, fixed_par_ids + ) - logger.debug("Fixed parameters preequilibration: " - f"{condition_map_preeq_fix}") - logger.debug("Fixed parameters simulation: " - f"{condition_map_sim_fix}") - logger.debug("Variable parameters preequilibration: " - f"{condition_map_preeq_var}") - logger.debug("Variable parameters simulation: " - f"{condition_map_sim_var}") + logger.debug("Fixed parameters preequilibration: " f"{condition_map_preeq_fix}") + logger.debug("Fixed parameters simulation: " f"{condition_map_sim_fix}") + logger.debug("Variable parameters preequilibration: " f"{condition_map_preeq_var}") + logger.debug("Variable parameters simulation: " f"{condition_map_sim_var}") petab.merge_preeq_and_sim_pars_condition( - condition_map_preeq_var, condition_map_sim_var, - condition_scale_map_preeq_var, condition_scale_map_sim_var, - condition) + condition_map_preeq_var, + condition_map_sim_var, + condition_scale_map_preeq_var, + condition_scale_map_sim_var, + condition, + ) logger.debug(f"Merged: {condition_map_sim_var}") parameter_mapping_for_condition = ParameterMappingForCondition( @@ -679,16 +770,16 @@ def _set_initial_state(condition_id, element_id, init_par_id, map_sim_var=condition_map_sim_var, scale_map_preeq_fix=condition_scale_map_preeq_fix, scale_map_sim_fix=condition_scale_map_sim_fix, - scale_map_sim_var=condition_scale_map_sim_var + scale_map_sim_var=condition_scale_map_sim_var, ) return parameter_mapping_for_condition def create_edatas( - amici_model: AmiciModel, - petab_problem: petab.Problem, - simulation_conditions: Union[pd.DataFrame, Dict] = None, + amici_model: AmiciModel, + petab_problem: petab.Problem, + simulation_conditions: Union[pd.DataFrame, Dict] = None, ) -> List[amici.ExpData]: """Create list of :class:`amici.amici.ExpData` objects for PEtab problem. @@ -705,30 +796,29 @@ def create_edatas( with filled in timepoints and data. """ if simulation_conditions is None: - simulation_conditions = \ + simulation_conditions = ( petab_problem.get_simulation_conditions_from_measurement_df() + ) observable_ids = amici_model.getObservableIds() - measurement_dfs = dict(list( - petab_problem.measurement_df.groupby( - [petab.SIMULATION_CONDITION_ID, - petab.PREEQUILIBRATION_CONDITION_ID] - if petab.PREEQUILIBRATION_CONDITION_ID in simulation_conditions - else petab.SIMULATION_CONDITION_ID - ) - )) + measurement_groupvar = [SIMULATION_CONDITION_ID] + if PREEQUILIBRATION_CONDITION_ID in simulation_conditions: + measurement_groupvar.append(petab.PREEQUILIBRATION_CONDITION_ID) + measurement_dfs = dict( + list(petab_problem.measurement_df.groupby(measurement_groupvar)) + ) edatas = [] for _, condition in simulation_conditions.iterrows(): # Create amici.ExpData for each simulation - if petab.PREEQUILIBRATION_CONDITION_ID in condition: + if PREEQUILIBRATION_CONDITION_ID in condition: measurement_index = ( - condition.get(petab.SIMULATION_CONDITION_ID), - condition.get(petab.PREEQUILIBRATION_CONDITION_ID) + condition.get(SIMULATION_CONDITION_ID), + condition.get(PREEQUILIBRATION_CONDITION_ID), ) else: - measurement_index = condition.get(petab.SIMULATION_CONDITION_ID) + measurement_index = (condition.get(SIMULATION_CONDITION_ID),) edata = create_edata_for_condition( condition=condition, amici_model=amici_model, @@ -742,11 +832,11 @@ def create_edatas( def create_edata_for_condition( - condition: Union[Dict, pd.Series], - measurement_df: pd.DataFrame, - amici_model: AmiciModel, - petab_problem: petab.Problem, - observable_ids: List[str], + condition: Union[Dict, pd.Series], + measurement_df: pd.DataFrame, + amici_model: AmiciModel, + petab_problem: petab.Problem, + observable_ids: List[str], ) -> amici.ExpData: """Get :class:`amici.amici.ExpData` for the given PEtab condition. @@ -768,8 +858,10 @@ def create_edata_for_condition( ExpData instance. """ if amici_model.nytrue != len(observable_ids): - raise AssertionError("Number of AMICI model observables does not " - "match number of PEtab observables.") + raise AssertionError( + "Number of AMICI model observables does not " + "match number of PEtab observables." + ) # create an ExpData object edata = amici.ExpData(amici_model) @@ -778,44 +870,48 @@ def create_edata_for_condition( edata.id += "+" + condition.get(PREEQUILIBRATION_CONDITION_ID) ########################################################################## # enable initial parameters reinitialization - states_in_condition_table = [ - col for col in petab_problem.condition_df - if not pd.isna(petab_problem.condition_df.loc[ - condition[SIMULATION_CONDITION_ID], col]) - and element_is_state(petab_problem.sbml_model, col) - ] - if condition.get(PREEQUILIBRATION_CONDITION_ID) \ - and states_in_condition_table: + + states_in_condition_table = get_states_in_condition_table( + petab_problem, condition=condition + ) + if condition.get(PREEQUILIBRATION_CONDITION_ID) and states_in_condition_table: state_ids = amici_model.getStateIds() - state_idx_reinitalization = [state_ids.index(s) - for s in states_in_condition_table] + state_idx_reinitalization = [ + state_ids.index(s) + for s, (v, v_preeq) in states_in_condition_table.items() + if not np.isnan(v) + ] edata.reinitialization_state_idxs_sim = state_idx_reinitalization - logger.debug("Enabling state reinitialization for condition " - f"{condition.get(PREEQUILIBRATION_CONDITION_ID, '')} - " - f"{condition.get(SIMULATION_CONDITION_ID)} " - f"{states_in_condition_table}") + logger.debug( + "Enabling state reinitialization for condition " + f"{condition.get(PREEQUILIBRATION_CONDITION_ID, '')} - " + f"{condition.get(SIMULATION_CONDITION_ID)} " + f"{states_in_condition_table}" + ) ########################################################################## # timepoints # find replicate numbers of time points - timepoints_w_reps = _get_timepoints_with_replicates( - df_for_condition=measurement_df) + timepoints_w_reps = _get_timepoints_with_replicates(df_for_condition=measurement_df) edata.setTimepoints(timepoints_w_reps) ########################################################################## # measurements and sigmas y, sigma_y = _get_measurements_and_sigmas( - df_for_condition=measurement_df, timepoints_w_reps=timepoints_w_reps, - observable_ids=observable_ids) + df_for_condition=measurement_df, + timepoints_w_reps=timepoints_w_reps, + observable_ids=observable_ids, + ) edata.setObservedData(y.flatten()) edata.setObservedDataStdDev(sigma_y.flatten()) return edata -def _subset_dict(full: Dict[Any, Any], - *args: Collection[Any]) -> Iterator[Dict[Any, Any]]: +def _subset_dict( + full: Dict[Any, Any], *args: Collection[Any] +) -> Iterator[Dict[Any, Any]]: """Get subset of dictionary based on provided keys :param full: @@ -831,7 +927,8 @@ def _subset_dict(full: Dict[Any, Any], def _get_timepoints_with_replicates( - df_for_condition: pd.DataFrame) -> List[numbers.Number]: + df_for_condition: pd.DataFrame, +) -> List[numbers.Number]: """ Get list of timepoints including replicate measurements @@ -849,12 +946,9 @@ def _get_timepoints_with_replicates( timepoints_w_reps = [] for time in timepoints: # subselect for time - df_for_time = df_for_condition[ - df_for_condition.time.astype(float) == time - ] + df_for_time = df_for_condition[df_for_condition.time.astype(float) == time] # rep number is maximum over rep numbers for observables - n_reps = max(df_for_time.groupby( - [OBSERVABLE_ID, TIME]).size()) + n_reps = max(df_for_time.groupby([OBSERVABLE_ID, TIME]).size()) # append time point n_rep times timepoints_w_reps.extend([time] * n_reps) @@ -862,10 +956,10 @@ def _get_timepoints_with_replicates( def _get_measurements_and_sigmas( - df_for_condition: pd.DataFrame, - timepoints_w_reps: Sequence[numbers.Number], - observable_ids: Sequence[str], - ) -> Tuple[np.array, np.array]: + df_for_condition: pd.DataFrame, + timepoints_w_reps: Sequence[numbers.Number], + observable_ids: Sequence[str], +) -> Tuple[np.array, np.array]: """ Get measurements and sigmas @@ -885,8 +979,7 @@ def _get_measurements_and_sigmas( arrays for measurement and sigmas """ # prepare measurement matrix - y = np.full(shape=(len(timepoints_w_reps), len(observable_ids)), - fill_value=np.nan) + y = np.full(shape=(len(timepoints_w_reps), len(observable_ids)), fill_value=np.nan) # prepare sigma matrix sigma_y = y.copy() @@ -912,19 +1005,19 @@ def _get_measurements_and_sigmas( time_ix_for_obs_ix[observable_ix] = time_ix_0 # fill observable and possibly noise parameter - y[time_ix_for_obs_ix[observable_ix], - observable_ix] = measurement[MEASUREMENT] - if isinstance(measurement.get(NOISE_PARAMETERS, None), - numbers.Number): - sigma_y[time_ix_for_obs_ix[observable_ix], - observable_ix] = measurement[NOISE_PARAMETERS] + y[time_ix_for_obs_ix[observable_ix], observable_ix] = measurement[ + MEASUREMENT + ] + if isinstance(measurement.get(NOISE_PARAMETERS, None), numbers.Number): + sigma_y[time_ix_for_obs_ix[observable_ix], observable_ix] = measurement[ + NOISE_PARAMETERS + ] return y, sigma_y def rdatas_to_measurement_df( - rdatas: Sequence[amici.ReturnData], - model: AmiciModel, - measurement_df: pd.DataFrame) -> pd.DataFrame: + rdatas: Sequence[amici.ReturnData], model: AmiciModel, measurement_df: pd.DataFrame +) -> pd.DataFrame: """ Create a measurement dataframe in the PEtab format from the passed ``rdatas`` and own information. @@ -942,8 +1035,7 @@ def rdatas_to_measurement_df( :return: A dataframe built from the rdatas in the format of ``measurement_df``. """ - simulation_conditions = petab.get_simulation_conditions( - measurement_df) + simulation_conditions = petab.get_simulation_conditions(measurement_df) observable_ids = model.getObservableIds() rows = [] @@ -955,8 +1047,7 @@ def rdatas_to_measurement_df( t = list(rdata.ts) # extract rows for condition - cur_measurement_df = petab.get_rows_for_condition( - measurement_df, condition) + cur_measurement_df = petab.get_rows_for_condition(measurement_df, condition) # iterate over entries for the given condition # note: this way we only generate a dataframe entry for every @@ -981,17 +1072,17 @@ def rdatas_to_measurement_df( def rdatas_to_simulation_df( - rdatas: Sequence[amici.ReturnData], - model: AmiciModel, - measurement_df: pd.DataFrame) -> pd.DataFrame: + rdatas: Sequence[amici.ReturnData], model: AmiciModel, measurement_df: pd.DataFrame +) -> pd.DataFrame: """Create a PEtab simulation dataframe from :class:`amici.amici.ReturnData` s. See :func:`rdatas_to_measurement_df` for details, only that model outputs will appear in column ``simulation`` instead of ``measurement``.""" - df = rdatas_to_measurement_df(rdatas=rdatas, model=model, - measurement_df=measurement_df) + df = rdatas_to_measurement_df( + rdatas=rdatas, model=model, measurement_df=measurement_df + ) return df.rename(columns={MEASUREMENT: SIMULATION}) @@ -1023,10 +1114,12 @@ def _default_scaled_parameters( The scaled parameter vector. """ if problem_parameters is None: - problem_parameters = dict(zip( - petab_problem.x_ids, - petab_problem.x_nominal_scaled, - )) + problem_parameters = dict( + zip( + petab_problem.x_ids, + petab_problem.x_nominal_scaled, + ) + ) elif not scaled_parameters: problem_parameters = petab_problem.scale_parameters(problem_parameters) return problem_parameters diff --git a/python/sdist/amici/petab_simulate.py b/python/sdist/amici/petab_simulate.py index 09403153b1..d243a28b8b 100644 --- a/python/sdist/amici/petab_simulate.py +++ b/python/sdist/amici/petab_simulate.py @@ -15,25 +15,22 @@ from typing import Callable import pandas as pd - -from amici import SensitivityMethod_none -from amici import AmiciModel -from amici.petab_import import import_petab_problem -from amici.petab_objective import (simulate_petab, - rdatas_to_measurement_df, - RDATAS) import petab +from amici import AmiciModel, SensitivityMethod_none +from amici.petab_import import import_petab_problem +from amici.petab_objective import RDATAS, rdatas_to_measurement_df, simulate_petab -AMICI_MODEL = 'amici_model' -AMICI_SOLVER = 'solver' -MODEL_NAME = 'model_name' -MODEL_OUTPUT_DIR = 'model_output_dir' +AMICI_MODEL = "amici_model" +AMICI_SOLVER = "solver" +MODEL_NAME = "model_name" +MODEL_OUTPUT_DIR = "model_output_dir" -PETAB_PROBLEM = 'petab_problem' +PETAB_PROBLEM = "petab_problem" class PetabSimulator(petab.simulate.Simulator): """Implementation of the PEtab `Simulator` class that uses AMICI.""" + def __init__(self, *args, amici_model: AmiciModel = None, **kwargs): super().__init__(*args, **kwargs) self.amici_model = amici_model @@ -52,11 +49,13 @@ def simulate_without_noise(self, **kwargs) -> pd.DataFrame: in the Simulator constructor (including the PEtab problem). """ if AMICI_MODEL in {*kwargs, *dir(self)} and ( - any(k in kwargs for k in - inspect.signature(import_petab_problem).parameters)): - print('Arguments related to the PEtab import are unused if ' - f'`{AMICI_MODEL}` is specified, or the ' - '`PetabSimulator.simulate()` method was previously called.') + any(k in kwargs for k in inspect.signature(import_petab_problem).parameters) + ): + print( + "Arguments related to the PEtab import are unused if " + f"`{AMICI_MODEL}` is specified, or the " + "`PetabSimulator.simulate()` method was previously called." + ) kwargs[PETAB_PROBLEM] = self.petab_problem @@ -80,13 +79,12 @@ def simulate_without_noise(self, **kwargs) -> pd.DataFrame: if AMICI_SOLVER not in kwargs: kwargs[AMICI_SOLVER] = self.amici_model.getSolver() - kwargs[AMICI_SOLVER].setSensitivityMethod( - SensitivityMethod_none) + kwargs[AMICI_SOLVER].setSensitivityMethod(SensitivityMethod_none) result = _subset_call(simulate_petab, kwargs) - return rdatas_to_measurement_df(result[RDATAS], - self.amici_model, - self.petab_problem.measurement_df) + return rdatas_to_measurement_df( + result[RDATAS], self.amici_model, self.petab_problem.measurement_df + ) def _subset_call(method: Callable, kwargs: dict): @@ -104,7 +102,5 @@ def _subset_call(method: Callable, kwargs: dict): ``kwargs``. """ method_args = inspect.signature(method).parameters - subset_kwargs = {k: v - for k, v in kwargs.items() - if k in method_args} + subset_kwargs = {k: v for k, v in kwargs.items() if k in method_args} return method(**subset_kwargs) diff --git a/python/sdist/amici/petab_util.py b/python/sdist/amici/petab_util.py new file mode 100644 index 0000000000..31f1ae1313 --- /dev/null +++ b/python/sdist/amici/petab_util.py @@ -0,0 +1,102 @@ +"""Various helper functions for working with PEtab problems.""" +import re +from typing import Dict, Tuple, Union + +import libsbml +import pandas as pd +import petab +from petab.C import PREEQUILIBRATION_CONDITION_ID, SIMULATION_CONDITION_ID +from petab.mapping import resolve_mapping +from petab.models import MODEL_TYPE_PYSB, MODEL_TYPE_SBML + +# ID of model parameter that is to be added to SBML model to indicate +# preequilibration +PREEQ_INDICATOR_ID = "preequilibration_indicator" + + +def get_states_in_condition_table( + petab_problem: petab.Problem, + condition: Union[Dict, pd.Series] = None, + return_patterns: bool = False, +) -> Dict[str, Tuple[Union[float, str, None], Union[float, str, None]]]: + """Get states and their initial condition as specified in the condition table. + + Returns: Dictionary: ``stateId -> (initial condition simulation, initial condition preequilibration)`` + """ + if petab_problem.model.type_id not in (MODEL_TYPE_SBML, MODEL_TYPE_PYSB): + raise NotImplementedError() + + species_check_funs = { + MODEL_TYPE_SBML: lambda x: _element_is_sbml_state( + petab_problem.sbml_model, x + ), + MODEL_TYPE_PYSB: lambda x: _element_is_pysb_pattern( + petab_problem.model.model, x + ), + } + states = { + resolve_mapping(petab_problem.mapping_df, col): (None, None) + if condition is None + else ( + petab_problem.condition_df.loc[ + condition[SIMULATION_CONDITION_ID], col + ], + petab_problem.condition_df.loc[ + condition[PREEQUILIBRATION_CONDITION_ID], col + ] + if PREEQUILIBRATION_CONDITION_ID in condition + else None, + ) + for col in petab_problem.condition_df.columns + if species_check_funs[petab_problem.model.type_id]( + resolve_mapping(petab_problem.mapping_df, col) + ) + } + + if petab_problem.model.type_id == MODEL_TYPE_PYSB: + if return_patterns: + return states + import pysb.pattern + + try: + spm = pysb.pattern.SpeciesPatternMatcher( + model=petab_problem.model.model + ) + except NotImplementedError as e: + raise NotImplementedError( + "Requires https://github.com/pysb/pysb/pull/570. " + "To use this functionality, update pysb via " + "`pip install git+https://github.com/FFroehlich/pysb@fix_pattern_matching`" + ) + + # expose model components as variables so we can evaluate patterns + for c in petab_problem.model.model.components: + globals()[c.name] = c + + states = { + f"__s{ix}": value + for pattern, value in states.items() + for ix in spm.match(eval(pattern), index=True, exact=True) + } + return states + + +def _element_is_pysb_pattern(model: "pysb.Model", element: str) -> bool: + """Check if element is a pysb pattern""" + if match := re.match(r"[a-zA-Z_][\w_]*\(", element): + return match[0][:-1] in [m.name for m in model.monomers] + return False + + +def _element_is_sbml_state(sbml_model: libsbml.Model, sbml_id: str) -> bool: + """Does the element with ID `sbml_id` correspond to a state variable?""" + if sbml_model.getCompartment(sbml_id) is not None: + return True + if sbml_model.getSpecies(sbml_id) is not None: + return True + if ( + rule := sbml_model.getRuleByVariable(sbml_id) + ) is not None and rule.getTypeCode() == libsbml.SBML_RATE_RULE: + return True + + return False diff --git a/python/sdist/amici/plotting.py b/python/sdist/amici/plotting.py index d21bea6a99..da718c1ec7 100644 --- a/python/sdist/amici/plotting.py +++ b/python/sdist/amici/plotting.py @@ -14,11 +14,11 @@ def plot_state_trajectories( - rdata: ReturnDataView, - state_indices: Optional[Iterable[int]] = None, - ax: Optional[Axes] = None, - model: Model = None, - prefer_names: bool = True, + rdata: ReturnDataView, + state_indices: Optional[Iterable[int]] = None, + ax: Optional[Axes] = None, + model: Model = None, + prefer_names: bool = True, ) -> None: """ Plot state trajectories @@ -42,27 +42,27 @@ def plot_state_trajectories( if not ax: fig, ax = plt.subplots() if not state_indices: - state_indices = range(rdata['x'].shape[1]) + state_indices = range(rdata["x"].shape[1]) for ix in state_indices: if model is None: - label = f'$x_{{{ix}}}$' + label = f"$x_{{{ix}}}$" elif prefer_names and model.getStateNames()[ix]: label = model.getStateNames()[ix] else: label = model.getStateIds()[ix] - ax.plot(rdata['t'], rdata['x'][:, ix], label=label) - ax.set_xlabel('$t$') - ax.set_ylabel('$x(t)$') + ax.plot(rdata["t"], rdata["x"][:, ix], label=label) + ax.set_xlabel("$t$") + ax.set_ylabel("$x(t)$") ax.legend() - ax.set_title('State trajectories') + ax.set_title("State trajectories") def plot_observable_trajectories( - rdata: ReturnDataView, - observable_indices: Optional[Iterable[int]] = None, - ax: Optional[Axes] = None, - model: Model = None, - prefer_names: bool = True, + rdata: ReturnDataView, + observable_indices: Optional[Iterable[int]] = None, + ax: Optional[Axes] = None, + model: Model = None, + prefer_names: bool = True, ) -> None: """ Plot observable trajectories @@ -86,19 +86,19 @@ def plot_observable_trajectories( if not ax: fig, ax = plt.subplots() if not observable_indices: - observable_indices = range(rdata['y'].shape[1]) + observable_indices = range(rdata["y"].shape[1]) for iy in observable_indices: if model is None: - label = f'$y_{{{iy}}}$' + label = f"$y_{{{iy}}}$" elif prefer_names and model.getObservableNames()[iy]: label = model.getObservableNames()[iy] else: label = model.getObservableIds()[iy] - ax.plot(rdata['t'], rdata['y'][:, iy], label=label) - ax.set_xlabel('$t$') - ax.set_ylabel('$y(t)$') + ax.plot(rdata["t"], rdata["y"][:, iy], label=label) + ax.set_xlabel("$t$") + ax.set_ylabel("$y(t)$") ax.legend() - ax.set_title('Observable trajectories') + ax.set_title("Observable trajectories") def plot_jacobian(rdata: ReturnDataView): @@ -111,6 +111,7 @@ def plot_jacobian(rdata: ReturnDataView): sns.heatmap(df, center=0.0) plt.title("Jacobian") + # backwards compatibility plotStateTrajectories = plot_state_trajectories plotObservableTrajectories = plot_observable_trajectories diff --git a/python/sdist/amici/pysb_import.py b/python/sdist/amici/pysb_import.py index 8e7167873d..7e413a2a88 100644 --- a/python/sdist/amici/pysb_import.py +++ b/python/sdist/amici/pysb_import.py @@ -10,8 +10,7 @@ import os import sys from pathlib import Path -from typing import (Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, - Union) +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union import numpy as np import pysb @@ -19,15 +18,26 @@ import pysb.pattern import sympy as sp -from .import_utils import (_get_str_symbol_identifiers, - _parse_special_functions, - generate_measurement_symbol, - noise_distribution_to_cost_function, - noise_distribution_to_observable_transformation) +from .de_export import ( + Constant, + DEExporter, + DEModel, + DifferentialState, + Expression, + LogLikelihoodY, + Observable, + Parameter, + SigmaY, + _default_simplify, +) +from .import_utils import ( + _get_str_symbol_identifiers, + _parse_special_functions, + generate_measurement_symbol, + noise_distribution_to_cost_function, + noise_distribution_to_observable_transformation, +) from .logging import get_logger, log_execution_time, set_log_level -from .de_export import (Constant, Expression, LogLikelihoodY, DEExporter, - DEModel, Observable, Parameter, SigmaY, DifferentialState, - _default_simplify) CL_Prototype = Dict[str, Dict[str, Any]] ConservationLaw = Dict[str, Union[Dict, str, sp.Basic]] @@ -36,23 +46,23 @@ def pysb2amici( - model: pysb.Model, - output_dir: Optional[Union[str, Path]] = 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, - assume_pow_positivity: bool = False, - compiler: str = None, - compute_conservation_laws: bool = True, - compile: bool = True, - simplify: Callable = _default_simplify, - # Do not enable by default without testing. - # See https://github.com/AMICI-dev/AMICI/pull/1672 - cache_simplify: bool = False, - generate_sensitivity_code: bool = True, - model_name: Optional[str] = None, + model: pysb.Model, + output_dir: Optional[Union[str, Path]] = 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, + assume_pow_positivity: bool = False, + compiler: str = None, + compute_conservation_laws: bool = True, + compile: bool = True, + simplify: Callable = _default_simplify, + # Do not enable by default without testing. + # See https://github.com/AMICI-dev/AMICI/pull/1672 + cache_simplify: bool = False, + generate_sensitivity_code: bool = True, + model_name: Optional[str] = None, ): r""" Generate AMICI C++ files for the provided model. @@ -72,7 +82,7 @@ def pysb2amici( generated module :param output_dir: - see :meth:`amici.ode_export.ODEExporter.set_paths` + see :meth:`amici.de_export.ODEExporter.set_paths` :param observables: list of :class:`pysb.core.Expression` or :class:`pysb.core.Observable` @@ -143,8 +153,10 @@ def pysb2amici( set_log_level(logger, verbose) ode_model = ode_model_from_pysb_importer( - model, constant_parameters=constant_parameters, - observables=observables, sigmas=sigmas, + model, + constant_parameters=constant_parameters, + observables=observables, + sigmas=sigmas, noise_distributions=noise_distributions, compute_conservation_laws=compute_conservation_laws, simplify=simplify, @@ -158,7 +170,7 @@ def pysb2amici( verbose=verbose, assume_pow_positivity=assume_pow_positivity, compiler=compiler, - generate_sensitivity_code=generate_sensitivity_code + generate_sensitivity_code=generate_sensitivity_code, ) exporter.generate_model_code() @@ -166,19 +178,19 @@ def pysb2amici( exporter.compile_model() -@log_execution_time('creating ODE model', logger) +@log_execution_time("creating ODE model", logger) def ode_model_from_pysb_importer( - model: pysb.Model, - constant_parameters: List[str] = None, - observables: List[str] = None, - sigmas: Dict[str, str] = None, - noise_distributions: Optional[Dict[str, Union[str, Callable]]] = 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, + model: pysb.Model, + constant_parameters: List[str] = None, + observables: List[str] = None, + sigmas: Dict[str, str] = None, + noise_distributions: Optional[Dict[str, Union[str, Callable]]] = 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, ) -> DEModel: """ Creates an :class:`amici.DEModel` instance from a :class:`pysb.Model` @@ -243,12 +255,10 @@ def ode_model_from_pysb_importer( _process_pysb_parameters(model, ode, constant_parameters) if compute_conservation_laws: _process_pysb_conservation_laws(model, ode) - _process_pysb_observables(model, ode, observables, sigmas, - noise_distributions) - _process_pysb_expressions(model, ode, observables, sigmas, - noise_distributions) + _process_pysb_observables(model, ode, observables, sigmas, noise_distributions) + _process_pysb_expressions(model, ode, observables, sigmas, noise_distributions) ode._has_quadratic_nllh = not noise_distributions or all( - noise_distr in ['normal', 'lin-normal', 'log-normal', 'log10-normal'] + noise_distr in ["normal", "lin-normal", "log-normal", "log10-normal"] for noise_distr in noise_distributions.values() ) @@ -259,11 +269,10 @@ def ode_model_from_pysb_importer( return ode -@log_execution_time('processing PySB stoich. matrix', logger) -def _process_stoichiometric_matrix(pysb_model: pysb.Model, - ode_model: DEModel, - constant_parameters: List[str]) -> None: - +@log_execution_time("processing PySB stoich. matrix", logger) +def _process_stoichiometric_matrix( + pysb_model: pysb.Model, ode_model: DEModel, constant_parameters: List[str] +) -> None: """ Exploits the PySB stoichiometric matrix to generate xdot derivatives @@ -277,10 +286,10 @@ def _process_stoichiometric_matrix(pysb_model: pysb.Model, list of constant parameters """ - x = ode_model.sym('x') - w = list(ode_model.sym('w')) - p = list(ode_model.sym('p')) - x_rdata = list(ode_model.sym('x_rdata')) + x = ode_model.sym("x") + w = list(ode_model.sym("w")) + p = list(ode_model.sym("p")) + x_rdata = list(ode_model.sym("x_rdata")) n_x = len(x) n_w = len(w) @@ -305,7 +314,7 @@ def get_cached_index(symbol, sarray, index_cache): return idx for ir, rxn in enumerate(pysb_model.reactions): - for ix in np.unique(rxn['reactants']): + for ix in np.unique(rxn["reactants"]): idx = solver_index.get(ix, None) if idx is not None: # species @@ -315,12 +324,12 @@ def get_cached_index(symbol, sarray, index_cache): idx = get_cached_index(x_rdata[ix], w, wx_idx) values = dflux_dw_dict - values[(ir, idx)] = sp.diff(rxn['rate'], x_rdata[ix]) + values[(ir, idx)] = sp.diff(rxn["rate"], x_rdata[ix]) # typically <= 3 free symbols in rate, we already account for # species above so we only need to account for propensity, which # can only be a parameter or expression - for fs in rxn['rate'].free_symbols: + for fs in rxn["rate"].free_symbols: # dw if isinstance(fs, pysb.Expression): var = w @@ -337,7 +346,7 @@ def get_cached_index(symbol, sarray, index_cache): continue idx = get_cached_index(fs, var, idx_cache) - values[(ir, idx)] = sp.diff(rxn['rate'], fs) + values[(ir, idx)] = sp.diff(rxn["rate"], fs) dflux_dx = sp.ImmutableSparseMatrix(n_r, n_x, dflux_dx_dict) dflux_dw = sp.ImmutableSparseMatrix(n_r, n_w, dflux_dw_dict) @@ -345,20 +354,20 @@ def get_cached_index(symbol, sarray, index_cache): # use dok format to convert numeric csc to sparse symbolic S = sp.ImmutableSparseMatrix( - n_x, n_r, # don't use shape here as we are eliminating rows + n_x, + n_r, # don't use shape here as we are eliminating rows pysb_model.stoichiometry_matrix[ - np.asarray(list(solver_index.keys())),: - ].todok() + np.asarray(list(solver_index.keys())), : + ].todok(), ) # don't use `.dot` since it's awfully slow - ode_model._eqs['dxdotdx_explicit'] = S*dflux_dx - ode_model._eqs['dxdotdw'] = S*dflux_dw - ode_model._eqs['dxdotdp_explicit'] = S*dflux_dp + ode_model._eqs["dxdotdx_explicit"] = S * dflux_dx + ode_model._eqs["dxdotdw"] = S * dflux_dw + ode_model._eqs["dxdotdp_explicit"] = S * dflux_dp -@log_execution_time('processing PySB species', logger) -def _process_pysb_species(pysb_model: pysb.Model, - ode_model: DEModel) -> None: +@log_execution_time("processing PySB species", logger) +def _process_pysb_species(pysb_model: pysb.Model, ode_model: DEModel) -> None: """ Converts pysb Species into States and adds them to the DEModel instance @@ -371,10 +380,9 @@ def _process_pysb_species(pysb_model: pysb.Model, xdot = sp.Matrix(pysb_model.odes) for ix, specie in enumerate(pysb_model.species): - init = sp.sympify('0.0') + init = sp.sympify("0.0") for ic in pysb_model.odes.model.initials: - if pysb.pattern.match_complex_pattern( - ic.pattern, specie, exact=True): + if pysb.pattern.match_complex_pattern(ic.pattern, specie, exact=True): # we don't want to allow expressions in initial conditions if ic.value in pysb_model.expressions: init = pysb_model.expressions[ic.value.name].expand_expr() @@ -382,20 +390,15 @@ def _process_pysb_species(pysb_model: pysb.Model, init = ic.value ode_model.add_component( - DifferentialState( - sp.Symbol(f'__s{ix}'), - f'{specie}', - init, - xdot[ix] - ) + DifferentialState(sp.Symbol(f"__s{ix}"), f"{specie}", init, xdot[ix]) ) - logger.debug(f'Finished Processing PySB species ') + logger.debug(f"Finished Processing PySB species ") -@log_execution_time('processing PySB parameters', logger) -def _process_pysb_parameters(pysb_model: pysb.Model, - ode_model: DEModel, - constant_parameters: List[str]) -> None: +@log_execution_time("processing PySB parameters", logger) +def _process_pysb_parameters( + pysb_model: pysb.Model, ode_model: DEModel, constant_parameters: List[str] +) -> None: """ Converts pysb parameters into Parameters or Constants and adds them to the DEModel instance @@ -415,18 +418,16 @@ def _process_pysb_parameters(pysb_model: pysb.Model, else: comp = Parameter - ode_model.add_component( - comp(par, f'{par.name}', par.value) - ) + ode_model.add_component(comp(par, f"{par.name}", par.value)) -@log_execution_time('processing PySB expressions', logger) +@log_execution_time("processing PySB expressions", logger) def _process_pysb_expressions( - pysb_model: pysb.Model, - ode_model: DEModel, - observables: List[str], - sigmas: Dict[str, str], - noise_distributions: Optional[Dict[str, Union[str, Callable]]] = None, + pysb_model: pysb.Model, + ode_model: DEModel, + observables: List[str], + sigmas: Dict[str, str], + noise_distributions: Optional[Dict[str, Union[str, Callable]]] = None, ) -> None: r""" Converts pysb expressions/observables into Observables (with @@ -459,20 +460,27 @@ def _process_pysb_expressions( # we use _constant and _dynamic functions to get access to derived # expressions that are otherwise only accessible as private attribute - for expr in pysb_model.expressions_constant(include_derived=True)\ - | pysb_model.expressions_dynamic(include_derived=True): + for expr in pysb_model.expressions_constant( + include_derived=True + ) | pysb_model.expressions_dynamic(include_derived=True): if any( - isinstance(symbol, pysb.Tag) - for symbol in expr.expand_expr().free_symbols + isinstance(symbol, pysb.Tag) for symbol in expr.expand_expr().free_symbols ): # we only need explicit instantiations of expressions with tags, # which are defined in the derived expressions. The abstract # expressions are not needed and lead to compilation errors so # we skip them. continue - _add_expression(expr, expr.name, expr.expr, - pysb_model, ode_model, observables, sigmas, - noise_distributions) + _add_expression( + expr, + expr.name, + expr.expr, + pysb_model, + ode_model, + observables, + sigmas, + noise_distributions, + ) def _add_expression( @@ -513,46 +521,37 @@ def _add_expression( :param ode_model: see :py:func:`_process_pysb_expressions` """ - ode_model.add_component( - Expression(sym, name, _parse_special_functions(expr)) - ) + ode_model.add_component(Expression(sym, name, _parse_special_functions(expr))) if name in observables: - noise_dist = noise_distributions.get(name, 'normal') \ - if noise_distributions else 'normal' + noise_dist = ( + noise_distributions.get(name, "normal") if noise_distributions else "normal" + ) - y = sp.Symbol(f'{name}') + y = sp.Symbol(f"{name}") trafo = noise_distribution_to_observable_transformation(noise_dist) obs = Observable(y, name, sym, transformation=trafo) ode_model.add_component(obs) - sigma_name, sigma_value = _get_sigma_name_and_value( - pysb_model, name, sigmas - ) + sigma_name, sigma_value = _get_sigma_name_and_value(pysb_model, name, sigmas) sigma = sp.Symbol(sigma_name) - ode_model.add_component(SigmaY(sigma, f'{sigma_name}', sigma_value)) - + ode_model.add_component(SigmaY(sigma, f"{sigma_name}", sigma_value)) cost_fun_str = noise_distribution_to_cost_function(noise_dist)(name) my = generate_measurement_symbol(obs.get_id()) - cost_fun_expr = sp.sympify(cost_fun_str, - locals=dict(zip( - _get_str_symbol_identifiers(name), - (y, my, sigma)))) + cost_fun_expr = sp.sympify( + cost_fun_str, + locals=dict(zip(_get_str_symbol_identifiers(name), (y, my, sigma))), + ) ode_model.add_component( - LogLikelihoodY( - sp.Symbol(f'llh_{name}'), - f'llh_{name}', - cost_fun_expr - ) + LogLikelihoodY(sp.Symbol(f"llh_{name}"), f"llh_{name}", cost_fun_expr) ) def _get_sigma_name_and_value( - pysb_model: pysb.Model, - obs_name: str, - sigmas: Dict[str, str]) -> Tuple[str, sp.Basic]: + pysb_model: pysb.Model, obs_name: str, sigmas: Dict[str, str] +) -> Tuple[str, sp.Basic]: """ Tries to extract standard deviation symbolic identifier and formula for a given observable name from the pysb model and if no specification is @@ -576,26 +575,26 @@ def _get_sigma_name_and_value( sigma_name = sigmas[obs_name] try: # find corresponding Expression instance - sigma_expr = next(x for x in pysb_model.expressions - if x.name == sigma_name) + sigma_expr = next(x for x in pysb_model.expressions if x.name == sigma_name) except StopIteration: - raise ValueError(f'value of sigma {obs_name} is not a ' - f'valid expression.') + raise ValueError( + f"value of sigma {obs_name} is not a " f"valid expression." + ) sigma_value = sigma_expr.expand_expr() else: - sigma_name = f'sigma_{obs_name}' + sigma_name = f"sigma_{obs_name}" sigma_value = sp.sympify(1.0) return sigma_name, sigma_value -@log_execution_time('processing PySB observables', logger) +@log_execution_time("processing PySB observables", logger) def _process_pysb_observables( - pysb_model: pysb.Model, - ode_model: DEModel, - observables: List[str], - sigmas: Dict[str, str], - noise_distributions: Optional[Dict[str, Union[str, Callable]]] = None, + pysb_model: pysb.Model, + ode_model: DEModel, + observables: List[str], + sigmas: Dict[str, str], + noise_distributions: Optional[Dict[str, Union[str, Callable]]] = None, ) -> None: """ Converts :class:`pysb.core.Observable` into @@ -621,14 +620,20 @@ def _process_pysb_observables( # only add those pysb observables that occur in the added # Observables as expressions for obs in pysb_model.observables: - _add_expression(obs, obs.name, obs.expand_obs(), - pysb_model, ode_model, observables, sigmas, - noise_distributions) + _add_expression( + obs, + obs.name, + obs.expand_obs(), + pysb_model, + ode_model, + observables, + sigmas, + noise_distributions, + ) -@log_execution_time('computing PySB conservation laws', logger) -def _process_pysb_conservation_laws(pysb_model: pysb.Model, - ode_model: DEModel) -> None: +@log_execution_time("computing PySB conservation laws", logger) +def _process_pysb_conservation_laws(pysb_model: pysb.Model, ode_model: DEModel) -> None: """ Removes species according to conservation laws to ensure that the jacobian has full rank @@ -642,11 +647,11 @@ def _process_pysb_conservation_laws(pysb_model: pysb.Model, monomers_without_conservation_law = set() for rule in pysb_model.rules: - monomers_without_conservation_law |= \ - _get_unconserved_monomers(rule, pysb_model) + monomers_without_conservation_law |= _get_unconserved_monomers(rule, pysb_model) - monomers_without_conservation_law |= \ + monomers_without_conservation_law |= ( _compute_monomers_with_fixed_initial_conditions(pysb_model) + ) cl_prototypes = _generate_cl_prototypes( monomers_without_conservation_law, pysb_model, ode_model @@ -662,8 +667,7 @@ def _process_pysb_conservation_laws(pysb_model: pysb.Model, ode_model.add_conservation_law(**cl) -def _compute_monomers_with_fixed_initial_conditions( - pysb_model: pysb.Model) -> Set[str]: +def _compute_monomers_with_fixed_initial_conditions(pysb_model: pysb.Model) -> Set[str]: """ Computes the set of monomers in a model with species that have fixed initial conditions @@ -679,19 +683,21 @@ def _compute_monomers_with_fixed_initial_conditions( # check if monomer has an initial condition that is fixed (means # that corresponding state is constant and all conservation # laws are broken) - if any([ - ic.fixed # true or false - for ic in pysb_model.initials - if monomer.name in extract_monomers(ic.pattern) - ]): + if any( + [ + ic.fixed # true or false + for ic in pysb_model.initials + if monomer.name in extract_monomers(ic.pattern) + ] + ): monomers_with_fixed_initial_conditions |= {monomer.name} return monomers_with_fixed_initial_conditions -def _generate_cl_prototypes(excluded_monomers: Iterable[str], - pysb_model: pysb.Model, - ode_model: DEModel) -> CL_Prototype: +def _generate_cl_prototypes( + excluded_monomers: Iterable[str], pysb_model: pysb.Model, ode_model: DEModel +) -> CL_Prototype: """ Constructs a dict that contains preprocessed information for the construction of conservation laws @@ -711,18 +717,19 @@ def _generate_cl_prototypes(excluded_monomers: Iterable[str], """ cl_prototypes = dict() - _compute_possible_indices(cl_prototypes, pysb_model, ode_model, - excluded_monomers) + _compute_possible_indices(cl_prototypes, pysb_model, ode_model, excluded_monomers) _compute_dependency_idx(cl_prototypes) _compute_target_index(cl_prototypes, ode_model) return cl_prototypes -def _compute_possible_indices(cl_prototypes: CL_Prototype, - pysb_model: pysb.Model, - ode_model: DEModel, - excluded_monomers: Iterable[str]) -> None: +def _compute_possible_indices( + cl_prototypes: CL_Prototype, + pysb_model: pysb.Model, + ode_model: DEModel, + excluded_monomers: Iterable[str], +) -> None: """ Computes viable choices for target_index, ie species that could be removed and replaced by an algebraic expression according to the @@ -752,28 +759,28 @@ def _compute_possible_indices(cl_prototypes: CL_Prototype, ] if len(set(compartments)) > 1: - raise ValueError('Conservation laws involving species in ' - 'multiple compartments are currently not ' - 'supported! Please run pysb2amici with ' - 'compute_conservation_laws=False') + raise ValueError( + "Conservation laws involving species in " + "multiple compartments are currently not " + "supported! Please run pysb2amici with " + "compute_conservation_laws=False" + ) # TODO: implement this, multiply species by the volume of # their respective compartment and allow total_cl to depend # on parameters + constants and update the respective symbolic # derivative accordingly prototype = dict() - prototype['possible_indices'] = [ + prototype["possible_indices"] = [ ix for ix, specie in enumerate(pysb_model.species) if monomer.name in extract_monomers(specie) - and not ode_model.state_is_constant(ix) + and not ode_model.state_is_constant(ix) ] - prototype['species_count'] = len( - prototype['possible_indices'] - ) + prototype["species_count"] = len(prototype["possible_indices"]) - if prototype['possible_indices']: + if prototype["possible_indices"]: cl_prototypes[monomer.name] = prototype @@ -791,35 +798,34 @@ def _compute_dependency_idx(cl_prototypes: CL_Prototype) -> None: """ # for monomer_i, prototype_i in cl_prototypes.items(): - if 'dependency_idx' not in prototype_i: - prototype_i['dependency_idx'] = dict() + if "dependency_idx" not in prototype_i: + prototype_i["dependency_idx"] = dict() for monomer_j, prototype_j in cl_prototypes.items(): if monomer_i == monomer_j: continue - if 'dependency_idx' not in prototype_j: - prototype_j['dependency_idx'] = dict() + if "dependency_idx" not in prototype_j: + prototype_j["dependency_idx"] = dict() - idx_overlap = set(prototype_i['possible_indices']).intersection( - set(prototype_j['possible_indices']) + idx_overlap = set(prototype_i["possible_indices"]).intersection( + set(prototype_j["possible_indices"]) ) if len(idx_overlap) == 0: continue for idx in idx_overlap: - if idx not in prototype_i['dependency_idx']: - prototype_i['dependency_idx'][idx] = set() + if idx not in prototype_i["dependency_idx"]: + prototype_i["dependency_idx"][idx] = set() - if idx not in prototype_j['dependency_idx']: - prototype_j['dependency_idx'][idx] = set() + if idx not in prototype_j["dependency_idx"]: + prototype_j["dependency_idx"][idx] = set() - prototype_i['dependency_idx'][idx] |= {monomer_j} - prototype_j['dependency_idx'][idx] |= {monomer_i} + prototype_i["dependency_idx"][idx] |= {monomer_j} + prototype_j["dependency_idx"][idx] |= {monomer_i} -def _compute_target_index(cl_prototypes: CL_Prototype, - ode_model: DEModel) -> None: +def _compute_target_index(cl_prototypes: CL_Prototype, ode_model: DEModel) -> None: """ Computes the target index for every monomer @@ -829,10 +835,18 @@ def _compute_target_index(cl_prototypes: CL_Prototype, :param ode_model: DEModel instance """ - possible_indices = list(set(list(itertools.chain(*[ - cl_prototypes[monomer]['possible_indices'] - for monomer in cl_prototypes - ])))) + possible_indices = list( + set( + list( + itertools.chain( + *[ + cl_prototypes[monomer]["possible_indices"] + for monomer in cl_prototypes + ] + ) + ) + ) + ) # Note: currently this function is supposed to also count appearances in # expressions. However, expressions are currently still empty as they @@ -848,22 +862,22 @@ def _compute_target_index(cl_prototypes: CL_Prototype, for monomer in cl_prototypes: prototype = cl_prototypes[monomer] # extract monomer specific appearance counts - prototype['appearance_counts'] = \ - [ - appearance_counts[possible_indices.index(idx)] - for idx in prototype['possible_indices'] - ] + prototype["appearance_counts"] = [ + appearance_counts[possible_indices.index(idx)] + for idx in prototype["possible_indices"] + ] # select target index as possible index with minimal appearance count - if len(prototype['appearance_counts']) == 0: - raise RuntimeError(f'Failed to compute conservation law for ' - f'monomer {monomer}') + if len(prototype["appearance_counts"]) == 0: + raise RuntimeError( + f"Failed to compute conservation law for " f"monomer {monomer}" + ) - idx = np.argmin(prototype['appearance_counts']) + idx = np.argmin(prototype["appearance_counts"]) # remove entries from possible indices and appearance counts so we # do not consider them again in later iterations - prototype['target_index'] = prototype['possible_indices'].pop(idx) - prototype['appearance_count'] = prototype['appearance_counts'].pop(idx) + prototype["target_index"] = prototype["possible_indices"].pop(idx) + prototype["appearance_count"] = prototype["appearance_counts"].pop(idx) # this is only an approximation as the effective species count # of other conservation laws may also be affected by the chosen @@ -871,8 +885,7 @@ def _compute_target_index(cl_prototypes: CL_Prototype, # multimers has a low upper bound and the species count does not # vary too much across conservation laws, this approximation # should be fine - prototype['fillin'] = \ - prototype['appearance_count'] * prototype['species_count'] + prototype["fillin"] = prototype["appearance_count"] * prototype["species_count"] # we might end up with the same index for multiple monomers, so loop until # we have a set of unique target indices @@ -893,10 +906,7 @@ def _cl_prototypes_are_valid(cl_prototypes: CL_Prototype) -> bool: if len(cl_prototypes) != len(set(_get_target_indices(cl_prototypes))): return False # conservation law dependencies are cycle free - if any( - _cl_has_cycle(monomer, cl_prototypes) - for monomer in cl_prototypes - ): + if any(_cl_has_cycle(monomer, cl_prototypes) for monomer in cl_prototypes): return False return True @@ -919,28 +929,20 @@ def _cl_has_cycle(monomer: str, cl_prototypes: CL_Prototype) -> bool: prototype = cl_prototypes[monomer] - if prototype['target_index'] not in prototype['dependency_idx']: + if prototype["target_index"] not in prototype["dependency_idx"]: return False visited = [monomer] root = monomer return any( - _is_in_cycle( - connecting_monomer, - cl_prototypes, - visited, - root - ) - for connecting_monomer in prototype['dependency_idx'][ - prototype['target_index'] - ] + _is_in_cycle(connecting_monomer, cl_prototypes, visited, root) + for connecting_monomer in prototype["dependency_idx"][prototype["target_index"]] ) -def _is_in_cycle(monomer: str, - cl_prototypes: CL_Prototype, - visited: List[str], - root: str) -> bool: +def _is_in_cycle( + monomer: str, cl_prototypes: CL_Prototype, visited: List[str], root: str +) -> bool: """ Recursively checks for cycles in conservation law dependencies via Depth First Search @@ -973,19 +975,12 @@ def _is_in_cycle(monomer: str, prototype = cl_prototypes[monomer] - if prototype['target_index'] not in prototype['dependency_idx']: + if prototype["target_index"] not in prototype["dependency_idx"]: return False return any( - _is_in_cycle( - connecting_monomer, - cl_prototypes, - visited, - root - ) - for connecting_monomer in prototype['dependency_idx'][ - prototype['target_index'] - ] + _is_in_cycle(connecting_monomer, cl_prototypes, visited, root) + for connecting_monomer in prototype["dependency_idx"][prototype["target_index"]] ) @@ -1002,8 +997,9 @@ def _greedy_target_index_update(cl_prototypes: CL_Prototype) -> None: target_indices = _get_target_indices(cl_prototypes) for monomer, prototype in cl_prototypes.items(): - if target_indices.count(prototype['target_index']) > 1 or \ - _cl_has_cycle(monomer, cl_prototypes): + if target_indices.count(prototype["target_index"]) > 1 or _cl_has_cycle( + monomer, cl_prototypes + ): # compute how much fillin the next best target_index would yield # we exclude already existing target indices to avoid that @@ -1012,52 +1008,44 @@ def _greedy_target_index_update(cl_prototypes: CL_Prototype) -> None: # solution but prevents infinite loops for target_index in list(set(target_indices)): try: - local_idx = prototype['possible_indices'].index( - target_index - ) + local_idx = prototype["possible_indices"].index(target_index) except ValueError: local_idx = None if local_idx: - del prototype['possible_indices'][local_idx] - del prototype['appearance_counts'][local_idx] + del prototype["possible_indices"][local_idx] + del prototype["appearance_counts"][local_idx] - if len(prototype['possible_indices']) == 0: - prototype['diff_fillin'] = -1 + if len(prototype["possible_indices"]) == 0: + prototype["diff_fillin"] = -1 continue - idx = np.argmin(prototype['appearance_counts']) + idx = np.argmin(prototype["appearance_counts"]) - prototype['local_index'] = idx - prototype['alternate_target_index'] = \ - prototype['possible_indices'][idx] - prototype['alternate_appearance_count'] = \ - prototype['appearance_counts'][idx] + prototype["local_index"] = idx + prototype["alternate_target_index"] = prototype["possible_indices"][idx] + prototype["alternate_appearance_count"] = prototype["appearance_counts"][ + idx + ] - prototype['alternate_fillin'] = \ - prototype['alternate_appearance_count'] \ - * prototype['species_count'] + prototype["alternate_fillin"] = ( + prototype["alternate_appearance_count"] * prototype["species_count"] + ) - prototype['diff_fillin'] = \ - prototype['alternate_fillin'] - prototype['fillin'] + prototype["diff_fillin"] = ( + prototype["alternate_fillin"] - prototype["fillin"] + ) else: - prototype['diff_fillin'] = -1 + prototype["diff_fillin"] = -1 - if all( - prototype['diff_fillin'] == -1 - for prototype in cl_prototypes.values() - ): - raise RuntimeError('Could not compute a valid set of conservation ' - 'laws for this model!') + if all(prototype["diff_fillin"] == -1 for prototype in cl_prototypes.values()): + raise RuntimeError( + "Could not compute a valid set of conservation " "laws for this model!" + ) # this puts prototypes with high diff_fillin last - cl_prototypes = sorted( - cl_prototypes.items(), key=lambda kv: kv[1]['diff_fillin'] - ) - cl_prototypes = { - proto[0]: proto[1] - for proto in cl_prototypes - } + cl_prototypes = sorted(cl_prototypes.items(), key=lambda kv: kv[1]["diff_fillin"]) + cl_prototypes = {proto[0]: proto[1] for proto in cl_prototypes} for monomer in cl_prototypes: prototype = cl_prototypes[monomer] @@ -1069,24 +1057,19 @@ def _greedy_target_index_update(cl_prototypes: CL_Prototype) -> None: # with the highest diff_fillin (note that the target index counts # are recomputed on the fly) - if prototype['diff_fillin'] > -1 \ - and ( - _get_target_indices(cl_prototypes).count( - prototype['target_index'] - ) > 1 - or _cl_has_cycle(monomer, cl_prototypes) + if prototype["diff_fillin"] > -1 and ( + _get_target_indices(cl_prototypes).count(prototype["target_index"]) > 1 + or _cl_has_cycle(monomer, cl_prototypes) ): - prototype['fillin'] = prototype['alternate_fillin'] - prototype['target_index'] = prototype['alternate_target_index'] - prototype['appearance_count'] = \ - prototype['alternate_appearance_count'] + prototype["fillin"] = prototype["alternate_fillin"] + prototype["target_index"] = prototype["alternate_target_index"] + prototype["appearance_count"] = prototype["alternate_appearance_count"] - del prototype['possible_indices'][prototype['local_index']] - del prototype['appearance_counts'][prototype['local_index']] + del prototype["possible_indices"][prototype["local_index"]] + del prototype["appearance_counts"][prototype["local_index"]] -def _get_target_indices( - cl_prototypes: CL_Prototype) -> List[List[int]]: +def _get_target_indices(cl_prototypes: CL_Prototype) -> List[List[int]]: """ Computes the list target indices for the current conservation law prototype @@ -1097,14 +1080,11 @@ def _get_target_indices( :return: List of lists of target indices """ - return [ - prototype['target_index'] for prototype in cl_prototypes.values() - ] + return [prototype["target_index"] for prototype in cl_prototypes.values()] def _construct_conservation_from_prototypes( - cl_prototypes: CL_Prototype, - pysb_model: pysb.Model + cl_prototypes: CL_Prototype, pysb_model: pysb.Model ) -> List[ConservationLaw]: """ Computes the algebraic expression for the total amount of a given @@ -1121,26 +1101,27 @@ def _construct_conservation_from_prototypes( """ conservation_laws = [] for monomer_name in cl_prototypes: - target_index = cl_prototypes[monomer_name]['target_index'] + target_index = cl_prototypes[monomer_name]["target_index"] coefficients = dict() for ix, specie in enumerate(pysb_model.species): count = extract_monomers(specie).count(monomer_name) if count > 0: - coefficients[sp.Symbol(f'__s{ix}')] = count - - conservation_laws.append({ - 'state': sp.Symbol(f'__s{target_index}'), - 'total_abundance': sp.Symbol(f'tcl__s{target_index}'), - 'coefficients': coefficients, - }) + coefficients[sp.Symbol(f"__s{ix}")] = count + + conservation_laws.append( + { + "state": sp.Symbol(f"__s{target_index}"), + "total_abundance": sp.Symbol(f"tcl__s{target_index}"), + "coefficients": coefficients, + } + ) return conservation_laws def _add_conservation_for_constant_species( - ode_model: DEModel, - conservation_laws: List[ConservationLaw] + ode_model: DEModel, conservation_laws: List[ConservationLaw] ) -> None: """ Computes the algebraic expression for the total amount of a given @@ -1156,15 +1137,16 @@ def _add_conservation_for_constant_species( for ix in range(ode_model.num_states_rdata()): if ode_model.state_is_constant(ix): - conservation_laws.append({ - 'state': sp.Symbol(f'__s{ix}'), - 'total_abundance': sp.Symbol(f'tcl__s{ix}'), - 'coefficients': {sp.Symbol(f'__s{ix}'): 1.0} - }) + conservation_laws.append( + { + "state": sp.Symbol(f"__s{ix}"), + "total_abundance": sp.Symbol(f"tcl__s{ix}"), + "coefficients": {sp.Symbol(f"__s{ix}"): 1.0}, + } + ) -def _flatten_conservation_laws( - conservation_laws: List[ConservationLaw]) -> None: +def _flatten_conservation_laws(conservation_laws: List[ConservationLaw]) -> None: """ Flatten the conservation laws such that the state_expr not longer depend on any states that are replaced by conservation laws @@ -1172,22 +1154,20 @@ def _flatten_conservation_laws( :param conservation_laws: see return of :func:`_construct_conservation_from_prototypes` """ - conservation_law_subs = \ - _get_conservation_law_subs(conservation_laws) + conservation_law_subs = _get_conservation_law_subs(conservation_laws) while conservation_law_subs: for cl in conservation_laws: # only update if we changed something if any( - _apply_conseration_law_sub(cl, sub) - for sub in conservation_law_subs + _apply_conseration_law_sub(cl, sub) for sub in conservation_law_subs ): - conservation_law_subs = \ - _get_conservation_law_subs(conservation_laws) + conservation_law_subs = _get_conservation_law_subs(conservation_laws) -def _apply_conseration_law_sub(cl: ConservationLaw, - sub: Tuple[sp.Symbol, ConservationLaw]) -> bool: +def _apply_conseration_law_sub( + cl: ConservationLaw, sub: Tuple[sp.Symbol, ConservationLaw] +) -> bool: """ Applies a substitution to a conservation law by replacing the coefficient of the state of the @@ -1204,26 +1184,24 @@ def _apply_conseration_law_sub(cl: ConservationLaw, if not _state_in_cl_formula(sub[0], cl): return False - coeff = cl['coefficients'].pop(sub[0], 0.0) + coeff = cl["coefficients"].pop(sub[0], 0.0) # x_j = T/b_j - sum_{i≠j}(x_i * b_i) / b_j # don't need to account for totals here as we can simply # absorb that into the new total for k, v in sub[1].items(): if k == sub[0]: continue - update = - coeff * v / sub[1][sub[0]] + update = -coeff * v / sub[1][sub[0]] - if k in cl['coefficients']: - cl['coefficients'][k] += update + if k in cl["coefficients"]: + cl["coefficients"][k] += update else: - cl['coefficients'][k] = update + cl["coefficients"][k] = update return True -def _state_in_cl_formula( - state: sp.Symbol, cl: ConservationLaw -) -> bool: +def _state_in_cl_formula(state: sp.Symbol, cl: ConservationLaw) -> bool: """ Checks whether state appears in the formula the provided cl @@ -1236,14 +1214,14 @@ def _state_in_cl_formula( :return: boolean indicator """ - if cl['state'] == state: + if cl["state"] == state: return False - return cl['coefficients'].get(state, 0.0) != 0.0 + return cl["coefficients"].get(state, 0.0) != 0.0 def _get_conservation_law_subs( - conservation_laws: List[ConservationLaw] + conservation_laws: List[ConservationLaw], ) -> List[Tuple[sp.Symbol, Dict[sp.Symbol, sp.Expr]]]: """ Computes a list of (state, coeffs) tuples for conservation laws that still @@ -1257,19 +1235,21 @@ def _get_conservation_law_subs( subs """ return [ - (cl['state'], cl['coefficients']) for cl in conservation_laws + (cl["state"], cl["coefficients"]) + for cl in conservation_laws if any( - _state_in_cl_formula(cl['state'], other_cl) + _state_in_cl_formula(cl["state"], other_cl) for other_cl in conservation_laws ) ] -def has_fixed_parameter_ic(specie: pysb.core.ComplexPattern, - pysb_model: pysb.Model, - ode_model: DEModel) -> bool: + +def has_fixed_parameter_ic( + specie: pysb.core.ComplexPattern, pysb_model: pysb.Model, ode_model: DEModel +) -> bool: """ Wrapper to interface - :meth:`ode_export.DEModel.state_has_fixed_parameter_initial_condition` + :meth:`de_export.DEModel.state_has_fixed_parameter_initial_condition` from a pysb specie/model arguments :param specie: @@ -1284,29 +1264,25 @@ def has_fixed_parameter_ic(specie: pysb.core.ComplexPattern, :return: ``False`` if the species does not have an initial condition at all. Otherwise the return value of - :meth:`ode_export.DEModel.state_has_fixed_parameter_initial_condition` + :meth:`de_export.DEModel.state_has_fixed_parameter_initial_condition` """ # ComplexPatterns are not hashable, so we have to compare by string ic_index = next( ( ic for ic, condition in enumerate(pysb_model.initials) - if pysb.pattern.match_complex_pattern(condition[0], - specie, exact=True) + if pysb.pattern.match_complex_pattern(condition[0], specie, exact=True) ), - None + None, ) if ic_index is None: return False else: - return ode_model.state_has_fixed_parameter_initial_condition( - ic_index - ) + return ode_model.state_has_fixed_parameter_initial_condition(ic_index) def extract_monomers( - complex_patterns: Union[pysb.ComplexPattern, - List[pysb.ComplexPattern]] + complex_patterns: Union[pysb.ComplexPattern, List[pysb.ComplexPattern]] ) -> List[str]: """ Constructs a list of monomer names contained in complex patterns. @@ -1328,8 +1304,7 @@ def extract_monomers( ] -def _get_unconserved_monomers(rule: pysb.Rule, - pysb_model: pysb.Model) -> Set[str]: +def _get_unconserved_monomers(rule: pysb.Rule, pysb_model: pysb.Model) -> Set[str]: """ Constructs the set of monomer names for which the specified rule changes the stoichiometry of the monomer in the specified model. @@ -1345,31 +1320,29 @@ def _get_unconserved_monomers(rule: pysb.Rule, """ unconserved_monomers = set() - if not rule.delete_molecules \ - and len(rule.product_pattern.complex_patterns) == 0: + if not rule.delete_molecules and len(rule.product_pattern.complex_patterns) == 0: # if delete_molecules is not True but we have a degradation rule, # we have to actually go through the reactions that are created by # the rule - for reaction in [r for r in pysb_model.reactions - if rule.name in r['rule']]: + for reaction in [r for r in pysb_model.reactions if rule.name in r["rule"]]: unconserved_monomers |= _get_changed_stoichiometries( - [pysb_model.species[ix] for ix in reaction['reactants']], - [pysb_model.species[ix] for ix in reaction['products']] + [pysb_model.species[ix] for ix in reaction["reactants"]], + [pysb_model.species[ix] for ix in reaction["products"]], ) else: # otherwise we can simply extract all information for the rule # itself, which is computationally much more efficient unconserved_monomers |= _get_changed_stoichiometries( rule.reactant_pattern.complex_patterns, - rule.product_pattern.complex_patterns + rule.product_pattern.complex_patterns, ) return unconserved_monomers def _get_changed_stoichiometries( - reactants: Union[pysb.ComplexPattern, List[pysb.ComplexPattern]], - products: Union[pysb.ComplexPattern, List[pysb.ComplexPattern]] + reactants: Union[pysb.ComplexPattern, List[pysb.ComplexPattern]], + products: Union[pysb.ComplexPattern, List[pysb.ComplexPattern]], ) -> Set[str]: """ Constructs the set of monomer names which have different @@ -1386,13 +1359,9 @@ def _get_changed_stoichiometries( changed_stoichiometries = set() - reactant_monomers = extract_monomers( - reactants - ) + reactant_monomers = extract_monomers(reactants) - product_monomers = extract_monomers( - products - ) + product_monomers = extract_monomers(products) for monomer in set(reactant_monomers + product_monomers): if reactant_monomers.count(monomer) != product_monomers.count(monomer): @@ -1408,12 +1377,13 @@ def pysb_model_from_path(pysb_model_file: Union[str, Path]) -> pysb.Model: :return: The pysb Model instance """ - pysb_model_module_name = \ - os.path.splitext(os.path.split(pysb_model_file)[-1])[0] + pysb_model_module_name = os.path.splitext(os.path.split(pysb_model_file)[-1])[0] import importlib.util + spec = importlib.util.spec_from_file_location( - pysb_model_module_name, pysb_model_file) + pysb_model_module_name, pysb_model_file + ) module = importlib.util.module_from_spec(spec) sys.modules[pysb_model_module_name] = module spec.loader.exec_module(module) diff --git a/python/sdist/amici/sbml_import.py b/python/sdist/amici/sbml_import.py index 6995a252d5..365e3441a6 100644 --- a/python/sdist/amici/sbml_import.py +++ b/python/sdist/amici/sbml_import.py @@ -6,44 +6,52 @@ """ import copy import itertools as itt -import numpy as np import logging import math import os import re import warnings +import xml.etree.ElementTree as ET from pathlib import Path -from typing import (Any, Callable, Dict, Iterable, List, Optional, Tuple, - Union, Set, List) +from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union import libsbml as sbml +import numpy as np import sympy as sp from . import has_clibs from .constants import SymbolId -from .import_utils import (RESERVED_SYMBOLS, - _check_unsupported_functions, - _get_str_symbol_identifiers, - _parse_special_functions, - generate_measurement_symbol, - generate_regularization_symbol, - noise_distribution_to_cost_function, - noise_distribution_to_observable_transformation, - smart_subs, smart_subs_dict, toposort_symbols, - SBMLException) -from .logging import get_logger, log_execution_time, set_log_level from .de_export import ( - DEExporter, DEModel, symbol_with_assumptions, _default_simplify, - smart_is_zero_matrix + DEExporter, + DEModel, + _default_simplify, + smart_is_zero_matrix, + symbol_with_assumptions, ) - +from .import_utils import ( + RESERVED_SYMBOLS, + _check_unsupported_functions, + _get_str_symbol_identifiers, + _parse_special_functions, + amici_time_symbol, + annotation_namespace, + generate_measurement_symbol, + generate_regularization_symbol, + noise_distribution_to_cost_function, + noise_distribution_to_observable_transformation, + sbml_time_symbol, + smart_subs, + smart_subs_dict, + toposort_symbols, +) +from .logging import get_logger, log_execution_time, set_log_level +from .sbml_utils import SBMLException, _parse_logical_operators +from .splines import AbstractSpline SymbolicFormula = Dict[sp.Symbol, sp.Expr] -default_symbols = { - symbol: {} for symbol in SymbolId -} +default_symbols = {symbol: {} for symbol in SymbolId} ConservationLaw = Dict[str, Union[str, sp.Expr]] @@ -114,10 +122,13 @@ class SbmlImporter: sets behaviour of SBML Formula parsing """ - def __init__(self, - sbml_source: Union[str, Path, sbml.Model], - show_sbml_warnings: bool = False, - from_file: bool = True) -> None: + def __init__( + self, + sbml_source: Union[str, Path, sbml.Model], + show_sbml_warnings: bool = False, + from_file: bool = True, + discard_annotations: bool = False, + ) -> None: """ Create a new Model instance. @@ -132,6 +143,9 @@ def __init__(self, :param from_file: Whether `sbml_source` is a file name (True, default), or an SBML string + + :param discard_annotations: + discard information contained in AMICI SBML annotations (debug). """ if isinstance(sbml_source, sbml.Model): self.sbml_doc: sbml.Document = sbml_source.getSBMLDocument() @@ -159,48 +173,58 @@ def __init__(self, self.species_assignment_rules: SymbolicFormula = {} self.parameter_assignment_rules: SymbolicFormula = {} self.initial_assignments: SymbolicFormula = {} + self.splines = [] self._reset_symbols() # http://sbml.org/Software/libSBML/5.18.0/docs/python-api/classlibsbml_1_1_l3_parser_settings.html#abcfedd34efd3cae2081ba8f42ea43f52 # all defaults except disable unit parsing self.sbml_parser_settings = sbml.L3ParserSettings( - self.sbml, sbml.L3P_PARSE_LOG_AS_LOG10, - sbml.L3P_EXPAND_UNARY_MINUS, sbml.L3P_NO_UNITS, + self.sbml, + sbml.L3P_PARSE_LOG_AS_LOG10, + sbml.L3P_EXPAND_UNARY_MINUS, + sbml.L3P_NO_UNITS, sbml.L3P_AVOGADRO_IS_CSYMBOL, - sbml.L3P_COMPARE_BUILTINS_CASE_INSENSITIVE, None, - sbml.L3P_MODULO_IS_PIECEWISE + sbml.L3P_COMPARE_BUILTINS_CASE_INSENSITIVE, + None, + sbml.L3P_MODULO_IS_PIECEWISE, ) - @log_execution_time('loading SBML', logger) + self._discard_annotations: bool = discard_annotations + + @log_execution_time("loading SBML", logger) def _process_document(self) -> None: """ Validate and simplify document. """ # Ensure we got a valid SBML model, otherwise further processing # might lead to undefined results - log_execution_time('validating SBML', logger)( - self.sbml_doc.validateSBML - )() + log_execution_time("validating SBML", logger)(self.sbml_doc.validateSBML)() _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) # Flatten "comp" model? Do that before any other converters are run - if any(self.sbml_doc.getPlugin(i_plugin).getPackageName() == 'comp' - for i_plugin in range(self.sbml_doc.getNumPlugins())): + if any( + self.sbml_doc.getPlugin(i_plugin).getPackageName() == "comp" + for i_plugin in range(self.sbml_doc.getNumPlugins()) + ): # see libsbml CompFlatteningConverter for options conversion_properties = sbml.ConversionProperties() conversion_properties.addOption("flatten comp", True) conversion_properties.addOption("leave_ports", False) conversion_properties.addOption("performValidation", False) conversion_properties.addOption("abortIfUnflattenable", "none") - if log_execution_time('converting SBML local parameters', logger)( - self.sbml_doc.convert)(conversion_properties) \ - != sbml.LIBSBML_OPERATION_SUCCESS: + if ( + log_execution_time("converting SBML local parameters", logger)( + self.sbml_doc.convert + )(conversion_properties) + != sbml.LIBSBML_OPERATION_SUCCESS + ): raise SBMLException( - 'Required SBML comp extension is currently not supported ' - 'and flattening the model failed.') + "Required SBML comp extension is currently not supported " + "and flattening the model failed." + ) # check the flattened model is still valid - log_execution_time('re-validating SBML', logger)( + log_execution_time("re-validating SBML", logger)( self.sbml_doc.validateSBML )() _check_lib_sbml_errors(self.sbml_doc, self.show_sbml_warnings) @@ -208,15 +232,15 @@ def _process_document(self) -> None: # apply several model simplifications that make our life substantially # easier if self.sbml_doc.getModel().getNumFunctionDefinitions(): - convert_config = sbml.SBMLFunctionDefinitionConverter()\ - .getDefaultProperties() - log_execution_time('converting SBML functions', logger)( + convert_config = ( + sbml.SBMLFunctionDefinitionConverter().getDefaultProperties() + ) + log_execution_time("converting SBML functions", logger)( self.sbml_doc.convert )(convert_config) - convert_config = sbml.SBMLLocalParameterConverter().\ - getDefaultProperties() - log_execution_time('converting SBML local parameters', logger)( + convert_config = sbml.SBMLLocalParameterConverter().getDefaultProperties() + log_execution_time("converting SBML local parameters", logger)( self.sbml_doc.convert )(convert_config) @@ -236,26 +260,26 @@ def _reset_symbols(self) -> None: self._local_symbols = {} def sbml2amici( - self, - model_name: str, - output_dir: Union[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, - 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, - cache_simplify: bool = False, - log_as_log10: bool = True, - generate_sensitivity_code: bool = True, + self, + model_name: str, + output_dir: Union[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, + 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, + cache_simplify: bool = False, + log_as_log10: bool = True, + generate_sensitivity_code: bool = True, ) -> None: """ Generate and compile AMICI C++ files for the model provided to the @@ -276,7 +300,7 @@ def sbml2amici( name of the model/model directory :param output_dir: - see :meth:`amici.ode_export.ODEExporter.set_paths` + see :meth:`amici.de_export.ODEExporter.set_paths` :param observables: dictionary( observableId:{'name':observableName @@ -323,7 +347,7 @@ def sbml2amici( python extension :param allow_reinit_fixpar_initcond: - see :class:`amici.ode_export.ODEExporter` + see :class:`amici.de_export.ODEExporter` :param compile: If ``True``, compile the generated Python package, @@ -386,37 +410,38 @@ def sbml2amici( assume_pow_positivity=assume_pow_positivity, compiler=compiler, allow_reinit_fixpar_initcond=allow_reinit_fixpar_initcond, - generate_sensitivity_code=generate_sensitivity_code + generate_sensitivity_code=generate_sensitivity_code, ) exporter.generate_model_code() if compile: if not has_clibs: - warnings.warn('AMICI C++ extensions have not been built. ' - 'Generated model code, but unable to compile.') + warnings.warn( + "AMICI C++ extensions have not been built. " + "Generated model code, but unable to compile." + ) exporter.compile_model() def _build_ode_model( - self, - 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, - compute_conservation_laws: bool = True, - simplify: Optional[Callable] = _default_simplify, - cache_simplify: bool = False, - log_as_log10: bool = True, + self, + 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, + compute_conservation_laws: bool = True, + simplify: Optional[Callable] = _default_simplify, + cache_simplify: bool = False, + log_as_log10: bool = True, ) -> DEModel: """Generate an ODEModel from this SBML model. See :py:func:`sbml2amici` for parameters. """ - constant_parameters = list(constant_parameters) \ - if constant_parameters else [] + constant_parameters = list(constant_parameters) if constant_parameters else [] if sigmas is None: sigmas = {} @@ -432,32 +457,29 @@ def _build_ode_model( self._reset_symbols() self.sbml_parser_settings.setParseLog( - sbml.L3P_PARSE_LOG_AS_LOG10 if log_as_log10 else - sbml.L3P_PARSE_LOG_AS_LN + sbml.L3P_PARSE_LOG_AS_LOG10 if log_as_log10 else sbml.L3P_PARSE_LOG_AS_LN ) self._process_sbml(constant_parameters) - if self.symbols.get(SymbolId.EVENT, False) \ - or any(x['value'].has(sp.Heaviside, sp.Piecewise) - for x in self.symbols[SymbolId.EXPRESSION].values())\ - or self.flux_vector.has(sp.Heaviside, sp.Piecewise): + if ( + self.symbols.get(SymbolId.EVENT, False) + or any( + x["value"].has(sp.Heaviside, sp.Piecewise) + for x in self.symbols[SymbolId.EXPRESSION].values() + ) + or self.flux_vector.has(sp.Heaviside, sp.Piecewise) + ): if compute_conservation_laws: logger.warning( - 'Conservation laws are currently not supported for models ' - 'with events, piecewise or Heaviside functions, ' - 'and will be turned off.' + "Conservation laws are currently not supported for models " + "with events, piecewise or Heaviside functions, " + "and will be turned off." ) compute_conservation_laws = False - self._process_observables( - observables, - sigmas, - noise_distributions - ) + self._process_observables(observables, sigmas, noise_distributions) self._process_event_observables( - event_observables, - event_sigmas, - event_noise_distributions + event_observables, event_sigmas, event_noise_distributions ) self._replace_compartments_with_volumes() @@ -469,12 +491,10 @@ def _build_ode_model( simplify=simplify, cache_simplify=cache_simplify, ) - ode_model.import_from_sbml_importer( - self, compute_cls=compute_conservation_laws - ) + ode_model.import_from_sbml_importer(self, compute_cls=compute_conservation_laws) return ode_model - @log_execution_time('importing SBML', logger) + @log_execution_time("importing SBML", logger) def _process_sbml(self, constant_parameters: List[str] = None) -> None: """ Read parameters, species, reactions, and so on from SBML model @@ -482,6 +502,8 @@ def _process_sbml(self, constant_parameters: List[str] = None) -> None: :param constant_parameters: SBML Ids identifying constant parameters """ + if not self._discard_annotations: + self._process_annotations() self.check_support() self._gather_locals() self._process_parameters(constant_parameters) @@ -501,22 +523,25 @@ def check_support(self) -> None: """ # Check for required but unsupported SBML extensions - if self.sbml_doc.getLevel() != 3 \ - and hasattr(self.sbml, 'all_elements_from_plugins') \ - and self.sbml.all_elements_from_plugins.getSize(): - raise SBMLException('SBML extensions are currently not supported!') + if ( + self.sbml_doc.getLevel() != 3 + and hasattr(self.sbml, "all_elements_from_plugins") + and self.sbml.all_elements_from_plugins.getSize() + ): + raise SBMLException("SBML extensions are currently not supported!") if self.sbml_doc.getLevel() == 3: # the "required" attribute is only available in SBML Level 3 for i_plugin in range(self.sbml.getNumPlugins()): plugin = self.sbml.getPlugin(i_plugin) - if self.sbml_doc.getPkgRequired(plugin.getPackageName()) \ - is False: + if self.sbml_doc.getPkgRequired(plugin.getPackageName()) is False: # if not "required", this has no impact on model # simulation, and we can safely ignore it - if plugin.getPackageName() == "fbc" \ - and plugin.getListOfAllElements(): + if ( + plugin.getPackageName() == "fbc" + and plugin.getListOfAllElements() + ): # fbc is labeled not-required, but in fact it is. # we don't care about the extra attributes of core # elements, such as fbc:chemicalFormula, but we can't @@ -525,9 +550,7 @@ def check_support(self) -> None: raise SBMLException( "The following fbc extension elements are " "currently not supported: " - + ', '.join( - list(map(str, plugin.getListOfAllElements())) - ) + + ", ".join(list(map(str, plugin.getListOfAllElements()))) ) continue @@ -536,18 +559,25 @@ def check_support(self) -> None: # ignore the enabled package if plugin.getListOfAllElements(): raise SBMLException( - f'Required SBML extension {plugin.getPackageName()} ' - f'is currently not supported!') + f"Required SBML extension {plugin.getPackageName()} " + f"is currently not supported!" + ) - if any(rule.isRate() and not isinstance( - self.sbml.getElementBySId(rule.getVariable()), - (sbml.Compartment, sbml.Species, sbml.Parameter) - ) for rule in self.sbml.getListOfRules()): - raise SBMLException('Rate rules are only supported for ' - 'species, compartments, and parameters.') + if any( + rule.isRate() + and not isinstance( + self.sbml.getElementBySId(rule.getVariable()), + (sbml.Compartment, sbml.Species, sbml.Parameter), + ) + for rule in self.sbml.getListOfRules() + ): + raise SBMLException( + "Rate rules are only supported for " + "species, compartments, and parameters." + ) if any(r.getFast() for r in self.sbml.getListOfReactions()): - raise SBMLException('Fast reactions are currently not supported!') + raise SBMLException("Fast reactions are currently not supported!") # Check events for unsupported functionality self.check_event_support() @@ -576,31 +606,38 @@ def check_event_support(self) -> None: # `TypeError` would be raised in the above `float(...)` # if the delay is not a fixed time except (TypeError, ValueError): - raise SBMLException('Events with execution delays are ' - 'currently not supported in AMICI.') + raise SBMLException( + "Events with execution delays are " + "currently not supported in AMICI." + ) # Check for priorities if event.getPriority() is not None: - raise SBMLException(f'Event {event_id} has a priority ' - 'specified. This is currently not ' - 'supported in AMICI.') + raise SBMLException( + f"Event {event_id} has a priority " + "specified. This is currently not " + "supported in AMICI." + ) # check trigger trigger_sbml = event.getTrigger() if trigger_sbml is None: - logger.warning(f'Event {event_id} trigger has no trigger, ' - 'so will be skipped.') + logger.warning( + f"Event {event_id} trigger has no trigger, " "so will be skipped." + ) continue if trigger_sbml.getMath() is None: - logger.warning(f'Event {event_id} trigger has no trigger ' - 'expression, so a dummy trigger will be set.') + logger.warning( + f"Event {event_id} trigger has no trigger " + "expression, so a dummy trigger will be set." + ) if not trigger_sbml.getPersistent(): raise SBMLException( - f'Event {event_id} has a non-persistent trigger.' - 'This is currently not supported in AMICI.' + f"Event {event_id} has a non-persistent trigger." + "This is currently not supported in AMICI." ) - @log_execution_time('gathering local SBML symbols', logger) + @log_execution_time("gathering local SBML symbols", logger) def _gather_locals(self) -> None: """ Populate self.local_symbols with all model entities. @@ -620,21 +657,23 @@ def _gather_base_locals(self): special_symbols_and_funs = { # oo is sympy infinity - 'INF': sp.oo, - 'NaN': sp.nan, - 'rem': sp.Mod, - 'time': symbol_with_assumptions('time'), + "INF": sp.oo, + "NaN": sp.nan, + "rem": sp.Mod, + "time": symbol_with_assumptions("time"), # SBML L3 explicitly defines this value, which is not equal # to the most recent SI definition. - 'avogadro': sp.Float(6.02214179e23), - 'exponentiale': sp.E, + "avogadro": sp.Float(6.02214179e23), + "exponentiale": sp.E, } for s, v in special_symbols_and_funs.items(): self.add_local_symbol(s, v) - for c in itt.chain(self.sbml.getListOfSpecies(), - self.sbml.getListOfParameters(), - self.sbml.getListOfCompartments()): + for c in itt.chain( + self.sbml.getListOfSpecies(), + self.sbml.getListOfParameters(), + self.sbml.getListOfCompartments(), + ): if not c.isSetId(): continue @@ -643,8 +682,7 @@ def _gather_base_locals(self): for x_ref in _get_list_of_species_references(self.sbml): if not x_ref.isSetId(): continue - if x_ref.isSetStoichiometry() and not \ - self.is_assignment_rule_target(x_ref): + if x_ref.isSetStoichiometry() and not self.is_assignment_rule_target(x_ref): value = sp.Float(x_ref.getStoichiometry()) else: value = _get_identifier_symbol(x_ref) @@ -655,18 +693,6 @@ def _gather_base_locals(self): self.add_local_symbol(x_ref.getId(), value) - for r in self.sbml.getListOfReactions(): - for e in itt.chain(r.getListOfReactants(), r.getListOfProducts()): - if isinstance(e, sbml.SpeciesReference): - continue - - if not (e.isSetId() and e.isSetStoichiometry()) or \ - self.is_assignment_rule_target(e): - continue - - self.add_local_symbol(e.getId(), - sp.Float(e.getStoichiometry())) - def _gather_dependent_locals(self): """ Populate self.local_symbols with symbol definitions that may depend on @@ -676,8 +702,7 @@ def _gather_dependent_locals(self): if not r.isSetId(): continue self.add_local_symbol( - r.getId(), - self._sympy_from_sbml_math(r.getKineticLaw() or sp.Float(0)) + r.getId(), self._sympy_from_sbml_math(r.getKineticLaw() or sp.Float(0)) ) def add_local_symbol(self, key: str, value: sp.Expr): @@ -694,22 +719,22 @@ def add_local_symbol(self, key: str, value: sp.Expr): """ if key in self._local_symbols.keys(): raise SBMLException( - f'AMICI tried to add a local symbol {key} with value {value}, ' - f'but {key} was already instantiated with ' - f'{self._local_symbols[key]}. This means that there ' - f'are multiple SBML elements with SId {key}, which is ' - f'invalid SBML. This can be fixed by renaming ' - f'the elements with SId {key}.' + f"AMICI tried to add a local symbol {key} with value {value}, " + f"but {key} was already instantiated with " + f"{self._local_symbols[key]}. This means that there " + f"are multiple SBML elements with SId {key}, which is " + f"invalid SBML. This can be fixed by renaming " + f"the elements with SId {key}." ) - if key in {'True', 'False', 'true', 'false', 'pi'}: + if key in {"True", "False", "true", "false", "pi"}: raise SBMLException( - f'AMICI tried to add a local symbol {key} with value {value}, ' - f'but {key} is a reserved symbol in AMICI. This can be fixed ' - f'by renaming the element with SId {key}.' + f"AMICI tried to add a local symbol {key} with value {value}, " + f"but {key} is a reserved symbol in AMICI. This can be fixed " + f"by renaming the element with SId {key}." ) self._local_symbols[key] = value - @log_execution_time('processing SBML compartments', logger) + @log_execution_time("processing SBML compartments", logger) def _process_compartments(self) -> None: """ Get compartment information, stoichiometric matrix and fluxes from @@ -729,15 +754,13 @@ def _process_compartments(self) -> None: self.compartments[_get_identifier_symbol(comp)] = init - @log_execution_time('processing SBML species', logger) + @log_execution_time("processing SBML species", logger) def _process_species(self) -> None: """ Get species information from SBML model. """ if self.sbml.isSetConversionFactor(): - conversion_factor = symbol_with_assumptions( - self.sbml.getConversionFactor() - ) + conversion_factor = symbol_with_assumptions(self.sbml.getConversionFactor()) else: conversion_factor = 1 @@ -745,23 +768,21 @@ def _process_species(self) -> None: if self.is_assignment_rule_target(s): continue self.symbols[SymbolId.SPECIES][_get_identifier_symbol(s)] = { - 'name': s.getName() if s.isSetName() else s.getId(), - 'compartment': _get_species_compartment_symbol(s), - 'constant': s.getConstant() or s.getBoundaryCondition(), - 'amount': s.getHasOnlySubstanceUnits(), - 'conversion_factor': symbol_with_assumptions( - s.getConversionFactor() - ) + "name": s.getName() if s.isSetName() else s.getId(), + "compartment": _get_species_compartment_symbol(s), + "constant": s.getConstant() or s.getBoundaryCondition(), + "amount": s.getHasOnlySubstanceUnits(), + "conversion_factor": symbol_with_assumptions(s.getConversionFactor()) if s.isSetConversionFactor() else conversion_factor, - 'index': len(self.symbols[SymbolId.SPECIES]), + "index": len(self.symbols[SymbolId.SPECIES]), } self._convert_event_assignment_parameter_targets_to_species() self._process_species_initial() self._process_rate_rules() - @log_execution_time('processing SBML species initials', logger) + @log_execution_time("processing SBML species initials", logger) def _process_species_initial(self): """ Extract initial values and initial assignments from species @@ -776,23 +797,18 @@ def _process_species_initial(self): # targets to have InitialAssignments. species = self.symbols[SymbolId.SPECIES].get(species_id, None) - ia_initial = self._get_element_initial_assignment( - species_variable.getId() - ) + ia_initial = self._get_element_initial_assignment(species_variable.getId()) if ia_initial is not None: initial = ia_initial if species: - species['init'] = initial + species["init"] = initial # don't assign this since they need to stay in order - sorted_species = toposort_symbols(self.symbols[SymbolId.SPECIES], - 'init') + sorted_species = toposort_symbols(self.symbols[SymbolId.SPECIES], "init") for species in self.symbols[SymbolId.SPECIES].values(): - species['init'] = smart_subs_dict(species['init'], - sorted_species, - 'init') + species["init"] = smart_subs_dict(species["init"], sorted_species, "init") - @log_execution_time('processing SBML rate rules', logger) + @log_execution_time("processing SBML rate rules", logger) def _process_rate_rules(self): """ Process rate rules for species, compartments and parameters. @@ -818,7 +834,7 @@ def _process_rate_rules(self): # implemented as species). ia_init = self._get_element_initial_assignment(rule.getVariable()) if variable in self.symbols[SymbolId.SPECIES]: - init = self.symbols[SymbolId.SPECIES][variable]['init'] + init = self.symbols[SymbolId.SPECIES][variable]["init"] name = None if variable in self.compartments: @@ -828,9 +844,9 @@ def _process_rate_rules(self): elif variable in self.symbols[SymbolId.PARAMETER]: init = self._sympy_from_sbml_math( - self.symbols[SymbolId.PARAMETER][variable]['value'], + self.symbols[SymbolId.PARAMETER][variable]["value"], ) - name = self.symbols[SymbolId.PARAMETER][variable]['name'] + name = self.symbols[SymbolId.PARAMETER][variable]["name"] del self.symbols[SymbolId.PARAMETER][variable] # parameter with initial assignment, cannot use @@ -844,11 +860,11 @@ def _process_rate_rules(self): self.add_d_dt(formula, variable, init, name) def add_d_dt( - self, - d_dt: sp.Expr, - variable: sp.Symbol, - variable0: Union[float, sp.Expr], - name: str, + self, + d_dt: sp.Expr, + variable: sp.Symbol, + variable0: Union[float, sp.Expr], + name: str, ) -> None: """ Creates or modifies species, to implement rate rules for @@ -869,30 +885,51 @@ def add_d_dt( """ if variable in self.symbols[SymbolId.SPECIES]: # only update dt if species was already generated - self.symbols[SymbolId.SPECIES][variable]['dt'] = d_dt + self.symbols[SymbolId.SPECIES][variable]["dt"] = d_dt else: # update initial values for species_id, species in self.symbols[SymbolId.SPECIES].items(): - variable0 = smart_subs(variable0, species_id, species['init']) + variable0 = smart_subs(variable0, species_id, species["init"]) for species in self.symbols[SymbolId.SPECIES].values(): - species['init'] = smart_subs(species['init'], - variable, variable0) + species["init"] = smart_subs(species["init"], variable, variable0) # add compartment/parameter species self.symbols[SymbolId.SPECIES][variable] = { - 'name': name, - 'init': variable0, - 'amount': False, - 'conversion_factor': 1.0, - 'constant': False, - 'index': len(self.symbols[SymbolId.SPECIES]), - 'dt': d_dt, + "name": name, + "init": variable0, + "amount": False, + "conversion_factor": 1.0, + "constant": False, + "index": len(self.symbols[SymbolId.SPECIES]), + "dt": d_dt, } - @log_execution_time('processing SBML parameters', logger) - def _process_parameters(self, - constant_parameters: List[str] = None) -> None: + @log_execution_time("processing SBML annotations", logger) + def _process_annotations(self) -> None: + """ + Process annotations that make modifications to the + SBML model and thus have to be run before everything else + """ + # Remove all parameters (and corresponding rules) + # for which amici:discard is set + parameter_ids_to_remove = [] + for p in self.sbml.getListOfParameters(): + annotation = p.getAnnotationString() + assert isinstance(annotation, str) + if len(annotation) != 0: + annotation = ET.fromstring(annotation) + for child in annotation: + if child.tag == f"{{{annotation_namespace}}}discard": + parameter_ids_to_remove.append(p.getIdAttribute()) + for parameter_id in parameter_ids_to_remove: + # Remove corresponding rules + self.sbml.removeRuleByVariable(parameter_id) + # Remove parameter + self.sbml.removeParameter(parameter_id) + + @log_execution_time("processing SBML parameters", logger) + def _process_parameters(self, constant_parameters: List[str] = None) -> None: """ Get parameter information from SBML model. @@ -906,8 +943,10 @@ def _process_parameters(self, # Ensure specified constant parameters exist in the model for parameter in constant_parameters: if not self.sbml.getParameter(parameter): - raise KeyError('Cannot make %s a constant parameter: ' - 'Parameter does not exist.' % parameter) + raise KeyError( + "Cannot make %s a constant parameter: " + "Parameter does not exist." % parameter + ) fixed_parameters = [ parameter @@ -915,38 +954,42 @@ def _process_parameters(self, if parameter.getId() in constant_parameters ] for parameter in fixed_parameters: - if self._get_element_initial_assignment(parameter.getId()) is not \ - None or self.is_assignment_rule_target(parameter) or \ - self.is_rate_rule_target(parameter): + if ( + self._get_element_initial_assignment(parameter.getId()) is not None + or self.is_assignment_rule_target(parameter) + or self.is_rate_rule_target(parameter) + ): raise SBMLException( - f'Cannot turn parameter {parameter.getId()} into a ' - 'constant/fixed parameter since it either has an ' - 'initial assignment or is the target of an assignment or ' - 'rate rule.' + f"Cannot turn parameter {parameter.getId()} into a " + "constant/fixed parameter since it either has an " + "initial assignment or is the target of an assignment or " + "rate rule." ) parameters = [ - parameter for parameter - in self.sbml.getListOfParameters() + parameter + for parameter in self.sbml.getListOfParameters() if parameter.getId() not in constant_parameters and self._get_element_initial_assignment(parameter.getId()) is None and not self.is_assignment_rule_target(parameter) ] loop_settings = { - SymbolId.PARAMETER: {'var': parameters, 'name': 'parameter'}, - SymbolId.FIXED_PARAMETER: {'var': fixed_parameters, - 'name': 'fixed_parameter'} + SymbolId.PARAMETER: {"var": parameters, "name": "parameter"}, + SymbolId.FIXED_PARAMETER: { + "var": fixed_parameters, + "name": "fixed_parameter", + }, } for partype, settings in loop_settings.items(): - for par in settings['var']: + for par in settings["var"]: self.symbols[partype][_get_identifier_symbol(par)] = { - 'name': par.getName() if par.isSetName() else par.getId(), - 'value': par.getValue() + "name": par.getName() if par.isSetName() else par.getId(), + "value": par.getValue(), } - @log_execution_time('processing SBML reactions', logger) + @log_execution_time("processing SBML reactions", logger) def _process_reactions(self): """ Get reactions from SBML model. @@ -964,39 +1007,39 @@ def _process_reactions(self): # level 3 version 2 the ID attribute was not mandatory and may be # unset) self.flux_ids = [ - f"flux_{reaction.getId()}" if reaction.isSetId() + f"flux_{reaction.getId()}" + if reaction.isSetId() else f"flux_r{reaction_idx}" for reaction_idx, reaction in enumerate(reactions) - ] or ['flux_r0'] + ] or ["flux_r0"] reaction_ids = [ - reaction.getId() for reaction in reactions - if reaction.isSetId() + reaction.getId() for reaction in reactions if reaction.isSetId() ] for reaction_index, reaction in enumerate(reactions): - for element_list, sign in [(reaction.getListOfReactants(), -1), - (reaction.getListOfProducts(), 1)]: + for element_list, sign in [ + (reaction.getListOfReactants(), -1), + (reaction.getListOfProducts(), 1), + ]: for element in element_list: - stoichiometry = self._get_element_stoichiometry( - element - ) + stoichiometry = self._get_element_stoichiometry(element) sbml_species = self.sbml.getSpecies(element.getSpecies()) if self.is_assignment_rule_target(sbml_species): continue species_id = _get_identifier_symbol(sbml_species) species = self.symbols[SymbolId.SPECIES][species_id] - if species['constant']: + if species["constant"]: continue # Division by species compartment size (to find the # rate of change in species concentration) now occurs # in the `dx_dt` method in "de_export.py", which also # accounts for possibly variable compartments. - self.stoichiometric_matrix[species['index'], - reaction_index] += \ - sign * stoichiometry * species['conversion_factor'] + self.stoichiometric_matrix[species["index"], reaction_index] += ( + sign * stoichiometry * species["conversion_factor"] + ) if reaction.isSetId(): sym_math = self._local_symbols[reaction.getId()] else: @@ -1010,11 +1053,11 @@ def _process_reactions(self): for symbol in self.flux_vector[reaction_index].free_symbols ): raise SBMLException( - 'Kinetic laws involving reaction ids are currently' - ' not supported!' + "Kinetic laws involving reaction ids are currently" + " not supported!" ) - @log_execution_time('processing SBML rules', logger) + @log_execution_time("processing SBML rules", logger) def _process_rules(self) -> None: """ Process Rules defined in the SBML model. @@ -1029,22 +1072,22 @@ def _process_rules(self) -> None: # not interested in implementing level 2 boundary condition # shenanigans, see test 01787 in the sbml testsuite raise SBMLException( - 'Algebraic rules are only supported in SBML L3+' + "Algebraic rules are only supported in SBML L3+" ) self._process_rule_algebraic(rule) else: self._process_rule_assignment(rule) self.symbols[SymbolId.EXPRESSION] = toposort_symbols( - self.symbols[SymbolId.EXPRESSION], 'value' + self.symbols[SymbolId.EXPRESSION], "value" ) # expressions must not occur in definition of x0 for species in self.symbols[SymbolId.SPECIES].values(): - species['init'] = self._make_initial( - smart_subs_dict(species['init'], - self.symbols[SymbolId.EXPRESSION], - 'value') + species["init"] = self._make_initial( + smart_subs_dict( + species["init"], self.symbols[SymbolId.EXPRESSION], "value" + ) ) def _process_rule_algebraic(self, rule: sbml.AlgebraicRule): @@ -1067,22 +1110,25 @@ def _process_rule_algebraic(self, rule: sbml.AlgebraicRule): continue # and there must also not be a rate rule or assignment # rule for it - if self.is_assignment_rule_target(sbml_var) or \ - self.is_rate_rule_target(sbml_var): + if self.is_assignment_rule_target(sbml_var) or self.is_rate_rule_target( + sbml_var + ): continue # Furthermore, if the entity is a Species object, its value # must not be determined by reactions, which means that it # must either have the attribute boundaryCondition=“false” # or else not be involved in any reaction at all. is_species = isinstance(sbml_var, sbml.Species) - is_boundary_condition = is_species and \ - sbml_var.isSetBoundaryCondition() and \ - sbml_var.getBoundaryCondition() - is_involved_in_reaction = is_species and \ - not smart_is_zero_matrix(self.stoichiometric_matrix[ - list(self.symbols[SymbolId.SPECIES].keys()).index(symbol), - : - ]) + is_boundary_condition = ( + is_species + and sbml_var.isSetBoundaryCondition() + and sbml_var.getBoundaryCondition() + ) + is_involved_in_reaction = is_species and not smart_is_zero_matrix( + self.stoichiometric_matrix[ + list(self.symbols[SymbolId.SPECIES].keys()).index(symbol), : + ] + ) if is_species and not is_boundary_condition and is_involved_in_reaction: continue free_variables.add(symbol) @@ -1092,20 +1138,18 @@ def _process_rule_algebraic(self, rule: sbml.AlgebraicRule): assert len(free_variables) >= 1 self.symbols[SymbolId.ALGEBRAIC_EQUATION][ - f'ae{len(self.symbols[SymbolId.ALGEBRAIC_EQUATION])}' - ] = { - 'value': formula - } + f"ae{len(self.symbols[SymbolId.ALGEBRAIC_EQUATION])}" + ] = {"value": formula} # remove the symbol from the original definition and add to # algebraic symbols (if not already done) for var in free_variables: if var in self.symbols[SymbolId.FIXED_PARAMETER]: raise SBMLException( - 'There are algebraic rules that specify the ' - f'value of {var}, which is also marked as ' - 'fixed parameter. This is currently not supported! ' - f'If {var} is supposed to be a fixed parameter, ' - 'set its SBML attribute `constant` to True.' + "There are algebraic rules that specify the " + f"value of {var}, which is also marked as " + "fixed parameter. This is currently not supported! " + f"If {var} is supposed to be a fixed parameter, " + "set its SBML attribute `constant` to True." ) if var in self.symbols[SymbolId.ALGEBRAIC_STATE]: @@ -1113,55 +1157,79 @@ def _process_rule_algebraic(self, rule: sbml.AlgebraicRule): if var in self.compartments: init = self.compartments[var] symbol = { - 'name': str(var), - 'value': init, + "name": str(var), + "value": init, } - symbol_id = 'compartment' + symbol_id = "compartment" var_ix = np.nan del self.compartments[var] else: symbol_id, source_symbols = next( - ((symbol_id, self.symbols[symbol_id]) - for symbol_id in (SymbolId.PARAMETER, SymbolId.SPECIES) - if var in self.symbols[symbol_id]), + ( + (symbol_id, self.symbols[symbol_id]) + for symbol_id in (SymbolId.PARAMETER, SymbolId.SPECIES) + if var in self.symbols[symbol_id] + ), ) var_ix = list(source_symbols.keys()).index(var) symbol = source_symbols.pop(var) # update symbol and adapt stoichiometric matrix if symbol_id != SymbolId.SPECIES: # parameters have numeric values so we can use Float here - symbol['init'] = sp.Float(symbol.pop('value')) + symbol["init"] = sp.Float(symbol.pop("value")) # if not a species, add a zeros row to the stoichiometric # matrix - if (isinstance(symbol['init'], float) - and np.isnan(symbol['init'])) or \ - (isinstance(symbol['init'], sp.Number) - and symbol['init'] == sp.nan): + if (isinstance(symbol["init"], float) and np.isnan(symbol["init"])) or ( + isinstance(symbol["init"], sp.Number) and symbol["init"] == sp.nan + ): # placeholder, needs to be determined in IC calculation - symbol['init'] = sp.Float(0.0) + symbol["init"] = sp.Float(0.0) self.stoichiometric_matrix = self.stoichiometric_matrix.row_insert( self.stoichiometric_matrix.shape[0], - sp.SparseMatrix([ - [0] * self.stoichiometric_matrix.shape[1] - ]) + sp.SparseMatrix([[0] * self.stoichiometric_matrix.shape[1]]), ) elif var_ix != self.stoichiometric_matrix.shape[0] - 1: # if not the last col, move it to the end # as we reorder state variables - state_ordering = list(range( - len(self.symbols[SymbolId.SPECIES]) + - len(self.symbols[SymbolId.ALGEBRAIC_STATE]) + - 1 - )) + state_ordering = list( + range( + len(self.symbols[SymbolId.SPECIES]) + + len(self.symbols[SymbolId.ALGEBRAIC_STATE]) + + 1 + ) + ) state_ordering.append(state_ordering.pop(var_ix)) - self.stoichiometric_matrix = \ - self.stoichiometric_matrix[state_ordering, :] + self.stoichiometric_matrix = self.stoichiometric_matrix[ + state_ordering, : + ] self.symbols[SymbolId.ALGEBRAIC_STATE][var] = symbol def _process_rule_assignment(self, rule: sbml.AssignmentRule): sbml_var = self.sbml.getElementBySId(rule.getVariable()) sym_id = symbol_with_assumptions(rule.getVariable()) + + # Check whether this rule is a spline rule. + if not self._discard_annotations: + if rule.getTypeCode() == sbml.SBML_ASSIGNMENT_RULE: + annotation = AbstractSpline.get_annotation(rule) + if annotation is not None: + spline = AbstractSpline.from_annotation( + sym_id, + annotation, + locals_=self._local_symbols, + ) + if ( + spline.evaluate_at != amici_time_symbol + and spline.evaluate_at != sbml_time_symbol + ): + raise NotImplementedError( + "AMICI at the moment does not support splines " + "whose evaluation point is not the model time." + ) + self.splines.append(spline) + return + formula = self._sympy_from_sbml_math(rule) if formula is None: return @@ -1177,18 +1245,14 @@ def _process_rule_assignment(self, rule: sbml.AssignmentRule): self.parameter_assignment_rules[sym_id] = formula self.symbols[SymbolId.EXPRESSION][sym_id] = { - 'name': str(sym_id), - 'value': formula + "name": str(sym_id), + "value": formula, } def _process_time(self) -> None: """ Convert time_symbol into cpp variable. """ - sbml_time_symbol = symbol_with_assumptions('time') - amici_time_symbol = symbol_with_assumptions('t') - self.amici_time_symbol = amici_time_symbol - self._replace_in_all_expressions(sbml_time_symbol, amici_time_symbol) def _convert_event_assignment_parameter_targets_to_species(self): @@ -1198,17 +1262,16 @@ def _convert_event_assignment_parameter_targets_to_species(self): This is for the convenience of only implementing event assignments for "species". """ - parameter_targets = \ - _collect_event_assignment_parameter_targets(self.sbml) + parameter_targets = _collect_event_assignment_parameter_targets(self.sbml) for parameter_target in parameter_targets: # Parameter rate rules already exist as species. if parameter_target in self.symbols[SymbolId.SPECIES]: continue if parameter_target in self.parameter_assignment_rules: raise SBMLException( - 'AMICI does not currently support models with SBML events ' - 'that affect parameters that are also the target of ' - 'assignment rules.' + "AMICI does not currently support models with SBML events " + "that affect parameters that are also the target of " + "assignment rules." ) parameter_def = None for symbol_id in {SymbolId.PARAMETER, SymbolId.FIXED_PARAMETER}: @@ -1217,36 +1280,33 @@ def _convert_event_assignment_parameter_targets_to_species(self): # `symbol_id` dictionaries. if parameter_def is not None: raise AssertionError( - 'Unexpected error. The parameter target of an ' - 'event assignment was processed twice.' + "Unexpected error. The parameter target of an " + "event assignment was processed twice." ) - parameter_def = \ - self.symbols[symbol_id].pop(parameter_target) + parameter_def = self.symbols[symbol_id].pop(parameter_target) if parameter_def is None: # this happens for parameters that have initial assignments # or are assignment rule targets par = self.sbml.getElementBySId(str(parameter_target)) - ia_init = self._get_element_initial_assignment( - par.getId() - ) + ia_init = self._get_element_initial_assignment(par.getId()) parameter_def = { - 'name': par.getName() if par.isSetName() else par.getId(), - 'value': par.getValue() if ia_init is None else ia_init + "name": par.getName() if par.isSetName() else par.getId(), + "value": par.getValue() if ia_init is None else ia_init, } # Fixed parameters are added as species such that they can be # targets of events. self.symbols[SymbolId.SPECIES][parameter_target] = { - 'name': parameter_def['name'], - 'init': sp.Float(parameter_def['value']), + "name": parameter_def["name"], + "init": sp.Float(parameter_def["value"]), # 'compartment': None, # can ignore for amounts - 'constant': False, - 'amount': True, + "constant": False, + "amount": True, # 'conversion_factor': 1.0, # can be ignored - 'index': len(self.symbols[SymbolId.SPECIES]), - 'dt': sp.Float(0), + "index": len(self.symbols[SymbolId.SPECIES]), + "dt": sp.Float(0), } - @log_execution_time('processing SBML events', logger) + @log_execution_time("processing SBML events", logger) def _process_events(self) -> None: """Process SBML events.""" events = self.sbml.getListOfEvents() @@ -1256,7 +1316,7 @@ def get_empty_bolus_value() -> sp.Float: Used in the event update vector for species that are not affected by the event. """ - return sp.Symbol('AMICI_EMTPY_BOLUS') + return sp.Symbol("AMICI_EMTPY_BOLUS") # Used to update species concentrations when an event affects a # compartment. @@ -1266,20 +1326,21 @@ def get_empty_bolus_value() -> sp.Float: } for species, species_def in self.symbols[SymbolId.SPECIES].items(): if ( - # Species is a concentration - not species_def.get('amount', True) and - # Species has a compartment - 'compartment' in species_def + # Species is a concentration + not species_def.get("amount", True) + and + # Species has a compartment + "compartment" in species_def ): - concentration_species_by_compartment[ - species_def['compartment'] - ].append(species) + concentration_species_by_compartment[species_def["compartment"]].append( + species + ) for ievent, event in enumerate(events): # get the event id (which is optional unfortunately) event_id = event.getId() - if event_id is None or event_id == '': - event_id = f'event_{ievent}' + if event_id is None or event_id == "": + event_id = f"event_{ievent}" event_sym = sp.Symbol(event_id) # get and parse the trigger function @@ -1296,8 +1357,7 @@ def get_empty_bolus_value() -> sp.Float: event_assignments = event.getListOfEventAssignments() compartment_event_assignments = set() for event_assignment in event_assignments: - variable_sym = \ - symbol_with_assumptions(event_assignment.getVariable()) + variable_sym = symbol_with_assumptions(event_assignment.getVariable()) if event_assignment.getMath() is None: # Ignore event assignments with no change in value. continue @@ -1308,10 +1368,10 @@ def get_empty_bolus_value() -> sp.Float: bolus[index] = formula except ValueError: raise SBMLException( - '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.' + "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." ) try: # Try working with the formula now to detect errors @@ -1319,15 +1379,14 @@ def get_empty_bolus_value() -> sp.Float: _ = formula - variable_sym except TypeError: raise SBMLException( - 'Could not process event assignment for ' - f'{str(variable_sym)}. AMICI only allows symbolic ' - 'expressions as event assignments.' + "Could not process event assignment for " + f"{str(variable_sym)}. AMICI only allows symbolic " + "expressions as event assignments." ) if variable_sym in concentration_species_by_compartment: compartment_event_assignments.add(variable_sym) - for comp, assignment in \ - self.compartment_assignment_rules.items(): + for comp, assignment in self.compartment_assignment_rules.items(): if variable_sym not in assignment.free_symbols: continue compartment_event_assignments.add(comp) @@ -1336,13 +1395,13 @@ def get_empty_bolus_value() -> sp.Float: # in compartments that were affected by the event assignments. for compartment_sym in compartment_event_assignments: for species_sym in concentration_species_by_compartment[ - compartment_sym + compartment_sym ]: # If the species was not affected by an event assignment # then the old value should be updated. if ( - bolus[state_vector.index(species_sym)] - == get_empty_bolus_value() + bolus[state_vector.index(species_sym)] + == get_empty_bolus_value() ): species_value = species_sym # else the species was affected by an event assignment, @@ -1360,11 +1419,11 @@ def get_empty_bolus_value() -> sp.Float: for index in range(len(bolus)): if bolus[index] != get_empty_bolus_value(): bolus[index] -= state_vector[index] - bolus[index] = bolus[index].subs(get_empty_bolus_value(), - sp.Float(0.0)) + bolus[index] = bolus[index].subs(get_empty_bolus_value(), sp.Float(0.0)) - initial_value = trigger_sbml.getInitialValue() \ - if trigger_sbml is not None else True + initial_value = ( + trigger_sbml.getInitialValue() if trigger_sbml is not None else True + ) if self.symbols[SymbolId.ALGEBRAIC_EQUATION] and not initial_value: # in principle this could be implemented, requires running # IDACalcIc (in solver->setup) before check event initialization @@ -1373,23 +1432,23 @@ def get_empty_bolus_value() -> sp.Float: # (it might not, but this could be checked when someone actually # needs the feature). raise SBMLException( - 'Events with initial values are not supported in models with' - ' algebraic rules.' + "Events with initial values are not supported in models with" + " algebraic rules." ) self.symbols[SymbolId.EVENT][event_sym] = { - 'name': event_id, - 'value': trigger, - 'state_update': sp.MutableDenseMatrix(bolus), - 'initial_value': initial_value, + "name": event_id, + "value": trigger, + "state_update": sp.MutableDenseMatrix(bolus), + "initial_value": initial_value, } - @log_execution_time('processing SBML observables', logger) + @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]], - noise_distributions: Dict[str, str] + noise_distributions: Dict[str, str], ) -> None: """ Perform symbolic computations required for observable and objective @@ -1409,8 +1468,7 @@ def _process_observables( See :py:func:`sbml2amici`. """ - _validate_observables(observables, sigmas, noise_distributions, - events=False) + _validate_observables(observables, sigmas, noise_distributions, events=False) # add user-provided observables or make all species, and compartments # with assignment rules, observable @@ -1421,22 +1479,18 @@ def _process_observables( self.symbols[SymbolId.OBSERVABLE] = { symbol_with_assumptions(obs): { - 'name': definition.get('name', f'y{iobs}'), - 'value': self._sympy_from_sbml_math( - definition['formula'] + "name": definition.get("name", f"y{iobs}"), + "value": self._sympy_from_sbml_math(definition["formula"]), + "transformation": noise_distribution_to_observable_transformation( + noise_distributions.get(obs, "normal") ), - 'transformation': - noise_distribution_to_observable_transformation( - noise_distributions.get(obs, 'normal') - ) } for iobs, (obs, definition) in enumerate(observables.items()) } # check for nesting of observables (unsupported) observable_syms = set(self.symbols[SymbolId.OBSERVABLE].keys()) for obs in self.symbols[SymbolId.OBSERVABLE].values(): - if any(sym in observable_syms - for sym in obs['value'].free_symbols): + if any(sym in observable_syms for sym in obs["value"].free_symbols): raise ValueError( "Nested observables are not supported, " f"but observable `{obs['name']} = {obs['value']}` " @@ -1445,17 +1499,16 @@ def _process_observables( elif observables is None: self._generate_default_observables() - _check_symbol_nesting(self.symbols[SymbolId.OBSERVABLE], - 'eventObservable') + _check_symbol_nesting(self.symbols[SymbolId.OBSERVABLE], "eventObservable") self._process_log_likelihood(sigmas, noise_distributions) - @log_execution_time('processing SBML event observables', logger) + @log_execution_time("processing SBML event observables", logger) def _process_event_observables( - self, - event_observables: Dict[str, Dict[str, str]], - event_sigmas: Dict[str, Union[str, float]], - event_noise_distributions: Dict[str, str] + self, + event_observables: Dict[str, Dict[str, str]], + event_sigmas: Dict[str, Union[str, float]], + event_noise_distributions: Dict[str, str], ) -> None: """ Perform symbolic computations required for observable and objective @@ -1473,54 +1526,54 @@ def _process_event_observables( if event_observables is None: return - _validate_observables(event_observables, event_sigmas, - event_noise_distributions, - events=True) + _validate_observables( + event_observables, event_sigmas, event_noise_distributions, events=True + ) # gather local symbols before parsing observable and sigma formulas for obs, definition in event_observables.items(): self.add_local_symbol(obs, symbol_with_assumptions(obs)) # check corresponding event exists - if sp.Symbol(definition['event']) not in \ - self.symbols[SymbolId.EVENT]: + if sp.Symbol(definition["event"]) not in self.symbols[SymbolId.EVENT]: raise ValueError( - 'Could not find an event with the event identifier ' + "Could not find an event with the event identifier " f'{definition["event"]} for the event observable with name' f'{definition["name"]}.' ) self.symbols[SymbolId.EVENT_OBSERVABLE] = { symbol_with_assumptions(obs): { - 'name': definition.get('name', f'z{iobs}'), - 'value': self._sympy_from_sbml_math( - definition['formula'] + "name": definition.get("name", f"z{iobs}"), + "value": self._sympy_from_sbml_math(definition["formula"]), + "event": sp.Symbol(definition.get("event")), + "transformation": noise_distribution_to_observable_transformation( + event_noise_distributions.get(obs, "normal") ), - 'event': sp.Symbol(definition.get('event')), - 'transformation': - noise_distribution_to_observable_transformation( - event_noise_distributions.get(obs, 'normal') - ) } - for iobs, (obs, definition) in - enumerate(event_observables.items()) + for iobs, (obs, definition) in enumerate(event_observables.items()) } - wrong_t = sp.Symbol('t') + wrong_t = sp.Symbol("t") for eo in self.symbols[SymbolId.EVENT_OBSERVABLE].values(): - if eo['value'].has(wrong_t): - warnings.warn(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!') + if eo["value"].has(wrong_t): + warnings.warn( + 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!" + ) # check for nesting of observables (unsupported) - _check_symbol_nesting(self.symbols[SymbolId.EVENT_OBSERVABLE], - 'eventObservable') + _check_symbol_nesting( + self.symbols[SymbolId.EVENT_OBSERVABLE], "eventObservable" + ) - self._process_log_likelihood(event_sigmas, event_noise_distributions, - events=True) - self._process_log_likelihood(event_sigmas, event_noise_distributions, - events=True, event_reg=True) + self._process_log_likelihood( + event_sigmas, event_noise_distributions, events=True + ) + self._process_log_likelihood( + event_sigmas, event_noise_distributions, events=True, event_reg=True + ) def _generate_default_observables(self): """ @@ -1528,40 +1581,44 @@ def _generate_default_observables(self): (initial) assignment rules. """ self.symbols[SymbolId.OBSERVABLE] = { - symbol_with_assumptions(f'y{state_id}'): { - 'name': state['name'], - 'value': state_id + symbol_with_assumptions(f"y{state_id}"): { + "name": state["name"], + "value": state_id, } - for state_id, state - in { + for state_id, state in { **self.symbols[SymbolId.SPECIES], - **self.symbols[SymbolId.ALGEBRAIC_STATE] + **self.symbols[SymbolId.ALGEBRAIC_STATE], }.items() } for variable, formula in itt.chain( - self.parameter_assignment_rules.items(), - self.initial_assignments.items(), - self.compartment_assignment_rules.items(), - self.species_assignment_rules.items(), - self.compartments.items() + self.parameter_assignment_rules.items(), + self.initial_assignments.items(), + self.compartment_assignment_rules.items(), + self.species_assignment_rules.items(), + self.compartments.items(), ): - symbol = symbol_with_assumptions(f'y{variable}') + symbol = symbol_with_assumptions(f"y{variable}") # Assignment rules take precedence over compartment volume # definitions, so they need to be evaluated first. # Species assignment rules always overwrite. - if symbol in self.symbols[SymbolId.OBSERVABLE] \ - and variable not in self.species_assignment_rules: + if ( + symbol in self.symbols[SymbolId.OBSERVABLE] + and variable not in self.species_assignment_rules + ): continue self.symbols[SymbolId.OBSERVABLE][symbol] = { - 'name': str(variable), 'value': formula + "name": str(variable), + "value": formula, } - def _process_log_likelihood(self, - sigmas: Dict[str, Union[str, float]], - noise_distributions: Dict[str, str], - events: bool = False, - event_reg: bool = False): + def _process_log_likelihood( + self, + sigmas: Dict[str, Union[str, float]], + noise_distributions: Dict[str, str], + events: bool = False, + event_reg: bool = False, + ): """ Perform symbolic computations required for objective function evaluation. @@ -1599,43 +1656,45 @@ def _process_log_likelihood(self, llh_symbol = SymbolId.LLHY for obs_id, obs in self.symbols[obs_symbol].items(): - obs['measurement_symbol'] = generate_measurement_symbol(obs_id) + obs["measurement_symbol"] = generate_measurement_symbol(obs_id) if event_reg: - obs['reg_symbol'] = generate_regularization_symbol(obs_id) + obs["reg_symbol"] = generate_regularization_symbol(obs_id) if not event_reg: self.symbols[sigma_symbol] = { - symbol_with_assumptions(f'sigma_{obs_id}'): { - 'name': f'sigma_{obs["name"]}', - 'value': self._sympy_from_sbml_math( - sigmas.get(str(obs_id), '1.0') - ) + symbol_with_assumptions(f"sigma_{obs_id}"): { + "name": f'sigma_{obs["name"]}', + "value": self._sympy_from_sbml_math(sigmas.get(str(obs_id), "1.0")), } for obs_id, obs in self.symbols[obs_symbol].items() } self.symbols[llh_symbol] = {} for (obs_id, obs), (sigma_id, sigma) in zip( - self.symbols[obs_symbol].items(), - self.symbols[sigma_symbol].items() + self.symbols[obs_symbol].items(), self.symbols[sigma_symbol].items() ): - symbol = symbol_with_assumptions(f'J{obs_id}') - dist = noise_distributions.get(str(obs_id), 'normal') + symbol = symbol_with_assumptions(f"J{obs_id}") + dist = noise_distributions.get(str(obs_id), "normal") cost_fun = noise_distribution_to_cost_function(dist)(obs_id) - value = sp.sympify(cost_fun, locals=dict(zip( - _get_str_symbol_identifiers(obs_id), - (obs_id, obs['measurement_symbol'], sigma_id) - ))) + value = sp.sympify( + cost_fun, + locals=dict( + zip( + _get_str_symbol_identifiers(obs_id), + (obs_id, obs["measurement_symbol"], sigma_id), + ) + ), + ) if event_reg: - value = value.subs(obs['measurement_symbol'], 0.0) - value = value.subs(obs_id, obs['reg_symbol']) + value = value.subs(obs["measurement_symbol"], 0.0) + value = value.subs(obs_id, obs["reg_symbol"]) self.symbols[llh_symbol][symbol] = { - 'name': f'J{obs["name"]}', - 'value': value, - 'dist': dist, - } + "name": f'J{obs["name"]}', + "value": value, + "dist": dist, + } - @log_execution_time('processing SBML initial assignments', logger) + @log_execution_time("processing SBML initial assignments", logger) def _process_initial_assignments(self): """ Accounts for initial assignments of parameters and species @@ -1645,17 +1704,18 @@ def _process_initial_assignments(self): """ for ia in self.sbml.getListOfInitialAssignments(): identifier = _get_identifier_symbol(ia) - if identifier in itt.chain(self.symbols[SymbolId.SPECIES], - self.compartments): + if identifier in itt.chain( + self.symbols[SymbolId.SPECIES], self.compartments + ): continue sym_math = self._get_element_initial_assignment(ia.getId()) if sym_math is None: continue - sym_math = self._make_initial(smart_subs_dict( - sym_math, self.symbols[SymbolId.EXPRESSION], 'value' - )) + sym_math = self._make_initial( + smart_subs_dict(sym_math, self.symbols[SymbolId.EXPRESSION], "value") + ) self.initial_assignments[_get_identifier_symbol(ia)] = sym_math # sort and flatten @@ -1668,7 +1728,7 @@ def _process_initial_assignments(self): for identifier, sym_math in list(self.initial_assignments.items()): self._replace_in_all_expressions(identifier, sym_math) - @log_execution_time('processing SBML species references', logger) + @log_execution_time("processing SBML species references", logger) def _process_species_references(self): """ Replaces species references that define anything but stoichiometries. @@ -1679,21 +1739,26 @@ def _process_species_references(self): # doesnt look like there is a better way to get hold of those lists: species_references = _get_list_of_species_references(self.sbml) for species_reference in species_references: - if hasattr(species_reference, 'getStoichiometryMath') and \ - species_reference.getStoichiometryMath() is not None: - raise SBMLException('StoichiometryMath is currently not ' - 'supported for species references.') - if species_reference.getId() == '': + if ( + hasattr(species_reference, "getStoichiometryMath") + and species_reference.getStoichiometryMath() is not None + ): + raise SBMLException( + "StoichiometryMath is currently not " + "supported for species references." + ) + if species_reference.getId() == "": continue stoich = self._get_element_stoichiometry(species_reference) self._replace_in_all_expressions( _get_identifier_symbol(species_reference), - self._sympy_from_sbml_math(stoich) + self._sympy_from_sbml_math(stoich), ) - def _make_initial(self, sym_math: Union[sp.Expr, None, float] - ) -> Union[sp.Expr, None, float]: + def _make_initial( + self, sym_math: Union[sp.Expr, None, float] + ) -> Union[sp.Expr, None, float]: """ Transforms an expression to its value at the initial time point by replacing species by their initial values. @@ -1708,11 +1773,10 @@ def _make_initial(self, sym_math: Union[sp.Expr, None, float] return sym_math for species_id, species in self.symbols[SymbolId.SPECIES].items(): - if 'init' in species: - sym_math = smart_subs(sym_math, species_id, species['init']) + if "init" in species: + sym_math = smart_subs(sym_math, species_id, species["init"]) - sym_math = smart_subs(sym_math, self._local_symbols['time'], - sp.Float(0)) + sym_math = smart_subs(sym_math, self._local_symbols["time"], sp.Float(0)) return sym_math @@ -1730,11 +1794,18 @@ def process_conservation_laws(self, ode_model) -> None: ode_model, conservation_laws ) # Non-constant species processed here - if "AMICI_EXPERIMENTAL_SBML_NONCONST_CLS" in os.environ \ - or "GITHUB_ACTIONS" in os.environ: - species_solver = list(set( - self._add_conservation_for_non_constant_species( - ode_model, conservation_laws)) & set(species_solver)) + if ( + "AMICI_EXPERIMENTAL_SBML_NONCONST_CLS" in os.environ + or "GITHUB_ACTIONS" in os.environ + ): + species_solver = list( + set( + self._add_conservation_for_non_constant_species( + ode_model, conservation_laws + ) + ) + & set(species_solver) + ) # add algebraic variables to species_solver as they were ignored above ndifferential = len(ode_model._differential_states) @@ -1749,16 +1820,15 @@ def process_conservation_laws(self, ode_model) -> None: species_solver = list(range(ode_model.num_states_rdata())) # prune out species from stoichiometry and - self.stoichiometric_matrix = \ - self.stoichiometric_matrix[species_solver, :] + self.stoichiometric_matrix = self.stoichiometric_matrix[species_solver, :] # add the found CLs to the ode_model for cl in conservation_laws: ode_model.add_conservation_law(**cl) def _get_conservation_laws_demartino( - self, - ode_model: DEModel, + self, + ode_model: DEModel, ) -> List[Tuple[int, List[int], List[float]]]: """Identify conservation laws based on algorithm by DeMartino et al. (see conserved_moieties.py). @@ -1770,32 +1840,32 @@ def _get_conservation_laws_demartino( quantity (including the eliminated one) (2) coefficients for the species in (1) """ - from .conserved_quantities_demartino \ - import compute_moiety_conservation_laws + from .conserved_quantities_demartino import compute_moiety_conservation_laws - sm = self.stoichiometric_matrix[:len(self.symbols[SymbolId.SPECIES]), :] + sm = self.stoichiometric_matrix[: len(self.symbols[SymbolId.SPECIES]), :] try: - stoichiometric_list = [ - float(entry) for entry in sm.T.flat() - ] + stoichiometric_list = [float(entry) for entry in sm.T.flat()] except TypeError: # Due to the numerical algorithm currently used to identify # conserved quantities, we can't have symbols in the # stoichiometric matrix - warnings.warn("Conservation laws for non-constant species in " - "combination with parameterized stoichiometric " - "coefficients are not currently supported " - "and will be turned off.") + warnings.warn( + "Conservation laws for non-constant species in " + "combination with parameterized stoichiometric " + "coefficients are not currently supported " + "and will be turned off." + ) return [] if not _non_const_conservation_laws_supported(self.sbml): return [] cls_state_idxs, cls_coefficients = compute_moiety_conservation_laws( - stoichiometric_list, *sm.shape, + stoichiometric_list, + *sm.shape, rng_seed=32, - species_names=[str(x.get_id()) for x in ode_model._differential_states] + species_names=[str(x.get_id()) for x in ode_model._differential_states], ) # Sparsify conserved quantities @@ -1808,8 +1878,9 @@ def _get_conservation_laws_demartino( # pivot species are the ones to be eliminated. The resulting state # expressions are sparse and void of any circular dependencies. A = sp.zeros(len(cls_coefficients), len(ode_model._differential_states)) - for i_cl, (cl, coefficients) in enumerate(zip(cls_state_idxs, - cls_coefficients)): + for i_cl, (cl, coefficients) in enumerate( + zip(cls_state_idxs, cls_coefficients) + ): for i, c in zip(cl, coefficients): A[i_cl, i] = sp.Rational(c) rref, pivots = A.rref() @@ -1817,16 +1888,14 @@ def _get_conservation_laws_demartino( raw_cls = [] for i_cl, target_state_model_idx in enumerate(pivots): # collect values for species engaged in the current CL - state_idxs = [i for i, coeff in enumerate(rref[i_cl, :]) - if coeff] + state_idxs = [i for i, coeff in enumerate(rref[i_cl, :]) if coeff] coefficients = [coeff for coeff in rref[i_cl, :] if coeff] - raw_cls.append((target_state_model_idx, state_idxs, - coefficients),) + raw_cls.append( + (target_state_model_idx, state_idxs, coefficients), + ) return raw_cls - def _get_conservation_laws_rref( - self - ) -> List[Tuple[int, List[int], List[float]]]: + def _get_conservation_laws_rref(self) -> List[Tuple[int, List[int], List[float]]]: """Identify conservation laws based on left nullspace of the stoichiometric matrix, computed through (numeric) Gaussian elimination @@ -1838,22 +1907,24 @@ def _get_conservation_laws_rref( """ import numpy as np from numpy.linalg import matrix_rank + from .conserved_quantities_rref import nullspace_by_rref, rref try: S = np.asarray( - self.stoichiometric_matrix[ - :len(self.symbols[SymbolId.SPECIES]), : - ], dtype=float + self.stoichiometric_matrix[: len(self.symbols[SymbolId.SPECIES]), :], + dtype=float, ) except TypeError: # Due to the numerical algorithm currently used to identify # conserved quantities, we can't have symbols in the # stoichiometric matrix - warnings.warn("Conservation laws for non-constant species in " - "combination with parameterized stoichiometric " - "coefficients are not currently supported " - "and will be turned off.") + warnings.warn( + "Conservation laws for non-constant species in " + "combination with parameterized stoichiometric " + "coefficients are not currently supported " + "and will be turned off." + ) return [] if not _non_const_conservation_laws_supported(self.sbml): @@ -1878,14 +1949,14 @@ def _get_conservation_laws_rref( for row in kernel: state_idxs = [i for i, coeff in enumerate(row) if coeff] coefficients = [coeff for coeff in row if coeff] - raw_cls.append((state_idxs[0], state_idxs, coefficients),) + raw_cls.append( + (state_idxs[0], state_idxs, coefficients), + ) return raw_cls def _add_conservation_for_non_constant_species( - self, - model: DEModel, - conservation_laws: List[ConservationLaw] + self, model: DEModel, conservation_laws: List[ConservationLaw] ) -> List[int]: """Add non-constant species to conservation laws @@ -1914,23 +1985,21 @@ def _add_conservation_for_non_constant_species( # keep new conservations laws separate until we know everything worked new_conservation_laws = [] # previously removed constant species - eliminated_state_ids = {cl['state'] for cl in conservation_laws} + eliminated_state_ids = {cl["state"] for cl in conservation_laws} all_state_ids = [x.get_id() for x in model.states()] all_compartment_sizes = [] for state_id in all_state_ids: symbol = { **self.symbols[SymbolId.SPECIES], - **self.symbols[SymbolId.ALGEBRAIC_STATE] + **self.symbols[SymbolId.ALGEBRAIC_STATE], }[state_id] - if 'amount' not in symbol: + if "amount" not in symbol: continue # not a species - if symbol['amount']: + if symbol["amount"]: compartment_size = sp.Integer(1) else: - compartment_size = self.compartments[ - symbol['compartment'] - ] + compartment_size = self.compartments[symbol["compartment"]] all_compartment_sizes.append(compartment_size) # iterate over list of conservation laws, create symbolic expressions, @@ -1943,17 +2012,20 @@ def _add_conservation_for_non_constant_species( compartment_sizes = [all_compartment_sizes[i] for i in state_idxs] target_state_id = all_state_ids[target_state_model_idx] - total_abundance = symbol_with_assumptions(f'tcl_{target_state_id}') - - new_conservation_laws.append({ - 'state': target_state_id, - 'total_abundance': total_abundance, - 'coefficients': { - state_id: coeff * compartment - for state_id, coeff, compartment - in zip(state_ids, coefficients, compartment_sizes) - }, - }) + total_abundance = symbol_with_assumptions(f"tcl_{target_state_id}") + + new_conservation_laws.append( + { + "state": target_state_id, + "total_abundance": total_abundance, + "coefficients": { + state_id: coeff * compartment + for state_id, coeff, compartment in zip( + state_ids, coefficients, compartment_sizes + ) + }, + } + ) species_to_be_removed.add(target_state_model_idx) conservation_laws.extend(new_conservation_laws) @@ -1967,23 +2039,23 @@ def _replace_compartments_with_volumes(self): (possibly variable) volumes. """ for comp, vol in self.compartments.items(): - if comp in self.symbols[SymbolId.SPECIES] \ - or comp in self.symbols[SymbolId.ALGEBRAIC_STATE]: + if ( + comp in self.symbols[SymbolId.SPECIES] + or comp in self.symbols[SymbolId.ALGEBRAIC_STATE] + ): # for comps with rate rules volume is only initial for state in { **self.symbols[SymbolId.SPECIES], - **self.symbols[SymbolId.ALGEBRAIC_STATE] + **self.symbols[SymbolId.ALGEBRAIC_STATE], }.values(): - if isinstance(state['init'], sp.Expr): - state['init'] = smart_subs(state['init'], - comp, vol) + if isinstance(state["init"], sp.Expr): + state["init"] = smart_subs(state["init"], comp, vol) continue self._replace_in_all_expressions(comp, vol) - def _replace_in_all_expressions(self, - old: sp.Symbol, - new: sp.Expr, - replace_identifiers=False) -> None: + def _replace_in_all_expressions( + self, old: sp.Symbol, new: sp.Expr, replace_identifiers=False + ) -> None: """ Replace 'old' by 'new' in all symbolic expressions. @@ -1994,17 +2066,19 @@ def _replace_in_all_expressions(self, replacement symbolic variables """ fields = [ - 'stoichiometric_matrix', 'flux_vector', + "stoichiometric_matrix", + "flux_vector", ] for field in fields: if field in dir(self): - self.__setattr__(field, smart_subs( - self.__getattribute__(field), old, new - )) + self.__setattr__( + field, smart_subs(self.__getattribute__(field), old, new) + ) dictfields = [ - 'compartment_assignment_rules', 'parameter_assignment_rules', - 'initial_assignments' + "compartment_assignment_rules", + "parameter_assignment_rules", + "initial_assignments", ] for dictfield in dictfields: d = getattr(self, dictfield) @@ -2014,7 +2088,7 @@ def _replace_in_all_expressions(self, d[new] = d[old] del d[old] - if dictfield == 'initial_assignments': + if dictfield == "initial_assignments": tmp_new = self._make_initial(new) else: tmp_new = new @@ -2025,55 +2099,69 @@ def _replace_in_all_expressions(self, # replace in identifiers if replace_identifiers: - for symbol in [SymbolId.EXPRESSION, SymbolId.SPECIES, - SymbolId.ALGEBRAIC_STATE]: + for symbol in [ + SymbolId.EXPRESSION, + SymbolId.SPECIES, + SymbolId.ALGEBRAIC_STATE, + ]: # completely recreate the dict to keep ordering consistent if old not in self.symbols[symbol]: continue self.symbols[symbol] = { - smart_subs(k, old, new): v - for k, v in self.symbols[symbol].items() + smart_subs(k, old, new): v for k, v in self.symbols[symbol].items() } - for symbol in [SymbolId.OBSERVABLE, SymbolId.LLHY, - SymbolId.SIGMAY]: + for symbol in [SymbolId.OBSERVABLE, SymbolId.LLHY, SymbolId.SIGMAY]: if old not in self.symbols[symbol]: continue self.symbols[symbol][new] = self.symbols[symbol][old] del self.symbols[symbol][old] # replace in values - for symbol in [SymbolId.OBSERVABLE, SymbolId.LLHY, SymbolId.LLHZ, - SymbolId.SIGMAY, SymbolId.SIGMAZ, SymbolId.EXPRESSION, - SymbolId.EVENT, SymbolId.EVENT_OBSERVABLE, - SymbolId.ALGEBRAIC_EQUATION]: + for symbol in [ + SymbolId.OBSERVABLE, + SymbolId.LLHY, + SymbolId.LLHZ, + SymbolId.SIGMAY, + SymbolId.SIGMAZ, + SymbolId.EXPRESSION, + SymbolId.EVENT, + SymbolId.EVENT_OBSERVABLE, + SymbolId.ALGEBRAIC_EQUATION, + ]: for element in self.symbols[symbol].values(): - element['value'] = smart_subs(element['value'], old, new) + element["value"] = smart_subs(element["value"], old, new) # replace in event state updates (boluses) if self.symbols.get(SymbolId.EVENT, False): for event in self.symbols[SymbolId.EVENT].values(): - for index in range(len(event['state_update'])): - event['state_update'][index] = \ - smart_subs(event['state_update'][index], old, new) + for index in range(len(event["state_update"])): + event["state_update"][index] = smart_subs( + event["state_update"][index], old, new + ) for state in { **self.symbols[SymbolId.SPECIES], - **self.symbols[SymbolId.ALGEBRAIC_STATE] + **self.symbols[SymbolId.ALGEBRAIC_STATE], }.values(): - state['init'] = smart_subs(state['init'], - old, self._make_initial(new)) + state["init"] = smart_subs(state["init"], old, self._make_initial(new)) - if 'dt' in state: - state['dt'] = smart_subs(state['dt'], old, new) + if "dt" in state: + state["dt"] = smart_subs(state["dt"], old, new) # Initial compartment volume may also be specified with an assignment # rule (at the end of the _process_species method), hence needs to be # processed here too. - self.compartments = {smart_subs(c, old, new) if replace_identifiers - else c: - smart_subs(v, old, self._make_initial(new)) - for c, v in self.compartments.items()} + self.compartments = { + smart_subs(c, old, new) + if replace_identifiers + else c: smart_subs(v, old, self._make_initial(new)) + for c, v in self.compartments.items() + } + + # Substitute inside spline definitions + for spline in self.splines: + spline._replace_in_all_expressions(old, new) def _clean_reserved_symbols(self) -> None: """ @@ -2081,9 +2169,10 @@ def _clean_reserved_symbols(self) -> None: """ for sym in RESERVED_SYMBOLS: old_symbol = symbol_with_assumptions(sym) - new_symbol = symbol_with_assumptions(f'amici_{sym}') - self._replace_in_all_expressions(old_symbol, new_symbol, - replace_identifiers=True) + new_symbol = symbol_with_assumptions(f"amici_{sym}") + self._replace_in_all_expressions( + old_symbol, new_symbol, replace_identifiers=True + ) for symbols_ids, symbols in self.symbols.items(): if old_symbol in symbols: # reconstitute the whole dict in order to keep the ordering @@ -2092,8 +2181,9 @@ def _clean_reserved_symbols(self) -> None: for k, v in symbols.items() } - def _sympy_from_sbml_math(self, var_or_math: [sbml.SBase, str] - ) -> Union[sp.Expr, float, None]: + def _sympy_from_sbml_math( + self, var_or_math: [sbml.SBase, str] + ) -> Union[sp.Expr, float, None]: """ Sympify Math of SBML variables with all sanity checks and transformations @@ -2105,41 +2195,43 @@ def _sympy_from_sbml_math(self, var_or_math: [sbml.SBase, str] """ if isinstance(var_or_math, sbml.SBase): math_string = sbml.formulaToL3StringWithSettings( - var_or_math.getMath(), - self.sbml_parser_settings + var_or_math.getMath(), self.sbml_parser_settings ) ele_name = var_or_math.element_name else: math_string = var_or_math - ele_name = 'string' + ele_name = "string" math_string = replace_logx(math_string) try: try: - formula = sp.sympify(_parse_logical_operators( - math_string - ), locals=self._local_symbols) + formula = sp.sympify( + _parse_logical_operators(math_string), locals=self._local_symbols + ) except TypeError as err: - if str(err) == 'BooleanAtom not allowed in this context.': - formula = sp.sympify(_parse_logical_operators( - math_string - ), locals={'true': sp.Float(1.0), 'false': sp.Float(0.0), - **self._local_symbols}) + if str(err) == "BooleanAtom not allowed in this context.": + formula = sp.sympify( + _parse_logical_operators(math_string), + locals={ + "true": sp.Float(1.0), + "false": sp.Float(0.0), + **self._local_symbols, + }, + ) else: raise except (sp.SympifyError, TypeError, ZeroDivisionError) as err: - raise SBMLException(f'{ele_name} "{math_string}" ' - 'contains an unsupported expression: ' - f'{err}.') + raise SBMLException( + f'{ele_name} "{math_string}" ' + "contains an unsupported expression: " + f"{err}." + ) if isinstance(formula, sp.Expr): formula = _parse_special_functions_sbml(formula) - _check_unsupported_functions_sbml(formula, - expression_type=ele_name) + _check_unsupported_functions_sbml(formula, expression_type=ele_name) return formula - def _get_element_initial_assignment(self, - element_id: str) -> Union[sp.Expr, - None]: + def _get_element_initial_assignment(self, element_id: str) -> Union[sp.Expr, None]: """ Extract value of sbml variable according to its initial assignment @@ -2148,9 +2240,7 @@ def _get_element_initial_assignment(self, :return: """ - assignment = self.sbml.getInitialAssignment( - element_id - ) + assignment = self.sbml.getInitialAssignment(element_id) if assignment is None: return None sym = self._sympy_from_sbml_math(assignment) @@ -2159,8 +2249,7 @@ def _get_element_initial_assignment(self, sym = self._make_initial(sym) return sym - def _get_element_stoichiometry(self, - ele: sbml.SBase) -> sp.Expr: + def _get_element_stoichiometry(self, ele: sbml.SBase) -> sp.Expr: """ Computes the stoichiometry of a reactant or product of a reaction @@ -2179,8 +2268,11 @@ def _get_element_stoichiometry(self, if ele.isSetStoichiometry(): stoichiometry: float = ele.getStoichiometry() - return sp.Integer(stoichiometry) if stoichiometry.is_integer() \ + return ( + sp.Integer(stoichiometry) + if stoichiometry.is_integer() else sp.Float(stoichiometry) + ) return sp.Integer(1) @@ -2213,8 +2305,9 @@ def is_rate_rule_target(self, element: sbml.SBase) -> bool: return a is not None and self._sympy_from_sbml_math(a) is not None -def _check_lib_sbml_errors(sbml_doc: sbml.SBMLDocument, - show_warnings: bool = False) -> None: +def _check_lib_sbml_errors( + sbml_doc: sbml.SBMLDocument, show_warnings: bool = False +) -> None: """ Checks the error log in the current self.sbml_doc. @@ -2232,17 +2325,17 @@ def _check_lib_sbml_errors(sbml_doc: sbml.SBMLDocument, for i_error in range(sbml_doc.getNumErrors()): error = sbml_doc.getError(i_error) # we ignore any info messages for now - if error.getSeverity() >= sbml.LIBSBML_SEV_ERROR \ - or (show_warnings and - error.getSeverity() >= sbml.LIBSBML_SEV_WARNING): - logger.error(f'libSBML {error.getCategoryAsString()} ' - f'({error.getSeverityAsString()}):' - f' {error.getMessage()}') + if error.getSeverity() >= sbml.LIBSBML_SEV_ERROR or ( + show_warnings and error.getSeverity() >= sbml.LIBSBML_SEV_WARNING + ): + logger.error( + f"libSBML {error.getCategoryAsString()} " + f"({error.getSeverityAsString()}):" + f" {error.getMessage()}" + ) if num_error + num_fatal: - raise SBMLException( - 'SBML Document failed to load (see error messages above)' - ) + raise SBMLException("SBML Document failed to load (see error messages above)") def _parse_event_trigger(trigger: sp.Expr) -> sp.Expr: @@ -2259,15 +2352,18 @@ def _parse_event_trigger(trigger: sp.Expr) -> sp.Expr: return sp.Float(1.0) if trigger.is_Relational: root = trigger.args[0] - trigger.args[1] - _check_unsupported_functions_sbml(root, 'sympy.Expression') + _check_unsupported_functions_sbml(root, "sympy.Expression") # convert relational expressions into trigger functions - if isinstance(trigger, (sp.core.relational.LessThan, - sp.core.relational.StrictLessThan)): + if isinstance( + trigger, (sp.core.relational.LessThan, sp.core.relational.StrictLessThan) + ): # y < x or y <= x return -root - if isinstance(trigger, (sp.core.relational.GreaterThan, - sp.core.relational.StrictGreaterThan)): + if isinstance( + trigger, + (sp.core.relational.GreaterThan, sp.core.relational.StrictGreaterThan), + ): # y >= x or y > x return root @@ -2279,34 +2375,14 @@ def _parse_event_trigger(trigger: sp.Expr) -> sp.Expr: return sp.Min(*[_parse_event_trigger(arg) for arg in trigger.args]) raise SBMLException( - 'AMICI can not parse piecewise/event trigger functions with argument ' - f'{trigger}.' + "AMICI can not parse piecewise/event trigger functions with argument " + f"{trigger}." ) -def _parse_logical_operators(math_str: Union[str, float, None] - ) -> Union[str, float, None]: - """ - Parses a math string in order to replace logical operators by a form - parsable for sympy - - :param math_str: - str with mathematical expression - :param math_str: - parsed math_str - """ - if not isinstance(math_str, str): - return math_str - - if ' xor(' in math_str or ' Xor(' in math_str: - raise SBMLException('Xor is currently not supported as logical ' - 'operation.') - - return (math_str.replace('&&', '&')).replace('||', '|') - - -def assignmentRules2observables(sbml_model: sbml.Model, - filter_function: Callable = lambda *_: True): +def assignmentRules2observables( + sbml_model: sbml.Model, filter_function: Callable = lambda *_: True +): """ Turn assignment rules into observables. @@ -2329,13 +2405,12 @@ def assignmentRules2observables(sbml_model: sbml.Model, if rule.getTypeCode() != sbml.SBML_ASSIGNMENT_RULE: continue parameter_id = rule.getVariable() - if (p := sbml_model.getParameter(parameter_id)) \ - and filter_function(p): + if (p := sbml_model.getParameter(parameter_id)) and filter_function(p): observables[parameter_id] = { - 'name': p.getName() if p.isSetName() else parameter_id, - 'formula': sbml_model.getAssignmentRuleByVariable( + "name": p.getName() if p.isSetName() else parameter_id, + "formula": sbml_model.getAssignmentRuleByVariable( parameter_id - ).getFormula() + ).getFormula(), } for parameter_id in observables: @@ -2346,8 +2421,7 @@ def assignmentRules2observables(sbml_model: sbml.Model, def _add_conservation_for_constant_species( - ode_model: DEModel, - conservation_laws: List[ConservationLaw] + ode_model: DEModel, conservation_laws: List[ConservationLaw] ) -> List[int]: """ Adds constant species to conservations laws @@ -2371,12 +2445,14 @@ def _add_conservation_for_constant_species( # dont use sym('x') here since conservation laws need to be # added before symbols are generated target_state = ode_model._differential_states[ix].get_id() - total_abundance = symbol_with_assumptions(f'tcl_{target_state}') - conservation_laws.append({ - 'state': target_state, - 'total_abundance': total_abundance, - 'coefficients': {target_state: 1.0}, - }) + total_abundance = symbol_with_assumptions(f"tcl_{target_state}") + conservation_laws.append( + { + "state": target_state, + "total_abundance": total_abundance, + "coefficients": {target_state: 1.0}, + } + ) # mark species to delete from stoichiometric matrix species_solver.pop(ix) @@ -2441,8 +2517,9 @@ def get_species_initial(species: sbml.Species) -> sp.Expr: return sp.Float(0.0) -def _get_list_of_species_references(sbml_model: sbml.Model) \ - -> List[sbml.SpeciesReference]: +def _get_list_of_species_references( + sbml_model: sbml.Model, +) -> List[sbml.SpeciesReference]: """ Extracts list of species references as SBML doesn't provide a native function for this. @@ -2454,11 +2531,11 @@ def _get_list_of_species_references(sbml_model: sbml.Model) \ ListOfSpeciesReferences """ return [ - reference - for element in sbml_model.all_elements - if isinstance(element, sbml.ListOfSpeciesReferences) - for reference in element - ] + reference + for reaction in sbml_model.getListOfReactions() + for reference in + itt.chain(reaction.getListOfReactants(), reaction.getListOfProducts(), reaction.getListOfModifiers()) + ] def replace_logx(math_str: Union[str, float, None]) -> Union[str, float, None]: @@ -2474,9 +2551,7 @@ def replace_logx(math_str: Union[str, float, None]) -> Union[str, float, None]: if not isinstance(math_str, str): return math_str - return re.sub( - r'(^|\W)log(\d+)\(', r'\g<1>1/ln(\2)*ln(', math_str - ) + return re.sub(r"(^|\W)log(\d+)\(", r"\g<1>1/ln(\2)*ln(", math_str) def _collect_event_assignment_parameter_targets(sbml_model: sbml.Model): @@ -2487,23 +2562,24 @@ def _collect_event_assignment_parameter_targets(sbml_model: sbml.Model): for event_assignment in event.getListOfEventAssignments(): target_id = event_assignment.getVariable() if target_id in sbml_parameter_ids: - targets.add(_get_identifier_symbol( - sbml_parameters[sbml_parameter_ids.index(target_id)] - )) + targets.add( + _get_identifier_symbol( + sbml_parameters[sbml_parameter_ids.index(target_id)] + ) + ) return targets -def _check_unsupported_functions_sbml(sym: sp.Expr, - expression_type: str, - full_sym: Optional[sp.Expr] = None): +def _check_unsupported_functions_sbml( + sym: sp.Expr, expression_type: str, full_sym: Optional[sp.Expr] = None +): try: _check_unsupported_functions(sym, expression_type, full_sym) except RuntimeError as err: raise SBMLException(str(err)) -def _parse_special_functions_sbml(sym: sp.Expr, - toplevel: bool = True) -> sp.Expr: +def _parse_special_functions_sbml(sym: sp.Expr, toplevel: bool = True) -> sp.Expr: try: return _parse_special_functions(sym, toplevel) except RuntimeError as err: @@ -2514,9 +2590,8 @@ def _validate_observables( observables: Union[Dict[str, Dict[str, str]], None], sigmas: Dict[str, Union[str, float]], noise_distributions: Dict[str, str], - events: bool = False + events: bool = False, ) -> None: - if observables is None or not observables: return @@ -2527,25 +2602,26 @@ def _validate_observables( raise ValueError( f"Sigma provided for unknown " f"{'eventO' if events else 'o'}bservableIds: " - f"{unknown_ids}.") + f"{unknown_ids}." + ) # Ensure no non-existing observableIds have been specified # (no problem here, but usually an upstream bug) - unknown_ids = set(noise_distributions.keys()) - \ - set(observables.keys()) + unknown_ids = set(noise_distributions.keys()) - set(observables.keys()) if unknown_ids: raise ValueError( f"Noise distribution provided for unknown " f"{'eventO' if events else 'o'}bservableIds: " - f"{unknown_ids}.") + f"{unknown_ids}." + ) -def _check_symbol_nesting(symbols: Dict[sp.Symbol, Dict[str, sp.Expr]], - symbol_type: str): +def _check_symbol_nesting( + symbols: Dict[sp.Symbol, Dict[str, sp.Expr]], symbol_type: str +): observable_syms = set(symbols.keys()) for obs in symbols.values(): - if any(sym in observable_syms - for sym in obs['value'].free_symbols): + if any(sym in observable_syms for sym in obs["value"].free_symbols): raise ValueError( "Nested observables are not supported, " f"but {symbol_type} `{obs['name']} = {obs['value']}` " @@ -2556,20 +2632,28 @@ def _check_symbol_nesting(symbols: Dict[sp.Symbol, Dict[str, sp.Expr]], def _non_const_conservation_laws_supported(sbml_model: sbml.Model) -> bool: """Check whether non-constant conservation laws can be handled for the given model.""" - if any(rule.getTypeCode() == sbml.SBML_RATE_RULE - for rule in sbml_model.getListOfRules()): + if any( + rule.getTypeCode() == sbml.SBML_RATE_RULE + for rule in sbml_model.getListOfRules() + ): # see SBML semantic test suite, case 33 for an example - warnings.warn("Conservation laws for non-constant species in " - "models with RateRules are currently not supported " - "and will be turned off.") + warnings.warn( + "Conservation laws for non-constant species in " + "models with RateRules are currently not supported " + "and will be turned off." + ) return False - if any(rule.getTypeCode() == sbml.SBML_ASSIGNMENT_RULE and - sbml_model.getSpecies(rule.getVariable()) - for rule in sbml_model.getListOfRules()): - warnings.warn("Conservation laws for non-constant species in " - "models with Species-AssignmentRules are currently not " - "supported and will be turned off.") + if any( + rule.getTypeCode() == sbml.SBML_ASSIGNMENT_RULE + and sbml_model.getSpecies(rule.getVariable()) + for rule in sbml_model.getListOfRules() + ): + warnings.warn( + "Conservation laws for non-constant species in " + "models with Species-AssignmentRules are currently not " + "supported and will be turned off." + ) return False return True diff --git a/python/sdist/amici/sbml_utils.py b/python/sdist/amici/sbml_utils.py new file mode 100644 index 0000000000..cce2a6c4fa --- /dev/null +++ b/python/sdist/amici/sbml_utils.py @@ -0,0 +1,537 @@ +""" +SBML Utilities +-------------- +This module provides helper functions for working with SBML. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import sympy as sp + +if TYPE_CHECKING: + from typing import Any, Dict, Optional, Tuple, Union + + SbmlID = Union[str, sp.Symbol] + +import xml.dom.minidom + +import libsbml +from sympy.core.parameters import evaluate +from sympy.printing.mathml import MathMLContentPrinter + +from .import_utils import ( + SBMLException, + _check_unsupported_functions, + _parse_special_functions, + amici_time_symbol, + sbml_time_symbol, +) + + +class SbmlInvalidIdSyntax(SBMLException): + pass + + +class SbmlDuplicateComponentIdError(SBMLException): + pass + + +class SbmlMissingComponentIdError(SBMLException): + pass + + +class SbmlMathError(SBMLException): + pass + + +class SbmlAnnotationError(SBMLException): + pass + + +def create_sbml_model( + model_id: str, level: int = 2, version: int = 5 +) -> Tuple[libsbml.SBMLDocument, libsbml.Model]: + """Helper for creating an empty SBML model. + + :param model_id: + SBML ID of the new model. + + :param level: + Level of the new SBML document. + + :param version: + Version of the new SBML document. + + :return: + A tuple containing the newly created :py:class:`libsbml.SBMLDocument` + and :py:class:`libsbml.Model`. + """ + doc = libsbml.SBMLDocument(level, version) + model = doc.createModel() + model.setId(model_id) + return doc, model + + +def add_compartment( + model: libsbml.Model, + compartment_id: SbmlID, + *, + size: float = 1.0, +) -> libsbml.Species: + """Helper for adding a compartment to a SBML model. + + :param model: + SBML model to which the compartment is to be added. + + :param compartment_id: + SBML ID of the new compartment. + + :param size: + Size of the new compartment. Defaults to `1.0`. + + :return: + The new compartment as a :py:class:`libsbml.Compartment` object. + """ + compartment_id = str(compartment_id) + + # Check whether a compartment with the same ID already exists + # TODO the resulting SBML may still be invalid + # if other types of objects (e.g., parameter) have the same ID + if model.getCompartment(compartment_id): + raise SbmlDuplicateComponentIdError( + f"A compartment with ID {compartment_id} has already been defined" + ) + + cmp = model.createCompartment() + if cmp.setId(compartment_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{compartment_id} is not a valid SBML ID") + cmp.setSize(size) + + return cmp + + +def add_species( + model: libsbml.Model, + species_id: SbmlID, + *, + compartment_id: Optional[str] = None, + name: Union[bool, str] = False, + initial_amount: float = 0.0, + units: Optional[str] = None, +) -> libsbml.Species: + """Helper for adding a species to a SBML model. + + :param model: + SBML model to which the species is to be added. + + :param species_id: + SBML ID of the new species. + + :param compartment_id: + Compartment ID for the new species. + If there is only one compartment it can be auto-selected. + + :param initial_amount: + Initial amount of the new species. + + :param units: + Units attribute for the new species. + + :return: + The new species as a :py:class:`libsbml.Species` object. + """ + species_id = str(species_id) + if name is True: + name = species_id + + # Check whether an element with the same ID already exists + if model.getElementBySId(species_id): + raise SbmlDuplicateComponentIdError( + f"An element with ID {species_id} has already been defined." + ) + + if compartment_id is None: + compartments = model.getListOfCompartments() + if len(compartments) != 1: + raise ValueError( + "Compartment auto-selection is possible " + "only if there is one and only one compartment." + ) + compartment_id = compartments[0].getId() + elif not model.getCompartment(compartment_id): + raise SbmlMissingComponentIdError(f"No compartment with ID {compartment_id}.") + + sp = model.createSpecies() + if sp.setIdAttribute(species_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{species_id} is not a valid SBML ID.") + sp.setCompartment(compartment_id) + sp.setInitialAmount(float(initial_amount)) + if units is not None: + sp.setUnits(str(units)) + if isinstance(name, str): + sp.setName(name) + + return sp + + +def add_parameter( + model: libsbml.Model, + parameter_id: SbmlID, + *, + name: Union[bool, str] = False, + value: Optional[float] = None, + units: Optional[str] = None, + constant: Optional[bool] = None, +) -> libsbml.Parameter: + """Helper for adding a parameter to a SBML model. + + :param model: + SBML model to which the parameter is to be added. + + :param parameter_id: + SBML ID of the new parameter. + + :param name: + SBML name of the new parameter. + + :param value: + Value attribute for the new parameter. + + :param units: + Units attribute for the new parameter. + + :param constant: + Constant attribute for the new parameter. + + :return: + The new parameter as a :py:class:`libsbml.Parameter` object. + """ + parameter_id = str(parameter_id) + if name is True: + name = parameter_id + + # Check whether an element with the same ID already exists + if model.getElementBySId(parameter_id): + raise SbmlDuplicateComponentIdError( + f"An element with ID {parameter_id} has already been defined." + ) + + par = model.createParameter() + if par.setIdAttribute(parameter_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{parameter_id} is not a valid SBML ID.") + if units is not None: + par.setUnits(str(units)) + if constant is not None: + par.setConstant(bool(constant)) + if value is not None: + par.setValue(float(value)) + if isinstance(name, str): + par.setName(name) + + return par + + +def add_assignment_rule( + model: libsbml.Model, + variable_id: SbmlID, + formula, + rule_id: Optional[str] = None, +) -> libsbml.AssignmentRule: + """Helper for adding an assignment rule to a SBML model. + + :param model: + SBML model to which the assignment rule is to be added. + + :param variable_id: + SBML ID of the quantity for which the assignment rule is to be added. + + :param formula: + Formula for the assignment rule (it will be sympified). + + :param rule_id: + SBML ID of the new assignment rule. + Defaults to `'assignment_' + variableId`. + + :return: + The assignment rule as a :py:class:`libsbml.AssignmentRule` object. + """ + variable_id = str(variable_id) + if rule_id is None: + rule_id = "assignment_" + variable_id + + # Check whether rules exists for this parameter or with the same name + if model.getRuleByVariable(variable_id): + raise SbmlDuplicateComponentIdError( + f"A rule for parameter {variable_id} has already been defined." + ) + if model.getElementBySId(rule_id): + raise SbmlDuplicateComponentIdError( + f"An element with SBML ID {rule_id} has already been defined." + ) + + rule = model.createAssignmentRule() + if rule.setVariable(variable_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{variable_id} is not a valid SBML ID.") + if rule.setIdAttribute(rule_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{rule_id} is not a valid SBML ID.") + set_sbml_math(rule, formula) + + return rule + + +def add_rate_rule( + model: libsbml.Model, + variable_id: SbmlID, + formula, + rule_id: Optional[str] = None, +) -> libsbml.RateRule: + """ + Helper for adding a rate rule to a SBML model. + + :param model: + SBML model to which the rate rule is to be added. + + :param variable_id: + SBML ID of the quantity for which the rate rule is to be added. + + :param formula: + Formula for the rate rule (it will be sympified). + + :param rule_id: + SBML ID of the new rate rule. + Defaults to `'rate_' + variableId`. + + :return: + The new rate rule as a :py:class:`libsbml.RateRule` object. + """ + variable_id = str(variable_id) + if rule_id is None: + rule_id = "rate_" + variable_id + + # Check whether rules exists for this parameter or with the same name + if model.getRuleByVariable(variable_id): + raise SbmlDuplicateComponentIdError( + f"A rule for parameter {variable_id} has already been defined." + ) + if model.getElementBySId(rule_id): + raise SbmlDuplicateComponentIdError( + f"An element with SBML ID {rule_id} has already been defined." + ) + + rule = model.createRateRule() + if rule.setVariable(variable_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{variable_id} is not a valid SBML ID.") + if rule.setIdAttribute(rule_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{rule_id} is not a valid SBML ID.") + set_sbml_math(rule, formula) + + return rule + + +def add_inflow( + model: libsbml.Model, + species_id: SbmlID, + rate, + *, + reaction_id: Optional[str] = None, + reversible: bool = False, +) -> libsbml.Reaction: + species_id = str(species_id) + if reaction_id is None: + reaction_id = f"inflow_of_{species_id}" + + if model.getElementBySId(reaction_id): + raise SbmlDuplicateComponentIdError( + f"An element with SBML ID {reaction_id} has already been defined." + ) + + reaction = model.createReaction() + if reaction.setId(reaction_id) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlInvalidIdSyntax(f"{reaction_id} is not a valid SBML ID.") + reaction.setReversible(reversible) + + spr = reaction.createProduct() + spr.setSpecies(species_id) + + kl = reaction.createKineticLaw() + compartment_id = model.getSpecies(species_id).getCompartment() + set_sbml_math(kl, sp.Symbol(compartment_id) * rate) + + return reaction + + +def get_sbml_units( + model: libsbml.Model, x: Union[SbmlID, sp.Basic] +) -> Union[None, str]: + """Try to get the units for expression `x`. + + :param model: + SBML model. + :param x: + Expression to get the units of. + :return: + A string if the units could be determined, otherwise `None`. + """ + # TODO can the SBML unit inference machinery be used? + x = sp.sympify(x) + if not x.is_Symbol: + return None + if x.name == sbml_time_symbol.name: + if model.isSetTimeUnits(): + return model.getTimeUnits() + return None + par = model.getParameter(x.name) + if par is None: + return None + units = par.getUnits() + if units == "": + return None + return units + + +def pretty_xml(ugly_xml: str) -> str: + "Prettifies an XML document (given as a string)." + dom = xml.dom.minidom.parseString(ugly_xml) + pretty_xml = dom.toprettyxml() + # We must delete the first line (xml header) + return pretty_xml[pretty_xml.index("\n") + 1 :] + + +class MathMLSbmlPrinter(MathMLContentPrinter): + """Prints a SymPy expression to a MathML expression parsable by libSBML. + + Differences from `sympy.MathMLContentPrinter`: + 1. underscores in symbol names are not converted to subscripts + 2. symbols with name 'time' are converted to the SBML time symbol + """ + + def _print_Symbol(self, sym: sp.Symbol) -> xml.dom.minidom.Element: + ci = self.dom.createElement(self.mathml_tag(sym)) + ci.appendChild(self.dom.createTextNode(sym.name)) + return ci + + def doprint(self, expr, *, pretty: bool = False) -> str: + mathml = '' + mathml += super().doprint(expr) + mathml += "" + mathml = mathml.replace( + "time", + ' time ', + ) + return pretty_xml(mathml) if pretty else mathml + + +def sbml_mathml( + expr, *, replace_time: bool = False, pretty: bool = False, **settings +) -> str: + """Prints a SymPy expression to a MathML expression parsable by libSBML. + + :param expr: + expression to be converted to MathML (will be sympified). + + :param replace_time: + replace the AMICI time symbol with the SBML time symbol. + + :param pretty: + prettify the resulting MathML. + """ + with evaluate(False): + expr = sp.sympify(expr) + if replace_time: + expr = expr.subs(amici_time_symbol, sbml_time_symbol) + return MathMLSbmlPrinter(settings).doprint(expr, pretty=pretty) + + +def sbml_math_ast(expr, **kwargs) -> libsbml.ASTNode: + """Convert a SymPy expression to SBML math AST. + + :param expr: + expression to be converted (will be sympified). + + :param kwargs: + extra options for MathML conversion. + """ + mathml = sbml_mathml(expr, **kwargs) + ast = libsbml.readMathMLFromString(mathml) + if ast is None: + raise SbmlMathError( + f"error while converting the following expression to SBML AST.\n" + f"expression:\n{expr}\n" + f"MathML:\n{pretty_xml(mathml)}" + ) + return ast + + +def set_sbml_math(obj: libsbml.SBase, expr, **kwargs) -> None: + """Set the math attribute of a SBML node using a SymPy expression. + + :param obj: + SBML node supporting `setMath` method. + + :param expr: + expression to which the math attribute of `obj` should be se to + (will be sympified). + + :param kwargs: + extra options for MathML conversion. + """ + mathml = sbml_math_ast(expr, **kwargs) + if obj.setMath(mathml) != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlMathError( + f"Could not set math attribute of SBML object {obj}\n" + f"expression:\n{expr}\n" + f"MathML:\n{pretty_xml(mathml)}" + ) + + +def mathml2sympy( + mathml: str, + *, + evaluate: bool = False, + locals: Optional[Dict[str, Any]] = None, + expression_type: str = "mathml2sympy", +) -> sp.Basic: + ast = libsbml.readMathMLFromString(mathml) + if ast is None: + raise ValueError( + f"libSBML could not parse MathML string:\n{pretty_xml(mathml)}" + ) + + formula = _parse_logical_operators(libsbml.formulaToL3String(ast)) + + if evaluate: + expr = sp.sympify(formula, locals=locals) + else: + with sp.core.parameters.evaluate(False): + expr = sp.sympify(formula, locals=locals) + + expr = _parse_special_functions(expr) + + if expression_type is not None: + _check_unsupported_functions(expr, expression_type) + + return expr + + +def _parse_logical_operators( + math_str: Union[str, float, None] +) -> Union[str, float, None]: + """ + Parses a math string in order to replace logical operators by a form + parsable for sympy + + :param math_str: + str with mathematical expression + :param math_str: + parsed math_str + """ + if not isinstance(math_str, str): + return math_str + + if " xor(" in math_str or " Xor(" in math_str: + raise SBMLException("Xor is currently not supported as logical " "operation.") + + return (math_str.replace("&&", "&")).replace("||", "|") diff --git a/python/sdist/amici/setup.template.py b/python/sdist/amici/setup.template.py index 4eb7eca14a..e7995e2c52 100644 --- a/python/sdist/amici/setup.template.py +++ b/python/sdist/amici/setup.template.py @@ -2,11 +2,10 @@ import os from pathlib import Path -from cmake_build_extension import CMakeExtension -from setuptools import find_namespace_packages, setup - from amici import _get_amici_path from amici.custom_commands import AmiciBuildCMakeExtension +from cmake_build_extension import CMakeExtension +from setuptools import find_namespace_packages, setup def get_extension() -> CMakeExtension: @@ -18,15 +17,15 @@ def get_extension() -> CMakeExtension: # handle parallel building # Note: can be empty to use all hardware threads - if (parallel_jobs := os.environ.get('AMICI_PARALLEL_COMPILE')) is not None: - os.environ['CMAKE_BUILD_PARALLEL_LEVEL'] = parallel_jobs + if (parallel_jobs := os.environ.get("AMICI_PARALLEL_COMPILE")) is not None: + os.environ["CMAKE_BUILD_PARALLEL_LEVEL"] = parallel_jobs else: - os.environ['CMAKE_BUILD_PARALLEL_LEVEL'] = "1" + os.environ["CMAKE_BUILD_PARALLEL_LEVEL"] = "1" return CMakeExtension( - name='model_ext', + name="model_ext", source_dir=os.getcwd(), - install_prefix='TPL_MODELNAME', + install_prefix="TPL_MODELNAME", cmake_configure_options=[ "-DCMAKE_VERBOSE_MAKEFILE=ON", "-DCMAKE_MODULE_PATH=" @@ -44,34 +43,34 @@ def get_extension() -> CMakeExtension: MODEL_EXT = get_extension() CLASSIFIERS = [ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Science/Research', - 'Operating System :: POSIX :: Linux', - 'Operating System :: MacOS :: MacOS X', - 'Programming Language :: Python', - 'Programming Language :: C++', - 'Topic :: Scientific/Engineering :: Bio-Informatics', + "Development Status :: 3 - Alpha", + "Intended Audience :: Science/Research", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python", + "Programming Language :: C++", + "Topic :: Scientific/Engineering :: Bio-Informatics", ] CMDCLASS = { # for CMake-based builds - 'build_ext': AmiciBuildCMakeExtension, + "build_ext": AmiciBuildCMakeExtension, } # Install setup( - name='TPL_MODELNAME', + name="TPL_MODELNAME", cmdclass=CMDCLASS, - version='TPL_PACKAGE_VERSION', - description='AMICI-generated module for model TPL_MODELNAME', - url='https://github.com/AMICI-dev/AMICI', - author='model-author-todo', - author_email='model-author-todo', + version="TPL_PACKAGE_VERSION", + description="AMICI-generated module for model TPL_MODELNAME", + url="https://github.com/AMICI-dev/AMICI", + author="model-author-todo", + author_email="model-author-todo", ext_modules=[MODEL_EXT], packages=find_namespace_packages(), - install_requires=['amici==TPL_AMICI_VERSION'], - extras_require={'wurlitzer': ['wurlitzer']}, - python_requires='>=3.9', + install_requires=["amici==TPL_AMICI_VERSION"], + extras_require={"wurlitzer": ["wurlitzer"]}, + python_requires=">=3.9", package_data={}, zip_safe=False, classifiers=CLASSIFIERS, diff --git a/python/sdist/amici/splines.py b/python/sdist/amici/splines.py new file mode 100644 index 0000000000..bb82b692c6 --- /dev/null +++ b/python/sdist/amici/splines.py @@ -0,0 +1,1759 @@ +""" +Splines +------- +This module provides helper functions for reading/writing splines with AMICI +annotations from/to SBML files and for adding such splines to the AMICI C++ +code. +""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from numbers import Real + from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union + + from . import sbml_import + + BClike = Union[None, str, Tuple[Union[None, str], Union[None, str]]] + + NormalizedBC = Tuple[Union[None, str], Union[None, str]] + +import collections.abc +import logging +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod +from itertools import count +from numbers import Integral + +import libsbml +import numpy as np +import sympy as sp +from sympy.core.parameters import evaluate + +from .import_utils import ( + amici_time_symbol, + annotation_namespace, + sbml_time_symbol, + symbol_with_assumptions, +) +from .logging import get_logger +from .sbml_utils import ( + SbmlAnnotationError, + add_assignment_rule, + add_parameter, + get_sbml_units, + mathml2sympy, + pretty_xml, + sbml_mathml, +) + +logger = get_logger(__name__, logging.WARNING) + + +def sympify_noeval(x): + with evaluate(False): + return sp.sympify(x) + + +############################################################################### + + +class UniformGrid(collections.abc.Sequence): + """ + A grid of uniformly-spaced real points, computed with rational arithmetic. + + Implements the :py:class:`collections.abc.Sequence` interface and can be + converted to a :py:class:`numpy.ndarray` (conversion to float can be + specified with ``dtype=float``). + + :ivar start: first point. + + :ivar stop: last point. + + :ivar step: distance between consecutive points. + + :ivar number_of_nodes: number of grid nodes. + """ + + def __init__( + self, + start: Union[Real, sp.Basic], + stop: Union[Real, sp.Basic], + step: Optional[Union[Real, sp.Basic]] = None, + *, + number_of_nodes: Optional[Integral] = None, + always_include_stop: bool = True, + ): + """Create a new ``UniformGrid``. + + Note: A ``UniformGrid`` with a single node cannot be created. + + :param start: + First point in the grid + :param stop: + Last point in the grid (some caveats apply, see ``always_include_stop``) + :param step: + Desired step size of the grid. Mutually exclusive with ``number_of_nodes``. + :param number_of_nodes: + Number of grid nodes, i.e., the length of the grid. + It must be greater than or equal to 2. + Mutually exclusive with ``step``. + :param always_include_stop: + Controls the behaviour when ``step`` is not ``None``. + If ``True`` (default), the endpoint is the smallest + ``start + k * step``, with ``k`` integer, which is + greater than or equal to ``stop``. + Otherwise, the endpoint is the largest + ``start + k * step``, with ``k`` integer, which is + smaller than or equal to ``stop``. + """ + start = sp.nsimplify(sp.sympify(start)) + stop = sp.nsimplify(sp.sympify(stop)) + if step is None: + if number_of_nodes is None: + raise ValueError("One of step/number_of_nodes must be specified!") + if not isinstance(number_of_nodes, Integral): + raise TypeError("Length must be an integer!") + if number_of_nodes < 2: + raise ValueError("Length must be at least 2!") + step = (stop - start) / (number_of_nodes - 1) + elif number_of_nodes is not None: + raise ValueError("Only one of step/number_of_nodes can be specified!") + else: + step = sp.nsimplify(sp.sympify(step)) + + if start > stop: + raise ValueError(f"Start point {start} greater than stop point {stop}!") + + if step <= 0: + raise ValueError(f"Step size {step} must be strictly positive!") + + xx = [] + for i in count(): + x = start + i * step + if not always_include_stop and x > stop: + break + xx.append(x) + if always_include_stop and x >= stop: + break + + if len(xx) == 1: + raise ValueError( + f"Step size {step} is less than (stop - start) = {stop - start} " + "and always_include_stop is set to False, " + "leading to a UniformGrid with a single node, " + "which is unsupported!" + ) + + self._xx = np.asarray(xx) + + @property + def start(self) -> sp.Basic: + """First point.""" + return self._xx[0] + + @property + def stop(self) -> sp.Basic: + """Last point.""" + return self._xx[-1] + + @property + def step(self) -> sp.Basic: + """Distance between consecutive points.""" + return self._xx[1] - self._xx[0] + + @property + def number_of_nodes(self) -> sp.Basic: + """Number of grid nodes.""" + return len(self._xx) + + def __getitem__(self, i: Integral) -> sp.Basic: + return self._xx[i] + + def __len__(self) -> int: + return len(self._xx) + + def __array__(self, dtype=None) -> np.ndarray: + if dtype is None: + return self._xx + return np.array(self._xx, dtype=dtype) + + def __repr__(self) -> str: + return ( + f"UniformGrid(start={self.start}, stop={self.stop}, " f"step={self.step})" + ) + + +############################################################################### + + +class AbstractSpline(ABC): + """ + Base class for spline functions which can be computed efficiently + thanks to tailored C++ implementations in AMICI. + Inside an SBML file, such splines are implemented with + an assignment rule containing both a symbolic piecewise formula + for the spline (allowing compatibility with any SBML-aware software) + and annotations which encode the necessary information for AMICI to + recreate the spline object (allowing for fast computations when the SBML + file is used together with AMICI). + """ + + def __init__( + self, + sbml_id: Union[str, sp.Symbol], + nodes: Sequence, + values_at_nodes: Sequence, + *, + evaluate_at: Optional[Union[str, sp.Basic]] = None, + bc: BClike = None, + extrapolate: BClike = None, + logarithmic_parametrization: bool = False, + ): + """Base constructor for ``AbstractSpline`` objects. + + :param sbml_id: + The SBML ID of the parameter associated to the spline + as a string or a SymPy symbol. + + :param nodes: + The points at which the spline values are known. + Currently, they must be numeric or only depend on constant parameters. + These points should be strictly increasing. + This argument will be sympified. + + :param values_at_nodes: + The spline values at each of the points in ``nodes``. + They must not depend on model species. + This argument will be sympified. + + :param evaluate_at: + The point at which the spline is evaluated. + It will be sympified. + Defaults to model time. + + :param bc: + Tuple of applied boundary conditions, one for each side of the + spline domain. If a single boundary condition is given it will be + applied to both sides. + Possible boundary conditions + (allowed values depend on the ``AbstractSpline`` subclass): + + `None` or `'no_bc'`: + Boundary conditions are not needed for this spline object; + `'zeroderivative'`: + first derivative set to zero; + `'natural'`: + second derivative set to zero; + `'zeroderivative+natural'`: + first and second derivatives set to zero; + `'periodic'`: + periodic bc. + + :param extrapolate: + Whether to extrapolate the spline outside the base interval + defined by ``(nodes[0], nodes[-1])``. + It is a tuple of extrapolation methods, one for each side of the + base interval. + If it is not a tuple, then the same extrapolation will be applied + on both sides. + Extrapolation methods supported: + + `None` or `'no_extrapolation'`: + no extrapolation should be performed. An exception will be + raised in the C++ code if the spline is evaluated outside the + base interval. In the fallback SBML symbolic expression + `'polynomial'` extrapolation will be used. + `'polynomial'`: + the cubic polynomial used in the nearest spline segment will be + used. + `'constant'`: + constant extrapolation will be used. + Requires `'zeroderivative'` boundary condition. + For splines which are continuous up to the second derivative, + it requires the stricter `'zeroderivative+natural'` + boundary condition. + `'linear'`: + linear extrapolation will be used. + For splines which are continuous up to the second derivative, + this requires the `'natural'` boundary condition. + `'periodic'`: + Periodic extrapolation. Requires `'periodic'` boundary + conditions. + + :param logarithmic_parametrization: + Whether interpolation should be done in log-scale. + """ + + if isinstance(sbml_id, str): + sbml_id = symbol_with_assumptions(sbml_id) + elif not isinstance(sbml_id, sp.Symbol): + raise TypeError( + "sbml_id must be either a string or a SymPy symbol, " + f"got {sbml_id} of type {type(sbml_id)} instead!" + ) + + if evaluate_at is None: + evaluate_at = amici_time_symbol + else: + evaluate_at = sympify_noeval(evaluate_at) + if not isinstance(evaluate_at, sp.Basic): + # It may still be e.g. a list! + raise ValueError(f"Invalid evaluate_at = {evaluate_at}!") + if evaluate_at != amici_time_symbol and evaluate_at != sbml_time_symbol: + logger.warning( + "At the moment AMICI only supports evaluate_at = (model time). " + "Annotations with correct piecewise MathML formulas " + "can still be created and used in other tools, " + "but they will raise an error when imported by AMICI." + ) + + if not isinstance(nodes, UniformGrid): + nodes = np.asarray([sympify_noeval(x) for x in nodes]) + values_at_nodes = np.asarray([sympify_noeval(y) for y in values_at_nodes]) + + if len(nodes) != len(values_at_nodes): + raise ValueError( + "Length of nodes and values_at_nodes must be the same " + f"(instead len(nodes) = {len(nodes)} and len(values_at_nodes) = {len(values_at_nodes)})!" + ) + + if all(x.is_Number for x in nodes) and not np.all(np.diff(nodes) >= 0): + raise ValueError("nodes should be strictly increasing!") + + if ( + logarithmic_parametrization + and all(y.is_Number for y in values_at_nodes) + and any(y <= 0 for y in values_at_nodes) + ): + raise ValueError( + "When interpolation is done in log-scale, " + "values_at_nodes should be strictly positive!" + ) + + bc, extrapolate = self._normalize_bc_and_extrapolate(bc, extrapolate) + if bc == ("periodic", "periodic") and values_at_nodes[0] != values_at_nodes[-1]: + raise ValueError( + "If the spline is to be periodic, " + "the first and last elements of values_at_nodes must be equal!" + ) + + self._sbml_id: sp.Symbol = sbml_id + self._evaluate_at = evaluate_at + self._nodes = nodes + self._values_at_nodes = values_at_nodes + self._bc = bc + self._extrapolate = extrapolate + self._logarithmic_parametrization = logarithmic_parametrization + self._formula_cache = {} + + def _normalize_bc_and_extrapolate(self, bc: BClike, extrapolate: BClike): + bc = AbstractSpline._normalize_bc(bc) + return self._normalize_extrapolate(bc, extrapolate) + + @staticmethod + def _normalize_bc(bc: BClike) -> NormalizedBC: + """ + Preprocess the boundary condition `bc` to a standard form. + """ + if not isinstance(bc, tuple): + bc = (bc, bc) + elif len(bc) != 2: + raise TypeError(f"bc should be a 2-tuple, got {bc} instead!") + + bc = list(bc) + + valid_bc = ( + "periodic", + "zeroderivative", + "zeroderivative+natural", + "natural", + "no_bc", + "auto", + None, + ) + + for i in (0, 1): + if bc[i] not in valid_bc: + raise ValueError( + f"Unsupported bc = {bc[i]}! " + f"The currently supported bc methods are: {valid_bc}" + ) + elif bc[i] == "no_bc": + bc[i] = None + + if (bc[0] == "periodic" or bc[1] == "periodic") and bc[0] != bc[1]: + raise ValueError( + "If the bc on one side is periodic, " + "then the bc on the other side must be periodic too!" + ) + + return bc[0], bc[1] + + def _normalize_extrapolate( + self, bc: NormalizedBC, extrapolate: BClike + ) -> Tuple[NormalizedBC, NormalizedBC]: + """ + Preprocess `extrapolate` to a standard form + and perform consistency checks + """ + if not isinstance(extrapolate, tuple): + extrapolate = (extrapolate, extrapolate) + elif len(extrapolate) != 2: + raise TypeError( + f"extrapolate should be a 2-tuple, got {extrapolate} instead!" + ) + extrapolate = list(extrapolate) + + if not isinstance(bc, tuple) or len(bc) != 2: + raise TypeError(f"bc should be a 2-tuple, got {bc} instead!") + bc = list(bc) + + valid_extrapolate = ( + "no_extrapolation", + "constant", + "linear", + "polynomial", + "periodic", + None, + ) + + for i in (0, 1): + if extrapolate[i] not in valid_extrapolate: + raise ValueError( + f"Unsupported extrapolate= {extrapolate[i]}!" + + " The currently supported extrapolation methods are: " + + str(valid_extrapolate) + ) + + if extrapolate[i] == "no_extrapolation": + extrapolate[i] = None + + if extrapolate[i] == "periodic": + if bc[0] == "auto": + bc[0] = "periodic" + if bc[1] == "auto": + bc[1] = "periodic" + if not (bc[0] == bc[1] == "periodic"): + raise ValueError( + "The spline must satisfy periodic boundary conditions " + "on both sides of the base interval " + "in order for periodic extrapolation to be used!" + ) + + elif extrapolate[i] == "constant": + assert self.smoothness > 0 + if self.smoothness == 1: + if bc[i] == "auto": + bc[i] = "zeroderivative" + elif bc[i] != "zeroderivative": + raise ValueError( + "The spline must satisfy zero-derivative bc " + "in order for constant extrapolation to be used!" + ) + elif bc[i] == "auto": + bc[i] = "zeroderivative+natural" + elif bc[i] != "zeroderivative+natural": + raise ValueError( + "The spline must satisfy zero-derivative and natural" + " bc in order for constant extrapolation to be used!" + ) + + elif extrapolate[i] == "linear": + assert self.smoothness > 0 + if self.smoothness > 1: + if bc[i] == "auto": + bc[i] = "natural" + elif bc[i] != "natural": + raise ValueError( + "The spline must satisfy natural bc " + "in order for linear extrapolation to be used!" + ) + elif bc[i] == "auto": + bc[i] = None + + elif bc[i] == "auto": + bc[i] = None + + if ( + (extrapolate[0] == "periodic" or extrapolate[1] == "periodic") + and extrapolate[0] != extrapolate[1] + and extrapolate[0] is not None + and extrapolate[1] is not None + ): + raise NotImplementedError( + "At the moment if periodic extrapolation is applied " + "to one side, the extrapolation at the other side " + "must either be periodic or not be applied " + "(in which case it will be periodic anyway)." + ) + + return (bc[0], bc[1]), (extrapolate[0], extrapolate[1]) + + @property + def sbml_id(self) -> sp.Symbol: + """SBML ID of the spline parameter.""" + return self._sbml_id + + @property + def evaluate_at(self) -> sp.Basic: + """The symbolic argument at which the spline is evaluated.""" + return self._evaluate_at + + @property + def nodes(self) -> np.ndarray: + """The points at which the spline values are known.""" + return self._nodes + + @property + def values_at_nodes(self) -> np.ndarray: + """The spline values at each of the points in ``nodes``.""" + return self._values_at_nodes + + @property + def bc(self) -> NormalizedBC: + """Boundary conditions applied to this spline.""" + return self._bc + + @property + def extrapolate(self) -> NormalizedBC: + """Whether to extrapolate the spline outside the base interval.""" + return self._extrapolate + + @property + def logarithmic_parametrization(self) -> bool: + """Whether interpolation is done in log-scale.""" + return self._logarithmic_parametrization + + @property + @abstractmethod + def smoothness(self) -> int: + """Smoothness of this spline.""" + raise NotImplementedError() + + @property + @abstractmethod + def method(self) -> str: + """Spline method.""" + raise NotImplementedError() + + def check_if_valid(self, importer: sbml_import.SbmlImporter) -> None: + """ + Check if the spline described by this object can be correctly + be implemented by AMICI. E.g., check whether the formulas + for spline grid points, values, ... contain species symbols. + """ + # At the moment only basic checks are done. + # There may still be some edge cases that break + # the AMICI spline implementation. + # If found, they should be checked for here + # until (if at all) they are accounted for. + from .de_export import SymbolId + + fixed_parameters: List[sp.Symbol] = list( + importer.symbols[SymbolId.FIXED_PARAMETER].keys() + ) + species: List[sp.Symbol] = list(importer.symbols[SymbolId.SPECIES].keys()) + + for x in self.nodes: + if not x.free_symbols.issubset(fixed_parameters): + raise ValueError("nodes should only depend on constant parameters!") + + for y in self.values_at_nodes: + if y.free_symbols.intersection(species): + raise ValueError("values_at_nodes should not depend on model species!") + + fixed_parameters_values = [ + importer.symbols[SymbolId.FIXED_PARAMETER][fp]["value"] + for fp in fixed_parameters + ] + subs = dict(zip(fixed_parameters, fixed_parameters_values)) + nodes_values = [sp.simplify(x.subs(subs)) for x in self.nodes] + for x in nodes_values: + assert x.is_Number + if not np.all(np.diff(nodes_values) >= 0): + raise ValueError("nodes should be strictly increasing!") + + def poly(self, i: Integral, *, x: Union[Real, sp.Basic] = None) -> sp.Basic: + """ + Get the polynomial interpolant on the ``(nodes[i], nodes[i+1])`` interval. + The polynomial is written in Horner form with respect to the scaled + variable ``poly_variable(x, i)``. + If no variable ``x`` is provided, it will default to the one given at + initialization time. + """ + if i < 0: + i += len(self.nodes) - 1 + + if not 0 <= i < len(self.nodes) - 1: + raise ValueError(f"Interval index {i} is out of bounds!") + + if x is None: + x = self.evaluate_at + + # Compute polynomial in Horner form for the scaled variable + t = sp.Dummy("t") + poly = self._poly(t, i).expand().as_poly(wrt=t, domain=sp.RR) + + # Rewrite in Horner form + # NB any coefficient containing functions must be rewritten for some reason + subs = {} + reverse_subs = {} + for s in poly.args[2:]: + if not s.is_Symbol: + wild = sp.Dummy() + subs[s] = wild + reverse_subs[wild] = s + poly = sp.horner(poly.subs(subs)).subs(reverse_subs) + + # Replace scaled variable with its value, + # without changing the expression form + t_value = self._poly_variable(x, i) + with evaluate(False): + return poly.subs(t, t_value) + + def poly_variable(self, x: Union[Real, sp.Basic], i: Integral) -> sp.Basic: + """ + Given an evaluation point, return the value of the variable + in which the polynomial on the ``i``-th interval is expressed. + """ + if not 0 <= i < len(self.nodes) - 1: + raise ValueError(f"Interval index {i} is out of bounds!") + return self._poly_variable(x, i) + + @abstractmethod + def _poly_variable(self, x: Union[Real, sp.Basic], i: Integral) -> sp.Basic: + """This function (and not poly_variable) should be implemented by the + subclasses""" + raise NotImplementedError() + + @abstractmethod + def _poly(self, t: Union[Real, sp.Basic], i: Integral) -> sp.Basic: + """ + Return the symbolic expression for the spline restricted to the `i`-th + interval as a polynomial in the scaled variable `t`. + """ + raise NotImplementedError() + + def segment_formula( + self, i: Integral, *, x: Union[Real, sp.Basic] = None + ) -> sp.Basic: + """ + Return the formula for the actual value of the spline expression + on the ``(nodes[i], nodes[i+1])`` interval. + Unless logarithmic parametrization is used, + this is equal to the interpolating polynomial. + """ + if x is None: + x = self.evaluate_at + poly = self.poly(i, x=x) + if self.logarithmic_parametrization: + return sp.exp(poly) + return poly + + def y_scaled(self, i: Integral): + """ + Return the values which should be interpolated by a polynomial. + Unless logarithmic parametrization is used, + they are equal to the values given at initialization time. + """ + if self.logarithmic_parametrization: + return sp.log(self.values_at_nodes[i]) + return self.values_at_nodes[i] + + @property + def extrapolation_formulas( + self, + ) -> Tuple[Union[None, sp.Basic], Union[None, sp.Basic]]: + """ + Returns the extrapolation formulas on the left and right side + of the interval ``(nodes[0], nodes[-1])``. + A value of ``None`` means that no extrapolation is required. + """ + return self._extrapolation_formulas(self.evaluate_at) + + def _extrapolation_formulas( + self, + x: Union[Real, sp.Basic], + extrapolate: Optional[NormalizedBC] = None, + ) -> Tuple[Union[None, sp.Expr], Union[None, sp.Expr]]: + if extrapolate is None: + extr_left, extr_right = self.extrapolate + else: + extr_left, extr_right = extrapolate + + if extr_left == "constant": + extr_left = self.values_at_nodes[0] + elif extr_left == "linear": + dx = x - self.nodes[0] + dydx = self.derivative(self.nodes[0], extrapolate=None) + extr_left = self.values_at_nodes[0] + dydx * dx + elif extr_left == "polynomial": + extr_left = None + else: + assert extr_left is None + + if extr_right == "constant": + extr_right = self.values_at_nodes[-1] + elif extr_right == "linear": + dx = x - self.nodes[-1] + dydx = self.derivative(self.nodes[-1], extrapolate=None) + extr_right = self.values_at_nodes[-1] + dydx * dx + elif extr_right == "polynomial": + extr_right = None + else: + assert extr_right is None + + return extr_left, extr_right + + @property + def formula(self) -> sp.Piecewise: + """ + Compute a symbolic piecewise formula for the spline. + """ + return self._formula(sbml_syms=False, sbml_ops=False) + + @property + def sbml_formula(self) -> sp.Piecewise: + """ + Compute a symbolic piecewise formula for the spline, + using SBML symbol naming + (the AMICI time symbol will be replaced with its SBML counterpart). + """ + return self._formula(sbml_syms=True, sbml_ops=False) + + @property + def mathml_formula(self) -> sp.Piecewise: + """ + Compute a symbolic piecewise formula for the spline for use inside + a SBML assignment rule: SBML symbol naming will be used + and operations not supported by SBML MathML will be avoided. + """ + return self._formula(sbml_syms=True, sbml_ops=True) + + def _formula( + self, + *, + x: Union[Real, sp.Basic] = None, + sbml_syms: bool = False, + sbml_ops: bool = False, + cache: bool = True, + **kwargs, + ) -> sp.Piecewise: + # Cache formulas in the case they are reused + if cache: + if "extrapolate" in kwargs.keys(): + key = (x, sbml_syms, sbml_ops, kwargs["extrapolate"]) + else: + key = (x, sbml_syms, sbml_ops) + if key in self._formula_cache.keys(): + return self._formula_cache[key] + + if x is None: + x = self.evaluate_at + if "extrapolate" in kwargs.keys(): + _bc, extrapolate = self._normalize_extrapolate( + self.bc, kwargs["extrapolate"] + ) + assert self.bc == _bc + else: + extrapolate = self.extrapolate + + pieces = [] + + if extrapolate[0] == "periodic" or extrapolate[1] == "periodic": + if sbml_ops: + # NB mod is not supported in SBML + x = symbol_with_assumptions( + self.sbml_id.name + "_x_in_fundamental_period" + ) + # NB we will do the parameter substitution in SBML + # because the formula for x will be a piecewise + # and sympy handles Piecewises inside other Piecewises + # really badly. + else: + x = self._to_base_interval(x) + extr_left, extr_right = None, None + else: + extr_left, extr_right = self._extrapolation_formulas(x, extrapolate) + + if extr_left is not None: + pieces.append((extr_left, x < self.nodes[0])) + + for i in range(len(self.nodes) - 2): + pieces.append((self.segment_formula(i, x=x), x < self.nodes[i + 1])) + + if extr_right is not None: + pieces.append((self.segment_formula(-1, x=x), x < self.nodes[-1])) + pieces.append((extr_right, sp.sympify(True))) + else: + pieces.append((self.segment_formula(-1, x=x), sp.sympify(True))) + + with evaluate(False): + if sbml_syms: + pieces = [ + ( + p.subs(amici_time_symbol, sbml_time_symbol), + c.subs(amici_time_symbol, sbml_time_symbol), + ) + for (p, c) in pieces + ] + formula = sp.Piecewise(*pieces) + + if cache: + self._formula_cache[key] = formula + return formula + + @property + def period(self) -> Union[sp.Basic, None]: + """Period of a periodic spline. `None` if the spline is not periodic.""" + if self.bc == ("periodic", "periodic"): + return self.nodes[-1] - self.nodes[0] + return None + + def _to_base_interval( + self, x: Union[Real, sp.Basic], *, with_interval_number: bool = False + ) -> Union[sp.Basic, Tuple[sp.core.numbers.Integer, sp.Basic]]: + """For periodic splines, maps the real point `x` to the reference + period.""" + if self.bc != ("periodic", "periodic"): + raise ValueError("_to_base_interval makes no sense with non-periodic bc") + + xA = self.nodes[0] + xB = self.nodes[-1] + T = self.period + z = xA + sp.Mod(x - xA, T) + assert not z.is_Number or xA <= z < xB + + if with_interval_number: + k = sp.floor((x - xA) / T) + assert isinstance(k, sp.core.numbers.Integer) + assert x == z + k * T + return k, z + return z + + def evaluate(self, x: Union[Real, sp.Basic]) -> sp.Basic: + """Evaluate the spline at the point `x`.""" + _x = sp.Dummy("x") + return self._formula(x=_x, cache=False).subs(_x, x) + + def derivative(self, x: Union[Real, sp.Basic], **kwargs) -> sp.Expr: + """Evaluate the spline derivative at the point `x`.""" + # NB kwargs are used to pass on extrapolate=None + # when called from .extrapolation_formulas() + _x = sp.Dummy("x") + return self._formula(x=_x, cache=False, **kwargs).diff(_x).subs(_x, x) + + def second_derivative(self, x: Union[Real, sp.Basic]) -> sp.Basic: + """Evaluate the spline second derivative at the point `x`.""" + _x = sp.Dummy("x") + return self._formula(x=_x, cache=False).diff(_x).diff(_x).subs(_x, x) + + def squared_L2_norm_of_curvature(self) -> sp.Basic: + """ + Return the squared L2 norm of the spline's curvature + (commonly used as a regularizer). + This is always computed in the spline native scale + (i.e., in log-scale for positivity enforcing splines). + """ + x = sp.Dummy("x") + integral = sp.sympify(0) + for i in range(len(self.nodes) - 1): + formula = self.poly(i, x=x).diff(x, 2) ** 2 + integral += sp.integrate(formula, (x, self.nodes[i], self.nodes[i + 1])) + return sp.simplify(integral) + + def integrate( + self, x0: Union[Real, sp.Basic], x1: Union[Real, sp.Basic] + ) -> sp.Basic: + """Integrate the spline between the points `x0` and `x1`.""" + x = sp.Dummy("x") + x0, x1 = sp.sympify((x0, x1)) + + if x0 > x1: + raise ValueError("x0 > x1") + + if x0 == x1: + return sp.sympify(0) + + if self.extrapolate != ("periodic", "periodic"): + return self._formula(x=x, cache=False).integrate((x, x0, x1)) + + formula = self._formula(x=x, cache=False, extrapolate=None) + + xA, xB = self.nodes[0], self.nodes[-1] + k0, z0 = self._to_base_interval(x0, with_interval_number=True) + k1, z1 = self._to_base_interval(x1, with_interval_number=True) + + assert k0 <= k1 + + if k0 == k1: + return formula.integrate((x, z0, z1)) + + if k0 + 1 == k1: + return formula.integrate((x, z0, xB)) + formula.integrate((x, xA, z1)) + + return ( + formula.integrate((x, z0, xB)) + + (k1 - k0 - 1) * formula.integrate((x, xA, xB)) + + formula.integrate((x, xA, z1)) + ) + + @property + def amici_annotation(self) -> str: + """An SBML annotation describing the spline.""" + annotation = f'" + for gc in grandchildren: + annotation += gc + annotation += f"" + + annotation += "" + + # Check XML and prettify + return pretty_xml(annotation) + + def _annotation_attributes(self) -> Dict[str, Any]: + attributes = {"spline_method": self.method} + + if self.bc[0] == self.bc[1]: + if self.bc[0] is not None: + attributes["spline_bc"] = self.bc[0] + else: + bc1, bc2 = self.bc + bc1 = "no_bc" if bc1 is None else bc1 + bc2 = "no_bc" if bc2 is None else bc2 + attributes["spline_bc"] = f"({bc1}, {bc2})" + + if self.extrapolate[0] == self.extrapolate[1]: + extr = None if self.extrapolate is None else self.extrapolate[0] + else: + extr1, extr2 = self.extrapolate + extr1 = "no_extrapolation" if extr1 is None else extr1 + extr2 = "no_extrapolation" if extr2 is None else extr2 + extr = f"({extr1}, {extr2})" + if extr is not None: + attributes["spline_extrapolate"] = extr + + if self.logarithmic_parametrization: + attributes["spline_logarithmic_parametrization"] = True + + return attributes + + def _annotation_children(self) -> Dict[str, Union[str, List[str]]]: + children = {} + + with evaluate(False): + x = self.evaluate_at.subs(amici_time_symbol, sbml_time_symbol) + children["spline_evaluation_point"] = sbml_mathml(x) + + if isinstance(self.nodes, UniformGrid): + children["spline_uniform_grid"] = [ + sbml_mathml(self.nodes.start), + sbml_mathml(self.nodes.stop), + sbml_mathml(self.nodes.step), + ] + else: + for x in self.nodes: + assert amici_time_symbol not in x.free_symbols + children["spline_grid"] = [sbml_mathml(x) for x in self.nodes] + + children["spline_values"] = [sbml_mathml(y) for y in self.values_at_nodes] + + return children + + def add_to_sbml_model( + self, + model: libsbml.Model, + *, + auto_add: Union[bool, str] = False, + x_nominal: Optional[Sequence[float]] = None, + y_nominal: Optional[Union[Sequence[float], float]] = None, + x_units: Optional[str] = None, + y_units: Optional[str] = None, + y_constant: Optional[Union[Sequence[bool], bool]] = None, + ) -> None: + """ + Function to add the spline to an SBML model using an assignment rule + with AMICI-specific annotations. + + :param model: + A :py:class:`libsbml.Model` to which the spline is to be added. + + :param auto_add: + Automatically add missing parameters to the SBML model + (defaults to `False`). + Only used for expressions consisting in a single symbol. + If equal to `'spline'`, + only the parameter representing the spline will be added. + + :param x_nominal: + Nominal values used when auto-adding parameters for `nodes`. + + :param y_nominal: + Nominal values used when auto-adding parameters for `values_at_nodes`. + + :param x_units: + Units used when auto-adding parameters for `nodes`. + + :param y_units: + Units used when auto-adding parameters for `values_at_nodes`. + + :param y_constant: + Constant flags used when auto-adding parameters for `values_at_nodes`. + """ + # Convert time from AMICI to SBML naming + with evaluate(False): + x = self.evaluate_at.subs(amici_time_symbol, sbml_time_symbol) + + # Try to auto-determine units + if x_units is None: + x_units = get_sbml_units(model, x) + for _x in self.nodes: + if x_units is not None: + break + x_units = get_sbml_units(model, _x) + if y_units is None: + for _y in self.values_at_nodes: + y_units = get_sbml_units(model, _y) + if y_units is not None: + break + + # Autoadd parameters + if auto_add is True or auto_add == "spline": + if not model.getParameter(str(self.sbml_id)) and not model.getSpecies( + str(self.sbml_id) + ): + add_parameter(model, self.sbml_id, constant=False, units=y_units) + + if auto_add is True: + if isinstance(x_nominal, collections.abc.Sequence): + if len(x_nominal) != len(self.nodes): + raise ValueError( + "If x_nominal is a list, then it must have " + "the same length as the spline grid!" + ) + for i in range(len(x_nominal) - 1): + if x[i] >= x[i + 1]: + raise ValueError("x_nominal must be strictly increasing!") + elif x_nominal is None: + x_nominal = len(self.nodes) * [None] + else: + # 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): + if _x.is_Symbol and not model.getParameter(_x.name): + add_parameter(model, _x.name, value=_val, units=x_units) + + if isinstance(y_nominal, collections.abc.Sequence): + if len(y_nominal) != len(self.values_at_nodes): + raise ValueError( + "If y_nominal is a list, then it must have " + "the same length as the spline values!" + ) + else: + y_nominal = len(self.values_at_nodes) * [y_nominal] + if isinstance(y_constant, collections.abc.Sequence): + if len(y_constant) != len(self.values_at_nodes): + raise ValueError( + "If y_constant is a list, then it must have " + "the same length as the spline values!" + ) + else: + y_constant = len(self.values_at_nodes) * [y_constant] + for _y, _val, _const in zip( + self.values_at_nodes, y_nominal, y_constant + ): + if _y.is_Symbol and not model.getParameter(_y.name): + add_parameter( + model, + _y.name, + value=_val, + constant=_const, + units=y_units, + ) + + elif auto_add is not False: + raise ValueError(f"Invalid value {auto_add} for auto_add!") + + # Create assignment rule for spline + rule = add_assignment_rule(model, self.sbml_id, self.mathml_formula) + + # Add annotation specifying spline method + retcode = rule.setAnnotation(self.amici_annotation) + if retcode != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlAnnotationError("Could not set SBML annotation!") + + # Create additional assignment rule for periodic extrapolation + # TODO is supported in SBML Level 3 (but not in Level 2). + # Consider simplifying the formulas using it + # (after checking it actually works as expected), + # checking what level the input SBML model is. + if any(extr == "periodic" for extr in self.extrapolate): + parameter_id = self.sbml_id.name + "_x_in_fundamental_period" + T = self.nodes[-1] - self.nodes[0] + x0 = self.nodes[0] + s = 2 * sp.pi * ((x - x0) / T - sp.sympify(1) / 4) + k = sp.Piecewise((3, sp.cos(s) < 0), (1, True)) + formula = x0 + T * (sp.atan(sp.tan(s)) / (2 * sp.pi) + k / 4) + assert amici_time_symbol not in formula.free_symbols + par = add_parameter(model, parameter_id, constant=False, units=x_units) + retcode = par.setAnnotation( + f'' + ) + if retcode != libsbml.LIBSBML_OPERATION_SUCCESS: + raise SbmlAnnotationError("Could not set SBML annotation!") + add_assignment_rule(model, parameter_id, formula) + + def _replace_in_all_expressions(self, old: sp.Symbol, new: sp.Symbol) -> None: + if self.sbml_id == old: + self._sbml_id = new + self._x = self.evaluate_at.subs(old, new) + if not isinstance(self.nodes, UniformGrid): + self._nodes = [x.subs(old, new) for x in self.nodes] + self._values_at_nodes = [y.subs(old, new) for y in self.values_at_nodes] + + @staticmethod + def is_spline(rule: libsbml.AssignmentRule) -> bool: + """ + Determine if an SBML assignment rule (given as a + :py:class:`libsbml.AssignmentRule` object) is an AMICI-annotated + spline formula. + """ + return AbstractSpline.get_annotation(rule) is not None + + @staticmethod + def get_annotation(rule: libsbml.AssignmentRule) -> Union[ET.Element, None]: + """ + Extract AMICI spline annotation from an SBML assignment rule + (given as a :py:class:`libsbml.AssignmentRule` object). + Return ``None`` if any such annotation could not be found. + """ + if not isinstance(rule, libsbml.AssignmentRule): + raise TypeError("Rule must be an AssignmentRule!") + if rule.isSetAnnotation(): + annotation = ET.fromstring(rule.getAnnotationString()) + for child in annotation: + if child.tag == f"{{{annotation_namespace}}}spline": + return child + return None + + @staticmethod + def from_annotation( + sbml_id: sp.Symbol, + annotation: ET.Element, + *, + locals_: Dict[str, Any], + ) -> AbstractSpline: + """Create a spline object from a SBML annotation. + + This function extracts annotation and children from the XML annotation + and gives them to the ``_fromAnnotation`` function for parsing. + Subclass behaviour should be implemented by extending + ``_fromAnnotation``. + However, the mapping between method strings and subclasses + must be hard-coded into this function here (at the moment). + """ + if annotation.tag != f"{{{annotation_namespace}}}spline": + raise ValueError("The given annotation is not an AMICI spline annotation!") + + attributes = {} + for key, value in annotation.items(): + if not key.startswith(f"{{{annotation_namespace}}}"): + raise ValueError( + f"Unexpected attribute {key} inside spline annotation!" + ) + key = key[len(annotation_namespace) + 2 :] + if value == "true": + value = True + elif value == "false": + value = False + attributes[key] = value + + children = {} + for child in annotation: + if not child.tag.startswith(f"{{{annotation_namespace}}}"): + raise ValueError( + f"Unexpected node {child.tag} inside spline annotation!" + ) + key = child.tag[len(annotation_namespace) + 2 :] + value = [ + mathml2sympy( + ET.tostring(gc).decode(), + evaluate=False, + locals=locals_, + expression_type="Rule", + ) + for gc in child + ] + children[key] = value + + if attributes["spline_method"] == "cubic_hermite": + cls = CubicHermiteSpline + else: + raise ValueError(f"Unknown spline method {attributes['spline_method']}!") + + del attributes["spline_method"] + kwargs = cls._from_annotation(attributes, children) + + if attributes: + raise ValueError( + "Unprocessed attributes in spline annotation!\n" + str(attributes) + ) + + if children: + raise ValueError( + "Unprocessed children in spline annotation!\n" + str(children) + ) + + return cls(sbml_id, **kwargs) + + @classmethod + def _from_annotation( + cls, + attributes: Dict[str, Any], + children: Dict[str, List[sp.Basic]], + ) -> Dict[str, Any]: + """ + Given the attributes and children of a AMICI spline annotation, + returns the keyword arguments to be passed + to the spline object ``__init__`` function. + """ + kwargs = {} + + bc = attributes.pop("spline_bc", None) + if isinstance(bc, str) and bc.startswith("("): + if not bc.endswith(")"): + raise ValueError(f"Ill-formatted bc {bc}!") + bc_cmps = bc[1:-1].split(",") + if len(bc_cmps) != 2: + raise ValueError(f"Ill-formatted bc {bc}!") + bc = (bc_cmps[0].strip(), bc_cmps[1].strip()) + kwargs["bc"] = bc + + extr = attributes.pop("spline_extrapolate", None) + if isinstance(extr, str) and extr.startswith("("): + if not extr.endswith(")"): + raise ValueError(f"Ill-formatted extrapolation {extr}!") + extr_cmps = extr[1:-1].split(",") + if len(extr_cmps) != 2: + raise ValueError(f"Ill-formatted extrapolation {extr}!") + extr = (extr_cmps[0].strip(), extr_cmps[1].strip()) + kwargs["extrapolate"] = extr + + kwargs["logarithmic_parametrization"] = attributes.pop( + "spline_logarithmic_parametrization", False + ) + + if "spline_evaluation_point" not in children.keys(): + raise ValueError( + "Required spline annotation 'spline_evaluation_point' missing!" + ) + x = children.pop("spline_evaluation_point") + if len(x) != 1: + raise ValueError( + "Ill-formatted spline annotation 'spline_evaluation_point' " + "(more than one children is present)!" + ) + kwargs["evaluate_at"] = x[0] + + if "spline_uniform_grid" in children: + start, stop, step = children.pop("spline_uniform_grid") + kwargs["nodes"] = UniformGrid(start, stop, step) + elif "spline_grid" in children: + kwargs["nodes"] = children.pop("spline_grid") + else: + raise ValueError( + "Spline annotation requires either " + "'spline_grid' or 'spline_uniform_grid' to be specified!" + ) + + if "spline_values" not in children: + raise ValueError("Required spline annotation 'spline_values' missing!") + kwargs["values_at_nodes"] = children.pop("spline_values") + + return kwargs + + def parameters(self, importer: sbml_import.SbmlImporter) -> Set[sp.Symbol]: + """Returns the SBML parameters used by this spline""" + from .de_export import SymbolId + + return self._parameters().intersection( + set(importer.symbols[SymbolId.PARAMETER].keys()) + ) + + def _parameters(self) -> Set[sp.Symbol]: + parameters = set() + for y in self.values_at_nodes: + parameters.update(y.free_symbols) + return parameters + + def ode_model_symbol(self, importer: sbml_import.SbmlImporter) -> sp.Function: + """ + Returns the `sympy` object to be used by + :py:class:`amici.de_export.ODEModel`. + This expression can be differentiated and easily mapped to the C++ + code. + """ + parameters = list(self.parameters(importer)) + + class AmiciSpline(sp.Function): + # AmiciSpline(splineId, x, *parameters) + nargs = (len(parameters) + 2,) + + @classmethod + def eval(cls, *args): + return None # means leave unevaluated + + def fdiff(self, argindex=1): + if argindex == 1: + # Derivative with respect to the spline SBML ID + # Since the SBML ID is not a real function parameter + # (more like a subscript), the derivative will be zero + return sp.Integer(0) + + if argindex == 2: + + class AmiciSplineDerivative(sp.Function): + # Spline derivative + # AmiciSplineDerivative(splineId, x, *parameters) + nargs = (len(parameters) + 2,) + + @classmethod + def eval(cls, *args): + return None # means leave unevaluated + + def fdiff(self, argindex=1): + return NotImplementedError( + "Second order derivatives for spline " + "are not implemented yet." + ) + + def _eval_is_real(self): + return True + + return AmiciSplineDerivative(*self.args) + + pindex = argindex - 3 + assert 0 <= pindex < len(parameters) + + class AmiciSplineSensitivity(sp.Function): + # Derivative with respect to a parameter paramId + # AmiciSplineSensitivity(splineId, x, paramId, *parameters) + nargs = (len(parameters) + 3,) + + @classmethod + def eval(cls, *args): + return None # means leave unevaluated + + def fdiff(self, argindex=1): + return NotImplementedError( + "Second order derivatives for spline " + "are not implemented yet." + ) + + def _eval_is_real(self): + return True + + return AmiciSplineSensitivity( + self.args[0], self.args[1], parameters[pindex], *self.args[2:] + ) + + def _eval_is_real(self): + return True + + return AmiciSpline(self.sbml_id, self.evaluate_at, *parameters) + + def plot( + self, + parameters: Optional[Dict] = None, + *, + xlim: Optional[Tuple[float, float]] = None, + npoints: int = 100, + xlabel: Optional[str] = None, + ylabel: Union[str, None] = "spline value", + ax=None, + ): + "Plots the spline, highlighting the nodes positions." + if parameters is None: + parameters = {} + if ax is None: + from matplotlib import pyplot as plt + + fig, ax = plt.subplots() + if xlim is None: + nodes = np.asarray(self.nodes) + xlim = (float(nodes[0]), float(nodes[-1])) + nodes = np.linspace(*xlim, npoints) + ax.plot(nodes, [float(self.evaluate(x).subs(parameters)) for x in nodes]) + ax.plot( + self.nodes, [float(y.subs(parameters)) for y in self.values_at_nodes], "o" + ) + if xlabel is not None: + ax.set_xlabel(xlabel) + if ylabel is not None: + ax.set_ylabel(ylabel) + return ax + + +def spline_user_functions( + splines: List[AbstractSpline], + p_index: Dict[sp.Symbol, int], +) -> Dict[str, List[Tuple[Callable[..., bool], Callable[..., str]]]]: + """ + Custom user functions to be used in `ODEExporter` + for linking spline expressions to C++ code. + """ + spline_ids = [spline.sbml_id.name for spline in splines] + return { + "AmiciSpline": [ + ( + lambda *args: True, + lambda spline_id, x, *p: f"spl_{spline_ids.index(spline_id)}", + ) + ], + "AmiciSplineDerivative": [ + ( + lambda *args: True, + lambda spline_id, x, *p: f"dspl_{spline_ids.index(spline_id)}", + ) + ], + "AmiciSplineSensitivity": [ + ( + lambda *args: True, + lambda spline_id, x, param_id, *p: f"sspl_{spline_ids.index(spline_id)}_{p_index[param_id]}", + ) + ], + } + + +class CubicHermiteSpline(AbstractSpline): + def __init__( + self, + sbml_id: Union[str, sp.Symbol], + nodes: Sequence, + values_at_nodes: Sequence, + derivatives_at_nodes: Sequence = None, + *, + evaluate_at: Optional[Union[str, sp.Basic]] = None, + bc: BClike = "auto", + extrapolate: BClike = None, + logarithmic_parametrization: bool = False, + ): + """ + Constructor for `CubicHermiteSpline` objects. + + :param sbml_id: + The SBML ID of the parameter associated to the spline + as a string or a SymPy symbol. + + :param x: + The point at which the spline is evaluated. + It will be sympified. + + :param nodes: + The points at which the spline values are known. + Currently, they must be numeric or only depend on constant parameters. + These points should be strictly increasing. + This argument will be sympified. + + :param values_at_nodes: + The spline values at each of the points in `nodes`. + They must not depend on model species. + This argument will be sympified. + + :param derivatives_at_nodes: + The spline derivatives at each of the points in `nodes`. + They must not depend on model species. + This argument will be sympified. + If not specified, it will be computed by finite differences. + + :param evaluate_at: + The point at which the spline is evaluated. + It will be sympified. + Defaults to model time. + + :param bc: + Applied boundary conditions + (see `AbstractSpline` documentation). + If `'auto'` (the default), the boundary conditions will be + automatically set depending on the extrapolation methods. + + :param extrapolate: + Extrapolation method (see `AbstractSpline` documentation). + + :param logarithmic_parametrization: + Whether interpolation should be done in log-scale. + """ + + if not isinstance(nodes, UniformGrid): + nodes = np.asarray([sympify_noeval(x) for x in nodes]) + values_at_nodes = np.asarray([sympify_noeval(y) for y in values_at_nodes]) + + if len(nodes) != len(values_at_nodes): + # NB this would be checked in AbstractSpline.__init__() + # however, we check it now so that an informative message + # can be printed (otherwise finite difference computation fails) + raise ValueError( + "Length of nodes and values_at_nodes must be the same " + f"(instead len(nodes) = {len(nodes)} and len(values_at_nodes) = {len(values_at_nodes)})!" + ) + + bc, extrapolate = self._normalize_bc_and_extrapolate(bc, extrapolate) + if bc[0] == "zeroderivative+natural" or bc[1] == "zeroderivative+natural": + raise ValueError( + "zeroderivative+natural bc not supported by " "CubicHermiteSplines!" + ) + + if derivatives_at_nodes is None: + derivatives_at_nodes = _finite_differences(nodes, values_at_nodes, bc) + self._derivatives_by_fd = True + else: + derivatives_at_nodes = np.asarray( + [sympify_noeval(d) for d in derivatives_at_nodes] + ) + self._derivatives_by_fd = False + + if len(nodes) != len(derivatives_at_nodes): + raise ValueError( + "Length of nodes and derivatives_at_nodes must be the same " + f"(instead len(nodes) = {len(nodes)} and len(derivatives_at_nodes) = {len(derivatives_at_nodes)})!" + ) + + if bc == ("periodic", "periodic") and ( + values_at_nodes[0] != values_at_nodes[-1] + or derivatives_at_nodes[0] != derivatives_at_nodes[-1] + ): + raise ValueError( + "bc=periodic but given values_at_nodes and derivatives_at_nodes do not satisfy " + "periodic boundary conditions!" + ) + + super().__init__( + sbml_id, + nodes, + values_at_nodes, + evaluate_at=evaluate_at, + bc=bc, + extrapolate=extrapolate, + logarithmic_parametrization=logarithmic_parametrization, + ) + + self._derivatives_at_nodes = derivatives_at_nodes + + @property + def derivatives_at_nodes(self) -> np.ndarray: + """The spline derivatives at each of the points in `nodes`.""" + return self._derivatives_at_nodes + + @property + def smoothness(self) -> int: + """ + Smoothness of this spline (equal to 1 for cubic Hermite splines + since they are continuous up to the first derivative). + """ + return 1 + + @property + def method(self) -> str: + """Spline method (cubic Hermite spline)""" + return "cubic_hermite" + + @property + def derivatives_by_fd(self) -> bool: + return self._derivatives_by_fd + + def check_if_valid(self, importer: sbml_import.SbmlImporter) -> None: + """ + Check if the spline described by this object can be correctly + be implemented by AMICI. E.g., check whether the formulas + for spline grid points, values, ... contain species symbols. + """ + # TODO this is very much a draft + from .de_export import SymbolId + + species: List[sp.Symbol] = list(importer.symbols[SymbolId.SPECIES]) + for d in self.derivatives_at_nodes: + if len(d.free_symbols.intersection(species)) != 0: + raise ValueError( + "derivatives_at_nodes should not depend on model species" + ) + + super().check_if_valid(importer) + + def d_scaled(self, i: Integral) -> sp.Expr: + """ + Return the derivative of the polynomial interpolant at the `i`-th + point. Unless logarithmic parametrization is used, it is equal to the + derivative of the spline expression. + """ + if self.logarithmic_parametrization: + return self.derivatives_at_nodes[i] / self.values_at_nodes[i] + return self.derivatives_at_nodes[i] + + def _poly_variable(self, x: Union[Real, sp.Basic], i: Integral) -> sp.Basic: + assert 0 <= i < len(self.nodes) - 1 + dx = self.nodes[i + 1] - self.nodes[i] + with evaluate(False): + return (x - self.nodes[i]) / dx + + def _poly(self, t: Union[Real, sp.Basic], i: Integral) -> sp.Basic: + """ + Return the symbolic expression for the spline restricted to the `i`-th + interval as polynomial in the scaled variable `t`. + """ + assert 0 <= i < len(self.nodes) - 1 + + dx = self.nodes[i + 1] - self.nodes[i] + + h00 = 2 * t**3 - 3 * t**2 + 1 + h10 = t**3 - 2 * t**2 + t + h01 = -2 * t**3 + 3 * t**2 + h11 = t**3 - t**2 + + y0 = self.y_scaled(i) + y1 = self.y_scaled(i + 1) + dy0 = self.d_scaled(i) + dy1 = self.d_scaled(i + 1) + + with evaluate(False): + return h00 * y0 + h10 * dx * dy0 + h01 * y1 + h11 * dx * dy1 + + def _annotation_children(self) -> Dict[str, Union[str, List[str]]]: + children = super()._annotation_children() + if not self._derivatives_by_fd: + children["spline_derivatives"] = [ + sbml_mathml(d) for d in self.derivatives_at_nodes + ] + return children + + def _parameters(self) -> Set[sp.Symbol]: + parameters = super()._parameters() + for d in self.derivatives_at_nodes: + parameters.update(d.free_symbols) + return parameters + + def _replace_in_all_expressions(self, old: sp.Symbol, new: sp.Symbol) -> None: + super()._replace_in_all_expressions(old, new) + self._derivatives_at_nodes = [ + d.subs(old, new) for d in self.derivatives_at_nodes + ] + + @classmethod + def _from_annotation(cls, attributes, children) -> Dict[str, Any]: + kwargs = super()._from_annotation(attributes, children) + + if "spline_derivatives" in children.keys(): + kwargs["derivatives_at_nodes"] = children.pop("spline_derivatives") + + return kwargs + + def __str__(self) -> str: + s = ( + "HermiteCubicSpline " + + f"on ({self.nodes[0]}, {self.nodes[-1]}) with {len(self.nodes)} points" + ) + cmps = [] + if self.bc != (None, None): + if self.bc == ("periodic", "periodic"): + cmps.append("periodic") + else: + cmps.append(f"bc = {self.bc}") + if self.derivatives_by_fd: + cmps.append("finite differences") + if self.extrapolate != (None, None): + cmps.append(f"extrapolate = {self.extrapolate}") + if not cmps: + return s + return s + " [" + ", ".join(cmps) + "]" + + +def _finite_differences(xx: np.ndarray, yy: np.ndarray, bc: NormalizedBC) -> np.ndarray: + dd = [] + + if bc[0] == "periodic": + fd = _centered_fd(yy[-2], yy[0], yy[1], xx[-1] - xx[-2], xx[1] - xx[0]) + elif bc[0] == "zeroderivative": + fd = sp.Integer(0) + elif bc[0] == "natural": + if len(xx) < 3: + raise ValueError( + "At least 3 nodes are needed " + "for computing finite differences with natural bc!" + ) + fd = _natural_fd(yy[0], xx[1] - xx[0], yy[1], xx[2] - xx[1], yy[2]) + else: + fd = _onesided_fd(yy[0], yy[1], xx[1] - xx[0]) + dd.append(fd) + + for i in range(1, len(xx) - 1): + dd.append( + _centered_fd( + yy[i - 1], yy[i], yy[i + 1], xx[i] - xx[i - 1], xx[i + 1] - xx[i] + ) + ) + + if bc[1] == "periodic": + fd = dd[0] + elif bc[1] == "zeroderivative": + fd = sp.Integer(0) + elif bc[1] == "natural": + if len(xx) < 3: + raise ValueError( + "At least 3 nodes are needed " + "for computing finite differences with natural bc!" + ) + fd = _natural_fd(yy[-1], xx[-2] - xx[-1], yy[-2], xx[-3] - xx[-2], yy[-3]) + else: + fd = _onesided_fd(yy[-2], yy[-1], xx[-1] - xx[-2]) + dd.append(fd) + + return np.asarray(dd) + + +def _onesided_fd(y0: sp.Expr, y1: sp.Expr, h: sp.Expr) -> sp.Basic: + return sp.Mul(1 / h, y1 - y0, evaluate=False) + + +def _centered_fd( + ym1: sp.Expr, + y0: sp.Expr, + yp1: sp.Expr, + hm: sp.Expr, + hp: sp.Expr, +) -> sp.Expr: + if hm == hp: + return sp.Mul(1 / (2 * hm), yp1 - ym1, evaluate=False) + else: + return ((yp1 - y0) / hp + (y0 - ym1) / hm) / 2 + + +def _natural_fd( + y0: sp.Expr, + dx1: sp.Expr, + y1: sp.Expr, + dx2: sp.Expr, + y2: sp.Expr, +) -> sp.Expr: + if dx1 == dx2: + den = 4 * dx1 + with evaluate(False): + return (6 * y1 - 5 * y0 - y2) / den + else: + with evaluate(False): + return ((y1 - y2) / dx2 - 5 * (y0 - y1) / dx1) / 4 + # Another formula, which depends on + # y0, dx1 = x1 - x0, y1 and dy1 (derivative at x1) + # (-dx1*dy1 - 3*y0 + 3*y1)/(2*dx1) diff --git a/python/sdist/amici/swig.py b/python/sdist/amici/swig.py index c7b12a370f..bfb2964a3a 100644 --- a/python/sdist/amici/swig.py +++ b/python/sdist/amici/swig.py @@ -8,43 +8,40 @@ class TypeHintFixer(ast.NodeTransformer): """Replaces SWIG-generated C++ typehints by corresponding Python types""" mapping = { - 'void': None, - 'double': ast.Name('float'), - 'int': ast.Name('int'), - 'long': ast.Name('int'), - 'ptrdiff_t': ast.Name('int'), - 'size_t': ast.Name('int'), - 'bool': ast.Name('bool'), - 'std::unique_ptr< amici::Solver >': ast.Constant('Solver'), - 'amici::InternalSensitivityMethod': - ast.Constant('InternalSensitivityMethod'), - 'amici::InterpolationType': ast.Constant('InterpolationType'), - 'amici::LinearMultistepMethod': ast.Constant('LinearMultistepMethod'), - 'amici::LinearSolver': ast.Constant('LinearSolver'), - 'amici::Model *': ast.Constant('Model'), - 'amici::Model const *': ast.Constant('Model'), - 'amici::NewtonDampingFactorMode': - ast.Constant('NewtonDampingFactorMode'), - 'amici::NonlinearSolverIteration': - ast.Constant('NonlinearSolverIteration'), - 'amici::ObservableScaling': ast.Constant('ObservableScaling'), - 'amici::ParameterScaling': ast.Constant('ParameterScaling'), - 'amici::RDataReporting': ast.Constant('RDataReporting'), - 'amici::SensitivityMethod': ast.Constant('SensitivityMethod'), - 'amici::SensitivityOrder': ast.Constant('SensitivityOrder'), - 'amici::Solver *': ast.Constant('Solver'), - 'amici::SteadyStateSensitivityMode': - ast.Constant('SteadyStateSensitivityMode'), - 'amici::realtype': ast.Name('float'), - 'DoubleVector': ast.Constant('Sequence[float]'), - 'IntVector': ast.Name('Sequence[int]'), - 'std::string': ast.Name('str'), - 'std::string const &': ast.Name('str'), - 'std::unique_ptr< amici::ExpData >': ast.Constant('ExpData'), - 'std::unique_ptr< amici::ReturnData >': ast.Constant('ReturnData'), - 'std::vector< amici::ParameterScaling,' - 'std::allocator< amici::ParameterScaling > > const &': - ast.Constant('ParameterScalingVector') + "void": None, + "double": ast.Name("float"), + "int": ast.Name("int"), + "long": ast.Name("int"), + "ptrdiff_t": ast.Name("int"), + "size_t": ast.Name("int"), + "bool": ast.Name("bool"), + "std::unique_ptr< amici::Solver >": ast.Constant("Solver"), + "amici::InternalSensitivityMethod": ast.Constant("InternalSensitivityMethod"), + "amici::InterpolationType": ast.Constant("InterpolationType"), + "amici::LinearMultistepMethod": ast.Constant("LinearMultistepMethod"), + "amici::LinearSolver": ast.Constant("LinearSolver"), + "amici::Model *": ast.Constant("Model"), + "amici::Model const *": ast.Constant("Model"), + "amici::NewtonDampingFactorMode": ast.Constant("NewtonDampingFactorMode"), + "amici::NonlinearSolverIteration": ast.Constant("NonlinearSolverIteration"), + "amici::ObservableScaling": ast.Constant("ObservableScaling"), + "amici::ParameterScaling": ast.Constant("ParameterScaling"), + "amici::RDataReporting": ast.Constant("RDataReporting"), + "amici::SensitivityMethod": ast.Constant("SensitivityMethod"), + "amici::SensitivityOrder": ast.Constant("SensitivityOrder"), + "amici::Solver *": ast.Constant("Solver"), + "amici::SteadyStateSensitivityMode": ast.Constant("SteadyStateSensitivityMode"), + "amici::realtype": ast.Name("float"), + "DoubleVector": ast.Constant("Sequence[float]"), + "IntVector": ast.Name("Sequence[int]"), + "std::string": ast.Name("str"), + "std::string const &": ast.Name("str"), + "std::unique_ptr< amici::ExpData >": ast.Constant("ExpData"), + "std::unique_ptr< amici::ReturnData >": ast.Constant("ReturnData"), + "std::vector< amici::ParameterScaling," + "std::allocator< amici::ParameterScaling > > const &": ast.Constant( + "ParameterScalingVector" + ), } def visit_FunctionDef(self, node): @@ -73,15 +70,21 @@ def _new_annot(self, old_annot: str): return ast.Name("int") # std::vector value type - if (value_type := re.sub( - r'std::vector< (.*) >::value_type(?: const &)?', - r'\1', old_annot)) in self.mapping: + if ( + value_type := re.sub( + r"std::vector< (.*) >::value_type(?: const &)?", r"\1", old_annot + ) + ) in self.mapping: return self.mapping[value_type] # std::vector - if (value_type := re.sub( - r'std::vector< (.*),std::allocator< \1 > >(?: const &)?', - r'\1', old_annot)) in self.mapping: + if ( + value_type := re.sub( + r"std::vector< (.*),std::allocator< \1 > >(?: const &)?", + r"\1", + old_annot, + ) + ) in self.mapping: value_type_annot = self.mapping[value_type] if isinstance(value_type_annot, ast.Constant): return ast.Name(f"Tuple['{value_type_annot.value}']") @@ -94,11 +97,11 @@ def _new_annot(self, old_annot: str): def fix_typehints(infilename, outfilename): """Change SWIG-generated C++ typehints to Python typehints""" # Only available from Python3.9 - if not getattr(ast, 'unparse', None): + if not getattr(ast, "unparse", None): return # file -> AST - with open(infilename, 'r') as f: + with open(infilename, "r") as f: source = f.read() parsed_source = ast.parse(source) @@ -107,5 +110,5 @@ def fix_typehints(infilename, outfilename): parsed_source = fixer.visit(parsed_source) # AST -> file - with open(outfilename, 'w') as f: + with open(outfilename, "w") as f: f.write(ast.unparse(parsed_source)) diff --git a/python/sdist/amici/swig_wrappers.py b/python/sdist/amici/swig_wrappers.py index 8601b97c0f..65798cd6ac 100644 --- a/python/sdist/amici/swig_wrappers.py +++ b/python/sdist/amici/swig_wrappers.py @@ -1,11 +1,14 @@ """Convenience wrappers for the swig interface""" import logging import sys + +import warnings from contextlib import contextmanager, suppress -from typing import List, Optional, Union, Sequence, Dict, Any +from typing import Any, Dict, List, Optional, Sequence, Union import amici import amici.amici as amici_swig + from . import numpy from .logging import get_logger @@ -13,18 +16,25 @@ __all__ = [ - 'runAmiciSimulation', 'runAmiciSimulations', 'ExpData', - 'readSolverSettingsFromHDF5', 'writeSolverSettingsToHDF5', - 'set_model_settings', 'get_model_settings', - 'AmiciModel', 'AmiciSolver', 'AmiciExpData', 'AmiciReturnData', - 'AmiciExpDataVector' + "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]] +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: @@ -45,9 +55,13 @@ def _capture_cstdout(): def _get_ptr( - obj: Union[AmiciModel, AmiciExpData, AmiciSolver, AmiciReturnData] -) -> Union['amici_swig.Model', 'amici_swig.ExpData', - 'amici_swig.Solver', 'amici_swig.ReturnData']: + 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 @@ -57,17 +71,22 @@ def _get_ptr( :returns: Non-smart pointer """ - if isinstance(obj, (amici_swig.ModelPtr, amici_swig.ExpDataPtr, - amici_swig.SolverPtr, amici_swig.ReturnDataPtr)): + 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 -) -> 'numpy.ReturnDataView': + model: AmiciModel, solver: AmiciSolver, edata: Optional[AmiciExpData] = None +) -> "numpy.ReturnDataView": """ Convenience wrapper around :py:func:`amici.amici.runAmiciSimulation` (generated by swig) @@ -85,16 +104,29 @@ def runAmiciSimulation( :returns: ReturnData object with simulation results """ + if ( + model.ne > 0 + and solver.getSensitivityMethod() + == amici_swig.SensitivityMethod.adjoint + and solver.getSensitivityOrder() == amici_swig.SensitivityOrder.first + ): + 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." + ) + with _capture_cstdout(): rdata = amici_swig.runAmiciSimulation( - _get_ptr(solver), _get_ptr(edata), _get_ptr(model)) + _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': +def ExpData(*args) -> "amici_swig.ExpData": """ Convenience wrapper for :py:class:`amici.amici.ExpData` constructors @@ -103,7 +135,7 @@ def ExpData(*args) -> 'amici_swig.ExpData': :returns: ExpData Instance """ if isinstance(args[0], numpy.ReturnDataView): - return amici_swig.ExpData(_get_ptr(args[0]['ptr']), *args[1:]) + return amici_swig.ExpData(_get_ptr(args[0]["ptr"]), *args[1:]) elif 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 @@ -116,12 +148,12 @@ def ExpData(*args) -> 'amici_swig.ExpData': def runAmiciSimulations( - model: AmiciModel, - solver: AmiciSolver, - edata_list: AmiciExpDataVector, - failfast: bool = True, - num_threads: int = 1, -) -> List['numpy.ReturnDataView']: + model: AmiciModel, + solver: AmiciSolver, + edata_list: AmiciExpDataVector, + failfast: bool = True, + num_threads: int = 1, +) -> List["numpy.ReturnDataView"]: """ Convenience wrapper for loops of amici.runAmiciSimulation @@ -134,14 +166,22 @@ def runAmiciSimulations( :returns: list of simulation results """ + if ( + model.ne > 0 + and solver.getSensitivityMethod() + == amici_swig.SensitivityMethod.adjoint + and solver.getSensitivityOrder() == amici_swig.SensitivityOrder.first + ): + 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." + ) + 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 + _get_ptr(solver), edata_ptr_vector, _get_ptr(model), failfast, num_threads ) for rdata in rdata_ptr_list: _log_simulation(rdata) @@ -152,9 +192,7 @@ def runAmiciSimulations( def readSolverSettingsFromHDF5( - file: str, - solver: AmiciSolver, - location: Optional[str] = 'solverSettings' + file: str, solver: AmiciSolver, location: Optional[str] = "solverSettings" ) -> None: """ Convenience wrapper for :py:func:`amici.readSolverSettingsFromHDF5` @@ -167,9 +205,9 @@ def readSolverSettingsFromHDF5( def writeSolverSettingsToHDF5( - solver: AmiciSolver, - file: Union[str, object], - location: Optional[str] = 'solverSettings' + solver: AmiciSolver, + file: Union[str, object], + location: Optional[str] = "solverSettings", ) -> None: """ Convenience wrapper for :py:func:`amici.amici.writeSolverSettingsToHDF5` @@ -189,27 +227,27 @@ def writeSolverSettingsToHDF5( model_instance_settings = [ # `setParameter{List,Scale}` will clear initial state sensitivities, so # `setParameter{List,Scale}` has to be called first. - 'ParameterList', - 'ParameterScale', # getter returns a SWIG object - 'AddSigmaResiduals', - 'AlwaysCheckFinite', - 'FixedParameters', - 'InitialStates', - ('getInitialStateSensitivities', 'setUnscaledInitialStateSensitivities'), - 'MinimumSigmaResiduals', - ('nMaxEvent', 'setNMaxEvent'), - 'Parameters', - 'ReinitializationStateIdxs', - 'ReinitializeFixedParameterInitialStates', - 'StateIsNonNegative', - 'SteadyStateSensitivityMode', - ('t0', 'setT0'), - 'Timepoints', + "ParameterList", + "ParameterScale", # getter returns a SWIG object + "AddSigmaResiduals", + "AlwaysCheckFinite", + "FixedParameters", + "InitialStates", + ("getInitialStateSensitivities", "setUnscaledInitialStateSensitivities"), + "MinimumSigmaResiduals", + ("nMaxEvent", "setNMaxEvent"), + "Parameters", + "ReinitializationStateIdxs", + "ReinitializeFixedParameterInitialStates", + "StateIsNonNegative", + "SteadyStateSensitivityMode", + ("t0", "setT0"), + "Timepoints", ] def get_model_settings( - model: AmiciModel, + model: AmiciModel, ) -> Dict[str, Any]: """Get model settings that are set independently of the compiled model. @@ -219,27 +257,29 @@ def get_model_settings( """ settings = {} for setting in model_instance_settings: - getter = setting[0] if isinstance(setting, tuple) else f'get{setting}' + getter = setting[0] if isinstance(setting, tuple) else f"get{setting}" - if getter == 'getInitialStates' and not model.hasCustomInitialStates(): + if getter == "getInitialStates" and not model.hasCustomInitialStates(): settings[setting] = [] continue - if getter == 'getInitialStateSensitivities' \ - and not model.hasCustomInitialStateSensitivities(): + if ( + getter == "getInitialStateSensitivities" + and not model.hasCustomInitialStateSensitivities() + ): settings[setting] = [] continue settings[setting] = getattr(model, getter)() # TODO `amici.Model.getParameterScale` returns a SWIG object instead # of a Python list/tuple. - if setting == 'ParameterScale': + if setting == "ParameterScale": settings[setting] = tuple(settings[setting]) return settings def set_model_settings( - model: AmiciModel, - settings: Dict[str, Any], + model: AmiciModel, + settings: Dict[str, Any], ) -> None: """Set model settings. @@ -248,7 +288,7 @@ def set_model_settings( values are provided to the setters. """ for setting, value in settings.items(): - setter = setting[1] if isinstance(setting, tuple) else f'set{setting}' + setter = setting[1] if isinstance(setting, tuple) else f"set{setting}" getattr(model, setter)(value) @@ -263,23 +303,21 @@ def _log_simulation(rdata: amici_swig.ReturnData): condition = f"[{rdata.id}]" if rdata.id else "" logger.log( amici_severity_to_logging[msg.severity], - f"{condition}[{msg.identifier}] {msg.message}" + f"{condition}[{msg.identifier}] {msg.message}", ) -def _ids_and_names_to_rdata( - rdata: amici_swig.ReturnData, - model: amici_swig.Model -): +def _ids_and_names_to_rdata(rdata: amici_swig.ReturnData, model: amici_swig.Model): """Copy entity IDs and names from a Model to ReturnData.""" - for entity_type in ('State', 'Observable', 'Expression', - 'Parameter', 'FixedParameter'): - for name_or_id in ('Ids', 'Names'): - names_or_ids = getattr(model, f'get{entity_type}{name_or_id}')() - setattr( - rdata, - f"{entity_type.lower()}_{name_or_id.lower()}", - names_or_ids - ) + for entity_type in ( + "State", + "Observable", + "Expression", + "Parameter", + "FixedParameter", + ): + for name_or_id in ("Ids", "Names"): + names_or_ids = getattr(model, f"get{entity_type}{name_or_id}")() + setattr(rdata, f"{entity_type.lower()}_{name_or_id.lower()}", names_or_ids) rdata.state_ids_solver = model.getStateIdsSolver() rdata.state_names_solver = model.getStateNamesSolver() diff --git a/python/sdist/amici/testing.py b/python/sdist/amici/testing.py index de1f69a1cc..cdee80b1f0 100644 --- a/python/sdist/amici/testing.py +++ b/python/sdist/amici/testing.py @@ -9,16 +9,19 @@ # see also https://stackoverflow.com/a/62364698 ON_VALGRIND = any( needle in haystack - for needle in ('valgrind', 'vgpreload') - for haystack in (os.getenv("LD_PRELOAD", ""), - os.getenv("DYLD_INSERT_LIBRARIES", "")) + for needle in ("valgrind", "vgpreload") + for haystack in ( + os.getenv("LD_PRELOAD", ""), + os.getenv("DYLD_INSERT_LIBRARIES", ""), + ) ) # Decorator to skip certain tests when we are under valgrind # (those that are independent of the AMICI C++ parts, or that take too long, # or that test performance) skip_on_valgrind = pytest.mark.skipif( - ON_VALGRIND, reason="Takes too long or is meaningless under valgrind") + ON_VALGRIND, reason="Takes too long or is meaningless under valgrind" +) class TemporaryDirectoryWinSafe(TemporaryDirectory): @@ -28,11 +31,12 @@ class TemporaryDirectoryWinSafe(TemporaryDirectory): otherwise fail on Windows with a ``PermissionError``. This class ignores such failures. """ + def cleanup(self): try: super().cleanup() except PermissionError as e: - if sys.platform not in {'win32', 'cygwin'}: + if sys.platform not in {"win32", "cygwin"}: raise e except NotADirectoryError: # Ignore exception on Windows for pyd files: diff --git a/python/sdist/pyproject.toml b/python/sdist/pyproject.toml index 25914ee4ac..3e77875ca1 100644 --- a/python/sdist/pyproject.toml +++ b/python/sdist/pyproject.toml @@ -12,3 +12,6 @@ requires = [ "cmake-build-extension==0.5.1", ] build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 80 diff --git a/python/sdist/setup.cfg b/python/sdist/setup.cfg index 26255e77be..733a2a4938 100644 --- a/python/sdist/setup.cfg +++ b/python/sdist/setup.cfg @@ -44,7 +44,7 @@ include_package_data = True zip_safe = False [options.extras_require] -petab = petab>=0.2.0 +petab = petab>=0.2.1 pysb = pysb>=1.13.1 test = pytest diff --git a/python/sdist/setup.py b/python/sdist/setup.py index ede595f4cb..30a72d6eb5 100755 --- a/python/sdist/setup.py +++ b/python/sdist/setup.py @@ -21,9 +21,13 @@ sys.path.insert(0, os.path.dirname(__file__)) from amici.custom_commands import ( - AmiciInstall, AmiciDevelop, - AmiciInstallLib, AmiciSDist, AmiciBuildPy, - AmiciBuildCMakeExtension) + AmiciBuildCMakeExtension, + AmiciBuildPy, + AmiciDevelop, + AmiciInstall, + AmiciInstallLib, + AmiciSDist, +) def get_extensions(): @@ -41,63 +45,63 @@ def get_extensions(): # SuiteSparse Config suitesparse_config = CMakeExtension( - name='SuiteSparse_config', - install_prefix='amici', - source_dir='amici/ThirdParty/SuiteSparse/SuiteSparse_config', + name="SuiteSparse_config", + install_prefix="amici", + source_dir="amici/ThirdParty/SuiteSparse/SuiteSparse_config", cmake_configure_options=[ *global_cmake_configure_options, "-DBLA_VENDOR=All", "-DENABLE_CUDA=FALSE", "-DNFORTRAN=TRUE", - ] + ], ) # SuiteSparse AMD amd = CMakeExtension( - name='amd', - install_prefix='amici', - source_dir='amici/ThirdParty/SuiteSparse/AMD', + name="amd", + install_prefix="amici", + source_dir="amici/ThirdParty/SuiteSparse/AMD", cmake_configure_options=[ *global_cmake_configure_options, "-DNFORTRAN=TRUE", - ] + ], ) # SuiteSparse BTF btf = CMakeExtension( - name='btf', - install_prefix='amici', - source_dir='amici/ThirdParty/SuiteSparse/BTF', + name="btf", + install_prefix="amici", + source_dir="amici/ThirdParty/SuiteSparse/BTF", cmake_configure_options=[ *global_cmake_configure_options, "-DNFORTRAN=TRUE", - ] + ], ) # SuiteSparse COLAMD colamd = CMakeExtension( - name='colamd', - install_prefix='amici', - source_dir='amici/ThirdParty/SuiteSparse/COLAMD', + name="colamd", + install_prefix="amici", + source_dir="amici/ThirdParty/SuiteSparse/COLAMD", cmake_configure_options=[ *global_cmake_configure_options, "-DNFORTRAN=TRUE", - ] + ], ) # SuiteSparse KLU klu = CMakeExtension( - name='klu', - install_prefix='amici', - source_dir='amici/ThirdParty/SuiteSparse/KLU', + name="klu", + install_prefix="amici", + source_dir="amici/ThirdParty/SuiteSparse/KLU", cmake_configure_options=[ *global_cmake_configure_options, "-DNCHOLMOD=ON", "-DENABLE_CUDA=FALSE", "-DNFORTRAN=TRUE", - ] + ], ) # SUNDIALS sundials = CMakeExtension( - name='sundials', - install_prefix='amici', - source_dir='amici/ThirdParty/sundials', + name="sundials", + install_prefix="amici", + source_dir="amici/ThirdParty/sundials", cmake_configure_options=[ *global_cmake_configure_options, "-DBUILD_ARKODE=OFF", @@ -117,18 +121,18 @@ def get_extensions(): # before being passed to CMake. "-DKLU_LIBRARY_DIR='${build_dir}/amici/lib'", "-DKLU_INCLUDE_DIR='${build_dir}/amici/include'", - ] + ], ) # AMICI amici_ext = CMakeExtension( - name='amici', - install_prefix='amici', - source_dir='amici', + name="amici", + install_prefix="amici", + source_dir="amici", cmake_configure_options=[ *global_cmake_configure_options, - '-DAMICI_PYTHON_BUILD_EXT_ONLY=ON', - f'-DPython3_EXECUTABLE={Path(sys.executable).as_posix()}', - ] + "-DAMICI_PYTHON_BUILD_EXT_ONLY=ON", + f"-DPython3_EXECUTABLE={Path(sys.executable).as_posix()}", + ], ) # Order matters! return [suitesparse_config, amd, btf, colamd, klu, sundials, amici_ext] @@ -137,28 +141,29 @@ 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"), - "r", encoding="utf-8") as fh: + with open( + os.path.join(os.path.dirname(__file__), "README.md"), "r", encoding="utf-8" + ) as fh: long_description = fh.read() ext_modules = get_extensions() # handle parallel building # Note: can be empty to use all hardware threads - if (parallel_jobs := os.environ.get('AMICI_PARALLEL_COMPILE')) is not None: - os.environ['CMAKE_BUILD_PARALLEL_LEVEL'] = parallel_jobs + if (parallel_jobs := os.environ.get("AMICI_PARALLEL_COMPILE")) is not None: + os.environ["CMAKE_BUILD_PARALLEL_LEVEL"] = parallel_jobs else: - os.environ['CMAKE_BUILD_PARALLEL_LEVEL'] = "1" + os.environ["CMAKE_BUILD_PARALLEL_LEVEL"] = "1" # Install setup( cmdclass={ - 'install': AmiciInstall, - 'sdist': AmiciSDist, - 'build_ext': AmiciBuildCMakeExtension, - 'install_lib': AmiciInstallLib, - 'develop': AmiciDevelop, - 'build_py': AmiciBuildPy, + "install": AmiciInstall, + "sdist": AmiciSDist, + "build_ext": AmiciBuildCMakeExtension, + "install_lib": AmiciInstallLib, + "develop": AmiciDevelop, + "build_py": AmiciBuildPy, }, long_description=long_description, long_description_content_type="text/markdown", @@ -166,5 +171,5 @@ def main(): ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/python/tests/conftest.py b/python/tests/conftest.py index adfe6b93c8..1c07ddacac 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -5,9 +5,8 @@ import shutil import sys -import pytest - import amici +import pytest from amici.testing import TemporaryDirectoryWinSafe @@ -15,19 +14,23 @@ def sbml_example_presimulation_module(): """SBML example_presimulation model module fixture""" - sbml_file = os.path.join(os.path.dirname(__file__), '..', - 'examples', 'example_presimulation', - 'model_presimulation.xml') + sbml_file = os.path.join( + os.path.dirname(__file__), + "..", + "examples", + "example_presimulation", + "model_presimulation.xml", + ) sbml_importer = amici.SbmlImporter(sbml_file) - constant_parameters = ['DRUG_0', 'KIN_0'] + constant_parameters = ["DRUG_0", "KIN_0"] observables = amici.assignmentRules2observables( sbml_importer.sbml, # the libsbml model object - filter_function=lambda variable: variable.getName() == 'pPROT_obs' + filter_function=lambda variable: variable.getName() == "pPROT_obs", ) - module_name = 'test_model_presimulation' + module_name = "test_model_presimulation" with TemporaryDirectoryWinSafe(prefix=module_name) as outdir: sbml_importer.sbml2amici( @@ -35,12 +38,11 @@ def sbml_example_presimulation_module(): output_dir=outdir, verbose=False, observables=observables, - constant_parameters=constant_parameters) - - yield amici.import_model_module( - module_name=module_name, module_path=outdir + constant_parameters=constant_parameters, ) + yield amici.import_model_module(module_name=module_name, module_path=outdir) + @pytest.fixture(scope="session") def pysb_example_presimulation_module(): @@ -48,27 +50,32 @@ def pysb_example_presimulation_module(): pysb = pytest.importorskip("pysb") from amici.pysb_import import pysb2amici - constant_parameters = ['DRUG_0', 'KIN_0'] + constant_parameters = ["DRUG_0", "KIN_0"] pysb.SelfExporter.cleanup() # reset pysb pysb.SelfExporter.do_export = True - model_path = os.path.join(os.path.dirname(__file__), '..', - 'examples', 'example_presimulation') + model_path = os.path.join( + os.path.dirname(__file__), "..", "examples", "example_presimulation" + ) with amici.add_path(model_path): - if 'createModelPresimulation' in sys.modules: - importlib.reload(sys.modules['createModelPresimulation']) - model_module = sys.modules['createModelPresimulation'] + if "createModelPresimulation" in sys.modules: + importlib.reload(sys.modules["createModelPresimulation"]) + model_module = sys.modules["createModelPresimulation"] else: - model_module = importlib.import_module('createModelPresimulation') + model_module = importlib.import_module("createModelPresimulation") model = copy.deepcopy(model_module.model) - model.name = 'test_model_presimulation_pysb' + model.name = "test_model_presimulation_pysb" with TemporaryDirectoryWinSafe(prefix=model.name) as outdir: - pysb2amici(model, outdir, verbose=True, - observables=['pPROT_obs'], - constant_parameters=constant_parameters) + pysb2amici( + model, + outdir, + verbose=True, + observables=["pPROT_obs"], + constant_parameters=constant_parameters, + ) yield amici.import_model_module(model.name, outdir) diff --git a/python/tests/petab_test_problems/lotka_volterra/model/lotka_volterra.yaml b/python/tests/petab_test_problems/lotka_volterra/model/lotka_volterra.yaml index 27fd6ec29a..b6f756cb33 100644 --- a/python/tests/petab_test_problems/lotka_volterra/model/lotka_volterra.yaml +++ b/python/tests/petab_test_problems/lotka_volterra/model/lotka_volterra.yaml @@ -7,14 +7,14 @@ odes: rightHandSide: delta * prey * predator - gamma * predator + arrival_predator * prey initialValue: 2 -parameters: +parameters: - parameterId: alpha nominalValue: 2 parameterScale: log10 lowerBound: 0.1 upperBound: 10 estimate: 1 - + - parameterId: beta nominalValue: 4 parameterScale: log10 @@ -26,17 +26,17 @@ parameters: lowerBound: 0.1 upperBound: 10 estimate: 1 - + - parameterId: delta nominalValue: 3 parameterScale: log10 estimate: 0 - + - parameterId: departure_prey nominalValue: 3 parameterScale: log10 estimate: 0 - + - parameterId: arrival_predator nominalValue: 3 parameterScale: log10 diff --git a/python/tests/petab_test_problems/lotka_volterra/model/writer.py b/python/tests/petab_test_problems/lotka_volterra/model/writer.py index 680cbe43e6..76b98c3deb 100644 --- a/python/tests/petab_test_problems/lotka_volterra/model/writer.py +++ b/python/tests/petab_test_problems/lotka_volterra/model/writer.py @@ -3,17 +3,16 @@ import petab import yaml2sbml - -yaml2sbml_yaml = 'lotka_volterra.yaml' -petab_path = Path(__file__).parent.parent / 'petab' -petab_yaml = 'problem.yaml' -measurements_tsv = 'measurements.tsv' -model_name = 'lotka_volterra' +yaml2sbml_yaml = "lotka_volterra.yaml" +petab_path = Path(__file__).parent.parent / "petab" +petab_yaml = "problem.yaml" +measurements_tsv = "measurements.tsv" +model_name = "lotka_volterra" yaml2sbml.yaml2petab( yaml_dir=yaml2sbml_yaml, output_dir=str(petab_path), - sbml_name=model_name, + sbml_name=model_name, petab_yaml_name=petab_yaml, measurement_table_name=measurements_tsv, ) 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 aa6f409c02..4723d3ac36 100644 --- a/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py +++ b/python/tests/pysb_test_models/bngwiki_egfr_simple_deletemolecules.py @@ -4,98 +4,113 @@ """ from __future__ import print_function + from pysb import * Model() -Parameter('NA', 6.02e23) # Avogadro's number (molecules/mol) -Parameter('f', 1) # Fraction of the cell to simulate -Expression('Vo', f*1.0e-10) # Extracellular volume=1/cell_density (L) -Expression('V', f*3.0e-12) # Cytoplasmic volume (L) +Parameter("NA", 6.02e23) # Avogadro's number (molecules/mol) +Parameter("f", 1) # Fraction of the cell to simulate +Expression("Vo", f * 1.0e-10) # Extracellular volume=1/cell_density (L) +Expression("V", f * 3.0e-12) # Cytoplasmic volume (L) # Initial amount of ligand (20 nM) converted to copies per cell -Expression('EGF_init', 20*1e-9*NA*Vo) +Expression("EGF_init", 20 * 1e-9 * NA * Vo) # Initial amounts of cellular components (copies per cell) -Expression('EGFR_init', f*1.8e5) -Expression('Grb2_init', f*1.5e5) -Expression('Sos1_init', f*6.2e4) +Expression("EGFR_init", f * 1.8e5) +Expression("Grb2_init", f * 1.5e5) +Expression("Sos1_init", f * 6.2e4) # Rate constants # Divide by NA*V to convert bimolecular rate constants from /M/sec to # /(molecule/cell)/sec -Expression('kp1', 9.0e7/(NA*Vo)) # ligand-monomer binding -Parameter('km1', 0.06) # ligand-monomer dissociation -Expression('kp2', 1.0e7/(NA*V)) # aggregation of bound monomers -Parameter('km2', 0.1) # dissociation of bound monomers -Parameter('kp3', 0.5) # dimer transphosphorylation -Parameter('km3', 4.505) # dimer dephosphorylation -Expression('kp4', 1.5e6/(NA*V)) # binding of Grb2 to receptor -Parameter('km4', 0.05) # dissociation of Grb2 from receptor -Expression('kp5', 1.0e7/(NA*V)) # binding of Grb2 to Sos1 -Parameter('km5', 0.06) # dissociation of Grb2 from Sos1 -Parameter('deg', 0.01) # degradation of receptor dimers +Expression("kp1", 9.0e7 / (NA * Vo)) # ligand-monomer binding +Parameter("km1", 0.06) # ligand-monomer dissociation +Expression("kp2", 1.0e7 / (NA * V)) # aggregation of bound monomers +Parameter("km2", 0.1) # dissociation of bound monomers +Parameter("kp3", 0.5) # dimer transphosphorylation +Parameter("km3", 4.505) # dimer dephosphorylation +Expression("kp4", 1.5e6 / (NA * V)) # binding of Grb2 to receptor +Parameter("km4", 0.05) # dissociation of Grb2 from receptor +Expression("kp5", 1.0e7 / (NA * V)) # binding of Grb2 to Sos1 +Parameter("km5", 0.06) # dissociation of Grb2 from Sos1 +Parameter("deg", 0.01) # degradation of receptor dimers -Monomer('EGF', ['R']) -Monomer('EGFR', ['L','CR1','Y1068'], {'Y1068':('U','P')}) -Monomer('Grb2', ['SH2','SH3']) -Monomer('Sos1', ['PxxP']) +Monomer("EGF", ["R"]) +Monomer("EGFR", ["L", "CR1", "Y1068"], {"Y1068": ("U", "P")}) +Monomer("Grb2", ["SH2", "SH3"]) +Monomer("Sos1", ["PxxP"]) Initial(EGF(R=None), EGF_init) -Initial(EGFR(L=None, CR1=None, Y1068='U'), EGFR_init) +Initial(EGFR(L=None, CR1=None, Y1068="U"), EGFR_init) Initial(Grb2(SH2=None, SH3=None), Grb2_init) Initial(Sos1(PxxP=None), Sos1_init) -Observable('EGFR_tot', EGFR()) -Observable('Lig_free', EGF(R=None)) -Observable('Dim', EGFR(CR1=ANY), match='species') -Observable('RP', EGFR(Y1068=('P',WILD))) -Observable('Grb2Sos1', Grb2(SH2=None, SH3=1) % Sos1(PxxP=1)) -Observable('Sos1_act', EGFR(Y1068=1) % Grb2(SH2=1, SH3=2) % Sos1(PxxP=2)) +Observable("EGFR_tot", EGFR()) +Observable("Lig_free", EGF(R=None)) +Observable("Dim", EGFR(CR1=ANY), match="species") +Observable("RP", EGFR(Y1068=("P", WILD))) +Observable("Grb2Sos1", Grb2(SH2=None, SH3=1) % Sos1(PxxP=1)) +Observable("Sos1_act", EGFR(Y1068=1) % Grb2(SH2=1, SH3=2) % Sos1(PxxP=2)) # Ligand-receptor binding -Rule('egf_bind_egfr', - EGFR(L=None, CR1=None) + EGF(R=None) | EGFR(L=1, CR1=None) % EGF(R=1), - kp1, km1) +Rule( + "egf_bind_egfr", + EGFR(L=None, CR1=None) + EGF(R=None) | EGFR(L=1, CR1=None) % EGF(R=1), + kp1, + km1, +) # Receptor-aggregation -Rule('egfr_dimerize', - EGFR(L=ANY, CR1=None) + EGFR(L=ANY, CR1=None) | - EGFR(L=ANY, CR1=1) % EGFR(L=ANY, CR1=1), - kp2, km2) +Rule( + "egfr_dimerize", + EGFR(L=ANY, CR1=None) + EGFR(L=ANY, CR1=None) + | EGFR(L=ANY, CR1=1) % EGFR(L=ANY, CR1=1), + kp2, + km2, +) # Transphosphorylation of EGFR by RTK -Rule('egfr_transphos', - EGFR(CR1=ANY, Y1068='U') >> EGFR(CR1=ANY, Y1068='P'), kp3) +Rule("egfr_transphos", EGFR(CR1=ANY, Y1068="U") >> EGFR(CR1=ANY, Y1068="P"), kp3) # Dephosphorylation -Rule('egfr_dephos', - EGFR(Y1068='P') >> EGFR(Y1068='U'), km3) +Rule("egfr_dephos", EGFR(Y1068="P") >> EGFR(Y1068="U"), km3) # Grb2 binding to pY1068 -Rule('grb2_bind_egfr', - EGFR(Y1068='P') + Grb2(SH2=None) | EGFR(Y1068=('P',1)) % Grb2(SH2=1), - kp4, km4) +Rule( + "grb2_bind_egfr", + EGFR(Y1068="P") + Grb2(SH2=None) | EGFR(Y1068=("P", 1)) % Grb2(SH2=1), + kp4, + km4, +) # Grb2 binding to Sos1 -Rule('sos1_bind_grb2', - Grb2(SH3=None) + Sos1(PxxP=None) | Grb2(SH3=1) % Sos1(PxxP=1), - kp5, km5) +Rule( + "sos1_bind_grb2", + Grb2(SH3=None) + Sos1(PxxP=None) | Grb2(SH3=1) % Sos1(PxxP=1), + kp5, + km5, +) # Receptor dimer internalization/degradation -Rule('egfr_dimer_degrade', - EGF(R=1) % EGF(R=2) % EGFR(L=1, CR1=3) % EGFR(L=2, CR1=3) >> None, - deg, - delete_molecules=True) +Rule( + "egfr_dimer_degrade", + EGF(R=1) % EGF(R=2) % EGFR(L=1, CR1=3) % EGFR(L=2, CR1=3) >> None, + deg, + delete_molecules=True, +) -if __name__ == '__main__': +if __name__ == "__main__": print(__doc__, "\n", model) - print(""" + print( + """ NOTE: This model code is designed to be imported and programatically manipulated, not executed directly. The above output is merely a -diagnostic aid.""") \ No newline at end of file +diagnostic aid.""" + ) diff --git a/python/tests/splines_utils.py b/python/tests/splines_utils.py new file mode 100644 index 0000000000..0e1ebf27b4 --- /dev/null +++ b/python/tests/splines_utils.py @@ -0,0 +1,936 @@ +""" +Utilities for creating test SBML models containing splines, +for running them and for comparing them to a symbolically +computed ground truth. +""" + +import math +import os +import uuid +from tempfile import mkdtemp +from typing import Any, Dict, List, Optional, Sequence, Union + +import amici +import numpy as np +import pandas as pd +import petab +import sympy as sp +from amici.gradient_check import _check_results +from amici.petab_import import import_petab_problem +from amici.petab_objective import EDATAS, LLH, RDATAS, SLLH, simulate_petab +from amici.sbml_utils import ( + add_compartment, + add_inflow, + add_parameter, + add_rate_rule, + add_species, + amici_time_symbol, + create_sbml_model, +) +from amici.splines import AbstractSpline, CubicHermiteSpline, UniformGrid +from amici.testing import TemporaryDirectoryWinSafe as TemporaryDirectory +from petab.models.sbml_model import SbmlModel + + +def evaluate_spline( + spline: AbstractSpline, params: dict, tt: Sequence[float], **kwargs +): + """ + Evaluate the `AbstractSpline` `spline` at timepoints `tt` + for the parameters given in the dictionary `params`. + """ + return np.asarray([spline.evaluate(t).subs(params) for t in tt], **kwargs) + + +def integrate_spline( + spline: AbstractSpline, + params: Union[Dict, None], + tt: Sequence[float], + initial_value: float = 0, + **kwargs, +): + """ + Integrate the `AbstractSpline` `spline` at timepoints `tt` + for the parameters given in the dictionary `params`. + """ + ispline = [initial_value + spline.integrate(0, t) for t in tt] + if params is not None: + ispline = [x.subs(params) for x in ispline] + return np.asarray(ispline, **kwargs) + + +def create_condition_table() -> pd.DataFrame: + """Create a PEtab condition table.""" + condition_df = pd.DataFrame({"conditionId": ["condition1"]}) + condition_df.set_index(["conditionId"], inplace=True) + return condition_df + + +def create_parameter_table(**columns) -> pd.DataFrame: + """Create a PEtab parameter table.""" + if isinstance(columns["parameterId"], str): + columns["parameterId"] = [columns["parameterId"]] + columns.setdefault("parameterScale", "lin") + columns.setdefault("estimate", 1) + parameter_df = pd.DataFrame(columns) + parameter_df.set_index(["parameterId"], inplace=True) + return parameter_df + + +def create_observable_table(**columns) -> pd.DataFrame: + """Create a PEtab observable table.""" + if isinstance(columns["observableId"], str): + columns["observableId"] = [columns["observableId"]] + columns.setdefault("observableTransformation", "lin") + columns.setdefault("noiseDistribution", "normal") + observable_df = pd.DataFrame(columns) + observable_df.set_index(["observableId"], inplace=True) + return observable_df + + +def create_measurement_table(**columns) -> pd.DataFrame: + """Create a PEtab measurement table.""" + if isinstance(columns["observableId"], str): + columns["observableId"] = [columns["observableId"]] + columns.setdefault("simulationConditionId", "condition1") + return pd.DataFrame(columns) + + +def species(i) -> str: + """Name to use for the `i`-th species.""" + return f"z{i}" + + +def observable(i) -> str: + """ + Name to use for the `i`-th observable, + i.e., the observable associated to the + `i`-th species. + """ + return f"{species(i)}_obs" + + +def species_to_index(name) -> int: + """Get the species index from a species name.""" + assert name[0] == "z" + return int(name[1:]) + + +def create_petab_problem( + splines: List[AbstractSpline], + params_true: Dict, + initial_values: Optional[np.ndarray] = None, + use_reactions: bool = False, + measure_upsample: int = 6, + sigma: float = 1.0, + t_extrapolate: float = 0.25, + folder: Optional[str] = None, + model_name: str = "test_splines", +): + """ + Given a list of `AbstractSplines`, create a PEtab problem for the system of + ODEs given by `z_i(t)' = spline[i](t)`. + + :param params_true: + parameter values used to compute the analytical solution of the ODE + system in order to fill the PEtab measurement table + + :param initial_values: + initial values of the state variables + + :param use_reactions: + whether the ODEs are encoded in the SBML model as reactions (inflows) + or rate rules + + :param measure_upsample: + controls the number of time points at which synthetic measurements are + taken. The interval between subsequent time points is equal to the + smallest interval between subsequent spline nodes divided by + `measure_upsample` + + :param sigma: + standard deviation for additive Normal noise used to corrupt synthetic + measurements + + :param t_extrapolate: + factor controlling how long after the final spline node the simulation + should continue in order to test extrapolation methods. + + :param folder: + if not `None`, save the PEtab problem to this folder + + :param model_name: + name of the SBML model to be created + """ + + for spline in splines: + if spline.evaluate_at != amici_time_symbol: + raise ValueError( + "the given splines must be evaluated at the simulation time" + ) + + if initial_values is None: + initial_values = np.zeros(len(splines)) + + # Create SBML document + doc, model = create_sbml_model(model_name) + add_compartment(model, "compartment") + for i, spline in enumerate(splines): + spline.add_to_sbml_model(model) + add_species(model, species(i), initial_amount=initial_values[i]) + if use_reactions: + add_inflow(model, species(i), splines[i].sbml_id) + else: + add_rate_rule(model, species(i), splines[i].sbml_id) + for parId, value in params_true.items(): + add_parameter(model, parId, value=value, constant=True) + for spline in splines: + add_parameter(model, spline.sbml_id, constant=False) + + # Compute simulation time + # Must cover all the intervals of definition for the splines, + # plus something extra for extrapolated or periodic splines + T = 0 + for spline in splines: + if spline.extrapolate[0] is None and spline.nodes[0] > 0: + raise ValueError( + "if no left-extrapolation is defined for a spline, " + "its interval of definition should contain zero" + ) + if spline.extrapolate[1] is not None: + f = ( + t_extrapolate + if spline.extrapolate[1] != "periodic" + else 1 + t_extrapolate + ) + DT = f * (spline.nodes[-1] - spline.nodes[0]) + else: + DT = 0 + T = max(T, spline.nodes[-1] + DT) + + # Compute synthetic measurements + dt = min(np.diff(spline.nodes).min() for spline in splines) + dt /= measure_upsample + n_obs = math.ceil(T / dt) + 1 + tt_obs = np.linspace(0, float(T), n_obs) + zz_true = [ + integrate_spline(spline, params_true, tt_obs, iv, dtype=float) + for (spline, iv) in zip(splines, initial_values) + ] + zz_obs = [zz + sigma * np.random.randn(len(zz)) for zz in zz_true] + + # Create PEtab tables + condition_df = create_condition_table() + # ensure that same parameter order is used for all columns + _params = list(params_true.items()) + parameter_df = create_parameter_table( + parameterId=[p.name for (p, v) in _params], + lowerBound=min(v for (p, v) in _params) if _params else [], + upperBound=max(v for (p, v) in _params) if _params else [], + nominalValue=[v for (p, v) in _params], + estimate=1, + ) + observable_df = create_observable_table( + observableId=[observable(i) for i in range(len(splines))], + observableFormula=[species(i) for i in range(len(splines))], + noiseFormula=sigma if sigma > 0 else 1.0, + ) + measurement_df = create_measurement_table( + observableId=np.concatenate( + [len(tt_obs) * [observable(i)] for i in range(len(splines))] + ), + time=len(splines) * list(tt_obs), + measurement=np.concatenate(zz_obs), + ) + + # Create and validate PEtab problem + problem = petab.Problem( + model=SbmlModel( + sbml_document=doc, + sbml_model=model, + ), + condition_df=condition_df, + measurement_df=measurement_df, + parameter_df=parameter_df, + observable_df=observable_df, + ) + if petab.lint_problem(problem): + raise RuntimeError("PEtab lint failed") + + # Write PEtab problem to disk + if folder is None: + return problem, initial_values, T + folder = os.path.abspath(folder) + os.makedirs(folder, exist_ok=True) + problem.to_files( + sbml_file=os.path.join(folder, f"{model_name}_model.xml"), + condition_file=os.path.join(folder, f"{model_name}_conditions.tsv"), + measurement_file=os.path.join(folder, f"{model_name}_measurements.tsv"), + parameter_file=os.path.join(folder, f"{model_name}_parameters.tsv"), + observable_file=os.path.join(folder, f"{model_name}_observables.tsv"), + yaml_file=os.path.join(folder, f"{model_name}.yaml"), + ) + return os.path.join(folder, f"{model_name}.yaml"), initial_values, T + + +def simulate_splines( + splines, + params_true, + initial_values=None, + *, + folder: Optional[str] = None, + keep_temporary: bool = False, + benchmark: Union[bool, int] = False, + rtol: float = 1e-12, + atol: float = 1e-12, + maxsteps: int = 500_000, + discard_annotations: bool = False, + use_adjoint: bool = False, + skip_sensitivity: bool = False, + petab_problem=None, + amici_model=None, + **kwargs, +): + """ + Create a PEtab problem using `create_petab_problem` and simulate it with + AMICI. + + :param splines: + passed to `create_petab_problem` + + :param params_true: + passed to `create_petab_problem` + + :param initial_values: + passed to `create_petab_problem` + + :param folder: + working directory (a temporary one is used if not specified) + + :param keep_temporary: + whether to keep or delete temporary working directories on exit + + :param benchmark: + instead of returning the simulation data, run the simulation + `benchmark` times (defaults to `50` if `benchmark` is `True`) + and return execution times + + :param rtol: + relative tolerance for AMICI solver + + :param atol: + absolute tolerance for AMICI solver + + :param maxsteps: + maximum number of steps for AMICI solver + + :param discard_annotations: + whether to discard spline annotations, + forcing AMICI to read the spline as a piecewise assignment rule + + :param use_adjoint: + whether to use adjoint sensitivity computation + + :param skip_sensitivity: + whether to skip sensitivity computation + + :param petab_problem: + PEtab problem (if already created) + + :param amici_model: + AMICI model (if already created) + + :param kwargs: + passed to `create_petab_problem` + """ + # If no working directory is given, create a temporary one + if folder is None: + if keep_temporary: + folder = mkdtemp() + print(f"temporary folder is {folder}") + else: + with TemporaryDirectory() as folder: + return simulate_splines( + splines, + params_true, + initial_values, + folder=folder, + benchmark=benchmark, + rtol=rtol, + atol=atol, + maxsteps=maxsteps, + discard_annotations=discard_annotations, + use_adjoint=use_adjoint, + skip_sensitivity=skip_sensitivity, + petab_problem=petab_problem, + amici_model=amici_model, + **kwargs, + ) + + if petab_problem is None and amici_model is not None: + raise ValueError("if amici_model is given, petab_problem must be given too") + + if petab_problem is not None and initial_values is None: + raise ValueError("if petab_problem is given, initial_values must be given too") + + if petab_problem is None: + # Create PEtab problem + path, initial_values, T = create_petab_problem( + splines, params_true, initial_values, sigma=0.0, folder=folder, **kwargs + ) + petab_problem = petab.Problem.from_yaml(path) + + if amici_model is None: + # Create and compile AMICI model + model_id = uuid.uuid4().hex[-5:] # to prevent folder/module collisions + amici_model = import_petab_problem( + petab_problem, + discard_sbml_annotations=discard_annotations, + model_output_dir=os.path.join(folder, f"amici_models_{model_id}"), + model_name=f"splinetest_{model_id}", + ) + + # Set solver options + solver = amici_model.getSolver() + solver.setRelativeTolerance(rtol) + solver.setAbsoluteTolerance(atol) + solver.setMaxSteps(maxsteps) + if not skip_sensitivity: + solver.setSensitivityOrder(amici.SensitivityOrder.first) + if use_adjoint: + solver.setSensitivityMethod(amici.SensitivityMethod.adjoint) + else: + solver.setSensitivityMethod(amici.SensitivityMethod.forward) + + # Compute and set timepoints + # NB not working, will always be equal to the observation times + # n = max(len(spline.nodes) for spline in splines) * simulate_upsample + # tt = np.linspace(0, float(T), n) + # model.setTimepoints(tt) + + # Create dictionary for parameter values + params_str = {p.name: v for (p, v) in params_true.items()} + + if benchmark is False: + # Simulate PEtab problem + res = simulate_petab(petab_problem, amici_model, solver, params_str) + llh, rdatas, edatas = res[LLH], res[RDATAS], res[EDATAS] + assert len(rdatas) == 1 + llh = float(llh) + rdata = rdatas[0] + assert SLLH in rdata.keys() + sllh = rdata[SLLH] + assert len(edatas) == 1 + edata = edatas[0] + + # Return state/parameter ordering + state_ids = amici_model.getStateIds() + param_ids = amici_model.getParameterIds() + + return ( + initial_values, + petab_problem, + amici_model, + solver, + llh, + sllh, + rdata, + edata, + state_ids, + param_ids, + ) + + if benchmark is True: + benchmark = 50 + import time + + runtimes = [] + for _ in range(int(benchmark)): + t0 = time.perf_counter() + simulate_petab(petab_problem, amici_model, solver, params_str) + t_elapsed = time.perf_counter() - t0 + runtimes.append(t_elapsed) + + return dict( + runtimes=runtimes, + mean=np.mean(runtimes), + median=np.median(runtimes), + min=min(runtimes), + max=max(runtimes), + ) + + +def compute_ground_truth(splines, initial_values, times, params_true, params_sorted): + x_true_sym = sp.Matrix( + [ + integrate_spline(spline, None, times, iv) + for (spline, iv) in zip(splines, initial_values) + ] + ).transpose() + groundtruth = {"x_true": np.asarray(x_true_sym.subs(params_true), dtype=float)} + sx_by_state = [ + x_true_sym[:, i].jacobian(params_sorted).subs(params_true) + for i in range(x_true_sym.shape[1]) + ] + sx_by_state = [np.asarray(sx, dtype=float) for sx in sx_by_state] + groundtruth["sx_true"] = np.concatenate( + [sx[:, :, np.newaxis] for sx in sx_by_state], axis=2 + ) + return groundtruth + + +def check_splines( + splines, + params_true, + initial_values=None, + *, + discard_annotations: bool = False, + use_adjoint: bool = False, + skip_sensitivity: bool = False, + debug: Union[bool, str] = False, + parameter_lists: Optional[Sequence[Sequence[int]]] = None, + llh_rtol: float = 1e-8, + sllh_atol: float = 1e-8, + x_rtol: float = 1e-11, + x_atol: float = 1e-11, + w_rtol: float = 1e-11, + w_atol: float = 1e-11, + sx_rtol: float = 1e-10, + sx_atol: float = 1e-10, + groundtruth: Optional[Union[str, Dict[str, Any]]] = None, + **kwargs, +): + """ + Create a PEtab problem using `create_petab_problem`, + simulate it with `simulate_splines` + and check it against the analytical solution. + + :param splines: + passed to `simulate_splines` + + :param params_true: + passed to `simulate_splines` + + :param initial_values: + passed to `simulate_splines` + + :param discard_annotations: + whether to discard spline annotations, + forcing AMICI to read the spline as a piecewise assignment rule + + :param use_adjoint: + whether to use adjoint sensitivity computation + + :param skip_sensitivity: + whether to skip sensitivity computation + + :param debug: + if not `False`, do not check and return results and ground truth + instead. + If equal to `'print'`, in addition to the above print error values. + + :param parameter_lists: + Set AMICI parameter list to these values, + in order to check that partial sensitivity computation works. + + :param kwargs: + passed to `simulate_splines` + """ + if isinstance(splines, AbstractSpline): + splines = [splines] + + # Simulate PEtab problem + ( + initial_values, + petab_problem, + amici_model, + amici_solver, + llh, + sllh, + rdata, + edata, + state_ids, + param_ids, + ) = simulate_splines( + splines, + params_true, + initial_values, + discard_annotations=discard_annotations, + skip_sensitivity=skip_sensitivity, + use_adjoint=use_adjoint, + **kwargs, + ) + + tt = rdata["ts"] + + # Sort splines/ics/parameters as in the AMICI model + splines = [splines[species_to_index(name)] for name in state_ids] + initial_values = [initial_values[species_to_index(name)] for name in state_ids] + + def param_by_name(id): + for p in params_true.keys(): + if p.name == id: + return p + assert False + + params_sorted = [param_by_name(id) for id in param_ids] + + # Check states + if groundtruth == "compute": + groundtruth = compute_ground_truth( + splines, initial_values, tt, params_true, params_sorted + ) + if groundtruth is None: + x_true_sym = sp.Matrix( + [ + integrate_spline(spline, None, tt, iv) + for (spline, iv) in zip(splines, initial_values) + ] + ).transpose() + x_true = np.asarray(x_true_sym.subs(params_true), dtype=float) + else: + x_true = groundtruth["x_true"] + if not debug: + assert rdata.x.shape == x_true.shape + _check_results(rdata, "x", x_true, atol=x_atol, rtol=x_rtol) + elif debug == "print": + x_err_abs = abs(rdata["x"] - x_true) + x_err_rel = np.where(x_err_abs == 0, 0, x_err_abs / abs(x_true)) + print(f"x_atol={x_atol} x_rtol={x_rtol}") + print("x_err_abs:") + print(np.squeeze(x_err_abs)) + print("x_err_abs (maximum):") + print(x_err_abs.max()) + print("x_err_rel:") + print(np.squeeze(x_err_rel)) + print("x_err_rel (maximum):") + print(x_err_rel.max()) + + # Check spline evaluations + # TODO can we know how the splines are ordered inside w? + if False and discard_annotations and len(splines) == 1: + assert rdata["w"].shape[1] == 1 + w_true = np.column_stack( + [ + evaluate_spline(spline, params_true, tt, dtype=float) + for spline in splines + ] + ) + if not debug: + _check_results( + rdata, + "w", + w_true, + atol=w_atol, + rtol=w_rtol, + ) + elif debug == "print": + w_err_abs = abs(rdata["w"] - w_true) + w_err_rel = np.where(w_err_abs == 0, 0, w_err_abs / abs(w_true)) + print(f"w_atol={w_atol} w_rtol={w_rtol}") + print("w_err_abs:") + print(np.squeeze(w_err_abs)) + print("w_err_abs (maximum):") + print(w_err_abs.max()) + print("w_err_rel:") + print(np.squeeze(w_err_rel)) + print("w_err_rel (maximum):") + print(w_err_rel.max()) + else: + w_true = None + + # Check sensitivities + if params_sorted and not use_adjoint: + if skip_sensitivity: + pass + if groundtruth is None: + sx_by_state = [ + x_true_sym[:, i].jacobian(params_sorted).subs(params_true) + for i in range(x_true_sym.shape[1]) + ] + sx_by_state = [np.asarray(sx, dtype=float) for sx in sx_by_state] + sx_true = np.concatenate( + [sx[:, :, np.newaxis] for sx in sx_by_state], axis=2 + ) + else: + sx_true = groundtruth["sx_true"] + if not debug: + assert rdata.sx.shape == sx_true.shape + _check_results( + rdata, + "sx", + sx_true, + atol=sx_atol, + rtol=sx_rtol, + ) + elif debug == "print": + sx_err_abs = abs(rdata["sx"] - sx_true) + sx_err_rel = np.where(sx_err_abs == 0, 0, sx_err_abs / abs(sx_true)) + print(f"sx_atol={sx_atol} sx_rtol={sx_rtol}") + print("sx_err_abs:") + print(np.squeeze(sx_err_abs)) + print("sx_err_abs (maximum):") + print(sx_err_abs.max()) + print("sx_err_rel:") + print(np.squeeze(sx_err_rel)) + print("sx_err_rel (maximum):") + print(sx_err_rel.max()) + else: + assert rdata["sx"] is None + + # Check log-likelihood + llh_true = -0.5 * rdata["y"].size * np.log(2 * np.pi) + llh_error_rel = abs(llh - llh_true) / abs(llh_true) + if (llh_error_rel > llh_rtol and debug is not True) or debug == "print": + print(f"{llh_rtol=}") + print(f"{llh_error_rel=}") + if not debug: + assert llh_error_rel <= llh_rtol + + # Check log-likelihood sensitivities + # (should be all zero, since we simulated with the true parameters) + if params_sorted: + if not skip_sensitivity: + 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 + else: + assert sllh is None + + # Try different parameter lists + if not skip_sensitivity and (not use_adjoint) and parameter_lists is not None: + for plist in parameter_lists: + amici_model.setParameterList(plist) + amici_model.setTimepoints(rdata.t) + rdata_partial = amici.runAmiciSimulation(amici_model, amici_solver) + assert rdata.sx[:, plist, :].shape == rdata_partial.sx.shape + assert np.allclose(rdata.sx[:, plist, :], rdata_partial.sx) + + if debug: + return dict( + splines=splines, + initial_values=initial_values, + petab_problem=petab_problem, + amici_model=amici_model, + groundtruth=groundtruth, + rdata=rdata, + params_true=params_true, + params_sorted=params_sorted, + x_true=x_true, + w_true=w_true, + sx_true=sx_true, + llh_true=llh_true, + ) + else: + return dict( + initial_values=initial_values, + petab_problem=petab_problem, + amici_model=amici_model, + groundtruth=groundtruth, + ) + + +def check_splines_full( + splines, + params, + tols, + *args, + check_piecewise: bool = True, + check_forward: bool = True, + check_adjoint: bool = True, + folder: Optional[str] = None, + groundtruth: Optional[Union[dict, str]] = "compute", + return_groundtruth: bool = False, + **kwargs, +): + """ + Check example PEtab problem with `check_splines` + both using adjoint and forward sensitivities + and also in the case in which the splines are read as piecewise functions. + """ + if folder is None: + with TemporaryDirectory() as folder: + return check_splines_full( + splines, + params, + tols, + *args, + check_piecewise=check_piecewise, + check_forward=check_forward, + check_adjoint=check_adjoint, + folder=folder, + groundtruth=groundtruth, + return_groundtruth=return_groundtruth, + **kwargs, + ) + + if isinstance(tols, dict): + tols1 = tols2 = tols3 = tols + else: + tols1, tols2, tols3 = tols + + if isinstance(splines, AbstractSpline): + splines = [splines] + + contains_periodic = any( + spline.extrapolate == ("periodic", "periodic") for spline in splines + ) + + # Amortize creation of PEtab and AMICI objects + results = None + initial_values = None + petab_problem = None + amici_model = None + + if check_piecewise and not contains_periodic: + results = check_splines( + splines, + params, + *args, + **kwargs, + **tols1, + folder=folder, + discard_annotations=True, + use_adjoint=False, + groundtruth=groundtruth, + ) + initial_values = results["initial_values"] + petab_problem = results["petab_problem"] + groundtruth = results["groundtruth"] + + if check_forward: + results = check_splines( + splines, + params, + *args, + **kwargs, + **tols2, + initial_values=initial_values, + folder=folder, + petab_problem=petab_problem, + use_adjoint=False, + groundtruth=groundtruth, + ) + initial_values = results["initial_values"] + petab_problem = results["petab_problem"] + amici_model = results["amici_model"] + groundtruth = results["groundtruth"] + + if check_adjoint: + results = check_splines( + splines, + params, + *args, + **kwargs, + **tols3, + initial_values=initial_values, + folder=folder, + petab_problem=petab_problem, + amici_model=amici_model, + use_adjoint=True, + groundtruth=( + None if groundtruth == "compute" else groundtruth + ), # do not compute sensitivities if possible + ) + + if return_groundtruth: + if groundtruth is not None and not isinstance(groundtruth, str): + return groundtruth + elif results is None: + return None + else: + return results["groundtruth"] + + +def example_spline_1( + idx: int = 0, + offset: float = 0, + scale: float = 1, + num_nodes: int = 9, + fixed_values=None, # a list of indices or 'all' + extrapolate=None, +): + """A simple spline with no extrapolation.""" + + yy_true = np.asarray( + [0.0, 2.0, 5.0, 6.0, 5.0, 4.0, 2.0, 3.0, 4.0, 6.0, 7.0, 7.5, 6.5, 4.0] + ) + if num_nodes is not None: + assert 1 < num_nodes <= len(yy_true) + yy_true = yy_true[:num_nodes] + yy_true = scale * yy_true + offset + xx = UniformGrid(0, 25, number_of_nodes=len(yy_true)) + yy = list(sp.symbols(f"y{idx}_0:{len(yy_true)}")) + + if fixed_values is None: + params = dict(zip(yy, yy_true)) + elif fixed_values == "all": + params = {} + for i in range(len(yy_true)): + yy[i] = yy_true[i] + else: + params = {} + for i in range(len(yy_true)): + if i in fixed_values: + yy[i] = yy_true[i] + else: + params[yy[i]] = yy_true[i] + + spline = CubicHermiteSpline( + f"y{idx}", nodes=xx, values_at_nodes=yy, bc=None, extrapolate=extrapolate + ) + + if os.name == "nt": + 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), + ) + else: + tols = ( + dict(llh_rtol=1e-15), + dict(llh_rtol=1e-15), + dict(llh_rtol=1e-15, sllh_atol=5e-8), + ) + + return spline, params, tols + + +def example_spline_2(idx: int = 0): + """A simple periodic spline.""" + yy_true = [0.0, 2.0, 3.0, 4.0, 1.0, -0.5, -1, -1.5, 0.5, 0.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)) + spline = CubicHermiteSpline( + f"y{idx}", nodes=xx, values_at_nodes=yy, bc="periodic", extrapolate="periodic" + ) + tols = ( + dict(llh_rtol=1e-15), + dict(llh_rtol=1e-15), + dict(llh_rtol=1e-15, sllh_atol=5e-8, x_rtol=1e-10, x_atol=5e-10), + ) + return spline, params, tols + + +def example_spline_3(idx: int = 0): + """A simple spline with extrapolation on the right side.""" + 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)) + spline = CubicHermiteSpline( + f"y{idx}", + nodes=xx, + values_at_nodes=yy, + bc=(None, "zeroderivative"), + extrapolate=(None, "constant"), + ) + tols = {} + return spline, params, tols diff --git a/python/tests/test_bngl.py b/python/tests/test_bngl.py index b1b0c117e5..42926e379a 100644 --- a/python/tests/test_bngl.py +++ b/python/tests/test_bngl.py @@ -1,38 +1,56 @@ import os +import amici import numpy as np import pytest -import amici - pysb = pytest.importorskip("pysb") from amici.bngl_import import bngl2amici -from pysb.simulator import ScipyOdeSimulator +from amici.testing import TemporaryDirectoryWinSafe, skip_on_valgrind from pysb.importers.bngl import model_from_bngl -from amici.testing import skip_on_valgrind, TemporaryDirectoryWinSafe - +from pysb.simulator import ScipyOdeSimulator tests = [ - 'CaOscillate_Func', 'deleteMolecules', 'empty_compartments_block', - 'gene_expr', 'gene_expr_func', 'gene_expr_simple', 'isomerization', - 'Motivating_example_cBNGL', 'motor', 'simple_system', - 'test_compartment_XML', 'test_setconc', 'test_synthesis_cBNGL_simple', - 'test_synthesis_complex', 'test_synthesis_complex_0_cBNGL', - 'test_synthesis_complex_source_cBNGL', 'test_synthesis_simple', - 'univ_synth', 'Repressilator', 'test_paramname', 'tlmr' + "CaOscillate_Func", + "deleteMolecules", + "empty_compartments_block", + "gene_expr", + "gene_expr_func", + "gene_expr_simple", + "isomerization", + "Motivating_example_cBNGL", + "motor", + "simple_system", + "test_compartment_XML", + "test_setconc", + "test_synthesis_cBNGL_simple", + "test_synthesis_complex", + "test_synthesis_complex_0_cBNGL", + "test_synthesis_complex_source_cBNGL", + "test_synthesis_simple", + "univ_synth", + "Repressilator", + "test_paramname", + "tlmr", ] @skip_on_valgrind -@pytest.mark.parametrize('example', tests) +@pytest.mark.parametrize("example", tests) def test_compare_to_pysb_simulation(example): atol = 1e-8 rtol = 1e-8 - model_file = os.path.join(os.path.dirname(__file__), '..', '..', - 'ThirdParty', 'BioNetGen-2.7.0', 'Validate', - f'{example}.bngl') + model_file = os.path.join( + os.path.dirname(__file__), + "..", + "..", + "ThirdParty", + "BioNetGen-2.7.0", + "Validate", + f"{example}.bngl", + ) pysb_model = model_from_bngl(model_file) @@ -41,17 +59,17 @@ def test_compare_to_pysb_simulation(example): sim = ScipyOdeSimulator( pysb_model, tspan=tspan, - integrator_options={'rtol': rtol, 'atol': atol}, - compiler='python' + integrator_options={"rtol": rtol, "atol": atol}, + compiler="python", ) pysb_simres = sim.run() # amici part - cl = example not in ['Motivating_example_cBNGL', 'univ_synth'] + cl = example not in ["Motivating_example_cBNGL", "univ_synth"] kwargs = { - 'compute_conservation_laws': cl, - 'observables': list(pysb_model.observables.keys()) + "compute_conservation_laws": cl, + "observables": list(pysb_model.observables.keys()), } with TemporaryDirectoryWinSafe(prefix=pysb_model.name) as outdir: @@ -59,15 +77,14 @@ def test_compare_to_pysb_simulation(example): with pytest.raises(ValueError, match="Conservation laws"): bngl2amici(model_file, outdir, compute_conservation_laws=True) - if example in ['empty_compartments_block', 'motor']: + if example in ["empty_compartments_block", "motor"]: with pytest.raises(ValueError, match="Cannot add"): bngl2amici(model_file, outdir, **kwargs) return else: bngl2amici(model_file, outdir, **kwargs) - amici_model_module = amici.import_model_module(pysb_model.name, - outdir) + amici_model_module = amici.import_model_module(pysb_model.name, outdir) model_amici = amici_model_module.getModel() diff --git a/python/tests/test_compare_conservation_laws_sbml.py b/python/tests/test_compare_conservation_laws_sbml.py index 92dc319bd4..79a26fd948 100644 --- a/python/tests/test_compare_conservation_laws_sbml.py +++ b/python/tests/test_compare_conservation_laws_sbml.py @@ -1,37 +1,38 @@ import os import warnings +import amici import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal -import amici - @pytest.fixture def edata_fixture(): """edata is generated to test pre- and postequilibration""" - edata_pre = amici.ExpData(2, 0, 0, - np.array([0., 0.1, 0.2, 0.5, 1., 2., 5., 10.])) + edata_pre = amici.ExpData( + 2, 0, 0, np.array([0.0, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]) + ) edata_pre.setObservedData([1.5] * 16) - edata_pre.fixedParameters = np.array([5., 20.]) - edata_pre.fixedParametersPreequilibration = np.array([0., 10.]) + edata_pre.fixedParameters = np.array([5.0, 20.0]) + edata_pre.fixedParametersPreequilibration = np.array([0.0, 10.0]) edata_pre.reinitializeFixedParameterInitialStates = True # edata for postequilibration - edata_post = amici.ExpData(2, 0, 0, - np.array([float('inf')] * 3)) + edata_post = amici.ExpData(2, 0, 0, np.array([float("inf")] * 3)) edata_post.setObservedData([0.75] * 6) - edata_post.fixedParameters = np.array([7.5, 30.]) + edata_post.fixedParameters = np.array([7.5, 30.0]) # edata with both equilibrations - edata_full = amici.ExpData(2, 0, 0, - np.array( - [0., 0., 0., 1., 2., 2., 4., float('inf'), - float('inf')])) + edata_full = amici.ExpData( + 2, + 0, + 0, + np.array([0.0, 0.0, 0.0, 1.0, 2.0, 2.0, 4.0, float("inf"), float("inf")]), + ) edata_full.setObservedData([3.14] * 18) - edata_full.fixedParameters = np.array([1., 2.]) - edata_full.fixedParametersPreequilibration = np.array([3., 4.]) + edata_full.fixedParameters = np.array([1.0, 2.0]) + edata_full.fixedParametersPreequilibration = np.array([3.0, 4.0]) edata_full.reinitializeFixedParameterInitialStates = True return edata_pre, edata_post, edata_full @@ -40,43 +41,51 @@ def edata_fixture(): @pytest.fixture(scope="session") def models(): # SBML model we want to import - sbml_file = os.path.join(os.path.dirname(__file__), '..', - 'examples', 'example_constant_species', - 'model_constant_species.xml') + sbml_file = os.path.join( + os.path.dirname(__file__), + "..", + "examples", + "example_constant_species", + "model_constant_species.xml", + ) sbml_importer = amici.SbmlImporter(sbml_file) # Name of the model that will also be the name of the python module - model_name = model_output_dir = 'model_constant_species' - model_name_cl = model_output_dir_cl = 'model_constant_species_cl' + model_name = model_output_dir = "model_constant_species" + model_name_cl = model_output_dir_cl = "model_constant_species_cl" # Define constants, observables, sigmas - constant_parameters = ['synthesis_substrate', 'init_enzyme'] + constant_parameters = ["synthesis_substrate", "init_enzyme"] observables = { - 'observable_product': {'name': '', 'formula': 'product'}, - 'observable_substrate': {'name': '', 'formula': 'substrate'}, + "observable_product": {"name": "", "formula": "product"}, + "observable_substrate": {"name": "", "formula": "substrate"}, } - sigmas = {'observable_product': 1.0, 'observable_substrate': 1.0} + sigmas = {"observable_product": 1.0, "observable_substrate": 1.0} # wrap models with and without conservations laws - sbml_importer.sbml2amici(model_name_cl, - model_output_dir_cl, - observables=observables, - constant_parameters=constant_parameters, - sigmas=sigmas) - sbml_importer.sbml2amici(model_name, - model_output_dir, - observables=observables, - constant_parameters=constant_parameters, - sigmas=sigmas, - compute_conservation_laws=False) + sbml_importer.sbml2amici( + model_name_cl, + model_output_dir_cl, + observables=observables, + constant_parameters=constant_parameters, + sigmas=sigmas, + ) + sbml_importer.sbml2amici( + model_name, + model_output_dir, + observables=observables, + constant_parameters=constant_parameters, + sigmas=sigmas, + compute_conservation_laws=False, + ) # load both models model_without_cl_module = amici.import_model_module( - model_name, - module_path=os.path.abspath(model_name)) + model_name, module_path=os.path.abspath(model_name) + ) model_with_cl_module = amici.import_model_module( - model_name_cl, - module_path=os.path.abspath(model_name_cl)) + model_name_cl, module_path=os.path.abspath(model_name_cl) + ) # get the models and return model_without_cl = model_without_cl_module.getModel() @@ -84,11 +93,15 @@ def models(): return model_with_cl, model_without_cl -def get_results(model, edata=None, sensi_order=0, - sensi_meth=amici.SensitivityMethod.forward, - sensi_meth_preeq=amici.SensitivityMethod.forward, - stst_sensi_mode=amici.SteadyStateSensitivityMode.newtonOnly, - reinitialize_states=False): +def get_results( + model, + edata=None, + sensi_order=0, + sensi_meth=amici.SensitivityMethod.forward, + sensi_meth_preeq=amici.SensitivityMethod.forward, + stst_sensi_mode=amici.SteadyStateSensitivityMode.newtonOnly, + reinitialize_states=False, +): # set model and data properties model.setReinitializeFixedParameterInitialStates(reinitialize_states) @@ -116,69 +129,82 @@ def test_compare_conservation_laws_sbml(models, edata_fixture): assert model_without_cl.nx_rdata == model_with_cl.nx_rdata assert model_with_cl.nx_solver < model_without_cl.nx_solver assert len(model_with_cl.getStateIdsSolver()) == model_with_cl.nx_solver - assert len(model_without_cl.getStateIdsSolver()) \ - == model_without_cl.nx_solver + assert len(model_without_cl.getStateIdsSolver()) == model_without_cl.nx_solver # ----- compare simulations wo edata, sensi = 0, states ------------------ # run simulations rdata_cl = get_results(model_with_cl) - assert rdata_cl['status'] == amici.AMICI_SUCCESS + assert rdata_cl["status"] == amici.AMICI_SUCCESS rdata = get_results(model_without_cl) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS # compare state trajectories - assert_allclose(rdata['x'], rdata_cl['x'], - rtol=1.e-5, atol=1.e-8, - err_msg="rdata.x mismatch") + assert_allclose( + rdata["x"], rdata_cl["x"], rtol=1.0e-5, atol=1.0e-8, err_msg="rdata.x mismatch" + ) # ----- compare simulations wo edata, sensi = 1, states and sensis ------- # run simulations rdata_cl = get_results(model_with_cl, sensi_order=1) - assert rdata_cl['status'] == amici.AMICI_SUCCESS + assert rdata_cl["status"] == amici.AMICI_SUCCESS rdata = get_results(model_without_cl, sensi_order=1) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS # compare state trajectories - for field in ['x', 'sx']: - assert_allclose(rdata[field], rdata_cl[field], - rtol=1.e-5, atol=1.e-8, - err_msg=f"rdata.{field} mismatch") + for field in ["x", "sx"]: + assert_allclose( + rdata[field], + rdata_cl[field], + rtol=1.0e-5, + atol=1.0e-8, + err_msg=f"rdata.{field} mismatch", + ) # ----- compare simulations wo edata, sensi = 0, states and sensis ------- # run simulations edata, _, _ = edata_fixture rdata_cl = get_results(model_with_cl, edata=edata) - assert rdata_cl['status'] == amici.AMICI_SUCCESS + assert rdata_cl["status"] == amici.AMICI_SUCCESS rdata = get_results(model_without_cl, edata=edata) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS # compare preequilibrated states - for field in ['x', 'x_ss', 'llh']: - assert_allclose(rdata[field], rdata_cl[field], - rtol=1.e-5, atol=1.e-8, - err_msg=f"rdata.{field} mismatch") + for field in ["x", "x_ss", "llh"]: + assert_allclose( + rdata[field], + rdata_cl[field], + rtol=1.0e-5, + atol=1.0e-8, + err_msg=f"rdata.{field} mismatch", + ) # ----- compare simulations wo edata, sensi = 1, states and sensis ------- # run simulations rdata_cl = get_results(model_with_cl, edata=edata, sensi_order=1) - assert rdata_cl['status'] == amici.AMICI_SUCCESS + assert rdata_cl["status"] == amici.AMICI_SUCCESS rdata = get_results( - model_without_cl, edata=edata, sensi_order=1, - stst_sensi_mode=amici.SteadyStateSensitivityMode.integrateIfNewtonFails + model_without_cl, + edata=edata, + sensi_order=1, + stst_sensi_mode=amici.SteadyStateSensitivityMode.integrateIfNewtonFails, ) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS # check that steady state computation succeeded only by sim in full model - assert_array_equal(rdata['preeq_status'], np.array([[-3, 1, 0]])) + assert_array_equal(rdata["preeq_status"], np.array([[-3, 1, 0]])) # check that steady state computation succeeded by Newton in reduced model - assert_array_equal(rdata_cl['preeq_status'], np.array([[1, 0, 0]])) + assert_array_equal(rdata_cl["preeq_status"], np.array([[1, 0, 0]])) # compare state sensitivities with edata and preequilibration - for field in ['x', 'x_ss', 'sx', 'llh', 'sllh']: - assert_allclose(rdata[field], rdata_cl[field], - rtol=1.e-5, atol=1.e-8, - err_msg=f"rdata.{field} mismatch") + for field in ["x", "x_ss", "sx", "llh", "sllh"]: + assert_allclose( + rdata[field], + rdata_cl[field], + rtol=1.0e-5, + atol=1.0e-8, + err_msg=f"rdata.{field} mismatch", + ) # ----- check failure st.st. sensi computation if run wo CLs ------------- # check failure of steady state sensitivity computation if run wo CLs @@ -188,7 +214,7 @@ def test_compare_conservation_laws_sbml(models, edata_fixture): with warnings.catch_warnings(): warnings.filterwarnings("ignore") rdata = get_results(model_without_cl, edata=edata, sensi_order=1) - assert rdata['status'] == amici.AMICI_ERROR + assert rdata["status"] == amici.AMICI_ERROR def test_adjoint_pre_and_post_equilibration(models, edata_fixture): @@ -201,42 +227,51 @@ def test_adjoint_pre_and_post_equilibration(models, edata_fixture): # compare different ways of preequilibration, full rank Jacobian # forward preequilibration, forward simulation rff_cl = get_results( - model_cl, edata=edata, sensi_order=1, + model_cl, + edata=edata, + sensi_order=1, sensi_meth=amici.SensitivityMethod.forward, sensi_meth_preeq=amici.SensitivityMethod.forward, - reinitialize_states=reinit) + reinitialize_states=reinit, + ) # forward preequilibration, adjoint simulation rfa_cl = get_results( - model_cl, edata=edata, sensi_order=1, + model_cl, + edata=edata, + sensi_order=1, sensi_meth=amici.SensitivityMethod.adjoint, sensi_meth_preeq=amici.SensitivityMethod.forward, - reinitialize_states=reinit) + reinitialize_states=reinit, + ) # adjoint preequilibration, adjoint simulation raa_cl = get_results( - model_cl, edata=edata, sensi_order=1, + model_cl, + edata=edata, + sensi_order=1, sensi_meth=amici.SensitivityMethod.adjoint, sensi_meth_preeq=amici.SensitivityMethod.adjoint, - reinitialize_states=reinit) + reinitialize_states=reinit, + ) # assert all are close - assert_allclose(rff_cl['sllh'], rfa_cl['sllh'], - rtol=1.e-5, atol=1.e-8) - assert_allclose(rfa_cl['sllh'], raa_cl['sllh'], - rtol=1.e-5, atol=1.e-8) - assert_allclose(raa_cl['sllh'], rff_cl['sllh'], - rtol=1.e-5, atol=1.e-8) + assert_allclose(rff_cl["sllh"], rfa_cl["sllh"], rtol=1.0e-5, atol=1.0e-8) + assert_allclose(rfa_cl["sllh"], raa_cl["sllh"], rtol=1.0e-5, atol=1.0e-8) + assert_allclose(raa_cl["sllh"], rff_cl["sllh"], rtol=1.0e-5, atol=1.0e-8) # compare fully adjoint approach to simulation with singular # Jacobian raa = get_results( - model, edata=edata, sensi_order=1, + model, + edata=edata, + sensi_order=1, sensi_meth=amici.SensitivityMethod.adjoint, sensi_meth_preeq=amici.SensitivityMethod.adjoint, stst_sensi_mode=amici.SteadyStateSensitivityMode.integrateIfNewtonFails, - reinitialize_states=reinit) + reinitialize_states=reinit, + ) # assert gradients are close (quadrature tolerances are laxer) - assert_allclose(raa_cl['sllh'], raa['sllh'], 1e-5, 1e-5) + assert_allclose(raa_cl["sllh"], raa["sllh"], 1e-5, 1e-5) def test_get_set_model_settings(models): diff --git a/python/tests/test_conserved_quantities_demartino.py b/python/tests/test_conserved_quantities_demartino.py index b25d09df5a..3c67d0145a 100644 --- a/python/tests/test_conserved_quantities_demartino.py +++ b/python/tests/test_conserved_quantities_demartino.py @@ -5,57 +5,177 @@ import numpy as np import pytest import sympy as sp - -from amici.conserved_quantities_demartino import ( - _fill, _kernel, - _output as output, - compute_moiety_conservation_laws -) +from amici.conserved_quantities_demartino import _fill, _kernel +from amici.conserved_quantities_demartino import _output as output +from amici.conserved_quantities_demartino import compute_moiety_conservation_laws from amici.logging import get_logger, log_execution_time from amici.testing import skip_on_valgrind - logger = get_logger(__name__) # reference data for `engaged_species` after kernel() demartino2014_kernel_engaged_species = [ - 179, 181, 185, 186, 187, 190, 191, 194, 195, 197, 198, 200, 208, 209, 210, - 211, 214, 215, 218, 219, 221, 222, 224, 277, 292, 340, 422, 467, 468, 490, - 491, 598, 613, 966, 968, 1074, 1171, 1221, 1223, 1234, 1266, 1478, 1479, - 1480, 1481, 1496, 1497, 1498, 1501, 1526, 1527, 1528, 1529, 394, 1066, 398, - 465, 466, 594, 671, 429, 990, 652, 655, 662, 663, 664, 665, 666, 667, 668, - 669, 759, 760, 920, 921, 569, 1491, 1055, 1546, 276, 1333, 1421, 1429, - 1430, 1438, 1551, 1428, 1439, 1552, 1513, 1553, 1520, 1523, 1530, 1531, - 384, 1536, 440, 1537, 447, 1538, 456, 1539, 582, 1540, 876, 1541, 885, - 1542, 911, 1543, 978, 1544, 1010, 1545, 1070, 1547, 761, 1127, 1548, 1324, - 1549, 1370, 1550, 1554, 1560, 1555, 1580, 1556, 1644 + 179, + 181, + 185, + 186, + 187, + 190, + 191, + 194, + 195, + 197, + 198, + 200, + 208, + 209, + 210, + 211, + 214, + 215, + 218, + 219, + 221, + 222, + 224, + 277, + 292, + 340, + 422, + 467, + 468, + 490, + 491, + 598, + 613, + 966, + 968, + 1074, + 1171, + 1221, + 1223, + 1234, + 1266, + 1478, + 1479, + 1480, + 1481, + 1496, + 1497, + 1498, + 1501, + 1526, + 1527, + 1528, + 1529, + 394, + 1066, + 398, + 465, + 466, + 594, + 671, + 429, + 990, + 652, + 655, + 662, + 663, + 664, + 665, + 666, + 667, + 668, + 669, + 759, + 760, + 920, + 921, + 569, + 1491, + 1055, + 1546, + 276, + 1333, + 1421, + 1429, + 1430, + 1438, + 1551, + 1428, + 1439, + 1552, + 1513, + 1553, + 1520, + 1523, + 1530, + 1531, + 384, + 1536, + 440, + 1537, + 447, + 1538, + 456, + 1539, + 582, + 1540, + 876, + 1541, + 885, + 1542, + 911, + 1543, + 978, + 1544, + 1010, + 1545, + 1070, + 1547, + 761, + 1127, + 1548, + 1324, + 1549, + 1370, + 1550, + 1554, + 1560, + 1555, + 1580, + 1556, + 1644, ] @pytest.fixture(scope="session") def data_demartino2014(): """Get tests from DeMartino2014 Suppl. Material""" - import urllib.request - import io import gzip + import io + import urllib.request # stoichiometric matrix response = urllib.request.urlopen( - r'https://github.com/AMICI-dev/AMICI/files/11430971/DeMartinoDe2014_test-ecoli.dat.gz', - timeout=10 + r"https://github.com/AMICI-dev/AMICI/files/11430971/DeMartinoDe2014_test-ecoli.dat.gz", + timeout=10, ) data = gzip.GzipFile(fileobj=io.BytesIO(response.read())) - S = [int(item) for sl in - [entry.decode('ascii').strip().split('\t') - for entry in data.readlines()] for item in sl] + S = [ + int(item) + for sl in [ + entry.decode("ascii").strip().split("\t") for entry in data.readlines() + ] + for item in sl + ] # metabolite / row names response = urllib.request.urlopen( - r'https://github.com/AMICI-dev/AMICI/files/11430970/test-ecoli-met.txt', - timeout=10 + r"https://github.com/AMICI-dev/AMICI/files/11430970/test-ecoli-met.txt", + timeout=10, ) - row_names = [entry.decode('ascii').strip() - for entry in io.BytesIO(response.read())] + row_names = [entry.decode("ascii").strip() for entry in io.BytesIO(response.read())] return S, row_names @@ -67,34 +187,46 @@ def test_kernel_demartino2014(data_demartino2014, quiet=True): stoichiometric_list, row_names = data_demartino2014 num_species = 1668 num_reactions = 2381 - assert len(stoichiometric_list) == num_species * num_reactions, \ - "Unexpected dimension of stoichiometric matrix" + assert ( + len(stoichiometric_list) == num_species * num_reactions + ), "Unexpected dimension of stoichiometric matrix" # Expected number of metabolites per conservation law found after kernel() - expected_num_species = \ - [53] + [2] * 11 + [6] + [3] * 2 + [2] * 15 + [3] + [2] * 5 + expected_num_species = [53] + [2] * 11 + [6] + [3] * 2 + [2] * 15 + [3] + [2] * 5 - (kernel_dim, engaged_species, int_kernel_dim, conserved_moieties, - cls_species_idxs, cls_coefficients) = _kernel( - stoichiometric_list, num_species, num_reactions) + ( + kernel_dim, + engaged_species, + int_kernel_dim, + conserved_moieties, + cls_species_idxs, + cls_coefficients, + ) = _kernel(stoichiometric_list, num_species, num_reactions) if not quiet: - output(int_kernel_dim, kernel_dim, engaged_species, cls_species_idxs, - cls_coefficients, row_names) + output( + int_kernel_dim, + kernel_dim, + engaged_species, + cls_species_idxs, + cls_coefficients, + row_names, + ) # There are 38 conservation laws, engaging 131 metabolites # 36 are integers (conserved moieties), engaging 128 metabolites (from C++) assert kernel_dim == 38, "Not all conservation laws found" assert int_kernel_dim == 36, "Not all conserved moiety laws found" - assert engaged_species == demartino2014_kernel_engaged_species, \ - "Wrong engaged metabolites reported" - assert len(conserved_moieties) == 128, \ - "Wrong number of conserved moieties reported" + assert ( + engaged_species == demartino2014_kernel_engaged_species + ), "Wrong engaged metabolites reported" + assert len(conserved_moieties) == 128, "Wrong number of conserved moieties reported" # Assert that each conserved moiety has the correct number of metabolites for i in range(int_kernel_dim - 2): - assert (len(cls_species_idxs[i]) == expected_num_species[i]), \ - f"Moiety #{i + 1} failed for test case (De Martino et al.)" + assert ( + len(cls_species_idxs[i]) == expected_num_species[i] + ), f"Moiety #{i + 1} failed for test case (De Martino et al.)" @skip_on_valgrind @@ -102,111 +234,554 @@ def test_fill_demartino2014(data_demartino2014): """Test creation of interaction matrix""" stoichiometric_list, row_names = data_demartino2014 num_species = 1668 - J, J2, fields = _fill(stoichiometric_list, - demartino2014_kernel_engaged_species, num_species) + J, J2, fields = _fill( + stoichiometric_list, demartino2014_kernel_engaged_species, num_species + ) ref_for_J = [ - [25, 27], [12, 42], [13, 43], [14, 44], [15, 41], [16, 45], - [17, 47], [18, 48], [19, 23, 49], [20, 50], [21, 51], [22, 52], - [1, 23, 30, 35], [2, 23, 29, 35], [3, 23, 35, 46], - [4, 23, 33, 35], [5, 23, 31, 35], [6, 23, 35, 37], - [7, 23, 28, 35], [8, 23, 32, 35], [9, 23, 34, 35], - [10, 23, 35, 40], [11, 23, 35, 36], - [8, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 28, - 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 46], [23, 25], - [0, 23, 24, 35], [23], [0, 28], [18, 23, 27, 35], - [13, 23, 35, 42], [12, 23, 35, 47], [16, 23, 35, 47], - [19, 23, 35, 45], [15, 23, 35, 44], [20, 23, 35, 48], - [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 25, 28, 29, - 30, 31, 32, 33, 34, 36, 37, 40, 46], [22, 23, 35, 49], - [17, 23, 35, 50], [23, 51], [23, 41], [21, 23, 35, 52], - [4, 39], [1, 29], [2, 46], [3, 33], [5, 32], [14, 23, 35, 43], - [6, 30, 31], [7, 34], [8, 36], [9, 37], [10, 38], [11, 40], - [54], [53], [58, 80], [57, 59, 82], [56], [55, 59, 80], - [56, 58, 82], [61], [60], [63], [62], [65], [64], [67, 68, 69], - [66, 68, 69], [66, 67, 69, 70, 71, 94, 95], - [66, 67, 68, 70, 71, 94, 95], [68, 69, 71], [68, 69, 70], [73], - [72], [75], [74], [77], [76], [79], [78], [55, 58, 81], [80], - [56, 59], [84], [83, 85, 87], [84, 86, 87], [85], [84, 85], - [89], [88], [91], [90], [93], [92], [68, 69, 95], [68, 69, 94], - [97], [96], [99], [98], [101], [100], [103], [102], [105], - [104], [107], [106], [109], [108], [111], [110], [113], [112], - [115], [114], [117], [116], [119], [118, 120], [119], [122], - [121], [124], [123], [126], [125], [128], [127], [130], [129] + [25, 27], + [12, 42], + [13, 43], + [14, 44], + [15, 41], + [16, 45], + [17, 47], + [18, 48], + [19, 23, 49], + [20, 50], + [21, 51], + [22, 52], + [1, 23, 30, 35], + [2, 23, 29, 35], + [3, 23, 35, 46], + [4, 23, 33, 35], + [5, 23, 31, 35], + [6, 23, 35, 37], + [7, 23, 28, 35], + [8, 23, 32, 35], + [9, 23, 34, 35], + [10, 23, 35, 40], + [11, 23, 35, 36], + [ + 8, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 24, + 25, + 26, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 46, + ], + [23, 25], + [0, 23, 24, 35], + [23], + [0, 28], + [18, 23, 27, 35], + [13, 23, 35, 42], + [12, 23, 35, 47], + [16, 23, 35, 47], + [19, 23, 35, 45], + [15, 23, 35, 44], + [20, 23, 35, 48], + [ + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 25, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 36, + 37, + 40, + 46, + ], + [22, 23, 35, 49], + [17, 23, 35, 50], + [23, 51], + [23, 41], + [21, 23, 35, 52], + [4, 39], + [1, 29], + [2, 46], + [3, 33], + [5, 32], + [14, 23, 35, 43], + [6, 30, 31], + [7, 34], + [8, 36], + [9, 37], + [10, 38], + [11, 40], + [54], + [53], + [58, 80], + [57, 59, 82], + [56], + [55, 59, 80], + [56, 58, 82], + [61], + [60], + [63], + [62], + [65], + [64], + [67, 68, 69], + [66, 68, 69], + [66, 67, 69, 70, 71, 94, 95], + [66, 67, 68, 70, 71, 94, 95], + [68, 69, 71], + [68, 69, 70], + [73], + [72], + [75], + [74], + [77], + [76], + [79], + [78], + [55, 58, 81], + [80], + [56, 59], + [84], + [83, 85, 87], + [84, 86, 87], + [85], + [84, 85], + [89], + [88], + [91], + [90], + [93], + [92], + [68, 69, 95], + [68, 69, 94], + [97], + [96], + [99], + [98], + [101], + [100], + [103], + [102], + [105], + [104], + [107], + [106], + [109], + [108], + [111], + [110], + [113], + [112], + [115], + [114], + [117], + [116], + [119], + [118, 120], + [119], + [122], + [121], + [124], + [123], + [126], + [125], + [128], + [127], + [130], + [129], ] ref_for_J2 = [ - [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], [-1, -1], - [-1, -1], [-1, -1], [-1, -2, -1], [-1, -1], [-1, -1], - [-1, -1], [-1, 1, -1, -1], [-1, 1, -1, -1], [-1, 1, -1, -1], - [-1, 1, -1, -1], [-1, 1, -1, -1], [-1, 1, -1, -1], - [-1, 1, -1, -1], [-1, 1, -1, -1], [-1, 1, -1, -1], - [-1, 1, -1, -1], [-1, 1, -1, -1], - [-2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, -2, 1, -1, -1, -1, -1, - -3, -6, -6, -1, -13, -7, -3, -3, -3, -5, -5], [-2, -1], - [-1, 1, -1, -2], [-1], [-1, -2], [-1, -1, -2, 1], - [-1, -1, 1, -2], [-1, -1, 1, -1], [-1, -3, 1, -2], - [-1, -6, 1, -2], [-1, -6, 1, -2], [-1, -1, 1, -2], - [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -13, -2, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1], [-1, -7, 1, -2], [-1, -3, 1, -2], - [-3, -2], [-3, -2], [-1, -5, 1, -2], [-1, -2], [-1, -2], - [-1, -2], [-1, -2], [-1, -2], [-1, -5, 1, -2], [-1, -1, -2], - [-1, -2], [-1, -2], [-1, -2], [-1, -2], [-1, -2], [-2], [-2], - [1, -1], [-2, -1, -1], [-2], [1, -1, -1], [-1, -1, 1], [-1], - [-1], [-2], [-2], [-2], [-2], [-2, -1, 1], [-2, 1, -1], - [-1, 1, -3, -1, 1, -1, 1], [1, -1, -3, 1, -1, 1, -1], - [-1, 1, -2], [1, -1, -2], [-5], [-5], [-6], [-6], [-2], [-2], - [-1], [-1], [-1, -1, -1], [-1], [-1, 1], [-1], [-1, 1, -1], - [1, -1, -1], [-1], [-1, -1], [-1], [-1], [-1], [-1], [-2], - [-2], [-1, 1, -10], [1, -1, -10], [-1], [-1], [-1], [-1], - [-1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], [-2], [-2], - [-1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], - [-1, -1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], [-1], - [-1], [-1], [-1] + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, -2, -1], + [-1, -1], + [-1, -1], + [-1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [-1, 1, -1, -1], + [ + -2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + -2, + 1, + -1, + -1, + -1, + -1, + -3, + -6, + -6, + -1, + -13, + -7, + -3, + -3, + -3, + -5, + -5, + ], + [-2, -1], + [-1, 1, -1, -2], + [-1], + [-1, -2], + [-1, -1, -2, 1], + [-1, -1, 1, -2], + [-1, -1, 1, -1], + [-1, -3, 1, -2], + [-1, -6, 1, -2], + [-1, -6, 1, -2], + [-1, -1, 1, -2], + [ + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -13, + -2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + ], + [-1, -7, 1, -2], + [-1, -3, 1, -2], + [-3, -2], + [-3, -2], + [-1, -5, 1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-1, -5, 1, -2], + [-1, -1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-1, -2], + [-2], + [-2], + [1, -1], + [-2, -1, -1], + [-2], + [1, -1, -1], + [-1, -1, 1], + [-1], + [-1], + [-2], + [-2], + [-2], + [-2], + [-2, -1, 1], + [-2, 1, -1], + [-1, 1, -3, -1, 1, -1, 1], + [1, -1, -3, 1, -1, 1, -1], + [-1, 1, -2], + [1, -1, -2], + [-5], + [-5], + [-6], + [-6], + [-2], + [-2], + [-1], + [-1], + [-1, -1, -1], + [-1], + [-1, 1], + [-1], + [-1, 1, -1], + [1, -1, -1], + [-1], + [-1, -1], + [-1], + [-1], + [-1], + [-1], + [-2], + [-2], + [-1, 1, -10], + [1, -1, -10], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-2], + [-2], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1, -1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], + [-1], ] ref_for_fields = [ - 2, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, - 2, 2, 2, 2, 51, 3, 3, 1, 3, 3, 3, 2, 5, 8, 8, 3, 15, 9, 5, - 5, 5, 7, 3, 3, 3, 3, 3, 7, 4, 3, 3, 3, 3, 3, 2, 2, 1, 3, - 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 2, 2, 5, 5, 6, 6, - 2, 2, 1, 1, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 10, - 10, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 4, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 51, + 3, + 3, + 1, + 3, + 3, + 3, + 2, + 5, + 8, + 8, + 3, + 15, + 9, + 5, + 5, + 5, + 7, + 3, + 3, + 3, + 3, + 3, + 7, + 4, + 3, + 3, + 3, + 3, + 3, + 2, + 2, + 1, + 3, + 2, + 2, + 2, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 3, + 3, + 2, + 2, + 5, + 5, + 6, + 6, + 2, + 2, + 1, + 1, + 2, + 1, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 10, + 10, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, ] # compare J from Python with reference from C++ for i in range(len(ref_for_J)): - assert J[i] == ref_for_J[i], \ - f"J_{i} ({J[i]}) does not match J_{i}_ref ({ref_for_J[i]})" - assert not any(J[len(ref_for_J):]) + assert ( + J[i] == ref_for_J[i] + ), f"J_{i} ({J[i]}) does not match J_{i}_ref ({ref_for_J[i]})" + assert not any(J[len(ref_for_J) :]) # compare J2 from Python with reference from C++ for i in range(len(ref_for_J2)): - assert J2[i] == ref_for_J2[i], \ - f"J_{i} ({J2[i]}) does not match J_{i}_ref ({ref_for_J2[i]})" - assert not any(J2[len(ref_for_J2):]) + assert ( + J2[i] == ref_for_J2[i] + ), f"J_{i} ({J2[i]}) does not match J_{i}_ref ({ref_for_J2[i]})" + assert not any(J2[len(ref_for_J2) :]) # compare fields from Python with reference from C++ for i in range(len(ref_for_fields)): - assert fields[i] == ref_for_fields[i], \ - f"J_{i} ({fields[i]}) does not match J_{i}_ref ({ref_for_fields[i]})" - assert not any(fields[len(ref_for_fields):]) + assert ( + fields[i] == ref_for_fields[i] + ), f"J_{i} ({fields[i]}) does not match J_{i}_ref ({ref_for_fields[i]})" + assert not any(fields[len(ref_for_fields) :]) -def compute_moiety_conservation_laws_demartino2014( - data_demartino2014, quiet=False -): +def compute_moiety_conservation_laws_demartino2014(data_demartino2014, quiet=False): """Compute conserved quantities for De Martino's published results for E. coli network""" stoichiometric_list, row_names = data_demartino2014 num_species = 1668 num_reactions = 2381 - assert len(stoichiometric_list) == num_species * num_reactions, \ - "Unexpected dimension of stoichiometric matrix" + assert ( + len(stoichiometric_list) == num_species * num_reactions + ), "Unexpected dimension of stoichiometric matrix" start = perf_counter() cls_state_idxs, cls_coefficients = compute_moiety_conservation_laws( - stoichiometric_list, - num_species=num_species, - num_reactions=num_reactions + stoichiometric_list, num_species=num_species, num_reactions=num_reactions ) runtime = perf_counter() - start if not quiet: @@ -220,9 +795,7 @@ def compute_moiety_conservation_laws_demartino2014( def test_compute_moiety_conservation_laws_demartino2014(data_demartino2014): """Invoke test case and benchmarking for De Martino's published results for E. coli network""" - compute_moiety_conservation_laws_demartino2014( - data_demartino2014, quiet=False - ) + compute_moiety_conservation_laws_demartino2014(data_demartino2014, quiet=False) @skip_on_valgrind @@ -239,7 +812,8 @@ def test_cl_detect_execution_time(data_demartino2014): for _ in range(max_tries): runtime = compute_moiety_conservation_laws_demartino2014( - data_demartino2014, quiet=True) + data_demartino2014, quiet=True + ) if runtime < max_time_seconds: break assert runtime < max_time_seconds, "Took too long" @@ -248,29 +822,29 @@ def test_cl_detect_execution_time(data_demartino2014): @skip_on_valgrind def test_compute_moiety_conservation_laws_simple(): """Test a simple example, ensure the conservation laws are identified - reliably. Requires the Monte Carlo to identify all.""" - stoichiometric_matrix = sp.Matrix([ - [-1.0, 1.0], - [-1.0, 1.0], - [1.0, -1.0], - [1.0, -1.0]] + reliably. Requires the Monte Carlo to identify all.""" + stoichiometric_matrix = sp.Matrix( + [[-1.0, 1.0], [-1.0, 1.0], [1.0, -1.0], [1.0, -1.0]] ) - stoichiometric_list = [ - float(entry) for entry in stoichiometric_matrix.T.flat() - ] + stoichiometric_list = [float(entry) for entry in stoichiometric_matrix.T.flat()] num_tries = 1000 found_all_n_times = 0 for _ in range(num_tries): cls_state_idxs, cls_coefficients = compute_moiety_conservation_laws( - stoichiometric_list, *stoichiometric_matrix.shape) - - assert cls_state_idxs in ([[0, 3], [1, 2], [1, 3]], - [[0, 3], [1, 2], [0, 2]], - # should happen rarely - [[0, 3], [1, 2]]) - assert cls_coefficients in ([[1.0, 1.0], [1.0, 1.0], [1.0, 1.0]], - [[1.0, 1.0], [1.0, 1.0]]) + stoichiometric_list, *stoichiometric_matrix.shape + ) + + assert cls_state_idxs in ( + [[0, 3], [1, 2], [1, 3]], + [[0, 3], [1, 2], [0, 2]], + # should happen rarely + [[0, 3], [1, 2]], + ) + assert cls_coefficients in ( + [[1.0, 1.0], [1.0, 1.0], [1.0, 1.0]], + [[1.0, 1.0], [1.0, 1.0]], + ) num_cls_found = len(cls_state_idxs) if num_cls_found == 3: diff --git a/python/tests/test_conserved_quantities_rref.py b/python/tests/test_conserved_quantities_rref.py index 2368b06b8a..ada4b46729 100644 --- a/python/tests/test_conserved_quantities_rref.py +++ b/python/tests/test_conserved_quantities_rref.py @@ -3,7 +3,6 @@ import numpy as np import pytest import sympy as sp - from amici.conserved_quantities_rref import nullspace_by_rref, pivots, rref from amici.testing import skip_on_valgrind @@ -39,7 +38,10 @@ def test_nullspace_by_rref(mat): assert np.allclose(mat.dot(actual.T), 0) expected = sp.Matrix(mat).nullspace() - expected = np.hstack(np.asarray(expected, dtype=float)).T \ - if len(expected) else np.array([]) + expected = ( + np.hstack(np.asarray(expected, dtype=float)).T + if len(expected) + else np.array([]) + ) assert np.allclose(actual, expected, rtol=1e-8) diff --git a/python/tests/test_cxxcodeprinter.py b/python/tests/test_cxxcodeprinter.py index 833a8cd492..384b8ad9ae 100644 --- a/python/tests/test_cxxcodeprinter.py +++ b/python/tests/test_cxxcodeprinter.py @@ -1,6 +1,6 @@ +import sympy as sp from amici.cxxcodeprinter import AmiciCxxCodePrinter from sympy.codegen.rewriting import optims_c99 -import sympy as sp def test_optimizations(): diff --git a/python/tests/test_edata.py b/python/tests/test_edata.py index 7df799d4d9..c2d2ea470b 100644 --- a/python/tests/test_edata.py +++ b/python/tests/test_edata.py @@ -1,11 +1,10 @@ """Tests related to amici.ExpData via Python""" -import numpy as np - import amici +import numpy as np from amici.testing import skip_on_valgrind - from test_sbml_import import model_units_module + @skip_on_valgrind def test_edata_sensi_unscaling(model_units_module): """ @@ -17,10 +16,10 @@ def test_edata_sensi_unscaling(model_units_module): sx0 = (3, 3, 3, 3) - parameter_scales_log10 = \ - [amici.ParameterScaling.log10.value]*len(parameters0) - amici_parameter_scales_log10 = \ - amici.parameterScalingFromIntVector(parameter_scales_log10) + parameter_scales_log10 = [amici.ParameterScaling.log10.value] * len(parameters0) + amici_parameter_scales_log10 = amici.parameterScalingFromIntVector( + parameter_scales_log10 + ) model = model_units_module.getModel() model.setTimepoints(np.linspace(0, 1, 3)) diff --git a/python/tests/test_events.py b/python/tests/test_events.py index a6f5334b83..b69c271734 100644 --- a/python/tests/test_events.py +++ b/python/tests/test_events.py @@ -3,17 +3,27 @@ import numpy as np import pytest - -from util import (check_trajectories_with_forward_sensitivities, - check_trajectories_without_sensitivities, create_amici_model, - create_sbml_model) from amici.testing import skip_on_valgrind - - -@pytest.fixture(params=[ - pytest.param('events_plus_heavisides', marks=skip_on_valgrind), - 'nested_events', -]) +from util import ( + check_trajectories_with_forward_sensitivities, + check_trajectories_without_sensitivities, + create_amici_model, + create_sbml_model, +) + + +@pytest.fixture( + params=[ + pytest.param("events_plus_heavisides", marks=skip_on_valgrind), + pytest.param("piecewise_plus_event_simple_case", marks=skip_on_valgrind), + pytest.param("piecewise_plus_event_semi_complicated", marks=skip_on_valgrind), + pytest.param( + "piecewise_plus_event_trigger_depends_on_state", marks=skip_on_valgrind + ), + pytest.param("nested_events", marks=skip_on_valgrind), + pytest.param("event_state_dep_ddeltax_dtpx", marks=skip_on_valgrind), + ] +) def model(request): """Returns the requested AMICI model and analytical expressions.""" ( @@ -23,8 +33,8 @@ def model(request): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) = get_model_definition(request.param) # SBML model @@ -45,22 +55,28 @@ def model(request): ) amici_model.setTimepoints(timepoints) - return amici_model, parameters, timepoints, x_pected, sx_pected + return amici_model, parameters, timepoints, x_expected, sx_expected def get_model_definition(model_name): - if model_name == 'events_plus_heavisides': - return model_definition_events_plus_heavisides() - if model_name == 'nested_events': + if model_name == "piecewise_plus_event_simple_case": + return model_definition_piecewise_plus_event_simple_case() + if model_name == "piecewise_plus_event_semi_complicated": + return model_definition_piecewise_plus_event_semi_complicated() + if model_name == "piecewise_plus_event_trigger_depends_on_state": + return model_definition_piecewise_plus_event_trigger_depends_on_state() + if model_name == "events_plus_heavisides": + return model_definition_events_plus_heavisides() + if model_name == "nested_events": return model_definition_nested_events() - else: - raise NotImplementedError( - f'Model with name {model_name} is not implemented.' - ) + if model_name == "event_state_dep_ddeltax_dtpx": + return model_definition_event_state_dep_ddeltax_dtpx() + + raise NotImplementedError(f"Model with name {model_name} is not implemented.") def model_definition_events_plus_heavisides(): - """Test model for state- and parameter-dependent heavisides. + """Test model for state- and parameter-dependent Heavisides. ODEs ---- @@ -87,44 +103,44 @@ def model_definition_events_plus_heavisides(): [ zeta / 3]] """ # Model components - species = ['x_1', 'x_2', 'x_3'] + species = ["x_1", "x_2", "x_3"] initial_assignments = { - 'x_1': 'k1', - 'x_2': 'k2', - 'x_3': 'k3', + "x_1": "k1", + "x_2": "k2", + "x_3": "k3", } rate_rules = { - 'x_1': 'piecewise( -alpha * x_1, time >= delta, 0)', - 'x_2': 'beta * x_1 - gamma * x_2', - 'x_3': '-eta * x_3 + piecewise( 1, time >= zeta, 0)', + "x_1": "piecewise( -alpha * x_1, time >= delta, 0)", + "x_2": "beta * x_1 - gamma * x_2", + "x_3": "-eta * x_3 + piecewise( 1, time >= zeta, 0)", } parameters = { - 'k1': 2, - 'k2': 0.01, - 'k3': 5, - 'alpha': 2, - 'beta': 3, - 'gamma': 2, - 'delta': 3, - 'eta': 1, - 'zeta': 5, + "k1": 2, + "k2": 0.01, + "k3": 5, + "alpha": 2, + "beta": 3, + "gamma": 2, + "delta": 3, + "eta": 1, + "zeta": 5, } events = { - 'event_1': { - 'trigger': 'x_3 < k1', - 'target': 'x_1', - 'assignment': 'x_1 - x_3 / 2' + "event_1": { + "trigger": "x_3 < k1", + "target": "x_1", + "assignment": "x_1 - x_3 / 2", + }, + "event_2": { + "trigger": "time >= zeta", + "target": "x_3", + "assignment": "x_3 + zeta / 3", }, - 'event_2': { - 'trigger': 'time >= zeta', - 'target': 'x_3', - 'assignment': 'x_3 + zeta / 3' - } } timepoints = np.linspace(0, 8, 400) # Analytical solution - def x_pected(t, k1, k2, k3, alpha, beta, gamma, delta, eta, zeta): + def x_expected(t, k1, k2, k3, alpha, beta, gamma, delta, eta, zeta): # The system reads dx/dt = Ax + b # x0 = (k1, k2, k3) x0 = np.array([[k1], [k2], [k3]]) @@ -138,17 +154,13 @@ def get_early_x(t): # compute dynamics if t < event_1_time: # Define A - A = np.array([[0, 0, 0], - [beta, -gamma, 0], - [0, 0, -eta]]) + A = np.array([[0, 0, 0], [beta, -gamma, 0], [0, 0, -eta]]) tmp_x = expm(t * A) return np.matmul(tmp_x, x0) elif t <= event_2_time: # "simulate" until first event - A = np.array([[0, 0, 0], - [beta, -gamma, 0], - [0, 0, -eta]]) + A = np.array([[0, 0, 0], [beta, -gamma, 0], [0, 0, -eta]]) tmp_x = expm(event_1_time * A) x1 = np.matmul(tmp_x, x0) # apply bolus @@ -163,42 +175,40 @@ def get_early_x(t): elif t < event_3_time: x2 = get_early_x(event_2_time) - A = np.array([[-alpha, 0, 0], - [beta, -gamma, 0], - [0, 0, -eta]]) + A = np.array([[-alpha, 0, 0], [beta, -gamma, 0], [0, 0, -eta]]) tmp_x = expm((t - event_2_time) * A) x = np.matmul(tmp_x, x2).flatten() else: x2 = get_early_x(event_2_time) - A = np.array([[-alpha, 0, 0], - [beta, -gamma, 0], - [0, 0, -eta]]) + A = np.array([[-alpha, 0, 0], [beta, -gamma, 0], [0, 0, -eta]]) tmp_x = expm((event_3_time - event_2_time) * A) x3 = np.matmul(tmp_x, x2) # apply bolus x3 += np.array([[0], [0], [zeta / 3]]) hom_x = np.matmul(expm((t - event_3_time) * A), x3) - inhom_x = [[0], [0], - [-np.exp(-eta * (t - event_3_time)) / (eta) - + 1 / (eta)]] + inhom_x = [ + [0], + [0], + [-np.exp(-eta * (t - event_3_time)) / eta + 1 / eta], + ] x = (hom_x + inhom_x).flatten() return np.array(x) - def sx_pected(t, parameters): - # get sx, w.r.t. parameters, via finite differences + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" sx = [] + eps = 1e-6 for ip in parameters: - eps = 1e-6 perturbed_params = deepcopy(parameters) perturbed_params[ip] += eps - sx_p = x_pected(t, **perturbed_params) - perturbed_params[ip] -= 2*eps - sx_m = x_pected(t, **perturbed_params) + sx_p = x_expected(t, **perturbed_params) + perturbed_params[ip] -= 2 * eps + sx_m = x_expected(t, **perturbed_params) sx.append((sx_p - sx_m) / (2 * eps)) return np.array(sx) @@ -210,8 +220,8 @@ def sx_pected(t, parameters): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) @@ -237,49 +247,49 @@ def model_definition_nested_events(): [ bolus]] """ # Model components - species = ['x_1', 'x_2'] + species = ["x_1", "x_2"] initial_assignments = { - 'x_1': 'k1', - 'x_2': 'k2', + "x_1": "k1", + "x_2": "k2", } rate_rules = { - 'x_1': 'inflow_1 - decay_1 * x_1', - 'x_2': '- decay_2 * x_2', + "x_1": "inflow_1 - decay_1 * x_1", + "x_2": "- decay_2 * x_2", } parameters = { - 'k1': 0, - 'k2': 0, - 'inflow_1': 4, - 'decay_1': 2, - 'decay_2': 5, - 'bolus': 0, # for bolus != 0, nested event sensitivities are off! + "k1": 0, + "k2": 0, + "inflow_1": 4, + "decay_1": 2, + "decay_2": 5, + "bolus": 0, # for bolus != 0, nested event sensitivities are off! } events = { - 'event_1': { - 'trigger': 'x_1 > inflow_1 / decay_2', - 'target': 'x_2', - 'assignment': 'x_2 - 1 / time' + "event_1": { + "trigger": "x_1 > inflow_1 / decay_2", + "target": "x_2", + "assignment": "x_2 - 1 / time", + }, + "event_2": { + "trigger": "x_2 < - 0.5", + "target": ["x_1", "x_2"], + "assignment": ["x_1 + bolus", "x_2 + bolus"], }, - 'event_2': { - 'trigger': 'x_2 < - 0.5', - 'target': ['x_1', 'x_2'], - 'assignment': ['x_1 + bolus', 'x_2 + bolus'], - } } timepoints = np.linspace(0, 1, 101) # Analytical solution - def x_pected(t, k1, k2, inflow_1, decay_1, decay_2, bolus): + def x_expected(t, k1, k2, inflow_1, decay_1, decay_2, bolus): # gather temporary variables # event_time = x_1 > inflow_1 / decay_2 equil = inflow_1 / decay_1 tmp1 = inflow_1 / decay_2 - inflow_1 / decay_1 tmp2 = k1 - inflow_1 / decay_1 - event_time = (- 1 / decay_1) * np.log( tmp1 / tmp2) + event_time = (-1 / decay_1) * np.log(tmp1 / tmp2) def get_early_x(t): # compute dynamics before event - x_1 = equil * (1 - np.exp(-decay_1 * t)) + k1*np.exp(-decay_1 * t) + x_1 = equil * (1 - np.exp(-decay_1 * t)) + k1 * np.exp(-decay_1 * t) x_2 = k2 * np.exp(-decay_2 * t) return np.array([[x_1], [x_2]]) @@ -293,25 +303,96 @@ def get_early_x(t): # compute dynamics after event inhom = np.exp(decay_1 * event_time) * tau_x1 - x_1 = equil * (1 - np.exp(decay_1 * (event_time - t))) + \ - inhom * np.exp(- decay_1 * t) + x_1 = equil * (1 - np.exp(decay_1 * (event_time - t))) + inhom * np.exp( + -decay_1 * t + ) x_2 = tau_x2 * np.exp(decay_2 * event_time) * np.exp(-decay_2 * t) x = np.array([[x_1], [x_2]]) return x.flatten() - def sx_pected(t, parameters): - # get sx, w.r.t. parameters, via finite differences + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" + sx = [] + eps = 1e-6 + + for ip in parameters: + perturbed_params = deepcopy(parameters) + perturbed_params[ip] += eps + sx_p = x_expected(t, **perturbed_params) + perturbed_params[ip] -= 2 * eps + sx_m = x_expected(t, **perturbed_params) + sx.append((sx_p - sx_m) / (2 * eps)) + + return np.array(sx) + + return ( + initial_assignments, + parameters, + rate_rules, + species, + events, + timepoints, + x_expected, + sx_expected, + ) + + +def model_definition_piecewise_plus_event_simple_case(): + """Test model for boolean operations in a piecewise condition. + + ODEs + ---- + d/dt x_1: + - { 1, (alpha <= t and t < beta) + - { 0, otherwise + """ + # Model components + species = ["x_1"] + initial_assignments = {"x_1": "x_1_0"} + rate_rules = {"x_1": "piecewise(1, (alpha < time && time < beta), 0)"} + parameters = { + "alpha": 2, + "beta": 3, + "gamma": 4.5, + "x_1_0": 1, + } + timepoints = np.linspace(0.0, 5.0, 100) # np.array((0.0, 4.0,)) + events = { + "event_1": {"trigger": "time > alpha", "target": "x_1", "assignment": "gamma"}, + "event_2": { + "trigger": "time > beta", + "target": "x_1", + "assignment": "x_1 + 2.5", + }, + } + + # Analytical solution + def x_expected(t, x_1_0, alpha, beta, gamma): + t_event_1 = alpha + t_event_2 = beta + + if t < t_event_1: + x = x_1_0 + elif t < t_event_2: + x = gamma + t - t_event_1 + else: + x = gamma + t_event_2 - t_event_1 + 2.5 + + return np.array((x,)) + + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" sx = [] + eps = 1e-6 for ip in parameters: - eps = 1e-6 perturbed_params = deepcopy(parameters) perturbed_params[ip] += eps - sx_p = x_pected(t, **perturbed_params) - perturbed_params[ip] -= 2*eps - sx_m = x_pected(t, **perturbed_params) + sx_p = np.array(x_expected(t, **perturbed_params)) + perturbed_params[ip] -= 2 * eps + sx_m = np.array(x_expected(t, **perturbed_params)) sx.append((sx_p - sx_m) / (2 * eps)) return np.array(sx) @@ -323,29 +404,270 @@ def sx_pected(t, parameters): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, + ) + + +def model_definition_event_state_dep_ddeltax_dtpx(): + """Test model with state-dependent partial derivatives of update functions wrt parameters, time, and states.""" + # Model components + species = ["x_1"] + initial_assignments = {"x_1": "x_1_0"} + rate_rules = {"x_1": "1"} + parameters = { + "alpha": 1.5, + "beta": 2.5, + "gamma": 3.5, + "delta": 5.5, + "x_1_0": 1, + } + timepoints = np.linspace(0.0, 5.0, 100) + events = { + # state-dependent ddeltaxdt + "event_1": {"trigger": "time > alpha", "target": "x_1", "assignment": "x_1 * time"}, + # state-dependent ddeltaxdp + "event_2": { + "trigger": "time > beta", + "target": "x_1", + "assignment": "x_1 * delta", + }, + # state-dependent ddeltaxdx + "event_3": { + "trigger": "time > gamma", + "target": "x_1", + "assignment": "2 * x_1 * x_1", + }, + } + + # Analytical solution + def x_expected(t, x_1_0, alpha, beta, gamma, delta): + if t < alpha: + # before first event triggered + x = x_1_0 + t + elif t < beta: + # after first event triggered + x = (x_1_0 + alpha) * alpha + (t - alpha) + elif t < gamma: + # after second event triggered + x = ((x_1_0 + alpha) * alpha + (beta - alpha)) * delta + (t - beta) + else: + # after third event triggered + x = (((x_1_0 + alpha) * alpha + (beta - alpha)) * delta + (gamma - beta)) ** 2 * 2 + (t - gamma) + + return np.array((x,)) + + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" + sx = [] + eps = 1e-6 + + for ip in parameters: + perturbed_params = deepcopy(parameters) + perturbed_params[ip] += eps + sx_p = np.array(x_expected(t, **perturbed_params)) + perturbed_params[ip] -= 2 * eps + sx_m = np.array(x_expected(t, **perturbed_params)) + sx.append((sx_p - sx_m) / (2 * eps)) + + return np.array(sx) + + return ( + initial_assignments, + parameters, + rate_rules, + species, + events, + timepoints, + x_expected, + sx_expected, + ) + + +def model_definition_piecewise_plus_event_semi_complicated(): + """Test model for boolean operations in a piecewise condition, discrete + events and a non-vanishing quadrature for the adjoint state. + """ + # Model components + species = ["x_1", "x_2"] + initial_assignments = {"x_1": "x_1_0", "x_2": "x_2_0"} + rate_rules = { + "x_1": "piecewise(delta * x_1, (alpha < time && time < beta), - x_1)", + "x_2": "- eta * x_2", + } + parameters = { + "alpha": 2, + "beta": 3, + "gamma": 4.5, + "x_1_0": 1, + "x_2_0": 5, + "delta": 2.5, + "eta": 1.4, + } + timepoints = np.linspace(0.0, 5.0, 100) + events = { + "event_1": { + "trigger": "time > alpha / 2", + "target": "x_1", + "assignment": "gamma", + }, + "event_2": { + "trigger": "time > beta", + "target": "x_1", + "assignment": "x_1 + x_2", + }, + } + + # Analytical solution + def x_expected(t, x_1_0, x_2_0, alpha, beta, gamma, delta, eta): + t_event_1 = alpha / 2 + t_event_2 = beta + heaviside_1 = alpha + + x_2 = x_2_0 * np.exp(-eta * t) + + if t < t_event_1: + x_1 = x_1_0 * np.exp(-t) + elif t < heaviside_1: + x_1 = gamma * np.exp(-(t - t_event_1)) + elif t < t_event_2: + x_1_heaviside_1 = gamma * np.exp(-(heaviside_1 - t_event_1)) + x_1 = x_1_heaviside_1 * np.exp(delta * (t - heaviside_1)) + else: + x_1_heaviside_1 = gamma * np.exp(-(heaviside_1 - t_event_1)) + x_1_at_event_2 = x_1_heaviside_1 * np.exp(delta * (t_event_2 - heaviside_1)) + x_2_at_event_2 = x_2_0 * np.exp(-eta * t_event_2) + x1_after_event_2 = x_1_at_event_2 + x_2_at_event_2 + x_1 = x1_after_event_2 * np.exp(-(t - t_event_2)) + + return np.array((x_1, x_2)) + + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" + sx = [] + eps = 1e-6 + + for ip in parameters: + perturbed_params = deepcopy(parameters) + perturbed_params[ip] += eps + sx_p = np.array(x_expected(t, **perturbed_params)) + perturbed_params[ip] -= 2 * eps + sx_m = np.array(x_expected(t, **perturbed_params)) + sx.append((sx_p - sx_m) / (2 * eps)) + + return np.array(sx) + + return ( + initial_assignments, + parameters, + rate_rules, + species, + events, + timepoints, + x_expected, + sx_expected, + ) + + +def model_definition_piecewise_plus_event_trigger_depends_on_state(): + """Test model for boolean operations in a piecewise condition. + + ODEs + ---- + d/dt x_1: + - { 1, (alpha <= t and t < beta) + - { 0, otherwise + """ + # Model components + species = ["x_1", "x_2"] + initial_assignments = {"x_1": "x_1_0", "x_2": "x_2_0"} + rate_rules = { + "x_1": "piecewise(1, (alpha < time && time < beta), 0)", + "x_2": "- x_2", + } + parameters = { + "alpha": 2, + "beta": 3, + "gamma": 4.5, + "x_1_0": 1, + "x_2_0": 5, + } + timepoints = np.linspace(0.0, 5.0, 100) + events = { + "event_1": { + "trigger": "x_1 > 1.4", + "target": "x_1", + "assignment": "x_1 + gamma", + }, + "event_2": { + "trigger": "time > beta", + "target": "x_1", + "assignment": "x_1 + x_2", + }, + } + + # Analytical solution + def x_expected(t, x_1_0, x_2_0, alpha, beta, gamma): + heaviside_1 = alpha + t_event_1 = alpha + 1.4 - x_1_0 + t_event_2 = beta + # This should hold in order that the analytical solution is correct + assert heaviside_1 < t_event_1 + + # x_2 never gets perturbed + x_2 = x_2_0 * np.exp(-t) + + if t < heaviside_1: + x_1 = x_1_0 + elif t < t_event_1: + x_1 = (t - heaviside_1) + x_1_0 + elif t < t_event_2: + x_1 = gamma + (t - heaviside_1) + x_1_0 + else: + x_2_at_event_2 = x_2_0 * np.exp(-t_event_2) + x_1_at_event_2 = gamma + (t_event_2 - heaviside_1) + x_1_0 + x_1 = x_1_at_event_2 + x_2_at_event_2 + + return np.array((x_1, x_2)) + + def sx_expected(t, parameters): + """get sx, w.r.t. parameters, via finite differences""" + sx = [] + eps = 1e-6 + + for ip in parameters: + perturbed_params = deepcopy(parameters) + perturbed_params[ip] += eps + sx_p = np.array(x_expected(t, **perturbed_params)) + perturbed_params[ip] -= 2 * eps + sx_m = np.array(x_expected(t, **perturbed_params)) + sx.append((sx_p - sx_m) / (2 * eps)) + + return np.array(sx) + + return ( + initial_assignments, + parameters, + rate_rules, + species, + events, + timepoints, + x_expected, + sx_expected, ) def test_models(model): - amici_model, parameters, timepoints, x_pected, sx_pected = model + amici_model, parameters, timepoints, x_expected, sx_expected = model - result_expected_x = np.array([ - x_pected(t, **parameters) - for t in timepoints - ]) - result_expected_sx = np.array([ - sx_pected(t, parameters) - for t in timepoints - ]) + result_expected_x = np.array([x_expected(t, **parameters) for t in timepoints]) + result_expected_sx = np.array([sx_expected(t, parameters) for t in timepoints]) # assert correctness of trajectories - check_trajectories_without_sensitivities(amici_model, - result_expected_x) - check_trajectories_with_forward_sensitivities(amici_model, - result_expected_x, - result_expected_sx) + check_trajectories_without_sensitivities(amici_model, result_expected_x) + check_trajectories_with_forward_sensitivities( + amici_model, result_expected_x, result_expected_sx + ) def expm(x): @@ -354,4 +676,5 @@ def expm(x): Uses ``expm`` from ``mpmath``. *Something* changed in scipy's ``expm`` in version 1.9.0 breaking these tests""" from mpmath import expm + return np.array(expm(x).tolist()).astype(float) diff --git a/python/tests/test_hdf5.py b/python/tests/test_hdf5.py index 40098f9212..c47d8653eb 100644 --- a/python/tests/test_hdf5.py +++ b/python/tests/test_hdf5.py @@ -10,18 +10,18 @@ def _modify_solver_attrs(solver): # change to non-default values for attr in dir(solver): - if not attr.startswith('set'): + if not attr.startswith("set"): continue - val = getattr(solver, attr.replace('set', 'get'))() + val = getattr(solver, attr.replace("set", "get"))() if isinstance(val, bool): cval = not val - elif attr == 'setStabilityLimitFlag': + elif attr == "setStabilityLimitFlag": cval = 0 - elif attr == 'setReturnDataReportingMode': + elif attr == "setReturnDataReportingMode": cval = amici.RDataReporting.likelihood - elif attr == 'setMaxTime': + elif attr == "setMaxTime": # default value is the maximum, must not add to that cval = random.random() elif isinstance(val, int): @@ -32,8 +32,7 @@ def _modify_solver_attrs(solver): getattr(solver, attr)(cval) -@pytest.mark.skipif(not amici.hdf5_enabled, - reason='AMICI was compiled without HDF5') +@pytest.mark.skipif(not amici.hdf5_enabled, reason="AMICI was compiled without HDF5") def test_solver_hdf5_roundtrip(sbml_example_presimulation_module): """TestCase class for AMICI HDF5 I/O""" @@ -41,29 +40,31 @@ def test_solver_hdf5_roundtrip(sbml_example_presimulation_module): solver = model.getSolver() _modify_solver_attrs(solver) - hdf5file = 'solverSettings.hdf5' + hdf5file = "solverSettings.hdf5" - amici.writeSolverSettingsToHDF5(solver, hdf5file, 'ssettings') + amici.writeSolverSettingsToHDF5(solver, hdf5file, "ssettings") new_solver = model.getSolver() # check that we changed everything for attr in dir(solver): - if not attr.startswith('set'): + if not attr.startswith("set"): continue - assert getattr(solver, attr.replace('set', 'get'))() \ - != getattr(new_solver, attr.replace('set', 'get'))(), attr + assert ( + getattr(solver, attr.replace("set", "get"))() + != getattr(new_solver, attr.replace("set", "get"))() + ), attr - amici.readSolverSettingsFromHDF5(hdf5file, new_solver, 'ssettings') + amici.readSolverSettingsFromHDF5(hdf5file, new_solver, "ssettings") # check that reading in settings worked for attr in dir(solver): - if not attr.startswith('set'): + if not attr.startswith("set"): continue - assert getattr(solver, attr.replace('set', 'get'))() \ - == pytest.approx( - getattr(new_solver, attr.replace('set', 'get'))()), attr + assert getattr(solver, attr.replace("set", "get"))() == pytest.approx( + getattr(new_solver, attr.replace("set", "get"))() + ), attr os.remove(hdf5file) diff --git a/python/tests/test_heavisides.py b/python/tests/test_heavisides.py index 3d1a3564b4..4cef7723a6 100644 --- a/python/tests/test_heavisides.py +++ b/python/tests/test_heavisides.py @@ -1,20 +1,21 @@ """Tests for SBML events, including piecewise expressions.""" import numpy as np import pytest - - from util import ( - create_sbml_model, - create_amici_model, - check_trajectories_without_sensitivities, check_trajectories_with_forward_sensitivities, + check_trajectories_without_sensitivities, + create_amici_model, + create_sbml_model, ) -@pytest.fixture(params=[ - 'state_and_param_dep_heavisides', - 'piecewise_with_boolean_operations', - 'piecewise_many_conditions', -]) + +@pytest.fixture( + params=[ + "state_and_param_dep_heavisides", + "piecewise_with_boolean_operations", + "piecewise_many_conditions", + ] +) def model(request): """Returns the requested AMICI model and analytical expressions.""" ( @@ -24,8 +25,8 @@ def model(request): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) = get_model_definition(request.param) # SBML model @@ -46,40 +47,31 @@ def model(request): ) amici_model.setTimepoints(timepoints) - return amici_model, parameters, timepoints, x_pected, sx_pected + return amici_model, parameters, timepoints, x_expected, sx_expected def test_models(model): - amici_model, parameters, timepoints, x_pected, sx_pected = model + amici_model, parameters, timepoints, x_expected, sx_expected = model - result_expected_x = np.array([ - x_pected(t, **parameters) - for t in timepoints - ]) - result_expected_sx = np.array([ - sx_pected(t, **parameters) - for t in timepoints - ]) + result_expected_x = np.array([x_expected(t, **parameters) for t in timepoints]) + result_expected_sx = np.array([sx_expected(t, **parameters) for t in timepoints]) # Does the AMICI simulation match the analytical solution? - check_trajectories_without_sensitivities(amici_model, - result_expected_x) - check_trajectories_with_forward_sensitivities(amici_model, - result_expected_x, - result_expected_sx) + check_trajectories_without_sensitivities(amici_model, result_expected_x) + check_trajectories_with_forward_sensitivities( + amici_model, result_expected_x, result_expected_sx + ) def get_model_definition(model_name): - if model_name == 'state_and_param_dep_heavisides': + if model_name == "state_and_param_dep_heavisides": return model_definition_state_and_parameter_dependent_heavisides() - elif model_name == 'piecewise_with_boolean_operations': + elif model_name == "piecewise_with_boolean_operations": return model_definition_piecewise_with_boolean_operations() - elif model_name == 'piecewise_many_conditions': + elif model_name == "piecewise_many_conditions": return model_definition_piecewise_many_conditions() else: - raise NotImplementedError( - f'Model with name {model_name} is not implemented.' - ) + raise NotImplementedError(f"Model with name {model_name} is not implemented.") def model_definition_state_and_parameter_dependent_heavisides(): @@ -95,44 +87,44 @@ def model_definition_state_and_parameter_dependent_heavisides(): - { eta, t >= delta """ # Model components - species = ['x_1', 'x_2'] + species = ["x_1", "x_2"] initial_assignments = { - 'x_1': 'zeta', + "x_1": "zeta", } rate_rules = { - 'x_1': 'piecewise( alpha * x_1, time < x_2, -beta * x_1 )', - 'x_2': 'piecewise( gamma * x_2, time < delta, eta )', + "x_1": "piecewise( alpha * x_1, time < x_2, -beta * x_1 )", + "x_2": "piecewise( gamma * x_2, time < delta, eta )", } parameters = { - 'alpha': float(np.log(2)), - 'beta': float(np.log(4)), - 'gamma': float(np.log(3)), - 'delta': 1, - 'eta': 0.5, - 'zeta': 0.25, + "alpha": float(np.log(2)), + "beta": float(np.log(4)), + "gamma": float(np.log(3)), + "delta": 1, + "eta": 0.5, + "zeta": 0.25, } timepoints = np.linspace(0, 10, 100) events = {} # Analytical solution - def x_pected(t, alpha, beta, gamma, delta, eta, zeta): + def x_expected(t, alpha, beta, gamma, delta, eta, zeta): # get x_1 tau_1 = (np.exp(gamma * delta) - delta * eta) / (1 - eta) if t < tau_1: x_1 = zeta * np.exp(alpha * t) else: - x_1 = zeta * np.exp(alpha * tau_1 - beta*(t - tau_1)) + x_1 = zeta * np.exp(alpha * tau_1 - beta * (t - tau_1)) # get x_2 tau_2 = delta if t < tau_2: - x_2 = np.exp(gamma*t) + x_2 = np.exp(gamma * t) else: - x_2 = np.exp(gamma*delta) + eta*(t-delta) + x_2 = np.exp(gamma * delta) + eta * (t - delta) - return (x_1, x_2) + return x_1, x_2 - def sx_pected(t, alpha, beta, gamma, delta, eta, zeta): + def sx_expected(t, alpha, beta, gamma, delta, eta, zeta): # get sx_1, w.r.t. parameters tau_1 = (np.exp(gamma * delta) - delta * eta) / (1 - eta) if t < tau_1: @@ -144,53 +136,48 @@ def sx_pected(t, alpha, beta, gamma, delta, eta, zeta): sx_1_zeta = np.exp(alpha * t) else: # Never trust Wolfram Alpha... - sx_1_alpha = ( - zeta * tau_1 * np.exp(alpha * tau_1 - beta*(t - tau_1)) - ) - sx_1_beta = ( - zeta * (tau_1 - t) - * np.exp(alpha * tau_1 - beta*(t - tau_1)) - ) + sx_1_alpha = zeta * tau_1 * np.exp(alpha * tau_1 - beta * (t - tau_1)) + sx_1_beta = zeta * (tau_1 - t) * np.exp(alpha * tau_1 - beta * (t - tau_1)) sx_1_gamma = ( - zeta * (alpha + beta) * delta * np.exp(gamma * delta) + zeta + * (alpha + beta) + * delta + * np.exp(gamma * delta) / (1 - eta) - * np.exp(alpha * tau_1 - beta*(t - tau_1)) + * np.exp(alpha * tau_1 - beta * (t - tau_1)) ) sx_1_delta = ( - zeta * (alpha + beta) - * np.exp(alpha * tau_1 - beta*(t - tau_1)) + zeta + * (alpha + beta) + * np.exp(alpha * tau_1 - beta * (t - tau_1)) * (gamma * np.exp(gamma * delta) - eta) / (1 - eta) ) sx_1_eta = ( - zeta * (alpha + beta) - * (-delta * (1-eta) + np.exp(gamma * delta) - delta * eta) - / (1 - eta)**2 - * np.exp(alpha * tau_1 - beta*(t - tau_1)) + zeta + * (alpha + beta) + * (-delta * (1 - eta) + np.exp(gamma * delta) - delta * eta) + / (1 - eta) ** 2 + * np.exp(alpha * tau_1 - beta * (t - tau_1)) ) - sx_1_zeta = np.exp(alpha * tau_1 - beta*(t - tau_1)) + sx_1_zeta = np.exp(alpha * tau_1 - beta * (t - tau_1)) # get sx_2, w.r.t. parameters tau_2 = delta + sx_2_alpha = 0 + sx_2_beta = 0 + sx_2_zeta = 0 if t < tau_2: - sx_2_alpha = 0 - sx_2_beta = 0 - sx_2_gamma = t * np.exp(gamma*t) + sx_2_gamma = t * np.exp(gamma * t) sx_2_delta = 0 sx_2_eta = 0 - sx_2_zeta = 0 else: - sx_2_alpha = 0 - sx_2_beta = 0 - sx_2_gamma = delta * np.exp(gamma*delta) - sx_2_delta = gamma*np.exp(gamma*delta) - eta + sx_2_gamma = delta * np.exp(gamma * delta) + sx_2_delta = gamma * np.exp(gamma * delta) - eta sx_2_eta = t - delta - sx_2_zeta = 0 - sx_1 = (sx_1_alpha, sx_1_beta, sx_1_gamma, - sx_1_delta, sx_1_eta, sx_1_zeta) - sx_2 = (sx_2_alpha, sx_2_beta, sx_2_gamma, - sx_2_delta, sx_2_eta, sx_2_zeta) + sx_1 = (sx_1_alpha, sx_1_beta, sx_1_gamma, sx_1_delta, sx_1_eta, sx_1_zeta) + sx_2 = (sx_2_alpha, sx_2_beta, sx_2_gamma, sx_2_delta, sx_2_eta, sx_2_zeta) return np.array((sx_1, sx_2)).transpose() @@ -201,8 +188,8 @@ def sx_pected(t, alpha, beta, gamma, delta, eta, zeta): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) @@ -216,30 +203,30 @@ def model_definition_piecewise_with_boolean_operations(): - { 0, otherwise """ # Model components - species = ['x_1'] - initial_assignments = {'x_1': 'x_1_0'} + species = ["x_1"] + initial_assignments = {"x_1": "x_1_0"} rate_rules = { - 'x_1': ( - 'piecewise(' - '1, ' # noqa - '(alpha <= time && time < beta) || ' # noqa - '(gamma <= time && time < delta), ' - '0' - ')' + "x_1": ( + "piecewise(" + "1, " # noqa + "(alpha <= time && time < beta) || " # noqa + "(gamma <= time && time < delta), " + "0" + ")" ), } parameters = { - 'alpha': 1, - 'beta': 2, - 'gamma': 3, - 'delta': 4, - 'x_1_0': 1, + "alpha": 1, + "beta": 2, + "gamma": 3, + "delta": 4, + "x_1_0": 1, } timepoints = np.linspace(0, 5, 100) events = {} # Analytical solution - def x_pected(t, x_1_0, alpha, beta, gamma, delta): + def x_expected(t, x_1_0, alpha, beta, gamma, delta): if t < alpha: return (x_1_0,) elif alpha <= t < beta: @@ -249,27 +236,16 @@ def x_pected(t, x_1_0, alpha, beta, gamma, delta): elif gamma <= t < delta: return (x_1_0 + (beta - alpha) + (t - gamma),) else: - return (x_1_0 + (beta - alpha) + (delta - gamma), ) + return (x_1_0 + (beta - alpha) + (delta - gamma),) - def sx_pected(t, x_1_0, alpha, beta, gamma, delta): + def sx_expected(t, x_1_0, alpha, beta, gamma, delta): # x0 is very simple... sx_x0 = 1 - sx_alpha = 0 - sx_beta = 0 - sx_gamma = 0 - sx_delta = 0 - - if t >= alpha: - sx_alpha = -1 - if t >= beta: - sx_beta = 1 - if t >= gamma: - sx_gamma = -1 - if t >= delta: - sx_delta = 1 - + sx_alpha = -1 if t >= alpha else 0 + sx_beta = 1 if t >= beta else 0 + sx_gamma = -1 if t >= gamma else 0 + sx_delta = 1 if t >= delta else 0 sx = (sx_alpha, sx_beta, sx_gamma, sx_delta, sx_x0) - return np.array((sx,)).transpose() return ( @@ -279,8 +255,8 @@ def sx_pected(t, x_1_0, alpha, beta, gamma, delta): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) @@ -294,36 +270,44 @@ def model_definition_piecewise_many_conditions(): - { 0, otherwise """ # Model components - species = ['x_1'] - initial_assignments = {'x_1': 'x_1_0'} + species = ["x_1"] + initial_assignments = {"x_1": "x_1_0"} t_final = 5 - pieces = 'piecewise(' + pieces = "piecewise(" for t in range(t_final): if t > 0: - pieces += ', ' + pieces += ", " if t % 2 == 1: - pieces += f'1, time < {t + 1}' + pieces += f"1, time < {t + 1}" else: - pieces += f'0, time < {t + 1}' - pieces += ', 0)' - rate_rules = {'x_1': pieces, } + pieces += f"0, time < {t + 1}" + pieces += ", 0)" + rate_rules = { + "x_1": pieces, + } parameters = { - 'x_1_0': 1, + "x_1_0": 1, } timepoints = np.linspace(0, t_final, 100) events = {} # Analytical solution - def x_pected(t, x_1_0): + def x_expected(t, x_1_0): if np.floor(t) % 2 == 1: - return (x_1_0 + (np.floor(t)-1)/2 + (t-np.floor(t)), ) + return (x_1_0 + (np.floor(t) - 1) / 2 + (t - np.floor(t)),) else: - return (x_1_0 + np.floor(t)/2, ) - - def sx_pected(t, x_1_0): - return np.array([[1, ], ]) + return (x_1_0 + np.floor(t) / 2,) + + def sx_expected(t, x_1_0): + return np.array( + [ + [ + 1, + ], + ] + ) return ( initial_assignments, @@ -332,6 +316,6 @@ def sx_pected(t, x_1_0): species, events, timepoints, - x_pected, - sx_pected + x_expected, + sx_expected, ) diff --git a/python/tests/test_misc.py b/python/tests/test_misc.py index 2e70c4c6e3..331c806623 100644 --- a/python/tests/test_misc.py +++ b/python/tests/test_misc.py @@ -3,12 +3,11 @@ import os import subprocess from pathlib import Path -import pytest -import sympy as sp import amici -from amici.de_export import _custom_pow_eval_derivative, _monkeypatched, \ - smart_subs_dict +import pytest +import sympy as sp +from amici.de_export import _custom_pow_eval_derivative, _monkeypatched, smart_subs_dict from amici.testing import skip_on_valgrind @@ -19,38 +18,42 @@ def test_parameter_scaling_from_int_vector(): [ amici.ParameterScaling.log10, amici.ParameterScaling.ln, - amici.ParameterScaling.none - ]) + amici.ParameterScaling.none, + ] + ) assert scale_vector[0] == amici.ParameterScaling.log10 assert scale_vector[1] == amici.ParameterScaling.ln assert scale_vector[2] == amici.ParameterScaling.none + @skip_on_valgrind def test_hill_function_dwdx(): """Kinetic laws with Hill functions, may lead to NaNs in the Jacobian if involved states are zero if not properly arranged symbolically. Test that what we are applying the right sympy simplification.""" - w = sp.Matrix([[sp.sympify('Pow(x1, p1) / (Pow(x1, p1) + a)')]]) - dwdx = w.diff(sp.Symbol('x1')) + w = sp.Matrix([[sp.sympify("Pow(x1, p1) / (Pow(x1, p1) + a)")]]) + dwdx = w.diff(sp.Symbol("x1")) # Verify that without simplification we fail with pytest.raises(ZeroDivisionError): with sp.evaluate(False): - res = dwdx.subs({'x1': 0.0}) + res = dwdx.subs({"x1": 0.0}) _ = str(res) # Test that powsimp does the job dwdx = dwdx.applyfunc(lambda x: sp.powsimp(x, deep=True)) with sp.evaluate(False): - res = dwdx.subs({'x1': 0.0}) + res = dwdx.subs({"x1": 0.0}) _ = str(res) @skip_on_valgrind -@pytest.mark.skipif(os.environ.get('AMICI_SKIP_CMAKE_TESTS', '') == 'TRUE', - reason='skipping cmake based test') +@pytest.mark.skipif( + os.environ.get("AMICI_SKIP_CMAKE_TESTS", "") == "TRUE", + reason="skipping cmake based test", +) def test_cmake_compilation(sbml_example_presimulation_module): """Check that CMake build succeeds for one of the models generated during Python tests""" @@ -59,14 +62,17 @@ def test_cmake_compilation(sbml_example_presimulation_module): build_dir = f"{source_dir}/build" # path hint for amici base installation, in case CMake configuration has # not been exported - amici_dir = (Path(__file__).parents[2] / 'build').absolute() - cmd = f"set -e; " \ - f"cmake -S {source_dir} -B '{build_dir}' -DAmici_DIR={amici_dir}; " \ - f"cmake --build '{build_dir}'" + amici_dir = (Path(__file__).parents[2] / "build").absolute() + cmd = ( + f"set -e; " + f"cmake -S {source_dir} -B '{build_dir}' -DAmici_DIR={amici_dir}; " + f"cmake --build '{build_dir}'" + ) try: - subprocess.run(cmd, shell=True, check=True, - stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.run( + cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) except subprocess.CalledProcessError as e: print(e.stdout.decode()) print(e.stderr.decode()) @@ -75,13 +81,13 @@ def test_cmake_compilation(sbml_example_presimulation_module): @skip_on_valgrind def test_smart_subs_dict(): - expr_str = 'c + d' + expr_str = "c + d" subs_dict = { - 'c': 'a + b', - 'd': 'c + a', + "c": "a + b", + "d": "c + a", } - expected_default_str = '3*a + 2*b' - expected_reverse_str = '2*a + b + c' + expected_default_str = "3*a + 2*b" + expected_reverse_str = "2*a + b + c" expr_sym = sp.sympify(expr_str) subs_sym = {sp.sympify(k): sp.sympify(v) for k, v in subs_dict.items()} @@ -97,32 +103,30 @@ def test_smart_subs_dict(): @skip_on_valgrind def test_monkeypatch(): - t = sp.Symbol('t') - n = sp.Symbol('n') - vals = [(t, 0), - (n, 1)] + t = sp.Symbol("t") + n = sp.Symbol("n") + vals = [(t, 0), (n, 1)] # check that the removable singularity still exists assert (t**n).diff(t).subs(vals) is sp.nan # check that we can monkeypatch it out - with _monkeypatched(sp.Pow, '_eval_derivative', - _custom_pow_eval_derivative): - assert (t ** n).diff(t).subs(vals) is not sp.nan + with _monkeypatched(sp.Pow, "_eval_derivative", _custom_pow_eval_derivative): + assert (t**n).diff(t).subs(vals) is not sp.nan # check that the monkeypatch is transient - assert (t ** n).diff(t).subs(vals) is sp.nan + assert (t**n).diff(t).subs(vals) is sp.nan @skip_on_valgrind def test_get_default_argument(): # no default with pytest.raises(ValueError): - amici._get_default_argument(lambda x: x, 'x') + amici._get_default_argument(lambda x: x, "x") # non-existant parameter with pytest.raises(KeyError): - amici._get_default_argument(lambda x: x, 'y') + amici._get_default_argument(lambda x: x, "y") # okay - assert amici._get_default_argument(lambda x=1: x, 'x') == 1 + assert amici._get_default_argument(lambda x=1: x, "x") == 1 diff --git a/python/tests/test_observable_events.py b/python/tests/test_observable_events.py index 8cd932a248..83a7b94c7e 100644 --- a/python/tests/test_observable_events.py +++ b/python/tests/test_observable_events.py @@ -1,12 +1,14 @@ -import amici -import pytest import os -from util import create_sbml_model, create_amici_model +import amici +import pytest from test_pregenerated_models import ( - options_file, expected_results, expected_results_file, - verify_simulation_results + expected_results, + expected_results_file, + options_file, + verify_simulation_results, ) +from util import create_amici_model, create_sbml_model def model_neuron_def(): @@ -28,45 +30,39 @@ def model_neuron_def(): observable: t """ # Model components - species = ['v', 'u'] + species = ["v", "u"] initial_assignments = { - 'v': 'v0', - 'u': 'b*v0', + "v": "v0", + "u": "b*v0", } rate_rules = { - 'v': '0.04*v^2 + 5*v + 140 - u + I0', - 'u': 'a*(b*v - u)', + "v": "0.04*v^2 + 5*v + 140 - u + I0", + "u": "a*(b*v - u)", } parameters = { - 'a': 0.02, - 'b': 0.3, - 'c': 65, - 'd': 0.9, - 'v0': -60, - 'I0': 10, + "a": 0.02, + "b": 0.3, + "c": 65, + "d": 0.9, + "v0": -60, + "I0": 10, } events = { - 'event_1': { - 'trigger': 'v > 30', - 'target': ['v', 'u'], - 'assignment': ['-c', 'd+u'] + "event_1": { + "trigger": "v > 30", + "target": ["v", "u"], + "assignment": ["-c", "d+u"], }, } observables = { - 'y1': { - 'name': 'v', - 'formula': 'v', + "y1": { + "name": "v", + "formula": "v", } } - event_observables = { - 'z1': { - 'name': 'z1', - 'event': 'event_1', - 'formula': 'time' - } - } + event_observables = {"z1": {"name": "z1", "event": "event_1", "formula": "time"}} return ( initial_assignments, parameters, @@ -74,7 +70,7 @@ def model_neuron_def(): species, events, observables, - event_observables + event_observables, ) @@ -102,58 +98,42 @@ def model_events_def(): observable: t """ # Model components - species = ['x1', 'x2', 'x3'] + species = ["x1", "x2", "x3"] initial_assignments = { - 'x1': 'k1', - 'x2': 'k2', - 'x3': 'k3', + "x1": "k1", + "x2": "k2", + "x3": "k3", } rate_rules = { - 'x1': '-p1*piecewise(1.0, time>p4, 0.0)*x1', - 'x2': 'p2*x1*exp(-0.1*time)-p3*x2', - 'x3': '-x3+piecewise(1.0, time>4, 0.0)' + "x1": "-p1*piecewise(1.0, time>p4, 0.0)*x1", + "x2": "p2*x1*exp(-0.1*time)-p3*x2", + "x3": "-x3+piecewise(1.0, time>4, 0.0)", } parameters = { - 'p1': 0.5, - 'p2': 2, - 'p3': 0.5, - 'p4': 0.5, - 'k1': 4, - 'k2': 8, - 'k3': 10, - 'k4': 4, + "p1": 0.5, + "p2": 2, + "p3": 0.5, + "p4": 0.5, + "k1": 4, + "k2": 8, + "k3": 10, + "k4": 4, } events = { - 'event_1': { - 'trigger': 'x2 > x3', - 'target': [], - 'assignment': [] - }, - 'event_2': { - 'trigger': 'x1 > x3', - 'target': [], - 'assignment': [] - }, + "event_1": {"trigger": "x2 > x3", "target": [], "assignment": []}, + "event_2": {"trigger": "x1 > x3", "target": [], "assignment": []}, } observables = { - 'y1': { - 'name': 'y1', - 'formula': 'p4*(x1+x2+x3)', + "y1": { + "name": "y1", + "formula": "p4*(x1+x2+x3)", } } event_observables = { - 'z1': { - 'name': 'z1', - 'event': 'event_1', - 'formula': 'time' - }, - 'z2': { - 'name': 'z2', - 'event': 'event_2', - 'formula': 'time' - } + "z1": {"name": "z1", "event": "event_1", "formula": "time"}, + "z2": {"name": "z2", "event": "event_2", "formula": "time"}, } return ( initial_assignments, @@ -162,18 +142,20 @@ def model_events_def(): species, events, observables, - event_observables + event_observables, ) models = [ - (model_neuron_def, 'model_neuron', ['v0', 'I0']), - (model_events_def, 'model_events', ['k1', 'k2', 'k3', 'k4']), + (model_neuron_def, "model_neuron", ["v0", "I0"]), + (model_events_def, "model_events", ["k1", "k2", "k3", "k4"]), ] -@pytest.mark.skipif(os.environ.get('AMICI_SKIP_CMAKE_TESTS', '') == 'TRUE', - reason='skipping cmake based test') +@pytest.mark.skipif( + os.environ.get("AMICI_SKIP_CMAKE_TESTS", "") == "TRUE", + reason="skipping cmake based test", +) @pytest.mark.parametrize("model_def,model_name,constants", models) def test_models(model_def, model_name, constants): ( @@ -183,7 +165,7 @@ def test_models(model_def, model_name, constants): species, events, observables, - event_observables + event_observables, ) = model_def() sbml_document, sbml_model = create_sbml_model( @@ -201,7 +183,7 @@ def test_models(model_def, model_name, constants): model_name=model_name, observables=observables, constant_parameters=constants, - event_observables=event_observables + event_observables=event_observables, ) run_test_cases(model) @@ -210,40 +192,36 @@ def test_models(model_def, model_name, constants): def run_test_cases(model): - solver = model.getSolver() model_name = model.getName() for case in list(expected_results[model_name].keys()): - - if case.startswith('sensi2'): + if case.startswith("sensi2"): continue amici.readModelDataFromHDF5( - options_file, model.get(), - f'/{model_name}/{case}/options' + options_file, model.get(), f"/{model_name}/{case}/options" ) amici.readSolverSettingsFromHDF5( - options_file, solver.get(), - f'/{model_name}/{case}/options' + options_file, solver.get(), f"/{model_name}/{case}/options" ) edata = None - if 'data' in expected_results[model.getName()][case].keys(): + if "data" in expected_results[model.getName()][case].keys(): edata = amici.readSimulationExpData( - str(expected_results_file), - f'/{model_name}/{case}/data', model.get() + str(expected_results_file), f"/{model_name}/{case}/data", model.get() ) rdata = amici.runAmiciSimulation(model, solver, edata) verify_simulation_opts = dict() - if model_name.startswith('model_neuron'): - verify_simulation_opts['atol'] = 1e-5 - verify_simulation_opts['rtol'] = 1e-2 + if model_name.startswith("model_neuron"): + verify_simulation_opts["atol"] = 1e-5 + verify_simulation_opts["rtol"] = 1e-2 verify_simulation_results( - rdata, expected_results[model.getName()][case]['results'], - **verify_simulation_opts + rdata, + expected_results[model.getName()][case]["results"], + **verify_simulation_opts, ) diff --git a/python/tests/test_ode_export.py b/python/tests/test_ode_export.py index 27e1a31ff9..b30d451a4a 100644 --- a/python/tests/test_ode_export.py +++ b/python/tests/test_ode_export.py @@ -4,23 +4,29 @@ from amici.cxxcodeprinter import AmiciCxxCodePrinter from amici.testing import skip_on_valgrind + @skip_on_valgrind def test_csc_matrix(): """Test sparse CSC matrix creation""" printer = AmiciCxxCodePrinter() matrix = sp.Matrix([[1, 0], [2, 3]]) - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, sparse_matrix \ - = printer.csc_matrix( + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = printer.csc_matrix( matrix, - rownames=[sp.Symbol('a1'), sp.Symbol('a2')], - colnames=[sp.Symbol('b1'), sp.Symbol('b2')] + rownames=[sp.Symbol("a1"), sp.Symbol("a2")], + colnames=[sp.Symbol("b1"), sp.Symbol("b2")], ) assert symbol_col_ptrs == [0, 2, 3] assert symbol_row_vals == [0, 1, 1] assert sparse_list == sp.Matrix([[1], [2], [3]]) - assert symbol_list == ['da1_db1', 'da2_db1', 'da2_db2'] - assert str(sparse_matrix) == 'Matrix([[da1_db1, 0], [da2_db1, da2_db2]])' + assert symbol_list == ["da1_db1", "da2_db1", "da2_db2"] + assert str(sparse_matrix) == "Matrix([[da1_db1, 0], [da2_db1, da2_db2]])" @skip_on_valgrind @@ -28,14 +34,19 @@ def test_csc_matrix_empty(): """Test sparse CSC matrix creation for empty matrix""" printer = AmiciCxxCodePrinter() matrix = sp.Matrix() - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, sparse_matrix \ - = printer.csc_matrix(matrix, rownames=[], colnames=[]) + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = printer.csc_matrix(matrix, rownames=[], colnames=[]) assert symbol_col_ptrs == [] assert symbol_row_vals == [] assert sparse_list == sp.Matrix(0, 0, []) assert symbol_list == [] - assert str(sparse_matrix) == 'Matrix(0, 0, [])' + assert str(sparse_matrix) == "Matrix(0, 0, [])" @skip_on_valgrind @@ -43,30 +54,43 @@ def test_csc_matrix_vector(): """Test sparse CSC matrix creation from matrix slice""" printer = AmiciCxxCodePrinter() matrix = sp.Matrix([[1, 0], [2, 3]]) - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, sparse_matrix \ - = printer.csc_matrix( - matrix[:, 0], colnames=[sp.Symbol('b')], - rownames=[sp.Symbol('a1'), sp.Symbol('a2')] - ) + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = printer.csc_matrix( + matrix[:, 0], + colnames=[sp.Symbol("b")], + rownames=[sp.Symbol("a1"), sp.Symbol("a2")], + ) assert symbol_col_ptrs == [0, 2] assert symbol_row_vals == [0, 1] assert sparse_list == sp.Matrix([[1], [2]]) - assert symbol_list == ['da1_db', 'da2_db'] - assert str(sparse_matrix) == 'Matrix([[da1_db], [da2_db]])' + assert symbol_list == ["da1_db", "da2_db"] + assert str(sparse_matrix) == "Matrix([[da1_db], [da2_db]])" # Test continuation of numbering of symbols - symbol_col_ptrs, symbol_row_vals, sparse_list, symbol_list, sparse_matrix \ - = printer.csc_matrix( - matrix[:, 1], colnames=[sp.Symbol('b')], - rownames=[sp.Symbol('a1'), sp.Symbol('a2')], identifier=1 - ) + ( + symbol_col_ptrs, + symbol_row_vals, + sparse_list, + symbol_list, + sparse_matrix, + ) = printer.csc_matrix( + matrix[:, 1], + colnames=[sp.Symbol("b")], + rownames=[sp.Symbol("a1"), sp.Symbol("a2")], + identifier=1, + ) assert symbol_col_ptrs == [0, 1] assert symbol_row_vals == [1] assert sparse_list == sp.Matrix([[3]]) - assert symbol_list == ['da2_db_1'] - assert str(sparse_matrix) == 'Matrix([[0], [da2_db_1]])' + assert symbol_list == ["da2_db_1"] + assert str(sparse_matrix) == "Matrix([[0], [da2_db_1]])" def test_match_deriv(): diff --git a/python/tests/test_pandas.py b/python/tests/test_pandas.py index 80ee457354..e904fce7cc 100644 --- a/python/tests/test_pandas.py +++ b/python/tests/test_pandas.py @@ -6,20 +6,19 @@ import numpy as np import pytest - # test parameters for test_pandas_import_export -combos = itertools.product( - [(10, 5), (5, 10), ()], - repeat=3 -) -cases = [{ - 'fixedParameters': combo[0], - 'fixedParametersPreequilibration': combo[1], - 'fixedParametersPresimulation': combo[2], -} for combo in combos] - - -@pytest.mark.parametrize('case', cases) +combos = itertools.product([(10, 5), (5, 10), ()], repeat=3) +cases = [ + { + "fixedParameters": combo[0], + "fixedParametersPreequilibration": combo[1], + "fixedParametersPresimulation": combo[2], + } + for combo in combos +] + + +@pytest.mark.parametrize("case", cases) def test_pandas_import_export(sbml_example_presimulation_module, case): """TestCase class for testing csv import using pandas""" @@ -39,19 +38,19 @@ def test_pandas_import_export(sbml_example_presimulation_module, case): df_edata = amici.getDataObservablesAsDataFrame(model, edata) edata_reconstructed = amici.getEdataFromDataFrame(model, df_edata) - for fp in ['fixedParameters', 'fixedParametersPreequilibration', - 'fixedParametersPresimulation']: - - if fp != 'fixedParameters' or case[fp] != (): + for fp in [ + "fixedParameters", + "fixedParametersPreequilibration", + "fixedParametersPresimulation", + ]: + if fp != "fixedParameters" or case[fp] != (): assert getattr(edata[0], fp) == getattr(edata_reconstructed[0], fp) assert case[fp] == getattr(edata_reconstructed[0], fp) else: - assert model.getFixedParameters() \ - == getattr(edata_reconstructed[0], fp) + assert model.getFixedParameters() == getattr(edata_reconstructed[0], fp) - assert model.getFixedParameters() == \ - getattr(edata_reconstructed[0], fp) + assert model.getFixedParameters() == getattr(edata_reconstructed[0], fp) assert getattr(edata[0], fp) == case[fp] diff --git a/python/tests/test_parameter_mapping.py b/python/tests/test_parameter_mapping.py index e2663f4409..32ddf93103 100644 --- a/python/tests/test_parameter_mapping.py +++ b/python/tests/test_parameter_mapping.py @@ -2,9 +2,7 @@ import os import pytest - -from amici.parameter_mapping import (ParameterMapping, - ParameterMappingForCondition) +from amici.parameter_mapping import ParameterMapping, ParameterMappingForCondition from amici.testing import skip_on_valgrind @@ -14,27 +12,29 @@ def test_parameter_mapping_for_condition_default_args(): par_map_for_condition = ParameterMappingForCondition() for attr in [ - 'map_sim_var', 'scale_map_sim_var', 'map_preeq_fix', - 'scale_map_preeq_fix', 'map_sim_fix', 'scale_map_sim_fix']: + "map_sim_var", + "scale_map_sim_var", + "map_preeq_fix", + "scale_map_preeq_fix", + "map_sim_fix", + "scale_map_sim_fix", + ]: assert not getattr(par_map_for_condition, attr) - map_sim_var = {'sim_par0': 8, 'sim_par1': 'opt_par0'} - map_preeq_fix = {'sim_par2': 'opt_par1'} - map_sim_fix = {'sim_par2': 'opt_par2'} + map_sim_var = {"sim_par0": 8, "sim_par1": "opt_par0"} + map_preeq_fix = {"sim_par2": "opt_par1"} + map_sim_fix = {"sim_par2": "opt_par2"} par_map_for_condition = ParameterMappingForCondition( - map_sim_var=map_sim_var, map_preeq_fix=map_preeq_fix, - map_sim_fix=map_sim_fix) + map_sim_var=map_sim_var, map_preeq_fix=map_preeq_fix, map_sim_fix=map_sim_fix + ) - expected_scale_map_sim_var = {'sim_par0': 'lin', 'sim_par1': 'lin'} - expected_scale_map_preeq_fix = {'sim_par2': 'lin'} - expected_scale_map_sim_fix = {'sim_par2': 'lin'} + expected_scale_map_sim_var = {"sim_par0": "lin", "sim_par1": "lin"} + expected_scale_map_preeq_fix = {"sim_par2": "lin"} + expected_scale_map_sim_fix = {"sim_par2": "lin"} - assert par_map_for_condition.scale_map_sim_var == \ - expected_scale_map_sim_var - assert par_map_for_condition.scale_map_preeq_fix == \ - expected_scale_map_preeq_fix - assert par_map_for_condition.scale_map_sim_fix == \ - expected_scale_map_sim_fix + assert par_map_for_condition.scale_map_sim_var == expected_scale_map_sim_var + assert par_map_for_condition.scale_map_preeq_fix == expected_scale_map_preeq_fix + assert par_map_for_condition.scale_map_sim_fix == expected_scale_map_sim_fix @skip_on_valgrind @@ -44,12 +44,12 @@ def test_parameter_mapping(): parameter_mapping = ParameterMapping() assert len(parameter_mapping) == 0 - map_sim_var = {'sim_par0': 8, 'sim_par1': 'opt_par0'} - map_preeq_fix = {'sim_par2': 'opt_par1'} - map_sim_fix = {'sim_par2': 'opt_par2'} + map_sim_var = {"sim_par0": 8, "sim_par1": "opt_par0"} + map_preeq_fix = {"sim_par2": "opt_par1"} + map_sim_fix = {"sim_par2": "opt_par2"} par_map_for_condition = ParameterMappingForCondition( - map_sim_var=map_sim_var, map_preeq_fix=map_preeq_fix, - map_sim_fix=map_sim_fix) + map_sim_var=map_sim_var, map_preeq_fix=map_preeq_fix, map_sim_fix=map_sim_fix + ) parameter_mapping.append(par_map_for_condition) diff --git a/python/tests/test_petab_import.py b/python/tests/test_petab_import.py index eeaf21a3e9..f6db30f18a 100644 --- a/python/tests/test_petab_import.py +++ b/python/tests/test_petab_import.py @@ -1,10 +1,9 @@ """Tests related to amici.petab_import""" import libsbml -import pytest import pandas as pd -from amici.testing import skip_on_valgrind, TemporaryDirectoryWinSafe - +import pytest +from amici.testing import TemporaryDirectoryWinSafe, skip_on_valgrind petab = pytest.importorskip("petab", reason="Missing petab") amici_petab_import = pytest.importorskip("amici.petab_import") @@ -19,7 +18,7 @@ def simple_sbml_model(): model.setId("simple_sbml_model") model.setTimeUnits("second") model.setExtentUnits("mole") - model.setSubstanceUnits('mole') + model.setSubstanceUnits("mole") for par_idx in range(1, 6): p = model.createParameter() @@ -30,7 +29,7 @@ def simple_sbml_model(): c.setId("c1") s = model.createSpecies() - s.setId('x1') + s.setId("x1") s.setConstant(True) s.setInitialConcentration(1.0) s.setCompartment(c.getId()) @@ -49,55 +48,63 @@ def test_get_fixed_parameters(simple_sbml_model): p5: fixed (implicitly, because not listed as estimated) """ from petab.models.sbml_model import SbmlModel + sbml_doc, sbml_model = simple_sbml_model condition_df = petab.get_condition_df( - pd.DataFrame({ - petab.CONDITION_ID: ["condition0"], - "p1": [1.0], - "p2": ["p1"], - }) + pd.DataFrame( + { + petab.CONDITION_ID: ["condition0"], + "p1": [1.0], + "p2": ["p1"], + } + ) ) parameter_df = petab.get_parameter_df( - pd.DataFrame({ - petab.PARAMETER_ID: ["p3", "p4"], - petab.ESTIMATE: [0, 1] - }) + pd.DataFrame({petab.PARAMETER_ID: ["p3", "p4"], petab.ESTIMATE: [0, 1]}) ) print(condition_df) print(parameter_df) - petab_problem = petab.Problem(model=SbmlModel(sbml_model), - parameter_df=parameter_df, - condition_df=condition_df) - assert set(amici_petab_import.get_fixed_parameters(petab_problem)) \ - == {"p1", "p3", "p5"} - - assert set(amici_petab_import.get_fixed_parameters( - petab_problem, - non_estimated_parameters_as_constants=False)) \ - == {"p1", "p5"} + petab_problem = petab.Problem( + model=SbmlModel(sbml_model), + parameter_df=parameter_df, + condition_df=condition_df, + ) + assert set(amici_petab_import.get_fixed_parameters(petab_problem)) == { + "p1", + "p3", + "p5", + } + + assert set( + amici_petab_import.get_fixed_parameters( + petab_problem, non_estimated_parameters_as_constants=False + ) + ) == {"p1", "p5"} @skip_on_valgrind def test_default_output_parameters(simple_sbml_model): from petab.models.sbml_model import SbmlModel + sbml_doc, sbml_model = simple_sbml_model condition_df = petab.get_condition_df( - pd.DataFrame({ - petab.CONDITION_ID: ["condition0"], - }) + pd.DataFrame( + { + petab.CONDITION_ID: ["condition0"], + } + ) ) parameter_df = petab.get_parameter_df( - pd.DataFrame({ - petab.PARAMETER_ID: [], - petab.ESTIMATE: [] - }) + pd.DataFrame({petab.PARAMETER_ID: [], petab.ESTIMATE: []}) ) observable_df = petab.get_observable_df( - pd.DataFrame({ - petab.OBSERVABLE_ID: ["obs1"], - petab.OBSERVABLE_FORMULA: ["observableParameter1_obs1"], - petab.NOISE_FORMULA: [1], - }) + pd.DataFrame( + { + petab.OBSERVABLE_ID: ["obs1"], + petab.OBSERVABLE_FORMULA: ["observableParameter1_obs1"], + petab.NOISE_FORMULA: [1], + } + ) ) petab_problem = petab.Problem( model=SbmlModel(sbml_model), @@ -109,17 +116,19 @@ def test_default_output_parameters(simple_sbml_model): with TemporaryDirectoryWinSafe() as outdir: sbml_importer = amici_petab_import.import_model( petab_problem=petab_problem, - output_parameter_defaults={'observableParameter1_obs1': 1.0}, + output_parameter_defaults={"observableParameter1_obs1": 1.0}, compile=False, model_output_dir=outdir, ) - assert 1.0 == sbml_importer.sbml\ - .getParameter("observableParameter1_obs1").getValue() + assert ( + 1.0 + == sbml_importer.sbml.getParameter("observableParameter1_obs1").getValue() + ) with pytest.raises(ValueError): amici_petab_import.import_model( petab_problem=petab_problem, - output_parameter_defaults={'nonExistentParameter': 1.0}, + output_parameter_defaults={"nonExistentParameter": 1.0}, compile=False, model_output_dir=outdir, ) diff --git a/python/tests/test_petab_objective.py b/python/tests/test_petab_objective.py index 5dfe7db890..1b2436ceab 100755 --- a/python/tests/test_petab_objective.py +++ b/python/tests/test_petab_objective.py @@ -12,7 +12,6 @@ import pytest from amici.petab_objective import SLLH - # Absolute and relative tolerances for finite difference gradient checks. ATOL: float = 1e-3 RTOL: float = 1e-3 @@ -20,13 +19,15 @@ @pytest.fixture def lotka_volterra() -> petab.Problem: - return petab.Problem.from_yaml(str( - Path(__file__).parent - / 'petab_test_problems' - / 'lotka_volterra' - / 'petab' - / 'problem.yaml' - )) + return petab.Problem.from_yaml( + str( + Path(__file__).parent + / "petab_test_problems" + / "lotka_volterra" + / "petab" + / "problem.yaml" + ) + ) def test_simulate_petab_sensitivities(lotka_volterra): @@ -37,18 +38,19 @@ def test_simulate_petab_sensitivities(lotka_volterra): amici_solver.setSensitivityOrder(amici.SensitivityOrder_first) amici_solver.setMaxSteps(int(1e5)) - problem_parameters = dict(zip( - petab_problem.x_ids, - petab_problem.x_nominal, - )) + problem_parameters = dict( + zip( + petab_problem.x_ids, + petab_problem.x_nominal, + ) + ) results = {} for scaled_parameters in [True, False]: for scaled_gradients in [True, False]: _problem_parameters = problem_parameters.copy() if scaled_parameters: - _problem_parameters = \ - petab_problem.scale_parameters(problem_parameters) + _problem_parameters = petab_problem.scale_parameters(problem_parameters) results[(scaled_parameters, scaled_gradients)] = pd.Series( amici.petab_objective.simulate_petab( petab_problem=petab_problem, @@ -62,14 +64,18 @@ def test_simulate_petab_sensitivities(lotka_volterra): # Computed previously, is the same as a central difference gradient # check, to >4 s.f. - expected_results_scaled = pd.Series({ - "alpha": -2.112626, - "gamma": 21.388535, - }) - expected_results_unscaled = pd.Series({ - "alpha": -0.458800, - "gamma": 3.096308, - }) + expected_results_scaled = pd.Series( + { + "alpha": -2.112626, + "gamma": 21.388535, + } + ) + expected_results_unscaled = pd.Series( + { + "alpha": -0.458800, + "gamma": 3.096308, + } + ) assert_equal = partial(pd.testing.assert_series_equal, rtol=1e-3) diff --git a/python/tests/test_petab_simulate.py b/python/tests/test_petab_simulate.py index 1c8ebb59c3..385f98e05e 100644 --- a/python/tests/test_petab_simulate.py +++ b/python/tests/test_petab_simulate.py @@ -1,18 +1,18 @@ """Tests for petab_simulate.py.""" -from pathlib import Path -import pytest import tempfile +from pathlib import Path -from amici.petab_simulate import PetabSimulator import petab import petabtests +import pytest +from amici.petab_simulate import PetabSimulator from amici.testing import skip_on_valgrind @pytest.fixture def petab_problem() -> petab.Problem: """Create a PEtab problem for use in tests.""" - test_case = '0001' + test_case = "0001" test_case_dir = petabtests.get_case_dir( id_=test_case, format_="sbml", version="v1.0.0" ) @@ -48,18 +48,17 @@ def test_subset_call(petab_problem): `model_output_dir`, import is skipped if `amici_model` is specified), and :py:func:`amici.petab_objective.simulate_petab` (`amici_model`, `solver`). """ - model_name = 'model_name_dummy' + model_name = "model_name_dummy" model_output_dir = tempfile.mkdtemp() simulator0 = PetabSimulator(petab_problem) - assert not (Path(model_output_dir)/model_name).is_dir() - simulator0.simulate(model_name=model_name, - model_output_dir=model_output_dir) + assert not (Path(model_output_dir) / model_name).is_dir() + simulator0.simulate(model_name=model_name, model_output_dir=model_output_dir) # Model name is handled correctly assert simulator0.amici_model.getName() == model_name # Check model output directory is created, by # :py:func:`amici.petab_import.import_petab_problem` - assert (Path(model_output_dir)/model_name).is_dir() + assert (Path(model_output_dir) / model_name).is_dir() simulator = PetabSimulator(petab_problem) simulator.simulate(amici_model=simulator0.amici_model) diff --git a/python/tests/test_preequilibration.py b/python/tests/test_preequilibration.py index 1f16d1d240..8fc0defb76 100644 --- a/python/tests/test_preequilibration.py +++ b/python/tests/test_preequilibration.py @@ -2,17 +2,15 @@ import itertools +import amici import numpy as np import pytest from numpy.testing import assert_allclose - -import amici from test_pysb import get_data @pytest.fixture def preeq_fixture(pysb_example_presimulation_module): - model = pysb_example_presimulation_module.getModel() model.setReinitializeFixedParameterInitialStates(True) @@ -30,56 +28,68 @@ def preeq_fixture(pysb_example_presimulation_module): edata_preeq = amici.ExpData(edata) edata_preeq.t_presim = 0 edata_preeq.setTimepoints([np.infty]) - edata_preeq.fixedParameters = \ - edata.fixedParametersPreequilibration + edata_preeq.fixedParameters = edata.fixedParametersPreequilibration edata_preeq.fixedParametersPresimulation = () edata_preeq.fixedParametersPreequilibration = () edata_presim = amici.ExpData(edata) edata_presim.t_presim = 0 edata_presim.setTimepoints([edata.t_presim]) - edata_presim.fixedParameters = \ - edata.fixedParametersPresimulation + edata_presim.fixedParameters = edata.fixedParametersPresimulation edata_presim.fixedParametersPresimulation = () edata_presim.fixedParametersPreequilibration = () edata_sim = amici.ExpData(edata) edata_sim.t_presim = 0 edata_sim.setTimepoints(edata.getTimepoints()) - edata_sim.fixedParameters = \ - edata.fixedParameters + edata_sim.fixedParameters = edata.fixedParameters edata_sim.fixedParametersPresimulation = () edata_sim.fixedParametersPreequilibration = () pscales = [ - amici.ParameterScaling.log10, amici.ParameterScaling.ln, + amici.ParameterScaling.log10, + amici.ParameterScaling.ln, amici.ParameterScaling.none, - amici.parameterScalingFromIntVector([ - amici.ParameterScaling.log10, amici.ParameterScaling.ln, - amici.ParameterScaling.none, amici.ParameterScaling.log10, - amici.ParameterScaling.ln, amici.ParameterScaling.none - ]) + amici.parameterScalingFromIntVector( + [ + amici.ParameterScaling.log10, + amici.ParameterScaling.ln, + amici.ParameterScaling.none, + amici.ParameterScaling.log10, + amici.ParameterScaling.ln, + amici.ParameterScaling.none, + ] + ), ] plists = [ - [3, 1, 2, 4], [0, 1, 2, 3, 4, 5], [5, 3, 2, 0, 4, 1], - [1, 2, 3, 4, 5], [1, 1, 1], + [3, 1, 2, 4], + [0, 1, 2, 3, 4, 5], + [5, 3, 2, 0, 4, 1], + [1, 2, 3, 4, 5], + [1, 1, 1], ] - return (model, solver, edata, edata_preeq, - edata_presim, edata_sim, pscales, plists) + return (model, solver, edata, edata_preeq, edata_presim, edata_sim, pscales, plists) def test_manual_preequilibration(preeq_fixture): """Manual preequilibration""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture settings = itertools.product(pscales, plists) for pscale, plist in settings: - model.setInitialStates([]) model.setInitialStateSensitivities([]) model.setParameterList(plist) @@ -94,10 +104,10 @@ def test_manual_preequilibration(preeq_fixture): assert rdata_preeq.status == amici.AMICI_SUCCESS # manual reinitialization + presimulation - x0 = rdata_preeq['x'][0, :] + x0 = rdata_preeq["x"][0, :] x0[1] = edata_presim.fixedParameters[0] x0[2] = edata_presim.fixedParameters[1] - sx0 = rdata_preeq['sx'][0, :, :] + sx0 = rdata_preeq["sx"][0, :, :] sx0[:, 1] = 0 sx0[:, 2] = 0 model.setInitialStates(x0) @@ -106,10 +116,10 @@ def test_manual_preequilibration(preeq_fixture): assert rdata_presim.status == amici.AMICI_SUCCESS # manual reinitialization + simulation - x0 = rdata_presim['x'][0, :] + x0 = rdata_presim["x"][0, :] x0[1] = edata_sim.fixedParameters[0] x0[2] = edata_sim.fixedParameters[1] - sx0 = rdata_presim['sx'][0, :, :] + sx0 = rdata_presim["sx"][0, :, :] sx0[:, 1] = 0 sx0[:, 2] = 0 model.setInitialStates(x0) @@ -117,20 +127,29 @@ def test_manual_preequilibration(preeq_fixture): rdata_sim = amici.runAmiciSimulation(model, solver, edata_sim) assert rdata_sim.status == amici.AMICI_SUCCESS - for variable in ['x', 'sx']: + for variable in ["x", "sx"]: assert_allclose( rdata_auto[variable], rdata_sim[variable], - atol=1e-6, rtol=1e-6, - err_msg=str(dict(pscale=pscale, plist=plist, variable=variable)) + atol=1e-6, + rtol=1e-6, + err_msg=str(dict(pscale=pscale, plist=plist, variable=variable)), ) def test_parameter_reordering(preeq_fixture): """Test parameter reordering""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture rdata_ordered = amici.runAmiciSimulation(model, solver, edata) @@ -140,17 +159,26 @@ def test_parameter_reordering(preeq_fixture): for ip, p_index in enumerate(plist): assert np.isclose( - rdata_ordered['sx'][:, p_index, :], - rdata_reordered['sx'][:, ip, :], - 1e-6, 1e-6 + rdata_ordered["sx"][:, p_index, :], + rdata_reordered["sx"][:, ip, :], + 1e-6, + 1e-6, ).all(), plist def test_data_replicates(preeq_fixture): """Test data replicates""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture sensi_meth = amici.SensitivityMethod.forward solver.setSensitivityMethod(sensi_meth) @@ -175,20 +203,29 @@ def test_data_replicates(preeq_fixture): rdata_double = amici.runAmiciSimulation(model, solver, edata) - for variable in ['llh', 'sllh']: + for variable in ["llh", "sllh"]: assert_allclose( 2 * rdata_single[variable], rdata_double[variable], - atol=1e-6, rtol=1e-6, - err_msg=str(dict(variable=variable, sensi_meth=sensi_meth)) + atol=1e-6, + rtol=1e-6, + err_msg=str(dict(variable=variable, sensi_meth=sensi_meth)), ) def test_parameter_in_expdata(preeq_fixture): """Test parameter in ExpData""" - model, solver, edata, edata_preeq, edata_presim, \ - edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture rdata = amici.runAmiciSimulation(model, solver, edata) @@ -201,57 +238,61 @@ def test_parameter_in_expdata(preeq_fixture): edata.sx0 = model.getInitialStateSensitivities() # perturb model initial states - model.setInitialStates(rdata['x_ss'] * 4) - model.setInitialStateSensitivities(rdata['sx_ss'].flatten() / 2) + model.setInitialStates(rdata["x_ss"] * 4) + model.setInitialStateSensitivities(rdata["sx_ss"].flatten() / 2) # set ExpData plist edata.plist = model.getParameterList() # perturb model parameter list - model.setParameterList([ - i for i in reversed(model.getParameterList()) - ]) + model.setParameterList([i for i in reversed(model.getParameterList())]) # set ExpData parameters edata.parameters = model.getParameters() # perturb model parameters - model.setParameters(tuple( - p * 2 for p in model.getParameters() - )) + model.setParameters(tuple(p * 2 for p in model.getParameters())) # set ExpData pscale edata.pscale = model.getParameterScale() # perturb model pscale, needs to be done after getting parameters, # otherwise we will mess up parameter value - model.setParameterScale(amici.parameterScalingFromIntVector([ - amici.ParameterScaling.log10 - if scaling == amici.ParameterScaling.none - else amici.ParameterScaling.none - for scaling in model.getParameterScale() - ])) - - rdata_edata = amici.runAmiciSimulation( - model, solver, edata + model.setParameterScale( + amici.parameterScalingFromIntVector( + [ + amici.ParameterScaling.log10 + if scaling == amici.ParameterScaling.none + else amici.ParameterScaling.none + for scaling in model.getParameterScale() + ] + ) ) - for variable in ['x', 'sx']: + + rdata_edata = amici.runAmiciSimulation(model, solver, edata) + for variable in ["x", "sx"]: assert np.isclose( - rdata[variable][0, :], - rdata_edata[variable][0, :], - 1e-6, 1e-6 + rdata[variable][0, :], rdata_edata[variable][0, :], 1e-6, 1e-6 ).all(), variable def test_raise_presimulation_with_adjoints(preeq_fixture): """Test simulation failures with adjoin+presimulation""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture # preequilibration and presimulation with adjoints: # this needs to fail unless we remove presimulation solver.setSensitivityMethod(amici.SensitivityMethod.adjoint) rdata = amici.runAmiciSimulation(model, solver, edata) - assert rdata['status'] == amici.AMICI_ERROR + assert rdata["status"] == amici.AMICI_ERROR # add postequilibration y = edata.getObservedData() @@ -267,15 +308,23 @@ def test_raise_presimulation_with_adjoints(preeq_fixture): # no presim any more, this should work rdata = amici.runAmiciSimulation(model, solver, edata) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS def test_equilibration_methods_with_adjoints(preeq_fixture): """Test different combinations of equilibration and simulation sensitivity methods""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture # we don't want presim edata.t_presim = 0.0 @@ -290,11 +339,12 @@ def test_equilibration_methods_with_adjoints(preeq_fixture): edata.setObservedDataStdDev(np.hstack([stdy, stdy[0]])) rdatas = {} - equil_meths = [amici.SteadyStateSensitivityMode.newtonOnly, - amici.SteadyStateSensitivityMode.integrationOnly, - amici.SteadyStateSensitivityMode.integrateIfNewtonFails] - sensi_meths = [amici.SensitivityMethod.forward, - amici.SensitivityMethod.adjoint] + equil_meths = [ + amici.SteadyStateSensitivityMode.newtonOnly, + amici.SteadyStateSensitivityMode.integrationOnly, + amici.SteadyStateSensitivityMode.integrateIfNewtonFails, + ] + sensi_meths = [amici.SensitivityMethod.forward, amici.SensitivityMethod.adjoint] settings = itertools.product(equil_meths, sensi_meths) for setting in settings: @@ -308,23 +358,29 @@ def test_equilibration_methods_with_adjoints(preeq_fixture): rdatas[setting] = amici.runAmiciSimulation(model, solver, edata) # assert successful simulation - assert rdatas[setting]['status'] == amici.AMICI_SUCCESS + assert rdatas[setting]["status"] == amici.AMICI_SUCCESS for setting1, setting2 in itertools.product(settings, settings): # assert correctness of result - for variable in ['llh', 'sllh']: + for variable in ["llh", "sllh"]: assert np.isclose( - rdatas[setting1][variable], - rdatas[setting2][variable], - 1e-6, 1e-6 + rdatas[setting1][variable], rdatas[setting2][variable], 1e-6, 1e-6 ).all(), variable def test_newton_solver_equilibration(preeq_fixture): """Test data replicates""" - model, solver, edata, edata_preeq, \ - edata_presim, edata_sim, pscales, plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture # we don't want presim edata.t_presim = 0.0 @@ -339,8 +395,10 @@ def test_newton_solver_equilibration(preeq_fixture): edata.setObservedDataStdDev(np.hstack([stdy, stdy[0]])) rdatas = {} - settings = [amici.SteadyStateSensitivityMode.integrationOnly, - amici.SteadyStateSensitivityMode.newtonOnly] + settings = [ + amici.SteadyStateSensitivityMode.integrationOnly, + amici.SteadyStateSensitivityMode.newtonOnly, + ] solver.setNewtonStepSteadyStateCheck(True) solver.setRelativeToleranceSteadyState(1e-12) @@ -357,22 +415,28 @@ def test_newton_solver_equilibration(preeq_fixture): rdatas[equil_meth] = amici.runAmiciSimulation(model, solver, edata) # assert successful simulation - assert rdatas[equil_meth]['status'] == amici.AMICI_SUCCESS + assert rdatas[equil_meth]["status"] == amici.AMICI_SUCCESS # assert correct results - for variable in ['llh', 'sllh', 'sx0', 'sx_ss', 'x_ss']: + for variable in ["llh", "sllh", "sx0", "sx_ss", "x_ss"]: assert np.isclose( - rdatas[settings[0]][variable], - rdatas[settings[1]][variable], - 1e-5, 1e-5 + rdatas[settings[0]][variable], rdatas[settings[1]][variable], 1e-5, 1e-5 ).all(), variable def test_newton_steadystate_check(preeq_fixture): """Test data replicates""" - model, solver, edata, edata_preeq, edata_presim, edata_sim, pscales, \ - plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture # we don't want presim edata.t_presim = 0.0 @@ -399,38 +463,49 @@ def test_newton_steadystate_check(preeq_fixture): rdatas[newton_check] = amici.runAmiciSimulation(model, solver, edata) # assert successful simulation - assert rdatas[newton_check]['status'] == amici.AMICI_SUCCESS + assert rdatas[newton_check]["status"] == amici.AMICI_SUCCESS # assert correct results - for variable in ['llh', 'sllh', 'sx0', 'sx_ss', 'x_ss']: + for variable in ["llh", "sllh", "sx0", "sx_ss", "x_ss"]: assert np.isclose( - rdatas[True][variable], - rdatas[False][variable], - 1e-6, 1e-6 + rdatas[True][variable], rdatas[False][variable], 1e-6, 1e-6 ).all(), variable def test_simulation_errors(preeq_fixture): - model, solver, edata, edata_preeq, edata_presim, edata_sim, pscales, \ - plists = preeq_fixture + ( + model, + solver, + edata, + edata_preeq, + edata_presim, + edata_sim, + pscales, + plists, + ) = preeq_fixture solver.setSensitivityOrder(amici.SensitivityOrder.first) solver.setSensitivityMethodPreequilibration(amici.SensitivityMethod.forward) - model.setSteadyStateSensitivityMode(amici.SteadyStateSensitivityMode.integrationOnly) + model.setSteadyStateSensitivityMode( + amici.SteadyStateSensitivityMode.integrationOnly + ) solver.setMaxSteps(1) # exceeded maxsteps # preeq & posteq for e in [edata, edata_preeq]: rdata = amici.runAmiciSimulation(model, solver, e) - assert rdata['status'] != amici.AMICI_SUCCESS + assert rdata["status"] != amici.AMICI_SUCCESS assert rdata._swigptr.messages[0].severity == amici.LogSeverity_debug - assert rdata._swigptr.messages[0].identifier == 'EQUILIBRATION_FAILURE' - assert 'exceeded maximum number of integration steps' in rdata._swigptr.messages[0].message + assert rdata._swigptr.messages[0].identifier == "EQUILIBRATION_FAILURE" + assert ( + "exceeded maximum number of integration steps" + in rdata._swigptr.messages[0].message + ) assert rdata._swigptr.messages[1].severity == amici.LogSeverity_error - assert rdata._swigptr.messages[1].identifier == 'OTHER' + assert rdata._swigptr.messages[1].identifier == "OTHER" assert rdata._swigptr.messages[2].severity == amici.LogSeverity_debug - assert rdata._swigptr.messages[2].identifier == 'BACKTRACE' + assert rdata._swigptr.messages[2].identifier == "BACKTRACE" # too long simulations solver.setMaxSteps(int(1e4)) @@ -439,18 +514,13 @@ def test_simulation_errors(preeq_fixture): # preeq & posteq for e in [edata_preeq, edata]: rdata = amici.runAmiciSimulation(model, solver, e) - assert rdata['status'] != amici.AMICI_SUCCESS + assert rdata["status"] != amici.AMICI_SUCCESS assert rdata._swigptr.messages[0].severity == amici.LogSeverity_debug - assert rdata._swigptr.messages[0].identifier == 'CVODES:CVode:RHSFUNC_FAIL' + assert rdata._swigptr.messages[0].identifier == "CVODES:CVode:RHSFUNC_FAIL" assert rdata._swigptr.messages[1].severity == amici.LogSeverity_debug - assert rdata._swigptr.messages[1].identifier == 'EQUILIBRATION_FAILURE' - assert 'exceedingly long simulation time' in rdata._swigptr.messages[1].message + assert rdata._swigptr.messages[1].identifier == "EQUILIBRATION_FAILURE" + assert "exceedingly long simulation time" in rdata._swigptr.messages[1].message assert rdata._swigptr.messages[2].severity == amici.LogSeverity_error - assert rdata._swigptr.messages[2].identifier == 'OTHER' + assert rdata._swigptr.messages[2].identifier == "OTHER" assert rdata._swigptr.messages[3].severity == amici.LogSeverity_debug - assert rdata._swigptr.messages[3].identifier == 'BACKTRACE' - - - - - + assert rdata._swigptr.messages[3].identifier == "BACKTRACE" diff --git a/python/tests/test_pregenerated_models.py b/python/tests/test_pregenerated_models.py index d63e4b924b..bd4bb7e53b 100755 --- a/python/tests/test_pregenerated_models.py +++ b/python/tests/test_pregenerated_models.py @@ -10,23 +10,26 @@ import h5py import numpy as np import pytest -from amici.gradient_check import check_derivatives, _check_results +from amici.gradient_check import _check_results, check_derivatives from amici.testing import skip_on_valgrind +cpp_test_dir = Path(__file__).parents[2] / "tests" / "cpp" +options_file = str(cpp_test_dir / "testOptions.h5") +expected_results_file = str(cpp_test_dir / "expectedResults.h5") +expected_results = h5py.File(expected_results_file, "r") -cpp_test_dir = Path(__file__).parents[2] / 'tests' / 'cpp' -options_file = str(cpp_test_dir / 'testOptions.h5') -expected_results_file = str(cpp_test_dir / 'expectedResults.h5') -expected_results = h5py.File(expected_results_file, 'r') - -model_cases = [(sub_test, case) - for sub_test in expected_results.keys() - for case in list(expected_results[sub_test].keys())] +model_cases = [ + (sub_test, case) + for sub_test in expected_results.keys() + for case in list(expected_results[sub_test].keys()) +] @skip_on_valgrind -@pytest.mark.skipif(os.environ.get('AMICI_SKIP_CMAKE_TESTS', '') == 'TRUE', - reason='skipping cmake based test') +@pytest.mark.skipif( + os.environ.get("AMICI_SKIP_CMAKE_TESTS", "") == "TRUE", + reason="skipping cmake based test", +) @pytest.mark.parametrize("sub_test,case", model_cases) def test_pregenerated_model(sub_test, case): """Tests models that were pregenerated using the matlab code @@ -36,77 +39,81 @@ def test_pregenerated_model(sub_test, case): the python modules for the test models. """ - if case.startswith('sensi2'): - model_name = sub_test + '_o2' + if case.startswith("sensi2"): + model_name = sub_test + "_o2" else: model_name = sub_test - model_swig_folder = str(Path(__file__).parents[2] / 'build' / 'tests' - / 'cpp' / f'external_{model_name}-prefix' / 'src' - / f'external_{model_name}-build' / 'swig') + model_swig_folder = str( + Path(__file__).parents[2] + / "build" + / "tests" + / "cpp" + / f"external_{model_name}-prefix" + / "src" + / f"external_{model_name}-build" + / "swig" + ) test_model_module = amici.import_model_module( - module_name=model_name, module_path=model_swig_folder) + module_name=model_name, module_path=model_swig_folder + ) model = test_model_module.getModel() solver = model.getSolver() amici.readModelDataFromHDF5( - options_file, model.get(), - f'/{sub_test}/{case}/options' + options_file, model.get(), f"/{sub_test}/{case}/options" ) amici.readSolverSettingsFromHDF5( - options_file, solver.get(), - f'/{sub_test}/{case}/options' + options_file, solver.get(), f"/{sub_test}/{case}/options" ) edata = None - if 'data' in expected_results[sub_test][case].keys(): + if "data" in expected_results[sub_test][case].keys(): edata = amici.readSimulationExpData( - str(expected_results_file), - f'/{sub_test}/{case}/data', model.get() + str(expected_results_file), f"/{sub_test}/{case}/data", model.get() ) - rdata = amici.runAmiciSimulation(model, solver, - edata) + rdata = amici.runAmiciSimulation(model, solver, edata) check_derivative_opts = dict() - if model_name == 'model_nested_events': - check_derivative_opts['rtol'] = 1e-2 - elif model_name == 'model_events': - check_derivative_opts['atol'] = 1e-3 - - if edata \ - and solver.getSensitivityMethod() \ - and solver.getSensitivityOrder() \ - and len(model.getParameterList()) \ - and not model_name.startswith('model_neuron') \ - and not case.endswith('byhandpreeq'): + if model_name == "model_nested_events": + check_derivative_opts["rtol"] = 1e-2 + elif model_name == "model_events": + check_derivative_opts["atol"] = 1e-3 + + if ( + edata + and solver.getSensitivityMethod() + and solver.getSensitivityOrder() + and len(model.getParameterList()) + and not model_name.startswith("model_neuron") + and not case.endswith("byhandpreeq") + ): check_derivatives(model, solver, edata, **check_derivative_opts) verify_simulation_opts = dict() - if model_name.startswith('model_neuron'): - verify_simulation_opts['atol'] = 1e-5 - verify_simulation_opts['rtol'] = 1e-2 + if model_name.startswith("model_neuron"): + verify_simulation_opts["atol"] = 1e-5 + verify_simulation_opts["rtol"] = 1e-2 - if model_name.startswith('model_robertson') and \ - case == 'sensiforwardSPBCG': - verify_simulation_opts['atol'] = 1e-3 - verify_simulation_opts['rtol'] = 1e-3 + if model_name.startswith("model_robertson") and case == "sensiforwardSPBCG": + verify_simulation_opts["atol"] = 1e-3 + verify_simulation_opts["rtol"] = 1e-3 verify_simulation_results( - rdata, expected_results[sub_test][case]['results'], - **verify_simulation_opts + rdata, expected_results[sub_test][case]["results"], **verify_simulation_opts ) - if model_name == 'model_steadystate' and \ - case == 'sensiforwarderrorint': + if model_name == "model_steadystate" and case == "sensiforwarderrorint": edata = amici.amici.ExpData(model.get()) # Test runAmiciSimulations: ensure running twice # with same ExpData yields same results - if edata and model_name != 'model_neuron_o2' and not ( - model_name == 'model_robertson' and - case == 'sensiforwardSPBCG' + if ( + edata + and model_name != "model_neuron_o2" + and not (model_name == "model_robertson" and case == "sensiforwardSPBCG") ): if isinstance(edata, amici.amici.ExpData): edatas = [edata, edata] @@ -114,16 +121,17 @@ def test_pregenerated_model(sub_test, case): edatas = [edata.get(), edata.get()] rdatas = amici.runAmiciSimulations( - model, solver, edatas, num_threads=2, - failfast=False + model, solver, edatas, num_threads=2, failfast=False ) verify_simulation_results( - rdatas[0], expected_results[sub_test][case]['results'], - **verify_simulation_opts + rdatas[0], + expected_results[sub_test][case]["results"], + **verify_simulation_opts, ) verify_simulation_results( - rdatas[1], expected_results[sub_test][case]['results'], - **verify_simulation_opts + rdatas[1], + expected_results[sub_test][case]["results"], + **verify_simulation_opts, ) # test residuals mode @@ -134,9 +142,10 @@ def test_pregenerated_model(sub_test, case): solver.setReturnDataReportingMode(amici.RDataReporting.residuals) rdata = amici.runAmiciSimulation(model, solver, edata) verify_simulation_results( - rdata, expected_results[sub_test][case]['results'], - fields=['t', 'res', 'sres', 'y', 'sy', 'sigmay', 'ssigmay'], - **verify_simulation_opts + rdata, + expected_results[sub_test][case]["results"], + fields=["t", "res", "sres", "y", "sy", "sigmay", "ssigmay"], + **verify_simulation_opts, ) with pytest.raises(RuntimeError): solver.setSensitivityMethod(amici.SensitivityMethod.adjoint) @@ -147,22 +156,30 @@ def test_pregenerated_model(sub_test, case): solver.setReturnDataReportingMode(amici.RDataReporting.likelihood) rdata = amici.runAmiciSimulation(model, solver, edata) verify_simulation_results( - rdata, expected_results[sub_test][case]['results'], - fields=['t', 'llh', 'sllh', 's2llh', 'FIM'], **verify_simulation_opts + rdata, + expected_results[sub_test][case]["results"], + fields=["t", "llh", "sllh", "s2llh", "FIM"], + **verify_simulation_opts, ) # test sigma residuals - if model_name == 'model_jakstat_adjoint' and \ - solver.getSensitivityMethod() != amici.SensitivityMethod.adjoint: + if ( + model_name == "model_jakstat_adjoint" + and solver.getSensitivityMethod() != amici.SensitivityMethod.adjoint + ): model.setAddSigmaResiduals(True) solver.setReturnDataReportingMode(amici.RDataReporting.full) rdata = amici.runAmiciSimulation(model, solver, edata) # check whether activation changes chi2 assert chi2_ref != rdata.chi2 - if edata and solver.getSensitivityMethod() and \ - solver.getSensitivityOrder() and len(model.getParameterList()): + if ( + edata + and solver.getSensitivityMethod() + and solver.getSensitivityOrder() + and len(model.getParameterList()) + ): check_derivatives(model, solver, edata, **check_derivative_opts) chi2_ref = rdata.chi2 @@ -180,11 +197,12 @@ def test_pregenerated_model(sub_test, case): assert np.isnan(rdata.chi2) with pytest.raises(RuntimeError): - model.getParameterByName('thisParameterDoesNotExist') + model.getParameterByName("thisParameterDoesNotExist") -def verify_simulation_results(rdata, expected_results, fields=None, - atol=1e-8, rtol=1e-4): +def verify_simulation_results( + rdata, expected_results, fields=None, atol=1e-8, rtol=1e-4 +): """ compares all fields of the simulation results in rdata against the expectedResults using the provided tolerances @@ -200,42 +218,48 @@ def verify_simulation_results(rdata, expected_results, fields=None, if fields is None: attrs = expected_results.attrs.keys() fields = expected_results.keys() - if 'diagnosis' in expected_results.keys(): - subfields = expected_results['diagnosis'].keys() + if "diagnosis" in expected_results.keys(): + subfields = expected_results["diagnosis"].keys() else: - attrs = [field for field in fields - if field in expected_results.attrs.keys()] - if 'diagnosis' in expected_results.keys(): - subfields = [field for field in fields - if field in expected_results['diagnosis'].keys()] - fields = [field for field in fields - if field in expected_results.keys()] - - if expected_results.attrs['status'][0] != 0: - assert rdata['status'] == expected_results.attrs['status'][0] + attrs = [field for field in fields if field in expected_results.attrs.keys()] + if "diagnosis" in expected_results.keys(): + subfields = [ + field + for field in fields + if field in expected_results["diagnosis"].keys() + ] + fields = [field for field in fields if field in expected_results.keys()] + + if expected_results.attrs["status"][0] != 0: + assert rdata["status"] == expected_results.attrs["status"][0] return for field in expected_results.keys(): - if field == 'diagnosis': - for subfield in ['J', 'xdot']: + if field == "diagnosis": + for subfield in ["J", "xdot"]: if subfield not in subfields: assert rdata[subfield] is None, field continue - _check_results(rdata, subfield, - expected_results[field][subfield][()], - atol=1e-8, rtol=1e-8) + _check_results( + rdata, + subfield, + expected_results[field][subfield][()], + atol=1e-8, + rtol=1e-8, + ) else: if field not in fields: assert rdata[field] is None, field continue - if field == 's2llh': - _check_results(rdata, field, expected_results[field][()], - atol=1e-4, rtol=1e-3) + if field == "s2llh": + _check_results( + rdata, field, expected_results[field][()], atol=1e-4, rtol=1e-3 + ) else: - _check_results(rdata, field, expected_results[field][()], - atol=atol, rtol=rtol) + _check_results( + rdata, field, expected_results[field][()], atol=atol, rtol=rtol + ) for attr in attrs: - _check_results(rdata, attr, expected_results.attrs[attr], - atol=atol, rtol=rtol) + _check_results(rdata, attr, expected_results.attrs[attr], atol=atol, rtol=rtol) diff --git a/python/tests/test_pysb.py b/python/tests/test_pysb.py index fc0de542f1..334fca208d 100644 --- a/python/tests/test_pysb.py +++ b/python/tests/test_pysb.py @@ -10,20 +10,21 @@ import amici import numpy as np -import sympy as sp import pysb.examples import pytest -from amici.pysb_import import pysb2amici +import sympy as sp from amici import ParameterScaling, parameterScalingFromIntVector -from pysb.simulator import ScipyOdeSimulator - from amici.gradient_check import check_derivatives -from amici.testing import skip_on_valgrind, TemporaryDirectoryWinSafe +from amici.pysb_import import pysb2amici +from amici.testing import TemporaryDirectoryWinSafe, skip_on_valgrind from numpy.testing import assert_allclose +from pysb.simulator import ScipyOdeSimulator + @skip_on_valgrind -def test_compare_to_sbml_import(pysb_example_presimulation_module, - sbml_example_presimulation_module): +def test_compare_to_sbml_import( + pysb_example_presimulation_module, sbml_example_presimulation_module +): # -------------- PYSB ----------------- model_pysb = pysb_example_presimulation_module.getModel() @@ -39,35 +40,51 @@ def test_compare_to_sbml_import(pysb_example_presimulation_module, rdata_sbml = get_results(model_sbml, edata) # check if preequilibration fixed parameters are correctly applied: - for rdata, model, importer in zip([rdata_sbml, rdata_pysb], - [model_sbml, model_pysb], - ['sbml', 'pysb']): + for rdata, model, importer in zip( + [rdata_sbml, rdata_pysb], [model_sbml, model_pysb], ["sbml", "pysb"] + ): # check equilibrium fixed parameters assert np.isclose( [sum(rdata["x_ss"][[1, 3]]), sum(rdata["x_ss"][[2, 4]])], edata.fixedParametersPreequilibration, - atol=1e-6, rtol=1e-6 - ).all(), f'{importer} preequilibration' + atol=1e-6, + rtol=1e-6, + ).all(), f"{importer} preequilibration" # check equilibrium initial parameters assert np.isclose( sum(rdata["x_ss"][[0, 3, 4, 5]]), - model.getParameterByName('PROT_0'), - atol=1e-6, rtol=1e-6 - ), f'{importer} preequilibration' + model.getParameterByName("PROT_0"), + atol=1e-6, + rtol=1e-6, + ), f"{importer} preequilibration" # check reinitialization with fixed parameter after # presimulation assert np.isclose( [rdata["x0"][1], rdata["x0"][2]], edata.fixedParameters, - atol=1e-6, rtol=1e-6 - ).all(), f'{importer} presimulation' - - skip_attrs = ['ptr', 'preeq_t', 'numsteps', 'preeq_numsteps', - 'numrhsevals', 'numerrtestfails', 'order', 'J', 'xdot', - 'preeq_wrms', 'preeq_cpu_time', 'cpu_time', - 'cpu_timeB', 'cpu_time_total', 'w'] + atol=1e-6, + rtol=1e-6, + ).all(), f"{importer} presimulation" + + skip_attrs = [ + "ptr", + "preeq_t", + "numsteps", + "preeq_numsteps", + "numrhsevals", + "numerrtestfails", + "order", + "J", + "xdot", + "preeq_wrms", + "preeq_cpu_time", + "cpu_time", + "cpu_timeB", + "cpu_time_total", + "w", + ] for field in rdata_pysb: if field in skip_attrs: @@ -83,61 +100,79 @@ def test_compare_to_sbml_import(pysb_example_presimulation_module, assert np.isnan(rdata_sbml[field]).all(), field else: assert_allclose( - rdata_sbml[field], rdata_pysb[field], - atol=1e-6, rtol=1e-6, - err_msg=field + rdata_sbml[field], + rdata_pysb[field], + atol=1e-6, + rtol=1e-6, + err_msg=field, ) pysb_models = [ - 'tyson_oscillator', 'robertson', 'expression_observables', - 'bax_pore_sequential', 'bax_pore', 'bngwiki_egfr_simple', - 'bngwiki_enzymatic_cycle_mm', 'bngwiki_simple', 'earm_1_0', - 'earm_1_3', 'move_connected', 'michment', 'kinase_cascade', - 'hello_pysb', 'fricker_2010_apoptosis', 'explicit', - 'fixed_initial', 'localfunc' + "tyson_oscillator", + "robertson", + "expression_observables", + "bax_pore_sequential", + "bax_pore", + "bngwiki_egfr_simple", + "bngwiki_enzymatic_cycle_mm", + "bngwiki_simple", + "earm_1_0", + "earm_1_3", + "move_connected", + "michment", + "kinase_cascade", + "hello_pysb", + "fricker_2010_apoptosis", + "explicit", + "fixed_initial", + "localfunc", ] custom_models = [ - 'bngwiki_egfr_simple_deletemolecules', + "bngwiki_egfr_simple_deletemolecules", ] @skip_on_valgrind -@pytest.mark.parametrize('example', pysb_models + custom_models) +@pytest.mark.parametrize("example", pysb_models + custom_models) def test_compare_to_pysb_simulation(example): - atol = 1e-8 rtol = 1e-8 with amici.add_path(os.path.dirname(pysb.examples.__file__)): - with amici.add_path(os.path.join(os.path.dirname(__file__), '..', - 'tests', 'pysb_test_models')): + with amici.add_path( + os.path.join(os.path.dirname(__file__), "..", "tests", "pysb_test_models") + ): # load example pysb.SelfExporter.cleanup() # reset pysb pysb.SelfExporter.do_export = True module = importlib.import_module(example) pysb_model = module.model - pysb_model.name = pysb_model.name.replace('pysb.examples.', '') + pysb_model.name = pysb_model.name.replace("pysb.examples.", "") # avoid naming clash for custom pysb models - pysb_model.name += '_amici' + pysb_model.name += "_amici" # pysb part tspan = np.linspace(0, 100, 101) sim = ScipyOdeSimulator( pysb_model, tspan=tspan, - integrator_options={'rtol': rtol, 'atol': atol}, - compiler='python' + integrator_options={"rtol": rtol, "atol": atol}, + compiler="python", ) pysb_simres = sim.run() # amici part with TemporaryDirectoryWinSafe(prefix=pysb_model.name) as outdir: - if pysb_model.name in ['move_connected_amici']: + if pysb_model.name in ["move_connected_amici"]: with pytest.raises(Exception): - pysb2amici(pysb_model, outdir, verbose=logging.INFO, - compute_conservation_laws=True) + pysb2amici( + pysb_model, + outdir, + verbose=logging.INFO, + compute_conservation_laws=True, + ) compute_conservation_laws = False else: compute_conservation_laws = True @@ -147,11 +182,10 @@ def test_compare_to_pysb_simulation(example): outdir, verbose=logging.INFO, compute_conservation_laws=compute_conservation_laws, - observables=list(pysb_model.observables.keys()) + observables=list(pysb_model.observables.keys()), ) - amici_model_module = amici.import_model_module(pysb_model.name, - outdir) + amici_model_module = amici.import_model_module(pysb_model.name, outdir) model_pysb = amici_model_module.getModel() model_pysb.setTimepoints(tspan) @@ -162,16 +196,22 @@ def test_compare_to_pysb_simulation(example): rdata = amici.runAmiciSimulation(model_pysb, solver) # check agreement of species simulations - assert np.isclose(rdata['x'], - pysb_simres.species, 1e-4, 1e-4).all() - - if example not in ['fricker_2010_apoptosis', 'fixed_initial', - 'bngwiki_egfr_simple_deletemolecules']: - if example in ['tyson_oscillator', 'bax_pore_sequential', - 'bax_pore', 'kinase_cascade', - 'bngwiki_egfr_simple', - 'bngwiki_enzymatic_cycle_mm', - 'bngwiki_simple']: + assert np.isclose(rdata["x"], pysb_simres.species, 1e-4, 1e-4).all() + + if example not in [ + "fricker_2010_apoptosis", + "fixed_initial", + "bngwiki_egfr_simple_deletemolecules", + ]: + if example in [ + "tyson_oscillator", + "bax_pore_sequential", + "bax_pore", + "kinase_cascade", + "bngwiki_egfr_simple", + "bngwiki_enzymatic_cycle_mm", + "bngwiki_simple", + ]: solver.setAbsoluteTolerance(1e-14) solver.setRelativeTolerance(1e-14) epsilon = 1e-4 @@ -182,17 +222,21 @@ def test_compare_to_pysb_simulation(example): model_pysb.setParameterScale( parameterScalingFromIntVector( [ - ParameterScaling.log10 if p > 0 + ParameterScaling.log10 + if p > 0 else ParameterScaling.none for p in model_pysb.getParameters() ] ) ) - check_derivatives(model_pysb, solver, - epsilon=epsilon, - rtol=1e-2, - atol=1e-2, - skip_zero_pars=True) + check_derivatives( + model_pysb, + solver, + epsilon=epsilon, + rtol=1e-2, + atol=1e-2, + skip_zero_pars=True, + ) def get_data(model): @@ -227,44 +271,45 @@ def get_results(model, edata): def test_names_and_ids(pysb_example_presimulation_module): model_pysb = pysb_example_presimulation_module.getModel() expected = { - 'ExpressionIds': ( - '__s2', - '__s1', - '__s5', - 'pPROT', - 'tPROT', - 'initProt', - 'initDrug', - 'initKin', - 'pPROT_obs'), - 'FixedParameterIds': ('DRUG_0', 'KIN_0'), - 'FixedParameterNames': ('DRUG_0', 'KIN_0'), - 'ObservableIds': ('pPROT_obs',), - 'ObservableNames': ('pPROT_obs',), - 'ParameterIds': ( - 'PROT_0', - 'kon_prot_drug', - 'koff_prot_drug', - 'kon_prot_kin', - 'kphospho_prot_kin', - 'kdephospho_prot' + "ExpressionIds": ( + "__s2", + "__s1", + "__s5", + "pPROT", + "tPROT", + "initProt", + "initDrug", + "initKin", + "pPROT_obs", + ), + "FixedParameterIds": ("DRUG_0", "KIN_0"), + "FixedParameterNames": ("DRUG_0", "KIN_0"), + "ObservableIds": ("pPROT_obs",), + "ObservableNames": ("pPROT_obs",), + "ParameterIds": ( + "PROT_0", + "kon_prot_drug", + "koff_prot_drug", + "kon_prot_kin", + "kphospho_prot_kin", + "kdephospho_prot", ), - 'StateIds': ('__s0', '__s1', '__s2', '__s3', '__s4', '__s5'), - 'StateNames': ( + "StateIds": ("__s0", "__s1", "__s2", "__s3", "__s4", "__s5"), + "StateNames": ( "PROT(kin=None, drug=None, phospho='u')", - 'DRUG(bound=None)', - 'KIN(bound=None)', + "DRUG(bound=None)", + "KIN(bound=None)", "DRUG(bound=1) % PROT(kin=None, drug=1, phospho='u')", "KIN(bound=1) % PROT(kin=1, drug=None, phospho='u')", - "PROT(kin=None, drug=None, phospho='p')" + "PROT(kin=None, drug=None, phospho='p')", ), } # Names and IDs are the same here - expected['ExpressionNames'] = expected['ExpressionIds'] - expected['ParameterNames'] = expected['ParameterIds'] + expected["ExpressionNames"] = expected["ExpressionIds"] + expected["ParameterNames"] = expected["ParameterIds"] for field_name, cur_expected in expected.items(): - actual = getattr(model_pysb, f'get{field_name}')() + actual = getattr(model_pysb, f"get{field_name}")() assert actual == cur_expected @@ -273,62 +318,59 @@ def test_heavyside_and_special_symbols(): pysb.SelfExporter.cleanup() # reset pysb pysb.SelfExporter.do_export = True - model = pysb.Model('piecewise_test') - a = pysb.Monomer('A') - pysb.Initial(a(), pysb.Parameter('a0')) + model = pysb.Model("piecewise_test") + a = pysb.Monomer("A") + pysb.Initial(a(), pysb.Parameter("a0")) pysb.Rule( - 'deg', + "deg", a() >> None, pysb.Expression( - 'rate', - sp.Piecewise((1, pysb.Observable('a', a()) < 1), - (0.0, True)) - ) + "rate", sp.Piecewise((1, pysb.Observable("a", a()) < 1), (0.0, True)) + ), ) with TemporaryDirectoryWinSafe(prefix=model.name) as outdir: - pysb2amici(model, outdir, verbose=True, observables=['a']) + pysb2amici(model, outdir, verbose=True, observables=["a"]) - model_module = amici.import_model_module(module_name=model.name, - module_path=outdir) + model_module = amici.import_model_module( + module_name=model.name, module_path=outdir + ) amici_model = model_module.getModel() assert amici_model.ne @skip_on_valgrind def test_energy(): - model_pysb = pysb.Model('energy') - pysb.Monomer('A', ['a', 'b']) - pysb.Monomer('B', ['a']) - pysb.Parameter('RT', 2) - pysb.Parameter('A_0', 10) - pysb.Parameter('AB_0', 10) - pysb.Parameter('phi', 0.5) - pysb.Expression('E_AAB_RT', -5 / RT) - pysb.Expression('E0_AA_RT', -1 / RT) + model_pysb = pysb.Model("energy") + pysb.Monomer("A", ["a", "b"]) + pysb.Monomer("B", ["a"]) + pysb.Parameter("RT", 2) + pysb.Parameter("A_0", 10) + pysb.Parameter("AB_0", 10) + pysb.Parameter("phi", 0.5) + pysb.Expression("E_AAB_RT", -5 / RT) + pysb.Expression("E0_AA_RT", -1 / RT) pysb.Rule( - 'A_dimerize', + "A_dimerize", A(a=None) + A(a=None) | A(a=1) % A(a=1), phi, E0_AA_RT, energy=True, ) - pysb.EnergyPattern('epAAB', A(a=1) % A(a=1, b=2) % B(a=2), E_AAB_RT) + pysb.EnergyPattern("epAAB", A(a=1) % A(a=1, b=2) % B(a=2), E_AAB_RT) pysb.Initial(A(a=None, b=None), A_0) pysb.Initial(A(a=None, b=1) % B(a=1), AB_0) with TemporaryDirectoryWinSafe(prefix=model_pysb.name) as outdir: pysb2amici(model_pysb, output_dir=outdir) - model_module = amici.import_model_module(module_name=model_pysb.name, - module_path=outdir) + model_module = amici.import_model_module( + module_name=model_pysb.name, module_path=outdir + ) amici_model = model_module.getModel() amici_model.setTimepoints(np.logspace(-4, 5, 10)) solver = amici_model.getSolver() solver.setRelativeTolerance(1e-14) solver.setAbsoluteTolerance(1e-14) - check_derivatives(amici_model, solver, - epsilon=1e-4, - rtol=1e-2, - atol=1e-2) + check_derivatives(amici_model, solver, epsilon=1e-4, rtol=1e-2, atol=1e-2) diff --git a/python/tests/test_rdata.py b/python/tests/test_rdata.py index 0e6847e689..cbfc6dc7a9 100644 --- a/python/tests/test_rdata.py +++ b/python/tests/test_rdata.py @@ -1,11 +1,11 @@ """Test amici.ReturnData(View)-related functionality""" +import amici import numpy as np import pytest - -import amici from numpy.testing import assert_array_equal -@pytest.fixture(scope='session') + +@pytest.fixture(scope="session") def rdata_by_id_fixture(sbml_example_presimulation_module): model_module = sbml_example_presimulation_module model = model_module.getModel() @@ -21,35 +21,19 @@ def rdata_by_id_fixture(sbml_example_presimulation_module): def test_rdata_by_id(rdata_by_id_fixture): model, rdata = rdata_by_id_fixture - assert_array_equal( - rdata.by_id(model.getStateIds()[1]), - rdata.x[:, 1] - ) - assert_array_equal( - rdata.by_id(model.getStateIds()[1], 'x'), - rdata.x[:, 1] - ) - assert_array_equal( - rdata.by_id(model.getStateIds()[1], 'x', model), - rdata.x[:, 1] - ) - + assert_array_equal(rdata.by_id(model.getStateIds()[1]), rdata.x[:, 1]) + assert_array_equal(rdata.by_id(model.getStateIds()[1], "x"), rdata.x[:, 1]) + assert_array_equal(rdata.by_id(model.getStateIds()[1], "x", model), rdata.x[:, 1]) assert_array_equal( - rdata.by_id(model.getObservableIds()[0], 'y', model), - rdata.y[:, 0] + rdata.by_id(model.getObservableIds()[0], "y", model), rdata.y[:, 0] ) + assert_array_equal(rdata.by_id(model.getExpressionIds()[1]), rdata.w[:, 1]) assert_array_equal( - rdata.by_id(model.getExpressionIds()[1]), - rdata.w[:, 1] - ) - assert_array_equal( - rdata.by_id(model.getExpressionIds()[1], 'w', model), - rdata.w[:, 1] + rdata.by_id(model.getExpressionIds()[1], "w", model), rdata.w[:, 1] ) assert_array_equal( - rdata.by_id(model.getStateIds()[1], 'sx', model), - rdata.sx[:, :, 1] + rdata.by_id(model.getStateIds()[1], "sx", model), rdata.sx[:, :, 1] ) diff --git a/python/tests/test_sbml_import.py b/python/tests/test_sbml_import.py index 813690170d..d0ce9cae5c 100644 --- a/python/tests/test_sbml_import.py +++ b/python/tests/test_sbml_import.py @@ -5,20 +5,20 @@ from pathlib import Path from urllib.request import urlopen +import amici import libsbml import numpy as np import pytest -from numpy.testing import assert_allclose, assert_array_equal - -import amici from amici.gradient_check import check_derivatives from amici.sbml_import import SbmlImporter -from amici.testing import TemporaryDirectoryWinSafe as TemporaryDirectory, \ - skip_on_valgrind +from amici.testing import TemporaryDirectoryWinSafe as TemporaryDirectory +from amici.testing import skip_on_valgrind +from numpy.testing import assert_allclose, assert_array_equal -EXAMPLES_DIR = Path(__file__).parent / '..' / 'examples' -STEADYSTATE_MODEL_FILE = (EXAMPLES_DIR / 'example_steadystate' - / 'model_steadystate_scaled.xml') +EXAMPLES_DIR = Path(__file__).parent / ".." / "examples" +STEADYSTATE_MODEL_FILE = ( + EXAMPLES_DIR / "example_steadystate" / "model_steadystate_scaled.xml" +) @pytest.fixture @@ -28,16 +28,16 @@ def simple_sbml_model(): model = document.createModel() model.setTimeUnits("second") model.setExtentUnits("mole") - model.setSubstanceUnits('mole') + model.setSubstanceUnits("mole") c1 = model.createCompartment() - c1.setId('C1') + c1.setId("C1") model.addCompartment(c1) s1 = model.createSpecies() - s1.setId('S1') - s1.setCompartment('C1') + s1.setId("S1") + s1.setCompartment("C1") model.addSpecies(s1) p1 = model.createParameter() - p1.setId('p1') + p1.setId("p1") p1.setValue(0.0) model.addParameter(p1) @@ -47,34 +47,33 @@ def simple_sbml_model(): def test_sbml2amici_no_observables(simple_sbml_model): """Test model generation works for model without observables""" sbml_doc, sbml_model = simple_sbml_model - sbml_importer = SbmlImporter(sbml_source=sbml_model, - from_file=False) + sbml_importer = SbmlImporter(sbml_source=sbml_model, from_file=False) model_name = "test_sbml2amici_no_observables" with TemporaryDirectory() as tmpdir: - sbml_importer.sbml2amici(model_name=model_name, - output_dir=tmpdir, - observables=None, - compute_conservation_laws=False) + sbml_importer.sbml2amici( + model_name=model_name, + output_dir=tmpdir, + observables=None, + compute_conservation_laws=False, + ) # Ensure import succeeds (no missing symbols) module_module = amici.import_model_module(model_name, tmpdir) - assert hasattr(module_module, 'getModel') + assert hasattr(module_module, "getModel") @skip_on_valgrind def test_sbml2amici_nested_observables_fail(simple_sbml_model): """Test model generation works for model without observables""" sbml_doc, sbml_model = simple_sbml_model - sbml_importer = SbmlImporter(sbml_source=sbml_model, - from_file=False) + sbml_importer = SbmlImporter(sbml_source=sbml_model, from_file=False) model_name = "test_sbml2amici_nested_observables_fail" with TemporaryDirectory() as tmpdir: with pytest.raises(ValueError, match="(?i)nested"): sbml_importer.sbml2amici( model_name=model_name, output_dir=tmpdir, - observables={'outer': {'formula': 'inner'}, - 'inner': {'formula': 'S1'}}, + observables={"outer": {"formula": "inner"}, "inner": {"formula": "S1"}}, compute_conservation_laws=False, generate_sensitivity_code=False, compile=False, @@ -83,18 +82,20 @@ def test_sbml2amici_nested_observables_fail(simple_sbml_model): def test_nosensi(simple_sbml_model): sbml_doc, sbml_model = simple_sbml_model - sbml_importer = SbmlImporter(sbml_source=sbml_model, - from_file=False) + sbml_importer = SbmlImporter(sbml_source=sbml_model, from_file=False) model_name = "test_nosensi" with TemporaryDirectory() as tmpdir: - sbml_importer.sbml2amici(model_name=model_name, - output_dir=tmpdir, - observables=None, - compute_conservation_laws=False, - generate_sensitivity_code=False) + sbml_importer.sbml2amici( + model_name=model_name, + output_dir=tmpdir, + observables=None, + compute_conservation_laws=False, + generate_sensitivity_code=False, + ) - model_module = amici.import_model_module(module_name=model_name, - module_path=tmpdir) + model_module = amici.import_model_module( + module_name=model_name, module_path=tmpdir + ) model = model_module.getModel() model.setTimepoints(np.linspace(0, 60, 61)) @@ -115,24 +116,26 @@ def observable_dependent_error_model(simple_sbml_model): rr.setVariable("S1") rr.setMath(libsbml.parseL3Formula("p1")) relative_sigma = sbml_model.createParameter() - relative_sigma.setId('relative_sigma') + relative_sigma.setId("relative_sigma") relative_sigma.setValue(0.05) - sbml_importer = SbmlImporter(sbml_source=sbml_model, - from_file=False) + sbml_importer = SbmlImporter(sbml_source=sbml_model, from_file=False) model_name = "observable_dependent_error_model" with TemporaryDirectory() as tmpdir: sbml_importer.sbml2amici( model_name=model_name, output_dir=tmpdir, - observables={'observable_s1': {'formula': 'S1'}, - 'observable_s1_scaled': {'formula': '0.5 * S1'}}, - sigmas={'observable_s1': '0.1 + relative_sigma * observable_s1', - 'observable_s1_scaled': '0.02 * observable_s1_scaled'}, + observables={ + "observable_s1": {"formula": "S1"}, + "observable_s1_scaled": {"formula": "0.5 * S1"}, + }, + sigmas={ + "observable_s1": "0.1 + relative_sigma * observable_s1", + "observable_s1_scaled": "0.02 * observable_s1_scaled", + }, ) - yield amici.import_model_module(module_name=model_name, - module_path=tmpdir) + yield amici.import_model_module(module_name=model_name, module_path=tmpdir) @skip_on_valgrind @@ -145,10 +148,10 @@ def test_sbml2amici_observable_dependent_error(observable_dependent_error_model) # generate artificial data rdata = amici.runAmiciSimulation(model, solver) - assert_allclose(rdata.sigmay[:, 0], 0.1 + 0.05 * rdata.y[:, 0], - rtol=1.e-5, atol=1.e-8) - assert_allclose(rdata.sigmay[:, 1], 0.02 * rdata.y[:, 1], - rtol=1.e-5, atol=1.e-8) + assert_allclose( + rdata.sigmay[:, 0], 0.1 + 0.05 * rdata.y[:, 0], rtol=1.0e-5, atol=1.0e-8 + ) + assert_allclose(rdata.sigmay[:, 1], 0.02 * rdata.y[:, 1], rtol=1.0e-5, atol=1.0e-8) edata = amici.ExpData(rdata, 1.0, 0.0) edata.setObservedDataStdDev(np.nan) @@ -179,50 +182,48 @@ def test_logging_works(observable_dependent_error_model, caplog): assert rdata.status != amici.AMICI_SUCCESS assert "mxstep steps taken" in caplog.text + @skip_on_valgrind def test_model_module_is_set(observable_dependent_error_model): model_module = observable_dependent_error_model assert isinstance(model_module.getModel().module, amici.ModelModule) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def model_steadystate_module(): sbml_file = STEADYSTATE_MODEL_FILE sbml_importer = amici.SbmlImporter(sbml_file) observables = amici.assignmentRules2observables( sbml_importer.sbml, - filter_function=lambda variable: - variable.getId().startswith('observable_') and - not variable.getId().endswith('_sigma') + filter_function=lambda variable: variable.getId().startswith("observable_") + and not variable.getId().endswith("_sigma"), ) - module_name = 'test_model_steadystate_scaled' + module_name = "test_model_steadystate_scaled" with TemporaryDirectory(prefix=module_name) as outdir: sbml_importer.sbml2amici( model_name=module_name, output_dir=outdir, observables=observables, - constant_parameters=['k0'], - sigmas={'observable_x1withsigma': 'observable_x1withsigma_sigma'}) + constant_parameters=["k0"], + sigmas={"observable_x1withsigma": "observable_x1withsigma_sigma"}, + ) - yield amici.import_model_module(module_name=module_name, - module_path=outdir) + yield amici.import_model_module(module_name=module_name, module_path=outdir) -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def model_units_module(): - sbml_file = EXAMPLES_DIR / 'example_units' / 'model_units.xml' - module_name = 'test_model_units' + sbml_file = EXAMPLES_DIR / "example_units" / "model_units.xml" + module_name = "test_model_units" sbml_importer = amici.SbmlImporter(sbml_file) with TemporaryDirectory() as outdir: - sbml_importer.sbml2amici(model_name=module_name, - output_dir=outdir) + sbml_importer.sbml2amici(model_name=module_name, output_dir=outdir) - yield amici.import_model_module(module_name=module_name, - module_path=outdir) + yield amici.import_model_module(module_name=module_name, module_path=outdir) def test_presimulation(sbml_example_presimulation_module): @@ -242,8 +243,8 @@ def test_presimulation(sbml_example_presimulation_module): edata.fixedParametersPresimulation = [10, 2] edata.fixedParametersPreequilibration = [3, 0] assert isinstance( - amici.runAmiciSimulation(model, solver, edata), - amici.ReturnDataView) + amici.runAmiciSimulation(model, solver, edata), amici.ReturnDataView + ) solver.setRelativeTolerance(1e-12) solver.setAbsoluteTolerance(1e-12) @@ -268,51 +269,61 @@ def test_steadystate_simulation(model_steadystate_module): edata_reconstructed = amici.getEdataFromDataFrame(model, df_edata) assert_allclose( - amici.ExpDataView(edata[0])['observedData'], - amici.ExpDataView(edata_reconstructed[0])['observedData'], - rtol=1.e-5, atol=1.e-8 + amici.ExpDataView(edata[0])["observedData"], + amici.ExpDataView(edata_reconstructed[0])["observedData"], + rtol=1.0e-5, + atol=1.0e-8, ) assert_allclose( - amici.ExpDataView(edata[0])['observedDataStdDev'], - amici.ExpDataView(edata_reconstructed[0])['observedDataStdDev'], - rtol=1.e-5, atol=1.e-8 + amici.ExpDataView(edata[0])["observedDataStdDev"], + amici.ExpDataView(edata_reconstructed[0])["observedDataStdDev"], + rtol=1.0e-5, + atol=1.0e-8, ) if len(edata[0].fixedParameters): - assert list(edata[0].fixedParameters) \ - == list(edata_reconstructed[0].fixedParameters) + assert list(edata[0].fixedParameters) == list( + edata_reconstructed[0].fixedParameters + ) else: - assert list(model.getFixedParameters()) \ - == list(edata_reconstructed[0].fixedParameters) + assert list(model.getFixedParameters()) == list( + edata_reconstructed[0].fixedParameters + ) - assert list(edata[0].fixedParametersPreequilibration) == \ - list(edata_reconstructed[0].fixedParametersPreequilibration) + assert list(edata[0].fixedParametersPreequilibration) == list( + edata_reconstructed[0].fixedParametersPreequilibration + ) df_state = amici.getSimulationStatesAsDataFrame(model, edata, rdata) assert_allclose( - rdata[0]['x'], df_state[list(model.getStateIds())].values, - rtol=1.e-5, atol=1.e-8 + rdata[0]["x"], + df_state[list(model.getStateIds())].values, + rtol=1.0e-5, + atol=1.0e-8, ) df_obs = amici.getSimulationObservablesAsDataFrame(model, edata, rdata) assert_allclose( - rdata[0]['y'], df_obs[list(model.getObservableIds())].values, - rtol=1.e-5, atol=1.e-8 + rdata[0]["y"], + df_obs[list(model.getObservableIds())].values, + rtol=1.0e-5, + atol=1.0e-8, ) amici.getResidualsAsDataFrame(model, edata, rdata) df_expr = amici.pandas.get_expressions_as_dataframe(model, edata, rdata) assert_allclose( - rdata[0]['w'], df_expr[list(model.getExpressionIds())].values, - rtol=1.e-5, atol=1.e-8 + rdata[0]["w"], + df_expr[list(model.getExpressionIds())].values, + rtol=1.0e-5, + atol=1.0e-8, ) solver.setRelativeTolerance(1e-12) solver.setAbsoluteTolerance(1e-12) - check_derivatives(model, solver, edata[0], atol=1e-3, - rtol=1e-3, epsilon=1e-4) + check_derivatives(model, solver, edata[0], atol=1e-3, rtol=1e-3, epsilon=1e-4) # Run some additional tests which need a working Model, # but don't need precomputed expectations. @@ -328,8 +339,8 @@ def test_solver_reuse(model_steadystate_module): edata = amici.ExpData(rdata, 1, 0) for sensi_method in ( - amici.SensitivityMethod.forward, - amici.SensitivityMethod.adjoint, + amici.SensitivityMethod.forward, + amici.SensitivityMethod.adjoint, ): solver.setSensitivityMethod(sensi_method) rdata1 = amici.runAmiciSimulation(model, solver, edata) @@ -338,13 +349,15 @@ def test_solver_reuse(model_steadystate_module): assert rdata1.status == amici.AMICI_SUCCESS for attr in rdata1: - if 'time' in attr: + if "time" in attr: continue val1 = getattr(rdata1, attr) val2 = getattr(rdata2, attr) - msg = f"Values for {attr} do not match for sensitivity "\ - f"method {sensi_method}" + msg = ( + f"Values for {attr} do not match for sensitivity " + f"method {sensi_method}" + ) if isinstance(val1, np.ndarray): assert_array_equal(val1, val2, err_msg=msg) elif isinstance(val1, Number) and np.isnan(val1): @@ -353,7 +366,6 @@ def test_solver_reuse(model_steadystate_module): assert val1 == val2, msg - @pytest.fixture def model_test_likelihoods(): """Test model for various likelihood functions.""" @@ -363,35 +375,38 @@ def model_test_likelihoods(): # define observables observables = { - 'o1': {'formula': 'x1'}, - 'o2': {'formula': '10^x1'}, - 'o3': {'formula': '10^x1'}, - 'o4': {'formula': 'x1'}, - 'o5': {'formula': '10^x1'}, - 'o6': {'formula': '10^x1'}, - 'o7': {'formula': 'x1'} + "o1": {"formula": "x1"}, + "o2": {"formula": "10^x1"}, + "o3": {"formula": "10^x1"}, + "o4": {"formula": "x1"}, + "o5": {"formula": "10^x1"}, + "o6": {"formula": "10^x1"}, + "o7": {"formula": "x1"}, } # define different noise models noise_distributions = { - 'o1': 'normal', 'o2': 'log-normal', 'o3': 'log10-normal', - 'o4': 'laplace', 'o5': 'log-laplace', 'o6': 'log10-laplace', - 'o7': lambda str_symbol: f'Abs({str_symbol} - m{str_symbol}) ' - f'/ sigma{str_symbol}', + "o1": "normal", + "o2": "log-normal", + "o3": "log10-normal", + "o4": "laplace", + "o5": "log-laplace", + "o6": "log10-laplace", + "o7": lambda str_symbol: f"Abs({str_symbol} - m{str_symbol}) " + f"/ sigma{str_symbol}", } - module_name = 'model_test_likelihoods' + module_name = "model_test_likelihoods" with TemporaryDirectory(prefix=module_name) as outdir: sbml_importer.sbml2amici( model_name=module_name, output_dir=outdir, observables=observables, - constant_parameters=['k0'], + constant_parameters=["k0"], noise_distributions=noise_distributions, ) - yield amici.import_model_module(module_name=module_name, - module_path=outdir) + yield amici.import_model_module(module_name=module_name, module_path=outdir) @skip_on_valgrind @@ -405,7 +420,7 @@ def test_likelihoods(model_test_likelihoods): # run model once to create an edata rdata = amici.runAmiciSimulation(model, solver) - sigmas = rdata['y'].max(axis=0) * 0.05 + sigmas = rdata["y"].max(axis=0) * 0.05 edata = amici.ExpData(rdata, sigmas, []) # just make all observables positive since some are logarithmic while min(edata.getObservedData()) < 0: @@ -415,38 +430,47 @@ def test_likelihoods(model_test_likelihoods): rdata = amici.runAmiciSimulations(model, solver, [edata])[0] # check if the values make overall sense - assert np.isfinite(rdata['llh']) - assert np.all(np.isfinite(rdata['sllh'])) - assert np.any(rdata['sllh']) + assert np.isfinite(rdata["llh"]) + assert np.all(np.isfinite(rdata["sllh"])) + assert np.any(rdata["sllh"]) rdata_df = amici.getSimulationObservablesAsDataFrame( - model, edata, rdata, by_id=True) - edata_df = amici.getDataObservablesAsDataFrame( - model, edata, by_id=True) + model, edata, rdata, by_id=True + ) + edata_df = amici.getDataObservablesAsDataFrame(model, edata, by_id=True) # check correct likelihood value - llh_exp = - sum([ - normal_nllh(edata_df['o1'], rdata_df['o1'], sigmas[0]), - log_normal_nllh(edata_df['o2'], rdata_df['o2'], sigmas[1]), - log10_normal_nllh(edata_df['o3'], rdata_df['o3'], sigmas[2]), - laplace_nllh(edata_df['o4'], rdata_df['o4'], sigmas[3]), - log_laplace_nllh(edata_df['o5'], rdata_df['o5'], sigmas[4]), - log10_laplace_nllh(edata_df['o6'], rdata_df['o6'], sigmas[5]), - custom_nllh(edata_df['o7'], rdata_df['o7'], sigmas[6]), - ]) - assert np.isclose(rdata['llh'], llh_exp) + llh_exp = -sum( + [ + normal_nllh(edata_df["o1"], rdata_df["o1"], sigmas[0]), + log_normal_nllh(edata_df["o2"], rdata_df["o2"], sigmas[1]), + log10_normal_nllh(edata_df["o3"], rdata_df["o3"], sigmas[2]), + laplace_nllh(edata_df["o4"], rdata_df["o4"], sigmas[3]), + log_laplace_nllh(edata_df["o5"], rdata_df["o5"], sigmas[4]), + log10_laplace_nllh(edata_df["o6"], rdata_df["o6"], sigmas[5]), + custom_nllh(edata_df["o7"], rdata_df["o7"], sigmas[6]), + ] + ) + assert np.isclose(rdata["llh"], llh_exp) # check gradient - for sensi_method in [amici.SensitivityMethod.forward, - amici.SensitivityMethod.adjoint]: + for sensi_method in [ + amici.SensitivityMethod.forward, + amici.SensitivityMethod.adjoint, + ]: solver = model.getSolver() solver.setSensitivityMethod(sensi_method) solver.setSensitivityOrder(amici.SensitivityOrder.first) solver.setRelativeTolerance(1e-12) solver.setAbsoluteTolerance(1e-12) check_derivatives( - model, solver, edata, atol=1e-2, rtol=1e-2, - epsilon=1e-5, check_least_squares=False + model, + solver, + edata, + atol=1e-2, + rtol=1e-2, + epsilon=1e-5, + check_least_squares=False, ) @@ -457,19 +481,19 @@ def test_likelihoods_error(): sbml_importer = amici.SbmlImporter(sbml_file) # define observables - observables = {'o1': {'formula': 'x1'}} + observables = {"o1": {"formula": "x1"}} # define different noise models - noise_distributions = {'o1': 'nörmal'} + noise_distributions = {"o1": "nörmal"} - module_name = 'test_likelihoods_error' - outdir = 'test_likelihoods_error' + module_name = "test_likelihoods_error" + outdir = "test_likelihoods_error" with pytest.raises(ValueError): sbml_importer.sbml2amici( model_name=module_name, output_dir=outdir, observables=observables, - constant_parameters=['k0'], + constant_parameters=["k0"], noise_distributions=noise_distributions, ) @@ -484,40 +508,46 @@ def test_units(model_units_module): solver = model.getSolver() rdata = amici.runAmiciSimulation(model, solver) - assert rdata['status'] == amici.AMICI_SUCCESS + assert rdata["status"] == amici.AMICI_SUCCESS @skip_on_valgrind -@pytest.mark.skipif(os.name == 'nt', - reason='Avoid `CERTIFICATE_VERIFY_FAILED` error') +@pytest.mark.skipif(os.name == "nt", reason="Avoid `CERTIFICATE_VERIFY_FAILED` error") def test_sympy_exp_monkeypatch(): """ This model contains a removeable discontinuity at t=0 that requires monkeypatching sympy.Pow._eval_derivative in order to be able to compute non-nan sensitivities """ - url = 'https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000529.2?' \ - 'filename=BIOMD0000000529_url.xml' + url = ( + "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000529.2?" + "filename=BIOMD0000000529_url.xml" + ) importer = amici.SbmlImporter( - urlopen(url, timeout=20).read().decode('utf-8'), from_file=False + urlopen(url, timeout=20).read().decode("utf-8"), from_file=False ) - module_name = 'BIOMD0000000529' + module_name = "BIOMD0000000529" with TemporaryDirectory() as outdir: importer.sbml2amici(module_name, outdir) - model_module = amici.import_model_module(module_name=module_name, - module_path=outdir) + model_module = amici.import_model_module( + module_name=module_name, module_path=outdir + ) model = model_module.getModel() model.setTimepoints(np.linspace(0, 8, 250)) model.requireSensitivitiesForAllParameters() model.setAlwaysCheckFinite(True) - model.setParameterScale(amici.parameterScalingFromIntVector([ - amici.ParameterScaling.none - if re.match(r'n[0-9]+$', par_id) - else amici.ParameterScaling.log10 - for par_id in model.getParameterIds() - ])) + model.setParameterScale( + amici.parameterScalingFromIntVector( + [ + amici.ParameterScaling.none + if re.match(r"n[0-9]+$", par_id) + else amici.ParameterScaling.log10 + for par_id in model.getParameterIds() + ] + ) + ) solver = model.getSolver() solver.setSensitivityMethod(amici.SensitivityMethod.forward) @@ -526,40 +556,50 @@ def test_sympy_exp_monkeypatch(): rdata = amici.runAmiciSimulation(model, solver) # print sensitivity-related results - assert rdata['status'] == amici.AMICI_SUCCESS - check_derivatives(model, solver, None, atol=1e-2, rtol=1e-2, - epsilon=1e-3) + assert rdata["status"] == amici.AMICI_SUCCESS + check_derivatives(model, solver, None, atol=1e-2, rtol=1e-2, epsilon=1e-3) def normal_nllh(m, y, sigma): - return sum(.5*(np.log(2*np.pi*sigma**2) + ((y-m)/sigma)**2)) + return sum(0.5 * (np.log(2 * np.pi * sigma**2) + ((y - m) / sigma) ** 2)) def log_normal_nllh(m, y, sigma): - return sum(.5*(np.log(2*np.pi*sigma**2*m**2) - + ((np.log(y)-np.log(m))/sigma)**2)) + return sum( + 0.5 + * ( + np.log(2 * np.pi * sigma**2 * m**2) + + ((np.log(y) - np.log(m)) / sigma) ** 2 + ) + ) def log10_normal_nllh(m, y, sigma): - return sum(.5*(np.log(2*np.pi*sigma**2*m**2*np.log(10)**2) - + ((np.log10(y) - np.log10(m))/sigma)**2)) + return sum( + 0.5 + * ( + np.log(2 * np.pi * sigma**2 * m**2 * np.log(10) ** 2) + + ((np.log10(y) - np.log10(m)) / sigma) ** 2 + ) + ) def laplace_nllh(m, y, sigma): - return sum(np.log(2*sigma) + np.abs(y-m)/sigma) + return sum(np.log(2 * sigma) + np.abs(y - m) / sigma) def log_laplace_nllh(m, y, sigma): - return sum(np.log(2*sigma*m) + np.abs(np.log(y)-np.log(m))/sigma) + return sum(np.log(2 * sigma * m) + np.abs(np.log(y) - np.log(m)) / sigma) def log10_laplace_nllh(m, y, sigma): - return sum(np.log(2*sigma*m*np.log(10)) - + np.abs(np.log10(y)-np.log10(m))/sigma) + return sum( + np.log(2 * sigma * m * np.log(10)) + np.abs(np.log10(y) - np.log10(m)) / sigma + ) def custom_nllh(m, y, sigma): - return sum(np.abs(m-y)/sigma) + return sum(np.abs(m - y) / sigma) def _test_set_parameters_by_dict(model_module): @@ -600,9 +640,9 @@ def test_code_gen_uses_cse(extract_cse): model_name=model_name, compile=False, generate_sensitivity_code=False, - output_dir = tmpdir + output_dir=tmpdir, ) - xdot = Path(tmpdir, 'xdot.cpp').read_text() + xdot = Path(tmpdir, "xdot.cpp").read_text() assert ("__amici_cse_0 = " in xdot) == extract_cse finally: os.environ = old_environ @@ -618,7 +658,7 @@ def test_code_gen_uses_lhs_symbol_ids(): model_name=model_name, compile=False, generate_sensitivity_code=False, - output_dir=tmpdir + output_dir=tmpdir, ) - dwdx = Path(tmpdir, 'dwdx.cpp').read_text() + dwdx = Path(tmpdir, "dwdx.cpp").read_text() assert "dobservable_x1_dx1 = " in dwdx diff --git a/python/tests/test_sbml_import_special_functions.py b/python/tests/test_sbml_import_special_functions.py index 6615828992..9bcdb66bae 100644 --- a/python/tests/test_sbml_import_special_functions.py +++ b/python/tests/test_sbml_import_special_functions.py @@ -6,47 +6,50 @@ import os +import amici import numpy as np import pytest -from scipy.special import loggamma - -import amici from amici.gradient_check import check_derivatives from amici.testing import TemporaryDirectoryWinSafe, skip_on_valgrind +from scipy.special import loggamma @pytest.fixture(scope="session") def model_special_likelihoods(): """Test model for special likelihood functions.""" # load sbml model - sbml_file = os.path.join(os.path.dirname(__file__), '..', - 'examples', 'example_steadystate', - 'model_steadystate_scaled.xml') + sbml_file = os.path.join( + os.path.dirname(__file__), + "..", + "examples", + "example_steadystate", + "model_steadystate_scaled.xml", + ) sbml_importer = amici.SbmlImporter(sbml_file) # define observables observables = { - 'o1': {'formula': '100*10^x1'}, - 'o2': {'formula': '100*10^x1'}, + "o1": {"formula": "100*10^x1"}, + "o2": {"formula": "100*10^x1"}, } # define different noise models noise_distributions = { - 'o1': 'binomial', 'o2': 'negative-binomial', + "o1": "binomial", + "o2": "negative-binomial", } - module_name = 'test_special_likelihoods' + module_name = "test_special_likelihoods" with TemporaryDirectoryWinSafe(prefix=module_name) as outdir: sbml_importer.sbml2amici( model_name=module_name, output_dir=outdir, observables=observables, - constant_parameters=['k0'], + constant_parameters=["k0"], noise_distributions=noise_distributions, ) - yield amici.import_model_module(module_name=module_name, - module_path=outdir) + yield amici.import_model_module(module_name=module_name, module_path=outdir) @skip_on_valgrind @@ -79,31 +82,35 @@ def test_special_likelihoods(model_special_likelihoods): rdata = amici.runAmiciSimulations(model, solver, [edata])[0] # check if the values make overall sense - assert np.isfinite(rdata['llh']) - assert np.all(np.isfinite(rdata['sllh'])) - assert np.any(rdata['sllh']) + assert np.isfinite(rdata["llh"]) + assert np.all(np.isfinite(rdata["sllh"])) + assert np.any(rdata["sllh"]) rdata_df = amici.getSimulationObservablesAsDataFrame( - model, edata, rdata, by_id=True) - edata_df = amici.getDataObservablesAsDataFrame( - model, edata, by_id=True) + model, edata, rdata, by_id=True + ) + edata_df = amici.getDataObservablesAsDataFrame(model, edata, by_id=True) # check correct likelihood value - llh_exp = - sum([ - binomial_nllh(edata_df['o1'], rdata_df['o1'], sigma), - negative_binomial_nllh(edata_df['o2'], rdata_df['o2'], sigma), - ]) - assert np.isclose(rdata['llh'], llh_exp) + llh_exp = -sum( + [ + binomial_nllh(edata_df["o1"], rdata_df["o1"], sigma), + negative_binomial_nllh(edata_df["o2"], rdata_df["o2"], sigma), + ] + ) + assert np.isclose(rdata["llh"], llh_exp) # check gradient - for sensi_method in [amici.SensitivityMethod.forward, - amici.SensitivityMethod.adjoint]: + for sensi_method in [ + amici.SensitivityMethod.forward, + amici.SensitivityMethod.adjoint, + ]: solver = model.getSolver() solver.setSensitivityMethod(sensi_method) solver.setSensitivityOrder(amici.SensitivityOrder.first) check_derivatives( - model, solver, edata, atol=1e-4, rtol=1e-3, - check_least_squares=False) + model, solver, edata, atol=1e-4, rtol=1e-3, check_least_squares=False + ) # Test for m > y, i.e. in region with 0 density @@ -120,19 +127,29 @@ def test_special_likelihoods(model_special_likelihoods): rdata = amici.runAmiciSimulations(model, solver, [edata])[0] # m > y -> outside binomial domain -> 0 density - assert rdata['llh'] == -np.inf + assert rdata["llh"] == -np.inf # check for non-informative gradient - assert all(np.isnan(rdata['sllh'])) + assert all(np.isnan(rdata["sllh"])) def binomial_nllh(m: np.ndarray, y: np.ndarray, p: float): if any(m > y): return np.inf - return sum(- loggamma(y+1) + loggamma(m+1) + loggamma(y-m+1) \ - - m * np.log(p) - (y-m) * np.log(1-p)) + return sum( + -loggamma(y + 1) + + loggamma(m + 1) + + loggamma(y - m + 1) + - m * np.log(p) + - (y - m) * np.log(1 - p) + ) def negative_binomial_nllh(m: np.ndarray, y: np.ndarray, p: float): - r = y * (1-p) / p - return sum(- loggamma(m+r) + loggamma(m+1) + loggamma(r) - - r * np.log(1-p) - m * np.log(p)) + r = y * (1 - p) / p + return sum( + -loggamma(m + r) + + loggamma(m + 1) + + loggamma(r) + - r * np.log(1 - p) + - m * np.log(p) + ) diff --git a/python/tests/test_splines.py b/python/tests/test_splines.py new file mode 100644 index 0000000000..2385631ab5 --- /dev/null +++ b/python/tests/test_splines.py @@ -0,0 +1,105 @@ +""" +Test AMICI's C++ spline implementation by comparing +the results of simulations of simple SBML models +containing simple splines with a symbolically-computed +ground truth. NB: The test in this file takes a long +time to complete. +""" + +import os + +import numpy as np +from amici.testing import skip_on_valgrind +from splines_utils import ( + check_splines_full, + example_spline_1, + example_spline_2, + example_spline_3, +) + + +@skip_on_valgrind +def test_multiple_splines(**kwargs): + """ + Test a SBML model containing multiple splines. + """ + spline0, params0, tols0 = example_spline_1( + 0, num_nodes=9, fixed_values=[0, 2], extrapolate="linear" + ) + spline1, params1, tols1 = example_spline_1( + 1, num_nodes=14, scale=1.5, offset=5, extrapolate="linear" + ) + spline2, params2, tols2 = example_spline_1( + 2, num_nodes=5, scale=0.5, offset=-5, extrapolate="linear" + ) + spline3, params3, tols3 = example_spline_1( + 3, fixed_values="all", extrapolate="linear" + ) + spline4, params4, tols4 = example_spline_2(4) + spline5, params5, tols5 = example_spline_3(5) + + splines = [spline0, spline1, spline2, spline3, spline4, spline5] + + params = dict(params0) + params.update(params1) + params.update(params2) + params.update(params3) + params.update(params4) + params.update(params5) + + if isinstance(tols0, dict): + tols0 = (tols0, tols0, tols0) + if isinstance(tols1, dict): + tols1 = (tols1, tols1, tols1) + if isinstance(tols2, dict): + tols2 = (tols2, tols2, tols2) + if isinstance(tols3, dict): + tols3 = (tols3, tols3, tols3) + if isinstance(tols4, dict): + tols4 = (tols4, tols4, tols4) + if isinstance(tols5, dict): + tols5 = (tols5, tols5, tols5) + + tols = [] + for t0, t1, t2, t3, t4, t5 in zip(tols0, tols1, tols2, tols3, tols4, tols5): + keys = set().union( + t0.keys(), t1.keys(), t2.keys(), t3.keys(), t4.keys(), t5.keys() + ) + t = { + key: max( + t0.get(key, 0.0), + t1.get(key, 0.0), + t2.get(key, 0.0), + t3.get(key, 0.0), + t4.get(key, 0.0), + t5.get(key, 0.0), + ) + for key in keys + } + tols.append(t) + + tols[1]["x_rtol"] = max(1e-9, tols[1].get("x_rtol", -np.inf)) + tols[1]["x_atol"] = max(5e-9, tols[1].get("x_atol", -np.inf)) + tols[1]["sx_rtol"] = max(1e-5, tols[1].get("llh_rtol", -np.inf)) + tols[1]["sx_atol"] = max(5e-9, tols[1].get("sx_atol", -np.inf)) + tols[1]["llh_rtol"] = max(5e-14, tols[1].get("llh_rtol", -np.inf)) + tols[1]["sllh_atol"] = max(5e-5, tols[1].get("sllh_atol", -np.inf)) + + tols[2]["x_rtol"] = max(5e-10, tols[2].get("x_rtol", -np.inf)) + tols[2]["x_atol"] = max(1e-8, tols[2].get("x_atol", -np.inf)) + tols[2]["llh_rtol"] = max(5e-14, tols[2].get("llh_rtol", -np.inf)) + tols[2]["sllh_atol"] = max(5e-5, tols[2].get("sllh_atol", -np.inf)) + + if os.name == "nt": + tols[2]["sllh_atol"] = max(5e-4, tols[2]["sllh_atol"]) + + # Load precomputed results + # They be computed again by + # groundtruth = test_multiple_splines(return_groundtruth=True) + # They should be recomputed only if the splines used in the test change + precomputed_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "test_splines_precomputed.npz" + ) + kwargs["groundtruth"] = dict(np.load(precomputed_path)) + + return check_splines_full(splines, params, tols, **kwargs) diff --git a/python/tests/test_splines_precomputed.npz b/python/tests/test_splines_precomputed.npz new file mode 100644 index 0000000000..c6ee624a36 Binary files /dev/null and b/python/tests/test_splines_precomputed.npz differ diff --git a/python/tests/test_splines_python.py b/python/tests/test_splines_python.py new file mode 100644 index 0000000000..905ae8b83d --- /dev/null +++ b/python/tests/test_splines_python.py @@ -0,0 +1,331 @@ +""" +Test AMICI's Python spline implementation by +creating splines with different properties, +evaluating them and comparing them with +the true analytical values. +""" + +import math + +import amici +import sympy as sp +from amici.testing import skip_on_valgrind + + +@skip_on_valgrind +def test_SplineUniform(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + ) + assert math.isclose(float(spline.evaluate(0.0)), 0.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.74609375) + assert math.isclose(float(spline.evaluate(1.0 / 3)), 2.0) + assert math.isclose(float(spline.evaluate(0.50)), 1.3437499999999996) + assert math.isclose(float(spline.evaluate(2.0 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.484375) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + + +@skip_on_valgrind +def test_SplineNonUniform(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=[0.0, 0.1, 0.5, 1.0], + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + ) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.05)), 1.1484375) + assert math.isclose(float(spline.evaluate(0.10)), 2.0) + assert math.isclose(float(spline.evaluate(0.25)), 2.0498046875) + assert math.isclose(float(spline.evaluate(0.50)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.6015625) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + + +@skip_on_valgrind +def test_SplineExplicit(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=5), + values_at_nodes=[0.0, 2.0, 0.5, 1.0, 0.75], + derivatives_at_nodes=[1.0, 0.0, 0.1, -0.1, 0.0], + ) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.20)), 1.8000000000000003) + assert math.isclose(float(spline.evaluate(0.25)), 2.0) + assert math.isclose(float(spline.evaluate(0.40)), 1.02439999999999985) + assert math.isclose(float(spline.evaluate(0.50)), 0.5) + assert math.isclose(float(spline.evaluate(0.60)), 0.6819999999999999) + assert math.isclose(float(spline.evaluate(0.75)), 1.0) + assert math.isclose(float(spline.evaluate(0.80)), 0.9707999999999999) + assert math.isclose(float(spline.evaluate(1.00)), 0.75) + + +@skip_on_valgrind +def test_SplineZeroBC(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + bc="zeroderivative", + ) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.65234375) + assert math.isclose(float(spline.evaluate(0.50)), 1.3437499999999996) + assert math.isclose(float(spline.evaluate(0.75)), 0.5078125) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + + +@skip_on_valgrind +def test_SplineLogarithmic(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=5), + values_at_nodes=[0.2, 2.0, 0.5, 1.0, 0.75], + logarithmic_parametrization=True, + ) + assert math.isclose(float(spline.evaluate(0.00)), 0.2) + assert math.isclose(float(spline.evaluate(0.20)), 2.07939779651678) + assert math.isclose(float(spline.evaluate(0.25)), 2.0) + assert math.isclose(float(spline.evaluate(0.40)), 0.947459046694449) + assert math.isclose(float(spline.evaluate(0.50)), 0.5) + assert math.isclose(float(spline.evaluate(0.60)), 0.545987404053269) + assert math.isclose(float(spline.evaluate(0.75)), 1.0) + assert math.isclose(float(spline.evaluate(0.80)), 0.996753014029391) + assert math.isclose(float(spline.evaluate(1.00)), 0.75) + + +@skip_on_valgrind +def test_SplineUniformConstantExtrapolation(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + extrapolate="constant", + ) + assert math.isclose(float(spline.evaluate(-2.00)), 0.0) + assert math.isclose(float(spline.evaluate(-1.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.65234375) + assert math.isclose(float(spline.evaluate(1.0 / 3)), 2.0) + assert math.isclose(float(spline.evaluate(0.50)), 1.3437499999999996) + assert math.isclose(float(spline.evaluate(2.0 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.5078125) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + assert math.isclose(float(spline.evaluate(2.00)), 1.0) + assert math.isclose(float(spline.evaluate(3.00)), 1.0) + + +@skip_on_valgrind +def test_SplineUniformLinearExtrapolation(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + extrapolate="linear", + ) + assert math.isclose(float(spline.evaluate(-2.00)), -12.0) + assert math.isclose(float(spline.evaluate(-1.00)), -6.0) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.74609375) + assert math.isclose(float(spline.evaluate(1.0 / 3)), 2.0) + assert math.isclose(float(spline.evaluate(0.50)), 1.3437499999999996) + assert math.isclose(float(spline.evaluate(2.0 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.484375) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + assert math.isclose(float(spline.evaluate(2.00)), 2.5) + assert math.isclose(float(spline.evaluate(3.00)), 4.0) + + +@skip_on_valgrind +def test_SplineUniformPolynomialExtrapolation(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[0.0, 2.0, 0.5, 1.0], + extrapolate="polynomial", + ) + assert math.isclose(float(spline.evaluate(-2.00)), 429.0) + assert math.isclose(float(spline.evaluate(-1.00)), 57.0) + assert math.isclose(float(spline.evaluate(0.00)), 0.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.74609375) + assert math.isclose(float(spline.evaluate(1.0 / 3)), 2.0) + assert math.isclose(float(spline.evaluate(0.50)), 1.3437499999999996) + assert math.isclose(float(spline.evaluate(2.0 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.484375) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + assert math.isclose(float(spline.evaluate(2.00)), -33.5) + assert math.isclose(float(spline.evaluate(3.00)), -248.0) + + +@skip_on_valgrind +def test_SplineUniformPeriodicExtrapolation(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[1.0, 2.0, 0.5, 1.0], + extrapolate="periodic", + ) + assert math.isclose(float(spline.evaluate(-4 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(-0.5)), 1.2812499999999996) + assert math.isclose(float(spline.evaluate(0.00)), 1.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.9140625) + assert math.isclose(float(spline.evaluate(1 / 3)), 2.0) + assert math.isclose(float(spline.evaluate(0.50)), 1.2812499999999996) + assert math.isclose(float(spline.evaluate(2 / 3)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.47265625) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + assert math.isclose(float(spline.evaluate(1.25)), 1.9140625) + assert math.isclose(float(spline.evaluate(2.75)), 0.47265625) + + +@skip_on_valgrind +def test_SplineNonUniformPeriodicExtrapolation(): + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=[0.0, 0.1, 0.5, 1.0], + values_at_nodes=[1.0, 2.0, 0.5, 1.0], + extrapolate="periodic", + ) + assert math.isclose(float(spline.evaluate(-1.90)), 2.0) + assert math.isclose(float(spline.evaluate(-0.25)), 0.3203125) + assert math.isclose(float(spline.evaluate(0.00)), 1.0) + assert math.isclose(float(spline.evaluate(0.05)), 1.5296875) + assert math.isclose(float(spline.evaluate(0.10)), 2.0) + assert math.isclose(float(spline.evaluate(0.25)), 1.7568359375) + assert math.isclose(float(spline.evaluate(0.50)), 0.5) + assert math.isclose(float(spline.evaluate(0.75)), 0.3203125) + assert math.isclose(float(spline.evaluate(1.00)), 1.0) + assert math.isclose(float(spline.evaluate(1.50)), 0.5) + assert math.isclose(float(spline.evaluate(2.05)), 1.5296875) + + +@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): + assert math.isclose(float(value.diff(p).subs(subs)), exp, rel_tol=rel_tol) + + +@skip_on_valgrind +def test_SplineUniformSensitivity(): + params = (a, b, c) = sp.symbols("a b c") + params_values = [0.5, 1.0, 2.5] + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[3 * a + b, c**2 - 3, 1, sp.log(b) + 3 * c - 6 * a], + ) + check_gradient(spline, 0.00, params, params_values, [3.0, 1.0, 0.0]) + check_gradient( + spline, 0.25, params, params_values, [0.539062, 0.179688, 4.45312], rel_tol=1e-5 + ) + check_gradient(spline, 1.0 / 3, params, params_values, [0.0, 0.0, 5.0]) + check_gradient(spline, 0.50, params, params_values, [0.1875, -0.125, 2.625]) + check_gradient(spline, 2.0 / 3, params, params_values, [0.0, 0.0, 0.0]) + check_gradient( + spline, 0.75, params, params_values, [-1.07812, 0.179688, 0.1875], rel_tol=1e-5 + ) + check_gradient(spline, 1.00, params, params_values, [-6.0, 1.0, 3.0]) + + +@skip_on_valgrind +def test_SplineNonUniformSensitivity(): + params = (a, b, c) = sp.symbols("a b c") + params_values = [0.5, 1.0, 2.5] + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=[0.0, 0.1, 0.5, 1.0], + values_at_nodes=[3 * a + b, c**2 - 3, 1, sp.log(b) + 3 * c - 6 * a], + ) + check_gradient(spline, 0.00, params, params_values, [3.0, 1.0, 0.0]) + check_gradient( + spline, 0.05, params, params_values, [1.3125, 0.4375, 2.89062], rel_tol=1e-5 + ) + check_gradient(spline, 0.10, params, params_values, [0.0, 0.0, 5.0]) + check_gradient(spline, 0.30, params, params_values, [-0.45, -0.3, 3.6]) + check_gradient(spline, 0.50, params, params_values, [0.0, 0.0, 0.0]) + check_gradient(spline, 0.75, params, params_values, [-2.625, 0.4375, 0.921875]) + check_gradient(spline, 1.00, params, params_values, [-6.0, 1.0, 3.0]) + + +@skip_on_valgrind +def test_SplineExplicitSensitivity(): + params = (a, b, c) = sp.symbols("a b c") + params_values = [0.5, 1.0, 2.5] + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[3 * a + b, c**2 - 3, 1, sp.log(b) + 3 * c - 6 * a], + derivatives_at_nodes=[ + c**3 - 2, + sp.sqrt(b) * sp.log(b) + 3 * c, + 4 * a - sp.sin(b), + 1, + ], + ) + check_gradient(spline, 0.00, params, params_values, [3.0, 1.0, 0.0]) + check_gradient( + spline, 0.25, params, params_values, [0.46875, 0.109375, 4.37109], rel_tol=1e-6 + ) + check_gradient(spline, 1.0 / 3, params, params_values, [0.0, 0.0, 5.0]) + check_gradient( + spline, 0.50, params, params_values, [-0.166667, 0.0641793, 2.625], rel_tol=1e-5 + ) + check_gradient(spline, 2.0 / 3, params, params_values, [0.0, 0.0, 0.0]) + check_gradient( + spline, 0.75, params, params_values, [-0.75, 0.130923, 0.46875], rel_tol=1e-5 + ) + check_gradient(spline, 1.00, params, params_values, [-6.0, 1.0, 3.0]) + + +@skip_on_valgrind +def test_SplineLogarithmicSensitivity(): + params = (a, b, c) = sp.symbols("a b c") + params_values = [0.5, 1.0, 2.5] + spline = amici.splines.CubicHermiteSpline( + sbml_id="f", + evaluate_at=amici.sbml_utils.amici_time_symbol, + nodes=amici.splines.UniformGrid(0, 1, number_of_nodes=4), + values_at_nodes=[3 * a + b, c**2 - 3, 1, sp.log(b) + 3 * c - 6 * a], + logarithmic_parametrization=True, + ) + check_gradient(spline, 0.00, params, params_values, [3.0, 1.0, 0.0]) + check_gradient( + spline, 0.25, params, params_values, [0.585881, 0.195294, 4.38532], rel_tol=1e-5 + ) + check_gradient(spline, 1.0 / 3, params, params_values, [0.0, 0.0, 5.0]) + check_gradient( + spline, + 0.50, + params, + params_values, + [0.514003, -0.132395, 1.52044], + rel_tol=1e-5, + ) + check_gradient(spline, 2.0 / 3, params, params_values, [0.0, 0.0, 0.0]) + check_gradient( + spline, + 0.75, + params, + params_values, + [-0.820743, 0.13679, -0.0577988], + rel_tol=1e-5, + ) + check_gradient(spline, 1.00, params, params_values, [-6.0, 1.0, 3.0]) diff --git a/python/tests/test_splines_short.py b/python/tests/test_splines_short.py new file mode 100644 index 0000000000..37df5f5db9 --- /dev/null +++ b/python/tests/test_splines_short.py @@ -0,0 +1,140 @@ +""" +Test AMICI's C++ spline implementation by comparing +the results of simulations of simple SBML models +containing simple splines with a symbolically-computed +ground truth. +""" + +import numpy as np +import sympy as sp +from amici.splines import CubicHermiteSpline, UniformGrid +from amici.testing import skip_on_valgrind +from splines_utils import check_splines_full, example_spline_1 + + +def test_spline_piecewise(**kwargs): + """ + Test a SBML model containing a single spline. + AMICI's behaviour in absence of spline annotations is also tested. + """ + spline, params, tols = example_spline_1() + check_splines_full(spline, params, tols, **kwargs) + + +@skip_on_valgrind +def test_two_splines(**kwargs): + """ + Test a SBML model containing two splines. + """ + spline0, params0, tols0 = example_spline_1( + 0, num_nodes=4, fixed_values=[0, 2], extrapolate="linear" + ) + spline1, params1, tols1 = example_spline_1( + 1, num_nodes=5, scale=1.5, offset=5, extrapolate="linear" + ) + + splines = [spline0, spline1] + + params = dict(params0) + params.update(params1) + + if isinstance(tols0, dict): + tols0 = (tols0, tols0, tols0) + if isinstance(tols1, dict): + tols1 = (tols1, tols1, tols1) + + tols = [] + for t0, t1 in zip(tols0, tols1): + keys = set().union(t0.keys(), t1.keys()) + t = { + key: max( + t0.get(key, 0.0), + t1.get(key, 0.0), + ) + for key in keys + } + tols.append(t) + + tols[1]["x_rtol"] = max(1e-9, tols[1].get("x_rtol", -np.inf)) + tols[1]["x_atol"] = max(5e-9, tols[1].get("x_atol", -np.inf)) + tols[1]["sx_rtol"] = max(1e-5, tols[1].get("llh_rtol", -np.inf)) + tols[1]["sx_atol"] = max(5e-9, tols[1].get("sx_atol", -np.inf)) + tols[1]["llh_rtol"] = max(5e-14, tols[1].get("llh_rtol", -np.inf)) + tols[1]["sllh_atol"] = max(5e-5, tols[1].get("sllh_atol", -np.inf)) + + tols[2]["x_rtol"] = max(5e-10, tols[2].get("x_rtol", -np.inf)) + tols[2]["x_atol"] = max(1e-8, tols[2].get("x_atol", -np.inf)) + tols[2]["llh_rtol"] = max(5e-14, tols[2].get("llh_rtol", -np.inf)) + tols[2]["sllh_atol"] = max(5e-5, tols[2].get("sllh_atol", -np.inf)) + + check_splines_full(splines, params, tols, check_piecewise=False, **kwargs) + + +@skip_on_valgrind +def test_splines_plist(): + """ + Test if AMICI's spline implementation + handles correctly a change in the parameter list. + """ + # Dummy spline #1 + xx = UniformGrid(0, 5, number_of_nodes=3) + yy = np.asarray([0.0, 1.0, 0.5]) + spline1 = CubicHermiteSpline( + "y1", + nodes=xx, + values_at_nodes=yy, + bc="auto", + extrapolate=(None, "constant"), + ) + # Dummy spline #2 + xx = UniformGrid(0, 5, number_of_nodes=4) + yy = np.asarray([0.0, 0.5, -0.5, 0.5]) + spline2 = CubicHermiteSpline( + "y2", + nodes=xx, + values_at_nodes=yy, + bc="auto", + extrapolate=(None, "constant"), + ) + # Real spline #3 + xx = UniformGrid(0, 5, number_of_nodes=6) + p1, p2, p3, p4, p5 = sp.symbols("p1 p2 p3 p4 p5") + yy = np.asarray([p1 + p2, p2 * p3, p4, sp.cos(p1 + p3), p4 * sp.log(p1), p3]) + dd = np.asarray([-0.75, -0.875, p5, 0.125, 1.15057181, 0.0]) + params = {p1: 1.0, p2: 0.5, p3: 1.5, p4: -0.25, p5: -0.5} + # print([y.subs(params).evalf() for y in yy]) + spline3 = CubicHermiteSpline( + "y3", + nodes=xx, + values_at_nodes=yy, + derivatives_at_nodes=dd, + bc="auto", + extrapolate=(None, "constant"), + ) + # Dummy spline 4 + xx = UniformGrid(0, 5, number_of_nodes=3) + yy = np.asarray([0.0, -0.5, 0.5]) + spline4 = CubicHermiteSpline( + "y4", + nodes=xx, + values_at_nodes=yy, + bc="auto", + extrapolate=(None, "constant"), + ) + tols = dict( + x_rtol=1e-6, + x_atol=1e-11, + sx_rtol=1e-6, + sx_atol=5e-11, + llh_rtol=1e-14, + sllh_atol=5e-9, + ) + check_splines_full( + [spline1, spline2, spline3, spline4], + params, + tols, + check_piecewise=False, + check_forward=False, + check_adjoint=True, # plist cannot be checked, but complex parameter dependence can + parameter_lists=[[0, 1, 4], [2, 3]], + ) diff --git a/python/tests/test_swig_interface.py b/python/tests/test_swig_interface.py index eeaebceede..c2ae631030 100644 --- a/python/tests/test_swig_interface.py +++ b/python/tests/test_swig_interface.py @@ -21,10 +21,12 @@ def test_copy_constructors(pysb_example_presimulation_module): for obj in [model, solver]: for attr in dir(obj): - if attr.startswith('__') \ - or attr == 'this' \ - or attr == 'thisown' \ - or is_callable_but_not_getter(obj, attr): + if ( + attr.startswith("__") + or attr == "this" + or attr == "thisown" + or is_callable_but_not_getter(obj, attr) + ): continue # objects will be initialized with default values so we @@ -47,8 +49,7 @@ def test_copy_constructors(pysb_example_presimulation_module): obj_clone = obj.clone() - assert get_val(obj, attr) == get_val(obj_clone, attr), \ - f"{obj} - {attr}" + assert get_val(obj, attr) == get_val(obj_clone, attr), f"{obj} - {attr}" # `None` values are skipped in `test_model_instance_settings`. @@ -61,57 +62,51 @@ def test_copy_constructors(pysb_example_presimulation_module): # Default values are based on `pysb_example_presimulation_module`. model_instance_settings0 = { # setting name: [default value, custom value] - 'AddSigmaResiduals': [ - False, - True - ], - 'AlwaysCheckFinite': [ + "AddSigmaResiduals": [False, True], + "AlwaysCheckFinite": [ False, True, ], # Skipped due to model dependency in `'InitialStates'`. - 'FixedParameters': None, - 'InitialStates': [ + "FixedParameters": None, + "InitialStates": [ (10.0, 9.0, 1.0, 0.0, 0.0, 0.0), - tuple([.1]*6), + tuple([0.1] * 6), ], - ('getInitialStateSensitivities', 'setUnscaledInitialStateSensitivities'): [ - tuple([1.0] + [0.0]*35), - tuple([.1]*36), + ("getInitialStateSensitivities", "setUnscaledInitialStateSensitivities"): [ + tuple([1.0] + [0.0] * 35), + tuple([0.1] * 36), ], - 'MinimumSigmaResiduals': [ + "MinimumSigmaResiduals": [ 50.0, 60.0, ], - ('nMaxEvent', 'setNMaxEvent'): [ + ("nMaxEvent", "setNMaxEvent"): [ 10, 20, ], - 'Parameters': [ - (10.0, 0.1, 0.1, 0.1, 0.1, 0.1), - tuple([1.0] * 6) - ], + "Parameters": [(10.0, 0.1, 0.1, 0.1, 0.1, 0.1), tuple([1.0] * 6)], # Skipped due to interdependency with `'InitialStateSensitivities'`. - 'ParameterList': None, + "ParameterList": None, # Skipped due to interdependency with `'InitialStateSensitivities'`. - 'ParameterScale': None, + "ParameterScale": None, # Skipped due to interdependencies with # `'ReinitializeFixedParameterInitialStates'`. - 'ReinitializationStateIdxs': None, + "ReinitializationStateIdxs": None, # Skipped due to interdependencies with `'ReinitializationStateIdxs'`. - 'ReinitializeFixedParameterInitialStates': None, + "ReinitializeFixedParameterInitialStates": None, # Skipped due to conservation laws in the test model # `pysb_example_presimulation_module.getModel()`. - 'StateIsNonNegative': None, - 'SteadyStateSensitivityMode': [ + "StateIsNonNegative": None, + "SteadyStateSensitivityMode": [ 0, 1, ], - ('t0', 'setT0'): [ + ("t0", "setT0"): [ 0.0, 1.0, ], - 'Timepoints': [ + "Timepoints": [ tuple(), (1.0, 2.0, 3.0), ], @@ -129,24 +124,26 @@ def test_model_instance_settings(pysb_example_presimulation_module): i_setter = 1 # All settings are tested. - assert set(model_instance_settings0) \ - == set(amici.swig_wrappers.model_instance_settings) + assert set(model_instance_settings0) == set( + amici.swig_wrappers.model_instance_settings + ) # Skip settings with interdependencies. - model_instance_settings = \ - {k: v for k, v in model_instance_settings0.items() if v is not None} + model_instance_settings = { + k: v for k, v in model_instance_settings0.items() if v is not None + } # All custom values are different to default values. assert all( default != custom for name, (default, custom) in model_instance_settings.items() - if name != 'ReinitializeFixedParameterInitialStates' + if name != "ReinitializeFixedParameterInitialStates" ) # All default values are as expected. for name, (default, custom) in model_instance_settings.items(): - getter = name[i_getter] if isinstance(name, tuple) else f'get{name}' - setter = name[i_setter] if isinstance(name, tuple) else f'set{name}' + getter = name[i_getter] if isinstance(name, tuple) else f"get{name}" + setter = name[i_setter] if isinstance(name, tuple) else f"set{name}" # Default values are as expected. assert getattr(model0, getter)() == default # Custom value is set correctly. @@ -164,15 +161,17 @@ def test_model_instance_settings(pysb_example_presimulation_module): # The new model has the default settings. model_default_settings = amici.get_model_settings(model) for name in model_instance_settings: - if (name == "InitialStates" and not model.hasCustomInitialStates())\ - or (name == ('getInitialStateSensitivities', - 'setUnscaledInitialStateSensitivities') - and not model.hasCustomInitialStateSensitivities()): + if (name == "InitialStates" and not model.hasCustomInitialStates()) or ( + name + == ("getInitialStateSensitivities", "setUnscaledInitialStateSensitivities") + and not model.hasCustomInitialStateSensitivities() + ): # Here the expected value differs from what the getter would return assert model_default_settings[name] == [] else: - assert model_default_settings[name] == \ - model_instance_settings[name][i_default], name + assert ( + model_default_settings[name] == model_instance_settings[name][i_default] + ), name # The grouped setter method works. custom_settings_not_none = { @@ -197,20 +196,20 @@ def test_interdependent_settings(pysb_example_presimulation_module): model = pysb_example_presimulation_module.getModel() original_settings = { - 'FixedParameters': (9.0, 1.0), - 'ParameterList': (0, 1, 2, 3, 4, 5), - 'ParameterScale': [0, 0, 0, 0, 0, 0], - 'ReinitializationStateIdxs': tuple(), - 'ReinitializeFixedParameterInitialStates': False, - 'StateIsNonNegative': (False, False, False), + "FixedParameters": (9.0, 1.0), + "ParameterList": (0, 1, 2, 3, 4, 5), + "ParameterScale": [0, 0, 0, 0, 0, 0], + "ReinitializationStateIdxs": tuple(), + "ReinitializeFixedParameterInitialStates": False, + "StateIsNonNegative": (False, False, False), } expected_settings = { - 'FixedParameters': (8.0, 2.0), - 'ParameterList': (0, 1, 2, 3, 4), - 'ParameterScale': [1, 0, 0, 0, 0, 0], - 'ReinitializationStateIdxs': (0,), - 'ReinitializeFixedParameterInitialStates': True, + "FixedParameters": (8.0, 2.0), + "ParameterList": (0, 1, 2, 3, 4), + "ParameterScale": [1, 0, 0, 0, 0, 0], + "ReinitializationStateIdxs": (0,), + "ReinitializeFixedParameterInitialStates": True, # Skipped due to conservation laws in the test model. # 'StateIsNonNegative': None, } @@ -218,14 +217,13 @@ def test_interdependent_settings(pysb_example_presimulation_module): # Some values need to be transformed to be tested in Python # (e.g. SWIG objects). Default transformer is no transformation # (the identity function). - getter_transformers = { - setting: (lambda x: x) - for setting in original_settings - } - getter_transformers.update({ - # Convert from SWIG object. - 'ParameterScale': lambda x: list(x) - }) + getter_transformers = {setting: (lambda x: x) for setting in original_settings} + getter_transformers.update( + { + # Convert from SWIG object. + "ParameterScale": lambda x: list(x) + } + ) default_settings = amici.get_model_settings(model) for original_setting, original_setting_value in original_settings.items(): @@ -241,18 +239,14 @@ def test_interdependent_settings(pysb_example_presimulation_module): amici.set_model_settings(model, input_settings) output_settings = amici.get_model_settings(model) - test_value = getter_transformers[setting]( - output_settings[setting] - ) + test_value = getter_transformers[setting](output_settings[setting]) # The setter works. assert test_value == expected_value input_settings = {setting: output_settings[setting]} amici.set_model_settings(model, input_settings) output_settings = amici.get_model_settings(model) - test_value = getter_transformers[setting]( - output_settings[setting] - ) + test_value = getter_transformers[setting](output_settings[setting]) # (round-trip) The output of the getter can be used as input to the # setter, and does not change the value. assert test_value == expected_value @@ -272,54 +266,53 @@ def test_unhandled_settings(pysb_example_presimulation_module): model = pysb_example_presimulation_module.getModel() not_handled = [ - 'get', - 'getAmiciCommit', - 'getAmiciVersion', - 'getExpressionIds', - 'getExpressionNames', - 'getFixedParameterById', - 'getFixedParameterByName', - 'getFixedParameterIds', - 'getFixedParameterNames', - 'getName', - 'getObservableIds', - 'getObservableNames', - 'getObservableScaling', - 'getParameterById', - 'getParameterByName', - 'getParameterIds', - 'getParameterNames', - 'getSolver', - 'getStateIds', - 'getStateNames', - 'getStateIdsSolver', - 'getStateNamesSolver', - 'getTimepoint', - 'getUnscaledParameters', - 'setAllStatesNonNegative', - 'setFixedParameterById', - 'setFixedParameterByName', - 'setFixedParametersByIdRegex', - 'setFixedParametersByNameRegex', - 'setParameterById', - 'setParameterByName', - 'setParametersByIdRegex', - 'setParametersByNameRegex', - 'setInitialStateSensitivities', + "get", + "getAmiciCommit", + "getAmiciVersion", + "getExpressionIds", + "getExpressionNames", + "getFixedParameterById", + "getFixedParameterByName", + "getFixedParameterIds", + "getFixedParameterNames", + "getName", + "getObservableIds", + "getObservableNames", + "getObservableScaling", + "getParameterById", + "getParameterByName", + "getParameterIds", + "getParameterNames", + "getSolver", + "getStateIds", + "getStateNames", + "getStateIdsSolver", + "getStateNamesSolver", + "getTimepoint", + "getUnscaledParameters", + "setAllStatesNonNegative", + "setFixedParameterById", + "setFixedParameterByName", + "setFixedParametersByIdRegex", + "setFixedParametersByNameRegex", + "setParameterById", + "setParameterByName", + "setParametersByIdRegex", + "setParametersByNameRegex", + "setInitialStateSensitivities", ] from amici.swig_wrappers import model_instance_settings + handled = [ name for names in model_instance_settings for name in ( - names - if isinstance(names, tuple) else - (f'get{names}', f'set{names}') + names if isinstance(names, tuple) else (f"get{names}", f"set{names}") ) ] for attribute in dir(model): - if attribute[:3] in ['get', 'set'] and attribute not in not_handled: + if attribute[:3] in ["get", "set"] and attribute not in not_handled: assert attribute in handled, attribute @@ -327,11 +320,12 @@ def is_callable_but_not_getter(obj, attr): if not callable(getattr(obj, attr)): return False - if attr.startswith('get'): - return \ - 'set' + attr[3:] not in dir(obj) \ - or attr.endswith('ById') \ - or attr.endswith('ByName') + if attr.startswith("get"): + return ( + "set" + attr[3:] not in dir(obj) + or attr.endswith("ById") + or attr.endswith("ByName") + ) else: return True @@ -344,12 +338,12 @@ def get_val(obj, attr): def get_mod_val(val, attr): - if attr == 'getReturnDataReportingMode': + if attr == "getReturnDataReportingMode": return amici.RDataReporting.likelihood - elif attr == 'getParameterList': - return tuple(get_mod_val(val[0], '') for _ in val) - elif attr == 'getStateIsNonNegative': - raise ValueError('Cannot modify value') + elif attr == "getParameterList": + return tuple(get_mod_val(val[0], "") for _ in val) + elif attr == "getStateIsNonNegative": + raise ValueError("Cannot modify value") elif isinstance(val, bool): return not val elif isinstance(val, numbers.Number): @@ -357,12 +351,12 @@ def get_mod_val(val, attr): elif isinstance(val, tuple): return tuple(get_mod_val(v, attr) for v in val) - raise ValueError('Cannot modify value') + raise ValueError("Cannot modify value") def set_val(obj, attr, val): if callable(getattr(obj, attr)): - getattr(obj, 'set' + attr[3:])(val) + getattr(obj, "set" + attr[3:])(val) else: setattr(obj, attr, val) @@ -377,8 +371,7 @@ def test_model_instance_settings_custom_x0(pysb_example_presimulation_module): assert not model.hasCustomInitialStateSensitivities() settings = amici.get_model_settings(model) model.setInitialStates(model.getInitialStates()) - model.setUnscaledInitialStateSensitivities( - model.getInitialStateSensitivities()) + model.setUnscaledInitialStateSensitivities(model.getInitialStateSensitivities()) amici.set_model_settings(model, settings) assert not model.hasCustomInitialStates() assert not model.hasCustomInitialStateSensitivities() @@ -420,10 +413,10 @@ def test_edata_repr(): edata = amici.ExpData(ny, nz, ne, range(nt)) edata_ptr = amici.ExpDataPtr(edata.this) expected_strs = ( - f'{nt}x{ny} time-resolved datapoints', - f'{ne}x{nz} event-resolved datapoints', - f'(0/{ny * nt} measurements', - f'(0/{nz * ne} measurements' + f"{nt}x{ny} time-resolved datapoints", + f"{ne}x{nz} event-resolved datapoints", + f"(0/{ny * nt} measurements", + f"(0/{nz * ne} measurements", ) for e in [edata, edata_ptr]: for expected_str in expected_strs: @@ -431,4 +424,3 @@ def test_edata_repr(): assert expected_str in repr(e) # avoid double delete!! edata_ptr.release() - diff --git a/python/tests/util.py b/python/tests/util.py index c51070a8b0..14f514c997 100644 --- a/python/tests/util.py +++ b/python/tests/util.py @@ -1,15 +1,17 @@ """Tests for SBML events, including piecewise expressions.""" -import libsbml -import numpy as np +import sys +import tempfile from pathlib import Path +import libsbml +import numpy as np from amici import ( AmiciModel, - import_model_module, - runAmiciSimulation, SbmlImporter, SensitivityMethod, - SensitivityOrder + SensitivityOrder, + import_model_module, + runAmiciSimulation, ) from amici.gradient_check import _check_close @@ -18,30 +20,30 @@ def create_amici_model(sbml_model, model_name, **kwargs) -> AmiciModel: """ Import an sbml file and create an AMICI model from it """ - sbml_test_models = Path('sbml_test_models') - sbml_test_models_output_dir = sbml_test_models / 'amici_models' + sbml_test_models_output_dir = Path("amici_models") sbml_test_models_output_dir.mkdir(parents=True, exist_ok=True) sbml_importer = SbmlImporter(sbml_model) - output_dir = sbml_test_models_output_dir / model_name - sbml_importer.sbml2amici( - model_name=model_name, - output_dir=str(output_dir), - **kwargs + # try not to exceed the stupid maximum path length on windows 💩 + output_dir = ( + sbml_test_models_output_dir / model_name + if sys.platform != "win32" + else tempfile.mkdtemp() ) - model_module = import_model_module(model_name, str(output_dir.resolve())) - model = model_module.getModel() - return model + sbml_importer.sbml2amici(model_name=model_name, output_dir=output_dir, **kwargs) + + model_module = import_model_module(model_name, output_dir) + return model_module.getModel() def create_sbml_model( - initial_assignments, - parameters, - rate_rules, - species, - events, - to_file: str = None, + initial_assignments, + parameters, + rate_rules, + species, + events, + to_file: str = None, ): """Create an SBML model from simple definitions. @@ -54,18 +56,18 @@ def create_sbml_model( model = document.createModel() compartment = model.createCompartment() - compartment.setId('compartment') + compartment.setId("compartment") compartment.setConstant(True) compartment.setSize(1) compartment.setSpatialDimensions(3) - compartment.setUnits('dimensionless') + compartment.setUnits("dimensionless") for species_id in species: species = model.createSpecies() species.setId(species_id) - species.setCompartment('compartment') + species.setCompartment("compartment") species.setConstant(False) - species.setSubstanceUnits('dimensionless') + species.setSubstanceUnits("dimensionless") species.setBoundaryCondition(False) species.setHasOnlySubstanceUnits(False) species.setInitialConcentration(1.0) @@ -85,7 +87,7 @@ def create_sbml_model( parameter.setId(parameter_id) parameter.setConstant(True) parameter.setValue(parameter_value) - parameter.setUnits('dimensionless') + parameter.setUnits("dimensionless") for event_id, event_def in events.items(): event = model.createEvent() @@ -93,30 +95,26 @@ def create_sbml_model( event.setName(event_id) event.setUseValuesFromTriggerTime(True) trigger = event.createTrigger() - trigger.setMath(libsbml.parseL3Formula(event_def['trigger'])) + trigger.setMath(libsbml.parseL3Formula(event_def["trigger"])) trigger.setPersistent(True) trigger.setInitialValue(True) - def creat_event_assignment(target, assignment): + def create_event_assignment(target, assignment): ea = event.createEventAssignment() ea.setVariable(target) ea.setMath(libsbml.parseL3Formula(assignment)) - if isinstance(event_def['target'], list): + 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"] ): - creat_event_assignment(event_target, event_assignment) + create_event_assignment(event_target, event_assignment) else: - creat_event_assignment(event_def['target'], - event_def['assignment']) + create_event_assignment(event_def["target"], event_def["assignment"]) if to_file: - libsbml.writeSBMLToFile( - document, - str(to_file), - ) + libsbml.writeSBMLToFile(document, to_file) # Need to return document, else AMICI throws an error. # (possibly due to garbage collection?) @@ -124,52 +122,29 @@ def creat_event_assignment(target, assignment): def check_trajectories_without_sensitivities( - amici_model: AmiciModel, - result_expected_x: np.ndarray, + amici_model: AmiciModel, + result_expected_x: np.ndarray, ): """ Check whether the AMICI simulation matches a known solution (ideally an analytically calculated one). """ - - # Does the AMICI simulation match the analytical solution? - solver = amici_model.getSolver() - solver.setAbsoluteTolerance(1e-15) - rdata = runAmiciSimulation(amici_model, solver=solver) - _check_close(rdata['x'], result_expected_x, field="x", - rtol=5e-5, atol=1e-13) - - # Show that we can do arbitrary precision here (test 8 digits) solver = amici_model.getSolver() solver.setAbsoluteTolerance(1e-15) solver.setRelativeTolerance(1e-12) rdata = runAmiciSimulation(amici_model, solver=solver) - _check_close(rdata['x'], result_expected_x, field="x", - rtol=5e-9, atol=1e-13) + _check_close(rdata["x"], result_expected_x, field="x", rtol=5e-9, atol=1e-13) def check_trajectories_with_forward_sensitivities( - amici_model: AmiciModel, - result_expected_x: np.ndarray, - result_expected_sx: np.ndarray, + amici_model: AmiciModel, + result_expected_x: np.ndarray, + result_expected_sx: np.ndarray, ): """ Check whether the forward sensitivities of the AMICI simulation match a known solution (ideally an analytically calculated one). """ - - # Show that we can do arbitrary precision here (test 8 digits) - solver = amici_model.getSolver() - solver.setAbsoluteTolerance(1e-15) - solver.setSensitivityOrder(SensitivityOrder.first) - solver.setSensitivityMethod(SensitivityMethod.forward) - rdata = runAmiciSimulation(amici_model, solver=solver) - _check_close(rdata['x'], result_expected_x, field="x", - rtol=1e-5, atol=1e-13) - _check_close(rdata['sx'], result_expected_sx, field="sx", - rtol=1e-5, atol=1e-7) - - # Show that we can do arbitrary precision here (test 8 digits) solver = amici_model.getSolver() solver.setSensitivityOrder(SensitivityOrder.first) solver.setSensitivityMethod(SensitivityMethod.forward) @@ -178,7 +153,5 @@ def check_trajectories_with_forward_sensitivities( solver.setAbsoluteToleranceFSA(1e-15) solver.setRelativeToleranceFSA(1e-13) rdata = runAmiciSimulation(amici_model, solver=solver) - _check_close(rdata['x'], result_expected_x, field="x", - rtol=1e-10, atol=1e-12) - _check_close(rdata['sx'], result_expected_sx, field="sx", - rtol=1e-10, atol=1e-9) + _check_close(rdata["x"], result_expected_x, field="x", rtol=1e-10, atol=1e-12) + _check_close(rdata["sx"], result_expected_sx, field="sx", rtol=1e-7, atol=1e-9) diff --git a/scripts/README.md b/scripts/README.md index d656d9d499..f7e75f34b4 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,9 +1,9 @@ # Contents of `scripts/` -This directory contains a number of build, installation, and CI scripts. +This directory contains a number of build, installation, and CI scripts. * `buildAll.sh` - + Build AMICI along with dependencies and test suite * `buildAmici.sh` @@ -12,14 +12,14 @@ This directory contains a number of build, installation, and CI scripts. * `buildBNGL.sh` - Download and build + Download and build [BioNetGen](https://github.com/RuleWorld/bionetgen) (required for some tests) - + * `buildSuiteSparse.sh` Build [SuiteSparse](http://faculty.cse.tamu.edu/davis/suitesparse.html) included in this repository - + * `buildSundials.sh` Build [Sundials](https://computation.llnl.gov/projects/sundials/) @@ -41,14 +41,14 @@ This directory contains a number of build, installation, and CI scripts. * `downloadAndBuildSwig.sh` - Download and build [SWIG](http://www.swig.org/) + Download and build [SWIG](http://www.swig.org/) * `installAmiciArchive.sh` Create a Python virtual environment and do an AMICI development installation * `installAmiciSource.sh` - + Create a Python virtual environment and do a regular AMICI installation * `run-codecov.sh` @@ -78,10 +78,10 @@ This directory contains a number of build, installation, and CI scripts. * `run-SBMLTestsuite.sh` - Download and run the semantic + Download and run the semantic [SBML Test Suite](https://github.com/sbmlteam/sbml-test-suite/) * `run-valgrind.sh` Run memory leak check using valgrind for all unit and integration tests. - Assumes they have been built before in the default location. + Assumes they have been built before in the default location. diff --git a/scripts/downloadAndBuildDoxygen.sh b/scripts/downloadAndBuildDoxygen.sh index 503934cb31..c51c05c599 100755 --- a/scripts/downloadAndBuildDoxygen.sh +++ b/scripts/downloadAndBuildDoxygen.sh @@ -9,7 +9,7 @@ DOXYGEN_DIR="${AMICI_PATH}"/ThirdParty/doxygen cd "${AMICI_PATH}"/ThirdParty if [[ ! -d ${DOXYGEN_DIR} ]]; then # git clone --depth 1 https://github.com/doxygen/doxygen.git "${DOXYGEN_DIR}" - git clone --single-branch --branch Release_1_9_6 --depth 1 https://github.com/doxygen/doxygen.git "${DOXYGEN_DIR}" + git clone --single-branch --branch Release_1_9_7 --depth 1 https://github.com/doxygen/doxygen.git "${DOXYGEN_DIR}" fi cd "${DOXYGEN_DIR}" diff --git a/scripts/installAmiciSource.sh b/scripts/installAmiciSource.sh index 0e4954acfa..aa330bef22 100755 --- a/scripts/installAmiciSource.sh +++ b/scripts/installAmiciSource.sh @@ -30,6 +30,7 @@ fi pip install -U "setuptools<64" pip install --upgrade pip wheel pip install --upgrade pip scipy matplotlib coverage pytest \ - pytest-cov cmake_build_extension numpy -pip install --verbose -e ${AMICI_PATH}/python/sdist[petab,test,pysb,vis] --no-build-isolation + pytest-cov cmake_build_extension numpy +pip install git+https://github.com/FFroehlich/pysb@fix_pattern_matching # pin to PR for SPM with compartments +pip install --verbose -e ${AMICI_PATH}/python/sdist[petab,test,vis] --no-build-isolation deactivate diff --git a/scripts/run-cppcheck.sh b/scripts/run-cppcheck.sh index 57c669439a..5726bf8e3b 100755 --- a/scripts/run-cppcheck.sh +++ b/scripts/run-cppcheck.sh @@ -15,4 +15,3 @@ cppcheck \ "-I${AMICI_PATH}/include/" \ --enable=style \ "--exitcode-suppressions=${AMICI_PATH}/.cppcheck-exitcode-suppressions" - diff --git a/scripts/run-python-tests.sh b/scripts/run-python-tests.sh index e0b33eb47a..982aa02f0f 100755 --- a/scripts/run-python-tests.sh +++ b/scripts/run-python-tests.sh @@ -15,4 +15,4 @@ source "${amici_path}"/build/venv/bin/activate pip install scipy h5py pytest pytest-cov # PEtab tests are run separately -pytest --ignore-glob=*petab* +pytest --ignore-glob=*petab* --ignore-glob=*test_splines.py diff --git a/scripts/runNotebook.sh b/scripts/runNotebook.sh index 437fb7217a..4fa815a5b5 100755 --- a/scripts/runNotebook.sh +++ b/scripts/runNotebook.sh @@ -10,7 +10,7 @@ AMICI_PATH=$(cd $SCRIPT_PATH/.. && pwd) runNotebook () { set +e tempfile=$(mktemp) - jupyter nbconvert --debug --stdout --execute --ExecutePreprocessor.timeout=300 --to markdown $@ &> $tempfile + jupyter nbconvert --debug --stdout --execute --ExecutePreprocessor.timeout=600 --to markdown $@ &> $tempfile ret=$? if [[ $ret != 0 ]]; then cat $tempfile diff --git a/src/abstract_model.cpp b/src/abstract_model.cpp index a610023bd1..dc2c469173 100644 --- a/src/abstract_model.cpp +++ b/src/abstract_model.cpp @@ -2,749 +2,672 @@ namespace amici { -std::string -AbstractModel::getAmiciVersion() const -{ +std::string AbstractModel::getAmiciVersion() const { throw AmiException("Version not set during code generation"); } -std::string -AbstractModel::getAmiciCommit() const -{ +std::string AbstractModel::getAmiciCommit() const { throw AmiException("Commit not set during code generation"); } -void -AbstractModel::fx0(realtype* /*x0*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); +void AbstractModel:: + fx0(realtype* /*x0*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); } -bool -AbstractModel::isFixedParameterStateReinitializationAllowed() const -{ +bool AbstractModel::isFixedParameterStateReinitializationAllowed() const { return false; } -void -AbstractModel::fx0_fixedParameters(realtype* /*x0*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/, - gsl::span /*reinitialization_state_idxs*/) -{ +void AbstractModel::fx0_fixedParameters( + realtype* /*x0*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/, gsl::span /*reinitialization_state_idxs*/ +) { // no-op default implementation } -void -AbstractModel::fsx0_fixedParameters(realtype* /*sx0*/, - const realtype /*t*/, - const realtype* /*x0*/, - const realtype* /*p*/, - const realtype* /*k*/, - const int /*ip*/, - gsl::span /*reinitialization_state_idxs*/) -{ +void AbstractModel::fsx0_fixedParameters( + realtype* /*sx0*/, const realtype /*t*/, realtype const* /*x0*/, + realtype const* /*p*/, realtype const* /*k*/, int const /*ip*/, + gsl::span /*reinitialization_state_idxs*/ +) { // no-op default implementation } -void -AbstractModel::fsx0(realtype* /*sx0*/, - const realtype /*t*/, - const realtype* /*x0*/, - const realtype* /*p*/, - const realtype* /*k*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); +void AbstractModel::fsx0( + realtype* /*sx0*/, const realtype /*t*/, realtype const* /*x0*/, + realtype const* /*p*/, realtype const* /*k*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); } -void -AbstractModel::fdx0(AmiVector& /*x0*/, AmiVector& /*dx0*/) -{ +void AbstractModel::fdx0(AmiVector& /*x0*/, AmiVector& /*dx0*/) { // no-op default implementation } -void -AbstractModel::fstau(realtype* /*stau*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*tcl*/, - const realtype* /*sx*/, - const int /*ip*/, - const int /*ie*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fy(realtype* /*y*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdydp(realtype* /*dydp*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ip*/, - const realtype* /*w*/, - const realtype* /*dwdp*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdydp(realtype */*dydp*/, const realtype /*t*/, - const realtype */*x*/, const realtype */*p*/, - const realtype */*k*/, const realtype */*h*/, - int /*ip*/, const realtype */*w*/, - const realtype */*tcl*/, const realtype */*dtcldp*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdydx(realtype* /*dydx*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/, - const realtype* /*dwdx*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fz(realtype* /*z*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fsz(realtype* /*sz*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*sx*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::frz(realtype* /*rz*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fsrz(realtype* /*srz*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*sx*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdzdp(realtype* /*dzdp*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdzdx(realtype* /*dzdx*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdrzdp(realtype* /*drzdp*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdrzdx(realtype* /*drzdx*/, - const int /*ie*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdeltax(realtype* /*deltax*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ie*/, - const realtype* /*xdot*/, - const realtype* /*xdot_old*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdeltasx(realtype* /*deltasx*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/, - const int /*ip*/, - const int /*ie*/, - const realtype* /*xdot*/, - const realtype* /*xdot_old*/, - const realtype* /*sx*/, - const realtype* /*stau*/, - const realtype* /*tcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdeltaxB(realtype* /*deltaxB*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ie*/, - const realtype* /*xdot*/, - const realtype* /*xdot_old*/, - const realtype* /*xB*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdeltaqB(realtype* /*deltaqB*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const int /*ip*/, - const int /*ie*/, - const realtype* /*xdot*/, - const realtype* /*xdot_old*/, - const realtype* /*xB*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fsigmay(realtype* /*sigmay*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype */*y*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdsigmaydp(realtype* /*dsigmaydp*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype */*y*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdsigmaydy(realtype */*dsigmaydy*/, - const realtype /*t*/, - const realtype */*p*/, - const realtype */*k*/, - const realtype */*y*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fsigmaz(realtype* /*sigmaz*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdsigmazdp(realtype* /*dsigmazdp*/, - const realtype /*t*/, - const realtype* /*p*/, - const realtype* /*k*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fJy(realtype* /*nllh*/, - const int /*iy*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*y*/, - const realtype* /*sigmay*/, - const realtype* /*my*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fJz(realtype* /*nllh*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*z*/, - const realtype* /*sigmaz*/, - const realtype* /*mz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fJrz(realtype* /*nllh*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*z*/, - const realtype* /*sigmaz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJydy(realtype* /*dJydy*/, - const int /*iy*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*y*/, - const realtype* /*sigmay*/, - const realtype* /*my*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJydy_colptrs(SUNMatrixWrapper &/*indexptrs*/, - int /*index*/) { - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJydy_rowvals(SUNMatrixWrapper & /*indexptrs*/, - int /*index*/) { - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJydsigma(realtype* /*dJydsigma*/, - const int /*iy*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*y*/, - const realtype* /*sigmay*/, - const realtype* /*my*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJzdz(realtype* /*dJzdz*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*z*/, - const realtype* /*sigmaz*/, - const realtype* /*mz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJzdsigma(realtype* /*dJzdsigma*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*z*/, - const realtype* /*sigmaz*/, - const realtype* /*mz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJrzdz(realtype* /*dJrzdz*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*rz*/, - const realtype* /*sigmaz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdJrzdsigma(realtype* /*dJrzdsigma*/, - const int /*iz*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*rz*/, - const realtype* /*sigmaz*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fw(realtype* /*w*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*tcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdp(realtype* /*dwdp*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/, - const realtype* /*tcl*/, - const realtype* /*stcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdp_colptrs(SUNMatrixWrapper &/*dwdp*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdp_rowvals(SUNMatrixWrapper &/*dwdp*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdp(realtype* /*dwdp*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/, - const realtype* /*tcl*/, - const realtype* /*stcl*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdx(realtype* /*dwdx*/, - const realtype /*t*/, - const realtype* /*x*/, - const realtype* /*p*/, - const realtype* /*k*/, - const realtype* /*h*/, - const realtype* /*w*/, - const realtype* /*tcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdx_colptrs(SUNMatrixWrapper &/*dwdx*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdx_rowvals(SUNMatrixWrapper &/*dwdx*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void -AbstractModel::fdwdw(realtype */*dwdw*/, - realtype /*t*/, - const realtype */*x*/, - const realtype */*p*/, - const realtype */*k*/, - const realtype */*h*/, - const realtype */*w*/, - const realtype */*tcl*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); -} - -void AbstractModel::fdwdw_colptrs(SUNMatrixWrapper &/*dwdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); -} - -void AbstractModel::fdwdw_rowvals(SUNMatrixWrapper &/*dwdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadx_solver(realtype */*dx_rdatadx_solver*/, - const realtype */*x*/, const realtype */*tcl*/, - const realtype */*p*/, const realtype */*k*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadx_solver_rowvals(SUNMatrixWrapper &/*dxrdxs*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadx_solver_colptrs(SUNMatrixWrapper &/*dxrdxs*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadp(realtype */*dx_rdatadp*/, const realtype */*x*/, - const realtype */*tcl*/, const realtype */*p*/, - const realtype */*k*/, const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadtcl(realtype */*dx_rdatadtcl*/, const realtype */*x*/, - const realtype */*tcl*/, const realtype */*p*/, - const realtype */*k*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadtcl_rowvals(SUNMatrixWrapper &/*dxrdtcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdx_rdatadtcl_colptrs(SUNMatrixWrapper &/*dxrdtcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdtotal_cldp(realtype */*dtotal_cldp*/, - const realtype */*x_rdata*/, - const realtype */*p*/, - const realtype */*k*/, - const int /*ip*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdtotal_cldx_rdata(realtype */*dtotal_cldx_rdata*/, - const realtype */*x_rdata*/, - const realtype */*p*/, - const realtype */*k*/, - const realtype */*tcl*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} - -void AbstractModel::fdtotal_cldx_rdata_colptrs( - SUNMatrixWrapper &/*dtotal_cldx_rdata*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); -} +void AbstractModel::fstau( + realtype* /*stau*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*tcl*/, realtype const* /*sx*/, int const /*ip*/, + int const /*ie*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fy(realtype* /*y*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdydp( + realtype* /*dydp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ip*/, realtype const* /*w*/, realtype const* /*dwdp*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdydp( + realtype* /*dydp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int /*ip*/, realtype const* /*w*/, realtype const* /*tcl*/, + realtype const* /*dtcldp*/, realtype const* /*spl*/, + realtype const* /*sspl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdydx( + realtype* /*dydx*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*dwdx*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fz(realtype* /*z*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fsz(realtype* /*sz*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/, realtype const* /*sx*/, int const /*ip*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + frz(realtype* /*rz*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fsrz( + realtype* /*srz*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/, realtype const* /*sx*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdzdp( + realtype* /*dzdp*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdzdx( + realtype* /*dzdx*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdrzdp( + realtype* /*drzdp*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdrzdx( + realtype* /*drzdx*/, int const /*ie*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdeltax( + realtype* /*deltax*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ie*/, realtype const* /*xdot*/, realtype const* /*xdot_old*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdeltasx( + realtype* /*deltasx*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, int const /*ip*/, int const /*ie*/, + realtype const* /*xdot*/, realtype const* /*xdot_old*/, + realtype const* /*sx*/, realtype const* /*stau*/, realtype const* /*tcl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdeltaxB( + realtype* /*deltaxB*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ie*/, realtype const* /*xdot*/, realtype const* /*xdot_old*/, + realtype const* /*xB*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdeltaqB( + realtype* /*deltaqB*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ip*/, int const /*ie*/, realtype const* /*xdot*/, + realtype const* /*xdot_old*/, realtype const* /*xB*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fsigmay( + realtype* /*sigmay*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdsigmaydp( + realtype* /*dsigmaydp*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdsigmaydy( + realtype* /*dsigmaydy*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fsigmaz( + realtype* /*sigmaz*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdsigmazdp( + realtype* /*dsigmazdp*/, const realtype /*t*/, realtype const* /*p*/, + realtype const* /*k*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fJy(realtype* /*nllh*/, int const /*iy*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/, + realtype const* /*sigmay*/, realtype const* /*my*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fJz(realtype* /*nllh*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*z*/, + realtype const* /*sigmaz*/, realtype const* /*mz*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fJrz( + realtype* /*nllh*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*z*/, realtype const* /*sigmaz*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJydy( + realtype* /*dJydy*/, int const /*iy*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/, realtype const* /*sigmay*/, + realtype const* /*my*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fdJydy_colptrs(SUNMatrixWrapper& /*indexptrs*/, int /*index*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fdJydy_rowvals(SUNMatrixWrapper& /*indexptrs*/, int /*index*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJydsigma( + realtype* /*dJydsigma*/, int const /*iy*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*y*/, realtype const* /*sigmay*/, + realtype const* /*my*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJzdz( + realtype* /*dJzdz*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*z*/, realtype const* /*sigmaz*/, + realtype const* /*mz*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJzdsigma( + realtype* /*dJzdsigma*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*z*/, realtype const* /*sigmaz*/, + realtype const* /*mz*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJrzdz( + realtype* /*dJrzdz*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*rz*/, realtype const* /*sigmaz*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdJrzdsigma( + realtype* /*dJrzdsigma*/, int const /*iz*/, realtype const* /*p*/, + realtype const* /*k*/, realtype const* /*rz*/, realtype const* /*sigmaz*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fw(realtype* /*w*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*tcl*/, realtype const* /*spl*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdp( + realtype* /*dwdp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*tcl*/, realtype const* /*stcl*/, + realtype const* /*spl*/, realtype const* /*sspl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdp_colptrs(SUNMatrixWrapper& /*dwdp*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdp_rowvals(SUNMatrixWrapper& /*dwdp*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdp( + realtype* /*dwdp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*tcl*/, realtype const* /*stcl*/, + realtype const* /*spl*/, realtype const* /*sspl*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdx( + realtype* /*dwdx*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*tcl*/, realtype const* /*spl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdx_colptrs(SUNMatrixWrapper& /*dwdx*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdx_rowvals(SUNMatrixWrapper& /*dwdx*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdw( + realtype* /*dwdw*/, realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*tcl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdw_colptrs(SUNMatrixWrapper& /*dwdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdwdw_rowvals(SUNMatrixWrapper& /*dwdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadx_solver( + realtype* /*dx_rdatadx_solver*/, realtype const* /*x*/, + realtype const* /*tcl*/, realtype const* /*p*/, realtype const* /*k*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadx_solver_rowvals(SUNMatrixWrapper& /*dxrdxs*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadx_solver_colptrs(SUNMatrixWrapper& /*dxrdxs*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadp( + realtype* /*dx_rdatadp*/, realtype const* /*x*/, realtype const* /*tcl*/, + realtype const* /*p*/, realtype const* /*k*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadtcl( + realtype* /*dx_rdatadtcl*/, realtype const* /*x*/, realtype const* /*tcl*/, + realtype const* /*p*/, realtype const* /*k*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadtcl_rowvals(SUNMatrixWrapper& /*dxrdtcl*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdx_rdatadtcl_colptrs(SUNMatrixWrapper& /*dxrdtcl*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdtotal_cldp( + realtype* /*dtotal_cldp*/, realtype const* /*x_rdata*/, + realtype const* /*p*/, realtype const* /*k*/, int const /*ip*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel::fdtotal_cldx_rdata( + realtype* /*dtotal_cldx_rdata*/, realtype const* /*x_rdata*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*tcl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fdtotal_cldx_rdata_colptrs(SUNMatrixWrapper& /*dtotal_cldx_rdata*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +void AbstractModel:: + fdtotal_cldx_rdata_rowvals(SUNMatrixWrapper& /*dtotal_cldx_rdata*/) { + throw AmiException( + "Requested functionality is not supported as %s is " + "not implemented for this model!", + __func__ + ); +} + +std::vector +AbstractModel::fcreate_splines(realtype const* /*p*/, realtype const* /*k*/) { + return std::vector(); +} + +void AbstractModel::fdspline_valuesdp( + realtype* /*dspline_valuesdp*/, realtype const* /*p*/, + realtype const* /*k*/, int const /*ip*/ +) { + // no-op default implementation +} -void AbstractModel::fdtotal_cldx_rdata_rowvals( - SUNMatrixWrapper &/*dtotal_cldx_rdata*/) -{ - throw AmiException("Requested functionality is not supported as %s is " - "not implemented for this model!", - __func__); +void AbstractModel::fdspline_slopesdp( + realtype* /*dspline_slopesdp*/, realtype const* /*p*/, + realtype const* /*k*/, int const /*ip*/ +) { + // no-op default implementation } } // namespace amici diff --git a/src/amici.cpp b/src/amici.cpp index c50bac4227..ee3949b0bf 100644 --- a/src/amici.cpp +++ b/src/amici.cpp @@ -5,10 +5,10 @@ #include "amici/amici.h" -#include "amici/steadystateproblem.h" #include "amici/backwardproblem.h" #include "amici/forwardproblem.h" #include "amici/logging.h" +#include "amici/steadystateproblem.h" #include //return codes #include //realtype @@ -23,24 +23,36 @@ #include // ensure definitions are in sync -static_assert(amici::AMICI_SUCCESS == CV_SUCCESS, - "AMICI_SUCCESS != CV_SUCCESS"); -static_assert(amici::AMICI_DATA_RETURN == CV_TSTOP_RETURN, - "AMICI_DATA_RETURN != CV_TSTOP_RETURN"); -static_assert(amici::AMICI_ROOT_RETURN == CV_ROOT_RETURN, - "AMICI_ROOT_RETURN != CV_ROOT_RETURN"); -static_assert(amici::AMICI_ILL_INPUT == CV_ILL_INPUT, - "AMICI_ILL_INPUT != CV_ILL_INPUT"); -static_assert(amici::AMICI_NORMAL == CV_NORMAL, - "AMICI_NORMAL != CV_NORMAL"); -static_assert(amici::AMICI_ONE_STEP == CV_ONE_STEP, - "AMICI_ONE_STEP != CV_ONE_STEP"); -static_assert(amici::AMICI_SINGULAR_JACOBIAN == SUNLS_PACKAGE_FAIL_UNREC, - "AMICI_SINGULAR_JACOBIAN != SUNLS_PACKAGE_FAIL_UNREC"); -static_assert(amici::AMICI_SINGULAR_JACOBIAN == SUNLS_PACKAGE_FAIL_UNREC, - "AMICI_SINGULAR_JACOBIAN != SUNLS_PACKAGE_FAIL_UNREC"); -static_assert(std::is_same::value, - "Definition of realtype does not match"); +static_assert( + amici::AMICI_SUCCESS == CV_SUCCESS, "AMICI_SUCCESS != CV_SUCCESS" +); +static_assert( + amici::AMICI_DATA_RETURN == CV_TSTOP_RETURN, + "AMICI_DATA_RETURN != CV_TSTOP_RETURN" +); +static_assert( + amici::AMICI_ROOT_RETURN == CV_ROOT_RETURN, + "AMICI_ROOT_RETURN != CV_ROOT_RETURN" +); +static_assert( + amici::AMICI_ILL_INPUT == CV_ILL_INPUT, "AMICI_ILL_INPUT != CV_ILL_INPUT" +); +static_assert(amici::AMICI_NORMAL == CV_NORMAL, "AMICI_NORMAL != CV_NORMAL"); +static_assert( + amici::AMICI_ONE_STEP == CV_ONE_STEP, "AMICI_ONE_STEP != CV_ONE_STEP" +); +static_assert( + amici::AMICI_SINGULAR_JACOBIAN == SUNLS_PACKAGE_FAIL_UNREC, + "AMICI_SINGULAR_JACOBIAN != SUNLS_PACKAGE_FAIL_UNREC" +); +static_assert( + amici::AMICI_SINGULAR_JACOBIAN == SUNLS_PACKAGE_FAIL_UNREC, + "AMICI_SINGULAR_JACOBIAN != SUNLS_PACKAGE_FAIL_UNREC" +); +static_assert( + std::is_same::value, + "Definition of realtype does not match" +); namespace amici { @@ -73,8 +85,9 @@ std::unique_ptr runAmiciSimulation( solver.logger = &logger; model.logger = &logger; // prevent dangling pointer - auto _ = gsl::finally([&solver, &model] - { solver.logger = model.logger = nullptr; }); + auto _ = gsl::finally([&solver, &model] { + solver.logger = model.logger = nullptr; + }); CpuTimer cpu_timer; solver.startTimer(); @@ -83,16 +96,16 @@ std::unique_ptr runAmiciSimulation( * out of scope */ ConditionContext cc1(&model, edata, FixedParameterContext::simulation); - std::unique_ptr rdata = std::make_unique(solver, - model); - if(edata) { + std::unique_ptr rdata + = std::make_unique(solver, model); + if (edata) { rdata->id = edata->id; } - std::unique_ptr preeq {}; - std::unique_ptr fwd {}; - std::unique_ptr bwd {}; - std::unique_ptr posteq {}; + std::unique_ptr preeq{}; + std::unique_ptr fwd{}; + std::unique_ptr bwd{}; + std::unique_ptr posteq{}; // tracks whether backwards integration finished without exceptions bool bwd_success = true; @@ -106,25 +119,25 @@ std::unique_ptr runAmiciSimulation( preeq->workSteadyStateProblem(solver, model, -1); } - - fwd = std::make_unique(edata, &model, &solver, - preeq.get()); + fwd = std::make_unique( + edata, &model, &solver, preeq.get() + ); fwd->workForwardProblem(); - if (fwd->getCurrentTimeIteration() < model.nt()) { posteq = std::make_unique(solver, model); - posteq->workSteadyStateProblem(solver, model, - fwd->getCurrentTimeIteration()); + posteq->workSteadyStateProblem( + solver, model, fwd->getCurrentTimeIteration() + ); } - if (edata && solver.computingASA()) { fwd->getAdjointUpdates(model, *edata); if (posteq) { posteq->getAdjointUpdates(model, *edata); - posteq->workSteadyStateBackwardProblem(solver, model, - bwd.get()); + posteq->workSteadyStateBackwardProblem( + solver, model, bwd.get() + ); } bwd_success = false; @@ -135,19 +148,19 @@ std::unique_ptr runAmiciSimulation( bwd_success = true; if (preeq) { - ConditionContext cc2(&model, edata, - FixedParameterContext::preequilibration); - preeq->workSteadyStateBackwardProblem(solver, model, - bwd.get()); + ConditionContext cc2( + &model, edata, FixedParameterContext::preequilibration + ); + preeq->workSteadyStateBackwardProblem(solver, model, bwd.get()); } } rdata->status = AMICI_SUCCESS; } catch (amici::IntegrationFailure const& ex) { - if(ex.error_code == AMICI_RHSFUNC_FAIL && solver.timeExceeded()) { + if (ex.error_code == AMICI_RHSFUNC_FAIL && solver.timeExceeded()) { rdata->status = AMICI_MAX_TIME_EXCEEDED; - if(rethrow) + if (rethrow) throw; logger.log( LogSeverity::error, "MAXTIME_EXCEEDED", @@ -166,7 +179,7 @@ std::unique_ptr runAmiciSimulation( ); } } catch (amici::IntegrationFailureB const& ex) { - if(ex.error_code == AMICI_RHSFUNC_FAIL && solver.timeExceeded()) { + if (ex.error_code == AMICI_RHSFUNC_FAIL && solver.timeExceeded()) { rdata->status = AMICI_MAX_TIME_EXCEEDED; if (rethrow) throw; @@ -194,13 +207,13 @@ std::unique_ptr runAmiciSimulation( if (rethrow) throw; logger.log( - LogSeverity::error, "OTHER", - "AMICI simulation failed: %s", ex.what() + LogSeverity::error, "OTHER", "AMICI simulation failed: %s", + ex.what() ); logger.log( LogSeverity::debug, "BACKTRACE", "The previous error occurred at:\n%s", ex.getBacktrace() - ); + ); } catch (std::exception const& ex) { rdata->status = AMICI_ERROR; @@ -213,9 +226,9 @@ std::unique_ptr runAmiciSimulation( } rdata->processSimulationObjects( - preeq.get(), fwd.get(), - bwd_success ? bwd.get() : nullptr, - posteq.get(), model, solver, edata); + preeq.get(), fwd.get(), bwd_success ? bwd.get() : nullptr, posteq.get(), + model, solver, edata + ); rdata->cpu_time_total = cpu_timer.elapsed_milliseconds(); @@ -266,8 +279,8 @@ std::vector> runAmiciSimulations( interface */ if (skipThrough) { ConditionContext conditionContext(myModel.get(), edatas[i]); - results[i] = - std::unique_ptr(new ReturnData(solver, model)); + results[i] + = std::unique_ptr(new ReturnData(solver, model)); } else { results[i] = runAmiciSimulation(*mySolver, edatas[i], *myModel); } @@ -278,8 +291,7 @@ std::vector> runAmiciSimulations( return results; } -std::string simulation_status_to_str(int status) -{ +std::string simulation_status_to_str(int status) { try { return simulation_status_to_str_map.at(status); } catch (std::out_of_range const&) { diff --git a/src/backwardproblem.cpp b/src/backwardproblem.cpp index 4b0832d1f9..d1b89967db 100644 --- a/src/backwardproblem.cpp +++ b/src/backwardproblem.cpp @@ -1,67 +1,68 @@ #include "amici/backwardproblem.h" -#include "amici/model.h" -#include "amici/solver.h" -#include "amici/exception.h" #include "amici/edata.h" +#include "amici/exception.h" #include "amici/forwardproblem.h" -#include "amici/steadystateproblem.h" #include "amici/misc.h" +#include "amici/model.h" +#include "amici/solver.h" +#include "amici/steadystateproblem.h" -#include #include +#include namespace amici { -BackwardProblem::BackwardProblem(const ForwardProblem &fwd, - const SteadystateProblem *posteq): - model_(fwd.model), - solver_(fwd.solver), - edata_(fwd.edata), - t_(fwd.getTime()), - xB_(fwd.model->nx_solver), - dxB_(fwd.model->nx_solver), - xQB_(fwd.model->nJ*fwd.model->nplist()), - x_disc_(fwd.getStatesAtDiscontinuities()), - xdot_disc_(fwd.getRHSAtDiscontinuities()), - xdot_old_disc_(fwd.getRHSBeforeDiscontinuities()), - sx0_(fwd.getStateSensitivity()), - nroots_(fwd.getNumberOfRoots()), - discs_(fwd.getDiscontinuities()), - root_idx_(fwd.getRootIndexes()), - dJydx_(fwd.getDJydx()), - dJzdx_(fwd.getDJzdx()) { - /* complement dJydx from postequilibration. This shouldn't overwrite - * anything but only fill in previously 0 values, as only non-inf - * timepoints are filled from fwd. - */ - for (int it = 0; it < fwd.model->nt(); it++) { - if (std::isinf(fwd.model->getTimepoint(it))) { - if (!posteq) - throw AmiException("Model has non-finite timepoint but, " - "postequilibration did not run"); - - /* copy adjoint update to postequilibration */ - writeSlice(slice(posteq->getDJydx(), it, - fwd.model->nx_solver * fwd.model->nJ), - slice(dJydx_, it, - fwd.model->nx_solver * fwd.model->nJ)); - - /* If adjoint sensis were computed, copy also quadratures */ - xQB_.zero(); - xQB_ = posteq->getEquilibrationQuadratures(); - } +BackwardProblem::BackwardProblem( + ForwardProblem const& fwd, SteadystateProblem const* posteq +) + : model_(fwd.model) + , solver_(fwd.solver) + , edata_(fwd.edata) + , t_(fwd.getTime()) + , xB_(fwd.model->nx_solver) + , dxB_(fwd.model->nx_solver) + , xQB_(fwd.model->nJ * fwd.model->nplist()) + , x_disc_(fwd.getStatesAtDiscontinuities()) + , xdot_disc_(fwd.getRHSAtDiscontinuities()) + , xdot_old_disc_(fwd.getRHSBeforeDiscontinuities()) + , sx0_(fwd.getStateSensitivity()) + , nroots_(fwd.getNumberOfRoots()) + , discs_(fwd.getDiscontinuities()) + , root_idx_(fwd.getRootIndexes()) + , dJydx_(fwd.getDJydx()) + , dJzdx_(fwd.getDJzdx()) { + /* complement dJydx from postequilibration. This shouldn't overwrite + * anything but only fill in previously 0 values, as only non-inf + * timepoints are filled from fwd. + */ + for (int it = 0; it < fwd.model->nt(); it++) { + if (std::isinf(fwd.model->getTimepoint(it))) { + if (!posteq) + throw AmiException("Model has non-finite timepoint but, " + "postequilibration did not run"); + + /* copy adjoint update to postequilibration */ + writeSlice( + slice( + posteq->getDJydx(), it, fwd.model->nx_solver * fwd.model->nJ + ), + slice(dJydx_, it, fwd.model->nx_solver * fwd.model->nJ) + ); + + /* If adjoint sensis were computed, copy also quadratures */ + xQB_.zero(); + xQB_ = posteq->getEquilibrationQuadratures(); } - } - +} void BackwardProblem::workBackwardProblem() { - if (model_->nx_solver <= 0 || - solver_->getSensitivityOrder() < SensitivityOrder::first || - solver_->getSensitivityMethod() != SensitivityMethod::adjoint || - model_->nplist() == 0) { + if (model_->nx_solver <= 0 + || solver_->getSensitivityOrder() < SensitivityOrder::first + || solver_->getSensitivityMethod() != SensitivityMethod::adjoint + || model_->nplist() == 0) { return; } @@ -74,10 +75,12 @@ void BackwardProblem::workBackwardProblem() { /* initialize state vectors, depending on postequilibration */ model_->initializeB(xB_, dxB_, xQB_, it < model_->nt() - 1); - if ((it >= 0 || !discs_.empty()) && model_->getTimepoint(it) > model_->t0()) - { + if ((it >= 0 || !discs_.empty()) + && model_->getTimepoint(it) > model_->t0()) { handleDataPointB(it); - solver_->setupB(&which, model_->getTimepoint(it), model_, xB_, dxB_, xQB_); + solver_->setupB( + &which, model_->getTimepoint(it), model_, xB_, dxB_, xQB_ + ); /* for initial datapoint diagnosis needs to be stored after setup as it is not called in handleDataPointB*/ solver_->storeDiagnosisB(which); @@ -99,7 +102,7 @@ void BackwardProblem::workBackwardProblem() { } /* handle data-point */ - if (it >=0 && tnext == model_->getTimepoint(it)) { + if (it >= 0 && tnext == model_->getTimepoint(it)) { handleDataPointB(it); it--; } @@ -118,13 +121,14 @@ void BackwardProblem::workBackwardProblem() { } if (edata_ && edata_->t_presim > 0) { - ConditionContext cc(model_, edata_, FixedParameterContext::presimulation); + ConditionContext cc( + model_, edata_, FixedParameterContext::presimulation + ); solver_->runB(model_->t0() - edata_->t_presim); solver_->writeSolutionB(&t_, xB_, dxB_, xQB_, which); } } - void BackwardProblem::handleEventB() { auto rootidx = root_idx_.back(); this->root_idx_.pop_back(); @@ -144,19 +148,19 @@ void BackwardProblem::handleEventB() { continue; } - model_->addAdjointQuadratureEventUpdate(xQB_, ie, t_, x_disc, xB_, - xdot_disc, - xdot_old_disc); - model_->addAdjointStateEventUpdate(xB_, ie, t_, x_disc, - xdot_disc, - xdot_old_disc); + model_->addAdjointQuadratureEventUpdate( + xQB_, ie, t_, x_disc, xB_, xdot_disc, xdot_old_disc + ); + model_->addAdjointStateEventUpdate( + xB_, ie, t_, x_disc, xdot_disc, xdot_old_disc + ); if (model_->nz > 0) { for (int ix = 0; ix < model_->nxtrue_solver; ++ix) { for (int iJ = 0; iJ < model_->nJ; ++iJ) { - xB_[ix + iJ * model_->nxtrue_solver] += - dJzdx_[iJ + ( ix + nroots_[ie] * model_->nx_solver ) - * model_->nJ]; + xB_[ix + iJ * model_->nxtrue_solver] += dJzdx_ + [iJ + + (ix + nroots_[ie] * model_->nx_solver) * model_->nJ]; } } } @@ -167,7 +171,7 @@ void BackwardProblem::handleEventB() { model_->updateHeavisideB(rootidx.data()); } -void BackwardProblem::handleDataPointB(const int it) { +void BackwardProblem::handleDataPointB(int const it) { /* solver wasn't reset yet, as xB_ is necessary for solver setup. For initial time point (we are integrating backwards!), diagnosis needs to be stored outside this function. */ @@ -177,12 +181,12 @@ void BackwardProblem::handleDataPointB(const int it) { for (int ix = 0; ix < model_->nxtrue_solver; ix++) { for (int iJ = 0; iJ < model_->nJ; iJ++) // we only need the 1:nxtrue_solver (not the nx_true) slice here! - xB_[ix + iJ * model_->nxtrue_solver] += - dJydx_[iJ + ( ix + it * model_->nx_solver ) * model_->nJ]; + xB_[ix + iJ * model_->nxtrue_solver] + += dJydx_[iJ + (ix + it * model_->nx_solver) * model_->nJ]; } } -realtype BackwardProblem::getTnext(const int it) { +realtype BackwardProblem::getTnext(int const it) { if (it < 0 && discs_.empty()) { throw AmiException( "No more timepoints (it=%d, ie=%d) available at %f. This should " @@ -192,8 +196,8 @@ realtype BackwardProblem::getTnext(const int it) { ); } - if (!discs_.empty() && - (it < 0 || discs_.back() > model_->getTimepoint(it))) { + if (!discs_.empty() + && (it < 0 || discs_.back() > model_->getTimepoint(it))) { double tdisc = discs_.back(); return tdisc; } diff --git a/src/cblas.cpp b/src/cblas.cpp index 377ca092f7..386514ef13 100644 --- a/src/cblas.cpp +++ b/src/cblas.cpp @@ -11,33 +11,39 @@ #elif defined(AMICI_BLAS_MKL) #include #else -extern "C" -{ - #include +extern "C" { +#include } #endif namespace amici { -void amici_dgemm(BLASLayout layout, BLASTranspose TransA, - BLASTranspose TransB, const int M, const int N, - const int K, const double alpha, const double *A, - const int lda, const double *B, const int ldb, - const double beta, double *C, const int ldc) { - cblas_dgemm((CBLAS_ORDER)layout, (CBLAS_TRANSPOSE)TransA, - (CBLAS_TRANSPOSE)TransB, M, N, K, alpha, A, lda, B, ldb, beta, - C, ldc); +void amici_dgemm( + BLASLayout layout, BLASTranspose TransA, BLASTranspose TransB, int const M, + int const N, int const K, double const alpha, double const* A, + int const lda, double const* B, int const ldb, double const beta, double* C, + int const ldc +) { + cblas_dgemm( + (CBLAS_ORDER)layout, (CBLAS_TRANSPOSE)TransA, (CBLAS_TRANSPOSE)TransB, + M, N, K, alpha, A, lda, B, ldb, beta, C, ldc + ); } -void amici_dgemv(BLASLayout layout, BLASTranspose TransA, - const int M, const int N, const double alpha, const double *A, - const int lda, const double *X, const int incX, - const double beta, double *Y, const int incY) { - cblas_dgemv((CBLAS_ORDER)layout, (CBLAS_TRANSPOSE)TransA, M, N, alpha, A, - lda, X, incX, beta, Y, incY); +void amici_dgemv( + BLASLayout layout, BLASTranspose TransA, int const M, int const N, + double const alpha, double const* A, int const lda, double const* X, + int const incX, double const beta, double* Y, int const incY +) { + cblas_dgemv( + (CBLAS_ORDER)layout, (CBLAS_TRANSPOSE)TransA, M, N, alpha, A, lda, X, + incX, beta, Y, incY + ); } -void amici_daxpy(int n, double alpha, const double *x, const int incx, double *y, int incy) { +void amici_daxpy( + int n, double alpha, double const* x, int const incx, double* y, int incy +) { cblas_daxpy(n, alpha, x, incx, y, incy); } diff --git a/src/edata.cpp b/src/edata.cpp index a3ba581543..ddc3b65992 100644 --- a/src/edata.cpp +++ b/src/edata.cpp @@ -1,50 +1,56 @@ #include "amici/edata.h" -#include "amici/rdata.h" -#include "amici/symbolic_functions.h" // getNaN #include "amici/defines.h" #include "amici/model.h" +#include "amici/rdata.h" +#include "amici/symbolic_functions.h" // getNaN +#include #include #include #include -#include namespace amici { ExpData::ExpData(int nytrue, int nztrue, int nmaxevent) - : nytrue_(nytrue), nztrue_(nztrue), nmaxevent_(nmaxevent) -{ + : nytrue_(nytrue) + , nztrue_(nztrue) + , nmaxevent_(nmaxevent) { applyDimensions(); } -ExpData::ExpData(int nytrue, int nztrue, int nmaxevent, - std::vector ts) - : SimulationParameters(ts), nytrue_(nytrue), - nztrue_(nztrue), nmaxevent_(nmaxevent) -{ +ExpData::ExpData( + int nytrue, int nztrue, int nmaxevent, std::vector ts +) + : SimulationParameters(ts) + , nytrue_(nytrue) + , nztrue_(nztrue) + , nmaxevent_(nmaxevent) { applyDimensions(); } -ExpData::ExpData(int nytrue, int nztrue, int nmaxevent, - std::vector ts, - std::vector fixedParameters - ) - : SimulationParameters(ts), nytrue_(nytrue), - nztrue_(nztrue), nmaxevent_(nmaxevent) -{ +ExpData::ExpData( + int nytrue, int nztrue, int nmaxevent, std::vector ts, + std::vector fixedParameters +) + : SimulationParameters(ts) + , nytrue_(nytrue) + , nztrue_(nztrue) + , nmaxevent_(nmaxevent) { this->fixedParameters = std::move(fixedParameters); applyDimensions(); } -ExpData::ExpData(int nytrue, int nztrue, int nmaxevent, - std::vector ts, - std::vector const& observedData, - std::vector const& observedDataStdDev, - std::vector const& observedEvents, - std::vector const& observedEventsStdDev) - : SimulationParameters(ts), nytrue_(nytrue), nztrue_(nztrue), - nmaxevent_(nmaxevent) -{ +ExpData::ExpData( + int nytrue, int nztrue, int nmaxevent, std::vector ts, + std::vector const& observedData, + std::vector const& observedDataStdDev, + std::vector const& observedEvents, + std::vector const& observedEventsStdDev +) + : SimulationParameters(ts) + , nytrue_(nytrue) + , nztrue_(nztrue) + , nmaxevent_(nmaxevent) { applyDimensions(); setObservedData(observedData); setObservedDataStdDev(observedDataStdDev); @@ -52,27 +58,41 @@ ExpData::ExpData(int nytrue, int nztrue, int nmaxevent, setObservedEventsStdDev(observedEventsStdDev); } -ExpData::ExpData(Model const &model) - : ExpData(model.nytrue, model.nztrue, model.nMaxEvent(), - model.getTimepoints(), model.getFixedParameters()) { - reinitializeFixedParameterInitialStates = - model.getReinitializeFixedParameterInitialStates() - && model.getReinitializationStateIdxs().empty(); +ExpData::ExpData(Model const& model) + : ExpData( + model.nytrue, model.nztrue, model.nMaxEvent(), model.getTimepoints(), + model.getFixedParameters() + ) { + reinitializeFixedParameterInitialStates + = model.getReinitializeFixedParameterInitialStates() + && model.getReinitializationStateIdxs().empty(); reinitialization_state_idxs_sim = model.getReinitializationStateIdxs(); } ExpData::ExpData(ReturnData const& rdata, realtype sigma_y, realtype sigma_z) - : ExpData(rdata, std::vector(rdata.nytrue*rdata.nt, sigma_y), std::vector(rdata.nztrue*rdata.nmaxevent, sigma_z)) {} - -ExpData::ExpData(ReturnData const& rdata, std::vector sigma_y, - std::vector sigma_z) - : ExpData(rdata.nytrue, rdata.nztrue, rdata.nmaxevent, rdata.ts) -{ - if (sigma_y.size() != (unsigned) nytrue_ && sigma_y.size() != (unsigned) nytrue_*nt()) - throw AmiException("Dimension of sigma_y must be %d or %d, was %d", nytrue_, nytrue_*nt(), sigma_y.size()); - - if (sigma_z.size() != (unsigned) nztrue_ && sigma_z.size() != (unsigned) nztrue_*nmaxevent_) - throw AmiException("Dimension of sigma_z must be %d or %d, was %d", nztrue_, nztrue_*nmaxevent_, sigma_z.size()); + : ExpData( + rdata, std::vector(rdata.nytrue * rdata.nt, sigma_y), + std::vector(rdata.nztrue * rdata.nmaxevent, sigma_z) + ) {} + +ExpData::ExpData( + ReturnData const& rdata, std::vector sigma_y, + std::vector sigma_z +) + : ExpData(rdata.nytrue, rdata.nztrue, rdata.nmaxevent, rdata.ts) { + if (sigma_y.size() != (unsigned)nytrue_ + && sigma_y.size() != (unsigned)nytrue_ * nt()) + throw AmiException( + "Dimension of sigma_y must be %d or %d, was %d", nytrue_, + nytrue_ * nt(), sigma_y.size() + ); + + if (sigma_z.size() != (unsigned)nztrue_ + && sigma_z.size() != (unsigned)nztrue_ * nmaxevent_) + throw AmiException( + "Dimension of sigma_z must be %d or %d, was %d", nztrue_, + nztrue_ * nmaxevent_, sigma_z.size() + ); std::random_device rd{}; std::mt19937 gen{rd()}; @@ -84,18 +104,24 @@ ExpData::ExpData(ReturnData const& rdata, std::vector sigma_y, for (int iy = 0; iy < nytrue_; ++iy) { for (int it = 0; it < nt(); ++it) { - sigma = sigma_y.size() == (unsigned) nytrue_ ? sigma_y.at(iy) : sigma_y.at(iy + nytrue_ * it); + sigma = sigma_y.size() == (unsigned)nytrue_ + ? sigma_y.at(iy) + : sigma_y.at(iy + nytrue_ * it); std::normal_distribution<> e{0, sigma}; - observed_data_.at(iy + nytrue_ * it) = rdata.y.at(iy + rdata.ny * it) + e(gen); + observed_data_.at(iy + nytrue_ * it) + = rdata.y.at(iy + rdata.ny * it) + e(gen); observed_data_std_dev_.at(iy + nytrue_ * it) = sigma; } } for (int iz = 0; iz < nztrue_; ++iz) { for (int ie = 0; ie < nmaxevent_; ++ie) { - sigma = sigma_z.size() == (unsigned) nztrue_ ? sigma_z.at(iz) : sigma_z.at(iz + nztrue_ * ie); + sigma = sigma_z.size() == (unsigned)nztrue_ + ? sigma_z.at(iz) + : sigma_z.at(iz + nztrue_ * ie); std::normal_distribution<> e{0, sigma}; - observed_events_.at(iz + rdata.nztrue * ie) = rdata.z.at(iz + rdata.nz * ie) + e(gen); + observed_events_.at(iz + rdata.nztrue * ie) + = rdata.z.at(iz + rdata.nz * ie) + e(gen); observed_data_std_dev_.at(iz + rdata.nztrue * ie) = sigma; } } @@ -103,62 +129,67 @@ ExpData::ExpData(ReturnData const& rdata, std::vector sigma_y, id = rdata.id; } -void ExpData::setTimepoints(const std::vector &ts) { +void ExpData::setTimepoints(std::vector const& ts) { if (!std::is_sorted(ts.begin(), ts.end())) - throw AmiException("Encountered non-monotonic timepoints, please order timepoints such that they are monotonically increasing!"); + throw AmiException( + "Encountered non-monotonic timepoints, please order timepoints " + "such that they are monotonically increasing!" + ); ts_ = ts; applyDataDimension(); } -std::vector const& ExpData::getTimepoints() const { - return ts_; -} +std::vector const& ExpData::getTimepoints() const { return ts_; } -int ExpData::nt() const { - return gsl::narrow(ts_.size()); -} +int ExpData::nt() const { return gsl::narrow(ts_.size()); } -realtype ExpData::getTimepoint(int it) const { - return ts_.at(it); -} +realtype ExpData::getTimepoint(int it) const { return ts_.at(it); } -void ExpData::setObservedData(const std::vector &observedData) { +void ExpData::setObservedData(std::vector const& observedData) { checkDataDimension(observedData, "observedData"); - if (observedData.size() == (unsigned) nt() * nytrue_) + if (observedData.size() == (unsigned)nt() * nytrue_) observed_data_ = observedData; else if (observedData.empty()) observed_data_.clear(); } -void ExpData::setObservedData(const std::vector &observedData, int iy) { - if (observedData.size() != (unsigned) nt()) - throw AmiException("Input observedData did not match dimensions nt (%i), was %i", nt(), observedData.size()); +void ExpData::setObservedData( + std::vector const& observedData, int iy +) { + if (observedData.size() != (unsigned)nt()) + throw AmiException( + "Input observedData did not match dimensions nt (%i), was %i", nt(), + observedData.size() + ); for (int it = 0; it < nt(); ++it) - observed_data_.at(iy + it*nytrue_) = observedData.at(it); + observed_data_.at(iy + it * nytrue_) = observedData.at(it); } bool ExpData::isSetObservedData(int it, int iy) const { - return !observed_data_.empty() && !isNaN(observed_data_.at(it * nytrue_ + iy)); + return !observed_data_.empty() + && !isNaN(observed_data_.at(it * nytrue_ + iy)); } std::vector const& ExpData::getObservedData() const { return observed_data_; } -const realtype *ExpData::getObservedDataPtr(int it) const { +realtype const* ExpData::getObservedDataPtr(int it) const { if (!observed_data_.empty()) - return &observed_data_.at(it*nytrue_); + return &observed_data_.at(it * nytrue_); return nullptr; } -void ExpData::setObservedDataStdDev(const std::vector &observedDataStdDev) { +void ExpData::setObservedDataStdDev( + std::vector const& observedDataStdDev +) { checkDataDimension(observedDataStdDev, "observedDataStdDev"); checkSigmaPositivity(observedDataStdDev, "observedDataStdDev"); - if (observedDataStdDev.size() == (unsigned) nt()*nytrue_) + if (observedDataStdDev.size() == (unsigned)nt() * nytrue_) observed_data_std_dev_ = observedDataStdDev; else if (observedDataStdDev.empty()) observed_data_std_dev_.clear(); @@ -166,77 +197,95 @@ void ExpData::setObservedDataStdDev(const std::vector &observedDataStd void ExpData::setObservedDataStdDev(const realtype stdDev) { checkSigmaPositivity(stdDev, "stdDev"); - std::fill(observed_data_std_dev_.begin() ,observed_data_std_dev_.end(), stdDev); -} - -void ExpData::setObservedDataStdDev(const std::vector &observedDataStdDev, int iy) { - if (observedDataStdDev.size() != (unsigned) nt()) - throw AmiException("Input observedDataStdDev did not match dimensions nt (%i), was %i", nt(), observedDataStdDev.size()); + std::fill( + observed_data_std_dev_.begin(), observed_data_std_dev_.end(), stdDev + ); +} + +void ExpData::setObservedDataStdDev( + std::vector const& observedDataStdDev, int iy +) { + if (observedDataStdDev.size() != (unsigned)nt()) + throw AmiException( + "Input observedDataStdDev did not match dimensions nt (%i), was %i", + nt(), observedDataStdDev.size() + ); checkSigmaPositivity(observedDataStdDev, "observedDataStdDev"); for (int it = 0; it < nt(); ++it) - observed_data_std_dev_.at(iy + it*nytrue_) = observedDataStdDev.at(it); + observed_data_std_dev_.at(iy + it * nytrue_) + = observedDataStdDev.at(it); } void ExpData::setObservedDataStdDev(const realtype stdDev, int iy) { checkSigmaPositivity(stdDev, "stdDev"); for (int it = 0; it < nt(); ++it) - observed_data_std_dev_.at(iy + it*nytrue_) = stdDev; + observed_data_std_dev_.at(iy + it * nytrue_) = stdDev; } bool ExpData::isSetObservedDataStdDev(int it, int iy) const { - return !observed_data_std_dev_.empty() && !isNaN(observed_data_std_dev_.at(it * nytrue_ + iy)); + return !observed_data_std_dev_.empty() + && !isNaN(observed_data_std_dev_.at(it * nytrue_ + iy)); } std::vector const& ExpData::getObservedDataStdDev() const { return observed_data_std_dev_; } -const realtype *ExpData::getObservedDataStdDevPtr(int it) const { +realtype const* ExpData::getObservedDataStdDevPtr(int it) const { if (!observed_data_std_dev_.empty()) - return &observed_data_std_dev_.at(it*nytrue_); + return &observed_data_std_dev_.at(it * nytrue_); return nullptr; } -void ExpData::setObservedEvents(const std::vector &observedEvents) { +void ExpData::setObservedEvents(std::vector const& observedEvents) { checkEventsDimension(observedEvents, "observedEvents"); - if (observedEvents.size() == (unsigned) nmaxevent_*nztrue_) + if (observedEvents.size() == (unsigned)nmaxevent_ * nztrue_) observed_events_ = observedEvents; else if (observedEvents.empty()) observed_events_.clear(); } -void ExpData::setObservedEvents(const std::vector &observedEvents, int iz) { - if (observedEvents.size() != (unsigned) nmaxevent_) { - throw AmiException("Input observedEvents did not match dimensions nmaxevent (%i), was %i", nmaxevent_, observedEvents.size()); +void ExpData::setObservedEvents( + std::vector const& observedEvents, int iz +) { + if (observedEvents.size() != (unsigned)nmaxevent_) { + throw AmiException( + "Input observedEvents did not match dimensions nmaxevent (%i), was " + "%i", + nmaxevent_, observedEvents.size() + ); } for (int ie = 0; ie < nmaxevent_; ++ie) - observed_events_.at(iz + ie*nztrue_) = observedEvents.at(ie); + observed_events_.at(iz + ie * nztrue_) = observedEvents.at(ie); } bool ExpData::isSetObservedEvents(int ie, int iz) const { - return !observed_events_.empty() && !isNaN(observed_events_.at(ie * nztrue_ + iz)); + return !observed_events_.empty() + && !isNaN(observed_events_.at(ie * nztrue_ + iz)); } std::vector const& ExpData::getObservedEvents() const { return observed_events_; } -const realtype *ExpData::getObservedEventsPtr(int ie) const { +realtype const* ExpData::getObservedEventsPtr(int ie) const { if (!observed_events_.empty()) - return &observed_events_.at(ie*nztrue_); + return &observed_events_.at(ie * nztrue_); return nullptr; } -void ExpData::setObservedEventsStdDev(const std::vector &observedEventsStdDev) { +void ExpData::setObservedEventsStdDev( + std::vector const& observedEventsStdDev +) { checkEventsDimension(observedEventsStdDev, "observedEventsStdDev"); checkSigmaPositivity(observedEventsStdDev, "observedEventsStdDev"); - if (observedEventsStdDev.size() == (unsigned) nmaxevent_*nztrue_) + if (observedEventsStdDev.size() == (unsigned)nmaxevent_ * nztrue_) observed_events_std_dev_ = observedEventsStdDev; else if (observedEventsStdDev.empty()) observed_events_std_dev_.clear(); @@ -244,23 +293,32 @@ void ExpData::setObservedEventsStdDev(const std::vector &observedEvent void ExpData::setObservedEventsStdDev(const realtype stdDev) { checkSigmaPositivity(stdDev, "stdDev"); - std::fill(observed_events_std_dev_.begin() ,observed_events_std_dev_.end(), stdDev); -} - -void ExpData::setObservedEventsStdDev(const std::vector &observedEventsStdDev, int iz) { - if (observedEventsStdDev.size() != (unsigned) nmaxevent_) - throw AmiException("Input observedEventsStdDev did not match dimensions nmaxevent (%i), was %i", nmaxevent_, observedEventsStdDev.size()); + std::fill( + observed_events_std_dev_.begin(), observed_events_std_dev_.end(), stdDev + ); +} + +void ExpData::setObservedEventsStdDev( + std::vector const& observedEventsStdDev, int iz +) { + if (observedEventsStdDev.size() != (unsigned)nmaxevent_) + throw AmiException( + "Input observedEventsStdDev did not match dimensions nmaxevent " + "(%i), was %i", + nmaxevent_, observedEventsStdDev.size() + ); checkSigmaPositivity(observedEventsStdDev, "observedEventsStdDev"); for (int ie = 0; ie < nmaxevent_; ++ie) - observed_events_std_dev_.at(iz + ie*nztrue_) = observedEventsStdDev.at(ie); + observed_events_std_dev_.at(iz + ie * nztrue_) + = observedEventsStdDev.at(ie); } void ExpData::setObservedEventsStdDev(const realtype stdDev, int iz) { checkSigmaPositivity(stdDev, "stdDev"); for (int ie = 0; ie < nmaxevent_; ++ie) - observed_events_std_dev_.at(iz + ie*nztrue_) = stdDev; + observed_events_std_dev_.at(iz + ie * nztrue_) = stdDev; } bool ExpData::isSetObservedEventsStdDev(int ie, int iz) const { @@ -274,9 +332,9 @@ std::vector const& ExpData::getObservedEventsStdDev() const { return observed_events_std_dev_; } -const realtype *ExpData::getObservedEventsStdDevPtr(int ie) const { +realtype const* ExpData::getObservedEventsStdDevPtr(int ie) const { if (!observed_events_std_dev_.empty()) - return &observed_events_std_dev_.at(ie*nztrue_); + return &observed_events_std_dev_.at(ie * nztrue_); return nullptr; } @@ -287,194 +345,209 @@ void ExpData::applyDimensions() { } void ExpData::applyDataDimension() { - observed_data_.resize(nt()*nytrue_, getNaN()); - observed_data_std_dev_.resize(nt()*nytrue_, getNaN()); + observed_data_.resize(nt() * nytrue_, getNaN()); + observed_data_std_dev_.resize(nt() * nytrue_, getNaN()); } void ExpData::applyEventDimension() { - observed_events_.resize(nmaxevent_*nztrue_, getNaN()); - observed_events_std_dev_.resize(nmaxevent_*nztrue_, getNaN()); + observed_events_.resize(nmaxevent_ * nztrue_, getNaN()); + observed_events_std_dev_.resize(nmaxevent_ * nztrue_, getNaN()); } -void ExpData::checkDataDimension(std::vector const& input, const char *fieldname) const { - if (input.size() != (unsigned) nt()*nytrue_ && !input.empty()) - throw AmiException("Input %s did not match dimensions nt (%i) x nytrue (%i), was %i", fieldname, nt(), nytrue_, input.size()); +void ExpData::checkDataDimension( + std::vector const& input, char const* fieldname +) const { + if (input.size() != (unsigned)nt() * nytrue_ && !input.empty()) + throw AmiException( + "Input %s did not match dimensions nt (%i) x nytrue (%i), was %i", + fieldname, nt(), nytrue_, input.size() + ); } -void ExpData::checkEventsDimension(std::vector const& input, const char *fieldname) const { - if (input.size() != (unsigned) nmaxevent_*nztrue_ && !input.empty()) - throw AmiException("Input %s did not match dimensions nt (%i) x nytrue (%i), was %i", fieldname, nmaxevent_, nztrue_, input.size()); +void ExpData::checkEventsDimension( + std::vector const& input, char const* fieldname +) const { + if (input.size() != (unsigned)nmaxevent_ * nztrue_ && !input.empty()) + throw AmiException( + "Input %s did not match dimensions nt (%i) x nytrue (%i), was %i", + fieldname, nmaxevent_, nztrue_, input.size() + ); } -void checkSigmaPositivity(std::vector const& sigmaVector, const char *vectorName) { +void checkSigmaPositivity( + std::vector const& sigmaVector, char const* vectorName +) { for (auto&& sigma : sigmaVector) checkSigmaPositivity(sigma, vectorName); } -void checkSigmaPositivity(const realtype sigma, const char *sigmaName) { +void checkSigmaPositivity(const realtype sigma, char const* sigmaName) { if (sigma <= 0.0) - throw AmiException("Encountered sigma <= 0 in %s! value: %f", sigmaName, sigma); + throw AmiException( + "Encountered sigma <= 0 in %s! value: %f", sigmaName, sigma + ); } -int ExpData::nytrue() const -{ - return nytrue_; -} +int ExpData::nytrue() const { return nytrue_; } -int ExpData::nztrue() const -{ - return nztrue_; -} +int ExpData::nztrue() const { return nztrue_; } -int ExpData::nmaxevent() const -{ - return nmaxevent_; -} +int ExpData::nmaxevent() const { return nmaxevent_; } -ConditionContext::ConditionContext(Model *model, const ExpData *edata, - FixedParameterContext fpc) - : model_(model), - original_parameters_(model->getParameters()), - original_fixed_parameters_(model->getFixedParameters()), - original_tstart_(model->t0()), - original_timepoints_(model->getTimepoints()), - original_parameter_list_(model->getParameterList()), - original_scaling_(model->getParameterScale()), - original_reinitialize_fixed_parameter_initial_states_( +ConditionContext::ConditionContext( + Model* model, ExpData const* edata, FixedParameterContext fpc +) + : model_(model) + , original_parameters_(model->getParameters()) + , original_fixed_parameters_(model->getFixedParameters()) + , original_tstart_(model->t0()) + , original_timepoints_(model->getTimepoints()) + , original_parameter_list_(model->getParameterList()) + , original_scaling_(model->getParameterScale()) + , original_reinitialize_fixed_parameter_initial_states_( model->getReinitializeFixedParameterInitialStates() - && model->getReinitializationStateIdxs().empty()), - original_reinitialization_state_idxs( - model->getReinitializationStateIdxs()) -{ - if(model->hasCustomInitialStates()) + && model->getReinitializationStateIdxs().empty() + ) + , original_reinitialization_state_idxs(model->getReinitializationStateIdxs() + ) { + if (model->hasCustomInitialStates()) original_x0_ = model->getInitialStates(); - if(model->hasCustomInitialStateSensitivities()) + if (model->hasCustomInitialStateSensitivities()) original_sx0_ = model->getInitialStateSensitivities(); applyCondition(edata, fpc); } -ConditionContext::~ConditionContext() -{ - restore(); -} +ConditionContext::~ConditionContext() { restore(); } -void ConditionContext::applyCondition(const ExpData *edata, - FixedParameterContext fpc) -{ - if(!edata) +void ConditionContext::applyCondition( + ExpData const* edata, FixedParameterContext fpc +) { + if (!edata) return; // this needs to go first, otherwise nplist will not have the right // dimension for all other fields that depend on Model::nplist - if(!edata->plist.empty()) + if (!edata->plist.empty()) model_->setParameterList(edata->plist); // this needs to go second as setParameterScale will reset sx0 - if(!edata->pscale.empty()) { - if(edata->pscale.size() != (unsigned) model_->np()) - throw AmiException("Number of parameters (%d) in model does not" - " match ExpData (%zd).", - model_->np(), edata->pscale.size()); + if (!edata->pscale.empty()) { + if (edata->pscale.size() != (unsigned)model_->np()) + throw AmiException( + "Number of parameters (%d) in model does not" + " match ExpData (%zd).", + model_->np(), edata->pscale.size() + ); model_->setParameterScale(edata->pscale); } // this needs to be set in the model before handling initial state // sensitivities, which may be unscaled using model parameter values - if(!edata->parameters.empty()) { - if(edata->parameters.size() != (unsigned) model_->np()) - throw AmiException("Number of parameters (%d) in model does not" - " match ExpData (%zd).", - model_->np(), edata->parameters.size()); + if (!edata->parameters.empty()) { + if (edata->parameters.size() != (unsigned)model_->np()) + throw AmiException( + "Number of parameters (%d) in model does not" + " match ExpData (%zd).", + model_->np(), edata->parameters.size() + ); model_->setParameters(edata->parameters); } - if(!edata->x0.empty()) { - if(edata->x0.size() != (unsigned) model_->nx_rdata) - throw AmiException("Number of initial conditions (%d) in model does" - " not match ExpData (%zd).", - model_->nx_rdata, edata->x0.size()); + if (!edata->x0.empty()) { + if (edata->x0.size() != (unsigned)model_->nx_rdata) + throw AmiException( + "Number of initial conditions (%d) in model does" + " not match ExpData (%zd).", + model_->nx_rdata, edata->x0.size() + ); model_->setInitialStates(edata->x0); } - if(!edata->sx0.empty()) { - if(edata->sx0.size() != (unsigned) model_->nx_rdata * model_->nplist()) - throw AmiException("Number of initial conditions sensitivities (%d)" - " in model does not match ExpData (%zd).", - model_->nx_rdata * model_->nplist(), - edata->sx0.size()); + if (!edata->sx0.empty()) { + if (edata->sx0.size() != (unsigned)model_->nx_rdata * model_->nplist()) + throw AmiException( + "Number of initial conditions sensitivities (%d)" + " in model does not match ExpData (%zd).", + model_->nx_rdata * model_->nplist(), edata->sx0.size() + ); model_->setInitialStateSensitivities(edata->sx0); } model_->setReinitializeFixedParameterInitialStates( - edata->reinitializeFixedParameterInitialStates); + edata->reinitializeFixedParameterInitialStates + ); switch (fpc) { case FixedParameterContext::simulation: - if (!edata->fixedParameters.empty()) { - // fixed parameter in model are superseded by those provided in - // edata - if (edata->fixedParameters.size() - != (unsigned)model_->nk()) - throw AmiException("Number of fixed parameters (%d) in model does" - "not match ExpData (%zd).", - model_->nk(), edata->fixedParameters.size()); - model_->setFixedParameters(edata->fixedParameters); - if(!edata->reinitializeFixedParameterInitialStates) - model_->setReinitializationStateIdxs( - edata->reinitialization_state_idxs_sim); - } - break; + if (!edata->fixedParameters.empty()) { + // fixed parameter in model are superseded by those provided in + // edata + if (edata->fixedParameters.size() != (unsigned)model_->nk()) + throw AmiException( + "Number of fixed parameters (%d) in model does" + "not match ExpData (%zd).", + model_->nk(), edata->fixedParameters.size() + ); + model_->setFixedParameters(edata->fixedParameters); + if (!edata->reinitializeFixedParameterInitialStates) + model_->setReinitializationStateIdxs( + edata->reinitialization_state_idxs_sim + ); + } + break; case FixedParameterContext::preequilibration: - if (!edata->fixedParametersPreequilibration.empty()) { - // fixed parameter in model are superseded by those provided in - // edata - if (edata->fixedParametersPreequilibration.size() != - (unsigned)model_->nk()) - throw AmiException("Number of fixed parameters (%d) in model does" - "not match ExpData (preequilibration) (%zd).", - model_->nk(), - edata->fixedParametersPreequilibration.size()); - model_->setFixedParameters(edata->fixedParametersPreequilibration); - } - break; + if (!edata->fixedParametersPreequilibration.empty()) { + // fixed parameter in model are superseded by those provided in + // edata + if (edata->fixedParametersPreequilibration.size() + != (unsigned)model_->nk()) + throw AmiException( + "Number of fixed parameters (%d) in model does" + "not match ExpData (preequilibration) (%zd).", + model_->nk(), edata->fixedParametersPreequilibration.size() + ); + model_->setFixedParameters(edata->fixedParametersPreequilibration); + } + break; case FixedParameterContext::presimulation: - if (!edata->fixedParametersPresimulation.empty()) { - // fixed parameter in model are superseded by those provided in - // edata - if (edata->fixedParametersPresimulation.size() - != (unsigned)model_->nk()) - throw AmiException("Number of fixed parameters (%d) in model does" - " not match ExpData (presimulation) (%zd).", - model_->nk(), - edata->fixedParametersPresimulation.size()); - model_->setFixedParameters(edata->fixedParametersPresimulation); - if(!edata->reinitializeFixedParameterInitialStates) - model_->setReinitializationStateIdxs( - edata->reinitialization_state_idxs_presim); - } - break; + if (!edata->fixedParametersPresimulation.empty()) { + // fixed parameter in model are superseded by those provided in + // edata + if (edata->fixedParametersPresimulation.size() + != (unsigned)model_->nk()) + throw AmiException( + "Number of fixed parameters (%d) in model does" + " not match ExpData (presimulation) (%zd).", + model_->nk(), edata->fixedParametersPresimulation.size() + ); + model_->setFixedParameters(edata->fixedParametersPresimulation); + if (!edata->reinitializeFixedParameterInitialStates) + model_->setReinitializationStateIdxs( + edata->reinitialization_state_idxs_presim + ); + } + break; } model_->setT0(edata->tstart_); - if(edata->nt()) { + if (edata->nt()) { // fixed parameter in model are superseded by those provided in edata model_->setTimepoints(edata->getTimepoints()); } } -void ConditionContext::restore() -{ +void ConditionContext::restore() { // parameter list has to be set before initial state sensitivities model_->setParameterList(original_parameter_list_); // parameter scale has to be set before initial state sensitivities model_->setParameterScale(original_scaling_); - if(!original_x0_.empty()) + if (!original_x0_.empty()) model_->setInitialStates(original_x0_); - if(!original_sx0_.empty()) + if (!original_sx0_.empty()) model_->setUnscaledInitialStateSensitivities(original_sx0_); model_->setParameters(original_parameters_); @@ -482,10 +555,9 @@ void ConditionContext::restore() model_->setT0(original_tstart_); model_->setTimepoints(original_timepoints_); model_->setReinitializeFixedParameterInitialStates( - original_reinitialize_fixed_parameter_initial_states_); + original_reinitialize_fixed_parameter_initial_states_ + ); model_->setReinitializationStateIdxs(original_reinitialization_state_idxs); - } - } // namespace amici diff --git a/src/exception.cpp b/src/exception.cpp index 871c240c9b..3a5811e4f5 100644 --- a/src/exception.cpp +++ b/src/exception.cpp @@ -5,7 +5,6 @@ #include #include - namespace amici { AmiException::AmiException(int const first_frame) { @@ -45,13 +44,15 @@ IDAException::IDAException(int const error_code, char const* function) "IDA routine %s failed with error code %i", function, error_code ) {} -IntegrationFailure::IntegrationFailure(int code, realtype t) : - AmiException("AMICI failed to integrate the forward problem"), - error_code(code), time(t) {} +IntegrationFailure::IntegrationFailure(int code, realtype t) + : AmiException("AMICI failed to integrate the forward problem") + , error_code(code) + , time(t) {} -IntegrationFailureB::IntegrationFailureB(int code, realtype t) : - AmiException("AMICI failed to integrate the backward problem"), - error_code(code), time(t) {} +IntegrationFailureB::IntegrationFailureB(int code, realtype t) + : AmiException("AMICI failed to integrate the backward problem") + , error_code(code) + , time(t) {} NewtonFailure::NewtonFailure(int code, char const* function) : AmiException( diff --git a/src/forwardproblem.cpp b/src/forwardproblem.cpp index eae3f87c1b..c70a9074b1 100644 --- a/src/forwardproblem.cpp +++ b/src/forwardproblem.cpp @@ -1,10 +1,10 @@ #include "amici/forwardproblem.h" +#include "amici/edata.h" +#include "amici/exception.h" #include "amici/misc.h" #include "amici/model.h" #include "amici/solver.h" -#include "amici/exception.h" -#include "amici/edata.h" #include "amici/steadystateproblem.h" #include @@ -13,28 +13,29 @@ namespace amici { -ForwardProblem::ForwardProblem(const ExpData *edata, Model *model, - Solver *solver, const SteadystateProblem *preeq) - : model(model), - solver(solver), - edata(edata), - nroots_(gsl::narrow(model->ne), 0), - rootvals_(gsl::narrow(model->ne), 0.0), - rval_tmp_(gsl::narrow(model->ne), 0.0), - dJydx_(model->nJ * model->nx_solver * model->nt(), 0.0), - dJzdx_(model->nJ * model->nx_solver * model->nMaxEvent(), 0.0), - t_(model->t0()), - roots_found_(model->ne, 0), - x_(model->nx_solver), - x_old_(model->nx_solver), - dx_(model->nx_solver), - dx_old_(model->nx_solver), - xdot_(model->nx_solver), - xdot_old_(model->nx_solver), - sx_(model->nx_solver,model->nplist()), - sdx_(model->nx_solver,model->nplist()), - stau_(model->nplist()) -{ +ForwardProblem::ForwardProblem( + ExpData const* edata, Model* model, Solver* solver, + SteadystateProblem const* preeq +) + : model(model) + , solver(solver) + , edata(edata) + , nroots_(gsl::narrow(model->ne), 0) + , rootvals_(gsl::narrow(model->ne), 0.0) + , rval_tmp_(gsl::narrow(model->ne), 0.0) + , dJydx_(model->nJ * model->nx_solver * model->nt(), 0.0) + , dJzdx_(model->nJ * model->nx_solver * model->nMaxEvent(), 0.0) + , t_(model->t0()) + , roots_found_(model->ne, 0) + , x_(model->nx_solver) + , x_old_(model->nx_solver) + , dx_(model->nx_solver) + , dx_old_(model->nx_solver) + , xdot_(model->nx_solver) + , xdot_old_(model->nx_solver) + , sx_(model->nx_solver, model->nplist()) + , sdx_(model->nx_solver, model->nplist()) + , stau_(model->nplist()) { if (preeq) { x_ = preeq->getState(); sx_ = preeq->getStateSensitivity(); @@ -49,10 +50,11 @@ void ForwardProblem::workForwardProblem() { /* if preequilibration was done, model was already initialized */ if (!preequilibrated_) - model->initialize(x_, dx_, sx_, sdx_, - solver->getSensitivityOrder() >= - SensitivityOrder::first, - roots_found_); + model->initialize( + x_, dx_, sx_, sdx_, + solver->getSensitivityOrder() >= SensitivityOrder::first, + roots_found_ + ); else if (model->ne) { model->initEvents(x_, dx_, roots_found_); } @@ -63,8 +65,10 @@ void ForwardProblem::workForwardProblem() { t0 -= edata->t_presim; solver->setup(t0, model, x_, dx_, sx_, sdx_); - if (model->ne && std::any_of(roots_found_.begin(), roots_found_.end(), - [](int rf){return rf==1;})) + if (model->ne + && std::any_of(roots_found_.begin(), roots_found_.end(), [](int rf) { + return rf == 1; + })) handleEvent(&t0, false, true); /* perform presimulation if necessary */ @@ -76,8 +80,10 @@ void ForwardProblem::workForwardProblem() { t_ = model->t0(); if (model->ne) { model->initEvents(x_, dx_, roots_found_); - if (std::any_of(roots_found_.begin(), roots_found_.end(), - [](int rf){return rf==1;})) + if (std::any_of( + roots_found_.begin(), roots_found_.end(), + [](int rf) { return rf == 1; } + )) handleEvent(&t0, false, true); } } @@ -99,7 +105,7 @@ void ForwardProblem::workForwardProblem() { wont harm. when computing ASA, we only want to update here, if we didn't update before presimulation (if applicable). */ - if (solver->computingFSA() || (solver->computingASA() && !presimulate )) + if (solver->computingFSA() || (solver->computingASA() && !presimulate)) sx_ = solver->getStateSensitivity(model->t0()); /* store initial state and sensitivity*/ @@ -136,8 +142,7 @@ void ForwardProblem::workForwardProblem() { } } -void ForwardProblem::handlePresimulation() -{ +void ForwardProblem::handlePresimulation() { // Are there dedicated condition preequilibration parameters provided? ConditionContext cond(model, edata, FixedParameterContext::presimulation); solver->updateAndReinitStatesAndSensitivities(model); @@ -146,9 +151,9 @@ void ForwardProblem::handlePresimulation() solver->writeSolution(&t_, x_, dx_, sx_, dx_); } - -void ForwardProblem::handleEvent(realtype *tlastroot, const bool seflag, - const bool initial_event) { +void ForwardProblem::handleEvent( + realtype* tlastroot, bool const seflag, bool const initial_event +) { /* store Heaviside information at event occurrence */ model->froot(t_, x_, dx_, rootvals_); @@ -175,7 +180,7 @@ void ForwardProblem::handleEvent(realtype *tlastroot, const bool seflag, *tlastroot = t_; } - if(model->nz > 0) + if (model->nz > 0) storeEvent(); /* if we need to do forward sensitivities later on we need to store the old @@ -249,8 +254,7 @@ void ForwardProblem::handleEvent(realtype *tlastroot, const bool seflag, * if the secondary event has a bolus... */ if (solver->computingFSA() && solver->logger) solver->logger->log( - LogSeverity::warning, - "SECONDARY_EVENT", + LogSeverity::warning, "SECONDARY_EVENT", "Secondary event was triggered. Depending on " "the bolus of the secondary event, forward " "sensitivities can be incorrect." @@ -292,15 +296,16 @@ void ForwardProblem::storeEvent() { continue; /* only consider transitions false -> true or event filling */ - if (roots_found_.at(ie) != 1 && - t_ != model->getTimepoint(model->nt() - 1)) { + if (roots_found_.at(ie) != 1 + && t_ != model->getTimepoint(model->nt() - 1)) { continue; } if (edata && solver->computingASA()) - model->getAdjointStateEventUpdate(slice(dJzdx_, nroots_.at(ie), - model->nx_solver * model->nJ), - ie, nroots_.at(ie), t_, x_, *edata); + model->getAdjointStateEventUpdate( + slice(dJzdx_, nroots_.at(ie), model->nx_solver * model->nJ), ie, + nroots_.at(ie), t_, x_, *edata + ); nroots_.at(ie)++; } @@ -331,12 +336,12 @@ void ForwardProblem::applyEventSensiBolusFSA() { for (int ie = 0; ie < model->ne; ie++) if (roots_found_.at(ie) == 1) // only consider transitions false -> true /* */ - model->addStateSensitivityEventUpdate(sx_, ie, t_, x_old_, xdot_, - xdot_old_, stau_); + model->addStateSensitivityEventUpdate( + sx_, ie, t_, x_old_, xdot_, xdot_old_, stau_ + ); } -void ForwardProblem::getAdjointUpdates(Model &model, - const ExpData &edata) { +void ForwardProblem::getAdjointUpdates(Model& model, ExpData const& edata) { for (int it = 0; it < model.nt(); it++) { if (std::isinf(model.getTimepoint(it))) return; diff --git a/src/hdf5.cpp b/src/hdf5.cpp index 569bf3fd93..6489297ac7 100644 --- a/src/hdf5.cpp +++ b/src/hdf5.cpp @@ -9,16 +9,16 @@ #include +#include #include -#include #include -#include +#include #include +#include #include #include -#include #ifdef AMI_HDF5_H_DEBUG #ifndef __APPLE__ #include @@ -26,9 +26,8 @@ #include #endif #endif -#include #include - +#include namespace amici { namespace hdf5 { @@ -39,25 +38,27 @@ namespace hdf5 { * @param n * @param model */ -void checkMeasurementDimensionsCompatible(hsize_t m, hsize_t n, - Model const& model) { +void checkMeasurementDimensionsCompatible( + hsize_t m, hsize_t n, Model const& model +) { bool compatible = true; // if this is rank 1, n and m can be swapped if (n == 1) { - compatible &= (n == (unsigned)model.nt() || n == (unsigned)model.nytrue); - compatible &= (m == (unsigned)model.nytrue || m == (unsigned)model.nt()); + compatible + &= (n == (unsigned)model.nt() || n == (unsigned)model.nytrue); + compatible + &= (m == (unsigned)model.nytrue || m == (unsigned)model.nt()); compatible &= (m * n == (unsigned)model.nytrue * model.nt()); } else { compatible &= (n == (unsigned)model.nytrue); compatible &= (m == (unsigned)model.nt()); } - if(!compatible) + if (!compatible) throw(AmiException("HDF5 measurement data does not match model. " "Incompatible dimensions.")); } - /** * @brief assertEventDimensionsCompatible * @param m @@ -69,25 +70,26 @@ void checkEventDimensionsCompatible(hsize_t m, hsize_t n, Model const& model) { // if this is rank 1, n and m can be swapped if (n == 1) { - compatible &= (n == (unsigned)model.nMaxEvent() || - n == (unsigned)model.nztrue); - compatible &= (m == (unsigned)model.nztrue || - m == (unsigned)model.nMaxEvent()); + compatible + &= (n == (unsigned)model.nMaxEvent() || n == (unsigned)model.nztrue + ); + compatible + &= (m == (unsigned)model.nztrue || m == (unsigned)model.nMaxEvent() + ); compatible &= (m * n == (unsigned)model.nytrue * model.nMaxEvent()); } else { compatible &= (n == (unsigned)model.nztrue); compatible &= (m == (unsigned)model.nMaxEvent()); } - if(!compatible) + if (!compatible) throw(AmiException("HDF5 event data does not match model. " "Incompatible dimensions.")); } - -void createGroup(H5::H5File const& file, - std::string const& groupPath, - bool recursively) { +void createGroup( + H5::H5File const& file, std::string const& groupPath, bool recursively +) { #if H5_VERSION_GE(1, 10, 6) H5::LinkCreatPropList lcpl; lcpl.setCreateIntermediateGroup(recursively); @@ -100,178 +102,206 @@ void createGroup(H5::H5File const& file, H5Pset_create_intermediate_group(groupCreationPropertyList, 1); } - hid_t group = H5Gcreate(file.getId(), groupPath.c_str(), - groupCreationPropertyList, - H5P_DEFAULT, H5P_DEFAULT); + hid_t group = H5Gcreate( + file.getId(), groupPath.c_str(), groupCreationPropertyList, H5P_DEFAULT, + H5P_DEFAULT + ); H5Pclose(groupCreationPropertyList); if (group < 0) - throw(AmiException("Failed to create group in hdf5CreateGroup: %s", - groupPath.c_str())); + throw(AmiException( + "Failed to create group in hdf5CreateGroup: %s", groupPath.c_str() + )); H5Gclose(group); #endif } -std::unique_ptr readSimulationExpData(std::string const& hdf5Filename, - std::string const& hdf5Root, - Model const& model) { +std::unique_ptr readSimulationExpData( + std::string const& hdf5Filename, std::string const& hdf5Root, + Model const& model +) { H5::H5File file(hdf5Filename.c_str(), H5F_ACC_RDONLY); hsize_t m, n; auto edata = std::unique_ptr(new ExpData(model)); - if(attributeExists(file, hdf5Root, "id")) { + if (attributeExists(file, hdf5Root, "id")) { edata->id = getStringAttribute(file, hdf5Root, "id"); } if (model.ny * model.nt() > 0) { - if(locationExists(file, hdf5Root + "/Y")) { + if (locationExists(file, hdf5Root + "/Y")) { auto my = getDoubleDataset2D(file, hdf5Root + "/Y", m, n); checkMeasurementDimensionsCompatible(m, n, model); edata->setObservedData(my); } else { - throw AmiException("Missing %s/Y in %s", hdf5Root.c_str(), - hdf5Filename.c_str()); + throw AmiException( + "Missing %s/Y in %s", hdf5Root.c_str(), hdf5Filename.c_str() + ); } - if(locationExists(file, hdf5Root + "/Sigma_Y")) { + if (locationExists(file, hdf5Root + "/Sigma_Y")) { auto sigmay = getDoubleDataset2D(file, hdf5Root + "/Sigma_Y", m, n); checkMeasurementDimensionsCompatible(m, n, model); edata->setObservedDataStdDev(sigmay); } else { - throw AmiException("Missing %s/Sigma_Y in %s", hdf5Root.c_str(), - hdf5Filename.c_str()); + throw AmiException( + "Missing %s/Sigma_Y in %s", hdf5Root.c_str(), + hdf5Filename.c_str() + ); } } if (model.nz * model.nMaxEvent() > 0) { - if(locationExists(file, hdf5Root + "/Z")) { + if (locationExists(file, hdf5Root + "/Z")) { auto mz = getDoubleDataset2D(file, hdf5Root + "/Z", m, n); checkEventDimensionsCompatible(m, n, model); edata->setObservedEvents(mz); } else { - throw AmiException("Missing %s/Z in %s", hdf5Root.c_str(), - hdf5Filename.c_str()); + throw AmiException( + "Missing %s/Z in %s", hdf5Root.c_str(), hdf5Filename.c_str() + ); } - if(locationExists(file, hdf5Root + "/Sigma_Z")) { + if (locationExists(file, hdf5Root + "/Sigma_Z")) { auto sigmaz = getDoubleDataset2D(file, hdf5Root + "/Sigma_Z", m, n); checkEventDimensionsCompatible(m, n, model); edata->setObservedEventsStdDev(sigmaz); } else { - throw AmiException("Missing %s/Sigma_Z in %s", hdf5Root.c_str(), - hdf5Filename.c_str()); + throw AmiException( + "Missing %s/Sigma_Z in %s", hdf5Root.c_str(), + hdf5Filename.c_str() + ); } } - if(locationExists(file, hdf5Root + "/condition")) { - edata->fixedParameters = getDoubleDataset1D(file, - hdf5Root + "/condition"); + if (locationExists(file, hdf5Root + "/condition")) { + edata->fixedParameters + = getDoubleDataset1D(file, hdf5Root + "/condition"); } - if(locationExists(file, hdf5Root + "/conditionPreequilibration")) { - edata->fixedParametersPreequilibration = getDoubleDataset1D( - file, hdf5Root + "/conditionPreequilibration"); + if (locationExists(file, hdf5Root + "/conditionPreequilibration")) { + edata->fixedParametersPreequilibration + = getDoubleDataset1D(file, hdf5Root + "/conditionPreequilibration"); } - if(locationExists(file, hdf5Root + "/conditionPresimulation")) { - edata->fixedParametersPresimulation = getDoubleDataset1D( - file, hdf5Root + "/conditionPresimulation"); + if (locationExists(file, hdf5Root + "/conditionPresimulation")) { + edata->fixedParametersPresimulation + = getDoubleDataset1D(file, hdf5Root + "/conditionPresimulation"); } - if(attributeExists(file, hdf5Root, "t_presim")) { + if (attributeExists(file, hdf5Root, "t_presim")) { edata->t_presim = getDoubleScalarAttribute(file, hdf5Root, "t_presim"); } - if(locationExists(file, hdf5Root + "/ts")) { + if (locationExists(file, hdf5Root + "/ts")) { edata->setTimepoints(getDoubleDataset1D(file, hdf5Root + "/ts")); } - if(attributeExists(file, hdf5Root, - "/reinitializeFixedParameterInitialStates")) { - edata->reinitializeFixedParameterInitialStates = static_cast( - getIntScalarAttribute(file, hdf5Root, - "/reinitializeFixedParameterInitialStates")); + if (attributeExists( + file, hdf5Root, "/reinitializeFixedParameterInitialStates" + )) { + edata->reinitializeFixedParameterInitialStates + = static_cast(getIntScalarAttribute( + file, hdf5Root, "/reinitializeFixedParameterInitialStates" + )); } return edata; } -void writeSimulationExpData(const ExpData &edata, H5::H5File const& file, - const std::string &hdf5Location) -{ +void writeSimulationExpData( + ExpData const& edata, H5::H5File const& file, + std::string const& hdf5Location +) { - if(!locationExists(file, hdf5Location)) + if (!locationExists(file, hdf5Location)) createGroup(file, hdf5Location); - H5LTset_attribute_string(file.getId(), hdf5Location.c_str(), "id", - edata.id.c_str()); - + H5LTset_attribute_string( + file.getId(), hdf5Location.c_str(), "id", edata.id.c_str() + ); if (edata.nt()) - createAndWriteDouble1DDataset(file, hdf5Location + "/ts", - edata.getTimepoints()); + createAndWriteDouble1DDataset( + file, hdf5Location + "/ts", edata.getTimepoints() + ); if (!edata.fixedParameters.empty()) - createAndWriteDouble1DDataset(file, hdf5Location + "/condition", - edata.fixedParameters); + createAndWriteDouble1DDataset( + file, hdf5Location + "/condition", edata.fixedParameters + ); if (!edata.fixedParametersPreequilibration.empty()) createAndWriteDouble1DDataset( - file, hdf5Location + "/conditionPreequilibration", - edata.fixedParametersPreequilibration); + file, hdf5Location + "/conditionPreequilibration", + edata.fixedParametersPreequilibration + ); if (!edata.fixedParametersPresimulation.empty()) createAndWriteDouble1DDataset( - file, hdf5Location + "/conditionPresimulation", - edata.fixedParametersPresimulation); + file, hdf5Location + "/conditionPresimulation", + edata.fixedParametersPresimulation + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), "t_presim", - &edata.t_presim, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "t_presim", &edata.t_presim, 1 + ); if (!edata.getObservedData().empty()) createAndWriteDouble2DDataset( - file, hdf5Location + "/Y", edata.getObservedData(), - edata.nt(), edata.nytrue()); + file, hdf5Location + "/Y", edata.getObservedData(), edata.nt(), + edata.nytrue() + ); if (!edata.getObservedDataStdDev().empty()) createAndWriteDouble2DDataset( - file, hdf5Location + "/Sigma_Y", - edata.getObservedDataStdDev(), edata.nt(), edata.nytrue()); + file, hdf5Location + "/Sigma_Y", edata.getObservedDataStdDev(), + edata.nt(), edata.nytrue() + ); if (!edata.getObservedEvents().empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/Z", - edata.getObservedEvents(), - edata.nmaxevent(), edata.nztrue()); + createAndWriteDouble2DDataset( + file, hdf5Location + "/Z", edata.getObservedEvents(), + edata.nmaxevent(), edata.nztrue() + ); if (!edata.getObservedEventsStdDev().empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/Sigma_Z", - edata.getObservedEventsStdDev(), - edata.nmaxevent(), edata.nztrue()); + createAndWriteDouble2DDataset( + file, hdf5Location + "/Sigma_Z", edata.getObservedEventsStdDev(), + edata.nmaxevent(), edata.nztrue() + ); int int_attr = edata.reinitializeFixedParameterInitialStates; - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "reinitializeFixedParameterInitialStates", - &int_attr, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), + "reinitializeFixedParameterInitialStates", &int_attr, 1 + ); } -void writeReturnData(const ReturnData &rdata, H5::H5File const& file, const std::string &hdf5Location) -{ +void writeReturnData( + ReturnData const& rdata, H5::H5File const& file, + std::string const& hdf5Location +) { - if(!locationExists(file, hdf5Location)) + if (!locationExists(file, hdf5Location)) createGroup(file, hdf5Location); if (!rdata.ts.empty()) createAndWriteDouble1DDataset(file, hdf5Location + "/t", rdata.ts); - H5LTset_attribute_string(file.getId(), hdf5Location.c_str(), "id", - rdata.id.c_str()); + H5LTset_attribute_string( + file.getId(), hdf5Location.c_str(), "id", rdata.id.c_str() + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "llh", &rdata.llh, 1); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "chi2", &rdata.chi2, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "llh", &rdata.llh, 1 + ); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "chi2", &rdata.chi2, 1 + ); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "status", &rdata.status, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "status", &rdata.status, 1 + ); if (!rdata.sllh.empty()) createAndWriteDouble1DDataset(file, hdf5Location + "/sllh", rdata.sllh); @@ -279,845 +309,997 @@ void writeReturnData(const ReturnData &rdata, H5::H5File const& file, const std: if (!rdata.res.empty()) createAndWriteDouble1DDataset(file, hdf5Location + "/res", rdata.res); if (!rdata.sres.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/sres", rdata.sres, - rdata.nt*rdata.nytrue, rdata.nplist); + createAndWriteDouble2DDataset( + file, hdf5Location + "/sres", rdata.sres, rdata.nt * rdata.nytrue, + rdata.nplist + ); if (!rdata.FIM.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/FIM", - rdata.FIM, rdata.nplist, rdata.nplist); + createAndWriteDouble2DDataset( + file, hdf5Location + "/FIM", rdata.FIM, rdata.nplist, rdata.nplist + ); if (!rdata.x0.empty()) createAndWriteDouble1DDataset(file, hdf5Location + "/x0", rdata.x0); if (!rdata.x.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/x", rdata.x, - rdata.nt, rdata.nx); + createAndWriteDouble2DDataset( + file, hdf5Location + "/x", rdata.x, rdata.nt, rdata.nx + ); if (!rdata.y.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/y", rdata.y, - rdata.nt, rdata.ny); + createAndWriteDouble2DDataset( + file, hdf5Location + "/y", rdata.y, rdata.nt, rdata.ny + ); if (!rdata.z.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/z", rdata.z, - rdata.nmaxevent, rdata.nz); + createAndWriteDouble2DDataset( + file, hdf5Location + "/z", rdata.z, rdata.nmaxevent, rdata.nz + ); if (!rdata.rz.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/rz", rdata.rz, - rdata.nmaxevent, rdata.nz); + createAndWriteDouble2DDataset( + file, hdf5Location + "/rz", rdata.rz, rdata.nmaxevent, rdata.nz + ); if (!rdata.sigmay.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/sigmay", - rdata.sigmay, rdata.nt, rdata.ny); + createAndWriteDouble2DDataset( + file, hdf5Location + "/sigmay", rdata.sigmay, rdata.nt, rdata.ny + ); if (!rdata.sigmaz.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/sigmaz", - rdata.sigmaz, rdata.nmaxevent, rdata.nz); + createAndWriteDouble2DDataset( + file, hdf5Location + "/sigmaz", rdata.sigmaz, rdata.nmaxevent, + rdata.nz + ); if (!rdata.s2llh.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/s2llh", - rdata.s2llh, rdata.nJ - 1, rdata.nplist); + createAndWriteDouble2DDataset( + file, hdf5Location + "/s2llh", rdata.s2llh, rdata.nJ - 1, + rdata.nplist + ); if (!rdata.sx0.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/sx0", rdata.sx0, - rdata.nplist, rdata.nx); + createAndWriteDouble2DDataset( + file, hdf5Location + "/sx0", rdata.sx0, rdata.nplist, rdata.nx + ); if (!rdata.sx.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/sx", rdata.sx, - rdata.nt, rdata.nplist, rdata.nx); + createAndWriteDouble3DDataset( + file, hdf5Location + "/sx", rdata.sx, rdata.nt, rdata.nplist, + rdata.nx + ); if (!rdata.sy.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/sy", rdata.sy, - rdata.nt, rdata.nplist, rdata.ny); + createAndWriteDouble3DDataset( + file, hdf5Location + "/sy", rdata.sy, rdata.nt, rdata.nplist, + rdata.ny + ); if (!rdata.ssigmay.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/ssigmay", - rdata.ssigmay, rdata.nt, - rdata.nplist, rdata.ny); + createAndWriteDouble3DDataset( + file, hdf5Location + "/ssigmay", rdata.ssigmay, rdata.nt, + rdata.nplist, rdata.ny + ); if (!rdata.sz.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/sz", rdata.sz, - rdata.nmaxevent, rdata.nplist, rdata.nz); + createAndWriteDouble3DDataset( + file, hdf5Location + "/sz", rdata.sz, rdata.nmaxevent, rdata.nplist, + rdata.nz + ); if (!rdata.srz.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/srz", rdata.srz, - rdata.nmaxevent, rdata.nplist, rdata.nz); + createAndWriteDouble3DDataset( + file, hdf5Location + "/srz", rdata.srz, rdata.nmaxevent, + rdata.nplist, rdata.nz + ); if (!rdata.ssigmaz.empty()) - createAndWriteDouble3DDataset(file, hdf5Location + "/ssigmaz", - rdata.ssigmaz, - rdata.nmaxevent, rdata.nplist, rdata.nz); + createAndWriteDouble3DDataset( + file, hdf5Location + "/ssigmaz", rdata.ssigmaz, rdata.nmaxevent, + rdata.nplist, rdata.nz + ); writeReturnDataDiagnosis(rdata, file, hdf5Location + "/diagnosis"); } -void writeReturnDataDiagnosis(const ReturnData &rdata, - H5::H5File const& file, - const std::string& hdf5Location) { +void writeReturnDataDiagnosis( + ReturnData const& rdata, H5::H5File const& file, + std::string const& hdf5Location +) { - if(!locationExists(file, hdf5Location)) + if (!locationExists(file, hdf5Location)) createGroup(file, hdf5Location); if (!rdata.xdot.empty()) createAndWriteDouble1DDataset(file, hdf5Location + "/xdot", rdata.xdot); if (!rdata.numsteps.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numsteps", - rdata.numsteps); + createAndWriteInt1DDataset( + file, hdf5Location + "/numsteps", rdata.numsteps + ); if (!rdata.numrhsevals.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numrhsevals", - rdata.numrhsevals); + createAndWriteInt1DDataset( + file, hdf5Location + "/numrhsevals", rdata.numrhsevals + ); if (!rdata.numerrtestfails.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numerrtestfails", - rdata.numerrtestfails); + createAndWriteInt1DDataset( + file, hdf5Location + "/numerrtestfails", rdata.numerrtestfails + ); if (!rdata.numnonlinsolvconvfails.empty()) - createAndWriteInt1DDataset(file, - hdf5Location + "/numnonlinsolvconvfails", - rdata.numnonlinsolvconvfails); + createAndWriteInt1DDataset( + file, hdf5Location + "/numnonlinsolvconvfails", + rdata.numnonlinsolvconvfails + ); if (!rdata.order.empty()) createAndWriteInt1DDataset(file, hdf5Location + "/order", rdata.order); if (!rdata.numstepsB.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numstepsB", - rdata.numstepsB); + createAndWriteInt1DDataset( + file, hdf5Location + "/numstepsB", rdata.numstepsB + ); if (!rdata.numrhsevalsB.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numrhsevalsB", - rdata.numrhsevalsB); + createAndWriteInt1DDataset( + file, hdf5Location + "/numrhsevalsB", rdata.numrhsevalsB + ); if (!rdata.numerrtestfailsB.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/numerrtestfailsB", - rdata.numerrtestfailsB); + createAndWriteInt1DDataset( + file, hdf5Location + "/numerrtestfailsB", rdata.numerrtestfailsB + ); if (!rdata.numnonlinsolvconvfailsB.empty()) createAndWriteInt1DDataset( - file, hdf5Location + "/numnonlinsolvconvfailsB", - rdata.numnonlinsolvconvfailsB); + file, hdf5Location + "/numnonlinsolvconvfailsB", + rdata.numnonlinsolvconvfailsB + ); if (!rdata.preeq_status.empty()) { - std::vector preeq_status_int (rdata.preeq_status.size()); + std::vector preeq_status_int(rdata.preeq_status.size()); for (int i = 0; (unsigned)i < rdata.preeq_status.size(); i++) preeq_status_int[i] = static_cast(rdata.preeq_status[i]); - createAndWriteInt1DDataset(file, hdf5Location + "/preeq_status", - preeq_status_int); + createAndWriteInt1DDataset( + file, hdf5Location + "/preeq_status", preeq_status_int + ); } if (!rdata.preeq_numsteps.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/preeq_numsteps", - rdata.preeq_numsteps); + createAndWriteInt1DDataset( + file, hdf5Location + "/preeq_numsteps", rdata.preeq_numsteps + ); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "preeq_numstepsB", &rdata.preeq_numstepsB, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "preeq_numstepsB", + &rdata.preeq_numstepsB, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "preeq_cpu_time", &rdata.preeq_cpu_time, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "preeq_cpu_time", + &rdata.preeq_cpu_time, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "preeq_cpu_timeB", &rdata.preeq_cpu_timeB, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "preeq_cpu_timeB", + &rdata.preeq_cpu_timeB, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), "preeq_t", - &rdata.preeq_t, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "preeq_t", &rdata.preeq_t, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), "preeq_wrms", - &rdata.preeq_wrms, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "preeq_wrms", &rdata.preeq_wrms, 1 + ); if (!rdata.posteq_status.empty()) { - std::vector posteq_status_int (rdata.posteq_status.size()); + std::vector posteq_status_int(rdata.posteq_status.size()); for (int i = 0; (unsigned)i < rdata.posteq_status.size(); i++) posteq_status_int[i] = static_cast(rdata.posteq_status[i]); - createAndWriteInt1DDataset(file, hdf5Location + "/posteq_status", - posteq_status_int); + createAndWriteInt1DDataset( + file, hdf5Location + "/posteq_status", posteq_status_int + ); } if (!rdata.posteq_numsteps.empty()) - createAndWriteInt1DDataset(file, hdf5Location + "/posteq_numsteps", - rdata.posteq_numsteps); + createAndWriteInt1DDataset( + file, hdf5Location + "/posteq_numsteps", rdata.posteq_numsteps + ); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "posteq_numstepsB", &rdata.posteq_numstepsB, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "posteq_numstepsB", + &rdata.posteq_numstepsB, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "posteq_cpu_time", &rdata.posteq_cpu_time, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "posteq_cpu_time", + &rdata.posteq_cpu_time, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "posteq_cpu_timeB", &rdata.posteq_cpu_timeB, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "posteq_cpu_timeB", + &rdata.posteq_cpu_timeB, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), "posteq_t", - &rdata.posteq_t, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "posteq_t", &rdata.posteq_t, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), "posteq_wrms", - &rdata.posteq_wrms, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "posteq_wrms", &rdata.posteq_wrms, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "cpu_time", &rdata.cpu_time, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "cpu_time", &rdata.cpu_time, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "cpu_timeB", &rdata.cpu_timeB, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "cpu_timeB", &rdata.cpu_timeB, 1 + ); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "cpu_time_total", &rdata.cpu_time_total, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "cpu_time_total", + &rdata.cpu_time_total, 1 + ); if (!rdata.J.empty()) - createAndWriteDouble2DDataset(file, hdf5Location + "/J", rdata.J, - rdata.nx, rdata.nx); - + createAndWriteDouble2DDataset( + file, hdf5Location + "/J", rdata.J, rdata.nx, rdata.nx + ); } - -void writeReturnData(ReturnData const& rdata, - std::string const& hdf5Filename, - std::string const& hdf5Location) { +void writeReturnData( + ReturnData const& rdata, std::string const& hdf5Filename, + std::string const& hdf5Location +) { auto file = createOrOpenForWriting(hdf5Filename); writeReturnData(rdata, file, hdf5Location); } -std::string getStringAttribute(H5::H5File const& file, - std::string const& optionsObject, - std::string const& attributeName) { +std::string getStringAttribute( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +) { hsize_t dims; H5T_class_t type_class; size_t type_size; - auto status = H5LTget_attribute_info(file.getId(), optionsObject.c_str(), - attributeName.c_str(), &dims, - &type_class,&type_size); - if(status < 0) { - throw AmiException("Could get info for attribute %s for object %s.", - attributeName.c_str(), optionsObject.c_str()); + auto status = H5LTget_attribute_info( + file.getId(), optionsObject.c_str(), attributeName.c_str(), &dims, + &type_class, &type_size + ); + if (status < 0) { + throw AmiException( + "Could get info for attribute %s for object %s.", + attributeName.c_str(), optionsObject.c_str() + ); } std::vector value(type_size); - status = H5LTget_attribute_string(file.getId(), optionsObject.c_str(), - attributeName.c_str(), value.data()); + status = H5LTget_attribute_string( + file.getId(), optionsObject.c_str(), attributeName.c_str(), value.data() + ); #ifdef AMI_HDF5_H_DEBUG printf("%s: %s\n", attributeName.c_str(), value.data()); #endif - if(status < 0) - throw AmiException("Attribute %s not found for object %s.", - attributeName.c_str(), optionsObject.c_str()); + if (status < 0) + throw AmiException( + "Attribute %s not found for object %s.", attributeName.c_str(), + optionsObject.c_str() + ); return std::string(value.data()); } -double getDoubleScalarAttribute(H5::H5File const& file, - std::string const& optionsObject, - std::string const& attributeName) { +double getDoubleScalarAttribute( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +) { double data = NAN; - herr_t status = H5LTget_attribute_double(file.getId(), optionsObject.c_str(), - attributeName.c_str(), &data); + herr_t status = H5LTget_attribute_double( + file.getId(), optionsObject.c_str(), attributeName.c_str(), &data + ); #ifdef AMI_HDF5_H_DEBUG printf("%s: %e\n", attributeName.c_str(), data); #endif - if(status < 0) - throw AmiException("Attribute %s not found for object %s.", - attributeName.c_str(), optionsObject.c_str()); + if (status < 0) + throw AmiException( + "Attribute %s not found for object %s.", attributeName.c_str(), + optionsObject.c_str() + ); return data; } -int getIntScalarAttribute(H5::H5File const& file, - std::string const& optionsObject, - std::string const& attributeName) { +int getIntScalarAttribute( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +) { int data = 0; - herr_t status = H5LTget_attribute_int(file.getId(), optionsObject.c_str(), - attributeName.c_str(), &data); + herr_t status = H5LTget_attribute_int( + file.getId(), optionsObject.c_str(), attributeName.c_str(), &data + ); #ifdef AMI_HDF5_H_DEBUG printf("%s: %d\n", attributeName.c_str(), data); #endif - if(status < 0) - throw AmiException("Attribute %s not found for object %s.", - attributeName.c_str(), optionsObject.c_str()); + if (status < 0) + throw AmiException( + "Attribute %s not found for object %s.", attributeName.c_str(), + optionsObject.c_str() + ); return data; } - -void createAndWriteInt1DDataset(H5::H5File const& file, - std::string const& datasetName, - gsl::span buffer) { +void createAndWriteInt1DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer +) { hsize_t size = buffer.size(); H5::DataSpace dataspace(1, &size); - auto dataset = file.createDataSet(datasetName.c_str(), H5::PredType::NATIVE_INT, - dataspace); + auto dataset = file.createDataSet( + datasetName.c_str(), H5::PredType::NATIVE_INT, dataspace + ); dataset.write(buffer.data(), H5::PredType::NATIVE_INT); } -void createAndWriteDouble1DDataset(const H5::H5File &file, - std::string const& datasetName, - gsl::span buffer) { +void createAndWriteDouble1DDataset( + const H5::H5File& file, std::string const& datasetName, + gsl::span buffer +) { hsize_t size = buffer.size(); H5::DataSpace dataspace(1, &size); - auto dataset = file.createDataSet(datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, - dataspace); + auto dataset = file.createDataSet( + datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, dataspace + ); dataset.write(buffer.data(), H5::PredType::NATIVE_DOUBLE); } -void createAndWriteDouble2DDataset(const H5::H5File &file, - std::string const& datasetName, - gsl::span buffer, hsize_t m, - hsize_t n) { - const hsize_t adims[] {m, n}; +void createAndWriteDouble2DDataset( + const H5::H5File& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n +) { + const hsize_t adims[]{m, n}; H5::DataSpace dataspace(2, adims); - auto dataset = file.createDataSet(datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, - dataspace); + auto dataset = file.createDataSet( + datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, dataspace + ); dataset.write(buffer.data(), H5::PredType::NATIVE_DOUBLE); } -void createAndWriteInt2DDataset(H5::H5File const& file, - std::string const& datasetName, - gsl::span buffer, hsize_t m, - hsize_t n) { - const hsize_t adims[] {m, n}; +void createAndWriteInt2DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n +) { + const hsize_t adims[]{m, n}; H5::DataSpace dataspace(2, adims); - auto dataset = file.createDataSet(datasetName.c_str(), H5::PredType::NATIVE_INT, - dataspace); + auto dataset = file.createDataSet( + datasetName.c_str(), H5::PredType::NATIVE_INT, dataspace + ); dataset.write(buffer.data(), H5::PredType::NATIVE_INT); } -void createAndWriteDouble3DDataset(H5::H5File const& file, - std::string const& datasetName, - gsl::span buffer, hsize_t m, - hsize_t n, hsize_t o) { - const hsize_t adims[] {m, n, o}; +void createAndWriteDouble3DDataset( + H5::H5File const& file, std::string const& datasetName, + gsl::span buffer, hsize_t m, hsize_t n, hsize_t o +) { + const hsize_t adims[]{m, n, o}; H5::DataSpace dataspace(3, adims); - auto dataset = file.createDataSet(datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, - dataspace); + auto dataset = file.createDataSet( + datasetName.c_str(), H5::PredType::NATIVE_DOUBLE, dataspace + ); dataset.write(buffer.data(), H5::PredType::NATIVE_DOUBLE); } - -bool attributeExists(H5::H5File const& file, - const std::string &optionsObject, - const std::string &attributeName) { +bool attributeExists( + H5::H5File const& file, std::string const& optionsObject, + std::string const& attributeName +) { AMICI_H5_SAVE_ERROR_HANDLER; - int result = H5Aexists_by_name(file.getId(), optionsObject.c_str(), - attributeName.c_str(), H5P_DEFAULT); + int result = H5Aexists_by_name( + file.getId(), optionsObject.c_str(), attributeName.c_str(), H5P_DEFAULT + ); AMICI_H5_RESTORE_ERROR_HANDLER; return result > 0; } -bool attributeExists(H5::H5Object const& object, - const std::string &attributeName) { +bool attributeExists( + H5::H5Object const& object, std::string const& attributeName +) { AMICI_H5_SAVE_ERROR_HANDLER; int result = H5Aexists(object.getId(), attributeName.c_str()); AMICI_H5_RESTORE_ERROR_HANDLER; return result > 0; } -void writeSolverSettingsToHDF5(Solver const& solver, - std::string const& hdf5Filename, - std::string const& hdf5Location) { +void writeSolverSettingsToHDF5( + Solver const& solver, std::string const& hdf5Filename, + std::string const& hdf5Location +) { auto file = createOrOpenForWriting(hdf5Filename); writeSolverSettingsToHDF5(solver, file, hdf5Location); } -void writeSolverSettingsToHDF5(Solver const& solver, - H5::H5File const& file, - const std::string& hdf5Location) { - if(!locationExists(file, hdf5Location)) +void writeSolverSettingsToHDF5( + Solver const& solver, H5::H5File const& file, + std::string const& hdf5Location +) { + if (!locationExists(file, hdf5Location)) createGroup(file, hdf5Location); double dbuffer; int ibuffer; dbuffer = solver.getAbsoluteTolerance(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "atol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "atol", &dbuffer, 1 + ); dbuffer = solver.getRelativeTolerance(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "rtol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "rtol", &dbuffer, 1 + ); dbuffer = solver.getAbsoluteToleranceFSA(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "atol_fsa", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "atol_fsa", &dbuffer, 1 + ); dbuffer = solver.getRelativeToleranceFSA(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "rtol_fsa", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "rtol_fsa", &dbuffer, 1 + ); dbuffer = solver.getAbsoluteToleranceB(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "atolB", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "atolB", &dbuffer, 1 + ); dbuffer = solver.getRelativeToleranceB(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "rtolB", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "rtolB", &dbuffer, 1 + ); dbuffer = solver.getAbsoluteToleranceQuadratures(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "quad_atol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "quad_atol", &dbuffer, 1 + ); dbuffer = solver.getRelativeToleranceQuadratures(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "quad_rtol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "quad_rtol", &dbuffer, 1 + ); dbuffer = solver.getSteadyStateToleranceFactor(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_tol_factor", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_tol_factor", &dbuffer, 1 + ); dbuffer = solver.getAbsoluteToleranceSteadyState(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_atol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_atol", &dbuffer, 1 + ); dbuffer = solver.getRelativeToleranceSteadyState(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_rtol", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_rtol", &dbuffer, 1 + ); dbuffer = solver.getSteadyStateSensiToleranceFactor(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_tol_sensi_factor", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_tol_sensi_factor", &dbuffer, 1 + ); dbuffer = solver.getAbsoluteToleranceSteadyStateSensi(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_atol_sensi", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_atol_sensi", &dbuffer, 1 + ); dbuffer = solver.getRelativeToleranceSteadyStateSensi(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "ss_rtol_sensi", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "ss_rtol_sensi", &dbuffer, 1 + ); dbuffer = solver.getMaxTime(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "maxtime", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "maxtime", &dbuffer, 1 + ); ibuffer = gsl::narrow(solver.getMaxSteps()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "maxsteps", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "maxsteps", &ibuffer, 1 + ); ibuffer = gsl::narrow(solver.getMaxStepsBackwardProblem()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "maxstepsB", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "maxstepsB", &ibuffer, 1 + ); ibuffer = static_cast(solver.getLinearMultistepMethod()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "lmm", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "lmm", &ibuffer, 1 + ); ibuffer = static_cast(solver.getNonlinearSolverIteration()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "iter", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "iter", &ibuffer, 1 + ); ibuffer = static_cast(solver.getStabilityLimitFlag()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "stldet", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "stldet", &ibuffer, 1 + ); ibuffer = static_cast(solver.getStateOrdering()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "ordering", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "ordering", &ibuffer, 1 + ); ibuffer = static_cast(solver.getInterpolationType()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "interpType", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "interpType", &ibuffer, 1 + ); ibuffer = static_cast(solver.getSensitivityMethod()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "sensi_meth", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "sensi_meth", &ibuffer, 1 + ); ibuffer = static_cast(solver.getSensitivityMethodPreequilibration()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "sensi_meth_preeq", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "sensi_meth_preeq", &ibuffer, 1 + ); ibuffer = static_cast(solver.getSensitivityOrder()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "sensi", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "sensi", &ibuffer, 1 + ); ibuffer = gsl::narrow(solver.getNewtonMaxSteps()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "newton_maxsteps", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "newton_maxsteps", &ibuffer, 1 + ); ibuffer = static_cast(solver.getNewtonDampingFactorMode()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "newton_damping_factor_mode", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "newton_damping_factor_mode", + &ibuffer, 1 + ); dbuffer = solver.getNewtonDampingFactorLowerBound(); - H5LTset_attribute_double(file.getId(), hdf5Location.c_str(), - "newton_damping_factor_lower_bound", &dbuffer, 1); + H5LTset_attribute_double( + file.getId(), hdf5Location.c_str(), "newton_damping_factor_lower_bound", + &dbuffer, 1 + ); ibuffer = static_cast(solver.getLinearSolver()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "linsol", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "linsol", &ibuffer, 1 + ); ibuffer = static_cast(solver.getInternalSensitivityMethod()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "ism", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "ism", &ibuffer, 1 + ); ibuffer = static_cast(solver.getReturnDataReportingMode()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "rdrm", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "rdrm", &ibuffer, 1 + ); ibuffer = static_cast(solver.getNewtonStepSteadyStateCheck()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "newton_step_steadystate_conv", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "newton_step_steadystate_conv", + &ibuffer, 1 + ); ibuffer = static_cast(solver.getSensiSteadyStateCheck()); - H5LTset_attribute_int(file.getId(), hdf5Location.c_str(), - "check_sensi_steadystate_conv", &ibuffer, 1); + H5LTset_attribute_int( + file.getId(), hdf5Location.c_str(), "check_sensi_steadystate_conv", + &ibuffer, 1 + ); } -void readSolverSettingsFromHDF5(H5::H5File const& file, Solver &solver, - const std::string &datasetPath) { +void readSolverSettingsFromHDF5( + H5::H5File const& file, Solver& solver, std::string const& datasetPath +) { - if(attributeExists(file, datasetPath, "atol")) { + if (attributeExists(file, datasetPath, "atol")) { solver.setAbsoluteTolerance( - getDoubleScalarAttribute(file, datasetPath, "atol")); + getDoubleScalarAttribute(file, datasetPath, "atol") + ); } - if(attributeExists(file, datasetPath, "rtol")) { + if (attributeExists(file, datasetPath, "rtol")) { solver.setRelativeTolerance( - getDoubleScalarAttribute(file, datasetPath, "rtol")); + getDoubleScalarAttribute(file, datasetPath, "rtol") + ); } - if(attributeExists(file, datasetPath, "atol_fsa")) { + if (attributeExists(file, datasetPath, "atol_fsa")) { solver.setAbsoluteToleranceFSA( - getDoubleScalarAttribute(file, datasetPath, "atol_fsa")); + getDoubleScalarAttribute(file, datasetPath, "atol_fsa") + ); } - if(attributeExists(file, datasetPath, "rtol_fsa")) { + if (attributeExists(file, datasetPath, "rtol_fsa")) { solver.setRelativeToleranceFSA( - getDoubleScalarAttribute(file, datasetPath, "rtol_fsa")); + getDoubleScalarAttribute(file, datasetPath, "rtol_fsa") + ); } - if(attributeExists(file, datasetPath, "atolB")) { + if (attributeExists(file, datasetPath, "atolB")) { solver.setAbsoluteToleranceB( - getDoubleScalarAttribute(file, datasetPath, "atolB")); + getDoubleScalarAttribute(file, datasetPath, "atolB") + ); } - if(attributeExists(file, datasetPath, "rtolB")) { + if (attributeExists(file, datasetPath, "rtolB")) { solver.setRelativeToleranceB( - getDoubleScalarAttribute(file, datasetPath, "rtolB")); + getDoubleScalarAttribute(file, datasetPath, "rtolB") + ); } - if(attributeExists(file, datasetPath, "quad_atol")) { + if (attributeExists(file, datasetPath, "quad_atol")) { solver.setAbsoluteToleranceQuadratures( - getDoubleScalarAttribute(file, datasetPath, "quad_atol")); + getDoubleScalarAttribute(file, datasetPath, "quad_atol") + ); } - if(attributeExists(file, datasetPath, "quad_rtol")) { + if (attributeExists(file, datasetPath, "quad_rtol")) { solver.setRelativeToleranceQuadratures( - getDoubleScalarAttribute(file, datasetPath, "quad_rtol")); + getDoubleScalarAttribute(file, datasetPath, "quad_rtol") + ); } - if(attributeExists(file, datasetPath, "ss_tol_factor")) { + if (attributeExists(file, datasetPath, "ss_tol_factor")) { solver.setSteadyStateToleranceFactor( - getDoubleScalarAttribute(file, datasetPath, "ss_tol_factor")); + getDoubleScalarAttribute(file, datasetPath, "ss_tol_factor") + ); } - if(attributeExists(file, datasetPath, "ss_atol")) { + if (attributeExists(file, datasetPath, "ss_atol")) { solver.setAbsoluteToleranceSteadyState( - getDoubleScalarAttribute(file, datasetPath, "ss_atol")); + getDoubleScalarAttribute(file, datasetPath, "ss_atol") + ); } - if(attributeExists(file, datasetPath, "ss_rtol")) { + if (attributeExists(file, datasetPath, "ss_rtol")) { solver.setRelativeToleranceSteadyState( - getDoubleScalarAttribute(file, datasetPath, "ss_rtol")); + getDoubleScalarAttribute(file, datasetPath, "ss_rtol") + ); } - if(attributeExists(file, datasetPath, "ss_tol_sensi_factor")) { + if (attributeExists(file, datasetPath, "ss_tol_sensi_factor")) { solver.setSteadyStateSensiToleranceFactor( - getDoubleScalarAttribute(file, datasetPath, "ss_tol_sensi_factor")); + getDoubleScalarAttribute(file, datasetPath, "ss_tol_sensi_factor") + ); } - if(attributeExists(file, datasetPath, "ss_atol_sensi")) { + if (attributeExists(file, datasetPath, "ss_atol_sensi")) { solver.setAbsoluteToleranceSteadyStateSensi( - getDoubleScalarAttribute(file, datasetPath, - "ss_atol_sensi")); + getDoubleScalarAttribute(file, datasetPath, "ss_atol_sensi") + ); } - if(attributeExists(file, datasetPath, "ss_rtol_sensi")) { + if (attributeExists(file, datasetPath, "ss_rtol_sensi")) { solver.setRelativeToleranceSteadyStateSensi( - getDoubleScalarAttribute(file, datasetPath, - "ss_rtol_sensi")); + getDoubleScalarAttribute(file, datasetPath, "ss_rtol_sensi") + ); } - if(attributeExists(file, datasetPath, "maxtime")) { - solver.setMaxTime( - getDoubleScalarAttribute(file, datasetPath, "maxtime")); + if (attributeExists(file, datasetPath, "maxtime")) { + solver.setMaxTime(getDoubleScalarAttribute(file, datasetPath, "maxtime") + ); } - if(attributeExists(file, datasetPath, "maxsteps")) { - solver.setMaxSteps( - getIntScalarAttribute(file, datasetPath, "maxsteps")); + if (attributeExists(file, datasetPath, "maxsteps")) { + solver.setMaxSteps(getIntScalarAttribute(file, datasetPath, "maxsteps") + ); } - if(attributeExists(file, datasetPath, "maxstepsB")) { + if (attributeExists(file, datasetPath, "maxstepsB")) { solver.setMaxStepsBackwardProblem( - getIntScalarAttribute(file, datasetPath, "maxstepsB")); + getIntScalarAttribute(file, datasetPath, "maxstepsB") + ); } - if(attributeExists(file, datasetPath, "lmm")) { - solver.setLinearMultistepMethod( - static_cast( - getIntScalarAttribute(file, datasetPath, "lmm"))); + if (attributeExists(file, datasetPath, "lmm")) { + solver.setLinearMultistepMethod(static_cast( + getIntScalarAttribute(file, datasetPath, "lmm") + )); } - if(attributeExists(file, datasetPath, "iter")) { + if (attributeExists(file, datasetPath, "iter")) { solver.setNonlinearSolverIteration( - static_cast( - getIntScalarAttribute(file, datasetPath, "iter"))); + static_cast( + getIntScalarAttribute(file, datasetPath, "iter") + ) + ); } - if(attributeExists(file, datasetPath, "stldet")) { + if (attributeExists(file, datasetPath, "stldet")) { solver.setStabilityLimitFlag( - getIntScalarAttribute(file, datasetPath, "stldet")); + getIntScalarAttribute(file, datasetPath, "stldet") + ); } - if(attributeExists(file, datasetPath, "ordering")) { + if (attributeExists(file, datasetPath, "ordering")) { solver.setStateOrdering( - getIntScalarAttribute(file, datasetPath, "ordering")); + getIntScalarAttribute(file, datasetPath, "ordering") + ); } - if(attributeExists(file, datasetPath, "interpType")) { - solver.setInterpolationType( - static_cast( - getIntScalarAttribute(file, datasetPath, - "interpType"))); + if (attributeExists(file, datasetPath, "interpType")) { + solver.setInterpolationType(static_cast( + getIntScalarAttribute(file, datasetPath, "interpType") + )); } - if(attributeExists(file, datasetPath, "sensi_meth")) { - solver.setSensitivityMethod( - static_cast( - getIntScalarAttribute(file, datasetPath, "sensi_meth"))); + if (attributeExists(file, datasetPath, "sensi_meth")) { + solver.setSensitivityMethod(static_cast( + getIntScalarAttribute(file, datasetPath, "sensi_meth") + )); } - if(attributeExists(file, datasetPath, "sensi_meth_preeq")) { + if (attributeExists(file, datasetPath, "sensi_meth_preeq")) { solver.setSensitivityMethodPreequilibration( static_cast( - getIntScalarAttribute(file, datasetPath, "sensi_meth_preeq"))); + getIntScalarAttribute(file, datasetPath, "sensi_meth_preeq") + ) + ); } - if(attributeExists(file, datasetPath, "sensi")) { - solver.setSensitivityOrder( - static_cast( - getIntScalarAttribute(file, datasetPath, "sensi"))); + if (attributeExists(file, datasetPath, "sensi")) { + solver.setSensitivityOrder(static_cast( + getIntScalarAttribute(file, datasetPath, "sensi") + )); } - if(attributeExists(file, datasetPath, "newton_maxsteps")) { + if (attributeExists(file, datasetPath, "newton_maxsteps")) { solver.setNewtonMaxSteps( - getIntScalarAttribute(file, datasetPath, "newton_maxsteps")); + getIntScalarAttribute(file, datasetPath, "newton_maxsteps") + ); } - if(attributeExists(file, datasetPath, "newton_damping_factor_mode")) { + if (attributeExists(file, datasetPath, "newton_damping_factor_mode")) { solver.setNewtonDampingFactorMode( - static_cast( - getIntScalarAttribute(file, datasetPath, "newton_damping_factor_mode"))); + static_cast(getIntScalarAttribute( + file, datasetPath, "newton_damping_factor_mode" + )) + ); } - if(attributeExists(file, datasetPath, "newton_damping_factor_lower_bound")) { - solver.setNewtonDampingFactorLowerBound( - getDoubleScalarAttribute(file, datasetPath, "newton_damping_factor_lower_bound")); + if (attributeExists( + file, datasetPath, "newton_damping_factor_lower_bound" + )) { + solver.setNewtonDampingFactorLowerBound(getDoubleScalarAttribute( + file, datasetPath, "newton_damping_factor_lower_bound" + )); } - if(attributeExists(file, datasetPath, "linsol")) { - solver.setLinearSolver( - static_cast( - getIntScalarAttribute(file, datasetPath, "linsol"))); + if (attributeExists(file, datasetPath, "linsol")) { + solver.setLinearSolver(static_cast( + getIntScalarAttribute(file, datasetPath, "linsol") + )); } - if(attributeExists(file, datasetPath, "ism")) { + if (attributeExists(file, datasetPath, "ism")) { solver.setInternalSensitivityMethod( - static_cast( - getIntScalarAttribute(file, datasetPath, "ism"))); + static_cast( + getIntScalarAttribute(file, datasetPath, "ism") + ) + ); } - if(attributeExists(file, datasetPath, "rdrm")) { - solver.setReturnDataReportingMode( - static_cast( - getIntScalarAttribute(file, datasetPath, "rdrm"))); + if (attributeExists(file, datasetPath, "rdrm")) { + solver.setReturnDataReportingMode(static_cast( + getIntScalarAttribute(file, datasetPath, "rdrm") + )); } - if(attributeExists(file, datasetPath, "newton_step_steadystate_conv")) { - solver.setNewtonStepSteadyStateCheck( - getIntScalarAttribute(file, datasetPath, "newton_step_steadystate_conv")); + if (attributeExists(file, datasetPath, "newton_step_steadystate_conv")) { + solver.setNewtonStepSteadyStateCheck(getIntScalarAttribute( + file, datasetPath, "newton_step_steadystate_conv" + )); } - if(attributeExists(file, datasetPath, "check_sensi_steadystate_conv")) { - solver.setSensiSteadyStateCheck( - getIntScalarAttribute(file, datasetPath, "check_sensi_steadystate_conv")); + if (attributeExists(file, datasetPath, "check_sensi_steadystate_conv")) { + solver.setSensiSteadyStateCheck(getIntScalarAttribute( + file, datasetPath, "check_sensi_steadystate_conv" + )); } } -void readSolverSettingsFromHDF5(const std::string &hdffile, Solver &solver, - const std::string &datasetPath) { - H5::H5File file(hdffile.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT, - H5::FileAccPropList::DEFAULT); +void readSolverSettingsFromHDF5( + std::string const& hdffile, Solver& solver, std::string const& datasetPath +) { + H5::H5File file( + hdffile.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT, + H5::FileAccPropList::DEFAULT + ); readSolverSettingsFromHDF5(file, solver, datasetPath); } -void readModelDataFromHDF5(const std::string &hdffile, Model &model, - const std::string &datasetPath) { - H5::H5File file(hdffile.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT, - H5::FileAccPropList::DEFAULT); +void readModelDataFromHDF5( + std::string const& hdffile, Model& model, std::string const& datasetPath +) { + H5::H5File file( + hdffile.c_str(), H5F_ACC_RDONLY, H5P_DEFAULT, + H5::FileAccPropList::DEFAULT + ); readModelDataFromHDF5(file, model, datasetPath); } -void readModelDataFromHDF5(const H5::H5File &file, Model &model, - const std::string &datasetPath) { - if(attributeExists(file, datasetPath, "tstart")) { +void readModelDataFromHDF5( + const H5::H5File& file, Model& model, std::string const& datasetPath +) { + if (attributeExists(file, datasetPath, "tstart")) { model.setT0(getDoubleScalarAttribute(file, datasetPath, "tstart")); } - if(locationExists(file, datasetPath + "/pscale")) { + if (locationExists(file, datasetPath + "/pscale")) { auto pscaleInt = getIntDataset1D(file, datasetPath + "/pscale"); std::vector pscale(pscaleInt.size()); - for(int i = 0; (unsigned)i < pscaleInt.size(); ++i) + for (int i = 0; (unsigned)i < pscaleInt.size(); ++i) pscale[i] = static_cast(pscaleInt[i]); model.setParameterScale(pscale); } else if (attributeExists(file, datasetPath, "pscale")) { // if pscale is the same for all parameters, // it can be set as scalar attribute for convenience - model.setParameterScale( - static_cast( - getDoubleScalarAttribute(file, datasetPath, "pscale"))); + model.setParameterScale(static_cast( + getDoubleScalarAttribute(file, datasetPath, "pscale") + )); } - if(attributeExists(file, datasetPath, "nmaxevent")) { - model.setNMaxEvent(getIntScalarAttribute(file, datasetPath, "nmaxevent")); + if (attributeExists(file, datasetPath, "nmaxevent")) { + model.setNMaxEvent(getIntScalarAttribute(file, datasetPath, "nmaxevent") + ); } - if(attributeExists(file, datasetPath, "steadyStateSensitivityMode")) { + if (attributeExists(file, datasetPath, "steadyStateSensitivityMode")) { model.setSteadyStateSensitivityMode( - static_cast( - getIntScalarAttribute(file, datasetPath, - "steadyStateSensitivityMode"))); + static_cast(getIntScalarAttribute( + file, datasetPath, "steadyStateSensitivityMode" + )) + ); } - if(locationExists(file, datasetPath + "/theta")) { + if (locationExists(file, datasetPath + "/theta")) { model.setParameters(getDoubleDataset1D(file, datasetPath + "/theta")); } - if(locationExists(file, datasetPath + "/kappa")) { - model.setFixedParameters(getDoubleDataset1D(file, datasetPath + "/kappa")); + if (locationExists(file, datasetPath + "/kappa")) { + model.setFixedParameters( + getDoubleDataset1D(file, datasetPath + "/kappa") + ); } - if(locationExists(file, datasetPath + "/ts")) { + if (locationExists(file, datasetPath + "/ts")) { model.setTimepoints(getDoubleDataset1D(file, datasetPath + "/ts")); } - if(locationExists(file, datasetPath + "/sens_ind")) { + if (locationExists(file, datasetPath + "/sens_ind")) { auto sensInd = getIntDataset1D(file, datasetPath + "/sens_ind"); model.setParameterList(sensInd); } - if(locationExists(file, datasetPath + "/x0")) { + if (locationExists(file, datasetPath + "/x0")) { auto x0 = getDoubleDataset1D(file, datasetPath + "/x0"); - if(!x0.empty()) + if (!x0.empty()) model.setInitialStates(x0); } - if(locationExists(file, datasetPath + "/sx0")) { + if (locationExists(file, datasetPath + "/sx0")) { hsize_t length0 = 0; hsize_t length1 = 0; - auto sx0 = getDoubleDataset2D(file, datasetPath + "/sx0", - length0, length1); - if(!sx0.empty()) { - if (length0 != (unsigned) model.nplist() - && length1 != (unsigned) model.nx_rdata) - throw(AmiException("Dimension mismatch when reading sx0. " - "Expected %dx%d, got %llu, %llu.", - model.nx_rdata, model.nplist(), length0, length1)); + auto sx0 + = getDoubleDataset2D(file, datasetPath + "/sx0", length0, length1); + if (!sx0.empty()) { + if (length0 != (unsigned)model.nplist() + && length1 != (unsigned)model.nx_rdata) + throw(AmiException( + "Dimension mismatch when reading sx0. " + "Expected %dx%d, got %llu, %llu.", + model.nx_rdata, model.nplist(), length0, length1 + )); model.setUnscaledInitialStateSensitivities(sx0); } } - if(attributeExists(file, datasetPath, "sigma_res")) { + if (attributeExists(file, datasetPath, "sigma_res")) { auto sigma_res = getIntScalarAttribute(file, datasetPath, "sigma_res"); model.setAddSigmaResiduals(static_cast(sigma_res)); } - if(attributeExists(file, datasetPath, "min_sigma")) { - auto min_sigma = getDoubleScalarAttribute(file, datasetPath, - "min_sigma"); + if (attributeExists(file, datasetPath, "min_sigma")) { + auto min_sigma + = getDoubleScalarAttribute(file, datasetPath, "min_sigma"); model.setMinimumSigmaResiduals(min_sigma); } - } -H5::H5File createOrOpenForWriting(const std::string &hdf5filename) -{ +H5::H5File createOrOpenForWriting(std::string const& hdf5filename) { AMICI_H5_SAVE_ERROR_HANDLER; try { H5::H5File file(hdf5filename.c_str(), H5F_ACC_RDWR); AMICI_H5_RESTORE_ERROR_HANDLER; return file; - } catch(...) { + } catch (...) { AMICI_H5_RESTORE_ERROR_HANDLER; return H5::H5File(hdf5filename.c_str(), H5F_ACC_EXCL); } } -bool locationExists(const H5::H5File &file, const std::string &location) -{ +bool locationExists(const H5::H5File& file, std::string const& location) { AMICI_H5_SAVE_ERROR_HANDLER; auto result = H5Lexists(file.getId(), location.c_str(), H5P_DEFAULT) > 0; AMICI_H5_RESTORE_ERROR_HANDLER; return result; } -bool locationExists(const std::string &filename, const std::string &location) -{ +bool locationExists(std::string const& filename, std::string const& location) { H5::H5File file(filename.c_str(), H5F_ACC_RDONLY); return locationExists(file, location); } -std::vector getIntDataset1D(const H5::H5File &file, - std::string const& name) { +std::vector +getIntDataset1D(const H5::H5File& file, std::string const& name) { auto dataset = file.openDataSet(name.c_str()); auto dataspace = dataset.getSpace(); int rank = dataspace.getSimpleExtentNdims(); - if(rank != 1) + if (rank != 1) throw(AmiException("Expected array of rank 1 in %s", name.c_str())); hsize_t dim; dataspace.getSimpleExtentDims(&dim); std::vector result(dim); - if(!result.empty()) + if (!result.empty()) dataset.read(result.data(), H5::PredType::NATIVE_INT); return result; } - -std::vector getDoubleDataset1D(const H5::H5File &file, - const std::string &name) -{ +std::vector +getDoubleDataset1D(const H5::H5File& file, std::string const& name) { auto dataset = file.openDataSet(name.c_str()); auto dataspace = dataset.getSpace(); int rank = dataspace.getSimpleExtentNdims(); - if(rank != 1) + if (rank != 1) throw(AmiException("Expected array of rank 1 in %s", name.c_str())); hsize_t dim; dataspace.getSimpleExtentDims(&dim); std::vector result(dim); - if(!result.empty()) + if (!result.empty()) dataset.read(result.data(), H5::PredType::NATIVE_DOUBLE); return result; - } -std::vector getDoubleDataset2D(const H5::H5File &file, - const std::string &name, - hsize_t &m, hsize_t &n) -{ +std::vector getDoubleDataset2D( + const H5::H5File& file, std::string const& name, hsize_t& m, hsize_t& n +) { m = n = 0; auto dataset = file.openDataSet(name.c_str()); auto dataspace = dataset.getSpace(); int rank = dataspace.getSimpleExtentNdims(); - if(rank != 2) + if (rank != 2) throw(AmiException("Expected array of rank 2 in %s", name.c_str())); hsize_t dims[2]; @@ -1126,23 +1308,23 @@ std::vector getDoubleDataset2D(const H5::H5File &file, n = dims[1]; std::vector result(m * n); - if(!result.empty()) + if (!result.empty()) dataset.read(result.data(), H5::PredType::NATIVE_DOUBLE); return result; } -std::vector getDoubleDataset3D(const H5::H5File &file, - const std::string &name, - hsize_t &m, hsize_t &n, hsize_t &o) -{ +std::vector getDoubleDataset3D( + const H5::H5File& file, std::string const& name, hsize_t& m, hsize_t& n, + hsize_t& o +) { m = n = o = 0; auto dataset = file.openDataSet(name.c_str()); auto dataspace = dataset.getSpace(); int rank = dataspace.getSimpleExtentNdims(); - if(rank != 3) + if (rank != 3) throw(AmiException("Expected array of rank 3 in %s", name.c_str())); hsize_t dims[3]; @@ -1152,7 +1334,7 @@ std::vector getDoubleDataset3D(const H5::H5File &file, o = dims[2]; std::vector result(m * n * o); - if(!result.empty()) + if (!result.empty()) dataset.read(result.data(), H5::PredType::NATIVE_DOUBLE); return result; diff --git a/src/interface_matlab.cpp b/src/interface_matlab.cpp index 4204e5b503..3caae66a96 100644 --- a/src/interface_matlab.cpp +++ b/src/interface_matlab.cpp @@ -9,11 +9,11 @@ #include "amici/interface_matlab.h" #include "amici/amici.h" -#include "amici/model.h" -#include "amici/exception.h" #include "amici/edata.h" -#include "amici/solver.h" +#include "amici/exception.h" +#include "amici/model.h" #include "amici/returndata_matlab.h" +#include "amici/solver.h" #include #include @@ -41,7 +41,6 @@ enum mexRhsArguments { RHS_NUMARGS }; - /*! * Translates AMICI_BLAS_TRANSPOSE values to CBLAS readable strings * @@ -59,7 +58,9 @@ char amici_blasCBlasTransToBlasTrans(BLASTranspose trans) { case BLASTranspose::conjTrans: return 'C'; } - throw std::invalid_argument("Invalid argument to amici_blasCBlasTransToBlasTrans"); + throw std::invalid_argument( + "Invalid argument to amici_blasCBlasTransToBlasTrans" + ); } void amici_dgemm( @@ -79,9 +80,9 @@ void amici_dgemm( char const transA = amici_blasCBlasTransToBlasTrans(TransA); char const transB = amici_blasCBlasTransToBlasTrans(TransB); - FORTRAN_WRAPPER(dgemm)(&transA, &transB, - &M_, &N_, &K_, - &alpha, A, &lda_, B, &ldb_, &beta, C, &ldc_); + FORTRAN_WRAPPER(dgemm) + (&transA, &transB, &M_, &N_, &K_, &alpha, A, &lda_, B, &ldb_, &beta, C, + &ldc_); } void amici_dgemv( @@ -98,7 +99,8 @@ void amici_dgemv( const ptrdiff_t incY_ = incY; char const transA = amici_blasCBlasTransToBlasTrans(TransA); - FORTRAN_WRAPPER(dgemv)(&transA, &M_, &N_, &alpha, A, &lda_, X, &incX_, &beta, Y, &incY_); + FORTRAN_WRAPPER(dgemv) + (&transA, &M_, &N_, &alpha, A, &lda_, X, &incX_, &beta, Y, &incY_); } void amici_daxpy( @@ -113,10 +115,10 @@ void amici_daxpy( } /** conversion from mxArray to vector - * @param array Matlab array to create vector from - * @param length Number of elements in array - * @return std::vector with data from array - */ + * @param array Matlab array to create vector from + * @param length Number of elements in array + * @return std::vector with data from array + */ std::vector mxArrayToVector(mxArray const* array, int length) { return {mxGetPr(array), mxGetPr(array) + length}; } @@ -129,20 +131,24 @@ expDataFromMatlabCall(mxArray const* prhs[], Model const& model) { auto edata = std::make_unique(model); // Y - if (mxArray *dataY = mxGetProperty(prhs[RHS_DATA], 0, "Y")) { + if (mxArray* dataY = mxGetProperty(prhs[RHS_DATA], 0, "Y")) { auto ny_my = static_cast(mxGetN(dataY)); if (ny_my != model.nytrue) { - throw AmiException("Number of observables in data matrix (%i) does " - "not match model ny (%i)", - ny_my, model.nytrue); + throw AmiException( + "Number of observables in data matrix (%i) does " + "not match model ny (%i)", + ny_my, model.nytrue + ); } auto nt_my = static_cast(mxGetM(dataY)); if (nt_my != model.nt()) { - throw AmiException("Number of time-points in data matrix does (%i) " - "not match provided time vector (%i)", - nt_my, model.nt()); + throw AmiException( + "Number of time-points in data matrix does (%i) " + "not match provided time vector (%i)", + nt_my, model.nt() + ); } - mxArray *dataYT; + mxArray* dataYT; mexCallMATLAB(1, &dataYT, 1, &dataY, "transpose"); auto observedData = mxArrayToVector(dataYT, ny_my * nt_my); edata->setObservedData(observedData); @@ -152,43 +158,54 @@ expDataFromMatlabCall(mxArray const* prhs[], Model const& model) { } // Sigma Y - if (mxArray *dataSigmaY = mxGetProperty(prhs[RHS_DATA], 0, "Sigma_Y")) { + if (mxArray* dataSigmaY = mxGetProperty(prhs[RHS_DATA], 0, "Sigma_Y")) { auto ny_sigmay = static_cast(mxGetN(dataSigmaY)); if (ny_sigmay != model.nytrue) { - throw AmiException("Number of observables in data-sigma matrix (%i) " - "does not match model ny (%i)", - ny_sigmay, model.nytrue); + throw AmiException( + "Number of observables in data-sigma matrix (%i) " + "does not match model ny (%i)", + ny_sigmay, model.nytrue + ); } auto nt_sigmay = static_cast(mxGetM(dataSigmaY)); if (nt_sigmay != model.nt()) { - throw AmiException("Number of time-points in data-sigma matrix (%i) " - "does not match provided time vector (%i)", - nt_sigmay, model.nt()); + throw AmiException( + "Number of time-points in data-sigma matrix (%i) " + "does not match provided time vector (%i)", + nt_sigmay, model.nt() + ); } - mxArray *dataSigmaYT; + mxArray* dataSigmaYT; mexCallMATLAB(1, &dataSigmaYT, 1, &dataSigmaY, "transpose"); - auto observedDataSigma = mxArrayToVector(dataSigmaYT, ny_sigmay * nt_sigmay); + auto observedDataSigma + = mxArrayToVector(dataSigmaYT, ny_sigmay * nt_sigmay); edata->setObservedDataStdDev(observedDataSigma); } else { - throw AmiException("Field Sigma_Y not specified as field in data struct!"); + throw AmiException( + "Field Sigma_Y not specified as field in data struct!" + ); } // Z - if (mxArray *dataZ = mxGetProperty(prhs[RHS_DATA], 0, "Z")) { + if (mxArray* dataZ = mxGetProperty(prhs[RHS_DATA], 0, "Z")) { auto nz_mz = static_cast(mxGetN(dataZ)); if (nz_mz != model.nztrue) { - throw AmiException("Number of events in event matrix (%i) does not " - "match provided nz (%i)", - nz_mz, model.nztrue); + throw AmiException( + "Number of events in event matrix (%i) does not " + "match provided nz (%i)", + nz_mz, model.nztrue + ); } auto ne_mz = static_cast(mxGetM(dataZ)); if (ne_mz != model.nMaxEvent()) { - throw AmiException("Number of time-points in event matrix (%i) does " - "not match provided nmaxevent (%i)", - ne_mz, model.nMaxEvent()); + throw AmiException( + "Number of time-points in event matrix (%i) does " + "not match provided nmaxevent (%i)", + ne_mz, model.nMaxEvent() + ); } - mxArray *dataZT; + mxArray* dataZT; mexCallMATLAB(1, &dataZT, 1, &dataZ, "transpose"); auto observedEvents = mxArrayToVector(dataZT, nz_mz * ne_mz); edata->setObservedEvents(observedEvents); @@ -197,137 +214,192 @@ expDataFromMatlabCall(mxArray const* prhs[], Model const& model) { } // Sigma Z - if (mxArray *dataSigmaZ = mxGetProperty(prhs[RHS_DATA], 0, "Sigma_Z")) { + if (mxArray* dataSigmaZ = mxGetProperty(prhs[RHS_DATA], 0, "Sigma_Z")) { auto nz_sigmaz = static_cast(mxGetN(dataSigmaZ)); if (nz_sigmaz != model.nztrue) { - throw AmiException("Number of events in event-sigma matrix (%i) does " - "not match provided nz (%i)", - nz_sigmaz, model.nztrue); + throw AmiException( + "Number of events in event-sigma matrix (%i) does " + "not match provided nz (%i)", + nz_sigmaz, model.nztrue + ); } auto ne_sigmaz = static_cast(mxGetM(dataSigmaZ)); if (ne_sigmaz != model.nMaxEvent()) { - throw AmiException("Number of time-points in event-sigma matrix (%i) " - "does not match provided nmaxevent (%i)", - ne_sigmaz, model.nMaxEvent()); + throw AmiException( + "Number of time-points in event-sigma matrix (%i) " + "does not match provided nmaxevent (%i)", + ne_sigmaz, model.nMaxEvent() + ); } - mxArray *dataSigmaZT; + mxArray* dataSigmaZT; mexCallMATLAB(1, &dataSigmaZT, 1, &dataSigmaZ, "transpose"); - auto observedEventsSigma = mxArrayToVector(dataSigmaZT, nz_sigmaz * ne_sigmaz); + auto observedEventsSigma + = mxArrayToVector(dataSigmaZT, nz_sigmaz * ne_sigmaz); edata->setObservedEventsStdDev(observedEventsSigma); } else { - throw AmiException("Field Sigma_Z not specified as field in data struct!"); - + throw AmiException( + "Field Sigma_Z not specified as field in data struct!" + ); } // preequilibration condition parameters - if (mxArray *dataPreeq = mxGetProperty(prhs[RHS_DATA], 0, "conditionPreequilibration")) { + if (mxArray* dataPreeq + = mxGetProperty(prhs[RHS_DATA], 0, "conditionPreequilibration")) { int m = (int)mxGetM(dataPreeq); int n = (int)mxGetN(dataPreeq); - if(m * n > 0) { + if (m * n > 0) { if (m * n != model.nk() || (m != 1 && n != 1)) { - throw AmiException("Number of preequilibration parameters (%dx%d) does " - "not match model (%d)", m, n, model.nk()); + throw AmiException( + "Number of preequilibration parameters (%dx%d) does " + "not match model (%d)", + m, n, model.nk() + ); } - edata->fixedParametersPreequilibration = - std::vector(mxGetPr(dataPreeq), mxGetPr(dataPreeq) + m * n); + edata->fixedParametersPreequilibration = std::vector( + mxGetPr(dataPreeq), mxGetPr(dataPreeq) + m * n + ); } } // preequilibration condition parameters if (mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")) - edata->reinitializeFixedParameterInitialStates = - static_cast(mxGetScalar(mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates"))); + edata->reinitializeFixedParameterInitialStates = static_cast( + mxGetScalar(mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")) + ); return edata; } /** conversion from double to int with checking for loss of data - * @param x input - * @return int_x casted value - */ + * @param x input + * @return int_x casted value + */ int dbl2int(double const x) { - if((std::round(x)-x) != 0.0) + if ((std::round(x) - x) != 0.0) throw AmiException("Invalid non-integer value for integer option"); - return(static_cast(x)); + return (static_cast(x)); } void setSolverOptions(mxArray const* prhs[], int nrhs, Solver& solver) { if (mxGetPr(prhs[RHS_OPTIONS])) { if (mxGetProperty(prhs[RHS_OPTIONS], 0, "atol")) { - solver.setAbsoluteTolerance(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "atol"))); + solver.setAbsoluteTolerance( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "atol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "rtol")) { - solver.setRelativeTolerance(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "rtol"))); + solver.setRelativeTolerance( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "rtol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_atol")) { - solver.setAbsoluteToleranceQuadratures(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_atol"))); + solver.setAbsoluteToleranceQuadratures( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_atol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_rtol")) { - solver.setRelativeToleranceQuadratures(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_rtol"))); + solver.setRelativeToleranceQuadratures( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "quad_rtol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_atol")) { - solver.setAbsoluteToleranceQuadratures(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_atol"))); + solver.setAbsoluteToleranceQuadratures( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_atol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_rtol")) { - solver.setRelativeToleranceQuadratures(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_rtol"))); + solver.setRelativeToleranceQuadratures( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ss_rtol")) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "maxsteps")) { - solver.setMaxSteps(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "maxsteps")))); + solver.setMaxSteps(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "maxsteps")) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "maxstepsB")) { - solver.setMaxStepsBackwardProblem(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "maxstepsB")))); + solver.setMaxStepsBackwardProblem(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "maxstepsB")) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "lmm")) { - solver.setLinearMultistepMethod(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "lmm"))))); + solver.setLinearMultistepMethod(static_cast( + dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "lmm"))) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "iter")) { - solver.setNonlinearSolverIteration(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "iter"))))); + solver.setNonlinearSolverIteration( + static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "iter")) + )) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "interpType")) { - solver.setInterpolationType(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "interpType"))))); + solver.setInterpolationType(static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "interpType")) + ))); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "linsol")) { - solver.setLinearSolver(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "linsol"))))); + solver.setLinearSolver(static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "linsol")) + ))); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi")) { - solver.setSensitivityOrder(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi"))))); + solver.setSensitivityOrder(static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi")) + ))); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "ism")) { - solver.setInternalSensitivityMethod(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ism"))))); + solver.setInternalSensitivityMethod( + static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ism")) + )) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth")) { - solver.setSensitivityMethod(static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth"))))); + solver.setSensitivityMethod(static_cast(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth")) + ))); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth_preeq")) { solver.setSensitivityMethodPreequilibration( - static_cast(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth_preeq"))))); + static_cast(dbl2int(mxGetScalar( + mxGetProperty(prhs[RHS_OPTIONS], 0, "sensi_meth_preeq") + ))) + ); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "ordering")) { - solver.setStateOrdering(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ordering")))); + solver.setStateOrdering(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "ordering")) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "stldet")) { - solver.setStabilityLimitFlag(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "stldet")))); + solver.setStabilityLimitFlag(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "stldet")) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "newton_maxsteps")) { - solver.setNewtonMaxSteps(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "newton_maxsteps")))); + solver.setNewtonMaxSteps(dbl2int(mxGetScalar( + mxGetProperty(prhs[RHS_OPTIONS], 0, "newton_maxsteps") + ))); } } } @@ -335,22 +407,30 @@ void setSolverOptions(mxArray const* prhs[], int nrhs, Solver& solver) { void setModelData(mxArray const* prhs[], int nrhs, Model& model) { if (mxGetPr(prhs[RHS_OPTIONS])) { if (mxGetProperty(prhs[RHS_OPTIONS], 0, "nmaxevent")) { - model.setNMaxEvent(dbl2int(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "nmaxevent")))); + model.setNMaxEvent(dbl2int( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "nmaxevent")) + )); } if (mxGetProperty(prhs[RHS_OPTIONS], 0, "tstart")) { - model.setT0(mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "tstart"))); + model.setT0( + mxGetScalar(mxGetProperty(prhs[RHS_OPTIONS], 0, "tstart")) + ); } - if (mxArray *a = mxGetProperty(prhs[RHS_OPTIONS], 0, "pscale")) { - if(mxGetM(a) == 1 && mxGetN(a) == 1) { - model.setParameterScale(static_cast(dbl2int(mxGetScalar(a)))); + if (mxArray* a = mxGetProperty(prhs[RHS_OPTIONS], 0, "pscale")) { + if (mxGetM(a) == 1 && mxGetN(a) == 1) { + model.setParameterScale( + static_cast(dbl2int(mxGetScalar(a))) + ); } else if((mxGetM(a) == 1 && gsl::narrow(mxGetN(a)) == model.np()) || (mxGetN(a) == 1 && gsl::narrow(mxGetM(a)) == model.np())) { - auto pscaleArray = static_cast(mxGetData(a)); + auto pscaleArray = static_cast(mxGetData(a)); std::vector pscale(model.np()); - for(int ip = 0; ip < model.np(); ++ip) { - pscale[ip] = static_cast(dbl2int(pscaleArray[ip])); + for (int ip = 0; ip < model.np(); ++ip) { + pscale[ip] + = static_cast(dbl2int(pscaleArray[ip]) + ); } model.setParameterScale(pscale); } else { @@ -359,13 +439,13 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { } } - if (prhs[RHS_TIMEPOINTS] && - mxGetM(prhs[RHS_TIMEPOINTS]) * mxGetN(prhs[RHS_TIMEPOINTS]) > 0) { - model.setTimepoints(std::vector( - mxGetPr(prhs[RHS_TIMEPOINTS]), - mxGetPr(prhs[RHS_TIMEPOINTS]) - + (int)mxGetM(prhs[RHS_TIMEPOINTS]) * mxGetN(prhs[RHS_TIMEPOINTS]))); - + if (prhs[RHS_TIMEPOINTS] + && mxGetM(prhs[RHS_TIMEPOINTS]) * mxGetN(prhs[RHS_TIMEPOINTS]) > 0) { + model.setTimepoints(std::vector( + mxGetPr(prhs[RHS_TIMEPOINTS]), + mxGetPr(prhs[RHS_TIMEPOINTS] + ) + (int)mxGetM(prhs[RHS_TIMEPOINTS]) * mxGetN(prhs[RHS_TIMEPOINTS]) + )); } if (model.np() > 0) { @@ -374,9 +454,12 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { mxGetM(prhs[RHS_PARAMETERS]) * mxGetN(prhs[RHS_PARAMETERS]) ) == model.np()) { - model.setParameters(std::vector(mxGetPr(prhs[RHS_PARAMETERS]), - mxGetPr(prhs[RHS_PARAMETERS]) - + mxGetM(prhs[RHS_PARAMETERS]) * mxGetN(prhs[RHS_PARAMETERS]))); + model.setParameters(std::vector( + mxGetPr(prhs[RHS_PARAMETERS]), + mxGetPr(prhs[RHS_PARAMETERS]) + + mxGetM(prhs[RHS_PARAMETERS]) + * mxGetN(prhs[RHS_PARAMETERS]) + )); } } } @@ -387,28 +470,35 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { mxGetM(prhs[RHS_CONSTANTS]) * mxGetN(prhs[RHS_CONSTANTS]) ) == model.nk()) { - model.setFixedParameters(std::vector(mxGetPr(prhs[RHS_CONSTANTS]), - mxGetPr(prhs[RHS_CONSTANTS]) - + mxGetM(prhs[RHS_CONSTANTS]) * mxGetN(prhs[RHS_CONSTANTS]))); + model.setFixedParameters(std::vector( + mxGetPr(prhs[RHS_CONSTANTS]), + mxGetPr(prhs[RHS_CONSTANTS]) + + mxGetM(prhs[RHS_CONSTANTS]) + * mxGetN(prhs[RHS_CONSTANTS]) + )); } } } if (mxGetPr(prhs[RHS_PLIST])) { - model.setParameterList(std::vector(mxGetPr(prhs[RHS_PLIST]), - mxGetPr(prhs[RHS_PLIST]) - + mxGetM(prhs[RHS_PLIST]) * mxGetN(prhs[RHS_PLIST]))); + model.setParameterList(std::vector( + mxGetPr(prhs[RHS_PLIST]), + mxGetPr(prhs[RHS_PLIST]) + + mxGetM(prhs[RHS_PLIST]) * mxGetN(prhs[RHS_PLIST]) + )); } else { model.requireSensitivitiesForAllParameters(); } /* Check, if initial states and sensitivities are passed by user or must be - * calculated */ + * calculated */ if (mxGetPr(prhs[RHS_INITIALIZATION])) { - mxArray *x0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "x0"); + mxArray* x0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "x0"); if (x0 && (mxGetM(x0) * mxGetN(x0)) > 0) { /* check dimensions */ if (mxGetN(x0) != 1) { - throw AmiException("Number of rows in x0 field must be equal to 1!"); + throw AmiException( + "Number of rows in x0 field must be equal to 1!" + ); } if (gsl::narrow(mxGetM(x0)) != model.nx_rdata) { throw AmiException("Number of columns in x0 field " @@ -419,13 +509,15 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { } /* Check, if initial states and sensitivities are passed by user or must be - * calculated */ + * calculated */ if (mxGetPr(prhs[RHS_INITIALIZATION])) { - mxArray *x0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "x0"); + mxArray* x0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "x0"); if (x0 && (mxGetM(x0) * mxGetN(x0)) > 0) { /* check dimensions */ if (mxGetN(x0) != 1) { - throw AmiException("Number of rows in x0 field must be equal to 1!"); + throw AmiException( + "Number of rows in x0 field must be equal to 1!" + ); } if (gsl::narrow(mxGetM(x0)) != model.nx_rdata) { throw AmiException("Number of columns in x0 field " @@ -433,11 +525,12 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { "model states!"); } - model.setInitialStates(std::vector(mxGetPr(x0), - mxGetPr(x0) + mxGetM(x0) * mxGetN(x0))); + model.setInitialStates(std::vector( + mxGetPr(x0), mxGetPr(x0) + mxGetM(x0) * mxGetN(x0) + )); } - mxArray *sx0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "sx0"); + mxArray* sx0 = mxGetField(prhs[RHS_INITIALIZATION], 0, "sx0"); if (sx0 && (mxGetM(sx0) * mxGetN(sx0)) > 0) { /* check dimensions */ if (gsl::narrow(mxGetN(sx0)) != model.nplist()) { @@ -450,14 +543,17 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { "field does not agree with " "number of model states!"); } - model.setInitialStateSensitivities(std::vector(mxGetPr(sx0), - mxGetPr(sx0) + mxGetM(sx0) * mxGetN(sx0))); + model.setInitialStateSensitivities(std::vector( + mxGetPr(sx0), mxGetPr(sx0) + mxGetM(sx0) * mxGetN(sx0) + )); } } // preequilibration condition parameters - if (mxGetPr(prhs[RHS_DATA]) && mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")) - model.setReinitializeFixedParameterInitialStates( - static_cast(mxGetScalar(mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")))); + if (mxGetPr(prhs[RHS_DATA]) + && mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")) + model.setReinitializeFixedParameterInitialStates(static_cast( + mxGetScalar(mxGetProperty(prhs[RHS_DATA], 0, "reinitializeStates")) + )); } } // namespace amici @@ -475,11 +571,15 @@ void setModelData(mxArray const* prhs[], int nrhs, Model& model) { */ void mexFunction(int nlhs, mxArray* plhs[], int nrhs, mxArray const* prhs[]) { if (nlhs != 1) { - mexErrMsgIdAndTxt("AMICI:mex:setup", - "Incorrect number of output arguments (must be 1)!"); - } else if(nrhs < amici::RHS_NUMARGS_REQUIRED) { - mexErrMsgIdAndTxt("AMICI:mex:setup", - "Incorrect number of input arguments (must be at least 7)!"); + mexErrMsgIdAndTxt( + "AMICI:mex:setup", + "Incorrect number of output arguments (must be 1)!" + ); + } else if (nrhs < amici::RHS_NUMARGS_REQUIRED) { + mexErrMsgIdAndTxt( + "AMICI:mex:setup", + "Incorrect number of input arguments (must be at least 7)!" + ); }; auto model = amici::generic_model::getModel(); @@ -492,18 +592,20 @@ void mexFunction(int nlhs, mxArray* plhs[], int nrhs, mxArray const* prhs[]) { try { edata = amici::expDataFromMatlabCall(prhs, *model); } catch (amici::AmiException const& ex) { - mexErrMsgIdAndTxt("AMICI:mex:setup","Failed to read experimental data:\n%s",ex.what()); + mexErrMsgIdAndTxt( + "AMICI:mex:setup", "Failed to read experimental data:\n%s", + ex.what() + ); } - } else if (solver->getSensitivityOrder() >= amici::SensitivityOrder::first && - solver->getSensitivityMethod() == amici::SensitivityMethod::adjoint) { - mexErrMsgIdAndTxt("AMICI:mex:setup","No data provided!"); + } else if (solver->getSensitivityOrder() >= amici::SensitivityOrder::first && solver->getSensitivityMethod() == amici::SensitivityMethod::adjoint) { + mexErrMsgIdAndTxt("AMICI:mex:setup", "No data provided!"); } /* ensures that plhs[0] is available */ auto rdata = amici::runAmiciSimulation(*solver, edata.get(), *model); plhs[0] = getReturnDataMatlabFromAmiciCall(rdata.get()); - for(auto const& msg: rdata->messages) { + for (auto const& msg : rdata->messages) { auto identifier = "AMICI:simulation:" + msg.identifier; mexWarnMsgIdAndTxt(identifier.c_str(), msg.message.c_str()); } diff --git a/src/logging.cpp b/src/logging.cpp index 7e5d861486..e974612ea5 100644 --- a/src/logging.cpp +++ b/src/logging.cpp @@ -5,17 +5,16 @@ namespace amici { -void Logger::log(LogSeverity severity, - std::string const& identifier, - std::string const& message) -{ +void Logger::log( + LogSeverity severity, std::string const& identifier, + std::string const& message +) { items.emplace_back(severity, identifier, message); } -void Logger::log(LogSeverity severity, - std::string const& identifier, - const char *format, ...) -{ +void Logger::log( + LogSeverity severity, std::string const& identifier, char const* format, ... +) { va_list argptr; va_start(argptr, format); auto message = printfToString(format, argptr); @@ -24,5 +23,4 @@ void Logger::log(LogSeverity severity, log(severity, identifier, message); } - } // namespace amici diff --git a/src/main.template.cpp b/src/main.template.cpp index 00a5e6b448..ecdff85a46 100644 --- a/src/main.template.cpp +++ b/src/main.template.cpp @@ -1,14 +1,13 @@ #include -#include /* AMICI base functions */ -#include "wrapfunctions.h" /* model-provided functions */ +#include "wrapfunctions.h" /* model-provided functions */ +#include /* AMICI base functions */ -template < class T > -std::ostream& operator << (std::ostream& os, const std::vector& v) -{ +template +std::ostream& operator<<(std::ostream& os, std::vector const& v) { os << "["; - for (typename std::vector::const_iterator ii = v.begin(); ii != v.end(); ++ii) - { + for (typename std::vector::const_iterator ii = v.begin(); ii != v.end(); + ++ii) { os << " " << *ii; } os << "]"; @@ -21,9 +20,9 @@ std::ostream& operator << (std::ostream& os, const std::vector& v) */ int main() { - std::cout<<"********************************"<getObservableIds(); - std::cout<<"Simulated observables for timepoints "<ts<<"\n\n"; - for(int i_observable = 0; i_observable < rdata->ny; ++i_observable) { - std::cout<nt; ++i_time) { + std::cout << "Simulated observables for timepoints " << rdata->ts << "\n\n"; + for (int i_observable = 0; i_observable < rdata->ny; ++i_observable) { + std::cout << observable_ids[i_observable] << ":\n\t"; + for (int i_time = 0; i_time < rdata->nt; ++i_time) { // rdata->y is a flat 2D array in row-major ordering - std::cout<y[i_time * rdata->ny + i_observable]<<" "; + std::cout << rdata->y[i_time * rdata->ny + i_observable] << " "; } - std::cout<setSensitivityOrder(amici::SensitivityOrder::first); @@ -78,18 +76,17 @@ int main() { auto state_ids = model->getStateIds(); auto parameter_ids = model->getParameterIds(); - std::cout<<"State sensitivities for timepoint " - <ts[i_time] - <nx; ++i_state) { - std::cout<<"\td("<plist(i_nplist)]<<") = "; + std::cout << "State sensitivities for timepoint " << rdata->ts[i_time] + << std::endl; // nt x nplist x nx + for (int i_state = 0; i_state < rdata->nx; ++i_state) { + std::cout << "\td(" << state_ids[i_state] << ")/d(" + << parameter_ids[model->plist(i_nplist)] << ") = "; // rdata->sx is a flat 3D array in row-major ordering - std::cout<sx[i_time * rdata->nplist * rdata->nx - + i_nplist * rdata->nx - + i_state]; - std::cout<sx + [i_time * rdata->nplist * rdata->nx + + i_nplist * rdata->nx + i_state]; + std::cout << std::endl; } return 0; diff --git a/src/misc.cpp b/src/misc.cpp index bce3fdfddb..f54a45052d 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -1,10 +1,10 @@ #include "amici/misc.h" #include "amici/symbolic_functions.h" +#include #include #include #include -#include #if defined(_WIN32) #define PLATFORM_WINDOWS // Windows @@ -13,9 +13,9 @@ #elif defined(__CYGWIN__) && !defined(_WIN32) #define PLATFORM_WINDOWS // Windows (Cygwin POSIX under Microsoft Window) #else +#include // for __cxa_demangle +#include // for dladdr #include -#include // for dladdr -#include // for __cxa_demangle #endif namespace amici { @@ -24,8 +24,7 @@ void writeSlice(AmiVector const& s, gsl::span b) { writeSlice(s.getVector(), b); }; -double getUnscaledParameter(double scaledParameter, ParameterScaling scaling) -{ +double getUnscaledParameter(double scaledParameter, ParameterScaling scaling) { switch (scaling) { case ParameterScaling::log10: return pow(10, scaledParameter); @@ -45,14 +44,13 @@ void unscaleParameters( Expects(bufferScaled.size() == pscale.size()); Expects(bufferScaled.size() == bufferUnscaled.size()); - for (gsl::span::index_type ip = 0; - ip < bufferScaled.size(); ++ip) { + for (gsl::span::index_type ip = 0; ip < bufferScaled.size(); + ++ip) { bufferUnscaled[ip] = getUnscaledParameter(bufferScaled[ip], pscale[ip]); } } -double getScaledParameter(double unscaledParameter, ParameterScaling scaling) -{ +double getScaledParameter(double unscaledParameter, ParameterScaling scaling) { switch (scaling) { case ParameterScaling::log10: return log10(unscaledParameter); @@ -72,8 +70,8 @@ void scaleParameters( Expects(bufferScaled.size() == pscale.size()); Expects(bufferScaled.size() == bufferUnscaled.size()); - for (gsl::span::index_type ip = 0; - ip < bufferUnscaled.size(); ++ip) { + for (gsl::span::index_type ip = 0; ip < bufferUnscaled.size(); + ++ip) { bufferScaled[ip] = getScaledParameter(bufferUnscaled[ip], pscale[ip]); } } @@ -88,29 +86,34 @@ std::string backtraceString(int const maxFrames, int const first_frame) { void* callstack[last_frame]; char buf[1024]; int nFrames = backtrace(callstack, last_frame); - char **symbols = backtrace_symbols(callstack, nFrames); + char** symbols = backtrace_symbols(callstack, nFrames); for (int i = first_frame; i < nFrames; i++) { // call Dl_info info; if (dladdr(callstack[i], &info) && info.dli_sname) { - char *demangled = nullptr; + char* demangled = nullptr; int status = -1; if (info.dli_sname[0] == '_') - demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, - &status); - snprintf(buf, sizeof(buf), "%-3d %*p %s + %zd\n", i - 2, - int(2 + sizeof(void *) * 2), callstack[i], - status == 0 ? demangled - : info.dli_sname == nullptr ? symbols[i] - : info.dli_sname, - static_cast((char *)callstack[i] - - (char *)info.dli_saddr)); + demangled = abi::__cxa_demangle( + info.dli_sname, nullptr, nullptr, &status + ); + snprintf( + buf, sizeof(buf), "%-3d %*p %s + %zd\n", i - 2, + int(2 + sizeof(void*) * 2), callstack[i], + status == 0 ? demangled + : info.dli_sname == nullptr ? symbols[i] + : info.dli_sname, + static_cast( + (char*)callstack[i] - (char*)info.dli_saddr + ) + ); free(demangled); } else { - snprintf(buf, sizeof(buf), "%-3d %*p %s\n", i - 2, - int(2 + sizeof(void *) * 2), callstack[i], - symbols[i]); + snprintf( + buf, sizeof(buf), "%-3d %*p %s\n", i - 2, + int(2 + sizeof(void*) * 2), callstack[i], symbols[i] + ); } trace_buf << buf; } @@ -122,8 +125,7 @@ std::string backtraceString(int const maxFrames, int const first_frame) { return trace_buf.str(); } -std::string regexErrorToString(std::regex_constants::error_type err_type) -{ +std::string regexErrorToString(std::regex_constants::error_type err_type) { switch (err_type) { case std::regex_constants::error_collate: return "error_collate"; @@ -173,8 +175,7 @@ std::string printfToString(char const* fmt, va_list ap) { return str; } -std::pair unravel_index(size_t flat_idx, size_t num_cols) -{ +std::pair unravel_index(size_t flat_idx, size_t num_cols) { return {flat_idx / num_cols, flat_idx % num_cols}; } diff --git a/src/model.cpp b/src/model.cpp index 1e73efd2fa..2aa8ee72ee 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -1,26 +1,25 @@ -#include #include +#include #include #include +#include #include -#include - #include +#include #include #include #include #include #include #include -#include namespace amici { /** * @brief Maps ModelQuantity items to their string value */ -const std::map model_quantity_to_str { +const std::map model_quantity_to_str{ {ModelQuantity::J, "J"}, {ModelQuantity::JB, "JB"}, {ModelQuantity::Jv, "Jv"}, @@ -78,8 +77,7 @@ const std::map model_quantity_to_str { }; - -static void setNaNtoZero(std::vector &vec) { +static void setNaNtoZero(std::vector& vec) { std::for_each(vec.begin(), vec.end(), [](double& val) { if (std::isnan(val)) { val = 0.0; @@ -96,16 +94,17 @@ static void setNaNtoZero(std::vector &vec) { * @param id_name string indicating whether name or id was specified * @return value of the selected parameter */ -static realtype getValueById(std::vector const &ids, - std::vector const &values, - std::string const &id, const char *variable_name, - const char *id_name) { +static realtype getValueById( + std::vector const& ids, std::vector const& values, + std::string const& id, char const* variable_name, char const* id_name +) { auto it = std::find(ids.begin(), ids.end(), id); if (it != ids.end()) return values.at(it - ids.begin()); - throw AmiException("Could not find %s with specified %s", variable_name, - id_name); + throw AmiException( + "Could not find %s with specified %s", variable_name, id_name + ); } /** @@ -117,16 +116,18 @@ static realtype getValueById(std::vector const &ids, * @param variable_name string indicating what variable we are looking at * @param id_name string indicating whether name or id was specified */ -static void setValueById(std::vector const &ids, - std::vector &values, realtype value, - std::string const &id, const char *variable_name, - const char *id_name) { +static void setValueById( + std::vector const& ids, std::vector& values, + realtype value, std::string const& id, char const* variable_name, + char const* id_name +) { auto it = std::find(ids.begin(), ids.end(), id); if (it != ids.end()) values.at(it - ids.begin()) = value; else - throw AmiException("Could not find %s with specified %s", variable_name, - id_name); + throw AmiException( + "Could not find %s with specified %s", variable_name, id_name + ); } /** @@ -140,14 +141,15 @@ static void setValueById(std::vector const &ids, * @return number of matched names/ids */ -static int setValueByIdRegex(std::vector const &ids, - std::vector &values, realtype value, - std::string const ®ex, - const char *variable_name, const char *id_name) { +static int setValueByIdRegex( + std::vector const& ids, std::vector& values, + realtype value, std::string const& regex, char const* variable_name, + char const* id_name +) { try { std::regex pattern(regex); int n_found = 0; - for (const auto &id : ids) { + for (auto const& id : ids) { if (std::regex_match(id, pattern)) { values.at(&id - &ids[0]) = value; ++n_found; @@ -155,42 +157,59 @@ static int setValueByIdRegex(std::vector const &ids, } if (n_found == 0) - throw AmiException("Could not find %s with specified %s (%s)", - variable_name, id_name, regex.c_str()); + throw AmiException( + "Could not find %s with specified %s (%s)", variable_name, + id_name, regex.c_str() + ); return n_found; - } catch (std::regex_error const &e) { + } catch (std::regex_error const& e) { auto err_string = regexErrorToString(e.code()); - throw AmiException("Specified regex pattern %s could not be compiled:" - " %s (%s)", regex.c_str(), e.what(), - err_string.c_str()); - } -} - -Model::Model(ModelDimensions const & model_dimensions, - SimulationParameters simulation_parameters, - SecondOrderMode o2mode, std::vector idlist, - std::vector z2event, - const bool pythonGenerated, const int ndxdotdp_explicit, - const int ndxdotdx_explicit, const int w_recursion_depth) - : ModelDimensions(model_dimensions), pythonGenerated(pythonGenerated), - o2mode(o2mode), idlist(std::move(idlist)), - derived_state_(model_dimensions), - z2event_(std::move(z2event)), - state_is_non_negative_(nx_solver, false), - w_recursion_depth_(w_recursion_depth), - simulation_parameters_(std::move(simulation_parameters)) { - Expects(model_dimensions.np == gsl::narrow(simulation_parameters_.parameters.size())); - Expects(model_dimensions.nk == gsl::narrow(simulation_parameters_.fixedParameters.size())); - - simulation_parameters.pscale = std::vector(model_dimensions.np, ParameterScaling::none); + throw AmiException( + "Specified regex pattern %s could not be compiled:" + " %s (%s)", + regex.c_str(), e.what(), err_string.c_str() + ); + } +} + +Model::Model( + ModelDimensions const& model_dimensions, + SimulationParameters simulation_parameters, SecondOrderMode o2mode, + std::vector idlist, std::vector z2event, + bool const pythonGenerated, int const ndxdotdp_explicit, + int const ndxdotdx_explicit, int const w_recursion_depth +) + : ModelDimensions(model_dimensions) + , pythonGenerated(pythonGenerated) + , o2mode(o2mode) + , idlist(std::move(idlist)) + , derived_state_(model_dimensions) + , z2event_(std::move(z2event)) + , state_is_non_negative_(nx_solver, false) + , w_recursion_depth_(w_recursion_depth) + , simulation_parameters_(std::move(simulation_parameters)) { + Expects( + model_dimensions.np + == gsl::narrow(simulation_parameters_.parameters.size()) + ); + Expects( + model_dimensions.nk + == gsl::narrow(simulation_parameters_.fixedParameters.size()) + ); + + simulation_parameters.pscale = std::vector( + model_dimensions.np, ParameterScaling::none + ); state_.h.resize(ne, 0.0); state_.total_cl.resize(nx_rdata - nx_solver, 0.0); state_.stotal_cl.resize((nx_rdata - nx_solver) * np(), 0.0); state_.unscaledParameters.resize(np()); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, state_.unscaledParameters); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); state_.fixedParameters = simulation_parameters_.fixedParameters; state_.plist = simulation_parameters_.plist; @@ -207,85 +226,93 @@ Model::Model(ModelDimensions const & model_dimensions, derived_state_.dwdp_ = SUNMatrixWrapper(nw, np(), 0, CSC_MAT); for (int irec = 0; irec <= w_recursion_depth_; ++irec) { - /* for the first element we know the exact size, while for all others we - guess the size*/ + /* for the first element we know the exact size, while for all + others we guess the size*/ dwdp_hierarchical_.emplace_back( - SUNMatrixWrapper(nw, np(), irec * ndwdw + ndwdp, CSC_MAT)); + SUNMatrixWrapper(nw, np(), irec * ndwdw + ndwdp, CSC_MAT) + ); dwdx_hierarchical_.emplace_back( - SUNMatrixWrapper(nw, nx_solver, irec * ndwdw + ndwdx, CSC_MAT)); + SUNMatrixWrapper(nw, nx_solver, irec * ndwdw + ndwdx, CSC_MAT) + ); } - assert(gsl::narrow(dwdp_hierarchical_.size()) == - w_recursion_depth_ + 1); - assert(gsl::narrow(dwdx_hierarchical_.size()) == - w_recursion_depth_ + 1); + assert( + gsl::narrow(dwdp_hierarchical_.size()) + == w_recursion_depth_ + 1 + ); + assert( + gsl::narrow(dwdx_hierarchical_.size()) + == w_recursion_depth_ + 1 + ); - derived_state_.dxdotdp_explicit = SUNMatrixWrapper( - nx_solver, np(), ndxdotdp_explicit, CSC_MAT); + derived_state_.dxdotdp_explicit + = SUNMatrixWrapper(nx_solver, np(), ndxdotdp_explicit, CSC_MAT); // guess size, will be dynamically reallocated - derived_state_.dxdotdp_implicit = SUNMatrixWrapper( - nx_solver, np(), ndwdp + ndxdotdw, CSC_MAT); + derived_state_.dxdotdp_implicit + = SUNMatrixWrapper(nx_solver, np(), ndwdp + ndxdotdw, CSC_MAT); derived_state_.dxdotdx_explicit = SUNMatrixWrapper( - nx_solver, nx_solver, ndxdotdx_explicit, CSC_MAT); + nx_solver, nx_solver, ndxdotdx_explicit, CSC_MAT + ); // guess size, will be dynamically reallocated - derived_state_.dxdotdx_implicit = SUNMatrixWrapper( - nx_solver, nx_solver, ndwdx + ndxdotdw, CSC_MAT); + derived_state_.dxdotdx_implicit + = SUNMatrixWrapper(nx_solver, nx_solver, ndwdx + ndxdotdw, CSC_MAT); // dynamically allocate on first call - derived_state_.dxdotdp_full = SUNMatrixWrapper( - nx_solver, np(), 0, CSC_MAT); + derived_state_.dxdotdp_full + = SUNMatrixWrapper(nx_solver, np(), 0, CSC_MAT); for (int iytrue = 0; iytrue < nytrue; ++iytrue) derived_state_.dJydy_.emplace_back( - SUNMatrixWrapper(nJ, ny, ndJydy.at(iytrue), CSC_MAT)); + SUNMatrixWrapper(nJ, ny, ndJydy.at(iytrue), CSC_MAT) + ); } else { derived_state_.dwdx_ = SUNMatrixWrapper(nw, nx_solver, ndwdx, CSC_MAT); derived_state_.dwdp_ = SUNMatrixWrapper(nw, np(), ndwdp, CSC_MAT); - derived_state_.dJydy_matlab_ = std::vector( - nJ * nytrue * ny, 0.0); + derived_state_.dJydy_matlab_ + = std::vector(nJ * nytrue * ny, 0.0); } requireSensitivitiesForAllParameters(); } -bool operator==(const Model &a, const Model &b) { +bool operator==(Model const& a, Model const& b) { if (typeid(a) != typeid(b)) return false; return (static_cast(a) == static_cast(b)) - && (a.o2mode == b.o2mode) && - (a.z2event_ == b.z2event_) && (a.idlist == b.idlist) && - (a.simulation_parameters_ == b.simulation_parameters_) && - (a.x0data_ == b.x0data_) && - (a.sx0data_ == b.sx0data_) && - (a.nmaxevent_ == b.nmaxevent_) && - (a.state_is_non_negative_ == b.state_is_non_negative_) && - (a.sigma_res_ == b.sigma_res_) && - (a.min_sigma_ == b.min_sigma_) + && (a.o2mode == b.o2mode) && (a.z2event_ == b.z2event_) + && (a.idlist == b.idlist) + && (a.simulation_parameters_ == b.simulation_parameters_) + && (a.x0data_ == b.x0data_) && (a.sx0data_ == b.sx0data_) + && (a.nmaxevent_ == b.nmaxevent_) + && (a.state_is_non_negative_ == b.state_is_non_negative_) + && (a.sigma_res_ == b.sigma_res_) && (a.min_sigma_ == b.min_sigma_) && a.state_ == b.state_; } -bool operator==(const ModelDimensions &a, const ModelDimensions &b) { +bool operator==(ModelDimensions const& a, ModelDimensions const& b) { if (typeid(a) != typeid(b)) return false; - return (a.nx_rdata == b.nx_rdata) && (a.nxtrue_rdata == b.nxtrue_rdata) && - (a.nx_solver == b.nx_solver) && - (a.nxtrue_solver == b.nxtrue_solver) && - (a.nx_solver_reinit == b.nx_solver_reinit) && - (a.np == b.np) && (a.nk == b.nk) && (a.ny == b.ny) && - (a.nytrue == b.nytrue) && (a.nz == b.nz) && (a.nztrue == b.nztrue) && - (a.ne == b.ne) && (a.nw == b.nw) && (a.ndwdx == b.ndwdx) && - (a.ndwdp == b.ndwdp) && (a.ndwdw == b.ndwdw) && - (a.ndxdotdw == b.ndxdotdw) && (a.ndJydy == b.ndJydy) && - (a.nnz == b.nnz) && (a.nJ == b.nJ) && (a.ubw == b.ubw) && - (a.lbw == b.lbw); -} - - -void Model::initialize(AmiVector &x, AmiVector &dx, AmiVectorArray &sx, - AmiVectorArray & /*sdx*/, bool computeSensitivities, - std::vector &roots_found) { + return (a.nx_rdata == b.nx_rdata) && (a.nxtrue_rdata == b.nxtrue_rdata) + && (a.nx_solver == b.nx_solver) + && (a.nxtrue_solver == b.nxtrue_solver) + && (a.nx_solver_reinit == b.nx_solver_reinit) && (a.np == b.np) + && (a.nk == b.nk) && (a.ny == b.ny) && (a.nytrue == b.nytrue) + && (a.nz == b.nz) && (a.nztrue == b.nztrue) && (a.ne == b.ne) + && (a.nw == b.nw) && (a.ndwdx == b.ndwdx) && (a.ndwdp == b.ndwdp) + && (a.ndwdw == b.ndwdw) && (a.ndxdotdw == b.ndxdotdw) + && (a.ndJydy == b.ndJydy) && (a.nnz == b.nnz) && (a.nJ == b.nJ) + && (a.ubw == b.ubw) && (a.lbw == b.lbw); +} + +void Model::initialize( + AmiVector& x, AmiVector& dx, AmiVectorArray& sx, AmiVectorArray& /*sdx*/, + bool computeSensitivities, std::vector& roots_found +) { initializeStates(x); - if (computeSensitivities) + initializeSplines(); + if (computeSensitivities) { initializeStateSensitivities(sx, x); + initializeSplineSensitivities(); + } fdx0(x, dx); if (computeSensitivities) @@ -295,22 +322,24 @@ void Model::initialize(AmiVector &x, AmiVector &dx, AmiVectorArray &sx, initEvents(x, dx, roots_found); } -void Model::initializeB(AmiVector &xB, AmiVector &dxB, AmiVector &xQB, - bool posteq) const { +void Model::initializeB( + AmiVector& xB, AmiVector& dxB, AmiVector& xQB, bool posteq +) const { xB.zero(); dxB.zero(); if (!posteq) xQB.zero(); } -void Model::initializeStates(AmiVector &x) { +void Model::initializeStates(AmiVector& x) { if (x0data_.empty()) { fx0(x); } else { std::vector x0_solver(nx_solver, 0.0); - ftotal_cl(state_.total_cl.data(), x0data_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data()); + ftotal_cl( + state_.total_cl.data(), x0data_.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); fx_solver(x0_solver.data(), x0data_.data()); std::copy(x0_solver.cbegin(), x0_solver.cend(), x.data()); } @@ -318,21 +347,82 @@ void Model::initializeStates(AmiVector &x) { checkFinite(x.getVector(), ModelQuantity::x0); } -void Model::initializeStateSensitivities(AmiVectorArray &sx, - AmiVector const &x) { +void Model::initializeSplines() { + splines_ = fcreate_splines( + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); + state_.spl_.resize(splines_.size(), 0.0); + for (auto& spline : splines_) { + spline.compute_coefficients(); + } +} + +void Model::initializeSplineSensitivities() { + derived_state_.sspl_ = SUNMatrixWrapper(splines_.size(), np()); + int allnodes = 0; + for (auto const& spline : splines_) { + allnodes += spline.n_nodes(); + } + + std::vector dspline_valuesdp(allnodes * nplist(), 0.0); + std::vector dspline_slopesdp(allnodes * nplist(), 0.0); + std::vector tmp_dvalues(allnodes, 0.0); + std::vector tmp_dslopes(allnodes, 0.0); + for (int ip = 0; ip < nplist(); ip++) { + std::fill(tmp_dvalues.begin(), tmp_dvalues.end(), 0.0); + std::fill(tmp_dslopes.begin(), tmp_dslopes.end(), 0.0); + fdspline_valuesdp( + tmp_dvalues.data(), state_.unscaledParameters.data(), + state_.fixedParameters.data(), plist(ip) + ); + fdspline_slopesdp( + tmp_dslopes.data(), state_.unscaledParameters.data(), + state_.fixedParameters.data(), plist(ip) + ); + /* NB dspline_valuesdp/dspline_slopesdp must be filled + * using the following order for the indices + * (from slower to faster): spline, node, parameter. + * That is what the current spline implementation expects. + */ + int k = 0; + int offset = ip; + for (auto const& spline : splines_) { + for (int n = 0; n < spline.n_nodes(); n++) { + dspline_valuesdp[offset] = tmp_dvalues[k]; + dspline_slopesdp[offset] = tmp_dslopes[k]; + offset += nplist(); + k += 1; + } + } + assert(k == allnodes); + } + + int spline_offset = 0; + for (auto& spline : splines_) { + spline.compute_coefficients_sensi( + nplist(), spline_offset, dspline_valuesdp, dspline_slopesdp + ); + spline_offset += spline.n_nodes() * nplist(); + } +} + +void Model::initializeStateSensitivities( + AmiVectorArray& sx, AmiVector const& x +) { if (sx0data_.empty()) { fsx0(sx, x); } else { - realtype *stcl = nullptr; + realtype* stcl = nullptr; std::vector sx0_solver_slice(nx_solver, 0.0); for (int ip = 0; ip < nplist(); ip++) { if (ncl() > 0) stcl = &state_.stotal_cl.at(plist(ip) * ncl()); - fstotal_cl(stcl, &sx0data_.at(ip * nx_rdata), plist(ip), - derived_state_.x_rdata_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - state_.total_cl.data()); + fstotal_cl( + stcl, &sx0data_.at(ip * nx_rdata), plist(ip), + derived_state_.x_rdata_.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.total_cl.data() + ); fsx_solver(sx0_solver_slice.data(), &sx0data_.at(ip * nx_rdata)); for (int ix = 0; ix < nx_solver; ix++) { sx.at(ix, ip) = sx0_solver_slice.at(ix); @@ -341,8 +431,9 @@ void Model::initializeStateSensitivities(AmiVectorArray &sx, } } -void Model::initEvents(AmiVector const &x, AmiVector const &dx, - std::vector &roots_found) { +void Model::initEvents( + AmiVector const& x, AmiVector const& dx, std::vector& roots_found +) { std::vector rootvals(ne, 0.0); froot(simulation_parameters_.tstart_, x, dx, rootvals); std::fill(roots_found.begin(), roots_found.end(), 0); @@ -359,226 +450,283 @@ void Model::initEvents(AmiVector const &x, AmiVector const &dx, int Model::nplist() const { return gsl::narrow(state_.plist.size()); } -int Model::np() const { return gsl::narrow(static_cast(*this).np); } +int Model::np() const { + return gsl::narrow(static_cast(*this).np); +} -int Model::nk() const { return gsl::narrow(state_.fixedParameters.size()); } +int Model::nk() const { + return gsl::narrow(state_.fixedParameters.size()); +} int Model::ncl() const { return nx_rdata - nx_solver; } int Model::nx_reinit() const { return nx_solver_reinit; } -const double *Model::k() const { return state_.fixedParameters.data(); } +double const* Model::k() const { return state_.fixedParameters.data(); } int Model::nMaxEvent() const { return nmaxevent_; } void Model::setNMaxEvent(int nmaxevent) { nmaxevent_ = nmaxevent; } -int Model::nt() const { return gsl::narrow(simulation_parameters_.ts_.size()); } +int Model::nt() const { + return gsl::narrow(simulation_parameters_.ts_.size()); +} -const std::vector &Model::getParameterScale() const { +std::vector const& Model::getParameterScale() const { return simulation_parameters_.pscale; } void Model::setParameterScale(ParameterScaling pscale) { - simulation_parameters_.pscale.assign(simulation_parameters_.pscale.size(), pscale); - scaleParameters(state_.unscaledParameters, simulation_parameters_.pscale, - simulation_parameters_.parameters); + simulation_parameters_.pscale.assign( + simulation_parameters_.pscale.size(), pscale + ); + scaleParameters( + state_.unscaledParameters, simulation_parameters_.pscale, + simulation_parameters_.parameters + ); sx0data_.clear(); } -void Model::setParameterScale(std::vector const &pscaleVec) { +void Model::setParameterScale(std::vector const& pscaleVec) { if (pscaleVec.size() != simulation_parameters_.parameters.size()) throw AmiException("Dimension mismatch. Size of parameter scaling does " "not match number of model parameters."); simulation_parameters_.pscale = pscaleVec; - scaleParameters(state_.unscaledParameters, simulation_parameters_.pscale, - simulation_parameters_.parameters); + scaleParameters( + state_.unscaledParameters, simulation_parameters_.pscale, + simulation_parameters_.parameters + ); sx0data_.clear(); } -const std::vector &Model::getUnscaledParameters() const { +std::vector const& Model::getUnscaledParameters() const { return state_.unscaledParameters; } -std::vector const &Model::getParameters() const { +std::vector const& Model::getParameters() const { return simulation_parameters_.parameters; } -realtype Model::getParameterById(std::string const &par_id) const { +realtype Model::getParameterById(std::string const& par_id) const { if (!hasParameterIds()) throw AmiException( - "Could not access parameters by id as they are not set"); - return getValueById(getParameterIds(), simulation_parameters_.parameters, - par_id, "parameters", "id"); + "Could not access parameters by id as they are not set" + ); + return getValueById( + getParameterIds(), simulation_parameters_.parameters, par_id, + "parameters", "id" + ); } -realtype Model::getParameterByName(std::string const &par_name) const { +realtype Model::getParameterByName(std::string const& par_name) const { if (!hasParameterNames()) throw AmiException( - "Could not access parameters by name as they are not set"); - return getValueById(getParameterNames(), simulation_parameters_.parameters, - par_name, "parameters", "name"); + "Could not access parameters by name as they are not set" + ); + return getValueById( + getParameterNames(), simulation_parameters_.parameters, par_name, + "parameters", "name" + ); } -void Model::setParameters(const std::vector &p) { +void Model::setParameters(std::vector const& p) { if (p.size() != (unsigned)np()) throw AmiException("Dimension mismatch. Size of parameters does not " "match number of model parameters."); simulation_parameters_.parameters = p; state_.unscaledParameters.resize(simulation_parameters_.parameters.size()); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, - state_.unscaledParameters); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); } -void Model::setParameterById(const std::map &p, - bool ignoreErrors) -{ +void Model::setParameterById( + std::map const& p, bool ignoreErrors +) { for (auto& kv : p) { try { setParameterById(kv.first, kv.second); } catch (AmiException const&) { - if(!ignoreErrors) + if (!ignoreErrors) throw; } } } -void Model::setParameterById(std::string const &par_id, realtype value) { +void Model::setParameterById(std::string const& par_id, realtype value) { if (!hasParameterIds()) throw AmiException( - "Could not access parameters by id as they are not set"); + "Could not access parameters by id as they are not set" + ); - setValueById(getParameterIds(), simulation_parameters_.parameters, - value, par_id, "parameter", "id"); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, - state_.unscaledParameters); + setValueById( + getParameterIds(), simulation_parameters_.parameters, value, par_id, + "parameter", "id" + ); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); } -int Model::setParametersByIdRegex(std::string const &par_id_regex, - realtype value) { +int Model::setParametersByIdRegex( + std::string const& par_id_regex, realtype value +) { if (!hasParameterIds()) throw AmiException( - "Could not access parameters by id as they are not set"); - int n_found = setValueByIdRegex(getParameterIds(), - simulation_parameters_.parameters, - value, par_id_regex, "parameter", "id"); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, state_.unscaledParameters); + "Could not access parameters by id as they are not set" + ); + int n_found = setValueByIdRegex( + getParameterIds(), simulation_parameters_.parameters, value, + par_id_regex, "parameter", "id" + ); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); return n_found; } -void Model::setParameterByName(std::string const &par_name, realtype value) { +void Model::setParameterByName(std::string const& par_name, realtype value) { if (!hasParameterNames()) throw AmiException( - "Could not access parameters by name as they are not set"); + "Could not access parameters by name as they are not set" + ); - setValueById(getParameterNames(), simulation_parameters_.parameters, - value, par_name, "parameter", "name"); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, state_.unscaledParameters); + setValueById( + getParameterNames(), simulation_parameters_.parameters, value, par_name, + "parameter", "name" + ); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); } -void Model::setParameterByName(const std::map &p, - bool ignoreErrors) -{ +void Model::setParameterByName( + std::map const& p, bool ignoreErrors +) { for (auto& kv : p) { try { setParameterByName(kv.first, kv.second); } catch (AmiException const&) { - if(!ignoreErrors) + if (!ignoreErrors) throw; } } } -int Model::setParametersByNameRegex(std::string const &par_name_regex, - realtype value) { +int Model::setParametersByNameRegex( + std::string const& par_name_regex, realtype value +) { if (!hasParameterNames()) throw AmiException( - "Could not access parameters by name as they are not set"); + "Could not access parameters by name as they are not set" + ); - int n_found = setValueByIdRegex(getParameterNames(), - simulation_parameters_.parameters, - value, par_name_regex, "parameter", "name"); + int n_found = setValueByIdRegex( + getParameterNames(), simulation_parameters_.parameters, value, + par_name_regex, "parameter", "name" + ); - unscaleParameters(simulation_parameters_.parameters, - simulation_parameters_.pscale, state_.unscaledParameters); + unscaleParameters( + simulation_parameters_.parameters, simulation_parameters_.pscale, + state_.unscaledParameters + ); return n_found; } -const std::vector &Model::getFixedParameters() const { +std::vector const& Model::getFixedParameters() const { return state_.fixedParameters; } -realtype Model::getFixedParameterById(std::string const &par_id) const { +realtype Model::getFixedParameterById(std::string const& par_id) const { if (!hasFixedParameterIds()) throw AmiException( - "Could not access fixed parameters by id as they are not set"); + "Could not access fixed parameters by id as they are not set" + ); - return getValueById(getFixedParameterIds(), state_.fixedParameters, - par_id, "fixedParameters", "id"); + return getValueById( + getFixedParameterIds(), state_.fixedParameters, par_id, + "fixedParameters", "id" + ); } -realtype Model::getFixedParameterByName(std::string const &par_name) const { +realtype Model::getFixedParameterByName(std::string const& par_name) const { if (!hasFixedParameterNames()) throw AmiException( - "Could not access fixed parameters by name as they are not set"); + "Could not access fixed parameters by name as they are not set" + ); - return getValueById(getFixedParameterNames(), state_.fixedParameters, - par_name, "fixedParameters", "name"); + return getValueById( + getFixedParameterNames(), state_.fixedParameters, par_name, + "fixedParameters", "name" + ); } -void Model::setFixedParameters(const std::vector &k) { +void Model::setFixedParameters(std::vector const& k) { if (k.size() != (unsigned)nk()) throw AmiException("Dimension mismatch. Size of fixedParameters does " "not match number of fixed model parameters."); state_.fixedParameters = k; } -void Model::setFixedParameterById(std::string const &par_id, realtype value) { +void Model::setFixedParameterById(std::string const& par_id, realtype value) { if (!hasFixedParameterIds()) throw AmiException( - "Could not access fixed parameters by id as they are not set"); + "Could not access fixed parameters by id as they are not set" + ); - setValueById(getFixedParameterIds(), state_.fixedParameters, value, par_id, - "fixedParameters", "id"); + setValueById( + getFixedParameterIds(), state_.fixedParameters, value, par_id, + "fixedParameters", "id" + ); } -int Model::setFixedParametersByIdRegex(std::string const &par_id_regex, - realtype value) { +int Model::setFixedParametersByIdRegex( + std::string const& par_id_regex, realtype value +) { if (!hasFixedParameterIds()) throw AmiException( - "Could not access fixed parameters by id as they are not set"); + "Could not access fixed parameters by id as they are not set" + ); - return setValueByIdRegex(getFixedParameterIds(), state_.fixedParameters, - value, par_id_regex, "fixedParameters", "id"); + return setValueByIdRegex( + getFixedParameterIds(), state_.fixedParameters, value, par_id_regex, + "fixedParameters", "id" + ); } -void Model::setFixedParameterByName(std::string const &par_name, - realtype value) { +void Model::setFixedParameterByName( + std::string const& par_name, realtype value +) { if (!hasFixedParameterNames()) throw AmiException( - "Could not access fixed parameters by name as they are not set"); + "Could not access fixed parameters by name as they are not set" + ); - setValueById(getFixedParameterNames(), state_.fixedParameters, value, - par_name, "fixedParameters", "name"); + setValueById( + getFixedParameterNames(), state_.fixedParameters, value, par_name, + "fixedParameters", "name" + ); } -int Model::setFixedParametersByNameRegex(std::string const &par_name_regex, - realtype value) { +int Model::setFixedParametersByNameRegex( + std::string const& par_name_regex, realtype value +) { if (!hasFixedParameterNames()) throw AmiException( - "Could not access fixed parameters by name as they are not set"); + "Could not access fixed parameters by name as they are not set" + ); - return setValueByIdRegex(getFixedParameterIds(), state_.fixedParameters, - value, par_name_regex, "fixedParameters", "name"); + return setValueByIdRegex( + getFixedParameterIds(), state_.fixedParameters, value, par_name_regex, + "fixedParameters", "name" + ); } -std::string Model::getName() const { - return ""; -} +std::string Model::getName() const { return ""; } bool Model::hasParameterNames() const { return np() == 0 || !getParameterNames().empty(); @@ -600,7 +748,6 @@ std::vector Model::getStateNamesSolver() const { return std::vector(); } - bool Model::hasFixedParameterNames() const { return nk() == 0 || !getFixedParameterNames().empty(); } @@ -669,16 +816,17 @@ std::vector Model::getExpressionIds() const { return std::vector(); } +bool Model::hasQuadraticLLH() const { return true; } -bool Model::hasQuadraticLLH() const { - return true; +std::vector const& Model::getTimepoints() const { + return simulation_parameters_.ts_; } -std::vector const &Model::getTimepoints() const { return simulation_parameters_.ts_; } - -double Model::getTimepoint(const int it) const { return simulation_parameters_.ts_.at(it); } +double Model::getTimepoint(int const it) const { + return simulation_parameters_.ts_.at(it); +} -void Model::setTimepoints(const std::vector &ts) { +void Model::setTimepoints(std::vector const& ts) { if (!std::is_sorted(ts.begin(), ts.end())) throw AmiException("Encountered non-monotonic timepoints, please order" " timepoints such that they are monotonically" @@ -690,17 +838,17 @@ double Model::t0() const { return simulation_parameters_.tstart_; } void Model::setT0(double t0) { simulation_parameters_.tstart_ = t0; } -std::vector const &Model::getStateIsNonNegative() const { +std::vector const& Model::getStateIsNonNegative() const { return state_is_non_negative_; } -void Model::setStateIsNonNegative(std::vector const &nonNegative) { - auto any_state_non_negative = std::any_of(nonNegative.begin(), - nonNegative.end(), - [](bool x) { - return x; }); +void Model::setStateIsNonNegative(std::vector const& nonNegative) { + auto any_state_non_negative + = std::any_of(nonNegative.begin(), nonNegative.end(), [](bool x) { + return x; + }); if (nx_solver != nx_rdata) { - if(any_state_non_negative) + if (any_state_non_negative) throw AmiException("Non-negative states are not supported with" " conservation laws enabled."); // nothing to do, as `state_is_non_negative_` will always be all-false @@ -708,9 +856,11 @@ void Model::setStateIsNonNegative(std::vector const &nonNegative) { return; } if (state_is_non_negative_.size() != gsl::narrow(nx_rdata)) { - throw AmiException("Dimension of input stateIsNonNegative (%u) does " - "not agree with number of state variables (%d)", - state_is_non_negative_.size(), nx_rdata); + throw AmiException( + "Dimension of input stateIsNonNegative (%u) does " + "not agree with number of state variables (%d)", + state_is_non_negative_.size(), nx_rdata + ); } state_is_non_negative_ = nonNegative; any_state_non_negative_ = any_state_non_negative; @@ -720,14 +870,15 @@ void Model::setAllStatesNonNegative() { setStateIsNonNegative(std::vector(nx_solver, true)); } -const std::vector &Model::getParameterList() const { return state_.plist; } +std::vector const& Model::getParameterList() const { return state_.plist; } int Model::plist(int pos) const { return state_.plist.at(pos); } -void Model::setParameterList(const std::vector &plist) { +void Model::setParameterList(std::vector const& plist) { int np = this->np(); // cannot capture 'this' in lambda expression - if (std::any_of(plist.begin(), plist.end(), - [&np](int idx) { return idx < 0 || idx >= np; })) { + if (std::any_of(plist.begin(), plist.end(), [&np](int idx) { + return idx < 0 || idx >= np; + })) { throw AmiException("Indices in plist must be in [0..np]"); } state_.plist = plist; @@ -736,7 +887,7 @@ void Model::setParameterList(const std::vector &plist) { } std::vector Model::getInitialStates() { - if(!x0data_.empty()) { + if (!x0data_.empty()) { return x0data_; } @@ -745,12 +896,12 @@ std::vector Model::getInitialStates() { * changing parameters etc. */ std::vector x0(nx_rdata, 0.0); - fx0(x0.data(), simulation_parameters_.tstart_, state_.unscaledParameters.data(), - state_.fixedParameters.data()); + fx0(x0.data(), simulation_parameters_.tstart_, + state_.unscaledParameters.data(), state_.fixedParameters.data()); return x0; } -void Model::setInitialStates(const std::vector &x0) { +void Model::setInitialStates(std::vector const& x0) { if (x0.size() != (unsigned)nx_rdata && !x0.empty()) throw AmiException("Dimension mismatch. Size of x0 does not match " "number of model states."); @@ -763,13 +914,10 @@ void Model::setInitialStates(const std::vector &x0) { x0data_ = x0; } -bool Model::hasCustomInitialStates() const -{ - return !x0data_.empty(); -} +bool Model::hasCustomInitialStates() const { return !x0data_.empty(); } std::vector Model::getInitialStateSensitivities() { - if(!sx0data_.empty()) { + if (!sx0data_.empty()) { return sx0data_; } @@ -780,15 +928,16 @@ std::vector Model::getInitialStateSensitivities() { std::vector sx0(nx_rdata * nplist(), 0.0); auto x0 = getInitialStates(); for (int ip = 0; ip < nplist(); ip++) { - fsx0(sx0.data(), simulation_parameters_.tstart_, x0.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), plist(ip)); + fsx0( + sx0.data(), simulation_parameters_.tstart_, x0.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + plist(ip) + ); } return sx0; - } -void Model::setInitialStateSensitivities(const std::vector &sx0) { +void Model::setInitialStateSensitivities(std::vector const& sx0) { if (sx0.size() != (unsigned)nx_rdata * nplist() && !sx0.empty()) throw AmiException("Dimension mismatch. Size of sx0 does not match " "number of model states * number of parameter " @@ -817,20 +966,20 @@ void Model::setInitialStateSensitivities(const std::vector &sx0) { } for (int ix = 0; ix < nx_rdata; ++ix) { - sx0_rdata.at(ip * nx_rdata + ix) = - sx0.at(ip * nx_rdata + ix) / chainrulefactor; + sx0_rdata.at(ip * nx_rdata + ix) + = sx0.at(ip * nx_rdata + ix) / chainrulefactor; } } setUnscaledInitialStateSensitivities(sx0_rdata); } -bool Model::hasCustomInitialStateSensitivities() const -{ +bool Model::hasCustomInitialStateSensitivities() const { return !sx0data_.empty(); } void Model::setUnscaledInitialStateSensitivities( - const std::vector &sx0) { + std::vector const& sx0 +) { if (sx0.size() != (unsigned)nx_rdata * nplist() && !sx0.empty()) throw AmiException("Dimension mismatch. Size of sx0 does not match " "number of model states * number of parameter " @@ -844,8 +993,8 @@ void Model::setUnscaledInitialStateSensitivities( sx0data_ = sx0; } -void Model::setSteadyStateSensitivityMode( - const SteadyStateSensitivityMode mode) { +void Model::setSteadyStateSensitivityMode(const SteadyStateSensitivityMode mode +) { steadystate_sensitivity_mode_ = mode; } @@ -859,11 +1008,15 @@ void Model::setReinitializeFixedParameterInitialStates(bool flag) { "State reinitialization cannot be enabled for this model " "as this feature was disabled at compile time. Most likely," " this was because some initial states depending on " - "fixedParameters also depended on parameters."); + "fixedParameters also depended on parameters." + ); simulation_parameters_.reinitializeFixedParameterInitialStates = flag; - if(flag) { - simulation_parameters_.reinitializeAllFixedParameterDependentInitialStatesForSimulation(nx_rdata); + if (flag) { + simulation_parameters_ + .reinitializeAllFixedParameterDependentInitialStatesForSimulation( + nx_rdata + ); } else { simulation_parameters_.reinitialization_state_idxs_sim.clear(); } @@ -871,7 +1024,7 @@ void Model::setReinitializeFixedParameterInitialStates(bool flag) { bool Model::getReinitializeFixedParameterInitialStates() const { return simulation_parameters_.reinitializeFixedParameterInitialStates - || !simulation_parameters_.reinitialization_state_idxs_sim.empty(); + || !simulation_parameters_.reinitialization_state_idxs_sim.empty(); } void Model::requireSensitivitiesForAllParameters() { @@ -880,15 +1033,16 @@ void Model::requireSensitivitiesForAllParameters() { initializeVectors(); } -void Model::getExpression(gsl::span w, const realtype t, - const AmiVector &x) -{ +void Model::getExpression( + gsl::span w, const realtype t, AmiVector const& x +) { fw(t, computeX_pos(x)); writeSlice(derived_state_.w_, w); } -void Model::getObservable(gsl::span y, const realtype t, - const AmiVector &x) { +void Model::getObservable( + gsl::span y, const realtype t, AmiVector const& x +) { fy(t, x); writeSlice(derived_state_.y_, y); } @@ -897,13 +1051,15 @@ ObservableScaling Model::getObservableScaling(int /*iy*/) const { return ObservableScaling::lin; } -void Model::getObservableSensitivity(gsl::span sy, const realtype t, - const AmiVector &x, - const AmiVectorArray &sx) { +void Model::getObservableSensitivity( + gsl::span sy, const realtype t, AmiVector const& x, + AmiVectorArray const& sx +) { if (!ny) return; fdydx(t, x); + fsspl(t); fdydp(t, x); derived_state_.sx_.resize(nx_solver * nplist()); @@ -913,34 +1069,38 @@ void Model::getObservableSensitivity(gsl::span sy, const realtype t, // dydx A[ny,nx_solver] * sx B[nx_solver,nplist] = sy C[ny,nplist] // M K K N M N // lda ldb ldc - setNaNtoZero(derived_state_.dydx_); - setNaNtoZero(derived_state_.sx_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, ny, nplist(), nx_solver, 1.0, - derived_state_.dydx_.data(), ny, - derived_state_.sx_.data(), nx_solver, 1.0, - derived_state_.dydp_.data(), - ny); + if (nx_solver) { + setNaNtoZero(derived_state_.dydx_); + setNaNtoZero(derived_state_.sx_); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, ny, nplist(), nx_solver, 1.0, + derived_state_.dydx_.data(), ny, derived_state_.sx_.data(), + nx_solver, 1.0, derived_state_.dydp_.data(), ny + ); + } writeSlice(derived_state_.dydp_, sy); if (always_check_finite_) checkFinite(sy, ModelQuantity::sy, nplist()); } -void Model::getObservableSigma(gsl::span sigmay, const int it, - const ExpData *edata) { +void Model::getObservableSigma( + gsl::span sigmay, int const it, ExpData const* edata +) { fsigmay(it, edata); writeSlice(derived_state_.sigmay_, sigmay); } -void Model::getObservableSigmaSensitivity(gsl::span ssigmay, - gsl::span sy, - const int it, const ExpData *edata) { +void Model::getObservableSigmaSensitivity( + gsl::span ssigmay, gsl::span sy, int const it, + ExpData const* edata +) { fdsigmaydp(it, edata); writeSlice(derived_state_.dsigmaydp_, ssigmay); - if(pythonGenerated) { + if (pythonGenerated) { // ssigmay = dsigmaydy*(dydx_solver*sx+dydp)+dsigmaydp // = dsigmaydy*sy+dsigmaydp @@ -953,18 +1113,21 @@ void Model::getObservableSigmaSensitivity(gsl::span ssigmay, setNaNtoZero(derived_state_.dsigmaydy_); derived_state_.sy_.assign(sy.begin(), sy.end()); setNaNtoZero(derived_state_.sy_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, ny, nplist(), ny, 1.0, - derived_state_.dsigmaydy_.data(), ny, - derived_state_.sy_.data(), ny, 1.0, ssigmay.data(), ny); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, ny, nplist(), ny, 1.0, + derived_state_.dsigmaydy_.data(), ny, derived_state_.sy_.data(), ny, + 1.0, ssigmay.data(), ny + ); } if (always_check_finite_) checkFinite(ssigmay, ModelQuantity::ssigmay, nplist()); } -void Model::addObservableObjective(realtype &Jy, const int it, - const AmiVector &x, const ExpData &edata) { +void Model::addObservableObjective( + realtype& Jy, int const it, AmiVector const& x, ExpData const& edata +) { fy(edata.getTimepoint(it), x); fsigmay(it, &edata); @@ -973,20 +1136,17 @@ void Model::addObservableObjective(realtype &Jy, const int it, if (edata.isSetObservedData(it, iyt)) { std::fill(nllh.begin(), nllh.end(), 0.0); fJy(nllh.data(), iyt, state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.y_.data(), - derived_state_.sigmay_.data(), - edata.getObservedDataPtr(it)); + state_.fixedParameters.data(), derived_state_.y_.data(), + derived_state_.sigmay_.data(), edata.getObservedDataPtr(it)); Jy -= nllh.at(0); } } } -void Model::addObservableObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int it, const AmiVector &x, - const AmiVectorArray &sx, - const ExpData &edata) { +void Model::addObservableObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const it, + AmiVector const& x, AmiVectorArray const& sx, ExpData const& edata +) { if (!ny) return; @@ -1002,19 +1162,20 @@ void Model::addObservableObjectiveSensitivity(std::vector &sllh, // C := alpha*op(A)*op(B) + beta*C, setNaNtoZero(derived_state_.dJydx_); setNaNtoZero(derived_state_.sx_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nx_solver, 1.0, - derived_state_.dJydx_.data(), nJ, - derived_state_.sx_.data(), nx_solver, 1.0, - derived_state_.dJydp_.data(), - nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, BLASTranspose::noTrans, + nJ, nplist(), nx_solver, 1.0, derived_state_.dJydx_.data(), nJ, + derived_state_.sx_.data(), nx_solver, 1.0, derived_state_.dJydp_.data(), + nJ + ); writeLLHSensitivitySlice(derived_state_.dJydp_, sllh, s2llh); } void Model::addPartialObservableObjectiveSensitivity( - std::vector &sllh, std::vector &s2llh, const int it, - const AmiVector &x, const ExpData &edata) { + std::vector& sllh, std::vector& s2llh, int const it, + AmiVector const& x, ExpData const& edata +) { if (!ny) return; @@ -1023,22 +1184,25 @@ void Model::addPartialObservableObjectiveSensitivity( writeLLHSensitivitySlice(derived_state_.dJydp_, sllh, s2llh); } -void Model::getAdjointStateObservableUpdate(gsl::span dJydx, - const int it, const AmiVector &x, - const ExpData &edata) { +void Model::getAdjointStateObservableUpdate( + gsl::span dJydx, int const it, AmiVector const& x, + ExpData const& edata +) { fdJydx(it, x, edata); writeSlice(derived_state_.dJydx_, dJydx); } -void Model::getEvent(gsl::span z, const int ie, const realtype t, - const AmiVector &x) { +void Model::getEvent( + gsl::span z, int const ie, const realtype t, AmiVector const& x +) { fz(ie, t, x); writeSliceEvent(derived_state_.z_, z, ie); } -void Model::getEventSensitivity(gsl::span sz, const int ie, - const realtype t, const AmiVector &x, - const AmiVectorArray &sx) { +void Model::getEventSensitivity( + gsl::span sz, int const ie, const realtype t, AmiVector const& x, + AmiVectorArray const& sx +) { if (pythonGenerated) { if (!nz) return; @@ -1055,12 +1219,12 @@ void Model::getEventSensitivity(gsl::span sz, const int ie, // lda ldb ldc setNaNtoZero(derived_state_.dzdx_); setNaNtoZero(derived_state_.sx_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nz, nplist(), nx_solver, 1.0, - derived_state_.dzdx_.data(), nz, - derived_state_.sx_.data(), nx_solver, 1.0, - derived_state_.dzdp_.data(), - nz); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nz, nplist(), nx_solver, 1.0, + derived_state_.dzdx_.data(), nz, derived_state_.sx_.data(), + nx_solver, 1.0, derived_state_.dzdp_.data(), nz + ); addSlice(derived_state_.dzdp_, sz); @@ -1075,8 +1239,9 @@ void Model::getEventSensitivity(gsl::span sz, const int ie, } } -void Model::getUnobservedEventSensitivity(gsl::span sz, - const int ie) { +void Model::getUnobservedEventSensitivity( + gsl::span sz, int const ie +) { checkBufferSize(sz, nz * nplist()); for (int iz = 0; iz < nz; ++iz) @@ -1085,16 +1250,17 @@ void Model::getUnobservedEventSensitivity(gsl::span sz, sz[ip * nz + iz] = 0.0; } -void Model::getEventRegularization(gsl::span rz, const int ie, - const realtype t, const AmiVector &x) { +void Model::getEventRegularization( + gsl::span rz, int const ie, const realtype t, AmiVector const& x +) { frz(ie, t, x); writeSliceEvent(derived_state_.rz_, rz, ie); } -void Model::getEventRegularizationSensitivity(gsl::span srz, - const int ie, const realtype t, - const AmiVector &x, - const AmiVectorArray &sx) { +void Model::getEventRegularizationSensitivity( + gsl::span srz, int const ie, const realtype t, AmiVector const& x, + AmiVectorArray const& sx +) { if (pythonGenerated) { if (!nz) return; @@ -1111,12 +1277,12 @@ void Model::getEventRegularizationSensitivity(gsl::span srz, // lda ldb ldc setNaNtoZero(derived_state_.drzdx_); setNaNtoZero(derived_state_.sx_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nz, nplist(), nx_solver, 1.0, - derived_state_.drzdx_.data(), nz, - derived_state_.sx_.data(), nx_solver, 1.0, - derived_state_.drzdp_.data(), - nz); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nz, nplist(), nx_solver, 1.0, + derived_state_.drzdx_.data(), nz, derived_state_.sx_.data(), + nx_solver, 1.0, derived_state_.drzdp_.data(), nz + ); addSlice(derived_state_.drzdp_, srz); @@ -1124,31 +1290,35 @@ void Model::getEventRegularizationSensitivity(gsl::span srz, checkFinite(srz, ModelQuantity::srz, nplist()); } else { for (int ip = 0; ip < nplist(); ip++) { - fsrz(&srz[ip * nz], ie, t, computeX_pos(x), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), sx.data(ip), - plist(ip)); + fsrz( + &srz[ip * nz], ie, t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), sx.data(ip), plist(ip) + ); } } } -void Model::getEventSigma(gsl::span sigmaz, const int ie, - const int nroots, const realtype t, - const ExpData *edata) { +void Model::getEventSigma( + gsl::span sigmaz, int const ie, int const nroots, + const realtype t, ExpData const* edata +) { fsigmaz(ie, nroots, t, edata); writeSliceEvent(derived_state_.sigmaz_, sigmaz, ie); } -void Model::getEventSigmaSensitivity(gsl::span ssigmaz, const int ie, - const int nroots, const realtype t, - const ExpData *edata) { +void Model::getEventSigmaSensitivity( + gsl::span ssigmaz, int const ie, int const nroots, + const realtype t, ExpData const* edata +) { fdsigmazdp(ie, nroots, t, edata); writeSensitivitySliceEvent(derived_state_.dsigmazdp_, ssigmaz, ie); } -void Model::addEventObjective(realtype &Jz, const int ie, const int nroots, - const realtype t, const AmiVector &x, - const ExpData &edata) { +void Model::addEventObjective( + realtype& Jz, int const ie, int const nroots, const realtype t, + AmiVector const& x, ExpData const& edata +) { fz(ie, t, x); fsigmaz(ie, nroots, t, &edata); @@ -1157,18 +1327,18 @@ void Model::addEventObjective(realtype &Jz, const int ie, const int nroots, if (edata.isSetObservedEvents(nroots, iztrue)) { std::fill(nllh.begin(), nllh.end(), 0.0); fJz(nllh.data(), iztrue, state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.z_.data(), derived_state_.sigmaz_.data(), + state_.fixedParameters.data(), derived_state_.z_.data(), + derived_state_.sigmaz_.data(), edata.getObservedEventsPtr(nroots)); Jz -= nllh.at(0); } } } -void Model::addEventObjectiveRegularization(realtype &Jrz, const int ie, - const int nroots, const realtype t, - const AmiVector &x, - const ExpData &edata) { +void Model::addEventObjectiveRegularization( + realtype& Jrz, int const ie, int const nroots, const realtype t, + AmiVector const& x, ExpData const& edata +) { frz(ie, t, x); fsigmaz(ie, nroots, t, &edata); @@ -1176,20 +1346,21 @@ void Model::addEventObjectiveRegularization(realtype &Jrz, const int ie, for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (edata.isSetObservedEvents(nroots, iztrue)) { std::fill(nllh.begin(), nllh.end(), 0.0); - fJrz(nllh.data(), iztrue, state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.rz_.data(), derived_state_.sigmaz_.data()); + fJrz( + nllh.data(), iztrue, state_.unscaledParameters.data(), + state_.fixedParameters.data(), derived_state_.rz_.data(), + derived_state_.sigmaz_.data() + ); Jrz -= nllh.at(0); } } } -void Model::addEventObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int ie, const int nroots, - const realtype t, const AmiVector &x, - const AmiVectorArray &sx, - const ExpData &edata) { +void Model::addEventObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const ie, + int const nroots, const realtype t, AmiVector const& x, + AmiVectorArray const& sx, ExpData const& edata +) { if (!nz) return; @@ -1210,31 +1381,29 @@ void Model::addEventObjectiveSensitivity(std::vector &sllh, // C := alpha*op(A)*op(B) + beta*C, setNaNtoZero(derived_state_.dJzdx_); setNaNtoZero(derived_state_.sx_); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nx_solver, 1.0, - derived_state_.dJzdx_.data(), nJ, - derived_state_.sx_.data(), nx_solver, 1.0, - derived_state_.dJzdp_.data(), - nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, BLASTranspose::noTrans, + nJ, nplist(), nx_solver, 1.0, derived_state_.dJzdx_.data(), nJ, + derived_state_.sx_.data(), nx_solver, 1.0, derived_state_.dJzdp_.data(), + nJ + ); // sJy += multResult + dJydp writeLLHSensitivitySlice(derived_state_.dJzdp_, sllh, s2llh); } -void Model::getAdjointStateEventUpdate(gsl::span dJzdx, const int ie, - const int nroots, const realtype t, - const AmiVector &x, - const ExpData &edata) { +void Model::getAdjointStateEventUpdate( + gsl::span dJzdx, int const ie, int const nroots, const realtype t, + AmiVector const& x, ExpData const& edata +) { fdJzdx(ie, nroots, t, x, edata); writeSlice(derived_state_.dJzdx_, dJzdx); } -void Model::addPartialEventObjectiveSensitivity(std::vector &sllh, - std::vector &s2llh, - const int ie, const int nroots, - const realtype t, - const AmiVector &x, - const ExpData &edata) { +void Model::addPartialEventObjectiveSensitivity( + std::vector& sllh, std::vector& s2llh, int const ie, + int const nroots, const realtype t, AmiVector const& x, ExpData const& edata +) { if (!nz) return; @@ -1243,49 +1412,51 @@ void Model::addPartialEventObjectiveSensitivity(std::vector &sllh, writeLLHSensitivitySlice(derived_state_.dJzdp_, sllh, s2llh); } -void Model::getEventTimeSensitivity(std::vector &stau, - const realtype t, const int ie, - const AmiVector &x, - const AmiVectorArray &sx) { +void Model::getEventTimeSensitivity( + std::vector& stau, const realtype t, int const ie, + AmiVector const& x, AmiVectorArray const& sx +) { std::fill(stau.begin(), stau.end(), 0.0); for (int ip = 0; ip < nplist(); ip++) { - fstau(&stau.at(ip), t, computeX_pos(x), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), state_.total_cl.data(), sx.data(ip), - plist(ip), ie); + fstau( + &stau.at(ip), t, computeX_pos(x), state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + state_.total_cl.data(), sx.data(ip), plist(ip), ie + ); } } -void Model::addStateEventUpdate(AmiVector &x, const int ie, const realtype t, - const AmiVector &xdot, - const AmiVector &xdot_old) { +void Model::addStateEventUpdate( + AmiVector& x, int const ie, const realtype t, AmiVector const& xdot, + AmiVector const& xdot_old +) { derived_state_.deltax_.assign(nx_solver, 0.0); std::copy_n(computeX_pos(x), nx_solver, x.data()); // compute update - fdeltax(derived_state_.deltax_.data(), t, x.data(), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), ie, xdot.data(), xdot_old.data()); + fdeltax( + derived_state_.deltax_.data(), t, x.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), ie, xdot.data(), xdot_old.data() + ); if (always_check_finite_) { checkFinite(derived_state_.deltax_, ModelQuantity::deltax); } // update - amici_daxpy(nx_solver, 1.0, derived_state_.deltax_.data(), 1, x.data(), - 1); + amici_daxpy(nx_solver, 1.0, derived_state_.deltax_.data(), 1, x.data(), 1); } -void Model::addStateSensitivityEventUpdate(AmiVectorArray &sx, const int ie, - const realtype t, - const AmiVector &x_old, - const AmiVector &xdot, - const AmiVector &xdot_old, - const std::vector &stau) { +void Model::addStateSensitivityEventUpdate( + AmiVectorArray& sx, int const ie, const realtype t, AmiVector const& x_old, + AmiVector const& xdot, AmiVector const& xdot_old, + std::vector const& stau +) { fw(t, x_old.data()); for (int ip = 0; ip < nplist(); ip++) { @@ -1293,35 +1464,39 @@ void Model::addStateSensitivityEventUpdate(AmiVectorArray &sx, const int ie, derived_state_.deltasx_.assign(nx_solver, 0.0); // compute update - fdeltasx(derived_state_.deltasx_.data(), t, x_old.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data(), plist(ip), ie, - xdot.data(), xdot_old.data(), sx.data(ip), &stau.at(ip), - state_.total_cl.data()); + fdeltasx( + derived_state_.deltasx_.data(), t, x_old.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), derived_state_.w_.data(), plist(ip), ie, + xdot.data(), xdot_old.data(), sx.data(ip), &stau.at(ip), + state_.total_cl.data() + ); if (always_check_finite_) { - checkFinite(derived_state_.deltasx_, ModelQuantity::deltasx, - nplist()); + checkFinite( + derived_state_.deltasx_, ModelQuantity::deltasx, nplist() + ); } - amici_daxpy(nx_solver, 1.0, derived_state_.deltasx_.data(), 1, - sx.data(ip), 1); + amici_daxpy( + nx_solver, 1.0, derived_state_.deltasx_.data(), 1, sx.data(ip), 1 + ); } } -void Model::addAdjointStateEventUpdate(AmiVector &xB, const int ie, - const realtype t, const AmiVector &x, - const AmiVector &xdot, - const AmiVector &xdot_old) { +void Model::addAdjointStateEventUpdate( + AmiVector& xB, int const ie, const realtype t, AmiVector const& x, + AmiVector const& xdot, AmiVector const& xdot_old +) { derived_state_.deltaxB_.assign(nx_solver, 0.0); // compute update - fdeltaxB(derived_state_.deltaxB_.data(), t, computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), ie, xdot.data(), - xdot_old.data(), xB.data()); + fdeltaxB( + derived_state_.deltaxB_.data(), t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), ie, xdot.data(), xdot_old.data(), xB.data() + ); if (always_check_finite_) { checkFinite(derived_state_.deltaxB_, ModelQuantity::deltaxB); @@ -1330,20 +1505,23 @@ void Model::addAdjointStateEventUpdate(AmiVector &xB, const int ie, // apply update for (int ix = 0; ix < nxtrue_solver; ++ix) for (int iJ = 0; iJ < nJ; ++iJ) - xB.at(ix + iJ * nxtrue_solver) += - derived_state_.deltaxB_.at(ix + iJ * nxtrue_solver); + xB.at(ix + iJ * nxtrue_solver) + += derived_state_.deltaxB_.at(ix + iJ * nxtrue_solver); } void Model::addAdjointQuadratureEventUpdate( - AmiVector xQB, const int ie, const realtype t, const AmiVector &x, - const AmiVector &xB, const AmiVector &xdot, const AmiVector &xdot_old) { + AmiVector xQB, int const ie, const realtype t, AmiVector const& x, + AmiVector const& xB, AmiVector const& xdot, AmiVector const& xdot_old +) { for (int ip = 0; ip < nplist(); ip++) { derived_state_.deltaqB_.assign(nJ, 0.0); - fdeltaqB(derived_state_.deltaqB_.data(), t, computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip), ie, - xdot.data(), xdot_old.data(), xB.data()); + fdeltaqB( + derived_state_.deltaqB_.data(), t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip), ie, xdot.data(), xdot_old.data(), + xB.data() + ); for (int iJ = 0; iJ < nJ; ++iJ) xQB.at(iJ) += derived_state_.deltaqB_.at(iJ); @@ -1354,24 +1532,25 @@ void Model::addAdjointQuadratureEventUpdate( } } -void Model::updateHeaviside(const std::vector &rootsfound) { +void Model::updateHeaviside(std::vector const& rootsfound) { for (int ie = 0; ie < ne; ie++) { state_.h.at(ie) += rootsfound.at(ie); } } -void Model::updateHeavisideB(const int *rootsfound) { +void Model::updateHeavisideB(int const* rootsfound) { for (int ie = 0; ie < ne; ie++) { state_.h.at(ie) -= rootsfound[ie]; } } -int Model::checkFinite(gsl::span array, - ModelQuantity model_quantity) const -{ - auto it = std::find_if(array.begin(), array.end(), - [](realtype x){return !std::isfinite(x);}); - if(it == array.end()) { +int Model::checkFinite( + gsl::span array, ModelQuantity model_quantity +) const { + auto it = std::find_if(array.begin(), array.end(), [](realtype x) { + return !std::isfinite(x); + }); + if (it == array.end()) { return AMICI_SUCCESS; } @@ -1401,27 +1580,27 @@ int Model::checkFinite(gsl::span array, case ModelQuantity::JDiag: case ModelQuantity::deltax: case ModelQuantity::deltaxB: - if(hasStateIds()) { + if (hasStateIds()) { element_id = getStateIdsSolver()[flat_index]; } break; case ModelQuantity::y: - if(hasObservableIds()) { + if (hasObservableIds()) { element_id = getObservableIds()[flat_index]; } break; case ModelQuantity::w: - if(hasExpressionIds()) { + if (hasExpressionIds()) { element_id = getExpressionIds()[flat_index]; } break; case ModelQuantity::k: - if(hasFixedParameterIds()) { + if (hasFixedParameterIds()) { element_id = getFixedParameterIds()[flat_index]; } break; case ModelQuantity::p: - if(hasParameterIds()) { + if (hasParameterIds()) { element_id = getParameterIds()[flat_index]; } break; @@ -1438,26 +1617,21 @@ int Model::checkFinite(gsl::span array, gsl_ExpectsDebug(false); model_quantity_str = std::to_string(static_cast(model_quantity)); } - if(logger) + if (logger) logger->log( - LogSeverity::warning, - msg_id, + LogSeverity::warning, msg_id, "AMICI encountered a %s value for %s[%i] (%s)", - non_finite_type.c_str(), - model_quantity_str.c_str(), - gsl::narrow(flat_index), - element_id.c_str() + non_finite_type.c_str(), model_quantity_str.c_str(), + gsl::narrow(flat_index), element_id.c_str() ); // check upstream, without infinite recursion - if(model_quantity != ModelQuantity::k - && model_quantity != ModelQuantity::p - && model_quantity != ModelQuantity::ts) - { + if (model_quantity != ModelQuantity::k && model_quantity != ModelQuantity::p + && model_quantity != ModelQuantity::ts) { checkFinite(state_.fixedParameters, ModelQuantity::k); checkFinite(state_.unscaledParameters, ModelQuantity::p); checkFinite(simulation_parameters_.ts_, ModelQuantity::ts); - if(!always_check_finite_ && model_quantity != ModelQuantity::w) { + if (!always_check_finite_ && model_quantity != ModelQuantity::w) { // don't check twice if always_check_finite_ is true checkFinite(derived_state_.w_, ModelQuantity::w); } @@ -1465,12 +1639,14 @@ int Model::checkFinite(gsl::span array, return AMICI_RECOVERABLE_ERROR; } -int Model::checkFinite(gsl::span array, - ModelQuantity model_quantity, size_t num_cols) const -{ - auto it = std::find_if(array.begin(), array.end(), - [](realtype x){return !std::isfinite(x);}); - if(it == array.end()) { +int Model::checkFinite( + gsl::span array, ModelQuantity model_quantity, + size_t num_cols +) const { + auto it = std::find_if(array.begin(), array.end(), [](realtype x) { + return !std::isfinite(x); + }); + if (it == array.end()) { return AMICI_SUCCESS; } @@ -1495,27 +1671,27 @@ int Model::checkFinite(gsl::span array, case ModelQuantity::ssigmay: case ModelQuantity::dydp: case ModelQuantity::dsigmaydp: - if(hasObservableIds()) + if (hasObservableIds()) row_id += " " + getObservableIds()[row]; - if(hasParameterIds()) + if (hasParameterIds()) col_id += " " + getParameterIds()[plist(gsl::narrow(col))]; break; case ModelQuantity::dydx: - if(hasObservableIds()) + if (hasObservableIds()) row_id += " " + getObservableIds()[row]; - if(hasStateIds()) + if (hasStateIds()) col_id += " " + getStateIdsSolver()[col]; break; case ModelQuantity::deltasx: - if(hasStateIds()) + if (hasStateIds()) row_id += " " + getStateIdsSolver()[row]; - if(hasParameterIds()) + if (hasParameterIds()) col_id += " " + getParameterIds()[plist(gsl::narrow(col))]; break; case ModelQuantity::dJydy: case ModelQuantity::dJydy_matlab: case ModelQuantity::dJydsigma: - if(hasObservableIds()) + if (hasObservableIds()) col_id += " " + getObservableIds()[col]; break; case ModelQuantity::dJydx: @@ -1523,7 +1699,7 @@ int Model::checkFinite(gsl::span array, case ModelQuantity::dJrzdx: case ModelQuantity::dzdx: case ModelQuantity::drzdx: - if(hasStateIds()) + if (hasStateIds()) col_id += " " + getStateIdsSolver()[col]; break; case ModelQuantity::deltaqB: @@ -1531,11 +1707,11 @@ int Model::checkFinite(gsl::span array, case ModelQuantity::dzdp: case ModelQuantity::drzdp: case ModelQuantity::dsigmazdp: - if(hasParameterIds()) + if (hasParameterIds()) col_id += " " + getParameterIds()[plist(gsl::narrow(col))]; break; case ModelQuantity::dsigmaydy: - if(hasObservableIds()) { + if (hasObservableIds()) { auto obs_ids = getObservableIds(); row_id += " " + obs_ids[row]; col_id += " " + obs_ids[col]; @@ -1555,16 +1731,12 @@ int Model::checkFinite(gsl::span array, model_quantity_str = std::to_string(static_cast(model_quantity)); } - if(logger) + if (logger) logger->log( - LogSeverity::warning, - msg_id, + LogSeverity::warning, msg_id, "AMICI encountered a %s value for %s[%i] (%s, %s)", - non_finite_type.c_str(), - model_quantity_str.c_str(), - gsl::narrow(flat_index), - row_id.c_str(), - col_id.c_str() + non_finite_type.c_str(), model_quantity_str.c_str(), + gsl::narrow(flat_index), row_id.c_str(), col_id.c_str() ); // check upstream @@ -1576,14 +1748,15 @@ int Model::checkFinite(gsl::span array, return AMICI_RECOVERABLE_ERROR; } -int Model::checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) const -{ +int Model::checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) + const { // check flat array, to see if there are any issues // (faster, in particular for sparse arrays) auto m_flat = gsl::make_span(m); - auto it = std::find_if(m_flat.begin(), m_flat.end(), - [](realtype x){return !std::isfinite(x);}); - if(it == m_flat.end()) { + auto it = std::find_if(m_flat.begin(), m_flat.end(), [](realtype x) { + return !std::isfinite(x); + }); + if (it == m_flat.end()) { return AMICI_SUCCESS; } @@ -1601,7 +1774,8 @@ int Model::checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) co non_finite_type = "Inf"; } else { throw std::runtime_error( - "Value is not finite, but neither infinite nor NaN."); + "Value is not finite, but neither infinite nor NaN." + ); } std::string row_id = std::to_string(row); std::string col_id = std::to_string(col); @@ -1609,29 +1783,29 @@ int Model::checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) co switch (model_quantity) { case ModelQuantity::J: case ModelQuantity::JB: - if(hasStateIds()) { + if (hasStateIds()) { auto state_ids = getStateIdsSolver(); row_id += " " + state_ids[row]; col_id += " " + state_ids[col]; } break; case ModelQuantity::dwdx: - if(hasExpressionIds()) + if (hasExpressionIds()) row_id += " " + getExpressionIds()[row]; - if(hasStateIds()) + if (hasStateIds()) col_id += " " + getStateIdsSolver()[col]; break; case ModelQuantity::dwdw: - if(hasExpressionIds()) { + if (hasExpressionIds()) { auto expr_ids = getExpressionIds(); row_id += " " + expr_ids[row]; col_id += " " + expr_ids[col]; } break; case ModelQuantity::dwdp: - if(hasExpressionIds()) + if (hasExpressionIds()) row_id += " " + getExpressionIds()[row]; - if(hasParameterIds()) + if (hasParameterIds()) col_id += " " + getParameterIds()[plist(gsl::narrow(col))]; break; default: @@ -1648,17 +1822,12 @@ int Model::checkFinite(SUNMatrix m, ModelQuantity model_quantity, realtype t) co model_quantity_str = std::to_string(static_cast(model_quantity)); } - if(logger) + if (logger) logger->log( - LogSeverity::warning, - msg_id, + LogSeverity::warning, msg_id, "AMICI encountered a %s value for %s[%i] (%s, %s) at t=%g", - non_finite_type.c_str(), - model_quantity_str.c_str(), - gsl::narrow(flat_index), - row_id.c_str(), - col_id.c_str(), - t + non_finite_type.c_str(), model_quantity_str.c_str(), + gsl::narrow(flat_index), row_id.c_str(), col_id.c_str(), t ); // check upstream @@ -1676,21 +1845,23 @@ void Model::setAlwaysCheckFinite(bool alwaysCheck) { bool Model::getAlwaysCheckFinite() const { return always_check_finite_; } -void Model::fx0(AmiVector &x) { - std::fill(derived_state_.x_rdata_.begin(), derived_state_.x_rdata_.end(), 0.0); +void Model::fx0(AmiVector& x) { + std::fill( + derived_state_.x_rdata_.begin(), derived_state_.x_rdata_.end(), 0.0 + ); /* this function also computes initial total abundances */ fx0(derived_state_.x_rdata_.data(), simulation_parameters_.tstart_, - state_.unscaledParameters.data(), - state_.fixedParameters.data()); + state_.unscaledParameters.data(), state_.fixedParameters.data()); fx_solver(x.data(), derived_state_.x_rdata_.data()); - ftotal_cl(state_.total_cl.data(), derived_state_.x_rdata_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data()); + ftotal_cl( + state_.total_cl.data(), derived_state_.x_rdata_.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); checkFinite(derived_state_.x_rdata_, ModelQuantity::x0_rdata); } -void Model::fx0_fixedParameters(AmiVector &x) { +void Model::fx0_fixedParameters(AmiVector& x) { if (!getReinitializeFixedParameterInitialStates()) return; @@ -1698,94 +1869,104 @@ void Model::fx0_fixedParameters(AmiVector &x) { x0_fixedparameters to (i) enable updates to states that were removed from conservation laws and (ii) be able to correctly compute total abundances after updating the state variables */ - fx_rdata(derived_state_.x_rdata_.data(), computeX_pos(x), - state_.total_cl.data(), state_.unscaledParameters.data(), - state_.fixedParameters.data()); - fx0_fixedParameters(derived_state_.x_rdata_.data(), - simulation_parameters_.tstart_, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - simulation_parameters_.reinitialization_state_idxs_sim - ); + fx_rdata( + derived_state_.x_rdata_.data(), computeX_pos(x), state_.total_cl.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); + fx0_fixedParameters( + derived_state_.x_rdata_.data(), simulation_parameters_.tstart_, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + simulation_parameters_.reinitialization_state_idxs_sim + ); fx_solver(x.data(), derived_state_.x_rdata_.data()); /* update total abundances */ - ftotal_cl(state_.total_cl.data(), derived_state_.x_rdata_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data()); + ftotal_cl( + state_.total_cl.data(), derived_state_.x_rdata_.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); } -void Model::fsx0(AmiVectorArray &sx, const AmiVector &x) { +void Model::fsx0(AmiVectorArray& sx, AmiVector const& x) { /* this function also computes initial total abundance sensitivities */ - realtype *stcl = nullptr; + realtype* stcl = nullptr; for (int ip = 0; ip < nplist(); ip++) { if (ncl() > 0) stcl = &state_.stotal_cl.at(plist(ip) * ncl()); - std::fill(derived_state_.sx_rdata_.begin(), - derived_state_.sx_rdata_.end(), 0.0); - fsx0(derived_state_.sx_rdata_.data(), simulation_parameters_.tstart_, - computeX_pos(x), state_.unscaledParameters.data(), - state_.fixedParameters.data(), plist(ip)); + std::fill( + derived_state_.sx_rdata_.begin(), derived_state_.sx_rdata_.end(), + 0.0 + ); + fsx0( + derived_state_.sx_rdata_.data(), simulation_parameters_.tstart_, + computeX_pos(x), state_.unscaledParameters.data(), + state_.fixedParameters.data(), plist(ip) + ); fsx_solver(sx.data(ip), derived_state_.sx_rdata_.data()); - fstotal_cl(stcl, derived_state_.sx_rdata_.data(), plist(ip), - derived_state_.x_rdata_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - state_.total_cl.data()); + fstotal_cl( + stcl, derived_state_.sx_rdata_.data(), plist(ip), + derived_state_.x_rdata_.data(), state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.total_cl.data() + ); } } -void Model::fsx0_fixedParameters(AmiVectorArray &sx, const AmiVector &x) { +void Model::fsx0_fixedParameters(AmiVectorArray& sx, AmiVector const& x) { if (!getReinitializeFixedParameterInitialStates()) return; - realtype *stcl = nullptr; + realtype* stcl = nullptr; for (int ip = 0; ip < nplist(); ip++) { if (ncl() > 0) stcl = &state_.stotal_cl.at(plist(ip) * ncl()); - fsx_rdata(derived_state_.sx_rdata_.data(), sx.data(ip), stcl, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - x.data(), state_.total_cl.data(), - plist(ip)); - fsx0_fixedParameters(derived_state_.sx_rdata_.data(), - simulation_parameters_.tstart_, - computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - plist(ip), - simulation_parameters_.reinitialization_state_idxs_sim); + fsx_rdata( + derived_state_.sx_rdata_.data(), sx.data(ip), stcl, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + x.data(), state_.total_cl.data(), plist(ip) + ); + fsx0_fixedParameters( + derived_state_.sx_rdata_.data(), simulation_parameters_.tstart_, + computeX_pos(x), state_.unscaledParameters.data(), + state_.fixedParameters.data(), plist(ip), + simulation_parameters_.reinitialization_state_idxs_sim + ); fsx_solver(sx.data(ip), derived_state_.sx_rdata_.data()); - fstotal_cl(stcl, derived_state_.sx_rdata_.data(), plist(ip), - derived_state_.x_rdata_.data(), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - state_.total_cl.data()); + fstotal_cl( + stcl, derived_state_.sx_rdata_.data(), plist(ip), + derived_state_.x_rdata_.data(), state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.total_cl.data() + ); } } void Model::fsdx0() {} -void Model::fx_rdata(AmiVector &x_rdata, const AmiVector &x) { - fx_rdata(x_rdata.data(), computeX_pos(x), state_.total_cl.data(), - state_.unscaledParameters.data(), state_.fixedParameters.data()); +void Model::fx_rdata(AmiVector& x_rdata, AmiVector const& x) { + fx_rdata( + x_rdata.data(), computeX_pos(x), state_.total_cl.data(), + state_.unscaledParameters.data(), state_.fixedParameters.data() + ); if (always_check_finite_) checkFinite(x_rdata.getVector(), ModelQuantity::x_rdata); } -void Model::fsx_rdata(AmiVectorArray &sx_rdata, const AmiVectorArray &sx, - AmiVector const& x_solver) { - realtype *stcl = nullptr; +void Model::fsx_rdata( + AmiVectorArray& sx_rdata, AmiVectorArray const& sx, + AmiVector const& x_solver +) { + realtype* stcl = nullptr; for (int ip = 0; ip < nplist(); ip++) { if (ncl() > 0) stcl = &state_.stotal_cl.at(plist(ip) * ncl()); - fsx_rdata(sx_rdata.data(ip), sx.data(ip), stcl, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), x_solver.data(), - state_.total_cl.data(), plist(ip)); + fsx_rdata( + sx_rdata.data(ip), sx.data(ip), stcl, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + x_solver.data(), state_.total_cl.data(), plist(ip) + ); } } -void Model::writeSliceEvent(gsl::span slice, - gsl::span buffer, const int ie) { +void Model::writeSliceEvent( + gsl::span slice, gsl::span buffer, int const ie +) { checkBufferSize(buffer, slice.size()); checkBufferSize(buffer, z2event_.size()); for (unsigned izt = 0; izt < z2event_.size(); ++izt) @@ -1793,9 +1974,9 @@ void Model::writeSliceEvent(gsl::span slice, buffer[izt] = slice[izt]; } -void Model::writeSensitivitySliceEvent(gsl::span slice, - gsl::span buffer, - const int ie) { +void Model::writeSensitivitySliceEvent( + gsl::span slice, gsl::span buffer, int const ie +) { checkBufferSize(buffer, slice.size()); checkBufferSize(buffer, z2event_.size() * nplist()); for (int ip = 0; ip < nplist(); ++ip) @@ -1804,26 +1985,33 @@ void Model::writeSensitivitySliceEvent(gsl::span slice, buffer[ip * nztrue + izt] = slice[ip * nztrue + izt]; } -void Model::writeLLHSensitivitySlice(const std::vector &dLLhdp, - std::vector &sllh, - std::vector &s2llh) { +void Model::writeLLHSensitivitySlice( + std::vector const& dLLhdp, std::vector& sllh, + std::vector& s2llh +) { checkLLHBufferSize(sllh, s2llh); amici_daxpy(nplist(), -1.0, dLLhdp.data(), nJ, sllh.data(), 1); for (int iJ = 1; iJ < nJ; ++iJ) - amici_daxpy(nplist(), -1.0, &dLLhdp.at(iJ), nJ, &s2llh.at(iJ - 1), - nJ - 1); + amici_daxpy( + nplist(), -1.0, &dLLhdp.at(iJ), nJ, &s2llh.at(iJ - 1), nJ - 1 + ); } -void Model::checkLLHBufferSize(std::vector const &sllh, - std::vector const &s2llh) const { +void Model::checkLLHBufferSize( + std::vector const& sllh, std::vector const& s2llh +) const { if (sllh.size() != gsl::narrow(nplist())) - throw AmiException("Incorrect sllh buffer size! Was %u, expected %i.", - sllh.size(), nplist()); + throw AmiException( + "Incorrect sllh buffer size! Was %u, expected %i.", sllh.size(), + nplist() + ); if (s2llh.size() != gsl::narrow((nJ - 1) * nplist())) - throw AmiException("Incorrect s2llh buffer size! Was %u, expected %i.", - s2llh.size(), (nJ - 1) * nplist()); + throw AmiException( + "Incorrect s2llh buffer size! Was %u, expected %i.", s2llh.size(), + (nJ - 1) * nplist() + ); } void Model::initializeVectors() { @@ -1832,7 +2020,7 @@ void Model::initializeVectors() { derived_state_.dxdotdp = AmiVectorArray(nx_solver, nplist()); } -void Model::fy(const realtype t, const AmiVector &x) { +void Model::fy(const realtype t, AmiVector const& x) { if (!ny) return; @@ -1841,16 +2029,18 @@ void Model::fy(const realtype t, const AmiVector &x) { derived_state_.y_.assign(ny, 0.0); fw(t, x_pos); - fy(derived_state_.y_.data(), t, x_pos, - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data()); + fy(derived_state_.y_.data(), t, x_pos, state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data()); if (always_check_finite_) { - checkFinite(gsl::make_span(derived_state_.y_.data(), ny), ModelQuantity::y); + checkFinite( + gsl::make_span(derived_state_.y_.data(), ny), ModelQuantity::y + ); } } -void Model::fdydp(const realtype t, const AmiVector &x) { +void Model::fdydp(const realtype t, AmiVector const& x) { if (!ny) return; @@ -1863,16 +2053,20 @@ void Model::fdydp(const realtype t, const AmiVector &x) { /* get dydp slice (ny) for current time and parameter */ for (int ip = 0; ip < nplist(); ip++) if (pythonGenerated) { - fdydp(&derived_state_.dydp_.at(ip * ny), t, x_pos, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip), - derived_state_.w_.data(), state_.total_cl.data(), - state_.stotal_cl.data()); + fdydp( + &derived_state_.dydp_.at(ip * ny), t, x_pos, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip), derived_state_.w_.data(), + state_.total_cl.data(), state_.stotal_cl.data(), + state_.spl_.data(), derived_state_.sspl_.data() + ); } else { - fdydp(&derived_state_.dydp_.at(ip * ny), t, x_pos, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip), - derived_state_.w_.data(), derived_state_.dwdp_.data()); + fdydp( + &derived_state_.dydp_.at(ip * ny), t, x_pos, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip), derived_state_.w_.data(), + derived_state_.dwdp_.data() + ); } if (always_check_finite_) { @@ -1880,7 +2074,7 @@ void Model::fdydp(const realtype t, const AmiVector &x) { } } -void Model::fdydx(const realtype t, const AmiVector &x) { +void Model::fdydx(const realtype t, AmiVector const& x) { if (!ny) return; @@ -1890,27 +2084,28 @@ void Model::fdydx(const realtype t, const AmiVector &x) { fw(t, x_pos); fdwdx(t, x_pos); - fdydx(derived_state_.dydx_.data(), t, x_pos, - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data(), - derived_state_.dwdx_.data()); + fdydx( + derived_state_.dydx_.data(), t, x_pos, state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data(), derived_state_.dwdx_.data() + ); if (always_check_finite_) { checkFinite(derived_state_.dydx_, ModelQuantity::dydx, ny); } } -void Model::fsigmay(const int it, const ExpData *edata) { +void Model::fsigmay(int const it, ExpData const* edata) { if (!ny) return; derived_state_.sigmay_.assign(ny, 0.0); - fsigmay(derived_state_.sigmay_.data(), - getTimepoint(it), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.y_.data()); + fsigmay( + derived_state_.sigmay_.data(), getTimepoint(it), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data() + ); if (edata) { auto sigmay_edata = edata->getObservedDataStdDevPtr(it); @@ -1924,15 +2119,17 @@ void Model::fsigmay(const int it, const ExpData *edata) { * that this is actually what we want */ for (int iJ = 1; iJ < nJ; iJ++) - derived_state_.sigmay_.at(iytrue + iJ*nytrue) = 0; + derived_state_.sigmay_.at(iytrue + iJ * nytrue) = 0; if (edata->isSetObservedData(it, iytrue)) - checkSigmaPositivity(derived_state_.sigmay_.at(iytrue), "sigmay"); + checkSigmaPositivity( + derived_state_.sigmay_.at(iytrue), "sigmay" + ); } } } -void Model::fdsigmaydp(const int it, const ExpData *edata) { +void Model::fdsigmaydp(int const it, ExpData const* edata) { if (!ny) return; @@ -1940,11 +2137,11 @@ void Model::fdsigmaydp(const int it, const ExpData *edata) { for (int ip = 0; ip < nplist(); ip++) // get dsigmaydp slice (ny) for current timepoint and parameter - fdsigmaydp(&derived_state_.dsigmaydp_.at(ip * ny), getTimepoint(it), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.y_.data(), - plist(ip)); + fdsigmaydp( + &derived_state_.dsigmaydp_.at(ip * ny), getTimepoint(it), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data(), plist(ip) + ); // sigmas in edata override model-sigma -> for those sigmas, set dsigmaydp // to zero @@ -1959,25 +2156,27 @@ void Model::fdsigmaydp(const int it, const ExpData *edata) { } if (always_check_finite_) { - checkFinite(derived_state_.dsigmaydp_, ModelQuantity::dsigmaydp, - nplist()); + checkFinite( + derived_state_.dsigmaydp_, ModelQuantity::dsigmaydp, nplist() + ); } } -void Model::fdsigmaydy(const int it, const ExpData *edata) { +void Model::fdsigmaydy(int const it, ExpData const* edata) { if (!ny) return; derived_state_.dsigmaydy_.assign(ny * ny, 0.0); // get dsigmaydy slice (ny) for current timepoint - fdsigmaydy(derived_state_.dsigmaydy_.data(), getTimepoint(it), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.y_.data()); + fdsigmaydy( + derived_state_.dsigmaydy_.data(), getTimepoint(it), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data() + ); - // sigmas in edata override model-sigma -> for those sigmas, set dsigmaydy - // to zero + // sigmas in edata override model-sigma -> for those sigmas, set dsigmaydy + // to zero if (edata) { for (int isigmay = 0; isigmay < nytrue; ++isigmay) { if (!edata->isSetObservedDataStdDev(it, isigmay)) @@ -1993,8 +2192,7 @@ void Model::fdsigmaydy(const int it, const ExpData *edata) { } } - -void Model::fdJydy(const int it, const AmiVector &x, const ExpData &edata) { +void Model::fdJydy(int const it, AmiVector const& x, ExpData const& edata) { if (!ny) return; @@ -2019,59 +2217,70 @@ void Model::fdJydy(const int it, const AmiVector &x, const ExpData &edata) { continue; // get dJydy slice (ny) for current timepoint and observable - fdJydy(derived_state_.dJydy_.at(iyt).data(), iyt, state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.y_.data(), - derived_state_.sigmay_.data(), - edata.getObservedDataPtr(it)); + fdJydy( + derived_state_.dJydy_.at(iyt).data(), iyt, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data(), derived_state_.sigmay_.data(), + edata.getObservedDataPtr(it) + ); // dJydy += dJydsigma * dsigmaydy // C(nJ,ny) A(nJ,ny) * B(ny,ny) // sparse dense dense tmp_dense.zero(); - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, ny, ny, 1.0, - &derived_state_.dJydsigma_.at(iyt * nJ * ny), nJ, - derived_state_.dsigmaydy_.data(), ny, 1.0, - tmp_dense.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, ny, ny, 1.0, + &derived_state_.dJydsigma_.at(iyt * nJ * ny), nJ, + derived_state_.dsigmaydy_.data(), ny, 1.0, tmp_dense.data(), nJ + ); auto tmp_sparse = SUNMatrixWrapper(tmp_dense, 0.0, CSC_MAT); - auto ret = SUNMatScaleAdd(1.0, derived_state_.dJydy_.at(iyt).get(), - tmp_sparse.get()); - if(ret != SUNMAT_SUCCESS) { - throw AmiException("SUNMatScaleAdd failed with status %d in %s", - ret, __func__); + auto ret = SUNMatScaleAdd( + 1.0, derived_state_.dJydy_.at(iyt).get(), tmp_sparse.get() + ); + if (ret != SUNMAT_SUCCESS) { + throw AmiException( + "SUNMatScaleAdd failed with status %d in %s", ret, __func__ + ); } derived_state_.dJydy_.at(iyt).refresh(); if (always_check_finite_) { checkFinite( gsl::make_span(derived_state_.dJydy_.at(iyt).get()), - ModelQuantity::dJydy, ny); + ModelQuantity::dJydy, ny + ); } } } else { - std::fill(derived_state_.dJydy_matlab_.begin(), - derived_state_.dJydy_matlab_.end(), 0.0); + std::fill( + derived_state_.dJydy_matlab_.begin(), + derived_state_.dJydy_matlab_.end(), 0.0 + ); for (int iyt = 0; iyt < nytrue; iyt++) { if (!edata.isSetObservedData(it, iyt)) continue; - fdJydy(&derived_state_.dJydy_matlab_.at(iyt * ny * nJ), iyt, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.y_.data(), - derived_state_.sigmay_.data(), - edata.getObservedDataPtr(it)); + fdJydy( + &derived_state_.dJydy_matlab_.at(iyt * ny * nJ), iyt, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data(), derived_state_.sigmay_.data(), + edata.getObservedDataPtr(it) + ); if (always_check_finite_) { // get dJydy slice (ny) for current timepoint and observable checkFinite( gsl::span( - &derived_state_.dJydy_matlab_[iyt * ny * nJ], ny * nJ), - ModelQuantity::dJydy, ny); + &derived_state_.dJydy_matlab_[iyt * ny * nJ], ny * nJ + ), + ModelQuantity::dJydy, ny + ); } } } } -void Model::fdJydsigma(const int it, const AmiVector &x, const ExpData &edata) { +void Model::fdJydsigma(int const it, AmiVector const& x, ExpData const& edata) { if (!ny) return; @@ -2083,22 +2292,25 @@ void Model::fdJydsigma(const int it, const AmiVector &x, const ExpData &edata) { for (int iyt = 0; iyt < nytrue; iyt++) { if (edata.isSetObservedData(it, iyt)) { // get dJydsigma slice (ny) for current timepoint and observable - fdJydsigma(&derived_state_.dJydsigma_.at(iyt * ny * nJ), iyt, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.y_.data(), - derived_state_.sigmay_.data(), - edata.getObservedDataPtr(it)); + fdJydsigma( + &derived_state_.dJydsigma_.at(iyt * ny * nJ), iyt, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.y_.data(), derived_state_.sigmay_.data(), + edata.getObservedDataPtr(it) + ); if (always_check_finite_) { checkFinite( gsl::span( - &derived_state_.dJydsigma_.at(iyt * ny * nJ), ny * nJ), - ModelQuantity::dJydsigma, ny); + &derived_state_.dJydsigma_.at(iyt * ny * nJ), ny * nJ + ), + ModelQuantity::dJydsigma, ny + ); } } } } -void Model::fdJydp(const int it, const AmiVector &x, const ExpData &edata) { +void Model::fdJydp(int const it, AmiVector const& x, ExpData const& edata) { // dJydy nJ, nytrue x ny // dydp nplist * ny // dJydp nplist x nJ @@ -2123,26 +2335,35 @@ void Model::fdJydp(const int it, const AmiVector &x, const ExpData &edata) { // dJydp = 1.0 * dJydp + 1.0 * dJydy * dydp for (int iplist = 0; iplist < nplist(); ++iplist) { derived_state_.dJydy_.at(iyt).multiply( - gsl::span(&derived_state_.dJydp_.at(iplist * nJ), nJ), - gsl::span(&derived_state_.dydp_.at(iplist * ny), ny)); + gsl::span( + &derived_state_.dJydp_.at(iplist * nJ), nJ + ), + gsl::span( + &derived_state_.dydp_.at(iplist * ny), ny + ) + ); } } else { - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), ny, 1.0, - &derived_state_.dJydy_matlab_.at(iyt * nJ * ny), nJ, - derived_state_.dydp_.data(), ny, - 1.0, derived_state_.dJydp_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), ny, 1.0, + &derived_state_.dJydy_matlab_.at(iyt * nJ * ny), nJ, + derived_state_.dydp_.data(), ny, 1.0, + derived_state_.dJydp_.data(), nJ + ); } // dJydp = 1.0 * dJydp + 1.0 * dJydsigma * dsigmaydp - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), ny, 1.0, - &derived_state_.dJydsigma_.at(iyt * nJ * ny), nJ, - derived_state_.dsigmaydp_.data(), ny, 1.0, - derived_state_.dJydp_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), ny, 1.0, + &derived_state_.dJydsigma_.at(iyt * nJ * ny), nJ, + derived_state_.dsigmaydp_.data(), ny, 1.0, + derived_state_.dJydp_.data(), nJ + ); } } -void Model::fdJydx(const int it, const AmiVector &x, const ExpData &edata) { +void Model::fdJydx(int const it, AmiVector const& x, ExpData const& edata) { if (!ny) return; @@ -2166,14 +2387,19 @@ void Model::fdJydx(const int it, const AmiVector &x, const ExpData &edata) { for (int ix = 0; ix < nx_solver; ++ix) { derived_state_.dJydy_.at(iyt).multiply( gsl::span(&derived_state_.dJydx_.at(ix * nJ), nJ), - gsl::span(&derived_state_.dydx_.at(ix * ny), ny)); + gsl::span( + &derived_state_.dydx_.at(ix * ny), ny + ) + ); } } else { - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nx_solver, ny, 1.0, - &derived_state_.dJydy_matlab_.at(iyt * ny * nJ), nJ, - derived_state_.dydx_.data(), ny, - 1.0, derived_state_.dJydx_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nx_solver, ny, 1.0, + &derived_state_.dJydy_matlab_.at(iyt * ny * nJ), nJ, + derived_state_.dydx_.data(), ny, 1.0, + derived_state_.dJydx_.data(), nJ + ); } } @@ -2182,7 +2408,7 @@ void Model::fdJydx(const int it, const AmiVector &x, const ExpData &edata) { } } -void Model::fz(const int ie, const realtype t, const AmiVector &x) { +void Model::fz(int const ie, const realtype t, AmiVector const& x) { derived_state_.z_.assign(nz, 0.0); @@ -2191,16 +2417,18 @@ void Model::fz(const int ie, const realtype t, const AmiVector &x) { state_.h.data()); } -void Model::fdzdp(const int ie, const realtype t, const AmiVector &x) { +void Model::fdzdp(int const ie, const realtype t, AmiVector const& x) { if (!nz) return; derived_state_.dzdp_.assign(nz * nplist(), 0.0); for (int ip = 0; ip < nplist(); ip++) { - fdzdp(derived_state_.dzdp_.data(), ie, t, computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip)); + fdzdp( + derived_state_.dzdp_.data(), ie, t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip) + ); } if (always_check_finite_) { @@ -2208,40 +2436,44 @@ void Model::fdzdp(const int ie, const realtype t, const AmiVector &x) { } } -void Model::fdzdx(const int ie, const realtype t, const AmiVector &x) { +void Model::fdzdx(int const ie, const realtype t, AmiVector const& x) { if (!nz) return; derived_state_.dzdx_.assign(nz * nx_solver, 0.0); - fdzdx(derived_state_.dzdx_.data(), ie, t, computeX_pos(x), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data()); + fdzdx( + derived_state_.dzdx_.data(), ie, t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data() + ); if (always_check_finite_) { checkFinite(derived_state_.dzdx_, ModelQuantity::dzdx, nx_solver); } } -void Model::frz(const int ie, const realtype t, const AmiVector &x) { +void Model::frz(int const ie, const realtype t, AmiVector const& x) { derived_state_.rz_.assign(nz, 0.0); frz(derived_state_.rz_.data(), ie, t, computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data()); + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data()); } -void Model::fdrzdp(const int ie, const realtype t, const AmiVector &x) { +void Model::fdrzdp(int const ie, const realtype t, AmiVector const& x) { if (!nz) return; derived_state_.drzdp_.assign(nz * nplist(), 0.0); for (int ip = 0; ip < nplist(); ip++) { - fdrzdp(derived_state_.drzdp_.data(), ie, t, computeX_pos(x), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip)); + fdrzdp( + derived_state_.drzdp_.data(), ie, t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip) + ); } if (always_check_finite_) { @@ -2249,36 +2481,41 @@ void Model::fdrzdp(const int ie, const realtype t, const AmiVector &x) { } } -void Model::fdrzdx(const int ie, const realtype t, const AmiVector &x) { +void Model::fdrzdx(int const ie, const realtype t, AmiVector const& x) { if (!nz) return; derived_state_.drzdx_.assign(nz * nx_solver, 0.0); - fdrzdx(derived_state_.drzdx_.data(), ie, t, computeX_pos(x), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data()); + fdrzdx( + derived_state_.drzdx_.data(), ie, t, computeX_pos(x), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data() + ); if (always_check_finite_) { checkFinite(derived_state_.drzdx_, ModelQuantity::drzdx, nx_solver); } } -void Model::fsigmaz(const int ie, const int nroots, const realtype t, - const ExpData *edata) { +void Model::fsigmaz( + int const ie, int const nroots, const realtype t, ExpData const* edata +) { if (!nz) return; derived_state_.sigmaz_.assign(nz, 0.0); - fsigmaz(derived_state_.sigmaz_.data(), t, state_.unscaledParameters.data(), - state_.fixedParameters.data()); + fsigmaz( + derived_state_.sigmaz_.data(), t, state_.unscaledParameters.data(), + state_.fixedParameters.data() + ); if (edata) { for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (z2event_.at(iztrue) - 1 == ie) { if (edata->isSetObservedEventsStdDev(nroots, iztrue)) { - auto sigmaz_edata = - edata->getObservedEventsStdDevPtr(nroots); + auto sigmaz_edata + = edata->getObservedEventsStdDevPtr(nroots); derived_state_.sigmaz_.at(iztrue) = sigmaz_edata[iztrue]; } @@ -2286,18 +2523,20 @@ void Model::fsigmaz(const int ie, const int nroots, const realtype t, * that this is actually what we want */ for (int iJ = 1; iJ < nJ; iJ++) - derived_state_.sigmaz_.at(iztrue + iJ*nztrue) = 0; + derived_state_.sigmaz_.at(iztrue + iJ * nztrue) = 0; if (edata->isSetObservedEvents(nroots, iztrue)) - checkSigmaPositivity(derived_state_.sigmaz_.at(iztrue), - "sigmaz"); + checkSigmaPositivity( + derived_state_.sigmaz_.at(iztrue), "sigmaz" + ); } } } } -void Model::fdsigmazdp(const int ie, const int nroots, const realtype t, - const ExpData *edata) { +void Model::fdsigmazdp( + int const ie, int const nroots, const realtype t, ExpData const* edata +) { if (!nz) return; @@ -2305,16 +2544,19 @@ void Model::fdsigmazdp(const int ie, const int nroots, const realtype t, for (int ip = 0; ip < nplist(); ip++) { // get dsigmazdp slice (nz) for current event and parameter - fdsigmazdp(&derived_state_.dsigmazdp_.at(ip * nz), t, state_.unscaledParameters.data(), - state_.fixedParameters.data(), plist(ip)); + fdsigmazdp( + &derived_state_.dsigmazdp_.at(ip * nz), t, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + plist(ip) + ); } // sigmas in edata override model-sigma -> for those sigmas, set dsigmazdp // to zero if (edata) { for (int iz = 0; iz < nztrue; iz++) { - if (z2event_.at(iz) - 1 == ie && - !edata->isSetObservedEventsStdDev(nroots, iz)) { + if (z2event_.at(iz) - 1 == ie + && !edata->isSetObservedEventsStdDev(nroots, iz)) { for (int ip = 0; ip < nplist(); ip++) derived_state_.dsigmazdp_.at(iz + nz * ip) = 0; } @@ -2322,13 +2564,16 @@ void Model::fdsigmazdp(const int ie, const int nroots, const realtype t, } if (always_check_finite_) { - checkFinite(derived_state_.dsigmazdp_, ModelQuantity::dsigmazdp, - nplist()); + checkFinite( + derived_state_.dsigmazdp_, ModelQuantity::dsigmazdp, nplist() + ); } } -void Model::fdJzdz(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJzdz( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata +) { if (!nz) return; @@ -2339,24 +2584,28 @@ void Model::fdJzdz(const int ie, const int nroots, const realtype t, for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (edata.isSetObservedEvents(nroots, iztrue)) { - fdJzdz(&derived_state_.dJzdz_.at(iztrue * nz * nJ), iztrue, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), - derived_state_.z_.data(), derived_state_.sigmaz_.data(), - edata.getObservedEventsPtr(nroots)); + fdJzdz( + &derived_state_.dJzdz_.at(iztrue * nz * nJ), iztrue, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.z_.data(), derived_state_.sigmaz_.data(), + edata.getObservedEventsPtr(nroots) + ); if (always_check_finite_) { checkFinite( gsl::span( - &derived_state_.dJzdz_.at(iztrue * nz * nJ), - nz * nJ), - ModelQuantity::dJzdz, nz); + &derived_state_.dJzdz_.at(iztrue * nz * nJ), nz * nJ + ), + ModelQuantity::dJzdz, nz + ); } } } } -void Model::fdJzdsigma(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJzdsigma( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata +) { if (!nz) return; @@ -2367,24 +2616,28 @@ void Model::fdJzdsigma(const int ie, const int nroots, const realtype t, for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (edata.isSetObservedEvents(nroots, iztrue)) { - fdJzdsigma(&derived_state_.dJzdsigma_.at(iztrue * nz * nJ), iztrue, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.z_.data(), - derived_state_.sigmaz_.data(), - edata.getObservedEventsPtr(nroots)); + fdJzdsigma( + &derived_state_.dJzdsigma_.at(iztrue * nz * nJ), iztrue, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.z_.data(), derived_state_.sigmaz_.data(), + edata.getObservedEventsPtr(nroots) + ); if (always_check_finite_) { checkFinite( gsl::span( - &derived_state_.dJzdsigma_.at(iztrue * nz * nJ), - nz * nJ), - ModelQuantity::dJzdsigma, nz); + &derived_state_.dJzdsigma_.at(iztrue * nz * nJ), nz * nJ + ), + ModelQuantity::dJzdsigma, nz + ); } } } } -void Model::fdJzdp(const int ie, const int nroots, realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJzdp( + int const ie, int const nroots, realtype t, AmiVector const& x, + ExpData const& edata +) { if (!nz) return; // dJzdz nJ x nz x nztrue @@ -2413,40 +2666,50 @@ void Model::fdJzdp(const int ie, const int nroots, realtype t, if (t < edata.getTimepoint(edata.nt() - 1)) { // with z - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, - &derived_state_.dJzdz_.at(izt * nz * nJ), nJ, - derived_state_.dzdp_.data(), nz, 1.0, - derived_state_.dJzdp_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, + &derived_state_.dJzdz_.at(izt * nz * nJ), nJ, + derived_state_.dzdp_.data(), nz, 1.0, + derived_state_.dJzdp_.data(), nJ + ); } else { // with rz - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, - &derived_state_.dJrzdsigma_.at(izt * nz * nJ), nJ, - derived_state_.dsigmazdp_.data(), nz, - 1.0, derived_state_.dJzdp_.data(), nJ); - - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, - &derived_state_.dJrzdz_.at(izt * nz * nJ), nJ, - derived_state_.dzdp_.data(), nz, 1.0, - derived_state_.dJzdp_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, + &derived_state_.dJrzdsigma_.at(izt * nz * nJ), nJ, + derived_state_.dsigmazdp_.data(), nz, 1.0, + derived_state_.dJzdp_.data(), nJ + ); + + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, + &derived_state_.dJrzdz_.at(izt * nz * nJ), nJ, + derived_state_.dzdp_.data(), nz, 1.0, + derived_state_.dJzdp_.data(), nJ + ); } - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, - &derived_state_.dJzdsigma_.at(izt * nz * nJ), nJ, - derived_state_.dsigmazdp_.data(), nz, 1.0, - derived_state_.dJzdp_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nplist(), nz, 1.0, + &derived_state_.dJzdsigma_.at(izt * nz * nJ), nJ, + derived_state_.dsigmazdp_.data(), nz, 1.0, + derived_state_.dJzdp_.data(), nJ + ); } } -void Model::fdJzdx(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJzdx( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata +) { // dJzdz nJ x nz x nztrue // dzdx nz x nx_solver // dJzdx nJ x nx_solver x nmaxevent - if(!nz) + if (!nz) return; derived_state_.dJzdx_.assign(nJ * nx_solver, 0.0); @@ -2455,7 +2718,7 @@ void Model::fdJzdx(const int ie, const int nroots, const realtype t, fdJrzdz(ie, nroots, t, x, edata); fdzdx(ie, t, x); fdrzdx(ie, t, x); - + setNaNtoZero(derived_state_.dJzdz_); setNaNtoZero(derived_state_.dJrzdz_); setNaNtoZero(derived_state_.dzdx_); @@ -2467,24 +2730,30 @@ void Model::fdJzdx(const int ie, const int nroots, const realtype t, if (t < edata.getTimepoint(edata.nt() - 1)) { // z - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nx_solver, nz, 1.0, - &derived_state_.dJzdz_.at(izt * nz * nJ), nJ, - derived_state_.dzdx_.data(), nz, 1.0, - derived_state_.dJzdx_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nx_solver, nz, 1.0, + &derived_state_.dJzdz_.at(izt * nz * nJ), nJ, + derived_state_.dzdx_.data(), nz, 1.0, + derived_state_.dJzdx_.data(), nJ + ); } else { // rz - amici_dgemm(BLASLayout::colMajor, BLASTranspose::noTrans, - BLASTranspose::noTrans, nJ, nx_solver, nz, 1.0, - &derived_state_.dJrzdz_.at(izt * nz * nJ), nJ, - derived_state_.drzdx_.data(), nz, 1.0, - derived_state_.dJzdx_.data(), nJ); + amici_dgemm( + BLASLayout::colMajor, BLASTranspose::noTrans, + BLASTranspose::noTrans, nJ, nx_solver, nz, 1.0, + &derived_state_.dJrzdz_.at(izt * nz * nJ), nJ, + derived_state_.drzdx_.data(), nz, 1.0, + derived_state_.dJzdx_.data(), nJ + ); } } } -void Model::fdJrzdz(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJrzdz( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata +) { if (!nz) return; @@ -2495,23 +2764,27 @@ void Model::fdJrzdz(const int ie, const int nroots, const realtype t, for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (edata.isSetObservedEvents(nroots, iztrue)) { - fdJrzdz(&derived_state_.dJrzdz_.at(iztrue * nz * nJ), iztrue, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.rz_.data(), - derived_state_.sigmaz_.data()); + fdJrzdz( + &derived_state_.dJrzdz_.at(iztrue * nz * nJ), iztrue, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.rz_.data(), derived_state_.sigmaz_.data() + ); if (always_check_finite_) { checkFinite( gsl::span( - &derived_state_.dJrzdz_.at(iztrue * nz * nJ), - nz * nJ), - ModelQuantity::dJrzdz, nz); + &derived_state_.dJrzdz_.at(iztrue * nz * nJ), nz * nJ + ), + ModelQuantity::dJrzdz, nz + ); } } } } -void Model::fdJrzdsigma(const int ie, const int nroots, const realtype t, - const AmiVector &x, const ExpData &edata) { +void Model::fdJrzdsigma( + int const ie, int const nroots, const realtype t, AmiVector const& x, + ExpData const& edata +) { if (!nz) return; @@ -2522,32 +2795,52 @@ void Model::fdJrzdsigma(const int ie, const int nroots, const realtype t, for (int iztrue = 0; iztrue < nztrue; iztrue++) { if (edata.isSetObservedEvents(nroots, iztrue)) { - fdJrzdsigma(&derived_state_.dJrzdsigma_.at(iztrue * nz * nJ), iztrue, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), derived_state_.rz_.data(), - derived_state_.sigmaz_.data()); + fdJrzdsigma( + &derived_state_.dJrzdsigma_.at(iztrue * nz * nJ), iztrue, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + derived_state_.rz_.data(), derived_state_.sigmaz_.data() + ); if (always_check_finite_) { checkFinite( gsl::span( &derived_state_.dJrzdsigma_.at(iztrue * nz * nJ), - nz * nJ), - ModelQuantity::dJrzdsigma, nz); + nz * nJ + ), + ModelQuantity::dJrzdsigma, nz + ); } } } } -void Model::fw(const realtype t, const realtype *x) { +void Model::fspl(const realtype t) { + for (int ispl = 0; ispl < nspl; ispl++) + state_.spl_[ispl] = splines_[ispl].get_value(t); +} + +void Model::fsspl(const realtype t) { + derived_state_.sspl_.zero(); + realtype* sspl_data = derived_state_.sspl_.data(); + for (int ip = 0; ip < nplist(); ip++) { + for (int ispl = 0; ispl < nspl; ispl++) + sspl_data[ispl + nspl * plist(ip)] + = splines_[ispl].get_sensitivity(t, ip, state_.spl_[ispl]); + } +} + +void Model::fw(const realtype t, realtype const* x) { std::fill(derived_state_.w_.begin(), derived_state_.w_.end(), 0.0); + fspl(t); fw(derived_state_.w_.data(), t, x, state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), state_.total_cl.data()); + state_.fixedParameters.data(), state_.h.data(), state_.total_cl.data(), + state_.spl_.data()); if (always_check_finite_) { checkFinite(derived_state_.w_, ModelQuantity::w); } } -void Model::fdwdp(const realtype t, const realtype *x) { +void Model::fdwdp(const realtype t, realtype const* x) { if (!nw) return; @@ -2556,19 +2849,25 @@ void Model::fdwdp(const realtype t, const realtype *x) { if (pythonGenerated) { if (!dwdp_hierarchical_.at(0).capacity()) return; - fdwdw(t,x); + fsspl(t); + fdwdw(t, x); dwdp_hierarchical_.at(0).zero(); fdwdp_colptrs(dwdp_hierarchical_.at(0)); fdwdp_rowvals(dwdp_hierarchical_.at(0)); - fdwdp(dwdp_hierarchical_.at(0).data(), t, x, - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data(), state_.total_cl.data(), - state_.stotal_cl.data()); + fdwdp( + dwdp_hierarchical_.at(0).data(), t, x, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), derived_state_.w_.data(), state_.total_cl.data(), + state_.stotal_cl.data(), state_.spl_.data(), + derived_state_.sspl_.data() + ); for (int irecursion = 1; irecursion <= w_recursion_depth_; irecursion++) { - dwdw_.sparse_multiply(dwdp_hierarchical_.at(irecursion), - dwdp_hierarchical_.at(irecursion - 1)); + dwdw_.sparse_multiply( + dwdp_hierarchical_.at(irecursion), + dwdp_hierarchical_.at(irecursion - 1) + ); } derived_state_.dwdp_.sparse_sum(dwdp_hierarchical_); @@ -2576,10 +2875,13 @@ void Model::fdwdp(const realtype t, const realtype *x) { if (!derived_state_.dwdp_.capacity()) return; // matlab generated - fdwdp(derived_state_.dwdp_.data(), t, x, - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data(), - state_.total_cl.data(), state_.stotal_cl.data()); + fdwdp( + derived_state_.dwdp_.data(), t, x, state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data(), state_.total_cl.data(), + state_.stotal_cl.data(), state_.spl_.data(), + derived_state_.sspl_.data() + ); } if (always_check_finite_) { @@ -2587,7 +2889,7 @@ void Model::fdwdp(const realtype t, const realtype *x) { } } -void Model::fdwdx(const realtype t, const realtype *x) { +void Model::fdwdx(const realtype t, realtype const* x) { if (!nw) return; @@ -2596,19 +2898,24 @@ void Model::fdwdx(const realtype t, const realtype *x) { derived_state_.dwdx_.zero(); if (pythonGenerated) { if (!dwdx_hierarchical_.at(0).capacity()) - return; - fdwdw(t,x); + return; + fdwdw(t, x); dwdx_hierarchical_.at(0).zero(); fdwdx_colptrs(dwdx_hierarchical_.at(0)); fdwdx_rowvals(dwdx_hierarchical_.at(0)); - fdwdx(dwdx_hierarchical_.at(0).data(), t, x, - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data(), state_.total_cl.data()); + fdwdx( + dwdx_hierarchical_.at(0).data(), t, x, + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), derived_state_.w_.data(), state_.total_cl.data(), + state_.spl_.data() + ); for (int irecursion = 1; irecursion <= w_recursion_depth_; irecursion++) { - dwdw_.sparse_multiply(dwdx_hierarchical_.at(irecursion), - dwdx_hierarchical_.at(irecursion - 1)); + dwdw_.sparse_multiply( + dwdx_hierarchical_.at(irecursion), + dwdx_hierarchical_.at(irecursion - 1) + ); } derived_state_.dwdx_.sparse_sum(dwdx_hierarchical_); @@ -2616,11 +2923,11 @@ void Model::fdwdx(const realtype t, const realtype *x) { if (!derived_state_.dwdx_.capacity()) return; derived_state_.dwdx_.zero(); - fdwdx(derived_state_.dwdx_.data(), t, x, - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), - derived_state_.w_.data(), - state_.total_cl.data()); + fdwdx( + derived_state_.dwdx_.data(), t, x, state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data(), state_.total_cl.data(), state_.spl_.data() + ); } if (always_check_finite_) { @@ -2628,36 +2935,40 @@ void Model::fdwdx(const realtype t, const realtype *x) { } } -void Model::fdwdw(const realtype t, const realtype *x) { +void Model::fdwdw(const realtype t, realtype const* x) { if (!nw || !dwdw_.capacity()) return; dwdw_.zero(); fdwdw_colptrs(dwdw_); fdwdw_rowvals(dwdw_); - fdwdw(dwdw_.data(), t, x, state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), - derived_state_.w_.data(), state_.total_cl.data()); + fdwdw( + dwdw_.data(), t, x, state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data(), state_.total_cl.data() + ); if (always_check_finite_) { checkFinite(dwdw_.get(), ModelQuantity::dwdw, t); } } -void Model::fx_rdata(realtype *x_rdata, const realtype *x_solver, - const realtype * /*tcl*/, const realtype */*p*/, - const realtype */*k*/) { +void Model::fx_rdata( + realtype* x_rdata, realtype const* x_solver, realtype const* /*tcl*/, + realtype const* /*p*/, realtype const* /*k*/ +) { if (nx_solver != nx_rdata) throw AmiException( "A model that has differing nx_solver and nx_rdata needs " - "to implement its own fx_rdata"); + "to implement its own fx_rdata" + ); std::copy_n(x_solver, nx_solver, x_rdata); } -void Model::fsx_rdata(realtype *sx_rdata, const realtype *sx_solver, - const realtype *stcl, const realtype *p, - const realtype *k, const realtype *x_solver, - const realtype *tcl, - const int ip) { +void Model::fsx_rdata( + realtype* sx_rdata, realtype const* sx_solver, realtype const* stcl, + realtype const* p, realtype const* k, realtype const* x_solver, + realtype const* tcl, int const ip +) { if (nx_solver == nx_rdata) { std::copy_n(sx_solver, nx_solver, sx_rdata); return; @@ -2670,56 +2981,64 @@ void Model::fsx_rdata(realtype *sx_rdata, const realtype *sx_solver, std::fill_n(sx_rdata, nx_rdata, 0.0); fdx_rdatadp(sx_rdata, x_solver, tcl, p, k, ip); - // the following could be moved to the calling function, as it's independent // of `ip` // 2) sx_rdata(nx_rdata, 1) += // dx_rdata/dx_solver(nx_rdata,nx_solver) * sx_solver(nx_solver, 1) derived_state_.dx_rdatadx_solver.zero(); - fdx_rdatadx_solver(derived_state_.dx_rdatadx_solver.data(), - x_solver, tcl, p, k); + fdx_rdatadx_solver( + derived_state_.dx_rdatadx_solver.data(), x_solver, tcl, p, k + ); fdx_rdatadx_solver_colptrs(derived_state_.dx_rdatadx_solver); fdx_rdatadx_solver_rowvals(derived_state_.dx_rdatadx_solver); - derived_state_.dx_rdatadx_solver.multiply(gsl::make_span(sx_rdata, nx_rdata), - gsl::make_span(sx_solver, nx_solver)); + derived_state_.dx_rdatadx_solver.multiply( + gsl::make_span(sx_rdata, nx_rdata), gsl::make_span(sx_solver, nx_solver) + ); // 3) sx_rdata(nx_rdata, 1) += dx_rdata/d_tcl(nx_rdata,ntcl) * stcl derived_state_.dx_rdatadtcl.zero(); fdx_rdatadtcl(derived_state_.dx_rdatadtcl.data(), x_solver, tcl, p, k); fdx_rdatadtcl_colptrs(derived_state_.dx_rdatadtcl); fdx_rdatadtcl_rowvals(derived_state_.dx_rdatadtcl); - derived_state_.dx_rdatadtcl.multiply(gsl::make_span(sx_rdata, nx_rdata), - gsl::make_span(stcl, (nx_rdata - nx_solver))); + derived_state_.dx_rdatadtcl.multiply( + gsl::make_span(sx_rdata, nx_rdata), + gsl::make_span(stcl, (nx_rdata - nx_solver)) + ); } -void Model::fx_solver(realtype *x_solver, const realtype *x_rdata) { +void Model::fx_solver(realtype* x_solver, realtype const* x_rdata) { if (nx_solver != nx_rdata) throw AmiException( "A model that has differing nx_solver and nx_rdata needs " - "to implement its own fx_solver"); + "to implement its own fx_solver" + ); std::copy_n(x_rdata, nx_rdata, x_solver); } -void Model::fsx_solver(realtype *sx_solver, const realtype *sx_rdata) { +void Model::fsx_solver(realtype* sx_solver, realtype const* sx_rdata) { /* for the moment we do not need an implementation of fsx_solver as * we can simply reuse fx_solver and replace states by their * sensitivities */ fx_solver(sx_solver, sx_rdata); } -void Model::ftotal_cl(realtype * /*total_cl*/, const realtype * /*x_rdata*/, - const realtype */*p*/, const realtype */*k*/) { +void Model::ftotal_cl( + realtype* /*total_cl*/, realtype const* /*x_rdata*/, realtype const* /*p*/, + realtype const* /*k*/ +) { if (nx_solver != nx_rdata) throw AmiException( "A model that has differing nx_solver and nx_rdata needs " - "to implement its own ftotal_cl"); + "to implement its own ftotal_cl" + ); } -void Model::fstotal_cl(realtype *stotal_cl, const realtype *sx_rdata, - const int ip, const realtype *x_rdata, - const realtype *p, const realtype *k, - const realtype *tcl) { +void Model::fstotal_cl( + realtype* stotal_cl, realtype const* sx_rdata, int const ip, + realtype const* x_rdata, realtype const* p, realtype const* k, + realtype const* tcl +) { if (nx_solver == nx_rdata) return; @@ -2731,25 +3050,25 @@ void Model::fstotal_cl(realtype *stotal_cl, const realtype *sx_rdata, std::fill_n(stotal_cl, ncl(), 0.0); fdtotal_cldp(stotal_cl, x_rdata, p, k, ip); - // 2) stotal_cl += dtotal_cl/dx_rdata(ncl,nx_rdata) * sx_rdata(nx_rdata,1) derived_state_.dtotal_cldx_rdata.zero(); - fdtotal_cldx_rdata(derived_state_.dtotal_cldx_rdata.data(), - x_rdata, tcl, p, k); + fdtotal_cldx_rdata( + derived_state_.dtotal_cldx_rdata.data(), x_rdata, tcl, p, k + ); fdtotal_cldx_rdata_colptrs(derived_state_.dtotal_cldx_rdata); fdtotal_cldx_rdata_rowvals(derived_state_.dtotal_cldx_rdata); derived_state_.dtotal_cldx_rdata.multiply( - gsl::make_span(stotal_cl, ncl()), - gsl::make_span(sx_rdata, nx_rdata)); + gsl::make_span(stotal_cl, ncl()), gsl::make_span(sx_rdata, nx_rdata) + ); } const_N_Vector Model::computeX_pos(const_N_Vector x) { if (any_state_non_negative_) { for (int ix = 0; ix < derived_state_.x_pos_tmp_.getLength(); ++ix) { - derived_state_.x_pos_tmp_.at(ix) = - (state_is_non_negative_.at(ix) && NV_Ith_S(x, ix) < 0) - ? 0 - : NV_Ith_S(x, ix); + derived_state_.x_pos_tmp_.at(ix) + = (state_is_non_negative_.at(ix) && NV_Ith_S(x, ix) < 0) + ? 0 + : NV_Ith_S(x, ix); } return derived_state_.x_pos_tmp_.getNVector(); } @@ -2757,7 +3076,7 @@ const_N_Vector Model::computeX_pos(const_N_Vector x) { return x; } -const realtype *Model::computeX_pos(AmiVector const& x) { +realtype const* Model::computeX_pos(AmiVector const& x) { if (any_state_non_negative_) { computeX_pos(x.getNVector()); return derived_state_.x_pos_tmp_.data(); @@ -2765,9 +3084,8 @@ const realtype *Model::computeX_pos(AmiVector const& x) { return x.data(); } -void Model::setReinitializationStateIdxs(std::vector const& idxs) -{ - for(auto idx: idxs) { +void Model::setReinitializationStateIdxs(std::vector const& idxs) { + for (auto idx : idxs) { if (idx < 0 || idx >= nx_rdata) throw AmiException("Invalid state index given: %d", idx); } @@ -2775,17 +3093,16 @@ void Model::setReinitializationStateIdxs(std::vector const& idxs) simulation_parameters_.reinitialization_state_idxs_sim = idxs; } -const std::vector &Model::getReinitializationStateIdxs() const -{ +std::vector const& Model::getReinitializationStateIdxs() const { return simulation_parameters_.reinitialization_state_idxs_sim; } -const AmiVectorArray &Model::get_dxdotdp() const{ +AmiVectorArray const& Model::get_dxdotdp() const { assert(!pythonGenerated); return derived_state_.dxdotdp; } -const SUNMatrixWrapper &Model::get_dxdotdp_full() const{ +SUNMatrixWrapper const& Model::get_dxdotdp_full() const { assert(pythonGenerated); return derived_state_.dxdotdp_full; } diff --git a/src/model.template.cpp b/src/model.template.cpp index f585acd788..e43f46e818 100644 --- a/src/model.template.cpp +++ b/src/model.template.cpp @@ -1,10 +1,12 @@ -#include #include +#include namespace amici { namespace model_TPL_MODELNAME { +// clang-format off + std::array parameterNames = { TPL_PARAMETER_NAMES_INITIALIZER_LIST }; @@ -57,6 +59,8 @@ std::array rootInitialValues = { TPL_ROOT_INITIAL_VALUES }; +// clang-format on + } // namespace model_TPL_MODELNAME } // namespace amici diff --git a/src/model_dae.cpp b/src/model_dae.cpp index a2c0ec5ffb..43a8d81313 100644 --- a/src/model_dae.cpp +++ b/src/model_dae.cpp @@ -3,27 +3,33 @@ namespace amici { -void Model_DAE::fJ(const realtype t, const realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xdot, SUNMatrix J) { +void Model_DAE::fJ( + const realtype t, const realtype cj, AmiVector const& x, + AmiVector const& dx, AmiVector const& xdot, SUNMatrix J +) { fJ(t, cj, x.getNVector(), dx.getNVector(), xdot.getNVector(), J); } -void Model_DAE::fJ(realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, - const_N_Vector /*xdot*/, SUNMatrix J) { +void Model_DAE::fJ( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector /*xdot*/, SUNMatrix J +) { fJSparse(t, cj, x, dx, derived_state_.J_.get()); derived_state_.J_.refresh(); auto JDense = SUNMatrixWrapper(J); derived_state_.J_.to_dense(JDense); } -void Model_DAE::fJSparse(const realtype t, const realtype cj, - const AmiVector &x, const AmiVector &dx, - const AmiVector & /*xdot*/, SUNMatrix J) { +void Model_DAE::fJSparse( + const realtype t, const realtype cj, AmiVector const& x, + AmiVector const& dx, AmiVector const& /*xdot*/, SUNMatrix J +) { fJSparse(t, cj, x.getNVector(), dx.getNVector(), J); } -void Model_DAE::fJSparse(realtype t, realtype cj, const_N_Vector x, - const_N_Vector dx, SUNMatrix J) { +void Model_DAE::fJSparse( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, SUNMatrix J +) { auto x_pos = computeX_pos(x); fdwdx(t, N_VGetArrayPointerConst(x_pos)); if (pythonGenerated) { @@ -39,78 +45,96 @@ void Model_DAE::fJSparse(realtype t, realtype cj, const_N_Vector x, N_VGetArrayPointerConst(x_pos), state_.unscaledParameters.data(), state_.fixedParameters.data(), state_.h.data(), N_VGetArrayPointerConst(dx), - derived_state_.w_.data()); + derived_state_.w_.data() + ); } fdxdotdw(t, x_pos, dx); /* Sparse matrix multiplication dxdotdx_implicit += dxdotdw * dwdx */ - derived_state_.dxdotdw_.sparse_multiply(derived_state_.dxdotdx_implicit, - derived_state_.dwdx_); - - derived_state_.dfdx_.sparse_add(derived_state_.dxdotdx_explicit, 1.0, - derived_state_.dxdotdx_implicit, 1.0); + derived_state_.dxdotdw_.sparse_multiply( + derived_state_.dxdotdx_implicit, derived_state_.dwdx_ + ); + + derived_state_.dfdx_.sparse_add( + derived_state_.dxdotdx_explicit, 1.0, + derived_state_.dxdotdx_implicit, 1.0 + ); fM(t, x_pos); - JSparse.sparse_add(derived_state_.MSparse_, -cj, - derived_state_.dfdx_, 1.0); + JSparse.sparse_add( + derived_state_.MSparse_, -cj, derived_state_.dfdx_, 1.0 + ); } else { - fJSparse(static_cast(SM_CONTENT_S(J)), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), cj, - N_VGetArrayPointerConst(dx), - derived_state_.w_.data(), derived_state_.dwdx_.data()); + fJSparse( + static_cast(SM_CONTENT_S(J)), t, + N_VGetArrayPointerConst(x_pos), state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), cj, + N_VGetArrayPointerConst(dx), derived_state_.w_.data(), + derived_state_.dwdx_.data() + ); } } -void Model_DAE::fJv(const realtype t, const AmiVector &x, const AmiVector &dx, - const AmiVector & /*xdot*/, const AmiVector &v, - AmiVector &Jv, const realtype cj) { +void Model_DAE::fJv( + const realtype t, AmiVector const& x, AmiVector const& dx, + AmiVector const& /*xdot*/, AmiVector const& v, AmiVector& Jv, + const realtype cj +) { fJv(t, x.getNVector(), dx.getNVector(), v.getNVector(), Jv.getNVector(), cj); } -void Model_DAE::fJv(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector v, N_Vector Jv, realtype cj) { +void Model_DAE::fJv( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector v, + N_Vector Jv, realtype cj +) { N_VConst(0.0, Jv); fJSparse(t, cj, x, dx, derived_state_.J_.get()); derived_state_.J_.refresh(); derived_state_.J_.multiply(Jv, v); } -void Model_DAE::froot(const realtype t, const AmiVector &x, const AmiVector &dx, - gsl::span root) { +void Model_DAE::froot( + const realtype t, AmiVector const& x, AmiVector const& dx, + gsl::span root +) { froot(t, x.getNVector(), dx.getNVector(), root); } -void Model_DAE::froot(realtype t, const_N_Vector x, const_N_Vector dx, - gsl::span root) { +void Model_DAE::froot( + realtype t, const_N_Vector x, const_N_Vector dx, gsl::span root +) { std::fill(root.begin(), root.end(), 0.0); auto x_pos = computeX_pos(x); - froot(root.data(), t, N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), N_VGetArrayPointerConst(dx)); + froot( + root.data(), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), N_VGetArrayPointerConst(dx) + ); } -void Model_DAE::fxdot(const realtype t, const AmiVector &x, const AmiVector &dx, - AmiVector &xdot) { +void Model_DAE::fxdot( + const realtype t, AmiVector const& x, AmiVector const& dx, AmiVector& xdot +) { fxdot(t, x.getNVector(), dx.getNVector(), xdot.getNVector()); } -void Model_DAE::fxdot(realtype t, const_N_Vector x, const_N_Vector dx, - N_Vector xdot) { +void Model_DAE::fxdot( + realtype t, const_N_Vector x, const_N_Vector dx, N_Vector xdot +) { auto x_pos = computeX_pos(x); fw(t, N_VGetArrayPointerConst(x)); N_VConst(0.0, xdot); - fxdot(N_VGetArrayPointer(xdot), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), N_VGetArrayPointerConst(dx), - derived_state_.w_.data()); + fxdot( + N_VGetArrayPointer(xdot), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), N_VGetArrayPointerConst(dx), derived_state_.w_.data() + ); } -void Model_DAE::fJDiag(const realtype t, AmiVector &JDiag, - const realtype /*cj*/, const AmiVector &x, - const AmiVector &dx) { +void Model_DAE::fJDiag( + const realtype t, AmiVector& JDiag, const realtype /*cj*/, + AmiVector const& x, AmiVector const& dx +) { fJSparse(t, 0.0, x.getNVector(), dx.getNVector(), derived_state_.J_.get()); derived_state_.J_.refresh(); derived_state_.J_.to_diag(JDiag.getNVector()); @@ -118,40 +142,46 @@ void Model_DAE::fJDiag(const realtype t, AmiVector &JDiag, throw AmiException("Evaluation of fJDiag failed!"); } -void Model_DAE::fdxdotdw(const realtype t, const_N_Vector x, - const const_N_Vector dx) { +void Model_DAE::fdxdotdw( + const realtype t, const_N_Vector x, const const_N_Vector dx +) { derived_state_.dxdotdw_.zero(); if (nw > 0 && derived_state_.dxdotdw_.capacity()) { auto x_pos = computeX_pos(x); fdxdotdw_colptrs(derived_state_.dxdotdw_); fdxdotdw_rowvals(derived_state_.dxdotdw_); - fdxdotdw(derived_state_.dxdotdw_.data(), t, N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), N_VGetArrayPointerConst(dx), - derived_state_.w_.data()); + fdxdotdw( + derived_state_.dxdotdw_.data(), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), N_VGetArrayPointerConst(dx), + derived_state_.w_.data() + ); } } -void Model_DAE::fdxdotdp(const realtype t, const const_N_Vector x, - const const_N_Vector dx) { +void Model_DAE::fdxdotdp( + const realtype t, const const_N_Vector x, const const_N_Vector dx +) { auto x_pos = computeX_pos(x); if (pythonGenerated) { // python generated, not yet implemented for DAEs - throw AmiException("Wrapping of DAEs is not yet implemented from Python"); + throw AmiException("Wrapping of DAEs is not yet implemented from Python" + ); } else { // matlab generated fdwdp(t, N_VGetArrayPointerConst(x_pos)); for (int ip = 0; ip < nplist(); ip++) { N_VConst(0.0, derived_state_.dxdotdp.getNVector(ip)); - fdxdotdp(derived_state_.dxdotdp.data(ip), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip), - N_VGetArrayPointerConst(dx), derived_state_.w_.data(), - derived_state_.dwdp_.data()); + fdxdotdp( + derived_state_.dxdotdp.data(ip), t, + N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip), N_VGetArrayPointerConst(dx), + derived_state_.w_.data(), derived_state_.dwdp_.data() + ); } } } @@ -167,7 +197,7 @@ void Model_DAE::fM(realtype t, const_N_Vector x) { int ndiff = 0; for (int ix = 0; ix < nx_solver; ix++) { derived_state_.MSparse_.set_indexptr(ix, ndiff); - if (this->idlist.at(ix) == 1.0){ + if (this->idlist.at(ix) == 1.0) { derived_state_.MSparse_.set_data(ndiff, 1.0); derived_state_.MSparse_.set_indexval(ndiff, ix); ndiff++; @@ -178,8 +208,7 @@ void Model_DAE::fM(realtype t, const_N_Vector x) { } else { auto x_pos = computeX_pos(x); fM(derived_state_.M_.data(), t, N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), - state_.fixedParameters.data()); + state_.unscaledParameters.data(), state_.fixedParameters.data()); } } @@ -187,147 +216,186 @@ std::unique_ptr Model_DAE::getSolver() { return std::unique_ptr(new amici::IDASolver()); } -void Model_DAE::fJSparse(SUNMatrixContent_Sparse /*JSparse*/, realtype /*t*/, - const realtype * /*x*/, const double * /*p*/, - const double * /*k*/, const realtype * /*h*/, - realtype /*cj*/, const realtype * /*dx*/, - const realtype * /*w*/, const realtype * /*dwdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::froot(realtype * /*root*/, const realtype /*t*/, - const realtype * /*x*/, const double * /*p*/, const double * /*k*/, - const realtype * /*h*/, const realtype * /*dx*/) { - throw AmiException("Requested functionality is not supported as %s is not " - "implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdp(realtype * /*dxdotdp*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const int /*ip*/, const realtype * /*dx*/, - const realtype * /*w*/, const realtype * /*dwdp*/) { - throw AmiException("Requested functionality is not supported as %s is not " - "implemented for this model!", - __func__); -} - -void Model_DAE::fdxdotdp_explicit(realtype * /*dxdotdp_explicit*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*dx*/, const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdp_explicit_colptrs(SUNMatrixWrapper &/*dxdotdp*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdp_explicit_rowvals(SUNMatrixWrapper &/*dxdotdp*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdx_explicit(realtype * /*dxdotdx_explicit*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*dx*/, const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdx_explicit_colptrs(SUNMatrixWrapper &/*dxdotdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdx_explicit_rowvals(SUNMatrixWrapper &/*dxdotdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdw(realtype * /*dxdotdw*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*dx*/, const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdw_colptrs(SUNMatrixWrapper &/*dxdotdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fdxdotdw_rowvals(SUNMatrixWrapper &/*dxdotdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_DAE::fM(realtype */*M*/, const realtype /*t*/, const realtype */*x*/, - const realtype */*p*/, const realtype */*k*/){} - -void Model_DAE::fJB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &/*dxB*/, const AmiVector & /*xBdot*/, - SUNMatrix JB) { +void Model_DAE::fJSparse( + SUNMatrixContent_Sparse /*JSparse*/, realtype /*t*/, realtype const* /*x*/, + double const* /*p*/, double const* /*k*/, realtype const* /*h*/, + realtype /*cj*/, realtype const* /*dx*/, realtype const* /*w*/, + realtype const* /*dwdx*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::froot( + realtype* /*root*/, const realtype /*t*/, realtype const* /*x*/, + double const* /*p*/, double const* /*k*/, realtype const* /*h*/, + realtype const* /*dx*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is not " + "implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdp( + realtype* /*dxdotdp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ip*/, realtype const* /*dx*/, realtype const* /*w*/, + realtype const* /*dwdp*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is not " + "implemented for this model!", + __func__ + ); +} + +void Model_DAE::fdxdotdp_explicit( + realtype* /*dxdotdp_explicit*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*dx*/, realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdp_explicit_colptrs(SUNMatrixWrapper& /*dxdotdp*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdp_explicit_rowvals(SUNMatrixWrapper& /*dxdotdp*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdx_explicit( + realtype* /*dxdotdx_explicit*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*dx*/, realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdx_explicit_colptrs(SUNMatrixWrapper& /*dxdotdx*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdx_explicit_rowvals(SUNMatrixWrapper& /*dxdotdx*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdw( + realtype* /*dxdotdw*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*dx*/, realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdw_colptrs(SUNMatrixWrapper& /*dxdotdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE::fdxdotdw_rowvals(SUNMatrixWrapper& /*dxdotdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_DAE:: + fM(realtype* /*M*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/) {} + +void Model_DAE::fJB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& /*dxB*/, AmiVector const& /*xBdot*/, + SUNMatrix JB +) { fJB(t, cj, x.getNVector(), dx.getNVector(), xB.getNVector(), dx.getNVector(), JB); } - -void Model_DAE::fJB(realtype t, realtype cj, const_N_Vector x, - const_N_Vector dx, const_N_Vector /*xB*/, - const_N_Vector /*dxB*/, SUNMatrix JB) { +void Model_DAE::fJB( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector /*xB*/, const_N_Vector /*dxB*/, SUNMatrix JB +) { fJSparse(t, cj, x, dx, derived_state_.J_.get()); derived_state_.J_.refresh(); auto JBDense = SUNMatrixWrapper(JB); derived_state_.J_.transpose(JBDense, -1.0, nxtrue_solver); } -void Model_DAE::fJSparseB(const realtype t, realtype cj, const AmiVector &x, - const AmiVector &dx, const AmiVector &xB, - const AmiVector &dxB, const AmiVector & /*xBdot*/, - SUNMatrix JB) { - fJSparseB(t, cj, x.getNVector(), dx.getNVector(), xB.getNVector(), dxB.getNVector(), JB); +void Model_DAE::fJSparseB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& /*xBdot*/, + SUNMatrix JB +) { + fJSparseB( + t, cj, x.getNVector(), dx.getNVector(), xB.getNVector(), + dxB.getNVector(), JB + ); } -void Model_DAE::fJSparseB(realtype t, realtype cj, const_N_Vector x, - const_N_Vector dx, - const_N_Vector /*xB*/, const_N_Vector /*dxB*/, - SUNMatrix JB) { +void Model_DAE::fJSparseB( + realtype t, realtype cj, const_N_Vector x, const_N_Vector dx, + const_N_Vector /*xB*/, const_N_Vector /*dxB*/, SUNMatrix JB +) { fJSparse(t, cj, x, dx, derived_state_.J_.get()); derived_state_.J_.refresh(); auto JSparseB = SUNMatrixWrapper(JB); derived_state_.J_.transpose(JSparseB, -1.0, nxtrue_solver); } -void Model_DAE::fJvB(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector dxB, const_N_Vector vB, - N_Vector JvB, realtype cj) { +void Model_DAE::fJvB( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector dxB, const_N_Vector vB, N_Vector JvB, realtype cj +) { N_VConst(0.0, JvB); fJSparseB(t, cj, x, dx, xB, dxB, derived_state_.JB_.get()); derived_state_.JB_.refresh(); derived_state_.JB_.multiply(JvB, vB); } -void Model_DAE::fxBdot(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, - const_N_Vector dxB, N_Vector xBdot) { +void Model_DAE::fxBdot( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector dxB, N_Vector xBdot +) { N_VConst(0.0, xBdot); fJSparseB(t, 1.0, x, dx, xB, dxB, derived_state_.JB_.get()); derived_state_.JB_.refresh(); @@ -335,33 +403,39 @@ void Model_DAE::fxBdot(realtype t, const_N_Vector x, const_N_Vector dx, derived_state_.JB_.multiply(xBdot, xB); } -void Model_DAE::fqBdot(realtype t, const_N_Vector x, const_N_Vector dx, - const_N_Vector xB, const_N_Vector /*dxB*/, - N_Vector qBdot) { +void Model_DAE::fqBdot( + realtype t, const_N_Vector x, const_N_Vector dx, const_N_Vector xB, + const_N_Vector /*dxB*/, N_Vector qBdot +) { N_VConst(0.0, qBdot); fdxdotdp(t, x, dx); for (int ip = 0; ip < nplist(); ip++) { for (int ix = 0; ix < nxtrue_solver; ix++) - NV_Ith_S(qBdot, ip * nJ) -= NV_Ith_S(xB, ix) - * derived_state_.dxdotdp.at(ix, ip); + NV_Ith_S(qBdot, ip * nJ) + -= NV_Ith_S(xB, ix) * derived_state_.dxdotdp.at(ix, ip); // second order part for (int iJ = 1; iJ < nJ; iJ++) for (int ix = 0; ix < nxtrue_solver; ix++) - NV_Ith_S(qBdot, ip * nJ + iJ) -= - NV_Ith_S(xB, ix) - * derived_state_.dxdotdp.at(ix + iJ * nxtrue_solver, ip) - + NV_Ith_S(xB, ix + iJ * nxtrue_solver) - * derived_state_.dxdotdp.at(ix, ip); + NV_Ith_S(qBdot, ip * nJ + iJ) + -= NV_Ith_S(xB, ix) + * derived_state_.dxdotdp.at( + ix + iJ * nxtrue_solver, ip + ) + + NV_Ith_S(xB, ix + iJ * nxtrue_solver) + * derived_state_.dxdotdp.at(ix, ip); } } -void Model_DAE::fxBdot_ss(const realtype t, const AmiVector &xB, - const AmiVector &dxB, AmiVector &xBdot) { +void Model_DAE::fxBdot_ss( + const realtype t, AmiVector const& xB, AmiVector const& dxB, + AmiVector& xBdot +) { fxBdot_ss(t, xB.getNVector(), dxB.getNVector(), xBdot.getNVector()); } -void Model_DAE::fxBdot_ss(realtype /*t*/, const_N_Vector xB, const_N_Vector /*dxB*/, - N_Vector xBdot) const { +void Model_DAE::fxBdot_ss( + realtype /*t*/, const_N_Vector xB, const_N_Vector /*dxB*/, N_Vector xBdot +) const { /* Right hand side of the adjoint state for steady state computations. J is fixed (as x remains in steady state), so the RHS becomes simple. */ N_VConst(0.0, xBdot); @@ -370,8 +444,9 @@ void Model_DAE::fxBdot_ss(realtype /*t*/, const_N_Vector xB, const_N_Vector /*dx N_VScale(-1.0, xBdot, xBdot); } -void Model_DAE::fqBdot_ss(realtype /*t*/, const_N_Vector xB, - const_N_Vector /*dxB*/, N_Vector qBdot) const { +void Model_DAE::fqBdot_ss( + realtype /*t*/, const_N_Vector xB, const_N_Vector /*dxB*/, N_Vector qBdot +) const { /* Quadratures when computing adjoints for steady state. The integrand is just the adjoint state itself. */ N_VScale(1.0, const_cast(xB), qBdot); @@ -383,27 +458,34 @@ void Model_DAE::fJSparseB_ss(SUNMatrix JB) { derived_state_.JB_.refresh(); } -void Model_DAE::writeSteadystateJB(const realtype t, realtype cj, - const AmiVector &x, const AmiVector & dx, - const AmiVector &xB, const AmiVector & dxB, - const AmiVector &/*xBdot*/) { +void Model_DAE::writeSteadystateJB( + const realtype t, realtype cj, AmiVector const& x, AmiVector const& dx, + AmiVector const& xB, AmiVector const& dxB, AmiVector const& /*xBdot*/ +) { /* Get backward Jacobian */ - fJSparseB(t, cj, x.getNVector(), dx.getNVector(), xB.getNVector(), - dxB.getNVector(), derived_state_.JB_.get()); + fJSparseB( + t, cj, x.getNVector(), dx.getNVector(), xB.getNVector(), + dxB.getNVector(), derived_state_.JB_.get() + ); derived_state_.JB_.refresh(); /* Switch sign, as we integrate forward in time, not backward */ derived_state_.JB_.scale(-1); } -void Model_DAE::fsxdot(const realtype t, const AmiVector &x, - const AmiVector &dx, const int ip, const AmiVector &sx, - const AmiVector &sdx, AmiVector &sxdot) { - fsxdot(t, x.getNVector(), dx.getNVector(), ip, sx.getNVector(), - sdx.getNVector(), sxdot.getNVector()); +void Model_DAE::fsxdot( + const realtype t, AmiVector const& x, AmiVector const& dx, int const ip, + AmiVector const& sx, AmiVector const& sdx, AmiVector& sxdot +) { + fsxdot( + t, x.getNVector(), dx.getNVector(), ip, sx.getNVector(), + sdx.getNVector(), sxdot.getNVector() + ); } -void Model_DAE::fsxdot(realtype t, const_N_Vector x, const_N_Vector dx, int ip, - const_N_Vector sx, const_N_Vector sdx, N_Vector sxdot) { +void Model_DAE::fsxdot( + realtype t, const_N_Vector x, const_N_Vector dx, int ip, const_N_Vector sx, + const_N_Vector sdx, N_Vector sxdot +) { if (ip == 0) { // we only need to call this for the first parameter index will be // the same for all remaining @@ -415,7 +497,8 @@ void Model_DAE::fsxdot(realtype t, const_N_Vector x, const_N_Vector dx, int ip, if (pythonGenerated) { // python generated, not yet implemented for DAEs - throw AmiException("Wrapping of DAEs is not yet implemented from Python"); + throw AmiException("Wrapping of DAEs is not yet implemented from Python" + ); } else { /* copy dxdotdp over */ N_VScale(1.0, derived_state_.dxdotdp.getNVector(ip), sxdot); diff --git a/src/model_header.template.h b/src/model_header.template.h index 97d4fcd57b..af05c8ccc5 100644 --- a/src/model_header.template.h +++ b/src/model_header.template.h @@ -5,6 +5,7 @@ #include #include "amici/model_TPL_MODEL_TYPE_LOWER.h" +#include "amici/splinefunctions.h" namespace amici { @@ -93,6 +94,10 @@ TPL_DTOTAL_CLDP_DEF TPL_DTOTAL_CLDX_RDATA_DEF TPL_DTOTAL_CLDX_RDATA_COLPTRS_DEF TPL_DTOTAL_CLDX_RDATA_ROWVALS_DEF +TPL_CREATE_SPLINES_DEF +TPL_DSPLINE_VALUESDP_DEF +TPL_DSPLINE_SLOPESDP_DEF + /** * @brief AMICI-generated model subclass. */ @@ -116,6 +121,7 @@ class Model_TPL_MODELNAME : public amici::Model_TPL_MODEL_TYPE_UPPER { TPL_NZ, // nz TPL_NZTRUE, // nztrue TPL_NEVENT, // nevent + TPL_NSPL, // nspl TPL_NOBJECTIVE, // nobjective TPL_NW, // nw TPL_NDWDX, // ndwdx @@ -229,6 +235,10 @@ class Model_TPL_MODELNAME : public amici::Model_TPL_MODEL_TYPE_UPPER { TPL_DJYDY_COLPTRS_IMPL TPL_DJYDY_ROWVALS_IMPL + TPL_CREATE_SPLINES_IMPL + TPL_DSPLINE_VALUESDP_IMPL + TPL_DSPLINE_SLOPESDP_IMPL + TPL_DWDP_IMPL TPL_DWDP_COLPTRS_IMPL TPL_DWDP_ROWVALS_IMPL diff --git a/src/model_ode.cpp b/src/model_ode.cpp index 65552b8579..24787df8ad 100644 --- a/src/model_ode.cpp +++ b/src/model_ode.cpp @@ -1,16 +1,19 @@ -#include #include "amici/model_ode.h" #include "amici/solver_cvodes.h" +#include namespace amici { -void Model_ODE::fJ(const realtype t, const realtype /*cj*/, const AmiVector &x, - const AmiVector & /*dx*/, const AmiVector &xdot, - SUNMatrix J) { +void Model_ODE::fJ( + const realtype t, const realtype /*cj*/, AmiVector const& x, + AmiVector const& /*dx*/, AmiVector const& xdot, SUNMatrix J +) { fJ(t, x.getNVector(), xdot.getNVector(), J); } -void Model_ODE::fJ(realtype t, const_N_Vector x, const_N_Vector /*xdot*/, SUNMatrix J) { +void Model_ODE::fJ( + realtype t, const_N_Vector x, const_N_Vector /*xdot*/, SUNMatrix J +) { auto x_pos = computeX_pos(x); fdwdx(t, N_VGetArrayPointerConst(x_pos)); fJSparse(t, x, derived_state_.J_.get()); @@ -19,9 +22,10 @@ void Model_ODE::fJ(realtype t, const_N_Vector x, const_N_Vector /*xdot*/, SUNMat derived_state_.J_.to_dense(JDense); } -void Model_ODE::fJSparse(const realtype t, const realtype /*cj*/, - const AmiVector &x, const AmiVector & /*dx*/, - const AmiVector & /*xdot*/, SUNMatrix J) { +void Model_ODE::fJSparse( + const realtype t, const realtype /*cj*/, AmiVector const& x, + AmiVector const& /*dx*/, AmiVector const& /*xdot*/, SUNMatrix J +) { fJSparse(t, x.getNVector(), J); } @@ -40,54 +44,67 @@ void Model_ODE::fJSparse(realtype t, const_N_Vector x, SUNMatrix J) { derived_state_.dxdotdx_explicit.data(), t, N_VGetArrayPointerConst(x_pos), state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data()); + state_.h.data(), derived_state_.w_.data() + ); } fdxdotdw(t, x_pos); /* Sparse matrix multiplication dxdotdx_implicit += dxdotdw * dwdx */ - derived_state_.dxdotdw_.sparse_multiply(derived_state_.dxdotdx_implicit, - derived_state_.dwdx_); - - JSparse.sparse_add(derived_state_.dxdotdx_explicit, 1.0, - derived_state_.dxdotdx_implicit, 1.0); + derived_state_.dxdotdw_.sparse_multiply( + derived_state_.dxdotdx_implicit, derived_state_.dwdx_ + ); + + JSparse.sparse_add( + derived_state_.dxdotdx_explicit, 1.0, + derived_state_.dxdotdx_implicit, 1.0 + ); } else { - fJSparse(static_cast(SM_CONTENT_S(J)), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), - derived_state_.w_.data(), - derived_state_.dwdx_.data()); + fJSparse( + static_cast(SM_CONTENT_S(J)), t, + N_VGetArrayPointerConst(x_pos), state_.unscaledParameters.data(), + state_.fixedParameters.data(), state_.h.data(), + derived_state_.w_.data(), derived_state_.dwdx_.data() + ); } } -void Model_ODE::fJv(const realtype t, const AmiVector &x, - const AmiVector & /*dx*/, const AmiVector & /*xdot*/, - const AmiVector &v, AmiVector &Jv, const realtype /*cj*/) { +void Model_ODE:: + fJv(const realtype t, AmiVector const& x, AmiVector const& /*dx*/, + AmiVector const& /*xdot*/, AmiVector const& v, AmiVector& Jv, + const realtype /*cj*/) { fJv(v.getNVector(), Jv.getNVector(), t, x.getNVector()); } -void Model_ODE::fJv(const_N_Vector v, N_Vector Jv, realtype t, const_N_Vector x) { +void Model_ODE::fJv( + const_N_Vector v, N_Vector Jv, realtype t, const_N_Vector x +) { N_VConst(0.0, Jv); fJSparse(t, x, derived_state_.J_.get()); derived_state_.J_.refresh(); derived_state_.J_.multiply(Jv, v); } -void Model_ODE::froot(const realtype t, const AmiVector &x, - const AmiVector & /*dx*/, gsl::span root) { +void Model_ODE::froot( + const realtype t, AmiVector const& x, AmiVector const& /*dx*/, + gsl::span root +) { froot(t, x.getNVector(), root); } void Model_ODE::froot(realtype t, const_N_Vector x, gsl::span root) { auto x_pos = computeX_pos(x); std::fill(root.begin(), root.end(), 0.0); - froot(root.data(), t, N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), state_.total_cl.data()); + froot( + root.data(), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), state_.total_cl.data() + ); } -void Model_ODE::fxdot(const realtype t, const AmiVector &x, - const AmiVector & /*dx*/, AmiVector &xdot) { +void Model_ODE::fxdot( + const realtype t, AmiVector const& x, AmiVector const& /*dx*/, + AmiVector& xdot +) { fxdot(t, x.getNVector(), xdot.getNVector()); } @@ -95,15 +112,17 @@ void Model_ODE::fxdot(realtype t, const_N_Vector x, N_Vector xdot) { auto x_pos = computeX_pos(x); fw(t, N_VGetArrayPointerConst(x_pos)); N_VConst(0.0, xdot); - fxdot(N_VGetArrayPointer(xdot), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data()); + fxdot( + N_VGetArrayPointer(xdot), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), derived_state_.w_.data() + ); } -void Model_ODE::fJDiag(const realtype t, AmiVector &JDiag, - const realtype /*cj*/, const AmiVector &x, - const AmiVector & /*dx*/) { +void Model_ODE::fJDiag( + const realtype t, AmiVector& JDiag, const realtype /*cj*/, + AmiVector const& x, AmiVector const& /*dx*/ +) { fJDiag(t, JDiag.getNVector(), x.getNVector()); if (checkFinite(JDiag.getVector(), ModelQuantity::JDiag) != AMICI_SUCCESS) throw AmiException("Evaluation of fJDiag failed!"); @@ -116,9 +135,11 @@ void Model_ODE::fdxdotdw(const realtype t, const_N_Vector x) { fdxdotdw_colptrs(derived_state_.dxdotdw_); fdxdotdw_rowvals(derived_state_.dxdotdw_); - fdxdotdw(derived_state_.dxdotdw_.data(), t, N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data()); + fdxdotdw( + derived_state_.dxdotdw_.data(), t, N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), derived_state_.w_.data() + ); } } @@ -137,33 +158,38 @@ void Model_ODE::fdxdotdp(const realtype t, const_N_Vector x) { derived_state_.dxdotdp_explicit.data(), t, N_VGetArrayPointerConst(x_pos), state_.unscaledParameters.data(), state_.fixedParameters.data(), - state_.h.data(), derived_state_.w_.data()); + state_.h.data(), derived_state_.w_.data() + ); } fdxdotdw(t, x_pos); /* Sparse matrix multiplication dxdotdp_implicit += dxdotdw * dwdp */ - derived_state_.dxdotdw_.sparse_multiply(derived_state_.dxdotdp_implicit, - derived_state_.dwdp_); + derived_state_.dxdotdw_.sparse_multiply( + derived_state_.dxdotdp_implicit, derived_state_.dwdp_ + ); derived_state_.dxdotdp_full.sparse_add( - derived_state_.dxdotdp_explicit, 1.0, - derived_state_.dxdotdp_implicit, 1.0); + derived_state_.dxdotdp_explicit, 1.0, + derived_state_.dxdotdp_implicit, 1.0 + ); } else { // matlab generated for (int ip = 0; ip < nplist(); ip++) { N_VConst(0.0, derived_state_.dxdotdp.getNVector(ip)); - fdxdotdp(derived_state_.dxdotdp.data(ip), t, - N_VGetArrayPointerConst(x_pos), - state_.unscaledParameters.data(), - state_.fixedParameters.data(), state_.h.data(), plist(ip), - derived_state_.w_.data(), derived_state_.dwdp_.data()); + fdxdotdp( + derived_state_.dxdotdp.data(ip), t, + N_VGetArrayPointerConst(x_pos), + state_.unscaledParameters.data(), state_.fixedParameters.data(), + state_.h.data(), plist(ip), derived_state_.w_.data(), + derived_state_.dwdp_.data() + ); } } } -void Model_ODE::fdxdotdp(const realtype t, const AmiVector &x, - const AmiVector & /*dx*/) { +void Model_ODE:: + fdxdotdp(const realtype t, AmiVector const& x, AmiVector const& /*dx*/) { fdxdotdp(t, x.getNVector()); } @@ -171,143 +197,184 @@ std::unique_ptr Model_ODE::getSolver() { return std::unique_ptr(new amici::CVodeSolver()); } -void Model_ODE::fJSparse(SUNMatrixContent_Sparse /*JSparse*/, - const realtype /*t*/, const realtype * /*x*/, - const realtype * /*p*/, const realtype * /*k*/, - const realtype * /*h*/, const realtype * /*w*/, - const realtype * /*dwdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fJSparse(realtype * /*JSparse*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*w*/, const realtype * /*dwdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fJSparse_colptrs(SUNMatrixWrapper &/*JSparse*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fJSparse_rowvals(SUNMatrixWrapper &/*JSparse*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::froot(realtype * /*root*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype */*tcl*/) { - throw AmiException("Requested functionality is not supported as %s is not " - "implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdp(realtype * /*dxdotdp*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const int /*ip*/, const realtype * /*w*/, - const realtype * /*dwdp*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdp_explicit(realtype * /*dxdotdp_explicit*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdp_explicit_colptrs(SUNMatrixWrapper &/*dxdotdp*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdp_explicit_rowvals(SUNMatrixWrapper &/*dxdotdp*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdx_explicit(realtype * /*dxdotdx_explicit*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdx_explicit_colptrs(SUNMatrixWrapper &/*dxdotdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdx_explicit_rowvals(SUNMatrixWrapper &/*dxdotdx*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdw(realtype * /*dxdotdw*/, const realtype /*t*/, - const realtype * /*x*/, const realtype * /*p*/, - const realtype * /*k*/, const realtype * /*h*/, - const realtype * /*w*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdw_colptrs(SUNMatrixWrapper &/*dxdotdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fdxdotdw_rowvals(SUNMatrixWrapper &/*dxdotdw*/) { - throw AmiException("Requested functionality is not supported as %s " - "is not implemented for this model!", - __func__); // not implemented -} - -void Model_ODE::fJB(const realtype t, realtype /*cj*/, const AmiVector &x, - const AmiVector & /*dx*/, const AmiVector &xB, - const AmiVector & /*dxB*/, const AmiVector &xBdot, - SUNMatrix JB) { +void Model_ODE::fJSparse( + SUNMatrixContent_Sparse /*JSparse*/, const realtype /*t*/, + realtype const* /*x*/, realtype const* /*p*/, realtype const* /*k*/, + realtype const* /*h*/, realtype const* /*w*/, realtype const* /*dwdx*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fJSparse( + realtype* /*JSparse*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/, realtype const* /*dwdx*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fJSparse_colptrs(SUNMatrixWrapper& /*JSparse*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fJSparse_rowvals(SUNMatrixWrapper& /*JSparse*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::froot( + realtype* /*root*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*tcl*/ +) { + throw AmiException( + "Requested functionality is not supported as %s is not " + "implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdp( + realtype* /*dxdotdp*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + int const /*ip*/, realtype const* /*w*/, realtype const* /*dwdp*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdp_explicit( + realtype* /*dxdotdp_explicit*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdp_explicit_colptrs(SUNMatrixWrapper& /*dxdotdp*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdp_explicit_rowvals(SUNMatrixWrapper& /*dxdotdp*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdx_explicit( + realtype* /*dxdotdx_explicit*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdx_explicit_colptrs(SUNMatrixWrapper& /*dxdotdx*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdx_explicit_rowvals(SUNMatrixWrapper& /*dxdotdx*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdw( + realtype* /*dxdotdw*/, const realtype /*t*/, realtype const* /*x*/, + realtype const* /*p*/, realtype const* /*k*/, realtype const* /*h*/, + realtype const* /*w*/ +) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdw_colptrs(SUNMatrixWrapper& /*dxdotdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fdxdotdw_rowvals(SUNMatrixWrapper& /*dxdotdw*/) { + throw AmiException( + "Requested functionality is not supported as %s " + "is not implemented for this model!", + __func__ + ); // not implemented +} + +void Model_ODE::fJB( + const realtype t, realtype /*cj*/, AmiVector const& x, + AmiVector const& /*dx*/, AmiVector const& xB, AmiVector const& /*dxB*/, + AmiVector const& xBdot, SUNMatrix JB +) { fJB(t, x.getNVector(), xB.getNVector(), xBdot.getNVector(), JB); } -void Model_ODE::fJB(realtype t, const_N_Vector x, const_N_Vector /*xB*/, - const_N_Vector /*xBdot*/, SUNMatrix JB) { +void Model_ODE::fJB( + realtype t, const_N_Vector x, const_N_Vector /*xB*/, + const_N_Vector /*xBdot*/, SUNMatrix JB +) { fJSparse(t, x, derived_state_.J_.get()); derived_state_.J_.refresh(); auto JDenseB = SUNMatrixWrapper(JB); derived_state_.J_.transpose(JDenseB, -1.0, nxtrue_solver); } -void Model_ODE::fJSparseB(const realtype t, realtype /*cj*/, const AmiVector &x, - const AmiVector & /*dx*/, const AmiVector &xB, - const AmiVector & /*dxB*/, const AmiVector &xBdot, - SUNMatrix JB) { +void Model_ODE::fJSparseB( + const realtype t, realtype /*cj*/, AmiVector const& x, + AmiVector const& /*dx*/, AmiVector const& xB, AmiVector const& /*dxB*/, + AmiVector const& xBdot, SUNMatrix JB +) { fJSparseB(t, x.getNVector(), xB.getNVector(), xBdot.getNVector(), JB); } -void Model_ODE::fJSparseB(realtype t, const_N_Vector x, const_N_Vector /*xB*/, - const_N_Vector /*xBdot*/, SUNMatrix JB) { +void Model_ODE::fJSparseB( + realtype t, const_N_Vector x, const_N_Vector /*xB*/, + const_N_Vector /*xBdot*/, SUNMatrix JB +) { fJSparse(t, x, derived_state_.J_.get()); derived_state_.J_.refresh(); auto JSparseB = SUNMatrixWrapper(JB); @@ -320,8 +387,10 @@ void Model_ODE::fJDiag(realtype t, N_Vector JDiag, const_N_Vector x) { derived_state_.J_.to_diag(JDiag); } -void Model_ODE::fJvB(const_N_Vector vB, N_Vector JvB, realtype t, const_N_Vector x, - const_N_Vector xB) { +void Model_ODE::fJvB( + const_N_Vector vB, N_Vector JvB, realtype t, const_N_Vector x, + const_N_Vector xB +) { N_VConst(0.0, JvB); fJSparseB(t, x, xB, nullptr, derived_state_.JB_.get()); derived_state_.JB_.refresh(); @@ -335,8 +404,9 @@ void Model_ODE::fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot) { derived_state_.JB_.multiply(xBdot, xB); } -void Model_ODE::fqBdot(realtype t, const_N_Vector x, const_N_Vector xB, - N_Vector qBdot) { +void Model_ODE::fqBdot( + realtype t, const_N_Vector x, const_N_Vector xB, N_Vector qBdot +) { /* initialize with zeros */ N_VConst(0.0, qBdot); fdxdotdp(t, x); @@ -349,26 +419,31 @@ void Model_ODE::fqBdot(realtype t, const_N_Vector x, const_N_Vector xB, /* was matlab generated */ for (int ip = 0; ip < nplist(); ip++) { for (int ix = 0; ix < nxtrue_solver; ix++) - NV_Ith_S(qBdot, ip * nJ) -= NV_Ith_S(xB, ix) - * derived_state_.dxdotdp.at(ix, ip); + NV_Ith_S(qBdot, ip * nJ) + -= NV_Ith_S(xB, ix) * derived_state_.dxdotdp.at(ix, ip); // second order part for (int iJ = 1; iJ < nJ; iJ++) for (int ix = 0; ix < nxtrue_solver; ix++) - NV_Ith_S(qBdot, ip * nJ + iJ) -= - NV_Ith_S(xB, ix) - * derived_state_.dxdotdp.at(ix + iJ * nxtrue_solver, ip) - + NV_Ith_S(xB, ix + iJ * nxtrue_solver) - * derived_state_.dxdotdp.at(ix, ip); + NV_Ith_S(qBdot, ip * nJ + iJ) + -= NV_Ith_S(xB, ix) + * derived_state_.dxdotdp.at( + ix + iJ * nxtrue_solver, ip + ) + + NV_Ith_S(xB, ix + iJ * nxtrue_solver) + * derived_state_.dxdotdp.at(ix, ip); } } } -void Model_ODE::fxBdot_ss(const realtype t, const AmiVector &xB, - const AmiVector & /*dx*/, AmiVector &xBdot) { +void Model_ODE::fxBdot_ss( + const realtype t, AmiVector const& xB, AmiVector const& /*dx*/, + AmiVector& xBdot +) { fxBdot_ss(t, xB.getNVector(), xBdot.getNVector()); } -void Model_ODE::fxBdot_ss(realtype /*t*/, const_N_Vector xB, N_Vector xBdot) const { +void Model_ODE::fxBdot_ss(realtype /*t*/, const_N_Vector xB, N_Vector xBdot) + const { /* Right hand side of the adjoint state for steady state computations. J is fixed (as x remains in steady state), so the RHS becomes simple. */ N_VConst(0.0, xBdot); @@ -387,27 +462,31 @@ void Model_ODE::fJSparseB_ss(SUNMatrix JB) { derived_state_.JB_.refresh(); } -void Model_ODE::writeSteadystateJB(const realtype t, realtype /*cj*/, - const AmiVector &x, const AmiVector & /*dx*/, - const AmiVector &xB, const AmiVector & /*dxB*/, - const AmiVector &xBdot) { +void Model_ODE::writeSteadystateJB( + const realtype t, realtype /*cj*/, AmiVector const& x, + AmiVector const& /*dx*/, AmiVector const& xB, AmiVector const& /*dxB*/, + AmiVector const& xBdot +) { /* Get backward Jacobian */ - fJSparseB(t, x.getNVector(), xB.getNVector(), xBdot.getNVector(), - derived_state_.JB_.get()); + fJSparseB( + t, x.getNVector(), xB.getNVector(), xBdot.getNVector(), + derived_state_.JB_.get() + ); derived_state_.JB_.refresh(); /* Switch sign, as we integrate forward in time, not backward */ derived_state_.JB_.scale(-1); } -void Model_ODE::fsxdot(const realtype t, const AmiVector &x, - const AmiVector & /*dx*/, const int ip, - const AmiVector &sx, const AmiVector & /*sdx*/, - AmiVector &sxdot) { +void Model_ODE::fsxdot( + const realtype t, AmiVector const& x, AmiVector const& /*dx*/, int const ip, + AmiVector const& sx, AmiVector const& /*sdx*/, AmiVector& sxdot +) { fsxdot(t, x.getNVector(), ip, sx.getNVector(), sxdot.getNVector()); } -void Model_ODE::fsxdot(realtype t, const_N_Vector x, int ip, const_N_Vector sx, - N_Vector sxdot) { +void Model_ODE::fsxdot( + realtype t, const_N_Vector x, int ip, const_N_Vector sx, N_Vector sxdot +) { /* sxdot is just the total derivative d(xdot)dp, so we just call dxdotdp and copy the stuff over */ @@ -422,11 +501,12 @@ void Model_ODE::fsxdot(realtype t, const_N_Vector x, int ip, const_N_Vector sx, /* copy dxdotdp and the implicit version over */ // initialize N_VConst(0.0, sxdot); - realtype *sxdot_tmp = N_VGetArrayPointer(sxdot); + realtype* sxdot_tmp = N_VGetArrayPointer(sxdot); - derived_state_.dxdotdp_full.scatter(plist(ip), 1.0, nullptr, - gsl::make_span(sxdot_tmp, nx_solver), - 0, nullptr, 0); + derived_state_.dxdotdp_full.scatter( + plist(ip), 1.0, nullptr, gsl::make_span(sxdot_tmp, nx_solver), 0, + nullptr, 0 + ); } else { /* copy dxdotdp over */ diff --git a/src/model_state.cpp b/src/model_state.cpp index 6bad8db959..b6d1d9c850 100644 --- a/src/model_state.cpp +++ b/src/model_state.cpp @@ -2,21 +2,23 @@ namespace amici { -ModelStateDerived::ModelStateDerived(const ModelDimensions &dim) - : J_(dim.nx_solver, dim.nx_solver, dim.nnz, CSC_MAT), - JB_(dim.nx_solver, dim.nx_solver, dim.nnz, CSC_MAT), - dxdotdw_(dim.nx_solver, dim.nw, dim.ndxdotdw, CSC_MAT), - dx_rdatadx_solver(dim.nx_rdata, dim.nx_solver, dim.ndxrdatadxsolver, - CSC_MAT), - dx_rdatadtcl(dim.nx_rdata, dim.nx_rdata - dim.nx_solver, dim.ndxrdatadtcl, - CSC_MAT), - dtotal_cldx_rdata(dim.nx_rdata - dim.nx_solver, dim.nx_rdata, - dim.ndtotal_cldx_rdata, CSC_MAT), - w_(dim.nw), - x_rdata_(dim.nx_rdata, 0.0), - sx_rdata_(dim.nx_rdata, 0.0), - x_pos_tmp_(dim.nx_solver) -{} - +ModelStateDerived::ModelStateDerived(ModelDimensions const& dim) + : J_(dim.nx_solver, dim.nx_solver, dim.nnz, CSC_MAT) + , JB_(dim.nx_solver, dim.nx_solver, dim.nnz, CSC_MAT) + , dxdotdw_(dim.nx_solver, dim.nw, dim.ndxdotdw, CSC_MAT) + , dx_rdatadx_solver( + dim.nx_rdata, dim.nx_solver, dim.ndxrdatadxsolver, CSC_MAT + ) + , dx_rdatadtcl( + dim.nx_rdata, dim.nx_rdata - dim.nx_solver, dim.ndxrdatadtcl, CSC_MAT + ) + , dtotal_cldx_rdata( + dim.nx_rdata - dim.nx_solver, dim.nx_rdata, dim.ndtotal_cldx_rdata, + CSC_MAT + ) + , w_(dim.nw) + , x_rdata_(dim.nx_rdata, 0.0) + , sx_rdata_(dim.nx_rdata, 0.0) + , x_pos_tmp_(dim.nx_solver) {} } // namespace amici diff --git a/src/newton_solver.cpp b/src/newton_solver.cpp index 1d903b7456..b8cbe8f34d 100644 --- a/src/newton_solver.cpp +++ b/src/newton_solver.cpp @@ -4,9 +4,9 @@ #include #include +#include // roundoffs #include // dense solver #include // sparse solver -#include // roundoffs #include #include @@ -14,12 +14,14 @@ namespace amici { -NewtonSolver::NewtonSolver(const Model &model) - : xdot_(model.nx_solver), x_(model.nx_solver), - xB_(model.nJ * model.nx_solver), dxB_(model.nJ * model.nx_solver) {} +NewtonSolver::NewtonSolver(Model const& model) + : xdot_(model.nx_solver) + , x_(model.nx_solver) + , xB_(model.nJ * model.nx_solver) + , dxB_(model.nJ * model.nx_solver) {} std::unique_ptr -NewtonSolver::getSolver(const Solver &simulationSolver, const Model &model) { +NewtonSolver::getSolver(Solver const& simulationSolver, Model const& model) { std::unique_ptr solver; @@ -64,34 +66,36 @@ NewtonSolver::getSolver(const Solver &simulationSolver, const Model &model) { return solver; } -void NewtonSolver::getStep(AmiVector &delta, Model &model, - const SimulationState &state) { +void NewtonSolver::getStep( + AmiVector& delta, Model& model, SimulationState const& state +) { prepareLinearSystem(model, state); delta.minus(); solveLinearSystem(delta); } -void NewtonSolver::computeNewtonSensis(AmiVectorArray &sx, Model &model, - const SimulationState &state) { +void NewtonSolver::computeNewtonSensis( + AmiVectorArray& sx, Model& model, SimulationState const& state +) { prepareLinearSystem(model, state); model.fdxdotdp(state.t, state.x, state.dx); if (model.logger && is_singular(model, state)) { model.logger->log( - LogSeverity::warning, - "NEWTON_JAC_SINGULAR", + LogSeverity::warning, "NEWTON_JAC_SINGULAR", "Jacobian is singular at steadystate, " "sensitivities may be inaccurate." - ); + ); } if (model.pythonGenerated) { for (int ip = 0; ip < model.nplist(); ip++) { N_VConst(0.0, sx.getNVector(ip)); - model.get_dxdotdp_full().scatter(model.plist(ip), -1.0, nullptr, - gsl::make_span(sx.getNVector(ip)), - 0, nullptr, 0); + model.get_dxdotdp_full().scatter( + model.plist(ip), -1.0, nullptr, + gsl::make_span(sx.getNVector(ip)), 0, nullptr, 0 + ); solveLinearSystem(sx[ip]); } @@ -105,16 +109,18 @@ void NewtonSolver::computeNewtonSensis(AmiVectorArray &sx, Model &model, } } -NewtonSolverDense::NewtonSolverDense(const Model &model) - : NewtonSolver(model), Jtmp_(model.nx_solver, model.nx_solver), - linsol_(SUNLinSol_Dense(x_.getNVector(), Jtmp_.get())) { +NewtonSolverDense::NewtonSolverDense(Model const& model) + : NewtonSolver(model) + , Jtmp_(model.nx_solver, model.nx_solver) + , linsol_(SUNLinSol_Dense(x_.getNVector(), Jtmp_.get())) { auto status = SUNLinSolInitialize_Dense(linsol_); if (status != SUNLS_SUCCESS) throw NewtonFailure(status, "SUNLinSolInitialize_Dense"); } -void NewtonSolverDense::prepareLinearSystem(Model &model, - const SimulationState &state) { +void NewtonSolverDense::prepareLinearSystem( + Model& model, SimulationState const& state +) { model.fJ(state.t, 0.0, state.x, state.dx, xdot_, Jtmp_.get()); Jtmp_.refresh(); auto status = SUNLinSolSetup_Dense(linsol_, Jtmp_.get()); @@ -122,8 +128,9 @@ void NewtonSolverDense::prepareLinearSystem(Model &model, throw NewtonFailure(status, "SUNLinSolSetup_Dense"); } -void NewtonSolverDense::prepareLinearSystemB(Model &model, - const SimulationState &state) { +void NewtonSolverDense::prepareLinearSystemB( + Model& model, SimulationState const& state +) { model.fJB(state.t, 0.0, state.x, state.dx, xB_, dxB_, xdot_, Jtmp_.get()); Jtmp_.refresh(); auto status = SUNLinSolSetup_Dense(linsol_, Jtmp_.get()); @@ -131,9 +138,10 @@ void NewtonSolverDense::prepareLinearSystemB(Model &model, throw NewtonFailure(status, "SUNLinSolSetup_Dense"); } -void NewtonSolverDense::solveLinearSystem(AmiVector &rhs) { - auto status = SUNLinSolSolve_Dense(linsol_, Jtmp_.get(), rhs.getNVector(), - rhs.getNVector(), 0.0); +void NewtonSolverDense::solveLinearSystem(AmiVector& rhs) { + auto status = SUNLinSolSolve_Dense( + linsol_, Jtmp_.get(), rhs.getNVector(), rhs.getNVector(), 0.0 + ); Jtmp_.refresh(); // last argument is tolerance and does not have any influence on result @@ -145,8 +153,8 @@ void NewtonSolverDense::reinitialize(){ /* dense solver does not need reinitialization */ }; -bool NewtonSolverDense::is_singular(Model &model, - const SimulationState &state) const { +bool NewtonSolverDense::is_singular(Model& model, SimulationState const& state) + const { // dense solver doesn't have any implementation for rcond/condest, so use // sparse solver interface, not the most efficient solution, but who is // concerned about speed and used the dense solver anyways ¯\_(ツ)_/¯ @@ -160,17 +168,18 @@ NewtonSolverDense::~NewtonSolverDense() { SUNLinSolFree_Dense(linsol_); } -NewtonSolverSparse::NewtonSolverSparse(const Model &model) - : NewtonSolver(model), - Jtmp_(model.nx_solver, model.nx_solver, model.nnz, CSC_MAT), - linsol_(SUNKLU(x_.getNVector(), Jtmp_.get())) { +NewtonSolverSparse::NewtonSolverSparse(Model const& model) + : NewtonSolver(model) + , Jtmp_(model.nx_solver, model.nx_solver, model.nnz, CSC_MAT) + , linsol_(SUNKLU(x_.getNVector(), Jtmp_.get())) { auto status = SUNLinSolInitialize_KLU(linsol_); if (status != SUNLS_SUCCESS) throw NewtonFailure(status, "SUNLinSolInitialize_KLU"); } -void NewtonSolverSparse::prepareLinearSystem(Model &model, - const SimulationState &state) { +void NewtonSolverSparse::prepareLinearSystem( + Model& model, SimulationState const& state +) { /* Get sparse Jacobian */ model.fJSparse(state.t, 0.0, state.x, state.dx, xdot_, Jtmp_.get()); Jtmp_.refresh(); @@ -179,21 +188,24 @@ void NewtonSolverSparse::prepareLinearSystem(Model &model, throw NewtonFailure(status, "SUNLinSolSetup_KLU"); } -void NewtonSolverSparse::prepareLinearSystemB(Model &model, - const SimulationState &state) { +void NewtonSolverSparse::prepareLinearSystemB( + Model& model, SimulationState const& state +) { /* Get sparse Jacobian */ - model.fJSparseB(state.t, 0.0, state.x, state.dx, xB_, dxB_, xdot_, - Jtmp_.get()); + model.fJSparseB( + state.t, 0.0, state.x, state.dx, xB_, dxB_, xdot_, Jtmp_.get() + ); Jtmp_.refresh(); auto status = SUNLinSolSetup_KLU(linsol_, Jtmp_.get()); if (status != SUNLS_SUCCESS) throw NewtonFailure(status, "SUNLinSolSetup_KLU"); } -void NewtonSolverSparse::solveLinearSystem(AmiVector &rhs) { +void NewtonSolverSparse::solveLinearSystem(AmiVector& rhs) { /* Pass pointer to the linear solver */ - auto status = SUNLinSolSolve_KLU(linsol_, Jtmp_.get(), rhs.getNVector(), - rhs.getNVector(), 0.0); + auto status = SUNLinSolSolve_KLU( + linsol_, Jtmp_.get(), rhs.getNVector(), rhs.getNVector(), 0.0 + ); // last argument is tolerance and does not have any influence on result if (status != SUNLS_SUCCESS) @@ -202,19 +214,20 @@ void NewtonSolverSparse::solveLinearSystem(AmiVector &rhs) { void NewtonSolverSparse::reinitialize() { /* partial reinitialization, don't need to reallocate Jtmp_ */ - auto status = SUNLinSol_KLUReInit(linsol_, Jtmp_.get(), Jtmp_.capacity(), - SUNKLU_REINIT_PARTIAL); + auto status = SUNLinSol_KLUReInit( + linsol_, Jtmp_.get(), Jtmp_.capacity(), SUNKLU_REINIT_PARTIAL + ); if (status != SUNLS_SUCCESS) throw NewtonFailure(status, "SUNLinSol_KLUReInit"); } -bool NewtonSolverSparse::is_singular(Model& /*model*/, - const SimulationState& /*state*/) const { +bool NewtonSolverSparse:: + is_singular(Model& /*model*/, SimulationState const& /*state*/) const { // adapted from SUNLinSolSetup_KLU in sunlinsol/klu/sunlinsol_klu.c auto content = (SUNLinearSolverContent_KLU)(linsol_->content); // first cheap check via rcond - auto status = - sun_klu_rcond(content->symbolic, content->numeric, &content->common); + auto status + = sun_klu_rcond(content->symbolic, content->numeric, &content->common); if (status == 0) throw NewtonFailure(content->last_flag, "sun_klu_rcond"); @@ -222,9 +235,10 @@ bool NewtonSolverSparse::is_singular(Model& /*model*/, if (content->common.rcond < precision) { // cheap check indicates singular, expensive check via condest - status = sun_klu_condest(SM_INDEXPTRS_S(Jtmp_.get()), - SM_DATA_S(Jtmp_.get()), content->symbolic, - content->numeric, &content->common); + status = sun_klu_condest( + SM_INDEXPTRS_S(Jtmp_.get()), SM_DATA_S(Jtmp_.get()), + content->symbolic, content->numeric, &content->common + ); if (status == 0) throw NewtonFailure(content->last_flag, "sun_klu_rcond"); return content->common.condest > 1.0 / precision; diff --git a/src/rdata.cpp b/src/rdata.cpp index 53a622882a..5acce5d6f0 100644 --- a/src/rdata.cpp +++ b/src/rdata.cpp @@ -9,39 +9,49 @@ #include "amici/steadystateproblem.h" #include "amici/symbolic_functions.h" -#include #include +#include namespace amici { -ReturnData::ReturnData(Solver const &solver, const Model &model) - : ReturnData(model.getTimepoints(), - ModelDimensions(static_cast(model)), - model.nplist(), model.nMaxEvent(), model.nt(), - solver.getNewtonMaxSteps(), - model.getParameterScale(), model.o2mode, - solver.getSensitivityOrder(), solver.getSensitivityMethod(), - solver.getReturnDataReportingMode(), model.hasQuadraticLLH(), - model.getAddSigmaResiduals(), - model.getMinimumSigmaResiduals()) {} - -ReturnData::ReturnData(std::vector ts, - ModelDimensions const& model_dimensions, - int nplist, int nmaxevent, - int nt, int newton_maxsteps, - std::vector pscale, - SecondOrderMode o2mode, SensitivityOrder sensi, - SensitivityMethod sensi_meth, RDataReporting rdrm, - bool quadratic_llh, bool sigma_res, - realtype sigma_offset) - : ModelDimensions(model_dimensions), ts(std::move(ts)), nx(nx_rdata), - nxtrue(nxtrue_rdata), nplist(nplist), - nmaxevent(nmaxevent), nt(nt), newton_maxsteps(newton_maxsteps), - pscale(std::move(pscale)), o2mode(o2mode), sensi(sensi), - sensi_meth(sensi_meth), rdata_reporting(rdrm), sigma_res(sigma_res), - sigma_offset(sigma_offset), x_solver_(nx_solver), - sx_solver_(nx_solver, nplist), x_rdata_(nx), sx_rdata_(nx, nplist), - nroots_(ne) { +ReturnData::ReturnData(Solver const& solver, Model const& model) + : ReturnData( + model.getTimepoints(), + ModelDimensions(static_cast(model)), + model.nplist(), model.nMaxEvent(), model.nt(), + solver.getNewtonMaxSteps(), model.getParameterScale(), model.o2mode, + solver.getSensitivityOrder(), solver.getSensitivityMethod(), + solver.getReturnDataReportingMode(), model.hasQuadraticLLH(), + model.getAddSigmaResiduals(), model.getMinimumSigmaResiduals() + ) {} + +ReturnData::ReturnData( + std::vector ts, ModelDimensions const& model_dimensions, + int nplist, int nmaxevent, int nt, int newton_maxsteps, + std::vector pscale, SecondOrderMode o2mode, + SensitivityOrder sensi, SensitivityMethod sensi_meth, RDataReporting rdrm, + bool quadratic_llh, bool sigma_res, realtype sigma_offset +) + : ModelDimensions(model_dimensions) + , ts(std::move(ts)) + , nx(nx_rdata) + , nxtrue(nxtrue_rdata) + , nplist(nplist) + , nmaxevent(nmaxevent) + , nt(nt) + , newton_maxsteps(newton_maxsteps) + , pscale(std::move(pscale)) + , o2mode(o2mode) + , sensi(sensi) + , sensi_meth(sensi_meth) + , rdata_reporting(rdrm) + , sigma_res(sigma_res) + , sigma_offset(sigma_offset) + , x_solver_(nx_solver) + , sx_solver_(nx_solver, nplist) + , x_rdata_(nx) + , sx_rdata_(nx, nplist) + , nroots_(ne) { switch (rdata_reporting) { case RDataReporting::full: initializeFullReporting(quadratic_llh); @@ -65,8 +75,9 @@ void ReturnData::initializeLikelihoodReporting(bool enable_fim) { if (sensi >= SensitivityOrder::second) s2llh.resize(nplist * (nJ - 1), getNaN()); - if ((sensi_meth == SensitivityMethod::forward || - sensi >= SensitivityOrder::second) && enable_fim) + if ((sensi_meth == SensitivityMethod::forward + || sensi >= SensitivityOrder::second) + && enable_fim) FIM.resize(nplist * nplist, 0.0); } } @@ -77,8 +88,8 @@ void ReturnData::initializeResidualReporting(bool enable_res) { if (enable_res) res.resize((sigma_res ? 2 : 1) * nt * nytrue, 0.0); - if ((sensi_meth == SensitivityMethod::forward && - sensi >= SensitivityOrder::first) + if ((sensi_meth == SensitivityMethod::forward + && sensi >= SensitivityOrder::first) || sensi >= SensitivityOrder::second) { sy.resize(nt * ny * nplist, 0.0); @@ -117,8 +128,8 @@ void ReturnData::initializeFullReporting(bool quadratic_llh) { numnonlinsolvconvfails.resize(nt, 0); order.resize(nt, 0); - if (sensi_meth == SensitivityMethod::adjoint && - sensi >= SensitivityOrder::first) { + if (sensi_meth == SensitivityMethod::adjoint + && sensi >= SensitivityOrder::first) { numstepsB.resize(nt, 0); numrhsevalsB.resize(nt, 0); numerrtestfailsB.resize(nt, 0); @@ -133,8 +144,8 @@ void ReturnData::initializeFullReporting(bool quadratic_llh) { sx0.resize(nx * nplist, getNaN()); sx_ss.resize(nx * nplist, getNaN()); - if (sensi_meth == SensitivityMethod::forward || - sensi >= SensitivityOrder::second) { + if (sensi_meth == SensitivityMethod::forward + || sensi >= SensitivityOrder::second) { // for second order we can fill in from the augmented states sx.resize(nt * nx * nplist, 0.0); sz.resize(nmaxevent * nz * nplist, 0.0); @@ -142,18 +153,17 @@ void ReturnData::initializeFullReporting(bool quadratic_llh) { } ssigmaz.resize(nmaxevent * nz * nplist, 0.0); - if (sensi >= SensitivityOrder::second && - sensi_meth == SensitivityMethod::forward) + if (sensi >= SensitivityOrder::second + && sensi_meth == SensitivityMethod::forward) s2rz.resize(nmaxevent * nztrue * nplist * nplist, 0.0); } } -void ReturnData::processSimulationObjects(SteadystateProblem const *preeq, - ForwardProblem const *fwd, - BackwardProblem const *bwd, - SteadystateProblem const *posteq, - Model &model, Solver const &solver, - ExpData const *edata) { +void ReturnData::processSimulationObjects( + SteadystateProblem const* preeq, ForwardProblem const* fwd, + BackwardProblem const* bwd, SteadystateProblem const* posteq, Model& model, + Solver const& solver, ExpData const* edata +) { ModelContext mc(&model); processSolver(solver); @@ -182,8 +192,9 @@ void ReturnData::processSimulationObjects(SteadystateProblem const *preeq, applyChainRuleFactorToSimulationResults(model); } -void ReturnData::processPreEquilibration(SteadystateProblem const &preeq, - Model &model) { +void ReturnData::processPreEquilibration( + SteadystateProblem const& preeq, Model& model +) { readSimulationState(preeq.getFinalSimulationState(), model); if (!x_ss.empty()) { @@ -207,8 +218,9 @@ void ReturnData::processPreEquilibration(SteadystateProblem const &preeq, writeSlice(preeq.getNumSteps(), preeq_numsteps); } -void ReturnData::processPostEquilibration(SteadystateProblem const &posteq, - Model &model, ExpData const *edata) { +void ReturnData::processPostEquilibration( + SteadystateProblem const& posteq, Model& model, ExpData const* edata +) { for (int it = 0; it < nt; it++) { auto t = model.getTimepoint(it); if (std::isinf(t)) { @@ -228,8 +240,9 @@ void ReturnData::processPostEquilibration(SteadystateProblem const &posteq, writeSlice(posteq.getNumSteps(), posteq_numsteps); } -void ReturnData::processForwardProblem(ForwardProblem const &fwd, Model &model, - ExpData const *edata) { +void ReturnData::processForwardProblem( + ForwardProblem const& fwd, Model& model, ExpData const* edata +) { if (edata) initializeObjectiveFunction(model.hasQuadraticLLH()); @@ -272,7 +285,7 @@ void ReturnData::processForwardProblem(ForwardProblem const &fwd, Model &model, } } -void ReturnData::getDataOutput(int it, Model &model, ExpData const *edata) { +void ReturnData::getDataOutput(int it, Model& model, ExpData const* edata) { if (!x.empty()) { model.fx_rdata(x_rdata_, x_solver_); writeSlice(x_rdata_, slice(x, it, nx)); @@ -297,41 +310,45 @@ void ReturnData::getDataOutput(int it, Model &model, ExpData const *edata) { getDataSensisFSA(it, model, edata); } else if (edata && !sllh.empty()) { model.addPartialObservableObjectiveSensitivity( - sllh, s2llh, it, x_solver_, *edata); + sllh, s2llh, it, x_solver_, *edata + ); } if (!ssigmay.empty()) - model.getObservableSigmaSensitivity(slice(ssigmay, it, nplist * ny), - slice(sy, it, nplist * ny), - it, edata); + model.getObservableSigmaSensitivity( + slice(ssigmay, it, nplist * ny), slice(sy, it, nplist * ny), it, + edata + ); } } -void ReturnData::getDataSensisFSA(int it, Model &model, ExpData const *edata) { +void ReturnData::getDataSensisFSA(int it, Model& model, ExpData const* edata) { if (!sx.empty()) { model.fsx_rdata(sx_rdata_, sx_solver_, x_solver_); for (int ip = 0; ip < nplist; ip++) { - writeSlice(sx_rdata_[ip], - slice(sx, it * nplist + ip, nx)); + writeSlice(sx_rdata_[ip], slice(sx, it * nplist + ip, nx)); } } if (!sy.empty()) { - model.getObservableSensitivity(slice(sy, it, nplist * ny), ts[it], - x_solver_, sx_solver_); + model.getObservableSensitivity( + slice(sy, it, nplist * ny), ts[it], x_solver_, sx_solver_ + ); } if (edata) { if (!sllh.empty()) - model.addObservableObjectiveSensitivity(sllh, s2llh, it, x_solver_, - sx_solver_, *edata); + model.addObservableObjectiveSensitivity( + sllh, s2llh, it, x_solver_, sx_solver_, *edata + ); fsres(it, model, *edata); fFIM(it, model, *edata); } } -void ReturnData::getEventOutput(realtype t, std::vector rootidx, - Model &model, ExpData const *edata) { +void ReturnData::getEventOutput( + realtype t, std::vector rootidx, Model& model, ExpData const* edata +) { for (int ie = 0; ie < ne; ie++) { if (rootidx.at(ie) != 1 || nroots_.at(ie) >= nmaxevent) @@ -344,22 +361,27 @@ void ReturnData::getEventOutput(realtype t, std::vector rootidx, then also get the root function value */ if (t == model.getTimepoint(nt - 1)) if (!rz.empty()) - model.getEventRegularization(slice(rz, nroots_.at(ie), nz), ie, - t, x_solver_); + model.getEventRegularization( + slice(rz, nroots_.at(ie), nz), ie, t, x_solver_ + ); if (edata) { if (!sigmaz.empty()) - model.getEventSigma(slice(sigmaz, nroots_.at(ie), nz), ie, - nroots_.at(ie), t, edata); + model.getEventSigma( + slice(sigmaz, nroots_.at(ie), nz), ie, nroots_.at(ie), t, + edata + ); if (!isNaN(llh)) - model.addEventObjective(llh, ie, nroots_.at(ie), t, x_solver_, - *edata); + model.addEventObjective( + llh, ie, nroots_.at(ie), t, x_solver_, *edata + ); /* if called from fillEvent at last timepoint, add regularization based on rz */ if (t == model.getTimepoint(nt - 1) && !isNaN(llh)) { model.addEventObjectiveRegularization( - llh, ie, nroots_.at(ie), t, x_solver_, *edata); + llh, ie, nroots_.at(ie), t, x_solver_, *edata + ); } } @@ -368,39 +390,45 @@ void ReturnData::getEventOutput(realtype t, std::vector rootidx, getEventSensisFSA(ie, t, model, edata); } else if (edata && !sllh.empty()) { model.addPartialEventObjectiveSensitivity( - sllh, s2llh, ie, nroots_.at(ie), t, x_solver_, *edata); + sllh, s2llh, ie, nroots_.at(ie), t, x_solver_, *edata + ); } } nroots_.at(ie)++; } } -void ReturnData::getEventSensisFSA(int ie, realtype t, Model &model, - ExpData const *edata) { +void ReturnData::getEventSensisFSA( + int ie, realtype t, Model& model, ExpData const* edata +) { if (t == model.getTimepoint(nt - 1)) { // call from fillEvent at last timepoint if (!sz.empty()) model.getUnobservedEventSensitivity( - slice(sz, nroots_.at(ie), nz * nplist), ie); + slice(sz, nroots_.at(ie), nz * nplist), ie + ); if (!srz.empty()) model.getEventRegularizationSensitivity( slice(srz, nroots_.at(ie), nz * nplist), ie, t, x_solver_, - sx_solver_); + sx_solver_ + ); } else if (!sz.empty()) { - model.getEventSensitivity(slice(sz, nroots_.at(ie), nz * nplist), ie, - t, x_solver_, sx_solver_); + model.getEventSensitivity( + slice(sz, nroots_.at(ie), nz * nplist), ie, t, x_solver_, sx_solver_ + ); } if (edata && !sllh.empty()) { - model.addEventObjectiveSensitivity(sllh, s2llh, ie, nroots_.at(ie), - t, x_solver_, sx_solver_, *edata); + model.addEventObjectiveSensitivity( + sllh, s2llh, ie, nroots_.at(ie), t, x_solver_, sx_solver_, *edata + ); } } -void ReturnData::processBackwardProblem(ForwardProblem const &fwd, - BackwardProblem const &bwd, - SteadystateProblem const *preeq, - Model &model) { +void ReturnData::processBackwardProblem( + ForwardProblem const& fwd, BackwardProblem const& bwd, + SteadystateProblem const* preeq, Model& model +) { if (sllh.empty()) return; readSimulationState(fwd.getInitialSimulationState(), model); @@ -420,17 +448,18 @@ void ReturnData::processBackwardProblem(ForwardProblem const &fwd, if (iJ == 0) { sllh.at(ip) -= llhS0[ip] + xQB[ip * model.nJ]; } else { - s2llh.at(iJ - 1 + ip * (model.nJ - 1)) -= - llhS0[ip + iJ * model.nplist()] + xQB[iJ + ip * model.nJ]; + s2llh.at(iJ - 1 + ip * (model.nJ - 1)) + -= llhS0[ip + iJ * model.nplist()] + + xQB[iJ + ip * model.nJ]; } } } } -void ReturnData::handleSx0Backward(const Model &model, - SteadystateProblem const &preeq, - std::vector &llhS0, - AmiVector &xQB) const { +void ReturnData::handleSx0Backward( + Model const& model, SteadystateProblem const& preeq, + std::vector& llhS0, AmiVector& xQB +) const { /* If preequilibration is run in adjoint mode, the scalar product of sx0 with its adjoint counterpart (see handleSx0Forward()) is not necessary: the actual simulation is "extended" by the preequilibration time. @@ -438,13 +467,13 @@ void ReturnData::handleSx0Backward(const Model &model, and so is the scalar product. Instead of the scalar product, the quadratures xQB from preequilibration contribute to the gradient (see example notebook on equilibration for further documentation). */ - const auto &xQBpreeq = preeq.getAdjointQuadrature(); + auto const& xQBpreeq = preeq.getAdjointQuadrature(); for (int ip = 0; ip < model.nplist(); ++ip) xQB[ip] += xQBpreeq.at(ip); /* We really need references here, as sx0 can be large... */ - const auto& sx0preeq = preeq.getStateSensitivity(); - const auto& xBpreeq = preeq.getAdjointState(); + auto const& sx0preeq = preeq.getStateSensitivity(); + auto const& xBpreeq = preeq.getAdjointState(); /* Add the contribution for sx0 from preequilibration. If backward * preequilibration was done by simulation due to a singular Jacobian, @@ -457,9 +486,9 @@ void ReturnData::handleSx0Backward(const Model &model, } } -void ReturnData::handleSx0Forward(const Model &model, - std::vector &llhS0, - AmiVector &xB) const { +void ReturnData::handleSx0Forward( + Model const& model, std::vector& llhS0, AmiVector& xB +) const { /* If preequilibration is run in forward mode or is not needed, then adjoint sensitivity analysis still needs the state sensitivities at t=0 (sx0), to compute the gradient. For each parameter, the scalar product of sx0 @@ -476,20 +505,24 @@ void ReturnData::handleSx0Forward(const Model &model, for (int ip = 0; ip < model.nplist(); ++ip) { llhS0[ip + iJ * model.nplist()] = 0.0; for (int ix = 0; ix < model.nxtrue_solver; ++ix) { - llhS0[ip + iJ * model.nplist()] += - xB[ix + iJ * model.nxtrue_solver] * sx_solver_.at(ix, ip) + - xB[ix] * sx_solver_.at(ix + iJ * model.nxtrue_solver, ip); + llhS0[ip + iJ * model.nplist()] + += xB[ix + iJ * model.nxtrue_solver] + * sx_solver_.at(ix, ip) + + xB[ix] + * sx_solver_.at( + ix + iJ * model.nxtrue_solver, ip + ); } } } } } -void ReturnData::processSolver(Solver const &solver) { +void ReturnData::processSolver(Solver const& solver) { cpu_time = solver.getCpuTime(); - const std::vector *tmp; + std::vector const* tmp; if (!numsteps.empty()) { tmp = &solver.getNumSteps(); @@ -537,13 +570,15 @@ void ReturnData::processSolver(Solver const &solver) { if (!numnonlinsolvconvfailsB.empty()) { tmp = &solver.getNumNonlinSolvConvFailsB(); - std::copy_n(tmp->cbegin(), tmp->size(), - numnonlinsolvconvfailsB.begin()); + std::copy_n( + tmp->cbegin(), tmp->size(), numnonlinsolvconvfailsB.begin() + ); } } -void ReturnData::readSimulationState(SimulationState const &state, - Model &model) { +void ReturnData::readSimulationState( + SimulationState const& state, Model& model +) { x_solver_ = state.x; dx_solver_ = state.dx; if (computingFSA() || state.t == model.t0()) @@ -552,7 +587,7 @@ void ReturnData::readSimulationState(SimulationState const &state, model.setModelState(state.state); } -void ReturnData::invalidate(const int it_start) { +void ReturnData::invalidate(int const it_start) { if (it_start >= nt) return; @@ -584,14 +619,15 @@ void ReturnData::invalidateSLLH() { } } -void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { +void ReturnData::applyChainRuleFactorToSimulationResults(Model const& model) { // chain-rule factor: multiplier for am_p std::vector coefficient(nplist, 1.0); std::vector pcoefficient(nplist, 1.0); std::vector unscaledParameters = model.getParameters(); - unscaleParameters(unscaledParameters, model.getParameterScale(), - unscaledParameters); + unscaleParameters( + unscaledParameters, model.getParameterScale(), unscaledParameters + ); std::vector augcoefficient(np, 1.0); @@ -614,8 +650,8 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { switch (pscale[model.plist(ip)]) { case ParameterScaling::log10: coefficient.at(ip) = log(10.0); - pcoefficient.at(ip) = - unscaledParameters.at(model.plist(ip)) * log(10); + pcoefficient.at(ip) + = unscaledParameters.at(model.plist(ip)) * log(10); break; case ParameterScaling::ln: pcoefficient.at(ip) = unscaledParameters.at(model.plist(ip)); @@ -628,56 +664,52 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { if (sensi >= SensitivityOrder::first) { // recover first order sensitivities from states for adjoint sensitivity // analysis - if (sensi == SensitivityOrder::second - && o2mode == SecondOrderMode::full + if (sensi == SensitivityOrder::second && o2mode == SecondOrderMode::full && sensi_meth == SensitivityMethod::adjoint) { if (!sx.empty() && !x.empty()) for (int ip = 0; ip < nplist; ++ip) for (int ix = 0; ix < nxtrue; ++ix) for (int it = 0; it < nt; ++it) - sx.at(ix + nxtrue * (ip + it * nplist)) = - x.at(it * nx + nxtrue + ip * nxtrue + ix); + sx.at(ix + nxtrue * (ip + it * nplist)) + = x.at(it * nx + nxtrue + ip * nxtrue + ix); if (!sy.empty() && !y.empty()) for (int ip = 0; ip < nplist; ++ip) for (int iy = 0; iy < nytrue; ++iy) for (int it = 0; it < nt; ++it) - sy.at(iy + nytrue * (ip + it * nplist)) = - y.at(it * ny + nytrue + ip * nytrue + iy); + sy.at(iy + nytrue * (ip + it * nplist)) + = y.at(it * ny + nytrue + ip * nytrue + iy); if (!sz.empty() && !z.empty()) for (int ip = 0; ip < nplist; ++ip) for (int iz = 0; iz < nztrue; ++iz) for (int it = 0; it < nt; ++it) - sz.at(iz + nztrue * (ip + it * nplist)) = - z.at(it * nz + nztrue + ip * nztrue + iz); - + sz.at(iz + nztrue * (ip + it * nplist)) + = z.at(it * nz + nztrue + ip * nztrue + iz); } if (!sllh.empty()) for (int ip = 0; ip < nplist; ++ip) sllh.at(ip) *= pcoefficient.at(ip); - if (!sres.empty()) for (int ires = 0; ires < gsl::narrow(res.size()); ++ires) for (int ip = 0; ip < nplist; ++ip) sres.at((ires * nplist + ip)) *= pcoefficient.at(ip); - - if(!FIM.empty()) + if (!FIM.empty()) for (int ip = 0; ip < nplist; ++ip) for (int jp = 0; jp < nplist; ++jp) - FIM.at(jp + ip * nplist) *= - pcoefficient.at(ip)*pcoefficient.at(jp); + FIM.at(jp + ip * nplist) + *= pcoefficient.at(ip) * pcoefficient.at(jp); #define chainRule(QUANT, IND1, N1T, N1, IND2, N2) \ if (!s##QUANT.empty()) \ for (int IND1 = 0; (IND1) < (N1T); ++(IND1)) \ for (int ip = 0; ip < nplist; ++ip) \ for (int IND2 = 0; (IND2) < (N2); ++(IND2)) { \ - s##QUANT.at(((IND2)*nplist + ip) * (N1) + (IND1)) *= \ - pcoefficient.at(ip); \ + s##QUANT.at(((IND2)*nplist + ip) * (N1) + (IND1)) \ + *= pcoefficient.at(ip); \ } chainRule(x, ix, nxtrue, nx, it, nt); @@ -693,11 +725,11 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { if (!s2llh.empty() && !sllh.empty()) { for (int ip = 0; ip < nplist; ++ip) { for (int iJ = 1; iJ < nJ; ++iJ) { - s2llh[ip * nplist + (iJ - 1)] *= - pcoefficient.at(ip) * augcoefficient[iJ - 1]; + s2llh[ip * nplist + (iJ - 1)] + *= pcoefficient.at(ip) * augcoefficient[iJ - 1]; if (model.plist(ip) == iJ - 1) - s2llh[ip * nplist + (iJ - 1)] += - sllh.at(ip) * coefficient.at(ip); + s2llh[ip * nplist + (iJ - 1)] + += sllh.at(ip) * coefficient.at(ip); } } } @@ -708,15 +740,14 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { for (int iJ = 1; iJ < nJ; ++iJ) \ for (int IND1 = 0; IND1 < N1T; ++IND1) \ for (int IND2 = 0; IND2 < N2; ++IND2) { \ - s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + \ - iJ * N1T) *= \ - pcoefficient.at(ip) * augcoefficient[iJ - 1]; \ + s##QUANT.at( \ + (IND2 * nplist + ip) * N1 + IND1 + iJ * N1T \ + ) *= pcoefficient.at(ip) * augcoefficient[iJ - 1]; \ if (model.plist(ip) == iJ - 1) \ - s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + \ - iJ * N1T) += \ - s##QUANT.at((IND2 * nplist + ip) * N1 + \ - IND1) * \ - coefficient[ip]; \ + s##QUANT.at( \ + (IND2 * nplist + ip) * N1 + IND1 + iJ * N1T \ + ) += s##QUANT.at((IND2 * nplist + ip) * N1 + IND1) \ + * coefficient[ip]; \ } s2ChainRule(x, ix, nxtrue, nx, it, nt); @@ -730,8 +761,8 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { if (o2mode == SecondOrderMode::directional) { // directional for (int ip = 0; ip < nplist; ++ip) { s2llh.at(ip) *= pcoefficient.at(ip); - s2llh.at(ip) += model.k()[nk - nplist + ip] * sllh.at(ip) / - unscaledParameters[model.plist(ip)]; + s2llh.at(ip) += model.k()[nk - nplist + ip] * sllh.at(ip) + / unscaledParameters[model.plist(ip)]; } #define s2vecChainRule(QUANT, IND1, N1T, N1, IND2, N2) \ @@ -739,12 +770,12 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { for (int ip = 0; ip < nplist; ++ip) \ for (int IND1 = 0; IND1 < N1T; ++IND1) \ for (int IND2 = 0; IND2 < N2; ++IND2) { \ - s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + N1T) *= \ - pcoefficient.at(ip); \ - s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + N1T) += \ - model.k()[nk - nplist + ip] * \ - s##QUANT.at((IND2 * nplist + ip) * N1 + IND1) / \ - unscaledParameters[model.plist(ip)]; \ + s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + N1T) \ + *= pcoefficient.at(ip); \ + s##QUANT.at((IND2 * nplist + ip) * N1 + IND1 + N1T) \ + += model.k()[nk - nplist + ip] \ + * s##QUANT.at((IND2 * nplist + ip) * N1 + IND1) \ + / unscaledParameters[model.plist(ip)]; \ } s2vecChainRule(x, ix, nxtrue, nx, it, nt); @@ -757,36 +788,37 @@ void ReturnData::applyChainRuleFactorToSimulationResults(const Model &model) { } void ReturnData::initializeObjectiveFunction(bool enable_chi2) { - if (rdata_reporting == RDataReporting::likelihood || - rdata_reporting == RDataReporting::full) { + if (rdata_reporting == RDataReporting::likelihood + || rdata_reporting == RDataReporting::full) { llh = 0.0; std::fill(sllh.begin(), sllh.end(), 0.0); std::fill(s2llh.begin(), s2llh.end(), 0.0); } - if ((rdata_reporting == RDataReporting::residuals || - rdata_reporting == RDataReporting::full) && enable_chi2) + if ((rdata_reporting == RDataReporting::residuals + || rdata_reporting == RDataReporting::full) + && enable_chi2) chi2 = 0.0; } -static realtype fres(realtype y, realtype my, realtype sigma_y, - ObservableScaling scale) { +static realtype +fres(realtype y, realtype my, realtype sigma_y, ObservableScaling scale) { switch (scale) { - case amici::ObservableScaling::lin: - return (y - my) / sigma_y; - case amici::ObservableScaling::log: - return (std::log(y) - std::log(my)) / sigma_y; - case amici::ObservableScaling::log10: - return (std::log10(y) - std::log10(my)) / sigma_y; - default: - throw std::invalid_argument("only lin, log, log10 allowed."); + case amici::ObservableScaling::lin: + return (y - my) / sigma_y; + case amici::ObservableScaling::log: + return (std::log(y) - std::log(my)) / sigma_y; + case amici::ObservableScaling::log10: + return (std::log10(y) - std::log10(my)) / sigma_y; + default: + throw std::invalid_argument("only lin, log, log10 allowed."); } } static realtype fres_error(realtype sigma_y, realtype sigma_offset) { - return sqrt(2*log(sigma_y) + sigma_offset); + return sqrt(2 * log(sigma_y) + sigma_offset); } -void ReturnData::fres(const int it, Model &model, const ExpData &edata) { +void ReturnData::fres(int const it, Model& model, ExpData const& edata) { if (res.empty()) return; @@ -802,17 +834,18 @@ void ReturnData::fres(const int it, Model &model, const ExpData &edata) { if (!edata.isSetObservedData(it, iy)) continue; - res.at(iyt) = amici::fres(y_it.at(iy), observedData[iy], - sigmay_it.at(iy), - model.getObservableScaling(iy)); + res.at(iyt) = amici::fres( + y_it.at(iy), observedData[iy], sigmay_it.at(iy), + model.getObservableScaling(iy) + ); if (sigma_res) - res.at(iyt + nt * nytrue) = fres_error(sigmay_it.at(iy), - sigma_offset); + res.at(iyt + nt * nytrue) + = fres_error(sigmay_it.at(iy), sigma_offset); } } -void ReturnData::fchi2(const int it, const ExpData &edata) { +void ReturnData::fchi2(int const it, ExpData const& edata) { if (res.empty() || isNaN(chi2)) return; @@ -824,28 +857,28 @@ void ReturnData::fchi2(const int it, const ExpData &edata) { } } -static realtype fsres(realtype y, realtype sy, realtype my, - realtype sigma_y, realtype ssigma_y, - ObservableScaling scale) { +static realtype fsres( + realtype y, realtype sy, realtype my, realtype sigma_y, realtype ssigma_y, + ObservableScaling scale +) { auto res = fres(y, my, sigma_y, scale); switch (scale) { - case amici::ObservableScaling::lin: - return (sy - ssigma_y * res) / sigma_y; - case amici::ObservableScaling::log: - return (sy / y - ssigma_y * res) / sigma_y; - case amici::ObservableScaling::log10: - return (sy / (y * std::log(10)) - ssigma_y * res) / sigma_y; - default: - throw std::invalid_argument("only lin, log, log10 allowed."); + case amici::ObservableScaling::lin: + return (sy - ssigma_y * res) / sigma_y; + case amici::ObservableScaling::log: + return (sy / y - ssigma_y * res) / sigma_y; + case amici::ObservableScaling::log10: + return (sy / (y * std::log(10)) - ssigma_y * res) / sigma_y; + default: + throw std::invalid_argument("only lin, log, log10 allowed."); } } -static realtype fsres_error(realtype sigma_y, realtype ssigma_y, - realtype sigma_offset) { - return ssigma_y / ( fres_error(sigma_y, sigma_offset) * sigma_y); +static realtype +fsres_error(realtype sigma_y, realtype ssigma_y, realtype sigma_offset) { + return ssigma_y / (fres_error(sigma_y, sigma_offset) * sigma_y); } - -void ReturnData::fsres(const int it, Model &model, const ExpData &edata) { +void ReturnData::fsres(int const it, Model& model, ExpData const& edata) { if (sres.empty()) return; @@ -866,24 +899,26 @@ void ReturnData::fsres(const int it, Model &model, const ExpData &edata) { for (int ip = 0; ip < nplist; ++ip) { int idx = (iy + it * edata.nytrue()) * nplist + ip; - sres.at(idx) = amici::fsres(y_it.at(iy), sy_it.at(iy + ny * ip), - observedData[iy], sigmay_it.at(iy), - ssigmay_it.at(iy + ny * ip), - model.getObservableScaling(iy)); + sres.at(idx) = amici::fsres( + y_it.at(iy), sy_it.at(iy + ny * ip), observedData[iy], + sigmay_it.at(iy), ssigmay_it.at(iy + ny * ip), + model.getObservableScaling(iy) + ); if (sigma_res) { - int idx_res = - (iy + it * edata.nytrue() + edata.nytrue() * edata.nt()) * - nplist + ip; - sres.at(idx_res) = amici::fsres_error(sigmay_it.at(iy), - ssigmay_it.at(iy + ny * ip), - sigma_offset); + int idx_res + = (iy + it * edata.nytrue() + edata.nytrue() * edata.nt()) + * nplist + + ip; + sres.at(idx_res) = amici::fsres_error( + sigmay_it.at(iy), ssigmay_it.at(iy + ny * ip), sigma_offset + ); } } } } -void ReturnData::fFIM(int it, Model &model, const ExpData &edata) { +void ReturnData::fFIM(int it, Model& model, ExpData const& edata) { if (FIM.empty()) return; @@ -971,7 +1006,7 @@ void ReturnData::fFIM(int it, Model &model, const ExpData &edata) { auto dy_j = sy_it.at(iy + ny * jp); auto ds_j = ssigmay_it.at(iy + ny * jp); auto sr_j = amici::fsres(y, dy_j, m, s, ds_j, os); - FIM.at(ip + nplist * jp) += sr_i*sr_j; + FIM.at(ip + nplist * jp) += sr_i * sr_j; if (sigma_res) { auto sre_j = amici::fsres_error(s, ds_j, sigma_offset); FIM.at(ip + nplist * jp) += sre_i * sre_j; @@ -982,8 +1017,9 @@ void ReturnData::fFIM(int it, Model &model, const ExpData &edata) { } } -ModelContext::ModelContext(Model *model) - : model_(model), original_state_(model->getModelState()) {} +ModelContext::ModelContext(Model* model) + : model_(model) + , original_state_(model->getModelState()) {} ModelContext::~ModelContext() { restore(); } diff --git a/src/returndata_matlab.cpp b/src/returndata_matlab.cpp index 9599be2673..87dca58890 100644 --- a/src/returndata_matlab.cpp +++ b/src/returndata_matlab.cpp @@ -1,15 +1,15 @@ #include "amici/returndata_matlab.h" -#include "amici/exception.h" #include "amici/defines.h" +#include "amici/exception.h" namespace amici { -mxArray *getReturnDataMatlabFromAmiciCall(ReturnData const *rdata) { - mxArray *matlabSolutionStruct = initMatlabReturnFields(rdata); +mxArray* getReturnDataMatlabFromAmiciCall(ReturnData const* rdata) { + mxArray* matlabSolutionStruct = initMatlabReturnFields(rdata); return matlabSolutionStruct; } -mxArray *initMatlabReturnFields(ReturnData const *rdata) { +mxArray* initMatlabReturnFields(ReturnData const* rdata) { int const numFields = 22; char const* field_names_sol[numFields] = {"status", "llh", "sllh", "s2llh", "chi2", "t", @@ -17,10 +17,10 @@ mxArray *initMatlabReturnFields(ReturnData const *rdata) { "z", "sz", "sigmaz", "ssigmaz", "rz", "srz", "s2rz", "x0", "sx0", "diagnosis"}; - checkFieldNames(field_names_sol,numFields); + checkFieldNames(field_names_sol, numFields); - mxArray *matlabSolutionStruct = - mxCreateStructMatrix(1, 1, numFields, field_names_sol); + mxArray* matlabSolutionStruct + = mxCreateStructMatrix(1, 1, numFields, field_names_sol); std::vector perm0 = {1, 0}; std::vector perm1 = {0, 1}; @@ -29,61 +29,111 @@ mxArray *initMatlabReturnFields(ReturnData const *rdata) { writeMatlabField0(matlabSolutionStruct, "status", rdata->status); - writeMatlabField1(matlabSolutionStruct, "t", gsl::make_span(rdata->ts), rdata->nt); + writeMatlabField1( + matlabSolutionStruct, "t", gsl::make_span(rdata->ts), rdata->nt + ); writeMatlabField0(matlabSolutionStruct, "llh", rdata->llh); writeMatlabField0(matlabSolutionStruct, "chi2", rdata->chi2); if ((rdata->nz > 0) & (rdata->ne > 0)) { - writeMatlabField2(matlabSolutionStruct, "z", rdata->z, rdata->nmaxevent, rdata->nz, perm1); - writeMatlabField2(matlabSolutionStruct, "rz", rdata->rz, rdata->nmaxevent, rdata->nz, perm1); - writeMatlabField2(matlabSolutionStruct, "sigmaz", rdata->sigmaz, rdata->nmaxevent, rdata->nz, perm1); + writeMatlabField2( + matlabSolutionStruct, "z", rdata->z, rdata->nmaxevent, rdata->nz, + perm1 + ); + writeMatlabField2( + matlabSolutionStruct, "rz", rdata->rz, rdata->nmaxevent, rdata->nz, + perm1 + ); + writeMatlabField2( + matlabSolutionStruct, "sigmaz", rdata->sigmaz, rdata->nmaxevent, + rdata->nz, perm1 + ); } if (rdata->nx > 0) { - writeMatlabField2(matlabSolutionStruct, "x", rdata->x, rdata->nt, rdata->nx, perm1); - writeMatlabField2(matlabSolutionStruct, "x0", rdata->x0, rdata->nx, 1, perm1); + writeMatlabField2( + matlabSolutionStruct, "x", rdata->x, rdata->nt, rdata->nx, perm1 + ); + writeMatlabField2( + matlabSolutionStruct, "x0", rdata->x0, rdata->nx, 1, perm1 + ); } if (rdata->ny > 0) { - writeMatlabField2(matlabSolutionStruct, "y", rdata->y, rdata->nt, rdata->ny, perm1); - writeMatlabField2(matlabSolutionStruct, "sigmay", rdata->sigmay, rdata->nt, rdata->ny, perm1); + writeMatlabField2( + matlabSolutionStruct, "y", rdata->y, rdata->nt, rdata->ny, perm1 + ); + writeMatlabField2( + matlabSolutionStruct, "sigmay", rdata->sigmay, rdata->nt, rdata->ny, + perm1 + ); } if (rdata->sensi >= SensitivityOrder::first) { - writeMatlabField1(matlabSolutionStruct, "sllh", gsl::make_span(rdata->sllh), rdata->nplist); - writeMatlabField2(matlabSolutionStruct, "sx0", rdata->sx0, rdata->nplist, rdata->nx, perm0); + writeMatlabField1( + matlabSolutionStruct, "sllh", gsl::make_span(rdata->sllh), + rdata->nplist + ); + writeMatlabField2( + matlabSolutionStruct, "sx0", rdata->sx0, rdata->nplist, rdata->nx, + perm0 + ); if (rdata->sensi_meth == SensitivityMethod::forward) { - writeMatlabField3(matlabSolutionStruct, "sx", rdata->sx, rdata->nt, rdata->nplist, rdata->nx, perm2); + writeMatlabField3( + matlabSolutionStruct, "sx", rdata->sx, rdata->nt, rdata->nplist, + rdata->nx, perm2 + ); if (rdata->ny > 0) { - writeMatlabField3(matlabSolutionStruct, "sy", rdata->sy, rdata->nt, rdata->nplist, rdata->ny, perm2); + writeMatlabField3( + matlabSolutionStruct, "sy", rdata->sy, rdata->nt, + rdata->nplist, rdata->ny, perm2 + ); } if ((rdata->nz > 0) & (rdata->ne > 0)) { - writeMatlabField3(matlabSolutionStruct, "srz", rdata->srz, rdata->nmaxevent, rdata->nplist, rdata->nz, perm2); + writeMatlabField3( + matlabSolutionStruct, "srz", rdata->srz, rdata->nmaxevent, + rdata->nplist, rdata->nz, perm2 + ); if (rdata->sensi >= SensitivityOrder::second) { - writeMatlabField4(matlabSolutionStruct, "s2rz", rdata->s2rz, rdata->nmaxevent, rdata->nplist, rdata->nztrue, - rdata->nplist, perm3); + writeMatlabField4( + matlabSolutionStruct, "s2rz", rdata->s2rz, + rdata->nmaxevent, rdata->nplist, rdata->nztrue, + rdata->nplist, perm3 + ); } - writeMatlabField3(matlabSolutionStruct, "sz", rdata->sz, rdata->nmaxevent, rdata->nplist, rdata->nz, perm2); + writeMatlabField3( + matlabSolutionStruct, "sz", rdata->sz, rdata->nmaxevent, + rdata->nplist, rdata->nz, perm2 + ); } } if (!rdata->ssigmay.empty()) { - writeMatlabField3(matlabSolutionStruct, "ssigmay", rdata->ssigmay, rdata->nt, rdata->nplist, rdata->ny, perm2); + writeMatlabField3( + matlabSolutionStruct, "ssigmay", rdata->ssigmay, rdata->nt, + rdata->nplist, rdata->ny, perm2 + ); } if ((rdata->nz > 0) & (rdata->ne > 0)) { - writeMatlabField3(matlabSolutionStruct, "ssigmaz", rdata->ssigmaz, rdata->nmaxevent, rdata->nplist, rdata->nz, perm2); + writeMatlabField3( + matlabSolutionStruct, "ssigmaz", rdata->ssigmaz, + rdata->nmaxevent, rdata->nplist, rdata->nz, perm2 + ); } if (rdata->sensi >= SensitivityOrder::second) { - writeMatlabField2(matlabSolutionStruct, "s2llh", rdata->s2llh, rdata->nplist, rdata->nJ - 1, perm1); + writeMatlabField2( + matlabSolutionStruct, "s2llh", rdata->s2llh, rdata->nplist, + rdata->nJ - 1, perm1 + ); } } - mxArray *diagnosis = initMatlabDiagnosisFields(rdata); + mxArray* diagnosis = initMatlabDiagnosisFields(rdata); mxSetField(matlabSolutionStruct, 0, "diagnosis", diagnosis); - return(matlabSolutionStruct); + return (matlabSolutionStruct); } -mxArray *initMatlabDiagnosisFields(ReturnData const *rdata) { +mxArray* initMatlabDiagnosisFields(ReturnData const* rdata) { int const numFields = 25; char const* field_names_sol[numFields] = {"xdot", @@ -112,10 +162,10 @@ mxArray *initMatlabDiagnosisFields(ReturnData const *rdata) { "posteq_t", "posteq_wrms"}; - checkFieldNames(field_names_sol,numFields); + checkFieldNames(field_names_sol, numFields); - mxArray *matlabDiagnosisStruct = - mxCreateStructMatrix(1, 1, numFields, field_names_sol); + mxArray* matlabDiagnosisStruct + = mxCreateStructMatrix(1, 1, numFields, field_names_sol); std::vector perm1 = {0, 1}; int finite_nt = 0; @@ -125,68 +175,107 @@ mxArray *initMatlabDiagnosisFields(ReturnData const *rdata) { writeMatlabField1( matlabDiagnosisStruct, "numsteps", - gsl::make_span(rdata->numsteps).subspan(0, finite_nt), - finite_nt); + gsl::make_span(rdata->numsteps).subspan(0, finite_nt), finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numrhsevals", - gsl::make_span(rdata->numrhsevals).subspan(0, finite_nt), - finite_nt); + gsl::make_span(rdata->numrhsevals).subspan(0, finite_nt), finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numerrtestfails", - gsl::make_span(rdata->numerrtestfails).subspan(0, finite_nt), - finite_nt); + gsl::make_span(rdata->numerrtestfails).subspan(0, finite_nt), finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numnonlinsolvconvfails", gsl::make_span(rdata->numnonlinsolvconvfails).subspan(0, finite_nt), - finite_nt); + finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "order", - gsl::make_span(rdata->order).subspan(0, finite_nt), - finite_nt); + gsl::make_span(rdata->order).subspan(0, finite_nt), finite_nt + ); if (rdata->nx > 0) { - writeMatlabField1(matlabDiagnosisStruct, "xdot", gsl::make_span(rdata->xdot), rdata->nx_solver); - writeMatlabField2(matlabDiagnosisStruct, "J", rdata->J, rdata->nx_solver, rdata->nx_solver, perm1); - - writeMatlabField1(matlabDiagnosisStruct, "preeq_status", gsl::make_span(rdata->preeq_status), 3); - writeMatlabField1(matlabDiagnosisStruct, "preeq_numsteps", gsl::make_span(rdata->preeq_numsteps), 3); - writeMatlabField0(matlabDiagnosisStruct, "preeq_numstepsB", rdata->preeq_numstepsB); - writeMatlabField0(matlabDiagnosisStruct, "preeq_cpu_time", rdata->preeq_cpu_time); - writeMatlabField0(matlabDiagnosisStruct, "preeq_cpu_timeB", rdata->preeq_cpu_timeB); + writeMatlabField1( + matlabDiagnosisStruct, "xdot", gsl::make_span(rdata->xdot), + rdata->nx_solver + ); + writeMatlabField2( + matlabDiagnosisStruct, "J", rdata->J, rdata->nx_solver, + rdata->nx_solver, perm1 + ); + + writeMatlabField1( + matlabDiagnosisStruct, "preeq_status", + gsl::make_span(rdata->preeq_status), 3 + ); + writeMatlabField1( + matlabDiagnosisStruct, "preeq_numsteps", + gsl::make_span(rdata->preeq_numsteps), 3 + ); + writeMatlabField0( + matlabDiagnosisStruct, "preeq_numstepsB", rdata->preeq_numstepsB + ); + writeMatlabField0( + matlabDiagnosisStruct, "preeq_cpu_time", rdata->preeq_cpu_time + ); + writeMatlabField0( + matlabDiagnosisStruct, "preeq_cpu_timeB", rdata->preeq_cpu_timeB + ); writeMatlabField0(matlabDiagnosisStruct, "preeq_t", rdata->preeq_t); - writeMatlabField0(matlabDiagnosisStruct, "preeq_wrms", rdata->preeq_wrms); - - writeMatlabField1(matlabDiagnosisStruct, "posteq_status", gsl::make_span(rdata->posteq_status), 3); - writeMatlabField1(matlabDiagnosisStruct, "posteq_numsteps", gsl::make_span(rdata->posteq_numsteps), 3); - writeMatlabField0(matlabDiagnosisStruct, "posteq_numstepsB", rdata->posteq_numstepsB); - writeMatlabField0(matlabDiagnosisStruct, "posteq_cpu_time", rdata->posteq_cpu_time); - writeMatlabField0(matlabDiagnosisStruct, "posteq_cpu_timeB", rdata->posteq_cpu_timeB); + writeMatlabField0( + matlabDiagnosisStruct, "preeq_wrms", rdata->preeq_wrms + ); + + writeMatlabField1( + matlabDiagnosisStruct, "posteq_status", + gsl::make_span(rdata->posteq_status), 3 + ); + writeMatlabField1( + matlabDiagnosisStruct, "posteq_numsteps", + gsl::make_span(rdata->posteq_numsteps), 3 + ); + writeMatlabField0( + matlabDiagnosisStruct, "posteq_numstepsB", rdata->posteq_numstepsB + ); + writeMatlabField0( + matlabDiagnosisStruct, "posteq_cpu_time", rdata->posteq_cpu_time + ); + writeMatlabField0( + matlabDiagnosisStruct, "posteq_cpu_timeB", rdata->posteq_cpu_timeB + ); writeMatlabField0(matlabDiagnosisStruct, "posteq_t", rdata->posteq_t); - writeMatlabField0(matlabDiagnosisStruct, "posteq_wrms", rdata->posteq_wrms); + writeMatlabField0( + matlabDiagnosisStruct, "posteq_wrms", rdata->posteq_wrms + ); } if (rdata->sensi >= SensitivityOrder::first) { if (rdata->sensi_meth == SensitivityMethod::adjoint) { writeMatlabField1( matlabDiagnosisStruct, "numstepsB", gsl::make_span(rdata->numstepsB).subspan(0, finite_nt), - finite_nt); + finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numrhsevalsB", gsl::make_span(rdata->numrhsevalsB).subspan(0, finite_nt), - finite_nt); + finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numerrtestfailsB", gsl::make_span(rdata->numerrtestfailsB).subspan(0, finite_nt), - finite_nt); + finite_nt + ); writeMatlabField1( matlabDiagnosisStruct, "numnonlinsolvconvfailsB", - gsl::make_span(rdata->numnonlinsolvconvfailsB - ).subspan(0, finite_nt), - finite_nt); + gsl::make_span(rdata->numnonlinsolvconvfailsB) + .subspan(0, finite_nt), + finite_nt + ); } } - return(matlabDiagnosisStruct); + return (matlabDiagnosisStruct); } template @@ -196,7 +285,7 @@ void writeMatlabField0( std::vector dim = {(mwSize)(1), (mwSize)(1)}; - double *array = initAndAttachArray(matlabStruct, fieldName, dim); + double* array = initAndAttachArray(matlabStruct, fieldName, dim); array[0] = static_cast(fieldData); } @@ -206,14 +295,16 @@ void writeMatlabField1( mxArray* matlabStruct, char const* fieldName, gsl::span const& fieldData, mwSize dim0 ) { - if(fieldData.size() != dim0) - throw AmiException("Dimension mismatch when writing rdata->%s to " - "matlab results (expected %d, got %d)", - fieldName, dim0, static_cast(fieldData.size())); + if (fieldData.size() != dim0) + throw AmiException( + "Dimension mismatch when writing rdata->%s to " + "matlab results (expected %d, got %d)", + fieldName, dim0, static_cast(fieldData.size()) + ); std::vector dim = {(mwSize)(dim0), (mwSize)(1)}; - double *array = initAndAttachArray(matlabStruct, fieldName, dim); + double* array = initAndAttachArray(matlabStruct, fieldName, dim); auto data_ptr = fieldData.data(); for (mwSize i = 0; i < dim0; i++) @@ -226,25 +317,27 @@ void writeMatlabField2( std::vector const& fieldData, mwSize dim0, mwSize dim1, std::vector perm ) { - if(fieldData.size() != dim0*dim1) - throw AmiException("Dimension mismatch when writing rdata->%s to " - "matlab results (expected: %d, actual: %d)", - fieldName, dim0 * dim1, - static_cast(fieldData.size())); - - if(perm.size() != 2) + if (fieldData.size() != dim0 * dim1) + throw AmiException( + "Dimension mismatch when writing rdata->%s to " + "matlab results (expected: %d, actual: %d)", + fieldName, dim0 * dim1, static_cast(fieldData.size()) + ); + + if (perm.size() != 2) throw AmiException("Dimension mismatch when applying permutation!"); std::vector dim = {dim0, dim1}; - double *array = initAndAttachArray(matlabStruct, fieldName, reorder(dim,perm)); + double* array + = initAndAttachArray(matlabStruct, fieldName, reorder(dim, perm)); std::vector index = {0, 0}; /* transform rowmajor (c++) to colmajor (matlab) and apply permutation */ for (index[0] = 0; index[0] < dim[0]; index[0]++) { for (index[1] = 0; index[1] < dim[1]; index[1]++) { - array[index[perm[0]] + index[perm[1]]*dim[perm[0]]] = - static_cast(fieldData[index[0]*dim[1] + index[1]]); + array[index[perm[0]] + index[perm[1]] * dim[perm[0]]] + = static_cast(fieldData[index[0] * dim[1] + index[1]]); } } } @@ -255,23 +348,33 @@ void writeMatlabField3( std::vector const& fieldData, mwSize dim0, mwSize dim1, mwSize dim2, std::vector perm ) { - if(fieldData.size() != dim0*dim1*dim2) - throw AmiException("Dimension mismatch when writing rdata->%s to matlab results",fieldName); + if (fieldData.size() != dim0 * dim1 * dim2) + throw AmiException( + "Dimension mismatch when writing rdata->%s to matlab results", + fieldName + ); - if(perm.size() != 3) + if (perm.size() != 3) throw AmiException("Dimension mismatch when applying permutation!"); std::vector dim = {(mwSize)(dim0), (mwSize)(dim1), (mwSize)(dim2)}; - double *array = initAndAttachArray(matlabStruct, fieldName, reorder(dim,perm)); + double* array + = initAndAttachArray(matlabStruct, fieldName, reorder(dim, perm)); std::vector index = {0, 0, 0}; /* transform rowmajor (c++) to colmajor (matlab) and apply permutation */ for (index[0] = 0; index[0] < dim[0]; index[0]++) { for (index[1] = 0; index[1] < dim[1]; index[1]++) { for (index[2] = 0; index[2] < dim[2]; index[2]++) { - array[index[perm[0]] + (index[perm[1]] + index[perm[2]]*dim[perm[1]])*dim[perm[0]]] = - static_cast(fieldData[(index[0]*dim[1] + index[1])*dim[2] + index[2]]); + array + [index[perm[0]] + + (index[perm[1]] + index[perm[2]] * dim[perm[1]]) + * dim[perm[0]]] + = static_cast( + fieldData + [(index[0] * dim[1] + index[1]) * dim[2] + index[2]] + ); } } } @@ -283,15 +386,20 @@ void writeMatlabField4( std::vector const& fieldData, mwSize dim0, mwSize dim1, mwSize dim2, mwSize dim3, std::vector perm ) { - if(fieldData.size() != dim0*dim1*dim2*dim3) - throw AmiException("Dimension mismatch when writing rdata->%s to matlab results!",fieldName); + if (fieldData.size() != dim0 * dim1 * dim2 * dim3) + throw AmiException( + "Dimension mismatch when writing rdata->%s to matlab results!", + fieldName + ); - if(perm.size() != 4) + if (perm.size() != 4) throw AmiException("Dimension mismatch when applying permutation!"); - std::vector dim = {(mwSize)(dim0), (mwSize)(dim1), (mwSize)(dim2), (mwSize)(dim3)}; + std::vector dim + = {(mwSize)(dim0), (mwSize)(dim1), (mwSize)(dim2), (mwSize)(dim3)}; - double *array = initAndAttachArray(matlabStruct, fieldName, reorder(dim,perm)); + double* array + = initAndAttachArray(matlabStruct, fieldName, reorder(dim, perm)); std::vector index = {0, 0, 0, 0}; /* transform rowmajor (c++) to colmajor (matlab) and apply permutation */ @@ -299,8 +407,19 @@ void writeMatlabField4( for (index[1] = 0; index[1] < dim[1]; index[1]++) { for (index[2] = 0; index[2] < dim[2]; index[2]++) { for (index[3] = 0; index[3] < dim[3]; index[3]++) { - array[index[perm[0]] + (index[perm[1]] + (index[perm[2]] + index[perm[3]]*dim[perm[2]])*dim[perm[1]])*dim[perm[0]]] = - static_cast(fieldData[((index[0]*dim[1] + index[1])*dim[2] + index[2])*dim[3] + index[3]]); + array + [index[perm[0]] + + (index[perm[1]] + + (index[perm[2]] + index[perm[3]] * dim[perm[2]]) + * dim[perm[1]]) + * dim[perm[0]]] + = static_cast( + fieldData + [((index[0] * dim[1] + index[1]) * dim[2] + + index[2]) + * dim[3] + + index[3]] + ); } } } @@ -310,29 +429,35 @@ void writeMatlabField4( double* initAndAttachArray( mxArray* matlabStruct, char const* fieldName, std::vector dim ) { - if(!mxIsStruct(matlabStruct)) - throw AmiException("Passing non-struct mxArray to initAndAttachArray!",fieldName); + if (!mxIsStruct(matlabStruct)) + throw AmiException( + "Passing non-struct mxArray to initAndAttachArray!", fieldName + ); int fieldNumber = mxGetFieldNumber(matlabStruct, fieldName); - if(fieldNumber<0) - throw AmiException("Trying to access non-existent field '%s'!",fieldName); + if (fieldNumber < 0) + throw AmiException( + "Trying to access non-existent field '%s'!", fieldName + ); - mxArray *array = mxCreateNumericArray(dim.size(), dim.data(), mxDOUBLE_CLASS, mxREAL); + mxArray* array + = mxCreateNumericArray(dim.size(), dim.data(), mxDOUBLE_CLASS, mxREAL); mxSetFieldByNumber(matlabStruct, 0, fieldNumber, array); - return(mxGetPr(array)); + return (mxGetPr(array)); } void checkFieldNames(char const** fieldNames, int const fieldCount) { - for (int ifield = 0; ifield -std::vector reorder(std::vector const& input, - std::vector const& order) { - if(order.size() != input.size()) +template +std::vector +reorder(std::vector const& input, std::vector const& order) { + if (order.size() != input.size()) throw AmiException("Input dimension mismatch!"); std::vector reordered; reordered.resize(input.size()); @@ -341,5 +466,4 @@ std::vector reorder(std::vector const& input, return reordered; } - } // namespace amici diff --git a/src/simulation_parameters.cpp b/src/simulation_parameters.cpp index 990e872dfd..fd2e607441 100644 --- a/src/simulation_parameters.cpp +++ b/src/simulation_parameters.cpp @@ -5,43 +5,51 @@ namespace amici { -bool operator==(const SimulationParameters &a, const SimulationParameters &b) { - return is_equal(a.fixedParameters, b.fixedParameters) && - is_equal(a.fixedParametersPreequilibration, - b.fixedParametersPreequilibration) && - is_equal(a.fixedParametersPresimulation, - b.fixedParametersPresimulation) && - is_equal(a.parameters, b.parameters) && - (a.plist == b.plist) && - (a.pscale == b.pscale) && - (a.reinitializeFixedParameterInitialStates - == b.reinitializeFixedParameterInitialStates) && - is_equal(a.sx0, b.sx0) && - (a.t_presim == b.t_presim) && - (a.tstart_ == b.tstart_) && - (a.ts_ == b.ts_); +bool operator==(SimulationParameters const& a, SimulationParameters const& b) { + return is_equal(a.fixedParameters, b.fixedParameters) + && is_equal( + a.fixedParametersPreequilibration, + b.fixedParametersPreequilibration + ) + && is_equal( + a.fixedParametersPresimulation, b.fixedParametersPresimulation + ) + && is_equal(a.parameters, b.parameters) && (a.plist == b.plist) + && (a.pscale == b.pscale) + && (a.reinitializeFixedParameterInitialStates + == b.reinitializeFixedParameterInitialStates) + && is_equal(a.sx0, b.sx0) && (a.t_presim == b.t_presim) + && (a.tstart_ == b.tstart_) && (a.ts_ == b.ts_); } -void SimulationParameters::reinitializeAllFixedParameterDependentInitialStatesForPresimulation(int nx_rdata) -{ +void SimulationParameters:: + reinitializeAllFixedParameterDependentInitialStatesForPresimulation( + int nx_rdata + ) { reinitialization_state_idxs_presim.resize(nx_rdata); - std::iota(reinitialization_state_idxs_presim.begin(), - reinitialization_state_idxs_presim.end(), 0); - + std::iota( + reinitialization_state_idxs_presim.begin(), + reinitialization_state_idxs_presim.end(), 0 + ); } -void SimulationParameters::reinitializeAllFixedParameterDependentInitialStatesForSimulation(int nx_rdata) -{ +void SimulationParameters:: + reinitializeAllFixedParameterDependentInitialStatesForSimulation( + int nx_rdata + ) { reinitialization_state_idxs_sim.resize(nx_rdata); - std::iota(reinitialization_state_idxs_sim.begin(), - reinitialization_state_idxs_sim.end(), 0); + std::iota( + reinitialization_state_idxs_sim.begin(), + reinitialization_state_idxs_sim.end(), 0 + ); } -void SimulationParameters::reinitializeAllFixedParameterDependentInitialStates(int nx_rdata) -{ - reinitializeAllFixedParameterDependentInitialStatesForPresimulation(nx_rdata); +void SimulationParameters::reinitializeAllFixedParameterDependentInitialStates( + int nx_rdata +) { + reinitializeAllFixedParameterDependentInitialStatesForPresimulation(nx_rdata + ); reinitializeAllFixedParameterDependentInitialStatesForSimulation(nx_rdata); } - } // namespace amici diff --git a/src/solver.cpp b/src/solver.cpp index c79e9e1097..56bed2a1a3 100644 --- a/src/solver.cpp +++ b/src/solver.cpp @@ -4,7 +4,6 @@ #include "amici/model.h" #include "amici/symbolic_functions.h" - #include #include #include @@ -12,49 +11,73 @@ namespace amici { -Solver::Solver(const Solver &other) - : ism_(other.ism_), lmm_(other.lmm_), iter_(other.iter_), - interp_type_(other.interp_type_), maxsteps_(other.maxsteps_), - maxtime_(other.maxtime_), simulation_timer_(other.simulation_timer_), - sensi_meth_(other.sensi_meth_), sensi_meth_preeq_(other.sensi_meth_preeq_), - stldet_(other.stldet_), ordering_(other.ordering_), - newton_maxsteps_(other.newton_maxsteps_), - newton_damping_factor_mode_(other.newton_damping_factor_mode_), - newton_damping_factor_lower_bound_(other.newton_damping_factor_lower_bound_), - linsol_(other.linsol_), atol_(other.atol_), rtol_(other.rtol_), - atol_fsa_(other.atol_fsa_), rtol_fsa_(other.rtol_fsa_), - atolB_(other.atolB_), rtolB_(other.rtolB_), quad_atol_(other.quad_atol_), - quad_rtol_(other.quad_rtol_), ss_tol_factor_(other.ss_tol_factor_), - ss_atol_(other.ss_atol_), ss_rtol_(other.ss_rtol_), - ss_tol_sensi_factor_(other.ss_tol_sensi_factor_), - ss_atol_sensi_(other.ss_atol_sensi_), - ss_rtol_sensi_(other.ss_rtol_sensi_), rdata_mode_(other.rdata_mode_), - newton_step_steadystate_conv_(other.newton_step_steadystate_conv_), - check_sensi_steadystate_conv_(other.check_sensi_steadystate_conv_), - maxstepsB_(other.maxstepsB_), sensi_(other.sensi_) -{} +Solver::Solver(Solver const& other) + : ism_(other.ism_) + , lmm_(other.lmm_) + , iter_(other.iter_) + , interp_type_(other.interp_type_) + , maxsteps_(other.maxsteps_) + , maxtime_(other.maxtime_) + , simulation_timer_(other.simulation_timer_) + , sensi_meth_(other.sensi_meth_) + , sensi_meth_preeq_(other.sensi_meth_preeq_) + , stldet_(other.stldet_) + , ordering_(other.ordering_) + , newton_maxsteps_(other.newton_maxsteps_) + , newton_damping_factor_mode_(other.newton_damping_factor_mode_) + , newton_damping_factor_lower_bound_( + other.newton_damping_factor_lower_bound_ + ) + , linsol_(other.linsol_) + , atol_(other.atol_) + , rtol_(other.rtol_) + , atol_fsa_(other.atol_fsa_) + , rtol_fsa_(other.rtol_fsa_) + , atolB_(other.atolB_) + , rtolB_(other.rtolB_) + , quad_atol_(other.quad_atol_) + , quad_rtol_(other.quad_rtol_) + , ss_tol_factor_(other.ss_tol_factor_) + , ss_atol_(other.ss_atol_) + , ss_rtol_(other.ss_rtol_) + , ss_tol_sensi_factor_(other.ss_tol_sensi_factor_) + , ss_atol_sensi_(other.ss_atol_sensi_) + , ss_rtol_sensi_(other.ss_rtol_sensi_) + , rdata_mode_(other.rdata_mode_) + , newton_step_steadystate_conv_(other.newton_step_steadystate_conv_) + , check_sensi_steadystate_conv_(other.check_sensi_steadystate_conv_) + , maxstepsB_(other.maxstepsB_) + , sensi_(other.sensi_) {} void Solver::apply_max_num_steps() const { - // set remaining steps, setMaxNumSteps only applies to a single call of solve + // set remaining steps, setMaxNumSteps only applies to a single call of + // solve long int cursteps; getNumSteps(solver_memory_.get(), &cursteps); if (maxsteps_ <= cursteps) - throw AmiException("Reached maximum number of steps %ld before reaching " - "tout at t=%g.", maxsteps_, t_); + throw AmiException( + "Reached maximum number of steps %ld before reaching " + "tout at t=%g.", + maxsteps_, t_ + ); setMaxNumSteps(maxsteps_ - cursteps); } void Solver::apply_max_num_steps_B() const { - // set remaining steps, setMaxNumSteps only applies to a single call of solve + // set remaining steps, setMaxNumSteps only applies to a single call of + // solve long int curstepsB; auto maxstepsB = (maxstepsB_ == 0) ? maxsteps_ * 100 : maxstepsB_; for (int i_mem_b = 0; i_mem_b < (int)solver_memory_B_.size(); ++i_mem_b) { if (solver_memory_B_.at(i_mem_b)) { getNumSteps(solver_memory_B_.at(i_mem_b).get(), &curstepsB); if (maxstepsB <= curstepsB) - throw AmiException("Reached maximum number of steps %ld before " - "reaching tout at t=%g in backward " - "problem %i.", maxstepsB_, t_, i_mem_b); + throw AmiException( + "Reached maximum number of steps %ld before " + "reaching tout at t=%g in backward " + "problem %i.", + maxstepsB_, t_, i_mem_b + ); setMaxNumStepsB(i_mem_b, maxstepsB - curstepsB); } } @@ -106,13 +129,15 @@ void Solver::runB(const realtype tout) const { t_ = tout; } -void Solver::setup(const realtype t0, Model *model, const AmiVector &x0, - const AmiVector &dx0, const AmiVectorArray &sx0, - const AmiVectorArray &sdx0) const { - if (nx() != model->nx_solver || nplist() != model->nplist() || - nquad() != model->nJ * model->nplist()) { - resetMutableMemory(model->nx_solver, model->nplist(), - model->nJ * model->nplist()); +void Solver::setup( + const realtype t0, Model* model, AmiVector const& x0, AmiVector const& dx0, + AmiVectorArray const& sx0, AmiVectorArray const& sdx0 +) const { + if (nx() != model->nx_solver || nplist() != model->nplist() + || nquad() != model->nJ * model->nplist()) { + resetMutableMemory( + model->nx_solver, model->nplist(), model->nJ * model->nplist() + ); } /* Create solver memory object if necessary */ allocateSolver(); @@ -144,8 +169,8 @@ void Solver::setup(const realtype t0, Model *model, const AmiVector &x0, initializeLinearSolver(model); initializeNonLinearSolver(); - if (sensi_ >= SensitivityOrder::first && - sensi_meth_ > SensitivityMethod::none && model->nx_solver > 0) { + if (sensi_ >= SensitivityOrder::first + && sensi_meth_ > SensitivityMethod::none && model->nx_solver > 0) { auto plist = model->getParameterList(); sensInit1(sx0, sdx0); if (sensi_meth_ == SensitivityMethod::forward && !plist.empty()) { @@ -172,12 +197,13 @@ void Solver::setup(const realtype t0, Model *model, const AmiVector &x0, cpu_timeB_ = 0.0; } -void Solver::setupB(int *which, const realtype tf, Model *model, - const AmiVector &xB0, const AmiVector &dxB0, - const AmiVector &xQB0) const { +void Solver::setupB( + int* which, const realtype tf, Model* model, AmiVector const& xB0, + AmiVector const& dxB0, AmiVector const& xQB0 +) const { if (!solver_memory_) - throw AmiException( - "Solver for the forward problem must be setup first"); + throw AmiException("Solver for the forward problem must be setup first" + ); /* allocate memory for the backward problem */ allocateSolverB(which); @@ -201,12 +227,12 @@ void Solver::setupB(int *which, const realtype tf, Model *model, applyQuadTolerancesASA(*which); setStabLimDetB(*which, stldet_); - } -void Solver::setupSteadystate(const realtype t0, Model *model, const AmiVector &x0, - const AmiVector &dx0, const AmiVector &xB0, - const AmiVector &dxB0, const AmiVector &xQ0) const { +void Solver::setupSteadystate( + const realtype t0, Model* model, AmiVector const& x0, AmiVector const& dx0, + AmiVector const& xB0, AmiVector const& dxB0, AmiVector const& xQ0 +) const { /* Initialize CVodes/IDAs solver with steadystate RHS function */ initSteadystate(t0, x0, dx0); @@ -219,20 +245,21 @@ void Solver::setupSteadystate(const realtype t0, Model *model, const AmiVector & /* Check linear solver (works only with KLU atm) */ if (linsol_ != LinearSolver::KLU) throw AmiException("Backward steady state computation via integration " - "is currently only implemented for KLU linear solver"); + "is currently only implemented for KLU linear solver" + ); /* Set Jacobian function and initialize values */ setSparseJacFn_ss(); model->writeSteadystateJB(t0, 0, x0, dx0, xB0, dxB0, xB0); } -void Solver::updateAndReinitStatesAndSensitivities(Model *model) const { +void Solver::updateAndReinitStatesAndSensitivities(Model* model) const { model->fx0_fixedParameters(x_); reInit(t_, x_, dx_); if (getSensitivityOrder() >= SensitivityOrder::first) { - model->fsx0_fixedParameters(sx_, x_); - if (getSensitivityMethod() == SensitivityMethod::forward) - sensReInit(sx_, sdx_); + model->fsx0_fixedParameters(sx_, x_); + if (getSensitivityMethod() == SensitivityMethod::forward) + sensReInit(sx_, sdx_); } } @@ -277,7 +304,7 @@ void Solver::storeDiagnosis() const { order_.push_back(number); } -void Solver::storeDiagnosisB(const int which) const { +void Solver::storeDiagnosisB(int const which) const { if (!solver_was_called_B_ || !solver_memory_B_.at(which)) { nsB_.push_back(0); nrhsB_.push_back(0); @@ -300,7 +327,7 @@ void Solver::storeDiagnosisB(const int which) const { nnlscfB_.push_back(gsl::narrow(number)); } -void Solver::initializeLinearSolver(const Model *model) const { +void Solver::initializeLinearSolver(Model const* model) const { switch (linsol_) { /* DIRECT SOLVERS */ @@ -312,8 +339,8 @@ void Solver::initializeLinearSolver(const Model *model) const { break; case LinearSolver::band: - linear_solver_ = - std::make_unique(x_, model->ubw, model->lbw); + linear_solver_ + = std::make_unique(x_, model->ubw, model->lbw); setLinearSolver(); setBandJacFn(); break; @@ -354,7 +381,8 @@ void Solver::initializeLinearSolver(const Model *model) const { case LinearSolver::KLU: linear_solver_ = std::make_unique( x_, model->nnz, CSC_MAT, - static_cast(getStateOrdering())); + static_cast(getStateOrdering()) + ); setLinearSolver(); setSparseJacFn(); break; @@ -364,37 +392,41 @@ void Solver::initializeLinearSolver(const Model *model) const { // TODO state ordering linearSolver = std::make_unique( *x, model->nnz, CSC_MAT, - static_cast(getStateOrdering())); + static_cast(getStateOrdering()) + ); setLinearSolver(); setSparseJacFn(); break; #endif default: - throw AmiException("Invalid choice of solver: %d", - static_cast(linsol_)); + throw AmiException( + "Invalid choice of solver: %d", static_cast(linsol_) + ); } } void Solver::initializeNonLinearSolver() const { switch (iter_) { case NonlinearSolverIteration::newton: - non_linear_solver_ = std::make_unique(x_.getNVector()); + non_linear_solver_ + = std::make_unique(x_.getNVector()); break; case NonlinearSolverIteration::fixedpoint: - non_linear_solver_ = - std::make_unique(x_.getNVector()); + non_linear_solver_ + = std::make_unique(x_.getNVector()); break; default: - throw AmiException("Invalid non-linear solver specified (%d).", - static_cast(iter_)); + throw AmiException( + "Invalid non-linear solver specified (%d).", static_cast(iter_) + ); } setNonLinearSolver(); } -void Solver::initializeLinearSolverB(const Model *model, - const int which) const { +void Solver::initializeLinearSolverB(Model const* model, int const which) + const { switch (linsol_) { /* DIRECT SOLVERS */ case LinearSolver::dense: @@ -404,8 +436,8 @@ void Solver::initializeLinearSolverB(const Model *model, break; case LinearSolver::band: - linear_solver_B_ = - std::make_unique(xB_, model->ubw, model->lbw); + linear_solver_B_ + = std::make_unique(xB_, model->ubw, model->lbw); setLinearSolverB(which); setBandJacFnB(which); break; @@ -446,7 +478,8 @@ void Solver::initializeLinearSolverB(const Model *model, case LinearSolver::KLU: linear_solver_B_ = std::make_unique( xB_, model->nnz, CSC_MAT, - static_cast(getStateOrdering())); + static_cast(getStateOrdering()) + ); setLinearSolverB(which); setSparseJacFnB(which); break; @@ -454,68 +487,74 @@ void Solver::initializeLinearSolverB(const Model *model, case LinearSolver::SuperLUMT: linearSolverB = std::make_unique( *xB, model->nnz, CSC_MAT, - static_cast(getStateOrdering())); + static_cast(getStateOrdering()) + ); setLinearSolverB(which); setSparseJacFnB(which); break; #endif default: - throw AmiException("Invalid choice of solver: %d", - static_cast(linsol_)); + throw AmiException( + "Invalid choice of solver: %d", static_cast(linsol_) + ); } } -void Solver::initializeNonLinearSolverB(const int which) const { +void Solver::initializeNonLinearSolverB(int const which) const { switch (iter_) { case NonlinearSolverIteration::newton: - non_linear_solver_B_ = - std::make_unique(xB_.getNVector()); + non_linear_solver_B_ + = std::make_unique(xB_.getNVector()); break; case NonlinearSolverIteration::fixedpoint: - non_linear_solver_B_ = - std::make_unique(xB_.getNVector()); + non_linear_solver_B_ + = std::make_unique(xB_.getNVector()); break; default: - throw AmiException("Invalid non-linear solver specified (%d).", - static_cast(iter_)); + throw AmiException( + "Invalid non-linear solver specified (%d).", static_cast(iter_) + ); } setNonLinearSolverB(which); } -bool operator==(const Solver &a, const Solver &b) { +bool operator==(Solver const& a, Solver const& b) { if (typeid(a) != typeid(b)) return false; - return (a.interp_type_ == b.interp_type_) && (a.lmm_ == b.lmm_) && - (a.iter_ == b.iter_) && (a.stldet_ == b.stldet_) && - (a.ordering_ == b.ordering_) && - (a.newton_maxsteps_ == b.newton_maxsteps_) && - (a.newton_damping_factor_mode_ == b.newton_damping_factor_mode_) && - (a.newton_damping_factor_lower_bound_ == b.newton_damping_factor_lower_bound_) && - (a.ism_ == b.ism_) && - (a.linsol_ == b.linsol_) && (a.atol_ == b.atol_) && (a.rtol_ == b.rtol_) && - (a.maxsteps_ == b.maxsteps_) && (a.maxstepsB_ == b.maxstepsB_) && - (a.quad_atol_ == b.quad_atol_) && (a.quad_rtol_ == b.quad_rtol_) && - (a.maxtime_ == b.maxtime_) && - (a.getAbsoluteToleranceSteadyState() == - b.getAbsoluteToleranceSteadyState()) && - (a.getRelativeToleranceSteadyState() == - b.getRelativeToleranceSteadyState()) && - (a.getAbsoluteToleranceSteadyStateSensi() == - b.getAbsoluteToleranceSteadyStateSensi()) && - (a.getRelativeToleranceSteadyStateSensi() == - b.getRelativeToleranceSteadyStateSensi()) && - (a.rtol_fsa_ == b.rtol_fsa_ || - (isNaN(a.rtol_fsa_) && isNaN(b.rtol_fsa_))) && - (a.atol_fsa_ == b.atol_fsa_ || - (isNaN(a.atol_fsa_) && isNaN(b.atol_fsa_))) && - (a.rtolB_ == b.rtolB_ || (isNaN(a.rtolB_) && isNaN(b.rtolB_))) && - (a.atolB_ == b.atolB_ || (isNaN(a.atolB_) && isNaN(b.atolB_))) && - (a.sensi_ == b.sensi_) && (a.sensi_meth_ == b.sensi_meth_) && - (a.newton_step_steadystate_conv_ == b.newton_step_steadystate_conv_) && - (a.check_sensi_steadystate_conv_ == b.check_sensi_steadystate_conv_) && - (a.rdata_mode_ == b.rdata_mode_); + return (a.interp_type_ == b.interp_type_) && (a.lmm_ == b.lmm_) + && (a.iter_ == b.iter_) && (a.stldet_ == b.stldet_) + && (a.ordering_ == b.ordering_) + && (a.newton_maxsteps_ == b.newton_maxsteps_) + && (a.newton_damping_factor_mode_ == b.newton_damping_factor_mode_) + && (a.newton_damping_factor_lower_bound_ + == b.newton_damping_factor_lower_bound_) + && (a.ism_ == b.ism_) && (a.linsol_ == b.linsol_) + && (a.atol_ == b.atol_) && (a.rtol_ == b.rtol_) + && (a.maxsteps_ == b.maxsteps_) && (a.maxstepsB_ == b.maxstepsB_) + && (a.quad_atol_ == b.quad_atol_) && (a.quad_rtol_ == b.quad_rtol_) + && (a.maxtime_ == b.maxtime_) + && (a.getAbsoluteToleranceSteadyState() + == b.getAbsoluteToleranceSteadyState()) + && (a.getRelativeToleranceSteadyState() + == b.getRelativeToleranceSteadyState()) + && (a.getAbsoluteToleranceSteadyStateSensi() + == b.getAbsoluteToleranceSteadyStateSensi()) + && (a.getRelativeToleranceSteadyStateSensi() + == b.getRelativeToleranceSteadyStateSensi()) + && (a.rtol_fsa_ == b.rtol_fsa_ + || (isNaN(a.rtol_fsa_) && isNaN(b.rtol_fsa_))) + && (a.atol_fsa_ == b.atol_fsa_ + || (isNaN(a.atol_fsa_) && isNaN(b.atol_fsa_))) + && (a.rtolB_ == b.rtolB_ || (isNaN(a.rtolB_) && isNaN(b.rtolB_))) + && (a.atolB_ == b.atolB_ || (isNaN(a.atolB_) && isNaN(b.atolB_))) + && (a.sensi_ == b.sensi_) && (a.sensi_meth_ == b.sensi_meth_) + && (a.newton_step_steadystate_conv_ + == b.newton_step_steadystate_conv_) + && (a.check_sensi_steadystate_conv_ + == b.check_sensi_steadystate_conv_) + && (a.rdata_mode_ == b.rdata_mode_); } void Solver::applyTolerances() const { @@ -541,7 +580,7 @@ void Solver::applyTolerancesFSA() const { } } -void Solver::applyTolerancesASA(const int which) const { +void Solver::applyTolerancesASA(int const which) const { if (!getAdjInitDone()) throw AmiException("Adjoint solver instance was not yet set up, the " "tolerances cannot be applied yet!"); @@ -553,7 +592,7 @@ void Solver::applyTolerancesASA(const int which) const { setSStolerancesB(which, getRelativeToleranceB(), getAbsoluteToleranceB()); } -void Solver::applyQuadTolerancesASA(const int which) const { +void Solver::applyQuadTolerancesASA(int const which) const { if (!getAdjInitDone()) throw AmiException("Adjoint solver instance was not yet set up, the " "tolerances cannot be applied yet!"); @@ -601,22 +640,27 @@ void Solver::applySensitivityTolerances() const { SensitivityMethod Solver::getSensitivityMethod() const { return sensi_meth_; } -SensitivityMethod Solver::getSensitivityMethodPreequilibration() const { return sensi_meth_preeq_; } +SensitivityMethod Solver::getSensitivityMethodPreequilibration() const { + return sensi_meth_preeq_; +} void Solver::setSensitivityMethod(const SensitivityMethod sensi_meth) { checkSensitivityMethod(sensi_meth, false); this->sensi_meth_ = sensi_meth; } -void Solver::setSensitivityMethodPreequilibration(const SensitivityMethod sensi_meth_preeq) { +void Solver::setSensitivityMethodPreequilibration( + const SensitivityMethod sensi_meth_preeq +) { checkSensitivityMethod(sensi_meth_preeq, true); sensi_meth_preeq_ = sensi_meth_preeq; } -void Solver::checkSensitivityMethod(const SensitivityMethod sensi_meth, - bool preequilibration) const { - if (rdata_mode_ == RDataReporting::residuals && - sensi_meth == SensitivityMethod::adjoint) +void Solver::checkSensitivityMethod( + const SensitivityMethod sensi_meth, bool preequilibration +) const { + if (rdata_mode_ == RDataReporting::residuals + && sensi_meth == SensitivityMethod::adjoint) throw AmiException("Adjoint Sensitivity Analysis is not compatible with" " only reporting residuals!"); if (!preequilibration && sensi_meth != sensi_meth_) @@ -625,22 +669,28 @@ void Solver::checkSensitivityMethod(const SensitivityMethod sensi_meth, int Solver::getNewtonMaxSteps() const { return newton_maxsteps_; } -void Solver::setNewtonMaxSteps(const int newton_maxsteps) { +void Solver::setNewtonMaxSteps(int const newton_maxsteps) { if (newton_maxsteps < 0) throw AmiException("newton_maxsteps must be a non-negative number"); newton_maxsteps_ = newton_maxsteps; } -NewtonDampingFactorMode Solver::getNewtonDampingFactorMode() const { return newton_damping_factor_mode_; } +NewtonDampingFactorMode Solver::getNewtonDampingFactorMode() const { + return newton_damping_factor_mode_; +} -void Solver::setNewtonDampingFactorMode(NewtonDampingFactorMode dampingFactorMode) { - newton_damping_factor_mode_ = dampingFactorMode; +void Solver::setNewtonDampingFactorMode( + NewtonDampingFactorMode dampingFactorMode +) { + newton_damping_factor_mode_ = dampingFactorMode; } -double Solver::getNewtonDampingFactorLowerBound() const { return newton_damping_factor_lower_bound_; } +double Solver::getNewtonDampingFactorLowerBound() const { + return newton_damping_factor_lower_bound_; +} void Solver::setNewtonDampingFactorLowerBound(double dampingFactorLowerBound) { - newton_damping_factor_lower_bound_ = dampingFactorLowerBound; + newton_damping_factor_lower_bound_ = dampingFactorLowerBound; } SensitivityOrder Solver::getSensitivityOrder() const { return sensi_; } @@ -658,7 +708,7 @@ double Solver::getRelativeTolerance() const { return static_cast(rtol_); } -void Solver::setRelativeTolerance(const double rtol) { +void Solver::setRelativeTolerance(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -690,7 +740,7 @@ double Solver::getRelativeToleranceFSA() const { return static_cast(isNaN(rtol_fsa_) ? rtol_ : rtol_fsa_); } -void Solver::setRelativeToleranceFSA(const double rtol) { +void Solver::setRelativeToleranceFSA(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -705,7 +755,7 @@ double Solver::getAbsoluteToleranceFSA() const { return static_cast(isNaN(atol_fsa_) ? atol_ : atol_fsa_); } -void Solver::setAbsoluteToleranceFSA(const double atol) { +void Solver::setAbsoluteToleranceFSA(double const atol) { if (atol < 0) throw AmiException("atol must be a non-negative number"); @@ -720,7 +770,7 @@ double Solver::getRelativeToleranceB() const { return static_cast(isNaN(rtolB_) ? rtol_ : rtolB_); } -void Solver::setRelativeToleranceB(const double rtol) { +void Solver::setRelativeToleranceB(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -735,7 +785,7 @@ double Solver::getAbsoluteToleranceB() const { return static_cast(isNaN(atolB_) ? atol_ : atolB_); } -void Solver::setAbsoluteToleranceB(const double atol) { +void Solver::setAbsoluteToleranceB(double const atol) { if (atol < 0) throw AmiException("atol must be a non-negative number"); @@ -750,7 +800,7 @@ double Solver::getRelativeToleranceQuadratures() const { return static_cast(quad_rtol_); } -void Solver::setRelativeToleranceQuadratures(const double rtol) { +void Solver::setRelativeToleranceQuadratures(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -768,7 +818,7 @@ double Solver::getAbsoluteToleranceQuadratures() const { return static_cast(quad_atol_); } -void Solver::setAbsoluteToleranceQuadratures(const double atol) { +void Solver::setAbsoluteToleranceQuadratures(double const atol) { if (atol < 0) throw AmiException("atol must be a non-negative number"); @@ -786,7 +836,7 @@ double Solver::getSteadyStateToleranceFactor() const { return static_cast(ss_tol_factor_); } -void Solver::setSteadyStateToleranceFactor(const double ss_tol_factor) { +void Solver::setSteadyStateToleranceFactor(double const ss_tol_factor) { if (ss_tol_factor < 0) throw AmiException("ss_tol_factor must be a non-negative number"); @@ -794,10 +844,12 @@ void Solver::setSteadyStateToleranceFactor(const double ss_tol_factor) { } double Solver::getRelativeToleranceSteadyState() const { - return static_cast(isNaN(ss_rtol_) ? rtol_ * ss_tol_factor_ : ss_rtol_); + return static_cast( + isNaN(ss_rtol_) ? rtol_ * ss_tol_factor_ : ss_rtol_ + ); } -void Solver::setRelativeToleranceSteadyState(const double rtol) { +void Solver::setRelativeToleranceSteadyState(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -805,10 +857,12 @@ void Solver::setRelativeToleranceSteadyState(const double rtol) { } double Solver::getAbsoluteToleranceSteadyState() const { - return static_cast(isNaN(ss_atol_) ? atol_ * ss_tol_factor_ : ss_atol_); + return static_cast( + isNaN(ss_atol_) ? atol_ * ss_tol_factor_ : ss_atol_ + ); } -void Solver::setAbsoluteToleranceSteadyState(const double atol) { +void Solver::setAbsoluteToleranceSteadyState(double const atol) { if (atol < 0) throw AmiException("atol must be a non-negative number"); @@ -819,7 +873,8 @@ double Solver::getSteadyStateSensiToleranceFactor() const { return static_cast(ss_tol_sensi_factor_); } -void Solver::setSteadyStateSensiToleranceFactor(const double ss_tol_sensi_factor) { +void Solver::setSteadyStateSensiToleranceFactor(double const ss_tol_sensi_factor +) { if (ss_tol_sensi_factor < 0) throw AmiException("ss_tol_sensi_factor must be a non-negative number"); @@ -827,10 +882,12 @@ void Solver::setSteadyStateSensiToleranceFactor(const double ss_tol_sensi_factor } double Solver::getRelativeToleranceSteadyStateSensi() const { - return static_cast(isNaN(ss_rtol_sensi_) ? rtol_ * ss_tol_sensi_factor_ : ss_rtol_sensi_); + return static_cast( + isNaN(ss_rtol_sensi_) ? rtol_ * ss_tol_sensi_factor_ : ss_rtol_sensi_ + ); } -void Solver::setRelativeToleranceSteadyStateSensi(const double rtol) { +void Solver::setRelativeToleranceSteadyStateSensi(double const rtol) { if (rtol < 0) throw AmiException("rtol must be a non-negative number"); @@ -838,10 +895,12 @@ void Solver::setRelativeToleranceSteadyStateSensi(const double rtol) { } double Solver::getAbsoluteToleranceSteadyStateSensi() const { - return static_cast(isNaN(ss_atol_sensi_) ? atol_ * ss_tol_sensi_factor_ : ss_atol_sensi_); + return static_cast( + isNaN(ss_atol_sensi_) ? atol_ * ss_tol_sensi_factor_ : ss_atol_sensi_ + ); } -void Solver::setAbsoluteToleranceSteadyStateSensi(const double atol) { +void Solver::setAbsoluteToleranceSteadyStateSensi(double const atol) { if (atol < 0) throw AmiException("atol must be a non-negative number"); @@ -852,22 +911,17 @@ long int Solver::getMaxSteps() const { return maxsteps_; } double Solver::getMaxTime() const { return maxtime_.count(); } -void Solver::setMaxTime(double maxtime) -{ +void Solver::setMaxTime(double maxtime) { maxtime_ = std::chrono::duration(maxtime); } -void Solver::startTimer() const -{ - simulation_timer_.reset(); -} +void Solver::startTimer() const { simulation_timer_.reset(); } -bool Solver::timeExceeded(int interval) const -{ +bool Solver::timeExceeded(int interval) const { static int eval_counter = 0; // 0 means infinite time - if(maxtime_.count() == 0) + if (maxtime_.count() == 0) return false; if (++eval_counter % interval) @@ -878,7 +932,7 @@ bool Solver::timeExceeded(int interval) const return std::chrono::duration(elapsed_s) > maxtime_; } -void Solver::setMaxSteps(const long int maxsteps) { +void Solver::setMaxSteps(long int const maxsteps) { if (maxsteps <= 0) throw AmiException("maxsteps must be a positive number"); @@ -889,7 +943,7 @@ void Solver::setMaxSteps(const long int maxsteps) { long int Solver::getMaxStepsBackwardProblem() const { return maxstepsB_; } -void Solver::setMaxStepsBackwardProblem(const long int maxsteps) { +void Solver::setMaxStepsBackwardProblem(long int const maxsteps) { if (maxsteps < 0) throw AmiException("maxsteps must be a non-negative number"); @@ -927,26 +981,26 @@ int Solver::getStateOrdering() const { return ordering_; } void Solver::setStateOrdering(int ordering) { ordering_ = ordering; if (solver_memory_ && linsol_ == LinearSolver::KLU) { - auto klu = dynamic_cast(linear_solver_.get()); + auto klu = dynamic_cast(linear_solver_.get()); klu->setOrdering(static_cast(ordering)); - klu = dynamic_cast(linear_solver_B_.get()); + klu = dynamic_cast(linear_solver_B_.get()); klu->setOrdering(static_cast(ordering)); } #ifdef SUNDIALS_SUPERLUMT if (solverMemory && linsol == LinearSolver::SuperLUMT) { - auto klu = dynamic_cast(linearSolver.get()); - klu->setOrdering( - static_cast(ordering)); - klu = dynamic_cast(linearSolverB.get()); - klu->setOrdering( - static_cast(ordering)); + auto klu = dynamic_cast(linearSolver.get()); + klu->setOrdering(static_cast(ordering + )); + klu = dynamic_cast(linearSolverB.get()); + klu->setOrdering(static_cast(ordering + )); } #endif } bool Solver::getStabilityLimitFlag() const { return stldet_; } -void Solver::setStabilityLimitFlag(const bool stldet) { +void Solver::setStabilityLimitFlag(bool const stldet) { stldet_ = stldet; if (solver_memory_) { setStabLimDet(stldet); @@ -979,29 +1033,31 @@ RDataReporting Solver::getReturnDataReportingMode() const { }; void Solver::setReturnDataReportingMode(RDataReporting rdrm) { - if (rdrm == RDataReporting::residuals && - sensi_meth_ == SensitivityMethod::adjoint) + if (rdrm == RDataReporting::residuals + && sensi_meth_ == SensitivityMethod::adjoint) throw AmiException("Adjoint Sensitivity Analysis cannot report " "residuals!"); rdata_mode_ = rdrm; } -void Solver::initializeNonLinearSolverSens(const Model *model) const { +void Solver::initializeNonLinearSolverSens(Model const* model) const { switch (iter_) { case NonlinearSolverIteration::newton: switch (ism_) { case InternalSensitivityMethod::staggered: case InternalSensitivityMethod::simultaneous: non_linear_solver_sens_ = std::make_unique( - 1 + model->nplist(), x_.getNVector()); + 1 + model->nplist(), x_.getNVector() + ); break; case InternalSensitivityMethod::staggered1: - non_linear_solver_sens_ = - std::make_unique(x_.getNVector()); + non_linear_solver_sens_ + = std::make_unique(x_.getNVector()); break; default: throw AmiException( - "Unsupported internal sensitivity method selected: %d", ism_); + "Unsupported internal sensitivity method selected: %d", ism_ + ); } break; case NonlinearSolverIteration::fixedpoint: @@ -1009,20 +1065,23 @@ void Solver::initializeNonLinearSolverSens(const Model *model) const { case InternalSensitivityMethod::staggered: case InternalSensitivityMethod::simultaneous: non_linear_solver_sens_ = std::make_unique( - 1 + model->nplist(), x_.getNVector()); + 1 + model->nplist(), x_.getNVector() + ); break; case InternalSensitivityMethod::staggered1: - non_linear_solver_sens_ = - std::make_unique(x_.getNVector()); + non_linear_solver_sens_ + = std::make_unique(x_.getNVector()); break; default: throw AmiException( - "Unsupported internal sensitivity method selected: %d", ism_); + "Unsupported internal sensitivity method selected: %d", ism_ + ); } break; default: - throw AmiException("Invalid non-linear solver specified (%d).", - static_cast(iter_)); + throw AmiException( + "Invalid non-linear solver specified (%d).", static_cast(iter_) + ); } setNonLinearSolverSens(); @@ -1040,14 +1099,14 @@ bool Solver::getSensInitDone() const { return sens_initialized_; } bool Solver::getAdjInitDone() const { return adj_initialized_; } -bool Solver::getInitDoneB(const int which) const { - return static_cast(initializedB_.size()) > which && - initializedB_.at(which); +bool Solver::getInitDoneB(int const which) const { + return static_cast(initializedB_.size()) > which + && initializedB_.at(which); } -bool Solver::getQuadInitDoneB(const int which) const { - return static_cast(initializedQB_.size()) > which && - initializedQB_.at(which); +bool Solver::getQuadInitDoneB(int const which) const { + return static_cast(initializedQB_.size()) > which + && initializedQB_.at(which); } bool Solver::getQuadInitDone() const { return quad_initialized_; } @@ -1058,13 +1117,13 @@ void Solver::setSensInitDone() const { sens_initialized_ = true; } void Solver::setAdjInitDone() const { adj_initialized_ = true; } -void Solver::setInitDoneB(const int which) const { +void Solver::setInitDoneB(int const which) const { if (which >= static_cast(initializedB_.size())) initializedB_.resize(which + 1, false); initializedB_.at(which) = true; } -void Solver::setQuadInitDoneB(const int which) const { +void Solver::setQuadInitDoneB(int const which) const { if (which >= static_cast(initializedQB_.size())) initializedQB_.resize(which + 1, false); initializedQB_.at(which) = true; @@ -1072,20 +1131,14 @@ void Solver::setQuadInitDoneB(const int which) const { void Solver::setQuadInitDone() const { quad_initialized_ = true; } -void Solver::switchForwardSensisOff() const { - sensToggleOff(); -} +void Solver::switchForwardSensisOff() const { sensToggleOff(); } -realtype Solver::getCpuTime() const { - return cpu_time_; -} +realtype Solver::getCpuTime() const { return cpu_time_; } -realtype Solver::getCpuTimeB() const { - return cpu_timeB_; -} +realtype Solver::getCpuTimeB() const { return cpu_timeB_; } -void Solver::resetMutableMemory(const int nx, const int nplist, - const int nquad) const { +void Solver::resetMutableMemory(int const nx, int const nplist, int const nquad) + const { solver_memory_ = nullptr; initialized_ = false; adj_initialized_ = false; @@ -1109,8 +1162,9 @@ void Solver::resetMutableMemory(const int nx, const int nplist, initializedQB_.clear(); } -void Solver::writeSolution(realtype *t, AmiVector &x, AmiVector &dx, - AmiVectorArray &sx, AmiVector &xQ) const { +void Solver::writeSolution( + realtype* t, AmiVector& x, AmiVector& dx, AmiVectorArray& sx, AmiVector& xQ +) const { *t = gett(); if (quad_initialized_) xQ.copy(getQuadrature(*t)); @@ -1120,15 +1174,16 @@ void Solver::writeSolution(realtype *t, AmiVector &x, AmiVector &dx, dx.copy(getDerivativeState(*t)); } -void Solver::writeSolutionB(realtype *t, AmiVector &xB, AmiVector &dxB, - AmiVector &xQB, const int which) const { +void Solver::writeSolutionB( + realtype* t, AmiVector& xB, AmiVector& dxB, AmiVector& xQB, int const which +) const { *t = gett(); xB.copy(getAdjointState(which, *t)); dxB.copy(getAdjointDerivativeState(which, *t)); xQB.copy(getAdjointQuadrature(which, *t)); } -const AmiVector &Solver::getState(const realtype t) const { +AmiVector const& Solver::getState(const realtype t) const { if (t == t_) return x_; @@ -1138,7 +1193,7 @@ const AmiVector &Solver::getState(const realtype t) const { return dky_; } -const AmiVector &Solver::getDerivativeState(const realtype t) const { +AmiVector const& Solver::getDerivativeState(const realtype t) const { if (t == t_) return dx_; @@ -1148,7 +1203,7 @@ const AmiVector &Solver::getDerivativeState(const realtype t) const { return dky_; } -const AmiVectorArray &Solver::getStateSensitivity(const realtype t) const { +AmiVectorArray const& Solver::getStateSensitivity(const realtype t) const { if (sens_initialized_ && solver_was_called_F_) { if (t == t_) { getSens(); @@ -1159,8 +1214,8 @@ const AmiVectorArray &Solver::getStateSensitivity(const realtype t) const { return sx_; } -const AmiVector &Solver::getAdjointState(const int which, - const realtype t) const { +AmiVector const& +Solver::getAdjointState(int const which, const realtype t) const { if (adj_initialized_) { if (solver_was_called_B_) { if (t == t_) { @@ -1175,8 +1230,8 @@ const AmiVector &Solver::getAdjointState(const int which, return dky_; } -const AmiVector &Solver::getAdjointDerivativeState(const int which, - const realtype t) const { +AmiVector const& +Solver::getAdjointDerivativeState(int const which, const realtype t) const { if (adj_initialized_) { if (solver_was_called_B_) { if (t == t_) { @@ -1191,8 +1246,8 @@ const AmiVector &Solver::getAdjointDerivativeState(const int which, return dky_; } -const AmiVector &Solver::getAdjointQuadrature(const int which, - const realtype t) const { +AmiVector const& +Solver::getAdjointQuadrature(int const which, const realtype t) const { if (adj_initialized_) { if (solver_was_called_B_) { if (t == t_) { @@ -1207,7 +1262,7 @@ const AmiVector &Solver::getAdjointQuadrature(const int which, return xQB_; } -const AmiVector &Solver::getQuadrature(realtype t) const { +AmiVector const& Solver::getQuadrature(realtype t) const { if (quad_initialized_) { if (solver_was_called_F_) { if (t == t_) { @@ -1222,16 +1277,19 @@ const AmiVector &Solver::getQuadrature(realtype t) const { return xQ_; } - realtype Solver::gett() const { return t_; } -void wrapErrHandlerFn(int error_code, const char *module, - const char *function, char *msg, void * eh_data) { +void wrapErrHandlerFn( + int error_code, char const* module, char const* function, char* msg, + void* eh_data +) { constexpr int BUF_SIZE = 250; char buffer[BUF_SIZE]; char buffid[BUF_SIZE]; - snprintf(buffer, BUF_SIZE, "AMICI ERROR: in module %s in function %s : %s ", module, - function, msg); + snprintf( + buffer, BUF_SIZE, "AMICI ERROR: in module %s in function %s : %s ", + module, function, msg + ); switch (error_code) { case 99: snprintf(buffid, BUF_SIZE, "%s:%s:WARNING", module, function); @@ -1266,12 +1324,11 @@ void wrapErrHandlerFn(int error_code, const char *module, break; } - - if(!eh_data) { + if (!eh_data) { throw std::runtime_error("eh_data unset"); } auto solver = static_cast(eh_data); - if(solver->logger) + if (solver->logger) solver->logger->log(LogSeverity::debug, buffid, buffer); } diff --git a/src/solver_cvodes.cpp b/src/solver_cvodes.cpp index 9903c2b514..7157302c9e 100644 --- a/src/solver_cvodes.cpp +++ b/src/solver_cvodes.cpp @@ -20,8 +20,9 @@ namespace amici { // Ensure AMICI options are in sync with Sundials options -static_assert((int)InternalSensitivityMethod::simultaneous == CV_SIMULTANEOUS, - ""); +static_assert( + (int)InternalSensitivityMethod::simultaneous == CV_SIMULTANEOUS, "" +); static_assert((int)InternalSensitivityMethod::staggered == CV_STAGGERED, ""); static_assert((int)InternalSensitivityMethod::staggered1 == CV_STAGGERED1, ""); @@ -33,68 +34,76 @@ static_assert((int)LinearMultistepMethod::BDF == CV_BDF, ""); static_assert(AMICI_ROOT_RETURN == CV_ROOT_RETURN, ""); - /* * The following static members are callback function to CVODES. * Their signatures must not be changes. */ -static int fxdot(realtype t, N_Vector x, N_Vector xdot, void *user_data); - -static int fJSparse(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, - void *user_data, N_Vector tmp1, N_Vector tmp2, - N_Vector tmp3); +static int fxdot(realtype t, N_Vector x, N_Vector xdot, void* user_data); -static int fJ(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, - void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); +static int fJSparse( + realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, void* user_data, + N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); -static int fJB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1B, - N_Vector tmp2B, N_Vector tmp3B); +static int +fJ(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, void* user_data, + N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); -static int fJSparseB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1B, - N_Vector tmp2B, N_Vector tmp3B); +static int +fJB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector tmp1B, N_Vector tmp2B, N_Vector tmp3B); -static int fJBand(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, - void *user_data, N_Vector tmp1, N_Vector tmp2, - N_Vector tmp3); +static int fJSparseB( + realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector tmp1B, N_Vector tmp2B, N_Vector tmp3B +); -static int fJBandB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1B, - N_Vector tmp2B, N_Vector tmp3B); +static int fJBand( + realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, void* user_data, + N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); -static int fJv(N_Vector v, N_Vector Jv, realtype t, N_Vector x, - N_Vector xdot, void *user_data, N_Vector tmp); +static int fJBandB( + realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector tmp1B, N_Vector tmp2B, N_Vector tmp3B +); -static int fJvB(N_Vector vB, N_Vector JvB, realtype t, N_Vector x, - N_Vector xB, N_Vector xBdot, void *user_data, - N_Vector tmpB); +static int +fJv(N_Vector v, N_Vector Jv, realtype t, N_Vector x, N_Vector xdot, + void* user_data, N_Vector tmp); -static int froot(realtype t, N_Vector x, realtype *root, void *user_data); +static int fJvB( + N_Vector vB, N_Vector JvB, realtype t, N_Vector x, N_Vector xB, + N_Vector xBdot, void* user_data, N_Vector tmpB +); -static int fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - void *user_data); +static int froot(realtype t, N_Vector x, realtype* root, void* user_data); -static int fqBdot(realtype t, N_Vector x, N_Vector xB, N_Vector qBdot, - void *user_data); +static int +fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, void* user_data); -static int fxBdot_ss(realtype t, N_Vector xB, N_Vector xBdot, void *user_data); +static int +fqBdot(realtype t, N_Vector x, N_Vector xB, N_Vector qBdot, void* user_data); -static int fqBdot_ss(realtype t, N_Vector xB, N_Vector qBdot, void *user_data); +static int fxBdot_ss(realtype t, N_Vector xB, N_Vector xBdot, void* user_data); -static int fJSparseB_ss(realtype t, N_Vector x, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1, - N_Vector tmp2, N_Vector tmp3); +static int fqBdot_ss(realtype t, N_Vector xB, N_Vector qBdot, void* user_data); -static int fsxdot(int Ns, realtype t, N_Vector x, N_Vector xdot, int ip, - N_Vector sx, N_Vector sxdot, void *user_data, - N_Vector tmp1, N_Vector tmp2); +static int fJSparseB_ss( + realtype t, N_Vector x, N_Vector xBdot, SUNMatrix JB, void* user_data, + N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); +static int fsxdot( + int Ns, realtype t, N_Vector x, N_Vector xdot, int ip, N_Vector sx, + N_Vector sxdot, void* user_data, N_Vector tmp1, N_Vector tmp2 +); /* Function implementations */ -void CVodeSolver::init(const realtype t0, const AmiVector &x0, - const AmiVector & /*dx0*/) const { +void CVodeSolver:: + init(const realtype t0, AmiVector const& x0, AmiVector const& /*dx0*/) + const { solver_was_called_F_ = false; force_reinit_postprocess_F_ = false; t_ = t0; @@ -110,8 +119,9 @@ void CVodeSolver::init(const realtype t0, const AmiVector &x0, throw CvodeException(status, "CVodeInit"); } -void CVodeSolver::initSteadystate(const realtype /*t0*/, const AmiVector &/*x0*/, - const AmiVector &/*dx0*/) const { +void CVodeSolver::initSteadystate( + const realtype /*t0*/, AmiVector const& /*x0*/, AmiVector const& /*dx0*/ +) const { // We need to set the steadystate rhs function. Sundials doesn't have this // in its public API, so we have to change it in the solver memory, // as re-calling init would unset solver settings. @@ -124,8 +134,8 @@ void CVodeSolver::initSteadystate(const realtype /*t0*/, const AmiVector &/*x0*/ CVodeSetNlsRhsFn(solver_memory_.get(), fxBdot_ss); } -void CVodeSolver::sensInit1(const AmiVectorArray &sx0, - const AmiVectorArray & /*sdx0*/) const { +void CVodeSolver:: + sensInit1(AmiVectorArray const& sx0, AmiVectorArray const& /*sdx0*/) const { int status = CV_SUCCESS; sx_ = sx0; if (getSensitivityMethod() == SensitivityMethod::forward && nplist() > 0) { @@ -133,12 +143,14 @@ void CVodeSolver::sensInit1(const AmiVectorArray &sx0, status = CVodeSensReInit( solver_memory_.get(), static_cast(getInternalSensitivityMethod()), - sx_.getNVectorArray()); + sx_.getNVectorArray() + ); } else { - status = - CVodeSensInit1(solver_memory_.get(), nplist(), - static_cast(getInternalSensitivityMethod()), - fsxdot, sx_.getNVectorArray()); + status = CVodeSensInit1( + solver_memory_.get(), nplist(), + static_cast(getInternalSensitivityMethod()), fsxdot, + sx_.getNVectorArray() + ); setSensInitDone(); } } @@ -146,32 +158,37 @@ void CVodeSolver::sensInit1(const AmiVectorArray &sx0, throw CvodeException(status, "CVodeSensInit1"); } -void CVodeSolver::binit(const int which, const realtype tf, - const AmiVector &xB0, - const AmiVector & /*dxB0*/) const { +void CVodeSolver::binit( + int const which, const realtype tf, AmiVector const& xB0, + AmiVector const& /*dxB0*/ +) const { solver_was_called_B_ = false; force_reinit_postprocess_B_ = false; xB_ = xB0; int status; if (getInitDoneB(which)) { - status = CVodeReInitB(solver_memory_.get(), which, tf, xB_.getNVector()); + status + = CVodeReInitB(solver_memory_.get(), which, tf, xB_.getNVector()); } else { - status = - CVodeInitB(solver_memory_.get(), which, fxBdot, tf, xB_.getNVector()); + status = CVodeInitB( + solver_memory_.get(), which, fxBdot, tf, xB_.getNVector() + ); setInitDoneB(which); } if (status != CV_SUCCESS) throw CvodeException(status, "CVodeInitB"); } -void CVodeSolver::qbinit(const int which, const AmiVector &xQB0) const { +void CVodeSolver::qbinit(int const which, AmiVector const& xQB0) const { xQB_ = xQB0; int status; if (getQuadInitDoneB(which)) { - status = CVodeQuadReInitB(solver_memory_.get(), which, xQB_.getNVector()); + status + = CVodeQuadReInitB(solver_memory_.get(), which, xQB_.getNVector()); } else { - status = - CVodeQuadInitB(solver_memory_.get(), which, fqBdot, xQB_.getNVector()); + status = CVodeQuadInitB( + solver_memory_.get(), which, fqBdot, xQB_.getNVector() + ); setQuadInitDoneB(which); } if (status != CV_SUCCESS) @@ -238,71 +255,76 @@ void CVodeSolver::setSparseJacFn_ss() const { throw CvodeException(status, "CVodeSetJacFn"); } -Solver *CVodeSolver::clone() const { return new CVodeSolver(*this); } +Solver* CVodeSolver::clone() const { return new CVodeSolver(*this); } void CVodeSolver::allocateSolver() const { if (!solver_memory_) - solver_memory_ = std::unique_ptr>( + solver_memory_ = std::unique_ptr>( CVodeCreate(static_cast(lmm_)), - [](void *ptr) { CVodeFree(&ptr); }); + [](void* ptr) { CVodeFree(&ptr); } + ); } -void CVodeSolver::setSStolerances(const double rtol, const double atol) const { +void CVodeSolver::setSStolerances(double const rtol, double const atol) const { int status = CVodeSStolerances(solver_memory_.get(), rtol, atol); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSStolerances"); } -void CVodeSolver::setSensSStolerances(const double rtol, - const double *atol) const { - int status = CVodeSensSStolerances(solver_memory_.get(), rtol, - const_cast(atol)); +void CVodeSolver::setSensSStolerances(double const rtol, double const* atol) + const { + int status = CVodeSensSStolerances( + solver_memory_.get(), rtol, const_cast(atol) + ); if (status != CV_SUCCESS) - throw CvodeException(status, "CVodeSensEEtolerances"); + throw CvodeException(status, "CVodeSensSStolerances"); } -void CVodeSolver::setSensErrCon(const bool error_corr) const { +void CVodeSolver::setSensErrCon(bool const error_corr) const { int status = CVodeSetSensErrCon(solver_memory_.get(), error_corr); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetSensErrCon"); } -void CVodeSolver::setQuadErrConB(const int which, const bool flag) const { +void CVodeSolver::setQuadErrConB(int const which, bool const flag) const { int status = CVodeSetQuadErrConB(solver_memory_.get(), which, flag); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetQuadErrConB"); } -void CVodeSolver::setQuadErrCon(const bool flag) const { +void CVodeSolver::setQuadErrCon(bool const flag) const { int status = CVodeSetQuadErrCon(solver_memory_.get(), flag); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetQuadErrCon"); } -void CVodeSolver::getRootInfo(int *rootsfound) const { +void CVodeSolver::getRootInfo(int* rootsfound) const { int status = CVodeGetRootInfo(solver_memory_.get(), rootsfound); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetRootInfo"); } void CVodeSolver::setLinearSolver() const { - int status = CVodeSetLinearSolver(solver_memory_.get(), linear_solver_->get(), - linear_solver_->getMatrix()); + int status = CVodeSetLinearSolver( + solver_memory_.get(), linear_solver_->get(), linear_solver_->getMatrix() + ); if (status != CV_SUCCESS) throw CvodeException(status, "setLinearSolver"); } void CVodeSolver::setLinearSolverB(int which) const { - int status = - CVodeSetLinearSolverB(solver_memory_.get(), which, linear_solver_B_->get(), - linear_solver_B_->getMatrix()); + int status = CVodeSetLinearSolverB( + solver_memory_.get(), which, linear_solver_B_->get(), + linear_solver_B_->getMatrix() + ); if (status != CV_SUCCESS) throw CvodeException(status, "setLinearSolverB"); } void CVodeSolver::setNonLinearSolver() const { - int status = - CVodeSetNonlinearSolver(solver_memory_.get(), non_linear_solver_->get()); + int status = CVodeSetNonlinearSolver( + solver_memory_.get(), non_linear_solver_->get() + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetNonlinearSolver"); } @@ -317,80 +339,82 @@ void CVodeSolver::setNonLinearSolverSens() const { switch (ism_) { case InternalSensitivityMethod::staggered: - status = CVodeSetNonlinearSolverSensStg(solver_memory_.get(), - non_linear_solver_sens_->get()); + status = CVodeSetNonlinearSolverSensStg( + solver_memory_.get(), non_linear_solver_sens_->get() + ); break; case InternalSensitivityMethod::simultaneous: - status = CVodeSetNonlinearSolverSensSim(solver_memory_.get(), - non_linear_solver_sens_->get()); + status = CVodeSetNonlinearSolverSensSim( + solver_memory_.get(), non_linear_solver_sens_->get() + ); break; case InternalSensitivityMethod::staggered1: - status = CVodeSetNonlinearSolverSensStg1(solver_memory_.get(), - non_linear_solver_sens_->get()); + status = CVodeSetNonlinearSolverSensStg1( + solver_memory_.get(), non_linear_solver_sens_->get() + ); break; default: throw AmiException( - "Unsupported internal sensitivity method selected: %d", ism_); + "Unsupported internal sensitivity method selected: %d", ism_ + ); } if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSolver::setNonLinearSolverSens"); } -void CVodeSolver::setNonLinearSolverB(const int which) const { - int status = CVodeSetNonlinearSolverB(solver_memory_.get(), which, - non_linear_solver_B_->get()); +void CVodeSolver::setNonLinearSolverB(int const which) const { + int status = CVodeSetNonlinearSolverB( + solver_memory_.get(), which, non_linear_solver_B_->get() + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetNonlinearSolverB"); } void CVodeSolver::setErrHandlerFn() const { - int status = - CVodeSetErrHandlerFn(solver_memory_.get(), wrapErrHandlerFn, - reinterpret_cast( - const_cast(this))); + int status = CVodeSetErrHandlerFn( + solver_memory_.get(), wrapErrHandlerFn, + reinterpret_cast(const_cast(this)) + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetErrHandlerFn"); } void CVodeSolver::setUserData() const { - int status = CVodeSetUserData( - solver_memory_.get(), - &user_data - ); + int status = CVodeSetUserData(solver_memory_.get(), &user_data); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetUserData"); } -void CVodeSolver::setUserDataB(const int which) const { +void CVodeSolver::setUserDataB(int const which) const { int status = CVodeSetUserDataB(solver_memory_.get(), which, &user_data); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetUserDataB"); } -void CVodeSolver::setMaxNumSteps(const long mxsteps) const { +void CVodeSolver::setMaxNumSteps(long const mxsteps) const { int status = CVodeSetMaxNumSteps(solver_memory_.get(), mxsteps); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetMaxNumSteps"); } -void CVodeSolver::setStabLimDet(const int stldet) const { +void CVodeSolver::setStabLimDet(int const stldet) const { int status = CVodeSetStabLimDet(solver_memory_.get(), stldet); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetStabLimDet"); } -void CVodeSolver::setStabLimDetB(const int which, const int stldet) const { +void CVodeSolver::setStabLimDetB(int const which, int const stldet) const { int status = CVodeSetStabLimDetB(solver_memory_.get(), which, stldet); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetStabLimDetB"); } -void CVodeSolver::setId(const Model * /*model*/) const {} +void CVodeSolver::setId(Model const* /*model*/) const {} -void CVodeSolver::setSuppressAlg(const bool /*flag*/) const {} +void CVodeSolver::setSuppressAlg(bool const /*flag*/) const {} -void CVodeSolver::resetState(void *ami_mem, const_N_Vector y0) const { +void CVodeSolver::resetState(void* ami_mem, const_N_Vector y0) const { auto cv_mem = static_cast(ami_mem); /* here we force the order in the next step to zero, and update the @@ -441,16 +465,18 @@ void CVodeSolver::reInitPostProcessB(const realtype tnext) const { // store current backward problem in ca_mem to make it accessible in // adjoint rhs wrapper functions ca_mem->ca_bckpbCrt = cvB_mem; - reInitPostProcess(static_cast(cvB_mem->cv_mem), &tBret, &xB_, - tnext); + reInitPostProcess( + static_cast(cvB_mem->cv_mem), &tBret, &xB_, tnext + ); cvB_mem->cv_tout = tBret; cvB_mem = cvB_mem->cv_next; } force_reinit_postprocess_B_ = false; } -void CVodeSolver::reInitPostProcess(void *ami_mem, realtype *t, AmiVector *yout, - const realtype tout) const { +void CVodeSolver::reInitPostProcess( + void* ami_mem, realtype* t, AmiVector* yout, const realtype tout +) const { auto cv_mem = static_cast(ami_mem); auto nst_tmp = cv_mem->cv_nst; cv_mem->cv_nst = 0; @@ -462,10 +488,13 @@ void CVodeSolver::reInitPostProcess(void *ami_mem, realtype *t, AmiVector *yout, status = CVode(ami_mem, tout, yout->getNVector(), t, CV_ONE_STEP); if (status == CV_ROOT_RETURN) - throw CvodeException(status, "CVode returned a root after " + throw CvodeException( + status, + "CVode returned a root after " "reinitialization. The initial step-size after the event or " "heaviside function is too small. To fix this, increase absolute " - "and relative tolerances!"); + "and relative tolerances!" + ); if (status != CV_SUCCESS) throw CvodeException(status, "reInitPostProcess"); @@ -496,8 +525,9 @@ void CVodeSolver::reInitPostProcess(void *ami_mem, realtype *t, AmiVector *yout, } } -void CVodeSolver::reInit(const realtype t0, const AmiVector &yy0, - const AmiVector & /*yp0*/) const { +void CVodeSolver:: + reInit(const realtype t0, AmiVector const& yy0, AmiVector const& /*yp0*/) + const { auto cv_mem = static_cast(solver_memory_.get()); cv_mem->cv_tn = t0; if (solver_was_called_F_) @@ -506,8 +536,9 @@ void CVodeSolver::reInit(const realtype t0, const AmiVector &yy0, resetState(cv_mem, x_.getNVector()); } -void CVodeSolver::sensReInit(const AmiVectorArray &yyS0, - const AmiVectorArray & /*ypS0*/) const { +void CVodeSolver:: + sensReInit(AmiVectorArray const& yyS0, AmiVectorArray const& /*ypS0*/) + const { auto cv_mem = static_cast(solver_memory_.get()); /* Initialize znS[0] in the history array */ for (int is = 0; is < nplist(); is++) @@ -515,17 +546,20 @@ void CVodeSolver::sensReInit(const AmiVectorArray &yyS0, if (solver_was_called_F_) force_reinit_postprocess_F_ = true; sx_.copy(yyS0); - int status = N_VScaleVectorArray(nplist(), cv_mem->cv_cvals, - sx_.getNVectorArray(), cv_mem->cv_znS[0]); + int status = N_VScaleVectorArray( + nplist(), cv_mem->cv_cvals, sx_.getNVectorArray(), cv_mem->cv_znS[0] + ); if (status != CV_SUCCESS) throw CvodeException(CV_VECTOROP_ERR, "CVodeSensReInit"); } -void CVodeSolver::reInitB(const int which, const realtype tB0, - const AmiVector &yyB0, - const AmiVector & /*ypB0*/) const { - auto cv_memB = - static_cast(CVodeGetAdjCVodeBmem(solver_memory_.get(), which)); +void CVodeSolver::reInitB( + int const which, const realtype tB0, AmiVector const& yyB0, + AmiVector const& /*ypB0*/ +) const { + auto cv_memB = static_cast( + CVodeGetAdjCVodeBmem(solver_memory_.get(), which) + ); if (solver_was_called_B_) force_reinit_postprocess_B_ = true; cv_memB->cv_tn = tB0; @@ -542,20 +576,23 @@ void CVodeSolver::sensToggleOff() const { sens_initialized_ = false; } -void CVodeSolver::quadReInitB(int which, const AmiVector &yQB0) const { - auto cv_memB = - static_cast(CVodeGetAdjCVodeBmem(solver_memory_.get(), which)); +void CVodeSolver::quadReInitB(int which, AmiVector const& yQB0) const { + auto cv_memB = static_cast( + CVodeGetAdjCVodeBmem(solver_memory_.get(), which) + ); if (solver_was_called_B_) force_reinit_postprocess_B_ = true; xQB_.copy(yQB0); N_VScale(ONE, xQB_.getNVector(), cv_memB->cv_znQ[0]); } -void CVodeSolver::setSensParams(const realtype *p, const realtype *pbar, - const int *plist) const { +void CVodeSolver::setSensParams( + realtype const* p, realtype const* pbar, int const* plist +) const { int status = CVodeSetSensParams( - solver_memory_.get(), const_cast(p), - const_cast(pbar), const_cast(plist)); + solver_memory_.get(), const_cast(p), + const_cast(pbar), const_cast(plist) + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetSensParams"); } @@ -568,52 +605,55 @@ void CVodeSolver::getDky(realtype t, int k) const { void CVodeSolver::getSens() const { realtype tDummy = 0; - int status = - CVodeGetSens(solver_memory_.get(), &tDummy, sx_.getNVectorArray()); + int status + = CVodeGetSens(solver_memory_.get(), &tDummy, sx_.getNVectorArray()); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetSens"); } -void CVodeSolver::getSensDky(const realtype t, const int k) const { - int status = - CVodeGetSensDky(solver_memory_.get(), t, k, sx_.getNVectorArray()); +void CVodeSolver::getSensDky(const realtype t, int const k) const { + int status + = CVodeGetSensDky(solver_memory_.get(), t, k, sx_.getNVectorArray()); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetSens"); } -void CVodeSolver::getDkyB(const realtype t, const int k, - const int which) const { - int status = CVodeGetDky(CVodeGetAdjCVodeBmem(solver_memory_.get(), which), t, - k, dky_.getNVector()); +void CVodeSolver::getDkyB(const realtype t, int const k, int const which) + const { + int status = CVodeGetDky( + CVodeGetAdjCVodeBmem(solver_memory_.get(), which), t, k, + dky_.getNVector() + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetDkyB"); } void CVodeSolver::getQuadB(int which) const { realtype tDummy = 0; - int status = - CVodeGetQuadB(solver_memory_.get(), which, &tDummy, xQB_.getNVector()); + int status = CVodeGetQuadB( + solver_memory_.get(), which, &tDummy, xQB_.getNVector() + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetQuadB"); } -void CVodeSolver::getQuad(realtype &t) const { +void CVodeSolver::getQuad(realtype& t) const { int status = CVodeGetQuad(solver_memory_.get(), &t, xQ_.getNVector()); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetQuad"); } -void CVodeSolver::getQuadDkyB(const realtype t, const int k, int which) const { - int status = - CVodeGetQuadDky(CVodeGetAdjCVodeBmem(solver_memory_.get(), which), t, k, - xQB_.getNVector()); +void CVodeSolver::getQuadDkyB(const realtype t, int const k, int which) const { + int status = CVodeGetQuadDky( + CVodeGetAdjCVodeBmem(solver_memory_.get(), which), t, k, + xQB_.getNVector() + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetQuadDkyB"); } -void CVodeSolver::getQuadDky(const realtype t, const int k) const { - int status = - CVodeGetQuadDky(solver_memory_.get(), t, k, xQ_.getNVector()); +void CVodeSolver::getQuadDky(const realtype t, int const k) const { + int status = CVodeGetQuadDky(solver_memory_.get(), t, k, xQ_.getNVector()); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetQuadDky"); } @@ -623,75 +663,86 @@ void CVodeSolver::adjInit() const { if (getAdjInitDone()) { status = CVodeAdjReInit(solver_memory_.get()); } else { - status = CVodeAdjInit(solver_memory_.get(), static_cast(maxsteps_), - static_cast(interp_type_)); + status = CVodeAdjInit( + solver_memory_.get(), static_cast(maxsteps_), + static_cast(interp_type_) + ); setAdjInitDone(); } if (status != CV_SUCCESS) throw CvodeException(status, "CVodeAdjInit"); } -void CVodeSolver::quadInit(const AmiVector &xQ0) const { +void CVodeSolver::quadInit(AmiVector const& xQ0) const { int status; xQ_.copy(xQ0); if (getQuadInitDone()) { - status = CVodeQuadReInit(solver_memory_.get(), - const_cast(xQ0.getNVector())); + status = CVodeQuadReInit( + solver_memory_.get(), const_cast(xQ0.getNVector()) + ); } else { - status = CVodeQuadInit(solver_memory_.get(), fqBdot_ss, xQ_.getNVector()); + status + = CVodeQuadInit(solver_memory_.get(), fqBdot_ss, xQ_.getNVector()); setQuadInitDone(); } if (status != CV_SUCCESS) throw CvodeException(status, "CVodeQuadInit"); } -void CVodeSolver::allocateSolverB(int *which) const { +void CVodeSolver::allocateSolverB(int* which) const { if (!solver_memory_B_.empty()) { *which = 0; return; } - int status = CVodeCreateB(solver_memory_.get(), static_cast(lmm_), which); + int status + = CVodeCreateB(solver_memory_.get(), static_cast(lmm_), which); if (*which + 1 > static_cast(solver_memory_B_.size())) solver_memory_B_.resize(*which + 1); - solver_memory_B_.at(*which) = - std::unique_ptr>( - getAdjBmem(solver_memory_.get(), *which), [](void * /*ptr*/) {}); + solver_memory_B_.at(*which) + = std::unique_ptr>( + getAdjBmem(solver_memory_.get(), *which), [](void* /*ptr*/) {} + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeCreateB"); } -void CVodeSolver::setSStolerancesB(const int which, const realtype relTolB, - const realtype absTolB) const { - int status = - CVodeSStolerancesB(solver_memory_.get(), which, relTolB, absTolB); +void CVodeSolver::setSStolerancesB( + int const which, const realtype relTolB, const realtype absTolB +) const { + int status + = CVodeSStolerancesB(solver_memory_.get(), which, relTolB, absTolB); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSStolerancesB"); } -void CVodeSolver::quadSStolerancesB(const int which, const realtype reltolQB, - const realtype abstolQB) const { - int status = - CVodeQuadSStolerancesB(solver_memory_.get(), which, reltolQB, abstolQB); +void CVodeSolver::quadSStolerancesB( + int const which, const realtype reltolQB, const realtype abstolQB +) const { + int status = CVodeQuadSStolerancesB( + solver_memory_.get(), which, reltolQB, abstolQB + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeQuadSStolerancesB"); } -void CVodeSolver::quadSStolerances(const realtype reltolQB, - const realtype abstolQB) const { - int status = - CVodeQuadSStolerances(solver_memory_.get(), reltolQB, abstolQB); +void CVodeSolver::quadSStolerances( + const realtype reltolQB, const realtype abstolQB +) const { + int status + = CVodeQuadSStolerances(solver_memory_.get(), reltolQB, abstolQB); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeQuadSStolerances"); } -void CVodeSolver::getB(const int which) const { +void CVodeSolver::getB(int const which) const { realtype tDummy = 0; - int status = CVodeGetB(solver_memory_.get(), which, &tDummy, xB_.getNVector()); + int status + = CVodeGetB(solver_memory_.get(), which, &tDummy, xB_.getNVector()); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetB"); } -int CVodeSolver::solve(const realtype tout, const int itask) const { +int CVodeSolver::solve(const realtype tout, int const itask) const { if (force_reinit_postprocess_F_) reInitPostProcessF(tout); int status = CVode(solver_memory_.get(), tout, x_.getNVector(), &t_, itask); @@ -701,19 +752,20 @@ int CVodeSolver::solve(const realtype tout, const int itask) const { return status; } -int CVodeSolver::solveF(const realtype tout, const int itask, - int *ncheckPtr) const { +int CVodeSolver::solveF(const realtype tout, int const itask, int* ncheckPtr) + const { if (force_reinit_postprocess_F_) reInitPostProcessF(tout); - int status = - CVodeF(solver_memory_.get(), tout, x_.getNVector(), &t_, itask, ncheckPtr); + int status = CVodeF( + solver_memory_.get(), tout, x_.getNVector(), &t_, itask, ncheckPtr + ); if (status < 0) // status > 0 is okay and is used for e.g. root return throw IntegrationFailure(status, t_); solver_was_called_F_ = true; return status; } -void CVodeSolver::solveB(const realtype tBout, const int itaskB) const { +void CVodeSolver::solveB(const realtype tBout, int const itaskB) const { if (force_reinit_postprocess_B_) reInitPostProcessB(tBout); int status = CVodeB(solver_memory_.get(), tBout, itaskB); @@ -722,7 +774,7 @@ void CVodeSolver::solveB(const realtype tBout, const int itaskB) const { solver_was_called_B_ = true; } -void CVodeSolver::setMaxNumStepsB(const int which, const long mxstepsB) const { +void CVodeSolver::setMaxNumStepsB(int const which, long const mxstepsB) const { int status = CVodeSetMaxNumStepsB(solver_memory_.get(), which, mxstepsB); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeSetMaxNumStepsB"); @@ -734,54 +786,58 @@ void CVodeSolver::diag() const { throw CvodeException(status, "CVDiag"); } -void CVodeSolver::diagB(const int which) const { +void CVodeSolver::diagB(int const which) const { int status = CVDiagB(solver_memory_.get(), which); if (status != CV_SUCCESS) throw CvodeException(status, "CVDiagB"); } -void CVodeSolver::getNumSteps(const void *ami_mem, long int *numsteps) const { - int status = CVodeGetNumSteps(const_cast(ami_mem), numsteps); +void CVodeSolver::getNumSteps(void const* ami_mem, long int* numsteps) const { + int status = CVodeGetNumSteps(const_cast(ami_mem), numsteps); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetNumSteps"); } -void CVodeSolver::getNumRhsEvals(const void *ami_mem, - long int *numrhsevals) const { - int status = CVodeGetNumRhsEvals(const_cast(ami_mem), numrhsevals); +void CVodeSolver::getNumRhsEvals(void const* ami_mem, long int* numrhsevals) + const { + int status = CVodeGetNumRhsEvals(const_cast(ami_mem), numrhsevals); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetNumRhsEvals"); } -void CVodeSolver::getNumErrTestFails(const void *ami_mem, - long int *numerrtestfails) const { - int status = - CVodeGetNumErrTestFails(const_cast(ami_mem), numerrtestfails); +void CVodeSolver::getNumErrTestFails( + void const* ami_mem, long int* numerrtestfails +) const { + int status + = CVodeGetNumErrTestFails(const_cast(ami_mem), numerrtestfails); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetNumErrTestFails"); } void CVodeSolver::getNumNonlinSolvConvFails( - const void *ami_mem, long int *numnonlinsolvconvfails) const { - int status = CVodeGetNumNonlinSolvConvFails(const_cast(ami_mem), - numnonlinsolvconvfails); + void const* ami_mem, long int* numnonlinsolvconvfails +) const { + int status = CVodeGetNumNonlinSolvConvFails( + const_cast(ami_mem), numnonlinsolvconvfails + ); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetNumNonlinSolvConvFails"); } -void CVodeSolver::getLastOrder(const void *ami_mem, int *order) const { - int status = CVodeGetLastOrder(const_cast(ami_mem), order); +void CVodeSolver::getLastOrder(void const* ami_mem, int* order) const { + int status = CVodeGetLastOrder(const_cast(ami_mem), order); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeGetLastOrder"); } -void *CVodeSolver::getAdjBmem(void *ami_mem, int which) const { +void* CVodeSolver::getAdjBmem(void* ami_mem, int which) const { return CVodeGetAdjCVodeBmem(ami_mem, which); } void CVodeSolver::calcIC(const realtype /*tout1*/) const {}; -void CVodeSolver::calcICB(const int /*which*/, const realtype /*tout1*/) const {}; +void CVodeSolver::calcICB(int const /*which*/, const realtype /*tout1*/) + const {}; void CVodeSolver::setStopTime(const realtype tstop) const { int status = CVodeSetStopTime(solver_memory_.get(), tstop); @@ -789,26 +845,23 @@ void CVodeSolver::setStopTime(const realtype tstop) const { throw CvodeException(status, "CVodeSetStopTime"); } - void CVodeSolver::turnOffRootFinding() const { int status = CVodeRootInit(solver_memory_.get(), 0, nullptr); if (status != CV_SUCCESS) throw CvodeException(status, "CVodeRootInit"); } - -const Model *CVodeSolver::getModel() const { +Model const* CVodeSolver::getModel() const { if (!solver_memory_) throw AmiException("Solver has not been allocated, information is not " "available"); auto cv_mem = static_cast(solver_memory_.get()); - auto typed_udata = static_cast(cv_mem->cv_user_data); + auto typed_udata = static_cast(cv_mem->cv_user_data); Expects(typed_udata); return typed_udata->first; } - /** * @brief Jacobian of xdot with respect to states x * @param t timepoint @@ -821,19 +874,18 @@ const Model *CVodeSolver::getModel() const { * @param tmp3 temporary storage vector * @return status flag indicating successful execution **/ -static int fJ(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, - void *user_data, N_Vector /*tmp1*/, N_Vector /*tmp2*/, - N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +static int +fJ(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, void* user_data, + N_Vector /*tmp1*/, N_Vector /*tmp2*/, N_Vector /*tmp3*/) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJ(t, x, xdot, J); return model->checkFinite(J, ModelQuantity::J, t); } - /** * @brief Jacobian of xBdot with respect to adjoint state xB * @param t timepoint @@ -847,19 +899,19 @@ static int fJ(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, * @param tmp3B temporary storage vector * @return status flag indicating successful execution **/ -static int fJB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector /*tmp1B*/, - N_Vector /*tmp2B*/, N_Vector /*tmp3B*/) { - auto typed_udata = static_cast(user_data); +static int +fJB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector /*tmp1B*/, N_Vector /*tmp2B*/, + N_Vector /*tmp3B*/) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJB(t, x, xB, xBdot, JB); return model->checkFinite(gsl::make_span(JB), ModelQuantity::JB); } - /** * @brief J in sparse form (for sparse solvers from the SuiteSparse Package) * @param t timepoint @@ -872,19 +924,19 @@ static int fJB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -static int fJSparse(realtype t, N_Vector x, N_Vector /*xdot*/, - SUNMatrix J, void *user_data, N_Vector /*tmp1*/, - N_Vector /*tmp2*/, N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +static int fJSparse( + realtype t, N_Vector x, N_Vector /*xdot*/, SUNMatrix J, void* user_data, + N_Vector /*tmp1*/, N_Vector /*tmp2*/, N_Vector /*tmp3*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparse(t, x, J); return model->checkFinite(J, ModelQuantity::J, t); } - /** * @brief JB in sparse form (for sparse solvers from the SuiteSparse Package) * @param t timepoint @@ -898,19 +950,19 @@ static int fJSparse(realtype t, N_Vector x, N_Vector /*xdot*/, * @param tmp3B temporary storage vector * @return status flag indicating successful execution */ -static int fJSparseB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector /*tmp1B*/, - N_Vector /*tmp2B*/, N_Vector /*tmp3B*/) { - auto typed_udata = static_cast(user_data); +static int fJSparseB( + realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector /*tmp1B*/, N_Vector /*tmp2B*/, N_Vector /*tmp3B*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparseB(t, x, xB, xBdot, JB); return model->checkFinite(gsl::make_span(JB), ModelQuantity::JB); } - /** * @brief J in banded form (for banded solvers) * @param t timepoint @@ -923,12 +975,13 @@ static int fJSparseB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -static int fJBand(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, - void *user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3) { +static int fJBand( + realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, void* user_data, + N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +) { return fJ(t, x, xdot, J, user_data, tmp1, tmp2, tmp3); } - /** * @brief JB in banded form (for banded solvers) * @param t timepoint @@ -942,13 +995,13 @@ static int fJBand(realtype t, N_Vector x, N_Vector xdot, SUNMatrix J, * @param tmp3B temporary storage vector * @return status flag indicating successful execution */ -static int fJBandB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1B, - N_Vector tmp2B, N_Vector tmp3B) { +static int fJBandB( + realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector tmp1B, N_Vector tmp2B, N_Vector tmp3B +) { return fJB(t, x, xB, xBdot, JB, user_data, tmp1B, tmp2B, tmp3B); } - /** * @brief Matrix vector product of J with a vector v (for iterative solvers) * @param t timepoint @@ -961,18 +1014,18 @@ static int fJBandB(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, * @param tmp temporary storage vector * @return status flag indicating successful execution **/ -static int fJv(N_Vector v, N_Vector Jv, realtype t, N_Vector x, - N_Vector /*xdot*/, void *user_data, N_Vector /*tmp*/) { - auto typed_udata = static_cast(user_data); +static int +fJv(N_Vector v, N_Vector Jv, realtype t, N_Vector x, N_Vector /*xdot*/, + void* user_data, N_Vector /*tmp*/) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJv(v, Jv, t, x); return model->checkFinite(gsl::make_span(Jv), ModelQuantity::Jv); } - /** * @brief Matrix vector product of JB with a vector v (for iterative solvers) * @param t timepoint @@ -986,19 +1039,19 @@ static int fJv(N_Vector v, N_Vector Jv, realtype t, N_Vector x, * @param tmpB temporary storage vector * @return status flag indicating successful execution **/ -static int fJvB(N_Vector vB, N_Vector JvB, realtype t, N_Vector x, - N_Vector xB, N_Vector /*xBdot*/, void *user_data, - N_Vector /*tmpB*/) { - auto typed_udata = static_cast(user_data); +static int fJvB( + N_Vector vB, N_Vector JvB, realtype t, N_Vector x, N_Vector xB, + N_Vector /*xBdot*/, void* user_data, N_Vector /*tmpB*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJvB(vB, JvB, t, x, xB); return model->checkFinite(gsl::make_span(JvB), ModelQuantity::JvB); } - /** * @brief Event trigger function for events * @param t timepoint @@ -1007,19 +1060,18 @@ static int fJvB(N_Vector vB, N_Vector JvB, realtype t, N_Vector x, * @param user_data object with user input * @return status flag indicating successful execution */ -static int froot(realtype t, N_Vector x, realtype *root, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int froot(realtype t, N_Vector x, realtype* root, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->froot(t, x, gsl::make_span(root, model->ne)); - return model->checkFinite(gsl::make_span(root, model->ne), - ModelQuantity::root); + return model->checkFinite( + gsl::make_span(root, model->ne), ModelQuantity::root + ); } - /** * @brief residual function of the ODE * @param t timepoint @@ -1028,12 +1080,12 @@ static int froot(realtype t, N_Vector x, realtype *root, * @param user_data object with user input * @return status flag indicating successful execution */ -static int fxdot(realtype t, N_Vector x, N_Vector xdot, void *user_data) { - auto typed_udata = static_cast(user_data); +static int fxdot(realtype t, N_Vector x, N_Vector xdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); - auto solver = dynamic_cast(typed_udata->second); + auto solver = dynamic_cast(typed_udata->second); Expects(model); if (solver->timeExceeded(500)) { @@ -1053,7 +1105,6 @@ static int fxdot(realtype t, N_Vector x, N_Vector xdot, void *user_data) { return model->checkFinite(gsl::make_span(xdot), ModelQuantity::xdot); } - /** * @brief Right hand side of differential equation for adjoint state xB * @param t timepoint @@ -1063,11 +1114,11 @@ static int fxdot(realtype t, N_Vector x, N_Vector xdot, void *user_data) { * @param user_data object with user input * @return status flag indicating successful execution */ -static int fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int +fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); auto solver = dynamic_cast(typed_udata->second); Expects(model); @@ -1080,7 +1131,6 @@ static int fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, return model->checkFinite(gsl::make_span(xBdot), ModelQuantity::xBdot); } - /** * @brief Right hand side of integral equation for quadrature states qB * @param t timepoint @@ -1090,18 +1140,17 @@ static int fxBdot(realtype t, N_Vector x, N_Vector xB, N_Vector xBdot, * @param user_data pointer to temp data object * @return status flag indicating successful execution */ -static int fqBdot(realtype t, N_Vector x, N_Vector xB, N_Vector qBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int +fqBdot(realtype t, N_Vector x, N_Vector xB, N_Vector qBdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fqBdot(t, x, xB, qBdot); return model->checkFinite(gsl::make_span(qBdot), ModelQuantity::qBdot); } - /** * @brief Right hand side of differential equation for adjoint state xB * when simulating in steadystate mode @@ -1111,18 +1160,16 @@ static int fqBdot(realtype t, N_Vector x, N_Vector xB, N_Vector qBdot, * @param user_data object with user input * @return status flag indicating successful execution */ -static int fxBdot_ss(realtype t, N_Vector xB, N_Vector xBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int fxBdot_ss(realtype t, N_Vector xB, N_Vector xBdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fxBdot_ss(t, xB, xBdot); return model->checkFinite(gsl::make_span(xBdot), ModelQuantity::xBdot_ss); } - /** * @brief Right hand side of integral equation for quadrature states qB * when simulating in steadystate mode @@ -1132,11 +1179,10 @@ static int fxBdot_ss(realtype t, N_Vector xB, N_Vector xBdot, * @param user_data pointer to temp data object * @return status flag indicating successful execution */ -static int fqBdot_ss(realtype t, N_Vector xB, N_Vector qBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int fqBdot_ss(realtype t, N_Vector xB, N_Vector qBdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fqBdot_ss(t, xB, qBdot); @@ -1155,20 +1201,21 @@ static int fqBdot_ss(realtype t, N_Vector xB, N_Vector qBdot, * @param tmp3B temporary storage vector * @return status flag indicating successful execution */ -static int fJSparseB_ss(realtype /*t*/, N_Vector /*x*/, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector /*tmp1*/, - N_Vector /*tmp2*/, N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +static int fJSparseB_ss( + realtype /*t*/, N_Vector /*x*/, N_Vector xBdot, SUNMatrix JB, + void* user_data, N_Vector /*tmp1*/, N_Vector /*tmp2*/, N_Vector /*tmp3*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparseB_ss(JB); - return model->checkFinite(gsl::make_span(xBdot), - ModelQuantity::JSparseB_ss); + return model->checkFinite( + gsl::make_span(xBdot), ModelQuantity::JSparseB_ss + ); } - /** * @brief Right hand side of differential equation for state sensitivities sx * @param Ns number of parameters @@ -1184,20 +1231,21 @@ static int fJSparseB_ss(realtype /*t*/, N_Vector /*x*/, N_Vector xBdot, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -static int fsxdot(int /*Ns*/, realtype t, N_Vector x, N_Vector /*xdot*/, - int ip, N_Vector sx, N_Vector sxdot, void *user_data, - N_Vector /*tmp1*/, N_Vector /*tmp2*/) { - auto typed_udata = static_cast(user_data); +static int fsxdot( + int /*Ns*/, realtype t, N_Vector x, N_Vector /*xdot*/, int ip, N_Vector sx, + N_Vector sxdot, void* user_data, N_Vector /*tmp1*/, N_Vector /*tmp2*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fsxdot(t, x, ip, sx, sxdot); return model->checkFinite(gsl::make_span(sxdot), ModelQuantity::sxdot); } -bool operator==(const CVodeSolver &a, const CVodeSolver &b) { - return static_cast(a) == static_cast(b); +bool operator==(CVodeSolver const& a, CVodeSolver const& b) { + return static_cast(a) == static_cast(b); } } // namespace amici diff --git a/src/solver_idas.cpp b/src/solver_idas.cpp index b68cb357e3..63f65b184d 100644 --- a/src/solver_idas.cpp +++ b/src/solver_idas.cpp @@ -21,93 +21,109 @@ namespace amici { * Their signatures must not be changes. */ -static int fxdot(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, - void *user_data); - -static int fJ(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xdot, SUNMatrix J, void *user_data, N_Vector tmp1, - N_Vector tmp2, N_Vector tmp3); - -static int fJSparse(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xdot, SUNMatrix J, void *user_data, - N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); - -static int fJB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector xBdot, SUNMatrix JB, - void *user_data, N_Vector tmp1B, N_Vector tmp2B, - N_Vector tmp3B); - -static int fJSparseB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector xBdot, - SUNMatrix JB, void *user_data, N_Vector tmp1B, - N_Vector tmp2B, N_Vector tmp3B); - -static int fJBand(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xdot, SUNMatrix J, void *user_data, - N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); - -static int fJBandB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector xBdot, SUNMatrix JB, - void *user_data, N_Vector tmp1B, N_Vector tmp2B, - N_Vector tmp3B); - -static int fJv(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, - N_Vector v, N_Vector Jv, realtype cj, void *user_data, - N_Vector tmp1, N_Vector tmp2); - -static int fJvB(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector xBdot, N_Vector vB, N_Vector JvB, - realtype cj, void *user_data, N_Vector tmpB1, - N_Vector tmpB2); - -static int froot(realtype t, N_Vector x, N_Vector dx, realtype *root, - void *user_data); - -static int fxBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector xBdot, void *user_data); - -static int fqBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector qBdot, void *user_data); - -static int fxBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector xBdot, - void *user_data); - -static int fqBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, - void *user_data); - -static int fJSparseB_ss(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xBdot, SUNMatrix JB, void *user_data, - N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); - -static int fsxdot(int Ns, realtype t, N_Vector x, N_Vector dx, - N_Vector xdot, N_Vector *sx, N_Vector *sdx, - N_Vector *sxdot, void *user_data, N_Vector tmp1, - N_Vector tmp2, N_Vector tmp3); - +static int +fxdot(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, void* user_data); + +static int +fJ(realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xdot, SUNMatrix J, + void* user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3); + +static int fJSparse( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xdot, + SUNMatrix J, void* user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); + +static int +fJB(realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, SUNMatrix JB, void* user_data, N_Vector tmp1B, + N_Vector tmp2B, N_Vector tmp3B); + +static int fJSparseB( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, SUNMatrix JB, void* user_data, N_Vector tmp1B, + N_Vector tmp2B, N_Vector tmp3B +); + +static int fJBand( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xdot, + SUNMatrix J, void* user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); + +static int fJBandB( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, SUNMatrix JB, void* user_data, N_Vector tmp1B, + N_Vector tmp2B, N_Vector tmp3B +); + +static int +fJv(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, N_Vector v, N_Vector Jv, + realtype cj, void* user_data, N_Vector tmp1, N_Vector tmp2); + +static int fJvB( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, N_Vector vB, N_Vector JvB, realtype cj, void* user_data, + N_Vector tmpB1, N_Vector tmpB2 +); + +static int +froot(realtype t, N_Vector x, N_Vector dx, realtype* root, void* user_data); + +static int fxBdot( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, void* user_data +); + +static int fqBdot( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector qBdot, void* user_data +); + +static int fxBdot_ss( + realtype t, N_Vector xB, N_Vector dxB, N_Vector xBdot, void* user_data +); + +static int fqBdot_ss( + realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, void* user_data +); + +static int fJSparseB_ss( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xBdot, + SUNMatrix JB, void* user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +); + +static int fsxdot( + int Ns, realtype t, N_Vector x, N_Vector dx, N_Vector xdot, N_Vector* sx, + N_Vector* sdx, N_Vector* sxdot, void* user_data, N_Vector tmp1, + N_Vector tmp2, N_Vector tmp3 +); /* Function implementations */ -void IDASolver::init(const realtype t0, const AmiVector &x0, - const AmiVector &dx0) const { +void IDASolver::init( + const realtype t0, AmiVector const& x0, AmiVector const& dx0 +) const { int status; solver_was_called_F_ = false; t_ = t0; x_ = x0; dx_ = dx0; if (getInitDone()) { - status = - IDAReInit(solver_memory_.get(), t_, x_.getNVector(), dx_.getNVector()); + status = IDAReInit( + solver_memory_.get(), t_, x_.getNVector(), dx_.getNVector() + ); } else { - status = IDAInit(solver_memory_.get(), fxdot, t_, x_.getNVector(), - dx_.getNVector()); + status = IDAInit( + solver_memory_.get(), fxdot, t_, x_.getNVector(), dx_.getNVector() + ); setInitDone(); } if (status != IDA_SUCCESS) throw IDAException(status, "IDAInit"); } -void IDASolver::initSteadystate(const realtype /*t0*/, const AmiVector &/*x0*/, - const AmiVector &/*dx0*/) const { +void IDASolver::initSteadystate( + const realtype /*t0*/, AmiVector const& /*x0*/, AmiVector const& /*dx0*/ +) const { /* We need to set the steadystate rhs function. SUndials doesn't have this in its public api, so we have to change it in the solver memory, as re-calling init would unset solver settings. */ @@ -115,22 +131,24 @@ void IDASolver::initSteadystate(const realtype /*t0*/, const AmiVector &/*x0*/, ida_mem->ida_res = fxBdot_ss; } -void IDASolver::sensInit1(const AmiVectorArray &sx0, - const AmiVectorArray &sdx0) const { +void IDASolver::sensInit1(AmiVectorArray const& sx0, AmiVectorArray const& sdx0) + const { int status = IDA_SUCCESS; sx_ = sx0; sdx_ = sdx0; if (getSensitivityMethod() == SensitivityMethod::forward && nplist() > 0) { if (getSensInitDone()) { - status = - IDASensReInit(solver_memory_.get(), - static_cast(getInternalSensitivityMethod()), - sx_.getNVectorArray(), sdx_.getNVectorArray()); + status = IDASensReInit( + solver_memory_.get(), + static_cast(getInternalSensitivityMethod()), + sx_.getNVectorArray(), sdx_.getNVectorArray() + ); } else { status = IDASensInit( solver_memory_.get(), nplist(), static_cast(getInternalSensitivityMethod()), fsxdot, - sx_.getNVectorArray(), sdx_.getNVectorArray()); + sx_.getNVectorArray(), sdx_.getNVectorArray() + ); setSensInitDone(); } } @@ -138,32 +156,38 @@ void IDASolver::sensInit1(const AmiVectorArray &sx0, throw IDAException(status, "IDASensInit"); } -void IDASolver::binit(const int which, const realtype tf, const AmiVector &xB0, - const AmiVector &dxB0) const { +void IDASolver::binit( + int const which, const realtype tf, AmiVector const& xB0, + AmiVector const& dxB0 +) const { int status; xB_ = xB0; dxB_ = dxB0; if (getInitDoneB(which)) - status = IDAReInitB(solver_memory_.get(), which, tf, xB_.getNVector(), - dxB_.getNVector()); + status = IDAReInitB( + solver_memory_.get(), which, tf, xB_.getNVector(), dxB_.getNVector() + ); else { - status = IDAInitB(solver_memory_.get(), which, fxBdot, tf, - xB_.getNVector(), dxB_.getNVector()); + status = IDAInitB( + solver_memory_.get(), which, fxBdot, tf, xB_.getNVector(), + dxB_.getNVector() + ); setInitDoneB(which); } if (status != IDA_SUCCESS) throw IDAException(status, "IDAInitB"); } -void IDASolver::qbinit(const int which, const AmiVector &xQB0) const { +void IDASolver::qbinit(int const which, AmiVector const& xQB0) const { int status; xQB_.copy(xQB0); if (getQuadInitDoneB(which)) status = IDAQuadReInitB(solver_memory_.get(), which, xQB_.getNVector()); else { - status = - IDAQuadInitB(solver_memory_.get(), which, fqBdot, xQB_.getNVector()); + status = IDAQuadInitB( + solver_memory_.get(), which, fqBdot, xQB_.getNVector() + ); setQuadInitDoneB(which); } if (status != IDA_SUCCESS) @@ -200,25 +224,25 @@ void IDASolver::setJacTimesVecFn() const { throw IDAException(status, "IDASpilsSetJacTimesVecFn"); } -void IDASolver::setDenseJacFnB(const int which) const { +void IDASolver::setDenseJacFnB(int const which) const { int status = IDASetJacFnB(solver_memory_.get(), which, fJB); if (status != IDA_SUCCESS) throw IDAException(status, "IDADlsSetDenseJacFnB"); } -void IDASolver::setSparseJacFnB(const int which) const { +void IDASolver::setSparseJacFnB(int const which) const { int status = IDASetJacFnB(solver_memory_.get(), which, fJSparseB); if (status != IDA_SUCCESS) throw IDAException(status, "IDASlsSetSparseJacFnB"); } -void IDASolver::setBandJacFnB(const int which) const { +void IDASolver::setBandJacFnB(int const which) const { int status = IDASetJacFnB(solver_memory_.get(), which, fJBandB); if (status != IDA_SUCCESS) throw IDAException(status, "IDADlsSetBandJacFnB"); } -void IDASolver::setJacTimesVecFnB(const int which) const { +void IDASolver::setJacTimesVecFnB(int const which) const { int status = IDASetJacTimesB(solver_memory_.get(), which, nullptr, fJvB); if (status != IDA_SUCCESS) throw IDAException(status, "IDASpilsSetJacTimesVecFnB"); @@ -230,57 +254,59 @@ void IDASolver::setSparseJacFn_ss() const { throw IDAException(status, "IDASetJacFn"); } -Solver *IDASolver::clone() const { return new IDASolver(*this); } +Solver* IDASolver::clone() const { return new IDASolver(*this); } void IDASolver::allocateSolver() const { if (!solver_memory_) - solver_memory_ = std::unique_ptr>( - IDACreate(), [](void *ptr) { IDAFree(&ptr); }); + solver_memory_ = std::unique_ptr>( + IDACreate(), [](void* ptr) { IDAFree(&ptr); } + ); } -void IDASolver::setSStolerances(const realtype rtol, - const realtype atol) const { +void IDASolver::setSStolerances(const realtype rtol, const realtype atol) + const { int status = IDASStolerances(solver_memory_.get(), rtol, atol); if (status != IDA_SUCCESS) throw IDAException(status, "IDASStolerances"); } -void IDASolver::setSensSStolerances(const realtype rtol, - const realtype *atol) const { - int status = IDASensSStolerances(solver_memory_.get(), rtol, - const_cast(atol)); +void IDASolver::setSensSStolerances(const realtype rtol, realtype const* atol) + const { + int status = IDASensSStolerances( + solver_memory_.get(), rtol, const_cast(atol) + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDASensEEtolerances"); } -void IDASolver::setSensErrCon(const bool error_corr) const { +void IDASolver::setSensErrCon(bool const error_corr) const { int status = IDASetSensErrCon(solver_memory_.get(), error_corr); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetSensErrCon"); } -void IDASolver::setQuadErrConB(const int which, const bool flag) const { +void IDASolver::setQuadErrConB(int const which, bool const flag) const { int status = IDASetQuadErrConB(solver_memory_.get(), which, flag); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetQuadErrConB"); } -void IDASolver::setQuadErrCon(const bool flag) const { +void IDASolver::setQuadErrCon(bool const flag) const { int status = IDASetQuadErrCon(solver_memory_.get(), flag); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetQuadErrCon"); } -void IDASolver::getRootInfo(int *rootsfound) const { +void IDASolver::getRootInfo(int* rootsfound) const { int status = IDAGetRootInfo(solver_memory_.get(), rootsfound); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetRootInfo"); } void IDASolver::setErrHandlerFn() const { - int status = - IDASetErrHandlerFn(solver_memory_.get(), wrapErrHandlerFn, - reinterpret_cast( - const_cast(this))); + int status = IDASetErrHandlerFn( + solver_memory_.get(), wrapErrHandlerFn, + reinterpret_cast(const_cast(this)) + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetErrHandlerFn"); } @@ -297,21 +323,22 @@ void IDASolver::setUserDataB(int which) const { throw IDAException(status, "IDASetUserDataB"); } -void IDASolver::setMaxNumSteps(const long int mxsteps) const { +void IDASolver::setMaxNumSteps(long int const mxsteps) const { int status = IDASetMaxNumSteps(solver_memory_.get(), mxsteps); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetMaxNumSteps"); } -void IDASolver::setStabLimDet(const int /*stldet*/) const {} +void IDASolver::setStabLimDet(int const /*stldet*/) const {} -void IDASolver::setStabLimDetB(const int /*which*/, - const int /*stldet*/) const {} +void IDASolver::setStabLimDetB(int const /*which*/, int const /*stldet*/) + const {} -void IDASolver::setId(const Model *model) const { +void IDASolver::setId(Model const* model) const { - N_Vector id = N_VMake_Serial(model->nx_solver, - const_cast(model->idlist.data())); + N_Vector id = N_VMake_Serial( + model->nx_solver, const_cast(model->idlist.data()) + ); int status = IDASetId(solver_memory_.get(), id); if (status != IDA_SUCCESS) @@ -320,14 +347,15 @@ void IDASolver::setId(const Model *model) const { N_VDestroy_Serial(id); } -void IDASolver::setSuppressAlg(const bool flag) const { +void IDASolver::setSuppressAlg(bool const flag) const { int status = IDASetSuppressAlg(solver_memory_.get(), flag); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetSuppressAlg"); } -void IDASolver::resetState(void *ami_mem, const_N_Vector yy0, - const_N_Vector yp0) const { +void IDASolver::resetState( + void* ami_mem, const_N_Vector yy0, const_N_Vector yp0 +) const { auto ida_mem = static_cast(ami_mem); /* here we force the order in the next step to zero, and update the @@ -343,7 +371,7 @@ void IDASolver::resetState(void *ami_mem, const_N_Vector yy0, /* Set step parameters */ /* current order */ - ida_mem->ida_kk = 0; + ida_mem->ida_kk = 0; } void IDASolver::reInitPostProcessF(const realtype tnext) const { @@ -360,32 +388,34 @@ void IDASolver::reInitPostProcessB(const realtype tnext) const { // store current backward problem in ca_mem to make it accessible in // adjoint rhs wrapper functions idaadj_mem->ia_bckpbCrt = idaB_mem; - reInitPostProcess(static_cast(idaB_mem->IDA_mem), &tBret, &xB_, - &dxB_, tnext); + reInitPostProcess( + static_cast(idaB_mem->IDA_mem), &tBret, &xB_, &dxB_, tnext + ); // idaB_mem->ida_tout = tBret; idaB_mem = idaB_mem->ida_next; } force_reinit_postprocess_B_ = false; } -void IDASolver::reInitPostProcess(void *ami_mem, realtype *t, - AmiVector *yout, AmiVector *ypout, - realtype tout) const { +void IDASolver::reInitPostProcess( + void* ami_mem, realtype* t, AmiVector* yout, AmiVector* ypout, realtype tout +) const { auto ida_mem = static_cast(ami_mem); auto nst_tmp = ida_mem->ida_nst; ida_mem->ida_nst = 0; auto status = IDASetStopTime(ida_mem, tout); - if(status != IDA_SUCCESS) + if (status != IDA_SUCCESS) throw IDAException(status, "CVodeSetStopTime"); - status = IDASolve(ami_mem, tout, t, yout->getNVector(), ypout->getNVector(), - IDA_ONE_STEP); + status = IDASolve( + ami_mem, tout, t, yout->getNVector(), ypout->getNVector(), IDA_ONE_STEP + ); - if(status != IDA_SUCCESS) + if (status != IDA_SUCCESS) throw IDAException(status, "reInitPostProcess"); - ida_mem->ida_nst = nst_tmp+1; + ida_mem->ida_nst = nst_tmp + 1; if (ida_mem->ida_adjMallocDone == SUNTRUE) { /* add new step to history array, this is copied from CVodeF */ auto ia_mem = ida_mem->ida_adj_mem; @@ -400,8 +430,9 @@ void IDASolver::reInitPostProcess(void *ami_mem, realtype *t, /* Load next point in dt_mem */ dt_mem[ida_mem->ida_nst % ia_mem->ia_nsteps]->t = *t; - ia_mem->ia_storePnt(ida_mem, - dt_mem[ida_mem->ida_nst % ia_mem->ia_nsteps]); + ia_mem->ia_storePnt( + ida_mem, dt_mem[ida_mem->ida_nst % ia_mem->ia_nsteps] + ); /* Set t1 field of the current ckeck point structure for the case in which there will be no future @@ -413,8 +444,9 @@ void IDASolver::reInitPostProcess(void *ami_mem, realtype *t, } } -void IDASolver::reInit(const realtype t0, const AmiVector &yy0, - const AmiVector &yp0) const { +void IDASolver::reInit( + const realtype t0, AmiVector const& yy0, AmiVector const& yp0 +) const { auto ida_mem = static_cast(solver_memory_.get()); ida_mem->ida_tn = t0; @@ -425,8 +457,9 @@ void IDASolver::reInit(const realtype t0, const AmiVector &yy0, resetState(ida_mem, x_.getNVector(), xB_.getNVector()); } -void IDASolver::sensReInit(const AmiVectorArray &yyS0, - const AmiVectorArray &ypS0) const { +void IDASolver::sensReInit( + AmiVectorArray const& yyS0, AmiVectorArray const& ypS0 +) const { auto ida_mem = static_cast(solver_memory_.get()); /* Initialize znS[0] in the history array */ for (int is = 0; is < nplist(); is++) @@ -435,13 +468,16 @@ void IDASolver::sensReInit(const AmiVectorArray &yyS0, force_reinit_postprocess_F_ = true; sx_.copy(yyS0); sdx_.copy(ypS0); - auto status = - N_VScaleVectorArray(nplist(), ida_mem->ida_cvals, sx_.getNVectorArray(), - ida_mem->ida_phiS[0]); + auto status = N_VScaleVectorArray( + nplist(), ida_mem->ida_cvals, sx_.getNVectorArray(), + ida_mem->ida_phiS[0] + ); if (status != IDA_SUCCESS) throw IDAException(IDA_VECTOROP_ERR, "IDASensReInit"); - status = N_VScaleVectorArray(nplist(), ida_mem->ida_cvals, - sdx_.getNVectorArray(), ida_mem->ida_phiS[1]); + status = N_VScaleVectorArray( + nplist(), ida_mem->ida_cvals, sdx_.getNVectorArray(), + ida_mem->ida_phiS[1] + ); if (status != IDA_SUCCESS) throw IDAException(IDA_VECTOROP_ERR, "IDASensReInit"); } @@ -455,11 +491,13 @@ void IDASolver::sensToggleOff() const { sens_initialized_ = false; } -void IDASolver::reInitB(const int which, const realtype tB0, - const AmiVector &yyB0, const AmiVector &ypB0) const { +void IDASolver::reInitB( + int const which, const realtype tB0, AmiVector const& yyB0, + AmiVector const& ypB0 +) const { - auto ida_memB = - static_cast(IDAGetAdjIDABmem(solver_memory_.get(), which)); + auto ida_memB + = static_cast(IDAGetAdjIDABmem(solver_memory_.get(), which)); if (solver_was_called_B_) force_reinit_postprocess_B_ = true; ida_memB->ida_tn = tB0; @@ -468,25 +506,27 @@ void IDASolver::reInitB(const int which, const realtype tB0, resetState(ida_memB, xB_.getNVector(), dxB_.getNVector()); } -void IDASolver::quadReInitB(const int which, const AmiVector &yQB0) const { - auto ida_memB = - static_cast(IDAGetAdjIDABmem(solver_memory_.get(), which)); +void IDASolver::quadReInitB(int const which, AmiVector const& yQB0) const { + auto ida_memB + = static_cast(IDAGetAdjIDABmem(solver_memory_.get(), which)); if (solver_was_called_B_) force_reinit_postprocess_B_ = true; xQB_.copy(yQB0); N_VScale(ONE, xQB_.getNVector(), ida_memB->ida_phiQ[0]); } -void IDASolver::setSensParams(const realtype *p, const realtype *pbar, - const int *plist) const { - int status = IDASetSensParams(solver_memory_.get(), const_cast(p), - const_cast(pbar), - const_cast(plist)); +void IDASolver::setSensParams( + realtype const* p, realtype const* pbar, int const* plist +) const { + int status = IDASetSensParams( + solver_memory_.get(), const_cast(p), + const_cast(pbar), const_cast(plist) + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetSensParams"); } -void IDASolver::getDky(const realtype t, const int k) const { +void IDASolver::getDky(const realtype t, int const k) const { int status = IDAGetDky(solver_memory_.get(), t, k, dky_.getNVector()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetDky"); @@ -494,53 +534,59 @@ void IDASolver::getDky(const realtype t, const int k) const { void IDASolver::getSens() const { realtype tDummy = 0; - int status = IDAGetSens(solver_memory_.get(), &tDummy, sx_.getNVectorArray()); + int status + = IDAGetSens(solver_memory_.get(), &tDummy, sx_.getNVectorArray()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetSens"); } -void IDASolver::getSensDky(const realtype t, const int k) const { - int status = IDAGetSensDky(solver_memory_.get(), t, k, sx_.getNVectorArray()); +void IDASolver::getSensDky(const realtype t, int const k) const { + int status + = IDAGetSensDky(solver_memory_.get(), t, k, sx_.getNVectorArray()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetSens"); } -void IDASolver::getB(const int which) const { +void IDASolver::getB(int const which) const { realtype tDummy = 0; - int status = IDAGetB(solver_memory_.get(), which, &tDummy, xB_.getNVector(), - dxB_.getNVector()); + int status = IDAGetB( + solver_memory_.get(), which, &tDummy, xB_.getNVector(), + dxB_.getNVector() + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetB"); } -void IDASolver::getDkyB(const realtype t, int k, const int which) const { - int status = IDAGetDky(IDAGetAdjIDABmem(solver_memory_.get(), which), t, k, - dky_.getNVector()); +void IDASolver::getDkyB(const realtype t, int k, int const which) const { + int status = IDAGetDky( + IDAGetAdjIDABmem(solver_memory_.get(), which), t, k, dky_.getNVector() + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetB"); } void IDASolver::getQuadB(int which) const { realtype tDummy = 0; - int status = - IDAGetQuadB(solver_memory_.get(), which, &tDummy, xQB_.getNVector()); + int status + = IDAGetQuadB(solver_memory_.get(), which, &tDummy, xQB_.getNVector()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetQuadB"); } -void IDASolver::getQuad(realtype &t) const { +void IDASolver::getQuad(realtype& t) const { int status = IDAGetQuad(solver_memory_.get(), &t, xQ_.getNVector()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetQuad"); } -void IDASolver::getQuadDkyB(const realtype t, int k, const int which) const { - int status = IDAGetQuadDky(IDAGetAdjIDABmem(solver_memory_.get(), which), t, - k, xQB_.getNVector()); +void IDASolver::getQuadDkyB(const realtype t, int k, int const which) const { + int status = IDAGetQuadDky( + IDAGetAdjIDABmem(solver_memory_.get(), which), t, k, xQB_.getNVector() + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetB"); } -void IDASolver::getQuadDky(const realtype t, const int k) const { +void IDASolver::getQuadDky(const realtype t, int const k) const { int status = IDAGetQuadDky(solver_memory_.get(), t, k, xQ_.getNVector()); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetQuadDky"); @@ -551,20 +597,23 @@ void IDASolver::adjInit() const { if (getAdjInitDone()) { status = IDAAdjReInit(solver_memory_.get()); } else { - status = IDAAdjInit(solver_memory_.get(), static_cast(maxsteps_), - static_cast(interp_type_)); + status = IDAAdjInit( + solver_memory_.get(), static_cast(maxsteps_), + static_cast(interp_type_) + ); setAdjInitDone(); } if (status != IDA_SUCCESS) throw IDAException(status, "IDAAdjInit"); } -void IDASolver::quadInit(const AmiVector &xQ0) const { +void IDASolver::quadInit(AmiVector const& xQ0) const { int status; xQ_.copy(xQ0); if (getQuadInitDone()) { - status = IDAQuadReInit(solver_memory_.get(), - const_cast(xQ0.getNVector())); + status = IDAQuadReInit( + solver_memory_.get(), const_cast(xQ0.getNVector()) + ); } else { status = IDAQuadInit(solver_memory_.get(), fqBdot_ss, xQ_.getNVector()); setQuadInitDone(); @@ -573,7 +622,7 @@ void IDASolver::quadInit(const AmiVector &xQ0) const { throw IDAException(status, "IDAQuadInit"); } -void IDASolver::allocateSolverB(int *which) const { +void IDASolver::allocateSolverB(int* which) const { if (!solver_memory_B_.empty()) { *which = 0; return; @@ -581,60 +630,68 @@ void IDASolver::allocateSolverB(int *which) const { int status = IDACreateB(solver_memory_.get(), which); if (*which + 1 > static_cast(solver_memory_B_.size())) solver_memory_B_.resize(*which + 1); - solver_memory_B_.at(*which) = - std::unique_ptr>( - getAdjBmem(solver_memory_.get(), *which), [](void * /*ptr*/) {}); + solver_memory_B_.at(*which) + = std::unique_ptr>( + getAdjBmem(solver_memory_.get(), *which), [](void* /*ptr*/) {} + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDACreateB"); } -void IDASolver::setSStolerancesB(const int which, const realtype relTolB, - const realtype absTolB) const { - int status = IDASStolerancesB(solver_memory_.get(), which, relTolB, absTolB); +void IDASolver::setSStolerancesB( + int const which, const realtype relTolB, const realtype absTolB +) const { + int status + = IDASStolerancesB(solver_memory_.get(), which, relTolB, absTolB); if (status != IDA_SUCCESS) throw IDAException(status, "IDASStolerancesB"); } -void IDASolver::quadSStolerancesB(const int which, const realtype reltolQB, - const realtype abstolQB) const { - int status = - IDAQuadSStolerancesB(solver_memory_.get(), which, reltolQB, abstolQB); +void IDASolver::quadSStolerancesB( + int const which, const realtype reltolQB, const realtype abstolQB +) const { + int status + = IDAQuadSStolerancesB(solver_memory_.get(), which, reltolQB, abstolQB); if (status != IDA_SUCCESS) throw IDAException(status, "IDAQuadSStolerancesB"); } -void IDASolver::quadSStolerances(const realtype reltolQB, - const realtype abstolQB) const { +void IDASolver::quadSStolerances( + const realtype reltolQB, const realtype abstolQB +) const { int status = IDAQuadSStolerances(solver_memory_.get(), reltolQB, abstolQB); if (status != IDA_SUCCESS) throw IDAException(status, "IDAQuadSStolerances"); } - -int IDASolver::solve(const realtype tout, const int itask) const { +int IDASolver::solve(const realtype tout, int const itask) const { if (force_reinit_postprocess_F_) reInitPostProcessF(tout); - int status = IDASolve(solver_memory_.get(), tout, &t_, x_.getNVector(), - dx_.getNVector(), itask); + int status = IDASolve( + solver_memory_.get(), tout, &t_, x_.getNVector(), dx_.getNVector(), + itask + ); solver_was_called_F_ = true; if (status < 0) // status > 0 is okay and is used for e.g. root return throw IntegrationFailure(status, t_); return status; } -int IDASolver::solveF(const realtype tout, const int itask, - int *ncheckPtr) const { +int IDASolver::solveF(const realtype tout, int const itask, int* ncheckPtr) + const { if (force_reinit_postprocess_F_) reInitPostProcessF(tout); - int status = IDASolveF(solver_memory_.get(), tout, &t_, x_.getNVector(), - xB_.getNVector(), itask, ncheckPtr); + int status = IDASolveF( + solver_memory_.get(), tout, &t_, x_.getNVector(), xB_.getNVector(), + itask, ncheckPtr + ); solver_was_called_F_ = true; if (status < 0) // status > 0 is okay and is used for e.g. root return throw IntegrationFailure(status, t_); return status; } -void IDASolver::solveB(const realtype tBout, const int itaskB) const { +void IDASolver::solveB(const realtype tBout, int const itaskB) const { if (force_reinit_postprocess_B_) reInitPostProcessB(tBout); int status = IDASolveB(solver_memory_.get(), tBout, itaskB); @@ -643,8 +700,8 @@ void IDASolver::solveB(const realtype tBout, const int itaskB) const { throw IntegrationFailure(status, tBout); } -void IDASolver::setMaxNumStepsB(const int which, - const long int mxstepsB) const { +void IDASolver::setMaxNumStepsB(int const which, long int const mxstepsB) + const { int status = IDASetMaxNumStepsB(solver_memory_.get(), which, mxstepsB); if (status != IDA_SUCCESS) throw IDAException(status, "IDASetMaxNumStepsB"); @@ -654,46 +711,49 @@ void IDASolver::diag() const { throw AmiException("Diag Solver was not implemented for DAEs"); } -void IDASolver::diagB(const int /*which*/) const { +void IDASolver::diagB(int const /*which*/) const { throw AmiException("Diag Solver was not implemented for DAEs"); } -void IDASolver::getNumSteps(const void *ami_mem, long int *numsteps) const { - int status = IDAGetNumSteps(const_cast(ami_mem), numsteps); +void IDASolver::getNumSteps(void const* ami_mem, long int* numsteps) const { + int status = IDAGetNumSteps(const_cast(ami_mem), numsteps); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetNumSteps"); } -void IDASolver::getNumRhsEvals(const void *ami_mem, - long int *numrhsevals) const { - int status = IDAGetNumResEvals(const_cast(ami_mem), numrhsevals); +void IDASolver::getNumRhsEvals(void const* ami_mem, long int* numrhsevals) + const { + int status = IDAGetNumResEvals(const_cast(ami_mem), numrhsevals); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetNumResEvals"); } -void IDASolver::getNumErrTestFails(const void *ami_mem, - long int *numerrtestfails) const { - int status = - IDAGetNumErrTestFails(const_cast(ami_mem), numerrtestfails); +void IDASolver::getNumErrTestFails( + void const* ami_mem, long int* numerrtestfails +) const { + int status + = IDAGetNumErrTestFails(const_cast(ami_mem), numerrtestfails); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetNumErrTestFails"); } void IDASolver::getNumNonlinSolvConvFails( - const void *ami_mem, long int *numnonlinsolvconvfails) const { - int status = IDAGetNumNonlinSolvConvFails(const_cast(ami_mem), - numnonlinsolvconvfails); + void const* ami_mem, long int* numnonlinsolvconvfails +) const { + int status = IDAGetNumNonlinSolvConvFails( + const_cast(ami_mem), numnonlinsolvconvfails + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetNumNonlinSolvConvFails"); } -void IDASolver::getLastOrder(const void *ami_mem, int *order) const { - int status = IDAGetLastOrder(const_cast(ami_mem), order); +void IDASolver::getLastOrder(void const* ami_mem, int* order) const { + int status = IDAGetLastOrder(const_cast(ami_mem), order); if (status != IDA_SUCCESS) throw IDAException(status, "IDAGetLastOrder"); } -void *IDASolver::getAdjBmem(void *ami_mem, int which) const { +void* IDASolver::getAdjBmem(void* ami_mem, int which) const { return IDAGetAdjIDABmem(ami_mem, which); } @@ -701,15 +761,17 @@ void IDASolver::calcIC(realtype tout1) const { int status = IDACalcIC(solver_memory_.get(), IDA_YA_YDP_INIT, tout1); if (status != IDA_SUCCESS) throw IDAException(status, "IDACalcIC"); - status = - IDAGetConsistentIC(solver_memory_.get(), x_.getNVector(), dx_.getNVector()); + status = IDAGetConsistentIC( + solver_memory_.get(), x_.getNVector(), dx_.getNVector() + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDACalcIC"); } -void IDASolver::calcICB(const int which, const realtype tout1) const { - int status = IDACalcICB(solver_memory_.get(), which, tout1, xB_.getNVector(), - dxB_.getNVector()); +void IDASolver::calcICB(int const which, const realtype tout1) const { + int status = IDACalcICB( + solver_memory_.get(), which, tout1, xB_.getNVector(), dxB_.getNVector() + ); if (status != IDA_SUCCESS) throw IDAException(status, "IDACalcICB"); } @@ -726,35 +788,39 @@ void IDASolver::turnOffRootFinding() const { throw IDAException(status, "IDARootInit"); } -const Model *IDASolver::getModel() const { +Model const* IDASolver::getModel() const { if (!solver_memory_) throw AmiException( - "Solver has not been allocated, information is not available"); + "Solver has not been allocated, information is not available" + ); auto ida_mem = static_cast(solver_memory_.get()); - auto user_data = static_cast(ida_mem->ida_user_data); - if(user_data) + auto user_data = static_cast(ida_mem->ida_user_data); + if (user_data) return user_data->first; return nullptr; } void IDASolver::setLinearSolver() const { - int status = IDASetLinearSolver(solver_memory_.get(), linear_solver_->get(), - linear_solver_->getMatrix()); + int status = IDASetLinearSolver( + solver_memory_.get(), linear_solver_->get(), linear_solver_->getMatrix() + ); if (status != IDA_SUCCESS) throw IDAException(status, "setLinearSolver"); } -void IDASolver::setLinearSolverB(const int which) const { - int status = - IDASetLinearSolverB(solver_memory_B_[which].get(), which, - linear_solver_B_->get(), linear_solver_B_->getMatrix()); +void IDASolver::setLinearSolverB(int const which) const { + int status = IDASetLinearSolverB( + solver_memory_B_[which].get(), which, linear_solver_B_->get(), + linear_solver_B_->getMatrix() + ); if (status != IDA_SUCCESS) throw IDAException(status, "setLinearSolverB"); } void IDASolver::setNonLinearSolver() const { - int status = - IDASetNonlinearSolver(solver_memory_.get(), non_linear_solver_->get()); + int status = IDASetNonlinearSolver( + solver_memory_.get(), non_linear_solver_->get() + ); if (status != IDA_SUCCESS) throw CvodeException(status, "CVodeSetNonlinearSolver"); } @@ -769,17 +835,20 @@ void IDASolver::setNonLinearSolverSens() const { switch (ism_) { case InternalSensitivityMethod::staggered: - status = IDASetNonlinearSolverSensStg(solver_memory_.get(), - non_linear_solver_sens_->get()); + status = IDASetNonlinearSolverSensStg( + solver_memory_.get(), non_linear_solver_sens_->get() + ); break; case InternalSensitivityMethod::simultaneous: - status = IDASetNonlinearSolverSensSim(solver_memory_.get(), - non_linear_solver_sens_->get()); + status = IDASetNonlinearSolverSensSim( + solver_memory_.get(), non_linear_solver_sens_->get() + ); break; case InternalSensitivityMethod::staggered1: default: throw AmiException( - "Unsupported internal sensitivity method selected: %d", ism_); + "Unsupported internal sensitivity method selected: %d", ism_ + ); } if (status != IDA_SUCCESS) @@ -787,8 +856,9 @@ void IDASolver::setNonLinearSolverSens() const { } void IDASolver::setNonLinearSolverB(int which) const { - int status = IDASetNonlinearSolverB(solver_memory_.get(), which, - non_linear_solver_B_->get()); + int status = IDASetNonlinearSolverB( + solver_memory_.get(), which, non_linear_solver_B_->get() + ); if (status != IDA_SUCCESS) throw CvodeException(status, "CVodeSetNonlinearSolverB"); } @@ -808,12 +878,14 @@ void IDASolver::setNonLinearSolverB(int which) const { * @param tmp3 temporary storage vector * @return status flag indicating successful execution **/ -int fJ(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xdot, SUNMatrix J, void *user_data, - N_Vector /*tmp1*/, N_Vector /*tmp2*/, N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +int fJ( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xdot, + SUNMatrix J, void* user_data, N_Vector /*tmp1*/, N_Vector /*tmp2*/, + N_Vector /*tmp3*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJ(t, cj, x, dx, xdot, J); return model->checkFinite(J, ModelQuantity::J, t); @@ -835,18 +907,18 @@ int fJ(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp3B temporary storage vector * @return status flag indicating successful execution **/ -int fJB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector /*xBdot*/, SUNMatrix JB, - void *user_data, N_Vector /*tmp1B*/, N_Vector /*tmp2B*/, - N_Vector /*tmp3B*/) { - auto typed_udata = static_cast(user_data); +int fJB( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector /*xBdot*/, SUNMatrix JB, void* user_data, N_Vector /*tmp1B*/, + N_Vector /*tmp2B*/, N_Vector /*tmp3B*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJB(t, cj, x, dx, xB, dxB, JB); return model->checkFinite(JB, ModelQuantity::JB, t); - } /** @@ -863,13 +935,14 @@ int fJB(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -int fJSparse(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector /*xdot*/, SUNMatrix J, void *user_data, - N_Vector /*tmp1*/, N_Vector /*tmp2*/, - N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +int fJSparse( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector /*xdot*/, + SUNMatrix J, void* user_data, N_Vector /*tmp1*/, N_Vector /*tmp2*/, + N_Vector /*tmp3*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparse(t, cj, x, dx, J); @@ -892,13 +965,14 @@ int fJSparse(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp3B temporary storage vector * @return status flag indicating successful execution */ -int fJSparseB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector /*xBdot*/, - SUNMatrix JB, void *user_data, N_Vector /*tmp1B*/, - N_Vector /*tmp2B*/, N_Vector /*tmp3B*/) { - auto typed_udata = static_cast(user_data); +int fJSparseB( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector /*xBdot*/, SUNMatrix JB, void* user_data, N_Vector /*tmp1B*/, + N_Vector /*tmp2B*/, N_Vector /*tmp3B*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparseB(t, cj, x, dx, xB, dxB, JB); @@ -919,9 +993,10 @@ int fJSparseB(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -int fJBand(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xdot, SUNMatrix J, void *user_data, - N_Vector tmp1, N_Vector tmp2, N_Vector tmp3) { +int fJBand( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xdot, + SUNMatrix J, void* user_data, N_Vector tmp1, N_Vector tmp2, N_Vector tmp3 +) { return fJ(t, cj, x, dx, xdot, J, user_data, tmp1, tmp2, tmp3); } @@ -941,12 +1016,14 @@ int fJBand(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp3B temporary storage vector * @return status flag indicating successful execution */ -int fJBandB(realtype t, realtype cj, N_Vector x, N_Vector dx, - N_Vector xB, N_Vector dxB, N_Vector xBdot, SUNMatrix JB, - void *user_data, N_Vector tmp1B, N_Vector tmp2B, - N_Vector tmp3B) { - return fJB(t, cj, x, dx, xB, dxB, xBdot, JB, user_data, tmp1B, tmp2B, - tmp3B); +int fJBandB( + realtype t, realtype cj, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, SUNMatrix JB, void* user_data, N_Vector tmp1B, + N_Vector tmp2B, N_Vector tmp3B +) { + return fJB( + t, cj, x, dx, xB, dxB, xBdot, JB, user_data, tmp1B, tmp2B, tmp3B + ); } /** @@ -963,13 +1040,15 @@ int fJBandB(realtype t, realtype cj, N_Vector x, N_Vector dx, * @param tmp2 temporary storage vector * @return status flag indicating successful execution **/ -int fJv(realtype t, N_Vector x, N_Vector dx, N_Vector /*xdot*/, - N_Vector v, N_Vector Jv, realtype cj, void *user_data, - N_Vector /*tmp1*/, N_Vector /*tmp2*/) { +int fJv( + realtype t, N_Vector x, N_Vector dx, N_Vector /*xdot*/, N_Vector v, + N_Vector Jv, realtype cj, void* user_data, N_Vector /*tmp1*/, + N_Vector /*tmp2*/ +) { - auto typed_udata = static_cast(user_data); + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJv(t, x, dx, v, Jv, cj); @@ -992,14 +1071,15 @@ int fJv(realtype t, N_Vector x, N_Vector dx, N_Vector /*xdot*/, * @param tmpB2 temporary storage vector * @return status flag indicating successful execution **/ -int fJvB(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector /*xBdot*/, N_Vector vB, N_Vector JvB, - realtype cj, void *user_data, N_Vector /*tmpB1*/, - N_Vector /*tmpB2*/) { +int fJvB( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector /*xBdot*/, N_Vector vB, N_Vector JvB, realtype cj, void* user_data, + N_Vector /*tmpB1*/, N_Vector /*tmpB2*/ +) { - auto typed_udata = static_cast(user_data); + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJvB(t, x, dx, xB, dxB, vB, JvB, cj); @@ -1015,16 +1095,18 @@ int fJvB(realtype t, N_Vector x, N_Vector dx, N_Vector xB, * @param user_data object with user input * @return status flag indicating successful execution */ -int froot(realtype t, N_Vector x, N_Vector dx, realtype *root, - void *user_data) { - auto typed_udata = static_cast(user_data); +int froot( + realtype t, N_Vector x, N_Vector dx, realtype* root, void* user_data +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->froot(t, x, dx, gsl::make_span(root, model->ne)); - return model->checkFinite(gsl::make_span(root, model->ne), - ModelQuantity::root); + return model->checkFinite( + gsl::make_span(root, model->ne), ModelQuantity::root + ); } /** @@ -1036,11 +1118,10 @@ int froot(realtype t, N_Vector x, N_Vector dx, realtype *root, * @param user_data object with user input * @return status flag indicating successful execution */ -int fxdot(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +int fxdot(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, void* user_data) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); auto solver = dynamic_cast(typed_udata->second); Expects(model); @@ -1073,11 +1154,13 @@ int fxdot(realtype t, N_Vector x, N_Vector dx, N_Vector xdot, * @param user_data object with user input * @return status flag indicating successful execution */ -int fxBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector xBdot, void *user_data) { - auto typed_udata = static_cast(user_data); +int fxBdot( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector xBdot, void* user_data +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); auto solver = dynamic_cast(typed_udata->second); Expects(model); @@ -1101,19 +1184,20 @@ int fxBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, * @param user_data pointer to temp data object * @return status flag indicating successful execution */ -int fqBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, - N_Vector dxB, N_Vector qBdot, void *user_data) { +int fqBdot( + realtype t, N_Vector x, N_Vector dx, N_Vector xB, N_Vector dxB, + N_Vector qBdot, void* user_data +) { - auto typed_udata = static_cast(user_data); + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fqBdot(t, x, dx, xB, dxB, qBdot); return model->checkFinite(gsl::make_span(qBdot), ModelQuantity::qBdot); } - /** * @brief Right hand side of differential equation for adjoint state xB * when simulating in steadystate mode @@ -1124,18 +1208,18 @@ int fqBdot(realtype t, N_Vector x, N_Vector dx, N_Vector xB, * @param user_data object with user input * @return status flag indicating successful execution */ -static int fxBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector xBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int fxBdot_ss( + realtype t, N_Vector xB, N_Vector dxB, N_Vector xBdot, void* user_data +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fxBdot_ss(t, xB, dxB, xBdot); return model->checkFinite(gsl::make_span(xBdot), ModelQuantity::xBdot_ss); } - /** * @brief Right hand side of integral equation for quadrature states qB * when simulating in steadystate mode @@ -1146,11 +1230,12 @@ static int fxBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector xBdot, * @param user_data pointer to temp data object * @return status flag indicating successful execution */ -static int fqBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, - void *user_data) { - auto typed_udata = static_cast(user_data); +static int fqBdot_ss( + realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, void* user_data +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fqBdot_ss(t, xB, dxB, qBdot); @@ -1171,18 +1256,20 @@ static int fqBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ - static int fJSparseB_ss(realtype /*t*/, realtype /*cj*/, N_Vector /*x*/, - N_Vector /*dx*/, N_Vector xBdot, SUNMatrix JB, - void *user_data, N_Vector /*tmp1*/, - N_Vector /*tmp2*/, N_Vector /*tmp3*/) { - auto typed_udata = static_cast(user_data); +static int fJSparseB_ss( + realtype /*t*/, realtype /*cj*/, N_Vector /*x*/, N_Vector /*dx*/, + N_Vector xBdot, SUNMatrix JB, void* user_data, N_Vector /*tmp1*/, + N_Vector /*tmp2*/, N_Vector /*tmp3*/ +) { + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); model->fJSparseB_ss(JB); - return model->checkFinite(gsl::make_span(xBdot), - ModelQuantity::JSparseB_ss); + return model->checkFinite( + gsl::make_span(xBdot), ModelQuantity::JSparseB_ss + ); } /** @@ -1201,20 +1288,21 @@ static int fqBdot_ss(realtype t, N_Vector xB, N_Vector dxB, N_Vector qBdot, * @param tmp3 temporary storage vector * @return status flag indicating successful execution */ -int fsxdot(int /*Ns*/, realtype t, N_Vector x, N_Vector dx, - N_Vector /*xdot*/, N_Vector *sx, N_Vector *sdx, - N_Vector *sxdot, void *user_data, N_Vector /*tmp1*/, - N_Vector /*tmp2*/, N_Vector /*tmp3*/) { +int fsxdot( + int /*Ns*/, realtype t, N_Vector x, N_Vector dx, N_Vector /*xdot*/, + N_Vector* sx, N_Vector* sdx, N_Vector* sxdot, void* user_data, + N_Vector /*tmp1*/, N_Vector /*tmp2*/, N_Vector /*tmp3*/ +) { - auto typed_udata = static_cast(user_data); + auto typed_udata = static_cast(user_data); Expects(typed_udata); - auto model = dynamic_cast(typed_udata->first); + auto model = dynamic_cast(typed_udata->first); Expects(model); for (int ip = 0; ip < model->nplist(); ip++) { model->fsxdot(t, x, dx, ip, sx[ip], sdx[ip], sxdot[ip]); if (model->checkFinite(gsl::make_span(sxdot[ip]), ModelQuantity::sxdot) - != AMICI_SUCCESS) + != AMICI_SUCCESS) return AMICI_RECOVERABLE_ERROR; } diff --git a/src/spline.cpp b/src/spline.cpp index 4cbfd82456..4e7d742520 100644 --- a/src/spline.cpp +++ b/src/spline.cpp @@ -7,60 +7,62 @@ namespace amici { /************************************************/ +/* Legacy implementation of spline functions */ /* adapted from */ /* CMATH. Copyright (c) 1989 Design Software */ /* */ /************************************************/ - -int spline(int n, int end1, int end2, double slope1, double slope2, double x[], - double y[], double b[], double c[], double d[]) - /** - Evaluate the coefficients b[i], c[i], d[i], i = 0, 1, .. n-1 for - a cubic interpolating spline - - S(xx) = Y[i] + b[i] * w + c[i] * w**2 + d[i] * w**3 - where w = xx - x[i] - and x[i] <= xx <= x[i+1] - - The n supplied data points are x[i], y[i], i = 0 ... n-1. - - @param[in] n The number of data points or knots (n >= 2) - @param[in] end1 0: default condition 1: specify the slopes at x[0] - @param[in] end2 0: default condition 1: specify the slopes at x[n-1] - @param[in] slope1 slope at x[0] - @param[in] slope2 slope at x[n-1] - @param[in] x[] the abscissas of the knots in strictly increasing order - @param[in] y[] the ordinates of the knots - @param[out] b[] array of spline coefficients - @param[out] c[] array of spline coefficients - @param[out] d[] array of spline coefficients - - @retval 0 normal return - @retval 1 less than two data points; cannot interpolate - @retval 2 x[] are not in ascending order - - Notes - ----- - - The accompanying function seval() may be used to evaluate the - spline while deriv will provide the first derivative. - - Using p to denote differentiation - y[i] = S(X[i]) - b[i] = Sp(X[i]) - c[i] = Spp(X[i])/2 - d[i] = Sppp(X[i])/6 ( Derivative from the right ) - - Since the zero elements of the arrays ARE NOW used here, - all arrays to be passed from the main program should be - dimensioned at least [n]. These routines will use elements - [0 .. n-1]. - - Adapted from the text - Forsythe, G.E., Malcolm, M.A. and Moler, C.B. (1977) - "Computer Methods for Mathematical Computations" - Prentice Hall - - Note that although there are only n-1 polynomial segments, - n elements are requird in b, c, d. The elements b[n-1], - c[n-1] and d[n-1] are set to continue the last segment - past x[n-1]. +int spline( + int n, int end1, int end2, double slope1, double slope2, double x[], + double y[], double b[], double c[], double d[] +) +/** +Evaluate the coefficients b[i], c[i], d[i], i = 0, 1, .. n-1 for +a cubic interpolating spline + +S(xx) = Y[i] + b[i] * w + c[i] * w**2 + d[i] * w**3 +where w = xx - x[i] +and x[i] <= xx <= x[i+1] + +The n supplied data points are x[i], y[i], i = 0 ... n-1. + +@param[in] n The number of data points or knots (n >= 2) +@param[in] end1 0: default condition 1: specify the slopes at x[0] +@param[in] end2 0: default condition 1: specify the slopes at x[n-1] +@param[in] slope1 slope at x[0] +@param[in] slope2 slope at x[n-1] +@param[in] x[] the abscissas of the knots in strictly increasing order +@param[in] y[] the ordinates of the knots +@param[out] b[] array of spline coefficients +@param[out] c[] array of spline coefficients +@param[out] d[] array of spline coefficients + +@retval 0 normal return +@retval 1 less than two data points; cannot interpolate +@retval 2 x[] are not in ascending order + +Notes +----- + - The accompanying function seval() may be used to evaluate the + spline while deriv will provide the first derivative. + - Using p to denote differentiation + y[i] = S(X[i]) + b[i] = Sp(X[i]) + c[i] = Spp(X[i])/2 + d[i] = Sppp(X[i])/6 ( Derivative from the right ) + - Since the zero elements of the arrays ARE NOW used here, + all arrays to be passed from the main program should be + dimensioned at least [n]. These routines will use elements + [0 .. n-1]. + - Adapted from the text + Forsythe, G.E., Malcolm, M.A. and Moler, C.B. (1977) + "Computer Methods for Mathematical Computations" + Prentice Hall + - Note that although there are only n-1 polynomial segments, + n elements are requird in b, c, d. The elements b[n-1], + c[n-1] and d[n-1] are set to continue the last segment + past x[n-1]. */ { /* begin procedure spline() */ @@ -107,8 +109,8 @@ int spline(int n, int end1, int end2, double slope1, double slope2, double x[], c[nm1] = 0.0; if (n != 3) { c[0] = c[2] / (x[3] - x[1]) - c[1] / (x[2] - x[0]); - c[nm1] = c[n - 2] / (x[nm1] - x[n - 3]) - - c[n - 3] / (x[n - 2] - x[n - 4]); + c[nm1] = c[n - 2] / (x[nm1] - x[n - 3]) + - c[n - 3] / (x[n - 2] - x[n - 4]); c[0] = c[0] * d[0] * d[0] / (x[3] - x[0]); c[nm1] = -c[nm1] * d[n - 2] * d[n - 2] / (x[nm1] - x[n - 4]); } @@ -140,8 +142,8 @@ int spline(int n, int end1, int end2, double slope1, double slope2, double x[], /* c[i] is now the sigma[i] of the text */ /* Compute the polynomial coefficients */ - b[nm1] = (y[nm1] - y[n - 2]) / d[n - 2] + - d[n - 2] * (c[n - 2] + 2.0 * c[nm1]); + b[nm1] = (y[nm1] - y[n - 2]) / d[n - 2] + + d[n - 2] * (c[n - 2] + 2.0 * c[nm1]); for (i = 0; i < nm1; ++i) { b[i] = (y[i + 1] - y[i]) / d[i] - d[i] * (c[i + 1] + 2.0 * c[i]); d[i] = (c[i + 1] - c[i]) / d[i]; @@ -192,8 +194,9 @@ int spline(int n, int end1, int end2, double slope1, double slope2, double x[], */ -double seval(int n, double u, double x[], double y[], double b[], double c[], - double d[]) +double seval( + int n, double u, double x[], double y[], double b[], double c[], double d[] +) { /* begin function seval() */ @@ -253,8 +256,9 @@ double seval(int n, double u, double x[], double y[], double b[], double c[], */ -double sinteg(int n, double u, double x[], double y[], double b[], double c[], - double d[]) { /* begin function sinteg() */ +double sinteg( + int n, double u, double x[], double y[], double b[], double c[], double d[] +) { /* begin function sinteg() */ int i, j; double sum, dx; @@ -277,14 +281,15 @@ double sinteg(int n, double u, double x[], double y[], double b[], double c[], /* ---- Evaluate the integral for segments x < u ---- */ for (j = 0; j < i; ++j) { dx = x[j + 1] - x[j]; - sum += dx * (y[j] + - dx * (0.5 * b[j] + dx * (c[j] / 3.0 + dx * 0.25 * d[j]))); + sum += dx + * (y[j] + + dx * (0.5 * b[j] + dx * (c[j] / 3.0 + dx * 0.25 * d[j]))); } /* ---- Evaluate the integral fot this segment ---- */ dx = u - x[i]; - sum += - dx * (y[i] + dx * (0.5 * b[i] + dx * (c[i] / 3.0 + dx * 0.25 * d[i]))); + sum += dx + * (y[i] + dx * (0.5 * b[i] + dx * (c[i] / 3.0 + dx * 0.25 * d[i]))); return (sum); } diff --git a/src/splinefunctions.cpp b/src/splinefunctions.cpp new file mode 100644 index 0000000000..ba9865a729 --- /dev/null +++ b/src/splinefunctions.cpp @@ -0,0 +1,1029 @@ +#include "amici/splinefunctions.h" +#include "amici/amici.h" +#include "amici/defines.h" +#include "amici/exception.h" +#include "amici/vector.h" + +#include // std::min +#include +#include + +namespace amici { + +static realtype +evaluate_polynomial(realtype const x, gsl::span coeff) { + /* Use Horner's method (https://en.wikipedia.org/wiki/Horner%27s_method) + * for numerical efficiency: + * + * spline(t) = a * t**3 + b * t**2 + c * t + d + * = d + t * (c + t * (b + t * a)) + * with coeff[0, 1, 2, 3] = [d, c, b, a] + */ + assert(coeff.size() >= 4); + auto coeff_p = coeff.data(); + return coeff_p[0] + x * (coeff_p[1] + x * (coeff_p[2] + x * coeff_p[3])); +} + +AbstractSpline::AbstractSpline( + std::vector nodes, std::vector node_values, + bool equidistant_spacing, bool logarithmic_parametrization +) + : nodes_(std::move(nodes)) + , node_values_(std::move(node_values)) + , equidistant_spacing_(equidistant_spacing) + , logarithmic_parametrization_(logarithmic_parametrization) { + + /* we want to set the number of nodes */ + auto n_nodes_ = static_cast(node_values_.size()); + + /* In case we have equidistant spacing, compute node locations */ + if (equidistant_spacing_) { + if (nodes_.size() != 2) + throw AmiException("Splines with equidistant spacing need a nodes " + "vector with two elements (first/last node)."); + realtype node_start = nodes_[0]; + realtype node_step = (nodes_[1] - nodes_[0]) / (n_nodes_ - 1); + nodes_.resize(n_nodes_); + nodes_[n_nodes_ - 1] = nodes_[1]; + for (int i_node = 0; i_node < n_nodes_ - 1; i_node++) + nodes_[i_node] = node_start + i_node * node_step; + } else if (nodes_.size() != node_values_.size()) { + throw std::invalid_argument( + "Number of nodes and number of node_values do not match." + ); + } +} + +realtype AbstractSpline::get_value(const realtype t) const { + auto y = get_value_scaled(t); + return logarithmic_parametrization_ ? std::exp(y) : y; +} + +realtype AbstractSpline::get_sensitivity(const realtype t, int const ip) const { + auto s = get_sensitivity_scaled(t, ip); + return logarithmic_parametrization_ ? s * get_value(t) : s; +} + +realtype AbstractSpline::get_sensitivity( + const realtype t, int const ip, const realtype value +) const { + auto s = get_sensitivity_scaled(t, ip); + return logarithmic_parametrization_ ? s * value : s; +} + +realtype AbstractSpline::get_node_value(int const i) const { + return node_values_[i]; +} + +realtype AbstractSpline::get_node_value_scaled(int const i) const { + // TODO It could be precomputed and stored in the object. + // Not sure if its worth the effort. + if (logarithmic_parametrization_) + return std::log(node_values_[i]); + else + return node_values_[i]; +} + +realtype AbstractSpline::get_final_value_scaled() const { + return final_value_scaled_; +} + +realtype AbstractSpline::get_final_value() const { + auto y = get_final_value_scaled(); + return logarithmic_parametrization_ ? std::exp(y) : y; +} + +void AbstractSpline::set_final_value_scaled(realtype finalValue) { + final_value_scaled_ = finalValue; +} + +realtype AbstractSpline::get_final_sensitivity_scaled(int const ip) const { + return final_sensitivity_scaled_[ip]; +} + +realtype AbstractSpline::get_final_sensitivity(int const ip) const { + auto s = get_final_sensitivity_scaled(ip); + if (logarithmic_parametrization_) { + auto v = get_final_value(); + if (std::isinf(v)) { + assert( + v > 0 + ); // logarithmic parameterization means positive values only + assert( + std::isnan(s) || s == 0 + ); // in the case the limit is +inf, sensitivity in log-scale will + // either be NaN or zero + return s; + } else { + return s * v; + } + } else { + return s; + } +} + +void AbstractSpline::set_final_sensitivity_scaled( + std::vector finalSensitivity +) { + final_sensitivity_scaled_ = std::move(finalSensitivity); +} + +bool AbstractSpline::get_equidistant_spacing() const { + return equidistant_spacing_; +} + +bool AbstractSpline::get_logarithmic_parametrization() const { + return logarithmic_parametrization_; +} + +HermiteSpline::HermiteSpline( + std::vector nodes, std::vector node_values, + std::vector node_values_derivative, + SplineBoundaryCondition firstNodeBC, SplineBoundaryCondition lastNodeBC, + SplineExtrapolation firstNodeExtrapol, SplineExtrapolation lastNodeExtrapol, + bool node_derivative_by_FD, bool equidistant_spacing, + bool logarithmic_parametrization +) + : AbstractSpline( + std::move(nodes), std::move(node_values), equidistant_spacing, + logarithmic_parametrization + ) + , node_values_derivative_(std::move(node_values_derivative)) + , first_node_bc_(firstNodeBC) + , last_node_bc_(lastNodeBC) + , first_node_ep_(firstNodeExtrapol) + , last_node_ep_(lastNodeExtrapol) + , node_derivative_by_FD_(node_derivative_by_FD) { + if (!node_derivative_by_FD_ + && node_values_derivative_.size() != nodes_.size()) { + throw std::invalid_argument( + "Size of node_values_derivative does not match number of nodes." + ); + } + + /* We may have to compute the derivatives at the nodes */ + handle_inner_derivatives(); + /* First and last node need to be handled separately */ + handle_boundary_conditions(); +} + +void HermiteSpline::handle_inner_derivatives() { + /* If values of the derivative at the nodes are to be computed by finite + * differences, we have to fill up node_values_derivative_ */ + if (node_derivative_by_FD_) { + node_values_derivative_.resize(n_nodes(), 0.0); + if (get_equidistant_spacing()) { + realtype hx2 = 2 * (nodes_[1] - nodes_[0]); + for (int i_node = 1; i_node < n_nodes() - 1; i_node++) + node_values_derivative_[i_node] + = (node_values_[i_node + 1] - node_values_[i_node - 1]) + / hx2; + } else { + for (int i_node = 1; i_node < n_nodes() - 1; i_node++) { + realtype dleft + = (node_values_[i_node] - node_values_[i_node - 1]) + / (nodes_[i_node] - nodes_[i_node - 1]); + realtype dright + = (node_values_[i_node + 1] - node_values_[i_node]) + / (nodes_[i_node + 1] - nodes_[i_node]); + node_values_derivative_[i_node] = (dleft + dright) / 2; + } + } + } +} + +void HermiteSpline::handle_boundary_conditions() { + int last = n_nodes() - 1; + + if ((first_node_bc_ == SplineBoundaryCondition::periodic + || last_node_bc_ == SplineBoundaryCondition::periodic) + && first_node_bc_ != last_node_bc_) + throw AmiException("If one of the boundary conditions is periodic, " + "the other one must be periodic too."); + + /* We have to take special care of the first node */ + switch (first_node_bc_) { + case SplineBoundaryCondition::given: + if (node_derivative_by_FD_) + /* 1-sided FD */ + node_values_derivative_[0] + = (node_values_[1] - node_values_[0]) / (nodes_[1] - nodes_[0]); + break; + + case SplineBoundaryCondition::zeroDerivative: + node_values_derivative_[0] = 0; + break; + + case SplineBoundaryCondition::natural: + node_values_derivative_[0] = -0.5 * node_values_derivative_[1] + + 1.5 * (node_values_[1] - node_values_[0]) + / (nodes_[1] - nodes_[0]); + break; + + case SplineBoundaryCondition::naturalZeroDerivative: + throw AmiException("Natural boundary condition with zero " + "derivative is not allowed for Hermite splines."); + + case SplineBoundaryCondition::periodic: + if (node_derivative_by_FD_) { + if (get_equidistant_spacing()) { + realtype hx2 = 2 * (nodes_[1] - nodes_[0]); + node_values_derivative_[0] + = (node_values_[1] - node_values_[last - 1]) / hx2; + } else { + realtype dleft = (node_values_[last] - node_values_[last - 1]) + / (nodes_[last] - nodes_[last - 1]); + realtype dright = (node_values_[1] - node_values_[0]) + / (nodes_[1] - nodes_[0]); + node_values_derivative_[0] = (dleft + dright) / 2; + } + } + break; + + default: + throw AmiException("Invalid value for boundary condition."); + } + + /* ...and the last node (1-sided FD). */ + switch (last_node_bc_) { + case SplineBoundaryCondition::given: + if (node_derivative_by_FD_) + /* 1-sided FD */ + node_values_derivative_[last] + = (node_values_[last] - node_values_[last - 1]) + / (nodes_[last] - nodes_[last - 1]); + break; + + case SplineBoundaryCondition::zeroDerivative: + node_values_derivative_[last] = 0; + break; + + case SplineBoundaryCondition::natural: + node_values_derivative_[last] + = -0.5 * node_values_derivative_[last - 1] + + 1.5 * (node_values_[last] - node_values_[last - 1]) + / (nodes_[last] - nodes_[last - 1]); + break; + + case SplineBoundaryCondition::naturalZeroDerivative: + throw AmiException("Natural boundary condition with zero " + "derivative is not allowed for Hermite splines."); + + case SplineBoundaryCondition::periodic: + if (node_derivative_by_FD_) + // if one bc is periodic, the other is periodic too + node_values_derivative_[last] = node_values_derivative_[0]; + break; + + default: + throw AmiException("Invalid value for boundary condition."); + } +} + +realtype HermiteSpline::get_node_derivative(int const i) const { + return node_values_derivative_[i]; +} + +realtype HermiteSpline::get_node_derivative_scaled(int const i) const { + // TODO It could be precomputed and stored in the object. + // Not sure if its worth the effort. + if (get_logarithmic_parametrization()) + return node_values_derivative_[i] / node_values_[i]; + else + return node_values_derivative_[i]; +} + +void HermiteSpline::compute_coefficients() { + /* Allocate space for the coefficients for Horner's method. + * They are stored in the vector as + * [d_0, c_0, b_0, a_0, d_1, c_1, ... , b_{n_nodes-1}, a_{n_nodes-1}] */ + coefficients.resize(4 * (n_nodes() - 1), 0.0); + + /* Compute the coefficients of the spline polynomials: + * spline(t) = a * t**3 + b * t**2 + c * t + d + * = d + t * (c + t * (b + t * a)) + * with coefficients[4 * i_node + (0, 1, 2, 3)] = (d, c, b, a) + */ + + for (int i_node = 0; i_node < n_nodes() - 1; i_node++) { + /* Get the length of the interval. Yes, we could save computation time + * by exploiting equidistant spacing, but we're talking about <1k FLOPs + * for sure, no matter what model. Screw it. */ + realtype len = nodes_[i_node + 1] - nodes_[i_node]; + + /* Coefficients for cubic Hermite polynomials */ + coefficients[4 * i_node] = get_node_value_scaled(i_node); + coefficients[4 * i_node + 1] = len * get_node_derivative_scaled(i_node); + coefficients[4 * i_node + 2] + = -3 * get_node_value_scaled(i_node) + - 2 * len * get_node_derivative_scaled(i_node) + + 3 * get_node_value_scaled(i_node + 1) + - len * get_node_derivative_scaled(i_node + 1); + coefficients[4 * i_node + 3] + = 2 * get_node_value_scaled(i_node) + + len * get_node_derivative_scaled(i_node) + - 2 * get_node_value_scaled(i_node + 1) + + len * get_node_derivative_scaled(i_node + 1); + } + + /* Take care of coefficients for extrapolation */ + compute_coefficients_extrapolation(); +} + +void HermiteSpline::compute_coefficients_extrapolation() { + /* Do we want to extrapolate at all? */ + bool needExtrapolationCoefficients + = first_node_ep_ == SplineExtrapolation::constant + || first_node_ep_ == SplineExtrapolation::linear + || last_node_ep_ == SplineExtrapolation::constant + || last_node_ep_ == SplineExtrapolation::linear; + if (!needExtrapolationCoefficients) + return; + + coefficients_extrapolate.resize(4, 0.0); + + int last = n_nodes() - 1; + + /* Beyond the spline nodes, we need to extrapolate using a * t + b. + * Those coefficients are stored as [b_first, a_first, b_last, a_last] */ + switch (first_node_ep_) { + case SplineExtrapolation::constant: + coefficients_extrapolate[0] = get_node_value_scaled(0); + coefficients_extrapolate[1] = 0; + break; + + case SplineExtrapolation::linear: + coefficients_extrapolate[0] + = get_node_value_scaled(0) + - nodes_[0] * get_node_derivative_scaled(0); + coefficients_extrapolate[1] = get_node_derivative_scaled(0); + break; + + default: + /* We don't need specific coefficients in the cases of: + * noExtrapolation, polynomial, periodic*/ + break; + } + switch (last_node_ep_) { + case SplineExtrapolation::constant: + coefficients_extrapolate[2] = get_node_value_scaled(last); + coefficients_extrapolate[3] = 0; + break; + + case SplineExtrapolation::linear: + coefficients_extrapolate[2] + = get_node_value_scaled(last) + - nodes_[last] * get_node_derivative_scaled(last); + coefficients_extrapolate[3] = get_node_derivative_scaled(last); + break; + + default: + /* We don't need specific coefficients in the cases of: + * noExtrapolation, polynomial, periodic*/ + break; + } +} + +#ifdef DVALUESDP +#error "Preprocessor macro DVALUESDP already defined?!" +#else +#define DVALUESDP(i_node) dvaluesdp[node_offset + (i_node)*nplist] +#endif +#ifdef DSLOPESDP +#error "Preprocessor macro DSLOPESDP already defined?!" +#else +#define DSLOPESDP(i_node) dslopesdp[node_offset + (i_node)*nplist] +#endif + +void HermiteSpline::compute_coefficients_sensi( + int nplist, int spline_offset, gsl::span dvaluesdp, + gsl::span dslopesdp +) { + // If slopes are computed by finite differences, + // we need to autocompute the slope sensitivities + if (node_derivative_by_FD_) { + assert(dvaluesdp.size() == dslopesdp.size()); + for (int ip = 0; ip < nplist; ip++) + compute_slope_sensitivities_by_fd( + nplist, spline_offset, ip, dvaluesdp, dslopesdp + ); + } + + /* Ensure that dslopesdp satisfies the BC */ + if (first_node_bc_ == SplineBoundaryCondition::zeroDerivative) { + for (int ip = 0; ip < nplist; ip++) + dslopesdp[spline_offset + ip] = 0.0; + } + if (last_node_bc_ == SplineBoundaryCondition::zeroDerivative) { + int last = n_nodes() - 1; + for (int ip = 0; ip < nplist; ip++) + dslopesdp[spline_offset + ip + last * nplist] = 0.0; + } + + // If necessary, translate sensitivities to logarithmic parametrization + if (get_logarithmic_parametrization()) { + for (int i_node = 0; i_node < n_nodes(); i_node++) { + for (int ip = 0; ip < nplist; ip++) { + int node_offset = spline_offset + ip; + realtype value = get_node_value(i_node); + realtype slope = node_values_derivative_[i_node]; + realtype dvaluedp = DVALUESDP(i_node); + realtype dslopedp = DSLOPESDP(i_node); + DVALUESDP(i_node) = dvaluedp / value; + DSLOPESDP(i_node) + = (dslopedp - dvaluedp * slope / value) / value; + } + } + } + + /* + * Allocate space for the coefficients + * They are stored in the vector as + * [ D[d_0, p0], D[c_0, p0], D[b_0, p0], D[a_0, p0], D[d_1, p0], + * ... , + * D[b_{n_nodes-1}, p0], D[a_{n_nodes-1}, p0], + * D[d_0, p1], D[c_0, p1], ... + * ..., D[b_{n_nodes-1}, p{nplist-1}, D[a_{n_nodes-1}, p{nplist-1}] + * ] + */ + int n_spline_coefficients = 4 * (n_nodes() - 1); + coefficients_sensi.resize(n_spline_coefficients * nplist, 0.0); + + /* + * We're using short hand notation for some node values or slopes, based on + * the notation used on https://en.wikipedia.org/wiki/Cubic_Hermite_spline + * In brief: "p" denotes the current (k-th) spline node value, + * "m" its tangent or slope, "s" in front the sensitivity, "1" at the end + * means the following node (" + 1"), so "smk1" is the sensitivity of the + * slope at node k + 1, w.r.t. to the current parameter (looping index). + */ + + /* Parametric derivatives of splines are splines again. + * We compute the coefficients for those polynomials now. */ + for (int i_node = 0; i_node < n_nodes() - 1; i_node++) { + /* Get the length of the interval. */ + realtype len = nodes_[i_node + 1] - nodes_[i_node]; + + /* As computing the coefficient is a mess, it's in another function */ + for (int ip = 0; ip < nplist; ip++) + get_coeffs_sensi_lowlevel( + ip, i_node, nplist, n_spline_coefficients, spline_offset, len, + dvaluesdp, dslopesdp, coefficients_sensi + ); + } + + /* We need the coefficients for extrapolating beyond the spline domain */ + compute_coefficients_extrapolation_sensi( + nplist, spline_offset, dvaluesdp, dslopesdp + ); +} + +void HermiteSpline::compute_slope_sensitivities_by_fd( + int nplist, int spline_offset, int ip, gsl::span dvaluesdp, + gsl::span dslopesdp +) { + int last = n_nodes() - 1; + int node_offset = spline_offset + ip; + + // Left boundary (first node) + switch (first_node_bc_) { + case SplineBoundaryCondition::given: + DSLOPESDP(0) = (DVALUESDP(1) - DVALUESDP(0)) / (nodes_[1] - nodes_[0]); + break; + + case SplineBoundaryCondition::zeroDerivative: + DSLOPESDP(0) = 0; + break; + + case SplineBoundaryCondition::natural: + throw AmiException("Natural boundary condition for Hermite " + "splines is not implemented yet."); + + case SplineBoundaryCondition::periodic: + if (get_equidistant_spacing()) { + realtype hx2 = 2 * (nodes_[1] - nodes_[0]); + DSLOPESDP(0) = (DVALUESDP(1) - DVALUESDP(last - 1)) / hx2; + } else { + realtype dleft = (DVALUESDP(last) - DVALUESDP(last - 1)) + / (nodes_[last] - nodes_[last - 1]); + realtype dright + = (DVALUESDP(1) - DVALUESDP(0)) / (nodes_[1] - nodes_[0]); + DSLOPESDP(0) = (dleft + dright) / 2; + } + break; + + default: + throw AmiException("Unexpected value for boundary condition."); + } + + // Inner nodes + if (get_equidistant_spacing()) { + realtype hx2 = 2 * (nodes_[1] - nodes_[0]); + for (int i_node = 1; i_node < n_nodes() - 1; i_node++) + DSLOPESDP(i_node) + = (DVALUESDP(i_node + 1) - DVALUESDP(i_node - 1)) / hx2; + } else { + for (int i_node = 1; i_node < n_nodes() - 1; i_node++) { + realtype dleft = (DVALUESDP(i_node) - DVALUESDP(i_node - 1)) + / (nodes_[i_node] - nodes_[i_node - 1]); + realtype dright = (DVALUESDP(i_node + 1) - DVALUESDP(i_node)) + / (nodes_[i_node + 1] - nodes_[i_node]); + DSLOPESDP(i_node) = (dleft + dright) / 2; + } + } + + // Right boundary (last nodes) + switch (last_node_bc_) { + case SplineBoundaryCondition::given: + DSLOPESDP(last) = (DVALUESDP(last) - DVALUESDP(last - 1)) + / (nodes_[last] - nodes_[last - 1]); + break; + + case SplineBoundaryCondition::zeroDerivative: + DSLOPESDP(last) = 0; + break; + + case SplineBoundaryCondition::natural: + throw AmiException("Natural boundary condition for Hermite " + "splines is not implemented yet."); + + case SplineBoundaryCondition::naturalZeroDerivative: + throw AmiException("Natural boundary condition with zero " + "derivative is not allowed for Hermite splines."); + + case SplineBoundaryCondition::periodic: + // if one bc is periodic, the other is periodic too + DSLOPESDP(last) = DSLOPESDP(0); + break; + + default: + throw AmiException("Unexpected value for boundary condition."); + } +} + +#undef DVALUESDP +#undef DSLOPESDP + +void HermiteSpline::compute_coefficients_extrapolation_sensi( + int nplist, int spline_offset, gsl::span dvaluesdp, + gsl::span dslopesdp +) { + + /* Do we want to extrapolate at all? */ + bool needExtrapolationCoefficients + = first_node_ep_ == SplineExtrapolation::constant + || first_node_ep_ == SplineExtrapolation::linear + || last_node_ep_ == SplineExtrapolation::constant + || last_node_ep_ == SplineExtrapolation::linear; + if (!needExtrapolationCoefficients) + return; + + /* Beyond the spline nodes, we need to extrapolate using a * t + b. + * Those coefficients are stored as + * [ + * D[b_first, p0], D[a_first, p0], D[b_last, p0], D[a_last, p0], + * D[b_first, p1], ... D[a_last, p{nplist-1}] + * ] + */ + coefficients_extrapolate_sensi.resize(4 * nplist, 0.0); + + realtype sm0; + for (int ip = 0; ip < nplist; ip++) { + realtype sp0 = dvaluesdp[spline_offset + ip]; + switch (first_node_ep_) { + /* This whole switch-case-if-else-if-thing could be moved + * outside the loop, I know. Yet, it's at most some thousand + * if's done once in the program for saving many lines of code + * and getting a much clearer code structure. */ + case SplineExtrapolation::constant: + sm0 = 0; + break; + + case SplineExtrapolation::linear: + if (first_node_bc_ == SplineBoundaryCondition::zeroDerivative) { + sm0 = 0; + } else if (get_node_derivative_by_fd() && first_node_bc_ == SplineBoundaryCondition::given) { + sm0 = (dvaluesdp[spline_offset + ip + nplist] - sp0) + / (nodes_[1] - nodes_[0]); + + } else if (get_node_derivative_by_fd() && first_node_bc_ == SplineBoundaryCondition::natural) { + throw AmiException( + "Natural boundary condition for " + "Hermite splines with linear extrapolation is " + "not yet implemented." + ); + + } else if (!get_node_derivative_by_fd() && first_node_bc_ == SplineBoundaryCondition::given) { + sm0 = dslopesdp[spline_offset + ip]; + + } else if (!get_node_derivative_by_fd() && first_node_bc_ == SplineBoundaryCondition::natural) { + throw AmiException( + "Natural boundary condition for " + "Hermite splines with linear extrapolation is " + "not yet implemented." + ); + + } else { + throw AmiException( + "Some weird combination of spline boundary " + "condition, extrapolation and finite differences was " + "passed which should not be allowed." + ); + } + break; + + default: + /* We don't need specific coefficients in the cases of: + * noExtrapolation, polynomial, periodic + * NB the corresponding values in coefficients_extrapolate_sensi + * will never be accessed, so it's safe to leave them + * undefined. + */ + continue; + } + /* Write them to the vector */ + coefficients_extrapolate_sensi[4 * ip] = sp0 - sm0 * nodes_[0]; + coefficients_extrapolate_sensi[4 * ip + 1] = sm0; + } + + realtype sm_end; + for (int ip = 0; ip < nplist; ip++) { + realtype sp_end + = dvaluesdp[spline_offset + ip + (n_nodes() - 1) * nplist]; + switch (last_node_ep_) { + /* This whole switch-case-if-else-if-thing could be moved + * outside the loop, I know. Yet, it's at most some thousand + * if's done once in the program for saving many lines of code + * and getting a much clearer code structure. */ + case SplineExtrapolation::constant: + sm_end = 0; + break; + + case SplineExtrapolation::linear: + if (last_node_bc_ == SplineBoundaryCondition::zeroDerivative) { + sm_end = 0; + } else if (get_node_derivative_by_fd() && last_node_bc_ == SplineBoundaryCondition::given) { + sm_end = (sp_end + - dvaluesdp + [spline_offset + ip + (n_nodes() - 2) * nplist]) + / (nodes_[n_nodes() - 1] - nodes_[n_nodes() - 2]); + + } else if (get_node_derivative_by_fd() && last_node_bc_ == SplineBoundaryCondition::natural) { + throw AmiException( + "Natural boundary condition for " + "Hermite splines with linear extrapolation is " + "not yet implemented." + ); + + } else if (!get_node_derivative_by_fd() && last_node_bc_ == SplineBoundaryCondition::given) { + sm_end + = dslopesdp[spline_offset + ip + (n_nodes() - 1) * nplist]; + + } else if (!get_node_derivative_by_fd() && last_node_bc_ == SplineBoundaryCondition::natural) { + throw AmiException( + "Natural boundary condition for " + "Hermite splines with linear extrapolation is " + "not yet implemented." + ); + + } else { + throw AmiException( + "Some weird combination of spline boundary " + "condition, extrapolation and finite differences was " + "passed which should not be allowed." + ); + } + break; + + default: + /* We don't need specific coefficients in the cases of: + * noExtrapolation, polynomial, periodic + * NB the corresponding values in coefficients_extrapolate_sensi + * will never be accessed, so it's safe to leave them + * undefined. + */ + continue; + } + /* Write them to the vector */ + coefficients_extrapolate_sensi[4 * ip + 2] + = sp_end - sm_end * nodes_[n_nodes() - 1]; + coefficients_extrapolate_sensi[4 * ip + 3] = sm_end; + } +} + +void HermiteSpline::get_coeffs_sensi_lowlevel( + int ip, int i_node, int nplist, int n_spline_coefficients, + int spline_offset, realtype len, gsl::span dnodesdp, + gsl::span dslopesdp, gsl::span coeffs +) const { + /* We're using the short hand notation for node values and slopes from + * compute_coefficients_sensi() here. See this function for documentation. + */ + int node_offset = spline_offset + ip; + realtype spk = dnodesdp[node_offset + i_node * nplist]; + realtype spk1 = dnodesdp[node_offset + (i_node + 1) * nplist]; + realtype smk = dslopesdp[node_offset + i_node * nplist]; + realtype smk1 = dslopesdp[node_offset + (i_node + 1) * nplist]; + + /* Compute the actual coefficients */ + coeffs[ip * n_spline_coefficients + 4 * i_node] = spk; + coeffs[ip * n_spline_coefficients + 4 * i_node + 1] = len * smk; + coeffs[ip * n_spline_coefficients + 4 * i_node + 2] + = 3 * (spk1 - spk) - len * (2 * smk + smk1); + coeffs[ip * n_spline_coefficients + 4 * i_node + 3] + = 2 * (spk - spk1) + len * (smk + smk1); +} + +void HermiteSpline::compute_final_value() { + /* We need to compute the final value of the spline, depending on its + * boundary condition and the extrapolation option. */ + realtype finalValue; + if (last_node_ep_ == SplineExtrapolation::constant) { + finalValue = coefficients_extrapolate[2]; + } else if (last_node_ep_ == SplineExtrapolation::linear) { + if (last_node_bc_ == SplineBoundaryCondition::zeroDerivative) { + finalValue = coefficients_extrapolate[2]; + } else if (coefficients_extrapolate[3] < 0) { + finalValue = -INFINITY; + } else if (coefficients_extrapolate[3] > 0) { + finalValue = INFINITY; + } else { + finalValue = coefficients_extrapolate[2]; + } + } else if (last_node_ep_ == SplineExtrapolation::polynomial) { + int last = 4 * (n_nodes() - 1) - 1; + if (coefficients[last] < 0) { + finalValue = -INFINITY; + } else if (coefficients[last] > 0) { + finalValue = INFINITY; + } else if (coefficients[last - 1] < 0) { + finalValue = -INFINITY; + } else if (coefficients[last - 1] > 0) { + finalValue = INFINITY; + } else if (coefficients[last - 2] < 0) { + finalValue = -INFINITY; + } else if (coefficients[last - 2] > 0) { + finalValue = INFINITY; + } else { + finalValue = coefficients[last - 3]; + } + } else { + /* Periodic: will not yield a steady state, unless the spline is the + * constant function */ + finalValue = get_node_value_scaled(0); + for (int i = 0; i < n_nodes(); i++) { + if (get_node_value_scaled(i) != finalValue + || get_node_derivative_scaled(i) != 0) { + finalValue = NAN; + break; + } + } + } + set_final_value_scaled(finalValue); +} + +void HermiteSpline::compute_final_sensitivity( + int nplist, int /*spline_offset*/, gsl::span /*dvaluesdp*/, + gsl::span /*dslopesdp*/ +) { + /* We need to compute the final value of the spline, depending on its + * boundary condition and the extrapolation option. */ + std::vector finalSensitivity(nplist, 0); + if ((last_node_ep_ == SplineExtrapolation::constant) + || (last_node_bc_ == SplineBoundaryCondition::zeroDerivative + && last_node_ep_ == SplineExtrapolation::linear)) { + for (int ip = 0; ip < nplist; ip++) + finalSensitivity[ip] = coefficients_extrapolate_sensi[4 * ip + 2]; + } else if (last_node_ep_ == SplineExtrapolation::linear) { + /* If steady state is infinity, sensitivity must be 0 + * (unless the derivative is zero and the final value will change + * abruptly from finite to +-inf) (if the derivative is constant zero in + * a neighbourhood, then the final value will not change, but this is + * impossible to determine just from the sensitivity of the derivative) + */ + int last = n_nodes() - 1; + if (get_node_derivative_scaled(last) == 0) + std::fill(finalSensitivity.begin(), finalSensitivity.end(), NAN); + } else if (last_node_ep_ == SplineExtrapolation::polynomial) { + /* Yes, that's not correct. But I don't see any good reason for + * implementing a case, which anybody with more than a dead fish + * between the ears will never use. */ + std::fill(finalSensitivity.begin(), finalSensitivity.end(), NAN); + } else { + /* Periodic: will not yield a steady state + * (unless the spline is the constant funtion, + * but even in that case sensitivity information is not able to tell us + * whether the steady state continues to exist in a neighbourhood of the + * current parameters + */ + std::fill(finalSensitivity.begin(), finalSensitivity.end(), NAN); + } + set_final_sensitivity_scaled(finalSensitivity); +} + +realtype HermiteSpline::get_value_scaled(const realtype t) const { + /* Is this a steady state computation? */ + if (std::isinf(t)) + return get_final_value_scaled(); + + /* Compute the spline value */ + int i_node; + realtype len; + + /* Are we past the last node? Extrapolate! */ + if (t > nodes_[n_nodes() - 1]) { + switch (last_node_ep_) { + case SplineExtrapolation::noExtrapolation: + throw AmiException( + "Trying to evaluate spline after last " + "spline node, but spline has been specified not to allow " + "extrapolation." + ); + + case SplineExtrapolation::constant: + return coefficients_extrapolate[2]; + + case SplineExtrapolation::linear: + return coefficients_extrapolate[2] + + t * coefficients_extrapolate[3]; + + case SplineExtrapolation::polynomial: + /* Evaluate last interpolation polynomial */ + i_node = n_nodes() - 2; + len = nodes_[i_node + 1] - nodes_[i_node]; + return evaluate_polynomial( + (t - nodes_[i_node]) / len, + gsl::make_span(coefficients).subspan(i_node * 4) + ); + + case SplineExtrapolation::periodic: + len = nodes_[n_nodes() - 1] - nodes_[0]; + return get_value(nodes_[0] + std::fmod(t - nodes_[0], len)); + + default: + throw AmiException("Unsupported SplineExtrapolation type"); + } + } + + /* Are we before the first node? Extrapolate! */ + if (t < nodes_[0]) { + switch (first_node_ep_) { + case SplineExtrapolation::noExtrapolation: + throw AmiException( + "Trying to evaluate spline before first " + "spline node, but spline has been specified not to allow " + "extrapolation." + ); + + case SplineExtrapolation::constant: + return coefficients_extrapolate[0]; + + case SplineExtrapolation::linear: + return coefficients_extrapolate[0] + + t * coefficients_extrapolate[1]; + + case SplineExtrapolation::polynomial: + /* Evaluate last interpolation polynomial */ + len = nodes_[1] - nodes_[0]; + return evaluate_polynomial((t - nodes_[0]) / len, coefficients); + + case SplineExtrapolation::periodic: + len = nodes_[n_nodes() - 1] - nodes_[0]; + return get_value( + nodes_[n_nodes() - 1] + std::fmod(t - nodes_[0], len) + ); + default: + throw AmiException("Unsupported SplineExtrapolation type"); + } + } + + /* Get the spline interval which we need */ + if (get_equidistant_spacing()) { + /* equidistant spacing: just compute the interval */ + len = nodes_[1] - nodes_[0]; + i_node = static_cast(std::trunc((t - nodes_[0]) / len)); + i_node = std::min(i_node, n_nodes() - 2); + } else { + /* no equidistant spacing: we need to iterate */ + i_node = 0; + while (nodes_[i_node + 1] < t) { + i_node++; + } + if (t == nodes_[i_node + 1]) + return get_node_value_scaled(i_node + 1); // make it exact on nodes + len = nodes_[i_node + 1] - nodes_[i_node]; + } + + /* Evaluate the interpolation polynomial */ + return evaluate_polynomial( + (t - nodes_[i_node]) / len, + gsl::make_span(coefficients).subspan(i_node * 4) + ); +} + +realtype +HermiteSpline::get_sensitivity_scaled(const realtype t, int const ip) const { + /* Is this a steady state computation? */ + if (std::isinf(t)) + return get_final_sensitivity_scaled(ip); + + /* Compute the parametric derivative of the spline value */ + int i_node; + realtype len; + + if (t > nodes_[n_nodes() - 1]) { + /* Are we past the last node? Extrapolate! */ + switch (last_node_ep_) { + case SplineExtrapolation::noExtrapolation: + throw AmiException( + "Trying to evaluate spline sensitivity " + "after last spline node, but spline has been specified " + "to not allow extrapolation." + ); + + case SplineExtrapolation::constant: + return coefficients_extrapolate_sensi[4 * ip + 2]; + + case SplineExtrapolation::linear: + return coefficients_extrapolate_sensi[4 * ip + 2] + + t * coefficients_extrapolate_sensi[4 * ip + 3]; + + case SplineExtrapolation::polynomial: + /* Evaluate last interpolation polynomial */ + i_node = n_nodes() - 2; + len = nodes_[i_node + 1] - nodes_[i_node]; + return evaluate_polynomial( + (t - nodes_[i_node]) / len, + gsl::make_span(coefficients_sensi) + .subspan(ip * (n_nodes() - 1) * 4 + i_node * 4) + ); + + case SplineExtrapolation::periodic: + len = nodes_[n_nodes() - 1] - nodes_[0]; + return get_sensitivity( + nodes_[0] + std::fmod(t - nodes_[0], len), ip + ); + default: + throw AmiException("Unsupported SplineExtrapolation type"); + } + } + + if (t < nodes_[0]) { + /* Are we before the first node? Extrapolate! */ + switch (first_node_ep_) { + case SplineExtrapolation::noExtrapolation: + throw AmiException( + "Trying to evaluate spline before first " + "spline node, but spline has been specified to not allow " + "extrapolation." + ); + + case SplineExtrapolation::constant: + return coefficients_extrapolate_sensi[4 * ip + 0]; + + case SplineExtrapolation::linear: + return coefficients_extrapolate_sensi[4 * ip + 0] + + t * coefficients_extrapolate_sensi[4 * ip + 1]; + + case SplineExtrapolation::polynomial: + /* Evaluate last interpolation polynomial */ + len = nodes_[1] - nodes_[0]; + return evaluate_polynomial( + (t - nodes_[0]) / len, gsl::make_span(coefficients_sensi) + .subspan(ip * (n_nodes() - 1) * 4) + ); + + case SplineExtrapolation::periodic: + len = nodes_[n_nodes() - 1] - nodes_[0]; + return get_sensitivity( + nodes_[n_nodes() - 1] + std::fmod(t - nodes_[0], len), ip + ); + default: + throw AmiException("Unsupported SplineExtrapolation type"); + } + } + + /* Get the spline interval which we need */ + if (get_equidistant_spacing()) { + /* equidistant spacing: just compute the interval */ + len = nodes_[1] - nodes_[0]; + i_node = static_cast(std::trunc((t - nodes_[0]) / len)); + i_node = std::min(i_node, n_nodes() - 2); + } else { + /* no equidistant spacing: we need to iterate */ + i_node = 0; + while (nodes_[i_node + 1] < t) { + i_node++; + } + len = nodes_[i_node + 1] - nodes_[i_node]; + } + + /* Evaluate the interpolation polynomial */ + return evaluate_polynomial( + (t - nodes_[i_node]) / len, + gsl::make_span(coefficients_sensi) + .subspan(ip * (n_nodes() - 1) * 4 + i_node * 4) + ); +} + +} // namespace amici diff --git a/src/steadystateproblem.cpp b/src/steadystateproblem.cpp index 3435316741..c561e6a8c3 100644 --- a/src/steadystateproblem.cpp +++ b/src/steadystateproblem.cpp @@ -51,10 +51,10 @@ SteadystateProblem::SteadystateProblem(Solver const& solver, Model const& model) , newton_step_conv_(solver.getNewtonStepSteadyStateCheck()) , check_sensi_conv_(solver.getSensiSteadyStateCheck()) { /* Check for compatibility of options */ - if (solver.getSensitivityMethod() == SensitivityMethod::forward && - solver.getSensitivityMethodPreequilibration() == - SensitivityMethod::adjoint && - solver.getSensitivityOrder() > SensitivityOrder::none) + if (solver.getSensitivityMethod() == SensitivityMethod::forward + && solver.getSensitivityMethodPreequilibration() + == SensitivityMethod::adjoint + && solver.getSensitivityOrder() > SensitivityOrder::none) throw AmiException("Preequilibration using adjoint sensitivities " "is not compatible with using forward " "sensitivities during simulation"); @@ -70,16 +70,18 @@ void SteadystateProblem::workSteadyStateProblem( findSteadyState(solver, model, it); /* Check whether state sensis still need to be computed */ - if (getSensitivityFlag(model, solver, it, - SteadyStateContext::newtonSensi)) { + if (getSensitivityFlag( + model, solver, it, SteadyStateContext::newtonSensi + )) { try { /* this might still fail, if the Jacobian is singular and simulation did not find a steady state */ newton_solver_->computeNewtonSensis(state_.sx, model, state_); - } catch (NewtonFailure const &) { + } catch (NewtonFailure const&) { throw AmiException( "Steady state sensitivity computation failed due " - "to unsuccessful factorization of RHS Jacobian"); + "to unsuccessful factorization of RHS Jacobian" + ); } } cpu_time_ = cpu_timer.elapsed_milliseconds(); @@ -109,12 +111,15 @@ void SteadystateProblem::findSteadyState( forward sensitivities ODEs is coupled. If 'integrationOnly' approach is chosen for sensitivity computation it is enforced that steady state is computed only by numerical integration as well. */ - bool turnOffNewton = solver.getNewtonMaxSteps() == 0 || ( - model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::integrationOnly && - ((it == -1 && solver.getSensitivityMethodPreequilibration() == - SensitivityMethod::forward) || solver.getSensitivityMethod() == - SensitivityMethod::forward)); + bool turnOffNewton + = solver.getNewtonMaxSteps() == 0 + || (model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::integrationOnly + && ((it == -1 + && solver.getSensitivityMethodPreequilibration() + == SensitivityMethod::forward) + || solver.getSensitivityMethod() == SensitivityMethod::forward + )); /* First, try to run the Newton solver */ if (!turnOffNewton) @@ -133,21 +138,22 @@ void SteadystateProblem::findSteadyState( handleSteadyStateFailure(); } -void SteadystateProblem::findSteadyStateByNewtonsMethod(Model &model, - bool newton_retry) { +void SteadystateProblem::findSteadyStateByNewtonsMethod( + Model& model, bool newton_retry +) { int ind = newton_retry ? 2 : 0; try { applyNewtonsMethod(model, newton_retry); steady_state_status_[ind] = SteadyStateStatus::success; - } catch (NewtonFailure const &ex) { + } catch (NewtonFailure const& ex) { /* nothing to be done */ switch (ex.error_code) { case AMICI_TOO_MUCH_WORK: steady_state_status_[ind] = SteadyStateStatus::failed_convergence; break; case AMICI_NO_STEADY_STATE: - steady_state_status_[ind] = - SteadyStateStatus::failed_too_long_simulation; + steady_state_status_[ind] + = SteadyStateStatus::failed_too_long_simulation; break; case AMICI_SINGULAR_JACOBIAN: steady_state_status_[ind] = SteadyStateStatus::failed_factorization; @@ -169,49 +175,53 @@ void SteadystateProblem::findSteadyStateBySimulation( if (it < 0) { /* Preequilibration? -> Create a new solver instance for sim */ bool integrateSensis = getSensitivityFlag( - model, solver, it, SteadyStateContext::solverCreation); + model, solver, it, SteadyStateContext::solverCreation + ); auto newtonSimSolver = createSteadystateSimSolver( - solver, model, integrateSensis, false); + solver, model, integrateSensis, false + ); runSteadystateSimulation(*newtonSimSolver, model, false); } else { /* Solver was already created, use this one */ runSteadystateSimulation(solver, model, false); } steady_state_status_[1] = SteadyStateStatus::success; - } catch (IntegrationFailure const &ex) { + } catch (IntegrationFailure const& ex) { switch (ex.error_code) { case AMICI_TOO_MUCH_WORK: steady_state_status_[1] = SteadyStateStatus::failed_convergence; - if(model.logger) + if (model.logger) model.logger->log( LogSeverity::debug, "EQUILIBRATION_FAILURE", "AMICI equilibration exceeded maximum number of" - " integration steps at t=%g.", ex.time + " integration steps at t=%g.", + ex.time ); break; case AMICI_RHSFUNC_FAIL: - steady_state_status_[1] = - SteadyStateStatus::failed_too_long_simulation; - if(model.logger) + steady_state_status_[1] + = SteadyStateStatus::failed_too_long_simulation; + if (model.logger) model.logger->log( LogSeverity::debug, "EQUILIBRATION_FAILURE", "AMICI equilibration was stopped after exceedingly" - " long simulation time at t=%g.", ex.time + " long simulation time at t=%g.", + ex.time ); break; default: steady_state_status_[1] = SteadyStateStatus::failed; - if(model.logger) + if (model.logger) model.logger->log( LogSeverity::debug, "OTHER", "AMICI equilibration failed at t=%g.", ex.time ); } - } catch (AmiException const &ex) { - if(model.logger) + } catch (AmiException const& ex) { + if (model.logger) model.logger->log( - LogSeverity::debug, "OTHER", - "AMICI equilibration failed: %s", ex.what() + LogSeverity::debug, "OTHER", "AMICI equilibration failed: %s", + ex.what() ); steady_state_status_[1] = SteadyStateStatus::failed; } @@ -225,10 +235,10 @@ void SteadystateProblem::initializeForwardProblem( if (it == -1) { /* solver was not run before, set up everything */ auto roots_found = std::vector(model.ne, 0); - model.initialize(state_.x, state_.dx, state_.sx, sdx_, - solver.getSensitivityOrder() >= - SensitivityOrder::first, - roots_found); + model.initialize( + state_.x, state_.dx, state_.sx, sdx_, + solver.getSensitivityOrder() >= SensitivityOrder::first, roots_found + ); state_.t = model.t0(); solver.setup(state_.t, &model, state_.x, state_.dx, state_.sx, sdx_); } else { @@ -253,8 +263,8 @@ bool SteadystateProblem::initializeBackwardProblem( /* note that state_ is still set from forward run */ if (bwd) { /* preequilibration */ - if (solver.getSensitivityMethodPreequilibration() != - SensitivityMethod::adjoint) + if (solver.getSensitivityMethodPreequilibration() + != SensitivityMethod::adjoint) return false; /* if not adjoint mode, there's nothing to do */ /* If we need to reinitialize solver states, this won't work yet. */ @@ -262,7 +272,8 @@ bool SteadystateProblem::initializeBackwardProblem( throw NewtonFailure( AMICI_NOT_IMPLEMENTED, "Adjoint preequilibration with reinitialization of " - "non-constant states is not yet implemented. Stopping."); + "non-constant states is not yet implemented. Stopping." + ); solver.reInit(state_.t, state_.x, state_.dx); solver.updateAndReinitStatesAndSensitivities(&model); @@ -292,13 +303,15 @@ void SteadystateProblem::computeSteadyStateQuadrature( /* Try to compute the analytical solution for quadrature algebraically */ if (sensitivityMode == SteadyStateSensitivityMode::newtonOnly - || sensitivityMode == SteadyStateSensitivityMode::integrateIfNewtonFails) + || sensitivityMode + == SteadyStateSensitivityMode::integrateIfNewtonFails) getQuadratureByLinSolve(model); /* Perform simulation */ - if (sensitivityMode == SteadyStateSensitivityMode::integrationOnly || - (sensitivityMode == SteadyStateSensitivityMode::integrateIfNewtonFails - && !hasQuadrature())) + if (sensitivityMode == SteadyStateSensitivityMode::integrationOnly + || (sensitivityMode + == SteadyStateSensitivityMode::integrateIfNewtonFails + && !hasQuadrature())) getQuadratureBySimulation(solver, model); /* If analytic solution and integration did not work, throw an Exception */ @@ -306,10 +319,11 @@ void SteadystateProblem::computeSteadyStateQuadrature( throw AmiException( "Steady state backward computation failed: Linear " "system could not be solved (possibly due to singular Jacobian), " - "and numerical integration did not equilibrate within maxsteps"); + "and numerical integration did not equilibrate within maxsteps" + ); } -void SteadystateProblem::getQuadratureByLinSolve(Model &model) { +void SteadystateProblem::getQuadratureByLinSolve(Model& model) { /* Computes the integral over the adjoint state xB: If the Jacobian has full rank, this has an analytical solution, since d/dt[ xB(t) ] = JB^T(x(t), p) xB(t) = JB^T(x_ss, p) xB(t) @@ -334,7 +348,7 @@ void SteadystateProblem::getQuadratureByLinSolve(Model &model) { /* Finalize by setting adjoint state to zero (its steady state) */ xB_.zero(); - } catch (NewtonFailure const &) { + } catch (NewtonFailure const&) { hasQuadrature_ = false; } } @@ -358,7 +372,7 @@ void SteadystateProblem::getQuadratureBySimulation( try { runSteadystateSimulation(*simSolver, model, true); hasQuadrature_ = true; - } catch (NewtonFailure const &) { + } catch (NewtonFailure const&) { hasQuadrature_ = false; } } @@ -376,8 +390,9 @@ void SteadystateProblem::getQuadratureBySimulation( throw AmiException(errorString.c_str()); } -void SteadystateProblem::writeErrorString(std::string *errorString, - SteadyStateStatus status) const { +void SteadystateProblem::writeErrorString( + std::string* errorString, SteadyStateStatus status +) const { /* write error message according to steady state status */ switch (status) { case SteadyStateStatus::failed_too_long_simulation: @@ -411,44 +426,43 @@ bool SteadystateProblem::getSensitivityFlag( bool preequilibration = (it == -1); /* Have we maybe already computed forward sensitivities? */ - bool forwardSensisAlreadyComputed = - solver.getSensitivityOrder() >= SensitivityOrder::first && - steady_state_status_[1] == SteadyStateStatus::success && - (model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::integrationOnly || - model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::integrateIfNewtonFails); - - bool simulationStartedInSteadystate = - steady_state_status_[0] == SteadyStateStatus::success && - numsteps_[0] == 0; + bool forwardSensisAlreadyComputed + = solver.getSensitivityOrder() >= SensitivityOrder::first + && steady_state_status_[1] == SteadyStateStatus::success + && (model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::integrationOnly + || model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::integrateIfNewtonFails); + + bool simulationStartedInSteadystate + = steady_state_status_[0] == SteadyStateStatus::success + && numsteps_[0] == 0; /* Do we need forward sensis for postequilibration? */ - bool needForwardSensisPosteq = - !preequilibration && !forwardSensisAlreadyComputed && - solver.getSensitivityOrder() >= SensitivityOrder::first && - solver.getSensitivityMethod() == SensitivityMethod::forward; + bool needForwardSensisPosteq + = !preequilibration && !forwardSensisAlreadyComputed + && solver.getSensitivityOrder() >= SensitivityOrder::first + && solver.getSensitivityMethod() == SensitivityMethod::forward; /* Do we need forward sensis for preequilibration? */ - bool needForwardSensisPreeq = - preequilibration && !forwardSensisAlreadyComputed && - solver.getSensitivityMethodPreequilibration() == - SensitivityMethod::forward && - solver.getSensitivityOrder() >= SensitivityOrder::first; + bool needForwardSensisPreeq + = preequilibration && !forwardSensisAlreadyComputed + && solver.getSensitivityMethodPreequilibration() + == SensitivityMethod::forward + && solver.getSensitivityOrder() >= SensitivityOrder::first; /* Do we need to do the linear system solve to get forward sensitivities? */ - bool needForwardSensisNewton = - (needForwardSensisPreeq || needForwardSensisPosteq) && - !simulationStartedInSteadystate; + bool needForwardSensisNewton + = (needForwardSensisPreeq || needForwardSensisPosteq) + && !simulationStartedInSteadystate; /* When we're creating a new solver object */ - bool needForwardSensiAtCreation = - needForwardSensisPreeq && - (model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::integrationOnly || - model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::integrateIfNewtonFails - ); + bool needForwardSensiAtCreation + = needForwardSensisPreeq + && (model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::integrationOnly + || model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::integrateIfNewtonFails); /* Check if we need to store sensis */ switch (context) { @@ -456,8 +470,8 @@ bool SteadystateProblem::getSensitivityFlag( return needForwardSensisNewton; case SteadyStateContext::sensiStorage: - return needForwardSensisNewton || forwardSensisAlreadyComputed || - simulationStartedInSteadystate; + return needForwardSensisNewton || forwardSensisAlreadyComputed + || simulationStartedInSteadystate; case SteadyStateContext::solverCreation: return needForwardSensiAtCreation; @@ -483,12 +497,13 @@ realtype SteadystateProblem::getWrmsNorm( /* ewt = 1/ewt (ewt = 1/(rtol*x+atol)) */ N_VInv(ewt.getNVector(), ewt.getNVector()); /* wrms = sqrt(sum((xdot/ewt)**2)/n) where n = size of state vector */ - return N_VWrmsNorm(const_cast(xdot.getNVector()), - ewt.getNVector()); + return N_VWrmsNorm( + const_cast(xdot.getNVector()), ewt.getNVector() + ); } -realtype SteadystateProblem::getWrms(Model &model, - SensitivityMethod sensi_method) { +realtype +SteadystateProblem::getWrms(Model& model, SensitivityMethod sensi_method) { realtype wrms = INFINITY; if (sensi_method == SensitivityMethod::adjoint) { /* In the adjoint case, only xQB contributes to the gradient, the exact @@ -500,7 +515,8 @@ realtype SteadystateProblem::getWrms(Model &model, throw NewtonFailure( AMICI_NOT_IMPLEMENTED, "Newton type convergence check is not implemented for adjoint " - "steady state computations. Stopping."); + "steady state computations. Stopping." + ); wrms = getWrmsNorm(xQB_, xQBdot_, atol_quad_, rtol_quad_, ewtQB_); } else { /* If we're doing a forward simulation (with or without sensitivities: @@ -509,13 +525,14 @@ realtype SteadystateProblem::getWrms(Model &model, getNewtonStep(model); else updateRightHandSide(model); - wrms = getWrmsNorm(state_.x, newton_step_conv_ ? delta_ : xdot_, - atol_, rtol_, ewt_); + wrms = getWrmsNorm( + state_.x, newton_step_conv_ ? delta_ : xdot_, atol_, rtol_, ewt_ + ); } return wrms; } -realtype SteadystateProblem::getWrmsFSA(Model &model) { +realtype SteadystateProblem::getWrmsFSA(Model& model) { /* Forward sensitivities: Compute weighted error norm for their RHS */ realtype wrms = 0.0; @@ -525,12 +542,13 @@ realtype SteadystateProblem::getWrmsFSA(Model &model) { xdot_updated_ = false; for (int ip = 0; ip < model.nplist(); ++ip) { - model.fsxdot(state_.t, state_.x, state_.dx, ip, state_.sx[ip], - state_.dx, xdot_); + model.fsxdot( + state_.t, state_.x, state_.dx, ip, state_.sx[ip], state_.dx, xdot_ + ); if (newton_step_conv_) newton_solver_->solveLinearSystem(xdot_); - wrms = - getWrmsNorm(state_.sx[ip], xdot_, atol_sensi_, rtol_sensi_, ewt_); + wrms + = getWrmsNorm(state_.sx[ip], xdot_, atol_sensi_, rtol_sensi_, ewt_); /* ideally this function would report the maximum of all wrms over all ip, but for practical purposes we can just report the wrms for the first ip where we know that the convergence threshold is not @@ -545,14 +563,16 @@ realtype SteadystateProblem::getWrmsFSA(Model &model) { bool SteadystateProblem::checkSteadyStateSuccess() const { /* Did one of the attempts yield s steady state? */ - return std::any_of(steady_state_status_.begin(), steady_state_status_.end(), - [](SteadyStateStatus status) { - return status == SteadyStateStatus::success; - }); + return std::any_of( + steady_state_status_.begin(), steady_state_status_.end(), + [](SteadyStateStatus status) { + return status == SteadyStateStatus::success; + } + ); } -void SteadystateProblem::applyNewtonsMethod(Model &model, bool newton_retry) { - int &i_newtonstep = numsteps_.at(newton_retry ? 2 : 0); +void SteadystateProblem::applyNewtonsMethod(Model& model, bool newton_retry) { + int& i_newtonstep = numsteps_.at(newton_retry ? 2 : 0); i_newtonstep = 0; gamma_ = 1.0; bool update_direction = true; @@ -579,8 +599,10 @@ void SteadystateProblem::applyNewtonsMethod(Model &model, bool newton_retry) { } /* Try step with new gamma_/delta_ */ - linearSum(1.0, x_old_, gamma_, - update_direction ? delta_ : delta_old_, state_.x); + linearSum( + 1.0, x_old_, gamma_, update_direction ? delta_ : delta_old_, + state_.x + ); flagUpdatedState(); /* Compute new xdot and residuals */ @@ -608,7 +630,7 @@ void SteadystateProblem::applyNewtonsMethod(Model &model, bool newton_retry) { throw NewtonFailure(AMICI_TOO_MUCH_WORK, "applyNewtonsMethod"); } -bool SteadystateProblem::makePositiveAndCheckConvergence(Model &model) { +bool SteadystateProblem::makePositiveAndCheckConvergence(Model& model) { /* Ensure positivity of the found state and recheck if the convergence still holds */ auto nonnegative = model.getStateIsNonNegative(); @@ -632,9 +654,11 @@ bool SteadystateProblem::updateDampingFactor(bool step_successful) { gamma_ = gamma_ / 4.0; if (gamma_ < damping_factor_lower_bound_) - throw NewtonFailure(AMICI_DAMPING_FACTOR_ERROR, - "Newton solver failed: the damping factor " - "reached its lower bound"); + throw NewtonFailure( + AMICI_DAMPING_FACTOR_ERROR, + "Newton solver failed: the damping factor " + "reached its lower bound" + ); return step_successful; } @@ -651,21 +675,21 @@ void SteadystateProblem::runSteadystateSimulation( /* Do we also have to check for convergence of sensitivities? */ SensitivityMethod sensitivityFlag = SensitivityMethod::none; - if (solver.getSensitivityOrder() > SensitivityOrder::none && - solver.getSensitivityMethod() == SensitivityMethod::forward) + if (solver.getSensitivityOrder() > SensitivityOrder::none + && solver.getSensitivityMethod() == SensitivityMethod::forward) sensitivityFlag = SensitivityMethod::forward; /* If flag for forward sensitivity computation by simulation is not set, disable forward sensitivity integration. Sensitivities will be computed by newtonsolver.computeNewtonSensis then */ - if (model.getSteadyStateSensitivityMode() == - SteadyStateSensitivityMode::newtonOnly) { + if (model.getSteadyStateSensitivityMode() + == SteadyStateSensitivityMode::newtonOnly) { solver.switchForwardSensisOff(); sensitivityFlag = SensitivityMethod::none; } if (backward) sensitivityFlag = SensitivityMethod::adjoint; - int &sim_steps = backward ? numstepsB_ : numsteps_.at(1); + int& sim_steps = backward ? numstepsB_ : numsteps_.at(1); int convergence_check_frequency = 1; @@ -678,7 +702,7 @@ void SteadystateProblem::runSteadystateSimulation( // start in steady state) wrms_ = getWrms(model, sensitivityFlag); if (wrms_ < conv_thresh) { - if(check_sensi_conv_ + if (check_sensi_conv_ && sensitivityFlag == SensitivityMethod::forward) { updateSensiSimulation(solver); // getWrms needs to be called before getWrmsFSA @@ -711,8 +735,9 @@ void SteadystateProblem::runSteadystateSimulation( if (backward) { solver.writeSolution(&state_.t, xB_, state_.dx, state_.sx, xQ_); } else { - solver.writeSolution(&state_.t, state_.x, state_.dx, state_.sx, - xQ_); + solver.writeSolution( + &state_.t, state_.x, state_.dx, state_.sx, xQ_ + ); flagUpdatedState(); } } @@ -751,11 +776,13 @@ std::unique_ptr SteadystateProblem::createSteadystateSimSolver( sim_solver->setup(model.t0(), &model, state_.x, state_.dx, state_.sx, sdx_); if (backward) { sim_solver->setup(model.t0(), &model, xB_, xB_, state_.sx, sdx_); - sim_solver->setupSteadystate(model.t0(), &model, state_.x, state_.dx, - xB_, xB_, xQ_); + sim_solver->setupSteadystate( + model.t0(), &model, state_.x, state_.dx, xB_, xB_, xQ_ + ); } else { - sim_solver->setup(model.t0(), &model, state_.x, state_.dx, state_.sx, - sdx_); + sim_solver->setup( + model.t0(), &model, state_.x, state_.dx, state_.sx, sdx_ + ); } return sim_solver; @@ -773,8 +800,9 @@ void SteadystateProblem::computeQBfromQ( /* fill dxdotdp with current values */ auto const& plist = model.getParameterList(); model.fdxdotdp(state_.t, state_.x, state_.dx); - model.get_dxdotdp_full().multiply(yQB.getNVector(), yQ.getNVector(), - plist, true); + model.get_dxdotdp_full().multiply( + yQB.getNVector(), yQ.getNVector(), plist, true + ); } else { for (int ip = 0; ip < model.nplist(); ++ip) yQB[ip] = dotProd(yQ, model.get_dxdotdp()[ip]); @@ -787,7 +815,8 @@ void SteadystateProblem::getAdjointUpdates(Model& model, ExpData const& edata) { if (std::isinf(model.getTimepoint(it))) { model.getAdjointStateObservableUpdate( slice(dJydx_, it, model.nx_solver * model.nJ), it, state_.x, - edata); + edata + ); for (int ix = 0; ix < model.nxtrue_solver; ix++) xB_[ix] += dJydx_[ix + it * model.nx_solver]; } @@ -807,14 +836,14 @@ void SteadystateProblem::updateSensiSimulation(Solver const& solver) { sensis_updated_ = true; } -void SteadystateProblem::updateRightHandSide(Model &model) { +void SteadystateProblem::updateRightHandSide(Model& model) { if (xdot_updated_) return; model.fxdot(state_.t, state_.x, state_.dx, xdot_); xdot_updated_ = true; } -void SteadystateProblem::getNewtonStep(Model &model) { +void SteadystateProblem::getNewtonStep(Model& model) { if (delta_updated_) return; updateRightHandSide(model); diff --git a/src/sundials_linsol_wrapper.cpp b/src/sundials_linsol_wrapper.cpp index ae4333985c..de5d4f1d61 100644 --- a/src/sundials_linsol_wrapper.cpp +++ b/src/sundials_linsol_wrapper.cpp @@ -7,14 +7,15 @@ namespace amici { -SUNLinSolWrapper::SUNLinSolWrapper(SUNLinearSolver linsol) : solver_(linsol) {} +SUNLinSolWrapper::SUNLinSolWrapper(SUNLinearSolver linsol) + : solver_(linsol) {} SUNLinSolWrapper::~SUNLinSolWrapper() { if (solver_) SUNLinSolFree(solver_); } -SUNLinSolWrapper::SUNLinSolWrapper(SUNLinSolWrapper &&other) noexcept { +SUNLinSolWrapper::SUNLinSolWrapper(SUNLinSolWrapper&& other) noexcept { std::swap(solver_, other.solver_); } @@ -37,9 +38,12 @@ void SUNLinSolWrapper::setup(SUNMatrix A) const { throw AmiException("Solver setup failed with code %d", res); } -void SUNLinSolWrapper::setup(const SUNMatrixWrapper& A) const { return setup(A.get()); } +void SUNLinSolWrapper::setup(SUNMatrixWrapper const& A) const { + return setup(A.get()); +} -int SUNLinSolWrapper::Solve(SUNMatrix A, N_Vector x, N_Vector b, realtype tol) const { +int SUNLinSolWrapper::Solve(SUNMatrix A, N_Vector x, N_Vector b, realtype tol) + const { return SUNLinSolSolve(solver_, A, x, b, tol); } @@ -47,7 +51,7 @@ long SUNLinSolWrapper::getLastFlag() const { return gsl::narrow(SUNLinSolLastFlag(solver_)); } -int SUNLinSolWrapper::space(long *lenrwLS, long *leniwLS) const { +int SUNLinSolWrapper::space(long* lenrwLS, long* leniwLS) const { return SUNLinSolSpace(solver_, lenrwLS, leniwLS); } @@ -61,12 +65,12 @@ SUNNonLinSolWrapper::~SUNNonLinSolWrapper() { SUNNonlinSolFree(solver); } -SUNNonLinSolWrapper::SUNNonLinSolWrapper(SUNNonLinSolWrapper &&other) noexcept { +SUNNonLinSolWrapper::SUNNonLinSolWrapper(SUNNonLinSolWrapper&& other) noexcept { std::swap(solver, other.solver); } -SUNNonLinSolWrapper &SUNNonLinSolWrapper:: -operator=(SUNNonLinSolWrapper &&other) noexcept { +SUNNonLinSolWrapper& SUNNonLinSolWrapper::operator=(SUNNonLinSolWrapper&& other +) noexcept { std::swap(solver, other.solver); return *this; } @@ -77,15 +81,17 @@ SUNNonlinearSolver_Type SUNNonLinSolWrapper::getType() const { return SUNNonlinSolGetType(solver); } -int SUNNonLinSolWrapper::setup(N_Vector y, void *mem) { +int SUNNonLinSolWrapper::setup(N_Vector y, void* mem) { auto res = SUNNonlinSolSetup(solver, y, mem); if (res != SUN_NLS_SUCCESS) throw AmiException("Nonlinear solver setup failed with code %d", res); return res; } -int SUNNonLinSolWrapper::Solve(N_Vector y0, N_Vector y, N_Vector w, - realtype tol, bool callLSetup, void *mem) { +int SUNNonLinSolWrapper::Solve( + N_Vector y0, N_Vector y, N_Vector w, realtype tol, bool callLSetup, + void* mem +) { return SUNNonlinSolSolve(solver, y0, y, w, tol, callLSetup, mem); } @@ -101,8 +107,9 @@ int SUNNonLinSolWrapper::setLSolveFn(SUNNonlinSolLSolveFn SolveFn) { return SUNNonlinSolSetLSolveFn(solver, SolveFn); } -int SUNNonLinSolWrapper::setConvTestFn(SUNNonlinSolConvTestFn CTestFn, - void* ctest_data) { +int SUNNonLinSolWrapper::setConvTestFn( + SUNNonlinSolConvTestFn CTestFn, void* ctest_data +) { return SUNNonlinSolSetConvTestFn(solver, CTestFn, ctest_data); } @@ -132,8 +139,9 @@ long SUNNonLinSolWrapper::getNumConvFails() const { long int nconvfails = -1; auto res = SUNNonlinSolGetNumConvFails(solver, &nconvfails); if (res != SUN_NLS_SUCCESS) { - throw AmiException("SUNNonlinSolGetNumConvFails failed with code %d", - res); + throw AmiException( + "SUNNonlinSolGetNumConvFails failed with code %d", res + ); } return nconvfails; } @@ -142,7 +150,8 @@ void SUNNonLinSolWrapper::initialize() { int status = SUNNonlinSolInitialize(solver); if (status != SUN_NLS_SUCCESS) throw AmiException( - "Nonlinear solver initialization failed with code %d", status); + "Nonlinear solver initialization failed with code %d", status + ); } SUNLinSolBand::SUNLinSolBand(N_Vector x, SUNMatrix A) @@ -151,18 +160,17 @@ SUNLinSolBand::SUNLinSolBand(N_Vector x, SUNMatrix A) throw AmiException("Failed to create solver."); } -SUNLinSolBand::SUNLinSolBand(const AmiVector &x, int ubw, int lbw) : - A_(SUNMatrixWrapper(x.getLength(), ubw, lbw)) { +SUNLinSolBand::SUNLinSolBand(AmiVector const& x, int ubw, int lbw) + : A_(SUNMatrixWrapper(x.getLength(), ubw, lbw)) { solver_ = SUNLinSol_Band(const_cast(x.getNVector()), A_.get()); if (!solver_) throw AmiException("Failed to create solver."); - } SUNMatrix SUNLinSolBand::getMatrix() const { return A_.get(); } -SUNLinSolDense::SUNLinSolDense(const AmiVector &x) : - A_(SUNMatrixWrapper(x.getLength(), x.getLength())) { +SUNLinSolDense::SUNLinSolDense(AmiVector const& x) + : A_(SUNMatrixWrapper(x.getLength(), x.getLength())) { solver_ = SUNLinSol_Dense(const_cast(x.getNVector()), A_.get()); if (!solver_) throw AmiException("Failed to create solver."); @@ -176,9 +184,10 @@ SUNLinSolKLU::SUNLinSolKLU(N_Vector x, SUNMatrix A) throw AmiException("Failed to create solver."); } -SUNLinSolKLU::SUNLinSolKLU(const AmiVector &x, int nnz, int sparsetype, - StateOrdering ordering) : - A_(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) { +SUNLinSolKLU::SUNLinSolKLU( + AmiVector const& x, int nnz, int sparsetype, StateOrdering ordering +) + : A_(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) { solver_ = SUNLinSol_KLU(const_cast(x.getNVector()), A_.get()); if (!solver_) throw AmiException("Failed to create solver."); @@ -206,12 +215,13 @@ SUNLinSolPCG::SUNLinSolPCG(N_Vector y, int pretype, int maxl) throw AmiException("Failed to create solver."); } -int SUNLinSolPCG::setATimes(void *A_data, ATimesFn ATimes) { +int SUNLinSolPCG::setATimes(void* A_data, ATimesFn ATimes) { return SUNLinSolSetATimes_PCG(solver_, A_data, ATimes); } -int SUNLinSolPCG::setPreconditioner(void *P_data, PSetupFn Pset, - PSolveFn Psol) { +int SUNLinSolPCG::setPreconditioner( + void* P_data, PSetupFn Pset, PSolveFn Psol +) { return SUNLinSolSetPreconditioner_PCG(solver_, P_data, Pset, Psol); } @@ -221,7 +231,9 @@ int SUNLinSolPCG::setScalingVectors(N_Vector s, N_Vector nul) { int SUNLinSolPCG::getNumIters() const { return SUNLinSolNumIters_PCG(solver_); } -realtype SUNLinSolPCG::getResNorm() const { return SUNLinSolResNorm_PCG(solver_); } +realtype SUNLinSolPCG::getResNorm() const { + return SUNLinSolResNorm_PCG(solver_); +} N_Vector SUNLinSolPCG::getResid() const { return SUNLinSolResid_PCG(solver_); } @@ -231,18 +243,20 @@ SUNLinSolSPBCGS::SUNLinSolSPBCGS(N_Vector x, int pretype, int maxl) throw AmiException("Failed to create solver."); } -SUNLinSolSPBCGS::SUNLinSolSPBCGS(const AmiVector &x, int pretype, int maxl) { - solver_ = SUNLinSol_SPBCGS(const_cast(x.getNVector()), pretype, maxl); +SUNLinSolSPBCGS::SUNLinSolSPBCGS(AmiVector const& x, int pretype, int maxl) { + solver_ + = SUNLinSol_SPBCGS(const_cast(x.getNVector()), pretype, maxl); if (!solver_) throw AmiException("Failed to create solver."); } -int SUNLinSolSPBCGS::setATimes(void *A_data, ATimesFn ATimes) { +int SUNLinSolSPBCGS::setATimes(void* A_data, ATimesFn ATimes) { return SUNLinSolSetATimes_SPBCGS(solver_, A_data, ATimes); } -int SUNLinSolSPBCGS::setPreconditioner(void *P_data, PSetupFn Pset, - PSolveFn Psol) { +int SUNLinSolSPBCGS::setPreconditioner( + void* P_data, PSetupFn Pset, PSolveFn Psol +) { return SUNLinSolSetPreconditioner_SPBCGS(solver_, P_data, Pset, Psol); } @@ -250,26 +264,33 @@ int SUNLinSolSPBCGS::setScalingVectors(N_Vector s, N_Vector nul) { return SUNLinSolSetScalingVectors_SPBCGS(solver_, s, nul); } -int SUNLinSolSPBCGS::getNumIters() const { return SUNLinSolNumIters_SPBCGS(solver_); } +int SUNLinSolSPBCGS::getNumIters() const { + return SUNLinSolNumIters_SPBCGS(solver_); +} realtype SUNLinSolSPBCGS::getResNorm() const { return SUNLinSolResNorm_SPBCGS(solver_); } -N_Vector SUNLinSolSPBCGS::getResid() const { return SUNLinSolResid_SPBCGS(solver_); } +N_Vector SUNLinSolSPBCGS::getResid() const { + return SUNLinSolResid_SPBCGS(solver_); +} -SUNLinSolSPFGMR::SUNLinSolSPFGMR(const AmiVector &x, int pretype, int maxl) - : SUNLinSolWrapper(SUNLinSol_SPFGMR(const_cast(x.getNVector()), pretype, maxl)) { +SUNLinSolSPFGMR::SUNLinSolSPFGMR(AmiVector const& x, int pretype, int maxl) + : SUNLinSolWrapper( + SUNLinSol_SPFGMR(const_cast(x.getNVector()), pretype, maxl) + ) { if (!solver_) throw AmiException("Failed to create solver."); } -int SUNLinSolSPFGMR::setATimes(void *A_data, ATimesFn ATimes) { +int SUNLinSolSPFGMR::setATimes(void* A_data, ATimesFn ATimes) { return SUNLinSolSetATimes_SPFGMR(solver_, A_data, ATimes); } -int SUNLinSolSPFGMR::setPreconditioner(void *P_data, PSetupFn Pset, - PSolveFn Psol) { +int SUNLinSolSPFGMR::setPreconditioner( + void* P_data, PSetupFn Pset, PSolveFn Psol +) { return SUNLinSolSetPreconditioner_SPFGMR(solver_, P_data, Pset, Psol); } @@ -277,27 +298,33 @@ int SUNLinSolSPFGMR::setScalingVectors(N_Vector s, N_Vector nul) { return SUNLinSolSetScalingVectors_SPFGMR(solver_, s, nul); } -int SUNLinSolSPFGMR::getNumIters() const { return SUNLinSolNumIters_SPFGMR(solver_); } +int SUNLinSolSPFGMR::getNumIters() const { + return SUNLinSolNumIters_SPFGMR(solver_); +} realtype SUNLinSolSPFGMR::getResNorm() const { return SUNLinSolResNorm_SPFGMR(solver_); } -N_Vector SUNLinSolSPFGMR::getResid() const { return SUNLinSolResid_SPFGMR(solver_); } +N_Vector SUNLinSolSPFGMR::getResid() const { + return SUNLinSolResid_SPFGMR(solver_); +} -SUNLinSolSPGMR::SUNLinSolSPGMR(const AmiVector &x, int pretype, int maxl) - : SUNLinSolWrapper(SUNLinSol_SPGMR(const_cast(x.getNVector()), - pretype, maxl)) { +SUNLinSolSPGMR::SUNLinSolSPGMR(AmiVector const& x, int pretype, int maxl) + : SUNLinSolWrapper( + SUNLinSol_SPGMR(const_cast(x.getNVector()), pretype, maxl) + ) { if (!solver_) throw AmiException("Failed to create solver."); } -int SUNLinSolSPGMR::setATimes(void *A_data, ATimesFn ATimes) { +int SUNLinSolSPGMR::setATimes(void* A_data, ATimesFn ATimes) { return SUNLinSolSetATimes_SPGMR(solver_, A_data, ATimes); } -int SUNLinSolSPGMR::setPreconditioner(void *P_data, PSetupFn Pset, - PSolveFn Psol) { +int SUNLinSolSPGMR::setPreconditioner( + void* P_data, PSetupFn Pset, PSolveFn Psol +) { return SUNLinSolSetPreconditioner_SPGMR(solver_, P_data, Pset, Psol); } @@ -305,11 +332,17 @@ int SUNLinSolSPGMR::setScalingVectors(N_Vector s, N_Vector nul) { return SUNLinSolSetScalingVectors_SPGMR(solver_, s, nul); } -int SUNLinSolSPGMR::getNumIters() const { return SUNLinSolNumIters_SPGMR(solver_); } +int SUNLinSolSPGMR::getNumIters() const { + return SUNLinSolNumIters_SPGMR(solver_); +} -realtype SUNLinSolSPGMR::getResNorm() const { return SUNLinSolResNorm_SPGMR(solver_); } +realtype SUNLinSolSPGMR::getResNorm() const { + return SUNLinSolResNorm_SPGMR(solver_); +} -N_Vector SUNLinSolSPGMR::getResid() const { return SUNLinSolResid_SPGMR(solver_); } +N_Vector SUNLinSolSPGMR::getResid() const { + return SUNLinSolResid_SPGMR(solver_); +} SUNLinSolSPTFQMR::SUNLinSolSPTFQMR(N_Vector x, int pretype, int maxl) : SUNLinSolWrapper(SUNLinSol_SPTFQMR(x, pretype, maxl)) { @@ -317,18 +350,21 @@ SUNLinSolSPTFQMR::SUNLinSolSPTFQMR(N_Vector x, int pretype, int maxl) throw AmiException("Failed to create solver."); } -SUNLinSolSPTFQMR::SUNLinSolSPTFQMR(const AmiVector &x, int pretype, int maxl) { - solver_ = SUNLinSol_SPTFQMR(const_cast(x.getNVector()), pretype, maxl); +SUNLinSolSPTFQMR::SUNLinSolSPTFQMR(AmiVector const& x, int pretype, int maxl) { + solver_ = SUNLinSol_SPTFQMR( + const_cast(x.getNVector()), pretype, maxl + ); if (!solver_) throw AmiException("Failed to create solver."); } -int SUNLinSolSPTFQMR::setATimes(void *A_data, ATimesFn ATimes) { +int SUNLinSolSPTFQMR::setATimes(void* A_data, ATimesFn ATimes) { return SUNLinSolSetATimes_SPTFQMR(solver_, A_data, ATimes); } -int SUNLinSolSPTFQMR::setPreconditioner(void *P_data, PSetupFn Pset, - PSolveFn Psol) { +int SUNLinSolSPTFQMR::setPreconditioner( + void* P_data, PSetupFn Pset, PSolveFn Psol +) { return SUNLinSolSetPreconditioner_SPTFQMR(solver_, P_data, Pset, Psol); } @@ -344,11 +380,12 @@ realtype SUNLinSolSPTFQMR::getResNorm() const { return SUNLinSolResNorm_SPTFQMR(solver_); } -N_Vector SUNLinSolSPTFQMR::getResid() const { return SUNLinSolResid_SPTFQMR(solver_); } +N_Vector SUNLinSolSPTFQMR::getResid() const { + return SUNLinSolResid_SPTFQMR(solver_); +} SUNNonLinSolNewton::SUNNonLinSolNewton(N_Vector x) - : SUNNonLinSolWrapper(SUNNonlinSol_Newton(x)) { -} + : SUNNonLinSolWrapper(SUNNonlinSol_Newton(x)) {} SUNNonLinSolNewton::SUNNonLinSolNewton(int count, N_Vector x) : SUNNonLinSolWrapper(SUNNonlinSol_NewtonSens(count, x)) { @@ -356,7 +393,7 @@ SUNNonLinSolNewton::SUNNonLinSolNewton(int count, N_Vector x) throw(AmiException("SUNNonlinSol_NewtonSens failed")); } -int SUNNonLinSolNewton::getSysFn(SUNNonlinSolSysFn *SysFn) const { +int SUNNonLinSolNewton::getSysFn(SUNNonlinSolSysFn* SysFn) const { return SUNNonlinSolGetSysFn_Newton(solver, SysFn); } @@ -364,31 +401,32 @@ SUNNonLinSolFixedPoint::SUNNonLinSolFixedPoint(const_N_Vector x, int m) : SUNNonLinSolWrapper(SUNNonlinSol_FixedPoint(const_cast(x), m)) { } -SUNNonLinSolFixedPoint::SUNNonLinSolFixedPoint(int count, const_N_Vector x, int m) +SUNNonLinSolFixedPoint::SUNNonLinSolFixedPoint( + int count, const_N_Vector x, int m +) : SUNNonLinSolWrapper( - SUNNonlinSol_FixedPointSens(count, const_cast(x), m)) { -} + SUNNonlinSol_FixedPointSens(count, const_cast(x), m) + ) {} -int SUNNonLinSolFixedPoint::getSysFn(SUNNonlinSolSysFn *SysFn) const { +int SUNNonLinSolFixedPoint::getSysFn(SUNNonlinSolSysFn* SysFn) const { return SUNNonlinSolGetSysFn_FixedPoint(solver, SysFn); } #ifdef SUNDIALS_SUPERLUMT SUNLinSolSuperLUMT::SUNLinSolSuperLUMT(N_Vector x, SUNMatrix A, int numThreads) - : SUNLinSolWrapper(SUNLinSol_SuperLUMT(x, A, numThreads)) -{ + : SUNLinSolWrapper(SUNLinSol_SuperLUMT(x, A, numThreads)) { if (!solver) throw AmiException("Failed to create solver."); } SUNLinSolSuperLUMT::SUNLinSolSuperLUMT( - const AmiVector &x, int nnz, int sparsetype, - SUNLinSolSuperLUMT::StateOrdering ordering) - : A(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) -{ + AmiVector const& x, int nnz, int sparsetype, + SUNLinSolSuperLUMT::StateOrdering ordering +) + : A(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) { int numThreads = 1; - if(auto env = std::getenv("AMICI_SUPERLUMT_NUM_THREADS")) { + if (auto env = std::getenv("AMICI_SUPERLUMT_NUM_THREADS")) { numThreads = std::max(1, std::stoi(env)); } @@ -399,29 +437,27 @@ SUNLinSolSuperLUMT::SUNLinSolSuperLUMT( setOrdering(ordering); } -SUNLinSolSuperLUMT::SUNLinSolSuperLUMT(const AmiVector &x, int nnz, - int sparsetype, StateOrdering ordering, - int numThreads) - : A(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) -{ - solver = SUNLinSol_SuperLUMT(x.getNVector(), A.get(), numThreads); - if (!solver) - throw AmiException("Failed to create solver."); - - setOrdering(ordering); -} +SUNLinSolSuperLUMT::SUNLinSolSuperLUMT( + AmiVector const& x, int nnz, int sparsetype, StateOrdering ordering, + int numThreads +) + : A(SUNMatrixWrapper(x.getLength(), x.getLength(), nnz, sparsetype)) { + solver = SUNLinSol_SuperLUMT(x.getNVector(), A.get(), numThreads); + if (!solver) + throw AmiException("Failed to create solver."); -SUNMatrix SUNLinSolSuperLUMT::getMatrix() const -{ - return A.get(); + setOrdering(ordering); } +SUNMatrix SUNLinSolSuperLUMT::getMatrix() const { return A.get(); } -void SUNLinSolSuperLUMT::setOrdering(StateOrdering ordering) -{ - auto status = SUNLinSol_SuperLUMTSetOrdering(solver, static_cast(ordering)); +void SUNLinSolSuperLUMT::setOrdering(StateOrdering ordering) { + auto status + = SUNLinSol_SuperLUMTSetOrdering(solver, static_cast(ordering)); if (status != SUNLS_SUCCESS) - throw AmiException("SUNLinSol_SuperLUMTSetOrdering failed with %d", status); + throw AmiException( + "SUNLinSol_SuperLUMTSetOrdering failed with %d", status + ); } #endif diff --git a/src/sundials_matrix_wrapper.cpp b/src/sundials_matrix_wrapper.cpp index 490e1bc9d3..b5c7300628 100644 --- a/src/sundials_matrix_wrapper.cpp +++ b/src/sundials_matrix_wrapper.cpp @@ -3,16 +3,18 @@ #include -#include // bad_alloc -#include +#include // bad_alloc #include // invalid_argument and domain_error +#include namespace amici { -SUNMatrixWrapper::SUNMatrixWrapper(sunindextype M, sunindextype N, - sunindextype NNZ, int sparsetype) - : matrix_(SUNSparseMatrix(M, N, NNZ, sparsetype)), id_(SUNMATRIX_SPARSE), - sparsetype_(sparsetype) { +SUNMatrixWrapper::SUNMatrixWrapper( + sunindextype M, sunindextype N, sunindextype NNZ, int sparsetype +) + : matrix_(SUNSparseMatrix(M, N, NNZ, sparsetype)) + , id_(SUNMATRIX_SPARSE) + , sparsetype_(sparsetype) { if (sparsetype != CSC_MAT && sparsetype != CSR_MAT) throw std::invalid_argument("Invalid sparsetype. Must be CSC_MAT or " @@ -29,7 +31,8 @@ SUNMatrixWrapper::SUNMatrixWrapper(sunindextype M, sunindextype N, } SUNMatrixWrapper::SUNMatrixWrapper(sunindextype M, sunindextype N) - : matrix_(SUNDenseMatrix(M, N)), id_(SUNMATRIX_DENSE) { + : matrix_(SUNDenseMatrix(M, N)) + , id_(SUNMATRIX_DENSE) { if (M && N && !matrix_) throw std::bad_alloc(); @@ -38,17 +41,21 @@ SUNMatrixWrapper::SUNMatrixWrapper(sunindextype M, sunindextype N) assert(N == columns() || !matrix_); } -SUNMatrixWrapper::SUNMatrixWrapper(sunindextype M, sunindextype ubw, - sunindextype lbw) - : matrix_(SUNBandMatrix(M, ubw, lbw)), id_(SUNMATRIX_BAND) { +SUNMatrixWrapper::SUNMatrixWrapper( + sunindextype M, sunindextype ubw, sunindextype lbw +) + : matrix_(SUNBandMatrix(M, ubw, lbw)) + , id_(SUNMATRIX_BAND) { if (M && !matrix_) throw std::bad_alloc(); finish_init(); } -SUNMatrixWrapper::SUNMatrixWrapper(const SUNMatrixWrapper &A, realtype droptol, - int sparsetype) - : id_(SUNMATRIX_SPARSE), sparsetype_(sparsetype) { +SUNMatrixWrapper::SUNMatrixWrapper( + SUNMatrixWrapper const& A, realtype droptol, int sparsetype +) + : id_(SUNMATRIX_SPARSE) + , sparsetype_(sparsetype) { if (sparsetype != CSC_MAT && sparsetype != CSR_MAT) throw std::invalid_argument("Invalid sparsetype. Must be CSC_MAT or " "CSR_MAT"); @@ -70,21 +77,23 @@ SUNMatrixWrapper::SUNMatrixWrapper(const SUNMatrixWrapper &A, realtype droptol, num_nonzeros_ = indexptrs_[num_indexptrs()]; } -static inline SUNMatrix_ID get_sparse_id_w_default(SUNMatrix mat) { +inline static SUNMatrix_ID get_sparse_id_w_default(SUNMatrix mat) { if (mat) return SUNMatGetID(mat); return SUNMATRIX_CUSTOM; } -static inline int get_sparse_type_w_default(SUNMatrix mat) { +inline static int get_sparse_type_w_default(SUNMatrix mat) { if (mat && SUNMatGetID(mat) == SUNMATRIX_SPARSE) return SM_SPARSETYPE_S(mat); return CSC_MAT; } SUNMatrixWrapper::SUNMatrixWrapper(SUNMatrix mat) - : matrix_(mat), id_(get_sparse_id_w_default(mat)), - sparsetype_(get_sparse_type_w_default(mat)), ownmat(false) { + : matrix_(mat) + , id_(get_sparse_id_w_default(mat)) + , sparsetype_(get_sparse_type_w_default(mat)) + , ownmat(false) { finish_init(); } @@ -93,9 +102,9 @@ SUNMatrixWrapper::~SUNMatrixWrapper() { SUNMatDestroy(matrix_); } -SUNMatrixWrapper::SUNMatrixWrapper(const SUNMatrixWrapper &other) - : id_(get_sparse_id_w_default(other.matrix_)), - sparsetype_(get_sparse_type_w_default(other.matrix_)) { +SUNMatrixWrapper::SUNMatrixWrapper(SUNMatrixWrapper const& other) + : id_(get_sparse_id_w_default(other.matrix_)) + , sparsetype_(get_sparse_type_w_default(other.matrix_)) { if (!other.matrix_) return; @@ -107,20 +116,20 @@ SUNMatrixWrapper::SUNMatrixWrapper(const SUNMatrixWrapper &other) finish_init(); } -SUNMatrixWrapper::SUNMatrixWrapper(SUNMatrixWrapper &&other) - : id_(get_sparse_id_w_default(other.matrix_)), - sparsetype_(get_sparse_type_w_default(other.matrix_)) { +SUNMatrixWrapper::SUNMatrixWrapper(SUNMatrixWrapper&& other) + : id_(get_sparse_id_w_default(other.matrix_)) + , sparsetype_(get_sparse_type_w_default(other.matrix_)) { std::swap(matrix_, other.matrix_); finish_init(); } -SUNMatrixWrapper &SUNMatrixWrapper::operator=(const SUNMatrixWrapper &other) { - if(&other == this) +SUNMatrixWrapper& SUNMatrixWrapper::operator=(SUNMatrixWrapper const& other) { + if (&other == this) return *this; return *this = SUNMatrixWrapper(other); } -SUNMatrixWrapper &SUNMatrixWrapper::operator=(SUNMatrixWrapper &&other) { +SUNMatrixWrapper& SUNMatrixWrapper::operator=(SUNMatrixWrapper&& other) { std::swap(matrix_, other.matrix_); id_ = other.id_; sparsetype_ = other.sparsetype_; @@ -134,8 +143,11 @@ void SUNMatrixWrapper::reallocate(sunindextype NNZ) { "CSR_MAT."); if (int ret = SUNSparseMatrix_Reallocate(matrix_, NNZ) != SUNMAT_SUCCESS) - throw std::runtime_error("SUNSparseMatrix_Reallocate failed with " - "error code " + std::to_string(ret) + "."); + throw std::runtime_error( + "SUNSparseMatrix_Reallocate failed with " + "error code " + + std::to_string(ret) + "." + ); update_ptrs(); capacity_ = NNZ; @@ -147,22 +159,24 @@ void SUNMatrixWrapper::realloc() { throw std::invalid_argument("Invalid sparsetype. Must be CSC_MAT or " "CSR_MAT."); if (int ret = SUNSparseMatrix_Realloc(matrix_) != SUNMAT_SUCCESS) - throw std::runtime_error("SUNSparseMatrix_Realloc failed with " - "error code " + std::to_string(ret) + "."); + throw std::runtime_error( + "SUNSparseMatrix_Realloc failed with " + "error code " + + std::to_string(ret) + "." + ); update_ptrs(); capacity_ = num_nonzeros_; assert(capacity() || !matrix_); } - - sunindextype SUNMatrixWrapper::num_indexptrs() const { assert(matrix_id() == SUNMATRIX_SPARSE); - assert(!matrix_ || - (sparsetype() == CSC_MAT ? - num_indexptrs_ == num_columns_ : - num_indexptrs_ == num_rows_)); + assert( + !matrix_ + || (sparsetype() == CSC_MAT ? num_indexptrs_ == num_columns_ + : num_indexptrs_ == num_rows_) + ); assert(!matrix_ || num_indexptrs_ == SM_NP_S(matrix_)); return num_indexptrs_; } @@ -175,18 +189,15 @@ sunindextype SUNMatrixWrapper::capacity() const { sunindextype SUNMatrixWrapper::num_nonzeros() const { assert(matrix_id() == SUNMATRIX_SPARSE); - assert(!matrix_ || - num_nonzeros_ == SM_INDEXPTRS_S(matrix_)[SM_NP_S(matrix_)]); + assert( + !matrix_ || num_nonzeros_ == SM_INDEXPTRS_S(matrix_)[SM_NP_S(matrix_)] + ); return num_nonzeros_; } -const realtype *SUNMatrixWrapper::data() const { - return data_; -} +realtype const* SUNMatrixWrapper::data() const { return data_; } -realtype *SUNMatrixWrapper::data() { - return data_; -} +realtype* SUNMatrixWrapper::data() { return data_; } int SUNMatrixWrapper::sparsetype() const { assert(matrix_); @@ -202,27 +213,28 @@ void SUNMatrixWrapper::scale(realtype a) { } } -void SUNMatrixWrapper::multiply(N_Vector c, const_N_Vector b, - const realtype alpha) const { - multiply(gsl::make_span(NV_DATA_S(c), NV_LENGTH_S(c)), - gsl::make_span(NV_DATA_S(b), NV_LENGTH_S(b)), - alpha); +void SUNMatrixWrapper::multiply( + N_Vector c, const_N_Vector b, const realtype alpha +) const { + multiply( + gsl::make_span(NV_DATA_S(c), NV_LENGTH_S(c)), + gsl::make_span(NV_DATA_S(b), NV_LENGTH_S(b)), alpha + ); } #ifndef NDEBUG -static inline void check_csc(const SUNMatrixWrapper *mat) { +inline static void check_csc(SUNMatrixWrapper const* mat) { assert(mat->matrix_id() == SUNMATRIX_SPARSE); assert(mat->sparsetype() == CSC_MAT); } #else // avoid "unused parameter" warning -static inline void check_csc(const SUNMatrixWrapper */*mat*/) {} +inline static void check_csc(SUNMatrixWrapper const* /*mat*/) {} #endif -void SUNMatrixWrapper::multiply(gsl::span c, - gsl::span b, - const realtype alpha) const { - +void SUNMatrixWrapper::multiply( + gsl::span c, gsl::span b, const realtype alpha +) const { if (!matrix_) return; @@ -232,39 +244,40 @@ void SUNMatrixWrapper::multiply(gsl::span c, switch (matrix_id()) { case SUNMATRIX_DENSE: - amici_dgemv(BLASLayout::colMajor, BLASTranspose::noTrans, - gsl::narrow(rows()), gsl::narrow(columns()), - alpha, data(), gsl::narrow(rows()), - b.data(), 1, 1.0, c.data(), 1); + amici_dgemv( + BLASLayout::colMajor, BLASTranspose::noTrans, + gsl::narrow(rows()), gsl::narrow(columns()), alpha, + data(), gsl::narrow(rows()), b.data(), 1, 1.0, c.data(), 1 + ); break; case SUNMATRIX_SPARSE: - if(!num_nonzeros()) { + if (!num_nonzeros()) { return; } check_csc(this); for (sunindextype icol = 0; icol < columns(); ++icol) { - scatter(icol, b[icol] * alpha, nullptr, c, icol+1, nullptr, 0); + scatter(icol, b[icol] * alpha, nullptr, c, icol + 1, nullptr, 0); } break; default: throw std::domain_error("Not Implemented."); } - } -void SUNMatrixWrapper::multiply(N_Vector c, - const_N_Vector b, - gsl::span cols, - bool transpose) const { - multiply(gsl::make_span(NV_DATA_S(c), NV_LENGTH_S(c)), - gsl::make_span(NV_DATA_S(b), NV_LENGTH_S(b)), - cols, transpose); +void SUNMatrixWrapper::multiply( + N_Vector c, const_N_Vector b, gsl::span cols, bool transpose +) const { + multiply( + gsl::make_span(NV_DATA_S(c), NV_LENGTH_S(c)), + gsl::make_span(NV_DATA_S(b), NV_LENGTH_S(b)), cols, + transpose + ); } -void SUNMatrixWrapper::multiply(gsl::span c, - gsl::span b, - gsl::span cols, - bool transpose) const { +void SUNMatrixWrapper::multiply( + gsl::span c, gsl::span b, + gsl::span cols, bool transpose +) const { if (!matrix_) return; @@ -317,9 +330,9 @@ void SUNMatrixWrapper::multiply(gsl::span c, } } - -void SUNMatrixWrapper::sparse_multiply(SUNMatrixWrapper &C, - const SUNMatrixWrapper &B) const { +void SUNMatrixWrapper::sparse_multiply( + SUNMatrixWrapper& C, SUNMatrixWrapper const& B +) const { if (!matrix_ || !B.matrix_ || !C.matrix_) return; @@ -339,8 +352,8 @@ void SUNMatrixWrapper::sparse_multiply(SUNMatrixWrapper &C, if (num_nonzeros() == 0 || B.num_nonzeros() == 0) return; // nothing to multiply - - /* see https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_multiply.c + /* see + * https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_multiply.c * modified such that we don't need to use CSparse memory structure and can * work with preallocated C. This should minimize number of necessary * reallocations as we can assume that C doesn't change size. @@ -352,25 +365,27 @@ void SUNMatrixWrapper::sparse_multiply(SUNMatrixWrapper &C, sunindextype cidx; auto w = std::vector(rows()); // sparsity of C(:,j) - auto x = std::vector(rows()); // entries in C(:,j) + auto x = std::vector(rows()); // entries in C(:,j) - for (bcol = 0; bcol < B.columns(); bcol++) // k in C(i,j) = sum_k A(i,k)*B(k,j) + for (bcol = 0; bcol < B.columns(); + bcol++) // k in C(i,j) = sum_k A(i,k)*B(k,j) { - C.set_indexptr(bcol, nnz); /* column j of C starts here */ - if ((B.get_indexptr(bcol+1) > B.get_indexptr(bcol)) - && (nnz + rows() > C.capacity())) - { + C.set_indexptr(bcol, nnz); /* column j of C starts here */ + if ((B.get_indexptr(bcol + 1) > B.get_indexptr(bcol)) + && (nnz + rows() > C.capacity())) { /* * if memory usage becomes a concern, remove the factor two here, * as it effectively trades memory efficiency against less * reallocations */ - C.reallocate(2*C.capacity() + rows()); + C.reallocate(2 * C.capacity() + rows()); } - for (bidx = B.get_indexptr(bcol); bidx < B.get_indexptr(bcol+1); bidx++) - { - nnz = scatter(B.get_indexval(bidx), B.get_data(bidx), - w.data(), gsl::make_span(x), bcol+1, &C, nnz); + for (bidx = B.get_indexptr(bcol); bidx < B.get_indexptr(bcol + 1); + bidx++) { + nnz = scatter( + B.get_indexval(bidx), B.get_data(bidx), w.data(), + gsl::make_span(x), bcol + 1, &C, nnz + ); assert(nnz - C.get_indexptr(bcol) <= rows()); } for (cidx = C.get_indexptr(bcol); cidx < nnz; cidx++) @@ -384,8 +399,10 @@ void SUNMatrixWrapper::sparse_multiply(SUNMatrixWrapper &C, */ } -void SUNMatrixWrapper::sparse_add(const SUNMatrixWrapper &A, realtype alpha, - const SUNMatrixWrapper &B, realtype beta) { +void SUNMatrixWrapper::sparse_add( + SUNMatrixWrapper const& A, realtype alpha, SUNMatrixWrapper const& B, + realtype beta +) { // matrix_ == nullptr is allowed on the first call if (!A.matrix_ || !B.matrix_) return; @@ -401,12 +418,12 @@ void SUNMatrixWrapper::sparse_add(const SUNMatrixWrapper &A, realtype alpha, zero(); - if (columns() == 0 || rows() == 0 || - (A.num_nonzeros() + B.num_nonzeros() == 0)) + if (columns() == 0 || rows() == 0 + || (A.num_nonzeros() + B.num_nonzeros() == 0)) return; // nothing to do - - /* see https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_add.c + /* see + * https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_add.c * modified such that we don't need to use CSparse memory structure and can * work with preallocated C. This should minimize number of necessary * reallocations as we can assume that C doesn't change size. @@ -418,19 +435,20 @@ void SUNMatrixWrapper::sparse_add(const SUNMatrixWrapper &A, realtype alpha, sunindextype cidx; // first call, make sure that matrix is initialized with no capacity - if(!capacity()) + if (!capacity()) reallocate(A.num_nonzeros() + B.num_nonzeros()); auto w = std::vector(rows()); auto x = std::vector(rows()); - for (ccol = 0; ccol < columns(); ccol++) - { - set_indexptr(ccol, nnz); /* column j of C starts here */ - nnz = A.scatter(ccol, alpha, w.data(), gsl::make_span(x), ccol+1, this, - nnz); - nnz = B.scatter(ccol, beta, w.data(), gsl::make_span(x), ccol+1, this, - nnz); + for (ccol = 0; ccol < columns(); ccol++) { + set_indexptr(ccol, nnz); /* column j of C starts here */ + nnz = A.scatter( + ccol, alpha, w.data(), gsl::make_span(x), ccol + 1, this, nnz + ); + nnz = B.scatter( + ccol, beta, w.data(), gsl::make_span(x), ccol + 1, this, nnz + ); // no reallocation should happen here for (cidx = get_indexptr(ccol); cidx < nnz; cidx++) { auto x_idx = get_indexval(cidx); @@ -440,21 +458,22 @@ void SUNMatrixWrapper::sparse_add(const SUNMatrixWrapper &A, realtype alpha, } set_indexptr(num_indexptrs(), nnz); if (capacity() == A.num_nonzeros() + B.num_nonzeros()) - realloc(); // resize if necessary, will have correct size in future calls + realloc( + ); // resize if necessary, will have correct size in future calls } -void SUNMatrixWrapper::sparse_sum(const std::vector &mats) { +void SUNMatrixWrapper::sparse_sum(std::vector const& mats) { // matrix_ == nullptr is allowed on the first call - auto all_empty = std::all_of(mats.begin(), mats.end(), - [](const SUNMatrixWrapper &m){ - return !m.matrix_; - }); + auto all_empty + = std::all_of(mats.begin(), mats.end(), [](SUNMatrixWrapper const& m) { + return !m.matrix_; + }); if (all_empty) return; check_csc(this); int max_total_nonzero = 0; - for (auto & mat : mats) { + for (auto& mat : mats) { check_csc(&mat); assert(rows() == mat.rows()); assert(columns() == mat.columns()); @@ -466,7 +485,8 @@ void SUNMatrixWrapper::sparse_sum(const std::vector &mats) { if (columns() == 0 || rows() == 0 || max_total_nonzero == 0) return; // nothing to do - /* see https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_add.c + /* see + * https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_add.c * modified such that we don't need to use CSparse memory structure and can * work with preallocated C. This should minimize number of necessary * reallocations as we can assume that C doesn't change size. @@ -477,18 +497,18 @@ void SUNMatrixWrapper::sparse_sum(const std::vector &mats) { sunindextype acol; sunindextype aidx; // first call, make sure that matrix is initialized with no capacity - if(!capacity()) + if (!capacity()) reallocate(max_total_nonzero); auto w = std::vector(rows()); auto x = std::vector(rows()); - for (acol = 0; acol < columns(); acol++) - { + for (acol = 0; acol < columns(); acol++) { set_indexptr(acol, nnz); /* column j of A starts here */ - for (auto & mat : mats) - nnz = mat.scatter(acol, 1.0, w.data(), gsl::make_span(x), acol+1, - this, nnz); + for (auto& mat : mats) + nnz = mat.scatter( + acol, 1.0, w.data(), gsl::make_span(x), acol + 1, this, nnz + ); // no reallocation should happen here for (aidx = get_indexptr(acol); aidx < nnz; aidx++) { auto x_idx = get_indexval(aidx); @@ -501,13 +521,11 @@ void SUNMatrixWrapper::sparse_sum(const std::vector &mats) { realloc(); // resize if necessary } -sunindextype SUNMatrixWrapper::scatter(const sunindextype acol, - const realtype beta, - sunindextype *w, - gsl::span x, - const sunindextype mark, - SUNMatrixWrapper *C, - sunindextype nnz) const { +sunindextype SUNMatrixWrapper::scatter( + const sunindextype acol, const realtype beta, sunindextype* w, + gsl::span x, const sunindextype mark, SUNMatrixWrapper* C, + sunindextype nnz +) const { if (!matrix_) return nnz; @@ -519,20 +537,25 @@ sunindextype SUNMatrixWrapper::scatter(const sunindextype acol, return nnz; auto x_data = x.data(); - /* see https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_scatter.c */ + /* see + * https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_scatter.c + */ sunindextype aidx; - for (aidx = get_indexptr(acol); aidx < get_indexptr(acol+1); aidx++) - { - auto arow = get_indexval(aidx); /* A(arow,acol) is nonzero */ + for (aidx = get_indexptr(acol); aidx < get_indexptr(acol + 1); aidx++) { + auto arow = get_indexval(aidx); /* A(arow,acol) is nonzero */ assert(arow >= 0 && gsl::narrow(arow) <= x.size()); if (w && w[arow] < mark) { - w[arow] = mark; /* arow is new entry in C(:,*) */ + w[arow] = mark; /* arow is new entry in C(:,*) */ if (C) - C->set_indexval(nnz++, arow); /* add arow to pattern of C(:,*) */ - x_data[arow] = beta * get_data(aidx); /* x(arow) = beta*A(arow,acol) */ + C->set_indexval( + nnz++, arow + ); /* add arow to pattern of C(:,*) */ + x_data[arow] + = beta * get_data(aidx); /* x(arow) = beta*A(arow,acol) */ } else { - x_data[arow] += beta * get_data(aidx); /* arow exists in C(:,*) already */ + x_data[arow] + += beta * get_data(aidx); /* arow exists in C(:,*) already */ } } assert(!C || nnz <= C->capacity()); @@ -541,20 +564,20 @@ sunindextype SUNMatrixWrapper::scatter(const sunindextype acol, // https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_cumsum.c /* p [0..n] = cumulative sum of c[0..n-1], and then copy p [0..n-1] into c */ -static void cumsum(gsl::span p, std::vector &c) { +static void cumsum(gsl::span p, std::vector& c) { sunindextype nz = 0; assert(p.size() == c.size() + 1); - for (sunindextype i = 0; i < gsl::narrow(c.size()); i++) - { + for (sunindextype i = 0; i < gsl::narrow(c.size()); i++) { p[i] = nz; nz += c[i]; - c[i] = p[i]; /* also copy p[0..n-1] back into c[0..n-1]*/ + c[i] = p[i]; /* also copy p[0..n-1] back into c[0..n-1]*/ } p[c.size()] = nz; } -void SUNMatrixWrapper::transpose(SUNMatrixWrapper &C, const realtype alpha, - sunindextype blocksize) const{ +void SUNMatrixWrapper::transpose( + SUNMatrixWrapper& C, const realtype alpha, sunindextype blocksize +) const { if (!matrix_ || !C.matrix_) return; @@ -579,7 +602,8 @@ void SUNMatrixWrapper::transpose(SUNMatrixWrapper &C, const realtype alpha, if (!num_nonzeros() || !columns() || !rows()) return; - // see https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_transpose.c + // see + // https://github.com/DrTimothyAldenDavis/SuiteSparse/blob/master/CSparse/Source/cs_transpose.c auto nrows = rows(); @@ -588,58 +612,59 @@ void SUNMatrixWrapper::transpose(SUNMatrixWrapper &C, const realtype alpha, auto w_data = w.data(); for (sunindextype acol = 0; acol < nrows; acol++) { /* row counts */ - auto next_indexptr = get_indexptr(acol+1); - auto widx_offset = (acol/blocksize)*blocksize; - for (sunindextype aidx = get_indexptr(acol); - aidx < next_indexptr; aidx++) { - sunindextype widx = widx_offset + get_indexval(aidx) % blocksize; + auto next_indexptr = get_indexptr(acol + 1); + auto widx_offset = (acol / blocksize) * blocksize; + for (sunindextype aidx = get_indexptr(acol); aidx < next_indexptr; + aidx++) { + sunindextype widx + = widx_offset + get_indexval(aidx) % blocksize; assert(widx >= 0 && widx < (sunindextype)w.size()); w_data[widx]++; assert(w_data[widx] <= nrows); } } /* row pointers */ - cumsum(gsl::make_span(C.indexptrs_, C.columns()+1), w); + cumsum(gsl::make_span(C.indexptrs_, C.columns() + 1), w); - for (sunindextype acol = 0; acol < nrows; acol++) - { - auto next_indexptr = get_indexptr(acol+1); - auto ccol_offset = (acol/blocksize)*blocksize; + for (sunindextype acol = 0; acol < nrows; acol++) { + auto next_indexptr = get_indexptr(acol + 1); + auto ccol_offset = (acol / blocksize) * blocksize; auto crow_offset = acol % blocksize; - for (sunindextype aidx = get_indexptr(acol); aidx < next_indexptr; aidx++) - { + for (sunindextype aidx = get_indexptr(acol); aidx < next_indexptr; + aidx++) { auto indexval_aidx = get_indexval(aidx); sunindextype ccol = ccol_offset + indexval_aidx % blocksize; - sunindextype crow = (indexval_aidx/blocksize)*blocksize + crow_offset; + sunindextype crow + = (indexval_aidx / blocksize) * blocksize + crow_offset; assert(crow < nrows); assert(ccol < columns()); assert(aidx < capacity()); assert(ccol >= 0 && ccol < (sunindextype)w.size()); sunindextype cidx = w_data[ccol]++; - C.set_indexval(cidx, crow); /* place A(i,j) as entry C(j,i) */ + C.set_indexval(cidx, crow); /* place A(i,j) as entry C(j,i) */ C.set_data(cidx, alpha * get_data(aidx)); } } } else { - for (sunindextype acol = 0; acol < nrows; acol++) - { - auto next_indexptr = get_indexptr(acol+1); + for (sunindextype acol = 0; acol < nrows; acol++) { + auto next_indexptr = get_indexptr(acol + 1); - for (sunindextype aidx = get_indexptr(acol); aidx < next_indexptr; aidx++) - { - sunindextype ccol = (acol/blocksize)*blocksize + get_indexval(aidx) % blocksize; - sunindextype crow = (get_indexval(aidx)/blocksize)*blocksize + acol % blocksize; + for (sunindextype aidx = get_indexptr(acol); aidx < next_indexptr; + aidx++) { + sunindextype ccol = (acol / blocksize) * blocksize + + get_indexval(aidx) % blocksize; + sunindextype crow = (get_indexval(aidx) / blocksize) * blocksize + + acol % blocksize; assert(crow < nrows); assert(ccol < columns()); C.set_data(crow, ccol, alpha * get_data(aidx)); } } } - } -void SUNMatrixWrapper::to_dense(SUNMatrixWrapper &D) const { +void SUNMatrixWrapper::to_dense(SUNMatrixWrapper& D) const { if (!matrix_ || !D.matrix_) return; check_csc(this); @@ -653,7 +678,7 @@ void SUNMatrixWrapper::to_dense(SUNMatrixWrapper &D) const { sunindextype icol; sunindextype idx; for (icol = 0; icol < columns(); ++icol) - for (idx = get_indexptr(icol); idx < get_indexptr(icol+1); ++idx) { + for (idx = get_indexptr(icol); idx < get_indexptr(icol + 1); ++idx) { D.set_data(get_indexval(idx), icol, get_data(idx)); } } @@ -672,19 +697,18 @@ void SUNMatrixWrapper::to_diag(N_Vector v) const { sunindextype icol; sunindextype idx; for (icol = 0; icol < columns(); ++icol) - for (idx = get_indexptr(icol); idx < get_indexptr(icol+1); ++idx) + for (idx = get_indexptr(icol); idx < get_indexptr(icol + 1); ++idx) if (get_indexval(idx) == icol) NV_Ith_S(v, icol) = get_data(idx); } - -void SUNMatrixWrapper::zero() -{ +void SUNMatrixWrapper::zero() { if (!matrix_) return; - if(int res = SUNMatZero(matrix_)) - throw std::runtime_error("SUNMatrixWrapper::zero() failed with " - + std::to_string(res) + "."); + if (int res = SUNMatZero(matrix_)) + throw std::runtime_error( + "SUNMatrixWrapper::zero() failed with " + std::to_string(res) + "." + ); } void SUNMatrixWrapper::finish_init() { @@ -748,11 +772,11 @@ void SUNMatrixWrapper::refresh() { SUNMatrix SUNMatrixWrapper::get() const { return matrix_; } -std::pair unravel_index(sunindextype i, SUNMatrix m) -{ +std::pair +unravel_index(sunindextype i, SUNMatrix m) { gsl_ExpectsDebug(i >= 0); auto mat_id = SUNMatGetID(m); - if(mat_id == SUNMATRIX_DENSE) { + if (mat_id == SUNMATRIX_DENSE) { gsl_ExpectsDebug(i < SM_COLUMNS_D(m) * SM_ROWS_D(m)); auto num_rows = SM_ROWS_D(m); @@ -768,12 +792,12 @@ std::pair unravel_index(sunindextype i, SUNMatrix m) return {row, col}; } - if(mat_id == SUNMATRIX_SPARSE) { + if (mat_id == SUNMATRIX_SPARSE) { gsl_ExpectsDebug(i < SM_NNZ_S(m)); sunindextype row = SM_INDEXVALS_S(m)[i]; sunindextype i_colptr = 0; - while(SM_INDEXPTRS_S(m)[i_colptr] < SM_NNZ_S(m)) { - if(SM_INDEXPTRS_S(m)[i_colptr + 1] > i) { + while (SM_INDEXPTRS_S(m)[i_colptr] < SM_NNZ_S(m)) { + if (SM_INDEXPTRS_S(m)[i_colptr + 1] > i) { sunindextype col = i_colptr; gsl_EnsuresDebug(row >= 0); gsl_EnsuresDebug(row < SM_ROWS_S(m)); @@ -789,4 +813,3 @@ std::pair unravel_index(sunindextype i, SUNMatrix m) } } // namespace amici - diff --git a/src/symbolic_functions.cpp b/src/symbolic_functions.cpp index df8397644f..6c18d851b7 100644 --- a/src/symbolic_functions.cpp +++ b/src/symbolic_functions.cpp @@ -71,7 +71,7 @@ double sign(double x) { return 0.0; } -double max(double a, double b, double /*c*/) { +double max(double a, double b, double /*c*/) { int anan = isNaN(a), bnan = isNaN(b); if (anan || bnan) { if (anan && !bnan) @@ -83,11 +83,9 @@ double max(double a, double b, double /*c*/) { return (std::max(a, b)); } -double min(double a, double b, double c) { - return (-max(-a,-b,c)); -} +double min(double a, double b, double c) { return (-max(-a, -b, c)); } -double Dmax(int id, double a, double b, double /*c*/) { +double Dmax(int id, double a, double b, double /*c*/) { if (id == 1.0) { if (a > b) return 1.0; @@ -100,16 +98,15 @@ double Dmax(int id, double a, double b, double /*c*/) { } double Dmin(int id, double a, double b, double c) { - return Dmax(id,-a,-b,c); + return Dmax(id, -a, -b, c); } double pos_pow(double base, double exponent) { - // we do NOT want to propagate NaN values here, if base is nan, so should the output be - return pow(std::max(base, 0.0),exponent); + // we do NOT want to propagate NaN values here, if base is nan, so should + // the output be + return pow(std::max(base, 0.0), exponent); } - - // Legacy spline implementation in C (MATLAB only) double spline(double t, int num, ...) { @@ -119,12 +116,12 @@ double spline(double t, int num, ...) { double ss; double dudt; - auto *ts = (double *)alloca(num * sizeof(double)); - auto *us = (double *)alloca(num * sizeof(double)); + auto* ts = (double*)alloca(num * sizeof(double)); + auto* us = (double*)alloca(num * sizeof(double)); - auto *b = (double *)alloca(num * sizeof(double)); - auto *c = (double *)alloca(num * sizeof(double)); - auto *d = (double *)alloca(num * sizeof(double)); + auto* b = (double*)alloca(num * sizeof(double)); + auto* c = (double*)alloca(num * sizeof(double)); + auto* d = (double*)alloca(num * sizeof(double)); /* Variable list type macro */ /* initialize valist for num number of arguments */ @@ -155,13 +152,13 @@ double spline_pos(double t, int num, ...) { double ss; double dudt; - auto *ts = (double *)alloca(num * sizeof(double)); - auto *us = (double *)alloca(num * sizeof(double)); - auto *uslog = (double *)alloca(num * sizeof(double)); + auto* ts = (double*)alloca(num * sizeof(double)); + auto* us = (double*)alloca(num * sizeof(double)); + auto* uslog = (double*)alloca(num * sizeof(double)); - auto *b = (double *)alloca(num * sizeof(double)); - auto *c = (double *)alloca(num * sizeof(double)); - auto *d = (double *)alloca(num * sizeof(double)); + auto* b = (double*)alloca(num * sizeof(double)); + auto* c = (double*)alloca(num * sizeof(double)); + auto* d = (double*)alloca(num * sizeof(double)); /* initialize valist for num number of arguments */ va_start(valist, num); @@ -192,13 +189,13 @@ double Dspline(int id, double t, int num, ...) { double ss; double dudt; - double *ts = (double *)alloca(num * sizeof(double)); - double *us = (double *)alloca(num * sizeof(double)); - double *ps = (double *)alloca(num * sizeof(double)); + double* ts = (double*)alloca(num * sizeof(double)); + double* us = (double*)alloca(num * sizeof(double)); + double* ps = (double*)alloca(num * sizeof(double)); - double *b = (double *)alloca(num * sizeof(double)); - double *c = (double *)alloca(num * sizeof(double)); - double *d = (double *)alloca(num * sizeof(double)); + double* b = (double*)alloca(num * sizeof(double)); + double* c = (double*)alloca(num * sizeof(double)); + double* d = (double*)alloca(num * sizeof(double)); int did = id / 2 - 2; @@ -228,14 +225,14 @@ double Dspline_pos(int id, double t, int num, ...) { va_list valist; - auto *ts = (double *)alloca(num * sizeof(double)); - auto *us = (double *)alloca(num * sizeof(double)); - auto *sus = (double *)alloca(num * sizeof(double)); - auto *uslog = (double *)alloca(num * sizeof(double)); + auto* ts = (double*)alloca(num * sizeof(double)); + auto* us = (double*)alloca(num * sizeof(double)); + auto* sus = (double*)alloca(num * sizeof(double)); + auto* uslog = (double*)alloca(num * sizeof(double)); - auto *b = (double *)alloca(num * sizeof(double)); - auto *c = (double *)alloca(num * sizeof(double)); - auto *d = (double *)alloca(num * sizeof(double)); + auto* b = (double*)alloca(num * sizeof(double)); + auto* c = (double*)alloca(num * sizeof(double)); + auto* d = (double*)alloca(num * sizeof(double)); double uout; double ss; @@ -272,21 +269,23 @@ double Dspline_pos(int id, double t, int num, ...) { return uout; } -double DDspline(int /*id1*/, int /*id2*/, double /*t*/, int /*num*/, ...) { return 0.0; } +double DDspline(int /*id1*/, int /*id2*/, double /*t*/, int /*num*/, ...) { + return 0.0; +} double DDspline_pos(int id1, int id2, double t, int num, ...) { va_list valist; - auto *ts = (double *)alloca(num * sizeof(double)); - auto *us = (double *)alloca(num * sizeof(double)); - auto *sus1 = (double *)alloca(num * sizeof(double)); - auto *sus2 = (double *)alloca(num * sizeof(double)); - auto *uslog = (double *)alloca(num * sizeof(double)); + auto* ts = (double*)alloca(num * sizeof(double)); + auto* us = (double*)alloca(num * sizeof(double)); + auto* sus1 = (double*)alloca(num * sizeof(double)); + auto* sus2 = (double*)alloca(num * sizeof(double)); + auto* uslog = (double*)alloca(num * sizeof(double)); - auto *b = (double *)alloca(num * sizeof(double)); - auto *c = (double *)alloca(num * sizeof(double)); - auto *d = (double *)alloca(num * sizeof(double)); + auto* b = (double*)alloca(num * sizeof(double)); + auto* c = (double*)alloca(num * sizeof(double)); + auto* d = (double*)alloca(num * sizeof(double)); double uout; double ss; diff --git a/src/vector.cpp b/src/vector.cpp index 89670602ce..78b66d7954 100644 --- a/src/vector.cpp +++ b/src/vector.cpp @@ -1,54 +1,57 @@ #include "amici/vector.h" -#include #include +#include namespace amici { -AmiVector &AmiVector::operator=(AmiVector const &other) { +AmiVector& AmiVector::operator=(AmiVector const& other) { vec_ = other.vec_; synchroniseNVector(); return *this; } -realtype *AmiVector::data() { return vec_.data(); } +realtype* AmiVector::data() { return vec_.data(); } -const realtype *AmiVector::data() const { return vec_.data(); } +realtype const* AmiVector::data() const { return vec_.data(); } N_Vector AmiVector::getNVector() { return nvec_; } const_N_Vector AmiVector::getNVector() const { return nvec_; } -std::vector const &AmiVector::getVector() const { return vec_; } +std::vector const& AmiVector::getVector() const { return vec_; } int AmiVector::getLength() const { return gsl::narrow(vec_.size()); } void AmiVector::zero() { set(0.0); } void AmiVector::minus() { - std::transform(vec_.begin(), vec_.end(), - vec_.begin(), std::negate()); + std::transform( + vec_.begin(), vec_.end(), vec_.begin(), std::negate() + ); } void AmiVector::set(realtype val) { std::fill(vec_.begin(), vec_.end(), val); } -realtype &AmiVector::operator[](int pos) { +realtype& AmiVector::operator[](int pos) { return vec_.at(gsl::narrow(pos)); } -realtype &AmiVector::at(int pos) { +realtype& AmiVector::at(int pos) { return vec_.at(gsl::narrow(pos)); } -const realtype &AmiVector::at(int pos) const { +realtype const& AmiVector::at(int pos) const { return vec_.at(gsl::narrow(pos)); } -void AmiVector::copy(const AmiVector &other) { - if(getLength() != other.getLength()) - throw AmiException("Dimension of AmiVector (%i) does not " - "match input dimension (%i)", - getLength(), other.getLength()); +void AmiVector::copy(AmiVector const& other) { + if (getLength() != other.getLength()) + throw AmiException( + "Dimension of AmiVector (%i) does not " + "match input dimension (%i)", + getLength(), other.getLength() + ); std::copy(other.vec_.begin(), other.vec_.end(), vec_.begin()); synchroniseNVector(); } @@ -72,7 +75,7 @@ AmiVectorArray::AmiVectorArray(long int length_inner, long int length_outer) } } -AmiVectorArray &AmiVectorArray::operator=(AmiVectorArray const &other) { +AmiVectorArray& AmiVectorArray::operator=(AmiVectorArray const& other) { vec_array_ = other.vec_array_; nvec_array_.resize(other.getLength()); for (int idx = 0; idx < other.getLength(); idx++) { @@ -81,7 +84,7 @@ AmiVectorArray &AmiVectorArray::operator=(AmiVectorArray const &other) { return *this; } -AmiVectorArray::AmiVectorArray(const AmiVectorArray &vaold) +AmiVectorArray::AmiVectorArray(AmiVectorArray const& vaold) : vec_array_(vaold.vec_array_) { nvec_array_.resize(vaold.getLength()); for (int idx = 0; idx < vaold.getLength(); idx++) { @@ -89,29 +92,31 @@ AmiVectorArray::AmiVectorArray(const AmiVectorArray &vaold) } } -realtype *AmiVectorArray::data(int pos) { return vec_array_.at(pos).data(); } +realtype* AmiVectorArray::data(int pos) { return vec_array_.at(pos).data(); } -const realtype *AmiVectorArray::data(int pos) const { +realtype const* AmiVectorArray::data(int pos) const { return vec_array_.at(pos).data(); } -realtype &AmiVectorArray::at(int ipos, int jpos) { +realtype& AmiVectorArray::at(int ipos, int jpos) { return vec_array_.at(jpos).at(ipos); } -const realtype &AmiVectorArray::at(int ipos, int jpos) const { +realtype const& AmiVectorArray::at(int ipos, int jpos) const { return vec_array_.at(jpos).at(ipos); } -N_Vector *AmiVectorArray::getNVectorArray() { return nvec_array_.data(); } +N_Vector* AmiVectorArray::getNVectorArray() { return nvec_array_.data(); } N_Vector AmiVectorArray::getNVector(int pos) { return nvec_array_.at(pos); } -const_N_Vector AmiVectorArray::getNVector(int pos) const { return nvec_array_.at(pos); } +const_N_Vector AmiVectorArray::getNVector(int pos) const { + return nvec_array_.at(pos); +} -AmiVector &AmiVectorArray::operator[](int pos) { return vec_array_.at(pos); } +AmiVector& AmiVectorArray::operator[](int pos) { return vec_array_.at(pos); } -const AmiVector &AmiVectorArray::operator[](int pos) const { +AmiVector const& AmiVectorArray::operator[](int pos) const { return vec_array_.at(pos); } @@ -120,20 +125,22 @@ int AmiVectorArray::getLength() const { } void AmiVectorArray::zero() { - for (auto &v : vec_array_) + for (auto& v : vec_array_) v.zero(); } -void AmiVectorArray::flatten_to_vector(std::vector &vec) const { +void AmiVectorArray::flatten_to_vector(std::vector& vec) const { int n_outer = gsl::narrow(vec_array_.size()); if (n_outer == 0) return; // nothing to do ... int n_inner = vec_array_.at(0).getLength(); if (gsl::narrow(vec.size()) != n_inner * n_outer) { - throw AmiException("Dimension of AmiVectorArray (%ix%i) does not " - "match target vector dimension (%u)", - n_inner, n_outer, vec.size()); + throw AmiException( + "Dimension of AmiVectorArray (%ix%i) does not " + "match target vector dimension (%u)", + n_inner, n_outer, vec.size() + ); } for (int outer = 0; outer < n_outer; ++outer) { @@ -142,11 +149,13 @@ void AmiVectorArray::flatten_to_vector(std::vector &vec) const { } } -void AmiVectorArray::copy(const AmiVectorArray &other) { +void AmiVectorArray::copy(AmiVectorArray const& other) { if (getLength() != other.getLength()) - throw AmiException("Dimension of AmiVectorArray (%i) does not " - "match input dimension (%i)", - getLength(), other.getLength()); + throw AmiException( + "Dimension of AmiVectorArray (%i) does not " + "match input dimension (%i)", + getLength(), other.getLength() + ); for (int iv = 0; iv < getLength(); ++iv) { vec_array_.at(iv).copy(other.vec_array_.at(iv)); diff --git a/src/wrapfunctions.template.cpp b/src/wrapfunctions.template.cpp index bb363c0c95..e7ab71ddbe 100644 --- a/src/wrapfunctions.template.cpp +++ b/src/wrapfunctions.template.cpp @@ -1,16 +1,16 @@ -#include "amici/model.h" #include "wrapfunctions.h" #include "TPL_MODELNAME.h" +#include "amici/model.h" namespace amici { namespace generic_model { std::unique_ptr getModel() { return std::unique_ptr( - new amici::model_TPL_MODELNAME::Model_TPL_MODELNAME()); + new amici::model_TPL_MODELNAME::Model_TPL_MODELNAME() + ); } - } // namespace generic_model } // namespace amici diff --git a/swig/abstract_model.i b/swig/abstract_model.i index 75a48c1675..47a0dfb99d 100644 --- a/swig/abstract_model.i +++ b/swig/abstract_model.i @@ -68,4 +68,5 @@ %ignore fdx_rdatadx_solver_rowvals; %ignore fdtotal_cldx_rdata_colptrs; %ignore fdtotal_cldx_rdata_rowvals; +%ignore fcreate_splines; %include "amici/abstract_model.h" diff --git a/swig/amici.i b/swig/amici.i index 6c796391e3..0015ca1bd0 100644 --- a/swig/amici.i +++ b/swig/amici.i @@ -154,12 +154,15 @@ wrap_unique_ptr(ExpDataPtr, amici::ExpData) %naturalvar amici::SimulationParameters::reinitialization_state_idxs_sim; %naturalvar amici::SimulationParameters::reinitialization_state_idxs_presim; +// DO NOT IGNORE amici::SimulationParameters, amici::ModelDimensions, amici::CpuTimer %ignore amici::ModelContext; %ignore amici::ContextManager; %ignore amici::ModelState; %ignore amici::ModelStateDerived; %ignore amici::unravel_index; %ignore amici::backtraceString; +%ignore amici::Logger; +%ignore amici::SimulationState; // Include before any other header which uses enums defined there %include "amici/defines.h" diff --git a/swig/model.i b/swig/model.i index d2c9f2eabd..3063590c21 100644 --- a/swig/model.i +++ b/swig/model.i @@ -84,6 +84,16 @@ using namespace amici; %ignore getObservableSigma; %ignore getObservableSigmaSensitivity; %ignore getUnobservedEventSensitivity; +%ignore fdsigmaydy; +%ignore fdspline_slopesdp; +%ignore fdspline_valuesdp; +%ignore fdtotal_cldp; +%ignore fdtotal_cldx_rdata; +%ignore fdx_rdatadp; +%ignore fdx_rdatadtcl; +%ignore fdx_rdatadx_solver; +%ignore fdsigmaydy; + diff --git a/swig/model_ode.i b/swig/model_ode.i index de342fef93..a372c1efad 100644 --- a/swig/model_ode.i +++ b/swig/model_ode.i @@ -15,5 +15,3 @@ using namespace amici; // Process symbols in header %include "amici/model_ode.h" - - diff --git a/swig/solver.i b/swig/solver.i index 319e654530..992842c409 100644 --- a/swig/solver.i +++ b/swig/solver.i @@ -39,6 +39,21 @@ using namespace amici; %ignore turnOffRootFinding; %ignore getRootInfo; %ignore updateAndReinitStatesAndSensitivities; +%ignore getCpuTime; +%ignore getCpuTimeB; +%ignore getLastOrder; +%ignore getNumErrTestFails; +%ignore getNumErrTestFailsB; +%ignore getNumNonlinSolvConvFails; +%ignore getNumNonlinSolvConvFailsB; +%ignore getNumRhsEvals; +%ignore getNumRhsEvalsB; +%ignore getNumSteps; +%ignore getNumStepsB; +%ignore gett; +%ignore startTimer; +%ignore switchForwardSensisOff; +%ignore timeExceeded; // Solver.__repr__ %pythoncode %{ diff --git a/tests/benchmark-models/benchmark_models.yaml b/tests/benchmark-models/benchmark_models.yaml index 6e291cb010..4e196e5261 100644 --- a/tests/benchmark-models/benchmark_models.yaml +++ b/tests/benchmark-models/benchmark_models.yaml @@ -116,4 +116,3 @@ Zheng_PNAS2012: t_fwd: 0.05 t_adj: 0.05 note: benchmark collection reference ignores factor 1/2 - diff --git a/tests/benchmark-models/evaluate_benchmark.py b/tests/benchmark-models/evaluate_benchmark.py index f1d88197db..bcf1f63bb8 100644 --- a/tests/benchmark-models/evaluate_benchmark.py +++ b/tests/benchmark-models/evaluate_benchmark.py @@ -4,46 +4,52 @@ Aggregate computation times from different benchmarks and plot """ import os + +import matplotlib.pyplot as plt import pandas as pd import seaborn as sns -import matplotlib.pyplot as plt # read benchmark results for different models -outfile = 'computation_times.csv' -df = pd.concat([ - pd.read_csv(f, header=[0], index_col=[0]).rename(columns={'0': '_'.join(f.split('_')[:2])}).T - for f in os.listdir() if f.endswith('.csv') if f != outfile -]) -df.sort_values('np', inplace=True) +outfile = "computation_times.csv" +df = pd.concat( + [ + pd.read_csv(f, header=[0], index_col=[0]) + .rename(columns={"0": "_".join(f.split("_")[:2])}) + .T + for f in os.listdir() + if f.endswith(".csv") + if f != outfile + ] +) +df.sort_values("np", inplace=True) df.to_csv(outfile) -ratios = pd.concat( - [df[sensi]/df['t_sim'].values for sensi in ['t_fwd', 't_adj']] + [df.np], axis=1, -).reset_index().melt(id_vars=['index', 'np']).rename( - columns={'index': 'model', 'variable': 'sensitivity', 'value': 'ratio'} +ratios = ( + pd.concat( + [df[sensi] / df["t_sim"].values for sensi in ["t_fwd", "t_adj"]] + [df.np], + axis=1, + ) + .reset_index() + .melt(id_vars=["index", "np"]) + .rename(columns={"index": "model", "variable": "sensitivity", "value": "ratio"}) ) -ratios['sensitivity'] = ratios['sensitivity'].replace( - {'t_fwd': 'forward', 't_adj': 'adjoint'} +ratios["sensitivity"] = ratios["sensitivity"].replace( + {"t_fwd": "forward", "t_adj": "adjoint"} ) plt.figure(figsize=(10, 5)) g = sns.barplot( - data=ratios, - order=list(df.index), - x='model', - y='ratio', - hue='sensitivity' + data=ratios, order=list(df.index), x="model", y="ratio", hue="sensitivity" ) for ir, row in ratios.iterrows(): - if row.sensitivity == 'adjoint': + if row.sensitivity == "adjoint": continue - g.text(ir, row['np'], int(row['np']), color='black', ha="center", weight='bold') + g.text(ir, row["np"], int(row["np"]), color="black", ha="center", weight="bold") -plt.xticks(rotation=30, horizontalalignment='right') +plt.xticks(rotation=30, horizontalalignment="right") plt.tight_layout() -plt.savefig('computation_times.png') - +plt.savefig("computation_times.png") diff --git a/tests/benchmark-models/test_petab_benchmark.py b/tests/benchmark-models/test_petab_benchmark.py index 6ed237190d..40071b5ecb 100755 --- a/tests/benchmark-models/test_petab_benchmark.py +++ b/tests/benchmark-models/test_petab_benchmark.py @@ -9,38 +9,38 @@ import pandas as pd import petab import pytest - -from fiddy import get_derivative, MethodId -from fiddy.success import Consistency +from fiddy import MethodId, get_derivative from fiddy.derivative_check import NumpyIsCloseDerivativeCheck -from fiddy.extensions.amici import ( - simulate_petab_to_cached_functions, -) - +from fiddy.extensions.amici import simulate_petab_to_cached_functions +from fiddy.success import Consistency # Absolute and relative tolerances for finite difference gradient checks. ATOL: float = 1e-3 RTOL: float = 1e-2 -benchmark_path = Path(__file__).parent.parent.parent / "Benchmark-Models-PEtab" / "Benchmark-Models" +benchmark_path = ( + Path(__file__).parent.parent.parent / "Benchmark-Models-PEtab" / "Benchmark-Models" +) # reuse compiled models from test_benchmark_collection.sh benchmark_outdir = Path(__file__).parent.parent.parent / "test_bmc" models = [ str(petab_path.stem) - for petab_path in benchmark_path.glob("*") if petab_path.is_dir() - if str(petab_path.stem) not in ( + for petab_path in benchmark_path.glob("*") + if petab_path.is_dir() + if str(petab_path.stem) + not in ( # excluded due to excessive runtime - 'Bachmann_MSB2011', - 'Chen_MSB2009', - 'Froehlich_CellSystems2018', - 'Raimundez_PCB2020', - 'Lucarelli_CellSystems2018', - 'Isensee_JCB2018', - 'Beer_MolBioSystems2014', - 'Alkan_SciSignal2018', + "Bachmann_MSB2011", + "Chen_MSB2009", + "Froehlich_CellSystems2018", + "Raimundez_PCB2020", + "Lucarelli_CellSystems2018", + "Isensee_JCB2018", + "Beer_MolBioSystems2014", + "Alkan_SciSignal2018", # excluded due to excessive numerical failures - 'Crauste_CellSystems2017', - 'Fujita_SciSignal2010', + "Crauste_CellSystems2017", + "Fujita_SciSignal2010", ) ] @@ -54,13 +54,13 @@ @pytest.mark.parametrize("model", models) def test_benchmark_gradient(model, scale): if not scale and model in ( - 'Smith_BMCSystBiol2013', - 'Brannmark_JBC2010', - 'Elowitz_Nature2000', - 'Borghans_BiophysChem1997', - 'Sneyd_PNAS2002', - 'Bertozzi_PNAS2020', - 'Okuonghae_ChaosSolitonsFractals2020', + "Smith_BMCSystBiol2013", + "Brannmark_JBC2010", + "Elowitz_Nature2000", + "Borghans_BiophysChem1997", + "Sneyd_PNAS2002", + "Bertozzi_PNAS2020", + "Okuonghae_ChaosSolitonsFractals2020", ): # not really worth the effort trying to fix these cases if they # only fail on linear scale @@ -82,22 +82,20 @@ def test_benchmark_gradient(model, scale): amici_solver.setAbsoluteTolerance(1e-12) amici_solver.setRelativeTolerance(1e-12) if model in ( - 'Smith_BMCSystBiol2013', - 'Oliveira_NatCommun2021', + "Smith_BMCSystBiol2013", + "Oliveira_NatCommun2021", ): amici_solver.setAbsoluteTolerance(1e-10) amici_solver.setRelativeTolerance(1e-10) - elif model in ( - 'Okuonghae_ChaosSolitonsFractals2020', - ): + elif model in ("Okuonghae_ChaosSolitonsFractals2020",): amici_solver.setAbsoluteTolerance(1e-14) amici_solver.setRelativeTolerance(1e-14) amici_solver.setMaxSteps(int(1e5)) - if model in ( - 'Brannmark_JBC2010', - ): - amici_model.setSteadyStateSensitivityMode(amici.SteadyStateSensitivityMode.integrationOnly) + if model in ("Brannmark_JBC2010",): + amici_model.setSteadyStateSensitivityMode( + amici.SteadyStateSensitivityMode.integrationOnly + ) amici_function, amici_derivative = simulate_petab_to_cached_functions( petab_problem=petab_problem, @@ -113,9 +111,13 @@ def test_benchmark_gradient(model, scale): np.random.seed(0) if scale: - point = np.asarray(list( - petab_problem.scale_parameters(dict(parameter_df_free.nominalValue)).values() - )) + point = np.asarray( + list( + petab_problem.scale_parameters( + dict(parameter_df_free.nominalValue) + ).values() + ) + ) point_noise = np.random.randn(len(point)) * noise_level else: point = parameter_df_free.nominalValue.values @@ -131,9 +133,7 @@ def test_benchmark_gradient(model, scale): 1e-4, 1e-5, ] - if model in ( - 'Okuonghae_ChaosSolitonsFractals2020', - ): + if model in ("Okuonghae_ChaosSolitonsFractals2020",): sizes.insert(0, 0.2) derivative = get_derivative( @@ -156,18 +156,22 @@ def test_benchmark_gradient(model, scale): success = check(rtol=RTOL, atol=ATOL) if debug: - df = pd.DataFrame([ - { - ('fd', r.metadata['size_absolute'], str(r.method_id)): r.value - for c in d.computers - for r in c.results - } for d in derivative.directional_derivatives - ], index=parameter_ids) - df[('fd', 'full', '')] = derivative.series.values - df[('amici', '', '')] = expected_derivative + df = pd.DataFrame( + [ + { + ("fd", r.metadata["size_absolute"], str(r.method_id)): r.value + for c in d.computers + for r in c.results + } + for d in derivative.directional_derivatives + ], + index=parameter_ids, + ) + df[("fd", "full", "")] = derivative.series.values + df[("amici", "", "")] = expected_derivative file_name = f"{model}_scale={scale}.tsv" - df.to_csv(debug_path / file_name, sep='\t') + df.to_csv(debug_path / file_name, sep="\t") # The gradients for all parameters are correct. assert success, derivative.df diff --git a/tests/benchmark-models/test_petab_model.py b/tests/benchmark-models/test_petab_model.py index 42b29bb983..c31255f9da 100755 --- a/tests/benchmark-models/test_petab_model.py +++ b/tests/benchmark-models/test_petab_model.py @@ -9,16 +9,14 @@ import logging import os import sys -import pandas as pd -import numpy as np +import amici +import numpy as np +import pandas as pd import petab import yaml - -import amici from amici.logging import get_logger -from amici.petab_objective import (simulate_petab, rdatas_to_measurement_df, - LLH, RDATAS) +from amici.petab_objective import LLH, RDATAS, rdatas_to_measurement_df, simulate_petab from petab.visualize import plot_problem logger = get_logger(f"amici.{__name__}", logging.WARNING) @@ -32,33 +30,64 @@ def parse_cli_args(): """ parser = argparse.ArgumentParser( - description='Simulate PEtab-format model using AMICI.') + description="Simulate PEtab-format model using AMICI." + ) # General options: - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true', - help='More verbose output') - parser.add_argument('-c', '--check', dest='check', action='store_true', - help='Compare to reference value') - parser.add_argument('-p', '--plot', dest='plot', action='store_true', - help='Plot measurement and simulation results') + parser.add_argument( + "-v", + "--verbose", + dest="verbose", + action="store_true", + help="More verbose output", + ) + parser.add_argument( + "-c", + "--check", + dest="check", + action="store_true", + help="Compare to reference value", + ) + parser.add_argument( + "-p", + "--plot", + dest="plot", + action="store_true", + help="Plot measurement and simulation results", + ) # PEtab problem - parser.add_argument('-y', '--yaml', dest='yaml_file_name', - required=True, - help='PEtab YAML problem filename') + parser.add_argument( + "-y", + "--yaml", + dest="yaml_file_name", + required=True, + help="PEtab YAML problem filename", + ) # Corresponding AMICI model - parser.add_argument('-m', '--model-name', dest='model_name', - help='Name of the AMICI module of the model to ' - 'simulate.', required=True) - parser.add_argument('-d', '--model-dir', dest='model_directory', - help='Directory containing the AMICI module of the ' - 'model to simulate. Required if model is not ' - 'in python path.') - - parser.add_argument('-o', '--simulation-file', dest='simulation_file', - help='File to write simulation result to, in PEtab' - 'measurement table format.') + parser.add_argument( + "-m", + "--model-name", + dest="model_name", + help="Name of the AMICI module of the model to " "simulate.", + required=True, + ) + parser.add_argument( + "-d", + "--model-dir", + dest="model_directory", + help="Directory containing the AMICI module of the " + "model to simulate. Required if model is not " + "in python path.", + ) + + parser.add_argument( + "-o", + "--simulation-file", + dest="simulation_file", + help="File to write simulation result to, in PEtab" "measurement table format.", + ) return parser.parse_args() @@ -70,9 +99,11 @@ def main(): loglevel = logging.DEBUG if args.verbose else logging.INFO logger.setLevel(loglevel) - logger.info(f"Simulating '{args.model_name}' " - f"({args.model_directory}) using PEtab data from " - f"{args.yaml_file_name}") + logger.info( + f"Simulating '{args.model_name}' " + f"({args.model_directory}) using PEtab data from " + f"{args.yaml_file_name}" + ) # load PEtab files problem = petab.Problem.from_yaml(args.yaml_file_name) @@ -88,7 +119,7 @@ def main(): amici_solver.setAbsoluteTolerance(1e-8) amici_solver.setRelativeTolerance(1e-8) amici_solver.setMaxSteps(int(1e4)) - if args.model_name in ('Brannmark_JBC2010', 'Isensee_JCB2018'): + if args.model_name in ("Brannmark_JBC2010", "Isensee_JCB2018"): amici_model.setSteadyStateSensitivityMode( amici.SteadyStateSensitivityMode.integrationOnly ) @@ -96,9 +127,9 @@ def main(): times = dict() for label, sensi_mode in { - 't_sim': amici.SensitivityMethod.none, - 't_fwd': amici.SensitivityMethod.forward, - 't_adj': amici.SensitivityMethod.adjoint + "t_sim": amici.SensitivityMethod.none, + "t_fwd": amici.SensitivityMethod.forward, + "t_adj": amici.SensitivityMethod.adjoint, }.items(): amici_solver.setSensitivityMethod(sensi_mode) if sensi_mode == amici.SensitivityMethod.none: @@ -107,35 +138,39 @@ def main(): amici_solver.setSensitivityOrder(amici.SensitivityOrder.first) res_repeats = [ - simulate_petab(petab_problem=problem, amici_model=amici_model, - solver=amici_solver, log_level=loglevel) + simulate_petab( + petab_problem=problem, + amici_model=amici_model, + solver=amici_solver, + log_level=loglevel, + ) for _ in range(3) # repeat to get more stable timings ] res = res_repeats[0] - times[label] = np.mean([ - sum(r.cpu_time + r.cpu_timeB for r in res[RDATAS]) / 1000 - # only forwards/backwards simulation - for res in res_repeats - ]) + times[label] = np.mean( + [ + sum(r.cpu_time + r.cpu_timeB for r in res[RDATAS]) / 1000 + # only forwards/backwards simulation + for res in res_repeats + ] + ) if sensi_mode == amici.SensitivityMethod.none: rdatas = res[RDATAS] llh = res[LLH] - times['np'] = sum(problem.parameter_df[petab.ESTIMATE]) + times["np"] = sum(problem.parameter_df[petab.ESTIMATE]) - pd.Series(times).to_csv( - f'./tests/benchmark-models/{args.model_name}_benchmark.csv' - ) + pd.Series(times).to_csv(f"./tests/benchmark-models/{args.model_name}_benchmark.csv") for rdata in rdatas: - assert rdata.status == amici.AMICI_SUCCESS, \ - f"Simulation failed for {rdata.id}" + assert rdata.status == amici.AMICI_SUCCESS, f"Simulation failed for {rdata.id}" # create simulation PEtab table - sim_df = rdatas_to_measurement_df(rdatas=rdatas, model=amici_model, - measurement_df=problem.measurement_df) + sim_df = rdatas_to_measurement_df( + rdatas=rdatas, model=amici_model, measurement_df=problem.measurement_df + ) sim_df.rename(columns={petab.MEASUREMENT: petab.SIMULATION}, inplace=True) if args.simulation_file: @@ -148,14 +183,16 @@ def main(): # save figure for plot_id, ax in axs.items(): - fig_path = os.path.join(args.model_directory, - f"{args.model_name}_{plot_id}_vis.png") + fig_path = os.path.join( + args.model_directory, f"{args.model_name}_{plot_id}_vis.png" + ) logger.info(f"Saving figure to {fig_path}") ax.get_figure().savefig(fig_path, dpi=150) if args.check: - references_yaml = os.path.join(os.path.dirname(__file__), - "benchmark_models.yaml") + references_yaml = os.path.join( + os.path.dirname(__file__), "benchmark_models.yaml" + ) with open(references_yaml) as f: refs = yaml.full_load(f) @@ -166,14 +203,15 @@ def main(): rtol = 1e-3 adiff = np.abs(llh - ref_llh) atol = 1e-3 - tolstr = f' Absolute difference is {adiff:.2e} ' \ - f'(tol {atol:.2e}) and relative difference is ' \ - f'{rdiff:.2e} (tol {rtol:.2e}).' + tolstr = ( + f" Absolute difference is {adiff:.2e} " + f"(tol {atol:.2e}) and relative difference is " + f"{rdiff:.2e} (tol {rtol:.2e})." + ) if np.isclose(llh, ref_llh, rtol=rtol, atol=atol): logger.info( - f"Computed llh {llh:.4e} matches reference {ref_llh:.4e}." - + tolstr + f"Computed llh {llh:.4e} matches reference {ref_llh:.4e}." + tolstr ) else: logger.error( @@ -182,13 +220,15 @@ def main(): ) sys.exit(1) except KeyError: - logger.error("No reference likelihood found for " - f"{args.model_name} in {references_yaml}") + logger.error( + "No reference likelihood found for " + f"{args.model_name} in {references_yaml}" + ) for label, key in { - 'simulation': 't_sim', - 'adjoint sensitivity': 't_adj', - 'forward sensitivity': 't_fwd', + "simulation": "t_sim", + "adjoint sensitivity": "t_adj", + "forward sensitivity": "t_fwd", }.items(): try: ref = refs[args.model_name][key] @@ -204,8 +244,10 @@ def main(): f"within reference ({ref:.2e})." ) except KeyError: - logger.error(f"No reference time for {label} found for " - f"{args.model_name} in {references_yaml}") + logger.error( + f"No reference time for {label} found for " + f"{args.model_name} in {references_yaml}" + ) if __name__ == "__main__": diff --git a/tests/conftest.py b/tests/conftest.py index a213d1901c..4d2f5521ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,16 +3,16 @@ import re import sys from pathlib import Path -from typing import List, Tuple, Set +from typing import List, Set, Tuple import pytest - # stores passed SBML semantic test suite IDs passed_ids = [] -SBML_SEMANTIC_CASES_DIR = \ - Path(__file__).parent / 'sbml-test-suite' / 'cases' / 'semantic' +SBML_SEMANTIC_CASES_DIR = ( + Path(__file__).parent / "sbml-test-suite" / "cases" / "semantic" +) @pytest.fixture @@ -29,11 +29,11 @@ def parse_selection(selection_str: str, last: int) -> List[int]: Valid input e.g.: "1", "1,3", "-3,4,6-7" """ indices = [] - for group in selection_str.split(','): - if not re.match(r'^(?:-?\d+|\d+-\d*)$', group): + for group in selection_str.split(","): + if not re.match(r"^(?:-?\d+|\d+-\d*)$", group): print("Invalid selection", group) sys.exit() - spl = group.split('-') + spl = group.split("-") if len(spl) == 1: indices.append(int(spl[0])) elif len(spl) == 2: @@ -46,9 +46,10 @@ def parse_selection(selection_str: str, last: int) -> List[int]: def get_all_semantic_case_ids(): """Get iterator over test sorted IDs of all cases in the SBML semantic suite""" - pattern = re.compile(r'\d{5}') - return sorted(str(x.name) for x in SBML_SEMANTIC_CASES_DIR.iterdir() - if pattern.match(x.name)) + pattern = re.compile(r"\d{5}") + return sorted( + str(x.name) for x in SBML_SEMANTIC_CASES_DIR.iterdir() if pattern.match(x.name) + ) def pytest_addoption(parser): @@ -77,12 +78,12 @@ def pytest_generate_tests(metafunc): def pytest_sessionfinish(session, exitstatus): """Process test results""" global passed_ids - terminalreporter = session.config.pluginmanager.get_plugin( - 'terminalreporter') + terminalreporter = session.config.pluginmanager.get_plugin("terminalreporter") terminalreporter.ensure_newline() # parse test names to get passed case IDs (don't know any better way to # access fixture values) from testSBMLSuite import format_test_id + passed_ids = [format_test_id(_) for _ in passed_ids] if passed_ids: write_passed_tags(passed_ids, terminalreporter) @@ -99,21 +100,22 @@ def write_passed_tags(passed_ids, out=sys.stdout): passed_component_tags |= cur_component_tags passed_test_tags |= cur_test_tags - out.write("\nAt least one test with the following component tags has " - "passed:\n") - out.write(' ' + '\n '.join(sorted(passed_component_tags))) - out.write("\n\nAt least one test with the following test tags has " - "passed:\n") - out.write(' ' + '\n '.join(sorted(passed_test_tags))) + out.write("\nAt least one test with the following component tags has " "passed:\n") + out.write(" " + "\n ".join(sorted(passed_component_tags))) + out.write("\n\nAt least one test with the following test tags has " "passed:\n") + out.write(" " + "\n ".join(sorted(passed_test_tags))) def pytest_runtest_logreport(report: "TestReport") -> None: """Collect test case IDs of passed SBML semantic test suite cases""" - if report.when == 'call'\ - and report.outcome == 'passed'\ - and '::test_sbml_testsuite_case[' in report.nodeid: - test_case_id = re.sub(r'^.*::test_sbml_testsuite_case\[(\d+)].*$', - r'\1', report.nodeid) + if ( + report.when == "call" + and report.outcome == "passed" + and "::test_sbml_testsuite_case[" in report.nodeid + ): + test_case_id = re.sub( + r"^.*::test_sbml_testsuite_case\[(\d+)].*$", r"\1", report.nodeid + ) passed_ids.append(test_case_id) @@ -124,19 +126,19 @@ def get_tags_for_test(test_id: str) -> Tuple[Set[str], Set[str]]: Tuple of set of strings for componentTags and testTags """ current_test_path = SBML_SEMANTIC_CASES_DIR / test_id - info_file = current_test_path / f'{test_id}-model.m' + info_file = current_test_path / f"{test_id}-model.m" with open(info_file) as f: component_tags = set() test_tags = set() for line in f: - if line.startswith('testTags:'): - test_tags = set( - re.split(r'[ ,:]', line[len('testTags:'):].strip())) - test_tags.discard('') - if line.startswith('componentTags:'): + if line.startswith("testTags:"): + test_tags = set(re.split(r"[ ,:]", line[len("testTags:") :].strip())) + test_tags.discard("") + if line.startswith("componentTags:"): component_tags = set( - re.split(r'[ ,:]', line[len('componentTags:'):].strip())) - component_tags.discard('') + re.split(r"[ ,:]", line[len("componentTags:") :].strip()) + ) + component_tags.discard("") if test_tags and component_tags: return component_tags, test_tags print(f"No componentTags or testTags found for test case {test_id}.") diff --git a/tests/cpp/unittests/CMakeLists.txt b/tests/cpp/unittests/CMakeLists.txt index 839bac1290..475c89f220 100644 --- a/tests/cpp/unittests/CMakeLists.txt +++ b/tests/cpp/unittests/CMakeLists.txt @@ -2,7 +2,7 @@ project(unittests) find_package(Boost COMPONENTS serialization) -set(SRC_LIST testMisc.cpp testExpData.cpp) +set(SRC_LIST testMisc.cpp testExpData.cpp testSplines.cpp) add_executable(${PROJECT_NAME} ${SRC_LIST}) diff --git a/tests/cpp/unittests/testExpData.cpp b/tests/cpp/unittests/testExpData.cpp index 83c6dda740..416a41227b 100644 --- a/tests/cpp/unittests/testExpData.cpp +++ b/tests/cpp/unittests/testExpData.cpp @@ -49,6 +49,7 @@ class ExpDataTest : public ::testing::Test { nz, // nz nz, // nztrue nmaxevent, // ne + 0, // nspl 0, // nJ 0, // nw 0, // ndwdx diff --git a/tests/cpp/unittests/testMisc.cpp b/tests/cpp/unittests/testMisc.cpp index a1763c399e..aa3ae226c9 100644 --- a/tests/cpp/unittests/testMisc.cpp +++ b/tests/cpp/unittests/testMisc.cpp @@ -65,6 +65,7 @@ class ModelTest : public ::testing::Test { nz, // nz nz, // nztrue nmaxevent, // ne + 0, // nspl 0, // nJ 0, // nw 0, // ndwdx @@ -302,6 +303,7 @@ class SolverTest : public ::testing::Test { nz, // nz nz, // nztrue ne, // ne + 0, // nspl 0, // nJ 0, // nw 0, // ndwdx diff --git a/tests/cpp/unittests/testSerialization.cpp b/tests/cpp/unittests/testSerialization.cpp index b1b5850dee..5b4fb1ed2a 100644 --- a/tests/cpp/unittests/testSerialization.cpp +++ b/tests/cpp/unittests/testSerialization.cpp @@ -142,6 +142,7 @@ TEST(ModelSerializationTest, ToFile) nz, // nz nz, // nztrue ne, // ne + 0, // nspl 0, // nJ 9, // nw 2, // ndwdx @@ -206,6 +207,7 @@ TEST(ReturnDataSerializationTest, ToString) nz, // nz nz, // nztrue ne, // ne + 0, // nspl 0, // nJ 9, // nw 10, // ndwdx diff --git a/tests/cpp/unittests/testSplines.cpp b/tests/cpp/unittests/testSplines.cpp new file mode 100644 index 0000000000..df17cae33f --- /dev/null +++ b/tests/cpp/unittests/testSplines.cpp @@ -0,0 +1,923 @@ +#include +#include + +#include + +#include +#include +#include + +using std::exp; +using amici::HermiteSpline; +using amici::SplineBoundaryCondition; +using amici::SplineExtrapolation; +using amici::AmiException; + +#define ASSERT_APPROX(x, x0, rtol, atol) ASSERT_LE(std::abs((x) - (x0)), (atol) + (rtol) * std::abs(x0)) + +void test_spline_values( + HermiteSpline const& spline, + std::vector> const& expectations) +{ + for (auto const& [time, expected_value] : expectations) { + ASSERT_DOUBLE_EQ(spline.get_value(time), expected_value); + } +} + +void test_spline_values( + HermiteSpline const& spline, + std::vector> const& expectations, + const double rtol, const double atol) +{ + for (auto const& [time, expected_value] : expectations) { + ASSERT_APPROX(spline.get_value(time), expected_value, rtol, atol); + } +} + +void test_spline_sensitivities( + HermiteSpline const& spline, + std::vector>> const& expectations) +{ + for (auto const& [time, expected_values] : expectations) { + for (std::vector::size_type ip = 0; ip < expected_values.size(); ip++) + ASSERT_DOUBLE_EQ(spline.get_sensitivity(time, ip), expected_values[ip]); + } +} + +void test_spline_sensitivities( + HermiteSpline const& spline, + std::vector>> const& expectations, + const double rtol, const double atol) +{ + for (auto const& [time, expected_values] : expectations) { + for (std::vector::size_type ip = 0; ip < expected_values.size(); ip++) + ASSERT_APPROX(spline.get_sensitivity(time, ip), expected_values[ip], rtol, atol); + } +} + +TEST(Splines, SplineUniform) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {0.00, 0.0}, + {0.25, 1.74609375}, + {1.0/3, 2.0}, + {0.50, 1.3437499999999996}, + {2.0/3, 0.5}, + {0.75, 0.484375}, + {1.00, 1.0}, + }; + test_spline_values(spline, expectations); + ASSERT_THROW(spline.get_value(-0.05), AmiException); + ASSERT_THROW(spline.get_value(1.05), AmiException); +} + +TEST(Splines, SplineNonUniform) +{ + // Non-uniform grid + HermiteSpline spline({ 0.0, 0.1, 0.5, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + false, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {0.00, 0.0}, + {0.05, 1.1484375}, + {0.10, 2.0}, + {0.25, 2.0498046875}, + {0.50, 0.5}, + {0.75, 0.6015625}, + {1.00, 1.0}, + }; + test_spline_values(spline, expectations); + ASSERT_THROW(spline.get_value(-0.05), AmiException); + ASSERT_THROW(spline.get_value(1.05), AmiException); +} + +TEST(Splines, SplineExplicit) +{ + // Derivatives are given explicitly + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0, 0.75 }, + { 1.0, 0.0, 0.1, -0.1, 0.0 }, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + false, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {0.0, 0.0}, + {0.2, 1.8000000000000003}, + {0.25, 2.0}, + {0.4, 1.0243999999999998}, + {0.5, 0.5}, + {0.6, 0.6819999999999999}, + {0.75, 1.0}, + {0.8, 0.9707999999999999}, + {1.0, 0.75}, + }; + test_spline_values(spline, expectations); + ASSERT_THROW(spline.get_value(-0.05), AmiException); + ASSERT_THROW(spline.get_value(1.05), AmiException); +} + +TEST(Splines, SplineZeroBC) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::zeroDerivative, + SplineBoundaryCondition::zeroDerivative, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {0.0, 0.0}, + {0.25, 1.65234375}, + {0.5, 1.3437499999999996}, + {0.75, 0.5078125}, + {1.0, 1.0}, + }; + test_spline_values(spline, expectations); + ASSERT_THROW(spline.get_value(-0.05), AmiException); + ASSERT_THROW(spline.get_value(1.05), AmiException); +} + +TEST(Splines, SplineLogarithmic) +{ + // Logarithmic parametrization + HermiteSpline spline({ 0.0, 1.0 }, + { 0.2, 2.0, 0.5, 1.0, 0.75 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + true); // logarithmic_parametrization + // log-space values [-1.60943791, 0.69314718, -0.69314718, 0, -0.28768207] + // log-space derivatives [36, 0.3, -4, 0.5, -1.33333333] + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {0.0, 0.2}, + {0.2, 2.07939779651678}, + {0.25, 2.0}, + {0.4, 0.947459046694449}, + {0.5, 0.5}, + {0.6, 0.545987404053269}, + {0.75, 1.0}, + {0.8, 0.996753014029391}, + {1.0, 0.75}, + }; + test_spline_values(spline, expectations, 1e-14, 0.0); + ASSERT_THROW(spline.get_value(-0.05), AmiException); + ASSERT_THROW(spline.get_value(1.05), AmiException); +} + +TEST(Splines, SplineUniformConstantExtrapolation) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::constant, + SplineExtrapolation::constant, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {-2.00, 0.0}, + {-1.00, 0.0}, + { 0.00, 0.0}, + { 0.25, 1.74609375}, + { 1.0/3, 2.0}, + { 0.50, 1.3437499999999996}, + { 2.0/3, 0.5}, + { 0.75, 0.484375}, + { 1.00, 1.0}, + { 2.00, 1.0}, + { 3.00, 1.0}, + }; + test_spline_values(spline, expectations); +} + +TEST(Splines, SplineUniformLinearExtrapolation) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::linear, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {-2.00, -12.0}, + {-1.00, -6.0}, + { 0.00, 0.0}, + { 0.25, 1.74609375}, + { 1.0/3, 2.0}, + { 0.50, 1.3437499999999996}, + { 2.0/3, 0.5}, + { 0.75, 0.484375}, + { 1.00, 1.0}, + { 2.00, 2.5}, + { 3.00, 4.0}, + }; + test_spline_values(spline, expectations); +} + +TEST(Splines, SplineUniformPolynomialExtrapolation) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 0.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::polynomial, + SplineExtrapolation::polynomial, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {-2.00, 429.0}, + {-1.00, 57.0}, + { 0.00, 0.0}, + { 0.25, 1.74609375}, + { 1.0/3, 2.0}, + { 0.50, 1.3437499999999996}, + { 2.0/3, 0.5}, + { 0.75, 0.484375}, + { 1.00, 1.0}, + { 2.00, -33.5}, + { 3.00, -248.0}, + }; + test_spline_values(spline, expectations); +} + +TEST(Splines, SplineUniformPeriodicExtrapolation) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 1.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::periodic, + SplineBoundaryCondition::periodic, + SplineExtrapolation::periodic, + SplineExtrapolation::periodic, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {-4.0/3, 0.5}, + {-0.50, 1.2812499999999996}, + { 0.00, 1.0}, + { 0.25, 1.9140625}, + { 1.0/3, 2.0}, + { 0.50, 1.2812499999999996}, + { 2.0/3, 0.5}, + { 0.75, 0.47265625}, + { 1.00, 1.0}, + { 1.25, 1.9140625}, + { 2.75, 0.47265625}, + }; + test_spline_values(spline, expectations); +} + +TEST(Splines, SplineNonUniformPeriodicExtrapolation) +{ + // Non-uniform grid + HermiteSpline spline({ 0.0, 0.1, 0.5, 1.0 }, + { 1.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::periodic, + SplineBoundaryCondition::periodic, + SplineExtrapolation::periodic, + SplineExtrapolation::periodic, + true, // node_derivative_by_FD + false, // equidistant_spacing + false); // logarithmic_parametrization + + spline.compute_coefficients(); + std::vector> expectations = { + // t, expected value + {-1.90, 2.0}, + {-0.25, 0.3203125}, + { 0.00, 1.0}, + { 0.05, 1.5296875}, + { 0.10, 2.0}, + { 0.25, 1.7568359375}, + { 0.50, 0.5}, + { 0.75, 0.3203125}, + { 1.00, 1.0}, + { 1.50, 0.5}, + { 2.05, 1.5296875}, + }; + test_spline_values(spline, expectations, 1e-14, 0.0); +} + +TEST(Splines, SplineUniformSensitivity) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + std::vector>> expectations = { + // t, expected values of sensitivities + {0.00, {3.0, 1.0, 0.0}}, + {0.25, {0.539062, 0.179688, 4.45312}}, + {1.0/3, {0.0, 0.0, 5.0}}, + {0.50, {0.1875, -0.125, 2.625}}, + {2.0/3, {0.0, 0.0, 0.0}}, + {0.75, {-1.07812, 0.179688, 0.1875}}, + {1.00, {-6.0, 1.0, 3.0}}, + }; + test_spline_sensitivities(spline, expectations, 1e-5, 1e-6); + ASSERT_THROW(spline.get_sensitivity(-0.05, 0), AmiException); + ASSERT_THROW(spline.get_sensitivity( 1.05, 1), AmiException); +} + +TEST(Splines, SplineNonUniformSensitivity) +{ + HermiteSpline spline({ 0.0, 0.1, 0.5, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + false, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + std::vector>> expectations = { + // t, expected values of sensitivities + {0.00, { 3.0, 1.0, 0.0}}, + {0.05, { 1.3125, 0.4375, 2.89062}}, + {0.10, { 0.0, 0.0, 5.0}}, + {0.30, {-0.45, -0.3, 3.6}}, + {0.50, { 0.0, 0.0, 0.0}}, + {0.75, {-2.625, 0.4375, 0.921875}}, + {1.00, {-6.0, 1.0, 3.0}}, + }; + test_spline_sensitivities(spline, expectations, 1e-5, 1e-6); + ASSERT_THROW(spline.get_sensitivity(-0.05, 0), AmiException); + ASSERT_THROW(spline.get_sensitivity( 1.05, 1), AmiException); +} + +TEST(Splines, SplineExplicitSensitivity) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + { 13.625, 7.5, 1.1585290151921035, 1.0 }, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + false, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + std::vector dslopesdp = { + 0.0, 0.0, 18.75, + 0.0, 1.0, 3.0, + 4.0, -0.540302, 0.0, + 0.0, 0.0, 0.0, + }; + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + std::vector>> expectations = { + // t, expected values of sensitivities + {0.00, { 3.0, 1.0, 0.0}}, + {0.25, { 0.46875, 0.109375, 4.37109}}, + {1.0/3, { 0.0, 0.0, 5.0}}, + {0.50, {-0.166667, 0.0641793, 2.625}}, + {2.0/3, { 0.0, 0.0, 0.0}}, + {0.75, {-0.75, 0.130923, 0.46875}}, + {1.00, {-6.0, 1.0, 3.0}}, + }; + test_spline_sensitivities(spline, expectations, 1e-5, 0.0); + ASSERT_THROW(spline.get_sensitivity(-0.05, 0), AmiException); + ASSERT_THROW(spline.get_sensitivity( 1.05, 1), AmiException); +} + +TEST(Splines, SplineZeroDerivativeSensitivity) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::zeroDerivative, + SplineBoundaryCondition::zeroDerivative, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + std::vector>> expectations = { + // t, expected values of sensitivities + {0.00, { 3.0, 1.0, 0.0}}, + {0.25, { 0.679688, 0.226562, 4.21875}}, + {1.0/3, { 0.0, 0.0, 5.0}}, + {0.50, {0.1875, -0.125, 2.625}}, + {2.0/3, { 0.0, 0.0, 0.0}}, + {0.75, {-1.35938, 0.226562, 0.328125}}, + {1.00, {-6.0, 1.0, 3.0}}, + }; + test_spline_sensitivities(spline, expectations, 1e-5, 0.0); + ASSERT_THROW(spline.get_sensitivity(-0.05, 0), AmiException); + ASSERT_THROW(spline.get_sensitivity( 1.05, 1), AmiException); +} + +TEST(Splines, SplineLogarithmicSensitivity) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::noExtrapolation, + true, // node_derivative_by_FD + true, // equidistant_spacing + true); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + std::vector>> expectations = { + // t, expected values of sensitivities + {0.00, { 3.0, 1.0, 0.0}}, + {0.25, { 0.585881, 0.195294, 4.38532}}, + {1.0/3, { 0.0, 0.0, 5.0}}, + {0.50, { 0.514003, -0.132395, 1.52044}}, + {2.0/3, { 0.0, 0.0, 0.0}}, + {0.75, {-0.820743, 0.13679, -0.0577988}}, + {1.00, {-6.0, 1.0, 3.0}}, + }; + test_spline_sensitivities(spline, expectations, 1e-6, 1e-6); + ASSERT_THROW(spline.get_sensitivity(-0.05, 0), AmiException); + ASSERT_THROW(spline.get_sensitivity( 1.05, 1), AmiException); +} + +TEST(Splines, SplineFinalValue_ConstantExtrapolation) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::constant, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 4.5); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), -6.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 1.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 3.0); +} + +TEST(Splines, SplineFinalValue_LinearExtrapolationPositiveDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), INFINITY); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 0.0); +} + +TEST(Splines, SplineFinalValue_LinearExtrapolationNegativeDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 0.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), -INFINITY); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 0.0); +} + +TEST(Splines, SplineFinalValue_LinearExtrapolationZeroDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 1.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 1.0); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(0))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(1))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(2))); +} + +TEST(Splines, SplineFinalValue_LinearExtrapolationZeroDerivativeByBC) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 2.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::zeroDerivative, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 2.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), -6.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 1.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 3.0); +} + +TEST(Splines, SplineFinalValue_PolynomialExtrapolationPositive) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { -8.0, -6.0, -1.0, -2.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::polynomial, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), INFINITY); + /* NB sensitivities for this case are not implemented, since they are unlikely to be used*/ + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(0))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(1))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(2))); +} + +TEST(Splines, SplineFinalValue_PolynomialExtrapolationNegative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 2.0 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::polynomial, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), -INFINITY); + /* NB sensitivities for this case are not implemented, since they are unlikely to be used*/ + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(0))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(1))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(2))); +} + +TEST(Splines, SplineFinalValue_PeriodicExtrapolation) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 1.0, 2.0, 0.5, 1.0 }, + {}, + SplineBoundaryCondition::periodic, + SplineBoundaryCondition::periodic, + SplineExtrapolation::periodic, + SplineExtrapolation::periodic, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_TRUE(std::isnan(spline.get_final_value())); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(0))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(1))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(2))); +} + +TEST(Splines, SplineFinalValue_PeriodicExtrapolationConstant) +{ + // Uniform grid + HermiteSpline spline({ 0.0, 1.0 }, + { 1.0, 1.0, 1.0, 1.0 }, + {}, + SplineBoundaryCondition::periodic, + SplineBoundaryCondition::periodic, + SplineExtrapolation::periodic, + SplineExtrapolation::periodic, + true, // node_derivative_by_FD + true, // equidistant_spacing + false); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 1.0); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(0))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(1))); + ASSERT_TRUE(std::isnan(spline.get_final_sensitivity(2))); +} + +TEST(Splines, SplineFinalValue_LogarithmicPositiveDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 4.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + true); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), INFINITY); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 0.0); +} + +TEST(Splines, SplineFinalValue_LogarithmicNegativeDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 0.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::linear, + true, // node_derivative_by_FD + true, // equidistant_spacing + true); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 0.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 0.0); +} + +TEST(Splines, SplineFinalValue_LogarithmicZeroDerivative) +{ + HermiteSpline spline({ 0.0, 1.0 }, + { 2.5, 3.25, 1.0, 0.5 }, + {}, + SplineBoundaryCondition::given, + SplineBoundaryCondition::given, + SplineExtrapolation::noExtrapolation, + SplineExtrapolation::constant, + true, // node_derivative_by_FD + true, // equidistant_spacing + true); // logarithmic_parametrization + int n_params = 3; + std::vector dvaluesdp = { + 3.0, 1.0, 0.0, + 0.0, 0.0, 5.0, + 0.0, 0.0, 0.0, + -6.0, 1.0, 3.0 + }; + auto dslopesdp = std::vector(spline.n_nodes() * n_params); + spline.compute_coefficients(); + spline.compute_coefficients_sensi(n_params, 0, dvaluesdp, dslopesdp); + spline.compute_final_value(); + spline.compute_final_sensitivity(n_params, 0, dvaluesdp, dslopesdp); + ASSERT_DOUBLE_EQ(spline.get_final_value(), 0.5); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(0), -6.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(1), 1.0); + ASSERT_DOUBLE_EQ(spline.get_final_sensitivity(2), 3.0); +} diff --git a/tests/cpp/wrapTestModels.m b/tests/cpp/wrapTestModels.m index 28963e2e3a..80d8d05936 100644 --- a/tests/cpp/wrapTestModels.m +++ b/tests/cpp/wrapTestModels.m @@ -4,14 +4,14 @@ function wrapTestModels() % % Return values: % void - + amiciPath = fileparts(mfilename('fullpath')); amiciPath = [amiciPath '/../../matlab']; - + %% EXAMPLE STEADYSTATE - + cd([amiciPath '/examples/example_steadystate/']); - + try [exdir,~,~]=fileparts(which('example_steadystate.m')); amiwrap('model_steadystate','model_steadystate_syms',exdir); @@ -19,10 +19,10 @@ function wrapTestModels() disp(err.message) cd(fileparts(mfilename('fullpath'))); end - + %% EXAMPLE DIRAC cd([amiciPath '/examples/example_dirac/']); - + try [exdir,~,~]=fileparts(which('example_dirac.m')); amiwrap('model_dirac','model_dirac_syms',exdir); @@ -30,10 +30,10 @@ function wrapTestModels() disp(err.message) cd(fileparts(mfilename('fullpath'))); end - + %% EXAMPLE JAKSTAT cd([amiciPath '/examples/example_jakstat_adjoint/']); - + try [exdir,~,~]=fileparts(which('example_jakstat_adjoint.m')); amiwrap('model_jakstat_adjoint', 'model_jakstat_adjoint_syms', exdir, 1); @@ -44,7 +44,7 @@ function wrapTestModels() %% EXAMPLE NEURON cd([amiciPath '/examples/example_neuron/']); - + try [exdir,~,~]=fileparts(which('example_neuron.m')); amiwrap('model_neuron', 'model_neuron_syms', exdir, 1); @@ -54,10 +54,10 @@ function wrapTestModels() end cd(fileparts(mfilename('fullpath'))); - + %% EXAMPLE EVENTS cd([amiciPath '/examples/example_events/']); - + try [exdir,~,~]=fileparts(which('example_events.m')); amiwrap('model_events', 'model_events_syms', exdir); @@ -65,10 +65,10 @@ function wrapTestModels() disp(err.message) cd(fileparts(mfilename('fullpath'))); end - + %% EXAMPLE NESTED EVENTS cd([amiciPath '/examples/example_nested_events/']); - + try [exdir,~,~]=fileparts(which('example_nested_events.m')); amiwrap('model_nested_events', 'model_nested_events_syms', exdir); @@ -78,10 +78,10 @@ function wrapTestModels() end cd(fileparts(mfilename('fullpath'))); - + %% EXAMPLE ROBERTSON cd([amiciPath '/examples/example_robertson/']); - + try [exdir,~,~]=fileparts(which('example_robertson.m')); amiwrap('model_robertson', 'model_robertson_syms', exdir); @@ -91,10 +91,10 @@ function wrapTestModels() end cd(fileparts(mfilename('fullpath'))); - + %% EXAMPLE CALVETTI cd([amiciPath '/examples/example_calvetti/']); - + try [exdir,~,~]=fileparts(which('example_calvetti.m')); amiwrap('model_calvetti', 'model_calvetti_syms', exdir); @@ -104,6 +104,5 @@ function wrapTestModels() end cd(fileparts(mfilename('fullpath'))); - -end +end diff --git a/tests/generateTestConfig/example.py b/tests/generateTestConfig/example.py index 6fad77dfa9..a0b2891344 100644 --- a/tests/generateTestConfig/example.py +++ b/tests/generateTestConfig/example.py @@ -2,81 +2,77 @@ import numpy as np import pandas as pd + def dict2hdf5(object, dictionary): for key, value in dictionary.items(): if isArray(value): a = np.array(value) if not len(value): - dtype = 'f8' + dtype = "f8" elif isArray(value[0]): if isinstance(value[0][0], (np.float64, float)): - dtype = 'f8' + dtype = "f8" else: - dtype = ' List[int]: Valid input e.g.: "1", "1,3", "-3,4,6-7" """ indices = [] - for group in selection_str.split(','): - if not re.match(r'^(?:-?\d+)|(?:\d+(?:-\d+))$', group): + for group in selection_str.split(","): + if not re.match(r"^(?:-?\d+)|(?:\d+(?:-\d+))$", group): print("Invalid selection", group) sys.exit() - spl = group.split('-') + spl = group.split("-") if len(spl) == 1: indices.append(int(spl[0])) elif len(spl) == 2: @@ -31,20 +32,19 @@ def parse_selection(selection_str: str) -> List[int]: def pytest_addoption(parser): """Add pytest CLI options""" parser.addoption("--petab-cases", help="Test cases to run") - # TODO: re-enable in #1800 - # parser.addoption("--only-pysb", help="Run only PySB tests", - # action="store_true") - parser.addoption("--only-sbml", help="Run only SBML tests", - action="store_true", ) + parser.addoption("--only-pysb", help="Run only PySB tests", action="store_true") + parser.addoption( + "--only-sbml", + help="Run only SBML tests", + action="store_true", + ) def pytest_generate_tests(metafunc): """Parameterize tests""" # Run for all PEtab test suite cases - if "case" in metafunc.fixturenames \ - and "model_type" in metafunc.fixturenames: - + if "case" in metafunc.fixturenames and "model_type" in metafunc.fixturenames: # Get CLI option cases = metafunc.config.getoption("--petab-cases") if cases: @@ -56,22 +56,25 @@ def pytest_generate_tests(metafunc): if metafunc.config.getoption("--only-sbml"): argvalues = [ - (case, 'sbml', version) - for version in ('v1.0.0', ) - for case in (test_numbers if test_numbers - else get_cases("sbml", version=version)) + (case, "sbml", version) + for version in ("v1.0.0", "v2.0.0") + for case in ( + test_numbers if test_numbers else get_cases("sbml", version=version) + ) + ] + elif metafunc.config.getoption("--only-pysb"): + argvalues = [ + (case, "pysb", "v2.0.0") + for case in ( + test_numbers + if test_numbers + else get_cases("pysb", version="v2.0.0") + ) ] - # TODO: re-enable in #1800 - # elif metafunc.config.getoption("--only-pysb"): - # argvalues = [ - # (case, 'pysb', "v1.0.0") - # for case in (test_numbers if test_numbers - # else get_cases("pysb", version="v1.0.0")) - # ] else: argvalues = [] - for version in ('v1.0.0',): - for format in ('sbml',): + for version in ("v1.0.0", "v2.0.0"): + for format in ("sbml", "pysb"): argvalues.extend( (case, format, version) for case in test_numbers or get_cases(format, version) diff --git a/tests/petab_test_suite/test_petab_suite.py b/tests/petab_test_suite/test_petab_suite.py index 2e1761ef61..59e2ce7723 100755 --- a/tests/petab_test_suite/test_petab_suite.py +++ b/tests/petab_test_suite/test_petab_suite.py @@ -4,19 +4,21 @@ import logging import sys +import amici import pandas as pd import petab import petabtests import pytest from _pytest.outcomes import Skipped - -import amici from amici import SteadyStateSensitivityMode from amici.gradient_check import check_derivatives as amici_check_derivatives from amici.logging import get_logger, set_log_level -from amici.petab_import import PysbPetabProblem, import_petab_problem -from amici.petab_objective import (create_parameterized_edatas, - rdatas_to_measurement_df, simulate_petab) +from amici.petab_import import import_petab_problem +from amici.petab_objective import ( + create_parameterized_edatas, + rdatas_to_measurement_df, + simulate_petab, +) logger = get_logger(__name__, logging.DEBUG) set_log_level(get_logger("amici.petab_import"), logging.DEBUG) @@ -29,11 +31,14 @@ def test_case(case, model_type, version): try: _test_case(case, model_type, version) except Exception as e: - if isinstance(e, NotImplementedError) \ - or "Timepoint-specific parameter overrides" in str(e): - logger.info(f"Case {case} expectedly failed. " - "Required functionality is not yet " - f"implemented: {e}") + if isinstance( + e, NotImplementedError + ) or "Timepoint-specific parameter overrides" in str(e): + logger.info( + f"Case {case} expectedly failed. " + "Required functionality is not yet " + f"implemented: {e}" + ) pytest.skip(str(e)) else: raise e @@ -50,16 +55,16 @@ def _test_case(case, model_type, version): problem = petab.Problem.from_yaml(yaml_file) # compile amici model - if case.startswith('0006') and model_type != "pysb": + if case.startswith("0006"): petab.flatten_timepoint_specific_output_overrides(problem) - model_name = f"petab_{model_type}_test_case_{case}"\ - f"_{version.replace('.', '_')}" - model_output_dir = f'amici_models/{model_name}' + model_name = f"petab_{model_type}_test_case_{case}" f"_{version.replace('.', '_')}" + model_output_dir = f"amici_models/{model_name}" model = import_petab_problem( petab_problem=problem, model_output_dir=model_output_dir, model_name=model_name, - force_compile=True) + force_compile=True, + ) solver = model.getSolver() solver.setSteadyStateToleranceFactor(1.0) @@ -71,23 +76,23 @@ def _test_case(case, model_type, version): log_level=logging.DEBUG, ) - rdatas = ret['rdatas'] - chi2 = sum(rdata['chi2'] for rdata in rdatas) - llh = ret['llh'] - simulation_df = rdatas_to_measurement_df(rdatas, model, - problem.measurement_df) + rdatas = ret["rdatas"] + chi2 = sum(rdata["chi2"] for rdata in rdatas) + llh = ret["llh"] + simulation_df = rdatas_to_measurement_df(rdatas, model, problem.measurement_df) petab.check_measurement_df(simulation_df, problem.observable_df) - simulation_df = simulation_df.rename( - columns={petab.MEASUREMENT: petab.SIMULATION}) + simulation_df = simulation_df.rename(columns={petab.MEASUREMENT: petab.SIMULATION}) simulation_df[petab.TIME] = simulation_df[petab.TIME].astype(int) solution = petabtests.load_solution(case, model_type, version=version) gt_chi2 = solution[petabtests.CHI2] gt_llh = solution[petabtests.LLH] gt_simulation_dfs = solution[petabtests.SIMULATION_DFS] - if case.startswith('0006'): + if case.startswith("0006"): # account for flattening - gt_simulation_dfs[0].loc[:, petab.OBSERVABLE_ID] = ('obs_a__10__c0', - 'obs_a__15__c0') + gt_simulation_dfs[0].loc[:, petab.OBSERVABLE_ID] = ( + "obs_a__10__c0", + "obs_a__15__c0", + ) tol_chi2 = solution[petabtests.TOL_CHI2] tol_llh = solution[petabtests.TOL_LLH] tol_simulations = solution[petabtests.TOL_SIMULATIONS] @@ -95,41 +100,43 @@ def _test_case(case, model_type, version): chi2s_match = petabtests.evaluate_chi2(chi2, gt_chi2, tol_chi2) llhs_match = petabtests.evaluate_llh(llh, gt_llh, tol_llh) simulations_match = petabtests.evaluate_simulations( - [simulation_df], gt_simulation_dfs, tol_simulations) + [simulation_df], gt_simulation_dfs, tol_simulations + ) - logger.log(logging.DEBUG if simulations_match else logging.ERROR, - f"Simulations: match = {simulations_match}") + logger.log( + logging.DEBUG if simulations_match else logging.ERROR, + f"Simulations: match = {simulations_match}", + ) if not simulations_match: - with pd.option_context('display.max_rows', None, - 'display.max_columns', None, - 'display.width', 200): - logger.log(logging.DEBUG, f"x_ss: {model.getStateIds()} " - f"{[rdata.x_ss for rdata in rdatas]}") - logger.log(logging.ERROR, - f"Expected simulations:\n{gt_simulation_dfs}") - logger.log(logging.ERROR, - f"Actual simulations:\n{simulation_df}") - logger.log(logging.DEBUG if chi2s_match else logging.ERROR, - f"CHI2: simulated: {chi2}, expected: {gt_chi2}," - f" match = {chi2s_match}") - logger.log(logging.DEBUG if simulations_match else logging.ERROR, - f"LLH: simulated: {llh}, expected: {gt_llh}, " - f"match = {llhs_match}") + with pd.option_context( + "display.max_rows", None, "display.max_columns", None, "display.width", 200 + ): + logger.log( + logging.DEBUG, + f"x_ss: {model.getStateIds()} " f"{[rdata.x_ss for rdata in rdatas]}", + ) + logger.log(logging.ERROR, f"Expected simulations:\n{gt_simulation_dfs}") + logger.log(logging.ERROR, f"Actual simulations:\n{simulation_df}") + logger.log( + logging.DEBUG if chi2s_match else logging.ERROR, + f"CHI2: simulated: {chi2}, expected: {gt_chi2}," f" match = {chi2s_match}", + ) + logger.log( + logging.DEBUG if simulations_match else logging.ERROR, + f"LLH: simulated: {llh}, expected: {gt_llh}, " f"match = {llhs_match}", + ) check_derivatives(problem, model, solver) if not all([llhs_match, simulations_match]) or not chi2s_match: logger.error(f"Case {case} failed.") - raise AssertionError(f"Case {case}: Test results do not match " - "expectations") + raise AssertionError(f"Case {case}: Test results do not match " "expectations") logger.info(f"Case {case} passed.") def check_derivatives( - problem: petab.Problem, - model: amici.Model, - solver: amici.Solver + problem: petab.Problem, model: amici.Model, solver: amici.Solver ) -> None: """Check derivatives using finite differences for all experimental conditions @@ -139,18 +146,21 @@ def check_derivatives( model: AMICI model matching ``problem`` solver: AMICI solver """ - problem_parameters = {t.Index: getattr(t, petab.NOMINAL_VALUE) for t in - problem.parameter_df.itertuples()} + problem_parameters = { + t.Index: getattr(t, petab.NOMINAL_VALUE) + for t in problem.parameter_df.itertuples() + } solver.setSensitivityMethod(amici.SensitivityMethod.forward) solver.setSensitivityOrder(amici.SensitivityOrder.first) # Required for case 9 to not fail in # amici::NewtonSolver::computeNewtonSensis model.setSteadyStateSensitivityMode( - SteadyStateSensitivityMode.integrateIfNewtonFails) + SteadyStateSensitivityMode.integrateIfNewtonFails + ) for edata in create_parameterized_edatas( - amici_model=model, petab_problem=problem, - problem_parameters=problem_parameters): + amici_model=model, petab_problem=problem, problem_parameters=problem_parameters + ): # check_derivatives does currently not support parameters in ExpData model.setParameters(edata.parameters) model.setParameterScale(edata.pscale) @@ -164,12 +174,12 @@ def run(): n_success = 0 n_skipped = 0 n_total = 0 - for version in ("v1.0.0",): - cases = petabtests.get_cases('sbml', version=version) + for version in ("v1.0.0", "v2.0.0"): + cases = petabtests.get_cases("sbml", version=version) n_total += len(cases) for case in cases: try: - test_case(case, 'sbml', version=version) + test_case(case, "sbml", version=version) n_success += 1 except Skipped: n_skipped += 1 @@ -178,11 +188,10 @@ def run(): logger.error(f"Case {case} failed.") logger.error(e) - logger.info(f"{n_success} / {n_total} successful, " - f"{n_skipped} skipped") + logger.info(f"{n_success} / {n_total} successful, " f"{n_skipped} skipped") if n_success != len(cases): sys.exit(1) -if __name__ == '__main__': +if __name__ == "__main__": run() diff --git a/tests/testSBMLSuite.py b/tests/testSBMLSuite.py index 51c5046535..f11870b60d 100755 --- a/tests/testSBMLSuite.py +++ b/tests/testSBMLSuite.py @@ -16,20 +16,19 @@ import sys from pathlib import Path +import amici import libsbml as sbml import numpy as np import pandas as pd import pytest -from numpy.testing import assert_allclose - -import amici from amici.constants import SymbolId from amici.gradient_check import check_derivatives +from numpy.testing import assert_allclose @pytest.fixture(scope="session") def result_path() -> Path: - return Path(__file__).parent / 'amici-semantic-results' + return Path(__file__).parent / "amici-semantic-results" @pytest.fixture(scope="function", autouse=True) @@ -45,11 +44,7 @@ def sbml_test_dir(): sys.path = old_path -def test_sbml_testsuite_case( - test_number, - result_path, - sbml_semantic_cases_dir -): +def test_sbml_testsuite_case(test_number, result_path, sbml_semantic_cases_dir): test_id = format_test_id(test_number) model_dir = None @@ -60,43 +55,43 @@ def test_sbml_testsuite_case( # key: case ID; value: epsilon for finite differences sensitivity_check_cases = { # parameter-dependent conservation laws - '00783': 1.5e-2, + "00783": 1.5e-2, # initial events - '00995': 1e-3, + "00995": 1e-3, } try: current_test_path = sbml_semantic_cases_dir / test_id # parse expected results - results_file = current_test_path / f'{test_id}-results.csv' - results = pd.read_csv(results_file, delimiter=',') - results.rename(columns={c: c.replace(' ', '') - for c in results.columns}, - inplace=True) + results_file = current_test_path / f"{test_id}-results.csv" + results = pd.read_csv(results_file, delimiter=",") + results.rename( + columns={c: c.replace(" ", "") for c in results.columns}, inplace=True + ) # setup model - model_dir = Path(__file__).parent / 'SBMLTestModels' / test_id + model_dir = Path(__file__).parent / "SBMLTestModels" / test_id model, solver, wrapper = compile_model( - current_test_path, test_id, model_dir, - generate_sensitivity_code=test_id in sensitivity_check_cases) + current_test_path, + test_id, + model_dir, + generate_sensitivity_code=test_id in sensitivity_check_cases, + ) settings = read_settings_file(current_test_path, test_id) atol, rtol = apply_settings(settings, solver, model, test_id) # simulate model rdata = amici.runAmiciSimulation(model, solver) - if rdata['status'] != amici.AMICI_SUCCESS: - if test_id in ( - '00748', '00374', '00369' - ): - pytest.skip('Simulation Failed expectedly') + if rdata["status"] != amici.AMICI_SUCCESS: + if test_id in ("00748", "00374", "00369"): + pytest.skip("Simulation Failed expectedly") else: - raise RuntimeError('Simulation failed unexpectedly') + raise RuntimeError("Simulation failed unexpectedly") # verify - simulated = verify_results(settings, rdata, results, wrapper, - model, atol, rtol) + simulated = verify_results(settings, rdata, results, wrapper, model, atol, rtol) # record results write_result_file(simulated, test_id, result_path) @@ -114,36 +109,33 @@ def test_sbml_testsuite_case( shutil.rmtree(model_dir, ignore_errors=True) -def verify_results( - settings, rdata, expected, wrapper, - model, atol, rtol -): +def verify_results(settings, rdata, expected, wrapper, model, atol, rtol): """Verify test results""" amount_species, variables = get_amount_and_variables(settings) # collect states simulated = pd.DataFrame( - rdata['y'], - columns=[obs['name'] - for obs in wrapper.symbols[SymbolId.OBSERVABLE].values()] + rdata["y"], + columns=[obs["name"] for obs in wrapper.symbols[SymbolId.OBSERVABLE].values()], ) - simulated['time'] = rdata['ts'] + simulated["time"] = rdata["ts"] # collect parameters for par in model.getParameterIds(): - simulated[par] = rdata['ts'] * 0 + model.getParameterById(par) + simulated[par] = rdata["ts"] * 0 + model.getParameterById(par) # collect fluxes for expr_idx, expr_id in enumerate(model.getExpressionIds()): if expr_id.startswith("flux_"): simulated[expr_id.removeprefix("flux_")] = rdata.w[:, expr_idx] # handle renamed reserved symbols - simulated.rename(columns={c: c.replace('amici_', '') - for c in simulated.columns}, inplace=True) + simulated.rename( + columns={c: c.replace("amici_", "") for c in simulated.columns}, inplace=True + ) # SBML test suite case 01308 defines species with initialAmount and # hasOnlySubstanceUnits="true", but then request results as concentrations. requested_concentrations = [ - s for s in - settings['concentration'].replace(' ', '').replace('\n', '').split(',') + s + for s in settings["concentration"].replace(" ", "").replace("\n", "").split(",") if s ] # We only need to convert species that have only substance units @@ -153,13 +145,15 @@ def verify_results( **wrapper.symbols[SymbolId.SPECIES], **wrapper.symbols[SymbolId.ALGEBRAIC_STATE], }.items() - if str(state_id) in requested_concentrations and state.get('amount', False) + if str(state_id) in requested_concentrations and state.get("amount", False) ] - amounts_to_concentrations(concentration_species, wrapper, - simulated, requested_concentrations) + amounts_to_concentrations( + concentration_species, wrapper, simulated, requested_concentrations + ) - concentrations_to_amounts(amount_species, wrapper, simulated, - requested_concentrations) + concentrations_to_amounts( + amount_species, wrapper, simulated, requested_concentrations + ) # simulated may contain `object` dtype columns and `expected` may # contain `np.int64` columns, so we cast everything to `np.float64`. @@ -170,18 +164,19 @@ def verify_results( except KeyError as e: raise KeyError(f"Missing simulated value for `{variable}`") from e assert_allclose( - actual, expectation, atol, rtol, equal_nan=True, - err_msg=f"Mismatch for {variable}" + actual, + expectation, + atol, + rtol, + equal_nan=True, + err_msg=f"Mismatch for {variable}", ) - return simulated[variables + ['time']] + return simulated[variables + ["time"]] def amounts_to_concentrations( - amount_species, - wrapper, - simulated, - requested_concentrations + amount_species, wrapper, simulated, requested_concentrations ): """ Convert AMICI simulated amounts to concentrations @@ -197,18 +192,16 @@ def amounts_to_concentrations( This allows for the reuse of the concentrations_to_amounts method... """ for species in amount_species: - if species != '': + if species != "": simulated.loc[:, species] = 1 / simulated.loc[:, species] - concentrations_to_amounts([species], wrapper, simulated, - requested_concentrations) + concentrations_to_amounts( + [species], wrapper, simulated, requested_concentrations + ) simulated.loc[:, species] = 1 / simulated.loc[:, species] def concentrations_to_amounts( - amount_species, - wrapper, - simulated, - requested_concentrations + amount_species, wrapper, simulated, requested_concentrations ): """Convert AMICI simulated concentrations to amounts""" for species in amount_species: @@ -225,20 +218,15 @@ def concentrations_to_amounts( # Species with OnlySubstanceUnits don't have to be converted as long # as we don't request concentrations for them. Only applies when # called from amounts_to_concentrations. - if (is_amt and species not in requested_concentrations) \ - or comp is None: + if (is_amt and species not in requested_concentrations) or comp is None: continue simulated.loc[:, species] *= simulated.loc[ - :, comp if comp in simulated.columns else f'amici_{comp}' + :, comp if comp in simulated.columns else f"amici_{comp}" ] -def write_result_file( - simulated: pd.DataFrame, - test_id: str, - result_path: Path -): +def write_result_file(simulated: pd.DataFrame, test_id: str, result_path: Path): """ Create test result file for upload to http://raterule.caltech.edu/Facilities/Database @@ -247,7 +235,7 @@ def write_result_file( """ # TODO: only states are reported here, not compartments or parameters - filename = result_path / f'{test_id}.csv' + filename = result_path / f"{test_id}.csv" simulated.to_csv(filename, index=False) @@ -255,16 +243,10 @@ def get_amount_and_variables(settings): """Read amount and species from settings file""" # species for which results are expected as amounts - amount_species = settings['amount'] \ - .replace(' ', '') \ - .replace('\n', '') \ - .split(',') + amount_species = settings["amount"].replace(" ", "").replace("\n", "").split(",") # IDs of all variables for which results are expected/provided - variables = settings['variables'] \ - .replace(' ', '') \ - .replace('\n', '') \ - .split(',') + variables = settings["variables"].replace(" ", "").replace("\n", "").split(",") return amount_species, variables @@ -272,12 +254,13 @@ def get_amount_and_variables(settings): def apply_settings(settings, solver, model, test_id: str): """Apply model and solver settings as specified in the test case""" # start/duration/steps may be empty - ts = np.linspace(float(settings['start'] or 0), - float(settings['start'] or 0) - + float(settings['duration'] or 0), - int(settings['steps'] or 0) + 1) - atol = float(settings['absolute']) - rtol = float(settings['relative']) + ts = np.linspace( + float(settings["start"] or 0), + float(settings["start"] or 0) + float(settings["duration"] or 0), + int(settings["steps"] or 0) + 1, + ) + atol = float(settings["absolute"]) + rtol = float(settings["relative"]) model.setTimepoints(ts) solver.setMaxSteps(int(1e6)) @@ -291,18 +274,23 @@ def apply_settings(settings, solver, model, test_id: str): return atol, rtol -def compile_model(sbml_dir: Path, test_id: str, model_dir: Path, - generate_sensitivity_code: bool = False): +def compile_model( + sbml_dir: Path, + test_id: str, + model_dir: Path, + generate_sensitivity_code: bool = False, +): """Import the given test model to AMICI""" model_dir.mkdir(parents=True, exist_ok=True) sbml_file = find_model_file(sbml_dir, test_id) sbml_importer = amici.SbmlImporter(sbml_file) - model_name = f'SBMLTest{test_id}' + model_name = f"SBMLTest{test_id}" sbml_importer.sbml2amici( - model_name, output_dir=model_dir, - generate_sensitivity_code=generate_sensitivity_code + model_name, + output_dir=model_dir, + generate_sensitivity_code=generate_sensitivity_code, ) # settings @@ -317,27 +305,27 @@ def compile_model(sbml_dir: Path, test_id: str, model_dir: Path, def find_model_file(current_test_path: Path, test_id: str) -> Path: """Find model file for the given test (guess filename extension)""" - sbml_file = current_test_path / f'{test_id}-sbml-l3v2.xml' + sbml_file = current_test_path / f"{test_id}-sbml-l3v2.xml" if not sbml_file.is_file(): # fallback l3v1 - sbml_file = current_test_path / f'{test_id}-sbml-l3v1.xml' + sbml_file = current_test_path / f"{test_id}-sbml-l3v1.xml" if not sbml_file.is_file(): # fallback l2v5 - sbml_file = current_test_path / f'{test_id}-sbml-l2v5.xml' + sbml_file = current_test_path / f"{test_id}-sbml-l2v5.xml" return sbml_file def read_settings_file(current_test_path: Path, test_id: str): """Read settings for the given test""" - settings_file = current_test_path / f'{test_id}-settings.txt' + settings_file = current_test_path / f"{test_id}-settings.txt" settings = {} with open(settings_file) as f: for line in f: - if line != '\n': - (key, val) = line.split(':') + if line != "\n": + (key, val) = line.split(":") settings[key] = val.strip() return settings diff --git a/version.txt b/version.txt index 7cca7711a0..66333910a4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.17.1 +0.18.0