diff --git a/.all-contributorsrc b/.all-contributorsrc
index 4366246007..dcde101e80 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -747,7 +747,8 @@
"profile": "https://github.com/AbhishekChaudharii",
"contributions": [
"doc",
- "code"
+ "code",
+ "test"
]
},
{
@@ -960,6 +961,36 @@
"code",
"test"
]
+ },
+ {
+ "login": "medha-14",
+ "name": "Medha Bhardwaj",
+ "avatar_url": "https://avatars.githubusercontent.com/u/143182673?v=4",
+ "profile": "https://github.com/medha-14",
+ "contributions": [
+ "code"
+ ]
+ },
+ {
+ "login": "MarcBerliner",
+ "name": "Marc Berliner",
+ "avatar_url": "https://avatars.githubusercontent.com/u/34451391?v=4",
+ "profile": "http://marcberliner.com",
+ "contributions": [
+ "code",
+ "doc",
+ "infra",
+ "maintenance"
+ ]
+ },
+ {
+ "login": "Aswinr24",
+ "name": "Aswinr24",
+ "avatar_url": "https://avatars.githubusercontent.com/u/135364633?v=4",
+ "profile": "https://github.com/Aswinr24",
+ "contributions": [
+ "test"
+ ]
}
],
"contributorsPerLine": 7,
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 0f503f09a7..6984abf32c 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -10,7 +10,7 @@ src/pybamm/meshes/ @martinjrobins @rtimms @valentinsulzer @rtimms
src/pybamm/models/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms
src/pybamm/parameters/ @brosaplanella @DrSOKane @rtimms @valentinsulzer @TomTranter @rtimms @kratman
src/pybamm/plotting/ @martinjrobins @rtimms @Saransh-cpp @valentinsulzer @rtimms @kratman @agriyakhetarpal
-src/pybamm/solvers/ @martinjrobins @rtimms @valentinsulzer @TomTranter @rtimms
+src/pybamm/solvers/ @martinjrobins @rtimms @valentinsulzer @TomTranter @rtimms @MarcBerliner
src/pybamm/spatial_methods/ @martinjrobins @rtimms @valentinsulzer @rtimms
src/pybamm/* @pybamm-team/maintainers # the files directly under /pybamm/, will not recurse
diff --git a/.github/release_reminder.md b/.github/release_reminder.md
deleted file mode 100644
index 09c524fbec..0000000000
--- a/.github/release_reminder.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: Create {{ date | date('YY.MM') }} (final or rc0) release
-labels: "priority: high"
----
-Quarterly reminder to create a -
-
-1. pre-release if the month has just started.
-2. non-pre-release if the month is about to end (**before the end of the month**).
-
-See [Release Workflow](https://github.com/pybamm-team/PyBaMM/blob/develop/.github/release_workflow.md) for more information.
diff --git a/.github/workflows/benchmark_on_push.yml b/.github/workflows/benchmark_on_push.yml
index 2883eb5f26..3aa9fce9c0 100644
--- a/.github/workflows/benchmark_on_push.yml
+++ b/.github/workflows/benchmark_on_push.yml
@@ -8,6 +8,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
+
jobs:
benchmarks:
runs-on: ubuntu-latest
diff --git a/.github/workflows/lychee_url_checker.yml b/.github/workflows/lychee_url_checker.yml
index 9a636fda8a..e595bff826 100644
--- a/.github/workflows/lychee_url_checker.yml
+++ b/.github/workflows/lychee_url_checker.yml
@@ -28,7 +28,7 @@ jobs:
# use stable version for now to avoid breaking changes
- name: Lychee URL checker
- uses: lycheeverse/lychee-action@v1.10.0
+ uses: lycheeverse/lychee-action@v2.1.0
with:
# arguments with file types to check
args: >-
diff --git a/.github/workflows/periodic_benchmarks.yml b/.github/workflows/periodic_benchmarks.yml
index 641627c0ba..30603a0ea9 100644
--- a/.github/workflows/periodic_benchmarks.yml
+++ b/.github/workflows/periodic_benchmarks.yml
@@ -15,6 +15,9 @@ on:
# workflow manually
workflow_dispatch:
+env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
+
jobs:
benchmarks:
runs-on: ubuntu-latest
@@ -51,7 +54,7 @@ jobs:
LD_LIBRARY_PATH: $HOME/.local/lib
- name: Upload results as artifact
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: asv_periodic_results
path: results
diff --git a/.github/workflows/publish_pypi.yml b/.github/workflows/publish_pypi.yml
index 9ca277b653..944c900e9a 100644
--- a/.github/workflows/publish_pypi.yml
+++ b/.github/workflows/publish_pypi.yml
@@ -18,6 +18,7 @@ on:
# Set options available for all jobs that use cibuildwheel
env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
# Increase pip debugging output, equivalent to `pip -vv`
CIBW_BUILD_VERBOSITY: 2
# Disable build isolation to allow pre-installing build-time dependencies.
@@ -75,6 +76,7 @@ jobs:
run: pipx run cibuildwheel --output-dir wheelhouse
env:
CIBW_ENVIRONMENT: >
+ PYBAMM_DISABLE_TELEMETRY="true"
PYBAMM_USE_VCPKG=ON
VCPKG_ROOT_DIR=C:\vcpkg
VCPKG_DEFAULT_TRIPLET=x64-windows-static-md
@@ -92,7 +94,7 @@ jobs:
python -c "import pybamm; print(pybamm.IDAKLUSolver())"
python -m pytest -m cibw {project}/tests/unit
- name: Upload Windows wheels
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: wheels_windows
path: ./wheelhouse/*.whl
@@ -116,6 +118,8 @@ jobs:
- name: Build wheels on Linux
run: pipx run cibuildwheel --output-dir wheelhouse
env:
+ CIBW_ENVIRONMENT: >
+ PYBAMM_DISABLE_TELEMETRY="true"
CIBW_ARCHS_LINUX: x86_64
CIBW_BEFORE_ALL_LINUX: >
yum -y install openblas-devel lapack-devel &&
@@ -129,7 +133,7 @@ jobs:
python -m pytest -m cibw {project}/tests/unit
- name: Upload wheels for Linux
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: wheels_manylinux
path: ./wheelhouse/*.whl
@@ -242,7 +246,9 @@ jobs:
python scripts/install_KLU_Sundials.py
python -m cibuildwheel --output-dir wheelhouse
env:
- # 10.13 for Intel (macos-12/macos-13), 11.0 for Apple Silicon (macos-14 and macos-latest)
+ CIBW_ENVIRONMENT: >
+ PYBAMM_DISABLE_TELEMETRY="true"
+ # 10.13 for Intel (macos-13), 11.0 for Apple Silicon (macos-14 and macos-latest)
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.os == 'macos-14' && '11.0' || '10.13' }}
CIBW_ARCHS_MACOS: auto
CIBW_BEFORE_BUILD: python -m pip install cmake casadi setuptools wheel delocate
@@ -261,7 +267,7 @@ jobs:
python -m pytest -m cibw {project}/tests/unit
- name: Upload wheels for macOS (amd64, arm64)
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: wheels_${{ matrix.os }}
path: ./wheelhouse/*.whl
@@ -281,7 +287,7 @@ jobs:
run: pipx run build --sdist
- name: Upload SDist
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: sdist
path: ./dist/*.tar.gz
diff --git a/.github/workflows/release_reminder.yml b/.github/workflows/release_reminder.yml
deleted file mode 100644
index f838c8d57a..0000000000
--- a/.github/workflows/release_reminder.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Create a release reminder
-
-on:
- schedule:
- # Run at 10 am UTC on days-of-month 1 and 28 in January, May, and September.
- - cron: "0 10 1,28 1,5,9 *"
-
-permissions:
- contents: read
- issues: write
-
-jobs:
- remind:
- if: github.repository_owner == 'pybamm-team'
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: JasonEtco/create-an-issue@v2
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- filename: .github/release_reminder.md
diff --git a/.github/workflows/run_benchmarks_over_history.yml b/.github/workflows/run_benchmarks_over_history.yml
index d01564b210..28960fb4da 100644
--- a/.github/workflows/run_benchmarks_over_history.yml
+++ b/.github/workflows/run_benchmarks_over_history.yml
@@ -18,6 +18,10 @@ on:
ncommits:
description: "Number of commits to benchmark between commit_start and commit_end"
default: "100"
+
+env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
+
jobs:
benchmarks:
runs-on: ubuntu-latest
@@ -46,7 +50,7 @@ jobs:
${{ github.event.inputs.commit_start }}..${{ github.event.inputs.commit_end }}
- name: Upload results as artifact
- uses: actions/upload-artifact@v4.3.6
+ uses: actions/upload-artifact@v4.4.3
with:
name: asv_over_history_results
path: results
diff --git a/.github/workflows/run_periodic_tests.yml b/.github/workflows/run_periodic_tests.yml
index 9f10a9c6f7..bb164e9351 100644
--- a/.github/workflows/run_periodic_tests.yml
+++ b/.github/workflows/run_periodic_tests.yml
@@ -13,6 +13,7 @@ on:
- cron: "0 3 * * *"
env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
FORCE_COLOR: 3
PYBAMM_IDAKLU_EXPR_CASADI: ON
PYBAMM_IDAKLU_EXPR_IREE: ON
@@ -31,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ ubuntu-latest, macos-12, macos-14, windows-latest ]
+ os: [ ubuntu-latest, macos-13, macos-14, windows-latest ]
python-version: [ "3.9", "3.10", "3.11", "3.12" ]
name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
@@ -46,7 +47,7 @@ jobs:
sudo apt-get install gfortran gcc graphviz pandoc libopenblas-dev texlive-latex-extra dvipng
- name: Install macOS system dependencies
- if: matrix.os == 'macos-12' || matrix.os == 'macos-14'
+ if: matrix.os == 'macos-13' || matrix.os == 'macos-14'
env:
HOMEBREW_NO_INSTALL_CLEANUP: 1
HOMEBREW_NO_AUTO_UPDATE: 1
@@ -89,7 +90,7 @@ jobs:
- name: Upload coverage report
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v5.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 8b33553737..6d38fcadb6 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -59,7 +59,7 @@ jobs:
# 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@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6
+ uses: actions/upload-artifact@184d73b71b93c222403b2e7f1ffebe4508014249 # v4.4.1
with:
name: SARIF file
path: results.sarif
@@ -68,6 +68,6 @@ jobs:
# 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@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5
+ uses: github/codeql-action/upload-sarif@ea9e4e37992a54ee68a9622e985e60c8e8f12d9f # v3.27.4
with:
sarif_file: results.sarif
diff --git a/.github/workflows/test_on_push.yml b/.github/workflows/test_on_push.yml
index 9224b7df36..d54259bbd3 100644
--- a/.github/workflows/test_on_push.yml
+++ b/.github/workflows/test_on_push.yml
@@ -5,6 +5,7 @@ on:
pull_request:
env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
FORCE_COLOR: 3
PYBAMM_IDAKLU_EXPR_CASADI: ON
PYBAMM_IDAKLU_EXPR_IREE: ON
@@ -36,12 +37,11 @@ jobs:
pre-commit run -a
run_unit_integration_and_coverage_tests:
- needs: style
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
- os: [ubuntu-latest, macos-12, macos-14, windows-latest]
+ os: [ubuntu-latest, macos-13, macos-14, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12"]
name: Tests (${{ matrix.os }} / Python ${{ matrix.python-version }})
@@ -65,7 +65,7 @@ jobs:
sudo apt-get install libopenblas-dev texlive-latex-extra dvipng
- name: Install macOS system dependencies
- if: matrix.os == 'macos-12' || matrix.os == 'macos-14'
+ if: matrix.os == 'macos-13' || matrix.os == 'macos-14'
env:
HOMEBREW_NO_INSTALL_CLEANUP: 1
HOMEBREW_NO_AUTO_UPDATE: 1
@@ -123,7 +123,7 @@ jobs:
- name: Upload coverage report
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12'
- uses: codecov/codecov-action@v4.5.0
+ uses: codecov/codecov-action@v5.0.2
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -132,7 +132,6 @@ jobs:
# Skips IDAKLU module compilation for speedups, which is already tested in other jobs.
run_doctests:
- needs: style
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -177,7 +176,6 @@ jobs:
run: python -m nox -s docs
run_example_tests:
- needs: style
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -233,7 +231,6 @@ jobs:
run: python -m nox -s examples
run_scripts_tests:
- needs: style
runs-on: ubuntu-latest
strategy:
fail-fast: false
diff --git a/.github/workflows/work_precision_sets.yml b/.github/workflows/work_precision_sets.yml
index fafc5b1738..5810956786 100644
--- a/.github/workflows/work_precision_sets.yml
+++ b/.github/workflows/work_precision_sets.yml
@@ -5,6 +5,9 @@ on:
types: [published]
workflow_dispatch:
+env:
+ PYBAMM_DISABLE_TELEMETRY: "true"
+
jobs:
benchmarks_on_release:
if: github.repository_owner == 'pybamm-team'
@@ -27,7 +30,7 @@ jobs:
python benchmarks/work_precision_sets/time_vs_reltols.py
python benchmarks/work_precision_sets/time_vs_abstols.py
- name: Create Pull Request
- uses: peter-evans/create-pull-request@v6
+ uses: peter-evans/create-pull-request@v7
with:
delete-branch: true
branch-suffix: short-commit-hash
diff --git a/.gitignore b/.gitignore
index 42c76b7c55..8632f96d30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,7 +46,6 @@ input/*
# simulation outputs
out/
-config.py
matplotlibrc
*.pickle
*.sav
@@ -65,6 +64,7 @@ coverage.xml
htmlcov/
# virtual environment
+.venv
env/
venv/
venv3.5/
diff --git a/.lycheeignore b/.lycheeignore
index 55a4a4c623..929fe36475 100644
--- a/.lycheeignore
+++ b/.lycheeignore
@@ -15,3 +15,6 @@ file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/fundamentals/pybam
# Errors in docs/source/user_guide/index.md
file:///home/runner/work/PyBaMM/PyBaMM/docs/source/user_guide/api_docs
+
+# Telemetry
+https://us.i.posthog.com
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 43928bbc56..fa5a7336f3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,7 +4,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.6.3"
+ rev: "v0.7.4"
hooks:
- id: ruff
args: [--fix, --show-fixes]
@@ -13,13 +13,13 @@ repos:
types_or: [python, pyi, jupyter]
- repo: https://github.com/adamchainz/blacken-docs
- rev: "1.18.0"
+ rev: "1.19.1"
hooks:
- id: blacken-docs
additional_dependencies: [black==23.*]
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.6.0
+ rev: v5.0.0
hooks:
- id: check-added-large-files
- id: check-case-conflict
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5a959748f..b273435dff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,11 +1,54 @@
# [Unreleased](https://github.com/pybamm-team/PyBaMM/)
+# [v24.11.0](https://github.com/pybamm-team/PyBaMM/tree/v24.11.0) - 2024-11-20
+
+## Features
+
+- Added `CoupledVariable` which provides a placeholder variable whose equation can be elsewhere in the model. ([#4556](https://github.com/pybamm-team/PyBaMM/pull/4556))
+- Adds support to `pybamm.Experiment` for the `output_variables` option in the `IDAKLUSolver`. ([#4534](https://github.com/pybamm-team/PyBaMM/pull/4534))
+- Adds an option "voltage as a state" that can be "false" (default) or "true". If "true" adds an explicit algebraic equation for the voltage. ([#4507](https://github.com/pybamm-team/PyBaMM/pull/4507))
+- Improved `QuickPlot` accuracy for simulations with Hermite interpolation. ([#4483](https://github.com/pybamm-team/PyBaMM/pull/4483))
+- Added Hermite interpolation to the (`IDAKLUSolver`) that improves the accuracy and performance of post-processing variables. ([#4464](https://github.com/pybamm-team/PyBaMM/pull/4464))
+- Added basic telemetry to record which functions are being run. See [Telemetry section in the User Guide](https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry) for more information. ([#4441](https://github.com/pybamm-team/PyBaMM/pull/4441))
+- Added `BasicDFN` model for sodium-ion batteries ([#4451](https://github.com/pybamm-team/PyBaMM/pull/4451))
+- Added sensitivity calculation support for `pybamm.Simulation` and `pybamm.Experiment` ([#4415](https://github.com/pybamm-team/PyBaMM/pull/4415))
+- Added OpenMP parallelization to IDAKLU solver for lists of input parameters ([#4449](https://github.com/pybamm-team/PyBaMM/pull/4449))
+- Added phase-dependent particle options to LAM ([#4369](https://github.com/pybamm-team/PyBaMM/pull/4369))
+- Added a lithium ion equivalent circuit model with split open circuit voltages for each electrode (`SplitOCVR`). ([#4330](https://github.com/pybamm-team/PyBaMM/pull/4330))
+- Added the `pybamm.DiscreteTimeSum` expression node to sum an expression over a sequence of data times, and accompanying `pybamm.DiscreteTimeData` class to store the data times and values ([#4501](https://github.com/pybamm-team/PyBaMM/pull/4501))
+
+## Optimizations
+
+- Performance refactor of JAX BDF Solver with default Jax method set to `"BDF"`. ([#4456](https://github.com/pybamm-team/PyBaMM/pull/4456))
+- Improved performance of initialization and reinitialization of ODEs in the (`IDAKLUSolver`). ([#4453](https://github.com/pybamm-team/PyBaMM/pull/4453))
+- Removed the `start_step_offset` setting and disabled minimum `dt` warnings for drive cycles with the (`IDAKLUSolver`). ([#4416](https://github.com/pybamm-team/PyBaMM/pull/4416))
+
+## Bug Fixes
+- Added error for binary operators on two concatenations with different numbers of children. Previously, the extra children were dropped. Also fixed bug where Q_rxn was dropped from the total heating term in half-cell models. ([#4562](https://github.com/pybamm-team/PyBaMM/pull/4562))
+- Fixed bug where Q_rxn was set to 0 for the negative electrode in half-cell models. ([#4557](https://github.com/pybamm-team/PyBaMM/pull/4557))
+- Fixed bug in post-processing solutions with infeasible experiments using the (`IDAKLUSolver`). ([#4541](https://github.com/pybamm-team/PyBaMM/pull/4541))
+- Disabled IREE on MacOS due to compatibility issues and added the CasADI
+ path to the environment to resolve issues on MacOS and Linux. Windows
+ users may still experience issues with interpolation. ([#4528](https://github.com/pybamm-team/PyBaMM/pull/4528))
+- Added `_from_json()` functionality to `Sign` which was erroneously omitted previously. ([#4517](https://github.com/pybamm-team/PyBaMM/pull/4517))
+- Fixed bug where IDAKLU solver failed when `output variables` were specified and an extrapolation event is present. ([#4440](https://github.com/pybamm-team/PyBaMM/pull/4440))
+
+## Breaking changes
+
+- Deprecated `pybamm.Simulation.set_parameters` and `pybamm.Simulation. set_up_and_parameterise_experiment` functions in `pybamm.simulation.py`. ([#3752](https://github.com/pybamm-team/PyBaMM/pull/3752))
+- Removed all instances of `param = self.param` and now directly access `self.param` across the codebase. This change simplifies parameter references and enhances readability. ([#4484](https://github.com/pybamm-team/PyBaMM/pull/4494))
+- Removed the deprecation warning for the chemistry argument in
+ ParameterValues ([#4466](https://github.com/pybamm-team/PyBaMM/pull/4466))
+- The parameters "... electrode OCP entropic change [V.K-1]" and "... electrode volume change" are now expected to be functions of stoichiometry only instead of functions of both stoichiometry and maximum concentration ([#4427](https://github.com/pybamm-team/PyBaMM/pull/4427))
+- Renamed `set_events` function to `add_events_from` to better reflect its purpose. ([#4421](https://github.com/pybamm-team/PyBaMM/pull/4421))
+
# [v24.9.0](https://github.com/pybamm-team/PyBaMM/tree/v24.9.0) - 2024-09-03
## Features
- Added additional user-configurable options to the (`IDAKLUSolver`) and adjusted the default values to improve performance. ([#4282](https://github.com/pybamm-team/PyBaMM/pull/4282))
- Added the diffusion element to be used in the Thevenin model. ([#4254](https://github.com/pybamm-team/PyBaMM/pull/4254))
+- Added lumped surface thermal model ([#4203](https://github.com/pybamm-team/PyBaMM/pull/4203))
## Optimizations
diff --git a/CITATION.cff b/CITATION.cff
index d128cf485e..aee304dbde 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -24,6 +24,6 @@ keywords:
- "expression tree"
- "python"
- "symbolic differentiation"
-version: "24.9.0"
+version: "24.11.0"
repository-code: "https://github.com/pybamm-team/PyBaMM"
title: "Python Battery Mathematical Modelling (PyBaMM)"
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ad56ac34ca..ec594e5ca5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,7 +19,7 @@ endif()
project(idaklu)
-set(CMAKE_CXX_STANDARD 14)
+set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
@@ -82,6 +82,8 @@ pybind11_add_module(idaklu
src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp
src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.cpp
src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp
+ src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.cpp
+ src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.hpp
src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl
src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp
src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP_solvers.cpp
@@ -94,6 +96,8 @@ pybind11_add_module(idaklu
src/pybamm/solvers/c_solvers/idaklu/common.cpp
src/pybamm/solvers/c_solvers/idaklu/Solution.cpp
src/pybamm/solvers/c_solvers/idaklu/Solution.hpp
+ src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp
+ src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp
src/pybamm/solvers/c_solvers/idaklu/Options.hpp
src/pybamm/solvers/c_solvers/idaklu/Options.cpp
# IDAKLU expressions / function evaluation [abstract]
@@ -101,6 +105,8 @@ pybind11_add_module(idaklu
src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/Expression.hpp
src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionSet.hpp
src/pybamm/solvers/c_solvers/idaklu/Expressions/Base/ExpressionTypes.hpp
+ src/pybamm/solvers/c_solvers/idaklu/observe.hpp
+ src/pybamm/solvers/c_solvers/idaklu/observe.cpp
# IDAKLU expressions - concrete implementations
${IDAKLU_EXPR_CASADI_SOURCE_FILES}
${IDAKLU_EXPR_IREE_SOURCE_FILES}
@@ -138,6 +144,23 @@ set_target_properties(
INSTALL_RPATH_USE_LINK_PATH TRUE
)
+# openmp
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+ execute_process(
+ COMMAND "brew" "--prefix"
+ OUTPUT_VARIABLE HOMEBREW_PREFIX
+ OUTPUT_STRIP_TRAILING_WHITESPACE)
+ if (OpenMP_ROOT)
+ set(OpenMP_ROOT "${OpenMP_ROOT}:${HOMEBREW_PREFIX}/opt/libomp")
+ else()
+ set(OpenMP_ROOT "${HOMEBREW_PREFIX}/opt/libomp")
+ endif()
+endif()
+find_package(OpenMP)
+if(OpenMP_CXX_FOUND)
+ target_link_libraries(idaklu PRIVATE OpenMP::OpenMP_CXX)
+endif()
+
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR})
# Sundials
find_package(SUNDIALS REQUIRED)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 556a732518..eb510f7054 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -44,7 +44,7 @@ You now have everything you need to start making changes!
### B. Writing your code
-6. PyBaMM is developed in [Python](https://www.python.org)), and makes heavy use of [NumPy](https://numpy.org/) (see also [NumPy for MatLab users](https://numpy.org/doc/stable/user/numpy-for-matlab-users.html) and [Python for R users](https://www.rebeccabarter.com/blog/2023-09-11-from_r_to_python)).
+6. PyBaMM is developed in [Python](https://www.python.org), and makes heavy use of [NumPy](https://numpy.org/).
7. Make sure to follow our [coding style guidelines](#coding-style-guidelines).
8. Commit your changes to your branch with [useful, descriptive commit messages](https://chris.beams.io/posts/git-commit/): Remember these are
publicly visible and should still make sense a few months ahead in time.
@@ -116,8 +116,8 @@ PyBaMM provides a utility function `import_optional_dependency`, to check for th
Optional dependencies should never be imported at the module level, but always inside methods. For example:
-```
-def use_pybtex(x,y,z):
+```python
+def use_pybtex(x, y, z):
pybtex = import_optional_dependency("pybtex")
...
```
@@ -468,8 +468,8 @@ Editable notebooks are made available using [Google Colab](https://colab.researc
GitHub does some magic with particular filenames. In particular:
-- The first page people see when they go to [our GitHub page](https://github.com/pybamm-team/PyBaMM) displays the contents of [README.md](https://github.com/pybamm-team/PyBaMM/blob/develop/README.md), which is written in the [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) format. Some guidelines can be found [here](https://help.github.com/articles/about-readmes/).
-- The license for using PyBaMM is stored in [LICENSE](https://github.com/pybamm-team/PyBaMM/blob/develop/LICENSE.txt), and [automatically](https://help.github.com/articles/adding-a-license-to-a-repository/) linked to by GitHub.
+- The first page people see when they go to [our GitHub page](https://github.com/pybamm-team/PyBaMM) displays the contents of [README.md](https://github.com/pybamm-team/PyBaMM/blob/develop/README.md), which is written in the [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) format. Some guidelines can be found [here](https://docs.github.com/articles/about-readmes/).
+- The license for using PyBaMM is stored in [LICENSE](https://github.com/pybamm-team/PyBaMM/blob/develop/LICENSE.txt), and [automatically](https://docs.github.com/articles/adding-a-license-to-a-repository/) linked to by GitHub.
- This file, [CONTRIBUTING.md](https://github.com/pybamm-team/PyBaMM/blob/develop/CONTRIBUTING.md) is recognised as the contribution guidelines and a link is [automatically](https://github.com/blog/1184-contributing-guidelines) displayed when new issues or pull requests are created.
## Acknowledgements
diff --git a/README.md b/README.md
index a904e5a67c..2b5250d856 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@
[](https://scorecard.dev/viewer/?uri=github.com/pybamm-team/PyBaMM)
-[](#-contributors)
+[](#-contributors)
diff --git a/all_contributors.md b/all_contributors.md
index 9bb1e373d5..d4a41ba8e1 100644
--- a/all_contributors.md
+++ b/all_contributors.md
@@ -91,7 +91,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Agnik Bakshi 📖 |
 RuiheLi 💻 ⚠️ |
 chmabaur 🐛 💻 |
-  Abhishek Chaudhari 📖 💻 |
+  Abhishek Chaudhari 📖 💻 ⚠️ |
 Shubham Bhardwaj 🚇 |
 Jonathan Lauber 🚇 |
@@ -120,6 +120,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Ubham16 💻 |
 Mehrdad Babazadeh 💻 ⚠️ |
 Pip Liggins 💻 ⚠️ |
+  Medha Bhardwaj 💻 |
+
+
+  Marc Berliner 💻 📖 🚇 🚧 |
+  Aswinr24 ⚠️ |
diff --git a/conftest.py b/conftest.py
index 7ac6cf3c74..77513d56db 100644
--- a/conftest.py
+++ b/conftest.py
@@ -51,3 +51,8 @@ def set_random_seed():
@pytest.fixture(autouse=True)
def set_debug_value():
pybamm.settings.debug_mode = True
+
+
+@pytest.fixture(autouse=True)
+def disable_telemetry():
+ pybamm.telemetry.disable()
diff --git a/docs/conf.py b/docs/conf.py
index 55a4ac3f61..76dcffb18b 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -341,7 +341,7 @@
{% set github_docname =
'github/pybamm-team/pybamm/blob/develop/docs/' +
-env.doc2path(env.docname, base=None) %}
+env.doc2path(env.docname, base=None) | string() %}
{% set notebooks_version = env.config.html_context.notebooks_version %}
{% set github_download_url = env.config.html_context.github_download_url %}
diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst
index 33be0235a7..4667752157 100644
--- a/docs/source/api/index.rst
+++ b/docs/source/api/index.rst
@@ -9,10 +9,10 @@ API documentation
:Release: |version|
:Date: |today|
-This reference manual details functions, modules, and objects
-included in PyBaMM, describing what they are and what they do.
+This reference manual details the classes, functions, modules, and objects included in PyBaMM, describing what they are and what they do.
For a high-level introduction to PyBaMM, see the :ref:`user guide ` and the :ref:`examples `.
+
.. toctree::
:maxdepth: 2
diff --git a/docs/source/api/models/lithium_ion/ecm_split_ocv.rst b/docs/source/api/models/lithium_ion/ecm_split_ocv.rst
new file mode 100644
index 0000000000..a7d833cf55
--- /dev/null
+++ b/docs/source/api/models/lithium_ion/ecm_split_ocv.rst
@@ -0,0 +1,7 @@
+Equivalent Circuit Model with Split OCV (SplitOCVR)
+=====================================================
+
+.. autoclass:: pybamm.lithium_ion.SplitOCVR
+ :members:
+
+.. footbibliography::
diff --git a/docs/source/api/models/lithium_ion/index.rst b/docs/source/api/models/lithium_ion/index.rst
index 1a72c3c662..52efe44d6b 100644
--- a/docs/source/api/models/lithium_ion/index.rst
+++ b/docs/source/api/models/lithium_ion/index.rst
@@ -12,3 +12,4 @@ Lithium-ion Models
msmr
yang2017
electrode_soh
+ ecm_split_ocv
diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst
index 824ec6126d..9cf8d09470 100644
--- a/docs/source/api/util.rst
+++ b/docs/source/api/util.rst
@@ -19,3 +19,5 @@ Utility functions
.. autofunction:: pybamm.has_jax
.. autofunction:: pybamm.is_jax_compatible
+
+.. autofunction:: pybamm.set_logging_level
diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst
index a5958b327b..6ddaf5867e 100644
--- a/docs/source/examples/index.rst
+++ b/docs/source/examples/index.rst
@@ -54,6 +54,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries
notebooks/models/DFN-with-particle-size-distributions.ipynb
notebooks/models/DFN.ipynb
notebooks/models/electrode-state-of-health.ipynb
+ notebooks/models/graded-electrodes.ipynb
notebooks/models/half-cell.ipynb
notebooks/models/jelly-roll-model.ipynb
notebooks/models/latexify.ipynb
@@ -67,6 +68,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries
notebooks/models/SEI-on-cracks.ipynb
notebooks/models/simulate-3E-cell.ipynb
notebooks/models/simulating-ORegan-2022-parameter-set.ipynb
+ notebooks/models/sodium-ion.ipynb
notebooks/models/SPM.ipynb
notebooks/models/SPMe.ipynb
notebooks/models/submodel_cracking_DFN_or_SPM.ipynb
@@ -85,6 +87,7 @@ The notebooks are organised into subfolders, and can be viewed in the galleries
notebooks/parameterization/change-input-current.ipynb
notebooks/parameterization/parameter-values.ipynb
notebooks/parameterization/parameterization.ipynb
+ notebooks/parameterization/sensitivities_and_data_fitting.ipynb
.. nbgallery::
:caption: Simulations and Experiments
diff --git a/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb b/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb
index a35a81932f..02206d4210 100644
--- a/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb
+++ b/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb
@@ -25,18 +25,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "\n",
- "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip is available: \u001B[0m\u001B[31;49m23.3.1\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m24.0\u001B[0m\n",
- "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\n",
"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": [
@@ -74,7 +64,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "The parameter values are stored in a dictionary"
+ "The parameter values are stored in a dictionary-like object of class [`pybamm.ParameterValues`](https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html). "
]
},
{
@@ -98,8 +88,8 @@
" 'EC initial concentration in electrolyte [mol.m-3]': 4541.0,\n",
" 'Electrode height [m]': 0.065,\n",
" 'Electrode width [m]': 1.58,\n",
- " 'Electrolyte conductivity [S.m-1]': ,\n",
- " 'Electrolyte diffusivity [m2.s-1]': ,\n",
+ " 'Electrolyte conductivity [S.m-1]': ,\n",
+ " 'Electrolyte diffusivity [m2.s-1]': ,\n",
" 'Electron charge [C]': 1.602176634e-19,\n",
" 'Faraday constant [C.mol-1]': 96485.33212,\n",
" 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n",
@@ -125,14 +115,14 @@
" 'Negative current collector thickness [m]': 1.2e-05,\n",
" 'Negative electrode Bruggeman coefficient (electrode)': 0,\n",
" 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n",
- " 'Negative electrode OCP [V]': ,\n",
+ " 'Negative electrode OCP [V]': ,\n",
" 'Negative electrode OCP entropic change [V.K-1]': 0.0,\n",
" 'Negative electrode active material volume fraction': 0.75,\n",
" 'Negative electrode charge transfer coefficient': 0.5,\n",
" 'Negative electrode conductivity [S.m-1]': 215.0,\n",
" 'Negative electrode density [kg.m-3]': 1657.0,\n",
" 'Negative electrode double-layer capacity [F.m-2]': 0.2,\n",
- " 'Negative electrode exchange-current density [A.m-2]': ,\n",
+ " 'Negative electrode exchange-current density [A.m-2]': ,\n",
" 'Negative electrode porosity': 0.25,\n",
" 'Negative electrode reaction-driven LAM factor [m3.mol-1]': 0.0,\n",
" 'Negative electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n",
@@ -155,14 +145,14 @@
" 'Positive current collector thickness [m]': 1.6e-05,\n",
" 'Positive electrode Bruggeman coefficient (electrode)': 0,\n",
" 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n",
- " 'Positive electrode OCP [V]': ,\n",
+ " 'Positive electrode OCP [V]': ,\n",
" 'Positive electrode OCP entropic change [V.K-1]': 0.0,\n",
" 'Positive electrode active material volume fraction': 0.665,\n",
" 'Positive electrode charge transfer coefficient': 0.5,\n",
" 'Positive electrode conductivity [S.m-1]': 0.18,\n",
" 'Positive electrode density [kg.m-3]': 3262.0,\n",
" 'Positive electrode double-layer capacity [F.m-2]': 0.2,\n",
- " 'Positive electrode exchange-current density [A.m-2]': ,\n",
+ " 'Positive electrode exchange-current density [A.m-2]': ,\n",
" 'Positive electrode porosity': 0.335,\n",
" 'Positive electrode reaction-driven LAM factor [m3.mol-1]': 0.0,\n",
" 'Positive electrode specific heat capacity [J.kg-1.K-1]': 700.0,\n",
@@ -243,8 +233,8 @@
"output_type": "stream",
"text": [
"EC initial concentration in electrolyte [mol.m-3]\t4541.0\n",
- "Electrolyte conductivity [S.m-1]\t\n",
- "Electrolyte diffusivity [m2.s-1]\t\n",
+ "Electrolyte conductivity [S.m-1]\t\n",
+ "Electrolyte diffusivity [m2.s-1]\t\n",
"Initial concentration in electrolyte [mol.m-3]\t1000.0\n",
"Negative electrode Bruggeman coefficient (electrolyte)\t1.5\n",
"Positive electrode Bruggeman coefficient (electrolyte)\t1.5\n",
@@ -274,12 +264,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "2ac62159d85445f0b021b8800750726f",
+ "model_id": "5dd5facebda342afa83dca4f0838788c",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=3555.448018330181, step=35.55448018330181), …"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=3555.448018679505, step=35.55448018679505), …"
]
},
"metadata": {},
@@ -288,7 +278,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 6,
@@ -324,55 +314,58 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "| Parameter | Type of parameter |\n",
- "| ========================================================= | =========================================================================================================================================================================================================== |\n",
- "| Maximum concentration in positive electrode [mol.m-3] | Parameter |\n",
- "| Maximum concentration in negative electrode [mol.m-3] | Parameter |\n",
- "| Nominal cell capacity [A.h] | Parameter |\n",
- "| Electrode width [m] | Parameter |\n",
- "| Positive electrode Bruggeman coefficient (electrode) | Parameter |\n",
- "| Faraday constant [C.mol-1] | Parameter |\n",
- "| Number of electrodes connected in parallel to make a cell | Parameter |\n",
- "| Negative electrode Bruggeman coefficient (electrode) | Parameter |\n",
- "| Initial concentration in electrolyte [mol.m-3] | Parameter |\n",
- "| Electrode height [m] | Parameter |\n",
- "| Lower voltage cut-off [V] | Parameter |\n",
- "| Upper voltage cut-off [V] | Parameter |\n",
- "| Negative electrode Bruggeman coefficient (electrolyte) | Parameter |\n",
- "| Separator Bruggeman coefficient (electrolyte) | Parameter |\n",
- "| Number of cells connected in series to make a battery | Parameter |\n",
- "| Ideal gas constant [J.K-1.mol-1] | Parameter |\n",
- "| Positive electrode thickness [m] | Parameter |\n",
- "| Reference temperature [K] | Parameter |\n",
- "| Initial temperature [K] | Parameter |\n",
- "| Positive electrode Bruggeman coefficient (electrolyte) | Parameter |\n",
- "| Negative electrode thickness [m] | Parameter |\n",
- "| Separator thickness [m] | Parameter |\n",
- "| Electrolyte conductivity [S.m-1] | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Positive electrode OCP [V] | FunctionParameter with inputs(s) 'Positive particle stoichiometry' |\n",
- "| Negative particle radius [m] | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Positive electrode OCP entropic change [V.K-1] | FunctionParameter with inputs(s) 'Positive particle stoichiometry', 'Maximum positive particle surface concentration [mol.m-3]' |\n",
- "| Negative electrode porosity | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Positive particle radius [m] | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Positive electrode active material volume fraction | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Ambient temperature [K] | FunctionParameter with inputs(s) 'Distance across electrode width [m]', 'Distance across electrode height [m]', 'Time [s]' |\n",
- "| Initial concentration in positive electrode [mol.m-3] | FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]' |\n",
- "| Cation transference number | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Negative electrode OCP [V] | FunctionParameter with inputs(s) 'Negative particle stoichiometry' |\n",
- "| Negative particle diffusivity [m2.s-1] | FunctionParameter with inputs(s) 'Negative particle stoichiometry', 'Temperature [K]' |\n",
- "| Thermodynamic factor | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Positive electrode exchange-current density [A.m-2] | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Maximum positive particle surface concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Negative electrode active material volume fraction | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Positive particle diffusivity [m2.s-1] | FunctionParameter with inputs(s) 'Positive particle stoichiometry', 'Temperature [K]' |\n",
- "| Positive electrode porosity | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Positive electrode conductivity [S.m-1] | FunctionParameter with inputs(s) 'Temperature [K]' |\n",
- "| Initial concentration in negative electrode [mol.m-3] | FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]' |\n",
- "| Negative electrode OCP entropic change [V.K-1] | FunctionParameter with inputs(s) 'Negative particle stoichiometry', 'Maximum negative particle surface concentration [mol.m-3]' |\n",
- "| Current function [A] | FunctionParameter with inputs(s) 'Time [s]' |\n",
- "| Electrolyte diffusivity [m2.s-1] | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Separator porosity | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' |\n",
- "| Negative electrode exchange-current density [A.m-2] | FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Negative particle surface concentration [mol.m-3]', 'Maximum negative particle surface concentration [mol.m-3]', 'Temperature [K]' |\n",
- "| Negative electrode conductivity [S.m-1] | FunctionParameter with inputs(s) 'Temperature [K]' |\n"
+ "┌───────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐\n",
+ "│ Parameter │ Type of parameter │\n",
+ "├───────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤\n",
+ "│ Positive electrode Bruggeman coefficient (electrode) │ Parameter │\n",
+ "│ Faraday constant [C.mol-1] │ Parameter │\n",
+ "│ Separator Bruggeman coefficient (electrolyte) │ Parameter │\n",
+ "│ Reference temperature [K] │ Parameter │\n",
+ "│ Upper voltage cut-off [V] │ Parameter │\n",
+ "│ Lower voltage cut-off [V] │ Parameter │\n",
+ "│ Negative electrode thickness [m] │ Parameter │\n",
+ "│ Initial concentration in electrolyte [mol.m-3] │ Parameter │\n",
+ "│ Nominal cell capacity [A.h] │ Parameter │\n",
+ "│ Number of electrodes connected in parallel to make a cell │ Parameter │\n",
+ "│ Negative electrode Bruggeman coefficient (electrolyte) │ Parameter │\n",
+ "│ Separator thickness [m] │ Parameter │\n",
+ "│ Initial temperature [K] │ Parameter │\n",
+ "│ Maximum concentration in negative electrode [mol.m-3] │ Parameter │\n",
+ "│ Positive electrode Bruggeman coefficient (electrolyte) │ Parameter │\n",
+ "│ Positive electrode thickness [m] │ Parameter │\n",
+ "│ Ideal gas constant [J.K-1.mol-1] │ Parameter │\n",
+ "│ Maximum concentration in positive electrode [mol.m-3] │ Parameter │\n",
+ "│ Electrode height [m] │ Parameter │\n",
+ "│ Electrode width [m] │ Parameter │\n",
+ "│ Negative electrode Bruggeman coefficient (electrode) │ Parameter │\n",
+ "│ Number of cells connected in series to make a battery │ Parameter │\n",
+ "│ Negative electrode porosity │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Positive particle radius [m] │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Positive electrode OCP [V] │ FunctionParameter with inputs(s) 'Positive particle stoichiometry' │\n",
+ "│ Negative electrode OCP entropic change [V.K-1] │ FunctionParameter with inputs(s) 'Negative particle stoichiometry' │\n",
+ "│ Initial concentration in positive electrode [mol.m-3] │ FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]' │\n",
+ "│ Positive electrode conductivity [S.m-1] │ FunctionParameter with inputs(s) 'Temperature [K]' │\n",
+ "│ Negative electrode active material volume fraction │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Negative particle diffusivity [m2.s-1] │ FunctionParameter with inputs(s) 'Negative particle stoichiometry', 'Temperature [K]' │\n",
+ "│ Initial concentration in negative electrode [mol.m-3] │ FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]' │\n",
+ "│ Positive electrode porosity │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Positive electrode OCP entropic change [V.K-1] │ FunctionParameter with inputs(s) 'Positive particle stoichiometry' │\n",
+ "│ Electrolyte conductivity [S.m-1] │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Thermodynamic factor │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Electrolyte diffusivity [m2.s-1] │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Negative particle radius [m] │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Negative electrode OCP [V] │ FunctionParameter with inputs(s) 'Negative particle stoichiometry' │\n",
+ "│ Cation transference number │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Ambient temperature [K] │ FunctionParameter with inputs(s) 'Distance across electrode width [m]', 'Distance across electrode height [m]', 'Time [s]' │\n",
+ "│ Current function [A] │ FunctionParameter with inputs(s) 'Time [s]' │\n",
+ "│ Negative electrode exchange-current density [A.m-2] │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Negative particle surface concentration [mol.m-3]', 'Maximum negative particle surface concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Negative electrode conductivity [S.m-1] │ FunctionParameter with inputs(s) 'Temperature [K]' │\n",
+ "│ Positive electrode exchange-current density [A.m-2] │ FunctionParameter with inputs(s) 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Maximum positive particle surface concentration [mol.m-3]', 'Temperature [K]' │\n",
+ "│ Separator porosity │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Positive electrode active material volume fraction │ FunctionParameter with inputs(s) 'Through-cell distance (x) [m]' │\n",
+ "│ Positive particle diffusivity [m2.s-1] │ FunctionParameter with inputs(s) 'Positive particle stoichiometry', 'Temperature [K]' │\n",
+ "└───────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\n",
+ "\n"
]
}
],
@@ -424,12 +417,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "29a3805ee040456bbe863a52cc423492",
+ "model_id": "48c0f7150c154399b1d56dadd90a41ad",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=1703.071841649571, step=17.03071841649571), …"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=1703.0716533945217, step=17.030716533945217)…"
]
},
"metadata": {},
@@ -438,7 +431,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 9,
@@ -510,7 +503,7 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "f362d8ff79bc4b868f59470d58fdd9c6",
+ "model_id": "b8992b55090149ea932deb091190b655",
"version_major": 2,
"version_minor": 0
},
@@ -524,7 +517,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 11,
@@ -539,6 +532,49 @@
"sim.plot([\"Current [A]\", \"Voltage [V]\"])"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Input parameters\n",
+ "\n",
+ "If the value of a parameter is expected to change often (e.g. running a parameter sweep) is is more convenient to set a parameter as an \"input parameter\". This is a placeholder that can be filled in with a numerical value when the model is solved.\n",
+ "\n",
+ "To set a parameter as an input parameter, we can set its value to the string `[input]` in the parameter values dictionary. For example, we can set the `Current function [A]` to be an input parameter and then run a parameter sweep over different current values like so:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "parameter_values[\"Current function [A]\"] = \"[input]\"\n",
+ "sim = pybamm.Simulation(model, parameter_values=parameter_values)\n",
+ "solns = []\n",
+ "for c in [0.1, 0.2, 0.3]:\n",
+ " soln = sim.solve([0, 3600], inputs={\"Current function [A]\": c})\n",
+ " plt.plot(soln[\"Time [s]\"].entries, soln[\"Voltage [V]\"].entries, label=f\"{c} A\")\n",
+ " solns.append(soln[\"Terminal voltage [V]\"].entries)\n",
+ "plt.xlabel(\"Time [s]\")\n",
+ "plt.ylabel(\"Terminal voltage [V]\")\n",
+ "plt.legend()\n",
+ "plt.show()"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -550,7 +586,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
@@ -596,7 +632,7 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 14,
"metadata": {},
"outputs": [
{
@@ -604,10 +640,11 @@
"output_type": "stream",
"text": [
"[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n",
- "[2] Chang-Hui Chen, Ferran Brosa Planella, Kieran O'Regan, Dominika Gastol, W. Dhammika Widanage, and Emma Kendrick. Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n",
- "[3] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n",
- "[4] 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",
- "[5] 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",
+ "[2] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n",
+ "[3] Chang-Hui Chen, Ferran Brosa Planella, Kieran O'Regan, Dominika Gastol, W. Dhammika Widanage, and Emma Kendrick. Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n",
+ "[4] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n",
+ "[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] 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"
]
}
@@ -619,7 +656,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "pybamm",
+ "display_name": "env",
"language": "python",
"name": "python3"
},
@@ -633,7 +670,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.6"
+ "version": "3.10.12"
},
"toc": {
"base_numbering": 1,
@@ -647,11 +684,6 @@
"toc_position": {},
"toc_section_display": true,
"toc_window_display": true
- },
- "vscode": {
- "interpreter": {
- "hash": "1a781583db2df3c2e87436f6d22cce842c2e50a5670da93a3bd820b97dc43011"
- }
}
},
"nbformat": 4,
diff --git a/docs/source/examples/notebooks/models/graded-electrodes.ipynb b/docs/source/examples/notebooks/models/graded-electrodes.ipynb
new file mode 100644
index 0000000000..adb316ba2d
--- /dev/null
+++ b/docs/source/examples/notebooks/models/graded-electrodes.ipynb
@@ -0,0 +1,277 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Simulating graded electrodes\n",
+ "\n",
+ "In this notebook we explore how to simulate the effect of graded electrodes in the performance of a battery. Graded electrodes have a composition that varies along the thickness of the electrode, typically active material volume fraction and particle size. This variation can be used to improve the performance of the battery, for example, by increasing the power density.\n",
+ "\n",
+ "As usual, we start by importing PyBaMM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "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": [
+ "%pip install \"pybamm[plot,cite]\" -q # install PyBaMM if it is not installed\n",
+ "import pybamm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We use the DFN model for the simulations and the Chen2020 parameter set. Note that we will need to modify the default Chen2020 parameter set to describe graded electrodes."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model = pybamm.lithium_ion.DFN()\n",
+ "parameter_values = pybamm.ParameterValues(\"Chen2020\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "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."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "L_n = parameter_values[\"Negative electrode thickness [m]\"]\n",
+ "L_s = parameter_values[\"Separator thickness [m]\"]\n",
+ "L_p = parameter_values[\"Positive electrode thickness [m]\"]\n",
+ "\n",
+ "eps_n_0 = parameter_values[\"Negative electrode porosity\"]\n",
+ "eps_p_0 = parameter_values[\"Positive electrode porosity\"]\n",
+ "\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",
+ "]\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",
+ "]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Note that the distance through the electrode is computed from the negative electrode, so parameter need to be defined accordingly. Next, we can just solve the models for the various parameter sets. We apply a fairly high C-rate to see the effect of the graded electrodes on the discharge capacity."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "solutions = []\n",
+ "\n",
+ "experiment = pybamm.Experiment([\"Discharge at 3C until 2.5 V\"])\n",
+ "\n",
+ "for eps_n, eps_p in zip(eps_ns, eps_ps):\n",
+ " parameter_values[\"Negative electrode porosity\"] = eps_n\n",
+ " parameter_values[\"Positive electrode porosity\"] = eps_p\n",
+ " sim = pybamm.Simulation(\n",
+ " model,\n",
+ " parameter_values=parameter_values,\n",
+ " experiment=experiment,\n",
+ " solver=pybamm.IDAKLUSolver(),\n",
+ " )\n",
+ " sol = sim.solve()\n",
+ " solutions.append(sol)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "And plot the results:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "99f847ca09da40cba550dd02dd8281a2",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9136958613059, step=6.7391369586130585),…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "pybamm.dynamic_plot(\n",
+ " solutions,\n",
+ " labels=[\n",
+ " \"Constant porosity\",\n",
+ " \"Low porosity at separator\",\n",
+ " \"High porosity at separator\",\n",
+ " ],\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We observe that, even though the average porosity is the same for the three cases the discharge capacity is much higher with the graded electrode where porosity is higher near the separator. This is because the higher porosity near the separator facilitates the ion transport and the better utilisation of the active material.\n",
+ "\n",
+ "As a sanity check we can plot the porosity profiles for the three cases and see they match what we intended."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "829b68c6b3e04e0ebe5537daabec2278",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=673.9136958613059, step=6.7391369586130585),…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "pybamm.dynamic_plot(\n",
+ " solutions,\n",
+ " output_variables=[\"Negative electrode porosity\", \"Positive electrode porosity\"],\n",
+ " labels=[\n",
+ " \"Constant porosity\",\n",
+ " \"Low porosity at separator\",\n",
+ " \"High porosity at separator\",\n",
+ " ],\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n",
+ "[2] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n",
+ "[3] Chang-Hui Chen, Ferran Brosa Planella, Kieran O'Regan, Dominika Gastol, W. Dhammika Widanage, and Emma Kendrick. Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n",
+ "[4] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n",
+ "[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",
+ "\n"
+ ]
+ }
+ ],
+ "source": [
+ "pybamm.print_citations()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb b/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb
index ad428e6791..1ce1cca826 100644
--- a/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb
+++ b/docs/source/examples/notebooks/models/loss_of_active_materials.ipynb
@@ -30,7 +30,7 @@
"output_type": "stream",
"text": [
"At t = 57.3387, , mxstep steps taken before reaching tout.\n",
- "At t = 57.3387 and h = 7.05477e-15, the corrector convergence failed repeatedly or with |h| = hmin.\n",
+ "At t = 57.3387, , mxstep steps taken before reaching tout.\n",
"At t = 57.3387, , mxstep steps taken before reaching tout.\n",
"At t = 57.3387, , mxstep steps taken before reaching tout.\n"
]
@@ -83,12 +83,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "6b19474c3912495eb75217e009760637",
+ "model_id": "ccfc7ae873d1492197fa7b554339a3d7",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=2.329196798170269, step=0.02329196798170269)…"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=2.3291967981693755, step=0.02329196798169375…"
]
},
"metadata": {},
@@ -97,7 +97,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 2,
@@ -137,11 +137,11 @@
"output_type": "stream",
"text": [
"At t = 57.3387, , mxstep steps taken before reaching tout.\n",
- "At t = 57.3387 and h = 7.05477e-15, the corrector convergence failed repeatedly or with |h| = hmin.\n",
+ "At t = 57.3387, , mxstep steps taken before reaching tout.\n",
"At t = 57.3387, , mxstep steps taken before reaching tout.\n",
"At t = 57.3387, , mxstep steps taken before reaching tout.\n",
"At t = 57.3307, , mxstep steps taken before reaching tout.\n",
- "At t = 57.3307, , mxstep steps taken before reaching tout.\n",
+ "At t = 57.3307 and h = 3.45325e-14, the corrector convergence failed repeatedly or with |h| = hmin.\n",
"At t = 57.3307, , mxstep steps taken before reaching tout.\n",
"At t = 57.3307, , mxstep steps taken before reaching tout.\n",
"At t = 57.2504, , mxstep steps taken before reaching tout.\n",
@@ -153,12 +153,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "789a681c8c574bb8b3d3016a844dd9a2",
+ "model_id": "b34472112ae344da92ccc8af5178c64b",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=2.329196798170269, step=0.02329196798170269)…"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=2.3291967981693755, step=0.02329196798169375…"
]
},
"metadata": {},
@@ -167,7 +167,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 3,
@@ -225,12 +225,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "ad36439975754b29bbbef1bd94379408",
+ "model_id": "60db1d0de494460493cc8edd5b61d4e7",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8531298311682403, step=0.01853129831168240…"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=1.85353350947348, step=0.0185353350947348), …"
]
},
"metadata": {},
@@ -239,7 +239,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 4,
@@ -248,6 +248,16 @@
}
],
"source": [
+ "import pybamm\n",
+ "\n",
+ "experiment = pybamm.Experiment(\n",
+ " [\n",
+ " \"Discharge at 1C until 3 V\",\n",
+ " \"Rest for 600 seconds\",\n",
+ " \"Charge at 1C until 4.2 V\",\n",
+ " \"Hold at 4.199 V for 600 seconds\",\n",
+ " ]\n",
+ ")\n",
"model = pybamm.lithium_ion.DFN(\n",
" options={\n",
" \"SEI\": \"solvent-diffusion limited\",\n",
@@ -255,7 +265,7 @@
" }\n",
")\n",
"param = pybamm.ParameterValues(\"Chen2020\")\n",
- "param.update({\"Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-3})\n",
+ "param.update({\"Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-4})\n",
"sim = pybamm.Simulation(\n",
" model,\n",
" experiment=experiment,\n",
@@ -300,12 +310,12 @@
{
"data": {
"application/vnd.jupyter.widget-view+json": {
- "model_id": "91ea043e10d342049929095e48e98c5e",
+ "model_id": "1dfb1de5ccde449c9eefcda1b1f44468",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
- "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8506629989989005, step=0.01850662998998900…"
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=1.8506629988943608, step=0.01850662998894360…"
]
},
"metadata": {},
@@ -314,7 +324,7 @@
{
"data": {
"text/plain": [
- ""
+ ""
]
},
"execution_count": 5,
@@ -358,6 +368,297 @@
")"
]
},
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## LAM with composite electrode\n",
+ "The LAM submodel is also compatible with multiple phases within an electrode for both stress- and reaction-driven loss of active material. Currently, there is no single parameter set that combines both LAM degradation and composite materials. The following examples use the Chen2020 composite parameter set with LAM parameters taken from the Ai2020 parameter set. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Volume change functions from Ai2020 parameters\n",
+ "\n",
+ "\n",
+ "def graphite_volume_change_Ai2020(sto):\n",
+ " p1 = 145.907\n",
+ " p2 = -681.229\n",
+ " p3 = 1334.442\n",
+ " p4 = -1415.710\n",
+ " p5 = 873.906\n",
+ " p6 = -312.528\n",
+ " p7 = 60.641\n",
+ " p8 = -5.706\n",
+ " p9 = 0.386\n",
+ " p10 = -4.966e-05\n",
+ " t_change = (\n",
+ " p1 * sto**9\n",
+ " + p2 * sto**8\n",
+ " + p3 * sto**7\n",
+ " + p4 * sto**6\n",
+ " + p5 * sto**5\n",
+ " + p6 * sto**4\n",
+ " + p7 * sto**3\n",
+ " + p8 * sto**2\n",
+ " + p9 * sto\n",
+ " + p10\n",
+ " )\n",
+ " return t_change\n",
+ "\n",
+ "\n",
+ "def lico2_volume_change_Ai2020(sto):\n",
+ " omega = pybamm.Parameter(\"Positive electrode partial molar volume [m3.mol-1]\")\n",
+ " c_s_max = pybamm.Parameter(\"Maximum concentration in positive electrode [mol.m-3]\")\n",
+ " t_change = omega * c_s_max * sto\n",
+ " return t_change"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Stress-driven composite anode\n",
+ "The secondary phase LAM parameters have been adjusted from the Ai2020 by about 10% to show less degradation in that phase. The model is set up in the same way the single-phase simulation is but with additional parameters."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "options = {\n",
+ " \"particle phases\": (\"2\", \"1\"),\n",
+ " \"open-circuit potential\": ((\"single\", \"current sigmoid\"), \"single\"),\n",
+ " \"loss of active material\": \"stress-driven\",\n",
+ "}\n",
+ "\n",
+ "model = pybamm.lithium_ion.SPM(options)\n",
+ "parameter_values = pybamm.ParameterValues(\"Chen2020_composite\")\n",
+ "second = 0.1\n",
+ "parameter_values.update(\n",
+ " {\n",
+ " \"Primary: Negative electrode LAM constant proportional term [s-1]\": 1e-4 / 3600,\n",
+ " \"Secondary: Negative electrode LAM constant proportional term [s-1]\": 1e-4\n",
+ " / 3600\n",
+ " * second,\n",
+ " \"Positive electrode LAM constant proportional term [s-1]\": 1e-4 / 3600,\n",
+ " \"Primary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n",
+ " \"Primary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n",
+ " \"Primary: Negative electrode Poisson's ratio\": 0.3,\n",
+ " \"Primary: Negative electrode critical stress [Pa]\": 60000000.0,\n",
+ " \"Secondary: Negative electrode critical stress [Pa]\": 60000000.0,\n",
+ " \"Primary: Negative electrode LAM constant exponential term\": 2.0,\n",
+ " \"Secondary: Negative electrode LAM constant exponential term\": 2.0,\n",
+ " \"Secondary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06\n",
+ " * second,\n",
+ " \"Secondary: Negative electrode Young's modulus [Pa]\": 15000000000.0 * second,\n",
+ " \"Secondary: Negative electrode Poisson's ratio\": 0.3 * second,\n",
+ " \"Negative electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n",
+ " \"Primary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n",
+ " \"Secondary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n",
+ " \"Positive electrode partial molar volume [m3.mol-1]\": -7.28e-07,\n",
+ " \"Positive electrode Young's modulus [Pa]\": 375000000000.0,\n",
+ " \"Positive electrode Poisson's ratio\": 0.2,\n",
+ " \"Positive electrode critical stress [Pa]\": 375000000.0,\n",
+ " \"Positive electrode LAM constant exponential term\": 2.0,\n",
+ " \"Positive electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n",
+ " \"Positive electrode volume change\": lico2_volume_change_Ai2020,\n",
+ " },\n",
+ " check_already_exists=False,\n",
+ ")\n",
+ "\n",
+ "# sim = pybamm.Simulation(model, parameter_values=parameter_values)\n",
+ "# sim.solve([0, 4500])\n",
+ "experiment = pybamm.Experiment(\n",
+ " [\n",
+ " \"Discharge at 1C until 3 V\",\n",
+ " \"Rest for 600 seconds\",\n",
+ " \"Charge at 1C until 4.2 V\",\n",
+ " \"Hold at 4.199 V for 600 seconds\",\n",
+ " ]\n",
+ ")\n",
+ "sim = pybamm.Simulation(\n",
+ " model,\n",
+ " experiment=experiment,\n",
+ " parameter_values=parameter_values,\n",
+ " discretisation_kwargs={\"remove_independent_variables_from_rhs\": True},\n",
+ ")\n",
+ "solution = sim.solve(calc_esoh=False)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The two phase LAM model can be compared between the cathode and two anode phases."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "074bcadceb3e4fbd8cc786e798bb6508",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=2.1702864080208446, step=0.02170286408020844…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 8,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "pybamm.dynamic_plot(\n",
+ " sim,\n",
+ " [\n",
+ " \"Voltage [V]\",\n",
+ " \"Current [A]\",\n",
+ " [\n",
+ " \"Average negative primary particle concentration\",\n",
+ " \"Average negative secondary particle concentration\",\n",
+ " \"Average positive particle concentration\",\n",
+ " ],\n",
+ " \"X-averaged negative electrode primary active material volume fraction\",\n",
+ " \"X-averaged positive electrode active material volume fraction\",\n",
+ " \"X-averaged negative electrode secondary active material volume fraction\",\n",
+ " \"Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]\",\n",
+ " \"Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]\",\n",
+ " \"X-averaged positive particle surface tangential stress [Pa]\",\n",
+ " \"X-averaged negative primary particle surface tangential stress [Pa]\",\n",
+ " \"X-averaged negative secondary particle surface tangential stress [Pa]\",\n",
+ " ],\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Reaction-driven composite anode\n",
+ "The same process is repeated for the reaction-driven LAM degradation."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "98a2b1762a3c43bcaa9ceff5a146d704",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=2.081773444877257, step=0.02081773444877257)…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "options = {\n",
+ " \"particle phases\": (\"2\", \"1\"),\n",
+ " \"open-circuit potential\": ((\"single\", \"current sigmoid\"), \"single\"),\n",
+ " \"SEI\": \"solvent-diffusion limited\",\n",
+ " \"loss of active material\": \"reaction-driven\",\n",
+ "}\n",
+ "\n",
+ "model = pybamm.lithium_ion.SPM(options)\n",
+ "parameter_values = pybamm.ParameterValues(\"Chen2020_composite\")\n",
+ "second = 0.9\n",
+ "\n",
+ "parameter_values.update(\n",
+ " {\n",
+ " \"Primary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06,\n",
+ " \"Primary: Negative electrode Young's modulus [Pa]\": 15000000000.0,\n",
+ " \"Primary: Negative electrode Poisson's ratio\": 0.3,\n",
+ " \"Negative electrode critical stress [Pa]\": 60000000.0,\n",
+ " \"Negative electrode LAM constant exponential term\": 2.0,\n",
+ " \"Secondary: Negative electrode partial molar volume [m3.mol-1]\": 3.1e-06\n",
+ " * second,\n",
+ " \"Secondary: Negative electrode Young's modulus [Pa]\": 15000000000.0 * second,\n",
+ " \"Secondary: Negative electrode Poisson's ratio\": 0.3 * second,\n",
+ " \"Negative electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n",
+ " \"Primary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n",
+ " \"Secondary: Negative electrode volume change\": graphite_volume_change_Ai2020,\n",
+ " \"Positive electrode partial molar volume [m3.mol-1]\": -7.28e-07,\n",
+ " \"Positive electrode Young's modulus [Pa]\": 375000000000.0,\n",
+ " \"Positive electrode Poisson's ratio\": 0.2,\n",
+ " \"Positive electrode critical stress [Pa]\": 375000000.0,\n",
+ " \"Positive electrode LAM constant exponential term\": 2.0,\n",
+ " \"Positive electrode reference concentration for free of deformation [mol.m-3]\": 0.0,\n",
+ " \"Positive electrode volume change\": lico2_volume_change_Ai2020,\n",
+ " \"Primary: Negative electrode reaction-driven LAM factor [m3.mol-1]\": 1e-9,\n",
+ " \"Secondary: Negative electrode reaction-driven LAM factor [m3.mol-1]\": 10,\n",
+ " },\n",
+ " check_already_exists=False,\n",
+ ")\n",
+ "\n",
+ "# Changing secondary SEI solvent diffusivity to show different degradation between phases\n",
+ "parameter_values.update(\n",
+ " {\n",
+ " \"Secondary: Outer SEI solvent diffusivity [m2.s-1]\": 2.5000000000000002e-24,\n",
+ " }\n",
+ ")\n",
+ "\n",
+ "# sim = pybamm.Simulation(model, parameter_values=parameter_values)\n",
+ "# sim.solve([0, 4100])\n",
+ "sim = pybamm.Simulation(\n",
+ " model,\n",
+ " experiment=experiment,\n",
+ " parameter_values=parameter_values,\n",
+ " solver=pybamm.CasadiSolver(\"fast with events\"),\n",
+ ")\n",
+ "solution = sim.solve(calc_esoh=False)\n",
+ "\n",
+ "sim.plot(\n",
+ " [\n",
+ " \"Voltage [V]\",\n",
+ " \"Current [A]\",\n",
+ " \"Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]\",\n",
+ " \"X-averaged negative electrode primary active material volume fraction\",\n",
+ " \"X-averaged negative electrode secondary active material volume fraction\",\n",
+ " \"Negative total primary SEI thickness [m]\",\n",
+ " \"Negative total secondary SEI thickness [m]\",\n",
+ " ]\n",
+ ")"
+ ]
+ },
{
"cell_type": "markdown",
"metadata": {},
@@ -369,22 +670,26 @@
},
{
"cell_type": "code",
- "execution_count": 6,
+ "execution_count": 10,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "[1] Weilong Ai, Ludwig Kraft, Johannes Sturm, Andreas Jossen, and Billy Wu. Electrochemical thermal-mechanical modelling of stress inhomogeneity in lithium-ion pouch cells. Journal of The Electrochemical Society, 167(1):013512, 2019. doi:10.1149/2.0122001JES.\n",
- "[2] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n",
- "[3] Chang-Hui Chen, Ferran Brosa Planella, Kieran O'Regan, Dominika Gastol, W. Dhammika Widanage, and Emma Kendrick. Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n",
- "[4] Rutooj Deshpande, Mark Verbrugge, Yang-Tse Cheng, John Wang, and Ping Liu. Battery cycle life prediction with coupled chemical degradation and fatigue mechanics. Journal of the Electrochemical Society, 159(10):A1730, 2012. doi:10.1149/2.049210jes.\n",
- "[5] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n",
- "[6] 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",
- "[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.\n",
- "[8] Jorn M. Reniers, Grietus Mulder, and David A. Howey. Review and performance comparison of mechanical-chemical degradation models for lithium-ion batteries. Journal of The Electrochemical Society, 166(14):A3189, 2019. doi:10.1149/2.0281914jes.\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",
+ "[1] Weilong Ai, Niall Kirkaldy, Yang Jiang, Gregory Offer, Huizhi Wang, and Billy Wu. A composite electrode model for lithium-ion batteries with silicon/graphite negative electrodes. Journal of Power Sources, 527:231142, 2022. URL: https://www.sciencedirect.com/science/article/pii/S0378775322001604, doi:https://doi.org/10.1016/j.jpowsour.2022.231142.\n",
+ "[2] Weilong Ai, Ludwig Kraft, Johannes Sturm, Andreas Jossen, and Billy Wu. Electrochemical thermal-mechanical modelling of stress inhomogeneity in lithium-ion pouch cells. Journal of The Electrochemical Society, 167(1):013512, 2019. doi:10.1149/2.0122001JES.\n",
+ "[3] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n",
+ "[4] Ferran Brosa Planella and W. Dhammika Widanage. Systematic derivation of a Single Particle Model with Electrolyte and Side Reactions (SPMe+SR) for degradation of lithium-ion batteries. Submitted for publication, ():, 2022. doi:.\n",
+ "[5] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.\n",
+ "[6] Chang-Hui Chen, Ferran Brosa Planella, Kieran O'Regan, Dominika Gastol, W. Dhammika Widanage, and Emma Kendrick. Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models. Journal of The Electrochemical Society, 167(8):080534, 2020. doi:10.1149/1945-7111/ab9050.\n",
+ "[7] Rutooj Deshpande, Mark Verbrugge, Yang-Tse Cheng, John Wang, and Ping Liu. Battery cycle life prediction with coupled chemical degradation and fatigue mechanics. Journal of the Electrochemical Society, 159(10):A1730, 2012. doi:10.1149/2.049210jes.\n",
+ "[8] Marc Doyle, Thomas F. Fuller, and John Newman. Modeling of galvanostatic charge and discharge of the lithium/polymer/insertion cell. Journal of the Electrochemical society, 140(6):1526–1533, 1993. doi:10.1149/1.2221597.\n",
+ "[9] 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",
+ "[10] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.\n",
+ "[11] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n",
+ "[12] Jorn M. Reniers, Grietus Mulder, and David A. Howey. Review and performance comparison of mechanical-chemical degradation models for lithium-ion batteries. Journal of The Electrochemical Society, 166(14):A3189, 2019. doi:10.1149/2.0281914jes.\n",
+ "[13] 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"
]
}
@@ -417,7 +722,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.11.8"
+ "version": "3.11.9"
},
"toc": {
"base_numbering": 1,
diff --git a/docs/source/examples/notebooks/models/sodium-ion.ipynb b/docs/source/examples/notebooks/models/sodium-ion.ipynb
new file mode 100644
index 0000000000..671e7923e9
--- /dev/null
+++ b/docs/source/examples/notebooks/models/sodium-ion.ipynb
@@ -0,0 +1,185 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# DFN model for sodium-ion batteries\n",
+ "\n",
+ "In this notebook we use the DFN model to simulate sodium-ion batteries. The parameters are based on the article\n",
+ "> K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based modeling of sodium-ion batteries part II. Model and validation, Electrochimica Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.\n",
+ "\n",
+ "However, the specific values (including the data for the interpolants) are taken from the COMSOL implementation presented in [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341). As usual, we start by importing PyBaMM."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "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": [
+ "import pybamm"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We now need to define the model. In this case we take the `BasicDFN` model for sodium-ion batteries (note how it is called from the `pybamm.sodium_ion` submodule). Note that, at the moment, the model is identical to the one for lithium-ion batteries, but uses different parameter values."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "model = pybamm.sodium_ion.BasicDFN()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "In order to replicate the results in the COMSOL example, we discharge at different C-rates and compare the solutions. We loop over the C-rate dictionary and solve the model for each C-rate. We append the solutions into a list so we can later plots the results."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8ca3353c637e48d28c3b02f42d25fa03",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "interactive(children=(FloatSlider(value=0.0, description='t', max=10.80914150213347, step=0.1080914150213347),…"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "C_rates = [1 / 12, 5 / 12, 10 / 12, 1]\n",
+ "solutions = []\n",
+ "\n",
+ "for C_rate in C_rates:\n",
+ " sim = pybamm.Simulation(model, solver=pybamm.IDAKLUSolver(), C_rate=C_rate)\n",
+ " sol = sim.solve([0, 4000 / C_rate])\n",
+ " solutions.append(sol)\n",
+ "\n",
+ "pybamm.dynamic_plot(solutions)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We can now perform a manual plot of voltage versus capacity, to compare the results with the COMSOL example."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "for solution, C_rate in zip(solutions, C_rates):\n",
+ " capacity = [i * 1000 for i in solution[\"Discharge capacity [A.h]\"].entries]\n",
+ " voltage = solution[\"Voltage [V]\"].entries\n",
+ " plt.plot(capacity, voltage, label=f\"{(12 * C_rate)} A.m-2\")\n",
+ "\n",
+ "plt.xlabel(\"Discharge Capacity [mA.h]\")\n",
+ "plt.ylabel(\"Voltage [V]\");"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "[1] Joel A. E. Andersson, Joris Gillis, Greg Horn, James B. Rawlings, and Moritz Diehl. CasADi – A software framework for nonlinear optimization and optimal control. Mathematical Programming Computation, 11(1):1–36, 2019. doi:10.1007/s12532-018-0139-4.\n",
+ "[2] Kudakwashe Chayambuka, Grietus Mulder, Dmitri L Danilov, and Peter HL Notten. Physics-based modeling of sodium-ion batteries part ii. model and validation. Electrochimica Acta, 404:139764, 2022.\n",
+ "[3] 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",
+ "[4] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.\n",
+ "[5] 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",
+ "[6] Scott G. Marquis, Valentin Sulzer, Robert Timms, Colin P. Please, and S. Jon Chapman. An asymptotic derivation of a single particle model with electrolyte. Journal of The Electrochemical Society, 166(15):A3693–A3706, 2019. doi:10.1149/2.0341915jes.\n",
+ "[7] 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"
+ ]
+ }
+ ],
+ "source": [
+ "pybamm.print_citations()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "venv",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/docs/source/examples/notebooks/parameterization/parameter-values.ipynb b/docs/source/examples/notebooks/parameterization/parameter-values.ipynb
index b13084b166..12a2c439bf 100644
--- a/docs/source/examples/notebooks/parameterization/parameter-values.ipynb
+++ b/docs/source/examples/notebooks/parameterization/parameter-values.ipynb
@@ -19,7 +19,7 @@
},
{
"cell_type": "code",
- "execution_count": 12,
+ "execution_count": 1,
"metadata": {},
"outputs": [
{
@@ -44,12 +44,12 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "In `pybamm`, the object that sets parameter values for a model is the `ParameterValues` class, which extends `dict`. This takes the values of the parameters as input, which can be either a dictionary,"
+ "In `pybamm`, the object that sets parameter values for a model is the [`ParameterValues`](https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html) class, which extends `dict`. This takes the values of the parameters as input, which can be either a dictionary,"
]
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 2,
"metadata": {},
"outputs": [
{
@@ -81,7 +81,7 @@
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -105,12 +105,12 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "We can input functions into the parameter value (note we bypass the check that the parameter already exists)"
+ "We can alter the values of parameters by updating the dictionary, by using the `update` method or by using the `[]` operator."
]
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": 4,
"metadata": {},
"outputs": [
{
@@ -121,10 +121,42 @@
" 'Electron charge [C]': 1.602176634e-19,\n",
" 'Faraday constant [C.mol-1]': 96485.33212,\n",
" 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n",
- " 'a': 1,\n",
- " 'b': 2,\n",
- " 'c': 3,\n",
- " 'cube function': }\n"
+ " 'a': 2,\n",
+ " 'b': 3,\n",
+ " 'c': 4}\n"
+ ]
+ }
+ ],
+ "source": [
+ "parameter_values[\"a\"] = 2\n",
+ "parameter_values.update({\"b\": 3, \"c\": 4})\n",
+ "print(f\"parameter values are {parameter_values}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Parameter values can either be numerical values, python functions or PyBaMM expressions. We can input functions into the parameter value like so (note we bypass the check that the parameter already exists):\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "parameter values are {'Boltzmann constant [J.K-1]': 1.380649e-23,\n",
+ " 'Electron charge [C]': 1.602176634e-19,\n",
+ " 'Faraday constant [C.mol-1]': 96485.33212,\n",
+ " 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n",
+ " 'a': 2,\n",
+ " 'b': 3,\n",
+ " 'c': 4,\n",
+ " 'cube function': }\n"
]
}
],
@@ -141,19 +173,35 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "## Setting parameters for an expression"
+ "We can also use a PyBaMM expression to set the parameter value, allowing us to set parameters based on other parameters: "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "parameter_values.update({\"a\": pybamm.Parameter(\"b\") + pybamm.Parameter(\"c\")})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "We represent parameters in models using the classes `Parameter` and `FunctionParameter`. These cannot be evaluated directly,"
+ "## Setting parameters for a PyBaMM model"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "We represent parameters in models using the classes [`Parameter`](https://docs.pybamm.org/en/latest/source/api/expression_tree/parameter.html) and [`FunctionParameter`](https://docs.pybamm.org/en/latest/source/api/expression_tree/parameter.html#pybamm.FunctionParameter). These cannot be evaluated directly,"
]
},
{
"cell_type": "code",
- "execution_count": 16,
+ "execution_count": 7,
"metadata": {
"tags": [
"raises-exception"
@@ -190,14 +238,14 @@
},
{
"cell_type": "code",
- "execution_count": 17,
+ "execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "7.0 = 7.0\n"
+ "19.0 = 19.0\n"
]
}
],
@@ -208,14 +256,14 @@
},
{
"cell_type": "code",
- "execution_count": 18,
+ "execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
- "1.0 = 1.0\n"
+ "343.0 = 343.0\n"
]
}
],
@@ -233,7 +281,7 @@
},
{
"cell_type": "code",
- "execution_count": 19,
+ "execution_count": 10,
"metadata": {},
"outputs": [
{
@@ -279,12 +327,12 @@
},
{
"cell_type": "code",
- "execution_count": 20,
+ "execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
- "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAACVcElEQVR4nOzdd3iTZdvH8e+ddEJJmV1QStmWvaGIog8KyBAX4quCuAcq4uR53Atw4kBxsZygKCIqCAiCssEiUGWWMjqYbWihK8n7R0oldKXQNh2/j0eOkjvXfeeMNPTqmes6T8PhcDgQERERERERERGRasvk6QBERERERERERETEs5QkFBERERERERERqeaUJBQREREREREREanmlCQUERERERERERGp5pQkFBERERERERERqeaUJBQREREREREREanmlCQUERERERERERGp5pQkFBERERERERERqeaUJBQREREREREREanmlCQUEfGwvn370rZtW0+HISIiIiLloEmTJgwePNjTYYiI5KMkoYhICbz88sv07NmTBg0a4OfnR4sWLRg7diyHDx/2dGjnzW63M2PGDIYOHUp4eDg1a9akbdu2vPjii2RkZHg6PBEREZFyN3v2bG666SZatGiBYRj07dvX0yGVmpMnTzJlyhQuv/xyQkNDqVWrFp06deL999/HZrN5OjwR8QAvTwcgIlKZbNy4kY4dOzJixAhq1arF33//zUcffcSPP/5ITEwMNWvW9HSI5+zkyZOMHj2anj17cvfddxMUFMTq1at55plnWLp0Kb/++iuGYXg6TBEREZFy8/7777Nx40a6devG0aNHPR1OqdqzZw/3338///nPfxg3bhwWi4VFixZx7733smbNGmbOnOnpEEWknClJKCJSAnPnzs13rFevXlx77bX88MMPjBgxwgNRlQ4fHx/++OMPoqOj847dcccdNGnSJC9R2K9fPw9GKCIiIlK+Pv30Uxo2bIjJZKpy5WFCQkLYsmULbdq0yTt21113ceuttzJ9+nSeeuopmjdv7sEIRaS8abuxiFR48fHx3HvvvbRq1Qp/f3/q1avHddddx969ez0dGuCsKwOQkpJyXtfZuHEj0dHR+Pv7ExkZydSpU88/uBLw8fFxSRCedtVVVwHw999/l2s8IiIiUrVV9DkeQHh4OCZT2fza/Msvv9CxY0f8/PyIiori22+/LZPnKUz9+vVdEoSnae4nUn1pJaGIVHjr169n1apVjBgxgkaNGrF3717ef/99+vbtS2xsLDVq1Cjy/OPHj7tVV6VGjRrFXgvA4XBw9OhRcnJy2LlzJ0888QRms/m8atQcP36cK664guHDh3PDDTcwZ84c7rnnHnx8fLj11luLPDc1NZXs7Oxin8PPz4+AgIASx5aUlAQ4J5IiIiIipaWizfHK086dO7n++uu5++67GTVqFNOnT+e6665j4cKFXHbZZUWeW9avW3M/kerLcDgcDk8HISJSlFOnTuHv7+9ybM2aNfTq1YtZs2Zx8803F3l+kyZNiI+PL/Z5nnnmGZ599tlixyUlJREaGpp3v1GjRrz++usMHz682HML0rdvX3777Tdef/11xo0bB0BWVhY9evQgISGBAwcO4O3tXez5xRk1ahQzZswocXyXXXYZ69atIz4+ntq1a5f4fBEREZGCVLQ5XnHatm1L/fr1Wb58+Xld53Tcc+fO5eqrrwbAarXSunVrQkJC2LRpk1vnF+dcXndWVhadOnXi1KlT7NixAy8vrSsSqU70jheRCu/MyWN2djZWq5XmzZtTu3ZtNm3aVOwE8vPPP+fUqVPFPk/Tpk3diqdu3bosXryYjIwM/vzzT7799lvS0tLcOrcwXl5e3HXXXXn3fXx8uOuuu7jnnnvYuHEjPXv2LPTc119/nePHjxf7HGFhYSWO6+WXX2bJkiW89957ShCKiIhIqapoc7zyFBYWlretF8BisTBy5EgmTZpEUlISISEhhZ5blq97zJgxxMbG8uOPPypBKFIN6V0vIhXeqVOnmDBhAtOnT+fgwYOcuQA6NTW12PN79+5dqvH4+PjkNfAYPHgw//nPf+jduzdBQUEMHjz4nK4ZFhaWrzNyy5YtAdi7d2+RScIuXbqc03MWZ/bs2Tz55JPcdttt3HPPPWXyHCIiIlJ9VZQ53rFjx8jKysq77+/vT2BgYKlcuzDNmzfHMAyXY2fO/YpKEpb23Pa0V199lY8++ogXXniBK664okyeQ0QqNiUJRaTCu//++5k+fTpjx46lV69eBAYGYhgGI0aMwG63F3v+4cOH3arbEhAQcE41+6KjowkNDeXzzz8/5yTh+Th7YluYkkx4Fy9ezMiRIxk0aFC5N1ARERGR6qGizPGuvvpql9It51qipbyUxdx2xowZPP7449x99908+eST5xuiiFRSShKKSIX3zTffMGrUKF5//fW8YxkZGW53E+7WrVuZ16vJyMhw6xPvwiQkJJCenu6ymnDHjh3Av92TC3P2xLYw7k54165dy1VXXUXXrl2ZM2eOtpqIiIhImagoc7yzS7ecS4mWktq1axcOh8NlNaG7c7/Sntt+//333H777Vx99dVMmTKl2PEiUnXpNz8RqfDMZjNn91h655133PoEFUqvbkt6ejqGYeTrEjd37lyOHz9O165d3YqnIDk5OXzwwQcujUs++OADGjRoUOx24tKsSfj3338zaNAgmjRpwoIFC/IVExcREREpLRVljldWpVuKkpCQwHfffefSuGTWrFl07NixyK3GULo1CVesWMGIESO46KKL+PzzzzGZTO69ABGpkpQkFJEKb/DgwXz66acEBgYSFRXF6tWrWbJkCfXq1XPr/NKq27Jz50769evH9ddfT+vWrTGZTGzYsIHPPvuMJk2a8OCDD7qMP/0p8N69e4u9dlhYGJMmTWLv3r20bNmS2bNnExMTw4cfflhkZ2MovYntiRMn6N+/P8ePH+fRRx/lxx9/dHm8WbNm9OrVq1SeS0RERKSizPGKsmLFClasWAE4t/mmp6fz4osvAnDRRRdx0UUX5Y01DIOLL77Yre7HLVu25LbbbmP9+vUEBwczbdo0kpOTmT59erHnltbrjo+PZ+jQoRiGwbXXXsvXX3/t8nj79u1p3759qTyXiFQOShKKSIX31ltvYTab+fzzz8nIyKB3794sWbKE/v37l2scjRo14pprruHXX39l5syZZGdnExERwZgxY/jf//6Xb0Kbnp5O8+bN3bp2nTp1mDlzJvfffz8fffQRwcHBvPvuu9xxxx1l8VIKdPToUfbv3w/AE088ke/xUaNGKUkoIiIipaaizPGK8uuvv/Lcc8+5HHvqqacA53be00nCtLQ0AEJDQ926bosWLXjnnXd49NFH2b59O5GRkcyePbtcX3tcXFxeuZz77rsv3+PPPPOMkoQi1YzhOHt9t4iInLfY2FjatGnDggULGDRokKfDEREREZEy9NNPPzF48GA2b95Mu3btPB2OiMg5UcEBEZEysGzZMnr16qUEoYiIiEg1sGzZMkaMGKEEoYhUalpJKCIiIiIiIiIiUs1pJaGIiIiIiIiIiEg1pyShiIiIiIiIiIhINackoYiI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MEApT1onWon1yPidqT6TCeaY6wPWRJgz0vlmFxERERERERE5W+x8Z+7gjAQhQAPHUab6TGZh/xQlCKsZJQkrK7sNY+ETAJiMsx90OL8sfMK5bFhERERERERE5DS7zbmC8HT+4AzOHINB3RVPK6dQzShJWFnFrwJrAvnyg3kcYD3oHCciIiIiIiIiclpuTqEwhnIK1ZKShJVVWnLpjhMRERERERGR6kE5BSmAkoSVVUBw6Y4TERERERERkWohLiPAvYHKKVQrShJWVhHRzi7GhW44NsDS0DlORERERERERAT4aUsig+fbSXDUxV7oKOUUqiMlCSsrkxkGTMq945oozCs7OmCic5yIiIiIiIiIVGt2u4M3F+/g3s83kZ7t4JsGYzAwyL/4KPe+cgrVjpKElVnUUBg+CyyhLoeTqEf6sOnOx0VERERERESkWjuZlcN9X2ziraU7AbjtwkjuvechjAJyCljCnLkG5RSqHSUJK7uooTB2K4xagP3qj3m05sv0zniLqYeiPB2ZiIiIVCMrVqxgyJAhhIWFYRgG8+bNc3nc4XDw9NNPExoair+/P/369WPnzp2eCVZERKQaOZhyimvfX83PW5PwNhu8ck17nhochZfZ5JJT4JpPnF/HblGCsJoqcZKwuAng2b799lsuu+wyGjRogMVioVevXixatMhlzLPPPothGC631q1blzS06stkhsg+mNpfx38GXoMdE9N+j+NYepanIxMREZFqIj09nQ4dOjBlypQCH3/llVd4++23mTp1KmvXrqVmzZr079+fjIyMco5URESk+tiw9xhD3/md2EQr9QN8+PKOngzvFu46KDenQLtrnV+1xbjaKnGSsLgJ4NlWrFjBZZddxk8//cTGjRu55JJLGDJkCH/++afLuDZt2pCYmJh3+/3330samgD92wTTtqGF9CwbH/y229PhiIiISDUxcOBAXnzxRa666qp8jzkcDiZPnsyTTz7JlVdeSfv27Zk1axYJCQnFfuAsIiIi5+aLtfu44aM1HE3PIirUwvdjLqRrk7qeDksqMK+SnjBw4EAGDhzo9vjJkye73H/55Zf5/vvv+eGHH+jUqdO/gXh5ERISUtJw5CyGYTDuspbcOmMDM1fv5bY+kQTV8vN0WCIiIlKNxcXFkZSURL9+/fKOBQYG0qNHD1avXs2IESM8GJ2IiEjVkplj45nvt/HV+v0AXNEuhNeu60ANnxKngKSaKfeahHa7nRMnTlC3rmv2eufOnYSFhdG0aVNuvPFG9u3bV+g1MjMzsVqtLjf51yWtgujUuDYZ2XbeW6bVhCIiIuJZSUlJAAQHB7scDw4OznvsbJrviYiIlFxi6imu/2ANX63fj2HAYwNaMeX/OitBKG4p9yTha6+9RlpaGsOHD8871qNHD2bMmMHChQt5//33iYuLo0+fPpw4caLAa0yYMIHAwMC8W3h4eIHjqivDMHj4slaAc3lxQsopD0ckIiIiUjKa74mIiBTBboO4lbDlG+dXu421e44y5J3fidmfQqC/NzNGd+fevs0xDMPT0UolUa5Jwi+++ILnnnuOOXPmEBQUlHd84MCBXHfddbRv357+/fvz008/kZKSwpw5cwq8zvjx40lNTc277d+/v7xeQqXRu3k9ukfWJctm591luzwdjoiIiFRjp0vKJCcnuxxPTk4utNyM5nsiIiKFiJ0Pk9vCzMEw9zaYOZj0SRcw85N3OJKWReuQWvww5kIubtnA05FKJVNuScKvvvqK22+/nTlz5rjUoylI7dq1admyJbt2FZzc8vX1xWKxuNzElXM1YUsA5qzfz76jJz0ckYiIiFRXkZGRhISEsHTp0rxjVquVtWvX0qtXrwLP0XxPRESkALHzYc5IsCa4HPbPSOZdrzd5sukuvr03msb1angoQKnMyiVJ+OWXXzJ69Gi+/PJLBg0aVOz4tLQ0du/eTWhoaDlEV3X1aFqPPi3qk2N38Nov2z0djoiIiFRhaWlpxMTEEBMTAziblcTExLBv3z4Mw2Ds2LG8+OKLzJ8/ny1btjBy5EjCwsIYNmyYR+MWERGpNOw2WPg44Mj3kMlwLha6Le0Danhpe7GcmxInCYuaAIJza8jIkSPzxn/xxReMHDmS119/nR49epCUlERSUhKpqal5Yx555BF+++039u7dy6pVq7jqqqswm83ccMMN5/ny5PEBrQGYvzmBrQdTixktIiIicm42bNhAp06d6NSpEwDjxo2jU6dOPP300wA89thj3H///dx5551069aNtLQ0Fi5ciJ+fnyfDFhERqTziV+VbQXgmAweG9aBznMg5KHGSsLgJYGJioktn4g8//JCcnBzuu+8+QkND824PPvhg3pgDBw5www030KpVK4YPH069evVYs2YNDRpo//z5atswkCs7hgEw8ed/PByNiIiIVFV9+/bF4XDku82YMQNwrm54/vnnSUpKIiMjgyVLltCyZUvPBi0iIlKZpCUXP6Yk40TOUuIe2KcngIU5PRE8bfny5cVe86uvvippGFICj1zeip+3JPH7riOs2HGYi1S8VERERERERKRSSfOuR4A7AwOCyzoUqaLKtbuxeEZ43Rrc1DMCcK4mtNsLT/KKiIiIiIiISMXy14EUBn6XQ4KjLoX/Sm+ApSFERJdnaFKFKElYTYy5tDm1fL2ITbQyf3PhNQxEREREREREpGJwOBx8uiaea99fzf7ULN7zuwPDMICzm5Pk3h8wEUzm8g5TqgglCauJujV9uLtvMwBe+2U7mTk2D0ckIiIiIiIiIoVJz8xh7OwYnpq3lSybnf5tgnnsoUcxhs8CS6jrYEsYDJ8FUUM9E6xUCSWuSSiV1629I5m1ei8Hjp/i09Xx3N6nqadDEhEREREREZGz7Dp0grs/28SuQ2mYTQZPDGjN7X0inasIo4ZC60HOLsZpyc4ahBHRWkEo500rCasRfx8zD/VzdhF8d9kuUk9mezgiERERERERETnT9zEHGfruH+w6lEZQLV++urMnd1zUNHebcS6TGSL7QLtrnV+VIJRSoCRhNXNtl0a0CAog5WQ2b/+609PhiIiIiIiIiAiQkW3jf99t4cGvYjiZZSO6WT1+fKAP3ZrU9XRoUk0oSVjNeJlNPDk4CoCZq/ay53CahyMSERERERERqd52HUpj2JQ/+HztPgDGXNKcT2/rQYNavh6OTKoTJQmroYtbNuCSVg3IsTt4+ae/PR2OiIiIiIiISPVgt0HcStjyjfOr3cY3Gw8w5J3f+SfpBPVq+jDz1u480r8VZtPZHYxFypYal1RT/xsUxcqdK1jy9yFW7jxMnxYNPB2SiIiIiIiISNUVOx8WPg7WhLxDKV4NWHzyRk7ZuxPdrB6Tr+9IkMXPg0FKdaaVhNVU86AAbu4VAcALC2LJsdk9HJGIiIiIiIhIFRU7H+aMdEkQAliyD/O+92Te63SAT2/roQSheJSShNXYg/9pQe0a3uxITuPL9fs9HY6IiIiIiIhI1WO3OVcQ4sj3kMkAwzC44uBbmNHiHfEsJQmrsdo1fHioX0sA3vhlO6knsz0ckYiIiIiIiEgVE78q3wrCMxk4wHrQOU7Eg5QkrOZu7NGYFkEBHD+ZzdtL/slXQFVEREREREREzkNacumOEykjalxSzXmZTTw5OIovZrzL7RvHwKZj/z5oCYMBkyBqqOcCFBEREREREamkcmx2vt+RzTXuDA4ILutwRIqklYTCxTmrmeozmWCOuT5gTXQWVo2d75nARERERERERCqp/cdOMuLDNTy6PoAER90CKhKeZoClIUREl2N0IvkpSVjd5RVQdRZMdZX7T9jCJ7T1WERERERERMRN8/48yBVvrWRD/HFq+Pqwv/vTgJF7O1Pu/QETwWQu5yhFXGm7cXWXW0A1X34wzxkFVCP7lGNgIiIiIiIiIpWLNSObp+Zt5fsYZ6OSLhF1mHx9R8Lr1oDIes5FOmc2MbGEOROEKvMlFYCShNWdCqiKiIiIiIiInLf1e48x9qsYDqacwmwyePA/Lbi3bzO8zLmbOKOGQutBzkU4acnOGoQR0VpBKBWGkoTVnbuFUVVAVURERERERCSfbJudt5fuZMqyXdgd0LhuDSaP6EjnxnXyDzaZtUtPKiwlCau7iGjn8mZrIhRQRtWBgWEJUwFVERERERERkbPsPZLOg7Nj2Lw/BYBrOjfiuSvbEOCrdItUPvqure5MZhgwydnFGIMzE4V2BxgGKqAqIiIiIiIi1Zfdlm+LsMMw8eW6/bz4Yywns2xY/Lx46ap2DOkQ5uloRc6ZkoTirIswfFa+AqpJ1OMN02j+FzGAAhZJi4iIiIiIiFRtsfPz/a5sCwjlPf87eH1/awB6RNbljes70rC2v6eiFCkVShKK01kFVLP9g7htvo2/D53E8ePfvD68g6cjFBERERERESk/sfNzd925luYy0hK578TzbPceR8fLb+bW3pGYTIZnYhQpRaaSnrBixQqGDBlCWFgYhmEwb968Ys9Zvnw5nTt3xtfXl+bNmzNjxox8Y6ZMmUKTJk3w8/OjR48erFu3rqShyfk6XUC13bV4N7+Il67tiGHA3E0H+H3nEU9HJyIiIiIiIlI+7DbnCsICavebAAx4M/Arbu8doQShVBklThKmp6fToUMHpkyZ4tb4uLg4Bg0axCWXXEJMTAxjx47l9ttvZ9GiRXljZs+ezbhx43jmmWfYtGkTHTp0oH///hw6dKik4Ukp6ty4DiN7RgDw3++2cCrL5uGIRERERERERMpB/CqXLcZnMwHeaQnOcSJVRImThAMHDuTFF1/kqquucmv81KlTiYyM5PXXX+eCCy5gzJgxXHvttbz55pt5Y9544w3uuOMORo8eTVRUFFOnTqVGjRpMmzatpOFJKXt0QGtCA/3Yd+wkk5fu8HQ4IiIiIiIiImUvLbl0x4lUAiVOEpbU6tWr6devn8ux/v37s3r1agCysrLYuHGjyxiTyUS/fv3yxojnBPh68cKVbQH4eGUcWw+mejgiERERERERkbIVe8LNJiQBwWUbiEg5KvMkYVJSEsHBrm+a4OBgrFYrp06d4siRI9hstgLHJCUlFXjNzMxMrFary03KTr+oYAa1C8VmdzD+2y3k2OyeDklERERERESk1J3MyuG5H7Yx5Ac7CY66FP7brwGWhhARXY7RiZStMk8SloUJEyYQGBiYdwsPD/d0SFXeM0OjsPh5seVgKtP/2OvpcERERERERERK1ardRxgweSXT/9iLzWFiSeNxGBjA2Y1Jcu8PmOhsACpSRZR5kjAkJITkZNc9+snJyVgsFvz9/alfvz5ms7nAMSEhIQVec/z48aSmpubd9u/fX2bxi1NQLT/+N+gCAF77ZTu7D6d5OCIRERERERGR85eWmcP/vtvC/320ln3HThIW6MfMW7sz8rb7MYbPAkuo6wmWMBg+C6KGeiZgkTLiVdZP0KtXL3766SeXY4sXL6ZXr14A+Pj40KVLF5YuXcqwYcMAsNvtLF26lDFjxhR4TV9fX3x9fcs0bslveNdwFvyVyMqdR3jk6818c3c0ZrV6FxERERERkUpqxY7DjP92CwdTTgHwfz0aM35ga2r5eTsHRA2F1oOcXYzTkp01CCOitYJQqqQSryRMS0sjJiaGmJgYAOLi4oiJiWHfvn2Ac5XfyJEj88bffffd7Nmzh8cee4x//vmH9957jzlz5vDQQw/ljRk3bhwfffQRM2fO5O+//+aee+4hPT2d0aNHn+fLk9JkGAaTrmlPLV8v/tyXwscr93g6JBEREakkbDYbTz31FJGRkfj7+9OsWTNeeOEFHA6Hp0MTEZFqKPVUNo99s5mR09ZxMOUU4XX9+eL2Hrx8Vbt/E4SnmcwQ2QfaXev8qgShVFElXkm4YcMGLrnkkrz748aNA2DUqFHMmDGDxMTEvIQhQGRkJD/++CMPPfQQb731Fo0aNeLjjz+mf//+eWOuv/56Dh8+zNNPP01SUhIdO3Zk4cKF+ZqZiOeF1fbnqSFRPPbNX7y+eAeXtg6iRXAtT4clIiIiFdykSZN4//33mTlzJm3atGHDhg2MHj2awMBAHnjgAU+HJyIiVZHdVuAKwF//Sea/324lyZoBwC3RTXi0fytq+pb5ZkuRCs1wVIGPb61WK4GBgaSmpmKxWDwdTpXncDi4dcZ6lm0/TPtGgXx7TzRe5krZA0dERKTaK6951ODBgwkODuaTTz7JO3bNNdfg7+/PZ599VmHiFBGRKiJ2Pix8HKwJeYfstcKYabmH53Y3A6BJvRq8cm0HukfW9VSUIuXC3XmUMjtSYoZhMOHq9lj8vPjrQCofrNC2YxERESladHQ0S5cuZceOHQBs3ryZ33//nYEDB3o4MhERqXJi58OckS4JQgBOJDDqwFMMNK/jjj6R/PzgRUoQipxBa2nlnIQE+vHs0DaMm7OZyUuc244vCNWn+iIiIlKwJ554AqvVSuvWrTGbzdhsNl566SVuvPHGAsdnZmaSmZmZd99qtZZXqCIiUpnZbc4VhOTfNGkC7Aa8VXs2PgOfVm1BkbNoJaGcs6s6NaTfBcFk2xw8NDuGjGybp0MSERGRCmrOnDl8/vnnfPHFF2zatImZM2fy2muvMXPmzALHT5gwgcDAwLxbeHh4OUcsIiKVUvyq/CsIz2ACfNITneNExIWShHLOnNuO21Gvpg//JJ3g1UXbPR2SiIiIVFCPPvooTzzxBCNGjKBdu3bcfPPNPPTQQ0yYMKHA8ePHjyc1NTXvtn///nKOWEREKqW05NIdJ1KNKEko56VBLV9eva49AJ/8HseKHYc9HJGIiIhURCdPnsRkcp16ms1m7HZ7geN9fX2xWCwuNxERkaKczMrhs20Z7g0OCC7bYEQqISUJ5bxd2jqYkb0iAHj4680cS8/ycEQiIiJS0QwZMoSXXnqJH3/8kb179/Ldd9/xxhtvcNVVV3k6NBERqQJ+23GY/pNX8HRMIAmOugVUJDzNAEtDiIgux+hEKgclCaVU/PeKC2geFMDhE5k8PvcvHLYciFsJW75xfrWrXqGIiEh19s4773Dttddy7733csEFF/DII49w11138cILL3g6NBERqcSOpGUy9qs/GTVtHfuPnSK0dk2OXvgcBgZgnDU69/6AiWpaIlIAw+FwFJ5grySsViuBgYGkpqZqK4oHbUtIZdiUP7jUsZY3LF9SM+OMGg+WMBgwCaKGei5AERERyaeyzKMqS5wiIlI+bHYHX6zbx6sL/8GakYPJgFuiI3n48pbU9PWC2PnOLsdnNjGxNHQmCPV7qVQz7s6jvMoxJqni2oQF8l6ng/xny2Q4heuHNtZEmDMShs/SP8giIiIiIiJyzv46kMKT87by14FUANqEWXj5qnZ0CK/976CoodB6kLOLcVqyswZhRLRWEIoUQUlCKT12G/32vQFG/kXd4AAMWPiE8x9q/cMsIiIiIiIiJZB6MpvXftnOZ2vjcTiglq8Xj/RvxU09IzCb8v8WiskMkX3KP1CRSkpJQik98aswzlzKnY8DrAedn+ToH2oRERERERE5m92Wb/WfwzDx3Z8HefmnvzmS5myUeVWnhoy/ojVBtfw8HLBI1aEkoZSetOTix5RknIiIiIiIiFQfBdQRzK4Zylvet/FuUhQAzRrU5IVhbYluVt9TUYpUWUoSSukJCC7dcSIiIiIiIlI9xM531rHHtbeqOS2RcbzIPu9xXPCfm7jtwkh8vEyeiVGkitM7S0pPRLSzi3EBFQkBHBjOblIR0eUbl4iIiIiIiFRcdptzBeFZCUIAkwGGAW8EfsU9FzVRglCkDOndJaXHZIYBk3LvuCYK7Q4Ah7PdvJqWiIiIiIiIyGnxq1y2GJ/NALzSEpzjRKTMKEkopStqKAyfBZZQl8NJ1OPurLF8m9HZQ4GJiIiIiIhIRZR29KCbA1XfXqQsqSahlL6oodB6kEtHqjk767Po1z0s/3YLLYNr0bZhoKejFBEREREREQ+y2R18sW4fvy1K5GN3TlB9e5EypSShlA2TGSL75N19IMLB5oMnWLb9MHd/tpEfxlxInZo+HgxQREREREREPGXtnqM8+0MsfydaMdGcw/71qO84hlFAXUIwnPXvVd9epExpu7GUC5PJYPL1nYioV4MDx0/xwFd/YrMX9I+/iIiIiIiIVFUJKacY88Umrv9wDX8nWgn09+bZK9tR95o3cyvbn90IM/e+6tuLlDklCaXcBNbw5oObu+DvbWblziO8/st2T4ckIiIiIiIi5eBUlo23l+7k0teXs+CvREwG3NSzMcse6cvIXk0wt72ywPr2WMKcx6OGeiZwkWpE242lXLUOsTDp2vY88OWfvLd8N+0bBTKgbWjxJ4qIiIiIiEjFZLe51KQnIjpv1Z/d7uD7zQd5ZeF2ElMzAOjepC7PDI2iTdhZteoLqG9/5rVEpGwpSSjlbmiHMP7an8LHv8fx8JzNNKlfk9YhFk+HJSIiIiIiIiUVOx8WPg7WhH+PWcJgwCTW+V/Iiz/G8teBVAAa1vbn8YGtGdI+FMM4e1txrrPq24tI+VGSUDziiYGtiU20smr3UW6fuYF59/WmfoCvp8MSERERERERd8XOhzkj4axmIw5rIsy5mU+yxvKXvTsBvl7ce0kzbu0diZ+3VgWKVFSqSSge4WU28d6NnWmS28jk7k83kplj83RYIiIiIiIi4g67zbmCsIBuxAYOHA54xvtTburekOWP9uXevs2VIBSp4M4pSThlyhSaNGmCn58fPXr0YN26dYWO7du3L4Zh5LsNGjQob8wtt9yS7/EBAwacS2hSidSu4cMnt3Sjlp8XG+KPM/7bLTgc6ngsIiIiIiJS4cWvct1ifBaTAWHGUV7sdEK7xkQqiRInCWfPns24ceN45pln2LRpEx06dKB///4cOnSowPHffvstiYmJebetW7diNpu57rrrXMYNGDDAZdyXX355bq9IKpVmDQJ478bOmE0G3246yNTf9ng6JBERERERESlOWnLpjhMRjytxkvCNN97gjjvuYPTo0URFRTF16lRq1KjBtGnTChxft25dQkJC8m6LFy+mRo0a+ZKEvr6+LuPq1Klzbq9IKp0+LRrw7JAoAF5Z9A+LtiV5OCIREREREREpyvb0Gu4NDAgu20BEpNSUKEmYlZXFxo0b6dev378XMJno168fq1evdusan3zyCSNGjKBmzZoux5cvX05QUBCtWrXinnvu4ejRoyUJTSq5m3s1YWSvCBwOGPtVDFsPpno6JBERERERETnLrkNp3PXpBgbOs5HgqIu90IpRBlgaQkR0eYYnIuehREnCI0eOYLPZCA52/SQgODiYpKTiV3+tW7eOrVu3cvvtt7scHzBgALNmzWLp0qVMmjSJ3377jYEDB2KzFdzIIjMzE6vV6nKTyu/pwVH0aVGfU9k2Rs9Yz/5jJz0dkoiIiIiIiABJqRk8MfcvLn/zNxZtSwbDxJLG4zAMAzDOGp17f8BEMKlZiUhl4VWeT/bJJ5/Qrl07unfv7nJ8xIgReX9u164d7du3p1mzZixfvpz//Oc/+a4zYcIEnnvuuTKPV8qXl9nElBs7M3zqav5JOsGo6euYe2cP6hzZ4KxjERDs/BRKP2RERERERETKReqpbKb+tptpv8eRmWMH4PKoYB4b0IrmQYMgNsLZ5fjMJiaWMGeCMGqoh6IWkXNRoiRh/fr1MZvNJCe7Fh5NTk4mJCSkyHPT09P56quveP7554t9nqZNm1K/fn127dpVYJJw/PjxjBs3Lu++1WolPDzczVchFZnFz5sZo7tz9Xt/0OLoMmxv3gb2I2cMCIMBk/TDRkRERERE5HzZbc4uxQUsysjItjFr9V6mLNtN6qlsALpG1GH8Fa3pElH332tEDYXWgwq9johUHiVKEvr4+NClSxeWLl3KsGHDALDb7SxdupQxY8YUee7XX39NZmYmN910U7HPc+DAAY4ePUpoaGiBj/v6+uLrqxbqVVVIoB/f9D1CyKLJYMN15bo1EeaMhOGzlCgUERERERE5V7HzC1wBaOs/kW9PdebNxTtISM0AoEVQAI8PaM1/LgjK3V58FpMZIvuUU+AiUlZKvN143LhxjBo1iq5du9K9e3cmT55Meno6o0ePBmDkyJE0bNiQCRMmuJz3ySefMGzYMOrVq+dyPC0tjeeee45rrrmGkJAQdu/ezWOPPUbz5s3p37//ebw0qbTsNsJWP4sDyP/zxwEYsPAJ56dV+nRKRERERESkZGLnOxdf4Np1xGFNxPT1SJZkjSXB3p3QQD8euqwl13RuhNlUQHJQRKqUEicJr7/+eg4fPszTTz9NUlISHTt2ZOHChXnNTPbt24fJ5NoPZfv27fz+++/88ssv+a5nNpv566+/mDlzJikpKYSFhXH55ZfzwgsvaLVgdRW/CqwJ+Urf/ssB1oPOcfq0SkRERERExH12m3MFIfnbEhs4sDvgWZ9P6XrJjdwc3Qw/by3MEKkuzqlxyZgxYwrdXrx8+fJ8x1q1aoXDUXBfdH9/fxYtWnQuYUhVlZZc/JiSjBMRERERERGn3EUZhTEZEMpR7micDN4tyzEwEfE0U/FDRMpZQHDpjhMREREREREnLcoQkUIoSSgVT0S0s4txIRuO7Q445R/iHCciIiIiIiJu2XIglYm/H3dvsBZliFQ7ShJKxWMyw4BJuXdcE4WnN60/euL/+H23mz/cREREREREqrHtSSe4+9ONDHn3dz6MDyHRUbeAioSnGWBpqEUZItWQkoRSMUUNheGzwBLqetzSkA9DnmVBTlfumLWBjfHHPBOfiIiIiIhIBfdPkpX7Pt/EgLdWsHBbEoYBV3YKx3zFKxgY5N+9lXt/wETn4g0RqVbOqXGJSLmIGgqtBzkL66YlQ0AwRkQ0o+3wx8wNrNx5hFumr+eL23vSrlGgp6MVERERERGpELYlpPLO0l0s3JaUd2xg2xDGXdaSFsG1gI5Qy9fZ5fjMJiaWMGeCMGpouccsIp5nOAprO1yJWK1WAgMDSU1NxWKxeDocKQensmyMnLaW9XuPY/Hz4nMlCkVERM5JZZlHVZY4RUTKnN3mspCCiOi8VX9bDqTy9q87WRzrbDpiGHBFu1Duv7Q5rUMK+LeziGuJSNXh7jxKKwmlUvL3MTPtlm7cMn09G+OPc+PHa5QoFBERqeAOHjzI448/zs8//8zJkydp3rw506dPp2vXrp4OTUSkcoidX+Dqv73dnuaF3c1Z+s8hwJkcHNI+jDGXNqdlcK3Cr2cyQ2SfMg5aRCoL1SSUSquWnzczb+1Ol4g6WDNyuPHjNfx1IMXTYYmIiEgBjh8/Tu/evfH29ubnn38mNjaW119/nTp16ng6NBGRyiF2PswZ6ZogBOzWBBovuRuvHQswGXBVp4Ysfuhi3r6hU9EJQhGRs2i7sVR6aZk53DJtHRvinVuPP7u9B+0b1fZ0WCIiIpVCec2jnnjiCf744w9Wrlx5Tudrvici1ZrdBpPb5ksQ5j3sgFTvII7fsYGmwdpdJSKu3J1HaSWhVHoBvl7MuLU73ZqcXlG4ls37UzwdloiIiJxh/vz5dO3aleuuu46goCA6derERx99VOj4zMxMrFary01EpNqKX1VoghDAZECdnEM0PflXOQYlIlWNkoRSJQT4ejF9tDNReCI3Ubh2z1FPhyUiIiK59uzZw/vvv0+LFi1YtGgR99xzDw888AAzZ84scPyECRMIDAzMu4WHh5dzxCIiFYPN7mBT7D/uDU5LLttgRKRK03ZjqVLSMnO4Y+YGVu85iq+Xiak3deGS1kGeDktERKTCKq95lI+PD127dmXVqlV5xx544AHWr1/P6tWr843PzMwkMzPTJc7w8HDN90Sk2sjKsfPdnweY+tsego+t5yufF4s/adQCNSIRkXy03ViqJeeKwm70uyCIzBw7d8zawA+bc5fl220QtxK2fOP8ard5NlgREZFqJDQ0lKioKJdjF1xwAfv27StwvK+vLxaLxeUmIlIdpGfm8PHKPVz0yjIen7uFuCPpbPdpywmfIBwYhZxlgKUhRESXa6wiUrV4eToAkdLm523m/Zu68PCczczfnMADX/1JnfiFXLjrVdc6HpYwGDAJooZ6LlgREZFqonfv3mzfvt3l2I4dO4iIiPBQRCIi5cxuc9YWTEuGgGBnQs9kzns45WQWM1btZcaqvaSczAYg2OLLHX2ackP3xtTc/bqzuzEGcOaGwNzE4YCJLtcTESkpJQmlSvI2m3jz+o7U8vPiyPpviN44GYeB6+du1kTnD9nhs5QoFBERKWMPPfQQ0dHRvPzyywwfPpx169bx4Ycf8uGHH3o6NBGRshc7HxY+XuCihYNhlzHt9zi+XLePk1nO3U5N6tXg7oubcVXnhvh65Sb+ooY6f3cp8DoT9TuNiJw31SSUKs1hy+HEpAsIyDyEqcCV+Ybzh+rYLfrUTUREqqXynEctWLCA8ePHs3PnTiIjIxk3bhx33HFHhYtTRKRUxc7PXQHo+qu3I3dF4L3ZD/GzrRsAF4RauLdvM65oF4q54F9gil2RKCJyNnfnUVpJKFWasW81lqxDFFq6AwdYDzp/yKrAr4iISJkaPHgwgwcP9nQYIiLlx25zrvwj/9ocAwd2BzzlNYsTTS7jtota0LdlAwyj0F9enExm/e4iImVCSUKp2tKSS3eciIiIiIiIu+JXuW4NPovJgDCO8lk/O0QGlWNgIiL5qbuxVG0BwaU7TkRERERExE3pRw+6N1CLFkSkAtBKQqnaIqKdNQetiRS0xN8O5NQMxSciutxDExERERGRqmnf0ZNM+yOOuA0HmenO0hwtWhCRCkBJQqnaTGYYMCm3ULCzMPBpdpx3H7HewBWxhxnQNsRDQYqIiIiISGXncDhYvfso0/7Yy9J/knE4wERLDvvXp77jKEYBixbyGilq0YKIVABKEkrVFzUUhs9yFgw+sx5IrYa843Mb8w+2Zv5nG3m0fyvu7dus+ELBIiIiIiJSPbjRSfhUlo15MQeZ8cdetiefyDt+ccsG3HlRU+pnvYExZxRnL1rI6644YKK6E4tIhaAkoVQPUUOh9SCXH/CmiGjucxgc//FvZqzay6uLtrMz+QQTr2mPn7d+SIuIiIiIVGux8/MvNLCEOXcqRQ0lIeUUn66J58t1+0g5mQ1ADR8z13ZpxKjoJjRrEJB70pUFL1qwhDkThFFDy+81iYgUwXA4HAWtea5UrFYrgYGBpKamYrFYPB2OVEKfrYnnmfnbsNkddAyvzYcjuxBUy8/TYYmIiJS5yjKPqixxikgVETs/t2SR66/LjtzVfx+FPsuk+JbY7M7HG9Xx55boJlzXNZxAf++Cr+nGqkQRkbLg7jzqnLobT5kyhSZNmuDn50ePHj1Yt25doWNnzJiBYRguNz8/1+SLw+Hg6aefJjQ0FH9/f/r168fOnTvPJTSRc3JTzwg+vbU7gf7exOxPYeg7f7Bp33FPhyUiIiIiIuXNbnOu+iughqCBA4fDweCEt3HYbfRsWpcPbu7Cb49ewu19mhaeIARnQjCyD7S71vlVCUIRqWBKnCScPXs248aN45lnnmHTpk106NCB/v37c+jQoULPsVgsJCYm5t3i4+NdHn/llVd4++23mTp1KmvXrqVmzZr079+fjIyMkr8ikXMU3bw+8+7rTbMGNUmyZnD9B6v5dE08VWCxrYiIiIiIuCt+leu24LOYDAgzjrLsOh++urMX/duEYDaprrmIVH4lThK+8cYb3HHHHYwePZqoqCimTp1KjRo1mDZtWqHnGIZBSEhI3i04+N/27g6Hg8mTJ/Pkk09y5ZVX0r59e2bNmkVCQgLz5s07pxclcq4i69fk+zEXMrBtCNk2B0/N28rDX2/mVJbN06GJiIiIiEg5sJ9IcmtchM+J4geJiFQiJUoSZmVlsXHjRvr16/fvBUwm+vXrx+rVqws9Ly0tjYiICMLDw7nyyivZtm1b3mNxcXEkJSW5XDMwMJAePXoUes3MzEysVqvLTaS0BPh68d6NnfnvFa0xGfDtpoNc/f4q9h09+e8guw3iVsKWb5xf7UoiioiIiIhUZikns/hoxR4e+inRvRMCgosfIyJSiZSou/GRI0ew2WwuKwEBgoOD+eeffwo8p1WrVkybNo327duTmprKa6+9RnR0NNu2baNRo0YkJSXlXePsa55+7GwTJkzgueeeK0noIiViGAZ3XtSMtg0Duf+LP/k70crgd1YyeURHLrWvLbLLmYiIiIiIVB6b96fw6Zp4fticQGaOHRORjPetR7BxlII3ERvO+X9EdDlHKiJSts6pcUlJ9OrVi5EjR9KxY0cuvvhivv32Wxo0aMAHH3xwztccP348qampebf9+/eXYsQi/4puVp8FD1xIx/DaWDNymD3rPRxzbsZxdo0Sa6Kz+1nsfM8EKiIiIiIiTm7s+jmVZWPOhv0Mffd3rpzyB99sPEBmjp2oUAsvX92BOte8joEB+dKEufcHTFTjERGpckq0krB+/fqYzWaSk5NdjicnJxMSEuLWNby9venUqRO7du0CyDsvOTmZ0NBQl2t27NixwGv4+vri6+tbktBFzllooD+z7+rJxB+3ccemMTgczmLFrhyAAQufgNaDNGEQEREREfGE2PlF7vrZnnSCL9ft47s/D5J6KhsAH7OJwe1DualXBJ3Ca2MYBtAYvMyFXGuidhCJSJVUoiShj48PXbp0YenSpQwbNgwAu93O0qVLGTNmjFvXsNlsbNmyhSuuuAKAyMhIQkJCWLp0aV5S0Gq1snbtWu65556ShCdSZny9zDzTPhX+PFbEKAdYDzq7oUX2KbfYREREREQEZ4JwzkicH+D/y5G76+cVy395/1CbvOPhdf25qUcE13UNp25Nn/zXixrqXAAQvwrSkp01CCOitSBARKqsEiUJAcaNG8eoUaPo2rUr3bt3Z/LkyaSnpzN69GgARo4cScOGDZkwYQIAzz//PD179qR58+akpKTw6quvEh8fz+233w44a7+NHTuWF198kRYtWhAZGclTTz1FWFhYXiJSpEJISy5+TEnGiYiIiIhI6bDbnKv+zkoQAhg4sDvg5tSpfGJ6m0svCGVE93AuatEAU/4tQq5MZi0AEJFqo8RJwuuvv57Dhw/z9NNPk5SURMeOHVm4cGFe45F9+/ZhMv1b6vD48ePccccdJCUlUadOHbp06cKqVauIiorKG/PYY4+Rnp7OnXfeSUpKChdeeCELFy7Ez8+vFF6iSClxt3uZupyJiIiIiJSv+FWu24LPYjIgjKOsu6kGtaO6lGNgIiKVh+FwOPJ/1FLJWK1WAgMDSU1NxWKxeDocqarsNpjc1tmkpIBPKO0OSPMNxveRrfj6FLBdQUREpAKqLPOoyhKniJQ/h8NB3LKZNF3xYPGDr/kE2l1b9kGJiFQg7s6jyry7sUiVYTI7Cx4DZ3c5O50yfDTtBq58bw1/J1rLNTQRERERkerm0IkMPlqxh8vfXMF/lxx27yTt+hERKVSJtxuLVGtRQ2H4rHxdzgxLQ2LaPM76dWEcSzrB0Hd/56HLWnLXRc0wF1fnREREREREnDt3imkSkpVj59d/kvlm4wGWbT+Mze78uP6gdxQpXg0IzDmCUcCuHzCcnYkjosvhhYiIVE5KEoqUVCFdzjqZzCyKzuS/321hcWwyryzczqJtyUy6ph2tQ7QtSkRERESkULHz830QjyUMBkzCccEQtiVY+WbjAb6POcjxk9l5QzqG1+baLo0Y2jEMy543crsbG7iWB8r90H7ARHUmFhEpgmoSipQyh8PB3E0HeW7+Nk5k5uBlMri3bzPuu7Q5vl6alIiISMVSWeZRlSVOETkHsfNzk3uuv5o6cpN7L9Z4gk+Otcs7HlTLl6s7N+LaLg1pHlQr/7XyJRsbOhOEUUPL6hWIiFRo7s6jlCQUKSNJqRk89f1WFscmA9CsQU0mXtOebk3qejgyERGRf1WWeVRliVNESiivOWDBnYntDkiiHpfa3qFfVCjXdmnEhc3r42Uuory+G9uWRUSqE3fnUdpuLFJGQgL9+PDmLizcmsTT87ex+3A6101dzU09G/PYgNZY/Lw1gRERERGR6i1+VaEJQgCTAWEcZcNNNQho3dm9a5rMENmnlAIUEak+lCQUKUOGYTCwXSjRzerz8k9/M3vDfj5bs49F25J5t+MBum+fhFFA3RVthRARERGRqi7uSDp/r47hCjfGBmQfLfN4RESqOyUJRcpBYA1vJl3bnis7hfG/77bS8tgyuq2bnFdDOY810VmPZfgsJQpFREREpMo5kpbJgs0JfBeTwOb9KfQ0ZXCFjxsnBgSXeWwiItWdkoQi5Si6WX0WPhBN1mt3QGb+HKGzWLMBC59wdlDW1mMRERERqcjcKJ9zMiuHxbHJfPfnQVbuPILN7iyLbzLAr9mFnDwcjH/GIQwKKpdvOHfbRESXw4sREanelCQUKWe+B9fim3WooAxhLgdYDzonW6qlIiIiIiIVVYGdhJ3lc3JaDeaP3UeZ9+dBFm1L4mSWLW9Ih0aBXNmxIYM7hBJUyw9iX8vtbmzg2uE4d8I8YKI+PBcRKQdKEoqUt7Tk0h0nIiIiIlLeYufnJvZcV/85rIkw52bGmx7h65P/NhppXLcGwzqGcWWnhjRrEOB6raihznI7BSYcJ6oMj4hIOVGSUKS8uVlP5bVVKVwdkkbTsydRIiIiIiKeZLc5E3oFbA82cGB3wEO26fzq35VBHcO5smNDOjeujWEUupXGmQhsPajYrcsiIlJ2lCQUKW8R0c5PRa2JFDSxcgCJjnq8FxfM1DdXcFPPCB74Twvq1nSnorOIiIiISNlyxP+BceaKv7OYDAjjKGtvqolXs7buX9hkVrkdEREPMnk6AJFqx2SGAZNy75z9aarh/G/ABC5pHUKO3cGMVXu5+NVlTFm2i5NZOeUdrYiIiIgIDoeDvxOtvLroH174aplb53idPFTGUYmISGlSklDEE07XXbGEuh63hMHwWYT2up5PbunG57f3ICrUwomMHF5dtJ2LXlnGtN/jyMi25b+m3QZxK2HLN86v9gLGiIiIVBATJ07EMAzGjh3r6VBEqh83540Oh4MdySeYvGQHl725goFvrWTKst3Enqjh3vO4WWZHREQqBm03FvEUN+qu9G5enwX3X8j3mw8yeclO4o+e5PkFsXy0cg/3X9qC67o2wttsKrKznAo9i4hIRbN+/Xo++OAD2rdv7+lQRKqfYuaNDoeDbQlWft6ayM9bk9hzOD1vmI/ZxMWtGjCkfXvsS6dhOlFw+RwwnNeMiC7zlyMiIqXHcDgcBf2rXqlYrVYCAwNJTU3FYrF4OhyRMpFts/PNxgO8vXQniakZgLNL3KSovfTc8BBGvgla7lbm4bOUKBQRkUKV9zwqLS2Nzp0789577/Hiiy/SsWNHJk+eXOHiFKmSCutInDtv/KbZy7yV0JoDx0/lPeZjNnFhi/oMahfKZW2Csfh5n3Utzrqe5qAiIhWNu/MobTcWqSS8zSZu6N6YZY/05enBUdQP8OHAsTQi1j+Po8BPcHOPLXxCW49FRKTCuO+++xg0aBD9+vXzdCgi1UsxHYkdDge9d71GwvF0/LxNDGwbwlsjOrLxqX5Mu6Ub13Rp9G+CEIotn6MEoYhI5aPtxiKVjJ+3mVsvjGRE93AW/ziXsM3HihjtAOtB55ZmdYoTEREP++qrr9i0aRPr168vdmxmZiaZmZl5961Wa1mGJlL1xa9y3WJ8ltMdib+63Ea7C6/A38dc6Ng8bpTPERGRykNJQpFKqoaPF1c2N8NmNwanJZd5PCIiIkXZv38/Dz74IIsXL8bPz6/Y8RMmTOC5554rh8hEqjZrRja/bT/M0TVrucWN8d0b5IA7CcLTTGZ9GC0iUkUoSShSmbnZMe4wtWlQxqGIiIgUZePGjRw6dIjOnTvnHbPZbKxYsYJ3332XzMxMzOZ/ExPjx49n3LhxefetVivh4eHlGrNIhWO3ubVq72DKKZbEJrPk72TW7DlKts1BT5ODW3zceA51JBYRqbaUJBSpzCKinXVfrAV3lrM7IIl6XPxlBv23bOL2Pk3pGF673MMUERH5z3/+w5YtW1yOjR49mtatW/P444+7JAgBfH198fX1Lc8QRSq2IroSOy4YwrYEK4tzE4PbEly35zdtUJOOFwwka+vHeJ9MLqDhHagjsYiIKEkoUpmZzDBgUm5nOYMzE4UODAwDZte9l+wEgwV/JbLgr0S6RtTh9j6RXBYVgtlkeCx0ERGpXmrVqkXbtm1djtWsWZN69erlOy4iZymsK7E1EebczH+9HuPLtI55x00GdImoQ78LgukXFUyzBgHOB5q8WuC8Ma8j8YCJqicoIlKNnVN34ylTptCkSRP8/Pzo0aMH69atK3TsRx99RJ8+fahTpw516tShX79++cbfcsstGIbhchswYMC5hCZS/RTSWc6whGEMn8VDDzzCjw9cyNWdG+JtNtgQf5y7P9tE39eW8f7y3RxNy8x/TbsN4lbClm+cX9UdWURERMQziu1KDPdnf0JNb4P+bYJ59dr2rP9fP76+O5q7Lm72b4IQ1JFYRESKZDgcjoLWmhdq9uzZjBw5kqlTp9KjRw8mT57M119/zfbt2wkKCso3/sYbb6R3795ER0fj5+fHpEmT+O6779i2bRsNGzYEnEnC5ORkpk+fnneer68vderUcSsmq9VKYGAgqampWCyWkrwckarDjRo1ydYMZq3ey+dr95FyMhsAH7OJK9qFcHOvCDo3roPx9w+FbmXRxFFEpOqpLPOoyhKnSGlyOBzs3bCIyB+vL3Zs1k3z8Wl+sXsXdrO2oYiIVA3uzqNKnCTs0aMH3bp149133wXAbrcTHh7O/fffzxNPPFHs+TabjTp16vDuu+8ycuRIwJkkTElJYd68eSUJJY8mjSIlcyrLxg+bE/hsbTx/HUjNO35b3S08eXIizs3KZ8q9p0+YRUSqnMoyj6oscYrkU8KEXFpmDr/vPMKyfw6xbPsheqYv422fd4t/nms+gXbXlmLgIiJSVbg7jypRTcKsrCw2btzI+PHj846ZTCb69evH6tWr3brGyZMnyc7Opm7dui7Hly9fTlBQEHXq1OHSSy/lxRdfpF69eiUJT0Tc5O9jZni3cIZ3C2fz/hQ+WxPPgs0HuC39Axw4yF+q0AEYsPAJaD1InzSLiIiIuKOIZiOnP3h1OBzsPpzO8u2H+PWfQ6zfe4xs27/rOFK865591YKpK7GIiJynEiUJjxw5gs1mIzjY9QdQcHAw//zzj1vXePzxxwkLC6Nfv355xwYMGMDVV19NZGQku3fv5r///S8DBw5k9erV+TrdAWRmZpKZ+W8dNavVmm+MiLinQ3htOoTX5pl2xwj46lgRIx1gPej8JDyyT7nFJyIiIlIpFdJsBGsijjkj2XrhO3yT3oll2w+z79hJlyFN6tXgktZBXNIqiB5NLoN3PwZrYv5rAepKLCIipaVcuxtPnDiRr776iuXLl+Pn55d3fMSIEXl/bteuHe3bt6dZs2YsX76c//znP/muM2HCBJ577rlyiVmkugjIPurWuI3b/qFNo2j8vLWaUERERKRARTQbIbfZSL2Vz/Bp5lvYMeFjNtGjaV0uaRXEJa2DiKxf0/WUAZPUlVhERMpciZKE9evXx2w2k5yc7HI8OTmZkJCQIs997bXXmDhxIkuWLKF9+/ZFjm3atCn169dn165dBSYJx48fz7hx4/LuW61WwsPDS/BKRCQfN7eovLoqhW3rl3B5mxCu7BhGdLN6eJkLaZSuotgiIiJSHcWvct1ifBaTAWEc5YmoYzTp0p/ezetT07eIX81OdyUucOvyRNWMFhGRUlGiJKGPjw9dunRh6dKlDBs2DHA2Llm6dCljxowp9LxXXnmFl156iUWLFtG1a9din+fAgQMcPXqU0NDQAh/39fXF19e3JKGLSHEiop0TzUK2sjgwOOETxAGfDpywZjN30wHmbjpA/QAfBrULZWjHMGd3ZCP3E203avCIiIiIVFgl/LAzK8fOxvjjrNh5GGPrbzzmxlPc2akmtCl6sUWeqKHO2tD6AFZERMpIibcbjxs3jlGjRtG1a1e6d+/O5MmTSU9PZ/To0QCMHDmShg0bMmHCBAAmTZrE008/zRdffEGTJk1ISkoCICAggICAANLS0njuuee45pprCAkJYffu3Tz22GM0b96c/v37l+JLFZEimcxFbmUxAMuw11jR+jI2xB/n+5iD/LQlkSNpWcxcHc/M1fE0rO3PkA5hjAj4k4il92AUUIOHOSPVJVlEREQqNjcbjuw5ks7KHYdZufMIq/cc5WSWDYCeJl/wceN5StpsxGRWbWgRESkzhsPhKKhQRpHeffddXn31VZKSkujYsSNvv/02PXr0AKBv3740adKEGTNmANCkSRPi4+PzXeOZZ57h2Wef5dSpUwwbNow///yTlJQUwsLCuPzyy3nhhRfyNUgpjLutnEXEDQVOihsWuJUl22bn911H+CEmgUXbkkjPsmHCzu++DxBiHKPgTci5xbXHbtEn3yIiFUBlmUdVljilCiis4Ujux59ru77JN6c6s2rXERJSM1xG1A/w4cLm9bmoRV2uXNYfc1pSAddxXkvzIRERKS/uzqPOKUlY0WjSKFLKzqGW4KksG7/+c4ida39m7MGHin+OUQv0SbiISAVQWeZRlSVOqeTsNpjcttB6gnYHJFGPC89oONK1SR36tGjARS3rc0GIBZPpjNIrc0bmnllAsxHtrBARkXLi7jyqXLsbi0glcQ5bWfx9zAxqHwqGBeYWP371X7E0q9+NoFp+xQ8GNUERERGRMndq10r83Wg48kKnVBp16k+3JnWo4VPIr1RqNiIiIpWMkoQiUrrcrK3z1lora1YvpV3DQC5pHcSlrYNo3zDw30/fz6QmKCIiIuKOEn6omJljI2ZfCn/sPsrq3UdoeGAxk934DenGKD9o2aD4gWo2IiIilYiShCJSutzskpxRrzscTGPLwVS2HEzl7aU7qVfTh4tbNaBvqyCim9WjfoBv4XWB1ARFREREzuTGh4oZ2TZi9qewLu4Ya+OOsjH+OBnZ9rzhZlNt956rJA1H1GxEREQqCdUkFJHS52YNnsMnMlm+/RDLtx9mxY7DnMjMcblMm5CafJl+B7WyD1HA+kJU9FtE5PxVlnlUZYlTPKSQDxUduTOIH1pN5DNrB2L2p5CVY3cZUz/Ah17N6hPdrB69I+sQ/ml3jEI+7NTcQ0REKiPVJBQRz3GzBk+DWr5c1zWc67qGk22zszH+OMv+OcTKnUeITbRS69B6LD6HingiB1gPOrfw6BN6ERGR6sluc845CkjqGTiwO6DrP68wNrfZSP0AX3o0rUvPyLp0j6xHy+AADOOMjyMHTMpNOBoU+GHngIlKEIqISJWkJKGIlI0S1uDxNpvo2bQePZvWYzxwNC2TvcsTYEPxT7Vj107CG0bj7+PGhF0NUERERCqW8/jZnHoym53rfqarG81GPro4iyZd+9O0fk3XpODZ1HBERESqKSUJRaTsnEcNnnoBvtRr09qtJOHTy46yYdki2jYMpEdkXbo1qUvXJnWoXcPHdaAaoIiIiFQsJfjZ7HA4OHD8FBvij7Fh73E2xh9ne/IJhhhr6XrWj/yC/KeRAxoEuBeXGo6IiEg1pCShiFRcbjRBSfFqwH6fDuRYs4nZn0LM/hQ+WLEHgBZBAXRqXJuO4XXok7OaRovvwlADFBERkYqhmOZkOdfOZFvgxWyIP87G3MTgoROZ+S5jrh0Cp9x4vpI0GwE1HBERkWpHjUtEpGJzowmK44IhHDh+ivV7j7Eu7hjr9h5jz+H0vJEm7Pzu+wAhHMNU4O6icyxCrq3LIlIFVJZ5VGWJU9xkt8Hktq4rCM98GEh21KN3bh3B07zNBm3CAukaUYeuTerQOaIOQTW9c6+lZiMiIiIFUeMSEaka3KgLZADhdWsQXrcGV3duBMCRtExi9qXw5/7jZO78jbAjx4p4EmcDlI0rfiSs02WEWPyKrlUE2rosIiJyHh+WOeL/wCiqjiAQahzlEr9dOJpcSJeIOnSNqEOH8Nr4eRfwHGo2IiIict6UJBSRiu8c6gLVD/ClX1Qw/aKCIWwrzC3+aWb+spb5C83Uq+lDVJiFNmGBtG3o/BpRtwam08sQi9kepa3LIiJS5ZXwwzJrRjZ/7U/lz33HidmfQlD8Uia48TQfXd0IU/tuxQ9UsxEREZHzpiShiFQO51MXyM0aRH51wjAfNziansXKnUdYufPIv5fw9aJ1SC1aBdfgv9sfoQYO8q81dAAGLHzCmdTU1mUREamKivmwzHbtTLbXvYSY/Sl5ScFdh9M4s8hRT1MAuNFsxFQrxP241GxERETkvChJKCJVXzENUE7XKnpl7N08b4PtSSfYmpDKtgQr2w6m8nfSCdIyc9gQfxyv/X9Q0ye5iCdzbl0mfpX7SU1tXRYRkcrCbnP+zCrw56kDO3Do64cYfFYtQYDGdWvQMby2s6lYwx445k7DOFFMHcGI6JLFp2YjIiIi50xJQhGp+kxmt2sV+ZmgQ3htOoTXzhuRbbOz+3Aa25NOYNq2C3YW/5RPfbaUXcHeNG1Qk6YNAmjaoCbN6gfQsI4/5jO7p5T21mWtSBQRkaKc58+JkztXUMONWoIX++4kKzyaTuF16Bhem46Na1M/wNd18EDVERQREalIlCQUkerhPGoVeZtNtA6x0DrEAoGd3UoS7jxVkzV7jrJ6z1GX4z5eJprUq0HT+gE0re/HmM2P4F9aW5e1IlFERIpSwp8TJ7NyiE2wsvlAKlsOpPDXwVTaHl3C225sE/7kmnBM7XsWPUh1BEVERCoUJQlFpPoojVpFbmxdttcK47FrRhN3NIM9R9LYczidPYfTiTuaTlaOnR3JaexITqOnKZYabmxdPrh5CfXa9iu4m+NpZdFMRasSRUSqjmJ+TmRdM4NtgRez5WAqfx1IZcuBVHYeOoH9rOFBptpuPZ3btQRVR1BERKTCUJJQRKqX861V5MbWZdPAiXRuUp/OTVxPtdkdJKScYvdhZ+IwYOd2iC/+KSd9vYL5s3MItvjSuG4NwuvUoGEdf0ID/Qmr7UdDiw/Nfn4cUyH1oc6pmYpWJYqIVCzn88GNG3UEj3wzjmsKqCMYbPGlXcPatG8USPtGgbQLvQQ+nlZsnd8S1RJUHUEREZEKQUlCEZGSOsftUWaTQXjdGoTXrUHfVkDDbjCz+Kc74V0PMiHZmkmyNZP1e4+7PN7TFMtXPoXXhzq9IjFrz+/4NL+4+CdUnUQRkYrlPD64ScvM4eCmX2hVTB3BMOMo/WrsIiu8N+0b1aZ9w0DaNQok2OKX/wQ36/yKiIhI5aIkoYjIuSinrctYwpj24AOkZNjZd+xk3i0x9RSJKRkcTDlFRMoJt57ukWm/sMIvk6BavgRb/GiQ+zWoli9BtfwItvgSVNOb8J8fxyitVYmlvSJRCUcRqW5K8MHNkbRMtiVY2ZaQyrYEK7EJVvYeTWeIscqtOoIfDGuE0b578QNVS1BERKRKUpJQRORclcPWZQZMxDB7Uacm1Knp49J1OU+cATPfKvbpDlGblJPZpJzMZkdyWoFj3F2VuH3dInybX0zdAB9q+XphGPlbr5T6ikQlHEWkuilmm7ADgxPzHuGhtUFsTUwj2ZpZ4GVyagZBTvFPZ7hbRxBUS1BERKQKUpJQRMSTSmM1hhsrEh2WMD64836S07M5ZM0k2ZrBoRPOr4dP/Hs/7ESqW2FPmf8H83Or2fuYTdQL8KFuTR/qBfhSv6YP9WqYeXDrw9QspHOzAwOjpCsSK2rCUclGESnM+f77EL/K9d+psxg4sGQlk75zJcn2KAwDIuvXJCrUQpuwQNqEWWgTZqFejQEweWrp1hEE1RIUERGpYpQkFBHxtPNdjeHGikRjwEQCA/wIDPCjZXCtQi/liDPDzHeKfUpHrWBqnjKTnmUjy2YnMTWDxNSMvMd7mmIJ8DlU6PlG7orER1+fSrylM7X9valdw5vaNXwIPP1nfx9q1/Cmlo9B1E+PYS4k4XhOW6BLK+FYUVc3KnEp4nkl/PfBbnew//hJ/kk6wT+JJ/gnyUrY/mU85cZT3ds1gEe79KJ1iIWavoVM71VHUERERIqhJKGISEVwvqsxSqk+lBHR2606ie+MvQ9MZk5l2Tiansmx9CyOpmVxJM355+B9u2B38c+XeTyBdUebFDnGuQU6sYgRzoTj+7M+5VC9btTy88bi50UtPy9q+XkT4Hv6z17U8DIIya25eN4Jx4q6urGiJi5L81pKgkpFV8y/D+nDprPVcpEzIZjkTAhuTzrBySyby/CeJn9wo5bgRZ3aQkTdogepjqCIiIgUw3A4HAX9FlipWK1WAgMDSU1NxWKxeDocERHPKY3kSd4vt1DgahN3kl9xK2Hm4GKfaku/z4m3dCblZDapp7JJOZnF8dy6iamnskg5mU3P9GW8YHuz2Gs9kDWG+fait8o5E44vFnutd8LfJKFON2r6mKnh6+X61ceLmt7Q4/u+eJ9MLCDZCHlb98ZuOb/VjSX5f16a1znzeqWVcKzqSdCKmEx1U3nNoyZMmMC3337LP//8g7+/P9HR0UyaNIlWrVpVqDjP+/+/3QaT2xa6TdgOJDnqcWHmW9gxuTzm42WiZXAArYItXBBai9ZBNen1Q1/MacVsE3b335rSeH0iIiJS6bg7jzqnlYRTpkzh1VdfJSkpiQ4dOvDOO+/QvXvhndC+/vprnnrqKfbu3UuLFi2YNGkSV1xxRd7jDoeDZ555ho8++oiUlBR69+7N+++/T4sWLc4lPBGR6qs06kOVU51ELGG0ix5Iu+J+OY0zwczik4SXdGtHmF8z0jKzOZGRk3v798/pWTmEZrlXc3Hn7t3MtwcX+nhPUyx93FjdeO/L7xLr2x4/bzN+3mb8vc34eZvw9zHj52XGz8eMvxke3PowtYqo35i54DH+8u2Fj483vl4mfLxM+JhN+HqZ8PUyO++bHJiLaHDg8W3ZpXGt6rB6s7SToBXIb7/9xn333Ue3bt3Iycnhv//9L5dffjmxsbHUrFnT0+E5neP//4xsG7sPp7HrUBqndixnRBF1BE1AmHGUK2rFkdHIuUW4dWgtWofUokm9mniZXROH2Ep5m7DqCIqIiEghSryScPbs2YwcOZKpU6fSo0cPJk+ezNdff8327dsJCgrKN37VqlVcdNFFTJgwgcGDB/PFF18wadIkNm3aRNu2bQGYNGkSEyZMYObMmURGRvLUU0+xZcsWYmNj8fPzKzYmrSQUESkD57vapDRWJJ6OY3Lb4gvuu7OSxs0Vjj92/ojdNTuRnpXDyUyb69csG91OLOWJk68Ve53SXN04IutJ1tijzvs6L9V/ld0BnfD1MuFtNuFlNvA2mfD2MvAymfA2G/iYHNz95zACsg4VuFLSgcEp/2BWXPEr3l5eeJmd53mbTXiZnF9PX9vHcBA2oxumtPNcdVnM6qwqsXqztFeCuslT86jDhw8TFBTEb7/9xkUXXVTs+DKP043//xktBrH7cBo7k9PYeegEO5LT2Jl8gn3HTpLby4mhplW87fNu8c93zSfQ7lr3Y8uXvGyobcIiIiLiFnfnUSVOEvbo0YNu3brx7rvOyY/dbic8PJz777+fJ554It/466+/nvT0dBYsWJB3rGfPnnTs2JGpU6ficDgICwvj4Ycf5pFHHgEgNTWV4OBgZsyYwYgRI0rtxYqISDkrrV9sK1rC0c1kY9yg2Rxp0J2MbBunsmxk5NjJyLKRkWPLPWYnMuknhu56uthrveD7MAuNC8nMsZOV42wYk5ljx1HCxER5Ji5Lcq2Rtqf509QWk8nAy2RgPuPmZTLoZN/Km6eeLPY6rwS/xs6anQq8xuk/exsOxm69Bkt2YUlQSPMN5oteCzCZvDAMMJsMTIaByWRgMsBsGJixM+jXy/HPSC40mZrpH8LqIcsweXnlnWcYRu71yL2egdlhp9XsaLzTS2kLewl4ah61a9cuWrRowZYtW/I+OC5Kmcbpxhbhw0Z9emdMJsdhKnBMoL83LYMDuLzmTu7Y/UDxzzlqQclW9GmbsIiIiJyjMtlunJWVxcaNGxk/fnzeMZPJRL9+/Vi9enWB56xevZpx48a5HOvfvz/z5s0DIC4ujqSkJPr165f3eGBgID169GD16tVuJQlFRKSCOt/OzWdepzQK7rvRCdqtrXtubqeO7HIZkcWubuwMu4oP/akRl/DUWQkFh8NBjt1BVo4d+54aMLv4JOHwS7pyYe32ZObYyLI5yLHZ866RY7eTY3PQ8vBOiCs+ph71sznlE0i2zUF27nWybXbnn3OPNbRbi78QUNt2jBPZOYU+3t6U5FYDhwP797LYHlbkmJ6mWAKL7L4NtTKTWfbL98Wu3rzOJ7mI6zjwO5XIB5995mYytfgt7MSvqhJbRe12O2PHjqV3796FJggzMzPJzMzMu2+1uve9dE7iVxWxStW5RTjYcYSuxj/849+BlkG1aB4cQMugAFoGO//cIMAXwzDA3gMmTyz+w4iIopP1+YPQNmEREREpWyVKEh45cgSbzUZwsGudpuDgYP75558Cz0lKSipwfFJSUt7jp48VNuZs5Tpp/P/27j226vr+4/irFE4L/toCAXqBrhSHddwHs/2VwYBRbmsc/WW/UcxksMhcTFnWOXVuUTtmMspGxuZCpi4IdQ4qmwKJcxWsFCICJlAj9UKAVfHWMsyw5ea05/37g1/P16+9nnJOz+X7fCRN2+/5nE8/33c+fPPi3dPzBQBcm1D9xzaaGo6hajZKvW44dtZQSEhICPyZr/K+1qt5Zs3/Zi/+LPtcr5qEP/6f2fpx7qwe5hoiVT3U41z3LZ+nO7MK9anf1PaZj0/9fvnNNPg9SXt6boKWfG26/nvYZLX5/a65Pvv1+LOnpZM9n19xboIyU0erzW/ym8lMga/9ZrqptUE61/M8M4Z/rJZBqYHn+U1XP/uvft3mN+V9elHqukfquNB1UzKWlJWVqaGhQS+++GKXY9atW6e1a9f2z4J6WddHS8Yo5aYFV5uBXQnl9QEAAKAf9enGJZHWr6ERABA9oqnhGG2vboySxmVf5xo1aV73a8teLB3ueZ6vLyzpRRO0uVdNwhVFBVqRO62bea5IVT3Pc/e35ujunvZt46Be3aBH/9X1DXVixZo1a/TMM8/owIEDGjNmTJfjfvazn7n+GqWlpUXZ2dnhWVQv65o6cozUXYOwXaiuDwAAAP0oqCbhiBEjlJiYqOZm929bm5ublZGR0elzMjIyuh3f/rm5uVmZmZmuMdOmTet0zn4NjQCA+BSqO0FHy6sbQzlPKBuO8dwEjUAzNeg/UY0iZqYf/vCH2rlzp+rq6pSbm9vt+KSkJCUlJfXP4sJR/1BdHwAAAPpJ5++83AWfz6cZM2aotrY2cMzv96u2tlaFhYWdPqewsNA1XpL27t0bGJ+bm6uMjAzXmJaWFh05cqTLOZOSkpSamur6AAAgItqbjZP/9+rnvjYAJnxTKm+4ejODb22++rn8ePCvOArlPMsel1Iz3cdTs4K/y26o5grVPO0NR0nqcJuQPjQur3WeUM8VpcrKyvTEE09o27ZtSklJUVNTk5qamnT58uVILy189Q/V9QEAAKAfBH134yeffFIrV67UI488ovz8fP3ud7/Tjh079Oabbyo9PV3f/e53NXr0aK1bt06S9NJLL2nOnDmqrKxUcXGxqqur9atf/UrHjh0LvFH1+vXrVVlZqaqqKuXm5ur+++/Xq6++qtdff13Jyck9rom7GwMAECahvKNqqOYK1TyhvPt2KOYJ9Vy91F85qqv38duyZYtWrVrV4/P7ZZ0RqD8AAEC4heXuxpJUWlqqf/3rX3rggQfU1NSkadOmqaamJnDjkTNnzmjAAOcFijNnztS2bdt033336ec//7nGjx+vXbt2ue5kd8899+jixYu6/fbbdf78ec2aNUs1NTW9ahACAIAwCuUdVUM1VzS9N2Uo5wn1XFEmyN9LR0Yc1x8AAKAnQb+SMBrxSkIAAIC+iZUcFSvrBAAAiDa9zVFBvSchAAAAAAAAgPhDkxAAAAAAAADwOJqEAAAAAAAAgMfRJAQAAAAAAAA8jiYhAAAAAAAA4HE0CQEAAAAAAACPGxjpBYSCmUm6ektnAAAA9F57fmrPU9GKvAcAANA3vc17cdEkbG1tlSRlZ2dHeCUAAACxqbW1VWlpaZFeRpfIewAAANemp7yXYNH+a+Ne8Pv9ev/995WSkqKEhISw/qyWlhZlZ2frnXfeUWpqalh/ViygHg5q4UY9HNTCjXo4qIUb9XD0Zy3MTK2trcrKytKAAdH7TjTkvcihHg5q4UY9HNTCjXo4qIUb9XBEY96Li1cSDhgwQGPGjOnXn5mamur5Df1Z1MNBLdyoh4NauFEPB7Vwox6O/qpFNL+CsB15L/Koh4NauFEPB7Vwox4OauFGPRzRlPei99fFAAAAAAAAAPoFTUIAAAAAAADA42gSBikpKUkVFRVKSkqK9FKiAvVwUAs36uGgFm7Uw0Et3KiHg1pEFvV3ox4OauFGPRzUwo16OKiFG/VwRGMt4uLGJQAAAAAAAAD6jlcSAgAAAAAAAB5HkxAAAAAAAADwOJqEAAAAAAAAgMfRJJS0adMmjR07VsnJySooKNDLL7/c7fi//vWvuvHGG5WcnKzJkyfr2WefdT1uZnrggQeUmZmpwYMHq6ioSCdPngznKYRMMLX405/+pNmzZ2vYsGEaNmyYioqKOoxftWqVEhISXB+LFy8O92mETDD12Lp1a4dzTU5Odo3xyt6YO3duh1okJCSouLg4MCZW98aBAwd08803KysrSwkJCdq1a1ePz6mrq9P06dOVlJSkL37xi9q6dWuHMcFeh6JFsPV4+umntWDBAo0cOVKpqakqLCzUc8895xrzi1/8osPeuPHGG8N4FqERbC3q6uo6/XfS1NTkGueVvdHZNSEhIUETJ04MjInVvbFu3TrddNNNSklJ0ahRo1RSUqITJ070+Lx4zhuRQN5zkPfcyHsO8t5V5D038p6DvOdG3nPES97zfJPwySef1J133qmKigodO3ZMU6dO1aJFi3T27NlOx7/00ku65ZZbdNttt6m+vl4lJSUqKSlRQ0NDYMyvf/1rPfTQQ3r44Yd15MgRXXfddVq0aJGuXLnSX6fVJ8HWoq6uTrfccov27dunQ4cOKTs7WwsXLtR7773nGrd48WJ98MEHgY/t27f3x+lcs2DrIUmpqamuc3377bddj3tlbzz99NOuOjQ0NCgxMVHf/va3XeNicW9cvHhRU6dO1aZNm3o1vrGxUcXFxZo3b55eeeUVlZeXa/Xq1a6g1Je9Fi2CrceBAwe0YMECPfvsszp69KjmzZunm2++WfX19a5xEydOdO2NF198MRzLD6lga9HuxIkTrnMdNWpU4DEv7Y3f//73rjq88847Gj58eIfrRizujf3796usrEyHDx/W3r179cknn2jhwoW6ePFil8+J57wRCeQ9B3nPjbznIO85yHtu5D0Hec+NvOeIm7xnHpefn29lZWWB79va2iwrK8vWrVvX6fhly5ZZcXGx61hBQYH94Ac/MDMzv99vGRkZ9pvf/Cbw+Pnz5y0pKcm2b98ehjMInWBr8XmffvqppaSkWFVVVeDYypUrbenSpaFear8Ith5btmyxtLS0Lufz8t7YuHGjpaSk2IULFwLHYnlvtJNkO3fu7HbMPffcYxMnTnQdKy0ttUWLFgW+v9b6Rove1KMzEyZMsLVr1wa+r6iosKlTp4ZuYRHQm1rs27fPJNm///3vLsd4eW/s3LnTEhIS7K233goci4e9YWZ29uxZk2T79+/vckw8541IIO85yHtu5D0Hea9z5D038p6DvOdG3nOL1bzn6VcS/uc//9HRo0dVVFQUODZgwAAVFRXp0KFDnT7n0KFDrvGStGjRosD4xsZGNTU1ucakpaWpoKCgyzmjQV9q8XmXLl3SJ598ouHDh7uO19XVadSoUcrLy9Mdd9yhDz/8MKRrD4e+1uPChQvKyclRdna2li5dqtdeey3wmJf3xubNm7V8+XJdd911ruOxuDeC1dM1IxT1jWV+v1+tra0drhsnT55UVlaWxo0bp+985zs6c+ZMhFYYftOmTVNmZqYWLFiggwcPBo57fW9s3rxZRUVFysnJcR2Ph73x0UcfSVKHff9Z8Zo3IoG85yDvuZH3HOS9a0Pe6x55j7zXFfJe9OUNTzcJz507p7a2NqWnp7uOp6end3iPgHZNTU3djm//HMyc0aAvtfi8n/70p8rKynJt4MWLF+vxxx9XbW2t1q9fr/3792vJkiVqa2sL6fpDrS/1yMvL02OPPabdu3friSeekN/v18yZM/Xuu+9K8u7eePnll9XQ0KDVq1e7jsfq3ghWV9eMlpYWXb58OST/9mLZhg0bdOHCBS1btixwrKCgQFu3blVNTY3++Mc/qrGxUbNnz1Zra2sEVxp6mZmZevjhh/XUU0/pqaeeUnZ2tubOnatjx45JCs11OVa9//77+sc//tHhuhEPe8Pv96u8vFxf/epXNWnSpC7HxWveiATynoO850bec5D3rg15r3vkPfJeZ8h70Zk3BoZlVnhOZWWlqqurVVdX53rz5uXLlwe+njx5sqZMmaLrr79edXV1mj9/fiSWGjaFhYUqLCwMfD9z5kx96Utf0iOPPKIHH3wwgiuLrM2bN2vy5MnKz893HffS3kDntm3bprVr12r37t2u92VZsmRJ4OspU6aooKBAOTk52rFjh2677bZILDUs8vLylJeXF/h+5syZOn36tDZu3Kg///nPEVxZ5FVVVWno0KEqKSlxHY+HvVFWVqaGhoaYeG8d4PPIe+S9rpD30BXyHnmvK+S96OTpVxKOGDFCiYmJam5udh1vbm5WRkZGp8/JyMjodnz752DmjAZ9qUW7DRs2qLKyUnv27NGUKVO6HTtu3DiNGDFCp06duuY1h9O11KPdoEGD9OUvfzlwrl7cGxcvXlR1dXWvLuaxsjeC1dU1IzU1VYMHDw7JXotF1dXVWr16tXbs2NHhJfafN3ToUN1www1xtzc6k5+fHzhPr+4NM9Njjz2mFStWyOfzdTs21vbGmjVr9Mwzz2jfvn0aM2ZMt2PjNW9EAnnPQd5zI+85yHvXhrzXOfJe58h75L120Zg3PN0k9Pl8mjFjhmprawPH/H6/amtrXb8h/KzCwkLXeEnau3dvYHxubq4yMjJcY1paWnTkyJEu54wGfamFdPVOOw8++KBqamr0la98pcef8+677+rDDz9UZmZmSNYdLn2tx2e1tbXp+PHjgXP12t6Qrt7O/eOPP9att97a48+Jlb0RrJ6uGaHYa7Fm+/bt+t73vqft27eruLi4x/EXLlzQ6dOn425vdOaVV14JnKcX94Z09c5wp06d6tV/NmNlb5iZ1qxZo507d+qFF15Qbm5uj8+J17wRCeQ9B3nPjbznIO9dG/JeR+S9rpH3yHvtojJvhOV2KDGkurrakpKSbOvWrfb666/b7bffbkOHDrWmpiYzM1uxYoXde++9gfEHDx60gQMH2oYNG+yNN96wiooKGzRokB0/fjwwprKy0oYOHWq7d++2V1991ZYuXWq5ubl2+fLlfj+/YARbi8rKSvP5fPa3v/3NPvjgg8BHa2urmZm1trbaXXfdZYcOHbLGxkZ7/vnnbfr06TZ+/Hi7cuVKRM4xGMHWY+3atfbcc8/Z6dOn7ejRo7Z8+XJLTk621157LTDGK3uj3axZs6y0tLTD8VjeG62trVZfX2/19fUmyX77299afX29vf3222Zmdu+999qKFSsC4//5z3/akCFD7O6777Y33njDNm3aZImJiVZTUxMY01N9o1mw9fjLX/5iAwcOtE2bNrmuG+fPnw+M+clPfmJ1dXXW2NhoBw8etKKiIhsxYoSdPXu2388vGMHWYuPGjbZr1y47efKkHT9+3H70ox/ZgAED7Pnnnw+M8dLeaHfrrbdaQUFBp3PG6t644447LC0tzerq6lz7/tKlS4ExXsobkUDec5D33Mh7DvKeg7znRt5zkPfcyHuOeMl7nm8Smpn94Q9/sC984Qvm8/ksPz/fDh8+HHhszpw5tnLlStf4HTt22A033GA+n88mTpxof//7312P+/1+u//++y09Pd2SkpJs/vz5duLEif44lWsWTC1ycnJMUoePiooKMzO7dOmSLVy40EaOHGmDBg2ynJwc+/73vx8TF7t2wdSjvLw8MDY9Pd2+8Y1v2LFjx1zzeWVvmJm9+eabJsn27NnTYa5Y3hv79u3rdN+3n//KlSttzpw5HZ4zbdo08/l8Nm7cONuyZUuHeburbzQLth5z5szpdryZWWlpqWVmZprP57PRo0dbaWmpnTp1qn9PrA+CrcX69evt+uuvt+TkZBs+fLjNnTvXXnjhhQ7zemVvmJmdP3/eBg8ebI8++minc8bq3uisDpJc1wKv5Y1IIO85yHtu5D0Hee8q8p4bec9B3nMj7zniJe8l/P/JAAAAAAAAAPAoT78nIQAAAAAAAACahAAAAAAAAIDn0SQEAAAAAAAAPI4mIQAAAAAAAOBxNAkBAAAAAAAAj6NJCAAAAAAAAHgcTUIAAAAAAADA42gSAgAAAAAAAB5HkxAAAAAAAADwOJqEABBmc+fOVXl5eaSXAQAAgDAh7wGIBzQJAQAAAAAAAI9LMDOL9CIAIF6tWrVKVVVVrmONjY0aO3ZsZBYEAACAkCLvAYgXNAkBIIw++ugjLVmyRJMmTdIvf/lLSdLIkSOVmJgY4ZUBAAAgFMh7AOLFwEgvAADiWVpamnw+n4YMGaKMjIxILwcAAAAhRt4DEC94T0IAAAAAAADA42gSAgAAAAAAAB5HkxAAwszn86mtrS3SywAAAECYkPcAxAOahAAQZmPHjtWRI0f01ltv6dy5c/L7/ZFeEgAAAEKIvAcgHtAkBIAwu+uuu5SYmKgJEyZo5MiROnPmTKSXBAAAgBAi7wGIBwlmZpFeBAAAAAAAAIDI4ZWEAAAAAAAAgMfRJAQAAAAAAAA8jiYhAAAAAAAA4HE0CQEAAAAAAACPo0kIAAAAAAAAeBxNQgAAAAAAAMDjaBICAAAAAAAAHkeTEAAAAAAAAPA4moQAAAAAAACAx9EkBAAAAAAAADyOJiEAAAAAAADgcTQJAQAAAAAAAI/7P8fGWg712rKdAAAAAElFTkSuQmCC",
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABQkAAAGGCAYAAADYVwfrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAACVcElEQVR4nOzdd3iTZdvH8e+ddEJJmV1QStmWvaGIog8KyBAX4quCuAcq4uR53Atw4kBxsZygKCIqCAiCssEiUGWWMjqYbWihK8n7R0oldKXQNh2/j0eOkjvXfeeMNPTqmes6T8PhcDgQERERERERERGRasvk6QBERERERERERETEs5QkFBERERERERERqeaUJBQREREREREREanmlCQUERERERERERGp5pQkFBERERERERERqeaUJBQREREREREREanmlCQUERERERERERGp5pQkFBERERERERERqeaUJBQREREREREREanmlCQUEfGwvn370rZtW0+HISIiIiLloEmTJgwePNjTYYiI5KMkoYhICbz88sv07NmTBg0a4OfnR4sWLRg7diyHDx/2dGjnzW63M2PGDIYOHUp4eDg1a9akbdu2vPjii2RkZHg6PBEREZFyN3v2bG666SZatGiBYRj07dvX0yGVmpMnTzJlyhQuv/xyQkNDqVWrFp06deL999/HZrN5OjwR8QAvTwcgIlKZbNy4kY4dOzJixAhq1arF33//zUcffcSPP/5ITEwMNWvW9HSI5+zkyZOMHj2anj17cvfddxMUFMTq1at55plnWLp0Kb/++iuGYXg6TBEREZFy8/7777Nx40a6devG0aNHPR1OqdqzZw/3338///nPfxg3bhwWi4VFixZx7733smbNGmbOnOnpEEWknClJKCJSAnPnzs13rFevXlx77bX88MMPjBgxwgNRlQ4fHx/++OMPoqOj847dcccdNGnSJC9R2K9fPw9GKCIiIlK+Pv30Uxo2bIjJZKpy5WFCQkLYsmULbdq0yTt21113ceuttzJ9+nSeeuopmjdv7sEIRaS8abuxiFR48fHx3HvvvbRq1Qp/f3/q1avHddddx969ez0dGuCsKwOQkpJyXtfZuHEj0dHR+Pv7ExkZydSpU88/uBLw8fFxSRCedtVVVwHw999/l2s8IiIiUrVV9DkeQHh4OCZT2fza/Msvv9CxY0f8/PyIiori22+/LZPnKUz9+vVdEoSnae4nUn1pJaGIVHjr169n1apVjBgxgkaNGrF3717ef/99+vbtS2xsLDVq1Cjy/OPHj7tVV6VGjRrFXgvA4XBw9OhRcnJy2LlzJ0888QRms/m8atQcP36cK664guHDh3PDDTcwZ84c7rnnHnx8fLj11luLPDc1NZXs7Oxin8PPz4+AgIASx5aUlAQ4J5IiIiIipaWizfHK086dO7n++uu5++67GTVqFNOnT+e6665j4cKFXHbZZUWeW9avW3M/kerLcDgcDk8HISJSlFOnTuHv7+9ybM2aNfTq1YtZs2Zx8803F3l+kyZNiI+PL/Z5nnnmGZ599tlixyUlJREaGpp3v1GjRrz++usMHz682HML0rdvX3777Tdef/11xo0bB0BWVhY9evQgISGBAwcO4O3tXez5xRk1ahQzZswocXyXXXYZ69atIz4+ntq1a5f4fBEREZGCVLQ5XnHatm1L/fr1Wb58+Xld53Tcc+fO5eqrrwbAarXSunVrQkJC2LRpk1vnF+dcXndWVhadOnXi1KlT7NixAy8vrSsSqU70jheRCu/MyWN2djZWq5XmzZtTu3ZtNm3aVOwE8vPPP+fUqVPFPk/Tpk3diqdu3bosXryYjIwM/vzzT7799lvS0tLcOrcwXl5e3HXXXXn3fXx8uOuuu7jnnnvYuHEjPXv2LPTc119/nePHjxf7HGFhYSWO6+WXX2bJkiW89957ShCKiIhIqapoc7zyFBYWlretF8BisTBy5EgmTZpEUlISISEhhZ5blq97zJgxxMbG8uOPPypBKFIN6V0vIhXeqVOnmDBhAtOnT+fgwYOcuQA6NTW12PN79+5dqvH4+PjkNfAYPHgw//nPf+jduzdBQUEMHjz4nK4ZFhaWrzNyy5YtAdi7d2+RScIuXbqc03MWZ/bs2Tz55JPcdttt3HPPPWXyHCIiIlJ9VZQ53rFjx8jKysq77+/vT2BgYKlcuzDNmzfHMAyXY2fO/YpKEpb23Pa0V199lY8++ogXXniBK664okyeQ0QqNiUJRaTCu//++5k+fTpjx46lV69eBAYGYhgGI0aMwG63F3v+4cOH3arbEhAQcE41+6KjowkNDeXzzz8/5yTh+Th7YluYkkx4Fy9ezMiRIxk0aFC5N1ARERGR6qGizPGuvvpql9It51qipbyUxdx2xowZPP7449x99908+eST5xuiiFRSShKKSIX3zTffMGrUKF5//fW8YxkZGW53E+7WrVuZ16vJyMhw6xPvwiQkJJCenu6ymnDHjh3Av92TC3P2xLYw7k54165dy1VXXUXXrl2ZM2eOtpqIiIhImagoc7yzS7ecS4mWktq1axcOh8NlNaG7c7/Sntt+//333H777Vx99dVMmTKl2PEiUnXpNz8RqfDMZjNn91h655133PoEFUqvbkt6ejqGYeTrEjd37lyOHz9O165d3YqnIDk5OXzwwQcujUs++OADGjRoUOx24tKsSfj3338zaNAgmjRpwoIFC/IVExcREREpLRVljldWpVuKkpCQwHfffefSuGTWrFl07NixyK3GULo1CVesWMGIESO46KKL+PzzzzGZTO69ABGpkpQkFJEKb/DgwXz66acEBgYSFRXF6tWrWbJkCfXq1XPr/NKq27Jz50769evH9ddfT+vWrTGZTGzYsIHPPvuMJk2a8OCDD7qMP/0p8N69e4u9dlhYGJMmTWLv3r20bNmS2bNnExMTw4cfflhkZ2MovYntiRMn6N+/P8ePH+fRRx/lxx9/dHm8WbNm9OrVq1SeS0RERKSizPGKsmLFClasWAE4t/mmp6fz4osvAnDRRRdx0UUX5Y01DIOLL77Yre7HLVu25LbbbmP9+vUEBwczbdo0kpOTmT59erHnltbrjo+PZ+jQoRiGwbXXXsvXX3/t8nj79u1p3759qTyXiFQOShKKSIX31ltvYTab+fzzz8nIyKB3794sWbKE/v37l2scjRo14pprruHXX39l5syZZGdnExERwZgxY/jf//6Xb0Kbnp5O8+bN3bp2nTp1mDlzJvfffz8fffQRwcHBvPvuu9xxxx1l8VIKdPToUfbv3w/AE088ke/xUaNGKUkoIiIipaaizPGK8uuvv/Lcc8+5HHvqqacA53be00nCtLQ0AEJDQ926bosWLXjnnXd49NFH2b59O5GRkcyePbtcX3tcXFxeuZz77rsv3+PPPPOMkoQi1YzhOHt9t4iInLfY2FjatGnDggULGDRokKfDEREREZEy9NNPPzF48GA2b95Mu3btPB2OiMg5UcEBEZEysGzZMnr16qUEoYiIiEg1sGzZMkaMGKEEoYhUalpJKCIiIiIiIiIiUs1pJaGIiIiIiIiIiEg1pyShiIiIiIiIiIhINackoYiI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MEApT1onWon1yPidqT6TCeaY6wPWRJgz0vlmFxERERERERE5W+x8Z+7gjAQhQAPHUab6TGZh/xQlCKsZJQkrK7sNY+ETAJiMsx90OL8sfMK5bFhERERERERE5DS7zbmC8HT+4AzOHINB3RVPK6dQzShJWFnFrwJrAvnyg3kcYD3oHCciIiIiIiIiclpuTqEwhnIK1ZKShJVVWnLpjhMRERERERGR6kE5BSmAkoSVVUBw6Y4TERERERERkWohLiPAvYHKKVQrShJWVhHRzi7GhW44NsDS0DlORERERERERAT4aUsig+fbSXDUxV7oKOUUqiMlCSsrkxkGTMq945oozCs7OmCic5yIiIiIiIiIVGt2u4M3F+/g3s83kZ7t4JsGYzAwyL/4KPe+cgrVjpKElVnUUBg+CyyhLoeTqEf6sOnOx0VERERERESkWjuZlcN9X2ziraU7AbjtwkjuvechjAJyCljCnLkG5RSqHSUJK7uooTB2K4xagP3qj3m05sv0zniLqYeiPB2ZiIiIVCMrVqxgyJAhhIWFYRgG8+bNc3nc4XDw9NNPExoair+/P/369WPnzp2eCVZERKQaOZhyimvfX83PW5PwNhu8ck17nhochZfZ5JJT4JpPnF/HblGCsJoqcZKwuAng2b799lsuu+wyGjRogMVioVevXixatMhlzLPPPothGC631q1blzS06stkhsg+mNpfx38GXoMdE9N+j+NYepanIxMREZFqIj09nQ4dOjBlypQCH3/llVd4++23mTp1KmvXrqVmzZr079+fjIyMco5URESk+tiw9xhD3/md2EQr9QN8+PKOngzvFu46KDenQLtrnV+1xbjaKnGSsLgJ4NlWrFjBZZddxk8//cTGjRu55JJLGDJkCH/++afLuDZt2pCYmJh3+/3330samgD92wTTtqGF9CwbH/y229PhiIiISDUxcOBAXnzxRa666qp8jzkcDiZPnsyTTz7JlVdeSfv27Zk1axYJCQnFfuAsIiIi5+aLtfu44aM1HE3PIirUwvdjLqRrk7qeDksqMK+SnjBw4EAGDhzo9vjJkye73H/55Zf5/vvv+eGHH+jUqdO/gXh5ERISUtJw5CyGYTDuspbcOmMDM1fv5bY+kQTV8vN0WCIiIlKNxcXFkZSURL9+/fKOBQYG0qNHD1avXs2IESM8GJ2IiEjVkplj45nvt/HV+v0AXNEuhNeu60ANnxKngKSaKfeahHa7nRMnTlC3rmv2eufOnYSFhdG0aVNuvPFG9u3bV+g1MjMzsVqtLjf51yWtgujUuDYZ2XbeW6bVhCIiIuJZSUlJAAQHB7scDw4OznvsbJrviYiIlFxi6imu/2ANX63fj2HAYwNaMeX/OitBKG4p9yTha6+9RlpaGsOHD8871qNHD2bMmMHChQt5//33iYuLo0+fPpw4caLAa0yYMIHAwMC8W3h4eIHjqivDMHj4slaAc3lxQsopD0ckIiIiUjKa74mIiBTBboO4lbDlG+dXu421e44y5J3fidmfQqC/NzNGd+fevs0xDMPT0UolUa5Jwi+++ILnnnuOOXPmEBQUlHd84MCBXHfddbRv357+/fvz008/kZKSwpw5cwq8zvjx40lNTc277d+/v7xeQqXRu3k9ukfWJctm591luzwdjoiIiFRjp0vKJCcnuxxPTk4utNyM5nsiIiKFiJ0Pk9vCzMEw9zaYOZj0SRcw85N3OJKWReuQWvww5kIubtnA05FKJVNuScKvvvqK22+/nTlz5rjUoylI7dq1admyJbt2FZzc8vX1xWKxuNzElXM1YUsA5qzfz76jJz0ckYiIiFRXkZGRhISEsHTp0rxjVquVtWvX0qtXrwLP0XxPRESkALHzYc5IsCa4HPbPSOZdrzd5sukuvr03msb1angoQKnMyiVJ+OWXXzJ69Gi+/PJLBg0aVOz4tLQ0du/eTWhoaDlEV3X1aFqPPi3qk2N38Nov2z0djoiIiFRhaWlpxMTEEBMTAziblcTExLBv3z4Mw2Ds2LG8+OKLzJ8/ny1btjBy5EjCwsIYNmyYR+MWERGpNOw2WPg44Mj3kMlwLha6Le0Danhpe7GcmxInCYuaAIJza8jIkSPzxn/xxReMHDmS119/nR49epCUlERSUhKpqal5Yx555BF+++039u7dy6pVq7jqqqswm83ccMMN5/ny5PEBrQGYvzmBrQdTixktIiIicm42bNhAp06d6NSpEwDjxo2jU6dOPP300wA89thj3H///dx5551069aNtLQ0Fi5ciJ+fnyfDFhERqTziV+VbQXgmAweG9aBznMg5KHGSsLgJYGJioktn4g8//JCcnBzuu+8+QkND824PPvhg3pgDBw5www030KpVK4YPH069evVYs2YNDRpo//z5atsw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",
"text/plain": [
""
]
@@ -348,16 +396,16 @@
},
{
"cell_type": "code",
- "execution_count": 21,
+ "execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
- "{Variable(0x5f4a102fc03b7b39, u, children=[], domains={}): Multiplication(-0x32ae6bc94fa07109, *, children=['-a', 'y[0:1]'], domains={})}"
+ "{Variable(-0x7fabcf6f713434a8, u, children=[], domains={}): Multiplication(0x26349e0ba31c22ee, *, children=['-a', 'y[0:1]'], domains={})}"
]
},
- "execution_count": 21,
+ "execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
@@ -378,7 +426,7 @@
},
{
"cell_type": "code",
- "execution_count": 22,
+ "execution_count": 13,
"metadata": {},
"outputs": [
{
@@ -420,7 +468,7 @@
},
{
"cell_type": "code",
- "execution_count": 23,
+ "execution_count": 14,
"metadata": {},
"outputs": [
{
@@ -443,7 +491,7 @@
],
"metadata": {
"kernelspec": {
- "display_name": "dev",
+ "display_name": "env",
"language": "python",
"name": "python3"
},
@@ -457,7 +505,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.9.16"
+ "version": "3.10.12"
},
"toc": {
"base_numbering": 1,
@@ -471,11 +519,6 @@
"toc_position": {},
"toc_section_display": true,
"toc_window_display": true
- },
- "vscode": {
- "interpreter": {
- "hash": "bca2b99bfac80e18288b793d52fa0653ab9b5fe5d22e7b211c44eb982a41c00c"
- }
}
},
"nbformat": 4,
diff --git a/docs/source/examples/notebooks/parameterization/parameterization.ipynb b/docs/source/examples/notebooks/parameterization/parameterization.ipynb
index dabb5e5f76..a6ff34c772 100644
--- a/docs/source/examples/notebooks/parameterization/parameterization.ipynb
+++ b/docs/source/examples/notebooks/parameterization/parameterization.ipynb
@@ -586,7 +586,7 @@
"outputs": [
{
"data": {
- "text/plain": "{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n 'Faraday constant [C.mol-1]': 96485.33212,\n 'Negative electrode thickness [m]': 0.0001,\n 'Separator thickness [m]': 2.5e-05,\n 'Positive electrode thickness [m]': 0.0001,\n 'Electrode height [m]': 0.137,\n 'Electrode width [m]': 0.207,\n 'Nominal cell capacity [A.h]': 0.680616,\n 'Current function [A]': 0.680616,\n 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n 'Negative particle diffusivity [m2.s-1]': ,\n 'Negative electrode OCP [V]': ,\n 'Negative electrode porosity': 0.3,\n 'Negative electrode active material volume fraction': 0.6,\n 'Negative particle radius [m]': 1e-05,\n 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n 'Negative electrode exchange-current density [A.m-2]': ,\n 'Negative electrode OCP entropic change [V.K-1]': ,\n 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n 'Positive particle diffusivity [m2.s-1]': ,\n 'Positive electrode OCP [V]': ,\n 'Positive electrode porosity': 0.3,\n 'Positive electrode active material volume fraction': 0.5,\n 'Positive particle radius [m]': 1e-05,\n 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n 'Positive electrode exchange-current density [A.m-2]': ,\n 'Positive electrode OCP entropic change [V.K-1]': ,\n 'Separator porosity': 1.0,\n 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n 'Reference temperature [K]': 298.15,\n 'Ambient temperature [K]': 298.15,\n 'Number of electrodes connected in parallel to make a cell': 1.0,\n 'Number of cells connected in series to make a battery': 1.0,\n 'Lower voltage cut-off [V]': 3.105,\n 'Upper voltage cut-off [V]': 4.1,\n 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565}"
+ "text/plain": "{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,\n 'Faraday constant [C.mol-1]': 96485.33212,\n 'Negative electrode thickness [m]': 0.0001,\n 'Separator thickness [m]': 2.5e-05,\n 'Positive electrode thickness [m]': 0.0001,\n 'Electrode height [m]': 0.137,\n 'Electrode width [m]': 0.207,\n 'Nominal cell capacity [A.h]': 0.680616,\n 'Current function [A]': 0.680616,\n 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,\n 'Negative particle diffusivity [m2.s-1]': ,\n 'Negative electrode OCP [V]': ,\n 'Negative electrode porosity': 0.3,\n 'Negative electrode active material volume fraction': 0.6,\n 'Negative particle radius [m]': 1e-05,\n 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Negative electrode Bruggeman coefficient (electrode)': 1.5,\n 'Negative electrode exchange-current density [A.m-2]': ,\n 'Negative electrode OCP entropic change [V.K-1]': ,\n 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,\n 'Positive particle diffusivity [m2.s-1]': ,\n 'Positive electrode OCP [V]': ,\n 'Positive electrode porosity': 0.3,\n 'Positive electrode active material volume fraction': 0.5,\n 'Positive particle radius [m]': 1e-05,\n 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,\n 'Positive electrode Bruggeman coefficient (electrode)': 1.5,\n 'Positive electrode exchange-current density [A.m-2]': ,\n 'Positive electrode OCP entropic change [V.K-1]': ,\n 'Separator porosity': 1.0,\n 'Separator Bruggeman coefficient (electrolyte)': 1.5,\n 'Initial concentration in electrolyte [mol.m-3]': 1000.0,\n 'Reference temperature [K]': 298.15,\n 'Ambient temperature [K]': 298.15,\n 'Number of electrodes connected in parallel to make a cell': 1.0,\n 'Number of cells connected in series to make a battery': 1.0,\n 'Lower voltage cut-off [V]': 3.105,\n 'Upper voltage cut-off [V]': 4.1,\n 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,\n 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565}"
},
"execution_count": 61,
"metadata": {},
diff --git a/docs/source/examples/notebooks/parameterization/sensitivities_and_data_fitting.ipynb b/docs/source/examples/notebooks/parameterization/sensitivities_and_data_fitting.ipynb
new file mode 100644
index 0000000000..09b562c1ca
--- /dev/null
+++ b/docs/source/examples/notebooks/parameterization/sensitivities_and_data_fitting.ipynb
@@ -0,0 +1,327 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "bd9929be",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Note: you may need to restart the kernel to use updated packages.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%pip install \"pybamm[plot,cite]\" -q # install PyBaMM if it is not installed\n",
+ "# import dependencies\n",
+ "import pybamm\n",
+ "import numpy as np\n",
+ "import matplotlib.pylab as plt\n",
+ "import scipy.optimize"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b1223d98",
+ "metadata": {},
+ "source": [
+ "# Sensitivities and data fitting using PyBaMM\n",
+ "\n",
+ "PyBaMM input parameters [`pybamm.InputParameter`](https://docs.pybamm.org/en/stable/source/api/expression_tree/input_parameter.html) can be used to run many simulations with varying parameters. Here we will demonstrate PyBaMM's ability to calculate the senstivities of model outputs with respect to input parameters. \n",
+ "\n",
+ "To be more specific, given a model output $f(a)$, where $a$ is an input parameter, we wish to calculate $\\frac{\\partial f}{\\partial a}(a)$.\n",
+ "\n",
+ "First we will demonstrate using a toy model, given by the equations\n",
+ "\n",
+ "$$\\frac{dy}{dt} = a y$$\n",
+ "\n",
+ "with a scalar state variable $y$ and a scalar parameter $a$, and initial conditions\n",
+ "\n",
+ "$$y(0) = 1$$\n",
+ "\n",
+ "We will also define a model output given by $f = y^2$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "25970cdf",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# setup a simple test model\n",
+ "model = pybamm.BaseModel(\"name\")\n",
+ "y = pybamm.Variable(\"y\")\n",
+ "a = pybamm.InputParameter(\"a\")\n",
+ "model.rhs = {y: a * y}\n",
+ "model.initial_conditions = {y: 1}\n",
+ "model.variables = {\"y squared\": y**2}\n",
+ "\n",
+ "solver = pybamm.IDAKLUSolver(rtol=1e-10, atol=1e-10)\n",
+ "t_eval = np.linspace(0, 1, 80)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9f3d61bf",
+ "metadata": {},
+ "source": [
+ "Note that we have used the [`pybamm.IDAKLUSolver`](https://docs.pybamm.org/en/stable/source/api/solvers/idaklu_solver.html) solver for this example, this is currently the recommended solver for calculating sensitivities in PyBaMM.\n",
+ "\n",
+ "We can solve the model using a specific value of $a = 1$. However, now we will also calculate the forward sensitivities of the model by setting the argument `calculate_sensitivies=True`. Note that this argument will also accept a list of input parameters to calculate the sensitivities for, but setting it to `True` will calculate the sensitivities for **all** input parameters of the model "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "1b1781a9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "solution = solver.solve(model, [0, 1], inputs={\"a\": 1}, calculate_sensitivities=True)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4d6f176e",
+ "metadata": {},
+ "source": [
+ "We can now access the solution as normal, and the sensitivities using the syntax: `solution[output_name].sensitivities[input_parameter_name]`\n",
+ "\n",
+ "Note that a current restriction to the sensitivity calculation is that it will only return the sensitivities at the values of `t_eval` used to solve the model. Any interpolation between these values will have to be done manually"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "bf0a2d9c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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/afqXOvvixpHqe2fUts+Idj+1XGE9k/ObZ9KCCGa1rdHRERERHKjuDl1RERERM6IRR0RERGRArCoIyIiIlIAFnVERERECsCijoiIiEgBWNQRERERKQCLOiIiIiIFYFFHREREpAAs6oiIiIgUgEUdOaznn38ed955p9QxiIjsim0fXQ2LOnJYBw8eRP/+/aWOQURkV2z76Gp49is5nOrqanh5eaGmpsb23KBBg7B//34JUxERtS22fXQ9rlIHIGoqV1dX/Prrrxg0aBCSkpIQHBwMd3d3qWMREbUptn10PSzqyOGo1Wrk5eUhICAAvXv3ljoOEZFdsO2j6+GcOnJIR48eZaNGRE6HbR9dC4s6ckhJSUls2IjI6bDto2thUUcOKTk5GX369JE6BhGRXbHto2thUUcOyWKxIDU1FXl5edDr9VLHISKyC7Z9dC0s6sghvfHGG1ixYgU6dOiAN954Q+o4RER2wbaProX71BEREREpAHvqiIiIiBSARR0RERGRArCoIyIiIlIAFnVERERECsCijoiIiEgBWNQRERERKQCLOiIiIiIFYFFHREREpAAs6oiIiIgUgEUdERERkQKwqCMiIiJSABZ1RERERArw/wG4APAEUxDXOQAAAABJRU5ErkJggg==",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "fig, axs = plt.subplots(1, 2)\n",
+ "axs[0].plot(t_eval, solution[\"y squared\"](t_eval))\n",
+ "axs[0].set_ylabel(r\"$y^2$\")\n",
+ "axs[0].set_xlabel(r\"$t$\")\n",
+ "axs[1].plot(solution.t, solution[\"y squared\"].sensitivities[\"a\"])\n",
+ "axs[1].set_ylabel(r\"$\\frac{dy^2}{da}$\")\n",
+ "axs[1].set_xlabel(r\"$t$\")\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "495bcf05",
+ "metadata": {},
+ "source": [
+ "## Sensitivities for the DFN model\n",
+ "\n",
+ "We can do the same for the DFN model included in PyBaMM. We will setup the DFN model using \"Current function\" as an input parameter. This is the parameter we wish to calculate the sensitivities with respect to."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "e9119add",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# now lets do the same for the DFN model\n",
+ "\n",
+ "# load model\n",
+ "model = pybamm.lithium_ion.DFN()\n",
+ "\n",
+ "# load parameter values and process model and geometry\n",
+ "param = model.default_parameter_values\n",
+ "\n",
+ "# we want to calculate the sensitivities of the \"Current function\" parameter, so set\n",
+ "# this an an input parameter\n",
+ "param.update({\"Current function [A]\": \"[input]\"})\n",
+ "\n",
+ "solver = pybamm.IDAKLUSolver(rtol=1e-3, atol=1e-6)\n",
+ "\n",
+ "sim = pybamm.Simulation(model, parameter_values=param, solver=solver)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f198fbfe",
+ "metadata": {},
+ "source": [
+ "We can now evaluate the senstivities of, for example, the \"Terminal voltage\" output of the model with respect to the input parameter \"Current function\"."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "1d794537",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "solution = sim.solve(\n",
+ " [0, 3600], inputs={\"Current function [A]\": 0.15652}, calculate_sensitivities=True\n",
+ ")\n",
+ "plt.plot(\n",
+ " solution.t, solution[\"Terminal voltage [V]\"].sensitivities[\"Current function [A]\"]\n",
+ ")\n",
+ "\n",
+ "plt.xlabel(r\"$t$\")\n",
+ "plt.ylabel(\"sensitivities of Terminal voltage wrt Current fuction\")\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d84c83fb",
+ "metadata": {},
+ "source": [
+ "## Sensitivities and data fitting\n",
+ "\n",
+ "Sensitivities are often used to aid data fitting by providing a means to calculate the gradient of the function to be minimised. Take for example the data fitting exercise we introduced in the previous notebook. Once again we will generate some fake data for fitting, like so:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "33d95c39",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "t_eval = np.linspace(0, 3600, 100)\n",
+ "data = sim.solve([0, 3600], inputs={\"Current function [A]\": 0.2222})[\n",
+ " \"Terminal voltage [V]\"\n",
+ "](t_eval)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d5045b57",
+ "metadata": {},
+ "source": [
+ "Now we will contruct a function to minimise, but here we will return both the value of the function, and its gradient with respect to the input parameter \"Current function\". Note that our objective function is the sum of squared different between the vector $\\mathbf{f}$, the simulated \"Terminal voltage\", and $\\mathbf{d}$, the vector of fake data, given by\n",
+ "\n",
+ "$$\\mathcal{O}(a) = \\sum_{i=0}^{i=N} (f_i(a) - d_i)^2$$ \n",
+ "\n",
+ "where $a$ is the parameter to be optimised (in this case \"Current function\"), $f_i$ is each element of the vector $\\mathbf{f}$, and $d_i$ is each element of $\\mathbf{d}$. We wish to also find the gradient of this function wrt the parameter $a$, which is:\n",
+ "\n",
+ "$$\\frac{\\partial \\mathcal{O}}{\\partial a}(a) = 2 \\sum_{i=0}^{i=N} (f_i(a) - d_i) \\frac{\\partial f_i}{\\partial a} $$ \n",
+ "\n",
+ "Using these equations, we will define a function that takes in as an argument $a$, and returns $(\\mathcal{O}(a), \\frac{\\partial \\mathcal{O}}{\\partial a}(a))$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "ffad2bc0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def sum_of_squares_jac(parameters):\n",
+ " sol = sim.solve(\n",
+ " [0, 3600],\n",
+ " t_interp=t_eval,\n",
+ " inputs={\"Current function [A]\": parameters[0]},\n",
+ " calculate_sensitivities=True,\n",
+ " )\n",
+ " term_voltage = sol[\"Terminal voltage [V]\"].data\n",
+ " term_voltage_sens = sol[\"Terminal voltage [V]\"].sensitivities[\n",
+ " \"Current function [A]\"\n",
+ " ]\n",
+ "\n",
+ " f = np.sum((term_voltage - data) ** 2)\n",
+ " g = 2 * np.sum((term_voltage - data) * term_voltage_sens)\n",
+ " print(\n",
+ " f\"evaluating function and jacobian for p = {parameters[0]}, \\tloss = {f}, grad = {g}\"\n",
+ " )\n",
+ " return f, g"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fdcae8ac",
+ "metadata": {},
+ "source": [
+ "We can then use this function along with an optimisation algorithm to recover the value of the Current function that was used to generate the data. In this case we will use the `scipy.optimize` module again. This module allows the use of a function in the form given above to perform the minimisation, using both the value of the objective function and its gradient to find the minimum value of $a$ in the least number of steps.\n",
+ "\n",
+ "Once again, we will place bounds on \"Current function [A]\" between $(0.01, 0.6)$, and use a random starting value $x_0$ between these bounds."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "44f52a7e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "starting parameter is 0.4035076613514513\n",
+ "evaluating function and jacobian for p = 0.4035076613514513, \tloss = 0.358632833514908, grad = 3.7278265436340283\n",
+ "evaluating function and jacobian for p = 0.01, \tloss = 0.8765036816401419, grad = -9.691403058800336\n",
+ "evaluating function and jacobian for p = 0.23970725060294026, \tloss = 0.0036706826105448887, grad = 0.41303112361463107\n",
+ "evaluating function and jacobian for p = 0.21929734316448973, \tloss = 0.00010628748342624681, grad = -0.07293742235816741\n",
+ "evaluating function and jacobian for p = 0.22236059911277223, \tloss = 2.5627985467376454e-07, grad = 0.0035066000930420284\n",
+ "evaluating function and jacobian for p = 0.22222008304298907, \tloss = 7.758859239380127e-09, grad = 2.935984691530423e-05\n",
+ "evaluating function and jacobian for p = 0.22221889660489277, \tloss = 7.74141508243804e-09, grad = -1.183255640750795e-08\n",
+ "recovered parameter is 0.22221889660489277\n"
+ ]
+ }
+ ],
+ "source": [
+ "bounds = (0.01, 0.6)\n",
+ "x0 = np.random.uniform(low=bounds[0], high=bounds[1])\n",
+ "\n",
+ "print(\"starting parameter is\", x0)\n",
+ "res = scipy.optimize.minimize(sum_of_squares_jac, [x0], bounds=[bounds], jac=True)\n",
+ "print(\"recovered parameter is\", res.x[0])"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "env",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/source/user_guide/fundamentals/public_api.rst b/docs/source/user_guide/fundamentals/public_api.rst
new file mode 100644
index 0000000000..6d73ecaec1
--- /dev/null
+++ b/docs/source/user_guide/fundamentals/public_api.rst
@@ -0,0 +1,74 @@
+----------
+Public API
+----------
+
+.. module:: pybamm
+ :noindex:
+
+PyBaMM is a Python package for mathematical modelling and simulation of battery systems. The main classes and functions that are intended to be used by the user are described in this document.
+For a more detailed description of the classes and methods, see the :doc:`API reference `.
+
+Available PyBaMM models
+-----------------------
+
+PyBaMM includes a number of pre-implemented models, which can be used as they are or modified to suit your needs. The main models are:
+
+- :class:`lithium_ion.SPM`: Single Particle Model
+- :class:`lithium_ion.SPMe`: Single Particle Model with Electrolyte
+- :class:`lithium_ion.DFN`: Doyle-Fuller-Newman
+
+The behaviour of the models can be modified by passing in an :class:`BatteryModelOptions` object when creating the model.
+
+Simulations
+-----------
+
+:class:`Simulation` is a class that automates the process of setting up a model and solving it, and acts as the highest-level API to PyBaMM.
+Pass at least a :class:`BaseModel` object, and optionally the experiment, solver, parameter values, and geometry objects described below to the :class:`Simulation` object.
+Any of these optional arguments not provided will be supplied by the defaults specified in the model.
+
+Parameters
+----------
+
+PyBaMM models are parameterised by a set of parameters, which are stored in a :class:`ParameterValues` object. This object acts like a Python dictionary with a few extra PyBaMM specific features and methods.
+Parameters in a model are represented as either :class:`Parameter` objects or :class:`FunctionParameter` objects, and the values in the :class:`ParameterValues` object replace these objects in the model before it is solved.
+The values in the :class:`ParameterValues` object can be scalars, Python functions or expressions of type :class:`Symbol`.
+
+Experiments
+-----------
+
+An :class:`Experiment` object represents an experimental protocol that can be used to simulate the behaviour of a battery. The particular protocol can be provided as a Python string, or as a sequences of
+:class:`step.BaseStep` objects.
+
+Solvers
+-------
+
+The two main solvers in PyBaMM are the :class:`CasadiSolver` and the :class:`IDAKLUSolver`. Both are wrappers around the Sundials suite of solvers, but the :class:`CasadiSolver` uses the CasADi library
+whereas the :class:`IDAKLUSolver` is PyBaMM specific. Both solvers have many options that can be set to control the solver behaviour, see the documentation for each solver for more details.
+
+When a model is solved, the solution is returned as a :class:`Solution` object.
+
+Plotting
+--------
+
+A solution object can be plotted using the :meth:`Solution.plot` or :meth:`Simulation.plot` methods, which returns a :class:`QuickPlot` object.
+Note that the arguments to the plotting methods of both classes are the same as :class:`QuickPlot`.
+
+Other plotting functions are the :func:`plot_voltage_components` and :func:`plot_summary_variables` functions, which correspond to the similarly named methods of the :class:`Solution` and :class:`Simulation` classes.
+
+Writing PyBaMM models
+---------------------
+
+Each PyBaMM model, and the custom models written by users, are written as a set of expressions that describe the model. Each of the expressions is a subclass of the :class:`Symbol` class, which represents a mathematical expression.
+
+If you wish to create a custom model, you can use the :class:`BaseModel` class as a starting point.
+
+
+Discretisation
+--------------
+
+Each PyBaMM model contains continuous operators that must be discretised before they can be solved. This is done using a :class:`Discretisation` object, which takes a :class:`Mesh` object and a dictionary of :class:`SpatialMethod` objects.
+
+Logging
+-------
+
+PyBaMM uses the Python logging module to log messages at different levels of severity. Use the :func:`pybamm.set_logging_level` function to set the logging level for PyBaMM.
diff --git a/docs/source/user_guide/index.md b/docs/source/user_guide/index.md
index 58ce04101a..b497ed1a01 100644
--- a/docs/source/user_guide/index.md
+++ b/docs/source/user_guide/index.md
@@ -22,6 +22,7 @@ maxdepth: 2
---
fundamentals/index
fundamentals/battery_models
+fundamentals/public_api
```
```{toctree}
@@ -72,3 +73,12 @@ glob:
../examples/notebooks/creating_models/5-half-cell-model.ipynb
../examples/notebooks/creating_models/6-a-simple-SEI-model.ipynb
```
+
+# Telemetry
+
+PyBaMM optionally collects anonymous usage data to help improve the library. This telemetry is opt-in and can be easily disabled. Here's what you need to know:
+
+- **What is collected**: Basic usage information like PyBaMM version, Python version, and which functions are run.
+- **Why**: To understand how PyBaMM is used and prioritize development efforts.
+- **Opt-out**: To disable telemetry, set the environment variable `PYBAMM_DISABLE_TELEMETRY=true` (or any value other than `false`) or use `pybamm.telemetry.disable()` in your code.
+- **Privacy**: No personal information (name, email, etc) or sensitive information (parameter values, simulation results, etc) is ever collected.
diff --git a/docs/source/user_guide/installation/index.rst b/docs/source/user_guide/installation/index.rst
index d6411348c5..9225f1ee98 100644
--- a/docs/source/user_guide/installation/index.rst
+++ b/docs/source/user_guide/installation/index.rst
@@ -71,6 +71,7 @@ Package Minimum supp
`typing-extensions `__ 4.10.0
`pandas `__ 1.5.0
`pooch `__ 1.8.1
+`posthog `__ 3.6.5
=================================================================== ==========================
.. _install.optional_dependencies:
@@ -145,8 +146,8 @@ Dependency
`pre-commit `__ \- dev For managing and maintaining multi-language pre-commit hooks.
`ruff `__ \- dev For code formatting.
`nox `__ \- dev For running testing sessions in multiple environments.
+`pytest-subtests `__ \- dev For subtests pytest fixture.
`pytest-cov `__ \- dev For calculating test coverage.
-`parameterized `__ \- dev For test parameterization.
`pytest `__ 6.0.0 dev For running the test suites.
`pytest-doctestplus `__ \- dev For running doctests.
`pytest-xdist `__ \- dev For running tests in parallel across distributed workers.
diff --git a/examples/scripts/multiprocess_jax_solver.py b/examples/scripts/multiprocess_jax_solver.py
new file mode 100644
index 0000000000..8192256ed1
--- /dev/null
+++ b/examples/scripts/multiprocess_jax_solver.py
@@ -0,0 +1,57 @@
+import pybamm
+import time
+import numpy as np
+
+
+# This script provides an example for massively vectorised
+# model solves using the JAX BDF solver. First,
+# we set up the model and process parameters
+model = pybamm.lithium_ion.SPM()
+model.convert_to_format = "jax"
+model.events = [] # remove events (not supported in jax)
+geometry = model.default_geometry
+param = pybamm.ParameterValues("Chen2020")
+param.update({"Current function [A]": "[input]"})
+param.process_geometry(geometry)
+param.process_model(model)
+
+# Discretise and setup solver
+mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)
+disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
+disc.process_model(model)
+t_eval = np.linspace(0, 3600, 100)
+solver = pybamm.JaxSolver(atol=1e-6, rtol=1e-6, method="BDF")
+
+# Set number of vectorised solves
+values = np.linspace(0.01, 1.0, 1000)
+inputs = [{"Current function [A]": value} for value in values]
+
+# Run solve for all inputs, with a just-in-time compilation
+# occurring on the first solve. All sequential solves will
+# use the compiled code, with a large performance improvement.
+start_time = time.time()
+sol = solver.solve(model, t_eval, inputs=inputs)
+print(f"Time taken: {time.time() - start_time}") # 1.3s
+
+# Rerun the vectorised solve, showing performance improvement
+start_time = time.time()
+compiled_sol = solver.solve(model, t_eval, inputs=inputs)
+print(f"Compiled time taken: {time.time() - start_time}") # 0.42s
+
+# Plot one of the solves
+plot = pybamm.QuickPlot(
+ sol[5],
+ [
+ "Negative particle concentration [mol.m-3]",
+ "Electrolyte concentration [mol.m-3]",
+ "Positive particle concentration [mol.m-3]",
+ "Current [A]",
+ "Negative electrode potential [V]",
+ "Electrolyte potential [V]",
+ "Positive electrode potential [V]",
+ "Voltage [V]",
+ ],
+ time_unit="seconds",
+ spatial_unit="um",
+)
+plot.dynamic_plot()
diff --git a/noxfile.py b/noxfile.py
index 6567ed167c..5ab32f463f 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -2,7 +2,6 @@
import os
import sys
import warnings
-import platform
from pathlib import Path
@@ -27,28 +26,15 @@ def set_iree_state():
"""
state = "ON" if os.getenv("PYBAMM_IDAKLU_EXPR_IREE", "OFF") == "ON" else "OFF"
if state == "ON":
- if sys.platform == "win32":
+ if sys.platform == "win32" or sys.platform == "darwin":
warnings.warn(
(
- "IREE is not enabled on Windows yet. "
+ "IREE is not enabled on Windows and MacOS. "
"Setting PYBAMM_IDAKLU_EXPR_IREE=OFF."
),
stacklevel=2,
)
return "OFF"
- if sys.platform == "darwin":
- # iree-compiler is currently only available as a wheel on macOS 13 (or
- # higher) and Python version 3.11
- mac_ver = int(platform.mac_ver()[0].split(".")[0])
- if (not sys.version_info[:2] == (3, 11)) or mac_ver < 13:
- warnings.warn(
- (
- "IREE is only supported on MacOS 13 (or higher) and Python"
- "version 3.11. Setting PYBAMM_IDAKLU_EXPR_IREE=OFF."
- ),
- stacklevel=2,
- )
- return "OFF"
return state
@@ -222,7 +208,7 @@ def run_scripts(session):
# https://bitbucket.org/pybtex-devs/pybtex/issues/169/replace-pkg_resources-with
# is fixed
session.install("setuptools", silent=False)
- session.install("-e", ".[all,dev]", silent=False)
+ session.install("-e", ".[all,dev,jax]", silent=False)
session.run("python", "-m", "pytest", "-m", "scripts")
diff --git a/pyproject.toml b/pyproject.toml
index 7fb1a5ce95..1002b86cc0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ requires = [
"setuptools>=64",
"wheel",
# On Windows, use the CasADi vcpkg registry and CMake bundled from MSVC
- "casadi>=3.6.6; platform_system!='Windows'",
+ "casadi>=3.6.7; platform_system!='Windows'",
# Note: the version of CasADi as a build-time dependency should be matched
# across platforms, so updates to its minimum version here should be accompanied
# by a version bump in https://github.com/pybamm-team/casadi-vcpkg-registry.
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pybamm"
-version = "24.9.0"
+version = "24.11.0"
license = { file = "LICENSE.txt" }
description = "Python Battery Mathematical Modelling"
authors = [{name = "The PyBaMM Team", email = "pybamm@pybamm.org"}]
@@ -37,13 +37,14 @@ classifiers = [
dependencies = [
"numpy>=1.23.5,<2.0.0",
"scipy>=1.11.4",
- "casadi>=3.6.6",
+ "casadi>=3.6.7",
"xarray>=2022.6.0",
"anytree>=2.8.0",
"sympy>=1.12",
"typing-extensions>=4.10.0",
"pandas>=1.5.0",
"pooch>=1.8.1",
+ "posthog",
]
[project.urls]
@@ -84,6 +85,7 @@ plot = [
"matplotlib>=3.6.0",
]
cite = [
+ "setuptools", # Fix for a pybtex issue
"pybtex>=0.24.0",
]
# Battery Parameter eXchange format
@@ -105,12 +107,11 @@ dev = [
"pytest-cov",
# For doctest
"pytest-doctestplus",
- # For test parameterization
- "parameterized>=0.9",
# pytest and its plugins
"pytest>=6",
"pytest-xdist",
"pytest-mock",
+ "pytest-subtests",
# For testing Jupyter notebooks
"nbmake",
# To access the metadata for python packages
@@ -152,6 +153,7 @@ Ramadass2004 = "pybamm.input.parameters.lithium_ion.Ramadass2004:get_parameter_v
Xu2019 = "pybamm.input.parameters.lithium_ion.Xu2019:get_parameter_values"
ECM_Example = "pybamm.input.parameters.ecm.example_set:get_parameter_values"
MSMR_Example = "pybamm.input.parameters.lithium_ion.MSMR_example_set:get_parameter_values"
+Chayambuka2022 = "pybamm.input.parameters.sodium_ion.Chayambuka2022:get_parameter_values"
[tool.setuptools]
include-package-data = true
@@ -197,6 +199,8 @@ extend-select = [
"YTT", # flake8-2020
"TID252", # relative-imports
"S101", # to identify use of assert statement
+ "PT027", # remove unittest style assertion
+ "PT009", # Use pytest.raises instead of unittest-style
]
ignore = [
"E741", # Ambiguous variable name
@@ -230,6 +234,7 @@ minversion = "8"
required_plugins = [
"pytest-xdist",
"pytest-mock",
+ "pytest-subtests",
]
norecursedirs = 'pybind11*'
addopts = [
diff --git a/setup.py b/setup.py
index 74de1baca4..8a49bfd715 100644
--- a/setup.py
+++ b/setup.py
@@ -327,6 +327,8 @@ def compile_KLU():
"src/pybamm/solvers/c_solvers/idaklu/Solution.hpp",
"src/pybamm/solvers/c_solvers/idaklu/Options.hpp",
"src/pybamm/solvers/c_solvers/idaklu/Options.cpp",
+ "src/pybamm/solvers/c_solvers/idaklu/observe.hpp",
+ "src/pybamm/solvers/c_solvers/idaklu/observe.cpp",
"src/pybamm/solvers/c_solvers/idaklu.cpp",
],
)
diff --git a/src/pybamm/CITATIONS.bib b/src/pybamm/CITATIONS.bib
index 3d853738b4..62b4b1003e 100644
--- a/src/pybamm/CITATIONS.bib
+++ b/src/pybamm/CITATIONS.bib
@@ -22,6 +22,17 @@ @article{Ai2022
author = {Weilong Ai and Niall Kirkaldy and Yang Jiang and Gregory Offer and Huizhi Wang and Billy Wu},
}
+@article{Akanni1987,
+ title={Effective transport coefficients in heterogeneous media},
+ author={Akanni, KA and Evans, JW and Abramson, IS},
+ journal={Chemical Engineering Science},
+ volume={42},
+ number={8},
+ pages={1945--1954},
+ year={1987},
+ publisher={Elsevier}
+}
+
@article{Andersson2019,
author = {Andersson, Joel A. E. and Gillis, Joris and Horn, Greg
and Rawlings, James B. and Diehl, Moritz},
@@ -47,6 +58,39 @@ @article{Baker2018
publisher={IOP Publishing}
}
+@article{Baltensperger2003,
+ title={Spectral differencing with a twist},
+ author={Baltensperger, Richard and Trummer, Manfred R},
+ journal={SIAM journal on scientific computing},
+ volume={24},
+ number={5},
+ pages={1465--1487},
+ year={2003},
+ publisher={SIAM}
+}
+
+@article{Barletta2022thevenin,
+ title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink},
+ author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide},
+ journal={Energies},
+ volume={15},
+ number={17},
+ pages={6207},
+ year={2022},
+ publisher={MDPI}
+}
+
+@article{Beeckman1990,
+ title={Mathematical description of heterogeneous materials},
+ author={Beeckman, JW},
+ journal={Chemical engineering science},
+ volume={45},
+ number={8},
+ pages={2603--2610},
+ year={1990},
+ publisher={Elsevier}
+}
+
@article{BrosaPlanella2021,
title = {Systematic derivation and validation of a reduced thermal-electrochemical model for lithium-ion batteries using asymptotic methods},
author = {Brosa Planella, Ferran and Sheikh, Muhammad and Widanage, W. Dhammika},
@@ -70,6 +114,38 @@ @article{BrosaPlanella2022
doi = {},
}
+@article{Bruggeman1935,
+ title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen},
+ author={Bruggeman, Von DAG},
+ journal={Annalen der physik},
+ volume={416},
+ number={7},
+ pages={636--664},
+ year={1935},
+ publisher={Wiley Online Library}
+}
+
+@article{Byrne1975,
+ title={A polyalgorithm for the numerical solution of ordinary differential equations},
+ author={Byrne, George D. and Hindmarsh, Alan C.},
+ journal={ACM Transactions on Mathematical Software (TOMS)},
+ volume={1},
+ number={1},
+ pages={71--96},
+ year={1975},
+ publisher={ACM New York, NY, USA}
+}
+
+@article{Chayambuka2022,
+ title={Physics-based modeling of sodium-ion batteries part II. Model and validation},
+ author={Chayambuka, Kudakwashe and Mulder, Grietus and Danilov, Dmitri L and Notten, Peter HL},
+ journal={Electrochimica Acta},
+ volume={404},
+ pages={139764},
+ year={2022},
+ publisher={Elsevier}
+}
+
@article{Chen2020,
author = {Chen, Chang-Hui and Brosa Planella, Ferran and O'Regan, Kieran and Gastol, Dominika and Widanage, W. Dhammika and Kendrick, Emma},
title = {{Development of Experimental Techniques for Parameterization of Multi-scale Lithium-ion Battery Models}},
@@ -140,6 +216,16 @@ @article{Ecker2015ii
doi = {10.1149/2.0541509jes},
}
+@article{Fan2022,
+ title={Data-driven identification of lithium-ion batteries: A nonlinear equivalent circuit model with diffusion dynamics},
+ author={Fan, Chuanxin and O’Regan, Kieran and Li, Liuying and Higgins, Matthew D and Kendrick, Emma and Widanage, Widanalage D},
+ journal={Applied Energy},
+ volume={321},
+ pages={119336},
+ year={2022},
+ publisher={Elsevier}
+}
+
@article{Gustafsson2020,
doi = {10.21105/joss.02369},
year = {2020},
@@ -152,6 +238,13 @@ @article{Gustafsson2020
journal = {Journal of Open Source Software},
}
+@book{Hairer1993,
+ title={Solving ordinary differential equations. 1, Nonstiff problems},
+ author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard},
+ year={1993},
+ publisher={Springer-Vlg}
+}
+
@article{Hales2019,
title={The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries},
author={Hales, Alastair and Diaz, Laura Bravo and Marzook, Mohamed Waseem and Zhao, Yan and Patel, Yatish and Offer, Gregory},
@@ -198,7 +291,7 @@ @article{Hindmarsh2005
@misc{jax2018,
author = {James Bradbury and Roy Frostig and Peter Hawkins and Matthew James Johnson and Chris Leary and Dougal Maclaurin and Skye Wanderman-Milne},
title = {{JAX: composable transformations of Python+NumPy programs}},
- url = {http://github.com/google/jax},
+ url = {http://github.com/jax-ml/jax},
version = {0.2.5},
year = {2018},
}
@@ -252,7 +345,18 @@ @article{Lain2019
doi = {10.3390/batteries5040064},
}
-@article{lin2014lumped,
+@article{Landesfeind2019,
+ title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes},
+ author={Landesfeind, Johannes and Gasteiger, Hubert A},
+ journal={Journal of The Electrochemical Society},
+ volume={166},
+ number={14},
+ pages={A3079--A3097},
+ year={2019},
+ publisher={The Electrochemical Society}
+}
+
+@article{Lin2014,
title={A lumped-parameter electro-thermal model for cylindrical batteries},
author={Lin, Xinfan and Perez, Hector E and Mohan, Shankar and Siegel, Jason B and Stefanopoulou, Anna G and Ding, Yi and Castanier, Matthew P},
journal={Journal of Power Sources},
@@ -262,6 +366,17 @@ @article{lin2014lumped
publisher={Elsevier}
}
+@article{Mackie1955,
+ title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical},
+ author={Mackie, JS and Meares, P},
+ journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences},
+ volume={232},
+ number={1191},
+ pages={498--509},
+ year={1955},
+ publisher={The Royal Society London}
+}
+
@article{Marquis2019,
title = {{An asymptotic derivation of a single particle model with electrolyte}},
author = {Marquis, Scott G. and Sulzer, Valentin and Timms, Robert and Please, Colin P. and Chapman, S. Jon},
@@ -321,6 +436,17 @@ @article{Newman1962
publisher={IOP Publishing}
}
+@article{Nieto2012,
+author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro},
+year = {2012},
+month = {11},
+pages = {A212-A217},
+title = {Thermal Modeling of Large Format Lithium-Ion Cells},
+volume = {160},
+journal = {Journal of the Electrochemical Society},
+doi = {10.1149/2.042302jes}
+}
+
@article{Nyman2008,
title={Electrochemical characterisation and modelling of the mass transport phenomena in LiPF6--EC--EMC electrolyte},
author={Nyman, Andreas and Behm, M{\aa}rten and Lindbergh, G{\"o}ran},
@@ -370,6 +496,28 @@ @article{ORegan2022
doi = {10.1016/j.electacta.2022.140700},
}
+@article{Petersen1958,
+ title={Diffusion in a pore of varying cross section},
+ author={Petersen, EE},
+ journal={AIChE Journal},
+ volume={4},
+ number={3},
+ pages={343--345},
+ year={1958},
+ publisher={Wiley Online Library}
+}
+
+@article{Ploehn2004,
+ title={Solvent diffusion model for aging of lithium-ion battery cells},
+ author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E},
+ journal={Journal of The Electrochemical Society},
+ volume={151},
+ number={3},
+ pages={A456},
+ year={2004},
+ publisher={IOP Publishing}
+}
+
@article{Prada2013,
title = {{A simplified electrochemical and thermal aging model of LiFePO4-graphite Li-ion batteries: power and capacity fade simulations}},
author = {Prada, Eric and Di Domenico, D. and Creff, Y. and Bernard, J. and Sauvant-Moynot, Val{\'{e}}rie and Huet, Fran{\c{c}}ois},
@@ -439,6 +587,61 @@ @article{Richardson2021
doi = {10.1016/j.electacta.2021.138909},
}
+@article{Rieger2016,
+ title={A new method to model the thickness change of a commercial pouch cell during discharge},
+ author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas},
+ journal={Journal of The Electrochemical Society},
+ volume={163},
+ number={8},
+ pages={A1566},
+ year={2016},
+ publisher={IOP Publishing}
+}
+
+@article{Safari2008,
+ title={Multimodal physics-based aging model for life prediction of Li-ion batteries},
+ author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles},
+ journal={Journal of The Electrochemical Society},
+ volume={156},
+ number={3},
+ pages={A145},
+ year={2008},
+ publisher={IOP Publishing}
+}
+
+@article{Shampine1997,
+ title={The matlab ode suite},
+ author={Shampine, Lawrence F and Reichelt, Mark W},
+ journal={SIAM journal on scientific computing},
+ volume={18},
+ number={1},
+ pages={1--22},
+ year={1997},
+ publisher={SIAM}
+}
+
+@article{Shen2007,
+ title={Critical review of the impact of tortuosity on diffusion},
+ author={Shen, Lihua and Chen, Zhangxin},
+ journal={Chemical Engineering Science},
+ volume={62},
+ number={14},
+ pages={3748--3755},
+ year={2007},
+ publisher={Elsevier}
+}
+
+@article{Single2018,
+ title={Identifying the mechanism of continued growth of the solid--electrolyte interphase},
+ author={Single, Fabian and Latz, Arnulf and Horstmann, Birger},
+ journal={ChemSusChem},
+ volume={11},
+ number={12},
+ pages={1950--1955},
+ year={2018},
+ publisher={Wiley Online Library}
+}
+
@article{Sripad2020,
title={Kinetics of lithium electrodeposition and stripping},
author={Sripad, Shashank and Korff, Daniel and DeCaluwe, Steven C and Viswanathan, Venkatasubramanian},
@@ -510,6 +713,17 @@ @article{Timms2021
doi = {10.1137/20M1336898},
}
+@article{Tomadakis1993,
+ title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions},
+ author={Tomadakis, Manolis M and Sotirchos, Stratis V},
+ journal={The Journal of chemical physics},
+ volume={98},
+ number={1},
+ pages={616--626},
+ year={1993},
+ publisher={American Institute of Physics}
+}
+
@article{Valoen2005,
title={Transport properties of LiPF6-based Li-ion battery electrolytes},
author={Val{\o}en, Lars Ole and Reimers, Jan N},
@@ -556,6 +770,17 @@ @article{Wang2002
doi = {10.1006/jcph.2002.7041},
}
+@article{Weissberg1963,
+ title={Effective diffusion coefficient in porous media},
+ author={Weissberg, Harold L},
+ journal={Journal of Applied Physics},
+ volume={34},
+ number={9},
+ pages={2636--2639},
+ year={1963},
+ publisher={American Institute of Physics}
+}
+
@article{Weng2023,
title={Differential voltage analysis for battery manufacturing process control},
author={Weng, Andrew and Siegel, Jason B and Stefanopoulou, Anna},
@@ -563,6 +788,19 @@ @article{Weng2023
year={2023}
}
+@article{Wycisk2022,
+ title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells},
+ journal = {Journal of Energy Storage},
+ volume = {52},
+ pages = {105016},
+ year = {2022},
+ issn = {2352-152X},
+ doi = {https://doi.org/10.1016/j.est.2022.105016},
+ url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192},
+ author = {Dominik Wycisk and Marc Oldenburger and Marc Gerry Stoye and Toni Mrkonjic and Arnulf Latz},
+ keywords = {Lithium-ion battery, Voltage hysteresis, Plett-model, Silicon–graphite anode},
+}
+
@article{Xu2019,
title={Evolution of Dead Lithium Growth in Lithium Metal Batteries: Experimentally Validated Model of the Apparent Capacity Loss},
author={Xu, Shanshan and Chen, Kuan-Hung and Dasgupta, Neil P and Siegel, Jason B and Stefanopoulou, Anna G},
@@ -595,222 +833,3 @@ @article{Zhao2018
year={2018},
publisher={IOP Publishing}
}
-
-@article{Barletta2022thevenin,
- title={Th{\'e}venin’s Battery Model Parameter Estimation Based on Simulink},
- author={Barletta, Giulio and DiPrima, Piera and Papurello, Davide},
- journal={Energies},
- volume={15},
- number={17},
- pages={6207},
- year={2022},
- publisher={MDPI}
-}
-
-@article{Nieto2012,
-author = {Nieto, Nerea and Diaz, Luis and Gastelurrutia, Jon and Alava, Isabel and Blanco, Francisco and Ramos, Juan and Rivas, Alejandro},
-year = {2012},
-month = {11},
-pages = {A212-A217},
-title = {Thermal Modeling of Large Format Lithium-Ion Cells},
-volume = {160},
-journal = {Journal of the Electrochemical Society},
-doi = {10.1149/2.042302jes}
-}
-
-@article{shampine1997matlab,
- title={The matlab ode suite},
- author={Shampine, Lawrence F and Reichelt, Mark W},
- journal={SIAM journal on scientific computing},
- volume={18},
- number={1},
- pages={1--22},
- year={1997},
- publisher={SIAM}
-}
-
-@article{byrne1975polyalgorithm,
- title={A polyalgorithm for the numerical solution of ordinary differential equations},
- author={Byrne, George D. and Hindmarsh, Alan C.},
- journal={ACM Transactions on Mathematical Software (TOMS)},
- volume={1},
- number={1},
- pages={71--96},
- year={1975},
- publisher={ACM New York, NY, USA}
-}
-
-@book{hairer1993solving,
- title={Solving ordinary differential equations. 1, Nonstiff problems},
- author={Hairer, Ernst and N{\o}rsett, Syvert P and Wanner, Gerhard},
- year={1993},
- publisher={Springer-Vlg}
-}
-
-@article{baltensperger2003spectral,
- title={Spectral differencing with a twist},
- author={Baltensperger, Richard and Trummer, Manfred R},
- journal={SIAM journal on scientific computing},
- volume={24},
- number={5},
- pages={1465--1487},
- year={2003},
- publisher={SIAM}
-}
-
-@article{rieger2016new,
- title={A new method to model the thickness change of a commercial pouch cell during discharge},
- author={Rieger, Bernhard and Erhard, Simon V and Rumpf, Katharina and Jossen, Andreas},
- journal={Journal of The Electrochemical Society},
- volume={163},
- number={8},
- pages={A1566},
- year={2016},
- publisher={IOP Publishing}
-}
-
-@article{ploehn2004solvent,
- title={Solvent diffusion model for aging of lithium-ion battery cells},
- author={Ploehn, Harry J and Ramadass, Premanand and White, Ralph E},
- journal={Journal of The Electrochemical Society},
- volume={151},
- number={3},
- pages={A456},
- year={2004},
- publisher={IOP Publishing}
-}
-
-@article{single2018identifying,
- title={Identifying the mechanism of continued growth of the solid--electrolyte interphase},
- author={Single, Fabian and Latz, Arnulf and Horstmann, Birger},
- journal={ChemSusChem},
- volume={11},
- number={12},
- pages={1950--1955},
- year={2018},
- publisher={Wiley Online Library}
-}
-
-@article{safari2008multimodal,
- title={Multimodal physics-based aging model for life prediction of Li-ion batteries},
- author={Safari, M and Morcrette, Mathieu and Teyssot, A and Delacourt, Charles},
- journal={Journal of The Electrochemical Society},
- volume={156},
- number={3},
- pages={A145},
- year={2008},
- publisher={IOP Publishing}
-}
-
-@article{landesfeind2019temperature,
- title={Temperature and concentration dependence of the ionic transport properties of lithium-ion battery electrolytes},
- author={Landesfeind, Johannes and Gasteiger, Hubert A},
- journal={Journal of The Electrochemical Society},
- volume={166},
- number={14},
- pages={A3079--A3097},
- year={2019},
- publisher={The Electrochemical Society}
-}
-@article{akanni1987effective,
- title={Effective transport coefficients in heterogeneous media},
- author={Akanni, KA and Evans, JW and Abramson, IS},
- journal={Chemical Engineering Science},
- volume={42},
- number={8},
- pages={1945--1954},
- year={1987},
- publisher={Elsevier}
-}
-@article{petersen1958diffusion,
- title={Diffusion in a pore of varying cross section},
- author={Petersen, EE},
- journal={AIChE Journal},
- volume={4},
- number={3},
- pages={343--345},
- year={1958},
- publisher={Wiley Online Library}
-}
-@article{bruggeman1935berechnung,
- title={Berechnung verschiedener physikalischer Konstanten von heterogenen Substanzen. I. Dielektrizit{\"a}tskonstanten und Leitf{\"a}higkeiten der Mischk{\"o}rper aus isotropen Substanzen},
- author={Bruggeman, Von DAG},
- journal={Annalen der physik},
- volume={416},
- number={7},
- pages={636--664},
- year={1935},
- publisher={Wiley Online Library}
-}
-@article{weissberg1963effective,
- title={Effective diffusion coefficient in porous media},
- author={Weissberg, Harold L},
- journal={Journal of Applied Physics},
- volume={34},
- number={9},
- pages={2636--2639},
- year={1963},
- publisher={American Institute of Physics}
-}
-@article{tomadakis1993transport,
- title={Transport properties of random arrays of freely overlapping cylinders with various orientation distributions},
- author={Tomadakis, Manolis M and Sotirchos, Stratis V},
- journal={The Journal of chemical physics},
- volume={98},
- number={1},
- pages={616--626},
- year={1993},
- publisher={American Institute of Physics}
-}
-@article{beeckman1990mathematical,
- title={Mathematical description of heterogeneous materials},
- author={Beeckman, JW},
- journal={Chemical engineering science},
- volume={45},
- number={8},
- pages={2603--2610},
- year={1990},
- publisher={Elsevier}
-}
-@article{mackie1955diffusion,
- title={The diffusion of electrolytes in a cation-exchange resin membrane I. Theoretical},
- author={Mackie, JS and Meares, P},
- journal={Proceedings of the Royal Society of London. Series A. Mathematical and Physical Sciences},
- volume={232},
- number={1191},
- pages={498--509},
- year={1955},
- publisher={The Royal Society London}
-}
-@article{shen2007critical,
- title={Critical review of the impact of tortuosity on diffusion},
- author={Shen, Lihua and Chen, Zhangxin},
- journal={Chemical Engineering Science},
- volume={62},
- number={14},
- pages={3748--3755},
- year={2007},
- publisher={Elsevier}
-}
-@article{Wycisk2022,
- title = {Modified Plett-model for modeling voltage hysteresis in lithium-ion cells},
- journal = {Journal of Energy Storage},
- volume = {52},
- pages = {105016},
- year = {2022},
- issn = {2352-152X},
- doi = {https://doi.org/10.1016/j.est.2022.105016},
- url = {https://www.sciencedirect.com/science/article/pii/S2352152X22010192},
- author = {Dominik Wycisk and Marc Oldenburger and Marc Gerry Stoye and Toni Mrkonjic and Arnulf Latz},
- keywords = {Lithium-ion battery, Voltage hysteresis, Plett-model, Silicon–graphite anode},
-}
-
-@article{Fan2022,
- title={Data-driven identification of lithium-ion batteries: A nonlinear equivalent circuit model with diffusion dynamics},
- author={Fan, Chuanxin and O’Regan, Kieran and Li, Liuying and Higgins, Matthew D and Kendrick, Emma and Widanage, Widanalage D},
- journal={Applied Energy},
- volume={321},
- pages={119336},
- year={2022},
- publisher={Elsevier}
-}
diff --git a/src/pybamm/__init__.py b/src/pybamm/__init__.py
index 36ad0b137a..b466c3896b 100644
--- a/src/pybamm/__init__.py
+++ b/src/pybamm/__init__.py
@@ -1,5 +1,3 @@
-import sys
-
from pybamm.version import __version__
# Demote expressions to 32-bit floats/ints - option used for IDAKLU-MLIR compilation
@@ -23,6 +21,7 @@
from .logger import logger, set_logging_level, get_new_logger
from .settings import settings
from .citations import Citations, citations, print_citations
+from . import config
# Classes for the Expression Tree
from .expression_tree.symbol import *
@@ -36,10 +35,12 @@
from .expression_tree.broadcasts import *
from .expression_tree.functions import *
from .expression_tree.interpolant import Interpolant
+from .expression_tree.discrete_time_sum import *
from .expression_tree.input_parameter import InputParameter
from .expression_tree.parameter import Parameter, FunctionParameter
from .expression_tree.scalar import Scalar
from .expression_tree.variable import *
+from .expression_tree.coupled_variable import *
from .expression_tree.independent_variable import *
from .expression_tree.independent_variable import t
from .expression_tree.vector import Vector
@@ -75,6 +76,7 @@
from .models.full_battery_models import lead_acid
from .models.full_battery_models import lithium_ion
from .models.full_battery_models import equivalent_circuit
+from .models.full_battery_models import sodium_ion
# Submodel classes
from .models.submodels.base_submodel import BaseSubModel
@@ -157,7 +159,8 @@
# Solver classes
from .solvers.solution import Solution, EmptySolution, make_cycle_solution
-from .solvers.processed_variable import ProcessedVariable
+from .solvers.processed_variable_time_integral import ProcessedVariableTimeIntegral
+from .solvers.processed_variable import ProcessedVariable, process_variable
from .solvers.processed_variable_computed import ProcessedVariableComputed
from .solvers.base_solver import BaseSolver
from .solvers.dummy_solver import DummySolver
@@ -192,19 +195,24 @@
# Batch Study
from .batch_study import BatchStudy
-# Callbacks
-from . import callbacks
+# Callbacks, telemetry, config
+from . import callbacks, telemetry, config
# Pybamm Data manager using pooch
from .pybamm_data import DataLoader
-# Remove any imported modules, so we don't expose them as part of pybamm
-del sys
+# Fix Casadi import
+import os
+import pathlib
+import sysconfig
+
+os.environ["CASADIPATH"] = str(pathlib.Path(sysconfig.get_path("purelib")) / "casadi")
__all__ = [
"batch_study",
"callbacks",
"citations",
+ "config",
"discretisations",
"doc_utils",
"experiment",
@@ -220,8 +228,11 @@
"simulation",
"solvers",
"spatial_methods",
+ "telemetry",
"type_definitions",
"util",
"version",
"pybamm_data",
]
+
+config.generate()
diff --git a/src/pybamm/config.py b/src/pybamm/config.py
new file mode 100644
index 0000000000..0bd5c96eb6
--- /dev/null
+++ b/src/pybamm/config.py
@@ -0,0 +1,173 @@
+import uuid
+import os
+import platformdirs
+from pathlib import Path
+import pybamm
+import sys
+import threading
+import time
+
+
+def check_env_opt_out():
+ return os.getenv("PYBAMM_DISABLE_TELEMETRY", "false").lower() != "false"
+
+
+def check_opt_out():
+ opt_out = check_env_opt_out()
+ config = pybamm.config.read()
+ if config:
+ opt_out = opt_out or not config["enable_telemetry"]
+ return opt_out
+
+
+def is_running_tests(): # pragma: no cover
+ """
+ Detect if the code is being run as part of a test suite or building docs with Sphinx.
+
+ Returns:
+ bool: True if running tests or building docs, False otherwise.
+ """
+ # Check if pytest or unittest is running
+ if any(
+ test_module in sys.modules for test_module in ["pytest", "unittest", "nose"]
+ ):
+ return True
+
+ # Check for other common CI environment variables
+ ci_env_vars = [
+ "GITHUB_ACTIONS",
+ "CI",
+ "TRAVIS",
+ "CIRCLECI",
+ "JENKINS_URL",
+ "GITLAB_CI",
+ ]
+ if any(var in os.environ for var in ci_env_vars):
+ return True
+
+ # Check if building docs with Sphinx
+ if any(mod == "sphinx" or mod.startswith("sphinx.") for mod in sys.modules):
+ print(
+ f"Found Sphinx module: {[mod for mod in sys.modules if mod.startswith('sphinx')]}"
+ )
+ return True
+
+ return False
+
+
+def ask_user_opt_in(timeout=10):
+ """
+ Ask the user if they want to opt in to telemetry.
+
+ Parameters
+ ----------
+ timeout : float, optional
+ The timeout for the user to respond to the prompt. Default is 10 seconds.
+
+ Returns
+ -------
+ bool
+ True if the user opts in, False otherwise.
+ """
+ print(
+ "PyBaMM can collect usage data and send it to the PyBaMM team to "
+ "help us improve the software.\n"
+ "We do not collect any sensitive information such as models, parameters, "
+ "or simulation results - only information on which parts of the code are "
+ "being used and how frequently.\n"
+ "This is entirely optional and does not impact the functionality of PyBaMM.\n"
+ "For more information, see https://docs.pybamm.org/en/latest/source/user_guide/index.html#telemetry"
+ )
+
+ def get_input(): # pragma: no cover
+ try:
+ user_input = (
+ input("Do you want to enable telemetry? (Y/n): ").strip().lower()
+ )
+ answer.append(user_input)
+ except Exception:
+ # Handle any input errors
+ pass
+
+ time_start = time.time()
+
+ while True:
+ if time.time() - time_start > timeout:
+ print("\nTimeout reached. Defaulting to not enabling telemetry.")
+ return False
+
+ answer = []
+ # Create and start input thread
+ input_thread = threading.Thread(target=get_input)
+ input_thread.daemon = True
+ input_thread.start()
+
+ # Wait for either timeout or input
+ input_thread.join(timeout)
+
+ if answer:
+ if answer[0] in ["yes", "y", ""]:
+ print("\nTelemetry enabled.\n")
+ return True
+ elif answer[0] in ["no", "n"]:
+ print("\nTelemetry disabled.\n")
+ return False
+ else:
+ print("\nInvalid input. Please enter 'yes/y' for yes or 'no/n' for no.")
+ else:
+ print("\nTimeout reached. Defaulting to not enabling telemetry.")
+ return False
+
+
+def generate():
+ if is_running_tests() or check_opt_out():
+ return
+
+ # Check if the config file already exists
+ if read() is not None:
+ return
+
+ # Ask the user if they want to opt in to telemetry
+ opt_in = ask_user_opt_in()
+ config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
+ write_uuid_to_file(config_file, opt_in)
+
+ if opt_in:
+ pybamm.telemetry.capture("user-opted-in")
+
+
+def read():
+ config_file = Path(platformdirs.user_config_dir("pybamm")) / "config.yml"
+ return read_uuid_from_file(config_file)
+
+
+def write_uuid_to_file(config_file, opt_in):
+ # Create the directory if it doesn't exist
+ config_file.parent.mkdir(parents=True, exist_ok=True)
+
+ # Write the UUID to the config file in YAML format
+ with open(config_file, "w") as f:
+ f.write("pybamm:\n")
+ f.write(f" enable_telemetry: {opt_in}\n")
+ if opt_in:
+ unique_id = uuid.uuid4()
+ f.write(f" uuid: {unique_id}\n")
+
+
+def read_uuid_from_file(config_file):
+ # Check if the config file exists
+ if not config_file.exists():
+ return None
+
+ # Read the UUID from the config file
+ with open(config_file) as f:
+ content = f.read().strip()
+
+ # Extract the UUID using YAML parsing
+ try:
+ import yaml
+
+ config = yaml.safe_load(content)
+ return config["pybamm"]
+ except (yaml.YAMLError, ValueError):
+ return None
diff --git a/src/pybamm/discretisations/discretisation.py b/src/pybamm/discretisations/discretisation.py
index af4bd2edd6..3d9579ff9c 100644
--- a/src/pybamm/discretisations/discretisation.py
+++ b/src/pybamm/discretisations/discretisation.py
@@ -500,8 +500,8 @@ def check_tab_conditions(self, symbol, bcs):
if domain != "current collector":
raise pybamm.ModelError(
- f"""Boundary conditions can only be applied on the tabs in the domain
- 'current collector', but {symbol} has domain {domain}"""
+ "Boundary conditions can only be applied on the tabs in the domain "
+ f"'current collector', but {symbol} has domain {domain}"
)
# Replace keys with "left" and "right" as appropriate for 1D meshes
if isinstance(mesh, pybamm.SubMesh1D):
@@ -893,11 +893,9 @@ def _process_symbol(self, symbol):
y_slices = self.y_slices[symbol]
except KeyError as error:
raise pybamm.ModelError(
- f"""
- No key set for variable '{symbol.name}'. Make sure it is included in either
- model.rhs or model.algebraic in an unmodified form
- (e.g. not Broadcasted)
- """
+ f"No key set for variable '{symbol.name}'. Make sure it is included in either "
+ "model.rhs or model.algebraic in an unmodified form "
+ "(e.g. not Broadcasted)"
) from error
# Add symbol's reference and multiply by the symbol's scale
# so that the state vector is of order 1
@@ -938,6 +936,11 @@ def _process_symbol(self, symbol):
if symbol._expected_size is None:
symbol._expected_size = expected_size
return symbol.create_copy()
+
+ elif isinstance(symbol, pybamm.CoupledVariable):
+ new_symbol = self.process_symbol(symbol.children[0])
+ return new_symbol
+
else:
# Backup option: return the object
return symbol
diff --git a/src/pybamm/expression_tree/__init__.py b/src/pybamm/expression_tree/__init__.py
index 0b06746e61..7ac80e5353 100644
--- a/src/pybamm/expression_tree/__init__.py
+++ b/src/pybamm/expression_tree/__init__.py
@@ -2,4 +2,4 @@
'concatenations', 'exceptions', 'functions', 'independent_variable',
'input_parameter', 'interpolant', 'matrix', 'operations',
'parameter', 'printing', 'scalar', 'state_vector', 'symbol',
- 'unary_operators', 'variable', 'vector']
+ 'unary_operators', 'variable', 'vector', 'discrete_time_sum' ]
diff --git a/src/pybamm/expression_tree/averages.py b/src/pybamm/expression_tree/averages.py
index 5fa6c5f00f..11538ea153 100644
--- a/src/pybamm/expression_tree/averages.py
+++ b/src/pybamm/expression_tree/averages.py
@@ -251,8 +251,8 @@ def z_average(symbol: pybamm.Symbol) -> pybamm.Symbol:
# Symbol must have domain [] or ["current collector"]
if symbol.domain not in [[], ["current collector"]]:
raise pybamm.DomainError(
- f"""z-average only implemented in the 'current collector' domain,
- but symbol has domains {symbol.domain}"""
+ "z-average only implemented in the 'current collector' domain, "
+ f"but symbol has domains {symbol.domain}"
)
# If symbol doesn't have a domain, its average value is itself
if symbol.domain == []:
@@ -285,8 +285,8 @@ def yz_average(symbol: pybamm.Symbol) -> pybamm.Symbol:
# Symbol must have domain [] or ["current collector"]
if symbol.domain not in [[], ["current collector"]]:
raise pybamm.DomainError(
- f"""y-z-average only implemented in the 'current collector' domain,
- but symbol has domains {symbol.domain}"""
+ "y-z-average only implemented in the 'current collector' domain, "
+ f"but symbol has domains {symbol.domain}"
)
# If symbol doesn't have a domain, its average value is itself
if symbol.domain == []:
diff --git a/src/pybamm/expression_tree/binary_operators.py b/src/pybamm/expression_tree/binary_operators.py
index 1d630887b2..be3df653ad 100644
--- a/src/pybamm/expression_tree/binary_operators.py
+++ b/src/pybamm/expression_tree/binary_operators.py
@@ -36,7 +36,7 @@ def _preprocess_binary(
# Check both left and right are pybamm Symbols
if not (isinstance(left, pybamm.Symbol) and isinstance(right, pybamm.Symbol)):
raise NotImplementedError(
- f"""BinaryOperator not implemented for symbols of type {type(left)} and {type(right)}"""
+ f"BinaryOperator not implemented for symbols of type {type(left)} and {type(right)}"
)
# Do some broadcasting in special cases, to avoid having to do this manually
@@ -389,9 +389,9 @@ def _binary_jac(self, left_jac, right_jac):
return left @ right_jac
else:
raise NotImplementedError(
- f"""jac of 'MatrixMultiplication' is only
- implemented for left of type 'pybamm.Array',
- not {left.__class__}"""
+ f"jac of 'MatrixMultiplication' is only "
+ "implemented for left of type 'pybamm.Array', "
+ f"not {left.__class__}"
)
def _binary_evaluate(self, left, right):
@@ -856,12 +856,17 @@ def _simplified_binary_broadcast_concatenation(
elif isinstance(right, pybamm.Concatenation) and not isinstance(
right, pybamm.ConcatenationVariable
):
- return left.create_copy(
- [
- operator(left_child, right_child)
- for left_child, right_child in zip(left.orphans, right.orphans)
- ]
- )
+ if len(left.orphans) == len(right.orphans):
+ return left.create_copy(
+ [
+ operator(left_child, right_child)
+ for left_child, right_child in zip(left.orphans, right.orphans)
+ ]
+ )
+ else:
+ raise AssertionError(
+ "Concatenations must have the same number of children"
+ )
if isinstance(right, pybamm.Concatenation) and not isinstance(
right, pybamm.ConcatenationVariable
):
@@ -1541,8 +1546,8 @@ def source(
if left.domain != ["current collector"] or right.domain != ["current collector"]:
raise pybamm.DomainError(
- f"""'source' only implemented in the 'current collector' domain,
- but symbols have domains {left.domain} and {right.domain}"""
+ "'source' only implemented in the 'current collector' domain, "
+ f"but symbols have domains {left.domain} and {right.domain}"
)
if boundary:
return pybamm.BoundaryMass(right) @ left
diff --git a/src/pybamm/expression_tree/coupled_variable.py b/src/pybamm/expression_tree/coupled_variable.py
new file mode 100644
index 0000000000..04d03d2792
--- /dev/null
+++ b/src/pybamm/expression_tree/coupled_variable.py
@@ -0,0 +1,55 @@
+import pybamm
+
+from pybamm.type_definitions import DomainType
+
+
+class CoupledVariable(pybamm.Symbol):
+ """
+ A node in the expression tree representing a variable whose equation is set by a different model or submodel.
+
+
+ Parameters
+ ----------
+ name : str
+ name of the node
+ domain : iterable of str
+ list of domains that this coupled variable is valid over
+ """
+
+ def __init__(
+ self,
+ name: str,
+ domain: DomainType = None,
+ ) -> None:
+ super().__init__(name, domain=domain)
+
+ def _evaluate_for_shape(self):
+ """
+ Returns the scalar 'NaN' to represent the shape of a parameter.
+ See :meth:`pybamm.Symbol.evaluate_for_shape()`
+ """
+ return pybamm.evaluate_for_shape_using_domain(self.domains)
+
+ def create_copy(self):
+ """Creates a new copy of the coupled variable."""
+ new_coupled_variable = CoupledVariable(self.name, self.domain)
+ return new_coupled_variable
+
+ @property
+ def children(self):
+ return self._children
+
+ @children.setter
+ def children(self, expr):
+ self._children = expr
+
+ def set_coupled_variable(self, symbol, expr):
+ """Sets the children of the coupled variable to the expression passed in expr. If the symbol is not the coupled variable, then it searches the children of the symbol for the coupled variable. The coupled variable will be replaced by its first child (symbol.children[0], which should be expr) in the discretisation step."""
+ if self == symbol:
+ symbol.children = [
+ expr,
+ ]
+ else:
+ for child in symbol.children:
+ self.set_coupled_variable(child, expr)
+ symbol.set_id()
diff --git a/src/pybamm/expression_tree/discrete_time_sum.py b/src/pybamm/expression_tree/discrete_time_sum.py
new file mode 100644
index 0000000000..41cd14960d
--- /dev/null
+++ b/src/pybamm/expression_tree/discrete_time_sum.py
@@ -0,0 +1,88 @@
+import pybamm
+import numpy as np
+
+
+class DiscreteTimeData(pybamm.Interpolant):
+ """
+ A class for representing data that is only defined at discrete points in time.
+ This is implemented as a 1D interpolant with the time points as the nodes.
+
+ Parameters
+ ----------
+
+ time_points : :class:`numpy.ndarray`
+ The time points at which the data is defined
+ data : :class:`numpy.ndarray`
+ The data to be interpolated
+ name : str
+ The name of the data
+
+ """
+
+ def __init__(self, time_points: np.ndarray, data: np.ndarray, name: str):
+ super().__init__(time_points, data, pybamm.t, name)
+
+ def create_copy(self, new_children=None, perform_simplifications=True):
+ """See :meth:`pybamm.Symbol.new_copy()`."""
+ return pybamm.DiscreteTimeData(self.x[0], self.y, self.name)
+
+
+class DiscreteTimeSum(pybamm.UnaryOperator):
+ """
+ A node in the expression tree representing a discrete time sum operator.
+
+ .. math::
+ \\sum_{i=0}^{N} f(y(t_i), t_i)
+
+ where f is the expression given by the child, and the sum is over the discrete
+ time points t_i. The set of time points is given by the :class:`pybamm.DiscreteTimeData` node,
+ which must be somewhere in the expression tree given by the child. If the child
+ does not contain a :class:`pybamm.DiscreteTimeData` node, then an error will be raised when
+ the node is created. If the child contains multiple :class:`pybamm.DiscreteTimeData` nodes,
+ an error will be raised when the node is created.
+
+
+ Parameters
+ ----------
+ child: :class:`pybamm.Symbol`
+ The symbol to be summed
+
+ Attributes
+ ----------
+ data: :class:`pybamm.DiscreteTimeData`
+ The discrete time data node in the child
+
+ Raises
+ ------
+ :class:`pybamm.ModelError`
+ If the child does not contain a :class:`pybamm.DiscreteTimeData` node, or if the child
+ contains multiple :class:`pybamm.DiscreteTimeData` nodes.
+ """
+
+ def __init__(self, child: pybamm.Symbol):
+ self.data = None
+ for node in child.pre_order():
+ if isinstance(node, DiscreteTimeData):
+ # Check that there is exactly one DiscreteTimeData node in the child
+ if self.data is not None:
+ raise pybamm.ModelError(
+ "DiscreteTimeSum can only have one DiscreteTimeData node in the child"
+ )
+ self.data = node
+ if self.data is None:
+ raise pybamm.ModelError(
+ "DiscreteTimeSum must contain a DiscreteTimeData node"
+ )
+ super().__init__("discrete time sum", child)
+
+ @property
+ def sum_values(self):
+ return self.data.y
+
+ @property
+ def sum_times(self):
+ return self.data.x[0]
+
+ def _unary_evaluate(self, child):
+ # return result of evaluating the child, we'll only implement the sum once the model is solved (in pybamm.ProcessedVariable)
+ return child
diff --git a/src/pybamm/expression_tree/operations/convert_to_casadi.py b/src/pybamm/expression_tree/operations/convert_to_casadi.py
index 274fd95154..6b61b35263 100644
--- a/src/pybamm/expression_tree/operations/convert_to_casadi.py
+++ b/src/pybamm/expression_tree/operations/convert_to_casadi.py
@@ -7,6 +7,7 @@
import casadi
import numpy as np
from scipy import special
+from scipy import interpolate
class CasadiConverter:
@@ -165,6 +166,18 @@ def _convert(self, symbol, t, y, y_dot, inputs):
# for some reason, pybamm.Interpolant always returns a column vector, so match that
test = test.T
return test
+ elif solver == "bspline":
+ bspline = interpolate.make_interp_spline(
+ symbol.x[0], symbol.y, k=3
+ )
+ knots = [bspline.t]
+ coeffs = bspline.c.flatten()
+ degree = [bspline.k]
+ m = len(coeffs) // len(symbol.x[0])
+ f = casadi.Function.bspline(
+ symbol.name, knots, coeffs, degree, m
+ )
+ return f(converted_children[0])
else:
return casadi.interpolant(
"LUT", solver, symbol.x, symbol.y.flatten()
@@ -176,6 +189,20 @@ def _convert(self, symbol, t, y, y_dot, inputs):
symbol.y.ravel(order="F"),
converted_children,
)
+ elif solver == "bspline" and len(converted_children) == 2:
+ bspline = interpolate.RectBivariateSpline(
+ symbol.x[0], symbol.x[1], symbol.y
+ )
+ [tx, ty, c] = bspline.tck
+ [kx, ky] = bspline.degrees
+ knots = [tx, ty]
+ coeffs = c
+ degree = [kx, ky]
+ m = 1
+ f = casadi.Function.bspline(
+ symbol.name, knots, coeffs, degree, m
+ )
+ return f(casadi.hcat(converted_children).T).T
else:
LUT = casadi.interpolant(
"LUT", solver, symbol.x, symbol.y.ravel(order="F")
@@ -231,8 +258,6 @@ def _convert(self, symbol, t, y, y_dot, inputs):
else:
raise TypeError(
- f"""
- Cannot convert symbol of type '{type(symbol)}' to CasADi. Symbols must all be
- 'linear algebra' at this stage.
- """
+ f"Cannot convert symbol of type '{type(symbol)}' to CasADi. Symbols must all be "
+ "'linear algebra' at this stage."
)
diff --git a/src/pybamm/expression_tree/unary_operators.py b/src/pybamm/expression_tree/unary_operators.py
index ace1cd9942..f41897e2de 100644
--- a/src/pybamm/expression_tree/unary_operators.py
+++ b/src/pybamm/expression_tree/unary_operators.py
@@ -212,7 +212,8 @@ def __init__(self, child):
@classmethod
def _from_json(cls, snippet: dict):
- raise NotImplementedError()
+ """See :meth:`pybamm.UnaryOperator._from_json()`."""
+ return cls(snippet["children"][0])
def diff(self, variable):
"""See :meth:`pybamm.Symbol.diff()`."""
@@ -991,8 +992,8 @@ def __init__(self, name, child, side):
if side in ["negative tab", "positive tab"]:
if child.domain[0] != "current collector":
raise pybamm.ModelError(
- f"""Can only take boundary value on the tabs in the domain
- 'current collector', but {child} has domain {child.domain[0]}"""
+ "Can only take boundary value on the tabs in the domain "
+ f"'current collector', but {child} has domain {child.domain[0]}"
)
self.side = side
# boundary value of a child takes the primary domain from secondary domain
diff --git a/src/pybamm/expression_tree/vector.py b/src/pybamm/expression_tree/vector.py
index 6dc358afb0..e9067a4ffd 100644
--- a/src/pybamm/expression_tree/vector.py
+++ b/src/pybamm/expression_tree/vector.py
@@ -29,9 +29,7 @@ def __init__(
entries = entries[:, np.newaxis]
if entries.shape[1] != 1:
raise ValueError(
- f"""
- Entries must have 1 dimension or be column vector, not have shape {entries.shape}
- """
+ f"Entries must have 1 dimension or be column vector, not have shape {entries.shape}"
)
if name is None:
name = f"Column vector of length {entries.shape[0]!s}"
diff --git a/src/pybamm/input/parameters/__init__.py b/src/pybamm/input/parameters/__init__.py
index 9ef23b743d..3c21058270 100644
--- a/src/pybamm/input/parameters/__init__.py
+++ b/src/pybamm/input/parameters/__init__.py
@@ -1 +1 @@
-__all__ = ['ecm', 'lead_acid', 'lithium_ion']
+__all__ = ['ecm', 'lead_acid', 'lithium_ion', 'sodium_ion']
diff --git a/src/pybamm/input/parameters/lithium_ion/Ai2020.py b/src/pybamm/input/parameters/lithium_ion/Ai2020.py
index b45c04fa7f..4bf51f3440 100644
--- a/src/pybamm/input/parameters/lithium_ion/Ai2020.py
+++ b/src/pybamm/input/parameters/lithium_ion/Ai2020.py
@@ -5,7 +5,7 @@
def graphite_diffusivity_Dualfoil1998(sto, T):
"""
- Graphite diffusivity as a function of stochiometry [1, 2, 3].
+ Graphite diffusivity as a function of stoichiometry [1, 2, 3].
References
----------
@@ -20,7 +20,7 @@ def graphite_diffusivity_Dualfoil1998(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature, [K]
@@ -72,10 +72,10 @@ def graphite_electrolyte_exchange_current_density_Dualfoil1998(
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max):
+def graphite_entropy_Enertech_Ai2020_function(sto):
"""
Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at
- a temperature of 298.15K as a function of the stochiometry. The fit is taken
+ a temperature of 298.15K as a function of the stoichiometry. The fit is taken
from Ref [1], which is only accurate
for 0.43 < sto < 0.9936.
@@ -89,7 +89,7 @@ def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max):
Parameters
----------
sto: double
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
Returns
-------
@@ -126,9 +126,9 @@ def graphite_entropy_Enertech_Ai2020_function(sto, c_s_max):
return du_dT
-def graphite_volume_change_Ai2020(sto, c_s_max):
+def graphite_volume_change_Ai2020(sto):
"""
- Graphite particle volume change as a function of stochiometry [1, 2].
+ Graphite particle volume change as a function of stoichiometry [1, 2].
References
----------
@@ -143,7 +143,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry, dimensionless
+ Electrode stoichiometry, dimensionless
should be R-averaged particle concentration
c_s_max : :class:`pybamm.Symbol`
Maximum particle concentration [mol.m-3]
@@ -214,7 +214,7 @@ def graphite_cracking_rate_Ai2020(T_dim):
def lico2_diffusivity_Dualfoil1998(sto, T):
"""
- LiCo2 diffusivity as a function of stochiometry, in this case the
+ LiCo2 diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Dualfoil [1].
References
@@ -224,7 +224,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature, [K]
@@ -273,10 +273,10 @@ def lico2_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_m
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def lico2_entropic_change_Ai2020_function(sto, c_s_max):
+def lico2_entropic_change_Ai2020_function(sto):
"""
Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at
- a temperature of 298.15K as a function of the stochiometry. The fit is taken
+ a temperature of 298.15K as a function of the stoichiometry. The fit is taken
from Ref [1], which is only accurate
for 0.43 < sto < 0.9936.
@@ -290,7 +290,7 @@ def lico2_entropic_change_Ai2020_function(sto, c_s_max):
Parameters
----------
sto: double
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
Returns
-------
@@ -323,9 +323,9 @@ def lico2_entropic_change_Ai2020_function(sto, c_s_max):
return du_dT
-def lico2_volume_change_Ai2020(sto, c_s_max):
+def lico2_volume_change_Ai2020(sto):
"""
- lico2 particle volume change as a function of stochiometry [1, 2].
+ lico2 particle volume change as a function of stoichiometry [1, 2].
References
----------
@@ -340,10 +340,8 @@ def lico2_volume_change_Ai2020(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry, dimensionless
+ Electrode stoichiometry, dimensionless
should be R-averaged particle concentration
- c_s_max : :class:`pybamm.Symbol`
- Maximum particle concentration [mol.m-3]
Returns
-------
@@ -351,6 +349,7 @@ def lico2_volume_change_Ai2020(sto, c_s_max):
volume change, dimensionless, normalised by particle volume
"""
omega = pybamm.Parameter("Positive electrode partial molar volume [m3.mol-1]")
+ c_s_max = pybamm.Parameter("Maximum concentration in positive electrode [mol.m-3]")
t_change = omega * c_s_max * sto
return t_change
@@ -518,11 +517,11 @@ def lico2_ocp_Ai2020(sto):
def get_parameter_values():
"""
Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`,
- :footcite:t:`rieger2016new` and references therein.
+ :footcite:t:`Rieger2016` and references therein.
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
.. note::
diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020.py b/src/pybamm/input/parameters/lithium_ion/Chen2020.py
index 5a7460871b..eccac74615 100644
--- a/src/pybamm/input/parameters/lithium_ion/Chen2020.py
+++ b/src/pybamm/input/parameters/lithium_ion/Chen2020.py
@@ -4,7 +4,7 @@
def graphite_LGM50_ocp_Chen2020(sto):
"""
- LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken
+ LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken
from [1].
References
@@ -17,7 +17,7 @@ def graphite_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -75,7 +75,7 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020(
def nmc_LGM50_ocp_Chen2020(sto):
"""
- LG M50 NMC open-circuit potential as a function of stochiometry, fit taken
+ LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken
from [1].
References
@@ -88,7 +88,7 @@ def nmc_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -213,8 +213,8 @@ def get_parameter_values():
therein.
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
.. note::
diff --git a/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py b/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py
index 58b6211072..69b622a7c5 100644
--- a/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py
+++ b/src/pybamm/input/parameters/lithium_ion/Chen2020_composite.py
@@ -43,7 +43,7 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020(
def silicon_ocp_lithiation_Mark2016(sto):
"""
silicon Open-circuit Potential (OCP) as a a function of the
- stochiometry. The fit is taken from the Enertech cell [1], which is only accurate
+ stoichiometry. The fit is taken from the Enertech cell [1], which is only accurate
for 0 < sto < 1.
References
@@ -55,7 +55,7 @@ def silicon_ocp_lithiation_Mark2016(sto):
Parameters
----------
sto: double
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
Returns
-------
@@ -87,7 +87,7 @@ def silicon_ocp_lithiation_Mark2016(sto):
def silicon_ocp_delithiation_Mark2016(sto):
"""
silicon Open-circuit Potential (OCP) as a a function of the
- stochiometry. The fit is taken from the Enertech cell [1], which is only accurate
+ stoichiometry. The fit is taken from the Enertech cell [1], which is only accurate
for 0 < sto < 1.
References
@@ -99,7 +99,7 @@ def silicon_ocp_delithiation_Mark2016(sto):
Parameters
----------
sto: double
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
Returns
-------
@@ -170,7 +170,7 @@ def silicon_LGM50_electrolyte_exchange_current_density_Chen2020(
def nmc_LGM50_ocp_Chen2020(sto):
"""
- LG M50 NMC open-circuit potential as a function of stochiometry, fit taken
+ LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken
from [1].
References
@@ -183,7 +183,7 @@ def nmc_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py
index 32cc631293..30ca2ef827 100644
--- a/src/pybamm/input/parameters/lithium_ion/Ecker2015.py
+++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015.py
@@ -4,7 +4,7 @@
def graphite_diffusivity_Ecker2015(sto, T):
"""
- Graphite diffusivity as a function of stochiometry [1, 2, 3].
+ Graphite diffusivity as a function of stoichiometry [1, 2, 3].
References
----------
@@ -21,7 +21,7 @@ def graphite_diffusivity_Ecker2015(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -42,7 +42,7 @@ def graphite_diffusivity_Ecker2015(sto, T):
def graphite_ocp_Ecker2015(sto):
"""
- Graphite OCP as a function of stochiometry [1, 2, 3].
+ Graphite OCP as a function of stoichiometry [1, 2, 3].
References
----------
@@ -59,7 +59,7 @@ def graphite_ocp_Ecker2015(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -152,7 +152,7 @@ def graphite_electrolyte_exchange_current_density_Ecker2015(c_e, c_s_surf, c_s_m
def nco_diffusivity_Ecker2015(sto, T):
"""
- NCO diffusivity as a function of stochiometry [1, 2, 3].
+ NCO diffusivity as a function of stoichiometry [1, 2, 3].
References
----------
@@ -169,7 +169,7 @@ def nco_diffusivity_Ecker2015(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -190,7 +190,7 @@ def nco_diffusivity_Ecker2015(sto, T):
def nco_ocp_Ecker2015(sto):
"""
- NCO OCP as a function of stochiometry [1, 2, 3].
+ NCO OCP as a function of stoichiometry [1, 2, 3].
References
----------
@@ -207,7 +207,7 @@ def nco_ocp_Ecker2015(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -488,8 +488,8 @@ def get_parameter_values():
by Dr. Simon O'Kane in the paper :footcite:t:`Richardson2020`
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
.. note::
diff --git a/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py b/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py
index 365bb6386c..267f55e774 100644
--- a/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py
+++ b/src/pybamm/input/parameters/lithium_ion/Ecker2015_graphite_halfcell.py
@@ -34,7 +34,7 @@ def li_metal_electrolyte_exchange_current_density_Xu2019(c_e, c_Li, T):
def graphite_diffusivity_Ecker2015(sto, T):
"""
- Graphite diffusivity as a function of stochiometry [1, 2, 3].
+ Graphite diffusivity as a function of stoichiometry [1, 2, 3].
References
----------
@@ -51,7 +51,7 @@ def graphite_diffusivity_Ecker2015(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -72,7 +72,7 @@ def graphite_diffusivity_Ecker2015(sto, T):
def graphite_ocp_Ecker2015(sto):
"""
- Graphite OCP as a function of stochiometry [1, 2, 3].
+ Graphite OCP as a function of stoichiometry [1, 2, 3].
References
----------
@@ -89,7 +89,7 @@ def graphite_ocp_Ecker2015(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
diff --git a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py
index b1f63e6ff7..13b8f57966 100644
--- a/src/pybamm/input/parameters/lithium_ion/Marquis2019.py
+++ b/src/pybamm/input/parameters/lithium_ion/Marquis2019.py
@@ -4,7 +4,7 @@
def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
"""
- Graphite MCMB 2528 diffusivity as a function of stochiometry, in this case the
+ Graphite MCMB 2528 diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Dualfoil [1].
References
@@ -14,7 +14,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -34,7 +34,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
def graphite_mcmb2528_ocp_Dualfoil1998(sto):
"""
Graphite MCMB 2528 Open-circuit Potential (OCP) as a function of the
- stochiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data
+ stoichiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data
was measured by Chris Bogatu at Telcordia and PolyStor materials, 2000. However,
we could not find any other records of this measurment.
@@ -93,10 +93,10 @@ def graphite_electrolyte_exchange_current_density_Dualfoil1998(
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_entropic_change_Moura2016(sto, c_s_max):
+def graphite_entropic_change_Moura2016(sto):
"""
Graphite entropic change in open-circuit potential (OCP) at a temperature of
- 298.15K as a function of the stochiometry taken from Scott Moura's FastDFN code
+ 298.15K as a function of the stoichiometry taken from Scott Moura's FastDFN code
[1].
References
@@ -106,9 +106,12 @@ def graphite_entropic_change_Moura2016(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
+ # Original parametrization was expressed in terms of c_s_max, but we want to
+ # express it in terms of stoichiometry only
+ c_s_max = 24983.2619938437
du_dT = (
-1.5 * (120.0 / c_s_max) * np.exp(-120 * sto)
+ (0.0351 / (0.083 * c_s_max)) * ((np.cosh((sto - 0.286) / 0.083)) ** (-2))
@@ -126,7 +129,7 @@ def graphite_entropic_change_Moura2016(sto, c_s_max):
def lico2_diffusivity_Dualfoil1998(sto, T):
"""
- LiCo2 diffusivity as a function of stochiometry, in this case the
+ LiCo2 diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Dualfoil [1].
References
@@ -136,7 +139,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -155,7 +158,7 @@ def lico2_diffusivity_Dualfoil1998(sto, T):
def lico2_ocp_Dualfoil1998(sto):
"""
Lithium Cobalt Oxide (LiCO2) Open-circuit Potential (OCP) as a a function of the
- stochiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data
+ stoichiometry. The fit is taken from Dualfoil [1]. Dualfoil states that the data
was measured by Oscar Garcia 2001 using Quallion electrodes for 0.5 < sto < 0.99
and by Marc Doyle for sto<0.4 (for unstated electrodes). We could not find any
other records of the Garcia measurements. Doyles fits can be found in his
@@ -170,7 +173,7 @@ def lico2_ocp_Dualfoil1998(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -222,10 +225,10 @@ def lico2_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_m
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def lico2_entropic_change_Moura2016(sto, c_s_max):
+def lico2_entropic_change_Moura2016(sto):
"""
Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at
- a temperature of 298.15K as a function of the stochiometry. The fit is taken
+ a temperature of 298.15K as a function of the stoichiometry. The fit is taken
from Scott Moura's FastDFN code [1].
References
@@ -235,13 +238,15 @@ def lico2_entropic_change_Moura2016(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
# Since the equation for LiCo2 from this ref. has the stretch factor,
# should this too? If not, the "bumps" in the OCV don't line up.
stretch = 1.062
sto = stretch * sto
-
+ # Original parametrization was expressed in terms of c_s_max, but we want to
+ # express it in terms of stoichiometry only
+ c_s_max = 51217.9257309275
du_dT = (
0.07645 * (-54.4806 / c_s_max) * ((1.0 / np.cosh(30.834 - 54.4806 * sto)) ** 2)
+ 2.1581 * (-50.294 / c_s_max) * ((np.cosh(52.294 - 50.294 * sto)) ** (-2))
@@ -333,8 +338,8 @@ def get_parameter_values():
and references therein.
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
.. note::
diff --git a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py
index 9923d9d308..044cebe3c5 100644
--- a/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py
+++ b/src/pybamm/input/parameters/lithium_ion/Mohtat2020.py
@@ -4,7 +4,7 @@
def graphite_diffusivity_PeymanMPM(sto, T):
"""
- Graphite diffusivity as a function of stochiometry, in this case the
+ Graphite diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Peyman MPM.
References
@@ -14,7 +14,7 @@ def graphite_diffusivity_PeymanMPM(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -34,7 +34,7 @@ def graphite_diffusivity_PeymanMPM(sto, T):
def graphite_ocp_PeymanMPM(sto):
"""
Graphite Open-circuit Potential (OCP) as a function of the
- stochiometry. The fit is taken from Peyman MPM [1].
+ stoichiometry. The fit is taken from Peyman MPM [1].
References
----------
@@ -89,10 +89,10 @@ def graphite_electrolyte_exchange_current_density_PeymanMPM(c_e, c_s_surf, c_s_m
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_entropic_change_PeymanMPM(sto, c_s_max):
+def graphite_entropic_change_PeymanMPM(sto):
"""
Graphite entropic change in open-circuit potential (OCP) at a temperature of
- 298.15K as a function of the stochiometry taken from [1]
+ 298.15K as a function of the stoichiometry taken from [1]
References
----------
@@ -102,7 +102,7 @@ def graphite_entropic_change_PeymanMPM(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -121,7 +121,7 @@ def graphite_entropic_change_PeymanMPM(sto, c_s_max):
def NMC_diffusivity_PeymanMPM(sto, T):
"""
- NMC diffusivity as a function of stochiometry, in this case the
+ NMC diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Peyman MPM.
References
@@ -131,7 +131,7 @@ def NMC_diffusivity_PeymanMPM(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -151,7 +151,7 @@ def NMC_diffusivity_PeymanMPM(sto, T):
def NMC_ocp_PeymanMPM(sto):
"""
Nickel Managanese Cobalt Oxide (NMC) Open-circuit Potential (OCP) as a
- function of the stochiometry. The fit is taken from Peyman MPM.
+ function of the stoichiometry. The fit is taken from Peyman MPM.
References
----------
@@ -160,7 +160,7 @@ def NMC_ocp_PeymanMPM(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -209,7 +209,7 @@ def NMC_electrolyte_exchange_current_density_PeymanMPM(c_e, c_s_surf, c_s_max, T
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def NMC_entropic_change_PeymanMPM(sto, c_s_max):
+def NMC_entropic_change_PeymanMPM(sto):
"""
Nickel Manganese Cobalt (NMC) entropic change in open-circuit potential (OCP) at
a temperature of 298.15K as a function of the OCP. The fit is taken from [1].
@@ -224,7 +224,7 @@ def NMC_entropic_change_PeymanMPM(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -319,8 +319,8 @@ def get_parameter_values():
and references therein.
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
SEI parameters
diff --git a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py
index 7d0478b6d0..da1191fa8c 100644
--- a/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py
+++ b/src/pybamm/input/parameters/lithium_ion/NCA_Kim2011.py
@@ -16,7 +16,7 @@ def graphite_diffusivity_Kim2011(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -35,7 +35,7 @@ def graphite_diffusivity_Kim2011(sto, T):
def graphite_ocp_Kim2011(sto):
"""
- Graphite Open-circuit Potential (OCP) as a function of the stochiometry [1].
+ Graphite Open-circuit Potential (OCP) as a function of the stoichiometry [1].
References
----------
@@ -92,7 +92,7 @@ def graphite_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max
"""
i0_ref = 36 # reference exchange current density at 100% SOC
- sto = 0.36 # stochiometry at 100% SOC
+ sto = 0.36 # stoichiometry at 100% SOC
c_s_n_ref = sto * c_s_max # reference electrode concentration
c_e_ref = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
alpha = 0.5 # charge transfer coefficient
@@ -111,7 +111,7 @@ def graphite_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max
def nca_diffusivity_Kim2011(sto, T):
"""
- NCA diffusivity as a function of stochiometry [1].
+ NCA diffusivity as a function of stoichiometry [1].
References
----------
@@ -123,7 +123,7 @@ def nca_diffusivity_Kim2011(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -168,7 +168,7 @@ def nca_electrolyte_exchange_current_density_Kim2011(c_e, c_s_surf, c_s_max, T):
Exchange-current density [A.m-2]
"""
i0_ref = 4 # reference exchange current density at 100% SOC
- sto = 0.41 # stochiometry at 100% SOC
+ sto = 0.41 # stoichiometry at 100% SOC
c_s_ref = sto * c_s_max # reference electrode concentration
c_e_ref = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
alpha = 0.5 # charge transfer coefficient
@@ -252,7 +252,7 @@ def electrolyte_conductivity_Kim2011(c_e, T):
def nca_ocp_Kim2011(sto):
"""
- Graphite Open Circuit Potential (OCP) as a function of the stochiometry [1].
+ Graphite Open Circuit Potential (OCP) as a function of the stoichiometry [1].
References
----------
@@ -297,8 +297,8 @@ def get_parameter_values():
for the planar effective thermal conductivity.
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`
.. note::
diff --git a/src/pybamm/input/parameters/lithium_ion/OKane2022.py b/src/pybamm/input/parameters/lithium_ion/OKane2022.py
index b1e852dbdf..4ccb72bf62 100644
--- a/src/pybamm/input/parameters/lithium_ion/OKane2022.py
+++ b/src/pybamm/input/parameters/lithium_ion/OKane2022.py
@@ -96,7 +96,7 @@ def SEI_limited_dead_lithium_OKane2022(L_sei):
def graphite_LGM50_diffusivity_Chen2020(sto, T):
"""
- LG M50 Graphite diffusivity as a function of stochiometry, in this case the
+ LG M50 Graphite diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from [1].
References
@@ -109,7 +109,7 @@ def graphite_LGM50_diffusivity_Chen2020(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -165,9 +165,9 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020(
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_volume_change_Ai2020(sto, c_s_max):
+def graphite_volume_change_Ai2020(sto):
"""
- Graphite particle volume change as a function of stochiometry [1, 2].
+ Graphite particle volume change as a function of stoichiometry [1, 2].
References
----------
@@ -182,7 +182,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry, dimensionless
+ Electrode stoichiometry, dimensionless
should be R-averaged particle concentration
Returns
-------
@@ -260,7 +260,7 @@ def nmc_LGM50_diffusivity_Chen2020(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -279,7 +279,7 @@ def nmc_LGM50_diffusivity_Chen2020(sto, T):
def nmc_LGM50_ocp_Chen2020(sto):
"""
- LG M50 NMC open-circuit potential as a function of stochiometry, fit taken
+ LG M50 NMC open-circuit potential as a function of stoichiometry, fit taken
from [1].
References
@@ -292,7 +292,7 @@ def nmc_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -344,9 +344,9 @@ def nmc_LGM50_electrolyte_exchange_current_density_Chen2020(c_e, c_s_surf, c_s_m
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def volume_change_Ai2020(sto, c_s_max):
+def volume_change_Ai2020(sto):
"""
- Particle volume change as a function of stochiometry [1, 2].
+ Particle volume change as a function of stoichiometry [1, 2].
References
----------
@@ -361,7 +361,7 @@ def volume_change_Ai2020(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry, dimensionless
+ Electrode stoichiometry, dimensionless
should be R-averaged particle concentration
Returns
-------
@@ -369,6 +369,7 @@ def volume_change_Ai2020(sto, c_s_max):
volume change, dimensionless, normalised by particle volume
"""
omega = pybamm.Parameter("Positive electrode partial molar volume [m3.mol-1]")
+ c_s_max = pybamm.Parameter("Maximum concentration in positive electrode [mol.m-3]")
t_change = omega * c_s_max * sto
return t_change
diff --git a/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py b/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py
index 35533ba80e..c343dd23f4 100644
--- a/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py
+++ b/src/pybamm/input/parameters/lithium_ion/OKane2022_graphite_SiOx_halfcell.py
@@ -126,7 +126,7 @@ def SEI_limited_dead_lithium_OKane2022(L_sei):
def graphite_LGM50_diffusivity_Chen2020(sto, T):
"""
- LG M50 Graphite diffusivity as a function of stochiometry, in this case the
+ LG M50 Graphite diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from [1].
References
@@ -139,7 +139,7 @@ def graphite_LGM50_diffusivity_Chen2020(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -195,9 +195,9 @@ def graphite_LGM50_electrolyte_exchange_current_density_Chen2020(
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_volume_change_Ai2020(sto, c_s_max):
+def graphite_volume_change_Ai2020(sto):
"""
- Graphite particle volume change as a function of stochiometry [1, 2].
+ Graphite particle volume change as a function of stoichiometry [1, 2].
References
----------
@@ -212,7 +212,7 @@ def graphite_volume_change_Ai2020(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry, dimensionless
+ Electrode stoichiometry, dimensionless
should be R-averaged particle concentration
Returns
-------
diff --git a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py
index 3ca5f6824c..d7e240a7b6 100644
--- a/src/pybamm/input/parameters/lithium_ion/ORegan2022.py
+++ b/src/pybamm/input/parameters/lithium_ion/ORegan2022.py
@@ -233,7 +233,7 @@ def copper_thermal_conductivity_CRC(T):
def graphite_LGM50_diffusivity_ORegan2022(sto, T):
"""
- LG M50 Graphite diffusivity as a function of stochiometry, in this case the
+ LG M50 Graphite diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from [1].
References
@@ -245,7 +245,7 @@ def graphite_LGM50_diffusivity_ORegan2022(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -292,7 +292,7 @@ def graphite_LGM50_diffusivity_ORegan2022(sto, T):
def graphite_LGM50_ocp_Chen2020(sto):
"""
- LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken
+ LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken
from [1].
References
@@ -305,7 +305,7 @@ def graphite_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -439,10 +439,10 @@ def graphite_LGM50_thermal_conductivity_ORegan2022(T):
return lambda_wet
-def graphite_LGM50_entropic_change_ORegan2022(sto, c_s_max):
+def graphite_LGM50_entropic_change_ORegan2022(sto):
"""
LG M50 Graphite entropic change in open-circuit potential (OCP) at a temperature of
- 298.15K as a function of the stochiometry. The fit is taken from [1].
+ 298.15K as a function of the stoichiometry. The fit is taken from [1].
References
----------
@@ -453,7 +453,7 @@ def graphite_LGM50_entropic_change_ORegan2022(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -525,7 +525,7 @@ def nmc_LGM50_diffusivity_ORegan2022(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -579,7 +579,7 @@ def nmc_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -712,10 +712,10 @@ def nmc_LGM50_thermal_conductivity_ORegan2022(T):
return lambda_wet
-def nmc_LGM50_entropic_change_ORegan2022(sto, c_s_max):
+def nmc_LGM50_entropic_change_ORegan2022(sto):
"""
LG M50 NMC 811 entropic change in open-circuit potential (OCP) at a temperature of
- 298.15K as a function of the stochiometry. The fit is taken from [1].
+ 298.15K as a function of the stoichiometry. The fit is taken from [1].
References
----------
@@ -726,7 +726,7 @@ def nmc_LGM50_entropic_change_ORegan2022(sto, c_s_max):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -921,7 +921,7 @@ def get_parameter_values():
Parameters for an LG M50 cell, from the paper :footcite:t:`ORegan2022`
Parameters for a LiPF6 in EC:EMC (3:7 w:w) electrolyte are from the paper
- :footcite:t:`landesfeind2019temperature` and references therein.
+ :footcite:t:`Landesfeind2019` and references therein.
"""
return {
diff --git a/src/pybamm/input/parameters/lithium_ion/Prada2013.py b/src/pybamm/input/parameters/lithium_ion/Prada2013.py
index 0ba56516ab..f27ba23bdd 100644
--- a/src/pybamm/input/parameters/lithium_ion/Prada2013.py
+++ b/src/pybamm/input/parameters/lithium_ion/Prada2013.py
@@ -4,7 +4,7 @@
def graphite_LGM50_ocp_Chen2020(sto):
"""
- LG M50 Graphite open-circuit potential as a function of stochiometry, fit taken
+ LG M50 Graphite open-circuit potential as a function of stoichiometry, fit taken
from [1]. Prada2013 doesn't give an OCP for graphite, so we use this instead.
References
@@ -17,7 +17,7 @@ def graphite_LGM50_ocp_Chen2020(sto):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
Returns
-------
@@ -86,7 +86,7 @@ def LFP_ocp_Afshar2017(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
diff --git a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py
index 879a5f55c6..82c0df76bf 100644
--- a/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py
+++ b/src/pybamm/input/parameters/lithium_ion/Ramadass2004.py
@@ -4,7 +4,7 @@
def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
"""
- Graphite MCMB 2528 diffusivity as a function of stochiometry, in this case the
+ Graphite MCMB 2528 diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Dualfoil [1].
References
@@ -14,7 +14,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -34,7 +34,7 @@ def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
def graphite_ocp_Ramadass2004(sto):
"""
Graphite Open-circuit Potential (OCP) as a function of the
- stochiometry (theta?). The fit is taken from Ramadass 2004.
+ stoichiometry (theta?). The fit is taken from Ramadass 2004.
References
----------
@@ -92,10 +92,10 @@ def graphite_electrolyte_exchange_current_density_Ramadass2004(
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def graphite_entropic_change_Moura2016(sto, c_s_max):
+def graphite_entropic_change_Moura2016(sto):
"""
Graphite entropic change in open-circuit potential (OCP) at a temperature of
- 298.15K as a function of the stochiometry taken from Scott Moura's FastDFN code
+ 298.15K as a function of the stoichiometry taken from Scott Moura's FastDFN code
[1].
References
@@ -105,9 +105,12 @@ def graphite_entropic_change_Moura2016(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
+ # Original parametrization was expressed in terms of c_s_max, but we want to
+ # express it in terms of stoichiometry only
+ c_s_max = 24983.2619938437
du_dT = (
-1.5 * (120.0 / c_s_max) * np.exp(-120 * sto)
+ (0.0351 / (0.083 * c_s_max)) * ((np.cosh((sto - 0.286) / 0.083)) ** (-2))
@@ -125,7 +128,7 @@ def graphite_entropic_change_Moura2016(sto, c_s_max):
def lico2_diffusivity_Ramadass2004(sto, T):
"""
- LiCo2 diffusivity as a function of stochiometry, in this case the
+ LiCo2 diffusivity as a function of stoichiometry, in this case the
diffusivity is taken to be a constant. The value is taken from Ramadass 2004.
References
@@ -137,7 +140,7 @@ def lico2_diffusivity_Ramadass2004(sto, T):
Parameters
----------
sto: :class:`pybamm.Symbol`
- Electrode stochiometry
+ Electrode stoichiometry
T: :class:`pybamm.Symbol`
Dimensional temperature
@@ -156,7 +159,7 @@ def lico2_diffusivity_Ramadass2004(sto, T):
def lico2_ocp_Ramadass2004(sto):
"""
Lithium Cobalt Oxide (LiCO2) Open-circuit Potential (OCP) as a a function of the
- stochiometry. The fit is taken from Ramadass 2004. Stretch is considered the
+ stoichiometry. The fit is taken from Ramadass 2004. Stretch is considered the
overhang area negative electrode / area positive electrode, in Ramadass 2002.
References
@@ -168,7 +171,7 @@ def lico2_ocp_Ramadass2004(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -228,10 +231,10 @@ def lico2_electrolyte_exchange_current_density_Ramadass2004(c_e, c_s_surf, c_s_m
return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_s_max - c_s_surf) ** 0.5
-def lico2_entropic_change_Moura2016(sto, c_s_max):
+def lico2_entropic_change_Moura2016(sto):
"""
Lithium Cobalt Oxide (LiCO2) entropic change in open-circuit potential (OCP) at
- a temperature of 298.15K as a function of the stochiometry. The fit is taken
+ a temperature of 298.15K as a function of the stoichiometry. The fit is taken
from Scott Moura's FastDFN code [1].
References
@@ -241,13 +244,15 @@ def lico2_entropic_change_Moura2016(sto, c_s_max):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
# Since the equation for LiCo2 from this ref. has the stretch factor,
# should this too? If not, the "bumps" in the OCV don't line up.
stretch = 1.062
sto = stretch * sto
-
+ # Original parametrization was expressed in terms of c_s_max, but we want to
+ # express it in terms of stoichiometry only
+ c_s_max = 51217.9257309275
du_dT = (
0.07645 * (-54.4806 / c_s_max) * ((1.0 / np.cosh(30.834 - 54.4806 * sto)) ** 2)
+ 2.1581 * (-50.294 / c_s_max) * ((np.cosh(52.294 - 50.294 * sto)) ** (-2))
@@ -350,7 +355,7 @@ def get_parameter_values():
:footcite:t:`Zhao2018`
Parameters for SEI growth are from the papers :footcite:t:`Ramadass2004` and
- :footcite:t:`safari2008multimodal`
+ :footcite:t:`Safari2008`
.. note::
Ramadass 2004 has mistakes in units and values of SEI parameters, corrected by
diff --git a/src/pybamm/input/parameters/lithium_ion/Xu2019.py b/src/pybamm/input/parameters/lithium_ion/Xu2019.py
index edf3bd40b0..d1c5edea98 100644
--- a/src/pybamm/input/parameters/lithium_ion/Xu2019.py
+++ b/src/pybamm/input/parameters/lithium_ion/Xu2019.py
@@ -36,7 +36,7 @@ def li_metal_electrolyte_exchange_current_density_Xu2019(c_e, c_Li, T):
def nmc_ocp_Xu2019(sto):
"""
Nickel Managanese Cobalt Oxide (NMC) Open-circuit Potential (OCP) as a
- function of the stochiometry, from [1].
+ function of the stoichiometry, from [1].
References
----------
@@ -48,7 +48,7 @@ def nmc_ocp_Xu2019(sto):
Parameters
----------
sto : :class:`pybamm.Symbol`
- Stochiometry of material (li-fraction)
+ stoichiometry of material (li-fraction)
"""
@@ -201,8 +201,8 @@ def get_parameter_values():
^^^^^^^^^^^^^^^^^^^^^^
SEI parameters are example parameters for SEI growth from the papers
- :footcite:t:`Ramadass2004`, :footcite:t:`ploehn2004solvent`,
- :footcite:t:`single2018identifying`, :footcite:t:`safari2008multimodal`, and
+ :footcite:t:`Ramadass2004`, :footcite:t:`Ploehn2004`,
+ :footcite:t:`Single2018`, :footcite:t:`Safari2008`, and
:footcite:t:`Yang2017`.
.. note::
diff --git a/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py
new file mode 100644
index 0000000000..f8c423cf76
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/Chayambuka2022.py
@@ -0,0 +1,341 @@
+import pybamm
+import os
+
+path, _ = os.path.split(os.path.abspath(__file__))
+
+U_n_data = pybamm.parameters.process_1D_data("U_n.csv", path=path)
+U_p_data = pybamm.parameters.process_1D_data("U_p.csv", path=path)
+D_n_data = pybamm.parameters.process_1D_data("D_n.csv", path=path)
+D_p_data = pybamm.parameters.process_1D_data("D_p.csv", path=path)
+k_n_data = pybamm.parameters.process_1D_data("k_n.csv", path=path)
+k_p_data = pybamm.parameters.process_1D_data("k_p.csv", path=path)
+D_e_data = pybamm.parameters.process_1D_data("D_e.csv", path=path)
+sigma_e_data = pybamm.parameters.process_1D_data("sigma_e.csv", path=path)
+
+
+def HC_ocp_Chayambuka2022(sto):
+ """
+ HC open-circuit potential as a function of stochiometry, data taken
+ from [1].
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ sto: :class:`pybamm.Symbol`
+ Electrode stochiometry
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Open-circuit potential
+ """
+
+ name, (x, y) = U_n_data
+ return pybamm.Interpolant(x, y, sto, name)
+
+
+def HC_diffusivity_Chayambuka2022(sto, T):
+ """
+ HC diffusivity as a function of stochiometry, the data is taken from [1].
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ sto: :class:`pybamm.Symbol`
+ Electrode stochiometry
+ T: :class:`pybamm.Symbol`
+ Dimensional temperature
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Solid diffusivity
+ """
+
+ name, (x, y) = D_n_data
+ c_max = pybamm.Parameter("Maximum concentration in negative electrode [mol.m-3]")
+ return pybamm.Interpolant(x, y, sto * c_max, name)
+
+
+def HC_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T):
+ """
+ Exchange-current density for Butler-Volmer reactions between HC and NaPF6 in
+ EC:PC.
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ c_e : :class:`pybamm.Symbol`
+ Electrolyte concentration [mol.m-3]
+ c_s_surf : :class:`pybamm.Symbol`
+ Particle concentration [mol.m-3]
+ c_s_max : :class:`pybamm.Symbol`
+ Maximum particle concentration [mol.m-3]
+ T : :class:`pybamm.Symbol`
+ Temperature [K]
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Exchange-current density [A.m-2]
+ """
+ name, (x, y) = k_n_data
+ k_n = pybamm.Interpolant(x, y, c_s_surf, name)
+ c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
+
+ return (
+ pybamm.constants.F
+ * k_n
+ * (c_e / c_e0) ** 0.5
+ * c_s_surf**0.5
+ * (c_s_max - c_s_surf) ** 0.5
+ / 2
+ )
+
+
+def NVPF_ocp_Chayambuka2022(sto):
+ """
+ NVPF open-circuit potential as a function of stochiometry, data taken
+ from [1].
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ sto: :class:`pybamm.Symbol`
+ Electrode stochiometry
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Open-circuit potential
+ """
+
+ name, (x, y) = U_p_data
+ return pybamm.Interpolant(x, y, sto, name)
+
+
+def NVPF_diffusivity_Chayambuka2022(sto, T):
+ """
+ NVPF diffusivity as a function of stochiometry, the data is taken from [1].
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ sto: :class:`pybamm.Symbol`
+ Electrode stochiometry
+ T: :class:`pybamm.Symbol`
+ Dimensional temperature
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Solid diffusivity
+ """
+
+ name, (x, y) = D_p_data
+ c_max = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
+ return pybamm.Interpolant(x, y, sto * c_max, name)
+
+
+def NVPF_electrolyte_exchange_current_density_Chayambuka2022(c_e, c_s_surf, c_s_max, T):
+ """
+ Exchange-current density for Butler-Volmer reactions between NVPF and NaPF6 in
+ EC:PC.
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ c_e : :class:`pybamm.Symbol`
+ Electrolyte concentration [mol.m-3]
+ c_s_surf : :class:`pybamm.Symbol`
+ Particle concentration [mol.m-3]
+ c_s_max : :class:`pybamm.Symbol`
+ Maximum particle concentration [mol.m-3]
+ T : :class:`pybamm.Symbol`
+ Temperature [K]
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Exchange-current density [A.m-2]
+ """
+ name, (x, y) = k_p_data
+ k_p = pybamm.Interpolant(x, y, c_s_surf, name)
+ c_e0 = pybamm.Parameter("Initial concentration in electrolyte [mol.m-3]")
+
+ return (
+ pybamm.constants.F
+ * k_p
+ * (c_e / c_e0) ** 0.5
+ * c_s_surf**0.5
+ * (c_s_max - c_s_surf) ** 0.5
+ / 2
+ )
+
+
+def electrolyte_diffusivity_Chayambuka2022(c_e, T):
+ """
+ Diffusivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data
+ comes from [1]
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ c_e: :class:`pybamm.Symbol`
+ Dimensional electrolyte concentration
+ T: :class:`pybamm.Symbol`
+ Dimensional temperature
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Solid diffusivity
+ """
+
+ name, (x, y) = D_e_data
+ D_e = pybamm.Interpolant(x, y, c_e, name)
+
+ # Chayambuka et al. (2022) does not provide temperature dependence
+
+ return D_e
+
+
+def electrolyte_conductivity_Chayambuka2022(c_e, T):
+ """
+ Conductivity of NaPF6 in EC:PC (1:1) as a function of ion concentration. The data
+ comes from [1].
+
+ References
+ ----------
+ .. [1] K. Chayambuka, G. Mulder, D.L. Danilov, P.H.L. Notten, Physics-based
+ modeling of sodium-ion batteries part II. Model and validation, Electrochimica
+ Acta 404 (2022) 139764. https://doi.org/10.1016/j.electacta.2021.139764.
+
+ Parameters
+ ----------
+ c_e: :class:`pybamm.Symbol`
+ Dimensional electrolyte concentration
+ T: :class:`pybamm.Symbol`
+ Dimensional temperature
+
+ Returns
+ -------
+ :class:`pybamm.Symbol`
+ Solid diffusivity
+ """
+
+ name, (x, y) = sigma_e_data
+ sigma_e = pybamm.Interpolant(x, y, c_e, name)
+
+ # Chayambuka et al. (2022) does not provide temperature dependence
+
+ return sigma_e
+
+
+# Call dict via a function to avoid errors when editing in place
+def get_parameter_values():
+ """
+ Parameters for a sodium-ion cell, from the paper :footcite:t:`Chayambuka2022` and references
+ therein. The specific parameter values are taken from the COMSOL implementation presented in
+ [this example](https://www.comsol.com/model/1d-isothermal-sodium-ion-battery-117341).
+
+ """
+
+ return {
+ "chemistry": "sodium_ion",
+ # cell
+ "Negative electrode thickness [m]": 64e-6,
+ "Separator thickness [m]": 25e-6,
+ "Positive electrode thickness [m]": 68e-6,
+ "Electrode height [m]": 2.54e-4,
+ "Electrode width [m]": 1,
+ "Nominal cell capacity [A.h]": 3e-3,
+ "Current function [A]": 3e-3,
+ "Contact resistance [Ohm]": 0,
+ # negative electrode
+ "Negative electrode conductivity [S.m-1]": 256,
+ "Maximum concentration in negative electrode [mol.m-3]": 14540,
+ "Negative particle diffusivity [m2.s-1]": HC_diffusivity_Chayambuka2022,
+ "Negative electrode OCP [V]": HC_ocp_Chayambuka2022,
+ "Negative electrode porosity": 0.51,
+ "Negative electrode active material volume fraction": 0.489, # 1 - 0.51 - 0.001
+ "Negative particle radius [m]": 3.48e-6,
+ "Negative electrode Bruggeman coefficient (electrolyte)": 1.5,
+ "Negative electrode Bruggeman coefficient (electrode)": 0,
+ "Negative electrode charge transfer coefficient": 0.5,
+ "Negative electrode exchange-current density [A.m-2]"
+ "": HC_electrolyte_exchange_current_density_Chayambuka2022,
+ "Negative electrode OCP entropic change [V.K-1]": 0,
+ # positive electrode
+ "Positive electrode conductivity [S.m-1]": 50,
+ "Maximum concentration in positive electrode [mol.m-3]": 15320,
+ "Positive particle diffusivity [m2.s-1]": NVPF_diffusivity_Chayambuka2022,
+ "Positive electrode OCP [V]": NVPF_ocp_Chayambuka2022,
+ "Positive electrode porosity": 0.23,
+ "Positive electrode active material volume fraction": 0.55, # 1 - 0.23 - 0.22
+ "Positive particle radius [m]": 0.59e-6,
+ "Positive electrode Bruggeman coefficient (electrolyte)": 1.5,
+ "Positive electrode Bruggeman coefficient (electrode)": 0,
+ "Positive electrode charge transfer coefficient": 0.5,
+ "Positive electrode exchange-current density [A.m-2]"
+ "": NVPF_electrolyte_exchange_current_density_Chayambuka2022,
+ "Positive electrode OCP entropic change [V.K-1]": 0,
+ # separator
+ "Separator porosity": 0.55,
+ "Separator Bruggeman coefficient (electrolyte)": 1.5,
+ # electrolyte
+ "Initial concentration in electrolyte [mol.m-3]": 1000,
+ "Cation transference number": 0.45,
+ "Thermodynamic factor": 1,
+ "Electrolyte diffusivity [m2.s-1]": electrolyte_diffusivity_Chayambuka2022,
+ "Electrolyte conductivity [S.m-1]": electrolyte_conductivity_Chayambuka2022,
+ # experiment
+ "Reference temperature [K]": 298.15,
+ "Ambient temperature [K]": 298.15,
+ "Number of electrodes connected in parallel to make a cell": 1.0,
+ "Number of cells connected in series to make a battery": 1.0,
+ "Lower voltage cut-off [V]": 2.0,
+ "Upper voltage cut-off [V]": 4.2,
+ "Open-circuit voltage at 0% SOC [V]": 2.0,
+ "Open-circuit voltage at 100% SOC [V]": 4.2,
+ "Initial concentration in negative electrode [mol.m-3]": 13520,
+ "Initial concentration in positive electrode [mol.m-3]": 3320,
+ "Initial temperature [K]": 298.15,
+ # citations
+ "citations": ["Chayambuka2022"],
+ }
diff --git a/src/pybamm/input/parameters/sodium_ion/__init__.py b/src/pybamm/input/parameters/sodium_ion/__init__.py
new file mode 100644
index 0000000000..7591ba5554
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/__init__.py
@@ -0,0 +1 @@
+__all__ = ['Chayambuka2022']
diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_e.csv b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv
new file mode 100755
index 0000000000..3fe74a8cb8
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/D_e.csv
@@ -0,0 +1,14 @@
+Electrolyte concentration [mol.m-3],Electrolyte diffusivity [m2.s-1]
+1.131153,4.14E-11
+124.121064,3.87E-11
+249.37328,3.62E-11
+387.618465,3.33E-11
+538.734332,3.11E-11
+741.699786,2.81E-11
+1013.696117,2.49E-11
+1272.69948,2.25E-11
+1514.27698,2.14E-11
+1811.953531,1.99E-11
+2113.940691,1.86E-11
+2320.97218,1.79E-11
+2471.904616,1.76E-11
diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_n.csv b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv
new file mode 100755
index 0000000000..12d42cce5d
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/D_n.csv
@@ -0,0 +1,40 @@
+Negative particle concentration [mol.m-3],Negative electrode difusivity [m2.s-1]
+73.891626,2.57E-16
+246.305419,3.03E-16
+418.719212,3.54E-16
+714.285714,3.99E-16
+1108.374384,4.50E-16
+1477.832512,4.83E-16
+1847.29064,5.31E-16
+2241.37931,6.21E-16
+2733.990148,7.60E-16
+3177.339901,9.20E-16
+3620.689655,1.11E-15
+4113.300493,1.36E-15
+4630.541872,1.57E-15
+5197.044335,1.79E-15
+5714.285714,1.93E-15
+6280.788177,2.02E-15
+6896.551724,1.97E-15
+7413.793103,1.84E-15
+7832.512315,1.67E-15
+8325.123153,1.45E-15
+8669.950739,1.27E-15
+9039.408867,1.06E-15
+9384.236453,8.77E-16
+9729.064039,7.08E-16
+10073.89163,5.44E-16
+10394.08867,4.39E-16
+10714.28571,3.58E-16
+10960.59113,3.14E-16
+11206.89655,2.96E-16
+11502.46305,3.14E-16
+11847.29064,3.80E-16
+12142.85714,4.72E-16
+12487.68473,6.13E-16
+12832.51232,7.78E-16
+13226.60099,1.01E-15
+13719.21182,1.21E-15
+14014.77833,1.28E-15
+14261.08374,1.28E-15
+14433.49754,1.24E-15
diff --git a/src/pybamm/input/parameters/sodium_ion/data/D_p.csv b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv
new file mode 100755
index 0000000000..51665bcea9
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/D_p.csv
@@ -0,0 +1,35 @@
+Positive particle concentration [mol.m-3],Positive electrode difusivity [m2.s-1]
+131.578947,2.51E-15
+592.105263,1.71E-15
+1052.631579,1.16E-15
+1535.087719,7.41E-16
+2017.54386,4.61E-16
+2521.929825,2.62E-16
+2982.45614,1.56E-16
+3355.263158,9.92E-17
+3618.421053,7.92E-17
+3925.438596,6.61E-17
+4254.385965,5.65E-17
+4495.614035,5.53E-17
+4890.350877,6.07E-17
+5263.157895,6.65E-17
+5679.824561,7.64E-17
+6096.491228,8.38E-17
+6578.947368,8.79E-17
+7039.473684,8.81E-17
+7587.719298,8.44E-17
+8201.754386,7.52E-17
+8837.719298,6.46E-17
+9495.614035,5.05E-17
+10109.64912,4.13E-17
+10657.89474,3.45E-17
+11184.21053,3.16E-17
+11776.31579,3.17E-17
+12346.49123,3.40E-17
+12828.94737,3.57E-17
+13333.33333,3.66E-17
+13750,3.58E-17
+14254.38596,3.35E-17
+14649.12281,2.74E-17
+15000,2.29E-17
+15197.36842,1.95E-17
diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_n.csv b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv
new file mode 100755
index 0000000000..ddb213b3db
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/U_n.csv
@@ -0,0 +1,21 @@
+Negative particle stoichiometry,Negative electrode open-circuit potential [V]
+0.001436794,1.318963892
+0.001643334,1.21982507
+0.00811789,1.112038542
+0.01027308,1.077546494
+0.035479844,0.978299913
+0.060758448,0.844570264
+0.090185795,0.719443422
+0.127982471,0.577039126
+0.169900951,0.456168787
+0.232688871,0.317967115
+0.320485995,0.1753473
+0.403954777,0.110332349
+0.483167055,0.088439192
+0.574861484,0.075112923
+0.687380455,0.066007238
+0.799899424,0.056901553
+0.891593855,0.043575284
+0.960353451,0.038968561
+0.987446008,0.034541438
+0.995806356,0.021574368
diff --git a/src/pybamm/input/parameters/sodium_ion/data/U_p.csv b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv
new file mode 100755
index 0000000000..a2698567e6
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/U_p.csv
@@ -0,0 +1,28 @@
+Positive particle stoichiometry,Positive electrode open-circuit potential [V]
+0.21,4.288102031
+0.21004478,4.210892773
+0.219269532,4.175283892
+0.261497402,4.172472367
+0.332354842,4.172738781
+0.409317336,4.158176665
+0.474174208,4.152479924
+0.526985168,4.143767596
+0.560316674,4.11121965
+0.575706188,4.048901275
+0.582273973,3.941995276
+0.585975815,3.805375531
+0.590901654,3.725196032
+0.598588947,3.695521965
+0.655863013,3.698707605
+0.696598205,3.69292017
+0.741871138,3.684179499
+0.802205196,3.678465753
+0.864031932,3.675727917
+0.930500897,3.649245158
+0.959279735,3.62262069
+0.979341333,3.530616911
+0.980053837,3.450437411
+0.983058101,3.391026925
+0.992282853,3.355418044
+0.996268304,3.162363722
+0.999940293,3.031684459
diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_n.csv b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv
new file mode 100755
index 0000000000..152ebfac4e
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/k_n.csv
@@ -0,0 +1,32 @@
+Negative particle concentration [mol.m-3],Negative electrode exchange-current density rate [m.s-1]
+121.359223,3.33E-11
+412.621359,2.15E-11
+631.067961,1.39E-11
+800.970874,9.40E-12
+970.873786,6.88E-12
+1140.776699,5.71E-12
+1262.135922,5.26E-12
+1529.126214,7.03E-12
+1796.116505,1.04E-11
+2063.106796,1.58E-11
+2500,2.65E-11
+3058.252427,4.18E-11
+3713.592233,5.83E-11
+4660.194175,7.48E-11
+5412.621359,7.96E-11
+6067.961165,7.48E-11
+6868.932039,6.47E-11
+7718.446602,4.94E-11
+8422.330097,3.47E-11
+9004.854369,2.39E-11
+9538.834951,1.61E-11
+10024.27184,1.21E-11
+10388.34951,1.06E-11
+10752.42718,1.13E-11
+11213.59223,1.51E-11
+11723.30097,2.25E-11
+12378.64078,3.54E-11
+12912.62136,4.94E-11
+13470.87379,6.34E-11
+14004.85437,7.48E-11
+14417.47573,7.96E-11
diff --git a/src/pybamm/input/parameters/sodium_ion/data/k_p.csv b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv
new file mode 100755
index 0000000000..7c228e907c
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/k_p.csv
@@ -0,0 +1,24 @@
+Positive particle concentration [mol.m-3],Positive electrode exchange-current density rate [m.s-1]
+21.929825,2.27E-10
+372.807018,1.74E-10
+833.333333,1.24E-10
+1293.859649,8.51E-11
+1907.894737,4.83E-11
+2456.140351,2.96E-11
+2960.526316,1.95E-11
+3442.982456,1.45E-11
+3969.298246,1.16E-11
+4802.631579,1.08E-11
+5548.245614,1.17E-11
+6381.578947,1.18E-11
+7192.982456,1.10E-11
+8092.105263,8.79E-12
+8969.298246,6.65E-12
+9649.122807,5.03E-12
+10350.87719,3.87E-12
+11074.5614,3.34E-12
+12171.05263,3.37E-12
+13223.68421,3.60E-12
+14188.59649,3.30E-12
+14934.21053,2.35E-12
+15328.94737,1.91E-12
diff --git a/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv
new file mode 100644
index 0000000000..e8a1104901
--- /dev/null
+++ b/src/pybamm/input/parameters/sodium_ion/data/sigma_e.csv
@@ -0,0 +1,6 @@
+Electrolyte concentration [mol.m-3],Electrolyte conductivity [S.m-1]
+150,0.404
+500,0.72
+1000,0.883
+1500,0.861
+2000,0.76
diff --git a/src/pybamm/logger.py b/src/pybamm/logger.py
index 7dcacb5237..460e264416 100644
--- a/src/pybamm/logger.py
+++ b/src/pybamm/logger.py
@@ -24,6 +24,17 @@ def func(self, message, *args, **kws):
def set_logging_level(level):
+ """
+ Set the logging level for PyBaMM
+
+ Parameters
+ ----------
+
+ level: str
+ The logging level to set. Should be one of 'DEBUG', 'INFO', 'WARNING',
+ 'ERROR', 'CRITICAL'
+
+ """
logger.setLevel(level)
diff --git a/src/pybamm/meshes/one_dimensional_submeshes.py b/src/pybamm/meshes/one_dimensional_submeshes.py
index 8f27049411..027c6d0421 100644
--- a/src/pybamm/meshes/one_dimensional_submeshes.py
+++ b/src/pybamm/meshes/one_dimensional_submeshes.py
@@ -297,8 +297,8 @@ def __init__(self, lims, npts, edges=None):
if (npts + 1) != len(edges):
raise pybamm.GeometryError(
- f"""User-suppled edges has should have length (npts + 1) but has length
- {len(edges)}.Number of points (npts) for domain {spatial_var.domain} is {npts}.""".replace(
+ "User-suppled edges has should have length (npts + 1) but has length "
+ f"{len(edges)}.Number of points (npts) for domain {spatial_var.domain} is {npts}.".replace(
"\n ", " "
)
)
diff --git a/src/pybamm/meshes/scikit_fem_submeshes.py b/src/pybamm/meshes/scikit_fem_submeshes.py
index ba624c7f48..2d769f3ca2 100644
--- a/src/pybamm/meshes/scikit_fem_submeshes.py
+++ b/src/pybamm/meshes/scikit_fem_submeshes.py
@@ -92,8 +92,8 @@ def read_lims(self, lims):
# check coordinate system agrees
if spatial_vars[0].coord_sys != spatial_vars[1].coord_sys:
raise pybamm.DomainError(
- f"""spatial variables should have the same coordinate system,
- but have coordinate systems {spatial_vars[0].coord_sys} and {spatial_vars[1].coord_sys}"""
+ "spatial variables should have the same coordinate system, "
+ f"but have coordinate systems {spatial_vars[0].coord_sys} and {spatial_vars[1].coord_sys}"
)
return spatial_vars, tabs
@@ -360,9 +360,9 @@ def __init__(self, lims, npts, y_edges=None, z_edges=None):
# check that npts equals number of user-supplied edges
if npts[var.name] != len(edges[var.name]):
raise pybamm.GeometryError(
- f"""User-suppled edges has should have length npts but has length {len(edges[var.name])}.
- Number of points (npts) for variable {var.name} in
- domain {var.domain} is {npts[var.name]}."""
+ f"User-supplied edges has should have length npts but has length {len(edges[var.name])}. "
+ f"Number of points (npts) for variable {var.name} in "
+ f"domain {var.domain} is {npts[var.name]}."
)
# check end points of edges agree with spatial_lims
diff --git a/src/pybamm/models/base_model.py b/src/pybamm/models/base_model.py
index 0d4638e178..6cb3af2fd7 100644
--- a/src/pybamm/models/base_model.py
+++ b/src/pybamm/models/base_model.py
@@ -19,29 +19,27 @@ class BaseModel:
Attributes
----------
name: str
- A string giving the name of the model.
+ A string representing the name of the model.
submodels: dict
A dictionary of submodels that the model is composed of.
- boundary_conditions: dict
- A dictionary that maps expressions (variables) to expressions that represent
- the boundary conditions.
- variables: dict
- A dictionary that maps strings to expressions that represent
- the useful variables.
- use_jacobian : bool
+ use_jacobian: bool
Whether to use the Jacobian when solving the model (default is True).
- convert_to_format : str
- Whether to convert the expression trees representing the rhs and
- algebraic equations, Jacobain (if using) and events into a different format:
+ convert_to_format: str
+ Specifies the format to convert the expression trees representing the RHS,
+ algebraic equations, Jacobian, and events.
+ Options are:
- - None: keep PyBaMM expression tree structure.
- - "python": convert into pure python code that will calculate the result of \
- calling `evaluate(t, y)` on the given expression treeself.
- - "casadi": convert into CasADi expression tree, which then uses CasADi's \
- algorithm to calculate the Jacobian.
- - "jax": convert into JAX expression tree
+ - None: retain PyBaMM expression tree structure.
+ - "python": convert to Python code for evaluating `evaluate(t, y)` on expressions.
+ - "casadi": convert to CasADi expression tree for Jacobian calculation.
+ - "jax": convert to JAX expression tree.
Default is "casadi".
+ is_discretised: bool
+ Indicates whether the model has been discretised (default is False).
+ y_slices: None or list
+ Slices of the concatenated state vector after discretisation, used to track
+ different submodels in the full concatenated solution vector.
"""
def __init__(self, name="Unnamed model"):
@@ -58,6 +56,7 @@ def __init__(self, name="Unnamed model"):
self._boundary_conditions = {}
self._variables_by_submodel = {}
self._variables = pybamm.FuzzyDict({})
+ self._coupled_variables = {}
self._summary_variables = []
self._events = []
self._concatenated_rhs = None
@@ -144,6 +143,8 @@ def name(self, value):
@property
def rhs(self):
+ """Returns a dictionary mapping expressions (variables) to expressions that represent
+ the right-hand side (RHS) of the model's differential equations."""
return self._rhs
@rhs.setter
@@ -152,6 +153,8 @@ def rhs(self, rhs):
@property
def algebraic(self):
+ """Returns a dictionary mapping expressions (variables) to expressions that represent
+ the algebraic equations of the model."""
return self._algebraic
@algebraic.setter
@@ -160,6 +163,8 @@ def algebraic(self, algebraic):
@property
def initial_conditions(self):
+ """Returns a dictionary mapping expressions (variables) to expressions that represent
+ the initial conditions for the state variables."""
return self._initial_conditions
@initial_conditions.setter
@@ -170,14 +175,42 @@ def initial_conditions(self, initial_conditions):
@property
def boundary_conditions(self):
+ """Returns a dictionary mapping expressions (variables) to expressions representing
+ the boundary conditions of the model."""
return self._boundary_conditions
@boundary_conditions.setter
def boundary_conditions(self, boundary_conditions):
self._boundary_conditions = BoundaryConditionsDict(boundary_conditions)
+ @property
+ def coupled_variables(self):
+ """Returns a dictionary mapping strings to expressions representing variables needed by the model but whose equations were set by other models."""
+ return self._coupled_variables
+
+ @coupled_variables.setter
+ def coupled_variables(self, coupled_variables):
+ for name, var in coupled_variables.items():
+ if (
+ isinstance(var, pybamm.CoupledVariable)
+ and var.name != name
+ # Exception if the variable is also there under its own name
+ and not (
+ var.name in coupled_variables and coupled_variables[var.name] == var
+ )
+ ):
+ raise ValueError(
+ f"Coupled variable with name '{var.name}' is in coupled variables dictionary with "
+ f"name '{name}'. Names must match."
+ )
+ self._coupled_variables = coupled_variables
+
+ def list_coupled_variables(self):
+ return list(self._coupled_variables.keys())
+
@property
def variables(self):
+ """Returns a dictionary mapping strings to expressions representing the model's useful variables."""
return self._variables
@variables.setter
@@ -200,9 +233,7 @@ def variable_names(self):
@property
def variables_and_events(self):
- """
- Returns variables and events in a single dictionary
- """
+ """Returns a dictionary containing both models variables and events."""
try:
return self._variables_and_events
except AttributeError:
@@ -214,6 +245,8 @@ def variables_and_events(self):
@property
def events(self):
+ """Returns a dictionary mapping expressions (variables) to expressions that represent
+ the initial conditions for the state variables."""
return self._events
@events.setter
@@ -222,6 +255,7 @@ def events(self, events):
@property
def concatenated_rhs(self):
+ """Returns the concatenated right-hand side (RHS) expressions for the model after discretisation."""
return self._concatenated_rhs
@concatenated_rhs.setter
@@ -230,6 +264,7 @@ def concatenated_rhs(self, concatenated_rhs):
@property
def concatenated_algebraic(self):
+ """Returns the concatenated algebraic equations for the model after discretisation."""
return self._concatenated_algebraic
@concatenated_algebraic.setter
@@ -238,6 +273,8 @@ def concatenated_algebraic(self, concatenated_algebraic):
@property
def concatenated_initial_conditions(self):
+ """Returns the initial conditions for all variables after discretization, providing the
+ initial values for the state variables."""
return self._concatenated_initial_conditions
@concatenated_initial_conditions.setter
@@ -246,6 +283,7 @@ def concatenated_initial_conditions(self, concatenated_initial_conditions):
@property
def mass_matrix(self):
+ """Returns the mass matrix for the system of differential equations after discretisation."""
return self._mass_matrix
@mass_matrix.setter
@@ -254,6 +292,7 @@ def mass_matrix(self, mass_matrix):
@property
def mass_matrix_inv(self):
+ """Returns the inverse of the mass matrix for the differential equations after discretisation."""
return self._mass_matrix_inv
@mass_matrix_inv.setter
@@ -262,6 +301,7 @@ def mass_matrix_inv(self, mass_matrix_inv):
@property
def jacobian(self):
+ """Returns the Jacobian matrix for the model, computed automatically if `use_jacobian` is True."""
return self._jacobian
@jacobian.setter
@@ -270,6 +310,8 @@ def jacobian(self, jacobian):
@property
def jacobian_rhs(self):
+ """Returns the Jacobian matrix for the right-hand side (RHS) part of the model, computed
+ if `use_jacobian` is True."""
return self._jacobian_rhs
@jacobian_rhs.setter
@@ -278,6 +320,8 @@ def jacobian_rhs(self, jacobian_rhs):
@property
def jacobian_algebraic(self):
+ """Returns the Jacobian matrix for the algebraic part of the model, computed automatically
+ during solver setup if `use_jacobian` is True."""
return self._jacobian_algebraic
@jacobian_algebraic.setter
@@ -286,6 +330,7 @@ def jacobian_algebraic(self, jacobian_algebraic):
@property
def param(self):
+ """Returns a dictionary to store parameter values for the model."""
return self._param
@param.setter
@@ -294,6 +339,7 @@ def param(self, values):
@property
def options(self):
+ """Returns the model options dictionary that allows customization of the model's behavior."""
return self._options
@options.setter
@@ -326,27 +372,32 @@ def length_scales(self, values):
@property
def geometry(self):
+ """Returns the geometry of the model."""
return self._geometry
@property
def default_var_pts(self):
+ """Returns a dictionary of the default variable points for the model, which is empty by default."""
return {}
@property
def default_geometry(self):
+ """Returns a dictionary of the default geometry for the model, which is empty by default."""
return {}
@property
def default_submesh_types(self):
+ """Returns a dictionary of the default submesh types for the model, which is empty by default."""
return {}
@property
def default_spatial_methods(self):
+ """Returns a dictionary of the default spatial methods for the model, which is empty by default."""
return {}
@property
def default_solver(self):
- """Return default solver based on whether model is ODE/DAE or algebraic"""
+ """Returns the default solver for the model, based on whether it is an ODE/DAE or algebraic model."""
if len(self.rhs) == 0 and len(self.algebraic) != 0:
return pybamm.CasadiAlgebraicSolver()
else:
@@ -354,15 +405,17 @@ def default_solver(self):
@property
def default_quick_plot_variables(self):
+ """Returns the default variables for quick plotting (None by default)."""
return None
@property
def default_parameter_values(self):
+ """Returns the default parameter values for the model (an empty set of parameters by default)."""
return pybamm.ParameterValues({})
@property
def parameters(self):
- """Returns all the parameters in the model"""
+ """Returns a list of all parameter symbols used in the model."""
self._parameters = self._find_symbols(
(pybamm.Parameter, pybamm.InputParameter, pybamm.FunctionParameter)
)
@@ -370,7 +423,7 @@ def parameters(self):
@property
def input_parameters(self):
- """Returns all the input parameters in the model"""
+ """Returns a list of all input parameter symbols used in the model."""
if self._input_parameters is None:
self._input_parameters = self._find_symbols(pybamm.InputParameter)
return self._input_parameters
@@ -757,7 +810,7 @@ def build_model_equations(self):
f"Setting initial conditions for {submodel_name} submodel ({self.name})"
)
submodel.set_initial_conditions(self.variables)
- submodel.set_events(self.variables)
+ submodel.add_events_from(self.variables)
pybamm.logger.verbose(f"Updating {submodel_name} submodel ({self.name})")
self.update(submodel)
self.check_no_repeated_keys()
@@ -1057,7 +1110,7 @@ def check_ics_bcs(self):
for var in self.rhs.keys():
if var not in self.initial_conditions.keys():
raise pybamm.ModelError(
- f"""no initial condition given for variable '{var}'"""
+ f"no initial condition given for variable '{var}'"
)
def check_variables(self):
@@ -1079,11 +1132,9 @@ def check_variables(self):
for var in all_vars:
if var not in vars_in_keys:
raise pybamm.ModelError(
- f"""
- No key set for variable '{var}'. Make sure it is included in either
- model.rhs or model.algebraic, in an unmodified form
- (e.g. not Broadcasted)
- """
+ f"No key set for variable '{var}'. Make sure it is included in either "
+ "model.rhs or model.algebraic, in an unmodified form "
+ "(e.g. not Broadcasted)"
)
def check_no_repeated_keys(self):
@@ -1497,9 +1548,7 @@ def check_and_convert_bcs(self, boundary_conditions):
# Check types
if bc[1] not in ["Dirichlet", "Neumann"]:
raise pybamm.ModelError(
- f"""
- boundary condition types must be Dirichlet or Neumann, not '{bc[1]}'
- """
+ f"boundary condition types must be Dirichlet or Neumann, not '{bc[1]}'"
)
return boundary_conditions
diff --git a/src/pybamm/models/full_battery_models/__init__.py b/src/pybamm/models/full_battery_models/__init__.py
index 135f678289..0260f4dd07 100644
--- a/src/pybamm/models/full_battery_models/__init__.py
+++ b/src/pybamm/models/full_battery_models/__init__.py
@@ -1,2 +1,2 @@
__all__ = ['base_battery_model', 'equivalent_circuit', 'lead_acid',
- 'lithium_ion']
+ 'lithium_ion', 'sodium_ion']
diff --git a/src/pybamm/models/full_battery_models/base_battery_model.py b/src/pybamm/models/full_battery_models/base_battery_model.py
index ccda594b14..153c447ed3 100644
--- a/src/pybamm/models/full_battery_models/base_battery_model.py
+++ b/src/pybamm/models/full_battery_models/base_battery_model.py
@@ -210,6 +210,9 @@ class BatteryModelOptions(pybamm.FuzzyDict):
solve an algebraic equation for it. Default is "false", unless "SEI film
resistance" is distributed in which case it is automatically set to
"true".
+ * "voltage as a state" : str
+ Whether to make a state for the voltage and solve an algebraic equation
+ for it. Default is "false".
* "working electrode" : str
Can be "both" (default) for a standard battery or "positive" for a
half-cell where the negative electrode is replaced with a lithium metal
@@ -321,6 +324,7 @@ def __init__(self, extra_options):
"heterogeneous catalyst",
"cation-exchange membrane",
],
+ "voltage as a state": ["false", "true"],
"working electrode": ["both", "positive"],
"x-average side reactions": ["false", "true"],
}
@@ -618,14 +622,11 @@ def __init__(self, extra_options):
options["surface form"] != "false"
and options["particle size"] == "single"
and options["particle"] == "Fickian diffusion"
- and options["particle mechanics"] == "none"
- and options["loss of active material"] == "none"
):
raise pybamm.OptionError(
"If there are multiple particle phases: 'surface form' cannot be "
"'false', 'particle size' must be 'single', 'particle' must be "
- "'Fickian diffusion'. Also the following must "
- "be 'none': 'particle mechanics', 'loss of active material'"
+ "'Fickian diffusion'."
)
if options["surface temperature"] == "lumped":
@@ -752,7 +753,7 @@ def print_options(self):
Print the possible options with the ones currently selected
"""
for key, value in self.items():
- print(f"{key!r}: {value!r} (possible: {self.possible_options[key]!r})")
+ print(rf"{key!r}: {value!r} (possible: {self.possible_options[key]!r})")
def print_detailed_options(self):
"""
@@ -988,7 +989,6 @@ def options(self, extra_options):
raise pybamm.OptionError(
f"must use surface formulation to solve {self!s} with hydrolysis"
)
-
self._options = options
def set_standard_output_variables(self):
@@ -1033,7 +1033,7 @@ def build_model_equations(self):
f"Setting initial conditions for {submodel_name} submodel ({self.name})"
)
submodel.set_initial_conditions(self.variables)
- submodel.set_events(self.variables)
+ submodel.add_events_from(self.variables)
pybamm.logger.verbose(f"Updating {submodel_name} submodel ({self.name})")
self.update(submodel)
self.check_no_repeated_keys()
diff --git a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py
index 8caac98066..a67501fc72 100644
--- a/src/pybamm/models/full_battery_models/lead_acid/basic_full.py
+++ b/src/pybamm/models/full_battery_models/lead_acid/basic_full.py
@@ -26,7 +26,6 @@ def __init__(self, name="Basic full model"):
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
- param = self.param
######################
# Variables
@@ -37,17 +36,17 @@ def __init__(self, name="Basic full model"):
c_e_n = pybamm.Variable(
"Negative electrolyte concentration [mol.m-3]",
domain="negative electrode",
- scale=param.c_e_init,
+ scale=self.param.c_e_init,
)
c_e_s = pybamm.Variable(
"Separator electrolyte concentration [mol.m-3]",
domain="separator",
- scale=param.c_e_init,
+ scale=self.param.c_e_init,
)
c_e_p = pybamm.Variable(
"Positive electrolyte concentration [mol.m-3]",
domain="positive electrode",
- scale=param.c_e_init,
+ scale=self.param.c_e_init,
)
# Concatenations combine several variables into a single variable, to simplify
# implementing equations that hold over several domains
@@ -57,17 +56,17 @@ def __init__(self, name="Basic full model"):
phi_e_n = pybamm.Variable(
"Negative electrolyte potential [V]",
domain="negative electrode",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e_s = pybamm.Variable(
"Separator electrolyte potential [V]",
domain="separator",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e_p = pybamm.Variable(
"Positive electrolyte potential [V]",
domain="positive electrode",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p)
@@ -78,7 +77,7 @@ def __init__(self, name="Basic full model"):
phi_s_p = pybamm.Variable(
"Positive electrode potential [V]",
domain="positive electrode",
- reference=param.ocv_init,
+ reference=self.param.ocv_init,
)
# Porosity
@@ -92,29 +91,29 @@ def __init__(self, name="Basic full model"):
eps = pybamm.concatenation(eps_n, eps_s, eps_p)
# Constant temperature
- T = param.T_init
+ T = self.param.T_init
######################
# Other set-up
######################
# Current density
- i_cell = param.current_density_with_time
+ i_cell = self.param.current_density_with_time
# transport_efficiency
tor = pybamm.concatenation(
- eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e
+ eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e
)
# Interfacial reactions
- F_RT = param.F / (param.R * T)
- Feta_RT_n = F_RT * (phi_s_n - phi_e_n - param.n.prim.U(c_e_n, T))
- j0_n = param.n.prim.j0(c_e_n, T)
- j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n)
+ F_RT = self.param.F / (self.param.R * T)
+ Feta_RT_n = F_RT * (phi_s_n - phi_e_n - self.param.n.prim.U(c_e_n, T))
+ j0_n = self.param.n.prim.j0(c_e_n, T)
+ j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n)
j_s = pybamm.PrimaryBroadcast(0, "separator")
- Feta_RT_p = F_RT * (phi_s_p - phi_e_p - param.p.prim.U(c_e_p, T))
- j0_p = param.p.prim.j0(c_e_p, T)
- j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * (Feta_RT_p))
+ Feta_RT_p = F_RT * (phi_s_p - phi_e_p - self.param.p.prim.U(c_e_p, T))
+ j0_p = self.param.p.prim.j0(c_e_p, T)
+ j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * (Feta_RT_p))
a_n = pybamm.Parameter("Negative electrode surface area to volume ratio [m-1]")
a_p = pybamm.Parameter("Positive electrode surface area to volume ratio [m-1]")
@@ -125,7 +124,7 @@ def __init__(self, name="Basic full model"):
######################
# State of Charge
######################
- I = param.current_with_time
+ I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
@@ -135,28 +134,32 @@ def __init__(self, name="Basic full model"):
######################
# Current in the electrolyte
######################
- i_e = (param.kappa_e(c_e, T) * tor) * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ i_e = (self.param.kappa_e(c_e, T) * tor) * (
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * param.L_x**2
+ self.algebraic[phi_e] = (pybamm.div(i_e) - a_j) * self.param.L_x**2
self.boundary_conditions[phi_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[phi_e] = -param.n.prim.U_init
+ self.initial_conditions[phi_e] = -self.param.n.prim.U_init
######################
# Current in the solid
######################
- i_s_n = -param.n.sigma(T) * (1 - eps_n) ** param.n.b_s * pybamm.grad(phi_s_n)
- sigma_eff_p = param.p.sigma(T) * (1 - eps_p) ** param.p.b_s
+ i_s_n = (
+ -self.param.n.sigma(T)
+ * (1 - eps_n) ** self.param.n.b_s
+ * pybamm.grad(phi_s_n)
+ )
+ sigma_eff_p = self.param.p.sigma(T) * (1 - eps_p) ** self.param.p.b_s
i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
# The `algebraic` dictionary contains differential equations, with the key being
# the main scalar variable of interest in the equation
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * param.L_x**2
- self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * param.L_x**2
+ self.algebraic[phi_s_n] = (pybamm.div(i_s_n) + a_j_n) * self.param.L_x**2
+ self.algebraic[phi_s_p] = (pybamm.div(i_s_p) + a_j_p) * self.param.L_x**2
self.boundary_conditions[phi_s_n] = {
"left": (pybamm.Scalar(0), "Dirichlet"),
"right": (pybamm.Scalar(0), "Neumann"),
@@ -169,19 +172,19 @@ def __init__(self, name="Basic full model"):
# initial guess for a root-finding algorithm which calculates consistent initial
# conditions
self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
- self.initial_conditions[phi_s_p] = param.ocv_init
+ self.initial_conditions[phi_s_p] = self.param.ocv_init
######################
# Porosity
######################
DeltaVsurf = pybamm.concatenation(
- pybamm.PrimaryBroadcast(param.n.DeltaVsurf, "negative electrode"),
+ pybamm.PrimaryBroadcast(self.param.n.DeltaVsurf, "negative electrode"),
pybamm.PrimaryBroadcast(0, "separator"),
- pybamm.PrimaryBroadcast(param.p.DeltaVsurf, "positive electrode"),
+ pybamm.PrimaryBroadcast(self.param.p.DeltaVsurf, "positive electrode"),
)
- deps_dt = DeltaVsurf * a_j / param.F
+ deps_dt = DeltaVsurf * a_j / self.param.F
self.rhs[eps] = deps_dt
- self.initial_conditions[eps] = param.epsilon_init
+ self.initial_conditions[eps] = self.param.epsilon_init
self.events.extend(
[
pybamm.Event(
@@ -203,22 +206,22 @@ def __init__(self, name="Basic full model"):
# Electrolyte concentration
######################
N_e = (
- -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
- + param.t_plus(c_e, T) * i_e / param.F
+ -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
+ + self.param.t_plus(c_e, T) * i_e / self.param.F
)
s = pybamm.concatenation(
- pybamm.PrimaryBroadcast(param.n.prim.s_plus_S, "negative electrode"),
+ pybamm.PrimaryBroadcast(self.param.n.prim.s_plus_S, "negative electrode"),
pybamm.PrimaryBroadcast(0, "separator"),
- pybamm.PrimaryBroadcast(param.p.prim.s_plus_S, "positive electrode"),
+ pybamm.PrimaryBroadcast(self.param.p.prim.s_plus_S, "positive electrode"),
)
self.rhs[c_e] = (1 / eps) * (
- -pybamm.div(N_e) + s * a_j / param.F - c_e * deps_dt
+ -pybamm.div(N_e) + s * a_j / self.param.F - c_e * deps_dt
)
self.boundary_conditions[c_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[c_e] = param.c_e_init
+ self.initial_conditions[c_e] = self.param.c_e_init
self.events.append(
pybamm.Event(
"Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002
@@ -242,7 +245,11 @@ def __init__(self, name="Basic full model"):
}
self.events.extend(
[
- pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut),
- pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage),
+ pybamm.Event(
+ "Minimum voltage [V]", voltage - self.param.voltage_low_cut
+ ),
+ pybamm.Event(
+ "Maximum voltage [V]", self.param.voltage_high_cut - voltage
+ ),
]
)
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/__init__.py b/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
index b02868dbe9..556e8de31c 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/__init__.py
@@ -24,8 +24,9 @@
from .Yang2017 import Yang2017
from .mpm import MPM
from .msmr import MSMR
+from .basic_splitOCVR import SplitOCVR
__all__ = ['Yang2017', 'base_lithium_ion_model', 'basic_dfn',
'basic_dfn_composite', 'basic_dfn_half_cell', 'basic_spm', 'dfn',
'electrode_soh', 'electrode_soh_half_cell', 'mpm', 'msmr',
- 'newman_tobias', 'spm', 'spme']
+ 'newman_tobias', 'spm', 'spme', 'basic_splitOCVR']
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py
index dfe2512f6e..9b801e1130 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/base_lithium_ion_model.py
@@ -105,7 +105,6 @@ def set_standard_output_variables(self):
def set_degradation_variables(self):
"""Sets variables that quantify degradation (LAM, LLI, etc)"""
- param = self.param
domains = [d for d in self.options.whole_cell_domains if d != "separator"]
for domain in domains:
@@ -135,8 +134,8 @@ def set_degradation_variables(self):
# LLI is usually defined based only on the percentage lithium lost from
# particles
- LLI = (1 - n_Li_particles / param.n_Li_particles_init) * 100
- LLI_tot = (1 - n_Li / param.n_Li_init) * 100
+ LLI = (1 - n_Li_particles / self.param.n_Li_particles_init) * 100
+ LLI_tot = (1 - n_Li / self.param.n_Li_init) * 100
self.variables.update(
{
@@ -146,15 +145,16 @@ def set_degradation_variables(self):
# Total lithium
"Total lithium [mol]": n_Li,
"Total lithium in particles [mol]": n_Li_particles,
- "Total lithium capacity [A.h]": n_Li * param.F / 3600,
+ "Total lithium capacity [A.h]": n_Li * self.param.F / 3600,
"Total lithium capacity in particles [A.h]": n_Li_particles
- * param.F
+ * self.param.F
/ 3600,
# Lithium lost
- "Total lithium lost [mol]": param.n_Li_init - n_Li,
- "Total lithium lost from particles [mol]": param.n_Li_particles_init
+ "Total lithium lost [mol]": self.param.n_Li_init - n_Li,
+ "Total lithium lost from particles [mol]": self.param.n_Li_particles_init
- n_Li_particles,
- "Total lithium lost from electrolyte [mol]": param.n_Li_e_init - n_Li_e,
+ "Total lithium lost from electrolyte [mol]": self.param.n_Li_e_init
+ - n_Li_e,
}
)
@@ -177,7 +177,7 @@ def set_degradation_variables(self):
{
"Total lithium lost to side reactions [mol]": n_Li_lost_reactions,
"Total capacity lost to side reactions [A.h]": n_Li_lost_reactions
- * param.F
+ * self.param.F
/ 3600,
}
)
@@ -365,29 +365,31 @@ def set_crack_submodel(self):
for domain in self.options.whole_cell_domains:
if domain != "separator":
domain = domain.split()[0].lower()
- crack = getattr(self.options, domain)["particle mechanics"]
- if crack == "none":
- self.submodels[f"{domain} particle mechanics"] = (
- pybamm.particle_mechanics.NoMechanics(
- self.param, domain, options=self.options, phase="primary"
+ phases = self.options.phases[domain]
+ for phase in phases:
+ crack = getattr(self.options, domain)["particle mechanics"]
+ if crack == "none":
+ self.submodels[f"{domain} {phase}particle mechanics"] = (
+ pybamm.particle_mechanics.NoMechanics(
+ self.param, domain, options=self.options, phase=phase
+ )
)
- )
- elif crack == "swelling only":
- self.submodels[f"{domain} particle mechanics"] = (
- pybamm.particle_mechanics.SwellingOnly(
- self.param, domain, options=self.options, phase="primary"
+ elif crack == "swelling only":
+ self.submodels[f"{domain} {phase}particle mechanics"] = (
+ pybamm.particle_mechanics.SwellingOnly(
+ self.param, domain, options=self.options, phase=phase
+ )
)
- )
- elif crack == "swelling and cracking":
- self.submodels[f"{domain} particle mechanics"] = (
- pybamm.particle_mechanics.CrackPropagation(
- self.param,
- domain,
- self.x_average,
- options=self.options,
- phase="primary",
+ elif crack == "swelling and cracking":
+ self.submodels[f"{domain} {phase}particle mechanics"] = (
+ pybamm.particle_mechanics.CrackPropagation(
+ self.param,
+ domain,
+ self.x_average,
+ options=self.options,
+ phase=phase,
+ )
)
- )
def set_active_material_submodel(self):
for domain in ["negative", "positive"]:
@@ -401,7 +403,7 @@ def set_active_material_submodel(self):
)
else:
submod = pybamm.active_material.LossActiveMaterial(
- self.param, domain, self.options, self.x_average
+ self.param, domain, self.options, self.x_average, phase
)
self.submodels[f"{domain} {phase} active material"] = submod
@@ -500,9 +502,8 @@ def insert_reference_electrode(self, position=None):
"electrode manually."
)
- param = self.param
if position is None:
- position = param.n.L + param.s.L / 2
+ position = self.param.n.L + self.param.s.L / 2
phi_e_ref = pybamm.EvaluateAt(
self.variables["Electrolyte potential [V]"], position
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py
index 08809b645f..7865b84ff3 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn.py
@@ -27,7 +27,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
- param = self.param
######################
# Variables
@@ -90,14 +89,14 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
)
# Constant temperature
- T = param.T_init
+ T = self.param.T_init
######################
# Other set-up
######################
# Current density
- i_cell = param.current_density_with_time
+ i_cell = self.param.current_density_with_time
# Porosity
# Primary broadcasts are used to broadcast scalar quantities across a domain
@@ -119,29 +118,29 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
# transport_efficiency
tor = pybamm.concatenation(
- eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e
+ eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e
)
- a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ
- a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ
+ a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ
+ a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ
# Interfacial reactions
# Surf takes the surface value of a variable, i.e. its boundary value on the
# right side. This is also accessible via `boundary_value(x, "right")`, with
# "left" providing the boundary value of the left side
c_s_surf_n = pybamm.surf(c_s_n)
- sto_surf_n = c_s_surf_n / param.n.prim.c_max
- j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T)
- eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T)
- Feta_RT_n = param.F * eta_n / (param.R * T)
- j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n)
+ sto_surf_n = c_s_surf_n / self.param.n.prim.c_max
+ j0_n = self.param.n.prim.j0(c_e_n, c_s_surf_n, T)
+ eta_n = phi_s_n - phi_e_n - self.param.n.prim.U(sto_surf_n, T)
+ Feta_RT_n = self.param.F * eta_n / (self.param.R * T)
+ j_n = 2 * j0_n * pybamm.sinh(self.param.n.prim.ne / 2 * Feta_RT_n)
c_s_surf_p = pybamm.surf(c_s_p)
- sto_surf_p = c_s_surf_p / param.p.prim.c_max
- j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T)
- eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T)
- Feta_RT_p = param.F * eta_p / (param.R * T)
+ sto_surf_p = c_s_surf_p / self.param.p.prim.c_max
+ j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T)
+ eta_p = phi_s_p - phi_e_p - self.param.p.prim.U(sto_surf_p, T)
+ Feta_RT_p = self.param.F * eta_p / (self.param.R * T)
j_s = pybamm.PrimaryBroadcast(0, "separator")
- j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p)
+ j_p = 2 * j0_p * pybamm.sinh(self.param.p.prim.ne / 2 * Feta_RT_p)
a_j_n = a_n * j_n
a_j_p = a_p * j_p
@@ -150,7 +149,7 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
######################
# State of Charge
######################
- I = param.current_with_time
+ I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
@@ -163,39 +162,39 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
# The div and grad operators will be converted to the appropriate matrix
# multiplication at the discretisation stage
- N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n)
- N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
+ N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n)
+ N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
self.rhs[c_s_n] = -pybamm.div(N_s_n)
self.rhs[c_s_p] = -pybamm.div(N_s_p)
# Boundary conditions must be provided for equations with spatial derivatives
self.boundary_conditions[c_s_n] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))),
+ -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))),
"Neumann",
),
}
self.boundary_conditions[c_s_p] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))),
+ -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))),
"Neumann",
),
}
- self.initial_conditions[c_s_n] = param.n.prim.c_init
- self.initial_conditions[c_s_p] = param.p.prim.c_init
+ self.initial_conditions[c_s_n] = self.param.n.prim.c_init
+ self.initial_conditions[c_s_p] = self.param.p.prim.c_init
######################
# Current in the solid
######################
- sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s
+ sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s
i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n)
- sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s
+ sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s
i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
# The `algebraic` dictionary contains differential equations, with the key being
# the main scalar variable of interest in the equation
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
- self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
+ self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
+ self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
self.boundary_conditions[phi_s_n] = {
"left": (pybamm.Scalar(0), "Dirichlet"),
"right": (pybamm.Scalar(0), "Neumann"),
@@ -208,34 +207,34 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
# initial guess for a root-finding algorithm which calculates consistent initial
# conditions
self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
- self.initial_conditions[phi_s_p] = param.ocv_init
+ self.initial_conditions[phi_s_p] = self.param.ocv_init
######################
# Current in the electrolyte
######################
- i_e = (param.kappa_e(c_e, T) * tor) * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ i_e = (self.param.kappa_e(c_e, T) * tor) * (
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j)
+ self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j)
self.boundary_conditions[phi_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[phi_e] = -param.n.prim.U_init
+ self.initial_conditions[phi_e] = -self.param.n.prim.U_init
######################
# Electrolyte concentration
######################
- N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
+ N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
self.rhs[c_e] = (1 / eps) * (
- -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F
+ -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F
)
self.boundary_conditions[c_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[c_e] = param.c_e_init
+ self.initial_conditions[c_e] = self.param.c_e_init
######################
# (Some) variables
@@ -270,6 +269,6 @@ def __init__(self, name="Doyle-Fuller-Newman model"):
}
# Events specify points at which a solution should terminate
self.events += [
- pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut),
- pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage),
+ pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut),
+ pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage),
]
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py
index 95f65f4d50..273d1c037c 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_composite.py
@@ -28,7 +28,6 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
- param = self.param
######################
# Variables
@@ -39,17 +38,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
c_e_n = pybamm.Variable(
"Negative electrolyte concentration [mol.m-3]",
domain="negative electrode",
- scale=param.c_e_init_av,
+ scale=self.param.c_e_init_av,
)
c_e_s = pybamm.Variable(
"Separator electrolyte concentration [mol.m-3]",
domain="separator",
- scale=param.c_e_init_av,
+ scale=self.param.c_e_init_av,
)
c_e_p = pybamm.Variable(
"Positive electrolyte concentration [mol.m-3]",
domain="positive electrode",
- scale=param.c_e_init_av,
+ scale=self.param.c_e_init_av,
)
# Concatenations combine several variables into a single variable, to simplify
# implementing equations that hold over several domains
@@ -59,17 +58,17 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
phi_e_n = pybamm.Variable(
"Negative electrolyte potential [V]",
domain="negative electrode",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e_s = pybamm.Variable(
"Separator electrolyte potential [V]",
domain="separator",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e_p = pybamm.Variable(
"Positive electrolyte potential [V]",
domain="positive electrode",
- reference=-param.n.prim.U_init,
+ reference=-self.param.n.prim.U_init,
)
phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p)
@@ -80,7 +79,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
phi_s_p = pybamm.Variable(
"Positive electrode potential [V]",
domain="positive electrode",
- reference=param.ocv_init,
+ reference=self.param.ocv_init,
)
# Particle concentrations are variables on the particle domain, but also vary in
# the x-direction (electrode domain) and so must be provided with auxiliary
@@ -89,30 +88,30 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
"Negative primary particle concentration [mol.m-3]",
domain="negative primary particle",
auxiliary_domains={"secondary": "negative electrode"},
- scale=param.n.prim.c_max,
+ scale=self.param.n.prim.c_max,
)
c_s_n_p2 = pybamm.Variable(
"Negative secondary particle concentration [mol.m-3]",
domain="negative secondary particle",
auxiliary_domains={"secondary": "negative electrode"},
- scale=param.n.sec.c_max,
+ scale=self.param.n.sec.c_max,
)
c_s_p = pybamm.Variable(
"Positive particle concentration [mol.m-3]",
domain="positive particle",
auxiliary_domains={"secondary": "positive electrode"},
- scale=param.p.prim.c_max,
+ scale=self.param.p.prim.c_max,
)
# Constant temperature
- T = param.T_init
+ T = self.param.T_init
######################
# Other set-up
######################
# Current density
- i_cell = param.current_density_with_time
+ i_cell = self.param.current_density_with_time
# Porosity
# Primary broadcasts are used to broadcast scalar quantities across a domain
@@ -138,16 +137,16 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
# Tortuosity
tor = pybamm.concatenation(
- eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e
+ eps_n**self.param.n.b_e, eps_s**self.param.s.b_e, eps_p**self.param.p.b_e
)
# Open-circuit potentials
c_s_surf_n_p1 = pybamm.surf(c_s_n_p1)
- sto_surf_n_p1 = c_s_surf_n_p1 / param.n.prim.c_max
- ocp_n_p1 = param.n.prim.U(sto_surf_n_p1, T)
+ sto_surf_n_p1 = c_s_surf_n_p1 / self.param.n.prim.c_max
+ ocp_n_p1 = self.param.n.prim.U(sto_surf_n_p1, T)
c_s_surf_n_p2 = pybamm.surf(c_s_n_p2)
- sto_surf_n_p2 = c_s_surf_n_p2 / param.n.sec.c_max
+ sto_surf_n_p2 = c_s_surf_n_p2 / self.param.n.sec.c_max
k = 100
m_lith = pybamm.sigmoid(i_cell, 0, k) # for lithation (current < 0)
m_delith = 1 - m_lith # for delithiation (current > 0)
@@ -156,38 +155,42 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
ocp_n_p2 = m_lith * U_lith + m_delith * U_delith
c_s_surf_p = pybamm.surf(c_s_p)
- sto_surf_p = c_s_surf_p / param.p.prim.c_max
- ocp_p = param.p.prim.U(sto_surf_p, T)
+ sto_surf_p = c_s_surf_p / self.param.p.prim.c_max
+ ocp_p = self.param.p.prim.U(sto_surf_p, T)
# Interfacial reactions
# Surf takes the surface value of a variable, i.e. its boundary value on the
# right side. This is also accessible via `boundary_value(x, "right")`, with
# "left" providing the boundary value of the left side
- F_RT = param.F / (param.R * T)
- j0_n_p1 = param.n.prim.j0(c_e_n, c_s_surf_n_p1, T)
+ F_RT = self.param.F / (self.param.R * T)
+ j0_n_p1 = self.param.n.prim.j0(c_e_n, c_s_surf_n_p1, T)
j_n_p1 = (
2
* j0_n_p1
- * pybamm.sinh(param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1))
+ * pybamm.sinh(
+ self.param.n.prim.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p1)
+ )
)
- j0_n_p2 = param.n.sec.j0(c_e_n, c_s_surf_n_p2, T)
+ j0_n_p2 = self.param.n.sec.j0(c_e_n, c_s_surf_n_p2, T)
j_n_p2 = (
2
* j0_n_p2
- * pybamm.sinh(param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2))
+ * pybamm.sinh(
+ self.param.n.sec.ne / 2 * F_RT * (phi_s_n - phi_e_n - ocp_n_p2)
+ )
)
- j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T)
+ j0_p = self.param.p.prim.j0(c_e_p, c_s_surf_p, T)
a_j_s = pybamm.PrimaryBroadcast(0, "separator")
j_p = (
2
* j0_p
- * pybamm.sinh(param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p))
+ * pybamm.sinh(self.param.p.prim.ne / 2 * F_RT * (phi_s_p - phi_e_p - ocp_p))
)
# Volumetric
- a_n_p1 = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ
- a_n_p2 = 3 * param.n.sec.epsilon_s_av / param.n.sec.R_typ
- a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ
+ a_n_p1 = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ
+ a_n_p2 = 3 * self.param.n.sec.epsilon_s_av / self.param.n.sec.R_typ
+ a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ
a_j_n_p1 = a_n_p1 * j_n_p1
a_j_n_p2 = a_n_p2 * j_n_p2
a_j_n = a_j_n_p1 + a_j_n_p2
@@ -197,7 +200,7 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
######################
# State of Charge
######################
- I = param.current_with_time
+ I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
@@ -210,9 +213,9 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
# The div and grad operators will be converted to the appropriate matrix
# multiplication at the discretisation stage
- N_s_n_p1 = -param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1)
- N_s_n_p2 = -param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2)
- N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
+ N_s_n_p1 = -self.param.n.prim.D(c_s_n_p1, T) * pybamm.grad(c_s_n_p1)
+ N_s_n_p2 = -self.param.n.sec.D(c_s_n_p2, T) * pybamm.grad(c_s_n_p2)
+ N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
self.rhs[c_s_n_p1] = -pybamm.div(N_s_n_p1)
self.rhs[c_s_n_p2] = -pybamm.div(N_s_n_p2)
self.rhs[c_s_p] = -pybamm.div(N_s_p)
@@ -220,27 +223,27 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
self.boundary_conditions[c_s_n_p1] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_n_p1 / param.F / pybamm.surf(param.n.prim.D(c_s_n_p1, T)),
+ -j_n_p1 / self.param.F / pybamm.surf(self.param.n.prim.D(c_s_n_p1, T)),
"Neumann",
),
}
self.boundary_conditions[c_s_n_p2] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_n_p2 / param.F / pybamm.surf(param.n.sec.D(c_s_n_p2, T)),
+ -j_n_p2 / self.param.F / pybamm.surf(self.param.n.sec.D(c_s_n_p2, T)),
"Neumann",
),
}
self.boundary_conditions[c_s_p] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_p / param.F / pybamm.surf(param.p.prim.D(c_s_p, T)),
+ -j_p / self.param.F / pybamm.surf(self.param.p.prim.D(c_s_p, T)),
"Neumann",
),
}
- self.initial_conditions[c_s_n_p1] = param.n.prim.c_init
- self.initial_conditions[c_s_n_p2] = param.n.sec.c_init
- self.initial_conditions[c_s_p] = param.p.prim.c_init
+ self.initial_conditions[c_s_n_p1] = self.param.n.prim.c_init
+ self.initial_conditions[c_s_n_p2] = self.param.n.sec.c_init
+ self.initial_conditions[c_s_p] = self.param.p.prim.c_init
# Events specify points at which a solution should terminate
tolerance = 0.0000001
self.events += [
@@ -272,15 +275,15 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
######################
# Current in the solid
######################
- sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s
+ sigma_eff_n = self.param.n.sigma(T) * eps_s_n**self.param.n.b_s
i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n)
- sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s
+ sigma_eff_p = self.param.p.sigma(T) * eps_s_p**self.param.p.b_s
i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
# The `algebraic` dictionary contains differential equations, with the key being
# the main scalar variable of interest in the equation
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
- self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
+ self.algebraic[phi_s_n] = self.param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
+ self.algebraic[phi_s_p] = self.param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
self.boundary_conditions[phi_s_n] = {
"left": (pybamm.Scalar(0), "Dirichlet"),
"right": (pybamm.Scalar(0), "Neumann"),
@@ -295,34 +298,34 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
# We evaluate c_n_init at x=0 and c_p_init at x=1 (this is just an initial
# guess so actual value is not too important)
self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
- self.initial_conditions[phi_s_p] = param.ocv_init
+ self.initial_conditions[phi_s_p] = self.param.ocv_init
######################
# Current in the electrolyte
######################
- i_e = (param.kappa_e(c_e, T) * tor) * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ i_e = (self.param.kappa_e(c_e, T) * tor) * (
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j)
+ self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j)
self.boundary_conditions[phi_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[phi_e] = -param.n.prim.U_init
+ self.initial_conditions[phi_e] = -self.param.n.prim.U_init
######################
# Electrolyte concentration
######################
- N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
+ N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
self.rhs[c_e] = (1 / eps) * (
- -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F
+ -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F
)
self.boundary_conditions[c_e] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[c_e] = param.c_e_init
+ self.initial_conditions[c_e] = self.param.c_e_init
######################
# (Some) variables
@@ -400,8 +403,8 @@ def __init__(self, name="Composite graphite/silicon Doyle-Fuller-Newman model"):
}
# Events specify points at which a solution should terminate
self.events += [
- pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut),
- pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage),
+ pybamm.Event("Minimum voltage [V]", voltage - self.param.voltage_low_cut),
+ pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - voltage),
]
@property
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py
index bc1eba3a83..b23b9dba0f 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_dfn_half_cell.py
@@ -36,7 +36,6 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
- param = self.param
######################
# Variables
@@ -69,14 +68,14 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
phi_e = pybamm.concatenation(phi_e_s, phi_e_w)
# Constant temperature
- T = param.T_init
+ T = self.param.T_init
######################
# Other set-up
######################
# Current density
- i_cell = param.current_density_with_time
+ i_cell = self.param.current_density_with_time
# Define particle surface concentration
# Surf takes the surface value of a variable, i.e. its boundary value on the
@@ -94,39 +93,39 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
eps_w = pybamm.PrimaryBroadcast(
pybamm.Parameter("Positive electrode porosity"), "positive electrode"
)
- b_e_s = param.s.b_e
- b_e_w = param.p.b_e
+ b_e_s = self.param.s.b_e
+ b_e_w = self.param.p.b_e
# Interfacial reactions
- j0_w = param.p.prim.j0(c_e_w, c_s_surf_w, T)
- U_w = param.p.prim.U
- ne_w = param.p.prim.ne
+ j0_w = self.param.p.prim.j0(c_e_w, c_s_surf_w, T)
+ U_w = self.param.p.prim.U
+ ne_w = self.param.p.prim.ne
# Particle diffusion parameters
- D_w = param.p.prim.D
- c_w_init = param.p.prim.c_init
+ D_w = self.param.p.prim.D
+ c_w_init = self.param.p.prim.c_init
# Electrode equation parameters
eps_s_w = pybamm.Parameter("Positive electrode active material volume fraction")
- b_s_w = param.p.b_s
- sigma_w = param.p.sigma
+ b_s_w = self.param.p.b_s
+ sigma_w = self.param.p.sigma
# Other parameters (for outputs)
- c_w_max = param.p.prim.c_max
- L_w = param.p.L
+ c_w_max = self.param.p.prim.c_max
+ L_w = self.param.p.L
eps = pybamm.concatenation(eps_s, eps_w)
tor = pybamm.concatenation(eps_s**b_e_s, eps_w**b_e_w)
- F_RT = param.F / (param.R * T)
- RT_F = param.R * T / param.F
+ F_RT = self.param.F / (self.param.R * T)
+ RT_F = self.param.R * T / self.param.F
sto_surf_w = c_s_surf_w / c_w_max
j_w = (
2
* j0_w
* pybamm.sinh(ne_w / 2 * F_RT * (phi_s_w - phi_e_w - U_w(sto_surf_w, T)))
)
- R_w = param.p.prim.R_typ
+ R_w = self.param.p.prim.R_typ
a_w = 3 * eps_s_w / R_w
a_j_w = a_w * j_w
a_j_s = pybamm.PrimaryBroadcast(0, "separator")
@@ -135,7 +134,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
######################
# State of Charge
######################
- I = param.current_with_time
+ I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
@@ -154,7 +153,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
# derivatives
self.boundary_conditions[c_s_w] = {
"left": (pybamm.Scalar(0), "Neumann"),
- "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / param.F, "Neumann"),
+ "right": (-j_w / pybamm.surf(D_w(c_s_w, T)) / self.param.F, "Neumann"),
}
self.initial_conditions[c_s_w] = c_w_init
@@ -183,21 +182,23 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
),
}
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_s_w] = param.L_x**2 * (pybamm.div(i_s_w) + a_j_w)
+ self.algebraic[phi_s_w] = self.param.L_x**2 * (pybamm.div(i_s_w) + a_j_w)
# Initial conditions must also be provided for algebraic equations, as an
# initial guess for a root-finding algorithm which calculates consistent
# initial conditions
- self.initial_conditions[phi_s_w] = param.p.prim.U_init
+ self.initial_conditions[phi_s_w] = self.param.p.prim.U_init
######################
# Electrolyte concentration
######################
- N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
+ N_e = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
self.rhs[c_e] = (1 / eps) * (
- -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F
+ -pybamm.div(N_e) + (1 - self.param.t_plus(c_e, T)) * a_j / self.param.F
)
dce_dx = (
- -(1 - param.t_plus(c_e, T)) * i_cell / (tor * param.F * param.D_e(c_e, T))
+ -(1 - self.param.t_plus(c_e, T))
+ * i_cell
+ / (tor * self.param.F * self.param.D_e(c_e, T))
)
self.boundary_conditions[c_e] = {
@@ -205,7 +206,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[c_e] = param.c_e_init
+ self.initial_conditions[c_e] = self.param.c_e_init
self.events.append(
pybamm.Event(
"Zero electrolyte concentration cut-off", pybamm.min(c_e) - 0.002
@@ -215,16 +216,16 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
######################
# Current in the electrolyte
######################
- i_e = (param.kappa_e(c_e, T) * tor) * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ i_e = (self.param.kappa_e(c_e, T) * tor) * (
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# multiply by Lx**2 to improve conditioning
- self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j)
+ self.algebraic[phi_e] = self.param.L_x**2 * (pybamm.div(i_e) - a_j)
# reference potential
- L_Li = param.n.L
- sigma_Li = param.n.sigma
- j_Li = param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T)
+ L_Li = self.param.n.L
+ sigma_Li = self.param.n.sigma
+ j_Li = self.param.j0_Li_metal(pybamm.boundary_value(c_e, "left"), c_w_max, T)
eta_Li = 2 * RT_F * pybamm.arcsinh(i_cell / (2 * j_Li))
phi_s_cn = 0
@@ -237,7 +238,7 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
"right": (pybamm.Scalar(0), "Neumann"),
}
- self.initial_conditions[phi_e] = -param.n.prim.U_init
+ self.initial_conditions[phi_e] = -self.param.n.prim.U_init
######################
# (Some) variables
@@ -290,11 +291,15 @@ def __init__(self, options=None, name="Doyle-Fuller-Newman half cell model"):
"X-averaged positive particle surface concentration "
"[mol.m-3]": c_s_surf_w_av,
"Positive particle concentration [mol.m-3]": c_s_w,
- "Total lithium in positive electrode [mol]": c_s_vol_av * L_w * param.A_cc,
+ "Total lithium in positive electrode [mol]": c_s_vol_av
+ * L_w
+ * self.param.A_cc,
"Electrolyte concentration [mol.m-3]": c_e,
"Separator electrolyte concentration [mol.m-3]": c_e_s,
"Positive electrolyte concentration [mol.m-3]": c_e_w,
- "Total lithium in electrolyte [mol]": c_e_total * param.L_x * param.A_cc,
+ "Total lithium in electrolyte [mol]": c_e_total
+ * self.param.L_x
+ * self.param.A_cc,
"Current [A]": I,
"Current variable [A]": I, # for compatibility with pybamm.Experiment
"Current density [A.m-2]": i_cell,
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py
new file mode 100644
index 0000000000..386a5c08fc
--- /dev/null
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_splitOCVR.py
@@ -0,0 +1,100 @@
+#
+# Equivalent Circuit Model with split OCV
+#
+import pybamm
+
+
+class SplitOCVR(pybamm.BaseModel):
+ """Basic Equivalent Circuit Model that uses two OCV functions
+ for each electrode. This model is easily parameterizable with minimal parameters.
+ This class differs from the :class: pybamm.equivalent_circuit.Thevenin() due
+ to dual OCV functions to make up the voltage from each electrode.
+
+ Parameters
+ ----------
+ name: str, optional
+ The name of the model.
+ """
+
+ def __init__(self, name="ECM with split OCV"):
+ super().__init__(name)
+
+ ######################
+ # Variables
+ ######################
+ # All variables are only time-dependent
+ # No domain definition needed
+
+ theta_n = pybamm.Variable("Negative particle stoichiometry")
+ theta_p = pybamm.Variable("Positive particle stoichiometry")
+ Q = pybamm.Variable("Discharge capacity [A.h]")
+ V = pybamm.Variable("Voltage [V]")
+
+ # model is isothermal
+ I = pybamm.FunctionParameter("Current function [A]", {"Time [s]": pybamm.t})
+
+ # Capacity equation
+ self.rhs[Q] = I / 3600
+ self.initial_conditions[Q] = pybamm.Scalar(0)
+
+ # Capacity in each electrode
+ Q_n = pybamm.Parameter("Negative electrode capacity [A.h]")
+ Q_p = pybamm.Parameter("Positive electrode capacity [A.h]")
+
+ # State of charge electrode equations
+ theta_n_0 = pybamm.Parameter("Negative electrode initial stoichiometry")
+ theta_p_0 = pybamm.Parameter("Positive electrode initial stoichiometry")
+ self.rhs[theta_n] = -I / Q_n / 3600
+ self.rhs[theta_p] = I / Q_p / 3600
+ self.initial_conditions[theta_n] = theta_n_0
+ self.initial_conditions[theta_p] = theta_p_0
+
+ # Resistance for IR expression
+ R = pybamm.Parameter("Ohmic resistance [Ohm]")
+
+ # Open-circuit potential for each electrode
+ Un = pybamm.FunctionParameter(
+ "Negative electrode OCP [V]", {"Negative particle stoichiometry": theta_n}
+ )
+ Up = pybamm.FunctionParameter(
+ "Positive electrode OCP [V]", {"Positive particle stoichiometry": theta_p}
+ )
+
+ # Voltage expression
+ V = Up - Un - I * R
+
+ # Parameters for Voltage cutoff
+ voltage_high_cut = pybamm.Parameter("Upper voltage cut-off [V]")
+ voltage_low_cut = pybamm.Parameter("Lower voltage cut-off [V]")
+
+ self.variables = {
+ "Negative particle stoichiometry": theta_n,
+ "Positive particle stoichiometry": theta_p,
+ "Current [A]": I,
+ "Discharge capacity [A.h]": Q,
+ "Voltage [V]": V,
+ "Times [s]": pybamm.t,
+ "Positive electrode OCP [V]": Up,
+ "Negative electrode OCP [V]": Un,
+ "Current function [A]": I,
+ }
+
+ # Events specify points at which a solution should terminate
+ self.events += [
+ pybamm.Event("Minimum voltage [V]", V - voltage_low_cut),
+ pybamm.Event("Maximum voltage [V]", voltage_high_cut - V),
+ pybamm.Event("Maximum Negative Electrode stoichiometry", 0.999 - theta_n),
+ pybamm.Event("Maximum Positive Electrode stoichiometry", 0.999 - theta_p),
+ pybamm.Event("Minimum Negative Electrode stoichiometry", theta_n - 0.0001),
+ pybamm.Event("Minimum Positive Electrode stoichiometry", theta_p - 0.0001),
+ ]
+
+ @property
+ def default_quick_plot_variables(self):
+ return [
+ "Voltage [V]",
+ ["Negative particle stoichiometry", "Positive particle stoichiometry"],
+ "Negative electrode OCP [V]",
+ "Positive electrode OCP [V]",
+ "Current [A]",
+ ]
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py
index 6bd93f3b27..cd1b968017 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/basic_spm.py
@@ -26,7 +26,6 @@ def __init__(self, name="Single Particle Model"):
# `param` is a class containing all the relevant parameters and functions for
# this model. These are purely symbolic at this stage, and will be set by the
# `ParameterValues` class when the model is processed.
- param = self.param
######################
# Variables
@@ -44,23 +43,23 @@ def __init__(self, name="Single Particle Model"):
)
# Constant temperature
- T = param.T_init
+ T = self.param.T_init
######################
# Other set-up
######################
# Current density
- i_cell = param.current_density_with_time
- a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ
- a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ
- j_n = i_cell / (param.n.L * a_n)
- j_p = -i_cell / (param.p.L * a_p)
+ i_cell = self.param.current_density_with_time
+ a_n = 3 * self.param.n.prim.epsilon_s_av / self.param.n.prim.R_typ
+ a_p = 3 * self.param.p.prim.epsilon_s_av / self.param.p.prim.R_typ
+ j_n = i_cell / (self.param.n.L * a_n)
+ j_p = -i_cell / (self.param.p.L * a_p)
######################
# State of Charge
######################
- I = param.current_with_time
+ I = self.param.current_with_time
# The `rhs` dictionary contains differential equations, with the key being the
# variable in the d/dt
self.rhs[Q] = I / 3600
@@ -73,8 +72,8 @@ def __init__(self, name="Single Particle Model"):
# The div and grad operators will be converted to the appropriate matrix
# multiplication at the discretisation stage
- N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n)
- N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
+ N_s_n = -self.param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n)
+ N_s_p = -self.param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
self.rhs[c_s_n] = -pybamm.div(N_s_n)
self.rhs[c_s_p] = -pybamm.div(N_s_p)
# Surf takes the surface value of a variable, i.e. its boundary value on the
@@ -86,24 +85,24 @@ def __init__(self, name="Single Particle Model"):
self.boundary_conditions[c_s_n] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))),
+ -j_n / (self.param.F * pybamm.surf(self.param.n.prim.D(c_s_n, T))),
"Neumann",
),
}
self.boundary_conditions[c_s_p] = {
"left": (pybamm.Scalar(0), "Neumann"),
"right": (
- -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))),
+ -j_p / (self.param.F * pybamm.surf(self.param.p.prim.D(c_s_p, T))),
"Neumann",
),
}
# c_n_init and c_p_init are functions of r and x, but for the SPM we
# take the x-averaged value since there is no x-dependence in the particles
- self.initial_conditions[c_s_n] = pybamm.x_average(param.n.prim.c_init)
- self.initial_conditions[c_s_p] = pybamm.x_average(param.p.prim.c_init)
+ self.initial_conditions[c_s_n] = pybamm.x_average(self.param.n.prim.c_init)
+ self.initial_conditions[c_s_p] = pybamm.x_average(self.param.p.prim.c_init)
# Events specify points at which a solution should terminate
- sto_surf_n = c_s_surf_n / param.n.prim.c_max
- sto_surf_p = c_s_surf_p / param.p.prim.c_max
+ sto_surf_n = c_s_surf_n / self.param.n.prim.c_max
+ sto_surf_p = c_s_surf_p / self.param.p.prim.c_max
self.events += [
pybamm.Event(
"Minimum negative particle surface stoichiometry",
@@ -130,14 +129,14 @@ def __init__(self, name="Single Particle Model"):
# (Some) variables
######################
# Interfacial reactions
- RT_F = param.R * T / param.F
- j0_n = param.n.prim.j0(param.c_e_init_av, c_s_surf_n, T)
- j0_p = param.p.prim.j0(param.c_e_init_av, c_s_surf_p, T)
- eta_n = (2 / param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n))
- eta_p = (2 / param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p))
+ RT_F = self.param.R * T / self.param.F
+ j0_n = self.param.n.prim.j0(self.param.c_e_init_av, c_s_surf_n, T)
+ j0_p = self.param.p.prim.j0(self.param.c_e_init_av, c_s_surf_p, T)
+ eta_n = (2 / self.param.n.prim.ne) * RT_F * pybamm.arcsinh(j_n / (2 * j0_n))
+ eta_p = (2 / self.param.p.prim.ne) * RT_F * pybamm.arcsinh(j_p / (2 * j0_p))
phi_s_n = 0
- phi_e = -eta_n - param.n.prim.U(sto_surf_n, T)
- phi_s_p = eta_p + phi_e + param.p.prim.U(sto_surf_p, T)
+ phi_e = -eta_n - self.param.n.prim.U(sto_surf_n, T)
+ phi_s_p = eta_p + phi_e + self.param.p.prim.U(sto_surf_p, T)
V = phi_s_p
num_cells = pybamm.Parameter(
"Number of cells connected in series to make a battery"
@@ -157,7 +156,7 @@ def __init__(self, name="Single Particle Model"):
c_s_surf_n, "negative electrode"
),
"Electrolyte concentration [mol.m-3]": pybamm.PrimaryBroadcast(
- param.c_e_init_av, whole_cell
+ self.param.c_e_init_av, whole_cell
),
"X-averaged positive particle concentration [mol.m-3]": c_s_p,
"Positive particle surface "
@@ -178,6 +177,6 @@ def __init__(self, name="Single Particle Model"):
}
# Events specify points at which a solution should terminate
self.events += [
- pybamm.Event("Minimum voltage [V]", V - param.voltage_low_cut),
- pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - V),
+ pybamm.Event("Minimum voltage [V]", V - self.param.voltage_low_cut),
+ pybamm.Event("Maximum voltage [V]", self.param.voltage_high_cut - V),
]
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 a5710dc986..a743910905 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
@@ -657,6 +657,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
The tolerance for the solver used to compute the initial stoichiometries.
A lower value results in higher precision but may increase computation time.
Default is 1e-6.
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
Returns
-------
@@ -664,15 +666,14 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
The initial stoichiometries that give the desired initial state of charge
"""
parameter_values = self.parameter_values
- param = self.param
x_0, x_100, y_100, y_0 = self.get_min_max_stoichiometries(inputs=inputs)
if isinstance(initial_value, str) and initial_value.endswith("V"):
V_init = float(initial_value[:-1])
- V_min = parameter_values.evaluate(param.ocp_soc_0)
- V_max = parameter_values.evaluate(param.ocp_soc_100)
+ V_min = parameter_values.evaluate(self.param.ocp_soc_0)
+ V_max = parameter_values.evaluate(self.param.ocp_soc_100)
- if not V_min <= V_init <= V_max:
+ if not V_min - tol <= V_init <= V_max + tol:
raise ValueError(
f"Initial voltage {V_init}V is outside the voltage limits "
f"({V_min}, {V_max})"
@@ -685,8 +686,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
y = y_0 - soc * (y_0 - y_100)
T_ref = parameter_values["Reference temperature [K]"]
if self.options["open-circuit potential"] == "MSMR":
- xn = param.n.prim.x
- xp = param.p.prim.x
+ xn = self.param.n.prim.x
+ xp = self.param.p.prim.x
Up = pybamm.Variable("Up")
Un = pybamm.Variable("Un")
soc_model.algebraic[Up] = x - xn(Un, T_ref)
@@ -695,8 +696,8 @@ def get_initial_stoichiometries(self, initial_value, tol=1e-6, inputs=None):
soc_model.initial_conditions[Up] = V_max
soc_model.algebraic[soc] = Up - Un - V_init
else:
- Up = param.p.prim.U
- Un = param.n.prim.U
+ Up = self.param.p.prim.U
+ Un = self.param.n.prim.U
soc_model.algebraic[soc] = Up(y, T_ref) - Un(x, T_ref) - V_init
# initial guess for soc linearly interpolates between 0 and 1
# based on V linearly interpolating between V_max and V_min
@@ -727,6 +728,11 @@ def get_min_max_stoichiometries(self, inputs=None):
Calculate min/max stoichiometries
given voltage limits, open-circuit potentials, etc defined by parameter_values
+ Parameters
+ ----------
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
+
Returns
-------
x_0, x_100, y_100, y_0
@@ -734,24 +740,25 @@ def get_min_max_stoichiometries(self, inputs=None):
"""
inputs = inputs or {}
parameter_values = self.parameter_values
- param = self.param
- Q_n = parameter_values.evaluate(param.n.Q_init, inputs=inputs)
- Q_p = parameter_values.evaluate(param.p.Q_init, inputs=inputs)
+ Q_n = parameter_values.evaluate(self.param.n.Q_init, inputs=inputs)
+ Q_p = parameter_values.evaluate(self.param.p.Q_init, inputs=inputs)
if self.known_value == "cyclable lithium capacity":
- Q_Li = parameter_values.evaluate(param.Q_Li_particles_init, inputs=inputs)
+ Q_Li = parameter_values.evaluate(
+ self.param.Q_Li_particles_init, inputs=inputs
+ )
all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
elif self.known_value == "cell capacity":
Q = parameter_values.evaluate(
- param.Q / param.n_electrodes_parallel, inputs=inputs
+ self.param.Q / self.param.n_electrodes_parallel, inputs=inputs
)
all_inputs = {**inputs, "Q_n": Q_n, "Q_p": Q_p, "Q": Q}
# Solve the model and check outputs
sol = self.solve(all_inputs)
return [sol["x_0"], sol["x_100"], sol["y_100"], sol["y_0"]]
- def get_initial_ocps(self, initial_value, tol=1e-6):
+ def get_initial_ocps(self, initial_value, tol=1e-6, inputs=None):
"""
Calculate initial open-circuit potentials to start off the simulation at a
particular state of charge, given voltage limits, open-circuit potentials, etc
@@ -760,9 +767,14 @@ def get_initial_ocps(self, initial_value, tol=1e-6):
Parameters
----------
initial_value : float
- Target SOC, must be between 0 and 1.
+ Target initial value.
+ If integer, interpreted as SOC, must be between 0 and 1.
+ If string e.g. "4 V", interpreted as voltage,
+ must be between V_min and V_max.
tol: float, optional
Tolerance for the solver used in calculating initial stoichiometries.
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
Returns
-------
@@ -770,8 +782,7 @@ def get_initial_ocps(self, initial_value, tol=1e-6):
The initial open-circuit potentials at the desired initial state of charge
"""
parameter_values = self.parameter_values
- param = self.param
- x, y = self.get_initial_stoichiometries(initial_value, tol)
+ x, y = self.get_initial_stoichiometries(initial_value, tol, inputs=inputs)
if self.options["open-circuit potential"] == "MSMR":
msmr_pot_model = _get_msmr_potential_model(
self.parameter_values, self.param
@@ -783,8 +794,8 @@ def get_initial_ocps(self, initial_value, tol=1e-6):
Up = sol["Up"].data[0]
else:
T_ref = parameter_values["Reference temperature [K]"]
- Un = parameter_values.evaluate(param.n.prim.U(x, T_ref))
- Up = parameter_values.evaluate(param.p.prim.U(y, T_ref))
+ Un = parameter_values.evaluate(self.param.n.prim.U(x, T_ref), inputs=inputs)
+ Up = parameter_values.evaluate(self.param.p.prim.U(y, T_ref), inputs=inputs)
return Un, Up
def get_min_max_ocps(self):
@@ -798,16 +809,17 @@ def get_min_max_ocps(self):
The min/max ocps
"""
parameter_values = self.parameter_values
- param = self.param
- Q_n = parameter_values.evaluate(param.n.Q_init)
- Q_p = parameter_values.evaluate(param.p.Q_init)
+ Q_n = parameter_values.evaluate(self.param.n.Q_init)
+ Q_p = parameter_values.evaluate(self.param.p.Q_init)
if self.known_value == "cyclable lithium capacity":
- Q_Li = parameter_values.evaluate(param.Q_Li_particles_init)
+ Q_Li = parameter_values.evaluate(self.param.Q_Li_particles_init)
inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}
elif self.known_value == "cell capacity":
- Q = parameter_values.evaluate(param.Q / param.n_electrodes_parallel)
+ Q = parameter_values.evaluate(
+ self.param.Q / self.param.n_electrodes_parallel
+ )
inputs = {"Q_n": Q_n, "Q_p": Q_p, "Q": Q}
# Solve the model and check outputs
sol = self.solve(inputs)
@@ -822,10 +834,10 @@ def theoretical_energy_integral(self, inputs, points=1000):
x_vals = np.linspace(x_100, x_0, num=points)
y_vals = np.linspace(y_100, y_0, num=points)
# Calculate OCV at each stoichiometry
- param = self.param
- T = param.T_amb_av(0)
+ T = self.param.T_amb_av(0)
Vs = self.parameter_values.evaluate(
- param.p.prim.U(y_vals, T) - param.n.prim.U(x_vals, T), inputs=inputs
+ self.param.p.prim.U(y_vals, T) - self.param.n.prim.U(x_vals, T),
+ inputs=inputs,
).flatten()
# Calculate dQ
Q = Q_p * (y_0 - y_100)
@@ -869,8 +881,9 @@ def get_initial_stoichiometries(
:class:`pybamm.BatteryModelOptions`.
tol : float, optional
The tolerance for the solver used to compute the initial stoichiometries.
- A lower value results in higher precision but may increase computation time.
Default is 1e-6.
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
Returns
-------
@@ -918,6 +931,8 @@ def get_initial_ocps(
param=None,
known_value="cyclable lithium capacity",
options=None,
+ tol=1e-6,
+ inputs=None,
):
"""
Calculate initial open-circuit potentials to start off the simulation at a
@@ -942,6 +957,10 @@ def get_initial_ocps(
options : dict-like, optional
A dictionary of options to be passed to the model, see
:class:`pybamm.BatteryModelOptions`.
+ tol: float, optional
+ Tolerance for the solver used in calculating initial open-circuit potentials.
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
Returns
-------
@@ -949,7 +968,7 @@ def get_initial_ocps(
The initial electrode OCPs that give the desired initial state of charge
"""
esoh_solver = ElectrodeSOHSolver(parameter_values, param, known_value, options)
- return esoh_solver.get_initial_ocps(initial_value)
+ return esoh_solver.get_initial_ocps(initial_value, tol, inputs=inputs)
def get_min_max_ocps(
diff --git a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
index be7ced642e..1b54b6faad 100644
--- a/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
+++ b/src/pybamm/models/full_battery_models/lithium_ion/electrode_soh_half_cell.py
@@ -63,6 +63,7 @@ def get_initial_stoichiometry_half_cell(
parameter_values,
param=None,
options=None,
+ tol=1e-6,
inputs=None,
**kwargs,
):
@@ -80,7 +81,14 @@ def get_initial_stoichiometry_half_cell(
must be between V_min and V_max.
parameter_values : pybamm.ParameterValues
The parameter values to use in the calculation
-
+ param : :class:`pybamm.LithiumIonParameters`, optional
+ The symbolic parameter set to use for the simulation.
+ If not provided, the default parameter set will be used.
+ tol : float, optional
+ The tolerance for the solver used to compute the initial stoichiometries.
+ Default is 1e-6.
+ inputs : dict, optional
+ A dictionary of input parameters passed to the model.
Returns
-------
x
@@ -94,7 +102,7 @@ def get_initial_stoichiometry_half_cell(
V_min = parameter_values.evaluate(param.voltage_low_cut)
V_max = parameter_values.evaluate(param.voltage_high_cut)
- if not V_min <= V_init <= V_max:
+ if not V_min - tol <= V_init <= V_max + tol:
raise ValueError(
f"Initial voltage {V_init}V is outside the voltage limits "
f"({V_min}, {V_max})"
@@ -113,7 +121,9 @@ def get_initial_stoichiometry_half_cell(
soc_model.initial_conditions[soc] = (V_init - V_min) / (V_max - V_min)
soc_model.variables["soc"] = soc
parameter_values.process_model(soc_model)
- initial_soc = pybamm.AlgebraicSolver().solve(soc_model, [0])["soc"].data[0]
+ initial_soc = (
+ pybamm.AlgebraicSolver(tol=tol).solve(soc_model, [0])["soc"].data[0]
+ )
elif isinstance(initial_value, (int, float)):
initial_soc = initial_value
if not 0 <= initial_soc <= 1:
diff --git a/src/pybamm/models/full_battery_models/sodium_ion/__init__.py b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py
new file mode 100644
index 0000000000..52e4e54952
--- /dev/null
+++ b/src/pybamm/models/full_battery_models/sodium_ion/__init__.py
@@ -0,0 +1,6 @@
+#
+# Root of the sodium-ion models module.
+#
+from .basic_dfn import BasicDFN
+
+__all__ = ['basic_dfn']
diff --git a/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py
new file mode 100644
index 0000000000..c6f618d338
--- /dev/null
+++ b/src/pybamm/models/full_battery_models/sodium_ion/basic_dfn.py
@@ -0,0 +1,273 @@
+#
+# Basic Doyle-Fuller-Newman (DFN) Model
+#
+import pybamm
+
+
+class BasicDFN(pybamm.lithium_ion.BaseModel):
+ """Doyle-Fuller-Newman (DFN) model of a sodium-ion battery, from
+ :footcite:t:`Marquis2019`.
+
+ Parameters
+ ----------
+ name : str, optional
+ The name of the model.
+
+ """
+
+ def __init__(self, name="Doyle-Fuller-Newman model"):
+ super().__init__(name=name)
+ pybamm.citations.register("Marquis2019")
+ # `param` is a class containing all the relevant parameters and functions for
+ # this model. These are purely symbolic at this stage, and will be set by the
+ # `ParameterValues` class when the model is processed.
+ param = self.param
+
+ ######################
+ # Variables
+ ######################
+ # Variables that depend on time only are created without a domain
+ Q = pybamm.Variable("Discharge capacity [A.h]")
+
+ # Variables that vary spatially are created with a domain
+ c_e_n = pybamm.Variable(
+ "Negative electrolyte concentration [mol.m-3]",
+ domain="negative electrode",
+ )
+ c_e_s = pybamm.Variable(
+ "Separator electrolyte concentration [mol.m-3]",
+ domain="separator",
+ )
+ c_e_p = pybamm.Variable(
+ "Positive electrolyte concentration [mol.m-3]",
+ domain="positive electrode",
+ )
+ # Concatenations combine several variables into a single variable, to simplify
+ # implementing equations that hold over several domains
+ c_e = pybamm.concatenation(c_e_n, c_e_s, c_e_p)
+
+ # Electrolyte potential
+ phi_e_n = pybamm.Variable(
+ "Negative electrolyte potential [V]",
+ domain="negative electrode",
+ )
+ phi_e_s = pybamm.Variable(
+ "Separator electrolyte potential [V]",
+ domain="separator",
+ )
+ phi_e_p = pybamm.Variable(
+ "Positive electrolyte potential [V]",
+ domain="positive electrode",
+ )
+ phi_e = pybamm.concatenation(phi_e_n, phi_e_s, phi_e_p)
+
+ # Electrode potential
+ phi_s_n = pybamm.Variable(
+ "Negative electrode potential [V]", domain="negative electrode"
+ )
+ phi_s_p = pybamm.Variable(
+ "Positive electrode potential [V]",
+ domain="positive electrode",
+ )
+ # Particle concentrations are variables on the particle domain, but also vary in
+ # the x-direction (electrode domain) and so must be provided with auxiliary
+ # domains
+ c_s_n = pybamm.Variable(
+ "Negative particle concentration [mol.m-3]",
+ domain="negative particle",
+ auxiliary_domains={"secondary": "negative electrode"},
+ )
+ c_s_p = pybamm.Variable(
+ "Positive particle concentration [mol.m-3]",
+ domain="positive particle",
+ auxiliary_domains={"secondary": "positive electrode"},
+ )
+
+ # Constant temperature
+ T = param.T_init
+
+ ######################
+ # Other set-up
+ ######################
+
+ # Current density
+ i_cell = param.current_density_with_time
+
+ # Porosity
+ # Primary broadcasts are used to broadcast scalar quantities across a domain
+ # into a vector of the right shape, for multiplying with other vectors
+ eps_n = pybamm.PrimaryBroadcast(
+ pybamm.Parameter("Negative electrode porosity"), "negative electrode"
+ )
+ eps_s = pybamm.PrimaryBroadcast(
+ pybamm.Parameter("Separator porosity"), "separator"
+ )
+ eps_p = pybamm.PrimaryBroadcast(
+ pybamm.Parameter("Positive electrode porosity"), "positive electrode"
+ )
+ eps = pybamm.concatenation(eps_n, eps_s, eps_p)
+
+ # Active material volume fraction (eps + eps_s + eps_inactive = 1)
+ eps_s_n = pybamm.Parameter("Negative electrode active material volume fraction")
+ eps_s_p = pybamm.Parameter("Positive electrode active material volume fraction")
+
+ # transport_efficiency
+ tor = pybamm.concatenation(
+ eps_n**param.n.b_e, eps_s**param.s.b_e, eps_p**param.p.b_e
+ )
+ a_n = 3 * param.n.prim.epsilon_s_av / param.n.prim.R_typ
+ a_p = 3 * param.p.prim.epsilon_s_av / param.p.prim.R_typ
+
+ # Interfacial reactions
+ # Surf takes the surface value of a variable, i.e. its boundary value on the
+ # right side. This is also accessible via `boundary_value(x, "right")`, with
+ # "left" providing the boundary value of the left side
+ c_s_surf_n = pybamm.surf(c_s_n)
+ sto_surf_n = c_s_surf_n / param.n.prim.c_max
+ j0_n = param.n.prim.j0(c_e_n, c_s_surf_n, T)
+ eta_n = phi_s_n - phi_e_n - param.n.prim.U(sto_surf_n, T)
+ Feta_RT_n = param.F * eta_n / (param.R * T)
+ j_n = 2 * j0_n * pybamm.sinh(param.n.prim.ne / 2 * Feta_RT_n)
+
+ c_s_surf_p = pybamm.surf(c_s_p)
+ sto_surf_p = c_s_surf_p / param.p.prim.c_max
+ j0_p = param.p.prim.j0(c_e_p, c_s_surf_p, T)
+ eta_p = phi_s_p - phi_e_p - param.p.prim.U(sto_surf_p, T)
+ Feta_RT_p = param.F * eta_p / (param.R * T)
+ j_s = pybamm.PrimaryBroadcast(0, "separator")
+ j_p = 2 * j0_p * pybamm.sinh(param.p.prim.ne / 2 * Feta_RT_p)
+
+ a_j_n = a_n * j_n
+ a_j_p = a_p * j_p
+ a_j = pybamm.concatenation(a_j_n, j_s, a_j_p)
+
+ ######################
+ # State of Charge
+ ######################
+ I = param.current_with_time
+ # The `rhs` dictionary contains differential equations, with the key being the
+ # variable in the d/dt
+ self.rhs[Q] = I / 3600
+ # Initial conditions must be provided for the ODEs
+ self.initial_conditions[Q] = pybamm.Scalar(0)
+
+ ######################
+ # Particles
+ ######################
+
+ # The div and grad operators will be converted to the appropriate matrix
+ # multiplication at the discretisation stage
+ N_s_n = -param.n.prim.D(c_s_n, T) * pybamm.grad(c_s_n)
+ N_s_p = -param.p.prim.D(c_s_p, T) * pybamm.grad(c_s_p)
+ self.rhs[c_s_n] = -pybamm.div(N_s_n)
+ self.rhs[c_s_p] = -pybamm.div(N_s_p)
+ # Boundary conditions must be provided for equations with spatial derivatives
+ self.boundary_conditions[c_s_n] = {
+ "left": (pybamm.Scalar(0), "Neumann"),
+ "right": (
+ -j_n / (param.F * pybamm.surf(param.n.prim.D(c_s_n, T))),
+ "Neumann",
+ ),
+ }
+ self.boundary_conditions[c_s_p] = {
+ "left": (pybamm.Scalar(0), "Neumann"),
+ "right": (
+ -j_p / (param.F * pybamm.surf(param.p.prim.D(c_s_p, T))),
+ "Neumann",
+ ),
+ }
+ self.initial_conditions[c_s_n] = param.n.prim.c_init
+ self.initial_conditions[c_s_p] = param.p.prim.c_init
+ ######################
+ # Current in the solid
+ ######################
+ sigma_eff_n = param.n.sigma(T) * eps_s_n**param.n.b_s
+ i_s_n = -sigma_eff_n * pybamm.grad(phi_s_n)
+ sigma_eff_p = param.p.sigma(T) * eps_s_p**param.p.b_s
+ i_s_p = -sigma_eff_p * pybamm.grad(phi_s_p)
+ # The `algebraic` dictionary contains differential equations, with the key being
+ # the main scalar variable of interest in the equation
+ # multiply by Lx**2 to improve conditioning
+ self.algebraic[phi_s_n] = param.L_x**2 * (pybamm.div(i_s_n) + a_j_n)
+ self.algebraic[phi_s_p] = param.L_x**2 * (pybamm.div(i_s_p) + a_j_p)
+ self.boundary_conditions[phi_s_n] = {
+ "left": (pybamm.Scalar(0), "Dirichlet"),
+ "right": (pybamm.Scalar(0), "Neumann"),
+ }
+ self.boundary_conditions[phi_s_p] = {
+ "left": (pybamm.Scalar(0), "Neumann"),
+ "right": (i_cell / pybamm.boundary_value(-sigma_eff_p, "right"), "Neumann"),
+ }
+ # Initial conditions must also be provided for algebraic equations, as an
+ # initial guess for a root-finding algorithm which calculates consistent initial
+ # conditions
+ self.initial_conditions[phi_s_n] = pybamm.Scalar(0)
+ self.initial_conditions[phi_s_p] = param.ocv_init
+
+ ######################
+ # Current in the electrolyte
+ ######################
+ i_e = (param.kappa_e(c_e, T) * tor) * (
+ param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ )
+ # multiply by Lx**2 to improve conditioning
+ self.algebraic[phi_e] = param.L_x**2 * (pybamm.div(i_e) - a_j)
+ self.boundary_conditions[phi_e] = {
+ "left": (pybamm.Scalar(0), "Neumann"),
+ "right": (pybamm.Scalar(0), "Neumann"),
+ }
+ self.initial_conditions[phi_e] = -param.n.prim.U_init
+
+ ######################
+ # Electrolyte concentration
+ ######################
+ N_e = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
+ self.rhs[c_e] = (1 / eps) * (
+ -pybamm.div(N_e) + (1 - param.t_plus(c_e, T)) * a_j / param.F
+ )
+ self.boundary_conditions[c_e] = {
+ "left": (pybamm.Scalar(0), "Neumann"),
+ "right": (pybamm.Scalar(0), "Neumann"),
+ }
+ self.initial_conditions[c_e] = param.c_e_init
+
+ ######################
+ # (Some) variables
+ ######################
+ voltage = pybamm.boundary_value(phi_s_p, "right")
+ num_cells = pybamm.Parameter(
+ "Number of cells connected in series to make a battery"
+ )
+ # The `variables` dictionary contains all variables that might be useful for
+ # visualising the solution of the model
+ self.variables = {
+ "Negative particle concentration [mol.m-3]": c_s_n,
+ "Negative particle surface concentration [mol.m-3]": c_s_surf_n,
+ "Electrolyte concentration [mol.m-3]": c_e,
+ "Negative electrolyte concentration [mol.m-3]": c_e_n,
+ "Separator electrolyte concentration [mol.m-3]": c_e_s,
+ "Positive electrolyte concentration [mol.m-3]": c_e_p,
+ "Positive particle concentration [mol.m-3]": c_s_p,
+ "Positive particle surface concentration [mol.m-3]": c_s_surf_p,
+ "Current [A]": I,
+ "Current variable [A]": I, # for compatibility with pybamm.Experiment
+ "Negative electrode potential [V]": phi_s_n,
+ "Electrolyte potential [V]": phi_e,
+ "Negative electrolyte potential [V]": phi_e_n,
+ "Separator electrolyte potential [V]": phi_e_s,
+ "Positive electrolyte potential [V]": phi_e_p,
+ "Positive electrode potential [V]": phi_s_p,
+ "Voltage [V]": voltage,
+ "Battery voltage [V]": voltage * num_cells,
+ "Time [s]": pybamm.t,
+ "Discharge capacity [A.h]": Q,
+ }
+ # Events specify points at which a solution should terminate
+ self.events += [
+ pybamm.Event("Minimum voltage [V]", voltage - param.voltage_low_cut),
+ pybamm.Event("Maximum voltage [V]", param.voltage_high_cut - voltage),
+ ]
+
+ @property
+ def default_parameter_values(self):
+ return pybamm.ParameterValues("Chayambuka2022")
diff --git a/src/pybamm/models/submodels/active_material/base_active_material.py b/src/pybamm/models/submodels/active_material/base_active_material.py
index ba39adf852..ea0e826e09 100644
--- a/src/pybamm/models/submodels/active_material/base_active_material.py
+++ b/src/pybamm/models/submodels/active_material/base_active_material.py
@@ -23,7 +23,6 @@ def __init__(self, param, domain, options, phase="primary"):
super().__init__(param, domain, options=options, phase=phase)
def _get_standard_active_material_variables(self, eps_solid):
- param = self.param
phase_name = self.phase_name
domain, Domain = self.domain_Domain
@@ -61,9 +60,9 @@ def _get_standard_active_material_variables(self, eps_solid):
C = (
pybamm.yz_average(eps_solid_av)
* L
- * param.A_cc
+ * self.param.A_cc
* c_s_max
- * param.F
+ * self.param.F
/ 3600
)
if phase_name == "":
diff --git a/src/pybamm/models/submodels/active_material/constant_active_material.py b/src/pybamm/models/submodels/active_material/constant_active_material.py
index 3237775f1c..e978168e9a 100644
--- a/src/pybamm/models/submodels/active_material/constant_active_material.py
+++ b/src/pybamm/models/submodels/active_material/constant_active_material.py
@@ -23,6 +23,7 @@ class Constant(BaseModel):
def get_fundamental_variables(self):
domain = self.domain
+ phase = self.phase_name
eps_solid = self.phase_param.epsilon_s
deps_solid_dt = pybamm.FullBroadcast(
0, f"{domain} electrode", "current collector"
@@ -35,7 +36,7 @@ def get_fundamental_variables(self):
variables.update(
{
- "Loss of lithium due to loss of active material "
+ f"Loss of lithium due to loss of {phase}active material "
f"in {domain} electrode [mol]": pybamm.Scalar(0)
}
)
diff --git a/src/pybamm/models/submodels/active_material/loss_active_material.py b/src/pybamm/models/submodels/active_material/loss_active_material.py
index 6f027d89e6..ffba064d03 100644
--- a/src/pybamm/models/submodels/active_material/loss_active_material.py
+++ b/src/pybamm/models/submodels/active_material/loss_active_material.py
@@ -23,34 +23,36 @@ class LossActiveMaterial(BaseModel):
"""
- def __init__(self, param, domain, options, x_average):
- super().__init__(param, domain, options=options)
+ def __init__(self, param, domain, options, x_average, phase):
+ super().__init__(param, domain, options=options, phase=phase)
pybamm.citations.register("Reniers2019")
self.x_average = x_average
def get_fundamental_variables(self):
domain, Domain = self.domain_Domain
+ phase = self.phase_name
if self.x_average is True:
eps_solid_xav = pybamm.Variable(
- f"X-averaged {domain} electrode active material volume fraction",
+ f"X-averaged {domain} electrode {phase}active material volume fraction",
domain="current collector",
)
eps_solid = pybamm.PrimaryBroadcast(eps_solid_xav, f"{domain} electrode")
else:
eps_solid = pybamm.Variable(
- f"{Domain} electrode active material volume fraction",
+ f"{Domain} electrode {phase}active material volume fraction",
domain=f"{domain} electrode",
auxiliary_domains={"secondary": "current collector"},
)
variables = self._get_standard_active_material_variables(eps_solid)
lli_due_to_lam = pybamm.Variable(
- "Loss of lithium due to loss of active material "
+ f"Loss of lithium due to loss of {phase}active material "
f"in {domain} electrode [mol]"
)
+
variables.update(
{
- "Loss of lithium due to loss of active material "
+ f"Loss of lithium due to loss of {phase}active material "
f"in {domain} electrode [mol]": lli_due_to_lam
}
)
@@ -58,6 +60,7 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
domain, Domain = self.domain_Domain
+ phase_name = self.phase_name
deps_solid_dt = 0
lam_option = getattr(getattr(self.options, domain), self.phase)[
@@ -68,22 +71,22 @@ def get_coupled_variables(self, variables):
# This is loss of active material model by mechanical effects
if self.x_average is True:
stress_t_surf = variables[
- f"X-averaged {domain} particle surface tangential stress [Pa]"
+ f"X-averaged {domain} {phase_name}particle surface tangential stress [Pa]"
]
stress_r_surf = variables[
- f"X-averaged {domain} particle surface radial stress [Pa]"
+ f"X-averaged {domain} {phase_name}particle surface radial stress [Pa]"
]
else:
stress_t_surf = variables[
- f"{Domain} particle surface tangential stress [Pa]"
+ f"{Domain} {phase_name}particle surface tangential stress [Pa]"
]
stress_r_surf = variables[
- f"{Domain} particle surface radial stress [Pa]"
+ f"{Domain} {phase_name}particle surface radial stress [Pa]"
]
- beta_LAM = self.domain_param.beta_LAM
- stress_critical = self.domain_param.stress_critical
- m_LAM = self.domain_param.m_LAM
+ beta_LAM = self.phase_param.beta_LAM
+ stress_critical = self.phase_param.stress_critical
+ m_LAM = self.phase_param.m_LAM
stress_h_surf = (stress_r_surf + 2 * stress_t_surf) / 3
# compressive stress make no contribution
@@ -97,15 +100,15 @@ def get_coupled_variables(self, variables):
deps_solid_dt += j_stress_LAM
if "reaction" in lam_option:
- beta_LAM_sei = self.domain_param.beta_LAM_sei
+ beta_LAM_sei = self.phase_param.beta_LAM_sei
if self.x_average is True:
a_j_sei = variables[
- f"X-averaged {domain} electrode SEI "
+ f"X-averaged {domain} electrode {phase_name}SEI "
"volumetric interfacial current density [A.m-3]"
]
else:
a_j_sei = variables[
- f"{Domain} electrode SEI volumetric "
+ f"{Domain} electrode {phase_name}SEI volumetric "
"interfacial current density [A.m-3]"
]
@@ -131,19 +134,22 @@ def get_coupled_variables(self, variables):
def set_rhs(self, variables):
domain, Domain = self.domain_Domain
+ phase_name = self.phase_name
if self.x_average is True:
eps_solid = variables[
- f"X-averaged {domain} electrode active material volume fraction"
+ f"X-averaged {domain} electrode {phase_name}active material volume fraction"
]
deps_solid_dt = variables[
- f"X-averaged {domain} electrode active material "
+ f"X-averaged {domain} electrode {phase_name}active material "
"volume fraction change [s-1]"
]
else:
- eps_solid = variables[f"{Domain} electrode active material volume fraction"]
+ eps_solid = variables[
+ f"{Domain} electrode {phase_name}active material volume fraction"
+ ]
deps_solid_dt = variables[
- f"{Domain} electrode active material volume fraction change [s-1]"
+ f"{Domain} electrode {phase_name}active material volume fraction change [s-1]"
]
# Loss of lithium due to loss of active material
@@ -151,11 +157,13 @@ def set_rhs(self, variables):
# simulations using adaptive inter-cycle extrapolation algorithm."
# Journal of The Electrochemical Society 168.12 (2021): 120531.
lli_due_to_lam = variables[
- "Loss of lithium due to loss of active material "
+ f"Loss of lithium due to loss of {phase_name}active material "
f"in {domain} electrode [mol]"
]
# Multiply by mol.m-3 * m3 to get mol
- c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"]
+ c_s_av = variables[
+ f"Average {domain} {phase_name}particle concentration [mol.m-3]"
+ ]
V = self.domain_param.L * self.param.A_cc
self.rhs = {
@@ -166,20 +174,23 @@ def set_rhs(self, variables):
def set_initial_conditions(self, variables):
domain, Domain = self.domain_Domain
+ phase_name = self.phase_name
- eps_solid_init = self.domain_param.prim.epsilon_s
+ eps_solid_init = self.phase_param.epsilon_s
if self.x_average is True:
eps_solid_xav = variables[
- f"X-averaged {domain} electrode active material volume fraction"
+ f"X-averaged {domain} electrode {phase_name}active material volume fraction"
]
self.initial_conditions = {eps_solid_xav: pybamm.x_average(eps_solid_init)}
else:
- eps_solid = variables[f"{Domain} electrode active material volume fraction"]
+ eps_solid = variables[
+ f"{Domain} electrode {phase_name}active material volume fraction"
+ ]
self.initial_conditions = {eps_solid: eps_solid_init}
lli_due_to_lam = variables[
- "Loss of lithium due to loss of active material "
+ f"Loss of lithium due to loss of {phase_name}active material "
f"in {domain} electrode [mol]"
]
self.initial_conditions[lli_due_to_lam] = pybamm.Scalar(0)
diff --git a/src/pybamm/models/submodels/active_material/total_active_material.py b/src/pybamm/models/submodels/active_material/total_active_material.py
index 5e1d7e2f92..f86486ff53 100644
--- a/src/pybamm/models/submodels/active_material/total_active_material.py
+++ b/src/pybamm/models/submodels/active_material/total_active_material.py
@@ -34,6 +34,7 @@ def get_coupled_variables(self, variables):
f"{Domain} electrode {{}}active material volume fraction change [s-1]",
f"X-averaged {domain} electrode {{}}active material "
"volume fraction change [s-1]",
+ f"Loss of lithium due to loss of {{}}active material in {domain} electrode [mol]",
]:
sumvar = sum(
variables[variable_template.format(phase + " ")] for phase in phases
diff --git a/src/pybamm/models/submodels/base_submodel.py b/src/pybamm/models/submodels/base_submodel.py
index e120691edd..6b83d1f292 100644
--- a/src/pybamm/models/submodels/base_submodel.py
+++ b/src/pybamm/models/submodels/base_submodel.py
@@ -28,14 +28,28 @@ class BaseSubModel(pybamm.BaseModel):
Attributes
----------
- param: parameter class
- The model parameter symbols
- boundary_conditions: dict
- A dictionary that maps expressions (variables) to expressions that represent
- the boundary conditions
- variables: dict
- A dictionary that maps strings to expressions that represent
- the useful variables
+ param : parameter class
+ The model parameter symbols.
+ domain : str
+ The domain of the submodel, could be either 'Negative', 'Positive', 'Separator', or None.
+ name : str
+ The name of the submodel.
+ external : bool
+ A boolean flag indicating whether the variables defined by the submodel will be
+ provided externally by the user. Set to False by default.
+ options : dict or pybamm.BatteryModelOptions
+ A dictionary or an instance of `pybamm.BatteryModelOptions` that stores configuration
+ options for the submodel.
+ phase_name : str
+ A string representing the phase of the submodel, which could be "primary",
+ "secondary", or an empty string if there is only one phase.
+ phase : str or None
+ The current phase of the submodel, which could be "primary", "secondary", or None.
+ boundary_conditions : dict
+ A dictionary mapping variables to their respective boundary conditions.
+ variables : dict
+ A dictionary mapping variable names (strings) to expressions or objects that
+ represent the useful variables for the submodel.
"""
def __init__(
@@ -112,6 +126,7 @@ def domain(self, domain):
@property
def domain_Domain(self):
+ """Returns a tuple containing the current domain and its capitalized form."""
return self._domain, self._Domain
def get_parameter_info(self, by_submodel=False):
@@ -221,7 +236,7 @@ def set_initial_conditions(self, variables):
"""
pass
- def set_events(self, variables):
+ def add_events_from(self, variables):
"""
A method to set events related to the state of submodel variable. Note: this
method modifies the state of self.events. Unless overwritten by a submodel, the
diff --git a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py
index 33b58e2b23..c0423bfc41 100644
--- a/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py
+++ b/src/pybamm/models/submodels/convection/through_cell/explicit_convection.py
@@ -19,7 +19,6 @@ def __init__(self, param):
def get_coupled_variables(self, variables):
# Set up
- param = self.param
p_s = variables["X-averaged separator pressure [Pa]"]
for domain in self.options.whole_cell_domains:
if domain == "separator":
@@ -29,22 +28,30 @@ def get_coupled_variables(self, variables):
]
if domain == "negative electrode":
x_n = pybamm.standard_spatial_vars.x_n
- DeltaV_k = param.n.DeltaV
+ DeltaV_k = self.param.n.DeltaV
p_k = (
- -DeltaV_k * a_j_k_av / param.F * (-(x_n**2) + param.n.L**2) / 2
+ -DeltaV_k
+ * a_j_k_av
+ / self.param.F
+ * (-(x_n**2) + self.param.n.L**2)
+ / 2
+ p_s
)
- v_box_k = -DeltaV_k * a_j_k_av / param.F * x_n
+ v_box_k = -DeltaV_k * a_j_k_av / self.param.F * x_n
elif domain == "positive electrode":
x_p = pybamm.standard_spatial_vars.x_p
- DeltaV_k = param.p.DeltaV
+ DeltaV_k = self.param.p.DeltaV
p_k = (
- -DeltaV_k * a_j_k_av / param.F * ((x_p - 1) ** 2 - param.p.L**2) / 2
+ -DeltaV_k
+ * a_j_k_av
+ / self.param.F
+ * ((x_p - 1) ** 2 - self.param.p.L**2)
+ / 2
+ p_s
)
- v_box_k = -DeltaV_k * a_j_k_av / param.F * (x_p - param.L_x)
+ v_box_k = -DeltaV_k * a_j_k_av / self.param.F * (x_p - self.param.L_x)
div_v_box_k = pybamm.PrimaryBroadcast(
- -DeltaV_k * a_j_k_av / param.F, domain
+ -DeltaV_k * a_j_k_av / self.param.F, domain
)
variables.update(
@@ -58,13 +65,13 @@ def get_coupled_variables(self, variables):
"X-averaged separator transverse volume-averaged acceleration [m.s-2]"
]
i_boundary_cc = variables["Current collector current density [A.m-2]"]
- v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F
+ v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F
div_v_box_s_av = -div_Vbox_s
div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator")
# Simple formula for velocity in the separator
x_s = pybamm.standard_spatial_vars.x_s
- v_box_s = div_v_box_s_av * (x_s - param.n.L) + v_box_n_right
+ v_box_s = div_v_box_s_av * (x_s - self.param.n.L) + v_box_n_right
variables.update(
self._get_standard_sep_velocity_variables(v_box_s, div_v_box_s)
diff --git a/src/pybamm/models/submodels/convection/through_cell/full_convection.py b/src/pybamm/models/submodels/convection/through_cell/full_convection.py
index 0fdc089de7..07241bb236 100644
--- a/src/pybamm/models/submodels/convection/through_cell/full_convection.py
+++ b/src/pybamm/models/submodels/convection/through_cell/full_convection.py
@@ -43,8 +43,7 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
# Set up
- param = self.param
- L_n = param.n.L
+ L_n = self.param.n.L
x_s = pybamm.standard_spatial_vars.x_s
# Transverse velocity in the separator determines through-cell velocity
@@ -52,7 +51,7 @@ def get_coupled_variables(self, variables):
"X-averaged separator transverse volume-averaged acceleration [m.s-2]"
]
i_boundary_cc = variables["Current collector current density [A.m-2]"]
- v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F
+ v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F
div_v_box_s_av = -div_Vbox_s
div_v_box_s = pybamm.PrimaryBroadcast(div_v_box_s_av, "separator")
diff --git a/src/pybamm/models/submodels/convection/transverse/full_convection.py b/src/pybamm/models/submodels/convection/transverse/full_convection.py
index 16da47ae47..0a6367fec1 100644
--- a/src/pybamm/models/submodels/convection/transverse/full_convection.py
+++ b/src/pybamm/models/submodels/convection/transverse/full_convection.py
@@ -37,15 +37,13 @@ def get_fundamental_variables(self):
return variables
def set_algebraic(self, variables):
- param = self.param
-
p_s = variables["X-averaged separator pressure [Pa]"]
# Difference in negative and positive electrode velocities determines the
# velocity in the separator
i_boundary_cc = variables["Current collector current density [A.m-2]"]
- v_box_n_right = -param.n.DeltaV * i_boundary_cc / self.param.F
- v_box_p_left = -param.p.DeltaV * i_boundary_cc / self.param.F
- d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L
+ v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F
+ v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F
+ d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L
# Simple formula for velocity in the separator
div_Vbox_s = -d_vbox_s_dx
diff --git a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py
index 15a073c148..a4b05f1ad5 100644
--- a/src/pybamm/models/submodels/convection/transverse/uniform_convection.py
+++ b/src/pybamm/models/submodels/convection/transverse/uniform_convection.py
@@ -26,15 +26,14 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
# Set up
- param = self.param
z = pybamm.standard_spatial_vars.z
# Difference in negative and positive electrode velocities determines the
# velocity in the separator
i_boundary_cc = variables["Current collector current density [A.m-2]"]
- v_box_n_right = -param.n.DeltaV * i_boundary_cc / param.F
- v_box_p_left = -param.p.DeltaV * i_boundary_cc / param.F
- d_vbox_s_dx = (v_box_p_left - v_box_n_right) / param.s.L
+ v_box_n_right = -self.param.n.DeltaV * i_boundary_cc / self.param.F
+ v_box_p_left = -self.param.p.DeltaV * i_boundary_cc / self.param.F
+ d_vbox_s_dx = (v_box_p_left - v_box_n_right) / self.param.s.L
# Simple formula for velocity in the separator
div_Vbox_s = -d_vbox_s_dx
diff --git a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py
index 23001b9d02..808e0a34a3 100644
--- a/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py
+++ b/src/pybamm/models/submodels/current_collector/effective_resistance_current_collector.py
@@ -12,29 +12,28 @@ def default_parameter_values(self):
@property
def default_geometry(self):
geometry = {}
- param = self.param
if self.options["dimensionality"] == 1:
geometry["current collector"] = {
- "z": {"min": 0, "max": param.L_z},
+ "z": {"min": 0, "max": self.param.L_z},
"tabs": {
- "negative": {"z_centre": param.n.centre_z_tab},
- "positive": {"z_centre": param.p.centre_z_tab},
+ "negative": {"z_centre": self.param.n.centre_z_tab},
+ "positive": {"z_centre": self.param.p.centre_z_tab},
},
}
elif self.options["dimensionality"] == 2:
geometry["current collector"] = {
- "y": {"min": 0, "max": param.L_y},
- "z": {"min": 0, "max": param.L_z},
+ "y": {"min": 0, "max": self.param.L_y},
+ "z": {"min": 0, "max": self.param.L_z},
"tabs": {
"negative": {
- "y_centre": param.n.centre_y_tab,
- "z_centre": param.n.centre_z_tab,
- "width": param.n.L_tab,
+ "y_centre": self.param.n.centre_y_tab,
+ "z_centre": self.param.n.centre_z_tab,
+ "width": self.param.n.L_tab,
},
"positive": {
- "y_centre": param.p.centre_y_tab,
- "z_centre": param.p.centre_z_tab,
- "width": param.p.L_tab,
+ "y_centre": self.param.p.centre_y_tab,
+ "z_centre": self.param.p.centre_z_tab,
+ "width": self.param.p.L_tab,
},
},
}
@@ -131,11 +130,10 @@ def __init__(
def get_fundamental_variables(self):
# Get necessary parameters
- param = self.param
- L_cn = param.n.L_cc
- L_cp = param.p.L_cc
- sigma_cn = param.n.sigma_cc
- sigma_cp = param.p.sigma_cc
+ L_cn = self.param.n.L_cc
+ L_cp = self.param.p.L_cc
+ sigma_cn = self.param.n.sigma_cc
+ sigma_cp = self.param.p.sigma_cc
# Set model variables: Note: we solve using a scaled version that is
# better conditioned
@@ -273,13 +271,12 @@ def __init__(self):
self.param = pybamm.LithiumIonParameters()
# Get necessary parameters
- param = self.param
- L_cn = param.n.L_cc
- L_cp = param.p.L_cc
- L_tab_p = param.p.L_tab
+ L_cn = self.param.n.L_cc
+ L_cp = self.param.p.L_cc
+ L_tab_p = self.param.p.L_tab
A_tab_p = L_cp * L_tab_p
- sigma_cn = param.n.sigma_cc
- sigma_cp = param.p.sigma_cc
+ sigma_cn = self.param.n.sigma_cc
+ sigma_cp = self.param.p.sigma_cc
# Set model variables -- we solve a auxilliary problem in each current collector
# then relate this to the potentials and resistances later
@@ -347,11 +344,10 @@ def post_process(self, solution, param_values, V_av, I_av):
processed potentials.
"""
# Get evaluated parameters
- param = self.param
- L_cn = param_values.evaluate(param.n.L_cc)
- L_cp = param_values.evaluate(param.p.L_cc)
- sigma_cn = param_values.evaluate(param.n.sigma_cc)
- sigma_cp = param_values.evaluate(param.p.sigma_cc)
+ L_cn = param_values.evaluate(self.param.n.L_cc)
+ L_cp = param_values.evaluate(self.param.p.L_cc)
+ sigma_cn = param_values.evaluate(self.param.n.sigma_cc)
+ sigma_cp = param_values.evaluate(self.param.p.sigma_cc)
# Process unit solutions
f_n = solution["Unit solution in negative current collector"]
diff --git a/src/pybamm/models/submodels/current_collector/potential_pair.py b/src/pybamm/models/submodels/current_collector/potential_pair.py
index 68a9066da3..c2a197ae64 100644
--- a/src/pybamm/models/submodels/current_collector/potential_pair.py
+++ b/src/pybamm/models/submodels/current_collector/potential_pair.py
@@ -23,7 +23,6 @@ def __init__(self, param):
pybamm.citations.register("Timms2021")
def get_fundamental_variables(self):
- param = self.param
phi_s_cn = pybamm.Variable(
"Negative current collector potential [V]", domain="current collector"
)
@@ -35,7 +34,7 @@ def get_fundamental_variables(self):
i_boundary_cc = pybamm.Variable(
"Current collector current density [A.m-2]",
domain="current collector",
- scale=param.Q / (param.A_cc * param.n_electrodes_parallel),
+ scale=self.param.Q / (self.param.A_cc * self.param.n_electrodes_parallel),
)
variables.update(self._get_standard_current_variables(i_cc, i_boundary_cc))
@@ -43,16 +42,15 @@ def get_fundamental_variables(self):
return variables
def set_algebraic(self, variables):
- param = self.param
-
phi_s_cn = variables["Negative current collector potential [V]"]
phi_s_cp = variables["Positive current collector potential [V]"]
i_boundary_cc = variables["Current collector current density [A.m-2]"]
self.algebraic = {
- phi_s_cn: (param.n.sigma_cc * param.n.L_cc) * pybamm.laplacian(phi_s_cn)
+ phi_s_cn: (self.param.n.sigma_cc * self.param.n.L_cc)
+ * pybamm.laplacian(phi_s_cn)
- pybamm.source(i_boundary_cc, phi_s_cn),
- i_boundary_cc: (param.p.sigma_cc * param.p.L_cc)
+ i_boundary_cc: (self.param.p.sigma_cc * self.param.p.L_cc)
* pybamm.laplacian(phi_s_cp)
+ pybamm.source(i_boundary_cc, phi_s_cp),
}
@@ -77,15 +75,14 @@ def set_boundary_conditions(self, variables):
phi_s_cn = variables["Negative current collector potential [V]"]
phi_s_cp = variables["Positive current collector potential [V]"]
- param = self.param
applied_current_density = variables["Total current density [A.m-2]"]
- total_current = applied_current_density * param.A_cc
+ total_current = applied_current_density * self.param.A_cc
# In the 1+1D model, the behaviour is averaged over the y-direction, so the
# effective tab area is the cell width multiplied by the current collector
# thickness
- positive_tab_area = param.L_y * param.p.L_cc
- pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area)
+ positive_tab_area = self.param.L_y * self.param.p.L_cc
+ pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area)
# Boundary condition needs to be on the variables that go into the Laplacian,
# even though phi_s_cp isn't a pybamm.Variable object
@@ -111,20 +108,19 @@ def set_boundary_conditions(self, variables):
phi_s_cn = variables["Negative current collector potential [V]"]
phi_s_cp = variables["Positive current collector potential [V]"]
- param = self.param
applied_current_density = variables["Total current density [A.m-2]"]
- total_current = applied_current_density * param.A_cc
+ total_current = applied_current_density * self.param.A_cc
# Note: we divide by the *numerical* tab area so that the correct total
# current is applied. That is, numerically integrating the current density
# around the boundary gives the applied current exactly.
positive_tab_area = pybamm.BoundaryIntegral(
- pybamm.PrimaryBroadcast(param.p.L_cc, "current collector"),
+ pybamm.PrimaryBroadcast(self.param.p.L_cc, "current collector"),
region="positive tab",
)
# cc_area appears here due to choice of non-dimensionalisation
- pos_tab_bc = -total_current / (param.p.sigma_cc * positive_tab_area)
+ pos_tab_bc = -total_current / (self.param.p.sigma_cc * positive_tab_area)
# Boundary condition needs to be on the variables that go into the Laplacian,
# even though phi_s_cp isn't a pybamm.Variable object
diff --git a/src/pybamm/models/submodels/electrode/base_electrode.py b/src/pybamm/models/submodels/electrode/base_electrode.py
index 4248131a75..2b37ceb0d3 100644
--- a/src/pybamm/models/submodels/electrode/base_electrode.py
+++ b/src/pybamm/models/submodels/electrode/base_electrode.py
@@ -119,7 +119,7 @@ def _get_standard_current_collector_potential_variables(
V_cc = phi_s_cp - phi_s_cn
# Voltage
- # Note phi_s_cn is always zero at the negative tab
+ # Note phi_s_cn is always zero at the negative tab by definition
V = pybamm.boundary_value(phi_s_cp, "positive tab")
# Voltage is local current collector potential difference at the tabs, in 1D
@@ -128,10 +128,12 @@ def _get_standard_current_collector_potential_variables(
"Negative current collector potential [V]": phi_s_cn,
"Positive current collector potential [V]": phi_s_cp,
"Local voltage [V]": V_cc,
+ "Voltage expression [V]": V - delta_phi_contact,
"Terminal voltage [V]": V - delta_phi_contact,
- "Voltage [V]": V - delta_phi_contact,
"Contact overpotential [V]": delta_phi_contact,
}
+ if self.options["voltage as a state"] == "false":
+ variables.update({"Voltage [V]": V - delta_phi_contact})
return variables
@@ -170,9 +172,8 @@ def _get_standard_whole_cell_variables(self, variables):
phi_s_p = variables["Positive electrode potential [V]"]
phi_s_cp = pybamm.boundary_value(phi_s_p, "right")
if self.options["contact resistance"] == "true":
- param = self.param
I = variables["Current [A]"]
- delta_phi_contact = I * param.R_contact
+ delta_phi_contact = I * self.param.R_contact
else:
delta_phi_contact = pybamm.Scalar(0)
variables.update(
diff --git a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py
index 4845ea9fb2..7c4d8d62b8 100644
--- a/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py
+++ b/src/pybamm/models/submodels/electrode/ohm/composite_ohm.py
@@ -26,14 +26,13 @@ def __init__(self, param, domain, options=None):
def get_coupled_variables(self, variables):
domain = self.domain
- param = self.param
i_boundary_cc = variables["Current collector current density [A.m-2]"]
# import parameters and spatial variables
- L_n = param.n.L
- L_p = param.p.L
- L_x = param.L_x
+ L_n = self.param.n.L
+ L_p = self.param.p.L
+ L_x = self.param.L_x
x_n = pybamm.standard_spatial_vars.x_n
x_p = pybamm.standard_spatial_vars.x_p
diff --git a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py
index 7e414f94c9..8385a31fc1 100644
--- a/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py
+++ b/src/pybamm/models/submodels/electrode/ohm/leading_ohm.py
@@ -35,15 +35,14 @@ def get_coupled_variables(self, variables):
"""
Returns variables which are derived from the fundamental variables in the model.
"""
- param = self.param
i_boundary_cc = variables["Current collector current density [A.m-2]"]
phi_s_cn = variables["Negative current collector potential [V]"]
# import parameters and spatial variables
- L_n = param.n.L
- L_p = param.p.L
- L_x = param.L_x
+ L_n = self.param.n.L
+ L_p = self.param.p.L
+ L_x = self.param.L_x
x_n = pybamm.standard_spatial_vars.x_n
x_p = pybamm.standard_spatial_vars.x_p
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py
index d1178c8cc2..5a7d3163c2 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/base_electrolyte_conductivity.py
@@ -217,7 +217,6 @@ def _get_electrolyte_overpotentials(self, variables):
The variables including the whole-cell electrolyte potentials
and currents.
"""
- param = self.param
if self.options.electrode_types["negative"] == "planar":
# No concentration overpotential in the counter electrode
@@ -229,7 +228,7 @@ def _get_electrolyte_overpotentials(self, variables):
c_e_n = variables["Negative electrolyte concentration [mol.m-3]"]
T_n = variables["Negative electrode temperature [K]"]
indef_integral_n = pybamm.IndefiniteIntegral(
- param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n),
+ self.param.chiRT_over_Fc(c_e_n, T_n) * pybamm.grad(c_e_n),
pybamm.standard_spatial_vars.x_n,
)
@@ -243,11 +242,11 @@ def _get_electrolyte_overpotentials(self, variables):
# concentration overpotential
indef_integral_s = pybamm.IndefiniteIntegral(
- param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s),
+ self.param.chiRT_over_Fc(c_e_s, T_s) * pybamm.grad(c_e_s),
pybamm.standard_spatial_vars.x_s,
)
indef_integral_p = pybamm.IndefiniteIntegral(
- param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p),
+ self.param.chiRT_over_Fc(c_e_p, T_p) * pybamm.grad(c_e_p),
pybamm.standard_spatial_vars.x_p,
)
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py
index 475d1a4232..d6c7ea6473 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/composite_conductivity.py
@@ -52,23 +52,22 @@ def get_coupled_variables(self, variables):
T_av_s = pybamm.PrimaryBroadcast(T_av, "separator")
T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode")
- param = self.param
- RT_F_av = param.R * T_av / param.F
- RT_F_av_s = param.R * T_av_s / param.F
- RT_F_av_p = param.R * T_av_p / param.F
-
- L_n = param.n.L
- L_s = param.s.L
- L_p = param.p.L
- L_x = param.L_x
+ RT_F_av = self.param.R * T_av / self.param.F
+ RT_F_av_s = self.param.R * T_av_s / self.param.F
+ RT_F_av_p = self.param.R * T_av_p / self.param.F
+
+ L_n = self.param.n.L
+ L_s = self.param.s.L
+ L_p = self.param.p.L
+ L_x = self.param.L_x
x_s = pybamm.standard_spatial_vars.x_s
x_p = pybamm.standard_spatial_vars.x_p
# bulk conductivities
- kappa_s_av = param.kappa_e(c_e_av, T_av) * tor_s_av
- kappa_p_av = param.kappa_e(c_e_av, T_av) * tor_p_av
+ kappa_s_av = self.param.kappa_e(c_e_av, T_av) * tor_s_av
+ kappa_p_av = self.param.kappa_e(c_e_av, T_av) * tor_p_av
- chi_av = param.chi(c_e_av, T_av)
+ chi_av = self.param.chi(c_e_av, T_av)
chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator")
chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode")
@@ -79,8 +78,8 @@ def get_coupled_variables(self, variables):
x_n = pybamm.standard_spatial_vars.x_n
chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode")
T_av_n = pybamm.PrimaryBroadcast(T_av, "negative electrode")
- RT_F_av_n = param.R * T_av_n / param.F
- kappa_n_av = param.kappa_e(c_e_av, T_av) * tor_n_av
+ RT_F_av_n = self.param.R * T_av_n / self.param.F
+ kappa_n_av = self.param.kappa_e(c_e_av, T_av) * tor_n_av
i_e_n = i_boundary_cc * x_n / L_n
i_e_s = pybamm.PrimaryBroadcast(i_boundary_cc, "separator")
i_e_p = i_boundary_cc * (L_x - x_p) / L_p
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py
index 5acb7d2434..a688209441 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/full_conductivity.py
@@ -46,14 +46,13 @@ def get_fundamental_variables(self):
return variables
def get_coupled_variables(self, variables):
- param = self.param
T = variables["Cell temperature [K]"]
tor = variables["Electrolyte transport efficiency"]
c_e = variables["Electrolyte concentration [mol.m-3]"]
phi_e = variables["Electrolyte potential [V]"]
- i_e = (param.kappa_e(c_e, T) * tor) * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
+ i_e = (self.param.kappa_e(c_e, T) * tor) * (
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e) - pybamm.grad(phi_e)
)
# Override print_name
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py
index cb9979c6bb..2250d99f6d 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/integrated_conductivity.py
@@ -32,7 +32,6 @@ def _higher_order_macinnes_function(self, x):
return pybamm.log(x)
def get_coupled_variables(self, variables):
- param = self.param
c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"]
i_boundary_cc = variables["Current collector current density [A.m-2]"]
@@ -55,22 +54,21 @@ def get_coupled_variables(self, variables):
T_av_s = pybamm.PrimaryBroadcast(T_av, "separator")
T_av_p = pybamm.PrimaryBroadcast(T_av, "positive electrode")
- RT_F_av = param.R * T_av / param.F
- RT_F_av_n = param.R * T_av_n / param.F
- RT_F_av_s = param.R * T_av_s / param.F
- RT_F_av_p = param.R * T_av_p / param.F
+ RT_F_av = self.param.R * T_av / self.param.F
+ RT_F_av_n = self.param.R * T_av_n / self.param.F
+ RT_F_av_s = self.param.R * T_av_s / self.param.F
+ RT_F_av_p = self.param.R * T_av_p / self.param.F
- param = self.param
- L_n = param.n.L
- L_p = param.p.L
- L_x = param.L_x
+ L_n = self.param.n.L
+ L_p = self.param.p.L
+ L_x = self.param.L_x
x_n = pybamm.standard_spatial_vars.x_n
x_s = pybamm.standard_spatial_vars.x_s
x_p = pybamm.standard_spatial_vars.x_p
x_n_edge = pybamm.standard_spatial_vars.x_n_edge
x_p_edge = pybamm.standard_spatial_vars.x_p_edge
- chi_av = param.chi(c_e_av, T_av)
+ chi_av = self.param.chi(c_e_av, T_av)
chi_av_n = pybamm.PrimaryBroadcast(chi_av, "negative electrode")
chi_av_s = pybamm.PrimaryBroadcast(chi_av, "separator")
chi_av_p = pybamm.PrimaryBroadcast(chi_av, "positive electrode")
@@ -87,13 +85,13 @@ def get_coupled_variables(self, variables):
# electrolyte potential
indef_integral_n = pybamm.IndefiniteIntegral(
- i_e_n_edge / (param.kappa_e(c_e_n, T_av_n) * tor_n), x_n
+ i_e_n_edge / (self.param.kappa_e(c_e_n, T_av_n) * tor_n), x_n
)
indef_integral_s = pybamm.IndefiniteIntegral(
- i_e_s_edge / (param.kappa_e(c_e_s, T_av_s) * tor_s), x_s
+ i_e_s_edge / (self.param.kappa_e(c_e_s, T_av_s) * tor_s), x_s
)
indef_integral_p = pybamm.IndefiniteIntegral(
- i_e_p_edge / (param.kappa_e(c_e_p, T_av_p) * tor_p), x_p
+ i_e_p_edge / (self.param.kappa_e(c_e_p, T_av_p) * tor_p), x_p
)
integral_n = indef_integral_n
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py
index ad2a5b6486..42c7770c54 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/leading_order_conductivity.py
@@ -36,10 +36,9 @@ def get_coupled_variables(self, variables):
i_boundary_cc = variables["Current collector current density [A.m-2]"]
- param = self.param
- L_n = param.n.L
- L_p = param.p.L
- L_x = param.L_x
+ L_n = self.param.n.L
+ L_p = self.param.p.L
+ L_x = self.param.L_x
x_n = pybamm.standard_spatial_vars.x_n
x_p = pybamm.standard_spatial_vars.x_p
diff --git a/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py
index cceb88f83e..fd32e6a83c 100644
--- a/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py
+++ b/src/pybamm/models/submodels/electrolyte_conductivity/surface_potential_form/full_surface_form_conductivity.py
@@ -47,7 +47,6 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
Domain = self.domain.capitalize()
- param = self.param
if self.domain in ["negative", "positive"]:
conductivity, sigma_eff = self._get_conductivities(variables)
@@ -59,7 +58,7 @@ def get_coupled_variables(self, variables):
T = variables[f"{Domain} electrode temperature [K]"]
i_e = conductivity * (
- param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e)
+ self.param.chiRT_over_Fc(c_e, T) * pybamm.grad(c_e)
+ pybamm.grad(delta_phi)
+ i_boundary_cc / sigma_eff
)
@@ -83,8 +82,8 @@ def get_coupled_variables(self, variables):
tor_s = variables["Separator electrolyte transport efficiency"]
T = variables["Separator temperature [K]"]
- chiRT_over_Fc_e_s = param.chiRT_over_Fc(c_e_s, T)
- kappa_s_eff = param.kappa_e(c_e_s, T) * tor_s
+ chiRT_over_Fc_e_s = self.param.chiRT_over_Fc(c_e_s, T)
+ kappa_s_eff = self.param.kappa_e(c_e_s, T) * tor_s
phi_e = phi_e_n_s + pybamm.IndefiniteIntegral(
chiRT_over_Fc_e_s * pybamm.grad(c_e_s) - i_boundary_cc / kappa_s_eff,
@@ -124,7 +123,8 @@ def get_coupled_variables(self, variables):
grad_left = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left")
grad_right = (
(i_boundary_cc / pybamm.boundary_value(conductivity, "right"))
- - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "right") * grad_c_e
+ - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "right")
+ * grad_c_e
- i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right")
)
@@ -132,7 +132,8 @@ def get_coupled_variables(self, variables):
grad_c_e = pybamm.boundary_gradient(c_e, "left")
grad_left = (
(i_boundary_cc / pybamm.boundary_value(conductivity, "left"))
- - pybamm.boundary_value(param.chiRT_over_Fc(c_e, T), "left") * grad_c_e
+ - pybamm.boundary_value(self.param.chiRT_over_Fc(c_e, T), "left")
+ * grad_c_e
- i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "left")
)
grad_right = -i_boundary_cc * pybamm.boundary_value(1 / sigma_eff, "right")
@@ -150,14 +151,13 @@ def get_coupled_variables(self, variables):
def _get_conductivities(self, variables):
Domain = self.domain.capitalize()
- param = self.param
tor_e = variables[f"{Domain} electrolyte transport efficiency"]
tor_s = variables[f"{Domain} electrode transport efficiency"]
c_e = variables[f"{Domain} electrolyte concentration [mol.m-3]"]
T = variables[f"{Domain} electrode temperature [K]"]
sigma = self.domain_param.sigma(T)
- kappa_eff = param.kappa_e(c_e, T) * tor_e
+ kappa_eff = self.param.kappa_e(c_e, T) * tor_e
sigma_eff = sigma * tor_s
conductivity = kappa_eff / (1 + kappa_eff / sigma_eff)
diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py b/src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py
index eee441446f..006619e8bb 100644
--- a/src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py
+++ b/src/pybamm/models/submodels/electrolyte_diffusion/constant_concentration.py
@@ -76,6 +76,6 @@ def set_boundary_conditions(self, variables):
}
}
- def set_events(self, variables):
+ def add_events_from(self, variables):
# No event since the concentration is constant
pass
diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py
index 2fdd937966..06f95bc2f1 100644
--- a/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py
+++ b/src/pybamm/models/submodels/electrolyte_diffusion/full_diffusion.py
@@ -67,10 +67,8 @@ def get_coupled_variables(self, variables):
v_box = variables["Volume-averaged velocity [m.s-1]"]
T = variables["Cell temperature [K]"]
- param = self.param
-
- N_e_diffusion = -tor * param.D_e(c_e, T) * pybamm.grad(c_e)
- N_e_migration = param.t_plus(c_e, T) * i_e / param.F
+ N_e_diffusion = -tor * self.param.D_e(c_e, T) * pybamm.grad(c_e)
+ N_e_migration = self.param.t_plus(c_e, T) * i_e / self.param.F
N_e_convection = c_e * v_box
N_e = N_e_diffusion + N_e_migration + N_e_convection
@@ -106,7 +104,6 @@ def set_initial_conditions(self, variables):
}
def set_boundary_conditions(self, variables):
- param = self.param
c_e = variables["Electrolyte concentration [mol.m-3]"]
c_e_conc = variables["Electrolyte concentration concatenation [mol.m-3]"]
T = variables["Cell temperature [K]"]
@@ -118,7 +115,8 @@ def flux_bc(side):
# assuming v_box = 0 for now
return (
pybamm.boundary_value(
- -(1 - param.t_plus(c_e, T)) / (tor * param.D_e(c_e, T) * param.F),
+ -(1 - self.param.t_plus(c_e, T))
+ / (tor * self.param.D_e(c_e, T) * self.param.F),
side,
)
* i_boundary_cc
diff --git a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py
index 8dedc28cf5..104b12e34e 100644
--- a/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py
+++ b/src/pybamm/models/submodels/electrolyte_diffusion/leading_order_diffusion.py
@@ -52,8 +52,6 @@ def get_coupled_variables(self, variables):
return variables
def set_rhs(self, variables):
- param = self.param
-
c_e_av = variables["X-averaged electrolyte concentration [mol.m-3]"]
T_av = variables["X-averaged cell temperature [K]"]
@@ -86,17 +84,24 @@ def set_rhs(self, variables):
"reaction source terms [A.m-3]"
]
source_terms = (
- param.n.L * (sum_s_j_n_0 - param.t_plus(c_e_av, T_av) * sum_a_j_n_0)
- + param.p.L * (sum_s_j_p_0 - param.t_plus(c_e_av, T_av) * sum_a_j_p_0)
- ) / param.F
+ self.param.n.L
+ * (sum_s_j_n_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_n_0)
+ + self.param.p.L
+ * (sum_s_j_p_0 - self.param.t_plus(c_e_av, T_av) * sum_a_j_p_0)
+ ) / self.param.F
self.rhs = {
c_e_av: 1
- / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av)
+ / (
+ self.param.n.L * eps_n_av
+ + self.param.s.L * eps_s_av
+ + self.param.p.L * eps_p_av
+ )
* (
source_terms
- - c_e_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av)
- - c_e_av * param.s.L * div_Vbox_s_av
+ - c_e_av
+ * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av)
+ - c_e_av * self.param.s.L * div_Vbox_s_av
)
}
diff --git a/src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py
index c7f1b4bcd5..bf9a183875 100644
--- a/src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py
+++ b/src/pybamm/models/submodels/equivalent_circuit_elements/diffusion_element.py
@@ -102,7 +102,7 @@ def set_initial_conditions(self, variables):
z = variables["Distributed SoC"]
self.initial_conditions = {z: self.param.initial_soc}
- def set_events(self, variables):
+ def add_events_from(self, variables):
z_surf = variables["Surface SoC"]
self.events += [
pybamm.Event("Minimum surface SoC", z_surf),
diff --git a/src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py b/src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py
index 9d1adf3d57..901b63cf74 100644
--- a/src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py
+++ b/src/pybamm/models/submodels/equivalent_circuit_elements/ocv_element.py
@@ -54,7 +54,7 @@ def set_initial_conditions(self, variables):
soc = variables["SoC"]
self.initial_conditions = {soc: self.param.initial_soc}
- def set_events(self, variables):
+ def add_events_from(self, variables):
soc = variables["SoC"]
self.events += [
pybamm.Event("Minimum SoC", soc),
diff --git a/src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py b/src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py
index 380902fca5..89f8904f32 100644
--- a/src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py
+++ b/src/pybamm/models/submodels/equivalent_circuit_elements/voltage_model.py
@@ -54,7 +54,7 @@ def x_not_zero(x):
return variables
- def set_events(self, variables):
+ def add_events_from(self, variables):
voltage = variables["Voltage [V]"]
# Add voltage events
diff --git a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py
index 760e9e2b20..6dcd9a4541 100644
--- a/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py
+++ b/src/pybamm/models/submodels/external_circuit/explicit_control_external_circuit.py
@@ -19,28 +19,40 @@ def get_fundamental_variables(self):
"Current [A]": I,
"C-rate": I / self.param.Q,
}
+ if self.options.get("voltage as a state") == "true":
+ V = pybamm.Variable("Voltage [V]")
+ variables.update({"Voltage [V]": V})
return variables
+ def set_initial_conditions(self, variables):
+ if self.options.get("voltage as a state") == "true":
+ V = variables["Voltage [V]"]
+ self.initial_conditions[V] = self.param.ocv_init
+
+ def set_algebraic(self, variables):
+ if self.options.get("voltage as a state") == "true":
+ V = variables["Voltage [V]"]
+ V_expression = variables["Voltage expression [V]"]
+ self.algebraic[V] = V - V_expression
+
class ExplicitPowerControl(BaseModel):
"""External circuit with current set explicitly to hit target power."""
def get_coupled_variables(self, variables):
- param = self.param
-
# Current is given as applied power divided by voltage
V = variables["Voltage [V]"]
P = pybamm.FunctionParameter("Power function [W]", {"Time [s]": pybamm.t})
I = P / V
# Update derived variables
- i_cell = I / (param.n_electrodes_parallel * param.A_cc)
+ i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc)
variables = {
"Total current density [A.m-2]": i_cell,
"Current [A]": I,
- "C-rate": I / param.Q,
+ "C-rate": I / self.param.Q,
}
return variables
@@ -50,8 +62,6 @@ class ExplicitResistanceControl(BaseModel):
"""External circuit with current set explicitly to hit target resistance."""
def get_coupled_variables(self, variables):
- param = self.param
-
# Current is given as applied voltage divided by resistance
V = variables["Voltage [V]"]
R = pybamm.FunctionParameter(
@@ -60,12 +70,12 @@ def get_coupled_variables(self, variables):
I = V / R
# Update derived variables
- i_cell = I / (param.n_electrodes_parallel * param.A_cc)
+ i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc)
variables = {
"Total current density [A.m-2]": i_cell,
"Current [A]": I,
- "C-rate": I / param.Q,
+ "C-rate": I / self.param.Q,
}
return variables
diff --git a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py
index 60d6fb0e40..274d35954a 100644
--- a/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py
+++ b/src/pybamm/models/submodels/external_circuit/function_control_external_circuit.py
@@ -29,9 +29,8 @@ def __init__(self, param, external_circuit_function, options, control="algebraic
self.control = control
def get_fundamental_variables(self):
- param = self.param
# Current is a variable
- i_var = pybamm.Variable("Current variable [A]", scale=param.Q)
+ i_var = pybamm.Variable("Current variable [A]", scale=self.param.Q)
if self.control in ["algebraic", "differential"]:
I = i_var
elif self.control == "differential with max":
@@ -41,14 +40,17 @@ def get_fundamental_variables(self):
I = pybamm.maximum(i_var, i_input)
# Update derived variables
- i_cell = I / (param.n_electrodes_parallel * param.A_cc)
+ i_cell = I / (self.param.n_electrodes_parallel * self.param.A_cc)
variables = {
"Current variable [A]": i_var,
"Total current density [A.m-2]": i_cell,
"Current [A]": I,
- "C-rate": I / param.Q,
+ "C-rate": I / self.param.Q,
}
+ if self.options.get("voltage as a state") == "true":
+ V = pybamm.Variable("Voltage [V]")
+ variables.update({"Voltage [V]": V})
return variables
@@ -56,6 +58,9 @@ def set_initial_conditions(self, variables):
# Initial condition as a guess for consistent initial conditions
i_cell = variables["Current variable [A]"]
self.initial_conditions[i_cell] = self.param.Q
+ if self.options.get("voltage as a state") == "true":
+ V = variables["Voltage [V]"]
+ self.initial_conditions[V] = self.param.ocv_init
def set_rhs(self, variables):
# External circuit submodels are always equations on the current
@@ -72,6 +77,10 @@ def set_algebraic(self, variables):
if self.control == "algebraic":
i_cell = variables["Current variable [A]"]
self.algebraic[i_cell] = self.external_circuit_function(variables)
+ if self.options.get("voltage as a state") == "true":
+ V = variables["Voltage [V]"]
+ V_expression = variables["Voltage expression [V]"]
+ self.algebraic[V] = V - V_expression
class VoltageFunctionControl(FunctionControl):
diff --git a/src/pybamm/models/submodels/interface/base_interface.py b/src/pybamm/models/submodels/interface/base_interface.py
index ab9b80eae0..0ad08d5454 100644
--- a/src/pybamm/models/submodels/interface/base_interface.py
+++ b/src/pybamm/models/submodels/interface/base_interface.py
@@ -61,7 +61,6 @@ def _get_exchange_current_density(self, variables):
j0 : :class: `pybamm.Symbol`
The exchange current density.
"""
- param = self.param
phase_param = self.phase_param
domain, Domain = self.domain_Domain
phase_name = self.phase_name
@@ -132,8 +131,8 @@ def _get_exchange_current_density(self, variables):
elif self.reaction == "lithium metal plating":
# compute T on the surface of the anode (interface with separator)
T = pybamm.boundary_value(T, "right")
- c_Li_metal = 1 / param.V_bar_Li
- j0 = param.j0_Li_metal(c_e, c_Li_metal, T)
+ c_Li_metal = 1 / self.param.V_bar_Li
+ j0 = self.param.j0_Li_metal(c_e, c_Li_metal, T)
elif self.reaction == "lead-acid main":
# If variable was broadcast, take only the orphan
@@ -150,7 +149,7 @@ def _get_exchange_current_density(self, variables):
if self.domain == "negative":
j0 = pybamm.Scalar(0)
elif self.domain == "positive":
- j0 = param.p.prim.j0_Ox(c_e, T)
+ j0 = self.param.p.prim.j0_Ox(c_e, T)
return j0
diff --git a/src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py b/src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py
index bbd9af4fb6..e785204882 100644
--- a/src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py
+++ b/src/pybamm/models/submodels/interface/interface_utilisation/current_driven_utilisation.py
@@ -89,7 +89,7 @@ def set_initial_conditions(self, variables):
self.initial_conditions = {u: u_init}
- def set_events(self, variables):
+ def add_events_from(self, variables):
domain, Domain = self.domain_Domain
if self.reaction_loc == "full electrode":
diff --git a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py
index 08c2db2175..19b8dbea97 100644
--- a/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py
+++ b/src/pybamm/models/submodels/interface/kinetics/diffusion_limited.py
@@ -68,7 +68,6 @@ def get_coupled_variables(self, variables):
return variables
def _get_diffusion_limited_current_density(self, variables):
- param = self.param
if self.domain == "negative":
if self.order == "leading":
j_p = variables[
@@ -81,10 +80,10 @@ def _get_diffusion_limited_current_density(self, variables):
c_ox_s = variables["Separator oxygen concentration [mol.m-3]"]
N_ox_neg_sep_interface = (
-pybamm.boundary_value(tor_s, "left")
- * param.D_ox
+ * self.param.D_ox
* pybamm.boundary_gradient(c_ox_s, "left")
)
- j = -N_ox_neg_sep_interface / -param.s_ox_Ox / param.n.L
+ j = -N_ox_neg_sep_interface / -self.param.s_ox_Ox / self.param.n.L
return j
diff --git a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py
index 959cb027c1..b49993afd8 100644
--- a/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py
+++ b/src/pybamm/models/submodels/interface/kinetics/inverse_kinetics/inverse_butler_volmer.py
@@ -93,8 +93,9 @@ def get_coupled_variables(self, variables):
return variables
def _get_overpotential(self, j, j0, ne, T, u):
- param = self.param
- return (2 * (param.R * T) / param.F / ne) * pybamm.arcsinh(j / (2 * j0 * u))
+ return (2 * (self.param.R * T) / self.param.F / ne) * pybamm.arcsinh(
+ j / (2 * j0 * u)
+ )
class CurrentForInverseButlerVolmer(BaseInterface):
diff --git a/src/pybamm/models/submodels/interface/sei/sei_growth.py b/src/pybamm/models/submodels/interface/sei/sei_growth.py
index bed4b04952..2f506323ce 100644
--- a/src/pybamm/models/submodels/interface/sei/sei_growth.py
+++ b/src/pybamm/models/submodels/interface/sei/sei_growth.py
@@ -80,7 +80,6 @@ def get_fundamental_variables(self):
return variables
def get_coupled_variables(self, variables):
- param = self.param
phase_param = self.phase_param
domain, Domain = self.domain_Domain
SEI_option = getattr(getattr(self.options, domain), self.phase)["SEI"]
@@ -118,7 +117,7 @@ def get_coupled_variables(self, variables):
R_sei = phase_param.R_sei
eta_SEI = delta_phi - phase_param.U_sei - j * L_sei * R_sei
# Thermal prefactor for reaction, interstitial and EC models
- F_RT = param.F / (param.R * T)
+ F_RT = self.param.F / (self.param.R * T)
# Define alpha_SEI depending on whether it is symmetric or asymmetric. This
# applies to "reaction limited" and "EC reaction limited"
@@ -138,12 +137,12 @@ def get_coupled_variables(self, variables):
elif SEI_option == "interstitial-diffusion limited":
# Scott Marquis thesis (eq. 5.96)
j_sei = -(
- phase_param.D_li * phase_param.c_li_0 * param.F / L_sei_outer
+ phase_param.D_li * phase_param.c_li_0 * self.param.F / L_sei_outer
) * pybamm.exp(-F_RT * delta_phi)
elif SEI_option == "solvent-diffusion limited":
# Scott Marquis thesis (eq. 5.91)
- j_sei = -phase_param.D_sol * phase_param.c_sol * param.F / L_sei_outer
+ j_sei = -phase_param.D_sol * phase_param.c_sol * self.param.F / L_sei_outer
elif SEI_option.startswith("ec reaction limited"):
# we have a linear system for j and c
@@ -159,7 +158,7 @@ def get_coupled_variables(self, variables):
k_exp = phase_param.k_sei * pybamm.exp(-alpha_SEI * F_RT * eta_SEI)
L_over_D = L_sei / phase_param.D_ec
c_0 = phase_param.c_ec_0
- j_sei = -param.F * c_0 * k_exp / (1 + L_over_D * k_exp)
+ j_sei = -self.param.F * c_0 * k_exp / (1 + L_over_D * k_exp)
c_ec = c_0 / (1 + L_over_D * k_exp)
# Get variables related to the concentration
@@ -177,7 +176,9 @@ def get_coupled_variables(self, variables):
inner_sei_proportion = phase_param.inner_sei_proportion
# All SEI growth mechanisms assumed to have Arrhenius dependence
- Arrhenius = pybamm.exp(phase_param.E_sei / param.R * (1 / param.T_ref - 1 / T))
+ Arrhenius = pybamm.exp(
+ phase_param.E_sei / self.param.R * (1 / self.param.T_ref - 1 / T)
+ )
j_inner = inner_sei_proportion * Arrhenius * j_sei
j_outer = (1 - inner_sei_proportion) * Arrhenius * j_sei
@@ -192,7 +193,6 @@ def get_coupled_variables(self, variables):
def set_rhs(self, variables):
phase_param = self.phase_param
- param = self.param
domain, Domain = self.domain_Domain
if self.reaction_loc == "x-average":
@@ -249,10 +249,10 @@ def set_rhs(self, variables):
# V_bar / a converts from SEI moles to SEI thickness
# V_bar * j_sei / (F * z_sei) is the rate of SEI thickness change
dLdt_SEI_inner = (
- phase_param.V_bar_inner * j_inner / (param.F * phase_param.z_sei)
+ phase_param.V_bar_inner * j_inner / (self.param.F * phase_param.z_sei)
)
dLdt_SEI_outer = (
- phase_param.V_bar_outer * j_outer / (param.F * phase_param.z_sei)
+ phase_param.V_bar_outer * j_outer / (self.param.F * phase_param.z_sei)
)
# we have to add the spreading rate to account for cracking
diff --git a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py
index c69312e342..7ecad6fa4c 100644
--- a/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py
+++ b/src/pybamm/models/submodels/oxygen_diffusion/full_oxygen_diffusion.py
@@ -58,9 +58,7 @@ def get_coupled_variables(self, variables):
# TODO: allow charge and convection?
v_box = pybamm.Scalar(0)
- param = self.param
-
- N_ox_diffusion = -tor * param.D_ox * pybamm.grad(c_ox)
+ N_ox_diffusion = -tor * self.param.D_ox * pybamm.grad(c_ox)
N_ox = N_ox_diffusion + c_ox * v_box
# Flux in the negative electrode is zero
@@ -73,8 +71,6 @@ def get_coupled_variables(self, variables):
return variables
def set_rhs(self, variables):
- param = self.param
-
eps_s = variables["Separator porosity"]
eps_p = variables["Positive electrode porosity"]
eps = pybamm.concatenation(eps_s, eps_p)
@@ -93,12 +89,12 @@ def set_rhs(self, variables):
]
source_terms = pybamm.concatenation(
pybamm.FullBroadcast(0, "separator", "current collector"),
- param.s_ox_Ox * a_j_ox,
+ self.param.s_ox_Ox * a_j_ox,
)
self.rhs = {
c_ox: (1 / eps)
- * (-pybamm.div(N_ox) + source_terms / param.F - c_ox * deps_dt)
+ * (-pybamm.div(N_ox) + source_terms / self.param.F - c_ox * deps_dt)
}
def set_boundary_conditions(self, variables):
diff --git a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py
index 056c7f6715..bdc064c340 100644
--- a/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py
+++ b/src/pybamm/models/submodels/oxygen_diffusion/leading_oxygen_diffusion.py
@@ -41,8 +41,6 @@ def get_coupled_variables(self, variables):
return variables
def set_rhs(self, variables):
- param = self.param
-
c_ox_av = variables["X-averaged oxygen concentration [mol.m-3]"]
eps_n_av = variables["X-averaged negative electrode porosity"]
@@ -62,16 +60,21 @@ def set_rhs(self, variables):
]
source_terms = (
- param.n.L * param.s_ox_Ox * a_j_ox_n_av
- + param.p.L * param.s_ox_Ox * a_j_ox_p_av
+ self.param.n.L * self.param.s_ox_Ox * a_j_ox_n_av
+ + self.param.p.L * self.param.s_ox_Ox * a_j_ox_p_av
)
self.rhs = {
c_ox_av: 1
- / (param.n.L * eps_n_av + param.s.L * eps_s_av + param.p.L * eps_p_av)
+ / (
+ self.param.n.L * eps_n_av
+ + self.param.s.L * eps_s_av
+ + self.param.p.L * eps_p_av
+ )
* (
- source_terms / param.F
- - c_ox_av * (param.n.L * deps_n_dt_av + param.p.L * deps_p_dt_av)
+ source_terms / self.param.F
+ - c_ox_av
+ * (self.param.n.L * deps_n_dt_av + self.param.p.L * deps_p_dt_av)
)
}
diff --git a/src/pybamm/models/submodels/particle/base_particle.py b/src/pybamm/models/submodels/particle/base_particle.py
index dab48b5f79..b774e58a0c 100644
--- a/src/pybamm/models/submodels/particle/base_particle.py
+++ b/src/pybamm/models/submodels/particle/base_particle.py
@@ -28,7 +28,6 @@ def __init__(self, param, domain, options, phase="primary"):
self.size_distribution = domain_options["particle size"] == "distribution"
def _get_effective_diffusivity(self, c, T, current):
- param = self.param
domain, Domain = self.domain_Domain
domain_param = self.domain_param
phase_param = self.phase_param
@@ -57,10 +56,10 @@ def _get_effective_diffusivity(self, c, T, current):
if stress_option == "true":
# Ai2019 eq [12]
sto = c / phase_param.c_max
- Omega = pybamm.r_average(domain_param.Omega(sto, T))
- E = pybamm.r_average(domain_param.E(sto, T))
- nu = domain_param.nu
- theta_M = Omega / (param.R * T) * (2 * Omega * E) / (9 * (1 - nu))
+ Omega = pybamm.r_average(phase_param.Omega(sto, T))
+ E = pybamm.r_average(phase_param.E(sto, T))
+ nu = phase_param.nu
+ theta_M = Omega / (self.param.R * T) * (2 * Omega * E) / (9 * (1 - nu))
stress_factor = 1 + theta_M * (c - domain_param.c_0)
else:
stress_factor = 1
diff --git a/src/pybamm/models/submodels/particle/fickian_diffusion.py b/src/pybamm/models/submodels/particle/fickian_diffusion.py
index 31c5e6be6c..634e2ce730 100644
--- a/src/pybamm/models/submodels/particle/fickian_diffusion.py
+++ b/src/pybamm/models/submodels/particle/fickian_diffusion.py
@@ -130,7 +130,6 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
domain, Domain = self.domain_Domain
phase_name = self.phase_name
- param = self.param
if self.size_distribution is False:
if self.x_average is False:
@@ -208,7 +207,7 @@ def get_coupled_variables(self, variables):
* pybamm.div(N_s),
f"{Domain} {phase_name}particle bc [mol.m-4]": -j
* R_nondim
- / param.F
+ / self.param.F
/ pybamm.surf(D_eff),
}
)
diff --git a/src/pybamm/models/submodels/particle/msmr_diffusion.py b/src/pybamm/models/submodels/particle/msmr_diffusion.py
index fb712dcdef..8967116ce9 100644
--- a/src/pybamm/models/submodels/particle/msmr_diffusion.py
+++ b/src/pybamm/models/submodels/particle/msmr_diffusion.py
@@ -136,7 +136,6 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
domain, Domain = self.domain_Domain
phase_name = self.phase_name
- param = self.param
if self.size_distribution is False:
if self.x_average is False:
@@ -236,7 +235,7 @@ def get_coupled_variables(self, variables):
/ dxdU,
f"{Domain} {phase_name}particle bc [V.m-1]": j
* R_nondim
- / param.F
+ / self.param.F
/ pybamm.surf(c_max * x * (1 - x) * f * D_eff),
}
)
diff --git a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py
index 8b4b7ffe7c..9dccc0a6c4 100644
--- a/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py
+++ b/src/pybamm/models/submodels/particle/x_averaged_polynomial_profile.py
@@ -97,7 +97,6 @@ def get_fundamental_variables(self):
def get_coupled_variables(self, variables):
domain = self.domain
- param = self.param
c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"]
T_av = variables[f"X-averaged {domain} electrode temperature [K]"]
@@ -135,7 +134,7 @@ def get_coupled_variables(self, variables):
# an extra algebraic equation to solve. For now, using the average c is an
# ok approximation and means the SPM(e) still gives a system of ODEs rather
# than DAEs.
- c_s_surf_xav = c_s_av - (j_xav * R / 5 / param.F / D_eff_av)
+ c_s_surf_xav = c_s_av - (j_xav * R / 5 / self.param.F / D_eff_av)
elif self.name == "quartic profile":
# The surface concentration is computed from the average concentration,
# the average concentration gradient and the boundary flux (see notes
@@ -144,7 +143,9 @@ def get_coupled_variables(self, variables):
q_s_av = variables[
f"Average {domain} particle concentration gradient [mol.m-4]"
]
- c_s_surf_xav = c_s_av + R / 35 * (8 * q_s_av - (j_xav / param.F / D_eff_av))
+ c_s_surf_xav = c_s_av + R / 35 * (
+ 8 * q_s_av - (j_xav / self.param.F / D_eff_av)
+ )
# Set concentration depending on polynomial order
# Since c_s_xav doesn't depend on x, we need to define a spatial
@@ -223,7 +224,6 @@ def set_rhs(self, variables):
# using this model with 2D current collectors with the finite element
# method (see #1399)
domain = self.domain
- param = self.param
if self.size_distribution is False:
c_s_av = variables[f"Average {domain} particle concentration [mol.m-3]"]
@@ -243,7 +243,7 @@ def set_rhs(self, variables):
# eq 15 of Subramanian2005
# equivalent to dcdt = -i_cc / (eps * F * L)
- dcdt = -3 * j_xav / param.F / R
+ dcdt = -3 * j_xav / self.param.F / R
if self.size_distribution is False:
self.rhs = {c_s_av: pybamm.source(dcdt, c_s_av)}
@@ -262,7 +262,7 @@ def set_rhs(self, variables):
# eq 30 of Subramanian2005
dqdt = (
-30 * pybamm.surf(D_eff_xav) * q_s_av / R**2
- - 45 / 2 * j_xav / param.F / R**2
+ - 45 / 2 * j_xav / self.param.F / R**2
)
self.rhs[q_s_av] = pybamm.source(dqdt, q_s_av)
diff --git a/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py b/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py
index 4e25becbab..1301722da0 100644
--- a/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py
+++ b/src/pybamm/models/submodels/particle_mechanics/base_mechanics.py
@@ -38,30 +38,37 @@ def _get_standard_variables(self, l_cr):
def _get_mechanical_results(self, variables):
domain_param = self.domain_param
domain, Domain = self.domain_Domain
+ phase_name = self.phase_name
+ phase_param = self.phase_param
- c_s_rav = variables[f"R-averaged {domain} particle concentration [mol.m-3]"]
- sto_rav = variables[f"R-averaged {domain} particle concentration"]
- c_s_surf = variables[f"{Domain} particle surface concentration [mol.m-3]"]
+ c_s_rav = variables[
+ f"R-averaged {domain} {phase_name}particle concentration [mol.m-3]"
+ ]
+ sto_rav = variables[f"R-averaged {domain} {phase_name}particle concentration"]
+ c_s_surf = variables[
+ f"{Domain} {phase_name}particle surface concentration [mol.m-3]"
+ ]
T_xav = variables["X-averaged cell temperature [K]"]
- phase_name = self.phase_name
T = pybamm.PrimaryBroadcast(
variables[f"{Domain} electrode temperature [K]"],
[f"{domain} {phase_name}particle"],
)
- eps_s = variables[f"{Domain} electrode active material volume fraction"]
+ eps_s = variables[
+ f"{Domain} electrode {phase_name}active material volume fraction"
+ ]
# use a tangential approximation for omega
- sto = variables[f"{Domain} particle concentration"]
- Omega = pybamm.r_average(domain_param.Omega(sto, T))
- R0 = domain_param.prim.R
+ sto = variables[f"{Domain} {phase_name}particle concentration"]
+ Omega = pybamm.r_average(phase_param.Omega(sto, T))
+ R0 = phase_param.R
c_0 = domain_param.c_0
- E0 = pybamm.r_average(domain_param.E(sto, T))
- nu = domain_param.nu
+ E0 = pybamm.r_average(phase_param.E(sto, T))
+ nu = phase_param.nu
L0 = domain_param.L
- sto_init = pybamm.r_average(domain_param.prim.c_init / domain_param.prim.c_max)
+ sto_init = pybamm.r_average(phase_param.c_init / phase_param.c_max)
v_change = pybamm.x_average(
- eps_s * domain_param.prim.t_change(sto_rav)
- ) - pybamm.x_average(eps_s * domain_param.prim.t_change(sto_init))
+ eps_s * phase_param.t_change(sto_rav)
+ ) - pybamm.x_average(eps_s * phase_param.t_change(sto_init))
electrode_thickness_change = self.param.n_electrodes_parallel * v_change * L0
# Ai2019 eq [10]
@@ -81,18 +88,27 @@ def _get_mechanical_results(self, variables):
variables.update(
{
- f"{Domain} particle surface radial stress [Pa]": stress_r_surf,
- f"{Domain} particle surface tangential stress [Pa]": stress_t_surf,
- f"{Domain} particle surface displacement [m]": disp_surf,
- f"X-averaged {domain} particle surface "
+ f"{Domain} {phase_name}particle surface radial stress [Pa]": stress_r_surf,
+ f"{Domain} {phase_name}particle surface tangential stress [Pa]": stress_t_surf,
+ f"{Domain} {phase_name}particle surface displacement [m]": disp_surf,
+ f"X-averaged {domain} {phase_name}particle surface "
"radial stress [Pa]": stress_r_surf_av,
- f"X-averaged {domain} particle surface "
+ f"X-averaged {domain} {phase_name}particle surface "
"tangential stress [Pa]": stress_t_surf_av,
- f"X-averaged {domain} particle surface displacement [m]": disp_surf_av,
- f"{Domain} electrode thickness change [m]": electrode_thickness_change,
+ f"X-averaged {domain} {phase_name}particle surface displacement [m]": disp_surf_av,
+ f"{Domain} electrode {phase_name}thickness change [m]": electrode_thickness_change,
}
)
+ if (
+ f"{Domain} primary thickness change [m]" in variables
+ and f"{Domain} secondary thickness change [m]" in variables
+ ):
+ variables[f"{Domain} thickness change [m]"] = (
+ variables[f"{Domain} primary thickness change [m]"]
+ + variables[f"{Domain} secondary thickness change [m]"]
+ )
+
if (
"Negative electrode thickness change [m]" in variables
and "Positive electrode thickness change [m]" in variables
diff --git a/src/pybamm/models/submodels/particle_mechanics/crack_propagation.py b/src/pybamm/models/submodels/particle_mechanics/crack_propagation.py
index 3bb9ecb7eb..b51f1d1ebd 100644
--- a/src/pybamm/models/submodels/particle_mechanics/crack_propagation.py
+++ b/src/pybamm/models/submodels/particle_mechanics/crack_propagation.py
@@ -102,7 +102,7 @@ def set_initial_conditions(self, variables):
l_cr_0 = pybamm.PrimaryBroadcast(l_cr_0, f"{domain} electrode")
self.initial_conditions = {l_cr: l_cr_0}
- def set_events(self, variables):
+ def add_events_from(self, variables):
domain, Domain = self.domain_Domain
if self.x_average is True:
diff --git a/src/pybamm/models/submodels/porosity/constant_porosity.py b/src/pybamm/models/submodels/porosity/constant_porosity.py
index 0b9f3c0da4..6d6b93d76f 100644
--- a/src/pybamm/models/submodels/porosity/constant_porosity.py
+++ b/src/pybamm/models/submodels/porosity/constant_porosity.py
@@ -27,6 +27,6 @@ def get_fundamental_variables(self):
return variables
- def set_events(self, variables):
+ def add_events_from(self, variables):
# No events since porosity is constant
pass
diff --git a/src/pybamm/models/submodels/porosity/reaction_driven_porosity.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity.py
index fc69d0f1fd..989c90cd9c 100644
--- a/src/pybamm/models/submodels/porosity/reaction_driven_porosity.py
+++ b/src/pybamm/models/submodels/porosity/reaction_driven_porosity.py
@@ -64,7 +64,7 @@ def get_coupled_variables(self, variables):
return variables
- def set_events(self, variables):
+ def add_events_from(self, variables):
eps_p = variables["Positive electrode porosity"]
self.events.append(
pybamm.Event(
diff --git a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py
index 476845f054..8675842ebe 100644
--- a/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py
+++ b/src/pybamm/models/submodels/porosity/reaction_driven_porosity_ode.py
@@ -46,8 +46,6 @@ def get_fundamental_variables(self):
return variables
def get_coupled_variables(self, variables):
- param = self.param
-
depsdt_dict = {}
for domain in self.options.whole_cell_domains:
domain_param = self.param.domain_params[domain.split()[0]]
@@ -59,14 +57,14 @@ def get_coupled_variables(self, variables):
f"X-averaged {domain} volumetric "
"interfacial current density [A.m-3]"
]
- depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / param.F
+ depsdt_k_av = domain_param.DeltaVsurf * a_j_k_av / self.param.F
depsdt_k = pybamm.PrimaryBroadcast(depsdt_k_av, domain)
else:
Domain = domain.capitalize()
a_j_k = variables[
f"{Domain} volumetric interfacial current density [A.m-3]"
]
- depsdt_k = domain_param.DeltaVsurf * a_j_k / param.F
+ depsdt_k = domain_param.DeltaVsurf * a_j_k / self.param.F
depsdt_dict[domain] = depsdt_k
variables.update(self._get_standard_porosity_change_variables(depsdt_dict))
@@ -94,7 +92,7 @@ def set_initial_conditions(self, variables):
eps = variables["Porosity"]
self.initial_conditions = {eps: self.param.epsilon_init}
- def set_events(self, variables):
+ def add_events_from(self, variables):
for domain in self.options.whole_cell_domains:
if domain == "separator":
continue
diff --git a/src/pybamm/models/submodels/thermal/base_thermal.py b/src/pybamm/models/submodels/thermal/base_thermal.py
index c5ebbc7dbd..d5a39435fd 100644
--- a/src/pybamm/models/submodels/thermal/base_thermal.py
+++ b/src/pybamm/models/submodels/thermal/base_thermal.py
@@ -34,7 +34,6 @@ def _get_standard_fundamental_variables(self, T_dict):
For more information about this method in general,
see :meth:`pybamm.base_submodel._get_standard_fundamental_variables`
"""
- param = self.param
# The variable T is the concatenation of the temperature in the middle domains
# (e.g. negative electrode, separator and positive electrode for a full cell),
@@ -46,7 +45,7 @@ def _get_standard_fundamental_variables(self, T_dict):
# (y, z) only and time
y = pybamm.standard_spatial_vars.y
z = pybamm.standard_spatial_vars.z
- T_amb = param.T_amb(y, z, pybamm.t)
+ T_amb = self.param.T_amb(y, z, pybamm.t)
T_amb_av = self._yz_average(T_amb)
variables = {
@@ -69,8 +68,6 @@ def _get_standard_fundamental_variables(self, T_dict):
return variables
def _get_standard_coupled_variables(self, variables):
- param = self.param
-
# Ohmic heating in solid
i_s_p = variables["Positive electrode current density [A.m-2]"]
phi_s_p = variables["Positive electrode potential [V]"]
@@ -78,7 +75,7 @@ def _get_standard_coupled_variables(self, variables):
if self.options.electrode_types["negative"] == "planar":
i_boundary_cc = variables["Current collector current density [A.m-2]"]
T_n = variables["Negative electrode temperature [K]"]
- Q_ohm_s_n = i_boundary_cc**2 / param.n.sigma(T_n)
+ Q_ohm_s_n = i_boundary_cc**2 / self.param.n.sigma(T_n)
else:
i_s_n = variables["Negative electrode current density [A.m-2]"]
phi_s_n = variables["Negative electrode potential [V]"]
@@ -91,6 +88,12 @@ def _get_standard_coupled_variables(self, variables):
# TODO: change full stefan-maxwell conductivity so that i_e is always
# a Concatenation
i_e = variables["Electrolyte current density [A.m-2]"]
+ # Special case for half cell -- i_e has to be a concatenation for this to work due to a mismatch with Q_ohm, so we make a new i_e which is a concatenation.
+ if (not isinstance(i_e, pybamm.Concatenation)) and (
+ self.options.electrode_types["negative"] == "planar"
+ ):
+ i_e = self._get_i_e_for_half_cell_thermal(variables)
+
phi_e = variables["Electrolyte potential [V]"]
if isinstance(i_e, pybamm.Concatenation):
# compute by domain if possible
@@ -146,8 +149,12 @@ def _get_standard_coupled_variables(self, variables):
phase_names = ["primary ", "secondary "]
if self.options.electrode_types["negative"] == "planar":
- Q_rxn_n = pybamm.FullBroadcast(
- 0, ["negative electrode"], "current collector"
+ i_n = variables["Lithium metal total interfacial current density [A.m-2]"]
+ eta_r_n = variables["Lithium metal interface reaction overpotential [V]"]
+ Q_rxn_n = pybamm.PrimaryBroadcast(
+ i_n * eta_r_n / self.param.n.L,
+ ["negative electrode"],
+ "current collector",
)
Q_rev_n = pybamm.FullBroadcast(
0, ["negative electrode"], "current collector"
@@ -199,11 +206,11 @@ def _get_standard_coupled_variables(self, variables):
# Compute the integrated heat source per unit simulated electrode-pair area
# in W.m-2. Note: this can still be a function of y and z for
# higher-dimensional pouch cell models
- Q_ohm_Wm2 = Q_ohm_av * param.L
- Q_rxn_Wm2 = Q_rxn_av * param.L
- Q_rev_Wm2 = Q_rev_av * param.L
- Q_mix_Wm2 = Q_mix_av * param.L
- Q_Wm2 = Q_av * param.L
+ Q_ohm_Wm2 = Q_ohm_av * self.param.L
+ Q_rxn_Wm2 = Q_rxn_av * self.param.L
+ Q_rev_Wm2 = Q_rev_av * self.param.L
+ Q_mix_Wm2 = Q_mix_av * self.param.L
+ Q_Wm2 = Q_av * self.param.L
# Now average over the electrode height and width
Q_ohm_Wm2_av = self._yz_average(Q_ohm_Wm2)
@@ -216,8 +223,8 @@ def _get_standard_coupled_variables(self, variables):
# the product of electrode height * electrode width * electrode stack thickness
# Note: we multiply by the number of electrode pairs, since the Q_xx_Wm2_av
# variables are per electrode pair
- n_elec = param.n_electrodes_parallel
- A = param.L_y * param.L_z # *modelled* electrode area
+ n_elec = self.param.n_electrodes_parallel
+ A = self.param.L_y * self.param.L_z # *modelled* electrode area
Q_ohm_W = Q_ohm_Wm2_av * n_elec * A
Q_rxn_W = Q_rxn_Wm2_av * n_elec * A
Q_rev_W = Q_rev_Wm2_av * n_elec * A
@@ -226,7 +233,7 @@ def _get_standard_coupled_variables(self, variables):
# Compute volume-averaged heat source terms over the *entire cell volume*, not
# the product of electrode height * electrode width * electrode stack thickness
- V = param.V_cell # *actual* cell volume
+ V = self.param.V_cell # *actual* cell volume
Q_ohm_vol_av = Q_ohm_W / V
Q_rxn_vol_av = Q_rxn_W / V
Q_rev_vol_av = Q_rev_W / V
@@ -235,7 +242,7 @@ def _get_standard_coupled_variables(self, variables):
# Effective heat capacity
T_vol_av = variables["Volume-averaged cell temperature [K]"]
- rho_c_p_eff_av = param.rho_c_p_eff(T_vol_av)
+ rho_c_p_eff_av = self.param.rho_c_p_eff(T_vol_av)
variables.update(
{
@@ -314,7 +321,6 @@ def _current_collector_heating(self, variables):
def _heat_of_mixing(self, variables):
"""Compute heat of mixing source terms."""
- param = self.param
if self.options["heat of mixing"] == "true":
F = pybamm.constants.F.value
@@ -339,8 +345,10 @@ def _heat_of_mixing(self, variables):
T_n = variables["Negative electrode temperature [K]"]
T_n_part = pybamm.PrimaryBroadcast(T_n, ["negative particle"])
dc_n_dr2 = pybamm.inner(pybamm.grad(c_n), pybamm.grad(c_n))
- D_n = param.n.prim.D(c_n, T_n_part)
- dUeq_n = param.n.prim.U(c_n / param.n.prim.c_max, T_n_part).diff(c_n)
+ D_n = self.param.n.prim.D(c_n, T_n_part)
+ dUeq_n = self.param.n.prim.U(
+ c_n / self.param.n.prim.c_max, T_n_part
+ ).diff(c_n)
integrand_r_n = D_n * dc_n_dr2 * dUeq_n
integration_variable_r_n = [
pybamm.SpatialVariable("r", domain=integrand_r_n.domain)
@@ -360,8 +368,10 @@ def _heat_of_mixing(self, variables):
T_p = variables["Positive electrode temperature [K]"]
T_p_part = pybamm.PrimaryBroadcast(T_p, ["positive particle"])
dc_p_dr2 = pybamm.inner(pybamm.grad(c_p), pybamm.grad(c_p))
- D_p = param.p.prim.D(c_p, T_p_part)
- dUeq_p = param.p.prim.U(c_p / param.p.prim.c_max, T_p_part).diff(c_p)
+ D_p = self.param.p.prim.D(c_p, T_p_part)
+ dUeq_p = self.param.p.prim.U(c_p / self.param.p.prim.c_max, T_p_part).diff(
+ c_p
+ )
integrand_r_p = D_p * dc_p_dr2 * dUeq_p
integration_variable_r_p = [
pybamm.SpatialVariable("r", domain=integrand_r_p.domain)
@@ -407,3 +417,23 @@ def _yz_average(self, var):
return pybamm.z_average(var)
elif self.options["dimensionality"] == 2:
return pybamm.yz_average(var)
+
+ def _get_i_e_for_half_cell_thermal(self, variables):
+ c_e_s = variables["Separator electrolyte concentration [mol.m-3]"]
+ c_e_p = variables["Positive electrolyte concentration [mol.m-3]"]
+ grad_phi_e_s = variables["Gradient of separator electrolyte potential [V.m-1]"]
+ grad_phi_e_p = variables["Gradient of positive electrolyte potential [V.m-1]"]
+ grad_c_e_s = pybamm.grad(c_e_s)
+ grad_c_e_p = pybamm.grad(c_e_p)
+ T_s = variables["Separator temperature [K]"]
+ T_p = variables["Positive electrode temperature [K]"]
+ tor_s = variables["Separator electrolyte transport efficiency"]
+ tor_p = variables["Positive electrolyte transport efficiency"]
+ i_e_s = (self.param.kappa_e(c_e_s, T_s) * tor_s) * (
+ self.param.chiRT_over_Fc(c_e_s, T_s) * grad_c_e_s - grad_phi_e_s
+ )
+ i_e_p = (self.param.kappa_e(c_e_p, T_p) * tor_p) * (
+ self.param.chiRT_over_Fc(c_e_p, T_p) * grad_c_e_p - grad_phi_e_p
+ )
+ i_e = pybamm.concatenation(i_e_s, i_e_p)
+ return i_e
diff --git a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py
index fb026a9a0a..5c29ef0a9f 100644
--- a/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py
+++ b/src/pybamm/models/submodels/thermal/pouch_cell/pouch_cell_1D_current_collectors.py
@@ -86,24 +86,23 @@ def set_rhs(self, variables):
}
def set_boundary_conditions(self, variables):
- param = self.param
T_surf = variables["Surface temperature [K]"]
T_av = variables["X-averaged cell temperature [K]"]
# Find tab locations (top vs bottom)
- L_y = param.L_y
- L_z = param.L_z
- neg_tab_z = param.n.centre_z_tab
- pos_tab_z = param.p.centre_z_tab
+ L_y = self.param.L_y
+ L_z = self.param.L_z
+ neg_tab_z = self.param.n.centre_z_tab
+ pos_tab_z = self.param.p.centre_z_tab
neg_tab_top_bool = pybamm.Equality(neg_tab_z, L_z)
neg_tab_bottom_bool = pybamm.Equality(neg_tab_z, 0)
pos_tab_top_bool = pybamm.Equality(pos_tab_z, L_z)
pos_tab_bottom_bool = pybamm.Equality(pos_tab_z, 0)
# Calculate tab vs non-tab area on top and bottom
- neg_tab_area = param.n.L_tab * param.n.L_cc
- pos_tab_area = param.p.L_tab * param.p.L_cc
- total_area = param.L * param.L_y
+ neg_tab_area = self.param.n.L_tab * self.param.n.L_cc
+ pos_tab_area = self.param.p.L_tab * self.param.p.L_cc
+ total_area = self.param.L * self.param.L_y
non_tab_top_area = (
total_area
- neg_tab_area * neg_tab_top_bool
@@ -118,10 +117,10 @@ def set_boundary_conditions(self, variables):
# Calculate heat fluxes weighted by area
# Note: can't do y-average of h_edge here since y isn't meshed. Evaluate at
# midpoint.
- q_tab_n = -param.n.h_tab * (T_av - T_surf)
- q_tab_p = -param.p.h_tab * (T_av - T_surf)
- q_edge_top = -param.h_edge(L_y / 2, L_z) * (T_av - T_surf)
- q_edge_bottom = -param.h_edge(L_y / 2, 0) * (T_av - T_surf)
+ q_tab_n = -self.param.n.h_tab * (T_av - T_surf)
+ q_tab_p = -self.param.p.h_tab * (T_av - T_surf)
+ q_edge_top = -self.param.h_edge(L_y / 2, L_z) * (T_av - T_surf)
+ q_edge_bottom = -self.param.h_edge(L_y / 2, 0) * (T_av - T_surf)
q_top = (
q_tab_n * neg_tab_area * neg_tab_top_bool
+ q_tab_p * pos_tab_area * pos_tab_top_bool
@@ -136,7 +135,7 @@ def set_boundary_conditions(self, variables):
# just use left and right for clarity
# left = bottom of cell (z=0)
# right = top of cell (z=L_z)
- lambda_eff = param.lambda_eff(T_av)
+ lambda_eff = self.param.lambda_eff(T_av)
self.boundary_conditions = {
T_av: {
"left": (
diff --git a/src/pybamm/models/submodels/thermal/surface/lumped.py b/src/pybamm/models/submodels/thermal/surface/lumped.py
index dc481947e8..9fe3118e2f 100644
--- a/src/pybamm/models/submodels/thermal/surface/lumped.py
+++ b/src/pybamm/models/submodels/thermal/surface/lumped.py
@@ -19,7 +19,7 @@ class Lumped(pybamm.BaseSubModel):
def __init__(self, param, options=None):
super().__init__(param, options=options)
- pybamm.citations.register("lin2014lumped")
+ pybamm.citations.register("Lin2014")
def get_fundamental_variables(self):
T_surf = pybamm.Variable("Surface temperature [K]")
diff --git a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py
index ec26d7955d..a960f20e66 100644
--- a/src/pybamm/models/submodels/transport_efficiency/bruggeman.py
+++ b/src/pybamm/models/submodels/transport_efficiency/bruggeman.py
@@ -7,7 +7,7 @@
class Bruggeman(BaseModel):
"""Submodel for Bruggeman transport_efficiency,
- :footcite:t:`bruggeman1935berechnung`
+ :footcite:t:`Bruggeman1935`
Parameters
----------
@@ -28,7 +28,7 @@ def get_coupled_variables(self, variables):
for domain in self.options.whole_cell_domains:
Domain = domain.capitalize()
eps_k = variables[f"{Domain} porosity"]
- pybamm.citations.register("bruggeman1935berechnung")
+ pybamm.citations.register("Bruggeman1935")
b_k = self.param.domain_params[domain.split()[0]].b_e
tor_k = eps_k**b_k
tor_dict[domain] = tor_k
@@ -40,7 +40,7 @@ def get_coupled_variables(self, variables):
else:
Domain = domain.capitalize()
phi_k = 1 - variables[f"{Domain} porosity"]
- pybamm.citations.register("bruggeman1935berechnung")
+ pybamm.citations.register("Bruggeman1935")
b_k = self.param.domain_params[domain.split()[0]].b_s
tor_k = phi_k**b_k
tor_dict[domain] = tor_k
diff --git a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py
index 3ffb57e7de..b9165cf255 100644
--- a/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py
+++ b/src/pybamm/models/submodels/transport_efficiency/cation_exchange_membrane.py
@@ -7,7 +7,7 @@
class CationExchangeMembrane(BaseModel):
"""Submodel for Cation Exchange Membrane transport_efficiency,
- :footcite:t:`bruggeman1935berechnung`, :footcite:t:`shen2007critical`
+ :footcite:t:`Bruggeman1935`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("mackie1955diffusion")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Mackie1955")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py
index 7ec8bc3580..f60f71765c 100644
--- a/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py
+++ b/src/pybamm/models/submodels/transport_efficiency/heterogeneous_catalyst.py
@@ -7,7 +7,7 @@
class HeterogeneousCatalyst(BaseModel):
"""Submodel for Heterogeneous Catalyst transport_efficiency
- :footcite:t:`beeckman1990mathematical`, :footcite:t:`shen2007critical`
+ :footcite:t:`Beeckman1990`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("beeckman1990mathematical")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Beeckman1990")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py
index 306c66b774..fe7e8dfb1d 100644
--- a/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py
+++ b/src/pybamm/models/submodels/transport_efficiency/hyperbola_of_revolution.py
@@ -7,7 +7,7 @@
class HyperbolaOfRevolution(BaseModel):
"""Submodel for Hyperbola of revolution transport_efficiency
- :footcite:t:`petersen1958diffusion`, :footcite:t:`shen2007critical`
+ :footcite:t:`Petersen1958`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("petersen1958diffusion")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Petersen1958")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py
index 13b3a3515e..4b9e9b5dc5 100644
--- a/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py
+++ b/src/pybamm/models/submodels/transport_efficiency/ordered_packing.py
@@ -7,7 +7,7 @@
class OrderedPacking(BaseModel):
"""Submodel for Ordered Packing transport_efficiency
- :footcite:t:`akanni1987effective`, :footcite:t:`shen2007critical`
+ :footcite:t:`Akanni1987`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("akanni1987effective")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Akanni1987")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py
index 9bbed1fd05..ae2dbc590d 100644
--- a/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py
+++ b/src/pybamm/models/submodels/transport_efficiency/overlapping_spheres.py
@@ -7,7 +7,7 @@
class OverlappingSpheres(BaseModel):
"""Submodel for Overlapping Spheres transport_efficiency
- :footcite:t:`weissberg1963effective`, :footcite:t:`shen2007critical`
+ :footcite:t:`Weissberg1963`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("weissberg1963effective")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Weissberg1963")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py
index da32f2f4fe..b9eb49a54e 100644
--- a/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py
+++ b/src/pybamm/models/submodels/transport_efficiency/random_overlapping_cylinders.py
@@ -7,7 +7,7 @@
class RandomOverlappingCylinders(BaseModel):
"""Submodel for Random Overlapping Cylinders transport_efficiency,
- :footcite:t:`tomadakis1993transport`, :footcite:t:`shen2007critical`
+ :footcite:t:`Tomadakis1993`, :footcite:t:`Shen2007`
Parameters
----------
@@ -23,8 +23,8 @@ def __init__(self, param, component, options=None):
super().__init__(param, component, options=options)
def get_coupled_variables(self, variables):
- pybamm.citations.register("shen2007critical")
- pybamm.citations.register("tomadakis1993transport")
+ pybamm.citations.register("Shen2007")
+ pybamm.citations.register("Tomadakis1993")
if self.component == "Electrolyte":
tor_dict = {}
for domain in self.options.whole_cell_domains:
diff --git a/src/pybamm/parameters/bpx.py b/src/pybamm/parameters/bpx.py
index 7485e805b9..df380ad627 100644
--- a/src/pybamm/parameters/bpx.py
+++ b/src/pybamm/parameters/bpx.py
@@ -228,12 +228,6 @@ def _bpx_to_param_dict(bpx: BPX) -> dict:
def _arrhenius(Ea, T):
return exp(Ea / constants.R * (1 / T_ref - 1 / T))
- def _entropic_change(sto, c_s_max, dUdT, constant=False):
- if constant:
- return dUdT
- else:
- return dUdT(sto)
-
# reaction rates in pybamm exchange current is defined j0 = k * sqrt(ce * cs *
# (cs-cs_max)) in BPX exchange current is defined j0 = F * k_norm * sqrt((ce/ce0) *
# (cs/cs_max) * (1-cs/cs_max))
@@ -284,25 +278,10 @@ def _conductivity(c_e, T, Ea, sigma_ref, constant=False):
)
# entropic change
- dUdT = pybamm_dict[
- phase_domain_pre_name + "entropic change coefficient [V.K-1]"
- ]
- if callable(dUdT):
+ dUdT = pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"]
+ if isinstance(dUdT, tuple):
pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = (
- partial(_entropic_change, dUdT=dUdT)
- )
- elif isinstance(dUdT, tuple):
- pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = (
- partial(
- _entropic_change,
- dUdT=partial(
- _interpolant_func, name=dUdT[0], x=dUdT[1][0], y=dUdT[1][1]
- ),
- )
- )
- else:
- pybamm_dict[phase_domain_pre_name + "OCP entropic change [V.K-1]"] = (
- partial(_entropic_change, dUdT=dUdT, constant=True)
+ partial(_interpolant_func, name=dUdT[0], x=dUdT[1][0], y=dUdT[1][1])
)
# reaction rate
@@ -440,6 +419,10 @@ def _get_pybamm_name(pybamm_name, domain):
pybamm_name = domain.short_pre_name + pybamm_name_lower
elif pybamm_name.startswith("OCP"):
pybamm_name = domain.pre_name + pybamm_name
+ elif pybamm_name.startswith("Entropic change"):
+ pybamm_name = domain.pre_name + pybamm_name.replace(
+ "Entropic change coefficient", "OCP entropic change"
+ )
elif pybamm_name.startswith("Cation transference number"):
pybamm_name = pybamm_name
elif domain.pre_name != "":
diff --git a/src/pybamm/parameters/lithium_ion_parameters.py b/src/pybamm/parameters/lithium_ion_parameters.py
index f5a76c6d48..3902242d78 100644
--- a/src/pybamm/parameters/lithium_ion_parameters.py
+++ b/src/pybamm/parameters/lithium_ion_parameters.py
@@ -269,7 +269,6 @@ def _set_parameters(self):
self.tau_s = self.geo.tau_s
# Mechanical parameters
- self.nu = pybamm.Parameter(f"{Domain} electrode Poisson's ratio")
self.c_0 = pybamm.Parameter(
f"{Domain} electrode reference concentration for free of deformation "
"[mol.m-3]"
@@ -283,20 +282,6 @@ def _set_parameters(self):
self.b_cr = pybamm.Parameter(f"{Domain} electrode Paris' law constant b")
self.m_cr = pybamm.Parameter(f"{Domain} electrode Paris' law constant m")
- # Loss of active material parameters
- self.m_LAM = pybamm.Parameter(
- f"{Domain} electrode LAM constant exponential term"
- )
- self.beta_LAM = pybamm.Parameter(
- f"{Domain} electrode LAM constant proportional term [s-1]"
- )
- self.stress_critical = pybamm.Parameter(
- f"{Domain} electrode critical stress [Pa]"
- )
- self.beta_LAM_sei = pybamm.Parameter(
- f"{Domain} electrode reaction-driven LAM factor [m3.mol-1]"
- )
-
# Utilisation parameters
self.u_init = pybamm.Parameter(
f"Initial {domain} electrode interface utilisation"
@@ -313,22 +298,6 @@ def C_dl(self, T):
f"{Domain} electrode double-layer capacity [F.m-2]", inputs
)
- def Omega(self, sto, T):
- """Dimensional partial molar volume of Li in solid solution [m3.mol-1]"""
- Domain = self.domain.capitalize()
- inputs = {f"{Domain} particle stoichiometry": sto, "Temperature [K]": T}
- return pybamm.FunctionParameter(
- f"{Domain} electrode partial molar volume [m3.mol-1]", inputs
- )
-
- def E(self, sto, T):
- """Dimensional Young's modulus"""
- Domain = self.domain.capitalize()
- inputs = {f"{Domain} particle stoichiometry": sto, "Temperature [K]": T}
- return pybamm.FunctionParameter(
- f"{Domain} electrode Young's modulus [Pa]", inputs
- )
-
def sigma(self, T):
"""Dimensional electrical conductivity in electrode"""
inputs = {"Temperature [K]": T}
@@ -538,6 +507,23 @@ def _set_parameters(self):
if self.options["particle shape"] == "spherical":
self.a_typ = 3 * pybamm.xyz_average(self.epsilon_s) / self.R_typ
+ # Mechanical property
+ self.nu = pybamm.Parameter(f"{pref}{Domain} electrode Poisson's ratio")
+
+ # Loss of active material parameters
+ self.m_LAM = pybamm.Parameter(
+ f"{pref}{Domain} electrode LAM constant exponential term"
+ )
+ self.beta_LAM = pybamm.Parameter(
+ f"{pref}{Domain} electrode LAM constant proportional term [s-1]"
+ )
+ self.stress_critical = pybamm.Parameter(
+ f"{pref}{Domain} electrode critical stress [Pa]"
+ )
+ self.beta_LAM_sei = pybamm.Parameter(
+ f"{pref}{Domain} electrode reaction-driven LAM factor [m3.mol-1]"
+ )
+
def D(self, c_s, T, lithiation=None):
"""
Dimensional diffusivity in particle. In the parameter sets this is defined as
@@ -669,11 +655,9 @@ def dUdT(self, sto):
"MSMR" formulation, stoichiometry is explicitly defined as a function of U and
T, and dUdT is only used to calculate the reversible heat generation term.
"""
- domain, Domain = self.domain_Domain
+ Domain = self.domain.capitalize()
inputs = {
f"{Domain} particle stoichiometry": sto,
- f"{self.phase_prefactor}Maximum {domain} particle "
- "surface concentration [mol.m-3]": self.c_max,
}
return pybamm.FunctionParameter(
f"{self.phase_prefactor}{Domain} electrode OCP entropic change [V.K-1]",
@@ -794,12 +778,33 @@ def t_change(self, sto):
"""
Volume change for the electrode; sto should be R-averaged
"""
- domain, Domain = self.domain_Domain
+ Domain = self.domain.capitalize()
return pybamm.FunctionParameter(
- f"{Domain} electrode volume change",
+ f"{self.phase_prefactor}{Domain} electrode volume change",
{
- "Particle stoichiometry": sto,
- f"{self.phase_prefactor}Maximum {domain} particle "
- "surface concentration [mol.m-3]": self.c_max,
+ f"{Domain} particle stoichiometry": sto,
},
)
+
+ def Omega(self, sto, T):
+ """Dimensional partial molar volume of Li in solid solution [m3.mol-1]"""
+ domain, Domain = self.domain_Domain
+ inputs = {
+ f"{self.phase_prefactor} particle stoichiometry": sto,
+ "Temperature [K]": T,
+ }
+ return pybamm.FunctionParameter(
+ f"{self.phase_prefactor}{Domain} electrode partial molar volume [m3.mol-1]",
+ inputs,
+ )
+
+ def E(self, sto, T):
+ """Dimensional Young's modulus"""
+ domain, Domain = self.domain_Domain
+ inputs = {
+ f"{self.phase_prefactor} particle stoichiometry": sto,
+ "Temperature [K]": T,
+ }
+ return pybamm.FunctionParameter(
+ f"{self.phase_prefactor}{Domain} electrode Young's modulus [Pa]", inputs
+ )
diff --git a/src/pybamm/parameters/parameter_sets.py b/src/pybamm/parameters/parameter_sets.py
index a3ddd0ed2e..22b476f4e0 100644
--- a/src/pybamm/parameters/parameter_sets.py
+++ b/src/pybamm/parameters/parameter_sets.py
@@ -18,7 +18,7 @@ class ParameterSets(Mapping):
>>> import pybamm
>>> list(pybamm.parameter_sets)
- ['Ai2020', 'Chen2020', ...]
+ ['Ai2020', 'Chayambuka2022', ...]
Get the docstring for a parameter set:
@@ -26,7 +26,7 @@ class ParameterSets(Mapping):
>>> print(pybamm.parameter_sets.get_docstring("Ai2020"))
Parameters for the Enertech cell (Ai2020), from the papers :footcite:t:`Ai2019`,
- :footcite:t:`rieger2016new` and references therein.
+ :footcite:t:`Rieger2016` and references therein.
...
See also: :ref:`adding-parameter-sets`
diff --git a/src/pybamm/parameters/parameter_values.py b/src/pybamm/parameters/parameter_values.py
index 43c1ea17ce..bb30f24836 100644
--- a/src/pybamm/parameters/parameter_values.py
+++ b/src/pybamm/parameters/parameter_values.py
@@ -1,6 +1,3 @@
-#
-# Parameter values for a simulation
-#
import numpy as np
import pybamm
import numbers
@@ -35,15 +32,7 @@ class ParameterValues:
"""
- def __init__(self, values, chemistry=None):
- if chemistry is not None:
- raise ValueError(
- "The 'chemistry' keyword argument has been deprecated. "
- "Call `ParameterValues` with a dictionary dictionary of "
- "parameter values, or the name of a parameter set (string), "
- "as the single argument, e.g. `ParameterValues('Chen2020')`.",
- )
-
+ def __init__(self, values):
# add physical constants as default values
self._dict_items = pybamm.FuzzyDict(
{
@@ -192,7 +181,7 @@ def items(self):
return self._dict_items.items()
def pop(self, *args, **kwargs):
- self._dict_items.pop(*args, **kwargs)
+ return self._dict_items.pop(*args, **kwargs)
def copy(self):
"""Returns a copy of the parameter values. Makes sure to copy the internal
@@ -254,7 +243,7 @@ def update(self, values, check_conflict=False, check_already_exists=True, path="
f"Cannot update parameter '{name}' as it does not "
+ f"have a default value. ({err.args[0]}). If you are "
+ "sure you want to update this parameter, use "
- + "param.update({{name: value}}, check_already_exists=False)"
+ + "param.update({name: value}, check_already_exists=False)"
) from err
if isinstance(value, str):
if (
@@ -930,3 +919,9 @@ def print_evaluated_parameters(self, evaluated_parameters, output_file):
file.write((s + " : {:10.4g}\n").format(name, value))
else:
file.write((s + " : {:10.3E}\n").format(name, value))
+
+ def __contains__(self, key):
+ return key in self._dict_items
+
+ def __iter__(self):
+ return iter(self._dict_items)
diff --git a/src/pybamm/plotting/quick_plot.py b/src/pybamm/plotting/quick_plot.py
index 39dc974f9b..babfd2e761 100644
--- a/src/pybamm/plotting/quick_plot.py
+++ b/src/pybamm/plotting/quick_plot.py
@@ -84,6 +84,9 @@ class QuickPlot:
variable_limits : str or dict of str, optional
How to set the axis limits (for 0D or 1D variables) or colorbar limits (for 2D
variables). Options are:
+ n_t_linear: int, optional
+ The number of linearly spaced time points added to the t axis for each sub-solution.
+ Note: this is only used if the solution has hermite interpolation enabled.
- "fixed" (default): keep all axes fixes so that all data is visible
- "tight": make axes tight to plot at each time
@@ -105,6 +108,7 @@ def __init__(
time_unit=None,
spatial_unit="um",
variable_limits="fixed",
+ n_t_linear=100,
):
solutions = self.preprocess_solutions(solutions)
@@ -169,6 +173,24 @@ def __init__(
min_t = np.min([t[0] for t in self.ts_seconds])
max_t = np.max([t[-1] for t in self.ts_seconds])
+ hermite_interp = all(sol.hermite_interpolation for sol in solutions)
+
+ def t_sample(sol):
+ if hermite_interp and n_t_linear > 2:
+ # Linearly spaced time points
+ t_linspace = np.linspace(sol.t[0], sol.t[-1], n_t_linear + 2)[1:-1]
+ t_plot = np.union1d(sol.t, t_linspace)
+ else:
+ t_plot = sol.t
+ return t_plot
+
+ ts_seconds = []
+ for sol in solutions:
+ # Sample time points for each sub-solution
+ t_sol = [t_sample(sub_sol) for sub_sol in sol.sub_solutions]
+ ts_seconds.append(np.concatenate(t_sol))
+ self.ts_seconds = ts_seconds
+
# Set timescale
if time_unit is None:
# defaults depend on how long the simulation is
@@ -419,14 +441,14 @@ def reset_axis(self):
spatial_vars = self.spatial_variable_dict[key]
var_min = np.min(
[
- ax_min(var(self.ts_seconds[i], **spatial_vars, warn=False))
+ ax_min(var(self.ts_seconds[i], **spatial_vars))
for i, variable_list in enumerate(variable_lists)
for var in variable_list
]
)
var_max = np.max(
[
- ax_max(var(self.ts_seconds[i], **spatial_vars, warn=False))
+ ax_max(var(self.ts_seconds[i], **spatial_vars))
for i, variable_list in enumerate(variable_lists)
for var in variable_list
]
@@ -512,7 +534,7 @@ def plot(self, t, dynamic=False):
full_t = self.ts_seconds[i]
(self.plots[key][i][j],) = ax.plot(
full_t / self.time_scaling_factor,
- variable(full_t, warn=False),
+ variable(full_t),
color=self.colors[i],
linestyle=linestyle,
)
@@ -548,7 +570,7 @@ def plot(self, t, dynamic=False):
linestyle = self.linestyles[j]
(self.plots[key][i][j],) = ax.plot(
self.first_spatial_variable[key],
- variable(t_in_seconds, **spatial_vars, warn=False),
+ variable(t_in_seconds, **spatial_vars),
color=self.colors[i],
linestyle=linestyle,
zorder=10,
@@ -570,13 +592,13 @@ def plot(self, t, dynamic=False):
y_name = next(iter(spatial_vars.keys()))[0]
x = self.second_spatial_variable[key]
y = self.first_spatial_variable[key]
- var = variable(t_in_seconds, **spatial_vars, warn=False)
+ var = variable(t_in_seconds, **spatial_vars)
else:
x_name = next(iter(spatial_vars.keys()))[0]
y_name = list(spatial_vars.keys())[1][0]
x = self.first_spatial_variable[key]
y = self.second_spatial_variable[key]
- var = variable(t_in_seconds, **spatial_vars, warn=False).T
+ var = variable(t_in_seconds, **spatial_vars).T
ax.set_xlabel(f"{x_name} [{self.spatial_unit}]")
ax.set_ylabel(f"{y_name} [{self.spatial_unit}]")
vmin, vmax = self.variable_limits[key]
@@ -710,7 +732,6 @@ def slider_update(self, t):
var = variable(
time_in_seconds,
**self.spatial_variable_dict[key],
- warn=False,
)
plot[i][j].set_ydata(var)
var_min = min(var_min, ax_min(var))
@@ -729,11 +750,11 @@ def slider_update(self, t):
if self.x_first_and_y_second[key] is False:
x = self.second_spatial_variable[key]
y = self.first_spatial_variable[key]
- var = variable(time_in_seconds, **spatial_vars, warn=False)
+ var = variable(time_in_seconds, **spatial_vars)
else:
x = self.first_spatial_variable[key]
y = self.second_spatial_variable[key]
- var = variable(time_in_seconds, **spatial_vars, warn=False).T
+ var = variable(time_in_seconds, **spatial_vars).T
# store the plot and the var data (for testing) as cant access
# z data from QuadMesh or QuadContourSet object
if self.is_y_z[key] is True:
diff --git a/src/pybamm/settings.py b/src/pybamm/settings.py
index d190eaf47e..6b7a628195 100644
--- a/src/pybamm/settings.py
+++ b/src/pybamm/settings.py
@@ -12,7 +12,6 @@ class Settings:
_abs_smoothing = "exact"
max_words_in_line = 4
max_y_value = 1e5
- step_start_offset = 1e-9
tolerances = {
"D_e__c_e": 10, # dimensional
"kappa_e__c_e": 10, # dimensional
diff --git a/src/pybamm/simulation.py b/src/pybamm/simulation.py
index 5b999d6c83..cd4fc62ec8 100644
--- a/src/pybamm/simulation.py
+++ b/src/pybamm/simulation.py
@@ -8,9 +8,9 @@
import numpy as np
import hashlib
import warnings
-import sys
from functools import lru_cache
from datetime import timedelta
+import pybamm.telemetry
from pybamm.util import import_optional_dependency
from pybamm.expression_tree.operations.serialise import Serialise
@@ -175,7 +175,12 @@ def _set_random_seed(self):
% (2**32)
)
- def set_up_and_parameterise_experiment(self):
+ def set_up_and_parameterise_experiment(self, solve_kwargs=None):
+ msg = "pybamm.simulation.set_up_and_parameterise_experiment is deprecated and not meant to be accessed by users."
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
+ self._set_up_and_parameterise_experiment(solve_kwargs=solve_kwargs)
+
+ def _set_up_and_parameterise_experiment(self, solve_kwargs=None):
"""
Create and parameterise the models for each step in the experiment.
@@ -183,6 +188,46 @@ def set_up_and_parameterise_experiment(self):
reduces simulation time since the model formulation is efficient.
"""
parameter_values = self._parameter_values.copy()
+
+ # some parameters are used to control the experiment, and should not be
+ # input parameters
+ restrict_list = {"Initial temperature [K]", "Ambient temperature [K]"}
+ for step in self.experiment.steps:
+ if issubclass(step.__class__, pybamm.experiment.step.BaseStepImplicit):
+ restrict_list.update(step.get_parameter_values([]).keys())
+ elif issubclass(step.__class__, pybamm.experiment.step.BaseStepExplicit):
+ restrict_list.update(["Current function [A]"])
+ for key in restrict_list:
+ if key in parameter_values.keys() and isinstance(
+ parameter_values[key], pybamm.InputParameter
+ ):
+ raise pybamm.ModelError(
+ f"Cannot use '{key}' as an input parameter in this experiment. "
+ f"This experiment is controlled via the following parameters: {restrict_list}. "
+ f"None of these parameters are able to be input parameters."
+ )
+
+ if (
+ solve_kwargs is not None
+ and "calculate_sensitivities" in solve_kwargs
+ and solve_kwargs["calculate_sensitivities"]
+ ):
+ for step in self.experiment.steps:
+ if any(
+ [
+ isinstance(
+ term,
+ pybamm.experiment.step.step_termination.BaseTermination,
+ )
+ for term in step.termination
+ ]
+ ):
+ pybamm.logger.warning(
+ f"Step '{step}' has a termination condition based on an event. Sensitivity calculation will be inaccurate "
+ "if the time of each step event changes rapidly with respect to the parameters. "
+ )
+ break
+
# Set the initial temperature to be the temperature of the first step
# We can set this globally for all steps since any subsequent steps will either
# start at the temperature at the end of the previous step (if non-isothermal
@@ -214,10 +259,16 @@ def set_up_and_parameterise_experiment(self):
)
def set_parameters(self):
+ msg = (
+ "pybamm.set_parameters is deprecated and not meant to be accessed by users."
+ )
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
+ self._set_parameters()
+
+ def _set_parameters(self):
"""
A method to set the parameters in the model and the associated geometry.
"""
-
if self._model_with_set_params:
return
@@ -304,7 +355,7 @@ def build(self, initial_soc=None, inputs=None):
# rebuilt model so clear solver setup
self._solver._model_set_up = {}
- def build_for_experiment(self, initial_soc=None, inputs=None):
+ def build_for_experiment(self, initial_soc=None, inputs=None, solve_kwargs=None):
"""
Similar to :meth:`Simulation.build`, but for the case of simulating an
experiment, where there may be several models and solvers to build.
@@ -315,7 +366,7 @@ def build_for_experiment(self, initial_soc=None, inputs=None):
if self.steps_to_built_models:
return
else:
- self.set_up_and_parameterise_experiment()
+ self._set_up_and_parameterise_experiment(solve_kwargs)
# Can process geometry with default parameter values (only electrical
# parameters change between parameter values)
@@ -411,6 +462,8 @@ def solve(
Additional key-word arguments passed to `solver.solve`.
See :meth:`pybamm.BaseSolver.solve`.
"""
+ pybamm.telemetry.capture("simulation-solved")
+
# Setup
if solver is None:
solver = self._solver
@@ -464,9 +517,9 @@ def solve(
# the time data (to ensure the resolution of t_eval is fine enough).
# We only raise a warning here as users may genuinely only want
# the solution returned at some specified points.
- elif (
- set(np.round(time_data, 12)).issubset(set(np.round(t_eval, 12)))
- ) is False:
+ elif not isinstance(solver, pybamm.IDAKLUSolver) and not set(
+ np.round(time_data, 12)
+ ).issubset(set(np.round(t_eval, 12))):
warnings.warn(
"""
t_eval does not contain all of the time points in the data
@@ -478,16 +531,14 @@ def solve(
)
dt_data_min = np.min(np.diff(time_data))
dt_eval_max = np.max(np.diff(t_eval))
- if dt_eval_max > dt_data_min + sys.float_info.epsilon:
+ if dt_eval_max > np.nextafter(dt_data_min, np.inf):
warnings.warn(
- f"""
- The largest timestep in t_eval ({dt_eval_max}) is larger than
- the smallest timestep in the data ({dt_data_min}). The returned
- solution may not have the correct resolution to accurately
- capture the input. Try refining t_eval. Alternatively,
- passing t_eval = None automatically sets t_eval to be the
- points in the data.
- """,
+ f"The largest timestep in t_eval ({dt_eval_max}) is larger than "
+ f"the smallest timestep in the data ({dt_data_min}). The returned "
+ "solution may not have the correct resolution to accurately "
+ "capture the input. Try refining t_eval. Alternatively, "
+ "passing t_eval = None automatically sets t_eval to be the "
+ "points in the data.",
pybamm.SolverWarning,
stacklevel=2,
)
@@ -498,7 +549,9 @@ def solve(
elif self.operating_mode == "with experiment":
callbacks.on_experiment_start(logs)
- self.build_for_experiment(initial_soc=initial_soc, inputs=inputs)
+ self.build_for_experiment(
+ initial_soc=initial_soc, inputs=inputs, solve_kwargs=kwargs
+ )
if t_eval is not None:
pybamm.logger.warning(
"Ignoring t_eval as solution times are specified by the experiment"
@@ -581,7 +634,7 @@ def solve(
+ timedelta(seconds=float(current_solution.t[-1]))
)
).total_seconds()
- if rest_time > pybamm.settings.step_start_offset:
+ if rest_time > 0:
# logs["step operating conditions"] = "Initial rest for padding"
# callbacks.on_step_start(logs)
@@ -738,7 +791,7 @@ def solve(
+ timedelta(seconds=float(step_solution.t[-1]))
)
).total_seconds()
- if rest_time > pybamm.settings.step_start_offset:
+ if rest_time > 0:
logs["step number"] = (step_num, cycle_length)
logs["step operating conditions"] = "Rest for padding"
callbacks.on_step_start(logs)
diff --git a/src/pybamm/solvers/__init__.py b/src/pybamm/solvers/__init__.py
index fc8be7e2f8..e9d9d306f4 100644
--- a/src/pybamm/solvers/__init__.py
+++ b/src/pybamm/solvers/__init__.py
@@ -2,4 +2,4 @@
'casadi_algebraic_solver', 'casadi_solver', 'dummy_solver',
'idaklu_jax', 'idaklu_solver', 'jax_bdf_solver', 'jax_solver',
'lrudict', 'processed_variable', 'processed_variable_computed',
- 'scipy_solver', 'solution']
+ 'scipy_solver', 'solution', 'processed_variable_time_integral']
diff --git a/src/pybamm/solvers/base_solver.py b/src/pybamm/solvers/base_solver.py
index efef7e9357..19aab65407 100644
--- a/src/pybamm/solvers/base_solver.py
+++ b/src/pybamm/solvers/base_solver.py
@@ -86,6 +86,14 @@ def supports_interp(self):
def root_method(self):
return self._root_method
+ @property
+ def supports_parallel_solve(self):
+ return False
+
+ @property
+ def requires_explicit_sensitivities(self):
+ return True
+
@root_method.setter
def root_method(self, method):
if method == "casadi":
@@ -137,7 +145,7 @@ def set_up(self, model, inputs=None, t_eval=None, ics_only=False):
# see if we need to form the explicit sensitivity equations
calculate_sensitivities_explicit = (
- model.calculate_sensitivities and not isinstance(self, pybamm.IDAKLUSolver)
+ model.calculate_sensitivities and self.requires_explicit_sensitivities
)
self._set_up_model_sensitivities_inplace(
@@ -490,11 +498,7 @@ def _set_up_model_sensitivities_inplace(
# if we have a mass matrix, we need to extend it
def extend_mass_matrix(M):
M_extend = [M.entries] * (num_parameters + 1)
- M_extend_pybamm = pybamm.Matrix(block_diag(M_extend, format="csr"))
- return M_extend_pybamm
-
- model.mass_matrix = extend_mass_matrix(model.mass_matrix)
- model.mass_matrix = extend_mass_matrix(model.mass_matrix)
+ return pybamm.Matrix(block_diag(M_extend, format="csr"))
model.mass_matrix = extend_mass_matrix(model.mass_matrix)
@@ -670,6 +674,33 @@ def calculate_consistent_state(self, model, time=0, inputs=None):
y0 = root_sol.all_ys[0]
return y0
+ def _solve_process_calculate_sensitivities_arg(
+ inputs, model, calculate_sensitivities
+ ):
+ # get a list-only version of calculate_sensitivities
+ if isinstance(calculate_sensitivities, bool):
+ if calculate_sensitivities:
+ calculate_sensitivities_list = [p for p in inputs.keys()]
+ else:
+ calculate_sensitivities_list = []
+ else:
+ calculate_sensitivities_list = calculate_sensitivities
+
+ calculate_sensitivities_list.sort()
+ if not hasattr(model, "calculate_sensitivities"):
+ model.calculate_sensitivities = []
+
+ # Check that calculate_sensitivites have not been updated
+ sensitivities_have_changed = (
+ calculate_sensitivities_list != model.calculate_sensitivities
+ )
+
+ # save sensitivity parameters so we can identify them later on
+ # (FYI: this is used in the Solution class)
+ model.calculate_sensitivities = calculate_sensitivities_list
+
+ return calculate_sensitivities_list, sensitivities_have_changed
+
def solve(
self,
model,
@@ -700,7 +731,11 @@ def solve(
calculate_sensitivities : list of str or bool, optional
Whether the solver calculates sensitivities of all input parameters. Defaults to False.
If only a subset of sensitivities are required, can also pass a
- list of input parameter names
+ list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances
+ so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you
+ require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities
+ do not take into account the movement of step-transitions wrt input parameters, so do not use this feature
+ if the timings of your experimental protocol change rapidly with respect to your input parameters.
t_interp : None, list or ndarray, optional
The times (in seconds) at which to interpolate the solution. Defaults to None.
Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`).
@@ -722,15 +757,6 @@ def solve(
"""
pybamm.logger.info(f"Start solving {model.name} with {self.name}")
- # get a list-only version of calculate_sensitivities
- if isinstance(calculate_sensitivities, bool):
- if calculate_sensitivities:
- calculate_sensitivities_list = [p for p in inputs.keys()]
- else:
- calculate_sensitivities_list = []
- else:
- calculate_sensitivities_list = calculate_sensitivities
-
# Make sure model isn't empty
self._check_empty_model(model)
@@ -772,6 +798,12 @@ def solve(
self._set_up_model_inputs(model, inputs) for inputs in inputs_list
]
+ calculate_sensitivities_list, sensitivities_have_changed = (
+ BaseSolver._solve_process_calculate_sensitivities_arg(
+ model_inputs_list[0], model, calculate_sensitivities
+ )
+ )
+
# (Re-)calculate consistent initialization
# Assuming initial conditions do not depend on input parameters
# when len(inputs_list) > 1, only `model_inputs_list[0]`
@@ -792,13 +824,8 @@ def solve(
"for initial conditions."
)
- # Check that calculate_sensitivites have not been updated
- calculate_sensitivities_list.sort()
- if hasattr(model, "calculate_sensitivities"):
- model.calculate_sensitivities.sort()
- else:
- model.calculate_sensitivities = []
- if calculate_sensitivities_list != model.calculate_sensitivities:
+ # if any setup configuration has changed, we need to re-set up
+ if sensitivities_have_changed:
self._model_set_up.pop(model, None)
# CasadiSolver caches its integrators using model, so delete this too
if isinstance(self, pybamm.CasadiSolver):
@@ -873,17 +900,8 @@ def solve(
pybamm.logger.verbose(
f"Calling solver for {t_eval[start_index]} < t < {t_eval[end_index - 1]}"
)
- ninputs = len(model_inputs_list)
- if ninputs == 1:
- new_solution = self._integrate(
- model,
- t_eval[start_index:end_index],
- model_inputs_list[0],
- t_interp=t_interp,
- )
- new_solutions = [new_solution]
- elif model.convert_to_format == "jax":
- # Jax can parallelize over the inputs efficiently
+ if self.supports_parallel_solve:
+ # Jax and IDAKLU solver can accept a list of inputs
new_solutions = self._integrate(
model,
t_eval[start_index:end_index],
@@ -891,18 +909,28 @@ def solve(
t_interp,
)
else:
- with mp.get_context(self._mp_context).Pool(processes=nproc) as p:
- new_solutions = p.starmap(
- self._integrate,
- zip(
- [model] * ninputs,
- [t_eval[start_index:end_index]] * ninputs,
- model_inputs_list,
- [t_interp] * ninputs,
- ),
+ ninputs = len(model_inputs_list)
+ if ninputs == 1:
+ new_solution = self._integrate(
+ model,
+ t_eval[start_index:end_index],
+ model_inputs_list[0],
+ t_interp=t_interp,
)
- p.close()
- p.join()
+ new_solutions = [new_solution]
+ else:
+ with mp.get_context(self._mp_context).Pool(processes=nproc) as p:
+ new_solutions = p.starmap(
+ self._integrate,
+ zip(
+ [model] * ninputs,
+ [t_eval[start_index:end_index]] * ninputs,
+ model_inputs_list,
+ [t_interp] * ninputs,
+ ),
+ )
+ p.close()
+ p.join()
# Setting the solve time for each segment.
# pybamm.Solution.__add__ assumes attribute solve_time.
solve_time = timer.time()
@@ -972,7 +1000,7 @@ def solve(
)
# Return solution(s)
- if ninputs == 1:
+ if len(solutions) == 1:
return solutions[0]
else:
return solutions
@@ -1066,6 +1094,58 @@ def _check_events_with_initialization(t_eval, model, inputs_dict):
f"Events {event_names} are non-positive at initial conditions"
)
+ def _set_sens_initial_conditions_from(
+ self, solution: pybamm.Solution, model: pybamm.BaseModel
+ ) -> tuple:
+ """
+ A restricted version of BaseModel.set_initial_conditions_from that only extracts the
+ sensitivities from a solution object, and only for a model that has been descretised.
+ This is used when setting the initial conditions for a sensitivity model.
+
+ Parameters
+ ----------
+ solution : :class:`pybamm.Solution`
+ The solution to use to initialize the model
+
+ model: :class:`pybamm.BaseModel`
+ The model whose sensitivities to set
+
+ Returns
+ -------
+
+ initial_conditions : tuple of ndarray
+ The initial conditions for the sensitivities, each element of the tuple
+ corresponds to an input parameter
+ """
+
+ ninputs = len(model.calculate_sensitivities)
+ initial_conditions = tuple([] for _ in range(ninputs))
+ solution = solution.last_state
+ for var in model.initial_conditions:
+ final_state = solution[var.name]
+ final_state = final_state.sensitivities
+ final_state_eval = tuple(
+ final_state[key] for key in model.calculate_sensitivities
+ )
+
+ scale, reference = var.scale.value, var.reference.value
+ for i in range(ninputs):
+ scaled_final_state_eval = (final_state_eval[i] - reference) / scale
+ initial_conditions[i].append(scaled_final_state_eval)
+
+ # Also update the concatenated initial conditions if the model is already
+ # discretised
+ # Unpack slices for sorting
+ y_slices = {var: slce for var, slce in model.y_slices.items()}
+ slices = [y_slices[symbol][0] for symbol in model.initial_conditions.keys()]
+
+ # sort equations according to slices
+ concatenated_initial_conditions = [
+ casadi.vertcat(*[eq for _, eq in sorted(zip(slices, init))])
+ for init in initial_conditions
+ ]
+ return concatenated_initial_conditions
+
def process_t_interp(self, t_interp):
# set a variable for this
no_interp = (not self.supports_interp) and (
@@ -1092,6 +1172,7 @@ def step(
npts=None,
inputs=None,
save=True,
+ calculate_sensitivities=False,
t_interp=None,
):
"""
@@ -1117,6 +1198,14 @@ def step(
Any input parameters to pass to the model when solving
save : bool, optional
Save solution with all previous timesteps. Defaults to True.
+ calculate_sensitivities : list of str or bool, optional
+ Whether the solver calculates sensitivities of all input parameters. Defaults to False.
+ If only a subset of sensitivities are required, can also pass a
+ list of input parameter names. **Limitations**: sensitivities are not calculated up to numerical tolerances
+ so are not guarenteed to be within the tolerances set by the solver, please raise an issue if you
+ require this functionality. Also, when using this feature with `pybamm.Experiment`, the sensitivities
+ do not take into account the movement of step-transitions wrt input parameters, so do not use this feature
+ if the timings of your experimental protocol change rapidly with respect to your input parameters.
t_interp : None, list or ndarray, optional
The times (in seconds) at which to interpolate the solution. Defaults to None.
Only valid for solvers that support intra-solve interpolation (`IDAKLUSolver`).
@@ -1142,12 +1231,9 @@ def step(
# Make sure model isn't empty
self._check_empty_model(model)
- # Make sure dt is greater than the offset
- step_start_offset = pybamm.settings.step_start_offset
- if dt <= step_start_offset:
- raise pybamm.SolverError(
- f"Step time must be at least {pybamm.TimerTime(step_start_offset)}"
- )
+ # Make sure dt is greater than zero
+ if dt <= 0:
+ raise pybamm.SolverError("Step time must be >0")
# Raise deprecation warning for npts and convert it to t_eval
if npts is not None:
@@ -1176,11 +1262,11 @@ def step(
if t_start == 0:
t_start_shifted = t_start
else:
- # offset t_start by t_start_offset (default 1 ns)
+ # find the next largest floating point value for t_start
# to avoid repeated times in the solution
# from having the same time at the end of the previous step and
# the start of the next step
- t_start_shifted = t_start + step_start_offset
+ t_start_shifted = np.nextafter(t_start, np.inf)
t_eval[0] = t_start_shifted
if t_interp.size > 0 and t_interp[0] == t_start:
t_interp[0] = t_start_shifted
@@ -1191,8 +1277,15 @@ def step(
# Set up inputs
model_inputs = self._set_up_model_inputs(model, inputs)
+ # process calculate_sensitivities argument
+ calculate_sensitivities_list, sensitivities_have_changed = (
+ BaseSolver._solve_process_calculate_sensitivities_arg(
+ model_inputs, model, calculate_sensitivities
+ )
+ )
+
first_step_this_model = model not in self._model_set_up
- if first_step_this_model:
+ if first_step_this_model or sensitivities_have_changed:
if len(self._model_set_up) > 0:
existing_model = next(iter(self._model_set_up))
raise RuntimeError(
@@ -1211,18 +1304,45 @@ def step(
):
pybamm.logger.verbose(f"Start stepping {model.name} with {self.name}")
+ using_sensitivities = len(model.calculate_sensitivities) > 0
+
if isinstance(old_solution, pybamm.EmptySolution):
if not first_step_this_model:
# reset y0 to original initial conditions
self.set_up(model, model_inputs, ics_only=True)
elif old_solution.all_models[-1] == model:
- # initialize with old solution
- model.y0 = old_solution.all_ys[-1][:, -1]
+ last_state = old_solution.last_state
+ model.y0 = last_state.all_ys[0]
+ if using_sensitivities and isinstance(last_state._all_sensitivities, dict):
+ full_sens = last_state._all_sensitivities["all"][0]
+ model.y0S = tuple(full_sens[:, i] for i in range(full_sens.shape[1]))
+
else:
_, concatenated_initial_conditions = model.set_initial_conditions_from(
old_solution, return_type="ics"
)
model.y0 = concatenated_initial_conditions.evaluate(0, inputs=model_inputs)
+ if using_sensitivities:
+ model.y0S = self._set_sens_initial_conditions_from(old_solution, model)
+
+ # hopefully we'll get rid of explicit sensitivities soon so we can remove this
+ explicit_sensitivities = model.len_rhs_sens > 0 or model.len_alg_sens > 0
+ if (
+ explicit_sensitivities
+ and using_sensitivities
+ and not isinstance(old_solution, pybamm.EmptySolution)
+ and not old_solution.all_models[-1] == model
+ ):
+ y0_list = []
+ if model.len_rhs > 0:
+ y0_list.append(model.y0[: model.len_rhs])
+ for s in model.y0S:
+ y0_list.append(s[: model.len_rhs])
+ if model.len_alg > 0:
+ y0_list.append(model.y0[model.len_rhs :])
+ for s in model.y0S:
+ y0_list.append(s[model.len_rhs :])
+ model.y0 = casadi.vertcat(*y0_list)
set_up_time = timer.time()
@@ -1235,7 +1355,13 @@ def step(
# Step
pybamm.logger.verbose(f"Stepping for {t_start_shifted:.0f} < t < {t_end:.0f}")
timer.reset()
- solution = self._integrate(model, t_eval, model_inputs, t_interp)
+
+ # API for _integrate is different for JaxSolver and IDAKLUSolver
+ if self.supports_parallel_solve:
+ solutions = self._integrate(model, t_eval, [model_inputs], t_interp)
+ solution = solutions[0]
+ else:
+ solution = self._integrate(model, t_eval, model_inputs, t_interp)
solution.solve_time = timer.time()
# Check if extrapolation occurred
@@ -1326,6 +1452,7 @@ def get_termination_reason(solution, events):
solution.t_event,
solution.y_event,
solution.termination,
+ variables_returned=solution.variables_returned,
)
event_sol.solve_time = 0
event_sol.integration_time = 0
@@ -1363,8 +1490,12 @@ def check_extrapolation(self, solution, events):
# second pass: check if the extrapolation events are within the tolerance
last_state = solution.last_state
- t = last_state.all_ts[0][0]
- y = last_state.all_ys[0][:, 0]
+ if solution.t_event:
+ t = solution.t_event[0]
+ y = solution.y_event[:, 0]
+ else:
+ t = last_state.all_ts[0][0]
+ y = last_state.all_ys[0][:, 0]
inputs = last_state.all_inputs[0]
if isinstance(y, casadi.DM):
diff --git a/src/pybamm/solvers/c_solvers/idaklu.cpp b/src/pybamm/solvers/c_solvers/idaklu.cpp
index 3ef0194403..82a3cbe91c 100644
--- a/src/pybamm/solvers/c_solvers/idaklu.cpp
+++ b/src/pybamm/solvers/c_solvers/idaklu.cpp
@@ -9,6 +9,8 @@
#include
#include "idaklu/idaklu_solver.hpp"
+#include "idaklu/observe.hpp"
+#include "idaklu/IDAKLUSolverGroup.hpp"
#include "idaklu/IdakluJax.hpp"
#include "idaklu/common.hpp"
#include "idaklu/Expressions/Casadi/CasadiFunctions.hpp"
@@ -26,15 +28,19 @@ casadi::Function generate_casadi_function(const std::string &data)
namespace py = pybind11;
PYBIND11_MAKE_OPAQUE(std::vector);
+PYBIND11_MAKE_OPAQUE(std::vector);
+PYBIND11_MAKE_OPAQUE(std::vector);
PYBIND11_MODULE(idaklu, m)
{
m.doc() = "sundials solvers"; // optional module docstring
py::bind_vector>(m, "VectorNdArray");
+ py::bind_vector>(m, "VectorRealtypeNdArray");
+ py::bind_vector>(m, "VectorSolution");
- py::class_(m, "IDAKLUSolver")
- .def("solve", &IDAKLUSolver::solve,
+ py::class_(m, "IDAKLUSolverGroup")
+ .def("solve", &IDAKLUSolverGroup::solve,
"perform a solve",
py::arg("t_eval"),
py::arg("t_interp"),
@@ -43,8 +49,8 @@ PYBIND11_MODULE(idaklu, m)
py::arg("inputs"),
py::return_value_policy::take_ownership);
- m.def("create_casadi_solver", &create_idaklu_solver,
- "Create a casadi idaklu solver object",
+ m.def("create_casadi_solver_group", &create_idaklu_solver_group,
+ "Create a group of casadi idaklu solver objects",
py::arg("number_of_states"),
py::arg("number_of_parameters"),
py::arg("rhs_alg"),
@@ -69,9 +75,30 @@ PYBIND11_MODULE(idaklu, m)
py::arg("options"),
py::return_value_policy::take_ownership);
+ m.def("observe", &observe,
+ "Observe variables",
+ py::arg("ts"),
+ py::arg("ys"),
+ py::arg("inputs"),
+ py::arg("funcs"),
+ py::arg("is_f_contiguous"),
+ py::arg("shape"),
+ py::return_value_policy::take_ownership);
+
+ m.def("observe_hermite_interp", &observe_hermite_interp,
+ "Observe and Hermite interpolate variables",
+ py::arg("t_interp"),
+ py::arg("ts"),
+ py::arg("ys"),
+ py::arg("yps"),
+ py::arg("inputs"),
+ py::arg("funcs"),
+ py::arg("shape"),
+ py::return_value_policy::take_ownership);
+
#ifdef IREE_ENABLE
- m.def("create_iree_solver", &create_idaklu_solver,
- "Create a iree idaklu solver object",
+ m.def("create_iree_solver_group", &create_idaklu_solver_group,
+ "Create a group of iree idaklu solver objects",
py::arg("number_of_states"),
py::arg("number_of_parameters"),
py::arg("rhs_alg"),
@@ -164,7 +191,9 @@ PYBIND11_MODULE(idaklu, m)
py::class_(m, "solution")
.def_readwrite("t", &Solution::t)
.def_readwrite("y", &Solution::y)
+ .def_readwrite("yp", &Solution::yp)
.def_readwrite("yS", &Solution::yS)
+ .def_readwrite("ypS", &Solution::ypS)
.def_readwrite("y_term", &Solution::y_term)
.def_readwrite("flag", &Solution::flag);
}
diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp
index 29b451e6d3..379d64783a 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolver.hpp
@@ -2,7 +2,8 @@
#define PYBAMM_IDAKLU_CASADI_SOLVER_HPP
#include "common.hpp"
-#include "Solution.hpp"
+#include "SolutionData.hpp"
+
/**
* Abstract base class for solutions that can use different solvers and vector
@@ -24,14 +25,17 @@ class IDAKLUSolver
~IDAKLUSolver() = default;
/**
- * @brief Abstract solver method that returns a Solution class
+ * @brief Abstract solver method that executes the solver
*/
- virtual Solution solve(
- np_array t_eval_np,
- np_array t_interp_np,
- np_array y0_np,
- np_array yp0_np,
- np_array_dense inputs) = 0;
+ virtual SolutionData solve(
+ const std::vector &t_eval,
+ const std::vector &t_interp,
+ const realtype *y0,
+ const realtype *yp0,
+ const realtype *inputs,
+ bool save_adaptive_steps,
+ bool save_interp_steps
+ ) = 0;
/**
* Abstract method to initialize the solver, once vectors and solver classes
diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.cpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.cpp
new file mode 100644
index 0000000000..8a76d73cfe
--- /dev/null
+++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.cpp
@@ -0,0 +1,145 @@
+#include "IDAKLUSolverGroup.hpp"
+#include
+#include
+
+std::vector IDAKLUSolverGroup::solve(
+ np_array t_eval_np,
+ np_array t_interp_np,
+ np_array y0_np,
+ np_array yp0_np,
+ np_array inputs) {
+ DEBUG("IDAKLUSolverGroup::solve");
+
+ // If t_interp is empty, save all adaptive steps
+ bool save_adaptive_steps = t_interp_np.size() == 0;
+
+ const realtype* t_eval_begin = t_eval_np.data();
+ const realtype* t_eval_end = t_eval_begin + t_eval_np.size();
+ const realtype* t_interp_begin = t_interp_np.data();
+ const realtype* t_interp_end = t_interp_begin + t_interp_np.size();
+
+ // Process the time inputs
+ // 1. Get the sorted and unique t_eval vector
+ auto const t_eval = makeSortedUnique(t_eval_begin, t_eval_end);
+
+ // 2.1. Get the sorted and unique t_interp vector
+ auto const t_interp_unique_sorted = makeSortedUnique(t_interp_begin, t_interp_end);
+
+ // 2.2 Remove the t_eval values from t_interp
+ auto const t_interp_setdiff = setDiff(t_interp_unique_sorted.begin(), t_interp_unique_sorted.end(), t_eval_begin, t_eval_end);
+
+ // 2.3 Finally, get the sorted and unique t_interp vector with t_eval values removed
+ auto const t_interp = makeSortedUnique(t_interp_setdiff.begin(), t_interp_setdiff.end());
+
+ int const number_of_evals = t_eval.size();
+ int const number_of_interps = t_interp.size();
+
+ // setDiff removes entries of t_interp that overlap with
+ // t_eval, so we need to check if we need to interpolate any unique points.
+ // This is not the same as save_adaptive_steps since some entries of t_interp
+ // may be removed by setDiff
+ bool save_interp_steps = number_of_interps > 0;
+
+ // 3. Check if the timestepping entries are valid
+ if (number_of_evals < 2) {
+ throw std::invalid_argument(
+ "t_eval must have at least 2 entries"
+ );
+ } else if (save_interp_steps) {
+ if (t_interp.front() < t_eval.front()) {
+ throw std::invalid_argument(
+ "t_interp values must be greater than the smallest t_eval value: "
+ + std::to_string(t_eval.front())
+ );
+ } else if (t_interp.back() > t_eval.back()) {
+ throw std::invalid_argument(
+ "t_interp values must be less than the greatest t_eval value: "
+ + std::to_string(t_eval.back())
+ );
+ }
+ }
+
+ auto n_coeffs = number_of_states + number_of_parameters * number_of_states;
+
+ // check y0 and yp0 and inputs have the correct dimensions
+ if (y0_np.ndim() != 2)
+ throw std::domain_error("y0 has wrong number of dimensions. Expected 2 but got " + std::to_string(y0_np.ndim()));
+ if (yp0_np.ndim() != 2)
+ throw std::domain_error("yp0 has wrong number of dimensions. Expected 2 but got " + std::to_string(yp0_np.ndim()));
+ if (inputs.ndim() != 2)
+ throw std::domain_error("inputs has wrong number of dimensions. Expected 2 but got " + std::to_string(inputs.ndim()));
+
+ auto number_of_groups = y0_np.shape()[0];
+
+ // check y0 and yp0 and inputs have the correct shape
+ if (y0_np.shape()[1] != n_coeffs)
+ throw std::domain_error(
+ "y0 has wrong number of cols. Expected " + std::to_string(n_coeffs) +
+ " but got " + std::to_string(y0_np.shape()[1]));
+
+ if (yp0_np.shape()[1] != n_coeffs)
+ throw std::domain_error(
+ "yp0 has wrong number of cols. Expected " + std::to_string(n_coeffs) +
+ " but got " + std::to_string(yp0_np.shape()[1]));
+
+ if (yp0_np.shape()[0] != number_of_groups)
+ throw std::domain_error(
+ "yp0 has wrong number of rows. Expected " + std::to_string(number_of_groups) +
+ " but got " + std::to_string(yp0_np.shape()[0]));
+
+ if (inputs.shape()[0] != number_of_groups)
+ throw std::domain_error(
+ "inputs has wrong number of rows. Expected " + std::to_string(number_of_groups) +
+ " but got " + std::to_string(inputs.shape()[0]));
+
+ const std::size_t solves_per_thread = number_of_groups / m_solvers.size();
+ const std::size_t remainder_solves = number_of_groups % m_solvers.size();
+
+ const realtype *y0 = y0_np.data();
+ const realtype *yp0 = yp0_np.data();
+ const realtype *inputs_data = inputs.data();
+
+ std::vector results(number_of_groups);
+
+ std::optional exception;
+
+ omp_set_num_threads(m_solvers.size());
+ #pragma omp parallel for
+ for (int i = 0; i < m_solvers.size(); i++) {
+ try {
+ for (int j = 0; j < solves_per_thread; j++) {
+ const std::size_t index = i * solves_per_thread + j;
+ const realtype *y = y0 + index * y0_np.shape(1);
+ const realtype *yp = yp0 + index * yp0_np.shape(1);
+ const realtype *input = inputs_data + index * inputs.shape(1);
+ results[index] = m_solvers[i]->solve(t_eval, t_interp, y, yp, input, save_adaptive_steps, save_interp_steps);
+ }
+ } catch (std::exception &e) {
+ // If an exception is thrown, we need to catch it and rethrow it outside the parallel region
+ #pragma omp critical
+ {
+ exception = e;
+ }
+ }
+ }
+
+ if (exception.has_value()) {
+ py::set_error(PyExc_ValueError, exception->what());
+ throw py::error_already_set();
+ }
+
+ for (int i = 0; i < remainder_solves; i++) {
+ const std::size_t index = number_of_groups - remainder_solves + i;
+ const realtype *y = y0 + index * y0_np.shape(1);
+ const realtype *yp = yp0 + index * yp0_np.shape(1);
+ const realtype *input = inputs_data + index * inputs.shape(1);
+ results[index] = m_solvers[i]->solve(t_eval, t_interp, y, yp, input, save_adaptive_steps, save_interp_steps);
+ }
+
+ // create solutions (needs to be serial as we're using the Python GIL)
+ std::vector solutions(number_of_groups);
+ for (int i = 0; i < number_of_groups; i++) {
+ solutions[i] = results[i].generate_solution();
+ }
+ return solutions;
+}
diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.hpp
new file mode 100644
index 0000000000..609b3b6fca
--- /dev/null
+++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverGroup.hpp
@@ -0,0 +1,48 @@
+#ifndef PYBAMM_IDAKLU_SOLVER_GROUP_HPP
+#define PYBAMM_IDAKLU_SOLVER_GROUP_HPP
+
+#include "IDAKLUSolver.hpp"
+#include "common.hpp"
+
+/**
+ * @brief class for a group of solvers.
+ */
+class IDAKLUSolverGroup
+{
+public:
+
+ /**
+ * @brief Default constructor
+ */
+ IDAKLUSolverGroup(std::vector> solvers, int number_of_states, int number_of_parameters):
+ m_solvers(std::move(solvers)),
+ number_of_states(number_of_states),
+ number_of_parameters(number_of_parameters)
+ {}
+
+ // no copy constructor (unique_ptr cannot be copied)
+ IDAKLUSolverGroup(IDAKLUSolverGroup &) = delete;
+
+ /**
+ * @brief Default destructor
+ */
+ ~IDAKLUSolverGroup() = default;
+
+ /**
+ * @brief solver method that returns a vector of Solutions
+ */
+ std::vector solve(
+ np_array t_eval_np,
+ np_array t_interp_np,
+ np_array y0_np,
+ np_array yp0_np,
+ np_array inputs);
+
+
+ private:
+ std::vector> m_solvers;
+ int number_of_states;
+ int number_of_parameters;
+};
+
+#endif // PYBAMM_IDAKLU_SOLVER_GROUP_HPP
diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp
index ca710fbff6..ee2c03abff 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.hpp
@@ -52,10 +52,11 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
int const number_of_states; // cppcheck-suppress unusedStructMember
int const number_of_parameters; // cppcheck-suppress unusedStructMember
int const number_of_events; // cppcheck-suppress unusedStructMember
+ int number_of_timesteps;
int precon_type; // cppcheck-suppress unusedStructMember
- N_Vector yy, yp, avtol; // y, y', and absolute tolerance
+ N_Vector yy, yyp, y_cache, avtol; // y, y', y cache vector, and absolute tolerance
N_Vector *yyS; // cppcheck-suppress unusedStructMember
- N_Vector *ypS; // cppcheck-suppress unusedStructMember
+ N_Vector *yypS; // cppcheck-suppress unusedStructMember
N_Vector id; // rhs_alg_id
realtype rtol;
int const jac_times_cjmass_nnz; // cppcheck-suppress unusedStructMember
@@ -69,10 +70,14 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
vector res_dvar_dp;
bool const sensitivity; // cppcheck-suppress unusedStructMember
bool const save_outputs_only; // cppcheck-suppress unusedStructMember
+ bool save_hermite; // cppcheck-suppress unusedStructMember
+ bool is_ODE; // cppcheck-suppress unusedStructMember
int length_of_return_vector; // cppcheck-suppress unusedStructMember
vector t; // cppcheck-suppress unusedStructMember
vector> y; // cppcheck-suppress unusedStructMember
+ vector> yp; // cppcheck-suppress unusedStructMember
vector>> yS; // cppcheck-suppress unusedStructMember
+ vector>> ypS; // cppcheck-suppress unusedStructMember
SetupOptions const setup_opts;
SolverOptions const solver_opts;
@@ -106,12 +111,16 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
/**
* @brief The main solve method that solves for each variable and time step
*/
- Solution solve(
- np_array t_eval_np,
- np_array t_interp_np,
- np_array y0_np,
- np_array yp0_np,
- np_array_dense inputs) override;
+ SolutionData solve(
+ const std::vector &t_eval,
+ const std::vector &t_interp,
+ const realtype *y0,
+ const realtype *yp0,
+ const realtype *inputs,
+ bool save_adaptive_steps,
+ bool save_interp_steps
+ ) override;
+
/**
* @brief Concrete implementation of initialization method
@@ -138,6 +147,11 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
*/
void InitializeStorage(int const N);
+ /**
+ * @brief Initialize the storage for Hermite interpolation
+ */
+ void InitializeHermiteStorage(int const N);
+
/**
* @brief Apply user-configurable IDA options
*/
@@ -153,18 +167,51 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
*/
void PrintStats();
+ /**
+ * @brief Set a consistent initialization for ODEs
+ */
+ void ReinitializeIntegrator(const realtype& t_val);
+
+ /**
+ * @brief Set a consistent initialization for the system of equations
+ */
+ void ConsistentInitialization(
+ const realtype& t_val,
+ const realtype& t_next,
+ const int& icopt);
+
+ /**
+ * @brief Set a consistent initialization for DAEs
+ */
+ void ConsistentInitializationDAE(
+ const realtype& t_val,
+ const realtype& t_next,
+ const int& icopt);
+
+ /**
+ * @brief Set a consistent initialization for ODEs
+ */
+ void ConsistentInitializationODE(const realtype& t_val);
+
/**
* @brief Extend the adaptive arrays by 1
*/
void ExtendAdaptiveArrays();
+ /**
+ * @brief Extend the Hermite interpolation info by 1
+ */
+ void ExtendHermiteArrays();
+
/**
* @brief Set the step values
*/
void SetStep(
- realtype &t_val,
+ realtype &tval,
realtype *y_val,
+ realtype *yp_val,
vector const &yS_val,
+ vector const &ypS_val,
int &i_save
);
@@ -179,7 +226,9 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
realtype &t_prev,
realtype const &t_next,
realtype *y_val,
+ realtype *yp_val,
vector const &yS_val,
+ vector const &ypS_val,
int &i_save
);
@@ -223,6 +272,26 @@ class IDAKLUSolverOpenMP : public IDAKLUSolver
int &i_save
);
+ /**
+ * @brief Save the output function results at the requested time
+ */
+ void SetStepHermite(
+ realtype &t_val,
+ realtype *yp_val,
+ const vector &ypS_val,
+ int &i_save
+ );
+
+ /**
+ * @brief Save the output function sensitivities at the requested time
+ */
+ void SetStepHermiteSensitivities(
+ realtype &t_val,
+ realtype *yp_val,
+ const vector &ypS_val,
+ int &i_save
+ );
+
};
#include "IDAKLUSolverOpenMP.inl"
diff --git a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl
index 7ed4dcfad8..d128ae1809 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl
+++ b/src/pybamm/solvers/c_solvers/idaklu/IDAKLUSolverOpenMP.inl
@@ -1,8 +1,8 @@
#include "Expressions/Expressions.hpp"
#include "sundials_functions.hpp"
#include
-
#include "common.hpp"
+#include "SolutionData.hpp"
template
IDAKLUSolverOpenMP::IDAKLUSolverOpenMP(
@@ -47,7 +47,7 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP(
AllocateVectors();
if (sensitivity) {
yyS = N_VCloneVectorArray(number_of_parameters, yy);
- ypS = N_VCloneVectorArray(number_of_parameters, yp);
+ yypS = N_VCloneVectorArray(number_of_parameters, yyp);
}
// set initial values
realtype *atval = N_VGetArrayPointer(avtol);
@@ -57,14 +57,14 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP(
for (int is = 0; is < number_of_parameters; is++) {
N_VConst(RCONST(0.0), yyS[is]);
- N_VConst(RCONST(0.0), ypS[is]);
+ N_VConst(RCONST(0.0), yypS[is]);
}
// create Matrix objects
SetMatrix();
// initialise solver
- IDAInit(ida_mem, residual_eval, 0, yy, yp);
+ IDAInit(ida_mem, residual_eval, 0, yy, yyp);
// set tolerances
rtol = RCONST(rel_tol);
@@ -82,15 +82,33 @@ IDAKLUSolverOpenMP::IDAKLUSolverOpenMP(
if (this->setup_opts.preconditioner != "none") {
precon_type = SUN_PREC_LEFT;
}
+
+ // The default is to solve a DAE for generality. This may be changed
+ // to an ODE during the Initialize() call
+ is_ODE = false;
+
+ // Will be overwritten during the solve() call
+ save_hermite = solver_opts.hermite_interpolation;
}
template
void IDAKLUSolverOpenMP::AllocateVectors() {
+ DEBUG("IDAKLUSolverOpenMP::AllocateVectors (num_threads = " << setup_opts.num_threads << ")");
// Create vectors
- yy = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
- yp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
- avtol = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
- id = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ if (setup_opts.num_threads == 1) {
+ yy = N_VNew_Serial(number_of_states, sunctx);
+ yyp = N_VNew_Serial(number_of_states, sunctx);
+ y_cache = N_VNew_Serial(number_of_states, sunctx);
+ avtol = N_VNew_Serial(number_of_states, sunctx);
+ id = N_VNew_Serial(number_of_states, sunctx);
+ } else {
+ DEBUG("IDAKLUSolverOpenMP::AllocateVectors OpenMP");
+ yy = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ yyp = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ y_cache = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ avtol = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ id = N_VNew_OpenMP(number_of_states, setup_opts.num_threads, sunctx);
+ }
}
template
@@ -111,6 +129,26 @@ void IDAKLUSolverOpenMP::InitializeStorage(int const N) {
vector(length_of_return_vector, 0.0)
)
);
+
+ if (save_hermite) {
+ InitializeHermiteStorage(N);
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::InitializeHermiteStorage(int const N) {
+ yp = vector>(
+ N,
+ vector(number_of_states, 0.0)
+ );
+
+ ypS = vector>>(
+ N,
+ vector>(
+ number_of_parameters,
+ vector(number_of_states, 0.0)
+ )
+ );
}
template
@@ -269,7 +307,7 @@ void IDAKLUSolverOpenMP::Initialize() {
if (sensitivity) {
CheckErrors(IDASensInit(ida_mem, number_of_parameters, IDA_SIMULTANEOUS,
- sensitivities_eval, yyS, ypS));
+ sensitivities_eval, yyS, yypS));
CheckErrors(IDASensEEtolerances(ida_mem));
}
@@ -279,9 +317,13 @@ void IDAKLUSolverOpenMP::Initialize() {
realtype *id_val;
id_val = N_VGetArrayPointer(id);
- int ii;
- for (ii = 0; ii < number_of_states; ii++) {
+ // Determine if the system is an ODE
+ is_ODE = number_of_states > 0;
+ for (int ii = 0; ii < number_of_states; ii++) {
id_val[ii] = id_np_val[ii];
+ // check if id_val[ii] approximately equals 1 (>0.999) handles
+ // cases where id_val[ii] is not exactly 1 due to numerical errors
+ is_ODE &= id_val[ii] > 0.999;
}
// Variable types: differential (1) and algebraic (0)
@@ -290,6 +332,7 @@ void IDAKLUSolverOpenMP::Initialize() {
template
IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() {
+ DEBUG("IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP");
// Free memory
if (sensitivity) {
IDASensFree(ida_mem);
@@ -300,12 +343,13 @@ IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() {
SUNMatDestroy(J);
N_VDestroy(avtol);
N_VDestroy(yy);
- N_VDestroy(yp);
+ N_VDestroy(yyp);
+ N_VDestroy(y_cache);
N_VDestroy(id);
if (sensitivity) {
N_VDestroyVectorArray(yyS, number_of_parameters);
- N_VDestroyVectorArray(ypS, number_of_parameters);
+ N_VDestroyVectorArray(yypS, number_of_parameters);
}
IDAFree(&ida_mem);
@@ -313,61 +357,28 @@ IDAKLUSolverOpenMP::~IDAKLUSolverOpenMP() {
}
template
-Solution IDAKLUSolverOpenMP::solve(
- np_array t_eval_np,
- np_array t_interp_np,
- np_array y0_np,
- np_array yp0_np,
- np_array_dense inputs
+SolutionData IDAKLUSolverOpenMP::solve(
+ const std::vector &t_eval,
+ const std::vector &t_interp,
+ const realtype *y0,
+ const realtype *yp0,
+ const realtype *inputs,
+ bool save_adaptive_steps,
+ bool save_interp_steps
)
{
DEBUG("IDAKLUSolver::solve");
+ const int number_of_evals = t_eval.size();
+ const int number_of_interps = t_interp.size();
+
+ // Hermite interpolation is only available when saving
+ // 1. adaptive steps and 2. the full solution
+ save_hermite = (
+ solver_opts.hermite_interpolation &&
+ save_adaptive_steps &&
+ !save_outputs_only
+ );
- // If t_interp is empty, save all adaptive steps
- bool save_adaptive_steps = t_interp_np.unchecked<1>().size() == 0;
-
- // Process the time inputs
- // 1. Get the sorted and unique t_eval vector
- auto const t_eval = makeSortedUnique(t_eval_np);
-
- // 2.1. Get the sorted and unique t_interp vector
- auto const t_interp_unique_sorted = makeSortedUnique(t_interp_np);
-
- // 2.2 Remove the t_eval values from t_interp
- auto const t_interp_setdiff = setDiff(t_interp_unique_sorted, t_eval);
-
- // 2.3 Finally, get the sorted and unique t_interp vector with t_eval values removed
- auto const t_interp = makeSortedUnique(t_interp_setdiff);
-
- int const number_of_evals = t_eval.size();
- int const number_of_interps = t_interp.size();
-
- // setDiff removes entries of t_interp that overlap with
- // t_eval, so we need to check if we need to interpolate any unique points.
- // This is not the same as save_adaptive_steps since some entries of t_interp
- // may be removed by setDiff
- bool save_interp_steps = number_of_interps > 0;
-
- // 3. Check if the timestepping entries are valid
- if (number_of_evals < 2) {
- throw std::invalid_argument(
- "t_eval must have at least 2 entries"
- );
- } else if (save_interp_steps) {
- if (t_interp.front() < t_eval.front()) {
- throw std::invalid_argument(
- "t_interp values must be greater than the smallest t_eval value: "
- + std::to_string(t_eval.front())
- );
- } else if (t_interp.back() > t_eval.back()) {
- throw std::invalid_argument(
- "t_interp values must be less than the greatest t_eval value: "
- + std::to_string(t_eval.back())
- );
- }
- }
-
- // Initialize length_of_return_vector, t, y, and yS
InitializeStorage(number_of_evals + number_of_interps);
int i_save = 0;
@@ -386,34 +397,21 @@ Solution IDAKLUSolverOpenMP::solve(
t_interp_next = t_interp[0];
}
- auto y0 = y0_np.unchecked<1>();
- auto yp0 = yp0_np.unchecked<1>();
auto n_coeffs = number_of_states + number_of_parameters * number_of_states;
- if (y0.size() != n_coeffs) {
- throw std::domain_error(
- "y0 has wrong size. Expected " + std::to_string(n_coeffs) +
- " but got " + std::to_string(y0.size()));
- } else if (yp0.size() != n_coeffs) {
- throw std::domain_error(
- "yp0 has wrong size. Expected " + std::to_string(n_coeffs) +
- " but got " + std::to_string(yp0.size()));
- }
-
// set inputs
- auto p_inputs = inputs.unchecked<2>();
for (int i = 0; i < functions->inputs.size(); i++) {
- functions->inputs[i] = p_inputs(i, 0);
+ functions->inputs[i] = inputs[i];
}
// Setup consistent initialization
realtype *y_val = N_VGetArrayPointer(yy);
- realtype *yp_val = N_VGetArrayPointer(yp);
+ realtype *yp_val = N_VGetArrayPointer(yyp);
vector yS_val(number_of_parameters);
vector ypS_val(number_of_parameters);
for (int p = 0 ; p < number_of_parameters; p++) {
yS_val[p] = N_VGetArrayPointer(yyS[p]);
- ypS_val[p] = N_VGetArrayPointer(ypS[p]);
+ ypS_val[p] = N_VGetArrayPointer(yypS[p]);
for (int i = 0; i < number_of_states; i++) {
yS_val[p][i] = y0[i + (p + 1) * number_of_states];
ypS_val[p][i] = yp0[i + (p + 1) * number_of_states];
@@ -427,40 +425,39 @@ Solution IDAKLUSolverOpenMP::solve(
SetSolverOptions();
- CheckErrors(IDAReInit(ida_mem, t0, yy, yp));
- if (sensitivity) {
- CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS));
- }
-
// Prepare first time step
i_eval = 1;
realtype t_eval_next = t_eval[i_eval];
+
// Consistent initialization
+ ReinitializeIntegrator(t0);
int const init_type = solver_opts.init_all_y_ic ? IDA_Y_INIT : IDA_YA_YDP_INIT;
if (solver_opts.calc_ic) {
- DEBUG("IDACalcIC");
- // IDACalcIC will throw a warning if it fails to find initial conditions
- IDACalcIC(ida_mem, init_type, t_eval_next);
+ ConsistentInitialization(t0, t_eval_next, init_type);
}
+ // Set the initial stop time
+ IDASetStopTime(ida_mem, t_eval_next);
+
+ // Progress one step. This must be done before the while loop to ensure
+ // that we can run IDAGetDky at t0 for dky = 1
+ int retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP);
+
+ // Store consistent initialization
+ CheckErrors(IDAGetDky(ida_mem, t0, 0, yy));
if (sensitivity) {
- CheckErrors(IDAGetSens(ida_mem, &t_val, yyS));
+ CheckErrors(IDAGetSensDky(ida_mem, t0, 0, yyS));
}
- // Store Consistent initialization
- SetStep(t0, y_val, yS_val, i_save);
+ SetStep(t0, y_val, yp_val, yS_val, ypS_val, i_save);
- // Set the initial stop time
- IDASetStopTime(ida_mem, t_eval_next);
+ // Reset the states at t = t_val. Sensitivities are handled in the while-loop
+ CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy));
// Solve the system
- int retval;
DEBUG("IDASolve");
while (true) {
- // Progress one step
- retval = IDASolve(ida_mem, tf, &t_val, yy, yp, IDA_ONE_STEP);
-
if (retval < 0) {
// failed
break;
@@ -478,37 +475,45 @@ Solution IDAKLUSolverOpenMP::solve(
bool hit_adaptive = save_adaptive_steps && retval == IDA_SUCCESS;
if (sensitivity) {
- CheckErrors(IDAGetSens(ida_mem, &t_val, yyS));
+ CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS));
}
if (hit_tinterp) {
// Save the interpolated state at t_prev < t < t_val, for all t in t_interp
- SetStepInterp(i_interp,
+ SetStepInterp(
+ i_interp,
t_interp_next,
t_interp,
t_val,
t_prev,
t_eval_next,
y_val,
+ yp_val,
yS_val,
+ ypS_val,
i_save);
}
- if (hit_adaptive || hit_teval || hit_event) {
+ if (hit_adaptive || hit_teval || hit_event || hit_final_time) {
if (hit_tinterp) {
// Reset the states and sensitivities at t = t_val
CheckErrors(IDAGetDky(ida_mem, t_val, 0, yy));
if (sensitivity) {
- CheckErrors(IDAGetSens(ida_mem, &t_val, yyS));
+ CheckErrors(IDAGetSensDky(ida_mem, t_val, 0, yyS));
}
}
// Save the current state at t_val
- if (hit_adaptive) {
- // Dynamically allocate memory for the adaptive step
- ExtendAdaptiveArrays();
+ // First, check to make sure that the t_val is not equal to the current t value
+ // If it is, we don't want to save the current state twice
+ if (!hit_tinterp || t_val != t.back()) {
+ if (hit_adaptive) {
+ // Dynamically allocate memory for the adaptive step
+ ExtendAdaptiveArrays();
+ }
+
+ SetStep(t_val, y_val, yp_val, yS_val, ypS_val, i_save);
}
- SetStep(t_val, y_val, yS_val, i_save);
}
if (hit_final_time || hit_event) {
@@ -516,20 +521,19 @@ Solution IDAKLUSolverOpenMP::solve(
break;
} else if (hit_teval) {
// Set the next stop time
- i_eval += 1;
+ i_eval++;
t_eval_next = t_eval[i_eval];
CheckErrors(IDASetStopTime(ida_mem, t_eval_next));
// Reinitialize the solver to deal with the discontinuity at t = t_val.
- // We must reinitialize the algebraic terms, so do not use init_type.
- IDACalcIC(ida_mem, IDA_YA_YDP_INIT, t_eval_next);
- CheckErrors(IDAReInit(ida_mem, t_val, yy, yp));
- if (sensitivity) {
- CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, ypS));
- }
+ ReinitializeIntegrator(t_val);
+ ConsistentInitialization(t_val, t_eval_next, IDA_YA_YDP_INIT);
}
t_prev = t_val;
+
+ // Progress one step
+ retval = IDASolve(ida_mem, tf, &t_val, yy, yyp, IDA_ONE_STEP);
}
int const length_of_final_sv_slice = save_outputs_only ? number_of_states : 0;
@@ -543,8 +547,8 @@ Solution IDAKLUSolverOpenMP::solve(
PrintStats();
}
- int const number_of_timesteps = i_save;
- int count;
+ // store number of timesteps so we can generate the solution later
+ number_of_timesteps = i_save;
// Copy the data to return as numpy arrays
@@ -554,23 +558,9 @@ Solution IDAKLUSolverOpenMP::solve(
t_return[i] = t[i];
}
- py::capsule free_t_when_done(
- t_return,
- [](void *f) {
- realtype *vect = reinterpret_cast(f);
- delete[] vect;
- }
- );
-
- np_array t_ret = np_array(
- number_of_timesteps,
- &t_return[0],
- free_t_when_done
- );
-
// States, y
realtype *y_return = new realtype[number_of_timesteps * length_of_return_vector];
- count = 0;
+ int count = 0;
for (size_t i = 0; i < number_of_timesteps; i++) {
for (size_t j = 0; j < length_of_return_vector; j++) {
y_return[count] = y[i][j];
@@ -578,20 +568,6 @@ Solution IDAKLUSolverOpenMP::solve(
}
}
- py::capsule free_y_when_done(
- y_return,
- [](void *f) {
- realtype *vect = reinterpret_cast(f);
- delete[] vect;
- }
- );
-
- np_array y_ret = np_array(
- number_of_timesteps * length_of_return_vector,
- &y_return[0],
- free_y_when_done
- );
-
// Sensitivity states, yS
// Note: Ordering of vector is different if computing outputs vs returning
// the complete state vector
@@ -614,43 +590,50 @@ Solution IDAKLUSolverOpenMP::solve(
}
}
- py::capsule free_yS_when_done(
- yS_return,
- [](void *f) {
- realtype *vect = reinterpret_cast(f);
- delete[] vect;
+ realtype *yp_return = new realtype[(save_hermite ? 1 : 0) * (number_of_timesteps * number_of_states)];
+ realtype *ypS_return = new realtype[(save_hermite ? 1 : 0) * (arg_sens0 * arg_sens1 * arg_sens2)];
+ if (save_hermite) {
+ count = 0;
+ for (size_t i = 0; i < number_of_timesteps; i++) {
+ for (size_t j = 0; j < number_of_states; j++) {
+ yp_return[count] = yp[i][j];
+ count++;
+ }
}
- );
-
- np_array yS_ret = np_array(
- vector {
- arg_sens0,
- arg_sens1,
- arg_sens2
- },
- &yS_return[0],
- free_yS_when_done
- );
- // Final state slice, yterm
- py::capsule free_yterm_when_done(
- yterm_return,
- [](void *f) {
- realtype *vect = reinterpret_cast(f);
- delete[] vect;
+ // Sensitivity states, ypS
+ // Note: Ordering of vector is different if computing outputs vs returning
+ // the complete state vector
+ count = 0;
+ for (size_t idx0 = 0; idx0 < arg_sens0; idx0++) {
+ for (size_t idx1 = 0; idx1 < arg_sens1; idx1++) {
+ for (size_t idx2 = 0; idx2 < arg_sens2; idx2++) {
+ auto i = (save_outputs_only ? idx0 : idx1);
+ auto j = (save_outputs_only ? idx1 : idx2);
+ auto k = (save_outputs_only ? idx2 : idx0);
+
+ ypS_return[count] = ypS[i][k][j];
+ count++;
+ }
+ }
}
- );
+ }
- np_array y_term = np_array(
+ return SolutionData(
+ retval,
+ number_of_timesteps,
+ length_of_return_vector,
+ arg_sens0,
+ arg_sens1,
+ arg_sens2,
length_of_final_sv_slice,
- &yterm_return[0],
- free_yterm_when_done
- );
-
- // Store the solution
- Solution sol(retval, t_ret, y_ret, yS_ret, y_term);
-
- return sol;
+ save_hermite,
+ t_return,
+ y_return,
+ yp_return,
+ yS_return,
+ ypS_return,
+ yterm_return);
}
template
@@ -666,13 +649,78 @@ void IDAKLUSolverOpenMP::ExtendAdaptiveArrays() {
if (sensitivity) {
yS.emplace_back(number_of_parameters, vector(length_of_return_vector, 0.0));
}
+
+ if (save_hermite) {
+ ExtendHermiteArrays();
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::ExtendHermiteArrays() {
+ DEBUG("IDAKLUSolver::ExtendHermiteArrays");
+ // States
+ yp.emplace_back(number_of_states, 0.0);
+
+ // Sensitivity
+ if (sensitivity) {
+ ypS.emplace_back(number_of_parameters, vector(number_of_states, 0.0));
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::ReinitializeIntegrator(const realtype& t_val) {
+ DEBUG("IDAKLUSolver::ReinitializeIntegrator");
+ CheckErrors(IDAReInit(ida_mem, t_val, yy, yyp));
+ if (sensitivity) {
+ CheckErrors(IDASensReInit(ida_mem, IDA_SIMULTANEOUS, yyS, yypS));
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::ConsistentInitialization(
+ const realtype& t_val,
+ const realtype& t_next,
+ const int& icopt) {
+ DEBUG("IDAKLUSolver::ConsistentInitialization");
+
+ if (is_ODE && icopt == IDA_YA_YDP_INIT) {
+ ConsistentInitializationODE(t_val);
+ } else {
+ ConsistentInitializationDAE(t_val, t_next, icopt);
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::ConsistentInitializationDAE(
+ const realtype& t_val,
+ const realtype& t_next,
+ const int& icopt) {
+ DEBUG("IDAKLUSolver::ConsistentInitializationDAE");
+ IDACalcIC(ida_mem, icopt, t_next);
+}
+
+template
+void IDAKLUSolverOpenMP::ConsistentInitializationODE(
+ const realtype& t_val) {
+ DEBUG("IDAKLUSolver::ConsistentInitializationODE");
+
+ // For ODEs where the mass matrix M = I, we can simplify the problem
+ // by analytically computing the yp values. If we take our implicit
+ // DAE system res(t,y,yp) = f(t,y) - I*yp, then yp = res(t,y,0). This
+ // avoids an expensive call to IDACalcIC.
+ realtype *y_cache_val = N_VGetArrayPointer(y_cache);
+ std::memset(y_cache_val, 0, number_of_states * sizeof(realtype));
+ // Overwrite yp
+ residual_eval(t_val, yy, y_cache, yyp, functions.get());
}
template
void IDAKLUSolverOpenMP::SetStep(
realtype &tval,
realtype *y_val,
+ realtype *yp_val,
vector const &yS_val,
+ vector const &ypS_val,
int &i_save
) {
// Set adaptive step results for y and yS
@@ -685,6 +733,10 @@ void IDAKLUSolverOpenMP::SetStep(
SetStepOutput(tval, y_val, yS_val, i_save);
} else {
SetStepFull(tval, y_val, yS_val, i_save);
+
+ if (save_hermite) {
+ SetStepHermite(tval, yp_val, ypS_val, i_save);
+ }
}
i_save++;
@@ -700,7 +752,9 @@ void IDAKLUSolverOpenMP::SetStepInterp(
realtype &t_prev,
realtype const &t_eval_next,
realtype *y_val,
+ realtype *yp_val,
vector const &yS_val,
+ vector const &ypS_val,
int &i_save
) {
// Save the state at the requested time
@@ -713,7 +767,7 @@ void IDAKLUSolverOpenMP::SetStepInterp(
}
// Memory is already allocated for the interpolated values
- SetStep(t_interp_next, y_val, yS_val, i_save);
+ SetStep(t_interp_next, y_val, yp_val, yS_val, ypS_val, i_save);
i_interp++;
if (i_interp == (t_interp.size())) {
@@ -825,12 +879,55 @@ void IDAKLUSolverOpenMP::SetStepOutputSensitivities(
}
}
+template
+void IDAKLUSolverOpenMP::SetStepHermite(
+ realtype &tval,
+ realtype *yp_val,
+ vector const &ypS_val,
+ int &i_save
+) {
+ // Set adaptive step results for yp and ypS
+ DEBUG("IDAKLUSolver::SetStepHermite");
+
+ // States
+ CheckErrors(IDAGetDky(ida_mem, tval, 1, yyp));
+ auto &yp_back = yp[i_save];
+ for (size_t j = 0; j < length_of_return_vector; ++j) {
+ yp_back[j] = yp_val[j];
+
+ }
+
+ // Sensitivity
+ if (sensitivity) {
+ SetStepHermiteSensitivities(tval, yp_val, ypS_val, i_save);
+ }
+}
+
+template
+void IDAKLUSolverOpenMP::SetStepHermiteSensitivities(
+ realtype &tval,
+ realtype *yp_val,
+ vector const &ypS_val,
+ int &i_save
+) {
+ DEBUG("IDAKLUSolver::SetStepHermiteSensitivities");
+
+ // Calculate sensitivities for the full ypS array
+ CheckErrors(IDAGetSensDky(ida_mem, tval, 1, yypS));
+ for (size_t j = 0; j < number_of_parameters; ++j) {
+ auto &ypS_back_j = ypS[i_save][j];
+ auto &ypSval_j = ypS_val[j];
+ for (size_t k = 0; k < number_of_states; ++k) {
+ ypS_back_j[k] = ypSval_j[k];
+ }
+ }
+}
+
template
void IDAKLUSolverOpenMP::CheckErrors(int const & flag) {
if (flag < 0) {
- auto message = (std::string("IDA failed with flag ") + std::to_string(flag)).c_str();
- py::set_error(PyExc_ValueError, message);
- throw py::error_already_set();
+ auto message = std::string("IDA failed with flag ") + std::to_string(flag);
+ throw std::runtime_error(message.c_str());
}
}
diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp
index b6a33e016e..8eb605fe77 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/Options.cpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/Options.cpp
@@ -1,6 +1,7 @@
#include "Options.hpp"
#include
#include
+#include
using namespace std::string_literals;
@@ -11,9 +12,25 @@ SetupOptions::SetupOptions(py::dict &py_opts)
precon_half_bandwidth(py_opts["precon_half_bandwidth"].cast()),
precon_half_bandwidth_keep(py_opts["precon_half_bandwidth_keep"].cast()),
num_threads(py_opts["num_threads"].cast()),
+ num_solvers(py_opts["num_solvers"].cast()),
linear_solver(py_opts["linear_solver"].cast()),
linsol_max_iterations(py_opts["linsol_max_iterations"].cast())
{
+ if (num_solvers > num_threads)
+ {
+ throw std::domain_error(
+ "Number of solvers must be less than or equal to the number of threads"
+ );
+ }
+
+ // input num_threads is the overall number of threads to use. num_solvers of these
+ // will be used to run solvers in parallel, leaving num_threads / num_solvers threads
+ // to be used by each solver. From here on num_threads is the number of threads to be used by each solver
+ num_threads = static_cast(
+ std::floor(
+ static_cast(num_threads) / static_cast(num_solvers)
+ )
+ );
using_sparse_matrix = true;
using_banded_matrix = false;
@@ -132,6 +149,7 @@ SolverOptions::SolverOptions(py::dict &py_opts)
nonlinear_convergence_coefficient(RCONST(py_opts["nonlinear_convergence_coefficient"].cast())),
nonlinear_convergence_coefficient_ic(RCONST(py_opts["nonlinear_convergence_coefficient_ic"].cast())),
suppress_algebraic_error(py_opts["suppress_algebraic_error"].cast()),
+ hermite_interpolation(py_opts["hermite_interpolation"].cast()),
// IDA initial conditions calculation
calc_ic(py_opts["calc_ic"].cast()),
init_all_y_ic(py_opts["init_all_y_ic"].cast()),
diff --git a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp
index 66a175cfff..7418c68ec3 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/Options.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/Options.hpp
@@ -15,6 +15,7 @@ struct SetupOptions {
int precon_half_bandwidth;
int precon_half_bandwidth_keep;
int num_threads;
+ int num_solvers;
// IDALS linear solver interface
std::string linear_solver; // klu, lapack, spbcg
int linsol_max_iterations;
@@ -37,6 +38,7 @@ struct SolverOptions {
double nonlinear_convergence_coefficient;
double nonlinear_convergence_coefficient_ic;
sunbooleantype suppress_algebraic_error;
+ bool hermite_interpolation;
// IDA initial conditions calculation
bool calc_ic;
bool init_all_y_ic;
diff --git a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp
index 72d48fa644..8227bb9da8 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/Solution.hpp
@@ -9,18 +9,30 @@
class Solution
{
public:
+ /**
+ * @brief Default Constructor
+ */
+ Solution() = default;
+
/**
* @brief Constructor
*/
- Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yS_np, np_array &y_term_np)
- : flag(retval), t(t_np), y(y_np), yS(yS_np), y_term(y_term_np)
+ Solution(int &retval, np_array &t_np, np_array &y_np, np_array &yp_np, np_array &yS_np, np_array &ypS_np, np_array &y_term_np)
+ : flag(retval), t(t_np), y(y_np), yp(yp_np), yS(yS_np), ypS(ypS_np), y_term(y_term_np)
{
}
+ /**
+ * @brief Default copy from another Solution
+ */
+ Solution(const Solution &solution) = default;
+
int flag;
np_array t;
np_array y;
+ np_array yp;
np_array yS;
+ np_array ypS;
np_array y_term;
};
diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp
new file mode 100644
index 0000000000..bc48c646d3
--- /dev/null
+++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.cpp
@@ -0,0 +1,99 @@
+#include "SolutionData.hpp"
+
+Solution SolutionData::generate_solution() {
+ py::capsule free_t_when_done(
+ t_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array t_ret = np_array(
+ number_of_timesteps,
+ &t_return[0],
+ free_t_when_done
+ );
+
+ py::capsule free_y_when_done(
+ y_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array y_ret = np_array(
+ number_of_timesteps * length_of_return_vector,
+ &y_return[0],
+ free_y_when_done
+ );
+
+ py::capsule free_yp_when_done(
+ yp_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array yp_ret = np_array(
+ (save_hermite ? 1 : 0) * number_of_timesteps * length_of_return_vector,
+ &yp_return[0],
+ free_yp_when_done
+ );
+
+ py::capsule free_yS_when_done(
+ yS_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array yS_ret = np_array(
+ std::vector {
+ arg_sens0,
+ arg_sens1,
+ arg_sens2
+ },
+ &yS_return[0],
+ free_yS_when_done
+ );
+
+ py::capsule free_ypS_when_done(
+ ypS_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array ypS_ret = np_array(
+ std::vector {
+ (save_hermite ? 1 : 0) * arg_sens0,
+ arg_sens1,
+ arg_sens2
+ },
+ &ypS_return[0],
+ free_ypS_when_done
+ );
+
+ // Final state slice, yterm
+ py::capsule free_yterm_when_done(
+ yterm_return,
+ [](void *f) {
+ realtype *vect = reinterpret_cast(f);
+ delete[] vect;
+ }
+ );
+
+ np_array y_term = np_array(
+ length_of_final_sv_slice,
+ &yterm_return[0],
+ free_yterm_when_done
+ );
+
+ // Store the solution
+ return Solution(flag, t_ret, y_ret, yp_ret, yS_ret, ypS_ret, y_term);
+}
diff --git a/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp
new file mode 100644
index 0000000000..81ca7f5221
--- /dev/null
+++ b/src/pybamm/solvers/c_solvers/idaklu/SolutionData.hpp
@@ -0,0 +1,82 @@
+#ifndef PYBAMM_IDAKLU_SOLUTION_DATA_HPP
+#define PYBAMM_IDAKLU_SOLUTION_DATA_HPP
+
+
+#include "common.hpp"
+#include "Solution.hpp"
+
+/**
+ * @brief SolutionData class. Contains all the data needed to create a Solution
+ */
+class SolutionData
+{
+ public:
+ /**
+ * @brief Default constructor
+ */
+ SolutionData() = default;
+
+ /**
+ * @brief constructor using fields
+ */
+ SolutionData(
+ int flag,
+ int number_of_timesteps,
+ int length_of_return_vector,
+ int arg_sens0,
+ int arg_sens1,
+ int arg_sens2,
+ int length_of_final_sv_slice,
+ bool save_hermite,
+ realtype *t_return,
+ realtype *y_return,
+ realtype *yp_return,
+ realtype *yS_return,
+ realtype *ypS_return,
+ realtype *yterm_return):
+ flag(flag),
+ number_of_timesteps(number_of_timesteps),
+ length_of_return_vector(length_of_return_vector),
+ arg_sens0(arg_sens0),
+ arg_sens1(arg_sens1),
+ arg_sens2(arg_sens2),
+ length_of_final_sv_slice(length_of_final_sv_slice),
+ save_hermite(save_hermite),
+ t_return(t_return),
+ y_return(y_return),
+ yp_return(yp_return),
+ yS_return(yS_return),
+ ypS_return(ypS_return),
+ yterm_return(yterm_return)
+ {}
+
+
+ /**
+ * @brief Default copy from another SolutionData
+ */
+ SolutionData(const SolutionData &solution_data) = default;
+
+ /**
+ * @brief Create a solution object from this data
+ */
+ Solution generate_solution();
+
+private:
+
+ int flag;
+ int number_of_timesteps;
+ int length_of_return_vector;
+ int arg_sens0;
+ int arg_sens1;
+ int arg_sens2;
+ int length_of_final_sv_slice;
+ bool save_hermite;
+ realtype *t_return;
+ realtype *y_return;
+ realtype *yp_return;
+ realtype *yS_return;
+ realtype *ypS_return;
+ realtype *yterm_return;
+};
+
+#endif // PYBAMM_IDAKLU_SOLUTION_DATA_HPP
diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.cpp b/src/pybamm/solvers/c_solvers/idaklu/common.cpp
index bf38acc56a..161c14f340 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/common.cpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/common.cpp
@@ -11,21 +11,9 @@ std::vector numpy2realtype(const np_array& input_np) {
return output;
}
-std::vector setDiff(const std::vector& A, const std::vector& B) {
- std::vector result;
- if (!(A.empty())) {
- std::set_difference(A.begin(), A.end(), B.begin(), B.end(), std::back_inserter(result));
- }
- return result;
-}
-std::vector makeSortedUnique(const std::vector& input) {
- std::unordered_set uniqueSet(input.begin(), input.end()); // Remove duplicates
- std::vector uniqueVector(uniqueSet.begin(), uniqueSet.end()); // Convert to vector
- std::sort(uniqueVector.begin(), uniqueVector.end()); // Sort the vector
- return uniqueVector;
-}
std::vector makeSortedUnique(const np_array& input_np) {
- return makeSortedUnique(numpy2realtype(input_np));
+ const auto input_vec = numpy2realtype(input_np);
+ return makeSortedUnique(input_vec.begin(), input_vec.end());
}
diff --git a/src/pybamm/solvers/c_solvers/idaklu/common.hpp b/src/pybamm/solvers/c_solvers/idaklu/common.hpp
index 3289326541..90672080b6 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/common.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/common.hpp
@@ -31,8 +31,9 @@
#include
namespace py = pybind11;
-using np_array = py::array_t;
-using np_array_dense = py::array_t;
+// note: we rely on c_style ordering for numpy arrays so don't change this!
+using np_array = py::array_t;
+using np_array_realtype = py::array_t;
using np_array_int = py::array_t;
/**
@@ -83,12 +84,25 @@ std::vector numpy2realtype(const np_array& input_np);
/**
* @brief Utility function to compute the set difference of two vectors
*/
-std::vector setDiff(const std::vector& A, const std::vector& B);
+template
+std::vector setDiff(const T1 a_begin, const T1 a_end, const T2 b_begin, const T2 b_end) {
+ std::vector result;
+ if (std::distance(a_begin, a_end) > 0) {
+ std::set_difference(a_begin, a_end, b_begin, b_end, std::back_inserter(result));
+ }
+ return result;
+}
/**
* @brief Utility function to make a sorted and unique vector
*/
-std::vector makeSortedUnique(const std::vector& input);
+template
+std::vector makeSortedUnique(const T input_begin, const T input_end) {
+ std::unordered_set uniqueSet(input_begin, input_end); // Remove duplicates
+ std::vector uniqueVector(uniqueSet.begin(), uniqueSet.end()); // Convert to vector
+ std::sort(uniqueVector.begin(), uniqueVector.end()); // Sort the vector
+ return uniqueVector;
+}
std::vector makeSortedUnique(const np_array& input_np);
@@ -126,8 +140,7 @@ std::vector makeSortedUnique(const np_array& input_np);
} \
std::cout << "]" << std::endl; }
-#define DEBUG_v(v, M) {\
- int N = 2; \
+#define DEBUG_v(v, N) {\
std::cout << #v << "[n=" << N << "] = ["; \
for (int i = 0; i < N; i++) { \
std::cout << v[i]; \
diff --git a/src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp b/src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp
index ce1765aa82..dcc1e4f8cc 100644
--- a/src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp
+++ b/src/pybamm/solvers/c_solvers/idaklu/idaklu_solver.hpp
@@ -2,6 +2,7 @@
#define PYBAMM_CREATE_IDAKLU_SOLVER_HPP
#include "IDAKLUSolverOpenMP_solvers.hpp"
+#include "IDAKLUSolverGroup.hpp"
#include
#include
@@ -12,52 +13,21 @@
*/
template
IDAKLUSolver *create_idaklu_solver(
- int number_of_states,
+ std::unique_ptr functions,
int number_of_parameters,
- const typename ExprSet::BaseFunctionType &rhs_alg,
- const typename ExprSet::BaseFunctionType &jac_times_cjmass,
const np_array_int &jac_times_cjmass_colptrs,
const np_array_int &jac_times_cjmass_rowvals,
const int jac_times_cjmass_nnz,
const int jac_bandwidth_lower,
const int jac_bandwidth_upper,
- const typename ExprSet::BaseFunctionType &jac_action,
- const typename ExprSet::BaseFunctionType &mass_action,
- const typename ExprSet::BaseFunctionType &sens,
- const typename ExprSet::BaseFunctionType &events,
const int number_of_events,
np_array rhs_alg_id,
np_array atol_np,
double rel_tol,
int inputs_length,
- const std::vector& var_fcns,
- const std::vector& dvar_dy_fcns,
- const std::vector& dvar_dp_fcns,
- py::dict py_opts
+ SolverOptions solver_opts,
+ SetupOptions setup_opts
) {
- auto setup_opts = SetupOptions(py_opts);
- auto solver_opts = SolverOptions(py_opts);
- auto functions = std::make_unique(
- rhs_alg,
- jac_times_cjmass,
- jac_times_cjmass_nnz,
- jac_bandwidth_lower,
- jac_bandwidth_upper,
- jac_times_cjmass_rowvals,
- jac_times_cjmass_colptrs,
- inputs_length,
- jac_action,
- mass_action,
- sens,
- events,
- number_of_states,
- number_of_events,
- number_of_parameters,
- var_fcns,
- dvar_dy_fcns,
- dvar_dp_fcns,
- setup_opts
- );
IDAKLUSolver *idakluSolver = nullptr;
@@ -189,4 +159,88 @@ IDAKLUSolver *create_idaklu_solver(
return idakluSolver;
}
+/**
+ * @brief Create a group of solvers using create_idaklu_solver
+ */
+template
+IDAKLUSolverGroup *create_idaklu_solver_group(
+ int number_of_states,
+ int number_of_parameters,
+ const typename ExprSet::BaseFunctionType &rhs_alg,
+ const typename ExprSet::BaseFunctionType &jac_times_cjmass,
+ const np_array_int &jac_times_cjmass_colptrs,
+ const np_array_int &jac_times_cjmass_rowvals,
+ const int jac_times_cjmass_nnz,
+ const int jac_bandwidth_lower,
+ const int jac_bandwidth_upper,
+ const typename ExprSet::BaseFunctionType &jac_action,
+ const typename ExprSet::BaseFunctionType &mass_action,
+ const typename ExprSet::BaseFunctionType &sens,
+ const typename ExprSet::BaseFunctionType &events,
+ const int number_of_events,
+ np_array rhs_alg_id,
+ np_array atol_np,
+ double rel_tol,
+ int inputs_length,
+ const std::vector& var_fcns,
+ const std::vector