diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75a91fccb..e9847bc68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.10', '3.12'] steps: - name: Check out repository @@ -41,7 +41,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici @@ -60,10 +60,10 @@ jobs: file: ./coverage.xml mac: - runs-on: macos-latest + runs-on: macos-13 # TODO: change to macos-latest after the next release strategy: matrix: - python-version: ['3.11'] + python-version: ['3.12'] steps: - name: Check out repository @@ -80,14 +80,14 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici - name: Run tests timeout-minutes: 30 - run: tox -e base + run: ulimit -n 65536 65536 && tox -e base - name: Coverage uses: codecov/codecov-action@v3 @@ -99,7 +99,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: ['3.11'] + python-version: ['3.12'] steps: - name: Check out repository @@ -116,7 +116,7 @@ jobs: path: | ~\AppData\Local\pip\Cache .tox - key: ${{ runner.os }}-${{ matrix.python-version }}-ci + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: | @@ -132,7 +132,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.10', '3.12'] steps: - name: Check out repository @@ -149,7 +149,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici pysb @@ -171,7 +171,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.10', '3.12'] # needed to allow julia-actions/cache to delete old caches that it has created permissions: @@ -193,7 +193,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install julia uses: julia-actions/setup-julia@v1 @@ -225,8 +225,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - # ipopt does not work on 3.9 (https://github.com/mechmotum/cyipopt/issues/225) - python-version: ['3.11'] + python-version: ['3.12'] steps: - name: Check out repository @@ -243,7 +242,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh ipopt @@ -262,7 +261,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.10', '3.12'] steps: - name: Check out repository @@ -279,7 +278,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici @@ -298,7 +297,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.11'] + python-version: ['3.10', '3.12'] steps: - name: Check out repository @@ -315,7 +314,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici @@ -334,7 +333,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11'] + python-version: ['3.12'] steps: - name: Check out repository @@ -351,7 +350,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: pip install tox pre-commit @@ -383,7 +382,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh doc amici @@ -399,7 +398,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9'] + python-version: ['3.10'] steps: - name: Check out repository @@ -416,7 +415,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici ipopt @@ -429,7 +428,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9'] + python-version: ['3.10'] steps: - name: Check out repository @@ -446,7 +445,7 @@ jobs: path: | ~/.cache/pip .tox/ - key: ${{ runner.os }}-${{ matrix.python-version }}-ci-${{ github.job }} + key: "${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-ci-${{ github.job }}" - name: Install dependencies run: .github/workflows/install_deps.sh amici diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4cb46a6e3..e1ed397bb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9'] + python-version: ['3.12'] steps: - name: Check out repository diff --git a/CHANGELOG.rst b/CHANGELOG.rst index da8a08f33..d7fc65da3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,37 @@ Release notes .......... +0.5.2 (2024-05-27) +------------------- + +* **New Feature**: Variational inference with PyMC (#1306) +* PEtab + * Import of petab independent of amici (#1355) +* Problem + * Added option to sample startpoints for a problem, from the problem directly. (#1364) + * More detailed defaults for problem.get_full_vector (#1393) + * Save pypesto and python version to the problem. (#1382) +* Objective + * Fix calling priors in sampling with fixed parameters (#1378) + * Fix JaxObjective (#1400) +* Optimize + * ESS optimizers: suppress divide-by-zero warnings; report n_eval (#1380) + * SacessOptimizer: collect worker stats (#1381) + * Add load method to Hdf5AmiciHistory (#1370) +* Hierarchical + * Relative: fix log of zero for default 0 sigma values (#1377) +* Sample + * Fix pypesto.sample.geweke_test.spectrum for nfft<=3 (#1388) +* Visualize + * Handle correlation plot with nans (#1365) +* General + * Remove scipy requirement from pypesto[pymc] (#1376) + * Require and test python >=3.10 according to NEP 29 (#1379) + * Fix various warnings (#1384) + * Small changes to GHA actions and tests (#1386, #1387, #1402, #1385) + * Improve Documentation (#1394, #1391, #1399, #1292, #1390) + + 0.5.0 (2024-04-10) ------------------- diff --git a/README.md b/README.md index 036437888..c75c53c08 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,15 @@ pyPESTO features include: * Parameter estimation pipeline for systems biology problems specified in [SBML](http://sbml.org/) and [PEtab](https://github.com/PEtab-dev/PEtab) ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/petab_import.ipynb)) +* Parameter estimation with relative (scaled and offset) data as described in + [Schmiester et al. (2020)](https://doi.org/10.1093/bioinformatics/btz581). + ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/relative_data.ipynb)) * Parameter estimation with ordinal data as described in [Schmiester et al. (2020)](https://doi.org/10.1007/s00285-020-01522-w) and [Schmiester et al. (2021)](https://doi.org/10.1093/bioinformatics/btab512). - ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/ordinal.ipynb)) -* Parameter estimation with censored data. ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/censored.ipynb)) -* Parameter estimation with nonlinear-monotone data. ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/nonlinear_monotone.ipynb)) + ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/ordinal_data.ipynb)) +* Parameter estimation with censored data. ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/censored_data.ipynb)) +* Parameter estimation with nonlinear-monotone data. ([example](https://github.com/ICB-DCM/pyPESTO/blob/master/doc/example/semiquantitative_data.ipynb)) ## Quick install diff --git a/doc/example/amici.ipynb b/doc/example/amici.ipynb index 477c63e52..3b0bcb96e 100644 --- a/doc/example/amici.ipynb +++ b/doc/example/amici.ipynb @@ -36,7 +36,6 @@ "source": [ "# import\n", "import logging\n", - "import random\n", "import tempfile\n", "from pprint import pprint\n", "\n", @@ -57,7 +56,8 @@ "mpl.rcParams[\"figure.dpi\"] = 100\n", "mpl.rcParams[\"font.size\"] = 18\n", "\n", - "random.seed(1912)\n", + "# Set seed for reproducibility\n", + "np.random.seed(1912)\n", "\n", "\n", "# name of the model that will also be the name of the python module\n", @@ -929,8 +929,6 @@ "outputs": [], "source": [ "%%time\n", - "# Set seed for reproducibility\n", - "np.random.seed(1)\n", "result = optimize.minimize(\n", " problem=problem,\n", " optimizer=optimizer,\n", diff --git a/doc/example/roadrunner.ipynb b/doc/example/roadrunner.ipynb index 06b540937..f666efb6e 100644 --- a/doc/example/roadrunner.ipynb +++ b/doc/example/roadrunner.ipynb @@ -2,38 +2,50 @@ "cells": [ { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ - " # RoadRunner in pyPESTO\n", + "# RoadRunner in pyPESTO\n", "\n", "**After going through this notebook, you will be able to...**\n", "\n", "* ... create a pyPESTO problem using [RoadRunner](https://www.libroadrunner.org) as a simulator directly from a PEtab problem.\n", "* ... perform a parameter estimation using pyPESTO with RoadRunner as a simulator, setting advanced simulator features." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# install pyPESTO with roadrunner support\n", "# %pip install pypesto[roadrunner,petab] --quiet" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# import\n", - "import random\n", "import matplotlib as mpl\n", + "import numpy as np\n", "import petab\n", "import pypesto.objective\n", "import pypesto.optimize as optimize\n", @@ -47,7 +59,7 @@ "mpl.rcParams[\"figure.dpi\"] = 100\n", "mpl.rcParams[\"font.size\"] = 18\n", "\n", - "random.seed(1912)\n", + "np.random.seed(1912)\n", "\n", "\n", "# name of the model that will also be the name of the python module\n", @@ -55,25 +67,31 @@ "\n", "# output directory\n", "model_output_dir = \"tmp/\" + model_name" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "## Creating pyPESTO problem from PEtab\n", "\n", "The [PEtab file format](https://petab.readthedocs.io/en/latest/documentation_data_format.html) stores all the necessary information to define a parameter estimation problem. This includes the model, the experimental data, the parameters to estimate, and the experimental conditions. Using the `pypesto_rr.PetabImporterRR` class, we can create a pyPESTO problem directly from a PEtab problem." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "petab_yaml = f\"./{model_name}/{model_name}.yaml\"\n", @@ -81,34 +99,43 @@ "petab_problem = petab.Problem.from_yaml(petab_yaml)\n", "importer = pypesto_rr.PetabImporterRR(petab_problem)\n", "problem = importer.create_problem()" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "We now have a pyPESTO problem that we can use to perform parameter estimation. We can get some information on the RoadRunnerObjective and access the RoadRunner model." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "pprint(problem.objective.get_config())" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# direct simulation of the model using roadrunner\n", @@ -117,23 +144,29 @@ ")\n", "pprint(sim_res)\n", "problem.objective.roadrunner_instance.plot();" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "For more details on interacting with the roadrunner instance, we refer to the [documentation of libroadrunner](https://libroadrunner.readthedocs.io/en/latest/). However, we point out that while theoretical possible, we **strongly advice against** changing the model in that manner." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "ret = problem.objective(\n", @@ -142,25 +175,31 @@ " return_dict=True,\n", ")\n", "pprint(ret)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "## Optimization\n", "\n", "To optimize a problem using a RoadRunner objective, we can set additional solver options for the ODE solver." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "optimizer = optimize.ScipyOptimizer()\n", @@ -172,14 +211,17 @@ ")\n", "engine = pypesto.engine.SingleCoreEngine()\n", "problem.objective.solver_options = solver_options" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "result = optimize.minimize(\n", @@ -189,78 +231,99 @@ " engine=engine\n", ")\n", "display(Markdown(result.summary()))" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "Disclaimer: Currently there are two main things not yet fully supported with roadrunner objectives. One is parallelization of the optimization using MultiProcessEngine. The other is explicit gradients of the objective function. While the former will be added in a near future version, we will show a workaround for the latter, until it is implemented." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "### Visualization Methods\n", "\n", "In order to visualize the optimization, there are a few things possible. For a more extensive explanation we refer to the \"getting started\" notebook." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "visualize.waterfall(result);" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "visualize.parameters(result);" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "visualize.parameters_correlation_matrix(result);" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "markdown", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "### Sensitivities via finite differences\n", "\n", "Some solvers need a way to calculate the sensitivities, which currently RoadRunner Objectives do not suport. For this scenario, we can use the FiniteDifferences objective in pypesto, which wraps a given objective into one, that computes sensitivities via finite differences." - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "# no support for sensitivities\n", @@ -274,14 +337,17 @@ " pprint(ret)\n", "except ValueError as e:\n", " pprint(e)" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "objective_fd = pypesto.objective.FD(problem.objective)\n", @@ -296,31 +362,28 @@ " pprint(ret)\n", "except ValueError as e:\n", " pprint(e)" - ], - "metadata": { - "collapsed": false - } + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.10.2" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/doc/example/store.ipynb b/doc/example/store.ipynb index f96520f6c..58f9902af 100644 --- a/doc/example/store.ipynb +++ b/doc/example/store.ipynb @@ -59,7 +59,6 @@ "outputs": [], "source": [ "import logging\n", - "import random\n", "import tempfile\n", "\n", "import matplotlib as mpl\n", @@ -76,7 +75,7 @@ "mpl.rcParams[\"figure.dpi\"] = 100\n", "mpl.rcParams[\"font.size\"] = 18\n", "# set a random seed to get reproducible results\n", - "random.seed(3142)\n", + "np.random.seed(3142)\n", "\n", "%matplotlib inline" ] diff --git a/doc/using_pypesto.bib b/doc/using_pypesto.bib index bb19094d9..ebaa06055 100644 --- a/doc/using_pypesto.bib +++ b/doc/using_pypesto.bib @@ -79,17 +79,19 @@ @Article{FroehlichSor2022 } @Article{FroehlichGer2022, - author = {Fr{\"o}hlich, Fabian and Gerosa, Luca and Muhlich, Jeremy and Sorger, Peter K.}, - journal = {bioRxiv}, - title = {Mechanistic model of MAPK signaling reveals how allostery and rewiring contribute to drug resistance}, - year = {2022}, - abstract = {BRAFV600E is prototypical of oncogenic mutations that can be targeted therapeutically and treatment of BRAF-mutant melanomas with RAF and MEK inhibitors results in rapid tumor regression. However, drug-induced rewiring causes BRAFV600E melanoma cells to rapidly acquire a drug-adapted state. In patients this is thought to promote acquisition or selection for resistance mutations and disease recurrence. In this paper we use an energy-based implementation of ordinary differential equations in combination with proteomic, transcriptomic and imaging data from melanoma cells, to model the precise mechanisms responsible for adaptive rewiring. We demonstrate the presence of two parallel MAPK (RAF-MEK-ERK kinase) reaction channels in BRAFV600E melanoma cells that are differentially sensitive to RAF and MEK inhibitors. This arises from differences in protein oligomerization and allosteric regulation induced by oncogenic mutations and drug binding. As a result, the RAS-regulated MAPK channel can be active under conditions in which the BRAFV600E-driven channel is fully inhibited. Causal tracing demonstrates that this provides a sufficient quantitative explanation for initial and acquired responses to multiple different RAF and MEK inhibitors individually and in combination.HighlightsA thermodynamic framework enables structure-based description of allosteric interactions in the EGFR and MAPK pathwaysCausal decomposition of efficacy of targeted drugs elucidates rewiring of MAPK channelsModel-based extrapolation from type I{\textonehalf} RAF inhibitors to type II RAF inhibitorsA unified mechanistic explanation for adaptive and genetic resistance across BRAF-cancersCompeting Interest StatementPKS is a member of the SAB or Board of Directors of Glencoe Software, Applied Biomath, and RareCyte Inc. and has equity in these companies; PKS is also a member of the SAB of NanoString and a consultant for Montai Health and Merck. LG is currently an employee of Genentech. PKS and LG declare that none of these relationships are directly or indirectly related to the content of this manuscript.}, - creationdate = {2023-01-26T11:32:12}, - doi = {10.1101/2022.02.17.480899}, - elocation-id = {2022.02.17.480899}, - eprint = {https://www.biorxiv.org/content/early/2022/02/18/2022.02.17.480899.full.pdf}, - publisher = {Cold Spring Harbor Laboratory}, - url = {https://www.biorxiv.org/content/early/2022/02/18/2022.02.17.480899}, + author = {Fr{\"o}hlich, Fabian and Gerosa, Luca and Muhlich, Jeremy and Sorger, Peter K.}, + journal = {bioRxiv}, + title = {Mechanistic model of MAPK signaling reveals how allostery and rewiring contribute to drug resistance}, + year = {2022}, + abstract = {BRAFV600E is prototypical of oncogenic mutations that can be targeted therapeutically and treatment of BRAF-mutant melanomas with RAF and MEK inhibitors results in rapid tumor regression. However, drug-induced rewiring causes BRAFV600E melanoma cells to rapidly acquire a drug-adapted state. In patients this is thought to promote acquisition or selection for resistance mutations and disease recurrence. In this paper we use an energy-based implementation of ordinary differential equations in combination with proteomic, transcriptomic and imaging data from melanoma cells, to model the precise mechanisms responsible for adaptive rewiring. We demonstrate the presence of two parallel MAPK (RAF-MEK-ERK kinase) reaction channels in BRAFV600E melanoma cells that are differentially sensitive to RAF and MEK inhibitors. This arises from differences in protein oligomerization and allosteric regulation induced by oncogenic mutations and drug binding. As a result, the RAS-regulated MAPK channel can be active under conditions in which the BRAFV600E-driven channel is fully inhibited. Causal tracing demonstrates that this provides a sufficient quantitative explanation for initial and acquired responses to multiple different RAF and MEK inhibitors individually and in combination.HighlightsA thermodynamic framework enables structure-based description of allosteric interactions in the EGFR and MAPK pathwaysCausal decomposition of efficacy of targeted drugs elucidates rewiring of MAPK channelsModel-based extrapolation from type I{\textonehalf} RAF inhibitors to type II RAF inhibitorsA unified mechanistic explanation for adaptive and genetic resistance across BRAF-cancersCompeting Interest StatementPKS is a member of the SAB or Board of Directors of Glencoe Software, Applied Biomath, and RareCyte Inc. and has equity in these companies; PKS is also a member of the SAB of NanoString and a consultant for Montai Health and Merck. LG is currently an employee of Genentech. PKS and LG declare that none of these relationships are directly or indirectly related to the content of this manuscript.}, + creationdate = {2023-01-26T11:32:12}, + doi = {10.1101/2022.02.17.480899}, + elocation-id = {2022.02.17.480899}, + eprint = {https://www.biorxiv.org/content/early/2022/02/18/2022.02.17.480899.full.pdf}, + modificationdate = {2024-05-13T09:29:21}, + publisher = {Cold Spring Harbor Laboratory}, + ranking = {rank1}, + url = {https://www.biorxiv.org/content/early/2022/02/18/2022.02.17.480899}, } @Article{GerosaChi2020, @@ -207,4 +209,78 @@ @Article{FischerHolzhausenRoe2023 url = {https://www.biorxiv.org/content/early/2023/01/19/2023.01.17.523407}, } +@Article{KissVen2024, + author = {Kiss, Anna E and Venkatasubramani, Anuroop V and Pathirana, Dilan and Krause, Silke and Sparr, Aline Campos and Hasenauer, Jan and Imhof, Axel and Müller, Marisa and Becker, Peter B}, + journal = {Nucleic Acids Research}, + title = {{Processivity and specificity of histone acetylation by the male-specific lethal complex}}, + year = {2024}, + issn = {0305-1048}, + month = {02}, + pages = {gkae123}, + abstract = {{Acetylation of lysine 16 of histone H4 (H4K16ac) stands out among the histone modifications, because it decompacts the chromatin fiber. The metazoan acetyltransferase MOF (KAT8) regulates transcription through H4K16 acetylation. Antibody-based studies had yielded inconclusive results about the selectivity of MOF to acetylate the H4 N-terminus. We used targeted mass spectrometry to examine the activity of MOF in the male-specific lethal core (4-MSL) complex on nucleosome array substrates. This complex is part of the Dosage Compensation Complex (DCC) that activates X-chromosomal genes in male Drosophila. During short reaction times, MOF acetylated H4K16 efficiently and with excellent selectivity. Upon longer incubation, the enzyme progressively acetylated lysines 12, 8 and 5, leading to a mixture of oligo-acetylated H4. Mathematical modeling suggests that MOF recognizes and acetylates H4K16 with high selectivity, but remains substrate-bound and continues to acetylate more N-terminal H4 lysines in a processive manner. The 4-MSL complex lacks non-coding roX RNA, a critical component of the DCC. Remarkably, addition of RNA to the reaction non-specifically suppressed H4 oligo-acetylation in favor of specific H4K16 acetylation. Because RNA destabilizes the MSL-nucleosome interaction in vitro we speculate that RNA accelerates enzyme-substrate turn-over in vivo, thus limiting the processivity of MOF, thereby increasing specific H4K16 acetylation.}}, + creationdate = {2024-02-28T18:27:01}, + doi = {10.1093/nar/gkae123}, + eprint = {https://academic.oup.com/nar/advance-article-pdf/doi/10.1093/nar/gkae123/56756494/gkae123.pdf}, + modificationdate = {2024-02-28T18:27:01}, + url = {https://doi.org/10.1093/nar/gkae123}, +} + +@Article{DoresicGre2024, + author = {Domagoj Dore{\v s}i{\'c} and Stephan Grein and Jan Hasenauer}, + journal = {bioRxiv}, + title = {Efficient parameter estimation for ODE models of cellular processes using semi-quantitative data}, + year = {2024}, + abstract = {Quantitative dynamical models facilitate the understanding of biological processes and the prediction of their dynamics. The parameters of these models are commonly estimated from experimental data. Yet, experimental data generated from different techniques do not provide direct information about the state of the system but a non-linear (monotonic) transformation of it. For such semi-quantitative data, when this transformation is unknown, it is not apparent how the model simulations and the experimental data can be compared. Here, we propose a versatile spline-based approach for the integration of a broad spectrum of semi-quantitative data into parameter estimation. We derive analytical formulas for the gradients of the hierarchical objective function and show that this substantially increases the estimation efficiency. Subsequently, we demonstrate that the method allows for the reliable discovery of unknown measurement transformations. Furthermore, we show that this approach can significantly improve the parameter inference based on semi-quantitative data in comparison to available methods. Modelers can easily apply our method by using our implementation in the open-source Python Parameter EStimation TOolbox (pyPESTO).Competing Interest StatementThe authors have declared no competing interest.}, + creationdate = {2024-04-20T13:06:42}, + doi = {10.1101/2024.01.26.577371}, + elocation-id = {2024.01.26.577371}, + eprint = {https://www.biorxiv.org/content/early/2024/01/30/2024.01.26.577371.full.pdf}, + modificationdate = {2024-04-20T13:06:42}, + publisher = {Cold Spring Harbor Laboratory}, + url = {https://www.biorxiv.org/content/early/2024/01/30/2024.01.26.577371}, +} + +@Article{ArrudaSch2023, + author = {Jonas Arruda and Yannik Sch{\"a}lte and Clemens Peiter and Olga Teplytska and Ulrich Jaehde and Jan Hasenauer}, + journal = {bioRxiv}, + title = {An amortized approach to non-linear mixed-effects modeling based on neural posterior estimation}, + year = {2023}, + abstract = {Non-linear mixed-effects models are a powerful tool for studying heterogeneous populations in various fields, including biology, medicine, economics, and engineering. However, fitting these models to data is computationally challenging if the description of individuals is complex and the population is large. To address this issue, we propose a novel machine learning-based approach: We exploit neural density estimation based on normalizing flows to approximate individual-specific posterior distributions in an amortized fashion, thereby allowing for an efficient inference of population parameters. Applying this approach to problems from cell biology and pharmacology, we demonstrate its scalability to large data sets in an unprecedented manner. Moreover, we show that it enables accurate uncertainty quantification and extends to stochastic models, which established methods, such as SAEM and FOCEI are unable to handle. Thus, our approach outperforms state-of-the-art methods and improves the analysis capabilities for heterogeneous populations.Competing Interest StatementThe authors have declared no competing interest.}, + creationdate = {2024-04-22T12:56:00}, + doi = {10.1101/2023.08.22.554273}, + elocation-id = {2023.08.22.554273}, + eprint = {https://www.biorxiv.org/content/early/2023/08/23/2023.08.22.554273.full.pdf}, + modificationdate = {2024-04-22T12:56:00}, + publisher = {Cold Spring Harbor Laboratory}, + url = {https://www.biorxiv.org/content/early/2023/08/23/2023.08.22.554273}, +} + +@Article{MerktAli2024, + author = {Merkt, Simon and Ali, Solomon and Gudina, Esayas Kebede and Adissu, Wondimagegn and Gize, Addisu and Muenchhoff, Maximilian and Graf, Alexander and Krebs, Stefan and Elsbernd, Kira and Kisch, Rebecca and Betizazu, Sisay Sirgu and Fantahun, Bereket and Bekele, Delayehu and Rubio-Acero, Raquel and Gashaw, Mulatu and Girma, Eyob and Yilma, Daniel and Zeynudin, Ahmed and Paunovic, Ivana and Hoelscher, Michael and Blum, Helmut and Hasenauer, Jan and Kroidl, Arne and Wieser, Andreas}, + journal = {Nature Communications}, + title = {Long-term monitoring of SARS-CoV-2 seroprevalence and variants in Ethiopia provides prediction for immunity and cross-immunity}, + year = {2024}, + issn = {2041-1723}, + month = apr, + number = {1}, + volume = {15}, + creationdate = {2024-04-29T08:32:16}, + doi = {10.1038/s41467-024-47556-2}, + modificationdate = {2024-04-29T08:32:16}, + publisher = {Springer Science and Business Media LLC}, +} + +@Article{FalcoCoh2024a, + author = {Falcó, Carles and Cohen, Daniel J. and Carrillo, José A. and Baker, Ruth E.}, + journal = {Biophysical Journal}, + title = {Quantifying cell cycle regulation by tissue crowding}, + year = {2024}, + issn = {0006-3495}, + month = may, + creationdate = {2024-05-13T09:29:26}, + doi = {10.1016/j.bpj.2024.05.003}, + modificationdate = {2024-05-13T09:29:26}, + publisher = {Elsevier BV}, +} + @Comment{jabref-meta: databaseType:bibtex;} diff --git a/pypesto/hierarchical/inner_calculator_collector.py b/pypesto/hierarchical/inner_calculator_collector.py index 80e3151e5..a3f1a7dd8 100644 --- a/pypesto/hierarchical/inner_calculator_collector.py +++ b/pypesto/hierarchical/inner_calculator_collector.py @@ -48,7 +48,7 @@ try: import amici import petab - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: petab = None ParameterMapping = None @@ -325,7 +325,7 @@ def __call__( Whether to use the FIM (if available) instead of the Hessian (if requested). """ - import amici.parameter_mapping + from amici.petab.conditions import fill_in_parameters if mode == MODE_RES and any( data_type in self.data_types @@ -403,7 +403,7 @@ def __call__( x_dct = copy.deepcopy(x_dct) x_dct.update(self.necessary_par_dummy_values) # fill in parameters - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/hierarchical/ordinal/calculator.py b/pypesto/hierarchical/ordinal/calculator.py index 8044d5d7d..5fb9e60c6 100644 --- a/pypesto/hierarchical/ordinal/calculator.py +++ b/pypesto/hierarchical/ordinal/calculator.py @@ -32,7 +32,8 @@ try: import amici - from amici.parameter_mapping import ParameterMapping + from amici.petab.conditions import fill_in_parameters + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass @@ -155,7 +156,7 @@ def __call__( x_dct = copy.deepcopy(x_dct) # fill in parameters - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/hierarchical/ordinal/solver.py b/pypesto/hierarchical/ordinal/solver.py index 5c65b1722..66930faec 100644 --- a/pypesto/hierarchical/ordinal/solver.py +++ b/pypesto/hierarchical/ordinal/solver.py @@ -37,7 +37,7 @@ from .problem import OrdinalProblem try: - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass diff --git a/pypesto/hierarchical/relative/calculator.py b/pypesto/hierarchical/relative/calculator.py index 3ba59452f..cfd74a42f 100644 --- a/pypesto/hierarchical/relative/calculator.py +++ b/pypesto/hierarchical/relative/calculator.py @@ -7,8 +7,8 @@ try: import amici - import amici.parameter_mapping - from amici.parameter_mapping import ParameterMapping + from amici.petab.conditions import fill_in_parameters + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass @@ -296,7 +296,7 @@ def calculate_directly( amici_solver.setSensitivityOrder(sensi_order) x_dct.update(self.inner_problem.get_dummy_values(scaled=True)) # fill in parameters - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/hierarchical/relative/solver.py b/pypesto/hierarchical/relative/solver.py index a0a6ea3d7..b3000eaaa 100644 --- a/pypesto/hierarchical/relative/solver.py +++ b/pypesto/hierarchical/relative/solver.py @@ -27,7 +27,7 @@ try: import amici - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass @@ -82,7 +82,7 @@ def calculate_obj_function( mask=x.ixs, ) - return compute_nllh(relevant_data, sim, sigma) + return compute_nllh(relevant_data, sim, sigma, problem.data_mask) def calculate_gradients( self, @@ -502,7 +502,7 @@ def fun(x): f"`{par.inner_parameter_type}`." ) - return compute_nllh(_data, _sim, _sigma) + return compute_nllh(_data, _sim, _sigma, problem.data_mask) # TODO gradient objective = Objective(fun) diff --git a/pypesto/hierarchical/relative/util.py b/pypesto/hierarchical/relative/util.py index 4465b3b12..0639d7556 100644 --- a/pypesto/hierarchical/relative/util.py +++ b/pypesto/hierarchical/relative/util.py @@ -362,6 +362,11 @@ def compute_bounded_optimal_scaling_offset_coupled( relevant_data[i][~s.ixs[i]] = np.nan relevant_sim[i][~s.ixs[i]] = np.nan + # Get relevant data mask + relevant_data_mask = [] + for i in range(len(data)): + relevant_data_mask.append(~np.isnan(relevant_data[i])) + # Get bounds s_bounds = s.get_bounds() b_bounds = b.get_bounds() @@ -407,6 +412,7 @@ def compute_bounded_optimal_scaling_offset_coupled( for sim_i in relevant_sim ], sigma=sigma, + data_mask=relevant_data_mask, ) for candidate_point in candidate_points ] @@ -447,18 +453,31 @@ def compute_bounded_optimal_scaling_offset_coupled( def compute_nllh( - data: list[np.ndarray], sim: list[np.ndarray], sigma: list[np.ndarray] + data: list[np.ndarray], + sim: list[np.ndarray], + sigma: list[np.ndarray], + data_mask: list[np.ndarray], ) -> float: """Compute negative log-likelihood. Compute negative log-likelihood of the data, given the model outputs and sigmas. """ - return sum( - 0.5 * np.nansum(np.log(2 * np.pi * sigma_i**2)) - + 0.5 * np.nansum((data_i - sim_i) ** 2 / sigma_i**2) - for data_i, sim_i, sigma_i in zip(data, sim, sigma) - ) + nllh = 0.0 + for data_i, sim_i, sigma_i, data_mask_i in zip( + data, sim, sigma, data_mask + ): + # Mask the data, sim and sigma + data_i = data_i[data_mask_i] + sim_i = sim_i[data_mask_i] + sigma_i = sigma_i[data_mask_i] + + # Compute the negative log-likelihood + nllh += 0.5 * np.nansum( + np.log(2 * np.pi * sigma_i**2) + ) + 0.5 * np.nansum((data_i - sim_i) ** 2 / sigma_i**2) + + return nllh def compute_nllh_gradient_for_condition( diff --git a/pypesto/hierarchical/semiquantitative/calculator.py b/pypesto/hierarchical/semiquantitative/calculator.py index 3b2cd44a4..4991f6766 100644 --- a/pypesto/hierarchical/semiquantitative/calculator.py +++ b/pypesto/hierarchical/semiquantitative/calculator.py @@ -32,7 +32,8 @@ try: import amici - from amici.parameter_mapping import ParameterMapping + from amici.petab.conditions import fill_in_parameters + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass @@ -154,7 +155,7 @@ def __call__( ) # fill in parameters - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/hierarchical/semiquantitative/solver.py b/pypesto/hierarchical/semiquantitative/solver.py index 07254a913..49dfeec4c 100644 --- a/pypesto/hierarchical/semiquantitative/solver.py +++ b/pypesto/hierarchical/semiquantitative/solver.py @@ -27,7 +27,7 @@ from .problem import SemiquantProblem try: - from amici.parameter_mapping import ParameterMapping + from amici.petab.parameter_mapping import ParameterMapping except ImportError: pass diff --git a/pypesto/history/amici.py b/pypesto/history/amici.py index 6d0c8fb9b..0e2fcb44b 100644 --- a/pypesto/history/amici.py +++ b/pypesto/history/amici.py @@ -43,6 +43,18 @@ def __init__( ): super().__init__(id, file, options=options) + @staticmethod + def load( + id: str, + file: Union[str, Path], + options: Union[HistoryOptions, dict] = None, + ) -> "Hdf5AmiciHistory": + """Load the History object from memory.""" + history = Hdf5AmiciHistory(id=id, file=file, options=options) + if options is None: + history.recover_options(file) + return history + @staticmethod def _simulation_to_values(x, result, used_time): values = Hdf5History._simulation_to_values(x, result, used_time) diff --git a/pypesto/objective/amici/amici_util.py b/pypesto/objective/amici/amici_util.py index ff5ca29fd..c5533af03 100644 --- a/pypesto/objective/amici/amici_util.py +++ b/pypesto/objective/amici/amici_util.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: try: import amici - from amici.parameter_mapping import ( + from amici.petab.parameter_mapping import ( ParameterMapping, ParameterMappingForCondition, ) @@ -124,23 +124,25 @@ def create_identity_parameter_mapping( both in preequilibration and simulation, are assumed to be provided correctly in model or edatas already. """ - import amici.parameter_mapping + from amici.petab.parameter_mapping import ( + ParameterMapping, + ParameterMappingForCondition, + amici_to_petab_scale, + ) x_ids = list(amici_model.getParameterIds()) x_scales = list(amici_model.getParameterScale()) - parameter_mapping = amici.parameter_mapping.ParameterMapping() + parameter_mapping = ParameterMapping() for _ in range(n_conditions): condition_map_sim_var = {x_id: x_id for x_id in x_ids} condition_scale_map_sim_var = { - x_id: amici.parameter_mapping.amici_to_petab_scale(x_scale) + x_id: amici_to_petab_scale(x_scale) for x_id, x_scale in zip(x_ids, x_scales) } # assumes fixed parameters are filled in already - mapping_for_condition = ( - amici.parameter_mapping.ParameterMappingForCondition( - map_sim_var=condition_map_sim_var, - scale_map_sim_var=condition_scale_map_sim_var, - ) + mapping_for_condition = ParameterMappingForCondition( + map_sim_var=condition_map_sim_var, + scale_map_sim_var=condition_scale_map_sim_var, ) parameter_mapping.append(mapping_for_condition) diff --git a/pypesto/objective/jax/base.py b/pypesto/objective/jax/base.py index 49327bb37..ea86b07a0 100644 --- a/pypesto/objective/jax/base.py +++ b/pypesto/objective/jax/base.py @@ -8,7 +8,7 @@ import copy from functools import partial -from typing import Union +from typing import Callable, Union import numpy as np @@ -26,12 +26,24 @@ "`pip install jax jaxlib`." ) from None + +def _base_objective_as_jax_array_tuple(func: Callable): + def decorator(*args, **kwargs): + # make sure return is a tuple of jax arrays + results = func(*args, **kwargs) + if isinstance(results, tuple): + return tuple(jnp.array(r) for r in results) + return jnp.array(results) + + return decorator + + # jax compatible (jit-able) objective function using external callback, see # https://jax.readthedocs.io/en/latest/notebooks/external_callbacks.html @partial(custom_jvp, nondiff_argnums=(0,)) -def _device_fun(base_objective: ObjectiveBase, x: jnp.array): +def _device_fun(base_objective: ObjectiveBase, x: jnp.array) -> jnp.array: """Jax compatible objective function execution using external callback. Parameters @@ -40,15 +52,24 @@ def _device_fun(base_objective: ObjectiveBase, x: jnp.array): The wrapped jax objective. x: jax computed input array. + + Returns + ------- + fval : jnp.array + The function value as 0-dimensional jax array. """ return jax.pure_callback( - partial(base_objective, sensi_orders=(0,)), + _base_objective_as_jax_array_tuple( + partial(base_objective, sensi_orders=(0,)) + ), jax.ShapeDtypeStruct((), x.dtype), x, ) -def _device_fun_value_and_grad(base_objective: ObjectiveBase, x: jnp.array): +def _device_fun_value_and_grad( + base_objective: ObjectiveBase, x: jnp.array +) -> tuple[jnp.array, jnp.array]: """Jax compatible objective gradient execution using external callback. This function will be called when computing the gradient of the @@ -63,14 +84,23 @@ def _device_fun_value_and_grad(base_objective: ObjectiveBase, x: jnp.array): The wrapped jax objective. x: jax computed input array. + + Returns + ------- + fval : jnp.array + The function value as 0-dimensional jax array. + grad : jnp.array + The gradient as jax array. """ return jax.pure_callback( - partial( - base_objective, - sensi_orders=( - 0, - 1, - ), + _base_objective_as_jax_array_tuple( + partial( + base_objective, + sensi_orders=( + 0, + 1, + ), + ) ), ( jax.ShapeDtypeStruct((), x.dtype), @@ -112,7 +142,7 @@ class JaxObjective(ObjectiveBase): Note ---- - Currently only implements MODE_FUN and sensi_orders=(0,). Support for + Currently only implements MODE_FUN and sensi_orders<=1. Support for MODE_RES should be straightforward to add. """ @@ -143,7 +173,7 @@ def check_sensi_orders(self, sensi_orders, mode: ModeType) -> bool: else: return ( self.base_objective.check_sensi_orders(sensi_orders, mode) - and max(sensi_orders) == 0 + and max(sensi_orders) <= 1 ) def __call__( @@ -204,15 +234,20 @@ def __deepcopy__(self, memodict=None): @property def history(self): - """Exposes the history of the inner objective.""" + """Expose the history of the inner objective.""" return self.base_objective.history @property def pre_post_processor(self): - """Exposes the pre_post_processor of inner objective.""" + """Expose the pre_post_processor of inner objective.""" return self.base_objective.pre_post_processor + @pre_post_processor.setter + def pre_post_processor(self, new_pre_post_processor): + """Set the pre_post_processor of inner objective.""" + self.base_objective.pre_post_processor = new_pre_post_processor + @property def x_names(self): - """Exposes the x_names of inner objective.""" + """Expose the x_names of inner objective.""" return self.base_objective.x_names diff --git a/pypesto/objective/priors.py b/pypesto/objective/priors.py index 493cf9b41..3b8f177fe 100644 --- a/pypesto/objective/priors.py +++ b/pypesto/objective/priors.py @@ -1,7 +1,6 @@ import logging import math from collections.abc import Sequence -from copy import deepcopy from typing import Callable, Union import numpy as np @@ -68,11 +67,6 @@ def __init__( self.prior_list = prior_list super().__init__(x_names) - def __deepcopy__(self, memodict=None): - """Create deepcopy of object.""" - other = NegLogParameterPriors(deepcopy(self.prior_list)) - return other - def call_unprocessed( self, x: np.ndarray, diff --git a/pypesto/optimize/ess/ess.py b/pypesto/optimize/ess/ess.py index b0d62f718..479c67c61 100644 --- a/pypesto/optimize/ess/ess.py +++ b/pypesto/optimize/ess/ess.py @@ -647,8 +647,8 @@ def _report_final(self): formatter={"float": lambda x: "%.3g" % x}, ): self.logger.info( - f"-- Final ESS fval after {self.n_iter} " - f"iterations: {self.fx_best}. " + f"-- Final ESS fval after {self.n_iter} iterations, " + f"{self.evaluator.n_eval} function evaluations: {self.fx_best}. " f"Exit flag: {self.exit_flag.name}. " f"Num local solutions: {len(self.local_solutions)}." ) diff --git a/pypesto/optimize/ess/sacess.py b/pypesto/optimize/ess/sacess.py index c750f79c3..0dee7482a 100644 --- a/pypesto/optimize/ess/sacess.py +++ b/pypesto/optimize/ess/sacess.py @@ -6,6 +6,7 @@ import multiprocessing import os import time +from dataclasses import dataclass from math import ceil, sqrt from multiprocessing import get_context from multiprocessing.managers import SyncManager @@ -128,6 +129,7 @@ def __init__( self.exit_flag = ESSExitFlag.DID_NOT_RUN self.ess_loglevel = ess_loglevel self.sacess_loglevel = sacess_loglevel + self.worker_results: list[SacessWorkerResult] = [] logger.setLevel(self.sacess_loglevel) self._tmpdir = tmpdir @@ -249,21 +251,29 @@ def minimize( # wait for finish # collect results - histories = [ + self.worker_results = [ sacess_manager._result_queue.get() for _ in range(self.num_workers) ] - self.histories = histories for p in worker_processes: p.join() logging_thread.stop() + + self.histories = [ + worker_result.history for worker_result in self.worker_results + ] + result = self._create_result(problem) walltime = time.time() - start_time + n_eval_total = sum( + worker_result.n_eval for worker_result in self.worker_results + ) logger.info( - f"{self.__class__.__name__} stopped after {walltime:3g}s with global best " - f"{result.optimize_result[0].fval}." + f"{self.__class__.__name__} stopped after {walltime:3g}s " + f"and {n_eval_total} objective evaluations " + f"with global best {result.optimize_result[0].fval}." ) return result @@ -445,10 +455,15 @@ def submit_solution( # reject solution self._rejections.value += 1 + rel_change = ( + abs(abs_change / self._best_known_fx.value) + if self._best_known_fx.value != 0 + else np.nan + ) self._logger.debug( f"Rejected solution from worker {sender_idx} " f"abs change: {abs_change} " - f"rel change: {abs(abs_change / self._best_known_fx.value):.4g} " + f"rel change: {rel_change:.4g} " f"(threshold: {self._rejection_threshold.value}) " f"(total rejections: {self._rejections.value})." ) @@ -577,11 +592,20 @@ def run( self._logger.info( f"sacess worker {self._worker_idx} iteration {ess.n_iter} " - f"(best: {self._best_known_fx})." + f"(best: {self._best_known_fx}, " + f"n_eval: {ess.evaluator.n_eval})." ) ess.history.finalize(exitflag=ess.exit_flag.name) - self._manager._result_queue.put(ess.history) + worker_result = SacessWorkerResult( + x=ess.x_best, + fx=ess.fx_best, + history=ess.history, + n_eval=ess.evaluator.n_eval, + n_iter=ess.n_iter, + exit_flag=ess.exit_flag, + ) + self._manager._result_queue.put(worker_result) ess._report_final() def _setup_ess(self, startpoint_method: StartpointMethod) -> ESSOptimizer: @@ -650,10 +674,13 @@ def _maybe_adapt(self, problem: Problem): def maybe_update_best(self, x: np.array, fx: float): """Maybe update the best known solution and send it to the manager.""" + rel_change = ( + abs((fx - self._best_known_fx) / fx) if fx != 0 else np.nan + ) self._logger.debug( f"Worker {self._worker_idx} maybe sending solution {fx}. " f"best known: {self._best_known_fx}, " - f"rel change: {(fx - self._best_known_fx) / fx:.4g}, " + f"rel change: {rel_change:.4g}, " f"threshold: {self._acceptance_threshold}" ) @@ -1016,3 +1043,35 @@ def __call__( def __repr__(self): return f"{self.__class__.__name__}(fides_options={self._fides_options}, fides_kwargs={self._fides_kwargs})" + + +@dataclass +class SacessWorkerResult: + """Container for :class:`SacessWorker` results. + + Contains various information about the optimization process of a single + :class:`SacessWorker` instance that is to be sent to + :class:`SacessOptimizer`. + + Attributes + ---------- + x: + Best parameters found. + fx: + Objective value corresponding to ``x``. + n_eval: + Number of objective evaluations performed. + n_iter: + Number of scatter search iterations performed. + history: + History object containing information about the optimization process. + exit_flag: + Exit flag of the optimization process. + """ + + x: np.array + fx: float + n_eval: int + n_iter: int + history: "pypesto.history.memory.MemoryHistory" + exit_flag: ESSExitFlag diff --git a/pypesto/petab/importer.py b/pypesto/petab/importer.py index ea00937bf..8a34b4597 100644 --- a/pypesto/petab/importer.py +++ b/pypesto/petab/importer.py @@ -19,6 +19,15 @@ import numpy as np import pandas as pd +import petab +from petab.C import ( + ESTIMATE, + NOISE_PARAMETERS, + OBSERVABLE_ID, + PREEQUILIBRATION_CONDITION_ID, + SIMULATION_CONDITION_ID, +) +from petab.models import MODEL_TYPE_SBML from ..C import ( CENSORED, @@ -50,16 +59,7 @@ import amici.petab.conditions import amici.petab.parameter_mapping import amici.petab.simulations - import petab from amici.petab.import_helpers import check_model - from petab.C import ( - ESTIMATE, - NOISE_PARAMETERS, - OBSERVABLE_ID, - PREEQUILIBRATION_CONDITION_ID, - SIMULATION_CONDITION_ID, - ) - from petab.models import MODEL_TYPE_SBML except ImportError: amici = None diff --git a/pypesto/problem/base.py b/pypesto/problem/base.py index 8b11c5dd4..15548b023 100644 --- a/pypesto/problem/base.py +++ b/pypesto/problem/base.py @@ -1,5 +1,6 @@ import copy import logging +import sys from collections.abc import Iterable from typing import ( Callable, @@ -15,6 +16,7 @@ from ..objective import ObjectiveBase from ..objective.priors import NegLogParameterPriors from ..startpoint import StartpointMethod, to_startpoint_method, uniform +from ..version import __version__ SupportsFloatIterableOrValue = Union[Iterable[SupportsFloat], SupportsFloat] SupportsIntIterableOrValue = Union[Iterable[SupportsInt], SupportsInt] @@ -164,6 +166,9 @@ def __init__( startpoint_method = uniform # convert startpoint method to class instance self.startpoint_method = to_startpoint_method(startpoint_method) + # save python and pypesto version + self.python_version = ".".join(map(str, sys.version_info[:3])) + self.pypesto_version = __version__ @property def lb(self) -> np.ndarray: @@ -237,6 +242,15 @@ def normalize(self) -> None: x_fixed_vals=self.x_fixed_vals, ) + # make prior aware of fixed parameters (for sampling etc.) + if self.x_priors is not None: + self.x_priors.update_from_problem( + dim_full=self.dim_full, + x_free_indices=self.x_free_indices, + x_fixed_indices=self.x_fixed_indices, + x_fixed_vals=self.x_fixed_vals, + ) + # sanity checks if len(self.x_scales) != self.dim_full: raise AssertionError("x_scales dimension invalid.") @@ -332,7 +346,10 @@ def unfix_parameters( self.normalize() def get_full_vector( - self, x: Union[np.ndarray, None], x_fixed_vals: Iterable[float] = None + self, + x: Union[np.ndarray, None], + x_fixed_vals: Iterable[float] = None, + x_is_grad: bool = False, ) -> Union[np.ndarray, None]: """ Map vector from dim to dim_full. Usually used for x, grad. @@ -342,9 +359,9 @@ def get_full_vector( x: array_like, shape=(dim,) The vector in dimension dim. x_fixed_vals: array_like, ndim=1, optional - The values to be used for the fixed indices. If None, then nans are - inserted. Usually, None will be used for grad and - problem.x_fixed_vals for x. + The values to be used for the fixed indices. If None and x_is_grad=False, problem.x_fixed_vals is used; for x_is_grad=True, nans are inserted. + x_is_grad: bool + If true, x is treated as gradients. """ if x is None: return None @@ -362,6 +379,9 @@ def get_full_vector( x_full[..., self.x_free_indices] = x if x_fixed_vals is not None: x_full[..., self.x_fixed_indices] = x_fixed_vals + return x_full + if not x_is_grad: + x_full[..., self.x_fixed_indices] = self.x_fixed_vals return x_full def get_full_matrix( @@ -479,6 +499,22 @@ def print_parameter_summary(self) -> None: ) ) + def get_startpoints(self, n_starts: int) -> np.ndarray: + """ + Sample startpoints from method. + + Parameters + ---------- + n_starts: + Number of start points. + + Returns + ------- + xs: + Start points, shape (n_starts, dim). + """ + return self.startpoint_method(n_starts, self) + _convtypes = { "float": {"attr": "__float__", "conv": float}, diff --git a/pypesto/result/optimize.py b/pypesto/result/optimize.py index 4fb2883fa..6f1d69964 100644 --- a/pypesto/result/optimize.py +++ b/pypesto/result/optimize.py @@ -191,10 +191,10 @@ def update_to_full(self, problem: Problem) -> None: problem which contains info about how to convert to full vectors or matrices """ - self.x = problem.get_full_vector(self.x, problem.x_fixed_vals) - self.grad = problem.get_full_vector(self.grad) + self.x = problem.get_full_vector(self.x) + self.grad = problem.get_full_vector(self.grad, x_is_grad=True) self.hess = problem.get_full_matrix(self.hess) - self.x0 = problem.get_full_vector(self.x0, problem.x_fixed_vals) + self.x0 = problem.get_full_vector(self.x0) self.free_indices = np.array(problem.x_free_indices) diff --git a/pypesto/sample/emcee.py b/pypesto/sample/emcee.py index 1afe8930f..f0e30c569 100644 --- a/pypesto/sample/emcee.py +++ b/pypesto/sample/emcee.py @@ -149,7 +149,7 @@ def initialize( lb = self.problem.lb ub = self.problem.ub - # parameter dimenstion + # parameter dimension ndim = len(self.problem.x_free_indices) def log_prob(x): diff --git a/pypesto/sample/geweke_test.py b/pypesto/sample/geweke_test.py index 186c9890d..f9c810c6e 100644 --- a/pypesto/sample/geweke_test.py +++ b/pypesto/sample/geweke_test.py @@ -46,7 +46,11 @@ def spectrum(x: np.ndarray, nfft: int = None, nw: int = None) -> np.ndarray: n = nw # Number of windows - k = np.floor((n - n_overlap) / (nw - n_overlap)).astype(int) + k = ( + np.floor((n - n_overlap) / (nw - n_overlap)).astype(int) + if nw != n_overlap + else 0 + ) index = np.arange(nw) # Normalizing scale factor kmu = k * np.linalg.norm(w) ** 2 diff --git a/pypesto/sample/metropolis.py b/pypesto/sample/metropolis.py index 34c5d8d85..7213dfcc3 100644 --- a/pypesto/sample/metropolis.py +++ b/pypesto/sample/metropolis.py @@ -129,6 +129,14 @@ def _perform_step( # compute log prior lprior_new = -self.neglogprior(x_new) + # if lpost_new is -inf, x_new will not be accepted + if lpost_new == -np.inf: + # update proposal + self._update_proposal( + x, lpost, -np.inf, len(self.trace_neglogpost) + 1 + ) + return x, lpost, lprior + if not self.temper_lpost: # extract current log likelihood value llh = lpost - lprior diff --git a/pypesto/sample/parallel_tempering.py b/pypesto/sample/parallel_tempering.py index 2d54857f6..306774c46 100644 --- a/pypesto/sample/parallel_tempering.py +++ b/pypesto/sample/parallel_tempering.py @@ -260,7 +260,6 @@ def compute_log_evidence( # integrate from low to high temperature y=mean_loglike_per_beta, x=temps, - even="last", ) else: raise ValueError( diff --git a/pypesto/store/save_to_hdf5.py b/pypesto/store/save_to_hdf5.py index a38f34a06..3804bfbbb 100644 --- a/pypesto/store/save_to_hdf5.py +++ b/pypesto/store/save_to_hdf5.py @@ -92,7 +92,7 @@ def write(self, problem, overwrite: bool = False): value = np.asarray(value) if value.size: write_array(problem_grp, problem_attr, value) - elif isinstance(value, Integral): + elif isinstance(value, (Integral, str)): problem_grp.attrs[problem_attr] = value @@ -316,3 +316,9 @@ def write_result( if sample: pypesto_sample_writer = SamplingResultHDF5Writer(filename) pypesto_sample_writer.write(result, overwrite=overwrite) + + if hasattr(result, "variational_result"): + logger.warning( + "Results from variational inference are not saved in the hdf5 file. " + "You have to save them manually." + ) diff --git a/pypesto/variational/__init__.py b/pypesto/variational/__init__.py new file mode 100644 index 000000000..1d3065277 --- /dev/null +++ b/pypesto/variational/__init__.py @@ -0,0 +1,9 @@ +""" +Variational inference +====== + +Find the best variational approximation in a given family to a distribution from which we can sample. +""" + +from .pymc import PymcVariational +from .variational_inference import variational_fit diff --git a/pypesto/variational/pymc.py b/pypesto/variational/pymc.py new file mode 100644 index 000000000..cfb7348f4 --- /dev/null +++ b/pypesto/variational/pymc.py @@ -0,0 +1,196 @@ +"""Pymc v4 Sampler for Variational Inference.""" + +import logging +from typing import Optional + +import numpy as np +import pytensor.tensor as pt +from scipy import stats + +from ..objective import FD +from ..result import McmcPtResult +from ..sample.pymc import PymcObjectiveOp, PymcSampler +from ..sample.sampler import SamplerImportError + +logger = logging.getLogger(__name__) + + +# implementation based on the pymc sampler code in pypesto and: +# https://www.pymc.io/projects/examples/en/latest/variational_inference/variational_api_quickstart.html + + +class PymcVariational(PymcSampler): + """Wrapper around Pymc v4 variational inference. + + Parameters + ---------- + step_function: + A pymc step function, e.g. NUTS, Slice. If not specified, pymc + determines one automatically (preferable). + **kwargs: + Options are directly passed on to `pymc.fit`. + """ + + def fit( + self, + n_iterations: int, + method: str = "advi", + random_seed: Optional[int] = None, + start_sigma: Optional = None, + inf_kwargs: Optional = None, + beta: float = 1.0, + **kwargs, + ): + """ + Sample the problem. + + Parameters + ---------- + n_iterations: + Number of iterations. + method: str or :class:`Inference` of pymc + string name is case-insensitive in: + - 'advi' for ADVI + - 'fullrank_advi' for FullRankADVI + - 'svgd' for Stein Variational Gradient Descent + - 'asvgd' for Amortized Stein Variational Gradient Descent + random_seed: int + random seed for reproducibility + start_sigma: `dict[str, np.ndarray]` + starting standard deviation for inference, only available for method 'advi' + inf_kwargs: dict + additional kwargs passed to pymc.Inference + beta: + Inverse temperature (e.g. in parallel tempering). + """ + try: + import pymc + except ImportError: + raise SamplerImportError("pymc") from None + + problem = self.problem + if not problem.objective.has_grad: + logger.info( + "The objective function does not provide gradients. " + "Finite differences will be used." + ) + problem.objective = FD(obj=problem.objective) + log_post = PymcObjectiveOp.create_instance(problem.objective, beta) + + x0 = None + x_names_free = problem.get_reduced_vector(problem.x_names) + if self.x0 is not None: + x0 = { + x_name: val + for x_name, val in zip(problem.x_names, self.x0) + if x_name in x_names_free + } + + # create model context + with pymc.Model(): + # parameter bounds as uniform prior + _k = [ + pymc.Uniform(x_name, lower=lb, upper=ub) + for x_name, lb, ub in zip( + x_names_free, + problem.lb, + problem.ub, + ) + ] + + # convert parameters to PyTensor tensor variable + theta = pt.as_tensor_variable(_k) + + # define distribution with log-posterior as density + pymc.Potential("potential", log_post(theta)) + + # record function values + pymc.Deterministic("loggyposty", log_post(theta)) + + # perform the actual sampling + data = pymc.fit( + n=int(n_iterations), + method=method, + random_seed=random_seed, + start=x0, + start_sigma=start_sigma, + inf_kwargs=inf_kwargs, + **kwargs, + ) + + self.data = data + + def sample(self, n_samples: int, beta: float = 1.0) -> McmcPtResult: + """ + Sample from the variational approximation and return McmcPtResult object. + + Parameters + ---------- + n_samples: + Number of samples to be computed. + """ + # get InferenceData object + pymc_data = self.data.sample(n_samples) + x_names_free = self.problem.get_reduced_vector(self.problem.x_names) + post_samples = np.concatenate( + [pymc_data.posterior[name].values for name in x_names_free] + ).T + return McmcPtResult( + trace_x=post_samples[np.newaxis, :], + trace_neglogpost=pymc_data.posterior.loggyposty.values, + trace_neglogprior=np.full( + pymc_data.posterior.loggyposty.values.shape, np.nan + ), + betas=np.array([1.0] * post_samples.shape[0]), + burn_in=0, + auto_correlation=0, + effective_sample_size=n_samples, + message="variational inference results", + ) + + def get_variational_parameters(self) -> (list, list): + """Get the internal pymc variational parameters.""" + return ( + [param.name for param in self.data.params], + [param.eval() for param in self.data.params], + ) + + def set_variational_parameters(self, param_list: list): + """ + Set the internal pymc variational parameters. + + Parameters + ---------- + param_list: + List of tuples of the form (param_name, param_value). + """ + if len(param_list) != len(self.data.params): + raise ValueError( + "The number of parameters does not match the number of variational parameters." + ) + for i, param in enumerate(param_list): + self.data.params[i].set_value(param) + + def eval_variational_log_density(self, x: np.ndarray) -> np.ndarray: + """ + Evaluate the log density of the variational approximation at x_points. + + Parameters + ---------- + x: + The points at which to evaluate the log density. + """ + # TODO: add support for other methods + logger.warning( + "currently only supports the methods `advi` and `fullrank_advi`" + ) + + if x.ndim == 1: + x = x.reshape(1, -1) + log_density_at_points = np.zeros_like(x) + for i, point in enumerate(x): + log_density_at_points[i] = stats.multivariate_normal.logpdf( + point, mean=self.data.mean.eval(), cov=self.data.cov.eval() + ) + vi_log_density = np.sum(log_density_at_points, axis=-1) + return vi_log_density diff --git a/pypesto/variational/variational_inference.py b/pypesto/variational/variational_inference.py new file mode 100644 index 000000000..23b0c8266 --- /dev/null +++ b/pypesto/variational/variational_inference.py @@ -0,0 +1,136 @@ +"""Functions for variational inference accessible to the user. Currently only pymc is supported.""" + +import logging +from time import process_time +from typing import Callable, List, Optional, Union + +import numpy as np + +from ..problem import Problem +from ..result import Result +from ..sample.util import bound_n_samples_from_env +from ..store import autosave +from .pymc import PymcVariational + +logger = logging.getLogger(__name__) + + +def variational_fit( + problem: Problem, + n_iterations: int, + method: str = "advi", + n_samples: Optional[int] = None, + random_seed: Optional[int] = None, + start_sigma: Optional[dict[str, np.ndarray]] = None, + x0: Union[np.ndarray, List[np.ndarray]] = None, + result: Result = None, + filename: Union[str, Callable, None] = None, + overwrite: bool = False, + **kwargs, +) -> Result: + """ + Call to do parameter sampling. + + Parameters + ---------- + problem: + The problem to be solved. If None is provided, a + :class:`pypesto.AdaptiveMetropolisSampler` is used. + n_iterations: + Number of iterations for the optimization. + method: str or :class:`Inference` of pymc (only interface currently supported) + string name is case-insensitive in: + - 'advi' for ADVI + - 'fullrank_advi' for FullRankADVI + - 'svgd' for Stein Variational Gradient Descent + - 'asvgd' for Amortized Stein Variational Gradient Descent + n_samples: + Number of samples to generate after optimization. + random_seed: int + random seed for reproducibility + start_sigma: `dict[str, np.ndarray]` + starting standard deviation for inference, only available for method 'advi' + x0: + Initial parameter for the variational optimization. If None, the best parameter + found in optimization is used. + result: + A result to write to. If None provided, one is created from the + problem. + filename: + Name of the hdf5 file, where the result will be saved. Default is + None, which deactivates automatic saving. If set to + "Auto" it will automatically generate a file named + `year_month_day_profiling_result.hdf5`. + Optionally a method, see docs for `pypesto.store.auto.autosave`. + overwrite: + Whether to overwrite `result/sampling` in the autosave file + if it already exists. + + Returns + ------- + result: + A result with filled in sample_options part. + """ + # prepare result object + if result is None: + result = Result(problem) + + # number of samples + if n_iterations is not None: + n_iterations = bound_n_samples_from_env(n_iterations) + + # try to find initial parameters + if x0 is None: + result.optimize_result.sort() + if len(result.optimize_result.list) > 0: + x0 = problem.get_reduced_vector( + result.optimize_result.list[0]["x"] + ) + + # set variational inference + # currently we only support pymc + variational = PymcVariational() + + # initialize sampler to problem + variational.initialize(problem=problem, x0=x0) + + # perform the sampling and track time + t_start = process_time() + variational.fit( + n_iterations=n_iterations, + method=method, + random_seed=random_seed, + start_sigma=start_sigma, + **kwargs, + ) + t_elapsed = process_time() - t_start + logger.info("Elapsed time: " + str(t_elapsed)) + + # extract results and save samples to pypesto result + if n_samples is None or n_samples == 0: + # constructing a McmcPtResult object with nearly empty trace_x + n_samples = 1 + + result.sample_result = variational.sample(n_samples) + result.sample_result.time = t_elapsed + + autosave( + filename=filename, + result=result, + store_type="sample", + overwrite=overwrite, + ) + + # make pymc object available in result + # TODO: if needed, we can add a result object for variational inference methods + result.variational_result = variational + ( + result.sample_result.variational_parameters_names, + result.sample_result.variational_parameters, + ) = variational.get_variational_parameters() + if filename is not None: + logger.warning( + "Variational parameters are not saved in the hdf5 file. You have to save them manually." + ) + + return result diff --git a/pypesto/version.py b/pypesto/version.py index 3d187266f..722515271 100644 --- a/pypesto/version.py +++ b/pypesto/version.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.5.2" diff --git a/pypesto/visualize/misc.py b/pypesto/visualize/misc.py index d23f710b5..5d7c1b491 100644 --- a/pypesto/visualize/misc.py +++ b/pypesto/visualize/misc.py @@ -1,3 +1,4 @@ +import logging import warnings from collections.abc import Iterable from numbers import Number @@ -23,6 +24,8 @@ from ..util import assign_clusters, delete_nan_inf from .clust_color import assign_colors_for_list +logger = logging.getLogger(__name__) + def process_result_list( results: Union[Result, list[Result]], colors=None, legends=None @@ -303,8 +306,8 @@ def process_start_indices( """ Process the start_indices. - Create an array of indices if a number was provided and checks that the - indices do not exceed the max_index. + Create an array of indices if a number was provided, checks that the indices + do not exceed the max_index and removes starts with non-finite fval. Parameters ---------- @@ -323,7 +326,7 @@ def process_start_indices( start_indices = ALL if isinstance(start_indices, str): if start_indices == ALL: - return np.asarray(range(len(result.optimize_result))) + start_indices = np.asarray(range(len(result.optimize_result))) elif start_indices == ALL_CLUSTERED: clust_ind, clust_size = assign_clusters( delete_nan_inf(result.optimize_result.fval)[1] @@ -336,12 +339,12 @@ def process_start_indices( start_indices = np.concatenate( [np.where(clust_ind == i_clust)[0] for i_clust in clust_gr2] ) - return start_indices + start_indices = start_indices elif start_indices == FIRST_CLUSTER: clust_ind = assign_clusters( delete_nan_inf(result.optimize_result.fval)[1] )[0] - return np.where(clust_ind == 0)[0] + start_indices = np.where(clust_ind == 0)[0] else: raise ValueError( f"Permissible values for start_indices are {ALL}, " @@ -359,6 +362,18 @@ def process_start_indices( if start_index < len(result.optimize_result) ] + # filter out the indices that are not finite + start_indices_unfiltered = len(start_indices) + start_indices = [ + start_index + for start_index in start_indices + if np.isfinite(result.optimize_result[start_index].fval) + ] + if len(start_indices) != start_indices_unfiltered: + logger.warning( + "Some start indices were removed due to inf or nan function values." + ) + return np.asarray(start_indices) diff --git a/pypesto/visualize/model_fit.py b/pypesto/visualize/model_fit.py index dd2fd0d4a..ef21d0686 100644 --- a/pypesto/visualize/model_fit.py +++ b/pypesto/visualize/model_fit.py @@ -14,6 +14,7 @@ import matplotlib.pyplot as plt import numpy as np import petab +from amici.petab.conditions import fill_in_parameters from amici.petab.simulations import rdatas_to_simulation_df from petab.visualize import plot_problem @@ -269,7 +270,7 @@ def _get_simulation_rdatas( for j in range(len(edatas)): edatas[j].setTimepoints(simulation_timepoints) - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/visualize/optimization_stats.py b/pypesto/visualize/optimization_stats.py index 74a5323e7..65e864033 100644 --- a/pypesto/visualize/optimization_stats.py +++ b/pypesto/visualize/optimization_stats.py @@ -53,15 +53,19 @@ def optimization_run_properties_one_plot( Examples -------- - optimization_properties_per_multistart( - result1, - properties_to_plot=['time'], - colors=[.5, .9, .9, .3]) - - optimization_properties_per_multistart( - result1, - properties_to_plot=['time', 'n_grad'], - colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]]) + .. code-block:: python + + optimization_run_properties_one_plot( + result1, + properties_to_plot=['time'], + colors=[.5, .9, .9, .3] + ) + + optimization_run_properties_one_plot( + result1, + properties_to_plot=['time', 'n_grad'], + colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]] + ) """ if properties_to_plot is None: properties_to_plot = [ @@ -156,24 +160,30 @@ def optimization_run_properties_per_multistart( Examples -------- - optimization_properties_per_multistart( - result1, - properties_to_plot=['time'], - colors=[.5, .9, .9, .3]) - - optimization_properties_per_multistart( - [result1, result2], - properties_to_plot=['time'], - colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]]) - - optimization_properties_per_multistart( - result1, - properties_to_plot=['time', 'n_grad'], - colors=[.5, .9, .9, .3]) - - optimization_properties_per_multistart( - [result1, result2], properties_to_plot=['time', 'n_fval'], - colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]]) + .. code-block:: python + + optimization_run_properties_per_multistart( + result1, + properties_to_plot=['time'], + colors=[.5, .9, .9, .3] + ) + + optimization_run_properties_per_multistart( + [result1, result2], + properties_to_plot=['time'], + colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]] + ) + + optimization_run_properties_per_multistart( + result1, + properties_to_plot=['time', 'n_grad'], + colors=[.5, .9, .9, .3] + ) + + optimization_run_properties_per_multistart( + [result1, result2], properties_to_plot=['time', 'n_fval'], + colors=[[.5, .9, .9, .3], [.2, .1, .9, .5]] + ) """ if properties_to_plot is None: properties_to_plot = [ diff --git a/pypesto/visualize/ordinal_categories.py b/pypesto/visualize/ordinal_categories.py index 1672465a9..c229d92f6 100644 --- a/pypesto/visualize/ordinal_categories.py +++ b/pypesto/visualize/ordinal_categories.py @@ -9,6 +9,7 @@ try: import amici + from amici.petab.conditions import fill_in_parameters from petab.C import OBSERVABLE_ID from ..hierarchical.ordinal.calculator import OrdinalCalculator @@ -86,7 +87,7 @@ def plot_categories_from_pypesto_result( n_threads = pypesto_result.problem.objective.n_threads # Fill in the parameters. - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/pypesto/visualize/parameters.py b/pypesto/visualize/parameters.py index a482a94ae..c50f4fdeb 100644 --- a/pypesto/visualize/parameters.py +++ b/pypesto/visualize/parameters.py @@ -417,8 +417,8 @@ def handle_inputs( ub = result.problem.get_reduced_vector(ub, parameter_indices) x_labels = [x_labels[int(i)] for i in parameter_indices] else: - lb = result.problem.get_full_vector(lb) - ub = result.problem.get_full_vector(ub) + lb = result.problem.lb_full + ub = result.problem.ub_full if inner_xs is not None and plot_inner_parameters: lb = np.concatenate([lb, inner_lb]) @@ -601,23 +601,10 @@ def optimization_scatter( parameter_indices = process_parameter_indices( parameter_indices=parameter_indices, result=result ) - # remove all start indices that encounter an inf value at the start - # resulting in optimize_result[start]["x"] being None - start_indices_finite = start_indices[ - [ - result.optimize_result[i_start]["x"] is not None - for i_start in start_indices - ] - ] - # compare start_indices with start_indices_finite and log a warning - if len(start_indices) != len(start_indices_finite): - logger.warning( - "Some start indices were removed due to inf values at the start." - ) # put all parameters into a dataframe, where columns are parameters parameters = [ result.optimize_result[i_start]["x"][parameter_indices] - for i_start in start_indices_finite + for i_start in start_indices ] x_labels = [ result.problem.x_names[parameter_index] diff --git a/pypesto/visualize/spline_approximation.py b/pypesto/visualize/spline_approximation.py index 5f617db30..217f445f1 100644 --- a/pypesto/visualize/spline_approximation.py +++ b/pypesto/visualize/spline_approximation.py @@ -23,6 +23,7 @@ try: import amici + from amici.petab.conditions import fill_in_parameters from ..hierarchical import InnerCalculatorCollector from ..hierarchical.semiquantitative.calculator import SemiquantCalculator @@ -105,7 +106,7 @@ def plot_splines_from_pypesto_result( observable_ids = amici_model.getObservableIds() # Fill in the parameters. - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, @@ -379,7 +380,7 @@ def _add_spline_mapped_simulations_to_model_fit( n_threads = pypesto_problem.objective.n_threads # Fill in the parameters. - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, @@ -528,7 +529,7 @@ def _obtain_regularization_for_start( n_threads = pypesto_result.problem.objective.n_threads # Fill in the parameters. - amici.parameter_mapping.fill_in_parameters( + fill_in_parameters( edatas=edatas, problem_parameters=x_dct, scaled_parameters=True, diff --git a/setup.cfg b/setup.cfg index 129cedb13..425cbc8d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,9 +33,9 @@ classifiers = License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python + Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.9 keywords = parameter inference optimization @@ -58,7 +58,7 @@ install_requires = tqdm >= 4.46.0 tabulate >= 0.8.10 -python_requires = >=3.9 +python_requires = >=3.10 include_package_data = True # Where is my code @@ -117,7 +117,6 @@ mpi = mpi4py >= 3.0.3 pymc = arviz >= 0.12.1 - scipy < 1.13.0 # https://github.com/ICB-DCM/pyPESTO/issues/1354 aesara >= 2.8.6 pymc >= 4.2.1 aesara = diff --git a/test/base/test_history.py b/test/base/test_history.py index 57d01a3f2..9056fd7a2 100644 --- a/test/base/test_history.py +++ b/test/base/test_history.py @@ -236,9 +236,7 @@ def check_reconstruct_history( def check_history_consistency(self, start: pypesto.OptimizerResult): def xfull(x_trace): - return self.problem.get_full_vector( - x_trace, self.problem.x_fixed_vals - ) + return self.problem.get_full_vector(x_trace) if isinstance(start.history, (CsvHistory, Hdf5History)): # get index of optimal parameter diff --git a/test/base/test_objective.py b/test/base/test_objective.py index d2c556927..a9d2e3151 100644 --- a/test/base/test_objective.py +++ b/test/base/test_objective.py @@ -2,6 +2,7 @@ import copy import numbers +import sys from functools import partial import numpy as np @@ -12,6 +13,11 @@ from ..util import CRProblem, poly_for_sensi, rosen_for_sensi +pytest_skip_aesara = pytest.mark.skipif( + sys.version_info >= (3, 12), + reason="Skipped Aesara tests on Python 3.12 or higher", +) + @pytest.fixture(params=[True, False]) def integrated(request): @@ -178,6 +184,7 @@ def rel_err(eps_): ) +@pytest_skip_aesara def test_aesara(max_sensi_order, integrated): """Test function composition and gradient computation via aesara""" import aesara.tensor as aet @@ -216,7 +223,8 @@ def test_aesara(max_sensi_order, integrated): @pytest.mark.parametrize("enable_x64", [True, False]) -def test_jax(max_sensi_order, integrated, enable_x64): +@pytest.mark.parametrize("fix_parameters", [True, False]) +def test_jax(max_sensi_order, integrated, enable_x64, fix_parameters): """Test function composition and gradient computation via jax""" import jax import jax.numpy as jnp @@ -227,6 +235,7 @@ def test_jax(max_sensi_order, integrated, enable_x64): jax.config.update("jax_enable_x64", enable_x64) from pypesto.objective.jax import JaxObjective + from pypesto.objective.pre_post_process import FixedParametersProcessor prob = rosen_for_sensi(max_sensi_order, integrated, [0, 1]) @@ -243,9 +252,20 @@ def jax_op_out(x: jnp.array) -> jnp.array: # compose rosenbrock function with sinh transformation obj = JaxObjective(prob["obj"]) + if fix_parameters: + obj.pre_post_processor = FixedParametersProcessor( + dim_full=2, + x_free_indices=[0], + x_fixed_indices=[1], + x_fixed_vals=[0.0], + ) + # evaluate for a couple of random points such that we can assess # compatibility with vmap xx = x_ref + np.random.randn(10, x_ref.shape[0]) + if fix_parameters: + xx = xx[:, obj.pre_post_processor.x_free_indices] + rvals_ref = [ jax_op_out( prob["obj"](jax_op_in(xxi), sensi_orders=(max_sensi_order,)) @@ -256,6 +276,9 @@ def jax_op_out(x: jnp.array) -> jnp.array: def _fun(y, pypesto_fun, jax_fun_in, jax_fun_out): return jax_fun_out(pypesto_fun(jax_fun_in(y))) + assert obj.check_sensi_orders((max_sensi_order,), pypesto.C.MODE_FUN) + assert not obj.check_sensi_orders((max_sensi_order,), pypesto.C.MODE_RES) + for _obj in (obj, copy.deepcopy(obj)): fun = partial( _fun, @@ -266,6 +289,7 @@ def _fun(y, pypesto_fun, jax_fun_in, jax_fun_out): if max_sensi_order == 1: fun = jax.grad(fun) + # check compatibility with vmap and jit vmapped_fun = jax.vmap(fun) rvals_jax = vmapped_fun(xx) @@ -274,8 +298,11 @@ def _fun(y, pypesto_fun, jax_fun_in, jax_fun_out): # can't use rtol = 1e-8 for 32bit rtol = 1e-16 if enable_x64 else 1e-4 for x, rref, rj in zip(xx, rvals_ref, rvals_jax): + assert isinstance(rj, jnp.ndarray) if max_sensi_order == 0: - np.testing.assert_allclose(rref, rj, atol=atol, rtol=rtol) + np.testing.assert_allclose( + rref, float(rj), atol=atol, rtol=rtol + ) if max_sensi_order == 1: # g(x) = b(c(x)) => g'(x) = b'(c(x))) * c'(x) # f(x) = a(g(x)) => f'(x) = a'(g(x)) * g'(x) @@ -288,7 +315,9 @@ def _fun(y, pypesto_fun, jax_fun_in, jax_fun_out): ) @ jax.jacfwd(jax_op_in)(x) # f'(x) = a'(g(x)) * g'(x) f_prime = jax.jacfwd(jax_op_out)(g) * g_prime - np.testing.assert_allclose(f_prime, rj, atol=atol, rtol=rtol) + np.testing.assert_allclose( + f_prime, np.asarray(rj), atol=atol, rtol=rtol + ) @pytest.fixture( diff --git a/test/base/test_startpoint.py b/test/base/test_startpoint.py index 94a100022..cb3165eee 100644 --- a/test/base/test_startpoint.py +++ b/test/base/test_startpoint.py @@ -5,6 +5,8 @@ import pypesto +from ..visualize import create_problem + # default setting n_starts = 5 dim = 2 @@ -121,3 +123,17 @@ def grad(x: np.ndarray): assert not np.allclose(x_guesses, xs[:n_guesses, :]) else: assert np.allclose(x_guesses, xs[:n_guesses, :]) + + +def test_startpoints_from_problem(): + """Test that startpoints can be generated from a problem.""" + # create problem + problem = create_problem() + + # generate startpoints + xs = problem.get_startpoints(n_starts=n_starts) + + # check shape and bounds + assert xs.shape == (n_starts, problem.dim) + assert np.all(xs >= problem.lb) + assert np.all(xs <= problem.ub) diff --git a/test/base/test_x_fixed.py b/test/base/test_x_fixed.py index fc7ea2e00..320cd8d08 100644 --- a/test/base/test_x_fixed.py +++ b/test/base/test_x_fixed.py @@ -41,8 +41,7 @@ def test_optimize(): # fixed values written into parameter vector assert optimizer_result.x[1] == 1 - lb_full = problem.get_full_vector(problem.lb) - assert len(lb_full) == 5 + assert len(problem.lb_full) == 5 def create_problem(): diff --git a/test/sample/test_sample.py b/test/sample/test_sample.py index a33adde18..40d246aa4 100644 --- a/test/sample/test_sample.py +++ b/test/sample/test_sample.py @@ -3,7 +3,6 @@ import os import numpy as np -import petab import pytest import scipy.optimize as so from scipy.integrate import quad @@ -11,10 +10,8 @@ import pypesto import pypesto.optimize as optimize -import pypesto.petab import pypesto.sample as sample from pypesto.C import OBJECTIVE_NEGLOGLIKE, OBJECTIVE_NEGLOGPOST -from pypesto.sample.pymc import PymcSampler def gaussian_llh(x): @@ -96,6 +93,10 @@ def rosenbrock_problem(): def create_petab_problem(): + import petab + + import pypesto.petab + current_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.abspath( os.path.join(current_path, "..", "..", "doc", "example") @@ -187,6 +188,8 @@ def sampler(request): n_chains=5, ) elif request.param == "Pymc": + from pypesto.sample.pymc import PymcSampler + return PymcSampler(tune=5, progressbar=False) elif request.param == "Emcee": return sample.EmceeSampler(nwalkers=10) @@ -793,42 +796,43 @@ def test_thermodynamic_integration(): problem = gaussian_problem() # approximation should be better for more chains - for n_chains, tol in zip([10, 20], [1, 1e-1]): - sampler = sample.ParallelTemperingSampler( - internal_sampler=sample.AdaptiveMetropolisSampler(), - options={"show_progress": False, "beta_init": "beta_decay"}, - n_chains=n_chains, - ) + n_chains = 10 + tol = 1 + sampler = sample.ParallelTemperingSampler( + internal_sampler=sample.AdaptiveMetropolisSampler(), + options={"show_progress": False, "beta_init": "beta_decay"}, + n_chains=n_chains, + ) - result = optimize.minimize( - problem, - progress_bar=False, - ) + result = optimize.minimize( + problem, + progress_bar=False, + ) - result = sample.sample( - problem, - n_samples=10000, - result=result, - sampler=sampler, - ) + result = sample.sample( + problem, + n_samples=2000, + result=result, + sampler=sampler, + ) - # compute the log evidence using trapezoid and simpson rule - log_evidence = sampler.compute_log_evidence(result, method="trapezoid") - log_evidence_not_all = sampler.compute_log_evidence( - result, method="trapezoid", use_all_chains=False - ) - log_evidence_simps = sampler.compute_log_evidence( - result, method="simpson" - ) + # compute the log evidence using trapezoid and simpson rule + log_evidence = sampler.compute_log_evidence(result, method="trapezoid") + log_evidence_not_all = sampler.compute_log_evidence( + result, method="trapezoid", use_all_chains=False + ) + log_evidence_simps = sampler.compute_log_evidence(result, method="simpson") - # compute evidence - evidence = quad( - lambda x: 1 / (problem.ub - problem.lb) * np.exp(gaussian_llh(x)), - a=problem.lb, - b=problem.ub, - ) + # compute evidence + evidence = quad( + lambda x: 1 + / (problem.ub[0] - problem.lb[0]) + * np.exp(gaussian_llh(x)), + a=problem.lb[0], + b=problem.ub[0], + ) - # compare to known value - assert np.isclose(log_evidence, np.log(evidence[0]), atol=tol) - assert np.isclose(log_evidence_not_all, np.log(evidence[0]), atol=tol) - assert np.isclose(log_evidence_simps, np.log(evidence[0]), atol=tol) + # compare to known value + assert np.isclose(log_evidence, np.log(evidence[0]), atol=tol) + assert np.isclose(log_evidence_not_all, np.log(evidence[0]), atol=tol) + assert np.isclose(log_evidence_simps, np.log(evidence[0]), atol=tol) diff --git a/test/variational/__init__.py b/test/variational/__init__.py new file mode 100644 index 000000000..4ce01abf5 --- /dev/null +++ b/test/variational/__init__.py @@ -0,0 +1 @@ +"""Variational inference tests.""" diff --git a/test/variational/test_variational.py b/test/variational/test_variational.py new file mode 100644 index 000000000..c3b829bf3 --- /dev/null +++ b/test/variational/test_variational.py @@ -0,0 +1,72 @@ +"""Tests for `pypesto.sample` methods.""" + +import pytest +from scipy.stats import kstest + +import pypesto.optimize as optimize +from pypesto.variational import variational_fit + +from ..sample.test_sample import ( + gaussian_mixture_problem, + gaussian_problem, + rosenbrock_problem, +) + + +@pytest.fixture(params=["gaussian", "gaussian_mixture", "rosenbrock"]) +def problem(request): + if request.param == "gaussian": + return gaussian_problem() + if request.param == "gaussian_mixture": + return gaussian_mixture_problem() + elif request.param == "rosenbrock": + return rosenbrock_problem() + + +def test_pipeline(problem): + """Check that a typical pipeline runs through.""" + # optimization + optimizer = optimize.ScipyOptimizer(options={"maxiter": 10}) + result = optimize.minimize( + problem=problem, + n_starts=3, + optimizer=optimizer, + progress_bar=False, + ) + + # sample + result = variational_fit( + problem=problem, + n_iterations=100, + n_samples=10, + result=result, + ) + + +def test_ground_truth(): + """Test whether we actually retrieve correct distributions.""" + problem = gaussian_problem() + + result = optimize.minimize( + problem, + progress_bar=False, + ) + + result = variational_fit( + problem, + n_iterations=10000, + n_samples=5000, + result=result, + ) + + # get samples of first chain + samples = result.sample_result.trace_x[0].flatten() + + # test against different distributions + statistic, pval = kstest(samples, "norm") + print(statistic, pval) + assert statistic < 0.1 + + statistic, pval = kstest(samples, "uniform") + print(statistic, pval) + assert statistic > 0.1 diff --git a/test/visualize/test_visualize.py b/test/visualize/test_visualize.py index cff700497..0b568e12d 100644 --- a/test/visualize/test_visualize.py +++ b/test/visualize/test_visualize.py @@ -1179,3 +1179,15 @@ def test_sacess_history(): ) sacess.minimize(problem) sacess_history(sacess.histories) + + +@pytest.mark.parametrize( + "result_creation", + [create_optimization_result, create_optimization_result_nan_inf], +) +@close_fig +def test_parameters_correlation_matrix(result_creation): + """Test pypesto.visualize.parameters_correlation_matrix""" + result = result_creation() + + visualize.parameters_correlation_matrix(result)