From e49189436e83cecc5046e2c81367fb10559118af Mon Sep 17 00:00:00 2001 From: arrjon Date: Mon, 16 Feb 2026 21:47:49 +0100 Subject: [PATCH 01/55] Update CI configuration and dependencies for improved compatibility and performance --- .github/workflows/ci.yml | 360 +++++++++++------------ .github/workflows/deploy.yml | 49 +-- .github/workflows/generate_readme_rst.py | 12 - .github/workflows/install_deps.sh | 159 +++++----- MANIFEST.in | 8 +- pyproject.toml | 135 ++++++++- requirements-dev.txt | 2 - setup.cfg | 139 --------- setup.py | 3 - tox.ini | 163 +++++----- 10 files changed, 502 insertions(+), 528 deletions(-) delete mode 100644 .github/workflows/generate_readme_rst.py delete mode 100644 requirements-dev.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d2328960..d6588cc32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,263 +1,261 @@ name: CI -# trigger on: push: - branches: - - main - - develop + branches: [main, develop] pull_request: schedule: - # run Monday at 03:18 UTC - - cron: '18 15 * * MON' + # Monday at 15:18 UTC + - cron: "18 15 * * MON" jobs: - base: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.12', '3.11', '3.10'] + # Supported stable versions + python-version: ["3.13", "3.12", "3.11", "3.10"] steps: - - name: Check out repository - uses: actions/checkout@v4 + - name: Check out repository + uses: actions/checkout@v4 + + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: .github/workflows/install_deps.sh base R + + - name: Run tests + timeout-minutes: 15 + run: tox -e base - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - name: Run visualization tests + timeout-minutes: 5 + run: tox -e visualization - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-base + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + + # Early warning for upcoming Python (allowed to fail) + future-python: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + python-version: ["3.14-dev"] - - name: Install dependencies - run: .github/workflows/install_deps.sh base R + steps: + - name: Check out repository + uses: actions/checkout@v4 - - name: Run tests - timeout-minutes: 15 - run: tox -e base + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + allow-prereleases: true + cache: pip - - name: Run visualization tests - timeout-minutes: 3 - run: tox -e visualization + - name: Install dependencies + run: .github/workflows/install_deps.sh base R - - name: Coverage - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + - name: Run tests (base) + timeout-minutes: 20 + run: tox -e base external: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install julia - uses: julia-actions/setup-julia@v1 - with: - version: 1.7 - - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-external - - - name: Install dependencies - run: .github/workflows/install_deps.sh base R - - - name: Run tests - timeout-minutes: 15 - run: tox -e external - - - name: Coverage - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + - name: Check out repository + uses: actions/checkout@v4 + + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install Julia + uses: julia-actions/setup-julia@v1 + with: + version: "1.7" + + - name: Install dependencies + run: .github/workflows/install_deps.sh base R + + - name: Run external (R) tests + timeout-minutes: 20 + run: tox -e external-R + + - name: Run external (Julia/COPASI) tests + timeout-minutes: 20 + run: tox -e external-other-simulators + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml petab: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-petab - - - name: Install dependencies - run: .github/workflows/install_deps.sh amici - - - name: Run tests - timeout-minutes: 20 - run: | - tox -e petab - - - name: Coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + - name: Check out repository + uses: actions/checkout@v4 + + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: .github/workflows/install_deps.sh amici + + - name: Run tests + timeout-minutes: 30 + run: tox -e petab + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml mac: runs-on: macos-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - name: Check out repository uses: actions/checkout@v4 - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Cache - uses: actions/cache@v4 - with: - path: ~/Library/Caches/pip - key: ci-${{ runner.os }}-${{ matrix.python-version }}-mac + cache: pip - name: Install dependencies run: .github/workflows/install_deps.sh - name: Run tests - timeout-minutes: 10 + timeout-minutes: 15 run: tox -e mac - - name: Coverage - uses: codecov/codecov-action@v2 + - name: Upload coverage + uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + files: ./coverage.xml notebooks1: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - name: Check out repository + uses: actions/checkout@v4 - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-notebooks1 + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip - - name: Install dependencies - run: .github/workflows/install_deps.sh + - name: Install dependencies + run: .github/workflows/install_deps.sh - - name: Run notebooks - timeout-minutes: 15 - run: tox -e notebooks1 + - name: Run notebooks + timeout-minutes: 20 + run: tox -e notebooks1 notebooks2: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} + - name: Check out repository + uses: actions/checkout@v4 - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-notebooks2 + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip - - name: Install dependencies - run: .github/workflows/install_deps.sh R amici + - name: Install dependencies + run: .github/workflows/install_deps.sh R amici - - name: Run notebooks - timeout-minutes: 15 - run: tox -e notebooks2 + - name: Run notebooks + timeout-minutes: 20 + run: tox -e notebooks2 quality: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ['3.11'] + python-version: ["3.11"] steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Cache - uses: actions/cache@v4 - with: - path: ~/.cache - key: ci-${{ runner.os }}-${{ matrix.python-version }}-quality - - - name: Install dependencies - run: | - .github/workflows/install_deps.sh doc - pip install tox - pip install pypandoc - python .github/workflows/generate_readme_rst.py - - - - name: Run quality checks - timeout-minutes: 5 - run: tox -e project,flake8 - - - name: Build docs - timeout-minutes: 5 - run: tox -e doc - - - name: Test migration - timeout-minutes: 5 - run: tox -e migrate - - - name: Coverage - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml + - name: Check out repository + uses: actions/checkout@v4 + + - name: Prepare Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + .github/workflows/install_deps.sh doc + python -m pip install -U pip + python -m pip install tox + python .github/workflows/generate_readme_rst.py + + - name: Run quality checks + timeout-minutes: 10 + run: tox -e project,flake8 + + - name: Build docs + timeout-minutes: 10 + run: tox -e doc + + - name: Test migration + timeout-minutes: 10 + run: tox -e migrate + + - name: Upload coverage + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 712772ec4..972f4ea66 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,32 +5,33 @@ on: types: [created] workflow_dispatch: - jobs: deploy: runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.12] + + permissions: + contents: read steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - - name: Build and publish - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - name: Check out repository + uses: actions/checkout@v4 + + - name: Prepare Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install build tooling + run: | + python -m pip install -U pip + python -m pip install -U build twine + + - name: Build sdist and wheel + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* diff --git a/.github/workflows/generate_readme_rst.py b/.github/workflows/generate_readme_rst.py deleted file mode 100644 index 9a20a06bb..000000000 --- a/.github/workflows/generate_readme_rst.py +++ /dev/null @@ -1,12 +0,0 @@ -import os - -try: - import pypandoc - content = pypandoc.convert_file('README.md', 'rst') -except (ImportError, OSError): - with open('README.md', encoding='utf-8') as f: - content = f.read() - -with open('README.rst', 'w', encoding='utf-8') as f: - f.write(content) - diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 558c7df27..106ae8979 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -1,101 +1,88 @@ -#!/bin/sh +#!/usr/bin/env bash +set -euo pipefail -# pip -python -m pip install --upgrade pip +python -m pip install -U pip +python -m pip install -U wheel tox -# wheel -pip install wheel +is_macos() { + [[ "$(uname -s)" == "Darwin" ]] +} -# tox -pip install tox +apt_update_once() { + if [[ "${_APT_UPDATED:-0}" == "0" ]]; then + export _APT_UPDATED=1 + sudo apt-get update -y + fi +} -# update apt package lists -sudo apt-get update +apt_install() { + apt_update_once + sudo apt-get install -y --no-install-recommends "$@" +} -# optional dependencies -for par in "$@" -do - case $par in - base) - # basic setup - if [ "$(uname)" == "Darwin" ]; then - # MacOS - brew install redis - else - # Linux - sudo apt-get install redis-server - fi - ;; +install_base() { + if is_macos; then + brew install redis + else + apt_install redis-server + fi +} - R) - # R environment - if [ "$(uname)" == "Darwin" ]; then - # MacOS - brew install r - else - # Linux (Not compatible with Ubuntu 24: no CRAN repository currently available) - #wget -qO- https://cloud.r-project.org/bin/linux/ubuntu/marutter_pubkey.asc | sudo gpg --dearmor -o /usr/share/keyrings/r-project.gpg - #echo "deb [signed-by=/usr/share/keyrings/r-project.gpg] https://cloud.r-project.org/bin/linux/ubuntu jammy-cran40/" | sudo tee -a /etc/apt/sources.list.d/r-project.list - #sudo apt-get update - #sudo apt-get install libtiff5 r-base - sudo apt update - sudo apt install -y build-essential libreadline-dev libx11-dev libxt-dev libpng-dev libjpeg-dev libcairo2-dev libssl-dev libcurl4-openssl-dev libxml2-dev texinfo texlive texlive-fonts-extra screen wget - sudo apt install -y liblzma-dev +build_and_install_r_from_source() { + # Adjust if you want a different R version + local R_VER="${1:-4.4.0}" + local TAR="R-${R_VER}.tar.gz" + local URL="https://cran.r-project.org/src/base/R-4/${TAR}" -sudo apt install -y \ -build-essential \ -libreadline-dev \ -libx11-dev \ -libxt-dev \ -libpng-dev \ -libjpeg-dev \ -libcairo2-dev \ -libtiff-dev \ -libglib2.0-dev \ -liblzma-dev \ -libbz2-dev \ -libzstd-dev \ -libcurl4-openssl-dev \ -libssl-dev \ -libxml2-dev \ -texinfo \ -texlive \ -texlive-fonts-extra \ -texlive-latex-extra \ -zlib1g-dev \ -gfortran \ -libpcre2-dev \ -libicu-dev \ -libboost-all-dev + # Toolchain + common R build deps (Ubuntu 24.04-friendly) + apt_install \ + build-essential gfortran wget ca-certificates \ + libreadline-dev libx11-dev libxt-dev \ + libpng-dev libjpeg-dev libcairo2-dev libtiff-dev \ + libglib2.0-dev liblzma-dev libbz2-dev libzstd-dev zlib1g-dev \ + libcurl4-openssl-dev libssl-dev libxml2-dev \ + libpcre2-dev libicu-dev \ + texinfo texlive texlive-fonts-extra texlive-latex-extra - cd /tmp - wget https://cran.r-project.org/src/base/R-4/R-4.4.0.tar.gz - tar -xvzf R-4.4.0.tar.gz - cd R-4.4.0 - ./configure --enable-R-shlib --with-blas --with-lapack - make -j$(nproc) - sudo make install - fi - ;; + pushd /tmp >/dev/null + wget -q "${URL}" + tar -xzf "${TAR}" + cd "R-${R_VER}" + ./configure --enable-R-shlib --with-blas --with-lapack + make -j"$(nproc)" + sudo make install + popd >/dev/null +} - amici) - # AMICI dependencies - sudo apt-get install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev +install_r() { + if is_macos; then + brew install r + else + # Ubuntu-latest may not have the desired CRAN apt repo available; + # compiling R is the reliable option. + build_and_install_r_from_source "4.4.0" + fi +} - # pip install amici - pip uninstall amici pyabc - pip install 'pyabc[amici]' - ;; +install_amici() { + if ! is_macos; then + apt_install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev + fi - doc) - # documentation - sudo apt-get install pandoc - ;; + # Ensure non-interactive uninstalls in CI + python -m pip uninstall -y amici pyabc || true + python -m pip install -U "pyabc[amici]" +} - *) - echo "Unknown argument" >&2 - exit 1 - ;; +for arg in "$@"; do + case "$arg" in + base) install_base ;; + R) install_r ;; + amici) install_amici ;; + *) + echo "Unknown argument: ${arg}" >&2 + exit 1 + ;; esac done diff --git a/MANIFEST.in b/MANIFEST.in index cc9fdef33..81b45b066 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,10 @@ include README.md +include LICENSE.txt + +include pyabc/storage/alembic.ini + recursive-include pyabc/visserver/static * recursive-include pyabc/visserver/templates * -include pyabc/storage/alembic.ini -recursive-include pyabc/storage/migrations * recursive-include pyabc/visserver/assets * + +recursive-include pyabc/storage/migrations * diff --git a/pyproject.toml b/pyproject.toml index 6e3400743..ba5112fe1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,143 @@ -############################# -# Build System Requirements # -############################# [build-system] requires = [ - "setuptools >= 52.0.0", - "wheel >= 0.36.2", + "setuptools>=68.0.0", + "wheel>=0.36.2", ] build-backend = "setuptools.build_meta" +[project] +name = "pyabc" +dynamic = ["version"] + +description = "Distributed, likelihood-free ABC-SMC inference" +readme = { file = "README.md", content-type = "text/markdown" } +requires-python = ">=3.10" + +authors = [{ name = "The pyABC developers", email = "jonas.arruda@uni-bonn.de" }] +maintainers = [{ name = "Jonas Arruda", email = "jonas.arruda@uni-bonn.de" }] + +license = { text = "BSD-3-Clause" } + +keywords = [ + "likelihood-free", + "inference", + "abc", + "approximate Bayesian computation", + "sge", + "distributed", +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "numpy>=1.23.5", + "scipy>=1.5.2", + "pandas>=2.0.1", + "cloudpickle>=1.5.0", + "scikit-learn>=0.23.1", + "click>=7.1.2", + "redis>=2.10.6", + "distributed>=2022.10.2", + "matplotlib>=3.3.0", + "sqlalchemy", + "jabbar>=0.0.10", + "gitpython>=3.1.7", +] + +[project.urls] +Homepage = "https://github.com/icb-dcm/pyabc" +Download = "https://github.com/icb-dcm/pyabc/releases" +"Bug Tracker" = "https://github.com/icb-dcm/pyabc/issues" +Documentation = "https://pyabc.readthedocs.io" +Changelog = "https://github.com/ICB-DCM/pyABC/blob/main/CHANGELOG.rst" + +[project.optional-dependencies] +webserver-flask = [ + "flask-bootstrap>=3.3.7.1", + "flask>=1.1.2", + "bokeh>=3.0.1", +] +webserver-dash = [ + "dash>=2.11.1", + "dash-bootstrap-components>=1.4.2", +] +pyarrow = ["pyarrow>=6.0.0"] +R = [ + "rpy2>=3.4.4", + "cffi>=1.14.5", + "ipython>=7.18.1", + "pygments>=2.6.1", +] +julia = [ + "julia>=0.5.7", + "pygments>=2.6.1", +] +copasi = ["copasi-basico>=0.8"] +ot = ["pot>=0.7.0"] +petab = ["petab>=0.2.0"] +amici = ["amici>=0.18.0"] +yaml2sbml = ["yaml2sbml>=0.2.1"] +migrate = ["alembic>=1.5.4"] +plotly = ["plotly>=5.3.1", "kaleido>=0.2.1"] +autograd = ["autograd>=1.3"] +examples = ["notebook>=6.1.4"] +doc = [ + "sphinx>=8.1.3", + "nbsphinx>=0.9.5", + "nbconvert>=7.16.4", + "sphinx-rtd-theme>=1.2.0", + "sphinx-autodoc-typehints>=1.18.3", + "ipython>=8.4.0", +] +test = [ + "pytest>=5.4.3", + "pytest-cov>=2.10.0", + "pytest-rerunfailures>=9.1.1", +] + +[project.scripts] +abc-server-flask = "pyabc.visserver.server_flask:run_app" +abc-server-dash = "pyabc.visserver.server_dash:run_app" +abc-server = "pyabc.visserver.server_dash:run_app" +abc-redis-worker = "pyabc.sampler.redis_eps.cli:work" +abc-redis-manager = "pyabc.sampler.redis_eps.cli:manage" +abc-export = "pyabc.storage.db_export:main" +abc-migrate = "pyabc.storage.migrate:migrate" + +[tool.setuptools] +include-package-data = true +zip-safe = false +license-files = ["LICENSE.txt"] + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.dynamic] +version = { attr = "pyabc.version.__version__" } + [tool.black] line-length = 79 -target-version = ['py37', 'py38', 'py39'] +target-version = ["py310", "py311", "py312"] skip-string-normalization = true [tool.isort] profile = "black" line_length = 79 multi_line_output = 3 + +[dependency-groups] +dev = [ + "pre-commit>=4.5.1", + "tox>=4.36.0", +] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 9950b8f3e..000000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -tox >= 3.21.4 -pre-commit >= 2.10.1 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 51c81cb41..000000000 --- a/setup.cfg +++ /dev/null @@ -1,139 +0,0 @@ -########################## -# Setup.py Configuration # -########################## - -[metadata] -name = pyabc -version = attr: pyabc.version.__version__ -description = Distributed, likelihood-free ABC-SMC inference -long_description = file: README.rst -long_description_content_type = text/x-rst - -# URLs -url = https://github.com/icb-dcm/pyabc -download_url = https://github.com/icb-dcm/pyabc/releases -project_urls = - Bug Tracker = https://github.com/icb-dcm/pyabc/issues - Documentation = https://pyabc.readthedocs.io - Changelog = https://github.com/ICB-DCM/pyABC/blob/main/CHANGELOG.rst - -# Author information -author = The pyABC developers -author_email = yannik.schaelte@gmail.com -maintainer = Yannik Schaelte -maintainer_email = yannik.schaelte@gmail.com - -# License information -license = BSD-3-Clause -license_file = LICENSE.txt - -# Search tags -classifiers = - Development Status :: 5 - Production/Stable - 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 -keywords = - likelihood-free - inference - abc - approximate bayesian computation - sge - distributed - -[build-system] -requires = - wheel - setuptools - -[options] -install_requires = - numpy >= 1.23.5 - scipy >= 1.5.2 - pandas >= 2.0.1 - cloudpickle >= 1.5.0 - scikit-learn >= 0.23.1 - click >= 7.1.2 - redis >= 2.10.6 - distributed >= 2022.10.2 - matplotlib >= 3.3.0 - sqlalchemy - jabbar >= 0.0.10 - gitpython >= 3.1.7 - -python_requires = >=3.10 - -# not zip safe b/c of Flask templates -zip_safe = False -include_package_data = True - -# Where is my code -packages = find: - -[options.extras_require] -webserver-flask = - flask-bootstrap >= 3.3.7.1 - flask >= 1.1.2 - bokeh >= 3.0.1 -webserver-dash = - dash >= 2.11.1 - dash-bootstrap-components >= 1.4.2 -pyarrow = - pyarrow >= 6.0.0 -R = - rpy2 >= 3.4.4 - cffi >= 1.14.5 - ipython >= 7.18.1 - pygments >= 2.6.1 -julia = - julia >= 0.5.7 - pygments >= 2.6.1 -copasi = - copasi-basico >= 0.8 -ot = - pot >= 0.7.0 -petab = - petab >= 0.2.0 -amici = - amici >= 0.18.0 -yaml2sbml = - yaml2sbml >= 0.2.1 -migrate = - alembic >= 1.5.4 -plotly = - plotly >= 5.3.1 - kaleido >= 0.2.1 -autograd = - autograd >= 1.3 -examples = - notebook >= 6.1.4 -doc = - sphinx >= 8.1.3 - nbsphinx >= 0.9.5 - nbconvert >= 7.16.4 - sphinx-rtd-theme >= 1.2.0 - sphinx-autodoc-typehints >= 1.18.3 - ipython >= 8.4.0 -test = - pytest >= 5.4.3 - pytest-cov >= 2.10.0 - pytest-rerunfailures >= 9.1.1 -test-petab = - petabtests >= 0.0.0a6 - -[options.entry_points] -console_scripts = - abc-server-flask = pyabc.visserver.server_flask:run_app - abc-server-dash = pyabc.visserver.server_dash:run_app - abc-server = pyabc.visserver.server_dash:run_app - abc-redis-worker = pyabc.sampler.redis_eps.cli:work - abc-redis-manager = pyabc.sampler.redis_eps.cli:manage - abc-export = pyabc.storage.db_export:main - abc-migrate = pyabc.storage.migrate:migrate - -[bdist_wheel] -# Requires python 3 -universal = False diff --git a/setup.py b/setup.py deleted file mode 100644 index b908cbe55..000000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -import setuptools - -setuptools.setup() diff --git a/tox.ini b/tox.ini index dce09133f..7b752c418 100644 --- a/tox.ini +++ b/tox.ini @@ -1,39 +1,37 @@ -# tox (https://tox.readthedocs.io/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. -# See https://tox.readthedocs.io/en/latest/config.html for reference. - [tox] - -# Environments run by default and in this order -# unless specified via CLI -eENVLIST -# or environment variable TOXENV +requires = tox>=4 envlist = clean - # tests base visualization - external + external-R + external-other-simulators petab + migrate + notebooks1 + notebooks2 mac - # quality project flake8 doc - -# Base-environment +skip_missing_interpreters = true [testenv] - -# Sub-environments -# inherit settings defined in the base +description = {envname} +package = wheel +passenv = + HOME + CI + GITHUB_* + TOXENV + LD_LIBRARY_PATH +setenv = + PYTHONWARNINGS = default [testenv:clean] skip_install = true -allowlist_externals = - rm deps = coverage +allowlist_externals = rm commands = coverage erase rm -rf .coverage* @@ -42,18 +40,21 @@ commands = description = Clean up before tests -# Unit tests - [testenv:base] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib -extras = test,R,pyarrow,autograd -passenv = HOME +extras = + test + R + pyarrow + autograd +deps = commands = + python -m pip install -U pip # needed by pot - pip install cython - pip install pot - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pip install cython + python -m pip install pot + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base test_performance -s description = Test basic functionality @@ -61,9 +62,13 @@ description = [testenv:visualization] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib -extras = test,plotly,webserver_dash,webserver_flask +extras = + test + plotly + webserver-dash + webserver-flask commands = - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/visualization description = Test visualization @@ -71,34 +76,38 @@ description = [testenv:external-R] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib -extras = test,R +extras = + test + R commands = - # General - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/external/test_external.py -s - # R - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/external/test_rpy2.py -s description = - Test external model simulators + Test external model simulators (incl. R) [testenv:external-other-simulators] -extras = test,julia,copasi +extras = + test + julia + copasi commands = - # Julia python -c "import julia; julia.install()" python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/external/test_pyjulia.py -s - # Copasi - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/copasi -s description = - Test external model simulators + Test external model simulators (Julia, COPASI) [testenv:petab] -extras = test,petab,amici,test_petab +extras = + test + petab + amici + test-petab commands = - # Petab python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s description = @@ -107,25 +116,28 @@ description = [testenv:mac] extras = test commands = - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base/test_macos.py -s description = - Test basic macOS support (run there) + Test basic macOS support (run on macOS) [testenv:migrate] -extras = test,migrate +extras = + test + migrate deps = setuptools>=65.5.0 wheel pytest-console-scripts commands = - pip install setuptools>=65.5.0 wheel # to ensure distutils is there in python 3.12 + python -m pip install -U pip + python -m pip install "setuptools>=65.5.0" wheel # ensure distutils shim is available where needed # install an old pyabc version - pip install pyabc==0.10.13 numpy==1.23.5 pandas==1.5.0 sqlalchemy==1.4.48 + python -m pip install pyabc==0.10.13 numpy==1.23.5 pandas==1.5.0 sqlalchemy==1.4.48 python test/migrate/create_test_db.py - # back to latest pyabc version - pip install --upgrade . - pytest --cov=pyabc --cov-report=xml --cov-append \ + # back to latest (this repo) + python -m pip install --upgrade . + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/migrate -s description = Test database migration @@ -134,24 +146,29 @@ description = allowlist_externals = bash extras = examples commands = + python -m pip install -U pip # needed by pot - pip install cython - pip install pot + python -m pip install cython + python -m pip install pot bash test/run_notebooks.sh 1 description = - Run notebooks + Run notebooks (set 1) [testenv:notebooks2] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib allowlist_externals = bash -extras = examples,R,petab,yaml2sbml,amici,autograd +extras = + examples + R + petab + yaml2sbml + amici + autograd commands = bash test/run_notebooks.sh 2 description = - Run notebooks - -# Style, management, docs + Run notebooks (set 2) [testenv:project] skip_install = true @@ -162,31 +179,31 @@ commands = pyroma --min=10 . rst-lint README.rst description = - Check the package friendliness + Check package metadata / README quality [testenv:flake8] skip_install = true deps = - black >= 22.3.0 - flake8 >= 3.8.3 - flake8-bandit >= 4.1.1 - flake8-bugbear >= 22.8.23 - flake8-colors >= 0.1.6 - #flake8-commas >= 2.0.0 - flake8-comprehensions >= 3.2.3 - flake8-print >= 5.0.0 - flake8-black >= 0.2.3 - flake8-isort >= 4.0.0 - # flake8-docstrings >= 1.5.0 + black>=22.3.0 + flake8>=3.8.3 + flake8-bandit>=4.1.1 + flake8-bugbear>=22.8.23 + flake8-colors>=0.1.6 + flake8-comprehensions>=3.2.3 + flake8-print>=5.0.0 + flake8-black>=0.2.3 + flake8-isort>=4.0.0 commands = - flake8 pyabc test test_performance setup.py + flake8 pyabc test test_performance description = - Run flake8 with various plugins + Run flake8 (+ plugins) [testenv:doc] extras = - doc,petab,plotly + doc + petab + plotly commands = sphinx-build -W -b html doc/ doc/_build/html description = - Test whether docs build passes + Build docs (warnings as errors) From 970cbe2f63e5edcb0fa106af9d92c6a50bc8f9eb Mon Sep 17 00:00:00 2001 From: arrjon Date: Mon, 16 Feb 2026 22:08:06 +0100 Subject: [PATCH 02/55] fix doc --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6588cc32..f979f179e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -237,10 +237,8 @@ jobs: - name: Install dependencies run: | - .github/workflows/install_deps.sh doc python -m pip install -U pip python -m pip install tox - python .github/workflows/generate_readme_rst.py - name: Run quality checks timeout-minutes: 10 From b843df0e2e1f158f5c56cbe431a542ac1f604111 Mon Sep 17 00:00:00 2001 From: arrjon Date: Mon, 16 Feb 2026 22:11:33 +0100 Subject: [PATCH 03/55] fix doc --- tox.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 7b752c418..a0753b4db 100644 --- a/tox.ini +++ b/tox.ini @@ -174,12 +174,10 @@ description = skip_install = true deps = pyroma - restructuredtext-lint commands = pyroma --min=10 . - rst-lint README.rst description = - Check package metadata / README quality + Check the package friendliness [testenv:flake8] skip_install = true From c14a8bb25e3e6254d5869e9bc6df2dda92ee9855 Mon Sep 17 00:00:00 2001 From: arrjon Date: Mon, 16 Feb 2026 22:25:18 +0100 Subject: [PATCH 04/55] fix deps --- .github/workflows/ci.yml | 1 + .github/workflows/install_deps.sh | 9 +++++++++ pyabc/visualization/credible.py | 4 ++-- tox.ini | 13 ++++++++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f979f179e..4618d7c03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -237,6 +237,7 @@ jobs: - name: Install dependencies run: | + .github/workflows/install_deps.sh doc python -m pip install -U pip python -m pip install tox diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 106ae8979..3a64dc49b 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -74,12 +74,21 @@ install_amici() { python -m pip install -U "pyabc[amici]" } +install_doc_tools() { + if [[ "$(uname -s)" == "Darwin" ]]; then + brew install pandoc || true + else + sudo apt-get update -y + sudo apt-get install -y --no-install-recommends pandoc + fi +} for arg in "$@"; do case "$arg" in base) install_base ;; R) install_r ;; amici) install_amici ;; + doc) install_doc_tools ;; *) echo "Unknown argument: ${arg}" >&2 exit 1 diff --git a/pyabc/visualization/credible.py b/pyabc/visualization/credible.py index cb9414838..2d41a92b8 100644 --- a/pyabc/visualization/credible.py +++ b/pyabc/visualization/credible.py @@ -57,7 +57,7 @@ def _prepare_credible_intervals( for i_t, t in enumerate(ts): df, w = history.get_distribution(m=m, t=t) # normalize weights to be sure - w /= w.sum() + w = w / w.sum() # fit kde if show_kde_max: _kde_max_pnt = compute_kde_max(kde, df, w) @@ -470,7 +470,7 @@ def plot_credible_intervals_for_time( for i_run, (h, t, m) in enumerate(zip(histories, ts, ms)): df, w = h.get_distribution(m=m, t=t) # normalize weights to be sure - w /= w.sum() + w = w / w.sum() # fit kde if show_kde_max: _kde_max_pnt = compute_kde_max(kde, df, w) diff --git a/tox.ini b/tox.ini index a0753b4db..4be4a2ba7 100644 --- a/tox.ini +++ b/tox.ini @@ -106,13 +106,24 @@ extras = test petab amici - test-petab commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s description = Test PEtab support +[testenv:petabtests] +extras = + test + petab + amici +deps = + petabtests>=0.0.1 +commands = + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ + test/petab -s + + [testenv:mac] extras = test commands = From 84211910883b88d95f48312e4b4e77120fe57451 Mon Sep 17 00:00:00 2001 From: arrjon Date: Mon, 16 Feb 2026 23:13:49 +0100 Subject: [PATCH 05/55] fix deps --- .github/workflows/ci.yml | 36 +---- .github/workflows/install_deps.sh | 215 ++++++++++++++++++++++++++---- .pre-commit-config.yaml | 117 +++++++++------- pyproject.toml | 108 +++++++++++++-- tox.ini | 88 ++++++++---- 5 files changed, 418 insertions(+), 146 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4618d7c03..d140cc9ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,33 +44,6 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml - # Early warning for upcoming Python (allowed to fail) - future-python: - runs-on: ubuntu-latest - continue-on-error: true - strategy: - fail-fast: false - matrix: - python-version: ["3.14-dev"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - allow-prereleases: true - cache: pip - - - name: Install dependencies - run: .github/workflows/install_deps.sh base R - - - name: Run tests (base) - timeout-minutes: 20 - run: tox -e base - external: runs-on: ubuntu-latest strategy: @@ -89,9 +62,9 @@ jobs: cache: pip - name: Install Julia - uses: julia-actions/setup-julia@v1 + uses: julia-actions/setup-julia@v2 with: - version: "1.7" + version: "1.11" - name: Install dependencies run: .github/workflows/install_deps.sh base R @@ -241,6 +214,11 @@ jobs: python -m pip install -U pip python -m pip install tox + - name: Preview Ruff linting + timeout-minutes: 5 + run: tox -e lint + continue-on-error: true + - name: Run quality checks timeout-minutes: 10 run: tox -e project,flake8 diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 3a64dc49b..f8bc28f8d 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -1,40 +1,73 @@ #!/usr/bin/env bash +# CI dependency installation script for pyABC +# Supports Ubuntu 24.04 and macOS set -euo pipefail -python -m pip install -U pip -python -m pip install -U wheel tox +# Color output helpers +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' # No Color +log_info() { + echo -e "${GREEN}[INFO]${NC} $*" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $*" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $*" +} + +# Update pip, wheel, and tox +log_info "Updating pip, wheel, and tox..." +python -m pip install --upgrade pip wheel tox + +# Platform detection is_macos() { [[ "$(uname -s)" == "Darwin" ]] } +# APT management (Ubuntu/Debian) +_APT_UPDATED=0 + apt_update_once() { - if [[ "${_APT_UPDATED:-0}" == "0" ]]; then + if [[ "${_APT_UPDATED}" == "0" ]]; then export _APT_UPDATED=1 + log_info "Updating apt package lists..." sudo apt-get update -y fi } apt_install() { apt_update_once + log_info "Installing apt packages: $*" sudo apt-get install -y --no-install-recommends "$@" } +# Base dependencies (Redis) install_base() { + log_info "Installing base dependencies..." if is_macos; then brew install redis else apt_install redis-server + # Ensure redis-server is running + sudo service redis-server start || true fi } +# R installation from source build_and_install_r_from_source() { - # Adjust if you want a different R version - local R_VER="${1:-4.4.0}" + local R_VER="${1:-4.4.2}" local TAR="R-${R_VER}.tar.gz" local URL="https://cran.r-project.org/src/base/R-4/${TAR}" - # Toolchain + common R build deps (Ubuntu 24.04-friendly) + log_info "Building R ${R_VER} from source..." + + # Build dependencies for Ubuntu 24.04 apt_install \ build-essential gfortran wget ca-certificates \ libreadline-dev libx11-dev libxt-dev \ @@ -45,53 +78,177 @@ build_and_install_r_from_source() { texinfo texlive texlive-fonts-extra texlive-latex-extra pushd /tmp >/dev/null - wget -q "${URL}" + + log_info "Downloading R source..." + wget -q "${URL}" -O "${TAR}" + + log_info "Extracting R source..." tar -xzf "${TAR}" cd "R-${R_VER}" - ./configure --enable-R-shlib --with-blas --with-lapack + + log_info "Configuring R build..." + ./configure --enable-R-shlib --with-blas --with-lapack --prefix=/usr/local + + log_info "Compiling R (this may take several minutes)..." make -j"$(nproc)" + + log_info "Installing R..." sudo make install + + # Verify installation + if command -v R >/dev/null 2>&1; then + log_info "R successfully installed: $(R --version | head -n1)" + else + log_error "R installation verification failed" + popd >/dev/null + return 1 + fi + popd >/dev/null } +# R installation install_r() { + log_info "Installing R..." if is_macos; then brew install r else - # Ubuntu-latest may not have the desired CRAN apt repo available; - # compiling R is the reliable option. - build_and_install_r_from_source "4.4.0" + # Ubuntu: compile from source for better compatibility + build_and_install_r_from_source "4.4.2" + fi + + # Set LD_LIBRARY_PATH for R shared libraries + if ! is_macos; then + export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-/usr/lib}:/usr/local/lib/R/lib" + echo "export LD_LIBRARY_PATH=\"${LD_LIBRARY_PATH}\"" >> ~/.bashrc fi } +# AMICI dependencies install_amici() { + log_info "Installing AMICI dependencies..." + if ! is_macos; then - apt_install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev + apt_install \ + swig \ + libatlas-base-dev \ + libhdf5-serial-dev \ + libboost-all-dev fi - # Ensure non-interactive uninstalls in CI + log_info "Installing AMICI Python package..." + # Clean install to avoid version conflicts python -m pip uninstall -y amici pyabc || true - python -m pip install -U "pyabc[amici]" + python -m pip install --upgrade "pyabc[amici]" } +# Documentation tools install_doc_tools() { - if [[ "$(uname -s)" == "Darwin" ]]; then + log_info "Installing documentation tools..." + + if is_macos; then brew install pandoc || true else - sudo apt-get update -y - sudo apt-get install -y --no-install-recommends pandoc + apt_update_once + apt_install pandoc + fi +} + +# Julia installation +install_julia() { + log_info "Installing Julia..." + + if is_macos; then + brew install julia + else + # Install Julia via juliaup (recommended approach) + curl -fsSL https://install.julialang.org | sh -s -- -y + export PATH="$HOME/.juliaup/bin:$PATH" + fi + + # Initialize PyJulia + python -c "import julia; julia.install()" || log_warn "PyJulia initialization failed (non-critical)" +} + +# Development tools +install_dev_tools() { + log_info "Installing development tools..." + + python -m pip install --upgrade \ + pre-commit \ + ruff \ + build \ + twine \ + pytest \ + pytest-cov \ + pytest-xdist +} + +# All dependencies +install_all() { + log_info "Installing all dependencies..." + install_base + install_r + install_amici + install_doc_tools + install_julia + install_dev_tools +} + +# Display usage +usage() { + cat <&2 - exit 1 - ;; - esac -done +main "$@" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14d60c35b..b2ef19fd7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,51 +10,72 @@ # `pre-commit run --all-files` as by default only changed files are checked repos: -- repo: https://github.com/psf/black - rev: 23.7.0 - hooks: - - id: black - description: The uncompromising code formatter -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: isort (python) - - id: isort - name: isort (cython) - types: [cython] - - id: isort - name: isort (pyi) - types: [pyi] -- repo: https://github.com/nbQA-dev/nbQA - rev: 1.7.0 - hooks: - - id: nbqa-black - - id: nbqa-pyupgrade - args: [--py36-plus] - - id: nbqa-isort -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 - hooks: - - id: check-yaml - description: Check yaml files for parseable syntax - - id: check-added-large-files - description: Prevent large files from being committed - - id: check-merge-conflict - description: Check for files that contain merge conflict strings - - id: check-symlinks - description: Check for symlinks which do not point to anything - - id: trailing-whitespace - description: Trim trailing whitespaces - - id: end-of-file-fixer - description: Fix empty lines at ends of files - - id: detect-private-key - description: Detects the presence of private keys -- repo: local - hooks: - - id: style - name: Check style - description: Check style - entry: tox -e flake8 -- - language: system - types: [python] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 + hooks: + # Run the linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + description: Fast Python linter (replaces flake8 + plugins) + # Run the formatter + - id: ruff-format + description: Fast Python formatter (replaces black + isort) + + # Notebook formatting with nbQA + ruff + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.9.1 + hooks: + - id: nbqa-ruff + args: [--fix, --exit-non-zero-on-fix] + - id: nbqa-pyupgrade + args: [--py310-plus] + + # Standard pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-yaml + description: Check yaml files for parseable syntax + - id: check-toml + description: Check toml files for parseable syntax + - id: check-added-large-files + description: Prevent large files from being committed + args: ['--maxkb=1000'] + - id: check-merge-conflict + description: Check for files that contain merge conflict strings + - id: check-symlinks + description: Check for symlinks which do not point to anything + - id: trailing-whitespace + description: Trim trailing whitespaces + exclude: ^test/ + - id: end-of-file-fixer + description: Fix empty lines at ends of files + exclude: ^test/ + - id: detect-private-key + description: Detects the presence of private keys + - id: check-case-conflict + description: Check for files with case-insensitive name conflicts + - id: mixed-line-ending + description: Ensure consistent line endings + args: ['--fix=lf'] + +# Configuration +default_language_version: + python: python3.11 + +# Global exclusions +exclude: | + (?x)^( + \.git/| + \.tox/| + \.venv/| + venv/| + build/| + dist/| + .*\.egg-info/| + __pycache__/| + \.pytest_cache/| + \.mypy_cache/| + \.ruff_cache/| + doc/_build/ + ) diff --git a/pyproject.toml b/pyproject.toml index ba5112fe1..f4f06fb2b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,3 @@ - [build-system] requires = [ "setuptools>=68.0.0", @@ -50,7 +49,7 @@ dependencies = [ "redis>=2.10.6", "distributed>=2022.10.2", "matplotlib>=3.3.0", - "sqlalchemy", + "sqlalchemy>=2.0.0", "jabbar>=0.0.10", "gitpython>=3.1.7", ] @@ -61,6 +60,8 @@ Download = "https://github.com/icb-dcm/pyabc/releases" "Bug Tracker" = "https://github.com/icb-dcm/pyabc/issues" Documentation = "https://pyabc.readthedocs.io" Changelog = "https://github.com/ICB-DCM/pyABC/blob/main/CHANGELOG.rst" +Repository = "https://github.com/icb-dcm/pyabc" +"Source Code" = "https://github.com/icb-dcm/pyabc" [project.optional-dependencies] webserver-flask = [ @@ -96,14 +97,24 @@ doc = [ "sphinx>=8.1.3", "nbsphinx>=0.9.5", "nbconvert>=7.16.4", - "sphinx-rtd-theme>=1.2.0", - "sphinx-autodoc-typehints>=1.18.3", + "sphinx-rtd-theme>=2.0.0", + "sphinx-autodoc-typehints>=2.0.0", "ipython>=8.4.0", + "sphinx-autobuild>=2021.3.14", ] test = [ - "pytest>=5.4.3", - "pytest-cov>=2.10.0", - "pytest-rerunfailures>=9.1.1", + "pytest>=8.0.0", + "pytest-cov>=6.0.0", + "pytest-rerunfailures>=14.0.0", + "pytest-xdist>=3.5.0", + "coverage[toml]>=7.0.0", +] +dev = [ + "pre-commit>=4.0.0", + "tox>=4.0.0", + "ruff>=0.8.0", + "build>=1.0.0", + "twine>=4.0.0", ] [project.scripts] @@ -126,18 +137,87 @@ where = ["."] [tool.setuptools.dynamic] version = { attr = "pyabc.version.__version__" } -[tool.black] +[tool.ruff] line-length = 79 -target-version = ["py310", "py311", "py312"] -skip-string-normalization = true +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "ARG", # flake8-unused-arguments + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "B905", # zip() without an explicit strict= parameter +] -[tool.isort] -profile = "black" -line_length = 79 -multi_line_output = 3 +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # unused imports +"test/**" = ["ARG", "SIM"] # Allow unused arguments and complex logic in tests + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.pytest.ini_options] +minversion = "8.0" +addopts = [ + "-ra", + "--strict-markers", + "--strict-config", + "--showlocals", +] +testpaths = ["test", "test_performance"] +pythonpath = ["."] +filterwarnings = [ + "error", + "ignore::DeprecationWarning", + "ignore::PendingDeprecationWarning", +] + +[tool.coverage.run] +source = ["pyabc"] +parallel = true +branch = true + +[tool.coverage.report] +precision = 2 +show_missing = true +skip_covered = false +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + +[tool.coverage.paths] +source = [ + "pyabc", +] [dependency-groups] dev = [ "pre-commit>=4.5.1", "tox>=4.36.0", + "ruff>=0.8.0", + "build>=1.0.0", + "twine>=4.0.0", + "pytest>=8.0.0", + "pytest-cov>=6.0.0", + "pytest-xdist>=3.5.0", ] diff --git a/tox.ini b/tox.ini index 4be4a2ba7..4645eb307 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ envlist = notebooks2 mac project - flake8 + lint doc skip_missing_interpreters = true @@ -30,13 +30,15 @@ setenv = [testenv:clean] skip_install = true -deps = coverage +deps = coverage[toml]>=7.0 allowlist_externals = rm commands = coverage erase rm -rf .coverage* rm -rf coverage.xml rm -rf dask-worker-space + rm -rf .pytest_cache + rm -rf .tox description = Clean up before tests @@ -48,11 +50,9 @@ extras = R pyarrow autograd -deps = commands = - python -m pip install -U pip - # needed by pot - python -m pip install cython + python -m pip install --upgrade pip + python -m pip install cython numpy python -m pip install pot python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base test_performance -s @@ -122,7 +122,8 @@ deps = commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s - +description = + Test PEtab test suite [testenv:mac] extras = test @@ -137,12 +138,12 @@ extras = test migrate deps = - setuptools>=65.5.0 - wheel - pytest-console-scripts + setuptools>=68.0.0 + wheel>=0.36.2 + pytest-console-scripts>=1.4.1 commands = - python -m pip install -U pip - python -m pip install "setuptools>=65.5.0" wheel # ensure distutils shim is available where needed + python -m pip install --upgrade pip + python -m pip install "setuptools>=68.0.0" "wheel>=0.36.2" # install an old pyabc version python -m pip install pyabc==0.10.13 numpy==1.23.5 pandas==1.5.0 sqlalchemy==1.4.48 python test/migrate/create_test_db.py @@ -157,9 +158,8 @@ description = allowlist_externals = bash extras = examples commands = - python -m pip install -U pip - # needed by pot - python -m pip install cython + python -m pip install --upgrade pip + python -m pip install cython numpy python -m pip install pot bash test/run_notebooks.sh 1 description = @@ -184,28 +184,52 @@ description = [testenv:project] skip_install = true deps = - pyroma + pyroma>=4.0 + build>=1.0.0 + twine>=4.0.0 commands = pyroma --min=10 . + python -m build + twine check dist/* description = - Check the package friendliness + Check the package quality and metadata + +[testenv:lint] +skip_install = true +deps = + ruff>=0.8.0 +commands = + ruff check pyabc test test_performance + ruff format --check pyabc test test_performance +description = + Run linting with ruff [testenv:flake8] skip_install = true deps = - black>=22.3.0 - flake8>=3.8.3 + black>=24.0.0 + flake8>=7.0.0 flake8-bandit>=4.1.1 - flake8-bugbear>=22.8.23 - flake8-colors>=0.1.6 - flake8-comprehensions>=3.2.3 + flake8-bugbear>=24.0.0 + flake8-colors>=0.1.9 + flake8-comprehensions>=3.14.0 flake8-print>=5.0.0 - flake8-black>=0.2.3 - flake8-isort>=4.0.0 + flake8-black>=0.3.6 + flake8-isort>=6.1.0 commands = flake8 pyabc test test_performance description = - Run flake8 (+ plugins) + Run flake8 (+ plugins) - legacy linting + +[testenv:format] +skip_install = true +deps = + ruff>=0.8.0 +commands = + ruff check --fix pyabc test test_performance + ruff format pyabc test test_performance +description = + Auto-format code with ruff [testenv:doc] extras = @@ -215,4 +239,16 @@ extras = commands = sphinx-build -W -b html doc/ doc/_build/html description = - Build docs (warnings as errors) + Build documentation (warnings as errors) + +[testenv:doc-live] +extras = + doc + petab + plotly +deps = + sphinx-autobuild>=2021.3.14 +commands = + sphinx-autobuild -W -b html doc/ doc/_build/html --open-browser +description = + Build documentation with live reload From 415e913b8177b4bac56e6744e00549cde5e44770 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 13:40:04 +0100 Subject: [PATCH 06/55] add ruff --- doc/conf.py | 13 +- doc/examples/adaptive_distances.ipynb | 62 ++- doc/examples/aggregated_distances.ipynb | 31 +- doc/examples/chemical_reaction.ipynb | 52 +-- doc/examples/conversion_reaction.ipynb | 40 +- doc/examples/early_stopping.ipynb | 21 +- doc/examples/informative.ipynb | 212 +++++----- doc/examples/look_ahead.ipynb | 48 +-- doc/examples/petab_application.ipynb | 8 +- doc/examples/petab_yaml2sbml.ipynb | 16 +- doc/examples/using_copasi.ipynb | 32 +- doc/examples/using_julia.ipynb | 30 +- pyabc/acceptor/acceptor.py | 22 +- pyabc/acceptor/pdf_norm.py | 18 +- pyabc/copasi/model.py | 47 +-- pyabc/distance/aggregate.py | 32 +- pyabc/distance/base.py | 42 +- pyabc/distance/distance.py | 34 +- pyabc/distance/kernel.py | 61 +-- pyabc/distance/ot.py | 46 +- pyabc/distance/pnorm.py | 142 ++++--- pyabc/distance/scale.py | 70 ++-- pyabc/distance/util.py | 14 +- pyabc/epsilon/base.py | 16 +- pyabc/epsilon/epsilon.py | 46 +- pyabc/epsilon/silk.py | 38 +- pyabc/epsilon/temperature.py | 125 +++--- pyabc/external/base.py | 51 ++- pyabc/external/julia/jl_pyjulia.py | 33 +- pyabc/inference/smc.py | 71 ++-- pyabc/inference_util/inference_util.py | 61 ++- pyabc/model/model.py | 22 +- pyabc/petab/amici.py | 39 +- pyabc/petab/base.py | 56 ++- pyabc/population/population.py | 40 +- .../populationstrategy/populationstrategy.py | 34 +- pyabc/predictor/predictor.py | 120 +++--- pyabc/random_choice/random_choice.py | 2 +- pyabc/random_variables/random_variables.py | 52 +-- pyabc/sampler/base.py | 8 +- pyabc/sampler/eps_mixin.py | 13 +- pyabc/sampler/multicore.py | 12 +- pyabc/sampler/multicorebase.py | 11 +- pyabc/sampler/redis_eps/cli.py | 84 ++-- pyabc/sampler/redis_eps/sampler.py | 36 +- pyabc/sampler/redis_eps/sampler_static.py | 18 +- pyabc/settings/settings.py | 8 +- pyabc/sge/db.py | 41 +- pyabc/sge/execution_contexts.py | 10 +- pyabc/sge/test_sge.py | 14 +- pyabc/storage/db_export.py | 75 ++-- pyabc/storage/db_model.py | 50 +-- pyabc/storage/history.py | 233 +++++------ pyabc/storage/json.py | 2 +- pyabc/sumstat/base.py | 36 +- pyabc/sumstat/learn.py | 22 +- pyabc/sumstat/subset.py | 21 +- pyabc/transition/base.py | 19 +- pyabc/transition/jump.py | 18 +- pyabc/transition/local_transition.py | 20 +- pyabc/transition/model.py | 4 +- pyabc/transition/multivariatenormal.py | 16 +- pyabc/transition/predict_population_size.py | 8 +- pyabc/transition/randomwalk.py | 12 +- pyabc/transition/transitionmeta.py | 8 +- pyabc/util/dict2arr.py | 23 +- pyabc/util/event_ixs.py | 31 +- pyabc/util/par_trafo.py | 21 +- pyabc/util/read_sample.py | 4 +- pyabc/visserver/server_dash.py | 392 +++++++++--------- pyabc/visserver/server_flask.py | 97 +++-- pyabc/visualization/credible.py | 80 ++-- pyabc/visualization/data.py | 41 +- pyabc/visualization/distance.py | 26 +- pyabc/visualization/effective_sample_size.py | 40 +- pyabc/visualization/epsilon.py | 36 +- pyabc/visualization/sample.py | 192 ++++----- pyabc/visualization/sankey.py | 58 +-- pyabc/visualization/walltime.py | 164 ++++---- pyproject.toml | 3 +- test/base/test_populationstrategy.py | 15 +- test/base/test_storage.py | 196 ++++----- .../test_abc_smc_algorithm.py | 173 ++++---- test_performance/test_samplerperf.py | 24 +- 84 files changed, 2101 insertions(+), 2213 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 95b590a90..43872dc93 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # Network Analysis documentation build configuration file, created by # sphinx-quickstart on Wed Mar 23 08:55:36 2016. @@ -71,7 +70,7 @@ 'pandas': ('https://pandas.pydata.org/pandas-docs/dev', None), 'petab': ('https://petab.readthedocs.io/en/stable/', None), 'amici': ('https://amici.readthedocs.io/en/latest/', None), - "sklearn": ("https://scikit-learn.org/stable/", None), + 'sklearn': ('https://scikit-learn.org/stable/', None), } @@ -100,7 +99,7 @@ # # The short X.Y version. -import pyabc +import pyabc # noqa: E402 version = pyabc.__version__ # The full version, including alpha/beta/rc tags. @@ -111,7 +110,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = "en" +language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -142,7 +141,7 @@ pygments_style = 'sphinx' # added by emmanuel -highlight_language = "python3" +highlight_language = 'python3' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -183,10 +182,10 @@ # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -html_title = "pyABC documentation" +html_title = 'pyABC documentation' # A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = "pyABC" +html_short_title = 'pyABC' # The name of an image file (relative to this directory) to place at the top # of the sidebar. diff --git a/doc/examples/adaptive_distances.ipynb b/doc/examples/adaptive_distances.ipynb index ac59ef837..2fb2d1984 100644 --- a/doc/examples/adaptive_distances.ipynb +++ b/doc/examples/adaptive_distances.ipynb @@ -104,19 +104,19 @@ "metadata": {}, "outputs": [], "source": [ - "pyabc.settings.set_figure_params(\"pyabc\") # for beautified plots\n", + "pyabc.settings.set_figure_params('pyabc') # for beautified plots\n", "\n", "\n", "# for debugging\n", - "df_logger = logging.getLogger(\"ABC.Distance\")\n", + "df_logger = logging.getLogger('ABC.Distance')\n", "df_logger.setLevel(logging.DEBUG)\n", "\n", "\n", "# model definition\n", "def model(p):\n", " return {\n", - " \"s1\": p[\"theta\"] + 1 + 0.1 * np.random.normal(),\n", - " \"s2\": 2 + 10 * np.random.normal(),\n", + " 's1': p['theta'] + 1 + 0.1 * np.random.normal(),\n", + " 's2': 2 + 10 * np.random.normal(),\n", " }\n", "\n", "\n", @@ -124,13 +124,13 @@ "theta_true = 3\n", "\n", "# observed summary statistics\n", - "observation = {\"s1\": theta_true + 1, \"s2\": 2}\n", + "observation = {'s1': theta_true + 1, 's2': 2}\n", "\n", "# prior distribution\n", - "prior = pyabc.Distribution(theta=pyabc.RV(\"uniform\", 0, 10))\n", + "prior = pyabc.Distribution(theta=pyabc.RV('uniform', 0, 10))\n", "\n", "# database\n", - "db_path = pyabc.create_sqlite_db_id(file_=\"adaptive_distance.db\")\n", + "db_path = pyabc.create_sqlite_db_id(file_='adaptive_distance.db')\n", "\n", "\n", "def plot_history(history):\n", @@ -143,11 +143,11 @@ " w,\n", " xmin=0,\n", " xmax=10,\n", - " x=\"theta\",\n", + " x='theta',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", - " refval={\"theta\": theta_true},\n", - " refval_color=\"grey\",\n", + " label=f'PDF t={t}',\n", + " refval={'theta': theta_true},\n", + " refval_color='grey',\n", " )\n", " ax.legend()" ] @@ -282,7 +282,7 @@ } ], "source": [ - "scale_log_file = tempfile.mkstemp(suffix=\".json\")[1]\n", + "scale_log_file = tempfile.mkstemp(suffix='.json')[1]\n", "\n", "distance_adaptive = pyabc.AdaptivePNormDistance(\n", " p=2,\n", @@ -336,7 +336,7 @@ ], "source": [ "histories = [h_uni, h_ada]\n", - "labels = [\"Uniform weights\", \"Adaptive weights\"]\n", + "labels = ['Uniform weights', 'Adaptive weights']\n", "pyabc.visualization.plot_sample_numbers(histories, labels)" ] }, @@ -416,7 +416,7 @@ ], "source": [ "ts = list(range(h_ada.max_t + 1))\n", - "labels = [f\"t={t}\" for t in ts]\n", + "labels = [f't={t}' for t in ts]\n", "\n", "pyabc.visualization.plot_distance_weights(\n", " log_files=scale_log_file,\n", @@ -459,25 +459,21 @@ "outputs": [], "source": [ "import logging\n", - "import os\n", "import tempfile\n", "\n", - "import matplotlib.pyplot as pyplot\n", - "import numpy as np\n", - "\n", "import pyabc.visualization\n", "\n", "# for debugging\n", - "df_logger = logging.getLogger(\"Distance\")\n", + "df_logger = logging.getLogger('Distance')\n", "df_logger.setLevel(logging.DEBUG)\n", "\n", "\n", "# model definition\n", "def model(p):\n", " return {\n", - " \"s1\": p[\"theta\"] + 1 + 0.1 * np.random.normal(),\n", - " \"s2\": 2 + 10 * np.random.normal(),\n", - " \"s3\": 2 + 0.1 * np.random.normal(),\n", + " 's1': p['theta'] + 1 + 0.1 * np.random.normal(),\n", + " 's2': 2 + 10 * np.random.normal(),\n", + " 's3': 2 + 0.1 * np.random.normal(),\n", " }\n", "\n", "\n", @@ -485,13 +481,13 @@ "theta_true = 3\n", "\n", "# observed summary statistics\n", - "observation = {\"s1\": theta_true + 1, \"s2\": 2, \"s3\": 5}\n", + "observation = {'s1': theta_true + 1, 's2': 2, 's3': 5}\n", "\n", "# prior distribution\n", - "prior = pyabc.Distribution(theta=pyabc.RV(\"uniform\", 0, 10))\n", + "prior = pyabc.Distribution(theta=pyabc.RV('uniform', 0, 10))\n", "\n", "# database\n", - "db_path = pyabc.create_sqlite_db_id(file_=\"adaptive_distance.db\")\n", + "db_path = pyabc.create_sqlite_db_id(file_='adaptive_distance.db')\n", "\n", "\n", "def plot_history(history):\n", @@ -504,11 +500,11 @@ " w,\n", " xmin=0,\n", " xmax=10,\n", - " x=\"theta\",\n", + " x='theta',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", - " refval={\"theta\": theta_true},\n", - " refval_color=\"grey\",\n", + " label=f'PDF t={t}',\n", + " refval={'theta': theta_true},\n", + " refval_color='grey',\n", " )\n", " ax.legend()" ] @@ -712,7 +708,7 @@ } ], "source": [ - "scale_log_file_l1 = tempfile.mkstemp(suffix=\".json\")[1]\n", + "scale_log_file_l1 = tempfile.mkstemp(suffix='.json')[1]\n", "\n", "distance_adaptive = pyabc.AdaptivePNormDistance(\n", " p=1, # new, previously p=2\n", @@ -801,7 +797,7 @@ } ], "source": [ - "scale_log_file_pcmad = tempfile.mkstemp(suffix=\".json\")[1]\n", + "scale_log_file_pcmad = tempfile.mkstemp(suffix='.json')[1]\n", "\n", "distance_adaptive = pyabc.AdaptivePNormDistance(\n", " p=1,\n", @@ -859,7 +855,7 @@ "source": [ "pyabc.visualization.plot_distance_weights(\n", " [scale_log_file_l1, scale_log_file_pcmad],\n", - " labels=[\"L1+MAD\", \"L1+PCMAD\"],\n", + " labels=['L1+MAD', 'L1+PCMAD'],\n", ")" ] }, @@ -901,7 +897,7 @@ "source": [ "pyabc.visualization.plot_sample_numbers(\n", " [h_uni, h_ada, h_l1, h_pcmad],\n", - " [\"Uniform+L2\", \"Ada.+L2\", \"Ada.+L1\", \"Ada.+L1+PCMAD\"],\n", + " ['Uniform+L2', 'Ada.+L2', 'Ada.+L1', 'Ada.+L1+PCMAD'],\n", ")" ] }, diff --git a/doc/examples/aggregated_distances.ipynb b/doc/examples/aggregated_distances.ipynb index 77de70063..3ed80c8ab 100644 --- a/doc/examples/aggregated_distances.ipynb +++ b/doc/examples/aggregated_distances.ipynb @@ -49,7 +49,6 @@ "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "from scipy import stats\n", "\n", "import pyabc\n", "\n", @@ -76,7 +75,7 @@ "\n", "# prior\n", "prior = pyabc.Distribution(\n", - " p0=pyabc.RV(\"uniform\", -1, 2), p1=pyabc.RV(\"uniform\", -1, 2)\n", + " p0=pyabc.RV('uniform', -1, 2), p1=pyabc.RV('uniform', -1, 2)\n", ")" ] }, @@ -144,7 +143,7 @@ "distance = pyabc.AggregatedDistance([distance0, distance1])\n", "\n", "abc = pyabc.ABCSMC(model, prior, distance)\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"tmp.db\")\n", + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'tmp.db')\n", "abc.new(db_path, observation)\n", "history1 = abc.run(max_nr_populations=6)\n", "\n", @@ -162,7 +161,7 @@ " xmax=1,\n", " x='p0',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", + " label=f'PDF t={t}',\n", " refval=p_true,\n", " )\n", " ax.legend()\n", @@ -177,7 +176,7 @@ " xmax=1,\n", " x='p1',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", + " label=f'PDF t={t}',\n", " refval=p_true,\n", " )\n", " ax.legend()\n", @@ -259,10 +258,6 @@ "import os\n", "import tempfile\n", "\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from scipy import stats\n", - "\n", "import pyabc\n", "\n", "p_true = {'p0': 0, 'p1': 0}\n", @@ -288,13 +283,13 @@ "\n", "# prior\n", "prior = pyabc.Distribution(\n", - " p0=pyabc.RV(\"uniform\", -1, 2), p1=pyabc.RV(\"uniform\", -1, 2)\n", + " p0=pyabc.RV('uniform', -1, 2), p1=pyabc.RV('uniform', -1, 2)\n", ")\n", "\n", "distance = pyabc.AggregatedDistance([distance0, distance1])\n", "\n", "abc = pyabc.ABCSMC(model, prior, distance)\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"tmp.db\")\n", + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'tmp.db')\n", "abc.new(db_path, observation)\n", "history1 = abc.run(max_nr_populations=6)\n", "\n", @@ -312,7 +307,7 @@ " xmax=1,\n", " x='p0',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", + " label=f'PDF t={t}',\n", " refval=p_true,\n", " )\n", " ax.legend()\n", @@ -327,7 +322,7 @@ " xmax=1,\n", " x='p1',\n", " ax=ax,\n", - " label=f\"PDF t={t}\",\n", + " label=f'PDF t={t}',\n", " refval=p_true,\n", " )\n", " ax.legend()\n", @@ -400,13 +395,13 @@ "source": [ "# prior\n", "prior = pyabc.Distribution(\n", - " p0=pyabc.RV(\"uniform\", -1, 2), p1=pyabc.RV(\"uniform\", -1, 2)\n", + " p0=pyabc.RV('uniform', -1, 2), p1=pyabc.RV('uniform', -1, 2)\n", ")\n", "\n", "distance = pyabc.AdaptiveAggregatedDistance([distance0, distance1])\n", "\n", "abc = pyabc.ABCSMC(model, prior, distance)\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"tmp.db\")\n", + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'tmp.db')\n", "abc.new(db_path, observation)\n", "history2 = abc.run(max_nr_populations=6)\n", "\n", @@ -476,7 +471,7 @@ "source": [ "# prior\n", "prior = pyabc.Distribution(\n", - " p0=pyabc.RV(\"uniform\", -1, 2), p1=pyabc.RV(\"uniform\", -1, 2)\n", + " p0=pyabc.RV('uniform', -1, 2), p1=pyabc.RV('uniform', -1, 2)\n", ")\n", "\n", "distance = pyabc.AdaptiveAggregatedDistance(\n", @@ -484,7 +479,7 @@ ")\n", "\n", "abc = pyabc.ABCSMC(model, prior, distance)\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"tmp.db\")\n", + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'tmp.db')\n", "abc.new(db_path, observation)\n", "history3 = abc.run(max_nr_populations=6)\n", "\n", @@ -559,7 +554,7 @@ ], "source": [ "histories = [history1, history2, history3]\n", - "labels = [\"Standard\", \"Adaptive\", \"Pre-Calibrated\"]\n", + "labels = ['Standard', 'Adaptive', 'Pre-Calibrated']\n", "\n", "pyabc.visualization.plot_sample_numbers(histories, labels, rotation=45)\n", "pyabc.visualization.plot_total_sample_numbers(\n", diff --git a/doc/examples/chemical_reaction.ipynb b/doc/examples/chemical_reaction.ipynb index aaad3eedf..ebd398871 100644 --- a/doc/examples/chemical_reaction.ipynb +++ b/doc/examples/chemical_reaction.ipynb @@ -146,16 +146,16 @@ "\n", "\n", "class Model1:\n", - " __name__ = \"Model 1\"\n", + " __name__ = 'Model 1'\n", " x0 = np.array([40, 3]) # Initial molecule numbers\n", " pre = np.array([[1, 1]], dtype=int)\n", " post = np.array([[0, 2]])\n", "\n", " def __call__(self, par):\n", " t, X = gillespie(\n", - " self.x0, np.array([float(par[\"rate\"])]), self.pre, self.post, MAX_T\n", + " self.x0, np.array([float(par['rate'])]), self.pre, self.post, MAX_T\n", " )\n", - " return {\"t\": t, \"X\": X}" + " return {'t': t, 'X': X}" ] }, { @@ -173,7 +173,7 @@ "outputs": [], "source": [ "class Model2(Model1):\n", - " __name__ = \"Model 2\"\n", + " __name__ = 'Model 2'\n", " pre = np.array([[1, 0]], dtype=int)\n", " post = np.array([[0, 1]])" ] @@ -208,15 +208,15 @@ "import matplotlib.pyplot as plt\n", "\n", "true_rate = 2.3\n", - "observations = [Model1()({\"rate\": true_rate}), Model2()({\"rate\": 30})]\n", + "observations = [Model1()({'rate': true_rate}), Model2()({'rate': 30})]\n", "fig, axes = plt.subplots(ncols=2)\n", "fig.set_size_inches((12, 4))\n", - "for ax, title, obs in zip(axes, [\"Observation\", \"Competition\"], observations):\n", - " ax.step(obs[\"t\"], obs[\"X\"])\n", - " ax.legend([\"Species X\", \"Species Y\"])\n", - " ax.set_xlabel(\"Time\")\n", - " ax.set_ylabel(\"Concentration\")\n", - " ax.set_title(title);" + "for ax, title, obs in zip(axes, ['Observation', 'Competition'], observations):\n", + " ax.step(obs['t'], obs['X'])\n", + " ax.legend(['Species X', 'Species Y'])\n", + " ax.set_xlabel('Time')\n", + " ax.set_ylabel('Concentration')\n", + " ax.set_title(title)" ] }, { @@ -255,10 +255,10 @@ "\n", "\n", "def distance(x, y):\n", - " xt_ind = np.searchsorted(x[\"t\"], t_test_times) - 1\n", - " yt_ind = np.searchsorted(y[\"t\"], t_test_times) - 1\n", + " xt_ind = np.searchsorted(x['t'], t_test_times) - 1\n", + " yt_ind = np.searchsorted(y['t'], t_test_times) - 1\n", " error = (\n", - " np.absolute(x[\"X\"][:, 1][xt_ind] - y[\"X\"][:, 1][yt_ind]).sum()\n", + " np.absolute(x['X'][:, 1][xt_ind] - y['X'][:, 1][yt_ind]).sum()\n", " / t_test_times.size\n", " )\n", " return error" @@ -279,7 +279,7 @@ "source": [ "from pyabc import RV, Distribution\n", "\n", - "prior = Distribution(rate=RV(\"uniform\", 0, 100))" + "prior = Distribution(rate=RV('uniform', 0, 100))" ] }, { @@ -336,7 +336,7 @@ } ], "source": [ - "abc_id = abc.new(\"sqlite:////tmp/mjp.db\", observations[0])" + "abc_id = abc.new('sqlite:////tmp/mjp.db', observations[0])" ] }, { @@ -438,10 +438,10 @@ ], "source": [ "ax = history.get_model_probabilities().plot.bar()\n", - "ax.set_ylabel(\"Probability\")\n", - "ax.set_xlabel(\"Generation\")\n", + "ax.set_ylabel('Probability')\n", + "ax.set_xlabel('Generation')\n", "ax.legend(\n", - " [1, 2], title=\"Model\", ncol=2, loc=\"lower center\", bbox_to_anchor=(0.5, 1)\n", + " [1, 2], title='Model', ncol=2, loc='lower center', bbox_to_anchor=(0.5, 1)\n", ");" ] }, @@ -478,7 +478,7 @@ "fig, axes = plt.subplots(2)\n", "fig.set_size_inches((6, 6))\n", "axes = axes.flatten()\n", - "axes[0].axvline(true_rate, color=\"black\", linestyle=\"dotted\")\n", + "axes[0].axvline(true_rate, color='black', linestyle='dotted')\n", "for m, ax in enumerate(axes):\n", " for t in range(0, history.n_populations, 2):\n", " df, w = history.get_distribution(m=m, t=t)\n", @@ -486,15 +486,15 @@ " plot_kde_1d(\n", " df,\n", " w,\n", - " \"rate\",\n", + " 'rate',\n", " ax=ax,\n", - " label=f\"t={t}\",\n", + " label=f't={t}',\n", " xmin=0,\n", " xmax=20 if m == 0 else 100,\n", " numx=200,\n", " )\n", - " ax.set_title(f\"Model {m+1}\")\n", - "axes[0].legend(title=\"Generation\", loc=\"upper left\", bbox_to_anchor=(1, 1))\n", + " ax.set_title(f'Model {m+1}')\n", + "axes[0].legend(title='Generation', loc='upper left', bbox_to_anchor=(1, 1))\n", "\n", "fig.tight_layout()" ] @@ -530,8 +530,8 @@ ], "source": [ "populations = history.get_all_populations()\n", - "ax = populations[populations.t >= 1].plot(\"t\", \"particles\", style=\"o-\")\n", - "ax.set_xlabel(\"Generation\");" + "ax = populations[populations.t >= 1].plot('t', 'particles', style='o-')\n", + "ax.set_xlabel('Generation');" ] }, { diff --git a/doc/examples/conversion_reaction.ipynb b/doc/examples/conversion_reaction.ipynb index 96c48bf17..294c7773f 100644 --- a/doc/examples/conversion_reaction.ipynb +++ b/doc/examples/conversion_reaction.ipynb @@ -80,7 +80,7 @@ "from pyabc import ABCSMC, RV, Distribution, LocalTransition, MedianEpsilon\n", "from pyabc.visualization import plot_data_callback, plot_kde_2d\n", "\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"test.db\")" + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'test.db')" ] }, { @@ -199,7 +199,7 @@ "metadata": {}, "outputs": [], "source": [ - "def f(y, t0, theta1, theta2):\n", + "def f(y, t0, theta1, theta2): # noqa: ARG001\n", " x1, x2 = y\n", " dx1 = -theta1 * x1 + theta2 * x2\n", " dx2 = theta1 * x1 - theta2 * x2\n", @@ -208,9 +208,9 @@ "\n", "def model(pars):\n", " sol = sp.integrate.odeint(\n", - " f, init, measurement_times, args=(pars[\"theta1\"], pars[\"theta2\"])\n", + " f, init, measurement_times, args=(pars['theta1'], pars['theta2'])\n", " )\n", - " return {\"X_2\": sol[:, 1]}" + " return {'X_2': sol[:, 1]}" ] }, { @@ -226,7 +226,7 @@ "metadata": {}, "outputs": [], "source": [ - "true_trajectory = model({\"theta1\": theta1_true, \"theta2\": theta2_true})[\"X_2\"]" + "true_trajectory = model({'theta1': theta1_true, 'theta2': theta2_true})['X_2']" ] }, { @@ -255,8 +255,8 @@ } ], "source": [ - "plt.plot(true_trajectory, color=\"C0\", label='Simulation')\n", - "plt.scatter(measurement_times, measurement_data, color=\"C1\", label='Data')\n", + "plt.plot(true_trajectory, color='C0', label='Simulation')\n", + "plt.scatter(measurement_times, measurement_data, color='C1', label='Data')\n", "plt.xlabel('Time $t$')\n", "plt.ylabel('Measurement $Y$')\n", "plt.title('Conversion reaction: True parameters fit')\n", @@ -271,7 +271,7 @@ "outputs": [], "source": [ "def distance(simulation, data):\n", - " return np.absolute(data[\"X_2\"] - simulation[\"X_2\"]).sum()" + " return np.absolute(data['X_2'] - simulation['X_2']).sum()" ] }, { @@ -299,7 +299,7 @@ ], "source": [ "parameter_prior = Distribution(\n", - " theta1=RV(\"uniform\", 0, 1), theta2=RV(\"uniform\", 0, 1)\n", + " theta1=RV('uniform', 0, 1), theta2=RV('uniform', 0, 1)\n", ")\n", "parameter_prior.get_parameter_names()" ] @@ -342,7 +342,7 @@ } ], "source": [ - "abc.new(db_path, {\"X_2\": measurement_data});" + "abc.new(db_path, {'X_2': measurement_data});" ] }, { @@ -405,8 +405,8 @@ "\n", " ax = plot_kde_2d(\n", " *h.get_distribution(m=0, t=t),\n", - " \"theta1\",\n", - " \"theta2\",\n", + " 'theta1',\n", + " 'theta2',\n", " xmin=0,\n", " xmax=1,\n", " numx=200,\n", @@ -418,12 +418,10 @@ " ax.scatter(\n", " [theta1_true],\n", " [theta2_true],\n", - " color=\"C1\",\n", - " label=r'$\\Theta$ true = {:.3f}, {:.3f}'.format(\n", - " theta1_true, theta2_true\n", - " ),\n", + " color='C1',\n", + " label=rf'$\\Theta$ true = {theta1_true:.3f}, {theta2_true:.3f}',\n", " )\n", - " ax.set_title(f\"Posterior t={t}\")\n", + " ax.set_title(f'Posterior t={t}')\n", "\n", " ax.legend()\n", "fig.tight_layout()" @@ -458,12 +456,12 @@ "_, ax = plt.subplots()\n", "\n", "\n", - "def plot_data(sum_stat, weight, ax, **kwargs):\n", + "def plot_data(sum_stat, weight, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot a single trajectory\"\"\"\n", " ax.plot(measurement_times, sum_stat['X_2'], color='grey', alpha=0.1)\n", "\n", "\n", - "def plot_mean(sum_stats, weights, ax, **kwargs):\n", + "def plot_mean(sum_stats, weights, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot mean over all samples\"\"\"\n", " weights = np.array(weights)\n", " weights /= weights.sum()\n", @@ -474,8 +472,8 @@ "\n", "ax = plot_data_callback(h, plot_data, plot_mean, ax=ax)\n", "\n", - "plt.plot(true_trajectory, color=\"C0\", label='Simulation')\n", - "plt.scatter(measurement_times, measurement_data, color=\"C1\", label='Data')\n", + "plt.plot(true_trajectory, color='C0', label='Simulation')\n", + "plt.scatter(measurement_times, measurement_data, color='C1', label='Data')\n", "plt.xlabel('Time $t$')\n", "plt.ylabel('Measurement $Y$')\n", "plt.title('Conversion reaction: Simulated data fit')\n", diff --git a/doc/examples/early_stopping.ipynb b/doc/examples/early_stopping.ipynb index 7ff1eef3d..aaccc39dc 100644 --- a/doc/examples/early_stopping.ipynb +++ b/doc/examples/early_stopping.ipynb @@ -70,7 +70,6 @@ "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import pandas as pd\n", "\n", "import pyabc\n", "from pyabc import (\n", @@ -87,7 +86,7 @@ "\n", "pyabc.settings.set_figure_params('pyabc') # for beautified plots\n", "\n", - "db_path = \"sqlite:///\" + os.path.join(tempfile.gettempdir(), \"test.db\")" + "db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'test.db')" ] }, { @@ -214,10 +213,10 @@ "\n", "dist_1_2 = distance(gt_trajectory, trajectoy_2)\n", "\n", - "plt.plot(gt_trajectory, label=f\"Step size = {gt_step_size} (Ground Truth)\")\n", - "plt.plot(trajectoy_2, label=\"Step size = 2\")\n", + "plt.plot(gt_trajectory, label=f'Step size = {gt_step_size} (Ground Truth)')\n", + "plt.plot(trajectoy_2, label='Step size = 2')\n", "plt.legend()\n", - "plt.title(f\"Distance={dist_1_2:.2f}\");" + "plt.title(f'Distance={dist_1_2:.2f}');" ] }, { @@ -263,7 +262,7 @@ " trajectory = np.zeros(n_steps)\n", " for t in range(1, n_steps):\n", " xi = np.random.uniform()\n", - " next_val = trajectory[t - 1] + xi * pars[\"step_size\"]\n", + " next_val = trajectory[t - 1] + xi * pars['step_size']\n", " cumsum += abs(next_val - gt_trajectory[t])\n", " trajectory[t] = next_val\n", " if cumsum > eps:\n", @@ -271,7 +270,7 @@ " return ModelResult(accepted=False)\n", "\n", " return ModelResult(\n", - " accepted=True, distance=cumsum, sum_stat={\"trajectory\": trajectory}\n", + " accepted=True, distance=cumsum, sum_stat={'trajectory': trajectory}\n", " )" ] }, @@ -307,7 +306,7 @@ "metadata": {}, "outputs": [], "source": [ - "prior = Distribution(step_size=RV(\"uniform\", 0, 10))" + "prior = Distribution(step_size=RV('uniform', 0, 10))" ] }, { @@ -510,14 +509,14 @@ " particles = h.get_distribution(m=0, t=t)\n", " plot_kde_1d(\n", " *particles,\n", - " \"step_size\",\n", - " label=f\"t={t}\",\n", + " 'step_size',\n", + " label=f't={t}',\n", " ax=ax,\n", " xmin=0,\n", " xmax=10,\n", " numx=300,\n", " )\n", - "ax.axvline(gt_step_size, color=\"k\", linestyle=\"dashed\");" + "ax.axvline(gt_step_size, color='k', linestyle='dashed');" ] }, { diff --git a/doc/examples/informative.ipynb b/doc/examples/informative.ipynb index 6a70a2eb2..4e7a7d38e 100644 --- a/doc/examples/informative.ipynb +++ b/doc/examples/informative.ipynb @@ -57,15 +57,20 @@ "from IPython.display import SVG, display\n", "\n", "import pyabc\n", - "from pyabc.distance import *\n", - "from pyabc.predictor import *\n", - "from pyabc.sumstat import *\n", + "from pyabc.distance import (\n", + " AdaptivePNormDistance,\n", + " InfoWeightedPNormDistance,\n", + " PNormDistance,\n", + " mad,\n", + ")\n", + "from pyabc.predictor import LinearPredictor\n", + "from pyabc.sumstat import PredictorSumstat\n", "from pyabc.util import EventIxs, ParTrafo, dict2arrlabels\n", "\n", - "pyabc.settings.set_figure_params(\"pyabc\") # for beautified plots\n", + "pyabc.settings.set_figure_params('pyabc') # for beautified plots\n", "\n", "# for debugging\n", - "for logger in [\"ABC.Distance\", \"ABC.Predictor\", \"ABC.Sumstat\"]:\n", + "for logger in ['ABC.Distance', 'ABC.Predictor', 'ABC.Sumstat']:\n", " logging.getLogger(logger).setLevel(logging.DEBUG)" ] }, @@ -94,25 +99,25 @@ "source": [ "# problem definition\n", "\n", - "sigmas = {\"p1\": 0.1}\n", + "sigmas = {'p1': 0.1}\n", "\n", "\n", "def model(p):\n", " return {\n", - " \"y1\": p[\"p1\"] + 1 + sigmas[\"p1\"] * np.random.normal(),\n", - " \"y2\": 2 + 0.1 * np.random.normal(size=3),\n", + " 'y1': p['p1'] + 1 + sigmas['p1'] * np.random.normal(),\n", + " 'y2': 2 + 0.1 * np.random.normal(size=3),\n", " }\n", "\n", "\n", - "gt_par = {\"p1\": 3}\n", + "gt_par = {'p1': 3}\n", "\n", - "data = {\"y1\": gt_par[\"p1\"] + 1, \"y2\": 2 * np.ones(shape=3)}\n", + "data = {'y1': gt_par['p1'] + 1, 'y2': 2 * np.ones(shape=3)}\n", "\n", - "prior_bounds = {\"p1\": (0, 10)}\n", + "prior_bounds = {'p1': (0, 10)}\n", "\n", "prior = pyabc.Distribution(\n", " **{\n", - " key: pyabc.RV(\"uniform\", lb, ub - lb)\n", + " key: pyabc.RV('uniform', lb, ub - lb)\n", " for key, (lb, ub) in prior_bounds.items()\n", " },\n", ")" @@ -171,12 +176,12 @@ "# YPredictor = MLPPredictor\n", "\n", "distances = {\n", - " \"L1+Ada.+MAD\": AdaptivePNormDistance(\n", + " 'L1+Ada.+MAD': AdaptivePNormDistance(\n", " p=1,\n", " # adaptive scale normalization\n", " scale_function=mad,\n", " ),\n", - " \"L1+StatLR\": PNormDistance(\n", + " 'L1+StatLR': PNormDistance(\n", " p=1,\n", " # regression-based summary statistics\n", " sumstat=PredictorSumstat(\n", @@ -186,7 +191,7 @@ " fit_ixs=EventIxs(sims=fit_sims),\n", " ),\n", " ),\n", - " \"L1+Ada.+MAD+SensiLR\": InfoWeightedPNormDistance(\n", + " 'L1+Ada.+MAD+SensiLR': InfoWeightedPNormDistance(\n", " p=1,\n", " # adaptive scale normalization\n", " scale_function=mad,\n", @@ -197,7 +202,7 @@ " ),\n", "}\n", "\n", - "colors = {distance_id: f\"C{i}\" for i, distance_id in enumerate(distances)}" + "colors = {distance_id: f'C{i}' for i, distance_id in enumerate(distances)}" ] }, { @@ -357,7 +362,7 @@ "source": [ "# runs\n", "\n", - "db_file = tempfile.mkstemp(suffix=\".db\")[1]\n", + "db_file = tempfile.mkstemp(suffix='.db')[1]\n", "\n", "scale_log_file = tempfile.mkstemp()[1]\n", "info_log_file = tempfile.mkstemp()[1]\n", @@ -367,13 +372,13 @@ "for distance_id, distance in distances.items():\n", " print(distance_id)\n", " if isinstance(distance, AdaptivePNormDistance):\n", - " distance.scale_log_file = f\"{scale_log_file}_{distance_id}.json\"\n", + " distance.scale_log_file = f'{scale_log_file}_{distance_id}.json'\n", " if isinstance(distance, InfoWeightedPNormDistance):\n", - " distance.info_log_file = f\"{info_log_file}_{distance_id}.json\"\n", - " distance.info_sample_log_file = f\"{info_sample_log_file}_{distance_id}\"\n", + " distance.info_log_file = f'{info_log_file}_{distance_id}.json'\n", + " distance.info_sample_log_file = f'{info_sample_log_file}_{distance_id}'\n", "\n", " abc = pyabc.ABCSMC(model, prior, distance, population_size=pop_size)\n", - " h = abc.new(db=\"sqlite:///\" + db_file, observed_sum_stat=data)\n", + " h = abc.new(db='sqlite:///' + db_file, observed_sum_stat=data)\n", " abc.run(max_total_nr_simulations=total_sims)\n", " hs.append(h)" ] @@ -440,7 +445,10 @@ " pd: Probability density/densities at p.\n", " \"\"\"\n", " if p_to_y is None:\n", - " p_to_y = lambda p: p\n", + "\n", + " def p_to_y(_p):\n", + " return _p\n", + "\n", " y = p_to_y(p)\n", " pd = np.exp(-((y - y_obs) ** 2) / (2 * sigma**2))\n", " return pd\n", @@ -448,7 +456,9 @@ "\n", "for i_par, par in enumerate(gt_par.keys()):\n", " # define parameter-simulation transformation\n", - " p_to_y = lambda p: p + 1\n", + " def p_to_y(_p):\n", + " return _p + 1\n", + "\n", " # observed data corresponding to parameter\n", " y_obs = p_to_y(gt_par[par])\n", " # bounds\n", @@ -472,15 +482,15 @@ " axes[i_par].plot(\n", " xs,\n", " pdf(xs) / norm,\n", - " linestyle=\"dashed\",\n", - " color=\"grey\",\n", - " label=\"ground truth\",\n", + " linestyle='dashed',\n", + " color='grey',\n", + " label='ground truth',\n", " )\n", "\n", "# plot ABC approximations\n", "\n", "for i_par, par in enumerate(prior_bounds.keys()):\n", - " for distance_id, h in zip(distances.keys(), hs):\n", + " for distance_id, h in zip(distances, hs):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " h,\n", " x=par,\n", @@ -535,19 +545,19 @@ "\n", "scale_distance_ids = [\n", " distance_id\n", - " for distance_id in distances.keys()\n", - " if \"Ada.\" in distance_id and \"Stat\" not in distance_id\n", + " for distance_id in distances\n", + " if 'Ada.' in distance_id and 'Stat' not in distance_id\n", "]\n", "scale_log_files = []\n", - "for i_dist, distance_id in enumerate(scale_distance_ids):\n", - " scale_log_files.append(f\"{scale_log_file}_{distance_id}.json\")\n", + "for distance_id in scale_distance_ids:\n", + " scale_log_files.append(f'{scale_log_file}_{distance_id}.json')\n", "\n", "pyabc.visualization.plot_distance_weights(\n", " scale_log_files,\n", " labels=scale_distance_ids,\n", " colors=[colors[distance_id] for distance_id in scale_distance_ids],\n", - " xlabel=\"Model output\",\n", - " title=\"Scale weights\",\n", + " xlabel='Model output',\n", + " title='Scale weights',\n", " ax=axes[0],\n", " keys=dict2arrlabels(data, keys=data.keys()),\n", ")\n", @@ -555,18 +565,18 @@ "# info weights\n", "\n", "info_distance_ids = [\n", - " distance_id for distance_id in distances.keys() if \"Sensi\" in distance_id\n", + " distance_id for distance_id in distances if 'Sensi' in distance_id\n", "]\n", "info_log_files = []\n", - "for i_dist, distance_id in enumerate(info_distance_ids):\n", - " info_log_files.append(f\"{info_log_file}_{distance_id}.json\")\n", + "for distance_id in info_distance_ids:\n", + " info_log_files.append(f'{info_log_file}_{distance_id}.json')\n", "\n", "pyabc.visualization.plot_distance_weights(\n", " info_log_files,\n", " labels=info_distance_ids,\n", " colors=[colors[distance_id] for distance_id in info_distance_ids],\n", - " xlabel=\"Model output\",\n", - " title=\"Sensitivity weights\",\n", + " xlabel='Model output',\n", + " title='Sensitivity weights',\n", " ax=axes[1],\n", " keys=dict2arrlabels(data, keys=data.keys()),\n", ")\n", @@ -616,14 +626,14 @@ "# plot flow diagram\n", "\n", "fig = pyabc.visualization.plot_sensitivity_sankey(\n", - " info_sample_log_file=f\"{info_sample_log_file}_L1+Ada.+MAD+SensiLR\",\n", - " t=f\"{info_log_file}_L1+Ada.+MAD+SensiLR.json\",\n", + " info_sample_log_file=f'{info_sample_log_file}_L1+Ada.+MAD+SensiLR',\n", + " t=f'{info_log_file}_L1+Ada.+MAD+SensiLR.json',\n", " h=hs[-1],\n", " predictor=LinearPredictor(),\n", ")\n", "\n", "# here just showing a non-interactive plot to reduce storage\n", - "img_file = tempfile.mkstemp(suffix=(\".svg\"))[1]\n", + "img_file = tempfile.mkstemp(suffix=('.svg'))[1]\n", "fig.write_image(img_file)\n", "display(SVG(img_file))" ] @@ -666,41 +676,41 @@ "source": [ "# problem definition\n", "\n", - "sigmas = {\"p1\": 1e-1, \"p2\": 1e2, \"p3\": 1e2, \"p4\": 1e-1}\n", + "sigmas = {'p1': 1e-1, 'p2': 1e2, 'p3': 1e2, 'p4': 1e-1}\n", "\n", "\n", "def model(p):\n", " return {\n", - " \"y1\": p[\"p1\"] + sigmas[\"p1\"] * np.random.normal(),\n", - " \"y2\": p[\"p2\"] + sigmas[\"p2\"] * np.random.normal(),\n", - " \"y3\": p[\"p3\"]\n", - " + np.sqrt(4 * sigmas[\"p3\"] ** 2) * np.random.normal(size=4),\n", - " \"y4\": p[\"p4\"] ** 2 + sigmas[\"p4\"] * np.random.normal(),\n", - " \"y5\": 1e1 * np.random.normal(size=10),\n", + " 'y1': p['p1'] + sigmas['p1'] * np.random.normal(),\n", + " 'y2': p['p2'] + sigmas['p2'] * np.random.normal(),\n", + " 'y3': p['p3']\n", + " + np.sqrt(4 * sigmas['p3'] ** 2) * np.random.normal(size=4),\n", + " 'y4': p['p4'] ** 2 + sigmas['p4'] * np.random.normal(),\n", + " 'y5': 1e1 * np.random.normal(size=10),\n", " }\n", "\n", "\n", "prior_bounds = {\n", - " \"p1\": (-7e0, 7e0),\n", - " \"p2\": (-7e2, 7e2),\n", - " \"p3\": (-7e2, 7e2),\n", - " \"p4\": (-1e0, 1e0),\n", + " 'p1': (-7e0, 7e0),\n", + " 'p2': (-7e2, 7e2),\n", + " 'p3': (-7e2, 7e2),\n", + " 'p4': (-1e0, 1e0),\n", "}\n", "\n", "prior = pyabc.Distribution(\n", " **{\n", - " key: pyabc.RV(\"uniform\", lb, ub - lb)\n", + " key: pyabc.RV('uniform', lb, ub - lb)\n", " for key, (lb, ub) in prior_bounds.items()\n", " },\n", ")\n", "\n", - "gt_par = {\"p1\": 0, \"p2\": 0, \"p3\": 0, \"p4\": 0.5}\n", + "gt_par = {'p1': 0, 'p2': 0, 'p3': 0, 'p4': 0.5}\n", "data = {\n", - " \"y1\": 0,\n", - " \"y2\": 0,\n", - " \"y3\": 0 * np.ones(4),\n", - " \"y4\": 0.5**2,\n", - " \"y5\": 0 * np.ones(10),\n", + " 'y1': 0,\n", + " 'y2': 0,\n", + " 'y3': 0 * np.ones(4),\n", + " 'y4': 0.5**2,\n", + " 'y5': 0 * np.ones(10),\n", "}" ] }, @@ -751,18 +761,18 @@ " lambda x: x**3,\n", " lambda x: x**4,\n", "]\n", - "trafo_ids = [\"{par_id}^\" + str(i + 1) for i in range(4)]\n", + "trafo_ids = ['{par_id}^' + str(i + 1) for i in range(4)]\n", "fit_sims = 0.4 * total_sims\n", "\n", "YPredictor = LinearPredictor\n", "# YPredictor = MLPPredictor\n", "\n", "distances = {\n", - " \"L1+Ada.+MAD\": AdaptivePNormDistance(\n", + " 'L1+Ada.+MAD': AdaptivePNormDistance(\n", " p=1,\n", " scale_function=mad,\n", " ),\n", - " \"L1+StatLR\": PNormDistance(\n", + " 'L1+StatLR': PNormDistance(\n", " p=1,\n", " sumstat=PredictorSumstat(\n", " predictor=YPredictor(\n", @@ -771,7 +781,7 @@ " fit_ixs=EventIxs(sims=fit_sims),\n", " ),\n", " ),\n", - " \"L1+Ada.+MAD+StatLR\": AdaptivePNormDistance(\n", + " 'L1+Ada.+MAD+StatLR': AdaptivePNormDistance(\n", " p=1,\n", " scale_function=mad,\n", " sumstat=PredictorSumstat(\n", @@ -779,7 +789,7 @@ " fit_ixs=EventIxs(sims=fit_sims),\n", " ),\n", " ),\n", - " \"L1+Ada.+MAD+StatLR+P4\": AdaptivePNormDistance(\n", + " 'L1+Ada.+MAD+StatLR+P4': AdaptivePNormDistance(\n", " p=1,\n", " scale_function=mad,\n", " sumstat=PredictorSumstat(\n", @@ -788,24 +798,24 @@ " par_trafo=ParTrafo(trafos=par_trafos, trafo_ids=trafo_ids),\n", " ),\n", " ),\n", - " \"L1+Ada.+MAD+SensiLR\": InfoWeightedPNormDistance(\n", + " 'L1+Ada.+MAD+SensiLR': InfoWeightedPNormDistance(\n", " p=1,\n", " scale_function=mad,\n", " predictor=YPredictor(),\n", " fit_info_ixs=EventIxs(sims=fit_sims),\n", - " feature_normalization=\"mad\",\n", + " feature_normalization='mad',\n", " ),\n", - " \"L1+Ada.+MAD+SensiLR+P4\": InfoWeightedPNormDistance(\n", + " 'L1+Ada.+MAD+SensiLR+P4': InfoWeightedPNormDistance(\n", " p=1,\n", " scale_function=mad,\n", " predictor=YPredictor(),\n", " fit_info_ixs=EventIxs(sims=fit_sims),\n", - " feature_normalization=\"mad\",\n", + " feature_normalization='mad',\n", " par_trafo=ParTrafo(trafos=par_trafos, trafo_ids=trafo_ids),\n", " ),\n", "}\n", "\n", - "colors = {distance_id: f\"C{i}\" for i, distance_id in enumerate(distances)}" + "colors = {distance_id: f'C{i}' for i, distance_id in enumerate(distances)}" ] }, { @@ -1421,7 +1431,7 @@ "\n", "# runs\n", "\n", - "db_file = tempfile.mkstemp(suffix=\".db\")[1]\n", + "db_file = tempfile.mkstemp(suffix='.db')[1]\n", "\n", "scale_log_file = tempfile.mkstemp()[1]\n", "info_log_file = tempfile.mkstemp()[1]\n", @@ -1431,13 +1441,13 @@ "for distance_id, distance in distances.items():\n", " print(distance_id)\n", " if isinstance(distance, AdaptivePNormDistance):\n", - " distance.scale_log_file = f\"{scale_log_file}_{distance_id}.json\"\n", + " distance.scale_log_file = f'{scale_log_file}_{distance_id}.json'\n", " if isinstance(distance, InfoWeightedPNormDistance):\n", - " distance.info_log_file = f\"{info_log_file}_{distance_id}.json\"\n", - " distance.info_sample_log_file = f\"{info_sample_log_file}_{distance_id}\"\n", + " distance.info_log_file = f'{info_log_file}_{distance_id}.json'\n", + " distance.info_sample_log_file = f'{info_sample_log_file}_{distance_id}'\n", "\n", " abc = pyabc.ABCSMC(model, prior, distance, population_size=pop_size)\n", - " h = abc.new(db=\"sqlite:///\" + db_file, observed_sum_stat=data)\n", + " h = abc.new(db='sqlite:///' + db_file, observed_sum_stat=data)\n", " abc.run(max_total_nr_simulations=total_sims)\n", " hs.append(h)" ] @@ -1500,9 +1510,15 @@ "\n", "for i_par, par in enumerate(gt_par.keys()):\n", " # define parameter-simulation transformation\n", - " p_to_y = lambda p: p\n", - " if par == \"p4\":\n", - " p_to_y = lambda p: p**2\n", + " if par == 'p4':\n", + "\n", + " def p_to_y(_p):\n", + " return _p**2\n", + " else:\n", + "\n", + " def p_to_y(_p):\n", + " return _p\n", + "\n", " # observed data corresponding to parameter\n", " y_obs = p_to_y(gt_par[par])\n", " # bounds\n", @@ -1526,15 +1542,15 @@ " axes[i_par].plot(\n", " xs,\n", " pdf(xs) / norm,\n", - " linestyle=\"dashed\",\n", - " color=\"grey\",\n", - " label=\"ground truth\",\n", + " linestyle='dashed',\n", + " color='grey',\n", + " label='ground truth',\n", " )\n", "\n", "# plot ABC approximations\n", "\n", - "for i_par, par in enumerate(prior_bounds.keys()):\n", - " for distance_id, h in zip(distances.keys(), hs):\n", + "for i_par, par in enumerate(prior_bounds):\n", + " for distance_id, h in zip(distances, hs):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " h,\n", " x=par,\n", @@ -1543,7 +1559,7 @@ " xmax=prior_bounds[par][1],\n", " ax=axes[i_par],\n", " label=distance_id,\n", - " kde=pyabc.GridSearchCV() if par == \"p4\" else None,\n", + " kde=pyabc.GridSearchCV() if par == 'p4' else None,\n", " numx=500,\n", " )\n", "\n", @@ -1552,7 +1568,7 @@ " ax.set_ylabel(None)\n", "fig.tight_layout(rect=(0, 0.1, 1, 1))\n", "axes[-1].legend(\n", - " bbox_to_anchor=(1, -0.2), loc=\"upper right\", ncol=len(distances) + 1\n", + " bbox_to_anchor=(1, -0.2), loc='upper right', ncol=len(distances) + 1\n", ")" ] }, @@ -1592,19 +1608,19 @@ "\n", "scale_distance_ids = [\n", " distance_id\n", - " for distance_id in distances.keys()\n", - " if \"Ada.\" in distance_id and \"Stat\" not in distance_id\n", + " for distance_id in distances\n", + " if 'Ada.' in distance_id and 'Stat' not in distance_id\n", "]\n", "scale_log_files = []\n", - "for i_dist, distance_id in enumerate(scale_distance_ids):\n", - " scale_log_files.append(f\"{scale_log_file}_{distance_id}.json\")\n", + "for distance_id in scale_distance_ids:\n", + " scale_log_files.append(f'{scale_log_file}_{distance_id}.json')\n", "\n", "pyabc.visualization.plot_distance_weights(\n", " scale_log_files,\n", " labels=scale_distance_ids,\n", " colors=[colors[distance_id] for distance_id in scale_distance_ids],\n", - " xlabel=\"Model output\",\n", - " title=\"Scale weights\",\n", + " xlabel='Model output',\n", + " title='Scale weights',\n", " ax=axes[0],\n", " keys=dict2arrlabels(data, keys=data.keys()),\n", ")\n", @@ -1612,18 +1628,18 @@ "# info weights\n", "\n", "info_distance_ids = [\n", - " distance_id for distance_id in distances.keys() if \"Sensi\" in distance_id\n", + " distance_id for distance_id in distances if 'Sensi' in distance_id\n", "]\n", "info_log_files = []\n", - "for i_dist, distance_id in enumerate(info_distance_ids):\n", - " info_log_files.append(f\"{info_log_file}_{distance_id}.json\")\n", + "for distance_id in info_distance_ids:\n", + " info_log_files.append(f'{info_log_file}_{distance_id}.json')\n", "\n", "pyabc.visualization.plot_distance_weights(\n", " info_log_files,\n", " labels=info_distance_ids,\n", " colors=[colors[distance_id] for distance_id in info_distance_ids],\n", - " xlabel=\"Model output\",\n", - " title=\"Sensitivity weights\",\n", + " xlabel='Model output',\n", + " title='Sensitivity weights',\n", " ax=axes[1],\n", " keys=dict2arrlabels(data, keys=data.keys()),\n", ")\n", @@ -1744,8 +1760,8 @@ "# plot flow diagram\n", "\n", "fig = pyabc.visualization.plot_sensitivity_sankey(\n", - " info_sample_log_file=f\"{info_sample_log_file}_L1+Ada.+MAD+SensiLR+P4\",\n", - " t=f\"{info_log_file}_L1+Ada.+MAD+SensiLR+P4.json\",\n", + " info_sample_log_file=f'{info_sample_log_file}_L1+Ada.+MAD+SensiLR+P4',\n", + " t=f'{info_log_file}_L1+Ada.+MAD+SensiLR+P4.json',\n", " h=hs[-1],\n", " predictor=LinearPredictor(),\n", " par_trafo=ParTrafo(trafos=par_trafos, trafo_ids=trafo_ids),\n", @@ -1753,7 +1769,7 @@ ")\n", "\n", "# here just showing a non-interactive plot to reduce storage\n", - "img_file = tempfile.mkstemp(suffix=(\".svg\"))[1]\n", + "img_file = tempfile.mkstemp(suffix=('.svg'))[1]\n", "fig.write_image(img_file)\n", "display(SVG(img_file))" ] diff --git a/doc/examples/look_ahead.ipynb b/doc/examples/look_ahead.ipynb index 9ae7111b1..169c2e0f3 100644 --- a/doc/examples/look_ahead.ipynb +++ b/doc/examples/look_ahead.ipynb @@ -38,13 +38,12 @@ "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import pandas as pd\n", "import scipy as sp\n", "\n", "import pyabc\n", "\n", "# set to \"DEBUG\" to get full logging information from the sampler and workers\n", - "logging.getLogger(\"ABC.Sampler\").setLevel(\"WARNING\")\n", + "logging.getLogger('ABC.Sampler').setLevel('WARNING')\n", "\n", "pyabc.settings.set_figure_params('pyabc') # for beautified plots" ] @@ -78,13 +77,13 @@ "outputs": [], "source": [ "theta1_true, theta2_true = np.exp([-2.5, -2])\n", - "theta_true = {\"theta1\": theta1_true, \"theta2\": theta2_true}\n", + "theta_true = {'theta1': theta1_true, 'theta2': theta2_true}\n", "measurement_times = np.arange(11)\n", "init = np.array([1, 0])\n", "sigma = 0.03\n", "\n", "\n", - "def f(y, t0, theta1, theta2):\n", + "def f(y, t0, theta1, theta2): # noqa: ARG001\n", " x1, x2 = y\n", " dx1 = -theta1 * x1 + theta2 * x2\n", " dx2 = theta1 * x1 - theta2 * x2\n", @@ -94,30 +93,29 @@ "def model(pars):\n", " # numerical integration\n", " sol = sp.integrate.odeint(\n", - " f, init, measurement_times, args=(pars[\"theta1\"], pars[\"theta2\"])\n", + " f, init, measurement_times, args=(pars['theta1'], pars['theta2'])\n", " )\n", " # we only observe species 2\n", " sol = sol[:, 1]\n", "\n", " # add multiplicative measurement noise to ODE solution\n", - " noise = np.random.normal(1, 0.03, size=len(sol))\n", " noisysol = sol * np.random.normal(1, sigma, size=len(sol))\n", "\n", " # sleep a little to emulate heterogeneous run times\n", " sleep_s = np.random.lognormal(mean=-2, sigma=1)\n", " time.sleep(sleep_s)\n", "\n", - " return {\"X_2\": noisysol}\n", + " return {'X_2': noisysol}\n", "\n", "\n", "def distance(simulation, data):\n", - " return np.absolute(data[\"X_2\"] - simulation[\"X_2\"]).sum()\n", + " return np.absolute(data['X_2'] - simulation['X_2']).sum()\n", "\n", "\n", "measurement_data = model(theta_true)\n", "\n", "parameter_prior = pyabc.Distribution(\n", - " theta1=pyabc.RV(\"uniform\", 0, 1), theta2=pyabc.RV(\"uniform\", 0, 1)\n", + " theta1=pyabc.RV('uniform', 0, 1), theta2=pyabc.RV('uniform', 0, 1)\n", ")" ] }, @@ -219,7 +217,7 @@ ")\n", "hs_dyn = []\n", "\n", - "for i in range(0, iters_dyn):\n", + "for _ in range(0, iters_dyn):\n", " abc = pyabc.ABCSMC(\n", " models=model,\n", " parameter_priors=parameter_prior,\n", @@ -305,8 +303,8 @@ "source": [ "hs_la = []\n", "sampler_logfiles = []\n", - "for i in range(0, iters_la):\n", - " logfile = tempfile.mkstemp(prefix=\"redis_log\", suffix=\".csv\")[1]\n", + "for _ in range(0, iters_la):\n", + " logfile = tempfile.mkstemp(prefix='redis_log', suffix='.csv')[1]\n", " sampler_logfiles.append(logfile)\n", " redis_sampler = pyabc.sampler.RedisEvalParallelSamplerServerStarter(\n", " # main field: in generation t already preemptively sample for t+1 if cores\n", @@ -412,7 +410,7 @@ "\n", "hs_stat = []\n", "\n", - "for i in range(0, iters_stat):\n", + "for _ in range(0, iters_stat):\n", " abc = pyabc.ABCSMC(\n", " models=model,\n", " parameter_priors=parameter_prior,\n", @@ -469,44 +467,44 @@ "\n", "for i_h, h in enumerate(hs_dyn):\n", " df, w = h.get_distribution(m=0, t=h.max_t)\n", - " for par, ax in zip([\"theta1\", \"theta2\"], axes):\n", + " for par, ax in zip(['theta1', 'theta2'], axes):\n", " pyabc.visualization.plot_kde_1d(\n", " df,\n", " w,\n", " x=par,\n", " ax=ax,\n", - " color=\"C0\",\n", + " color='C0',\n", " xmin=0,\n", " xmax=1,\n", - " label=\"DYN\" if i_h == 0 else None,\n", + " label='DYN' if i_h == 0 else None,\n", " )\n", "\n", "for i_h, h in enumerate(hs_la):\n", " df, w = h.get_distribution(m=0, t=h.max_t)\n", - " for par, ax in zip([\"theta1\", \"theta2\"], axes):\n", + " for par, ax in zip(['theta1', 'theta2'], axes):\n", " pyabc.visualization.plot_kde_1d(\n", " df,\n", " w,\n", " x=par,\n", " ax=ax,\n", - " color=\"C1\",\n", + " color='C1',\n", " xmin=0,\n", " xmax=1,\n", - " label=\"LA\" if i_h == 0 else None,\n", + " label='LA' if i_h == 0 else None,\n", " )\n", "\n", "for i_h, h in enumerate(hs_stat):\n", " df, w = h.get_distribution(m=0, t=h.max_t)\n", - " for par, ax in zip([\"theta1\", \"theta2\"], axes):\n", + " for par, ax in zip(['theta1', 'theta2'], axes):\n", " pyabc.visualization.plot_kde_1d(\n", " df,\n", " w,\n", " x=par,\n", " ax=ax,\n", - " color=\"C2\",\n", + " color='C2',\n", " xmin=0,\n", " xmax=1,\n", - " label=\"STAT\" if i_h == 0 else None,\n", + " label='STAT' if i_h == 0 else None,\n", " )\n", "\n", "plt.legend()" @@ -558,9 +556,9 @@ "source": [ "hs = [*hs_dyn, *hs_la, *hs_stat]\n", "labels = [\n", - " *[\"DYN\"] * len(hs_dyn),\n", - " *[\"LA\"] * len(hs_la),\n", - " *[\"STAT\"] * len(hs_stat),\n", + " *['DYN'] * len(hs_dyn),\n", + " *['LA'] * len(hs_la),\n", + " *['STAT'] * len(hs_stat),\n", "]\n", "\n", "pyabc.visualization.plot_eps_walltime(hs, labels, group_by_label=True)\n", diff --git a/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index 2f530b766..a34da7a9e 100644 --- a/doc/examples/petab_application.ipynb +++ b/doc/examples/petab_application.ipynb @@ -30,13 +30,9 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", "import amici.petab_import\n", - "import numpy as np\n", "import petab\n", "\n", - "import pyabc\n", "from pyabc.petab import AmiciPetabImporter" ] }, @@ -87,8 +83,8 @@ "source": [ "# read the petab problem from yaml\n", "petab_problem = petab.Problem.from_yaml(\n", - " \"tmp/benchmark-models/hackathon_contributions_new_data_format/\"\n", - " \"Boehm_JProteomeRes2014/Boehm_JProteomeRes2014.yaml\"\n", + " 'tmp/benchmark-models/hackathon_contributions_new_data_format/'\n", + " 'Boehm_JProteomeRes2014/Boehm_JProteomeRes2014.yaml'\n", ")\n", "\n", "# compile the petab problem to an AMICI ODE model\n", diff --git a/doc/examples/petab_yaml2sbml.ipynb b/doc/examples/petab_yaml2sbml.ipynb index 834a95f01..0fb05e51b 100644 --- a/doc/examples/petab_yaml2sbml.ipynb +++ b/doc/examples/petab_yaml2sbml.ipynb @@ -35,7 +35,6 @@ "import sys\n", "\n", "import amici.petab_import\n", - "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "import pyabc\n", @@ -168,13 +167,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[32mChecking SBML model...\n", - "\u001b[0m\u001b[32mChecking measurement table...\n", - "\u001b[0m\u001b[32mChecking condition table...\n", - "\u001b[0m\u001b[32mChecking observable table...\n", - "\u001b[0m\u001b[32mChecking parameter table...\n", - "\u001b[0m\u001b[32mPEtab format check completed successfully.\n", - "\u001b[0m\u001b[0m" + "\u001B[32mChecking SBML model...\n", + "\u001B[0m\u001B[32mChecking measurement table...\n", + "\u001B[0m\u001B[32mChecking condition table...\n", + "\u001B[0m\u001B[32mChecking observable table...\n", + "\u001B[0m\u001B[32mChecking parameter table...\n", + "\u001B[0m\u001B[32mPEtab format check completed successfully.\n", + "\u001B[0m\u001B[0m" ] } ], @@ -444,7 +443,6 @@ " np.random.seed(2)\n", " sigma = 0.02\n", " obs_x2 = rdata['x'][:, 1] + sigma * np.random.randn(n_time)\n", - " obs_x2\n", "\n", " # to measurement dataframe\n", " df = pd.DataFrame(\n", diff --git a/doc/examples/using_copasi.ipynb b/doc/examples/using_copasi.ipynb index 1f6037592..fe38cbeb8 100644 --- a/doc/examples/using_copasi.ipynb +++ b/doc/examples/using_copasi.ipynb @@ -93,11 +93,11 @@ ], "source": [ "max_t = 0.1\n", - "model = BasicoModel(\"models/model1.xml\", duration=max_t)\n", + "model = BasicoModel('models/model1.xml', duration=max_t)\n", "\n", - "true_par = {\"rate\": 2.3}\n", + "true_par = {'rate': 2.3}\n", "obs = model(true_par)\n", - "plt.plot(obs[\"t\"], obs[\"X\"]);" + "plt.plot(obs['t'], obs['X']);" ] }, { @@ -120,16 +120,16 @@ "\n", "\n", "def distance(x, y):\n", - " xt_ind = np.searchsorted(x[\"t\"], t_test_times) - 1\n", - " yt_ind = np.searchsorted(y[\"t\"], t_test_times) - 1\n", + " xt_ind = np.searchsorted(x['t'], t_test_times) - 1\n", + " yt_ind = np.searchsorted(y['t'], t_test_times) - 1\n", " error = (\n", - " np.absolute(x[\"X\"][:, 1][xt_ind] - y[\"X\"][:, 1][yt_ind]).sum()\n", + " np.absolute(x['X'][:, 1][xt_ind] - y['X'][:, 1][yt_ind]).sum()\n", " / t_test_times.size\n", " )\n", " return error\n", "\n", "\n", - "prior = pyabc.Distribution(rate=pyabc.RV(\"uniform\", 0, 50))" + "prior = pyabc.Distribution(rate=pyabc.RV('uniform', 0, 50))" ] }, { @@ -205,8 +205,8 @@ ], "source": [ "abc = pyabc.ABCSMC(model, prior, distance, population_size=100)\n", - "db = tempfile.mkstemp(suffix=\".db\")[1]\n", - "abc.new(\"sqlite:///\" + db, obs)\n", + "db = tempfile.mkstemp(suffix='.db')[1]\n", + "abc.new('sqlite:///' + db, obs)\n", "h = abc.run(max_nr_populations=10, min_acceptance_rate=1e-2)" ] }, @@ -243,13 +243,13 @@ "for t in range(h.max_t):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " h,\n", - " x=\"rate\",\n", + " x='rate',\n", " xmin=0,\n", " xmax=20,\n", " t=t,\n", " refval=true_par,\n", - " refval_color=\"grey\",\n", - " label=f\"t={t}\",\n", + " refval_color='grey',\n", + " label=f't={t}',\n", " ax=ax,\n", " )\n", "ax.legend();" @@ -295,10 +295,10 @@ } ], "source": [ - "def plot_data(sumstat, weight, ax, **kwargs):\n", + "def plot_data(sumstat, weight, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot a single trajectory\"\"\"\n", " for i in range(2):\n", - " ax.plot(sumstat[\"t\"], sumstat['X'][:, i], color=f\"C{i}\", alpha=0.1)\n", + " ax.plot(sumstat['t'], sumstat['X'][:, i], color=f'C{i}', alpha=0.1)\n", "\n", "\n", "for t in [0, h.max_t]:\n", @@ -309,8 +309,8 @@ " t=t,\n", " ax=ax,\n", " )\n", - " ax.plot(obs[\"t\"], obs[\"X\"])\n", - " ax.set_title(f\"Simulations at t={t}\");" + " ax.plot(obs['t'], obs['X'])\n", + " ax.set_title(f'Simulations at t={t}')" ] } ], diff --git a/doc/examples/using_julia.ipynb b/doc/examples/using_julia.ipynb index 84c88ccd8..bbd77daa7 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -72,7 +72,7 @@ ], "source": [ "%%time\n", - "jl = Julia(module_name=\"SIR\", source_file=\"model_julia/SIR.jl\")" + "jl = Julia(module_name='SIR', source_file='model_julia/SIR.jl')" ] }, { @@ -257,7 +257,7 @@ "distance = jl.distance()\n", "obs = jl.observation()\n", "\n", - "_ = plt.plot(obs[\"t\"], obs[\"u\"])" + "_ = plt.plot(obs['t'], obs['u'])" ] }, { @@ -275,15 +275,15 @@ "metadata": {}, "outputs": [], "source": [ - "gt_par = {\"p1\": -4.0, \"p2\": -2.0}\n", + "gt_par = {'p1': -4.0, 'p2': -2.0}\n", "\n", "# parameter limits and prior\n", "par_limits = {\n", - " \"p1\": (-5, -3),\n", - " \"p2\": (-3, -1),\n", + " 'p1': (-5, -3),\n", + " 'p2': (-3, -1),\n", "}\n", "prior = Distribution(\n", - " **{key: RV(\"uniform\", lb, ub - lb) for key, (lb, ub) in par_limits.items()}\n", + " **{key: RV('uniform', lb, ub - lb) for key, (lb, ub) in par_limits.items()}\n", ")" ] }, @@ -369,8 +369,8 @@ " distance,\n", " sampler=MulticoreEvalParallelSampler(),\n", ")\n", - "db = tempfile.mkstemp(suffix=\".db\")[1]\n", - "abc.new(\"sqlite:///\" + db, obs)\n", + "db = tempfile.mkstemp(suffix='.db')[1]\n", + "abc.new('sqlite:///' + db, obs)\n", "h = abc.run(max_nr_populations=10)" ] }, @@ -420,10 +420,10 @@ " t=t,\n", " limits=par_limits,\n", " refval=gt_par,\n", - " refval_color=\"grey\",\n", + " refval_color='grey',\n", " )\n", - " plt.gcf().suptitle(f\"Posterior at t={t}\")\n", - " plt.gcf().tight_layout();" + " plt.gcf().suptitle(f'Posterior at t={t}')\n", + " plt.gcf().tight_layout()" ] }, { @@ -466,10 +466,10 @@ } ], "source": [ - "def plot_data(sumstat, weight, ax, **kwargs):\n", + "def plot_data(sumstat, weight, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot a single trajectory\"\"\"\n", " for i in range(3):\n", - " ax.plot(sumstat[\"t\"], sumstat['u'][:, i], color=f\"C{i}\", alpha=0.1)\n", + " ax.plot(sumstat['t'], sumstat['u'][:, i], color=f'C{i}', alpha=0.1)\n", "\n", "\n", "for t in [0, h.max_t]:\n", @@ -480,8 +480,8 @@ " t=t,\n", " ax=ax,\n", " )\n", - " ax.plot(obs[\"t\"], obs[\"u\"])\n", - " ax.set_title(f\"Simulations at t={t}\");" + " ax.plot(obs['t'], obs['u'])\n", + " ax.set_title(f'Simulations at t={t}')" ] } ], diff --git a/pyabc/acceptor/acceptor.py b/pyabc/acceptor/acceptor.py index c4d82451b..21d2497a5 100644 --- a/pyabc/acceptor/acceptor.py +++ b/pyabc/acceptor/acceptor.py @@ -15,7 +15,7 @@ """ import logging -from typing import Callable, Union +from collections.abc import Callable import numpy as np import pandas as pd @@ -26,7 +26,7 @@ from ..storage import save_dict_to_json from .pdf_norm import pdf_norm_from_kernel, pdf_norm_max_found -logger = logging.getLogger("ABC.Acceptor") +logger = logging.getLogger('ABC.Acceptor') class AcceptorResult(dict): @@ -59,7 +59,7 @@ def __getattr__(self, key): try: return self[key] except KeyError: - raise AttributeError(key) + raise AttributeError(key) from None __setattr__ = dict.__setitem__ __delattr__ = dict.__delitem__ @@ -187,8 +187,7 @@ def is_adaptive(self) -> bool: """ return False - # pylint: disable=R0201 - def get_epsilon_config(self, t: int) -> dict: + def get_epsilon_config(self, t: int) -> dict: # noqa: ARG002 """ Create a configuration object that contains all values of interest for the update of the Epsilon object. @@ -224,7 +223,7 @@ def __call__(self, distance_function, eps, x, x_0, t, par): return self.fun(distance_function, eps, x, x_0, t, par) @staticmethod - def to_acceptor(maybe_acceptor: Union[Acceptor, Callable]) -> Acceptor: + def to_acceptor(maybe_acceptor: Acceptor | Callable) -> Acceptor: """ Create an acceptor object from input. @@ -451,7 +450,7 @@ def _update( self.log(t) def log(self, t): - logger.debug(f"pdf_norm={self.pdf_norms[t]:.4e} for t={t}.") + logger.debug(f'pdf_norm={self.pdf_norms[t]:.4e} for t={t}.') if self.log_file: save_dict_to_json(self.pdf_norms, self.log_file) @@ -487,10 +486,7 @@ def __call__( # accept threshold = np.random.uniform(low=0, high=1) - if acc_prob >= threshold: - accept = True - else: - accept = False + accept = acc_prob >= threshold # weight if acc_prob == 0.0: @@ -503,8 +499,8 @@ def __call__( # check pdf max ok if pdf_norm < density: logger.debug( - f"Encountered density={density:.4e} > c={pdf_norm:.4e}, " - f"thus weight={weight:.4e}." + f'Encountered density={density:.4e} > c={pdf_norm:.4e}, ' + f'thus weight={weight:.4e}.' ) # return unscaled density value and the acceptance flag diff --git a/pyabc/acceptor/pdf_norm.py b/pyabc/acceptor/pdf_norm.py index 2e96a2a73..f9afbf292 100644 --- a/pyabc/acceptor/pdf_norm.py +++ b/pyabc/acceptor/pdf_norm.py @@ -1,10 +1,10 @@ -from typing import Callable, Union +from collections.abc import Callable import numpy as np import pandas as pd -def pdf_norm_from_kernel(kernel_val: float, **kwargs): +def pdf_norm_from_kernel(kernel_val: float, **kwargs): # noqa: ARG001 """ Just use the pdf_max value passed, usually originating from the distance function. @@ -13,9 +13,9 @@ def pdf_norm_from_kernel(kernel_val: float, **kwargs): def pdf_norm_max_found( - prev_pdf_norm: Union[float, None], + prev_pdf_norm: float | None, get_weighted_distances: Callable[[], pd.DataFrame], - **kwargs, + **kwargs, # noqa: ARG001 ): """ Take as pdf_max the maximum over the values found so far in the history, @@ -68,18 +68,18 @@ def __init__( alpha: float = 0.5, min_acceptance_rate: bool = 0.1, ): - self.factor = 10 + self.factor = factor self.alpha = alpha self.min_acceptance_rate = min_acceptance_rate self._hit = False def __call__( self, - prev_pdf_norm: Union[float, None], + prev_pdf_norm: float | None, get_weighted_distances: Callable[[], pd.DataFrame], - prev_temp: Union[float, None], + prev_temp: float | None, acceptance_rate: float, - **kwargs, + **kwargs, # noqa: ARG002 ): # base: the maximum found temperature pdf_norm = pdf_norm_max_found( @@ -97,7 +97,7 @@ def __call__( # from now on rescale self._hit = True - if prev_temp is None: + if prev_temp is None: # noqa SIM108 # can't take temperature into account, thus effectively assume T=1 next_temp = 1 else: diff --git a/pyabc/copasi/model.py b/pyabc/copasi/model.py index ab70acd56..ba0f52ffa 100644 --- a/pyabc/copasi/model.py +++ b/pyabc/copasi/model.py @@ -2,20 +2,19 @@ import logging import os -from typing import Dict, List from .. import Parameter from ..model import Model -logger = logging.getLogger("ABC.Copasi") +logger = logging.getLogger('ABC.Copasi') try: import basico except ImportError: basico = None logger.error( - "Install BasiCO (see https://basico.rtfd.io) to use the BasiCO model, " - "e.g. via `pip install pyabc[copasi]`" + 'Install BasiCO (see https://basico.rtfd.io) to use the BasiCO model, ' + 'e.g. via `pip install pyabc[copasi]`' ) @@ -35,15 +34,15 @@ class BasicoModel(Model): def __init__( self, sbml_file: str, - changes: Dict[str, float] = None, + changes: dict[str, float] = None, change_unit: bool = True, - method: str = "stochastic", + method: str = 'stochastic', t0: float = None, duration: float = None, num_steps: int = None, automatic: bool = True, use_numbers: bool = False, - output: List[str] = None, + output: list[str] = None, model_name: str = None, ): """ @@ -84,7 +83,7 @@ def __init__( self.model_name = model_name super().__init__( - name=f"BasicoModel_{model_name}", + name=f'BasicoModel_{model_name}', ) self.sbml_file = sbml_file @@ -110,7 +109,7 @@ def __init__( self.output = output self.method = method - def __call__(self, pars: Dict[str, float], return_raw: bool = False): + def __call__(self, pars: dict[str, float], return_raw: bool = False): """Simulate data for given parameters. Calls the time course and returns the selected result. @@ -141,11 +140,11 @@ def __call__(self, pars: Dict[str, float], return_raw: bool = False): # cache output columns if self.output is None: - self.output = list(set(tc.columns) - {"Time"}) + self.output = list(set(tc.columns) - {'Time'}) return { - "t": tc.Time.to_numpy(), - "X": tc[self.output].to_numpy(), + 't': tc.Time.to_numpy(), + 'X': tc[self.output].to_numpy(), } def sample(self, pars: Parameter): @@ -156,7 +155,7 @@ def sample(self, pars: Parameter): """ return self(pars, return_raw=False) - def apply_parameters(self, pars: Dict[str, float]): + def apply_parameters(self, pars: dict[str, float]): """Set the parameters of the model. Parameters @@ -179,17 +178,17 @@ def apply_parameters(self, pars: Dict[str, float]): def __getstate__(self): # all arguments state = { - "sbml_file": self.sbml_file, - "changes": self.changes, - "change_unit": self.change_unit, - "method": self.method, - "t0": self.t0, - "duration": self.duration, - "num_steps": self.num_steps, - "automatic": self.automatic, - "use_numbers": self.use_numbers, - "output": self.output, - "model_name": self.model_name, + 'sbml_file': self.sbml_file, + 'changes': self.changes, + 'change_unit': self.change_unit, + 'method': self.method, + 't0': self.t0, + 'duration': self.duration, + 'num_steps': self.num_steps, + 'automatic': self.automatic, + 'use_numbers': self.use_numbers, + 'output': self.output, + 'model_name': self.model_name, } return state diff --git a/pyabc/distance/aggregate.py b/pyabc/distance/aggregate.py index 913cdc4ec..85372819f 100644 --- a/pyabc/distance/aggregate.py +++ b/pyabc/distance/aggregate.py @@ -1,7 +1,7 @@ """Aggregated distances.""" import logging -from typing import Callable, List, Union +from collections.abc import Callable import numpy as np @@ -10,7 +10,7 @@ from .base import Distance, FunctionDistance from .scale import span -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') class AggregatedDistance(Distance): @@ -25,9 +25,9 @@ class AggregatedDistance(Distance): def __init__( self, - distances: List[Union[Distance, Callable]], - weights: Union[List, dict] = None, - factors: Union[List, dict] = None, + distances: list[Distance | Callable], + weights: list | dict = None, + factors: list | dict = None, ): """ Parameters @@ -52,9 +52,9 @@ def __init__( """ super().__init__() - if isinstance(distances, (Distance, Callable)): + if isinstance(distances, Distance | Callable): distances = [distances] - self.distances: List[Distance] = [ + self.distances: list[Distance] = [ FunctionDistance.to_distance(distance) for distance in distances ] @@ -220,18 +220,18 @@ class AdaptiveAggregatedDistance(AggregatedDistance): def __init__( self, - distances: List[Distance], - initial_weights: List = None, - factors: Union[List, dict] = None, + distances: list[Distance], + initial_weights: list = None, + factors: list | dict = None, adaptive: bool = True, scale_function: Callable = None, log_file: str = None, ): super().__init__(distances=distances) - self.initial_weights: List = initial_weights - self.factors: Union[List, dict] = factors + self.initial_weights: list = initial_weights + self.factors: list | dict = factors self.adaptive: bool = adaptive - self.x_0: Union[dict, None] = None + self.x_0: dict | None = None if scale_function is None: scale_function = span self.scale_function: Callable = scale_function @@ -327,8 +327,8 @@ def _update( w = np.array(w) if w.size != len(self.distances): raise AssertionError( - f"weights.size={w.size} != " - f"len(distances)={len(self.distances)}" + f'weights.size={w.size} != ' + f'len(distances)={len(self.distances)}' ) # add to w attribute, at time t @@ -354,7 +354,7 @@ def configure_sampler(self, sampler) -> None: sampler.sample_factory.record_rejected() def log(self, t: int) -> None: - logger.debug(f"Weights[{t}] = {self.weights[t]}") + logger.debug(f'Weights[{t}] = {self.weights[t]}') if self.log_file: save_dict_to_json(self.weights, self.log_file) diff --git a/pyabc/distance/base.py b/pyabc/distance/base.py index 28a93f88c..b7f60f958 100644 --- a/pyabc/distance/base.py +++ b/pyabc/distance/base.py @@ -2,7 +2,7 @@ import json from abc import ABC, abstractmethod -from typing import Callable, Union +from collections.abc import Callable from ..population import Sample @@ -15,7 +15,7 @@ class Distance(ABC): should inherit from this class. """ - def initialize( + def initialize( # noqa: B027 self, t: int, get_sample: Callable[[], Sample], @@ -40,7 +40,7 @@ def initialize( The total number of simulations so far. """ - def configure_sampler(self, sampler): + def configure_sampler(self, sampler): # noqa: B027 """Configure the sampler. This method is called by the inference routine at the beginning. @@ -56,9 +56,9 @@ def configure_sampler(self, sampler): def update( self, - t: int, - get_sample: Callable[[], Sample], - total_sims: int, + t: int, # noqa: ARG002 + get_sample: Callable[[], Sample], # noqa: ARG002 + total_sims: int, # noqa: ARG002 ) -> bool: """Update for the upcoming generation t. @@ -146,7 +146,7 @@ def get_config(self) -> dict: config: dict Dictionary describing the distance. """ - return {"name": self.__class__.__name__} + return {'name': self.__class__.__name__} def to_json(self) -> str: """ @@ -181,13 +181,13 @@ def __init__(self): def __call__( self, - x: dict, - x_0: dict, - t: int = None, - par: dict = None, + x: dict, # noqa: ARG002 + x_0: dict, # noqa: ARG002 + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: raise AssertionError( - f"Distance {self.__class__.__name__} should not be called." + f'Distance {self.__class__.__name__} should not be called.' ) @@ -201,10 +201,10 @@ class AcceptAllDistance(Distance): def __call__( self, - x: dict, - x_0: dict, - t: int = None, - par: dict = None, + x: dict, # noqa: ARG002 + x_0: dict, # noqa: ARG002 + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: return -1 @@ -232,8 +232,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: return self.fun(x, x_0) @@ -241,16 +241,16 @@ def get_config(self): conf = super().get_config() # try to get the function name try: - conf["name"] = self.fun.__name__ + conf['name'] = self.fun.__name__ except AttributeError: try: - conf["name"] = self.fun.__class__.__name__ + conf['name'] = self.fun.__class__.__name__ except AttributeError: pass return conf @staticmethod - def to_distance(maybe_distance: Union[Callable, Distance]) -> Distance: + def to_distance(maybe_distance: Callable | Distance) -> Distance: """ Parameters ---------- diff --git a/pyabc/distance/distance.py b/pyabc/distance/distance.py index 7f8c20e14..c6cb12efe 100644 --- a/pyabc/distance/distance.py +++ b/pyabc/distance/distance.py @@ -1,7 +1,7 @@ """Various basic distances.""" import logging -from typing import Callable, List, Union +from collections.abc import Callable import numpy as np from scipy import linalg as la @@ -9,7 +9,7 @@ from ..population import Sample from .base import Distance -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') class DistanceWithMeasureList(Distance): @@ -35,17 +35,17 @@ def __init__( def initialize( self, - t: int, - get_sample: Callable[[], Sample], + t: int, # noqa: ARG002 + get_sample: Callable[[], Sample], # noqa: ARG002 x_0: dict, - total_sims: int, + total_sims: int, # noqa: ARG002 ): if self.measures_to_use == 'all': self.measures_to_use = x_0.keys() def get_config(self): config = super().get_config() - config["measures_to_use"] = self.measures_to_use + config['measures_to_use'] = self.measures_to_use return config @@ -65,8 +65,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: return sum( abs((x[key] - x_0[key]) / x_0[key]) @@ -98,7 +98,7 @@ class PCADistance(DistanceWithMeasureList): def __init__(self, measures_to_use='all', p: float = 2): super().__init__(measures_to_use) self.p: float = p - self.trafo: Union[np.ndarray, None] = None + self.trafo: np.ndarray | None = None def _dict_to_vect(self, x): return np.asarray([x[key] for key in self.measures_to_use]) @@ -142,8 +142,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: x_vec, x_0_vec = self._dict_to_vect(x), self._dict_to_vect(x_0) distance = la.norm( @@ -172,7 +172,7 @@ class RangeEstimatorDistance(DistanceWithMeasureList): """ @staticmethod - def lower(parameter_list: List[float]): + def lower(parameter_list: list[float]): """ Calculate the lower margin form a list of parameter values. @@ -188,7 +188,7 @@ def lower(parameter_list: List[float]): """ @staticmethod - def upper(parameter_list: List[float]): + def upper(parameter_list: list[float]): """ Calculate the upper margin form a list of parameter values. @@ -209,7 +209,7 @@ def __init__(self, measures_to_use='all'): def get_config(self): config = super().get_config() - config["normalization"] = self.normalization + config['normalization'] = self.normalization return config def _calculate_normalization(self, sum_stats): @@ -249,8 +249,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: distance = sum( abs((x[key] - x_0[key]) / self.normalization[key]) @@ -294,5 +294,5 @@ def lower(parameter_list): def get_config(self): config = super().get_config() - config["PERCENTILE"] = self.PERCENTILE + config['PERCENTILE'] = self.PERCENTILE return config diff --git a/pyabc/distance/kernel.py b/pyabc/distance/kernel.py index d902d6cf2..feb973220 100644 --- a/pyabc/distance/kernel.py +++ b/pyabc/distance/kernel.py @@ -1,5 +1,6 @@ """Stochastic kernels.""" -from typing import Callable, List, Sequence, Union + +from collections.abc import Callable, Sequence import numpy as np from scipy import stats @@ -7,8 +8,8 @@ from ..population import Sample from .base import Distance -SCALE_LIN = "SCALE_LIN" -SCALE_LOG = "SCALE_LOG" +SCALE_LIN = 'SCALE_LIN' +SCALE_LOG = 'SCALE_LOG' SCALES = [SCALE_LIN, SCALE_LOG] @@ -44,7 +45,7 @@ class StochasticKernel(Distance): def __init__( self, ret_scale: str = SCALE_LIN, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): StochasticKernel.check_ret_scale(ret_scale) @@ -54,10 +55,10 @@ def __init__( def initialize( self, - t: int, - get_sample: Callable[[], Sample], + t: int, # noqa: ARG002 + get_sample: Callable[[], Sample], # noqa: ARG002 x_0: dict, - total_sims: int, + total_sims: int, # noqa: ARG002 ): """ Remember the summary statistic keys in sorted order, @@ -71,7 +72,7 @@ def initialize( def check_ret_scale(ret_scale): if ret_scale not in SCALES: raise ValueError( - f"The ret_scale {ret_scale} must be one of {SCALES}." + f'The ret_scale {ret_scale} must be one of {SCALES}.' ) def initialize_keys(self, x): @@ -95,7 +96,7 @@ def __init__( self, fun: Callable, ret_scale: str = SCALE_LIN, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=ret_scale, keys=keys, pdf_max=pdf_max) @@ -134,7 +135,7 @@ def __init__( self, cov: np.ndarray = None, ret_scale: str = SCALE_LOG, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=ret_scale, keys=keys, pdf_max=pdf_max) @@ -181,8 +182,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: """ Return the value of the normal distribution at x - x_0, or its @@ -228,8 +229,8 @@ class IndependentNormalKernel(StochasticKernel): def __init__( self, - var: Union[Callable, Sequence[float], float] = None, - keys: List[str] = None, + var: Callable | Sequence[float] | float = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=SCALE_LOG, keys=keys, pdf_max=pdf_max) @@ -268,7 +269,7 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, + t: int = None, # noqa: ARG002 par: dict = None, ): # safety check @@ -319,8 +320,8 @@ class IndependentLaplaceKernel(StochasticKernel): def __init__( self, - scale: Union[Callable, Sequence[float], float] = None, - keys: List[str] = None, + scale: Callable | Sequence[float] | float = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=SCALE_LOG, keys=keys, pdf_max=pdf_max) @@ -360,7 +361,7 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, + t: int = None, # noqa: ARG002 par: dict = None, ): # safety check @@ -401,17 +402,17 @@ class BinomialKernel(StochasticKernel): def __init__( self, - p: Union[float, Callable], + p: float | Callable, ret_scale: str = SCALE_LOG, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=ret_scale, keys=keys, pdf_max=pdf_max) if not callable(p) and (p > 1 or p < 0): raise ValueError( - f"The success probability p={p} must be in the interval" - f"[0, 1]." + f'The success probability p={p} must be in the interval' + f'[0, 1].' ) self.p = p @@ -441,7 +442,7 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, + t: int = None, # noqa: ARG002 par: dict = None, ) -> float: x = np.asarray(_arr(x, self.keys), dtype=int) @@ -470,7 +471,7 @@ class PoissonKernel(StochasticKernel): def __init__( self, ret_scale: str = SCALE_LOG, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=ret_scale, keys=keys, pdf_max=pdf_max) @@ -500,8 +501,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: x = np.asarray(_arr(x, self.keys), dtype=int) x_0 = np.asarray(_arr(x_0, self.keys), dtype=int) @@ -530,15 +531,15 @@ def __init__( self, p: float, ret_scale: str = SCALE_LOG, - keys: List[str] = None, + keys: list[str] = None, pdf_max: float = None, ): super().__init__(ret_scale=ret_scale, keys=keys, pdf_max=pdf_max) if not callable(p) and (p > 1 or p < 0): raise ValueError( - f"The success probability p={p} must be in the interval" - f"[0, 1]." + f'The success probability p={p} must be in the interval' + f'[0, 1].' ) self.p = p @@ -563,7 +564,7 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, + t: int = None, # noqa: ARG002 par: dict = None, ) -> float: x = np.asarray(_arr(x, self.keys), dtype=int) diff --git a/pyabc/distance/ot.py b/pyabc/distance/ot.py index b81f25538..c0bfa343f 100644 --- a/pyabc/distance/ot.py +++ b/pyabc/distance/ot.py @@ -1,8 +1,8 @@ """Optimal transport distances.""" import logging +from collections.abc import Callable from functools import partial -from typing import Callable, Union import numpy as np import scipy.linalg as la @@ -18,7 +18,7 @@ ot = None -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') class WassersteinDistance(Distance): @@ -68,7 +68,7 @@ def __init__( self, sumstat: Sumstat, p: float = 2.0, - dist: Union[str, Callable] = None, + dist: str | Callable = None, emd_args: dict = None, ): """ @@ -88,8 +88,8 @@ def __init__( """ if ot is None: raise ImportError( - "This distance requires the optimal transport library pot. " - "Install via `pip install pyabc[ot]` or `pip install pot`.", + 'This distance requires the optimal transport library pot. ' + 'Install via `pip install pyabc[ot]` or `pip install pot`.', ) super().__init__() @@ -100,12 +100,12 @@ def __init__( if dist is None: # translate from p if p == 1.0: - dist = "cityblock" + dist = 'cityblock' elif p == 2.0: - dist = "sqeuclidean" + dist = 'sqeuclidean' else: # of course, we could permit arbitrary norms here if needed - raise ValueError(f"Cannot translate p={p} into a distance.") + raise ValueError(f'Cannot translate p={p} into a distance.') if isinstance(dist, str): dist = partial(spat.distance.cdist, metric=dist) self.dist: Callable = dist @@ -115,8 +115,8 @@ def __init__( self.emd_args: dict = emd_args # observed data - self.x0: Union[dict, None] = None - self.s0: Union[np.ndarray, None] = None + self.x0: dict | None = None + self.s0: np.ndarray | None = None def initialize( self, @@ -157,8 +157,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: # compute summary statistics, shape (n, dim), (n0, dim) s, s0 = self.sumstat(x), self.sumstat(x_0) @@ -222,10 +222,10 @@ class SlicedWassersteinDistance(Distance): def __init__( self, sumstat: Sumstat, - metric: str = "sqeuclidean", + metric: str = 'sqeuclidean', p: float = 2.0, n_proj: int = 50, - seed: Union[int, np.random.RandomState] = None, + seed: int | np.random.RandomState = None, emd_1d_args: dict = None, ): """ @@ -249,8 +249,8 @@ def __init__( """ if ot is None: raise ImportError( - "This distance requires the optimal transport library pot. " - "Install via `pip install pyabc[ot]` or `pip install pot`.", + 'This distance requires the optimal transport library pot. ' + 'Install via `pip install pyabc[ot]` or `pip install pot`.', ) super().__init__() @@ -258,14 +258,14 @@ def __init__( self.metric: str = metric self.p: float = p self.n_proj: int = n_proj - self.seed: Union[int, np.random.RandomState] = seed + self.seed: int | np.random.RandomState = seed if emd_1d_args is None: emd_1d_args = {} self.emd_1d_args: dict = emd_1d_args # observed data - self.x0: Union[dict, None] = None - self.s0: Union[np.ndarray, None] = None + self.x0: dict | None = None + self.s0: np.ndarray | None = None def initialize( self, @@ -302,8 +302,8 @@ def __call__( self, x: dict, x_0: dict, - t: int = None, - par: dict = None, + t: int = None, # noqa: ARG002 + par: dict = None, # noqa: ARG002 ) -> float: # compute summary statistics, shape (n, dim), (n0, dim) s, s0 = self.sumstat(x), self.sumstat(x_0) @@ -311,7 +311,7 @@ def __call__( dim, dim0 = s.shape[1], s0.shape[1] if dim != dim0: - raise ValueError(f"Sumstat dimensions do not match: {dim}!={dim0}") + raise ValueError(f'Sumstat dimensions do not match: {dim}!={dim0}') # unit sphere samples for Monte-Carlo approximation, # shape (n_proj, dim) @@ -355,7 +355,7 @@ def __call__( def uniform_unit_sphere_samples( n_proj: int, dim: int, - seed: Union[int, np.random.RandomState] = None, + seed: int | np.random.RandomState = None, ) -> np.ndarray: r""" Generate uniformly distributed samples from the :math:`d-1`-dim. diff --git a/pyabc/distance/pnorm.py b/pyabc/distance/pnorm.py index a23e0b128..e77d6298f 100644 --- a/pyabc/distance/pnorm.py +++ b/pyabc/distance/pnorm.py @@ -1,8 +1,8 @@ """p-norm based (adaptive) distances.""" import logging +from collections.abc import Callable, Collection from numbers import Number -from typing import Callable, Collection, Dict, List, Union import numpy as np @@ -21,7 +21,7 @@ from .scale import mad from .util import bound_weights, fd_nabla1_multi_delta, log_weights -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') class PNormDistance(Distance): @@ -60,9 +60,7 @@ class PNormDistance(Distance): def __init__( self, p: float = 1, - fixed_weights: Union[ - Dict[str, float], Dict[int, Dict[str, float]] - ] = None, + fixed_weights: dict[str, float] | dict[int, dict[str, float]] = None, sumstat: Sumstat = None, ): super().__init__() @@ -72,15 +70,15 @@ def __init__( self.sumstat: Sumstat = sumstat if p < 1: - raise ValueError("It must be p >= 1") + raise ValueError('It must be p >= 1') self.p: float = p self._arg_fixed_weights = fixed_weights - self.fixed_weights: Union[Dict[int, np.ndarray], None] = None + self.fixed_weights: dict[int, np.ndarray] | None = None # to cache the observed data and summary statistics - self.x_0: Union[dict, None] = None - self.s_0: Union[np.ndarray, None] = None + self.x_0: dict | None = None + self.s_0: np.ndarray | None = None def initialize( self, @@ -148,10 +146,10 @@ def get_weights(self, t: int) -> np.ndarray: @staticmethod def format_dict( - vals: Union[Dict[str, float], Dict[int, Dict[str, float]]], + vals: dict[str, float] | dict[int, dict[str, float]], t: int, - s_ids: List[str], - ) -> Dict[int, Union[float, np.ndarray]]: + s_ids: list[str], + ) -> dict[int, float | np.ndarray]: """Normalize weight dictionary to the employed format. Parameters @@ -178,7 +176,7 @@ def format_dict( return vals @staticmethod - def for_t_or_latest(w: Dict[int, np.ndarray], t: int) -> np.ndarray: + def for_t_or_latest(w: dict[int, np.ndarray], t: int) -> np.ndarray: """Extract values from dict for given time point. Parameters @@ -192,7 +190,7 @@ def for_t_or_latest(w: Dict[int, np.ndarray], t: int) -> np.ndarray: """ # take last time point for which values exist if t not in w: - smaller_ts = [t_ for t_ in w.keys() if t_ <= t] + smaller_ts = [t_ for t_ in w if t_ <= t] if len(smaller_ts) == 0: return np.asarray(1.0) t = max(smaller_ts) @@ -204,7 +202,7 @@ def __call__( x: dict, x_0: dict, t: int = None, - par: dict = None, + par: dict = None, # noqa: ARG002 ) -> float: # extract weights for given time point weights = self.get_weights(t=t) @@ -215,8 +213,8 @@ def __call__( # assert shapes match if s.shape != weights.shape and weights.shape or s.shape != s0.shape: raise AssertionError( - f"Shapes do not match: s={s.shape}, s0={s0.shape}, " - f"weights={weights.shape}" + f'Shapes do not match: s={s.shape}, s0={s0.shape}, ' + f'weights={weights.shape}' ) # component-wise distances @@ -229,16 +227,16 @@ def __call__( def get_config(self) -> dict: return { - "name": self.__class__.__name__, - "p": self.p, - "fixed_weights": self.fixed_weights, - "sumstat": self.sumstat.__str__(), + 'name': self.__class__.__name__, + 'p': self.p, + 'fixed_weights': self.fixed_weights, + 'sumstat': self.sumstat.__str__(), } def weights2dict( self, - weights: Dict[int, np.ndarray], - ) -> Dict[int, Dict[str, float]]: + weights: dict[int, np.ndarray], + ) -> dict[int, dict[str, float]]: """Create labeled weights dictionary. Parameters @@ -310,9 +308,9 @@ class AdaptivePNormDistance(PNormDistance): def __init__( self, p: float = 1, - initial_scale_weights: Dict[str, float] = None, - fixed_weights: Dict[str, float] = None, - fit_scale_ixs: Union[EventIxs, Collection[int], int] = np.inf, + initial_scale_weights: dict[str, float] = None, + fixed_weights: dict[str, float] = None, + fit_scale_ixs: EventIxs | Collection[int] | int = np.inf, scale_function: Callable = None, max_scale_weight_ratio: float = None, scale_log_file: str = None, @@ -322,15 +320,15 @@ def __init__( # call p-norm constructor super().__init__(p=p, fixed_weights=fixed_weights, sumstat=sumstat) - self.initial_scale_weights: Dict[str, float] = initial_scale_weights + self.initial_scale_weights: dict[str, float] = initial_scale_weights - self.scale_weights: Dict[int, np.ndarray] = {} + self.scale_weights: dict[int, np.ndarray] = {} # extract indices when to fit scales from input if fit_scale_ixs is None: fit_scale_ixs = {np.inf} self.fit_scale_ixs: EventIxs = EventIxs.to_instance(fit_scale_ixs) - logger.debug(f"Fit scale ixs: {self.fit_scale_ixs}") + logger.debug(f'Fit scale ixs: {self.fit_scale_ixs}') if scale_function is None: scale_function = mad @@ -382,7 +380,7 @@ def initialize( if not self.fit_scale_ixs.act(t=t, total_sims=total_sims): raise ValueError( - f"Initial scale weights (t={t}) must be fitted or provided." + f'Initial scale weights (t={t}) must be fitted or provided.' ) # execute cached function @@ -408,7 +406,7 @@ def update( if not self.fit_scale_ixs.act(t=t, total_sims=total_sims): if updated: logger.warning( - f"t={t}: Updated sumstat but not scale weights." + f't={t}: Updated sumstat but not scale weights.' ) return updated @@ -461,7 +459,7 @@ def fit_scales( t=t, weights=self.scale_weights, keys=self.sumstat.get_ids(), - label="Scale", + label='Scale', log_file=self.scale_log_file, ) @@ -485,10 +483,10 @@ def get_config(self) -> dict: config = super().get_config() config.update( { - "fit_scale_ixs": self.fit_scale_ixs.__repr__(), - "scale_function": self.scale_function.__name__, - "max_scale_weight_ratio": self.max_scale_weight_ratio, - "all_particles_for_scale": self.all_particles_for_scale, + 'fit_scale_ixs': self.fit_scale_ixs.__repr__(), + 'scale_function': self.scale_function.__name__, + 'max_scale_weight_ratio': self.max_scale_weight_ratio, + 'all_particles_for_scale': self.all_particles_for_scale, } ) return config @@ -497,21 +495,21 @@ def get_config(self) -> dict: class InfoWeightedPNormDistance(AdaptivePNormDistance): """Weight summary statistics by sensitivity of a predictor `y -> theta`.""" - WEIGHTS = "weights" - STD = "std" - MAD = "mad" - NONE = "none" + WEIGHTS = 'weights' + STD = 'std' + MAD = 'mad' + NONE = 'none' FEATURE_NORMALIZATIONS = [WEIGHTS, STD, MAD, NONE] def __init__( self, predictor: Predictor, p: float = 1, - initial_scale_weights: Dict[str, float] = None, - initial_info_weights: Dict[str, float] = None, - fixed_weights: Dict[str, float] = None, - fit_scale_ixs: Union[EventIxs, Collection, int] = np.inf, - fit_info_ixs: Union[EventIxs, Collection, int] = None, + initial_scale_weights: dict[str, float] = None, + initial_info_weights: dict[str, float] = None, + fixed_weights: dict[str, float] = None, + fit_scale_ixs: EventIxs | Collection | int = np.inf, + fit_info_ixs: EventIxs | Collection | int = None, normalize_by_par: bool = True, scale_function: Callable = None, max_scale_weight_ratio: float = None, @@ -520,7 +518,7 @@ def __init__( info_log_file: str = None, info_sample_log_file: str = None, sumstat: Sumstat = None, - fd_deltas: Union[List[float], float] = None, + fd_deltas: list[float] | float = None, subsetter: Subsetter = None, all_particles_for_scale: bool = True, all_particles_for_prediction: bool = True, @@ -597,19 +595,19 @@ def __init__( self.predictor = predictor - self.initial_info_weights: Dict[str, float] = initial_info_weights - self.info_weights: Dict[int, np.ndarray] = {} + self.initial_info_weights: dict[str, float] = initial_info_weights + self.info_weights: dict[int, np.ndarray] = {} if fit_info_ixs is None: fit_info_ixs = {9, 15} self.fit_info_ixs: EventIxs = EventIxs.to_instance(fit_info_ixs) - logger.debug(f"Fit info ixs: {self.fit_info_ixs}") + logger.debug(f'Fit info ixs: {self.fit_info_ixs}') self.normalize_by_par: bool = normalize_by_par self.max_info_weight_ratio: float = max_info_weight_ratio self.info_log_file: str = info_log_file self.info_sample_log_file: str = info_sample_log_file - self.fd_deltas: Union[List[float], float] = fd_deltas + self.fd_deltas: list[float] | float = fd_deltas if subsetter is None: subsetter = IdSubsetter() @@ -622,8 +620,8 @@ def __init__( not in InfoWeightedPNormDistance.FEATURE_NORMALIZATIONS ): raise ValueError( - f"Feature normalization {feature_normalization} must be in " - f"{InfoWeightedPNormDistance.FEATURE_NORMALIZATIONS}", + f'Feature normalization {feature_normalization} must be in ' + f'{InfoWeightedPNormDistance.FEATURE_NORMALIZATIONS}', ) self.feature_normalization: str = feature_normalization @@ -746,7 +744,7 @@ def fit_info( scale_weights=self.scale_weights, ) x, y, weights, use_ixs, x0 = ( - ret[key] for key in ("x", "y", "weights", "use_ixs", "x0") + ret[key] for key in ('x', 'y', 'weights', 'use_ixs', 'x0') ) # learn predictor model @@ -769,7 +767,7 @@ def fit_info( if np.allclose(info_weights_red, 0): info_weights_red = 1 * np.ones_like(info_weights_red) - logger.info("All info weights zeros, thus resetting to ones.") + logger.info('All info weights zeros, thus resetting to ones.') else: # in order to make each sumstat count a little, avoid zero values zero_info = np.isclose(info_weights_red, 0) @@ -796,7 +794,7 @@ def fit_info( t=t, weights=self.info_weights, keys=self.sumstat.get_ids(), - label="Info", + label='Info', log_file=self.info_log_file, ) @@ -809,8 +807,8 @@ def normalize_sample( t: int, subsetter: Subsetter, feature_normalization: str, - scale_weights: Dict[int, np.ndarray], - ) -> Dict: + scale_weights: dict[int, np.ndarray], + ) -> dict: """Normalize samples prior to regression model training. Parameters @@ -844,7 +842,7 @@ def normalize_sample( # define feature scaling if feature_normalization == InfoWeightedPNormDistance.WEIGHTS: if scale_weights is None: - raise ValueError("Requiested scale weights but None passed") + raise ValueError('Requiested scale weights but None passed') scale_weights = scale_weights[t] offset_x = np.zeros_like(scale_weights) scale_x = np.zeros_like(scale_weights) @@ -862,8 +860,8 @@ def normalize_sample( scale_x = np.ones(shape=x.shape[1]) else: raise ValueError( - f"Feature normalization {feature_normalization} must be " - f"in {InfoWeightedPNormDistance.FEATURE_NORMALIZATIONS}", + f'Feature normalization {feature_normalization} must be ' + f'in {InfoWeightedPNormDistance.FEATURE_NORMALIZATIONS}', ) # remove trivial features @@ -887,17 +885,17 @@ def normalize_sample( y = (y - mean_y) / std_y return { - "x": x, - "y": y, - "weights": weights, - "use_ixs": use_ixs, - "x0": x0, + 'x': x, + 'y': y, + 'weights': weights, + 'use_ixs': use_ixs, + 'x0': x0, } @staticmethod def calculate_sensis( predictor: Predictor, - fd_deltas: Union[List[float], float], + fd_deltas: list[float] | float, x0: np.ndarray, n_x: int, n_y: int, @@ -929,7 +927,7 @@ def fun(_x): # shape (n_x, n_y) sensis = fd_nabla1_multi_delta(x=x0, fun=fun, test_deltas=fd_deltas) if sensis.shape != (n_x, n_y): - raise AssertionError("Sensitivity shape did not match.") + raise AssertionError('Sensitivity shape did not match.') # we are only interested in absolute values sensis = np.abs(sensis) @@ -947,7 +945,7 @@ def fun(_x): insensi_par_keys = [ par_trafo_ids[ix] for ix in np.flatnonzero(~y_has_sensi) ] - logger.info(f"Zero info for parameters {insensi_par_keys}") + logger.info(f'Zero info for parameters {insensi_par_keys}') if normalize_by_par: # normalize sums over sumstats to 1 @@ -975,10 +973,10 @@ def get_config(self) -> dict: config = super().get_config() config.update( { - "predictor": self.predictor.__str__(), - "fit_info_ixs": self.fit_info_ixs.__repr__(), - "scale_function": self.scale_function.__name__, - "max_info_weight_ratio": self.max_info_weight_ratio, + 'predictor': self.predictor.__str__(), + 'fit_info_ixs': self.fit_info_ixs.__repr__(), + 'scale_function': self.scale_function.__name__, + 'max_info_weight_ratio': self.max_info_weight_ratio, } ) return config diff --git a/pyabc/distance/scale.py b/pyabc/distance/scale.py index b4af7179d..ef4b1ca25 100644 --- a/pyabc/distance/scale.py +++ b/pyabc/distance/scale.py @@ -31,12 +31,12 @@ * mean * median """ + import logging -from typing import List import numpy as np -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') def check_io(fun): @@ -46,27 +46,27 @@ def check_io(fun): """ def checked_fun(samples: np.ndarray, **kwargs): - if "s0" in kwargs: - if (samples.ndim == 1 and np.ndim(kwargs["s0"]) > 0) or ( - samples.ndim > 1 and samples.shape[1] != kwargs["s0"].shape[0] + if 's0' in kwargs: # noqa: SIM102 + if (samples.ndim == 1 and np.ndim(kwargs['s0']) > 0) or ( + samples.ndim > 1 and samples.shape[1] != kwargs['s0'].shape[0] ): - raise AssertionError("Shape mismatch of samples and s0") - if "s_ids" in kwargs: - if (samples.ndim == 1 and len(kwargs["s_ids"]) > 1) or ( - samples.ndim > 1 and len(kwargs["s_ids"]) != samples.shape[1] + raise AssertionError('Shape mismatch of samples and s0') + if 's_ids' in kwargs: # noqa: SIM102 + if (samples.ndim == 1 and len(kwargs['s_ids']) > 1) or ( + samples.ndim > 1 and len(kwargs['s_ids']) != samples.shape[1] ): - raise AssertionError("Shape mismatch of samples and s_ids") + raise AssertionError('Shape mismatch of samples and s_ids') scales: np.ndarray = fun(samples=samples, **kwargs) if (samples.ndim == 1 and np.ndim(scales) > 0) or ( samples.ndim > 1 and scales.shape != (samples.shape[1],) ): - raise AssertionError("Shape mismatch of s0 and scales") + raise AssertionError('Shape mismatch of s0 and scales') return scales return checked_fun -def warn_obs_off(off_ixs: np.ndarray, s_ids: List[str]): +def warn_obs_off(off_ixs: np.ndarray, s_ids: list[str]): """Raise warnings for features with high bias to the samples. Parameters @@ -77,11 +77,11 @@ def warn_obs_off(off_ixs: np.ndarray, s_ids: List[str]): off_ixs = np.asarray(off_ixs, dtype=int) if len(off_ixs) > 0: off_ix_ids = [s_ids[ix] for ix in off_ixs] - logger.info(f"Features {off_ix_ids} (ixs={off_ixs}) have a high bias.") + logger.info(f'Features {off_ix_ids} (ixs={off_ixs}) have a high bias.') @check_io -def median_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def median_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """ Calculate the sample `median absolute deviation (MAD) `_ @@ -95,7 +95,7 @@ def median_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: @check_io -def mean_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def mean_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """ Calculate the mean absolute deviation from the mean. """ @@ -104,7 +104,7 @@ def mean_absolute_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: @check_io -def standard_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def standard_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """ Calculate the sample `standard deviation (SD) `_. @@ -117,7 +117,7 @@ def standard_deviation(*, samples: np.ndarray, **kwargs) -> np.ndarray: @check_io -def bias(*, samples: np.ndarray, s0: np.ndarray, **kwargs) -> np.ndarray: +def bias(*, samples: np.ndarray, s0: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """Bias of sample to observed value.""" bias = np.nanmean(samples, axis=0) - s0 return bias @@ -128,8 +128,8 @@ def root_mean_square_deviation( *, samples: np.ndarray, s0: np.ndarray, - s_ids: List[str], - **kwargs, + s_ids: list[str], + **kwargs, # noqa: ARG001 ) -> np.ndarray: """ Square root of the mean squared error, i.e. @@ -154,15 +154,15 @@ def std_or_rmsd( *, samples: np.ndarray, s0: np.ndarray, - s_ids: List[str], - **kwargs, + s_ids: list[str], + **kwargs, # noqa: ARG001 ) -> np.ndarray: """Correct std by bias if not too many of the points have bias > std.""" bs = bias(samples=samples, s0=s0) std = standard_deviation(samples=samples) if sum(bs > 2 * std) > 1 / 3 * len(std): - logger.info("Too many high-bias values, correcting only for scale.") + logger.info('Too many high-bias values, correcting only for scale.') return std mse = bs**2 + std**2 @@ -179,7 +179,7 @@ def median_absolute_deviation_to_observation( *, samples: np.ndarray, s0: np.ndarray, - **kwargs, + **kwargs, # noqa: ARG001 ) -> np.ndarray: """Median absolute deviation of samples w.r.t. the observation s0.""" mado = np.nanmedian(np.abs(samples - s0), axis=0) @@ -194,7 +194,7 @@ def mean_absolute_deviation_to_observation( *, samples: np.ndarray, s0: np.ndarray, - **kwargs, + **kwargs, # noqa: ARG001 ) -> np.ndarray: """Mean absolute deviation of samples w.r.t. the observation s0.""" mado = np.nanmean(np.abs(samples - s0), axis=0) @@ -206,8 +206,8 @@ def combined_median_absolute_deviation( *, samples: np.ndarray, s0: np.ndarray, - s_ids: List[str], - **kwargs, + s_ids: list[str], + **kwargs, # noqa: ARG001 ) -> np.ndarray: """ Compute the sum of the median absolute deviations to the @@ -231,15 +231,15 @@ def mad_or_cmad( *, samples: np.ndarray, s0: np.ndarray, - s_ids: List[str], - **kwargs, + s_ids: list[str], + **kwargs, # noqa: ARG001 ) -> np.ndarray: """Correct mad std by mado if not too many of the points have mado > mad.""" mad = median_absolute_deviation(samples=samples) mado = median_absolute_deviation_to_observation(samples=samples, s0=s0) if sum(mado > 2 * mad) > 1 / 3 * len(mad): - logger.info("Too many high-bias values, correcting only for scale.") + logger.info('Too many high-bias values, correcting only for scale.') return mad cmad = mad + mado @@ -258,8 +258,8 @@ def combined_mean_absolute_deviation( *, samples: np.ndarray, s0: np.ndarray, - s_ids: List[str], - **kwargs, + s_ids: list[str], + **kwargs, # noqa: ARG001 ) -> np.ndarray: """ Compute the sum of the mean absolute deviations to the @@ -280,7 +280,7 @@ def standard_deviation_to_observation( *, samples: np.ndarray, s0: np.ndarray, - **kwargs, + **kwargs, # noqa: ARG001 ) -> np.ndarray: """ Standard deviation of absolute deviations of the samples w.r.t. @@ -291,18 +291,18 @@ def standard_deviation_to_observation( @check_io -def span(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def span(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """Compute the difference of largest and smallest sample point.""" return np.nanmax(samples, axis=0) - np.nanmin(samples, axis=0) @check_io -def mean(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def mean(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """Compute the mean.""" return np.nanmean(samples, axis=0) @check_io -def median(*, samples: np.ndarray, **kwargs) -> np.ndarray: +def median(*, samples: np.ndarray, **kwargs) -> np.ndarray: # noqa: ARG001 """Compute the median.""" return np.nanmedian(samples, axis=0) diff --git a/pyabc/distance/util.py b/pyabc/distance/util.py index d7d209dfd..25ffda037 100644 --- a/pyabc/distance/util.py +++ b/pyabc/distance/util.py @@ -1,12 +1,12 @@ import logging import os -from typing import Callable, Dict, List, Sequence +from collections.abc import Callable, Sequence import numpy as np from ..storage import load_dict_from_json, save_dict_to_json -logger = logging.getLogger("ABC.Distance") +logger = logging.getLogger('ABC.Distance') def bound_weights( @@ -44,8 +44,8 @@ def bound_weights( def log_weights( t: int, - weights: Dict[int, np.ndarray], - keys: List[str], + weights: dict[int, np.ndarray], + keys: list[str], label: str, log_file: str, ) -> None: @@ -73,8 +73,8 @@ def log_weights( # add column if t in dct: logger.warning( - f"Time {t} already in log file {log_file}. " - "Overwriting, but this looks suspicious.", + f'Time {t} already in log file {log_file}. ' + 'Overwriting, but this looks suspicious.', ) dct[t] = weights # save to file @@ -158,7 +158,7 @@ def fd_nabla1_multi_delta( delta_opt = np.array([test_deltas[ix] for ix in min_ixs]) # log - logger.debug(f"Optimal FD delta: {delta_opt}") + logger.debug(f'Optimal FD delta: {delta_opt}') return fd_nabla_1(x=x, fun=fun, delta_vec=delta_opt) diff --git a/pyabc/epsilon/base.py b/pyabc/epsilon/base.py index 72b9e3db6..93f915e3d 100644 --- a/pyabc/epsilon/base.py +++ b/pyabc/epsilon/base.py @@ -1,6 +1,6 @@ import json from abc import ABC, abstractmethod -from typing import Callable, List +from collections.abc import Callable import numpy as np import pandas as pd @@ -14,11 +14,11 @@ class Epsilon(ABC): each new population. """ - def initialize( + def initialize( # noqa: B027 self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], max_nr_populations: int, acceptor_config: dict, ): @@ -45,7 +45,7 @@ def initialize( """ pass - def configure_sampler(self, sampler): + def configure_sampler(self, sampler): # noqa: B027 """ This is called by the ABCSMC class and gives the epsilon the opportunity to configure the sampler. @@ -64,11 +64,11 @@ def configure_sampler(self, sampler): The sampler used in ABCSMC. """ - def update( + def update( # noqa: B027 self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], acceptance_rate: float, acceptor_config: dict, ): @@ -137,7 +137,7 @@ def get_config(self): config: dict Dictionary describing the distance function. """ - return {"name": self.__class__.__name__} + return {'name': self.__class__.__name__} def to_json(self): """ @@ -162,5 +162,5 @@ class NoEpsilon(Epsilon): acceptance threshold. """ - def __call__(self, t: int) -> float: + def __call__(self, t: int) -> float: # noqa: ARG002 return np.nan diff --git a/pyabc/epsilon/epsilon.py b/pyabc/epsilon/epsilon.py index b5394dc99..14f78739e 100644 --- a/pyabc/epsilon/epsilon.py +++ b/pyabc/epsilon/epsilon.py @@ -1,5 +1,5 @@ import logging -from typing import Callable, List, Union +from collections.abc import Callable import numpy as np import pandas as pd @@ -7,7 +7,7 @@ from ..weighted_statistics import weighted_quantile from .base import Epsilon -logger = logging.getLogger("ABC.Epsilon") +logger = logging.getLogger('ABC.Epsilon') class ConstantEpsilon(Epsilon): @@ -28,10 +28,10 @@ def __init__(self, constant_epsilon_value: float): def get_config(self): config = super().get_config() - config["constant_epsilon_value"] = self.constant_epsilon_value + config['constant_epsilon_value'] = self.constant_epsilon_value return config - def __call__(self, t: int): + def __call__(self, t: int): # noqa: ARG002 return self.constant_epsilon_value @@ -47,13 +47,13 @@ class ListEpsilon(Epsilon): ``values[t]`` is the value for population t. """ - def __init__(self, values: List[float]): + def __init__(self, values: list[float]): super().__init__() self.epsilon_values = list(values) def get_config(self): config = super().get_config() - config["epsilon_values"] = self.epsilon_values + config['epsilon_values'] = self.epsilon_values return config def __call__(self, t: int): @@ -102,7 +102,7 @@ class QuantileEpsilon(Epsilon): def __init__( self, - initial_epsilon: Union[str, int, float] = 'from_sample', + initial_epsilon: str | int | float = 'from_sample', alpha: float = 0.5, quantile_multiplier: float = 1, weighted: bool = True, @@ -115,16 +115,16 @@ def __init__( self._look_up = {} if self.alpha > 1 or self.alpha <= 0: - raise ValueError("It must be 0 < alpha <= 1") + raise ValueError('It must be 0 < alpha <= 1') def get_config(self): config = super().get_config() config.update( { - "initial_epsilon": self._initial_epsilon, - "alpha": self.alpha, - "quantile_multiplier": self.quantile_multiplier, - "weighted": self.weighted, + 'initial_epsilon': self._initial_epsilon, + 'alpha': self.alpha, + 'quantile_multiplier': self.quantile_multiplier, + 'weighted': self.weighted, } ) @@ -140,9 +140,9 @@ def initialize( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, - acceptor_config: dict, + get_all_records: Callable[[], list[dict]], # noqa: ARG002 + max_nr_populations: int, # noqa: ARG002 + acceptor_config: dict, # noqa: ARG002 ): if not self.requires_calibration(): # safety check in __call__ @@ -155,7 +155,7 @@ def initialize( self._update(t, weighted_distances) # logging - logger.debug(f"Initial eps: {self._look_up[t]:.4e}") + logger.debug(f'Initial eps: {self._look_up[t]:.4e}') def __call__(self, t: int) -> float: """ @@ -175,8 +175,8 @@ def __call__(self, t: int) -> float: eps = self._look_up[t] except KeyError as e: raise KeyError( - f"The epsilon value for time {t} does not exist: {repr(e)}" - ) + f'The epsilon value for time {t} does not exist: {repr(e)}' + ) from None return eps @@ -187,9 +187,9 @@ def update( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - acceptance_rate: float, - acceptor_config: dict, + get_all_records: Callable[[], list[dict]], # noqa: ARG002 + acceptance_rate: float, # noqa: ARG002 + acceptor_config: dict, # noqa: ARG002 ): """ Compute quantile of the (weighted) distances given in population, @@ -202,7 +202,7 @@ def update( self._update(t, weighted_distances) # logger - logger.debug(f"New eps, t={t}, eps={self._look_up[t]}") + logger.debug(f'New eps, t={t}, eps={self._look_up[t]}') def _update( self, @@ -241,7 +241,7 @@ class MedianEpsilon(QuantileEpsilon): def __init__( self, - initial_epsilon: Union[str, int, float] = 'from_sample', + initial_epsilon: str | int | float = 'from_sample', median_multiplier: float = 1, weighted: bool = True, ): diff --git a/pyabc/epsilon/silk.py b/pyabc/epsilon/silk.py index 1ce5c306c..8d50cbf65 100644 --- a/pyabc/epsilon/silk.py +++ b/pyabc/epsilon/silk.py @@ -1,7 +1,7 @@ """Acceptance rate based optimal threshold.""" import logging -from typing import Callable, Dict, List +from collections.abc import Callable import numpy as np import pandas as pd @@ -15,7 +15,7 @@ from .base import Epsilon -logger = logging.getLogger("ABC.Epsilon") +logger = logging.getLogger('ABC.Epsilon') class SilkOptimalEpsilon(Epsilon): @@ -73,21 +73,21 @@ def __init__( """ if hessian is None: raise ImportError( - "Install autograd, e.g. via `pip install pyabc[autograd]`" + 'Install autograd, e.g. via `pip install pyabc[autograd]`' ) super().__init__() self.min_rate: float = min_rate self.k: float = k - self.eps: Dict[int, float] = {} + self.eps: dict[int, float] = {} def initialize( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, - acceptor_config: dict, + get_all_records: Callable[[], list[dict]], + max_nr_populations: int, # noqa: ARG002 + acceptor_config: dict, # noqa: ARG002 ): self._update( get_weighted_distances=get_weighted_distances, @@ -99,9 +99,9 @@ def update( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - acceptance_rate: float, - acceptor_config: dict, + get_all_records: Callable[[], list[dict]], + acceptance_rate: float, # noqa: ARG002 + acceptor_config: dict, # noqa: ARG002 ): self._update( get_weighted_distances=get_weighted_distances, @@ -116,7 +116,7 @@ def configure_sampler(self, sampler): def _update( self, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], t: int, ): # extract accepted particles @@ -165,9 +165,9 @@ def acc_rate(eps: float, k: float = self.k): acc_rate_opt = acc_rate(eps_opt) logger.info( - f"Optimal threshold for t={t}: eps={eps_opt:.4e}, " - f"estimated rate={acc_rate_opt:.4e} " - f"(discontinuous={acc_rate(eps_opt, k=np.inf):.4e})" + f'Optimal threshold for t={t}: eps={eps_opt:.4e}, ' + f'estimated rate={acc_rate_opt:.4e} ' + f'(discontinuous={acc_rate(eps_opt, k=np.inf):.4e})' ) # use value if acceptance rate high enough or value high enough @@ -180,9 +180,9 @@ def acc_rate(eps: float, k: float = self.k): ub=dist_max, ) logger.info( - f"Overriding via trade-off: eps={the_eps}, " - f"estimated rate={acc_rate(the_eps)} " - f"(discontinuous={acc_rate(the_eps, k=np.inf):.4e})" + f'Overriding via trade-off: eps={the_eps}, ' + f'estimated rate={acc_rate(the_eps)} ' + f'(discontinuous={acc_rate(the_eps, k=np.inf):.4e})' ) self.eps[t] = the_eps @@ -213,7 +213,7 @@ def optimal_eps_from_second_order( ret = optimize.minimize_scalar( lambda x: -hess(x), bounds=(0, ub), - method="bounded", + method='bounded', ) eps_opt = ret.x @@ -246,7 +246,7 @@ def obj(eps): ret = optimize.minimize_scalar( obj, bounds=(0, ub), - method="bounded", + method='bounded', ) eps = ret.x diff --git a/pyabc/epsilon/temperature.py b/pyabc/epsilon/temperature.py index 8d85f1e67..8a48717f2 100644 --- a/pyabc/epsilon/temperature.py +++ b/pyabc/epsilon/temperature.py @@ -1,6 +1,6 @@ import logging import numbers -from typing import Callable, List, Union +from collections.abc import Callable import numpy as np import pandas as pd @@ -11,7 +11,7 @@ from ..storage import save_dict_to_json from .base import Epsilon -logger = logging.getLogger("ABC.Epsilon") +logger = logging.getLogger('ABC.Epsilon') class TemperatureBase(Epsilon): @@ -34,7 +34,7 @@ class ListTemperature(TemperatureBase): For exact inference, finish with 1. """ - def __init__(self, values: List[float]): + def __init__(self, values: list[float]): self.values = values def __call__(self, t: int) -> float: @@ -80,8 +80,8 @@ class Temperature(TemperatureBase): def __init__( self, - schemes: Union[Callable, List[Callable]] = None, - aggregate_fun: Callable[[List[float]], float] = None, + schemes: Callable | list[Callable] = None, + aggregate_fun: Callable[[list[float]], float] = None, initial_temperature: float = None, enforce_exact_final_temperature: bool = True, log_file: str = None, @@ -120,7 +120,7 @@ def initialize( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], max_nr_populations: int, acceptor_config: dict, ): @@ -156,7 +156,7 @@ def update( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], acceptance_rate: float, acceptor_config: dict, ): @@ -173,7 +173,7 @@ def _update( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], acceptance_rate: float, acceptor_config, ): @@ -206,7 +206,7 @@ def _update( temps = [self.initial_temperature] else: raise ValueError( - "Initial temperature must be a float or a callable" + 'Initial temperature must be a float or a callable' ) else: # evaluate schemes @@ -217,20 +217,18 @@ def _update( # compute next temperature based on proposals and fallback # should not be higher than before - fallback = ( - self.temperatures[t - 1] if t - 1 in self.temperatures else np.inf - ) + fallback = self.temperatures.get(t - 1, np.inf) temperature = self.aggregate_fun(temps) # also a value lower than 1.0 does not make sense temperature = max(min(temperature, fallback), 1.0) if not np.isfinite(temperature): - raise ValueError("Temperature must be finite.") + raise ValueError('Temperature must be finite.') # record found value self.temperatures[t] = temperature # logging - logger.debug(f"Proposed temperatures for {t}: {temps}.") + logger.debug(f'Proposed temperatures for {t}: {temps}.') self.temperature_proposals[t] = temps if self.log_file: save_dict_to_json(self.temperature_proposals, self.log_file) @@ -256,7 +254,7 @@ def __call__( self, t: int, get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_all_records: Callable[[], list[dict]], max_nr_populations: int, pdf_norm: float, kernel_scale: str, @@ -328,13 +326,13 @@ def configure_sampler(self, sampler): def __call__( self, - t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, + t: int, # noqa: ARG002 + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], + max_nr_populations: int, # noqa: ARG002 pdf_norm: float, kernel_scale: str, - prev_temperature: float, + prev_temperature: float, # noqa: ARG002 acceptance_rate: float, ): # check minimum rate @@ -398,7 +396,7 @@ def obj(b): b_opt = 0 elif obj(min_b) < 0: # it is obj(-inf) > 0 always - logger.info("AcceptanceRateScheme: Numerics limit temperature.") + logger.info('AcceptanceRateScheme: Numerics limit temperature.') b_opt = min_b else: # perform binary search @@ -448,19 +446,19 @@ def __init__(self): def __call__( self, t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], # noqa: ARG002 max_nr_populations: int, - pdf_norm: float, - kernel_scale: str, + pdf_norm: float, # noqa: ARG002 + kernel_scale: str, # noqa: ARG002 prev_temperature: float, - acceptance_rate: float, + acceptance_rate: float, # noqa: ARG002 ): # needs a finite number of iterations if max_nr_populations == np.inf: raise ValueError( - "The ExpDecayFixedIterScheme requires a finite " - "`max_nr_populations`." + 'The ExpDecayFixedIterScheme requires a finite ' + '`max_nr_populations`.' ) # needs a starting temperature @@ -523,11 +521,11 @@ def __init__( def __call__( self, t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, - pdf_norm: float, - kernel_scale: str, + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], # noqa: ARG002 + max_nr_populations: int, # noqa: ARG002 + pdf_norm: float, # noqa: ARG002 + kernel_scale: str, # noqa: ARG002 prev_temperature: float, acceptance_rate: float, ): @@ -540,13 +538,13 @@ def __call__( # check if acceptance rate criterion violated if acceptance_rate > self.max_rate and t > 1: logger.debug( - "ExpDecayFixedRatioScheme: " - "Reacting to high acceptance rate." + 'ExpDecayFixedRatioScheme: ' + 'Reacting to high acceptance rate.' ) alpha = max(alpha / 2, alpha - (1 - alpha) * 2) if acceptance_rate < self.min_rate: logger.debug( - "ExpDecayFixedRatioScheme: " "Reacting to low acceptance rate." + 'ExpDecayFixedRatioScheme: ' 'Reacting to low acceptance rate.' ) # increase alpha alpha = alpha + (1 - alpha) / 2 @@ -589,13 +587,13 @@ def __init__(self, exponent: float = 3): def __call__( self, t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], # noqa: ARG002 max_nr_populations: int, - pdf_norm: float, - kernel_scale: str, + pdf_norm: float, # noqa: ARG002 + kernel_scale: str, # noqa: ARG002 prev_temperature: float, - acceptance_rate: float, + acceptance_rate: float, # noqa: ARG002 ): # needs a starting temperature # if not available, return infinite temperature @@ -608,8 +606,8 @@ def __call__( # check if we can compute a decay step if max_nr_populations == np.inf: raise ValueError( - "Can only perform PolynomialDecayScheme step " - "with a finite max_nr_populations." + 'Can only perform PolynomialDecayScheme step ' + 'with a finite max_nr_populations.' ) # how many steps left? @@ -622,7 +620,7 @@ def __call__( ) logger.debug( - f"Temperatures proposed by polynomial decay method: " f"{temps}." + f'Temperatures proposed by polynomial decay method: ' f'{temps}.' ) # pre-last step is the next step @@ -663,11 +661,11 @@ def __init__(self, alpha: float = 0.5, min_rate: float = 1e-4): def __call__( self, t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, - pdf_norm: float, - kernel_scale: str, + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], # noqa: ARG002 + max_nr_populations: int, # noqa: ARG002 + pdf_norm: float, # noqa: ARG002 + kernel_scale: str, # noqa: ARG002 prev_temperature: float, acceptance_rate: float, ): @@ -689,7 +687,7 @@ def __call__( k_base = self.k[t - 1] if acceptance_rate < self.min_rate: - logger.debug("DalyScheme: Reacting to low acceptance rate.") + logger.debug('DalyScheme: Reacting to low acceptance rate.') # reduce reduction k_base = self.alpha * k_base @@ -712,13 +710,13 @@ class FrielPettittScheme(TemperatureScheme): def __call__( self, t: int, - get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], + get_weighted_distances: Callable[[], pd.DataFrame], # noqa: ARG002 + get_all_records: Callable[[], list[dict]], # noqa: ARG002 max_nr_populations: int, - pdf_norm: float, - kernel_scale: str, + pdf_norm: float, # noqa: ARG002 + kernel_scale: str, # noqa: ARG002 prev_temperature: float, - acceptance_rate: float, + acceptance_rate: float, # noqa: ARG002 ): # needs a starting temperature # if not available, return infinite temperature @@ -728,8 +726,8 @@ def __call__( # check if we can compute a decay step if max_nr_populations == np.inf: raise ValueError( - "Can only perform FrielPettittScheme step with a " - "finite max_nr_populations." + 'Can only perform FrielPettittScheme step with a ' + 'finite max_nr_populations.' ) # base temperature @@ -760,14 +758,14 @@ def __init__(self, target_relative_ess: float = 0.8): def __call__( self, - t: int, + t: int, # noqa: ARG002 get_weighted_distances: Callable[[], pd.DataFrame], - get_all_records: Callable[[], List[dict]], - max_nr_populations: int, + get_all_records: Callable[[], list[dict]], # noqa: ARG002 + max_nr_populations: int, # noqa: ARG002 pdf_norm: float, kernel_scale: str, prev_temperature: float, - acceptance_rate: float, + acceptance_rate: float, # noqa: ARG002 ): # execute function (expensive if in calibration) df = get_weighted_distances() @@ -786,10 +784,7 @@ def __call__( target_ess = len(weights) * self.target_relative_ess - if prev_temperature is None: - beta_base = 0.0 - else: - beta_base = 1.0 / prev_temperature + beta_base = 0.0 if prev_temperature is None else 1.0 / prev_temperature # objective to minimize def obj(beta): diff --git a/pyabc/external/base.py b/pyabc/external/base.py index 66764e0d1..62848851a 100644 --- a/pyabc/external/base.py +++ b/pyabc/external/base.py @@ -3,7 +3,6 @@ import os import subprocess # noqa: S404 import tempfile -from typing import List import numpy as np import pandas as pd @@ -12,14 +11,14 @@ from ..parameters import Parameter from .utils import timethis -logger = logging.getLogger("ABC.External") +logger = logging.getLogger('ABC.External') # timeout error code TIMEOUT: int = -15 # location key -LOC: str = "loc" +LOC: str = 'loc' # returncode key -RETURNCODE: str = "returncode" +RETURNCODE: str = 'returncode' class ExternalHandler: @@ -33,7 +32,7 @@ def __init__( self, executable: str, file: str = None, - fixed_args: List = None, + fixed_args: list = None, create_folder: bool = False, suffix: str = None, prefix: str = None, @@ -108,10 +107,10 @@ def create_executable(self, loc): Replaces instances of {loc} by the location `loc`. """ - executable = self.executable.replace("{loc}", loc) + executable = self.executable.replace('{loc}', loc) return executable - def run(self, args: List[str] = None, cmd: str = None, loc: str = None): + def run(self, args: list[str] = None, cmd: str = None, loc: str = None): """Run the script for the given arguments. Parameters @@ -130,12 +129,12 @@ def run(self, args: List[str] = None, cmd: str = None, loc: str = None): loc = self.create_loc() # redirect output - devnull = open(os.devnull, 'w') stdout = stderr = {} - if not self.show_stdout: - stdout = {'stdout': devnull} - if not self.show_stderr: - stderr = {'stderr': devnull} + with open(os.devnull, 'w') as devnull: + if not self.show_stdout: + stdout = {'stdout': devnull} + if not self.show_stderr: + stderr = {'stderr': devnull} # call try: @@ -161,13 +160,13 @@ def run(self, args: List[str] = None, cmd: str = None, loc: str = None): **stderr, timeout=self.timeout, ) - returncode, msg = status.returncode, "" + returncode, msg = status.returncode, '' except subprocess.TimeoutExpired as e: returncode, msg = TIMEOUT, str(e) if returncode: msg = ( - f"Simulation error for arguments {args}: " - f"returncode {returncode}, msg={msg}." + f'Simulation error for arguments {args}: ' + f'returncode {returncode}, msg={msg}.' ) if self.raise_on_error: raise ValueError(msg) @@ -201,16 +200,16 @@ def __init__( self, executable: str, file: str, - fixed_args: List = None, + fixed_args: list = None, create_folder: bool = False, suffix: str = None, - prefix: str = "modelsim_", + prefix: str = 'modelsim_', dir: str = None, show_stdout: bool = False, show_stderr: bool = True, raise_on_error: bool = False, timeout: float = None, - name: str = "ExternalModel", + name: str = 'ExternalModel', ): """Initialize the model. @@ -239,7 +238,7 @@ def __init__( def __call__(self, pars: Parameter): args = [] for key, val in pars.items(): - args.append(f"{key}={val} ") + args.append(f'{key}={val} ') return self.eh.run(args) def sample(self, pars): @@ -341,10 +340,10 @@ def __init__( self, executable: str, file: str, - fixed_args: List = None, + fixed_args: list = None, create_folder: bool = False, suffix: str = None, - prefix: str = "sumstat_", + prefix: str = 'sumstat_', dir: str = None, show_stdout: bool = False, show_stderr: bool = True, @@ -370,7 +369,7 @@ def __call__(self, model_output): Create summary statistics from the `model_output` generated by the model. """ - args = [f"model_output={model_output[LOC]}"] + args = [f'model_output={model_output[LOC]}'] return self.eh.run(args=args) @@ -390,9 +389,9 @@ def __init__( self, executable: str, file: str, - fixed_args: List = None, + fixed_args: list = None, suffix: str = None, - prefix: str = "dist_", + prefix: str = 'dist_', dir: str = None, show_stdout: bool = False, show_stderr: bool = True, @@ -418,8 +417,8 @@ def __call__(self, sumstat_0, sumstat_1): if sumstat_0[RETURNCODE] or sumstat_1[RETURNCODE]: return np.nan args = [ - f"sumstat_0={sumstat_0[LOC]}", - f"sumstat_1={sumstat_1[LOC]}", + f'sumstat_0={sumstat_0[LOC]}', + f'sumstat_1={sumstat_1[LOC]}', ] ret = self.eh.run(args) # read in distance diff --git a/pyabc/external/julia/jl_pyjulia.py b/pyabc/external/julia/jl_pyjulia.py index a1c8d640d..df01ebf73 100644 --- a/pyabc/external/julia/jl_pyjulia.py +++ b/pyabc/external/julia/jl_pyjulia.py @@ -1,6 +1,7 @@ """Interface to Julia via PyJulia.""" -from typing import Any, Callable, Dict, Union +from collections.abc import Callable +from typing import Any import numpy as np import pandas as pd @@ -12,8 +13,8 @@ def _dict_vars2py( - dct: Dict[str, Any] -) -> Dict[str, Union[np.ndarray, pd.Series, pd.DataFrame]]: + dct: dict[str, Any], +) -> dict[str, np.ndarray | pd.Series | pd.DataFrame]: """Convert non-pandas dictionary entries to numpy arrays. Parameters @@ -25,7 +26,7 @@ def _dict_vars2py( dct: The same dictionary with modified values. """ for key, val in dct.items(): - if not isinstance(val, (pd.DataFrame, pd.Series)): + if not isinstance(val, pd.DataFrame | pd.Series): dct[key] = np.asarray(val) return dct @@ -42,7 +43,7 @@ def _read_source(module_name: str, source_file: str) -> None: Main.include(source_file) -class _JlWrap(object): +class _JlWrap: """Wrapper around Julia object. This in particular makes the objects pickleable, by reconstruction from @@ -63,9 +64,9 @@ def _get_callable(self): def __getstate__(self): return { - "module_name": self.module_name, - "source_file": self.source_file, - "function_name": self.function_name, + 'module_name': self.module_name, + 'source_file': self.source_file, + 'function_name': self.function_name, } def __setstate__(self, d): @@ -77,8 +78,8 @@ def __setstate__(self, d): @property def __name__(self): return ( - f"{self.__class__.__name__}_{self.function_name}_" - f"{self.module_name}_{self.source_file}" + f'{self.__class__.__name__}_{self.function_name}_' + f'{self.module_name}_{self.source_file}' ) @@ -157,16 +158,16 @@ class Julia: def __init__(self, module_name: str, source_file: str = None): if Main is None: raise ImportError( - "Install PyJulia, e.g. via `pip install pyabc[julia]`, " - "and see the class documentation", + 'Install PyJulia, e.g. via `pip install pyabc[julia]`, ' + 'and see the class documentation', ) self.module_name: str = module_name if source_file is None: - source_file = module_name + ".jl" + source_file = module_name + '.jl' self.source_file: str = source_file _read_source(self.module_name, self.source_file) - def model(self, name: str = "model"): + def model(self, name: str = 'model'): """Get a wrapped Julia model callable. Parameters @@ -179,7 +180,7 @@ def model(self, name: str = "model"): function_name=name, ) - def observation(self, name: str = "observation"): + def observation(self, name: str = 'observation'): """Get the observed data from Julia. Parameters @@ -190,7 +191,7 @@ def observation(self, name: str = "observation"): observation = _dict_vars2py(observation) return observation - def distance(self, name: str = "distance") -> Callable: + def distance(self, name: str = 'distance') -> Callable: """Get the distance function from Julia. Parameters diff --git a/pyabc/inference/smc.py b/pyabc/inference/smc.py index 5edf27add..7185bbe2a 100644 --- a/pyabc/inference/smc.py +++ b/pyabc/inference/smc.py @@ -2,8 +2,9 @@ import copy import logging +from collections.abc import Callable from datetime import datetime, timedelta -from typing import Callable, List, Tuple, TypeVar, Union +from typing import TypeVar import numpy as np @@ -44,9 +45,9 @@ ) from ..weighted_statistics import effective_sample_size -logger = logging.getLogger("ABC") +logger = logging.getLogger('ABC') -model_output = TypeVar("model_output") +model_output = TypeVar('model_output') def identity(x): @@ -56,7 +57,7 @@ def identity(x): def run_cleanup(run): """Wrapper: Run and in any case clean up afterwards.""" - def wrapped_run(self: "ABCSMC", *args, **kwargs): + def wrapped_run(self: 'ABCSMC', *args, **kwargs): try: # the actual run ret = run(self, *args, **kwargs) @@ -170,14 +171,14 @@ class ABCSMC: def __init__( self, - models: Union[List[Model], Model, Callable], - parameter_priors: Union[List[Distribution], Distribution, Callable], - distance_function: Union[Distance, Callable] = None, - population_size: Union[PopulationStrategy, int] = 100, + models: list[Model] | Model | Callable, + parameter_priors: list[Distribution] | Distribution | Callable, + distance_function: Distance | Callable = None, + population_size: PopulationStrategy | int = 100, summary_statistics: Callable[[model_output], dict] = identity, model_prior: RV = None, model_perturbation_kernel: ModelPerturbationKernel = None, - transitions: Union[List[Transition], Transition] = None, + transitions: list[Transition] | Transition = None, eps: Epsilon = None, sampler: Sampler = None, acceptor: Acceptor = None, @@ -196,7 +197,7 @@ def __init__( # sanity checks if len(self.models) != len(self.parameter_priors): raise AssertionError( - "Number models and number parameter priors have to agree." + 'Number models and number parameter priors have to agree.' ) if distance_function is None: @@ -208,7 +209,7 @@ def __init__( self.summary_statistics = summary_statistics if model_prior is None: - model_prior = RV("randint", 0, len(self.models)) + model_prior = RV('randint', 0, len(self.models)) self.model_prior = model_prior if model_perturbation_kernel is None: @@ -221,7 +222,7 @@ def __init__( transitions = [MultivariateNormalTransition() for _ in self.models] if not isinstance(transitions, list): transitions = [transitions] - self.transitions: List[Transition] = transitions + self.transitions: list[Transition] = transitions if eps is None: eps = MedianEpsilon(median_multiplier=1) @@ -269,9 +270,9 @@ def _sanity_check(self): # check if usage is consistent if not all(stochastics) and any(stochastics): raise ValueError( - "Please only use acceptor.StochasticAcceptor, " - "epsilon.TemperatureBase and distance.StochasticKernel " - "together." + 'Please only use acceptor.StochasticAcceptor, ' + 'epsilon.TemperatureBase and distance.StochasticKernel ' + 'together.' ) # check sampler @@ -499,7 +500,7 @@ def get_initial_records(): def _get_initial_population( self, t: int - ) -> Tuple[List[float], List[dict]]: + ) -> tuple[list[float], list[dict]]: """ Get initial samples, either from the last population stored in history, or via sampling sum stats from the prior. This can be used to calibrate @@ -547,7 +548,7 @@ def _sample_from_prior(self, t: int) -> Population: # create simulate function simulate_one = self._create_simulate_from_prior_function() - logger.info(f"Calibration sample t = {t}.") + logger.info(f'Calibration sample t = {t}.') # call sampler sample = self.sampler.sample_until_n_accepted( @@ -700,9 +701,9 @@ def run( ret = self.run_generation(t=t) # check whether to discontinue - if not ret["successful"] or self.check_terminate( + if not ret['successful'] or self.check_terminate( t=t, - acceptance_rate=ret["acceptance_rate"], + acceptance_rate=ret['acceptance_rate'], ): break @@ -795,9 +796,9 @@ def run_generation( current_eps = self.eps(t) if current_eps is None or np.isnan(current_eps): raise ValueError( - f"The epsilon threshold {current_eps} is invalid." + f'The epsilon threshold {current_eps} is invalid.' ) - logger.info(f"t: {t}, eps: {current_eps:.8e}.") + logger.info(f't: {t}, eps: {current_eps:.8e}.') # create simulate function simulate_one = self._create_simulate_function(t) @@ -811,7 +812,7 @@ def run_generation( ) # perform the sampling - logger.debug(f"Submitting population {t}.") + logger.debug(f'Submitting population {t}.') sample = self.sampler.sample_until_n_accepted( n=pop_size, simulate_one=simulate_one, @@ -822,9 +823,9 @@ def run_generation( # check sample health if not sample.ok: - logger.info("Stopping: sample not ok.") + logger.info('Stopping: sample not ok.') return { - "successful": False, + 'successful': False, } # normalize accepted population weight to 1 @@ -832,7 +833,7 @@ def run_generation( # retrieve accepted population population = sample.get_accepted_population() - logger.debug(f"Population {t} done.") + logger.debug(f'Population {t} done.') # save to database n_sim = self.sampler.nr_evaluations_ @@ -841,8 +842,8 @@ def run_generation( t, current_eps, population, n_sim, model_names ) logger.debug( - f"Total samples up to t = {t}: " - f"{self.history.total_nr_simulations}." + f'Total samples up to t = {t}: ' + f'{self.history.total_nr_simulations}.' ) # acceptance rate and ess @@ -850,8 +851,8 @@ def run_generation( acceptance_rate = pop_size / n_sim ess = effective_sample_size(population.get_weighted_distances()['w']) logger.info( - f"Accepted: {pop_size} / {n_sim} = " - f"{acceptance_rate:.4e}, ESS: {ess:.4e}." + f'Accepted: {pop_size} / {n_sim} = ' + f'{acceptance_rate:.4e}, ESS: {ess:.4e}.' ) # prepare next iteration @@ -863,8 +864,8 @@ def run_generation( ) return { - "successful": True, - "acceptance_rate": acceptance_rate, + 'successful': True, + 'acceptance_rate': acceptance_rate, } def check_terminate( @@ -884,7 +885,7 @@ def check_terminate( terminate: Whether to terminate (True) or not (False). """ # check termination criteria - if termination_criteria_fulfilled( + return termination_criteria_fulfilled( current_eps=self.eps(t=t), min_eps=self.minimum_epsilon, prev_eps=eps_from_hist(history=self.history, t=t - 1), @@ -900,9 +901,7 @@ def check_terminate( max_walltime=self.max_walltime, t=t, max_t=self.max_t, - ): - return True - return False + ) def _prepare_next_iteration( self, @@ -1014,7 +1013,7 @@ def _adapt_population_size(self, t): # model probabilities w = self.history.get_model_probabilities(self.history.max_t)[ - "p" + 'p' ].values # make a copy in case the population strategy messes with diff --git a/pyabc/inference_util/inference_util.py b/pyabc/inference_util/inference_util.py index 61a040763..357100be4 100644 --- a/pyabc/inference_util/inference_util.py +++ b/pyabc/inference_util/inference_util.py @@ -2,8 +2,8 @@ import logging import uuid +from collections.abc import Callable from datetime import datetime, timedelta -from typing import Callable, List import numpy as np import pandas as pd @@ -19,7 +19,7 @@ from ..storage.history import History from ..transition import ModelPerturbationKernel, Transition -logger = logging.getLogger("ABC") +logger = logging.getLogger('ABC') class AnalysisVars: @@ -31,10 +31,10 @@ class AnalysisVars: def __init__( self, model_prior: RV, - parameter_priors: List[Distribution], + parameter_priors: list[Distribution], model_perturbation_kernel: ModelPerturbationKernel, - transitions: List[Transition], - models: List[Model], + transitions: list[Transition], + models: list[Model], summary_statistics: Callable, x_0: dict, distance_function: Distance, @@ -75,8 +75,8 @@ def __init__( def create_simulate_from_prior_function( model_prior: RV, - parameter_priors: List[Distribution], - models: List[Model], + parameter_priors: list[Distribution], + models: list[Model], summary_statistics: Callable, ) -> Callable: """Create a function that simulates from the prior. @@ -134,9 +134,9 @@ def generate_valid_proposal( m: np.ndarray, p: np.ndarray, model_prior: RV, - parameter_priors: List[Distribution], + parameter_priors: list[Distribution], model_perturbation_kernel: ModelPerturbationKernel, - transitions: List[Transition], + transitions: list[Transition], ): """Sample a parameter for a model. @@ -188,8 +188,8 @@ def generate_valid_proposal( n_sample += 1 if n_sample == n_sample_soft_limit: logger.warning( - "Unusually many (model, parameter) samples have prior " - "density zero. The transition might be inappropriate." + 'Unusually many (model, parameter) samples have prior ' + 'density zero. The transition might be inappropriate.' ) @@ -197,7 +197,7 @@ def evaluate_proposal( m_ss: int, theta_ss: Parameter, t: int, - models: List[Model], + models: list[Model], summary_statistics: Callable, distance_function: Distance, eps: Epsilon, @@ -253,7 +253,7 @@ def evaluate_proposal( def create_prior_pdf( - model_prior: RV, parameter_priors: List[Distribution] + model_prior: RV, parameter_priors: list[Distribution] ) -> Callable: """Create a function that calculates a sample's prior density. @@ -275,7 +275,7 @@ def prior_pdf(m_ss, theta_ss): def create_transition_pdf( - transitions: List[Transition], + transitions: list[Transition], model_probabilities: pd.DataFrame, model_perturbation_kernel: ModelPerturbationKernel, ) -> Callable: @@ -302,7 +302,7 @@ def transition_pdf(m_ss, theta_ss): transition_pd = model_factor * particle_factor if transition_pd == 0: - logger.debug("Transition density is zero!") + logger.debug('Transition density is zero!') return transition_pd return transition_pdf @@ -353,10 +353,10 @@ def create_simulate_function( t: int, model_probabilities: pd.DataFrame, model_perturbation_kernel: ModelPerturbationKernel, - transitions: List[Transition], + transitions: list[Transition], model_prior: RV, - parameter_priors: List[Distribution], - models: List[Model], + parameter_priors: list[Distribution], + models: list[Model], summary_statistics: Callable, x_0: dict, distance_function: Distance, @@ -467,7 +467,7 @@ def only_simulate_data_for_proposal( m_ss: int, theta_ss: Parameter, t: int, - models: List[Model], + models: list[Model], summary_statistics: Callable, weight_function: Callable, proposal_id: int, @@ -517,7 +517,7 @@ def evaluate_preliminary_particle( evaluated_particle: The evaluated particle """ if not particle.preliminary: - raise AssertionError("Particle is not preliminary") + raise AssertionError('Particle is not preliminary') acc_res = ana_vars.acceptor( distance_function=ana_vars.distance_function, @@ -531,10 +531,7 @@ def evaluate_preliminary_particle( # reconstruct weighting function from `weight_function` sampling_weight = particle.weight # the weight is the sampling weight times the acceptance weight(s) - if acc_res.accept: - weight = sampling_weight * acc_res.weight - else: - weight = 0.0 + weight = sampling_weight * acc_res.weight if acc_res.accept else 0.0 # return the evaluated particle return Particle( @@ -587,25 +584,25 @@ def termination_criteria_fulfilled( True if any criterion is met, otherwise False. """ if t >= max_t: - logger.info("Stop: Maximum number of generations.") + logger.info('Stop: Maximum number of generations.') return True if current_eps <= min_eps: - logger.info("Stop: Minimum epsilon.") + logger.info('Stop: Minimum epsilon.') return True if prev_eps is not None and abs(current_eps - prev_eps) < min_eps_diff: - logger.info("Stop: Minimum epsilon difference") + logger.info('Stop: Minimum epsilon difference') return True elif stop_if_single_model_alive and nr_of_models_alive <= 1: - logger.info("Stop: Single model alive.") + logger.info('Stop: Single model alive.') return True elif acceptance_rate < min_acceptance_rate: - logger.info("Stop: Minimum acceptance rate.") + logger.info('Stop: Minimum acceptance rate.') return True elif total_nr_simulations >= max_total_nr_simulations: - logger.info("Stop: Total simulations budget.") + logger.info('Stop: Total simulations budget.') return True elif max_walltime is not None and walltime > max_walltime: - logger.info("Stop: Maximum walltime.") + logger.info('Stop: Maximum walltime.') return True return False @@ -624,4 +621,4 @@ def eps_from_hist(history: History, t: int = None) -> float: return None if t is None: return pops.epsilon.to_numpy()[-1] - return pops.set_index("t").loc[t].epsilon + return pops.set_index('t').loc[t].epsilon diff --git a/pyabc/model/model.py b/pyabc/model/model.py index 8a41a52bd..901c103df 100644 --- a/pyabc/model/model.py +++ b/pyabc/model/model.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, Union +from collections.abc import Callable +from typing import Any from ..acceptor import Acceptor from ..distance import Distance @@ -57,11 +58,11 @@ class Model: analysis for the user as it is stored in the database. """ - def __init__(self, name: str = "Model"): + def __init__(self, name: str = 'Model'): self.name = name def __repr__(self): - return "<{} {}>".format(self.__class__.__name__, self.name) + return f'<{self.__class__.__name__} {self.name}>' def sample(self, pars: Parameter): """ @@ -83,7 +84,10 @@ def sample(self, pars: Parameter): raise NotImplementedError() def summary_statistics( - self, t: int, pars: Parameter, sum_stat_calculator: Callable + self, + t: int, # noqa: ARG002 + pars: Parameter, + sum_stat_calculator: Callable, # noqa: ARG002 ) -> ModelResult: """ Sample, and then calculate the summary statistics. @@ -244,7 +248,7 @@ def sample(self, pars: Parameter): return self.sample_function(pars) @staticmethod - def to_model(maybe_model: Union[Callable, Model]) -> Model: + def to_model(maybe_model: Callable | Model) -> Model: """ Alternative constructor. Accepts either a Model instance or a function and returns always a Model instance. @@ -315,10 +319,10 @@ def accept( self, t: int, pars: Parameter, - sum_stat_calculator: Callable, - distance_calculator: Distance, + sum_stat_calculator: Callable, # noqa: ARG002 + distance_calculator: Distance, # noqa: ARG002 eps_calculator: Epsilon, - acceptor: Acceptor, - x_0: dict, + acceptor: Acceptor, # noqa: ARG002 + x_0: dict, # noqa: ARG002 ): return self.integrated_simulate(pars, eps_calculator(t)) diff --git a/pyabc/petab/amici.py b/pyabc/petab/amici.py index ad0ccd322..23054375f 100644 --- a/pyabc/petab/amici.py +++ b/pyabc/petab/amici.py @@ -4,14 +4,13 @@ import logging import os import tempfile -from collections.abc import Mapping, Sequence -from typing import Callable, Dict, Union +from collections.abc import Callable, Mapping, Sequence import pyabc from .base import PetabImporter, rescale -logger = logging.getLogger("ABC.PEtab") +logger = logging.getLogger('ABC.PEtab') try: import petab.v1 as petab @@ -19,8 +18,8 @@ except ImportError: petab = C = None logger.error( - "Install PEtab (see https://github.com/icb-dcm/petab) to use " - "the petab functionality, e.g. via `pip install pyabc[petab]`" + 'Install PEtab (see https://github.com/icb-dcm/petab) to use ' + 'the petab functionality, e.g. via `pip install pyabc[petab]`' ) try: @@ -30,8 +29,8 @@ except ImportError: amici = amici_petab_import = simulate_petab = LLH = RDATAS = None logger.error( - "Install amici (see https://github.com/icb-dcm/amici) to use " - "the amici functionality, e.g. via `pip install pyabc[amici]`" + 'Install amici (see https://github.com/icb-dcm/amici) to use ' + 'the amici functionality, e.g. via `pip install pyabc[amici]`' ) @@ -62,7 +61,7 @@ def __init__( self.return_simulations = return_simulations self.return_rdatas = return_rdatas - def __call__(self, par: Union[Sequence, Mapping]) -> Mapping: + def __call__(self, par: Sequence | Mapping) -> Mapping: """The model function. Note: The parameters are assumed to be passed on prior scale. @@ -79,7 +78,7 @@ def __call__(self, par: Union[Sequence, Mapping]) -> Mapping: par[key] = val # scale parameters whose priors are not on scale - for key in self.prior_scales.keys(): + for key in self.prior_scales: par[key] = rescale( val=par[key], origin_scale=self.prior_scales[key], @@ -105,7 +104,7 @@ def __call__(self, par: Union[Sequence, Mapping]) -> Mapping: return ret - def __getstate__(self) -> Dict: + def __getstate__(self) -> dict: state = {} for key in set(self.__dict__.keys()) - {'amici_model', 'amici_solver'}: state[key] = self.__dict__[key] @@ -117,8 +116,8 @@ def __getstate__(self) -> Dict: amici.writeSolverSettingsToHDF5(self.amici_solver, _file) except AttributeError as e: e.args += ( - "Pickling the AmiciObjective requires an AMICI " - "installation with HDF5 support.", + 'Pickling the AmiciObjective requires an AMICI ' + 'installation with HDF5 support.', ) raise # read in byte stream @@ -131,7 +130,7 @@ def __getstate__(self) -> Dict: return state - def __setstate__(self, state: Dict): + def __setstate__(self, state: dict): self.__dict__.update(state) model = amici_petab_import.import_petab_problem(self.petab_problem) @@ -149,8 +148,8 @@ def __setstate__(self, state: Dict): if not err.args: err.args = ('',) err.args += ( - "Unpickling an AmiciObjective requires an AMICI " - "installation with HDF5 support.", + 'Unpickling an AmiciObjective requires an AMICI ' + 'installation with HDF5 support.', ) raise finally: @@ -183,8 +182,8 @@ class AmiciPetabImporter(PetabImporter): def __init__( self, petab_problem: petab.Problem, - amici_model: "amici.Model" = None, - amici_solver: "amici.Solver" = None, + amici_model: 'amici.Model' = None, + amici_solver: 'amici.Solver' = None, ): super().__init__(petab_problem=petab_problem) @@ -202,7 +201,7 @@ def create_model( self, return_simulations: bool = False, return_rdatas: bool = False, - ) -> Callable[[Union[Sequence, Mapping]], Mapping]: + ) -> Callable[[Sequence | Mapping], Mapping]: """Create model. Note that since AMICI uses deterministic ODE simulations, @@ -235,7 +234,7 @@ def create_model( if set(self.prior_scales.keys()) != set(x_free_ids): # this should not happen - raise AssertionError("Parameter id mismatch") + raise AssertionError('Parameter id mismatch') # no gradients for pyabc self.amici_solver.setSensitivityOrder(0) @@ -267,7 +266,7 @@ def create_kernel( A pyabc distribution encoding the kernel function. """ - def kernel_fun(x, x_0, t, par) -> float: + def kernel_fun(x, x_0, t, par) -> float: # noqa: ARG001 """The kernel function.""" # the kernel value is computed by amici already return x['llh'] diff --git a/pyabc/petab/base.py b/pyabc/petab/base.py index c8373177a..05d7910de 100644 --- a/pyabc/petab/base.py +++ b/pyabc/petab/base.py @@ -2,16 +2,15 @@ import abc import logging -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from numbers import Number -from typing import Callable, Tuple, Union import numpy as np import pandas as pd import pyabc -logger = logging.getLogger("ABC.PEtab") +logger = logging.getLogger('ABC.PEtab') try: import petab.v1 as petab @@ -19,8 +18,8 @@ except ImportError: petab = C = None logger.error( - "Install PEtab (see https://github.com/icb-dcm/petab) to use " - "the petab functionality, e.g. via `pip install pyabc[petab]`" + 'Install PEtab (see https://github.com/icb-dcm/petab) to use ' + 'the petab functionality, e.g. via `pip install pyabc[petab]`' ) @@ -76,7 +75,7 @@ def create_prior(self) -> pyabc.Distribution: @abc.abstractmethod def create_model( self, - ) -> Callable[[Union[Sequence, Mapping]], Mapping]: + ) -> Callable[[Sequence | Mapping], Mapping]: """Create model. The model takes parameters and simulates data @@ -166,13 +165,13 @@ def get_parameter_names(self, target_scale: str = 'prior'): # scale if target_scale == C.LIN: - target_scales = {key: C.LIN for key in self.prior_scales} + target_scales = dict.fromkeys(self.prior_scales, C.LIN) elif target_scale == 'prior': target_scales = self.prior_scales elif target_scale == 'scaled': target_scales = self.scaled_scales else: - raise ValueError(f"Did not recognize target scale {target_scale}") + raise ValueError(f'Did not recognize target scale {target_scale}') names = {} for _, row in parameter_df.reset_index().iterrows(): @@ -180,15 +179,15 @@ def get_parameter_names(self, target_scale: str = 'prior'): continue key = row[C.PARAMETER_ID] name = str(key) - if C.PARAMETER_NAME in parameter_df: - if not petab.is_empty(row[C.PARAMETER_NAME]): - name = str(row[C.PARAMETER_NAME]) + if C.PARAMETER_NAME in parameter_df and not petab.is_empty( + row[C.PARAMETER_NAME] + ): + name = str(row[C.PARAMETER_NAME]) target_scale = target_scales[key] - if target_scale != C.LIN: + if target_scale != C.LIN and not name.startswith('log'): # mini check whether the name might indicate the scale already - if not name.startswith("log"): - name = target_scale + "(" + name + ")" + name = target_scale + '(' + name + ')' names[key] = name return names @@ -200,9 +199,9 @@ def _sanity_check(self): for key in self.prior_scales ): logger.warning( - "Found parameters with prior scale lin, parameter scale not " - "lin. Note that pyABC currently ignores the parameter scale " - "in this case and just performs sampling on the prior scale." + 'Found parameters with prior scale lin, parameter scale not ' + 'lin. Note that pyABC currently ignores the parameter scale ' + 'in this case and just performs sampling on the prior scale.' ) @@ -266,7 +265,7 @@ def create_prior(parameter_df: pd.DataFrame) -> pyabc.Distribution: # as a simple calculation shows rv = pyabc.RV('loglaplace', c=1 / b, scale=np.exp(mean)) else: - raise ValueError(f"Cannot handle prior type {prior_type}.") + raise ValueError(f'Cannot handle prior type {prior_type}.') prior_dct[row[C.PARAMETER_ID]] = rv @@ -276,7 +275,7 @@ def create_prior(parameter_df: pd.DataFrame) -> pyabc.Distribution: return prior -def get_scales(parameter_df: pd.DataFrame) -> Tuple[dict, dict]: +def get_scales(parameter_df: pd.DataFrame) -> tuple[dict, dict]: """Unravel whether the priors and evaluations are on or off scale. Only the `parameterScale...` priors are on-scale, the other priors are on @@ -350,10 +349,7 @@ def get_nominal_parameters( """ # unscaled parameters par = pyabc.Parameter( - { - key: parameter_df.loc[key, C.NOMINAL_VALUE] - for key in prior_scales.keys() - } + {key: parameter_df.loc[key, C.NOMINAL_VALUE] for key in prior_scales} ) # scale @@ -365,7 +361,7 @@ def get_nominal_parameters( elif target_scale == 'scaled': target_scales = scaled_scales else: - raise ValueError(f"Did not recognize target scale {target_scale}") + raise ValueError(f'Did not recognize target scale {target_scale}') # map linear to target scales component-wise return map_rescale(par, origin_scales=C.LIN, target_scales=target_scales) @@ -397,13 +393,13 @@ def get_bounds( # scale if target_scale == C.LIN: - target_scales = {key: C.LIN for key in prior_scales} + target_scales = dict.fromkeys(prior_scales, C.LIN) elif target_scale == 'prior': target_scales = prior_scales elif target_scale == 'scaled': target_scales = scaled_scales else: - raise ValueError(f"Did not recognize target scale {target_scale}") + raise ValueError(f'Did not recognize target scale {target_scale}') # extract bounds bounds = {} @@ -439,8 +435,8 @@ def get_bounds( def map_rescale( par: pyabc.Parameter, - origin_scales: Union[dict, str], - target_scales: Union[dict, str], + origin_scales: dict | str, + target_scales: dict | str, ) -> pyabc.Parameter: """Rescale parameter dictionary. @@ -456,9 +452,9 @@ def map_rescale( """ # handle convenience input if isinstance(origin_scales, str): - origin_scales = {key: origin_scales for key in par.keys()} + origin_scales = dict.fromkeys(par.keys(), origin_scales) if isinstance(target_scales, str): - target_scales = {key: target_scales for key in par.keys()} + target_scales = dict.fromkeys(par.keys(), target_scales) # rescale each component for key, val in par.items(): diff --git a/pyabc/population/population.py b/pyabc/population/population.py index 58c4f49b6..73c440ec2 100644 --- a/pyabc/population/population.py +++ b/pyabc/population/population.py @@ -1,12 +1,12 @@ import logging -from typing import Callable, Dict, List, Tuple +from collections.abc import Callable import numpy as np import pandas as pd from ..parameters import Parameter -logger = logging.getLogger("ABC.Population") +logger = logging.getLogger('ABC.Population') class Particle: @@ -84,19 +84,19 @@ class Population: Particles that constitute the accepted population. """ - def __init__(self, particles: List[Particle]): + def __init__(self, particles: list[Particle]): self.particles = particles self._model_probabilities = None # checks if any(not p.accepted for p in particles): raise AssertionError( - "A population should only consist of accepted particles" + 'A population should only consist of accepted particles' ) if not np.isclose(total_weight := sum(p.weight for p in particles), 1): raise AssertionError( - f"The population total weight {total_weight} is not normalized." + f'The population total weight {total_weight} is not normalized.' ) self.calculate_model_probabilities() @@ -161,18 +161,18 @@ def get_model_probabilities(self) -> pd.DataFrame: The model probabilities. """ # _model_probabilities are cached at the beginning - vars = [(key, val) for key, val in self._model_probabilities.items()] + vars = list(self._model_probabilities.items()) ms = [var[0] for var in vars] ps = [var[1] for var in vars] return pd.DataFrame({'m': ms, 'p': ps}).set_index('m') - def get_alive_models(self) -> List: + def get_alive_models(self) -> list: return self._model_probabilities.keys() def nr_of_models_alive(self) -> int: return len(self.get_alive_models()) - def get_distribution(self, m: int) -> Tuple[pd.DataFrame, np.ndarray]: + def get_distribution(self, m: int) -> tuple[pd.DataFrame, np.ndarray]: particles = self.get_particles_by_model()[m] parameters = pd.DataFrame([p.parameter for p in particles]) weights = np.array([p.weight for p in particles]) @@ -210,7 +210,7 @@ def get_weighted_sum_stats(self) -> tuple: sum_stats.append(particle.sum_stat) return weights, sum_stats - def get_accepted_sum_stats(self) -> List[dict]: + def get_accepted_sum_stats(self) -> list[dict]: """Return a list of all accepted summary statistics.""" return [particle.sum_stat for particle in self.particles] @@ -229,7 +229,7 @@ def get_for_keys(self, keys): allowed_keys = ['weight', 'distance', 'parameter', 'sum_stat'] for key in keys: if key not in allowed_keys: - raise ValueError(f"Key {key} not in {allowed_keys}.") + raise ValueError(f'Key {key} not in {allowed_keys}.') ret = {key: [] for key in keys} for particle in self.particles: @@ -244,7 +244,7 @@ def get_for_keys(self, keys): return ret - def get_particles_by_model(self) -> Dict[int, List[Particle]]: + def get_particles_by_model(self) -> dict[int, list[Particle]]: """Get particles by model. Returns @@ -261,7 +261,7 @@ def get_particles_by_model(self) -> Dict[int, List[Particle]]: # if key not yet existent particlees_by_model.setdefault(particle.m, []).append(particle) else: - logger.warning("Empty particle.") + logger.warning('Empty particle.') return particlees_by_model @@ -293,8 +293,8 @@ def __init__( is_look_ahead: bool = False, ok: bool = True, ): - self.accepted_particles: List[Particle] = [] - self.rejected_particles: List[Particle] = [] + self.accepted_particles: list[Particle] = [] + self.rejected_particles: list[Particle] = [] self.record_rejected: bool = record_rejected self.max_nr_rejected: int = max_nr_rejected self.is_look_ahead: bool = is_look_ahead @@ -318,7 +318,7 @@ def from_population(population: Population) -> 'Sample': for particle in population.particles: if not particle.accepted: raise AssertionError( - "A population should only consist of accepted particles" + 'A population should only consist of accepted particles' ) sample.append(particle) return sample @@ -356,7 +356,7 @@ def append(self, particle: Particle): ): self.rejected_particles.append(particle) - def __add__(self, other: "Sample"): + def __add__(self, other: 'Sample'): sample = Sample( record_rejected=self.record_rejected, max_nr_rejected=self.max_nr_rejected, @@ -406,11 +406,11 @@ def normalize_weights(self): if np.isclose(total_weight_accepted, 0.0): logger.warning( - f"The total population weight {total_weight_accepted} is " - "close to zero, which can be numerically problematic" + f'The total population weight {total_weight_accepted} is ' + 'close to zero, which can be numerically problematic' ) if total_weight_accepted == 0.0: - raise AssertionError("The total population weight is zero") + raise AssertionError('The total population weight is zero') for p in self.all_particles: p.weight /= total_weight_accepted @@ -437,7 +437,7 @@ def __init__( def record_rejected(self, record: bool = True): """Switch whether to record rejected particles.""" - logger.info(f"Recording also rejected particles: {record}") + logger.info(f'Recording also rejected particles: {record}') self._record_rejected = record def __call__(self, is_look_ahead: bool = False): diff --git a/pyabc/populationstrategy/populationstrategy.py b/pyabc/populationstrategy/populationstrategy.py index bf9fa8fb1..d03575b80 100644 --- a/pyabc/populationstrategy/populationstrategy.py +++ b/pyabc/populationstrategy/populationstrategy.py @@ -1,7 +1,6 @@ import json import logging from abc import ABC, abstractmethod -from typing import Dict, List, Union import numpy as np @@ -11,7 +10,7 @@ from ..transition.predict_population_size import predict_population_size from ..util import bound_pop_size_from_env -logger = logging.getLogger("ABC.Adaptation") +logger = logging.getLogger('ABC.Adaptation') def dec_bound_pop_size_from_env(fun): @@ -42,9 +41,10 @@ class directly. Subclasses must override the `update` method. def __init__(self, nr_calibration_particles: int = None): self.nr_calibration_particles = nr_calibration_particles + @abstractmethod def update( self, - transitions: List[Transition], + transitions: list[Transition], model_weights: np.ndarray, t: int = None, ): @@ -75,8 +75,8 @@ def get_config(self) -> dict: Configuration of the class as dictionary """ return { - "name": self.__class__.__name__, - "nr_calibration_particles": self.nr_calibration_particles, + 'name': self.__class__.__name__, + 'nr_calibration_particles': self.nr_calibration_particles, } def to_json(self) -> str: @@ -121,7 +121,7 @@ def __call__(self, t: int = None) -> int: def get_config(self) -> dict: config = super().get_config() - config["nr_particles"] = self.nr_particles + config['nr_particles'] = self.nr_particles return config @@ -189,18 +189,18 @@ def __init__( def get_config(self) -> dict: config = super().get_config() - config["start_nr_particles"] = self.start_nr_particles - config["max_population_size"] = self.max_population_size - config["min_population_size"] = self.min_population_size - config["mean_cv"] = self.mean_cv - config["n_bootstrap"] = self.n_bootstrap + config['start_nr_particles'] = self.start_nr_particles + config['max_population_size'] = self.max_population_size + config['min_population_size'] = self.min_population_size + config['mean_cv'] = self.mean_cv + config['n_bootstrap'] = self.n_bootstrap return config def update( self, - transitions: List[Transition], + transitions: list[Transition], model_weights: np.ndarray, - t: int = None, + t: int = None, # noqa: ARG002 ): test_X = [trans.X for trans in transitions] test_w = [trans.w for trans in transitions] @@ -227,9 +227,7 @@ def update( ) logger.info( - "Change nr particles {} -> {}".format( - reference_nr_part, self.nr_particles - ) + f'Change nr particles {reference_nr_part} -> {self.nr_particles}' ) @dec_bound_pop_size_from_env @@ -255,7 +253,7 @@ class ListPopulationSize(PopulationStrategy): def __init__( self, - values: Union[List[int], Dict[int, int]], + values: list[int] | dict[int, int], nr_calibration_particles: int = None, ): super().__init__(nr_calibration_particles=nr_calibration_particles) @@ -263,7 +261,7 @@ def __init__( def get_config(self) -> dict: config = super().get_config() - config["values"] = self.values + config['values'] = self.values return config @dec_bound_pop_size_from_env diff --git a/pyabc/predictor/predictor.py b/pyabc/predictor/predictor.py index e7a58e3b8..5a29ec007 100644 --- a/pyabc/predictor/predictor.py +++ b/pyabc/predictor/predictor.py @@ -3,8 +3,8 @@ import copy import logging from abc import ABC, abstractmethod +from collections.abc import Callable from time import time -from typing import Callable, List, Tuple, Union import numpy as np from scipy.stats import pearsonr @@ -18,7 +18,7 @@ skl_lm = skl_gp = skl_nn = skl_ms = None -logger = logging.getLogger("ABC.Predictor") +logger = logging.getLogger('ABC.Predictor') class Predictor(ABC): @@ -73,7 +73,7 @@ def wrapped_fun(self, x: np.ndarray, y: np.ndarray, w: np.ndarray): # actual fitting ret = fit(self, x, y, w) - logger.info(f"Fitted {self} in {time() - start_time:.2f}s") + logger.info(f'Fitted {self} in {time() - start_time:.2f}s') if self.log_pearson: # shape: n_sample, n_out y_pred = self.predict(x) @@ -82,10 +82,10 @@ def wrapped_fun(self, x: np.ndarray, y: np.ndarray, w: np.ndarray): for i in range(y_pred.shape[1]) ] logger.info( - " ".join( + ' '.join( [ - "Pearson correlations:", - *[f"{coeff:.3f}" for coeff in coeffs], + 'Pearson correlations:', + *[f'{coeff:.3f}' for coeff in coeffs], ] ), ) @@ -127,7 +127,7 @@ def __init__( """ self.predictor = predictor # only used if not joint - self.single_predictors: Union[List, None] = None + self.single_predictors: list | None = None self.normalize_features: bool = normalize_features self.normalize_labels: bool = normalize_labels @@ -138,13 +138,13 @@ def __init__( self.log_pearson: bool = log_pearson # indices to use - self.use_ixs: Union[np.ndarray, None] = None + self.use_ixs: np.ndarray | None = None # z-score normalization coefficients - self.mean_x: Union[np.ndarray, None] = None - self.std_x: Union[np.ndarray, None] = None - self.mean_y: Union[np.ndarray, None] = None - self.std_y: Union[np.ndarray, None] = None + self.mean_x: np.ndarray | None = None + self.std_x: np.ndarray | None = None + self.mean_y: np.ndarray | None = None + self.std_y: np.ndarray | None = None @wrap_fit_log def fit(self, x: np.ndarray, y: np.ndarray, w: np.ndarray = None) -> None: @@ -182,7 +182,7 @@ def fit(self, x: np.ndarray, y: np.ndarray, w: np.ndarray = None) -> None: # set up predictors if self.single_predictors is None: n_par = y.shape[1] - self.single_predictors: List = [ + self.single_predictors: list = [ copy.deepcopy(self.predictor) for _ in range(n_par) ] # fit a model for each parameter separately @@ -249,22 +249,22 @@ def set_use_ixs(self, x: np.ndarray, log: bool = True) -> None: # log omitted indices if log and not self.use_ixs.all(): logger.info( - "Ignore trivial features " - f"{list(np.flatnonzero(~self.use_ixs))}" + 'Ignore trivial features ' + f'{list(np.flatnonzero(~self.use_ixs))}' ) def __repr__(self) -> str: - rep = f"<{self.__class__.__name__} predictor={self.predictor}" + rep = f'<{self.__class__.__name__} predictor={self.predictor}' # print everything that is customized if not self.normalize_features: - rep += f" normalize_features={self.normalize_features}" + rep += f' normalize_features={self.normalize_features}' if not self.normalize_labels: - rep += f" normalize_labels={self.normalize_labels}" + rep += f' normalize_labels={self.normalize_labels}' if not self.joint: - rep += f" joint={self.joint}" + rep += f' joint={self.joint}' if self.weight_samples: - rep += f" weight_samples={self.weight_samples}" - return rep + ">" + rep += f' weight_samples={self.weight_samples}' + return rep + '>' class LinearPredictor(SimplePredictor): @@ -292,8 +292,8 @@ def __init__( # check installation if skl_lm is None: raise ImportError( - "This predictor requires an installation of scikit-learn. " - "Install e.g. via `pip install pyabc[scikit-learn]`" + 'This predictor requires an installation of scikit-learn. ' + 'Install e.g. via `pip install pyabc[scikit-learn]`' ) predictor = skl_lm.LinearRegression(**kwargs) @@ -312,14 +312,14 @@ def fit(self, x: np.ndarray, y: np.ndarray, w: np.ndarray = None) -> None: # log if self.joint: logger.debug( - "Linear regression coefficients (n_target, n_feature):\n" - f"{self.predictor.coef_}" + 'Linear regression coefficients (n_target, n_feature):\n' + f'{self.predictor.coef_}' ) else: for i_pred, predictor in enumerate(self.single_predictors): logger.debug( - "Linear regression coefficients (n_target, n_feature):\n" - f"for predictor {i_pred}: {predictor.coef_}" + 'Linear regression coefficients (n_target, n_feature):\n' + f'for predictor {i_pred}: {predictor.coef_}' ) @@ -341,8 +341,8 @@ def __init__( # check installation if skl_lm is None: raise ImportError( - "This predictor requires an installation of scikit-learn. " - "Install e.g. via `pip install pyabc[scikit-learn]`" + 'This predictor requires an installation of scikit-learn. ' + 'Install e.g. via `pip install pyabc[scikit-learn]`' ) predictor = skl_lm.Lasso(**kwargs) @@ -372,7 +372,7 @@ class GPPredictor(SimplePredictor): def __init__( self, - kernel: Union[Callable, skl_gp.kernels.Kernel] = None, + kernel: Callable | skl_gp.kernels.Kernel = None, normalize_features: bool = True, normalize_labels: bool = True, joint: bool = True, @@ -388,14 +388,14 @@ def __init__( # check installation if skl_gp is None: raise ImportError( - "This predictor requires an installation of scikit-learn. " - "Install e.g. via `pip install pyabc[scikit-learn]`" + 'This predictor requires an installation of scikit-learn. ' + 'Install e.g. via `pip install pyabc[scikit-learn]`' ) # default kernel if kernel is None: kernel = GPKernelHandle() - self.kernel: Union[Callable, skl_gp.kernels.Kernel] = kernel + self.kernel: Callable | skl_gp.kernels.Kernel = kernel self.kwargs = kwargs @@ -434,12 +434,12 @@ class GPKernelHandle: """ # kernels supporting features specific length scales - ARD_KERNELS = ["RBF", "Matern"] + ARD_KERNELS = ['RBF', 'Matern'] def __init__( self, - kernels: List[str] = None, - kernel_kwargs: List[dict] = None, + kernels: list[str] = None, + kernel_kwargs: list[dict] = None, ard: bool = True, ): """ @@ -461,8 +461,8 @@ def __init__( list needs to be updated. """ if kernels is None: - kernels = ["RBF", "WhiteKernel"] - self.kernels: List[str] = kernels + kernels = ['RBF', 'WhiteKernel'] + self.kernels: list[str] = kernels if kernel_kwargs is None: kernel_kwargs = [{} for _ in self.kernels] @@ -470,7 +470,7 @@ def __init__( self.ard: bool = ard - def __call__(self, n_in: int) -> "skl_gp.kernels.Kernel": + def __call__(self, n_in: int) -> 'skl_gp.kernels.Kernel': """ Parameters ---------- @@ -508,7 +508,7 @@ def __init__( normalize_features: bool = True, normalize_labels: bool = True, joint: bool = True, - hidden_layer_sizes: Union[Tuple[int, ...], Callable] = None, + hidden_layer_sizes: tuple[int, ...] | Callable = None, log_pearson: bool = True, **kwargs, ): @@ -525,8 +525,8 @@ def __init__( # check installation if skl_nn is None: raise ImportError( - "This predictor requires an installation of scikit-learn. " - "Install e.g. via `pip install pyabc[scikit-learn]`" + 'This predictor requires an installation of scikit-learn. ' + 'Install e.g. via `pip install pyabc[scikit-learn]`' ) if hidden_layer_sizes is None: @@ -574,14 +574,14 @@ class HiddenLayerHandle: Allows to define sizes depending on problem dimensions. """ - HEURISTIC = "heuristic" - MEAN = "mean" - MAX = "max" + HEURISTIC = 'heuristic' + MEAN = 'mean' + MAX = 'max' METHODS = [HEURISTIC, MEAN, MAX] def __init__( self, - method: Union[str, List[str]] = MEAN, + method: str | list[str] = MEAN, n_layer: int = 1, max_size: int = np.inf, alpha: float = 1.0, @@ -613,7 +613,7 @@ def __init__( for m in method: if m not in HiddenLayerHandle.METHODS: raise ValueError( - f"Method {m} must be in {HiddenLayerHandle.METHODS}" + f'Method {m} must be in {HiddenLayerHandle.METHODS}' ) self.method = method self.n_layer = n_layer @@ -625,7 +625,7 @@ def __call__( n_in: int, n_out: int, n_sample: int, - ) -> Tuple[int, ...]: + ) -> tuple[int, ...]: """ Parameters ---------- @@ -649,7 +649,7 @@ def __call__( elif method == HiddenLayerHandle.MAX: neurons = max(n_in, n_out) else: - raise ValueError(f"Did not recognize method {self.method}.") + raise ValueError(f'Did not recognize method {self.method}.') neurons_arr.append(neurons) # take minimum over proposed values @@ -662,7 +662,7 @@ def __call__( neurons_per_layer = int(max(2.0, neurons_per_layer)) layer_sizes = tuple(neurons_per_layer for _ in range(self.n_layer)) - logger.info(f"Layer sizes: {layer_sizes}") + logger.info(f'Layer sizes: {layer_sizes}') return layer_sizes @@ -674,13 +674,13 @@ class ModelSelectionPredictor(Predictor): full data set. """ - CROSS_VALIDATION = "cross_validation" - TRAIN_TEST_SPLIT = "train_test_split" + CROSS_VALIDATION = 'cross_validation' + TRAIN_TEST_SPLIT = 'train_test_split' SPLIT_METHODS = [CROSS_VALIDATION, TRAIN_TEST_SPLIT] def __init__( self, - predictors: List[Predictor], + predictors: list[Predictor], split_method: str = TRAIN_TEST_SPLIT, n_splits: int = 5, test_size: float = 0.2, @@ -708,12 +708,12 @@ def __init__( standard variation, and returns the score as a float. """ super().__init__() - self.predictors: List[Predictor] = predictors + self.predictors: list[Predictor] = predictors if split_method not in ModelSelectionPredictor.SPLIT_METHODS: raise ValueError( - f"Split method {split_method} must be in " - f"{ModelSelectionPredictor.SPLIT_METHODS}", + f'Split method {split_method} must be in ' + f'{ModelSelectionPredictor.SPLIT_METHODS}', ) self.split_method: str = split_method @@ -724,7 +724,7 @@ def __init__( self.f_score = root_mean_square_error # holds the chosen predictor model - self.chosen_one: Union[Predictor, None] = None + self.chosen_one: Predictor | None = None def fit(self, x: np.ndarray, y: np.ndarray, w: np.ndarray = None) -> None: # output normalization @@ -789,8 +789,8 @@ def fit(self, x: np.ndarray, y: np.ndarray, w: np.ndarray = None) -> None: self.predictors, scores_train, scores_test ): logger.info( - f"Test score {score_test:.3e} (train {score_train:.3e}) for " - f"{predictor}" + f'Test score {score_test:.3e} (train {score_train:.3e}) for ' + f'{predictor}' ) # best predictor has minimum score @@ -807,7 +807,7 @@ def predict(self, x: np.ndarray, normalize: bool = False) -> np.ndarray: def root_mean_square_error( y1: np.ndarray, y2: np.ndarray, - sigma: Union[np.ndarray, float], + sigma: np.ndarray | float, ) -> float: """Root mean square error of `y1 - y2 / sigma`. diff --git a/pyabc/random_choice/random_choice.py b/pyabc/random_choice/random_choice.py index 6d8ba31df..dfb66580c 100644 --- a/pyabc/random_choice/random_choice.py +++ b/pyabc/random_choice/random_choice.py @@ -22,4 +22,4 @@ def fast_random_choice(weights): return k # error when u > sum(weights) < 1 (not checked pro-actively) - raise ValueError("Random choice error {}".format(weights)) + raise ValueError(f'Random choice error {weights}') diff --git a/pyabc/random_variables/random_variables.py b/pyabc/random_variables/random_variables.py index 1e5b63d85..62bef7377 100644 --- a/pyabc/random_variables/random_variables.py +++ b/pyabc/random_variables/random_variables.py @@ -1,11 +1,10 @@ import logging from abc import ABC, abstractmethod from functools import reduce -from typing import Union from ..parameters import Parameter, ParameterStructure -rv_logger = logging.getLogger("ABC.RV") +rv_logger = logging.getLogger('ABC.RV') class RVBase(ABC): @@ -27,7 +26,7 @@ class RVBase(ABC): """ @abstractmethod - def copy(self) -> "RVBase": + def copy(self) -> 'RVBase': """Copy the random variable. Returns @@ -108,7 +107,7 @@ class RV(RVBase): """ @classmethod - def from_dictionary(cls, dictionary: dict) -> "RV": + def from_dictionary(cls, dictionary: dict) -> 'RV': """Construct random variable from dictionary. Parameters @@ -138,7 +137,7 @@ def __init__(self, name: str, *args, **kwargs): self.args = args self.kwargs = kwargs self.distribution = None - "the scipy.stats. ... distribution object" + 'the scipy.stats. ... distribution object' self.__setstate__(self.__getstate__()) def __getattr__(self, item): @@ -172,9 +171,7 @@ def cdf(self, x, *args, **kwargs): return self.distribution.cdf(x, *args, **kwargs) def __repr__(self): - return "".format( - name=self.name, args=self.args, kwargs=self.kwargs - ) + return f'' class RVDecorator(RVBase): @@ -229,13 +226,10 @@ def decorator_repr(self) -> str: # pylint: disable=R0201 A string representing the decorator only. """ - return "Decorator" + return 'Decorator' def __repr__(self): - return ( - "[{decorator_repr}]".format(decorator_repr=self.decorator_repr()) - + self.component.__repr__() - ) + return f'[{self.decorator_repr()}]' + self.component.__repr__() class LowerBoundDecorator(RVDecorator): @@ -264,18 +258,18 @@ class LowerBoundDecorator(RVDecorator): def __init__(self, component: RV, lower_bound: float): if component.cdf(lower_bound) == 1: raise Exception( - "LowerBoundDecorator: Conditioning on a set of measure zero." + 'LowerBoundDecorator: Conditioning on a set of measure zero.' ) self.lower_bound = lower_bound - super(LowerBoundDecorator, self).__init__(component) + super().__init__(component) def copy(self): return self.__class__(self.component.copy(), self.lower_bound) def decorator_repr(self): - return "Lower: X > {lower:2f}".format(lower=self.lower_bound) + return f'Lower: X > {self.lower_bound:2f}' - def rvs(self, *args, **kwargs): + def rvs(self, *args, **kwargs): # noqa: ARG002 for _ in range(LowerBoundDecorator.MAX_TRIES): sample = self.component.rvs() # not sure whether > is the exact opposite. but <= is consistent @@ -283,21 +277,21 @@ def rvs(self, *args, **kwargs): return sample # with the other functions return None - def pdf(self, x, *args, **kwargs): + def pdf(self, x, *args, **kwargs): # noqa: ARG002 if x <= self.lower_bound: return 0.0 return self.component.pdf(x) / ( 1 - self.component.cdf(self.lower_bound) ) - def pmf(self, x, *args, **kwargs): + def pmf(self, x, *args, **kwargs): # noqa: ARG002 if x <= self.lower_bound: return 0.0 return self.component.pmf(x) / ( 1 - self.component.cdf(self.lower_bound) ) - def cdf(self, x, *args, **kwargs): + def cdf(self, x, *args, **kwargs): # noqa: ARG002 if x <= self.lower_bound: return 0.0 lower_mass = self.component.cdf(self.lower_bound) @@ -323,7 +317,7 @@ def rvs(self, *args, **kwargs) -> Parameter: """ @abstractmethod - def pdf(self, x: Union[Parameter, dict]): + def pdf(self, x: Parameter | dict): """Get probability density at point `x`. Parameters @@ -344,15 +338,15 @@ class Distribution(DistributionBase, ParameterStructure): def __repr__(self): return ( - "" + '' ) @classmethod def from_dictionary_of_dictionaries( cls, dict_of_dicts: dict - ) -> "Distribution": + ) -> 'Distribution': """Create distribution from dictionary of dictionaries. Parameters @@ -374,7 +368,7 @@ def from_dictionary_of_dictionaries( rv_dictionary[key] = RV.from_dictionary(value) return cls(rv_dictionary) - def copy(self) -> "Distribution": + def copy(self) -> 'Distribution': """Copy the distribution. Returns @@ -422,7 +416,7 @@ def rvs(self, *args, **kwargs) -> Parameter: **{key: val.rvs(*args, **kwargs) for key, val in self.items()} ) - def pdf(self, x: Union[Parameter, dict]): + def pdf(self, x: Parameter | dict): """Get probability density at point `x` (product of marginals). Combination of probability density functions (for continuous @@ -436,9 +430,9 @@ def pdf(self, x: Union[Parameter, dict]): # check if the parameters match if sorted(x.keys()) != sorted(self.keys()): raise Exception( - "Random variable parameter mismatch. Expected: " + 'Random variable parameter mismatch. Expected: ' + str(sorted(self.keys())) - + " got " + + ' got ' + str(sorted(x.keys())) ) if len(self) > 0: diff --git a/pyabc/sampler/base.py b/pyabc/sampler/base.py index dcdd03024..a391ec5da 100644 --- a/pyabc/sampler/base.py +++ b/pyabc/sampler/base.py @@ -1,6 +1,6 @@ from abc import ABC, ABCMeta, abstractmethod +from collections.abc import Callable from numbers import Real -from typing import Callable, Union import numpy as np @@ -22,11 +22,11 @@ def sample_until_n_accepted(self, n, simulate_one, t, **kwargs): if sample.n_accepted != n and sample.ok: # this should not happen if the sampler is configured correctly raise AssertionError( - f"Expected {n} but got {sample.n_accepted} acceptances." + f'Expected {n} but got {sample.n_accepted} acceptances.' ) if any(particle.preliminary for particle in sample.all_particles): - raise AssertionError("There cannot be non-evaluated particles.") + raise AssertionError('There cannot be non-evaluated particles.') return sample @@ -74,7 +74,7 @@ def __init__(self): record_rejected=False ) self.show_progress: bool = False - self.analysis_id: Union[str, None] = None + self.analysis_id: str | None = None def _create_empty_sample(self) -> Sample: return self.sample_factory() diff --git a/pyabc/sampler/eps_mixin.py b/pyabc/sampler/eps_mixin.py index 1a74fb171..192e135d8 100644 --- a/pyabc/sampler/eps_mixin.py +++ b/pyabc/sampler/eps_mixin.py @@ -3,7 +3,6 @@ import random from abc import ABC, abstractmethod from time import sleep -from typing import Union import cloudpickle import numpy as np @@ -78,7 +77,7 @@ def __init__( self.default_pickle: bool = default_pickle self.batch_size: int = batch_size - self._simulate_accept_one: Union[bytes, None] = None + self._simulate_accept_one: bytes | None = None @abstractmethod def client_cores(self) -> int: @@ -88,11 +87,11 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, - max_eval=np.inf, - all_accepted=False, - ana_vars=None, + max_eval=np.inf, # noqa: ARG002 + all_accepted=False, # noqa: ARG002 + ana_vars=None, # noqa: ARG002 ): # For default pickling if self.default_pickle: @@ -189,7 +188,7 @@ def full_submit_function(job_id): if sample.n_accepted != n: raise AssertionError( - f"Got {sample.n_accepted} accepted particles but expected {n}" + f'Got {sample.n_accepted} accepted particles but expected {n}' ) self.nr_evaluations_ = next_job_id diff --git a/pyabc/sampler/multicore.py b/pyabc/sampler/multicore.py index 70ee0b71e..fe7a7c799 100644 --- a/pyabc/sampler/multicore.py +++ b/pyabc/sampler/multicore.py @@ -9,7 +9,7 @@ from .multicorebase import MultiCoreSampler, get_if_worker_healthy from .singlecore import SingleCoreSampler -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') SENTINEL = None @@ -76,19 +76,17 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, max_eval=np.inf, - all_accepted=False, - ana_vars=None, + all_accepted=False, # noqa: ARG002 + ana_vars=None, # noqa: ARG002 ): # starting more than n jobs # does not help in this parallelization scheme n_procs = min(n, self.n_procs) logger.debug( - "Start sampling on {} cores ({} requested)".format( - n_procs, self.n_procs - ) + f'Start sampling on {n_procs} cores ({self.n_procs} requested)' ) feed_q = Queue() result_q = Queue() diff --git a/pyabc/sampler/multicorebase.py b/pyabc/sampler/multicorebase.py index ffbcc576d..404605dc3 100644 --- a/pyabc/sampler/multicorebase.py +++ b/pyabc/sampler/multicorebase.py @@ -3,11 +3,10 @@ import platform from multiprocessing import Process, ProcessError, Queue from queue import Empty -from typing import List from .base import Sampler -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') class MultiCoreSampler(Sampler): @@ -42,13 +41,13 @@ def __init__( self.daemon = daemon if pickle is None: pickle = False - if platform.system() == "Darwin": # macos + if platform.system() == 'Darwin': # macos pickle = True self.pickle = pickle self.check_max_eval = check_max_eval # inform user about number of cores used - logger.info(f"Parallelize sampling on {self.n_procs} processes.") + logger.info(f'Parallelize sampling on {self.n_procs} processes.') @property def n_procs(self): @@ -78,7 +77,7 @@ def healthy(worker): return all(worker.exitcode in [0, None] for worker in worker) -def get_if_worker_healthy(workers: List[Process], queue: Queue): +def get_if_worker_healthy(workers: list[Process], queue: Queue): """ Parameters @@ -100,4 +99,4 @@ def get_if_worker_healthy(workers: List[Process], queue: Queue): return item except Empty: if not healthy(workers): - raise ProcessError("At least one worker is dead.") + raise ProcessError('At least one worker is dead.') from None diff --git a/pyabc/sampler/redis_eps/cli.py b/pyabc/sampler/redis_eps/cli.py index 807979b21..36e60c271 100644 --- a/pyabc/sampler/redis_eps/cli.py +++ b/pyabc/sampler/redis_eps/cli.py @@ -28,9 +28,9 @@ from .work import work_on_population_dynamic from .work_static import work_on_population_static -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') -TIMES = {"s": 1, "m": 60, "h": 3600, "d": 24 * 3600} +TIMES = {'s': 1, 'm': 60, 'h': 3600, 'd': 24 * 3600} def runtime_parse(s): @@ -39,12 +39,12 @@ def runtime_parse(s): return unit * nr -@click.command(help="Evaluation parallel redis sampler for pyABC.") -@click.option('--host', default="localhost", help='Redis host.') +@click.command(help='Evaluation parallel redis sampler for pyABC.') +@click.option('--host', default='localhost', help='Redis host.') @click.option('--port', default=6379, type=int, help='Redis port.') @click.option( '--runtime', - default="2h", + default='2h', type=str, help='Max worker runtime if the form , ' 'where is any number and can be s, ' @@ -63,19 +63,19 @@ def runtime_parse(s): '--processes', default=1, type=int, - help="The number of worker processes to start", + help='The number of worker processes to start', ) @click.option( '--daemon', default=True, type=bool, - help="Create subprocesses in daemon mode.", + help='Create subprocesses in daemon mode.', ) -@click.option('--catch', default=True, type=bool, help="Catch errors.") +@click.option('--catch', default=True, type=bool, help='Catch errors.') def work( - host="localhost", + host='localhost', port=6379, - runtime="2h", + runtime='2h', password=None, processes=1, daemon=True, @@ -104,7 +104,7 @@ def work( # log for proc in procs: - logger.info(f"Started subprocess with pid {proc.pid}") + logger.info(f'Started subprocess with pid {proc.pid}') # wait for them to return for proc in procs: @@ -112,7 +112,7 @@ def work( def _work( - host="localhost", port=6379, runtime="2h", password=None, catch=True + host='localhost', port=6379, runtime='2h', password=None, catch=True ): np.random.seed() random.seed() @@ -122,8 +122,8 @@ def _work( start_time = time() max_runtime_s = runtime_parse(runtime) logger.info( - f"Start redis worker. Max run time {max_runtime_s}s, " - f"HOST={socket.gethostname()}, PID={os.getpid()}" + f'Start redis worker. Max run time {max_runtime_s}s, ' + f'HOST={socket.gethostname()}, PID={os.getpid()}' ) # connect to the redis server @@ -133,7 +133,7 @@ def _work( p = redis.pubsub() p.subscribe(MSG) - logger.info(f"Subscribed to host {host} port {port}") + logger.info(f'Subscribed to host {host} port {port}') # Block-wait for publications. Every message on the channel that has not # been overridden yet is processed by all workers exactly once via @@ -141,9 +141,9 @@ def _work( # When the server is stopped, an error makes the workers stop too. for msg in p.listen(): try: - data = msg["data"].decode() + data = msg['data'].decode() except AttributeError: - data = msg["data"] + data = msg['data'] if data == START or isinstance(data, int): # Sometimes, redis weirdly only publishes an int (1) at the @@ -192,10 +192,10 @@ def _work( ) else: # this should never happen - raise ValueError(f"Did not recognize mode {mode}") + raise ValueError(f'Did not recognize mode {mode}') elif data == STOP: - logger.info("Received stop signal. Shutdown redis worker.") + logger.info('Received stop signal. Shutdown redis worker.') return # TODO other messages (some integers?) are ignored @@ -204,52 +204,50 @@ def _work( elapsed_time = time() - start_time if elapsed_time > max_runtime_s: logger.info( - "Shutdown redis worker. Max runtime {}s reached".format( - max_runtime_s - ) + f'Shutdown redis worker. Max runtime {max_runtime_s}s reached' ) return @click.command( - help="ABC Redis cluster manager. " + help='ABC Redis cluster manager. ' "The command can be 'info' or 'stop'. " "For 'stop' the workers are shut down cleanly " - "after the current population. " + 'after the current population. ' "For 'info' you'll see how many workers are connected, " - "how many evaluations the current population has, and " - "how many particles are still missing. " + 'how many evaluations the current population has, and ' + 'how many particles are still missing. ' "For 'reset-workers', the worker count will be resetted to" - "zero. This does not cancel the sampling. This is useful " - "if workers were unexpectedly killed." + 'zero. This does not cancel the sampling. This is useful ' + 'if workers were unexpectedly killed.' ) -@click.option('--host', default="localhost", help="Redis host.") -@click.option('--port', default=6379, type=int, help="Redis port.") -@click.option('--password', default=None, type=str, help="Redis password.") +@click.option('--host', default='localhost', help='Redis host.') +@click.option('--port', default=6379, type=int, help='Redis port.') +@click.option('--password', default=None, type=str, help='Redis password.') @click.option( - '--time', '-t', 't', default=None, type=int, help="Generation t." + '--time', '-t', 't', default=None, type=int, help='Generation t.' ) @click.argument('command', type=str) -def manage(command, host="localhost", port=6379, password=None, t=None): +def manage(command, host='localhost', port=6379, password=None, t=None): """Manage workers. Corresponds to the entry point abc-redis-manager. """ return _manage(command, host=host, port=port, password=password, t=t) -def _manage(command, host="localhost", port=6379, password=None, t=None): - if command not in ["info", "stop", "reset-workers"]: - print("Unknown command: ", command) +def _manage(command, host='localhost', port=6379, password=None, t=None): + if command not in ['info', 'stop', 'reset-workers']: + print('Unknown command: ', command) return redis = StrictRedis(host=host, port=port, password=password) - if command == "stop": + if command == 'stop': redis.publish(MSG, STOP) return # check whether an analysis is running if not is_server_used(redis): - print("No active generation") + print('No active generation') return # id of the current analysis @@ -259,7 +257,7 @@ def _manage(command, host="localhost", port=6379, password=None, t=None): if t is None: t = int(redis.get(idfy(GENERATION, ana_id)).decode()) - if command == "info": + if command == 'info': pipeline = redis.pipeline() res = ( pipeline.get(idfy(N_WORKER, ana_id, t)) @@ -270,17 +268,15 @@ def _manage(command, host="localhost", port=6379, password=None, t=None): ) res = [r.decode() if r is not None else r for r in res] print( - "Workers={} Evaluations={} Acceptances={}/{} (generation {})".format( + 'Workers={} Evaluations={} Acceptances={}/{} (generation {})'.format( *res, t ) ) - elif command == "reset-workers": + elif command == 'reset-workers': redis.set(idfy(N_WORKER, ana_id, t), 0) def is_server_used(redis: StrictRedis): """Check whether the server is currently in use.""" analysis_id = redis.get(ANALYSIS_ID) - if analysis_id is None: - return False - return True + return analysis_id is not None diff --git a/pyabc/sampler/redis_eps/sampler.py b/pyabc/sampler/redis_eps/sampler.py index 1cff89703..5138444a9 100644 --- a/pyabc/sampler/redis_eps/sampler.py +++ b/pyabc/sampler/redis_eps/sampler.py @@ -2,9 +2,9 @@ import copy import logging +from collections.abc import Callable from datetime import datetime from time import sleep -from typing import Callable, Dict, List, Tuple import cloudpickle as pickle import numpy as np @@ -48,7 +48,7 @@ ) from .redis_logging import RedisSamplerLogger -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') class RedisSamplerBase(Sampler): @@ -72,13 +72,13 @@ class RedisSamplerBase(Sampler): def __init__( self, - host: str = "localhost", + host: str = 'localhost', port: int = 6379, password: str = None, log_file: str = None, ): super().__init__() - logger.debug(f"Redis sampler: host={host} port={port}") + logger.debug(f'Redis sampler: host={host} port={port}') # handles the connection to the redis-server self.redis: StrictRedis = StrictRedis( host=host, port=port, password=password @@ -101,7 +101,7 @@ def set_analysis_id(self, analysis_id: str): super().set_analysis_id(analysis_id) if self.redis.get(ANALYSIS_ID): raise AssertionError( - "The server seems busy with an analysis already" + 'The server seems busy with an analysis already' ) self.redis.set(ANALYSIS_ID, analysis_id) @@ -217,7 +217,7 @@ class RedisEvalParallelSampler(RedisSamplerBase): def __init__( self, - host: str = "localhost", + host: str = 'localhost', port: int = 6379, password: str = None, batch_size: int = 1, @@ -244,7 +244,7 @@ def sample_until_n_accepted( simulate_one, t, *, - max_eval=np.inf, + max_eval=np.inf, # noqa: ARG002 all_accepted=False, ana_vars=None, ) -> Sample: @@ -488,7 +488,7 @@ def maybe_start_next_generation( self, t: int, n: int, - id_results: List, + id_results: list, all_accepted: bool, ana_vars: AnalysisVars, ) -> None: @@ -591,7 +591,7 @@ def maybe_start_next_generation( max_n_eval_look_ahead=max_n_eval_look_ahead, ) - def create_sample(self, id_results: List[Tuple], n: int) -> Sample: + def create_sample(self, id_results: list[tuple], n: int) -> Sample: """Create a single sample result. Order the results by starting point to avoid a bias towards short-running simulations (dynamic scheduling). @@ -612,7 +612,7 @@ def create_sample(self, id_results: List[Tuple], n: int) -> Sample: # check number of acceptances if (n_accepted := sample.n_accepted) != n: raise AssertionError( - f"Expected {n} accepted particles but got {n_accepted}" + f'Expected {n} accepted particles but got {n_accepted}' ) return sample @@ -634,8 +634,8 @@ def _check_bad(var): # iteration we do not look ahead if var.is_adaptive(): raise AssertionError( - f"{var.__class__.__name__} cannot be used in look-ahead " - "mode without delayed acceptance. Consider setting the " + f'{var.__class__.__name__} cannot be used in look-ahead ' + 'mode without delayed acceptance. Consider setting the ' "sampler's `look_ahead_delay_evaluation` flag." ) @@ -717,7 +717,7 @@ def post_check_acceptance( redis, ana_vars, logger: RedisSamplerLogger, -) -> Tuple: +) -> tuple: """Check whether the sample is really acceptable. This is where evaluation of preliminary samples happens, using the analysis @@ -740,7 +740,7 @@ def post_check_acceptance( if n_accepted != 1: # this should never happen raise AssertionError( - "Expected exactly one accepted particle in sample." + 'Expected exactly one accepted particle in sample.' ) # increase general acceptance counter @@ -757,8 +757,8 @@ def post_check_acceptance( if len(sample.all_particles) != 1: # this should never happen raise AssertionError( - "Expected number of particles in sample: 1. " - f"Got: {len(sample.all_particles)}" + 'Expected number of particles in sample: 1. ' + f'Got: {len(sample.all_particles)}' ) # from here on, we may assume that all particles (#=1) are yet to be judged @@ -815,7 +815,7 @@ def self_normalize_within_subpopulations(sample: Sample, n: int) -> Sample: if len(sample.accepted_particles) != n: # this should not happen - raise AssertionError("Unexpected number of acceptances") + raise AssertionError('Unexpected number of acceptances') # get particles per proposal particles_per_prop = { @@ -829,7 +829,7 @@ def self_normalize_within_subpopulations(sample: Sample, n: int) -> Sample: # normalize weights by $ESS_l / sum_{i<=N_l} w^l_i$ for proposal id l # this is s.t. $sum_{i<=N_l} w^l_i \propto ESS_l$ - normalizations: Dict[int, float] = {} + normalizations: dict[int, float] = {} for prop_id, particles_for_prop in particles_per_prop.items(): weights = np.array( [particle.weight for particle in particles_for_prop] diff --git a/pyabc/sampler/redis_eps/sampler_static.py b/pyabc/sampler/redis_eps/sampler_static.py index 75603ae40..67272e526 100644 --- a/pyabc/sampler/redis_eps/sampler_static.py +++ b/pyabc/sampler/redis_eps/sampler_static.py @@ -2,8 +2,8 @@ import logging import pickle +from collections.abc import Callable from time import sleep -from typing import Callable, List import cloudpickle import numpy as np @@ -29,7 +29,7 @@ ) from .sampler import RedisSamplerBase -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') class RedisStaticSampler(RedisSamplerBase): @@ -39,11 +39,11 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, - max_eval=np.inf, - all_accepted=False, - ana_vars=None, + max_eval=np.inf, # noqa: ARG002 + all_accepted=False, # noqa: ARG002 + ana_vars=None, # noqa: ARG002 ): # get the analysis id ana_id = self.analysis_id @@ -60,7 +60,7 @@ def sample_until_n_accepted( if len(sample.accepted_particles) != 1: # this should never happen raise AssertionError( - "Expected exactly one accepted particle in sample." + 'Expected exactly one accepted particle in sample.' ) samples.append(sample) bar.inc() @@ -137,13 +137,13 @@ def clear_generation_t(self, t: int) -> None: .execute() ) - def create_sample(self, samples: List[Sample], n: int) -> Sample: + def create_sample(self, samples: list[Sample], n: int) -> Sample: """Create a single sample result. Order the results by starting point to avoid a bias towards short-running simulations (dynamic scheduling). """ if len(samples) != n: - raise AssertionError(f"Expected {n} samples, got {len(samples)}.") + raise AssertionError(f'Expected {n} samples, got {len(samples)}.') # create 1 to-be-returned sample from results sample = self._create_empty_sample() diff --git a/pyabc/settings/settings.py b/pyabc/settings/settings.py index a0d0a0c12..7c0b211bf 100644 --- a/pyabc/settings/settings.py +++ b/pyabc/settings/settings.py @@ -1,5 +1,3 @@ -from typing import List - import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib import rcParams @@ -9,7 +7,7 @@ def set_figure_params( theme: str = 'pyabc', style: str = None, color_map: str = 'plasma', - color_cycle: List[str] = None, + color_cycle: list[str] = None, ) -> None: """Set global figure parameters for a consistent, beautified design. @@ -42,13 +40,13 @@ def set_figure_params( elif theme == 'default': _set_figure_params_default() else: - raise ValueError(f"Theme not recognized: {theme}") + raise ValueError(f'Theme not recognized: {theme}') def _set_figure_params_pyabc( style: str, color_map: str, - color_cycle: List[str], + color_cycle: list[str], ) -> None: """Set layout parameters for style 'pyabc'""" # overall style diff --git a/pyabc/sge/db.py b/pyabc/sge/db.py index b5aaac326..1463f608a 100644 --- a/pyabc/sge/db.py +++ b/pyabc/sge/db.py @@ -22,25 +22,25 @@ def __init__(self, tmp_dir): def clean_up(self): pass - def create(self, nr_jobs): + def create(self, nr_jobs): # noqa: ARG002 # create database for job information with self.connection: self.connection.execute( - "CREATE TABLE IF NOT EXISTS " - "status(ID INTEGER, status TEXT, time REAL)" + 'CREATE TABLE IF NOT EXISTS ' + 'status(ID INTEGER, status TEXT, time REAL)' ) def start(self, ID): with self.connection: self.connection.execute( - "INSERT INTO status VALUES(?,?,?)", + 'INSERT INTO status VALUES(?,?,?)', (ID, 'started', time.time()), ) def finish(self, ID): with self.connection: self.connection.execute( - "INSERT INTO status VALUES(?,?,?)", + 'INSERT INTO status VALUES(?,?,?)', (ID, 'finished', time.time()), ) @@ -53,8 +53,7 @@ def wait_for_job(self, ID, max_run_time_h): # pre-calculated expressions with self.connection: results = self.connection.execute( - "SELECT status, time from status WHERE ID=" # noqa: S608, B608 - + str(ID) + 'SELECT status, time from status WHERE ID=' + str(ID) ).fetchall() nr_rows = len(results) @@ -71,12 +70,12 @@ def wait_for_job(self, ID, max_run_time_h): if nr_rows == 2: # job finished return False # something not catched here - raise Exception('Something went wrong. nr_rows={}'.format(nr_rows)) + raise Exception(f'Something went wrong. nr_rows={nr_rows}') class RedisJobDB: - FINISHED_STATE = "finished" - STARTED_STATE = "started" + FINISHED_STATE = 'finished' + STARTED_STATE = 'started' @staticmethod def server_online(cls): @@ -89,12 +88,12 @@ def server_online(cls): def __init__(self, tmp_dir): config = get_config() - self.HOST = config["REDIS"]["HOST"] + self.HOST = config['REDIS']['HOST'] self.job_name = os.path.basename(tmp_dir) self.connection = redis.Redis(host=self.HOST, decode_responses=True) def key(self, ID): - return self.job_name + ":" + str(ID) + return self.job_name + ':' + str(ID) def clean_up(self): IDs = map(int, self.connection.lrange(self.job_name, 0, -1)) @@ -112,12 +111,12 @@ def create(self, nr_jobs): def start(self, ID): self.connection.hmset( - self.key(ID), {"status": self.STARTED_STATE, "time": time.time()} + self.key(ID), {'status': self.STARTED_STATE, 'time': time.time()} ) def finish(self, ID): self.connection.hmset( - self.key(ID), {"status": self.FINISHED_STATE, "time": time.time()} + self.key(ID), {'status': self.FINISHED_STATE, 'time': time.time()} ) def wait_for_job(self, ID, max_run_time_h): @@ -125,16 +124,14 @@ def wait_for_job(self, ID, max_run_time_h): if len(values) == 0: # not yet set, job not yet started return True - status = values["status"] - time_stamp = float(values["time"]) + status = values['status'] + time_stamp = float(values['time']) if status == self.FINISHED_STATE: return False if status == self.STARTED_STATE: - if within_time(time_stamp, max_run_time_h): - return True - return False + return within_time(time_stamp, max_run_time_h) raise Exception('Something went wrong.') @@ -147,8 +144,8 @@ def job_db_factory(tmp_path): SQLite or redis db depending on availability """ config = get_config() - if config["BROKER"]["TYPE"] == "REDIS": + if config['BROKER']['TYPE'] == 'REDIS': return RedisJobDB(tmp_path) - if config["BROKER"]["TYPE"] == "SQLITE": + if config['BROKER']['TYPE'] == 'SQLITE': return SQLiteJobDB(tmp_path) - raise Exception("Unknown broker: {}".format(config["BROKER"]["TYPE"])) + raise Exception('Unknown broker: {}'.format(config['BROKER']['TYPE'])) diff --git a/pyabc/sge/execution_contexts.py b/pyabc/sge/execution_contexts.py index 4b6486dd6..81523ec4c 100644 --- a/pyabc/sge/execution_contexts.py +++ b/pyabc/sge/execution_contexts.py @@ -22,9 +22,7 @@ def __init__(self, tmp_path, job_nr): self.original_stdout_write = sys.stdout.write self.original_stderr_write = sys.stderr.write - self.name_tag = "[{}:{}]".format( - os.path.basename(self.tmp_path), self.job_nr - ) + self.name_tag = f'[{os.path.basename(self.tmp_path)}:{self.job_nr}]' def __enter__(self): import sys @@ -39,7 +37,7 @@ def __exit__(self, exc_type, exc_value, traceback): sys.stderr.write = self.original_stderr_write def process_text(self, text): - return text.replace("\n", self.name_tag + "\n") + return text.replace('\n', self.name_tag + '\n') def named_write_stdout(self, text): self.original_stdout_write(self.process_text(text)) @@ -69,7 +67,7 @@ class ProfilingContext(ExecutionContextMixin): Useful for debugging. Do not use in production. """ - RELATIVE_OUTPUT_FOLDER = "profiling" + RELATIVE_OUTPUT_FOLDER = 'profiling' keep_output_directory = True def __init__(self, tmp_path, job_nr): @@ -89,7 +87,7 @@ def _dump_profile(self): ) self._make_output_directory(output_directory) output_file = os.path.join( - output_directory, str(self.job_nr) + ".pstats" + output_directory, str(self.job_nr) + '.pstats' ) self._profile.dump_stats(output_file) diff --git a/pyabc/sge/test_sge.py b/pyabc/sge/test_sge.py index 435a868db..7878a8e63 100644 --- a/pyabc/sge/test_sge.py +++ b/pyabc/sge/test_sge.py @@ -2,20 +2,20 @@ from pyabc.sge import SGE -if __name__ == "__main__": +if __name__ == '__main__': def f(x): sleep(30) return x * 2 - print("Start sge test", flush=True) # noqa: T201 - sge = SGE(priority=0, memory="1G", name="test", time_h=1) + print('Start sge test', flush=True) # noqa: T201 + sge = SGE(priority=0, memory='1G', name='test', time_h=1) - print("Do map", flush=True) # noqa: T201 + print('Do map', flush=True) # noqa: T201 res = sge.map(f, [1, 2, 3, 4]) - print("Got results", flush=True) # noqa: T201 + print('Got results', flush=True) # noqa: T201 if res != [2, 4, 6, 8]: - raise AssertionError("Wrong result, got {}".format(res)) + raise AssertionError(f'Wrong result, got {res}') - print("Finished", flush=True) # noqa: T201 + print('Finished', flush=True) # noqa: T201 diff --git a/pyabc/storage/db_export.py b/pyabc/storage/db_export.py index 368cd0d39..21768745f 100644 --- a/pyabc/storage/db_export.py +++ b/pyabc/storage/db_export.py @@ -1,62 +1,60 @@ -from typing import Union - import click from .df_to_file import to_file from .history import History -@click.command(name="abc-dump") +@click.command(name='abc-dump') @click.option( - "--db", - help="The db connection or file in which the pyABC data " - "is stored and from from which we want to to dump " - "to a file", + '--db', + help='The db connection or file in which the pyABC data ' + 'is stored and from from which we want to to dump ' + 'to a file', ) -@click.option("--out", help="The file to which to dump") +@click.option('--out', help='The file to which to dump') @click.option( - "--format", + '--format', 'out_format', - default="feather", - help="The format to which to dump, e.g. feather, " - "csv, hdf, json, html, msgpack, stata", + default='feather', + help='The format to which to dump, e.g. feather, ' + 'csv, hdf, json, html, msgpack, stata', ) @click.option( - "--generation", - default="last", - help="The generation to dump. Can be " - "\"all\" or \"last\" or an integer " - "number", + '--generation', + default='last', + help='The generation to dump. Can be ' + '"all" or "last" or an integer ' + 'number', ) @click.option( - "--model", - default="all", - help="The model number to dump. Defaults" - "to \"all\", which means all models are" - "dumped. Can be an integer, which" - "identifies the model number. Note that the first model " - "has number 0.", + '--model', + default='all', + help='The model number to dump. Defaults' + 'to "all", which means all models are' + 'dumped. Can be an integer, which' + 'identifies the model number. Note that the first model ' + 'has number 0.', ) @click.option( - "--id", + '--id', default=1, type=int, - help="The ABC-SMC run id which to dump. " "Defaults to 1", + help='The ABC-SMC run id which to dump. ' 'Defaults to 1', ) @click.option( - "--tidy", + '--tidy', default=True, type=bool, - help="If True, the individual parameter and summary statistic " - "names are pivoted. Only works for a single model and " - "time point.", + help='If True, the individual parameter and summary statistic ' + 'names are pivoted. Only works for a single model and ' + 'time point.', ) def main( db: str, out: str, out_format: str, - generation: Union[int, str] = "last", - model: Union[int, str] = "all", + generation: int | str = 'last', + model: int | str = 'all', id: int = 1, tidy: bool = True, ): # pylint: disable=W0622 @@ -81,20 +79,17 @@ def export( db: str, out: str, out_format: str, - generation: Union[int, str] = "last", - model: Union[int, str] = "all", + generation: int | str = 'last', + model: int | str = 'all', id: int = 1, tidy: bool = True, ): # pylint: disable=W0622 # check if db is a file or SQLAlchemy identifier - if ":///" not in db: - db = "sqlite:///" + db + if ':///' not in db: + db = 'sqlite:///' + db # parse model - if model == "all": - m = None - else: - m = int(model) + m = None if model == 'all' else int(model) # parse generation t = generation diff --git a/pyabc/storage/db_model.py b/pyabc/storage/db_model.py index 4fc13b959..1b7efbf32 100644 --- a/pyabc/storage/db_model.py +++ b/pyabc/storage/db_model.py @@ -45,10 +45,10 @@ class BytesStorage(types.TypeDecorator): # (guaranteed to produce the same bind/result behavior every time) cache_ok = True - def process_bind_param(self, value, dialect): # pylint: disable=R0201 + def process_bind_param(self, value, dialect): # noqa: ARG002 return to_bytes(value) - def process_result_value(self, value, dialect): # pylint: disable=R0201 + def process_result_value(self, value, dialect): # noqa: ARG002 return from_bytes(value) @@ -69,26 +69,26 @@ class ABCSMC(Base): epsilon_function = Column(String(5000)) population_strategy = Column(String(5000)) git_hash = Column(String(120)) - populations = relationship("Population") + populations = relationship('Population') def __repr__(self): return ( - f"" + f'' ) def start_info(self): return ( - f"" + f'' ) def end_info(self): duration = self.end_time - self.start_time return ( - f"" + f'' ) @@ -100,18 +100,18 @@ class Population(Base): population_end_time = Column(DateTime) nr_samples = Column(Integer) epsilon = Column(Float) - models = relationship("Model") + models = relationship('Model') - def __init__(self, *args, **kwargs): - super(Population, self).__init__(**kwargs) + def __init__(self, *args, **kwargs): # noqa ARG002 + super().__init__(**kwargs) self.population_end_time = datetime.datetime.now() def __repr__(self): return ( - f"" + f'' ) @@ -122,12 +122,12 @@ class Model(Base): m = Column(Integer) name = Column(String(200)) p_model = Column(Float) - particles = relationship("Particle") + particles = relationship('Particle') def __repr__(self): return ( - f"" + f'' ) @@ -136,8 +136,8 @@ class Particle(Base): id = Column(Integer, primary_key=True) model_id = Column(Integer, ForeignKey('models.id')) w = Column(Float) - parameters = relationship("Parameter") - samples = relationship("Sample") + parameters = relationship('Parameter') + samples = relationship('Sample') proposal_id = Column(Integer, default=0) @@ -149,7 +149,7 @@ class Parameter(Base): value = Column(Float) def __repr__(self): - return f"" + return f'' class Sample(Base): @@ -157,7 +157,7 @@ class Sample(Base): id = Column(Integer, primary_key=True) particle_id = Column(Integer, ForeignKey('particles.id')) distance = Column(Float) - summary_statistics = relationship("SummaryStatistic") + summary_statistics = relationship('SummaryStatistic') class SummaryStatistic(Base): @@ -170,4 +170,4 @@ class SummaryStatistic(Base): def datetime2str(datetime: datetime.datetime) -> str: """Format print datetime.""" - return datetime.strftime("%Y-%m-%d %H:%M:%S") + return datetime.strftime('%Y-%m-%d %H:%M:%S') diff --git a/pyabc/storage/history.py b/pyabc/storage/history.py index a2f7a099d..d5b02ae54 100644 --- a/pyabc/storage/history.py +++ b/pyabc/storage/history.py @@ -4,7 +4,6 @@ import os import tempfile from functools import wraps -from typing import List, Tuple, Union import numpy as np import pandas as pd @@ -27,15 +26,15 @@ ) from .version import __db_version__ -logger = logging.getLogger("ABC.History") +logger = logging.getLogger('ABC.History') -SQLITE_STR = "sqlite:///" +SQLITE_STR = 'sqlite:///' def with_session(f): @wraps(f) - def f_wrapper(self: "History", *args, **kwargs): - logger.debug(f"Database access through {f.__name__}") + def f_wrapper(self: 'History', *args, **kwargs): + logger.debug(f'Database access through {f.__name__}') no_session = self._session is None and self._engine is None if no_session: self._make_session() @@ -48,16 +47,16 @@ def f_wrapper(self: "History", *args, **kwargs): def internal_docstring_warning(f): - first_line = f.__doc__.split("\n")[1] + first_line = f.__doc__.split('\n')[1] indent_level = len(first_line) - len(first_line.lstrip()) - indent = " " * indent_level + indent = ' ' * indent_level warning = ( - "\n\n" + '\n\n' + indent - + "**Note.** This function is called by the :class:`pyabc.ABCSMC` " - "class internally. " - "You should most likely not find it necessary to call " - "this method under normal circumstances." + + '**Note.** This function is called by the :class:`pyabc.ABCSMC` ' + 'class internally. ' + 'You should most likely not find it necessary to call ' + 'this method under normal circumstances.' ) f.__doc__ += warning @@ -81,7 +80,7 @@ def git_hash(): return hash_ -def create_sqlite_db_id(dir_: str = None, file_: str = "pyabc_test.db"): +def create_sqlite_db_id(dir_: str = None, file_: str = 'pyabc_test.db'): """ Convenience function to create an sqlite database identifier which can be understood by sqlalchemy. @@ -98,13 +97,13 @@ def create_sqlite_db_id(dir_: str = None, file_: str = "pyabc_test.db"): """ if dir_ is None: dir_ = tempfile.gettempdir() - return "sqlite:///" + os.path.join(dir_, file_) + return 'sqlite:///' + os.path.join(dir_, file_) def database_exists(db: str) -> bool: """Does the database file exist already?""" return ( - db != "sqlite://" + db != 'sqlite://' and os.path.exists(db[len(SQLITE_STR) :]) and os.path.getsize(db[len(SQLITE_STR) :]) > 0 ) @@ -157,7 +156,7 @@ def __init__( """ db_exists = database_exists(db) if not create and not db_exists: - raise ValueError(f"Database file {db} does not exist.") + raise ValueError(f'Database file {db} does not exist.') self.db = db self.stores_sum_stats = stores_sum_stats @@ -179,17 +178,17 @@ def __init__( self._id = _id def db_file(self): - f = self.db.split(":")[-1][3:] + f = self.db.split(':')[-1][3:] return f @property def in_memory(self): return ( - self._engine is not None and str(self._engine.url) == "sqlite://" + self._engine is not None and str(self._engine.url) == 'sqlite://' ) @property - def db_size(self) -> Union[int, str]: + def db_size(self) -> int | str: """ Size of the database. @@ -204,7 +203,7 @@ def db_size(self) -> Union[int, str]: try: return os.path.getsize(self.db_file()) / 10**6 except FileNotFoundError: - return "Cannot calculate size" + return 'Cannot calculate size' @with_session def all_runs(self): @@ -233,11 +232,11 @@ def _check_version(self): # compare to current version if version != __db_version__: raise AssertionError( - f"Database has version {version}, latest format version is " - f"{__db_version__}. Thus, not all queries may work correctly. " - "Consider migrating the database to the latest version via " - "`abc-migrate`. Check `abc-migrate --help` and the " - "documentation for further information." + f'Database has version {version}, latest format version is ' + f'{__db_version__}. Thus, not all queries may work correctly. ' + 'Consider migrating the database to the latest version via ' + '`abc-migrate`. Check `abc-migrate --help` and the ' + 'documentation for further information.' ) @with_session @@ -270,11 +269,11 @@ def id(self, val): if val is None: val = self._find_latest_id() elif val not in [obj.id for obj in self._session.query(ABCSMC).all()]: - raise ValueError(f"Specified id {val} does not exist in database.") + raise ValueError(f'Specified id {val} does not exist in database.') self._id = val @with_session - def alive_models(self, t: int = None) -> List: + def alive_models(self, t: int = None) -> list: """ Get the models which are still alive at time `t`. @@ -290,10 +289,7 @@ def alive_models(self, t: int = None) -> List: models which are still alive. """ - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) alive = ( self._session.query(Model.m) @@ -308,7 +304,7 @@ def alive_models(self, t: int = None) -> List: @with_session def get_distribution( self, m: int = 0, t: int = None - ) -> Tuple[pd.DataFrame, np.ndarray]: + ) -> tuple[pd.DataFrame, np.ndarray]: """ Returns the weighted population sample for model m and timepoint t as a tuple. @@ -328,10 +324,7 @@ def get_distribution( * w: are the weights associated with each parameter """ m = int(m) - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) query = ( self._session.query( @@ -347,13 +340,13 @@ def get_distribution( ) df = pd.read_sql_query(query.statement, self._engine) pars = df.pivot( - index="id", columns="name", values="value" + index='id', columns='name', values='value' ).sort_index() - w = df[["id", "w"]].drop_duplicates().set_index("id").sort_index() + w = df[['id', 'w']].drop_duplicates().set_index('id').sort_index() w_arr = w.w.values if w_arr.size > 0 and not np.isclose(w_arr.sum(), 1): raise AssertionError( - "Weight not close to 1, w.sum()={}".format(w_arr.sum()) + f'Weight not close to 1, w.sum()={w_arr.sum()}' ) return pars, w_arr @@ -414,8 +407,8 @@ def get_all_populations(self): df = pd.read_sql_query(query.statement, self._engine) particles = self.get_nr_particles_per_population() particles.index += 1 - df["particles"] = particles - df = df.rename(columns={"nr_samples": "samples"}) + df['particles'] = particles + df = df.rename(columns={'nr_samples': 'samples'}) return df @with_session @@ -426,7 +419,7 @@ def store_initial_data( options: dict, observed_summary_statistics: dict, ground_truth_parameter: dict, - model_names: List[str], + model_names: list[str], distance_function_json_str: str, eps_function_json_str: str, population_strategy_json_str: str, @@ -485,7 +478,7 @@ def store_initial_data( ) # log - logger.info(f"Start {abcsmc.start_info()}") + logger.info(f'Start {abcsmc.start_info()}') @with_session @internal_docstring_warning @@ -494,7 +487,7 @@ def store_pre_population( ground_truth_model: int, observed_summary_statistics: dict, ground_truth_parameter: dict, - model_names: List[str], + model_names: list[str], ): """ Store a dummy pre-population containing some configuration data @@ -653,7 +646,7 @@ def _set_version(self): """ # check if version exists already if len(self._session.query(Version).all()) > 0: - raise AssertionError("Cannot set version as already exists.") + raise AssertionError('Cannot set version as already exists.') # set version to latest self._session.add(Version(version_num=__db_version__)) @@ -672,8 +665,8 @@ def _close_session(self): def __getstate__(self): dct = self.__dict__.copy() if self.in_memory: - dct["_engine"] = None - dct["_session"] = None + dct['_engine'] = None + dct['_session'] = None return dct @with_session @@ -693,7 +686,7 @@ def done(self, end_time: datetime.datetime = None): abcsmc = self._session.query(ABCSMC).filter(ABCSMC.id == self.id).one() abcsmc.end_time = end_time self._session.commit() - logger.info(f"Done {abcsmc.end_info()}") + logger.info(f'Done {abcsmc.end_info()}') @with_session def _save_to_population_db( @@ -754,7 +747,7 @@ def _save_to_population_db( # append nested dimension to parameter particle.parameters.append( Parameter( - name=key + "_" + key_dict, value=value_dict + name=key + '_' + key_dict, value=value_dict ) ) else: @@ -771,7 +764,7 @@ def _save_to_population_db( if self.stores_sum_stats: for name, value in py_particle.sum_stat.items(): if name is None: - raise Exception("Summary statistics need names.") + raise Exception('Summary statistics need names.') sample.summary_statistics.append( SummaryStatistic(name=name, value=value) ) @@ -780,7 +773,7 @@ def _save_to_population_db( self._session.commit() # log - logger.debug("Appended population") + logger.debug('Appended population') @internal_docstring_warning def append_population( @@ -820,9 +813,7 @@ def append_population( ) @with_session - def get_model_probabilities( - self, t: Union[int, None] = None - ) -> pd.DataFrame: + def get_model_probabilities(self, t: int | None = None) -> pd.DataFrame: """ Model probabilities. @@ -853,16 +844,16 @@ def get_model_probabilities( # TODO this is a mess if t is not None: p_models_df = pd.DataFrame( - [p[:2] for p in p_models], columns=["p", "m"] - ).set_index("m") + [p[:2] for p in p_models], columns=['p', 'm'] + ).set_index('m') # TODO the following line is redundant # only models with no-zero weight are stored for each population p_models_df = p_models_df[p_models_df.p >= 0] return p_models_df else: p_models_df = ( - pd.DataFrame(p_models, columns=["p", "m", "t"]) - .pivot(index="t", columns="m", values="p") + pd.DataFrame(p_models, columns=['p', 'm', 't']) + .pivot(index='t', columns='m', values='p') .fillna(0) ) return p_models_df @@ -882,10 +873,7 @@ def nr_of_models_alive(self, t: int = None) -> int: Number of models still alive. None is for the last population """ - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) model_probs = self.get_model_probabilities(t) @@ -910,10 +898,7 @@ def get_weighted_distances(self, t: int = None) -> pd.DataFrame: The dataframe has column "w" for the weights and column "distance" for the distances. """ - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) models = ( self._session.query(Model) @@ -997,7 +982,7 @@ def n_populations(self): @with_session def get_weighted_sum_stats_for_model( self, m: int = 0, t: int = None - ) -> Tuple[np.ndarray, List]: + ) -> tuple[np.ndarray, list]: """ Summary statistics for model `m`. The weights sum to 1, unless there were multiple acceptances per particle. @@ -1016,10 +1001,7 @@ def get_weighted_sum_stats_for_model( * sum_stats: list of summary statistics """ m = int(m) - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) particles = ( self._session.query(Particle) @@ -1046,7 +1028,7 @@ def get_weighted_sum_stats_for_model( @with_session def get_weighted_sum_stats( self, t: int = None - ) -> Tuple[List[float], List[dict]]: + ) -> tuple[list[float], list[dict]]: """ Population's weighted summary statistics. These weights do not necessarily sum up to 1. @@ -1065,10 +1047,7 @@ def get_weighted_sum_stats( statistics. """ - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) models = ( self._session.query(Model) @@ -1112,10 +1091,7 @@ def get_population(self, t: int = None): t: int, optional (default = self.max_t) The population index. """ - if t is None: - t = self.max_t - else: - t = int(t) + t = self.max_t if t is None else int(t) models = ( self._session.query(Model) @@ -1153,7 +1129,7 @@ def get_population(self, t: int = None): # simulations # TODO this is legacy from when there were multiple if len(particle.samples) != 1: - raise AssertionError("There should be exactly one sample.") + raise AssertionError('There should be exactly one sample.') sample = particle.samples[0] # summary statistics py_sum_stat = {} @@ -1193,8 +1169,8 @@ def get_population_strategy(self): def get_population_extended( self, *, - m: Union[int, None] = None, - t: Union[int, str] = "last", + m: int | None = None, + t: int | str = 'last', tidy: bool = True, ) -> pd.DataFrame: """ @@ -1224,17 +1200,17 @@ def get_population_extended( self._session.query( Population.t, Population.epsilon, - Population.nr_samples.label("samples"), + Population.nr_samples.label('samples'), Model.m, - Model.name.label("model_name"), + Model.name.label('model_name'), Model.p_model, Particle.w, - Particle.id.label("particle_id"), + Particle.id.label('particle_id'), Sample.distance, - Parameter.name.label("par_name"), - Parameter.value.label("par_val"), - SummaryStatistic.name.label("sumstat_name"), - SummaryStatistic.value.label("sumstat_val"), + Parameter.name.label('par_name'), + Parameter.value.label('par_val'), + SummaryStatistic.name.label('sumstat_name'), + SummaryStatistic.value.label('sumstat_val'), ) .join(ABCSMC) .join(Model) @@ -1248,58 +1224,55 @@ def get_population_extended( if m is not None: query = query.filter(Model.m == m) - if t == "last": + if t == 'last': t = self.max_t # if t is not "all", filter for time point t - if t != "all": + if t != 'all': query = query.filter(Population.t == t) df = pd.read_sql_query(query.statement, self._engine) if len(df.m.unique()) == 1: - del df["m"] - del df["model_name"] - del df["p_model"] + del df['m'] + del df['model_name'] + del df['p_model'] if isinstance(t, int): - del df["t"] - - if tidy: - if isinstance(t, int) and "m" not in df: - df = df.set_index("particle_id") - df_unique = df[["distance", "w"]].drop_duplicates() - - df_par = ( - df[["par_name", "par_val"]] - .reset_index() - .drop_duplicates(subset=["particle_id", "par_name"]) - .pivot( - index="particle_id", - columns="par_name", - values="par_val", - ) + del df['t'] + + if tidy and isinstance(t, int) and 'm' not in df: + df = df.set_index('particle_id') + df_unique = df[['distance', 'w']].drop_duplicates() + + df_par = ( + df[['par_name', 'par_val']] + .reset_index() + .drop_duplicates(subset=['particle_id', 'par_name']) + .pivot( + index='particle_id', + columns='par_name', + values='par_val', ) - df_par.columns = ["par_" + c for c in df_par.columns] - - df_sumstat = ( - df[["sumstat_name", "sumstat_val"]] - .reset_index() - .drop_duplicates(subset=["particle_id", "sumstat_name"]) - .pivot( - index="particle_id", - columns="sumstat_name", - values="sumstat_val", - ) + ) + df_par.columns = ['par_' + c for c in df_par.columns] + + df_sumstat = ( + df[['sumstat_name', 'sumstat_val']] + .reset_index() + .drop_duplicates(subset=['particle_id', 'sumstat_name']) + .pivot( + index='particle_id', + columns='sumstat_name', + values='sumstat_val', ) - df_sumstat.columns = [ - "sumstat_" + c for c in df_sumstat.columns - ] - - df_tidy = df_unique.merge( - df_par, left_index=True, right_index=True - ).merge(df_sumstat, left_index=True, right_index=True) - df = df_tidy + ) + df_sumstat.columns = ['sumstat_' + c for c in df_sumstat.columns] + + df_tidy = df_unique.merge( + df_par, left_index=True, right_index=True + ).merge(df_sumstat, left_index=True, right_index=True) + df = df_tidy return df diff --git a/pyabc/storage/json.py b/pyabc/storage/json.py index af1bdbc6b..d2f91fdd8 100644 --- a/pyabc/storage/json.py +++ b/pyabc/storage/json.py @@ -40,7 +40,7 @@ def load_dict_from_json(file_: str, key_type: type = int): ------- dct: The json file contents. """ - with open(file_, 'r') as f: + with open(file_) as f: _dct = json.load(f) dct = {} for key, val in _dct.items(): diff --git a/pyabc/sumstat/base.py b/pyabc/sumstat/base.py index 3a91285c7..3439cae36 100644 --- a/pyabc/sumstat/base.py +++ b/pyabc/sumstat/base.py @@ -1,7 +1,7 @@ """Basic summary statistics.""" from abc import ABC, abstractmethod -from typing import Callable, Dict, List, Tuple, Union +from collections.abc import Callable import numpy as np @@ -25,19 +25,19 @@ def __init__(self, pre: 'Sumstat' = None): pre: Previously applied summary statistics, enables chaining. """ # data keys (for correct order) - self.x_keys: Union[List[str], None] = None + self.x_keys: list[str] | None = None # observed data - self.x_0: Union[dict, None] = None + self.x_0: dict | None = None # previous chained statistics - self.pre: Union['Sumstat', None] = pre + self.pre: Sumstat | None = pre # ids - self.ids: Union[List[str], None] = None + self.ids: list[str] | None = None @abstractmethod def __call__( self, - data: Union[dict, np.ndarray], - ) -> Union[np.ndarray, Dict[str, float]]: + data: dict | np.ndarray, + ) -> np.ndarray | dict[str, float]: """Calculate summary statistics. Parameters @@ -73,7 +73,7 @@ def initialize( The total number of simulations so far. """ # record data keys - self.x_keys: List[str] = list(x_0.keys()) + self.x_keys: list[str] = list(x_0.keys()) # record observed data self.x_0: dict = x_0 # initialize previous statistics @@ -149,16 +149,16 @@ def is_adaptive(self) -> bool: return self.pre.is_adaptive() return False - def get_ids(self) -> List[str]: + def get_ids(self) -> list[str]: """Get ids/labels for the summary statistics. Defaults to indexing the statistics as `S_{ix}`. """ s_0 = self(self.x_0) - return [f"s_{ix}" for ix in range(s_0.size)] + return [f's_{ix}' for ix in range(s_0.size)] def __str__(self) -> str: - return f"<{self.__class__.__name__} pre={self.pre.__str__()}>" + return f'<{self.__class__.__name__} pre={self.pre.__str__()}>' class IdentitySumstat(Sumstat): @@ -166,9 +166,9 @@ class IdentitySumstat(Sumstat): def __init__( self, - trafos: List[Callable[[np.ndarray], np.ndarray]] = None, + trafos: list[Callable[[np.ndarray], np.ndarray]] = None, pre: Sumstat = None, - shape_out: Tuple[int, ...] = (-1,), + shape_out: tuple[int, ...] = (-1,), ): """ Parameters @@ -188,11 +188,11 @@ def __init__( deriving from Sumstat or IdentitySumstat. """ super().__init__(pre=pre) - self.trafos: List[Callable[[np.ndarray], np.ndarray]] = trafos - self.shape_out: Tuple[int, ...] = shape_out + self.trafos: list[Callable[[np.ndarray], np.ndarray]] = trafos + self.shape_out: tuple[int, ...] = shape_out @io_dict2arr - def __call__(self, data: Union[dict, np.ndarray]) -> np.ndarray: + def __call__(self, data: dict | np.ndarray) -> np.ndarray: """ Returns ------- @@ -225,6 +225,6 @@ def get_ids(self): def __str__(self) -> str: return ( - f"<{self.__class__.__name__} pre={self.pre}, " - f"trafos={self.trafos}>" + f'<{self.__class__.__name__} pre={self.pre}, ' + f'trafos={self.trafos}>' ) diff --git a/pyabc/sumstat/learn.py b/pyabc/sumstat/learn.py index cf401b7a6..89f8fba11 100644 --- a/pyabc/sumstat/learn.py +++ b/pyabc/sumstat/learn.py @@ -1,7 +1,7 @@ """Summary statistics learning.""" import logging -from typing import Callable, Collection, List, Union +from collections.abc import Callable, Collection import numpy as np @@ -24,7 +24,7 @@ from .base import IdentitySumstat, Sumstat from .subset import IdSubsetter, Subsetter -logger = logging.getLogger("ABC.Sumstat") +logger = logging.getLogger('ABC.Sumstat') class PredictorSumstat(Sumstat): @@ -47,8 +47,8 @@ class PredictorSumstat(Sumstat): def __init__( self, - predictor: Union[Predictor, Callable], - fit_ixs: Union[EventIxs, Collection[int], int] = None, + predictor: Predictor | Callable, + fit_ixs: EventIxs | Collection[int] | int = None, all_particles: bool = True, normalize_labels: bool = True, fitted: bool = False, @@ -107,7 +107,7 @@ def __init__( if fit_ixs is None: fit_ixs = {9, 15} self.fit_ixs: EventIxs = EventIxs.to_instance(fit_ixs) - logger.debug(f"Fit model ixs: {self.fit_ixs}") + logger.debug(f'Fit model ixs: {self.fit_ixs}') self.all_particles: bool = all_particles self.normalize_labels: bool = normalize_labels @@ -228,7 +228,7 @@ def is_adaptive(self) -> bool: ) @io_dict2arr - def __call__(self, data: Union[dict, np.ndarray]): + def __call__(self, data: dict | np.ndarray): # check whether to return data directly if not self.fitted and not self.pre_before_fit: return data @@ -245,20 +245,20 @@ def __call__(self, data: Union[dict, np.ndarray]): ).flatten() if sumstat.size != len(self.par_trafo): - raise AssertionError("Predictor should return #parameters values") + raise AssertionError('Predictor should return #parameters values') return sumstat def __str__(self) -> str: return ( - f"<{self.__class__.__name__} pre={self.pre}, " - f"predictor={self.predictor}>" + f'<{self.__class__.__name__} pre={self.pre}, ' + f'predictor={self.predictor}>' ) - def get_ids(self) -> List[str]: + def get_ids(self) -> list[str]: # label by parameter keys if self.fitted: - return [f"s_{key}" for key in self.par_trafo.get_ids()] + return [f's_{key}' for key in self.par_trafo.get_ids()] if not self.pre_before_fit: return dict2arrlabels(self.x_0, keys=self.x_keys) return self.pre.get_ids() diff --git a/pyabc/sumstat/subset.py b/pyabc/sumstat/subset.py index 85757f990..60d4b242b 100644 --- a/pyabc/sumstat/subset.py +++ b/pyabc/sumstat/subset.py @@ -2,7 +2,6 @@ import logging from abc import ABC, abstractmethod -from typing import Tuple import numpy as np @@ -14,7 +13,7 @@ skl_mx = None -logger = logging.getLogger("ABC.Sumstat") +logger = logging.getLogger('ABC.Sumstat') class Subsetter(ABC): @@ -37,7 +36,7 @@ def select( x: np.ndarray, y: np.ndarray, w: np.ndarray, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Select samples for model training. This is the main method. Parameters @@ -61,7 +60,7 @@ def select( x: np.ndarray, y: np.ndarray, w: np.ndarray, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Just return x, y, w unchanged.""" return x, y, w @@ -107,8 +106,8 @@ def __init__( ): if skl_mx is None: raise ImportError( - "This class requires an installation of scikit-learn. " - "Install e.g. via `pip install pyabc[scikit-learn]`" + 'This class requires an installation of scikit-learn. ' + 'Install e.g. via `pip install pyabc[scikit-learn]`' ) self.n_components_min: int = n_components_min @@ -132,7 +131,7 @@ def select( x: np.ndarray, y: np.ndarray, w: np.ndarray, - ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Select based on GMM clusters.""" # normalize if self.normalize_labels: @@ -166,9 +165,9 @@ def select( x_new, y_new, w_new = x[selected], y[selected], w[selected] logger.info( - f"Subsetting: #clusters: {n_components}, " - f"target cluster points: {sum(in_cluster)}, using {len(y_new)} " - f"(BICs: {bics})", + f'Subsetting: #clusters: {n_components}, ' + f'target cluster points: {sum(in_cluster)}, using {len(y_new)} ' + f'(BICs: {bics})', ) return x_new, y_new, w_new @@ -250,6 +249,6 @@ def get_augmented_subset( in_cluster[ixs_not_in_cluster[ixs_nearest]] = True if sum(in_cluster) != desired: - raise AssertionError("Unexpected number of entries.") + raise AssertionError('Unexpected number of entries.') return in_cluster diff --git a/pyabc/transition/base.py b/pyabc/transition/base.py index 9a2f74efb..818edf7c4 100644 --- a/pyabc/transition/base.py +++ b/pyabc/transition/base.py @@ -1,6 +1,5 @@ import logging from abc import abstractmethod -from typing import Dict, Tuple, Union import numpy as np import pandas as pd @@ -12,7 +11,7 @@ from .predict_population_size import predict_population_size from .transitionmeta import TransitionMeta -logger = logging.getLogger("ABC.Transition") +logger = logging.getLogger('ABC.Transition') class Transition(BaseEstimator, metaclass=TransitionMeta): @@ -64,7 +63,7 @@ def rvs_single(self) -> Parameter: A sample from the fitted model. """ - def rvs(self, size: int = None) -> Union[Parameter, pd.DataFrame]: + def rvs(self, size: int = None) -> Parameter | pd.DataFrame: """ Sample from the density. @@ -92,8 +91,8 @@ def rvs(self, size: int = None) -> Union[Parameter, pd.DataFrame]: @abstractmethod def pdf( - self, x: Union[Parameter, pd.Series, pd.DataFrame] - ) -> Union[float, np.ndarray]: + self, x: Parameter | pd.Series | pd.DataFrame + ) -> float | np.ndarray: """ Evaluate the probability density function (PDF) at `x`. @@ -120,7 +119,7 @@ def score(self, X: pd.DataFrame, w: np.ndarray): def no_meaningful_particles(self) -> bool: return len(self.X) == 0 or self.no_parameters - def mean_cv(self, n_samples: Union[None, int] = None) -> float: + def mean_cv(self, n_samples: None | int = None) -> float: """ Estimate the uncertainty on the KDE. @@ -204,7 +203,7 @@ class AggregatedTransition(Transition): transition kernel to be used for those parameters. """ - def __init__(self, mapping: Dict[Union[str, Tuple[str, ...]], Transition]): + def __init__(self, mapping: dict[str | tuple[str, ...], Transition]): # normalize input tidy_mapping = {} for keys, transition in mapping.items(): @@ -222,7 +221,7 @@ def fit(self, X: pd.DataFrame, w: np.ndarray) -> None: transition.fit(X_for_keys, w) def rvs_single(self) -> Parameter: - sample = Parameter({key: np.nan for key in self.X.columns}) + sample = Parameter(dict.fromkeys(self.X.columns, np.nan)) for transition in self.mapping.values(): sample_for_keys = transition.rvs_single() # in-place update @@ -230,8 +229,8 @@ def rvs_single(self) -> Parameter: return sample def pdf( - self, x: Union[Parameter, pd.Series, pd.DataFrame] - ) -> Union[float, np.ndarray]: + self, x: Parameter | pd.Series | pd.DataFrame + ) -> float | np.ndarray: # density pd = 1.0 for keys, transition in self.mapping.items(): diff --git a/pyabc/transition/jump.py b/pyabc/transition/jump.py index dcf655e68..ab9f0d2cb 100644 --- a/pyabc/transition/jump.py +++ b/pyabc/transition/jump.py @@ -1,7 +1,5 @@ """A discrete jump transition function.""" -from typing import Union - import numpy as np import pandas as pd @@ -24,13 +22,13 @@ class PerturbationKernel: def __init__(self, domain: np.ndarray, p_stay: float = 0.7): if len(np.unique(domain)) != len(domain): - raise ValueError("The domain contains duplicates.") + raise ValueError('The domain contains duplicates.') self.domain = domain if len(domain) == 1: p_stay = 1.0 if not 0 <= p_stay <= 1: - raise ValueError("p_stay must be in [0, 1].") + raise ValueError('p_stay must be in [0, 1].') self.p_stay = p_stay self.p_move = (1 - p_stay) / (len(self.domain) - 1) @@ -42,7 +40,7 @@ def __init__(self, domain: np.ndarray, p_stay: float = 0.7): def rvs(self, a: float) -> float: """Sample a kernel jump from parameter `a` to another parameter.""" if a not in self.domain: - raise ValueError("The parameter value is not in the domain.") + raise ValueError('The parameter value is not in the domain.') if len(self.domain) == 1: return a @@ -64,7 +62,7 @@ def pdf(self, b: float, a: float) -> float: """Probability mass function for a jump to target `b` from source `a`.""" if a not in self.domain or b not in self.domain: raise ValueError( - "At least one parameter value is not in the domain." + 'At least one parameter value is not in the domain.' ) return self.p_stay if b == a else self.p_move @@ -98,7 +96,7 @@ def fit(self, X: pd.DataFrame, w: np.ndarray) -> None: # this is only meant to be used with a single parameter if len(X.columns) != 1: raise ValueError( - "This transition can only handle a single parameter." + 'This transition can only handle a single parameter.' ) # compute a single weight per unique parameter value x = np.array(X).flatten() @@ -121,12 +119,12 @@ def rvs_single(self) -> Parameter: return Parameter({self.X.columns[0]: value}) def pdf( - self, x: Union[Parameter, pd.Series, pd.DataFrame] - ) -> Union[float, np.ndarray]: + self, x: Parameter | pd.Series | pd.DataFrame + ) -> float | np.ndarray: """Compute the probability mass function at `x`.""" # extract values key = self.X.columns[0] - if isinstance(x, (Parameter, pd.Series)): + if isinstance(x, Parameter | pd.Series): x = np.array([[x[key]]]) else: x = x.to_numpy() diff --git a/pyabc/transition/local_transition.py b/pyabc/transition/local_transition.py index 9f9f500e9..f87b53ca1 100644 --- a/pyabc/transition/local_transition.py +++ b/pyabc/transition/local_transition.py @@ -1,5 +1,4 @@ import logging -from typing import Union import numpy as np import numpy.linalg as la @@ -11,7 +10,7 @@ from .exceptions import NotEnoughParticles from .util import smart_cov -logger = logging.getLogger("ABC.Transition") +logger = logging.getLogger('ABC.Transition') class LocalTransition(Transition): @@ -65,10 +64,7 @@ def __init__(self, k=None, k_fraction=1 / 4, scaling=1): @property def k(self): if self.k_fraction is not None: - if self.w is None: - k_ = 0 - else: - k_ = int(self.k_fraction * len(self.w)) + k_ = 0 if self.w is None else int(self.k_fraction * len(self.w)) else: k_ = self._k @@ -79,9 +75,9 @@ def k(self): return max([k_, self.MIN_K, dim]) - def fit(self, X, w): + def fit(self, X, w=None): # noqa ARG002 if len(X) == 0: - raise NotEnoughParticles("Fitting not possible.") + raise NotEnoughParticles('Fitting not possible.') self.X_arr = X.values ctree = cKDTree(self.X_arr) @@ -99,12 +95,12 @@ def fit(self, X, w): ) if not np.isreal(self.normalization).all(): - raise Exception("Normalization not real") + raise Exception('Normalization not real') self.normalization = np.real(self.normalization) - def pdf(self, x: Union[Parameter, pd.Series, pd.DataFrame]): + def pdf(self, x: Parameter | pd.Series | pd.DataFrame): # convert to numpy array in correct order - if isinstance(x, (Parameter, pd.Series)): + if isinstance(x, Parameter | pd.Series): x = np.array([x[key] for key in self.X.columns]) else: x = x[self.X.columns].to_numpy() @@ -117,7 +113,7 @@ def pdf(self, x: Union[Parameter, pd.Series, pd.DataFrame]): def _pdf_single(self, x: np.ndarray): distance = self.X_arr - x cov_distance = np.einsum( - "ij,ijk,ik->i", distance, self.inv_covs, distance + 'ij,ijk,ik->i', distance, self.inv_covs, distance ) return float( np.average( diff --git a/pyabc/transition/model.py b/pyabc/transition/model.py index a946c0cb4..e8a243042 100644 --- a/pyabc/transition/model.py +++ b/pyabc/transition/model.py @@ -1,5 +1,3 @@ -from typing import Union - from ..random_variables import RV @@ -16,7 +14,7 @@ class ModelPerturbationKernel: """ def __init__( - self, nr_of_models: int, probability_to_stay: Union[float, None] = None + self, nr_of_models: int, probability_to_stay: float | None = None ): self.nr_of_models = nr_of_models if nr_of_models == 1: diff --git a/pyabc/transition/multivariatenormal.py b/pyabc/transition/multivariatenormal.py index e0045c2cc..1f830c74f 100644 --- a/pyabc/transition/multivariatenormal.py +++ b/pyabc/transition/multivariatenormal.py @@ -1,4 +1,4 @@ -from typing import Callable, Union +from collections.abc import Callable import numpy as np import pandas as pd @@ -67,9 +67,9 @@ def __init__( self.scaling: float = scaling self.bandwidth_selector: BandwidthSelector = bandwidth_selector # base population as an array - self._X_arr: Union[np.ndarray, None] = None + self._X_arr: np.ndarray | None = None # perturbation covariance matrix - self.cov: Union[np.ndarray, None] = None + self.cov: np.ndarray | None = None # normal perturbation distribution self.normal = None # cache a range array @@ -77,7 +77,7 @@ def __init__( def fit(self, X: pd.DataFrame, w: np.ndarray) -> None: if len(X) == 0: - raise NotEnoughParticles("Fitting not possible.") + raise NotEnoughParticles('Fitting not possible.') self._X_arr = X.values sample_cov = smart_cov(self._X_arr, w) @@ -91,7 +91,7 @@ def fit(self, X: pd.DataFrame, w: np.ndarray) -> None: # cache range array self._range = np.arange(len(self.X)) - def rvs(self, size: int = None) -> Union[Parameter, pd.DataFrame]: + def rvs(self, size: int = None) -> Parameter | pd.DataFrame: if size is None: return self.rvs_single() sample_ind = np.random.choice( @@ -113,10 +113,10 @@ def rvs_single(self) -> Parameter: def pdf( self, - x: Union[Parameter, pd.Series, pd.DataFrame], - ) -> Union[float, np.ndarray]: + x: Parameter | pd.Series | pd.DataFrame, + ) -> float | np.ndarray: # convert to numpy array in correct order - if isinstance(x, (Parameter, pd.Series)): + if isinstance(x, Parameter | pd.Series): x = np.array([x[key] for key in self.X.columns]) else: x = x[self.X.columns].to_numpy() diff --git a/pyabc/transition/predict_population_size.py b/pyabc/transition/predict_population_size.py index 464811640..5e764ce95 100644 --- a/pyabc/transition/predict_population_size.py +++ b/pyabc/transition/predict_population_size.py @@ -3,9 +3,9 @@ from pyabc.cv import fitpowerlaw -logger = logging.getLogger("ABC.Transition") +logger = logging.getLogger('ABC.Transition') -CVEstimate = namedtuple("CVEstimate", "n_estimated n_samples_list cvs f popt") +CVEstimate = namedtuple('CVEstimate', 'n_estimated n_samples_list cvs f popt') def predict_population_size( @@ -59,7 +59,7 @@ def predict_population_size( return CVEstimate(suggested_pop_size, n_samples_list, cvs, f, popt) except RuntimeError: logger.warning( - "Power law fit failed. " - "Falling back to current nr particles {}".format(current_pop_size) + 'Power law fit failed. ' + f'Falling back to current nr particles {current_pop_size}' ) return CVEstimate(current_pop_size, n_samples_list, cvs, None, None) diff --git a/pyabc/transition/randomwalk.py b/pyabc/transition/randomwalk.py index 0ca85effa..ebbc71f8e 100644 --- a/pyabc/transition/randomwalk.py +++ b/pyabc/transition/randomwalk.py @@ -1,5 +1,3 @@ -from typing import Union - import numpy as np import pandas as pd import scipy.stats as stats @@ -58,21 +56,21 @@ def rvs_single(self) -> Parameter: return Parameter(perturbed_point) def pdf( - self, x: Union[Parameter, pd.Series, pd.DataFrame] - ) -> Union[float, np.ndarray]: + self, x: Parameter | pd.Series | pd.DataFrame + ) -> float | np.ndarray: """ Evaluate the probability mass function (PMF) at `x`. """ # convert to numpy array in correct order - if isinstance(x, (Parameter, pd.Series)): + if isinstance(x, Parameter | pd.Series): x = np.array([x[key] for key in self.X.columns]) else: x = x[self.X.columns].to_numpy() if not np.all(np.isclose(x, x.astype(int))): raise ValueError( - f"Transition can only handle integer values, not fulfilled " - f"by x={x}." + f'Transition can only handle integer values, not fulfilled ' + f'by x={x}.' ) if len(x.shape) == 1: diff --git a/pyabc/transition/transitionmeta.py b/pyabc/transition/transitionmeta.py index 07ec37006..13c4b8f96 100644 --- a/pyabc/transition/transitionmeta.py +++ b/pyabc/transition/transitionmeta.py @@ -1,6 +1,5 @@ import functools from abc import ABCMeta -from typing import Union import numpy as np import pandas as pd @@ -15,9 +14,8 @@ def fit(self, X: pd.DataFrame, w: np.ndarray): self.no_parameters = True return self.no_parameters = False - if w.size > 0: - if not np.isclose(w.sum(), 1): - w /= w.sum() + if w.size > 0 and not np.isclose(w.sum(), 1): + w /= w.sum() f(self, X, w) return fit @@ -25,7 +23,7 @@ def fit(self, X: pd.DataFrame, w: np.ndarray): def wrap_pdf(f): @functools.wraps(f) - def pdf(self, x: Union[pd.Series, pd.DataFrame]): + def pdf(self, x: pd.Series | pd.DataFrame): if self.no_parameters: return 1 return f(self, x) diff --git a/pyabc/util/dict2arr.py b/pyabc/util/dict2arr.py index 1e205cc28..4d0602f77 100644 --- a/pyabc/util/dict2arr.py +++ b/pyabc/util/dict2arr.py @@ -1,13 +1,12 @@ """Transform dictionaries to arrays.""" from numbers import Number -from typing import List, Union import numpy as np import pandas as pd -def dict2arr(dct: Union[dict, np.ndarray], keys: List) -> np.ndarray: +def dict2arr(dct: dict | np.ndarray, keys: list) -> np.ndarray: """Convert dictionary to 1d array, in specified key order. Parameters @@ -26,7 +25,7 @@ def dict2arr(dct: Union[dict, np.ndarray], keys: List) -> np.ndarray: arr = [] for key in keys: val = dct[key] - if isinstance(val, (pd.DataFrame, pd.Series)): + if isinstance(val, pd.DataFrame | pd.Series): arr.append(val.to_numpy().flatten()) elif isinstance(val, np.ndarray): arr.append(val.flatten()) @@ -34,8 +33,8 @@ def dict2arr(dct: Union[dict, np.ndarray], keys: List) -> np.ndarray: arr.append([val]) else: raise TypeError( - f"Cannot parse variable {key}={val} of type {type(val)} " - "to numeric." + f'Cannot parse variable {key}={val} of type {type(val)} ' + 'to numeric.' ) # for efficiency, directly parse single entries @@ -46,7 +45,7 @@ def dict2arr(dct: Union[dict, np.ndarray], keys: List) -> np.ndarray: return np.asarray(arr) -def dict2arrlabels(dct: dict, keys: List) -> List[str]: +def dict2arrlabels(dct: dict, keys: list) -> list[str]: """Get label array consistent with the output of `dict2arr`. Can be called e.g. once on the observed data and used for logging. @@ -63,21 +62,21 @@ def dict2arrlabels(dct: dict, keys: List) -> List[str]: labels = [] for key in keys: val = dct[key] - if isinstance(val, (pd.DataFrame, pd.Series)): + if isinstance(val, pd.DataFrame | pd.Series): # default flattening mode is 'C', i.e. row-major, i.e. row-by-row for row in range(len(val.index)): for col in val.columns: - labels.append(f"{key}:{col}:{row}") + labels.append(f'{key}:{col}:{row}') elif isinstance(val, np.ndarray): # array can have any dimension, thus just flat indices for ix in range(val.size): - labels.append(f"{key}:{ix}") + labels.append(f'{key}:{ix}') elif isinstance(val, Number): labels.append(key) else: raise TypeError( - f"Cannot parse variable {key}={val} of type {type(val)} " - "to numeric." + f'Cannot parse variable {key}={val} of type {type(val)} ' + 'to numeric.' ) return labels @@ -89,7 +88,7 @@ def io_dict2arr(fun): variable. """ - def wrapped_fun(self, data: Union[dict, np.ndarray], *args, **kwargs): + def wrapped_fun(self, data: dict | np.ndarray, *args, **kwargs): # convert input to array data = dict2arr(data, self.x_keys) # call the actual function diff --git a/pyabc/util/event_ixs.py b/pyabc/util/event_ixs.py index a892b2912..e71fcc369 100644 --- a/pyabc/util/event_ixs.py +++ b/pyabc/util/event_ixs.py @@ -1,5 +1,6 @@ import collections.abc -from typing import Collection, List, Union +from collections.abc import Collection +from typing import Union import numpy as np @@ -12,8 +13,8 @@ class EventIxs: def __init__( self, - ts: Union[Collection[int], int] = None, - sims: Union[Collection[int], int] = None, + ts: Collection[int] | int = None, + sims: Collection[int] | int = None, from_t: int = None, from_sims: int = None, ): @@ -43,7 +44,7 @@ def __init__( for ix in ts: if ix != np.inf and int(ix) != ix: raise AssertionError( - f"Index {ix} must be either inf or an int" + f'Index {ix} must be either inf or an int' ) self.ts: Collection[int] = set(ts) @@ -55,11 +56,11 @@ def __init__( # check conversion to index for sim in sims: if int(sim) != sim: - raise AssertionError(f"Simulation number {sim} must be an int") - self.sims: List[int] = list(sims) + raise AssertionError(f'Simulation number {sim} must be an int') + self.sims: list[int] = list(sims) # track which simulation numbers have been hit - self.sims_hit: List[bool] = [False] * len(self.sims) + self.sims_hit: list[bool] = [False] * len(self.sims) self.from_t: int = from_t self.from_sims: int = from_sims @@ -146,22 +147,22 @@ def requires_calibration(self) -> bool: ) def __repr__(self) -> str: - repr = f"<{self.__class__.__name__}" + repr = f'<{self.__class__.__name__}' if self.ts: - repr += f", ts={self.ts}" + repr += f', ts={self.ts}' if self.sims: - repr += f", sims={self.sims}" + repr += f', sims={self.sims}' if self.from_t is not None: - repr += f", from_t={self.from_t}" + repr += f', from_t={self.from_t}' if self.from_sims is not None: - repr += f", from_sims={self.from_sims}" - repr += ">" + repr += f', from_sims={self.from_sims}' + repr += '>' return repr @staticmethod def to_instance( - maybe_event_ixs: Union["EventIxs", Collection[int], int], - ) -> "EventIxs": + maybe_event_ixs: Union['EventIxs', Collection[int], int], + ) -> 'EventIxs': """Create instance from instance or collection of time points. Parameters diff --git a/pyabc/util/par_trafo.py b/pyabc/util/par_trafo.py index 7c2a77ac8..f59848928 100644 --- a/pyabc/util/par_trafo.py +++ b/pyabc/util/par_trafo.py @@ -1,7 +1,7 @@ """Parameter transformations.""" from abc import ABC, abstractmethod -from typing import Callable, List, Union +from collections.abc import Callable import numpy as np @@ -17,7 +17,8 @@ class ParTrafoBase(ABC): In particular, this can help overcome non-identifiabilities. """ - def initialize(self, keys: List[str]): + @abstractmethod + def initialize(self, keys: list[str]): """Initialize. Called once per analysis.""" @abstractmethod @@ -29,7 +30,7 @@ def __len__(self): """Length of expected parameter transformation.""" @abstractmethod - def get_ids(self) -> List[str]: + def get_ids(self) -> list[str]: """Identifiers for the parameter transformations.""" @@ -45,19 +46,19 @@ class ParTrafo(ParTrafoBase): def __init__( self, - trafos: List[Callable[[np.ndarray], np.ndarray]] = None, - trafo_ids: Union[str, List[str]] = "{par_id}_{trafo_ix}", + trafos: list[Callable[[np.ndarray], np.ndarray]] = None, + trafo_ids: str | list[str] = '{par_id}_{trafo_ix}', ): self.trafos = trafos if not isinstance(trafo_ids, str) and len(trafos) != len(trafo_ids): - raise AssertionError("Lengths of trafos and trafo_ids must match") + raise AssertionError('Lengths of trafos and trafo_ids must match') self.trafo_ids = trafo_ids # to maintain key order - self.keys: Union[List[str], None] = None + self.keys: list[str] | None = None - def initialize(self, keys: List[str]): + def initialize(self, keys: list[str]): # remember key order self.keys = keys @@ -83,13 +84,13 @@ def __len__(self): return len(self.keys) return len(self.keys) * len(self.trafos) - def get_ids(self) -> List[str]: + def get_ids(self) -> list[str]: """ Calculate keys as: {par_id_1}_{trafo_1}, ..., {par_id_n}_{trafo_1}, ..., {par_id_1}_{trafo_m}, ..., {par_id_n}_{trafo_m} """ - par_ids = [f"{key}" for key in self.keys] + par_ids = [f'{key}' for key in self.keys] if self.trafos is None: return par_ids diff --git a/pyabc/util/read_sample.py b/pyabc/util/read_sample.py index ce748a072..48567a6ba 100644 --- a/pyabc/util/read_sample.py +++ b/pyabc/util/read_sample.py @@ -1,7 +1,5 @@ """Read sample to array.""" -from typing import Tuple - import numpy as np from ..population import Sample @@ -40,7 +38,7 @@ def read_sample( sumstat, all_particles: bool, par_trafo: ParTrafoBase, -) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: +) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """Read in sample. Parameters diff --git a/pyabc/visserver/server_dash.py b/pyabc/visserver/server_dash.py index 9a7096d6b..58f5b5050 100644 --- a/pyabc/visserver/server_dash.py +++ b/pyabc/visserver/server_dash.py @@ -25,11 +25,13 @@ static = str(pathlib.Path(__file__).parent.absolute()) + '/assets/' DOWNLOAD_DIR = tempfile.mkdtemp() + '/' db_path = DOWNLOAD_DIR -parameter = "" +parameter = '' square_png = static + 'square_v2.png' -square_base64 = base64.b64encode(open(square_png, 'rb').read()).decode('ascii') +with open(square_png, 'rb') as f: + square_base64 = base64.b64encode(f).decode('ascii') pyABC_png = str(static) + '/pyABC_logo.png' -pyABC_base64 = base64.b64encode(open(pyABC_png, 'rb').read()).decode('ascii') +with open(pyABC_png, 'rb') as f: + pyABC_base64 = base64.b64encode(f).decode('ascii') para_list = [] colors = { 'div': '#595959', @@ -56,20 +58,18 @@ ), html.Img( id='pyABC_logo', - src='data:image/png;base64,{}'.format( - pyABC_base64 - ), - width="300", + src=f'data:image/png;base64,{pyABC_base64}', + width='300', ), html.Br(), html.H3('Visualization web server'), html.Hr(), html.Div( [ - "A copy of the generated files will be" - " save in: ", + 'A copy of the generated files will be' + ' save in: ', dcc.Input( - id="download_path", + id='download_path', placeholder=DOWNLOAD_DIR, type='text', value=DOWNLOAD_DIR, @@ -77,7 +77,7 @@ ], style={'display': 'none'}, ), - html.Div(id="hidden-div", style={"display": "none"}), + html.Div(id='hidden-div', style={'display': 'none'}), dcc.Input( id='upload-data', type='text', @@ -116,7 +116,7 @@ [ html.Div( [ - "ABC runs: ", + 'ABC runs: ', dcc.Dropdown(id='ABC_runs'), ] ), @@ -144,7 +144,7 @@ def parse_contents(filename): if 'db' in filename: # Assume that the user uploaded a db file global db_path - history = h.History("sqlite:///" + db_path) + history = h.History('sqlite:///' + db_path) all_runs = h.History.all_runs(history) list_run_ids = [x.id for x in all_runs] # get file name form full path @@ -152,7 +152,7 @@ def parse_contents(filename): path = Path(filename) last_modified = datetime.fromtimestamp(path.stat().st_mtime) # show the date in the format: 2020-01-01 00:00:00 - last_modified = last_modified.strftime("%Y-%m-%d %H:%M:%S") + last_modified = last_modified.strftime('%Y-%m-%d %H:%M:%S') # get the file size in MB size = path.stat().st_size size_mb = size / 1000000 @@ -168,26 +168,26 @@ def parse_contents(filename): return ( 'There was an error processing this file.', [], - "", + '', dbc.Alert( [ - html.H4("Error!", className="alert-heading"), + html.H4('Error!', className='alert-heading'), html.P( - f"There was an error while loading the database. " - f"{str(e)}.", + f'There was an error while loading the database. ' + f'{str(e)}.', ), ], - id="user_update_alert", + id='user_update_alert', is_open=True, fade=True, - color="danger", + color='danger', dismissable=True, ), ) return html.Div( [ html.Button( - "Name: " + name, + 'Name: ' + name, id='btn-nclicks-1', ), # html.Button( @@ -242,13 +242,13 @@ def prepare_run_detailes(history): 'nr_calibration_particles' ] else: - pop_str_calib = "None" + pop_str_calib = 'None' if 'nr_samples_per_parameter' in history.get_population_strategy(): pop_str_sample_per_par = history.get_population_strategy()[ 'nr_samples_per_parameter' ] else: - pop_str_sample_per_par = "None" + pop_str_sample_per_par = 'None' pop_str_nr_particles = history.get_population_strategy()[ 'nr_particles' @@ -281,12 +281,12 @@ def prepare_run_detailes(history): [ html.Div( [ - html.H5("General info:"), - html.Label("Start time: " + str(start_time)), - html.Label("End time: " + str(end_time)), + html.H5('General info:'), + html.Label('Start time: ' + str(start_time)), + html.Label('End time: ' + str(end_time)), html.Label("Run's ID: " + str(id)), html.Label( - "Number of parameters: " + str(len(para_list)) + 'Number of parameters: ' + str(len(para_list)) ), ], style={ @@ -305,8 +305,8 @@ def prepare_run_detailes(history): ), html.Div( [ - html.H5("Distance function:"), - html.Label("Name: " + str(dist_name)), + html.H5('Distance function:'), + html.Label('Name: ' + str(dist_name)), ], style={ 'display': 'inline-block', @@ -323,18 +323,18 @@ def prepare_run_detailes(history): ), html.Div( [ - html.H5("Population strategy:"), - html.Label("Name: " + str(pop_str_name)), + html.H5('Population strategy:'), + html.Label('Name: ' + str(pop_str_name)), html.Label( - "Number of calibration particles: " + 'Number of calibration particles: ' + str(pop_str_calib) ), html.Label( - "Number of samples per parameter: " + 'Number of samples per parameter: ' + str(pop_str_sample_per_par) ), html.Label( - "Number of particles: " + 'Number of particles: ' + str(pop_str_nr_particles) ), ], @@ -353,16 +353,16 @@ def prepare_run_detailes(history): ), html.Div( [ - html.H5("Epsilon function:"), - html.Label("Name: " + str(eps_name)), + html.H5('Epsilon function:'), + html.Label('Name: ' + str(eps_name)), html.Label( - "Initial epsilon: " + str(eps_init_spe) + 'Initial epsilon: ' + str(eps_init_spe) ), - html.Label("Alpha: " + str(eps_alpha)), + html.Label('Alpha: ' + str(eps_alpha)), html.Label( - "Quantile multiplier: " + str(eps_quant) + 'Quantile multiplier: ' + str(eps_quant) ), - html.Label("Weighted: " + str(eps_weited)), + html.Label('Weighted: ' + str(eps_weited)), ], style={ 'display': 'inline-block', @@ -382,48 +382,48 @@ def prepare_run_detailes(history): html.Div( children=[ dcc.Tabs( - id="tabs", + id='tabs', value='tab-1', parent_className='custom-tabs', className='custom-tabs-container', children=[ dcc.Tab( - id="tab-1", + id='tab-1', label='Probability density functions', value='tab-pdf', className='custom-tab', selected_className='custom-tab--selected', ), dcc.Tab( - id="tab-2", + id='tab-2', label='Samples', value='tab-samples', className='custom-tab', selected_className='custom-tab--selected', ), dcc.Tab( - id="tab-3", + id='tab-3', label='Particles', value='tab-particles', className='custom-tab', selected_className='custom-tab--selected', ), dcc.Tab( - id="tab-4", + id='tab-4', label='Epsilons', value='tab-epsilons', className='custom-tab', selected_className='custom-tab--selected', ), dcc.Tab( - id="tab-5", + id='tab-5', label='Credible intervals', value='tab-credible', className='custom-tab', selected_className='custom-tab--selected', ), dcc.Tab( - id="tab-6", + id='tab-6', label='Effective sample sizes', value='tab-effective', className='custom-tab', @@ -449,15 +449,15 @@ def prepare_run_detailes(history): @app.callback( - dash.dependencies.Output("information_grid", "children"), + dash.dependencies.Output('information_grid', 'children'), [dash.dependencies.Input('ABC_runs', 'value')], ) def display_info(smc_id): global db_path try: - history = h.History("sqlite:///" + db_path, _id=smc_id) + history = h.History('sqlite:///' + db_path, _id=smc_id) except Exception: - return " " + return ' ' global para_list dist_df = history.get_distribution() para_list.clear() @@ -467,7 +467,7 @@ def display_info(smc_id): @app.callback( - dash.dependencies.Output("hidden-div", "children"), + dash.dependencies.Output('hidden-div', 'children'), [dash.dependencies.Input('download_path', 'value')], ) def update_download_path(new_download_path): @@ -488,7 +488,7 @@ def update_download_path(new_download_path): Output('output-data-upload', 'children'), Output('ABC_runs', 'options'), Output('ABC_runs', 'value'), - dash.dependencies.Output("alert_div", "children"), + dash.dependencies.Output('alert_div', 'children'), ], [ Input('upload_file', 'n_clicks'), @@ -502,44 +502,44 @@ def update_DB_details(btn_click, db_path_new): # save_file(file_name, list_of_contents[0]) global db_path db_path = db_path_new - history = h.History("sqlite:///" + db_path_new) + history = h.History('sqlite:///' + db_path_new) except Exception as e: - if e.__class__.__name__ == "TypeError": + if e.__class__.__name__ == 'TypeError': return ( - "", + '', [], - "", + '', dbc.Alert( [ html.H4( - "There was an error while loading the " - "database!", - className="alert-heading", + 'There was an error while loading the ' + 'database!', + className='alert-heading', ), ], - id="user_update_alert", + id='user_update_alert', is_open=True, fade=True, - color="danger", + color='danger', dismissable=True, ), ) else: return ( - "", + '', [], - "", + '', dbc.Alert( [ - html.H4("Error!", className="alert-heading"), + html.H4('Error!', className='alert-heading'), html.P( - f"Please upload a database. " f"{str(e)}.", + f'Please upload a database. ' f'{str(e)}.', ), ], - id="user_update_alert", + id='user_update_alert', is_open=True, fade=True, - color="danger", + color='danger', dismissable=True, ), ) @@ -553,33 +553,33 @@ def update_DB_details(btn_click, db_path_new): list_run_ids[-1], dbc.Alert( [ - html.H4("Great!", className="alert-heading"), + html.H4('Great!', className='alert-heading'), html.P( - "The database was loaded successfully.", + 'The database was loaded successfully.', ), ], - id="user_update_alert", + id='user_update_alert', is_open=True, - color="success", + color='success', dismissable=True, ), ) else: return ( - "", + '', [], - "", + '', dbc.Alert( [ - html.H4("Tip!", className="alert-heading"), + html.H4('Tip!', className='alert-heading'), html.P( - "Please upload a database!.", + 'Please upload a database!.', ), ], - id="user_update_alert", + id='user_update_alert', is_open=True, fade=True, - color="info", + color='info', duration=5000, ), ) @@ -587,7 +587,7 @@ def update_DB_details(btn_click, db_path_new): def prepare_fig_tab(smc_id): global db_path - history = h.History("sqlite:///" + db_path, _id=smc_id) + history = h.History('sqlite:///' + db_path, _id=smc_id) dist_df = history.get_distribution() para_list = [] for col in dist_df[0].columns: @@ -598,7 +598,7 @@ def prepare_fig_tab(smc_id): [ html.Label('Select parameter: '), dcc.Dropdown( - id="parameters", + id='parameters', options=[ {'label': name, 'value': name} for name in para_list @@ -612,7 +612,7 @@ def prepare_fig_tab(smc_id): [ html.Label('Select parameter: '), dcc.Dropdown( - id="figure_type", + id='figure_type', options=[ {'label': 'pdf_1d', 'value': 'pdf_1d'}, {'label': 'pdf_2d', 'value': 'pdf_2d'}, @@ -641,7 +641,7 @@ def prepare_fig_tab(smc_id): # html.Br(), html.Img( id='abc_run_plot', - src='data:image/png;base64,{}'.format(square_base64), + src=f'data:image/png;base64,{square_base64}', ), # img element, ] ), @@ -659,14 +659,14 @@ def prepare_fig_tab(smc_id): dash.dependencies.Output('tabs-content', 'children'), # src attribute [ dash.dependencies.Input('ABC_runs', 'value'), - dash.dependencies.Input("tabs", "value"), + dash.dependencies.Input('tabs', 'value'), ], ) def update_figure_ABC_run(smc_id, f_type): # create some matplotlib graph - history = h.History("sqlite:///" + db_path, _id=smc_id) + history = h.History('sqlite:///' + db_path, _id=smc_id) global para_list - if f_type == "tab-pdf": + if f_type == 'tab-pdf': parameters = para_list.copy() fig, ax = plt.subplots() for t in range(history.max_t + 1): @@ -681,19 +681,19 @@ def update_figure_ABC_run(smc_id, f_type): xmax=xmax, x=parameters[0], ax=ax, - label="PDF t={}".format(t), + label=f'PDF t={t}', ) ax.legend() buf = io.BytesIO() # in-memory files - plt.savefig(buf, format="png") # save to the above file object + plt.savefig(buf, format='png') # save to the above file object data = base64.b64encode(buf.getbuffer()).decode( - "utf8" + 'utf8' ) # encode to html elements return [ html.Label('Select parameter: '), dcc.Dropdown( - id="parameters", + id='parameters', options=[{'label': name, 'value': name} for name in para_list], multi=True, value=[para_list[0]], @@ -703,7 +703,7 @@ def update_figure_ABC_run(smc_id, f_type): # "ABC run plots: ", html.Img( id='abc_run_plot', - src='data:image/png;base64,{}'.format(square_base64), + src=f'data:image/png;base64,{square_base64}', ), # img element, ], style={'textAlign': 'center'}, @@ -711,36 +711,36 @@ def update_figure_ABC_run(smc_id, f_type): html.Div( dbc.Card( [ - dbc.CardHeader("Plot configration"), + dbc.CardHeader('Plot configration'), dbc.CardBody( [ - "x limits: ", + 'x limits: ', html.Br(), html.Br(), html.Div( children=[ - "min: ", + 'min: ', dcc.Input( - id="bar_min", + id='bar_min', placeholder='min', type='number', value='0', ), - "max: ", + 'max: ', dcc.Input( - id="bar_max", + id='bar_max', placeholder='max', type='number', value='20', ), - " Copy code to clipboard: ", + ' Copy code to clipboard: ', # html.Button('Generate', id='copy', n_clicks=0), dcc.Clipboard( - id="code_copy", + id='code_copy', style={ - "fontSize": 25, - "display": "inline-block", - "verticalAlign": "middle", + 'fontSize': 25, + 'display': 'inline-block', + 'verticalAlign': 'middle', }, ), html.Hr(), @@ -751,8 +751,8 @@ def update_figure_ABC_run(smc_id, f_type): marks=None, value=[xmin, xmax], tooltip={ - "placement": "bottom", - "always_visible": True, + 'placement': 'bottom', + 'always_visible': True, }, ), ] @@ -766,8 +766,8 @@ def update_figure_ABC_run(smc_id, f_type): html.Div( [ dcc.Dropdown( - options=["linear", "log", "symlog", "logit"], - id="y_scale", + options=['linear', 'log', 'symlog', 'logit'], + id='y_scale', style={ 'display': 'none', }, @@ -779,12 +779,12 @@ def update_figure_ABC_run(smc_id, f_type): marks=None, value=[0.50, 0.95], tooltip={ - "placement": "bottom", - "always_visible": True, + 'placement': 'bottom', + 'always_visible': True, }, ), dbc.Checklist( - id="switches-mean", + id='switches-mean', options=['Show mean'], switch=True, ), @@ -794,26 +794,26 @@ def update_figure_ABC_run(smc_id, f_type): }, ), ] - elif f_type == "tab-samples": + elif f_type == 'tab-samples': pyabc.visualization.plot_sample_numbers(history) - elif f_type == "tab-particles": + elif f_type == 'tab-particles': _, ax2 = plt.subplots() particles = ( history.get_nr_particles_per_population() .reset_index() - .rename(columns={"count": "particles"}) - .query("t >= 0") + .rename(columns={'count': 'particles'}) + .query('t >= 0') ) ax2.set_xlabel('population index') ax2.set_ylabel('Particles') - ax2.plot(particles["t"], particles["particles"], "x-") + ax2.plot(particles['t'], particles['particles'], 'x-') - elif f_type == "tab-epsilons": + elif f_type == 'tab-epsilons': pyabc.visualization.plot_epsilons(history) buf = io.BytesIO() # in-memory files - plt.savefig(buf, format="png") # save to the above file object + plt.savefig(buf, format='png') # save to the above file object data = base64.b64encode(buf.getbuffer()).decode( - "utf8" + 'utf8' ) # encode to html elements return [ @@ -821,7 +821,7 @@ def update_figure_ABC_run(smc_id, f_type): [ html.Img( id='abc_run_plot', - src="data:image/png;base64,{}".format(data), + src=f'data:image/png;base64,{data}', ), # img element, ], @@ -829,28 +829,28 @@ def update_figure_ABC_run(smc_id, f_type): ), dbc.Card( [ - dbc.CardHeader("Plot configration"), + dbc.CardHeader('Plot configration'), dbc.CardBody( [ - "Y scale: ", + 'Y scale: ', dcc.Dropdown( - options=["linear", "log", "symlog", "logit"], - id="y_scale", - value="log", + options=['linear', 'log', 'symlog', 'logit'], + id='y_scale', + value='log', style={ - "display": "inline-block", - "verticalAlign": "middle", - "width": "100px", + 'display': 'inline-block', + 'verticalAlign': 'middle', + 'width': '100px', }, ), - " Copy code to clipboard: ", + ' Copy code to clipboard: ', # html.Button('Generate', id='copy', n_clicks=0), dcc.Clipboard( - id="code_copy", + id='code_copy', style={ - "fontSize": 25, - "display": "inline-block", - "verticalAlign": "middle", + 'fontSize': 25, + 'display': 'inline-block', + 'verticalAlign': 'middle', }, ), ] @@ -859,16 +859,16 @@ def update_figure_ABC_run(smc_id, f_type): ), html.Div( children=[ - "min: ", + 'min: ', dcc.Input( - id="bar_min", + id='bar_min', placeholder='min', type='number', value='0', ), - "max: ", + 'max: ', dcc.Input( - id="bar_max", + id='bar_max', placeholder='max', type='number', value='20', @@ -887,7 +887,7 @@ def update_figure_ABC_run(smc_id, f_type): html.Div( [ dcc.Dropdown( - id="parameters", + id='parameters', options=[ {'label': name, 'value': name} for name in para_list @@ -902,12 +902,12 @@ def update_figure_ABC_run(smc_id, f_type): marks=None, value=[0.50, 0.95], tooltip={ - "placement": "bottom", - "always_visible": True, + 'placement': 'bottom', + 'always_visible': True, }, ), dbc.Checklist( - id="switches-mean", + id='switches-mean', options=['Show mean'], switch=True, ), @@ -917,7 +917,7 @@ def update_figure_ABC_run(smc_id, f_type): }, ), ] - elif f_type == "tab-credible": + elif f_type == 'tab-credible': # buf = io.BytesIO() # in-memory files # # plt.savefig(buf, format="png") # save to the above file object @@ -934,7 +934,7 @@ def update_figure_ABC_run(smc_id, f_type): return [ html.Label('Select parameter: '), dcc.Dropdown( - id="parameters", + id='parameters', options=[{'label': name, 'value': name} for name in para_list], style={'color': 'red'}, multi=True, @@ -947,17 +947,17 @@ def update_figure_ABC_run(smc_id, f_type): # html.Br(), html.Img( id='abc_run_plot', - src='data:image/png;base64,{}'.format(square_base64), + src=f'data:image/png;base64,{square_base64}', ), ], style={'textAlign': 'center'}, ), dbc.Card( [ - dbc.CardHeader("Plot configration"), + dbc.CardHeader('Plot configration'), dbc.CardBody( [ - "confidence levels: ", + 'confidence levels: ', html.Br(), dcc.RangeSlider( id='levels_slider', @@ -966,33 +966,33 @@ def update_figure_ABC_run(smc_id, f_type): value=[0.50, 0.95], ), dbc.Checklist( - id="switches-mean", + id='switches-mean', options=['Show mean', 'Show KDE max'], value=['Show mean'], switch=True, ), - " Copy code to clipboard: ", + ' Copy code to clipboard: ', # html.Button('Generate', id='copy', n_clicks=0), dcc.Clipboard( - id="code_copy", + id='code_copy', style={ - "fontSize": 25, - "display": "inline-block", - "verticalAlign": "middle", + 'fontSize': 25, + 'display': 'inline-block', + 'verticalAlign': 'middle', }, ), html.Div( children=[ - "min: ", + 'min: ', dcc.Input( - id="bar_min", + id='bar_min', placeholder='min', type='number', value='0', ), - "max: ", + 'max: ', dcc.Input( - id="bar_max", + id='bar_max', placeholder='max', type='number', value='20', @@ -1009,11 +1009,11 @@ def update_figure_ABC_run(smc_id, f_type): style={'display': 'none'}, ), dcc.Dropdown( - options=["linear", "log", "symlog", "logit"], - id="y_scale", + options=['linear', 'log', 'symlog', 'logit'], + id='y_scale', style={ - "verticalAlign": "middle", - "width": "100px", + 'verticalAlign': 'middle', + 'width': '100px', 'display': 'none', }, ), @@ -1022,12 +1022,12 @@ def update_figure_ABC_run(smc_id, f_type): ] ), ] - elif f_type == "tab-effective": + elif f_type == 'tab-effective': pyabc.visualization.plot_effective_sample_sizes(history) buf = io.BytesIO() # in-memory files - plt.savefig(buf, format="png") # save to the above file object + plt.savefig(buf, format='png') # save to the above file object data = base64.b64encode(buf.getbuffer()).decode( - "utf8" + 'utf8' ) # encode to html elements # plt.close() return [ @@ -1039,7 +1039,7 @@ def update_figure_ABC_run(smc_id, f_type): [ html.Img( id='abc_run_plot', - src="data:image/png;base64,{}".format(data), + src=f'data:image/png;base64,{data}', ), # img element, ], @@ -1047,17 +1047,17 @@ def update_figure_ABC_run(smc_id, f_type): ), dbc.Card( [ - dbc.CardHeader("Plot configration"), + dbc.CardHeader('Plot configration'), dbc.CardBody( [ - " Copy code to clipboard: ", + ' Copy code to clipboard: ', # html.Button('Generate', id='copy', n_clicks=0), dcc.Clipboard( - id="code_copy", + id='code_copy', style={ - "fontSize": 25, - "display": "inline-block", - "verticalAlign": "middle", + 'fontSize': 25, + 'display': 'inline-block', + 'verticalAlign': 'middle', }, ), ] @@ -1076,7 +1076,7 @@ def update_figure_ABC_run(smc_id, f_type): [ dash.dependencies.Input('ABC_runs', 'value'), dash.dependencies.Input('parameters', 'value'), - dash.dependencies.Input("tabs", "value"), + dash.dependencies.Input('tabs', 'value'), dash.dependencies.Input('my-range-slider', 'value'), Input('y_scale', 'value'), dash.dependencies.Input('levels_slider', 'value'), @@ -1087,13 +1087,13 @@ def update_figure_ABC_run_parameters( smc_id, parameters, f_type, bar_val, scale_val, levels_val, mean_val ): # create some matplotlib graph - history = h.History("sqlite:///" + db_path, _id=smc_id) + history = h.History('sqlite:///' + db_path, _id=smc_id) xmin = 0 xmax = 0 mean_flag = True kde_flag = True global para_list - if f_type == "tab-pdf": + if f_type == 'tab-pdf': fig, ax = plt.subplots() if len(parameters) == 1: for t in range(history.max_t + 1): @@ -1112,7 +1112,7 @@ def update_figure_ABC_run_parameters( xmax=xmax, x=parameters[0], ax=ax, - label="PDF t={}".format(t), + label=f'PDF t={t}', ) ax.legend() elif len(parameters) == 2: @@ -1140,7 +1140,7 @@ def update_figure_ABC_run_parameters( xmin = bar_val[0] xmax = bar_val[1] - elif f_type == "tab-credible": + elif f_type == 'tab-credible': df, w = history.get_distribution() if bar_val == [0, 0]: xmin = df[parameters[0]].min() @@ -1155,9 +1155,9 @@ def update_figure_ABC_run_parameters( mean_flag = False kde_flag = False - elif "Show mean" not in mean_val: + elif 'Show mean' not in mean_val: mean_flag = False - elif "Show KDE max" not in mean_val: + elif 'Show KDE max' not in mean_val: kde_flag = False pyabc.visualization.plot_credible_intervals( history, @@ -1166,21 +1166,21 @@ def update_figure_ABC_run_parameters( show_kde_max_1d=kde_flag, par_names=parameters, ) - elif f_type == "tab-epsilons": + elif f_type == 'tab-epsilons': if scale_val is None: - scale_val = "log" + scale_val = 'log' pyabc.visualization.plot_epsilons(history, yscale=scale_val) buf = io.BytesIO() # in-memory files plt.gcf().set_size_inches(7, 5) plt.tight_layout() - plt.savefig(buf, format="png") # save to the above file object + plt.savefig(buf, format='png') # save to the above file object data = base64.b64encode(buf.getbuffer()).decode( - "utf8" + 'utf8' ) # encode to html elements # plt.close() return ( - "data:image/png;base64,{}".format(data), + f'data:image/png;base64,{data}', math.floor(xmin), math.ceil(xmax), ) @@ -1206,12 +1206,12 @@ def update_figure_ABC_run_parameters( @app.callback( [ - dash.dependencies.Output("my-range-slider", "min"), - dash.dependencies.Output("my-range-slider", "max"), + dash.dependencies.Output('my-range-slider', 'min'), + dash.dependencies.Output('my-range-slider', 'max'), ], [ - dash.dependencies.Input("bar_min", "value"), - dash.dependencies.Input("bar_max", "value"), + dash.dependencies.Input('bar_min', 'value'), + dash.dependencies.Input('bar_max', 'value'), ], ) def number_render(min, max): @@ -1220,10 +1220,10 @@ def number_render(min, max): def save_file(name, content): """Decode and store a file uploaded with Dash.""" - data = content.encode("utf8").split(b";base64,")[1] + data = content.encode('utf8').split(b';base64,')[1] if not os.path.exists(DOWNLOAD_DIR): os.makedirs(DOWNLOAD_DIR) - with open(os.path.join(DOWNLOAD_DIR, name), "wb") as fp: + with open(os.path.join(DOWNLOAD_DIR, name), 'wb') as fp: fp.write(base64.decodebytes(data)) @@ -1241,15 +1241,15 @@ def save_file(name, content): @app.callback( - Output("code_copy", "content"), - Input("code_copy", "n_clicks"), - Input("tabs", "value"), + Output('code_copy', 'content'), + Input('code_copy', 'n_clicks'), + Input('tabs', 'value'), ) def displayClick(btn_click, tab_type): if btn_click is None: - return "" + return '' if btn_click > 0: - code_pt2 = "" + code_pt2 = '' if tab_type == 'tab-pdf': code_pt2 = dedent( """ @@ -1325,22 +1325,22 @@ def displayClick(btn_click, tab_type): @click.command() @click.option( - "--debug", + '--debug', default=False, type=bool, - help="Whether to run the server in debug mode", + help='Whether to run the server in debug mode', ) @click.option( - "--port", + '--port', default=8050, type=int, - help="The port on which the server runs (default: 8050)", + help='The port on which the server runs (default: 8050)', ) @click.option( - "--host", - default="localhost", + '--host', + default='localhost', type=str, - help="Host name (default: 127.0.0.1 / localhost)", + help='Host name (default: 127.0.0.1 / localhost)', ) def run_app(host, port, debug): app.run(host=host, port=port, debug=debug) diff --git a/pyabc/visserver/server_flask.py b/pyabc/visserver/server_flask.py index 44cc4aab6..7b8967f75 100644 --- a/pyabc/visserver/server_flask.py +++ b/pyabc/visserver/server_flask.py @@ -15,12 +15,12 @@ from pyabc import History -logger = logging.getLogger("ABC.VisServer") +logger = logging.getLogger('ABC.VisServer') # enable ctrl+c handling (python+R fight for it otherwise) -def signal_handler(sig, frame): - logger.info("Handling SIGINT with an exit.") +def signal_handler(sig, frame): # noqa: ARG001 + logger.info('Handling SIGINT with an exit.') sys.exit(0) @@ -53,14 +53,14 @@ def __init__(self, script, div): @app.route('/') def main(): - return render_template("index.html") + return render_template('index.html') -@app.route("/abc") +@app.route('/abc') def abc_overview(): - history = app.config["HISTORY"] + history = app.config['HISTORY'] runs = history.all_runs() - return render_template("abc_overview.html", runs=runs) + return render_template('abc_overview.html', runs=runs) class ABCInfo: @@ -75,9 +75,9 @@ def __getattr__(self, item): return {} -@app.route("/abc/") +@app.route('/abc/') def abc_detail(abc_id): - history = app.config["HISTORY"] + history = app.config['HISTORY'] history.id = abc_id abc = ABCInfo(history.get_abc()) @@ -85,7 +85,7 @@ def abc_detail(abc_id): model_probabilities = history.get_model_probabilities() model_ids = model_probabilities.columns model_probabilities.columns = list( - map("{}".format, model_probabilities.columns) + map('{}'.format, model_probabilities.columns) ) model_probabilities = model_probabilities.reset_index() @@ -95,14 +95,14 @@ def abc_detail(abc_id): particles = ( history.get_nr_particles_per_population() .reset_index() - .query("t >= 0") - .rename(columns={"count": "particles"}) + .query('t >= 0') + .rename(columns={'count': 'particles'}) ) melted = pd.melt( - model_probabilities, id_vars="t", var_name="m", value_name="p" + model_probabilities, id_vars='t', var_name='m', value_name='p' ) - melted["m"] = pd.to_numeric(melted["m"]) + melted['m'] = pd.to_numeric(melted['m']) # although it might seem cumbersome, not using the bkcharts # package works more reliably @@ -110,11 +110,11 @@ def abc_detail(abc_id): prob_plot = figure() prob_plot.xaxis.axis_label = 'Generation t' prob_plot.yaxis.axis_label = 'Probability' - for c, (m, data) in zip(DEFAULT_PALETTE, melted.groupby("m")): + for c, (m, data) in zip(DEFAULT_PALETTE, melted.groupby('m')): prob_plot.line( - data["t"], - data["p"], - legend_label="Model " + str(m), + data['t'], + data['p'], + legend_label='Model ' + str(m), color=c, line_width=2, ) @@ -123,33 +123,33 @@ def abc_detail(abc_id): particles_fig.xaxis.axis_label = 'Generation t' particles_fig.yaxis.axis_label = 'Particles' particles_fig.line( - particles["t"], particles["particles"], line_width=2 + particles['t'], particles['particles'], line_width=2 ) samples_fig = figure() samples_fig.xaxis.axis_label = 'Generation t' samples_fig.yaxis.axis_label = 'Samples' samples_fig.line( - populations["t"], populations["samples"], line_width=2 + populations['t'], populations['samples'], line_width=2 ) eps_fig = figure() eps_fig.xaxis.axis_label = 'Generation t' eps_fig.yaxis.axis_label = 'Epsilon' - eps_fig.line(populations["t"], populations["epsilon"], line_width=2) + eps_fig.line(populations['t'], populations['epsilon'], line_width=2) plot = Tabs( tabs=[ - TabPanel(child=prob_plot, title="Probability"), - TabPanel(child=samples_fig, title="Samples"), - TabPanel(child=particles_fig, title="Particles"), - TabPanel(child=eps_fig, title="Epsilon"), + TabPanel(child=prob_plot, title='Probability'), + TabPanel(child=samples_fig, title='Samples'), + TabPanel(child=particles_fig, title='Particles'), + TabPanel(child=eps_fig, title='Epsilon'), ] ) plot = PlotScriptDiv(*components(plot)) return render_template( - "abc_detail.html", + 'abc_detail.html', abc_id=abc_id, plot=plot, BOKEH=BOKEH, @@ -157,41 +157,38 @@ def abc_detail(abc_id): abc=abc, ) return render_template( - "abc_detail.html", + 'abc_detail.html', abc_id=abc_id, - plot=PlotScriptDiv("", "Exception: No data found."), + plot=PlotScriptDiv('', 'Exception: No data found.'), BOKEH=BOKEH, abc=abc, ) -@app.route("/abc//model//t/") +@app.route('/abc//model//t/') def abc_model(abc_id, model_id, t): - history = app.config["HISTORY"] + history = app.config['HISTORY'] history.id = abc_id - if t == "max": - t = history.max_t - else: - t = int(t) + t = history.max_t if t == 'max' else int(t) df, w = history.get_distribution(model_id, t) - df["CDF"] = w + df['CDF'] = w tabs = [] model_ids = history.get_model_probabilities().columns - for parameter in [col for col in df if col != "CDF"]: - plot_df = df[["CDF", parameter]].sort_values(parameter) + for parameter in [col for col in df if col != 'CDF']: + plot_df = df[['CDF', parameter]].sort_values(parameter) plot_df_cumsum = plot_df.cumsum() plot_df_cumsum[parameter] = plot_df[parameter] f = figure() - f.line(x=plot_df_cumsum[parameter], y=plot_df_cumsum["CDF"]) + f.line(x=plot_df_cumsum[parameter], y=plot_df_cumsum['CDF']) p = TabPanel(child=f, title=parameter) tabs.append(p) if len(tabs) == 0: - plot = PlotScriptDiv("", "This model has no Parameters") + plot = PlotScriptDiv('', 'This model has no Parameters') else: plot = PlotScriptDiv(*components(Tabs(tabs=tabs))) return render_template( - "model.html", + 'model.html', abc_id=abc_id, model_id=model_id, plot=plot, @@ -202,34 +199,34 @@ def abc_model(abc_id, model_id, t): ) -@app.route("/info") +@app.route('/info') def server_info(): - history = app.config["HISTORY"] + history = app.config['HISTORY'] return render_template( - "server_info.html", + 'server_info.html', db_path=history.db_file(), db_size=round(history.db_size, 2), ) @app.errorhandler(404) -def page_not_found(e): +def page_not_found(e): # noqa: ARG001 return render_template('404.html'), 404 @click.command() @click.option( - "--debug", + '--debug', default=False, type=bool, - help="Whether to run the server in debug mode", + help='Whether to run the server in debug mode', ) @click.option( - "--port", default=8050, type=int, help="The port on which the server runs" + '--port', default=8050, type=int, help='The port on which the server runs' ) -@click.argument("db") +@click.argument('db') def run_app(db, debug, port): db = os.path.expanduser(db) - history = History("sqlite:///" + db) - app.config["HISTORY"] = history + history = History('sqlite:///' + db) + app.config['HISTORY'] = history app.run(port=port, debug=debug) diff --git a/pyabc/visualization/credible.py b/pyabc/visualization/credible.py index 2d41a92b8..1e76b851c 100644 --- a/pyabc/visualization/credible.py +++ b/pyabc/visualization/credible.py @@ -1,7 +1,5 @@ """Bayesian credible interval plots""" -from typing import List, Union - import matplotlib.axes import matplotlib.pyplot as plt import numpy as np @@ -16,9 +14,9 @@ def _prepare_credible_intervals( history: History, m: int, - ts: Union[List[int], int], - par_names: List, - levels: List, + ts: list[int] | int, + par_names: list, + levels: list, show_mean: bool, show_kde_max: bool, show_kde_max_1d: bool, @@ -100,10 +98,10 @@ def _prepare_credible_intervals( def plot_credible_intervals( history: History, m: int = 0, - ts: Union[List[int], int] = None, - par_names: List = None, - levels: List = None, - colors: List = None, + ts: list[int] | int = None, + par_names: list = None, + levels: list = None, + colors: list = None, color_median: str = None, show_mean: bool = False, color_mean: str = None, @@ -116,7 +114,7 @@ def plot_credible_intervals( refval_color: str = 'C1', kde: Transition = None, kde_1d: Transition = None, - arr_ax: List[matplotlib.axes.Axes] = None, + arr_ax: list[matplotlib.axes.Axes] = None, ): """Plot credible intervals over time. @@ -208,7 +206,7 @@ def plot_credible_intervals( _, arr_ax = plt.subplots( nrows=n_par, ncols=1, sharex=False, sharey=False, figsize=size ) - if not isinstance(arr_ax, (list, np.ndarray)): + if not isinstance(arr_ax, list | np.ndarray): arr_ax = [arr_ax] fig = arr_ax[0].get_figure() @@ -225,13 +223,13 @@ def plot_credible_intervals( color=color_median, ecolor=colors[i_c], capsize=(5.0 / n_confidence) * (i_c + 1), - label="{:.2f}".format(confidence), + label=f'{confidence:.2f}', ) - ax.set_title(f"Parameter {par}") + ax.set_title(f'Parameter {par}') # mean if show_mean: ax.plot( - range(n_pop), mean[i_par], 'x-', label="Mean", color=color_mean + range(n_pop), mean[i_par], 'x-', label='Mean', color=color_mean ) # kde max if show_kde_max: @@ -239,7 +237,7 @@ def plot_credible_intervals( range(n_pop), kde_max[i_par], 'x-', - label="Max KDE", + label='Max KDE', color=color_kde_max, ) if show_kde_max_1d: @@ -247,7 +245,7 @@ def plot_credible_intervals( range(n_pop), kde_max_1d[i_par], 'x-', - label="Max KDE 1d", + label='Max KDE 1d', color=color_kde_max_1d, ) # reference value @@ -257,7 +255,7 @@ def plot_credible_intervals( xmin=0, xmax=n_pop - 1, color=refval_color, - label="Reference value", + label='Reference value', ) ax.set_xticks(range(n_pop)) ax.set_xticklabels(ts) @@ -265,7 +263,7 @@ def plot_credible_intervals( ax.legend() # format - arr_ax[-1].set_xlabel("Population t") + arr_ax[-1].set_xlabel('Population t') if size is not None: fig.set_size_inches(size) fig.tight_layout() @@ -276,9 +274,9 @@ def plot_credible_intervals( def plot_credible_intervals_plotly( history: History, m: int = 0, - ts: Union[List[int], int] = None, - par_names: List = None, - levels: List = None, + ts: list[int] | int = None, + par_names: list = None, + levels: list = None, colors=None, size: tuple = None, refval: dict = None, @@ -345,7 +343,7 @@ def plot_credible_intervals_plotly( mode='lines+markers', marker={'color': colors[i_c]}, opacity=opacities[i_c], - name="{:.2f}".format(confidence), + name=f'{confidence:.2f}', showlegend=showlegend, ), row=i_par + 1, @@ -359,7 +357,7 @@ def plot_credible_intervals_plotly( y=[refval[par]] * n_pop, mode='lines', marker={'color': refval_color}, - name="Reference value", + name='Reference value', showlegend=showlegend, ), row=i_par + 1, @@ -376,18 +374,18 @@ def plot_credible_intervals_plotly( def plot_credible_intervals_for_time( - histories: Union[List[History], History], - labels: Union[List[str], str] = None, - ms: Union[List[int], int] = None, - ts: Union[List[int], int] = None, - par_names: List[str] = None, - levels: List[float] = None, + histories: list[History] | History, + labels: list[str] | str = None, + ms: list[int] | int = None, + ts: list[int] | int = None, + par_names: list[str] = None, + levels: list[float] = None, show_mean: bool = False, show_kde_max: bool = False, show_kde_max_1d: bool = False, size: tuple = None, rotation: int = 0, - refvals: Union[List[dict], dict] = None, + refvals: list[dict] | dict = None, kde: Transition = None, kde_1d: Transition = None, ): @@ -516,7 +514,7 @@ def plot_credible_intervals_for_time( # reference value if refvals[i_run] is not None: ax.plot([i_run], [refvals[i_run][par]], 'x', color='black') - ax.set_title(f"Parameter {par}") + ax.set_title(f'Parameter {par}') # mean if show_mean: ax.plot(range(n_run), mean[i_par], 'x', color=f'C{n_confidence}') @@ -535,27 +533,27 @@ def plot_credible_intervals_for_time( ax.set_xticks(range(n_run)) ax.set_xticklabels(labels, rotation=rotation) leg_colors = [f'C{i_c}' for i_c in reversed(range(n_confidence))] - leg_labels = ['{:.2f}'.format(c) for c in reversed(levels)] + leg_labels = [f'{c:.2f}' for c in reversed(levels)] if show_mean: leg_colors.append(f'C{n_confidence}') - leg_labels.append("Mean") + leg_labels.append('Mean') if show_kde_max: leg_colors.append(f'C{n_confidence + 1}') - leg_labels.append("Max KDE") + leg_labels.append('Max KDE') if show_kde_max_1d: leg_colors.append(f'C{n_confidence + 2}') - leg_labels.append("Max KDE 1d") + leg_labels.append('Max KDE 1d') if refvals is not None: leg_colors.append('black') - leg_labels.append("Reference value") + leg_labels.append('Reference value') handles = [ - Line2D([0], [0], color=c, label=l) - for c, l in zip(leg_colors, leg_labels) + Line2D([0], [0], color=c, label=label) + for c, label in zip(leg_colors, leg_labels) ] - ax.legend(handles=handles, bbox_to_anchor=(1.04, 1), loc="upper left") + ax.legend(handles=handles, bbox_to_anchor=(1.04, 1), loc='upper left') # format - arr_ax[-1].set_xlabel("Population t") + arr_ax[-1].set_xlabel('Population t') if size is not None: fig.set_size_inches(size) fig.tight_layout() @@ -575,7 +573,7 @@ def compute_credible_interval(vals, weights, confidence: float = 0.95): """ if confidence <= 0.0 or confidence >= 1.0: raise ValueError( - f"Confidence {confidence} must be in the interval (0.0, 1.0)." + f'Confidence {confidence} must be in the interval (0.0, 1.0).' ) alpha_lb = 0.5 * (1.0 - confidence) alpha_ub = confidence + alpha_lb diff --git a/pyabc/visualization/data.py b/pyabc/visualization/data.py index 6603d7b8d..bb20fca57 100644 --- a/pyabc/visualization/data.py +++ b/pyabc/visualization/data.py @@ -1,7 +1,7 @@ """Data and summary statistics plots""" import logging -from typing import Callable, List, Union +from collections.abc import Callable import matplotlib.axes import matplotlib.pyplot as plt @@ -10,7 +10,7 @@ from ..storage import History -logger = logging.getLogger("ABC.Visualization") +logger = logging.getLogger('ABC.Visualization') def plot_data_callback( @@ -63,8 +63,8 @@ def plot_data_callback( def plot_data_callback_lowlevel( - sum_stats: List, - weights: List, + sum_stats: list, + weights: list, f_plot: Callable, f_plot_aggregated: Callable = None, n_sample: int = None, @@ -100,7 +100,7 @@ def plot_data_callback_lowlevel( def plot_data_default( - obs_data: dict, sim_data: dict, keys: Union[List[str], str] = None + obs_data: dict, sim_data: dict, keys: list[str] | str = None ): """ Plot summary statistic data. @@ -149,43 +149,40 @@ def plot_data_default( for plot_index, ((obs_key, obs), (_, sim)) in enumerate( zip(obs_data.items(), sim_data.items()) ): - if nrows == ncols == 1: - ax = arr_ax - else: - ax = arr_ax.flatten()[plot_index] + ax = arr_ax if nrows == ncols == 1 else arr_ax.flatten()[plot_index] # data frame if isinstance(obs, pd.DataFrame): if len(obs.columns) == 1: # 1d: plot - ax.plot(sim.values.flatten(), '-x', label="Simulation") - ax.plot(obs.values.flatten(), '-x', label="Data") - ax.set_xlabel("Index") + ax.plot(sim.values.flatten(), '-x', label='Simulation') + ax.plot(obs.values.flatten(), '-x', label='Data') + ax.set_xlabel('Index') ax.set_ylabel(obs.columns[0]) else: # nd: scatter for key in obs.columns: ax.scatter(obs[key].values, sim[key].values, label=key) - ax.set_xlabel("Data") - ax.set_ylabel("Simulation") + ax.set_xlabel('Data') + ax.set_ylabel('Simulation') elif isinstance(obs, np.ndarray) and obs.ndim == 1: # 1d: plot obs_value = obs sim_value = sim - ax.plot(sim_value, '-x', color="C0", label='Simulation') - ax.plot(obs_value, '-x', color="C1", label='Data') - ax.set_xlabel("Index") + ax.plot(sim_value, '-x', color='C0', label='Simulation') + ax.plot(obs_value, '-x', color='C1', label='Data') + ax.set_xlabel('Index') ax.set_ylabel(str(obs_key)) elif isinstance(obs, np.ndarray): # nd: scatter for j, (obs_val, sim_val) in enumerate(zip(obs, sim)): - ax.scatter(obs_val, sim_val, label=f"Coordinate {j}") - ax.set_xlabel("Data") - ax.set_ylabel("Simulation") + ax.scatter(obs_val, sim_val, label=f'Coordinate {j}') + ax.set_xlabel('Data') + ax.set_ylabel('Simulation') else: logger.info( - f"Data type {type(obs)} for key {obs_key} is " - f"not supported." + f'Data type {type(obs)} for key {obs_key} is ' + f'not supported.' ) # remove not needed axis ax.axis('off') diff --git a/pyabc/visualization/distance.py b/pyabc/visualization/distance.py index 134aa567d..c2023763b 100644 --- a/pyabc/visualization/distance.py +++ b/pyabc/visualization/distance.py @@ -1,6 +1,6 @@ """Visualization of distance functions.""" -from typing import Any, List, Tuple, Union +from typing import Any import matplotlib as mpl import matplotlib.axes @@ -13,18 +13,18 @@ def plot_distance_weights( - log_files: Union[List[str], str], - ts: Union[List[int], List[str], int, str] = "last", - labels: Union[List[str], str] = None, - colors: Union[List[Any], Any] = None, - linestyles: Union[List[str], str] = None, + log_files: list[str] | str, + ts: list[int] | list[str] | int | str = 'last', + labels: list[str] | str = None, + colors: list[Any] | Any = None, + linestyles: list[str] | str = None, keys_as_labels: bool = True, - keys: List[str] = None, + keys: list[str] = None, xticklabel_rotation: float = 0, normalize: bool = True, - size: Tuple[float, float] = None, - xlabel: str = "Summary statistic", - ylabel: str = "Weight", + size: tuple[float, float] = None, + xlabel: str = 'Summary statistic', + ylabel: str = 'Weight', title: str = None, ax: mpl.axes.Axes = None, **kwargs, @@ -79,8 +79,8 @@ def plot_distance_weights( labels = get_labels(labels, len(log_files)) # default keyword arguments - if "marker" not in kwargs: - kwargs["marker"] = "x" + if 'marker' not in kwargs: + kwargs['marker'] = 'x' n_run = len(log_files) @@ -95,7 +95,7 @@ def plot_distance_weights( log_files, ts, labels, colors, linestyles ): weights = load_dict_from_json(log_file) - if t == "last": + if t == 'last': t = max(weights.keys()) weights = weights[t] if keys is None: diff --git a/pyabc/visualization/effective_sample_size.py b/pyabc/visualization/effective_sample_size.py index 98bf54375..6a535915a 100644 --- a/pyabc/visualization/effective_sample_size.py +++ b/pyabc/visualization/effective_sample_size.py @@ -1,6 +1,6 @@ """Effective sample size plots""" -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING import matplotlib as mpl import matplotlib.pyplot as plt @@ -15,9 +15,9 @@ def _prepare_plot_effective_sample_sizes( - histories: Union[List, History], - labels: Union[List, str], - colors: List, + histories: list | History, + labels: list | str, + colors: list, relative: bool, ): # preprocess input @@ -43,12 +43,12 @@ def _prepare_plot_effective_sample_sizes( def plot_effective_sample_sizes( - histories: Union[List, History], - labels: Union[List, str] = None, + histories: list | History, + labels: list | str = None, rotation: int = 0, - title: str = "Effective sample size", + title: str = 'Effective sample size', relative: bool = False, - colors: List = None, + colors: list = None, size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -100,8 +100,8 @@ def plot_effective_sample_sizes( ax.plot(range(0, len(esss)), esss, 'x-', label=label, color=color) # format - ax.set_xlabel("Population index") - ax.set_ylabel("ESS") + ax.set_xlabel('Population index') + ax.set_ylabel('ESS') if any(lab is not None for lab in labels): ax.legend() ax.set_title(title) @@ -120,15 +120,15 @@ def plot_effective_sample_sizes( def plot_effective_sample_sizes_plotly( - histories: Union[List, History], - labels: Union[List, str] = None, + histories: list | History, + labels: list | str = None, rotation: int = 0, - title: str = "Effective sample size", + title: str = 'Effective sample size', relative: bool = False, - colors: List = None, + colors: list = None, size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot effective sample sizes using plotly.""" import plotly.graph_objects as go @@ -147,7 +147,7 @@ def plot_effective_sample_sizes_plotly( go.Scatter( x=list(range(0, len(esss))), y=esss, - mode="lines+markers", + mode='lines+markers', name=label, marker={'color': color}, ) @@ -155,10 +155,10 @@ def plot_effective_sample_sizes_plotly( # format fig.update_layout( - xaxis_title="Population index", - yaxis_title="ESS", + xaxis_title='Population index', + yaxis_title='ESS', title=title, - xaxis={'tickmode': "linear"}, + xaxis={'tickmode': 'linear'}, ) # rotate x tick labels fig.update_xaxes(tickangle=rotation) diff --git a/pyabc/visualization/epsilon.py b/pyabc/visualization/epsilon.py index 7bb0c41ae..4dc67d27c 100644 --- a/pyabc/visualization/epsilon.py +++ b/pyabc/visualization/epsilon.py @@ -1,6 +1,6 @@ """Epsilon threshold plots""" -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING import matplotlib as mpl import matplotlib.pyplot as plt @@ -15,9 +15,9 @@ def _prepare( - histories: Union[List, History], - labels: Union[List, str], - colors: List, + histories: list | History, + labels: list | str, + colors: list, ): # preprocess input histories = to_lists(histories) @@ -36,11 +36,11 @@ def _prepare( def plot_epsilons( - histories: Union[List, History], - labels: Union[List, str] = None, - colors: List = None, + histories: list | History, + labels: list | str = None, + colors: list = None, yscale: str = 'log', - title: str = "Epsilon values", + title: str = 'Epsilon values', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -85,8 +85,8 @@ def plot_epsilons( ax.plot(ep, 'x-', label=label, color=color) # format - ax.set_xlabel("Population index") - ax.set_ylabel("Epsilon") + ax.set_xlabel('Population index') + ax.set_ylabel('Epsilon') if any(lab is not None for lab in labels): ax.legend() ax.set_title(title) @@ -102,14 +102,14 @@ def plot_epsilons( def plot_epsilons_plotly( - histories: Union[List, History], - labels: Union[List, str] = None, - colors: List = None, + histories: list | History, + labels: list | str = None, + colors: list = None, yscale: str = 'log', - title: str = "Epsilon values", + title: str = 'Epsilon values', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot epsilon trajectory using plotly.""" import plotly.graph_objects as go @@ -134,8 +134,8 @@ def plot_epsilons_plotly( # format fig.update_layout( - xaxis_title="Population index", - yaxis_title="Epsilon", + xaxis_title='Population index', + yaxis_title='Epsilon', title=title, yaxis_type=yscale, ) diff --git a/pyabc/visualization/sample.py b/pyabc/visualization/sample.py index cdf68de8e..9b87a5b42 100644 --- a/pyabc/visualization/sample.py +++ b/pyabc/visualization/sample.py @@ -1,6 +1,6 @@ """Sample number plots""" -from typing import TYPE_CHECKING, List, Tuple, Union +from typing import TYPE_CHECKING import matplotlib as mpl import matplotlib.pyplot as plt @@ -17,8 +17,8 @@ def _prepare_plot_sample_numbers( - histories: Union[List[History], History], - labels: Union[List[str], str], + histories: list[History] | History, + labels: list[str] | str, ): # preprocess input histories = to_lists(histories) @@ -43,11 +43,11 @@ def _prepare_plot_sample_numbers( def plot_sample_numbers( - histories: Union[List[History], History], - labels: Union[List[str], str] = None, + histories: list[History] | History, + labels: list[str] | str = None, rotation: int = 0, - title: str = "Required samples", - size: Tuple[float, float] = None, + title: str = 'Required samples', + size: tuple[float, float] = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: """ @@ -92,15 +92,15 @@ def plot_sample_numbers( x=np.arange(n_run), height=matrix[i_pop, :], bottom=np.sum(matrix[:i_pop, :], axis=0), - label=f"Generation {i_pop-1}", + label=f'Generation {i_pop-1}', ) # add labels ax.set_xticks(np.arange(n_run)) ax.set_xticklabels(labels, rotation=rotation) ax.set_title(title) - ax.set_ylabel("Samples") - ax.set_xlabel("Run") + ax.set_ylabel('Samples') + ax.set_xlabel('Run') ax.legend() # set size if size is not None: @@ -111,13 +111,13 @@ def plot_sample_numbers( def plot_sample_numbers_plotly( - histories: Union[List[History], History], - labels: Union[List[str], str] = None, + histories: list[History] | History, + labels: list[str] | str = None, rotation: int = 0, - title: str = "Required samples", - size: Tuple[float, float] = None, - fig: "go.Figure" = None, -) -> "go.Figure": + title: str = 'Required samples', + size: tuple[float, float] = None, + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot sample numbers using plotly.""" import plotly.graph_objects as go @@ -128,7 +128,7 @@ def plot_sample_numbers_plotly( # none or empty values are not supported by plotly for ix in range(n_run): if labels[ix] is None: - labels[ix] = " " + labels[ix] = ' ' # create figure if fig is None: @@ -140,7 +140,7 @@ def plot_sample_numbers_plotly( go.Bar( x=np.arange(n_run), y=matrix[i_pop, :], - name=f"Generation {i_pop-1}", + name=f'Generation {i_pop-1}', offsetgroup=0, base=np.sum(matrix[:i_pop, :], axis=0), ) @@ -149,13 +149,13 @@ def plot_sample_numbers_plotly( # add labels fig.update_layout( xaxis=go.layout.XAxis( - tickmode="array", + tickmode='array', tickvals=list(range(n_run)), ticktext=labels, tickangle=rotation, - title="Run", + title='Run', ), - yaxis=go.layout.YAxis(title="Samples"), + yaxis=go.layout.YAxis(title='Samples'), title=title, ) @@ -166,8 +166,8 @@ def plot_sample_numbers_plotly( def _prepare_plot_total_sample_numbers( - histories: Union[List[History], History], - labels: Union[List[str], str], + histories: list[History] | History, + labels: list[str] | str, yscale: str, ): # preprocess input @@ -186,22 +186,22 @@ def _prepare_plot_total_sample_numbers( samples = np.array(samples) # apply scale - ylabel = "Total samples" + ylabel = 'Total samples' if yscale == 'log': samples = np.log(samples) - ylabel = "log(" + ylabel + ")" + ylabel = 'log(' + ylabel + ')' elif yscale == 'log10': samples = np.log10(samples) - ylabel = "log10(" + ylabel + ")" + ylabel = 'log10(' + ylabel + ')' return samples, labels, ylabel, n_run def plot_total_sample_numbers( - histories: Union[List, History], - labels: Union[List, str] = None, + histories: list | History, + labels: list | str = None, rotation: int = 0, - title: str = "Total required samples", + title: str = 'Total required samples', yscale: str = 'lin', size: tuple = None, ax: mpl.axes.Axes = None, @@ -255,7 +255,7 @@ def plot_total_sample_numbers( ax.set_xticklabels(labels, rotation=rotation) ax.set_title(title) ax.set_ylabel(ylabel) - ax.set_xlabel("Run") + ax.set_xlabel('Run') # set size if size is not None: fig.set_size_inches(size) @@ -265,14 +265,14 @@ def plot_total_sample_numbers( def plot_total_sample_numbers_plotly( - histories: Union[List, History], - labels: Union[List, str] = None, + histories: list | History, + labels: list | str = None, rotation: int = 0, - title: str = "Total required samples", + title: str = 'Total required samples', yscale: str = 'lin', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot total sample numbers using plotly.""" import plotly.graph_objects as go @@ -290,7 +290,7 @@ def plot_total_sample_numbers_plotly( go.Bar( x=np.arange(n_run), y=samples, - name="Total samples", + name='Total samples', offsetgroup=0, base=0, ) @@ -299,7 +299,7 @@ def plot_total_sample_numbers_plotly( # add labels fig.update_layout( xaxis=go.layout.XAxis( - tickmode="array", + tickmode='array', tickvals=list(range(n_run)), ticktext=labels, tickangle=rotation, @@ -315,8 +315,8 @@ def plot_total_sample_numbers_plotly( def _prepare_plot_sample_numbers_trajectory( - histories: Union[List, History], - labels: Union[List, str], + histories: list | History, + labels: list | str, yscale: str, ): """Prepare data for plotting sample number trajectories.""" @@ -336,21 +336,21 @@ def _prepare_plot_sample_numbers_trajectory( samples.append(np.array(h_info['samples'])) # apply scale - ylabel = "Samples" + ylabel = 'Samples' if yscale == 'log': samples = [np.log(sample) for sample in samples] - ylabel = "log(" + ylabel + ")" + ylabel = 'log(' + ylabel + ')' elif yscale == 'log10': samples = [np.log10(sample) for sample in samples] - ylabel = "log10(" + ylabel + ")" + ylabel = 'log10(' + ylabel + ')' return samples, times, labels, ylabel def plot_sample_numbers_trajectory( - histories: Union[List, History], - labels: Union[List, str] = None, - title: str = "Required samples", + histories: list | History, + labels: list | str = None, + title: str = 'Required samples', yscale: str = 'lin', size: tuple = None, ax: mpl.axes.Axes = None, @@ -401,7 +401,7 @@ def plot_sample_numbers_trajectory( ax.legend() ax.set_title(title) ax.set_ylabel(ylabel) - ax.set_xlabel("Population index $t$") + ax.set_xlabel('Population index $t$') # set size if size is not None: fig.set_size_inches(size) @@ -411,13 +411,13 @@ def plot_sample_numbers_trajectory( def plot_sample_numbers_trajectory_plotly( - histories: Union[List, History], - labels: Union[List, str] = None, - title: str = "Required samples", + histories: list | History, + labels: list | str = None, + title: str = 'Required samples', yscale: str = 'lin', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot sample number trajectories using plotly.""" import plotly.graph_objects as go @@ -444,7 +444,7 @@ def plot_sample_numbers_trajectory_plotly( # add labels fig.update_layout( title=title, - xaxis=go.layout.XAxis(title="Population index $t$"), + xaxis=go.layout.XAxis(title='Population index $t$'), yaxis=go.layout.YAxis(title=ylabel), ) @@ -455,10 +455,10 @@ def plot_sample_numbers_trajectory_plotly( def _prepare_plot_acceptance_rates_trajectory( - histories: Union[List, History], - labels: Union[List, str], + histories: list | History, + labels: list | str, yscale: str, - colors: List[str], + colors: list[str], normalize_by_ess: bool, ): # preprocess input @@ -493,24 +493,24 @@ def _prepare_plot_acceptance_rates_trajectory( rates.append(pop_size / sample) # apply scale - ylabel = "Acceptance rate" + ylabel = 'Acceptance rate' if yscale == 'log': rates = [np.log(rate) for rate in rates] - ylabel = "log(" + ylabel + ")" + ylabel = 'log(' + ylabel + ')' elif yscale == 'log10': rates = [np.log10(rate) for rate in rates] - ylabel = "log10(" + ylabel + ")" + ylabel = 'log10(' + ylabel + ')' return rates, times, labels, ylabel, colors def plot_acceptance_rates_trajectory( - histories: Union[List, History], - labels: Union[List, str] = None, - title: str = "Acceptance rates", + histories: list | History, + labels: list | str = None, + title: str = 'Acceptance rates', yscale: str = 'lin', size: tuple = None, - colors: List[str] = None, + colors: list[str] = None, normalize_by_ess: bool = False, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -572,7 +572,7 @@ def plot_acceptance_rates_trajectory( ax.legend() ax.set_title(title) ax.set_ylabel(ylabel) - ax.set_xlabel("Population index $t$") + ax.set_xlabel('Population index $t$') # set size if size is not None: fig.set_size_inches(size) @@ -582,15 +582,15 @@ def plot_acceptance_rates_trajectory( def plot_acceptance_rates_trajectory_plotly( - histories: Union[List, History], - labels: Union[List, str] = None, - title: str = "Acceptance rates", + histories: list | History, + labels: list | str = None, + title: str = 'Acceptance rates', yscale: str = 'lin', size: tuple = None, - colors: List[str] = None, + colors: list[str] = None, normalize_by_ess: bool = False, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot acceptance rates trajectories using plotly.""" import plotly.graph_objects as go @@ -624,7 +624,7 @@ def plot_acceptance_rates_trajectory_plotly( # add labels fig.update_layout( title=title, - xaxis_title="Population index $t$", + xaxis_title='Population index $t$', yaxis_title=ylabel, ) # set size @@ -635,12 +635,12 @@ def plot_acceptance_rates_trajectory_plotly( def plot_lookahead_evaluations( - sampler_df: Union[pd.DataFrame, str], + sampler_df: pd.DataFrame | str, relative: bool = False, fill: bool = False, alpha: float = None, t_min: int = 0, - title: str = "Total evaluations", + title: str = 'Total evaluations', size: tuple = None, ax: mpl.axes.Axes = None, ): @@ -701,8 +701,8 @@ def plot_lookahead_evaluations( # plot if fill: - ax.fill_between(t, n_la, n_eval, alpha=alpha, label="Actual") - ax.fill_between(t, 0, n_la, alpha=alpha, label="Look-ahead") + ax.fill_between(t, n_la, n_eval, alpha=alpha, label='Actual') + ax.fill_between(t, 0, n_la, alpha=alpha, label='Look-ahead') else: ax.plot( t, @@ -711,16 +711,16 @@ def plot_lookahead_evaluations( marker='o', color='black', alpha=alpha, - label="Total", + label='Total', ) - ax.plot(t, n_act, marker='o', alpha=alpha, label="Actual") - ax.plot(t, n_la, marker='o', alpha=alpha, label="Look-ahead") + ax.plot(t, n_act, marker='o', alpha=alpha, label='Actual') + ax.plot(t, n_la, marker='o', alpha=alpha, label='Look-ahead') # prettify plot ax.legend() ax.set_title(title) - ax.set_xlabel("Population index") - ax.set_ylabel("Evaluations") + ax.set_xlabel('Population index') + ax.set_ylabel('Evaluations') ax.set_ylim(bottom=0) # enforce integer ticks ax.xaxis.set_major_locator(MaxNLocator(integer=True)) @@ -731,13 +731,13 @@ def plot_lookahead_evaluations( def plot_lookahead_final_acceptance_fractions( - sampler_df: Union[pd.DataFrame, str], - population_sizes: Union[np.ndarray, History], + sampler_df: pd.DataFrame | str, + population_sizes: np.ndarray | History, relative: bool = False, fill: bool = False, alpha: float = None, t_min: int = 0, - title: str = "Composition of final acceptances", + title: str = 'Composition of final acceptances', size: tuple = None, ax: mpl.axes.Axes = None, ): @@ -818,9 +818,9 @@ def plot_lookahead_final_acceptance_fractions( # plot if fill: ax.fill_between( - t, n_la_acc, population_sizes, alpha=alpha, label="Actual" + t, n_la_acc, population_sizes, alpha=alpha, label='Actual' ) - ax.fill_between(t, 0, n_la_acc, alpha=alpha, label="Look-ahead") + ax.fill_between(t, 0, n_la_acc, alpha=alpha, label='Look-ahead') else: ax.plot( t, @@ -829,16 +829,16 @@ def plot_lookahead_final_acceptance_fractions( marker='o', color='black', alpha=alpha, - label="Population size", + label='Population size', ) - ax.plot(t, n_act_acc, marker='o', alpha=alpha, label="Actual") - ax.plot(t, n_la_acc, marker='o', alpha=alpha, label="Look-ahead") + ax.plot(t, n_act_acc, marker='o', alpha=alpha, label='Actual') + ax.plot(t, n_la_acc, marker='o', alpha=alpha, label='Look-ahead') # prettify plot ax.legend() ax.set_title(title) - ax.set_xlabel("Population index") - ax.set_ylabel("Final acceptances") + ax.set_xlabel('Population index') + ax.set_ylabel('Final acceptances') ax.set_ylim(bottom=0) # enforce integer ticks ax.xaxis.set_major_locator(MaxNLocator(integer=True)) @@ -849,9 +849,9 @@ def plot_lookahead_final_acceptance_fractions( def plot_lookahead_acceptance_rates( - sampler_df: Union[pd.DataFrame, str], + sampler_df: pd.DataFrame | str, t_min: int = 0, - title: str = "Acceptance rates", + title: str = 'Acceptance rates', size: tuple = None, ax: mpl.axes.Axes = None, ): @@ -915,16 +915,16 @@ def plot_lookahead_acceptance_rates( linestyle='--', marker='o', color='black', - label="Combined", + label='Combined', ) - ax.plot(t, n_act_acc / n_act, marker='o', label="Actual") - ax.plot(t, n_la_acc / n_la, marker='o', label="Look-ahead") + ax.plot(t, n_act_acc / n_act, marker='o', label='Actual') + ax.plot(t, n_la_acc / n_la, marker='o', label='Look-ahead') # prettify plot ax.legend() ax.set_title(title) - ax.set_xlabel("Population index") - ax.set_ylabel("Acceptance rate") + ax.set_xlabel('Population index') + ax.set_ylabel('Acceptance rate') ax.set_ylim(bottom=0) # enforce integer ticks ax.xaxis.set_major_locator(MaxNLocator(integer=True)) diff --git a/pyabc/visualization/sankey.py b/pyabc/visualization/sankey.py index aeb59349f..fe9719926 100644 --- a/pyabc/visualization/sankey.py +++ b/pyabc/visualization/sankey.py @@ -1,6 +1,6 @@ """Sensitivity sankey flow plot.""" -from typing import Callable, Dict, List, Union +from collections.abc import Callable import numpy as np @@ -20,7 +20,7 @@ def plot_sensitivity_sankey( info_sample_log_file: str, - t: Union[int, str], + t: int | str, h: pyabc.storage.History, predictor: pyabc.predictor.Predictor, par_trafo: pyabc.util.ParTrafoBase = None, @@ -28,9 +28,9 @@ def plot_sensitivity_sankey( subsetter: pyabc.sumstat.Subsetter = None, feature_normalization: str = pyabc.distance.InfoWeightedPNormDistance.MAD, normalize_by_par: bool = True, - fd_deltas: Union[List[float], float] = None, - scale_weights: Dict[int, np.ndarray] = None, - title: str = "Data-parameter sensitivities", + fd_deltas: list[float] | float = None, + scale_weights: dict[int, np.ndarray] = None, + title: str = 'Data-parameter sensitivities', width: float = None, height: float = None, sumstat_color: Callable[[str], str] = None, @@ -116,11 +116,11 @@ def plot_sensitivity_sankey( if node_kwargs is None: node_kwargs = {} node_kwargs_all = { - "pad": 15, - "thickness": 20, - "line": { - "color": "black", - "width": 0.5, + 'pad': 15, + 'thickness': 20, + 'line': { + 'color': 'black', + 'width': 0.5, }, } node_kwargs_all.update(node_kwargs) @@ -128,9 +128,9 @@ def plot_sensitivity_sankey( if layout_kwargs is None: layout_kwargs = {} layout_kwargs_all = { - "title_x": 0.5, - "font_size": 12, - "template": "simple_white", + 'title_x': 0.5, + 'font_size': 12, + 'template': 'simple_white', } layout_kwargs_all.update(layout_kwargs) @@ -148,8 +148,8 @@ def plot_sensitivity_sankey( # read training samples sumstats, parameters, weights = [ - np.load(f"{info_sample_log_file}_{t}_{var}.npy") - for var in ["sumstats", "parameters", "weights"] + np.load(f'{info_sample_log_file}_{t}_{var}.npy') + for var in ['sumstats', 'parameters', 'weights'] ] s_0 = sumstat(data) @@ -166,7 +166,7 @@ def plot_sensitivity_sankey( scale_weights=scale_weights, ) x, y, weights, use_ixs, x0 = ( - ret[key] for key in ("x", "y", "weights", "use_ixs", "x0") + ret[key] for key in ('x', 'y', 'weights', 'use_ixs', 'x0') ) # learn predictor model @@ -205,7 +205,7 @@ def plot_sensitivity_sankey( def default_sumstat_color(id_: str): # extract summary statistic name - base = id_.split(":")[0] + base = id_.split(':')[0] if base in sumstat_color_dict: return sumstat_color_dict[base] @@ -213,7 +213,7 @@ def default_sumstat_color(id_: str): i = len(sumstat_color_dict) color = getattr( colors, - f"{colors.REDSORANGES[i % len(colors.REDSORANGES)]}400", + f'{colors.REDSORANGES[i % len(colors.REDSORANGES)]}400', ) sumstat_color_dict[base] = color return color @@ -223,12 +223,12 @@ def default_sumstat_color(id_: str): def default_par_color(id_: str): # extract parameter base name # this may require customization - if "^" in id_: - base = id_.split("^")[0] - elif "(" in id_: - base = id_.split("(")[1].split(")")[0] + if '^' in id_: + base = id_.split('^')[0] + elif '(' in id_: + base = id_.split('(')[1].split(')')[0] else: - base = id_.split("_")[0] + base = id_.split('_')[0] if base in par_color_dict: return par_color_dict[base] @@ -236,7 +236,7 @@ def default_par_color(id_: str): i = len(par_color_dict) color = getattr( colors, - f"{colors.GREENSBLUES[i % len(colors.GREENSBLUES)]}400", + f'{colors.GREENSBLUES[i % len(colors.GREENSBLUES)]}400', ) par_color_dict[base] = color return color @@ -256,14 +256,14 @@ def default_par_color(id_: str): data=[ go.Sankey( node={ - "label": node_label, - "color": node_color, + 'label': node_label, + 'color': node_color, **node_kwargs_all, }, link={ - "source": source, - "target": target, - "value": value, + 'source': source, + 'target': target, + 'value': value, }, ), ], diff --git a/pyabc/visualization/walltime.py b/pyabc/visualization/walltime.py index 83f1e753d..52b9c5c22 100644 --- a/pyabc/visualization/walltime.py +++ b/pyabc/visualization/walltime.py @@ -1,7 +1,7 @@ """Walltime plots""" import datetime -from typing import TYPE_CHECKING, Any, List, Union +from typing import TYPE_CHECKING, Any import matplotlib as mpl import matplotlib.pyplot as plt @@ -22,8 +22,8 @@ def _prepare_plot_total_walltime( - histories: Union[List[History], History], - labels: Union[List, str], + histories: list[History] | History, + labels: list | str, unit: str, ): # preprocess input @@ -33,7 +33,7 @@ def _prepare_plot_total_walltime( # check time unit if unit not in TIME_UNITS: - raise AssertionError(f"`unit` must be in {TIME_UNITS}") + raise AssertionError(f'`unit` must be in {TIME_UNITS}') # extract total walltimes walltimes = [] @@ -54,11 +54,11 @@ def _prepare_plot_total_walltime( def plot_total_walltime( - histories: Union[List[History], History], - labels: Union[List, str] = None, + histories: list[History] | History, + labels: list | str = None, unit: str = 's', rotation: int = 0, - title: str = "Total walltimes", + title: str = 'Total walltimes', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -103,8 +103,8 @@ def plot_total_walltime( ax.set_xticks(np.arange(n_run)) ax.set_xticklabels(labels, rotation=rotation) ax.set_title(title) - ax.set_xlabel("Run") - ax.set_ylabel(f"Time [{unit}]") + ax.set_xlabel('Run') + ax.set_ylabel(f'Time [{unit}]') if size is not None: fig.set_size_inches(size) fig.tight_layout() @@ -113,14 +113,14 @@ def plot_total_walltime( def plot_total_walltime_plotly( - histories: Union[List[History], History], - labels: Union[List, str] = None, + histories: list[History] | History, + labels: list | str = None, unit: str = 's', rotation: int = 0, - title: str = "Total walltimes", + title: str = 'Total walltimes', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot total walltimes using plotly.""" import plotly.graph_objects as go @@ -145,8 +145,8 @@ def plot_total_walltime_plotly( 'tickangle': rotation, }, title=title, - xaxis_title="Run", - yaxis_title=f"Time [{unit}]", + xaxis_title='Run', + yaxis_title=f'Time [{unit}]', ) if size is not None: fig.update_layout(width=size[0], height=size[1]) @@ -155,7 +155,7 @@ def plot_total_walltime_plotly( def _prepare_walltime( - histories: Union[List[History], History], + histories: list[History] | History, show_calibration: bool, ): # preprocess input @@ -180,12 +180,12 @@ def _prepare_walltime( def plot_walltime( - histories: Union[List[History], History], - labels: Union[List, str] = None, + histories: list[History] | History, + labels: list | str = None, show_calibration: bool = None, unit: str = 's', rotation: int = 0, - title: str = "Walltime by generation", + title: str = 'Walltime by generation', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -237,15 +237,15 @@ def plot_walltime( def plot_walltime_plotly( - histories: Union[List[History], History], - labels: Union[List, str] = None, + histories: list[History] | History, + labels: list | str = None, show_calibration: bool = None, unit: str = 's', rotation: int = 0, - title: str = "Walltime by generation", + title: str = 'Walltime by generation', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot walltimes using plotly.""" # preprocess input start_times, end_times, show_calibration = _prepare_walltime( @@ -266,9 +266,9 @@ def plot_walltime_plotly( def _prepare_plot_walltime_lowlevel( - end_times: List, - start_times: Union[List, None] = None, - labels: Union[List, str] = None, + end_times: list, + start_times: list | None = None, + labels: list | str = None, show_calibration: bool = None, unit: str = 's', ): @@ -281,14 +281,14 @@ def _prepare_plot_walltime_lowlevel( if start_times is None: if show_calibration: raise AssertionError( - "To plot the calibration iteration, start times are needed." + 'To plot the calibration iteration, start times are needed.' ) # fill in dummy times which will not be used anyhow start_times = [datetime.datetime.now() for _ in range(n_run)] # check time unit if unit not in TIME_UNITS: - raise AssertionError(f"`unit` must be in {TIME_UNITS}") + raise AssertionError(f'`unit` must be in {TIME_UNITS}') # extract relative walltimes walltimes = [] @@ -323,13 +323,13 @@ def _prepare_plot_walltime_lowlevel( def plot_walltime_lowlevel( - end_times: List, - start_times: Union[List, None] = None, - labels: Union[List, str] = None, + end_times: list, + start_times: list | None = None, + labels: list | str = None, show_calibration: bool = None, unit: str = 's', rotation: int = 0, - title: str = "Walltime by generation", + title: str = 'Walltime by generation', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -361,15 +361,15 @@ def plot_walltime_lowlevel( x=np.arange(n_run), height=matrix[i_pop, :], bottom=np.sum(matrix[:i_pop, :], axis=0), - label=f"Generation {pop_ix}", + label=f'Generation {pop_ix}', ) # prettify plot ax.set_xticks(np.arange(n_run)) ax.set_xticklabels(labels, rotation=rotation) ax.set_title(title) - ax.set_xlabel("Run") - ax.set_ylabel(f"Time [{unit}]") + ax.set_xlabel('Run') + ax.set_ylabel(f'Time [{unit}]') ax.legend() if size is not None: fig.set_size_inches(size) @@ -379,16 +379,16 @@ def plot_walltime_lowlevel( def plot_walltime_lowlevel_plotly( - end_times: List, - start_times: Union[List, None] = None, - labels: Union[List, str] = None, + end_times: list, + start_times: list | None = None, + labels: list | str = None, show_calibration: bool = None, unit: str = 's', rotation: int = 0, - title: str = "Walltime by generation", + title: str = 'Walltime by generation', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Low-level access to `plot_walltime_plotly`.""" import plotly.graph_objects as go @@ -414,7 +414,7 @@ def plot_walltime_lowlevel_plotly( go.Bar( x=np.arange(n_run), y=matrix[i_pop, :], - name=f"Generation {pop_ix}", + name=f'Generation {pop_ix}', offsetgroup=0, base=np.sum(matrix[:i_pop, :], axis=0), ) @@ -422,13 +422,13 @@ def plot_walltime_lowlevel_plotly( # prettify plot fig.update_layout( - xaxis_title="Run", - yaxis_title=f"Time [{unit}]", + xaxis_title='Run', + yaxis_title=f'Time [{unit}]', title=title, xaxis_tickvals=np.arange(n_run), xaxis_ticktext=labels, xaxis_tickangle=rotation, - legend_title="Generation", + legend_title='Generation', ) if size is not None: @@ -438,7 +438,7 @@ def plot_walltime_lowlevel_plotly( def _prepare_plot_eps_walltime( - histories: Union[List[History], History], + histories: list[History] | History, ): # preprocess input histories = to_lists(histories) @@ -455,15 +455,15 @@ def _prepare_plot_eps_walltime( def plot_eps_walltime( - histories: Union[List[History], History], - labels: Union[List, str] = None, - colors: List[Any] = None, + histories: list[History] | History, + labels: list | str = None, + colors: list[Any] = None, group_by_label: bool = True, indicate_end: bool = True, unit: str = 's', xscale: str = 'linear', yscale: str = 'log', - title: str = "Epsilon over walltime", + title: str = 'Epsilon over walltime', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -521,18 +521,18 @@ def plot_eps_walltime( def plot_eps_walltime_plotly( - histories: Union[List[History], History], - labels: Union[List, str] = None, - colors: List[Any] = None, + histories: list[History] | History, + labels: list | str = None, + colors: list[Any] = None, group_by_label: bool = True, indicate_end: bool = True, unit: str = 's', xscale: str = 'linear', yscale: str = 'log', - title: str = "Epsilon over walltime", + title: str = 'Epsilon over walltime', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot epsilon values over walltime using plotly.""" # preprocess input end_times, eps = _prepare_plot_eps_walltime(histories=histories) @@ -554,10 +554,10 @@ def plot_eps_walltime_plotly( def _prepare_plot_eps_walltime_lowlevel( - end_times: List, - eps: List, - labels: Union[List, str], - colors: List[Any], + end_times: list, + eps: list, + labels: list | str, + colors: list[Any], group_by_label: bool, unit: str, ): @@ -573,7 +573,7 @@ def _prepare_plot_eps_walltime_lowlevel( for ix, label in enumerate(labels): if label not in labels[:ix]: color_ix += 1 - colors.append(f"C{color_ix}") + colors.append(f'C{color_ix}') labels = [ x if x not in labels[:ix] else None for ix, x in enumerate(labels) @@ -583,7 +583,7 @@ def _prepare_plot_eps_walltime_lowlevel( # check time unit if unit not in TIME_UNITS: - raise AssertionError(f"`unit` must be in {TIME_UNITS}") + raise AssertionError(f'`unit` must be in {TIME_UNITS}') # extract relative walltimes walltimes = [] @@ -602,16 +602,16 @@ def _prepare_plot_eps_walltime_lowlevel( def plot_eps_walltime_lowlevel( - end_times: List, - eps: List, - labels: Union[List, str] = None, - colors: List[Any] = None, + end_times: list, + eps: list, + labels: list | str = None, + colors: list[Any] = None, group_by_label: bool = True, indicate_end: bool = True, unit: str = 's', xscale: str = 'linear', yscale: str = 'log', - title: str = "Epsilon over walltime", + title: str = 'Epsilon over walltime', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -662,8 +662,8 @@ def plot_eps_walltime_lowlevel( if n_run > 1: ax.legend() ax.set_title(title) - ax.set_xlabel(f"Time [{unit}]") - ax.set_ylabel("Epsilon") + ax.set_xlabel(f'Time [{unit}]') + ax.set_ylabel('Epsilon') ax.set_xscale(xscale) ax.set_yscale(yscale) # enforce integer ticks @@ -677,19 +677,19 @@ def plot_eps_walltime_lowlevel( def plot_eps_walltime_lowlevel_plotly( - end_times: List, - eps: List, - labels: Union[List, str] = None, - colors: List[Any] = None, + end_times: list, + eps: list, + labels: list | str = None, + colors: list[Any] = None, group_by_label: bool = True, indicate_end: bool = True, unit: str = 's', xscale: str = 'linear', yscale: str = 'log', - title: str = "Epsilon over walltime", + title: str = 'Epsilon over walltime', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot epsilon values over walltime using plotly.""" import plotly.graph_objects as go @@ -728,22 +728,22 @@ def plot_eps_walltime_lowlevel_plotly( if indicate_end: # add a vertical line from minimum to maximum value fig.add_shape( - type="line", + type='line', x0=wt[-1], y0=ep[-1], x1=wt[-1], y1=ep[0], line={ 'width': 1, - 'dash': "dash", + 'dash': 'dash', }, ) # prettify plot fig.update_layout( title=title, - xaxis_title=f"Time [{unit}]", - yaxis_title="Epsilon", + xaxis_title=f'Time [{unit}]', + yaxis_title='Epsilon', xaxis_type=xscale, yaxis_type=yscale, ) diff --git a/pyproject.toml b/pyproject.toml index f4f06fb2b..90725c362 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,8 +155,9 @@ select = [ ] ignore = [ "E501", # line too long (handled by formatter) - "B008", # do not perform function calls in argument defaults "B905", # zip() without an explicit strict= parameter + "SIM105", # Replace `try`-`except`-`pass` + "C408", ] [tool.ruff.lint.per-file-ignores] diff --git a/test/base/test_populationstrategy.py b/test/base/test_populationstrategy.py index cc9b01205..f98909c3c 100644 --- a/test/base/test_populationstrategy.py +++ b/test/base/test_populationstrategy.py @@ -46,7 +46,7 @@ def population_strategy_calibration(request): def test_adapt_single_model(population_strategy: PopulationStrategy): n = 10 - df = pd.DataFrame([{"s": np.random.rand()} for _ in range(n)]) + df = pd.DataFrame([{'s': np.random.rand()} for _ in range(n)]) w = np.ones(n) / n kernel = MultivariateNormalTransition() kernel.fit(df, w) @@ -59,7 +59,7 @@ def test_adapt_two_models(population_strategy: PopulationStrategy): n = 10 kernels = [] for _ in range(2): - df = pd.DataFrame([{"s": np.random.rand()} for _ in range(n)]) + df = pd.DataFrame([{'s': np.random.rand()} for _ in range(n)]) w = np.ones(n) / n kernel = MultivariateNormalTransition() kernel.fit(df, w) @@ -96,7 +96,7 @@ def test_one_with_one_without_parameters( kernel_without.fit(df_without, w_without) kernels.append(kernel_without) - df_with = pd.DataFrame([{"s": np.random.rand()} for _ in range(n)]) + df_with = pd.DataFrame([{'s': np.random.rand()} for _ in range(n)]) w_with = np.ones(n) / n kernel_with = MultivariateNormalTransition() kernel_with.fit(df_with, w_with) @@ -109,10 +109,10 @@ def test_one_with_one_without_parameters( def test_transitions_not_modified(population_strategy: PopulationStrategy): n = 10 kernels = [] - test_points = pd.DataFrame([{"s": np.random.rand()} for _ in range(n)]) + test_points = pd.DataFrame([{'s': np.random.rand()} for _ in range(n)]) for _ in range(2): - df = pd.DataFrame([{"s": np.random.rand()} for _ in range(n)]) + df = pd.DataFrame([{'s': np.random.rand()} for _ in range(n)]) w = np.ones(n) / n kernel = MultivariateNormalTransition() kernel.fit(df, w) @@ -128,8 +128,9 @@ def test_transitions_not_modified(population_strategy: PopulationStrategy): (k1 == k2).all() for k1, k2 in zip(test_weights, after_adaptation_weights) ) - err_msg = "Population strategy {}" " modified the transitions".format( - population_strategy + err_msg = ( + f'Population strategy {population_strategy}' + ' modified the transitions' ) assert same, err_msg diff --git a/test/base/test_storage.py b/test/base/test_storage.py index 48a2fe95f..bbc1ace50 100644 --- a/test/base/test_storage.py +++ b/test/base/test_storage.py @@ -30,31 +30,31 @@ def example_df(): return pd.DataFrame( - {"col_a": [1, 2], "col_b": [1.1, 2.2], "col_c": ["foo", "bar"]}, - index=["ind_first", "ind_second"], + {'col_a': [1, 2], 'col_b': [1.1, 2.2], 'col_c': ['foo', 'bar']}, + index=['ind_first', 'ind_second'], ) def path(): - return os.path.join(tempfile.gettempdir(), "history_test.db") + return os.path.join(tempfile.gettempdir(), 'history_test.db') -@pytest.fixture(params=["file", "memory"]) +@pytest.fixture(params=['file', 'memory']) def history(request): # Test in-memory and filesystem based database - if request.param == "file": - this_path = "/" + path() - elif request.param == "memory": - this_path = "" + if request.param == 'file': + this_path = '/' + path() + elif request.param == 'memory': + this_path = '' else: - raise Exception(f"Bad database type for testing: {request.param}") - model_names = ["fake_name_{}".format(k) for k in range(50)] - h = History("sqlite://" + this_path) + raise Exception(f'Bad database type for testing: {request.param}') + model_names = [f'fake_name_{k}' for k in range(50)] + h = History('sqlite://' + this_path) h.store_initial_data( - 0, {}, {}, {}, model_names, "", "", '{"name": "pop_strategy_str_test"}' + 0, {}, {}, {}, model_names, '', '', '{"name": "pop_strategy_str_test"}' ) yield h - if request.param == "file": + if request.param == 'file': try: os.remove(this_path) except FileNotFoundError: @@ -66,7 +66,7 @@ def history_uninitialized(): # Don't use memory database for testing. # A real file with disconnect and reconnect is closer to the real scenario this_path = path() - h = History("sqlite:///" + this_path) + h = History('sqlite:///' + this_path) yield h try: os.remove(this_path) @@ -94,15 +94,15 @@ def rand_pop_list(m: int = 0, normalized: bool = True, n_sample: int = None): Particle( m=m, parameter=Parameter( - {"a": np.random.randint(10), "b": np.random.randn()} + {'a': np.random.randint(10), 'b': np.random.randn()} ), weight=np.random.rand() * 42, sum_stat={ - "ss_float": 0.1, - "ss_int": 42, - "ss_str": "foo bar string", - "ss_np": np.random.rand(13, 42), - "ss_df": example_df(), + 'ss_float': 0.1, + 'ss_int': 42, + 'ss_str': 'foo bar string', + 'ss_np': np.random.rand(13, 42), + 'ss_df': example_df(), }, accepted=True, distance=np.random.rand(), @@ -122,13 +122,13 @@ def test_single_particle_save_load(history: History): particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=1.0, - sum_stat={"ss": 0.1}, + sum_stat={'ss': 0.1}, distance=0.1, ), ] - history.append_population(0, 42, Population(particle_list), 2, [""]) + history.append_population(0, 42, Population(particle_list), 2, ['']) df, w = history.get_distribution(0, 0) assert w[0] == 1 @@ -145,9 +145,9 @@ def test_save_no_sum_stats(history: History): for _ in range(0, 6): particle = Particle( m=0, - parameter=Parameter({"th0": np.random.random()}), + parameter=Parameter({'th0': np.random.random()}), weight=1.0 / 6, - sum_stat={"ss0": np.random.random(), "ss1": np.random.random()}, + sum_stat={'ss0': np.random.random(), 'ss1': np.random.random()}, distance=np.random.random(), ) particle_list.append(particle) @@ -165,7 +165,7 @@ def test_save_no_sum_stats(history: History): current_epsilon=42.97, population=population, nr_simulations=10, - model_names=[""], + model_names=[''], ) # just call @@ -197,7 +197,7 @@ def test_get_population(history: History): current_epsilon=7.0, population=population, nr_simulations=200, - model_names=["m0"], + model_names=['m0'], ) population_h = history.get_population(t=0) @@ -225,13 +225,13 @@ def test_single_particle_save_load_np_int64(history: History): particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=1.0, - sum_stat={"ss": 0.1}, + sum_stat={'ss': 0.1}, distance=0.1, ) ] - history.append_population(0, 42, Population(particle_list), 2, [""]) + history.append_population(0, 42, Population(particle_list), 2, ['']) for m in m_list: for t in t_list: @@ -247,60 +247,60 @@ def test_sum_stats_save_load(history: History): particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=0.2, sum_stat={ - "ss1": 0.1, - "ss2": arr2, - "ss3": example_df(), - "rdf0": r["iris"], + 'ss1': 0.1, + 'ss2': arr2, + 'ss3': example_df(), + 'rdf0': r['iris'], }, distance=0.1, ), Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=0.8, sum_stat={ - "ss12": 0.11, - "ss22": arr, - "ss33": example_df(), - "rdf": r["mtcars"], + 'ss12': 0.11, + 'ss22': arr, + 'ss33': example_df(), + 'rdf': r['mtcars'], }, distance=0.1, ), ] history.append_population( - 0, 42, Population(particle_list), 2, ["m1", "m2"] + 0, 42, Population(particle_list), 2, ['m1', 'm2'] ) weights, sum_stats = history.get_weighted_sum_stats_for_model(0, 0) assert (weights == np.array([0.2, 0.8])).all() - assert sum_stats[0]["ss1"] == 0.1 - assert (sum_stats[0]["ss2"] == arr2).all() - assert (sum_stats[0]["ss3"] == example_df()).all().all() + assert sum_stats[0]['ss1'] == 0.1 + assert (sum_stats[0]['ss2'] == arr2).all() + assert (sum_stats[0]['ss3'] == example_df()).all().all() with localconverter(pandas2ri.converter): - assert (sum_stats[0]["rdf0"] == r["iris"]).all().all() - assert sum_stats[1]["ss12"] == 0.11 - assert (sum_stats[1]["ss22"] == arr).all() - assert (sum_stats[1]["ss33"] == example_df()).all().all() + assert (sum_stats[0]['rdf0'] == r['iris']).all().all() + assert sum_stats[1]['ss12'] == 0.11 + assert (sum_stats[1]['ss22'] == arr).all() + assert (sum_stats[1]['ss33'] == example_df()).all().all() with localconverter(pandas2ri.converter): - assert (sum_stats[1]["rdf"] == r["mtcars"]).all().all() + assert (sum_stats[1]['rdf'] == r['mtcars']).all().all() def test_total_nr_samples(history: History): particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=1.0, - sum_stat={"ss": 0.1}, + sum_stat={'ss': 0.1}, distance=0.1, ) ] population = Population(particle_list) - history.append_population(0, 42, population, 4234, ["m1"]) - history.append_population(0, 42, population, 3, ["m1"]) + history.append_population(0, 42, population, 4234, ['m1']) + history.append_population(0, 42, population, 3, ['m1']) assert 4237 == history.total_nr_simulations @@ -309,24 +309,24 @@ def test_t_count(history: History): particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=1.0, - sum_stat={"ss": 0.1}, + sum_stat={'ss': 0.1}, distance=0.1, ) ] for t in range(1, 10): - history.append_population(t, 42, Population(particle_list), 2, ["m1"]) + history.append_population(t, 42, Population(particle_list), 2, ['m1']) assert t == history.max_t def test_dataframe_storage_readout(): - path = os.path.join(tempfile.gettempdir(), "history_test.db") - model_names = ["fake_name"] * 5 + path = os.path.join(tempfile.gettempdir(), 'history_test.db') + model_names = ['fake_name'] * 5 def make_hist(): - h = History("sqlite:///" + path) - h.store_initial_data(0, {}, {}, {}, model_names, "", "", "") + h = History('sqlite:///' + path) + h.store_initial_data(0, {}, {}, {}, model_names, '', '', '') return h pops = {} @@ -367,16 +367,16 @@ def make_hist(): def test_population_retrieval(history: History): history.append_population( - 1, 0.23, Population(rand_pop_list(0)), 234, ["m1"] + 1, 0.23, Population(rand_pop_list(0)), 234, ['m1'] ) history.append_population( - 2, 0.123, Population(rand_pop_list(0)), 345, ["m1"] + 2, 0.123, Population(rand_pop_list(0)), 345, ['m1'] ) history.append_population( - 2, 0.1235, Population(rand_pop_list(5)), 20345, ["m1"] * 6 + 2, 0.1235, Population(rand_pop_list(5)), 20345, ['m1'] * 6 ) history.append_population( - 3, 0.12330, Population(rand_pop_list(30)), 30345, ["m1"] * 31 + 3, 0.12330, Population(rand_pop_list(30)), 30345, ['m1'] * 31 ) df = history.get_all_populations() @@ -397,12 +397,12 @@ def test_population_retrieval(history: History): def test_population_strategy_storage(history): res = history.get_population_strategy() - assert res["name"] == "pop_strategy_str_test" + assert res['name'] == 'pop_strategy_str_test' def test_model_probabilities(history): history.append_population( - 1, 0.23, Population(rand_pop_list(3)), 234, ["m0", "m1", "m2", "m3"] + 1, 0.23, Population(rand_pop_list(3)), 234, ['m0', 'm1', 'm2', 'm3'] ) probs = history.get_model_probabilities(1) assert probs.p[3] == 1 @@ -411,13 +411,13 @@ def test_model_probabilities(history): def test_model_probabilities_all(history): history.append_population( - 1, 0.23, Population(rand_pop_list(3)), 234, ["m0", "m1", "m2", "m3"] + 1, 0.23, Population(rand_pop_list(3)), 234, ['m0', 'm1', 'm2', 'm3'] ) probs = history.get_model_probabilities() assert (probs[3].values == np.array([1])).all() -@pytest.fixture(params=[0, None], ids=["GT=0", "GT=None"]) +@pytest.fixture(params=[0, None], ids=['GT=0', 'GT=None']) def gt_model(request): return request.param @@ -425,30 +425,30 @@ def gt_model(request): def test_observed_sum_stats(history_uninitialized: History, gt_model): h = history_uninitialized obs_sum_stats = { - "s1": 1, - "s2": 1.1, - "s3": np.array(0.1), - "s4": np.random.rand(10), + 's1': 1, + 's2': 1.1, + 's3': np.array(0.1), + 's4': np.random.rand(10), } - h.store_initial_data(gt_model, {}, obs_sum_stats, {}, [""], "", "", "") + h.store_initial_data(gt_model, {}, obs_sum_stats, {}, [''], '', '', '') h2 = History(h.db) loaded_sum_stats = h2.observed_sum_stat() - for k in ["s1", "s2", "s3"]: + for k in ['s1', 's2', 's3']: assert loaded_sum_stats[k] == obs_sum_stats[k] - assert (loaded_sum_stats["s4"] == obs_sum_stats["s4"]).all() - assert loaded_sum_stats["s1"] == obs_sum_stats["s1"] - assert loaded_sum_stats["s2"] == obs_sum_stats["s2"] - assert loaded_sum_stats["s3"] == obs_sum_stats["s3"] - assert loaded_sum_stats["s4"] is not obs_sum_stats["s4"] + assert (loaded_sum_stats['s4'] == obs_sum_stats['s4']).all() + assert loaded_sum_stats['s1'] == obs_sum_stats['s1'] + assert loaded_sum_stats['s2'] == obs_sum_stats['s2'] + assert loaded_sum_stats['s3'] == obs_sum_stats['s3'] + assert loaded_sum_stats['s4'] is not obs_sum_stats['s4'] def test_model_name_load(history_uninitialized: History): h = history_uninitialized - model_names = ["m1", "m2", "m3"] - h.store_initial_data(0, {}, {}, {}, model_names, "", "", "") + model_names = ['m1', 'm2', 'm3'] + h.store_initial_data(0, {}, {}, {}, model_names, '', '', '') h2 = History(h.db) model_names_loaded = h2.model_names() @@ -457,8 +457,8 @@ def test_model_name_load(history_uninitialized: History): def test_model_name_load_no_gt_model(history_uninitialized: History): h = history_uninitialized - model_names = ["m1", "m2", "m3"] - h.store_initial_data(None, {}, {}, {}, model_names, "", "", "") + model_names = ['m1', 'm2', 'm3'] + h.store_initial_data(None, {}, {}, {}, model_names, '', '', '') h2 = History(h.db) model_names_loaded = h2.model_names() @@ -467,14 +467,14 @@ def test_model_name_load_no_gt_model(history_uninitialized: History): def test_model_name_load_single_with_pop(history_uninitialized: History): h = history_uninitialized - model_names = ["m1"] - h.store_initial_data(0, {}, {}, {}, model_names, "", "", "") + model_names = ['m1'] + h.store_initial_data(0, {}, {}, {}, model_names, '', '', '') particle_list = [ Particle( m=0, - parameter=Parameter({"a": 23, "b": 12}), + parameter=Parameter({'a': 23, 'b': 12}), weight=1.0, - sum_stat={"ss": 0.1}, + sum_stat={'ss': 0.1}, distance=0.1, ) ] @@ -494,7 +494,7 @@ def test_population_to_df(history: History): 0.23, Population(rand_pop_list(m)), 234, - ["m0", "m1", "m2", "m3"], + ['m0', 'm1', 'm2', 'm3'], ) df = history.get_population_extended(m=0) df_js = sumstat_to_json(df) @@ -502,7 +502,7 @@ def test_population_to_df(history: History): def test_update_after_calibration(history: History): - history.store_initial_data(None, {}, {}, {}, ["m0"], "", "", "") + history.store_initial_data(None, {}, {}, {}, ['m0'], '', '', '') pops = history.get_all_populations() assert 0 == pops[pops['t'] == History.PRE_TIME]['samples'].values time = datetime.datetime.now() @@ -526,18 +526,18 @@ def test_dict_from_and_to_json(): def test_create_db(): # temporary file name - file_ = tempfile.mkstemp(suffix=".db")[1] + file_ = tempfile.mkstemp(suffix='.db')[1] # set up history - pyabc.History("sqlite:///" + file_) + pyabc.History('sqlite:///' + file_) # should work just fine though file mostly empty - pyabc.History("sqlite:///" + file_, create=False) + pyabc.History('sqlite:///' + file_, create=False) # delete file and check we cannot create a History object os.remove(file_) with pytest.raises(ValueError): - pyabc.History("sqlite:///" + file_, create=False) + pyabc.History('sqlite:///' + file_, create=False) def test_dataframe_formats(): @@ -575,20 +575,20 @@ def test_export(): # simple problem def model(p): - return {"y": p["p"] + 0.1 * np.random.normal()} + return {'y': p['p'] + 0.1 * np.random.normal()} - prior = pyabc.Distribution(p=pyabc.RV("uniform", -1, 2)) + prior = pyabc.Distribution(p=pyabc.RV('uniform', -1, 2)) distance = pyabc.PNormDistance() try: # run - db_file = tempfile.mkstemp(suffix=".db")[1] + db_file = tempfile.mkstemp(suffix='.db')[1] abc = pyabc.ABCSMC(model, prior, distance, population_size=100) - abc.new("sqlite:///" + db_file, model({"p": 0})) + abc.new('sqlite:///' + db_file, model({'p': 0})) abc.run(max_nr_populations=3) # export history - for fmt in ["csv", "json", "html", "stata"]: + for fmt in ['csv', 'json', 'html', 'stata']: out_file = tempfile.mkstemp()[1] try: pyabc.storage.export(db_file, out=out_file, out_format=fmt) diff --git a/test_nondeterministic/test_abc_smc_algorithm.py b/test_nondeterministic/test_abc_smc_algorithm.py index fdf381a22..2d096e798 100644 --- a/test_nondeterministic/test_abc_smc_algorithm.py +++ b/test_nondeterministic/test_abc_smc_algorithm.py @@ -53,8 +53,8 @@ def sampler(request): @pytest.fixture def db_path(): - db_file_location = os.path.join(tempfile.gettempdir(), "abc_unittest.db") - db = "sqlite:///" + db_file_location + db_file_location = os.path.join(tempfile.gettempdir(), 'abc_unittest.db') + db = 'sqlite:///' + db_file_location yield db if REMOVE_DB: try: @@ -65,8 +65,8 @@ def db_path(): def test_cookie_jar(db_path, sampler): def make_model(theta): - def model(args): - return {"result": 1 if random.random() > theta else 0} + def model(**kwargs): # noqa: ARG001 + return {'result': 1 if random.random() > theta else 0} return model @@ -82,28 +82,29 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) - abc.new(db_path, {"result": 0}) + abc.new(db_path, {'result': 0}) minimum_epsilon = 0.2 history = abc.run(minimum_epsilon, max_nr_populations=1) mp = history.get_model_probabilities(history.max_t) - expected_p1, expected_p2 = theta1 / (theta1 + theta2), theta2 / ( - theta1 + theta2 + expected_p1, expected_p2 = ( + theta1 / (theta1 + theta2), + theta2 / (theta1 + theta2), ) assert abs(mp.p[0] - expected_p1) + abs(mp.p[1] - expected_p2) < 0.05 def test_empty_population(db_path, sampler): def make_model(theta): - def model(args): - return {"result": 1 if random.random() > theta else 0} + def model(**kwargs): # noqa: ARG001 + return {'result': 1 if random.random() > theta else 0} return model @@ -118,19 +119,20 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0), sampler=sampler, ) - abc.new(db_path, {"result": 0}) + abc.new(db_path, {'result': 0}) minimum_epsilon = -1 history = abc.run(minimum_epsilon, max_nr_populations=3) mp = history.get_model_probabilities(history.max_t) - expected_p1, expected_p2 = theta1 / (theta1 + theta2), theta2 / ( - theta1 + theta2 + expected_p1, expected_p2 = ( + theta1 / (theta1 + theta2), + theta2 / (theta1 + theta2), ) assert abs(mp.p[0] - expected_p1) + abs(mp.p[1] - expected_p2) < 0.05 @@ -139,7 +141,7 @@ def test_beta_binomial_two_identical_models(db_path, sampler): binomial_n = 5 def model_fun(args): - return {"result": st.binom(binomial_n, args.theta).rvs()} + return {'result': st.binom(binomial_n, args.theta).rvs()} models = [model_fun for _ in range(2)] models = list(map(FunctionModel, models)) @@ -150,12 +152,12 @@ def model_fun(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) - abc.new(db_path, {"result": 2}) + abc.new(db_path, {'result': 2}) minimum_epsilon = 0.2 history = abc.run(minimum_epsilon, max_nr_populations=3) @@ -164,19 +166,10 @@ def model_fun(args): class AllInOneModel(Model): - def summary_statistics(self, t, pars, sum_stat_calculator) -> ModelResult: - return ModelResult(sum_stat={"result": 1}) - - def accept( - self, - t, - pars, - sum_stat_calculator, - distance_calculator, - eps_calculator, - acceptor, - x_0, - ) -> ModelResult: + def summary_statistics(self, t, pars, sum_stat_calculator) -> ModelResult: # noqa: ARG002 + return ModelResult(sum_stat={'result': 1}) + + def accept(self, **kwargs) -> ModelResult: # noqa: ARG002 return ModelResult(accepted=True) @@ -184,17 +177,17 @@ def test_all_in_one_model(db_path, sampler): models = [AllInOneModel() for _ in range(2)] population_size = ConstantPopulationSize(800) parameter_given_model_prior_distribution = [ - Distribution(theta=RV("beta", 1, 1)) for _ in range(2) + Distribution(theta=RV('beta', 1, 1)) for _ in range(2) ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) - abc.new(db_path, {"result": 2}) + abc.new(db_path, {'result': 2}) minimum_epsilon = 0.2 history = abc.run(minimum_epsilon, max_nr_populations=3) @@ -206,7 +199,7 @@ def test_beta_binomial_different_priors(db_path, sampler): binomial_n = 5 def model(args): - return {"result": st.binom(binomial_n, args['theta']).rvs()} + return {'result': st.binom(binomial_n, args['theta']).rvs()} models = [model for _ in range(2)] models = list(map(FunctionModel, models)) @@ -214,19 +207,19 @@ def model(args): a1, b1 = 1, 1 a2, b2 = 10, 1 parameter_given_model_prior_distribution = [ - Distribution(theta=RV("beta", a1, b1)), - Distribution(theta=RV("beta", a2, b2)), + Distribution(theta=RV('beta', a1, b1)), + Distribution(theta=RV('beta', a2, b2)), ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) n1 = 2 - abc.new(db_path, {"result": n1}) + abc.new(db_path, {'result': n1}) minimum_epsilon = 0.2 history = abc.run(minimum_epsilon, max_nr_populations=3) @@ -255,7 +248,7 @@ def test_beta_binomial_different_priors_initial_epsilon_from_sample( binomial_n = 5 def model(args): - return {"result": st.binom(binomial_n, args.theta).rvs()} + return {'result': st.binom(binomial_n, args.theta).rvs()} models = [model for _ in range(2)] models = list(map(FunctionModel, models)) @@ -263,19 +256,19 @@ def model(args): a1, b1 = 1, 1 a2, b2 = 10, 1 parameter_given_model_prior_distribution = [ - Distribution(theta=RV("beta", a1, b1)), - Distribution(theta=RV("beta", a2, b2)), + Distribution(theta=RV('beta', a1, b1)), + Distribution(theta=RV('beta', a2, b2)), ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(median_multiplier=0.9), sampler=sampler, ) n1 = 2 - abc.new(db_path, {"result": n1}) + abc.new(db_path, {'result': n1}) minimum_epsilon = -1 history = abc.run(minimum_epsilon, max_nr_populations=5) @@ -301,30 +294,30 @@ def expected_p(a, b, n1): def test_continuous_non_gaussian(db_path, sampler): def model(args): - return {"result": np.random.rand() * args['u']} + return {'result': np.random.rand() * args['u']} models = [model] models = list(map(FunctionModel, models)) population_size = ConstantPopulationSize(250) parameter_given_model_prior_distribution = [ - Distribution(u=RV("uniform", 0, 1)) + Distribution(u=RV('uniform', 0, 1)) ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.2), sampler=sampler, ) d_observed = 0.5 - abc.new(db_path, {"result": d_observed}) + abc.new(db_path, {'result': d_observed}) abc.do_not_stop_when_only_single_model_alive() minimum_epsilon = -1 history = abc.run(minimum_epsilon, max_nr_populations=2) posterior_x, posterior_weight = history.get_distribution(0, None) - posterior_x = posterior_x["u"].values + posterior_x = posterior_x['u'].values sort_indices = np.argsort(posterior_x) f_empirical = np.interpolate.interp1d( np.hstack((-200, posterior_x[sort_indices], 200)), @@ -358,31 +351,31 @@ def test_gaussian_single_population(db_path, sampler): observed_data = 1 def model(args): - return {"y": st.norm(args['x'], sigma_ground_truth).rvs()} + return {'y': st.norm(args['x'], sigma_ground_truth).rvs()} models = [model] models = list(map(FunctionModel, models)) nr_populations = 1 population_size = ConstantPopulationSize(600) parameter_given_model_prior_distribution = [ - Distribution(x=RV("norm", 0, sigma_prior)) + Distribution(x=RV('norm', 0, sigma_prior)) ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) - abc.new(db_path, {"y": observed_data}) + abc.new(db_path, {'y': observed_data}) minimum_epsilon = -1 abc.do_not_stop_when_only_single_model_alive() history = abc.run(minimum_epsilon, max_nr_populations=nr_populations) posterior_x, posterior_weight = history.get_distribution(0, None) - posterior_x = posterior_x["x"].values + posterior_x = posterior_x['x'].values sort_indices = np.argsort(posterior_x) f_empirical = sp.interpolate.interp1d( np.hstack((-200, posterior_x[sort_indices], 200)), @@ -392,9 +385,7 @@ def model(args): sigma_x_given_y = 1 / np.sqrt( 1 / sigma_prior**2 + 1 / sigma_ground_truth**2 ) - mu_x_given_y = ( - sigma_x_given_y**2 * observed_data / sigma_ground_truth**2 - ) + mu_x_given_y = sigma_x_given_y**2 * observed_data / sigma_ground_truth**2 expected_posterior_x = st.norm(mu_x_given_y, sigma_x_given_y) x = np.linspace(-8, 8) max_distribution_difference = np.absolute( @@ -413,7 +404,7 @@ def test_gaussian_multiple_populations(db_path, sampler): y_observed = 2 def model(args): - return {"y": st.norm(args['x'], sigma_y).rvs()} + return {'y': st.norm(args['x'], sigma_y).rvs()} models = [model] models = list(map(FunctionModel, models)) @@ -425,20 +416,20 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, eps=MedianEpsilon(0.2), sampler=sampler, ) - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) minimum_epsilon = -1 abc.do_not_stop_when_only_single_model_alive() history = abc.run(minimum_epsilon, max_nr_populations=nr_populations) posterior_x, posterior_weight = history.get_distribution(0, None) - posterior_x = posterior_x["x"].values + posterior_x = posterior_x['x'].values sort_indices = np.argsort(posterior_x) f_empirical = sp.interpolate.interp1d( np.hstack((-200, posterior_x[sort_indices], 200)), @@ -465,7 +456,7 @@ def test_gaussian_multiple_populations_crossval_kde(db_path, sampler): y_observed = 2 def model(args): - return {"y": st.norm(args['x'], sigma_y).rvs()} + return {'y': st.norm(args['x'], sigma_y).rvs()} models = [model] models = list(map(FunctionModel, models)) @@ -477,26 +468,26 @@ def model(args): parameter_perturbation_kernels = [ GridSearchCV( MultivariateNormalTransition(), - {"scaling": np.logspace(-1, 1.5, 5)}, + {'scaling': np.logspace(-1, 1.5, 5)}, ) ] abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, transitions=parameter_perturbation_kernels, eps=MedianEpsilon(0.2), sampler=sampler, ) - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) minimum_epsilon = -1 abc.do_not_stop_when_only_single_model_alive() history = abc.run(minimum_epsilon, max_nr_populations=nr_populations) posterior_x, posterior_weight = history.get_distribution(0, None) - posterior_x = posterior_x["x"].values + posterior_x = posterior_x['x'].values sort_indices = np.argsort(posterior_x) f_empirical = sp.interpolate.interp1d( np.hstack((-200, posterior_x[sort_indices], 200)), @@ -525,7 +516,7 @@ def test_two_competing_gaussians_single_population( y_observed = 1 def model(args): - return {"y": st.norm(args['x'], sigma_y).rvs()} + return {'y': st.norm(args['x'], sigma_y).rvs()} models = [model, model] models = list(map(FunctionModel, models)) @@ -538,13 +529,13 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, transitions=[transition(), transition()], eps=MedianEpsilon(0.02), sampler=sampler, ) - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) minimum_epsilon = -1 nr_populations = 1 @@ -576,7 +567,7 @@ def test_two_competing_gaussians_multiple_population( sigma = 0.5 def model(args): - return {"y": st.norm(args['x'], sigma).rvs()} + return {'y': st.norm(args['x'], sigma).rvs()} # We define two models, but they are identical so far models = [model, model] @@ -596,7 +587,7 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - PercentileDistance(measures_to_use=["y"]), + PercentileDistance(measures_to_use=['y']), population_size, eps=MedianEpsilon(0.2), transitions=[transition(), transition()], @@ -606,7 +597,7 @@ def model(args): # Finally we add meta data such as model names and define where to store the results # y_observed is the important piece here: our actual observation. y_observed = 1 - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) # We run the ABC with 3 populations max minimum_epsilon = 0.05 @@ -634,8 +625,8 @@ def p_y_given_model(mu_x_model): def test_empty_population_adaptive(db_path, sampler): def make_model(theta): - def model(args): - return {"result": 1 if random.random() > theta else 0} + def model(**kwargs): # noqa: ARG001 + return {'result': 1 if random.random() > theta else 0} return model @@ -650,19 +641,20 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0), sampler=sampler, ) - abc.new(db_path, {"result": 0}) + abc.new(db_path, {'result': 0}) minimum_epsilon = -1 history = abc.run(minimum_epsilon, max_nr_populations=3) mp = history.get_model_probabilities(history.max_t) - expected_p1, expected_p2 = theta1 / (theta1 + theta2), theta2 / ( - theta1 + theta2 + expected_p1, expected_p2 = ( + theta1 / (theta1 + theta2), + theta2 / (theta1 + theta2), ) assert abs(mp.p[0] - expected_p1) + abs(mp.p[1] - expected_p2) < 0.1 @@ -671,7 +663,7 @@ def test_beta_binomial_two_identical_models_adaptive(db_path, sampler): binomial_n = 5 def model_fun(args): - return {"result": st.binom(binomial_n, args.theta).rvs()} + return {'result': st.binom(binomial_n, args.theta).rvs()} models = [model_fun for _ in range(2)] models = list(map(FunctionModel, models)) @@ -682,12 +674,12 @@ def model_fun(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["result"]), + MinMaxDistance(measures_to_use=['result']), population_size, eps=MedianEpsilon(0.1), sampler=sampler, ) - abc.new(db_path, {"result": 2}) + abc.new(db_path, {'result': 2}) minimum_epsilon = 0.2 history = abc.run(minimum_epsilon, max_nr_populations=3) @@ -703,7 +695,7 @@ def test_gaussian_multiple_populations_adpative_population_size( y_observed = 2 def model(args): - return {"y": st.norm(args['x'], sigma_y).rvs()} + return {'y': st.norm(args['x'], sigma_y).rvs()} models = [model] models = list(map(FunctionModel, models)) @@ -715,19 +707,19 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, eps=MedianEpsilon(0.2), sampler=sampler, ) - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) minimum_epsilon = -1 abc.do_not_stop_when_only_single_model_alive() history = abc.run(minimum_epsilon, max_nr_populations=nr_populations) posterior_x, posterior_weight = history.get_distribution(0, None) - posterior_x = posterior_x["x"].values + posterior_x = posterior_x['x'].values sort_indices = np.argsort(posterior_x) f_empirical = sp.interpolate.interp1d( np.hstack((-200, posterior_x[sort_indices], 200)), @@ -755,14 +747,14 @@ def test_two_competing_gaussians_multiple_population_adaptive_populatin_size( sigma = 0.5 def model(args): - return {"y": st.norm(args['x'], sigma).rvs()} + return {'y': st.norm(args['x'], sigma).rvs()} # We define two models, but they are identical so far models = [model, model] models = list(map(FunctionModel, models)) # The prior over the model classes is uniform - model_prior = RV("randint", 0, 2) + model_prior = RV('randint', 0, 2) # However, our models' priors are not the same. Their mean differs. mu_x_1, mu_x_2 = 0, 1 @@ -771,11 +763,6 @@ def model(args): Distribution(x=st.norm(mu_x_2, sigma)), ] - # Particles are perturbed in a Gaussian fashion - parameter_perturbation_kernels = [ - MultivariateNormalTransition() for _ in range(2) - ] - # We plug all the ABC setup together nr_populations = 3 population_size = AdaptivePopulationSize( @@ -784,7 +771,7 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - MinMaxDistance(measures_to_use=["y"]), + MinMaxDistance(measures_to_use=['y']), population_size, model_prior=model_prior, eps=MedianEpsilon(0.2), @@ -794,7 +781,7 @@ def model(args): # Finally we add meta data such as model names and define where to store the results # y_observed is the important piece here: our actual observation. y_observed = 1 - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) # We run the ABC with 3 populations max minimum_epsilon = 0.05 diff --git a/test_performance/test_samplerperf.py b/test_performance/test_samplerperf.py index 72dbb798d..2c66a0e08 100644 --- a/test_performance/test_samplerperf.py +++ b/test_performance/test_samplerperf.py @@ -38,7 +38,7 @@ def multi_proc_map(f, x): class GenericFutureWithProcessPool(ConcurrentFutureSampler): - def __init__(self, map=None): + def __init__(self): cfuture_executor = ProcessPoolExecutor(max_workers=8) client_core_load_factor = 1.0 client_max_jobs = 8 @@ -52,7 +52,7 @@ def __init__(self, map=None): class GenericFutureWithThreadPool(ConcurrentFutureSampler): - def __init__(self, map=None): + def __init__(self): cfuture_executor = ThreadPoolExecutor(max_workers=8) client_core_load_factor = 1.0 client_max_jobs = 8 @@ -66,18 +66,18 @@ def __init__(self, map=None): class MultiProcessingMappingSampler(MappingSampler): - def __init__(self, map=None): + def __init__(self): super().__init__(multi_proc_map) class DaskDistributedSamplerBatch(DaskDistributedSampler): - def __init__(self, map=None): + def __init__(self): batch_size = 10 super().__init__(batch_size=batch_size) class GenericFutureWithProcessPoolBatch(ConcurrentFutureSampler): - def __init__(self, map=None): + def __init__(self): cfuture_executor = ProcessPoolExecutor(max_workers=8) client_max_jobs = 8 batch_size = 10 @@ -103,8 +103,8 @@ def sampler(request): @pytest.fixture def db_path(): - db_file_location = os.path.join(tempfile.gettempdir(), "abc_unittest.db") - db = "sqlite:///" + db_file_location + db_file_location = os.path.join(tempfile.gettempdir(), 'abc_unittest.db') + db = 'sqlite:///' + db_file_location yield db if REMOVE_DB: try: @@ -119,7 +119,7 @@ def test_two_competing_gaussians_multiple_population(db_path, sampler): sigma = 0.5 def model(args): - return {"y": st.norm(args['x'], sigma).rvs()} + return {'y': st.norm(args['x'], sigma).rvs()} # We define two models, but they are identical so far models = [model, model] @@ -138,7 +138,7 @@ def model(args): abc = ABCSMC( models, parameter_given_model_prior_distribution, - PercentileDistance(measures_to_use=["y"]), + PercentileDistance(measures_to_use=['y']), population_size, eps=MedianEpsilon(0.2), sampler=sampler, @@ -148,7 +148,7 @@ def model(args): # define where to store the results # y_observed is the important piece here: our actual observation. y_observed = 2 - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) # We run the ABC with 3 populations max minimum_epsilon = 0.05 @@ -160,9 +160,7 @@ def model(args): history.get_model_probabilities(history.max_t) def p_y_given_model(mu_x_model): - res = st.norm(mu_x_model, np.sqrt(sigma**2 + sigma**2)).pdf( - y_observed - ) + res = st.norm(mu_x_model, np.sqrt(sigma**2 + sigma**2)).pdf(y_observed) return res p1_expected_unnormalized = p_y_given_model(mu_x_1) From fb83056077e58f94ac5bd57926e9f31b434fd8c6 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 13:40:39 +0100 Subject: [PATCH 07/55] add ruff --- doc/examples/custom_priors.ipynb | 38 ++++++++----- doc/examples/discrete_parameters.ipynb | 13 +++-- doc/examples/external_simulators.ipynb | 34 ++++++------ doc/examples/noise.ipynb | 54 +++++++++---------- doc/examples/optimal_threshold.ipynb | 48 ++++++++--------- pyabc/external/r/r_rpy2.py | 14 ++--- pyabc/parameters/parameters.py | 14 ++--- pyabc/sampler/mapping.py | 8 +-- .../sampler/multicore_evaluation_parallel.py | 6 +-- pyabc/sampler/redis_eps/util.py | 2 +- pyabc/sampler/singlecore.py | 6 +-- pyabc/sge/job_info_redis.py | 43 +++++++-------- pyabc/sge/sge.py | 53 +++++++++--------- pyabc/storage/dataframe_bytes_storage.py | 22 ++++---- pyabc/visualization/histogram.py | 12 ++--- pyabc/visualization/kde.py | 37 +++++++------ test/external/test_pyjulia.py | 36 ++++++------- 17 files changed, 219 insertions(+), 221 deletions(-) diff --git a/doc/examples/custom_priors.ipynb b/doc/examples/custom_priors.ipynb index 7bbe72c8a..7297dd0ff 100644 --- a/doc/examples/custom_priors.ipynb +++ b/doc/examples/custom_priors.ipynb @@ -33,7 +33,17 @@ "source": [ "import numpy as np\n", "\n", - "from pyabc import *\n", + "from pyabc import (\n", + " ABCSMC,\n", + " RV,\n", + " Distribution,\n", + " DistributionBase,\n", + " GridSearchCV,\n", + " Parameter,\n", + " PNormDistance,\n", + " create_sqlite_db_id,\n", + " visualization,\n", + ")\n", "\n", "rng = np.random.default_rng()" ] @@ -60,8 +70,8 @@ "def model(p):\n", " \"\"\"Quadratic model with two in- and outputs.\"\"\"\n", " return {\n", - " \"y0\": p[\"p0\"] ** 2 + std * rng.normal(),\n", - " \"y1\": p[\"p1\"] ** 2 + std * rng.normal(),\n", + " 'y0': p['p0'] ** 2 + std * rng.normal(),\n", + " 'y1': p['p1'] ** 2 + std * rng.normal(),\n", " }\n", "\n", "\n", @@ -69,7 +79,7 @@ "distance = PNormDistance(p=2)\n", "\n", "# ground truth parameters\n", - "gt_par = {\"p0\": 1, \"p1\": 1.2}\n", + "gt_par = {'p0': 1, 'p1': 1.2}\n", "\n", "# observed data\n", "obs = model(gt_par)\n", @@ -79,7 +89,7 @@ "max_total_sim = 50 * pop_size\n", "\n", "# parameter boundaries\n", - "prior_bounds = {\"p0\": (-2, 2), \"p1\": (-2, 2)}" + "prior_bounds = {'p0': (-2, 2), 'p1': (-2, 2)}" ] }, { @@ -98,8 +108,8 @@ "outputs": [], "source": [ "prior = Distribution(\n", - " p0=RV(\"uniform\", -2, 4),\n", - " p1=RV(\"uniform\", -2, 4),\n", + " p0=RV('uniform', -2, 4),\n", + " p1=RV('uniform', -2, 4),\n", ")" ] }, @@ -210,25 +220,25 @@ }, { "cell_type": "code", - "execution_count": 6, - "id": "7c5152cc-a39a-4db2-b80b-7aa3d99ea96d", + "execution_count": null, + "id": "59115ab2565324b4", "metadata": {}, "outputs": [], "source": [ "class ConstrainedPrior(DistributionBase):\n", " def __init__(self):\n", - " self.p0 = RV(\"uniform\", -2, 4)\n", - " self.p1 = RV(\"uniform\", -2, 4)\n", + " self.p0 = RV('uniform', -2, 4)\n", + " self.p1 = RV('uniform', -2, 4)\n", " self.min_sum: float = 1.0\n", "\n", - " def rvs(self, *args, **kwargs):\n", + " def rvs(self, *args, **kwargs): # noqa: ARG002\n", " while True:\n", " p0, p1 = self.p0.rvs(), self.p1.rvs()\n", " if p0 + p1 > self.min_sum:\n", " return Parameter(p0=p0, p1=p1)\n", "\n", " def pdf(self, x):\n", - " p0, p1 = x[\"p0\"], x[\"p1\"]\n", + " p0, p1 = x['p0'], x['p1']\n", " if p0 + p1 <= self.min_sum:\n", " return 0.0\n", " return self.p0.pdf(p0) * self.p1.pdf(p1)\n", @@ -336,7 +346,7 @@ " refval=gt_par,\n", " kde=GridSearchCV(),\n", ")\n", - "arr_ax[0][1].plot([-2, 2], [3, -1], linestyle=\"dotted\", color=\"grey\")" + "arr_ax[0][1].plot([-2, 2], [3, -1], linestyle='dotted', color='grey')" ] } ], diff --git a/doc/examples/discrete_parameters.ipynb b/doc/examples/discrete_parameters.ipynb index 644236bc3..a794a3e63 100644 --- a/doc/examples/discrete_parameters.ipynb +++ b/doc/examples/discrete_parameters.ipynb @@ -78,7 +78,10 @@ " }\n", "\n", "\n", - "distance = lambda x, x0: sum((x['y'] - x0['y']) ** 2)\n", + "def distance(x, x0):\n", + " return sum((x['y'] - x0['y']) ** 2)\n", + "\n", + "\n", "p_true = {'p_discrete': 2, 'p_continuous': 0.5}\n", "obs = model(p_true)" ] @@ -104,9 +107,9 @@ "source": [ "# plot the data\n", "ax = plt.hist(obs['y'])\n", - "plt.xlabel(\"Observable y_i\")\n", - "plt.ylabel(\"Frequency\")\n", - "plt.title(\"Data\")\n", + "plt.xlabel('Observable y_i')\n", + "plt.ylabel('Frequency')\n", + "plt.title('Data')\n", "plt.show()" ] }, @@ -238,7 +241,7 @@ " align='left',\n", " ax=axes[t, 1],\n", " )\n", - " axes[t, 0].set_ylabel(f\"Posterior t={t}\")\n", + " axes[t, 0].set_ylabel(f'Posterior t={t}')\n", "fig.tight_layout()" ] } diff --git a/doc/examples/external_simulators.ipynb b/doc/examples/external_simulators.ipynb index 9ddba0769..83adb0cf1 100644 --- a/doc/examples/external_simulators.ipynb +++ b/doc/examples/external_simulators.ipynb @@ -39,8 +39,13 @@ "metadata": {}, "outputs": [], "source": [ + "import os\n", + "from tempfile import gettempdir\n", + "\n", "import pyabc\n", - "import pyabc.external" + "import pyabc.external\n", + "from pyabc import ABCSMC, RV, Distribution\n", + "from pyabc.visualization import plot_kde_2d" ] }, { @@ -56,16 +61,16 @@ "metadata": {}, "outputs": [], "source": [ - "dir = \"model_r/\"\n", + "dir = 'model_r/'\n", "\n", "model = pyabc.external.ExternalModel(\n", - " executable=\"Rscript\", file=f\"{dir}/model.r\"\n", + " executable='Rscript', file=f'{dir}/model.r'\n", ")\n", "sumstat = pyabc.external.ExternalSumStat(\n", - " executable=\"Rscript\", file=f\"{dir}/sumstat.r\"\n", + " executable='Rscript', file=f'{dir}/sumstat.r'\n", ")\n", "distance = pyabc.external.ExternalDistance(\n", - " executable=\"Rscript\", file=f\"{dir}/distance.r\"\n", + " executable='Rscript', file=f'{dir}/distance.r'\n", ")\n", "\n", "dummy_sumstat = (\n", @@ -141,17 +146,12 @@ } ], "source": [ - "from pyabc import ABCSMC, RV, Distribution\n", - "\n", - "prior = Distribution(meanX=RV(\"uniform\", 0, 10), meanY=RV(\"uniform\", 0, 10))\n", + "prior = Distribution(meanX=RV('uniform', 0, 10), meanY=RV('uniform', 0, 10))\n", "abc = ABCSMC(\n", " model, prior, distance, summary_statistics=sumstat, population_size=20\n", ")\n", "\n", - "import os\n", - "from tempfile import gettempdir\n", - "\n", - "db = \"sqlite:///\" + os.path.join(gettempdir(), \"test.db\")\n", + "db = 'sqlite:///' + os.path.join(gettempdir(), 'test.db')\n", "abc.new(db, dummy_sumstat)\n", "\n", "history = abc.run(minimum_epsilon=0.9, max_nr_populations=4)" @@ -219,15 +219,13 @@ } ], "source": [ - "from pyabc.visualization import plot_kde_2d\n", - "\n", "for t in range(history.n_populations):\n", " df, w = abc.history.get_distribution(0, t)\n", " ax = plot_kde_2d(\n", " df,\n", " w,\n", - " \"meanX\",\n", - " \"meanY\",\n", + " 'meanX',\n", + " 'meanY',\n", " xmin=0,\n", " xmax=10,\n", " ymin=0,\n", @@ -236,10 +234,10 @@ " numy=100,\n", " )\n", " ax.scatter(\n", - " [4], [8], edgecolor=\"black\", facecolor=\"white\", label=\"Observation\"\n", + " [4], [8], edgecolor='black', facecolor='white', label='Observation'\n", " )\n", " ax.legend()\n", - " ax.set_title(f\"PDF t={t}\")" + " ax.set_title(f'PDF t={t}')" ] } ], diff --git a/doc/examples/noise.ipynb b/doc/examples/noise.ipynb index 57ecc7381..d753e8dee 100644 --- a/doc/examples/noise.ipynb +++ b/doc/examples/noise.ipynb @@ -45,7 +45,7 @@ "measurement_times = np.linspace(0, 10, n_time)\n", "\n", "\n", - "def f(y, t0, theta1, theta2=0.12):\n", + "def f(y, t0, theta1, theta2=0.12): # noqa: ARG001\n", " \"\"\"ODE right-hand side.\"\"\"\n", " x1, x2 = y\n", " dx1 = -theta1 * x1 + theta2 * x2\n", @@ -55,7 +55,7 @@ "\n", "def model(p: dict):\n", " \"\"\"ODE model.\"\"\"\n", - " sol = sp.integrate.odeint(f, init, measurement_times, args=(p[\"theta1\"],))\n", + " sol = sp.integrate.odeint(f, init, measurement_times, args=(p['theta1'],))\n", " return {'X_2': sol[:, 1]}\n", "\n", "\n", @@ -66,7 +66,7 @@ "theta1_min, theta1_max = 0.05, 0.12\n", "theta_lims = {'theta1': (theta1_min, theta1_max)}\n", "prior = pyabc.Distribution(\n", - " theta1=pyabc.RV(\"uniform\", theta1_min, theta1_max - theta1_min)\n", + " theta1=pyabc.RV('uniform', theta1_min, theta1_max - theta1_min)\n", ")\n", "\n", "# true noise-free data\n", @@ -137,9 +137,9 @@ "\n", "# plot data\n", "plt.plot(\n", - " measurement_times, true_trajectory['X_2'], color=\"C0\", label='Simulation'\n", + " measurement_times, true_trajectory['X_2'], color='C0', label='Simulation'\n", ")\n", - "plt.scatter(measurement_times, measured_data['X_2'], color=\"C1\", label='Data')\n", + "plt.scatter(measurement_times, measured_data['X_2'], color='C1', label='Data')\n", "plt.xlabel('Time $t$')\n", "plt.ylabel('Measurement $Y$')\n", "plt.title('Conversion reaction: True parameters fit')\n", @@ -247,7 +247,7 @@ "source": [ "def distance(simulation, data):\n", " \"\"\"Here we use an l2 distance.\"\"\"\n", - " return np.sum((data[\"X_2\"] - simulation[\"X_2\"]) ** 2)\n", + " return np.sum((data['X_2'] - simulation['X_2']) ** 2)\n", "\n", "\n", "abc = pyabc.ABCSMC(model, prior, distance, population_size=pop_size)\n", @@ -285,7 +285,7 @@ "for t in range(history_ignore.max_t + 1):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " history_ignore,\n", - " x=\"theta1\",\n", + " x='theta1',\n", " t=t,\n", " refval=theta_true,\n", " refval_color='grey',\n", @@ -293,9 +293,9 @@ " xmax=theta1_max,\n", " numx=200,\n", " ax=ax,\n", - " label=f\"Generation {t}\",\n", + " label=f'Generation {t}',\n", " )\n", - "ax.plot(xs, true_fvals, color='black', linestyle='--', label=\"True\")\n", + "ax.plot(xs, true_fvals, color='black', linestyle='--', label='True')\n", "ax.legend()\n", "plt.show()" ] @@ -382,7 +382,7 @@ "for t in range(history_noisy_output.max_t + 1):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " history_noisy_output,\n", - " x=\"theta1\",\n", + " x='theta1',\n", " t=t,\n", " refval=theta_true,\n", " refval_color='grey',\n", @@ -390,9 +390,9 @@ " xmax=theta1_max,\n", " ax=ax,\n", " numx=200,\n", - " label=f\"Generation {t}\",\n", + " label=f'Generation {t}',\n", " )\n", - "ax.plot(xs, true_fvals, color='black', linestyle='--', label=\"True\")\n", + "ax.plot(xs, true_fvals, color='black', linestyle='--', label='True')\n", "ax.legend()" ] }, @@ -487,7 +487,7 @@ "for t in range(history_acceptor.max_t + 1):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " history_acceptor,\n", - " x=\"theta1\",\n", + " x='theta1',\n", " t=t,\n", " refval=theta_true,\n", " refval_color='grey',\n", @@ -495,9 +495,9 @@ " xmax=theta1_max,\n", " ax=ax,\n", " numx=200,\n", - " label=f\"Generation {t}\",\n", + " label=f'Generation {t}',\n", " )\n", - "ax.plot(xs, true_fvals, color='black', linestyle='--', label=\"True\")\n", + "ax.plot(xs, true_fvals, color='black', linestyle='--', label='True')\n", "ax.legend()\n", "plt.show()" ] @@ -529,7 +529,7 @@ ], "source": [ "histories = [history_noisy_output, history_acceptor]\n", - "labels = [\"noisy model\", \"stochastic acceptor\"]\n", + "labels = ['noisy model', 'stochastic acceptor']\n", "\n", "pyabc.visualization.plot_sample_numbers(histories, labels)\n", "plt.show()" @@ -568,8 +568,8 @@ "}\n", "\n", "prior = pyabc.Distribution(\n", - " theta1=pyabc.RV(\"uniform\", theta1_min, theta1_max - theta1_min),\n", - " std=pyabc.RV(\"uniform\", std_min, std_max - std_min),\n", + " theta1=pyabc.RV('uniform', theta1_min, theta1_max - theta1_min),\n", + " std=pyabc.RV('uniform', std_min, std_max - std_min),\n", ")" ] }, @@ -621,8 +621,8 @@ " posterior_normalization = sp.integrate.dblquad(\n", " lambda std, theta1: posterior_unscaled({'theta1': theta1, 'std': std}),\n", " *theta_lims_var['theta1'],\n", - " lambda theta1: std_min,\n", - " lambda theta1: std_max,\n", + " lambda _: std_min,\n", + " lambda _: std_max,\n", " epsabs=1e-4,\n", " )[0]\n", " print(posterior_normalization)\n", @@ -762,7 +762,7 @@ "for t in range(history_acceptor_var.max_t + 1):\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " history_acceptor_var,\n", - " x=\"theta1\",\n", + " x='theta1',\n", " t=t,\n", " refval=theta_true_var,\n", " refval_color='grey',\n", @@ -770,11 +770,11 @@ " xmax=theta1_max,\n", " ax=ax[0],\n", " numx=200,\n", - " label=f\"Generation {t}\",\n", + " label=f'Generation {t}',\n", " )\n", " pyabc.visualization.plot_kde_1d_highlevel(\n", " history_acceptor_var,\n", - " x=\"std\",\n", + " x='std',\n", " t=t,\n", " refval=theta_true_var,\n", " refval_color='grey',\n", @@ -782,13 +782,13 @@ " xmax=std_max,\n", " ax=ax[1],\n", " numx=200,\n", - " label=f\"Generation {t}\",\n", + " label=f'Generation {t}',\n", " )\n", - "ax[1].set_xlabel(\"log10(std)\")\n", + "ax[1].set_xlabel('log10(std)')\n", "ax[1].set_ylabel(None)\n", "\n", - "ax[0].plot(theta1s, vals_theta1, color='black', linestyle='--', label=\"True\")\n", - "ax[1].plot(stds, vals_std, color='black', linestyle='--', label=\"True\")\n", + "ax[0].plot(theta1s, vals_theta1, color='black', linestyle='--', label='True')\n", + "ax[1].plot(stds, vals_std, color='black', linestyle='--', label='True')\n", "\n", "ax[1].legend()\n", "fig.set_size_inches((8, 4))\n", diff --git a/doc/examples/optimal_threshold.ipynb b/doc/examples/optimal_threshold.ipynb index be11630b6..8412dbf07 100644 --- a/doc/examples/optimal_threshold.ipynb +++ b/doc/examples/optimal_threshold.ipynb @@ -43,11 +43,11 @@ "\n", "import pyabc\n", "\n", - "pyabc.settings.set_figure_params(\"pyabc\") # for beautified plots\n", + "pyabc.settings.set_figure_params('pyabc') # for beautified plots\n", "\n", "# debug\n", - "for debugger in [\"ABC.Distance\", \"ABC.Epsilon\"]:\n", - " logging.getLogger(\"ABC.Distance\").setLevel(logging.DEBUG)" + "for debugger in ['ABC.Distance', 'ABC.Epsilon']:\n", + " logging.getLogger(debugger).setLevel(logging.DEBUG)" ] }, { @@ -108,17 +108,17 @@ ], "source": [ "def model(p):\n", - " theta = p[\"theta\"]\n", - " return {\"y\": (theta - 10) ** 2 - 100 * np.exp(-100 * (theta - 3) ** 2)}\n", + " theta = p['theta']\n", + " return {'y': (theta - 10) ** 2 - 100 * np.exp(-100 * (theta - 3) ** 2)}\n", "\n", "\n", - "p_true = {\"theta\": 3}\n", + "p_true = {'theta': 3}\n", "y_obs = model(p_true)\n", "\n", - "bounds = {\"theta\": (0, 20)}\n", + "bounds = {'theta': (0, 20)}\n", "prior = pyabc.Distribution(\n", " **{\n", - " key: pyabc.RV(\"uniform\", lb, ub - lb)\n", + " key: pyabc.RV('uniform', lb, ub - lb)\n", " for key, (lb, ub) in bounds.items()\n", " }\n", ")\n", @@ -126,10 +126,10 @@ "# plot observables over parameters\n", "_, ax = plt.subplots()\n", "xs = np.linspace(0, 20, 1000)\n", - "ys = model({\"theta\": xs})[\"y\"]\n", - "ax.plot(xs, ys, color=\"grey\")\n", - "ax.set_xlabel(r\"$\\theta$\")\n", - "ax.set_ylabel(\"y\");" + "ys = model({'theta': xs})['y']\n", + "ax.plot(xs, ys, color='grey')\n", + "ax.set_xlabel(r'$\\theta$')\n", + "ax.set_ylabel('y');" ] }, { @@ -164,14 +164,14 @@ " pyabc.visualization.plot_kde_1d(\n", " df,\n", " w,\n", - " xmin=bounds[\"theta\"][0],\n", - " xmax=bounds[\"theta\"][1],\n", + " xmin=bounds['theta'][0],\n", + " xmax=bounds['theta'][1],\n", " numx=1000,\n", - " x=\"theta\",\n", + " x='theta',\n", " ax=ax,\n", - " label=f\"t={t}\" if show_history else None,\n", + " label=f't={t}' if show_history else None,\n", " refval=p_true,\n", - " refval_color=\"grey\",\n", + " refval_color='grey',\n", " kde=pyabc.GridSearchCV(),\n", " )\n", "\n", @@ -179,19 +179,19 @@ "def plot_pars_over_data(history):\n", " \"\"\"Plot parameters over data background.\"\"\"\n", " _, ax = plt.subplots(figsize=(10, 6))\n", - " ax.plot(xs, ys - y_obs[\"y\"], color=\"grey\")\n", + " ax.plot(xs, ys - y_obs['y'], color='grey')\n", " for t in range(history.max_t + 1):\n", " df, _ = history.get_distribution(t=t)\n", - " eps = history.get_all_populations().set_index(\"t\").loc[t, \"epsilon\"]\n", + " eps = history.get_all_populations().set_index('t').loc[t, 'epsilon']\n", " ax.plot(\n", - " df[\"theta\"],\n", - " eps * np.ones_like(df[\"theta\"]),\n", - " \".\",\n", + " df['theta'],\n", + " eps * np.ones_like(df['theta']),\n", + " '.',\n", " markersize=1,\n", " color=pyabc.visualization.colors.RED900,\n", " )\n", - " ax.set_xlabel(r\"$\\theta$\")\n", - " ax.set_ylabel(r\"$d(y,y_ {obs})$\")" + " ax.set_xlabel(r'$\\theta$')\n", + " ax.set_ylabel(r'$d(y,y_ {obs})$')" ] }, { diff --git a/pyabc/external/r/r_rpy2.py b/pyabc/external/r/r_rpy2.py index feb5d4070..3ac75fae6 100644 --- a/pyabc/external/r/r_rpy2.py +++ b/pyabc/external/r/r_rpy2.py @@ -8,7 +8,7 @@ from ...parameters import Parameter -logger = logging.getLogger("ABC.External") +logger = logging.getLogger('ABC.External') try: from rpy2.robjects import ( @@ -25,11 +25,7 @@ def _dict_to_named_list(dct): - if ( - isinstance(dct, dict) - or isinstance(dct, Parameter) - or isinstance(dct, pd.core.series.Series) - ): + if isinstance(dct, dict | Parameter | pd.core.series.Series): dct = dict(dct.items()) # convert numbers, numpy arrays and pandas dataframes to builtin # types before conversion (see rpy2 #548) @@ -77,9 +73,9 @@ class R: def __init__(self, source_file: str): if r is None: - raise ImportError("Install rpy2, e.g. via `pip install pyabc[R]`") + raise ImportError('Install rpy2, e.g. via `pip install pyabc[R]`') warnings.warn( - "The support of R via rpy2 is considered experimental.", + 'The support of R via rpy2 is considered experimental.', stacklevel=2, ) self.source_file = source_file @@ -164,7 +160,7 @@ def distance_py(*args): if res.size != 1: raise ValueError( f"R distance function '{function_name}' must return a single " - f"numeric value, but got shape {res.shape} (size={res.size})." + f'numeric value, but got shape {res.shape} (size={res.size}).' ) return float(res.item()) diff --git a/pyabc/parameters/parameters.py b/pyabc/parameters/parameters.py index a0956e1ef..93fcb97f4 100644 --- a/pyabc/parameters/parameters.py +++ b/pyabc/parameters/parameters.py @@ -10,14 +10,14 @@ def flatten_dict(dict_: dict): if isinstance(value, dict): flattened = ParameterStructure.flatten_dict(value) for key_flat, value_flat in flattened.items(): - new_dict.update({str(key) + "." + key_flat: value_flat}) + new_dict.update({str(key) + '.' + key_flat: value_flat}) else: new_dict.update({key: value}) return new_dict def __init__(self, *args, **kwargs): if len(args) > 0 and len(kwargs) > 0: - raise Exception("Only keyword or dictionary allowed") + raise Exception('Only keyword or dictionary allowed') if len(args) > 0: flattened = ParameterStructure.flatten_dict(args[0]) elif len(kwargs) > 0: @@ -54,14 +54,14 @@ class Parameter(ParameterStructure): """ - def __add__(self, other: "Parameter") -> "Parameter": + def __add__(self, other: 'Parameter') -> 'Parameter': return Parameter(**{key: self[key] + other[key] for key in self}) - def __sub__(self, other: "Parameter") -> "Parameter": + def __sub__(self, other: 'Parameter') -> 'Parameter': return Parameter(**{key: self[key] - other[key] for key in self}) def __repr__(self): - return "" + return '' def __getattr__(self, item): """ @@ -70,7 +70,7 @@ def __getattr__(self, item): try: return self[item] except KeyError: - raise AttributeError + raise AttributeError from None def __getstate__(self): return dict(self) @@ -78,7 +78,7 @@ def __getstate__(self): def __setstate__(self, state): self.data = state - def copy(self) -> "Parameter": + def copy(self) -> 'Parameter': """ Copy the parameter. """ diff --git a/pyabc/sampler/mapping.py b/pyabc/sampler/mapping.py index e88215912..826ab3909 100644 --- a/pyabc/sampler/mapping.py +++ b/pyabc/sampler/mapping.py @@ -103,11 +103,11 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, - max_eval=np.inf, - all_accepted=False, - ana_vars=None, + max_eval=np.inf, # noqa: ARG002 + all_accepted=False, # noqa: ARG002 + ana_vars=None, # noqa: ARG002 ): # pickle them as a tuple instead of individual pickling # this should save time and should make better use of diff --git a/pyabc/sampler/multicore_evaluation_parallel.py b/pyabc/sampler/multicore_evaluation_parallel.py index 1e879839b..7d138afb1 100644 --- a/pyabc/sampler/multicore_evaluation_parallel.py +++ b/pyabc/sampler/multicore_evaluation_parallel.py @@ -8,7 +8,7 @@ from .multicorebase import MultiCoreSampler, get_if_worker_healthy -DONE = "Done" +DONE = 'Done' def work( @@ -94,11 +94,11 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, max_eval=np.inf, all_accepted=False, - ana_vars=None, + ana_vars=None, # noqa: ARG002 ): n_eval = Value(c_longlong) n_eval.value = 0 diff --git a/pyabc/sampler/redis_eps/util.py b/pyabc/sampler/redis_eps/util.py index a47b3f214..d8c5a7396 100644 --- a/pyabc/sampler/redis_eps/util.py +++ b/pyabc/sampler/redis_eps/util.py @@ -11,7 +11,7 @@ def __init__(self): signal.signal(signal.SIGTERM, self.handle) signal.signal(signal.SIGINT, self.handle) - def handle(self, *args): + def handle(self, *args): # noqa: ARG002 self.killed = True if self.exit: sys.exit(0) diff --git a/pyabc/sampler/singlecore.py b/pyabc/sampler/singlecore.py index 6320fcc78..e839951ca 100644 --- a/pyabc/sampler/singlecore.py +++ b/pyabc/sampler/singlecore.py @@ -22,11 +22,11 @@ def sample_until_n_accepted( self, n, simulate_one, - t, + t, # noqa: ARG002 *, max_eval=np.inf, - all_accepted=False, - ana_vars=None, + all_accepted=False, # noqa: ARG002 + ana_vars=None, # noqa: ARG002 ): nr_simulations = 0 sample = self._create_empty_sample() diff --git a/pyabc/sge/job_info_redis.py b/pyabc/sge/job_info_redis.py index 65d9b923a..3d9f5663a 100644 --- a/pyabc/sge/job_info_redis.py +++ b/pyabc/sge/job_info_redis.py @@ -1,6 +1,7 @@ """ Show info about the redis job queue """ + import argparse from redis import Redis @@ -9,7 +10,7 @@ # flake8: noqa: T001 def main(): parser = argparse.ArgumentParser() - parser.add_argument("--show-list", action="store_true") + parser.add_argument('--show-list', action='store_true') args = parser.parse_args() r = Redis(decode_responses=True) @@ -18,46 +19,46 @@ def default_dict(job): job_set = set(map(int, r.lrange(job, 0, -1))) nr_jobs_total = len(job_set) return { - "started": 0, - "finished": 0, - "jobs": job_set, - "nr_jobs_total": nr_jobs_total, + 'started': 0, + 'finished': 0, + 'jobs': job_set, + 'nr_jobs_total': nr_jobs_total, } results = {} - for key in r.keys(): - if ":" in key: - job, nr_str = key.split(":") + for key in r: + if ':' in key: + job, nr_str = key.split(':') nr = int(nr_str) - status = r.hget(key, "status") + status = r.hget(key, 'status') if job not in results: results[job] = default_dict(job) results[job][status] += 1 - results[job]["jobs"].remove(nr) + results[job]['jobs'].remove(nr) else: if key not in results: results[key] = default_dict(key) - FMT = "{:<20}{:<10}{:<10}{:<10}{:<10}" - print(FMT.format("Job", "started", "finished", "total", "submitted")) - print("-" * 60) + FMT = '{:<20}{:<10}{:<10}{:<10}{:<10}' + print(FMT.format('Job', 'started', 'finished', 'total', 'submitted')) + print('-' * 60) for job, states in sorted(results.items(), key=lambda x: x[0]): print( FMT.format( job, - states["started"], - states["finished"], - states["started"] + states["finished"], - states["nr_jobs_total"], + states['started'], + states['finished'], + states['started'] + states['finished'], + states['nr_jobs_total'], ) ) if args.show_list: - print("\n\nNeither started nor finished jobs") + print('\n\nNeither started nor finished jobs') for job, state in results.items(): - print("\nJob", job) - print(sorted(state["jobs"])) + print('\nJob', job) + print(sorted(state['jobs'])) -if __name__ == "__main__": +if __name__ == '__main__': main() diff --git a/pyabc/sge/sge.py b/pyabc/sge/sge.py index cadd5e79f..f22e1412f 100644 --- a/pyabc/sge/sge.py +++ b/pyabc/sge/sge.py @@ -16,7 +16,7 @@ from .execution_contexts import DefaultContext from .util import sge_available -logger = logging.getLogger("ABC.SGE") +logger = logging.getLogger('ABC.SGE') class SGESignatureMismatchException(Exception): @@ -126,7 +126,7 @@ def __init__( sge_error_file: str = None, sge_output_file: str = None, parallel_environment=None, - name="map", + name='map', queue=None, priority=None, num_threads: int = 1, @@ -140,25 +140,25 @@ def __init__( self.config = get_config() if parallel_environment is not None: - self.config["SGE"]["PARALLEL_ENVIRONMENT"] = parallel_environment + self.config['SGE']['PARALLEL_ENVIRONMENT'] = parallel_environment if queue is not None: - self.config["SGE"]["QUEUE"] = queue + self.config['SGE']['QUEUE'] = queue if tmp_directory is not None: - self.config["DIRECTORIES"]["TMP"] = tmp_directory + self.config['DIRECTORIES']['TMP'] = tmp_directory if priority is not None: - self.config["SGE"]["PRIORITY"] = str(priority) + self.config['SGE']['PRIORITY'] = str(priority) try: - os.makedirs(self.config["DIRECTORIES"]["TMP"]) + os.makedirs(self.config['DIRECTORIES']['TMP']) except FileExistsError: pass - self.time = str(time_h) + ":00:00" + self.time = str(time_h) + ':00:00' self.job_name = name - if self.config["SGE"]["PRIORITY"] == "0": + if self.config['SGE']['PRIORITY'] == '0': warnings.warn( - "Priority set to 0. " "This enables the reservation flag.", + 'Priority set to 0. ' 'This enables the reservation flag.', stacklevel=2, ) self.num_threads = num_threads @@ -167,13 +167,13 @@ def __init__( if chunk_size != 1: warnings.warn( - "Chunk size != 1. " - "This can potentially have bad side effect.", + 'Chunk size != 1. ' + 'This can potentially have bad side effect.', stacklevel=2, ) if not sge_available(): - logger.error("Could not find SGE installation.") + logger.error('Could not find SGE installation.') # python interpreter which executes the jobs if not isinstance(time_h, int): @@ -187,7 +187,7 @@ def __init__( self.sge_error_file = sge_error_file else: self.sge_error_file = os.path.join( - self.config["DIRECTORIES"]["TMP"], "sge_errors.txt" + self.config['DIRECTORIES']['TMP'], 'sge_errors.txt' ) # sge stdout @@ -195,7 +195,7 @@ def __init__( self.sge_output_file = sge_output_file else: self.sge_output_file = os.path.join( - self.config["DIRECTORIES"]["TMP"], "sge_output.txt" + self.config['DIRECTORIES']['TMP'], 'sge_output.txt' ) def __repr__(self): @@ -227,8 +227,8 @@ def _validate_function_arguments(function, array): signature.bind(argument) except TypeError as err: raise SGESignatureMismatchException( - "Your jobs were not submitted as the function could not be " - "applied to the arguments." + 'Your jobs were not submitted as the function could not be ' + 'applied to the arguments.' ) from err def map(self, function, array): @@ -256,7 +256,7 @@ def map(self, function, array): self._validate_function_arguments(function, array) tmp_dir = tempfile.mkdtemp( - prefix="", suffix='_SGE_job', dir=self.config["DIRECTORIES"]["TMP"] + prefix='', suffix='_SGE_job', dir=self.config['DIRECTORIES']['TMP'] ) # jobs directory @@ -324,15 +324,14 @@ def map(self, function, array): had_exception = False for task_nr in range(nr_tasks): try: - my_file = open( + with open( os.path.join( tmp_dir, 'results', str(task_nr + 1) + '.result' ), 'rb', - ) - single_result = pickle.load(my_file) + ) as my_file: + single_result = pickle.load(my_file) results += single_result - my_file.close() except Exception as e: results.append( Exception( @@ -359,7 +358,7 @@ def _render_batch_file(self, nr_tasks, tmp_dir): try: pythonpath = os.environ['PYTHONPATH'] except KeyError: - pythonpath = "" + pythonpath = '' batch_file = """#!/bin/bash #$ -N {job_name}-{map_name} @@ -390,12 +389,12 @@ def _render_batch_file(self, nr_tasks, tmp_dir): executable=self.python_executable_path, sge_error_file=self.sge_error_file, sge_output_file=self.sge_output_file, - priority=self.config["SGE"]["PRIORITY"], - parallel_environment=self.config["SGE"]["PARALLEL_ENVIRONMENT"], - queue=self.config["SGE"]["QUEUE"], + priority=self.config['SGE']['PRIORITY'], + parallel_environment=self.config['SGE']['PARALLEL_ENVIRONMENT'], + queue=self.config['SGE']['QUEUE'], map_name=os.path.split(tmp_dir)[-1], num_threads=self.num_threads, pythonpath=pythonpath, - reservation="y" if self.config["SGE"]["PRIORITY"] == "0" else "n", + reservation='y' if self.config['SGE']['PRIORITY'] == '0' else 'n', ) return batch_file diff --git a/pyabc/storage/dataframe_bytes_storage.py b/pyabc/storage/dataframe_bytes_storage.py index ea1024bff..d3c2615f0 100644 --- a/pyabc/storage/dataframe_bytes_storage.py +++ b/pyabc/storage/dataframe_bytes_storage.py @@ -6,7 +6,7 @@ import numpy as np import pandas as pd -logger = logging.getLogger("ABC.History") +logger = logging.getLogger('ABC.History') try: import pyarrow @@ -35,11 +35,11 @@ def df_from_bytes_csv(bytes_: bytes) -> pd.DataFrame: s, index_col=0, header=0, - float_precision="round_trip", + float_precision='round_trip', quotechar='"', ) except UnicodeDecodeError: - raise DataFrameLoadException("Not a csv DataFrame") + raise DataFrameLoadException('Not a csv DataFrame') from None def df_to_bytes_msgpack(df: pd.DataFrame) -> bytes: @@ -52,9 +52,9 @@ def df_from_bytes_msgpack(bytes_: bytes) -> pd.DataFrame: try: df = pd.read_msgpack(BytesIO(bytes_)) except UnicodeDecodeError: - raise DataFrameLoadException("Not a msgpack DataFrame") + raise DataFrameLoadException('Not a msgpack DataFrame') from None if not isinstance(df, pd.DataFrame): - raise DataFrameLoadException("Not a msgpack DataFrame") + raise DataFrameLoadException('Not a msgpack DataFrame') return df @@ -115,7 +115,7 @@ def df_from_bytes_np_records(bytes_: bytes) -> pd.DataFrame: """Pandas DataFrame from numpy.recarray.""" b = BytesIO(bytes_) rec = np.load(b) - df = pd.DataFrame.from_records(rec, index="index") + df = pd.DataFrame.from_records(rec, index='index') return df @@ -127,8 +127,8 @@ def df_to_bytes(df: pd.DataFrame) -> bytes: if pyarrow is None: warnings.warn( "Can't find pyarrow, falling back to less efficient csv " - "to store pandas DataFrames.\n" - "Install e.g. via `pip install pyabc[pyarrow]`", + 'to store pandas DataFrames.\n' + 'Install e.g. via `pip install pyabc[pyarrow]`', stacklevel=2, ) return df_to_bytes_csv(df) @@ -145,7 +145,7 @@ def df_from_bytes(bytes_: bytes) -> pd.DataFrame: return df_from_bytes_csv(bytes_) except DataFrameLoadException: raise DataFrameLoadException( - "Not a csv DataFrame. An installation of pyarrow " - "may be required, e.g. via `pip install pyabc[pyarrow]`" - ) + 'Not a csv DataFrame. An installation of pyarrow ' + 'may be required, e.g. via `pip install pyabc[pyarrow]`' + ) from None return df_from_bytes_parquet(bytes_) diff --git a/pyabc/visualization/histogram.py b/pyabc/visualization/histogram.py index a17a4d3e9..2183d615a 100644 --- a/pyabc/visualization/histogram.py +++ b/pyabc/visualization/histogram.py @@ -101,10 +101,7 @@ def plot_histogram_1d_lowlevel( if xname is None: xname = x - if xmin is not None and xmax is not None: - range_ = (xmin, xmax) - else: - range_ = None + range_ = (xmin, xmax) if xmin is not None and xmax is not None else None if refval is not None: ax.axvline(refval[x], color=refval_color, linestyle='dotted') @@ -233,10 +230,7 @@ def plot_histogram_2d_lowlevel( xrange_ = [xmin, xmax] if ymin is not None and ymax is not None: yrange_ = [ymin, ymax] - if xrange_ and yrange_: - range_ = [xrange_, yrange_] - else: - range_ = None + range_ = [xrange_, yrange_] if xrange_ and yrange_ else None # plot ax.hist2d( @@ -331,7 +325,7 @@ def plot_histogram_matrix_lowlevel( ) def scatter(x, y, ax, refval=None): - ax.scatter(x, y, color="k") + ax.scatter(x, y, color='k') if refval is not None: ax.scatter([refval[x.name]], [refval[y.name]], color=refval_color) diff --git a/pyabc/visualization/kde.py b/pyabc/visualization/kde.py index 85dbcaef1..82f997b9e 100644 --- a/pyabc/visualization/kde.py +++ b/pyabc/visualization/kde.py @@ -174,7 +174,7 @@ def plot_kde_1d_highlevel_plotly( xmin=None, xmax=None, numx: int = 50, - fig: "go.Figure" = None, + fig: 'go.Figure' = None, row: int = 1, col: int = 1, size=None, @@ -250,7 +250,7 @@ def plot_kde_1d( # TODO This fixes the upper bound inadequately # ax.set_ylim(bottom=min(ax.get_ylim()[0], 0)) ax.set_xlabel(xname) - ax.set_ylabel("Posterior") + ax.set_ylabel('Posterior') ax.set_xlim(xmin, xmax) if title is not None: ax.set_title(title) @@ -271,7 +271,7 @@ def plot_kde_1d_plotly( xmin=None, xmax=None, numx=50, - fig: "go.Figure" = None, + fig: 'go.Figure' = None, row: int = 1, col: int = 1, size=None, @@ -282,7 +282,7 @@ def plot_kde_1d_plotly( kde=None, xname: str = None, **kwargs, -) -> "go.Figure": +) -> 'go.Figure': """Plot 1d kde using plotly.""" import plotly.graph_objects as go from plotly.colors import DEFAULT_PLOTLY_COLORS @@ -320,7 +320,7 @@ def plot_kde_1d_plotly( if refval is not None: fig.add_vline( x=refval[x], - line_dash="dash", + line_dash='dash', line_color=refval_color, row=row, col=col, @@ -546,7 +546,7 @@ def plot_kde_2d_highlevel_plotly( ymax: float = None, numx: int = 50, numy: int = 50, - fig: "go.Figure" = None, + fig: 'go.Figure' = None, row: int = 1, col: int = 1, size=None, @@ -559,7 +559,7 @@ def plot_kde_2d_highlevel_plotly( xname: str = None, yname: str = None, **kwargs, -) -> "go.Figure": +) -> 'go.Figure': """ Plot 2d kernel density estimate of parameter samples using plotly. """ @@ -679,7 +679,7 @@ def plot_kde_2d_plotly( ymax=None, numx=50, numy=50, - fig: "go.Figure" = None, + fig: 'go.Figure' = None, row: int = 1, col: int = 1, size=None, @@ -726,7 +726,7 @@ def plot_kde_2d_plotly( z=PDF, showscale=showscale, showlegend=showlegend, - name="Posterior", + name='Posterior', **kwargs, ), row=row, @@ -838,16 +838,15 @@ def plot_kde_matrix_highlevel_plotly( m: int = 0, t: int = None, limits=None, - height: float = 30, + height: int = 30, numx: int = 50, numy: int = 50, refval=None, refval_color='gray', - marker_color=None, kde=None, names: dict = None, - title: str = "Univariate and bivariate distributions using KDE", -) -> "go.Figure": + title: str = 'Univariate and bivariate distributions using KDE', +) -> 'go.Figure': """ Plot a KDE matrix for 1- and 2-dim marginals of the parameter samples, using plotly. @@ -947,7 +946,7 @@ def scatter(x, y, ax): alpha = w / w.max() colors = np.zeros((alpha.size, 4)) colors[:, 3] = alpha - ax.scatter(x, y, color="k") + ax.scatter(x, y, color='k') if refval is not None: ax.scatter([refval[x.name]], [refval[y.name]], color=refval_color) ax.set_xlim(*limits.get(x.name, default)) @@ -1011,8 +1010,8 @@ def plot_kde_matrix_plotly( marker_color=None, kde=None, names: dict = None, - title: str = "Univariate and bivariate distributions using KDE", -) -> "go.Figure": + title: str = 'Univariate and bivariate distributions using KDE', +) -> 'go.Figure': """ Plot a KDE matrix for 1- and 2-dim marginals of the parameter samples, using plotly. @@ -1077,8 +1076,8 @@ def scatter(x, y, fig, row, col): go.Scatter( x=x, y=y, - mode="markers", - marker={'color': "black"}, + mode='markers', + marker={'color': 'black'}, ), row=row, col=col, @@ -1088,7 +1087,7 @@ def scatter(x, y, fig, row, col): go.Scatter( x=[refval[x.name]], y=[refval[y.name]], - mode="markers", + mode='markers', marker={'color': refval_color}, ), row=row, diff --git a/test/external/test_pyjulia.py b/test/external/test_pyjulia.py index 6fdebcc14..dd699b768 100644 --- a/test/external/test_pyjulia.py +++ b/test/external/test_pyjulia.py @@ -4,16 +4,14 @@ # https://pyjulia.readthedocs.io/en/latest/troubleshooting.html Julia(compiled_modules=False) -# The pyjulia wrapper appears to ignore global noqas, thus per line here +import os # noqa: E402 +import tempfile # noqa: E402 -import os -import tempfile +import numpy as np # noqa: E402 +import pytest # noqa: E402 -import numpy as np -import pytest - -import pyabc.external.julia -from pyabc import ( +import pyabc.external.julia # noqa: E402 +from pyabc import ( # noqa: E402 ABCSMC, RV, Distribution, @@ -52,8 +50,8 @@ def sampler(request): def test_pyjulia_pipeline(sampler: Sampler): """Test that a pipeline with Julia calls runs through.""" jl = pyabc.external.julia.Julia( - source_file="doc/examples/model_julia/Normal.jl", - module_name="Normal", + source_file='doc/examples/model_julia/Normal.jl', + module_name='Normal', ) # just call it assert jl.display_source_ipython() # noqa: S101 @@ -62,15 +60,15 @@ def test_pyjulia_pipeline(sampler: Sampler): distance = jl.distance() obs = jl.observation() - prior = Distribution(p=RV("uniform", -5, 10)) + prior = Distribution(p=RV('uniform', -5, 10)) if not isinstance(sampler, SingleCoreSampler): # call model once for Julia pre-combination distance(model(prior.rvs()), model(prior.rvs())) - db_file = tempfile.mkstemp(suffix=".db")[1] + db_file = tempfile.mkstemp(suffix='.db')[1] abc = ABCSMC(model, prior, distance, population_size=100, sampler=sampler) - abc.new("sqlite:///" + db_file, obs) + abc.new('sqlite:///' + db_file, obs) abc.run(max_nr_populations=2) if os.path.exists(db_file): @@ -80,17 +78,17 @@ def test_pyjulia_pipeline(sampler: Sampler): def test_pyjulia_conversion(): """Test Julia object conversion.""" jl = pyabc.external.julia.Julia( - source_file="doc/examples/model_julia/Normal.jl", - module_name="Normal", + source_file='doc/examples/model_julia/Normal.jl', + module_name='Normal', ) model = jl.model() distance = jl.distance() obs = jl.observation() - sim = model({"p": 0.5}) - assert sim.keys() == obs.keys() == {"y"} # noqa: S101 - assert isinstance(sim["y"], np.ndarray) # noqa: S101 - assert len(sim["y"]) == len(obs["y"]) == 4 # noqa: S101 + sim = model({'p': 0.5}) + assert sim.keys() == obs.keys() == {'y'} # noqa: S101 + assert isinstance(sim['y'], np.ndarray) # noqa: S101 + assert len(sim['y']) == len(obs['y']) == 4 # noqa: S101 d = distance(sim, obs) assert isinstance(d, float) # noqa: S101 From 6879782f5ddce8627ba0bfc0a9c6bb908e8007dd Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 13:48:04 +0100 Subject: [PATCH 08/55] fix ruff --- .github/workflows/ci.yml | 4 +- pyabc/__init__.py | 2 +- pyabc/distance/kernel.py | 6 +- pyabc/distance/util.py | 2 +- pyabc/epsilon/temperature.py | 7 +- pyabc/petab/__init__.py | 7 +- pyabc/platform_factory.py | 12 +- pyabc/sampler/redis_eps/cmd.py | 44 +- pyabc/sampler/redis_eps/redis_logging.py | 8 +- pyabc/sampler/redis_eps/server_starter.py | 24 +- pyabc/sampler/redis_eps/work.py | 30 +- pyabc/sampler/redis_eps/work_static.py | 32 +- pyabc/sge/config.py | 2 +- pyabc/sge/execute_sge_array_job.py | 11 +- pyabc/sge/sge.py | 21 +- pyabc/sge/util.py | 2 +- pyabc/storage/bytes_storage.py | 2 +- pyabc/storage/db_export.py | 6 +- pyabc/storage/df_to_file.py | 8 +- pyabc/storage/migrate.py | 28 +- pyabc/storage/migrations/env.py | 6 +- .../1_20210219_add_particles_proposal_id.py | 1 + pyabc/sumstat/base.py | 3 +- pyabc/transition/grid_search.py | 14 +- pyabc/util/log.py | 10 +- pyabc/util/test.py | 8 +- pyabc/visserver/server_dash.py | 2 +- pyabc/visualization/colors.py | 466 +++++++++--------- pyabc/visualization/contour.py | 4 +- pyabc/visualization/data.py | 3 +- pyabc/visualization/model_probabilities.py | 20 +- pyabc/visualization/sample.py | 4 +- pyabc/visualization/util.py | 18 +- .../weighted_statistics.py | 2 +- test/base/conftest.py | 4 +- test/base/test_acceptor.py | 2 +- test/base/test_bytesstorage.py | 142 +++--- test/base/test_dataframeserialization.py | 60 +-- test/base/test_distance.py | 78 +-- test/base/test_distance_conversion.py | 2 +- test/base/test_epsilon.py | 10 +- test/base/test_integrated_model.py | 6 +- test/base/test_macos.py | 2 +- test/base/test_multicore_sampler.py | 4 +- test/base/test_parameter.py | 8 +- test/base/test_populationstrategy.py | 3 +- test/base/test_predictor.py | 88 ++-- test/base/test_random_variable_composition.py | 26 +- test/base/test_resume_run.py | 14 +- test/base/test_samplers.py | 58 ++- test/base/test_sge.py | 2 +- test/base/test_stop_sampling.py | 28 +- test/base/test_sumstat.py | 46 +- test/base/test_transition.py | 14 +- test/copasi/conftest.py | 4 +- test/copasi/test_copasi.py | 24 +- test/external/test_external.py | 26 +- test/external/test_rpy2.py | 20 +- test/migrate/create_test_db.py | 2 +- test/migrate/test_migrate.py | 6 +- test/petab/test_petab.py | 18 +- test/petab/test_petab_suite.py | 33 +- test/visserver/test_dash_server.py | 2 +- test/visualization/test_base_viz.py | 42 +- test/visualization/test_lookahead_viz.py | 4 +- test/visualization/test_plotly.py | 8 +- test/visualization/test_visserver.py | 6 +- test_performance/test_parameter.py | 20 +- test_performance/test_random_choice.py | 4 +- 69 files changed, 812 insertions(+), 823 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d140cc9ef..40db6e806 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -131,7 +131,7 @@ jobs: cache: pip - name: Install dependencies - run: .github/workflows/install_deps.sh + run: .github/workflows/install_deps.sh base - name: Run tests timeout-minutes: 15 @@ -161,7 +161,7 @@ jobs: cache: pip - name: Install dependencies - run: .github/workflows/install_deps.sh + run: .github/workflows/install_deps.sh base - name: Run notebooks timeout-minutes: 20 diff --git a/pyabc/__init__.py b/pyabc/__init__.py index cb2e774b1..1765219b5 100644 --- a/pyabc/__init__.py +++ b/pyabc/__init__.py @@ -140,7 +140,7 @@ except KeyError: loglevel = 'INFO' -logger = logging.getLogger("ABC") +logger = logging.getLogger('ABC') logger.setLevel(loglevel) sh = logging.StreamHandler() sh.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s')) diff --git a/pyabc/distance/kernel.py b/pyabc/distance/kernel.py index feb973220..a01b81be9 100644 --- a/pyabc/distance/kernel.py +++ b/pyabc/distance/kernel.py @@ -411,8 +411,7 @@ def __init__( if not callable(p) and (p > 1 or p < 0): raise ValueError( - f'The success probability p={p} must be in the interval' - f'[0, 1].' + f'The success probability p={p} must be in the interval[0, 1].' ) self.p = p @@ -538,8 +537,7 @@ def __init__( if not callable(p) and (p > 1 or p < 0): raise ValueError( - f'The success probability p={p} must be in the interval' - f'[0, 1].' + f'The success probability p={p} must be in the interval[0, 1].' ) self.p = p diff --git a/pyabc/distance/util.py b/pyabc/distance/util.py index 25ffda037..292309de0 100644 --- a/pyabc/distance/util.py +++ b/pyabc/distance/util.py @@ -63,7 +63,7 @@ def log_weights( weights = dict(zip(keys, weights[t])) vals = [f"'{key}': {val:.4e}" for key, val in weights.items()] - logger.debug(f"{label} weights[{t}] = {{{', '.join(vals)}}}") + logger.debug(f'{label} weights[{t}] = {{{", ".join(vals)}}}') if log_file: # read in file diff --git a/pyabc/epsilon/temperature.py b/pyabc/epsilon/temperature.py index 8a48717f2..65e257b7f 100644 --- a/pyabc/epsilon/temperature.py +++ b/pyabc/epsilon/temperature.py @@ -538,13 +538,12 @@ def __call__( # check if acceptance rate criterion violated if acceptance_rate > self.max_rate and t > 1: logger.debug( - 'ExpDecayFixedRatioScheme: ' - 'Reacting to high acceptance rate.' + 'ExpDecayFixedRatioScheme: Reacting to high acceptance rate.' ) alpha = max(alpha / 2, alpha - (1 - alpha) * 2) if acceptance_rate < self.min_rate: logger.debug( - 'ExpDecayFixedRatioScheme: ' 'Reacting to low acceptance rate.' + 'ExpDecayFixedRatioScheme: Reacting to low acceptance rate.' ) # increase alpha alpha = alpha + (1 - alpha) / 2 @@ -620,7 +619,7 @@ def __call__( ) logger.debug( - f'Temperatures proposed by polynomial decay method: ' f'{temps}.' + f'Temperatures proposed by polynomial decay method: {temps}.' ) # pre-last step is the next step diff --git a/pyabc/petab/__init__.py b/pyabc/petab/__init__.py index 049d9b242..e163da907 100644 --- a/pyabc/petab/__init__.py +++ b/pyabc/petab/__init__.py @@ -4,6 +4,7 @@ Problem definitions in the PEtab format (https://petab.rtfd.io). """ + import warnings from .amici import AmiciPetabImporter @@ -12,8 +13,8 @@ import petab except ImportError: warnings.warn( - "PEtab import requires an installation of petab " - "(https://github.com/PEtab-dev/PEtab). " - "Install via `pip3 install petab`.", + 'PEtab import requires an installation of petab ' + '(https://github.com/PEtab-dev/PEtab). ' + 'Install via `pip3 install petab`.', stacklevel=1, ) diff --git a/pyabc/platform_factory.py b/pyabc/platform_factory.py index 3cd9e23ba..b959a202f 100644 --- a/pyabc/platform_factory.py +++ b/pyabc/platform_factory.py @@ -2,15 +2,15 @@ from .sampler import MulticoreEvalParallelSampler, SingleCoreSampler -_linux = {"sampler": MulticoreEvalParallelSampler} -_macos = {"sampler": MulticoreEvalParallelSampler} -_windows = {"sampler": SingleCoreSampler} +_linux = {'sampler': MulticoreEvalParallelSampler} +_macos = {'sampler': MulticoreEvalParallelSampler} +_windows = {'sampler': SingleCoreSampler} -if platform.system() == "Windows": +if platform.system() == 'Windows': _platform_factory = _windows -elif platform.system() == "Darwin": +elif platform.system() == 'Darwin': _platform_factory = _macos else: _platform_factory = _linux -DefaultSampler = _platform_factory["sampler"] +DefaultSampler = _platform_factory['sampler'] diff --git a/pyabc/sampler/redis_eps/cmd.py b/pyabc/sampler/redis_eps/cmd.py index 570cd23b7..014f07596 100644 --- a/pyabc/sampler/redis_eps/cmd.py +++ b/pyabc/sampler/redis_eps/cmd.py @@ -3,53 +3,53 @@ # mostly communication keys # id of the current analysis -ANALYSIS_ID = "analysis_id" +ANALYSIS_ID = 'analysis_id' # generation index -GENERATION = "generation" +GENERATION = 'generation' # dynamic or static -MODE = "mode" -STATIC = "static" -DYNAMIC = "dynamic" +MODE = 'mode' +STATIC = 'static' +DYNAMIC = 'dynamic' # the queue to return results through -QUEUE = "queue" +QUEUE = 'queue' # list of done indices -DONE_IXS = "done_ixs" +DONE_IXS = 'done_ixs' # default sleep time SLEEP_TIME = 0.1 # message channel -MSG = "abc_msg_pubsub" +MSG = 'abc_msg_pubsub' # start and stop messages -START = "start" -STOP = "stop" +START = 'start' +STOP = 'stop' # whether all particles will be accepted -ALL_ACCEPTED = "all_accepted" +ALL_ACCEPTED = 'all_accepted' # wrap of the main inference routine -SSA = "sample_simulate_accept" +SSA = 'sample_simulate_accept' # whether look-ahead mode is to be employed -IS_LOOK_AHEAD = "is_look_ahead" +IS_LOOK_AHEAD = 'is_look_ahead' # maximum number of evaluations in look-ahead mode -MAX_N_EVAL_LOOK_AHEAD = "max_n_eval_look_ahead" +MAX_N_EVAL_LOOK_AHEAD = 'max_n_eval_look_ahead' # batch size to use -BATCH_SIZE = "batch_size" +BATCH_SIZE = 'batch_size' # counters # evaluations -N_EVAL = "n_eval" +N_EVAL = 'n_eval' # acceptances -N_ACC = "n_acc" +N_ACC = 'n_acc' # required particles (population size) -N_REQ = "n_req" +N_REQ = 'n_req' # failures -N_FAIL = "n_fail" +N_FAIL = 'n_fail' # active workers -N_WORKER = "n_worker" +N_WORKER = 'n_worker' # lookahead evaluations -N_LOOKAHEAD_EVAL = "n_lookahead_eval" +N_LOOKAHEAD_EVAL = 'n_lookahead_eval' # jobs (static only) -N_JOB = "n_job" +N_JOB = 'n_job' def idfy(var: str, *args): diff --git a/pyabc/sampler/redis_eps/redis_logging.py b/pyabc/sampler/redis_eps/redis_logging.py index 58969418d..79a23b2d7 100644 --- a/pyabc/sampler/redis_eps/redis_logging.py +++ b/pyabc/sampler/redis_eps/redis_logging.py @@ -3,7 +3,7 @@ import pandas as pd -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') class RedisSamplerLogger: @@ -14,11 +14,11 @@ def __init__(self, log_file: str = None): if log_file: if os.path.exists(log_file) and os.stat(log_file).st_size > 0: raise ValueError( - f"Sampler log file {log_file} exists and is not empty." + f'Sampler log file {log_file} exists and is not empty.' ) if not log_file.endswith('.csv'): raise ValueError( - f"Sampler log file {log_file} must be a .csv file" + f'Sampler log file {log_file} must be a .csv file' ) # data is a list that can be translated to pandas @@ -54,7 +54,7 @@ def add_row(self, t: int, n_evaluated: int, n_lookahead: int): def write(self): """Write data to output.""" # write to screen - logger.debug(f"Sampling for time t: {self.data[-1]}") + logger.debug(f'Sampling for time t: {self.data[-1]}') # write to file if self.log_file: df = pd.DataFrame(self.data) diff --git a/pyabc/sampler/redis_eps/server_starter.py b/pyabc/sampler/redis_eps/server_starter.py index 1162dd6ce..14bac973e 100644 --- a/pyabc/sampler/redis_eps/server_starter.py +++ b/pyabc/sampler/redis_eps/server_starter.py @@ -32,11 +32,11 @@ def __init__( if password is not None: fname = tempfile.mkstemp()[1] with open(fname, 'w') as f: - f.write(f"requirepass {password}\n") + f.write(f'requirepass {password}\n') maybe_redis_conf = [fname] self.redis_server = Popen( # noqa: S607,S603 - ["redis-server", *maybe_redis_conf, "--port", str(port)] + ['redis-server', *maybe_redis_conf, '--port', str(port)] ) # give redis-server time to start @@ -45,22 +45,22 @@ def __init__( sleep(1) # initiate worker processes - maybe_password = [] if password is None else ["--password", password] - maybe_daemon = [] if daemon is None else ["--daemon", str(daemon)] + maybe_password = [] if password is None else ['--password', password] + maybe_daemon = [] if daemon is None else ['--daemon', str(daemon)] self.workers = [ Process( target=work, args=( [ - "--host", - "localhost", - "--port", + '--host', + 'localhost', + '--port', str(port), *maybe_password, - "--processes", + '--processes', str(processes_per_worker), *maybe_daemon, - "--catch", + '--catch', str(catch), ], ), @@ -83,7 +83,7 @@ def shutdown(self): return # send stop signal to workers - _manage("stop", port=self.port, password=self.password) + _manage('stop', port=self.port, password=self.password) for p in self.workers: # wait for workers to join p.join() @@ -127,7 +127,7 @@ def __init__( ) super().__init__( - host="localhost", + host='localhost', port=self.server_starter.port, password=self.server_starter.password, batch_size=batch_size, @@ -167,7 +167,7 @@ def __init__( ) super().__init__( - host="localhost", + host='localhost', port=self.server_starter.port, password=self.server_starter.password, log_file=log_file, diff --git a/pyabc/sampler/redis_eps/work.py b/pyabc/sampler/redis_eps/work.py index dc6e9229f..6a6f25054 100644 --- a/pyabc/sampler/redis_eps/work.py +++ b/pyabc/sampler/redis_eps/work.py @@ -28,7 +28,7 @@ idfy, ) -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') def work_on_population_dynamic( @@ -78,7 +78,7 @@ def get_int(var: str): return # notify sign up as worker n_worker = redis.incr(idfy(N_WORKER, ana_id, t)) - logger.info(f"Begin generation {t}, I am worker {n_worker}") + logger.info(f'Begin generation {t}, I am worker {n_worker}') # only allow stopping the worker at particular points kill_handler.exit = False @@ -105,9 +105,9 @@ def get_int(var: str): # check whether the process was externally asked to stop if kill_handler.killed: logger.info( - f"Worker {n_worker} received stop signal. " - "Terminating in the middle of a population " - f"after {internal_counter} samples." + f'Worker {n_worker} received stop signal. ' + 'Terminating in the middle of a population ' + f'after {internal_counter} samples.' ) # notify quit redis.decr(idfy(N_WORKER, ana_id, t)) @@ -117,9 +117,9 @@ def get_int(var: str): current_runtime = time() - start_time if current_runtime > max_runtime_s: logger.info( - f"Worker {n_worker} stops during population because " - f"runtime {current_runtime} exceeds " - f"max runtime {max_runtime_s}" + f'Worker {n_worker} stops during population because ' + f'runtime {current_runtime} exceeds ' + f'max runtime {max_runtime_s}' ) # notify quit redis.decr(idfy(N_WORKER, ana_id, t)) @@ -130,8 +130,8 @@ def get_int(var: str): ana_id_new_b = redis.get(ANALYSIS_ID) if ana_id_new_b is None or str(ana_id_new_b.decode()) != ana_id: logger.info( - f"Worker {n_worker} stops during population because " - "the analysis seems to have been stopped." + f'Worker {n_worker} stops during population because ' + 'the analysis seems to have been stopped.' ) # notify quit redis.decr(idfy(N_WORKER, ana_id, t)) @@ -179,8 +179,8 @@ def get_int(var: str): new_sim = simulate_one() except Exception as e: logger.warning( - f"Redis worker number {n_worker} failed. " - f"Error message is: {e}" + f'Redis worker number {n_worker} failed. ' + f'Error message is: {e}' ) # increment the failure counter redis.incr(idfy(N_FAIL, ana_id, t), 1) @@ -232,7 +232,7 @@ def get_int(var: str): kill_handler.exit = True population_total_time = time() - population_start_time logger.info( - f"Finished generation {t}, did {internal_counter} samples. " - f"Simulation time: {cumulative_simulation_time:.2f}s, " - f"total time {population_total_time:.2f}." + f'Finished generation {t}, did {internal_counter} samples. ' + f'Simulation time: {cumulative_simulation_time:.2f}s, ' + f'total time {population_total_time:.2f}.' ) diff --git a/pyabc/sampler/redis_eps/work_static.py b/pyabc/sampler/redis_eps/work_static.py index 9c9898027..3e3043884 100644 --- a/pyabc/sampler/redis_eps/work_static.py +++ b/pyabc/sampler/redis_eps/work_static.py @@ -21,7 +21,7 @@ idfy, ) -logger = logging.getLogger("ABC.Sampler") +logger = logging.getLogger('ABC.Sampler') def announce_work(work_on_population): @@ -36,7 +36,7 @@ def wrapper( ): # notify sign up as worker n_worker = redis.incr(idfy(N_WORKER, analysis_id, t)) - logger.info(f"Begin generation {t}. I am worker {n_worker}") + logger.info(f'Begin generation {t}. I am worker {n_worker}') # don't be killed during work kill_handler.exit = False @@ -108,10 +108,10 @@ def get_int(var: str): population_total_time = time() - population_start_time logger.info( "I'm a sad jobless worker. " - f"Finished generation {t}, did {internal_counter} " - "samples. " - f"Simulation time: {cumulative_simulation_time:.2f}s, " - f"total time {population_total_time:.2f}." + f'Finished generation {t}, did {internal_counter} ' + 'samples. ' + f'Simulation time: {cumulative_simulation_time:.2f}s, ' + f'total time {population_total_time:.2f}.' ) return @@ -125,9 +125,9 @@ def get_int(var: str): # check whether the process was externally asked to stop if kill_handler.killed: logger.info( - f"Worker {n_worker} received stop signal. " - "Terminating in the middle of a population " - f"after {internal_counter} samples." + f'Worker {n_worker} received stop signal. ' + 'Terminating in the middle of a population ' + f'after {internal_counter} samples.' ) # notify quit (manually here as we call exit) redis.decr(idfy(N_WORKER, ana_id, t)) @@ -138,9 +138,9 @@ def get_int(var: str): current_runtime = time() - start_time if current_runtime > max_runtime_s: logger.info( - f"Worker {n_worker} stops during population because " - f"runtime {current_runtime} exceeds " - f"max runtime {max_runtime_s}" + f'Worker {n_worker} stops during population because ' + f'runtime {current_runtime} exceeds ' + f'max runtime {max_runtime_s}' ) # return to task queue redis.incr(idfy(N_JOB, ana_id, t)) @@ -151,8 +151,8 @@ def get_int(var: str): ana_id_new_b = redis.get(ANALYSIS_ID) if ana_id_new_b is None or str(ana_id_new_b.decode()) != ana_id: logger.info( - f"Worker {n_worker} stops during population because " - "the analysis seems to have been stopped." + f'Worker {n_worker} stops during population because ' + 'the analysis seems to have been stopped.' ) # return to task queue redis.incr(idfy(N_JOB, ana_id, t)) @@ -170,8 +170,8 @@ def get_int(var: str): new_sim = simulate_one() except Exception as e: logger.warning( - f"Redis worker number {n_worker} failed. " - f"Error message is: {e}" + f'Redis worker number {n_worker} failed. ' + f'Error message is: {e}' ) # increment the failure counter redis.incr(idfy(N_FAIL, ana_id, t), 1) diff --git a/pyabc/sge/config.py b/pyabc/sge/config.py index 0017ce96f..491897c0c 100644 --- a/pyabc/sge/config.py +++ b/pyabc/sge/config.py @@ -23,7 +23,7 @@ def get_config(): config = configparser.ConfigParser() config.read_string(DEFAULT_CONFIG) - config_file = os.path.join(os.getenv("HOME"), ".parallel") + config_file = os.path.join(os.getenv('HOME'), '.parallel') if os.path.isfile(config_file): config.read(config_file) diff --git a/pyabc/sge/execute_sge_array_job.py b/pyabc/sge/execute_sge_array_job.py index fb49dde2f..cb7cc3379 100644 --- a/pyabc/sge/execute_sge_array_job.py +++ b/pyabc/sge/execute_sge_array_job.py @@ -9,7 +9,7 @@ from .db import job_db_factory -logger = logging.getLogger("ABC.SGE") +logger = logging.getLogger('ABC.SGE') tmp_path = sys.argv[1] job_nr = sys.argv[2] @@ -37,17 +37,18 @@ results_array = [] for element in array: try: - with NamedPrinter(tmp_path, job_nr), ExecutionContext( - tmp_path, job_nr + with ( + NamedPrinter(tmp_path, job_nr), + ExecutionContext(tmp_path, job_nr), ): single_result = function(element) except Exception as e: logger.error( - "execute_sge_array_job: Exception in sge-worker path=", + 'execute_sge_array_job: Exception in sge-worker path=', tmp_path, 'jobnr=', job_nr, - "exception", + 'exception', e, ) single_result = e diff --git a/pyabc/sge/sge.py b/pyabc/sge/sge.py index f22e1412f..dac9db6ad 100644 --- a/pyabc/sge/sge.py +++ b/pyabc/sge/sge.py @@ -158,7 +158,7 @@ def __init__( self.job_name = name if self.config['SGE']['PRIORITY'] == '0': warnings.warn( - 'Priority set to 0. ' 'This enables the reservation flag.', + 'Priority set to 0. This enables the reservation flag.', stacklevel=2, ) self.num_threads = num_threads @@ -167,8 +167,7 @@ def __init__( if chunk_size != 1: warnings.warn( - 'Chunk size != 1. ' - 'This can potentially have bad side effect.', + 'Chunk size != 1. This can potentially have bad side effect.', stacklevel=2, ) @@ -200,12 +199,12 @@ def __init__( def __repr__(self): return ( - f"" + f'' ) @staticmethod @@ -334,9 +333,7 @@ def map(self, function, array): results += single_result except Exception as e: results.append( - Exception( - 'Could not load temporary ' 'result file:' + str(e) - ) + Exception('Could not load temporary result file:' + str(e)) ) had_exception = True diff --git a/pyabc/sge/util.py b/pyabc/sge/util.py index 2c4ce0ad9..8d895fdce 100644 --- a/pyabc/sge/util.py +++ b/pyabc/sge/util.py @@ -16,7 +16,7 @@ def sge_available(): Whether SGE is available or not. """ try: - subprocess.run("qstat", stdout=subprocess.PIPE) # noqa: S607,S603 + subprocess.run('qstat', stdout=subprocess.PIPE) # noqa: S607,S603 return True except FileNotFoundError: return False diff --git a/pyabc/storage/bytes_storage.py b/pyabc/storage/bytes_storage.py index f96dfc19c..93732fb7c 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -39,6 +39,6 @@ def to_bytes(object_): def from_bytes(bytes_): - if bytes_[:6] == b"\x93NUMPY": + if bytes_[:6] == b'\x93NUMPY': return np_from_bytes(bytes_) return df_from_bytes(bytes_) diff --git a/pyabc/storage/db_export.py b/pyabc/storage/db_export.py index 21768745f..069fdac6f 100644 --- a/pyabc/storage/db_export.py +++ b/pyabc/storage/db_export.py @@ -22,9 +22,7 @@ @click.option( '--generation', default='last', - help='The generation to dump. Can be ' - '"all" or "last" or an integer ' - 'number', + help='The generation to dump. Can be "all" or "last" or an integer number', ) @click.option( '--model', @@ -39,7 +37,7 @@ '--id', default=1, type=int, - help='The ABC-SMC run id which to dump. ' 'Defaults to 1', + help='The ABC-SMC run id which to dump. Defaults to 1', ) @click.option( '--tidy', diff --git a/pyabc/storage/df_to_file.py b/pyabc/storage/df_to_file.py index 20a6267b0..4a0a430b5 100644 --- a/pyabc/storage/df_to_file.py +++ b/pyabc/storage/df_to_file.py @@ -19,7 +19,7 @@ def maybe_to_json(x): """ try: - return x.to_json(orient="records") + return x.to_json(orient='records') except AttributeError: pass try: @@ -36,12 +36,12 @@ def maybe_to_json(x): def sumstat_to_json(df: pd.DataFrame) -> pd.DataFrame: df = df.copy() for c in df: - if c.startswith("sumstat"): + if c.startswith('sumstat'): df[c] = df[c].map(maybe_to_json) return df -def to_file(df: pd.DataFrame, file: str, file_format="feather"): +def to_file(df: pd.DataFrame, file: str, file_format='feather'): df_json = sumstat_to_json(df) df_json_no_index = df_json.reset_index() - getattr(df_json_no_index, "to_" + file_format)(file) + getattr(df_json_no_index, 'to_' + file_format)(file) diff --git a/pyabc/storage/migrate.py b/pyabc/storage/migrate.py index 7506ceb73..a5ddda711 100644 --- a/pyabc/storage/migrate.py +++ b/pyabc/storage/migrate.py @@ -11,25 +11,25 @@ except ImportError: Config = command = None -SQLITE_STR = "sqlite:///" +SQLITE_STR = 'sqlite:///' @click.command( - help="**Migrate pyABC database**\n\n" + help='**Migrate pyABC database**\n\n' "Sometimes, changes to pyABC's storage format are unavoidable. " - "In such cases, this tool is intended to allow migrating databases " - "between versions. " - "To avoid data loss in the unlikely case that migration does not " - "work properly, we recommend keeping the original file by specifying " - "a different destination.\n\n" - "Note: Migration currently only supports sqlite databases." + 'In such cases, this tool is intended to allow migrating databases ' + 'between versions. ' + 'To avoid data loss in the unlikely case that migration does not ' + 'work properly, we recommend keeping the original file by specifying ' + 'a different destination.\n\n' + 'Note: Migration currently only supports sqlite databases.' ) @click.option( - '--src', required=True, type=str, help="Database to convert (filename)" + '--src', required=True, type=str, help='Database to convert (filename)' ) -@click.option('--dst', required=True, type=str, help="Destination (filename)") +@click.option('--dst', required=True, type=str, help='Destination (filename)') @click.option( - '--version', default='head', type=str, help="Target database version" + '--version', default='head', type=str, help='Target database version' ) def migrate(src: str, dst: str, version: str) -> None: """Migrate database. @@ -42,8 +42,8 @@ def migrate(src: str, dst: str, version: str) -> None: """ if Config is None or command is None: print( - "Error: migration tools not installed. Please run " - "`pip install pyabc[migrate]`" + 'Error: migration tools not installed. Please run ' + '`pip install pyabc[migrate]`' ) return @@ -56,7 +56,7 @@ def migrate(src: str, dst: str, version: str) -> None: # copy file if src != dst: if os.path.exists(dst): - print(f"Error: Destination file {dst} exists already.") + print(f'Error: Destination file {dst} exists already.') return # copy source to destination shutil.copyfile(src=src, dst=dst) diff --git a/pyabc/storage/migrations/env.py b/pyabc/storage/migrations/env.py index 6fb203c56..47e211c6a 100644 --- a/pyabc/storage/migrations/env.py +++ b/pyabc/storage/migrations/env.py @@ -41,12 +41,12 @@ def run_migrations_offline(): script output. """ - url = config.get_main_option("sqlalchemy.url") + url = config.get_main_option('sqlalchemy.url') context.configure( url=url, target_metadata=target_metadata, literal_binds=True, - dialect_opts={"paramstyle": "named"}, + dialect_opts={'paramstyle': 'named'}, ) with context.begin_transaction(): @@ -62,7 +62,7 @@ def run_migrations_online(): """ connectable = engine_from_config( config.get_section(config.config_ini_section), - prefix="sqlalchemy.", + prefix='sqlalchemy.', poolclass=pool.NullPool, ) diff --git a/pyabc/storage/migrations/versions/1_20210219_add_particles_proposal_id.py b/pyabc/storage/migrations/versions/1_20210219_add_particles_proposal_id.py index 2bbb40ada..678c24a44 100644 --- a/pyabc/storage/migrations/versions/1_20210219_add_particles_proposal_id.py +++ b/pyabc/storage/migrations/versions/1_20210219_add_particles_proposal_id.py @@ -5,6 +5,7 @@ Create Date: 2021-02-19 22:11:16.466274 """ + import sqlalchemy as sa from alembic import op diff --git a/pyabc/sumstat/base.py b/pyabc/sumstat/base.py index 3439cae36..0aef357a8 100644 --- a/pyabc/sumstat/base.py +++ b/pyabc/sumstat/base.py @@ -225,6 +225,5 @@ def get_ids(self): def __str__(self) -> str: return ( - f'<{self.__class__.__name__} pre={self.pre}, ' - f'trafos={self.trafos}>' + f'<{self.__class__.__name__} pre={self.pre}, trafos={self.trafos}>' ) diff --git a/pyabc/transition/grid_search.py b/pyabc/transition/grid_search.py index c58d84960..c5eaf0fca 100644 --- a/pyabc/transition/grid_search.py +++ b/pyabc/transition/grid_search.py @@ -5,7 +5,7 @@ from .multivariatenormal import MultivariateNormalTransition -logger = logging.getLogger("ABC.Transition") +logger = logging.getLogger('ABC.Transition') class GridSearchCV(GridSearchCVSKL): @@ -68,8 +68,8 @@ def fit(self, X, y=None, groups=None): res = self.estimator.fit(X, y) self.best_estimator_ = self.estimator logger.debug( - "Single sample Gridsearch. " - f"Params: {self.estimator.get_params()}" + 'Single sample Gridsearch. ' + f'Params: {self.estimator.get_params()}' ) return res @@ -79,16 +79,16 @@ def fit(self, X, y=None, groups=None): res = super().fit(X, y, groups=groups) self.cv = old_cv logger.info( - f"Reduced CV Gridsearch {self.cv} -> {len(X)}. " - f"Best params: {self.best_params_}" + f'Reduced CV Gridsearch {self.cv} -> {len(X)}. ' + f'Best params: {self.best_params_}' ) return res res = super().fit(X, y, groups=groups) - logger.info(f"Best params: {self.best_params_}") + logger.info(f'Best params: {self.best_params_}') return res def __getattr__(self, item): - if item == "best_estimator_": + if item == 'best_estimator_': raise AttributeError return getattr(self.best_estimator_, item) diff --git a/pyabc/util/log.py b/pyabc/util/log.py index 88be4ef76..5a07177c5 100644 --- a/pyabc/util/log.py +++ b/pyabc/util/log.py @@ -27,11 +27,11 @@ def log_samples( return # uniquely define log file - log_file += f"_{t}" + log_file += f'_{t}' for key, var in [ - ("sumstats", sumstats), - ("parameters", parameters), - ("weights", weights), + ('sumstats', sumstats), + ('parameters', parameters), + ('weights', weights), ]: - np.save(log_file + f"_{key}", var, allow_pickle=False) + np.save(log_file + f'_{key}', var, allow_pickle=False) diff --git a/pyabc/util/test.py b/pyabc/util/test.py index e72bb99db..53eb3df81 100644 --- a/pyabc/util/test.py +++ b/pyabc/util/test.py @@ -4,9 +4,9 @@ import os # maximum population size environment variable -PYABC_MAX_POP_SIZE = "PYABC_MAX_POP_SIZE" +PYABC_MAX_POP_SIZE = 'PYABC_MAX_POP_SIZE' -logger = logging.getLogger("ABC.Util") +logger = logging.getLogger('ABC.Util') def bound_pop_size_from_env(pop_size: int): @@ -26,8 +26,8 @@ def bound_pop_size_from_env(pop_size: int): pop_size = min(pop_size, int(os.environ[PYABC_MAX_POP_SIZE])) logger.warning( - f"Bounding population size to {pop_size} via environment variable " - f"{PYABC_MAX_POP_SIZE}" + f'Bounding population size to {pop_size} via environment variable ' + f'{PYABC_MAX_POP_SIZE}' ) return pop_size diff --git a/pyabc/visserver/server_dash.py b/pyabc/visserver/server_dash.py index 58f5b5050..13823fd21 100644 --- a/pyabc/visserver/server_dash.py +++ b/pyabc/visserver/server_dash.py @@ -533,7 +533,7 @@ def update_DB_details(btn_click, db_path_new): [ html.H4('Error!', className='alert-heading'), html.P( - f'Please upload a database. ' f'{str(e)}.', + f'Please upload a database. {str(e)}.', ), ], id='user_update_alert', diff --git a/pyabc/visualization/colors.py b/pyabc/visualization/colors.py index f643c4fa8..86b938d05 100644 --- a/pyabc/visualization/colors.py +++ b/pyabc/visualization/colors.py @@ -3,245 +3,245 @@ From: https://material.io/design/color/the-color-system.html#tools-for-picking-colors """ -RED50 = "#FFEBEE" -RED100 = "#FFCDD2" -RED200 = "#EF9A9A" -RED300 = "#E57373" -RED400 = "#EF5350" -RED500 = "#F44336" -RED600 = "#E53935" -RED700 = "#D32F2F" -RED800 = "#C62828" -RED900 = "#B71C1C" - -PINK50 = "#FCE4EC" -PINK100 = "#F8BBD0" -PINK200 = "#F48FB1" -PINK300 = "#F06292" -PINK400 = "#EC407A" -PINK500 = "#E91E63" -PINK600 = "#D81B60" -PINK700 = "#C2185B" -PINK800 = "#AD1457" -PINK900 = "#880E4F" - -PURPLE50 = "#F3E5F5" -PURPLE100 = "#E1BEE7" -PURPLE200 = "#CE93D8" -PURPLE300 = "#BA68C8" -PURPLE400 = "#AB47BC" -PURPLE500 = "#9C27B0" -PURPLE600 = "#8E24AA" -PURPLE700 = "#7B1FA2" -PURPLE800 = "#6A1B9A" -PURPLE900 = "#4A148C" - -DEEPPURPLE50 = "#EDE7F6" -DEEPPURPLE100 = "#D1C4E9" -DEEPPURPLE200 = "#B39DDB" -DEEPPURPLE300 = "#9575CD" -DEEPPURPLE400 = "#7E57C2" -DEEPPURPLE500 = "#673AB7" -DEEPPURPLE600 = "#5E35B1" -DEEPPURPLE700 = "#512DA8" -DEEPPURPLE800 = "#4527A0" -DEEPPURPLE900 = "#311B92" - -INDIGO50 = "#E8EAF6" -INDIGO100 = "#C5CAE9" -INDIGO200 = "#9FA8DA" -INDIGO300 = "#7986CB" -INDIGO400 = "#5C6BC0" -INDIGO500 = "#3F51B5" -INDIGO600 = "#3949AB" -INDIGO700 = "#303F9F" -INDIGO800 = "#283593" -INDIGO900 = "#1A237E" - -BLUE50 = "#E3F2FD" -BLUE100 = "#BBDEFB" -BLUE200 = "#90CAF9" -BLUE300 = "#64B5F6" -BLUE400 = "#42A5F5" -BLUE500 = "#2196F3" -BLUE600 = "#1E88E5" -BLUE700 = "#1976D2" -BLUE800 = "#1565C0" -BLUE900 = "#0D47A1" - -LIGHTBLUE50 = "#E1F5FE" -LIGHTBLUE100 = "#B3E5FC" -LIGHTBLUE200 = "#81D4FA" -LIGHTBLUE300 = "#4FC3F7" -LIGHTBLUE400 = "#29B6F6" -LIGHTBLUE500 = "#03A9F4" -LIGHTBLUE600 = "#039BE5" -LIGHTBLUE700 = "#0288D1" -LIGHTBLUE800 = "#0277BD" -LIGHTBLUE900 = "#01579B" - -CYAN50 = "#E0F7FA" -CYAN100 = "#B2EBF2" -CYAN200 = "#80DEEA" -CYAN300 = "#4DD0E1" -CYAN400 = "#26C6DA" -CYAN500 = "#00BCD4" -CYAN600 = "#00ACC1" -CYAN700 = "#0097A7" -CYAN800 = "#00838F" -CYAN900 = "#006064" - -TEAL50 = "#E0F2F1" -TEAL100 = "#B2DFDB" -TEAL200 = "#80CBC4" -TEAL300 = "#4DB6AC" -TEAL400 = "#26A69A" -TEAL500 = "#009688" -TEAL600 = "#00897B" -TEAL700 = "#00796B" -TEAL800 = "#00695C" -TEAL900 = "#004D40" - -GREEN50 = "#E8F5E9" -GREEN100 = "#C8E6C9" -GREEN200 = "#A5D6A7" -GREEN300 = "#81C784" -GREEN400 = "#66BB6A" -GREEN500 = "#4CAF50" -GREEN600 = "#43A047" -GREEN700 = "#388E3C" -GREEN800 = "#2E7D32" -GREEN900 = "#1B5E20" - -LIGHTGREEN50 = "#F1F8E9" -LIGHTGREEN100 = "#DCEDC8" -LIGHTGREEN200 = "#C5E1A5" -LIGHTGREEN300 = "#AED581" -LIGHTGREEN400 = "#9CCC65" -LIGHTGREEN500 = "#8BC34A" -LIGHTGREEN600 = "#7CB342" -LIGHTGREEN700 = "#689F38" -LIGHTGREEN800 = "#558B2F" -LIGHTGREEN900 = "#33691E" - -LIME50 = "#F9FBE7" -LIME100 = "#F0F4C3" -LIME200 = "#E6EE9C" -LIME300 = "#DCE775" -LIME400 = "#D4E157" -LIME500 = "#CDDC39" -LIME600 = "#C0CA33" -LIME700 = "#AFB42B" -LIME800 = "#9E9D24" -LIME900 = "#827717" - -YELLOW50 = "#FFFDE7" -YELLOW100 = "#FFF9C4" -YELLOW200 = "#FFF59D" -YELLOW300 = "#FFF176" -YELLOW400 = "#FFEE58" -YELLOW500 = "#FFEB3B" -YELLOW600 = "#FDD835" -YELLOW700 = "#FBC02D" -YELLOW800 = "#F9A825" -YELLOW900 = "#F57F17" - -AMBER50 = "#FFF8E1" -AMBER100 = "#FFECB3" -AMBER200 = "#FFE082" -AMBER300 = "#FFD54F" -AMBER400 = "#FFCA28" -AMBER500 = "#FFC107" -AMBER600 = "#FFB300" -AMBER700 = "#FFA000" -AMBER800 = "#FF8F00" -AMBER900 = "#FF6F00" - -ORANGE50 = "#FFF3E0" -ORANGE100 = "#FFE0B2" -ORANGE200 = "#FFCC80" -ORANGE300 = "#FFB74D" -ORANGE400 = "#FFA726" -ORANGE500 = "#FF9800" -ORANGE600 = "#FB8C00" -ORANGE700 = "#F57C00" -ORANGE800 = "#EF6C00" -ORANGE900 = "#E65100" - -DEEPORANGE50 = "#FBE9E7" -DEEPORANGE100 = "#FFCCBC" -DEEPORANGE200 = "#FFAB91" -DEEPORANGE300 = "#FF8A65" -DEEPORANGE400 = "#FF7043" -DEEPORANGE500 = "#FF5722" -DEEPORANGE600 = "#F4511E" -DEEPORANGE700 = "#E64A19" -DEEPORANGE800 = "#D84315" -DEEPORANGE900 = "#BF360C" - -BROWN50 = "#EFEBE9" -BROWN100 = "#D7CCC8" -BROWN200 = "#BCAAA4" -BROWN300 = "#A1887F" -BROWN400 = "#8D6E63" -BROWN500 = "#795548" -BROWN600 = "#6D4C41" -BROWN700 = "#5D4037" -BROWN800 = "#4E342E" -BROWN900 = "#3E2723" - -GRAY50 = "#FAFAFA" -GRAY100 = "#F5F5F5" -GRAY200 = "#EEEEEE" -GRAY300 = "#E0E0E0" -GRAY400 = "#BDBDBD" -GRAY500 = "#9E9E9E" -GRAY600 = "#757575" -GRAY700 = "#616161" -GRAY800 = "#424242" -GRAY900 = "#212121" - -BLUEGRAY50 = "#ECEFF1" -BLUEGRAY100 = "#CFD8DC" -BLUEGRAY200 = "#B0BEC5" -BLUEGRAY300 = "#90A4AE" -BLUEGRAY400 = "#78909C" -BLUEGRAY500 = "#607D8B" -BLUEGRAY600 = "#546E7A" -BLUEGRAY700 = "#455A64" -BLUEGRAY800 = "#37474F" -BLUEGRAY900 = "#263238" - -BLACK = "#000000" -WHITE = "#FFFFFF" +RED50 = '#FFEBEE' +RED100 = '#FFCDD2' +RED200 = '#EF9A9A' +RED300 = '#E57373' +RED400 = '#EF5350' +RED500 = '#F44336' +RED600 = '#E53935' +RED700 = '#D32F2F' +RED800 = '#C62828' +RED900 = '#B71C1C' + +PINK50 = '#FCE4EC' +PINK100 = '#F8BBD0' +PINK200 = '#F48FB1' +PINK300 = '#F06292' +PINK400 = '#EC407A' +PINK500 = '#E91E63' +PINK600 = '#D81B60' +PINK700 = '#C2185B' +PINK800 = '#AD1457' +PINK900 = '#880E4F' + +PURPLE50 = '#F3E5F5' +PURPLE100 = '#E1BEE7' +PURPLE200 = '#CE93D8' +PURPLE300 = '#BA68C8' +PURPLE400 = '#AB47BC' +PURPLE500 = '#9C27B0' +PURPLE600 = '#8E24AA' +PURPLE700 = '#7B1FA2' +PURPLE800 = '#6A1B9A' +PURPLE900 = '#4A148C' + +DEEPPURPLE50 = '#EDE7F6' +DEEPPURPLE100 = '#D1C4E9' +DEEPPURPLE200 = '#B39DDB' +DEEPPURPLE300 = '#9575CD' +DEEPPURPLE400 = '#7E57C2' +DEEPPURPLE500 = '#673AB7' +DEEPPURPLE600 = '#5E35B1' +DEEPPURPLE700 = '#512DA8' +DEEPPURPLE800 = '#4527A0' +DEEPPURPLE900 = '#311B92' + +INDIGO50 = '#E8EAF6' +INDIGO100 = '#C5CAE9' +INDIGO200 = '#9FA8DA' +INDIGO300 = '#7986CB' +INDIGO400 = '#5C6BC0' +INDIGO500 = '#3F51B5' +INDIGO600 = '#3949AB' +INDIGO700 = '#303F9F' +INDIGO800 = '#283593' +INDIGO900 = '#1A237E' + +BLUE50 = '#E3F2FD' +BLUE100 = '#BBDEFB' +BLUE200 = '#90CAF9' +BLUE300 = '#64B5F6' +BLUE400 = '#42A5F5' +BLUE500 = '#2196F3' +BLUE600 = '#1E88E5' +BLUE700 = '#1976D2' +BLUE800 = '#1565C0' +BLUE900 = '#0D47A1' + +LIGHTBLUE50 = '#E1F5FE' +LIGHTBLUE100 = '#B3E5FC' +LIGHTBLUE200 = '#81D4FA' +LIGHTBLUE300 = '#4FC3F7' +LIGHTBLUE400 = '#29B6F6' +LIGHTBLUE500 = '#03A9F4' +LIGHTBLUE600 = '#039BE5' +LIGHTBLUE700 = '#0288D1' +LIGHTBLUE800 = '#0277BD' +LIGHTBLUE900 = '#01579B' + +CYAN50 = '#E0F7FA' +CYAN100 = '#B2EBF2' +CYAN200 = '#80DEEA' +CYAN300 = '#4DD0E1' +CYAN400 = '#26C6DA' +CYAN500 = '#00BCD4' +CYAN600 = '#00ACC1' +CYAN700 = '#0097A7' +CYAN800 = '#00838F' +CYAN900 = '#006064' + +TEAL50 = '#E0F2F1' +TEAL100 = '#B2DFDB' +TEAL200 = '#80CBC4' +TEAL300 = '#4DB6AC' +TEAL400 = '#26A69A' +TEAL500 = '#009688' +TEAL600 = '#00897B' +TEAL700 = '#00796B' +TEAL800 = '#00695C' +TEAL900 = '#004D40' + +GREEN50 = '#E8F5E9' +GREEN100 = '#C8E6C9' +GREEN200 = '#A5D6A7' +GREEN300 = '#81C784' +GREEN400 = '#66BB6A' +GREEN500 = '#4CAF50' +GREEN600 = '#43A047' +GREEN700 = '#388E3C' +GREEN800 = '#2E7D32' +GREEN900 = '#1B5E20' + +LIGHTGREEN50 = '#F1F8E9' +LIGHTGREEN100 = '#DCEDC8' +LIGHTGREEN200 = '#C5E1A5' +LIGHTGREEN300 = '#AED581' +LIGHTGREEN400 = '#9CCC65' +LIGHTGREEN500 = '#8BC34A' +LIGHTGREEN600 = '#7CB342' +LIGHTGREEN700 = '#689F38' +LIGHTGREEN800 = '#558B2F' +LIGHTGREEN900 = '#33691E' + +LIME50 = '#F9FBE7' +LIME100 = '#F0F4C3' +LIME200 = '#E6EE9C' +LIME300 = '#DCE775' +LIME400 = '#D4E157' +LIME500 = '#CDDC39' +LIME600 = '#C0CA33' +LIME700 = '#AFB42B' +LIME800 = '#9E9D24' +LIME900 = '#827717' + +YELLOW50 = '#FFFDE7' +YELLOW100 = '#FFF9C4' +YELLOW200 = '#FFF59D' +YELLOW300 = '#FFF176' +YELLOW400 = '#FFEE58' +YELLOW500 = '#FFEB3B' +YELLOW600 = '#FDD835' +YELLOW700 = '#FBC02D' +YELLOW800 = '#F9A825' +YELLOW900 = '#F57F17' + +AMBER50 = '#FFF8E1' +AMBER100 = '#FFECB3' +AMBER200 = '#FFE082' +AMBER300 = '#FFD54F' +AMBER400 = '#FFCA28' +AMBER500 = '#FFC107' +AMBER600 = '#FFB300' +AMBER700 = '#FFA000' +AMBER800 = '#FF8F00' +AMBER900 = '#FF6F00' + +ORANGE50 = '#FFF3E0' +ORANGE100 = '#FFE0B2' +ORANGE200 = '#FFCC80' +ORANGE300 = '#FFB74D' +ORANGE400 = '#FFA726' +ORANGE500 = '#FF9800' +ORANGE600 = '#FB8C00' +ORANGE700 = '#F57C00' +ORANGE800 = '#EF6C00' +ORANGE900 = '#E65100' + +DEEPORANGE50 = '#FBE9E7' +DEEPORANGE100 = '#FFCCBC' +DEEPORANGE200 = '#FFAB91' +DEEPORANGE300 = '#FF8A65' +DEEPORANGE400 = '#FF7043' +DEEPORANGE500 = '#FF5722' +DEEPORANGE600 = '#F4511E' +DEEPORANGE700 = '#E64A19' +DEEPORANGE800 = '#D84315' +DEEPORANGE900 = '#BF360C' + +BROWN50 = '#EFEBE9' +BROWN100 = '#D7CCC8' +BROWN200 = '#BCAAA4' +BROWN300 = '#A1887F' +BROWN400 = '#8D6E63' +BROWN500 = '#795548' +BROWN600 = '#6D4C41' +BROWN700 = '#5D4037' +BROWN800 = '#4E342E' +BROWN900 = '#3E2723' + +GRAY50 = '#FAFAFA' +GRAY100 = '#F5F5F5' +GRAY200 = '#EEEEEE' +GRAY300 = '#E0E0E0' +GRAY400 = '#BDBDBD' +GRAY500 = '#9E9E9E' +GRAY600 = '#757575' +GRAY700 = '#616161' +GRAY800 = '#424242' +GRAY900 = '#212121' + +BLUEGRAY50 = '#ECEFF1' +BLUEGRAY100 = '#CFD8DC' +BLUEGRAY200 = '#B0BEC5' +BLUEGRAY300 = '#90A4AE' +BLUEGRAY400 = '#78909C' +BLUEGRAY500 = '#607D8B' +BLUEGRAY600 = '#546E7A' +BLUEGRAY700 = '#455A64' +BLUEGRAY800 = '#37474F' +BLUEGRAY900 = '#263238' + +BLACK = '#000000' +WHITE = '#FFFFFF' BASECOLORS = [ - "RED", - "PINK", - "PURPLE", - "DEEPPURPLE", - "INDIGO", - "LIGHTBLUE", - "CYAN", - "TEAL", - "GREEN", - "LIGHTGREEN", - "LIME", - "YELLOW", - "AMBER", - "ORANGE", - "DEEPORANGE", - "BROWN", - "GRAY", - "BLUEGRAY", + 'RED', + 'PINK', + 'PURPLE', + 'DEEPPURPLE', + 'INDIGO', + 'LIGHTBLUE', + 'CYAN', + 'TEAL', + 'GREEN', + 'LIGHTGREEN', + 'LIME', + 'YELLOW', + 'AMBER', + 'ORANGE', + 'DEEPORANGE', + 'BROWN', + 'GRAY', + 'BLUEGRAY', ] LEVELS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900] -REDS = ["RED", "PINK", "PURPLE", "DEEPPURPLE"] -BLUES = ["INDIGO", "BLUE", "LIGHTBLUE", "CYAN"] -GREENS = ["TEAL", "GREEN", "LIGHTGREEN", "LIME"] -ORANGES = ["YELLOW", "AMBER", "ORANGE", "DEEPORANGE"] +REDS = ['RED', 'PINK', 'PURPLE', 'DEEPPURPLE'] +BLUES = ['INDIGO', 'BLUE', 'LIGHTBLUE', 'CYAN'] +GREENS = ['TEAL', 'GREEN', 'LIGHTGREEN', 'LIME'] +ORANGES = ['YELLOW', 'AMBER', 'ORANGE', 'DEEPORANGE'] REDSORANGES = [*REDS, *ORANGES] GREENSBLUES = [*GREENS, *BLUES] diff --git a/pyabc/visualization/contour.py b/pyabc/visualization/contour.py index a2f1d9416..5c5e5f35b 100644 --- a/pyabc/visualization/contour.py +++ b/pyabc/visualization/contour.py @@ -210,7 +210,7 @@ def plot_contour_2d_lowlevel( # show legend if show_legend: - handles, labels = contour.legend_elements("pdf") + handles, labels = contour.legend_elements('pdf') ax.legend(handles, labels) # show title @@ -404,7 +404,7 @@ def scatter(x, y, ax): alpha = w / w.max() colors = np.zeros((alpha.size, 4)) colors[:, 3] = alpha - ax.scatter(x, y, color="k") + ax.scatter(x, y, color='k') if refval is not None: ax.scatter([refval[x.name]], [refval[y.name]], color=refval_color) ax.set_xlim(*limits.get(x.name, default)) diff --git a/pyabc/visualization/data.py b/pyabc/visualization/data.py index bb20fca57..cc60e0709 100644 --- a/pyabc/visualization/data.py +++ b/pyabc/visualization/data.py @@ -181,8 +181,7 @@ def plot_data_default( ax.set_ylabel('Simulation') else: logger.info( - f'Data type {type(obs)} for key {obs_key} is ' - f'not supported.' + f'Data type {type(obs)} for key {obs_key} is not supported.' ) # remove not needed axis ax.axis('off') diff --git a/pyabc/visualization/model_probabilities.py b/pyabc/visualization/model_probabilities.py index 2c6226dc4..c14410e31 100644 --- a/pyabc/visualization/model_probabilities.py +++ b/pyabc/visualization/model_probabilities.py @@ -14,7 +14,7 @@ def plot_model_probabilities( history: History, rotation: int = 0, - title: str = "Model probabilities", + title: str = 'Model probabilities', size: tuple = None, ax: mpl.axes.Axes = None, ) -> mpl.axes.Axes: @@ -48,14 +48,14 @@ def plot_model_probabilities( model_probabilities = history.get_model_probabilities() # displayed in plot legend - model_probabilities.columns.name = "Model" + model_probabilities.columns.name = 'Model' # plot ax = model_probabilities.plot.bar(rot=rotation, legend=True, ax=ax) # format plot - ax.set_ylabel("Probability") - ax.set_xlabel("Population index") + ax.set_ylabel('Probability') + ax.set_xlabel('Population index') ax.set_title(title) if size is not None: @@ -67,10 +67,10 @@ def plot_model_probabilities( def plot_model_probabilities_plotly( history: History, rotation: int = 0, - title: str = "Model probabilities", + title: str = 'Model probabilities', size: tuple = None, - fig: "go.Figure" = None, -) -> "go.Figure": + fig: 'go.Figure' = None, +) -> 'go.Figure': """Plot model probabilities using plotly.""" import plotly.graph_objects as go @@ -78,7 +78,7 @@ def plot_model_probabilities_plotly( model_probabilities = history.get_model_probabilities() # displayed in plot legend - model_probabilities.columns.name = "Model" + model_probabilities.columns.name = 'Model' # plot if fig is None: @@ -95,8 +95,8 @@ def plot_model_probabilities_plotly( # format plot fig.update_layout( title=title, - xaxis_title="Population index", - yaxis_title="Probability", + xaxis_title='Population index', + yaxis_title='Probability', xaxis_tickangle=rotation, ) diff --git a/pyabc/visualization/sample.py b/pyabc/visualization/sample.py index 9b87a5b42..36fcec77b 100644 --- a/pyabc/visualization/sample.py +++ b/pyabc/visualization/sample.py @@ -92,7 +92,7 @@ def plot_sample_numbers( x=np.arange(n_run), height=matrix[i_pop, :], bottom=np.sum(matrix[:i_pop, :], axis=0), - label=f'Generation {i_pop-1}', + label=f'Generation {i_pop - 1}', ) # add labels @@ -140,7 +140,7 @@ def plot_sample_numbers_plotly( go.Bar( x=np.arange(n_run), y=matrix[i_pop, :], - name=f'Generation {i_pop-1}', + name=f'Generation {i_pop - 1}', offsetgroup=0, base=np.sum(matrix[:i_pop, :], axis=0), ) diff --git a/pyabc/visualization/util.py b/pyabc/visualization/util.py index 012bd1d71..f6d7a448a 100644 --- a/pyabc/visualization/util.py +++ b/pyabc/visualization/util.py @@ -29,14 +29,14 @@ def to_lists(*args): # check length consistency if any(len(arg) != length for arg in args): - raise AssertionError(f"The argument lengths are inconsistent: {args}") + raise AssertionError(f'The argument lengths are inconsistent: {args}') if len(args) == 1: return args[0] return args -def get_labels(labels, n: int, default_label: str = "Run"): +def get_labels(labels, n: int, default_label: str = 'Run'): """Create list of length `n` labels, using `default_label` if the only `label is None`.""" # entry to array @@ -48,11 +48,11 @@ def get_labels(labels, n: int, default_label: str = "Run"): label = labels[0] if label is None: label = default_label - labels = [f"{label} {ix}" for ix in range(n)] + labels = [f'{label} {ix}' for ix in range(n)] # check length consistency if len(labels) != n: - raise AssertionError("The number of labels does not fit") + raise AssertionError('The number of labels does not fit') return labels @@ -74,8 +74,8 @@ def format_plot_matrix(arr_ax: np.ndarray, par_names: Sequence): for i in range(0, n_par): for j in range(0, n_par): # clear labels - arr_ax[i, j].set_xlabel("") - arr_ax[i, j].set_ylabel("") + arr_ax[i, j].set_xlabel('') + arr_ax[i, j].set_ylabel('') # clear legends arr_ax[i, j].legend = None @@ -91,7 +91,7 @@ def format_plot_matrix(arr_ax: np.ndarray, par_names: Sequence): ax.set_ylabel(label) -def format_plot_matrix_plotly(fig: "go.Figure", par_names: Sequence): +def format_plot_matrix_plotly(fig: 'go.Figure', par_names: Sequence): """Clear all labels and legends, and set the left-most and bottom-most labels to the parameter names. """ @@ -100,8 +100,8 @@ def format_plot_matrix_plotly(fig: "go.Figure", par_names: Sequence): for i in range(0, n_par): for j in range(0, n_par): # clear labels - fig.update_xaxes(title_text="", row=i + 1, col=j + 1) - fig.update_yaxes(title_text="", row=i + 1, col=j + 1) + fig.update_xaxes(title_text='', row=i + 1, col=j + 1) + fig.update_yaxes(title_text='', row=i + 1, col=j + 1) # set left-most and bottom-most labels to parameter names for i, label in enumerate(par_names): diff --git a/pyabc/weighted_statistics/weighted_statistics.py b/pyabc/weighted_statistics/weighted_statistics.py index 959ed0f2b..4a6e1dc5d 100644 --- a/pyabc/weighted_statistics/weighted_statistics.py +++ b/pyabc/weighted_statistics/weighted_statistics.py @@ -11,7 +11,7 @@ def weight_checked(function): @wraps(function) def function_with_checking(points, weights=None, **kwargs): if weights is not None and not np.isclose(weights.sum(), 1): - raise AssertionError(f"Weights not normalized: {weights.sum()}.") + raise AssertionError(f'Weights not normalized: {weights.sum()}.') return function(points, weights, **kwargs) return function_with_checking diff --git a/test/base/conftest.py b/test/base/conftest.py index 03b0d088e..e58fe7cbe 100644 --- a/test/base/conftest.py +++ b/test/base/conftest.py @@ -8,8 +8,8 @@ @pytest.fixture def db_path(): - db_file_location = os.path.join(tempfile.gettempdir(), "abc_unittest.db") - db = "sqlite:///" + db_file_location + db_file_location = os.path.join(tempfile.gettempdir(), 'abc_unittest.db') + db = 'sqlite:///' + db_file_location yield db if REMOVE_DB: try: diff --git a/test/base/test_acceptor.py b/test/base/test_acceptor.py index f674bd639..9d67a9abc 100644 --- a/test/base/test_acceptor.py +++ b/test/base/test_acceptor.py @@ -81,7 +81,7 @@ def dist(x, x_0): def test_stochastic_acceptor(): """Test the stochastic acceptor's features.""" # store pnorms - pnorm_file = tempfile.mkstemp(suffix=".json")[1] + pnorm_file = tempfile.mkstemp(suffix='.json')[1] acceptor = pyabc.StochasticAcceptor( pdf_norm_method=pyabc.pdf_norm_max_found, log_file=pnorm_file ) diff --git a/test/base/test_bytesstorage.py b/test/base/test_bytesstorage.py index 8ccdb3afb..72c81f299 100644 --- a/test/base/test_bytesstorage.py +++ b/test/base/test_bytesstorage.py @@ -15,102 +15,102 @@ @pytest.fixture( params=[ - "empty", - "df-int", - "df-float", - "df-non_numeric_str", - "df-numeric_str", - "df-int-float-numeric_str", - "df-int-float-non_numeric_str-str_ind", - "df-int-float-numeric_str-str_ind", - "series", - "series-no_ind", - "py-int", - "py-float", - "py-str", - "np-int", - "np-float", - "np-str", - "np-single-int", - "np-single-float", - "np-single-str", - "r-df-cars", - "r-df-faithful", - "r-df-iris", + 'empty', + 'df-int', + 'df-float', + 'df-non_numeric_str', + 'df-numeric_str', + 'df-int-float-numeric_str', + 'df-int-float-non_numeric_str-str_ind', + 'df-int-float-numeric_str-str_ind', + 'series', + 'series-no_ind', + 'py-int', + 'py-float', + 'py-str', + 'np-int', + 'np-float', + 'np-str', + 'np-single-int', + 'np-single-float', + 'np-single-str', + 'r-df-cars', + 'r-df-faithful', + 'r-df-iris', ] ) def object_(request): par = request.param - if par == "empty": + if par == 'empty': return pd.DataFrame() - if par == "df-int": + if par == 'df-int': return pd.DataFrame( { - "a": np.random.randint(-20, 20, 100), - "b": np.random.randint(-20, 20, 100), + 'a': np.random.randint(-20, 20, 100), + 'b': np.random.randint(-20, 20, 100), } ) - if par == "df-float": + if par == 'df-float': return pd.DataFrame( - {"a": np.random.randn(100), "b": np.random.randn(100)} + {'a': np.random.randn(100), 'b': np.random.randn(100)} ) - if par == "df-non_numeric_str": - return pd.DataFrame({"a": ["foo", "bar"], "b": ["bar", "foo"]}) + if par == 'df-non_numeric_str': + return pd.DataFrame({'a': ['foo', 'bar'], 'b': ['bar', 'foo']}) - if par == "df-numeric_str": + if par == 'df-numeric_str': return pd.DataFrame( { - "a": list(map(str, np.random.randn(100))), - "b": list(map(str, np.random.randint(-20, 20, 100))), + 'a': list(map(str, np.random.randn(100))), + 'b': list(map(str, np.random.randint(-20, 20, 100))), } ) - if par == "df-int-float-numeric_str": + if par == 'df-int-float-numeric_str': return pd.DataFrame( { - "a": np.random.randint(-20, 20, 100), - "b": np.random.randn(100), - "c": list(map(str, np.random.randint(-20, 20, 100))), + 'a': np.random.randint(-20, 20, 100), + 'b': np.random.randn(100), + 'c': list(map(str, np.random.randint(-20, 20, 100))), } ) - if par == "df-int-float-non_numeric_str-str_ind": + if par == 'df-int-float-non_numeric_str-str_ind': return pd.DataFrame( - {"a": [1, 2], "b": [1.1, 2.2], "c": ["foo", "bar"]}, - index=["first", "second"], + {'a': [1, 2], 'b': [1.1, 2.2], 'c': ['foo', 'bar']}, + index=['first', 'second'], ) - if par == "df-int-float-numeric_str-str_ind": + if par == 'df-int-float-numeric_str-str_ind': return pd.DataFrame( - {"a": [1, 2], "b": [1.1, 2.2], "c": ["1", "2"]}, - index=["first", "second"], + {'a': [1, 2], 'b': [1.1, 2.2], 'c': ['1', '2']}, + index=['first', 'second'], ) - if par == "series": + if par == 'series': return pd.Series({'a': 42, 'b': 3.8, 'c': 4.2}) - if par == "series-no_ind": + if par == 'series-no_ind': return pd.Series(np.random.randn(10)) - if par == "py-int": + if par == 'py-int': return 42 - if par == "py-float": + if par == 'py-float': return 42.42 - if par == "py-str": - return "foo bar" - if par == "np-int": + if par == 'py-str': + return 'foo bar' + if par == 'np-int': return np.random.randint(-20, 20, 100) - if par == "np-float": + if par == 'np-float': return np.random.randn(100) - if par == "np-str": - return np.array(["foo", "bar"]) - if par == "np-single-int": + if par == 'np-str': + return np.array(['foo', 'bar']) + if par == 'np-single-int': return np.array(3) - if par == "np-single-float": + if par == 'np-single-float': return np.array(4.1) - if par == "np-single-str": - return np.array("foo bar") - if par == "r-df-cars": - return r["mtcars"] - if par == "r-df-iris": - return r["iris"] - if par == "r-df-faithful": - return r["faithful"] - raise Exception("Invalid Test DataFrame Type") + if par == 'np-single-str': + return np.array('foo bar') + if par == 'r-df-cars': + return r['mtcars'] + if par == 'r-df-iris': + return r['iris'] + if par == 'r-df-faithful': + return r['faithful'] + raise Exception('Invalid Test DataFrame Type') def test_storage(object_): @@ -139,7 +139,7 @@ def test_storage(object_): with localconverter(pandas2ri.converter): assert (robjects.conversion.rpy2py(object_) == rebuilt).all().all() else: - raise Exception("Could not compare") + raise Exception('Could not compare') def _check_type(object_, rebuilt): @@ -158,7 +158,7 @@ def _check_type(object_, rebuilt): return except (TypeError, ValueError): pass - raise Exception("Could not check type") + raise Exception('Could not check type') # all others keep their type else: assert isinstance(rebuilt, type(object_)) @@ -166,20 +166,20 @@ def _check_type(object_, rebuilt): def test_reference_parameter(): def model(parameter): - return {"data": parameter["mean"] + 0.5 * np.random.randn()} + return {'data': parameter['mean'] + 0.5 * np.random.randn()} prior = pyabc.Distribution( - p0=pyabc.RV("uniform", 0, 5), p1=pyabc.RV("uniform", 0, 1) + p0=pyabc.RV('uniform', 0, 5), p1=pyabc.RV('uniform', 0, 1) ) def distance(x, y): - return abs(x["data"] - y["data"]) + return abs(x['data'] - y['data']) abc = pyabc.ABCSMC(model, prior, distance, population_size=2) - db_path = "sqlite:///" + os.path.join(tempfile.gettempdir(), "test.db") + db_path = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'test.db') observation = 2.5 gt_par = {'p0': 1, 'p1': 0.25} - abc.new(db_path, {"data": observation}, gt_par=gt_par) + abc.new(db_path, {'data': observation}, gt_par=gt_par) history = abc.history par_from_history = history.get_ground_truth_parameter() assert par_from_history == gt_par diff --git a/test/base/test_dataframeserialization.py b/test/base/test_dataframeserialization.py index 6a0dc2ce0..56c17e5bb 100644 --- a/test/base/test_dataframeserialization.py +++ b/test/base/test_dataframeserialization.py @@ -7,60 +7,60 @@ @pytest.fixture( params=[ - "empty", - "int", - "float", - "non_numeric_str", - "numeric_str", - "int-float-numeric_str", - "int-float-non_numeric_str-str_ind", - "int-float-numeric_str-str_ind", + 'empty', + 'int', + 'float', + 'non_numeric_str', + 'numeric_str', + 'int-float-numeric_str', + 'int-float-non_numeric_str-str_ind', + 'int-float-numeric_str-str_ind', ] ) def df(request): par = request.param - if par == "empty": + if par == 'empty': return pd.DataFrame() - if par == "int": + if par == 'int': return pd.DataFrame( { - "a": np.random.randint(-20, 20, 100), - "b": np.random.randint(-20, 20, 100), + 'a': np.random.randint(-20, 20, 100), + 'b': np.random.randint(-20, 20, 100), } ) - if par == "float": + if par == 'float': return pd.DataFrame( - {"a": np.random.randn(100), "b": np.random.randn(100)} + {'a': np.random.randn(100), 'b': np.random.randn(100)} ) - if par == "non_numeric_str": - return pd.DataFrame({"a": ["foo", "bar"], "b": ["bar", "foo"]}) + if par == 'non_numeric_str': + return pd.DataFrame({'a': ['foo', 'bar'], 'b': ['bar', 'foo']}) - if par == "numeric_str": + if par == 'numeric_str': return pd.DataFrame( { - "a": list(map(str, np.random.randn(100))), - "b": list(map(str, np.random.randint(-20, 20, 100))), + 'a': list(map(str, np.random.randn(100))), + 'b': list(map(str, np.random.randint(-20, 20, 100))), } ) - if par == "int-float-numeric_str": + if par == 'int-float-numeric_str': return pd.DataFrame( { - "a": np.random.randint(-20, 20, 100), - "b": np.random.randn(100), - "c": list(map(str, np.random.randint(-20, 20, 100))), + 'a': np.random.randint(-20, 20, 100), + 'b': np.random.randn(100), + 'c': list(map(str, np.random.randint(-20, 20, 100))), } ) - if par == "int-float-non_numeric_str-str_ind": + if par == 'int-float-non_numeric_str-str_ind': return pd.DataFrame( - {"a": [1, 2], "b": [1.1, 2.2], "c": ["foo", "bar"]}, - index=["first", "second"], + {'a': [1, 2], 'b': [1.1, 2.2], 'c': ['foo', 'bar']}, + index=['first', 'second'], ) - if par == "int-float-numeric_str-str_ind": + if par == 'int-float-numeric_str-str_ind': return pd.DataFrame( - {"a": [1, 2], "b": [1.1, 2.2], "c": ["1", "2"]}, - index=["first", "second"], + {'a': [1, 2], 'b': [1.1, 2.2], 'c': ['1', '2']}, + index=['first', 'second'], ) - raise Exception("Invalid Test DataFrame Type") + raise Exception('Invalid Test DataFrame Type') def test_serialize(df): diff --git a/test/base/test_distance.py b/test/base/test_distance.py index 138061620..e4ff8e113 100644 --- a/test/base/test_distance.py +++ b/test/base/test_distance.py @@ -77,34 +77,34 @@ def sample_from_prior(self) -> Sample: def test_single_parameter(): - dist_f = MinMaxDistance(measures_to_use=["a"]) - abc = MockABC([{"a": -3}, {"a": 3}, {"a": 10}]) + dist_f = MinMaxDistance(measures_to_use=['a']) + abc = MockABC([{'a': -3}, {'a': 3}, {'a': 10}]) dist_f.initialize(0, abc.sample_from_prior, {}, 0) - d = dist_f({"a": 1}, {"a": 2}) + d = dist_f({'a': 1}, {'a': 2}) assert 1.0 / 13 == d def test_two_parameters_but_only_one_used(): - dist_f = MinMaxDistance(measures_to_use=["a"]) - abc = MockABC([{"a": -3, "b": 2}, {"a": 3, "b": 3}, {"a": 10, "b": 4}]) + dist_f = MinMaxDistance(measures_to_use=['a']) + abc = MockABC([{'a': -3, 'b': 2}, {'a': 3, 'b': 3}, {'a': 10, 'b': 4}]) dist_f.initialize(0, abc.sample_from_prior, {}, 0) - d = dist_f({"a": 1, "b": 10}, {"a": 2, "b": 12}) + d = dist_f({'a': 1, 'b': 10}, {'a': 2, 'b': 12}) assert 1.0 / 13 == d def test_two_parameters_and_two_used(): - dist_f = MinMaxDistance(measures_to_use=["a", "b"]) - abc = MockABC([{"a": -3, "b": 2}, {"a": 3, "b": 3}, {"a": 10, "b": 4}]) + dist_f = MinMaxDistance(measures_to_use=['a', 'b']) + abc = MockABC([{'a': -3, 'b': 2}, {'a': 3, 'b': 3}, {'a': 10, 'b': 4}]) dist_f.initialize(0, abc.sample_from_prior, {}, 0) - d = dist_f({"a": 1, "b": 10}, {"a": 2, "b": 12}) + d = dist_f({'a': 1, 'b': 10}, {'a': 2, 'b': 12}) assert 1.0 / 13 + 2 / 2 == d def test_single_parameter_percentile(): - dist_f = PercentileDistance(measures_to_use=["a"]) - abc = MockABC([{"a": -3}, {"a": 3}, {"a": 10}]) + dist_f = PercentileDistance(measures_to_use=['a']) + abc = MockABC([{'a': -3}, {'a': 3}, {'a': 10}]) dist_f.initialize(0, abc.sample_from_prior, {}, 0) - d = dist_f({"a": 1}, {"a": 2}) + d = dist_f({'a': 1}, {'a': 2}) expected = 1 / ( np.percentile([-3, 3, 10], 80) - np.percentile([-3, 3, 10], 20) ) @@ -114,19 +114,19 @@ def test_single_parameter_percentile(): def test_zscore_distance(): """Test ZScoreDistance.""" dist_f = ZScoreDistance() - abc = MockABC([{"a": -3, "b": 2}, {"a": 3, "b": 3}, {"a": 10, "b": 4}]) - x0 = {"a": 7, "b": 3} + abc = MockABC([{'a': -3, 'b': 2}, {'a': 3, 'b': 3}, {'a': 10, 'b': 4}]) + x0 = {'a': 7, 'b': 3} n_y = len(x0) dist_f.initialize(0, abc.sample_from_prior, x0, 0) - d = dist_f({"a": 4, "b": 2}, {"a": -5, "b": 10}) + d = dist_f({'a': 4, 'b': 2}, {'a': -5, 'b': 10}) expected = (abs((-5 - 4) / 5) + abs((10 - 2) / 10)) / n_y assert expected == d - d = dist_f({"a": 4, "b": 2}, {"a": -5, "b": 0}) + d = dist_f({'a': 4, 'b': 2}, {'a': -5, 'b': 0}) assert np.inf == d - d = dist_f({"a": 4, "b": 0}, {"a": -5, "b": 0}) + d = dist_f({'a': 4, 'b': 0}, {'a': -5, 'b': 0}) expected = (abs((-5 - 4) / 5)) / n_y assert expected == d @@ -137,9 +137,9 @@ def test_pca_distance(): assert dist_f.requires_calibration() abc = MockABC( - [{"a": -3.0, "b": 2.0}, {"a": 3.0, "b": 3.0}, {"a": 10.0, "b": 4.0}] + [{'a': -3.0, 'b': 2.0}, {'a': 3.0, 'b': 3.0}, {'a': 10.0, 'b': 4.0}] ) - x0 = {"a": 7.0, "b": 3.0} + x0 = {'a': 7.0, 'b': 3.0} dist_f.initialize(0, abc.sample_from_prior, x0, 0) assert dist_f.trafo.shape == (2, 2) @@ -276,7 +276,7 @@ def test_scales(): samples = np.random.normal(size=(n_sample, n_y)) s0 = np.random.normal(size=(n_y,)) - s_ids = [f"s{ix}" for ix in range(n_y)] + s_ids = [f's{ix}' for ix in range(n_y)] for scale in scale_functions: assert np.isfinite(scale(samples=samples, s0=s0, s_ids=s_ids)).all() @@ -289,7 +289,7 @@ def test_scales(): with pytest.raises(AssertionError): scale(samples=samples, s0=s0_bad, s_ids=s_ids) - s_ids_bad = [f"s{ix}" for ix in range(n_y + 1)] + s_ids_bad = [f's{ix}' for ix in range(n_y + 1)] for scale in scale_functions: with pytest.raises(AssertionError): scale(samples=samples, s0=s0, s_ids=s_ids_bad) @@ -319,7 +319,7 @@ def test_adaptivepnormdistance_initial_weights(): def test_info_weighted_pnorm_distance(): """Just test the info weighted distance pipeline.""" - db_file = create_sqlite_db_id()[len("sqlite:///") :] + db_file = create_sqlite_db_id()[len('sqlite:///') :] scale_log_file = tempfile.mkstemp()[1] info_log_file = tempfile.mkstemp()[1] info_sample_log_file = tempfile.mkstemp()[1] @@ -328,14 +328,14 @@ def test_info_weighted_pnorm_distance(): def model(p): return { - "s0": p["p0"] + np.random.normal(), - "s1": p["p1"] + np.random.normal(size=2), + 's0': p['p0'] + np.random.normal(), + 's1': p['p1'] + np.random.normal(size=2), } - prior = Distribution(p0=RV("uniform", 0, 1), p1=RV("uniform", 0, 10)) - data = {"s0": 0.5, "s1": np.array([5, 5])} + prior = Distribution(p0=RV('uniform', 0, 1), p1=RV('uniform', 0, 10)) + data = {'s0': 0.5, 's1': np.array([5, 5])} - for feature_normalization in ["mad", "std", "weights", "none"]: + for feature_normalization in ['mad', 'std', 'weights', 'none']: distance = InfoWeightedPNormDistance( predictor=LinearPredictor(), fit_info_ixs={1, 3}, @@ -345,7 +345,7 @@ def model(p): info_sample_log_file=info_sample_log_file, ) abc = ABCSMC(model, prior, distance, population_size=100) - abc.new("sqlite:///" + db_file, data) + abc.new('sqlite:///' + db_file, data) abc.run(max_nr_populations=3) finally: if os.path.exists(db_file): @@ -639,7 +639,7 @@ def test_store_weights(): ) x_0 = {'s1': 0, 's2': 0, 's3': 1} - weights_file = tempfile.mkstemp(suffix=".json")[1] + weights_file = tempfile.mkstemp(suffix='.json')[1] print(weights_file) def distance0(x_, x_0_): @@ -682,15 +682,15 @@ def test_wasserstein_distance(): n_sample = 11 def model_1d(p): - return {"y": np.random.normal(p["p0"], 1.0, size=n_sample)} + return {'y': np.random.normal(p['p0'], 1.0, size=n_sample)} - p_true = {"p0": -0.5} + p_true = {'p0': -0.5} y0 = model_1d(p_true) - p1 = {"p0": -0.55} + p1 = {'p0': -0.55} y1 = model_1d(p1) - p2 = {"p0": 3.55} + p2 = {'p0': 3.55} y2 = model_1d(p2) class IdSumstat(Sumstat): @@ -698,7 +698,7 @@ class IdSumstat(Sumstat): def __call__(self, data: dict) -> np.ndarray: # shape (n, dim) - return data["y"].reshape((-1, 1)) + return data['y'].reshape((-1, 1)) for p in [1, 2]: for distance in [ @@ -729,8 +729,8 @@ def __call__(self, data: dict) -> np.ndarray: w = np.ones(shape=n_sample) / n_sample dist_exp = sp_dist.minkowski( - np.sort(y1["y"].flatten()), - np.sort(y0["y"]).flatten(), + np.sort(y1['y'].flatten()), + np.sort(y0['y']).flatten(), w=w, p=p, ) @@ -741,8 +741,8 @@ def __call__(self, data: dict) -> np.ndarray: WassersteinDistance(sumstat=IdSumstat(), p=3) # test integrated - prior = Distribution(p0=RV("norm", 0, 2)) - db_file = tempfile.mkstemp(suffix=".db")[1] + prior = Distribution(p0=RV('norm', 0, 2)) + db_file = tempfile.mkstemp(suffix='.db')[1] try: for distance in [ WassersteinDistance( @@ -753,7 +753,7 @@ def __call__(self, data: dict) -> np.ndarray: ), ]: abc = ABCSMC(model_1d, prior, distance, population_size=10) - abc.new("sqlite:///" + db_file, y0) + abc.new('sqlite:///' + db_file, y0) abc.run(max_nr_populations=3) finally: os.remove(db_file) diff --git a/test/base/test_distance_conversion.py b/test/base/test_distance_conversion.py index dac1dfbdf..01e980f75 100644 --- a/test/base/test_distance_conversion.py +++ b/test/base/test_distance_conversion.py @@ -15,4 +15,4 @@ def distance(request): def test_distance_none(distance): dist_func = FunctionDistance.to_distance(distance) config = dist_func.get_config() - assert "name" in config + assert 'name' in config diff --git a/test/base/test_epsilon.py b/test/base/test_epsilon.py index 0056ee75f..4b4faacff 100644 --- a/test/base/test_epsilon.py +++ b/test/base/test_epsilon.py @@ -204,16 +204,16 @@ def test_silk_optimal_eps(db_path): """Test an analysis with""" def model(p): - theta = p["theta"] - return {"y": (theta - 10) ** 2 - 100 * np.exp(-100 * (theta - 3) ** 2)} + theta = p['theta'] + return {'y': (theta - 10) ** 2 - 100 * np.exp(-100 * (theta - 3) ** 2)} - p_true = {"theta": 3} + p_true = {'theta': 3} y_obs = model(p_true) - bounds = {"theta": (0, 20)} + bounds = {'theta': (0, 20)} prior = pyabc.Distribution( **{ - key: pyabc.RV("uniform", lb, ub - lb) + key: pyabc.RV('uniform', lb, ub - lb) for key, (lb, ub) in bounds.items() } ) diff --git a/test/base/test_integrated_model.py b/test/base/test_integrated_model.py index 09776f10b..b774a0242 100644 --- a/test/base/test_integrated_model.py +++ b/test/base/test_integrated_model.py @@ -24,7 +24,7 @@ def integrated_simulate(self, pars, eps): trajectory = np.zeros(self.n_steps) for t in range(1, self.n_steps): xi = np.random.uniform() - next_val = trajectory[t - 1] + xi * pars["step_size"] + next_val = trajectory[t - 1] + xi * pars['step_size'] cumsum += abs(next_val - self.gt_trajectory[t]) trajectory[t] = next_val if cumsum > eps: @@ -32,7 +32,7 @@ def integrated_simulate(self, pars, eps): return pyabc.ModelResult(accepted=False) return pyabc.ModelResult( - accepted=True, distance=cumsum, sum_stat={"trajectory": trajectory} + accepted=True, distance=cumsum, sum_stat={'trajectory': trajectory} ) @@ -40,7 +40,7 @@ def test_early_stopping(): """Basic test whether an early stopping pipeline works. Heavily inspired by the `early_stopping` notebook. """ - prior = pyabc.Distribution(step_size=pyabc.RV("uniform", 0, 10)) + prior = pyabc.Distribution(step_size=pyabc.RV('uniform', 0, 10)) n_steps = 30 gt_step_size = 5 diff --git a/test/base/test_macos.py b/test/base/test_macos.py index 67bd303c3..52ef51226 100644 --- a/test/base/test_macos.py +++ b/test/base/test_macos.py @@ -36,7 +36,7 @@ def distance(x, y): return np.sum(x['s0'] - y['s0']) x0 = model({'p0': 2}) - prior = pyabc.Distribution(p0=pyabc.RV("uniform", 0, 10)) + prior = pyabc.Distribution(p0=pyabc.RV('uniform', 0, 10)) abc = pyabc.ABCSMC( model, prior, distance, sampler=sampler, population_size=50 diff --git a/test/base/test_multicore_sampler.py b/test/base/test_multicore_sampler.py index ddf2a40ff..901c7a05d 100644 --- a/test/base/test_multicore_sampler.py +++ b/test/base/test_multicore_sampler.py @@ -36,8 +36,8 @@ def test_no_pickle(sampler): def raise_exception(*args): raise Exception( - "Deliberate exception to be raised in the worker " - "processes and to be propagated to the parent process." + 'Deliberate exception to be raised in the worker ' + 'processes and to be propagated to the parent process.' ) diff --git a/test/base/test_parameter.py b/test/base/test_parameter.py index 51cdc2e0b..5fd25a342 100644 --- a/test/base/test_parameter.py +++ b/test/base/test_parameter.py @@ -6,17 +6,17 @@ def test_param_access(): p = Parameter(a=1, b=2) assert p.a == 1 - assert p["a"] == 1 + assert p['a'] == 1 def test_param_access_from_dict(): - p = Parameter({"a": 1, "b": 2}) + p = Parameter({'a': 1, 'b': 2}) assert p.a == 1 - assert p["a"] == 1 + assert p['a'] == 1 def test_pickle(): - par = Parameter({"a": 1, "b": 2}) + par = Parameter({'a': 1, 'b': 2}) s = pickle.dumps(par) loaded = pickle.loads(s) assert loaded is not par diff --git a/test/base/test_populationstrategy.py b/test/base/test_populationstrategy.py index f98909c3c..0a9689cac 100644 --- a/test/base/test_populationstrategy.py +++ b/test/base/test_populationstrategy.py @@ -129,8 +129,7 @@ def test_transitions_not_modified(population_strategy: PopulationStrategy): for k1, k2 in zip(test_weights, after_adaptation_weights) ) err_msg = ( - f'Population strategy {population_strategy}' - ' modified the transitions' + f'Population strategy {population_strategy} modified the transitions' ) assert same, err_msg diff --git a/test/base/test_predictor.py b/test/base/test_predictor.py index 03a855374..b5c135317 100644 --- a/test/base/test_predictor.py +++ b/test/base/test_predictor.py @@ -18,33 +18,33 @@ @pytest.fixture( params=[ - "linear", - "linear not joint", - "linear not normalized", - "linear weighted", - "linear not joint weighted", - "lasso", - "GP no ard", - "GP no ard not joint", - "GP ard", - "MLP heuristic", - "MLP mean", - "MLP max", - "MS TTS", - "MS CV", + 'linear', + 'linear not joint', + 'linear not normalized', + 'linear weighted', + 'linear not joint weighted', + 'lasso', + 'GP no ard', + 'GP no ard not joint', + 'GP ard', + 'MLP heuristic', + 'MLP mean', + 'MLP max', + 'MS TTS', + 'MS CV', ] ) def s_predictor(request) -> str: return request.param -@pytest.fixture(params=["linear", "quadratic"]) +@pytest.fixture(params=['linear', 'quadratic']) def s_model(request) -> str: return request.param @pytest.mark.flaky(reruns=5) -@pytest.mark.filterwarnings("ignore::sklearn.exceptions.ConvergenceWarning") +@pytest.mark.filterwarnings('ignore::sklearn.exceptions.ConvergenceWarning') def test_fit(s_model, s_predictor): """Test fit on a simple model.""" @@ -58,63 +58,63 @@ def test_fit(s_model, s_predictor): m = 1 + rng.normal(size=(n_y, n_p)) b = 1 + rng.normal(size=(1, n_p)) - if s_model == "linear": + if s_model == 'linear': def model(y: np.ndarray) -> np.ndarray: return np.dot(y, m) + b - elif s_model == "quadratic": + elif s_model == 'quadratic': def model(y: np.ndarray) -> np.ndarray: return np.dot(y, m) ** 2 + b else: - raise ValueError("Invalid argument") + raise ValueError('Invalid argument') - if s_predictor == "linear": + if s_predictor == 'linear': predictor = LinearPredictor() - elif s_predictor == "linear not joint": + elif s_predictor == 'linear not joint': predictor = LinearPredictor(joint=False) - elif s_predictor == "linear not normalized": + elif s_predictor == 'linear not normalized': predictor = LinearPredictor( normalize_features=False, normalize_labels=False ) - elif s_predictor == "linear weighted": + elif s_predictor == 'linear weighted': predictor = LinearPredictor(weight_samples=True) - elif s_predictor == "linear not joint weighted": + elif s_predictor == 'linear not joint weighted': predictor = LinearPredictor(joint=False, weight_samples=True) - elif s_predictor == "lasso": + elif s_predictor == 'lasso': predictor = LassoPredictor() - elif s_predictor == "GP no ard": + elif s_predictor == 'GP no ard': predictor = GPPredictor(kernel=GPKernelHandle(ard=False)) - elif s_predictor == "GP no ard not joint": + elif s_predictor == 'GP no ard not joint': predictor = GPPredictor(kernel=GPKernelHandle(ard=False), joint=False) - elif s_predictor == "GP ard": + elif s_predictor == 'GP ard': predictor = GPPredictor() - elif s_predictor == "MLP heuristic": + elif s_predictor == 'MLP heuristic': predictor = MLPPredictor( - hidden_layer_sizes=HiddenLayerHandle(method="heuristic") + hidden_layer_sizes=HiddenLayerHandle(method='heuristic') ) - elif s_predictor == "MLP mean": + elif s_predictor == 'MLP mean': predictor = MLPPredictor( - hidden_layer_sizes=HiddenLayerHandle(method="mean") + hidden_layer_sizes=HiddenLayerHandle(method='mean') ) - elif s_predictor == "MLP max": + elif s_predictor == 'MLP max': predictor = MLPPredictor( - hidden_layer_sizes=HiddenLayerHandle(method="max") + hidden_layer_sizes=HiddenLayerHandle(method='max') ) - elif s_predictor == "MS TTS": + elif s_predictor == 'MS TTS': predictor = ModelSelectionPredictor( predictors=[LinearPredictor(), MLPPredictor()], - split_method="train_test_split", + split_method='train_test_split', ) - elif s_predictor == "MS CV": + elif s_predictor == 'MS CV': predictor = ModelSelectionPredictor( predictors=[LinearPredictor(), MLPPredictor()], - split_method="cross_validation", + split_method='cross_validation', ) else: - raise ValueError("Invalid argument") + raise ValueError('Invalid argument') # training data ys_train = rng.normal(size=(n_sample_train, n_y)) @@ -133,15 +133,15 @@ def model(y: np.ndarray) -> np.ndarray: # measure discrepancy rmse = root_mean_square_error(ps_test, ps_pred, 1.0) - print(f"rmse {s_predictor} {s_model}:", rmse) + print(f'rmse {s_predictor} {s_model}:', rmse) # ignore lasso if isinstance( predictor, - (LinearPredictor, GPPredictor, MLPPredictor, ModelSelectionPredictor), + LinearPredictor | GPPredictor | MLPPredictor | ModelSelectionPredictor, ): - if s_model == "linear": - if s_predictor not in ["linear not normalized"]: + if s_model == 'linear': + if s_predictor not in ['linear not normalized']: # all should work well on linear assert rmse < 0.1 elif not isinstance(predictor, LinearPredictor): @@ -173,4 +173,4 @@ def test_error_functions(): def test_wrong_input(): """Test all kinds of wrong inputs.""" with pytest.raises(ValueError): - HiddenLayerHandle(method="potato")(n_in=10, n_out=10, n_sample=100) + HiddenLayerHandle(method='potato')(n_in=10, n_out=10, n_sample=100) diff --git a/test/base/test_random_variable_composition.py b/test/base/test_random_variable_composition.py index 9959ffa9e..6e65dfb04 100644 --- a/test/base/test_random_variable_composition.py +++ b/test/base/test_random_variable_composition.py @@ -8,31 +8,31 @@ class TextRVComposition(unittest.TestCase): def setUp(self): self.d = Distribution( **{ - "a": RV("randint", low=0, high=3 + 1), - "b": Distribution( + 'a': RV('randint', low=0, high=3 + 1), + 'b': Distribution( **{ - "b1": RV("randint", low=0, high=3 + 1), - "b2": RV("randint", low=0, high=3 + 1), + 'b1': RV('randint', low=0, high=3 + 1), + 'b2': RV('randint', low=0, high=3 + 1), } ), } ) self.d_plus_one = Distribution( **{ - "a": RV("randint", low=1, high=1 + 1), - "b": Distribution( + 'a': RV('randint', low=1, high=1 + 1), + 'b': Distribution( **{ - "b1": RV("randint", low=1, high=1 + 1), - "b2": RV("randint", low=1, high=1 + 1), + 'b1': RV('randint', low=1, high=1 + 1), + 'b2': RV('randint', low=1, high=1 + 1), } ), } ) - self.x_one = Parameter({"a": 1, "b": Parameter({"b1": 1, "b2": 1})}) + self.x_one = Parameter({'a': 1, 'b': Parameter({'b1': 1, 'b2': 1})}) - self.x_zero = Parameter({"a": 0, "b": Parameter({"b1": 0, "b2": 0})}) + self.x_zero = Parameter({'a': 0, 'b': Parameter({'b1': 0, 'b2': 0})}) - self.x_two = Parameter({"a": 2, "b": Parameter({"b1": 2, "b2": 2})}) + self.x_two = Parameter({'a': 2, 'b': Parameter({'b1': 2, 'b2': 2})}) def test_composition(self): self.assertEqual(1 / 4**3, self.d.pdf(self.x_one)) @@ -40,9 +40,9 @@ def test_composition(self): class TestRVInitialization(unittest.TestCase): def test_no_kwargs(self): - a = RV.from_dictionary({"type": "uniform", "args": [0, 0]}) + a = RV.from_dictionary({'type': 'uniform', 'args': [0, 0]}) self.assertEqual(0, a.rvs()) -if __name__ == "__main__": +if __name__ == '__main__': unittest.main() diff --git a/test/base/test_resume_run.py b/test/base/test_resume_run.py index 53a21ef11..56009c58c 100644 --- a/test/base/test_resume_run.py +++ b/test/base/test_resume_run.py @@ -41,25 +41,25 @@ def sampler(request): def test_resume(db_path, gt_model, sampler): def model(parameter): - return {"data": parameter["mean"] + np.random.randn()} + return {'data': parameter['mean'] + np.random.randn()} - prior = Distribution(mean=RV("uniform", 0, 5)) + prior = Distribution(mean=RV('uniform', 0, 5)) def distance(x, y): - x_data = x["data"] - y_data = y["data"] + x_data = x['data'] + y_data = y['data'] return abs(x_data - y_data) abc = ABCSMC(model, prior, distance, population_size=10, sampler=sampler) - history = abc.new(db_path, {"data": 2.5}, gt_model=gt_model) + history = abc.new(db_path, {'data': 2.5}, gt_model=gt_model) run_id = history.id - print("Run ID:", run_id) + print('Run ID:', run_id) hist_new = abc.run(minimum_epsilon=0, max_nr_populations=1) assert hist_new.n_populations == 1 abc_continued = ABCSMC(model, prior, distance, sampler=sampler) run_id_continued = abc_continued.load(db_path, run_id) - print("Run ID continued:", run_id_continued) + print('Run ID continued:', run_id_continued) hist_contd = abc_continued.run(minimum_epsilon=0, max_nr_populations=1) assert hist_contd.n_populations == 2 diff --git a/test/base/test_samplers.py b/test/base/test_samplers.py index 658e74c4d..8f27754ef 100644 --- a/test/base/test_samplers.py +++ b/test/base/test_samplers.py @@ -167,7 +167,7 @@ def basic_testcase(): """A simple test model.""" def model(p): - return {"y": p['p0'] + 0.1 * np.random.randn(10)} + return {'y': p['p0'] + 0.1 * np.random.randn(10)} prior = pyabc.Distribution( p0=pyabc.RV('uniform', -5, 10), p1=pyabc.RV('uniform', -2, 2) @@ -189,7 +189,7 @@ def two_competing_gaussians_multiple_population(db_path, sampler): sigma = 0.5 def model(args): - return {"y": st.norm(args['x'], sigma).rvs()} + return {'y': st.norm(args['x'], sigma).rvs()} # We define two models, but they are identical so far models = [model, model] @@ -198,8 +198,8 @@ def model(args): # However, our models' priors are not the same. Their mean differs. mu_x_1, mu_x_2 = 0, 1 parameter_given_model_prior_distribution = [ - pyabc.Distribution(x=pyabc.RV("norm", mu_x_1, sigma)), - pyabc.Distribution(x=pyabc.RV("norm", mu_x_2, sigma)), + pyabc.Distribution(x=pyabc.RV('norm', mu_x_1, sigma)), + pyabc.Distribution(x=pyabc.RV('norm', mu_x_2, sigma)), ] # We plug all the ABC setups together @@ -208,7 +208,7 @@ def model(args): abc = pyabc.ABCSMC( models, parameter_given_model_prior_distribution, - pyabc.PercentileDistance(measures_to_use=["y"]), + pyabc.PercentileDistance(measures_to_use=['y']), pop_size, eps=pyabc.MedianEpsilon(), sampler=sampler, @@ -218,7 +218,7 @@ def model(args): # define where to store the results # y_observed is the important piece here: our actual observation. y_observed = 1 - abc.new(db_path, {"y": y_observed}) + abc.new(db_path, {'y': y_observed}) # We run the ABC with 3 populations max minimum_epsilon = 0.05 @@ -228,9 +228,7 @@ def model(args): mp = history.get_model_probabilities(history.max_t) def p_y_given_model(mu_x_model): - res = st.norm(mu_x_model, np.sqrt(sigma**2 + sigma**2)).pdf( - y_observed - ) + res = st.norm(mu_x_model, np.sqrt(sigma**2 + sigma**2)).pdf(y_observed) return res p1_expected_unnormalized = p_y_given_model(mu_x_1) @@ -270,9 +268,9 @@ def p_y_given_model(mu_x_model): # for low-runtime models, this should not be a problem. Thus, only # print a warning here. logger.warning( - f"Had {pre_evals} simulations in the calibration iteration, " - f"but a maximum of {max_expected} would have been sufficient for " - f"the population size of {pop_size.nr_particles}." + f'Had {pre_evals} simulations in the calibration iteration, ' + f'but a maximum of {max_expected} would have been sufficient for ' + f'the population size of {pop_size.nr_particles}.' ) @@ -288,7 +286,7 @@ def test_progressbar(sampler): def test_in_memory(redis_starter_sampler): - db_path = "sqlite://" + db_path = 'sqlite://' two_competing_gaussians_multiple_population(db_path, redis_starter_sampler) @@ -320,7 +318,7 @@ def simulate_one(): ) try: # id needs to be set - sampler.set_analysis_id("ana_id") + sampler.set_analysis_id('ana_id') sample = sampler.sample_until_n_accepted(10, simulate_one, 0) assert 10 == len(sample.get_accepted_population()) @@ -331,13 +329,13 @@ def simulate_one(): def test_redis_catch_error(): def model(pars): if np.random.uniform() < 0.1: - raise ValueError("error") + raise ValueError('error') return {'s0': pars['p0'] + 0.2 * np.random.uniform()} def distance(s0, s1): return abs(s0['s0'] - s1['s0']) - prior = pyabc.Distribution(p0=pyabc.RV("uniform", 0, 10)) + prior = pyabc.Distribution(p0=pyabc.RV('uniform', 0, 10)) sampler = RedisEvalParallelSamplerServerStarter( batch_size=3, workers=1, processes_per_worker=1 ) @@ -346,7 +344,7 @@ def distance(s0, s1): model, prior, distance, sampler=sampler, population_size=10 ) - db_file = "sqlite:///" + os.path.join(tempfile.gettempdir(), "test.db") + db_file = 'sqlite:///' + os.path.join(tempfile.gettempdir(), 'test.db') data = {'s0': 2.8} abc.new(db_file, data) abc.run(minimum_epsilon=0.1, max_nr_populations=3) @@ -360,11 +358,11 @@ def simulate_one(): return pyabc.Particle(0, {}, 0.1, {}, 0, accepted) sampler = RedisEvalParallelSamplerServerStarter( # noqa: S106 - password="daenerys" + password='daenerys' ) try: # needs to be always set - sampler.set_analysis_id("ana_id") + sampler.set_analysis_id('ana_id') sample = sampler.sample_until_n_accepted(10, simulate_one, 0) assert 10 == len(sample.get_accepted_population()) finally: @@ -375,14 +373,14 @@ def test_redis_continuous_analyses(): """Test correct behavior of the redis server with multiple analyses.""" sampler = RedisEvalParallelSamplerServerStarter() try: - sampler.set_analysis_id("id1") + sampler.set_analysis_id('id1') # try "starting a new run while the old one has not finished yet" with pytest.raises(AssertionError) as e: - sampler.set_analysis_id("id2") - assert "busy with an analysis " in str(e.value) + sampler.set_analysis_id('id2') + assert 'busy with an analysis ' in str(e.value) # after stopping it should work sampler.stop() - sampler.set_analysis_id("id2") + sampler.set_analysis_id('id2') finally: sampler.shutdown() @@ -390,11 +388,11 @@ def test_redis_continuous_analyses(): def test_redis_subprocess(): """Test whether the instructed redis sampler allows worker subprocesses.""" # print worker output - logging.getLogger("Redis-Worker").addHandler(logging.StreamHandler()) + logging.getLogger('Redis-Worker').addHandler(logging.StreamHandler()) def model_process(p, pipe): """The actual model.""" - pipe.send({"y": p['p0'] + 0.1 * np.random.randn(10)}) + pipe.send({'y': p['p0'] + 0.1 * np.random.randn(10)}) def model(p): """Model calling a subprocess.""" @@ -428,7 +426,7 @@ def distance(y1, y2): sampler.shutdown() -@pytest.mark.parametrize("adapt_proposal", [False, True]) +@pytest.mark.parametrize('adapt_proposal', [False, True]) def test_redis_look_ahead(adapt_proposal: bool): """Test the redis sampler in look-ahead mode.""" model, prior, distance, obs = basic_testcase() @@ -509,12 +507,12 @@ def test_redis_look_ahead_error(): ) abc.new(pyabc.create_sqlite_db_id(), obs) abc.run(max_nr_populations=3) - assert "cannot be used in look-ahead mode" in str(e.value) + assert 'cannot be used in look-ahead mode' in str(e.value) finally: sampler.shutdown() -@pytest.mark.parametrize("adapt_proposal", [False, True]) +@pytest.mark.parametrize('adapt_proposal', [False, True]) def test_redis_look_ahead_delayed(adapt_proposal: bool): """Test the look-ahead sampler with delayed evaluation in an adaptive setup.""" @@ -576,9 +574,9 @@ def test_normalize_within_subpopulations(): proposal_id = np.random.choice(proposal_ids, p=[0.3, 0.2, 0.5]) particle = pyabc.Particle( m=0, - parameter={"theta": np.random.normal()}, + parameter={'theta': np.random.normal()}, weight=np.random.lognormal(sigma=abs(proposal_id) + 1), - sum_stat={"y": np.random.normal(size=2)}, + sum_stat={'y': np.random.normal(size=2)}, distance=np.random.lognormal(), accepted=accepted, proposal_id=proposal_id, diff --git a/test/base/test_sge.py b/test/base/test_sge.py index 3fd5158ba..217c8cc14 100644 --- a/test/base/test_sge.py +++ b/test/base/test_sge.py @@ -6,5 +6,5 @@ def test_sge_setup(): # to use pyabc.sge. Given one, the tests should be extended # considerably (which would require installing the gridengine # on the test system first). - sge = SGE(priority=-500, memory="1G", name="test", time_h=1) + sge = SGE(priority=-500, memory='1G', name='test', time_h=1) repr(sge) diff --git a/test/base/test_stop_sampling.py b/test/base/test_stop_sampling.py index 30e34bd9d..1174ee706 100644 --- a/test/base/test_stop_sampling.py +++ b/test/base/test_stop_sampling.py @@ -13,25 +13,25 @@ def model(x): """Some model""" - return {"par": x["par"] + np.random.randn()} + return {'par': x['par'] + np.random.randn()} def dist(x, y): """Some distance""" - return abs(x["par"] - y["par"]) + return abs(x['par'] - y['par']) def test_stop_acceptance_rate_too_low(db_path): """Test the acceptance rate condition.""" abc = ABCSMC(model, Distribution(par=st.uniform(0, 10)), dist, pop_size) - abc.new(db_path, {"par": 0.5}) + abc.new(db_path, {'par': 0.5}) history = abc.run(-1, 8, min_acceptance_rate=set_acc_rate) df = history.get_all_populations() - df["acceptance_rate"] = df["particles"] / df["samples"] - assert df["acceptance_rate"].iloc[-1] < set_acc_rate + df['acceptance_rate'] = df['particles'] / df['samples'] + assert df['acceptance_rate'].iloc[-1] < set_acc_rate assert ( - df["acceptance_rate"].iloc[-2] >= set_acc_rate - or df["t"].iloc[-2] == -1 + df['acceptance_rate'].iloc[-2] >= set_acc_rate + or df['t'].iloc[-2] == -1 ) # calibration iteration @@ -64,27 +64,27 @@ def test_stop_early(db_path, max_eval_checked_sampler): pop_size, sampler=sampler, ) - abc.new(db_path, {"par": 0.5}) + abc.new(db_path, {'par': 0.5}) history = abc.run(min_acceptance_rate=set_acc_rate) df = history.get_all_populations() # offset with n_procs as more processes can have run at termination n_procs = sampler.n_procs if hasattr(sampler, 'n_procs') else 1 - df["corrected_acceptance_rate"] = df["particles"] / ( - df["samples"] - (n_procs - 1) + df['corrected_acceptance_rate'] = df['particles'] / ( + df['samples'] - (n_procs - 1) ) # if already the first generation fails, the quotient is not meaningful assert ( max(df.t) == -1 - or df["corrected_acceptance_rate"].iloc[-1] >= set_acc_rate + or df['corrected_acceptance_rate'].iloc[-1] >= set_acc_rate ) def test_total_nr_simulations(db_path): """Test the total number of samples condition.""" abc = ABCSMC(model, Distribution(par=st.uniform(0, 10)), dist, pop_size) - abc.new(db_path, {"par": 0.5}) + abc.new(db_path, {'par': 0.5}) max_total_nr_sim = 142 history = abc.run(-1, 100, max_total_nr_simulations=max_total_nr_sim) assert history.total_nr_simulations >= max_total_nr_sim @@ -99,7 +99,7 @@ def test_total_nr_simulations(db_path): def test_max_walltime(db_path): """Test the maximum walltime condition.""" abc = ABCSMC(model, Distribution(par=st.uniform(0, 10)), dist, pop_size) - abc.new(db_path, {"par": 0.5}) + abc.new(db_path, {'par': 0.5}) init_walltime = datetime.now() max_walltime = timedelta(milliseconds=500) history = abc.run(-1, 100, max_walltime=max_walltime) @@ -110,7 +110,7 @@ def test_max_walltime(db_path): def test_min_eps_diff(db_path): """Test the minimum epsilon difference condition.""" abc = ABCSMC(model, Distribution(par=st.uniform(0, 10)), dist, pop_size) - abc.new(db_path, {"par": 0.5}) + abc.new(db_path, {'par': 0.5}) min_eps_diff = 1 history = abc.run( minimum_epsilon=-1, diff --git a/test/base/test_sumstat.py b/test/base/test_sumstat.py index a871f0f56..4e5138418 100644 --- a/test/base/test_sumstat.py +++ b/test/base/test_sumstat.py @@ -21,30 +21,30 @@ def test_dict2arr(): """Test conversion of dicts to arrays.""" dct = { - "s0": pd.DataFrame({"a": [0, 1], "b": [2, 3]}), - "s1": np.array([4, 5]), - "s2": 6, + 's0': pd.DataFrame({'a': [0, 1], 'b': [2, 3]}), + 's1': np.array([4, 5]), + 's2': 6, } - keys = ["s0", "s1", "s2"] + keys = ['s0', 's1', 's2'] arr = dict2arr(dct, keys=keys) assert (arr == np.array([0, 2, 1, 3, 4, 5, 6])).all() labels = dict2arrlabels(dct, keys=keys) assert len(labels) == len(arr) assert labels == [ - "s0:a:0", - "s0:b:0", - "s0:a:1", - "s0:b:1", - "s1:0", - "s1:1", - "s2", + 's0:a:0', + 's0:b:0', + 's0:a:1', + 's0:b:1', + 's1:0', + 's1:1', + 's2', ] with pytest.raises(TypeError): - dict2arr({"s0": "alice"}, keys=["s0"]) + dict2arr({'s0': 'alice'}, keys=['s0']) with pytest.raises(TypeError): - dict2arrlabels({"s0": "alice"}, keys=["s0"]) + dict2arrlabels({'s0': 'alice'}, keys=['s0']) @pytest.fixture(params=[None, [lambda x: x, lambda x: x**2]]) @@ -146,9 +146,9 @@ def test_predictor_sumstat(): pyabc.Particle( m=0, parameter=pyabc.Parameter( - {f"p{ix}": val for ix, val in enumerate(p)} + {f'p{ix}': val for ix, val in enumerate(p)} ), - sum_stat={f"s{ix}": val for ix, val in enumerate(y)}, + sum_stat={f's{ix}': val for ix, val in enumerate(y)}, distance=100 + 1 * rng.normal(), weight=1 + 0.01 * rng.normal(), ) @@ -166,7 +166,7 @@ def test_predictor_sumstat(): assert sumstat(x).shape == (n_y,) assert (sumstat(x) == ys[0]).all() assert len(sumstat.get_ids()) == n_y - assert sumstat.get_ids() == [f"s{ix}" for ix in range(n_y)] + assert sumstat.get_ids() == [f's{ix}' for ix in range(n_y)] # 3 is a fit index --> afterwards the output size should have changed sumstat.update(t=3, get_sample=lambda: sample, total_sims=0) @@ -238,7 +238,7 @@ def test_subsetter(): @pytest.fixture() def db_file(): - db_file = tempfile.mkstemp(suffix=".db")[1] + db_file = tempfile.mkstemp(suffix='.db')[1] try: yield db_file finally: @@ -250,19 +250,19 @@ def test_pipeline(db_file): rng = np.random.Generator(np.random.PCG64(0)) def model(p): - return {"s0": p["p0"] + 1e-2 * rng.normal(size=2), "s1": rng.normal()} + return {'s0': p['p0'] + 1e-2 * rng.normal(size=2), 's1': rng.normal()} - prior = pyabc.Distribution(p0=pyabc.RV("uniform", 0, 1)) + prior = pyabc.Distribution(p0=pyabc.RV('uniform', 0, 1)) distance = pyabc.AdaptivePNormDistance( sumstat=PredictorSumstat(LinearPredictor(), fit_ixs={1, 3}), ) - data = {"s0": np.array([0.1, 0.105]), "s1": 0.5} + data = {'s0': np.array([0.1, 0.105]), 's1': 0.5} # run a little analysis abc = pyabc.ABCSMC(model, prior, distance, population_size=100) - abc.new("sqlite:///" + db_file, data) + abc.new('sqlite:///' + db_file, data) h = abc.run(max_total_nr_simulations=1000) # first iteration @@ -278,7 +278,7 @@ def model(p): distance = pyabc.PNormDistance() abc = pyabc.ABCSMC(model, prior, distance, population_size=100) - abc.new("sqlite:///" + db_file, data) + abc.new('sqlite:///' + db_file, data) h = abc.run(max_total_nr_simulations=1000) df_comp, w_comp = h.get_distribution() @@ -291,7 +291,7 @@ def model(p): fit_info_ixs={1, 3}, ) abc = pyabc.ABCSMC(model, prior, distance, population_size=100) - abc.new("sqlite:///" + db_file, data) + abc.new('sqlite:///' + db_file, data) h = abc.run(max_total_nr_simulations=1000) df_info, w_info = h.get_distribution() diff --git a/test/base/test_transition.py b/test/base/test_transition.py index 788fc307c..caf31fa95 100644 --- a/test/base/test_transition.py +++ b/test/base/test_transition.py @@ -68,13 +68,13 @@ def transition_single(request): def data(n): - df = pd.DataFrame({"a": np.random.rand(n), "b": np.random.rand(n)}) + df = pd.DataFrame({'a': np.random.rand(n), 'b': np.random.rand(n)}) w = np.ones(len(df)) / len(df) return df, w def data_single(n): - df = pd.DataFrame({"a": np.random.rand(n)}) + df = pd.DataFrame({'a': np.random.rand(n)}) w = np.ones(len(df)) / len(df) return df, w @@ -84,7 +84,7 @@ def test_rvs_return_type(transition: Transition): transition.fit(df, w) sample = transition.rvs() sample = pd.Series(sample) - assert (sample.index == pd.Index(["a", "b"])).all() + assert (sample.index == pd.Index(['a', 'b'])).all() def test_pdf_return_types(transition: Transition): @@ -217,7 +217,7 @@ def test_score(transition: Transition): def test_grid_search_multivariate_normal(): m = MultivariateNormalTransition() - m_grid = GridSearchCV(m, {"scaling": np.logspace(-5, 1.5, 5)}, n_jobs=1) + m_grid = GridSearchCV(m, {'scaling': np.logspace(-5, 1.5, 5)}, n_jobs=1) df, w = data(20) m_grid.fit(df, w) @@ -229,7 +229,7 @@ def test_grid_search_two_samples_multivariate_normal(): cv = 5 m = MultivariateNormalTransition() m_grid = GridSearchCV( - m, {"scaling": np.logspace(-5, 1.5, 5)}, cv=cv, n_jobs=1 + m, {'scaling': np.logspace(-5, 1.5, 5)}, cv=cv, n_jobs=1 ) df, w = data(2) m_grid.fit(df, w) @@ -243,7 +243,7 @@ def test_grid_search_single_sample_multivariate_normal(): cv = 5 m = MultivariateNormalTransition() m_grid = GridSearchCV( - m, {"scaling": np.logspace(-5, 1.5, 5)}, cv=cv, n_jobs=1 + m, {'scaling': np.logspace(-5, 1.5, 5)}, cv=cv, n_jobs=1 ) df, w = data(1) m_grid.fit(df, w) @@ -257,7 +257,7 @@ def test_mean_coefficient_of_variation_sample_not_full_rank( This is a test created after I encountered this kind of bug """ n = 13 - df = pd.DataFrame({"a": np.ones(n) * 2, "b": np.ones(n)}) + df = pd.DataFrame({'a': np.ones(n) * 2, 'b': np.ones(n)}) w = np.ones(len(df)) / len(df) transition.fit(df, w) transition.mean_cv() diff --git a/test/copasi/conftest.py b/test/copasi/conftest.py index 03b0d088e..e58fe7cbe 100644 --- a/test/copasi/conftest.py +++ b/test/copasi/conftest.py @@ -8,8 +8,8 @@ @pytest.fixture def db_path(): - db_file_location = os.path.join(tempfile.gettempdir(), "abc_unittest.db") - db = "sqlite:///" + db_file_location + db_file_location = os.path.join(tempfile.gettempdir(), 'abc_unittest.db') + db = 'sqlite:///' + db_file_location yield db if REMOVE_DB: try: diff --git a/test/copasi/test_copasi.py b/test/copasi/test_copasi.py index 145ba18d8..073cc35a1 100644 --- a/test/copasi/test_copasi.py +++ b/test/copasi/test_copasi.py @@ -6,47 +6,47 @@ from pyabc.copasi import BasicoModel -@pytest.fixture(params=["stochastic", "deterministic", "directMethod", "sde"]) +@pytest.fixture(params=['stochastic', 'deterministic', 'directMethod', 'sde']) def method(request): return request.param MAX_T = 0.1 -TRUE_PAR = {"rate": 2.3} -MODEL1_PATH = "doc/examples/models/model1.xml" +TRUE_PAR = {'rate': 2.3} +MODEL1_PATH = 'doc/examples/models/model1.xml' def test_basic(method): model = BasicoModel(MODEL1_PATH, duration=MAX_T, method=method) data = model(TRUE_PAR) - assert data.keys() == {"t", "X"} - assert MAX_T / 2 < data["t"].max() <= MAX_T + assert data.keys() == {'t', 'X'} + assert MAX_T / 2 < data['t'].max() <= MAX_T def test_pickling(): - model = BasicoModel(MODEL1_PATH, duration=MAX_T, method="deterministic") + model = BasicoModel(MODEL1_PATH, duration=MAX_T, method='deterministic') model_re = pickle.loads(pickle.dumps(model)) ret = model(TRUE_PAR) ret_re = model_re(TRUE_PAR) - assert np.allclose(ret["t"], ret_re["t"]) + assert np.allclose(ret['t'], ret_re['t']) def test_pipeline(db_path): - model = BasicoModel(MODEL1_PATH, duration=MAX_T, method="deterministic") + model = BasicoModel(MODEL1_PATH, duration=MAX_T, method='deterministic') data = model(TRUE_PAR) - prior = pyabc.Distribution(rate=pyabc.RV("uniform", 0, 100)) + prior = pyabc.Distribution(rate=pyabc.RV('uniform', 0, 100)) n_test_times = 20 t_test_times = np.linspace(0, MAX_T, n_test_times) def distance(x, y): - xt_ind = np.searchsorted(x["t"], t_test_times) - 1 - yt_ind = np.searchsorted(y["t"], t_test_times) - 1 + xt_ind = np.searchsorted(x['t'], t_test_times) - 1 + yt_ind = np.searchsorted(y['t'], t_test_times) - 1 error = ( - np.absolute(x["X"][:, 1][xt_ind] - y["X"][:, 1][yt_ind]).sum() + np.absolute(x['X'][:, 1][xt_ind] - y['X'][:, 1][yt_ind]).sum() / t_test_times.size ) return error diff --git a/test/external/test_external.py b/test/external/test_external.py index 88785b1dc..b866ead48 100644 --- a/test/external/test_external.py +++ b/test/external/test_external.py @@ -35,14 +35,14 @@ def sampler(request): def test_external(): - folder = "doc/examples/model_r/" - executable = "Rscript" + folder = 'doc/examples/model_r/' + executable = 'Rscript' # initialize - model = pyabc.external.ExternalModel(executable, folder + "model.r") - sum_stat = pyabc.external.ExternalSumStat(executable, folder + "sumstat.r") + model = pyabc.external.ExternalModel(executable, folder + 'model.r') + sum_stat = pyabc.external.ExternalSumStat(executable, folder + 'sumstat.r') distance = pyabc.external.ExternalDistance( - executable, folder + "distance.r" + executable, folder + 'distance.r' ) # call representation function @@ -63,11 +63,11 @@ def test_external(): def test_external_handler(): eh = pyabc.external.ExternalHandler( - executable="bash", - file="", + executable='bash', + file='', create_folder=False, - suffix="sufftest", - prefix="preftest", + suffix='sufftest', + prefix='preftest', ) loc = eh.create_loc() assert os.path.exists(loc) and os.path.isfile(loc) @@ -77,9 +77,9 @@ def test_external_handler(): def test_eval_param_limits(): - folder = "doc/examples/rmodel/" - executable = "Rscript" - model = pyabc.external.ExternalModel(executable, folder + "model.r") - limits = {"meanX": (1, 5), "meanY": (1, 5.5)} + folder = 'doc/examples/rmodel/' + executable = 'Rscript' + model = pyabc.external.ExternalModel(executable, folder + 'model.r') + limits = {'meanX': (1, 5), 'meanY': (1, 5.5)} evaluation = pyabc.external.ExternalModel.eval_param_limits(model, limits) assert type(evaluation) is dict diff --git a/test/external/test_rpy2.py b/test/external/test_rpy2.py index 9a08becf5..00262b0aa 100644 --- a/test/external/test_rpy2.py +++ b/test/external/test_rpy2.py @@ -39,15 +39,15 @@ def sampler(request): def test_rpy2_pipeline(sampler): """Test that a pipeline with rpy2 calls runs through.""" # run the notebook example - r_file = "doc/examples/myRModel.R" + r_file = 'doc/examples/myRModel.R' r = pyabc.external.r.R(r_file) r.display_source_ipython() - model = r.model("myModel") - distance = r.distance("myDistance") - sum_stat = r.summary_statistics("mySummaryStatistics") - data = r.observation("mySumStatData") + model = r.model('myModel') + distance = r.distance('myDistance') + sum_stat = r.summary_statistics('mySummaryStatistics') + data = r.observation('mySumStatData') prior = pyabc.Distribution( - meanX=pyabc.RV("uniform", 0, 10), meanY=pyabc.RV("uniform", 0, 10) + meanX=pyabc.RV('uniform', 0, 10), meanY=pyabc.RV('uniform', 0, 10) ) abc = pyabc.ABCSMC( model, @@ -57,10 +57,10 @@ def test_rpy2_pipeline(sampler): sampler=sampler, population_size=5, ) - db = pyabc.create_sqlite_db_id(file_="test_external.db") + db = pyabc.create_sqlite_db_id(file_='test_external.db') abc.new(db, data) history = abc.run(minimum_epsilon=0.9, max_nr_populations=2) - history.get_weighted_sum_stats_for_model(m=0, t=1)[1][0]["cars"].head() + history.get_weighted_sum_stats_for_model(m=0, t=1)[1][0]['cars'].head() # try load id_ = history.id @@ -87,6 +87,6 @@ def model(pars): df.to_csv(file_) return {'loc': file_} - r = pyabc.external.r.R("doc/examples/model_r/sumstat_py.r") - sumstat = r.summary_statistics("sumstat", is_py_model=True) + r = pyabc.external.r.R('doc/examples/model_r/sumstat_py.r') + sumstat = r.summary_statistics('sumstat', is_py_model=True) sumstat(model({'p0': 42})) diff --git a/test/migrate/create_test_db.py b/test/migrate/create_test_db.py index 14cd8b1e9..b16fe2499 100644 --- a/test/migrate/create_test_db.py +++ b/test/migrate/create_test_db.py @@ -39,5 +39,5 @@ def model(p): sampler=pyabc.SingleCoreSampler(), ) db_file = os.path.join(tempfile.gettempdir(), 'pyabc_test_migrate.db') -abc.new("sqlite:///" + db_file, observation) +abc.new('sqlite:///' + db_file, observation) abc.run(minimum_epsilon=0.1, max_nr_populations=3) diff --git a/test/migrate/test_migrate.py b/test/migrate/test_migrate.py index b0d940530..9cf12d549 100644 --- a/test/migrate/test_migrate.py +++ b/test/migrate/test_migrate.py @@ -10,19 +10,19 @@ def test_db_import(script_runner): """Import an outdated database, assert import raises, and then convert.""" - db_file = os.path.join(tempfile.gettempdir(), "pyabc_test_migrate.db") + db_file = os.path.join(tempfile.gettempdir(), 'pyabc_test_migrate.db') # this database was created with a previos pyabc version, thus # import should fail with pytest.raises(AssertionError): - pyabc.History("sqlite:///" + db_file) + pyabc.History('sqlite:///' + db_file) # call the migration script ret = script_runner.run('abc-migrate', '--src', db_file, '--dst', db_file) assert ret.success # now it should work - h = pyabc.History("sqlite:///" + db_file) + h = pyabc.History('sqlite:///' + db_file) h.get_weighted_sum_stats() # remove file diff --git a/test/petab/test_petab.py b/test/petab/test_petab.py index 6ead7afae..3e6a050ed 100644 --- a/test/petab/test_petab.py +++ b/test/petab/test_petab.py @@ -34,7 +34,7 @@ def prior_specs(request): C.LOWER_BOUND: [np.nan], C.UPPER_BOUND: [np.nan], C.OBJECTIVE_PRIOR_TYPE: [prior_type], - C.OBJECTIVE_PRIOR_PARAMETERS: [f"{var1};{var2}"], + C.OBJECTIVE_PRIOR_PARAMETERS: [f'{var1};{var2}'], } ) else: @@ -115,7 +115,7 @@ def test_petab_prior(prior_specs): mean_th = var1 var_th = 2 * var2**2 else: - raise ValueError(f"Unexpected prior type: {prior_type}") + raise ValueError(f'Unexpected prior type: {prior_type}') # multiplicative tolerance of sample vs ground truth variables tol = 0.8 @@ -138,7 +138,7 @@ def test_petab_prior(prior_specs): elif prior_type == C.LOG_LAPLACE: distr = scipy.stats.loglaplace(c=1 / var2, scale=np.exp(var1)) else: - raise ValueError(f"Unexpected prior type: {prior_type}") + raise ValueError(f'Unexpected prior type: {prior_type}') # perform KS test _, p_value = scipy.stats.kstest(samples, distr.cdf) @@ -191,7 +191,7 @@ def test_get_nominal_parameters(): C.PARAMETER_SCALE_UNIFORM, C.UNIFORM, ], - C.OBJECTIVE_PRIOR_PARAMETERS: ["1;4", "1;3", "0;0.7"], + C.OBJECTIVE_PRIOR_PARAMETERS: ['1;4', '1;3', '0;0.7'], } ).set_index(C.PARAMETER_ID) @@ -236,7 +236,7 @@ def test_get_bounds(): C.PARAMETER_SCALE_UNIFORM, C.LAPLACE, ], - C.OBJECTIVE_PRIOR_PARAMETERS: ["1;4", "1;3", "0;0.7", "1;4"], + C.OBJECTIVE_PRIOR_PARAMETERS: ['1;4', '1;3', '0;0.7', '1;4'], } ).set_index(C.PARAMETER_ID) @@ -285,15 +285,15 @@ def test_get_bounds(): ) -benchmark_dir = "doc/examples/tmp/benchmark-models-petab" +benchmark_dir = 'doc/examples/tmp/benchmark-models-petab' def download_benchmark_repo(): # download archive if not os.path.exists(benchmark_dir): git.Repo.clone_from( - "https://github.com/benchmarking-initiative" - "/benchmark-models-petab.git", + 'https://github.com/benchmarking-initiative' + '/benchmark-models-petab.git', benchmark_dir, depth=1, ) @@ -343,7 +343,7 @@ def test_pickling(): model_re = pickle.loads(pickle.dumps(model)) p = importer.get_nominal_parameters() - assert np.isclose(model(p)["llh"], model_re(p)["llh"]) + assert np.isclose(model(p)['llh'], model_re(p)['llh']) def test_pipeline(): diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index 498441dbe..7f4e93019 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -22,8 +22,8 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -MODEL_TYPE = "sbml" -PETAB_VERSION = "v1.0.0" +MODEL_TYPE = 'sbml' +PETAB_VERSION = 'v1.0.0' @pytest.fixture(params=petabtests.get_cases(MODEL_TYPE, version=PETAB_VERSION)) @@ -36,11 +36,11 @@ def test_petab_suite(case): """Execute a given case from the PEtab test suite.""" try: execute_case(case) - logger.info(f"Case {case} passed") + logger.info(f'Case {case} passed') except Skipped: - logger.info(f"Case {case} skipped") + logger.info(f'Case {case} skipped') except Exception as e: - logger.error(f"Case {case} failed") + logger.error(f'Case {case} failed') raise e @@ -49,12 +49,12 @@ def execute_case(case): try: _execute_case(case) except Exception as e: - if isinstance(e, NotImplementedError) or "timepoint specific" in str( + if isinstance(e, NotImplementedError) or 'timepoint specific' in str( e ): logger.info( - f"Case {case} expectedly failed. Required functionality is " - f"not implemented: {e}" + f'Case {case} expectedly failed. Required functionality is ' + f'not implemented: {e}' ) pytest.skip(str(e)) else: @@ -64,7 +64,7 @@ def execute_case(case): def _execute_case(case): """Run a single PEtab test suite case""" case = petabtests.test_id_str(case) - logger.info(f"Case {case}") + logger.info(f'Case {case}') # load solution solution = petabtests.load_solution( @@ -93,7 +93,7 @@ def _execute_case(case): # use distinct model IDs for each test case since we cannot import different # models with the same name in a single python session - model_name = f"petab_{MODEL_TYPE}_test_case_{case}_{PETAB_VERSION.replace('.', '_')}" + model_name = f'petab_{MODEL_TYPE}_test_case_{case}_{PETAB_VERSION.replace(".", "_")}' amici_model = amici.petab.petab_import.import_petab_problem( petab_problem=petab_problem, @@ -139,22 +139,21 @@ def _execute_case(case): # log matches logger.log( logging.INFO if chi2s_match else logging.ERROR, - f"CHI2: simulated: {chi2}, expected: {gt_chi2}," - f" match = {chi2s_match}", + f'CHI2: simulated: {chi2}, expected: {gt_chi2}, match = {chi2s_match}', ) logger.log( logging.INFO if simulations_match else logging.ERROR, - f"LLH: simulated: {llh}, expected: {gt_llh}, " f"match = {llhs_match}", + f'LLH: simulated: {llh}, expected: {gt_llh}, match = {llhs_match}', ) logger.log( logging.INFO if simulations_match else logging.ERROR, - f"Simulations: match = {simulations_match}", + f'Simulations: match = {simulations_match}', ) if not all([llhs_match, chi2s_match, simulations_match]): - logger.error(f"Case {case} failed.") + logger.error(f'Case {case} failed.') raise AssertionError( - f"Case {case}: Test results do not match " "expectations" + f'Case {case}: Test results do not match expectations' ) - logger.info(f"Case {case} passed.") + logger.info(f'Case {case} passed.') diff --git a/test/visserver/test_dash_server.py b/test/visserver/test_dash_server.py index 8ba21ac23..6b9870a97 100644 --- a/test/visserver/test_dash_server.py +++ b/test/visserver/test_dash_server.py @@ -13,7 +13,7 @@ def call_dash_server(ret_value): def test_dash_server(): - ret_value = multiprocessing.Value("i", 1, lock=False) + ret_value = multiprocessing.Value('i', 1, lock=False) t1 = multiprocessing.Process(target=call_dash_server, args=[ret_value]) t1.start() time.sleep(3) diff --git a/test/visualization/test_base_viz.py b/test/visualization/test_base_viz.py index 86b61ff43..7f4ccf2d1 100644 --- a/test/visualization/test_base_viz.py +++ b/test/visualization/test_base_viz.py @@ -8,7 +8,7 @@ import pyabc -db_path = "sqlite:///" + tempfile.mkstemp(suffix='.db')[1] +db_path = 'sqlite:///' + tempfile.mkstemp(suffix='.db')[1] log_files = [] histories = [] labels = [] @@ -43,7 +43,7 @@ def setup_module(): sampler = pyabc.sampler.MulticoreEvalParallelSampler(n_procs=2) for _ in range(n_history): - log_file = tempfile.mkstemp(suffix=".json")[1] + log_file = tempfile.mkstemp(suffix='.json')[1] log_files.append(log_file) distance = pyabc.AdaptivePNormDistance(p=2, scale_log_file=log_file) @@ -57,12 +57,12 @@ def setup_module(): history = pyabc.History(db_path) history.id = j + 1 histories.append(history) - labels.append("Some run " + str(j)) + labels.append('Some run ' + str(j)) def teardown_module(): """Tear down module. Called after all tests here.""" - os.remove(db_path[len("sqlite:///") :]) + os.remove(db_path[len('sqlite:///') :]) for log_file in log_files: os.remove(log_file) @@ -195,7 +195,7 @@ def test_kdes(): history = histories[0] df, w = history.get_distribution(m=0, t=None) pyabc.visualization.plot_kde_1d( - df, w, x='p0', xmin=limits['p0'][0], xmax=limits['p0'][1], label="PDF" + df, w, x='p0', xmin=limits['p0'][0], xmax=limits['p0'][1], label='PDF' ) pyabc.visualization.plot_kde_2d(df, w, x='p0', y='p1') pyabc.visualization.plot_kde_matrix(df, w) @@ -220,9 +220,9 @@ def test_contours(): for kwargs in [ {}, { - "show_clabel": True, - "show_legend": True, - "clabel_kwargs": {"inline": False}, + 'show_clabel': True, + 'show_legend': True, + 'clabel_kwargs': {'inline': False}, }, ]: # lowlevel interfaces @@ -357,23 +357,23 @@ def test_distance_weights(): def test_sensitivity_sankey(): """Test pyabc.visualization.plot_sensitivity_sankey`""" - sigmas = {"p1": 0.1} + sigmas = {'p1': 0.1} def model(p): return { - "y1": p["p1"] + 1 + sigmas["p1"] * np.random.normal(), - "y2": 2 + 0.1 * np.random.normal(size=3), + 'y1': p['p1'] + 1 + sigmas['p1'] * np.random.normal(), + 'y2': 2 + 0.1 * np.random.normal(size=3), } - gt_par = {"p1": 3} + gt_par = {'p1': 3} - data = {"y1": gt_par["p1"] + 1, "y2": 2 * np.ones(shape=3)} + data = {'y1': gt_par['p1'] + 1, 'y2': 2 * np.ones(shape=3)} - prior_bounds = {"p1": (0, 10)} + prior_bounds = {'p1': (0, 10)} prior = pyabc.Distribution( **{ - key: pyabc.RV("uniform", lb, ub - lb) + key: pyabc.RV('uniform', lb, ub - lb) for key, (lb, ub) in prior_bounds.items() }, ) @@ -381,9 +381,9 @@ def model(p): total_sims = 1000 # tmp files - db_file = tempfile.mkstemp(suffix=".db")[1] - scale_log_file = tempfile.mkstemp(suffix=".json")[1] - info_log_file = tempfile.mkstemp(suffix=".json")[1] + db_file = tempfile.mkstemp(suffix='.db')[1] + scale_log_file = tempfile.mkstemp(suffix='.json')[1] + info_log_file = tempfile.mkstemp(suffix='.json')[1] info_sample_log_file = tempfile.mkstemp()[1] distance = pyabc.InfoWeightedPNormDistance( @@ -397,7 +397,7 @@ def model(p): ) abc = pyabc.ABCSMC(model, prior, distance, population_size=100) - h = abc.new(db="sqlite:///" + db_file, observed_sum_stat=data) + h = abc.new(db='sqlite:///' + db_file, observed_sum_stat=data) abc.run(max_total_nr_simulations=total_sims) pyabc.visualization.plot_sensitivity_sankey( @@ -412,7 +412,7 @@ def test_colors(): """Some basic tests regarding color codes.""" for basecolor in pyabc.visualization.colors.BASECOLORS: for level in pyabc.visualization.colors.LEVELS: - color = getattr(pyabc.visualization.colors, f"{basecolor}{level}") + color = getattr(pyabc.visualization.colors, f'{basecolor}{level}') assert len(color) == 7 - assert color[0] == "#" + assert color[0] == '#' assert int(color[1:], 16) diff --git a/test/visualization/test_lookahead_viz.py b/test/visualization/test_lookahead_viz.py index ef807482f..963e89515 100644 --- a/test/visualization/test_lookahead_viz.py +++ b/test/visualization/test_lookahead_viz.py @@ -30,7 +30,7 @@ def model(p): } ) -db_path = "sqlite:///" + tempfile.mkstemp(suffix='.db')[1] +db_path = 'sqlite:///' + tempfile.mkstemp(suffix='.db')[1] sampler_file = tempfile.mkstemp(suffix='.csv')[1] distance = pyabc.PNormDistance(p=2) @@ -49,7 +49,7 @@ def model(p): def teardown_module(): """Tear down module. Called after all tests here.""" - os.remove(db_path[len("sqlite:///") :]) + os.remove(db_path[len('sqlite:///') :]) os.remove(sampler_file) diff --git a/test/visualization/test_plotly.py b/test/visualization/test_plotly.py index a6a3669c7..33af56d79 100644 --- a/test/visualization/test_plotly.py +++ b/test/visualization/test_plotly.py @@ -9,7 +9,7 @@ import pyabc -db_path = "sqlite:///" + tempfile.mkstemp(suffix='.db')[1] +db_path = 'sqlite:///' + tempfile.mkstemp(suffix='.db')[1] log_files = [] histories = [] labels = [] @@ -44,7 +44,7 @@ def setup_module(): sampler = pyabc.sampler.MulticoreEvalParallelSampler(n_procs=2) for _ in range(n_history): - log_file = tempfile.mkstemp(suffix=".json")[1] + log_file = tempfile.mkstemp(suffix='.json')[1] log_files.append(log_file) distance = pyabc.AdaptivePNormDistance(p=2, scale_log_file=log_file) @@ -58,12 +58,12 @@ def setup_module(): history = pyabc.History(db_path) history.id = j + 1 histories.append(history) - labels.append("Some run " + str(j)) + labels.append('Some run ' + str(j)) def teardown_module(): """Tear down module. Called after all tests here.""" - os.remove(db_path[len("sqlite:///") :]) + os.remove(db_path[len('sqlite:///') :]) for log_file in log_files: os.remove(log_file) diff --git a/test/visualization/test_visserver.py b/test/visualization/test_visserver.py index 19c20312c..92e159b3b 100644 --- a/test/visualization/test_visserver.py +++ b/test/visualization/test_visserver.py @@ -9,7 +9,7 @@ import pyabc import pyabc.visserver.server_flask as server -db_path = "sqlite:///" + tempfile.mkstemp(suffix='.db')[1] +db_path = 'sqlite:///' + tempfile.mkstemp(suffix='.db')[1] def setup_module(): @@ -47,14 +47,14 @@ def teardown_module(): Called once after all tests. """ - os.remove(db_path[len("sqlite:///") :]) + os.remove(db_path[len('sqlite:///') :]) @pytest.fixture def client(): """A fake server client.""" history = pyabc.History(db_path) - server.app.config["HISTORY"] = history + server.app.config['HISTORY'] = history with server.app.test_client() as client: yield client diff --git a/test_performance/test_parameter.py b/test_performance/test_parameter.py index 47a871cac..9d923f998 100644 --- a/test_performance/test_parameter.py +++ b/test_performance/test_parameter.py @@ -29,25 +29,25 @@ def test_parameter_dict_numpy_conversion(): for _ in range(n): pars.append(dict(zip(keys, np.random.randn(nkey)))) time_create_dict = time() - start - print(f"Time to create dict: {time_create_dict}") + print(f'Time to create dict: {time_create_dict}') # convert to pandas start = time() pars_pd = [pd.Series(par) for par in pars] time_convert_pd = time() - start - print(f"Time to convert to pd.Series: {time_convert_pd}") + print(f'Time to convert to pd.Series: {time_convert_pd}') # mode 1: extract keys in pandas start = time() pars_np_1 = [np.array(par[keys]) for par in pars_pd] time_np_1 = time() - start - print(f"Time to extract to numpy via pandas keys: {time_np_1}") + print(f'Time to extract to numpy via pandas keys: {time_np_1}') # mode 2: use cached indices start = time() pars_np_2 = [np.array(par)[ixs] for par in pars_pd] time_np_2 = time() - start - print(f"Time to extract to numpy via cached indices: {time_np_2}") + print(f'Time to extract to numpy via cached indices: {time_np_2}') # This is a lot faster than mode 1 # mode 3: use cached indices and pandas to_numpy @@ -55,8 +55,8 @@ def test_parameter_dict_numpy_conversion(): pars_np_3 = [par.to_numpy()[ixs] for par in pars_pd] time_np_3 = time() - start print( - f"Time to extract to numpy via cached indices and to_numpy(): " - f"{time_np_3}" + f'Time to extract to numpy via cached indices and to_numpy(): ' + f'{time_np_3}' ) # This is a little faster than mode 2 (probably just by avoiding copying) @@ -64,7 +64,7 @@ def test_parameter_dict_numpy_conversion(): start = time() pars_np_4 = [np.array([par[key] for key in keys]) for par in pars] time_np_4 = time() - start - print(f"Time to extract directly from dict: {time_np_4}") + print(f'Time to extract directly from dict: {time_np_4}') # Taking into account the time to convert to pd, this is faster, however # this may change if the values need to be extracted as an array multiple # times. @@ -118,7 +118,7 @@ def test_sample_selection(): for _ in range(nsample): _ = df.sample(weights=w).iloc[0] time_pandas = time() - start - print(f"Time using pandas sample: {time_pandas}") + print(f'Time using pandas sample: {time_pandas}') # using numpy start = time() @@ -127,7 +127,7 @@ def test_sample_selection(): sample_ind = np.random.choice(arr, p=w, replace=True) _ = df.iloc[sample_ind] time_numpy = time() - start - print(f"Time using numpy choice: {time_numpy}") + print(f'Time using numpy choice: {time_numpy}') # using numpy with caching start = time() @@ -136,7 +136,7 @@ def test_sample_selection(): sample_ind = np.random.choice(arr, p=w, replace=True) _ = df.iloc[sample_ind] time_numpy_cached = time() - start - print(f"Time using numpy choice cache: {time_numpy_cached}") + print(f'Time using numpy choice cache: {time_numpy_cached}') # check times as expected tol = 0.9 diff --git a/test_performance/test_random_choice.py b/test_performance/test_random_choice.py index 98d537ae7..becae889d 100644 --- a/test_performance/test_random_choice.py +++ b/test_performance/test_random_choice.py @@ -19,14 +19,14 @@ def test_fast_random_choice(): for w in ws: np.random.choice(len(w), p=w) time_numpy = time() - start - print(f"Time numpy: {time_numpy}") + print(f'Time numpy: {time_numpy}') np.random.seed(0) start = time() for w in ws: fast_random_choice(w) time_fast = time() - start - print(f"Time fast_random_choice: {time_fast}") + print(f'Time fast_random_choice: {time_fast}') assert time_fast < time_numpy From 336619260e6d2cc5cc9f27a5f554a7f158a6581a Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 13:51:27 +0100 Subject: [PATCH 09/55] fix ruff --- pyabc/populationstrategy/populationstrategy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyabc/populationstrategy/populationstrategy.py b/pyabc/populationstrategy/populationstrategy.py index d03575b80..3a528c45c 100644 --- a/pyabc/populationstrategy/populationstrategy.py +++ b/pyabc/populationstrategy/populationstrategy.py @@ -41,8 +41,7 @@ class directly. Subclasses must override the `update` method. def __init__(self, nr_calibration_particles: int = None): self.nr_calibration_particles = nr_calibration_particles - @abstractmethod - def update( + def update( # noqa: B027 self, transitions: list[Transition], model_weights: np.ndarray, @@ -60,6 +59,7 @@ def update( t: Time to adapt for. """ + pass @abstractmethod def __call__(self, t: int = None) -> int: From 85ff99246fe6e1baf3e859088856b843aacdddd4 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 13:53:55 +0100 Subject: [PATCH 10/55] fix petab --- tox.ini | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tox.ini b/tox.ini index 4645eb307..70e09555c 100644 --- a/tox.ini +++ b/tox.ini @@ -102,17 +102,6 @@ description = Test external model simulators (Julia, COPASI) [testenv:petab] -extras = - test - petab - amici -commands = - python -m pytest --cov=pyabc --cov-report=xml --cov-append \ - test/petab -s -description = - Test PEtab support - -[testenv:petabtests] extras = test petab @@ -123,7 +112,7 @@ commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s description = - Test PEtab test suite + Test PEtab support [testenv:mac] extras = test From 6e75d9b9763d3ae8eaf71fee2447d71d40fa35bc Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 14:31:21 +0100 Subject: [PATCH 11/55] fix tox --- .github/workflows/ci.yml | 3 +-- .github/workflows/install_deps.sh | 18 ------------- pyproject.toml | 6 ++++- tox.ini | 43 ++++--------------------------- 4 files changed, 11 insertions(+), 59 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40db6e806..d8fc794a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,11 +217,10 @@ jobs: - name: Preview Ruff linting timeout-minutes: 5 run: tox -e lint - continue-on-error: true - name: Run quality checks timeout-minutes: 10 - run: tox -e project,flake8 + run: tox -e project - name: Build docs timeout-minutes: 10 diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index f8bc28f8d..68553bbc8 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -154,22 +154,6 @@ install_doc_tools() { fi } -# Julia installation -install_julia() { - log_info "Installing Julia..." - - if is_macos; then - brew install julia - else - # Install Julia via juliaup (recommended approach) - curl -fsSL https://install.julialang.org | sh -s -- -y - export PATH="$HOME/.juliaup/bin:$PATH" - fi - - # Initialize PyJulia - python -c "import julia; julia.install()" || log_warn "PyJulia initialization failed (non-critical)" -} - # Development tools install_dev_tools() { log_info "Installing development tools..." @@ -191,7 +175,6 @@ install_all() { install_r install_amici install_doc_tools - install_julia install_dev_tools } @@ -233,7 +216,6 @@ main() { R) install_r ;; amici) install_amici ;; doc) install_doc_tools ;; - julia) install_julia ;; dev) install_dev_tools ;; all) install_all ;; help|--help|-h) diff --git a/pyproject.toml b/pyproject.toml index 90725c362..d237b5b95 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,8 +85,12 @@ julia = [ "pygments>=2.6.1", ] copasi = ["copasi-basico>=0.8"] -ot = ["pot>=0.7.0"] +ot = [ + "pot>=0.7.0", + "cython>=0.29.0", +] petab = ["petab>=0.2.0"] +test-petab = ["petabtests>=0.0.1"] amici = ["amici>=0.18.0"] yaml2sbml = ["yaml2sbml>=0.2.1"] migrate = ["alembic>=1.5.4"] diff --git a/tox.ini b/tox.ini index 70e09555c..3dc69367d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] requires = tox>=4 envlist = - clean base visualization external-R @@ -50,10 +49,8 @@ extras = R pyarrow autograd + ot commands = - python -m pip install --upgrade pip - python -m pip install cython numpy - python -m pip install pot python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base test_performance -s description = @@ -106,8 +103,7 @@ extras = test petab amici -deps = - petabtests>=0.0.1 + petabtests commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s @@ -132,7 +128,6 @@ deps = pytest-console-scripts>=1.4.1 commands = python -m pip install --upgrade pip - python -m pip install "setuptools>=68.0.0" "wheel>=0.36.2" # install an old pyabc version python -m pip install pyabc==0.10.13 numpy==1.23.5 pandas==1.5.0 sqlalchemy==1.4.48 python test/migrate/create_test_db.py @@ -145,11 +140,10 @@ description = [testenv:notebooks1] allowlist_externals = bash -extras = examples +extras = + examples + ot commands = - python -m pip install --upgrade pip - python -m pip install cython numpy - python -m pip install pot bash test/run_notebooks.sh 1 description = Run notebooks (set 1) @@ -193,33 +187,6 @@ commands = description = Run linting with ruff -[testenv:flake8] -skip_install = true -deps = - black>=24.0.0 - flake8>=7.0.0 - flake8-bandit>=4.1.1 - flake8-bugbear>=24.0.0 - flake8-colors>=0.1.9 - flake8-comprehensions>=3.14.0 - flake8-print>=5.0.0 - flake8-black>=0.3.6 - flake8-isort>=6.1.0 -commands = - flake8 pyabc test test_performance -description = - Run flake8 (+ plugins) - legacy linting - -[testenv:format] -skip_install = true -deps = - ruff>=0.8.0 -commands = - ruff check --fix pyabc test test_performance - ruff format pyabc test test_performance -description = - Auto-format code with ruff - [testenv:doc] extras = doc From 34790988e5ebed8a0f5745b33d9fea9662f0c7b6 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 14:34:46 +0100 Subject: [PATCH 12/55] fix petab test --- .github/workflows/ci.yml | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8fc794a5..024f73429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,7 +214,7 @@ jobs: python -m pip install -U pip python -m pip install tox - - name: Preview Ruff linting + - name: Ruff linting timeout-minutes: 5 run: tox -e lint diff --git a/tox.ini b/tox.ini index 3dc69367d..f08a622a2 100644 --- a/tox.ini +++ b/tox.ini @@ -103,7 +103,7 @@ extras = test petab amici - petabtests + test-petab commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s From e42102ccb8bcc7eb56a5b59e14275dee9ffb359a Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 14:48:09 +0100 Subject: [PATCH 13/55] update ci --- .github/workflows/ci.yml | 292 ++++++++++++--------------------------- 1 file changed, 87 insertions(+), 205 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 024f73429..0a0fb89dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,233 +5,115 @@ on: branches: [main, develop] pull_request: schedule: - # Monday at 15:18 UTC - cron: "18 15 * * MON" jobs: - base: - runs-on: ubuntu-latest + tests: + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - # Supported stable versions - python-version: ["3.13", "3.12", "3.11", "3.10"] + include: + # Fast PR matrix + - os: ubuntu-latest + python: "3.13" + toxenv: base + - os: ubuntu-latest + python: "3.12" + toxenv: base + - os: ubuntu-latest + python: "3.11" + toxenv: base + - os: ubuntu-latest + python: "3.10" + toxenv: base + - os: ubuntu-latest + python: "3.11" + toxenv: visualization + + # macOS sanity + - os: macos-latest + python: "3.11" + toxenv: mac + + # Quality + - os: ubuntu-latest + python: "3.11" + toxenv: lint + - os: ubuntu-latest + python: "3.11" + toxenv: project + - os: ubuntu-latest + python: "3.11" + toxenv: doc + - os: ubuntu-latest + python: "3.11" + toxenv: migrate + + # Heavier + - os: ubuntu-latest + python: "3.11" + toxenv: external-R + heavy: true + - os: ubuntu-latest + python: "3.11" + toxenv: external-other-simulators + heavy: true + - os: ubuntu-latest + python: "3.11" + toxenv: petab + heavy: true + - os: ubuntu-latest + python: "3.11" + toxenv: notebooks1 + heavy: true + - os: ubuntu-latest + python: "3.11" + toxenv: notebooks2 + heavy: true steps: - - name: Check out repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python }} cache: pip - - name: Install dependencies - run: .github/workflows/install_deps.sh base R - - - name: Run tests - timeout-minutes: 15 - run: tox -e base - - - name: Run visualization tests - timeout-minutes: 5 - run: tox -e visualization - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - - external: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip + # Gate heavy items (example: only schedule or push to main) + - name: Skip heavy in PRs + if: ${{ matrix.heavy == true && github.event_name == 'pull_request' }} + run: | + echo "Skipping heavy job on PR" + exit 0 + # Install extra runtimes only when needed - name: Install Julia + if: ${{ startsWith(matrix.toxenv, 'external-') }} uses: julia-actions/setup-julia@v2 with: version: "1.11" - - name: Install dependencies - run: .github/workflows/install_deps.sh base R - - - name: Run external (R) tests - timeout-minutes: 20 - run: tox -e external-R - - - name: Run external (Julia/COPASI) tests - timeout-minutes: 20 - run: tox -e external-other-simulators - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - - petab: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: .github/workflows/install_deps.sh amici - - - name: Run tests - timeout-minutes: 30 - run: tox -e petab - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - - mac: - runs-on: macos-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: .github/workflows/install_deps.sh base - - - name: Run tests - timeout-minutes: 15 - run: tox -e mac - - - name: Upload coverage - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage.xml - - notebooks1: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: .github/workflows/install_deps.sh base - - - name: Run notebooks - timeout-minutes: 20 - run: tox -e notebooks1 - - notebooks2: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: .github/workflows/install_deps.sh R amici - - - name: Run notebooks - timeout-minutes: 20 - run: tox -e notebooks2 - - quality: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.11"] - - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Prepare Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - name: Install dependencies run: | - .github/workflows/install_deps.sh doc - python -m pip install -U pip - python -m pip install tox - - - name: Ruff linting - timeout-minutes: 5 - run: tox -e lint - - - name: Run quality checks - timeout-minutes: 10 - run: tox -e project - - - name: Build docs - timeout-minutes: 10 - run: tox -e doc - - - name: Test migration - timeout-minutes: 10 - run: tox -e migrate - + # Map toxenv -> deps groups (keep your script) + case "${{ matrix.toxenv }}" in + base|visualization|mac|notebooks1) .github/workflows/install_deps.sh base ;; + notebooks2) .github/workflows/install_deps.sh R amici ;; + petab) .github/workflows/install_deps.sh amici ;; + external-*) .github/workflows/install_deps.sh base R ;; + lint|project|doc|migrate) .github/workflows/install_deps.sh doc ;; + esac + python -m pip install -U pip tox + + - name: Run tox + run: tox -e ${{ matrix.toxenv }} + + # If each tox env generates coverage.xml, include flags; otherwise combine explicitly - name: Upload coverage + if: ${{ always() && matrix.os == 'ubuntu-latest' }} # optional uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml + flags: ${{ matrix.toxenv }} From 53aeb988b3d31cd71b4b2fc624932f0b5755b86e Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:02:56 +0100 Subject: [PATCH 14/55] update r dep --- test/base/test_bytesstorage.py | 11 ++++++++--- test/base/test_storage.py | 5 +++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/test/base/test_bytesstorage.py b/test/base/test_bytesstorage.py index 72c81f299..7ab964cbc 100644 --- a/test/base/test_bytesstorage.py +++ b/test/base/test_bytesstorage.py @@ -4,9 +4,6 @@ import numpy as np import pandas as pd import pytest -import rpy2.robjects as robjects -from rpy2.robjects import pandas2ri, r -from rpy2.robjects.conversion import localconverter import pyabc from pyabc.storage.bytes_storage import from_bytes, to_bytes @@ -40,6 +37,8 @@ ] ) def object_(request): + from rpy2.robjects import r + par = request.param if par == 'empty': return pd.DataFrame() @@ -114,6 +113,10 @@ def object_(request): def test_storage(object_): + import rpy2.robjects as robjects + from rpy2.robjects import pandas2ri + from rpy2.robjects.conversion import localconverter + serial = to_bytes(object_) assert isinstance(serial, bytes) @@ -143,6 +146,8 @@ def test_storage(object_): def _check_type(object_, rebuilt): + import rpy2.robjects as robjects + # r objects are converted to pd.DataFrame if isinstance(object_, robjects.DataFrame): assert isinstance(rebuilt, pd.DataFrame) diff --git a/test/base/test_storage.py b/test/base/test_storage.py index bbc1ace50..79a0d8842 100644 --- a/test/base/test_storage.py +++ b/test/base/test_storage.py @@ -7,8 +7,6 @@ import pandas as pd import pyarrow import pytest -from rpy2.robjects import pandas2ri, r -from rpy2.robjects.conversion import localconverter import pyabc from pyabc import History @@ -242,6 +240,9 @@ def test_single_particle_save_load_np_int64(history: History): def test_sum_stats_save_load(history: History): + from rpy2.robjects import pandas2ri, r + from rpy2.robjects.conversion import localconverter + arr = np.random.rand(10) arr2 = np.random.rand(10, 2) particle_list = [ From a05c9b46a0889c24d9685d908922c84e5ad250e8 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:17:41 +0100 Subject: [PATCH 15/55] update r dep --- .github/workflows/ci.yml | 13 --- .github/workflows/install_deps.sh | 162 ++++++------------------------ .pre-commit-config.yaml | 4 +- 3 files changed, 35 insertions(+), 144 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a0fb89dc..29d10082f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,23 +54,18 @@ jobs: - os: ubuntu-latest python: "3.11" toxenv: external-R - heavy: true - os: ubuntu-latest python: "3.11" toxenv: external-other-simulators - heavy: true - os: ubuntu-latest python: "3.11" toxenv: petab - heavy: true - os: ubuntu-latest python: "3.11" toxenv: notebooks1 - heavy: true - os: ubuntu-latest python: "3.11" toxenv: notebooks2 - heavy: true steps: - uses: actions/checkout@v4 @@ -80,13 +75,6 @@ jobs: python-version: ${{ matrix.python }} cache: pip - # Gate heavy items (example: only schedule or push to main) - - name: Skip heavy in PRs - if: ${{ matrix.heavy == true && github.event_name == 'pull_request' }} - run: | - echo "Skipping heavy job on PR" - exit 0 - # Install extra runtimes only when needed - name: Install Julia if: ${{ startsWith(matrix.toxenv, 'external-') }} @@ -96,7 +84,6 @@ jobs: - name: Install dependencies run: | - # Map toxenv -> deps groups (keep your script) case "${{ matrix.toxenv }}" in base|visualization|mac|notebooks1) .github/workflows/install_deps.sh base ;; notebooks2) .github/workflows/install_deps.sh R amici ;; diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 68553bbc8..56685363e 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -1,176 +1,97 @@ #!/usr/bin/env bash -# CI dependency installation script for pyABC -# Supports Ubuntu 24.04 and macOS set -euo pipefail -# Color output helpers readonly RED='\033[0;31m' readonly GREEN='\033[0;32m' readonly YELLOW='\033[1;33m' -readonly NC='\033[0m' # No Color +readonly NC='\033[0m' -log_info() { - echo -e "${GREEN}[INFO]${NC} $*" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $*" -} +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*"; } -log_error() { - echo -e "${RED}[ERROR]${NC} $*" -} - -# Update pip, wheel, and tox -log_info "Updating pip, wheel, and tox..." -python -m pip install --upgrade pip wheel tox +is_macos() { [[ "$(uname -s)" == "Darwin" ]]; } -# Platform detection -is_macos() { - [[ "$(uname -s)" == "Darwin" ]] -} - -# APT management (Ubuntu/Debian) _APT_UPDATED=0 - apt_update_once() { if [[ "${_APT_UPDATED}" == "0" ]]; then - export _APT_UPDATED=1 + _APT_UPDATED=1 log_info "Updating apt package lists..." sudo apt-get update -y fi } - apt_install() { apt_update_once log_info "Installing apt packages: $*" sudo apt-get install -y --no-install-recommends "$@" } -# Base dependencies (Redis) +export_env_var() { + local key="$1" + local value="$2" + export "${key}=${value}" + if [[ -n "${GITHUB_ENV:-}" ]]; then + echo "${key}=${value}" >> "$GITHUB_ENV" + fi +} + +log_info "Updating pip, wheel, tox..." +python -m pip install --upgrade pip wheel tox + install_base() { log_info "Installing base dependencies..." if is_macos; then brew install redis else apt_install redis-server - # Ensure redis-server is running sudo service redis-server start || true fi } -# R installation from source -build_and_install_r_from_source() { - local R_VER="${1:-4.4.2}" - local TAR="R-${R_VER}.tar.gz" - local URL="https://cran.r-project.org/src/base/R-4/${TAR}" - - log_info "Building R ${R_VER} from source..." - - # Build dependencies for Ubuntu 24.04 - apt_install \ - build-essential gfortran wget ca-certificates \ - libreadline-dev libx11-dev libxt-dev \ - libpng-dev libjpeg-dev libcairo2-dev libtiff-dev \ - libglib2.0-dev liblzma-dev libbz2-dev libzstd-dev zlib1g-dev \ - libcurl4-openssl-dev libssl-dev libxml2-dev \ - libpcre2-dev libicu-dev \ - texinfo texlive texlive-fonts-extra texlive-latex-extra - - pushd /tmp >/dev/null - - log_info "Downloading R source..." - wget -q "${URL}" -O "${TAR}" - - log_info "Extracting R source..." - tar -xzf "${TAR}" - cd "R-${R_VER}" - - log_info "Configuring R build..." - ./configure --enable-R-shlib --with-blas --with-lapack --prefix=/usr/local - - log_info "Compiling R (this may take several minutes)..." - make -j"$(nproc)" - - log_info "Installing R..." - sudo make install - - # Verify installation - if command -v R >/dev/null 2>&1; then - log_info "R successfully installed: $(R --version | head -n1)" - else - log_error "R installation verification failed" - popd >/dev/null - return 1 - fi - - popd >/dev/null -} - -# R installation install_r() { log_info "Installing R..." if is_macos; then brew install r else - # Ubuntu: compile from source for better compatibility - build_and_install_r_from_source "4.4.2" + # Prefer distro packages in CI + apt_install r-base r-base-dev + # Make R shared libs discoverable for the rest of the job + export_env_var LD_LIBRARY_PATH "${LD_LIBRARY_PATH:-/usr/lib}:/usr/lib/R/lib:/usr/local/lib/R/lib" fi - # Set LD_LIBRARY_PATH for R shared libraries - if ! is_macos; then - export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-/usr/lib}:/usr/local/lib/R/lib" - echo "export LD_LIBRARY_PATH=\"${LD_LIBRARY_PATH}\"" >> ~/.bashrc + if command -v R >/dev/null 2>&1; then + log_info "R installed: $(R --version | head -n1)" + else + log_error "R installation failed (R not on PATH)" + exit 1 fi } -# AMICI dependencies install_amici() { log_info "Installing AMICI dependencies..." - if ! is_macos; then - apt_install \ - swig \ - libatlas-base-dev \ - libhdf5-serial-dev \ - libboost-all-dev + apt_install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev fi - log_info "Installing AMICI Python package..." - # Clean install to avoid version conflicts python -m pip uninstall -y amici pyabc || true python -m pip install --upgrade "pyabc[amici]" } -# Documentation tools install_doc_tools() { log_info "Installing documentation tools..." - if is_macos; then brew install pandoc || true else - apt_update_once apt_install pandoc fi } -# Development tools install_dev_tools() { log_info "Installing development tools..." - - python -m pip install --upgrade \ - pre-commit \ - ruff \ - build \ - twine \ - pytest \ - pytest-cov \ - pytest-xdist + python -m pip install --upgrade pre-commit ruff build twine pytest pytest-cov pytest-xdist } -# All dependencies install_all() { - log_info "Installing all dependencies..." install_base install_r install_amici @@ -178,31 +99,21 @@ install_all() { install_dev_tools } -# Display usage usage() { cat < Date: Tue, 17 Feb 2026 15:28:38 +0100 Subject: [PATCH 16/55] update r dep --- .github/workflows/ci.yml | 3 ++- test/visualization/test_base_viz.py | 3 +++ tox.ini | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d10082f..59180699f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: jobs: tests: + name: ${{ matrix.toxenv }} (py${{ matrix.python }} • ${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -39,7 +40,7 @@ jobs: # Quality - os: ubuntu-latest python: "3.11" - toxenv: lint + toxenv: quality - os: ubuntu-latest python: "3.11" toxenv: project diff --git a/test/visualization/test_base_viz.py b/test/visualization/test_base_viz.py index 7f4ccf2d1..d97d9f47f 100644 --- a/test/visualization/test_base_viz.py +++ b/test/visualization/test_base_viz.py @@ -353,6 +353,7 @@ def test_distance_weights(): pyabc.visualization.plot_distance_weights( log_files, keys_as_labels=keys_as_labels ) + plt.close() def test_sensitivity_sankey(): @@ -406,6 +407,7 @@ def model(p): h=h, predictor=pyabc.predictor.LinearPredictor(), ) + plt.close() def test_colors(): @@ -416,3 +418,4 @@ def test_colors(): assert len(color) == 7 assert color[0] == '#' assert int(color[1:], 16) + plt.close() diff --git a/tox.ini b/tox.ini index f08a622a2..e4fb2cb34 100644 --- a/tox.ini +++ b/tox.ini @@ -177,7 +177,7 @@ commands = description = Check the package quality and metadata -[testenv:lint] +[testenv:quality] skip_install = true deps = ruff>=0.8.0 From d57d6ada9af65ed096e048ff0576264f636f56d3 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:31:39 +0100 Subject: [PATCH 17/55] update r dep --- pyproject.toml | 2 +- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d237b5b95..6e3aca8f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ webserver-dash = [ "dash-bootstrap-components>=1.4.2", ] pyarrow = ["pyarrow>=6.0.0"] -R = [ +r = [ "rpy2>=3.4.4", "cffi>=1.14.5", "ipython>=7.18.1", diff --git a/tox.ini b/tox.ini index e4fb2cb34..6eaaf88ba 100644 --- a/tox.ini +++ b/tox.ini @@ -46,7 +46,7 @@ setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib extras = test - R + r pyarrow autograd ot @@ -75,7 +75,7 @@ setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib extras = test - R + r commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/external/test_external.py -s @@ -154,7 +154,7 @@ setenv = allowlist_externals = bash extras = examples - R + r petab yaml2sbml amici From 6d02b858513a17840fddf4e45b8a4fa1ed15ce1d Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:37:32 +0100 Subject: [PATCH 18/55] update r dep --- .github/workflows/ci.yml | 4 ++-- test/visualization/test_base_viz.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59180699f..203ec4dd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ on: jobs: tests: - name: ${{ matrix.toxenv }} (py${{ matrix.python }} • ${{ matrix.os }}) + name: ${{ matrix.toxenv }} (${{ matrix.python }} • ${{ matrix.os }}) runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -86,7 +86,7 @@ jobs: - name: Install dependencies run: | case "${{ matrix.toxenv }}" in - base|visualization|mac|notebooks1) .github/workflows/install_deps.sh base ;; + base|visualization|mac|notebooks1) .github/workflows/install_deps.sh base R ;; notebooks2) .github/workflows/install_deps.sh R amici ;; petab) .github/workflows/install_deps.sh amici ;; external-*) .github/workflows/install_deps.sh base R ;; diff --git a/test/visualization/test_base_viz.py b/test/visualization/test_base_viz.py index d97d9f47f..94c38d733 100644 --- a/test/visualization/test_base_viz.py +++ b/test/visualization/test_base_viz.py @@ -237,7 +237,7 @@ def test_contours(): history, size=(7, 5), refval=p_true, **kwargs ) - plt.close() + plt.close() def test_credible_intervals(): From a3e5947024289a5a5e37ef31858a6a7171775d27 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:44:02 +0100 Subject: [PATCH 19/55] update r dep --- .github/workflows/install_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 56685363e..9057a0dd1 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -54,7 +54,7 @@ install_r() { brew install r else # Prefer distro packages in CI - apt_install r-base r-base-dev + apt-get update && apt-get install -y libtirpc-dev r-base r-base-dev # Make R shared libs discoverable for the rest of the job export_env_var LD_LIBRARY_PATH "${LD_LIBRARY_PATH:-/usr/lib}:/usr/lib/R/lib:/usr/local/lib/R/lib" fi From 9794de5de34ac59d4e72c768e1fec02fe6037446 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 15:45:42 +0100 Subject: [PATCH 20/55] update r dep --- .github/workflows/install_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/install_deps.sh b/.github/workflows/install_deps.sh index 9057a0dd1..d497a6f36 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -54,7 +54,7 @@ install_r() { brew install r else # Prefer distro packages in CI - apt-get update && apt-get install -y libtirpc-dev r-base r-base-dev + apt_install libtirpc-dev r-base r-base-dev # Make R shared libs discoverable for the rest of the job export_env_var LD_LIBRARY_PATH "${LD_LIBRARY_PATH:-/usr/lib}:/usr/lib/R/lib:/usr/local/lib/R/lib" fi From 992022ddbc54c275dd952632050192fca2394d38 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 16:16:58 +0100 Subject: [PATCH 21/55] update r dep --- pyabc/external/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyabc/external/base.py b/pyabc/external/base.py index 62848851a..a2cf808e6 100644 --- a/pyabc/external/base.py +++ b/pyabc/external/base.py @@ -314,7 +314,7 @@ def eval_param_limits_matrix(self, limits): lower_bound = self.sample_timing( {key_col: val_col[0], key_row: val_row[0]} ) - lower_bound = self.sample_timing( + upper_bound = self.sample_timing( {key_col: val_col[1], key_row: val_row[1]} ) time_eval_mat_df_lower.loc[[key_col], [key_row]] = lower_bound From 52addba6d5cb7e379886a777d489471e5023191a Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 16:27:51 +0100 Subject: [PATCH 22/55] update warnings --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e3aca8f4..506b661e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,11 +184,6 @@ addopts = [ ] testpaths = ["test", "test_performance"] pythonpath = ["."] -filterwarnings = [ - "error", - "ignore::DeprecationWarning", - "ignore::PendingDeprecationWarning", -] [tool.coverage.run] source = ["pyabc"] From 47e84509ac01aea2f9ff7a8eb6f5d3b3dc1cadfc Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 16:42:15 +0100 Subject: [PATCH 23/55] fix subprocess --- pyabc/external/base.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyabc/external/base.py b/pyabc/external/base.py index a2cf808e6..c68552503 100644 --- a/pyabc/external/base.py +++ b/pyabc/external/base.py @@ -129,12 +129,8 @@ def run(self, args: list[str] = None, cmd: str = None, loc: str = None): loc = self.create_loc() # redirect output - stdout = stderr = {} - with open(os.devnull, 'w') as devnull: - if not self.show_stdout: - stdout = {'stdout': devnull} - if not self.show_stderr: - stderr = {'stderr': devnull} + stdout = {} if self.show_stdout else {'stdout': subprocess.DEVNULL} + stderr = {} if self.show_stderr else {'stderr': subprocess.DEVNULL} # call try: From f543bf1b742b95a9b2569c021c7643dbbe2b6100 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 16:55:31 +0100 Subject: [PATCH 24/55] fix r2py --- pyabc/external/r/r_rpy2.py | 20 +++++++++++++------- pyabc/storage/bytes_storage.py | 7 ++++--- pyproject.toml | 2 ++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pyabc/external/r/r_rpy2.py b/pyabc/external/r/r_rpy2.py index 3ac75fae6..5beb1d539 100644 --- a/pyabc/external/r/r_rpy2.py +++ b/pyabc/external/r/r_rpy2.py @@ -19,6 +19,8 @@ pandas2ri, r, ) + from rpy2.robjects.conversion import get_conversion, localconverter + except ImportError: ListVector = conversion = r = None default_converter = numpy2ri = pandas2ri = None @@ -27,15 +29,19 @@ def _dict_to_named_list(dct): if isinstance(dct, dict | Parameter | pd.core.series.Series): dct = dict(dct.items()) - # convert numbers, numpy arrays and pandas dataframes to builtin - # types before conversion (see rpy2 #548) - with conversion.localconverter( - default_converter + pandas2ri.converter + numpy2ri.converter - ): + + # Build converter using the current conversion object + conv = get_conversion() + conv = ( + conv + default_converter + pandas2ri.converter + numpy2ri.converter + ) + + with localconverter(conv): for key, val in dct.items(): dct[key] = conversion.py2rpy(val) - r_list = ListVector(dct) - return r_list + + return ListVector(dct) + return dct diff --git a/pyabc/storage/bytes_storage.py b/pyabc/storage/bytes_storage.py index 93732fb7c..5a22590f6 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -6,12 +6,13 @@ try: import rpy2.robjects as robjects from rpy2.robjects import pandas2ri - from rpy2.robjects.conversion import localconverter + from rpy2.robjects.conversion import get_conversion, localconverter def r_to_py(object_): if isinstance(object_, robjects.DataFrame): - with localconverter(pandas2ri.converter): - py_object_ = robjects.conversion.rpy2py(object_) + conv = get_conversion() + with localconverter(conv + pandas2ri.converter): + py_object_ = conv.rpy2py(object_) return py_object_ return object_ diff --git a/pyproject.toml b/pyproject.toml index 506b661e0..5988511b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,10 +79,12 @@ r = [ "cffi>=1.14.5", "ipython>=7.18.1", "pygments>=2.6.1", + "pyarrow>=22.0.0", ] julia = [ "julia>=0.5.7", "pygments>=2.6.1", + "ipython>=7.18.1", ] copasi = ["copasi-basico>=0.8"] ot = [ From d419bb32329cd28f46ab3627451ab0d3cf787100 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:03:40 +0100 Subject: [PATCH 25/55] fix r2py conversion [skip ci] --- test/base/test_bytesstorage.py | 5 +++-- test/base/test_storage.py | 7 ++++--- test/migrate/test_migrate.py | 4 +++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/test/base/test_bytesstorage.py b/test/base/test_bytesstorage.py index 7ab964cbc..8427e82d2 100644 --- a/test/base/test_bytesstorage.py +++ b/test/base/test_bytesstorage.py @@ -115,7 +115,7 @@ def object_(request): def test_storage(object_): import rpy2.robjects as robjects from rpy2.robjects import pandas2ri - from rpy2.robjects.conversion import localconverter + from rpy2.robjects.conversion import get_conversion, localconverter serial = to_bytes(object_) assert isinstance(serial, bytes) @@ -139,7 +139,8 @@ def test_storage(object_): elif isinstance(object_, pd.Series): assert (object_.to_frame() == rebuilt).all().all() elif isinstance(object_, robjects.DataFrame): - with localconverter(pandas2ri.converter): + conv = get_conversion() + with localconverter(conv + pandas2ri.converter): assert (robjects.conversion.rpy2py(object_) == rebuilt).all().all() else: raise Exception('Could not compare') diff --git a/test/base/test_storage.py b/test/base/test_storage.py index 79a0d8842..2e32cafed 100644 --- a/test/base/test_storage.py +++ b/test/base/test_storage.py @@ -241,7 +241,7 @@ def test_single_particle_save_load_np_int64(history: History): def test_sum_stats_save_load(history: History): from rpy2.robjects import pandas2ri, r - from rpy2.robjects.conversion import localconverter + from rpy2.robjects.conversion import get_conversion, localconverter arr = np.random.rand(10) arr2 = np.random.rand(10, 2) @@ -280,12 +280,13 @@ def test_sum_stats_save_load(history: History): assert sum_stats[0]['ss1'] == 0.1 assert (sum_stats[0]['ss2'] == arr2).all() assert (sum_stats[0]['ss3'] == example_df()).all().all() - with localconverter(pandas2ri.converter): + conv = get_conversion() + with localconverter(conv + pandas2ri.converter): assert (sum_stats[0]['rdf0'] == r['iris']).all().all() assert sum_stats[1]['ss12'] == 0.11 assert (sum_stats[1]['ss22'] == arr).all() assert (sum_stats[1]['ss33'] == example_df()).all().all() - with localconverter(pandas2ri.converter): + with localconverter(conv + pandas2ri.converter): assert (sum_stats[1]['rdf'] == r['mtcars']).all().all() diff --git a/test/migrate/test_migrate.py b/test/migrate/test_migrate.py index 9cf12d549..752a405c1 100644 --- a/test/migrate/test_migrate.py +++ b/test/migrate/test_migrate.py @@ -18,7 +18,9 @@ def test_db_import(script_runner): pyabc.History('sqlite:///' + db_file) # call the migration script - ret = script_runner.run('abc-migrate', '--src', db_file, '--dst', db_file) + ret = script_runner.run( + ['abc-migrate', '--src', db_file, '--dst', db_file] + ) assert ret.success # now it should work From 4643f2feee0c103979907ae9f34645e86150cea1 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:20:43 +0100 Subject: [PATCH 26/55] fix r2py conversion --- pyabc/storage/bytes_storage.py | 4 ++++ pyabc/storage/dataframe_bytes_storage.py | 2 +- pyabc/storage/history.py | 2 +- test/base/test_sumstat.py | 4 ++-- test/run_notebooks.sh | 11 ++++++++++- 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pyabc/storage/bytes_storage.py b/pyabc/storage/bytes_storage.py index 5a22590f6..15152ebc8 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -13,6 +13,10 @@ def r_to_py(object_): conv = get_conversion() with localconverter(conv + pandas2ri.converter): py_object_ = conv.rpy2py(object_) + # Ensure factor columns are converted to strings + for col in py_object_.columns: + if pd.api.types.is_categorical_dtype(py_object_[col]): + py_object_[col] = py_object_[col].astype(str) return py_object_ return object_ diff --git a/pyabc/storage/dataframe_bytes_storage.py b/pyabc/storage/dataframe_bytes_storage.py index d3c2615f0..150a270e8 100644 --- a/pyabc/storage/dataframe_bytes_storage.py +++ b/pyabc/storage/dataframe_bytes_storage.py @@ -65,7 +65,7 @@ def df_to_bytes_json(df: pd.DataFrame) -> bytes: def df_from_bytes_json(bytes_: bytes) -> pd.DataFrame: """Pandas DataFrame from json.""" - return pd.read_json(bytes_.decode()) + return pd.read_json(StringIO(bytes_.decode())) def df_to_bytes_parquet(df: pd.DataFrame) -> bytes: diff --git a/pyabc/storage/history.py b/pyabc/storage/history.py index d5b02ae54..41177a1c3 100644 --- a/pyabc/storage/history.py +++ b/pyabc/storage/history.py @@ -82,7 +82,7 @@ def git_hash(): def create_sqlite_db_id(dir_: str = None, file_: str = 'pyabc_test.db'): """ - Convenience function to create an sqlite database identifier which + Convenience function to create a sqlite database identifier which can be understood by sqlalchemy. Parameters diff --git a/test/base/test_sumstat.py b/test/base/test_sumstat.py index 4e5138418..9acd6f9a4 100644 --- a/test/base/test_sumstat.py +++ b/test/base/test_sumstat.py @@ -187,7 +187,7 @@ def test_subsetter(): ss = rng.normal(size=(n_sample, n_y)) n_sample_half = int(n_sample / 2) # parameter set with two modes - par = np.row_stack( + par = np.vstack( ( np.array([10, 0]) + 0.1 * rng.normal(size=(n_sample_half, n_p)), np.array([-10, 0]) + 0.2 * rng.normal(size=(n_sample_half, n_p)), @@ -212,7 +212,7 @@ def test_subsetter(): # 3 components n_sample_third = int(n_sample / 3) - par = np.row_stack( + par = np.vstack( ( np.array([10, 10]) + 0.1 * rng.normal(size=(n_sample_third, n_p)), np.array([10, 0]) + 0.1 * rng.normal(size=(n_sample_third, n_p)), diff --git a/test/run_notebooks.sh b/test/run_notebooks.sh index 748d5c38d..4fee1518c 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -15,6 +15,10 @@ nbs_1=( "noise" "parameter_inference" "resuming" + "chemical_reaction" + "informative" + "look_ahead" + "sde_ion_channels" ) nbs_2=( "external_simulators" @@ -24,6 +28,11 @@ nbs_2=( "discrete_parameters" "optimal_threshold" "custom_priors" + "petab_application" + "petab_yaml2sbml" + "multiscale_agent_based" + "using_copasi" + "using_julia" ) # All notebooks @@ -59,7 +68,7 @@ done run_notebook () { # Run a notebook and raise upon failure - tempfile=$(tempfile) + tempfile=$(mktemp) echo $@ jupyter nbconvert --ExecutePreprocessor.timeout=-1 --debug \ --stdout --execute --to markdown $@ &> $tempfile From 9591ab4a2a4f5d56928c826b1deb2bd505b25e5f Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:35:02 +0100 Subject: [PATCH 27/55] fix petab.v1 --- doc/examples/petab_application.ipynb | 10 +++++----- doc/examples/petab_yaml2sbml.ipynb | 22 +++++++--------------- pyabc/storage/bytes_storage.py | 2 +- pyproject.toml | 3 ++- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index a34da7a9e..9cefed7c1 100644 --- a/doc/examples/petab_application.ipynb +++ b/doc/examples/petab_application.ipynb @@ -31,7 +31,7 @@ "outputs": [], "source": [ "import amici.petab_import\n", - "import petab\n", + "import petab.v1 as petab\n", "\n", "from pyabc.petab import AmiciPetabImporter" ] @@ -53,10 +53,10 @@ "output_type": "stream", "text": [ "Cloning into 'tmp/benchmark-models'...\n", - "remote: Enumerating objects: 3511, done.\u001b[K\n", - "remote: Counting objects: 100% (3511/3511), done.\u001b[K\n", - "remote: Compressing objects: 100% (922/922), done.\u001b[K\n", - "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001b[K\n", + "remote: Enumerating objects: 3511, done.\u001B[K\n", + "remote: Counting objects: 100% (3511/3511), done.\u001B[K\n", + "remote: Compressing objects: 100% (922/922), done.\u001B[K\n", + "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001B[K\n", "Receiving objects: 100% (3511/3511), 201.90 MiB | 20.18 MiB/s, done.\n", "Resolving deltas: 100% (2695/2695), done.\n", "Checking out files: 100% (3411/3411), done.\n" diff --git a/doc/examples/petab_yaml2sbml.ipynb b/doc/examples/petab_yaml2sbml.ipynb index 0fb05e51b..8c56f9cdd 100644 --- a/doc/examples/petab_yaml2sbml.ipynb +++ b/doc/examples/petab_yaml2sbml.ipynb @@ -26,15 +26,15 @@ ] }, { - "cell_type": "code", - "execution_count": 1, "metadata": {}, + "cell_type": "code", "outputs": [], + "execution_count": null, "source": [ "import os\n", "import sys\n", "\n", - "import amici.petab_import\n", + "import amici\n", "import numpy as np\n", "\n", "import pyabc\n", @@ -59,22 +59,14 @@ ] }, { - "cell_type": "code", - "execution_count": 2, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "YAML file is valid ✅\n" - ] - } - ], + "cell_type": "code", + "outputs": [], + "execution_count": null, "source": [ "import shutil\n", "\n", - "import petab\n", + "import petab.v1 as petab\n", "import yaml2sbml\n", "\n", "# check yaml file\n", diff --git a/pyabc/storage/bytes_storage.py b/pyabc/storage/bytes_storage.py index 15152ebc8..873a42165 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -15,7 +15,7 @@ def r_to_py(object_): py_object_ = conv.rpy2py(object_) # Ensure factor columns are converted to strings for col in py_object_.columns: - if pd.api.types.is_categorical_dtype(py_object_[col]): + if isinstance(py_object_[col], pd.CategoricalDtype): py_object_[col] = py_object_[col].astype(str) return py_object_ return object_ diff --git a/pyproject.toml b/pyproject.toml index 5988511b6..07aa04f7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ requires-python = ">=3.10" authors = [{ name = "The pyABC developers", email = "jonas.arruda@uni-bonn.de" }] maintainers = [{ name = "Jonas Arruda", email = "jonas.arruda@uni-bonn.de" }] -license = { text = "BSD-3-Clause" } +license = "BSD-3-Clause" +license-files = ["LICENSE.txt"] keywords = [ "likelihood-free", From cc3df23023fc2d70556b0c70bdeddad2c1f35f77 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:36:49 +0100 Subject: [PATCH 28/55] fix petab.v1 --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 07aa04f7d..57455abfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,6 @@ abc-migrate = "pyabc.storage.migrate:migrate" [tool.setuptools] include-package-data = true zip-safe = false -license-files = ["LICENSE.txt"] [tool.setuptools.packages.find] where = ["."] From be41466bc6cca9133f89e6073a2361ae2c750027 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:38:36 +0100 Subject: [PATCH 29/55] update toml file --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 57455abfe..e16aecded 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,6 @@ keywords = [ classifiers = [ "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", From 3e5bc4bf942cbc0ec723f99cf7f3302336654569 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 17:55:23 +0100 Subject: [PATCH 30/55] update notebooks --- doc/examples/petab_application.ipynb | 10 +++++----- pyabc/external/r/r_rpy2.py | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index 9cefed7c1..46f541dcb 100644 --- a/doc/examples/petab_application.ipynb +++ b/doc/examples/petab_application.ipynb @@ -30,7 +30,7 @@ "metadata": {}, "outputs": [], "source": [ - "import amici.petab_import\n", + "import amici\n", "import petab.v1 as petab\n", "\n", "from pyabc.petab import AmiciPetabImporter" @@ -53,10 +53,10 @@ "output_type": "stream", "text": [ "Cloning into 'tmp/benchmark-models'...\n", - "remote: Enumerating objects: 3511, done.\u001B[K\n", - "remote: Counting objects: 100% (3511/3511), done.\u001B[K\n", - "remote: Compressing objects: 100% (922/922), done.\u001B[K\n", - "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001B[K\n", + "remote: Enumerating objects: 3511, done.\u001b[K\n", + "remote: Counting objects: 100% (3511/3511), done.\u001b[K\n", + "remote: Compressing objects: 100% (922/922), done.\u001b[K\n", + "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001b[K\n", "Receiving objects: 100% (3511/3511), 201.90 MiB | 20.18 MiB/s, done.\n", "Resolving deltas: 100% (2695/2695), done.\n", "Checking out files: 100% (3411/3411), done.\n" diff --git a/pyabc/external/r/r_rpy2.py b/pyabc/external/r/r_rpy2.py index 5beb1d539..20c57c3cb 100644 --- a/pyabc/external/r/r_rpy2.py +++ b/pyabc/external/r/r_rpy2.py @@ -30,15 +30,13 @@ def _dict_to_named_list(dct): if isinstance(dct, dict | Parameter | pd.core.series.Series): dct = dict(dct.items()) - # Build converter using the current conversion object conv = get_conversion() conv = ( conv + default_converter + pandas2ri.converter + numpy2ri.converter ) with localconverter(conv): - for key, val in dct.items(): - dct[key] = conversion.py2rpy(val) + dct = {key: conv.py2rpy(val) for key, val in dct.items()} return ListVector(dct) From 66da45e67c355c2680354c39f8cf5bcb43e9a1d3 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:00:52 +0100 Subject: [PATCH 31/55] remove petabtests --- pyproject.toml | 1 - test/petab/test_petab_suite.py | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e16aecded..5f63d31a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,6 @@ ot = [ "cython>=0.29.0", ] petab = ["petab>=0.2.0"] -test-petab = ["petabtests>=0.0.1"] amici = ["amici>=0.18.0"] yaml2sbml = ["yaml2sbml>=0.2.1"] migrate = ["alembic>=1.5.4"] diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index 7f4e93019..ed6927111 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -3,7 +3,6 @@ import logging import sys -import petabtests import pytest from _pytest.outcomes import Skipped @@ -18,6 +17,15 @@ except ImportError: pass +try: + import petabtests +except ImportError: + pytest.skip( + 'PEtab test suite not available. Please install petabtests to run this ' + 'test.', + allow_module_level=True, + ) + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) From 422f8d693f73ce14de1a2f266b5b764b06c264ab Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:15:57 +0100 Subject: [PATCH 32/55] remove petabtests --- doc/examples/petab_application.ipynb | 4 ++-- doc/examples/petab_yaml2sbml.ipynb | 27 ++++++++++++++------------- pyabc/storage/bytes_storage.py | 3 ++- pyproject.toml | 3 +-- test/base/test_bytesstorage.py | 5 +++-- tox.ini | 1 - 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index 46f541dcb..f6a1a7917 100644 --- a/doc/examples/petab_application.ipynb +++ b/doc/examples/petab_application.ipynb @@ -30,8 +30,8 @@ "metadata": {}, "outputs": [], "source": [ - "import amici\n", "import petab.v1 as petab\n", + "from amici.importers.petab.v1 import import_petab_problem\n", "\n", "from pyabc.petab import AmiciPetabImporter" ] @@ -88,7 +88,7 @@ ")\n", "\n", "# compile the petab problem to an AMICI ODE model\n", - "model = amici.petab_import.import_petab_problem(petab_problem)\n", + "model = import_petab_problem(petab_problem)\n", "\n", "# the solver to numerically solve the ODE\n", "solver = model.getSolver()\n", diff --git a/doc/examples/petab_yaml2sbml.ipynb b/doc/examples/petab_yaml2sbml.ipynb index 8c56f9cdd..371efd9be 100644 --- a/doc/examples/petab_yaml2sbml.ipynb +++ b/doc/examples/petab_yaml2sbml.ipynb @@ -26,16 +26,17 @@ ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import os\n", "import sys\n", "\n", "import amici\n", "import numpy as np\n", + "from amici.importers.petab.v1 import import_petab_problem\n", "\n", "import pyabc\n", "import pyabc.petab\n", @@ -59,10 +60,10 @@ ] }, { - "metadata": {}, "cell_type": "code", - "outputs": [], "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import shutil\n", "\n", @@ -159,13 +160,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001B[32mChecking SBML model...\n", - "\u001B[0m\u001B[32mChecking measurement table...\n", - "\u001B[0m\u001B[32mChecking condition table...\n", - "\u001B[0m\u001B[32mChecking observable table...\n", - "\u001B[0m\u001B[32mChecking parameter table...\n", - "\u001B[0m\u001B[32mPEtab format check completed successfully.\n", - "\u001B[0m\u001B[0m" + "\u001b[32mChecking SBML model...\n", + "\u001b[0m\u001b[32mChecking measurement table...\n", + "\u001b[0m\u001b[32mChecking condition table...\n", + "\u001b[0m\u001b[32mChecking observable table...\n", + "\u001b[0m\u001b[32mChecking parameter table...\n", + "\u001b[0m\u001b[32mPEtab format check completed successfully.\n", + "\u001b[0m\u001b[0m" ] } ], @@ -205,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -216,7 +217,7 @@ "amici_dir = dir_out + model_name + '_amici'\n", "if amici_dir not in sys.path:\n", " sys.path.insert(0, os.path.abspath(amici_dir))\n", - "model = amici.petab_import.import_petab_problem(\n", + "model = import_petab_problem(\n", " petab_problem,\n", " model_output_dir=amici_dir,\n", " verbose=False,\n", diff --git a/pyabc/storage/bytes_storage.py b/pyabc/storage/bytes_storage.py index 873a42165..e4d006b3d 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -11,7 +11,8 @@ def r_to_py(object_): if isinstance(object_, robjects.DataFrame): conv = get_conversion() - with localconverter(conv + pandas2ri.converter): + conv = conv + pandas2ri.converter + with localconverter(conv): py_object_ = conv.rpy2py(object_) # Ensure factor columns are converted to strings for col in py_object_.columns: diff --git a/pyproject.toml b/pyproject.toml index 5f63d31a3..d1d87eeb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ dependencies = [ "sqlalchemy>=2.0.0", "jabbar>=0.0.10", "gitpython>=3.1.7", + "pyarrow>=22.0.0" ] [project.urls] @@ -73,13 +74,11 @@ webserver-dash = [ "dash>=2.11.1", "dash-bootstrap-components>=1.4.2", ] -pyarrow = ["pyarrow>=6.0.0"] r = [ "rpy2>=3.4.4", "cffi>=1.14.5", "ipython>=7.18.1", "pygments>=2.6.1", - "pyarrow>=22.0.0", ] julia = [ "julia>=0.5.7", diff --git a/test/base/test_bytesstorage.py b/test/base/test_bytesstorage.py index 8427e82d2..c4275af12 100644 --- a/test/base/test_bytesstorage.py +++ b/test/base/test_bytesstorage.py @@ -140,8 +140,9 @@ def test_storage(object_): assert (object_.to_frame() == rebuilt).all().all() elif isinstance(object_, robjects.DataFrame): conv = get_conversion() - with localconverter(conv + pandas2ri.converter): - assert (robjects.conversion.rpy2py(object_) == rebuilt).all().all() + conv = conv + pandas2ri.converter + with localconverter(conv): + assert (conv.rpy2py(object_) == rebuilt).all().all() else: raise Exception('Could not compare') diff --git a/tox.ini b/tox.ini index 6eaaf88ba..ee1ebacbd 100644 --- a/tox.ini +++ b/tox.ini @@ -103,7 +103,6 @@ extras = test petab amici - test-petab commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s From fce2d88a5d02b106e4eac6943a90e8e34e99da6e Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:17:46 +0100 Subject: [PATCH 33/55] fix tox --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index ee1ebacbd..70bf7d215 100644 --- a/tox.ini +++ b/tox.ini @@ -47,7 +47,6 @@ setenv = extras = test r - pyarrow autograd ot commands = From 3fe18253ed9ce8e9076be3a2785b5a9ce615b8da Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:34:00 +0100 Subject: [PATCH 34/55] update amici dep --- doc/examples/chemical_reaction.ipynb | 179 ++++----------------------- pyproject.toml | 6 +- tox.ini | 1 + 3 files changed, 31 insertions(+), 155 deletions(-) diff --git a/doc/examples/chemical_reaction.ipynb b/doc/examples/chemical_reaction.ipynb index ebd398871..57430e1a6 100644 --- a/doc/examples/chemical_reaction.ipynb +++ b/doc/examples/chemical_reaction.ipynb @@ -58,12 +58,17 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ + "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", + "from pyabc import ABCSMC, RV, Distribution\n", + "from pyabc.populationstrategy import AdaptivePopulationSize\n", + "from pyabc.visualization import plot_kde_1d\n", + "\n", "\n", "def h(x, pre, c):\n", " return (x**pre).prod(1) * c\n", @@ -110,7 +115,7 @@ " if h0 == 0:\n", " break\n", " delta_t = np.random.exponential(1 / h0)\n", - " # no reaction can occur any more\n", + " # no reaction can occur anymore\n", " if not np.isfinite(delta_t):\n", " t_store.append(max_t)\n", " x_store.append(x)\n", @@ -138,7 +143,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -168,7 +173,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -187,25 +192,11 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "%matplotlib inline\n", - "import matplotlib.pyplot as plt\n", "\n", "true_rate = 2.3\n", "observations = [Model1()({'rate': true_rate}), Model2()({'rate': 30})]\n", @@ -245,7 +236,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -273,12 +264,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "from pyabc import RV, Distribution\n", - "\n", "prior = Distribution(rate=RV('uniform', 0, 100))" ] }, @@ -291,26 +280,15 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:Sampler:Parallelizing the sampling on 4 cores.\n" - ] - } - ], + "outputs": [], "source": [ - "from pyabc import ABCSMC\n", - "from pyabc.populationstrategy import AdaptivePopulationSize\n", - "\n", "abc = ABCSMC(\n", " [Model1(), Model2()],\n", " [prior, prior],\n", " distance,\n", - " population_size=AdaptivePopulationSize(500, 0.15),\n", + " population_size=AdaptivePopulationSize(500, 0.1),\n", ")" ] }, @@ -324,17 +302,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:History:Start \n" - ] - } - ], + "outputs": [], "source": [ "abc_id = abc.new('sqlite:////tmp/mjp.db', observations[0])" ] @@ -348,65 +318,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:ABC:Calibration sample before t=0.\n", - "INFO:Epsilon:initial epsilon is 10.65\n", - "INFO:ABC:t: 0, eps: 10.65.\n", - "INFO:ABC:Acceptance rate: 500 / 1023 = 4.8876e-01, ESS=5.0000e+02.\n", - "INFO:Adaptation:Change nr particles 500 -> 115\n", - "INFO:ABC:t: 1, eps: 6.75.\n", - "INFO:ABC:Acceptance rate: 115 / 272 = 4.2279e-01, ESS=1.0816e+02.\n", - "INFO:Adaptation:Change nr particles 115 -> 89\n", - "INFO:ABC:t: 2, eps: 5.316484503773134.\n", - "INFO:ABC:Acceptance rate: 89 / 167 = 5.3293e-01, ESS=6.2536e+01.\n", - "INFO:Adaptation:Change nr particles 89 -> 82\n", - "INFO:ABC:t: 3, eps: 3.9215865820412024.\n", - "INFO:ABC:Acceptance rate: 82 / 220 = 3.7273e-01, ESS=5.8737e+01.\n", - "INFO:Adaptation:Change nr particles 82 -> 92\n", - "INFO:ABC:t: 4, eps: 3.2.\n", - "INFO:ABC:Acceptance rate: 92 / 330 = 2.7879e-01, ESS=2.1439e+01.\n", - "INFO:Adaptation:Change nr particles 92 -> 75\n", - "INFO:ABC:t: 5, eps: 2.45.\n", - "INFO:ABC:Acceptance rate: 75 / 401 = 1.8703e-01, ESS=2.1633e+01.\n", - "INFO:Adaptation:Change nr particles 75 -> 96\n", - "INFO:ABC:t: 6, eps: 2.145736215699017.\n", - "INFO:ABC:Acceptance rate: 96 / 549 = 1.7486e-01, ESS=2.9474e+01.\n", - "INFO:Adaptation:Change nr particles 96 -> 102\n", - "INFO:ABC:t: 7, eps: 1.75.\n", - "INFO:ABC:Acceptance rate: 102 / 792 = 1.2879e-01, ESS=7.8885e+01.\n", - "INFO:Adaptation:Change nr particles 102 -> 58\n", - "INFO:ABC:t: 8, eps: 1.4.\n", - "INFO:ABC:Acceptance rate: 58 / 357 = 1.6246e-01, ESS=4.9798e+01.\n", - "INFO:Adaptation:Change nr particles 58 -> 57\n", - "INFO:ABC:t: 9, eps: 1.25.\n", - "INFO:ABC:Acceptance rate: 57 / 583 = 9.7770e-02, ESS=4.1186e+01.\n", - "INFO:Adaptation:Change nr particles 57 -> 61\n", - "INFO:ABC:t: 10, eps: 1.0627609318694404.\n", - "INFO:ABC:Acceptance rate: 61 / 1185 = 5.1477e-02, ESS=5.7597e+01.\n", - "INFO:Adaptation:Change nr particles 61 -> 46\n", - "INFO:ABC:t: 11, eps: 0.95.\n", - "INFO:ABC:Acceptance rate: 46 / 1461 = 3.1485e-02, ESS=4.2846e+01.\n", - "INFO:Adaptation:Change nr particles 46 -> 52\n", - "INFO:ABC:t: 12, eps: 0.8.\n", - "INFO:ABC:Acceptance rate: 52 / 4317 = 1.2045e-02, ESS=3.9330e+01.\n", - "INFO:Adaptation:Change nr particles 52 -> 53\n", - "INFO:ABC:t: 13, eps: 0.75.\n", - "INFO:ABC:Acceptance rate: 53 / 4867 = 1.0890e-02, ESS=4.8614e+01.\n", - "INFO:Adaptation:Change nr particles 53 -> 51\n", - "INFO:ABC:t: 14, eps: 0.6526353965182403.\n", - "INFO:ABC:Acceptance rate: 51 / 15746 = 3.2389e-03, ESS=4.4594e+01.\n", - "INFO:Adaptation:Change nr particles 51 -> 55\n", - "INFO:ABC:Stopping: minimum epsilon.\n", - "INFO:History:Done \n" - ] - } - ], + "outputs": [], "source": [ "history = abc.run(minimum_epsilon=0.7, max_nr_populations=15)" ] @@ -420,22 +334,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYIAAAEvCAYAAABIeMa5AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAbt0lEQVR4nO3deZhddZ3n8fc3C5tASEgJSAiJWToGMWJiAk36ARt0ArQgW8uijSzyNCOK3dqYmWYgD9jTwW5kGgcakRbQbllHmDQgiw1o48iSICABghECJBAIW9iXUN/545zSa1GVqtStcyuV8349Tz11z3K/91s3qfrcs/1OZCaSpPoaMtANSJIGlkEgSTVnEEhSzRkEklRzBoEk1ZxBIEk1N2ygG5A6LFq06P3Dhg27EPgwfkhptXbggTVr1hw3ffr0Zwe6GbWWQaD1xrBhwy7cdtttP9TW1vbikCFDvMClhdrb22PVqlVTV65ceSGw/0D3o9byU5fWJx9ua2t72RBovSFDhmRbW9tqiq0x1YxBoPXJEENg4JTvvX8Tash/dKmTiJh+wAEHjO+Yfueddxg5cuS0T3ziExPXpc7222+/89NPP73W3a+9WUeqmkEgdbLpppu2L1myZNNXX301AK6++uott9lmm3cGui+pKgaB1IW999579ZVXXrkVwKWXXjrq4IMPfqFj2TPPPDN07733njB58uSp06ZNm3LnnXduCrBy5cqhu++++6SJEyfu9NnPfnbHxgEdzzvvvFE777zzh6ZMmTL1iCOO2HHNmjUt/5mk7hgEUhc+//nPv3D55ZePfP311+Ohhx7abLfddnutY9nJJ5/8gWnTpr3+yCOPPHjGGWesOOqoo8YDzJ079wO77bbbq0uXLl184IEHvvT0009vBHDPPfdsctVVV41auHDhww8//PCDQ4YMyfPPP3/rgfrZpM4MAqkLs2bNemP58uUbf+973xu19957r25cdtddd21x7LHHPg+w//77v/LSSy8Ne+GFF4bccccdWxxzzDHPAxx22GGrt9xyy3cBbrjhhi0eeOCBzaZNm/ahKVOmTL399tu3fPTRRzdu/U8ldc2DVFI35syZ89Jpp522w0033bTk2Wef7fPvSmbGoYce+vy55567oj/7k/qLWwRSN0444YTnvv71rz81c+bMNxrnz5o1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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "ax = history.get_model_probabilities().plot.bar()\n", "ax.set_ylabel('Probability')\n", @@ -456,25 +357,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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spMAcPdMJsX4WP31yk6OSinVlX3zzTdJY8aMqapv24MPPpiVlpYWXb58+aqvv/561bPPPpu9evVqD8CqVauSZs2alb9+/foVW7du9b7zzjspoVBIbrjhhtzXXnttw4oVK1ZddtllRT/72c/6VB8vHA7L8uXLV/3mN7/ZOWnSpIqvvvpq9apVq1ZOnTq15O67784ZNmxY+NJLLy380Y9+tHP16tUrJ0+eXFE7nunTpx92zz33bFu7du3KkSNHBn7xi1/0rt5mWZZ88803q/7whz/k33333b1pIT3F1waxqgixigjuHn4A3Dn2z8d//3BnhqWUOshccskluV988UWK2+02ffv2Da1evdo/d+7c7gDl5eXOlStX+jwejxk9enTloEGDIgAjR46s2rBhgycjI8Nat25d0sSJE4eCffowOzs7Un3sadOmlVQ/37Rpk+fss8/uW1hY6A6Hw45+/fqFGouruLjYWV5e7vze975XATBjxozi8847b2D19vPOO283wPHHH195yy23eFr6ujVBtUGk0B7B56pOUD3ta4iRnVV4B6R1WlxKqa5t9OjRgddee6179fKcOXO27tixwzV+/PgRffr0Cd9///1bzz333D2193n99ddTvV5vzVB1p9OJZVlijJHBgwcHvvrqq9X1/a7U1NRY9fPrrrsu98Ybbyy46KKLyl5//fXU1vR6avP5fAbA5XIRjUalpfvrKb42sOIj+NzZSUSjUb5Y/SUvez7l5dd1FJ9SqvXOPPPM8lAoJH/4wx9qZsGuqKhwAEyaNKns0UcfzQ6FQgKwbNky7549exr8LB8zZkywpKTE9e677yYDhEIhWbJkia++tuXl5c7c3NwIwDPPPFMzSi81NTVaXl7urNs+MzMz2q1bt2j19aWnnnoq87jjjquo2661NEG1QbTC7iU7u3lYunQpb7/9NiEi5DuK2LlzZydHp5TqqhwOB/PmzduwcOHC1D59+owePXr0iIsvvnjAr3/9620//elPi4YPHx4cPXr0iCFDhoycMWNG/0gk0mDvxOfzmRdffHHDrbfe2nfYsGF5I0eOzPvwww/rHbBw2223fTtt2rRBI0eOHJGZmWlVrz/33HNL33jjjfTqQRK193n66ac3/eIXv+g7dOjQvGXLliXde++937bX+9DhFXUPlPHjx5uOLlhY+p9NVHyynb4zT+SVV15h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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "from pyabc.visualization import plot_kde_1d\n", - "\n", "fig, axes = plt.subplots(2)\n", "fig.set_size_inches((6, 6))\n", "axes = axes.flatten()\n", @@ -512,22 +398,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "populations = history.get_all_populations()\n", "ax = populations[populations.t >= 1].plot('t', 'particles', style='o-')\n", diff --git a/pyproject.toml b/pyproject.toml index d1d87eeb2..3d561f7b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "sqlalchemy>=2.0.0", "jabbar>=0.0.10", "gitpython>=3.1.7", - "pyarrow>=22.0.0" + "ipykernel>=7.2.0", ] [project.urls] @@ -74,6 +74,7 @@ webserver-dash = [ "dash>=2.11.1", "dash-bootstrap-components>=1.4.2", ] +pyarrow = ["pyarrow>=22.0.0"] r = [ "rpy2>=3.4.4", "cffi>=1.14.5", @@ -91,7 +92,7 @@ ot = [ "cython>=0.29.0", ] petab = ["petab>=0.2.0"] -amici = ["amici>=0.18.0"] +amici = ["amici>=0.32.0"] yaml2sbml = ["yaml2sbml>=0.2.1"] migrate = ["alembic>=1.5.4"] plotly = ["plotly>=5.3.1", "kaleido>=0.2.1"] @@ -161,6 +162,7 @@ ignore = [ "B905", # zip() without an explicit strict= parameter "SIM105", # Replace `try`-`except`-`pass` "C408", + "E402" ] [tool.ruff.lint.per-file-ignores] diff --git a/tox.ini b/tox.ini index 70bf7d215..12b55a43a 100644 --- a/tox.ini +++ b/tox.ini @@ -49,6 +49,7 @@ extras = r autograd ot + pyarrow commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base test_performance -s From 187099822ec762186f1910462c287a18f6504b97 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:51:54 +0100 Subject: [PATCH 35/55] update amici dep --- doc/examples/petab_application.ipynb | 12 ++++++------ doc/examples/petab_yaml2sbml.ipynb | 20 ++++++++++---------- pyproject.toml | 1 + test/petab/test_petab_suite.py | 11 +---------- tox.ini | 1 + 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index f6a1a7917..7332092e9 100644 --- a/doc/examples/petab_application.ipynb +++ b/doc/examples/petab_application.ipynb @@ -21,7 +21,7 @@ "outputs": [], "source": [ "# install if not done yet\n", - "!pip install pyabc --quiet" + "!pip install pyabc[petab,amici] --quiet" ] }, { @@ -31,7 +31,7 @@ "outputs": [], "source": [ "import petab.v1 as petab\n", - "from amici.importers.petab.v1 import import_petab_problem\n", + "from amici.petab import import_petab_problem\n", "\n", "from pyabc.petab import AmiciPetabImporter" ] @@ -53,10 +53,10 @@ "output_type": "stream", "text": [ "Cloning into 'tmp/benchmark-models'...\n", - "remote: Enumerating objects: 3511, done.\u001b[K\n", - "remote: Counting objects: 100% (3511/3511), done.\u001b[K\n", - "remote: Compressing objects: 100% (922/922), done.\u001b[K\n", - "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001b[K\n", + "remote: Enumerating objects: 3511, done.\u001B[K\n", + "remote: Counting objects: 100% (3511/3511), done.\u001B[K\n", + "remote: Compressing objects: 100% (922/922), done.\u001B[K\n", + "remote: Total 3511 (delta 2695), reused 3170 (delta 2564), pack-reused 0\u001B[K\n", "Receiving objects: 100% (3511/3511), 201.90 MiB | 20.18 MiB/s, done.\n", "Resolving deltas: 100% (2695/2695), done.\n", "Checking out files: 100% (3411/3411), done.\n" diff --git a/doc/examples/petab_yaml2sbml.ipynb b/doc/examples/petab_yaml2sbml.ipynb index 371efd9be..8c5957843 100644 --- a/doc/examples/petab_yaml2sbml.ipynb +++ b/doc/examples/petab_yaml2sbml.ipynb @@ -26,17 +26,17 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, + "cell_type": "code", "outputs": [], + "execution_count": null, "source": [ "import os\n", "import sys\n", "\n", "import amici\n", "import numpy as np\n", - "from amici.importers.petab.v1 import import_petab_problem\n", + "from amici.petab import import_petab_problem\n", "\n", "import pyabc\n", "import pyabc.petab\n", @@ -160,13 +160,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "\u001b[32mChecking SBML model...\n", - "\u001b[0m\u001b[32mChecking measurement table...\n", - "\u001b[0m\u001b[32mChecking condition table...\n", - "\u001b[0m\u001b[32mChecking observable table...\n", - "\u001b[0m\u001b[32mChecking parameter table...\n", - "\u001b[0m\u001b[32mPEtab format check completed successfully.\n", - "\u001b[0m\u001b[0m" + "\u001B[32mChecking SBML model...\n", + "\u001B[0m\u001B[32mChecking measurement table...\n", + "\u001B[0m\u001B[32mChecking condition table...\n", + "\u001B[0m\u001B[32mChecking observable table...\n", + "\u001B[0m\u001B[32mChecking parameter table...\n", + "\u001B[0m\u001B[32mPEtab format check completed successfully.\n", + "\u001B[0m\u001B[0m" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 3d561f7b7..4519c4d39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,6 +92,7 @@ ot = [ "cython>=0.29.0", ] petab = ["petab>=0.2.0"] +petab-test = ["petabtests>=0.0.1"] amici = ["amici>=0.32.0"] yaml2sbml = ["yaml2sbml>=0.2.1"] migrate = ["alembic>=1.5.4"] diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index ed6927111..645078877 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -12,21 +12,12 @@ import amici.petab.petab_import import amici.petab.simulations import petab.v1 as petab + import petabtests import pyabc.petab except ImportError: pass -try: - import petabtests -except ImportError: - pytest.skip( - 'PEtab test suite not available. Please install petabtests to run this ' - 'test.', - allow_module_level=True, - ) - - logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/tox.ini b/tox.ini index 12b55a43a..ded032f94 100644 --- a/tox.ini +++ b/tox.ini @@ -103,6 +103,7 @@ extras = test petab amici + petab-test commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s From 621ef8b623333063df542deab1a9ce947da514f9 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 18:56:34 +0100 Subject: [PATCH 36/55] update amici dep --- pyproject.toml | 2 +- test/petab/test_petab_suite.py | 10 ++++++++++ tox.ini | 1 - 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4519c4d39..bbdfce0d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,7 +92,7 @@ ot = [ "cython>=0.29.0", ] petab = ["petab>=0.2.0"] -petab-test = ["petabtests>=0.0.1"] +#petab-test = ["petabtests>=0.0.1"] # problem with pysb amici = ["amici>=0.32.0"] yaml2sbml = ["yaml2sbml>=0.2.1"] migrate = ["alembic>=1.5.4"] diff --git a/test/petab/test_petab_suite.py b/test/petab/test_petab_suite.py index 645078877..d0f1bfa4e 100644 --- a/test/petab/test_petab_suite.py +++ b/test/petab/test_petab_suite.py @@ -18,6 +18,16 @@ except ImportError: pass +try: + import petabtests +except ImportError: + pytest.skip( + 'PEtab test suite not available. Please install petabtests to run this ' + 'test.', + allow_module_level=True, + ) + + logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) diff --git a/tox.ini b/tox.ini index ded032f94..12b55a43a 100644 --- a/tox.ini +++ b/tox.ini @@ -103,7 +103,6 @@ extras = test petab amici - petab-test commands = python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s From c4f9771fa4148b0f879de9fa51f7cf507384dd39 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 19:35:05 +0100 Subject: [PATCH 37/55] update notebooks --- test/run_notebooks.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/run_notebooks.sh b/test/run_notebooks.sh index 4fee1518c..09c1d15a5 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -18,7 +18,7 @@ nbs_1=( "chemical_reaction" "informative" "look_ahead" - "sde_ion_channels" + #"sde_ion_channels" # url error ) nbs_2=( "external_simulators" @@ -29,8 +29,8 @@ nbs_2=( "optimal_threshold" "custom_priors" "petab_application" - "petab_yaml2sbml" - "multiscale_agent_based" + # "petab_yaml2sbml" # yaml2sbml does not work with current petab version + # "multiscale_agent_based" # too slow "using_copasi" "using_julia" ) From 4ecb61aa32c4310d17c805a4df8830d12a657395 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 22:05:30 +0100 Subject: [PATCH 38/55] update notebooks 2 --- pyabc/__init__.py | 15 +++++++++++++ pyabc/inference_util/inference_util.py | 29 +++++++++++++++++--------- tox.ini | 3 +++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/pyabc/__init__.py b/pyabc/__init__.py index 1765219b5..6febf934f 100644 --- a/pyabc/__init__.py +++ b/pyabc/__init__.py @@ -73,6 +73,21 @@ TemperatureScheme, ) from .inference import ABCSMC +from .inference_util import ( + AnalysisVars, + create_analysis_id, + create_prior_pdf, + create_simulate_from_prior_function, + create_simulate_function, + create_transition_pdf, + create_weight_function, + eps_from_hist, + evaluate_preliminary_particle, + evaluate_proposal, + generate_valid_proposal, + only_simulate_data_for_proposal, + termination_criteria_fulfilled, +) from .model import FunctionModel, IntegratedModel, Model, ModelResult from .parameters import Parameter from .population import Particle, Population, Sample diff --git a/pyabc/inference_util/inference_util.py b/pyabc/inference_util/inference_util.py index 357100be4..c26e62910 100644 --- a/pyabc/inference_util/inference_util.py +++ b/pyabc/inference_util/inference_util.py @@ -4,20 +4,21 @@ import uuid from collections.abc import Callable from datetime import datetime, timedelta +from typing import TYPE_CHECKING import numpy as np import pandas as pd -from ..acceptor import Acceptor -from ..distance import Distance -from ..epsilon import Epsilon -from ..model import Model -from ..parameters import Parameter -from ..population import Particle -from ..random_choice import fast_random_choice -from ..random_variables import RV, Distribution -from ..storage.history import History -from ..transition import ModelPerturbationKernel, Transition +if TYPE_CHECKING: + from ..acceptor import Acceptor + from ..distance import Distance + from ..epsilon import Epsilon + from ..model import Model + from ..parameters import Parameter + from ..population import Particle + from ..random_variables import RV, Distribution + from ..storage.history import History + from ..transition import ModelPerturbationKernel, Transition logger = logging.getLogger('ABC') @@ -97,6 +98,7 @@ def create_simulate_from_prior_function( A function that returns a sampled particle. """ # simulation function, simplifying some parts compared to later + from ..population import Particle def simulate_one(): # sample model @@ -154,6 +156,8 @@ def generate_valid_proposal( ------- (m_ss, theta_ss): Model, parameter. """ + from ..random_choice import fast_random_choice + # first generation if t == 0: # sample from prior @@ -229,6 +233,8 @@ def evaluate_proposal( Data for the given parameters theta_ss are simulated, summary statistics computed and evaluated. """ + from ..population import Particle + # simulate, compute distance, check acceptance model_result = models[m_ss].accept( t, theta_ss, summary_statistics, distance_function, eps, acceptor, x_0 @@ -477,6 +483,7 @@ def only_simulate_data_for_proposal( Similar to `evaluate_proposal`, however here for the passed parameters only data are simulated, but no distances calculated or acceptance checked. That needs to be done post-hoc then, not checked here.""" + from ..population import Particle # simulate model_result = models[m_ss].summary_statistics( @@ -516,6 +523,8 @@ def evaluate_preliminary_particle( ------- evaluated_particle: The evaluated particle """ + from ..population import Particle + if not particle.preliminary: raise AssertionError('Particle is not preliminary') diff --git a/tox.ini b/tox.ini index 12b55a43a..2d36b768e 100644 --- a/tox.ini +++ b/tox.ini @@ -158,7 +158,10 @@ extras = yaml2sbml amici autograd + julia + copasi commands = + python -c "import julia; julia.install()" bash test/run_notebooks.sh 2 description = Run notebooks (set 2) From 6e9aef6856702a5c5d293e3d6217cca0017f1a61 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 22:11:12 +0100 Subject: [PATCH 39/55] annotations in inference_util --- pyabc/inference_util/inference_util.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyabc/inference_util/inference_util.py b/pyabc/inference_util/inference_util.py index c26e62910..89b9f28d3 100644 --- a/pyabc/inference_util/inference_util.py +++ b/pyabc/inference_util/inference_util.py @@ -1,4 +1,4 @@ -# Note: Due to cyclic imports, these need to be separated from other modules +from __future__ import annotations import logging import uuid @@ -9,7 +9,7 @@ import numpy as np import pandas as pd -if TYPE_CHECKING: +if TYPE_CHECKING: # to avoid circular imports from ..acceptor import Acceptor from ..distance import Distance from ..epsilon import Epsilon From b46332ec249f51d2cb5a4abbf83849c4c5a1b17c Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 22:18:47 +0100 Subject: [PATCH 40/55] fix notebooks 2 deps --- .github/workflows/ci.yml | 11 ++++------- tox.ini | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 203ec4dd1..f139523b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,6 @@ jobs: python: "3.11" toxenv: migrate - # Heavier - os: ubuntu-latest python: "3.11" toxenv: external-R @@ -63,10 +62,10 @@ jobs: toxenv: petab - os: ubuntu-latest python: "3.11" - toxenv: notebooks1 + toxenv: base-notebooks - os: ubuntu-latest python: "3.11" - toxenv: notebooks2 + toxenv: external-notebooks steps: - uses: actions/checkout@v4 @@ -76,7 +75,6 @@ jobs: python-version: ${{ matrix.python }} cache: pip - # Install extra runtimes only when needed - name: Install Julia if: ${{ startsWith(matrix.toxenv, 'external-') }} uses: julia-actions/setup-julia@v2 @@ -86,10 +84,9 @@ jobs: - name: Install dependencies run: | case "${{ matrix.toxenv }}" in - base|visualization|mac|notebooks1) .github/workflows/install_deps.sh base R ;; - notebooks2) .github/workflows/install_deps.sh R amici ;; + base|visualization|mac|base-notebooks) .github/workflows/install_deps.sh base R ;; petab) .github/workflows/install_deps.sh amici ;; - external-*) .github/workflows/install_deps.sh base R ;; + external-*) .github/workflows/install_deps.sh base R amici ;; lint|project|doc|migrate) .github/workflows/install_deps.sh doc ;; esac python -m pip install -U pip tox diff --git a/tox.ini b/tox.ini index 2d36b768e..420a0aab3 100644 --- a/tox.ini +++ b/tox.ini @@ -137,7 +137,7 @@ commands = description = Test database migration -[testenv:notebooks1] +[testenv:base-notebooks] allowlist_externals = bash extras = examples @@ -147,7 +147,7 @@ commands = description = Run notebooks (set 1) -[testenv:notebooks2] +[testenv:external-notebooks] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib allowlist_externals = bash From 3176737fcd0a45d7c4cd979eee6c2886c4e49418 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 22:51:24 +0100 Subject: [PATCH 41/55] fix notebooks 2 deps --- doc/examples/chemical_reaction.ipynb | 80 ++++++++++++++-------------- doc/examples/using_julia.ipynb | 14 +++++ test/run_notebooks.sh | 8 +-- 3 files changed, 58 insertions(+), 44 deletions(-) diff --git a/doc/examples/chemical_reaction.ipynb b/doc/examples/chemical_reaction.ipynb index 57430e1a6..ae3ddf8fc 100644 --- a/doc/examples/chemical_reaction.ipynb +++ b/doc/examples/chemical_reaction.ipynb @@ -48,19 +48,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# install if not done yet\n", "!pip install pyabc --quiet" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", @@ -128,7 +126,9 @@ " x_store.append(x)\n", "\n", " return np.array(t_store), np.array(x_store)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -143,9 +143,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "MAX_T = 0.1\n", "\n", @@ -161,7 +159,9 @@ " self.x0, np.array([float(par['rate'])]), self.pre, self.post, MAX_T\n", " )\n", " return {'t': t, 'X': X}" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -173,15 +173,15 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "class Model2(Model1):\n", " __name__ = 'Model 2'\n", " pre = np.array([[1, 0]], dtype=int)\n", " post = np.array([[0, 1]])" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -192,9 +192,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "%matplotlib inline\n", "\n", @@ -208,7 +206,9 @@ " ax.set_xlabel('Time')\n", " ax.set_ylabel('Concentration')\n", " ax.set_title(title)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -236,9 +236,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "N_TEST_TIMES = 20\n", "\n", @@ -253,7 +251,9 @@ " / t_test_times.size\n", " )\n", " return error" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -264,12 +264,12 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "prior = Distribution(rate=RV('uniform', 0, 100))" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -280,17 +280,17 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "abc = ABCSMC(\n", " [Model1(), Model2()],\n", " [prior, prior],\n", " distance,\n", - " population_size=AdaptivePopulationSize(500, 0.1),\n", + " population_size=AdaptivePopulationSize(500),\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -302,12 +302,12 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "abc_id = abc.new('sqlite:////tmp/mjp.db', observations[0])" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -318,12 +318,12 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "history = abc.run(minimum_epsilon=0.7, max_nr_populations=15)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -334,9 +334,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "ax = history.get_model_probabilities().plot.bar()\n", "ax.set_ylabel('Probability')\n", @@ -344,7 +342,9 @@ "ax.legend(\n", " [1, 2], title='Model', ncol=2, loc='lower center', bbox_to_anchor=(0.5, 1)\n", ");" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -357,9 +357,7 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "fig, axes = plt.subplots(2)\n", "fig.set_size_inches((6, 6))\n", @@ -383,7 +381,9 @@ "axes[0].legend(title='Generation', loc='upper left', bbox_to_anchor=(1, 1))\n", "\n", "fig.tight_layout()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -398,14 +398,14 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "populations = history.get_all_populations()\n", "ax = populations[populations.t >= 1].plot('t', 'particles', style='o-')\n", "ax.set_xlabel('Generation');" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", diff --git a/doc/examples/using_julia.ipynb b/doc/examples/using_julia.ipynb index bbd77daa7..5589f2a98 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -27,6 +27,20 @@ "pyABC's Julia interface is the class :class:`pyabc.external.julia.Julia `." ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "faddd912ab1fc7ba", + "metadata": {}, + "outputs": [], + "source": [ + "from julia.api import Julia as JuliaAPI\n", + "\n", + "# one way of making pyjulia work, see\n", + "# https://pyjulia.readthedocs.io/en/latest/troubleshooting.html\n", + "JuliaAPI(compiled_modules=False)" + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/test/run_notebooks.sh b/test/run_notebooks.sh index 09c1d15a5..371233d69 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -18,16 +18,16 @@ nbs_1=( "chemical_reaction" "informative" "look_ahead" + "data_plots" + "discrete_parameters" + "custom_priors" + "aggregated_distances" #"sde_ion_channels" # url error ) nbs_2=( "external_simulators" "using_R" - "aggregated_distances" - "data_plots" - "discrete_parameters" "optimal_threshold" - "custom_priors" "petab_application" # "petab_yaml2sbml" # yaml2sbml does not work with current petab version # "multiscale_agent_based" # too slow From 557667649cef673146991e4b90216ae689a4b75f Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 23:06:34 +0100 Subject: [PATCH 42/55] fix notebooks 2 deps --- .flake8 | 30 --- doc/examples/using_julia.ipynb | 323 +++------------------------------ pyproject.toml | 2 +- tox.ini | 2 + 4 files changed, 30 insertions(+), 327 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 787094bcc..000000000 --- a/.flake8 +++ /dev/null @@ -1,30 +0,0 @@ -######################## -# Flake8 Configuration # -######################## - -[flake8] - -extend-ignore = - # Related to security for pickles - S301, S403 - # White space before : - E203 - # Don't be crazy if line too long - E501 - # Empty method in an abstract base class - B027 - # Disable black would make changes warning - BLK100 - # Name is never assigned to scope - F824 - -per-file-ignores = - # Imported but unused - */__init__.py:F401 - # Print - */cli.py:T201 - pyabc/storage/migrate.py:T201 - # Print and asserts - test*/*.py:T201,S101 - # Module level import not at top of file - test/external/test_pyjulia.py:E402 diff --git a/doc/examples/using_julia.ipynb b/doc/examples/using_julia.ipynb index 5589f2a98..02f95fdbb 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -29,24 +29,8 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "faddd912ab1fc7ba", - "metadata": {}, - "outputs": [], - "source": [ - "from julia.api import Julia as JuliaAPI\n", - "\n", - "# one way of making pyjulia work, see\n", - "# https://pyjulia.readthedocs.io/en/latest/troubleshooting.html\n", - "JuliaAPI(compiled_modules=False)" - ] - }, - { - "cell_type": "code", - "execution_count": 1, "id": "0297966c-447b-47cc-ad70-423fab03ecc4", "metadata": {}, - "outputs": [], "source": [ "import tempfile\n", "\n", @@ -57,7 +41,9 @@ "from pyabc.external.julia import Julia\n", "\n", "pyabc.settings.set_figure_params('pyabc') # for beautified plots" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -71,173 +57,24 @@ }, { "cell_type": "code", - "execution_count": 2, "id": "a9ed8225-6884-44ab-9d73-a559cd916e68", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 33.7 s, sys: 1.46 s, total: 35.1 s\n", - "Wall time: 34.9 s\n" - ] - } - ], "source": [ "%%time\n", "jl = Julia(module_name='SIR', source_file='model_julia/SIR.jl')" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": 3, "id": "ce3cd228-8798-42d2-9fef-6b1558758e2a", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
module SIR\n",
-       "\n",
-       "# Install dependencies\n",
-       "using Pkg\n",
-       "Pkg.add("Catalyst")\n",
-       "Pkg.add("DiffEqJump")\n",
-       "\n",
-       "# Define reaction network\n",
-       "using Catalyst\n",
-       "sir_model = @reaction_network begin\n",
-       "    r1, S + I --> 2I\n",
-       "    r2, I --> R\n",
-       "end r1 r2\n",
-       "\n",
-       "# ground truth parameter\n",
-       "p = (0.0001, 0.01)\n",
-       "# initial state\n",
-       "u0 = [999, 1, 0]\n",
-       "# time span\n",
-       "tspan = (0.0, 250.0)\n",
-       "# formulate as discrete problem\n",
-       "prob  = DiscreteProblem(sir_model, u0, tspan, p)\n",
-       "\n",
-       "# formulate as Markov jump process\n",
-       "using DiffEqJump\n",
-       "jump_prob = JumpProblem(\n",
-       "    sir_model, prob, Direct(), save_positions=(false, false),\n",
-       ")\n",
-       "\n",
-       """"\n",
-       "Simulate model for parameters `10.0.^par`.\n",
-       """"\n",
-       "function model(par)\n",
-       "    p = 10.0.^((par["p1"], par["p2"]))\n",
-       "    sol = solve(remake(jump_prob, p=p), SSAStepper(), saveat=2.5)\n",
-       "    return Dict("t"=>sol.t, "u"=>sol.u)\n",
-       "end\n",
-       "\n",
-       "# observed data\n",
-       "observation = model(Dict("p1"=>log10(p[1]), "p2"=>log10(p[2])))\n",
-       "\n",
-       """"\n",
-       "Distance between model simulations or observed data `y` and `y0`.\n",
-       """"\n",
-       "function distance(y, y0)\n",
-       "    u, u0 = y["u"], y0["u"]\n",
-       "    if length(u) != length(u0)\n",
-       "        throw(AssertionError("Dimension mismatch"))\n",
-       "    end\n",
-       "    return sum((u .- u0).^2) / length(u0)\n",
-       "end\n",
-       "\n",
-       "end  # module\n",
-       "
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "jl.display_source_ipython()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -249,30 +86,17 @@ }, { "cell_type": "code", - "execution_count": 4, "id": "b44e7de7-2e85-4116-83eb-86375065d3c5", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "model = jl.model()\n", "distance = jl.distance()\n", "obs = jl.observation()\n", "\n", "_ = plt.plot(obs['t'], obs['u'])" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -284,10 +108,8 @@ }, { "cell_type": "code", - "execution_count": 5, "id": "19c2edea-0344-4c95-a928-5b34f1271c1d", "metadata": {}, - "outputs": [], "source": [ "gt_par = {'p1': -4.0, 'p2': -2.0}\n", "\n", @@ -299,7 +121,9 @@ "prior = Distribution(\n", " **{key: RV('uniform', lb, ub - lb) for key, (lb, ub) in par_limits.items()}\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -311,24 +135,13 @@ }, { "cell_type": "code", - "execution_count": 6, "id": "f47a8141-e1e4-4556-b326-14453e1e01d9", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "297286.7590759076" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "distance(model(gt_par), model(gt_par))" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -340,42 +153,8 @@ }, { "cell_type": "code", - "execution_count": 7, "id": "11832983-ac20-495e-b439-de59f61c1921", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "ABC.Sampler INFO: Parallelize sampling on 4 processes.\n", - "ABC.History INFO: Start \n", - "ABC INFO: Calibration sample t = -1.\n", - "ABC INFO: t: 0, eps: 2.18842571e+05.\n", - "ABC INFO: Accepted: 100 / 212 = 4.7170e-01, ESS: 1.0000e+02.\n", - "ABC INFO: t: 1, eps: 1.45222795e+05.\n", - "ABC INFO: Accepted: 100 / 203 = 4.9261e-01, ESS: 7.8092e+01.\n", - "ABC INFO: t: 2, eps: 8.71552898e+04.\n", - "ABC INFO: Accepted: 100 / 268 = 3.7313e-01, ESS: 8.9681e+01.\n", - "ABC INFO: t: 3, eps: 5.17737477e+04.\n", - "ABC INFO: Accepted: 100 / 211 = 4.7393e-01, ESS: 9.0513e+01.\n", - "ABC INFO: t: 4, eps: 2.39628036e+04.\n", - "ABC INFO: Accepted: 100 / 346 = 2.8902e-01, ESS: 9.3795e+01.\n", - "ABC INFO: t: 5, eps: 1.14595719e+04.\n", - "ABC INFO: Accepted: 100 / 317 = 3.1546e-01, ESS: 8.1326e+01.\n", - "ABC INFO: t: 6, eps: 6.61310965e+03.\n", - "ABC INFO: Accepted: 100 / 403 = 2.4814e-01, ESS: 8.4728e+01.\n", - "ABC INFO: t: 7, eps: 3.66662784e+03.\n", - "ABC INFO: Accepted: 100 / 556 = 1.7986e-01, ESS: 6.2655e+01.\n", - "ABC INFO: t: 8, eps: 1.90359721e+03.\n", - "ABC INFO: Accepted: 100 / 683 = 1.4641e-01, ESS: 9.1709e+01.\n", - "ABC INFO: t: 9, eps: 1.02475108e+03.\n", - "ABC INFO: Accepted: 100 / 989 = 1.0111e-01, ESS: 7.6864e+01.\n", - "ABC INFO: Stop: Maximum number of generations.\n", - "ABC.History INFO: Done \n" - ] - } - ], "source": [ "abc = ABCSMC(\n", " model,\n", @@ -386,7 +165,9 @@ "db = tempfile.mkstemp(suffix='.db')[1]\n", "abc.new('sqlite:///' + db, obs)\n", "h = abc.run(max_nr_populations=10)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -398,35 +179,8 @@ }, { "cell_type": "code", - "execution_count": 8, "id": "a9e65eb6-ddd3-4317-96a7-d737f0b6b5d3", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "for t in [0, h.max_t]:\n", " pyabc.visualization.plot_kde_matrix_highlevel(\n", @@ -438,7 +192,9 @@ " )\n", " plt.gcf().suptitle(f'Posterior at t={t}')\n", " plt.gcf().tight_layout()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -450,35 +206,8 @@ }, { "cell_type": "code", - "execution_count": 9, "id": "6124db18-a5d6-44ac-ba33-6300864ce942", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "source": [ "def plot_data(sumstat, weight, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot a single trajectory\"\"\"\n", @@ -496,7 +225,9 @@ " )\n", " ax.plot(obs['t'], obs['u'])\n", " ax.set_title(f'Simulations at t={t}')" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { diff --git a/pyproject.toml b/pyproject.toml index bbdfce0d3..1ab1b083a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ r = [ "pygments>=2.6.1", ] julia = [ - "julia>=0.5.7", + "julia>=0.6.2", "pygments>=2.6.1", "ipython>=7.18.1", ] diff --git a/tox.ini b/tox.ini index 420a0aab3..072b778aa 100644 --- a/tox.ini +++ b/tox.ini @@ -162,6 +162,8 @@ extras = copasi commands = python -c "import julia; julia.install()" + python -c "julia -e 'using Pkg; Pkg.add("PyCall"); using PyCall'" + python -c "julia -e 'using Pkg; Pkg.add("IJulia"); using IJulia; IJulia.installkernel("Julia")'" bash test/run_notebooks.sh 2 description = Run notebooks (set 2) From 15a9bd861b0c06e41f2b022a1ccbec5a39027120 Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 23:13:21 +0100 Subject: [PATCH 43/55] fix notebooks 2 julia --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 072b778aa..e11e20cba 100644 --- a/tox.ini +++ b/tox.ini @@ -162,8 +162,8 @@ extras = copasi commands = python -c "import julia; julia.install()" - python -c "julia -e 'using Pkg; Pkg.add("PyCall"); using PyCall'" - python -c "julia -e 'using Pkg; Pkg.add("IJulia"); using IJulia; IJulia.installkernel("Julia")'" + julia -e 'using Pkg; Pkg.add("PyCall"); using PyCall' + julia -e 'using Pkg; Pkg.add("IJulia"); using IJulia; IJulia.installkernel("Julia")' bash test/run_notebooks.sh 2 description = Run notebooks (set 2) From f9bfe197b9ca2259a0d3b2a82b14c2492ccd68dd Mon Sep 17 00:00:00 2001 From: arrjon Date: Tue, 17 Feb 2026 23:19:02 +0100 Subject: [PATCH 44/55] fix notebooks 2 julia --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e11e20cba..323404a27 100644 --- a/tox.ini +++ b/tox.ini @@ -150,7 +150,9 @@ description = [testenv:external-notebooks] setenv = LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib -allowlist_externals = bash +allowlist_externals = + bash + julia extras = examples r From 5ec11a2de9789564f89a022328a49a08df5fdef1 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 09:27:47 +0100 Subject: [PATCH 45/55] fix notebooks 2 julia --- test/run_notebooks.sh | 2 +- tox.ini | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/run_notebooks.sh b/test/run_notebooks.sh index 371233d69..8124c16f6 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -70,7 +70,7 @@ run_notebook () { # Run a notebook and raise upon failure tempfile=$(mktemp) echo $@ - jupyter nbconvert --ExecutePreprocessor.timeout=-1 --debug \ + python -m jupyter nbconvert --ExecutePreprocessor.timeout=-1 --debug \ --stdout --execute --to markdown $@ &> $tempfile ret=$? if [[ $ret != 0 ]]; then diff --git a/tox.ini b/tox.ini index 323404a27..916404fa7 100644 --- a/tox.ini +++ b/tox.ini @@ -164,8 +164,6 @@ extras = copasi commands = python -c "import julia; julia.install()" - julia -e 'using Pkg; Pkg.add("PyCall"); using PyCall' - julia -e 'using Pkg; Pkg.add("IJulia"); using IJulia; IJulia.installkernel("Julia")' bash test/run_notebooks.sh 2 description = Run notebooks (set 2) From af91a24857a60a344b0ce4843d89a33c6f8dd30b Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 10:11:51 +0100 Subject: [PATCH 46/55] fix notebooks 2 julia --- doc/examples/model_julia/SIR.jl | 16 ++++---- doc/examples/using_julia.ipynb | 69 ++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 34 deletions(-) diff --git a/doc/examples/model_julia/SIR.jl b/doc/examples/model_julia/SIR.jl index d5a2115b5..fd89ec0bd 100644 --- a/doc/examples/model_julia/SIR.jl +++ b/doc/examples/model_julia/SIR.jl @@ -12,12 +12,14 @@ using Catalyst sir_model = @reaction_network begin r1, S + I --> 2I r2, I --> R -end r1 r2 +end +@unpack S, I, R = sir_model +@unpack r1, r2 = sir_model # ground truth parameter -p = (0.0001, 0.01) +p = Dict(r1 => 0.0001, r2 => 0.01) # initial state -u0 = [999, 1, 0] +u0 = Dict(S => 999, I => 1, R => 0) # time span tspan = (0.0, 250.0) # formulate as discrete problem @@ -33,13 +35,13 @@ jump_prob = JumpProblem( Simulate model for parameters `10.0.^par`. """ function model(par) - p = 10.0.^((par["p1"], par["p2"])) - sol = solve(remake(jump_prob, p=p), SSAStepper(), saveat=2.5) - return Dict("t"=>sol.t, "u"=>sol.u) + pmap = (r1 => 10.0^(par["p1"]), r2 => 10.0^(par["p2"])) + sol = solve(remake(jump_prob; p=pmap), SSAStepper(); saveat=2.5) + return Dict("t" => sol.t, "u" => sol.u) end # observed data -observation = model(Dict("p1"=>log10(p[1]), "p2"=>log10(p[2]))) +observation = model(Dict("p1" => log10(p[r1]), "p2" => log10(p[r2]))) """ Distance between model simulations or observed data `y` and `y0`. diff --git a/doc/examples/using_julia.ipynb b/doc/examples/using_julia.ipynb index 02f95fdbb..d7ae0cc40 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -29,8 +29,25 @@ }, { "cell_type": "code", + "execution_count": null, + "id": "6eb3dcee4e697522", + "metadata": {}, + "outputs": [], + "source": [ + "import julia\n", + "\n", + "julia.install()\n", + "# For further information, see https://pyjulia.readthedocs.io/en/latest/installation.html.\n", + "# There are some known problems, e.g. with statically linked Python interpreters, see\n", + "# https://pyjulia.readthedocs.io/en/latest/troubleshooting.html for details." + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "0297966c-447b-47cc-ad70-423fab03ecc4", "metadata": {}, + "outputs": [], "source": [ "import tempfile\n", "\n", @@ -41,9 +58,7 @@ "from pyabc.external.julia import Julia\n", "\n", "pyabc.settings.set_figure_params('pyabc') # for beautified plots" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -57,24 +72,24 @@ }, { "cell_type": "code", + "execution_count": null, "id": "a9ed8225-6884-44ab-9d73-a559cd916e68", "metadata": {}, + "outputs": [], "source": [ "%%time\n", "jl = Julia(module_name='SIR', source_file='model_julia/SIR.jl')" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "code", + "execution_count": null, "id": "ce3cd228-8798-42d2-9fef-6b1558758e2a", "metadata": {}, + "outputs": [], "source": [ "jl.display_source_ipython()" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -86,17 +101,17 @@ }, { "cell_type": "code", + "execution_count": null, "id": "b44e7de7-2e85-4116-83eb-86375065d3c5", "metadata": {}, + "outputs": [], "source": [ "model = jl.model()\n", "distance = jl.distance()\n", "obs = jl.observation()\n", "\n", "_ = plt.plot(obs['t'], obs['u'])" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -108,8 +123,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "19c2edea-0344-4c95-a928-5b34f1271c1d", "metadata": {}, + "outputs": [], "source": [ "gt_par = {'p1': -4.0, 'p2': -2.0}\n", "\n", @@ -121,9 +138,7 @@ "prior = Distribution(\n", " **{key: RV('uniform', lb, ub - lb) for key, (lb, ub) in par_limits.items()}\n", ")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -135,13 +150,13 @@ }, { "cell_type": "code", + "execution_count": null, "id": "f47a8141-e1e4-4556-b326-14453e1e01d9", "metadata": {}, + "outputs": [], "source": [ "distance(model(gt_par), model(gt_par))" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -153,8 +168,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "11832983-ac20-495e-b439-de59f61c1921", "metadata": {}, + "outputs": [], "source": [ "abc = ABCSMC(\n", " model,\n", @@ -165,9 +182,7 @@ "db = tempfile.mkstemp(suffix='.db')[1]\n", "abc.new('sqlite:///' + db, obs)\n", "h = abc.run(max_nr_populations=10)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -179,8 +194,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "a9e65eb6-ddd3-4317-96a7-d737f0b6b5d3", "metadata": {}, + "outputs": [], "source": [ "for t in [0, h.max_t]:\n", " pyabc.visualization.plot_kde_matrix_highlevel(\n", @@ -192,9 +209,7 @@ " )\n", " plt.gcf().suptitle(f'Posterior at t={t}')\n", " plt.gcf().tight_layout()" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -206,8 +221,10 @@ }, { "cell_type": "code", + "execution_count": null, "id": "6124db18-a5d6-44ac-ba33-6300864ce942", "metadata": {}, + "outputs": [], "source": [ "def plot_data(sumstat, weight, ax, **kwargs): # noqa: ARG001\n", " \"\"\"Plot a single trajectory\"\"\"\n", @@ -225,9 +242,7 @@ " )\n", " ax.plot(obs['t'], obs['u'])\n", " ax.set_title(f'Simulations at t={t}')" - ], - "outputs": [], - "execution_count": null + ] } ], "metadata": { From fc81000eb31cc7a421b9c54557a2c117d38a127a Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 10:38:58 +0100 Subject: [PATCH 47/55] fix notebooks 2 julia --- pyabc/transition/transitionmeta.py | 1 + pyproject.toml | 11 ++--------- test/run_notebooks.sh | 6 ++++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pyabc/transition/transitionmeta.py b/pyabc/transition/transitionmeta.py index 13c4b8f96..69ca4a0b2 100644 --- a/pyabc/transition/transitionmeta.py +++ b/pyabc/transition/transitionmeta.py @@ -63,3 +63,4 @@ def __init__(cls, name, bases, attrs): cls.pdf = wrap_pdf(cls.pdf) cls.rvs = wrap_rvs(cls.rvs) cls.rvs_single = wrap_rvs_single(cls.rvs_single) + cls.no_parameters = False diff --git a/pyproject.toml b/pyproject.toml index 1ab1b083a..fcaba8d0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ description = "Distributed, likelihood-free ABC-SMC inference" readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.10" -authors = [{ name = "The pyABC developers", email = "jonas.arruda@uni-bonn.de" }] +authors = [{ name = "The pyABC developers", email = "jan.hasenauer@uni-bonn.de" }] maintainers = [{ name = "Jonas Arruda", email = "jonas.arruda@uni-bonn.de" }] license = "BSD-3-Clause" @@ -115,13 +115,6 @@ test = [ "pytest-xdist>=3.5.0", "coverage[toml]>=7.0.0", ] -dev = [ - "pre-commit>=4.0.0", - "tox>=4.0.0", - "ruff>=0.8.0", - "build>=1.0.0", - "twine>=4.0.0", -] [project.scripts] abc-server-flask = "pyabc.visserver.server_flask:run_app" @@ -215,7 +208,7 @@ source = [ [dependency-groups] dev = [ "pre-commit>=4.5.1", - "tox>=4.36.0", + "tox>=4.38.0", "ruff>=0.8.0", "build>=1.0.0", "twine>=4.0.0", diff --git a/test/run_notebooks.sh b/test/run_notebooks.sh index 8124c16f6..fb600b88b 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -70,8 +70,10 @@ run_notebook () { # Run a notebook and raise upon failure tempfile=$(mktemp) echo $@ - python -m jupyter nbconvert --ExecutePreprocessor.timeout=-1 --debug \ - --stdout --execute --to markdown $@ &> $tempfile + python -m jupyter nbconvert \ + --ExecutePreprocessor.timeout=-1 \ + --ExecutePreprocessor.kernel_name=python3 \ + --debug --stdout --execute --to markdown "$@" &> "$tempfile" ret=$? if [[ $ret != 0 ]]; then cat $tempfile From f6cecdadc2a289c19509c4219134d7c4bee91e7a Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 11:17:42 +0100 Subject: [PATCH 48/55] fix notebooks 2 julia --- doc/conf.py | 6 ++ doc/examples/model_julia/SIR.jl | 11 ++-- doc/examples/using_julia.ipynb | 99 ++++++++++++++++++++++++++++++++- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 43872dc93..95f437b58 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -346,3 +346,9 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False + +# -- Options for Notebooks output ------------------------------------------- +nbsphinx_execute_arguments = [ + '--ExecutePreprocessor.kernel_name=python3', + '--ExecutePreprocessor.timeout=-1', +] diff --git a/doc/examples/model_julia/SIR.jl b/doc/examples/model_julia/SIR.jl index fd89ec0bd..35e8f5c17 100644 --- a/doc/examples/model_julia/SIR.jl +++ b/doc/examples/model_julia/SIR.jl @@ -3,12 +3,14 @@ module SIR # Install dependencies -using Pkg -Pkg.add("Catalyst") -Pkg.add("JumpProcesses") +#using Pkg +#Pkg.add("Catalyst") +#Pkg.add("JumpProcesses") -# Define reaction network using Catalyst +using JumpProcesses + +# Define reaction network sir_model = @reaction_network begin r1, S + I --> 2I r2, I --> R @@ -26,7 +28,6 @@ tspan = (0.0, 250.0) prob = DiscreteProblem(sir_model, u0, tspan, p) # formulate as Markov jump process -using JumpProcesses jump_prob = JumpProblem( sir_model, prob, Direct(), save_positions=(false, false), ) diff --git a/doc/examples/using_julia.ipynb b/doc/examples/using_julia.ipynb index d7ae0cc40..028c4c770 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -29,10 +29,72 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "6eb3dcee4e697522", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-18T10:15:20.894619Z", + "start_time": "2026-02-18T10:15:18.460926Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m\u001b[1m[ \u001b[22m\u001b[39m\u001b[36m\u001b[1mInfo: \u001b[22m\u001b[39mJulia version info\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Julia Version 1.12.2\n", + "Commit ca9b6662be4 (2025-11-20 16:25 UTC)\n", + "Build Info:\n", + " Official https://julialang.org release\n", + "Platform Info:\n", + " OS: macOS (arm64-apple-darwin24.0.0)\n", + " uname: Darwin 25.3.0 Darwin Kernel Version 25.3.0: Wed Jan 28 20:53:15 PST 2026; root:xnu-12377.81.4~5/RELEASE_ARM64_T6000 arm64 arm\n", + " CPU: Apple M1 Pro: \n", + " speed user nice sys idle irq\n", + " #1-10 2400 MHz 207298 s 0 s 122616 s 3611702 s 0 s\n", + " Memory: 16.0 GB (181.53125 MB free)\n", + " Uptime: 42240.0 sec\n", + " Load Avg: 5.88818359375 4.306640625 3.7685546875\n", + " WORD_SIZE: 64\n", + " LLVM: libLLVM-18.1.7 (ORCJIT, apple-m1)\n", + " GC: Built with stock GC\n", + "Threads: 1 default, 1 interactive, 1 GC (on 8 virtual cores)\n", + "Environment:\n", + " HOMEBREW_PREFIX = /opt/homebrew\n", + " INFOPATH = /opt/homebrew/share/info:\n", + " PYTHONPATH = /Users/jonas.arruda/PyCharm Projects/pyABC\n", + " HOME = /Users/jonas.arruda\n", + " HOMEBREW_REPOSITORY = /opt/homebrew\n", + " PATH = /Users/jonas.arruda/PyCharm Projects/pyABC/.venv/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/pmk/env/global/bin:/Library/TeX/texbin:/Users/jonas.arruda/.juliaup/bin:/Users/jonas.arruda/.codeium/windsurf/bin:/Users/jonas.arruda/miniconda3/bin:/Library/Frameworks/Python.framework/Versions/3.10/bin:/Library/Frameworks/Python.framework/Versions/3.11/bin:/Library/Frameworks/Python.framework/Versions/3.12/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/Users/jonas.arruda/.local/bin\n", + " XPC_FLAGS = 0x0\n", + " HOMEBREW_CELLAR = /opt/homebrew/Cellar\n", + " TERM = xterm-color\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[36m\u001b[1m[ \u001b[22m\u001b[39m\u001b[36m\u001b[1mInfo: \u001b[22m\u001b[39mJulia executable: /Users/jonas.arruda/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia\n", + "\u001b[36m\u001b[1m[ \u001b[22m\u001b[39m\u001b[36m\u001b[1mInfo: \u001b[22m\u001b[39mTrying to import PyCall...\n", + "\u001b[36m\u001b[1m┌ \u001b[22m\u001b[39m\u001b[36m\u001b[1mInfo: \u001b[22m\u001b[39mPyCall is already installed and compatible with Python executable.\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39mPyCall:\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m python: /Users/jonas.arruda/PyCharm Projects/pyABC/.venv/bin/python\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m libpython: /Users/jonas.arruda/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/libpython3.12.dylib\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39mPython:\n", + "\u001b[36m\u001b[1m│ \u001b[22m\u001b[39m python: /Users/jonas.arruda/PyCharm Projects/pyABC/.venv/bin/python\n", + "\u001b[36m\u001b[1m└ \u001b[22m\u001b[39m libpython: /Users/jonas.arruda/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/libpython3.12.dylib\n" + ] + } + ], "source": [ "import julia\n", "\n", @@ -42,6 +104,37 @@ "# https://pyjulia.readthedocs.io/en/latest/troubleshooting.html for details." ] }, + { + "cell_type": "code", + "execution_count": 6, + "id": "528a275478a75316", + "metadata": { + "ExecuteTime": { + "end_time": "2026-02-18T10:17:29.356969Z", + "start_time": "2026-02-18T10:17:12.518257Z" + } + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m\u001b[1m Resolving\u001b[22m\u001b[39m package versions...\n", + "\u001b[36m\u001b[1m Project\u001b[22m\u001b[39m No packages added to or removed from `~/.julia/environments/v1.12/Project.toml`\n", + "\u001b[36m\u001b[1m Manifest\u001b[22m\u001b[39m No packages added to or removed from `~/.julia/environments/v1.12/Manifest.toml`\n", + "\u001b[32m\u001b[1m Resolving\u001b[22m\u001b[39m package versions...\n", + "\u001b[36m\u001b[1m Project\u001b[22m\u001b[39m No packages added to or removed from `~/.julia/environments/v1.12/Project.toml`\n", + "\u001b[36m\u001b[1m Manifest\u001b[22m\u001b[39m No packages added to or removed from `~/.julia/environments/v1.12/Manifest.toml`\n" + ] + } + ], + "source": [ + "from julia import Pkg\n", + "\n", + "Pkg.add('Catalyst')\n", + "Pkg.add('JumpProcesses')" + ] + }, { "cell_type": "code", "execution_count": null, From 68fcae33157489b9fd4b6ecc331475be98fb2ee2 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 11:27:20 +0100 Subject: [PATCH 49/55] check worker health before blocking on queue retrieval --- pyabc/sampler/multicorebase.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyabc/sampler/multicorebase.py b/pyabc/sampler/multicorebase.py index 404605dc3..6204eb271 100644 --- a/pyabc/sampler/multicorebase.py +++ b/pyabc/sampler/multicorebase.py @@ -94,9 +94,11 @@ def get_if_worker_healthy(workers: list[Process], queue: Queue): item: An item from the queue """ while True: + # Check health first before potentially blocking + if not healthy(workers): + raise ProcessError('At least one worker is dead.') from None try: - item = queue.get(True, 5) + item = queue.get(timeout=0.1) return item except Empty: - if not healthy(workers): - raise ProcessError('At least one worker is dead.') from None + pass From d4eede43ad5beeb371dd60c027042dc9d34d21eb Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 11:34:23 +0100 Subject: [PATCH 50/55] update distributed for python3.12 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fcaba8d0a..a4c3512a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "scikit-learn>=0.23.1", "click>=7.1.2", "redis>=2.10.6", - "distributed>=2022.10.2", + "distributed>=2024.2.0", "matplotlib>=3.3.0", "sqlalchemy>=2.0.0", "jabbar>=0.0.10", From 59b28059e7196b2e1cd418b0e69d2e5e0ffa6084 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 11:55:14 +0100 Subject: [PATCH 51/55] fix dask for python3.12 --- pyabc/sampler/dask_sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyabc/sampler/dask_sampler.py b/pyabc/sampler/dask_sampler.py index 9ce6ba9bc..42380252e 100644 --- a/pyabc/sampler/dask_sampler.py +++ b/pyabc/sampler/dask_sampler.py @@ -43,7 +43,7 @@ def __init__( self, dask_client: Client = None, client_max_jobs: int = np.inf, - default_pickle: bool = True, + default_pickle: bool = False, batch_size: int = 1, ): # Assign Client From 3bf084cb598e7c5dfab2f4dff266d7463f4ce22a Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 12:24:54 +0100 Subject: [PATCH 52/55] remove fix dask for python3.12 --- pyabc/sampler/dask_sampler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyabc/sampler/dask_sampler.py b/pyabc/sampler/dask_sampler.py index 42380252e..9ce6ba9bc 100644 --- a/pyabc/sampler/dask_sampler.py +++ b/pyabc/sampler/dask_sampler.py @@ -43,7 +43,7 @@ def __init__( self, dask_client: Client = None, client_max_jobs: int = np.inf, - default_pickle: bool = False, + default_pickle: bool = True, batch_size: int = 1, ): # Assign Client From fa23885efeeac9603e1323e5e14ada694f77e842 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 12:43:25 +0100 Subject: [PATCH 53/55] fix python3.12 test --- .github/workflows/ci.yml | 2 ++ test/base/test_samplers.py | 3 +++ tox.ini | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f139523b0..9ed1ff82e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,8 @@ jobs: - name: Run tox run: tox -e ${{ matrix.toxenv }} + env: + pythonLocation: ${{ env.pythonLocation }} # If each tox env generates coverage.xml, include flags; otherwise combine explicitly - name: Upload coverage diff --git a/test/base/test_samplers.py b/test/base/test_samplers.py index 8f27754ef..f0fd1a8b4 100644 --- a/test/base/test_samplers.py +++ b/test/base/test_samplers.py @@ -181,6 +181,9 @@ def distance(y1, y2): def test_two_competing_gaussians_multiple_population(db_path, sampler): + logging.info( + f'Testing sampler {sampler.__class__.__name__} with 2 competing gaussians' + ) two_competing_gaussians_multiple_population(db_path, sampler) diff --git a/tox.ini b/tox.ini index 916404fa7..733e12ab3 100644 --- a/tox.ini +++ b/tox.ini @@ -42,8 +42,9 @@ description = Clean up before tests [testenv:base] +passenv = pythonLocation LD_LIBRARY_PATH setenv = - LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib + LD_LIBRARY_PATH = {env:pythonLocation}/lib:{env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib extras = test r From 21aed8c8cab4156d3f248f2c8aca2288e713f4d4 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 13:07:06 +0100 Subject: [PATCH 54/55] fix python3.12 test --- .github/workflows/ci.yml | 8 +++----- tox.ini | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ed1ff82e..f4211e90b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,9 @@ jobs: - os: ubuntu-latest python: "3.13" toxenv: base - - os: ubuntu-latest - python: "3.12" - toxenv: base + #- os: ubuntu-latest + # python: "3.12" + # toxenv: base - os: ubuntu-latest python: "3.11" toxenv: base @@ -93,8 +93,6 @@ jobs: - name: Run tox run: tox -e ${{ matrix.toxenv }} - env: - pythonLocation: ${{ env.pythonLocation }} # If each tox env generates coverage.xml, include flags; otherwise combine explicitly - name: Upload coverage diff --git a/tox.ini b/tox.ini index 733e12ab3..916404fa7 100644 --- a/tox.ini +++ b/tox.ini @@ -42,9 +42,8 @@ description = Clean up before tests [testenv:base] -passenv = pythonLocation LD_LIBRARY_PATH setenv = - LD_LIBRARY_PATH = {env:pythonLocation}/lib:{env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib + LD_LIBRARY_PATH = {env:LD_LIBRARY_PATH:/usr/lib}:/usr/local/lib/R/lib extras = test r From e5155db46c386c57669f7d16a0be004a60cee759 Mon Sep 17 00:00:00 2001 From: arrjon Date: Wed, 18 Feb 2026 17:47:04 +0100 Subject: [PATCH 55/55] fix doc contact details [skip ci] --- doc/about.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/about.rst b/doc/about.rst index dbe899e98..e7dbe7176 100644 --- a/doc/about.rst +++ b/doc/about.rst @@ -13,12 +13,11 @@ Authors ------- The first main developer of the package was Emmanuel Klinger, with major contributions from Dennis Rickert and Nick Jagiella. -Currently, the main developer and maintainer is Yannik Schaelte. -Additional maintainers are Jonas Arruda and Stephan Grein. -Emad Alamoudi is co-developer with various contributions. +Then, the main developer and maintainer was Yannik Schaelte. +Emad Alamoudi was co-developer with various contributions. Elba Raimúndez-Álvarez contributed to the examples. Felipe Reck contributed to the "look-ahead" Redis-based sampler and example notebook. - +Currently, maintainers are Jonas Arruda and Stephan Grein. Contact ------- @@ -27,8 +26,6 @@ Discovered an error? Need help? Not sure if something works as intended? Please If you think that your issue could be of general interest, please consider creating an issue on github, which will then also be helpful for other users: https://github.com/icb-dcm/pyabc/issues -If you prefer to contact us via e-mail: `yannik.schaelte@helmholtz-muenchen.de `_ - License -------