diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 787094bc..00000000 --- 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d232896..f4211e90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,263 +1,104 @@ name: CI -# trigger on: push: - branches: - - main - - develop + branches: [main, develop] pull_request: schedule: - # run Monday at 03:18 UTC - - cron: '18 15 * * MON' + - cron: "18 15 * * MON" jobs: - - base: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.12', '3.11', '3.10'] - - 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 }}-base - - - 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: 3 - run: tox -e visualization - - - name: Coverage - uses: codecov/codecov-action@v2 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml - - external: - runs-on: ubuntu-latest - strategy: - matrix: - 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 - - petab: - runs-on: ubuntu-latest - strategy: - matrix: - 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 - - mac: - runs-on: macos-latest + tests: + name: ${{ matrix.toxenv }} (${{ matrix.python }} • ${{ matrix.os }}) + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - python-version: ['3.11'] + 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: quality + - os: ubuntu-latest + python: "3.11" + toxenv: project + - os: ubuntu-latest + python: "3.11" + toxenv: doc + - os: ubuntu-latest + python: "3.11" + toxenv: migrate + + - os: ubuntu-latest + python: "3.11" + toxenv: external-R + - os: ubuntu-latest + python: "3.11" + toxenv: external-other-simulators + - os: ubuntu-latest + python: "3.11" + toxenv: petab + - os: ubuntu-latest + python: "3.11" + toxenv: base-notebooks + - os: ubuntu-latest + python: "3.11" + toxenv: external-notebooks steps: - - name: Check out repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Prepare python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python-version }} + python-version: ${{ matrix.python }} + cache: pip - - name: Cache - uses: actions/cache@v4 + - name: Install Julia + if: ${{ startsWith(matrix.toxenv, 'external-') }} + uses: julia-actions/setup-julia@v2 with: - path: ~/Library/Caches/pip - key: ci-${{ runner.os }}-${{ matrix.python-version }}-mac + version: "1.11" - name: Install dependencies - run: .github/workflows/install_deps.sh - - - name: Run tests - timeout-minutes: 10 - run: tox -e mac - - - name: Coverage - uses: codecov/codecov-action@v2 + run: | + case "${{ matrix.toxenv }}" in + 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 amici ;; + 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 }} - file: ./coverage.xml - - notebooks1: - runs-on: ubuntu-latest - strategy: - matrix: - 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 }}-notebooks1 - - - name: Install dependencies - run: .github/workflows/install_deps.sh - - - name: Run notebooks - timeout-minutes: 15 - run: tox -e notebooks1 - - notebooks2: - runs-on: ubuntu-latest - strategy: - matrix: - 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 }}-notebooks2 - - - name: Install dependencies - run: .github/workflows/install_deps.sh R amici - - - name: Run notebooks - timeout-minutes: 15 - run: tox -e notebooks2 - - quality: - runs-on: ubuntu-latest - strategy: - matrix: - 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 + files: ./coverage.xml + flags: ${{ matrix.toxenv }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 712772ec..972f4ea6 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 9a20a06b..00000000 --- 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 558c7df2..d497a6f3 100755 --- a/.github/workflows/install_deps.sh +++ b/.github/workflows/install_deps.sh @@ -1,101 +1,140 @@ -#!/bin/sh - -# pip -python -m pip install --upgrade pip - -# wheel -pip install wheel - -# tox -pip install tox - -# update apt package lists -sudo apt-get update - -# 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 - ;; - - 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 - -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 - - 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 - ;; - - amici) - # AMICI dependencies - sudo apt-get install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev - - # pip install amici - pip uninstall amici pyabc - pip install 'pyabc[amici]' - ;; - - doc) - # documentation - sudo apt-get install pandoc - ;; - - *) - echo "Unknown argument" >&2 - exit 1 - ;; - - esac -done +#!/usr/bin/env bash +set -euo pipefail + +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*"; } + +is_macos() { [[ "$(uname -s)" == "Darwin" ]]; } + +_APT_UPDATED=0 +apt_update_once() { + if [[ "${_APT_UPDATED}" == "0" ]]; then + _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 "$@" +} + +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 + sudo service redis-server start || true + fi +} + +install_r() { + log_info "Installing R..." + if is_macos; then + brew install r + else + # Prefer distro packages in CI + 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 + + 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 +} + +install_amici() { + log_info "Installing AMICI dependencies..." + if ! is_macos; then + apt_install swig libatlas-base-dev libhdf5-serial-dev libboost-all-dev + fi + log_info "Installing AMICI Python package..." + python -m pip uninstall -y amici pyabc || true + python -m pip install --upgrade "pyabc[amici]" +} + +install_doc_tools() { + log_info "Installing documentation tools..." + if is_macos; then + brew install pandoc || true + else + apt_install pandoc + fi +} + +install_dev_tools() { + log_info "Installing development tools..." + python -m pip install --upgrade pre-commit ruff build twine pytest pytest-cov pytest-xdist +} + +install_all() { + install_base + install_r + install_amici + install_doc_tools + install_dev_tools +} + +usage() { + cat <`_ - License ------- diff --git a/doc/conf.py b/doc/conf.py index 95b590a9..95f437b5 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. @@ -347,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/adaptive_distances.ipynb b/doc/examples/adaptive_distances.ipynb index ac59ef83..2fb2d198 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 77de7006..3ed80c8a 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 aaad3eed..ae3ddf8f 100644 --- a/doc/examples/chemical_reaction.ipynb +++ b/doc/examples/chemical_reaction.ipynb @@ -48,22 +48,25 @@ }, { "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": 1, "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 +113,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", @@ -123,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", @@ -138,25 +143,25 @@ }, { "cell_type": "code", - "execution_count": 2, "metadata": {}, - "outputs": [], "source": [ "MAX_T = 0.1\n", "\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}" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -168,15 +173,15 @@ }, { "cell_type": "code", - "execution_count": 3, "metadata": {}, - "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]])" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -187,37 +192,23 @@ }, { "cell_type": "code", - "execution_count": 4, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "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", + "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)" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -245,9 +236,7 @@ }, { "cell_type": "code", - "execution_count": 5, "metadata": {}, - "outputs": [], "source": [ "N_TEST_TIMES = 20\n", "\n", @@ -255,14 +244,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" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -273,14 +264,12 @@ }, { "cell_type": "code", - "execution_count": 6, "metadata": {}, - "outputs": [], "source": [ - "from pyabc import RV, Distribution\n", - "\n", - "prior = Distribution(rate=RV(\"uniform\", 0, 100))" - ] + "prior = Distribution(rate=RV('uniform', 0, 100))" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -291,28 +280,17 @@ }, { "cell_type": "code", - "execution_count": 7, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:Sampler:Parallelizing the sampling on 4 cores.\n" - ] - } - ], "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),\n", ")" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -324,20 +302,12 @@ }, { "cell_type": "code", - "execution_count": 8, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:History:Start \n" - ] - } - ], "source": [ - "abc_id = abc.new(\"sqlite:////tmp/mjp.db\", observations[0])" - ] + "abc_id = abc.new('sqlite:////tmp/mjp.db', observations[0])" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -348,68 +318,12 @@ }, { "cell_type": "code", - "execution_count": 9, "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" - ] - } - ], "source": [ "history = abc.run(minimum_epsilon=0.7, max_nr_populations=15)" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -420,30 +334,17 @@ }, { "cell_type": "code", - "execution_count": 10, "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" - } - ], "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", ");" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -456,29 +357,12 @@ }, { "cell_type": "code", - "execution_count": 11, "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" - } - ], "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", - "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,18 +370,20 @@ " 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()" - ] + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -512,27 +398,14 @@ }, { "cell_type": "code", - "execution_count": 12, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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\n", - "text/plain": [ - "
" - ] - }, - "metadata": { - "needs_background": "light" - }, - "output_type": "display_data" - } - ], "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');" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", diff --git a/doc/examples/conversion_reaction.ipynb b/doc/examples/conversion_reaction.ipynb index 96c48bf1..294c7773 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/custom_priors.ipynb b/doc/examples/custom_priors.ipynb index 7bbe72c8..7297dd0f 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 644236bc..a794a3e6 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/early_stopping.ipynb b/doc/examples/early_stopping.ipynb index 7ff1eef3..aaccc39d 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/external_simulators.ipynb b/doc/examples/external_simulators.ipynb index 9ddba076..83adb0cf 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/informative.ipynb b/doc/examples/informative.ipynb index 6a70a2eb..4e7a7d38 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 9ae7111b..169c2e0f 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/model_julia/SIR.jl b/doc/examples/model_julia/SIR.jl index d5a2115b..35e8f5c1 100644 --- a/doc/examples/model_julia/SIR.jl +++ b/doc/examples/model_julia/SIR.jl @@ -3,28 +3,31 @@ 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 -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 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), ) @@ -33,13 +36,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/noise.ipynb b/doc/examples/noise.ipynb index 57ecc738..d753e8de 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 be11630b..8412dbf0 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/doc/examples/petab_application.ipynb b/doc/examples/petab_application.ipynb index 2f530b76..7332092e 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" ] }, { @@ -30,13 +30,9 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", + "import petab.v1 as petab\n", + "from amici.petab import import_petab_problem\n", "\n", - "import amici.petab_import\n", - "import numpy as np\n", - "import petab\n", - "\n", - "import pyabc\n", "from pyabc.petab import AmiciPetabImporter" ] }, @@ -57,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" @@ -87,12 +83,12 @@ "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", - "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 834a95f0..8c595784 100644 --- a/doc/examples/petab_yaml2sbml.ipynb +++ b/doc/examples/petab_yaml2sbml.ipynb @@ -26,17 +26,17 @@ ] }, { - "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 matplotlib.pyplot as plt\n", + "import amici\n", "import numpy as np\n", + "from amici.petab import import_petab_problem\n", "\n", "import pyabc\n", "import pyabc.petab\n", @@ -61,21 +61,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "YAML file is valid ✅\n" - ] - } - ], + "outputs": [], "source": [ "import shutil\n", "\n", - "import petab\n", + "import petab.v1 as petab\n", "import yaml2sbml\n", "\n", "# check yaml file\n", @@ -168,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" ] } ], @@ -214,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -225,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", @@ -444,7 +436,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 1f603759..fe38cbeb 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 84c88ccd..028c4c77 100644 --- a/doc/examples/using_julia.ipynb +++ b/doc/examples/using_julia.ipynb @@ -29,7 +29,115 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, + "id": "6eb3dcee4e697522", + "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", + "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": 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, "id": "0297966c-447b-47cc-ad70-423fab03ecc4", "metadata": {}, "outputs": [], @@ -57,170 +165,21 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "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')" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "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" - } - ], + "outputs": [], "source": [ "jl.display_source_ipython()" ] @@ -235,29 +194,16 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "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" - } - ], + "outputs": [], "source": [ "model = jl.model()\n", "distance = jl.distance()\n", "obs = jl.observation()\n", "\n", - "_ = plt.plot(obs[\"t\"], obs[\"u\"])" + "_ = plt.plot(obs['t'], obs['u'])" ] }, { @@ -270,20 +216,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "19c2edea-0344-4c95-a928-5b34f1271c1d", "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", ")" ] }, @@ -297,21 +243,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "f47a8141-e1e4-4556-b326-14453e1e01d9", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "297286.7590759076" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "distance(model(gt_par), model(gt_par))" ] @@ -326,42 +261,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "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" - ] - } - ], + "outputs": [], "source": [ "abc = ABCSMC(\n", " model,\n", @@ -369,8 +272,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)" ] }, @@ -384,35 +287,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "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" - } - ], + "outputs": [], "source": [ "for t in [0, h.max_t]:\n", " pyabc.visualization.plot_kde_matrix_highlevel(\n", @@ -420,10 +298,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()" ] }, { @@ -436,40 +314,15 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "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" - } - ], + "outputs": [], "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 +333,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/__init__.py b/pyabc/__init__.py index cb2e774b..6febf934 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 @@ -140,7 +155,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/acceptor/acceptor.py b/pyabc/acceptor/acceptor.py index c4d82451..21d2497a 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 2e96a2a7..f9afbf29 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 ab70acd5..ba0f52ff 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 913cdc4e..85372819 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 28a93f88..b7f60f95 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 7f8c20e1..c6cb12ef 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 d902d6cf..a01b81be 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,16 @@ 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[0, 1].' ) self.p = p @@ -441,7 +441,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 +470,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 +500,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 +530,14 @@ 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[0, 1].' ) self.p = p @@ -563,7 +562,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 b81f2553..c0bfa343 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 a23e0b12..e77d6298 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 b4af7179..ef4b1ca2 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 d7d209df..292309de 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: @@ -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 @@ -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 72b9e3db..93f915e3 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 b5394dc9..14f78739 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 1ce5c306..8d50cbf6 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 8d85f1e6..65e257b7 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,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 @@ -589,13 +586,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 +605,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 +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 @@ -663,11 +660,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 +686,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 +709,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 +725,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 +757,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 +783,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 66764e0d..c6855250 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,8 @@ 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} + stdout = {} if self.show_stdout else {'stdout': subprocess.DEVNULL} + stderr = {} if self.show_stderr else {'stderr': subprocess.DEVNULL} # call try: @@ -161,13 +156,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 +196,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 +234,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): @@ -315,7 +310,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 @@ -341,10 +336,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 +365,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 +385,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 +413,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 a1c8d640..df01ebf7 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/external/r/r_rpy2.py b/pyabc/external/r/r_rpy2.py index feb5d407..20c57c3c 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 ( @@ -19,27 +19,27 @@ pandas2ri, r, ) + from rpy2.robjects.conversion import get_conversion, localconverter + except ImportError: ListVector = conversion = r = None default_converter = numpy2ri = pandas2ri = None 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) - with conversion.localconverter( - default_converter + pandas2ri.converter + numpy2ri.converter - ): - for key, val in dct.items(): - dct[key] = conversion.py2rpy(val) - r_list = ListVector(dct) - return r_list + + conv = get_conversion() + conv = ( + conv + default_converter + pandas2ri.converter + numpy2ri.converter + ) + + with localconverter(conv): + dct = {key: conv.py2rpy(val) for key, val in dct.items()} + + return ListVector(dct) + return dct @@ -77,9 +77,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 +164,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/inference/smc.py b/pyabc/inference/smc.py index 5edf27ad..7185bbe2 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 61a04076..89b9f28d 100644 --- a/pyabc/inference_util/inference_util.py +++ b/pyabc/inference_util/inference_util.py @@ -1,25 +1,26 @@ -# Note: Due to cyclic imports, these need to be separated from other modules +from __future__ import annotations import logging import uuid +from collections.abc import Callable from datetime import datetime, timedelta -from typing import Callable, List +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: # to avoid circular imports + 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") +logger = logging.getLogger('ABC') class AnalysisVars: @@ -31,10 +32,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 +76,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. @@ -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 @@ -134,9 +136,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. @@ -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 @@ -188,8 +192,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 +201,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, @@ -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 @@ -253,7 +259,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 +281,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 +308,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 +359,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 +473,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, @@ -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,8 +523,10 @@ def evaluate_preliminary_particle( ------- evaluated_particle: The evaluated particle """ + from ..population import 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 +540,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 +593,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 +630,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 8a41a52b..901c103d 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/parameters/parameters.py b/pyabc/parameters/parameters.py index a0956e1e..93fcb97f 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/petab/__init__.py b/pyabc/petab/__init__.py index 049d9b24..e163da90 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/petab/amici.py b/pyabc/petab/amici.py index ad0ccd32..23054375 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 c8373177..05d7910d 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/platform_factory.py b/pyabc/platform_factory.py index 3cd9e23b..b959a202 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/population/population.py b/pyabc/population/population.py index 58c4f49b..73c440ec 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 bf9fa8fb..3a528c45 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,9 @@ class directly. Subclasses must override the `update` method. def __init__(self, nr_calibration_particles: int = None): self.nr_calibration_particles = nr_calibration_particles - def update( + def update( # noqa: B027 self, - transitions: List[Transition], + transitions: list[Transition], model_weights: np.ndarray, t: int = None, ): @@ -60,6 +59,7 @@ def update( t: Time to adapt for. """ + pass @abstractmethod def __call__(self, t: int = None) -> int: @@ -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 e7a58e3b..5a29ec00 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 6d8ba31d..dfb66580 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 1e5b63d8..62bef737 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 dcdd0302..a391ec5d 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 1a74fb17..192e135d 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/mapping.py b/pyabc/sampler/mapping.py index e8821591..826ab390 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.py b/pyabc/sampler/multicore.py index 70ee0b71..fe7a7c79 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/multicore_evaluation_parallel.py b/pyabc/sampler/multicore_evaluation_parallel.py index 1e879839..7d138afb 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/multicorebase.py b/pyabc/sampler/multicorebase.py index ffbcc576..6204eb27 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 @@ -95,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.") + pass diff --git a/pyabc/sampler/redis_eps/cli.py b/pyabc/sampler/redis_eps/cli.py index 807979b2..36e60c27 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/cmd.py b/pyabc/sampler/redis_eps/cmd.py index 570cd23b..014f0759 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 58969418..79a23b2d 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/sampler.py b/pyabc/sampler/redis_eps/sampler.py index 1cff8970..5138444a 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 75603ae4..67272e52 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/sampler/redis_eps/server_starter.py b/pyabc/sampler/redis_eps/server_starter.py index 1162dd6c..14bac973 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/util.py b/pyabc/sampler/redis_eps/util.py index a47b3f21..d8c5a739 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/redis_eps/work.py b/pyabc/sampler/redis_eps/work.py index dc6e9229..6a6f2505 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 9c989802..3e304388 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/sampler/singlecore.py b/pyabc/sampler/singlecore.py index 6320fcc7..e839951c 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/settings/settings.py b/pyabc/settings/settings.py index a0d0a0c1..7c0b211b 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/config.py b/pyabc/sge/config.py index 0017ce96..491897c0 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/db.py b/pyabc/sge/db.py index b5aaac32..1463f608 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/execute_sge_array_job.py b/pyabc/sge/execute_sge_array_job.py index fb49dde2..cb7cc337 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/execution_contexts.py b/pyabc/sge/execution_contexts.py index 4b6486dd..81523ec4 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/job_info_redis.py b/pyabc/sge/job_info_redis.py index 65d9b923..3d9f5663 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 cadd5e79..dac9db6a 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,12 @@ 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 +186,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,17 +194,17 @@ 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): return ( - f"" + f'' ) @staticmethod @@ -227,8 +226,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 +255,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,20 +323,17 @@ 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( - 'Could not load temporary ' 'result file:' + str(e) - ) + Exception('Could not load temporary result file:' + str(e)) ) had_exception = True @@ -359,7 +355,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 +386,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/sge/test_sge.py b/pyabc/sge/test_sge.py index 435a868d..7878a8e6 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/sge/util.py b/pyabc/sge/util.py index 2c4ce0ad..8d895fdc 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 f96dfc19..e4d006b3 100644 --- a/pyabc/storage/bytes_storage.py +++ b/pyabc/storage/bytes_storage.py @@ -6,12 +6,18 @@ 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() + 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: + if isinstance(py_object_[col], pd.CategoricalDtype): + py_object_[col] = py_object_[col].astype(str) return py_object_ return object_ @@ -39,6 +45,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/dataframe_bytes_storage.py b/pyabc/storage/dataframe_bytes_storage.py index ea1024bf..150a270e 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 @@ -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: @@ -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/storage/db_export.py b/pyabc/storage/db_export.py index 368cd0d3..069fdac6 100644 --- a/pyabc/storage/db_export.py +++ b/pyabc/storage/db_export.py @@ -1,62 +1,58 @@ -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 +77,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 4fc13b95..1b7efbf3 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/df_to_file.py b/pyabc/storage/df_to_file.py index 20a6267b..4a0a430b 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/history.py b/pyabc/storage/history.py index a2f7a099..41177a1c 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,9 +80,9 @@ 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 + Convenience function to create a sqlite database identifier which can be understood by sqlalchemy. Parameters @@ -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 af1bdbc6..d2f91fdd 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/storage/migrate.py b/pyabc/storage/migrate.py index 7506ceb7..a5ddda71 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 6fb203c5..47e211c6 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 2bbb40ad..678c24a4 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 3a91285c..0aef357a 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,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/sumstat/learn.py b/pyabc/sumstat/learn.py index cf401b7a..89f8fba1 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 85757f99..60d4b242 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 9a2f74ef..818edf7c 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/grid_search.py b/pyabc/transition/grid_search.py index c58d8496..c5eaf0fc 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/transition/jump.py b/pyabc/transition/jump.py index dcf655e6..ab9f0d2c 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 9f9f500e..f87b53ca 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 a946c0cb..e8a24304 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 e0045c2c..1f830c74 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 46481164..5e764ce9 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 0ca85eff..ebbc71f8 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 07ec3700..69ca4a0b 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) @@ -65,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/pyabc/util/dict2arr.py b/pyabc/util/dict2arr.py index 1e205cc2..4d0602f7 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 a892b291..e71fcc36 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/log.py b/pyabc/util/log.py index 88be4ef7..5a07177c 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/par_trafo.py b/pyabc/util/par_trafo.py index 7c2a77ac..f5984892 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 ce748a07..48567a6b 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/util/test.py b/pyabc/util/test.py index e72bb99d..53eb3df8 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 9a7096d6..13823fd2 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. {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 44cc4aab..7b8967f7 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/colors.py b/pyabc/visualization/colors.py index f643c4fa..86b938d0 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 a2f1d941..5c5e5f35 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/credible.py b/pyabc/visualization/credible.py index cb941483..1e76b851 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, @@ -57,7 +55,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) @@ -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, ): @@ -470,7 +468,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) @@ -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 6603d7b8..cc60e070 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,39 @@ 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 not supported.' ) # remove not needed axis ax.axis('off') diff --git a/pyabc/visualization/distance.py b/pyabc/visualization/distance.py index 134aa567..c2023763 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 98bf5437..6a535915 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 7bb0c41a..4dc67d27 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/histogram.py b/pyabc/visualization/histogram.py index a17a4d3e..2183d615 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 85dbcaef..82f997b9 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/pyabc/visualization/model_probabilities.py b/pyabc/visualization/model_probabilities.py index 2c6226dc..c14410e3 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 cdf68de8..36fcec77 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 aeb59349..fe971992 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/util.py b/pyabc/visualization/util.py index 012bd1d7..f6d7a448 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/visualization/walltime.py b/pyabc/visualization/walltime.py index 83f1e753..52b9c5c2 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/pyabc/weighted_statistics/weighted_statistics.py b/pyabc/weighted_statistics/weighted_statistics.py index 959ed0f2..4a6e1dc5 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/pyproject.toml b/pyproject.toml index 6e340074..a4c3512a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,20 +1,218 @@ -############################# -# 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" -[tool.black] +[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 = "jan.hasenauer@uni-bonn.de" }] +maintainers = [{ name = "Jonas Arruda", email = "jonas.arruda@uni-bonn.de" }] + +license = "BSD-3-Clause" +license-files = ["LICENSE.txt"] + +keywords = [ + "likelihood-free", + "inference", + "abc", + "approximate Bayesian computation", + "sge", + "distributed", +] + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "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>=2024.2.0", + "matplotlib>=3.3.0", + "sqlalchemy>=2.0.0", + "jabbar>=0.0.10", + "gitpython>=3.1.7", + "ipykernel>=7.2.0", +] + +[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" +Repository = "https://github.com/icb-dcm/pyabc" +"Source Code" = "https://github.com/icb-dcm/pyabc" + +[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>=22.0.0"] +r = [ + "rpy2>=3.4.4", + "cffi>=1.14.5", + "ipython>=7.18.1", + "pygments>=2.6.1", +] +julia = [ + "julia>=0.6.2", + "pygments>=2.6.1", + "ipython>=7.18.1", +] +copasi = ["copasi-basico>=0.8"] +ot = [ + "pot>=0.7.0", + "cython>=0.29.0", +] +petab = ["petab>=0.2.0"] +#petab-test = ["petabtests>=0.0.1"] # problem with pysb +amici = ["amici>=0.32.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>=2.0.0", + "sphinx-autodoc-typehints>=2.0.0", + "ipython>=8.4.0", + "sphinx-autobuild>=2021.3.14", +] +test = [ + "pytest>=8.0.0", + "pytest-cov>=6.0.0", + "pytest-rerunfailures>=14.0.0", + "pytest-xdist>=3.5.0", + "coverage[toml]>=7.0.0", +] + +[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 + +[tool.setuptools.packages.find] +where = ["."] + +[tool.setuptools.dynamic] +version = { attr = "pyabc.version.__version__" } + +[tool.ruff] line-length = 79 -target-version = ['py37', 'py38', 'py39'] -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) + "B905", # zip() without an explicit strict= parameter + "SIM105", # Replace `try`-`except`-`pass` + "C408", + "E402" +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # unused imports +"test/**" = ["ARG", "SIM"] # Allow unused arguments and complex logic in tests -[tool.isort] -profile = "black" -line_length = 79 -multi_line_output = 3 +[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 = ["."] + +[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.38.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/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 9950b8f3..00000000 --- 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 51c81cb4..00000000 --- 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 b908cbe5..00000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -import setuptools - -setuptools.setup() diff --git a/test/base/conftest.py b/test/base/conftest.py index 03b0d088..e58fe7cb 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 f674bd63..9d67a9ab 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 8ccdb3af..c4275af1 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 @@ -15,105 +12,111 @@ @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): + from rpy2.robjects import r + 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_): + import rpy2.robjects as robjects + from rpy2.robjects import pandas2ri + from rpy2.robjects.conversion import get_conversion, localconverter + serial = to_bytes(object_) assert isinstance(serial, bytes) @@ -136,13 +139,17 @@ 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): - assert (robjects.conversion.rpy2py(object_) == rebuilt).all().all() + conv = get_conversion() + conv = conv + pandas2ri.converter + with localconverter(conv): + assert (conv.rpy2py(object_) == rebuilt).all().all() else: - raise Exception("Could not compare") + raise Exception('Could not compare') 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) @@ -158,7 +165,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 +173,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 6a0dc2ce..56c17e5b 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 13806162..e4ff8e11 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 dac1dfbd..01e980f7 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 0056ee75..4b4faacf 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 09776f10..b774a024 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 67bd303c..52ef5122 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 ddf2a40f..901c7a05 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 51cdc2e0..5fd25a34 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 cc9b0120..0a9689ca 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,8 @@ 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_predictor.py b/test/base/test_predictor.py index 03a85537..b5c13531 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 9959ffa9..6e65dfb0 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 53a21ef1..56009c58 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 658e74c4..f0fd1a8b 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) @@ -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) @@ -189,7 +192,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 +201,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 +211,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 +221,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 +231,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 +271,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 +289,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 +321,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 +332,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 +347,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 +361,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 +376,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 +391,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 +429,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 +510,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 +577,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 3fd5158b..217c8cc1 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 30e34bd9..1174ee70 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_storage.py b/test/base/test_storage.py index 48a2fe95..2e32cafe 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 @@ -30,31 +28,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 +64,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 +92,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 +120,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 +143,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 +163,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 +195,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 +223,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: @@ -242,65 +240,69 @@ 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 get_conversion, localconverter + arr = np.random.rand(10) arr2 = np.random.rand(10, 2) 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() - 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() - with localconverter(pandas2ri.converter): - assert (sum_stats[1]["rdf"] == r["mtcars"]).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() + 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(conv + pandas2ri.converter): + 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 +311,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 +369,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 +399,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 +413,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 +427,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 +459,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 +469,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 +496,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 +504,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 +528,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 +577,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/base/test_sumstat.py b/test/base/test_sumstat.py index a871f0f5..9acd6f9a 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) @@ -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)), @@ -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 788fc307..caf31fa9 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 03b0d088..e58fe7cb 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 145ba18d..073cc35a 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 88785b1d..b866ead4 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_pyjulia.py b/test/external/test_pyjulia.py index 6fdebcc1..dd699b76 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 diff --git a/test/external/test_rpy2.py b/test/external/test_rpy2.py index 9a08becf..00262b0a 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 14cd8b1e..b16fe249 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 b0d94053..752a405c 100644 --- a/test/migrate/test_migrate.py +++ b/test/migrate/test_migrate.py @@ -10,19 +10,21 @@ 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) + 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 6ead7afa..3e6a050e 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 498441db..d0f1bfa4 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 @@ -13,17 +12,27 @@ 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__) -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 +45,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 +58,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 +73,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 +102,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 +148,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/run_notebooks.sh b/test/run_notebooks.sh index 748d5c38..fb600b88 100755 --- a/test/run_notebooks.sh +++ b/test/run_notebooks.sh @@ -15,15 +15,24 @@ nbs_1=( "noise" "parameter_inference" "resuming" + "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 + "using_copasi" + "using_julia" ) # All notebooks @@ -59,10 +68,12 @@ 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 + python -m jupyter nbconvert \ + --ExecutePreprocessor.timeout=-1 \ + --ExecutePreprocessor.kernel_name=python3 \ + --debug --stdout --execute --to markdown "$@" &> "$tempfile" ret=$? if [[ $ret != 0 ]]; then cat $tempfile diff --git a/test/visserver/test_dash_server.py b/test/visserver/test_dash_server.py index 8ba21ac2..6b9870a9 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 86b61ff4..94c38d73 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 @@ -237,7 +237,7 @@ def test_contours(): history, size=(7, 5), refval=p_true, **kwargs ) - plt.close() + plt.close() def test_credible_intervals(): @@ -353,27 +353,28 @@ def test_distance_weights(): pyabc.visualization.plot_distance_weights( log_files, keys_as_labels=keys_as_labels ) + plt.close() 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 +382,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 +398,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( @@ -406,13 +407,15 @@ def model(p): h=h, predictor=pyabc.predictor.LinearPredictor(), ) + plt.close() 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) + plt.close() diff --git a/test/visualization/test_lookahead_viz.py b/test/visualization/test_lookahead_viz.py index ef807482..963e8951 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 a6a3669c..33af56d7 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 19c20312..92e159b3 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_nondeterministic/test_abc_smc_algorithm.py b/test_nondeterministic/test_abc_smc_algorithm.py index fdf381a2..2d096e79 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_parameter.py b/test_performance/test_parameter.py index 47a871ca..9d923f99 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 98d537ae..becae889 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 diff --git a/test_performance/test_samplerperf.py b/test_performance/test_samplerperf.py index 72dbb798..2c66a0e0 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) diff --git a/tox.ini b/tox.ini index dce09133..916404fa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,59 +1,57 @@ -# 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 + lint 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 +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 -# 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 + autograd + ot + pyarrow commands = - # needed by pot - pip install cython - pip install pot - pytest --cov=pyabc --cov-report=xml --cov-append \ + python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/base test_performance -s description = Test basic functionality @@ -61,9 +59,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 +73,37 @@ 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 commands = - # Petab python -m pytest --cov=pyabc --cov-report=xml --cov-append \ test/petab -s description = @@ -107,86 +112,103 @@ 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 + setuptools>=68.0.0 + wheel>=0.36.2 + pytest-console-scripts>=1.4.1 commands = - pip install setuptools>=65.5.0 wheel # to ensure distutils is there in python 3.12 + python -m pip install --upgrade pip # 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 -[testenv:notebooks1] +[testenv:base-notebooks] allowlist_externals = bash -extras = examples +extras = + examples + ot commands = - # needed by pot - pip install cython - pip install pot bash test/run_notebooks.sh 1 description = - Run notebooks + 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 -extras = examples,R,petab,yaml2sbml,amici,autograd +allowlist_externals = + bash + julia +extras = + examples + r + petab + yaml2sbml + amici + autograd + julia + copasi commands = + python -c "import julia; julia.install()" bash test/run_notebooks.sh 2 description = - Run notebooks - -# Style, management, docs + Run notebooks (set 2) [testenv:project] skip_install = true deps = - pyroma - restructuredtext-lint + pyroma>=4.0 + build>=1.0.0 + twine>=4.0.0 commands = pyroma --min=10 . - rst-lint README.rst + python -m build + twine check dist/* description = - Check the package friendliness + Check the package quality and metadata -[testenv:flake8] +[testenv:quality] 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 + ruff>=0.8.0 commands = - flake8 pyabc test test_performance setup.py + ruff check pyabc test test_performance + ruff format --check pyabc test test_performance description = - Run flake8 with various plugins + Run linting with ruff [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 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