From 157fda1578c74111c2ba5aecfef1a68ec899edee Mon Sep 17 00:00:00 2001 From: Simon O'kane Date: Wed, 13 Aug 2025 11:27:45 +0100 Subject: [PATCH 01/35] Deleted all workflows except for test on push --- .github/workflows/benchmark_on_push.yml | 85 -------- .../workflows/discussion_autoresponder.yml | 24 --- .github/workflows/docker.yml | 51 ----- .github/workflows/lychee_url_checker.yml | 70 ------ .github/workflows/need_reply_remove.yml | 34 --- .github/workflows/needs_reply.yml | 23 -- .github/workflows/periodic_benchmarks.yml | 108 ---------- .github/workflows/publish_pypi.yml | 50 ----- .../workflows/run_benchmarks_over_history.yml | 169 --------------- .github/workflows/run_periodic_tests.yml | 200 ------------------ .github/workflows/scorecard.yml | 70 ------ .github/workflows/validation_benchmarks.yml | 30 --- 12 files changed, 914 deletions(-) delete mode 100644 .github/workflows/benchmark_on_push.yml delete mode 100644 .github/workflows/discussion_autoresponder.yml delete mode 100644 .github/workflows/docker.yml delete mode 100644 .github/workflows/lychee_url_checker.yml delete mode 100644 .github/workflows/need_reply_remove.yml delete mode 100644 .github/workflows/needs_reply.yml delete mode 100644 .github/workflows/periodic_benchmarks.yml delete mode 100644 .github/workflows/publish_pypi.yml delete mode 100644 .github/workflows/run_benchmarks_over_history.yml delete mode 100644 .github/workflows/run_periodic_tests.yml delete mode 100644 .github/workflows/scorecard.yml delete mode 100644 .github/workflows/validation_benchmarks.yml diff --git a/.github/workflows/benchmark_on_push.yml b/.github/workflows/benchmark_on_push.yml deleted file mode 100644 index f63648479b..0000000000 --- a/.github/workflows/benchmark_on_push.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Run benchmarks on push -on: - push: - branches: [main, develop] - - -permissions: {} - -concurrency: - # Cancel intermediate builds always - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - PYBAMM_DISABLE_TELEMETRY: "true" - -jobs: - benchmarks: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt install gfortran gcc libopenblas-dev - - - name: Set up uv - run: python -m pip install uv - - - name: Install python dependencies - run: | - python -m uv pip install asv[virtualenv] - - - name: Fetch base branch - run: | - # This workflow also runs for merge commits - # on develop. In this case, we don't want to be - # fetching the develop branch. - current_branch=$(git rev-parse --abbrev-ref HEAD) - # This workflow should also run on forks; hence, - # we should fetch the upstream develop branch. - git remote add upstream https://github.com/pybamm-team/PyBaMM/ - if [ $current_branch != "develop" ]; then - git fetch upstream develop:develop - fi - - - name: Run benchmarks - run: | - asv machine --machine "GitHubRunner" - # Get IDs of branch and PR commits - BASE_COMMIT=$(git rev-parse develop) - HEAD_COMMIT=$(git rev-parse HEAD) - echo $BASE_COMMIT | tee commits_to_compare.txt - echo $HEAD_COMMIT | tee -a commits_to_compare.txt - asv run HASHFILE:commits_to_compare.txt --m "GitHubRunner" --show-stderr -v - - - name: Compare commits' benchmark results - run: | - BASE_COMMIT=$(head -1 commits_to_compare.txt) - HEAD_COMMIT=$(tail -1 commits_to_compare.txt) - echo "SUMMARY OF CHANGES" - echo "==================" - asv compare $BASE_COMMIT $HEAD_COMMIT | tee compare_result.txt - # Make sure grep returns error code 0 even if code 1 is - # returned because no match is found - REGRESSIONS=$({ grep "+" compare_result.txt || test $? = 1; }) - if [ ! -z "$REGRESSIONS" ]; \ - then \ - echo "REGRESSIONS FOUND"; \ - echo "================="; \ - echo "$REGRESSIONS"; \ - echo "================="; \ - printf "Found %d regression(s)\n" $(echo "$REGRESSIONS" | wc -l); \ - exit 1; \ - fi diff --git a/.github/workflows/discussion_autoresponder.yml b/.github/workflows/discussion_autoresponder.yml deleted file mode 100644 index 936493074f..0000000000 --- a/.github/workflows/discussion_autoresponder.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Discussion Autoresponder - -on: - discussion: - types: [created] - - -permissions: {} - -jobs: - autorespond: - name: Autorespond to New Discussions - runs-on: ubuntu-latest - permissions: - discussions: write - contents: read - - steps: - - name: Run Discussion Autoresponder - uses: wesleyscholl/discussion-auto-responder@b1a3c1b9a1e3d1b1a3c1b9a1e3d1b1a3c1b9a1e3 # v1.0.8 - with: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - comment_body: "Hi! We have now moved our discussions to [Discourse](https://pybamm.discourse.group/). Please post your question there." - delay_milliseconds: 0 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 6cbbf401eb..0000000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Build and push Docker images to Docker Hub - -on: - workflow_dispatch: - release: - types: [published] - -permissions: {} - -jobs: - build_docker_image: - # This workflow is only of value to PyBaMM and would always be skipped in forks - if: github.repository == 'pybamm-team/PyBaMM' - name: Build image - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Set up QEMU - uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - with: - cache-binary: false - - name: Login to Docker Hub - uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - - name: Build and push Docker image to Docker Hub - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - with: - context: . - file: scripts/Dockerfile - tags: pybamm/pybamm:latest - push: true - platforms: linux/amd64 - no-cache: true - - - name: List built image(s) - run: docker images diff --git a/.github/workflows/lychee_url_checker.yml b/.github/workflows/lychee_url_checker.yml deleted file mode 100644 index 5c92a577e3..0000000000 --- a/.github/workflows/lychee_url_checker.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Check URLs with Lychee - -on: - push: - branches: - - main - - develop - pull_request: - schedule: - # Run everyday at 3 am UTC - - cron: "0 3 * * *" - - -permissions: {} - -jobs: - linkChecker: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - # cache Lychee results to avoid hitting rate limits - - name: Restore lychee cache - uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 - with: - path: .lycheecache - key: cache-lychee-${{ github.sha }} - restore-keys: cache-lychee- - - # check URLs with Lychee - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - # use stable version for now to avoid breaking changes - - name: Lychee URL checker - uses: lycheeverse/lychee-action@5c4ee84814c983aa7164eaee476f014e53ff3963 # v2.5.0 - with: - # arguments with file types to check - args: >- - --cache - --no-progress - --max-cache-age 2d - --timeout 10 - --max-retries 5 - --skip-missing - --exclude-loopback - --exclude https://twitter.com/pybamm_ - --exclude "https://doi\.org|www.sciencedirect\.com/*" - --exclude https://www.rse.ox.ac.uk - --accept 200,429 - --exclude-path ./CHANGELOG.md - --exclude-path ./scripts/update_version.py - --exclude-path asv.conf.json - --exclude-path docs/conf.py - './**/*.rst' - './**/*.md' - './**/*.py' - './**/*.ipynb' - './**/*.json' - './**/*.toml' - # fail the action on broken links - fail: true - jobSummary: true - format: markdown - env: - # to be used in case rate limits are surpassed - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/need_reply_remove.yml b/.github/workflows/need_reply_remove.yml deleted file mode 100644 index 2023dc1b3c..0000000000 --- a/.github/workflows/need_reply_remove.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Remove needs-reply label - -on: - schedule: - - cron: '0 3 * * 1' - issue_comment: - types: - - created - -permissions: {} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - contents: read - if: | - github.event.comment.author_association != 'OWNER' && - github.event.comment.author_association != 'COLLABORATOR' && - github.repository == 'pybamm-team/PyBaMM' && - github.event_name != 'pull_request' - steps: - - name: Remove needs-reply label - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 - continue-on-error: true - with: - route: DELETE /repos/:repository/issues/:issue/labels/:label - repository: ${{ github.repository }} - issue: ${{ github.event.issue.number }} - label: needs-reply - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/needs_reply.yml b/.github/workflows/needs_reply.yml deleted file mode 100644 index 9ee2369112..0000000000 --- a/.github/workflows/needs_reply.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Close old issues that need reply - -on: - schedule: - - cron: "0 0 * * *" - - -permissions: {} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - issues: read - pull-requests: read - contents: read - if: github.repository == 'pybamm-team/PyBaMM' - steps: - - name: Close old issues that need reply - uses: dwieeb/needs-reply@71e8d5144caa0d4a1e292348bfafa3866d08c855 # v2.0.0 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-label: needs-reply diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml deleted file mode 100644 index e3107f110a..0000000000 --- a/.github/workflows/periodic_benchmarks.yml +++ /dev/null @@ -1,108 +0,0 @@ -# This workflow periodically runs the benchmarks suite in benchmarks/ -# using asv and publish the results, effectively updating -# the display website hosted in the pybamm-bench repo - -# Steps: -# - Benchmark all commits since the last one that was benchmarked -# - Push results to pybamm-bench repo -# - Publish website -name: Benchmarks -on: - # Every day at 3 am UTC - schedule: - - cron: "0 3 * * *" - # Make it possible to trigger the - # workflow manually - workflow_dispatch: - - -permissions: {} - -env: - PYBAMM_DISABLE_TELEMETRY: "true" - -jobs: - benchmarks: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install gfortran gcc libopenblas-dev - - - name: Set up uv - run: python -m pip install uv - - - name: Install python dependencies - run: | - python -m uv pip install asv[virtualenv] - - - name: Run benchmarks - run: | - asv machine --machine "GitHubRunner" - asv run --machine "GitHubRunner" NEW --show-stderr -v - - - name: Upload results as artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: asv_periodic_results - path: results - if-no-files-found: error - - publish-results: - if: github.repository == 'pybamm-team/PyBaMM' - name: Push and publish results - needs: benchmarks - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Install asv - run: pip install asv - - - name: Checkout pybamm-bench repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - repository: pybamm-team/pybamm-bench - token: ${{ secrets.BENCH_PAT }} - persist-credentials: false - - - name: Download results artifact(s) - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - path: results - merge-multiple: true - - - name: Copy new results and push to pybamm-bench repo - env: - PUSH_BENCH_EMAIL: ${{ secrets.PUSH_BENCH_EMAIL }} - PUSH_BENCH_NAME: ${{ secrets.PUSH_BENCH_NAME }} - run: | - git config --global user.email "$PUSH_BENCH_EMAIL" - git config --global user.name "$PUSH_BENCH_NAME" - git add results - git commit -am "Add new results" - git push - - - name: Publish results - run: | - asv publish - git fetch origin gh-pages:gh-pages - asv gh-pages diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml deleted file mode 100644 index 5fa4332623..0000000000 --- a/.github/workflows/publish_pypi.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Build and publish package to PyPI -on: - release: - types: [published] - - -permissions: {} - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: true - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Build wheel - run: pipx run build --outdir deploy - - - name: Upload package - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: distributions - path: deploy/ - if-no-files-found: error - - publish: - needs: build - runs-on: ubuntu-latest - environment: pypi - permissions: - id-token: write - - steps: - - name: Download artifacts - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 - with: - path: deploy - merge-multiple: true - - - name: Publish on PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 - with: - packages-dir: deploy/ diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml deleted file mode 100644 index 895cdacef4..0000000000 --- a/.github/workflows/run_benchmarks_over_history.yml +++ /dev/null @@ -1,169 +0,0 @@ -# GitHub actions workflow that runs the benchmark suite in benchmarks/ -# from "commit_start" to "commit_end". It pushes the results to the -# pybamm-bench repo and updates the display website. - -# This workflow is meant to be triggered manually, see -# https://docs.github.com/en/enterprise-server@3.0/actions/managing-workflow-runs/manually-running-a-workflow - -name: Manual benchmarks -on: - workflow_dispatch: - inputs: - commit_start: - description: "Identifier of commit from which to start" - default: "v0.1.0" - type: string - pattern: '^[a-zA-Z0-9._-]+$' - commit_end: - description: "Identifier of commit at which to end" - default: "develop" - type: string - pattern: '^[a-zA-Z0-9._-]+$' - ncommits: - description: "Number of commits to benchmark between commit_start and commit_end" - default: "100" - type: string - pattern: '^[0-9]+$' - - -permissions: {} - -env: - PYBAMM_DISABLE_TELEMETRY: "true" - -jobs: - benchmarks: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Install nox and asv - run: pip install -U pip nox asv - - - name: Fetch develop branch - # Not required when worklow trigerred - # on develop, but useful when - # experimenting/developing on another branch. - if: github.ref != 'refs/heads/develop' - run: | - git fetch origin develop:develop - - - name: Validate commit_start - id: validate_start - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const input = context.payload.inputs.commit_start; - if (!input || !/^[a-zA-Z0-9._-]+$/.test(input)) { - core.setFailed('Invalid commit_start format'); - return; - } - core.setOutput('commit_start', input); - - - name: Validate commit_end - id: validate_end - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const input = context.payload.inputs.commit_end; - if (!input || !/^[a-zA-Z0-9._-]+$/.test(input)) { - core.setFailed('Invalid commit_end format'); - return; - } - core.setOutput('commit_end', input); - - - name: Validate ncommits - id: validate_ncommits - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 - with: - script: | - const input = context.payload.inputs.ncommits; - if (!input || !/^[0-9]+$/.test(input)) { - core.setFailed('Invalid ncommits format'); - return; - } - const numValue = parseInt(input, 10); - if (numValue < 1 || numValue > 10000) { - core.setFailed('ncommits must be between 1 and 10000'); - return; - } - if (numValue > 5000) { - core.warning('Processing a large number of commits. This may take a while....'); - } - core.setOutput('ncommits', numValue.toString()); - - - name: Set environment variables - env: - COMMIT_START: ${{ steps.validate_start.outputs.commit_start }} - COMMIT_END: ${{ steps.validate_end.outputs.commit_end }} - NCOMMITS: ${{ steps.validate_ncommits.outputs.ncommits }} - run: | - echo "COMMIT_START=$COMMIT_START" >> $GITHUB_ENV - echo "COMMIT_END=$COMMIT_END" >> $GITHUB_ENV - echo "NCOMMITS=$NCOMMITS" >> $GITHUB_ENV - - - name: Run benchmarks - run: | - asv machine --machine "GitHubRunner" - asv run -m "GitHubRunner" -s $NCOMMITS \ - $COMMIT_START..$COMMIT_END - - - name: Upload results as artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: asv_over_history_results - path: results - if-no-files-found: error - - publish-results: - if: github.repository == 'pybamm-team/PyBaMM' - name: Push and publish results - needs: benchmarks - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Install asv - run: pip install asv - - - name: Checkout pybamm-bench repo - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - repository: pybamm-team/pybamm-bench - token: ${{ secrets.BENCH_PAT }} - persist-credentials: false - - - name: Download results artifact(s) - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v4.6.2 - with: - name: asv_over_history_results - path: results - - - name: Copy new results and push to pybamm-bench repo - env: - PUSH_BENCH_EMAIL: ${{ secrets.PUSH_BENCH_EMAIL }} - PUSH_BENCH_NAME: ${{ secrets.PUSH_BENCH_NAME }} - run: | - git config --global user.email "$PUSH_BENCH_EMAIL" - git config --global user.name "$PUSH_BENCH_NAME" - git add results - git commit -am "Add new results" - git push - - - name: Publish results - run: | - git fetch origin gh-pages:gh-pages - asv gh-pages diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml deleted file mode 100644 index 08fc7dbe5e..0000000000 --- a/.github/workflows/run_periodic_tests.yml +++ /dev/null @@ -1,200 +0,0 @@ -name: Scheduled - -on: - workflow_dispatch: - # Run every day at 3 am UTC - schedule: - - cron: "0 3 * * *" - - -permissions: {} - -env: - PYBAMM_DISABLE_TELEMETRY: "true" - FORCE_COLOR: 3 - -concurrency: - # github.workflow: name of the workflow, so that we don't cancel other workflows - # github.event.pull_request.number || github.ref: pull request number or branch name if not a pull request - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - # Cancel in-progress runs when a new workflow with the same group name is triggered - # This avoids workflow runs on both pushes and PRs - cancel-in-progress: true - -jobs: - run_unit_integration_and_coverage_tests: - runs-on: ${{ matrix.os }} - permissions: - contents: read - - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, macos-13, macos-latest, windows-latest ] - python-version: ["3.10", "3.11", "3.12"] - name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Install Linux system dependencies - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install gfortran gcc graphviz pandoc - sudo apt-get install libopenblas-dev texlive-latex-extra dvipng - - - name: Install macOS system dependencies - if: matrix.os == 'macos-13' || matrix.os == 'macos-latest' - env: - HOMEBREW_NO_INSTALL_CLEANUP: 1 - HOMEBREW_NO_AUTO_UPDATE: 1 - HOMEBREW_NO_COLOR: 1 - # Speed up CI - NONINTERACTIVE: 1 - # sometimes gfortran cannot be found, so reinstall gcc just to be sure - run: | - brew analytics off - brew install graphviz libomp - brew reinstall gcc - - - name: Install Windows system dependencies - if: matrix.os == 'windows-latest' - run: choco install graphviz --version=8.0.5 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: ${{ matrix.python-version }} - - - name: Set up uv - run: python -m pip install uv - - - name: Install nox - run: python -m uv pip install nox[uv] - - - name: Run unit tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} - if: matrix.os != 'ubuntu-latest' || matrix.python-version != '3.12' - run: python -m nox -s unit - - - name: Run coverage tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - run: python -m nox -s coverage - - - name: Upload coverage report - if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - - name: Run integration tests for ${{ matrix.os }} with Python ${{ matrix.python-version }} - run: python -m nox -s integration - - run_doctests: - runs-on: ubuntu-latest - permissions: - contents: read - strategy: - fail-fast: false - name: Doctests (ubuntu-latest / Python 3.11) - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - fetch-depth: 0 - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install graphviz pandoc - sudo apt-get install texlive-latex-extra dvipng - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.11 - - - name: Set up uv - run: python -m pip install uv - - - name: Install nox - run: python -m uv pip install nox[uv] - - - name: Install docs dependencies and run doctests for GNU/Linux - run: python -m nox -s doctests - - - name: Check if the documentation can be built for GNU/Linux - run: python -m nox -s docs - - run_example_tests: - runs-on: ubuntu-latest - permissions: - contents: read - strategy: - fail-fast: false - name: Example notebooks (ubuntu-latest / Python 3.12) - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install gfortran gcc graphviz pandoc - sudo apt-get install libopenblas-dev texlive-latex-extra dvipng - - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Set up uv - run: python -m pip install uv - - - name: Install nox - run: python -m uv pip install nox[uv] - - - name: Run example notebooks tests for GNU/Linux with Python 3.12 - run: python -m nox -s examples - - run_scripts_tests: - runs-on: ubuntu-latest - permissions: - contents: read - strategy: - fail-fast: false - name: Example scripts (ubuntu-latest / Python 3.12) - - steps: - - name: Check out PyBaMM repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: Install Linux system dependencies - run: | - sudo apt-get update - sudo apt-get install gfortran gcc graphviz - sudo apt-get install libopenblas-dev texlive-latex-extra dvipng - - - name: Set up Python 3.12 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: 3.12 - - - name: Set up uv - run: python -m pip install uv - - - name: Install nox - run: python -m uv pip install nox[uv] - - - name: Run example scripts tests for GNU/Linux with Python 3.12 - run: python -m nox -s scripts diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml deleted file mode 100644 index c516817bb2..0000000000 --- a/.github/workflows/scorecard.yml +++ /dev/null @@ -1,70 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. They are provided -# by a third-party and are governed by separate terms of service, privacy -# policy, and support documentation. - -name: Scorecard supply-chain security -on: - # For Branch-Protection check. Only the default branch is supported. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection - branch_protection_rule: - # To guarantee Maintained check is occasionally updated. See - # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained - schedule: - - cron: '25 3 * * 5' - push: - branches: [ "develop", "main" ] - -permissions: - contents: read - -jobs: - analysis: - name: Scorecard analysis - runs-on: ubuntu-latest - permissions: - # Needed to upload the results to code-scanning dashboard. - security-events: write - # Needed to publish results and get a badge (see publish_results below). - id-token: write - - steps: - - name: "Checkout code" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - with: - persist-credentials: false - - - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 - with: - results_file: results.sarif - results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecard on a *private* repository - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. - # repo_token: ${{ secrets.SCORECARD_TOKEN }} - - # Public repositories: - # - Publish results to OpenSSF REST API for easy access by consumers - # - Allows the repository to include the Scorecard badge. - # - See https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories: - # - `publish_results` will always be set to `false`, regardless - # of the value entered here. - publish_results: true - - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - - # Upload the results to GitHub's code scanning dashboard (optional). - # Commenting out will disable upload of results to your repo's Code Scanning dashboard - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5 - with: - sarif_file: results.sarif diff --git a/.github/workflows/validation_benchmarks.yml b/.github/workflows/validation_benchmarks.yml deleted file mode 100644 index 986be5be8a..0000000000 --- a/.github/workflows/validation_benchmarks.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Alert validation repository -on: - workflow_dispatch: - release: - types: - - published - - -permissions: {} - - -jobs: - build: - if: github.repository == 'pybamm-team/PyBaMM' - name: Dispatch to `pybamm-validation` - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 - with: - token: ${{ secrets.BENCHMARKS_ACCESS_TOKEN }} - repository: pybamm-team/pybamm-validation - event-type: ${{ github.event_name }} - client-payload: |- - { - "message": { - "commit_hash": "$GITHUB_SHA" - } - } From e920c64ea4720ba8d561925fa5f7ba227751d8f2 Mon Sep 17 00:00:00 2001 From: Simon O'kane Date: Wed, 13 Aug 2025 11:31:01 +0100 Subject: [PATCH 02/35] Deleted more GitHub files. Down with periodic actions! --- .github/CODEOWNERS | 2 -- .github/dependabot.yml | 12 ------------ 2 files changed, 14 deletions(-) delete mode 100644 .github/CODEOWNERS delete mode 100644 .github/dependabot.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index 163b26428a..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,2 +0,0 @@ -# Automatically request reviews from maintainers -* @pybamm-team/maintainers diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index c4fbd385c1..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 -updates: - # Maintain dependencies for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - # group updates in a single PR - groups: - actions: - patterns: - - "*" From 0b24a54dcbe4ea4e65727a3a018abbf5899ec869 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Thu, 18 Sep 2025 16:50:49 +0100 Subject: [PATCH 03/35] First attempt at making all epsilons functions of x,y,z --- .../parameters/lithium_ion_parameters.py | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py index da81d6e2a7..2475794257 100644 --- a/src/pybamm/parameters/lithium_ion_parameters.py +++ b/src/pybamm/parameters/lithium_ion_parameters.py @@ -231,10 +231,19 @@ def _set_parameters(self): self.h_cc = self.therm.h_cc self.h_tab = self.therm.h_tab + y = pybamm.standard_spatial_vars.y + z = pybamm.standard_spatial_vars.z + if domain == "separator": x = pybamm.standard_spatial_vars.x_s self.epsilon_init = pybamm.FunctionParameter( - "Separator porosity", {"Through-cell distance (x) [m]": x} + "Separator porosity", + { + "Through-cell distance (x) [m]": x, + "In-plane coordinate (y) [m]": y, + "In-plane coordinate (z) [m]": z + + } ) self.epsilon_inactive = 1 - self.epsilon_init return @@ -267,7 +276,12 @@ def _set_parameters(self): ) if main.options.electrode_types[domain] == "porous": self.epsilon_init = pybamm.FunctionParameter( - f"{Domain} electrode porosity", {"Through-cell distance (x) [m]": x} + f"{Domain} electrode porosity", + { + "Through-cell distance (x) [m]": x, + "In-plane coordinate (y) [m]": y, + "In-plane coordinate (z) [m]": z + } ) epsilon_s_tot = sum(phase.epsilon_s for phase in self.phase_params.values()) self.epsilon_inactive = 1 - self.epsilon_init - epsilon_s_tot @@ -416,6 +430,8 @@ def _set_parameters(self): auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) + y = pybamm.standard_spatial_vars.y + z = pybamm.standard_spatial_vars.z r = pybamm.SpatialVariable( f"r_{domain[0]}", domain=[f"{domain} {self.phase_name}particle"], @@ -440,7 +456,11 @@ def _set_parameters(self): # Particle properties self.epsilon_s = pybamm.FunctionParameter( f"{pref}{Domain} electrode active material volume fraction", - {"Through-cell distance (x) [m]": x}, + { + "Through-cell distance (x) [m]": x, + "In-plane coordinate (y) [m]": y, + "In-plane coordinate (z) [m]": z + }, ) self.epsilon_s_av = pybamm.xyz_average(self.epsilon_s) self.c_max = pybamm.Parameter( From 02d9777a192adb152c649defde84673ac49582bd Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Fri, 19 Sep 2025 16:28:57 +0100 Subject: [PATCH 04/35] Model now works with events, but not with experiments or initial_soc --- .../parameters/lithium_ion_parameters.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py index 2475794257..2cba3bae01 100644 --- a/src/pybamm/parameters/lithium_ion_parameters.py +++ b/src/pybamm/parameters/lithium_ion_parameters.py @@ -231,11 +231,10 @@ def _set_parameters(self): self.h_cc = self.therm.h_cc self.h_tab = self.therm.h_tab - y = pybamm.standard_spatial_vars.y - z = pybamm.standard_spatial_vars.z - if domain == "separator": x = pybamm.standard_spatial_vars.x_s + y = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.y, "separator") + z = pybamm.PrimaryBroadcast(pybamm.standard_spatial_vars.z, "separator") self.epsilon_init = pybamm.FunctionParameter( "Separator porosity", { @@ -257,6 +256,14 @@ def _set_parameters(self): auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) + y = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.y, + f"{domain} electrode" + ) + z = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.z, + f"{domain} electrode" + ) # Macroscale geometry self.L_cc = self.geo.L_cc @@ -430,8 +437,14 @@ def _set_parameters(self): auxiliary_domains={"secondary": "current collector"}, coord_sys="cartesian", ) - y = pybamm.standard_spatial_vars.y - z = pybamm.standard_spatial_vars.z + y = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.y, + f"{domain} electrode" + ) + z = pybamm.PrimaryBroadcast( + pybamm.standard_spatial_vars.z, + f"{domain} electrode" + ) r = pybamm.SpatialVariable( f"r_{domain[0]}", domain=[f"{domain} {self.phase_name}particle"], From 7afb3b4a2a95bd1abe8f4a47a5c289449eac6f35 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Wed, 24 Sep 2025 16:22:40 +0100 Subject: [PATCH 05/35] Works for z by not y --- examples/scripts/graded_pouch.py | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 examples/scripts/graded_pouch.py diff --git a/examples/scripts/graded_pouch.py b/examples/scripts/graded_pouch.py new file mode 100644 index 0000000000..a841806c0d --- /dev/null +++ b/examples/scripts/graded_pouch.py @@ -0,0 +1,50 @@ +import pybamm +import numpy as np + +model = pybamm.lithium_ion.DFN( + {"current collector": "potential pair", "dimensionality": 1} +) +param = pybamm.ParameterValues("Ecker2015") +Ly = param["Electrode width [m]"] +Lz = param["Electrode height [m]"] + +def sigmoid(arg): + return (1 + np.tanh(arg)) / 2 + + +def top_hat(arg, a, b, k=500): + return sigmoid(k * (arg - a)) * sigmoid(k * (b - arg)) + + +def eps_s_n(x, y_cc, z_cc): + return 0.372403 * ( + top_hat(arg=z_cc, a=Lz*0.05, b=Lz*0.95) + ) + + +def eps_s_p(x, y_cc, z_cc): + return 0.40832 * ( + top_hat(arg=z_cc, a=Lz*0.05, b=Lz*0.95) + ) + +param_dryout = param.copy() +param_dryout.update( + { + "Negative electrode active material volume fraction": eps_s_n, + "Positive electrode active material volume fraction": eps_s_p, + } +) + +var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "z": 32} +exp = pybamm.Experiment(["Discharge at 1C until 2.7 V", "Charge at 1C until 4.2 V", "Hold at 4.2 V until C/20"]) +sim = pybamm.Simulation(model, var_pts=var_pts, parameter_values=param_dryout, experiment=exp) +sol = sim.solve() +output_variables = [ + "X-averaged negative electrode active material volume fraction", + "X-averaged positive electrode active material volume fraction", + "Current collector current density [A.m-2]", + "X-averaged negative particle surface stoichiometry", + "X-averaged negative electrode surface potential difference [V]", + "Voltage [V]", +] +plot = sol.plot(output_variables, variable_limits="tight", shading="auto") \ No newline at end of file From 662179f06a62aca82f927f2c682e9ea18f7b88a7 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Wed, 24 Sep 2025 16:33:42 +0100 Subject: [PATCH 06/35] Fixed style issues in graded_pouch example --- examples/scripts/graded_pouch.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/scripts/graded_pouch.py b/examples/scripts/graded_pouch.py index a841806c0d..ee44027025 100644 --- a/examples/scripts/graded_pouch.py +++ b/examples/scripts/graded_pouch.py @@ -36,8 +36,16 @@ def eps_s_p(x, y_cc, z_cc): ) var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "z": 32} -exp = pybamm.Experiment(["Discharge at 1C until 2.7 V", "Charge at 1C until 4.2 V", "Hold at 4.2 V until C/20"]) -sim = pybamm.Simulation(model, var_pts=var_pts, parameter_values=param_dryout, experiment=exp) +exp = pybamm.Experiment( + [ + "Discharge at 1C until 2.7 V", + "Charge at 1C until 4.2 V", + "Hold at 4.2 V until C/20" + ] +) +sim = pybamm.Simulation( + model, var_pts=var_pts, parameter_values=param_dryout, experiment=exp +) sol = sim.solve() output_variables = [ "X-averaged negative electrode active material volume fraction", @@ -47,4 +55,4 @@ def eps_s_p(x, y_cc, z_cc): "X-averaged negative electrode surface potential difference [V]", "Voltage [V]", ] -plot = sol.plot(output_variables, variable_limits="tight", shading="auto") \ No newline at end of file +plot = sol.plot(output_variables, variable_limits="tight", shading="auto") From cfb2c209d6dc0d7fff7a215a05f9441f21f860ea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 15:40:55 +0000 Subject: [PATCH 07/35] style: pre-commit fixes --- examples/scripts/graded_pouch.py | 15 ++++++------ .../parameters/lithium_ion_parameters.py | 23 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/examples/scripts/graded_pouch.py b/examples/scripts/graded_pouch.py index ee44027025..37432d48c9 100644 --- a/examples/scripts/graded_pouch.py +++ b/examples/scripts/graded_pouch.py @@ -1,6 +1,7 @@ -import pybamm import numpy as np +import pybamm + model = pybamm.lithium_ion.DFN( {"current collector": "potential pair", "dimensionality": 1} ) @@ -8,6 +9,7 @@ Ly = param["Electrode width [m]"] Lz = param["Electrode height [m]"] + def sigmoid(arg): return (1 + np.tanh(arg)) / 2 @@ -17,15 +19,12 @@ def top_hat(arg, a, b, k=500): def eps_s_n(x, y_cc, z_cc): - return 0.372403 * ( - top_hat(arg=z_cc, a=Lz*0.05, b=Lz*0.95) - ) + return 0.372403 * (top_hat(arg=z_cc, a=Lz * 0.05, b=Lz * 0.95)) def eps_s_p(x, y_cc, z_cc): - return 0.40832 * ( - top_hat(arg=z_cc, a=Lz*0.05, b=Lz*0.95) - ) + return 0.40832 * (top_hat(arg=z_cc, a=Lz * 0.05, b=Lz * 0.95)) + param_dryout = param.copy() param_dryout.update( @@ -40,7 +39,7 @@ def eps_s_p(x, y_cc, z_cc): [ "Discharge at 1C until 2.7 V", "Charge at 1C until 4.2 V", - "Hold at 4.2 V until C/20" + "Hold at 4.2 V until C/20", ] ) sim = pybamm.Simulation( diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py index 2cba3bae01..33d09c0ad1 100644 --- a/src/pybamm/parameters/lithium_ion_parameters.py +++ b/src/pybamm/parameters/lithium_ion_parameters.py @@ -240,9 +240,8 @@ def _set_parameters(self): { "Through-cell distance (x) [m]": x, "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z - - } + "In-plane coordinate (z) [m]": z, + }, ) self.epsilon_inactive = 1 - self.epsilon_init return @@ -257,12 +256,10 @@ def _set_parameters(self): coord_sys="cartesian", ) y = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.y, - f"{domain} electrode" + pybamm.standard_spatial_vars.y, f"{domain} electrode" ) z = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.z, - f"{domain} electrode" + pybamm.standard_spatial_vars.z, f"{domain} electrode" ) # Macroscale geometry @@ -287,8 +284,8 @@ def _set_parameters(self): { "Through-cell distance (x) [m]": x, "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z - } + "In-plane coordinate (z) [m]": z, + }, ) epsilon_s_tot = sum(phase.epsilon_s for phase in self.phase_params.values()) self.epsilon_inactive = 1 - self.epsilon_init - epsilon_s_tot @@ -438,12 +435,10 @@ def _set_parameters(self): coord_sys="cartesian", ) y = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.y, - f"{domain} electrode" + pybamm.standard_spatial_vars.y, f"{domain} electrode" ) z = pybamm.PrimaryBroadcast( - pybamm.standard_spatial_vars.z, - f"{domain} electrode" + pybamm.standard_spatial_vars.z, f"{domain} electrode" ) r = pybamm.SpatialVariable( f"r_{domain[0]}", @@ -472,7 +467,7 @@ def _set_parameters(self): { "Through-cell distance (x) [m]": x, "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z + "In-plane coordinate (z) [m]": z, }, ) self.epsilon_s_av = pybamm.xyz_average(self.epsilon_s) From 54b791d054ee8e1241cd5223d28455ee966a83d9 Mon Sep 17 00:00:00 2001 From: rtimms Date: Sat, 1 Nov 2025 23:20:12 +0000 Subject: [PATCH 08/35] allow get state from y_slice --- src/pybamm/models/base_model.py | 43 ++++++++++++++++++++++++++++----- test.py | 30 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 test.py diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 6cf682b2a2..0d80fd988d 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -931,15 +931,24 @@ def set_initial_conditions_from( solution = solution.last_state def get_final_state_eval(final_state): + # If already a numpy array (from y_slices), it's already the final state + if isinstance(final_state, np.ndarray): + return np.array(final_state) + + # Otherwise, it's a ProcessedVariable - extract .data if available if isinstance(solution, pybamm.Solution): - final_state = final_state.data + if hasattr(final_state, "data"): + final_state = final_state.data + + final_state = np.array(final_state) + # Extract final state from time series if final_state.ndim == 0: return np.array([final_state]) elif final_state.ndim == 1: - return final_state[-1:] + return np.array(final_state[-1:]) elif final_state.ndim == 2: - return final_state[:, -1] + return np.array(final_state[:, -1]) elif final_state.ndim == 3: return final_state[:, :, -1].flatten(order="F") elif final_state.ndim == 4: @@ -947,7 +956,29 @@ def get_final_state_eval(final_state): else: raise NotImplementedError("Variable must be 0D, 1D, 2D, or 3D") - def get_variable_state(var_name): + def get_variable_state(var): + var_name = var.name + if self.is_discretised: + try: + # Try to get slice directly from y_slices using variable object + if var in self.y_slices: + # Get slice and extract from solution.y + if isinstance(solution, pybamm.Solution): + y_slice = self.y_slices[var][0] + # Get last state from solution.y + y_last = ( + solution.y[:, -1] if solution.y.ndim > 1 else solution.y + ) + # Already have the final state from y_slices, return directly + return np.array(y_last[y_slice]) + else: + # If solution is a dict, can't use y_slices approach + raise KeyError(var_name) + except (KeyError, AttributeError, IndexError): + # Fall through to try solution[var_name] + pass + + # Try solution[var_name] (either not discretised or y_slices failed) try: return solution[var_name] except KeyError as e: @@ -963,13 +994,13 @@ def get_variable_state(var_name): var, pybamm.Concatenation ): try: - final_state = get_variable_state(var.name) + final_state = get_variable_state(var) final_state_eval = get_final_state_eval(final_state) except pybamm.ModelError as e: if isinstance(var, pybamm.Concatenation): children = [] for child in var.orphans: - final_state = get_variable_state(child.name) + final_state = get_variable_state(child) final_state_eval = get_final_state_eval(final_state) children.append(final_state_eval) final_state_eval = np.concatenate(children) diff --git a/test.py b/test.py new file mode 100644 index 0000000000..baae4b6987 --- /dev/null +++ b/test.py @@ -0,0 +1,30 @@ +import pybamm + +options = { + "cell geometry": "pouch", + "current collector": "potential pair", + "dimensionality": 2, +} +model = pybamm.lithium_ion.DFN(options=options) +parameter_values = pybamm.ParameterValues("Ecker2015") +experiment = pybamm.Experiment( + [ + "Discharge at 1C until 2.8V", + "Charge at C/2 until 4.2V", + ] +) +var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "y": 8, "z": 8} +sim = pybamm.Simulation( + model, + parameter_values=parameter_values, + experiment=experiment, + var_pts=var_pts, +) +sol = sim.solve() +output_variables = [ + "Current collector current density [A.m-2]", + "X-averaged negative particle surface stoichiometry", + "X-averaged negative electrode surface potential difference [V]", + "Voltage [V]", +] +plot = sol.plot(output_variables, variable_limits="tight", shading="auto") From 0b3f7da983aa6697e99a5990b3f6ee74c29424ea Mon Sep 17 00:00:00 2001 From: rtimms Date: Sat, 1 Nov 2025 23:48:10 +0000 Subject: [PATCH 09/35] add tests --- src/pybamm/models/base_model.py | 84 +++++++++++++++++------ tests/unit/test_models/test_base_model.py | 40 +++++++++++ 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 0d80fd988d..3f7d30526e 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -930,15 +930,55 @@ def set_initial_conditions_from( if isinstance(solution, pybamm.Solution): solution = solution.last_state + def _evaluate_symbol(symbol): + if hasattr(symbol, "evaluate"): + return symbol.evaluate() + return symbol + + def _find_matching_variable(var, solution_model): + if not ( + solution_model.is_discretised and solution_model.y_slices is not None + ): + return None + var_id = var.id + for sol_var in solution_model.y_slices.keys(): + if sol_var.id == var_id: + return sol_var + return None + + def _extract_from_y_slices(var, solution_var, solution_model, solution): + solution_y_slice = solution_model.y_slices[solution_var][0] + y_last = solution.y[:, -1] if solution.y.ndim > 1 else solution.y + + # Validate slice bounds + slice_stop = ( + solution_y_slice.stop + if solution_y_slice.stop is not None + else len(y_last) + ) + slice_start = ( + solution_y_slice.start if solution_y_slice.start is not None else 0 + ) + if slice_start < 0 or slice_stop > len(y_last): + return None + + # Extract scaled state vector values + y_scaled = np.array(y_last[solution_y_slice]) + + # Convert from scaled state vector to physical values + # physical = reference + scale * y_scaled + solution_scale = _evaluate_symbol(solution_var.scale) + solution_reference = _evaluate_symbol(solution_var.reference) + return solution_reference + solution_scale * y_scaled + def get_final_state_eval(final_state): - # If already a numpy array (from y_slices), it's already the final state + # If already a numpy array (e.g. from y_slices), it's already the final state if isinstance(final_state, np.ndarray): return np.array(final_state) # Otherwise, it's a ProcessedVariable - extract .data if available - if isinstance(solution, pybamm.Solution): - if hasattr(final_state, "data"): - final_state = final_state.data + if isinstance(solution, pybamm.Solution) and hasattr(final_state, "data"): + final_state = final_state.data final_state = np.array(final_state) @@ -958,27 +998,27 @@ def get_final_state_eval(final_state): def get_variable_state(var): var_name = var.name - if self.is_discretised: + + # Try y_slices for discretised models + if ( + self.is_discretised + and var in self.y_slices + and isinstance(solution, pybamm.Solution) + and len(solution.all_models) > 0 + ): try: - # Try to get slice directly from y_slices using variable object - if var in self.y_slices: - # Get slice and extract from solution.y - if isinstance(solution, pybamm.Solution): - y_slice = self.y_slices[var][0] - # Get last state from solution.y - y_last = ( - solution.y[:, -1] if solution.y.ndim > 1 else solution.y - ) - # Already have the final state from y_slices, return directly - return np.array(y_last[y_slice]) - else: - # If solution is a dict, can't use y_slices approach - raise KeyError(var_name) - except (KeyError, AttributeError, IndexError): - # Fall through to try solution[var_name] + solution_model = solution.all_models[-1] + solution_var = _find_matching_variable(var, solution_model) + if solution_var is not None: + final_state = _extract_from_y_slices( + var, solution_var, solution_model, solution + ) + if final_state is not None: + return final_state + except (KeyError, AttributeError, IndexError, TypeError): pass - # Try solution[var_name] (either not discretised or y_slices failed) + # Fall back to solution[var_name] lookup try: return solution[var_name] except KeyError as e: diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 422a141fc7..b08c9c70bb 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -1145,6 +1145,46 @@ def test_set_initial_conditions(self): new_model_disc.concatenated_initial_conditions.evaluate(), 5 ) + def test_set_initial_conditions_from_y_slices(self): + """Test that set_initial_conditions_from uses y_slices for discretised models.""" + # Set up a simple discretised model + model = pybamm.BaseModel() + var = pybamm.Variable("test_var", domain="negative electrode") + model.rhs = {var: -var} + model.initial_conditions = {var: 1} + + # Discretise + geometry = {"negative electrode": {"x_n": {"min": 0, "max": 1}}} + mesh = pybamm.Mesh( + geometry, {"negative electrode": pybamm.Uniform1DSubMesh}, {"x_n": 5} + ) + disc = pybamm.Discretisation( + mesh, {"negative electrode": pybamm.FiniteVolume()} + ) + model_disc = disc.process_model(model, inplace=False) + + # Get the discretised variable (key from initial_conditions) + disc_var = next(iter(model_disc.initial_conditions.keys())) + + # Create solution with known values + t = np.array([0, 1]) + # y has shape (n_vars, n_time), with value 10 for test_var's slice + y = np.zeros((model_disc.len_rhs_and_alg, 2)) + # Set last time step to 10 for the variable's slice using y_slices + if disc_var in model_disc.y_slices: + y_slice = model_disc.y_slices[disc_var][0] + y[y_slice, -1] = 10 + sol = pybamm.Solution(t, y, model_disc, {}) + + # Update initial conditions - should use y_slices path + model_disc.set_initial_conditions_from(sol) + + # Verify initial conditions were updated correctly + assert isinstance(model_disc.initial_conditions[disc_var], pybamm.Vector) + np.testing.assert_array_equal( + model_disc.initial_conditions[disc_var].entries, 10 + ) + def test_set_initial_condition_errors(self): model = pybamm.BaseModel() var = pybamm.Scalar(1) From cf5202c74c5a847c8b7bf85f90ecb5fa04a3524f Mon Sep 17 00:00:00 2001 From: rtimms Date: Sun, 2 Nov 2025 21:58:40 +0000 Subject: [PATCH 10/35] fix set initial conditions from dict --- src/pybamm/models/base_model.py | 100 +++++++++++++++++++++++++++----- 1 file changed, 86 insertions(+), 14 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 3f7d30526e..8417b986bf 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -927,15 +927,40 @@ def set_initial_conditions_from( """ mesh = mesh or {} initial_conditions = {} + is_dict_input = isinstance(solution, dict) if isinstance(solution, pybamm.Solution): solution = solution.last_state def _evaluate_symbol(symbol): + """Evaluate symbol to numpy array, handling errors gracefully.""" + if isinstance(symbol, numbers.Number): + return np.array(symbol) + if isinstance(symbol, np.ndarray): + return symbol if hasattr(symbol, "evaluate"): - return symbol.evaluate() + try: + result = symbol.evaluate() + # Ensure result is numpy array + if isinstance(result, numbers.Number): + return np.array(result) + if isinstance(result, np.ndarray): + return result + # If evaluate returns a Symbol, try to get value + if hasattr(result, "value"): + return np.array(result.value) + except (NotImplementedError, AttributeError): + # If evaluation fails, try to get value attribute + if hasattr(symbol, "value"): + val = symbol.value + return np.array(val) if not isinstance(val, np.ndarray) else val + # Fallback: if it has a value attribute, use it + if hasattr(symbol, "value"): + val = symbol.value + return np.array(val) if not isinstance(val, np.ndarray) else val return symbol def _find_matching_variable(var, solution_model): + """Find variable in solution model that matches var by id.""" if not ( solution_model.is_discretised and solution_model.y_slices is not None ): @@ -947,6 +972,7 @@ def _find_matching_variable(var, solution_model): return None def _extract_from_y_slices(var, solution_var, solution_model, solution): + """Extract variable state from solution.y using y_slices.""" solution_y_slice = solution_model.y_slices[solution_var][0] y_last = solution.y[:, -1] if solution.y.ndim > 1 else solution.y @@ -967,16 +993,56 @@ def _extract_from_y_slices(var, solution_var, solution_model, solution): # Convert from scaled state vector to physical values # physical = reference + scale * y_scaled - solution_scale = _evaluate_symbol(solution_var.scale) - solution_reference = _evaluate_symbol(solution_var.reference) - return solution_reference + solution_scale * y_scaled + try: + solution_scale = _evaluate_symbol(solution_var.scale) + solution_reference = _evaluate_symbol(solution_var.reference) + # Ensure scale and reference are numpy arrays of compatible shape + solution_scale = np.asarray(solution_scale) + solution_reference = np.asarray(solution_reference) + # Broadcast if needed + if solution_reference.ndim == 0: + solution_reference = solution_reference * np.ones_like(y_scaled) + elif solution_reference.shape != y_scaled.shape: + # Try to broadcast + try: + solution_reference = np.broadcast_to( + solution_reference, y_scaled.shape + ) + except ValueError: + return None # Shape mismatch, fall back to dict lookup - def get_final_state_eval(final_state): - # If already a numpy array (e.g. from y_slices), it's already the final state - if isinstance(final_state, np.ndarray): - return np.array(final_state) + if solution_scale.ndim == 0: + solution_scale = solution_scale * np.ones_like(y_scaled) + elif solution_scale.shape != y_scaled.shape: + try: + solution_scale = np.broadcast_to(solution_scale, y_scaled.shape) + except ValueError: + return None # Shape mismatch, fall back to dict lookup - # Otherwise, it's a ProcessedVariable - extract .data if available + return solution_reference + solution_scale * y_scaled + except (TypeError, ValueError, AttributeError): + # If conversion fails, fall back to dict lookup + return None + + def _extract_final_time_step(var_data): + """Extract final time step from time series data.""" + var_data = np.array(var_data) + if var_data.ndim == 0: + return var_data + elif var_data.ndim == 1: + # 1D: could be time series, take last element + return np.array(var_data[-1:]) + elif var_data.ndim == 2: + return np.array(var_data[:, -1]) + elif var_data.ndim == 3: + return var_data[:, :, -1].flatten(order="F") + elif var_data.ndim == 4: + return var_data[:, :, :, -1].flatten(order="F") + else: + raise NotImplementedError("Variable must be 0D, 1D, 2D, 3D, or 4D") + + def get_final_state_eval(final_state): + # If it's a ProcessedVariable, extract .data first if isinstance(solution, pybamm.Solution) and hasattr(final_state, "data"): final_state = final_state.data @@ -986,7 +1052,8 @@ def get_final_state_eval(final_state): if final_state.ndim == 0: return np.array([final_state]) elif final_state.ndim == 1: - return np.array(final_state[-1:]) + # 1D arrays are already final state (from y_slices or processed from dict) + return np.array(final_state) elif final_state.ndim == 2: return np.array(final_state[:, -1]) elif final_state.ndim == 3: @@ -994,7 +1061,7 @@ def get_final_state_eval(final_state): elif final_state.ndim == 4: return final_state[:, :, :, -1].flatten(order="F") else: - raise NotImplementedError("Variable must be 0D, 1D, 2D, or 3D") + raise NotImplementedError("Variable must be 0D, 1D, 2D, 3D, or 4D") def get_variable_state(var): var_name = var.name @@ -1020,7 +1087,12 @@ def get_variable_state(var): # Fall back to solution[var_name] lookup try: - return solution[var_name] + var_data = solution[var_name] + # For dict inputs, extract final time step here + if is_dict_input: + return _extract_final_time_step(var_data) + else: + return var_data except KeyError as e: raise pybamm.ModelError( "To update a model from a solution, each variable in " @@ -1057,10 +1129,10 @@ def get_variable_state(var): if self.is_discretised: scale, reference = var.scale, var.reference else: - scale, reference = 1, 0 + scale, reference = pybamm.Scalar(1), pybamm.Scalar(0) initial_conditions[var] = ( pybamm.Vector(final_state_eval) - reference - ) / scale + ) / scale.evaluate() # Also update the concatenated initial conditions if the model is already # discretised From d8d4568f27428314fb29c9b00a5b73d74cf55c26 Mon Sep 17 00:00:00 2001 From: rtimms Date: Mon, 3 Nov 2025 13:24:08 +0000 Subject: [PATCH 11/35] changelog --- CHANGELOG.md | 3 +++ src/pybamm/models/base_model.py | 11 +++-------- tests/unit/test_models/test_base_model.py | 1 - 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cf300203..c605c88145 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # [Unreleased](https://github.com/pybamm-team/PyBaMM/) + +## Features +- Allow setting initial conditions from `y_slices` of a `Solution` object. ([#5257](https://github.com/pybamm-team/PyBaMM/pull/5257)) - Added docstring to `FuzzyDict.copy` explaining its return value and behavior. ([#5242](https://github.com/pybamm-team/PyBaMM/pull/5242)) # [v25.10.0](https://github.com/pybamm-team/PyBaMM/tree/v25.10.0) - 2025-10-29 diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 8417b986bf..c83b44dc5d 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -931,8 +931,7 @@ def set_initial_conditions_from( if isinstance(solution, pybamm.Solution): solution = solution.last_state - def _evaluate_symbol(symbol): - """Evaluate symbol to numpy array, handling errors gracefully.""" + def _evaluate_symbol_to_array(symbol): if isinstance(symbol, numbers.Number): return np.array(symbol) if isinstance(symbol, np.ndarray): @@ -960,7 +959,6 @@ def _evaluate_symbol(symbol): return symbol def _find_matching_variable(var, solution_model): - """Find variable in solution model that matches var by id.""" if not ( solution_model.is_discretised and solution_model.y_slices is not None ): @@ -972,7 +970,6 @@ def _find_matching_variable(var, solution_model): return None def _extract_from_y_slices(var, solution_var, solution_model, solution): - """Extract variable state from solution.y using y_slices.""" solution_y_slice = solution_model.y_slices[solution_var][0] y_last = solution.y[:, -1] if solution.y.ndim > 1 else solution.y @@ -994,8 +991,8 @@ def _extract_from_y_slices(var, solution_var, solution_model, solution): # Convert from scaled state vector to physical values # physical = reference + scale * y_scaled try: - solution_scale = _evaluate_symbol(solution_var.scale) - solution_reference = _evaluate_symbol(solution_var.reference) + solution_scale = _evaluate_symbol_to_array(solution_var.scale) + solution_reference = _evaluate_symbol_to_array(solution_var.reference) # Ensure scale and reference are numpy arrays of compatible shape solution_scale = np.asarray(solution_scale) solution_reference = np.asarray(solution_reference) @@ -1025,12 +1022,10 @@ def _extract_from_y_slices(var, solution_var, solution_model, solution): return None def _extract_final_time_step(var_data): - """Extract final time step from time series data.""" var_data = np.array(var_data) if var_data.ndim == 0: return var_data elif var_data.ndim == 1: - # 1D: could be time series, take last element return np.array(var_data[-1:]) elif var_data.ndim == 2: return np.array(var_data[:, -1]) diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index b08c9c70bb..9e95fd017f 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -1146,7 +1146,6 @@ def test_set_initial_conditions(self): ) def test_set_initial_conditions_from_y_slices(self): - """Test that set_initial_conditions_from uses y_slices for discretised models.""" # Set up a simple discretised model model = pybamm.BaseModel() var = pybamm.Variable("test_var", domain="negative electrode") From 013ec1aaa478d9f7de25f65edfffb4587d53d17f Mon Sep 17 00:00:00 2001 From: rtimms Date: Mon, 3 Nov 2025 13:24:52 +0000 Subject: [PATCH 12/35] remove local test script --- test.py | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index baae4b6987..0000000000 --- a/test.py +++ /dev/null @@ -1,30 +0,0 @@ -import pybamm - -options = { - "cell geometry": "pouch", - "current collector": "potential pair", - "dimensionality": 2, -} -model = pybamm.lithium_ion.DFN(options=options) -parameter_values = pybamm.ParameterValues("Ecker2015") -experiment = pybamm.Experiment( - [ - "Discharge at 1C until 2.8V", - "Charge at C/2 until 4.2V", - ] -) -var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "y": 8, "z": 8} -sim = pybamm.Simulation( - model, - parameter_values=parameter_values, - experiment=experiment, - var_pts=var_pts, -) -sol = sim.solve() -output_variables = [ - "Current collector current density [A.m-2]", - "X-averaged negative particle surface stoichiometry", - "X-averaged negative electrode surface potential difference [V]", - "Voltage [V]", -] -plot = sol.plot(output_variables, variable_limits="tight", shading="auto") From 385cfc6ea5b0ea9ecbd789c50c6c69878023267f Mon Sep 17 00:00:00 2001 From: rtimms Date: Thu, 6 Nov 2025 20:05:20 +0000 Subject: [PATCH 13/35] coverage --- tests/unit/test_models/test_base_model.py | 78 +++++++++++++++++++++ tests/unit/test_solvers/test_base_solver.py | 2 +- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_models/test_base_model.py b/tests/unit/test_models/test_base_model.py index 9e95fd017f..449dc1b836 100644 --- a/tests/unit/test_models/test_base_model.py +++ b/tests/unit/test_models/test_base_model.py @@ -1207,6 +1207,84 @@ def test_set_initial_condition_errors(self): with pytest.raises(pybamm.ModelError, match="must appear in the solution"): model.set_initial_conditions_from({"wrong var": 2}) + def test_set_initial_conditions_4d_array(self): + # Test 4D array handling in _extract_final_time_step + model = pybamm.BaseModel() + var = pybamm.Variable("var") + model.rhs = {var: -var} + model.initial_conditions = {var: 1} + + # Create 4D array data + var_data_4d = np.random.rand( + 2, 3, 4, 10 + ) # 4D array with time as last dimension + sol_dict = {"var": var_data_4d} + + model.set_initial_conditions_from(sol_dict) + # Should extract final time step correctly + assert isinstance(model.initial_conditions[var], pybamm.Vector) + expected = var_data_4d[:, :, :, -1].flatten(order="F") + # Entries are stored as column vector, so compare flattened + np.testing.assert_array_equal( + model.initial_conditions[var].entries.flatten(), expected + ) + + def test_set_initial_conditions_processed_variable_with_data(self): + # Test ProcessedVariable with .data attribute + model = pybamm.BaseModel() + var = pybamm.Variable("var", domain="negative electrode") + model.rhs = {var: -var} + model.initial_conditions = {var: 1} + + # Discretise + geometry = {"negative electrode": {"x_n": {"min": 0, "max": 1}}} + mesh = pybamm.Mesh( + geometry, {"negative electrode": pybamm.Uniform1DSubMesh}, {"x_n": 5} + ) + disc = pybamm.Discretisation( + mesh, {"negative electrode": pybamm.FiniteVolume()} + ) + model_disc = disc.process_model(model, inplace=False) + + # Create solution + t = np.array([0, 1]) + y = np.ones((model_disc.len_rhs_and_alg, 2)) * 5 + sol = pybamm.Solution(t, y, model_disc, {}) + + disc_var = next(iter(model_disc.initial_conditions.keys())) + # The solution should handle ProcessedVariable correctly + model_disc.set_initial_conditions_from(sol) + assert isinstance(model_disc.initial_conditions[disc_var], pybamm.Vector) + + def test_set_initial_conditions_y_slices_fallback(self): + # Test fallback when y_slices extraction fails (e.g., invalid bounds) + model = pybamm.BaseModel() + var = pybamm.Variable("var", domain="negative electrode") + model.rhs = {var: -var} + model.initial_conditions = {var: 1} + + # Discretise + geometry = {"negative electrode": {"x_n": {"min": 0, "max": 1}}} + mesh = pybamm.Mesh( + geometry, {"negative electrode": pybamm.Uniform1DSubMesh}, {"x_n": 5} + ) + disc = pybamm.Discretisation( + mesh, {"negative electrode": pybamm.FiniteVolume()} + ) + model_disc = disc.process_model(model, inplace=False) + + # Create solution with dict fallback (simulating y_slices failure) + var_data = np.ones((5, 10)) * 7 + sol_dict = {"var": var_data} + + # Should fall back to dict lookup + model_disc.set_initial_conditions_from(sol_dict) + disc_var = next(iter(model_disc.initial_conditions.keys())) + assert isinstance(model_disc.initial_conditions[disc_var], pybamm.Vector) + np.testing.assert_array_equal( + model_disc.initial_conditions[disc_var].entries, 7 + ) + def test_set_variables_error(self): var = pybamm.Variable("var") model = pybamm.BaseModel() diff --git a/tests/unit/test_solvers/test_base_solver.py b/tests/unit/test_solvers/test_base_solver.py index c0bcbf3ce2..3ad228b038 100644 --- a/tests/unit/test_solvers/test_base_solver.py +++ b/tests/unit/test_solvers/test_base_solver.py @@ -402,7 +402,7 @@ def exact_diff_b(y, a, b): inputs = {"a": a_value, "b": b_value} all_inputs.append((t, y, inputs)) for t, y, inputs in all_inputs: - use_inputs = casadi.vertcat(*[x for x in inputs.values()]) + use_inputs = casadi.vertcat(*list(inputs.values())) sens = model.jacp_rhs_algebraic_eval(t, y, use_inputs) From 45722d47c3f36d46e851044301aeb2518e996331 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Fri, 7 Nov 2025 11:27:05 +0000 Subject: [PATCH 14/35] Made the new function parameters more readable --- src/pybamm/parameters/lithium_ion_parameters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py index 33d09c0ad1..d415ac5110 100644 --- a/src/pybamm/parameters/lithium_ion_parameters.py +++ b/src/pybamm/parameters/lithium_ion_parameters.py @@ -239,8 +239,8 @@ def _set_parameters(self): "Separator porosity", { "Through-cell distance (x) [m]": x, - "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z, + "Horizontal distance (y) [m]": y, + "Vertical distance (z) [m]": z, }, ) self.epsilon_inactive = 1 - self.epsilon_init @@ -283,8 +283,8 @@ def _set_parameters(self): f"{Domain} electrode porosity", { "Through-cell distance (x) [m]": x, - "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z, + "Horizontal distance (y) [m]": y, + "Vertical distance (z) [m]": z, }, ) epsilon_s_tot = sum(phase.epsilon_s for phase in self.phase_params.values()) @@ -466,8 +466,8 @@ def _set_parameters(self): f"{pref}{Domain} electrode active material volume fraction", { "Through-cell distance (x) [m]": x, - "In-plane coordinate (y) [m]": y, - "In-plane coordinate (z) [m]": z, + "Horizontal distance (y) [m]": y, + "Vertical distance (z) [m]": z, }, ) self.epsilon_s_av = pybamm.xyz_average(self.epsilon_s) From d1f61be237f70832d3b7bc6bf2d7adf4113c719b Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Fri, 7 Nov 2025 14:21:59 +0000 Subject: [PATCH 15/35] Re-wrote example --- examples/scripts/dried_out_pouch.py | 47 ++++++++++++++++++++++++ examples/scripts/graded_pouch.py | 57 ----------------------------- 2 files changed, 47 insertions(+), 57 deletions(-) create mode 100644 examples/scripts/dried_out_pouch.py delete mode 100644 examples/scripts/graded_pouch.py diff --git a/examples/scripts/dried_out_pouch.py b/examples/scripts/dried_out_pouch.py new file mode 100644 index 0000000000..5335f1123e --- /dev/null +++ b/examples/scripts/dried_out_pouch.py @@ -0,0 +1,47 @@ +import numpy as np +import pybamm + +model = pybamm.lithium_ion.DFN( + {"current collector": "potential pair", "dimensionality": 2} +) +param = pybamm.ParameterValues("Ecker2015") +Ly = param["Electrode width [m]"] +Lz = param["Electrode height [m]"] + + +def _sigmoid(arg): + return (1 + np.tanh(arg)) / 2 + + +def _top_hat(arg, a, b, k=500): + return _sigmoid(k * (arg - a)) * _sigmoid(k * (b - arg)) + + +# Simulate drying out of the negative electrode edges by reducing porosity +def eps_n(x, y, z): + return 0.329 * ( + _top_hat(arg=y, a=Ly * 0.02, b=Ly * 0.98) + * _top_hat(arg=z, a=Lz * 0.02, b=Lz * 0.98) + ) + + +param.update({"Negative electrode porosity": eps_n}) +var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "y": 24, "z": 24} +exp = pybamm.Experiment( + [ + "Discharge at 1C until 2.7 V", + "Charge at 1C until 4.2 V", + "Hold at 4.2 V until C/20", + ] +) +sim = pybamm.Simulation(model, var_pts=var_pts, parameter_values=param, experiment=exp) +sol = sim.solve() +output_variables = [ + "X-averaged negative electrode porosity", + "X-averaged negative particle surface stoichiometry", + "X-averaged negative electrode surface potential difference [V]", + "Current collector current density", + "Current [A]", + "Voltage [V]", +] +plot = sol.plot(output_variables, variable_limits="tight", shading="auto") diff --git a/examples/scripts/graded_pouch.py b/examples/scripts/graded_pouch.py deleted file mode 100644 index 37432d48c9..0000000000 --- a/examples/scripts/graded_pouch.py +++ /dev/null @@ -1,57 +0,0 @@ -import numpy as np - -import pybamm - -model = pybamm.lithium_ion.DFN( - {"current collector": "potential pair", "dimensionality": 1} -) -param = pybamm.ParameterValues("Ecker2015") -Ly = param["Electrode width [m]"] -Lz = param["Electrode height [m]"] - - -def sigmoid(arg): - return (1 + np.tanh(arg)) / 2 - - -def top_hat(arg, a, b, k=500): - return sigmoid(k * (arg - a)) * sigmoid(k * (b - arg)) - - -def eps_s_n(x, y_cc, z_cc): - return 0.372403 * (top_hat(arg=z_cc, a=Lz * 0.05, b=Lz * 0.95)) - - -def eps_s_p(x, y_cc, z_cc): - return 0.40832 * (top_hat(arg=z_cc, a=Lz * 0.05, b=Lz * 0.95)) - - -param_dryout = param.copy() -param_dryout.update( - { - "Negative electrode active material volume fraction": eps_s_n, - "Positive electrode active material volume fraction": eps_s_p, - } -) - -var_pts = {"x_n": 8, "x_s": 8, "x_p": 8, "r_n": 8, "r_p": 8, "z": 32} -exp = pybamm.Experiment( - [ - "Discharge at 1C until 2.7 V", - "Charge at 1C until 4.2 V", - "Hold at 4.2 V until C/20", - ] -) -sim = pybamm.Simulation( - model, var_pts=var_pts, parameter_values=param_dryout, experiment=exp -) -sol = sim.solve() -output_variables = [ - "X-averaged negative electrode active material volume fraction", - "X-averaged positive electrode active material volume fraction", - "Current collector current density [A.m-2]", - "X-averaged negative particle surface stoichiometry", - "X-averaged negative electrode surface potential difference [V]", - "Voltage [V]", -] -plot = sol.plot(output_variables, variable_limits="tight", shading="auto") From 2a67b07b03e680d482da505096fc9c53a6a5b411 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Fri, 7 Nov 2025 14:22:36 +0000 Subject: [PATCH 16/35] Moved example into 3d_examples folder --- examples/scripts/{ => 3d_examples}/dried_out_pouch.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/scripts/{ => 3d_examples}/dried_out_pouch.py (100%) diff --git a/examples/scripts/dried_out_pouch.py b/examples/scripts/3d_examples/dried_out_pouch.py similarity index 100% rename from examples/scripts/dried_out_pouch.py rename to examples/scripts/3d_examples/dried_out_pouch.py From 654efe3e196deac4aceda58e04882b3705652cf3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 14:27:04 +0000 Subject: [PATCH 17/35] style: pre-commit fixes --- examples/scripts/3d_examples/dried_out_pouch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/scripts/3d_examples/dried_out_pouch.py b/examples/scripts/3d_examples/dried_out_pouch.py index 5335f1123e..157fcdefc6 100644 --- a/examples/scripts/3d_examples/dried_out_pouch.py +++ b/examples/scripts/3d_examples/dried_out_pouch.py @@ -1,4 +1,5 @@ import numpy as np + import pybamm model = pybamm.lithium_ion.DFN( From 604b39aa62be9625e8ae13ebf3d18786bf0966eb Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Mon, 10 Nov 2025 14:25:38 +0000 Subject: [PATCH 18/35] Reinstated the remaining workflows --- .github/workflows/need_reply_remove.yml | 34 +++++++++++++++++++++++++ .github/workflows/needs_reply.yml | 23 +++++++++++++++++ CHANGELOG.md | 1 + 3 files changed, 58 insertions(+) create mode 100644 .github/workflows/need_reply_remove.yml create mode 100644 .github/workflows/needs_reply.yml diff --git a/.github/workflows/need_reply_remove.yml b/.github/workflows/need_reply_remove.yml new file mode 100644 index 0000000000..c333f6b3ab --- /dev/null +++ b/.github/workflows/need_reply_remove.yml @@ -0,0 +1,34 @@ +name: Remove needs-reply label + +on: + schedule: + - cron: '0 3 * * 1' + issue_comment: + types: + - created + +permissions: {} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + contents: read + if: | + github.event.comment.author_association != 'OWNER' && + github.event.comment.author_association != 'COLLABORATOR' && + github.repository == 'pybamm-team/PyBaMM' && + github.event_name != 'pull_request' + steps: + - name: Remove needs-reply label + uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 + continue-on-error: true + with: + route: DELETE /repos/:repository/issues/:issue/labels/:label + repository: ${{ github.repository }} + issue: ${{ github.event.issue.number }} + label: needs-reply + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/needs_reply.yml b/.github/workflows/needs_reply.yml new file mode 100644 index 0000000000..72eb6dc044 --- /dev/null +++ b/.github/workflows/needs_reply.yml @@ -0,0 +1,23 @@ +name: Close old issues that need reply + +on: + schedule: + - cron: "0 0 * * *" + + +permissions: {} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + issues: read + pull-requests: read + contents: read + if: github.repository == 'pybamm-team/PyBaMM' + steps: + - name: Close old issues that need reply + uses: dwieeb/needs-reply@71e8d5144caa0d4a1e292348bfafa3866d08c855 # v2.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-label: needs-reply \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a3027965..805d2a1beb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Features - Allow setting initial conditions from `y_slices` of a `Solution` object. ([#5257](https://github.com/pybamm-team/PyBaMM/pull/5257)) - Added docstring to `FuzzyDict.copy` explaining its return value and behavior. ([#5242](https://github.com/pybamm-team/PyBaMM/pull/5242)) +- Porosity and active material fractions are now `FunctionParameters` of y and z, as well as x ([#5214](https://github.com/pybamm-team/PyBaMM/pull/5214)) ## Bug fixes From fa656da6b97e18408b782c0a4e44ec13b271fce1 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Mon, 10 Nov 2025 14:33:52 +0000 Subject: [PATCH 19/35] tidies up example --- examples/scripts/3d_examples/dried_out_pouch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/scripts/3d_examples/dried_out_pouch.py b/examples/scripts/3d_examples/dried_out_pouch.py index 157fcdefc6..5335f1123e 100644 --- a/examples/scripts/3d_examples/dried_out_pouch.py +++ b/examples/scripts/3d_examples/dried_out_pouch.py @@ -1,5 +1,4 @@ import numpy as np - import pybamm model = pybamm.lithium_ion.DFN( From 13550b354846e4b3bcb2ad48c04c1ac6a9b5624a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:35:47 +0000 Subject: [PATCH 20/35] style: pre-commit fixes --- .github/workflows/need_reply_remove.yml | 2 +- .github/workflows/needs_reply.yml | 2 +- examples/scripts/3d_examples/dried_out_pouch.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/need_reply_remove.yml b/.github/workflows/need_reply_remove.yml index c333f6b3ab..2023dc1b3c 100644 --- a/.github/workflows/need_reply_remove.yml +++ b/.github/workflows/need_reply_remove.yml @@ -31,4 +31,4 @@ jobs: issue: ${{ github.event.issue.number }} label: needs-reply env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/needs_reply.yml b/.github/workflows/needs_reply.yml index 72eb6dc044..9ee2369112 100644 --- a/.github/workflows/needs_reply.yml +++ b/.github/workflows/needs_reply.yml @@ -20,4 +20,4 @@ jobs: uses: dwieeb/needs-reply@71e8d5144caa0d4a1e292348bfafa3866d08c855 # v2.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-label: needs-reply \ No newline at end of file + issue-label: needs-reply diff --git a/examples/scripts/3d_examples/dried_out_pouch.py b/examples/scripts/3d_examples/dried_out_pouch.py index 5335f1123e..157fcdefc6 100644 --- a/examples/scripts/3d_examples/dried_out_pouch.py +++ b/examples/scripts/3d_examples/dried_out_pouch.py @@ -1,4 +1,5 @@ import numpy as np + import pybamm model = pybamm.lithium_ion.DFN( From 7bb4d997f9823e66af981ab8cfea8b9a7a16f15b Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:12:24 +0530 Subject: [PATCH 21/35] Don't be too strict with func_args longer than symbol.children --- src/pybamm/parameters/parameter_values.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py index 2bec326a93..845be2b0bf 100644 --- a/src/pybamm/parameters/parameter_values.py +++ b/src/pybamm/parameters/parameter_values.py @@ -866,12 +866,21 @@ def _process_function_parameter(self, symbol): else: new_children.append(self.process_symbol(child)) - # Get the expression and inputs for the function + # Get the expression and inputs for the function. + # func_args may include arguments that were not explicitly wired up + # in this FunctionParameter (e.g., kwargs with default values). After + # serialisation/deserialisation, we only recover the children that were + # actually connected. + # + # Using strict=True here therefore raises a ValueError when there are + # more args than children. We allow func_args to be longer than + # symbol.children and only build the mapping for the args for which we + # actually have children. expression = function_parameter.child inputs = { arg: child for arg, child in zip( - function_parameter.func_args, symbol.children, strict=True + function_parameter.func_args, symbol.children, strict=False ) } From 56bd16fbb40614dfc78afb8d794fd886a8062c27 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Thu, 13 Nov 2025 09:15:45 +0530 Subject: [PATCH 22/35] Add a test --- .../test_parameters/test_parameter_values.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_parameters/test_parameter_values.py b/tests/unit/test_parameters/test_parameter_values.py index 769705cf51..50b4c7065d 100644 --- a/tests/unit/test_parameters/test_parameter_values.py +++ b/tests/unit/test_parameters/test_parameter_values.py @@ -1298,6 +1298,31 @@ def test_to_json_with_filename(self): finally: os.remove(temp_path) + def test_roundtrip_with_keyword_args(self): + def func_no_kwargs(x): + return 2 * x + + def func_with_kwargs(x, y=1): + return 2 * x + + x = pybamm.Scalar(2) + func_param = pybamm.FunctionParameter("func", {"x": x}) + + parameter_values = pybamm.ParameterValues({"func": func_no_kwargs}) + assert parameter_values.evaluate(func_param) == 4.0 + + serialized = parameter_values.to_json() + parameter_values_loaded = pybamm.ParameterValues.from_json(serialized) + assert parameter_values_loaded.evaluate(func_param) == 4.0 + + parameter_values = pybamm.ParameterValues({"func": func_with_kwargs}) + assert parameter_values.evaluate(func_param) == 4.0 + + serialized = parameter_values.to_json() + parameter_values_loaded = pybamm.ParameterValues.from_json(serialized) + + assert parameter_values_loaded.evaluate(func_param) == 4.0 + def test_convert_symbols_in_dict_with_interpolator(self): """Test convert_symbols_in_dict with interpolator (covers lines 1154-1170).""" import numpy as np From 425af117ee378ac1c77209100e5db74788145064 Mon Sep 17 00:00:00 2001 From: Swasti Mishra <140950062+swastim01@users.noreply.github.com> Date: Sun, 16 Nov 2025 02:59:38 +0530 Subject: [PATCH 23/35] Add support for uniform grid sizing across subdomains (#720) (#5253) Co-authored-by: Valentin Sulzer --- CHANGELOG.md | 1 + src/pybamm/meshes/meshes.py | 33 +++++++++++++++++++++++++++ tests/unit/test_meshes/test_meshes.py | 31 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d51fc7641..f5ff296807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ## Features +- Added uniform grid sizing across subdomains in the x-dimension, ensuring consistent grid spacing when geometries have varying lengths. ([#5253](https://github.com/pybamm-team/PyBaMM/pull/5253)) - Added the `electrode_phases` kwarg to `plot_voltage_components()` which allows choosing between plotting primary or secondary phase overpotentials. ([#5229](https://github.com/pybamm-team/PyBaMM/pull/5229)) - Added the `num_steps_no_progress` and `t_no_progress` options in the `IDAKLUSolver` to early terminate the simulation if little progress is detected. ([#5201](https://github.com/pybamm-team/PyBaMM/pull/5201)) - EvaluateAt symbol: add support for children evaluated at edges ([#5190](https://github.com/pybamm-team/PyBaMM/pull/5190)) diff --git a/src/pybamm/meshes/meshes.py b/src/pybamm/meshes/meshes.py index f717b0a347..ddc295d57b 100644 --- a/src/pybamm/meshes/meshes.py +++ b/src/pybamm/meshes/meshes.py @@ -9,6 +9,39 @@ import pybamm +def compute_var_pts_from_thicknesses(electrode_thicknesses, grid_size): + """ + Compute a ``var_pts`` dictionary using electrode thicknesses and a target cell size (dx). + + Added as per maintainer feedback in issue # to make mesh generation + explicit — ``grid_size`` now represents the mesh cell size in metres. + + Parameters + ---------- + electrode_thicknesses : dict + Domain thicknesses in metres. + grid_size : float + Desired uniform mesh cell size (m). + + Returns + ------- + dict + Mapping of each domain to its computed grid points. + """ + if not isinstance(electrode_thicknesses, dict): + raise TypeError("electrode_thicknesses must be a dictionary") + + if not isinstance(grid_size, (int | float)) or grid_size <= 0: + raise ValueError("grid_size must be a positive number") + + var_pts = {} + for domain, thickness in electrode_thicknesses.items(): + npts = max(round(thickness / grid_size), 2) + var_pts[domain] = {f"x_{domain[0]}": npts} + + return var_pts + + class Mesh(dict): """ Mesh contains a list of submeshes on each subdomain. diff --git a/tests/unit/test_meshes/test_meshes.py b/tests/unit/test_meshes/test_meshes.py index 8c26a0900f..15ef8317ba 100644 --- a/tests/unit/test_meshes/test_meshes.py +++ b/tests/unit/test_meshes/test_meshes.py @@ -584,6 +584,37 @@ def test_to_json(self): assert mesh_json == expected_json + def test_compute_var_pts_from_thicknesses_cell_size(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + electrode_thicknesses = { + "negative electrode": 100e-6, + "separator": 25e-6, + "positive electrode": 100e-6, + } + + cell_size = 5e-6 # 5 micrometres per cell + var_pts = compute_var_pts_from_thicknesses(electrode_thicknesses, cell_size) + + assert isinstance(var_pts, dict) + assert all(isinstance(v, dict) for v in var_pts.values()) + assert var_pts["negative electrode"]["x_n"] == 20 + assert var_pts["separator"]["x_s"] == 5 + assert var_pts["positive electrode"]["x_p"] == 20 + + def test_compute_var_pts_from_thicknesses_invalid_thickness_type(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + with pytest.raises(TypeError): + compute_var_pts_from_thicknesses(["not", "a", "dict"], 1e-6) + + def test_compute_var_pts_from_thicknesses_invalid_grid_size(self): + from pybamm.meshes.meshes import compute_var_pts_from_thicknesses + + electrode_thicknesses = {"negative electrode": 100e-6} + with pytest.raises(ValueError): + compute_var_pts_from_thicknesses(electrode_thicknesses, -1e-6) + class TestMeshGenerator: def test_init_name(self): From 6ba2cde8dcee5db4732752682fed6b646e613659 Mon Sep 17 00:00:00 2001 From: Chase Naples Date: Sun, 16 Nov 2025 11:24:14 -0500 Subject: [PATCH 24/35] Fix typo in Butler-Volmer equation docstring (#5279) --- src/pybamm/models/submodels/interface/kinetics/butler_volmer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py index c3e9ac0fc0..84cee586a3 100644 --- a/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py +++ b/src/pybamm/models/submodels/interface/kinetics/butler_volmer.py @@ -12,7 +12,7 @@ class SymmetricButlerVolmer(BaseKinetics): Submodel which implements the symmetric forward Butler-Volmer equation: .. math:: - j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / RT) + j = 2 * j_0(c) * \\sinh(ne * F * \\eta_r(c) / 2RT) Parameters ---------- From e8931565f4355951872d2bae0db50d7d73dbf6e1 Mon Sep 17 00:00:00 2001 From: Robert Timms <43040151+rtimms@users.noreply.github.com> Date: Tue, 18 Nov 2025 07:30:57 +0000 Subject: [PATCH 25/35] fix bug with bulk ocp lithiation (#5280) --- .../interface/open_circuit_potential/base_hysteresis_ocp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py b/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py index 934ed5e6cf..cdfc47ac5e 100644 --- a/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py +++ b/src/pybamm/models/submodels/interface/open_circuit_potential/base_hysteresis_ocp.py @@ -84,7 +84,7 @@ def _get_coupled_variables(self, variables): U_eq = self.phase_param.U(sto_surf, T) U_eq_x_av = self.phase_param.U(sto_surf, T) U_lith = self.phase_param.U(sto_surf, T, "lithiation") - U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk) + U_lith_bulk = self.phase_param.U(sto_bulk, T_bulk, "lithiation") U_delith = self.phase_param.U(sto_surf, T, "delithiation") U_delith_bulk = self.phase_param.U(sto_bulk, T_bulk, "delithiation") From 6c8cbfd3d83814925ebf9e1f1aef65e4a4d08ae7 Mon Sep 17 00:00:00 2001 From: Gregor Decristoforo Date: Wed, 19 Nov 2025 15:47:54 +0100 Subject: [PATCH 26/35] doc: fix typo in concentration description in notebook (#5284) * Fix typo in concentration description in notebook * Add CHANGELOG.md entry for typo fix * Remove unneccesary changelog entry Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --------- Co-authored-by: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> --- .../4-comparing-full-and-reduced-order-models.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb b/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb index 071fd54f95..19780371c0 100644 --- a/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb +++ b/docs/source/examples/notebooks/creating_models/4-comparing-full-and-reduced-order-models.ipynb @@ -24,7 +24,7 @@ "$$\n", "\\left.c\\right\\vert_{t=0} = c_0,\n", "$$\n", - "where $c$$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", + "where $c$ is the concentration, $r$ the radial coordinate, $t$ time, $R$ the particle radius, $D$ the diffusion coefficient, $j$ the interfacial current density, $F$ Faraday's constant, and $c_0$ the initial concentration. \n", "\n", "As in the previous example we use the following parameters:\n", "\n", From 0a1dae147fc883a4fd6e6b8129180ffa9315f339 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Thu, 20 Nov 2025 13:39:19 +0000 Subject: [PATCH 27/35] Fixed erroring examples --- .../notebooks/models/graded-electrodes.ipynb | 46 ++++++++----------- .../scripts/3d_examples/dried_out_pouch.py | 2 +- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/docs/source/examples/notebooks/models/graded-electrodes.ipynb b/docs/source/examples/notebooks/models/graded-electrodes.ipynb index 272248d7e8..7954c0fbd6 100644 --- a/docs/source/examples/notebooks/models/graded-electrodes.ipynb +++ b/docs/source/examples/notebooks/models/graded-electrodes.ipynb @@ -22,13 +22,6 @@ "text": [ "Note: you may need to restart the kernel to use updated packages.\n" ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "An NVIDIA GPU may be present on this machine, but a CUDA-enabled jaxlib is not installed. Falling back to cpu.\n" - ] } ], "source": [ @@ -59,7 +52,7 @@ "source": [ "We will vary the porosity in both electrodes and we will try three different scenarios: constant porosity, one where lower porosity occurs near the separator and one where lower porosity occurs near the current collector. All other parameters are kept constant. The varying porosity is defined to be linear centered around the default value and with a variation of $\\pm$ 10%.\n", "\n", - "We define the varying porosities and store them in a list so we can loop over when solving the model." + "We define the varying porosities and store them in a list so we can loop over when solving the model. Note that the porosities must be specified as functions of x, y, and z, even if y and z are not present in the model and not referred to at any other point, as in this example." ] }, { @@ -77,14 +70,14 @@ "\n", "eps_ns = [\n", " eps_n_0,\n", - " lambda x: eps_n_0 * (1.1 - 0.2 * (x / L_n)),\n", - " lambda x: eps_n_0 * (0.9 + 0.2 * (x / L_n)),\n", + " lambda x, y, z: eps_n_0 * (1.1 - 0.2 * (x / L_n)),\n", + " lambda x, y, z: eps_n_0 * (0.9 + 0.2 * (x / L_n)),\n", "]\n", "\n", "eps_ps = [\n", " eps_p_0,\n", - " lambda x: eps_p_0 * (0.9 - 0.2 / L_p * (L_n + L_s) + 0.2 * (x / L_p)),\n", - " lambda x: eps_p_0 * (1.1 + 0.2 / L_p * (L_n + L_s) - 0.2 * (x / L_p)),\n", + " lambda x, y, z: eps_p_0 * (0.9 - 0.2 / L_p * (L_n + L_s) + 0.2 * (x / L_p)),\n", + " lambda x, y, z: eps_p_0 * (1.1 + 0.2 / L_p * (L_n + L_s) - 0.2 * (x / L_p)),\n", "]" ] }, @@ -132,12 +125,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "99f847ca09da40cba550dd02dd8281a2", + "model_id": "9198b6c974714d07b0a68f1cbc42c988", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9136958613059, step=6.7391369586130585),…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9107055793413, step=6.739107055793413), …" ] }, "metadata": {}, @@ -146,7 +139,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -176,18 +169,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "829b68c6b3e04e0ebe5537daabec2278", + "model_id": "666bc13bf8cb4ee9b1a110cc828f025b", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9136958613059, step=6.7391369586130585),…" + "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9107055793413, step=6.739107055793413), …" ] }, "metadata": {}, @@ -196,10 +189,10 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -218,7 +211,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -232,10 +225,7 @@ "[5] Charles R. Harris, K. Jarrod Millman, Stéfan J. van der Walt, Ralf Gommers, Pauli Virtanen, David Cournapeau, Eric Wieser, Julian Taylor, Sebastian Berg, Nathaniel J. Smith, and others. Array programming with NumPy. Nature, 585(7825):357–362, 2020. doi:10.1038/s41586-020-2649-2.\n", "[6] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.\n", "[7] Alan C. Hindmarsh, Peter N. Brown, Keith E. Grant, Steven L. Lee, Radu Serban, Dan E. Shumaker, and Carol S. Woodward. SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers. ACM Transactions on Mathematical Software (TOMS), 31(3):363–396, 2005. doi:10.1145/1089014.1089020.\n", - "[8] Peyman Mohtat, Suhak Lee, Jason B Siegel, and Anna G Stefanopoulou. Towards better estimability of electrode-specific state of health: decoding the cell expansion. Journal of Power Sources, 427:101–111, 2019.\n", - "[9] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", - "[10] Pauli Virtanen, Ralf Gommers, Travis E. Oliphant, Matt Haberland, Tyler Reddy, David Cournapeau, Evgeni Burovski, Pearu Peterson, Warren Weckesser, Jonathan Bright, and others. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods, 17(3):261–272, 2020. doi:10.1038/s41592-019-0686-2.\n", - "[11] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.\n", + "[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.\n", "\n" ] } @@ -254,7 +244,7 @@ ], "metadata": { "kernelspec": { - "display_name": "venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -268,9 +258,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.6" + "version": "3.10.12" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/scripts/3d_examples/dried_out_pouch.py b/examples/scripts/3d_examples/dried_out_pouch.py index 157fcdefc6..f795e1676b 100644 --- a/examples/scripts/3d_examples/dried_out_pouch.py +++ b/examples/scripts/3d_examples/dried_out_pouch.py @@ -41,7 +41,7 @@ def eps_n(x, y, z): "X-averaged negative electrode porosity", "X-averaged negative particle surface stoichiometry", "X-averaged negative electrode surface potential difference [V]", - "Current collector current density", + "Current collector current density [A.m-2]", "Current [A]", "Voltage [V]", ] From 22ef9114caca766b191a3876710231a1f909d115 Mon Sep 17 00:00:00 2001 From: Simon O'Kane Date: Thu, 20 Nov 2025 13:55:56 +0000 Subject: [PATCH 28/35] dependabot --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..8e9016eeb6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + # group updates in a single PR + groups: + actions: + patterns: + - "*" \ No newline at end of file From 854f6069994c4aa5773ac3a42a7607eaac02f4c5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 13:56:13 +0000 Subject: [PATCH 29/35] style: pre-commit fixes --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8e9016eeb6..c4fbd385c1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,4 +9,4 @@ updates: groups: actions: patterns: - - "*" \ No newline at end of file + - "*" From 385ae7b3ccf37b6bf32fc37dc03dde80e0abeeda Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 20 Nov 2025 17:23:47 +0000 Subject: [PATCH 30/35] fix: instruct uv to install into system for CI (#5288) --- .github/workflows/benchmark_on_push.yml | 2 +- .github/workflows/periodic_benchmarks.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/benchmark_on_push.yml b/.github/workflows/benchmark_on_push.yml index 18a9e7931d..f70556c782 100644 --- a/.github/workflows/benchmark_on_push.yml +++ b/.github/workflows/benchmark_on_push.yml @@ -40,7 +40,7 @@ jobs: enable-cache: true - name: Install python dependencies - run: uv pip install asv[virtualenv] + run: uv pip install --system asv - name: Fetch base branch run: | diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml index 86cd1991ab..0a7a7d9c47 100644 --- a/.github/workflows/periodic_benchmarks.yml +++ b/.github/workflows/periodic_benchmarks.yml @@ -48,7 +48,7 @@ jobs: enable-cache: true - name: Install python dependencies - run: uv pip install asv[virtualenv] + run: uv pip install --system asv - name: Run benchmarks run: | From 1319b4a985b848a17d9714888c323149d42a4b9a Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:32:21 -0500 Subject: [PATCH 31/35] Fix `InputParameter` serialisation (#5289) * fix `InputParameter` serialisation * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ src/pybamm/expression_tree/operations/serialise.py | 2 ++ tests/unit/test_serialisation/test_serialisation.py | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba2390255..6fb52142ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## Bug fixes +- Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289)) + # [v25.10.1](https://github.com/pybamm-team/PyBaMM/tree/v25.10.1) - 2025-11-14 ## Features diff --git a/src/pybamm/expression_tree/operations/serialise.py b/src/pybamm/expression_tree/operations/serialise.py index 6b733e0fad..3cc8f3e98f 100644 --- a/src/pybamm/expression_tree/operations/serialise.py +++ b/src/pybamm/expression_tree/operations/serialise.py @@ -1625,6 +1625,8 @@ def convert_symbol_from_json(json_data): elif json_data["type"] == "Parameter": # Convert stored parameters back to PyBaMM Parameter objects return pybamm.Parameter(json_data["name"]) + elif json_data["type"] == "InputParameter": + return pybamm.InputParameter(json_data["name"]) elif json_data["type"] == "Scalar": # Convert stored numerical values back to PyBaMM Scalar objects return pybamm.Scalar(json_data["value"]) diff --git a/tests/unit/test_serialisation/test_serialisation.py b/tests/unit/test_serialisation/test_serialisation.py index d6ddac10e4..4dafcc5641 100644 --- a/tests/unit/test_serialisation/test_serialisation.py +++ b/tests/unit/test_serialisation/test_serialisation.py @@ -617,6 +617,14 @@ def test_serialise_time(self): t2 = convert_symbol_from_json(j) assert isinstance(t2, pybamm.Time) + def test_serialise_input_parameter(self): + """Test InputParameter serialization and deserialization.""" + ip = pybamm.InputParameter("test_param") + j = convert_symbol_to_json(ip) + ip_restored = convert_symbol_from_json(j) + assert isinstance(ip_restored, pybamm.InputParameter) + assert ip_restored.name == "test_param" + def test_convert_symbol_to_json_with_number_and_list(self): for val in (0, 3.14, -7, True): out = convert_symbol_to_json(val) From 80e18706796be782db7bf8cc0a9400b4a6546ff6 Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Fri, 21 Nov 2025 09:08:04 +0000 Subject: [PATCH 32/35] Bugfix: inputs for `initial_conditions_from` scale evaluation (#5285) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/pybamm/models/base_model.py | 11 +++++++++-- .../full_battery_models/lithium_ion/electrode_soh.py | 6 +++--- src/pybamm/solvers/base_solver.py | 4 +++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py index 9fd7483385..67096912cf 100644 --- a/src/pybamm/models/base_model.py +++ b/src/pybamm/models/base_model.py @@ -906,7 +906,12 @@ def _build_model(self): self.build_model_equations() def set_initial_conditions_from( - self, solution, inplace=True, return_type="model", mesh=None + self, + solution, + inputs=None, + inplace=True, + return_type="model", + mesh=None, ): """ Update initial conditions with the final states from a Solution object or from @@ -918,6 +923,8 @@ def set_initial_conditions_from( ---------- solution : :class:`pybamm.Solution`, or dict The solution to use to initialize the model + inputs : dict + The dictionary of model input parameters. inplace : bool, optional Whether to modify the model inplace or create a new model (default True) return_type : str, optional @@ -1081,7 +1088,7 @@ def get_variable_state(var): scale, reference = pybamm.Scalar(1), pybamm.Scalar(0) initial_conditions[var] = ( pybamm.Vector(final_state_eval) - reference - ) / scale.evaluate() + ) / scale.evaluate(inputs=inputs) # Also update the concatenated initial conditions if the model is already # discretised diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py index a9322991b2..b9abb4261f 100644 --- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py +++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh.py @@ -561,14 +561,14 @@ def _set_up_solve(self, inputs, direction): def _solve_full(self, inputs, ics, direction): sim = self._get_electrode_soh_sims_full(direction) sim.build() - sim.built_model.set_initial_conditions_from(ics) + sim.built_model.set_initial_conditions_from(ics, inputs=inputs) sol = sim.solve([0], inputs=inputs) return sol def _solve_split(self, inputs, ics, direction): x100_sim, x0_sim = self._get_electrode_soh_sims_split(direction) x100_sim.build() - x100_sim.built_model.set_initial_conditions_from(ics) + x100_sim.built_model.set_initial_conditions_from(ics, inputs=inputs) x100_sol = x100_sim.solve([0], inputs=inputs) if self.options["open-circuit potential"] == "MSMR": inputs["Un(x_100)"] = x100_sol["Un(x_100)"].data[0] @@ -577,7 +577,7 @@ def _solve_split(self, inputs, ics, direction): inputs["x_100"] = x100_sol["x_100"].data[0] inputs["y_100"] = x100_sol["y_100"].data[0] x0_sim.build() - x0_sim.built_model.set_initial_conditions_from(ics) + x0_sim.built_model.set_initial_conditions_from(ics, inputs=inputs) x0_sol = x0_sim.solve([0], inputs=inputs) return x0_sol diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py index 92c7381f43..a1db0c2953 100644 --- a/src/pybamm/solvers/base_solver.py +++ b/src/pybamm/solvers/base_solver.py @@ -1381,7 +1381,9 @@ def step( else: _, concatenated_initial_conditions = model.set_initial_conditions_from( - old_solution, return_type="ics" + old_solution, + inputs=model_inputs, + return_type="ics", ) model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs) if using_sensitivities: From 73ab559adc229264294fcc085daee503e969bdde Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:31:26 +0000 Subject: [PATCH 33/35] Add `silence_sundials_errors` solver option (#5290) * feat: add`silence_sundial_warnings` solver option * refactor: `silence_sundials_warnings` -> `silence_sundials_errors` --- CHANGELOG.md | 2 ++ src/pybamm/solvers/idaklu_solver.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb52142ad..3463d08e5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Features +- Adds `silence_sundials_errors` IDAKLU solver option with `default=False` to match historical output. ([#5290](https://github.com/pybamm-team/PyBaMM/pull/5290)) + ## Bug fixes - Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289)) diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index 4a0d9edb07..e418b4d3e5 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -76,6 +76,8 @@ class IDAKLUSolver(pybamm.BaseSolver): "increment_factor": 1.0, # Enable or disable linear solution scaling "linear_solution_scaling": True, + # Silence Sundials errors during solve + "silence_sundials_errors": False, ## Main solver # Maximum order of the linear multistep method "max_order_bdf": 5, @@ -176,6 +178,7 @@ def __init__( "epsilon_linear_tolerance": 0.05, "increment_factor": 1.0, "linear_solution_scaling": True, + "silence_sundials_errors": False, "max_order_bdf": 5, "max_num_steps": 100000, "dt_init": 0.0, From 593d2aa026f35efe9afd34f522ee36ab5e66a0bc Mon Sep 17 00:00:00 2001 From: Brady Planden <55357039+BradyPlanden@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:17:53 +0000 Subject: [PATCH 34/35] Update C-Rate current for changing nominal capacity (#5286) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- src/pybamm/simulation.py | 44 +++++++ .../test_simulation_with_experiment.py | 123 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index 8f3fcaaad1..8faff7a2e8 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -128,6 +128,7 @@ def __init__( self._model_with_set_params = None self._built_model = None self._built_initial_soc = None + self._built_nominal_capacity = None self.steps_to_built_models = None self.steps_to_built_solvers = None self._mesh = None @@ -163,6 +164,42 @@ def set_up_and_parameterise_experiment(self, solve_kwargs=None): warnings.warn(msg, DeprecationWarning, stacklevel=2) self._set_up_and_parameterise_experiment(solve_kwargs=solve_kwargs) + def _update_experiment_models_for_capacity(self, solve_kwargs=None): + """ + Check if the nominal capacity has changed and update the experiment models + if needed. This re-processes the models without rebuilding the mesh and + discretisation. + """ + current_capacity = self._parameter_values.get( + "Nominal cell capacity [A.h]", None + ) + + if self._built_nominal_capacity == current_capacity: + return + + # Capacity has changed, need to re-process the models + pybamm.logger.info( + f"Nominal capacity changed from {self._built_nominal_capacity} to " + f"{current_capacity}. Re-processing experiment models." + ) + + # Re-parameterise the experiment with the new capacity + self._set_up_and_parameterise_experiment(solve_kwargs) + + # Re-discretise the models + self.steps_to_built_models = {} + self.steps_to_built_solvers = {} + for ( + step, + model_with_set_params, + ) in self.experiment_unique_steps_to_model.items(): + built_model = self._disc.process_model(model_with_set_params, inplace=True) + solver = self._solver.copy() + self.steps_to_built_solvers[step] = solver + self.steps_to_built_models[step] = built_model + + self._built_nominal_capacity = current_capacity + def _set_up_and_parameterise_experiment(self, solve_kwargs=None): """ Create and parameterise the models for each step in the experiment. @@ -266,6 +303,7 @@ def set_initial_state(self, initial_soc, direction=None, inputs=None): # reset self._model_with_set_params = None self._built_model = None + self._built_nominal_capacity = None self.steps_to_built_models = None self.steps_to_built_solvers = None @@ -338,6 +376,8 @@ def build_for_experiment( self.set_initial_state(initial_soc, direction=direction, inputs=inputs) if self.steps_to_built_models: + # Check if we need to update the models due to capacity change + self._update_experiment_models_for_capacity(solve_kwargs) return else: self._set_up_and_parameterise_experiment(solve_kwargs) @@ -366,6 +406,10 @@ def build_for_experiment( self.steps_to_built_solvers[step] = solver self.steps_to_built_models[step] = built_model + self._built_nominal_capacity = self._parameter_values.get( + "Nominal cell capacity [A.h]", None + ) + def solve( self, t_eval=None, diff --git a/tests/unit/test_experiments/test_simulation_with_experiment.py b/tests/unit/test_experiments/test_simulation_with_experiment.py index 2b62b83db3..3c5099dde7 100644 --- a/tests/unit/test_experiments/test_simulation_with_experiment.py +++ b/tests/unit/test_experiments/test_simulation_with_experiment.py @@ -1,3 +1,4 @@ +import logging import os from datetime import datetime @@ -1022,3 +1023,125 @@ def neg_stoich_cutoff(variables): neg_stoich = sol["Negative electrode stoichiometry"].data assert neg_stoich[-1] == pytest.approx(0.5, abs=0.0001) + + def test_simulation_changing_capacity_crate_steps(self): + """Test that C-rate steps are correctly updated when capacity changes""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment( + [ + ( + "Discharge at C/5 for 20 minutes", + "Discharge at C/2 for 20 minutes", + "Discharge at 1C for 20 minutes", + ) + ] + ) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sol1 = sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Check that C-rates correspond to expected currents + I_C5_1 = np.abs(sol1.cycles[0].steps[0]["Current [A]"].data).mean() + I_C2_1 = np.abs(sol1.cycles[0].steps[1]["Current [A]"].data).mean() + I_1C_1 = np.abs(sol1.cycles[0].steps[2]["Current [A]"].data).mean() + + np.testing.assert_allclose(I_C5_1, original_capacity / 5, rtol=1e-2) + np.testing.assert_allclose(I_C2_1, original_capacity / 2, rtol=1e-2) + np.testing.assert_allclose(I_1C_1, original_capacity, rtol=1e-2) + + # Update capacity + new_capacity = 0.9 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Second solve with updated capacity + sol2 = sim.solve(calc_esoh=False) + + # Check that C-rates now correspond to updated currents + I_C5_2 = np.abs(sol2.cycles[0].steps[0]["Current [A]"].data).mean() + I_C2_2 = np.abs(sol2.cycles[0].steps[1]["Current [A]"].data).mean() + I_1C_2 = np.abs(sol2.cycles[0].steps[2]["Current [A]"].data).mean() + + np.testing.assert_allclose(I_C5_2, new_capacity / 5, rtol=1e-2) + np.testing.assert_allclose(I_C2_2, new_capacity / 2, rtol=1e-2) + np.testing.assert_allclose(I_1C_2, new_capacity, rtol=1e-2) + + # Verify all currents scaled proportionally + np.testing.assert_allclose(I_C5_2 / I_C5_1, 0.9, rtol=1e-2) + np.testing.assert_allclose(I_C2_2 / I_C2_1, 0.9, rtol=1e-2) + np.testing.assert_allclose(I_1C_2 / I_1C_1, 0.9, rtol=1e-2) + + def test_simulation_multiple_cycles_with_capacity_change(self): + """Test capacity changes across multiple experiment cycles""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment( + [("Discharge at 1C for 5 minutes", "Charge at 1C for 5 minutes")] * 2 + ) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sol1 = sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Get discharge currents for both cycles + I_discharge_cycle1 = np.abs(sol1.cycles[0].steps[0]["Current [A]"].data).mean() + I_discharge_cycle2 = np.abs(sol1.cycles[1].steps[0]["Current [A]"].data).mean() + + # Both cycles should use the same capacity initially + np.testing.assert_allclose(I_discharge_cycle1, original_capacity, rtol=1e-2) + np.testing.assert_allclose(I_discharge_cycle2, original_capacity, rtol=1e-2) + + # Update capacity between cycles + new_capacity = 0.85 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Solve again + sol2 = sim.solve(calc_esoh=False) + + # All cycles in the new solution should use updated capacity + I_discharge_cycle1_new = np.abs( + sol2.cycles[0].steps[0]["Current [A]"].data + ).mean() + I_discharge_cycle2_new = np.abs( + sol2.cycles[1].steps[0]["Current [A]"].data + ).mean() + + np.testing.assert_allclose(I_discharge_cycle1_new, new_capacity, rtol=1e-2) + np.testing.assert_allclose(I_discharge_cycle2_new, new_capacity, rtol=1e-2) + + def test_simulation_logging_with_capacity_change(self, caplog): + """Test that capacity changes are logged appropriately""" + model = pybamm.lithium_ion.SPM() + experiment = pybamm.Experiment([("Discharge at 1C for 10 minutes",)]) + param = pybamm.ParameterValues("Chen2020") + sim = pybamm.Simulation(model, experiment=experiment, parameter_values=param) + + # First solve + sim.solve(calc_esoh=False) + original_capacity = param["Nominal cell capacity [A.h]"] + + # Update capacity + new_capacity = 0.75 * original_capacity + sim._parameter_values.update({"Nominal cell capacity [A.h]": new_capacity}) + + # Set logging level to capture INFO messages + original_log_level = pybamm.logger.level + pybamm.set_logging_level("INFO") + + try: + # Second solve should log capacity change + with caplog.at_level(logging.INFO, logger="pybamm.logger"): + sim.solve(calc_esoh=False) + + # Check that a log message about capacity change was recorded + log_messages = [record.message for record in caplog.records] + capacity_change_logged = any( + "Nominal capacity changed" in msg for msg in log_messages + ) + assert capacity_change_logged + finally: + # Restore original logging level + pybamm.logger.setLevel(original_log_level) From df53205cbcbbf756f1340a1e72e358950255a7b6 Mon Sep 17 00:00:00 2001 From: Marc Berliner <34451391+MarcBerliner@users.noreply.github.com> Date: Mon, 1 Dec 2025 11:34:47 -0500 Subject: [PATCH 35/35] Update `IDAKLUSolver` error handling (#5291) * raise `SolverError` at failure to init sundials * Update simulation.py * Update idaklu_solver.py * reuse `pybammsolvers` error messages * Update test_idaklu_solver.py * bump `pybammsolvers` * Update CHANGELOG.md * Update CHANGELOG.md Update CHANGELOG.md --- CHANGELOG.md | 1 + pyproject.toml | 2 +- src/pybamm/simulation.py | 2 +- src/pybamm/solvers/idaklu_solver.py | 60 +++++-------------- tests/unit/test_solvers/test_idaklu_solver.py | 6 +- 5 files changed, 20 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3463d08e5d..20442e0959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ## Bug fixes +- Fixed a bug where `IDAKLUSolver` errors were not raised correctly. ([#5291](https://github.com/pybamm-team/PyBaMM/pull/5291)) - Fix a bug with serialising `InputParameter`s. ([#5289](https://github.com/pybamm-team/PyBaMM/pull/5289)) # [v25.10.1](https://github.com/pybamm-team/PyBaMM/tree/v25.10.1) - 2025-11-14 diff --git a/pyproject.toml b/pyproject.toml index 5f214ef642..364b31a52e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ "Topic :: Scientific/Engineering", ] dependencies = [ - "pybammsolvers>=0.3.0,<0.4.0", + "pybammsolvers>=0.3.3,<0.4.0", "black", "numpy", "scipy>=1.11.4", diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py index 8faff7a2e8..93d01c5af4 100644 --- a/src/pybamm/simulation.py +++ b/src/pybamm/simulation.py @@ -822,7 +822,7 @@ def solve( feasible = False # If none of the cycles worked, raise an error if cycle_num == 1 and step_num == 1: - raise error + raise error from error # Otherwise, just stop this cycle break diff --git a/src/pybamm/solvers/idaklu_solver.py b/src/pybamm/solvers/idaklu_solver.py index e418b4d3e5..147bc0314e 100644 --- a/src/pybamm/solvers/idaklu_solver.py +++ b/src/pybamm/solvers/idaklu_solver.py @@ -684,13 +684,17 @@ def _integrate( atol = self._check_atol_type(atol, y0full.size) timer = pybamm.Timer() - solns = self._setup["solver"].solve( - t_eval, - t_interp, - y0full, - ydot0full, - inputs, - ) + try: + solns = self._setup["solver"].solve( + t_eval, + t_interp, + y0full, + ydot0full, + inputs, + ) + except ValueError as e: + # Return from None to replace the C++ runtime error + raise pybamm.SolverError(str(e)) from None integration_time = timer.time() return [ @@ -737,14 +741,15 @@ def _post_process_solution(self, sol, model, integration_time, inputs_dict): termination = "final time" elif sol.flag < 0: termination = "failure" + msg = idaklu.sundials_error_message(sol.flag) match self._on_failure: case "warn": warnings.warn( - f"FAILURE {self._solver_flag(sol.flag)}, returning a partial solution.", + msg + ", returning a partial solution.", stacklevel=2, ) case "raise": - raise pybamm.SolverError(f"FAILURE {self._solver_flag(sol.flag)}") + raise pybamm.SolverError(msg) if sol.yp.size > 0: yp = sol.yp.reshape((number_of_timesteps, number_of_states)).T @@ -1005,40 +1010,3 @@ def jaxify( t_interp=t_interp, ) return obj - - @staticmethod - def _solver_flag(flag): - flags = { - 99: "IDA_WARNING: IDASolve succeeded but an unusual situation occurred.", - 2: "IDA_ROOT_RETURN: IDASolve succeeded and found one or more roots.", - 1: "IDA_TSTOP_RETURN: IDASolve succeeded by reaching the specified stopping point.", - 0: "IDA_SUCCESS: Successful function return.", - -1: "IDA_TOO_MUCH_WORK: The solver took mxstep internal steps but could not reach tout.", - -2: "IDA_TOO_MUCH_ACC: The solver could not satisfy the accuracy demanded by the user for some internal step.", - -3: "IDA_ERR_FAIL: Error test failures occurred too many times during one internal time step or minimum step size was reached.", - -4: "IDA_CONV_FAIL: Convergence test failures occurred too many times during one internal time step or minimum step size was reached.", - -5: "IDA_LINIT_FAIL: The linear solver's initialization function failed.", - -6: "IDA_LSETUP_FAIL: The linear solver's setup function failed in an unrecoverable manner.", - -7: "IDA_LSOLVE_FAIL: The linear solver's solve function failed in an unrecoverable manner.", - -8: "IDA_RES_FAIL: The user-provided residual function failed in an unrecoverable manner.", - -9: "IDA_REP_RES_FAIL: The user-provided residual function repeatedly returned a recoverable error flag, but the solver was unable to recover.", - -10: "IDA_RTFUNC_FAIL: The rootfinding function failed in an unrecoverable manner.", - -11: "IDA_CONSTR_FAIL: The inequality constraints were violated and the solver was unable to recover.", - -12: "IDA_FIRST_RES_FAIL: The user-provided residual function failed recoverably on the first call.", - -13: "IDA_LINESEARCH_FAIL: The line search failed.", - -14: "IDA_NO_RECOVERY: The residual function, linear solver setup function, or linear solver solve function had a recoverable failure, but IDACalcIC could not recover.", - -15: "IDA_NLS_INIT_FAIL: The nonlinear solver's init routine failed.", - -16: "IDA_NLS_SETUP_FAIL: The nonlinear solver's setup routine failed.", - -20: "IDA_MEM_NULL: The ida mem argument was NULL.", - -21: "IDA_MEM_FAIL: A memory allocation failed.", - -22: "IDA_ILL_INPUT: One of the function inputs is illegal.", - -23: "IDA_NO_MALLOC: The ida memory was not allocated by a call to IDAInit.", - -24: "IDA_BAD_EWT: Zero value of some error weight component.", - -25: "IDA_BAD_K: The k-th derivative is not available.", - -26: "IDA_BAD_T: The time t is outside the last step taken.", - -27: "IDA_BAD_DKY: The vector argument where derivative should be stored is NULL.", - } - - flag_unknown = "Unknown IDA flag." - - return flags.get(flag, flag_unknown) diff --git a/tests/unit/test_solvers/test_idaklu_solver.py b/tests/unit/test_solvers/test_idaklu_solver.py index a86e18b21d..5b55e5e3d8 100644 --- a/tests/unit/test_solvers/test_idaklu_solver.py +++ b/tests/unit/test_solvers/test_idaklu_solver.py @@ -583,7 +583,7 @@ def test_failures(self): solver = pybamm.IDAKLUSolver() t_eval = [0, 3] - with pytest.raises(ValueError): + with pytest.raises(pybamm.SolverError): solver.solve(model, t_eval) def test_dae_solver_algebraic_model(self): @@ -779,7 +779,7 @@ def test_solver_options(self): options = {option: options_fail[option]} solver = pybamm.IDAKLUSolver(options=options) - with pytest.raises(ValueError): + with pytest.raises(pybamm.SolverError): solver.solve(model, t_eval) def test_with_output_variables(self): @@ -1487,7 +1487,7 @@ def test_on_failure_option(self): model, t_eval=t_eval, t_interp=t_interp, inputs=input_parameters ) assert len(w) > 0 - assert "FAILURE" in str(w[0].message) + assert "_FAIL" in str(w[0].message) def test_no_progress_early_termination(self): # SPM at rest