PyBaMM documentation#

PyBaMM user guide#

This guide is an overview and explains the important features; details are found in API documentation.

Installation#

PyBaMM is available on GNU/Linux, MacOS and Windows. It can be installed using pip or conda, or from source.

PyBaMM can be installed via pip from PyPI.

pip install pybamm

PyBaMM is part of the Anaconda distribution and is available as a conda package through the conda-forge channel.

conda install -c conda-forge pybamm

PyBaMM can be installed via pip from PyPI.

brew install sundials && pip install pybamm

PyBaMM is part of the Anaconda distribution and is available as a conda package through the conda-forge channel.

conda install -c conda-forge pybamm

Optional solvers#

Following GNU/Linux and macOS solvers are optionally available:

Dependencies#

Required dependencies#

PyBaMM requires the following dependencies.

Package

Minimum supported version

NumPy

1.23.5

SciPy

1.9.3

CasADi

3.6.3

Xarray

2022.6.0

Anytree

2.8.0

Optional Dependencies#

PyBaMM has a number of optional dependencies for different functionalities. If the optional dependency is not installed, PyBaMM will raise an ImportError when the method requiring that dependency is called.

If you are using pip, optional PyBaMM dependencies can be installed or managed in a file (e.g., setup.py, or pyproject.toml) as optional extras (e.g.,``pybamm[dev,plot]``). All optional dependencies can be installed with pybamm[all], and specific sets of dependencies are listed in the sections below.

Plot dependencies#

Installable with pip install "pybamm[plot]"

Dependency

Minimum Version

pip extra

Notes

imageio

2.3.0

plot

For generating simulation GIFs.

matplotlib

3.6.0

plot

To plot various battery models, and analyzing battery performance.

Pandas dependencies#

Installable with pip install "pybamm[pandas]"

Dependency

Minimum Version

pip extra

Notes

pandas

1.5.0

pandas

For data manipulation and analysis.

Docs dependencies#

Installable with pip install "pybamm[docs]"

Dependency

Minimum Version

pip extra

Notes

sphinx

-

docs

Sphinx makes it easy to create intelligent and beautiful documentation.

pydata-sphinx-theme

-

docs

A clean, Bootstrap-based Sphinx theme.

sphinx_design

-

docs

A sphinx extension for designing.

sphinx-copybutton

-

docs

To copy codeblocks.

myst-parser

-

docs

For technical & scientific documentation.

sphinx-inline-tabs

-

docs

Add inline tabbed content to your Sphinx documentation.

sphinxcontrib-bibtex

-

docs

For BibTeX citations.

sphinx-autobuild

-

docs

For re-building docs once triggered.

Examples dependencies#

Installable with pip install "pybamm[examples]"

Dependency

Minimum Version

pip extra

Notes

jupyter

-

examples

For example notebooks rendering.

Dev dependencies#

Installable with pip install "pybamm[dev]"

Dependency

Minimum Version

pip extra

Notes

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.

coverage

-

dev

For calculating coverage of tests.

pytest

6.0.0

dev

For running Jupyter notebooks tests.

pytest-xdist

-

dev

For running tests in parallel across distributed workers.

nbmake

-

dev

A pytest plugin for executing Jupyter notebooks.

Cite dependencies#

Installable with pip install "pybamm[cite]"

Dependency

Minimum Version

pip extra

Notes

pybtex

0.24.0

cite

BibTeX-compatible bibliography processor.

Latexify dependencies#

Installable with pip install "pybamm[latexify]"

Dependency

Minimum Version

pip extra

Notes

sympy

1.9.3

latexify

For symbolic mathematics.

bpx dependencies#

Installable with pip install "pybamm[bpx]"

Dependency

Minimum Version

pip extra

Notes

bpx

-

bpx

Battery Parameter eXchange

tqdm dependencies#

Installable with pip install "pybamm[tqdm]"

Dependency

Minimum Version

pip extra

Notes

tqdm

-

tqdm

For logging loops.

Jax dependencies#

Installable with pip install "pybamm[jax]", currently supported on Python 3.9-3.11.

Dependency

Minimum Version

pip extra

Notes

JAX

0.4.20

jax

For the JAX solver

jaxlib

0.4.20

jax

Support library for JAX

odes dependencies#

Installable with pip install "pybamm[odes]"

Dependency

Minimum Version

pip extra

Notes

scikits.odes

-

odes

For scikits ODE & DAE solvers

Note

Before running pip install "pybamm[odes]", make sure to install scikits.odes build-time requirements as described here .

Full installation guide#

Installing a specific version? Installing from source? Check the advanced installation pages below

GNU/Linux & macOS#
Prerequisites#

To use PyBaMM, you must have Python 3.8, 3.9, 3.10, 3.11, or 3.12 installed.

To install Python 3 on Debian-based distributions (Debian, Ubuntu, Linux Mint), open a terminal and run

sudo apt update
sudo apt install python3

On Fedora or CentOS, you can use DNF or Yum. For example

sudo dnf install python3

On macOS, you can use the homebrew package manager. First, install brew:

ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

then follow instructions in the link on adding brew to path, and run

brew install python3
Install PyBaMM#
User install#

We recommend to install PyBaMM within a virtual environment, in order not to alter any distribution Python files. First, make sure you are using Python 3.8, 3.9, 3.10, 3.11, or 3.12. To create a virtual environment env within your current directory type:

virtualenv env

You can then “activate” the environment using:

source env/bin/activate

Now all the calls to pip described below will install PyBaMM and its dependencies into the environment env. When you are ready to exit the environment and go back to your original system, just type:

deactivate

PyBaMM can be installed via pip. On macOS, it is necessary to install the SUNDIALS library beforehand.

In a terminal, run the following command:

pip install pybamm

In a terminal, run the following commands:

brew install sundials
pip install pybamm

PyBaMM’s required dependencies (such as numpy, casadi, etc) will be installed automatically when you install PyBaMM using pip.

For an introduction to virtual environments, see (https://realpython.com/python-virtual-environments-a-primer/).

Optional - scikits.odes solver#

Users can install scikits.odes to utilize its interfaced SUNDIALS ODE and DAE solvers wrapped in PyBaMM.

Note

Currently, only GNU/Linux and macOS are supported.

Note

The scikits.odes solver is not supported on Python 3.12 yet. Please refer to bmcage/odes#162. There is support for Python 3.8, 3.9, 3.10, and 3.11.

In a terminal, run the following commands:

apt-get install libopenblas-dev
pip install wget cmake
pybamm_install_odes

system (under ~/.local), before installing scikits.odes. (Alternatively, one can install SUNDIALS without this script and run pip install pybamm[odes] to install pybamm with scikits.odes.)

In a terminal, run the following command:

brew install openblas gcc gfortran
pip install wget cmake
pybamm_install_odes

The pybamm_install_odes command, installed with PyBaMM, automatically downloads and installs the SUNDIALS library on your system (under ~/.local), before installing scikits.odes . (Alternatively, one can install SUNDIALS without this script and run pip install pybamm[odes] to install pybamm with scikits.odes)

To avoid installation failures when using pip install pybamm[odes], make sure to set the SUNDIALS_INST environment variable. If you have installed SUNDIALS using Homebrew, set the variable to the appropriate location. For example:

export SUNDIALS_INST=$(brew --prefix sundials)

Ensure that the path matches the installation location on your system. You can verify the installation location by running:

brew info sundials

Look for the installation path, and use that path to set the SUNDIALS_INST variable.

Note: The location where Homebrew installs SUNDIALS might vary based on the system architecture (ARM or Intel). Adjust the path in the export SUNDIALS_INST command accordingly.

To avoid manual setup of path the pybamm_install_odes is recommended for a smoother installation process, as it takes care of automatically downloading and installing the SUNDIALS library on your system.

Optional - JaxSolver#

Users can install jax and jaxlib to use the Jax solver.

Note

The Jax solver is only supported for Python versions 3.9 through 3.12.

pip install "pybamm[jax]"

The pip install "pybamm[jax]" command automatically downloads and installs pybamm and the compatible versions of jax and jaxlib on your system. (pybamm_install_jax is deprecated.)

Uninstall PyBaMM#

PyBaMM can be uninstalled by running

pip uninstall pybamm

in your virtual environment.

Windows#
Prerequisites#

To use PyBaMM, you must have Python 3.8, 3.9, 3.10, 3.11, or 3.12 installed.

To install Python 3 download the installation files from Python’s website. Make sure to tick the box on Add Python 3.X to PATH. For more detailed instructions please see the official Python on Windows guide.

Install PyBaMM#
User install#

Launch the Command Prompt and go to the directory where you want to install PyBaMM. You can find a reminder of how to navigate the terminal here.

We recommend to install PyBaMM within a virtual environment, in order not to alter any distribution python files.

To install virtualenv, type:

python -m pip install virtualenv

To create a virtual environment env within your current directory type:

python -m virtualenv env

You can then “activate” the environment using:

env\Scripts\activate.bat

Now all the calls to pip described below will install PyBaMM and its dependencies into the environment env. When you are ready to exit the environment and go back to your original system, just type:

deactivate

PyBaMM can be installed via pip:

pip install pybamm

PyBaMM’s dependencies (such as numpy, scipy, etc) will be installed automatically when you install PyBaMM using pip.

For an introduction to virtual environments, see (https://realpython.com/python-virtual-environments-a-primer/).

Optional - JaxSolver#

Users can install jax and jaxlib to use the Jax solver.

Note

The Jax solver is only supported for Python versions 3.9 through 3.12.

pip install "pybamm[jax]"

The pip install "pybamm[jax]" command automatically downloads and installs pybamm and the compatible versions of jax and jaxlib on your system. (pybamm_install_jax is deprecated.)

Uninstall PyBaMM#

PyBaMM can be uninstalled by running

pip uninstall pybamm

in your virtual environment.

Installation using WSL#

If you want to install the optional PyBaMM solvers, you have to use the Windows Subsystem for Linux (WSL). You can find the installation instructions here.

Install from source (Windows Subsystem for Linux)#

To make it easier to install PyBaMM, we recommend using the Windows Subsystem for Linux (WSL) along with Visual Studio Code. This guide will walk you through the process.

Install WSL#

Install Ubuntu 22.04 or 20.04 LTS as a distribution for WSL following Microsoft’s guide to install WSL. For a seamless development environment, refer to this guide.

Install PyBaMM#
Get PyBaMM’s Source Code#
  1. Open a terminal in your Ubuntu distribution by selecting “Ubuntu” from the Start menu. You’ll get a bash prompt in your home directory.

  2. Install Git by typing the following command:

sudo apt install git-core
  1. Clone the PyBaMM repository:

git clone https://github.com/pybamm-team/PyBaMM.git
  1. Enter the PyBaMM Directory by running:

cd PyBaMM
5. Follow the Installation Steps#

Follow the installation instructions for PyBaMM on Linux.

Using Visual Studio Code with the WSL#

To use Visual Studio Code with the Windows Subsystem for Linux (WSL), follow these steps:

  1. Open Visual Studio Code.

  2. Install the “Remote - WSL” extension if not already installed.

  3. Open the PyBaMM directory in Visual Studio Code.

  4. In the bottom pane, select the “+” sign and choose “New WSL Window.”

  5. This opens a WSL terminal in the PyBaMM directory within the WSL.

Now you can develop and edit PyBaMM code using Visual Studio Code while utilizing the WSL environment.

Install from source (GNU Linux and macOS)#

This page describes the build and installation of PyBaMM from the source code, available on GitHub. Note that this is not the recommended approach for most users and should be reserved to people wanting to participate in the development of PyBaMM, or people who really need to use bleeding-edge feature(s) not yet available in the latest released version. If you do not fall in the two previous categories, you would be better off installing PyBaMM using pip or conda.

Lastly, familiarity with the Python ecosystem is recommended (pip, virtualenvs). Here is a gentle introduction/refresher: Python Virtual Environments: A Primer.

Prerequisites#

The following instructions are valid for both GNU/Linux distributions and MacOS. If you are running Windows, consider using the Windows Subsystem for Linux (WSL).

To obtain the PyBaMM source code, clone the GitHub repository

git clone https://github.com/pybamm-team/PyBaMM.git

or download the source archive on the repository’s homepage.

To install PyBaMM, you will need:

  • Python 3 (PyBaMM supports versions 3.8, 3.9, 3.10, 3.11, and 3.12)

  • The Python headers file for your current Python version.

  • A BLAS library (for instance openblas).

  • A C compiler (ex: gcc).

  • A Fortran compiler (ex: gfortran).

  • graphviz (optional), if you wish to build the documentation locally.

You can install the above with

sudo apt install python3.X python3.X-dev libopenblas-dev gcc gfortran graphviz

Where X is the version sub-number.

Note

On Windows, you can install graphviz using the Chocolatey package manager, or follow the instructions on the graphviz website.

brew install python openblas gcc gfortran graphviz libomp

Finally, we recommend using Nox. You can install it with

python3.X -m pip install --user nox

Depending on your operating system, you may or may not have pip installed along Python. If pip is not found, you probably want to install the python3-pip package.

Installing the build-time requirements#

PyBaMM comes with a DAE solver based on the IDA solver provided by the SUNDIALS library. To use this solver, you must make sure that you have the necessary SUNDIALS components installed on your system.

The IDA-based solver is currently unavailable on windows. If you are running windows, you can simply skip this section and jump to Installing PyBaMM.

# in the PyBaMM/ directory
nox -s pybamm-requires

This will download, compile and install the SuiteSparse and SUNDIALS libraries. Both libraries are installed in ~/.local.

Manual install of build time requirements#

If you’d rather do things yourself,

  1. Make sure you have CMake installed

  2. Compile and install SuiteSparse (PyBaMM only requires the KLU component).

  3. Compile and install SUNDIALS.

  4. Clone the pybind11 repository in the PyBaMM/ directory (make sure the directory is named pybind11).

PyBaMM ships with a Python script that automates points 2. and 3. You can run it with

python scripts/install_KLU_Sundials.py
Installing PyBaMM#

You should now have everything ready to build and install PyBaMM successfully.

Manual install#

From the PyBaMM/ directory, you can install PyBaMM using

pip install .

If you intend to contribute to the development of PyBaMM, it is convenient to install in “editable mode”, along with all the optional dependencies and useful tools for development and documentation:

pip install -e .[all,dev,docs]

If you are using zsh, you would need to use different pattern matching:

pip install -e '.[all,dev,docs]'

Before you start contributing to PyBaMM, please read the contributing guidelines.

Running the tests#
Using Nox (recommended)#

You can use Nox to run the unit tests and example notebooks in isolated virtual environments.

The default command

nox

will run pre-commit, install Linux and macOS dependencies, and run the unit tests. This can take several minutes.

To just run the unit tests, use

nox -s unit

Similarly, to run the integration tests, use

nox -s integration

Finally, to run the unit and the integration suites sequentially, use

nox -s tests
Using the test runner#

You can run unit tests for PyBaMM using

# in the PyBaMM/ directory
python run-tests.py --unit

The above starts a sub-process using the current python interpreter (i.e. using your current Python environment) and run the unit tests. This can take a few minutes.

You can also use the test runner to run the doctests:

python run-tests.py --doctest

There is more to the PyBaMM test runner. To see a list of all options, type

python run-tests.py --help
How to build the PyBaMM documentation#

The documentation is built using

nox -s docs

This will build the documentation and serve it locally (thanks to sphinx-autobuild) for preview. The preview will be updated automatically following changes.

Doctests, examples, and coverage#

Nox can also be used to run doctests, run examples, and generate a coverage report using:

  • nox -s examples: Run the Jupyter notebooks in docs/source/examples/notebooks/.

  • nox -s examples -- <path-to-notebook-1.ipynb> <path-to_notebook-2.ipynb>: Run specific Jupyter notebooks.

  • nox -s scripts: Run the example scripts in examples/scripts/.

  • nox -s doctests: Run doctests.

  • nox -s coverage: Measure current test coverage and generate a coverage report.

  • nox -s quick: Run integration tests, unit tests, and doctests sequentially.

Extra tips while using Nox#

Here are some additional useful commands you can run with Nox:

  • --verbose or -v: Enables verbose mode, providing more detailed output during the execution of Nox sessions.

  • --list or -l: Lists all available Nox sessions and their descriptions.

  • --stop-on-first-error: Stops the execution of Nox sessions immediately after the first error or failure occurs.

  • --envdir <path>: Specifies the directory where Nox creates and manages the virtual environments used by the sessions. In this case, the directory is set to <path>.

  • --install-only: Skips the test execution and only performs the installation step defined in the Nox sessions.

  • --nocolor: Disables the color output in the console during the execution of Nox sessions.

  • --report output.json: Generates a JSON report of the Nox session execution and saves it to the specified file, in this case, “output.json”.

  • nox -s docs --non-interactive: Builds the documentation without serving it locally (using sphinx-build instead of sphinx-autobuild).

Troubleshooting#

Problem: I have made edits to source files in PyBaMM, but these are not being used when I run my Python script.

Solution: Make sure you have installed PyBaMM using the -e flag, i.e. pip install -e .. This sets the installed location of the source files to your current directory.

Problem: Errors when solving model ValueError: Integrator name ida does not exist, or ValueError: Integrator name cvode does not exist.

Solution: This could mean that you have not installed scikits.odes correctly, check the instructions given above and make sure each command was successful.

One possibility is that you have not set your LD_LIBRARY_PATH to point to the sundials library, type echo $LD_LIBRARY_PATH and make sure one of the directories printed out corresponds to where the SUNDIALS libraries are located.

Another common reason is that you forget to install a BLAS library such as OpenBLAS before installing SUNDIALS. Check the cmake output when you configured SUNDIALS, it might say:

-- A library with BLAS API not found. Please specify library location.
-- LAPACK requires BLAS

If this is the case, on a Debian or Ubuntu system you can install OpenBLAS using sudo apt-get install libopenblas-dev (or brew install openblas for Mac OS) and then re-install SUNDIALS using the instructions above.

Install from source (Docker)#

This page describes the build and installation of PyBaMM using a Dockerfile, available on GitHub. Note that this is not the recommended approach for most users and should be reserved to people wanting to participate in the development of PyBaMM, or people who really need to use bleeding-edge feature(s) not yet available in the latest released version. If you do not fall in the two previous categories, you would be better off installing PyBaMM using pip or conda.

Prerequisites#

Before you begin, make sure you have Docker installed on your system. You can download and install Docker from the official Docker website. Ensure Docker installation by running:

docker --version
Pulling the Docker image#

Use the following command to pull the PyBaMM Docker image from Docker Hub:

docker pull pybamm/pybamm:latest
docker pull pybamm/pybamm:odes
docker pull pybamm/pybamm:jax
docker pull pybamm/pybamm:idaklu
docker pull pybamm/pybamm:all
Running the Docker container#

Once you have pulled the Docker image, you can run a Docker container with the PyBaMM environment:

  1. In your terminal, use the following command to start a Docker container from the pulled image:

docker run -it pybamm/pybamm:latest
docker run -it pybamm/pybamm:odes
docker run -it pybamm/pybamm:jax
docker run -it pybamm/pybamm:idaklu
docker run -it pybamm/pybamm:all
  1. You will now be inside the Docker container’s shell. You can use PyBaMM and its dependencies as if you were in a virtual environment.

  2. You can execute PyBaMM-related commands, run tests develop & contribute from the container.

Exiting the Docker container#

To exit the Docker container’s shell, you can simply type:

exit

This will return you to your host machine’s terminal.

Building Docker image locally from source#

If you want to build the PyBaMM Docker image locally from the PyBaMM source code, follow these steps:

  1. Clone the PyBaMM GitHub repository to your local machine if you haven’t already:

git clone https://github.com/pybamm-team/PyBaMM.git
  1. Change into the PyBaMM directory:

cd PyBaMM
  1. Build the Docker image using the following command:

docker build -t pybamm -f scripts/Dockerfile .
  1. Once the image is built, you can run a Docker container using:

docker run -it pybamm
  1. Activate PyBaMM development environment inside docker container using:

conda activate pybamm
Building Docker images with optional arguments#

When building the PyBaMM Docker images locally, you have the option to include specific solvers by using optional arguments. These solvers include:

  • IDAKLU: For IDA solver provided by the SUNDIALS plus KLU.

  • ODES: For scikits.odes solver for ODE & DAE problems.

  • JAX: For Jax solver.

  • ALL: For all the above solvers.

To build the Docker images with optional arguments, you can follow these steps for each solver:

docker build -t pybamm:odes -f scripts/Dockerfile --build-arg ODES=true .
docker build -t pybamm:jax -f scripts/Dockerfile --build-arg JAX=true .
docker build -t pybamm:idaklu -f scripts/Dockerfile --build-arg IDAKLU=true .
docker build -t pybamm:all -f scripts/Dockerfile --build-arg ALL=true .

After building the Docker images with the desired solvers, use the docker run command followed by the desired image name. For example, to run a container from the image built with all optional solvers:

docker run -it pybamm:all

Activate PyBaMM development environment inside docker container using:

conda activate pybamm

If you want to exit the Docker container’s shell, you can simply type:

exit
Using Git inside a running Docker container#

Note

You might require re-configuring git while running the docker container for the first time. You can run git config --list to ensure if you have desired git configuration already.

  1. Setting up git configuration

git config --global user.name "Your Name"

git config --global user.email your@mail.com
  1. Setting a git remote

git remote set-url origin <fork_url>

git remote add upstream https://github.com/pybamm-team/PyBaMM

git fetch --all
Using Visual Studio Code inside a running Docker container#

You can easily use Visual Studio Code inside a running Docker container by attaching it directly. This provides a seamless development environment within the container. Here’s how:

  1. Install the “Docker” extension from Microsoft in your local Visual Studio Code if it’s not already installed.

  2. Pull and run the Docker image containing PyBaMM development environment.

  3. In your local Visual Studio Code, open the “Docker” extension by clicking on the Docker icon in the sidebar.

  4. Under the “Containers” section, you’ll see a list of running containers. Right-click the running PyBaMM container.

  5. Select “Attach Visual Studio Code” from the context menu.

  6. Visual Studio Code will now connect to the container, and a new VS Code window will open up, running inside the container. You can now edit, debug, and work on your code using VS Code as if you were working directly on your local machine.

Getting Started#

The easiest way to use PyBaMM is to run a 1C constant-current discharge with a model of your choice with all the default settings:

import pybamm

model = pybamm.lithium_ion.DFN()  # Doyle-Fuller-Newman model
sim = pybamm.Simulation(model)
sim.solve([0, 3600])  # solve for 1 hour
sim.plot()

or simulate an experiment such as a constant-current discharge followed by a constant-current-constant-voltage charge:

import pybamm

experiment = pybamm.Experiment(
    [
        (
            "Discharge at C/10 for 10 hours or until 3.3 V",
            "Rest for 1 hour",
            "Charge at 1 A until 4.1 V",
            "Hold at 4.1 V until 50 mA",
            "Rest for 1 hour",
        )
    ]
    * 3,
)
model = pybamm.lithium_ion.DFN()
sim = pybamm.Simulation(model, experiment=experiment, solver=pybamm.CasadiSolver())
sim.solve()
sim.plot()

However, much greater customisation is available. It is possible to change the physics, parameter values, geometry, submesh type, number of submesh points, methods for spatial discretisation and solver for integration (see DFN script or notebook).

For new users we recommend the Getting Started guides. These are intended to be very simple step-by-step guides to show the basic functionality of PyBaMM, and can either be downloaded and used locally, or used online through Google Colab.

Further details can be found in a number of detailed examples, hosted on GitHub. In addition, full details of classes and methods can be found in the API documentation. Additional supporting material can be found here.

Fundamentals#

PyBaMM (Python Battery Mathematical Modelling) is an open-source battery simulation package written in Python. Our mission is to accelerate battery modelling research by providing open-source tools for multi-institutional, interdisciplinary collaboration. Broadly, PyBaMM consists of

  1. a framework for writing and solving systems of differential equations,

  2. a library of battery models and parameters, and

  3. specialized tools for simulating battery-specific experiments and visualizing the results.

Together, these enable flexible model definitions and fast battery simulations, allowing users to explore the effect of different battery designs and modeling assumptions under a variety of operating scenarios.

NOTE: This user-guide is a work-in-progress, we hope that this brief but incomplete overview will be useful to you.

Core framework#

The core of the framework is a custom computer algebra system to define mathematical equations, and a domain specific modeling language to combine these equations into systems of differential equations (usually partial differential equations for variables depending on space and time). The expression tree example gives an introduction to the computer algebra system, and the Getting Started tutorials walk through creating models of increasing complexity.

Once a model has been defined symbolically, PyBaMM solves it using the Method of Lines. First, the equations are discretised in the spatial dimension, using the finite volume method. Then, the resulting system is solved using third-party numerical solvers. Depending on the form of the model, the system can be ordinary differential equations (ODEs) (if only model.rhs is defined), or algebraic equations (if only model.algebraic is defined), or differential-algebraic equations (DAEs) (if both model.rhs and model.algebraic are defined). Jupyter notebooks explaining the solvers can be found here.

Model and Parameter Library#

PyBaMM contains an extensive library of battery models and parameters. The bulk of the library consists of models for lithium-ion, but there are also some other chemistries (lead-acid, lithium metal). Models are first divided broadly into common named models of varying complexity, such as the single particle model (SPM) or Doyle-Fuller-Newman model (DFN). Most options can be applied to any model, but some are model-specific (an error will be raised if you attempt to set an option is not compatible with a model). See Base Battery Model for a list of options.

The parameter library is simply a collection of python files each defining a complete set of parameters for a particular battery chemistry, covering all major lithium-ion chemistries (NMC, LFP, NCA, …). External parameter sets can be linked using entry points (see Parameters Sets).

Battery-specific tools#

One of PyBaMM’s unique features is the Experiment class, which allows users to define synthetic experiments using simple instructions in English

pybamm.Experiment(
    [
        (
            "Discharge at C/10 for 10 hours or until 3.3 V",
            "Rest for 1 hour",
            "Charge at 1 A until 4.1 V",
            "Hold at 4.1 V until 50 mA",
            "Rest for 1 hour",
        )
    ]
    * 3,
)

The above instruction will conduct a standard discharge / rest / charge / rest cycle three times, with a 10 hour discharge and 1 hour rest at the end of each cycle.

The Simulation class handles simulating an Experiment, as well as calculating additional outputs such as capacity as a function of cycle number. For example, the following code will simulate the experiment above and plot the standard output variables:

import pybamm
import matplotlib.pyplot as plt

# load model and parameter values
model = pybamm.lithium_ion.DFN()
sim = pybamm.Simulation(model, experiment=experiment)
solution = sim.solve()
solution.plot()

Finally, PyBaMM provides custom visualization tools:

  • Quick Plot: for easily plotting simulation outputs in a grid, including comparing multiple simulations

  • pybamm.plot_voltage_components: for plotting the component overpotentials that make up a voltage curve

Users are not limited to these tools and can plot the output of a simulation solution by accessing the underlying numpy array for the solution variables as

solution["variable name"].data

and using the plotting library of their choice.

Battery Models#

References for the battery models used in PyBaMM simulations can be found calling

pybamm.print_citations()

However, a few papers are provided in this section for anyone interested in reading the theory behind the models before doing the tutorials.

Review Articles#

Review of physics-based lithium-ion battery models

Review of parameterisation and a novel database for Li-ion battery models

Model References#

Lithium-Ion Batteries#

Doyle-Fuller-Newman model

Single particle model

Lead-Acid Batteries#

Isothermal porous-electrode model

Leading-Order Quasi-Static model

Contributing to PyBaMM#

If you’d like to contribute to PyBaMM (thanks!), please have a look at the guidelines below.

If you’re already familiar with our workflow, maybe have a quick look at the pre-commit checks directly below.

Pre-commit checks#

Before you commit any code, please perform the following checks:

Installing and using pre-commit#

PyBaMM uses a set of pre-commit hooks and the pre-commit bot to format and prettify the codebase. The hooks can be installed locally using -

pip install pre-commit
pre-commit install

This would run the checks every time a commit is created locally. The checks will only run on the files modified by that commit, but the checks can be triggered for all the files using -

pre-commit run --all-files

If you would like to skip the failing checks and push the code for further discussion, use the --no-verify option with git commit.

Workflow#

We use GIT and GitHub to coordinate our work. When making any kind of update, we try to follow the procedure below.

A. Before you begin#
  1. Create an issue where new proposals can be discussed before any coding is done.

  2. Create a branch of this repo (ideally on your own fork), where all changes will be made

  3. Download the source code onto your local system, by cloning the repository (or your fork of the repository).

  4. Install PyBaMM with the developer options.

  5. Test if your installation worked, using the test script: $ python run-tests.py --unit.

You now have everything you need to start making changes!

B. Writing your code#
  1. PyBaMM is developed in Python, and makes heavy use of NumPy (see also NumPy for MatLab users and Python for R users).

  2. Make sure to follow our coding style guidelines.

  3. Commit your changes to your branch with useful, descriptive commit messages: Remember these are publicly visible and should still make sense a few months ahead in time. While developing, you can keep using the GitHub issue you’re working on as a place for discussion. Refer to your commits when discussing specific lines of code.

  4. If you want to add a dependency on another library, or re-use code you found somewhere else, have a look at these guidelines.

C. Merging your changes with PyBaMM#
  1. Test your code!

  2. PyBaMM has online documentation at http://docs.pybamm.org/. To make sure any new methods or classes you added show up there, please read the documentation section.

  3. If you added a major new feature, perhaps it should be showcased in an example notebook.

  4. When you feel your code is finished, or at least warrants serious discussion, run the pre-commit checks and then create a pull request (PR) on PyBaMM’s GitHub page.

  5. Once a PR has been created, it will be reviewed by any member of the community. Changes might be suggested which you can make by simply adding new commits to the branch. When everything’s finished, someone with the right GitHub permissions will merge your changes into PyBaMM main repository.

Finally, if you really, really, really love developing PyBaMM, have a look at the current project infrastructure.

Coding style guidelines#

PyBaMM follows the PEP8 recommendations for coding style. These are very common guidelines, and community tools have been developed to check how well projects implement them. We recommend using pre-commit hooks to check your code before committing it. See installing and using pre-commit section for more details.

Ruff#

We use ruff to check our PEP8 adherence. To try this on your system, navigate to the PyBaMM directory in a console and type

python -m pip install pre-commit
pre-commit run ruff

ruff is configured inside the file pre-commit-config.yaml, allowing us to ignore some errors. If you think this should be added or removed, please submit an issue

When you commit your changes they will be checked against ruff automatically (see Pre-commit checks).

Naming#

Naming is hard. In general, we aim for descriptive class, method, and argument names. Avoid abbreviations when possible without making names overly long, so mean is better than mu, but a class name like MyClass is fine.

Class names are CamelCase, and start with an upper case letter, for example MyOtherClass. Method and variable names are lower case, and use underscores for word separation, for example x or iteration_count.

Dependencies and reusing code#

While it’s a bad idea for developers to “reinvent the wheel”, it’s important for users to get a reasonably sized download and an easy install. In addition, external libraries can sometimes cease to be supported, and when they contain bugs it might take a while before fixes become available as automatic downloads to PyBaMM users. For these reasons, all dependencies in PyBaMM should be thought about carefully, and discussed on GitHub.

Direct inclusion of code from other packages is possible, as long as their license permits it and is compatible with ours, but again should be considered carefully and discussed in the group. Snippets from blogs and stackoverflow can often be included without attribution, but if they solve a particularly nasty problem (or are very hard to read) it’s often a good idea to attribute (and document) them, by making a comment with a link in the source code.

Separating dependencies#

On the other hand… We do want to compare several tools, to generate documentation, and to speed up development. For this reason, the dependency structure is split into 4 parts:

  1. Core PyBaMM: A minimal set, including things like NumPy, SciPy, etc. All infrastructure should run against this set of dependencies, as well as any numerical methods we implement ourselves.

  2. Extras: Other inference packages and their dependencies. Methods we don’t want to implement ourselves, but do want to provide an interface to can have their dependencies added here.

  3. Documentation generating code: Everything you need to generate and work on the docs.

  4. Development code: Everything you need to do PyBaMM development (so all of the above packages, plus ruff and other testing tools).

Only ‘core pybamm’ is installed by default. The others have to be specified explicitly when running the installation command.

Managing Optional Dependencies and Their Imports#

PyBaMM utilizes optional dependencies to allow users to choose which additional libraries they want to use. Managing these optional dependencies and their imports is essential to provide flexibility to PyBaMM users.

PyBaMM provides a utility function have_optional_dependency, to check for the availability of optional dependencies within methods. This function can be used to conditionally import optional dependencies only if they are available. Here’s how to use it:

Optional dependencies should never be imported at the module level, but always inside methods. For example:

def use_pybtex(x,y,z):
    pybtex = have_optional_dependency("pybtex")
    ...

While importing a specific module instead of an entire package/library:

def use_parse_file(x, y, z):
    parse_file = have_optional_dependency("pybtex.database", "parse_file")
    ...

This allows people to (1) use PyBaMM without importing optional dependencies by default and (2) configure module-dependent functionalities in their scripts, which must be done before e.g. print_citations method is first imported.

Writing Tests for Optional Dependencies

Whenever a new optional dependency is added for optional functionality, it is recommended to write a corresponding unit test in test_util.py. This ensures that an error is raised upon the absence of said dependency. Here’s an example:

from tests import TestCase
import pybamm


class TestUtil(TestCase):
    def test_optional_dependency(self):
        # Test that an error is raised when pybtex is not available
        with self.assertRaisesRegex(
            ModuleNotFoundError, "Optional dependency pybtex is not available"
        ):
            sys.modules["pybtex"] = None
            pybamm.function_using_pybtex(x, y, z)

        # Test that the function works when pybtex is available
        sys.modules["pybtex"] = pybamm.util.have_optional_dependency("pybtex")
        pybamm.function_using_pybtex(x, y, z)

Testing#

All code requires testing. We use the unittest package for our tests. (These tests typically just check that the code runs without error, and so, are more debugging than testing in a strict sense. Nevertheless, they are very useful to have!)

We also use pytest along with the nbmake and the pytest-xdist plugins to test the example notebooks.

If you have nox installed, to run unit tests, type

nox -s unit

else, type

python run-tests.py --unit
Writing tests#

Every new feature should have its own test. To create ones, have a look at the test directory and see if there’s a test for a similar method. Copy-pasting this is a good way to start.

Next, add some simple (and speedy!) tests of your main features. If these run without exceptions that’s a good start! Next, check the output of your methods using any of these assert methods.

Running more tests#

The tests are divided into unit tests, whose aim is to check individual bits of code (e.g. discretising a gradient operator, or solving a simple ODE), and integration tests, which check how parts of the program interact as a whole (e.g. solving a full model). If you want to check integration tests as well as unit tests, type

nox -s tests

When you commit anything to PyBaMM, these checks will also be run automatically (see infrastructure).

Testing the example notebooks#

To test all the example notebooks in the docs/source/examples/ folder with pytest and nbmake, type

nox -s examples

Alternatively, you may use pytest directly with the --nbmake flag:

pytest --nbmake

which runs all the notebooks in the docs/source/examples/notebooks/ folder in parallel by default, using the pytest-xdist plugin.

Sometimes, debugging a notebook can be a hassle. To run a single notebook, pass the path to it to pytest:

pytest --nbmake docs/source/examples/notebooks/notebook-name.ipynb

or, alternatively, you can use posargs to pass the path to the notebook to nox. For example:

nox -s examples -- docs/source/examples/notebooks/notebook-name.ipynb

You may also test multiple notebooks this way. Passing the path to a folder will run all the notebooks in that folder:

nox -s examples -- docs/source/examples/notebooks/models/

You may also use an appropriate glob pattern to run all notebooks matching a particular folder or name pattern.

To edit the structure and how the Jupyter notebooks get rendered in the Sphinx documentation (using nbsphinx), install Pandoc on your system, either using conda (through the conda-forge channel)

conda install -c conda-forge pandoc

or refer to the Pandoc installation instructions specific to your platform.

Testing the example scripts#

To test all the example scripts in the examples/ folder, type

nox -s scripts
Debugging#

Often, the code you write won’t pass the tests straight away, at which stage it will become necessary to debug. The key to successful debugging is to isolate the problem by finding the smallest possible example that causes the bug. In practice, there are a few tricks to help you to do this, which we give below. Once you’ve isolated the issue, it’s a good idea to add a unit test that replicates this issue, so that you can easily check whether it’s been fixed, and make sure that it’s easily picked up if it crops up again. This also means that, if you can’t fix the bug yourself, it will be much easier to ask for help (by opening a bug-report issue).

  1. Run individual test scripts instead of the whole test suite:

    python tests/unit/path/to/test
    

    You can also run an individual test from a particular script, e.g.

    python tests/unit/test_quick_plot.py TestQuickPlot.test_failure
    

    If you want to run several, but not all, the tests from a script, you can restrict which tests are run from a particular script by using the skipping decorator:

    @unittest.skip("")
    def test_bit_of_code(self):
        ...
    

    or by just commenting out all the tests you don’t want to run.

  2. Set break points, either in your IDE or using the Python debugging module. To use the latter, add the following line where you want to set the break point

    import ipdb
    
    ipdb.set_trace()
    

    This will start the Python interactive debugger. If you want to be able to use magic commands from ipython, such as %timeit, then set

    from IPython import embed
    
    embed()
    import ipdb
    
    ipdb.set_trace()
    

    at the break point instead. Figuring out where to start the debugger is the real challenge. Some good ways to set debugging break points are:

    1. Try-except blocks. Suppose the line do_something_complicated() is raising a ValueError. Then you can put a try-except block around that line as:

      try:
          do_something_complicated()
      except ValueError:
          import ipdb
      
          ipdb.set_trace()
      

      This will start the debugger at the point where the ValueError was raised, and allow you to investigate further. Sometimes, it is more informative to put the try-except block further up the call stack than exactly where the error is raised.

    2. Warnings. If functions are raising warnings instead of errors, it can be hard to pinpoint where this is coming from. Here, you can use the warnings module to convert warnings to errors:

      import warnings
      
      warnings.simplefilter("error")
      

      Then you can use a try-except block, as in a., but with, for example, RuntimeWarning instead of ValueError.

    3. Stepping through the expression tree. Most calls in PyBaMM are operations on expression trees. To view an expression tree in ipython, you can use the render command:

      expression_tree.render()
      

      You can then step through the expression tree, using the children attribute, to pinpoint exactly where a bug is coming from. For example, if expression_tree.jac(y) is failing, you can check expression_tree.children[0].jac(y), then expression_tree.children[0].children[0].jac(y), etc.

  3. To isolate whether a bug is in a model, its Jacobian or its simplified version, you can set the use_jacobian and/or use_simplify attributes of the model to False (they are both True by default for most models).

  4. If a model isn’t giving the answer you expect, you can try comparing it to other models. For example, you can investigate parameter limits in which two models should give the same answer by setting some parameters to be small or zero. The StandardOutputComparison class can be used to compare some standard outputs from battery models.

  5. To get more information about what is going on under the hood, and hence understand what is causing the bug, you can set the logging level to DEBUG by adding the following line to your test or script:

    pybamm.set_logging_level("DEBUG")
    
  6. In models that inherit from pybamm.BaseBatteryModel (i.e. any battery model), you can use self.process_parameters_and_discretise to process a symbol and see what it will look like.

Profiling#

Sometimes, a bit of code will take much longer than you expect to run. In this case, you can set

from IPython import embed

embed()
import ipdb

ipdb.set_trace()

as above, and then use some of the profiling tools. In order of increasing detail:

  1. Simple timer. In ipython, the command

    %time command_to_time()
    

    tells you how long the line command_to_time() takes. You can use %timeit instead to run the command several times and obtain more accurate timings.

  2. Simple profiler. Using %prun instead of %time will give a brief profiling report 3. Detailed profiler. You can install the detailed profiler snakeviz through pip:

    pip install snakeviz
    

    and then, in ipython, run

    %load_ext snakeviz
    %snakeviz command_to_time()
    

    This will open a window in your browser with detailed profiling information.

Documentation#

PyBaMM is documented in several ways.

First and foremost, every method and every class should have a docstring that describes in plain terms what it does, and what the expected input and output is.

These docstrings can be fairly simple, but can also make use of reStructuredText, a markup language designed specifically for writing technical documentation. For example, you can link to other classes and methods by writing :class:`pybamm.Model` and :meth:`run()` .

In addition, we write a (very) small bit of documentation in separate reStructuredText files in the docs directory. Most of what these files do is simply import docstrings from the source code. But they also do things like add tables and indexes. If you’ve added a new class to a module, search the docs directory for that module’s .rst file and add your class (in alphabetical order) to its index. If you’ve added a whole new module, copy-paste another module’s file and add a link to your new file in the appropriate index.rst file.

Using Sphinx the documentation in docs can be converted to HTML, PDF, and other formats. In particular, we use it to generate the documentation on http://docs.pybamm.org/

Building the documentation#

To test and debug the documentation, it’s best to build it locally. To do this, navigate to your PyBaMM directory in a console, and then type (on GNU/Linux, macOS, and Windows):

nox -s docs

And then visit the webpage served at http://127.0.0.1:8000. Each time a change to the documentation source is detected, the HTML is rebuilt and the browser automatically reloaded. In CI, the docs are built and tested using the docs session in the noxfile.py file with warnings turned into errors, to fail the build. The warnings can be removed or ignored by adding the appropriate warning identifier to the suppress_warnings list in docs/conf.py.

Example notebooks#

Major PyBaMM features are showcased in Jupyter notebooks stored in the docs/source/examples directory. Which features are “major” is of course wholly subjective, so please discuss on GitHub first!

All example notebooks should be listed in docs/source/examples/index.rst. Please follow the (naming and writing) style of existing notebooks where possible.

All the notebooks are tested daily.

Citations#

We aim to recognize all contributions by automatically generating citations to the relevant papers on which different parts of the code are built. These will change depending on what models and solvers you use. Adding the command

pybamm.print_citations()

to the end of a script will print all citations that were used by that script. This will print BibTeX information to the terminal; passing a filename to print_citations will print the BibTeX information to the specified file instead.

When you contribute code to PyBaMM, you can add your own papers that you would like to be cited if that code is used. First, add the BibTeX for your paper to CITATIONS.bib. Then, add the line

pybamm.citations.register("your_paper_bibtex_identifier")

wherever code is called that uses that citation (for example, in functions or in the __init__ method of a class such as a model or solver).

Infrastructure#

Installation#

Installation of PyBaMM and its dependencies is handled via pip and setuptools. It uses CMake to compile C++ extensions using pybind11 and casadi. The installation process is described in detail in the source installation page and is configured through the CMakeLists.txt file.

Configuration files:

setup.py
pyproject.toml
MANIFEST.in

Note: MANIFEST.in is used to include and exclude non-Python files and auxiliary package data for PyBaMM when distributing it. If a file is not included in MANIFEST.in, it will not be included in the source distribution (SDist) and subsequently not be included in the binary distribution (wheel).

Continuous Integration using GitHub Actions#

Each change pushed to the PyBaMM GitHub repository will trigger the test and benchmark suites to be run, using GitHub Actions.

Tests are run for different operating systems, and for all Python versions officially supported by PyBaMM. If you opened a Pull Request, feedback is directly available on the corresponding page. If all tests pass, a green tick will be displayed next to the corresponding test run. If one or more test(s) fail, a red cross will be displayed instead.

Similarly, the benchmark suite is automatically run for the most recently pushed commit. Benchmark results are compared to the results available for the latest commit on the develop branch. Should any significant performance regression be found, a red cross will be displayed next to the benchmark run.

In all cases, more details can be obtained by clicking on a specific run.

Configuration files for various GitHub actions workflow can be found in .github/worklfows.

Codecov#

Code coverage (how much of our code is actually seen by the (linux) unit tests) is tested using Codecov, a report is visible on https://codecov.io/gh/pybamm-team/PyBaMM.

Configuration files:

.coveragerc
Read the Docs#

Documentation is built using https://readthedocs.org/ and published on http://docs.pybamm.org/.

Google Colab#

Editable notebooks are made available using Google Colab here.

GitHub#

GitHub does some magic with particular filenames. In particular:

Acknowledgements#

This CONTRIBUTING.md file, along with large sections of the code infrastructure, was copied from the excellent Pints GitHub repo

Example notebooks#

PyBaMM ships with example notebooks that demonstrate how to use it and reveal some of its functionalities and its inner workings. For more examples, see the Examples section.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 1 - How to run a model#

Welcome to PyBaMM! In this notebook, we will run your first PyBaMM model in just a few simple lines.

To run through this jupyter notebook simply shift-enter to run the cells. If you are unfamiliar with Jupyter notebooks we recommend checking out this cheat sheet.

We begin by importing the PyBaMM library into this notebook:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

We now load the model that we wish to run. For this notebook, we choose the Doyle-Fuller-Newman (DFN) model:

[2]:
model = pybamm.lithium_ion.DFN()

We now use this model to create a PyBaMM Simulation, which is used to process and solve the model:

[3]:
sim = pybamm.Simulation(model)

We can then call ‘solve’ on our simulation object to solve the model, passing the window of time to solve for in seconds (here 1 hour):

[4]:
sim.solve([0, 3600])
[4]:
<pybamm.solvers.solution.Solution at 0x7f2af3787950>

Finally, we can call ‘plot’ to generate a dynamic plot of the key variables:

[5]:
sim.plot()

In this tutorial, we have solved a model with the inbuilt default settings. However, PyBaMM is designed to be highly customisable. Over the course of the getting started tutorials, we will see how various settings can be changed so that the model is appropriate for your situation.

In Tutorial 2 we cover how to simulate and compare different models.

References#

If you write a paper that uses PyBaMM, we would be grateful if you could cite the papers relevant to your code. These will change depending on what models and solvers you use. To find out which papers you should cite, you can run:

[6]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Alternatively, you can print the citations in BibTeX format by running

pybamm.print_citations(output_format="bibtex")

In both cases, you can pass the extra argument filename to store the citations into a file.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 2 - Compare models#

In Tutorial 1, we saw how to run a PyBaMM simulation of the DFN model. However, PyBaMM includes other standard electrochemical models such as the Single Particle Model (SPM) and the Single Particle Model with electrolyte (SPMe). In this tutorial, we will see how to simulate and compare these three models.

Again, the first step is to import the pybamm library into the notebook:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

We start creating a list of all the models we wish to solve

[2]:
models = [
    pybamm.lithium_ion.SPM(),
    pybamm.lithium_ion.SPMe(),
    pybamm.lithium_ion.DFN(),
]

and now we can loop over the list, creating and solving simulations as we go. The solved simulations are stored in the list sims

[3]:
sims = []
for model in models:
    sim = pybamm.Simulation(model)
    sim.solve([0, 3600])
    sims.append(sim)

We can now pass our list of simulations to the dynamic plot method, which will plot the different outputs in the same figure

[4]:
pybamm.dynamic_plot(sims, time_unit="seconds")
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fab1bee6490>

In this tutorial we have seen how easy it is to run and compare different electrochemical models. In Tutorial 3 we show how to create different plots using PyBaMM’s built-in plotting capability.

References#

The relevant papers for this notebook are:

[5]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 3 - Basic plotting#

In Tutorial 2, we made use of PyBaMM’s automatic plotting function when comparing models. This gave a good quick overview of many of the key variables in the model. However, by passing in just a few arguments it is easy to plot any of the many other variables that may be of interest to you. We start by building and solving a model as before:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt

model_dfn = pybamm.lithium_ion.DFN()
sim_dfn = pybamm.Simulation(model_dfn)
sim_dfn.solve([0, 3600])
Note: you may need to restart the kernel to use updated packages.
[1]:
<pybamm.solvers.solution.Solution at 0x139a534f0>

We now want to plot a selection of the model variables. To see a full list of the available variables just type:

[2]:
model_dfn.variable_names()
[2]:
['Time [s]',
 'Time [min]',
 'Time [h]',
 'x [m]',
 'x_n [m]',
 'x_s [m]',
 'x_p [m]',
 'r_n [m]',
 'r_p [m]',
 'Current variable [A]',
 'Total current density [A.m-2]',
 'Current [A]',
 'C-rate',
 'Discharge capacity [A.h]',
 'Discharge energy [W.h]',
 'Throughput energy [W.h]',
 'Throughput capacity [A.h]',
 'Porosity',
 'Negative electrode porosity',
 'X-averaged negative electrode porosity',
 'Separator porosity',
 'X-averaged separator porosity',
 'Positive electrode porosity',
 'X-averaged positive electrode porosity',
 'Porosity change',
 'Negative electrode porosity change [s-1]',
 'X-averaged negative electrode porosity change [s-1]',
 'Separator porosity change [s-1]',
 'X-averaged separator porosity change [s-1]',
 'Positive electrode porosity change [s-1]',
 'X-averaged positive electrode porosity change [s-1]',
 'Negative electrode interface utilisation variable',
 'X-averaged negative electrode interface utilisation variable',
 'Negative electrode interface utilisation',
 'X-averaged negative electrode interface utilisation',
 'Positive electrode interface utilisation variable',
 'X-averaged positive electrode interface utilisation variable',
 'Positive electrode interface utilisation',
 'X-averaged positive electrode interface utilisation',
 'Negative particle crack length [m]',
 'X-averaged negative particle crack length [m]',
 'Negative particle cracking rate [m.s-1]',
 'X-averaged negative particle cracking rate [m.s-1]',
 'Positive particle crack length [m]',
 'X-averaged positive particle crack length [m]',
 'Positive particle cracking rate [m.s-1]',
 'X-averaged positive particle cracking rate [m.s-1]',
 'Negative electrode active material volume fraction',
 'X-averaged negative electrode active material volume fraction',
 'Negative electrode capacity [A.h]',
 'Negative particle radius',
 'Negative particle radius [m]',
 'X-averaged negative particle radius [m]',
 'Negative electrode surface area to volume ratio [m-1]',
 'X-averaged negative electrode surface area to volume ratio [m-1]',
 'Negative electrode active material volume fraction change [s-1]',
 'X-averaged negative electrode active material volume fraction change [s-1]',
 'Loss of lithium due to loss of active material in negative electrode [mol]',
 'Positive electrode active material volume fraction',
 'X-averaged positive electrode active material volume fraction',
 'Positive electrode capacity [A.h]',
 'Positive particle radius',
 'Positive particle radius [m]',
 'X-averaged positive particle radius [m]',
 'Positive electrode surface area to volume ratio [m-1]',
 'X-averaged positive electrode surface area to volume ratio [m-1]',
 'Positive electrode active material volume fraction change [s-1]',
 'X-averaged positive electrode active material volume fraction change [s-1]',
 'Loss of lithium due to loss of active material in positive electrode [mol]',
 'Separator pressure [Pa]',
 'X-averaged separator pressure [Pa]',
 'negative electrode transverse volume-averaged velocity [m.s-1]',
 'X-averaged negative electrode transverse volume-averaged velocity [m.s-1]',
 'separator transverse volume-averaged velocity [m.s-1]',
 'X-averaged separator transverse volume-averaged velocity [m.s-1]',
 'positive electrode transverse volume-averaged velocity [m.s-1]',
 'X-averaged positive electrode transverse volume-averaged velocity [m.s-1]',
 'Transverse volume-averaged velocity [m.s-1]',
 'negative electrode transverse volume-averaged acceleration [m.s-2]',
 'X-averaged negative electrode transverse volume-averaged acceleration [m.s-2]',
 'separator transverse volume-averaged acceleration [m.s-2]',
 'X-averaged separator transverse volume-averaged acceleration [m.s-2]',
 'positive electrode transverse volume-averaged acceleration [m.s-2]',
 'X-averaged positive electrode transverse volume-averaged acceleration [m.s-2]',
 'Transverse volume-averaged acceleration [m.s-2]',
 'Negative electrode volume-averaged velocity [m.s-1]',
 'Negative electrode volume-averaged acceleration [m.s-2]',
 'X-averaged negative electrode volume-averaged acceleration [m.s-2]',
 'Negative electrode pressure [Pa]',
 'X-averaged negative electrode pressure [Pa]',
 'Positive electrode volume-averaged velocity [m.s-1]',
 'Positive electrode volume-averaged acceleration [m.s-2]',
 'X-averaged positive electrode volume-averaged acceleration [m.s-2]',
 'Positive electrode pressure [Pa]',
 'X-averaged positive electrode pressure [Pa]',
 'Negative particle stoichiometry',
 'Negative particle concentration',
 'Negative particle concentration [mol.m-3]',
 'X-averaged negative particle concentration',
 'X-averaged negative particle concentration [mol.m-3]',
 'R-averaged negative particle concentration',
 'R-averaged negative particle concentration [mol.m-3]',
 'Average negative particle concentration',
 'Average negative particle concentration [mol.m-3]',
 'Negative particle surface stoichiometry',
 'Negative particle surface concentration',
 'Negative particle surface concentration [mol.m-3]',
 'X-averaged negative particle surface concentration',
 'X-averaged negative particle surface concentration [mol.m-3]',
 'Negative electrode extent of lithiation',
 'X-averaged negative electrode extent of lithiation',
 'Minimum negative particle concentration',
 'Maximum negative particle concentration',
 'Minimum negative particle concentration [mol.m-3]',
 'Maximum negative particle concentration [mol.m-3]',
 'Minimum negative particle surface concentration',
 'Maximum negative particle surface concentration',
 'Minimum negative particle surface concentration [mol.m-3]',
 'Maximum negative particle surface concentration [mol.m-3]',
 'Positive particle stoichiometry',
 'Positive particle concentration',
 'Positive particle concentration [mol.m-3]',
 'X-averaged positive particle concentration',
 'X-averaged positive particle concentration [mol.m-3]',
 'R-averaged positive particle concentration',
 'R-averaged positive particle concentration [mol.m-3]',
 'Average positive particle concentration',
 'Average positive particle concentration [mol.m-3]',
 'Positive particle surface stoichiometry',
 'Positive particle surface concentration',
 'Positive particle surface concentration [mol.m-3]',
 'X-averaged positive particle surface concentration',
 'X-averaged positive particle surface concentration [mol.m-3]',
 'Positive electrode extent of lithiation',
 'X-averaged positive electrode extent of lithiation',
 'Minimum positive particle concentration',
 'Maximum positive particle concentration',
 'Minimum positive particle concentration [mol.m-3]',
 'Maximum positive particle concentration [mol.m-3]',
 'Minimum positive particle surface concentration',
 'Maximum positive particle surface concentration',
 'Minimum positive particle surface concentration [mol.m-3]',
 'Maximum positive particle surface concentration [mol.m-3]',
 'Negative electrode potential [V]',
 'X-averaged negative electrode potential [V]',
 'Negative electrode ohmic losses [V]',
 'X-averaged negative electrode ohmic losses [V]',
 'Gradient of negative electrode potential [V.m-1]',
 'Positive electrode potential [V]',
 'X-averaged positive electrode potential [V]',
 'Positive electrode ohmic losses [V]',
 'X-averaged positive electrode ohmic losses [V]',
 'Gradient of positive electrode potential [V.m-1]',
 'Porosity times concentration [mol.m-3]',
 'Negative electrode porosity times concentration [mol.m-3]',
 'Separator porosity times concentration [mol.m-3]',
 'Positive electrode porosity times concentration [mol.m-3]',
 'Total lithium in electrolyte [mol]',
 'Electrolyte potential [V]',
 'X-averaged electrolyte potential [V]',
 'X-averaged electrolyte overpotential [V]',
 'Gradient of electrolyte potential [V.m-1]',
 'Negative electrolyte potential [V]',
 'X-averaged negative electrolyte potential [V]',
 'Gradient of negative electrolyte potential [V.m-1]',
 'Separator electrolyte potential [V]',
 'X-averaged separator electrolyte potential [V]',
 'Gradient of separator electrolyte potential [V.m-1]',
 'Positive electrolyte potential [V]',
 'X-averaged positive electrolyte potential [V]',
 'Gradient of positive electrolyte potential [V.m-1]',
 'Ambient temperature [K]',
 'Cell temperature [K]',
 'Negative current collector temperature [K]',
 'Positive current collector temperature [K]',
 'X-averaged cell temperature [K]',
 'Volume-averaged cell temperature [K]',
 'Negative electrode temperature [K]',
 'X-averaged negative electrode temperature [K]',
 'Separator temperature [K]',
 'X-averaged separator temperature [K]',
 'Positive electrode temperature [K]',
 'X-averaged positive electrode temperature [K]',
 'Ambient temperature [C]',
 'Cell temperature [C]',
 'Negative current collector temperature [C]',
 'Positive current collector temperature [C]',
 'X-averaged cell temperature [C]',
 'Volume-averaged cell temperature [C]',
 'Negative electrode temperature [C]',
 'X-averaged negative electrode temperature [C]',
 'Separator temperature [C]',
 'X-averaged separator temperature [C]',
 'Positive electrode temperature [C]',
 'X-averaged positive electrode temperature [C]',
 'Negative current collector potential [V]',
 'Inner SEI thickness [m]',
 'Outer SEI thickness [m]',
 'X-averaged inner SEI thickness [m]',
 'X-averaged outer SEI thickness [m]',
 'SEI [m]',
 'Total SEI thickness [m]',
 'X-averaged SEI thickness [m]',
 'X-averaged total SEI thickness [m]',
 'X-averaged negative electrode resistance [Ohm.m2]',
 'Inner SEI interfacial current density [A.m-2]',
 'X-averaged inner SEI interfacial current density [A.m-2]',
 'Outer SEI interfacial current density [A.m-2]',
 'X-averaged outer SEI interfacial current density [A.m-2]',
 'SEI interfacial current density [A.m-2]',
 'X-averaged SEI interfacial current density [A.m-2]',
 'Inner SEI on cracks thickness [m]',
 'Outer SEI on cracks thickness [m]',
 'X-averaged inner SEI on cracks thickness [m]',
 'X-averaged outer SEI on cracks thickness [m]',
 'SEI on cracks [m]',
 'Total SEI on cracks thickness [m]',
 'X-averaged SEI on cracks thickness [m]',
 'X-averaged total SEI on cracks thickness [m]',
 'Inner SEI on cracks interfacial current density [A.m-2]',
 'X-averaged inner SEI on cracks interfacial current density [A.m-2]',
 'Outer SEI on cracks interfacial current density [A.m-2]',
 'X-averaged outer SEI on cracks interfacial current density [A.m-2]',
 'SEI on cracks interfacial current density [A.m-2]',
 'X-averaged SEI on cracks interfacial current density [A.m-2]',
 'Lithium plating concentration [mol.m-3]',
 'X-averaged lithium plating concentration [mol.m-3]',
 'Dead lithium concentration [mol.m-3]',
 'X-averaged dead lithium concentration [mol.m-3]',
 'Lithium plating thickness [m]',
 'X-averaged lithium plating thickness [m]',
 'Dead lithium thickness [m]',
 'X-averaged dead lithium thickness [m]',
 'Loss of lithium to lithium plating [mol]',
 'Loss of capacity to lithium plating [A.h]',
 'Negative electrode lithium plating reaction overpotential [V]',
 'X-averaged negative electrode lithium plating reaction overpotential [V]',
 'Lithium plating interfacial current density [A.m-2]',
 'X-averaged lithium plating interfacial current density [A.m-2]',
 'Negative crack surface to volume ratio [m-1]',
 'Negative electrode roughness ratio',
 'X-averaged negative electrode roughness ratio',
 'Positive crack surface to volume ratio [m-1]',
 'Positive electrode roughness ratio',
 'X-averaged positive electrode roughness ratio',
 'Electrolyte transport efficiency',
 'Negative electrolyte transport efficiency',
 'X-averaged negative electrolyte transport efficiency',
 'Separator electrolyte transport efficiency',
 'X-averaged separator electrolyte transport efficiency',
 'Positive electrolyte transport efficiency',
 'X-averaged positive electrolyte transport efficiency',
 'Electrode transport efficiency',
 'Negative electrode transport efficiency',
 'X-averaged negative electrode transport efficiency',
 'Separator electrode transport efficiency',
 'X-averaged separator electrode transport efficiency',
 'Positive electrode transport efficiency',
 'X-averaged positive electrode transport efficiency',
 'Separator volume-averaged velocity [m.s-1]',
 'Separator volume-averaged acceleration [m.s-2]',
 'X-averaged separator volume-averaged acceleration [m.s-2]',
 'Volume-averaged velocity [m.s-1]',
 'Volume-averaged acceleration [m.s-1]',
 'X-averaged volume-averaged acceleration [m.s-1]',
 'Pressure [Pa]',
 'Negative electrode open-circuit potential [V]',
 'X-averaged negative electrode open-circuit potential [V]',
 'Negative electrode entropic change [V.K-1]',
 'X-averaged negative electrode entropic change [V.K-1]',
 'Positive electrode open-circuit potential [V]',
 'X-averaged positive electrode open-circuit potential [V]',
 'Positive electrode entropic change [V.K-1]',
 'X-averaged positive electrode entropic change [V.K-1]',
 'Negative electrode effective conductivity',
 'Negative electrode current density [A.m-2]',
 'Positive electrode effective conductivity',
 'Positive electrode current density [A.m-2]',
 'Electrode current density [A.m-2]',
 'Positive current collector potential [V]',
 'Local voltage [V]',
 'Voltage [V]',
 'Contact overpotential [V]',
 'Electrolyte concentration concatenation [mol.m-3]',
 'Negative electrolyte concentration [mol.m-3]',
 'X-averaged negative electrolyte concentration [mol.m-3]',
 'Separator electrolyte concentration [mol.m-3]',
 'X-averaged separator electrolyte concentration [mol.m-3]',
 'Positive electrolyte concentration [mol.m-3]',
 'X-averaged positive electrolyte concentration [mol.m-3]',
 'Negative electrolyte concentration',
 'Negative electrolyte concentration [Molar]',
 'X-averaged negative electrolyte concentration',
 'X-averaged negative electrolyte concentration [Molar]',
 'Separator electrolyte concentration',
 'Separator electrolyte concentration [Molar]',
 'X-averaged separator electrolyte concentration',
 'X-averaged separator electrolyte concentration [Molar]',
 'Positive electrolyte concentration',
 'Positive electrolyte concentration [Molar]',
 'X-averaged positive electrolyte concentration',
 'X-averaged positive electrolyte concentration [Molar]',
 'Electrolyte concentration [mol.m-3]',
 'X-averaged electrolyte concentration [mol.m-3]',
 'Electrolyte concentration',
 'Electrolyte concentration [Molar]',
 'X-averaged electrolyte concentration',
 'X-averaged electrolyte concentration [Molar]',
 'Electrolyte current density [A.m-2]',
 'X-averaged concentration overpotential [V]',
 'X-averaged electrolyte ohmic losses [V]',
 'Negative electrode surface potential difference [V]',
 'X-averaged negative electrode surface potential difference [V]',
 'Positive electrode surface potential difference [V]',
 'X-averaged positive electrode surface potential difference [V]',
 'Ohmic heating [W.m-3]',
 'X-averaged Ohmic heating [W.m-3]',
 'Volume-averaged Ohmic heating [W.m-3]',
 'Irreversible electrochemical heating [W.m-3]',
 'X-averaged irreversible electrochemical heating [W.m-3]',
 'Volume-averaged irreversible electrochemical heating [W.m-3]',
 'Reversible heating [W.m-3]',
 'X-averaged reversible heating [W.m-3]',
 'Volume-averaged reversible heating [W.m-3]',
 'Total heating [W.m-3]',
 'X-averaged total heating [W.m-3]',
 'Volume-averaged total heating [W.m-3]',
 'Current collector current density [A.m-2]',
 'Inner SEI concentration [mol.m-3]',
 'X-averaged inner SEI concentration [mol.m-3]',
 'Outer SEI concentration [mol.m-3]',
 'X-averaged outer SEI concentration [mol.m-3]',
 'SEI concentration [mol.m-3]',
 'X-averaged SEI concentration [mol.m-3]',
 'Loss of lithium to SEI [mol]',
 'Loss of capacity to SEI [A.h]',
 'X-averaged negative electrode SEI interfacial current density [A.m-2]',
 'Negative electrode SEI interfacial current density [A.m-2]',
 'Positive electrode SEI interfacial current density [A.m-2]',
 'X-averaged positive electrode SEI volumetric interfacial current density [A.m-2]',
 'Positive electrode SEI volumetric interfacial current density [A.m-3]',
 'Negative electrode SEI volumetric interfacial current density [A.m-3]',
 'X-averaged negative electrode SEI volumetric interfacial current density [A.m-3]',
 'Inner SEI on cracks concentration [mol.m-3]',
 'X-averaged inner SEI on cracks concentration [mol.m-3]',
 'Outer SEI on cracks concentration [mol.m-3]',
 'X-averaged outer SEI on cracks concentration [mol.m-3]',
 'SEI on cracks concentration [mol.m-3]',
 'X-averaged SEI on cracks concentration [mol.m-3]',
 'Loss of lithium to SEI on cracks [mol]',
 'Loss of capacity to SEI on cracks [A.h]',
 'X-averaged negative electrode SEI on cracks interfacial current density [A.m-2]',
 'Negative electrode SEI on cracks interfacial current density [A.m-2]',
 'Positive electrode SEI on cracks interfacial current density [A.m-2]',
 'X-averaged positive electrode SEI on cracks volumetric interfacial current density [A.m-2]',
 'Positive electrode SEI on cracks volumetric interfacial current density [A.m-3]',
 'Negative electrode SEI on cracks volumetric interfacial current density [A.m-3]',
 'X-averaged negative electrode SEI on cracks volumetric interfacial current density [A.m-3]',
 'Negative electrode lithium plating interfacial current density [A.m-2]',
 'X-averaged negative electrode lithium plating interfacial current density [A.m-2]',
 'Lithium plating volumetric interfacial current density [A.m-3]',
 'X-averaged lithium plating volumetric interfacial current density [A.m-3]',
 'X-averaged positive electrode lithium plating interfacial current density [A.m-2]',
 'X-averaged positive electrode lithium plating volumetric interfacial current density [A.m-3]',
 'Positive electrode lithium plating interfacial current density [A.m-2]',
 'Positive electrode lithium plating volumetric interfacial current density [A.m-3]',
 'Negative electrode lithium plating volumetric interfacial current density [A.m-3]',
 'X-averaged negative electrode lithium plating volumetric interfacial current density [A.m-3]',
 'Negative electrode interfacial current density [A.m-2]',
 'X-averaged negative electrode interfacial current density [A.m-2]',
 'X-averaged negative electrode total interfacial current density [A.m-2]',
 'X-averaged negative electrode total volumetric interfacial current density [A.m-3]',
 'Negative electrode exchange current density [A.m-2]',
 'X-averaged negative electrode exchange current density [A.m-2]',
 'Negative electrode reaction overpotential [V]',
 'X-averaged negative electrode reaction overpotential [V]',
 'Negative electrode volumetric interfacial current density [A.m-3]',
 'X-averaged negative electrode volumetric interfacial current density [A.m-3]',
 'SEI film overpotential [V]',
 'X-averaged SEI film overpotential [V]',
 'Positive electrode interfacial current density [A.m-2]',
 'X-averaged positive electrode interfacial current density [A.m-2]',
 'X-averaged positive electrode total interfacial current density [A.m-2]',
 'X-averaged positive electrode total volumetric interfacial current density [A.m-3]',
 'Positive electrode exchange current density [A.m-2]',
 'X-averaged positive electrode exchange current density [A.m-2]',
 'Positive electrode reaction overpotential [V]',
 'X-averaged positive electrode reaction overpotential [V]',
 'Positive electrode volumetric interfacial current density [A.m-3]',
 'X-averaged positive electrode volumetric interfacial current density [A.m-3]',
 'Negative particle rhs [mol.m-3.s-1]',
 'Negative particle bc [mol.m-2]',
 'Negative particle effective diffusivity [m2.s-1]',
 'X-averaged negative particle effective diffusivity [m2.s-1]',
 'Negative particle flux [mol.m-2.s-1]',
 'Negative electrode stoichiometry',
 'Negative electrode volume-averaged concentration',
 'Negative electrode volume-averaged concentration [mol.m-3]',
 'Total lithium in primary phase in negative electrode [mol]',
 'Positive particle rhs [mol.m-3.s-1]',
 'Positive particle bc [mol.m-2]',
 'Positive particle effective diffusivity [m2.s-1]',
 'X-averaged positive particle effective diffusivity [m2.s-1]',
 'Positive particle flux [mol.m-2.s-1]',
 'Positive electrode stoichiometry',
 'Positive electrode volume-averaged concentration',
 'Positive electrode volume-averaged concentration [mol.m-3]',
 'Total lithium in primary phase in positive electrode [mol]',
 'Electrolyte flux [mol.m-2.s-1]',
 'Electrolyte diffusion flux [mol.m-2.s-1]',
 'Electrolyte migration flux [mol.m-2.s-1]',
 'Electrolyte convection flux [mol.m-2.s-1]',
 'Sum of negative electrode electrolyte reaction source terms [A.m-3]',
 'Sum of x-averaged negative electrode electrolyte reaction source terms [A.m-3]',
 'Sum of negative electrode volumetric interfacial current densities [A.m-3]',
 'Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]',
 'Sum of positive electrode electrolyte reaction source terms [A.m-3]',
 'Sum of x-averaged positive electrode electrolyte reaction source terms [A.m-3]',
 'Sum of positive electrode volumetric interfacial current densities [A.m-3]',
 'Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]',
 'Interfacial current density [A.m-2]',
 'Exchange current density [A.m-2]',
 'Sum of volumetric interfacial current densities [A.m-3]',
 'Sum of electrolyte reaction source terms [A.m-3]',
 'X-averaged open-circuit voltage [V]',
 'Measured open-circuit voltage [V]',
 'X-averaged reaction overpotential [V]',
 'X-averaged solid phase ohmic losses [V]',
 'X-averaged battery open-circuit voltage [V]',
 'Measured battery open-circuit voltage [V]',
 'X-averaged battery reaction overpotential [V]',
 'X-averaged battery solid phase ohmic losses [V]',
 'X-averaged battery electrolyte ohmic losses [V]',
 'X-averaged battery concentration overpotential [V]',
 'Battery voltage [V]',
 'Change in measured open-circuit voltage [V]',
 'Local ECM resistance [Ohm]',
 'Terminal power [W]',
 'Power [W]',
 'Resistance [Ohm]',
 'Total lithium in negative electrode [mol]',
 'LAM_ne [%]',
 'Loss of active material in negative electrode [%]',
 'Total lithium in positive electrode [mol]',
 'LAM_pe [%]',
 'Loss of active material in positive electrode [%]',
 'LLI [%]',
 'Loss of lithium inventory [%]',
 'Loss of lithium inventory, including electrolyte [%]',
 'Total lithium [mol]',
 'Total lithium in particles [mol]',
 'Total lithium capacity [A.h]',
 'Total lithium capacity in particles [A.h]',
 'Total lithium lost [mol]',
 'Total lithium lost from particles [mol]',
 'Total lithium lost from electrolyte [mol]',
 'Total lithium lost to side reactions [mol]',
 'Total capacity lost to side reactions [A.h]']

There are a lot of variables. You can also search the list of variables for a particular string (e.g. “electrolyte”)

[3]:
model_dfn.variables.search("electrolyte")
Electrolyte concentration
Electrolyte concentration [Molar]
Electrolyte concentration [mol.m-3]
Electrolyte concentration concatenation [mol.m-3]
Electrolyte convection flux [mol.m-2.s-1]
Electrolyte current density [A.m-2]
Electrolyte diffusion flux [mol.m-2.s-1]
Electrolyte flux [mol.m-2.s-1]
Electrolyte migration flux [mol.m-2.s-1]
Electrolyte potential [V]
Electrolyte transport efficiency
Gradient of electrolyte potential [V.m-1]
Gradient of negative electrolyte potential [V.m-1]
Gradient of positive electrolyte potential [V.m-1]
Gradient of separator electrolyte potential [V.m-1]
Loss of lithium inventory, including electrolyte [%]
Negative electrolyte concentration
Negative electrolyte concentration [Molar]
Negative electrolyte concentration [mol.m-3]
Negative electrolyte potential [V]
Negative electrolyte transport efficiency
Positive electrolyte concentration
Positive electrolyte concentration [Molar]
Positive electrolyte concentration [mol.m-3]
Positive electrolyte potential [V]
Positive electrolyte transport efficiency
Separator electrolyte concentration
Separator electrolyte concentration [Molar]
Separator electrolyte concentration [mol.m-3]
Separator electrolyte potential [V]
Separator electrolyte transport efficiency
Sum of electrolyte reaction source terms [A.m-3]
Sum of negative electrode electrolyte reaction source terms [A.m-3]
Sum of positive electrode electrolyte reaction source terms [A.m-3]
Sum of x-averaged negative electrode electrolyte reaction source terms [A.m-3]
Sum of x-averaged positive electrode electrolyte reaction source terms [A.m-3]
Total lithium in electrolyte [mol]
Total lithium lost from electrolyte [mol]
X-averaged battery electrolyte ohmic losses [V]
X-averaged electrolyte concentration
X-averaged electrolyte concentration [Molar]
X-averaged electrolyte concentration [mol.m-3]
X-averaged electrolyte ohmic losses [V]
X-averaged electrolyte overpotential [V]
X-averaged electrolyte potential [V]
X-averaged negative electrolyte concentration
X-averaged negative electrolyte concentration [Molar]
X-averaged negative electrolyte concentration [mol.m-3]
X-averaged negative electrolyte potential [V]
X-averaged negative electrolyte transport efficiency
X-averaged positive electrolyte concentration
X-averaged positive electrolyte concentration [Molar]
X-averaged positive electrolyte concentration [mol.m-3]
X-averaged positive electrolyte potential [V]
X-averaged positive electrolyte transport efficiency
X-averaged separator electrolyte concentration
X-averaged separator electrolyte concentration [Molar]
X-averaged separator electrolyte concentration [mol.m-3]
X-averaged separator electrolyte potential [V]
X-averaged separator electrolyte transport efficiency

We have tried to make variables names fairly self explanatory.

As a first example, we choose to plot the voltage. We add this to a list and then pass this list to the plot method of our simulation:

[4]:
output_variables = ["Voltage [V]"]
sim_dfn.plot(output_variables=output_variables)
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x1077ee910>

Alternatively, we may be interested in plotting both the electrolyte concentration and the voltage. In which case, we would do:

[5]:
output_variables = ["Electrolyte concentration [mol.m-3]", "Voltage [V]"]
sim_dfn.plot(output_variables=output_variables)
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x13a8fbe80>

You can also plot multiple variables on the same plot by nesting lists

[6]:
sim_dfn.plot(
    [
        ["Electrode current density [A.m-2]", "Electrolyte current density [A.m-2]"],
        "Voltage [V]",
    ]
)
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x13aaadd60>
[7]:
sim_dfn.plot()
[7]:
<pybamm.plotting.quick_plot.QuickPlot at 0x13ab92e20>

For plotting the voltage components you can use the plot_votage_components function

[8]:
pybamm.plot_voltage_components(sim_dfn.solution)
_images/source_examples_notebooks_getting_started_tutorial-3-basic-plotting_16_0.png
[8]:
(<Figure size 640x480 with 1 Axes>, <AxesSubplot: xlabel='Time [h]'>)

And with a few modifications (by creating subplots and by providing the axes on which the voltage components have to be plotted), it can also be used to compare the voltage components of different simulations

[9]:
# simulating and solving Single Particle Model
model_spm = pybamm.lithium_ion.SPM()
sim_spm = pybamm.Simulation(model_spm)
sim_spm.solve([0, 3700])

# comparing voltage components for Doyle-Fuller-Newman model and Single Particle Model
fig, axes = plt.subplots(1, 2, figsize=(15, 6), sharey=True)

pybamm.plot_voltage_components(sim_dfn.solution, ax=axes.flat[0])
pybamm.plot_voltage_components(sim_spm.solution, ax=axes.flat[1])

axes.flat[0].set_title("Doyle-Fuller-Newman Model")
axes.flat[1].set_title("Single Particle Model")

plt.show()
_images/source_examples_notebooks_getting_started_tutorial-3-basic-plotting_18_0.png

In this tutorial we have seen how to use the plotting functionality in PyBaMM.

In Tutorial 4 we show how to change parameter values.

References#

The relevant papers for this notebook are:

[10]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[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.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 4 - Setting parameter values#

In Tutorial 1 and Tutorial 2, we saw how to run a PyBaMM model with all the default settings. However, PyBaMM also allows you to tweak these settings for your application. In this tutorial, we will see how to change the parameters in PyBaMM.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os

os.chdir(pybamm.__path__[0] + "/..")
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/siegeljb/Documents/PyBaMM_Master/PyBaMM/.tox/dev/bin/python3.9 -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

Change the whole parameter set#

PyBaMM has a number of in-built parameter sets (check the list here), which can be selected doing

[2]:
parameter_values = pybamm.ParameterValues("Chen2020")

We can see all the parameters stored in the dictionary

[3]:
parameter_values
[3]:
{'Thermodynamic factor': 1.0,
 'Ambient temperature [K]': 298.15,
 'Bulk solvent concentration [mol.m-3]': 2636.0,
 'Cation transference number': 0.2594,
 'Cell cooling surface area [m2]': 0.00531,
 'Cell thermal expansion coefficient [m.K-1]': 1.1e-06,
 'Cell volume [m3]': 2.42e-05,
 'Current function [A]': 5.0,
 'EC diffusivity [m2.s-1]': 2e-18,
 'EC initial concentration in electrolyte [mol.m-3]': 4541.0,
 'Electrode height [m]': 0.065,
 'Electrode width [m]': 1.58,
 'Electrolyte conductivity [S.m-1]': <function electrolyte_conductivity_Nyman2008 at 0x7fa179f81c10>,
 'Electrolyte diffusivity [m2.s-1]': <function electrolyte_diffusivity_Nyman2008 at 0x7fa179f81b80>,
 'Initial concentration in electrolyte [mol.m-3]': 1000.0,
 'Initial concentration in negative electrode [mol.m-3]': 29866.0,
 'Initial concentration in positive electrode [mol.m-3]': 17038.0,
 'Initial inner SEI thickness [m]': 2.5e-09,
 'Initial outer SEI thickness [m]': 2.5e-09,
 'Initial temperature [K]': 298.15,
 'Inner SEI electron conductivity [S.m-1]': 8.95e-14,
 'Inner SEI lithium interstitial diffusivity [m2.s-1]': 1e-20,
 'Inner SEI open-circuit potential [V]': 0.1,
 'Inner SEI partial molar volume [m3.mol-1]': 9.585e-05,
 'Inner SEI reaction proportion': 0.5,
 'Lithium interstitial reference concentration [mol.m-3]': 15.0,
 'Lower voltage cut-off [V]': 2.5,
 'Maximum concentration in negative electrode [mol.m-3]': 33133.0,
 'Maximum concentration in positive electrode [mol.m-3]': 63104.0,
 'Negative current collector conductivity [S.m-1]': 58411000.0,
 'Negative current collector density [kg.m-3]': 8960.0,
 'Negative current collector specific heat capacity [J.kg-1.K-1]': 385.0,
 'Negative current collector thermal conductivity [W.m-1.K-1]': 401.0,
 'Negative current collector thickness [m]': 1.2e-05,
 'Negative electrode Bruggeman coefficient (electrode)': 1.5,
 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Negative electrode OCP [V]': <function graphite_LGM50_ocp_Chen2020 at 0x7fa179f81940>,
 'Negative electrode OCP entropic change [V.K-1]': 0.0,
 'Negative electrode active material volume fraction': 0.75,
 'Negative electrode cation signed stoichiometry': -1.0,
 'Negative electrode charge transfer coefficient': 0.5,
 'Negative electrode conductivity [S.m-1]': 215.0,
 'Negative electrode density [kg.m-3]': 1657.0,
 'Negative electrode diffusivity [m2.s-1]': 3.3e-14,
 'Negative electrode double-layer capacity [F.m-2]': 0.2,
 'Negative electrode electrons in reaction': 1.0,
 'Negative electrode exchange-current density [A.m-2]': <function graphite_LGM50_electrolyte_exchange_current_density_Chen2020 at 0x7fa179f819d0>,
 'Negative electrode porosity': 0.25,
 'Negative electrode reaction-driven LAM factor [m3.mol-1]': 0.0,
 'Negative electrode specific heat capacity [J.kg-1.K-1]': 700.0,
 'Negative electrode thermal conductivity [W.m-1.K-1]': 1.7,
 'Negative electrode thickness [m]': 8.52e-05,
 'Negative particle radius [m]': 5.86e-06,
 'Nominal cell capacity [A.h]': 5.0,
 'Number of cells connected in series to make a battery': 1.0,
 'Number of electrodes connected in parallel to make a cell': 1.0,
 'Open-circuit voltage at 0% SOC [V]': 2.5,
 'Open-circuit voltage at 100% SOC [V]': 4.2,
 'Outer SEI open-circuit potential [V]': 0.8,
 'Outer SEI partial molar volume [m3.mol-1]': 9.585e-05,
 'Outer SEI solvent diffusivity [m2.s-1]': 2.5000000000000002e-22,
 'Positive current collector conductivity [S.m-1]': 36914000.0,
 'Positive current collector density [kg.m-3]': 2700.0,
 'Positive current collector specific heat capacity [J.kg-1.K-1]': 897.0,
 'Positive current collector thermal conductivity [W.m-1.K-1]': 237.0,
 'Positive current collector thickness [m]': 1.6e-05,
 'Positive electrode Bruggeman coefficient (electrode)': 1.5,
 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Positive electrode OCP [V]': <function nmc_LGM50_ocp_Chen2020 at 0x7fa179f81a60>,
 'Positive electrode OCP entropic change [V.K-1]': 0.0,
 'Positive electrode active material volume fraction': 0.665,
 'Positive electrode cation signed stoichiometry': -1.0,
 'Positive electrode charge transfer coefficient': 0.5,
 'Positive electrode conductivity [S.m-1]': 0.18,
 'Positive electrode density [kg.m-3]': 3262.0,
 'Positive electrode diffusivity [m2.s-1]': 4e-15,
 'Positive electrode double-layer capacity [F.m-2]': 0.2,
 'Positive electrode electrons in reaction': 1.0,
 'Positive electrode exchange-current density [A.m-2]': <function nmc_LGM50_electrolyte_exchange_current_density_Chen2020 at 0x7fa179f81af0>,
 'Positive electrode porosity': 0.335,
 'Positive electrode reaction-driven LAM factor [m3.mol-1]': 0.0,
 'Positive electrode specific heat capacity [J.kg-1.K-1]': 700.0,
 'Positive electrode thermal conductivity [W.m-1.K-1]': 2.1,
 'Positive electrode thickness [m]': 7.56e-05,
 'Positive particle radius [m]': 5.22e-06,
 'Ratio of lithium moles to SEI moles': 2.0,
 'Reference temperature [K]': 298.15,
 'SEI growth activation energy [J.mol-1]': 0.0,
 'SEI kinetic rate constant [m.s-1]': 1e-12,
 'SEI open-circuit potential [V]': 0.4,
 'SEI reaction exchange current density [A.m-2]': 1.5e-07,
 'SEI resistivity [Ohm.m]': 200000.0,
 'Separator Bruggeman coefficient (electrolyte)': 1.5,
 'Separator density [kg.m-3]': 397.0,
 'Separator porosity': 0.47,
 'Separator specific heat capacity [J.kg-1.K-1]': 700.0,
 'Separator thermal conductivity [W.m-1.K-1]': 0.16,
 'Separator thickness [m]': 1.2e-05,
 'Total heat transfer coefficient [W.m-2.K-1]': 10.0,
 'Typical current [A]': 5.0,
 'Typical electrolyte concentration [mol.m-3]': 1000.0,
 'Upper voltage cut-off [V]': 4.2,
 'citations': ['Chen2020']}

or we can search for a particular parameter

[4]:
parameter_values.search("electrolyte")
EC initial concentration in electrolyte [mol.m-3]       4541.0
Electrolyte conductivity [S.m-1]        <function electrolyte_conductivity_Nyman2008 at 0x7fa179f81c10>
Electrolyte diffusivity [m2.s-1]        <function electrolyte_diffusivity_Nyman2008 at 0x7fa179f81b80>
Initial concentration in electrolyte [mol.m-3]  1000.0
Negative electrode Bruggeman coefficient (electrolyte)  1.5
Positive electrode Bruggeman coefficient (electrolyte)  1.5
Separator Bruggeman coefficient (electrolyte)   1.5
Typical electrolyte concentration [mol.m-3]     1000.0

To run a simulation with this parameter set, we can proceed as usual but passing the parameters as a keyword argument

[5]:
model = pybamm.lithium_ion.DFN()
sim = pybamm.Simulation(model, parameter_values=parameter_values)
sim.solve([0, 3600])
sim.plot()
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fa179c67670>

More details on each subset can be found here.

Change individual parameters#

We often want to quickly change a small number of parameter values to investigate how the behaviour or the battery changes. In such cases, we can change parameter values without having to leave the notebook or script you are working in.

We start initialising the model and the parameter values

[6]:
model = pybamm.lithium_ion.DFN()
parameter_values = pybamm.ParameterValues("Chen2020")

In this example we will change the current to 10 A

[11]:
parameter_values["Current function [A]"] = 10
parameter_values["Open-circuit voltage at 100% SOC [V]"] = 3.4
parameter_values["Open-circuit voltage at 0% SOC [V]"] = 3.0

Now we just need to run the simulation with the new parameter values

[12]:
sim = pybamm.Simulation(model, parameter_values=parameter_values)
sim.solve([0, 3600], initial_soc=1)
sim.plot()
[12]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fa172a55220>

Note that we still passed the interval [0, 3600] to sim.solve(), but the simulation terminated early as the lower voltage cut-off was reached.

Drive cycle#

You can implement drive cycles importing the dataset and creating an interpolant to pass as the current function.

[ ]:
import pandas as pd  # needed to read the csv data file

# Import drive cycle from file
drive_cycle = pd.read_csv(
    "pybamm/input/drive_cycles/US06.csv", comment="#", header=None
).to_numpy()

# Create interpolant
current_interpolant = pybamm.Interpolant(drive_cycle[:, 0], drive_cycle[:, 1], pybamm.t)

# Set drive cycle
parameter_values["Current function [A]"] = current_interpolant

Note that your drive cycle data can be stored anywhere, you just need to pass the path of the file. Then, again, the model can be solved as usual but notice that now, if t_eval is not specified, the solver will take the time points from the data set.

[ ]:
model = pybamm.lithium_ion.SPMe()
sim = pybamm.Simulation(model, parameter_values=parameter_values)
sim.solve()
sim.plot(["Current [A]", "Voltage [V]"])
<pybamm.plotting.quick_plot.QuickPlot at 0x7f0fc0d85790>
_images/source_examples_notebooks_getting_started_tutorial-4-setting-parameter-values_25_2.png
Custom current function#

Alternatively, we can define the current to be an arbitrary function of time

[ ]:
import numpy as np


def my_current(t):
    return pybamm.sin(2 * np.pi * t / 60)


parameter_values["Current function [A]"] = my_current

and we can now solve the model again. In this case, we can pass t_eval to the solver to make sure we have enough time points to resolve the function in our output.

[ ]:
model = pybamm.lithium_ion.SPMe()
sim = pybamm.Simulation(model, parameter_values=parameter_values)
t_eval = np.arange(0, 121, 1)
sim.solve(t_eval=t_eval)
sim.plot(["Current [A]", "Voltage [V]"])
<pybamm.plotting.quick_plot.QuickPlot at 0x7f0fc5d3de80>
_images/source_examples_notebooks_getting_started_tutorial-4-setting-parameter-values_30_2.png

In this notebook we have seen how we can change the parameters of our model. In Tutorial 5 we show how can we define and run experiments.

References#

The relevant papers for this notebook are:

[ ]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] 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.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 5 - Run experiments#

In Tutorial 4 we saw how to change the parameters, including the applied current. However, in some cases we might want to prescribe a given voltage, a given power or switch between different conditions to simulate experimental setups. We can use the Experiment class for these simulations.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np

[notice] A new release of pip is available: 23.0.1 -> 23.1.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

String-based instructions#

We start defining an experiment, which consists on a set of instructions on how to cycle the battery. For example, we can set the following experiment:

[2]:
experiment = pybamm.Experiment(
    [
        (
            "Discharge at C/10 for 10 hours or until 3.3 V",
            "Rest for 1 hour",
            "Charge at 1 A until 4.1 V",
            "Hold at 4.1 V until 50 mA",
            "Rest for 1 hour",
        ),
    ]
    * 3
)

A cycle is defined by a tuple of operating instructions. In this case, the experiment consists of a cycle of constant current C/10 discharge, a one hour rest, a constant current (1 A) constant voltage (4.1 V) and another one hour rest, all of it repeated three times (notice the * 3).

Then we can choose our model

[3]:
model = pybamm.lithium_ion.DFN()

and create our simulation, passing our experiment using a keyword argument

[4]:
sim = pybamm.Simulation(model, experiment=experiment)

We then solve and plot the solution

[5]:
sim.solve()
sim.plot()
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x17d7a9490>

As we have seen, experiments allow us to define complex simulations using a very simple syntax. The instructions can be of the form “(Dis)charge at x A/C/W”, “Rest”, or “Hold at x V”. The running time should be a time in seconds, minutes or hours, e.g. “10 seconds”, “3 minutes” or “1 hour”. The stopping conditions should be a circuit state, e.g. “1 A”, “C/50” or “3 V”.

Some examples of experiment instructions are:

"Discharge at 1C for 0.5 hours",
"Discharge at C/20 for 0.5 hours",
"Charge at 0.5 C for 45 minutes",
"Discharge at 1 A for 90 seconds",
"Charge at 200mA for 45 minutes",
"Discharge at 1 W for 0.5 hours",
"Charge at 200 mW for 45 minutes",
"Rest for 10 minutes",
"Hold at 1 V for 20 seconds",
"Charge at 1 C until 4.1V",
"Hold at 4.1 V until 50 mA",
"Hold at 3V until C/50",

Additionally, we can use the operators + and * on lists in order to combine and repeat cycles:

[6]:
[("Discharge at 1C for 0.5 hours", "Discharge at C/20 for 0.5 hours")] * 3 + [
    ("Charge at 0.5 C for 45 minutes",)
]
[6]:
[('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),
 ('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),
 ('Discharge at 1C for 0.5 hours', 'Discharge at C/20 for 0.5 hours'),
 ('Charge at 0.5 C for 45 minutes',)]

To pass additional arguments such as a period, temperature, or tags, the method pybamm.step.string should be used, for example:

[7]:
pybamm.step.string(
    "Discharge at 1C for 1 hour", period="1 minute", temperature="25oC", tags=["tag1"]
)
[7]:
_Step(C-rate, 1.0, duration=1 hour, period=1 minute, temperature=25oC, tags=['tag1'], description=Discharge at 1C for 1 hour)

Direct instructions#

Experiments can also be specified programmatically without having to use string formatting. For example,

[8]:
pybamm.step.current(1, duration="1 hour", termination="2.5 V")
[8]:
_Step(current, 1, duration=1 hour, termination=2.5 V)

is equivalent to

[9]:
pybamm.step.string("Discharge at 1A for 1 hour or until 2.5V")
[9]:
_Step(current, 1.0, duration=1 hour, termination=2.5V, description=Discharge at 1A for 1 hour or until 2.5V)

The available methods are current, c_rate, voltage, power, and resistance.

The period, temperature, and tags options are the same as for pybamm.step.string.

These methods can also be used for drive cycles:

[10]:
t = np.linspace(0, 1, 60)
sin_t = 0.5 * np.sin(2 * np.pi * t)
drive_cycle_power = np.column_stack([t, sin_t])
experiment = pybamm.Experiment([pybamm.step.power(drive_cycle_power)])
sim = pybamm.Simulation(model, experiment=experiment)
sim.solve()
sim.plot()
[10]:
<pybamm.plotting.quick_plot.QuickPlot at 0x2885170d0>

For a drive cycle, the duration is until the final time provided and the period is the smallest time step. For best results, we recommend using a constant time step size.

In this notebook we have seen how to use the Experiment class to run simulations of more complex operating conditions. In Tutorial 6 we will see how to manage the outputs of the simulation.

References#

The relevant papers for this notebook are:

[11]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] 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.
[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.
[7] 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.
[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 6 - Managing simulation outputs#

In the previous tutorials we have interacted with the outputs of the simulation via the default dynamic plot. However, usually we need to access the output data to manipulate it or transfer to another software which is the topic of this notebook.

We start by building and solving our model as shown in previous notebooks:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

model = pybamm.lithium_ion.SPMe()
sim = pybamm.Simulation(model)
sim.solve([0, 3600])
Note: you may need to restart the kernel to use updated packages.
[1]:
<pybamm.solvers.solution.Solution at 0x7f996c4a7e90>

Accessing solution variables#

We can now access the solved variables directly to visualise or create our own plots. We first extract the solution object:

[2]:
solution = sim.solution

and now we can create a post-processed variable (for a list of all the available variables see Tutorial 3)

[3]:
t = solution["Time [s]"]
V = solution["Voltage [V]"]

One option is to visualise the data set returned by the solver directly

[4]:
V.entries
[4]:
array([3.77047806, 3.75305182, 3.74567027, 3.74038822, 3.73581196,
       3.73153391, 3.72742393, 3.72343929, 3.71956623, 3.71580184,
       3.71214621, 3.7086004 , 3.70516561, 3.70184253, 3.69863121,
       3.69553118, 3.69254137, 3.68966018, 3.68688562, 3.68421526,
       3.68164637, 3.67917591, 3.6768006 , 3.67451688, 3.67232094,
       3.67020869, 3.66817572, 3.66621717, 3.66432762, 3.6625009 ,
       3.66072974, 3.65900536, 3.65731692, 3.65565066, 3.65398895,
       3.65230898, 3.65058135, 3.6487688 , 3.64682546, 3.64469798,
       3.64232968, 3.63966973, 3.63668796, 3.63339303, 3.62984711,
       3.62616692, 3.6225045 , 3.61901241, 3.61580868, 3.6129572 ,
       3.61046847, 3.60831405, 3.60644483, 3.60480596, 3.60334607,
       3.60202167, 3.60079822, 3.5996495 , 3.59855637, 3.59750531,
       3.59648723, 3.59549638, 3.59452954, 3.59358541, 3.59266405,
       3.59176646, 3.59089417, 3.59004885, 3.58923192, 3.58844407,
       3.58768477, 3.58695179, 3.58624057, 3.58554372, 3.58485045,
       3.58414611, 3.58341187, 3.58262441, 3.58175587, 3.58077378,
       3.57964098, 3.57831538, 3.5767492 , 3.57488745, 3.57266504,
       3.5700019 , 3.56679523, 3.56290766, 3.5581495 , 3.55225276,
       3.54483361, 3.53533853, 3.52296795, 3.50656968, 3.48449277,
       3.45439366, 3.41299182, 3.35578871, 3.27680072, 3.16842636])

which correspond to the data at the times

[5]:
t.entries
[5]:
array([   0.        ,   36.36363636,   72.72727273,  109.09090909,
        145.45454545,  181.81818182,  218.18181818,  254.54545455,
        290.90909091,  327.27272727,  363.63636364,  400.        ,
        436.36363636,  472.72727273,  509.09090909,  545.45454545,
        581.81818182,  618.18181818,  654.54545455,  690.90909091,
        727.27272727,  763.63636364,  800.        ,  836.36363636,
        872.72727273,  909.09090909,  945.45454545,  981.81818182,
       1018.18181818, 1054.54545455, 1090.90909091, 1127.27272727,
       1163.63636364, 1200.        , 1236.36363636, 1272.72727273,
       1309.09090909, 1345.45454545, 1381.81818182, 1418.18181818,
       1454.54545455, 1490.90909091, 1527.27272727, 1563.63636364,
       1600.        , 1636.36363636, 1672.72727273, 1709.09090909,
       1745.45454545, 1781.81818182, 1818.18181818, 1854.54545455,
       1890.90909091, 1927.27272727, 1963.63636364, 2000.        ,
       2036.36363636, 2072.72727273, 2109.09090909, 2145.45454545,
       2181.81818182, 2218.18181818, 2254.54545455, 2290.90909091,
       2327.27272727, 2363.63636364, 2400.        , 2436.36363636,
       2472.72727273, 2509.09090909, 2545.45454545, 2581.81818182,
       2618.18181818, 2654.54545455, 2690.90909091, 2727.27272727,
       2763.63636364, 2800.        , 2836.36363636, 2872.72727273,
       2909.09090909, 2945.45454545, 2981.81818182, 3018.18181818,
       3054.54545455, 3090.90909091, 3127.27272727, 3163.63636364,
       3200.        , 3236.36363636, 3272.72727273, 3309.09090909,
       3345.45454545, 3381.81818182, 3418.18181818, 3454.54545455,
       3490.90909091, 3527.27272727, 3563.63636364, 3600.        ])

In addition, post-processed variables can be called at any time (by interpolation)

[6]:
V([200, 400, 780, 1236])  # times in seconds
[6]:
array([3.72947892, 3.7086004 , 3.67810702, 3.65400557])

Saving the simulation and output data#

In some cases simulations might take a long time to run so it is advisable to save in your computer so it can be analysed later without re-running the simulation. You can save the whole simulation doing:

[7]:
sim.save("SPMe.pkl")

If you now check the root directory of your notebooks you will notice that a new file called "SPMe.pkl" has appeared. We can load the stored simulation doing

[8]:
sim2 = pybamm.load("SPMe.pkl")

which allows the same manipulation as the original simulation would allow

[9]:
sim2.plot()

Alternatively, we can just save the solution of the simulation in a similar way

[10]:
sol = sim.solution
sol.save("SPMe_sol.pkl")

and load it in a similar way too

[11]:
sol2 = pybamm.load("SPMe_sol.pkl")
pybamm.dynamic_plot(sol2)
[11]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f995f4bd390>

Another option is to just save the data for some variables

[12]:
sol.save_data("sol_data.pkl", ["Current [A]", "Voltage [V]"])

or save in csv or mat format

[13]:
sol.save_data("sol_data.csv", ["Current [A]", "Voltage [V]"], to_format="csv")
# matlab needs names without spaces
sol.save_data(
    "sol_data.mat",
    ["Current [A]", "Voltage [V]"],
    to_format="matlab",
    short_names={"Current [A]": "I", "Voltage [V]": "V"},
)

In this notebook we have shown how to extract and store the outputs of PyBaMM’s simulations. Next, in Tutorial 7 we will show how to change the model options.

Before finishing we will remove the data files we saved so that we leave the directory as we found it

[14]:
import os

os.remove("SPMe.pkl")
os.remove("SPMe_sol.pkl")
os.remove("sol_data.pkl")
os.remove("sol_data.csv")
os.remove("sol_data.mat")

References#

The relevant papers for this notebook are:

[15]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 7 - Model options#

In all of the previous tutorials, we have made use of the default forms of the inbuilt models in PyBaMM. However, PyBaMM provides a high-level interface for tweaking these models for your particular application.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

In this tutorial, we add a thermal model to the SPMe. From the documentation, we see that we have a choice of either a ‘x-full’ thermal model or a number of different lumped thermal models. For a deeper look at the thermal models see the thermal models notebook. We choose the full thermal model, which solves the spatially-dependent heat equation on our battery geometry, and couples the temperature with the electrochemistry. We set the model options by creating a Python dictionary:

[2]:
options = {"thermal": "x-full"}

and passing it to the model. Then, the model can be solved as shown in previous notebooks. We also increase the current to amplify the thermal effects:

[3]:
model = pybamm.lithium_ion.SPMe(options=options)  # loading in options

sim = pybamm.Simulation(model)
sim.solve([0, 3600])
[3]:
<pybamm.solvers.solution.Solution at 0x139461520>

We now plot the cell temperature and the total heating by passing these variables to the plot method as we saw in Tutorial 3:

[4]:
sim.plot(
    ["Cell temperature [K]", "Total heating [W.m-3]", "Current [A]", "Voltage [V]"]
)
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x139461fd0>

In this tutorial we have seen how to adjust the model options. To see all of the options currently available in PyBaMM, please take a look at the documentation here.

In the next tutorial we show how to change the solver options.

References#

The relevant papers for this notebook are:

[5]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.
[5] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 8 - Solver options#

In Tutorial 7 we saw how to change the model options. In this tutorial we will show how to pass options to the solver.

All models in PyBaMM have a default solver which is typically different depending on whether the model results in a system of ordinary differential equations (ODEs) or differential algebraic equations (DAEs).

One of the most common options you will want to change is the solver tolerances. By default all tolerances are set to \(10^{-6}\). However, depending on your simulation you may find you want to tighten the tolerances to obtain a more accurate solution, or you may want to loosen the tolerances to reduce the solve time. It is good practice to conduct a tolerance study, where you simulate the same problem with a tighter tolerances and compare the results. We do not show how to do this here, but we give an example of a mesh resolution study in the next tutorial, which is conducted in a similar way.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Here we will change the absolute and relative tolerances, as well as the “mode” of the CasadiSolver. For a list of all the solver options please consult the documentation.

The CasadiSolver can operate in a number of modes, including “safe” (default) and “fast”. Safe mode performs step-and-check integration and supports event handling (e.g. you can integrate until you hit a certain voltage), and is the recommended for simulations of a full charge or discharge. Fast mode performs direct integration, ignoring events, and is recommended when simulating a drive cycle or other simulation where no events should be triggered.

We’ll solve the DFN with all the default options in both “safe” and “fast” mode and compare the solutions. For both simulations we’ll use \(10^{-3}\) for both the absolute and relative tolerance. For demonstration purposes we’ll change the cut-off voltage to 3.6V so we can observe the different behaviour of the two solver modes.

[2]:
# load model and parameters
model = pybamm.lithium_ion.DFN()
param = model.default_parameter_values
param["Lower voltage cut-off [V]"] = 3.6

# load solvers
safe_solver = pybamm.CasadiSolver(atol=1e-3, rtol=1e-3, mode="safe")
fast_solver = pybamm.CasadiSolver(atol=1e-3, rtol=1e-3, mode="fast")

# create simulations
safe_sim = pybamm.Simulation(model, parameter_values=param, solver=safe_solver)
fast_sim = pybamm.Simulation(model, parameter_values=param, solver=fast_solver)

# solve
safe_sim.solve([0, 3600])
print(f"Safe mode solve time: {safe_sim.solution.solve_time}")
fast_sim.solve([0, 3600])
print(f"Fast mode solve time: {fast_sim.solution.solve_time}")

# plot solutions
pybamm.dynamic_plot([safe_sim, fast_sim])
Safe mode solve time: 297.861 ms
Fast mode solve time: 100.307 ms
[2]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f30b0efa3d0>

We see that both solvers give the same solution up to the time at which the cut-off voltage is reached. At this point the solver using “safe” mode stops, but the solver using “fast” mode carries on integrating until the final time. As its name suggests, “fast” mode integrates more quickly that “safe” mode, but is unsuitable if your simulation required events to be handled.

Usually the default solver options provide a good combination of speed and accuracy, but we encourage you to investigate different solvers and options to find the best combination for your problem.

In the next tutorial we show how to change the mesh.

References#

The relevant papers for this notebook are:

[3]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 9 - Changing the mesh#

In Tutorial 8 we saw how to change the solver options. In this tutorial we will change the mesh used in the simulation, and show how to investigate the influence of the mesh on the solution.

All models in PyBaMM have a default number of mesh points used in a simulation. However, depending on things like the operating conditions you are simulating or the parameters you are using, you may find you need to increase the number points in the mesh to obtain an accurate solution. On the other hand, you may find that you are able to decrease the number of mesh points and still obtain a solution with an acceptable degree of accuracy but in a shorter amount of computational time.

It is always good practice to conduct a mesh refinement study, where you simulate the same problem with a finer mesh and compare the results. Here will show how to do this graphically, but in practice you may wish to do a more detailed calculation of the relative error.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Changing the number of points in the mesh#

First we load a model

[2]:
model = pybamm.lithium_ion.SPMe()

We can then look at the default number of points, which are stored as a dictionary whose keys are the variables for each domain

[3]:
model.default_var_pts
[3]:
{'x_n': 20,
 'x_s': 20,
 'x_p': 20,
 'r_n': 20,
 'r_p': 20,
 'y': 10,
 'z': 10,
 'R_n': 30,
 'R_p': 30}

To run a simulation with a different number of points we can define our own dictionary

[4]:
# create our dictionary
var_pts = {
    "x_n": 10,  # negative electrode
    "x_s": 10,  # separator
    "x_p": 10,  # positive electrode
    "r_n": 10,  # negative particle
    "r_p": 10,  # positive particle
}

We then create and solve a simulation, passing the dictionary of points as a keyword argument

[5]:
sim = pybamm.Simulation(model, var_pts=var_pts)
sim.solve([0, 3600])
[5]:
<pybamm.solvers.solution.Solution at 0x7fc810855a60>

and plot the solution in the usual way

[6]:
sim.plot()
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fc801eb4850>

Conducting a mesh refinement study#

In order to investigate the influence of the mesh on the solution we must solve the model multiple times, increasing the mesh resolution as we go. We first create a list of the number of points per domain we would like to use

[7]:
npts = [4, 8, 16, 32, 64]

and now we can loop over the list, creating and solving simulations as we go. The solutions are stored in the list solutions

[8]:
# choose model and parameters
model = pybamm.lithium_ion.DFN()
parameter_values = pybamm.ParameterValues("Ecker2015")

# choose solver
solver = pybamm.CasadiSolver(mode="fast")

# loop over number of mesh points
solutions = []
for N in npts:
    var_pts = {
        "x_n": N,  # negative electrode
        "x_s": N,  # separator
        "x_p": N,  # positive electrode
        "r_n": N,  # negative particle
        "r_p": N,  # positive particle
    }
    sim = pybamm.Simulation(
        model, solver=solver, parameter_values=parameter_values, var_pts=var_pts
    )
    sim.solve([0, 3600])
    solutions.append(sim.solution)

We can now pass our list of solutions to the dynamic plot method, allowing use to see the influence of the mesh on the computed voltage. We pass our list of points using the labels keyword so that the plots are labeled with the number of points used in the simulation

[9]:
pybamm.dynamic_plot(solutions, ["Voltage [V]"], time_unit="seconds", labels=npts)
[9]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fc802147eb0>

In the next tutorial we show how to create a basic model from scratch in PyBaMM.

References#

The relevant papers for this notebook are:

[10]:
pybamm.print_citations()
[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.
[2] 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.
[3] Madeleine Ecker, Stefan Käbitz, Izaro Laresgoiti, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: II. Model Validation. Journal of The Electrochemical Society, 162(9):A1849–A1857, 2015. doi:10.1149/2.0541509jes.
[4] Madeleine Ecker, Thi Kim Dung Tran, Philipp Dechent, Stefan Käbitz, Alexander Warnecke, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: I. Determination of Parameters. Journal of the Electrochemical Society, 162(9):A1836–A1848, 2015. doi:10.1149/2.0551509jes.
[5] Alastair Hales, Laura Bravo Diaz, Mohamed Waseem Marzook, Yan Zhao, Yatish Patel, and Gregory Offer. The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries. Journal of The Electrochemical Society, 166(12):A2383, 2019.
[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.
[7] 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.
[8] Giles Richardson, Ivan Korotkin, Rahifa Ranom, Michael Castle, and Jamie M. Foster. Generalised single particle models for high-rate operation of graded lithium-ion electrodes: systematic derivation and validation. Electrochimica Acta, 339:135862, 2020. doi:10.1016/j.electacta.2020.135862.
[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.
[10] Yan Zhao, Yatish Patel, Teng Zhang, and Gregory J Offer. Modeling the effects of thermal gradients induced by tab and surface cooling on lithium ion cell performance. Journal of The Electrochemical Society, 165(13):A3169, 2018.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 10 - Creating a model#

In Tutorial 9 we showed how to change the mesh using on of the built-in battery models in PyBaMM. In this tutorial we show how to create a simple model from scratch in PyBaMM.

As simple example, we consider the problem of linear diffusion on a unit sphere with a flux at the boundary that depends on the concentration. We solve

\[\frac{\partial c}{\partial t} = \nabla \cdot (\nabla c),\]

with the following boundary and initial conditions:

\[\left.\frac{\partial c}{\partial r}\right\vert_{r=0} = 0, \quad \left.\frac{\partial c}{\partial r}\right\vert_{r=1} = -j, \quad \left.c\right\vert_{t=0} = c_0,\]

where

\[j = \left.j_0(1-c)^{1/2}c^{1/2}\right\vert_{r=1}\]

Here \(c_0\) and \(j_0\) are parameters we can control. In this example we will assume that everything is non-dimensional and focus on how to set up and solve the model rather than any specific physical interpretation.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Setting up the model#

First we load an empty model. We use the BaseModel class that sets up all the basic framework on which our model will be built.

[2]:
model = pybamm.BaseModel()

We then define our variables and parameters using the Variable and Parameter classes, respectively. Since we are solving a PDE we need to tell PyBaMM the domain each variable belongs to so that it can be discretised in space in the correct way. This is done by passing the keyword argument domain, and in this example we arbitrarily choose the domain “negative particle”.

[3]:
c = pybamm.Variable("Concentration", domain="negative particle")
c0 = pybamm.Parameter("Initial concentration")
j0 = pybamm.Parameter("Flux parameter")

We then state out governing equations. In PyBaMM we distinguish between Ordinary Differential Equations of the form \(dy/dt = \text{rhs}\) and Algebraic Equations of the form \(f(y) = 0\). The model equations are stored in dictionaries where the key is the variable and the value is the rhs for ODEs and the residual (\(f(y)\)) for algebraic equations.

Sometime it is useful to define intermediate quantities in order to express the governing equations more easily. In this example we define the flux, then define the rhs to be minus the divergence of the flux. The equation is then added to the dictionary model.rhs

[4]:
N = -pybamm.grad(c)  # define the flux
dcdt = -pybamm.div(N)  # define the rhs equation

model.rhs = {c: dcdt}  # add the equation to rhs dictionary with the variable as the key

Next we add the necessary boundary and initial conditions to the model. These are also stored in dictionaries called model.boundary_conditions and model.initial_conditions, respectively.

[5]:
# boundary conditions
c_surf = pybamm.surf(c)  # concentration at the surface of the sphere
j = j0 * (1 - c_surf) ** (1 / 2) * c_surf ** (1 / 2)  # prescribed boundary flux
model.boundary_conditions = {c: {"left": (0, "Neumann"), "right": (-j, "Neumann")}}

# initial conditions
model.initial_conditions = {c: c0}

We can add any variables of interest to the dictionary model.variables. These can simply be the variables we solve for (in this case \(c\)) or any other user-defined quantities.

[6]:
model.variables = {
    "Concentration": c,
    "Surface concentration": c_surf,
    "Flux": N,
    "Boundary flux": j,
}

Setting up the geometry and mesh#

In order to solve the model we need to define the geometry and choose how we are going to discretise the equations in space. We first define our radial coordinate using pybamm.SpatialVariable. When we define our spatial variable we pass in a name, the domain on which the variable lives, and the coordinate system we want to use.

[7]:
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)

We can then define our geometry using a dictionary. The key is the name of the domain, and the value is another dictionary which gives the coordinate to use and the limits. In this case we solve on a unit sphere, so we pass out SpatialVariable, r, and the limit 0 and 1.

[8]:
geometry = {"negative particle": {r: {"min": 0, "max": 1}}}

Finally we choose how we are going to discretise in space. We choose to use the Finite Volume method on a uniform mesh with 20 volumes.

[9]:
spatial_methods = {"negative particle": pybamm.FiniteVolume()}
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
# create a mesh of our geometry, using a uniform grid with 20 volumes
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

Solving the model#

Now we are ready to solve the model. First we need to provide values for the parameters in our model. We do this by passing a dictionary of parameter names and values to the pybamm.ParameterValues class.

[10]:
parameter_values = pybamm.ParameterValues(
    {
        "Initial concentration": 0.9,
        "Flux parameter": 0.8,
    }
)

Next we choose a solver. Since this is a system of ODEs we can use the ScipySolver which uses a Runge-Kutta scheme by default.

[11]:
solver = pybamm.ScipySolver()

We can then create a simulation by passing information about the model, geometry, parameters, discretisation and solver to the pybamm.Simulation class.

[12]:
sim = pybamm.Simulation(
    model,
    geometry=geometry,
    parameter_values=parameter_values,
    submesh_types=submesh_types,
    var_pts=var_pts,
    spatial_methods=spatial_methods,
    solver=solver,
)

Finally we can solve the model

[13]:
sim.solve([0, 1])  # solve up to a time of 1
[13]:
<pybamm.solvers.solution.Solution at 0x7f41afa86b20>

The easiest way to quickly plot the results is to call sim.plot to create a slider plot.

Note that at present the plot method is set up to plot dimensional results from battery simulations, so the labels include units which can be ignored (the model assumes a default length scale of 1m and default time scale of 1s). Alternatively we could extract the solution data as seen in Tutorial 6 and create the plots manually. You can find out more about customising plots in this notebook.

[14]:
# pass in a list of the variables we want to plot
sim.plot(["Concentration", "Surface concentration", "Flux", "Boundary flux"])
2022-07-11 13:21:23.957 - [WARNING] processed_variable.get_spatial_scale(521): No length scale set for negative particle. Using default of 1 [m].
2022-07-11 13:21:23.975 - [WARNING] processed_variable.get_spatial_scale(521): No length scale set for negative particle. Using default of 1 [m].
[14]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f41afa9a9d0>

Here we have seen how to create a basic model from scratch in PyBaMM. In the next tutorial we will see how to split this model up into separate submodels.

References#

The relevant papers for this notebook are:

[15]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Tutorial 11 - Creating a submodel#

In Tutorial 10 we showed how to create a simple model from scratch in PyBaMM. In this tutorial we will solve the same problem, but using separate submodels for the linear diffusion problem and the model for the surface flux. In this simple example the surface flux is just some known function of the concentration, so we could just explicitly define it in the model for diffusion. However, we write it as a separate model to show how submodels interact.

We solved the problem of linear diffusion on a unit sphere with a flux at the boundary that depends on the concentration

\[\frac{\partial c}{\partial t} = \nabla \cdot (\nabla c),\]

with the following boundary and initial conditions:

\[\left.\frac{\partial c}{\partial r}\right\vert_{r=0} = 0, \quad \left.\frac{\partial c}{\partial r}\right\vert_{r=1} = -j, \quad \left.c\right\vert_{t=0} = c_0,\]

where

\[j = \left.j_0(1-c)^{1/2}c^{1/2}\right\vert_{r=1}\]

Here \(c_0\) and \(j_0\) are parameters we can control. Again we will assume that everything is non-dimensional and focus on how to set up and solve the model rather than any specific physical interpretation.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

Setting up the model#

Again we start with an empty BaseModel class

[2]:
model = pybamm.BaseModel()

Next we set up the submodel for diffusion in the particle using a pybamm.BaseSubModel. Each submodel has methods that define the variables, equations, initial and boundary conditions corresponding to the physics the model describes.

First get_fundamental_variables defines any variables that can be defined independently of any other submodels that may be included in our model. Here we can define the concentration, surface concentration and flux, and add them to the dictionary of variables.

Next we can use get_coupled_variables to define any variables that do depend on variables defined in another submodel. In this simple example we don’t have any variables to define here. However, if we had included a temperature dependent diffusivity, for example, then we would have needed to define the flux in get_coupled_variables since it would now depend on the temperature which would be defined in a separate submodel.

Once we have defined all the variables we need we can write down our equations. Any equations that include time derivatives will turn into ordinary differential equations after discretisation. We set the right hand sides of such equations in the set_rhs method. In this example we add the right hand side of the diffusion equation to the rhs dictionary. Equations that don’t contain time derivatives give algebraic constraints in our model. These equations are set in the set_algebraic method. In this example we don’t have any algebraic equations, so we can skip this method.

Finally we set the boundary and initial conditions using the methods set_boundary_conditions and set_initial_conditions, respectively.

[3]:
class Particle(pybamm.BaseSubModel):
    def __init__(self, param, domain, options=None):
        super().__init__(param, domain, options=options)

    def get_fundamental_variables(self):
        # create concentration variable
        c = pybamm.Variable("Concentration", domain="negative particle")

        # define concentration at the surface of the sphere
        c_surf = pybamm.surf(c)

        # define flux
        N = -pybamm.grad(c)

        # create dictionary of model variables
        variables = {
            "Concentration": c,
            "Surface concentration": c_surf,
            "Flux": N,
        }

        return variables

    def get_coupled_variables(self, variables):
        return variables

    def set_rhs(self, variables):
        # extract the variables we need
        c = variables["Concentration"]
        N = variables["Flux"]

        # define the rhs of the PDE
        dcdt = -pybamm.div(N)

        # add it to the submodel dictionary
        self.rhs = {c: dcdt}

    def set_algebraic(self, variables):
        pass

    def set_boundary_conditions(self, variables):
        # extract the variables we need
        c = variables["Concentration"]
        j = variables["Boundary flux"]

        # add the boundary conditions to the submodel dictionary
        self.boundary_conditions = {
            c: {"left": (0, "Neumann"), "right": (-j, "Neumann")}
        }

    def set_initial_conditions(self, variables):
        # extract the variable we need
        c = variables["Concentration"]

        # define the initial concentration parameter
        c0 = pybamm.Parameter("Initial concentration")

        # add the initial conditions to the submodel dictionary
        self.initial_conditions = {c: c0}
[4]:
class BoundaryFlux(pybamm.BaseSubModel):
    def __init__(self, param, domain, options=None):
        super().__init__(param, domain, options=options)

    def get_coupled_variables(self, variables):
        # extract the variable we need
        c_surf = variables["Surface concentration"]

        # define the flux parameter
        j0 = pybamm.Parameter("Flux parameter")
        j = j0 * (1 - c_surf) ** (1 / 2) * c_surf ** (1 / 2)  # prescribed boundary flux

        # update dictionary of model variables
        variables.update({"Boundary flux": j})

        return variables

We can now set the submodels in a model by assigning a dictionary to model.submodels. The dictionary key is the name we want to give to the submodel and the value is an instance of the submodel class we want to use.

When we instantiate a submodel we are required to pass in param, a class of parameter symbols we are going to call, and domain, the domain on which the submodel lives. In this example we will simply set param to None and hard-code the definition of our parameters into the submodel. When writing lots of submodels it is more efficient to define all the parameters in a shared class, and pass this to each submodel. For the domain we will choose “Negative”.

[5]:
model.submodels = {
    "Particle": Particle(None, "Negative"),
    "Boundary flux": BoundaryFlux(None, "Negative"),
}

At this stage we have just told the model which submodels it is constructed from, but the variables and equations have not yet been created. For example if we look at the rhs dictionary it is empty.

[6]:
model.rhs
[6]:
{}

To populate the model variables, equations, boundary and initial conditions we need to “build” the model. To do this we call build_model

[7]:
model.build_model()

This loops through all of the submodels, first creating the “fundamental variables”, followed by the “coupled variables” and finally the equations (rhs and algebraic) and the boundary and initial conditions. Now we see that model.rhs contains our diffusion equation.

[8]:
model.rhs
[8]:
{Variable(0x48a91fa41dea72d3, Concentration, children=[], domains={'primary': ['negative particle']}): Divergence(0x4befa5e7cf20d0c5, div, children=['grad(Concentration)'], domains={'primary': ['negative particle']})}

Using the model#

We can now use our model as in the previous tutorial.

We first set up our geometry and mesh

[9]:
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)
geometry = {"negative particle": {r: {"min": 0, "max": 1}}}
spatial_methods = {"negative particle": pybamm.FiniteVolume()}
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

and then set up our simulation, remembering to set values for our parameters

[10]:
parameter_values = pybamm.ParameterValues(
    {
        "Initial concentration": 0.9,
        "Flux parameter": 0.8,
    }
)

solver = pybamm.ScipySolver()

sim = pybamm.Simulation(
    model,
    geometry=geometry,
    parameter_values=parameter_values,
    submesh_types=submesh_types,
    var_pts=var_pts,
    spatial_methods=spatial_methods,
    solver=solver,
)

Finally we can solve the model

[11]:
sim.solve([0, 1])
[11]:
<pybamm.solvers.solution.Solution at 0x7f9dc1b2e490>

and plot the results

[12]:
# pass in a list of the variables we want to plot
sim.plot(["Concentration", "Surface concentration", "Flux", "Boundary flux"])
2022-07-11 13:20:04.194 - [WARNING] processed_variable.get_spatial_scale(521): No length scale set for negative particle. Using default of 1 [m].
2022-07-11 13:20:04.209 - [WARNING] processed_variable.get_spatial_scale(521): No length scale set for negative particle. Using default of 1 [m].
[12]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f9dc1b2ec10>

In this notebook we saw how to split a model up into submodels. Although this was a simple example it let us understand how to construct submodels and see how they interact via coupled variables.

References#

The relevant papers for this notebook are:

[13]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Creating a simple ODE model#

In this series of notebooks, we will run through the steps involved in creating a new model within pybamm. Before using pybamm we recommend following the Getting Started guides.

In this notebook we create and solve the following simple ODE model:

\[\frac{\textrm{d}x}{\textrm{d} t} = 4x - 2y, \quad x(0) = 1,\]
\[\frac{\textrm{d}y}{\textrm{d} t} = 3x - y, \quad y(0) = 2.\]

We begin by importing the PyBaMM library into this notebook, along with NumPy and Matplotlib, which we use for plotting:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

Setting up the model#

We first initialise the model using the BaseModel class. This sets up the required structure for our model.

[2]:
model = pybamm.BaseModel()

Next, we define the variables in the model using the Variable class. In more complicated models we can give the variables more informative string names, but here we simply name the variables “x” and “y”

[3]:
x = pybamm.Variable("x")
y = pybamm.Variable("y")

We can now use the symbols we have created for our variables to write out our governing equations. Note that the governing equations must be provied in the explicit form d/dt = rhs since pybamm only stores the right hand side (rhs) and assumes that the left hand side is the time derivative.

[4]:
dxdt = 4 * x - 2 * y
dydt = 3 * x - y

The governing equations must then be added to the dictionary model.rhs. The dictionary stores key and item pairs, where the key is the variable which is governed by the equation stored in the corresponding item. Note that the keys are the symbols that represent the variables and are not the variable names (e.g. the key is x, not the string “x”).

[5]:
model.rhs = {x: dxdt, y: dydt}

The initial conditions are also stored in a dictionary, model.initial_conditions, which again uses the variable as the key

[6]:
model.initial_conditions = {x: pybamm.Scalar(1), y: pybamm.Scalar(2)}

Finally, we can add any variables of interest to our model. Note that these can be things other than the variables that are solved for. For example, we may want to store the variable defined by \(z=x+4y\) as a model output. Variables are added to the model using the model.variables dictionary as follows:

[7]:
model.variables = {"x": x, "y": y, "z": x + 4 * y}

Note that the keys of this dictionary are strings (i.e. the names of the variables). The string names can be different from the variable symbol, and should in general be something informative. The model is now completely defined and is ready to be discretised and solved!

Using the model#

We first discretise the model using the pybamm.Discretisation class. Calling the method process_model turns the model variables into a pybamm.StateVector object that can be passed to a solver. Since the model is a system of ODEs we do not need to provide a mesh or worry about any spatial dependence, so we can use the default discretisation. Details on how to provide a mesh will be covered in the following notebook.

[8]:
disc = pybamm.Discretisation()  # use the default discretisation
disc.process_model(model);

Now that the model has been discretised it is ready to be solved. Here we choose the ODE solver pybamm.ScipySolver and solve, returning the solution at 20 time points in the interval \(t \in [0, 1]\)

[9]:
solver = pybamm.ScipySolver()
t = np.linspace(0, 1, 20)
solution = solver.solve(model, t)

After solving, we can extract the variables from the solution. These are automatically post-processed so that the solutions can be called at any time \(t\) using interpolation. The times at which the model was solved at are stored in solution.t and the statevectors at those times are stored in solution.y

[10]:
t_sol, y_sol = solution.t, solution.y  # get solution times and states
x = solution["x"]  # extract and process x from the solution
y = solution["y"]  # extract and process y from the solution

We then plot the numerical solution against the exact solution.

[11]:
t_fine = np.linspace(0, t[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, 2 * np.exp(t_fine) - np.exp(2 * t_fine), t_sol, x(t_sol), "o")
ax1.set_xlabel("t")
ax1.legend(["2*exp(t) - exp(2*t)", "x"], loc="best")

ax2.plot(t_fine, 3 * np.exp(t_fine) - np.exp(2 * t_fine), t_sol, y(t_sol), "o")
ax2.set_xlabel("t")
ax2.legend(["3*exp(t) - exp(2*t)", "y"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_creating_models_1-an-ode-model_24_0.png

In the next notebook we show how to create, discretise and solve a PDE model in pybamm.

References#

The relevant papers for this notebook are:

[12]:
pybamm.print_citations()
[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.
[2] 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.
[3] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Creating a simple PDE model#

In the previous notebook we show how to create, discretise and solve an ODE model in pybamm. In this notebook we show how to create and solve a PDE problem, which will require meshing of the spatial domain.

As an example, we consider the problem of linear diffusion on a unit sphere,

\[\frac{\partial c}{\partial t} = \nabla \cdot (\nabla c),\]

with the following boundary and initial conditions:

\[\left.\frac{\partial c}{\partial r}\right\vert_{r=0} = 0, \quad \left.\frac{\partial c}{\partial r}\right\vert_{r=1} = 2, \quad \left.c\right\vert_{t=0} = 1.\]

As before, we begin by importing the PyBaMM library into this notebook, along with any other packages we require:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

Setting up the model#

As in the previous example, we start with a pybamm.BaseModel object and define our model variables. Since we are now solving a PDE we need to tell pybamm the domain each variable belongs to so that it can be discretised in space in the correct way. This is done by passing the keyword argument domain, and in this example we choose the domain “negative particle”.

[2]:
model = pybamm.BaseModel()

c = pybamm.Variable("Concentration", domain="negative particle")

Note that we have given our variable the (useful) name “Concentration”, but the symbol representing this variable is simply c.

We then state out governing equations. Sometime it is useful to define intermediate quantities in order to express the governing equations more easily. In this example we define the flux, then define the rhs to be minus the divergence of the flux. The equation is then added to the dictionary model.rhs

[3]:
N = -pybamm.grad(c)  # define the flux
dcdt = -pybamm.div(N)  # define the rhs equation

model.rhs = {c: dcdt}  # add the equation to rhs dictionary

Unlike ODE models, PDE models require both initial and boundary conditions. Similar to initial conditions, boundary conditions can be added using the dictionary model.boundary_conditions. Boundary conditions for each variable are provided as a dictionary of the form {side: (value, type), where, in 1D, side can be “left” or “right”, value is the value of the boundary conditions, and type is the type of boundary condition (at present, this can be “Dirichlet” or “Neumann”).

[4]:
# initial conditions
model.initial_conditions = {c: pybamm.Scalar(1)}

# boundary conditions
lbc = pybamm.Scalar(0)
rbc = pybamm.Scalar(2)
model.boundary_conditions = {c: {"left": (lbc, "Neumann"), "right": (rbc, "Neumann")}}

Note that in our example the boundary conditions take constant values, but the value can be any valid pybamm expression.

Finally, we add any variables of interest to the dictionary model.variables

[5]:
model.variables = {"Concentration": c, "Flux": N}

Using the model#

Now the model is now completely defined all that remains is to discretise and solve. Since this model is a PDE we need to define the geometry on which it will be solved, and choose how to mesh the geometry and discretise in space.

Defining a geometry and mesh#

We can define spatial variables in a similar way to how we defined model variables, providing a domain and a coordinate system. The geometry on which we wish to solve the model is defined using a nested dictionary. The first key is the domain name (here “negative particle”) and the entry is a dictionary giving the limits of the domain.

[6]:
# define geometry
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)
geometry = {
    "negative particle": {r: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}
}

We then create a uniform one-dimensional mesh with 20 points.

[7]:
# mesh and discretise
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

Example of meshes that do require parameters include the pybamm.Exponential1DSubMesh which clusters points close to one or both boundaries using an exponential rule. It takes a parameter which sets how closely the points are clustered together, and also lets the users select the side on which more points should be clustered. For example, to create a mesh with more nodes clustered to the right (i.e. the surface in the particle problem), using a stretch factor of 2, we pass an instance of the exponential submesh class and a dictionary of parameters into the MeshGenerator class as follows: pybamm.MeshGenerator(pybamm.Exponential1DSubMesh, submesh_params={"side": "right", "stretch": 2})

After defining a mesh we choose a spatial method. Here we choose the Finite Volume Method. We then set up a discretisation by passing the mesh and spatial methods to the class pybamm.Discretisation. The model is then processed, turning the variables into (slices of) a statevector, spatial variables into vector and spatial operators into matrix-vector multiplications.

[8]:
spatial_methods = {"negative particle": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model);

Now that the model has been discretised we are ready to solve.

Solving the model#

As before, we choose a solver and times at which we want the solution returned. We then solve, extract the variables we are interested in, and plot the result.

[9]:
# solve
solver = pybamm.ScipySolver()
t = np.linspace(0, 1, 100)
solution = solver.solve(model, t)

# post-process, so that the solution can be called at any time t or space r
# (using interpolation)
c = solution["Concentration"]

# plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

ax1.plot(solution.t, c(solution.t, r=1))
ax1.set_xlabel("t")
ax1.set_ylabel("Surface concentration")
r = np.linspace(0, 1, 100)
ax2.plot(r, c(t=0.5, r=r))
ax2.set_xlabel("r")
ax2.set_ylabel("Concentration at t=0.5")
plt.tight_layout()
plt.show()
2021-11-19 15:31:50,774 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for negative particle. Using default of 1 [m].
_images/source_examples_notebooks_creating_models_2-a-pde-model_25_1.png

In the next notebook we build on the example here to to solve the problem of diffusion in the negative electrode particle within the single particle model. In doing so we will also cover how to include parameters in a model.

References#

The relevant papers for this notebook are:

[10]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

A step towards the Single Particle Model#

In the previous notebook we saw how to solve a PDE model in pybamm. Now it is time to solve a real-life battery problem! We consider the problem of spherical diffusion in the negative electrode particle within the single particle model. That is,

\[\frac{\partial c}{\partial t} = \nabla \cdot (D \nabla c),\]

with the following boundary and initial conditions:

\[\left.\frac{\partial c}{\partial r}\right\vert_{r=0} = 0, \quad \left.\frac{\partial c}{\partial r}\right\vert_{r=R} = -\frac{j}{FD}, \quad \left.c\right\vert_{t=0} = c_0,\]

where \(c\) is the concentration, \(r\) the radial coordinate, \(t\) time, \(R\) the particle radius, \(D\) the diffusion coefficient, \(j\) the interfacial current density, \(F\) Faraday’s constant, and \(c_0\) the initial concentration.

In this example we use the following parameters:

Symbol

Units

Value

\(R\)

m

\(10 \times 10^{-6}\)

\(D\)

m\({^2}\) s\(^{-1}\)

\(3.9 \times 10^{-14}\)

\(j\)

A m\(^{-2}\)

\(1.4\)

\(F\)

C mol\(^{-1}\)

\(96485\)

\(c_0\)

mol m\(^{-3}\)

\(2.5 \times 10^{4}\)

Setting up the model#

As before, we begin by importing the PyBaMM library into this notebook, along with any other packages we require, and start with an empty pybamm.BaseModel

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt

model = pybamm.BaseModel()
Note: you may need to restart the kernel to use updated packages.

We then define all of the model variables and parameters. Parameters are created using the pybamm.Parameter class and are given informative names (with units). Later, we will provide parameter values and the Parameter objects will be turned into numerical values. For more information please see the parameter values notebook.

[2]:
R = pybamm.Parameter("Particle radius [m]")
D = pybamm.Parameter("Diffusion coefficient [m2.s-1]")
j = pybamm.Parameter("Interfacial current density [A.m-2]")
F = pybamm.Parameter("Faraday constant [C.mol-1]")
c0 = pybamm.Parameter("Initial concentration [mol.m-3]")

c = pybamm.Variable("Concentration [mol.m-3]", domain="negative particle")

Now we define our model equations, boundary and initial conditions, as in the previous example.

[3]:
# governing equations
N = -D * pybamm.grad(c)  # flux
dcdt = -pybamm.div(N)
model.rhs = {c: dcdt}

# boundary conditions
lbc = pybamm.Scalar(0)
rbc = -j / F / D
model.boundary_conditions = {c: {"left": (lbc, "Neumann"), "right": (rbc, "Neumann")}}

# initial conditions
model.initial_conditions = {c: c0}

Finally, we add any variables of interest to the dictionary model.variables

[4]:
model.variables = {
    "Concentration [mol.m-3]": c,
    "Surface concentration [mol.m-3]": pybamm.surf(c),
    "Flux [mol.m-2.s-1]": N,
}

Using the model#

In order to discretise and solve the model we need to provide values for all of the parameters. This is done via the pybamm.ParameterValues class, which accepts a dictionary of parameter names and values

[5]:
param = pybamm.ParameterValues(
    {
        "Particle radius [m]": 10e-6,
        "Diffusion coefficient [m2.s-1]": 3.9e-14,
        "Interfacial current density [A.m-2]": 1.4,
        "Faraday constant [C.mol-1]": 96485,
        "Initial concentration [mol.m-3]": 2.5e4,
    }
)

Here all of the parameters are simply scalars, but they can also be functions or read in from data (see parameter values notebook).

As in the previous example, we define the particle geometry. Note that in this example the definition of the geometry contains a parameter, the particle radius \(R\)

[6]:
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)
geometry = {"negative particle": {r: {"min": pybamm.Scalar(0), "max": R}}}

Both the model and geometry can now be processed by the parameter class. This replaces the parameters with the values

[7]:
param.process_model(model)
param.process_geometry(geometry)

We can now set up our mesh, choose a spatial method, and discretise our model

[8]:
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

spatial_methods = {"negative particle": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model);

The model is now discretised and ready to be solved.

Solving the model#

As is the previous example, we choose a solver and times at which we want the solution returned.

[9]:
# solve
solver = pybamm.ScipySolver()
t = np.linspace(0, 3600, 600)
solution = solver.solve(model, t)

# post-process, so that the solution can be called at any time t or space r
# (using interpolation)
c = solution["Concentration [mol.m-3]"]
c_surf = solution["Surface concentration [mol.m-3]"]

# plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

ax1.plot(solution.t, c_surf(solution.t))
ax1.set_xlabel("Time [s]")
ax1.set_ylabel("Surface concentration [mol.m-3]")

r = mesh["negative particle"].nodes  # radial position
time = 1000  # time in seconds
ax2.plot(r * 1e6, c(t=time, r=r), label=f"t={time}[s]")
ax2.set_xlabel("Particle radius [microns]")
ax2.set_ylabel("Concentration [mol.m-3]")
ax2.legend()

plt.tight_layout()
plt.show()
2021-11-19 15:29:22,931 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for negative particle. Using default of 1 [m].
_images/source_examples_notebooks_creating_models_3-negative-particle-problem_23_1.png

In the next notebook we consider the limit of fast diffusion in the particle. This leads to a reduced-order model for the particle behaviour, which we compare with the full (Fickian diffusion) model.

References#

The relevant papers for this notebook are:

[10]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Comparing full and reduced-order models#

In the previous notebook we saw how to solve the problem of diffusion on a sphere, motivated by the problem in the negative particle in battery modelling. In this notebook we consider a reduced-order ODE model for the particle behaviour, suitable in the limit of fast diffusion. We also show how to compare the results of the full and reduced-order models.

In the limit of fast diffusion in the particles the concentration is uniform in \(r\). This result in the following ODE model for the (uniform) concentration in the particle

\[\frac{\textrm{d} c}{\textrm{d} t} = -3\frac{j}{RF}\]

with the initial condition:

\[\left.c\right\vert_{t=0} = c_0,\]

where \(c\)$ is the concentration, \(r\) the radial coordinate, \(t\) time, \(R\) the particle radius, \(D\) the diffusion coefficient, \(j\) the interfacial current density, \(F\) Faraday’s constant, and \(c_0\) the initial concentration.

As in the previous example we use the following parameters:

Symbol

Units

Value

\(R\)

m

\(10 \times 10^{-6}\)

\(D\)

m\({^2}\) s\(^{-1}\)

\(3.9 \times 10^{-14}\)

\(j\)

A m\(^{-2}\)

\(1.4\)

\(F\)

C mol\(^{-1}\)

\(96485\)

\(c_0\)

mol m\(^{-3}\)

\(2.5 \times 10^{4}\)

Setting up the models#

As in the single particle diffusion example, we begin by importing the pybamm library into this notebook, along with any other packages we require. In this notebook we want to compare the results of the full and reduced-order models, so we create two empty pybamm.BaseModel objects. We can pass in a name when we create the model, for easy reference.

[11]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt

full_model = pybamm.BaseModel(name="full model")
reduced_model = pybamm.BaseModel(name="reduced model")
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

It can be useful to add the models to a list so that we can perform the same operations on each model easily

[12]:
models = [full_model, reduced_model]

We then define our parameters, as seen previously,

[13]:
R = pybamm.Parameter("Particle radius [m]")
D = pybamm.Parameter("Diffusion coefficient [m2.s-1]")
j = pybamm.Parameter("Interfacial current density [A.m-2]")
F = pybamm.Parameter("Faraday constant [C.mol-1]")
c0 = pybamm.Parameter("Initial concentration [mol.m-3]")

The reduced order model solves and ODE for the (uniform) concentration in the particle. In the parameter regime where this is valid, we expect that the solution of the ODE model should agree with the average concentration in the PDE mode. In anticipation of this, we create two variables: the concentration (which we will use in the PDE model), and the average concentration (which we will use in the ODE model). This will make it straightforward to compare the results in a consistent way. Note that the average concentration doesn’t have a domain since it is a scalar quantity.

[14]:
c = pybamm.Variable("Concentration [mol.m-3]", domain="negative particle")
c_av = pybamm.Variable("Average concentration [mol.m-3]")

Now we define our model equations, initial and boundary conditions (where appropriate)

[15]:
# governing equations for full model
N = -D * pybamm.grad(c)  # flux
dcdt = -pybamm.div(N)
full_model.rhs = {c: dcdt}

# governing equations for reduced model
dc_avdt = -3 * j / R / F
reduced_model.rhs = {c_av: dc_avdt}

# initial conditions (these are the same for both models)
full_model.initial_conditions = {c: c0}
reduced_model.initial_conditions = {c_av: c0}

# boundary conditions (only required for full model)
lbc = pybamm.Scalar(0)
rbc = -j / F / D
full_model.boundary_conditions = {
    c: {"left": (lbc, "Neumann"), "right": (rbc, "Neumann")}
}

We can now populate the variables dictionary of both models with any variables of interest. We can compute the average concentration in the full model using the operator pybamm.r_average. We may also wish to compare the concentration profile predicted by the full model with the uniform concentration profile predicted by the reduced model. We can use the operator pybamm.PrimaryBroadcast to broadcast the scalar valued uniform concentration across the particle domain so that it can be visualised as a function of \(r\).

Note: the “Primary” refers to the fact the we are broadcasting in only one dimension. For some models, such as the DFN, variables may depend on a “pseudo-dimension” (e.g. the position in \(x\) across the cell), but spatial operators only act in the “primary dimension” (e.g. the position in \(r\) within the particle). If you are unfamiliar with battery models, do not worry, the details of this are not important for this example. For more information see the broadcasts notebook.

[16]:
# full model
full_model.variables = {
    "Concentration [mol.m-3]": c,
    "Surface concentration [mol.m-3]": pybamm.surf(c),
    "Average concentration [mol.m-3]": pybamm.r_average(c),
}

# reduced model
reduced_model.variables = {
    "Concentration [mol.m-3]": pybamm.PrimaryBroadcast(c_av, "negative particle"),
    "Surface concentration [mol.m-3]": c_av,  # in this model the surface concentration is just equal to the scalar average concentration
    "Average concentration [mol.m-3]": c_av,
}

Using the model#

As before, we provide our parameter values

[17]:
param = pybamm.ParameterValues(
    {
        "Particle radius [m]": 10e-6,
        "Diffusion coefficient [m2.s-1]": 3.9e-14,
        "Interfacial current density [A.m-2]": 1.4,
        "Faraday constant [C.mol-1]": 96485,
        "Initial concentration [mol.m-3]": 2.5e4,
    }
)

We then define and process our geometry, and process both of the models

[18]:
# geometry
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)
geometry = {"negative particle": {r: {"min": pybamm.Scalar(0), "max": R}}}
param.process_geometry(geometry)

# models
for model in models:
    param.process_model(model)

We can now set up our mesh, choose a spatial method, and discretise our models. Note that, even though the reduced-order model is an ODE model, we discretise using the mesh for the particle so that our PrimaryBroadcast operator is discretised in the correct way.

[19]:
# mesh
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

# discretisation
spatial_methods = {"negative particle": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)

# process models
for model in models:
    disc.process_model(model)

Both models are now discretised and ready to be solved.

Solving the model#

As before, we choose a solver and times at which we want the solution returned. We then solve both models, post-process the results, and create a slider plot to compare the results.

[20]:
# loop over models to solve
t = np.linspace(0, 3600, 600)
solutions = [None] * len(models)  # create list to hold solutions
for i, model in enumerate(models):
    solver = pybamm.ScipySolver()
    solutions[i] = solver.solve(model, t)

# post-process the solution of the full model
c_full = solutions[0]["Concentration [mol.m-3]"]
c_av_full = solutions[0]["Average concentration [mol.m-3]"]


# post-process the solution of the reduced model
c_reduced = solutions[1]["Concentration [mol.m-3]"]
c_av_reduced = solutions[1]["Average concentration [mol.m-3]"]

# plot
r = mesh["negative particle"].nodes  # radial position


def plot(t):
    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

    # Plot concetration as a function of r
    ax1.plot(r * 1e6, c_full(t=t, r=r), label="Full Model")
    ax1.plot(r * 1e6, c_reduced(t=t, r=r), label="Reduced Model")
    ax1.set_xlabel("Particle radius [microns]")
    ax1.set_ylabel("Concentration [mol.m-3]")
    ax1.legend()

    # Plot average concentration over time
    t_hour = np.linspace(0, 3600, 600)  # plot over full hour
    c_min = c_av_reduced(t=3600) * 0.98  # minimum axes limit
    c_max = param["Initial concentration [mol.m-3]"] * 1.02  # maximum axes limit

    ax2.plot(t_hour, c_av_full(t=t_hour), label="Full Model")
    ax2.plot(t_hour, c_av_reduced(t=t_hour), label="Reduced Model")
    ax2.plot([t, t], [c_min, c_max], "k--")  # plot line to track time
    ax2.set_xlabel("Time [s]")
    ax2.set_ylabel("Average concentration [mol.m-3]")
    ax2.legend()

    plt.tight_layout()
    plt.show()


import ipywidgets as widgets

widgets.interact(plot, t=widgets.FloatSlider(min=0, max=3600, step=1, value=0));
2022-12-12 12:41:59.589 - [WARNING] processed_variable.get_spatial_scale(518): No length scale set for negative particle. Using default of 1 [m].
2022-12-12 12:41:59.609 - [WARNING] processed_variable.get_spatial_scale(518): No length scale set for negative particle. Using default of 1 [m].

From the results we observe that the reduced-order model does a good job of predicting the average concentration, but, since it is only an ODE model, cannot predicted the spatial distribution.

In the next notebook we will show how to set up and solve a model which contains multiple domains.

References#

The relevant papers for this notebook are:

[21]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

A half cell model#

In the previous notebook we saw how to compare full and reduced-order models. Both of these models were posed on a single domain: the negative electrode particle. Here we will see how to create a model which contains multiple domains.

We consider a problem posed on a half-cell geometry, which consists of a separator (\(-L_s<x<0\)) and a positive electrode (\(0<x<L_p\)). These two regions are considered “macroscale” domains. At each point in the positive electrode we treat a “microscale” problem to model diffusion of lithium within the positive particles (\(0<r<R_p\)). We will see how to create variables in each of the different domains so that the governing equations are properly coupled.

In the interest of simplicity we assume that the current in both the the solid and electrolyte is given by Ohm’s law, and ignore any concentration gradients in the electrolyte. The governing equations for charge conservation at the macroscale are then

\[\begin{split}i_e = -\kappa \nabla \phi_e, \quad \nabla i_e = a j, \quad -L_s<x<0, \\ i = -\sigma \nabla \phi, \quad \nabla \cdot i = -a j, \quad 0<x<L_p,\end{split}\]

where \(i\) and \(i_e\) are the current densities in the solid and electrolyte, respectively, \(\phi\) and \(\phi_e\) are the electric potentials in the solid and electrolyte, respectively, \(\sigma\) is the solid conductivity, \(\kappa\) is the ionic conductivity, \(a\) is the electrode surface area per unit volume and \(j\) the interfacial current density. The charge conservation equations are subject to the boundary conditions

\[\left.\phi_e\right\vert_{x=-L_s} = 0, \quad \left.i_e\right\vert_{x=L_p} = 0, \quad \left.i\right\vert_{x=0} = 0, \quad \left.i\right\vert_{x=L_p} = \frac{I_{\text{app}}}{A},\]

where \(I_{\text{app}}\) is the applied current and \(A\) is the electrode cross-sectional area. We then have an equation posed at each macroscopic point in the electrode (\(0<x<L_p\)) describing transport of lithium within the active material particles. That is,

\[\frac{\partial c}{\partial t} = \nabla \cdot (D \nabla c), \quad 0<r<R_p,\]

with the following boundary and initial conditions:

\[\left.\frac{\partial c}{\partial r}\right\vert_{r=0} = 0, \quad \left.\frac{\partial c}{\partial r}\right\vert_{r=R} = -\frac{j}{FD}, \quad \left.c\right\vert_{t=0} = c_0,\]

where \(c\) is the concentration, \(r\) the radial coordinate, \(t\) time, \(R\) the particle radius, \(D\) the diffusion coefficient, \(F\) Faraday’s constant, and \(c_0\) the initial concentration. For the interfacial current density we assume Butler-Volmer kinetics

\[\begin{split}j = \begin{cases} 0 &-L_s<x<0 \\ 2 j_0(c)\sinh\left(\frac{F}{2RT}(\phi-\phi_e-U(c))\right) &0<x<L_p \end{cases},\end{split}\]

where \(j_0\) is the exchange current density, and \(U\) is the open-circuit potential.

Setting up the model#

As before, we begin by importing the PyBaMM library into this notebook, along with any other packages we require, and start with an empty pybamm.BaseModel

[19]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np

model = pybamm.BaseModel()
Note: you may need to restart the kernel to use updated packages.

Let’s first define our model variables. We can define the electric potential in the positive electrode in the same way as we defined the concentration variables in the previous notebooks

[20]:
phi = pybamm.Variable("Positive electrode potential [V]", domain="positive electrode")

The potential in the electrolyte spans two domains. To set this up we first define the electric potential in each of the domains

[21]:
phi_e_s = pybamm.Variable("Separator electrolyte potential [V]", domain="separator")
phi_e_p = pybamm.Variable(
    "Positive electrolyte potential [V]", domain="positive electrode"
)

and then concatenate these two variables together to define a single variable that spans the separator and positive electrode

[22]:
phi_e = pybamm.concatenation(phi_e_s, phi_e_p)

Note that in our formulation the separator will be on the left and the positive electrode will be on the right, so this is the order in which we concatenated the variables for the electrolyte potential in each domain.

The concentration in the electrode particles can vary both in \(r\) and \(x\), but diffusion only occurs in the \(r\) direction. In order to handle this situation we introduce the concept of “auxiliary domains”. These are domains in which quantities can vary, but spatial operators do not act. To set up our concentration variable we create a Variable which has domain “positive particle” and secondary domain “positive electrode”

[23]:
c = pybamm.Variable(
    "Positive particle concentration [mol.m-3]",
    domain="positive particle",
    auxiliary_domains={
        "secondary": "positive electrode",
    },
)

Now spatial operators acting on c only act in the \(r\) direction (corresponding to the primary domain “positive particle”), but c can still depend on \(x\) (corresponding to the secondary domain “positive electrode”). For more details on the different domains (primary, secondary, etc.) see the broadcasts notebook.

Next we will define our parameters. As seen before, scalar parameters can be defined using the Parameter object

[24]:
F = pybamm.Parameter("Faraday constant [C.mol-1]")
R = pybamm.Parameter("Molar gas constant [J.mol-1.K-1]")
T = pybamm.Parameter("Temperature [K]")

a = pybamm.Parameter("Surface area per unit volume [m-1]")
R_p = pybamm.Parameter("Positive particle radius [m]")
L_s = pybamm.Parameter("Separator thickness [m]")
L_p = pybamm.Parameter("Positive electrode thickness [m]")
A = pybamm.Parameter("Electrode cross-sectional area [m2]")

sigma = pybamm.Parameter("Positive electrode conductivity [S.m-1]")
kappa = pybamm.Parameter("Electrolyte conductivity [S.m-1]")
D = pybamm.Parameter("Diffusion coefficient [m2.s-1]")

I_app = pybamm.Parameter("Applied current [A]")
c0 = pybamm.Parameter("Initial concentration [mol.m-3]")

Parameters can also have some functional dependence. We can define such parameters using the FunctionParameter object. We also need to specify the inputs (i.e. the variables on which the function depends). In our example both the exchange current density and the open-circuit potential will depend on the particle surface concentration.

[25]:
c_surf = pybamm.surf(c)  # get the surface concentration
inputs = {"Positive particle surface concentration [mol.m-3]": c_surf}
j0 = pybamm.FunctionParameter(
    "Positive electrode exchange-current density [A.m-2]", inputs
)
U = pybamm.FunctionParameter("Positive electrode OCP [V]", inputs)

We also need to define the interfacial current, which is zero in the separator and given by the Butler-Volmer equation in the positive electrode

[26]:
j_s = pybamm.PrimaryBroadcast(0, "separator")
j_p = 2 * j0 * pybamm.sinh((F / 2 / R / T) * (phi - phi_e_p - U))
j = pybamm.concatenation(j_s, j_p)

Now we can write our governing equations, boundary and initial conditions. Note that we provide initial conditions for the algebraic equations. These are not really initial conditions, but are used as an initial guess for the solver.

[27]:
# charge conservation equations
i = -sigma * pybamm.grad(phi)
i_e = -kappa * pybamm.grad(phi_e)
model.algebraic = {
    phi: pybamm.div(i) + a * j_p,
    phi_e: pybamm.div(i_e) - a * j,
}
# particle equations (mass conservation)
N = -D * pybamm.grad(c)  # flux
dcdt = -pybamm.div(N)
model.rhs = {c: dcdt}

# boundary conditions
model.boundary_conditions = {
    phi: {
        "left": (pybamm.Scalar(0), "Neumann"),
        "right": (-I_app / A / sigma, "Neumann"),
    },
    phi_e: {
        "left": (pybamm.Scalar(0), "Dirichlet"),
        "right": (pybamm.Scalar(0), "Neumann"),
    },
    c: {"left": (pybamm.Scalar(0), "Neumann"), "right": (-j_p / F / D, "Neumann")},
}

# initial conditions
inputs = {"Initial concentration [mol.m-3]": c0}
U_init = pybamm.FunctionParameter("Positive electrode OCP [V]", inputs)
model.initial_conditions = {phi: U_init, phi_e: 0, c: c0}

Finally we can add any variables of interest to the model

[28]:
model.variables = {
    "Positive electrode potential [V]": phi,
    "Electrolyte potential [V]": phi_e,
    "Positive particle concentration [mol.m-3]": c,
    "Positive particle surface concentration [mol.m-3]": c_surf,
    "Average positive particle surface concentration [mol.m-3]": pybamm.x_average(
        c_surf
    ),
    "Positive electrode interfacial current density [A.m-2]": j_p,
    "Positive electrode OCP [V]": pybamm.boundary_value(U, "right"),
    "Voltage [V]": pybamm.boundary_value(phi, "right"),
}

Using the model#

As we have seen before, we can provide values for our parameters using the ParameterValues class. As well as providing scalar values, we also need to provide the functional form using by our FunctionParameter objects. Here we will define these functions locally, but we could provide the path to a function defined elsewhere or provide data (see the parameterization notebook).

[29]:
from pybamm import tanh

# both functions will depend on the maximum concentration
c_max = pybamm.Parameter("Maximum concentration in positive electrode [mol.m-3]")


def exchange_current_density(c_surf):
    k = 6 * 10 ** (-7)  # reaction rate [(A/m2)(m3/mol)**1.5]
    c_e = 1000  # (constant) electrolyte concentration [mol.m-3]
    return k * c_e**0.5 * c_surf**0.5 * (c_max - c_surf) ** 0.5


def open_circuit_potential(c_surf):
    stretch = 1.062
    sto = stretch * c_surf / c_max

    u_eq = (
        2.16216
        + 0.07645 * tanh(30.834 - 54.4806 * sto)
        + 2.1581 * tanh(52.294 - 50.294 * sto)
        - 0.14169 * tanh(11.0923 - 19.8543 * sto)
        + 0.2051 * tanh(1.4684 - 5.4888 * sto)
        + 0.2531 * tanh((-sto + 0.56478) / 0.1316)
        - 0.02167 * tanh((sto - 0.525) / 0.006)
    )
    return u_eq

Now we can pass these functions, along with our scalar parameters, to ParameterValues

[30]:
param = pybamm.ParameterValues(
    {
        "Surface area per unit volume [m-1]": 0.15e6,
        "Positive particle radius [m]": 10e-6,
        "Separator thickness [m]": 25e-6,
        "Positive electrode thickness [m]": 100e-6,
        "Electrode cross-sectional area [m2]": 2.8e-2,
        "Applied current [A]": 0.9,
        "Positive electrode conductivity [S.m-1]": 10,
        "Electrolyte conductivity [S.m-1]": 1,
        "Diffusion coefficient [m2.s-1]": 1e-13,
        "Faraday constant [C.mol-1]": 96485,
        "Initial concentration [mol.m-3]": 25370,
        "Molar gas constant [J.mol-1.K-1]": 8.314,
        "Temperature [K]": 298.15,
        "Maximum concentration in positive electrode [mol.m-3]": 51217,
        "Positive electrode exchange-current density [A.m-2]": exchange_current_density,
        "Positive electrode OCP [V]": open_circuit_potential,
    }
)

Next we define the geometry. In the same way that our variable for the concentration in the electrode particles had and “auxiliary domain”, our spatial variable \(r\) also has an auxiliary domain. This means that when the model in discretised there will be the correct number of particles included in the geometry - one for each point in \(x\).

[31]:
r = pybamm.SpatialVariable(
    "r",
    domain=["positive particle"],
    auxiliary_domains={"secondary": "positive electrode"},
    coord_sys="spherical polar",
)
x_s = pybamm.SpatialVariable("x_s", domain=["separator"], coord_sys="cartesian")
x_p = pybamm.SpatialVariable(
    "x_p", domain=["positive electrode"], coord_sys="cartesian"
)


geometry = {
    "separator": {x_s: {"min": -L_s, "max": 0}},
    "positive electrode": {x_p: {"min": 0, "max": L_p}},
    "positive particle": {r: {"min": 0, "max": R_p}},
}

Both the model and geometry can now be processed by the parameter class. This replaces the parameters with the values

[32]:
param.process_model(model)
param.process_geometry(geometry)

We can now set up our mesh, choose a spatial method, and discretise our model

[33]:
submesh_types = {
    "separator": pybamm.Uniform1DSubMesh,
    "positive electrode": pybamm.Uniform1DSubMesh,
    "positive particle": pybamm.Uniform1DSubMesh,
}
var_pts = {x_s: 10, x_p: 20, r: 30}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

spatial_methods = {
    "separator": pybamm.FiniteVolume(),
    "positive electrode": pybamm.FiniteVolume(),
    "positive particle": pybamm.FiniteVolume(),
}
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model);

The model is now discretised and ready to be solved.

Solving the model#

We can now solve the model

[34]:
# solve
solver = pybamm.CasadiSolver(root_tol=1e-3)
t_eval = np.linspace(0, 3600, 600)
solution = solver.solve(model, t_eval)

and plot the results. To make the plot we will use dynamic_plot which automatically creates a slider plot given a solution and a list of variables to plot. By nesting variables in the list we can plot two variables together on the same axes.

[35]:
# plot
pybamm.dynamic_plot(
    solution,
    [
        "Positive electrode potential [V]",
        "Electrolyte potential [V]",
        "Positive electrode interfacial current density [A.m-2]",
        "Positive particle surface concentration [mol.m-3]",
        "Average positive particle surface concentration [mol.m-3]",
        ["Positive electrode OCP [V]", "Voltage [V]"],
    ],
)
2021-11-19 15:30:23,304 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for positive electrode. Using default of 1 [m].
2021-11-19 15:30:23,328 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for separator. Using default of 1 [m].
2021-11-19 15:30:23,367 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for positive electrode. Using default of 1 [m].
2021-11-19 15:30:23,395 - [WARNING] processed_variable.get_spatial_scale(520): No length scale set for positive electrode. Using default of 1 [m].
[35]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ff07a82d130>

In the next notebook we consider a simple model for SEI growth, and show how to correctly pose the model in non-dimensional form and then create and solve it using pybamm.

References#

The relevant papers for this notebook are:

[36]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Creating a Simple Model for SEI Growth#

Before adding a new model, please read the contribution guidelines

In this notebook, we will run through the steps involved in creating a new model within pybamm. We will then solve and plot the outputs of the model. We have chosen to implement a very simple model of SEI growth. We first give a brief derivation of the model and discuss how to nondimensionalise the model so that we can show the full process of model conception to solution within a single notebook.

Note: if you run the entire notebook and then try to evaluate the earlier cells, you will likely receive an error. This is because the state of objects is mutated as it is passed through various stages of processing. In this case, we recommend that you restart the Kernel and then evaluate cells in turn through the notebook.

A Simple Model of Solid Electrolyte Interphase (SEI) Growth#

The SEI is a porous layer that forms on the surfaces of negative electrode particles from the products of electrochemical reactions which consume lithium and electrolyte solvents. In the first few cycles of use, a lithium-ion battery loses a large amount of capacity; this is generally attributed to lithium being consumed to produce SEI. However, after a few cycles, the rate of capacity loss slows at a rate often (but not always) reported to scale with the square root of time. SEI growth is therefore often considered to be limited in some way by a diffusion process.

Dimensional Model#

In our simple SEI model, we consider a one-dimensional SEI which extends from the surface of a planar negative electrode at \(x=0\) until \(x=L\), where \(L\) is the thickness of the SEI. Since the SEI is porous, there is some electrolyte within the region \(x\in[0, L]\) and therefore some concentration of solvent, \(c\). Within the porous SEI, the solvent is transported via a diffusion process according to:

\[\begin{split}\frac{\partial c}{\partial t} = - \frac{\partial N}{\partial x}, \quad N = - D(c) \frac{\partial c}{\partial x} \label{1}\\\end{split}\]

where \(t\) is the time, \(N\) is the solvent flux, and \(D(c)\) is the effective solvent diffusivity (a function of the solvent concentration).

On the electrode-SEI surface (\(x=0\)) the solvent is consumed by the SEI growth reaction, \(R\). We assume that diffusion of solvent in the bulk electrolyte (\(x>L\)) is fast so that on the SEI-electrolyte surface (\(x=L\)) the concentration of solvent is fixed at the value \(c_{\infty}\). Therefore, the boundary conditions are

\[N|_{x=0} = - R, \quad c|_{x=L} = c_{\infty},\]

We also assume that the concentration of solvent within the SEI is initially uniform and equal to the bulk electrolyte solvent concentration, so that the initial condition is

\[c|_{t=0} = c_{\infty}\]

Since the SEI is growing, we require an additional equation for the SEI thickness. The thickness of the SEI grows at a rate proportional to the SEI growth reaction \(R\), where the constant of proportionality is the partial molar volume of the reaction products, \(\hat{V}\). We also assume that the SEI is initially of thickness \(L_0\). Therefore, we have

\[\frac{d L}{d t} = \hat{V} R, \quad L|_{t=0} = L_0\]

Finally, we assume for the sake of simplicity that the SEI growth reaction is irreversible and that the potential difference across the SEI is constant. The reaction is also assumed to be proportional to the concentration of solvent at the electrode-SEI surface (\(x=0\)). Therefore, the reaction flux is given by

\[R = k c|_{x=0}\]

where \(k\) is the reaction rate constant (which is in general dependent upon the potential difference across the SEI).

Fixing the moving boundary#

The model above is a moving boundary problem as it is defined in \(x\in[0, L]\). In order to solve it, we need to fix the boundary by introducing the scaling

\[x = L \xi.\]

Then, applying the chain rule we have

\[\frac{\partial}{\partial x} \rightarrow \frac{1}{L} \frac{\partial}{\partial \xi}, \quad \text{ and } \quad \frac{\partial}{\partial t} \rightarrow \frac{\partial}{\partial t} - \frac{L'(t)}{L(t)} \xi \frac{\partial}{\partial \xi}.\]

Then, (1) can be rewritten as

\[\frac{\partial c}{\partial t} = \frac{\hat{V} R}{L} \xi \frac{\partial c}{\partial \xi} + \frac{1}{L^2} \frac{\partial}{\partial \xi} \left( D(c) \frac{\partial c}{\partial \xi}\right)\]

Entering the Model into PyBaMM#

As always, we begin by importing PyBaMM and changing our working directory to the root of the pybamm/ folder.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os

os.chdir(pybamm.__path__[0] + "/..")

[notice] A new release of pip is available: 23.0.1 -> 23.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

A model is defined in six steps: 1. Initialise model 2. Define parameters and variables 3. State governing equations 4. State boundary conditions 5. State initial conditions 6. State output variables

We shall proceed through each step to enter our simple SEI growth model.

Initialise model#

We first initialise the model using the BaseModel class. This sets up the required structure for our model.

[2]:
model = pybamm.BaseModel()
Define parameters and variables#

In our SEI model, we have two dimensionless parameters, \(k\) and \(\hat{V}\), and one dimensionless function \(D(c)\), which are all given in terms of the dimensional parameters, see (5). In pybamm, inputs are dimensional, so we first state all the dimensional parameters. We then define the dimensionless parameters, which are expressed an non-dimensional groupings of dimensional parameters. To define the dimensional parameters, we use the Parameter object to create parameter symbols. Parameters which are functions are defined using FunctionParameter object and should be defined within a python function as shown.

[3]:
# dimensional parameters
k = pybamm.Parameter("Reaction rate constant [m.s-1]")
L_0 = pybamm.Parameter("Initial thickness [m]")
V_hat = pybamm.Parameter("Partial molar volume [m3.mol-1]")
c_inf = pybamm.Parameter("Bulk electrolyte solvent concentration [mol.m-3]")


def D(cc):
    return pybamm.FunctionParameter(
        "Diffusivity [m2.s-1]", {"Solvent concentration [mol.m-3]": cc}
    )

We now define the dimensionless variables in our model. Since these are the variables we solve for directly, we do not need to write them in terms of the dimensional variables. We simply use SpatialVariable and Variable to create the required symbols:

[4]:
xi = pybamm.SpatialVariable("xi", domain="SEI layer", coord_sys="cartesian")
c = pybamm.Variable("Solvent concentration [mol.m-3]", domain="SEI layer")
L = pybamm.Variable("SEI thickness [m]")
State governing equations#

We can now use the symbols we have created for our parameters and variables to write out our governing equations. Note that before we use the reaction flux and solvent flux, we must derive new symbols for them from the defined parameter and variable symbols. Each governing equation must also be stated in the explicit form d/dt = rhs since pybamm only stores the right hand side (rhs) and assumes that the left hand side is the time derivative. The governing equations are then simply

[5]:
# SEI reaction flux
R = k * pybamm.BoundaryValue(c, "left")

# solvent concentration equation
N = -1 / L * D(c) * pybamm.grad(c)
dcdt = (V_hat * R) / L * pybamm.inner(xi, pybamm.grad(c)) - 1 / L * pybamm.div(N)

# SEI thickness equation
dLdt = V_hat * R

Once we have stated the equations, we can add them to the model.rhs dictionary. This is a dictionary whose keys are the variables being solved for, and whose values correspond right hand sides of the governing equations for each variable.

[6]:
model.rhs = {c: dcdt, L: dLdt}
State boundary conditions#

We only have boundary conditions on the solvent concentration equation. We must state where a condition is Neumann (on the gradient) or Dirichlet (on the variable itself).

The boundary condition on the electrode-SEI (x=0) boundary is:

\[N|_{\xi=0} = - R, \quad N|_{\xi=0} = - D(c|_{\xi=0} )\left.\frac{\partial c}{\partial \xi}\right|_{\xi=0}\]

which is a Neumann condition. To implement this boundary condition in pybamm, we must first rearrange the equation so that the gradient of the concentration, \(\nabla c|_{x=0}\), is the subject. Therefore we have

\[\left.\frac{\partial c}{\partial \xi}\right|_{\xi=0} = \frac{R L}{D(c|_{\xi=0} )}\]

which we enter into pybamm as

[7]:
D_left = pybamm.BoundaryValue(
    D(c), "left"
)  # pybamm requires BoundaryValue(D(c)) and not D(BoundaryValue(c))
grad_c_left = R * L / D_left

On the SEI-electrolyte boundary (\(\xi=1\)), we have the boundary condition

\[c|_{\xi=1} = c_∞\]

which is a Dirichlet condition and is just entered as

[8]:
c_right = c_inf

We now load these boundary conditions into the model.boundary_conditions dictionary in the following way, being careful to state the type of boundary condition:

[9]:
model.boundary_conditions = {
    c: {"left": (grad_c_left, "Neumann"), "right": (c_right, "Dirichlet")}
}
State initial conditions#

There are two initial conditions in our model:

\[c|_{t=0} = c_∞, \quad L|_{t=0} = L_0\]

which are simply written in pybamm as

[10]:
c_init = c_inf
L_init = L_0

and then included into the model.initial_conditions dictionary:

[11]:
model.initial_conditions = {c: c_init, L: L_init}
State output variables#

We already have everything required in model for the model to be used and solved, but we have not yet stated what we actually want to output from the model. PyBaMM allows users to output any combination of symbols as an output variable therefore allowing the user the flexibility to output important quantities without further tedious postprocessing steps.

Some useful outputs for this simple model are: - the SEI thickness - the SEI growth rate - the solvent concentration

These are added to the model by adding entries to the model.variables dictionary

[12]:
model.variables = {
    "SEI thickness [m]": L,
    "SEI growth rate [m]": dLdt,
    "Solvent concentration [mol.m-3]": c,
}

The model is now fully defined and ready to be used. If you plan on reusing the model several times, you can additionally set model defaults which may include: a default geometry to run the model on, a default set of parameter values, a default solver, etc.

Using the Model#

The model will now behave in the same way as any of the inbuilt PyBaMM models. However, to demonstrate that the model works we display the steps involved in solving the model but we will not go into details within this notebook.

[13]:
# define geometry
geometry = pybamm.Geometry(
    {"SEI layer": {xi: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}}}
)


def Diffusivity(cc):
    return cc * 10 ** (-12)


# parameter values (not physically based, for example only!)
param = pybamm.ParameterValues(
    {
        "Reaction rate constant [m.s-1]": 1e-6,
        "Initial thickness [m]": 1e-6,
        "Partial molar volume [m3.mol-1]": 10,
        "Bulk electrolyte solvent concentration [mol.m-3]": 1,
        "Diffusivity [m2.s-1]": Diffusivity,
    }
)

# process model and geometry
param.process_model(model)
param.process_geometry(geometry)

# mesh and discretise
submesh_types = {"SEI layer": pybamm.Uniform1DSubMesh}
var_pts = {xi: 100}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

spatial_methods = {"SEI layer": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model)
[13]:
<pybamm.models.base_model.BaseModel at 0x7f3a8005b490>
[14]:
# solve
solver = pybamm.ScipySolver()
t = [0, 100]  # solve for 100s
solution = solver.solve(model, t)

# post-process output variables
L_out = solution["SEI thickness [m]"]
c_out = solution["Solvent concentration [mol.m-3]"]

Using these outputs, we can now plot the SEI thickness as a function of time and also the solvent concentration profile within the SEI. We use a slider to plot the concentration profile at different times. Note that, even though our model is written in nondimensional form, the processed variables are functions of dimensional space and time (in SI units).

[15]:
import matplotlib.pyplot as plt

# plot SEI thickness in microns as a function of t in microseconds
# and concentration in mol/m3 as a function of x in microns
L_0_eval = param.evaluate(L_0)
xi = np.linspace(0, 1, 100)  # dimensionless space


def plot(t):
    _, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
    ax1.plot(solution.t, L_out(solution.t) * 1e6)
    ax1.plot(t, L_out(t) * 1e6, "r.")
    ax1.set_ylabel(r"SEI thickness [$\mu$m]")
    ax1.set_xlabel(r"t [s]")

    ax2.plot(xi * L_out(t) * 1e6, c_out(t, xi))
    ax2.set_ylim(0, 1.1)
    ax2.set_xlim(0, L_out(solution.t[-1]) * 1e6)
    ax2.set_ylabel("Solvent concentration [mol.m-3]")
    ax2.set_xlabel(r"x [$\mu$m]")

    plt.tight_layout()
    plt.show()


import ipywidgets as widgets

widgets.interact(
    plot, t=widgets.FloatSlider(min=0, max=solution.t[-1], step=0.1, value=0)
);

Formally adding your model#

The purpose of this notebook has been to go through the steps involved in getting a simple model working within PyBaMM. However, if you plan on reusing your model and want greater flexibility then we recommend that you create a new class for your model. We have set out instructions on how to do this in the “Adding a Model” tutorial in the documentation.

References#

The relevant papers for this notebook are:

[16]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

API documentation#

Release:

24.1

Date:

Jan 31, 2024

This reference manual details functions, modules, and objects included in PyBaMM, describing what they are and what they do. For a high-level introduction to PyBaMM, see the user guide and the examples.

Expression Tree#

Symbol#

pybamm.simplify_if_constant(symbol)[source]#

Utility function to simplify an expression tree if it evalutes to a constant scalar, vector or matrix

class pybamm.Symbol(name, children=None, domain=None, auxiliary_domains=None, domains=None)[source]#

Base node class for the expression tree.

Parameters:
  • name (str) – name for the node

  • children (iterable Symbol, optional) – children to attach to this node, default to an empty list

  • domain (iterable of str, or str) – list of domains over which the node is valid (empty list indicates the symbol is valid over all domains)

  • auxiliary_domains (dict of str) – dictionary of auxiliary domains over which the node is valid (empty dictionary indicates no auxiliary domains). Keys can be “secondary”, “tertiary” or “quaternary”. The symbol is broadcast over its auxiliary domains. For example, a symbol might have domain “negative particle”, secondary domain “separator” and tertiary domain “current collector” (domain=”negative particle”, auxiliary_domains={“secondary”: “separator”, “tertiary”: “current collector”}).

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

__abs__()[source]#

return an AbsoluteValue object, or a smooth approximation.

__add__(other)[source]#

return an Addition object.

__array_ufunc__(ufunc, method, *inputs, **kwargs)[source]#

If a numpy ufunc is applied to a symbol, call the corresponding pybamm function instead.

__eq__(other)[source]#

Return self==value.

__ge__(other)[source]#

return a EqualHeaviside object, or a smooth approximation.

__gt__(other)[source]#

return a NotEqualHeaviside object, or a smooth approximation.

__hash__()[source]#

Return hash(self).

__init__(name, children=None, domain=None, auxiliary_domains=None, domains=None)[source]#
__le__(other)[source]#

return a EqualHeaviside object, or a smooth approximation.

__lt__(other)[source]#

return a NotEqualHeaviside object, or a smooth approximation.

__matmul__(other)[source]#

return a MatrixMultiplication object.

__mod__(other)[source]#

return an Modulo object.

__mul__(other)[source]#

return a Multiplication object.

__neg__()[source]#

return a Negate object.

__pow__(other)[source]#

return a Power object.

__radd__(other)[source]#

return an Addition object.

__repr__()[source]#

returns the string __class__(id, name, children, domain)

__rmatmul__(other)[source]#

return a MatrixMultiplication object.

__rmul__(other)[source]#

return a Multiplication object.

__rpow__(other)[source]#

return a Power object.

__rsub__(other)[source]#

return a Subtraction object.

__rtruediv__(other)[source]#

return a Division object.

__str__()[source]#

return a string representation of the node and its children.

__sub__(other)[source]#

return a Subtraction object.

__truediv__(other)[source]#

return a Division object.

__weakref__#

list of weak references to the object (if defined)

property auxiliary_domains#

Returns auxiliary domains.

property children#

returns the cached children of this node.

Note: it is assumed that children of a node are not modified after initial creation

clear_domains()[source]#

Clear domains, bypassing checks.

copy_domains(symbol)[source]#

Copy the domains from a given symbol, bypassing checks.

create_copy()[source]#

Make a new copy of a symbol, to avoid Tree corruption errors while bypassing copy.deepcopy(), which is slow.

diff(variable)[source]#

Differentiate a symbol with respect to a variable. For any symbol that can be differentiated, return 1 if differentiating with respect to yourself, self._diff(variable) if variable is in the expression tree of the symbol, and zero otherwise.

Parameters:

variable (pybamm.Symbol) – The variable with respect to which to differentiate

property domain#

list of applicable domains.

Return type:

iterable of str

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

Evaluate expression tree (wrapper to allow using dict of known values).

Parameters:
  • t (float or numeric type, optional) – time at which to evaluate (default None)

  • y (numpy.array, optional) – array with state values to evaluate when solving (default None)

  • y_dot (numpy.array, optional) – array with time derivatives of state values to evaluate when solving (default None)

  • inputs (dict, optional) – dictionary of inputs to use when solving (default None)

Returns:

the node evaluated at (t,y)

Return type:

number or array

evaluate_for_shape()[source]#

Evaluate expression tree to find its shape.

For symbols that cannot be evaluated directly (e.g. Variable or Parameter), a vector of the appropriate shape is returned instead, using the symbol’s domain. See pybamm.Symbol.evaluate()

evaluate_ignoring_errors(t=0)[source]#

Evaluates the expression. If a node exists in the tree that cannot be evaluated as a scalar or vector (e.g. Time, Parameter, Variable, StateVector), then None is returned. If there is an InputParameter in the tree then a 1 is returned. Otherwise the result of the evaluation is given.

See also

evaluate

evaluate the expression

evaluates_on_edges(dimension)[source]#

Returns True if a symbol evaluates on an edge, i.e. symbol contains a gradient operator, but not a divergence operator, and is not an IndefiniteIntegral. Caches the solution for faster results.

Parameters:

dimension (str) – The dimension (primary, secondary, etc) in which to query evaluation on edges

Returns:

Whether the symbol evaluates on edges (in the finite volume discretisation sense)

Return type:

bool

evaluates_to_number()[source]#

Returns True if evaluating the expression returns a number. Returns False otherwise, including if NotImplementedError or TyperError is raised. !Not to be confused with isinstance(self, pybamm.Scalar)!

See also

evaluate

evaluate the expression

get_children_domains(children)[source]#

Combine domains from children, at all levels.

has_symbol_of_classes(symbol_classes)[source]#

Returns True if equation has a term of the class(es) symbol_class.

Parameters:

symbol_classes (pybamm class or iterable of classes) – The classes to test the symbol against

is_constant()[source]#

returns true if evaluating the expression is not dependent on t or y or inputs

See also

evaluate

evaluate the expression

jac(variable, known_jacs=None, clear_domain=True)[source]#

Differentiate a symbol with respect to a (slice of) a StateVector or StateVectorDot. See pybamm.Jacobian.

property name#

name of the node.

property ndim_for_testing#

Number of dimensions of an object, found by evaluating it with appropriate t and y

new_copy()[source]#

Returns create_copy with added attributes

property orphans#

Returning new copies of the children, with parents removed to avoid corrupting the expression tree internal data

pre_order()[source]#

returns an iterable that steps through the tree in pre-order fashion.

Examples

>>> a = pybamm.Symbol('a')
>>> b = pybamm.Symbol('b')
>>> for node in (a*b).pre_order():
...     print(node.name)
*
a
b
property quaternary_domain#

Helper function to get the quaternary domain of a symbol.

relabel_tree(symbol, counter)[source]#

Finds all children of a symbol and assigns them a new id so that they can be visualised properly using the graphviz output

render()[source]#

Print out a visual representation of the tree (this node and its children)

property secondary_domain#

Helper function to get the secondary domain of a symbol.

set_id()[source]#

Set the immutable “identity” of a variable (e.g. for identifying y_slices).

Hashing can be slow, so we set the id when we create the node, and hence only need to hash once.

property shape#

Shape of an object, found by evaluating it with appropriate t and y.

property shape_for_testing#

Shape of an object for cases where it cannot be evaluated directly. If a symbol cannot be evaluated directly (e.g. it is a Variable or Parameter), it is instead given an arbitrary domain-dependent shape.

property size#

Size of an object, found by evaluating it with appropriate t and y

property size_for_testing#

Size of an object, based on shape for testing.

property tertiary_domain#

Helper function to get the tertiary domain of a symbol.

test_shape()[source]#

Check that the discretised self has a pybamm shape, i.e. can be evaluated.

Raises:

pybamm.ShapeError – If the shape of the object cannot be found

to_casadi(t=None, y=None, y_dot=None, inputs=None, casadi_symbols=None)[source]#

Convert the expression tree to a CasADi expression tree. See pybamm.CasadiConverter.

to_json()[source]#

Method to serialise a Symbol object into JSON.

visualise(filename)[source]#

Produces a .png file of the tree (this node and its children) with the name filename

Parameters:

filename (str) – filename to output, must end in “.png”

Parameter#

class pybamm.Parameter(name)[source]#

A node in the expression tree representing a parameter.

This node will be replaced by a pybamm.Scalar node

Parameters:

name (str) – name of the node

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

is_constant()[source]#

See pybamm.Symbol.is_constant().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

to_json()[source]#

Method to serialise a Symbol object into JSON.

class pybamm.FunctionParameter(name, inputs, diff_variable=None, print_name='calculate')[source]#

A node in the expression tree representing a function parameter.

This node will be replaced by a pybamm.Function node if a callable function is passed to the parameter values, and otherwise (in some rarer cases, such as constant current) a pybamm.Scalar node.

Parameters:
  • name (str) – name of the node

  • inputs (dict) – A dictionary with string keys and pybamm.Symbol values representing the function inputs. The string keys should provide a reasonable description of what the input to the function is (e.g. “Electrolyte concentration [mol.m-3]”)

  • diff_variable (pybamm.Symbol, optional) – if diff_variable is specified, the FunctionParameter node will be replaced by a pybamm.Function and then differentiated with respect to diff_variable. Default is None.

  • print_name (str, optional) – The name to show when printing. Default is ‘calculate’, in which case the name is calculated using sys._getframe().

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

diff(variable)[source]#

See pybamm.Symbol.diff().

set_id()[source]#

See pybamm.Symbol.set_id()

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

to_json()[source]#

Method to serialise a Symbol object into JSON.

Variable#

class pybamm.Variable(name, domain=None, auxiliary_domains=None, domains=None, bounds=None, print_name=None, scale=1, reference=0)[source]#

A node in the expression tree represending a dependent variable.

This node will be discretised by Discretisation and converted to a pybamm.StateVector node.

Parameters:
  • name (str) – name of the node domain : iterable of str, optional list of domains that this variable is valid over

  • auxiliary_domains (dict, optional) – dictionary of auxiliary domains ({‘secondary’: …, ‘tertiary’: …, ‘quaternary’: …}). For example, for the single particle model, the particle concentration would be a Variable with domain ‘negative particle’ and secondary auxiliary domain ‘current collector’. For the DFN, the particle concentration would be a Variable with domain ‘negative particle’, secondary domain ‘negative electrode’ and tertiary domain ‘current collector’

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

  • bounds (tuple, optional) – Physical bounds on the variable

  • print_name (str, optional) – The name to use for printing. Default is None, in which case self.name is used.

  • scale (float or pybamm.Symbol, optional) – The scale of the variable, used for scaling the model when solving. The state vector representing this variable will be multiplied by this scale. Default is 1.

  • reference (float or pybamm.Symbol, optional) – The reference value of the variable, used for scaling the model when solving. This value will be added to the state vector representing this variable. Default is 0.

Extends: pybamm.expression_tree.variable.VariableBase

diff(variable)[source]#

Differentiate a symbol with respect to a variable. For any symbol that can be differentiated, return 1 if differentiating with respect to yourself, self._diff(variable) if variable is in the expression tree of the symbol, and zero otherwise.

Parameters:

variable (pybamm.Symbol) – The variable with respect to which to differentiate

class pybamm.VariableDot(name, domain=None, auxiliary_domains=None, domains=None, bounds=None, print_name=None, scale=1, reference=0)[source]#

A node in the expression tree represending the time derviative of a dependent variable

This node will be discretised by Discretisation and converted to a pybamm.StateVectorDot node.

Parameters:
  • name (str) – name of the node

  • domain (iterable of str) – list of domains that this variable is valid over

  • auxiliary_domains (dict) – dictionary of auxiliary domains ({‘secondary’: …, ‘tertiary’: …, ‘quaternary’: …}). For example, for the single particle model, the particle concentration would be a Variable with domain ‘negative particle’ and secondary auxiliary domain ‘current collector’. For the DFN, the particle concentration would be a Variable with domain ‘negative particle’, secondary domain ‘negative electrode’ and tertiary domain ‘current collector’

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

  • bounds (tuple, optional) – Physical bounds on the variable. Included for compatibility with VariableBase, but ignored.

  • print_name (str, optional) – The name to use for printing. Default is None, in which case self.name is used.

  • scale (float or pybamm.Symbol, optional) – The scale of the variable, used for scaling the model when solving. The state vector representing this variable will be multiplied by this scale. Default is 1.

  • reference (float or pybamm.Symbol, optional) – The reference value of the variable, used for scaling the model when solving. This value will be added to the state vector representing this variable. Default is 0.

Extends: pybamm.expression_tree.variable.VariableBase

diff(variable)[source]#

Differentiate a symbol with respect to a variable. For any symbol that can be differentiated, return 1 if differentiating with respect to yourself, self._diff(variable) if variable is in the expression tree of the symbol, and zero otherwise.

Parameters:

variable (pybamm.Symbol) – The variable with respect to which to differentiate

get_variable()[source]#

return a Variable corresponding to this VariableDot

Note: Variable._jac adds a dash to the name of the corresponding VariableDot, so we remove this here

Independent Variable#

class pybamm.IndependentVariable(name, domain=None, auxiliary_domains=None, domains=None)[source]#

A node in the expression tree representing an independent variable.

Used for expressing functions depending on a spatial variable or time

Parameters:
  • name (str) – name of the node

  • domain (iterable of str) – list of domains that this variable is valid over

  • auxiliary_domains (dict, optional) – dictionary of auxiliary domains, defaults to empty dict

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

Extends: pybamm.expression_tree.symbol.Symbol

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

class pybamm.Time[source]#

A node in the expression tree representing time.

Extends: pybamm.expression_tree.independent_variable.IndependentVariable

create_copy()[source]#

See pybamm.Symbol.new_copy().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

class pybamm.SpatialVariable(name, domain=None, auxiliary_domains=None, domains=None, coord_sys=None)[source]#

A node in the expression tree representing a spatial variable.

Parameters:
  • name (str) – name of the node (e.g. “x”, “y”, “z”, “r”, “x_n”, “x_s”, “x_p”, “r_n”, “r_p”)

  • domain (iterable of str) – list of domains that this variable is valid over (e.g. “cartesian”, “spherical polar”)

  • auxiliary_domains (dict, optional) – dictionary of auxiliary domains, defaults to empty dict

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

Extends: pybamm.expression_tree.independent_variable.IndependentVariable

create_copy()[source]#

See pybamm.Symbol.new_copy().

pybamm.t = the independent variable time#

A node in the expression tree representing time.

Scalar#

class pybamm.Scalar(value, name=None)[source]#

A node in the expression tree representing a scalar value.

Parameters:
  • value (numeric) – the value returned by the node when evaluated

  • name (str, optional) – the name of the node. Defaulted to str(value) if not provided

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

is_constant()[source]#

See pybamm.Symbol.is_constant().

set_id()[source]#

See pybamm.Symbol.set_id().

to_equation()[source]#

Returns the value returned by the node when evaluated.

to_json()[source]#

Method to serialise a Symbol object into JSON.

property value#

The value returned by the node when evaluated.

Array#

class pybamm.Array(entries, name=None, domain=None, auxiliary_domains=None, domains=None, entries_string=None)[source]#

Node in the expression tree that holds an tensor type variable (e.g. numpy.array)

Parameters:
  • entries (numpy.array or list) – the array associated with the node. If a list is provided, it is converted to a numpy array

  • name (str, optional) – the name of the node

  • domain (iterable of str, optional) – list of domains the parameter is valid over, defaults to empty list

  • auxiliary_domains (dict, optional) – dictionary of auxiliary domains, defaults to empty dict

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

  • entries_string (str) – String representing the entries (slow to recalculate when copying)

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

is_constant()[source]#

See pybamm.Symbol.is_constant().

property ndim#

returns the number of dimensions of the tensor.

set_id()[source]#

See pybamm.Symbol.set_id().

property shape#

returns the number of entries along each dimension.

to_equation()[source]#

Returns the value returned by the node when evaluated.

to_json()[source]#

Method to serialise an Array object into JSON.

pybamm.linspace(start, stop, num=50, **kwargs)[source]#

Creates a linearly spaced array by calling numpy.linspace with keyword arguments ‘kwargs’. For a list of ‘kwargs’ see the numpy linspace documentation

pybamm.meshgrid(x, y, **kwargs)[source]#

Return coordinate matrices as from coordinate vectors by calling numpy.meshgrid with keyword arguments ‘kwargs’. For a list of ‘kwargs’ see the numpy meshgrid documentation

Matrix#

class pybamm.Matrix(entries, name=None, domain=None, auxiliary_domains=None, domains=None, entries_string=None)[source]#

Node in the expression tree that holds a matrix type (e.g. numpy.array)

Extends: pybamm.expression_tree.array.Array

Vector#

class pybamm.Vector(entries, name=None, domain=None, auxiliary_domains=None, domains=None, entries_string=None)[source]#

node in the expression tree that holds a vector type (e.g. numpy.array)

Extends: pybamm.expression_tree.array.Array

State Vector#

class pybamm.StateVector(*y_slices, name=None, domain=None, auxiliary_domains=None, domains=None, evaluation_array=None)[source]#

Node in the expression tree that holds a slice to read from an external vector type.

Parameters:
  • y_slice (slice) – the slice of an external y to read

  • name (str, optional) – the name of the node

  • domain (iterable of str, optional) – list of domains the parameter is valid over, defaults to empty list

  • auxiliary_domains (dict of str, optional) – dictionary of auxiliary domains

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

  • evaluation_array (list, optional) – List of boolean arrays representing slices. Default is None, in which case the evaluation_array is computed from y_slices.

Extends: pybamm.expression_tree.state_vector.StateVectorBase

diff(variable)[source]#

Differentiate a symbol with respect to a variable. For any symbol that can be differentiated, return 1 if differentiating with respect to yourself, self._diff(variable) if variable is in the expression tree of the symbol, and zero otherwise.

Parameters:

variable (pybamm.Symbol) – The variable with respect to which to differentiate

class pybamm.StateVectorDot(*y_slices, name=None, domain=None, auxiliary_domains=None, domains=None, evaluation_array=None)[source]#

Node in the expression tree that holds a slice to read from the ydot.

Parameters:
  • y_slice (slice) – the slice of an external ydot to read

  • name (str, optional) – the name of the node

  • domain (iterable of str, optional) – list of domains the parameter is valid over, defaults to empty list

  • auxiliary_domains (dict of str, optional) – dictionary of auxiliary domains

  • domains (dict) – A dictionary equivalent to {‘primary’: domain, auxiliary_domains}. Either ‘domain’ and ‘auxiliary_domains’, or just ‘domains’, should be provided (not both). In future, the ‘domain’ and ‘auxiliary_domains’ arguments may be deprecated.

  • evaluation_array (list, optional) – List of boolean arrays representing slices. Default is None, in which case the evaluation_array is computed from y_slices.

Extends: pybamm.expression_tree.state_vector.StateVectorBase

diff(variable)[source]#

Differentiate a symbol with respect to a variable. For any symbol that can be differentiated, return 1 if differentiating with respect to yourself, self._diff(variable) if variable is in the expression tree of the symbol, and zero otherwise.

Parameters:

variable (pybamm.Symbol) – The variable with respect to which to differentiate

Binary Operators#

class pybamm.BinaryOperator(name, left, right)[source]#

A node in the expression tree representing a binary operator (e.g. +, *)

Derived classes will specify the particular operator

Parameters:
  • name (str) – name of the node

  • left (Symbol or Number) – lhs child node (converted to Scalar if Number)

  • right (Symbol or Number) – rhs child node (converted to Scalar if Number)

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

See pybamm.Symbol.evaluate().

is_constant()[source]#

See pybamm.Symbol.is_constant().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

to_json()[source]#

Method to serialise a BinaryOperator object into JSON.

class pybamm.Power(left, right)[source]#

A node in the expression tree representing a ** power operator.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Addition(left, right)[source]#

A node in the expression tree representing an addition operator.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Subtraction(left, right)[source]#

A node in the expression tree representing a subtraction operator.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Multiplication(left, right)[source]#

A node in the expression tree representing a multiplication operator (Hadamard product). Overloads cases where the “*” operator would usually return a matrix multiplication (e.g. scipy.sparse.coo.coo_matrix)

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.MatrixMultiplication(left, right)[source]#

A node in the expression tree representing a matrix multiplication operator.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

diff(variable)[source]#

See pybamm.Symbol.diff().

class pybamm.Division(left, right)[source]#

A node in the expression tree representing a division operator.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Inner(left, right)[source]#

A node in the expression tree which represents the inner (or dot) product. This operator should be used to take the inner product of two mathematical vectors (as opposed to the computational vectors arrived at post-discretisation) of the form v = v_x e_x + v_y e_y + v_z e_z where v_x, v_y, v_z are scalars and e_x, e_y, e_z are x-y-z-directional unit vectors. For v and w mathematical vectors, inner product returns v_x * w_x + v_y * w_y + v_z * w_z. In addition, for some spatial discretisations mathematical vector quantities (such as i = grad(phi) ) are evaluated on a different part of the grid to mathematical scalars (e.g. for finite volume mathematical scalars are evaluated on the nodes but mathematical vectors are evaluated on cell edges). Therefore, inner also transfers the inner product of the vector onto the scalar part of the grid if required by a particular discretisation.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.expression_tree.binary_operators._Heaviside(name, left, right)[source]#

A node in the expression tree representing a heaviside step function. This class is semi-private and should not be called directly, use EqualHeaviside or NotEqualHeaviside instead, or < or <=.

Adding this operation to the rhs or algebraic equations in a model can often cause a discontinuity in the solution. For the specific cases listed below, this will be automatically handled by the solver. In the general case, you can explicitly tell the solver of discontinuities by adding a Event object with EventType DISCONTINUITY to the model’s list of events.

In the case where the Heaviside function is of the form pybamm.t < x, pybamm.t <= x, x < pybamm.t, or x <= pybamm.t, where x is any constant equation, this DISCONTINUITY event will automatically be added by the solver.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

diff(variable)[source]#

See pybamm.Symbol.diff().

class pybamm.EqualHeaviside(left, right)[source]#

A heaviside function with equality (return 1 when left = right)

Extends: pybamm.expression_tree.binary_operators._Heaviside

class pybamm.NotEqualHeaviside(left, right)[source]#

A heaviside function without equality (return 0 when left = right)

Extends: pybamm.expression_tree.binary_operators._Heaviside

class pybamm.Modulo(left, right)[source]#

Calculates the remainder of an integer division.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Minimum(left, right)[source]#

Returns the smaller of two objects.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

class pybamm.Maximum(left, right)[source]#

Returns the greater of two objects.

Extends: pybamm.expression_tree.binary_operators.BinaryOperator

pybamm.minimum(left, right)[source]#

Returns the smaller of two objects, possibly with a smoothing approximation. Not to be confused with pybamm.min(), which returns min function of child.

pybamm.maximum(left, right)[source]#

Returns the larger of two objects, possibly with a smoothing approximation. Not to be confused with pybamm.max(), which returns max function of child.

pybamm.softminus(left, right, k)[source]#

Softminus approximation to the minimum function. k is the smoothing parameter, set by pybamm.settings.min_max_smoothing. The recommended value is k=10.

pybamm.softplus(left, right, k)[source]#

Softplus approximation to the maximum function. k is the smoothing parameter, set by pybamm.settings.min_max_smoothing. The recommended value is k=10.

pybamm.sigmoid(left, right, k)[source]#

Sigmoidal approximation to the heaviside function. k is the smoothing parameter, set by pybamm.settings.heaviside_smoothing. The recommended value is k=10. Note that the concept of deciding which side to pick when left=right does not apply for this smooth approximation. When left=right, the value is (left+right)/2.

pybamm.source(left, right, boundary=False)[source]#

A convenience function for creating (part of) an expression tree representing a source term. This is necessary for spatial methods where the mass matrix is not the identity (e.g. finite element formulation with piecwise linear basis functions). The left child is the symbol representing the source term and the right child is the symbol of the equation variable (currently, the finite element formulation in PyBaMM assumes all functions are constructed using the same basis, and the matrix here is constructed accoutning for the boundary conditions of the right child). The method returns the matrix-vector product of the mass matrix (adjusted to account for any Dirichlet boundary conditions imposed the the right symbol) and the discretised left symbol.

Parameters:
  • left (Symbol) – The left child node, which represents the expression for the source term.

  • right (Symbol) – The right child node. This is the symbol whose boundary conditions are accounted for in the construction of the mass matrix.

  • boundary (bool, optional) – If True, then the mass matrix should is assembled over the boundary, corresponding to a source term which only acts on the boundary of the domain. If False (default), the matrix is assembled over the entire domain, corresponding to a source term in the bulk.

Unary Operators#

class pybamm.UnaryOperator(name, child, domains=None)[source]#

A node in the expression tree representing a unary operator (e.g. ‘-’, grad, div)

Derived classes will specify the particular operator

Parameters:
  • name (str) – name of the node

  • child (Symbol) – child node

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

See pybamm.Symbol.evaluate().

is_constant()[source]#

See pybamm.Symbol.is_constant().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

class pybamm.Negate(child)[source]#

A node in the expression tree representing a - negation operator.

Extends: pybamm.expression_tree.unary_operators.UnaryOperator

class pybamm.AbsoluteValue(child)[source]#

A node in the expression tree representing an abs operator.

Extends: pybamm.expression_tree.unary_operators.UnaryOperator

diff(variable)[source]#

See pybamm.Symbol.diff().

class pybamm.Sign(child)[source]#

A node in the expression tree representing a sign operator.

Extends: pybamm.expression_tree.unary_operators.UnaryOperator

diff(variable)[source]#

See pybamm.Symbol.diff().

class pybamm.Index(child, index, name=None, check_size=True)[source]#

A node in the expression tree, which stores the index that should be extracted from its child after the child has been evaluated.

Parameters:
  • child (pybamm.Symbol) – The symbol of which to take the index

  • index (int or slice) – The index (if int) or indices (if slice) to extract from the symbol

  • name (str, optional) – The name of the symbol

  • check_size (bool, optional) – Whether to check if the slice size exceeds the child size. Default is True. This should always be True when creating a new symbol so that the appropriate check is performed, but should be False for creating a new copy to avoid unnecessarily repeating the check.

Extends: pybamm.expression_tree.unary_operators.UnaryOperator

set_id()[source]#

See pybamm.Symbol.set_id()

to_json()[source]#

Method to serialise an Index object into JSON.

class pybamm.SpatialOperator(name, child, domains=None)[source]#

A node in the expression tree representing a unary spatial operator (e.g. grad, div)

Derived classes will specify the particular operator

This type of node will be replaced by the Discretisation class with a Matrix

Parameters:
  • name (str) – name of the node

  • child (Symbol) – child node

Extends: pybamm.expression_tree.unary_operators.UnaryOperator

to_json()[source]#

Method to serialise a Symbol object into JSON.

class pybamm.Gradient(child)[source]#

A node in the expression tree representing a grad operator.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.Divergence(child)[source]#

A node in the expression tree representing a div operator.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.Laplacian(child)[source]#

A node in the expression tree representing a Laplacian operator. This is currently only implemeted in the weak form for finite element formulations.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.GradientSquared(child)[source]#

A node in the expression tree representing a the inner product of the grad operator with itself. In particular, this is useful in the finite element formualtion where we only require the (sclar valued) square of the gradient, and not the gradient itself.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.Mass(child)[source]#

Returns the mass matrix for a given symbol, accounting for Dirchlet boundary conditions where necessary (e.g. in the finite element formualtion)

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.BoundaryMass(child)[source]#

Returns the mass matrix for a given symbol assembled over the boundary of the domain, accounting for Dirchlet boundary conditions where necessary (e.g. in the finite element formualtion)

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.Integral(child, integration_variable)[source]#

A node in the expression tree representing an integral operator.

\[I = \int_{a}^{b}\!f(u)\,du,\]

where \(a\) and \(b\) are the left-hand and right-hand boundaries of the domain respectively, and \(u\in\text{domain}\).

Parameters:

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.IndefiniteIntegral(child, integration_variable)[source]#

A node in the expression tree representing an indefinite integral operator.

\[I = \int_{x_ ext{min}}^{x}\!f(u)\,du\]

where \(u\in\text{domain}\) which can represent either a spatial or temporal variable.

Parameters:

Extends: pybamm.expression_tree.unary_operators.BaseIndefiniteIntegral

class pybamm.DefiniteIntegralVector(child, vector_type='row')[source]#

A node in the expression tree representing an integral of the basis used for discretisation

\[I = \int_{a}^{b}\!\psi(x)\,dx,\]

where \(a\) and \(b\) are the left-hand and right-hand boundaries of the domain respectively and \(\psi\) is the basis function.

Parameters:
  • variable (pybamm.Symbol) – The variable whose basis will be integrated over the entire domain (will become self.children[0])

  • vector_type (str, optional) – Whether to return a row or column vector (default is row)

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.BoundaryIntegral(child, region='entire')[source]#

A node in the expression tree representing an integral operator over the boundary of a domain

\[I = \int_{\partial a}\!f(u)\,du,\]

where \(\partial a\) is the boundary of the domain, and \(u\in\text{domain boundary}\).

Parameters:
  • function (pybamm.Symbol) – The function to be integrated (will become self.children[0])

  • region (str, optional) – The region of the boundary over which to integrate. If region is entire (default) the integration is carried out over the entire boundary. If region is negative tab or positive tab then the integration is only carried out over the appropriate part of the boundary corresponding to the tab.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.DeltaFunction(child, side, domain)[source]#

Delta function. Currently can only be implemented at the edge of a domain.

Parameters:
  • child (pybamm.Symbol) – The variable that sets the strength of the delta function

  • side (str) – Which side of the domain to implement the delta function on

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

evaluate_for_shape()[source]#

See pybamm.Symbol.evaluate_for_shape_using_domain()

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.BoundaryOperator(name, child, side)[source]#

A node in the expression tree which gets the boundary value of a variable on its primary domain.

Parameters:
  • name (str) – The name of the symbol

  • child (pybamm.Symbol) – The variable whose boundary value to take

  • side (str) – Which side to take the boundary value on (“left” or “right”)

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.BoundaryValue(child, side)[source]#

A node in the expression tree which gets the boundary value of a variable on its primary domain.

Parameters:
  • child (pybamm.Symbol) – The variable whose boundary value to take

  • side (str) – Which side to take the boundary value on (“left” or “right”)

Extends: pybamm.expression_tree.unary_operators.BoundaryOperator

class pybamm.BoundaryGradient(child, side)[source]#

A node in the expression tree which gets the boundary flux of a variable on its primary domain.

Parameters:
  • child (pybamm.Symbol) – The variable whose boundary flux to take

  • side (str) – Which side to take the boundary flux on (“left” or “right”)

Extends: pybamm.expression_tree.unary_operators.BoundaryOperator

class pybamm.EvaluateAt(child, position)[source]#

A node in the expression tree which evaluates a symbol at a given position in space in its primary domain. Currently this is only implemented for 1D primary domains.

Parameters:
  • child (pybamm.Symbol) – The variable to evaluate

  • position (pybamm.Symbol) – The position in space on the symbol’s primary domain at which to evaluate the symbol.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

set_id()[source]#

See pybamm.Symbol.set_id()

class pybamm.UpwindDownwind(name, child)[source]#

A node in the expression tree representing an upwinding or downwinding operator. Usually to be used for better stability in convection-dominated equations.

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

class pybamm.Upwind(child)[source]#

Upwinding operator. To be used if flow velocity is positive (left to right).

Extends: pybamm.expression_tree.unary_operators.UpwindDownwind

class pybamm.Downwind(child)[source]#

Downwinding operator. To be used if flow velocity is negative (right to left).

Extends: pybamm.expression_tree.unary_operators.UpwindDownwind

pybamm.grad(symbol)[source]#

convenience function for creating a Gradient

Parameters:

symbol (Symbol) – the gradient will be performed on this sub-symbol

Returns:

the gradient of symbol

Return type:

Gradient

pybamm.div(symbol)[source]#

convenience function for creating a Divergence

Parameters:

symbol (Symbol) – the divergence will be performed on this sub-symbol

Returns:

the divergence of symbol

Return type:

Divergence

pybamm.laplacian(symbol)[source]#

convenience function for creating a Laplacian

Parameters:

symbol (Symbol) – the Laplacian will be performed on this sub-symbol

Returns:

the Laplacian of symbol

Return type:

Laplacian

pybamm.grad_squared(symbol)[source]#

convenience function for creating a GradientSquared

Parameters:

symbol (Symbol) – the inner product of the gradient with itself will be performed on this sub-symbol

Returns:

inner product of the gradient of symbol with itself

Return type:

GradientSquared

pybamm.surf(symbol)[source]#

convenience function for creating a right BoundaryValue, usually in the spherical geometry.

Parameters:

symbol (pybamm.Symbol) – the surface value of this symbol will be returned

Returns:

the surface value of symbol

Return type:

pybamm.BoundaryValue

pybamm.x_average(symbol)[source]#

Convenience function for creating an average in the x-direction.

Parameters:

symbol (pybamm.Symbol) – The function to be averaged

Returns:

the new averaged symbol

Return type:

Symbol

pybamm.r_average(symbol)[source]#

Convenience function for creating an average in the r-direction.

Parameters:

symbol (pybamm.Symbol) – The function to be averaged

Returns:

the new averaged symbol

Return type:

Symbol

pybamm.size_average(symbol, f_a_dist=None)[source]#

Convenience function for averaging over particle size R using the area-weighted particle-size distribution.

Parameters:

symbol (pybamm.Symbol) – The function to be averaged

Returns:

the new averaged symbol

Return type:

Symbol

pybamm.z_average(symbol)[source]#

Convenience function for creating an average in the z-direction.

Parameters:

symbol (pybamm.Symbol) – The function to be averaged

Returns:

the new averaged symbol

Return type:

Symbol

pybamm.yz_average(symbol)[source]#

Convenience function for creating an average in the y-z-direction.

Parameters:

symbol (pybamm.Symbol) – The function to be averaged

Returns:

the new averaged symbol

Return type:

Symbol

pybamm.boundary_value(symbol, side)[source]#

convenience function for creating a pybamm.BoundaryValue

Parameters:
  • symbol (pybamm.Symbol) – The symbol whose boundary value to take

  • side (str) – Which side to take the boundary value on (“left” or “right”)

Returns:

the new integrated expression tree

Return type:

BoundaryValue

pybamm.smooth_absolute_value(symbol, k)[source]#

Smooth approximation to the absolute value function. k is the smoothing parameter, set by pybamm.settings.abs_smoothing. The recommended value is k=10.

pybamm.sign(symbol)[source]#

Returns a Sign object.

pybamm.upwind(symbol)[source]#

convenience function for creating a Upwind

pybamm.downwind(symbol)[source]#

convenience function for creating a Downwind

Concatenations#

class pybamm.Concatenation(*children, name=None, check_domain=True, concat_fun=None)[source]#

A node in the expression tree representing a concatenation of symbols.

Parameters:

children (iterable of pybamm.Symbol) – The symbols to concatenate

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

See pybamm.Symbol.evaluate().

get_children_domains(children)[source]#

Combine domains from children, at all levels.

is_constant()[source]#

See pybamm.Symbol.is_constant().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

class pybamm.NumpyConcatenation(*children)[source]#

A node in the expression tree representing a concatenation of equations, when we don’t care about domains. The class pybamm.DomainConcatenation, which is careful about domains and uses broadcasting where appropriate, should be used whenever possible instead.

Upon evaluation, equations are concatenated using numpy concatenation.

Parameters:

children (iterable of pybamm.Symbol) – The equations to concatenate

Extends: pybamm.expression_tree.concatenations.Concatenation

class pybamm.DomainConcatenation(children, full_mesh, copy_this=None)[source]#

A node in the expression tree representing a concatenation of symbols, being careful about domains.

It is assumed that each child has a domain, and the final concatenated vector will respect the sizes and ordering of domains established in mesh keys

Parameters:
  • children (iterable of pybamm.Symbol) – The symbols to concatenate

  • full_mesh (pybamm.BaseMesh) – The underlying mesh for discretisation, used to obtain the number of mesh points in each domain.

  • copy_this (pybamm.DomainConcatenation (optional)) – if provided, this class is initialised by copying everything except the children from copy_this. mesh is not used in this case

Extends: pybamm.expression_tree.concatenations.Concatenation

to_json()[source]#

Method to serialise a DomainConcatenation object into JSON.

class pybamm.SparseStack(*children)[source]#

A node in the expression tree representing a concatenation of sparse matrices. As with NumpyConcatenation, we don’t care about domains. The class pybamm.DomainConcatenation, which is careful about domains and uses broadcasting where appropriate, should be used whenever possible instead.

Parameters:

children (iterable of Concatenation) – The equations to concatenate

Extends: pybamm.expression_tree.concatenations.Concatenation

pybamm.numpy_concatenation(*children)[source]#

Helper function to create numpy concatenations.

pybamm.domain_concatenation(children, mesh)[source]#

Helper function to create domain concatenations.

Broadcasting Operators#

class pybamm.Broadcast(child, domains, name=None)[source]#

A node in the expression tree representing a broadcasting operator. Broadcasts a child to a specified domain. After discretisation, this will evaluate to an array of the right shape for the specified domain.

For an example of broadcasts in action, see this example notebook

Parameters:
  • child (Symbol) – child node

  • domains (iterable of str) – Domain(s) of the symbol after broadcasting

  • name (str) – name of the node

Extends: pybamm.expression_tree.unary_operators.SpatialOperator

to_json()[source]#

Method to serialise a Symbol object into JSON.

class pybamm.FullBroadcast(child, broadcast_domain=None, auxiliary_domains=None, broadcast_domains=None, name=None)[source]#

A class for full broadcasts.

Extends: pybamm.expression_tree.broadcasts.Broadcast

check_and_set_domains(child, broadcast_domains)[source]#

See Broadcast.check_and_set_domains()

reduce_one_dimension()[source]#

Reduce the broadcast by one dimension.

class pybamm.PrimaryBroadcast(child, broadcast_domain, name=None)[source]#

A node in the expression tree representing a primary broadcasting operator. Broadcasts in a primary dimension only. That is, makes explicit copies of the symbol in the domain specified by broadcast_domain. This should be used for broadcasting from a “larger” scale to a “smaller” scale, for example broadcasting temperature T(x) from the electrode to the particles, or broadcasting current collector current i(y, z) from the current collector to the electrodes.

Parameters:
  • child (Symbol) – child node

  • broadcast_domain (iterable of str) – Primary domain for broadcast. This will become the domain of the symbol

  • name (str) – name of the node

Extends: pybamm.expression_tree.broadcasts.Broadcast

check_and_set_domains(child, broadcast_domain)[source]#

See Broadcast.check_and_set_domains()

reduce_one_dimension()[source]#

Reduce the broadcast by one dimension.

class pybamm.SecondaryBroadcast(child, broadcast_domain, name=None)[source]#

A node in the expression tree representing a secondary broadcasting operator. Broadcasts in a secondary dimension only. That is, makes explicit copies of the symbol in the domain specified by broadcast_domain. This should be used for broadcasting from a “smaller” scale to a “larger” scale, for example broadcasting SPM particle concentrations c_s(r) from the particles to the electrodes. Note that this wouldn’t be used to broadcast particle concentrations in the DFN, since these already depend on both x and r.

Parameters:
  • child (Symbol) – child node

  • broadcast_domain (iterable of str) – Secondary domain for broadcast. This will become the secondary domain of the symbol, shifting the child’s secondary and tertiary (if present) over by one position.

  • name (str) – name of the node

Extends: pybamm.expression_tree.broadcasts.Broadcast

check_and_set_domains(child, broadcast_domain)[source]#

See Broadcast.check_and_set_domains()

reduce_one_dimension()[source]#

Reduce the broadcast by one dimension.

class pybamm.FullBroadcastToEdges(child, broadcast_domain=None, auxiliary_domains=None, broadcast_domains=None, name=None)[source]#

A full broadcast onto the edges of a domain (edges of primary dimension, nodes of other dimensions)

Extends: pybamm.expression_tree.broadcasts.FullBroadcast

reduce_one_dimension()[source]#

Reduce the broadcast by one dimension.

class pybamm.PrimaryBroadcastToEdges(child, broadcast_domain, name=None)[source]#

A primary broadcast onto the edges of the domain.

Extends: pybamm.expression_tree.broadcasts.PrimaryBroadcast

class pybamm.SecondaryBroadcastToEdges(child, broadcast_domain, name=None)[source]#

A secondary broadcast onto the edges of a domain.

Extends: pybamm.expression_tree.broadcasts.SecondaryBroadcast

pybamm.ones_like(*symbols)[source]#

Returns an array with the same shape and domains as the sum of the input symbols, with each entry equal to one.

Parameters:

symbols (Symbol) – Symbols whose shape to copy

pybamm.zeros_like(*symbols)[source]#

Returns an array with the same shape and domains as the sum of the input symbols, with each entry equal to zero.

Parameters:

symbols (Symbol) – Symbols whose shape to copy

pybamm.full_like(symbols, fill_value)[source]#

Returns an array with the same shape and domains as the sum of the input symbols, with a constant value given by fill_value.

Parameters:
  • symbols (Symbol) – Symbols whose shape to copy

  • fill_value (number) – Value to assign

Functions#

class pybamm.Function(function, *children, name=None, derivative='autograd', differentiated_function=None)[source]#

A node in the expression tree representing an arbitrary function.

Parameters:
  • function (method) – A function can have 0 or many inputs. If no inputs are given, self.evaluate() simply returns func(). Otherwise, self.evaluate(t, y, u) returns func(child0.evaluate(t, y, u), child1.evaluate(t, y, u), etc).

  • children (pybamm.Symbol) – The children nodes to apply the function to

  • derivative (str, optional) – Which derivative to use when differentiating (“autograd” or “derivative”). Default is “autograd”.

  • differentiated_function (method, optional) – The function which was differentiated to obtain this one. Default is None.

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

diff(variable)[source]#

See pybamm.Symbol.diff().

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

See pybamm.Symbol.evaluate().

is_constant()[source]#

See pybamm.Symbol.is_constant().

to_equation()[source]#

Convert the node and its subtree into a SymPy equation.

to_json()[source]#

Method to serialise a Symbol object into JSON.

class pybamm.SpecificFunction(function, child)[source]#

Parent class for the specific functions, which implement their own diff operators directly.

Parameters:
  • function (method) – Function to be applied to child

  • child (pybamm.Symbol) – The child to apply the function to

Extends: pybamm.expression_tree.functions.Function

to_json()[source]#

Method to serialise a SpecificFunction object into JSON.

class pybamm.Arcsinh(child)[source]#

Arcsinh function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.arcsinh(child)[source]#

Returns arcsinh function of child.

class pybamm.Arctan(child)[source]#

Arctan function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.arctan(child)[source]#

Returns hyperbolic tan function of child.

class pybamm.Cos(child)[source]#

Cosine function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.cos(child)[source]#

Returns cosine function of child.

class pybamm.Cosh(child)[source]#

Hyberbolic cosine function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.cosh(child)[source]#

Returns hyperbolic cosine function of child.

class pybamm.Erf(child)[source]#

Error function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.erf(child)[source]#

Returns error function of child.

pybamm.erfc(child)[source]#

Returns complementary error function of child.

class pybamm.Exp(child)[source]#

Exponential function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.exp(child)[source]#

Returns exponential function of child.

class pybamm.Log(child)[source]#

Logarithmic function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.log(child, base='e')[source]#

Returns logarithmic function of child (any base, default ‘e’).

pybamm.log10(child)[source]#

Returns logarithmic function of child, with base 10.

class pybamm.Max(child)[source]#

Max function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.max(child)[source]#

Returns max function of child. Not to be confused with pybamm.maximum(), which returns the larger of two objects.

class pybamm.Min(child)[source]#

Min function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.min(child)[source]#

Returns min function of child. Not to be confused with pybamm.minimum(), which returns the smaller of two objects.

pybamm.sech(child)[source]#

Returns hyperbolic sec function of child.

class pybamm.Sin(child)[source]#

Sine function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.sin(child)[source]#

Returns sine function of child.

class pybamm.Sinh(child)[source]#

Hyperbolic sine function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.sinh(child)[source]#

Returns hyperbolic sine function of child.

class pybamm.Sqrt(child)[source]#

Square root function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.sqrt(child)[source]#

Returns square root function of child.

class pybamm.Tanh(child)[source]#

Hyperbolic tan function.

Extends: pybamm.expression_tree.functions.SpecificFunction

pybamm.tanh(child)[source]#

Returns hyperbolic tan function of child.

Input Parameter#

class pybamm.InputParameter(name, domain=None, expected_size=None)[source]#

A node in the expression tree representing an input parameter.

This node’s value can be set at the point of solving, allowing parameter estimation and control

Parameters:
  • name (str) – name of the node

  • domain (iterable of str, or str) – list of domains over which the node is valid (empty list indicates the symbol is valid over all domains)

  • expected_size (int) – The size of the input parameter expected, defaults to 1 (scalar input)

Extends: pybamm.expression_tree.symbol.Symbol

create_copy()[source]#

See pybamm.Symbol.new_copy().

to_json()[source]#

Method to serialise an InputParameter object into JSON.

Interpolant#

class pybamm.Interpolant(x, y, children, name=None, interpolator='linear', extrapolate=True, entries_string=None)[source]#

Interpolate data in 1D, 2D, or 3D. Interpolation in 3D requires the input data to be on a regular grid (as per scipy.interpolate.RegularGridInterpolator).

Parameters:
  • x (iterable of numpy.ndarray) – The data point coordinates. If 1-D, then this is an array(s) of real values. If, 2D or 3D interpolation, then this is to ba a tuple of 1D arrays (one for each dimension) which together define the coordinates of the points.

  • y (numpy.ndarray) – The values of the function to interpolate at the data points. In 2D and 3D, this should be a matrix of two and three dimensions respectively.

  • children (iterable of pybamm.Symbol) – Node(s) to use when evaluating the interpolant. Each child corresponds to an entry of x

  • name (str, optional) – Name of the interpolant. Default is None, in which case the name “interpolating function” is given.

  • interpolator (str, optional) – Which interpolator to use. Can be “linear”, “cubic”, or “pchip”. Default is “linear”. For 3D interpolation, only “linear” an “cubic” are currently supported.

  • extrapolate (bool, optional) – Whether to extrapolate for points that are outside of the parametrisation range, or return NaN (following default behaviour from scipy). Default is True. Generally, it is best to set this to be False for 3D interpolation due to the higher potential for errors in extrapolation.

Extends: pybamm.expression_tree.functions.Function

set_id()[source]#

See pybamm.Symbol.set_id().

to_json()[source]#

Method to serialise an Interpolant object into JSON.

Operations on expression trees#

Classes and functions that operate on the expression tree

EvaluatorPython#
class pybamm.EvaluatorPython(symbol)[source]#

Converts a pybamm expression tree into pure python code that will calculate the result of calling evaluate(t, y) on the given expression tree.

Parameters:

symbol (pybamm.Symbol) – The symbol to convert to python code

Jacobian#
class pybamm.Jacobian(known_jacs=None, clear_domain=True)[source]#

Helper class to calculate the Jacobian of an expression.

Parameters:
  • known_jacs (dict {variable ids -> pybamm.Symbol}) – cached jacobians

  • clear_domain (bool) – whether or not the Jacobian clears the domain (default True)

jac(symbol, variable)[source]#

This function recurses down the tree, computing the Jacobian using the Jacobians defined in classes derived from pybamm.Symbol. E.g. the Jacobian of a ‘pybamm.Multiplication’ is computed via the product rule. If the Jacobian of a symbol has already been calculated, the stored value is returned. Note: The Jacobian is the derivative of a symbol with respect to a (slice of) a State Vector.

Parameters:
  • symbol (pybamm.Symbol) – The symbol to calculate the Jacobian of

  • variable (pybamm.Symbol) – The variable with respect to which to differentiate

Returns:

Symbol representing the Jacobian

Return type:

pybamm.Symbol

Convert to CasADi#
class pybamm.CasadiConverter(casadi_symbols=None)[source]#
convert(symbol, t, y, y_dot, inputs)[source]#

This function recurses down the tree, converting the PyBaMM expression tree to a CasADi expression tree

Parameters:
  • symbol (pybamm.Symbol) – The symbol to convert

  • t (casadi.MX) – A casadi symbol representing time

  • y (casadi.MX) – A casadi symbol representing state vectors

  • y_dot (casadi.MX) – A casadi symbol representing time derivatives of state vectors

  • inputs (dict) – A dictionary of casadi symbols representing parameters

Returns:

The converted symbol

Return type:

casadi.MX

Serialise#
class pybamm.expression_tree.operations.serialise.Serialise[source]#

Converts a discretised model to and from a JSON file.

load_model(filename: str, battery_model: BaseModel | None = None) BaseModel[source]#

Loads a discretised, ready to solve model into PyBaMM.

A new pybamm battery model instance will be created, which can be solved and the results plotted as usual.

Currently only available for pybamm models which have previously been written out using the save_model() option.

Warning: This only loads in discretised models. If you wish to make edits to the model or initial conditions, a new model will need to be constructed seperately.

Parameters:
  • filename (str) – Path to the JSON file containing the serialised model file

  • battery_model (pybamm.BaseModel (optional)) – PyBaMM model to be created (e.g. pybamm.lithium_ion.SPM), which will override any model names within the file. If None, the function will look for the saved object path, present if the original model came from PyBaMM.

Returns:

A PyBaMM model object, of type specified either in the JSON or in battery_model.

Return type:

pybamm.BaseModel

save_model(model: BaseModel, mesh: Mesh | None = None, variables: FuzzyDict | None = None, filename: str | None = None)[source]#

Saves a discretised model to a JSON file.

As the model is discretised and ready to solve, only the right hand side, algebraic and initial condition variables are saved.

Parameters:
  • model (pybamm.BaseModel) – The discretised model to be saved

  • mesh (pybamm.Mesh (optional)) – The mesh the model has been discretised over. Not neccesary to solve the model when read in, but required to use pybamm’s plotting tools.

  • variables (pybamm.FuzzyDict (optional)) – The discretised model varaibles. Not necessary to solve a model, but required to use pybamm’s plotting tools.

  • filename (str (optional)) – The desired name of the JSON file. If no name is provided, one will be created based on the model name, and the current datetime.

Symbol Unpacker#
class pybamm.SymbolUnpacker(classes_to_find, unpacked_symbols=None)[source]#

Helper class to unpack a (set of) symbol(s) to find all instances of a class. Uses caching to speed up the process.

Parameters:
  • classes_to_find (list of pybamm classes) – Classes to identify in the equations

  • unpacked_symbols (set, optional) – cached unpacked equations

unpack_list_of_symbols(list_of_symbols)[source]#

Unpack a list of symbols. See SymbolUnpacker.unpack()

Parameters:

list_of_symbols (list of pybamm.Symbol) – List of symbols to unpack

Returns:

Set of unpacked symbols with class in self.classes_to_find

Return type:

list of pybamm.Symbol

unpack_symbol(symbol)[source]#

This function recurses down the tree, unpacking the symbols and saving the ones that have a class in self.classes_to_find.

Parameters:

symbol (list of pybamm.Symbol) – The symbols to unpack

Returns:

List of unpacked symbols with class in self.classes_to_find

Return type:

list of pybamm.Symbol

Models#

Below is an overview of all the battery models included in PyBaMM. Each of the pre-built models contains a reference to the paper in which it is derived.

The models can be customised using the options dictionary defined in the pybamm.BaseBatteryModel (which also provides information on which options and models are compatible) Visit our examples page to see how these models can be solved, and compared, using PyBaMM.

Base Models#

Base Model#
class pybamm.BaseModel(name='Unnamed model')[source]#

Base model class for other models to extend.

name#

A string giving the name of the model.

Type:

str

options#

A dictionary of options to be passed to the model.

Type:

dict

submodels#

A dictionary of submodels that the model is composed of.

Type:

dict

rhs#

A dictionary that maps expressions (variables) to expressions that represent the rhs.

Type:

dict

algebraic#

A dictionary that maps expressions (variables) to expressions that represent the algebraic equations. The algebraic expressions are assumed to equate to zero. Note that all the variables in the model must exist in the keys of rhs or algebraic.

Type:

dict

initial_conditions#

A dictionary that maps expressions (variables) to expressions that represent the initial conditions for the state variables y. The initial conditions for algebraic variables are provided as initial guesses to a root finding algorithm that calculates consistent initial conditions.

Type:

dict

boundary_conditions#

A dictionary that maps expressions (variables) to expressions that represent the boundary conditions.

Type:

dict

variables#

A dictionary that maps strings to expressions that represent the useful variables.

Type:

dict

events#

A list of events. Each event can either cause the solver to terminate (e.g. concentration goes negative), or be used to inform the solver of the existance of a discontinuity (e.g. discontinuity in the input current).

Type:

list of pybamm.Event

concatenated_rhs#

After discretisation, contains the expressions representing the rhs equations concatenated into a single expression.

Type:

pybamm.Concatenation

concatenated_algebraic#

After discretisation, contains the expressions representing the algebraic equations concatenated into a single expression.

Type:

pybamm.Concatenation

concatenated_initial_conditions#

After discretisation, contains the vector of initial conditions.

Type:

numpy.array

mass_matrix#

After discretisation, contains the mass matrix for the model. This is computed automatically.

Type:

pybamm.Matrix

mass_matrix_inv#

After discretisation, contains the inverse mass matrix for the differential (rhs) part of model. This is computed automatically.

Type:

pybamm.Matrix

jacobian#

Contains the Jacobian for the model. If model.use_jacobian is True, the Jacobian is computed automatically during solver set up.

Type:

pybamm.Concatenation

jacobian_rhs#

Contains the Jacobian for the part of the model which contains time derivatives. If model.use_jacobian is True, the Jacobian is computed automatically during solver set up.

Type:

pybamm.Concatenation

jacobian_algebraic#

Contains the Jacobian for the algebraic part of the model. This may be used by the solver when calculating consistent initial conditions. If model.use_jacobian is True, the Jacobian is computed automatically during solver set up.

Type:

pybamm.Concatenation

use_jacobian#

Whether to use the Jacobian when solving the model (default is True).

Type:

bool

convert_to_format#

Whether to convert the expression trees representing the rhs and algebraic equations, Jacobain (if using) and events into a different format:

  • 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.

Default is “casadi”.

Type:

str

check_algebraic_equations(post_discretisation)[source]#

Check that the algebraic equations are well-posed. After discretisation, there must be at least one StateVector in each algebraic equation.

check_discretised_or_discretise_inplace_if_0D()[source]#

Discretise model if it isn’t already discretised This only works with purely 0D models, as otherwise the mesh and spatial method should be specified by the user

check_ics_bcs()[source]#

Check that the initial and boundary conditions are well-posed.

check_no_repeated_keys()[source]#

Check that no equation keys are repeated.

check_well_determined(post_discretisation)[source]#

Check that the model is not under- or over-determined.

check_well_posedness(post_discretisation=False)[source]#

Check that the model is well-posed by executing the following tests: - Model is not over- or underdetermined, by comparing keys and equations in rhs and algebraic. Overdetermined if more equations than variables, underdetermined if more variables than equations. - There is an initial condition in self.initial_conditions for each variable/equation pair in self.rhs - There are appropriate boundary conditions in self.boundary_conditions for each variable/equation pair in self.rhs and self.algebraic

Parameters:

post_discretisation (boolean) – A flag indicating tests to be skipped after discretisation

property default_solver#

Return default solver based on whether model is ODE/DAE or algebraic

classmethod deserialise(properties: dict)[source]#

Create a model instance from a serialised object.

export_casadi_objects(variable_names, input_parameter_order=None)[source]#

Export the constituent parts of the model (rhs, algebraic, initial conditions, etc) as casadi objects.

Parameters:
  • variable_names (list) – Variables to be exported alongside the model structure

  • input_parameter_order (list, optional) – Order in which the input parameters should be stacked. If input_parameter_order=None and len(self.input_parameters) > 1, a ValueError is raised (this helps to avoid accidentally using the wrong order)

Returns:

casadi_dict – Dictionary of {str: casadi object} pairs representing the model in casadi format

Return type:

dict

generate(filename, variable_names, input_parameter_order=None, cg_options=None)[source]#

Generate the model in C, using CasADi.

Parameters:
  • filename (str) – Name of the file to which to save the code

  • variable_names (list) – Variables to be exported alongside the model structure

  • input_parameter_order (list, optional) – Order in which the input parameters should be stacked. If input_parameter_order=None and len(self.input_parameters) > 1, a ValueError is raised (this helps to avoid accidentally using the wrong order)

  • cg_options (dict) – Options to pass to the code generator. See https://web.casadi.org/docs/#generating-c-code

get_parameter_info()[source]#

Extracts the parameter information and returns it as a dictionary. To get a list of all parameter-like objects without extra information, use model.parameters.

info(symbol_name)[source]#

Provides helpful summary information for a symbol.

Parameters:

parameter_name (str) –

property input_parameters#

Returns all the input parameters in the model

latexify(filename=None, newline=True, output_variables=None)[source]#

Converts all model equations in latex.

Parameters:
  • filename (str (optional)) – Accepted file formats - any image format, pdf and tex Default is None, When None returns all model equations in latex If not None, returns all model equations in given file format.

  • newline (bool (optional)) – Default is True, If True, returns every equation in a new line. If False, returns the list of all the equations.

  • model (Load) –

  • pybamm.lithium_ion.SPM() (>>> model =) –

  • png (This will returns all model equations in) –

  • model.latexify("equations.png") (>>>) –

  • latex (This will return all the model equations in) –

  • model.latexify() (>>>) –

  • equations (This will return first five model) –

  • model.latexify(newline=False) (>>>) –

  • equations

  • model.latexify(newline=False)[1 (>>>) –

new_copy()[source]#

Creates a copy of the model, explicitly copying all the mutable attributes to avoid issues with shared objects.

property parameters#

Returns all the parameters in the model

print_parameter_info()[source]#

Print parameter information in a formatted table from a dictionary of parameters

process_parameters_and_discretise(symbol, parameter_values, disc)[source]#

Process parameters and discretise a symbol using supplied parameter values and discretisation. Note: care should be taken if using spatial operators on dimensional symbols. Operators in pybamm are written in non-dimensional form, so may need to be scaled by the appropriate length scale. It is recommended to use this method on non-dimensional symbols.

Parameters:
Returns:

Processed symbol

Return type:

pybamm.Symbol

save_model(filename=None, mesh=None, variables=None)[source]#

Write out a discretised model to a JSON file

Parameters:
  • filename (str, optional) –

  • provided (The desired name of the JSON file. If no name is) –

  • created (one will be) –

  • name (based on the model) –

  • datetime. (and the current) –

set_initial_conditions_from(solution, inplace=True, return_type='model')[source]#

Update initial conditions with the final states from a Solution object or from a dictionary. This assumes that, for each variable in self.initial_conditions, there is a corresponding variable in the solution with the same name and size.

Parameters:
  • solution (pybamm.Solution, or dict) – The solution to use to initialize the model

  • inplace (bool, optional) – Whether to modify the model inplace or create a new model (default True)

  • return_type (str, optional) – Whether to return the model (default) or initial conditions (“ics”)

update(*submodels)[source]#

Update model to add new physics from submodels

Parameters:

submodel (iterable of pybamm.BaseModel) – The submodels from which to create new model

property variables_and_events#

Returns variables and events in a single dictionary

Base Battery Model#
class pybamm.BaseBatteryModel(options=None, name='Unnamed battery model')[source]#

Base model class with some default settings and required variables

Parameters:
  • options (dict-like, optional) – A dictionary of options to be passed to the model. If this is a dict (and not a subtype of dict), it will be processed by pybamm.BatteryModelOptions to ensure that the options are valid. If this is a subtype of dict, it is assumed that the options have already been processed and are valid. This allows for the use of custom options classes. The default options are given by pybamm.BatteryModelOptions.

  • name (str, optional) – The name of the model. The default is “Unnamed battery model”.

Extends: pybamm.models.base_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.base_battery_model.BaseBatteryModel

classmethod deserialise(properties: dict)[source]#

Create a model instance from a serialised object.

save_model(filename=None, mesh=None, variables=None)[source]#

Write out a discretised model to a JSON file

Parameters:
  • filename (str, optional) –

  • provided (The desired name of the JSON file. If no name is) –

  • created (one will be) –

  • name (based on the model) –

  • datetime. (and the current) –

set_degradation_variables()[source]#

Set variables that quantify degradation. This function is overriden by the base battery models

set_external_circuit_submodel()[source]#

Define how the external circuit defines the boundary conditions for the model, e.g. (not necessarily constant-) current, voltage, etc

set_soc_variables()[source]#

Set variables relating to the state of charge. This function is overriden by the base battery models

class pybamm.BatteryModelOptions(extra_options)[source]#
options#

A dictionary of options to be passed to the model. The options that can be set are listed below. Note that not all of the options are compatible with each other and with all of the models implemented in PyBaMM. Each option is optional and takes a default value if not provided. In general, the option provided must be a string, but there are some cases where a 2-tuple of strings can be provided instead to indicate a different option for the negative and positive electrodes.

  • “calculate discharge energy”: str

    Whether to calculate the discharge energy, throughput energy and throughput capacity in addition to discharge capacity. Must be one of “true” or “false”. “false” is the default, since calculating discharge energy can be computationally expensive for simple models like SPM.

  • “cell geometry”str

    Sets the geometry of the cell. Can be “arbitrary” (default) or “pouch”. The arbitrary geometry option solves a 1D electrochemical model with prescribed cell volume and cross-sectional area, and (if thermal effects are included) solves a lumped thermal model with prescribed surface area for cooling.

  • “calculate heat source for isothermal models”str

    Whether to calculate the heat source terms during isothermal operation. Can be “true” or “false”. If “false”, the heat source terms are set to zero. Default is “false” since this option may require additional parameters not needed by the electrochemical model.

  • “convection”str

    Whether to include the effects of convection in the model. Can be “none” (default), “uniform transverse” or “full transverse”. Must be “none” for lithium-ion models.

  • “current collector”str

    Sets the current collector model to use. Can be “uniform” (default), “potential pair” or “potential pair quite conductive”.

  • “diffusivity”str

    Sets the model for the diffusivity. Can be “single” (default) or “current sigmoid”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “dimensionality”int

    Sets the dimension of the current collector problem. Can be 0 (default), 1 or 2.

  • “electrolyte conductivity”str

    Can be “default” (default), “full”, “leading order”, “composite” or “integrated”.

  • “exchange-current density”str

    Sets the model for the exchange-current density. Can be “single” (default) or “current sigmoid”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “hydrolysis”str

    Whether to include hydrolysis in the model. Only implemented for lead-acid models. Can be “false” (default) or “true”. If “true”, then “surface form” cannot be ‘false’.

  • “intercalation kinetics”str

    Model for intercalation kinetics. Can be “symmetric Butler-Volmer” (default), “asymmetric Butler-Volmer”, “linear”, “Marcus”, “Marcus-Hush-Chidsey” (which uses the asymptotic form from Zeng 2014), or “MSMR” (which uses the form from Baker 2018). A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “interface utilisation”: str

    Can be “full” (default), “constant”, or “current-driven”.

  • “lithium plating”str

    Sets the model for lithium plating. Can be “none” (default), “reversible”, “partially reversible”, or “irreversible”.

  • “lithium plating porosity change”str

    Whether to include porosity change due to lithium plating, can be “false” (default) or “true”.

  • “loss of active material”str

    Sets the model for loss of active material. Can be “none” (default), “stress-driven”, “reaction-driven”, “current-driven”, or “stress and reaction-driven”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “number of MSMR reactions”str

    Sets the number of reactions to use in the MSMR model in each electrode. A 2-tuple can be provided to give a different number of reactions in the negative and positive electrodes. Default is “none”. Can be any 2-tuple of strings of integers. For example, set to (“6”, “4”) for a negative electrode with 6 reactions and a positive electrode with 4 reactions.

  • “open-circuit potential”str

    Sets the model for the open circuit potential. Can be “single” (default), “current sigmoid”, or “MSMR”. If “MSMR” then the “particle” option must also be “MSMR”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “operating mode”str

    Sets the operating mode for the model. This determines how the current is set. Can be:

    • “current” (default) : the current is explicity supplied

    • “voltage”/”power”/”resistance” : solve an algebraic equation for current such that voltage/power/resistance is correct

    • “differential power”/”differential resistance” : solve a differential equation for the power or resistance

    • “explicit power”/”explicit resistance” : current is defined in terms of the voltage such that power/resistance is correct

    • “CCCV”: a special implementation of the common constant-current constant-voltage charging protocol, via an ODE for the current

    • callable : if a callable is given as this option, the function defines the residual of an algebraic equation. The applied current will be solved for such that the algebraic constraint is satisfied.

  • “particle”str

    Sets the submodel to use to describe behaviour within the particle. Can be “Fickian diffusion” (default), “uniform profile”, “quadratic profile”, “quartic profile”, or “MSMR”. If “MSMR” then the “open-circuit potential” option must also be “MSMR”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “particle mechanics”str

    Sets the model to account for mechanical effects such as particle swelling and cracking. Can be “none” (default), “swelling only”, or “swelling and cracking”. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “particle phases”: str

    Number of phases present in the electrode. A 2-tuple can be provided for different behaviour in negative and positive electrodes. For example, set to (“2”, “1”) for a negative electrode with 2 phases, e.g. graphite and silicon.

  • “particle shape”str

    Sets the model shape of the electrode particles. This is used to calculate the surface area to volume ratio. Can be “spherical” (default), or “no particles”.

  • “particle size”str

    Sets the model to include a single active particle size or a distribution of sizes at any macroscale location. Can be “single” (default) or “distribution”. Option applies to both electrodes.

  • “SEI”str

    Set the SEI submodel to be used. Options are:

    • “none”: pybamm.sei.NoSEI (no SEI growth)

    • “constant”: pybamm.sei.Constant (constant SEI thickness)

    • “reaction limited”, “reaction limited (asymmetric)”, “solvent-diffusion limited”, “electron-migration limited”, “interstitial-diffusion limited”, “ec reaction limited” or “ec reaction limited (asymmetric)”: pybamm.sei.SEIGrowth

  • “SEI film resistance”str

    Set the submodel for additional term in the overpotential due to SEI. The default value is “none” if the “SEI” option is “none”, and “distributed” otherwise. This is because the “distributed” model is more complex than the model with no additional resistance, which adds unnecessary complexity if there is no SEI in the first place

    • “none”: no additional resistance
      \[\eta_r = \frac{F}{RT} * (\phi_s - \phi_e - U)\]
    • “distributed”: properly included additional resistance term
      \[\eta_r = \frac{F}{RT} * (\phi_s - \phi_e - U - R_{sei} * L_{sei} * j)\]
    • “average”: constant additional resistance term (approximation to the true model). This model can give similar results to the “distributed” case without needing to make j an algebraic state
      \[\eta_r = \frac{F}{RT} * (\phi_s - \phi_e - U - R_{sei} * L_{sei} * \frac{I}{aL})\]
  • “SEI on cracks”str

    Whether to include SEI growth on particle cracks, can be “false” (default) or “true”.

  • “SEI porosity change”str

    Whether to include porosity change due to SEI formation, can be “false” (default) or “true”.

  • “stress-induced diffusion”str

    Whether to include stress-induced diffusion, can be “false” or “true”. The default is “false” if “particle mechanics” is “none” and “true” otherwise. A 2-tuple can be provided for different behaviour in negative and positive electrodes.

  • “surface form”str

    Whether to use the surface formulation of the problem. Can be “false” (default), “differential” or “algebraic”.

  • “thermal”str

    Sets the thermal model to use. Can be “isothermal” (default), “lumped”, “x-lumped”, or “x-full”. The ‘cell geometry’ option must be set to ‘pouch’ for ‘x-lumped’ or ‘x-full’ to be valid. Using the ‘x-lumped’ option with ‘dimensionality’ set to 0 is equivalent to using the ‘lumped’ option.

  • “total interfacial current density as a state”str

    Whether to make a state for the total interfacial current density and solve an algebraic equation for it. Default is “false”, unless “SEI film resistance” is distributed in which case it is automatically set to “true”.

  • “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 counter electrode.

  • “x-average side reactions”: str

    Whether to average the side reactions (SEI growth, lithium plating and the respective porosity change) over the x-axis in Single Particle Models, can be “false” or “true”. Default is “false” for SPMe and “true” for SPM.

Type:

dict

Extends: pybamm.util.FuzzyDict

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.base_battery_model.BatteryModelOptions

property negative#

Returns the options for the negative electrode

property positive#

Returns the options for the positive electrode

print_detailed_options()[source]#

Print the docstring for Options

print_options()[source]#

Print the possible options with the ones currently selected

Event#
class pybamm.Event(name, expression, event_type=EventType.TERMINATION)[source]#

Defines an event for use within a pybamm model

name#

A string giving the name of the event.

Type:

str

expression#

An expression that defines when the event occurs.

Type:

pybamm.Symbol

event_type#

An enum defining the type of event. By default it is set to TERMINATION.

Type:

pybamm.EventType (optional)

evaluate(t=None, y=None, y_dot=None, inputs=None)[source]#

Acts as a drop-in replacement for pybamm.Symbol.evaluate()

to_json()[source]#

Method to serialise an Event object into JSON.

The expression is written out seperately, See pybamm.Serialise._SymbolEncoder.default()

class pybamm.EventType(value, names=None, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]#

Defines the type of event, see pybamm.Event

TERMINATION indicates an event that will terminate the solver, the expression should return 0 when the event is triggered

DISCONTINUITY indicates an expected discontinuity in the solution, the expression should return the time that the discontinuity occurs. The solver will integrate up to the discontinuity and then restart just after the discontinuity.

INTERPOLANT_EXTRAPOLATION indicates that a pybamm.Interpolant object has been evaluated outside of the range.

SWITCH indicates an event switch that is used in CasADI “fast with events” model.

Extends: enum.Enum

View inheritance diagram for this model

Inheritance diagram of pybamm.models.event.EventType

Lithium-ion Models#

Base Lithium-ion Model#
class pybamm.lithium_ion.BaseModel(options=None, name='Unnamed lithium-ion model', build=False)#

Overwrites default parameters from Base Model with default parameters for lithium-ion models

Parameters:
  • options (dict-like, optional) – A dictionary of options to be passed to the model. If this is a dict (and not a subtype of dict), it will be processed by pybamm.BatteryModelOptions to ensure that the options are valid. If this is a subtype of dict, it is assumed that the options have already been processed and are valid. This allows for the use of custom options classes. The default options are given by pybamm.BatteryModelOptions.

  • name (str, optional) – The name of the model. The default is “Unnamed battery model”.

  • build (bool, optional) – Whether to build the model on instantiation. Default is True. Setting this option to False allows users to change any number of the submodels before building the complete model (submodels cannot be changed after the model is built).

Extends: pybamm.models.full_battery_models.base_battery_model.BaseBatteryModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

insert_reference_electrode(position=None)#

Insert a reference electrode to measure the electrolyte potential at a given position in space. Adds model variables for the electrolyte potential at the reference electrode and for the potential difference between the electrode potentials measured at the electrode/current collector interface and the reference electrode. Only implemented for 1D models (i.e. where the ‘dimensionality’ option is 0).

Parameters:

position (pybamm.Symbol, optional) – The position in space at which to measure the electrolyte potential. If None, defaults to the mid-point of the separator.

set_degradation_variables()#

Sets variables that quantify degradation (LAM, LLI, etc)

set_summary_variables()#

Sets the default summary variables.

Single Particle Model (SPM)#
class pybamm.lithium_ion.SPM(options=None, name='Single Particle Model', build=True)#

Single Particle Model (SPM) of a lithium-ion battery, from Marquis et al.[1]. See pybamm.lithium_ion.BaseModel for more details.

Examples

>>> model = pybamm.lithium_ion.SPM()
>>> model.name
'Single Particle Model'

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.spm.SPM

class pybamm.lithium_ion.BasicSPM(name='Single Particle Model')#

Single Particle Model (SPM) model of a lithium-ion battery, from Marquis et al.[1].

This class differs from the pybamm.lithium_ion.SPM model class in that it shows the whole model in a single class. This comes at the cost of flexibility in combining different physical effects, and in general the main SPM class should be used instead.

Parameters:

name (str, optional) – The name of the model.

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.basic_spm.BasicSPM

References

Single Particle Model with Electrolyte (SPMe)#
class pybamm.lithium_ion.SPMe(options=None, name='Single Particle Model with electrolyte', build=True)#

Single Particle Model with Electrolyte (SPMe) of a lithium-ion battery, from Marquis et al.[1]. Inherits most submodels from SPM, only modifies potentials and electrolyte. See pybamm.lithium_ion.BaseModel for more details.

Examples

>>> model = pybamm.lithium_ion.SPMe()
>>> model.name
'Single Particle Model with electrolyte'

Extends: pybamm.models.full_battery_models.lithium_ion.spm.SPM

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.spme.SPMe

References

Many Particle Model (MPM)#
class pybamm.lithium_ion.MPM(options=None, name='Many-Particle Model', build=True)#

Many-Particle Model (MPM) of a lithium-ion battery with particle-size distributions for each electrode, from Kirk et al.[1]. See pybamm.lithium_ion.BaseModel for more details.

Examples

>>> model = pybamm.lithium_ion.MPM()
>>> model.name
'Many-Particle Model'

Extends: pybamm.models.full_battery_models.lithium_ion.spm.SPM

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.mpm.MPM

References

Doyle-Fuller-Newman (DFN)#
class pybamm.lithium_ion.DFN(options=None, name='Doyle-Fuller-Newman model', build=True)#

Doyle-Fuller-Newman (DFN) model of a lithium-ion battery, from Marquis et al.[1]. See pybamm.lithium_ion.BaseModel for more details.

Examples

>>> model = pybamm.lithium_ion.DFN()
>>> model.name
'Doyle-Fuller-Newman model'

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.dfn.DFN

class pybamm.lithium_ion.BasicDFN(name='Doyle-Fuller-Newman model')#

Doyle-Fuller-Newman (DFN) model of a lithium-ion battery, from Marquis et al.[1].

This class differs from the pybamm.lithium_ion.DFN model class in that it shows the whole model in a single class. This comes at the cost of flexibility in comparing different physical effects, and in general the main DFN class should be used instead.

Parameters:

name (str, optional) – The name of the model.

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.basic_dfn.BasicDFN

class pybamm.lithium_ion.BasicDFNComposite(name='Composite graphite/silicon Doyle-Fuller-Newman model')#

Doyle-Fuller-Newman (DFN) model of a lithium-ion battery with composite particles of graphite and silicon, from Ai et al.[2].

This class differs from the pybamm.lithium_ion.DFN model class in that it shows the whole model in a single class. This comes at the cost of flexibility in comparing different physical effects, and in general the main DFN class should be used instead.

Parameters:

name (str, optional) – The name of the model.

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.basic_dfn_composite.BasicDFNComposite

class pybamm.lithium_ion.BasicDFNHalfCell(options=None, name='Doyle-Fuller-Newman half cell model')#

Doyle-Fuller-Newman (DFN) model of a lithium-ion battery with lithium counter electrode, adapted from Doyle et al.[3].

This class differs from the pybamm.lithium_ion.BasicDFN model class in that it is for a cell with a lithium counter electrode (half cell). This is a feature under development (for example, it cannot be used with the Experiment class for the moment) and in the future it will be incorporated as a standard model with the full functionality.

The electrode labeled “positive electrode” is the working electrode, and the electrode labeled “negative electrode” is the counter electrode. This facilitates compatibility with the full-cell models.

Parameters:
  • options (dict) – A dictionary of options to be passed to the model. For the half cell it should include which is the working electrode.

  • name (str, optional) – The name of the model.

Extends: pybamm.models.full_battery_models.lithium_ion.base_lithium_ion_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.basic_dfn_half_cell.BasicDFNHalfCell

References

Newman-Tobias#
class pybamm.lithium_ion.NewmanTobias(options=None, name='Newman-Tobias model', build=True)#

Newman-Tobias model of a lithium-ion battery based on the formulation in Newman and Tobias[1]. This model assumes a uniform concentration profile in the electrolyte. Unlike the model posed in Newman and Tobias[1], this model accounts for nonlinear Butler-Volmer kinetics. It also tracks the average concentration in the solid phase in each electrode, which is equivalent to including an equation for the local state of charge as in Chu et al.[2]. The user can pass the “particle” option to include mass transport in the particles.

See pybamm.lithium_ion.BaseModel for more details.

Extends: pybamm.models.full_battery_models.lithium_ion.dfn.DFN

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.newman_tobias.NewmanTobias

References

Multi-Species Multi-Reaction (MSMR) Model#
class pybamm.lithium_ion.MSMR(options=None, name='MSMR', build=True)#
View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.msmr.MSMR

Yang et al 2017#
class pybamm.lithium_ion.Yang2017(options=None, name='Yang2017', build=True)#
View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lithium_ion.Yang2017.Yang2017

Electrode SOH models#
class pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param=None, known_value='cyclable lithium capacity', options=None)#

Class used to check if the electrode SOH model is feasible, and solve it if it is.

Parameters:
  • parameter_values (pybamm.ParameterValues.Parameters) – The parameters of the simulation

  • param (pybamm.LithiumIonParameters, optional) – Specific instance of the symbolic lithium-ion parameter class. If not provided, the default set of symbolic lithium-ion parameters will be used.

  • known_value (str, optional) – The known value needed to complete the electrode SOH model. Can be “cyclable lithium capacity” (default) or “cell capacity”.

  • options (dict-like, optional) – A dictionary of options to be passed to the model, see pybamm.BatteryModelOptions.

get_initial_ocps(initial_value)#

Calculate initial open-circuit potentials to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:

initial_value (float) – Target SOC, must be between 0 and 1.

Returns:

The initial open-circuit potentials at the desired initial state of charge

Return type:

Un, Up

get_initial_stoichiometries(initial_value)#

Calculate initial stoichiometries to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:

initial_value (float) – 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.

Returns:

The initial stoichiometries that give the desired initial state of charge

Return type:

x, y

get_min_max_ocps()#

Calculate min/max open-circuit potentials given voltage limits, open-circuit potentials, etc defined by parameter_values

Returns:

The min/max ocps

Return type:

Un_0, Un_100, Up_100, Up_0

get_min_max_stoichiometries()#

Calculate min/max stoichiometries given voltage limits, open-circuit potentials, etc defined by parameter_values

Returns:

The min/max stoichiometries

Return type:

x_0, x_100, y_100, y_0

pybamm.lithium_ion.get_initial_stoichiometries(initial_value, parameter_values, param=None, known_value='cyclable lithium capacity', options=None)#

Calculate initial stoichiometries to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:
  • initial_value (float) – 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.

  • parameter_values (pybamm.ParameterValues) – The parameter values class that will be used for the simulation. Required for calculating appropriate initial stoichiometries.

  • param (pybamm.LithiumIonParameters, optional) – The symbolic parameter set to use for the simulation. If not provided, the default parameter set will be used.

  • known_value (str, optional) – The known value needed to complete the electrode SOH model. Can be “cyclable lithium capacity” (default) or “cell capacity”.

  • options (dict-like, optional) – A dictionary of options to be passed to the model, see pybamm.BatteryModelOptions.

Returns:

The initial stoichiometries that give the desired initial state of charge

Return type:

x, y

pybamm.lithium_ion.get_min_max_stoichiometries(parameter_values, param=None, known_value='cyclable lithium capacity', options=None)#

Calculate min/max stoichiometries given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:
  • parameter_values (pybamm.ParameterValues) – The parameter values class that will be used for the simulation. Required for calculating appropriate initial stoichiometries.

  • param (pybamm.LithiumIonParameters, optional) – The symbolic parameter set to use for the simulation. If not provided, the default parameter set will be used.

  • known_value (str, optional) – The known value needed to complete the electrode SOH model. Can be “cyclable lithium capacity” (default) or “cell capacity”.

  • options (dict-like, optional) – A dictionary of options to be passed to the model, see pybamm.BatteryModelOptions.

Returns:

The min/max stoichiometries

Return type:

x_0, x_100, y_100, y_0

pybamm.lithium_ion.get_initial_ocps(initial_value, parameter_values, param=None, known_value='cyclable lithium capacity', options=None)#

Calculate initial open-circuit potentials to start off the simulation at a particular state of charge, given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:
  • initial_value (float) – 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.

  • parameter_values (pybamm.ParameterValues) – The parameter values class that will be used for the simulation. Required for calculating appropriate initial stoichiometries.

  • param (pybamm.LithiumIonParameters, optional) – The symbolic parameter set to use for the simulation. If not provided, the default parameter set will be used.

  • known_value (str, optional) – The known value needed to complete the electrode SOH model. Can be “cyclable lithium capacity” (default) or “cell capacity”.

  • options (dict-like, optional) – A dictionary of options to be passed to the model, see pybamm.BatteryModelOptions.

Returns:

The initial electrode OCPs that give the desired initial state of charge

Return type:

Un, Up

pybamm.lithium_ion.get_min_max_ocps(parameter_values, param=None, known_value='cyclable lithium capacity', options=None)#

Calculate min/max open-circuit potentials given voltage limits, open-circuit potentials, etc defined by parameter_values

Parameters:
  • parameter_values (pybamm.ParameterValues) – The parameter values class that will be used for the simulation. Required for calculating appropriate initial open-circuit potentials.

  • param (pybamm.LithiumIonParameters, optional) – The symbolic parameter set to use for the simulation. If not provided, the default parameter set will be used.

  • known_value (str, optional) – The known value needed to complete the electrode SOH model. Can be “cyclable lithium capacity” (default) or “cell capacity”.

  • options (dict-like, optional) – A dictionary of options to be passed to the model, see pybamm.BatteryModelOptions.

Returns:

The min/max OCPs

Return type:

Un_0, Un_100, Up_100, Up_0

Lead Acid Models#

Base Model#
class pybamm.lead_acid.BaseModel(options=None, name='Unnamed lead-acid model', build=False)#

Overwrites default parameters from Base Model with default parameters for lead-acid models

Parameters:
  • options (dict-like, optional) – A dictionary of options to be passed to the model. If this is a dict (and not a subtype of dict), it will be processed by pybamm.BatteryModelOptions to ensure that the options are valid. If this is a subtype of dict, it is assumed that the options have already been processed and are valid. This allows for the use of custom options classes. The default options are given by pybamm.BatteryModelOptions.

  • name (str, optional) – The name of the model. The default is “Unnamed battery model”.

  • build (bool, optional) – Whether to build the model on instantiation. Default is True. Setting this option to False allows users to change any number of the submodels before building the complete model (submodels cannot be changed after the model is built).

Extends: pybamm.models.full_battery_models.base_battery_model.BaseBatteryModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lead_acid.base_lead_acid_model.BaseModel

set_soc_variables()#

Set variables relating to the state of charge.

Leading-Order Quasi-Static Model#
class pybamm.lead_acid.LOQS(options=None, name='LOQS model', build=True)#

Leading-Order Quasi-Static model for lead-acid, from Sulzer et al.[1]. See pybamm.lead_acid.BaseModel for more details.

Extends: pybamm.models.full_battery_models.lead_acid.base_lead_acid_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lead_acid.loqs.LOQS

set_external_circuit_submodel()#

Define how the external circuit defines the boundary conditions for the model, e.g. (not necessarily constant-) current, voltage, etc

References

Full Model#
class pybamm.lead_acid.Full(options=None, name='Full model', build=True)#

Porous electrode model for lead-acid, from Sulzer et al.[1], based on the Newman-Tiedemann model. See pybamm.lead_acid.BaseModel for more details.

Extends: pybamm.models.full_battery_models.lead_acid.base_lead_acid_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lead_acid.full.Full

class pybamm.lead_acid.BasicFull(name='Basic full model')#

Porous electrode model for lead-acid, from Sulzer et al.[1].

This class differs from the pybamm.lead_acid.Full model class in that it shows the whole model in a single class. This comes at the cost of flexibility in comparing different physical effects, and in general the main DFN class should be used instead.

Parameters:

name (str, optional) – The name of the model.

Extends: pybamm.models.full_battery_models.lead_acid.base_lead_acid_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.lead_acid.basic_full.BasicFull

References

Equivalent Circuit Models#

Thevenin Model#
class pybamm.equivalent_circuit.Thevenin(name='Thevenin Equivalent Circuit Model', options=None, build=True)#

The classical Thevenin Equivalent Circuit Model of a battery as described in, for example, Barletta et al.[1].

This equivalent circuit model consists of an OCV element, a resistor element, and a number of RC elements (by default 1). The model is coupled to two lumped thermal models, one for the cell and one for the surrounding jig. Heat generation terms for each element follow equation (1) of Nieto et al.[2].

Parameters:
  • name (str, optional) – The name of the model. The default is “Thevenin Equivalent Circuit Model”.

  • options (dict, optional) –

    A dictionary of options to be passed to the model. The default is None. Possible options are:

    • ”number of rc elements”str

      The number of RC elements to be added to the model. The default is 1.

    • ”calculate discharge energy”: str

      Whether to calculate the discharge energy, throughput energy and throughput capacity in addition to discharge capacity. Must be one of “true” or “false”. “false” is the default, since calculating discharge energy can be computationally expensive for simple models like SPM.

    • ”operating mode”str

      Sets the operating mode for the model. This determines how the current is set. Can be:

      • ”current” (default) : the current is explicity supplied

      • ”voltage”/”power”/”resistance” : solve an algebraic equation for current such that voltage/power/resistance is correct

      • ”differential power”/”differential resistance” : solve a differential equation for the power or resistance

      • ”CCCV”: a special implementation of the common constant-current constant-voltage charging protocol, via an ODE for the current

      • callable : if a callable is given as this option, the function defines the residual of an algebraic equation. The applied current will be solved for such that the algebraic constraint is satisfied.

  • build (bool, optional) – Whether to build the model on instantiation. Default is True. Setting this option to False allows users to change any number of the submodels before building the complete model (submodels cannot be changed after the model is built).

Examples

>>> model = pybamm.equivalent_circuit.Thevenin()
>>> model.name
'Thevenin Equivalent Circuit Model'

Extends: pybamm.models.base_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.full_battery_models.equivalent_circuit.thevenin.Thevenin

set_external_circuit_submodel()#

Define how the external circuit defines the boundary conditions for the model, e.g. (not necessarily constant-) current, voltage, etc

References

Submodels#

Base Submodel#
class pybamm.BaseSubModel(param, domain=None, name='Unnamed submodel', external=False, options=None, phase=None)[source]#

The base class for all submodels. All submodels inherit from this class and must only provide public methods which overwrite those in this base class. Any methods added to a submodel that do not overwrite those in this bass class are made private with the prefix ‘_’, providing a consistent public interface for all submodels.

Parameters:
  • param (parameter class) – The model parameter symbols

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • name (str) – A string giving the name of the submodel

  • external (bool, optional) – Whether the variables defined by the submodel will be provided externally by the users. Default is ‘False’.

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is None).

param#

The model parameter symbols

Type:

parameter class

rhs#

A dictionary that maps expressions (variables) to expressions that represent the rhs

Type:

dict

algebraic#

A dictionary that maps expressions (variables) to expressions that represent the algebraic equations. The algebraic expressions are assumed to equate to zero. Note that all the variables in the model must exist in the keys of rhs or algebraic.

Type:

dict

initial_conditions#

A dictionary that maps expressions (variables) to expressions that represent the initial conditions for the state variables y. The initial conditions for algebraic variables are provided as initial guesses to a root finding algorithm that calculates consistent initial conditions.

Type:

dict

boundary_conditions#

A dictionary that maps expressions (variables) to expressions that represent the boundary conditions

Type:

dict

variables#

A dictionary that maps strings to expressions that represent the useful variables

Type:

dict

events#

A list of events. Each event can either cause the solver to terminate (e.g. concentration goes negative), or be used to inform the solver of the existance of a discontinuity (e.g. discontinuity in the input current)

Type:

list

Extends: pybamm.models.base_model.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.base_submodel.BaseSubModel

get_coupled_variables(variables)[source]#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()[source]#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)[source]#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_boundary_conditions(variables)[source]#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_events(variables)[source]#

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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)[source]#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)[source]#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Active Material#

Submodels for (loss of) active material

Base Model#
class pybamm.active_material.BaseModel(param, domain, options, phase='primary')#

Base class for active material volume fraction

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – Additional options to pass to the model

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.active_material.base_active_material.BaseModel

Constant Active Material#
class pybamm.active_material.Constant(param, domain, options, phase='primary')#

Submodel for constant active material

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – Additional options to pass to the model

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.active_material.base_active_material.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.active_material.constant_active_material.Constant

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Loss of Active Material#
class pybamm.active_material.LossActiveMaterial(param, domain, options, x_average)#

Submodel for varying active material volume fraction from Ai et al.[1] and Reniers et al.[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – Additional options to pass to the model

  • x_average (bool) – Whether to use x-averaged variables (SPM, SPMe, etc) or full variables (DFN)

Extends: pybamm.models.submodels.active_material.base_active_material.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.active_material.loss_active_material.LossActiveMaterial

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Current Collector#
Base Model#
class pybamm.current_collector.BaseModel(param)#

Base class for current collector submodels

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.base_current_collector.BaseModel

Effective Current collector Resistance models#
class pybamm.current_collector.EffectiveResistance(options=None, name='Effective resistance in current collector model')#

A model which calculates the effective Ohmic resistance of the current collectors in the limit of large electrical conductivity. For details see Timms et al.[1]. Note that this formulation assumes uniform potential across the tabs. See pybamm.AlternativeEffectiveResistance2D for the formulation that assumes a uniform current density at the tabs (in 1D the two formulations are equivalent).

Parameters:
  • options (dict) –

    A dictionary of options to be passed to the model. The options that can be set are listed below.

    • ”dimensionality”int, optional

      Sets the dimension of the current collector problem. Can be 1 (default) or 2.

  • name (str, optional) – The name of the model.

Extends: pybamm.models.submodels.current_collector.effective_resistance_current_collector.BaseEffectiveResistance

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.effective_resistance_current_collector.EffectiveResistance

post_process(solution, param_values, V_av, I_av)#

Calculates the potentials in the current collector and the terminal voltage given the average voltage and current. Note: This takes in the processed V_av and I_av from a 1D simulation representing the average cell behaviour and returns a dictionary of processed potentials.

class pybamm.current_collector.AlternativeEffectiveResistance2D#

A model which calculates the effective Ohmic resistance of the 2D current collectors in the limit of large electrical conductivity. This model assumes a uniform current density at the tabs and the solution is computed by first solving and auxilliary problem which is the related to the resistances.

Extends: pybamm.models.submodels.current_collector.effective_resistance_current_collector.BaseEffectiveResistance

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.effective_resistance_current_collector.AlternativeEffectiveResistance2D

post_process(solution, param_values, V_av, I_av)#

Calculates the potentials in the current collector given the average voltage and current. Note: This takes in the processed V_av and I_av from a 1D simulation representing the average cell behaviour and returns a dictionary of processed potentials.

References

Uniform#
class pybamm.current_collector.Uniform(param)#

A submodel for uniform potential in the current collectors which is valid in the limit of fast conductivity in the current collectors.

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.current_collector.base_current_collector.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.homogeneous_current_collector.Uniform

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Potential Pair models#
class pybamm.current_collector.BasePotentialPair(param)#

A submodel for Ohm’s law plus conservation of current in the current collectors. For details on the potential pair formulation see Timms et al.[1] and Marquis[2].

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.current_collector.base_current_collector.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.potential_pair.BasePotentialPair

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

class pybamm.current_collector.PotentialPair2plus1D(param)#

Base class for a 2+1D potential pair model

Extends: pybamm.models.submodels.current_collector.potential_pair.BasePotentialPair

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.potential_pair.PotentialPair2plus1D

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

class pybamm.current_collector.PotentialPair1plus1D(param)#

Base class for a 1+1D potential pair model.

Extends: pybamm.models.submodels.current_collector.potential_pair.BasePotentialPair

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.current_collector.potential_pair.PotentialPair1plus1D

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Convection#

The convection submodels are split up into “through-cell”, which is the x-direction problem in the electrode domains, and “transverse”, which is the z-direction problem in the separator domain

Base Convection#
class pybamm.convection.BaseModel(param, options=None)#

Base class for convection submodels.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.base_convection.BaseModel

Through-cell Convection#
Base Model#
class pybamm.convection.through_cell.BaseThroughCellModel(param, options=None)#

Base class for convection submodels in the through-cell direction.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.convection.base_convection.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.through_cell.base_through_cell_convection.BaseThroughCellModel

No Convection#
class pybamm.convection.through_cell.NoConvection(param, options=None)#

A submodel for case where there is no convection.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.convection.through_cell.base_through_cell_convection.BaseThroughCellModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.through_cell.no_convection.NoConvection

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Leading-Order Through-cell Model#
class pybamm.convection.through_cell.Explicit(param)#

A submodel for the leading-order approximation of pressure-driven convection

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.convection.through_cell.base_through_cell_convection.BaseThroughCellModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.through_cell.explicit_convection.Explicit

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Full Through-cell Model#
class pybamm.convection.through_cell.Full(param)#

Submodel for the full model of pressure-driven convection

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.convection.through_cell.base_through_cell_convection.BaseThroughCellModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.through_cell.full_convection.Full

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Transverse Convection#
Base Model#
class pybamm.convection.transverse.BaseTransverseModel(param, options=None)#

Base class for convection submodels in transverse directions.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.convection.base_convection.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.transverse.base_transverse_convection.BaseTransverseModel

No Transverse Convection#
class pybamm.convection.transverse.NoConvection(param, options=None)#

Submodel for no convection in transverse directions

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.convection.transverse.base_transverse_convection.BaseTransverseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.transverse.no_convection.NoConvection

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Uniform Transverse Model#
class pybamm.convection.transverse.Uniform(param)#

Submodel for uniform convection in transverse directions

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.convection.transverse.base_transverse_convection.BaseTransverseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.transverse.uniform_convection.Uniform

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Full Transverse Convection#
class pybamm.convection.transverse.Full(param)#

Submodel for the full model of pressure-driven convection in transverse directions

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.convection.transverse.base_transverse_convection.BaseTransverseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.convection.transverse.full_convection.Full

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Electrode#
Electrode Base Model#
class pybamm.electrode.BaseElectrode(param, domain, options=None, set_positive_potential=True)#

Base class for electrode submodels.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

  • set_positive_potential (bool, optional) – If True the battery model sets the positive potential based on the current. If False, the potential is specified by the user. Default is True.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.base_electrode.BaseElectrode

Ohmic#
Base Model#
class pybamm.electrode.ohm.BaseModel(param, domain, options=None, set_positive_potential=True)#

A base class for electrode submodels that employ Ohm’s law.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrode.base_electrode.BaseElectrode

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Leading Order Model#
class pybamm.electrode.ohm.LeadingOrder(param, domain, options=None, set_positive_potential=True)#

An electrode submodel that employs Ohm’s law the leading-order approximation to governing equations.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

  • set_positive_potential (bool, optional) – If True the battery model sets the positve potential based on the current. If False, the potential is specified by the user. Default is True.

Extends: pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.leading_ohm.LeadingOrder

get_coupled_variables(variables)#

Returns variables which are derived from the fundamental variables in the model.

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Composite Model#
class pybamm.electrode.ohm.Composite(param, domain, options=None)#

An explicit composite leading and first order solution to solid phase current conservation with ohm’s law. Note that the returned current density is only the leading order approximation.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘Negative electrode’ or ‘Positive electrode’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.composite_ohm.Composite

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Full Model#
class pybamm.electrode.ohm.Full(param, domain, options=None)#

Full model of electrode employing Ohm’s law.

Extends: pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.full_ohm.Full

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Surface Form#
class pybamm.electrode.ohm.SurfaceForm(param, domain, options=None)#

A submodel for the electrode with Ohm’s law in the surface potential formulation.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrode.ohm.base_ohm.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.surface_form_ohm.SurfaceForm

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Explicit potential drop for lithium metal#
class pybamm.electrode.ohm.LithiumMetalExplicit(param, domain, options=None)#

Explicit model for potential drop across a lithium metal electrode.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrode.ohm.li_metal.LithiumMetalBaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrode.ohm.li_metal.LithiumMetalExplicit

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Electrolyte Conductivity#
Base Electrolyte Conductivity Submodel#
class pybamm.electrolyte_conductivity.BaseElectrolyteConductivity(param, domain=None, options=None)#

Base class for conservation of charge in the electrolyte.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str, optional) – The domain in which the model holds

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Leading Order Model#
class pybamm.electrolyte_conductivity.LeadingOrder(param, domain=None, options=None)#

Leading-order model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations. (Leading refers to leading-order in the asymptotic reduction)

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str, optional) – The domain in which the model holds

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.leading_order_conductivity.LeadingOrder

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Composite Model#
class pybamm.electrolyte_conductivity.Composite(param, domain=None, options=None)#

Base class for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str, optional) – The domain in which the model holds

  • options (dict, optional) – A dictionary of options to be passed to the model.

  • higher_order_terms (str) – What kind of higher-order terms to use (‘composite’ or ‘first-order’)

Extends: pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.composite_conductivity.Composite

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Integrated Model#
class pybamm.electrolyte_conductivity.Integrated(param, domain=None, options=None)#

Integrated model for conservation of charge in the electrolyte derived from integrating the Stefan-Maxwell constitutive equations, from Brosa Planella et al.[1].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str, optional) – The domain in which the model holds

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.integrated_conductivity.Integrated

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

References

Full Model#
class pybamm.electrolyte_conductivity.Full(param, options=None)#

Full model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations. (Full refers to unreduced by asymptotic methods)

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.full_conductivity.Full

check_algebraic_equations(post_discretisation)#

Check that the algebraic equations are well-posed. After discretisation, there must be at least one StateVector in each algebraic equation.

check_discretised_or_discretise_inplace_if_0D()#

Discretise model if it isn’t already discretised This only works with purely 0D models, as otherwise the mesh and spatial method should be specified by the user

check_ics_bcs()#

Check that the initial and boundary conditions are well-posed.

check_no_repeated_keys()#

Check that no equation keys are repeated.

check_well_determined(post_discretisation)#

Check that the model is not under- or over-determined.

check_well_posedness(post_discretisation=False)#

Check that the model is well-posed by executing the following tests: - Model is not over- or underdetermined, by comparing keys and equations in rhs and algebraic. Overdetermined if more equations than variables, underdetermined if more variables than equations. - There is an initial condition in self.initial_conditions for each variable/equation pair in self.rhs - There are appropriate boundary conditions in self.boundary_conditions for each variable/equation pair in self.rhs and self.algebraic

Parameters:

post_discretisation (boolean) – A flag indicating tests to be skipped after discretisation

property default_solver#

Return default solver based on whether model is ODE/DAE or algebraic

classmethod deserialise(properties: dict)#

Create a model instance from a serialised object.

export_casadi_objects(variable_names, input_parameter_order=None)#

Export the constituent parts of the model (rhs, algebraic, initial conditions, etc) as casadi objects.

Parameters:
  • variable_names (list) – Variables to be exported alongside the model structure

  • input_parameter_order (list, optional) – Order in which the input parameters should be stacked. If input_parameter_order=None and len(self.input_parameters) > 1, a ValueError is raised (this helps to avoid accidentally using the wrong order)

Returns:

casadi_dict – Dictionary of {str: casadi object} pairs representing the model in casadi format

Return type:

dict

generate(filename, variable_names, input_parameter_order=None, cg_options=None)#

Generate the model in C, using CasADi.

Parameters:
  • filename (str) – Name of the file to which to save the code

  • variable_names (list) – Variables to be exported alongside the model structure

  • input_parameter_order (list, optional) – Order in which the input parameters should be stacked. If input_parameter_order=None and len(self.input_parameters) > 1, a ValueError is raised (this helps to avoid accidentally using the wrong order)

  • cg_options (dict) – Options to pass to the code generator. See https://web.casadi.org/docs/#generating-c-code

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

get_parameter_info()#

Extracts the parameter information and returns it as a dictionary. To get a list of all parameter-like objects without extra information, use model.parameters.

info(symbol_name)#

Provides helpful summary information for a symbol.

Parameters:

parameter_name (str) –

property input_parameters#

Returns all the input parameters in the model

latexify(filename=None, newline=True, output_variables=None)#

Converts all model equations in latex.

Parameters:
  • filename (str (optional)) – Accepted file formats - any image format, pdf and tex Default is None, When None returns all model equations in latex If not None, returns all model equations in given file format.

  • newline (bool (optional)) – Default is True, If True, returns every equation in a new line. If False, returns the list of all the equations.

  • model (Load) –

  • pybamm.lithium_ion.SPM() (>>> model =) –

  • png (This will returns all model equations in) –

  • model.latexify("equations.png") (>>>) –

  • latex (This will return all the model equations in) –

  • model.latexify() (>>>) –

  • equations (This will return first five model) –

  • model.latexify(newline=False) (>>>) –

  • equations

  • model.latexify(newline=False)[1 (>>>) –

new_copy()#

Creates a copy of the model, explicitly copying all the mutable attributes to avoid issues with shared objects.

property parameters#

Returns all the parameters in the model

print_parameter_info()#

Print parameter information in a formatted table from a dictionary of parameters

process_parameters_and_discretise(symbol, parameter_values, disc)#

Process parameters and discretise a symbol using supplied parameter values and discretisation. Note: care should be taken if using spatial operators on dimensional symbols. Operators in pybamm are written in non-dimensional form, so may need to be scaled by the appropriate length scale. It is recommended to use this method on non-dimensional symbols.

Parameters:
Returns:

Processed symbol

Return type:

pybamm.Symbol

save_model(filename=None, mesh=None, variables=None)#

Write out a discretised model to a JSON file

Parameters:
  • filename (str, optional) –

  • provided (The desired name of the JSON file. If no name is) –

  • created (one will be) –

  • name (based on the model) –

  • datetime. (and the current) –

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions_from(solution, inplace=True, return_type='model')#

Update initial conditions with the final states from a Solution object or from a dictionary. This assumes that, for each variable in self.initial_conditions, there is a corresponding variable in the solution with the same name and size.

Parameters:
  • solution (pybamm.Solution, or dict) – The solution to use to initialize the model

  • inplace (bool, optional) – Whether to modify the model inplace or create a new model (default True)

  • return_type (str, optional) – Whether to return the model (default) or initial conditions (“ics”)

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

update(*submodels)#

Update model to add new physics from submodels

Parameters:

submodel (iterable of pybamm.BaseModel) – The submodels from which to create new model

property variables_and_events#

Returns variables and events in a single dictionary

Surface Form#
Full Model#
class pybamm.electrolyte_conductivity.surface_potential_form.FullDifferential(param, domain, options=None)#

Full model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations and where capacitance is present. (Full refers to unreduced by asymptotic methods)

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.full_surface_form_conductivity.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.full_surface_form_conductivity.FullDifferential

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

class pybamm.electrolyte_conductivity.surface_potential_form.FullAlgebraic(param, domain, options=None)#

Full model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations. (Full refers to unreduced by asymptotic methods)

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.full_surface_form_conductivity.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.full_surface_form_conductivity.FullAlgebraic

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Leading Order Model#
class pybamm.electrolyte_conductivity.surface_potential_form.LeadingOrderDifferential(param, domain, options=None)#

Leading-order model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations employing the surface potential difference formulation and where capacitance is present.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.leading_surface_form_conductivity.BaseLeadingOrderSurfaceForm

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.leading_surface_form_conductivity.LeadingOrderDifferential

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

class pybamm.electrolyte_conductivity.surface_potential_form.LeadingOrderAlgebraic(param, domain, options=None)#

Leading-order model for conservation of charge in the electrolyte employing the Stefan-Maxwell constitutive equations employing the surface potential difference formulation.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.leading_surface_form_conductivity.BaseLeadingOrderSurfaceForm

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.leading_surface_form_conductivity.LeadingOrderAlgebraic

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Explicit Model#
class pybamm.electrolyte_conductivity.surface_potential_form.Explicit(param, domain, options)#

Class for deriving surface potential difference variables from the electrode and electrolyte potentials

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain in which the model holds

  • options (dict) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_conductivity.base_electrolyte_conductivity.BaseElectrolyteConductivity

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.explicit_surface_form_conductivity.Explicit

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Electrolyte Diffusion#
Base Electrolyte Diffusion Submodel#
class pybamm.electrolyte_diffusion.BaseElectrolyteDiffusion(param, options=None)#

Base class for conservation of mass in the electrolyte.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_diffusion.base_electrolyte_diffusion.BaseElectrolyteDiffusion

Constant Concentration#
class pybamm.electrolyte_diffusion.ConstantConcentration(param, options=None)#

Class for constant concentration of electrolyte

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_diffusion.base_electrolyte_diffusion.BaseElectrolyteDiffusion

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_diffusion.constant_concentration.ConstantConcentration

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

We provide boundary conditions even though the concentration is constant so that the gradient of the concentration has the correct shape after discretisation.

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Leading Order Model#
class pybamm.electrolyte_diffusion.LeadingOrder(param)#

Class for conservation of mass in the electrolyte employing the Stefan-Maxwell constitutive equations. (Leading refers to leading order of asymptotic reduction)

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.electrolyte_diffusion.base_electrolyte_diffusion.BaseElectrolyteDiffusion

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_diffusion.leading_order_diffusion.LeadingOrder

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Full Model#
class pybamm.electrolyte_diffusion.Full(param, options=None)#

Class for conservation of mass in the electrolyte employing the Stefan-Maxwell constitutive equations. (Full refers to unreduced by asymptotic methods)

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.electrolyte_diffusion.base_electrolyte_diffusion.BaseElectrolyteDiffusion

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.electrolyte_diffusion.full_diffusion.Full

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

External circuit#

Models to enforce different boundary conditions (as imposed by an imaginary external circuit) such as constant current, constant voltage, constant power, or any other relationship between the current and voltage. “Current control” enforces these directly through boundary conditions, while “Function control” submodels add an algebraic equation (for the current) and hence can be used to set any variable to be constant.

Explicit control external circuit#

Current is explicitly specified, either by a function or in terms of other variables.

class pybamm.external_circuit.ExplicitCurrentControl(param, options)#

External circuit with current control.

Extends: pybamm.models.submodels.external_circuit.base_external_circuit.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.explicit_control_external_circuit.ExplicitCurrentControl

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

class pybamm.external_circuit.ExplicitPowerControl(param, options)#

External circuit with current set explicitly to hit target power.

Extends: pybamm.models.submodels.external_circuit.base_external_circuit.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.explicit_control_external_circuit.ExplicitPowerControl

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

class pybamm.external_circuit.ExplicitResistanceControl(param, options)#

External circuit with current set explicitly to hit target resistance.

Extends: pybamm.models.submodels.external_circuit.base_external_circuit.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.explicit_control_external_circuit.ExplicitResistanceControl

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Function control external circuit#
class pybamm.external_circuit.FunctionControl(param, external_circuit_function, options, control='algebraic')#

External circuit with an arbitrary function, implemented as a control on the current either via an algebraic equation, or a differential equation.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • external_circuit_function (callable) – The function that controls the current

  • options (dict) – Dictionary of options to use for the submodel

  • control (str, optional) – The type of control to use. Must be one of ‘algebraic’ (default) or ‘differential’.

Extends: pybamm.models.submodels.external_circuit.base_external_circuit.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.function_control_external_circuit.FunctionControl

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

class pybamm.external_circuit.VoltageFunctionControl(param, options)#

External circuit with voltage control, implemented as an extra algebraic equation.

Extends: pybamm.models.submodels.external_circuit.function_control_external_circuit.FunctionControl

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.function_control_external_circuit.VoltageFunctionControl

class pybamm.external_circuit.PowerFunctionControl(param, options, control='algebraic')#

External circuit with power control.

Extends: pybamm.models.submodels.external_circuit.function_control_external_circuit.FunctionControl

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.function_control_external_circuit.PowerFunctionControl

class pybamm.external_circuit.ResistanceFunctionControl(param, options, control)#

External circuit with resistance control.

Extends: pybamm.models.submodels.external_circuit.function_control_external_circuit.FunctionControl

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.function_control_external_circuit.ResistanceFunctionControl

class pybamm.external_circuit.CCCVFunctionControl(param, options)#

External circuit with constant-current constant-voltage control, as implemented in Mohtat et al.[1].

References

Extends: pybamm.models.submodels.external_circuit.function_control_external_circuit.FunctionControl

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.external_circuit.function_control_external_circuit.CCCVFunctionControl

Interface#
Interface Base Model#
class pybamm.interface.BaseInterface(param, domain, reaction, options, phase='primary')#

Base class for interfacial currents

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.base_interface.BaseInterface

Total Interfacial Current Model#
class pybamm.interface.TotalInterfacialCurrent(param, chemistry, options)#

Total interfacial current, summing up contributions from all reactions

Parameters:
  • param – model parameters

  • chemistry (str) – The name of the battery chemistry whose reactions need to be summed up

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.total_interfacial_current.TotalInterfacialCurrent

get_coupled_variables(variables)#

Get variables associated with interfacial current over the whole cell domain This function also creates the “total source term” variables by summing all the reactions.

Interface Utilisation#
Utilisation Base Model#
class pybamm.interface.interface_utilisation.BaseModel(param, domain, options)#

Base class for interface utilisation

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.interface_utilisation.base_utilisation.BaseModel

Constant Utilisation#
class pybamm.interface.interface_utilisation.Constant(param, domain, options)#

Submodel for constant interface utilisation

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.interface.interface_utilisation.base_utilisation.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.interface_utilisation.constant_utilisation.Constant

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

CurrentDriven Utilisation#
class pybamm.interface.interface_utilisation.CurrentDriven(param, domain, options, reaction_loc)#

Current-driven ODE for interface utilisation

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

  • reaction_loc (str) – Where the reaction happens: “x-average” (SPM, SPMe, etc), “full electrode” (full DFN), or “interface” (half-cell model)

Extends: pybamm.models.submodels.interface.interface_utilisation.base_utilisation.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.interface_utilisation.current_driven_utilisation.CurrentDriven

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Full Utilisation#
class pybamm.interface.interface_utilisation.Full(param, domain, options)#

Submodel for constant interface utilisation

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – Either ‘negative’ or ‘positive’

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.interface.interface_utilisation.base_utilisation.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.interface_utilisation.full_utilisation.Full

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Kinetics#
Base Kinetics#
class pybamm.kinetics.BaseKinetics(param, domain, reaction, options, phase='primary')#

Base submodel for kinetics

Parameters:
  • param – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Butler Volmer#
class pybamm.kinetics.SymmetricButlerVolmer(param, domain, reaction, options, phase='primary')#

Submodel which implements the symmetric forward Butler-Volmer equation:

\[j = 2 * j_0(c) * \sinh(ne * F * \eta_r(c) / RT)\]
Parameters:
  • param (parameter class) – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.butler_volmer.SymmetricButlerVolmer

class pybamm.kinetics.AsymmetricButlerVolmer(param, domain, reaction, options, phase='primary')#

Submodel which implements the asymmetric forward Butler-Volmer equation

Parameters:
  • param (parameter class) – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.butler_volmer.AsymmetricButlerVolmer

Diffusion-limited#
class pybamm.kinetics.DiffusionLimited(param, domain, reaction, options, order)#

Submodel for diffusion-limited kinetics

Parameters:
  • param – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • order (str) – The order of the model (“leading” or “full”)

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.diffusion_limited.DiffusionLimited

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Linear#
class pybamm.kinetics.Linear(param, domain, reaction, options, phase='primary')#

Submodel which implements linear kinetics. Valid for small overpotentials/currents.

Parameters:
  • param (parameter class) – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.linear.Linear

Marcus#
class pybamm.kinetics.Marcus(param, domain, reaction, options, phase='primary')#

Submodel which implements Marcus kinetics.

Parameters:
  • param (parameter class) – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.marcus.Marcus

NoReaction#
class pybamm.kinetics.NoReaction(param, domain, reaction, options, phase='primary')#

Base submodel for when no reaction occurs

Parameters:
  • param – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.no_reaction.NoReaction

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Tafel#
class pybamm.kinetics.ForwardTafel(param, domain, reaction, options, phase='primary')#

Base submodel which implements the forward Tafel equation:

\[j = u * j_0(c) * \exp((ne * alpha * F * \eta_r(c) / RT)\]
Parameters:
  • param – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.tafel.ForwardTafel

MSMR Butler Volmer#
class pybamm.kinetics.MSMRButlerVolmer(param, domain, reaction, options, phase='primary')#

Submodel which implements the forward Butler-Volmer equation in the MSMR formulation in which the interfacial current density is summed over all reactions.

Parameters:
  • param (parameter class) – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.kinetics.base_kinetics.BaseKinetics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.msmr_butler_volmer.MSMRButlerVolmer

Total Main Kinetics#
class pybamm.kinetics.TotalMainKinetics(param, domain, reaction, options)#

Class summing up contributions to the main (e.g. intercalation) reaction for cases with primary, secondary, … reactions e.g. silicon-graphite

Parameters:
  • param – model parameters

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.total_main_kinetics.TotalMainKinetics

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Inverse Kinetics#
Inverse Butler-Volmer#
class pybamm.kinetics.InverseButlerVolmer(param, domain, reaction, options=None)#

A submodel that implements the inverted form of the Butler-Volmer relation to solve for the reaction overpotential.

Parameters:
  • param – Model parameters

  • domain (iter of str, optional) – The domain(s) in which to compute the interfacial current.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. In this case “SEI film resistance” is the important option. See pybamm.BaseBatteryModel

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.InverseButlerVolmer

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Lithium Plating#
Base Plating#
class pybamm.lithium_plating.BasePlating(param, domain, options=None)#

Base class for lithium plating models, from O’Kane et al.[1] and O’Kane et al.[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.lithium_plating.base_plating.BasePlating

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

References

No Plating#
class pybamm.lithium_plating.NoPlating(param, domain, options=None)#

Base class for no lithium plating/stripping.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.interface.lithium_plating.base_plating.BasePlating

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.lithium_plating.no_plating.NoPlating

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Plating#
class pybamm.lithium_plating.Plating(param, domain, x_average, options)#

Class for lithium plating, from O’Kane et al.[1] and O’Kane et al.[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • x_average (bool) – Whether to use x-averaged variables (SPM, SPMe, etc) or full variables (DFN)

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.interface.lithium_plating.base_plating.BasePlating

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.lithium_plating.plating.Plating

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Open-circuit potential models#
Base Open Circuit Potential#
class pybamm.open_circuit_potential.BaseOpenCircuitPotential(param, domain, reaction, options, phase='primary')#

Base class for open-circuit potentials

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain to implement the model, either: ‘Negative’ or ‘Positive’.

  • reaction (str) – The name of the reaction being implemented

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.open_circuit_potential.base_ocp.BaseOpenCircuitPotential

Current Sigmoid Open Circuit Potential#
class pybamm.open_circuit_potential.CurrentSigmoidOpenCircuitPotential(param, domain, reaction, options, phase='primary')#
View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.open_circuit_potential.current_sigmoid_ocp.CurrentSigmoidOpenCircuitPotential

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Single Open Circuit Potential#
class pybamm.open_circuit_potential.SingleOpenCircuitPotential(param, domain, reaction, options, phase='primary')#
View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.open_circuit_potential.single_ocp.SingleOpenCircuitPotential

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

MSMR Open Circuit Potential#
class pybamm.open_circuit_potential.MSMROpenCircuitPotential(param, domain, reaction, options, phase='primary')#

Class for open-circuit potential within the Multi-Species Multi-Reaction framework Baker and Verbrugge[1]. The thermodynamic model is presented in Verbrugge et al.[2], along with parameter values for a number of substitutional materials.

Extends: pybamm.models.submodels.interface.open_circuit_potential.base_ocp.BaseOpenCircuitPotential

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.open_circuit_potential.msmr_ocp.MSMROpenCircuitPotential

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

References

SEI models#
SEI Base Model#
class pybamm.sei.BaseModel(param, domain, options, phase='primary', cracks=False)#

Base class for SEI models.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict) – A dictionary of options to be passed to the model.

  • phase (str, optional) – Phase of the particle (default is “primary”)

  • cracks (bool, optional) – Whether this is a submodel for standard SEI or SEI on cracks

Extends: pybamm.models.submodels.interface.base_interface.BaseInterface

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.sei.base_sei.BaseModel

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Constant SEI#
class pybamm.sei.ConstantSEI(param, domain, options, phase='primary')#

Class for SEI with constant thickness.

Note that there is no SEI current, so we don’t need to update the “sum of interfacial current densities” variables from pybamm.interface.BaseInterface

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict) – A dictionary of options to be passed to the model.

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.interface.sei.base_sei.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.sei.constant_sei.ConstantSEI

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

No SEI#
class pybamm.sei.NoSEI(param, domain, options, phase='primary', cracks=False)#

Class for no SEI.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict) – A dictionary of options to be passed to the model.

  • phase (str, optional) – Phase of the particle (default is “primary”)

  • cracks (bool, optional) – Whether this is a submodel for standard SEI or SEI on cracks

Extends: pybamm.models.submodels.interface.sei.base_sei.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.sei.no_sei.NoSEI

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

SEI Growth#
class pybamm.sei.SEIGrowth(param, domain, reaction_loc, options, phase='primary', cracks=False)#

Class for SEI growth.

Most of the models are from Section 5.6.4 of Marquis[1] and references therein.

The ec reaction limited model is from Yang et al.[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • reaction_loc (str) – Where the reaction happens: “x-average” (SPM, SPMe, etc), “full electrode” (full DFN), or “interface” (half-cell model)

  • options (dict) – A dictionary of options to be passed to the model.

  • phase (str, optional) – Phase of the particle (default is “primary”)

  • cracks (bool, optional) – Whether this is a submodel for standard SEI or SEI on cracks

Extends: pybamm.models.submodels.interface.sei.base_sei.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.sei.sei_growth.SEIGrowth

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Total SEI#
class pybamm.sei.TotalSEI(param, domain, options, cracks=False)#

Class summing up contributions to the SEI reaction for cases with primary, secondary, … reactions e.g. silicon-graphite

Parameters:

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.interface.sei.total_sei.TotalSEI

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Oxygen Diffusion#
Base Model#
class pybamm.oxygen_diffusion.BaseModel(param)#

Base class for conservation of mass of oxygen.

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.oxygen_diffusion.base_oxygen_diffusion.BaseModel

Full Model#
class pybamm.oxygen_diffusion.Full(param)#

Class for conservation of mass of oxygen. (Full refers to unreduced by asymptotic methods) In this model, extremely fast oxygen kinetics in the negative electrode imposes zero oxygen concentration there, and so the oxygen variable only lives in the separator and positive electrode. The boundary condition at the negative electrode/ separator interface is homogeneous Dirichlet.

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.oxygen_diffusion.base_oxygen_diffusion.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.oxygen_diffusion.full_oxygen_diffusion.Full

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Leading Order Model#
class pybamm.oxygen_diffusion.LeadingOrder(param)#

Class for conservation of mass of oxygen. (Leading refers to leading order of asymptotic reduction)

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.oxygen_diffusion.base_oxygen_diffusion.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.oxygen_diffusion.leading_oxygen_diffusion.LeadingOrder

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

No Oxygen#
class pybamm.oxygen_diffusion.NoOxygen(param)#

Class for when there is no oxygen

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.oxygen_diffusion.base_oxygen_diffusion.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.oxygen_diffusion.no_oxygen.NoOxygen

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Particle#
Particle Base Model#
class pybamm.particle.BaseParticle(param, domain, options, phase='primary')#

Base class for molar conservation in particles.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.base_particle.BaseParticle

class pybamm.particle.TotalConcentration(param, domain, options, phase='primary')#

Class to calculate total particle concentrations

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.particle.base_particle.BaseParticle

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.total_particle_concentration.TotalConcentration

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Fickian Diffusion#
class pybamm.particle.FickianDiffusion(param, domain, options, phase='primary', x_average=False)#

Class for molar conservation in particles, employing Fick’s law

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

  • x_average (bool) – Whether the particle concentration is averaged over the x-direction

Extends: pybamm.models.submodels.particle.base_particle.BaseParticle

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.fickian_diffusion.FickianDiffusion

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Polynomial Profile#
class pybamm.particle.PolynomialProfile(param, domain, options, phase='primary')#

Class for molar conservation in particles employing Fick’s law, assuming a polynomial concentration profile in r, and allowing variation in the electrode domain. Model equations from Subramanian et al.[1].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.particle.base_particle.BaseParticle

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.polynomial_profile.PolynomialProfile

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

X-averaged Polynomial Profile#
class pybamm.particle.XAveragedPolynomialProfile(param, domain, options, phase='primary')#

Class for molar conservation in a single x-averaged particle employing Fick’s law, with an assumed polynomial concentration profile in r. Model equations from Subramanian et al.[1].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.particle.polynomial_profile.PolynomialProfile

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.x_averaged_polynomial_profile.XAveragedPolynomialProfile

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_algebraic(variables)#

A method to set the differential equations which do not contain a time derivative. Note: this method modifies the state of self.algebraic. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

For single or x-averaged particle models, initial conditions can’t depend on x or r so we take the r- and x-average of the initial conditions.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

MSMR Diffusion#
class pybamm.particle.MSMRDiffusion(param, domain, options, phase='primary', x_average=False)#

Class for molar conservation in particles within the Multi-Species Multi-Reaction framework Baker and Verbrugge[1]. The thermodynamic model is presented in Verbrugge et al.[2], along with parameter values for a number of substitutional materials.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

  • x_average (bool) – Whether the particle concentration is averaged over the x-direction

Extends: pybamm.models.submodels.particle.base_particle.BaseParticle

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle.msmr_diffusion.MSMRDiffusion

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Particle Cracking#
Base Particle Mechanics Model#
class pybamm.particle_mechanics.BaseMechanics(param, domain, options, phase='primary')#

Base class for particle mechanics models, referenced from Ai et al.[1] and Deshpande et al.[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (dict, optional) – Dictionary of either the electrode for “positive” or “Negative”

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle_mechanics.base_mechanics.BaseMechanics

References

Crack Propagation Model#
class pybamm.particle_mechanics.CrackPropagation(param, domain, x_average, options, phase='primary')#

Cracking behaviour in electrode particles. See Ai et al.[1] for mechanical model (thickness change) and Deshpande et al.[2] for cracking model.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • x_average (bool) – Whether to use x-averaged variables (SPM, SPMe, etc) or full variables (DFN)

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.particle_mechanics.base_mechanics.BaseMechanics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle_mechanics.crack_propagation.CrackPropagation

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Swelling Only Model#
class pybamm.particle_mechanics.SwellingOnly(param, domain, options, phase='primary')#

Class for swelling only (no cracking), from Ai et al.[1].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • domain (str) – The domain of the model either ‘Negative’ or ‘Positive’

  • options (dict) – A dictionary of options to be passed to the model. See pybamm.BaseBatteryModel

  • phase (str, optional) – Phase of the particle (default is “primary”)

Extends: pybamm.models.submodels.particle_mechanics.base_mechanics.BaseMechanics

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.particle_mechanics.swelling_only.SwellingOnly

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

References

Porosity#
Base Model#
class pybamm.porosity.BaseModel(param, options)#

Base class for porosity

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.porosity.base_porosity.BaseModel

Constant Porosity#
class pybamm.porosity.Constant(param, options)#

Submodel for constant porosity

Parameters:

param (parameter class) – The parameters to use for this submodel

Extends: pybamm.models.submodels.porosity.base_porosity.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.porosity.constant_porosity.Constant

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Reaction-driven Model#
class pybamm.porosity.ReactionDriven(param, options, x_average)#

Reaction-driven porosity changes as a multiple of SEI/plating thicknesses

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict) – Options dictionary passed from the full model

  • x_average (bool) – Whether to use x-averaged variables (SPM, SPMe, etc) or full variables (DFN)

Extends: pybamm.models.submodels.porosity.base_porosity.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.porosity.reaction_driven_porosity.ReactionDriven

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Reaction-driven Model as an ODE#
class pybamm.porosity.ReactionDrivenODE(param, options, x_average)#

Reaction-driven porosity changes as an ODE

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict) – Options dictionary passed from the full model

  • x_average (bool) – Whether to use x-averaged variables (SPM, SPMe, etc) or full variables (DFN)

Extends: pybamm.models.submodels.porosity.base_porosity.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.porosity.reaction_driven_porosity_ode.ReactionDrivenODE

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Thermal#
Base Thermal#
class pybamm.thermal.BaseThermal(param, options=None)#

Base class for thermal effects

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.base_thermal.BaseThermal

Isothermal Model#
class pybamm.thermal.isothermal.Isothermal(param, options=None)#

Class for isothermal submodel.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.thermal.base_thermal.BaseThermal

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.isothermal.Isothermal

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

Lumped Model#
class pybamm.thermal.lumped.Lumped(param, options=None)#

Class for lumped thermal submodel. For more information see Timms et al.[1] and Marquis[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.thermal.base_thermal.BaseThermal

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.lumped.Lumped

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Pouch Cell#
One Dimensional Model#
class pybamm.thermal.pouch_cell.x_full.OneDimensionalX(param, options=None)#

Class for one-dimensional (x-direction) thermal submodel. Note: this model assumes infinitely large electrical and thermal conductivity in the current collectors, so that the contribution to the Ohmic heating from the current collectors is zero and the boundary conditions are applied at the edges of the electrodes (at x=0 and x=1, in non-dimensional coordinates). For more information see Timms et al.[1] and Marquis[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.thermal.base_thermal.BaseThermal

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.pouch_cell.x_full.OneDimensionalX

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Thermal Model for “1+1D” Pouch Cell#
class pybamm.thermal.pouch_cell.CurrentCollector1D(param, options=None)#

Class for one-dimensional thermal submodel for use in the “1+1D” pouch cell model. The thermal model is averaged in the x-direction and is therefore referred to as ‘x-lumped’. For more information see Timms et al.[1] and Marquis[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.thermal.base_thermal.BaseThermal

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.pouch_cell.pouch_cell_1D_current_collectors.CurrentCollector1D

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

Thermal Model for “2+1D” Pouch Cell#
class pybamm.thermal.pouch_cell.CurrentCollector2D(param, options=None)#

Class for two-dimensional thermal submodel for use in the “2+1D” pouch cell model. The thermal model is averaged in the x-direction and is therefore referred to as ‘x-lumped’. For more information see Timms et al.[1] and Marquis[2].

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.thermal.base_thermal.BaseThermal

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.thermal.pouch_cell.pouch_cell_2D_current_collectors.CurrentCollector2D

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_boundary_conditions(variables)#

A method to set the boundary conditions for the submodel. Note: this method modifies the state of self.boundary_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

References

transport_efficiency#
Base Model#
class pybamm.transport_efficiency.BaseModel(param, component, options=None)#

Base class for transport_efficiency

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • component (str) – The material for the model (‘electrolyte’ or ‘electrode’).

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.transport_efficiency.base_transport_efficiency.BaseModel

Bruggeman Model#
class pybamm.transport_efficiency.Bruggeman(param, component, options=None)#

Submodel for Bruggeman transport_efficiency

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • component (str) – The material for the model (‘electrolyte’ or ‘electrode’).

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.transport_efficiency.base_transport_efficiency.BaseModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.transport_efficiency.bruggeman_transport_efficiency.Bruggeman

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

Equivalent Circuit Elements#
OCV Element#
class pybamm.equivalent_circuit_elements.OCVElement(param, options=None)#

Open-circuit Voltage (OCV) element for equivalent circuits.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.equivalent_circuit_elements.ocv_element.OCVElement

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Resistor Element#
class pybamm.equivalent_circuit_elements.ResistorElement(param, options=None)#

Resistor element for equivalent circuits.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.equivalent_circuit_elements.resistor_element.ResistorElement

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

RC Element#
class pybamm.equivalent_circuit_elements.RCElement(param, element_number, options=None)#

Parallel Resistor-Capacitor (RC) element for equivalent circuits.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • element_number (int) – The number of the element (i.e. whether it is the first, second, third, etc. element)

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.equivalent_circuit_elements.rc_element.RCElement

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Thermal SubModel#
class pybamm.equivalent_circuit_elements.ThermalSubModel(param, options=None)#

Thermal SubModel for use with equivalent circuits.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.equivalent_circuit_elements.thermal.ThermalSubModel

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

get_fundamental_variables()#

A public method that creates and returns the variables in a submodel which can be created independent of other submodels. For example, the electrolyte concentration variables can be created independent of whether any other variables have been defined in the model. As a rule, if a variable can be created without variables from other submodels, then it should be placed in this method.

Returns:

The variables created by the submodel which are independent of variables in other submodels.

Return type:

dict

set_initial_conditions(variables)#

A method to set the initial conditions for the submodel. Note: this method modifies the state of self.initial_conditions. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

set_rhs(variables)#

A method to set the right hand side of the differential equations which contain a time derivative. Note: this method modifies the state of self.rhs. Unless overwritten by a submodel, the default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Voltage Model#
class pybamm.equivalent_circuit_elements.VoltageModel(param, options=None)#

Voltage model for use with equivalent circuits. This model is used to calculate the voltage and total overpotentials from the other elements in the circuit.

Parameters:
  • param (parameter class) – The parameters to use for this submodel

  • options (dict, optional) – A dictionary of options to be passed to the model.

Extends: pybamm.models.submodels.base_submodel.BaseSubModel

View inheritance diagram for this model

Inheritance diagram of pybamm.models.submodels.equivalent_circuit_elements.voltage_model.VoltageModel

get_coupled_variables(variables)#

A public method that creates and returns the variables in a submodel which require variables in other submodels to be set first. For example, the exchange current density requires the concentration in the electrolyte to be created before it can be created. If a variable can be created independent of other submodels then it should be created in ‘get_fundamental_variables’ instead of this method.

Parameters:

variables (dict) – The variables in the whole model.

Returns:

The variables created in this submodel which depend on variables in other submodels.

Return type:

dict

set_events(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 default behaviour of ‘pass’ is used as implemented in pybamm.BaseSubModel.

Parameters:

variables (dict) – The variables in the whole model.

Parameters#

Parameter Values#

class pybamm.ParameterValues(values, chemistry=None)[source]#

The parameter values for a simulation.

Note that this class does not inherit directly from the python dictionary class as this causes issues with saving and loading simulations.

Parameters:

values (dict or string) – Explicit set of parameters, or reference to an inbuilt parameter set If string and matches one of the inbuilt parameter sets, returns that parameter set.

Examples

>>> values = {"some parameter": 1, "another parameter": 2}
>>> param = pybamm.ParameterValues(values)
>>> param["some parameter"]
1
>>> param = pybamm.ParameterValues("Marquis2019")
>>> param["Reference temperature [K]"]
298.15
copy()[source]#

Returns a copy of the parameter values. Makes sure to copy the internal dictionary.

static create_from_bpx(filename, target_soc=1)[source]#
Parameters:
  • filename (str) – The filename of the bpx file

  • target_soc (float, optional) – Target state of charge. Must be between 0 and 1. Default is 1.

Returns:

A parameter values object with the parameters in the bpx file

Return type:

ParameterValues

evaluate(symbol)[source]#

Process and evaluate a symbol.

Parameters:

symbol (pybamm.Symbol) – Symbol or Expression tree to evaluate

Returns:

The evaluated symbol

Return type:

number or array

get(key, default=None)[source]#

Return item corresponding to key if it exists, otherwise return default

items()[source]#

Get the items of the dictionary

keys()[source]#

Get the keys of the dictionary

print_evaluated_parameters(evaluated_parameters, output_file)[source]#

Print a dictionary of evaluated parameters to an output file

Parameters:
  • evaluated_parameters (defaultdict) – The evaluated parameters, for further processing if needed

  • output_file (string, optional) – The file to print parameters to. If None, the parameters are not printed, and this function simply acts as a test that all the parameters can be evaluated

print_parameters(parameters, output_file=None)[source]#

Return dictionary of evaluated parameters, and optionally print these evaluated parameters to an output file.

Parameters:
  • parameters (class or dict containing pybamm.Parameter objects) – Class or dictionary containing all the parameters to be evaluated

  • output_file (string, optional) – The file to print parameters to. If None, the parameters are not printed, and this function simply acts as a test that all the parameters can be evaluated, and returns the dictionary of evaluated parameters.

Returns:

evaluated_parameters – The evaluated parameters, for further processing if needed

Return type:

defaultdict

Notes

A C-rate of 1 C is the current required to fully discharge the battery in 1 hour, 2 C is current to discharge the battery in 0.5 hours, etc

process_boundary_conditions(model)[source]#

Process boundary conditions for a model Boundary conditions are dictionaries {“left”: left bc, “right”: right bc} in general, but may be imposed on the tabs (or not on the tab) for a small number of variables, e.g. {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc “no tab”: no tab bc}.

process_geometry(geometry)[source]#

Assign parameter values to a geometry (inplace).

Parameters:

geometry (dict) – Geometry specs to assign parameter values to

process_model(unprocessed_model, inplace=True)[source]#

Assign parameter values to a model. Currently inplace, could be changed to return a new model.

Parameters:
  • unprocessed_model (pybamm.BaseModel) – Model to assign parameter values for

  • inplace (bool, optional) – If True, replace the parameters in the model in place. Otherwise, return a new model with parameter values set. Default is True.

Raises:

pybamm.ModelError – If an empty model is passed (model.rhs = {} and model.algebraic = {} and model.variables = {})

process_symbol(symbol)[source]#

Walk through the symbol and replace any Parameter with a Value. If a symbol has already been processed, the stored value is returned.

Parameters:

symbol (pybamm.Symbol) – Symbol or Expression tree to set parameters for

Returns:

symbol – Symbol with Parameter instances replaced by Value

Return type:

pybamm.Symbol

search(key, print_values=True)[source]#

Search dictionary for keys containing ‘key’.

See pybamm.FuzzyDict.search().

set_initial_ocps(initial_value, param=None, known_value='cyclable lithium capacity', inplace=True, options=None)[source]#

Set the initial OCP of each electrode, based on the initial SOC or voltage

set_initial_stoichiometries(initial_value, param=None, known_value='cyclable lithium capacity', inplace=True, options=None)[source]#

Set the initial stoichiometry of each electrode, based on the initial SOC or voltage

set_initial_stoichiometry_half_cell(initial_value, param=None, known_value='cyclable lithium capacity', inplace=True, options=None)[source]#

Set the initial stoichiometry of the working electrode, based on the initial SOC or voltage

update(values, check_conflict=False, check_already_exists=True, path='')[source]#

Update parameter dictionary, while also performing some basic checks.

Parameters:
  • values (dict) – Dictionary of parameter values to update parameter dictionary with

  • check_conflict (bool, optional) – Whether to check that a parameter in values has not already been defined in the parameter class when updating it, and if so that its value does not change. This is set to True during initialisation, when parameters are combined from different sources, and is False by default otherwise

  • check_already_exists (bool, optional) – Whether to check that a parameter in values already exists when trying to update it. This is to avoid cases where an intended change in the parameters is ignored due a typo in the parameter name, and is True by default but can be manually overridden.

  • path (string, optional) – Path from which to load functions

values()[source]#

Get the values of the dictionary

Geometric Parameters#

class pybamm.GeometricParameters(options=None)[source]#

Standard geometric parameters

Extends: pybamm.parameters.base_parameters.BaseParameters

Electrical Parameters#

class pybamm.ElectricalParameters[source]#

Standard electrical parameters

Extends: pybamm.parameters.base_parameters.BaseParameters

Thermal Parameters#

class pybamm.ThermalParameters[source]#

Standard thermal parameters

Extends: pybamm.parameters.base_parameters.BaseParameters

Lithium-ion Parameters#

class pybamm.LithiumIonParameters(options=None)[source]#

Standard parameters for lithium-ion battery models

Parameters:

options (dict, optional) – A dictionary of options to be passed to the parameters, see pybamm.BatteryModelOptions.

Extends: pybamm.parameters.base_parameters.BaseParameters

Lead-Acid Parameters#

class pybamm.LeadAcidParameters[source]#

Standard Parameters for lead-acid battery models

Extends: pybamm.parameters.base_parameters.BaseParameters

Parameters Sets#

PyBaMM provides pre-defined parameters for common chemistries, as well as, a growing set of third-party parameter sets.

class pybamm.parameters.parameter_sets.ParameterSets[source]#

Dict-like interface for accessing registered pybamm parameter sets. Access via pybamm.parameter_sets

Examples

Listing available parameter sets:

>>> list(pybamm.parameter_sets)
['Ai2020', 'Chen2020', ...]

Get the docstring for a parameter set:

>>> 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.
...

See also: Adding Parameter Sets

Extends: collections.abc.Mapping

get_docstring(key)[source]#

Return the docstring for the key parameter set

Adding Parameter Sets#

Parameter sets can be added to PyBaMM by creating a python package, and registering a entry point to pybamm_parameter_sets. At a minimum, the package (cell_parameters) should consist of the following:

cell_parameters
├── pyproject.toml        # and/or setup.cfg, setup.py
└── src
    └── cell_parameters
        └── cell_alpha.py

The actual parameter set is defined within cell_alpha.py, as shown below. For an example, see the Marquis2019 parameter sets.

 1import pybamm
 2
 3
 4def get_parameter_values():
 5    """Doc string for cell-alpha"""
 6    return {
 7        "chemistry": "lithium_ion",
 8        "citation": "@book{van1995python, title={Python reference manual}}",
 9        # ...
10    }

Then register get_parameter_values to pybamm_parameter_sets in pyproject.toml:

[project.entry-points.pybamm_parameter_sets]
cell_alpha = "cell_parameters.cell_alpha:get_parameter_values"

If you are using setup.py or setup.cfg to setup your package, please see SetupTools’ documentation for registering entry points.

If you’re willing to open-source your parameter set, let us know, and we can add an entry to Third-Party Parameter Sets.

Third-Party Parameter Sets#

Registered a new parameter set to pybamm_parameter_sets? Let us know, and we’ll update our list.

Bundled Parameter Sets#

PyBaMM provides pre-defined parameter sets for several common chemistries, listed below. See Adding Parameter Sets for information on registering new parameter sets with PyBaMM.

Lead-acid Parameter Sets#
Sulzer2019#

Parameters for BBOXX lead-acid cells, from the paper Sulzer et al.[1] and references therein.

Lithium-ion Parameter Sets#
Ai2020#

Parameters for the Enertech cell (Ai2020), from the papers Ai et al.[2], Rieger et al.[3] and references therein.

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

Chen2020#

Parameters for an LG M50 cell, from the paper Chen et al.[9] and references therein.

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

Chen2020_composite#

Parameters for a composite graphite/silicon negative electrode, from the paper Ai et al.[10], based on the paper Chen et al.[9], and references therein.

SEI parameters are example parameters for composite SEI on silicon/graphite. Both phases use the same values, from the paper Yang et al.[8]

Ecker2015#

Parameters for a Kokam SLPB 75106100 cell, from the papers Ecker et al.[11] and Ecker et al.[12]

The tab placement parameters are taken from measurements in Hales et al.[13]

The thermal material properties are for a 5 Ah power pouch cell by Kokam. The data are extracted from Zhao et al.[14]

Graphite negative electrode parameters#

The fits to data for the electrode and electrolyte properties are those provided by Dr. Simon O’Kane in the paper Richardson et al.[15]

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

Ecker2015_graphite_halfcell#

Parameters for a graphite half-cell based on a Kokam SLPB 75106100 cell, from papers

Ecker, Madeleine, et al. “Parameterization of a physico-chemical model of a lithium-ion battery I. determination of parameters.” Journal of the Electrochemical Society 162.9 (2015): A1836-A1848.

Ecker, Madeleine, et al. “Parameterization of a physico-chemical model of a lithium-ion battery II. Model validation.” Journal of The Electrochemical Society 162.9 (2015): A1849-A1857.

The tab placement parameters are taken from measurements in

Hales, Alastair, et al. “The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries.” Journal of The Electrochemical Society 166.12 (2019): A2383.

The thermal material properties are for a 5 Ah power pouch cell by Kokam. The data are extracted from

Zhao, Y., et al. “Modeling the effects of thermal gradients induced by tab and surface cooling on lithium ion cell performance.”” Journal of The Electrochemical Society, 165.13 (2018): A3169-A3178.

Graphite electrode parameters#

The fits to data for the electrode and electrolyte properties are those provided by Dr. Simon O’Kane in the paper:

Richardson, Giles, et. al. “Generalised single particle models for high-rate operation of graded lithium-ion electrodes: Systematic derivation and validation.” Electrochemica Acta 339 (2020): 135862

SEI parameters are example parameters for SEI growth from the papers:

Ramadass, P., Haran, B., Gomadam, P. M., White, R., & Popov, B. N. (2004). Development of first principles capacity fade model for Li-ion cells. Journal of the Electrochemical Society, 151(2), A196-A203.

Ploehn, H. J., Ramadass, P., & White, R. E. (2004). Solvent diffusion model for aging of lithium-ion battery cells. Journal of The Electrochemical Society, 151(3), A456-A462.

Single, F., Latz, A., & Horstmann, B. (2018). Identifying the mechanism of continued growth of the solid-electrolyte interphase. ChemSusChem, 11(12), 1950-1955.

Safari, M., Morcrette, M., Teyssot, A., & Delacour, C. (2009). Multimodal Physics- Based Aging Model for Life Prediction of Li-Ion Batteries. Journal of The Electrochemical Society, 156(3),

Yang, X., Leng, Y., Zhang, G., Ge, S., Wang, C. (2017). Modeling of lithium plating induced aging of lithium-ion batteries: Transition from linear to nonlinear aging. Journal of Power Sources, 360, 28-40.

Note: this parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

MSMR_Example#

Example parameter values for use with MSMR models. The thermodynamic parameters are for Graphite and NMC622, and are taken from Table 1 of the paper

Mark Verbrugge, Daniel Baker, Brian Koch, Xingcheng Xiao and Wentian Gu. Thermodynamic Model for Substitutional Materials: Application to Lithiated Graphite, Spinel Manganese Oxide, Iron Phosphate, and Layered Nickel-Manganese-Cobalt Oxide. Journal of The Electrochemical Society, 164(11):3243-3253, 2017. doi:10.1149/2.0341708jes.

The remaining value are based on a parameterization of the LG M50 cell, from the paper

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.

and references therein. Verbrugge et al. (2017) does not provide kinetic parameters so we set the reference exchange current density to 5 A.m-2 for the positive electrode reactions and 2.7 A.m-2 for the negative electrode reactions, which are the values used in the Chen et al. (2020) paper. We also assume that the exchange-current density is symmetric. Note: the 4th reaction in the positive electrode gave unphysical results so we set the reference exchange current density and symmetry factor to 1e6 and 1, respectively. The parameter values are intended to serve as an example set to use with the MSMR model and do not claim to match any experimental cycling data.

Marquis2019#

Parameters for a Kokam SLPB78205130H cell, from the paper Marquis et al.[16] and references therein.

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

Mohtat2020#

Parameters for a graphite/NMC532 pouch cell from the paper Mohtat et al.[17] and references therein.

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

SEI parameters#

Parameters for lithium plating are from the paper Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

NCA_Kim2011#

Parameters for a “Nominal Design” graphite/NCA pouch cell, from the paper Kim et al.[18]

Note

Only an effective cell volumetric heat capacity is provided in the paper. We therefore used the values for the density and specific heat capacity reported in the Marquis2019 parameter set in each region and multiplied each density by the ratio of the volumetric heat capacity provided in smith to the calculated value. This ensures that the values produce the same effective cell volumetric heat capacity. This works fine for thermal models that are averaged over the x-direction but not for full (PDE in x direction) thermal models. We do the same for the planar effective thermal conductivity.

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8]

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

OKane2022#

Parameters for an LG M50 cell, from the paper O’Kane et al.[19], based on the paper Chen et al.[9] and references therein.

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

OKane2022_graphite_SiOx_halfcell#

Parameters for the graphite+SiOx negative electrode of a LG M50 cell, from the paper

Simon E. J. O’Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.

based on the paper

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.

and references therein.

Note: the SEI, plating and mechanical parameters do not claim to be representative of the true parameter values. These are merely the parameter values that were used in the referenced papers.

ORegan2022#

Parameters for an LG M50 cell, from the paper O’Regan et al.[20]

Parameters for a LiPF6 in EC:EMC (3:7 w:w) electrolyte are from the paper Landesfeind and Gasteiger[21] and references therein.

Prada2013#

Parameters for an LFP cell, from the paper Prada et al.[22]

Ramadass2004#

Ramadass2004 parameter set. This is a bit of a Frankenstein parameter set and should be used with caution.

Parameters for a graphite negative electrode, Lithium Cobalt Oxide positive electrode, and LiPF6 electrolyte are from the papers Marquis et al.[16], Ramadass et al.[4], and references therein.

Parameters for the separator are from the papers Ecker et al.[11]

The thermal material properties are for a 5 Ah power pouch cell by Kokam. The data are extracted from Lithium Cobalt Oxide positive electrode parameters in Zhao et al.[14]

Parameters for SEI growth are from the papers Ramadass et al.[4] and Safari et al.[7]

Note

Ramadass 2004 has mistakes in units and values of SEI parameters, corrected by Safari2009.

Xu2019#

Parameters for a Kokam SLPB78205130H half-cell, from the paper Xu et al.[23] and references therein. Anode is graphite MCMB 2528. Separator is Celgard 2325. Cathode is lithium Cobalt Oxide. Electrolyte is LiPF6.

Parameters for a LiPF6 electrolyte are from the paper Valøen and Reimers[24]

1C discharge from full#

SEI parameters are example parameters for SEI growth from the papers Ramadass et al.[4], Ploehn et al.[5], Single et al.[6], Safari et al.[7], and Yang et al.[8].

Note

This parameter set does not claim to be representative of the true parameter values. Instead these are parameter values that were used to fit SEI models to observed experimental data in the referenced papers.

References

Process Parameter Data#

pybamm.parameters.process_1D_data(name, path=None)[source]#

Process 1D data from a csv file

pybamm.parameters.process_2D_data(name, path=None)[source]#

Process 2D data from a JSON file

pybamm.parameters.process_2D_data_csv(name, path=None)[source]#

Process 2D data from a csv file. Assumes data is in the form of a three columns and that all data points lie on a regular grid. The first column is assumed to be the ‘slowest’ changing variable and the second column the ‘fastest’ changing variable, which is the C convention for indexing multidimensional arrays (as opposed to the Fortran convention where the ‘fastest’ changing variable comes first).

Parameters:
  • name (str) – The name to be given to the function

  • path (str) – The path to the file where the three dimensional data is stored.

Returns:

formatted_data – A tuple containing the name of the function and the data formatted correctly for use within three-dimensional interpolants.

Return type:

tuple

pybamm.parameters.process_3D_data_csv(name, path=None)[source]#

Process 3D data from a csv file. Assumes data is in the form of four columns and that all data points lie on a regular grid. The first column is assumed to be the ‘slowest’ changing variable and the third column the ‘fastest’ changing variable, which is the C convention for indexing multidimensional arrays (as opposed to the Fortran convention where the ‘fastest’ changing variable comes first).

Parameters:
  • name (str) – The name to be given to the function

  • path (str) – The path to the file where the three dimensional data is stored.

Returns:

formatted_data – A tuple containing the name of the function and the data formatted correctly for use within three-dimensional interpolants.

Return type:

tuple

Geometry#

Geometry#

class pybamm.Geometry(geometry)[source]#

A geometry class to store the details features of the cell geometry.

The values assigned to each domain are dictionaries containing the spatial variables in that domain, along with expression trees giving their min and maximum extents. For example, the following dictionary structure would represent a Geometry with a single domain “negative electrode”, defined using the variable x_n which has a range from 0 to the pre-defined parameter l_n.

{"negative electrode": {x_n: {"min": pybamm.Scalar(0), "max": l_n}}}
Parameters:

geometries (dict) – The dictionary to create the geometry with

Extends: builtins.dict

property parameters#

Returns all the parameters in the geometry

print_parameter_info()[source]#

Prints all the parameters’ information

Battery Geometry#

pybamm.battery_geometry(include_particles=True, options=None, form_factor='pouch')[source]#

A convenience function to create battery geometries.

Parameters:
  • include_particles (bool, optional) – Whether to include particle domains. Can be True (default) or False.

  • options (dict, optional) – Dictionary of model options. Necessary for “particle-size geometry”, relevant for lithium-ion chemistries.

  • form_factor (str, optional) – The form factor of the cell. Can be “pouch” (default) or “cylindrical”.

Returns:

A geometry class for the battery

Return type:

pybamm.Geometry

Meshes#

Meshes#

class pybamm.Mesh(geometry, submesh_types, var_pts)[source]#

Mesh contains a list of submeshes on each subdomain.

Parameters:
  • geometry – contains the geometry of the problem.

  • submesh_types (dict) – contains the types of submeshes to use (e.g. Uniform1DSubMesh)

  • submesh_pts (dict) – contains the number of points on each subdomain

Extends: builtins.dict

add_ghost_meshes()[source]#

Create meshes for potential ghost nodes on either side of each submesh, using self.submeshclass This will be useful for calculating the gradient with Dirichlet BCs.

combine_submeshes(*submeshnames)[source]#

Combine submeshes into a new submesh, using self.submeshclass Raises pybamm.DomainError if submeshes to be combined do not match up (edges are not aligned).

Parameters:

submeshnames (list of str) – The names of the submeshes to be combined

Returns:

submesh – A new submesh with the class defined by self.submeshclass

Return type:

self.submeshclass

class pybamm.SubMesh[source]#

Base submesh class. Contains the position of the nodes, the number of mesh points, and (optionally) information about the tab locations.

class pybamm.MeshGenerator(submesh_type, submesh_params=None)[source]#

Base class for mesh generator objects that are used to generate submeshes.

Parameters:
  • submesh_type (pybamm.SubMesh) – The type of submesh to use (e.g. Uniform1DSubMesh).

  • submesh_params (dict, optional) – Contains any parameters required by the submesh.

0D Sub Mesh#

class pybamm.SubMesh0D(position, npts=None)[source]#

0D submesh class. Contains the position of the node.

Parameters:
  • position (dict) – A dictionary that contains the position of the 0D submesh (a signle point) in space

  • npts (dict, optional) – Number of points to be used. Included for compatibility with other meshes, but ignored by this mesh class

Extends: pybamm.meshes.meshes.SubMesh

1D Sub Meshes#

class pybamm.SubMesh1D(edges, coord_sys, tabs=None)[source]#

1D submesh class. Contains the position of the nodes, the number of mesh points, and (optionally) information about the tab locations.

Parameters:
  • edges (array_like) – An array containing the points corresponding to the edges of the submesh

  • coord_sys (string) – The coordinate system of the submesh

  • tabs (dict, optional) – A dictionary that contains information about the size and location of the tabs

Extends: pybamm.meshes.meshes.SubMesh

class pybamm.Uniform1DSubMesh(lims, npts)[source]#

A class to generate a uniform submesh on a 1D domain

Parameters:
  • lims (dict) – A dictionary that contains the limits of the spatial variables

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1.

Extends: pybamm.meshes.one_dimensional_submeshes.SubMesh1D

class pybamm.Exponential1DSubMesh(lims, npts, side='symmetric', stretch=None)[source]#

A class to generate a submesh on a 1D domain in which the points are clustered close to one or both of boundaries using an exponential formula on the interval [a,b].

If side is “left”, the gridpoints are given by

\[x_{k} = (b-a) + \frac{\mathrm{e}^{\alpha k / N} - 1}{\mathrm{e}^{\alpha} - 1} + a,\]

for k = 1, …, N, where N is the number of nodes.

Is side is “right”, the gridpoints are given by

\[x_{k} = (b-a) + \frac{\mathrm{e}^{-\alpha k / N} - 1}{\mathrm{e}^{-\alpha} - 1} + a,\]

for k = 1, …, N.

If side is “symmetric”, the first half of the interval is meshed using the gridpoints

\[x_{k} = (b/2-a) + \frac{\mathrm{e}^{\alpha k / N} - 1}{\mathrm{e}^{\alpha} - 1} + a,\]

for k = 1, …, N. The grid spacing is then reflected to contruct the grid on the full interval [a,b].

In the above, alpha is a stretching factor. As the number of gridpoints tends to infinity, the ratio of the largest and smallest grid cells tends to exp(alpha).

Parameters:
  • lims (dict) – A dictionary that contains the limits of the spatial variables

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1.

  • side (str, optional) – Whether the points are clustered near to the left or right boundary, or both boundaries. Can be “left”, “right” or “symmetric”. Default is “symmetric”

  • stretch (float, optional) – The factor (alpha) which appears in the exponential. If side is “symmetric” then the default stretch is 1.15. If side is “left” or “right” then the default stretch is 2.3.

Extends: pybamm.meshes.one_dimensional_submeshes.SubMesh1D

class pybamm.Chebyshev1DSubMesh(lims, npts, tabs=None)[source]#

A class to generate a submesh on a 1D domain using Chebyshev nodes on the interval (a, b), given by

\[x_{k} = \frac{1}{2}(a+b) + \frac{1}{2}(b-a) \cos(\frac{2k-1}{2N}\pi),\]

for k = 1, …, N, where N is the number of nodes. Note: this mesh then appends the boundary edges, so that the mesh edges are given by

\[a < x_{1} < ... < x_{N} < b.\]
Parameters:
  • lims (dict) – A dictionary that contains the limits of the spatial variables

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1.

  • tabs (dict, optional) – A dictionary that contains information about the size and location of the tabs

Extends: pybamm.meshes.one_dimensional_submeshes.SubMesh1D

class pybamm.UserSupplied1DSubMesh(lims, npts, edges=None)[source]#

A class to generate a submesh on a 1D domain from a user supplied array of edges.

Parameters:
  • lims (dict) – A dictionary that contains the limits of the spatial variables

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1.

  • edges (array_like) – The array of points which correspond to the edges of the mesh.

Extends: pybamm.meshes.one_dimensional_submeshes.SubMesh1D

2D Sub Meshes#

class pybamm.ScikitSubMesh2D(edges, coord_sys, tabs)[source]#

2D submesh class. Contains information about the 2D finite element mesh. Note: This class only allows for the use of piecewise-linear triangular finite elements.

Parameters:
  • edges (array_like) – An array containing the points corresponding to the edges of the submesh

  • coord_sys (string) – The coordinate system of the submesh

  • tabs (dict, optional) – A dictionary that contains information about the size and location of the tabs

Extends: pybamm.meshes.meshes.SubMesh

on_boundary(y, z, tab)[source]#

A method to get the degrees of freedom corresponding to the subdomains for the tabs.

class pybamm.ScikitUniform2DSubMesh(lims, npts)[source]#

Contains information about the 2D finite element mesh with uniform grid spacing (can be different spacing in y and z). Note: This class only allows for the use of piecewise-linear triangular finite elements.

Parameters:
  • lims (dict) – A dictionary that contains the limits of each spatial variable

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable

Extends: pybamm.meshes.scikit_fem_submeshes.ScikitSubMesh2D

class pybamm.ScikitExponential2DSubMesh(lims, npts, side='top', stretch=2.3)[source]#

Contains information about the 2D finite element mesh generated by taking the tensor product of a uniformly spaced grid in the y direction, and a unequally spaced grid in the z direction in which the points are clustered close to the top boundary using an exponential formula on the interval [a,b]. The gridpoints in the z direction are given by

\[z_{k} = (b-a) + \frac{\exp{-\alpha k / N} - 1}{\exp{-\alpha} - 1} + a,\]

for k = 1, …, N, where N is the number of nodes. Here alpha is a stretching factor. As the number of gridpoints tends to infinity, the ratio of the largest and smallest grid cells tends to exp(alpha).

Note: in the future this will be extended to allow points to be clustered near any of the boundaries.

Parameters:
  • lims (dict) – A dictionary that contains the limits of each spatial variable

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable

  • side (str, optional) – Whether the points are clustered near to a particular boundary. At present, can only be “top”. Default is “top”.

  • stretch (float, optional) – The factor (alpha) which appears in the exponential. Default is 2.3.

Extends: pybamm.meshes.scikit_fem_submeshes.ScikitSubMesh2D

class pybamm.ScikitChebyshev2DSubMesh(lims, npts)[source]#

Contains information about the 2D finite element mesh generated by taking the tensor product of two 1D meshes which use Chebyshev nodes on the interval (a, b), given by

\[x_{k} = \frac{1}{2}(a+b) + \frac{1}{2}(b-a) \cos(\frac{2k-1}{2N}\pi),\]

for k = 1, …, N, where N is the number of nodes. Note: this mesh then appends the boundary edgess, so that the 1D mesh edges are given by

\[a < x_{1} < ... < x_{N} < b.\]

Note: This class only allows for the use of piecewise-linear triangular finite elements.

Parameters:
  • lims (dict) – A dictionary that contains the limits of each spatial variable

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable

Extends: pybamm.meshes.scikit_fem_submeshes.ScikitSubMesh2D

class pybamm.UserSupplied2DSubMesh(lims, npts, y_edges=None, z_edges=None)[source]#

A class to generate a tensor product submesh on a 2D domain by using two user supplied vectors of edges: one for the y-direction and one for the z-direction. Note: this mesh should be created using UserSupplied2DSubMeshGenerator.

Parameters:
  • lims (dict) – A dictionary that contains the limits of the spatial variables

  • npts (dict) – A dictionary that contains the number of points to be used on each spatial variable. Note: the number of nodes (located at the cell centres) is npts, and the number of edges is npts+1.

  • y_edges (array_like) – The array of points which correspond to the edges in the y direction of the mesh.

  • z_edges (array_like) – The array of points which correspond to the edges in the z direction of the mesh.

Extends: pybamm.meshes.scikit_fem_submeshes.ScikitSubMesh2D

Discretisation and spatial methods#

Discretisation#

class pybamm.Discretisation(mesh=None, spatial_methods=None)[source]#

The discretisation class, with methods to process a model and replace Spatial Operators with Matrices and Variables with StateVectors

Parameters:
  • mesh (pybamm.Mesh) – contains all submeshes to be used on each domain

  • spatial_methods (dict) – a dictionary of the spatial methods to be used on each domain. The keys correspond to the model domains and the values to the spatial method.

check_model(model)[source]#

Perform some basic checks to make sure the discretised model makes sense.

check_tab_conditions(symbol, bcs)[source]#

Check any boundary conditions applied on “negative tab”, “positive tab” and “no tab”. For 1D current collector meshes, these conditions are converted into boundary conditions on “left” (tab at z=0) or “right” (tab at z=l_z) depending on the tab location stored in the mesh. For 2D current collector meshes, the boundary conditions can be applied on the tabs directly.

Parameters:
Returns:

The dictionary of boundary conditions, with the keys changed to “left” and “right” where necessary.

Return type:

dict

check_variables(model)[source]#

Check variables in variable list against rhs. Be lenient with size check if the variable in model.variables is broadcasted, or a concatenation (if broadcasted, variable is a multiplication with a vector of ones)

create_mass_matrix(model)[source]#

Creates mass matrix of the discretised model. Note that the model is assumed to be of the form M*y_dot = f(t,y), where M is the (possibly singular) mass matrix.

Parameters:

model (pybamm.BaseModel) – Discretised model. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation})

Returns:

  • pybamm.Matrix – The mass matrix

  • pybamm.Matrix – The inverse of the ode part of the mass matrix (required by solvers which only accept the ODEs in explicit form)

process_boundary_conditions(model)[source]#

Discretise model boundary_conditions, also converting keys to ids

Parameters:

model (pybamm.BaseModel) – Model to dicretise. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation})

Returns:

Dictionary of processed boundary conditions

Return type:

dict

process_dict(var_eqn_dict, ics=False)[source]#

Discretise a dictionary of {variable: equation}, broadcasting if necessary (can be model.rhs, model.algebraic, model.initial_conditions or model.variables).

Parameters:
  • var_eqn_dict (dict) – Equations ({variable: equation} dict) to dicretise (can be model.rhs, model.algebraic, model.initial_conditions or model.variables)

  • ics (bool, optional) – Whether the equations are initial conditions. If True, the equations are scaled by the reference value of the variable, if given

Returns:

new_var_eqn_dict – Discretised equations

Return type:

dict

process_initial_conditions(model)[source]#

Discretise model initial_conditions.

Parameters:

model (pybamm.BaseModel) – Model to dicretise. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation})

Returns:

Tuple of processed_initial_conditions (dict of initial conditions) and concatenated_initial_conditions (numpy array of concatenated initial conditions)

Return type:

tuple

process_model(model, inplace=True, check_model=True, remove_independent_variables_from_rhs=True)[source]#

Discretise a model. Currently inplace, could be changed to return a new model.

Parameters:
  • model (pybamm.BaseModel) – Model to dicretise. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation})

  • inplace (bool, optional) – If True, discretise the model in place. Otherwise, return a new discretised model. Default is True.

  • check_model (bool, optional) – If True, model checks are performed after discretisation. For large systems these checks can be slow, so can be skipped by setting this option to False. When developing, testing or debugging it is recommended to leave this option as True as it may help to identify any errors. Default is True.

  • remove_independent_variables_from_rhs (bool, optional) – If True, model checks to see whether any variables from the RHS are used in any other equation. If a variable meets all of the following criteria (not used anywhere in the model, len(rhs)>1), then the variable is moved to be explicitly integrated when called by the solution object. Default is True.

Returns:

model_disc – The discretised model. Note that if inplace is True, model will have also been discretised in place so model == model_disc. If inplace is False, model != model_disc

Return type:

pybamm.BaseModel

Raises:

pybamm.ModelError – If an empty model is passed (model.rhs = {} and model.algebraic = {} and model.variables = {})

process_rhs_and_algebraic(model)[source]#

Discretise model equations - differential (‘rhs’) and algebraic.

Parameters:

model (pybamm.BaseModel) – Model to dicretise. Must have attributes rhs, initial_conditions and boundary_conditions (all dicts of {variable: equation})

Returns:

Tuple of processed_rhs (dict of processed differential equations), processed_concatenated_rhs, processed_algebraic (dict of processed algebraic equations) and processed_concatenated_algebraic

Return type:

tuple

process_symbol(symbol)[source]#

Discretise operators in model equations. If a symbol has already been discretised, the stored value is returned.

Parameters:

symbol (pybamm.expression_tree.symbol.Symbol) – Symbol to discretise

Returns:

Discretised symbol

Return type:

pybamm.expression_tree.symbol.Symbol

set_internal_boundary_conditions(model)[source]#

A method to set the internal boundary conditions for the submodel. These are required to properly calculate the gradient. Note: this method modifies the state of self.boundary_conditions.

set_variable_slices(variables)[source]#

Sets the slicing for variables.

Parameters:

variables (iterable of pybamm.Variables) – The variables for which to set slices

Spatial Method#

class pybamm.SpatialMethod(options=None)[source]#

A general spatial methods class, with default (trivial) behaviour for some spatial operations. All spatial methods will follow the general form of SpatialMethod in that they contain a method for broadcasting variables onto a mesh, a gradient operator, and a divergence operator.

Parameters:

mesh – Contains all the submeshes for discretisation

boundary_integral(child, discretised_child, region)[source]#

Implements the boundary integral for a spatial method.

Parameters:
  • child (pybamm.Symbol) – The symbol to which is being integrated

  • discretised_child (pybamm.Symbol) – The discretised symbol of the correct size

  • region (str) – The region of the boundary over which to integrate. If region is None (default) the integration is carried out over the entire boundary. If region is negative tab or positive tab then the integration is only carried out over the appropriate part of the boundary corresponding to the tab.

Returns:

Contains the result of acting the discretised boundary integral on the child discretised_symbol

Return type:

class: pybamm.Array

boundary_value_or_flux(symbol, discretised_child, bcs=None)[source]#

Returns the boundary value or flux using the appropriate expression for the spatial method. To do this, we create a sparse vector ‘bv_vector’ that extracts either the first (for side=”left”) or last (for side=”right”) point from ‘discretised_child’.

Parameters:
  • symbol (pybamm.Symbol) – The boundary value or flux symbol

  • discretised_child (pybamm.StateVector) – The discretised variable from which to calculate the boundary value

  • bcs (dict (optional)) – The boundary conditions. If these are supplied and “use bcs” is True in the options, then these will be used to improve the accuracy of the extrapolation.

Returns:

The variable representing the surface value.

Return type:

pybamm.MatrixMultiplication

broadcast(symbol, domains, broadcast_type)[source]#

Broadcast symbol to a specified domain.

Parameters:
  • symbol (pybamm.Symbol) – The symbol to be broadcasted

  • domains (dict of strings) – The domains for broadcasting

  • broadcast_type (str) – The type of broadcast: ‘primary to node’, ‘primary to edges’, ‘secondary to nodes’, ‘secondary to edges’, ‘tertiary to nodes’, ‘tertiary to edges’, ‘full to nodes’ or ‘full to edges’

Returns:

broadcasted_symbol – The discretised symbol of the correct size for the spatial method

Return type:

class: pybamm.Symbol

concatenation(disc_children)[source]#

Discrete concatenation object.

Parameters:

disc_children (list) – List of discretised children

Returns:

Concatenation of the discretised children

Return type:

pybamm.DomainConcatenation

delta_function(symbol, discretised_symbol)[source]#

Implements the delta function on the approriate side for a spatial method.

Parameters:
  • symbol (pybamm.Symbol) – The symbol to which is being integrated

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

divergence(symbol, discretised_symbol, boundary_conditions)[source]#

Implements the divergence for a spatial method.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the gradient of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“left”: left bc, “right”: right bc}})

Returns:

Contains the result of acting the discretised divergence on the child discretised_symbol

Return type:

class: pybamm.Array

evaluate_at(symbol, discretised_child, position)[source]#

Returns the symbol evaluated at a given position in space.

Parameters:
  • symbol (pybamm.Symbol) – The boundary value or flux symbol

  • discretised_child (pybamm.StateVector) – The discretised variable from which to calculate the boundary value

  • position (pybamm.Scalar) – The point in one-dimensional space at which to evaluate the symbol.

Returns:

The variable representing the value at the given point.

Return type:

pybamm.MatrixMultiplication

gradient(symbol, discretised_symbol, boundary_conditions)[source]#

Implements the gradient for a spatial method.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the gradient of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“left”: left bc, “right”: right bc}})

Returns:

Contains the result of acting the discretised gradient on the child discretised_symbol

Return type:

class: pybamm.Array

gradient_squared(symbol, discretised_symbol, boundary_conditions)[source]#

Implements the inner product of the gradient with itself for a spatial method.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the gradient of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“left”: left bc, “right”: right bc}})

Returns:

Contains the result of taking the inner product of the result of acting the discretised gradient on the child discretised_symbol with itself

Return type:

class: pybamm.Array

indefinite_integral(child, discretised_child, direction)[source]#

Implements the indefinite integral for a spatial method.

Parameters:
  • child (pybamm.Symbol) – The symbol to which is being integrated

  • discretised_child (pybamm.Symbol) – The discretised symbol of the correct size

  • direction (str) – The direction of integration

Returns:

Contains the result of acting the discretised indefinite integral on the child discretised_symbol

Return type:

class: pybamm.Array

integral(child, discretised_child, integration_dimension)[source]#

Implements the integral for a spatial method.

Parameters:
  • child (pybamm.Symbol) – The symbol to which is being integrated

  • discretised_child (pybamm.Symbol) – The discretised symbol of the correct size

  • integration_dimension (str, optional) – The dimension in which to integrate (default is “primary”)

Returns:

Contains the result of acting the discretised integral on the child discretised_symbol

Return type:

class: pybamm.Array

internal_neumann_condition(left_symbol_disc, right_symbol_disc, left_mesh, right_mesh)[source]#

A method to find the internal Neumann conditions between two symbols on adjacent subdomains.

Parameters:
  • left_symbol_disc (pybamm.Symbol) – The discretised symbol on the left subdomain

  • right_symbol_disc (pybamm.Symbol) – The discretised symbol on the right subdomain

  • left_mesh (list) – The mesh on the left subdomain

  • right_mesh (list) – The mesh on the right subdomain

laplacian(symbol, discretised_symbol, boundary_conditions)[source]#

Implements the Laplacian for a spatial method.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the gradient of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“left”: left bc, “right”: right bc}})

Returns:

Contains the result of acting the discretised Laplacian on the child discretised_symbol

Return type:

class: pybamm.Array

mass_matrix(symbol, boundary_conditions)[source]#

Calculates the mass matrix for a spatial method.

Parameters:
  • symbol (pybamm.Variable) – The variable corresponding to the equation for which we are calculating the mass matrix.

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“left”: left bc, “right”: right bc}})

Returns:

The (sparse) mass matrix for the spatial method.

Return type:

pybamm.Matrix

process_binary_operators(bin_op, left, right, disc_left, disc_right)[source]#

Discretise binary operators in model equations. Default behaviour is to return a new binary operator with the discretised children.

Parameters:
Returns:

Discretised binary operator

Return type:

pybamm.BinaryOperator

spatial_variable(symbol)[source]#

Convert a pybamm.SpatialVariable node to a linear algebra object that can be evaluated (here, a pybamm.Vector on either the nodes or the edges).

Parameters:

symbol (pybamm.SpatialVariable) – The spatial variable to be discretised.

Returns:

Contains the discretised spatial variable

Return type:

pybamm.Vector

Finite Volume#

class pybamm.FiniteVolume(options=None)[source]#

A class which implements the steps specific to the finite volume method during discretisation.

For broadcast and mass_matrix, we follow the default behaviour from SpatialMethod.

Parameters:

mesh (pybamm.Mesh) – Contains all the submeshes for discretisation

Extends: pybamm.spatial_methods.spatial_method.SpatialMethod

add_ghost_nodes(symbol, discretised_symbol, bcs)[source]#

Add ghost nodes to a symbol.

For Dirichlet bcs, for a boundary condition “y = a at the left-hand boundary”, we concatenate a ghost node to the start of the vector y with value “2*a - y1” where y1 is the value of the first node. Similarly for the right-hand boundary condition.

For Neumann bcs no ghost nodes are added. Instead, the exact value provided by the boundary condition is used at the cell edge when calculating the gradient (see pybamm.FiniteVolume.add_neumann_values()).

Parameters:
  • symbol (pybamm.SpatialVariable) – The variable to be discretised

  • discretised_symbol (pybamm.Vector) – Contains the discretised variable

  • bcs (dict of tuples (pybamm.Scalar, str)) – Dictionary (with keys “left” and “right”) of boundary conditions. Each boundary condition consists of a value and a flag indicating its type (e.g. “Dirichlet”)

Returns:

Matrix @ discretised_symbol + bcs_vector. When evaluated, this gives the discretised_symbol, with appropriate ghost nodes concatenated at each end.

Return type:

pybamm.Symbol

add_neumann_values(symbol, discretised_gradient, bcs, domain)[source]#

Add the known values of the gradient from Neumann boundary conditions to the discretised gradient.

Dirichlet bcs are implemented using ghost nodes, see pybamm.FiniteVolume.add_ghost_nodes().

Parameters:
  • symbol (pybamm.SpatialVariable) – The variable to be discretised

  • discretised_gradient (pybamm.Vector) – Contains the discretised gradient of symbol

  • bcs (dict of tuples (pybamm.Scalar, str)) – Dictionary (with keys “left” and “right”) of boundary conditions. Each boundary condition consists of a value and a flag indicating its type (e.g. “Dirichlet”)

  • domain (list of strings) – The domain of the gradient of the symbol (may include ghost nodes)

Returns:

Matrix @ discretised_gradient + bcs_vector. When evaluated, this gives the discretised_gradient, with the values of the Neumann boundary conditions concatenated at each end (if given).

Return type:

pybamm.Symbol

boundary_value_or_flux(symbol, discretised_child, bcs=None)[source]#

Uses extrapolation to get the boundary value or flux of a variable in the Finite Volume Method.

See pybamm.SpatialMethod.boundary_value()

concatenation(disc_children)[source]#

Discrete concatenation, taking edge_to_node for children that evaluate on edges. See pybamm.SpatialMethod.concatenation()

definite_integral_matrix(child, vector_type='row', integration_dimension='primary')[source]#

Matrix for finite-volume implementation of the definite integral in the primary dimension

\[I = \int_{a}^{b}\!f(s)\,ds\]

for where \(a\) and \(b\) are the left-hand and right-hand boundaries of the domain respectively

Parameters:
  • child (pybamm.Symbol) – The symbol being integrated

  • vector_type (str, optional) – Whether to return a row or column vector in the primary dimension (default is row)

  • integration_dimension (str, optional) – The dimension in which to integrate (default is “primary”)

Returns:

The finite volume integral matrix for the domain

Return type:

pybamm.Matrix

delta_function(symbol, discretised_symbol)[source]#

Delta function. Implemented as a vector whose only non-zero element is the first (if symbol.side = “left”) or last (if symbol.side = “right”), with appropriate value so that the integral of the delta function across the whole domain is the same as the integral of the discretised symbol across the whole domain.

See pybamm.SpatialMethod.delta_function()

divergence(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the divergence operator. See pybamm.SpatialMethod.divergence()

divergence_matrix(domains)[source]#

Divergence matrix for finite volumes in the appropriate domain. Equivalent to div(N) = (N[1:] - N[:-1])/dx

Parameters:

domains (dict) – The domain(s) and auxiliary domain in which to compute the divergence matrix

Returns:

The (sparse) finite volume divergence matrix for the domain

Return type:

pybamm.Matrix

edge_to_node(discretised_symbol, method='arithmetic')[source]#

Convert a discretised symbol evaluated on the cell edges to a discretised symbol evaluated on the cell nodes. See pybamm.FiniteVolume.shift()

evaluate_at(symbol, discretised_child, position)[source]#

Returns the symbol evaluated at a given position in space.

Parameters:
  • symbol (pybamm.Symbol) – The boundary value or flux symbol

  • discretised_child (pybamm.StateVector) – The discretised variable from which to calculate the boundary value

  • position (pybamm.Scalar) – The point in one-dimensional space at which to evaluate the symbol.

Returns:

The variable representing the value at the given point.

Return type:

pybamm.MatrixMultiplication

gradient(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the gradient operator. See pybamm.SpatialMethod.gradient()

gradient_matrix(domain, domains)[source]#

Gradient matrix for finite volumes in the appropriate domain. Equivalent to grad(y) = (y[1:] - y[:-1])/dx

Parameters:

domains (list) – The domain in which to compute the gradient matrix, including ghost nodes

Returns:

The (sparse) finite volume gradient matrix for the domain

Return type:

pybamm.Matrix

indefinite_integral(child, discretised_child, direction)[source]#

Implementation of the indefinite integral operator.

indefinite_integral_matrix_edges(domains, direction)[source]#

Matrix for finite-volume implementation of the indefinite integral where the integrand is evaluated on mesh edges (shape (n+1, 1)). The integral will then be evaluated on mesh nodes (shape (n, 1)).

Parameters:
  • domains (dict) – The domain(s) and auxiliary domains of integration

  • direction (str) – The direction of integration (forward or backward). See notes.

Returns:

The finite volume integral matrix for the domain

Return type:

pybamm.Matrix

Notes

Forward integral

\[F(x) = \int_0^x\!f(u)\,du\]

The indefinite integral must satisfy the following conditions:

  • \(F(0) = 0\)

  • \(f(x) = \frac{dF}{dx}\)

or, in discrete form,

  • BoundaryValue(F, “left”) = 0, i.e. \(3*F_0 - F_1 = 0\)

  • \(f_{i+1/2} = (F_{i+1} - F_i) / dx_{i+1/2}\)

Hence we must have

  • \(F_0 = du_{1/2} * f_{1/2} / 2\)

  • \(F_{i+1} = F_i + du_{i+1/2} * f_{i+1/2}\)

Note that \(f_{-1/2}\) and \(f_{end+1/2}\) are included in the discrete integrand vector f, so we add a column of zeros at each end of the indefinite integral matrix to ignore these.

Backward integral

\[F(x) = \int_x^{end}\!f(u)\,du\]

The indefinite integral must satisfy the following conditions:

  • \(F(end) = 0\)

  • \(f(x) = -\frac{dF}{dx}\)

or, in discrete form,

  • BoundaryValue(F, “right”) = 0, i.e. \(3*F_{end} - F_{end-1} = 0\)

  • \(f_{i+1/2} = -(F_{i+1} - F_i) / dx_{i+1/2}\)

Hence we must have

  • \(F_{end} = du_{end+1/2} * f_{end-1/2} / 2\)

  • \(F_{i-1} = F_i + du_{i-1/2} * f_{i-1/2}\)

Note that \(f_{-1/2}\) and \(f_{end+1/2}\) are included in the discrete integrand vector f, so we add a column of zeros at each end of the indefinite integral matrix to ignore these.

indefinite_integral_matrix_nodes(domains, direction)[source]#

Matrix for finite-volume implementation of the (backward) indefinite integral where the integrand is evaluated on mesh nodes (shape (n, 1)). The integral will then be evaluated on mesh edges (shape (n+1, 1)). This is just a straightforward (backward) cumulative sum of the integrand

Parameters:
  • domains (dict) – The domain(s) and auxiliary domains of integration

  • direction (str) – The direction of integration (forward or backward)

Returns:

The finite volume integral matrix for the domain

Return type:

pybamm.Matrix

integral(child, discretised_child, integration_dimension)[source]#

Vector-vector dot product to implement the integral operator.

internal_neumann_condition(left_symbol_disc, right_symbol_disc, left_mesh, right_mesh)[source]#

A method to find the internal Neumann conditions between two symbols on adjacent subdomains.

Parameters:
  • left_symbol_disc (pybamm.Symbol) – The discretised symbol on the left subdomain

  • right_symbol_disc (pybamm.Symbol) – The discretised symbol on the right subdomain

  • left_mesh (list) – The mesh on the left subdomain

  • right_mesh (list) – The mesh on the right subdomain

laplacian(symbol, discretised_symbol, boundary_conditions)[source]#

Laplacian operator, implemented as div(grad(.)) See pybamm.SpatialMethod.laplacian()

node_to_edge(discretised_symbol, method='arithmetic')[source]#

Convert a discretised symbol evaluated on the cell nodes to a discretised symbol evaluated on the cell edges. See pybamm.FiniteVolume.shift()

process_binary_operators(bin_op, left, right, disc_left, disc_right)[source]#

Discretise binary operators in model equations. Performs appropriate averaging of diffusivities if one of the children is a gradient operator, so that discretised sizes match up. For this averaging we use the harmonic mean [1].

[1] Recktenwald, Gerald. “The control-volume finite-difference approximation to the diffusion equation.” (2012).

Parameters:
Returns:

Discretised binary operator

Return type:

pybamm.BinaryOperator

shift(discretised_symbol, shift_key, method)[source]#

Convert a discretised symbol evaluated at edges/nodes, to a discretised symbol evaluated at nodes/edges. Can be the arithmetic mean or the harmonic mean.

Note: when computing fluxes at cell edges it is better to take the harmonic mean based on [1].

[1] Recktenwald, Gerald. “The control-volume finite-difference approximation to the diffusion equation.” (2012).

Parameters:
  • discretised_symbol (pybamm.Symbol) – Symbol to be averaged. When evaluated, this symbol returns either a scalar or an array of shape (n,) or (n+1,), where n is the number of points in the mesh for the symbol’s domain (n = self.mesh[symbol.domain].npts)

  • shift_key (str) – Whether to shift from nodes to edges (“node to edge”), or from edges to nodes (“edge to node”)

  • method (str) – Whether to use the “arithmetic” or “harmonic” mean

Returns:

Averaged symbol. When evaluated, this returns either a scalar or an array of shape (n+1,) (if shift_key = “node to edge”) or (n,) (if shift_key = “edge to node”)

Return type:

pybamm.Symbol

spatial_variable(symbol)[source]#

Creates a discretised spatial variable compatible with the FiniteVolume method.

Parameters:

symbol (pybamm.SpatialVariable) – The spatial variable to be discretised.

Returns:

Contains the discretised spatial variable

Return type:

pybamm.Vector

upwind_or_downwind(symbol, discretised_symbol, bcs, direction)[source]#

Implement an upwinding operator. Currently, this requires the symbol to have a Dirichlet boundary condition on the left side (for upwinding) or right side (for downwinding).

Parameters:
  • symbol (pybamm.SpatialVariable) – The variable to be discretised

  • discretised_gradient (pybamm.Vector) – Contains the discretised gradient of symbol

  • bcs (dict of tuples (pybamm.Scalar, str)) – Dictionary (with keys “left” and “right”) of boundary conditions. Each boundary condition consists of a value and a flag indicating its type (e.g. “Dirichlet”)

  • direction (str) – Direction in which to apply the operator (upwind or downwind)

Spectral Volume#

class pybamm.SpectralVolume(options=None, order=2)[source]#

A class which implements the steps specific to the Spectral Volume discretisation. It is implemented in such a way that it is very similar to FiniteVolume; that comes at the cost that it is only compatible with the SpectralVolume1DSubMesh (which is a certain subdivision of any 1D mesh, so it shouldn’t be a problem).

For broadcast and mass_matrix, we follow the default behaviour from SpatialMethod. For spatial_variable, divergence, divergence_matrix, laplacian, integral, definite_integral_matrix, indefinite_integral, indefinite_integral_matrix, indefinite_integral_matrix_nodes, indefinite_integral_matrix_edges, delta_function we follow the behaviour from FiniteVolume. This is possible since the node values are integral averages with Spectral Volume, just as with Finite Volume. delta_function assigns the integral value to a CV instead of a SV this way, but that doesn’t matter too much. Additional methods that are inherited by FiniteVolume which technically are not suitable for Spectral Volume are boundary_value_or_flux, process_binary_operators, concatenation, node_to_edge, edge_to_node and shift. While node_to_edge (as well as boundary_value_or_flux and process_binary_operators) could utilize the reconstruction approach of Spectral Volume, the inverse edge_to_node would still have to fall back to the Finite Volume behaviour. So these are simply inherited for consistency. boundary_value_or_flux might not benefit from the reconstruction approach at all, as it seems to only preprocess symbols.

Parameters:

mesh (pybamm.Mesh) – Contains all the submeshes for discretisation

Extends: pybamm.spatial_methods.finite_volume.FiniteVolume

chebyshev_collocation_points(noe, a=-1.0, b=1.0)[source]#

Calculates Chebyshev collocation points in descending order.

Parameters:
  • noe (integer) – The number of the collocation points. “number of edges”

  • a (float) – Left end of the interval on which the Chebyshev collocation points are constructed. Default is -1.

  • b (float) – Right end of the interval on which the Chebyshev collocation points are constructed. Default is 1.

Returns:

  • numpy.array

  • Chebyshev collocation points on [a,b].

chebyshev_differentiation_matrices(noe, dod)[source]#

Chebyshev differentiation matrices, from Baltensperger and Trummer[1].

Parameters:
  • noe (integer) – The number of the collocation points. “number of edges”

  • dod (integer) – The maximum order of differentiation for which a differentiation matrix shall be calculated. Note that it has to be smaller than ‘noe’. “degrees of differentiation”

Returns:

The differentiation matrices in ascending order of differentiation order. With exact arithmetic, the diff. matrix of order p would just be the pth matrix power of the diff. matrix of order 1. This method computes the higher orders in a more numerically stable way.

Return type:

list(numpy.array)

cv_boundary_reconstruction_matrix(domains)[source]#

“Broadcasts” the basic edge value reconstruction matrix to the actual shape of the discretised symbols. Note that the product of this and a discretised symbol is a vector which represents duplicate values for all inner SV edges. These are the reconstructed values from both sides.

Parameters:

domains (dict) – The domains in which to compute the gradient matrix

Returns:

The (sparse) CV reconstruction matrix for the domain

Return type:

pybamm.Matrix

cv_boundary_reconstruction_sub_matrix()[source]#

Coefficients for reconstruction of a function through averages. The resulting matrix is scale-invariant Wang[2].

gradient(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the gradient operator. See pybamm.SpatialMethod.gradient()

gradient_matrix(domain, domains)[source]#

Gradient matrix for Spectral Volume in the appropriate domain. Note that it contains the averaging of the duplicate SV edge gradient values, such that the product of it and a reconstructed discretised symbol simply represents CV edge values. On its own, it only works on non-concatenated domains, since only then the boundary conditions ensure correct behaviour. More generally, it only works if gradients are a result of boundary conditions rather than continuity conditions. For example, two adjacent SVs with gradient zero in each of them but with different variable values will have zero gradient between them. This is fixed with “penalty_matrix”.

Parameters:

domains (dict) – The domains in which to compute the gradient matrix

Returns:

The (sparse) Spectral Volume gradient matrix for the domain

Return type:

pybamm.Matrix

penalty_matrix(domains)[source]#

Penalty matrix for Spectral Volume in the appropriate domain. This works the same as the “gradient_matrix” of FiniteVolume does, just between SVs and not between CVs. Think of it as a continuity penalty.

Parameters:

domains (dict) – The domains in which to compute the gradient matrix

Returns:

The (sparse) Spectral Volume penalty matrix for the domain

Return type:

pybamm.Matrix

replace_dirichlet_values(symbol, discretised_symbol, bcs)[source]#

Replace the reconstructed value at Dirichlet boundaries with the boundary condition.

Parameters:
  • symbol (pybamm.SpatialVariable) – The variable to be discretised

  • discretised_symbol (pybamm.Vector) – Contains the discretised variable

  • bcs (dict of tuples (pybamm.Scalar, str)) – Dictionary (with keys “left” and “right”) of boundary conditions. Each boundary condition consists of a value and a flag indicating its type (e.g. “Dirichlet”)

Returns:

Matrix @ discretised_symbol + bcs_vector. When evaluated, this gives the discretised_symbol, with its boundary values replaced by the Dirichlet boundary conditions.

Return type:

pybamm.Symbol

replace_neumann_values(symbol, discretised_gradient, bcs)[source]#

Replace the known values of the gradient from Neumann boundary conditions into the discretised gradient.

Parameters:
  • symbol (pybamm.SpatialVariable) – The variable to be discretised

  • discretised_gradient (pybamm.Vector) – Contains the discretised gradient of symbol

  • bcs (dict of tuples (pybamm.Scalar, str)) – Dictionary (with keys “left” and “right”) of boundary conditions. Each boundary condition consists of a value and a flag indicating its type (e.g. “Dirichlet”)

Returns:

Matrix @ discretised_gradient + bcs_vector. When evaluated, this gives the discretised_gradient, with its boundary values replaced by the Neumann boundary conditions.

Return type:

pybamm.Symbol

References

Scikit Finite Elements#

class pybamm.ScikitFiniteElement(options=None)[source]#

A class which implements the steps specific to the finite element method during discretisation. The class uses scikit-fem to discretise the problem to obtain the mass and stiffness matrices. At present, this class is only used for solving the Poisson problem -grad^2 u = f in the y-z plane (i.e. not the through-cell direction).

For broadcast we follow the default behaviour from SpatialMethod.

Parameters:

mesh (pybamm.Mesh) – Contains all the submeshes for discretisation

Extends: pybamm.spatial_methods.spatial_method.SpatialMethod

assemble_mass_form(symbol, boundary_conditions, region='interior')[source]#

Assembles the form of the finite element mass matrix over the domain interior or boundary.

Parameters:
  • symbol (pybamm.Variable) – The variable corresponding to the equation for which we are calculating the mass matrix.

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

  • region (str, optional) – The domain over which to assemble the mass matrix form. Can be “interior” (default) or “boundary”.

Returns:

The (sparse) mass matrix for the spatial method.

Return type:

pybamm.Matrix

bc_apply(M, boundary, zero=False)[source]#

Adjusts the assemled finite element matrices to account for boundary conditons.

Parameters:
  • M (scipy.sparse.coo_matrix) – The assemled finite element matrix to adjust.

  • boundary (numpy.array) – Array of the indicies which correspond to the boundary.

  • zero (bool, optional) – If True, the rows of M given by the indicies in boundary are set to zero. If False, the diagonal element is set to one. default is False.

boundary_integral(child, discretised_child, region)[source]#

Implementation of the boundary integral operator. See pybamm.SpatialMethod.boundary_integral()

boundary_integral_vector(domain, region)[source]#

A node in the expression tree representing an integral operator over the boundary of a domain

\[I = \int_{\partial a}\!f(u)\,du,\]

where \(\partial a\) is the boundary of the domain, and \(u\in\text{domain boundary}\).

Parameters:
  • domain (list) – The domain(s) of the variable in the integrand

  • region (str) – The region of the boundary over which to integrate. If region is entire the integration is carried out over the entire boundary. If region is negative tab or positive tab then the integration is only carried out over the appropriate part of the boundary corresponding to the tab.

Returns:

The finite element integral vector for the domain

Return type:

pybamm.Matrix

boundary_mass_matrix(symbol, boundary_conditions)[source]#

Calculates the mass matrix for the finite element method assembled over the boundary.

Parameters:
  • symbol (pybamm.Variable) – The variable corresponding to the equation for which we are calculating the mass matrix.

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

The (sparse) mass matrix for the spatial method.

Return type:

pybamm.Matrix

boundary_value_or_flux(symbol, discretised_child, bcs=None)[source]#

Returns the average value of the symbol over the negative tab (“negative tab”) or the positive tab (“positive tab”) in the Finite Element Method.

Overwrites the default pybamm.SpatialMethod.boundary_value()

definite_integral_matrix(child, vector_type='row')[source]#

Matrix for finite-element implementation of the definite integral over the entire domain

\[I = \int_{\Omega}\!f(s)\,dx\]

for where \(\Omega\) is the domain.

Parameters:
  • child (pybamm.Symbol) – The symbol being integrated

  • vector_type (str, optional) – Whether to return a row or column vector (default is row)

Returns:

The finite element integral vector for the domain

Return type:

pybamm.Matrix

divergence(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the divergence operator. See pybamm.SpatialMethod.divergence()

gradient(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the gradient operator. The gradient w of the function u is approximated by the finite element method using the same function space as u, i.e. we solve w = grad(u), which corresponds to the weak form w*v*dx = grad(u)*v*dx, where v is a suitable test function.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the Laplacian of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

A concatenation that contains the result of acting the discretised gradient on the child discretised_symbol. The first column corresponds to the y-component of the gradient and the second column corresponds to the z component of the gradient.

Return type:

class: pybamm.Concatenation

gradient_matrix(symbol, boundary_conditions)[source]#

Gradient matrix for finite elements in the appropriate domain.

Parameters:
  • symbol (pybamm.Symbol) – The symbol for which we want to calculate the gradient matrix

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

The (sparse) finite element gradient matrix for the domain

Return type:

pybamm.Matrix

gradient_squared(symbol, discretised_symbol, boundary_conditions)[source]#

Multiplication to implement the inner product of the gradient operator with itself. See pybamm.SpatialMethod.gradient_squared()

indefinite_integral(child, discretised_child, direction)[source]#

Implementation of the indefinite integral operator. The input discretised child must be defined on the internal mesh edges. See pybamm.SpatialMethod.indefinite_integral()

integral(child, discretised_child, integration_dimension)[source]#

Vector-vector dot product to implement the integral operator. See pybamm.SpatialMethod.integral()

laplacian(symbol, discretised_symbol, boundary_conditions)[source]#

Matrix-vector multiplication to implement the Laplacian operator.

Parameters:
  • symbol (pybamm.Symbol) – The symbol that we will take the Laplacian of.

  • discretised_symbol (pybamm.Symbol) – The discretised symbol of the correct size

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

Contains the result of acting the discretised gradient on the child discretised_symbol

Return type:

class: pybamm.Array

mass_matrix(symbol, boundary_conditions)[source]#

Calculates the mass matrix for the finite element method.

Parameters:
  • symbol (pybamm.Variable) – The variable corresponding to the equation for which we are calculating the mass matrix.

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

The (sparse) mass matrix for the spatial method.

Return type:

pybamm.Matrix

spatial_variable(symbol)[source]#

Creates a discretised spatial variable compatible with the FiniteElement method.

Parameters:

symbol (pybamm.SpatialVariable) – The spatial variable to be discretised.

Returns:

Contains the discretised spatial variable

Return type:

pybamm.Vector

stiffness_matrix(symbol, boundary_conditions)[source]#

Laplacian (stiffness) matrix for finite elements in the appropriate domain.

Parameters:
  • symbol (pybamm.Symbol) – The symbol for which we want to calculate the Laplacian matrix

  • boundary_conditions (dict) – The boundary conditions of the model ({symbol: {“negative tab”: neg. tab bc, “positive tab”: pos. tab bc}})

Returns:

The (sparse) finite element stiffness matrix for the domain

Return type:

pybamm.Matrix

Zero Dimensional Spatial Method#

class pybamm.ZeroDimensionalSpatialMethod(options=None)[source]#

A discretisation class for the zero dimensional mesh

Parameters:

mesh – Contains all the submeshes for discretisation

Extends: pybamm.spatial_methods.spatial_method.SpatialMethod

boundary_value_or_flux(symbol, discretised_child, bcs=None)[source]#

In 0D, the boundary value is the identity operator. See SpatialMethod.boundary_value_or_flux()

indefinite_integral(child, discretised_child, direction)[source]#

Calculates the zero-dimensional indefinite integral. If ‘direction’ is forward, this is the identity operator. If ‘direction’ is backward, this is the negation operator.

integral(child, discretised_child, integration_dimension)[source]#

Calculates the zero-dimensional integral, i.e. the identity operator

mass_matrix(symbol, boundary_conditions)[source]#

Calculates the mass matrix for a spatial method. Since the spatial method is zero dimensional, this is simply the number 1.

Solvers#

Base Solver#

class pybamm.BaseSolver(method=None, rtol=1e-06, atol=1e-06, root_method=None, root_tol=1e-06, extrap_tol=None, output_variables=[])[source]#

Solve a discretised model.

Parameters:
  • method (str, optional) – The method to use for integration, specific to each solver

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • root_method (str or pybamm algebraic solver class, optional) – The method to use to find initial conditions (for DAE solvers). If a solver class, must be an algebraic solver class. If “casadi”, the solver uses casadi’s Newton rootfinding algorithm to find initial conditions. Otherwise, the solver uses ‘scipy.optimize.root’ with method specified by ‘root_method’ (e.g. “lm”, “hybr”, …)

  • root_tol (float, optional) – The tolerance for the initial-condition solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not. Default is 0.

  • output_variables (list[str], optional) – List of variables to calculate and return. If none are specified then the complete state vector is returned (can be very large) (default is [])

calculate_consistent_state(model, time=0, inputs=None)[source]#

Calculate consistent state for the algebraic equations through root-finding. model.y0 is used as the initial guess for rootfinding

Parameters:
  • model (pybamm.BaseModel) – The model for which to calculate initial conditions.

  • time (float) – The time at which to calculate the states

  • inputs (dict, optional) – Any input parameters to pass to the model when solving

Returns:

y0_consistent – Initial conditions that are consistent with the algebraic equations (roots of the algebraic equations). If self.root_method == None then returns model.y0.

Return type:

array-like, same shape as y0_guess

check_extrapolation(solution, events)[source]#

Check if extrapolation occurred for any of the interpolants. Note that with the current approach (evaluating all the events at the solution times) some extrapolations might not be found if they only occurred for a small period of time.

Parameters:
copy()[source]#

Returns a copy of the solver

get_termination_reason(solution, events)[source]#

Identify the cause for termination. In particular, if the solver terminated due to an event, (try to) pinpoint which event was responsible. If an event occurs the event time and state are added to the solution object. Note that the current approach (evaluating all the events and then finding which one is smallest at the final timestep) is pretty crude, but is the easiest one that works for all the different solvers.

Parameters:
set_up(model, inputs=None, t_eval=None, ics_only=False)[source]#

Unpack model, perform checks, and calculate jacobian.

Parameters:
  • model (pybamm.BaseModel) – The model whose solution to calculate. Must have attributes rhs and initial_conditions

  • inputs (dict, optional) – Any input parameters to pass to the model when solving

  • t_eval (numeric type, optional) – The times (in seconds) at which to compute the solution

solve(model, t_eval=None, inputs=None, nproc=None, calculate_sensitivities=False)[source]#

Execute the solver setup and calculate the solution of the model at specified times.

Parameters:
  • model (pybamm.BaseModel) – The model whose solution to calculate. Must have attributes rhs and initial_conditions. All calls to solve must pass in the same model or an error is raised

  • t_eval (numeric type) – The times (in seconds) at which to compute the solution

  • inputs (dict or list, optional) – A dictionary or list of dictionaries describing any input parameters to pass to the model when solving

  • nproc (int, optional) – Number of processes to use when solving for more than one set of input parameters. Defaults to value returned by “os.cpu_count()”.

  • calculate_sensitivities (list of str or bool) – If true, solver calculates sensitivities of all input parameters. If only a subset of sensitivities are required, can also pass a list of input parameter names

Returns:

If type of inputs is list, return a list of corresponding pybamm.Solution objects.

Return type:

pybamm.Solution or list of pybamm.Solution objects.

Raises:
  • pybamm.ModelError – If an empty model is passed (model.rhs = {} and model.algebraic={} and model.variables = {})

  • RuntimeError – If multiple calls to solve pass in different models

step(old_solution, model, dt, npts=2, inputs=None, save=True)[source]#

Step the solution of the model forward by a given time increment. The first time this method is called it executes the necessary setup by calling self.set_up(model).

Parameters:
  • old_solution (pybamm.Solution or None) – The previous solution to be added to. If None, a new solution is created.

  • model (pybamm.BaseModel) – The model whose solution to calculate. Must have attributes rhs and initial_conditions

  • dt (numeric type) – The timestep (in seconds) over which to step the solution

  • npts (int, optional) – The number of points at which the solution will be returned during the step dt. default is 2 (returns the solution at t0 and t0 + dt).

  • inputs (dict, optional) – Any input parameters to pass to the model when solving

  • save (bool) – Turn on to store the solution of all previous timesteps

Raises:

pybamm.ModelError – If an empty model is passed (model.rhs = {} and model.algebraic = {} and model.variables = {})

Dummy Solver#

class pybamm.DummySolver[source]#

Dummy solver class for empty models.

Extends: pybamm.solvers.base_solver.BaseSolver

Scipy Solver#

class pybamm.ScipySolver(method='BDF', rtol=1e-06, atol=1e-06, extrap_tol=None, extra_options=None)[source]#

Solve a discretised model, using scipy.integrate.solve_ivp.

Parameters:
  • method (str, optional) – The method to use in solve_ivp (default is “BDF”)

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not (default is 0).

  • extra_options (dict, optional) – Any options to pass to the solver. Please consult SciPy documentation for details.

Extends: pybamm.solvers.base_solver.BaseSolver

JAX Solver#

class pybamm.JaxSolver(method='RK45', root_method=None, rtol=1e-06, atol=1e-06, extrap_tol=None, extra_options=None)[source]#

Solve a discretised model using a JAX compiled solver.

Note: this solver will not work with models that have

termination events or are not converted to jax format

Raises:
Parameters:
  • method (str) – ‘RK45’ (default) uses jax.experimental.odeint ‘BDF’ uses custom jax_bdf_integrate (see jax_bdf_integrate.py for details)

  • root_method (str, optional) – Method to use to calculate consistent initial conditions. By default this uses the newton chord method internal to the jax bdf solver, otherwise choose from the set of default options defined in docs for pybamm.BaseSolver

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not (default is 0).

  • extra_options (dict, optional) – Any options to pass to the solver. Please consult JAX documentation for details.

Extends: pybamm.solvers.base_solver.BaseSolver

create_solve(model, t_eval)[source]#

Return a compiled JAX function that solves an ode model with input arguments.

Parameters:
  • model (pybamm.BaseModel) – The model whose solution to calculate.

  • t_eval (numpy.array, size (k,)) – The times at which to compute the solution

Returns:

A function with signature f(inputs), where inputs are a dict containing any input parameters to pass to the model when solving

Return type:

function

get_solve(model, t_eval)[source]#

Return a compiled JAX function that solves an ode model with input arguments.

Parameters:
  • model (pybamm.BaseModel) – The model whose solution to calculate.

  • t_eval (numpy.array, size (k,)) – The times at which to compute the solution

Returns:

A function with signature f(inputs), where inputs are a dict containing any input parameters to pass to the model when solving

Return type:

function

pybamm.jax_bdf_integrate(func, y0, t_eval, *args, rtol=1e-06, atol=1e-06, mass=None)[source]#

Backward Difference formula (BDF) implicit multistep integrator. The basic algorithm is derived in Byrne and Hindmarsh[1]. This particular implementation follows that implemented in the Matlab routine ode15s described in Shampine and Reichelt[2] and the SciPy implementation Virtanen et al.[3] which features the NDF formulas for improved stability, with associated differences in the error constants, and calculates the jacobian at J(t_{n+1}, y^0_{n+1}). This implementation was based on that implemented in the SciPy library Virtanen et al.[3], which also mainly follows Shampine and Reichelt[2] but uses the more standard jacobian update.

Parameters:
  • func (callable) – function to evaluate the time derivative of the solution y at time t as func(y, t, *args), producing the same shape/structure as y0.

  • y0 (ndarray) – initial state vector

  • t_eval (ndarray) – time points to evaluate the solution, has shape (m,)

  • args ((optional)) – tuple of additional arguments for fun, which must be arrays scalars, or (nested) standard Python containers (tuples, lists, dicts, namedtuples, i.e. pytrees) of those types.

  • rtol ((optional) float) – relative tolerance for the solver

  • atol ((optional) float) – absolute tolerance for the solver

  • mass ((optional) ndarray) – diagonal of the mass matrix with shape (n,)

Returns:

y – calculated state vector at each of the m time points

Return type:

ndarray with shape (n, m)

References

IDAKLU Solver#

class pybamm.IDAKLUSolver(rtol=1e-06, atol=1e-06, root_method='casadi', root_tol=1e-06, extrap_tol=None, output_variables=[], options=None)[source]#

Solve a discretised model, using sundials with the KLU sparse linear solver.

Parameters:
  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • root_method (str or pybamm algebraic solver class, optional) – The method to use to find initial conditions (for DAE solvers). If a solver class, must be an algebraic solver class. If “casadi”, the solver uses casadi’s Newton rootfinding algorithm to find initial conditions. Otherwise, the solver uses ‘scipy.optimize.root’ with method specified by ‘root_method’ (e.g. “lm”, “hybr”, …)

  • root_tol (float, optional) – The tolerance for the initial-condition solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not (default is 0).

  • output_variables (list[str], optional) – List of variables to calculate and return. If none are specified then the complete state vector is returned (can be very large) (default is [])

  • options (dict, optional) –

    Addititional options to pass to the solver, by default:

    options = {
        # print statistics of the solver after every solve
        "print_stats": False,
        # jacobian form, can be "none", "dense",
        # "banded", "sparse", "matrix-free"
        "jacobian": "sparse",
        # name of sundials linear solver to use options are: "SUNLinSol_KLU",
        # "SUNLinSol_Dense", "SUNLinSol_Band", "SUNLinSol_SPBCGS",
        # "SUNLinSol_SPFGMR", "SUNLinSol_SPGMR", "SUNLinSol_SPTFQMR",
        "linear_solver": "SUNLinSol_KLU",
        # preconditioner for iterative solvers, can be "none", "BBDP"
        "preconditioner": "BBDP",
        # for iterative linear solvers, max number of iterations
        "linsol_max_iterations": 5,
        # for iterative linear solver preconditioner, bandwidth of
        # approximate jacobian
        "precon_half_bandwidth": 5,
        # for iterative linear solver preconditioner, bandwidth of
        # approximate jacobian that is kept
        "precon_half_bandwidth_keep": 5,
        # Number of threads available for OpenMP
        "num_threads": 1,
    }
    

    Note: These options only have an effect if model.convert_to_format == ‘casadi’

Extends: pybamm.solvers.base_solver.BaseSolver

set_up(model, inputs=None, t_eval=None, ics_only=False)[source]#

Unpack model, perform checks, and calculate jacobian.

Parameters:
  • model (pybamm.BaseModel) – The model whose solution to calculate. Must have attributes rhs and initial_conditions

  • inputs (dict, optional) – Any input parameters to pass to the model when solving

  • t_eval (numeric type, optional) – The times (in seconds) at which to compute the solution

Scikits.odes Solvers#

class pybamm.ScikitsOdeSolver(method='cvode', rtol=1e-06, atol=1e-06, extrap_tol=None, extra_options=None)[source]#

Solve a discretised model, using scikits.odes.

Parameters:
  • method (str, optional) – The method to use in solve_ivp (default is “BDF”)

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not (default is 0).

  • extra_options (dict, optional) –

    Any options to pass to the solver. Please consult scikits.odes documentation for details. Some common keys:

    • ’linsolver’: can be ‘dense’ (= default), ‘lapackdense’, ‘spgmr’, ‘spbcgs’, ‘sptfqmr’

Extends: pybamm.solvers.base_solver.BaseSolver

class pybamm.ScikitsDaeSolver(method='ida', rtol=1e-06, atol=1e-06, root_method='casadi', root_tol=1e-06, extrap_tol=None, extra_options=None)[source]#

Solve a discretised model, using scikits.odes.

Parameters:
  • method (str, optional) – The method to use in solve_ivp (default is “BDF”)

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • root_method (str or pybamm algebraic solver class, optional) – The method to use to find initial conditions (for DAE solvers). If a solver class, must be an algebraic solver class. If “casadi”, the solver uses casadi’s Newton rootfinding algorithm to find initial conditions. Otherwise, the solver uses ‘scipy.optimize.root’ with method specified by ‘root_method’ (e.g. “lm”, “hybr”, …)

  • root_tol (float, optional) – The tolerance for the initial-condition solver (default is 1e-6).

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not (default is 0).

  • extra_options (dict, optional) –

    Any options to pass to the solver. Please consult scikits.odes documentation for details. Some common keys:

    • ’max_steps’: maximum (int) number of steps the solver can take

Extends: pybamm.solvers.base_solver.BaseSolver

Casadi Solver#

class pybamm.CasadiSolver(mode='safe', rtol=1e-06, atol=1e-06, root_method='casadi', root_tol=1e-06, max_step_decrease_count=5, dt_max=None, extrap_tol=None, extra_options_setup=None, extra_options_call=None, return_solution_if_failed_early=False, perturb_algebraic_initial_conditions=None, integrators_maxcount=100)[source]#

Solve a discretised model, using CasADi.

Parameters:
  • mode (str) –

    How to solve the model (default is “safe”):

    • ”fast”: perform direct integration, without accounting for events. Recommended when simulating a drive cycle or other simulation where no events should be triggered.

    • ”fast with events”: perform direct integration of the whole timespan, then go back and check where events were crossed. Experimental only.

    • ”safe”: perform step-and-check integration in global steps of size dt_max, checking whether events have been triggered. Recommended for simulations of a full charge or discharge.

    • ”safe without grid”: perform step-and-check integration step-by-step. Takes more steps than “safe” mode, but doesn’t require creating the grid each time, so may be faster. Experimental only.

  • rtol (float, optional) – The relative tolerance for the solver (default is 1e-6).

  • atol (float, optional) – The absolute tolerance for the solver (default is 1e-6).

  • root_method (str or pybamm algebraic solver class, optional) – The method to use to find initial conditions (for DAE solvers). If a solver class, must be an algebraic solver class. If “casadi”, the solver uses casadi’s Newton rootfinding algorithm to find initial conditions. Otherwise, the solver uses ‘scipy.optimize.root’ with method specified by ‘root_method’ (e.g. “lm”, “hybr”, …)

  • root_tol (float, optional) – The tolerance for root-finding. Default is 1e-6.

  • max_step_decrease_count (float, optional) – The maximum number of times step size can be decreased before an error is raised. Default is 5.

  • dt_max (float, optional) – The maximum global step size (in seconds) used in “safe” mode. If None the default value is 600 seconds.

  • extrap_tol (float, optional) – The tolerance to assert whether extrapolation occurs or not. Default is 0.

  • extra_options_setup (dict, optional) –

    Any options to pass to the CasADi integrator when creating the integrator. Please consult CasADi documentation for details. Some useful options:

    • ”max_num_steps”: Maximum number of integrator steps

    • ”print_stats”: Print out statistics after integration

  • extra_options_call (dict, optional) –

    Any options to pass to the CasADi integrator when calling the integrator. Please consult CasADi documentation for details.

  • return_solution_if_failed_early (bool, optional) – Whether to return a Solution object if the solver fails to reach the end of the simulation, but managed to take some successful steps. Default is False.

  • perturb_algebraic_initial_conditions (bool, optional) – Whether to perturb algebraic initial conditions to avoid a singularity. This can sometimes slow down the solver, but is kept True as default for “safe” mode as it seems to be more robust (False by default for other modes).

  • integrators_maxcount (int, optional) – The maximum number of integrators that the solver will retain before ejecting past integrators using an LRU methodology. A value of 0 or None leaves the number of integrators unbound. Default is 100.

Extends: pybamm.solvers.base_solver.BaseSolver

create_integrator(model, inputs, t_eval=None, use_event_switch=False)[source]#

Method to create a casadi integrator object. If t_eval is provided, the integrator uses t_eval to make the grid. Otherwise, the integrator has grid [0,1].

Algebraic Solvers#

class pybamm.AlgebraicSolver(method='lm', tol=1e-06, extra_options=None)[source]#

Solve a discretised model which contains only (time independent) algebraic equations using a root finding algorithm. Uses scipy.optimize.root. Note: this solver could be extended for quasi-static models, or models in which the time derivative is manually discretised and results in a (possibly nonlinear) algebaric system at each time level.

Parameters:
  • method (str, optional) – The method to use to solve the system (default is “lm”). If it starts with “lsq”, least-squares minimization is used. The method for least-squares can be specified in the form “lsq_methodname”

  • tol (float, optional) – The tolerance for the solver (default is 1e-6).

  • extra_options (dict, optional) – Any options to pass to the rootfinder. Vary depending on which method is chosen. Please consult SciPy documentation for details.

Extends: pybamm.solvers.base_solver.BaseSolver

class pybamm.CasadiAlgebraicSolver(tol=1e-06, extra_options=None)[source]#

Solve a discretised model which contains only (time independent) algebraic equations using CasADi’s root finding algorithm. Note: this solver could be extended for quasi-static models, or models in which the time derivative is manually discretised and results in a (possibly nonlinear) algebaric system at each time level.

Parameters:
  • tol (float, optional) – The tolerance for the solver (default is 1e-6).

  • extra_options (dict, optional) – Any options to pass to the CasADi rootfinder. Please consult CasADi documentation for details.

Extends: pybamm.solvers.base_solver.BaseSolver

Solutions#

class pybamm.Solution(all_ts, all_ys, all_models, all_inputs, t_event=None, y_event=None, termination='final time', sensitivities=False, check_solution=True)[source]#

Class containing the solution of, and various attributes associated with, a PyBaMM model.

Parameters:
  • all_ts (numpy.array, size (n,) (or list of these)) – A one-dimensional array containing the times at which the solution is evaluated. A list of times can be provided instead to initialize a solution with sub-solutions.

  • all_ys (numpy.array, size (m, n) (or list of these)) – A two-dimensional array containing the values of the solution. y[i, :] is the vector of solutions at time t[i]. A list of ys can be provided instead to initialize a solution with sub-solutions.

  • all_models (pybamm.BaseModel) – The model that was used to calculate the solution. A list of models can be provided instead to initialize a solution with sub-solutions that have been calculated using those models.

  • all_inputs (dict (or list of these)) – The inputs that were used to calculate the solution A list of inputs can be provided instead to initialize a solution with sub-solutions.

  • t_event (numpy.array, size (1,)) – A zero-dimensional array containing the time at which the event happens.

  • y_event (numpy.array, size (m,)) – A one-dimensional array containing the value of the solution at the time when the event happens.

  • termination (str) – String to indicate why the solution terminated

  • sensitivities (bool or dict) – True if sensitivities included as the solution of the explicit forwards equations. False if no sensitivities included/wanted. Dict if sensitivities are provided as a dict of {parameter: sensitivities} pairs.

property all_models#

Model(s) used for solution

property first_state#

A Solution object that only contains the first state. This is faster to evaluate than the full solution when only the first state is needed (e.g. to initialize a model with the solution)

get_data_dict(variables=None, short_names=None, cycles_and_steps=True)[source]#

Construct a (standard python) dictionary of the solution data containing the variables in variables. If variables is None then all variables are returned. Any variable names in short_names are replaced with the corresponding short name.

If the solution has cycles, then the cycle numbers and step numbers are also returned in the dictionary.

Parameters:
  • variables (list, optional) – List of variables to return. If None, returns all variables in solution.data

  • short_names (dict, optional) – Dictionary of shortened names to use when saving.

  • cycles_and_steps (bool, optional) – Whether to include the cycle numbers and step numbers in the dictionary

Returns:

A dictionary of the solution data

Return type:

dict

property last_state#

A Solution object that only contains the final state. This is faster to evaluate than the full solution when only the final state is needed (e.g. to initialize a model with the solution)

plot(output_variables=None, **kwargs)[source]#

A method to quickly plot the outputs of the solution. Creates a pybamm.QuickPlot object (with keyword arguments ‘kwargs’) and then calls pybamm.QuickPlot.dynamic_plot().

Parameters:
save(filename)[source]#

Save the whole solution using pickle

save_data(filename=None, variables=None, to_format='pickle', short_names=None)[source]#

Save solution data only (raw arrays)

Parameters:
  • filename (str, optional) – The name of the file to save data to. If None, then a str is returned

  • variables (list, optional) – List of variables to save. If None, saves all of the variables that have been created so far

  • to_format (str, optional) –

    The format to save to. Options are:

    • ’pickle’ (default): creates a pickle file with the data dictionary

    • ’matlab’: creates a .mat file, for loading in matlab

    • ’csv’: creates a csv file (0D variables only)

    • ’json’: creates a json file

  • short_names (dict, optional) – Dictionary of shortened names to use when saving. This may be necessary when saving to MATLAB, since no spaces or special characters are allowed in MATLAB variable names. Note that not all the variables need to be given a short name.

Returns:

data – str if ‘csv’ or ‘json’ is chosen and filename is None, otherwise None

Return type:

str, optional

property sensitivities#

np_array

Type:

Values of the sensitivities. Returns a dict of param_name

property sub_solutions#

List of sub solutions that have been concatenated to form the full solution

property t#

Times at which the solution is evaluated

property t_event#

Time at which the event happens

property termination#

Reason for termination

update(variables)[source]#

Add ProcessedVariables to the dictionary of variables in the solution

property y#

Values of the solution

property y_event#

Value of the solution at the time of the event

Post-Process Variables#

class pybamm.ProcessedVariable(base_variables, base_variables_casadi, solution, warn=True, cumtrapz_ic=None)[source]#

An object that can be evaluated at arbitrary (scalars or vectors) t and x, and returns the (interpolated) value of the base variable at that t and x.

Parameters:
  • base_variables (list of pybamm.Symbol) – A list of base variables with a method evaluate(t,y), each entry of which returns the value of that variable for that particular sub-solution. A Solution can be comprised of sub-solutions which are the solutions of different models. Note that this can be any kind of node in the expression tree, not just a pybamm.Variable. When evaluated, returns an array of size (m,n)

  • base_variable_casadis (list of casadi.Function) – A list of casadi functions. When evaluated, returns the same thing as base_Variable.evaluate (but more efficiently).

  • solution (pybamm.Solution) – The solution object to be used to create the processed variables

  • warn (bool, optional) – Whether to raise warnings when trying to evaluate time and length scales. Default is True.

property data#

Same as entries, but different name

initialise_2D()[source]#

Initialise a 2D object that depends on x and r, x and z, x and R, or R and r.

initialise_sensitivity_explicit_forward()[source]#

Set up the sensitivity dictionary

property sensitivities#

Returns a dictionary of sensitivities for each input parameter. The keys are the input parameters, and the value is a matrix of size (n_x * n_t, n_p), where n_x is the number of states, n_t is the number of time points, and n_p is the size of the input parameter

Experiments#

Classes to help set operating conditions for some standard battery modelling experiments

Base Experiment Class#

class pybamm.Experiment(operating_conditions: list[str], period: str = '1 minute', temperature: float | None = None, termination: list[str] | None = None, drive_cycles=None, cccv_handling=None)[source]#

Base class for experimental conditions under which to run the model. In general, a list of operating conditions should be passed in. Each operating condition should be either a pybamm.step._Step class, created using one of the methods pybamm.step.current, pybamm.step.c_rate, pybamm.step.voltage , pybamm.step.power, pybamm.step.resistance, or pybamm.step.string, or a string, in which case the string is passed to pybamm.step.string.

Parameters:
  • operating_conditions (list[str]) – List of strings representing the operating conditions.

  • period (string, optional) – Period (1/frequency) at which to record outputs. Default is 1 minute. Can be overwritten by individual operating conditions.

  • temperature (float, optional) – The ambient air temperature in degrees Celsius at which to run the experiment. Default is None whereby the ambient temperature is taken from the parameter set. This value is overwritten if the temperature is specified in a step.

  • termination (list[str], optional) – List of strings representing the conditions to terminate the experiment. Default is None. This is different from the termination for individual steps. Termination for individual steps is specified in the step itself, and the simulation moves to the next step when the termination condition is met (e.g. 2.5V discharge cut-off). Termination for the experiment as a whole is specified here, and the simulation stops when the termination condition is met (e.g. 80% capacity).

read_termination(termination)[source]#

Read the termination reason. If this condition is hit, the experiment will stop.

search_tag(tag)[source]#

Search for a tag in the experiment and return the cycles in which it appears.

Parameters:

tag (str) – The tag to search for

Returns:

A list of cycles in which the tag appears

Return type:

list

Experiment step functions#

The following functions can be used to define steps in an experiment.

pybamm.step.string(string, **kwargs)#

Create a step from a string.

Parameters:
  • string (str) – The string to parse. Each operating condition should be of the form “Do this for this long” or “Do this until this happens”. For example, “Charge at 1 C for 1 hour”, or “Charge at 1 C until 4.2 V”, or “Charge at 1 C for 1 hour or until 4.2 V”. The instructions can be of the form “(Dis)charge at x A/C/W”, “Rest”, or “Hold at x V until y A”. The running time should be a time in seconds, minutes or hours, e.g. “10 seconds”, “3 minutes” or “1 hour”. The stopping conditions should be a circuit state, e.g. “1 A”, “C/50” or “3 V”.

  • **kwargs – Any other keyword arguments are passed to the pybamm.step._Step class.

Returns:

A step parsed from the string.

Return type:

pybamm.step._Step

pybamm.step.current(value, **kwargs)#

Create a current-controlled step. Current is positive for discharge and negative for charge.

Parameters:
  • value (float) – The current value in A. It can be a number or a 2-column array (for drive cycles).

  • **kwargs – Any other keyword arguments are passed to the pybamm.step._Step class.

Returns:

A current-controlled step.

Return type:

pybamm.step._Step

pybamm.step.voltage(value, **kwargs)#

Create a voltage-controlled step. Voltage should always be positive.

Parameters:
  • value (float) – The voltage value in V. It can be a number or a 2-column array (for drive cycles).

  • **kwargs – Any other keyword arguments are passed to the pybamm.step._Step class.

Returns:

A voltage-controlled step.

Return type:

pybamm.step._Step

pybamm.step.power(value, **kwargs)#

Create a power-controlled step. Power is positive for discharge and negative for charge.

Parameters:
  • value (float) – The power value in W. It can be a number or a 2-column array (for drive cycles).

  • **kwargs – Any other keyword arguments are passed to the pybamm.step._Step class.

Returns:

A power-controlled step.

Return type:

pybamm.step._Step

pybamm.step.resistance(value, **kwargs)#

Create a resistance-controlled step. Resistance is positive for discharge and negative for charge.

Parameters:
  • value (float) – The resistance value in Ohm. It can be a number or a 2-column array (for drive cycles).

  • **kwargs – Any other keyword arguments are passed to the pybamm.step._Step class.

Returns:

A resistance-controlled step.

Return type:

pybamm.step._Step

These functions return the following step class, which is not intended to be used directly:

class pybamm.step._Step(typ, value, duration=None, termination=None, period=None, temperature=None, tags=None, start_time=None, description=None)#

Class representing one step in an experiment. All experiment steps are functions that return an instance of this class. This class is not intended to be used directly.

Parameters:
  • typ (str) – The type of step, can be “current”, “voltage”, “c_rate”, “power”, or “resistance”.

  • value (float) – The value of the step, corresponding to the type of step. Can be a number, a 2-tuple (for cccv_ode), or a 2-column array (for drive cycles)

  • duration (float, optional) – The duration of the step in seconds.

  • termination (str or list, optional) – A string or list of strings indicating the condition(s) that will terminate the step. If a list, the step will terminate when any of the conditions are met.

  • period (float or string, optional) – The period of the step. If a float, the value is in seconds. If a string, the value should be a valid time string, e.g. “1 hour”.

  • temperature (float or string, optional) – The temperature of the step. If a float, the value is in Kelvin. If a string, the value should be a valid temperature string, e.g. “25 oC”.

  • tags (str or list, optional) – A string or list of strings indicating the tags associated with the step.

  • datetime (str or datetime, optional) – A string or list of strings indicating the tags associated with the step.

  • description (str, optional) – A description of the step.

basic_repr()#

Return a basic representation of the step, only with type, value, termination and temperature, which are the variables involved in processing the model. Also used for hashing.

to_dict()#

Convert the step to a dictionary.

Returns:

A dictionary containing the step information.

Return type:

dict

Step terminations#

Standard step termination events are implemented by the following classes, which are called when the termination is specified by a specific string. These classes can be either be called directly or via the string format specified in the class docstring

class pybamm.step.CrateTermination(value)#

Termination based on C-rate, created when a string termination of the C-rate type (e.g. “C/10”) is provided

Extends: pybamm.experiment.step.step_termination.BaseTermination

get_event(variables, step_value)#

See BaseTermination.get_event()

class pybamm.step.CurrentTermination(value)#

Termination based on current, created when a string termination of the current type (e.g. “1A”) is provided

Extends: pybamm.experiment.step.step_termination.BaseTermination

get_event(variables, step_value)#

See BaseTermination.get_event()

class pybamm.step.VoltageTermination(value)#

Termination based on voltage, created when a string termination of the voltage type (e.g. “4.2V”) is provided

Extends: pybamm.experiment.step.step_termination.BaseTermination

get_event(variables, step_value)#

See BaseTermination.get_event()

The following classes can be used to define custom terminations for an experiment step:

class pybamm.step.BaseTermination(value)#

Base class for a termination event for an experiment step. To create a custom termination, a class must implement get_event to return a pybamm.Event corresponding to the desired termination. In most cases the class pybamm.step.CustomTermination can be used to assist with this.

Parameters:

value (float) – The value at which the event is triggered

get_event(variables, step_value)#

Return a pybamm.Event object corresponding to the termination event

Parameters:
  • variables (dict) – Dictionary of model variables, to be used for selecting the variable(s) that determine the event

  • step_value (float or pybamm.Symbol) – Value of the step for which this is a termination event, to be used in some cases to determine the sign of the event.

class pybamm.step.CustomTermination(name, event_function)#

Define a custom termination event using a function. This can be used to create an event based on any variable in the model.

Parameters:
  • name (str) – Name of the event

  • event_function (callable) – A function that takes in a dictionary of variables and evaluates the event value. Must be positive before the event is triggered and zero when the event is triggered.

Example

Add a cut-off based on negative electrode stoichiometry. The event will trigger when the negative electrode stoichiometry reaches 10%.

>>> def neg_stoich_cutoff(variables):
...    return variables["Negative electrode stoichiometry"] - 0.1
>>> neg_stoich_termination = pybamm.step.CustomTermination(
...    name="Negative stoichiometry cut-off", event_function=neg_stoich_cutoff
... )

Extends: pybamm.experiment.step.step_termination.BaseTermination

get_event(variables, step_value)#

See BaseTermination.get_event()

Simulation#

class pybamm.Simulation(model, experiment=None, geometry=None, parameter_values=None, submesh_types=None, var_pts=None, spatial_methods=None, solver=None, output_variables=None, C_rate=None)[source]#

A Simulation class for easy building and running of PyBaMM simulations.

Parameters:
  • model (pybamm.BaseModel) – The model to be simulated

  • experiment (pybamm.Experiment or string or list (optional)) – The experimental conditions under which to solve the model. If a string is passed, the experiment is constructed as pybamm.Experiment([experiment]). If a list is passed, the experiment is constructed as pybamm.Experiment(experiment).

  • geometry (pybamm.Geometry (optional)) – The geometry upon which to solve the model

  • parameter_values (pybamm.ParameterValues (optional)) – Parameters and their corresponding numerical values.

  • submesh_types (dict (optional)) – A dictionary of the types of submesh to use on each subdomain

  • var_pts (dict (optional)) – A dictionary of the number of points used by each spatial variable

  • spatial_methods (dict (optional)) – A dictionary of the types of spatial method to use on each domain (e.g. pybamm.FiniteVolume)

  • solver (pybamm.BaseSolver (optional)) – The solver to use to solve the model.

  • output_variables (list (optional)) – A list of variables to plot automatically

  • C_rate (float (optional)) – The C-rate at which you would like to run a constant current (dis)charge.

build(check_model=True, initial_soc=None)[source]#

A method to build the model into a system of matrices and vectors suitable for performing numerical computations. If the model has already been built or solved then this function will have no effect. This method will automatically set the parameters if they have not already been set.

Parameters:
  • check_model (bool, optional) – If True, model checks are performed after discretisation (see pybamm.Discretisation.process_model()). Default is True.

  • initial_soc (float, optional) – Initial State of Charge (SOC) for the simulation. Must be between 0 and 1. If given, overwrites the initial concentrations provided in the parameter set.

build_for_experiment(check_model=True, initial_soc=None)[source]#

Similar to Simulation.build(), but for the case of simulating an experiment, where there may be several models and solvers to build.

create_gif(number_of_images=80, duration=0.1, output_filename='plot.gif')[source]#

Generates x plots over a time span of t_eval and compiles them to create a GIF. For more information see pybamm.QuickPlot.create_gif()

Parameters:
  • number_of_images (int (optional)) – Number of images/plots to be compiled for a GIF.

  • duration (float (optional)) – Duration of visibility of a single image/plot in the created GIF.

  • output_filename (str (optional)) – Name of the generated GIF file.

plot(output_variables=None, **kwargs)[source]#

A method to quickly plot the outputs of the simulation. Creates a pybamm.QuickPlot object (with keyword arguments ‘kwargs’) and then calls pybamm.QuickPlot.dynamic_plot().

Parameters:
save(filename)[source]#

Save simulation using pickle module.

Parameters:

filename (str) – The file extension can be arbitrary, but it is common to use “.pkl” or “.pickle”

save_model(filename: str | None = None, mesh: bool = False, variables: bool = False)[source]#

Write out a discretised model to a JSON file

Parameters:
  • mesh (bool) – The mesh used to discretise the model. If false, plotting tools will not be available when the model is read back in and solved.

  • variables (bool) – The discretised variables. Not required to solve a model, but if false tools will not be availble. Will automatically save meshes as well, required for plotting tools.

  • filename (str, optional) – The desired name of the JSON file. If no name is provided, one will be created based on the model name, and the current datetime.

set_parameters()[source]#

A method to set the parameters in the model and the associated geometry.

set_up_and_parameterise_experiment()[source]#

Set up a simulation to run with an experiment. This creates a dictionary of inputs (current/voltage/power, running time, stopping condition) for each operating condition in the experiment. The model will then be solved by integrating the model successively with each group of inputs, one group at a time. This needs to be done here and not in the Experiment class because the nominal cell capacity (from the parameters) is used to convert C-rate to current.

set_up_and_parameterise_model_for_experiment()[source]#

Set up self._model to be able to run the experiment (new version). In this version, a new model is created for each step.

This increases set-up time since several models to be processed, but reduces simulation time since the model formulation is efficient.

solve(t_eval=None, solver=None, check_model=True, save_at_cycles=None, calc_esoh=True, starting_solution=None, initial_soc=None, callbacks=None, showprogress=False, **kwargs)[source]#

A method to solve the model. This method will automatically build and set the model parameters if not already done so.

Parameters:
  • t_eval (numeric type, optional) –

    The times (in seconds) at which to compute the solution. Can be provided as an array of times at which to return the solution, or as a list [t0, tf] where t0 is the initial time and tf is the final time. If provided as a list the solution is returned at 100 points within the interval [t0, tf].

    If not using an experiment or running a drive cycle simulation (current provided as data) t_eval must be provided.

    If running an experiment the values in t_eval are ignored, and the solution times are specified by the experiment.

    If None and the parameter “Current function [A]” is read from data (i.e. drive cycle simulation) the model will be solved at the times provided in the data.

  • solver (pybamm.BaseSolver, optional) – The solver to use to solve the model. If None, Simulation.solver is used

  • check_model (bool, optional) – If True, model checks are performed after discretisation (see pybamm.Discretisation.process_model()). Default is True.

  • save_at_cycles (int or list of ints, optional) – Which cycles to save the full sub-solutions for. If None, all cycles are saved. If int, every multiple of save_at_cycles is saved. If list, every cycle in the list is saved. The first cycle (cycle 1) is always saved.

  • calc_esoh (bool, optional) – Whether to include eSOH variables in the summary variables. If False then only summary variables that do not require the eSOH calculation are calculated. Default is True.

  • starting_solution (pybamm.Solution) – The solution to start stepping from. If None (default), then self._solution is used. Must be None if not using an experiment.

  • initial_soc (float, optional) – Initial State of Charge (SOC) for the simulation. Must be between 0 and 1. If given, overwrites the initial concentrations provided in the parameter set.

  • callbacks (list of callbacks, optional) – A list of callbacks to be called at each time step. Each callback must implement all the methods defined in pybamm.callbacks.BaseCallback.

  • showprogress (bool, optional) – Whether to show a progress bar for cycling. If true, shows a progress bar for cycles. Has no effect when not used with an experiment. Default is False.

  • **kwargs – Additional key-word arguments passed to solver.solve. See pybamm.BaseSolver.solve().

step(dt, solver=None, npts=2, save=True, starting_solution=None, **kwargs)[source]#

A method to step the model forward one timestep. This method will automatically build and set the model parameters if not already done so.

Parameters:
  • dt (numeric type) – The timestep over which to step the solution

  • solver (pybamm.BaseSolver) – The solver to use to solve the model.

  • npts (int, optional) – The number of points at which the solution will be returned during the step dt. Default is 2 (returns the solution at t0 and t0 + dt).

  • save (bool) – Turn on to store the solution of all previous timesteps

  • starting_solution (pybamm.Solution) – The solution to start stepping from. If None (default), then self._solution is used

  • **kwargs – Additional key-word arguments passed to solver.solve. See pybamm.BaseSolver.step().

Plotting#

Quick Plot#

class pybamm.QuickPlot(solutions, output_variables=None, labels=None, colors=None, linestyles=None, shading='auto', figsize=None, n_rows=None, time_unit=None, spatial_unit='um', variable_limits='fixed')[source]#

Generates a quick plot of a subset of key outputs of the model so that the model outputs can be easily assessed.

Parameters:
  • solutions ((iter of) pybamm.Solution or pybamm.Simulation) – The numerical solution(s) for the model(s), or the simulation object(s) containing the solution(s).

  • output_variables (list of str, optional) – List of variables to plot

  • labels (list of str, optional) – Labels for the different models. Defaults to model names

  • colors (list of str, optional) – The colors to loop over when plotting. Defaults to None, in which case the default color loop defined by matplotlib style sheet or rcParams is used.

  • linestyles (list of str, optional) – The linestyles to loop over when plotting. Defaults to [“-”, “:”, “–”, “-.”]

  • shading (str, optional) – The shading to use for 2D plots. Defaults to “auto”.

  • figsize (tuple of floats, optional) – The size of the figure to make

  • n_rows (int, optional) – The number of rows to use. If None (default), floor(n // sqrt(n)) is used where n = len(output_variables) so that the plot is as square as possible

  • time_unit (str, optional) – Format for the time output (“hours”, “minutes”, or “seconds”)

  • spatial_unit (str, optional) – Format for the spatial axes (“m”, “mm”, or “um”)

  • 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:

    • ”fixed” (default): keep all axes fixes so that all data is visible

    • ”tight”: make axes tight to plot at each time

    • dictionary: fine-grain control for each variable, can be either “fixed” or “tight” or a specific tuple (lower, upper).

create_gif(number_of_images=80, duration=0.1, output_filename='plot.gif')[source]#

Generates x plots over a time span of max_t - min_t and compiles them to create a GIF.

Parameters:
  • number_of_images (int (optional)) – Number of images/plots to be compiled for a GIF.

  • duration (float (optional)) – Duration of visibility of a single image/plot in the created GIF.

  • output_filename (str (optional)) – Name of the generated GIF file.

dynamic_plot(testing=False, step=None)[source]#

Generate a dynamic plot with a slider to control the time.

Parameters:
  • step (float) – For notebook mode, size of steps to allow in the slider. Defaults to 1/100th of the total time.

  • testing (bool) – Whether to actually make the plot (turned off for unit tests)

get_spatial_var(key, variable, dimension)[source]#

Return the appropriate spatial variable(s)

plot(t, dynamic=False)[source]#

Produces a quick plot with the internal states at time t.

Parameters:

t (float) – Dimensional time (in ‘time_units’) at which to plot.

reset_axis()[source]#

Reset the axis limits to the default values. These are calculated to fit around the minimum and maximum values of all the variables in each subplot

slider_update(t)[source]#

Update the plot in self.plot() with values at new time

pybamm.dynamic_plot(*args, **kwargs)[source]#

Creates a pybamm.QuickPlot object (with arguments ‘args’ and keyword arguments ‘kwargs’) and then calls pybamm.QuickPlot.dynamic_plot(). The key-word argument ‘testing’ is passed to the ‘dynamic_plot’ method, not the QuickPlot class.

Returns:

plot – The ‘QuickPlot’ object that was created

Return type:

pybamm.QuickPlot

class pybamm.QuickPlotAxes[source]#

Class to store axes for the QuickPlot

add(keys, axis)[source]#

Add axis

Parameters:
  • keys (iter) – Iterable of keys of variables being plotted on the axis

  • axis (matplotlib Axis object) – The axis object

by_variable(key)[source]#

Get axis by variable name

Plot#

pybamm.plot(x, y, ax=None, testing=False, **kwargs)[source]#

Generate a simple 1D plot. Calls matplotlib.pyplot.plot with keyword arguments ‘kwargs’. For a list of ‘kwargs’ see the matplotlib plot documentation

Parameters:
  • x (pybamm.Array) – The array to plot on the x axis

  • y (pybamm.Array) – The array to plot on the y axis

  • ax (matplotlib Axis, optional) – The axis on which to put the plot. If None, a new figure and axis is created.

  • testing (bool, optional) – Whether to actually make the plot (turned off for unit tests)

  • kwargs – Keyword arguments, passed to plt.plot

Plot 2D#

pybamm.plot2D(x, y, z, ax=None, testing=False, **kwargs)[source]#

Generate a simple 2D plot. Calls matplotlib.pyplot.contourf with keyword arguments ‘kwargs’. For a list of ‘kwargs’ see the matplotlib contourf documentation

Parameters:
  • x (pybamm.Array) – The array to plot on the x axis. Can be of shape (M, N) or (N, 1)

  • y (pybamm.Array) – The array to plot on the y axis. Can be of shape (M, N) or (M, 1)

  • z (pybamm.Array) – The array to plot on the z axis. Is of shape (M, N)

  • ax (matplotlib Axis, optional) – The axis on which to put the plot. If None, a new figure and axis is created.

  • testing (bool, optional) – Whether to actually make the plot (turned off for unit tests)

Plot Voltage Components#

pybamm.plot_voltage_components(solution, ax=None, show_legend=True, split_by_electrode=False, testing=False, **kwargs_fill)[source]#

Generate a plot showing the component overpotentials that make up the voltage

Parameters:
  • solution (pybamm.Solution) – Solution object from which to extract voltage components

  • ax (matplotlib Axis, optional) – The axis on which to put the plot. If None, a new figure and axis is created.

  • show_legend (bool, optional) – Whether to display the legend. Default is True

  • split_by_electrode (bool, optional) – Whether to show the overpotentials for the negative and positive electrodes separately. Default is False.

  • testing (bool, optional) – Whether to actually make the plot (turned off for unit tests)

  • kwargs_fill – Keyword arguments, passed to ax.fill_between

Plot Summary Variables#

pybamm.plot_summary_variables(solutions, output_variables=None, labels=None, testing=False, **kwargs_fig)[source]#

Generate a plot showing/comparing the summary variables.

Parameters:
  • solutions ((iter of) pybamm.Solution) – The solution(s) for the model(s) from which to extract summary variables.

  • output_variables (list (optional)) – A list of variables to plot automatically. If None, the default ones are used.

  • labels (list (optional)) – A list of labels to be added to the legend. No labels are added by default.

  • testing (bool (optional)) – Whether to actually make the plot (turned off for unit tests).

  • kwargs_fig – Keyword arguments, passed to plt.subplots.

Utility functions#

pybamm.get_git_commit_info()[source]#

Get the git commit info for the current PyBaMM version, e.g. v22.8-39-gb25ce8c41 (version 22.8, commit b25ce8c41)

pybamm.rmse(x, y)[source]#

Calculate the root-mean-square-error between two vectors x and y, ignoring NaNs

pybamm.root_dir()[source]#

return the root directory of the PyBaMM install directory

class pybamm.Timer[source]#

Provides accurate timing.

Example

timer = pybamm.Timer() print(timer.time())

reset()[source]#

Resets this timer’s start time.

time()[source]#

Returns the time (float, in seconds) since this timer was created, or since meth:reset() was last called.

class pybamm.TimerTime(value)[source]#
class pybamm.FuzzyDict[source]#
copy() a shallow copy of D[source]#
get_best_matches(key)[source]#

Get best matches from keys

search(key, print_values=False)[source]#

Search dictionary for keys containing ‘key’. If print_values is True, then both the keys and values will be printed. Otherwise just the values will be printed. If no results are found, the best matches are printed.

pybamm.load(filename)[source]#

Load a saved object

pybamm.install_jax(arguments=None)[source]#

Install compatible versions of jax, jaxlib.

Command Line Interface:

$ pybamm_install_jax
optional arguments:
-h, –help show help message
-f, –force force install compatible versions of jax and jaxlib
pybamm.have_jax()[source]#

Check if jax and jaxlib are installed with the correct versions

pybamm.is_jax_compatible()[source]#

Check if the available version of jax and jaxlib are compatible with PyBaMM

Callbacks#

class pybamm.callbacks.Callback[source]#

Base class for callbacks, for documenting callback methods.

Callbacks are used to perform actions (e.g. logging, saving) at certain points in the simulation. Each callback method is named on_<event>, where <event> describes the point at which the callback is called. For example, the callback on_experiment_start is called at the start of an experiment simulation. In general, callbacks take a single argument, logs, which is a dictionary of information about the simulation. Each callback method should return None (the output of the method is ignored).

EXPERIMENTAL - this class is experimental and the callback interface may change in future releases.

on_cycle_end(logs)[source]#

Called at the end of each cycle in an experiment simulation.

on_cycle_start(logs)[source]#

Called at the start of each cycle in an experiment simulation.

on_experiment_end(logs)[source]#

Called at the end of an experiment simulation.

on_experiment_error(logs)[source]#

Called when a SolverError occurs during an experiment simulation.

For example, this could be used to send an error alert with a bug report when running batch simulations in the cloud.

on_experiment_infeasible(logs)[source]#

Called when an experiment simulation is infeasible.

on_experiment_start(logs)[source]#

Called at the start of an experiment simulation.

on_step_end(logs)[source]#

Called at the end of each step in an experiment simulation.

on_step_start(logs)[source]#

Called at the start of each step in an experiment simulation.

class pybamm.callbacks.CallbackList(callbacks)[source]#

Container abstracting a list of callbacks, so that they can be called in a single step e.g. callbacks.on_simulation_end(…).

This is done without having to redefine the method each time by using the callback_loop_decorator decorator, which is applied to every method that starts with on_, using the inspect module. See https://stackoverflow.com/questions/1367514/how-to-decorate-a-method-inside-a-class.

If better control over how the callbacks are called is required, it might be better to be more explicit with the for loop.

Extends: pybamm.callbacks.Callback

on_cycle_end(*args, **kwargs)#

Called at the end of each cycle in an experiment simulation.

on_cycle_start(*args, **kwargs)#

Called at the start of each cycle in an experiment simulation.

on_experiment_end(*args, **kwargs)#

Called at the end of an experiment simulation.

on_experiment_error(*args, **kwargs)#

Called when a SolverError occurs during an experiment simulation.

For example, this could be used to send an error alert with a bug report when running batch simulations in the cloud.

on_experiment_infeasible(*args, **kwargs)#

Called when an experiment simulation is infeasible.

on_experiment_start(*args, **kwargs)#

Called at the start of an experiment simulation.

on_step_end(*args, **kwargs)#

Called at the end of each step in an experiment simulation.

on_step_start(*args, **kwargs)#

Called at the start of each step in an experiment simulation.

class pybamm.callbacks.LoggingCallback(logfile=None)[source]#

Logging callback, implements methods to log progress of the simulation.

Parameters:

logfile (str, optional) – Where to send the log output. If None, uses pybamm’s logger.

Extends: pybamm.callbacks.Callback

on_cycle_end(logs)[source]#

Called at the end of each cycle in an experiment simulation.

on_cycle_start(logs)[source]#

Called at the start of each cycle in an experiment simulation.

on_experiment_end(logs)[source]#

Called at the end of an experiment simulation.

on_experiment_error(logs)[source]#

Called when a SolverError occurs during an experiment simulation.

For example, this could be used to send an error alert with a bug report when running batch simulations in the cloud.

on_experiment_infeasible(logs)[source]#

Called when an experiment simulation is infeasible.

on_experiment_start(logs)[source]#

Called at the start of an experiment simulation.

on_step_end(logs)[source]#

Called at the end of each step in an experiment simulation.

on_step_start(logs)[source]#

Called at the start of each step in an experiment simulation.

pybamm.callbacks.setup_callbacks(callbacks)[source]#

Citations#

class pybamm.Citations[source]#

Entry point to citations management. This object may be used to record BibTeX citation information and then register that a particular citation is relevant for a particular simulation.

Citations listed in pybamm/CITATIONS.bib can be registered with their citation key. For all other works provide a BibTeX Citation to register().

Examples

>>> pybamm.citations.register("Sulzer2021")
>>> pybamm.citations.register("@misc{Newton1687, title={Mathematical...}}")
>>> pybamm.print_citations("citations.txt")
print(filename=None, output_format='text', verbose=False)[source]#

Print all citations that were used for running simulations. The verbose option is provided to print tags for citations in the output such that it can can be seen where the citations were registered due to the use of PyBaMM models and solvers in the code.

Note

If a citation is registered manually, it will not be tagged.

Warning

This function will notify the user if a citation that has been previously registered is invalid or cannot be parsed.

Parameters:
  • filename (str, optional) – Filename to which to print citations. If None, citations are printed to the terminal.

  • verbose (bool, optional) – If True, prints the citation tags for the citations that have been registered. An example of the output is shown below.

Examples

pybamm.lithium_ion.SPM()
pybamm.Citations.print(verbose=True) or pybamm.print_citations(verbose=True)

will append the following at the end of the list of citations:

Citations registered:

Marquis2019 was cited due to the use of SPM
read_citations()[source]#

Reads the citations in pybamm.CITATIONS.bib. Other works can be cited by passing a BibTeX citation to register().

register(key)[source]#

Register a paper to be cited, one at a time. The intended use is that register() should be called only when the referenced functionality is actually being used.

Warning

Registering a BibTeX citation, with the same key as an existing citation, will overwrite the current citation.

Parameters:

key (str) –

  • The citation key for an entry in pybamm/CITATIONS.bib or

  • A BibTeX formatted citation

pybamm.print_citations(filename=None, output_format='text', verbose=False)[source]#

See Citations.print()

Batch Study#

class pybamm.BatchStudy(models, experiments=None, geometries=None, parameter_values=None, submesh_types=None, var_pts=None, spatial_methods=None, solvers=None, output_variables=None, C_rates=None, repeats=1, permutations=False)[source]#

A BatchStudy class for comparison of different PyBaMM simulations.

Parameters:
  • models (dict) – A dictionary of models to be simulated

  • experiments (dict (optional)) – A dictionary of experimental conditions under which to solve the model. Default is None

  • geometries (dict (optional)) – A dictionary of geometries upon which to solve the model

  • parameter_values (dict (optional)) – A dictionary of parameters and their corresponding numerical values. Default is None

  • submesh_types (dict (optional)) – A dictionary of the types of submesh to use on each subdomain. Default is None

  • var_pts (dict (optional)) – A dictionary of the number of points used by each spatial variable. Default is None

  • spatial_methods (dict (optional)) – A dictionary of the types of spatial method to use on each domain. Default is None

  • solvers (dict (optional)) – A dictionary of solvers to use to solve the model. Default is None

  • output_variables (dict (optional)) – A dictionary of variables to plot automatically. Default is None

  • C_rates (dict (optional)) – A dictionary of C-rates at which you would like to run a constant current (dis)charge. Default is None

  • repeats (int (optional)) – The number of times solve should be called. Default is 1

  • permutations (bool (optional)) – If False runs first model with first solver, first experiment and second model with second solver, second experiment etc. If True runs a cartesian product of models, solvers and experiments. Default is False

create_gif(number_of_images=80, duration=0.1, output_filename='plot.gif')[source]#

Generates x plots over a time span of t_eval and compiles them to create a GIF. For more information see pybamm.QuickPlot.create_gif()

Parameters:
  • number_of_images (int, optional) – Number of images/plots to be compiled for a GIF.

  • duration (float, optional) – Duration of visibility of a single image/plot in the created GIF.

  • output_filename (str, optional) – Name of the generated GIF file.

plot(output_variables=None, **kwargs)[source]#

For more information on the parameters used in the plot, See pybamm.Simulation.plot()

solve(t_eval=None, solver=None, check_model=True, save_at_cycles=None, calc_esoh=True, starting_solution=None, initial_soc=None, **kwargs)[source]#

For more information on the parameters used in the solve, See pybamm.Simulation.solve()

Examples#

This folder contains a collection of Jupyter notebooks that demonstrate how to use PyBaMM and reveal some of its functionalities and inner workings. The notebooks are organised into subfolders, and can be viewed in the galleries below.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np

y = pybamm.StateVector(slice(0, 1))
t = pybamm.t
equation = 2 * y * (1 - y) + t
equation.visualise("expression_tree1.png")
Note: you may need to restart the kernel to use updated packages.

expression_tree1

Once the equation is constructed, we can evaluate it at a given \(t=1\) and \(\mathbf{y}=\begin{pmatrix} 2 \end{pmatrix}\).

[2]:
equation.evaluate(1, np.array([2]))
[2]:
array([[-3.]])

We can also calculate the expression tree representing the gradient of the equation with respect to \(t\),

[3]:
diff_wrt_equation = equation.diff(t)
diff_wrt_equation.visualise("expression_tree2.png")

expression_tree2

…and evaluate this expression,

[4]:
diff_wrt_equation.evaluate(t=1, y=np.array([2]), y_dot=np.array([2]))
[4]:
array([[-11.]])

The PyBaMM Pipeline#

Proposing, parameter setting and discretising a model in PyBaMM is a pipeline process, consisting of the following steps:

  1. The model is proposed, consisting of equations representing the right-hand-side of an ordinary differential equation (ODE), and/or algebraic equations for a differential algebraic equation (DAE), and also associated boundary condition equations

  2. The parameters present in the model are replaced by actual scalar values from a parameter file, using the `pybamm.ParameterValues <https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html>`__ class

  3. The equations in the model are discretised onto a mesh, any spatial gradients are replaced with linear algebra expressions and the variables of the model are replaced with state vector slices. This is done using the `pybamm.Discretisation <https://docs.pybamm.org/en/latest/source/api/spatial_methods/discretisation.html>`__ class.

Stage 1 - Symbolic Expression Trees#

At each stage, the expression tree consists of certain types of nodes. In the first stage, the model is first proposed using `pybamm.Parameter <https://docs.pybamm.org/en/latest/source/api/expression_tree/parameter.html>`__, `pybamm.Variable <https://docs.pybamm.org/en/latest/source/api/expression_tree/variable.html>`__, and other unary and binary operators (which also includes spatial operators such as `pybamm.Gradient <https://docs.pybamm.org/en/latest/source/api/expression_tree/unary_operator.html#pybamm.Gradient>`__ and `pybamm.Divergence <https://docs.pybamm.org/en/latest/source/api/expression_tree/unary_operator.html#pybamm.Divergence>`__). For example, the right hand side of the equation

\[\frac{d c}{dt} = D \nabla \cdot \nabla c\]

can be constructed as an expression tree like so:

[5]:
D = pybamm.Parameter("D")
c = pybamm.Variable("c", domain=["negative electrode"])

dcdt = D * pybamm.div(pybamm.grad(c))
dcdt.visualise("expression_tree3.png")

expression_tree3

Stage 2 - Setting parameters#

In the second stage, the pybamm.ParameterValues class is used to replace all the parameter nodes with scalar values, according to an input parameter file. For example, we’ll use a this class to set \(D = 2\)

[6]:
parameter_values = pybamm.ParameterValues({"D": 2})
dcdt = parameter_values.process_symbol(dcdt)
dcdt.visualise("expression_tree4.png")

expression_tree4

Stage 3 - Linear Algebra Expression Trees#

The third and final stage uses the pybamm.Discretisation class to discretise the spatial gradients and variables over a given mesh. After this stage the expression tree will encode a linear algebra expression that can be evaluated given the state vector \(\mathbf{y}\) and \(t\).

Note: for demonstration purposes, we use a dummy discretisation below. For a more complete description of the pybamm.Discretisation class, see the example notebook here.

[7]:
# Here, we import a dummy discretisation from the PyBaMM tests directory.
import sys

sys.path.insert(0, pybamm.root_dir())
from tests import get_discretisation_for_testing

disc = get_discretisation_for_testing()
disc.y_slices = {c: [slice(0, 40)]}
dcdt = disc.process_symbol(dcdt)
dcdt.visualise("expression_tree5.png")

expression_tree5

After the third stage, our expression tree is now able to be evaluated by one of the solver classes. Note that we have used a single equation above to illustrate the different types of expression trees in PyBaMM, but any given models will consist of many RHS or algebraic equations, along with boundary conditions. See here for more details of PyBaMM models.

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[1] 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.
[2] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Broadcasts#

This notebook explains the different types of broadcast available in PyBaMM. Understanding of the expression_tree and discretisation notebooks is assumed.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
Note: you may need to restart the kernel to use updated packages.

We also explicitly set up the discretisation that is used for this notebook. We use a small number of points in each domain, in order to easily visualise the results.

[2]:
var = pybamm.standard_spatial_vars
geometry = {
    "negative electrode": {var.x_n: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}},
    "negative particle": {var.r_n: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(1)}},
}

submesh_types = {
    "negative electrode": pybamm.Uniform1DSubMesh,
    "negative particle": pybamm.Uniform1DSubMesh,
}

var_pts = {var.x_n: 5, var.r_n: 3}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

spatial_methods = {
    "negative electrode": pybamm.FiniteVolume(),
    "negative particle": pybamm.FiniteVolume(),
}
disc = pybamm.Discretisation(mesh, spatial_methods)

Primary broadcasts#

Primary broadcasts are used to broadcast from a “larger” scale to a “smaller” scale, for example broadcasting temperature T(x) from the electrode to the particles, or broadcasting current collector current i(y, z) from the current collector to the electrodes. To demonstrate this, we first create a variable T on the negative electrode domain, discretise it, and evaluate it with a simple linear vector

[3]:
T = pybamm.Variable("T", domain="negative electrode")
disc.set_variable_slices([T])
disc_T = disc.process_symbol(T)
disc_T.evaluate(y=np.linspace(0, 1, 5))
[3]:
array([[0.  ],
       [0.25],
       [0.5 ],
       [0.75],
       [1.  ]])

We then broadcast T onto the “negative particle” domain (using primary broadcast as we are going from the larger electrode scale to the smaller particle scale), and discretise and evaluate the resulting object.

[4]:
primary_broad_T = pybamm.PrimaryBroadcast(T, "negative particle")
disc_T = disc.process_symbol(primary_broad_T)
disc_T.evaluate(y=np.linspace(0, 1, 5))
[4]:
array([[0.  ],
       [0.  ],
       [0.  ],
       [0.25],
       [0.25],
       [0.25],
       [0.5 ],
       [0.5 ],
       [0.5 ],
       [0.75],
       [0.75],
       [0.75],
       [1.  ],
       [1.  ],
       [1.  ]])

The broadcasted object makes 3 (since the r-grid has 3 points) copies of each element of T and stacks them all up to give an object with size 3x5=15. In the resulting vector, the first 3 entries correspond to the 3 points in the r-domain at the first x-grid point (where T=0 uniformly in r), the next 3 entries correspond to the next 3 points in the r-domain at the second x-grid point (where T=0.25 uniformly in r), etc

Secondary broadcasts#

Secondary broadcasts are used to broadcast from a “smaller” scale to a “larger” scale, for example broadcasting SPM particle concentrations c_s(r) from the particles to the electrodes. Note that this wouldn’t be used to broadcast particle concentrations in the DFN, since these already depend on both x and r. To demonstrate this, we first create a variable c_s on the negative particle domain, discretise it, and evaluate it with a simple linear vector

[5]:
c_s = pybamm.Variable("c_s", domain="negative particle")
disc.set_variable_slices([c_s])
disc_c_s = disc.process_symbol(c_s)
disc_c_s.evaluate(y=np.linspace(0, 1, 3))
[5]:
array([[0. ],
       [0.5],
       [1. ]])

We then broadcast c_s onto the “negative electrode” domain (using secondary broadcast as we are going from the smaller particle scale to the large electrode scale), and discretise and evaluate the resulting object.

[6]:
secondary_broad_c_s = pybamm.SecondaryBroadcast(c_s, "negative electrode")
disc_broad_c_s = disc.process_symbol(secondary_broad_c_s)
disc_broad_c_s.evaluate(y=np.linspace(0, 1, 3))
[6]:
array([[0. ],
       [0.5],
       [1. ],
       [0. ],
       [0.5],
       [1. ],
       [0. ],
       [0.5],
       [1. ],
       [0. ],
       [0.5],
       [1. ],
       [0. ],
       [0.5],
       [1. ]])

The broadcasted object makes 5 (since the x-grid has 5 points) identical copies of the whole variable c_s to give an object with size 5x3=15. In the resulting vector, the first 3 entries correspond to the 3 points in the r-domain at the first x-grid point (where c_s varies in r), the next 3 entries correspond to the next 3 points in the r-domain at the second x-grid point (where c_s varies in r), etc

References#

The relevant papers for this notebook are:

[7]:
pybamm.print_citations()
[1] 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.
[2] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Comparison of PyBaMM and COMSOL Discharge Curves#

In this notebook we compare the discharge curves obtained by solving the DFN model both in PyBaMM and COMSOL. Results are presented for a range of C-rates, and we see an excellent agreement between the two implementations. If you would like to compare internal variables please see the script compare_comsol_DFN which creates a slider plot comparing potentials and concentrations as functions of time and space for a given C-rate. For more information on the DFN model, see the DFN notebook.

First we need to import pybamm, and then change our working directory to the root of the pybamm folder.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import pickle
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")

We then create a dictionary of the C-rates we would like to solve for and compare. Note that the repository currently only contains COMSOL results for the C-rates listed below.

[2]:
C_rates = {"01": 0.1, "05": 0.5, "1": 1, "2": 2, "3": 3}

We get the DFN model equations, geometry, and default parameters. Before processing the model, we adjust the electrode height and depth to be 1 m, and also adjust the electrode conductivities, to match the one-dimensional model we solved in COMSOL. The model is then processed using the default geometry and updated parameters. Finally, we create a mesh and discretise the model.

[3]:
# load model and geometry
model = pybamm.lithium_ion.DFN()
geometry = model.default_geometry

# load parameters and process model and geometry
param = model.default_parameter_values
param.update(
    {
        "Electrode width [m]": 1,
        "Electrode height [m]": 1,
        "Negative electrode conductivity [S.m-1]": 126,
        "Positive electrode conductivity [S.m-1]": 16.6,
        "Current function [A]": "[input]",
    }
)
param.process_model(model)
param.process_geometry(geometry)

# create mesh
var_pts = {"x_n": 31, "x_s": 11, "x_p": 31, "r_n": 11, "r_p": 11}
mesh = pybamm.Mesh(geometry, model.default_submesh_types, var_pts)

# discretise model
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model);

We create the figure by looping over the dictionary of C-rates. In each step of the loop we load the COMSOL results from a .csv file and solve the DFN model in pybamm. The output variables are then processed, allowing us to plot the discharges curve computed using pybamm and COMSOL, and their absolute difference.

[4]:
# create figure
fig, ax = plt.subplots(figsize=(15, 8))
plt.tight_layout()
plt.subplots_adjust(left=-0.1)
discharge_curve = plt.subplot(211)
plt.xlim([0, 26])
plt.ylim([3.2, 3.9])
plt.xlabel(r"Discharge Capacity (Ah)")
plt.ylabel("Voltage (V)")
plt.title(r"Comsol $\cdots$ PyBaMM $-$")
voltage_difference_plot = plt.subplot(212)
plt.xlim([0, 26])
plt.yscale("log")
plt.grid(True)
plt.xlabel(r"Discharge Capacity (Ah)")
plt.ylabel(r"$\vert V - V_{comsol} \vert$")
colors = iter(plt.cycler(color="bgrcmyk"))

# loop over C_rates dict to create plot
for key, C_rate in C_rates.items():
    # load the comsol results
    comsol_results_path = pybamm.get_parameters_filepath(
        f"input/comsol_results/comsol_{key}C.pickle",
    )
    comsol_variables = pickle.load(open(comsol_results_path, "rb"))
    comsol_time = comsol_variables["time"]
    comsol_voltage = comsol_variables["voltage"]

    # update current density
    current = 24 * C_rate

    # solve model at comsol times
    solver = pybamm.CasadiSolver(mode="fast")
    solution = solver.solve(
        model, comsol_time, inputs={"Current function [A]": current}
    )
    time_in_seconds = solution["Time [s]"].entries
    # discharge capacity
    discharge_capacity = solution["Discharge capacity [A.h]"]
    discharge_capacity_sol = discharge_capacity(time_in_seconds)
    comsol_discharge_capacity = comsol_time * current / 3600

    # extract the voltage
    voltage = solution["Voltage [V]"]
    voltage_sol = voltage(time_in_seconds)

    # calculate the difference between the two solution methods
    end_index = min(len(time_in_seconds), len(comsol_time))
    voltage_difference = np.abs(voltage_sol[0:end_index] - comsol_voltage[0:end_index])

    # plot discharge curves and absolute voltage_difference
    color = next(colors)["color"]
    discharge_curve.plot(
        comsol_discharge_capacity, comsol_voltage, color=color, linestyle=":"
    )
    discharge_curve.plot(
        discharge_capacity_sol,
        voltage_sol,
        color=color,
        linestyle="-",
        label=f"{C_rate} C",
    )
    voltage_difference_plot.plot(
        discharge_capacity_sol[0:end_index], voltage_difference, color=color
    )

discharge_curve.legend(loc="best")
plt.subplots_adjust(
    top=0.92, bottom=0.08, left=0.10, right=0.95, hspace=0.25, wspace=0.35
)
plt.show()
/var/folders/z4/5lmf5d5d23sc2gkhs__zfnfc0000gn/T/ipykernel_2839/4153409347.py:5: MatplotlibDeprecationWarning: Auto-removal of overlapping axes is deprecated since 3.6 and will be removed two minor releases later; explicitly call ax.remove() as needed.
  discharge_curve = plt.subplot(211)
_images/source_examples_notebooks_models_compare-comsol-discharge-curve_9_1.png

References#

The relevant papers for this notebook are:

[5]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Comparing with Experimental Data#

In this notebook we show how to compare results generated in PyBaMM with experimental data. We compare the results of the DFN model (see the DFN notebook) with the experimental data from Ecker et. al. [3]. Results are compared for a constant current discharge at 1C and at 5C.

First we import pybamm and any other packages required by this example, and then change our working directory to the root of the pybamm folder.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

We then load the Ecker data in from the .csv files using pandas

[2]:
voltage_data_1C = pd.read_csv(
    "pybamm/input/discharge_data/Ecker2015/Ecker_1C.csv", header=None
).to_numpy()
voltage_data_5C = pd.read_csv(
    "pybamm/input/discharge_data/Ecker2015/Ecker_5C.csv", header=None
).to_numpy()

Note that the data is Time [s] vs Voltage [V].

We load the DFN model and select the parameter set from the Ecker paper [1]. We update the C-rate an InputParameter so that we can re-run the same model at different C-rates without the need to rebuild the model. This is done by passing the flag [input].

[3]:
# choose DFN
model = pybamm.lithium_ion.DFN()

# pick parameters, keeping C-rate as an input to be changed for each solve
parameter_values = pybamm.ParameterValues("Ecker2015")
parameter_values.update({"Current function [A]": "[input]"})

For this comparison we choose a fine mesh of 1 finite volume per micron in the electrodes and separator and 1 finite volume per 0.1 micron in the particles

[4]:
var = pybamm.standard_spatial_vars
var_pts = {
    var.x_n: int(parameter_values.evaluate(model.param.n.L / 1e-6)),
    var.x_s: int(parameter_values.evaluate(model.param.s.L / 1e-6)),
    var.x_p: int(parameter_values.evaluate(model.param.p.L / 1e-6)),
    var.r_n: int(parameter_values.evaluate(model.param.n.prim.R_typ / 1e-7)),
    var.r_p: int(parameter_values.evaluate(model.param.p.prim.R_typ / 1e-7)),
}

We create a simulation using our model, parameters and number of grid points

[5]:
sim = pybamm.Simulation(model, parameter_values=parameter_values, var_pts=var_pts)

We can then solve the model for a 1C and 5C discharge

[6]:
C_rates = [1, 5]  # C-rates to solve for
capacity = parameter_values["Nominal cell capacity [A.h]"]
t_evals = [
    np.linspace(0, 3800, 100),
    np.linspace(0, 720, 100),
]  # times to return the solution at
solutions = [None] * len(C_rates)  # empty list that will hold solutions

# loop over C-rates
for i, C_rate in enumerate(C_rates):
    current = C_rate * capacity
    sim.solve(
        t_eval=t_evals[i],
        solver=pybamm.CasadiSolver(mode="fast"),
        inputs={"Current function [A]": current},
    )
    solutions[i] = sim.solution

Finally we plot the numerical solution against the experimental data

[7]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

# plot the 1C results
t_sol = solutions[0]["Time [s]"].entries
ax1.plot(t_sol, solutions[0]["Voltage [V]"](t_sol))
ax1.plot(voltage_data_1C[:, 0], voltage_data_1C[:, 1], "o")
ax1.set_xlabel("Time [s]")
ax1.set_ylabel("Voltage [V]")
ax1.set_title("1C")
ax1.legend(["DFN", "Experiment"], loc="best")

# plot the 5C results
t_sol = solutions[1]["Time [s]"].entries
ax2.plot(t_sol, solutions[1]["Voltage [V]"](t_sol))
ax2.plot(voltage_data_5C[:, 0], voltage_data_5C[:, 1], "o")
ax2.set_xlabel("Time [s]")
ax2.set_ylabel("Voltage [V]")
ax2.set_title("5C")
ax2.legend(["DFN", "Experiment"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_models_compare-ecker-data_15_0.png

For a 1C discharge we observe an excellent agreement between the model and experiment, both in terms of the overall shape of the curve and the capacity. The agreement between model and experiment is less good at 5C, but in line with other implementations of the DFN (e.g. [6]).

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[5] Madeleine Ecker, Stefan Käbitz, Izaro Laresgoiti, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: II. Model Validation. Journal of The Electrochemical Society, 162(9):A1849–A1857, 2015. doi:10.1149/2.0541509jes.
[6] Madeleine Ecker, Thi Kim Dung Tran, Philipp Dechent, Stefan Käbitz, Alexander Warnecke, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: I. Determination of Parameters. Journal of the Electrochemical Society, 162(9):A1836–A1848, 2015. doi:10.1149/2.0551509jes.
[7] Alastair Hales, Laura Bravo Diaz, Mohamed Waseem Marzook, Yan Zhao, Yatish Patel, and Gregory Offer. The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries. Journal of The Electrochemical Society, 166(12):A2383, 2019.
[8] 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.
[9] Giles Richardson, Ivan Korotkin, Rahifa Ranom, Michael Castle, and Jamie M. Foster. Generalised single particle models for high-rate operation of graded lithium-ion electrodes: systematic derivation and validation. Electrochimica Acta, 339:135862, 2020. doi:10.1016/j.electacta.2020.135862.
[10] 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.
[11] Yan Zhao, Yatish Patel, Teng Zhang, and Gregory J Offer. Modeling the effects of thermal gradients induced by tab and surface cooling on lithium ion cell performance. Journal of The Electrochemical Society, 165(13):A3169, 2018.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Compare lithium-ion battery models#

We compare three one-dimensional lithium-ion battery models: the Doyle-Fuller-Newman (DFN) model, the single particle model (SPM), and the single particle model with electrolyte (SPMe). Further details on these models can be found in [4].

Key steps:#

Comparing models consists of 6 easy steps:

  1. Load models and geometry

  2. Process parameters

  3. Mesh the geometry

  4. Discretise models

  5. Solve models

  6. Plot results

But, as always we first import pybamm and other required modules

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os

os.chdir(pybamm.__path__[0] + "/..")

import numpy as np
import matplotlib.pyplot as plt
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

1. Load models#

Since the three models we want to compare are already implemented in PyBaMM, they can be easy loaded using:

[2]:
dfn = pybamm.lithium_ion.DFN()
spme = pybamm.lithium_ion.SPMe()
spm = pybamm.lithium_ion.SPM()

To allow us to perform the same operations on each model easily, we create a dictionary of these three models:

[3]:
models = {"DFN": dfn, "SPM": spm, "SPMe": spme}

Each model can then be accessed using:

[4]:
models["DFN"]
[4]:
<pybamm.models.full_battery_models.lithium_ion.dfn.DFN at 0x7f917c709f40>

For each model, we must also provide a cell geometry. The geometry is different for different models; for example, the SPM has solves for a single particle in each electrode whereas the DFN solves for many particles. For simplicity, we use the default geometry associated with each model but note that this can be easily changed.

[5]:
geometry = {
    "DFN": dfn.default_geometry,
    "SPM": spm.default_geometry,
    "SPMe": spme.default_geometry,
}

2. Process parameters#

For simplicity, we use the default parameters values associated with the DFN model, but change the current function to be an input so that we can quickly solve the model with different currents

[6]:
param = dfn.default_parameter_values
param["Current function [A]"] = "[input]"

It is simple to change this to a different parameter set if desired.

We then process the parameters in each of the models and geometries using this parameter set:

[7]:
for model_name in models.keys():
    param.process_model(models[model_name])
    param.process_geometry(geometry[model_name])

3. Mesh geometry#

We use the defaults mesh properties (the types of meshes and number of points to be used) for simplicity to generate a mesh of each model geometry. We store these meshes in a dictionary of similar structure to the geometry and models dictionaries:

[8]:
mesh = {}
for model_name, model in models.items():
    mesh[model_name] = pybamm.Mesh(
        geometry[model_name], model.default_submesh_types, model.default_var_pts
    )

4. Discretise model#

We now discretise each model using its associated mesh and the default spatial method associated with the model:

[9]:
for model_name, model in models.items():
    disc = pybamm.Discretisation(mesh[model_name], model.default_spatial_methods)
    disc.process_model(model)

5. Solve model#

We now solve each model using the default solver associated with each model:

[10]:
timer = pybamm.Timer()
solutions = {}
t_eval = np.linspace(0, 3600, 300)  # time in seconds
for model_name, model in models.items():
    solver = pybamm.CasadiSolver()
    timer.reset()
    solution = solver.solve(model, t_eval, inputs={"Current function [A]": 1})
    print(f"Solved the {model.name} in {timer.time()}")
    solutions[model_name] = solution
Solved the Doyle-Fuller-Newman model in 266.815 ms
Solved the Single Particle Model in 20.571 ms
Solved the Single Particle Model with electrolyte in 38.459 ms

6. Plot results#

To plot results, we extract the variables from the solutions dictionary. Matplotlib can then be used to plot the voltage predictions of each models as follows:

[11]:
for model_name, model in models.items():
    time = solutions[model_name]["Time [s]"].entries
    voltage = solutions[model_name]["Voltage [V]"].entries
    plt.plot(time, voltage, lw=2, label=model.name)
plt.xlabel("Time [s]", fontsize=15)
plt.ylabel("Voltage [V]", fontsize=15)
plt.legend(fontsize=15)
plt.show()
_images/source_examples_notebooks_models_compare-lithium-ion_29_0.png

Alternatively the inbuilt QuickPlot functionality can be employed to compare a set of variables over the discharge. We must first create a list of the solutions

[12]:
list_of_solutions = list(solutions.values())

And then employ QuickPlot:

[13]:
quick_plot = pybamm.QuickPlot(list_of_solutions)
quick_plot.dynamic_plot();

Changing parameters#

Since we have made current an input, it is easy to change it and then perform the calculations again:

[14]:
# update parameter values and solve again
# simulate for shorter time
t_eval = np.linspace(0, 800, 300)
for model_name, model in models.items():
    solutions[model_name] = model.default_solver.solve(
        model, t_eval, inputs={"Current function [A]": 3}
    )

# Plot
list_of_solutions = list(solutions.values())
quick_plot = pybamm.QuickPlot(list_of_solutions)
quick_plot.dynamic_plot();

By increasing the current we observe less agreement between the models, as expected.

References#

The relevant papers for this notebook are:

[15]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[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.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Compare particle diffusion models#

In this notebook we compare the different models for mass transport within the electrode particles. For a full list of all the particle models, see the documentation.

With the “Fickian diffusion” option a diffusion equation is solved within the particle domain, with the boundary flux prescribed at the surface related to the local current density. Alternatively, one can assume a particular (polynomial) concentration profile within the particle (at present, this can be uniform, quadratic, or quartic). The “uniform profile” model assumes that the concentration inside the particle is uniform in space (and therefore equal to the surface concentration through the entire particle - in effect ignoring transport resistance within the particle), and solves an ODE for the average particle concentration. The “quadratic profile” model additionally solves an algebraic equation for the surface concentration, taking into account the effect of diffusion within the particle. Finally, the “quartic profile” model also solves for the average concentration gradient (the integral of \(\partial c/ \partial r\)) in the particle, giving a higher-order approximation to the concentration profile within the particle.

As the exchange current density is a function of surface concentration, we can see the effect the choice of particle model has on the voltage profile arising from different overpotentials.

First we import the packages we’re going to use

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os
import numpy as np
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

We then create a list of models with the different particle diffusion submodels using the options functionality

[2]:
particle_options = [
    "Fickian diffusion",
    "uniform profile",
    "quadratic profile",
    "quartic profile",
]
models = [
    pybamm.lithium_ion.DFN(options={"particle": opt}, name=opt)
    for opt in particle_options
]

Next we set up simulations for each model with the current set as an input function. We will change this later to observe the effect on the different models.

[3]:
simulations = []
for model in models:
    param = model.default_parameter_values
    param["Current function [A]"] = "[input]"
    simulations.append(pybamm.Simulation(model, parameter_values=param))
[4]:
t_eval = np.linspace(0, 3600, 72)

First we run the simulations for each model with a current of 0.68A - this corresponds to a C-rate of about 1 with the standard cell size configured in the default parameters.

[5]:
solutions_1C = []
for sim in simulations:
    sim.solve(t_eval, inputs={"Current function [A]": 0.68})
    solutions_1C.append(sim.solution)
    print(f"Particle model: {sim.model.name}")
    print(f"Solve time: {sim.solution.solve_time}s")
Particle model: Fickian diffusion
Solve time: 344.798 mss
Particle model: uniform profile
Solve time: 207.218 mss
Particle model: quadratic profile
Solve time: 220.810 mss
Particle model: quartic profile
Solve time: 284.064 mss

By not solving the diffusion problem in the particles explicitly, and instead assuming a polynomial profile, we can speed up the simulation.

[6]:
plt.figure(figsize=(15, 15))
style = ["k", "r*", "b^", "g--"]
for i in range(len(models)):
    plt.plot(
        solutions_1C[i]["Time [s]"].entries,
        solutions_1C[i]["Voltage [V]"].entries,
        style[i],
        label=particle_options[i],
    )
plt.legend()
plt.title("Model Comparison 1C")
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.grid()
_images/source_examples_notebooks_models_compare-particle-diffusion-models_11_0.png

We can see that the Fickian, quadratic and quartic profiles agree very well and that the uniform profile over-predicts the cell voltage and capacity by ignoring this transport resistance. The only significant difference between the Fickian and quadratic models is on the first datapoint when transient effects after the initial state may differ. Observe what happens next when we increase the current.

[7]:
t_eval = np.linspace(0, 1800, 72)
solutions_2C = []
for sim in simulations:
    sim.solve(t_eval, inputs={"Current function [A]": 2 * 0.68})
    solutions_2C.append(sim.solution)
[8]:
plt.figure(figsize=(15, 15))
for i in range(len(models)):
    plt.plot(
        solutions_2C[i]["Time [s]"].entries,
        solutions_2C[i]["Voltage [V]"].entries,
        style[i],
        label=particle_options[i],
    )
plt.legend()
plt.title("Model Comparison 2C")
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.grid()
_images/source_examples_notebooks_models_compare-particle-diffusion-models_14_0.png

The quadratic model is still much better at approximating Fickian diffusion and the relative error in the uniform model has increased. However, the initial error in the quadratic model has grown slightly. Increasing current even more will highlight the problem further. The quartic model is still providing an excellent match to the Fickian diffusion profile.

[9]:
t_eval = np.linspace(0, 360, 72)
solutions_6C = []
for sim in simulations:
    sim.solve(t_eval, inputs={"Current function [A]": 6 * 0.68})
    solutions_6C.append(sim.solution)
[10]:
plt.figure(figsize=(15, 15))
for i in range(len(models)):
    plt.plot(
        solutions_6C[i]["Time [s]"].entries,
        solutions_6C[i]["Voltage [V]"].entries,
        style[i],
        label=particle_options[i],
    )
plt.legend()
plt.title("Model Comparison 6C")
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.grid()
_images/source_examples_notebooks_models_compare-particle-diffusion-models_17_0.png

Now the quadratic profile assumption begins to breakdown and that initial error propagates much further into the discharge. Happily the quartic model is higher-order and the match to the Fickian profile is still very good.

Finally we can take a look at some of the internal states using PyBaMM’s dynamic_plot

[11]:
pybamm.dynamic_plot(solutions_6C);

References#

The relevant papers for this notebook are:

[12]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Venkat R. Subramanian, Vinten D. Diwakar, and Deepak Tapriyal. Efficient macro-micro scale coupled modeling of batteries. Journal of The Electrochemical Society, 152(10):A2002, 2005. doi:10.1149/1.2032427.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

A composite electrode particle model#

A composite electrode particle model is developed for (negative) electrodes with two phases, e.g. graphite/silicon in LG M50 battery cells. The current version is demonstrated for negative composite electrodes only but is easily extended to positive composite electrodes. The reference is at the end of this notebook.

How to use the model#

Let us set up PyBaMM

[1]:
# %pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import os
import matplotlib.pyplot as plt
import numpy as np
import pybamm
import timeit
from matplotlib import style

style.use("ggplot")
os.chdir(pybamm.__path__[0] + "/..")
pybamm.set_logging_level("INFO")

Choose the option {"particle phases": ("2", "1")} to load the composite electrode particle model by specifying that there are two particle phases (graphite and silicon) in the negative electrode. The parameter set “Chen2020_composite” includes parameters for silicon as a secondary particle

[2]:
start = timeit.default_timer()
model = pybamm.lithium_ion.DFN(
    {
        "particle phases": ("2", "1"),
        "open-circuit potential": (("single", "current sigmoid"), "single"),
    }
)
param = pybamm.ParameterValues("Chen2020_composite")

param.update({"Upper voltage cut-off [V]": 4.5})
param.update({"Lower voltage cut-off [V]": 2.5})

param.update(
    {
        "Primary: Maximum concentration in negative electrode [mol.m-3]": 28700,
        "Primary: Initial concentration in negative electrode [mol.m-3]": 23000,
        "Primary: Negative electrode diffusivity [m2.s-1]": 5.5e-14,
        "Secondary: Negative electrode diffusivity [m2.s-1]": 1.67e-14,
        "Secondary: Initial concentration in negative electrode [mol.m-3]": 277000,
        "Secondary: Maximum concentration in negative electrode [mol.m-3]": 278000,
    }
)
2023-02-21 09:08:46.318 - [INFO] base_model._build_model(550): Start building Doyle-Fuller-Newman model
2023-02-21 09:08:46.452 - [INFO] base_battery_model.build_model(970): Finish building Doyle-Fuller-Newman model

Single Cycle Simulations#

Define a current loading

[3]:
C_rate = 0.5
capacity = param["Nominal cell capacity [A.h]"]
I_load = C_rate * capacity

t_eval = np.linspace(0, 10000, 1000)

param["Current function [A]"] = I_load

It is very easy to vary the relative volume fraction of each phase. The following example shows how to compare the results of batteries with three relative volume fractions (0.001, 0.04, 0.1) of silicon.

[4]:
v_si = [0.001, 0.04, 0.1]
total_am_volume_fraction = 0.75
solution = []
for v in v_si:
    param.update(
        {
            "Primary: Negative electrode active material volume fraction": (1 - v)
            * total_am_volume_fraction,  # primary
            "Secondary: Negative electrode active material volume fraction": v
            * total_am_volume_fraction,
        }
    )
    print(v)
    sim = pybamm.Simulation(
        model,
        parameter_values=param,
        solver=pybamm.CasadiSolver(dt_max=5),
    )
    solution.append(sim.solve(t_eval=t_eval))
stop = timeit.default_timer()
print("running time: " + str(stop - start) + "s")
2023-02-21 09:08:46.528 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
0.001
2023-02-21 09:08:46.741 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:08:46.743 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:08:46.750 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:08:47.292 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:08:47.293 - [INFO] base_solver.solve(703): Start solving Doyle-Fuller-Newman model with CasADi solver with 'safe' mode
2023-02-21 09:08:47.297 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:08:47.433 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:08:55.129 - [INFO] base_solver.solve(937): Finish solving Doyle-Fuller-Newman model (event: Minimum voltage)
2023-02-21 09:08:55.130 - [INFO] base_solver.solve(938): Set-up time: 136.675 ms, Solve time: 7.685 s (of which integration time: 6.012 s), Total time: 7.821 s
2023-02-21 09:08:55.137 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:08:55.250 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:08:55.252 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:08:55.260 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
0.04
2023-02-21 09:08:55.808 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:08:55.809 - [INFO] base_solver.solve(703): Start solving Doyle-Fuller-Newman model with CasADi solver with 'safe' mode
2023-02-21 09:08:55.814 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:08:55.947 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:06.233 - [INFO] base_solver.solve(937): Finish solving Doyle-Fuller-Newman model (event: Minimum voltage)
2023-02-21 09:09:06.234 - [INFO] base_solver.solve(938): Set-up time: 134.145 ms, Solve time: 10.272 s (of which integration time: 8.058 s), Total time: 10.407 s
2023-02-21 09:09:06.242 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:06.444 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
0.1
2023-02-21 09:09:06.447 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:06.453 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:07.047 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:07.048 - [INFO] base_solver.solve(703): Start solving Doyle-Fuller-Newman model with CasADi solver with 'safe' mode
2023-02-21 09:09:07.052 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:07.200 - [INFO] base_solver.set_up(236): Finish solver set-up
At t = 0.00076482 and h = 3.55373e-22, the corrector convergence failed repeatedly or with |h| = hmin.
At t = 0.000250492 and h = 1.20507e-16, the corrector convergence failed repeatedly or with |h| = hmin.
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:849: Calculating Jacobian failed
At t = 0.000250493 and h = 2.0588e-18, the corrector convergence failed repeatedly or with |h| = hmin.
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
At t = 0.000121913 and h = 6.48118e-25, the corrector convergence failed repeatedly or with |h| = hmin.
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
At t = 5.76225e-05 and h = 3.13053e-22, the corrector convergence failed repeatedly or with |h| = hmin.
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
At t = 2.54755e-05 and h = 4.28889e-31, the corrector convergence failed repeatedly or with |h| = hmin.
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
psetup failed: .../casadi/interfaces/sundials/idas_interface.cpp:852: Linear solve failed
At t = 9.40462e-06 and h = 4.82262e-22, the corrector convergence failed repeatedly or with |h| = hmin.
2023-02-21 09:09:21.630 - [INFO] base_solver.solve(937): Finish solving Doyle-Fuller-Newman model (event: Minimum voltage)
2023-02-21 09:09:21.631 - [INFO] base_solver.solve(938): Set-up time: 148.688 ms, Solve time: 14.417 s (of which integration time: 9.176 s), Total time: 14.566 s
running time: 35.389444837s

Results#

Compare the cell voltages of the three cells in this example, to see how silicon affects the output capacity

[5]:
ltype = ["k-", "r--", "b-.", "g:", "m-", "c--", "y-."]
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    V_i = solution[i]["Voltage [V]"].entries
    plt.plot(t_i, V_i, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Voltage [V]")
plt.legend()
[5]:
<matplotlib.legend.Legend at 0x12b1849a0>
_images/source_examples_notebooks_models_composite_particle_12_1.png

Results of interfacial current density in silicon

[6]:
plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    j_n_p1_av = solution[i][
        "X-averaged negative electrode primary interfacial current density [A.m-2]"
    ].entries
    plt.plot(t_i, j_n_p1_av, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Averaged interfacial current density [A/m$^{2}$]")
plt.legend()
plt.title("Graphite")

plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    j_n_p2_av = solution[i][
        "X-averaged negative electrode secondary interfacial current density [A.m-2]"
    ].entries
    plt.plot(t_i, j_n_p2_av, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Averaged interfacial current density [A/m$^{2}$]")
plt.legend()
plt.title("Silicon")
[6]:
Text(0.5, 1.0, 'Silicon')
_images/source_examples_notebooks_models_composite_particle_14_1.png
_images/source_examples_notebooks_models_composite_particle_14_2.png

Results of interfacial current density in graphite

[7]:
plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    j_n_p1_Vav = solution[i][
        "X-averaged negative electrode primary volumetric interfacial current density [A.m-3]"
    ].entries
    plt.plot(t_i, j_n_p1_Vav, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Averaged volumetric interfacial current density [A/m$^{3}$]")
plt.legend()
plt.title("Graphite")

plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    j_n_p2_Vav = solution[i][
        "X-averaged negative electrode secondary volumetric interfacial current density [A.m-3]"
    ].entries
    plt.plot(t_i, j_n_p2_Vav, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Averaged volumetric interfacial current density [A/m$^{3}$]")
plt.legend()
plt.title("Silicon")
[7]:
Text(0.5, 1.0, 'Silicon')
_images/source_examples_notebooks_models_composite_particle_16_1.png
_images/source_examples_notebooks_models_composite_particle_16_2.png

Results of average lithium concentration

[8]:
plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    c_s_xrav_n_p1 = solution[i][
        "Average negative primary particle concentration"
    ].entries
    plt.plot(t_i, c_s_xrav_n_p1, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("$c_\mathrm{g}/c_\mathrm{g,max}$")
plt.legend()
plt.title("Graphite")

plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    c_s_xrav_n_p2 = solution[i][
        "Average negative secondary particle concentration"
    ].entries
    plt.plot(t_i, c_s_xrav_n_p2, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("$c_\mathrm{si}/c_\mathrm{si,max}$")
plt.legend()
plt.title("Silicon")
[8]:
Text(0.5, 1.0, 'Silicon')
_images/source_examples_notebooks_models_composite_particle_18_1.png
_images/source_examples_notebooks_models_composite_particle_18_2.png

Results of equilibrium potential

[9]:
plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    ocp_p1 = solution[i][
        "X-averaged negative electrode primary open-circuit potential [V]"
    ].entries
    plt.plot(t_i, ocp_p1, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Equilibruim potential [V]")
plt.legend()
plt.title("Graphite")

plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    ocp_p2 = solution[i][
        "X-averaged negative electrode secondary open-circuit potential [V]"
    ].entries
    plt.plot(t_i, ocp_p2, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Equilibruim potential [V]")
plt.legend()
plt.title("Silicon")

plt.figure()
for i in range(0, len(v_si)):
    t_i = solution[len(v_si) - 1 - i]["Time [s]"].entries / 3600
    ocp_p = solution[len(v_si) - 1 - i][
        "X-averaged positive electrode open-circuit potential [V]"
    ].entries
    plt.plot(
        t_i,
        ocp_p,
        ltype[len(v_si) - 1 - i],
        label="$V_\mathrm{si}=$" + str(v_si[len(v_si) - 1 - i]),
    )
plt.xlabel("Time [h]")
plt.ylabel("Equilibrium potential [V]")
plt.legend()
plt.title("NMC811")
[9]:
Text(0.5, 1.0, 'NMC811')
_images/source_examples_notebooks_models_composite_particle_20_1.png
_images/source_examples_notebooks_models_composite_particle_20_2.png
_images/source_examples_notebooks_models_composite_particle_20_3.png

Multi-Cycle Simulations#

For multi-cycling, an experiment definition for static C/2 discharge and charge cycling is presented.

[10]:
experiment = pybamm.Experiment(
    [
        (
            "Discharge at C/2 until 3.0 V",
            "Rest for 1 hour",
            "Charge at C/2 until 4.2 V",
            "Rest for 1 hour",
        ),
    ]
    * 2
)
2023-02-21 09:09:26.957 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:26.959 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:26.961 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:26.963 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:26.964 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:26.965 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:26.966 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:26.967 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.

The solution is reintroduced, with calc_esoh=False passed into the solve function. Currently, composite electrode state of health predictions are not included in this model.

[11]:
solution = []
for v in v_si:
    param.update(
        {
            "Primary: Negative electrode active material volume fraction": (1 - v)
            * total_am_volume_fraction,  # primary
            "Secondary: Negative electrode active material volume fraction": v
            * total_am_volume_fraction,
        }
    )
    print(v)
    sim = pybamm.Simulation(
        model,
        experiment=experiment,
        parameter_values=param,
        solver=pybamm.CasadiSolver(dt_max=5),
    )
    solution.append(sim.solve(calc_esoh=False))
stop = timeit.default_timer()
print("running time: " + str(stop - start) + "s")
2023-02-21 09:09:27.094 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:27.096 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:27.097 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:27.098 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:27.099 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:27.099 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:27.100 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:27.101 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:27.115 - [INFO] callbacks.on_experiment_start(166): Start running experiment
2023-02-21 09:09:27.118 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:27.244 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:27.246 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:27.362 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
0.001
2023-02-21 09:09:27.364 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:27.482 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:27.486 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:27.492 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:28.059 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:28.060 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:28.066 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:28.601 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:28.602 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:28.610 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:29.139 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:29.140 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/2 (20.167 us elapsed) --------------------
2023-02-21 09:09:29.141 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:29.145 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:29.290 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:30.657 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:30.661 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:30.800 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:31.643 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:09:31.647 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:31.796 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:33.027 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 4/4: Rest for 1 hour
2023-02-21 09:09:33.910 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/2 (4.771 s elapsed) --------------------
2023-02-21 09:09:33.911 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:35.119 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:35.867 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:09:37.032 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 4/4: Rest for 1 hour
2023-02-21 09:09:37.783 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 8.643 s
2023-02-21 09:09:37.784 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:37.785 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:37.785 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:37.786 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:37.788 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:37.789 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:37.790 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:37.791 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:37.792 - [INFO] callbacks.on_experiment_start(166): Start running experiment
2023-02-21 09:09:37.794 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:37.912 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:37.914 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:38.035 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
0.04
2023-02-21 09:09:38.037 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:38.341 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:38.344 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:38.349 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:38.914 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:38.915 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:38.922 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:39.441 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:39.442 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:39.448 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:39.986 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:39.987 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/2 (16.366 us elapsed) --------------------
2023-02-21 09:09:39.988 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:39.993 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:40.141 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:41.853 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:41.858 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:41.990 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:42.685 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:09:42.690 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:42.834 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:44.378 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 4/4: Rest for 1 hour
2023-02-21 09:09:45.649 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/2 (5.662 s elapsed) --------------------
2023-02-21 09:09:45.650 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:47.096 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:47.757 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:09:49.184 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 4/4: Rest for 1 hour
2023-02-21 09:09:49.955 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 9.968 s
2023-02-21 09:09:49.956 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:49.957 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:49.958 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:49.959 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:49.960 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Discharge at C/2 until 3.0 V', using temperature from parameter values.
2023-02-21 09:09:49.961 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:49.962 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Charge at C/2 until 4.2 V', using temperature from parameter values.
2023-02-21 09:09:49.963 - [WARNING] experiment._read_and_drop_temperature(431): Temperature not found on step: 'Rest for 1 hour', using temperature from parameter values.
2023-02-21 09:09:49.964 - [INFO] callbacks.on_experiment_start(166): Start running experiment
2023-02-21 09:09:49.966 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:50.085 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:50.088 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:50.206 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
0.1
2023-02-21 09:09:50.209 - [INFO] parameter_values.process_model(425): Start setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:50.322 - [INFO] parameter_values.process_model(527): Finish setting parameters for Doyle-Fuller-Newman model
2023-02-21 09:09:50.326 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:50.332 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:50.868 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:50.869 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:50.877 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:51.557 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:51.557 - [INFO] discretisation.process_model(147): Start discretising Doyle-Fuller-Newman model
2023-02-21 09:09:51.563 - [INFO] discretisation.remove_independent_variables_from_rhs(1120): removing variable Discharge capacity [A.h] from rhs
2023-02-21 09:09:52.101 - [INFO] discretisation.process_model(259): Finish discretising Doyle-Fuller-Newman model
2023-02-21 09:09:52.101 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/2 (16.504 us elapsed) --------------------
2023-02-21 09:09:52.102 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:52.108 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:52.250 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:54.101 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:54.107 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:54.236 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:54.910 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:09:54.914 - [INFO] base_solver.set_up(111): Start solver set-up
2023-02-21 09:09:55.056 - [INFO] base_solver.set_up(236): Finish solver set-up
2023-02-21 09:09:56.719 - [NOTICE] callbacks.on_step_start(182): Cycle 1/2, step 4/4: Rest for 1 hour
2023-02-21 09:09:57.598 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/2 (5.496 s elapsed) --------------------
2023-02-21 09:09:57.598 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 1/4: Discharge at C/2 until 3.0 V
2023-02-21 09:09:59.232 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 2/4: Rest for 1 hour
2023-02-21 09:09:59.886 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 3/4: Charge at C/2 until 4.2 V
2023-02-21 09:10:01.516 - [NOTICE] callbacks.on_step_start(182): Cycle 2/2, step 4/4: Rest for 1 hour
2023-02-21 09:10:02.299 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 10.198 s
running time: 76.058786603s

Cycling Results#

The previously displayed single discharge results can be extended to the cycling solution. As an example, voltage is displayed below.

[12]:
ltype = ["k-", "r--", "b-.", "g:", "m-", "c--", "y-."]
for i in range(0, len(v_si)):
    t_i = solution[i]["Time [s]"].entries / 3600
    V_i = solution[i]["Voltage [V]"].entries
    plt.plot(t_i, V_i, ltype[i], label="$V_\mathrm{si}=$" + str(v_si[i]))
plt.xlabel("Time [h]")
plt.ylabel("Voltage [V]")
plt.legend()
[12]:
<matplotlib.legend.Legend at 0x12c290850>
_images/source_examples_notebooks_models_composite_particle_26_1.png

References#

The relevant papers for this notebook are:

[13]:
pybamm.print_citations()
[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.
[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.
[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.
[4] 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.
[5] 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.
[6] 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.
[7] 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.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Modelling coupled degradation mechanisms in PyBaMM#

This notebook shows how to set up a PyBaMM model in which many degradation mechanisms run at the same time and interact with one another.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt

[notice] A new release of pip available: 22.3.1 -> 23.1.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

O’Kane et al. [10] modelled four coupled degradation mechanisms: SEI growth, lithium plating, particle cracking and stress-driven loss of active material. The “SEI on cracks” option couples SEI growth and particle cracking by allowing SEI to grow on the cracks. The “partially reversible” option for lithium plating allows the SEI to influence the irreversible component of plating using a function in the OKane2022 parameter file. Particle cracking and stress-driven loss of active material are coupled by default because the stress-strain relations inside the particles are an input for both.

[2]:
model = pybamm.lithium_ion.DFN(
    {
        "SEI": "solvent-diffusion limited",
        "SEI porosity change": "true",
        "lithium plating": "partially reversible",
        "lithium plating porosity change": "true",  # alias for "SEI porosity change"
        "particle mechanics": ("swelling and cracking", "swelling only"),
        "SEI on cracks": "true",
        "loss of active material": "stress-driven",
        "calculate discharge energy": "true",  # for compatibility with older PyBaMM versions
    }
)

Depending on the parameter set being used, the particle cracking model can require a large number of mesh points inside the particles to be numerically stable.

[3]:
param = pybamm.ParameterValues("OKane2022")
var_pts = {
    "x_n": 5,  # negative electrode
    "x_s": 5,  # separator
    "x_p": 5,  # positive electrode
    "r_n": 30,  # negative particle
    "r_p": 30,  # positive particle
}

Define a cycling protocol and solve. The protocol from O’Kane et al. [10] is used here, except with 10 ageing cycles instead of 1000.

[4]:
cycle_number = 10
exp = pybamm.Experiment(
    [
        "Hold at 4.2 V until C/100 (5 minute period)",
        "Rest for 4 hours (5 minute period)",
        "Discharge at 0.1C until 2.5 V (5 minute period)",  # initial capacity check
        "Charge at 0.3C until 4.2 V (5 minute period)",
        "Hold at 4.2 V until C/100 (5 minute period)",
    ]
    + [
        (
            "Discharge at 1C until 2.5 V",  # ageing cycles
            "Charge at 0.3C until 4.2 V (5 minute period)",
            "Hold at 4.2 V until C/100 (5 minute period)",
        )
    ]
    * cycle_number
    + ["Discharge at 0.1C until 2.5 V (5 minute period)"],  # final capacity check
)
sim = pybamm.Simulation(model, parameter_values=param, experiment=exp, var_pts=var_pts)
sol = sim.solve()

Three of the degradation mechanisms - SEI, lithium plating and SEI on cracks - cause loss of lithium inventory (LLI). Plotting the different contributions to LLI against throughput capacity as opposed to cycle number allows them to be considered as continuous variables as opposed to discrete ones.

[5]:
Qt = sol["Throughput capacity [A.h]"].entries
Q_SEI = sol["Loss of capacity to negative SEI [A.h]"].entries
Q_SEI_cr = sol["Loss of capacity to negative SEI on cracks [A.h]"].entries
Q_plating = sol["Loss of capacity to negative lithium plating [A.h]"].entries
Q_side = sol["Total capacity lost to side reactions [A.h]"].entries
Q_LLI = (
    sol["Total lithium lost [mol]"].entries * 96485.3 / 3600
)  # convert from mol to A.h
plt.figure()
plt.plot(Qt, Q_SEI, label="SEI", linestyle="dashed")
plt.plot(Qt, Q_SEI_cr, label="SEI on cracks", linestyle="dashdot")
plt.plot(Qt, Q_plating, label="Li plating", linestyle="dotted")
plt.plot(Qt, Q_side, label="All side reactions", linestyle=(0, (6, 1)))
plt.plot(Qt, Q_LLI, label="All LLI")
plt.xlabel("Throughput capacity [A.h]")
plt.ylabel("Capacity loss [A.h]")
plt.legend()
plt.show()
_images/source_examples_notebooks_models_coupled-degradation_9_0.png

The capacity loss over 10 cycles is so small that the reversible component of the lithium plating is has a larger effect than all the irreversible mechanisms combined. Most of the irreversible capacity fade that does occur is caused by SEI on cracks.

The stress-driven loss of active material (LAM) mechanism [10,11] is also included, so the three main degradation modes - LLI and LAM in each electrode - can be plotted and compared.

[6]:
Qt = sol["Throughput capacity [A.h]"].entries
LLI = sol["Loss of lithium inventory [%]"].entries
LAM_neg = sol["Loss of active material in negative electrode [%]"].entries
LAM_pos = sol["Loss of active material in positive electrode [%]"].entries
plt.figure()
plt.plot(Qt, LLI, label="LLI")
plt.plot(Qt, LAM_neg, label="LAM (negative)")
plt.plot(Qt, LAM_pos, label="LAM (positive)")
plt.xlabel("Throughput capacity [A.h]")
plt.ylabel("Degradation modes [%]")
plt.legend()
plt.show()
_images/source_examples_notebooks_models_coupled-degradation_12_0.png

Both the reversible and irreversible components of LLI are far greater than LAM for this parameter set.

A key internal variable is the porosity. Pore clogging by SEI, lithium plating and other means can trigger other degradation mechanisms and reduce the rate capability of the cell. If the porosity reaches zero, the cell becomes completely unusable and PyBaMM will terminate the simulation.

[7]:
eps_neg_avg = sol["X-averaged negative electrode porosity"].entries
eps_neg_sep = sol["Negative electrode porosity"].entries[-1, :]
eps_neg_CC = sol["Negative electrode porosity"].entries[0, :]
plt.figure()
plt.plot(Qt, eps_neg_avg, label="Average")
plt.plot(Qt, eps_neg_sep, label="Separator", linestyle="dotted")
plt.plot(Qt, eps_neg_CC, label="Current collector", linestyle="dashed")
plt.xlabel("Throughput capacity [A.h]")
plt.ylabel("Negative electrode porosity")
plt.legend()
plt.show()
_images/source_examples_notebooks_models_coupled-degradation_15_0.png

If you want to see some serious degradation, try re-running the simulation with more ageing cycles, or using param.update({}) to increase the degradation parameters beyond the ranges considered by O’Kane et al. [10]

[8]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.
[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.
[9] Simon E. J. O'Kane, Ian D. Campbell, Mohamed W. J. Marzook, Gregory J. Offer, and Monica Marinescu. Physical origin of the differential voltage minimum associated with lithium plating in li-ion batteries. Journal of The Electrochemical Society, 167(9):090540, may 2020. URL: https://doi.org/10.1149/1945-7111/ab90ac, doi:10.1149/1945-7111/ab90ac.
[10] Simon E. J. O'Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.
[11] 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.
[12] 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.
[13] 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.
[14] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Doyle-Fuller-Newman Model with particle-size distributions#

This notebook demonstrates the extension of the Doyle-Fuller-Newman (DFN) model to include a distribution of particle sizes at every macroscale location (e.g. through-cell coordinate \(x\)) within the electrodes. This model, referred to here as the Many-Particle DFN (or MP-DFN), also extends the ideas of the Many-Particle Model or MPM (an \(x\)-averaged model) into the DFN framework. Note: this differs from a “size distribution in x”, where the particle size is a function of \(x\) but with only a single size at any given value of \(x\).

The MPM notebook describes how the particle-size distributions are implemented in PyBaMM, and how to input parameters and plot some relevant output variables. The model equations for the MP-DFN, which allow for variations in the through-cell \(x\) direction and therefore include electrolyte dynamics, are similar to those of the MPM and DFN, and are available in [5].

By default per electrode, the DFN has 1 microscale dimension (the radial coordinate within the active particles, \(r_{\mathrm{k}}\), \(\mathrm{k}=\mathrm{n,p}\)) and 1 macroscale dimension (the through-cell coordinate \(x\)), and is commonly called “pseudo-2D”. The MP-DFN adds another microscale variation with particle size \(R_{\mathrm{k}}\), \(\mathrm{k}=\mathrm{n,p}\) over an interval \([R_{\mathrm{k,min}}, R_{\mathrm{k,max}}]\), \(\mathrm{k}=\mathrm{n,p}\), and thus can be thought of as “pseudo-3D”. See [5] for more details.

Example solving the MP-DFN#

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

The MP-DFN can be accessed from the DFN model in PyBaMM, and specifying the "particle size" option. The default option for "particle size" is "single". Let’s change this to "distribution" and pass to the DFN when loading the model.

[2]:
# choose option(s)
options = {"particle size": "distribution"}

# load model
model = pybamm.lithium_ion.DFN(options=options, name="MP-DFN")

Adding size distribution parameters#

We now need to input and set the particle-size distributions and the minimum and maximum sizes for each electrode. Values for these are not currently in the default parameter set for the DFN, but they are easily added (to any parameter set of choice) - see the MPM notebook for more details.

Here, we start from the Marquis et al. (2019) [6] parameter set and use the convenience method pybamm.get_size_distribution_parameters(), which adds lognormals for the area-weighted size distribution in each electrode. By default, it chooses the "Negative particle radius [m]" and "Positive particle radius [m]" values already in the parameter set to be the mean of the lognormals, and sets the standard deviations to be 0.3 times the mean. (All parameters can be overwritten with keyword arguments.)

[3]:
# Base parameter set (no distribution parameters by default)
params = pybamm.ParameterValues("Marquis2019")

# Add distribution parameters to the set, with default values (lognormals)
params = pybamm.get_size_distribution_parameters(params)

Solve#

Solve for a 1C discharge

[4]:
# load parameter values into simuluation
solver = pybamm.CasadiSolver(mode="fast")
sim = pybamm.Simulation(model, parameter_values=params, solver=solver)

# solve
sim.solve(t_eval=[0, 3500])
[4]:
<pybamm.solvers.solution.Solution at 0x7fd4e17c43a0>
[5]:
# plot some variables that depend on particle size
output_variables = [
    "Negative particle surface concentration distribution [mol.m-3]",
    "Positive particle surface concentration distribution [mol.m-3]",
    "X-averaged negative particle surface concentration distribution [mol.m-3]",
    "Negative area-weighted particle-size distribution [m-1]",
    "Positive area-weighted particle-size distribution [m-1]",
    "Voltage [V]",
]

sim.plot(output_variables=output_variables)
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd4e17c45b0>

Variables that depend on particle size have names that end in "distribution", e.g. "Negative particle surface concentration distribution". The same variables without the "distribution" have been averaged over particle size, and can be compared to the corresponding variable with the same name from other models that have a single particle size (e.g. SPM).

The particle size distributions do not vary with \(x\), so you can use the “X-averaged” versions to better visualize the discretized distribution in \(R\) that PyBaMM uses.

[6]:
# The discrete sizes or "bins" used
R_p = sim.solution["Positive particle sizes [m]"].entries[
    :, 0, 0
]  # const in the x and current collector direction
R_n = sim.solution["Negative particle sizes [m]"].entries[:, 0, 0]

# The distributions (number, area, and volume-weighted)
f_a_p = sim.solution[
    "X-averaged positive area-weighted particle-size distribution [m-1]"
].entries[:, 0]
f_num_p = sim.solution[
    "X-averaged positive number-based particle-size distribution [m-1]"
].entries[:, 0]
f_v_p = sim.solution[
    "X-averaged positive volume-weighted particle-size distribution [m-1]"
].entries[:, 0]
f_a_n = sim.solution[
    "X-averaged negative area-weighted particle-size distribution [m-1]"
].entries[:, 0]
f_num_n = sim.solution[
    "X-averaged negative number-based particle-size distribution [m-1]"
].entries[:, 0]
f_v_n = sim.solution[
    "X-averaged negative volume-weighted particle-size distribution [m-1]"
].entries[:, 0]

# plot
f, axs = plt.subplots(1, 2, figsize=(10, 4))

# negative electrode
width_n = (R_n[-1] - R_n[-2]) / 1e-6
axs[0].bar(
    R_n / 1e-6,
    f_a_n * 1e-6,
    width=width_n,
    alpha=0.3,
    color="tab:blue",
    label="area-weighted",
)
axs[0].bar(
    R_n / 1e-6,
    f_num_n * 1e-6,
    width=width_n,
    alpha=0.3,
    color="tab:red",
    label="number-weighted",
)
axs[0].bar(
    R_n / 1e-6,
    f_v_n * 1e-6,
    width=width_n,
    alpha=0.3,
    color="tab:green",
    label="volume-weighted",
)
axs[0].set_xlim((0, 25))
axs[0].set_xlabel("Particle size $R_{\mathrm{n}}$ [$\mu$m]", fontsize=12)
axs[0].set_ylabel("[$\mu$m$^{-1}$]", fontsize=12)
axs[0].legend(fontsize=10)
axs[0].set_title("Discretized distributions (histograms) in negative electrode")

# positive electrode
width_p = (R_p[-1] - R_p[-2]) / 1e-6
axs[1].bar(
    R_p / 1e-6,
    f_a_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:blue",
    label="area-weighted",
)
axs[1].bar(
    R_p / 1e-6,
    f_num_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:red",
    label="number-weighted",
)
axs[1].bar(
    R_p / 1e-6,
    f_v_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:green",
    label="volume-weighted",
)
axs[1].set_xlim((0, 25))
axs[1].set_xlabel("Particle size $R_{\mathrm{p}}$ [$\mu$m]", fontsize=12)
axs[1].set_ylabel("[$\mu$m$^{-1}$]", fontsize=12)
axs[1].set_title("Positive electrode")
plt.tight_layout()
plt.show()
_images/source_examples_notebooks_models_DFN-with-particle-size-distributions_14_0.png

Custom size distributions#

Here we show how to change the distribution parameters, or set any distribution of choice. We will set the distribution parameters for the positive distribution (leaving the one for the negative electrode as the default lognormal). We refer to the MPM notebook for more examples.

[7]:
# Set the area-weighted mean radius to be the reference value from the parameter set
R_av_p_dim = params["Positive particle radius [m]"]

# Standard deviation (dimensional)
sd_p_dim = 0.6 * R_av_p_dim

# Minimum and maximum particle sizes (dimensional)
R_min_p = 0
R_max_p = 3 * R_av_p_dim

# Set the area-weighted particle-size distribution.
# Choose a lognormal (but any pybamm function could be used)


def f_a_dist_p_dim(R):
    return pybamm.lognormal(R, R_av_p_dim, sd_p_dim)


# Note: the only argument must be the particle size R
[8]:
# input params to the dictionary
distribution_params = {
    "Positive minimum particle radius [m]": R_min_p,
    "Positive maximum particle radius [m]": R_max_p,
    "Positive area-weighted " + "particle-size distribution [m-1]": f_a_dist_p_dim,
}
params.update(distribution_params, check_already_exists=False)

Solve and plot

[9]:
# load parameter values into simulation
sim_custom = pybamm.Simulation(model, parameter_values=params, solver=solver)

# solve
sim_custom.solve(t_eval=[0, 3500])

# plot
output_variables = [
    "X-averaged negative area-weighted particle-size distribution [m-1]",
    "X-averaged positive area-weighted particle-size distribution [m-1]",
    "Voltage [V]",
]
quickplot = pybamm.QuickPlot(
    [sim, sim_custom],
    output_variables=output_variables,
    labels=["default lognormals", "custom"],
)
quickplot.plot(0)
_images/source_examples_notebooks_models_DFN-with-particle-size-distributions_20_0.png

Compare MP-DFN to MPM and DFN models#

The MP-DFN is an extension of the MPM (size distributions, but no x variation) and DFN models (x variation, but no size distribution). Here we compare the three for the same parameter values, for a discharge followed by a relaxation. Note: this is implemented here by specifying the current function to be a Heaviside, not using the Experiment class.

[10]:
models = [
    pybamm.lithium_ion.DFN(options={"particle size": "distribution"}, name="MP-DFN"),
    pybamm.lithium_ion.MPM(name="MPM"),
    pybamm.lithium_ion.DFN(name="DFN"),
]

# parameters
params = pybamm.ParameterValues("Marquis2019")
params = pybamm.get_size_distribution_parameters(params)

# define current function
t_cutoff = 3450  # [s]
t_rest = 3600  # [s]
I_typ = params["Nominal cell capacity [A.h]"]  # current for 1C


def current(t):
    return I_typ * pybamm.EqualHeaviside(t, t_cutoff)


params.update({"Current function [A]": current})
t_eval = [0, t_cutoff + t_rest]

# solve
sims = []
for model in models:
    sim = pybamm.Simulation(model, parameter_values=params, solver=solver)
    sim.solve(t_eval=t_eval)
    sims.append(sim)
[11]:
# plot current, voltage
qp = pybamm.QuickPlot(sims, output_variables=["Current [A]", "Voltage [V]"])
qp.plot(0)
_images/source_examples_notebooks_models_DFN-with-particle-size-distributions_24_0.png

During discharge, the MPM overpredicts the voltage since it neglects the transport (and therefore resistances) through the electrolyte. During the relaxation portion, the DFN overpredicts the rate of relaxation to equilibrium compared to the MP-DFN and MPM. However, the slower relaxation of the size distribution models has been shown to agree better with experiment [5].

References#

The relevant papers for this notebook are:

[12]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] Toby L. Kirk, Jack Evans, Colin P. Please, and S. Jonathan Chapman. Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions. arXiv:2006.12208, 2020. URL: https://arxiv.org/abs/2006.12208, arXiv:2006.12208.
[5] Toby L. Kirk, Colin P. Please, and S. Jon Chapman. Physical modelling of the slow voltage relaxation phenomenon in lithium-ion batteries. Journal of The Electrochemical Society, 168(6):060554, jun 2021. URL: https://doi.org/10.1149/1945-7111/ac0bf7, doi:10.1149/1945-7111/ac0bf7.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Doyle-Fuller-Newman Model (DFN)#

Model Equations#

The DFN comprises equations for charge and mass conservation in the solid the solid and electrolyte, and also prescribes behaviour for the electrochemical reactions occurring on the interface between the solid an electrolyte. For more information please see [2] or other standard texts.

Below we summarise the model, with all parameters give in the table at the end of this notebook. Here we use a roman subscript \(\text{k} \in \text{n, s, p}\) is used to denote the regions negative electrode, separator, and positive electrode, respectively.

The model equations for the DFN read:

Charge conservation:#
\[\begin{split}\frac{\partial i_{\text{e,k}}}{\partial x} = \begin{cases} a_{\text{k}}j_{\text{k}}, \quad &\text{k} = \text{n, p}\\ 0, \qquad &\text{k} = \text{s} \end{cases},\end{split}\]
\[\begin{split}i_{\text{e,k}} = \epsilon_{\text{k}}^{\text{b}} \kappa_{\text{e}}(c_{\text{e,k}}) \left( - \frac{\partial \phi_{\text{e,k}}}{\partial x} + 2(1-t^+)\frac{RT}{F}\frac{\partial}{\partial x}\left(\log(c_{\text{e,k}})\right)\right), \text{k} \in \text{n, s, p}, \\ I-i_{\text{e,k}} = - \sigma_{\text{k}} \frac{\partial \phi_{\text{e,k}}}{\partial x}, \text{k} \in \text{n, s, p}.\end{split}\]
Mass conservation:#
\[\begin{split}\epsilon_{\text{k}} \frac{\partial c_{\text{e,k}}}{\partial t} = -\frac{\partial N_{\text{e,k}}}{\partial x} + \frac{1}{F}\frac{\partial i_{\text{e,k}}}{\partial x}, \text{k} \in \text{n, s, p},\\ N_{\text{e,k}} = -\epsilon_{\text{k}}^{\text{b}} D_{\text{e}}(c_{\text{e,k}}) \frac{\partial c_{\text{e,k}}}{\partial x} + \frac{t^+}{F} i_{\text{e,k}}, \\ \text{k} \in \text{n, s, p}, \\ \frac{\partial c_{\text{s,k}}}{\partial t} = -\frac{1}{r_{\text{k}}^2} \frac{\partial}{\partial r_{\text{k}}} \left(r_{\text{k}}^2 N_{\text{s,k}}\right), \\ \text{k} \in \text{n, p},\\ N_{\text{s,k}} = -D_{\text{s,k}}(c_{\text{s,k}}) \frac{\partial c_{\text{s,k}}}{\partial r_{\text{k}}}, \\ \text{k} \in \text{n, p}.\end{split}\]
Electrochemical reactions:#
\[\begin{split}j_{\text{k}} = 2 j_{\text{0,k}} \sinh\left(\frac{ F\eta_{\text{k}}}{2RT} \right), \\ \text{k} \in \text{n, p}, \\ j_{\text{0,k}} = c_{\text{s,k}}^{1/2} (1-c_{\text{s,k}})^{1/2}c_{\text{e,k}}^{1/2}\big|_{r_{\text{k}}=1}, \\ \text{k} \in \text{n, p}, \\ \eta_{\text{k}} = \phi_{\text{s,k}} - \phi_{\text{e,k}} - U_{\text{k}}(c_{\text{s,k}}\big|_{r_{\text{k}}=1}), \\ \text{k} \in \text{n, p}.\end{split}\]

These are to be solved subject to the following boundary conditions:

Current:#
\[\begin{split}i_{\text{e,n}}\big|_{x=0} = 0, \quad i_{\text{e,p}}\big|_{x=L}=0, \\ \phi_{\text{e,n}}\big|_{x=L_{\text{n}}} = \phi_{\text{e,s}}\big|_{x=L_{\text{n}}}, \quad i_{\text{e,n}}\big|_{x=L_{\text{n}}} = i_{\text{e,s}}\big\vert_{x=L_{\text{n}}} = I, \\ \phi_{\text{e,s}}\big|_{x=L-L_{\text{p}}} = \phi_{\text{e,p}}\big|_{x=L-L_{\text{p}}}, \quad i_{\text{e,s}}\big|_{x=L-L_{\text{p}}} = i_{\text{e,p}}\big|_{x=L-L_{\text{p}}} = I.\end{split}\]
Concentration in the electrolyte:#
\[\begin{split}N_{\text{e,n}}\big|_{x=0} = 0, \quad N_{\text{e,p}}\big|_{x=L}=0,\\ c_{\text{e,n}}\big|_{x=L_{\text{n}}} = c_{\text{e,s}}|_{x=L_{\text{n}}}, \quad N_{\text{e,n}}\big|_{x=L_{\text{n}}}=N_{\text{e,s}}\big|_{x=L_{\text{n}}}, \\ c_{\text{e,s}}|_{x=L-L_{\text{p}}}=c_{\text{e,p}}|_{x=L-L_{\text{p}}}, \quad N_{\text{e,s}}\big|_{x=L-L_{\text{p}}}=N_{\text{e,p}}\big|_{x=L-L_{\text{p}}}.\end{split}\]
Concentration in the electrode active material:#
\[N_{\text{s,k}}\big|_{r_{\text{k}}=0} = 0, \quad \text{k} \in \text{n, p}, \quad \ \ N_{\text{s,k}}\big|_{r_{\text{k}}=R_{\text{k}}} = \frac{j_{\text{k}}}{F}, \quad \text{k} \in \text{n, p}.\]
Reference potential:#
\[\phi_{\text{s,cn}} = 0, \quad \boldsymbol{x} \in \partial \Omega_{\text{tab,n}}.\]
And the initial conditions:#
\[\begin{split}c_{\text{s,k}}(x,r,0) = c_{\text{s,k,0}}, \quad \text{k} \in \text{n, p},\\ c_{\text{e,k}}(x,0) = c_{\text{e,0}}, \quad \text{k} \in \text{n, s, p}.\end{split}\]

Example solving DFN using PyBaMM#

Below we show how to solve the DFN model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM. For a more detailed example, see the notebook on the SPM.

In order to show off all the different points at which the process of setting up and solving a model in PyBaMM can be customised we explicitly handle the stages of choosing a geometry, setting parameters, discretising the model and solving the model. However, it is often simpler in practice to use the Simulation class, which handles many of the stages automatically, as shown here.

First we need to import pybamm, along with numpy which we will use in this notebook.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
WARNING: You are using pip version 21.0.1; however, version 21.1.2 is available.
You should consider upgrading via the '/home/user/Documents/PyBaMM/env/bin/python3.8 -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

We then load the DFN model and default geometry, and process them both using the default parameters.

[2]:
# load model
model = pybamm.lithium_ion.DFN()

# create geometry
geometry = model.default_geometry

# load parameter values and process model and geometry
param = model.default_parameter_values
param.process_model(model)
param.process_geometry(geometry)

The next step is to set the mesh and discretise the model. Again, we choose the default settings.

[3]:
# set mesh
mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)

# discretise model
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model);

The model is now ready to be solved. We select the default DAE solver for the DFN. Note that in order to successfully solve the system of DAEs we are required to give consistent initial conditions. This is handled automatically by PyBaMM during the solve operation.

[4]:
# solve model
solver = model.default_solver
t_eval = np.linspace(0, 3600, 300)  # time in seconds
solution = solver.solve(model, t_eval)

To get a quick overview of the model outputs we can use the QuickPlot class, which plots a common set of useful outputs. The method Quickplot.dynamic_plot makes a slider widget.

[8]:
quick_plot = pybamm.QuickPlot(
    solution, ["Positive electrode interfacial current density [A.m-2]"]
)
quick_plot.dynamic_plot();

References#

The relevant papers for this notebook are:

[6]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Electrode State of Health#

This notebook demonstrates some utilities to work with electrode State of Health (also sometimes called electrode stoichiometry), using the algorithm from Mohtat et al [1]

[1] Mohtat, P., Lee, S., Siegel, J. B., & Stefanopoulou, A. G. (2019). Towards better estimability of electrode-specific state of health: Decoding the cell expansion. Journal of Power Sources, 427, 101-111.

[26]:
%pip install "pybamm[plot,cite]" -q # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
import numpy as np
zsh:1: no matches found: pybamm[plot,cite]
Note: you may need to restart the kernel to use updated packages.

Create and solve model#

[27]:
spm = pybamm.lithium_ion.SPM()
experiment = pybamm.Experiment(
    [
        "Charge at 1C until 4.2V",
        "Hold at 4.2V until C/50",
        "Discharge at 1C until 2.8V",
        "Hold at 2.8V until C/50",
    ]
)
parameter_values = pybamm.ParameterValues("Mohtat2020")

sim = pybamm.Simulation(spm, experiment=experiment, parameter_values=parameter_values)
spm_sol = sim.solve()
spm_sol.plot(
    [
        "Voltage [V]",
        "Current [A]",
        "Negative electrode stoichiometry",
        "Positive electrode stoichiometry",
    ]
)
[27]:
<pybamm.plotting.quick_plot.QuickPlot at 0x17f99c580>

Solve for electrode SOH variables#

Given a total amount of cyclable lithium capacity, \(Q_{Li}\), electrode capacities, \(Q_n\) and \(Q_p\), and voltage limits, \(V_{min}\) and \(V_{max}\), we can solve for the min and max electrode SOCs, \(x_0\), \(x_{100}\), \(y_0\), and \(y_{100}\), and the cell capacity, \(C\), using the algorithm adapted from Mohtat et al [1]. First, we find \(x_{100}\) and \(y_{100}\) using

\[\begin{split}Q_{Li} = y_{100}Q_p + x_{100}Q_n, \\ V_{max} = U_p(y_{100}) - U_n(x_{100}).\end{split}\]

Note that Mohtat et al use \(n_{Li} = \frac{3600 Q_{Li}}{F}\) instead. Then, we find \(Q\) using

\[V_{min} = U_p(y_{0}) - U_n(x_{0}) = U_p\left(y_{100} + \frac{Q}{Q_p}\right) - U_n\left(x_{100} - \frac{Q}{Q_n}\right)\]

Finally, \(x_0\) and \(y_0\) are simply defined as

\[\begin{split}x_0 = x_{100} - \frac{Q}{Q_n}, \\ y_0 = y_{100} + \frac{Q}{Q_p}.\end{split}\]

We implement this in PyBaMM as an algebraic model.

[28]:
param = pybamm.LithiumIonParameters()

Vmin = 2.8
Vmax = 4.2
Q_n = parameter_values.evaluate(param.n.Q_init)
Q_p = parameter_values.evaluate(param.p.Q_init)
Q_Li = parameter_values.evaluate(param.Q_Li_particles_init)

U_n = param.n.prim.U
U_p = param.p.prim.U
T_ref = param.T_ref
[29]:
# First we solve for x_100 and y_100

model = pybamm.BaseModel()

x_100 = pybamm.Variable("x_100")
y_100 = (Q_Li - x_100 * Q_n) / Q_p

y_100_min = 1e-10

x_100_upper_limit = (Q_Li - y_100_min * Q_p) / Q_n

model.algebraic = {x_100: U_p(y_100, T_ref) - U_n(x_100, T_ref) - Vmax}

model.initial_conditions = {x_100: x_100_upper_limit}

model.variables = {"x_100": x_100, "y_100": y_100}

sim = pybamm.Simulation(model, parameter_values=parameter_values)
sol = sim.solve([0])

x_100 = sol["x_100"].data[0]
y_100 = sol["y_100"].data[0]

for var in ["x_100", "y_100"]:
    print(var, ":", sol[var].data[0])

# Based on the calculated values for x_100 and y_100 we solve for x_0
model = pybamm.BaseModel()

x_0 = pybamm.Variable("x_0")
Q = Q_n * (x_100 - x_0)
y_0 = y_100 + Q / Q_p

model.algebraic = {x_0: U_p(y_0, T_ref) - U_n(x_0, T_ref) - Vmin}
model.initial_conditions = {x_0: 0.1}

model.variables = {
    "Q": Q,
    "x_0": x_0,
    "y_0": y_0,
}

sim = pybamm.Simulation(model, parameter_values=parameter_values)
sol = sim.solve([0])


for var in ["Q", "x_0", "y_0"]:
    print(var, ":", sol[var].data[0])
x_100 : 0.8333742766485323
y_100 : 0.03354554691532985
Q : 4.968932683689383
x_0 : 0.0015118453536460618
y_0 : 0.8908948803914054

This is implemented in PyBaMM as the ElectrodeSOHSolver class

[30]:
esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param)

inputs = {"V_min": Vmin, "V_max": Vmax, "Q_n": Q_n, "Q_p": Q_p, "Q_Li": Q_Li}

esoh_sol = esoh_solver.solve(inputs)

for var in ["x_100", "y_100", "Q", "x_0", "y_0"]:
    print(var, ":", esoh_sol[var])
x_100 : 0.833374276202919
y_100 : 0.0335455473745959
Q : 4.968932679279884
x_0 : 0.0015118456462390713
y_0 : 0.890894880089848

Check against simulations#

Plotting the SPM simulations against the eSOH calculations validates the min/max stoichiometry calculations

[31]:
t = spm_sol["Time [h]"].data
x_spm = spm_sol["Negative electrode stoichiometry"].data
y_spm = spm_sol["Positive electrode stoichiometry"].data

x_0 = esoh_sol["x_0"].data * np.ones_like(t)
y_0 = esoh_sol["y_0"].data * np.ones_like(t)
x_100 = esoh_sol["x_100"].data * np.ones_like(t)
y_100 = esoh_sol["y_100"].data * np.ones_like(t)

fig, axes = plt.subplots(1, 2)

axes[0].plot(t, x_spm, "b")
axes[0].plot(t, x_0, "k:")
axes[0].plot(t, x_100, "k:")
axes[0].set_ylabel("x")

axes[1].plot(t, y_spm, "r")
axes[1].plot(t, y_0, "k:")
axes[1].plot(t, y_100, "k:")
axes[1].set_ylabel("y")

for k in range(2):
    axes[k].set_xlim([t[0], t[-1]])
    axes[k].set_ylim([0, 1])
    axes[k].set_xlabel("Time [h]")

fig.tight_layout()
_images/source_examples_notebooks_models_electrode-state-of-health_13_0.png

How does electrode SOH depend on cyclable lithium#

We can do a parameter sweep for the amount of cyclable lithium to see how it affects the electrode SOH parameters and cell capacity

[32]:
all_parameter_sets = [
    k
    for k, v in pybamm.parameter_sets.items()
    if v["chemistry"] == "lithium_ion"
    and k
    not in [
        "Xu2019",
        "Chen2020_composite",
        "Ecker2015_graphite_halfcell",
        "OKane2022_graphite_SiOx_halfcell",
    ]
]


def solve_esoh_sweep_QLi(parameter_set, param):
    parameter_values = pybamm.ParameterValues(parameter_set)

    # Vmin = parameter_values["Lower voltage cut-off [V]"]
    # Vmax = parameter_values["Upper voltage cut-off [V]"]
    Vmin = parameter_values["Open-circuit voltage at 0% SOC [V]"]
    Vmax = parameter_values["Open-circuit voltage at 100% SOC [V]"]

    Q_n = parameter_values.evaluate(param.n.Q_init)
    Q_p = parameter_values.evaluate(param.p.Q_init)

    Q = parameter_values.evaluate(param.Q / param.n_electrodes_parallel)
    esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(
        parameter_values, param, known_value="cell capacity"
    )
    inputs = {"V_max": Vmax, "V_min": Vmin, "Q": Q, "Q_n": Q_n, "Q_p": Q_p}
    sol_init_Q = esoh_solver.solve(inputs)

    Q_Li_init = parameter_values.evaluate(param.Q_Li_particles_init)
    esoh_solver = pybamm.lithium_ion.ElectrodeSOHSolver(parameter_values, param)
    inputs = {"V_max": Vmax, "V_min": Vmin, "Q_Li": Q_Li_init, "Q_n": Q_n, "Q_p": Q_p}
    sol_init_QLi = esoh_solver.solve(inputs)

    Q_Li_sweep = np.linspace(1e-6, Q_n + Q_p)
    sweep = {}
    variables = ["Q_Li", "x_0", "x_100", "y_0", "y_100", "Q"]
    for var in variables:
        sweep[var] = []

    for Q_Li in Q_Li_sweep:
        inputs["Q_Li"] = Q_Li
        try:
            sol = esoh_solver.solve(inputs)
            for var in variables:
                sweep[var].append(sol[var])
        except (ValueError, pybamm.SolverError):
            pass

    return sweep, sol_init_QLi, sol_init_Q


for parameter_set in ["Chen2020"]:
    sweep, sol_init_QLi, sol_init_Q = solve_esoh_sweep_QLi(parameter_set, param)
[33]:
def plot_sweep(sweep, sol_init, sol_init_Q, parameter_set):
    fig, axes = plt.subplots(1, 3, figsize=(10, 3))
    parameter_values = pybamm.ParameterValues(parameter_set)
    parameter_values.evaluate(param.n.Q_init)
    parameter_values.evaluate(param.p.Q_init)
    # Plot min/max stoichimetric limits, including the value with the given Q_Li
    for i, ks in enumerate([["x_0", "x_100"], ["y_0", "y_100"], ["Q"]]):
        ax = axes.flat[i]
        for j, k in enumerate(ks):
            if i == 0 and j == 0:
                label1 = "Stoichiometric envelope"
                label2 = "Calculation from cyclable lithium"
                label3 = "Calculation from cell capacity"
            else:
                label1 = label2 = label3 = None
            ax.plot(sweep["Q_Li"], sweep[k], "b-", label=label1)
            ax.axhline(sol_init_QLi[k], c="k", linestyle="--", label=label2)
            ax.axhline(sol_init_Q[k], c="r", linestyle="--", label=label3)
        ax.set_xlabel("Cyclable lithium [A.h]")
        ax.set_ylabel(ks[0][0])
        ax.set_xlim([np.min(sweep["Q_Li"]), np.max(sweep["Q_Li"])])
        ax.axvline(sol_init_QLi["Q_Li"], c="k", linestyle="--")
        ax.axvline(sol_init_Q["Q_Li"], c="r", linestyle="--")
        # Plot capacities of electrodes
        # ax.axvline(Qn,c="b",linestyle="--")
        # ax.axvline(Qp,c="r",linestyle="--")
    axes[-1].set_ylabel("Cell capacity [A.h]")

    # Plot initial values of stoichometries
    parameter_values.evaluate(param.n.prim.sto_init_av)
    parameter_values.evaluate(param.p.prim.sto_init_av)
    # axes[0].axhline(sto_n_init,c="g",linestyle="--")
    # axes[1].axhline(sto_p_init,c="g",linestyle="--")

    axes[1].set_title(parameter_set)
    fig.legend(loc="center left", bbox_to_anchor=(1.01, 0.5))
    fig.tight_layout()
    return fig, axes


plot_sweep(sweep, sol_init_QLi, sol_init_Q, "Chen2020");
_images/source_examples_notebooks_models_electrode-state-of-health_17_0.png
[34]:
# Skip the MSMR example parameter set since we need to set up the ESOH solver differently
all_parameter_sets.remove("MSMR_Example")
# Loop over all parameter sets and solve the ESOH problem
for parameter_set in all_parameter_sets:
    print(parameter_set)
    try:
        sweep, sol_init_QLi, sol_init_Q = solve_esoh_sweep_QLi(parameter_set, param)
        fig, axes = plot_sweep(sweep, sol_init_QLi, sol_init_Q, parameter_set)
    except ValueError:
        pass
Ai2020
Chen2020
Ecker2015
Marquis2019
Mohtat2020
NCA_Kim2011
OKane2022
ORegan2022
Prada2013
Ramadass2004
_images/source_examples_notebooks_models_electrode-state-of-health_18_1.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_2.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_3.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_4.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_5.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_6.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_7.png
_images/source_examples_notebooks_models_electrode-state-of-health_18_8.png

References#

The relevant papers for this notebook are:

[35]:
pybamm.print_citations()
[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.
[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.
[3] Daniel R Baker and Mark W Verbrugge. Multi-species, multi-reaction model for porous intercalation electrodes: part i. model formulation and a perturbation solution for low-scan-rate, linear-sweep voltammetry of a spinel lithium manganese oxide electrode. Journal of The Electrochemical Society, 165(16):A3952, 2018.
[4] 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.
[5] Madeleine Ecker, Stefan Käbitz, Izaro Laresgoiti, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: II. Model Validation. Journal of The Electrochemical Society, 162(9):A1849–A1857, 2015. doi:10.1149/2.0541509jes.
[6] Madeleine Ecker, Thi Kim Dung Tran, Philipp Dechent, Stefan Käbitz, Alexander Warnecke, and Dirk Uwe Sauer. Parameterization of a Physico-Chemical Model of a Lithium-Ion Battery: I. Determination of Parameters. Journal of the Electrochemical Society, 162(9):A1836–A1848, 2015. doi:10.1149/2.0551509jes.
[7] Alastair Hales, Laura Bravo Diaz, Mohamed Waseem Marzook, Yan Zhao, Yatish Patel, and Gregory Offer. The cell cooling coefficient: a standard to define heat rejection from lithium-ion batteries. Journal of The Electrochemical Society, 166(12):A2383, 2019.
[8] 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.
[9] Gi-Heon Kim, Kandler Smith, Kyu-Jin Lee, Shriram Santhanagopalan, and Ahmad Pesaran. Multi-domain modeling of lithium-ion batteries encompassing multi-physics in varied length scales. Journal of the Electrochemical Society, 158(8):A955–A969, 2011. doi:10.1149/1.3597614.
[10] 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.
[11] 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.
[12] Peyman Mohtat, Suhak Lee, Valentin Sulzer, Jason B. Siegel, and Anna G. Stefanopoulou. Differential Expansion and Voltage Model for Li-ion Batteries at Practical Charging Rates. Journal of The Electrochemical Society, 167(11):110561, 2020. doi:10.1149/1945-7111/aba5d1.
[13] Simon E. J. O'Kane, Ian D. Campbell, Mohamed W. J. Marzook, Gregory J. Offer, and Monica Marinescu. Physical origin of the differential voltage minimum associated with lithium plating in li-ion batteries. Journal of The Electrochemical Society, 167(9):090540, may 2020. URL: https://doi.org/10.1149/1945-7111/ab90ac, doi:10.1149/1945-7111/ab90ac.
[14] Kieran O'Regan, Ferran Brosa Planella, W. Dhammika Widanage, and Emma Kendrick. Thermal-electrochemical parameters of a high energy lithium-ion cylindrical battery. Electrochimica Acta, 425:140700, 2022. doi:10.1016/j.electacta.2022.140700.
[15] Simon E. J. O'Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.
[16] Eric Prada, D. Di Domenico, Y. Creff, J. Bernard, Valérie Sauvant-Moynot, and François Huet. A simplified electrochemical and thermal aging model of LiFePO4-graphite Li-ion batteries: power and capacity fade simulations. Journal of The Electrochemical Society, 160(4):A616, 2013. doi:10.1149/2.053304jes.
[17] P Ramadass, Bala Haran, Parthasarathy M Gomadam, Ralph White, and Branko N Popov. Development of first principles capacity fade model for li-ion cells. Journal of the Electrochemical Society, 151(2):A196, 2004. doi:10.1149/1.1634273.
[18] Giles Richardson, Ivan Korotkin, Rahifa Ranom, Michael Castle, and Jamie M. Foster. Generalised single particle models for high-rate operation of graded lithium-ion electrodes: systematic derivation and validation. Electrochimica Acta, 339:135862, 2020. doi:10.1016/j.electacta.2020.135862.
[19] 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.
[20] Mark Verbrugge, Daniel Baker, Brian Koch, Xingcheng Xiao, and Wentian Gu. Thermodynamic model for substitutional materials: application to lithiated graphite, spinel manganese oxide, iron phosphate, and layered nickel-manganese-cobalt oxide. Journal of The Electrochemical Society, 164(11):E3243, 2017.
[21] 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.
[22] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.
[23] Yan Zhao, Yatish Patel, Teng Zhang, and Gregory J Offer. Modeling the effects of thermal gradients induced by tab and surface cooling on lithium ion cell performance. Journal of The Electrochemical Society, 165(13):A3169, 2018.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Half-cell models in PyBaMM#

PyBaMM supports both negative and positive half-cells. In both cases, the working electrode is considered to be the positive electrode and lithium metal the negative electrode. The difference is solely down to the material the positive electrode is made of. This notebook demonstrates how to simulate half-cells made of a range of materials.

[1]:
%pip install pybamm -q  # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

To simulate a half-cell, pass {"working electrode": "positive"} to the options dictionary. This deletes the negative electrode and replaces it with lithium metal, which has a fixed open-circuit voltage of zero. First, we load the NMC-based positive half-cell studied by Xu et al. [12]

[2]:
model = pybamm.lithium_ion.DFN({"working electrode": "positive"})
param_nmc = pybamm.ParameterValues("Xu2019")

Start by simulating a pseudo-OCV cycle:

[3]:
exp_slow = pybamm.Experiment(
    ["Discharge at C/25 until 3.5 V", "Charge at C/25 until 4.2 V"]
)
sim1 = pybamm.Simulation(model, parameter_values=param_nmc, experiment=exp_slow)
sol1 = sim1.solve()
t = sol1["Time [s]"].entries
V = sol1["Voltage [V]"].entries
plt.figure()
plt.plot(t, V)
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.show()
_images/source_examples_notebooks_models_half-cell_5_0.png

The charge and discharge curves are the same, as expected. This is not the case for faster cycles:

[4]:
exp_fast = pybamm.Experiment(
    ["Discharge at 1C until 3.5 V", "Charge at 1C until 4.2 V"]
)
sim2 = pybamm.Simulation(model, parameter_values=param_nmc, experiment=exp_fast)
sol2 = sim2.solve()
t = sol2["Time [s]"].entries
V = sol2["Voltage [V]"].entries
plt.figure()
plt.plot(t, V)
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.show()
At t = 285.669 and h = 7.17426e-14, the corrector convergence failed repeatedly or with |h| = hmin.
_images/source_examples_notebooks_models_half-cell_7_1.png

Next, load a negative half-cell with a graphite-silicon composite as the positive electrode. This is the negative half of the full cell studied by O’Kane et al. [9] and therefore supports all the degradation mechanisms included in that paper. Just like in the coupled degradation notebook, use the options dictionary to switch the mechanisms on and off. Unlike for a full cell, the SEI option applies to the lithium metal electrode as well as the positive electrode. To set different options for each, use a 2-tuple. The SEI on cracks and lithium plating options do not work on the lithium metal electrode.

[5]:
model_with_degradation = pybamm.lithium_ion.DFN(
    {
        "working electrode": "positive",
        "SEI": "reaction limited",  # SEI on both electrodes
        "SEI porosity change": "true",
        "particle mechanics": "swelling and cracking",
        "SEI on cracks": "true",
        "lithium plating": "partially reversible",
        "lithium plating porosity change": "true",  # alias for "SEI porosity change"
    }
)
param_GrSi = pybamm.ParameterValues("OKane2022_graphite_SiOx_halfcell")
param_GrSi.update({"SEI reaction exchange current density [A.m-2]": 1.5e-07})
var_pts = {"x_n": 1, "x_s": 5, "x_p": 7, "r_n": 1, "r_p": 30}
exp_degradation = pybamm.Experiment(
    ["Charge at 0.3C until 1.5 V", "Discharge at 0.3C until 0.005 V"]
)
sim3 = pybamm.Simulation(
    model_with_degradation,
    parameter_values=param_GrSi,
    experiment=exp_degradation,
    var_pts=var_pts,
)
sol3 = sim3.solve()
t = sol3["Time [s]"].entries
V = sol3["Voltage [V]"].entries
plt.figure()
plt.plot(t, V)
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.show()
_images/source_examples_notebooks_models_half-cell_9_0.png

In order to get SEI growth to work on both electrodes, we had to add domains to the names of the degradation variables. Bear this in mind when writing your code.

[6]:
Q_SEI_n = sol3["Loss of capacity to negative SEI [A.h]"].entries
Q_SEI_p = sol3["Loss of capacity to positive SEI [A.h]"].entries
Q_SEI_cr = sol3["Loss of capacity to positive SEI on cracks [A.h]"].entries
Q_pl = sol3["Loss of capacity to positive lithium plating [A.h]"].entries
plt.figure()
plt.plot(t, Q_SEI_n, label="Negative SEI")
plt.plot(t, Q_SEI_p, label="Positive SEI")
plt.plot(t, Q_SEI_cr, label="SEI on cracks")
plt.plot(t, Q_pl, label="Lithium plating")
plt.xlabel("Time [s]")
plt.ylabel("Loss of lithium inventory [A.h]")
plt.legend()
plt.show()
_images/source_examples_notebooks_models_half-cell_11_0.png

The SEI growth is slow compared to the reversible component of the lithium plating. What happens if the SEI growth rate is increased?

[7]:
param_GrSi.update({"SEI reaction exchange current density [A.m-2]": 6e-07})
sim4 = pybamm.Simulation(
    model_with_degradation,
    parameter_values=param_GrSi,
    experiment=exp_degradation,
    var_pts=var_pts,
)
sol4 = sim4.solve()
t = sol4["Time [s]"].entries
V = sol4["Voltage [V]"].entries
plt.figure()
plt.plot(t, V)
plt.xlabel("Time [s]")
plt.ylabel("Voltage [V]")
plt.show()
_images/source_examples_notebooks_models_half-cell_13_0.png
[8]:
Q_SEI_n = sol4["Loss of capacity to negative SEI [A.h]"].entries
Q_SEI_p = sol4["Loss of capacity to positive SEI [A.h]"].entries
Q_SEI_cr = sol4["Loss of capacity to positive SEI on cracks [A.h]"].entries
Q_pl = sol4["Loss of capacity to positive lithium plating [A.h]"].entries
plt.figure()
plt.plot(t, Q_SEI_n, label="Negative SEI")
plt.plot(t, Q_SEI_p, label="Positive SEI")
plt.plot(t, Q_SEI_cr, label="SEI on cracks")
plt.plot(t, Q_pl, label="Lithium plating")
plt.xlabel("Time [s]")
plt.ylabel("Loss of lithium inventory [A.h]")
plt.legend()
plt.show()
_images/source_examples_notebooks_models_half-cell_14_0.png

The additional SEI increases the cell resistance, preventing the graphite-silicon composite from being fully lithiated, so there is less plating than before.

[9]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.
[8] Simon E. J. O'Kane, Ian D. Campbell, Mohamed W. J. Marzook, Gregory J. Offer, and Monica Marinescu. Physical origin of the differential voltage minimum associated with lithium plating in li-ion batteries. Journal of The Electrochemical Society, 167(9):090540, may 2020. URL: https://doi.org/10.1149/1945-7111/ab90ac, doi:10.1149/1945-7111/ab90ac.
[9] Simon E. J. O'Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.
[10] 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.
[11] Lars Ole Valøen and Jan N Reimers. Transport properties of lipf6-based li-ion battery electrolytes. Journal of The Electrochemical Society, 152(5):A882, 2005.
[12] Shanshan Xu, Kuan-Hung Chen, Neil P Dasgupta, Jason B Siegel, and Anna G Stefanopoulou. Evolution of dead lithium growth in lithium metal batteries: experimentally validated model of the apparent capacity loss. Journal of The Electrochemical Society, 166(14):A3456, 2019.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Jelly roll model#

In this notebook we show how to set up and solve the “two-potential” model from “Homogenisation of spirally-wound high-contrast layered materials”, S. Psaltis, R. Timms, C.P. Please, S.J. Chapman, SIAM Journal on Applied Mathematics, 2020.

We consider a spirally-wound cell, such as the common 18650 lithium-ion cell. In practice these cells are constructed by rolling a sandwich of layers containing the active cathode, positive current collector, active cathode, separator, active anode, negative current collector, active anode, and separator. The “two-potential” model consists of an equation for the potential \(\phi^\pm\) in each current collector. The potential difference drives a current \(I\) through the electrode/separator/electrode sandwich (which we refer to as the “active material” in the original paper). Thus, in non-dimensional form, the model is

\[\frac{\delta^+\sigma^+}{2\pi^2}\frac{1}{r}\frac{\mathrm{d}}{\mathrm{d}r}\left(\frac{1}{r}\frac{\mathrm{d}\phi^+}{\mathrm{d}r}\right) + 2I(\phi^+-\phi^-) = 0,\]
\[\frac{\delta^-\sigma^-}{2\pi^2}\frac{1}{r}\frac{\mathrm{d}}{\mathrm{d}r}\left(\frac{1}{r}\frac{\mathrm{d}\phi^-}{\mathrm{d}r}\right) - 2I(\phi^+-\phi^-) = 0,\]

with boundary conditions

\[\frac{\mathrm{d}\phi^+}{\mathrm{d}r}(r=r_0) = 0, \quad \phi^+(r=1) = 1, \quad \phi^-(r=0) = 0, \quad \frac{\mathrm{d}\phi^-}{\mathrm{d}r}(r=1) = 0.\]

For a complete description of the model and parameters, please refer to the original paper.

It can be shown that the active material can be modelled using any 1D battery model we like to describe the electrochemical/thermal behaviour in the electrode/separator/electrode sandwich. Such functionality will be added to PyBaMM in a future release and will enable efficient simulations of jelly roll cells.

Two-potential resistor model#

In this section we consider a simplified model in which we ignore the details of the anode, cathode and separator, and treat them as a single region of active material, modelled as an Ohmic conductor, with two such regions per winding. In this case the model becomes

\[\frac{\delta^+\sigma^+}{2\pi^2}\frac{1}{r}\frac{\mathrm{d}}{\mathrm{d}r}\left(\frac{1}{r}\frac{\mathrm{d}\phi^+}{\mathrm{d}r}\right) + \frac{2\sigma^{a}(\phi^--\phi^+)}{l\epsilon^4} = 0,\]
\[\frac{\delta^-\sigma^-}{2\pi^2}\frac{1}{r}\frac{\mathrm{d}}{\mathrm{d}r}\left(\frac{1}{r}\frac{\mathrm{d}\phi^-}{\mathrm{d}r}\right) - \frac{2\sigma^{a}(\phi^--\phi^+)}{l\epsilon^4} = 0,\]

along with the same boundary conditions.

We begin by importing PyBaMM along with some other useful packages

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt

[notice] A new release of pip is available: 23.3.1 -> 23.3.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

First we will define the parameters in the model. Note the model is posed in non-dimensional form.

[2]:
N = pybamm.Parameter("Number of winds")
r0 = pybamm.Parameter("Inner radius")
eps = (1 - r0) / N  # ratio of sandwich thickness to cell radius
delta = pybamm.Parameter("Current collector thickness")
delta_p = delta  # assume same thickness
delta_n = delta  # assume same thickness
l = 1 / 2 - delta_p - delta_n  # active material thickness
sigma_p = pybamm.Parameter("Positive current collector conductivity")
sigma_n = pybamm.Parameter("Negative current collector conductivity")
sigma_a = pybamm.Parameter("Active material conductivity")

Next we define our geometry and model

[3]:
# geometry
r = pybamm.SpatialVariable("radius", domain="cell", coord_sys="cylindrical polar")
geometry = {"cell": {r: {"min": r0, "max": 1}}}

# model
model = pybamm.BaseModel()
phi_p = pybamm.Variable("Positive potential", domain="cell")
phi_n = pybamm.Variable("Negative potential", domain="cell")

A_p = (2 * sigma_a / eps**4 / l) / (delta_p * sigma_p / 2 / pi**2)
A_n = (2 * sigma_a / eps**4 / l) / (delta_n * sigma_n / 2 / pi**2)
model.algebraic = {
    phi_p: pybamm.div((1 / r**2) * pybamm.grad(phi_p)) + A_p * (phi_n - phi_p),
    phi_n: pybamm.div((1 / r**2) * pybamm.grad(phi_n)) - A_n * (phi_n - phi_p),
}

model.boundary_conditions = {
    phi_p: {
        "left": (0, "Neumann"),
        "right": (1, "Dirichlet"),
    },
    phi_n: {
        "left": (0, "Dirichlet"),
        "right": (0, "Neumann"),
    },
}

model.initial_conditions = {phi_p: 1, phi_n: 0}  # initial guess for solver

model.variables = {"Negative potential": phi_n, "Positive potential": phi_p}

Next we provide values for our parameters, and process our geometry and model, thus replacing the Parameter symbols with numerical values

[4]:
params = pybamm.ParameterValues(
    {
        "Number of winds": 20,
        "Inner radius": 0.25,
        "Current collector thickness": 0.05,
        "Positive current collector conductivity": 5e6,
        "Negative current collector conductivity": 5e6,
        "Active material conductivity": 1,
    }
)
params.process_geometry(geometry)
params.process_model(model)
[4]:
<pybamm.models.base_model.BaseModel at 0x280b0ed90>

We choose to discretise in space using the Finite Volume method on a uniform grid

[5]:
# mesh
submesh_types = {"cell": pybamm.Uniform1DSubMesh}
var_pts = {r: 100}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
# method
spatial_methods = {"cell": pybamm.FiniteVolume()}
# discretise
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model);

We can now solve the model

[6]:
# solver
solver = pybamm.CasadiAlgebraicSolver()
solution = solver.solve(model)

The model gives the homogenised potentials in the negative a positive current collectors. Interestingly, the solid potential has microscale structure, varying linearly in the active material. In order to see this we need to post-process the solution and plot the potential as a function of radial position, being careful to capture the spiral geometry.

[7]:
# extract numerical parameter values
# Note: this overrides the definition of the `pybamm.Parameter` objects
N = params.evaluate(N)
r0 = params.evaluate(r0)
eps = params.evaluate(eps)
delta = params.evaluate(delta)
[8]:
# post-process homogenised potential
phi_n = solution["Negative potential"]
phi_p = solution["Positive potential"]


def alpha(r):
    return 2 * (phi_n(r=r) - phi_p(r=r))


def phi_am1(r, theta):
    # careful here - phi always returns a column vector so we need to add a new axis to r to get the right shape
    return alpha(r) * (r[:, np.newaxis] / eps - r0 / eps - delta - theta / 2 / pi) / (
        1 - 4 * delta
    ) + phi_p(r=r)


def phi_am2(r, theta):
    # careful here - phi always returns a column vector so we need to add a new axis to r to get the right shape
    return alpha(r) * (
        r0 / eps + 1 - delta + theta / 2 / pi - r[:, np.newaxis] / eps
    ) / (1 - 4 * delta) + phi_p(r=r)
[9]:
# define spiral


def spiral_pos_inner(t):
    return r0 - eps * delta + eps * t / (2 * pi)


def spiral_pos_outer(t):
    return r0 + eps * delta + eps * t / (2 * pi)


def spiral_neg_inner(t):
    return r0 - eps * delta + eps / 2 + eps * t / (2 * pi)


def spiral_neg_outer(t):
    return r0 + eps * delta + eps / 2 + eps * t / (2 * pi)


def spiral_am1_inner(t):
    return r0 + eps * delta + eps * t / (2 * pi)


def spiral_am1_outer(t):
    return r0 - eps * delta + eps / 2 + eps * t / (2 * pi)


def spiral_am2_inner(t):
    return r0 + eps * delta + eps / 2 + eps * t / (2 * pi)


def spiral_am2_outer(t):
    return r0 - eps * delta + eps + eps * t / (2 * pi)
[10]:
# Setup fine mesh with nr points per layer
nr = 10
rr = np.linspace(r0, 1, nr)
tt = np.arange(0, (N + 1) * 2 * pi, 2 * pi)
# N+1 winds of pos c.c.
r_mesh_pos = np.zeros((len(tt), len(rr)))
for i in range(len(tt)):
    r_mesh_pos[i, :] = np.linspace(spiral_pos_inner(tt[i]), spiral_pos_outer(tt[i]), nr)
# N winds of neg, am1, am2
r_mesh_neg = np.zeros((len(tt) - 1, len(rr)))
r_mesh_am1 = np.zeros((len(tt) - 1, len(rr)))
r_mesh_am2 = np.zeros((len(tt) - 1, len(rr)))
for i in range(len(tt) - 1):
    r_mesh_am2[i, :] = np.linspace(spiral_am2_inner(tt[i]), spiral_am2_outer(tt[i]), nr)
    r_mesh_neg[i, :] = np.linspace(spiral_neg_inner(tt[i]), spiral_neg_outer(tt[i]), nr)
    r_mesh_am1[i, :] = np.linspace(spiral_am1_inner(tt[i]), spiral_am1_outer(tt[i]), nr)
# Combine and sort
r_total_mesh = np.vstack((r_mesh_pos, r_mesh_neg, r_mesh_am1, r_mesh_am2))
r_total_mesh = np.sort(r_total_mesh, axis=None)
[11]:
# plot homogenised potential
fig, ax = plt.subplots(1, 1, figsize=(8, 6))

ax.plot(r_total_mesh, phi_n(r=r_total_mesh), "b", label=r"$\phi^-$")
ax.plot(r_total_mesh, phi_p(r=r_total_mesh), "r", label=r"$\phi^+$")
for i in range(len(tt)):
    ax.plot(
        r_mesh_pos[i, :],
        phi_p(r=r_mesh_pos[i, :]),
        "k",
        label=r"$\phi$" if i == 0 else "",
    )
for i in range(len(tt) - 1):
    ax.plot(r_mesh_neg[i, :], phi_n(r=r_mesh_neg[i, :]), "k")
    ax.plot(r_mesh_am1[i, :], phi_am1(r_mesh_am1[i, :], tt[i]), "k")
    ax.plot(r_mesh_am2[i, :], phi_am2(r_mesh_am2[i, :], tt[i]), "k")
ax.set_xlabel(r"$r$")
ax.set_ylabel(r"$\phi$")
ax.legend();
_images/source_examples_notebooks_models_jelly-roll-model_18_0.png

References#

The relevant papers for this notebook are:

[12]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Using latexify in PyBaMM#

In this notebook we show how to use latexify to print all the model equations.

First we import pybamm

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Then we load a model

[2]:
model = pybamm.lithium_ion.SPM()

Now that we have defined a model we can use latexify to get the latex of all the model equations

[3]:
model.latexify()
[3]:
$\displaystyle \large{\underline{\textbf{Single Particle Model Equations}}}\\\\\\ \textbf{Discharge capacity [A.h]}\\\\\frac{d}{d t} Q_{Ah} = \frac{I}{3600}\\\\Q_{Ah} = 0.0\quad \text{at}\; t=0\\\\\\ \textbf{X-averaged negative particle concentration [mol.m-3]}\\\\\frac{\partial}{\partial t} \overline{c}_{\mathrm{s,n}} = \nabla\cdot \left(D_{\mathrm{n}} \left(\nabla \overline{c}_{\mathrm{s,n}}\right)\right)\quad 0 < r < R_{\mathrm{n}}^{\mathrm{typ}}\\\\\overline{c}_{\mathrm{s,n}} = \int c_{\mathrm{n}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0\\\\\nabla \overline{c}_{\mathrm{s,n}} = 0.0\quad \text{at } r = 0\\\\\nabla \overline{c}_{\mathrm{s,n}} = - \frac{i_{\mathrm{cell}}}{D_{\mathrm{n}}^{surf} F L_{\mathrm{n}} \overline{a}_{\mathrm{n}}}\quad \text{at } r = R_{\mathrm{n}}^{\mathrm{typ}}\\\\\\ \textbf{X-averaged positive particle concentration [mol.m-3]}\\\\\frac{\partial}{\partial t} \overline{c}_{\mathrm{s,p}} = \nabla\cdot \left(D_{\mathrm{p}} \left(\nabla \overline{c}_{\mathrm{s,p}}\right)\right)\quad 0 < r < R_{\mathrm{p}}^{\mathrm{typ}}\\\\\overline{c}_{\mathrm{s,p}} = \int c_{\mathrm{p}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0\\\\\nabla \overline{c}_{\mathrm{s,p}} = 0.0\quad \text{at } r = 0\\\\\nabla \overline{c}_{\mathrm{s,p}} = - \frac{j_{\mathrm{p}}}{D_{\mathrm{p}}^{surf} F}\quad \text{at } r = R_{\mathrm{p}}^{\mathrm{typ}}\\\\\\ \textbf{Voltage [V]}\\\\V = - U_\mathrm{n}(c^\mathrm{surf}_\mathrm{s,n}, T) + U_\mathrm{p}(c^\mathrm{surf}_\mathrm{s,p}, T) + \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 j_{\mathrm{p}}}{j_{\mathrm{p}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{p}}} - \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 i_{\mathrm{cell}}}{L_{\mathrm{n}} \overline{a}_{\mathrm{n}} j_{\mathrm{n}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{n}}}\\\\\\ \textbf{Parameters and Variables}\\\\I = \text{Current function [A]}\\\\\overline{c}_{\mathrm{s,n}} = \text{X-averaged negative particle concentration [mol.m-3]}\\\\D_{\mathrm{n}} = \text{Negative electrode diffusivity [m2.s-1]}\\\\T_{\mathrm{amb}} = \text{Ambient temperature [K]}\\\\c_{\mathrm{n}}^{\mathrm{max}} = \text{Maximum concentration in negative electrode [mol.m-3]}\\\\\overline{c}_{\mathrm{s,p}} = \text{X-averaged positive particle concentration [mol.m-3]}\\\\D_{\mathrm{p}} = \text{Positive electrode diffusivity [m2.s-1]}\\\\c_{\mathrm{p}}^{\mathrm{max}} = \text{Maximum concentration in positive electrode [mol.m-3]}$

We can also get a list of all the equations instead of a single line joined by newline

[4]:
model.latexify(newline=False)
[4]:
$\displaystyle \left[ \large{\underline{\textbf{Single Particle Model Equations}}}, \ \\ \textbf{Discharge capacity [A.h]}, \ \frac{d}{d t} Q_{Ah} = \frac{I}{3600}, \ Q_{Ah} = 0.0\quad \text{at}\; t=0, \ \\ \textbf{X-averaged negative particle concentration [mol.m-3]}, \ \frac{\partial}{\partial t} \overline{c}_{\mathrm{s,n}} = \nabla\cdot \left(D_{\mathrm{n}} \left(\nabla \overline{c}_{\mathrm{s,n}}\right)\right)\quad 0 < r < R_{\mathrm{n}}^{\mathrm{typ}}, \ \overline{c}_{\mathrm{s,n}} = \int c_{\mathrm{n}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0, \ \nabla \overline{c}_{\mathrm{s,n}} = 0.0\quad \text{at } r = 0, \ \nabla \overline{c}_{\mathrm{s,n}} = - \frac{i_{\mathrm{cell}}}{D_{\mathrm{n}}^{surf} F L_{\mathrm{n}} \overline{a}_{\mathrm{n}}}\quad \text{at } r = R_{\mathrm{n}}^{\mathrm{typ}}, \ \\ \textbf{X-averaged positive particle concentration [mol.m-3]}, \ \frac{\partial}{\partial t} \overline{c}_{\mathrm{s,p}} = \nabla\cdot \left(D_{\mathrm{p}} \left(\nabla \overline{c}_{\mathrm{s,p}}\right)\right)\quad 0 < r < R_{\mathrm{p}}^{\mathrm{typ}}, \ \overline{c}_{\mathrm{s,p}} = \int c_{\mathrm{p}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0, \ \nabla \overline{c}_{\mathrm{s,p}} = 0.0\quad \text{at } r = 0, \ \nabla \overline{c}_{\mathrm{s,p}} = - \frac{j_{\mathrm{p}}}{D_{\mathrm{p}}^{surf} F}\quad \text{at } r = R_{\mathrm{p}}^{\mathrm{typ}}, \ \\ \textbf{Voltage [V]}, \ V = - U_\mathrm{n}(c^\mathrm{surf}_\mathrm{s,n}, T) + U_\mathrm{p}(c^\mathrm{surf}_\mathrm{s,p}, T) + \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 j_{\mathrm{p}}}{j_{\mathrm{p}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{p}}} - \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 i_{\mathrm{cell}}}{L_{\mathrm{n}} \overline{a}_{\mathrm{n}} j_{\mathrm{n}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{n}}}, \ \\ \textbf{Parameters and Variables}, \ I = \text{Current function [A]}, \ \overline{c}_{\mathrm{s,n}} = \text{X-averaged negative particle concentration [mol.m-3]}, \ D_{\mathrm{n}} = \text{Negative electrode diffusivity [m2.s-1]}, \ T_{\mathrm{amb}} = \text{Ambient temperature [K]}, \ c_{\mathrm{n}}^{\mathrm{max}} = \text{Maximum concentration in negative electrode [mol.m-3]}, \ \overline{c}_{\mathrm{s,p}} = \text{X-averaged positive particle concentration [mol.m-3]}, \ D_{\mathrm{p}} = \text{Positive electrode diffusivity [m2.s-1]}, \ c_{\mathrm{p}}^{\mathrm{max}} = \text{Maximum concentration in positive electrode [mol.m-3]}\right]$

latexify can also be used to get the equations in a file format like png, jpg, pdf or tex.

[5]:
model.latexify("spm_equations_.png")

will create a png file titled spm_equations.png in the working directory.

[6]:
model_spme = pybamm.lithium_ion.SPMe()

Jupyter notebook sometimes cannot render latex output that is too large. In that case we use newline=False and loop over the lines. When newline=False it returns a list, so it inherits all the properties of a list and you can also do something like this

[7]:
spme_latex = model_spme.latexify(newline=False)
for line in spme_latex:
    display(line)
$\displaystyle \large{\underline{\textbf{Single Particle Model with electrolyte Equations}}}$
$\displaystyle \\ \textbf{Discharge capacity [A.h]}$
$\displaystyle \frac{d}{d t} Q_{Ah} = \frac{I}{3600}$
$\displaystyle Q_{Ah} = 0.0\quad \text{at}\; t=0$
$\displaystyle \\ \textbf{X-averaged negative particle concentration [mol.m-3]}$
$\displaystyle \frac{\partial}{\partial t} \overline{c}_{\mathrm{s,n}} = \nabla\cdot \left(D_{\mathrm{n}} \left(\nabla \overline{c}_{\mathrm{s,n}}\right)\right)\quad 0 < r < R_{\mathrm{n}}^{\mathrm{typ}}$
$\displaystyle \overline{c}_{\mathrm{s,n}} = \int c_{\mathrm{n}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0$
$\displaystyle \nabla \overline{c}_{\mathrm{s,n}} = 0.0\quad \text{at } r = 0$
$\displaystyle \nabla \overline{c}_{\mathrm{s,n}} = - \frac{i_{\mathrm{cell}}}{D_{\mathrm{n}}^{surf} F L_{\mathrm{n}} \overline{a}_{\mathrm{n}}}\quad \text{at } r = R_{\mathrm{n}}^{\mathrm{typ}}$
$\displaystyle \\ \textbf{X-averaged positive particle concentration [mol.m-3]}$
$\displaystyle \frac{\partial}{\partial t} \overline{c}_{\mathrm{s,p}} = \nabla\cdot \left(D_{\mathrm{p}} \left(\nabla \overline{c}_{\mathrm{s,p}}\right)\right)\quad 0 < r < R_{\mathrm{p}}^{\mathrm{typ}}$
$\displaystyle \overline{c}_{\mathrm{s,p}} = \int c_{\mathrm{p}}^{\mathrm{init}}\, dxn\quad \text{at}\; t=0$
$\displaystyle \nabla \overline{c}_{\mathrm{s,p}} = 0.0\quad \text{at } r = 0$
$\displaystyle \nabla \overline{c}_{\mathrm{s,p}} = \frac{i_{\mathrm{cell}}}{D_{\mathrm{p}}^{surf} F L_{\mathrm{p}} \overline{a}_{\mathrm{p}}}\quad \text{at } r = R_{\mathrm{p}}^{\mathrm{typ}}$
$\displaystyle \\ \textbf{Porosity times concentration [mol.m-3](Negative electrode porosity times concentration [mol.m-3], Separator porosity times concentration [mol.m-3], Positive electrode porosity times concentration [mol.m-3])}$
$\displaystyle \frac{\partial}{\partial t} (\epsilon c)_{\mathrm{e}} = - \nabla\cdot \left(- D_{\mathrm{e}} \epsilon^{b_e} \left(\nabla c_{\mathrm{e}}\right)\right) + \frac{- \begin{cases}a_{\mathrm{n}} j_{\mathrm{n}}\\0.0\\a_{\mathrm{p}} j_{\mathrm{p}}\end{cases} t_{\mathrm{+}} + aj}{F}\quad 0 < x < L_x$
$\displaystyle (\epsilon c)_{\mathrm{e}} = \epsilon^{\mathrm{init}} c_{\mathrm{e}}^{\mathrm{init}}\quad \text{at}\; t=0$
$\displaystyle \\ \textbf{Voltage [V]}$
$\displaystyle V = - L_{\mathrm{n}} i_{\mathrm{cell}} \left(- \frac{1.0}{\kappa_{\mathrm{e}} \int \left(\epsilon_{\mathrm{s}}^{\mathrm{init}}\right)^{b_{\mathrm{e,s}}}\, dxn} + \frac{0.333333333333333}{\kappa_{\mathrm{e}} \int \left(\epsilon_{\mathrm{n}}^{\mathrm{init}}\right)^{b_{\mathrm{e,n}}}\, dxn}\right) - U_\mathrm{n}(c^\mathrm{surf}_\mathrm{s,n}, T) + U_\mathrm{p}(c^\mathrm{surf}_\mathrm{s,p}, T) - \int \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 i_{\mathrm{cell}}}{L_{\mathrm{n}} \overline{a}_{\mathrm{n}} j_{\mathrm{n}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{n}}}\, dxn + \int \frac{2.0 R T_{\mathrm{amb}} \operatorname{asinh}{\left(\frac{0.5 j_{\mathrm{p}}}{j_{\mathrm{p}}^{\mathrm{0}}} \right)}}{F ne_{\mathrm{p}}}\, dxn - \int \frac{0.5 i_{\mathrm{cell}} \left(- L_{x}^{2.0} + L_{\mathrm{p}}^{2.0} + x_{p} \left(2.0 L_{x} - x_{p}\right)\right)}{L_{\mathrm{p}} \kappa_{\mathrm{e}} \int \left(\epsilon_{\mathrm{p}}^{\mathrm{init}}\right)^{b_{\mathrm{e,p}}}\, dxn}\, dxn + \int \frac{R T_{\mathrm{amb}} \left(1+\frac{dlnf}{dlnc}\right) \left(2.0 - 2.0 t_{\mathrm{+}}\right) \log{\left(\max\left(1.0 \cdot 10^{-15}, \frac{(\epsilon c)_{\mathrm{e,p}}}{\epsilon_{\mathrm{p}}^{\mathrm{init}} \int c_{\mathrm{e}}\, dxn}\right) \right)}}{F}\, dxn + \int \frac{0.5 i_{\mathrm{cell}} x_{n} \left(- 2.0 L_{\mathrm{n}} + x_{n}\right)}{L_{\mathrm{n}} \sigma_{\mathrm{n}} \int \epsilon_{\mathrm{s,n}}^{b_{\mathrm{s,n}}}\, dxn}\, dxn + \frac{i_{\mathrm{cell}} \left(L_{x} - 0.333333333333333 L_{\mathrm{p}}\right)}{\sigma_{\mathrm{p}} \int \epsilon_{\mathrm{s,p}}^{b_{\mathrm{s,p}}}\, dxn} - \frac{i_{\mathrm{cell}} \left(x_{p} + \frac{0.5 \left(- L_{x} + x_{p}\right)^{2.0}}{L_{\mathrm{p}}}\right)}{\sigma_{\mathrm{p}} \int \epsilon_{\mathrm{s,p}}^{b_{\mathrm{s,p}}}\, dxn} - \frac{i_{\mathrm{cell}} \left(L_{\mathrm{n}} + L_{\mathrm{s}}\right)}{\kappa_{\mathrm{e}} \int \left(\epsilon_{\mathrm{s}}^{\mathrm{init}}\right)^{b_{\mathrm{e,s}}}\, dxn} - \frac{R T_{\mathrm{amb}} \left(1+\frac{dlnf}{dlnc}\right) \left(2.0 - 2.0 t_{\mathrm{+}}\right) \int \log{\left(\max\left(1.0 \cdot 10^{-15}, \frac{(\epsilon c)_{\mathrm{e,n}}}{\epsilon_{\mathrm{n}}^{\mathrm{init}} \int c_{\mathrm{e}}\, dxn}\right) \right)}\, dxn}{F}^{\mathtt{\text{right}}}$
$\displaystyle \\ \textbf{Parameters and Variables}$
$\displaystyle I = \text{Current function [A]}$
$\displaystyle \overline{c}_{\mathrm{s,n}} = \text{X-averaged negative particle concentration [mol.m-3]}$
$\displaystyle D_{\mathrm{n}} = \text{Negative electrode diffusivity [m2.s-1]}$
$\displaystyle T_{\mathrm{amb}} = \text{Ambient temperature [K]}$
$\displaystyle c_{\mathrm{n}}^{\mathrm{max}} = \text{Maximum concentration in negative electrode [mol.m-3]}$
$\displaystyle \overline{c}_{\mathrm{s,p}} = \text{X-averaged positive particle concentration [mol.m-3]}$
$\displaystyle D_{\mathrm{p}} = \text{Positive electrode diffusivity [m2.s-1]}$
$\displaystyle c_{\mathrm{p}}^{\mathrm{max}} = \text{Maximum concentration in positive electrode [mol.m-3]}$
$\displaystyle \epsilon_{\mathrm{p}}^{\mathrm{init}} = \text{Positive electrode porosity}$
$\displaystyle \epsilon_{\mathrm{s}}^{\mathrm{init}} = \text{Separator porosity}$
$\displaystyle \epsilon_{\mathrm{n}}^{\mathrm{init}} = \text{Negative electrode porosity}$
$\displaystyle (\epsilon c)_{\mathrm{e,p}} = \text{Positive electrode porosity times concentration [mol.m-3]}$
$\displaystyle (\epsilon c)_{\mathrm{e,s}} = \text{Separator porosity times concentration [mol.m-3]}$
$\displaystyle (\epsilon c)_{\mathrm{e,n}} = \text{Negative electrode porosity times concentration [mol.m-3]}$
$\displaystyle D_{\mathrm{e}} = \text{Electrolyte diffusivity [m2.s-1]}$
$\displaystyle b_{\mathrm{e,p}} = \text{Positive electrode Bruggeman coefficient (electrolyte)}$
$\displaystyle b_{\mathrm{e,s}} = \text{Separator Bruggeman coefficient (electrolyte)}$
$\displaystyle b_{\mathrm{e,n}} = \text{Negative electrode Bruggeman coefficient (electrolyte)}$
$\displaystyle F = \text{Faraday constant [C.mol-1]}$
$\displaystyle R_{\mathrm{p}} = \text{Positive particle radius [m]}$
$\displaystyle \epsilon_{\mathrm{s,p}} = \text{Positive electrode active material volume fraction}$
$\displaystyle L_{\mathrm{p}} = \text{Positive electrode thickness [m]}$
$\displaystyle L_{z} = \text{Electrode height [m]}$
$\displaystyle L_{y} = \text{Electrode width [m]}$
$\displaystyle n_{electrodes parallel} = \text{Number of electrodes connected in parallel to make a cell}$
$\displaystyle R_{\mathrm{n}} = \text{Negative particle radius [m]}$
$\displaystyle \epsilon_{\mathrm{s,n}} = \text{Negative electrode active material volume fraction}$
$\displaystyle L_{\mathrm{n}} = \text{Negative electrode thickness [m]}$
$\displaystyle t_{\mathrm{+}} = \text{Cation transference number}$
$\displaystyle c_{\mathrm{e}} = \frac{(\epsilon c)_{\mathrm{e}}}{\begin{cases}\epsilon_{\mathrm{n}}^{\mathrm{init}}\\\epsilon_{\mathrm{s}}^{\mathrm{init}}\\\epsilon_{\mathrm{p}}^{\mathrm{init}}\end{cases}}$
$\displaystyle (\epsilon c)_{\mathrm{e}} = \begin{cases}(\epsilon c)_{\mathrm{e,n}}\\(\epsilon c)_{\mathrm{e,s}}\\(\epsilon c)_{\mathrm{e,p}}\end{cases}$
$\displaystyle \epsilon^{b_e} = \begin{cases}\left(\epsilon_{\mathrm{n}}^{\mathrm{init}}\right)^{b_{\mathrm{e,n}}}\\\left(\epsilon_{\mathrm{s}}^{\mathrm{init}}\right)^{b_{\mathrm{e,s}}}\\\left(\epsilon_{\mathrm{p}}^{\mathrm{init}}\right)^{b_{\mathrm{e,p}}}\end{cases}$
$\displaystyle j_{\mathrm{p}} = - \frac{i_{\mathrm{cell}}}{L_{\mathrm{p}} \overline{a}_{\mathrm{p}}}$
$\displaystyle \overline{a}_{\mathrm{p}} = \int a_{\mathrm{p}}\, dxn$
$\displaystyle a_{\mathrm{p}} = \frac{3.0 \epsilon_{\mathrm{s,p}}}{R_{\mathrm{p}}}$
$\displaystyle i_{\mathrm{cell}} = \frac{I}{A_{\mathrm{cc}} n_{electrodes parallel}}$
$\displaystyle A_{\mathrm{cc}} = L_{y} L_{z}$
$\displaystyle j_{\mathrm{n}} = \frac{i_{\mathrm{cell}}}{L_{\mathrm{n}} \overline{a}_{\mathrm{n}}}$
$\displaystyle \overline{a}_{\mathrm{n}} = \int a_{\mathrm{n}}\, dxn$
$\displaystyle a_{\mathrm{n}} = \frac{3.0 \epsilon_{\mathrm{s,n}}}{R_{\mathrm{n}}}$
$\displaystyle aj = \begin{cases}a_{\mathrm{n}} j_{\mathrm{n}}\\0.0\\a_{\mathrm{p}} j_{\mathrm{p}}\end{cases}$

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[1] 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.
[2] 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.
[3] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Lead-Acid Models#

We compare a standard porous-electrode model for lead-acid batteries with two asymptotic reductions. For a more in-depth introduction to PyBaMM models, see the SPM notebook. Further details on the models can be found in [4].

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

“Full” model#

Electrolyte Concentration#
\[\begin{split}\frac{\partial }{\partial t}\left(\epsilon c\right) = -\frac{\partial N}{\partial x} + sj, \\ N = -\frac{\epsilon^b D(c)}{\mathcal{C}_\text{e}} \frac{\partial c}{\partial x}\\ N\big|_{x=0}= N\big|_{x=1}=0, \\ c\big|_{t=0} = 1\end{split}\]
Porosity#
\[\begin{split}\frac{\partial \epsilon}{\partial t} = -\beta^\text{surf}j, \\ \epsilon\big|_{t=0} = \epsilon^0\end{split}\]
Electrolyte Current#
\[\begin{split}\frac{\partial i_{\text{e}}}{\partial x} = j, \\ \mathcal{C}_\text{e} i_{\text{e}} = \epsilon_k^b \kappa(c) \left( \chi \frac{\partial}{\partial x}\log(c) - \frac{\partial\phi_{\text{e}}}{\partial x}\right)\\ i_{\text{e}}\big|_{x=0}= i_{\text{e}}\big|_{x=1}=0,\end{split}\]
Electrode Current#
\[\begin{split}\frac{\partial i_{\text{s}}}{\partial x} = -j,\\ i_{\text{s}} = -\sigma\frac{\partial\phi_{\text{s}}}{\partial x},\\ \phi_{\text{s}}\big|_{x=0} = i_{\text{s}}\big|_{x=l_\text{n}} = i_{\text{s}}\big|_{x=1-l_\text{p}} = 0, \\ i_{\text{s}}\big|_{x=1}=\mathcal{I},\end{split}\]
Interfacial current density#
\[\begin{split}j = \begin{cases} 2j_0(c) \sinh\left(\eta\right), \quad &0 < x < l_\text{n} \\ 0, \quad &l_\text{n} < x < 1-l_\text{p} \\ 2j_0(c) \sinh\left(\eta\right), \quad &1-l_\text{p} < x < 1 \end{cases} \\ \eta = \phi_{\text{s}} - \phi_\text{e} - U(c),\end{split}\]

This model is implemented in PyBaMM as the Full model

[2]:
full = pybamm.lead_acid.Full()

“Leading-order” model#

\[\begin{split}\frac{\mathrm{d} }{\mathrm{d} t}\left(\epsilon c\right) = (s_\text{n} - s_\text{p})\mathrm{I}, \\ \frac{\mathrm{d} \epsilon}{\mathrm{d} t} = -\beta^\text{surf}j, \\ j = \begin{cases} \mathrm{I}/l_\text{n}, \quad &0 < x < l_\text{n} \\ 0, \quad &l_\text{n} < x < 1-l_\text{p} \\ -\mathrm{I}/l_\text{p}, \quad &1-l_\text{p} < x < 1 \end{cases} \\ \phi_\text{e} = -U_\text{n}(c) + \sinh^{-1}\left(\frac{\mathrm{I}}{2l_\text{n}j_{0\text{n}}(c)}\right) \\ V = -\phi_\text{e} + U_\text{p}(c) - \sinh^{-1}\left(\frac{\mathrm{I}}{2l_\text{p}j_{0\text{p}}(c)}\right) \\\end{split}\]

This model is implemented in PyBaMM as LOQS (leading-order quasi-static)

[3]:
loqs = pybamm.lead_acid.LOQS()

Solving the models#

We load process parameters for each model, using the same set of (default) parameters for all. In anticipation of changing the current later, we make current an input parameter

[4]:
# load models
models = [loqs, full]

# process parameters
param = models[0].default_parameter_values
param["Current function [A]"] = "[input]"
for model in models:
    param.process_model(model)

Then, we discretise the models, using the default settings

[5]:
for model in models:
    # load and process default geometry
    geometry = model.default_geometry
    param.process_geometry(geometry)

    # discretise using default settings
    mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)
    disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
    disc.process_model(model)

Finally, we solve each model using CasADi’s solver and a current of 1A

[6]:
timer = pybamm.Timer()
solutions = {}
t_eval = np.linspace(0, 3600 * 17, 100)  # time in seconds
for model in models:
    solver = pybamm.CasadiSolver()
    timer.reset()
    solution = solver.solve(model, t_eval, inputs={"Current function [A]": 1})
    print(f"Solved the {model.name} in {timer.time()}")
    solutions[model] = solution
Solved the LOQS model in 64.611 ms
Solved the Full model in 736.504 ms

Results#

To plot the results, the variables are extracted from the solutions dictionary. For example, we can compare the voltages:

[7]:
for model in models:
    time = solutions[model]["Time [h]"].entries
    voltage = solutions[model]["Voltage [V]"].entries
    plt.plot(time, voltage, lw=2, label=model.name)
plt.xlabel("Time [h]", fontsize=15)
plt.ylabel("Voltage [V]", fontsize=15)
plt.legend(fontsize=15)
plt.show()
_images/source_examples_notebooks_models_lead-acid_16_0.png

Alternatively, using QuickPlot, we can compare the values of some variables

[8]:
solution_values = [solutions[model] for model in models]
quick_plot = pybamm.QuickPlot(solution_values)
quick_plot.dynamic_plot();

If we update the current, setting it to be 20 A, we observe a greater discrepancy between the full model and the reduced-order models.

[9]:
t_eval = np.linspace(0, 3600, 100)
for model in models:
    solver = pybamm.CasadiSolver()
    solutions[model] = solver.solve(model, t_eval, inputs={"Current function [A]": 20})

# Plot
solution_values = [solutions[model] for model in models]
quick_plot = pybamm.QuickPlot(solution_values)
quick_plot.dynamic_plot();
At t = 0.0087774 and h = 1.94278e-62, the corrector convergence failed repeatedly or with |h| = hmin.
At t = 0.00384476 and h = 1.59607e-100, the corrector convergence failed repeatedly or with |h| = hmin.

References#

The relevant papers for this notebook are:

[10]:
pybamm.print_citations()
[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.
[2] 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.
[3] Valentin Sulzer, S. Jon Chapman, Colin P. Please, David A. Howey, and Charles W. Monroe. Faster Lead-Acid Battery Simulations from Porous-Electrode Theory: Part I. Physical Model. Journal of The Electrochemical Society, 166(12):A2363–A2371, 2019. doi:10.1149/2.0301910jes.
[4] Valentin Sulzer, S. Jon Chapman, Colin P. Please, David A. Howey, and Charles W. Monroe. Faster Lead-Acid Battery Simulations from Porous-Electrode Theory: Part II. Asymptotic Analysis. Journal of The Electrochemical Society, 166(12):A2372–A2382, 2019. doi:10.1149/2.0441908jes.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Modelling lithium plating in PyBaMM#

This notebook shows how PyBaMM [8] can be used to model both reversible and irreversible lithium plating.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os

os.chdir(pybamm.__path__[0] + "/..")

The Doyle-Fuller-Newman model [3] is upgraded with three different lithium plating models. Model 1 contains the reversible lithium plating model of O’Kane et al. [5]. Model 2 contains the same model but with the lithium stripping capability removed, making the plating irreversible. Model 3 contains the updated partially reversible plating of O’Kane et al. [6]. The parameters are taken from Chen et al.’s investigation [2] of an LG M50 cell.

[2]:
# choose models
plating_options = ["reversible", "irreversible", "partially reversible"]
models = {
    option: pybamm.lithium_ion.DFN(options={"lithium plating": option}, name=option)
    for option in plating_options
}

# pick parameters
parameter_values = pybamm.ParameterValues("OKane2022")
parameter_values.update({"Ambient temperature [K]": 268.15})
parameter_values.update({"Upper voltage cut-off [V]": 4.21})
# parameter_values.update({"Lithium plating kinetic rate constant [m.s-1]": 1E-9})
parameter_values.update({"Lithium plating transfer coefficient": 0.5})
parameter_values.update({"Dead lithium decay constant [s-1]": 1e-4})

A series of simple fast charging experiments based on those of Ren et al. [7] is defined here. We first initialise the model at 0% SoC by performing a C/20 discharge (see more details on how to initialise a model from a simulation in this notebook).

[3]:
# specify experiments
pybamm.citations.register("Ren2018")

s = pybamm.step.string
experiment_discharge = pybamm.Experiment(
    [
        (
            s("Discharge at C/20 until 2.5 V", period="10 minutes"),
            s("Rest for 1 hour", period="3 minutes"),
        ),
    ]
)

sims_discharge = []
for model in models.values():
    sim_discharge = pybamm.Simulation(
        model, parameter_values=parameter_values, experiment=experiment_discharge
    )
    sol_discharge = sim_discharge.solve(calc_esoh=False)
    model.set_initial_conditions_from(sol_discharge, inplace=True)
    sims_discharge.append(sim_discharge)

And we can now define the different experiments to charge at different C-rates.

[4]:
C_rates = ["2C", "1C", "C/2", "C/4", "C/8"]
experiments = {}
for C_rate in C_rates:
    experiments[C_rate] = pybamm.Experiment(
        [
            (
                f"Charge at {C_rate} until 4.2 V",
                "Hold at 4.2 V until C/20",
                "Rest for 1 hour",
            )
        ]
    )

Solve the reversible plating model first. The default CasADi [1] solver is used here.

[5]:
def define_and_solve_sims(model, experiments, parameter_values):
    sims = {}
    for C_rate, experiment in experiments.items():
        sim = pybamm.Simulation(
            model, experiment=experiment, parameter_values=parameter_values
        )
        sim.solve(calc_esoh=False)
        sims[C_rate] = sim

    return sims


sims_reversible = define_and_solve_sims(
    models["reversible"], experiments, parameter_values
)
[6]:
colors = ["tab:purple", "tab:cyan", "tab:red", "tab:green", "tab:blue"]
linestyles = ["dashed", "dotted", "solid"]

param = models["reversible"].param
A = parameter_values.evaluate(param.L_y * param.L_z)
F = parameter_values.evaluate(param.F)
L_n = parameter_values.evaluate(param.n.L)

currents = [
    "X-averaged negative electrode volumetric interfacial current density [A.m-3]",
    "X-averaged negative electrode lithium plating volumetric interfacial current density [A.m-3]",
    "Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]",
]


def plot(sims):
    import matplotlib.pyplot as plt

    fig, axs = plt.subplots(2, 2, figsize=(13, 9))
    for (C_rate, sim), color in zip(sims.items(), colors):
        # Isolate final equilibration phase
        sol = sim.solution.cycles[0].steps[2]

        # Voltage vs time
        t = sol["Time [min]"].entries
        t = t - t[0]
        V = sol["Voltage [V]"].entries
        axs[0, 0].plot(t, V, color=color, linestyle="solid", label=C_rate)

        # Currents
        for current, ls in zip(currents, linestyles):
            j = sol[current].entries
            axs[0, 1].plot(t, j, color=color, linestyle=ls)

        # Plated lithium capacity
        Q_Li = sol["Loss of capacity to negative lithium plating [A.h]"].entries
        axs[1, 0].plot(t, Q_Li, color=color, linestyle="solid")

        # Capacity vs time
        Q_main = (
            sol["Negative electrode volume-averaged concentration [mol.m-3]"].entries
            * F
            * A
            * L_n
            / 3600
        )
        axs[1, 1].plot(t, Q_main, color=color, linestyle="solid")

    axs[0, 0].legend()
    axs[0, 0].set_ylabel("Voltage [V]")
    axs[0, 1].set_ylabel("Volumetric interfacial current density [A.m-3]")
    axs[0, 1].legend(("Deintercalation current", "Stripping current", "Total current"))
    axs[1, 0].set_ylabel("Plated lithium capacity [A.h]")
    axs[1, 1].set_ylabel("Intercalated lithium capacity [A.h]")

    for ax in axs.flat:
        ax.set_xlabel("Time [minutes]")

    fig.tight_layout()

    return fig, axs


plot(sims_reversible);
_images/source_examples_notebooks_models_lithium-plating_10_0.png

The results show both similarities and differences with those of Ren et al. [7]. Notably, unlike Ren et al., this model uses equations [5] that result in a small but finite amount of plated lithium being present in the steady state.

Now solve the irreversible plating model and see how it compares.

[7]:
sims_irreversible = define_and_solve_sims(
    models["irreversible"], experiments, parameter_values
)
[8]:
plot(sims_irreversible);
_images/source_examples_notebooks_models_lithium-plating_14_0.png

Unlike in the reversible case, there is no steady state and the capacity degrades quickly. The lithium inventory decreases by around 40 mAh in just an hour, which is unrealistic. The low temperature fast charge simulations are run one more time, with the partially reversible plating model.

[9]:
sims_partially_reversible = define_and_solve_sims(
    models["partially reversible"], experiments, parameter_values
)
[10]:
plot(sims_partially_reversible);
_images/source_examples_notebooks_models_lithium-plating_17_0.png

The partially reversible plating model has features of both the reversible and irreversible models, which is reflected in the results. The plated lithium capacity decreases with time as lithium is reversibly stripped, but the final plated lithium capacity now depends on charge rate, indicating that some lithium was irreversibly plated during charge.

References#

[11]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[5] Simon E. J. O'Kane, Ian D. Campbell, Mohamed W. J. Marzook, Gregory J. Offer, and Monica Marinescu. Physical origin of the differential voltage minimum associated with lithium plating in li-ion batteries. Journal of The Electrochemical Society, 167(9):090540, may 2020. URL: https://doi.org/10.1149/1945-7111/ab90ac, doi:10.1149/1945-7111/ab90ac.
[6] Simon E. J. O'Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.
[7] Dongsheng Ren, Kandler Smith, Dongxu Guo, Xuebing Han, Xuning Feng, Languang Lu, and Minggao Ouyang. Investigation of lithium plating-stripping process in li-ion batteries at low temperature using an electrochemical model. Journal of the Electrochemistry Society, 165:A2167-A2178, 2018. doi:10.1149/2.0661810jes.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Many Particle Model (MPM)#

The Many Paticle Model (MPM) of a lithium-ion battery is an extension of the Single Particle Model to account for a continuous distribution of active particle sizes in each electrode \(\text{k}=\text{n},\text{p}\). Therefore, many of the same model assumptions hold, e.g., the transport in the electrolyte is instantaneous and hence the through-cell variation (in \(x\)) is neglected. The full set of assumptions and description of the particle size geometry is given in [4]. Note that the MPM in [4] is for a half cell and the version implemented in PyBaMM is for a full cell and uses the notation and scaling given in [5].

Particle size geometry#

In each electrode \(\text{k}=\text{n},\text{p}\), there are spherical particles of each radius \(R_\text{k}\) in the range \(R_\text{k,min}<R_\text{k}<R_\text{k,max}\), with the fraction of all particles of a given radius \(R_\text{k}\) given by the particle-size distribution (base on number) \(f_\text{k,num}(R_\text{k})\). However, it is more convenient to deal with the fraction of surface area contributed by particles of radius \(R_\text{k}\), which we denote \(f_{\text{k},a}(R_\text{k})\) and refer to as the area-weighted particle-size distribution. The area and number-based distributions are related via

\[f_{\text{k},a}(R_\text{k}) = \frac{4\pi R_\text{k}^2 f_\text{k,num}(R_\text{k})}{\int_{R_\text{k,min}}^{R_\text{k,max}} 4\pi R_\text{k}^2 f_\text{k,num}(R_\text{k})\,\text{d}R_\text{k}}\]

The total amount of surface area (per unit volume) \(a_\text{k,tot}\) accounting for all particles is expressed in terms of the active material volume fraction \(\epsilon_{s,\text{k}}\), similar to the other models in PyBaMM (SPM, DFN):

\[\epsilon_{s,\text{k}}= \int \frac{1}{3} R_\text{k} \underbrace{a_\text{k,tot}f_{\text{k},a}(R_\text{k})}_{\text{area }a_\text{k}(R_\text{k})\text{ of particles size }R_\text{k}}\,\text{d}R_\text{k}\]

Rearranging and defining \(\bar{R}_{\text{k},a}=\int R_\text{k} f_{\text{k},a}(R_\text{k})\,\text{d}R_\text{k}\) as the mean of the area-weighted distribution, we find

\[a_\text{k,tot}=\frac{3\epsilon_{s,\text{k}}}{\int R_\text{k} f_{\text{k},a}(R_\text{k})\,\text{d}R_\text{k}} = \frac{3\epsilon_{s,\text{k}}}{\bar{R}_{\text{k},a}}.\]

Then \(a_\text{k,tot}\) is the aggregate surface area of the particle population and analogous to the variables "X-averaged negative electrode surface area to volume ratio [m-1]", etc. in the SPM, SPMe, and DFN models, and can be calculated in a similar way as shown above using the area-weighted mean radius \(\bar{R}_{\text{k},a}\) (other mean radii do not have this property). See [4] for more details on the different types of distribution and mean radii.

Another common way to express the size distribution is via particle volume. The fraction of volume contributed by the particles of radius \(R_\text{k}\), denoted the volume-weighted particle-size distribution is related to the number and area ones via

\[f_{\text{k},v}(R_\text{k}) = \frac{\frac{1}{3} R_\text{k} f_{\text{k},a}(R_\text{k})}{\int_{R_\text{k,min}}^{R_\text{k,max}} \frac{1}{3} R_\text{k} f_{\text{k},a}(R_\text{k})\,\text{d}R_\text{k}} =\frac{\frac{4}{3}\pi R_\text{k}^3 f_\text{k,num}(R_\text{k})}{\int_{R_\text{k,min}}^{R_\text{k,max}} \frac{4}{3}\pi R_\text{k}^3 f_\text{k,num}(R_\text{k})\,\text{d}R_\text{k}}\]

It is sufficient to specify \(f_{\text{k},a}(R_\text{k})\), which is present requirement in the MPM.

Model equations#

In each electrode, only one representative particle of each size \(R_\text{k}\) needs to be modelled. The concentration of lithium in the solid particles is denoted \(c_{\text{s,k}}(t,R_\text{k}, r_\text{k})\), which varies with time, particle radius \(R_\text{k}\), and the radial coordinate \(r_{\text{k}} \in[0,R_{\text{k}}]\) within the spherical particle. The potential is uniform across all particles in the electrode, \(\phi_{\text{s,k}}(t)\).

The equations for molar conservation of lithium (\(c_{\text{s,k}}\)) are then:

\[\begin{split}\frac{\partial c_{\text{s,k}}}{\partial t} = -\frac{1}{r_{\text{k}}^2} \frac{\partial}{\partial r_{\text{k}}} \left(r_{\text{k}}^2 N_{\text{s,k}}\right), \\ N_{\text{s,k}} = -D_{\text{s,k}}(c_{\text{s,k}}) \frac{\partial c_{\text{s,k}}}{\partial r_{\text{k}}}, \quad \text{k} \in \text{n, p},\end{split}\]
\[\begin{split}N_{\text{s,k}}\big|_{r_{\text{k}}=0} = 0, \ \ N_{\text{s,k}}\big|_{r_{\text{k}}=R_{\text{k}}} = \frac{j_{\text{k}}}{F} \quad \text{k} \in \text{n, p}, \quad\\ c_{\text{s,k}}(0,R_\text{k},r_{\text{k}}) = c_{\text{s,k,0}}, \quad \text{k} \in \text{n, p},\end{split}\]

where \(D_{\text{s,k}}\) is the diffusion coefficient in the solid, \(N_{\text{s,k}}\) denotes the flux of lithium ions in the solid particle, \(F\) is Faraday’s constant. The interfacial current density is given by \(j_\text{k}\), which also varies with particle size.

Algebraic equations for the potentials#

The potentials \(\phi_{\text{s,k}}(t)\) are determined via the integral constraint that the total current flowing across the electrode interface must equal (up to a minus sign) the through-cell current density \(i\). Writing this in terms of the potential differences \(\Delta \phi_{\text{s,k}} = \phi_{\text{s,k}} - \phi_{\text{e}}\),

\[\begin{split}L_\text{k}a_\text{k,tot}\int_{R_\text{k,min}}^{R_\text{k,max}} f_{\text{k},a}(R_\text{k})j_\text{k}\,\text{d}R_\text{k} = \begin{cases} i,\quad \text{k}=\text{n}\\ -i,\quad \text{k}=\text{p} \end{cases}\end{split}\]

with Butler-Volmer kinetics

\[j_\text{k}=j_{\text{0,k}} \sinh\left[\frac{F}{2R_g T}(\Delta \phi_{\text{s,k}}-U_{\text{k}}(c_{\text{s},\text{k}}))\right], \ \ j_{\text{0,k}} = m_{\text{k}}(c_{\text{e}}c_{\text{s,k}})^{1/2}(c_\text{k,max}-c_{\text{s,k}})^{1/2}.\]

This gives an integral (or algebraic once discretized) equation for \(\Delta \phi_{\text{s,k}}\) which is coupled to the concentration equations above. The voltage is then obtained from

\[V = \Delta \phi_{\text{s,p}} - \Delta \phi_{\text{s,n}}\]

Example solving MPM#

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

Create an instance of the model

[2]:
model = pybamm.lithium_ion.MPM()

First, let’s inspect some variables (e.g. the lithium concentration and interfacial current densities) that depend on particle size \(R_\text{k}\). The variables of interest are X-averaged versions as there is no dependence on \(x\).

[3]:
model.variables.search("X-averaged negative particle concentration")
X-averaged negative particle concentration
X-averaged negative particle concentration [mol.m-3]
X-averaged negative particle concentration distribution
X-averaged negative particle concentration distribution [mol.m-3]

The concentration that is being solved for in the MPM, and which varies with particle size is the one ending in "distribution".

[4]:
c_n_R_dependent = model.variables[
    "X-averaged negative particle concentration distribution [mol.m-3]"
]
c_n_R_dependent.domains
[4]:
{'primary': ['negative particle'],
 'secondary': ['negative particle size'],
 'tertiary': ['current collector']}

Notice that the secondary domain is 'negative particle size', which is treated as another (microscale) domain in PyBaMM.

The variable without the "distribution" has been “size averaged” and can be compared to the variable with the same name from the other lithium-ion models in PyBaMM with only a single particle size. The concentration within the particles is a volume-based quantity and is thus averaged by volume (to preserve the total amount of lithium):

\[\left<c_{\text{s,k}}\right>_v = \int_{R_\text{k,min}}^{R_\text{k,max}} f_{\text{k},v}(R_\text{k})c_{\text{s,k}}(t,R_\text{k}, r_\text{k})\,\text{d}R_\text{k}\]

In particular, if the variance of the particle-size distribution \(f_{\text{k},a}\) is shrunk to zero and all particles become concentrated at its mean radius \(\bar{R}_{\text{k},a}\), the variable "X-averaged negative particle concentration [mol.m-3]" will coincide with the same variable from an SPM with particle radius \(R_\text{k}=\bar{R}_{\text{k},a}\). However, "X-averaged negative particle concentration distribution [mol.m-3]" will remain “particle-size dependent”.

The convention of adding "distribution" to the end of a variable name to indicate particle-size dependence has been used for other variables, such as the interfacial current density:

[5]:
model.variables.search("X-averaged negative electrode interfacial current density")
X-averaged negative electrode interfacial current density
X-averaged negative electrode interfacial current density [A.m-2]
X-averaged negative electrode interfacial current density distribution
X-averaged negative electrode interfacial current density distribution [A.m-2]
X-averaged negative electrode interfacial current density per volume [A.m-3]

As the interfacial current density is a flux per unit area on the particle surface, the “size averaging” is done by area (to preserve the total flux of lithium):

\[\left<j_{\text{k}}\right>_a = \int_{R_\text{k,min}}^{R_\text{k,max}} f_{\text{k},a}(R_\text{k})j_{\text{k}}(t,R_\text{k})\,\text{d}R_\text{k}\]

The averaging is merely done to allow comparison to variables from other models with only a single size, and are not necessarily used within the MPM itself, or are physically meaningful.

Note: not all variables have a “distribution” version, such as the potentials or temperature variables, as they do not vary with particle size in the MPM as implemented here.

Mesh points#

By default, the size domain is discretized into 30 grid points on a uniform 1D mesh.

[6]:
for k, t in model.default_submesh_types.items():
    print(k, "is of type", t.__name__)
for var, npts in model.default_var_pts.items():
    print(var, "has", npts, "mesh points")
negative electrode is of type Uniform1DSubMesh
separator is of type Uniform1DSubMesh
positive electrode is of type Uniform1DSubMesh
negative particle is of type Uniform1DSubMesh
positive particle is of type Uniform1DSubMesh
negative particle size is of type Uniform1DSubMesh
positive particle size is of type Uniform1DSubMesh
current collector is of type SubMesh0D
x_n has 20 mesh points
x_s has 20 mesh points
x_p has 20 mesh points
r_n has 20 mesh points
r_p has 20 mesh points
y has 10 mesh points
z has 10 mesh points
R_n has 30 mesh points
R_p has 30 mesh points

Solve#

Now solve the MPM with the default parameters and size distributions.

[7]:
sim = pybamm.Simulation(model)
sim.solve(t_eval=[0, 3600])

# plot some variables that depend on R
output_variables = [
    "X-averaged negative particle surface concentration distribution [mol.m-3]",
    "X-averaged positive particle surface concentration distribution [mol.m-3]",
    "X-averaged positive electrode interfacial current density distribution [A.m-2]",
    "X-averaged negative area-weighted particle-size distribution [m-1]",
    "X-averaged positive area-weighted particle-size distribution [m-1]",
    "Voltage [V]",
]

sim.plot(output_variables=output_variables)
[7]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd39283a7f0>

We can also visualise the concentration within the particles.

[8]:
# Concentrations as a function of t, r and R
c_s_n = sim.solution[
    "X-averaged negative particle concentration distribution [mol.m-3]"
]
c_s_p = sim.solution[
    "X-averaged positive particle concentration distribution [mol.m-3]"
]

# r_n, r_p
r_n = sim.solution["r_n [m]"].entries[:, 0, 0]
r_p = sim.solution["r_p [m]"].entries[:, 0, 0]
# dimensional R_n, R_p
R_n = sim.solution["Negative particle sizes [m]"].entries[:, 0]
R_p = sim.solution["Positive particle sizes [m]"].entries[:, 0]
t = sim.solution["Time [s]"].entries


def plot_concentrations(t):
    f, axs = plt.subplots(1, 2, figsize=(10, 3))
    plot_c_n = axs[0].pcolormesh(
        R_n, r_n, c_s_n(r=r_n, R=R_n, t=t), vmin=0.15, vmax=0.8
    )
    plot_c_p = axs[1].pcolormesh(
        R_p, r_p, c_s_p(r=r_p, R=R_p, t=t), vmin=0.6, vmax=0.95
    )
    axs[0].set_xlabel(r"$R_n$ [$\mu$m]")
    axs[1].set_xlabel(r"$R_p$ [$\mu$m]")
    axs[0].set_ylabel(r"$r_n / R_n$")
    axs[1].set_ylabel(r"$r_p / R_p$")
    axs[0].set_title("Concentration in negative particles [mol.m-3]")
    axs[1].set_title("Concentration in positive particles [mol.m-3]")
    plt.colorbar(plot_c_n, ax=axs[0])
    plt.colorbar(plot_c_p, ax=axs[1])

    plt.show()


# initial time
plot_concentrations(t[0])
_images/source_examples_notebooks_models_MPM_20_0.png
[9]:
# final time
plot_concentrations(t[-1])
_images/source_examples_notebooks_models_MPM_21_0.png

Input custom particle-size distributions#

In order to solve the MPM, one must input the area-weighted particle-size distribution \(f_{\text{k},a}\) for each electrode \(\text{k}=\text{n,p}\) and the minimum and maximum radius limits \(R_\text{k,min}\), \(R_\text{k,max}\). The default distributions \(f_{\text{k},a}\), usable with the Marquis et al. [6] parameter set, are lognormals with means equal to the "Negative particle radius [m]" and "Positive particle radius [m]" values, and standard deviations equal to 0.3 times the mean.

You can input any size distribution \(f_{\text{k},a}(R_\text{k})\) as a function of \(R_\text{k}\), which we will now demonstrate.

Note: \(f_{\text{k},a}(R_\text{k})\) should ideally integrate to 1 over the specified \(R_\text{k}\) range, although it is automatically normalized within PyBaMM anyway. A distribution such as a lognormal, once restricted to \([R_\text{k,min},R_\text{k,max}]\), discretized, and then renormalized, strictly will not integrate to 1 or have the originally desired mean or variance. The mean and variance of the final discretized distribution can be checked as output variables (see below). Having a sufficient number of mesh points in \(R_\text{k}\) or a sufficiently wide interval \([R_\text{k,min},R_\text{k,max}]\) should alleviate this issue, however.

[10]:
# Parameter set (no distribution parameters by default)
params = pybamm.ParameterValues("Marquis2019")

# Extract the radii values. We will choose these to be the means of our area-weighted distributions
R_a_n_dim = params["Negative particle radius [m]"]
R_a_p_dim = params["Positive particle radius [m]"]

# Standard deviations (dimensional)
sd_a_n_dim = 0.2 * R_a_n_dim
sd_a_p_dim = 0.6 * R_a_p_dim

# Minimum and maximum particle sizes (dimensional)
R_min_n = 0
R_min_p = 0
R_max_n = 2 * R_a_n_dim
R_max_p = 3 * R_a_p_dim

# Set the area-weighted particle-size distributions.
# Choose a lognormal (but any pybamm function could be used)


def f_a_dist_n_dim(R):
    return pybamm.lognormal(R, R_a_n_dim, sd_a_n_dim)


def f_a_dist_p_dim(R):
    return pybamm.lognormal(R, R_a_p_dim, sd_a_p_dim)


# Note: the only argument must be the particle size R
[11]:
# input distribution params to the dictionary
distribution_params = {
    "Negative minimum particle radius [m]": R_min_n,
    "Positive minimum particle radius [m]": R_min_p,
    "Negative maximum particle radius [m]": R_max_n,
    "Positive maximum particle radius [m]": R_max_p,
    "Negative area-weighted " + "particle-size distribution [m-1]": f_a_dist_n_dim,
    "Positive area-weighted " + "particle-size distribution [m-1]": f_a_dist_p_dim,
}
params.update(distribution_params, check_already_exists=False)
[12]:
sim = pybamm.Simulation(model, parameter_values=params)
sim.solve(t_eval=[0, 3600])

sim.plot(output_variables=output_variables)
[12]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd393666a60>

The discretized size distributions can be plotted as histograms. Only the area-weighted distribution has been input, but the corresponding number and volume-weighted ones are also given as output variables.

[13]:
# The discrete sizes or "bins" used, and the distributions
R_p = sim.solution["Positive particle sizes [m]"].entries[
    :, 0
]  # const in the current collector direction
# The distributions
f_a_p = sim.solution[
    "X-averaged positive area-weighted particle-size distribution [m-1]"
].entries[:, 0]
f_num_p = sim.solution[
    "X-averaged positive number-based particle-size distribution [m-1]"
].entries[:, 0]
f_v_p = sim.solution[
    "X-averaged positive volume-weighted particle-size distribution [m-1]"
].entries[:, 0]


# plot
width_p = (R_p[-1] - R_p[-2]) / 1e-6
plt.bar(
    R_p / 1e-6,
    f_a_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:blue",
    label="area-weighted",
)
plt.bar(
    R_p / 1e-6,
    f_num_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:red",
    label="number-weighted",
)
plt.bar(
    R_p / 1e-6,
    f_v_p * 1e-6,
    width=width_p,
    alpha=0.3,
    color="tab:green",
    label="volume-weighted",
)
plt.xlim((0, 30))
plt.xlabel("Particle size $R_{\mathrm{p}}$ [$\mu$m]", fontsize=12)
plt.ylabel("[$\mu$m$^{-1}$]", fontsize=12)
plt.legend(fontsize=10)
plt.title("Discretized distributions (histograms) in positive electrode")
plt.show()
_images/source_examples_notebooks_models_MPM_28_0.png
Vary standard deviation as an input parameter#

You may define the standard deviation (or other distribution parameters except for the min or max radii) of the distribution as a pybamm “input” parameter, to quickly change the distribution at the solve stage.

[14]:
# Define standard deviation in negative electrode to vary
sd_a_p_dim = pybamm.Parameter(
    "Positive electrode area-weighted particle-size standard deviation [m]"
)

# Set the area-weighted particle-size distribution


def f_a_dist_p_dim(R):
    return pybamm.lognormal(R, R_a_p_dim, sd_a_p_dim)


# input to param dictionary
distribution_params = {
    "Positive electrode area-weighted particle-size "
    + "standard deviation [m]": "[input]",
    "Positive area-weighted " + "particle-size distribution [m-1]": f_a_dist_p_dim,
}
params.update(distribution_params, check_already_exists=False)
[15]:
# Experiment with a relaxation period, to see the effect of distribution width
experiment = pybamm.Experiment(["Discharge at 1 C for 3400 s", "Rest for 1 hours"])

sim = pybamm.Simulation(model, parameter_values=params, experiment=experiment)
solutions = []
for sd_a_p in [0.4, 0.6, 0.8]:
    solution = sim.solve(
        inputs={
            "Positive electrode area-weighted particle-size "
            + "standard deviation [m]": sd_a_p * R_a_p_dim
        }
    )
    solutions.append(solution)


pybamm.dynamic_plot(
    solutions,
    output_variables=output_variables,
    labels=["MPM, sd_a_p=0.4", "MPM, sd_a_p=0.6", "MPM, sd_a_p=0.8"],
)
[15]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd392c3d610>

Check the distribution statistics#

The mean and standard deviations of the final discretized distributions can be investigated using the output variables "Negative area-weighted mean particle radius" and "Negative area-weighted particle-size standard deviation", etc.

[16]:
print("The mean of the input lognormal was:", R_a_p_dim)
print("The means of discretized distributions are:")
for solution in solutions:
    R = solution["Positive area-weighted mean particle radius [m]"]
    print("Positive area-weighted mean particle radius [m]", R.entries[0])
The mean of the input lognormal was: 1e-05
The means of discretized distributions are:
Positive area-weighted mean particle radius [m] 9.972515783613799e-06
Positive area-weighted mean particle radius [m] 9.673853099212895e-06
Positive area-weighted mean particle radius [m] 9.124186918191047e-06
[17]:
print("The standard deviations of the input lognormal were:")
print(0.4 * R_a_p_dim)
print(0.6 * R_a_p_dim)
print(0.8 * R_a_p_dim)
print("The standard deviations of discretized distributions are:")
for solution in solutions:
    sd = solution["Positive area-weighted particle-size standard deviation [m]"]
    print("Positive area-weighted particle-size standard deviation [m]", sd.entries[0])
The standard deviations of the input lognormal were:
4.000000000000001e-06
6e-06
8.000000000000001e-06
The standard deviations of discretized distributions are:
Positive area-weighted particle-size standard deviation [m] 3.918218937679725e-06
Positive area-weighted particle-size standard deviation [m] 5.180362201055076e-06
Positive area-weighted particle-size standard deviation [m] 5.815728559306213e-06

Compare to SPM and DFN#

The MPM can also be easily compared to PyBaMM models with a single particle size. The standard output variables are computed in the MPM, averaging over the particle size domain.

[18]:
models = [pybamm.lithium_ion.SPM(), pybamm.lithium_ion.MPM(), pybamm.lithium_ion.DFN()]

# solve
sims = []
for model in models:
    sim = pybamm.Simulation(model)
    sim.solve(t_eval=[0, 3500])
    sims.append(sim)

# plot
pybamm.dynamic_plot(sims)
[18]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd3934ec970>

Model options#

The MPM is compatible with the current collector and thermal models (except the “x-full” thermal option). Currently, the MPM is not compatible with the various degradation submodels in PyBaMM (i.e. SEI models, particle cracking/swelling, or lithium plating).

Fickian diffusion vs Uniform profile#

One can choose from Fickian diffusion or a uniform concentration profile within the particles. Teh default is “Fickian diffusion”.

[19]:
model_Fickian = pybamm.lithium_ion.MPM(name="MPM Fickian")
model_Uniform = pybamm.lithium_ion.MPM(
    name="MPM Uniform", options={"particle": "uniform profile"}
)

sim_Fickian = pybamm.Simulation(model_Fickian)
sim_Uniform = pybamm.Simulation(model_Uniform)

sim_Fickian.solve(t_eval=[0, 3500])
sim_Uniform.solve(t_eval=[0, 3500])

pybamm.dynamic_plot([sim_Fickian, sim_Uniform], output_variables=output_variables)
[19]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd3a30ff880>
1D current collector model#

Add another macroscale dimension “z”, employing the “potential pair” option solving for the potential in the current collectors.

[20]:
# choose model options
model_cc = pybamm.lithium_ion.MPM(
    options={
        "current collector": "potential pair",
        "dimensionality": 1,
        "particle": "uniform profile",  # to reduce computation time
    }
)

# solve
sim_cc = pybamm.Simulation(model_cc)
sim_cc.solve(t_eval=[0, 3600])

# variables to plot
output_variables = [
    "X-averaged negative particle surface concentration distribution [mol.m-3]",
    "X-averaged positive particle surface concentration distribution [mol.m-3]",
    "X-averaged positive electrode interfacial current density distribution [A.m-2]",
    "Negative current collector potential [V]",
    "Positive current collector potential [V]",
    "Voltage [V]",
]
pybamm.dynamic_plot(sim_cc, output_variables=output_variables)
[20]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fd3307d5190>

References#

The relevant papers for this notebook are:

[21]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] Toby L. Kirk, Jack Evans, Colin P. Please, and S. Jonathan Chapman. Modelling electrode heterogeneity in lithium-ion batteries: unimodal and bimodal particle-size distributions. arXiv:2006.12208, 2020. URL: https://arxiv.org/abs/2006.12208, arXiv:2006.12208.
[5] Toby L. Kirk, Colin P. Please, and S. Jon Chapman. Physical modelling of the slow voltage relaxation phenomenon in lithium-ion batteries. Journal of The Electrochemical Society, 168(6):060554, jun 2021. URL: https://doi.org/10.1149/1945-7111/ac0bf7, doi:10.1149/1945-7111/ac0bf7.
[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.
[7] 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.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.
[9] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Multi-Species Multi-Reaction model#

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
zsh:1: no matches found: pybamm[plot,cite]
Note: you may need to restart the kernel to use updated packages.

Model Equations#

Here we briefly outline the models used for the open-circuit potential, kinetics, and solid phase transport used in the MSMR model, as described in Baker and Verbrugge (2018). The remaining physics is modelled differently depending on which options are selected. By default, the rest of the battery model is as described in Maquis et al. (2019). In the following we give equations for a single electrode.

Thermodynamics#

The MSMR model is developed by assuming that all electrochemical reactions at the electrode/electrolyte interface in a lithium insertion cell can be expressed in the form

\[\text{Li}^{+} + \text{e}^{-} + \text{H}_{j} \rightleftharpoons (\text{Li--H})_{j}.\]

For each species \(j\), a vacant host site \(\text{H}_{j}\) can accommodate one lithium leading to a filled host site \((\text{Li--H})_{j}\). The OCV for this reaction is written as

\[U_j = U_j^0 + \frac{\omega_j}{f}\log\left(\frac{X_j - x_j}{x_j}\right),\]

where \(f = (RT)/F\), and \(R\), \(T\), and \(F\) are the universal gas constant, temperature in Kelvin, and Faraday’s constant, respectively. Here \(X_j\) represents the total fraction of available host sites which can be occupied by species \(j\), \(x_j\) is the fraction of filled sites occupied by species \(j\), \(U_j^0\) is a concentration independent standard electrode potential, and the \(\omega_j\) is an unitless parameter that describes the level of disorder of the reaction represented by gallery \(j\).

The equation for each reaction can be inverted to give

\[x_j = \frac{X_j}{1+\exp[f(U-U_j^0)/\omega_j]}.\]

The overall electrode state of charge is given by summing the fractional occupancies

\[x = \sum_j x_j = \sum_j \frac{X_j}{1+\exp[f(U-U_j^0)/\omega_j]},\]

which is an explicit closed form expression for the inverse of the OCV. This is opposite to many battery models where one typically gives the OCV as an explicit function of the state of charge (or stoichiometry).

At a particle interface with the electrolyte, local equilibrium requires that

\[U_j = U(x) \quad \forall j.\]
Kinetics#

The kinetics of the insertion reaction are given as

\[i_j = i_{0,j}[e^{(1-\alpha_j)f\eta} - e^{-\alpha_jf\eta}], \qquad i = \sum_j i_j,\]

where \(i_j\) is the interfacial current associated with reaction \(j\), \(\alpha_j\) is the symmetry factor, \(\eta\) is the overpotential, given by

\[\eta = \phi_s - \phi_e - U(x),\]

where \(\phi_s\) and \(\phi_e\) are the solid phase and electrolyte potentials, respectively, and \(i_{0,j}\) is the exchange current density of reaction \(j\), given by

\[i_{0,j} = i_{0,j}^{ref}(x_j)^{\omega_j\alpha_j}(X_j-x_j)^{\omega_j(1-\alpha_j)}(c_e/c_e^{ref})^{1-\alpha_j},\]

where \(c_e\) is the electrolyte concentration.

Solid phase transport#

Within the MSMR framework, the flux within the particles is expressed in terms of gradient of the chemical potential

\[N = -c_{\text{T}}x\frac{D}{RT}\nabla \mu + x(N+N_{\text{H}}),\]

where \(N\) is the flux of lithiated sites, \(N_{\text{H}}\) is the flux of unlithiated sites, \(c_{\text{T}}\) is the total concentration of lithiated and delithiated sites, and \(D\) is a diffusion coefficient. Ignoring volumetric expansion during lithiation, the total flux of sites vanishes

\[N+N_{\text{H}}.\]

It can then be shown that

\[N = c_{\text{T}}fDx(1-x)\frac{\text{d}U}{\text{d}x}\nabla x.\]

A mass balance in the solid phase then gives

\[\frac{\partial x}{\partial t} = -\nabla\cdot\left(x(1-x)fD\frac{\text{d}U}{\text{d}x}\nabla x\right),\]

which, for a radially symmetric spherical particle, must be solved subject to the boundary conditions

\[N\big\vert_{r=0} = 0, \quad N\big\vert_{r=R} = \frac{i}{F},\]

where \(R\) is the particle radius. This must be supplemented with a suitable initial condition for the electrode state of charge.

Solution of this problem requires evaluate of the function \(U(x)\) and the derivative \(\text{d}U/\text{d}x\), but these functions cannot be explicitly integrated. This problem can be avoided by replacing the dependent variable \(x\) with a new dependent variable \(U\) subject to the transformation

\[x = \sum_j \frac{X_j}{1+\exp[f(U-U_j^0)/\omega_j]}.\]

This gives the following equation for mass balance within the particles

\[\frac{\text{d}U}{\text{d}x}\frac{\partial U}{\partial t} = -\nabla\cdot\left(x(1-x)fD\nabla x\right),\]

which must be solved along with the transformed boundary and initial conditions.

Parameterization of the MSMR model#

The behaviour of MSMR model is characterised by the parameters \(X_j\), \(U^0_j\), \(\omega_j\), \(\alpha_j\), and \(i_{0,j}^{ref}\). Let’s take a look at their values in the example parameter set provided in PyBaMM. The thermodynamic parameter values are taken from Verbrugge et al. (2017) and correspond to a graphite negative electrode and NMC positive electrode. The remaining value are based on a parameterization of the LG M50 cell, from Chen et al. (2020).

We first load in the MSMR model and specify the number of reactions in each electrode

[2]:
model = pybamm.lithium_ion.MSMR({"number of MSMR reactions": ("6", "4")})

Then we can inspect the parameter values

[3]:
parameter_values = model.default_parameter_values

# Loop over domains
for domain in ["negative", "positive"]:
    print(f"{domain} electrode:")
    d = domain[0]
    # Loop over reactions
    N = int(parameter_values["Number of reactions in " + domain + " electrode"])
    for i in range(N):
        print(
            f"X_{d}_{i} = {parameter_values[f'X_{d}_{i}']}, "
            f"U0_{d}_{i} = {parameter_values[f'U0_{d}_{i}']}, "
            f"w_{d}_{i} = {parameter_values[f'w_{d}_{i}']}, "
            f"a_{d}_{i} = {parameter_values[f'a_{d}_{i}']} "
            f"j0_ref_{d}_{i} = {parameter_values[f'j0_ref_{d}_{i}']}"
        )
negative electrode:
X_n_0 = 0.43336, U0_n_0 = 0.08843, w_n_0 = 0.08611, a_n_0 = 0.5 j0_ref_n_0 = 2.7
X_n_1 = 0.23963, U0_n_1 = 0.12799, w_n_1 = 0.08009, a_n_1 = 0.5 j0_ref_n_1 = 2.7
X_n_2 = 0.15018, U0_n_2 = 0.14331, w_n_2 = 0.72469, a_n_2 = 0.5 j0_ref_n_2 = 2.7
X_n_3 = 0.05462, U0_n_3 = 0.16984, w_n_3 = 2.53277, a_n_3 = 0.5 j0_ref_n_3 = 2.7
X_n_4 = 0.06744, U0_n_4 = 0.21446, w_n_4 = 0.0947, a_n_4 = 0.5 j0_ref_n_4 = 2.7
X_n_5 = 0.05476, U0_n_5 = 0.36325, w_n_5 = 5.97354, a_n_5 = 0.5 j0_ref_n_5 = 2.7
positive electrode:
X_p_0 = 0.13442, U0_p_0 = 3.62274, w_p_0 = 0.9671, a_p_0 = 0.5 j0_ref_p_0 = 5
X_p_1 = 0.3246, U0_p_1 = 3.72645, w_p_1 = 1.39712, a_p_1 = 0.5 j0_ref_p_1 = 5
X_p_2 = 0.21118, U0_p_2 = 3.90575, w_p_2 = 3.505, a_p_2 = 0.5 j0_ref_p_2 = 5
X_p_3 = 0.3298, U0_p_3 = 4.22955, w_p_3 = 5.52757, a_p_3 = 1 j0_ref_p_3 = 1000000.0

We can plot the functional form of the open-circuit potential \(U\), fractional occupancies \(x_j\), and exchange current densities \(i_{0,j}\) as a function of stoichiometry \(x\)

[4]:
# get symbolic parameters
param = model.param
param_n = param.n.prim
param_p = param.p.prim

# set up ranges for plotting
U_n = pybamm.linspace(0.05, 1.1, 1000)
U_p = pybamm.linspace(2.8, 4.4, 1000)

# get reference electrolyte concentration and temperature
c_e = param.c_e_init
T = param.T_init

# set up figure
fig, ax = plt.subplots(3, 2, figsize=(10, 10))
colors = ["r", "g", "b", "c", "m", "y"]

# sto vs potential
x_n = param_n.x(U_n)
x_p = param_p.x(U_p)
ax[0, 0].plot(parameter_values.evaluate(x_n), parameter_values.evaluate(U_n), "k-")
ax[0, 1].plot(parameter_values.evaluate(x_p), parameter_values.evaluate(U_p), "k-")
ax[0, 0].set_xlabel("x_n")
ax[0, 0].set_ylabel("U_n [V]")
ax[0, 1].set_xlabel("x_p")
ax[0, 1].set_ylabel("U_p [V]")

# fractional occupancy vs potential
for i in range(6):
    xj = param_n.x_j(U_n, i)
    ax[1, 0].plot(
        parameter_values.evaluate(x_n),
        parameter_values.evaluate(xj),
        color=colors[i],
        label=f"x_n_{i}",
    )
ax[1, 0].set_xlabel("x_n")
ax[1, 0].set_ylabel("x_n_j")
ax[1, 0].legend()
for i in range(4):
    xj = param_p.x_j(U_p, i)
    ax[1, 1].plot(
        parameter_values.evaluate(x_p),
        parameter_values.evaluate(xj),
        color=colors[i],
        label=f"x_p_{i}",
    )
ax[1, 1].set_xlabel("x_p")
ax[1, 1].set_ylabel("x_p_j")
ax[1, 1].legend()

# exchange current density vs potential
for i in range(6):
    xj = param_n.x_j(U_n, i)
    j0 = param_n.j0_j(c_e, U_n, T, i)
    ax[2, 0].plot(
        parameter_values.evaluate(x_n),
        parameter_values.evaluate(j0),
        color=colors[i],
        label=f"j0_n_{i}",
    )
ax[2, 0].set_xlabel("x_n")
ax[2, 0].set_ylabel("j0_n_j [A.m-2]")
ax[2, 0].legend()
for i in range(4):
    xj = param_p.x_j(U_p, i)
    j0 = param_p.j0_j(c_e, U_p, T, i)
    ax[2, 1].plot(
        parameter_values.evaluate(x_p),
        parameter_values.evaluate(j0),
        color=colors[i],
        label=f"j0_p_{i}",
    )
ax[2, 1].set_ylim([0, 0.5])
ax[2, 1].set_xlabel("x_p")
ax[2, 1].set_ylabel("j0_p_j [A.m-2]")
ax[2, 1].legend()

plt.tight_layout()
_images/source_examples_notebooks_models_MSMR_11_0.png

Example solving MSMR using PyBaMM#

Below we show how to set up and solve a CCCV experiment using the MSMR model in PyBaMM. We already created the model in the previous section, so we can go ahead and define our experiment, before creating and solving a simulation

[5]:
experiment = pybamm.Experiment(
    [
        (
            "Discharge at 1C for 1 hour or until 3 V",
            "Rest for 1 hour",
            "Charge at C/3 until 4.2 V",
            "Hold at 4.2 V until 10 mA",
            "Rest for 1 hour",
        ),
    ],
    period="10 seconds",
)
sim = pybamm.Simulation(model, experiment=experiment)
sim.solve()
At t = 275.026 and h = 2.68649e-11, the corrector convergence failed repeatedly or with |h| = hmin.
At t = 275.028 and h = 4.19765e-11, the corrector convergence failed repeatedly or with |h| = hmin.
[5]:
<pybamm.solvers.solution.Solution at 0x2853c7d00>

Finally we can plot the results. In the MSMR model we can look at both the potential and stoichiometry as a function of position through the electrode and within the particle

[6]:
sim.plot(
    [
        "Negative particle stoichiometry",
        "Positive particle stoichiometry",
        "X-averaged negative electrode open-circuit potential [V]",
        "X-averaged positive electrode open-circuit potential [V]",
        "Negative particle potential [V]",
        "Positive particle potential [V]",
        "Current [A]",
        "Voltage [V]",
    ],
    variable_limits="tight",  # make axes tight to plot at each timestep
)
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x285cd5640>

We can also look at the individual reactions

[7]:
xns = [
    f"Average x_n_{i}" for i in range(6)
]  # negative electrode reactions: x_n_0, x_n_1, ..., x_n_5
xps = [
    f"Average x_p_{i}" for i in range(4)
]  # positive electrode reactions: x_p_0, x_p_1, ..., x_p_3
sim.plot(
    [
        xns,
        xps,
        "Current [A]",
        "Negative electrode stoichiometry",
        "Positive electrode stoichiometry",
        "Voltage [V]",
    ]
)
[7]:
<pybamm.plotting.quick_plot.QuickPlot at 0x288076940>

and plot how they sum to give the electrode stoichiometry

[8]:
sol = sim.solution
time = sol["Time [h]"].data
fig, ax = plt.subplots(1, 2, figsize=(8, 4))

ax[0].plot(time, sol["Average negative particle stoichiometry"].data, "k-", label="x_n")
bottom = 0
for xn in xns:
    top = bottom + sol[xn].data
    ax[0].fill_between(time, bottom, top, label=xn[-4:])
    bottom = top
ax[0].set_xlabel("Time [h]")
ax[0].set_ylabel("x_n [-]")
ax[0].legend(loc="upper center", bbox_to_anchor=(0.5, -0.15), ncol=3)
ax[1].plot(time, sol["Average positive particle stoichiometry"].data, "k-", label="x_p")
bottom = 0
for xp in xps:
    top = bottom + sol[xp].data
    ax[1].fill_between(time, bottom, top, label=xp[-4:])
    bottom = top
ax[1].set_xlabel("Time [h]")
ax[1].set_ylabel("x_p [-]")
ax[1].legend(loc="upper center", bbox_to_anchor=(0.5, -0.15), ncol=3)
[8]:
<matplotlib.legend.Legend at 0x28c7d24f0>
_images/source_examples_notebooks_models_MSMR_19_1.png

References#

The relevant papers for this notebook are:

[9]:
pybamm.print_citations()
[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.
[2] Daniel R Baker and Mark W Verbrugge. Multi-species, multi-reaction model for porous intercalation electrodes: part i. model formulation and a perturbation solution for low-scan-rate, linear-sweep voltammetry of a spinel lithium manganese oxide electrode. Journal of The Electrochemical Society, 165(16):A3952, 2018.
[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.
[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.
[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.
[6] 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.
[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.
[8] Mark Verbrugge, Daniel Baker, Brian Koch, Xingcheng Xiao, and Wentian Gu. Thermodynamic model for substitutional materials: application to lithiated graphite, spinel manganese oxide, iron phosphate, and layered nickel-manganese-cobalt oxide. Journal of The Electrochemical Society, 164(11):E3243, 2017.
[9] 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.
[10] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Pouch cell model#

In this notebook we compare the solutions of two reduced-order models of a lithium-ion pouch cell with the full solution obtained using COMSOL. This example is based on the results in [6]. The code used to produce the results in [6] can be found here.

The full model is based on the Doyle-Fuller-Newman model [2] and, in the interest of simplicity, considers a one-dimensional current collector (i.e. variation in one of the current collector dimensions is ignored), resulting in a 2D macroscopic model.

The first of the reduced order models, which is applicable in the limit of large conductivity in the current collectors, solves a one-dimensional problem in the current collectors coupled to a one-dimensional DFN model describing the through-cell electrochemistry at each point. We refer to this as a 1+1D model, though since the DFN is already a pseudo-two-dimensional model, perhaps it is more properly a 1+1+1D model.

The second reduced order model, which is applicable in the limit of very large conductivity in the current collectors, solves a single (averaged) one-dimensional DFN model for the through-cell behaviour and an uncoupled problem for the distribution of potential in the current collectors (from which the resistance and heat source can be calculated). We refer to this model as the DFNCC, where the “CC” indicates the additional (uncoupled) current collector problem.

All of the model equations, and derivations of the reduced-order models, can be found in [6].

Solving the reduced-order pouch cell models in PyBaMM#

We begin by importing PyBaMM along with the other packages required in this notebook

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import pickle
import matplotlib.pyplot as plt
import numpy as np
import scipy.interpolate as interp

[notice] A new release of pip is available: 23.3.1 -> 23.3.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

We then need to load up the appropriate models. For the DFNCC we require a 1D model of the current collectors and an average 1D DFN model for the through-cell electrochemistry. The 1+1D pouch cell model is built directly into PyBaMM and are accessed by passing the model option “dimensionality” which can be 1 or 2, corresponding to 1D or 2D current collectors. This option can be passed to any existing electrochemical model (e.g. SPM, SPMe, DFN). Here we choose the DFN model.

For both electrochemical models we choose an “x-lumped” thermal model, meaning we assume that the temperature is uniform in the through-cell direction \(x\), but account for the variation in temperature in the transverse direction \(z\).

[2]:
cc_model = pybamm.current_collector.EffectiveResistance({"dimensionality": 1})
dfn_av = pybamm.lithium_ion.DFN({"thermal": "lumped"}, name="Average DFN")
dfn = pybamm.lithium_ion.DFN(
    {"current collector": "potential pair", "dimensionality": 1, "thermal": "x-lumped"},
    name="1+1D DFN",
)

We then add the models to a dictionary for easy access later

[3]:
models = {"Current collector": cc_model, "Average DFN": dfn_av, "1+1D DFN": dfn}

Next we update the parameters to match those used in the COMSOL simulation. In particular, we set the current to correspond to a 3C discharge and assume uniform Newton cooling on all boundaries.

[4]:
param = dfn.default_parameter_values
I_1C = param[
    "Nominal cell capacity [A.h]"
]  # 1C current is cell capacity multipled by 1 hour
param.update(
    {
        "Current function [A]": I_1C * 3,
        "Negative electrode diffusivity [m2.s-1]": 3.9 * 10 ** (-14),
        "Positive electrode diffusivity [m2.s-1]": 10 ** (-13),
        "Negative current collector surface heat transfer coefficient [W.m-2.K-1]": 10,
        "Positive current collector surface heat transfer coefficient [W.m-2.K-1]": 10,
        "Negative tab heat transfer coefficient [W.m-2.K-1]": 10,
        "Positive tab heat transfer coefficient [W.m-2.K-1]": 10,
        "Edge heat transfer coefficient [W.m-2.K-1]": 10,
        "Total heat transfer coefficient [W.m-2.K-1]": 10,
    }
)

In this example we choose to discretise in space using 16 nodes per domain.

[5]:
npts = 16
var_pts = {
    "x_n": npts,
    "x_s": npts,
    "x_p": npts,
    "r_n": npts,
    "r_p": npts,
    "z": npts,
}

Before solving the models we load the COMSOL data so that we can request the output at the times in the COMSOL solution

[6]:
comsol_results_path = pybamm.get_parameters_filepath(
    "input/comsol_results/comsol_1plus1D_3C.pickle"
)
comsol_variables = pickle.load(open(comsol_results_path, "rb"))

Next we loop over the models, creating and solving a simulation for each.

[7]:
simulations = {}
solutions = {}  # store solutions in a separate dict for easy access later
for name, model in models.items():
    sim = pybamm.Simulation(model, parameter_values=param, var_pts=var_pts)
    simulations[name] = sim  # store simulation for later
    if name == "Current collector":
        # model is independent of time, so just solve arbitrarily at t=0 using
        # the default algebraic solver
        t_eval = np.array([0])
        solutions[name] = sim.solve(t_eval=t_eval)
    else:
        # solve at COMSOL times using Casadi solver in "fast" mode
        t_eval = comsol_variables["time"]
        solutions[name] = sim.solve(
            solver=pybamm.CasadiSolver(mode="fast"), t_eval=t_eval
        )

Creating the COMSOL model#

In this section we show how to create a PyBaMM “model” from the COMSOL solution. If you are just interested in seeing the comparison the skip ahead to the section “Comparing the full and reduced-order models”.

To create a PyBaMM model from the COMSOL data we must create a pybamm.Function object for each variable. We do this by interpolating in space to match the PyBaMM mesh and then creating a function to interpolate in time. The following cell defines the function that handles the creation of the pybamm.Function object.

[8]:
# set up times
comsol_t = comsol_variables["time"]
pybamm_t = comsol_t
# set up space
mesh = simulations["1+1D DFN"].mesh
L_z = param.evaluate(dfn.param.L_z)
pybamm_z = mesh["current collector"].nodes
z_interp = pybamm_z


def get_interp_fun_curr_coll(variable_name):
    """
    Create a :class:`pybamm.Function` object using the variable (interpolate in space
    to match nodes, and then create function to interpolate in time)
    """

    comsol_z = comsol_variables[variable_name + "_z"]
    variable = comsol_variables[variable_name]
    variable = interp.interp1d(comsol_z, variable, axis=0, kind="linear")(z_interp)

    # Make sure to use dimensional time
    fun = pybamm.Interpolant(
        comsol_t, variable.T, pybamm.t, name=variable_name + "_comsol"
    )
    fun.domains = {"primary": "current collector"}
    fun.mesh = mesh.combine_submeshes("current collector")
    fun.secondary_mesh = None

    return fun

We then pass the variables of interest to the interpolating function

[9]:
comsol_voltage = pybamm.Interpolant(
    comsol_t,
    comsol_variables["voltage"],
    pybamm.t,
    name="voltage_comsol",
)
comsol_voltage.mesh = None
comsol_voltage.secondary_mesh = None
comsol_phi_s_cn = get_interp_fun_curr_coll("phi_s_cn")
comsol_phi_s_cp = get_interp_fun_curr_coll("phi_s_cp")
comsol_current = get_interp_fun_curr_coll("current")
comsol_temperature = get_interp_fun_curr_coll("temperature")

and add them to a pybamm.BaseModel object

[10]:
comsol_model = pybamm.BaseModel()
comsol_model._geometry = pybamm.battery_geometry(options={"dimensionality": 1})
comsol_model.variables = {
    "Voltage [V]": comsol_voltage,
    "Negative current collector potential [V]": comsol_phi_s_cn,
    "Positive current collector potential [V]": comsol_phi_s_cp,
    "Current collector current density [A.m-2]": comsol_current,
    "X-averaged cell temperature [K]": comsol_temperature,
    # Add spatial variables to match pybamm model
    "z [m]": simulations["1+1D DFN"].built_model.variables["z [m]"],
}

We then add the solution object from the 1+1D model. This is just so that PyBaMM uses the same times behind the scenes when dealing with COMSOL model and the reduced-order models: the variables in comsol_model.variables are functions of time only that return the (interpolated in space) COMSOL solution.

[11]:
comsol_solution = pybamm.Solution(
    solutions["1+1D DFN"].t, solutions["1+1D DFN"].y, comsol_model, {}
)

Comparing the full and reduced-order models#

The DFNCC requires some post-processing to extract the solution variables. In particular, we need to pass the current and voltage from the average DFN model to the current collector model in order to compute the distribution of the potential in the current collectors and to account for the effect of the current collector resistance in the voltage.

This process is automated by the method post_process which accepts the current collector solution object, the parameters and the voltage and current from the average DFN model. The results are stored in the dictionary dfncc_vars

[12]:
V_av = solutions["Average DFN"]["Voltage [V]"]
I_av = solutions["Average DFN"]["Total current density [A.m-2]"]

dfncc_vars = cc_model.post_process(solutions["Current collector"], param, V_av, I_av)

Next we create a function to create some custom plots. For a given variable the plots will show: (a) the COMSOL results as a function of position in the current collector \(z\) and time \(t\); (b) a comparison of the full and reduced-order models and a sequence of times; (c) the time-averaged error between the full and reduced-order models as a function of space; and (d) the space-averaged error between the full and reduced-order models as a function of time.

[13]:
def plot(
    t_plot,
    z_plot,
    t_slices,
    var_name,
    units,
    comsol_var_fun,
    dfn_var_fun,
    dfncc_var_fun,
    param,
    cmap="viridis",
):
    fig, ax = plt.subplots(2, 2, figsize=(13, 7))
    fig.subplots_adjust(
        left=0.15, bottom=0.1, right=0.95, top=0.95, wspace=0.4, hspace=0.8
    )
    # plot comsol var
    comsol_var = comsol_var_fun(t=t_plot, z=z_plot)
    comsol_var_plot = ax[0, 0].pcolormesh(
        z_plot * 1e3, t_plot, np.transpose(comsol_var), shading="gouraud", cmap=cmap
    )
    if "cn" in var_name:
        format = "%.0e"
    elif "cp" in var_name:
        format = "%.0e"
    else:
        format = None
    fig.colorbar(
        comsol_var_plot,
        ax=ax,
        format=format,
        location="top",
        shrink=0.42,
        aspect=20,
        anchor=(0.0, 0.0),
    )

    # plot slices
    ccmap = plt.get_cmap("inferno")
    for ind, t in enumerate(t_slices):
        color = ccmap(float(ind) / len(t_slices))
        comsol_var_slice = comsol_var_fun(t=t, z=z_plot)
        dfn_var_slice = dfn_var_fun(t=t, z=z_plot)
        dfncc_var_slice = dfncc_var_fun(t=np.array([t]), z=z_plot)
        ax[0, 1].plot(
            z_plot * 1e3, comsol_var_slice, "o", fillstyle="none", color=color
        )
        ax[0, 1].plot(
            z_plot * 1e3,
            dfn_var_slice,
            "-",
            color=color,
            label=f"{t_slices[ind]:.0f} s",
        )
        ax[0, 1].plot(z_plot * 1e3, dfncc_var_slice, ":", color=color)
    # add dummy points for legend of styles
    (comsol_p,) = ax[0, 1].plot(np.nan, np.nan, "ko", fillstyle="none")
    (pybamm_p,) = ax[0, 1].plot(np.nan, np.nan, "k-", fillstyle="none")
    (dfncc_p,) = ax[0, 1].plot(np.nan, np.nan, "k:", fillstyle="none")

    # compute errors
    dfn_var = dfn_var_fun(t=t_plot, z=z_plot)
    dfncc_var = dfncc_var_fun(t=t_plot, z=z_plot)
    error = np.abs(comsol_var - dfn_var)
    error_bar = np.abs(comsol_var - dfncc_var)

    # plot time averaged error
    ax[1, 0].plot(z_plot * 1e3, np.nanmean(error, axis=1), "k-", label=r"$1+1$D")
    ax[1, 0].plot(z_plot * 1e3, np.nanmean(error_bar, axis=1), "k:", label="DFNCC")

    # plot z averaged error
    ax[1, 1].plot(t_plot, np.nanmean(error, axis=0), "k-", label=r"$1+1$D")
    ax[1, 1].plot(t_plot, np.nanmean(error_bar, axis=0), "k:", label="DFNCC")

    # set ticks
    ax[0, 0].tick_params(which="both")
    ax[0, 1].tick_params(which="both")
    ax[1, 0].tick_params(which="both")
    if var_name in ["$\mathcal{I}^*$"]:
        ax[1, 0].set_yscale("log")
        ax[1, 0].set_yticks = [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e-2, 1e-1, 1]
    else:
        ax[1, 0].ticklabel_format(style="sci", scilimits=(-2, 2), axis="y")
    ax[1, 1].tick_params(which="both")
    if var_name in ["$\phi^*_{\mathrm{s,cn}}$", "$\phi^*_{\mathrm{s,cp}} - V^*$"]:
        ax[1, 0].ticklabel_format(style="sci", scilimits=(-2, 2), axis="y")
    else:
        ax[1, 1].set_yscale("log")
        ax[1, 1].set_yticks = [1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e-2, 1e-1, 1]

    # set labels
    ax[0, 0].set_xlabel(r"$z^*$ [mm]")
    ax[0, 0].set_ylabel(r"$t^*$ [s]")
    ax[0, 0].set_title(rf"{var_name} {units}", y=1.5)
    ax[0, 1].set_xlabel(r"$z^*$ [mm]")
    ax[0, 1].set_ylabel(rf"{var_name}")
    ax[1, 0].set_xlabel(r"$z^*$ [mm]")
    ax[1, 0].set_ylabel("Time-averaged" + "\n" + rf"absolute error {units}")
    ax[1, 1].set_xlabel(r"$t^*$ [s]")
    ax[1, 1].set_ylabel("Space-averaged" + "\n" + rf"absolute error {units}")

    ax[0, 0].text(-0.1, 1.6, "(a)", transform=ax[0, 0].transAxes)
    ax[0, 1].text(-0.1, 1.6, "(b)", transform=ax[0, 1].transAxes)
    ax[1, 0].text(-0.1, 1.2, "(c)", transform=ax[1, 0].transAxes)
    ax[1, 1].text(-0.1, 1.2, "(d)", transform=ax[1, 1].transAxes)

    leg1 = ax[0, 1].legend(
        bbox_to_anchor=(0, 1.1, 1.0, 0.102),
        loc="lower left",
        borderaxespad=0.0,
        ncol=3,
        mode="expand",
    )

    ax[0, 1].legend(
        [comsol_p, pybamm_p, dfncc_p],
        ["COMSOL", r"$1+1$D", "DFNCC"],
        bbox_to_anchor=(0, 1.5, 1.0, 0.102),
        loc="lower left",
        borderaxespad=0.0,
        ncol=3,
        mode="expand",
    )
    ax[0, 1].add_artist(leg1)

    ax[1, 0].legend(
        bbox_to_anchor=(0.0, 1.1, 1.0, 0.102),
        loc="lower right",
        borderaxespad=0.0,
        ncol=3,
    )
    ax[1, 1].legend(
        bbox_to_anchor=(0.0, 1.1, 1.0, 0.102),
        loc="lower right",
        borderaxespad=0.0,
        ncol=3,
    )

We then set up the times and points in space to use in the plots

[14]:
t_plot = comsol_t
z_plot = z_interp
t_slices = np.array([600, 1200, 1800, 2400, 3000]) / 3

and plot the negative current collector potential

[15]:
var = "Negative current collector potential [V]"
comsol_var_fun = comsol_solution[var]
dfn_var_fun = solutions["1+1D DFN"][var]

dfncc_var_fun = dfncc_vars[var]
plot(
    t_plot,
    z_plot,
    t_slices,
    "$\phi^*_{\mathrm{s,cn}}$",
    "[V]",
    comsol_var_fun,
    dfn_var_fun,
    dfncc_var_fun,
    param,
    cmap="cividis",
)
_images/source_examples_notebooks_models_pouch-cell-model_35_0.png

the positive current collector potential with respect to voltage

[16]:
var = "Positive current collector potential [V]"
comsol_var = comsol_solution[var]
V_comsol = comsol_solution["Voltage [V]"]


def comsol_var_fun(t, z):
    return comsol_var(t=t, z=z) - V_comsol(t=t)


dfn_var = solutions["1+1D DFN"][var]
V = solutions["1+1D DFN"]["Voltage [V]"]


def dfn_var_fun(t, z):
    return dfn_var(t=t, z=z) - V(t=t)


dfncc_var = dfncc_vars[var]
V_dfncc = dfncc_vars["Voltage [V]"]


def dfncc_var_fun(t, z):
    return dfncc_var(t=t, z=z) - V_dfncc(t)


plot(
    t_plot,
    z_plot,
    t_slices,
    "$\phi^*_{\mathrm{s,cp}} - V^*$",
    "[V]",
    comsol_var_fun,
    dfn_var_fun,
    dfncc_var_fun,
    param,
    cmap="viridis",
)
_images/source_examples_notebooks_models_pouch-cell-model_37_0.png

the through-cell current

[17]:
var = "Current collector current density [A.m-2]"
comsol_var_fun = comsol_solution[var]
dfn_var_fun = solutions["1+1D DFN"][var]

I_av = solutions["Average DFN"][var]


def dfncc_var_fun(t, z):
    "In the DFNCC the current is just the average current"
    return np.transpose(np.repeat(I_av(t)[:, np.newaxis], len(z), axis=1))


plot(
    t_plot,
    z_plot,
    t_slices,
    "$\mathcal{I}^*$",
    "[A/m${}^2$]",
    comsol_var_fun,
    dfn_var_fun,
    dfncc_var_fun,
    param,
    cmap="plasma",
)
_images/source_examples_notebooks_models_pouch-cell-model_39_0.png

and the temperature with respect to reference temperature

[18]:
T_ref = param.evaluate(dfn.param.T_ref)
var = "X-averaged cell temperature [K]"
comsol_var = comsol_solution[var]


def comsol_var_fun(t, z):
    return comsol_var(t=t, z=z) - T_ref


dfn_var = solutions["1+1D DFN"][var]


def dfn_var_fun(t, z):
    return dfn_var(t=t, z=z) - T_ref


T_av = solutions["Average DFN"][var]


def dfncc_var_fun(t, z):
    "In the DFNCC the temperature is just the average temperature"
    return np.transpose(np.repeat(T_av(t)[:, np.newaxis], len(z), axis=1)) - T_ref


plot(
    t_plot,
    z_plot,
    t_slices,
    "$\\bar{T}^* - \\bar{T}_0^*$",
    "[K]",
    comsol_var_fun,
    dfn_var_fun,
    dfncc_var_fun,
    param,
    cmap="inferno",
)
_images/source_examples_notebooks_models_pouch-cell-model_41_0.png

We see that the electrical conductivity of the current collectors is sufficiently high that the potentials remain fairly uniform in space, and both the 1+1D DFN and DFNCC models are able to accurately capture the potential distribution in the current collectors.

In the plot of the current we see that positioning both tabs at the top of the cell means that for most of the simulation the current preferentially travels through the upper part of the cell. Eventually, as the cell continues to discharge, this part becomes more (de)lithiated until the resultant local increase in through-cell resistance is sufficient for it to become preferential for the current to travel further along the current collectors and through the lower part of the cell. This behaviour is well captured by the 1+1D model. In the DFNCC formulation the through-cell current density is assumed uniform, so the greatest error is found at the ends of the current collectors where the current density deviates most from its average.

For the parameters used in this example we find that the temperature exhibits a relatively weak variation along the length of the current collectors.

References#

The relevant papers for this notebook are:

[19]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.
[6] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Generate rate capability plots#

In this notebook we use PyBaMM to generate rate capability plots and Ragone plots. Rate capability plots show discharge capacity at different C-rates. Similarly, multiplying the discharge current and capacity by the average discharge voltage we can obtain Ragon plots (that is energy vs power).

First we need to import pybamm and the other necessary packages.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

In this example we use the SPMe model with the default settings, but we could change it to other models, parameters, geometries… For example, you generate rate capability plots using the DFN model in this script.

[2]:
model = pybamm.lithium_ion.SPMe()

We now cycle a loop of different C-rates and run full discharge simulations for each C-rate. For each simulation we register the final discharge capacity, the applied current and the average discharge voltage. We can then generate the rate capability and the Ragone plots.

[3]:
C_rates = np.linspace(0.05, 5, 20)
capacities = np.zeros_like(C_rates)
currents = np.zeros_like(C_rates)
voltage_av = np.zeros_like(C_rates)

for i, C_rate in enumerate(C_rates):
    experiment = pybamm.Experiment(
        [f"Discharge at {C_rate:.4f}C until 3.2V"], period=f"{10 / C_rate:.4f} seconds"
    )
    sim = pybamm.Simulation(
        model, experiment=experiment, solver=pybamm.CasadiSolver(dt_max=120)
    )
    sim.solve()

    time = sim.solution["Time [s]"].entries
    capacity = sim.solution["Discharge capacity [A.h]"]
    current = sim.solution["Current [A]"]
    voltage = sim.solution["Voltage [V]"]

    capacities[i] = capacity(time[-1])
    currents[i] = current(time[-1])
    voltage_av[i] = np.mean(voltage(time))

plt.figure(1)
plt.scatter(C_rates, capacities)
plt.xlabel("C-rate")
plt.ylabel("Capacity [Ah]")

plt.figure(2)
plt.scatter(currents * voltage_av, capacities * voltage_av)
plt.xlabel("Power [W]")
plt.ylabel("Energy [Wh]")

plt.show()
_images/source_examples_notebooks_models_rate-capability_6_0.png
_images/source_examples_notebooks_models_rate-capability_6_1.png

References#

The relevant papers for this notebook are:

[4]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Saving PyBaMM models to file#

Models which are discretised (i.e. ready to solve/ previously solved, see this notebook for more information on the pybamm.Discretisation class) can be serialised and saved to a JSON file, ready to be read in again either in PyBaMM, or a different modelling library.

In the example below, we build a basic DFN model, and then save the model out to sim_model_example.json, which should have appear in the ‘models’ directory.

[10]:
%pip install pybamm -q    # install PyBaMM if it is not installed
import pybamm

# do the example
dfn_model = pybamm.lithium_ion.DFN()
dfn_sim = pybamm.Simulation(dfn_model)
# discretise and build the model
dfn_sim.build()

dfn_sim.save_model("sim_model_example")
Note: you may need to restart the kernel to use updated packages.

This model file can then be read in and solved by choosing a solver, and running as below.

[11]:
# Recreate the pybamm model from the JSON file
new_dfn_model = pybamm.load_model("sim_model_example.json")

sim_reloaded = pybamm.Simulation(new_dfn_model)
sim_reloaded.solve([0, 3600])
[11]:
<pybamm.solvers.solution.Solution at 0x2a4e10550>

It would be nice to plot the results of the two models, to confirm that they are producing the same result.

However, notice that running the code below generates an error stating that the model variables were not provided during the reading in of the model.

[12]:
dfn_models = [dfn_model, new_dfn_model]
sims = []
for model in dfn_models:
    plot_sim = pybamm.Simulation(model)
    plot_sim.solve([0, 3600])
    sims.append(plot_sim)

pybamm.dynamic_plot(sims, time_unit="seconds")
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
/Users/pipliggins/Documents/repos/pybamm-local/docs/source/examples/notebooks/models/saving_models.ipynb Cell 7 line 8
      <a href='vscode-notebook-cell:/Users/pipliggins/Documents/repos/pybamm-local/docs/source/examples/notebooks/models/saving_models.ipynb#W6sZmlsZQ%3D%3D?line=4'>5</a>     plot_sim.solve([0, 3600])
      <a href='vscode-notebook-cell:/Users/pipliggins/Documents/repos/pybamm-local/docs/source/examples/notebooks/models/saving_models.ipynb#W6sZmlsZQ%3D%3D?line=5'>6</a>     sims.append(plot_sim)
----> <a href='vscode-notebook-cell:/Users/pipliggins/Documents/repos/pybamm-local/docs/source/examples/notebooks/models/saving_models.ipynb#W6sZmlsZQ%3D%3D?line=7'>8</a> pybamm.dynamic_plot(sims, time_unit="seconds")

File ~/Documents/repos/pybamm-local/pybamm/plotting/dynamic_plot.py:20, in dynamic_plot(*args, **kwargs)
      8 """
      9 Creates a :class:`pybamm.QuickPlot` object (with arguments 'args' and keyword
     10 arguments 'kwargs') and then calls :meth:`pybamm.QuickPlot.dynamic_plot`.
   (...)
     17     The 'QuickPlot' object that was created
     18 """
     19 kwargs_for_class = {k: v for k, v in kwargs.items() if k != "testing"}
---> 20 plot = pybamm.QuickPlot(*args, **kwargs_for_class)
     21 plot.dynamic_plot(kwargs.get("testing", False))
     22 return plot

File ~/Documents/repos/pybamm-local/pybamm/plotting/quick_plot.py:146, in QuickPlot.__init__(self, solutions, output_variables, labels, colors, linestyles, shading, figsize, n_rows, time_unit, spatial_unit, variable_limits)
    144 # check variables have been provided after any serialisation
    145 if any(len(m.variables) == 0 for m in models):
--> 146     raise AttributeError("No variables to plot")
    148 self.n_rows = n_rows or int(
    149     len(output_variables) // np.sqrt(len(output_variables))
    150 )
    151 self.n_cols = int(np.ceil(len(output_variables) / self.n_rows))

AttributeError: No variables to plot

To be able to plot the results from a serialised model, the mesh and model variables need to be saved alongside the model itself.

To do this, set the variables option to True when saving the model as in the example below; notice how the models will now plot nicely.

[13]:
# using the first simulation, save a new file which includes a list of the model variables
dfn_sim.save_model("sim_model_variables", variables=True)

# read the model back in
model_with_vars = pybamm.load_model("sim_model_variables.json")

# plot the pre- and post-serialisation models together to prove they behave the same
models = [dfn_model, model_with_vars]
sims = []
for model in models:
    sim = pybamm.Simulation(model)
    sim.solve([0, 3600])
    sims.append(sim)

pybamm.dynamic_plot(sims, time_unit="seconds")
[13]:
<pybamm.plotting.quick_plot.QuickPlot at 0x111963010>

Saving from Model#

Alternatively, the model can be saved directly from the Model class.

Note that at the moment, only models derived from the BaseBatteryModel class can be serialised; those built from scratch using pybamm.BaseModel() are currently unsupported.

First set up the model, as explained in detail for the SPM.

[14]:
# create the model
spm_model = pybamm.lithium_ion.SPM()

# set up and discretise ready to solve
geometry = spm_model.default_geometry
param = spm_model.default_parameter_values
param.process_model(spm_model)
param.process_geometry(geometry)
mesh = pybamm.Mesh(geometry, spm_model.default_submesh_types, spm_model.default_var_pts)
disc = pybamm.Discretisation(mesh, spm_model.default_spatial_methods)
disc.process_model(spm_model)
[14]:
<pybamm.models.full_battery_models.lithium_ion.spm.SPM at 0x29cf65c90>

Then save the model. Note that in this case the model variables and the mesh must be provided directly.

[15]:
# Serialise the spm_model, providing the varaibles and the mesh
spm_model.save_model("example_model", variables=spm_model.variables, mesh=mesh)

Now you can read the model back in, solve and plot.

[16]:
# read back in
new_spm_model = pybamm.load_model("example_model.json")

# select a solver and solve
new_spm_solver = new_spm_model.default_solver
new_spm_solution = new_spm_solver.solve(new_spm_model, [0, 3600])

# plot the solution
new_spm_solution.plot()
[16]:
<pybamm.plotting.quick_plot.QuickPlot at 0x29c8a3d10>

Making edits to a serialised model#

As mentioned at the begining of this notebook, only models which have already been discretised can be serialised and readh back in. This means that after serialisation, the model cannot be edited, as the non-discretised elements of the model such as the original rhs are not saved.

If you are likely to want to save a model and then edit it in the future, you may wish to use the Simulation.save() functionality to pickle your simulation, as described in tutorial 6.

Before finishing we will remove the data files we saved so that we leave the directory as we found it

[17]:
import os

os.remove("example_model.json")
os.remove("sim_model_example.json")
os.remove("sim_model_variables.json")

References#

The relevant papers for this notebook are:

[18]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Modelling SEI growth on particle cracks#

This notebook provides a short demonsration of how the SEI and particle mechanics submodels can be combined to simulate SEI growth on particle cracks.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt

[notice] A new release of pip available: 22.3.1 -> 23.0.1
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

Define two models. In model1, the only degradation mechanism is solvent-diffusion limited SEI growth. model2 includes the same SEI growth mechanism but also includes particle cracking and SEI growth on the cracks. The SEI model is run twice: once on the initial surface and once on the cracks. The equations for SEI on cracks are reported by O’Kane et al. [9] To ensure a fair experiment, particle swelling is enabled in both models.

[2]:
model1 = pybamm.lithium_ion.DFN(
    {"SEI": "solvent-diffusion limited", "particle mechanics": "swelling only"}
)
model2 = pybamm.lithium_ion.DFN(
    {
        "particle mechanics": "swelling and cracking",
        "SEI": "solvent-diffusion limited",
        "SEI on cracks": "true",
    }
)

Depending on the parameter set being used, the particle cracking model can require a large number of mesh points inside the particles to be numerically stable.

[3]:
param = pybamm.ParameterValues("OKane2022")
var_pts = {
    "x_n": 20,  # negative electrode
    "x_s": 20,  # separator
    "x_p": 20,  # positive electrode
    "r_n": 26,  # negative particle
    "r_p": 26,  # positive particle
}

Solve the models with and without cracking. The steps before the 1C discharge make the model more numerically stable so fewer mesh points are required.

[4]:
exp = pybamm.Experiment(
    ["Hold at 4.2 V until C/100", "Rest for 1 hour", "Discharge at 1C until 2.5 V"]
)
sim1 = pybamm.Simulation(
    model1, parameter_values=param, experiment=exp, var_pts=var_pts
)
sol1 = sim1.solve(calc_esoh=False)
sim2 = pybamm.Simulation(
    model2, parameter_values=param, experiment=exp, var_pts=var_pts
)
sol2 = sim2.solve(calc_esoh=False)
At t = 426.174, , mxstep steps taken before reaching tout.
At t = 186.174, , mxstep steps taken before reaching tout.
At t = 430.603, , mxstep steps taken before reaching tout.
At t = 190.603, , mxstep steps taken before reaching tout.
[5]:
t1 = sol1["Time [s]"].entries
V1 = sol1["Voltage [V]"].entries
SEI1 = sol1["Loss of lithium to negative SEI [mol]"].entries
lithium_neg1 = sol1["Total lithium in negative electrode [mol]"].entries
lithium_pos1 = sol1["Total lithium in positive electrode [mol]"].entries
t2 = sol2["Time [s]"].entries
V2 = sol2["Voltage [V]"].entries
SEI2 = (
    sol2["Loss of lithium to negative SEI [mol]"].entries
    + sol2["Loss of lithium to negative SEI on cracks [mol]"].entries
)
lithium_neg2 = sol2["Total lithium in negative electrode [mol]"].entries
lithium_pos2 = sol2["Total lithium in positive electrode [mol]"].entries
[6]:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 4))
ax1.plot(t1, V1, label="without cracking")
ax1.plot(t2, V2, label="with cracking")
ax1.set_xlabel("Time [s]")
ax1.set_ylabel("Voltage [V]")
ax1.legend()
ax2.plot(t1, SEI1, label="without cracking")
ax2.plot(t2, SEI2, label="with cracking")
ax2.set_xlabel("Time [s]")
ax2.set_ylabel("Loss of lithium to SEI [mol]")
ax2.legend()
plt.show()
_images/source_examples_notebooks_models_SEI-on-cracks_9_0.png

The SEI on cracks consumes far more capacity than the SEI on the initial surface, in agreement with the literature. Finally, check lithium is conserved:

[7]:
fig, ax = plt.subplots()
ax.plot(t2, lithium_neg2 + lithium_pos2)
ax.plot(t2, lithium_neg2[0] + lithium_pos2[0] - SEI2, linestyle="dashed")
ax.set_xlabel("Time [s]")
ax.set_ylabel("Total lithium in electrodes [mol]")
plt.show()
_images/source_examples_notebooks_models_SEI-on-cracks_11_0.png
[8]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.
[8] Simon E. J. O'Kane, Ian D. Campbell, Mohamed W. J. Marzook, Gregory J. Offer, and Monica Marinescu. Physical origin of the differential voltage minimum associated with lithium plating in li-ion batteries. Journal of The Electrochemical Society, 167(9):090540, may 2020. URL: https://doi.org/10.1149/1945-7111/ab90ac, doi:10.1149/1945-7111/ab90ac.
[9] Simon E. J. O'Kane, Weilong Ai, Ganesh Madabattula, Diego Alonso-Alvarez, Robert Timms, Valentin Sulzer, Jacqueline Sophie Edge, Billy Wu, Gregory J. Offer, and Monica Marinescu. Lithium-ion battery degradation: how to model it. Phys. Chem. Chem. Phys., 24:7909-7922, 2022. URL: http://dx.doi.org/10.1039/D2CP00417H, doi:10.1039/D2CP00417H.
[10] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Simulating a 3E cell#

In this notebook we show how to insert a reference electrode to mimic a 3E cell.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

We first load a model

[2]:
model = pybamm.lithium_ion.DFN()

Next we use the helper function insert_reference_electrode to insert a reference electrode into the model. This function takes the position of the reference electrode as an optional argument. If no position is given, the reference electrode is inserted at the midpoint of the separator. The helper function adds the new variables “Reference electrode potential [V]”, “Negative electrode 3E potential [V]” and “Positive electrode 3E potential [V]” to the model.

In this example we will explicitly pass a position to show how it is done

[3]:
L_n = model.param.n.L  # Negative electrode thickness [m]
L_s = model.param.s.L  # Separator thickness [m]
L_ref = L_n + L_s / 2  # Reference electrode position [m]

model.insert_reference_electrode(L_ref)

Next we can set up a simulation and solve the model as usual

[4]:
sim = pybamm.Simulation(model)
sim.solve([0, 3600])
[4]:
<pybamm.solvers.solution.Solution at 0x169dc5950>

Let’s plot a comparison of the 3E potentials and the potential difference between the solid and electrolyte phases at the electrode/separator interfaces

[5]:
sim.plot(
    [
        [
            "Negative electrode surface potential difference at separator interface [V]",
            "Negative electrode 3E potential [V]",
        ],
        [
            "Positive electrode surface potential difference at separator interface [V]",
            "Positive electrode 3E potential [V]",
        ],
        "Voltage [V]",
    ]
)
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x169816a90>
[6]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Run simulations with O’Regan 2022 parameter set (LG M50)#

In this notebook we show an example on how to run the DFN model with the O’Regan 2022 parameter set for the LG M50 cell. Because of the concentration dependent diffusion coefficient, we need to customise the mesh so the simulations converge.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

We first initialise the model and define the parameter set and the solver:

[2]:
# DFN + lumped thermal
options = {"thermal": "lumped", "dimensionality": 0, "cell geometry": "arbitrary"}
model = pybamm.lithium_ion.DFN(options=options)

# O'Regan 2022 parameter set
param = pybamm.ParameterValues("ORegan2022")

# Choose CasADI fast (we do a short discharge so there are no events, if events are needed choose "fast with events")
solver = pybamm.CasadiSolver(mode="fast")

Now we define the new mesh. Because the particle diffusivity depends on concentration and get drop very abruptly, we set a fine mesh (40 points in each particle). Additionally, given that the larger gradients occur towards the particle surface, we choose an exponential mesh weighted towards the right (i.e. the particle surface). This generates a non-uniform mesh with a finer grid towards the particle surface and a coarser grid towards the particle centre.

[3]:
var_pts = {"x_n": 30, "x_s": 30, "x_p": 30, "r_n": 40, "r_p": 40}

submesh_types = model.default_submesh_types
submesh_types["negative particle"] = pybamm.MeshGenerator(
    pybamm.Exponential1DSubMesh, submesh_params={"side": "right"}
)
submesh_types["positive particle"] = pybamm.MeshGenerator(
    pybamm.Exponential1DSubMesh, submesh_params={"side": "right"}
)

# Define the simulation
sim = pybamm.Simulation(
    model,
    parameter_values=param,
    C_rate=1,
    solver=solver,
    var_pts=var_pts,
    submesh_types=submesh_types,
)

Finally, we can solve the simulation and plot the results. Note that because the nonlinear diffusion and the fine mesh, if we want to solve for longer time, such as full discharge or other experiments, the simulations might take a few minutes to run.

[4]:
sim.solve(
    [0, 10]
)  # solving time kept short for testing purposes, feel free to extend it
sim.plot()
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7fe0c1824eb0>
[5]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[5] Kieran O'Regan, Ferran Brosa Planella, W. Dhammika Widanage, and Emma Kendrick. Thermal-electrochemical parametrisation of a lithium-ion battery: mapping Li concentration and temperature dependencies. Journal of The Electrochemical Society, ():, 2021. doi:.
[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.
[7] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Single Particle Model (SPM)#

Model Equations#

The SPM consists of two spherically symmetric diffusion equations: one within a representative negative particle (\(\text{k}=\text{n}\)) and one within a representative positive particle (\(\text{k}=\text{p}\)). In the centre of the particle the standard no-flux condition is imposed. Since the SPM assumes that all particles in an electrode behave in exactly the same way, the flux on the surface of a particle is simply the current \(I\) divided by the thickness of the electrode \(L_{\text{k}}\). The concentration of lithium in electrode \(\text{k}\) is denoted \(c_{\text{k}}\) and the current is denoted by \(I\). The model equations for the SPM are then:

\[\begin{split}\frac{\partial c_{\text{s,k}}}{\partial t} = -\frac{1}{r_{\text{k}}^2} \frac{\partial}{\partial r_{\text{k}}} \left(r_{\text{k}}^2 N_{\text{s,k}}\right), \\ N_{\text{s,k}} = -D_{\text{s,k}}(c_{\text{s,k}}) \frac{\partial c_{\text{s,k}}}{\partial r_{\text{k}}}, \quad \text{k} \in \text{n, p},\end{split}\]
\[\begin{split}N_{\text{s,k}}\big|_{r_{\text{k}}=0} = 0, \quad \text{k} \in \text{n, p}, \quad \ \ - N_{\text{s,k}}\big|_{r_{\text{k}}=1} = \begin{cases} \frac{I}{Fa_{\text{n}}L_{\text{n}}}, \quad &\text{k}=\text{n}, \\ -\frac{I}{Fa_{\text{p}}L_{\text{p}}}, \quad &\text{k}=\text{p}, \end{cases} \\ c_{\text{s,k}}(r_{\text{k}},0) = c_{\text{s,k,0}}, \quad \text{k} \in \text{n, p},\end{split}\]

where \(D_{\text{s,k}}\) is the diffusion coefficient in the solid, \(N_{\text{s,k}}\) denotes the flux of lithium ions in the solid particle within the region \(\text{k}\), and \(r_{\text{k}} \in[0,1]\) is the radial coordinate of the particle in electrode \(\text{k}\).

Voltage Expression#

The voltage is obtained from the expression:

\[V = U_{\text{p}}(c_{\text{p}})\big|_{r_{\text{p}}=1} - U_{\text{n}}(c_{\text{n}})\big|_{r_{\text{n}}=1} - \frac{2RT}{F}\sinh^{-1}\left(\frac{I}{2j_{\text{0,p}} a_{\text{p}}L_{\text{p}}}\right) - \frac{2RT}{F}\sinh^{-1}\left(\frac{I}{2j_{\text{0,n}} a_{\text{p}}L_{\text{n}}}\right)\]

with the exchange current densities given by

\[j_{\text{0,k}} = (c_{\text{k}})^{1/2}(1-c_{\text{k}})^{1/2}\]

More details can be found in [3].

Example solving SPM using PyBaMM#

Below we show how to solve the Single Particle Model, using the default geometry, mesh, parameters, discretisation and solver provided with PyBaMM. In this notebook we explicitly handle all the stages of setting up, processing and solving the model in order to explain them in detail. However, it is often simpler in practice to use the Simulation class, which handles many of the stages automatically, as shown here.

First we need to import pybamm, and then change our working directory to the root of the pybamm folder.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

We then create an instance of the SPM:

[2]:
model = pybamm.lithium_ion.SPM()

The model object is a subtype of `pybamm.BaseModel <https://docs.pybamm.org/en/latest/source/api/models/base_models/base_model.html>`__, and contains all the equations that define this particular model. For example, the rhs dict contained in model has a dictionary mapping variables such as \(c_n\) to the equation representing its rate of change with time (i.e. \(\partial{c_n}/\partial{t}\)). We can see this explicitly by visualising this entry in the rhs dict:

[3]:
variable = list(model.rhs.keys())[1]
equation = list(model.rhs.values())[1]
print("rhs equation for variable '", variable, "' is:")
path = "docs/source/examples/notebooks/models/"
equation.visualise(path + "spm1.png")
rhs equation for variable ' Throughput capacity [A.h] ' is:

spm1

We need a geometry in which to define our model equations. In pybamm this is represented by the `pybamm.Geometry <https://docs.pybamm.org/en/latest/source/api/geometry/index.html>`__ class. In this case we use the default geometry object defined by the model

[4]:
geometry = model.default_geometry

This geometry object defines a number of domains, each with its own name, spatial variables and min/max limits (the latter are represented as equations similar to the rhs equation shown above). For instance, the SPM has the following domains:

[5]:
print("SPM domains:")
for i, (k, v) in enumerate(geometry.items()):
    print(str(i + 1) + ".", k, "with variables:")
    for var, rng in v.items():
        if "min" in rng:
            print("  -(", rng["min"], ") <=", var, "<= (", rng["max"], ")")
        else:
            print(var, "=", rng["position"])
SPM domains:
1. negative electrode with variables:
  -( 0 ) <= x_n <= ( Negative electrode thickness [m] )
2. separator with variables:
  -( Negative electrode thickness [m] ) <= x_s <= ( Negative electrode thickness [m] + Separator thickness [m] )
3. positive electrode with variables:
  -( Negative electrode thickness [m] + Separator thickness [m] ) <= x_p <= ( Negative electrode thickness [m] + Separator thickness [m] + Positive electrode thickness [m] )
4. negative particle with variables:
  -( 0 ) <= r_n <= ( Negative particle radius [m] )
5. positive particle with variables:
  -( 0 ) <= r_p <= ( Positive particle radius [m] )
6. current collector with variables:
z = 1

Both the model equations and the geometry include parameters, such as \(L_p\). We can substitute these symbolic parameters in the model with values by using the `pybamm.ParameterValues <https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html>`__ class, which takes either a python dictionary or CSV file with the mapping between parameter names and values. Rather than create our own instance of pybamm.ParameterValues, we will use the default parameter set included in the model

[6]:
param = model.default_parameter_values

We can then apply this parameter set to the model and geometry

[7]:
param.process_model(model)
param.process_geometry(geometry)

The next step is to mesh the input geometry. We can do this using the `pybamm.Mesh <https://docs.pybamm.org/en/latest/source/api/meshes/index.html>`__ class. This class takes in the geometry of the problem, and also two dictionaries containing the type of mesh to use within each domain of the geometry (i.e. within the positive or negative electrode domains), and the number of mesh points.

The default mesh types and the default number of points to use in each variable for the SPM are:

[8]:
for k, t in model.default_submesh_types.items():
    print(k, "is of type", t.__name__)
for var, npts in model.default_var_pts.items():
    print(var, "has", npts, "mesh points")
negative electrode is of type Uniform1DSubMesh
separator is of type Uniform1DSubMesh
positive electrode is of type Uniform1DSubMesh
negative particle is of type Uniform1DSubMesh
positive particle is of type Uniform1DSubMesh
negative primary particle is of type Uniform1DSubMesh
positive primary particle is of type Uniform1DSubMesh
negative secondary particle is of type Uniform1DSubMesh
positive secondary particle is of type Uniform1DSubMesh
negative particle size is of type Uniform1DSubMesh
positive particle size is of type Uniform1DSubMesh
current collector is of type SubMesh0D
x_n has 20 mesh points
x_s has 20 mesh points
x_p has 20 mesh points
r_n has 20 mesh points
r_p has 20 mesh points
r_n_prim has 20 mesh points
r_p_prim has 20 mesh points
r_n_sec has 20 mesh points
r_p_sec has 20 mesh points
y has 10 mesh points
z has 10 mesh points
R_n has 30 mesh points
R_p has 30 mesh points

With these defaults, we can then create our mesh of the given geometry:

[9]:
mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)

The next step is to discretise the model equations using this mesh. We do this using the `pybamm.Discretisation <https://docs.pybamm.org/en/latest/source/api/spatial_methods/discretisation.html>`__ class, which takes both the mesh we have already created, and a dictionary of spatial methods to use for each geometry domain. For the case of the SPM, we use the following defaults for the spatial discretisation methods:

[10]:
for k, method in model.default_spatial_methods.items():
    print(k, "is discretised using", method.__class__.__name__, "method")
macroscale is discretised using FiniteVolume method
negative particle is discretised using FiniteVolume method
positive particle is discretised using FiniteVolume method
negative primary particle is discretised using FiniteVolume method
positive primary particle is discretised using FiniteVolume method
negative secondary particle is discretised using FiniteVolume method
positive secondary particle is discretised using FiniteVolume method
negative particle size is discretised using FiniteVolume method
positive particle size is discretised using FiniteVolume method
current collector is discretised using ZeroDimensionalSpatialMethod method

We then create the pybamm.Discretisation object, and use this to discretise the model equations

[11]:
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model)
[11]:
<pybamm.models.full_battery_models.lithium_ion.spm.SPM at 0x17eb5f790>

After this stage, all of the variables in model have been discretised into pybamm.StateVector objects, and spatial operators have been replaced by matrix-vector multiplications, ready to be evaluated within a time-stepping algorithm of a given solver. For example, the rhs expression for \(\partial{c_n}/\partial{t}\) that we visualised above is now represented by:

[12]:
model.concatenated_rhs.children[1].visualise(path + "spm2.png")

spm2

Now we are ready to run the time-stepping routine to solve the model. Once again we use the default ODE solver.

[13]:
# Solve the model at the given time points (in seconds)
solver = model.default_solver
n = 250
t_eval = np.linspace(0, 3600, n)
print("Solving using", type(solver).__name__, "solver...")
solution = solver.solve(model, t_eval)
print("Finished.")
Solving using CasadiSolver solver...
Finished.

Each model in pybamm has a list of relevant variables defined in the model, for use in visualising the model solution or for comparison with other models. The SPM defines the following variables:

[14]:
print("SPM model variables:")
for v in model.variables.keys():
    print("\t-", v)
SPM model variables:
        - Time [s]
        - Time [min]
        - Time [h]
        - x [m]
        - x_n [m]
        - x_s [m]
        - x_p [m]
        - r_n [m]
        - r_p [m]
        - Current variable [A]
        - Total current density [A.m-2]
        - Current [A]
        - C-rate
        - Discharge capacity [A.h]
        - Throughput capacity [A.h]
        - Discharge energy [W.h]
        - Throughput energy [W.h]
        - Porosity
        - Negative electrode porosity
        - X-averaged negative electrode porosity
        - Separator porosity
        - X-averaged separator porosity
        - Positive electrode porosity
        - X-averaged positive electrode porosity
        - Porosity change
        - Negative electrode porosity change [s-1]
        - X-averaged negative electrode porosity change [s-1]
        - Separator porosity change [s-1]
        - X-averaged separator porosity change [s-1]
        - Positive electrode porosity change [s-1]
        - X-averaged positive electrode porosity change [s-1]
        - Negative electrode interface utilisation variable
        - X-averaged negative electrode interface utilisation variable
        - Negative electrode interface utilisation
        - X-averaged negative electrode interface utilisation
        - Positive electrode interface utilisation variable
        - X-averaged positive electrode interface utilisation variable
        - Positive electrode interface utilisation
        - X-averaged positive electrode interface utilisation
        - Negative particle crack length [m]
        - X-averaged negative particle crack length [m]
        - Negative particle cracking rate [m.s-1]
        - X-averaged negative particle cracking rate [m.s-1]
        - Positive particle crack length [m]
        - X-averaged positive particle crack length [m]
        - Positive particle cracking rate [m.s-1]
        - X-averaged positive particle cracking rate [m.s-1]
        - Negative electrode active material volume fraction
        - X-averaged negative electrode active material volume fraction
        - Negative electrode capacity [A.h]
        - Negative particle radius
        - Negative particle radius [m]
        - X-averaged negative particle radius [m]
        - Negative electrode surface area to volume ratio [m-1]
        - X-averaged negative electrode surface area to volume ratio [m-1]
        - Negative electrode active material volume fraction change [s-1]
        - X-averaged negative electrode active material volume fraction change [s-1]
        - Loss of lithium due to loss of active material in negative electrode [mol]
        - Positive electrode active material volume fraction
        - X-averaged positive electrode active material volume fraction
        - Positive electrode capacity [A.h]
        - Positive particle radius
        - Positive particle radius [m]
        - X-averaged positive particle radius [m]
        - Positive electrode surface area to volume ratio [m-1]
        - X-averaged positive electrode surface area to volume ratio [m-1]
        - Positive electrode active material volume fraction change [s-1]
        - X-averaged positive electrode active material volume fraction change [s-1]
        - Loss of lithium due to loss of active material in positive electrode [mol]
        - Separator pressure [Pa]
        - X-averaged separator pressure [Pa]
        - negative electrode transverse volume-averaged velocity [m.s-1]
        - X-averaged negative electrode transverse volume-averaged velocity [m.s-1]
        - separator transverse volume-averaged velocity [m.s-1]
        - X-averaged separator transverse volume-averaged velocity [m.s-1]
        - positive electrode transverse volume-averaged velocity [m.s-1]
        - X-averaged positive electrode transverse volume-averaged velocity [m.s-1]
        - Transverse volume-averaged velocity [m.s-1]
        - negative electrode transverse volume-averaged acceleration [m.s-2]
        - X-averaged negative electrode transverse volume-averaged acceleration [m.s-2]
        - separator transverse volume-averaged acceleration [m.s-2]
        - X-averaged separator transverse volume-averaged acceleration [m.s-2]
        - positive electrode transverse volume-averaged acceleration [m.s-2]
        - X-averaged positive electrode transverse volume-averaged acceleration [m.s-2]
        - Transverse volume-averaged acceleration [m.s-2]
        - Negative electrode volume-averaged velocity [m.s-1]
        - Negative electrode volume-averaged acceleration [m.s-2]
        - X-averaged negative electrode volume-averaged acceleration [m.s-2]
        - Negative electrode pressure [Pa]
        - X-averaged negative electrode pressure [Pa]
        - Positive electrode volume-averaged velocity [m.s-1]
        - Positive electrode volume-averaged acceleration [m.s-2]
        - X-averaged positive electrode volume-averaged acceleration [m.s-2]
        - Positive electrode pressure [Pa]
        - X-averaged positive electrode pressure [Pa]
        - Negative particle stoichiometry
        - Negative particle concentration
        - Negative particle concentration [mol.m-3]
        - X-averaged negative particle concentration
        - X-averaged negative particle concentration [mol.m-3]
        - R-averaged negative particle concentration
        - R-averaged negative particle concentration [mol.m-3]
        - Average negative particle concentration
        - Average negative particle concentration [mol.m-3]
        - Negative particle surface stoichiometry
        - Negative particle surface concentration
        - Negative particle surface concentration [mol.m-3]
        - X-averaged negative particle surface concentration
        - X-averaged negative particle surface concentration [mol.m-3]
        - Negative electrode extent of lithiation
        - X-averaged negative electrode extent of lithiation
        - Minimum negative particle concentration
        - Maximum negative particle concentration
        - Minimum negative particle concentration [mol.m-3]
        - Maximum negative particle concentration [mol.m-3]
        - Minimum negative particle surface concentration
        - Maximum negative particle surface concentration
        - Minimum negative particle surface concentration [mol.m-3]
        - Maximum negative particle surface concentration [mol.m-3]
        - Positive particle stoichiometry
        - Positive particle concentration
        - Positive particle concentration [mol.m-3]
        - X-averaged positive particle concentration
        - X-averaged positive particle concentration [mol.m-3]
        - R-averaged positive particle concentration
        - R-averaged positive particle concentration [mol.m-3]
        - Average positive particle concentration
        - Average positive particle concentration [mol.m-3]
        - Positive particle surface stoichiometry
        - Positive particle surface concentration
        - Positive particle surface concentration [mol.m-3]
        - X-averaged positive particle surface concentration
        - X-averaged positive particle surface concentration [mol.m-3]
        - Positive electrode extent of lithiation
        - X-averaged positive electrode extent of lithiation
        - Minimum positive particle concentration
        - Maximum positive particle concentration
        - Minimum positive particle concentration [mol.m-3]
        - Maximum positive particle concentration [mol.m-3]
        - Minimum positive particle surface concentration
        - Maximum positive particle surface concentration
        - Minimum positive particle surface concentration [mol.m-3]
        - Maximum positive particle surface concentration [mol.m-3]
        - Porosity times concentration [mol.m-3]
        - Negative electrode porosity times concentration [mol.m-3]
        - Separator porosity times concentration [mol.m-3]
        - Positive electrode porosity times concentration [mol.m-3]
        - Total lithium in electrolyte [mol]
        - Electrolyte flux [mol.m-2.s-1]
        - Ambient temperature [K]
        - Cell temperature [K]
        - Negative current collector temperature [K]
        - Positive current collector temperature [K]
        - X-averaged cell temperature [K]
        - Volume-averaged cell temperature [K]
        - Negative electrode temperature [K]
        - X-averaged negative electrode temperature [K]
        - Separator temperature [K]
        - X-averaged separator temperature [K]
        - Positive electrode temperature [K]
        - X-averaged positive electrode temperature [K]
        - Ambient temperature [C]
        - Cell temperature [C]
        - Negative current collector temperature [C]
        - Positive current collector temperature [C]
        - X-averaged cell temperature [C]
        - Volume-averaged cell temperature [C]
        - Negative electrode temperature [C]
        - X-averaged negative electrode temperature [C]
        - Separator temperature [C]
        - X-averaged separator temperature [C]
        - Positive electrode temperature [C]
        - X-averaged positive electrode temperature [C]
        - Negative current collector potential [V]
        - Inner SEI thickness [m]
        - Outer SEI thickness [m]
        - X-averaged inner SEI thickness [m]
        - X-averaged outer SEI thickness [m]
        - SEI [m]
        - Total SEI thickness [m]
        - X-averaged SEI thickness [m]
        - X-averaged total SEI thickness [m]
        - X-averaged negative electrode resistance [Ohm.m2]
        - Inner SEI interfacial current density [A.m-2]
        - X-averaged inner SEI interfacial current density [A.m-2]
        - Outer SEI interfacial current density [A.m-2]
        - X-averaged outer SEI interfacial current density [A.m-2]
        - SEI interfacial current density [A.m-2]
        - X-averaged SEI interfacial current density [A.m-2]
        - Inner SEI on cracks thickness [m]
        - Outer SEI on cracks thickness [m]
        - X-averaged inner SEI on cracks thickness [m]
        - X-averaged outer SEI on cracks thickness [m]
        - SEI on cracks [m]
        - Total SEI on cracks thickness [m]
        - X-averaged SEI on cracks thickness [m]
        - X-averaged total SEI on cracks thickness [m]
        - Inner SEI on cracks interfacial current density [A.m-2]
        - X-averaged inner SEI on cracks interfacial current density [A.m-2]
        - Outer SEI on cracks interfacial current density [A.m-2]
        - X-averaged outer SEI on cracks interfacial current density [A.m-2]
        - SEI on cracks interfacial current density [A.m-2]
        - X-averaged SEI on cracks interfacial current density [A.m-2]
        - Lithium plating concentration [mol.m-3]
        - X-averaged lithium plating concentration [mol.m-3]
        - Dead lithium concentration [mol.m-3]
        - X-averaged dead lithium concentration [mol.m-3]
        - Lithium plating thickness [m]
        - X-averaged lithium plating thickness [m]
        - Dead lithium thickness [m]
        - X-averaged dead lithium thickness [m]
        - Loss of lithium to lithium plating [mol]
        - Loss of capacity to lithium plating [A.h]
        - Negative electrode lithium plating reaction overpotential [V]
        - X-averaged negative electrode lithium plating reaction overpotential [V]
        - Lithium plating interfacial current density [A.m-2]
        - X-averaged lithium plating interfacial current density [A.m-2]
        - Negative crack surface to volume ratio [m-1]
        - Negative electrode roughness ratio
        - X-averaged negative electrode roughness ratio
        - Positive crack surface to volume ratio [m-1]
        - Positive electrode roughness ratio
        - X-averaged positive electrode roughness ratio
        - Electrolyte transport efficiency
        - Negative electrolyte transport efficiency
        - X-averaged negative electrolyte transport efficiency
        - Separator electrolyte transport efficiency
        - X-averaged separator electrolyte transport efficiency
        - Positive electrolyte transport efficiency
        - X-averaged positive electrolyte transport efficiency
        - Electrode transport efficiency
        - Negative electrode transport efficiency
        - X-averaged negative electrode transport efficiency
        - Separator electrode transport efficiency
        - X-averaged separator electrode transport efficiency
        - Positive electrode transport efficiency
        - X-averaged positive electrode transport efficiency
        - Separator volume-averaged velocity [m.s-1]
        - Separator volume-averaged acceleration [m.s-2]
        - X-averaged separator volume-averaged acceleration [m.s-2]
        - Volume-averaged velocity [m.s-1]
        - Volume-averaged acceleration [m.s-1]
        - X-averaged volume-averaged acceleration [m.s-1]
        - Pressure [Pa]
        - Negative electrode stoichiometry
        - Negative electrode volume-averaged concentration
        - Negative electrode volume-averaged concentration [mol.m-3]
        - Total lithium in primary phase in negative electrode [mol]
        - Positive electrode stoichiometry
        - Positive electrode volume-averaged concentration
        - Positive electrode volume-averaged concentration [mol.m-3]
        - Total lithium in primary phase in positive electrode [mol]
        - Electrolyte concentration concatenation [mol.m-3]
        - Negative electrolyte concentration [mol.m-3]
        - X-averaged negative electrolyte concentration [mol.m-3]
        - Separator electrolyte concentration [mol.m-3]
        - X-averaged separator electrolyte concentration [mol.m-3]
        - Positive electrolyte concentration [mol.m-3]
        - X-averaged positive electrolyte concentration [mol.m-3]
        - Negative electrolyte concentration [Molar]
        - X-averaged negative electrolyte concentration [Molar]
        - Separator electrolyte concentration [Molar]
        - X-averaged separator electrolyte concentration [Molar]
        - Positive electrolyte concentration [Molar]
        - X-averaged positive electrolyte concentration [Molar]
        - Electrolyte concentration [mol.m-3]
        - X-averaged electrolyte concentration [mol.m-3]
        - Electrolyte concentration [Molar]
        - X-averaged electrolyte concentration [Molar]
        - Ohmic heating [W.m-3]
        - X-averaged Ohmic heating [W.m-3]
        - Volume-averaged Ohmic heating [W.m-3]
        - Irreversible electrochemical heating [W.m-3]
        - X-averaged irreversible electrochemical heating [W.m-3]
        - Volume-averaged irreversible electrochemical heating [W.m-3]
        - Reversible heating [W.m-3]
        - X-averaged reversible heating [W.m-3]
        - Volume-averaged reversible heating [W.m-3]
        - Total heating [W.m-3]
        - X-averaged total heating [W.m-3]
        - Volume-averaged total heating [W.m-3]
        - Current collector current density [A.m-2]
        - Inner SEI concentration [mol.m-3]
        - X-averaged inner SEI concentration [mol.m-3]
        - Outer SEI concentration [mol.m-3]
        - X-averaged outer SEI concentration [mol.m-3]
        - SEI concentration [mol.m-3]
        - X-averaged SEI concentration [mol.m-3]
        - Loss of lithium to SEI [mol]
        - Loss of capacity to SEI [A.h]
        - X-averaged negative electrode SEI interfacial current density [A.m-2]
        - Negative electrode SEI interfacial current density [A.m-2]
        - Positive electrode SEI interfacial current density [A.m-2]
        - X-averaged positive electrode SEI volumetric interfacial current density [A.m-2]
        - Positive electrode SEI volumetric interfacial current density [A.m-3]
        - Negative electrode SEI volumetric interfacial current density [A.m-3]
        - X-averaged negative electrode SEI volumetric interfacial current density [A.m-3]
        - Inner SEI on cracks concentration [mol.m-3]
        - X-averaged inner SEI on cracks concentration [mol.m-3]
        - Outer SEI on cracks concentration [mol.m-3]
        - X-averaged outer SEI on cracks concentration [mol.m-3]
        - SEI on cracks concentration [mol.m-3]
        - X-averaged SEI on cracks concentration [mol.m-3]
        - Loss of lithium to SEI on cracks [mol]
        - Loss of capacity to SEI on cracks [A.h]
        - X-averaged negative electrode SEI on cracks interfacial current density [A.m-2]
        - Negative electrode SEI on cracks interfacial current density [A.m-2]
        - Positive electrode SEI on cracks interfacial current density [A.m-2]
        - X-averaged positive electrode SEI on cracks volumetric interfacial current density [A.m-2]
        - Positive electrode SEI on cracks volumetric interfacial current density [A.m-3]
        - Negative electrode SEI on cracks volumetric interfacial current density [A.m-3]
        - X-averaged negative electrode SEI on cracks volumetric interfacial current density [A.m-3]
        - Negative electrode lithium plating interfacial current density [A.m-2]
        - X-averaged negative electrode lithium plating interfacial current density [A.m-2]
        - Lithium plating volumetric interfacial current density [A.m-3]
        - X-averaged lithium plating volumetric interfacial current density [A.m-3]
        - X-averaged positive electrode lithium plating interfacial current density [A.m-2]
        - X-averaged positive electrode lithium plating volumetric interfacial current density [A.m-3]
        - Positive electrode lithium plating interfacial current density [A.m-2]
        - Positive electrode lithium plating volumetric interfacial current density [A.m-3]
        - Negative electrode lithium plating volumetric interfacial current density [A.m-3]
        - X-averaged negative electrode lithium plating volumetric interfacial current density [A.m-3]
        - Negative electrode open-circuit potential [V]
        - X-averaged negative electrode open-circuit potential [V]
        - Negative electrode bulk open-circuit potential [V]
        - Negative particle concentration overpotential [V]
        - Negative electrode entropic change [V.K-1]
        - X-averaged negative electrode entropic change [V.K-1]
        - Positive electrode open-circuit potential [V]
        - X-averaged positive electrode open-circuit potential [V]
        - Positive electrode bulk open-circuit potential [V]
        - Positive particle concentration overpotential [V]
        - Positive electrode entropic change [V.K-1]
        - X-averaged positive electrode entropic change [V.K-1]
        - X-averaged negative electrode total interfacial current density [A.m-2]
        - X-averaged negative electrode total volumetric interfacial current density [A.m-3]
        - SEI film overpotential [V]
        - X-averaged SEI film overpotential [V]
        - Negative electrode exchange current density [A.m-2]
        - X-averaged negative electrode exchange current density [A.m-2]
        - Negative electrode reaction overpotential [V]
        - X-averaged negative electrode reaction overpotential [V]
        - X-averaged negative electrode surface potential difference [V]
        - Negative electrode interfacial current density [A.m-2]
        - X-averaged negative electrode interfacial current density [A.m-2]
        - Negative electrode volumetric interfacial current density [A.m-3]
        - X-averaged negative electrode volumetric interfacial current density [A.m-3]
        - X-averaged positive electrode total interfacial current density [A.m-2]
        - X-averaged positive electrode total volumetric interfacial current density [A.m-3]
        - Positive electrode exchange current density [A.m-2]
        - X-averaged positive electrode exchange current density [A.m-2]
        - Positive electrode reaction overpotential [V]
        - X-averaged positive electrode reaction overpotential [V]
        - X-averaged positive electrode surface potential difference [V]
        - Positive electrode interfacial current density [A.m-2]
        - X-averaged positive electrode interfacial current density [A.m-2]
        - Positive electrode volumetric interfacial current density [A.m-3]
        - X-averaged positive electrode volumetric interfacial current density [A.m-3]
        - Negative particle rhs [mol.m-3.s-1]
        - Negative particle bc [mol.m-2]
        - Negative particle effective diffusivity [m2.s-1]
        - X-averaged negative particle effective diffusivity [m2.s-1]
        - Negative particle flux [mol.m-2.s-1]
        - X-averaged negative particle flux [mol.m-2.s-1]
        - Positive particle rhs [mol.m-3.s-1]
        - Positive particle bc [mol.m-2]
        - Positive particle effective diffusivity [m2.s-1]
        - X-averaged positive particle effective diffusivity [m2.s-1]
        - Positive particle flux [mol.m-2.s-1]
        - X-averaged positive particle flux [mol.m-2.s-1]
        - Negative electrode potential [V]
        - X-averaged negative electrode potential [V]
        - Negative electrode ohmic losses [V]
        - X-averaged negative electrode ohmic losses [V]
        - Gradient of negative electrode potential [V.m-1]
        - Negative electrode current density [A.m-2]
        - Electrolyte potential [V]
        - X-averaged electrolyte potential [V]
        - X-averaged electrolyte overpotential [V]
        - Gradient of electrolyte potential [V.m-1]
        - Negative electrolyte potential [V]
        - X-averaged negative electrolyte potential [V]
        - Gradient of negative electrolyte potential [V.m-1]
        - Separator electrolyte potential [V]
        - X-averaged separator electrolyte potential [V]
        - Gradient of separator electrolyte potential [V.m-1]
        - Positive electrolyte potential [V]
        - X-averaged positive electrolyte potential [V]
        - Gradient of positive electrolyte potential [V.m-1]
        - Electrolyte current density [A.m-2]
        - Negative electrolyte current density [A.m-2]
        - Positive electrolyte current density [A.m-2]
        - X-averaged concentration overpotential [V]
        - X-averaged electrolyte ohmic losses [V]
        - Negative electrode surface potential difference [V]
        - Negative electrode surface potential difference at separator interface [V]
        - Sum of negative electrode electrolyte reaction source terms [A.m-3]
        - Sum of x-averaged negative electrode electrolyte reaction source terms [A.m-3]
        - Sum of negative electrode volumetric interfacial current densities [A.m-3]
        - Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]
        - Sum of positive electrode electrolyte reaction source terms [A.m-3]
        - Sum of x-averaged positive electrode electrolyte reaction source terms [A.m-3]
        - Sum of positive electrode volumetric interfacial current densities [A.m-3]
        - Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]
        - Interfacial current density [A.m-2]
        - Exchange current density [A.m-2]
        - Sum of volumetric interfacial current densities [A.m-3]
        - Sum of electrolyte reaction source terms [A.m-3]
        - Positive electrode potential [V]
        - X-averaged positive electrode potential [V]
        - Positive electrode ohmic losses [V]
        - X-averaged positive electrode ohmic losses [V]
        - Gradient of positive electrode potential [V.m-1]
        - Positive electrode current density [A.m-2]
        - Electrode current density [A.m-2]
        - Positive current collector potential [V]
        - Local voltage [V]
        - Terminal voltage [V]
        - Voltage [V]
        - Contact overpotential [V]
        - Positive electrode surface potential difference [V]
        - Positive electrode surface potential difference at separator interface [V]
        - Surface open-circuit voltage [V]
        - Bulk open-circuit voltage [V]
        - Particle concentration overpotential [V]
        - X-averaged reaction overpotential [V]
        - X-averaged solid phase ohmic losses [V]
        - Battery open-circuit voltage [V]
        - Battery negative electrode bulk open-circuit potential [V]
        - Battery positive electrode bulk open-circuit potential [V]
        - Battery particle concentration overpotential [V]
        - Battery negative particle concentration overpotential [V]
        - Battery positive particle concentration overpotential [V]
        - X-averaged battery reaction overpotential [V]
        - X-averaged battery negative reaction overpotential [V]
        - X-averaged battery positive reaction overpotential [V]
        - X-averaged battery solid phase ohmic losses [V]
        - X-averaged battery negative solid phase ohmic losses [V]
        - X-averaged battery positive solid phase ohmic losses [V]
        - X-averaged battery electrolyte ohmic losses [V]
        - X-averaged battery concentration overpotential [V]
        - Battery voltage [V]
        - Change in open-circuit voltage [V]
        - Local ECM resistance [Ohm]
        - Terminal power [W]
        - Power [W]
        - Resistance [Ohm]
        - Total lithium in negative electrode [mol]
        - LAM_ne [%]
        - Loss of active material in negative electrode [%]
        - Total lithium in positive electrode [mol]
        - LAM_pe [%]
        - Loss of active material in positive electrode [%]
        - LLI [%]
        - Loss of lithium inventory [%]
        - Loss of lithium inventory, including electrolyte [%]
        - Total lithium [mol]
        - Total lithium in particles [mol]
        - Total lithium capacity [A.h]
        - Total lithium capacity in particles [A.h]
        - Total lithium lost [mol]
        - Total lithium lost from particles [mol]
        - Total lithium lost from electrolyte [mol]
        - Total lithium lost to side reactions [mol]
        - Total capacity lost to side reactions [A.h]

To help visualise the results, pybamm provides the pybamm.ProcessedVariable class, which takes the output of a solver and a variable, and allows the user to evaluate the value of that variable at any given time or \(x\) value. These processed variables are automatically created by the solution dictionary.

[15]:
voltage = solution["Voltage [V]"]
c_s_n_surf = solution["Negative particle surface concentration"]
c_s_p_surf = solution["Positive particle surface concentration"]

One we have these variables in hand, we can begin generating plots using a library such as Matplotlib. Below we plot the voltage and surface particle concentrations versus time

[16]:
t = solution["Time [s]"].entries
x = solution["x [m]"].entries[:, 0]
f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13, 4))

ax1.plot(t, voltage(t))
ax1.set_xlabel(r"$Time [s]$")
ax1.set_ylabel("Voltage [V]")

ax2.plot(
    t, c_s_n_surf(t=t, x=x[0])
)  # can evaluate at arbitrary x (single representative particle)
ax2.set_xlabel(r"$Time [s]$")
ax2.set_ylabel("Negative particle surface concentration")

ax3.plot(
    t, c_s_p_surf(t=t, x=x[-1])
)  # can evaluate at arbitrary x (single representative particle)
ax3.set_xlabel(r"$Time [s]$")
ax3.set_ylabel("Positive particle surface concentration")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_models_SPM_35_0.png

Some of the output variables are defined over space as well as time. Once option to visualise these variables is to use the interact slider widget. Below we plot the negative/positive particle concentration over \(r\), using a slider to change the current time point

[17]:
c_s_n = solution["Negative particle concentration"]
c_s_p = solution["Positive particle concentration"]
r_n = solution["r_n [m]"].entries[:, 0]
r_p = solution["r_p [m]"].entries[:, 0]
[18]:
c_s_n = solution["Negative particle concentration"]
c_s_p = solution["Positive particle concentration"]
r_n = solution["r_n [m]"].entries[:, 0, 0]
r_p = solution["r_p [m]"].entries[:, 0, 0]


def plot_concentrations(t):
    f, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 5))
    (plot_c_n,) = ax1.plot(
        r_n, c_s_n(r=r_n, t=t, x=x[0])
    )  # can evaluate at arbitrary x (single representative particle)
    (plot_c_p,) = ax2.plot(
        r_p, c_s_p(r=r_p, t=t, x=x[-1])
    )  # can evaluate at arbitrary x (single representative particle)
    ax1.set_ylabel("Negative particle concentration")
    ax2.set_ylabel("Positive particle concentration")
    ax1.set_xlabel(r"$r_n$ [m]")
    ax2.set_xlabel(r"$r_p$ [m]")
    ax1.set_ylim(0, 1)
    ax2.set_ylim(0, 1)
    plt.show()


import ipywidgets as widgets

widgets.interact(
    plot_concentrations, t=widgets.FloatSlider(min=0, max=3600, step=10, value=0)
);

The QuickPlot class can be used to plot the common set of useful outputs which should give you a good initial overview of the model. The method Quickplot.dynamic_plot employs the slider widget.

[19]:
quick_plot = pybamm.QuickPlot(solution)
quick_plot.dynamic_plot()

References#

The relevant papers for this notebook are:

[20]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Single Particle Model with Electrolyte (SPMe)#

Model Equations#

  1. The SPMe comprises an equation for the lithium concentration in a representative particle in the negative electrode \(c_{\text{s,n}}\), an equation for the lithium concentration in a representative particle in the positive electrode \(c_{\text{s,p}}\), and an equation which governs the behaviour of the first-order correction to the lithium concentration in the electrolyte \(c_{\text{e,k}}\). Here we use a roman subscript \(\text{k} \in \text{n, s, p}\) is used to denote the regions negative electrode, separator, and positive electrode, respectively.

  2. At the centre of each particle the standard no-flux condition is imposed, and the flux on the surface of the particle is simply the current \(I\) divided by the thickness of the electrode \(L_{\text{k}}\), as in the SPM. Since lithium is transferred between the electrolyte and particles, the flux through the particle surface also enters the electrolyte diffusion equation as a source/sink term. There is no transfer of lithium between the electrolyte and current collectors, which leads to no flux boundary conditions on the lithium concentration in the electrolyte \(c_{\text{e,k}}\) at either end of the cell.

  3. We must also impose initial conditions which correspond to setting an initial concentration in each particle \(c_{\text{s,k}}(t=0) = c_{\text{s,k,0}}\), and to having no deviation from the initial (uniform) lithium concentration in the electrolyte \(c_{\text{e,k}}(t=0) = c_{\text{e,0}}\).

The model equations for the SPMe read:

Particles:#
\[\begin{split}\frac{\partial c_{\text{s,k}}}{\partial t} = -\frac{1}{r_{\text{k}}^2} \frac{\partial}{\partial r_{\text{k}}} \left(r_{\text{k}}^2 N_{\text{s,k}}\right), \\ N_{\text{s,k}} = -D_{\text{s,k}}(c_{\text{s,k}}) \frac{\partial c_{\text{s,k}}}{\partial r_{\text{k}}}, \quad \text{k} \in \text{n, p},\end{split}\]
\[\begin{split}N_{\text{s,k}}\big|_{r_{\text{k}}=0} = 0, \quad \text{k} \in \text{n, p}, \quad \ \ N_{\text{s,k}}\big|_{r_{\text{k}}=R_{\text{k}}} = \begin{cases} \frac{I}{Fa_{\text{n}}L_{\text{n}}}, \quad &\text{k}=\text{n}, \\ -\frac{I}{Fa_{\text{p}}L_{\text{p}}}, \quad &\text{k}=\text{p}, \end{cases} \\\end{split}\]

where \(D_{\text{s,k}}\) is the diffusion coefficient in the solid, \(N_{\text{s,k}}\) denotes the flux of lithium ions in the solid particle within the region \(\text{k}\), and \(r_{\text{k}} \in[0,R_{\text{k}}]\) is the radial coordinate of the particle in electrode \(\text{k}\). All other relevant parameters are given in the table at the end of this notebook.

Electrolyte:#
\[\begin{split}\epsilon_{\text{k}} \frac{\partial c_{\text{e,k}}}{\partial t} = -\frac{\partial N_{\text{e,k}}}{\partial x} + \begin{cases} \frac{I}{FL_{\text{n}}}, \quad &\text{k}=\text{n}, \\ 0, \quad \\\text{k}=\text{s}, \\ -\frac{I}{FL_{\text{p}}}, \quad &\text{k}=\text{p}, \end{cases} \\ N_{\text{e,k}} = -\epsilon_{\text{k}}^{\text{b}} D_{\text{e}} \frac{\partial c_{\text{e,k}}}{\partial x} + \begin{cases} \frac{x t^+IRT}{F L_{\text{n}}}, \quad &\text{k}=\text{n} \\ \frac{t^+IRT}{F}, \quad &\text{k}=\text{s} \\ \frac{(1-x)t^+ IRT}{F L_{\text{p}}}, \quad &\text{k}=\text{p} \end{cases}\end{split}\]
\[\begin{split}N_{\text{e,n}}\big|_{x=0} = 0, \quad N_{\text{e,p}}\big|_{x=L}=0, \\ c_{\text{e,k}}(x,0) = 0, \quad \text{k} \in \text{n, s, p},\end{split}\]

where \(D_{\text{e}}\) is the diffusion coefficient in the solid, \(N_{\text{e,k}}\) denotes the flux of lithium ions in the electrolyte within the region \(\text{k}\), and \(x\in[0,L]\) is the macroscopic through-cell distance. This equation is also solved subject to continuity of concentration and flux at the electrode/separator interfaces.

Voltage Expression#

The voltage is obtained from the expression:

\[V = U_{\text{eq}} + \eta_r + \eta_c + \Delta\Phi_{\text{Elec}} + \Delta\Phi_{\text{Solid}}\]

where

\[\begin{split}U_{\text{eq}} = U_{\text{p}}(c_{\text{p}})\big|_{r_{\text{p}}=1} - U_{\text{n}}(c_{\text{n}})\big|_{r_{\text{n}}=1}, \\ \eta_{r} = -2\sinh^{-1}\left(\frac{I}{\bar{j}_{\text{0,p}} L_{\text{p}}}\right) - 2\sinh^{-1}\left(\frac{I}{\bar{j}_{\text{0,n}} L_{\text{n}}}\right), \\ \eta_c = 2 (1-t^+)\frac{RT}{F}\left(\bar{c}_{\text{e,p}} - \bar{c}_{\text{e,n}}\right), \\ \bar{j}_{\text{0,n}} = \frac{1}{L_{\text{n}}}\int_0^{L_{\text{n}}} \frac{\gamma_{\text{n}}}{\mathcal{C}_{\text{r,n}}} (c_{\text{n}})^{1/2}(1-c_{\text{n}})^{1/2} (1+\mathcal{C}_{\text{e}} c_{\text{e,n}})^{1/2} \, \text{d}x,\\ \bar{j}_{\text{0,p}} = \frac{1}{L_{\text{p}}}\int_{1-L_{\text{p}}}^1 \frac{\gamma_{\text{p}}}{\mathcal{C}_{\text{r,p}}} (c_{\text{p}})^{1/2}(1-c_{\text{p}})^{1/2} (1+\mathcal{C}_{\text{e}} c_{\text{e,p}})^{1/2} \, \text{d}x,\\ \Delta \Phi_{\text{Elec}} = -\frac{\mathcal{C}_{\text{e}}I}{\gamma_{\text{e}} \kappa_{\text{e}}(1)} \left(\frac{L_{\text{n}}}{3\epsilon_{\text{n}}^{\text{b}}} + \frac{L_{\text{s}}}{\epsilon_{\text{s}}^{\text{b}}} + \frac{L_{\text{p}}}{3\epsilon_{\text{p}}^{\text{b}}} \right), \\ \Delta \Phi_{\text{Solid}} = -\frac{I}{3}\left(\frac{L_{\text{p}}}{\sigma_{\text{p}}} + \frac{L_{\text{n}}}{\sigma_{\text{n}}} \right),\end{split}\]

where

\[\bar{c}_{\text{e,n}} = \frac{1}{L_{\text{n}}}\int_0^{L_{\text{n}}} c_{\text{e,n}} \, \text{d}x, \quad \bar{c}_{\text{e,p}} = \frac{1}{L_{\text{p}}}\int_{L-L_{\text{p}}}^{L} c_{\text{e,p}} \, \text{d}x.\]

More details can be found in [3].

Example solving SPMe using PyBaMM#

Below we show how to solve the SPMe model using the Simulation class with all the default settings. For a more detailed example, see the notebook on the SPM.

First we need to import pybamm

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

We then load the SPMe model and create a simulation

[2]:
# load model
model = pybamm.lithium_ion.SPMe()

# create simulation
simulation = pybamm.Simulation(model)

The simulation is now ready to be solved

[3]:
# solve simulation
simulation.solve([0, 3600])  # time interval in seconds
[3]:
<pybamm.solvers.solution.Solution at 0x7f2774bea390>

To get a quick overview of the model outputs we can use the built-in plot method of the simulation

[4]:
simulation.plot()

References#

The relevant papers for this notebook are:

[5]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Using crack submodels in PyBaMM#

In this notebook we show how to use the crack submodel with battery DFN or SPM models. To see all of the models and submodels available in PyBaMM, please take a look at the documentation here.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")

Then we load the DFN, SPMe or SPM, by choosing one and commenting the others.

When you load a model in PyBaMM it builds by default. Building the model sets all of the model variables and sets up any variables which are coupled between different submodels: this is the process which couples the submodels together and allows one submodel to access variables from another.

[2]:
model = pybamm.lithium_ion.DFN(
    options={
        "particle": "Fickian diffusion",
        "particle mechanics": "swelling and cracking",  # other options are "none", "swelling only"
    }
)

Load the parameter set Ai2020 which contains mechanical parameters. Other sets may not contain mechanical parameters should add them manually.

[3]:
param = pybamm.ParameterValues("Ai2020")
## It can update the speed of crack propagation using the commands below:
# param.update({"Negative electrode Cracking rate":3.9e-20*10}, check_already_exists=False)

We can get the default parameters for the model and update them with the parameters required by the cracking model. Eventually, we would like these to be added to their own chemistry (you might need to adjust the path to the parameters file to your system). Now the model can be processed and solved in the usual way, and we still have access to model defaults such as the default geometry and default spatial methods

Depending on the parameter set being used, the particle cracking model can require a large number of mesh points inside the particles to be numerically stable.

[4]:
var_pts = {
    "x_n": 20,  # negative electrode
    "x_s": 20,  # separator
    "x_p": 20,  # positive electrode
    "r_n": 26,  # negative particle
    "r_p": 26,  # positive particle
}
[5]:
sim = pybamm.Simulation(
    model,
    parameter_values=param,
    solver=pybamm.CasadiSolver(dt_max=600),
    var_pts=var_pts,
)
solution = sim.solve(t_eval=[0, 3600], inputs={"C-rate": 1})
# plot
quick_plot = pybamm.QuickPlot(solution)
quick_plot.dynamic_plot()

Plot the results as required.

[6]:
# extract voltage
E_n = param["Negative electrode Young's modulus [Pa]"]
stress_t_n_surf = solution["Negative particle surface tangential stress [Pa]"]
x = solution["x [m]"].entries[0:19, 0]
c_s_n = solution["Negative particle concentration"]
r_n = solution["r_n [m]"].entries[:, 0, 0]

# plot


def plot_concentrations(t):
    f, (ax1, ax2, ax3, ax4) = plt.subplots(1, 4, figsize=(20, 4))
    ax1.plot(x, stress_t_n_surf(t=t, x=x) / E_n)
    ax1.set_xlabel(r"$x_n$ [m]")
    ax1.set_ylabel("$\sigma_t/E_n$")

    (plot_c_n,) = ax2.plot(
        r_n, c_s_n(r=r_n, t=t, x=x[0])
    )  # can evaluate at arbitrary x (single representative particle)
    ax2.set_ylabel("Negative particle concentration")
    ax2.set_xlabel(r"$r_n$ [m]")
    ax2.set_ylim(0, 1)
    ax2.set_title("Close to current collector")
    ax2.grid()

    (plot_c_n,) = ax3.plot(
        r_n, c_s_n(r=r_n, t=t, x=x[10])
    )  # can evaluate at arbitrary x (single representative particle)
    ax3.set_ylabel("Negative particle concentration")
    ax3.set_xlabel(r"$r_n$ [m]")
    ax3.set_ylim(0, 1)
    ax3.set_title("In the middle")
    ax3.grid()

    (plot_c_n,) = ax4.plot(
        r_n, c_s_n(r=r_n, t=t, x=x[-1])
    )  # can evaluate at arbitrary x (single representative particle)
    ax4.set_ylabel("Negative particle concentration")
    ax4.set_xlabel(r"$r_n$ [m]")
    ax4.set_ylim(0, 1)
    ax4.set_title("Close to separator")
    ax4.grid()
    plt.show()


import ipywidgets as widgets

widgets.interact(
    plot_concentrations, t=widgets.FloatSlider(min=0, max=3600, step=10, value=0)
);

Plot results using the default functions

[7]:
label = ["Crack model"]
output_variables = [
    "Negative particle crack length [m]",
    "Positive particle crack length [m]",
    "X-averaged negative particle crack length [m]",
    "X-averaged positive particle crack length [m]",
]
quick_plot = pybamm.QuickPlot(
    solution, output_variables, label, variable_limits="tight"
)
quick_plot.dynamic_plot();

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Loss of active material submodels#

In this notebook we show how to use the loss of active materials (LAM) submodels in PyBaMM.

Stress-driven LAM#

The first model we consider is the stress-driven submodel, which follows equation (25) from Reniers et al (2019), and the stresses are calculated by equations (7)-(9) in Ai et al (2020). To see all of the models and submodels available in PyBaMM, please take a look at the documentation.

As usual, we start by defining the model. We choose a DFN model with stress-driven loss of active material, and we also include SEI growth. We then define the parameters and experiments, and solve the simulation.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

model = pybamm.lithium_ion.DFN(
    options={
        "SEI": "solvent-diffusion limited",
        "SEI porosity change": "false",
        "particle mechanics": "swelling only",
        "loss of active material": "stress-driven",
    }
)
param = pybamm.ParameterValues("Ai2020")
param.update({"Negative electrode LAM constant proportional term [s-1]": 1e-4 / 3600})
param.update({"Positive electrode LAM constant proportional term [s-1]": 1e-4 / 3600})
total_cycles = 2
experiment = pybamm.Experiment(
    [
        "Discharge at 1C until 3 V",
        "Rest for 600 seconds",
        "Charge at 1C until 4.2 V",
        "Hold at 4.199 V for 600 seconds",
    ]
    * total_cycles
)
sim = pybamm.Simulation(
    model,
    experiment=experiment,
    parameter_values=param,
    solver=pybamm.CasadiSolver("fast with events"),
)
solution = sim.solve(calc_esoh=False)
WARNING: pybamm 23.5 does not provide the extra 'cite'
WARNING: pybamm 23.5 does not provide the extra 'plot'

[notice] A new release of pip is available: 23.0.1 -> 23.2.1
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

We can now plot the results as usual.

[2]:
sim.plot(
    [
        "Voltage [V]",
        "Current [A]",
        "Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]",
        "Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]",
        "X-averaged positive electrode active material volume fraction",
        "X-averaged negative electrode active material volume fraction",
        "X-averaged positive particle surface tangential stress [Pa]",
        "X-averaged negative particle surface tangential stress [Pa]",
    ]
)
[2]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f91cc5d2d00>

To understand the effect of the LAM constant proportional term, let’s perform a parameter sweep.

[3]:
ks = [1e-4, 1e-3, 1e-2]
solutions = []

for k in ks:
    param.update({"Positive electrode LAM constant proportional term [s-1]": k / 3600})
    param.update({"Negative electrode LAM constant proportional term [s-1]": k / 3600})

    sim = pybamm.Simulation(
        model,
        experiment=experiment,
        parameter_values=param,
        solver=pybamm.CasadiSolver("fast with events"),
    )
    solution = sim.solve(calc_esoh=False)
    solutions.append(solution)

pybamm.dynamic_plot(
    solutions,
    output_variables=[
        "Voltage [V]",
        "Current [A]",
        "Sum of x-averaged positive electrode volumetric interfacial current densities [A.m-3]",
        "Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]",
        "X-averaged positive electrode active material volume fraction",
        "X-averaged negative electrode active material volume fraction",
        "X-averaged positive electrode surface area to volume ratio [m-1]",
        "X-averaged negative electrode surface area to volume ratio [m-1]",
    ],
    labels=[f"k={k:.0e}" for k in ks],
)
[3]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f9179084910>

Reaction-driven LAM#

Another option is to use reaction-driven (i.e. SEI) LAM. In this case we need to choose the "reaction-driven" option in the model, and proceed along the lines of the previous example.

[4]:
model = pybamm.lithium_ion.DFN(
    options={
        "SEI": "solvent-diffusion limited",
        "loss of active material": "reaction-driven",
    }
)
param = pybamm.ParameterValues("Chen2020")
param.update(
    {
        "Negative electrode reaction-driven LAM factor [m3.mol-1]": 1e-3,
    }
)
total_cycles = 2
experiment = pybamm.Experiment(
    [
        "Discharge at 1C until 3 V",
        "Rest for 600 seconds",
        "Charge at 1C until 4.2 V",
        "Hold at 4.199 V for 600 seconds",
    ]
    * total_cycles
)
sim = pybamm.Simulation(
    model,
    experiment=experiment,
    parameter_values=param,
    solver=pybamm.CasadiSolver("fast with events"),
)
solution = sim.solve(calc_esoh=False)

sim.plot(
    [
        "Voltage [V]",
        "Current [A]",
        "Sum of x-averaged negative electrode volumetric interfacial current densities [A.m-3]",
        "X-averaged negative electrode active material volume fraction",
        "Negative total SEI thickness [m]",
        "X-averaged negative total SEI thickness [m]",
    ]
)
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f91cc77d9a0>

Both stress-driven and reaction-driven can be combined by calling the "stress and reaction-driven" option.

Current-driven LAM#

The final submodel is current-driven LAM, which follows equation (26) from Reniers et al (2019). In this case we need to define the RHS of the equation as a function of current density and temperature. The example here is illustrative and does not represent any real scenario.

[5]:
def current_LAM(i, T):
    return -1e-10 * (abs(i) + 1e3 * abs(i) ** 0.5)


model = pybamm.lithium_ion.DFN(
    options={
        "loss of active material": "current-driven",
    }
)
param = pybamm.ParameterValues("Chen2020")
param.update(
    {
        "Positive electrode current-driven LAM rate": current_LAM,
        "Negative electrode current-driven LAM rate": current_LAM,
    },
    check_already_exists=False,
)
total_cycles = 2
experiment = pybamm.Experiment(
    [
        "Discharge at 1C until 3 V",
        "Rest for 600 seconds",
        "Charge at 1C until 4.2 V",
        "Hold at 4.199 V for 600 seconds",
    ]
    * total_cycles
)
sim = pybamm.Simulation(
    model,
    experiment=experiment,
    parameter_values=param,
    solver=pybamm.CasadiSolver("fast with events"),
)
solution = sim.solve(calc_esoh=False)

sim.plot(
    [
        "Voltage [V]",
        "Current [A]",
        "X-averaged positive electrode active material volume fraction",
        "X-averaged negative electrode active material volume fraction",
    ]
)
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f9170283dc0>

References#

The relevant papers for this notebook are:

[6]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] Scott G. Marquis. Long-term degradation of lithium-ion batteries. PhD thesis, University of Oxford, 2020.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Thermal models#

There are a number of thermal submodels available in PyBaMM. In this notebook we give details of each of the models, and highlight any relevant parameters. At present PyBaMM includes an isothermal and a lumped thermal model, both of which can be used with any cell geometry, as well as a 1D thermal model which accounts for the through-cell variation in temperature in a pouch cell, and “1+1D” and “2+1D” pouch cell models which assumed the temperature is uniform through the thickness of the pouch, but accounts for variations in temperature in the remaining dimensions. Here we give the governing equations for each model (except the isothermal model, which just sets the temperature to be equal to to the parameter “Ambient temperature [K]”).

A more comprehensive review of the pouch cell models, including how to properly compute the effective cooling terms, can be found in references [4] and [6] at the end of this notebook.

[4]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

[notice] A new release of pip is available: 23.3.1 -> 23.3.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

Lumped model#

The lumped thermal model solves the following ordinary differential equation for the average temperature, given here in dimensional terms,

\[\rho_{eff} \frac{\partial T}{\partial t} = \bar{Q} - \frac{hA}{V}(T-T_{\infty}),\]

where \(\rho_{eff}\) is effective volumetric heat capacity, \(T\) is the temperature, \(t\) is time, \(\bar{Q}\) is the averaged heat source term, \(h\) is the heat transfer coefficient, \(A\) is the surface area (available for cooling), \(V\) is the cell volume, and \(T_{\infty}\) is the ambient temperature. An initial temperature \(T_0\) must be prescribed.

The effective volumetric heat capacity is computed as

\[\rho_{eff} = \frac{\sum_k \rho_k c_{p,k} L_k}{\sum_k L_k},\]

where \(\rho_k\) is the density, \(c_{p,k}\) is the specific heat, and \(L_k\) is the thickness of each component. The subscript \(k \in \{cn, n, s, p, cp\}\) is used to refer to the components negative current collector, negative electrode, separator, positive electrode, and positive current collector.

The heat source term accounts for Ohmic heating \(Q_{Ohm,k}\) due to resistance in the solid and electrolyte, irreverisble heating due to electrochemical reactions \(Q_{rxn,k}\), and reversible heating due to entropic changes in the the electrode \(Q_{rev,k}\):

\[Q = Q_{Ohm,k}+Q_{rxn,k}+Q_{rev,k},\]

with

\[Q_{Ohm,k} = -i_k \nabla \phi_k, \quad Q_{rxn,k} = a_k j_k \eta_k, \quad Q_{rev,k} = a_k j_k T_k \frac{\partial U}{\partial T} \bigg|_{T=T_{\infty}}.\]

Here \(i_k\) is the current, \(\phi_k\) the potential, \(a_k\) the surface area to volume ratio, \(j_k\) the interfacial current density, \(\eta_k\) the overpotential, and \(U\) the open-circuit potential. The averaged heat source term \(\bar{Q}\) is computed by taking the volume-average of \(Q\).

When using the option {"cell geometry": "arbitrary"} the relevant parameters to specify the cooling conditions are:

“Total heat transfer coefficient [W.m-2.K-1]”
“Cell cooling surface area [m2]”
“Cell volume [m3]”

which correspond directly to the parameters \(h\), \(A\) and \(V\) in the governing equation.

When using the option {"cell geometry": "pouch"} the parameter \(A\) and \(V\) are computed automatically from the pouch dimensions, assuming a single-layer pouch cell, i.e. \(A\) is the total surface area of a single-layer pouch cell and \(V\) is the volume. The parameter \(h\) is still set by the “Total heat transfer coefficient [W.m-2.K-1]” parameter.

The lumped thermal option can be selected as follows

[5]:
options = {"cell geometry": "arbitrary", "thermal": "lumped"}
arbitrary_lumped_model = pybamm.lithium_ion.DFN(options)
# OR
options = {"cell geometry": "pouch", "thermal": "lumped"}
pouch_lumped_model = pybamm.lithium_ion.DFN(options)

If no cell geometry is specified, the “arbitrary” cell geometry is used by default

[6]:
options = {"thermal": "lumped"}
model = pybamm.lithium_ion.DFN(options)
print("Cell geometry:", model.options["cell geometry"])
Cell geometry: arbitrary

Pouch cell models#

1D (through-cell) model#

The 1D model solves for \(T(x,t)\), capturing variations through the thickness of the cell, but ignoring variations in the other dimensions. The temperature is found as the solution of a partial differential equation, given here in dimensional terms

\[\rho_k c_{p,k} \frac{\partial T}{\partial t} = \lambda_k \nabla^2 T + Q(x,t) - Q_{cool}(x,t)\]

with boundary conditions

\[-\lambda_{cn} \frac{\partial T}{\partial x}\bigg|_{x=0} = h_{cn}(T_{\infty} - T) \quad -\lambda_{cp} \frac{\partial T}{\partial x}\bigg|_{x=1} = h_{cp}(T-T_{\infty}),\]

and initial condition

\[T\big|_{t=0} = T_0.\]

Here \(\lambda_k\) is the thermal conductivity of component \(k\), and the heat transfer coefficients \(h_{cn}\) and \(h_{cp}\) correspond to heat transfer at the large surface of the pouch on the side of the negative current collector, heat transfer at the large surface of the pouch on the side of the positive current collector, respectively. The heat source term \(Q\) is as described in the section on lumped models. The term \(Q_cool\) accounts for additional heat losses due to heat transfer at the sides of the pouch, as well as the tabs. This term is computed automatically by PyBaMM based on the cell geometry and heat transfer coefficients on the edges and tabs of the cell.

The relevant heat transfer parameters are: “Negative current collector surface heat transfer coefficient [W.m-2.K-1]” “Positive current collector surface heat transfer coefficient [W.m-2.K-1]” “Negative tab heat transfer coefficient [W.m-2.K-1]” “Positive tab heat transfer coefficient [W.m-2.K-1]” “Edge heat transfer coefficient [W.m-2.K-1]”

The 1D model is termed “x-full” (since it fully accounts for variation in the x direction) and can be selected as follows

[7]:
options = {"thermal": "x-full"}
model = pybamm.lithium_ion.DFN(options)

Higher dimensional pouch cell models#

These pouch cell thermal models ignore any variation in temperature through the thickness of the cell (x direction), and solve for \(T(y,z,t)\). It is therefore referred to as an “x-lumped” model. The temperature is found as the solution of a partial differential equation, given here in dimensional terms,

\[\rho_{eff} \frac{\partial T}{\partial t} = \lambda_{eff} \nabla_\perp^2T + \bar{Q} - \frac{(h_{cn}+h_{cp})A}{V}(T-T_{\infty}),\]

along with boundary conditions

\[-\lambda_{eff} \nabla_\perp T \cdot \boldsymbol{n} = \frac{L_{cn}h_{cn} + (L_n+L_s+L_p+L_{cp})h_{edge}}{L_{cn}+L_n+L_s+L_p+L_{cp}}(T-T_\infty),\]

at the negative tab,

\[-\lambda_{eff} \nabla_\perp T \cdot \boldsymbol{n} = \frac{(L_{cn}+L_n+L_s+L_p)h_{edge}+L_{cp}h_{cp}}{L_{cn}+L_n+L_s+L_p+L_{cp}}(T-T_\infty),\]

at the positive tab, and

\[-\lambda_{eff} \nabla_\perp T \cdot \boldsymbol{n} = h_{edge}(T-T_\infty),\]

elsewhere. Again, an initial temperature \(T_0\) must be prescribed.

Here the heat source term is averaged in the x direction so that \(\bar{Q}=\bar{Q}(y,z)\). The parameter \(\lambda_{eff}\) is the effective thermal conductivity, computed as

\[\lambda_{eff} = \frac{\sum_k \lambda_k L_k}{\sum_k L_k}.\]

The heat transfer coefficients \(h_{cn}\), \(h_{cp}\) and \(h_{egde}\) correspond to heat transfer at the large surface of the pouch on the side of the negative current collector, heat transfer at the large surface of the pouch on the side of the positive current collector, and heat transfer at the remaining, respectively.

The relevant heat transfer parameters are: “Negative current collector surface heat transfer coefficient [W.m-2.K-1]” “Positive current collector surface heat transfer coefficient [W.m-2.K-1]” “Negative tab heat transfer coefficient [W.m-2.K-1]” “Positive tab heat transfer coefficient [W.m-2.K-1]” “Edge heat transfer coefficient [W.m-2.K-1]”

The “2+1D” model can be selected as follows

[8]:
options = {
    "current collector": "potential pair",
    "dimensionality": 2,
    "thermal": "x-lumped",
}
model = pybamm.lithium_ion.DFN(options)

Model usage#

Here we compare the “full” one-dimensional model with the lumped model for a pouch cell. We first set up our models, passing the relevant options, and then show how to adjust the parameters to so that the lumped and full models give the same behaviour

[9]:
full_thermal_model = pybamm.lithium_ion.SPMe(
    {"thermal": "x-full"}, name="full thermal model"
)
lumped_thermal_model = pybamm.lithium_ion.SPMe(
    {"thermal": "lumped"}, name="lumped thermal model"
)
models = [full_thermal_model, lumped_thermal_model]

We then pick our parameter set

[10]:
parameter_values = pybamm.ParameterValues("Marquis2019")

For the “full” model we use a heat transfer coefficient of \(5\, \text{Wm}^{-2}\text{K}^{-1}\) on the large surfaces of the pouch and zero heat transfer coefficient on the tabs and edges

[11]:
full_params = parameter_values.copy()
full_params.update(
    {
        "Negative current collector"
        + " surface heat transfer coefficient [W.m-2.K-1]": 5,
        "Positive current collector"
        + " surface heat transfer coefficient [W.m-2.K-1]": 5,
        "Negative tab heat transfer coefficient [W.m-2.K-1]": 0,
        "Positive tab heat transfer coefficient [W.m-2.K-1]": 0,
        "Edge heat transfer coefficient [W.m-2.K-1]": 0,
    }
)

For the lumped model we set the “Total heat transfer coefficient [W.m-2.K-1]” parameter as well as the “Cell cooling surface area [m2]” parameter. Since the “full” model only accounts for cooling from the large surfaces of the pouch, we set the “Surface area for cooling” parameter to the area of the large surfaces of the pouch, and the total heat transfer coefficient to \(5\, \text{Wm}^{-2}\text{K}^{-1}\)

[12]:
A = parameter_values["Electrode width [m]"] * parameter_values["Electrode height [m]"]
lumped_params = parameter_values.copy()
lumped_params.update(
    {
        "Total heat transfer coefficient [W.m-2.K-1]": 5,
        "Cell cooling surface area [m2]": 2 * A,
    }
)

Let’s run simulations with both options and compare the results. For demonstration purposes we’ll increase the current to amplify the thermal effects

[13]:
params = [full_params, lumped_params]
# loop over the models and solve
sols = []
for model, param in zip(models, params):
    param["Current function [A]"] = 3 * 0.68
    sim = pybamm.Simulation(model, parameter_values=param)
    sim.solve([0, 3600])
    sols.append(sim.solution)


# plot
output_variables = [
    "Voltage [V]",
    "X-averaged cell temperature [K]",
    "Cell temperature [K]",
]
pybamm.dynamic_plot(sols, output_variables)

# plot the results
pybamm.dynamic_plot(
    sols,
    [
        "Volume-averaged cell temperature [K]",
        "Volume-averaged total heating [W.m-3]",
        "Current [A]",
        "Voltage [V]",
    ],
)
[13]:
<pybamm.plotting.quick_plot.QuickPlot at 0x17f501e90>

References#

The relevant papers for this notebook are:

[14]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.
[6] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Solving the heat equation in PyBaMM#

In this notebook we create and solve a model for unsteady heat diffusion in 1D, with a spatially dependent heat source term. The notebook is adapted from example 4.1.2 on pg.16 of the online notes found here.

We consider the heat equation

\[T_{t} = kT_{xx} + Q(x), \quad 0 < x < L, \quad t > 0,\]

along with the boundary and initial conditions,

\[u(0, t)=0, \quad u(L, t)=0, \quad u(x, 0)=2x-x^2,\]

and heat source term

\[Q(x)=1-|x-1|.\]

As in the example, we solve the problem on the domain \(0 < x < 2\) (i.e. we take \(L=2\)). We extended the example to include a thermal diffusivity \(k\), which we take to be equal to 0.75.

Building the model#

As always, we start by importing PyBaMM, along with any other packages we require.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

We then load up an instance of the pybamm.BaseModel class.

[2]:
model = pybamm.BaseModel()

We now define the model variables and parameters. Note that we also need to define the spatial variable \(x\) here, so that we can write down the spatially dependent source term \(Q(x)\). Since we are solving in 1D we have decided to call the domain “rod”, but we could name it anything we like. Note that in PyBaMM variables and parameters can be given useful and meaningful names, such as “Temperature”, so that they can be easily referred to later.

[3]:
x = pybamm.SpatialVariable("x", domain="rod", coord_sys="cartesian")
T = pybamm.Variable("Temperature", domain="rod")
k = pybamm.Parameter("Thermal diffusivity")

Now that we have defined the variables, we can write down the model equations and add them to the model.rhs dictionary. This dictionary stores the right hand sides of any time-dependent differential equations (ordinary or partial). The key is the variable inside the time derivative (in this case \(T\)). To define the heat source term we use a pybamm.Function class. The first argument of the class is the function, and the second argument is the input.

[4]:
N = -k * pybamm.grad(T)  # Heat flux
Q = 1 - pybamm.Function(np.abs, x - 1)  # Source term
dTdt = -pybamm.div(N) + Q  # The right hand side of the PDE
model.rhs = {T: dTdt}  # Add to model

We now add the boundary conditions into the model.boundary_conditions dictionary. The keys of the dictionary indicate which end of the boundary the condition is applied to (in 1D this can be “left” or “right”), the entry is then give as a tuple of the value and type. In this example we have homogeneous Dirichlet boundary conditions at both ends.

[5]:
model.boundary_conditions = {
    T: {
        "left": (pybamm.Scalar(0), "Dirichlet"),
        "right": (pybamm.Scalar(0), "Dirichlet"),
    }
}

We also need to add the initial conditions to the model.initial_conditions dictionary.

[6]:
model.initial_conditions = {T: 2 * x - x**2}

Finally, we add any output variables to the model.variables dictionary. These variables can be easily accessed after the model has been solved. You can add any variables of interest to this dictionary. Here we have added the temperature, heat flux and heat source.

[7]:
model.variables = {"Temperature": T, "Heat flux": N, "Heat source": Q}

Using the model#

Now that the model has been constructed we can go ahead and define our geometry and parameter values. We start by defining the geometry for our “rod” domain. We need to define the spatial direction(s) in which spatial operators act (such as gradients). In this case it is simply \(x\). We then set the minimum and maximum values \(x\) can take. In this example we are solving the problem on the domain \(0<x<2\).

[8]:
geometry = {"rod": {x: {"min": pybamm.Scalar(0), "max": pybamm.Scalar(2)}}}

We also need to provide the values of any parameters using the pybamm.ParameterValues class. This class accepts a dictionary of parameter names and values. Note that the name we provide is the string name of the parameters and not its symbol.

[9]:
param = pybamm.ParameterValues({"Thermal diffusivity": 0.75})

Now that we have defined the geometry and provided the parameters values, we can process the model.

[10]:
param.process_model(model)
param.process_geometry(geometry)

Before we disctretise the spatial operators, we must choose and mesh and a spatial method. Here we choose to use a uniformly spaced 1D mesh with 30 points, and discretise the equations in space using the finite volume method. The information about the mesh is stored in a pybamm.Mesh object, wheres the spatial methods are stored in a dictionary which maps domain names to a spatial method. This allows the user to discretise different (sub)domains in a problem using different spatial methods. All of this information goes into a pybamm.Discretisation object, which accepts a mesh and a dictionary of spatial methods.

[11]:
submesh_types = {"rod": pybamm.Uniform1DSubMesh}
var_pts = {x: 30}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)
spatial_methods = {"rod": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)

The model is then processed using the discretisation, turning the spaital operators into matrix-vector multiplications.

[12]:
disc.process_model(model)
[12]:
<pybamm.models.base_model.BaseModel at 0x7f39430e6ad0>

Now that the model has been discretised we are ready to solve. We must first choose a solver to use. For this model we choose the Scipy ODE solver, but other solvers are available in PyBaMM (see here). To solve the model, we use the method solver.solve which takes in a model and an array of times at which we would like the solution to be returned. Ths solution is then stored in the solution object. The times and states can be accessed with solver.t and solver.y.

[13]:
solver = pybamm.ScipySolver()
t = np.linspace(0, 1, 100)
solution = solver.solve(model, t)

After solving, we can process variables using the class pybamm.ProcessedVariable. This returns a callable object which can be evaluated at any time and position by means of interpolating the solution. Processed variables provide a convinient way of comparing the solution to solutions from different models, or to exact solutions. Since all of the variables are names with informative strings, the user doesn’t need to keep track of where they are stored in the state vector solution.y. This is particularly useful in complex models with lots of variables, and is automatically handled by the solution dictionary.

[14]:
T_out = solution["Temperature"]
2021-01-24 20:03:41,111 - [WARNING] processed_variable.get_spatial_scale(518): No length scale set for rod. Using default of 1 [m].

Comparison with the exact solution#

This example admits the exact solution

\[T(x,t) = \sum_{n=1}^{\infty} \left(\frac{4}{kn^2\pi^2}q_n + \left( c_n - \frac{4}{kn^2\pi^2}q_n\right) \exp^{-k\left(\frac{n\pi}{2}\right)^2t} \right) \sin\left( \frac{n\pi x}{2}\right),\]

with

\[c_n = \frac{16}{n^3\pi^3}\left(1 - \cos(n \pi)\right), \quad \text{and} \quad q_n = \frac{8}{n^2\pi^2} \sin\left(\frac{n\pi}{2}\right).\]

We construct the exact solution by summing over some large number \(N\) of terms in the Fourier series.

[15]:
N = 100  # number of Fourier modes to sum
k_val = param[
    "Thermal diffusivity"
]  # extract value of diffusivity from the parameters dictionary


# Fourier coefficients
def q(n):
    return (8 / (n**2 * np.pi**2)) * np.sin(n * np.pi / 2)


def c(n):
    return (16 / (n**3 * np.pi**3)) * (1 - np.cos(n * np.pi))


def b(n):
    return c(n) - 4 * q(n) / (k_val * n**2 * np.pi**2)


def T_n(t, n):
    return (4 * q(n) / (k_val * n**2 * np.pi**2)) + b(n) * np.exp(
        -k_val * (n * np.pi / 2) ** 2 * t
    )


# Sum series to get the temperature
def T_exact(x, t):
    out = 0
    for n in np.arange(1, N):
        out += T_n(t, n) * np.sin(n * np.pi * x / 2)
    return out

Finally, we plot the numerical and exact solutions at a series of different times. The plot demonstrates an excellent agreement between the numerical solution provided by PyBaMM (dots) and the exact solution (solid lines). Note that in the finite volume method the variable is evaluated at the cell centres.

[16]:
x_nodes = mesh["rod"].nodes  # numerical gridpoints
xx = np.linspace(0, 2, 101)  # fine mesh to plot exact solution
plot_times = np.linspace(0, 1, 5)  # times at which to plot

plt.figure(figsize=(15, 8))
cmap = plt.get_cmap("inferno")
for i, t in enumerate(plot_times):
    color = cmap(float(i) / len(plot_times))
    plt.plot(
        x_nodes,
        T_out(t, x=x_nodes),
        "o",
        color=color,
        label="Numerical" if i == 0 else "",
    )
    plt.plot(
        xx,
        T_exact(xx, t),
        "-",
        color=color,
        label=f"Exact (t={plot_times[i]})",
    )
plt.xlabel("x", fontsize=16)
plt.ylabel("T", fontsize=16)
plt.legend()
plt.show()
_images/source_examples_notebooks_models_unsteady-heat-equation_34_0.png

References#

The relevant papers for this notebook are:

[17]:
pybamm.print_citations()
[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.
[2] 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.
[3] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Using model options in PyBaMM#

In this notebook we show how to pass options to models. This allows users to do things such as include extra physics (e.g. thermal effects) or change the macroscopic dimension of the problem (e.g. change from a 1D model to a 2+1D pouch cell model). To see all of the options currently available in PyBaMM, please take a look at the documentation here. For more information on combining submodels explicitly to create your own custom model, please see the Using Submodels notebook.

Example: Solving the SPMe with a lumped thermal model#

PyBaMM is designed to be a flexible modelling package that allows users to easily include different physics within a model without having to start from scratch. In this example, we show how to pass model options to include thermal effects in the SPMe (for more information on the SPMe see here). First we import PyBaMM and any other packages we need

[ ]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

We then choose out model options, which a set as a dictionary. We choose to model the behaviour in the particle by assuming the concentration profile is quadratic within the particle. We also choose a lumped thermal model (note that this is fully-coupled, i.e. parameters can depend on temperature). For an in-depth look at the thermal models see the thermal models notebook

[ ]:
options = {"particle": "quadratic profile", "thermal": "lumped"}

We then pass our options to the model

[3]:
model = pybamm.lithium_ion.SPMe(options)

We choose to use the parameters from Chen2020.

[4]:
param = pybamm.ParameterValues("Chen2020")

We then create and solve a simulation, making sure we pass in our updated parameter values

[5]:
simulation = pybamm.Simulation(model, parameter_values=param)
simulation.solve([0, 3600])
[5]:
<pybamm.solvers.solution.Solution at 0x7f6169de4690>

Finally we plot the voltage and the cell temperature

[6]:
simulation.plot(
    [
        "Voltage [V]",
        "X-averaged cell temperature [K]",
    ]
)

Note that the variable “X-averaged cell temperature [K]” is the scalar-valued lumped temperature, whereas the variable “Cell temperature [K]” is the value of the lumped temperature broadcasted across the whole cell domain. This type of behaviour is purposefully designed to allow easy comparison of different models and settings. For instance we may wish to compare a simulation that uses a lumped thermal model with a simulation that uses a full thermal model (i.e. one that solves the heat equation in the x-direction). When comparing these two model we could then plot the same variable “Cell temperature [K]” to compare the temperature throughout the cell.

References#

The relevant papers for this notebook are:

[7]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Venkat R. Subramanian, Vinten D. Diwakar, and Deepak Tapriyal. Efficient macro-micro scale coupled modeling of batteries. Journal of The Electrochemical Society, 152(10):A2002, 2005. doi:10.1149/1.2032427.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Using submodels in PyBaMM#

In this notebook we show how to modify existing models by swapping out submodels, and how to build your own model from scratch using existing submodels. To see all of the models and submodels available in PyBaMM, please take a look at the documentation here.

Changing a submodel in an existing battery model#

PyBaMM is designed to be a flexible modelling package that allows users to easily compare different models and numerical techniques within a common framework. Battery models within PyBaMM are built up using a number of submodels that describe different physics included within the model, such as mass conservation in the electrolyte or charge conservation in the solid. For ease of use, a number of popular battery models are pre-configured in PyBaMM. As an example, we look at the Single Particle Model (for more information see here).

First we import pybamm

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Then we load the SPM

[2]:
model = pybamm.lithium_ion.SPM()

We can look at the submodels that make up the SPM by accessing model.submodels, which is a dictionary of the submodel’s name (i.e. the physics it represents) and the submodel that is selected

[3]:
for name, submodel in model.submodels.items():
    print(name, submodel)
external circuit <pybamm.models.submodels.external_circuit.explicit_control_external_circuit.ExplicitCurrentControl object at 0x7f9cd479ea00>
porosity <pybamm.models.submodels.porosity.constant_porosity.Constant object at 0x7f9cd479ecd0>
Negative interface utilisation <pybamm.models.submodels.interface.interface_utilisation.full_utilisation.Full object at 0x7f9cd479ee80>
Positive interface utilisation <pybamm.models.submodels.interface.interface_utilisation.full_utilisation.Full object at 0x7f9cd479eee0>
negative particle mechanics <pybamm.models.submodels.particle_mechanics.no_mechanics.NoMechanics object at 0x7f9cd479ef40>
positive particle mechanics <pybamm.models.submodels.particle_mechanics.no_mechanics.NoMechanics object at 0x7f9cd479efd0>
negative primary active material <pybamm.models.submodels.active_material.constant_active_material.Constant object at 0x7f9cd47a60a0>
positive primary active material <pybamm.models.submodels.active_material.constant_active_material.Constant object at 0x7f9cd47a6160>
electrolyte transport efficiency <pybamm.models.submodels.transport_efficiency.bruggeman_transport_efficiency.Bruggeman object at 0x7f9cd479ed30>
electrode transport efficiency <pybamm.models.submodels.transport_efficiency.bruggeman_transport_efficiency.Bruggeman object at 0x7f9cd47a61f0>
transverse convection <pybamm.models.submodels.convection.transverse.no_convection.NoConvection object at 0x7f9cd47a6220>
through-cell convection <pybamm.models.submodels.convection.through_cell.no_convection.NoConvection object at 0x7f9cd47a62b0>
negative primary open-circuit potential <pybamm.models.submodels.interface.open_circuit_potential.single_ocp.SingleOpenCircuitPotential object at 0x7f9cd479ec40>
positive primary open-circuit potential <pybamm.models.submodels.interface.open_circuit_potential.single_ocp.SingleOpenCircuitPotential object at 0x7f9cd479ed60>
negative interface <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.InverseButlerVolmer object at 0x7f9cd479ea90>
negative interface current <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.CurrentForInverseButlerVolmer object at 0x7f9cd479e880>
positive interface <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.InverseButlerVolmer object at 0x7f9cd479e610>
positive interface current <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.CurrentForInverseButlerVolmer object at 0x7f9cd479e6d0>
negative primary particle <pybamm.models.submodels.particle.fickian_diffusion.FickianDiffusion object at 0x7f9cd479e3a0>
negative primary total particle concentration <pybamm.models.submodels.particle.total_particle_concentration.TotalConcentration object at 0x7f9cd479e490>
positive primary particle <pybamm.models.submodels.particle.fickian_diffusion.FickianDiffusion object at 0x7f9cd479e460>
positive primary total particle concentration <pybamm.models.submodels.particle.total_particle_concentration.TotalConcentration object at 0x7f9cd479e2e0>
negative electrode potential <pybamm.models.submodels.electrode.ohm.leading_ohm.LeadingOrder object at 0x7f9cd479e310>
positive electrode potential <pybamm.models.submodels.electrode.ohm.leading_ohm.LeadingOrder object at 0x7f9cd49119a0>
electrolyte diffusion <pybamm.models.submodels.electrolyte_diffusion.constant_concentration.ConstantConcentration object at 0x7f9cd4816d30>
leading-order electrolyte conductivity <pybamm.models.submodels.electrolyte_conductivity.leading_order_conductivity.LeadingOrder object at 0x7f9cd4911fd0>
negative surface potential difference <pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.explicit_surface_form_conductivity.Explicit object at 0x7f9cd4911e50>
positive surface potential difference <pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.explicit_surface_form_conductivity.Explicit object at 0x7f9cd4911cd0>
thermal <pybamm.models.submodels.thermal.isothermal.Isothermal object at 0x7f9cd4911bb0>
current collector <pybamm.models.submodels.current_collector.homogeneous_current_collector.Uniform object at 0x7f9cd4911ac0>
negative primary sei <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd4907220>
positive primary sei <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd49078b0>
negative primary sei on cracks <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd4819880>
positive primary sei on cracks <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd4819610>
negative lithium plating <pybamm.models.submodels.interface.lithium_plating.no_plating.NoPlating object at 0x7f9cd48e41c0>
positive lithium plating <pybamm.models.submodels.interface.lithium_plating.no_plating.NoPlating object at 0x7f9cd48f66a0>
total interface <pybamm.models.submodels.interface.total_interfacial_current.TotalInterfacialCurrent object at 0x7f9cd4911700>

When you load a model in PyBaMM it builds by default. Building the model sets all of the model variables and sets up any variables which are coupled between different submodels: this is the process which couples the submodels together and allows one submodel to access variables from another. If you would like to swap out a submodel in an existing battery model you need to load it without building it by passing the keyword build=False

[4]:
model = pybamm.lithium_ion.SPM(build=False)

This collects all of the submodels which make up the SPM, but doesn’t build the model. Now you are free to swap out one submodel for another. For instance, you may want to assume that diffusion within the negative particles is infinitely fast, so that the PDE describing diffusion is replaced with an ODE for the uniform particle concentration. To change a submodel you simply update the dictionary entry, in this case to the XAveragedPolynomialProfile submodel

[5]:
model.submodels[
    "negative primary particle"
] = pybamm.particle.XAveragedPolynomialProfile(
    model.param, "negative", options={**model.options, "particle": "uniform profile"}
)

where we pass in the model parameters, the electrode (negative or positive) the submodel corresponds to, and the name of the polynomial we want to use. In the example we assume uniform concentration within the particle, corresponding to a zero-order polynomial.

Now if we look at the submodels again we see that the model for the negative particle has been changed

[6]:
for name, submodel in model.submodels.items():
    print(name, submodel)
external circuit <pybamm.models.submodels.external_circuit.explicit_control_external_circuit.ExplicitCurrentControl object at 0x7f9cd48d94c0>
porosity <pybamm.models.submodels.porosity.constant_porosity.Constant object at 0x7f9cd48cde80>
Negative interface utilisation <pybamm.models.submodels.interface.interface_utilisation.full_utilisation.Full object at 0x7f9cd48cd970>
Positive interface utilisation <pybamm.models.submodels.interface.interface_utilisation.full_utilisation.Full object at 0x7f9cd48cd850>
negative particle mechanics <pybamm.models.submodels.particle_mechanics.no_mechanics.NoMechanics object at 0x7f9cd48cd790>
positive particle mechanics <pybamm.models.submodels.particle_mechanics.no_mechanics.NoMechanics object at 0x7f9cd48cd520>
negative primary active material <pybamm.models.submodels.active_material.constant_active_material.Constant object at 0x7f9cd48cd7c0>
positive primary active material <pybamm.models.submodels.active_material.constant_active_material.Constant object at 0x7f9cd48cd460>
electrolyte transport efficiency <pybamm.models.submodels.transport_efficiency.bruggeman_transport_efficiency.Bruggeman object at 0x7f9cd48cdb50>
electrode transport efficiency <pybamm.models.submodels.transport_efficiency.bruggeman_transport_efficiency.Bruggeman object at 0x7f9cd491dc70>
transverse convection <pybamm.models.submodels.convection.transverse.no_convection.NoConvection object at 0x7f9cd48b0ac0>
through-cell convection <pybamm.models.submodels.convection.through_cell.no_convection.NoConvection object at 0x7f9cd48c48e0>
negative primary open-circuit potential <pybamm.models.submodels.interface.open_circuit_potential.single_ocp.SingleOpenCircuitPotential object at 0x7f9cd481fd30>
positive primary open-circuit potential <pybamm.models.submodels.interface.open_circuit_potential.single_ocp.SingleOpenCircuitPotential object at 0x7f9cd481f8e0>
negative interface <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.InverseButlerVolmer object at 0x7f9cd481fd00>
negative interface current <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.CurrentForInverseButlerVolmer object at 0x7f9cd481f9d0>
positive interface <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.InverseButlerVolmer object at 0x7f9cd481fa60>
positive interface current <pybamm.models.submodels.interface.kinetics.inverse_kinetics.inverse_butler_volmer.CurrentForInverseButlerVolmer object at 0x7f9cd481f850>
negative primary particle <pybamm.models.submodels.particle.x_averaged_polynomial_profile.XAveragedPolynomialProfile object at 0x7f9cd4952160>
negative primary total particle concentration <pybamm.models.submodels.particle.total_particle_concentration.TotalConcentration object at 0x7f9cd481f6a0>
positive primary particle <pybamm.models.submodels.particle.fickian_diffusion.FickianDiffusion object at 0x7f9cd481f7c0>
positive primary total particle concentration <pybamm.models.submodels.particle.total_particle_concentration.TotalConcentration object at 0x7f9cd481f4c0>
negative electrode potential <pybamm.models.submodels.electrode.ohm.leading_ohm.LeadingOrder object at 0x7f9cd481f550>
positive electrode potential <pybamm.models.submodels.electrode.ohm.leading_ohm.LeadingOrder object at 0x7f9cd481f1c0>
electrolyte diffusion <pybamm.models.submodels.electrolyte_diffusion.constant_concentration.ConstantConcentration object at 0x7f9cd48c4a60>
leading-order electrolyte conductivity <pybamm.models.submodels.electrolyte_conductivity.leading_order_conductivity.LeadingOrder object at 0x7f9cd481fd90>
negative surface potential difference <pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.explicit_surface_form_conductivity.Explicit object at 0x7f9cd481fe20>
positive surface potential difference <pybamm.models.submodels.electrolyte_conductivity.surface_potential_form.explicit_surface_form_conductivity.Explicit object at 0x7f9cd481fe80>
thermal <pybamm.models.submodels.thermal.isothermal.Isothermal object at 0x7f9cd481fdf0>
current collector <pybamm.models.submodels.current_collector.homogeneous_current_collector.Uniform object at 0x7f9cd481ff10>
negative primary sei <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd48330d0>
positive primary sei <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd4833100>
negative primary sei on cracks <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd48331f0>
positive primary sei on cracks <pybamm.models.submodels.interface.sei.no_sei.NoSEI object at 0x7f9cd48331c0>
negative lithium plating <pybamm.models.submodels.interface.lithium_plating.no_plating.NoPlating object at 0x7f9cd4833280>
positive lithium plating <pybamm.models.submodels.interface.lithium_plating.no_plating.NoPlating object at 0x7f9cd48332e0>
total interface <pybamm.models.submodels.interface.total_interfacial_current.TotalInterfacialCurrent object at 0x7f9cd481ff70>

Building the model also sets up the equations, boundary and initial conditions for the model. For example, if we look at model.rhs before building we see that it is empty

[7]:
model.rhs
[7]:
{}

If we try to use this empty model, PyBaMM will give an error. So, before proceeding we must build the model

[8]:
model.build_model()

Now if we look at model.rhs we see that it contains an entry relating to the concentration in each particle, as expected for the SPM

[9]:
model.rhs
[9]:
{Variable(0x3825da4a5fc4eb0b, Discharge capacity [A.h], children=[], domains={}): Multiplication(0x7678edd47e530eec, *, children=['0.0002777777777777778', 'Current function [A]'], domains={}),
 Variable(-0x7fb8d0e6e9632372, Throughput capacity [A.h], children=[], domains={}): Multiplication(-0x7c65e8600b424661, *, children=['0.0002777777777777778', 'abs(Current function [A])'], domains={}),
 Variable(0x69f725db1a464db8, Average negative particle concentration [mol.m-3], children=[], domains={'primary': ['current collector']}): MatrixMultiplication(0xf98a766c86b2483, @, children=['mass(Average negative particle concentration [mol.m-3])', '-3.0 * Current function [A] / (Number of electrodes connected in parallel to make a cell * Electrode width [m] * Electrode height [m]) / Negative electrode thickness [m] / x-average(3.0 * Negative electrode active material volume fraction / Negative particle radius [m]) / Faraday constant [C.mol-1] / x-average(Negative particle radius [m])'], domains={'primary': ['current collector']}),
 Variable(0x48143b39c7603013, X-averaged positive particle concentration [mol.m-3], children=[], domains={'primary': ['positive particle'], 'secondary': ['current collector']}): Divergence(0x17c75a81711ad510, div, children=['Positive electrode diffusivity [m2.s-1] * grad(X-averaged positive particle concentration [mol.m-3])'], domains={'primary': ['positive particle'], 'secondary': ['current collector']})}

Now the model can be used in a simulation and solved in the usual way, and we still have access to model defaults such as the default geometry and default spatial methods which are used in the simulation

[10]:
simulation = pybamm.Simulation(model)
simulation.solve([0, 3600])
simulation.plot()
[10]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f9cd496abe0>

Building a custom model from submodels#

Instead of editing a pre-existing model, you may wish to build your own model from scratch by combining existing submodels of you choice. In this section, we build a Single Particle Model in which the diffusion is assumed infinitely fast in both particles.

To begin, we load a base lithium-ion model. This sets up the basic model structure behind the scenes, and also sets the default parameters to be those corresponding to a lithium-ion battery. Note that the base model does not select any default submodels, so there is no need to pass build=False.

[11]:
model = pybamm.lithium_ion.BaseModel()

Submodels can be added to the model.submodels dictionary in the same way that we changed the submodels earlier.

We use the simplest model for the external circuit, which is the explicit “current control” submodel

[12]:
model.submodels["external circuit"] = pybamm.external_circuit.ExplicitCurrentControl(
    model.param, model.options
)

We want to build a 1D model, so select the Uniform current collector model (if the current collectors are behaving uniformly, then a 1D model is appropriate). We also want the model to be isothermal, so select the thermal model accordingly. Further, we assume that the porosity and active material are constant in space and time.

[13]:
model.submodels["current collector"] = pybamm.current_collector.Uniform(model.param)
model.submodels["thermal"] = pybamm.thermal.isothermal.Isothermal(model.param)
model.submodels["porosity"] = pybamm.porosity.Constant(model.param, model.options)
model.submodels["negative active material"] = pybamm.active_material.Constant(
    model.param, "negative", model.options
)
model.submodels["positive active material"] = pybamm.active_material.Constant(
    model.param, "positive", model.options
)

We assume that the current density varies linearly in the electrodes. This corresponds the the leading-order terms in Ohm’s law in the limit in which the SPM is derived in [3]

[14]:
model.submodels["negative electrode potentials"] = pybamm.electrode.ohm.LeadingOrder(
    model.param, "negative"
)
model.submodels["positive electrode potentials"] = pybamm.electrode.ohm.LeadingOrder(
    model.param, "positive"
)

We assume uniform concentration in both the negative and positive particles. We also have to separately specify a model for the total concentration in each electrode, which is calculated from the concentration in the particles (not a separate ODE)

[15]:
options = {**model.options, "particle": "uniform profile"}
model.submodels[
    "negative primary particle"
] = pybamm.particle.XAveragedPolynomialProfile(model.param, "negative", options)
model.submodels[
    "positive primary particle"
] = pybamm.particle.XAveragedPolynomialProfile(model.param, "positive", options)

model.submodels[
    "negative total particle concentration"
] = pybamm.particle.TotalConcentration(model.param, "negative", options)
model.submodels[
    "positive total particle concentration"
] = pybamm.particle.TotalConcentration(model.param, "positive", options)

In the Single Particle Model, the overpotential can be obtained by inverting the Butler-Volmer relation, so we choose the InverseButlerVolmer submodel for the interface, with the “main” lithium-ion reaction (and default lithium ion options). Because of how the current is implemented, we also need to separately specify the CurrentForInverseButlerVolmer submodel. We also need to specify the submodel for open-circuit potential.

[16]:
model.submodels[
    "negative open-circuit potential"
] = pybamm.open_circuit_potential.SingleOpenCircuitPotential(
    model.param, "negative", "lithium-ion main", options=model.options
)
model.submodels[
    "positive open-circuit potential"
] = pybamm.open_circuit_potential.SingleOpenCircuitPotential(
    model.param, "positive", "lithium-ion main", options=model.options
)
model.submodels["negative interface"] = pybamm.kinetics.InverseButlerVolmer(
    model.param, "negative", "lithium-ion main", options=model.options
)
model.submodels["positive interface"] = pybamm.kinetics.InverseButlerVolmer(
    model.param, "positive", "lithium-ion main", options=model.options
)
model.submodels[
    "negative interface current"
] = pybamm.kinetics.CurrentForInverseButlerVolmer(
    model.param, "negative", "lithium-ion main"
)
model.submodels[
    "positive interface current"
] = pybamm.kinetics.CurrentForInverseButlerVolmer(
    model.param, "positive", "lithium-ion main"
)
model.submodels["negative interface utilisation"] = pybamm.interface_utilisation.Full(
    model.param, "negative", model.options
)
model.submodels["positive interface utilisation"] = pybamm.interface_utilisation.Full(
    model.param, "positive", model.options
)

We don’t want any particle mechanics, SEI formation or lithium plating in this model

[17]:
model.submodels["Negative particle mechanics"] = pybamm.particle_mechanics.NoMechanics(
    model.param, "negative", model.options
)
model.submodels["Positive particle mechanics"] = pybamm.particle_mechanics.NoMechanics(
    model.param, "positive", model.options
)
model.submodels["Negative sei"] = pybamm.sei.NoSEI(
    model.param, "negative", model.options
)
model.submodels["Positive sei"] = pybamm.sei.NoSEI(
    model.param, "positive", model.options
)
model.submodels["Negative sei on cracks"] = pybamm.sei.NoSEI(
    model.param, "negative", model.options, cracks=True
)
model.submodels["Positive sei on cracks"] = pybamm.sei.NoSEI(
    model.param, "positive", model.options, cracks=True
)
model.submodels["Negative lithium plating"] = pybamm.lithium_plating.NoPlating(
    model.param, "Negative"
)
model.submodels["Positive lithium plating"] = pybamm.lithium_plating.NoPlating(
    model.param, "Positive"
)

Finally, for the electrolyte we assume that diffusion is infinitely fast so that the concentration is uniform, and also use the leading-order model for charge conservation, which leads to a linear variation in ionic current in the electrodes

[18]:
model.submodels[
    "electrolyte diffusion"
] = pybamm.electrolyte_diffusion.ConstantConcentration(model.param)
model.submodels[
    "electrolyte conductivity"
] = pybamm.electrolyte_conductivity.LeadingOrder(model.param)

Now that we have set all of the submodels we can build the model

[19]:
model.build_model()

We can then use the model in a simulation in the usual way

[20]:
simulation = pybamm.Simulation(model)
simulation.solve([0, 3600])
simulation.plot()
[20]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f9ccfd7ba30>

References#

The relevant papers for this notebook are:

[21]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Venkat R. Subramanian, Vinten D. Diwakar, and Deepak Tapriyal. Efficient macro-micro scale coupled modeling of batteries. Journal of The Electrochemical Society, 152(10):A2002, 2005. doi:10.1149/1.2032427.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Test the parameter set of the Enertech cells#

In this notebook, we show how to use pybamm to reproduce the experimental results for the Enertech cells (LCO-G). To see all of the models and submodels available in PyBaMM, please take a look at the documentation here.

[15]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

Let’s load the DFN and include particle swelling

[16]:
model = pybamm.lithium_ion.DFN(
    options={
        "particle": "Fickian diffusion",
        "cell geometry": "arbitrary",
        "thermal": "lumped",
        "particle mechanics": "swelling only",
    }
)

We can get the parameter set Ai2020 for the model, which includes the mechanical properties required by the mechanical model.

[17]:
# update parameters, making C-rate and input
param = pybamm.ParameterValues("Ai2020")
capacity = param["Nominal cell capacity [A.h]"]
param.update({"Current function [A]": capacity * pybamm.InputParameter("C-rate")})

# update the mesh
var = pybamm.standard_spatial_vars
var_pts = {
    var.x_n: 50,
    var.x_s: 50,
    var.x_p: 50,
    var.r_n: 21,
    var.r_p: 21,
}

# define the simulation
sim = pybamm.Simulation(
    model,
    var_pts=var_pts,
    parameter_values=param,
    solver=pybamm.CasadiSolver(mode="fast"),
)

# solve for different C-rates
Crates = [0.5, 1, 2]
solutions = []
for Crate in Crates:
    print(f"{Crate} C")
    sol = sim.solve(t_eval=[0, 3600 / Crate * 1.05], inputs={"C-rate": Crate})
    solutions.append(sol)

# unpack solutions
solution05C, solution1C, solution2C = solutions
0.5 C
1 C
2 C

Load experimental results of the Enertech cells (see [1])

[18]:
# load experimental results
import pandas as pd

path = "pybamm/input/discharge_data/Enertech_cells/"
data_Disp_01C = pd.read_csv(
    path + "0.1C_discharge_displacement.txt", delimiter="\s+", header=None
)
data_Disp_05C = pd.read_csv(
    path + "0.5C_discharge_displacement.txt", delimiter="\s+", header=None
)
data_Disp_1C = pd.read_csv(
    path + "1C_discharge_displacement.txt", delimiter="\s+", header=None
)
data_Disp_2C = pd.read_csv(
    path + "2C_discharge_displacement.txt", delimiter="\s+", header=None
)
data_V_01C = pd.read_csv(path + "0.1C_discharge_U.txt", delimiter="\s+", header=None)
data_V_05C = pd.read_csv(path + "0.5C_discharge_U.txt", delimiter="\s+", header=None)
data_V_1C = pd.read_csv(path + "1C_discharge_U.txt", delimiter="\s+", header=None)
data_V_2C = pd.read_csv(path + "2C_discharge_U.txt", delimiter="\s+", header=None)
data_T_05C = pd.read_csv(path + "0.5C_discharge_T.txt", delimiter="\s+", header=None)
data_T_1C = pd.read_csv(path + "1C_discharge_T.txt", delimiter="\s+", header=None)
data_T_2C = pd.read_csv(path + "2C_discharge_T.txt", delimiter="\s+", header=None)

Plot the results.

[19]:
t_all2C = solution2C["Time [h]"].entries
V_n2C = solution2C["Voltage [V]"].entries
T_n2C = (
    solution2C["Volume-averaged cell temperature [K]"].entries
    - param["Initial temperature [K]"]
)
L_x2C = solution2C["Cell thickness change [m]"].entries

t_all1C = solution1C["Time [h]"].entries
V_n1C = solution1C["Voltage [V]"].entries
T_n1C = (
    solution1C["Volume-averaged cell temperature [K]"].entries
    - param["Initial temperature [K]"]
)
L_x1C = solution1C["Cell thickness change [m]"].entries

t_all05C = solution05C["Time [h]"].entries
V_n05C = solution05C["Voltage [V]"].entries
T_n05C = (
    solution05C["Volume-averaged cell temperature [K]"].entries
    - param["Initial temperature [K]"]
)
L_x05C = solution05C["Cell thickness change [m]"].entries

f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(14, 4))

ax1.plot(t_all2C, V_n2C, "r-", label="Simulation")
ax1.plot(
    data_V_2C.values[::30, 0] / 3600,
    data_V_2C.values[::30, 1],
    "ro",
    markerfacecolor="none",
    label="Experiment",
)
ax1.plot(t_all05C, V_n05C, "g-")
ax1.plot(
    data_V_05C.values[::100, 0] / 3600,
    data_V_05C.values[::100, 1],
    "go",
    markerfacecolor="none",
)
ax1.plot(t_all1C, V_n1C, "b-")
ax1.plot(
    data_V_1C.values[::50, 0] / 3600,
    data_V_1C.values[::50, 1],
    "bo",
    markerfacecolor="none",
)
ax1.legend()
ax1.set_xlabel("Time [h]")
ax1.set_ylabel("Voltage [V]")
ax1.text(0.1, 3.2, r"2 C", {"color": "r", "fontsize": 14})
ax1.text(1.1, 3.2, r"1 C", {"color": "b", "fontsize": 14})
ax1.text(1.6, 3.2, r"0.5 C", {"color": "g", "fontsize": 14})

ax2.plot(t_all2C, T_n2C, "r-", label="Simulation")
ax2.plot(
    data_T_2C.values[0:1754:50, 0] / 3600,
    data_T_2C.values[0:1754:50, 1],
    "ro",
    markerfacecolor="none",
    label="Experiment",
)
ax2.plot(t_all05C, T_n05C, "g-")
ax2.plot(
    data_T_05C.values[0:7301:200, 0] / 3600,
    data_T_05C.values[0:7301:200, 1],
    "go",
    markerfacecolor="none",
)
ax2.plot(t_all1C, T_n1C, "b-")
ax2.plot(
    data_T_1C.values[0:3598:100, 0] / 3600,
    data_T_1C.values[0:3598:100, 1],
    "bo",
    markerfacecolor="none",
)
ax2.legend()
ax2.set_xlabel("Time [h]")
ax2.set_ylabel("Temperature rise [K]")
ax2.text(0.5, 8, r"2 C", {"color": "r", "fontsize": 14})
ax2.text(0.8, 4.4, r"1 C", {"color": "b", "fontsize": 14})
ax2.text(1.5, 2, r"0.5 C", {"color": "g", "fontsize": 14})

ax3.plot(t_all2C, L_x2C, "r-", label="Simulation")
ax3.plot(
    data_Disp_2C.values[0:1754:5, 0] / 3600,
    data_Disp_2C.values[0:1754:5, 1] - data_Disp_2C.values[0, 1],
    "ro",
    markerfacecolor="none",
    label="Experiment",
)
ax3.plot(t_all05C, L_x05C, "g-")
ax3.plot(
    data_Disp_05C.values[0:1754:10, 0] / 3600,
    data_Disp_05C.values[0:1754:10, 1] - data_Disp_05C.values[0, 1],
    "go",
    markerfacecolor="none",
)
ax3.plot(t_all1C, L_x1C, "b-")
ax3.plot(
    data_Disp_1C.values[0:1754:10, 0] / 3600,
    data_Disp_1C.values[0:1754:10, 1] - data_Disp_1C.values[0, 1],
    "bo",
    markerfacecolor="none",
)
ax3.legend()
ax3.set_xlabel("Time [h]")
ax3.set_ylabel("Thickness change [m]")
ax3.text(0.1, -0.0001, r"2 C", {"color": "r", "fontsize": 14})
ax3.text(0.9, -0.0001, r"1 C", {"color": "b", "fontsize": 14})
ax3.text(1.8, -0.0001, r"0.5 C", {"color": "g", "fontsize": 14})

f.tight_layout()
f.show()
_images/source_examples_notebooks_models_Validating_mechanical_models_Enertech_DFN_9_0.png

Stress data below are from Fig. 6 in [1]

[20]:
E_n = param["Negative electrode Young's modulus [Pa]"]
E_p = param["Positive electrode Young's modulus [Pa]"]

cs_n_xav = solution2C["X-averaged negative particle concentration [mol.m-3]"].entries
cs_p_xav = solution2C["X-averaged positive particle concentration [mol.m-3]"].entries
st_surf_n = solution2C["Negative particle surface tangential stress [Pa]"].entries / E_n
st_surf_p = solution2C["Positive particle surface tangential stress [Pa]"].entries / E_p

data_st_n_2C = pd.read_csv(path + "stn_2C.txt", delimiter=",", header=3)
data_st_p_2C = pd.read_csv(path + "stp_2C.txt", delimiter=",", header=3)

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 3.5))

ax1.plot(t_all2C, st_surf_n[-1, :], "ro", markerfacecolor="none", label="Current")
ax1.plot(
    data_st_n_2C.values[:, 0] / 3600,
    data_st_n_2C.values[:, 1],
    "r-",
    label="Ai et al. 2020",
)
ax1.legend()
ax1.set_xlabel("Time [h]")
ax1.set_ylabel("$\sigma_{t,n}/E_n$")

ax2.plot(t_all2C, st_surf_p[0, :], "ro", markerfacecolor="none", label="Current")
ax2.plot(
    data_st_p_2C.values[0:3601, 0] / 3600,
    data_st_p_2C.values[0:3601, 1],
    "r-",
    label="Ai et al. 2020",
)
ax2.legend()
ax2.set_xlabel("Time [h]")
ax2.set_ylabel("$\sigma_{t,p}/E_p$")

f.tight_layout()
f.show()
_images/source_examples_notebooks_models_Validating_mechanical_models_Enertech_DFN_11_0.png

References#

The relevant papers for this notebook are:

[21]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[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.
[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.
[7] Robert Timms, Scott G Marquis, Valentin Sulzer, Colin P. Please, and S Jonathan Chapman. Asymptotic Reduction of a Lithium-ion Pouch Cell Model. SIAM Journal on Applied Mathematics, 81(3):765–788, 2021. doi:10.1137/20M1336898.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Changing the input current when solving PyBaMM models#

This notebook shows you how to change the input current when solving PyBaMM models. It also explains how to load in current data from a file, and how to add a user-defined current function. For more examples of different drive cycles see here.

Table of Contents#

  1. Constant current

  2. Loading in current data

  3. Adding your own current function

Constant current#

In this notebook we will use the SPM as the example model, and change the input current from the default option. If you are not familiar with running a model in PyBaMM, please see this notebook for more details.

In PyBaMM, the current function is set using the parameter “Current function [A]”. Below we load the SPM with the default parameters, and then change the the current function to be an input parameter, so that we can change it easily later.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os

os.chdir(pybamm.__path__[0] + "/..")

# create the model
model = pybamm.lithium_ion.DFN()

# set the default model parameters
param = model.default_parameter_values

# change the current function to be an input parameter
param["Current function [A]"] = "[input]"
Note: you may need to restart the kernel to use updated packages.

We can set up a simulation in the usual way, making sure we pass in our updated parameters. We choose to solve with a 1.6A current. In order to do this we must pass a dictionary of inputs whose keys are the parameter names and values are the values we want to use for that call to solve

[2]:
# set up simlation
simulation = pybamm.Simulation(model, parameter_values=param)

# solve the model at the given time points, passing the current as an input
t_eval = np.linspace(0, 600, 300)
simulation.solve(t_eval, inputs={"Current function [A]": 1.6})

# plot
simulation.plot()

PyBaMM can also simulate rest behaviour by setting the current function to zero:

[3]:
# solve the model at the given time points
simulation.solve(t_eval, inputs={"Current function [A]": 0})

# plot
simulation.plot()
Loading in current data#

To run drive cycles from data we can create an interpolant and pass it as the current function.

[4]:
import pandas as pd  # needed to read the csv data file

model = pybamm.lithium_ion.DFN()

# import drive cycle from file
drive_cycle = pd.read_csv(
    "pybamm/input/drive_cycles/US06.csv", comment="#", header=None
).to_numpy()

# load parameter values
param = model.default_parameter_values

# create interpolant - must be a function of *dimensional* time
current_interpolant = pybamm.Interpolant(drive_cycle[:, 0], drive_cycle[:, 1], pybamm.t)

# set drive cycle
param["Current function [A]"] = current_interpolant

# set up simulation - for drive cycles we recommend using the CasadiSolver in "fast" mode
solver = pybamm.CasadiSolver(mode="fast")
simulation = pybamm.Simulation(model, parameter_values=param, solver=solver)

Note that when simulating drive cycles there is no need to pass a list of times at which to return the solution, the results are automatically returned at the time points in the data. If you would like the solution returned at times different to those in the data then you can pass an array of times t_eval to solve in the usual way.

[5]:
# simulate US06 drive cycle (duration 600 seconds)
simulation.solve()

# plot
simulation.plot()

Note that some solvers try to evaluate the model equations at a very large value of t during the first step. This may raise a warning if the time requested by the solver is outside of the range of the data provided. However, this does not affect the solve since this large timestep is rejected by the solver, and a suitable shorter initial step is taken.

Adding your own current function#

A user defined current function can be passed to any model by specifying either a function or a set of data points for interpolation.

For example, you may want to simulate a sinusoidal current with amplitude A and frequency omega. In order to do so you must first define the method

[6]:
# create user-defined function


def my_fun(A, omega):
    def current(t):
        return A * pybamm.sin(2 * np.pi * omega * t)

    return current

Note that the function returns a function which takes the input time. Then the model may be loaded and the “Current function” parameter updated to my_fun called with a specific value of A and omega

[7]:
model = pybamm.lithium_ion.SPM()

# load default parameter values
param = model.default_parameter_values

# set user defined current function
A = model.param.I_typ
omega = 0.1
param["Current function [A]"] = my_fun(A, omega)

Note that when my_fun is evaluated with A and omega, this creates a new function current(t) which can then be used in the expression tree. The model may then be solved in the usual way

[8]:
# set up simulation
simulation = pybamm.Simulation(model, parameter_values=param)

# Example: simulate for 30 seconds
simulation_time = 30  # end time in seconds
npts = int(50 * simulation_time * omega)  # need enough timesteps to resolve output
t_eval = np.linspace(0, simulation_time, npts)
solution = simulation.solve(t_eval)
label = [f"Frequency: {omega} Hz"]

# plot current and voltage
output_variables = ["Current [A]", "Voltage [V]"]
simulation.plot(output_variables, labels=label)
References#

The relevant papers for this notebook are:

[9]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Parameter Values#

In this notebook, we explain how parameter values are set for a model. Information on creating new parameter sets is provided in our online documentation

Setting up parameter values#

[12]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

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,

[13]:
param_dict = {"a": 1, "b": 2, "c": 3}
parameter_values = pybamm.ParameterValues(param_dict)
print(f"parameter values are {parameter_values}")
parameter values are {'Boltzmann constant [J.K-1]': 1.380649e-23,
 'Electron charge [C]': 1.602176634e-19,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'a': 1,
 'b': 2,
 'c': 3}

or using one of the pre-set chemistries

[14]:
chem_parameter_values = pybamm.ParameterValues("Marquis2019")
print(
    "Negative current collector thickness is {} m".format(
        chem_parameter_values["Negative current collector thickness [m]"]
    )
)
Negative current collector thickness is 2.5e-05 m

We can input functions into the parameter value (note we bypass the check that the parameter already exists)

[15]:
def cubed(x):
    return x**3


parameter_values.update({"cube function": cubed}, check_already_exists=False)
print(f"parameter values are {parameter_values}")
parameter values are {'Boltzmann constant [J.K-1]': 1.380649e-23,
 'Electron charge [C]': 1.602176634e-19,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'a': 1,
 'b': 2,
 'c': 3,
 'cube function': <function cubed at 0x16118e0d0>}

Setting parameters for an expression#

We represent parameters in models using the classes Parameter and FunctionParameter. These cannot be evaluated directly,

[16]:
a = pybamm.Parameter("a")
b = pybamm.Parameter("b")
c = pybamm.Parameter("c")
func = pybamm.FunctionParameter("cube function", {"a": a})

expr = a + b * c
try:
    expr.evaluate()
except NotImplementedError as e:
    print(e)
method self.evaluate() not implemented for symbol a of type <class 'pybamm.expression_tree.parameter.Parameter'>

However, the ParameterValues class can walk through an expression, changing an Parameter objects it sees to the appropriate Scalar and any FunctionParameter object to the appropriate Function, and the resulting expression can be evaluated

[17]:
expr_eval = parameter_values.process_symbol(expr)
print(f"{expr_eval} = {expr_eval.evaluate()}")
7.0 = 7.0
[18]:
func_eval = parameter_values.process_symbol(func)
print(f"{func_eval} = {func_eval.evaluate()}")
1.0 = 1.0

If a parameter needs to be changed often (for example, for convergence studies or parameter estimation), the InputParameter class should be used. This is not fixed by parameter values, and its value can be set on evaluation (or on solve):

[19]:
d = pybamm.InputParameter("d")
expr = 2 + d
expr_eval = parameter_values.process_symbol(expr)
print("with d = {}, {} = {}".format(3, expr_eval, expr_eval.evaluate(inputs={"d": 3})))
print("with d = {}, {} = {}".format(5, expr_eval, expr_eval.evaluate(inputs={"d": 5})))
with d = 3, 2.0 + d = 5.0
with d = 5, 2.0 + d = 7.0

Solving a model#

The code below shows the entire workflow of:

  1. Proposing a toy model

  2. Discretising and solving it first with one set of parameters,

  3. then updating the parameters and solving again

The toy model used is:

\[\frac{\mathrm{d} u}{\mathrm{d} t} = -a u\]

with initial conditions \(u(0) = b\). The model is first solved with \(a = 3, b = 2\), then with \(a = -1, b = 2\)

[20]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
a = pybamm.Parameter("a")
b = pybamm.Parameter("b")
model.rhs = {u: -a * u}
model.initial_conditions = {u: b}
model.variables = {"u": u, "a": a, "b": b}

# Set parameters, with a as an input ########################
parameter_values = pybamm.ParameterValues({"a": "[input]", "b": 2})
parameter_values.process_model(model)
#############################################################

# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model)

# Solve
t_eval = np.linspace(0, 2, 30)
ode_solver = pybamm.ScipySolver()
solution = ode_solver.solve(model, t_eval, inputs={"a": 3})

# Post-process, so that u1 can be called at any time t (using interpolation)
t_sol1 = solution.t
u1 = solution["u"]

# Solve again with different inputs ###############################
solution = ode_solver.solve(model, t_eval, inputs={"a": -1})
t_sol2 = solution.t
u2 = solution["u"]
###################################################################

# Plot
t_fine = np.linspace(0, t_eval[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, 2 * np.exp(-3 * t_fine), t_sol1, u1(t_sol1), "o")
ax1.set_xlabel("t")
ax1.legend(["2 * exp(-3 * t)", "u1"], loc="best")
ax1.set_title("a = 3, b = 2")

ax2.plot(t_fine, 2 * np.exp(t_fine), t_sol2, u2(t_sol2), "o")
ax2.set_xlabel("t")
ax2.legend(["2 * exp(t)", "u2"], loc="best")
ax2.set_title("a = -1, b = 2")


plt.tight_layout()
plt.show()
_images/source_examples_notebooks_parameterization_parameter-values_19_0.png
[21]:
model.rhs
[21]:
{Variable(0x5f4a102fc03b7b39, u, children=[], domains={}): Multiplication(-0x32ae6bc94fa07109, *, children=['-a', 'y[0:1]'], domains={})}

Printing parameter values#

Since parameters objects must be processed by the ParameterValues class before they can be evaluated, it can be difficult to quickly check the value of a parameter that is a combination of other parameters. You can print all of the parameters (including combinations) in a model by using the print_parameters function.

[22]:
a = pybamm.Parameter("a")
b = pybamm.Parameter("b")
parameter_values = pybamm.ParameterValues({"a": 4, "b": 3})
parameters = {"a": a, "b": b, "a + b": a + b, "a * b": a * b}
param_eval = parameter_values.print_parameters(parameters)
for name, value in param_eval.items():
    print(f"{name}: {value}")
a: 4.0
b: 3.0
a + b: 7.0
a * b: 12.0

If you provide an output file to print_parameters, the parameters will be printed to that output file.

References#

The relevant papers for this notebook are:

[23]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.
[5] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Parameterisation#

In this notebook, we show how to find which parameters are needed in a model and define them.

For other notebooks about parameterization, see:

  • The API documentation of Parameters

  • Setting parameter values can be found at pybamm/docs/source/examples/notebooks/getting_started/tutorial-4-setting-parameter-values.ipynb. This explains the basics of how to set the parameters of a model (in less detail than here).

  • parameter-values.ipynb can be found at pybamm/examples/notebooks/parameterization/parameter-values.ipynb. This explains the basics of the ParameterValues class.

Adding your own parameter sets (using a dictionary)#

We will be using the model defined and explained in more detail in 3-negative-particle-problem.ipynb example notebook. We begin by importing the required libraries

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import matplotlib.pyplot as plt
/bin/bash: warning: setlocale: LC_ALL: cannot change locale (en_US.UTF-8)
Note: you may need to restart the kernel to use updated packages.

Setting up the model#

We define all the parameters and variables using pybamm.Parameter and pybamm.Variable respectively.

[2]:
c = pybamm.Variable("Concentration [mol.m-3]", domain="negative particle")

R = pybamm.Parameter("Particle radius [m]")
D = pybamm.FunctionParameter(
    "Diffusion coefficient [m2.s-1]", {"Concentration [mol.m-3]": c}
)
j = pybamm.InputParameter("Interfacial current density [A.m-2]")
c0 = pybamm.Parameter("Initial concentration [mol.m-3]")
c_e = pybamm.Parameter("Electrolyte concentration [mol.m-3]")

Now we define our model equations, boundary and initial conditions. We also add the variables required using the dictionary model.variables

[3]:
model = pybamm.BaseModel()

# governing equations
N = -D * pybamm.grad(c)  # flux
dcdt = -pybamm.div(N)
model.rhs = {c: dcdt}

# boundary conditions
lbc = pybamm.Scalar(0)
rbc = -j
model.boundary_conditions = {c: {"left": (lbc, "Neumann"), "right": (rbc, "Neumann")}}

# initial conditions
model.initial_conditions = {c: c0}

model.variables = {
    "Concentration [mol.m-3]": c,
    "Surface concentration [mol.m-3]": pybamm.surf(c),
    "Flux [mol.m-2.s-1]": N,
}

We also define the geometry, since there are parameters in the geometry too

[4]:
r = pybamm.SpatialVariable(
    "r", domain=["negative particle"], coord_sys="spherical polar"
)
geometry = pybamm.Geometry(
    {"negative particle": {r: {"min": pybamm.Scalar(0), "max": R}}}
)

Finding the parameters required#

To know what parameters are required by the model and geometry, we can do

[5]:
model.print_parameter_info()
geometry.print_parameter_info()
| Parameter                           | Type of parameter                                          |
| =================================== | ========================================================== |
| Initial concentration [mol.m-3]     | Parameter                                                  |
| Interfacial current density [A.m-2] | InputParameter                                             |
| Diffusion coefficient [m2.s-1]      | FunctionParameter with inputs(s) 'Concentration [mol.m-3]' |
Particle radius [m] (Parameter)

This tells us that we need to provide parameter values for the initial concentration and Faraday constant, an InputParameter at solve time for the interfacial current density, and diffusivity as a function of concentration. Since the electrolyte concentration does not appear anywhere in the model, there is no need to provide a value for it.

Adding the parameters#

Now we can proceed to the step where we add the parameter values using a dictionary. We set up a dictionary with parameter names as the dictionary keys and their respective values as the dictionary values.

[6]:
def D_fun(c):
    return 3.9  # * pybamm.exp(-c)


values = {
    "Particle radius [m]": 2,
    "Diffusion coefficient [m2.s-1]": D_fun,
    "Initial concentration [mol.m-3]": 2.5,
}

Now we can pass this dictionary in pybamm.ParameterValues class which accepts a dictionary of parameter names and values. We can then print param to check if it was initialised.

[7]:
param = pybamm.ParameterValues(values)

param
[7]:
{'Boltzmann constant [J.K-1]': 1.380649e-23,
 'Diffusion coefficient [m2.s-1]': <function D_fun at 0x7f48221f0220>,
 'Electron charge [C]': 1.602176634e-19,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'Initial concentration [mol.m-3]': 2.5,
 'Particle radius [m]': 2}

Updating the parameter values#

The parameter values or param can be further updated by using the update function of ParameterValues class. The update function takes a dictionary with keys being the parameters to be updated and their respective values being the updated values. Here we update the "Particle radius [m]" parameter’s value. Additionally, a function can also be passed as a parameter’s value which we will see ahead, and a new parameter can also be added by passing check_already_exists=False in the update function.

[8]:
param.update({"Initial concentration [mol.m-3]": 1.5})
param
[8]:
{'Boltzmann constant [J.K-1]': 1.380649e-23,
 'Diffusion coefficient [m2.s-1]': <function D_fun at 0x7f48221f0220>,
 'Electron charge [C]': 1.602176634e-19,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'Initial concentration [mol.m-3]': 1.5,
 'Particle radius [m]': 2}

Solving the model#

Finding the parameters in a model#

The parameter function of the BaseModel class can be used to obtain the parameters of a model.

[9]:
parameters = model.parameters
parameters
[9]:
[Parameter(-0x60748912cbf94f86, Initial concentration [mol.m-3], children=[], domains={}),
 InputParameter(0x650425db234f99f4, Interfacial current density [A.m-2], children=[], domains={}),
 FunctionParameter(-0x302b1e5afcbfd4d9, Diffusion coefficient [m2.s-1], children=['Concentration [mol.m-3]'], domains={'primary': ['negative particle']})]

As explained in the 3-negative-particle-problem.ipynb example, we first process both the model and the geometry.

[10]:
param.process_model(model)
param.process_geometry(geometry)

We can now set up our mesh, choose a spatial method, and discretise our model

[11]:
submesh_types = {"negative particle": pybamm.Uniform1DSubMesh}
var_pts = {r: 20}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

spatial_methods = {"negative particle": pybamm.FiniteVolume()}
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model);

We choose a solver and times at which we want the solution returned, and solve the model. Here we give a value for the current density j.

[12]:
# solve
solver = pybamm.ScipySolver()
t = np.linspace(0, 3600, 600)
solution = solver.solve(model, t, inputs={"Interfacial current density [A.m-2]": 1.4})

# post-process, so that the solution can be called at any time t or space r
# (using interpolation)
c = solution["Concentration [mol.m-3]"]
c_surf = solution["Surface concentration [mol.m-3]"]

# plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

ax1.plot(solution.t, c_surf(solution.t))
ax1.set_xlabel("Time [s]")
ax1.set_ylabel("Surface concentration [mol.m-3]")

rsol = mesh["negative particle"].nodes  # radial position
time = 1000  # time in seconds
ax2.plot(rsol * 1e6, c(t=time, r=rsol), label=f"t={time}[s]")
ax2.set_xlabel("Particle radius [microns]")
ax2.set_ylabel("Concentration [mol.m-3]")
ax2.legend()

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_parameterization_parameterization_27_0.png

Using pre-defined models in PyBaMM#

In the next few steps, we will be showing the same workflow with the Single Particle Model (SPM). We will also see how you can pass a function as a parameter’s value and how to plot such parameter functions.

We start by initializing our model

[13]:
spm = pybamm.lithium_ion.SPM()

Finding the parameters in a model#

We can print the parameters of a model by using the print_parameter_info function.

[14]:
spm.print_parameter_info()
| Parameter                                                 | Type of parameter                                                                                                                                                                                           |
| ========================================================= | =========================================================================================================================================================================================================== |
| Positive electrode Bruggeman coefficient (electrolyte)    | Parameter                                                                                                                                                                                                   |
| Electrode width [m]                                       | Parameter                                                                                                                                                                                                   |
| Positive electrode thickness [m]                          | Parameter                                                                                                                                                                                                   |
| Negative electrode Bruggeman coefficient (electrolyte)    | Parameter                                                                                                                                                                                                   |
| Negative electrode Bruggeman coefficient (electrode)      | Parameter                                                                                                                                                                                                   |
| Initial concentration in electrolyte [mol.m-3]            | Parameter                                                                                                                                                                                                   |
| Number of cells connected in series to make a battery     | Parameter                                                                                                                                                                                                   |
| Lower voltage cut-off [V]                                 | Parameter                                                                                                                                                                                                   |
| Ideal gas constant [J.K-1.mol-1]                          | Parameter                                                                                                                                                                                                   |
| Separator Bruggeman coefficient (electrolyte)             | Parameter                                                                                                                                                                                                   |
| Upper voltage cut-off [V]                                 | Parameter                                                                                                                                                                                                   |
| Positive electrode Bruggeman coefficient (electrode)      | Parameter                                                                                                                                                                                                   |
| Separator thickness [m]                                   | Parameter                                                                                                                                                                                                   |
| Maximum concentration in negative electrode [mol.m-3]     | Parameter                                                                                                                                                                                                   |
| Faraday constant [C.mol-1]                                | Parameter                                                                                                                                                                                                   |
| Reference temperature [K]                                 | Parameter                                                                                                                                                                                                   |
| Electrode height [m]                                      | Parameter                                                                                                                                                                                                   |
| Nominal cell capacity [A.h]                               | Parameter                                                                                                                                                                                                   |
| Maximum concentration in positive electrode [mol.m-3]     | Parameter                                                                                                                                                                                                   |
| Number of electrodes connected in parallel to make a cell | Parameter                                                                                                                                                                                                   |
| Negative electrode thickness [m]                          | Parameter                                                                                                                                                                                                   |
| Separator porosity                                        | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Negative electrode active material volume fraction        | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Negative electrode OCP [V]                                | FunctionParameter with inputs(s) 'Negative particle stoichiometry'                                                                                                                                          |
| Positive electrode porosity                               | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Negative particle radius [m]                              | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| 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]' |
| Positive particle radius [m]                              | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Positive electrode OCP [V]                                | FunctionParameter with inputs(s) 'Positive particle stoichiometry'                                                                                                                                          |
| 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]' |
| Negative electrode OCP entropic change [V.K-1]            | FunctionParameter with inputs(s) 'Negative particle stoichiometry', 'Maximum negative particle surface concentration [mol.m-3]'                                                                             |
| Current function [A]                                      | FunctionParameter with inputs(s) 'Time[s]'                                                                                                                                                                  |
| Initial concentration in positive electrode [mol.m-3]     | FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]'                                                                                                                 |
| Initial concentration in negative electrode [mol.m-3]     | FunctionParameter with inputs(s) 'Radial distance (r) [m]', 'Through-cell distance (x) [m]'                                                                                                                 |
| Positive electrode OCP entropic change [V.K-1]            | FunctionParameter with inputs(s) 'Positive particle stoichiometry', 'Maximum positive particle surface concentration [mol.m-3]'                                                                             |
| Positive electrode diffusivity [m2.s-1]                   | FunctionParameter with inputs(s) 'Positive particle stoichiometry', 'Temperature [K]'                                                                                                                       |
| Negative electrode porosity                               | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Positive electrode active material volume fraction        | FunctionParameter with inputs(s) 'Through-cell distance (x) [m]'                                                                                                                                            |
| Negative electrode diffusivity [m2.s-1]                   | FunctionParameter with inputs(s) 'Negative particle stoichiometry', 'Temperature [K]'                                                                                                                       |
| Ambient temperature [K]                                   | FunctionParameter with inputs(s) 'Distance across electrode width [m]', 'Distance across electrode height [m]', 'Time [s]'                                                                                  |

Note that there are no InputParameter objects in the default SPM. Also, note that if a FunctionParameter is expected, it is ok to provide a scalar (parameter) instead. However, if a Parameter is expected, you cannot provide a function instead.

Another way to view what parameters are needed is to print the default parameter values. This can also be used to get some good defaults (but care must be taken when combining parameters across datasets and chemistries)

[15]:
{k: v for k, v in spm.default_parameter_values.items() if k in spm.get_parameter_info()}
[15]:
{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Negative electrode thickness [m]': 0.0001,
 'Separator thickness [m]': 2.5e-05,
 'Positive electrode thickness [m]': 0.0001,
 'Electrode height [m]': 0.137,
 'Electrode width [m]': 0.207,
 'Nominal cell capacity [A.h]': 0.680616,
 'Current function [A]': 0.680616,
 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,
 'Negative electrode diffusivity [m2.s-1]': <function pybamm.input.parameters.lithium_ion.Marquis2019.graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T)>,
 'Negative electrode OCP [V]': <function pybamm.input.parameters.lithium_ion.Marquis2019.graphite_mcmb2528_ocp_Dualfoil1998(sto)>,
 'Negative electrode porosity': 0.3,
 'Negative electrode active material volume fraction': 0.6,
 'Negative particle radius [m]': 1e-05,
 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Negative electrode Bruggeman coefficient (electrode)': 1.5,
 'Negative electrode exchange-current density [A.m-2]': <function pybamm.input.parameters.lithium_ion.Marquis2019.graphite_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_max, T)>,
 'Negative electrode OCP entropic change [V.K-1]': <function pybamm.input.parameters.lithium_ion.Marquis2019.graphite_entropic_change_Moura2016(sto, c_s_max)>,
 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,
 'Positive electrode diffusivity [m2.s-1]': <function pybamm.input.parameters.lithium_ion.Marquis2019.lico2_diffusivity_Dualfoil1998(sto, T)>,
 'Positive electrode OCP [V]': <function pybamm.input.parameters.lithium_ion.Marquis2019.lico2_ocp_Dualfoil1998(sto)>,
 'Positive electrode porosity': 0.3,
 'Positive electrode active material volume fraction': 0.5,
 'Positive particle radius [m]': 1e-05,
 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Positive electrode Bruggeman coefficient (electrode)': 1.5,
 'Positive electrode exchange-current density [A.m-2]': <function pybamm.input.parameters.lithium_ion.Marquis2019.lico2_electrolyte_exchange_current_density_Dualfoil1998(c_e, c_s_surf, c_s_max, T)>,
 'Positive electrode OCP entropic change [V.K-1]': <function pybamm.input.parameters.lithium_ion.Marquis2019.lico2_entropic_change_Moura2016(sto, c_s_max)>,
 'Separator porosity': 1.0,
 'Separator Bruggeman coefficient (electrolyte)': 1.5,
 'Initial concentration in electrolyte [mol.m-3]': 1000.0,
 '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]': 3.105,
 'Upper voltage cut-off [V]': 4.1,
 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,
 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565}

We now define a dictionary of values for ParameterValues as before (here, a subset of the Chen2020 parameters)

[16]:
def graphite_mcmb2528_diffusivity_Dualfoil1998(sto, T):
    D_ref = 3.9 * 10 ** (-14)
    E_D_s = 42770
    arrhenius = exp(E_D_s / constants.R * (1 / 298.15 - 1 / T))
    return D_ref * arrhenius


neg_ocp = np.array(
    [
        [0.0, 1.81772748],
        [0.03129623, 1.0828807],
        [0.03499902, 0.99593794],
        [0.0387018, 0.90023398],
        [0.04240458, 0.79649431],
        [0.04610736, 0.73354429],
        [0.04981015, 0.66664314],
        [0.05351292, 0.64137149],
        [0.05721568, 0.59813869],
        [0.06091845, 0.5670836],
        [0.06462122, 0.54746181],
        [0.06832399, 0.53068399],
        [0.07202675, 0.51304734],
        [0.07572951, 0.49394092],
        [0.07943227, 0.47926274],
        [0.08313503, 0.46065259],
        [0.08683779, 0.45992726],
        [0.09054054, 0.43801501],
        [0.09424331, 0.42438665],
        [0.09794607, 0.41150269],
        [0.10164883, 0.40033659],
        [0.10535158, 0.38957134],
        [0.10905434, 0.37756538],
        [0.1127571, 0.36292541],
        [0.11645985, 0.34357086],
        [0.12016261, 0.3406314],
        [0.12386536, 0.32299468],
        [0.12756811, 0.31379458],
        [0.13127086, 0.30795386],
        [0.13497362, 0.29207319],
        [0.13867638, 0.28697687],
        [0.14237913, 0.27405477],
        [0.14608189, 0.2670497],
        [0.14978465, 0.25857493],
        [0.15348741, 0.25265783],
        [0.15719018, 0.24826777],
        [0.16089294, 0.2414345],
        [0.1645957, 0.23362778],
        [0.16829847, 0.22956218],
        [0.17200122, 0.22370236],
        [0.17570399, 0.22181271],
        [0.17940674, 0.22089651],
        [0.1831095, 0.2194268],
        [0.18681229, 0.21830064],
        [0.19051504, 0.21845333],
        [0.1942178, 0.21753715],
        [0.19792056, 0.21719357],
        [0.20162334, 0.21635373],
        [0.2053261, 0.21667822],
        [0.20902886, 0.21738444],
        [0.21273164, 0.21469313],
        [0.2164344, 0.21541846],
        [0.22013716, 0.21465495],
        [0.22383993, 0.2135479],
        [0.2275427, 0.21392964],
        [0.23124547, 0.21074206],
        [0.23494825, 0.20873788],
        [0.23865101, 0.20465319],
        [0.24235377, 0.20205732],
        [0.24605653, 0.19774358],
        [0.2497593, 0.19444147],
        [0.25346208, 0.19190285],
        [0.25716486, 0.18850531],
        [0.26086762, 0.18581399],
        [0.26457039, 0.18327537],
        [0.26827314, 0.18157659],
        [0.2719759, 0.17814088],
        [0.27567867, 0.17529686],
        [0.27938144, 0.1719375],
        [0.28308421, 0.16934161],
        [0.28678698, 0.16756649],
        [0.29048974, 0.16609676],
        [0.29419251, 0.16414985],
        [0.29789529, 0.16260378],
        [0.30159806, 0.16224113],
        [0.30530083, 0.160027],
        [0.30900361, 0.15827096],
        [0.31270637, 0.1588054],
        [0.31640913, 0.15552238],
        [0.32011189, 0.15580869],
        [0.32381466, 0.15220118],
        [0.32751744, 0.1511132],
        [0.33122021, 0.14987253],
        [0.33492297, 0.14874637],
        [0.33862575, 0.14678037],
        [0.34232853, 0.14620776],
        [0.34603131, 0.14555879],
        [0.34973408, 0.14389819],
        [0.35343685, 0.14359279],
        [0.35713963, 0.14242846],
        [0.36084241, 0.14038612],
        [0.36454517, 0.13882096],
        [0.36824795, 0.13954628],
        [0.37195071, 0.13946992],
        [0.37565348, 0.13780934],
        [0.37935626, 0.13973714],
        [0.38305904, 0.13698858],
        [0.38676182, 0.13523254],
        [0.3904646, 0.13441178],
        [0.39416737, 0.1352898],
        [0.39787015, 0.13507985],
        [0.40157291, 0.13647321],
        [0.40527567, 0.13601512],
        [0.40897844, 0.13435452],
        [0.41268121, 0.1334765],
        [0.41638398, 0.1348317],
        [0.42008676, 0.13275118],
        [0.42378953, 0.13286571],
        [0.4274923, 0.13263667],
        [0.43119506, 0.13456447],
        [0.43489784, 0.13471718],
        [0.43860061, 0.13395369],
        [0.44230338, 0.13448814],
        [0.44600615, 0.1334765],
        [0.44970893, 0.13298023],
        [0.45341168, 0.13259849],
        [0.45711444, 0.13338107],
        [0.46081719, 0.13309476],
        [0.46451994, 0.13275118],
        [0.46822269, 0.13443087],
        [0.47192545, 0.13315202],
        [0.47562821, 0.132713],
        [0.47933098, 0.1330184],
        [0.48303375, 0.13278936],
        [0.48673651, 0.13225491],
        [0.49043926, 0.13317111],
        [0.49414203, 0.13263667],
        [0.49784482, 0.13187316],
        [0.50154759, 0.13265574],
        [0.50525036, 0.13250305],
        [0.50895311, 0.13324745],
        [0.51265586, 0.13204496],
        [0.51635861, 0.13242669],
        [0.52006139, 0.13233127],
        [0.52376415, 0.13198769],
        [0.52746692, 0.13254122],
        [0.53116969, 0.13145325],
        [0.53487245, 0.13298023],
        [0.53857521, 0.13168229],
        [0.54227797, 0.1313578],
        [0.54598074, 0.13235036],
        [0.5496835, 0.13120511],
        [0.55338627, 0.13089971],
        [0.55708902, 0.13109058],
        [0.56079178, 0.13082336],
        [0.56449454, 0.13011713],
        [0.5681973, 0.129869],
        [0.57190006, 0.12992626],
        [0.57560282, 0.12942998],
        [0.57930558, 0.12796026],
        [0.58300835, 0.12862831],
        [0.58671112, 0.12656689],
        [0.59041389, 0.12734947],
        [0.59411664, 0.12509716],
        [0.59781941, 0.12110791],
        [0.60152218, 0.11839751],
        [0.60522496, 0.11244226],
        [0.60892772, 0.11307214],
        [0.61263048, 0.1092165],
        [0.61633325, 0.10683058],
        [0.62003603, 0.10433014],
        [0.6237388, 0.10530359],
        [0.62744156, 0.10056993],
        [0.63114433, 0.09950104],
        [0.63484711, 0.09854668],
        [0.63854988, 0.09921473],
        [0.64225265, 0.09541635],
        [0.64595543, 0.09980643],
        [0.64965823, 0.0986612],
        [0.653361, 0.09560722],
        [0.65706377, 0.09755413],
        [0.66076656, 0.09612258],
        [0.66446934, 0.09430929],
        [0.66817212, 0.09661885],
        [0.67187489, 0.09366032],
        [0.67557767, 0.09522548],
        [0.67928044, 0.09535909],
        [0.68298322, 0.09316404],
        [0.686686, 0.09450016],
        [0.69038878, 0.0930877],
        [0.69409156, 0.09343126],
        [0.69779433, 0.0932404],
        [0.70149709, 0.09350762],
        [0.70519988, 0.09339309],
        [0.70890264, 0.09291591],
        [0.7126054, 0.09303043],
        [0.71630818, 0.0926296],
        [0.72001095, 0.0932404],
        [0.72371371, 0.09261052],
        [0.72741648, 0.09249599],
        [0.73111925, 0.09240055],
        [0.73482204, 0.09253416],
        [0.7385248, 0.09209515],
        [0.74222757, 0.09234329],
        [0.74593034, 0.09366032],
        [0.74963312, 0.09333583],
        [0.75333589, 0.09322131],
        [0.75703868, 0.09264868],
        [0.76074146, 0.09253416],
        [0.76444422, 0.09243873],
        [0.76814698, 0.09230512],
        [0.77184976, 0.09310678],
        [0.77555253, 0.09165615],
        [0.77925531, 0.09159888],
        [0.78295807, 0.09207606],
        [0.78666085, 0.09175158],
        [0.79036364, 0.09177067],
        [0.79406641, 0.09236237],
        [0.79776918, 0.09241964],
        [0.80147197, 0.09320222],
        [0.80517474, 0.09199972],
        [0.80887751, 0.09167523],
        [0.81258028, 0.09322131],
        [0.81628304, 0.09190428],
        [0.81998581, 0.09167523],
        [0.82368858, 0.09285865],
        [0.82739136, 0.09180884],
        [0.83109411, 0.09150345],
        [0.83479688, 0.09186611],
        [0.83849965, 0.0920188],
        [0.84220242, 0.09320222],
        [0.84590519, 0.09131257],
        [0.84960797, 0.09117896],
        [0.85331075, 0.09133166],
        [0.85701353, 0.09089265],
        [0.86071631, 0.09058725],
        [0.86441907, 0.09051091],
        [0.86812186, 0.09033912],
        [0.87182464, 0.09041547],
        [0.87552742, 0.0911217],
        [0.87923019, 0.0894611],
        [0.88293296, 0.08999555],
        [0.88663573, 0.08921297],
        [0.89033849, 0.08881213],
        [0.89404126, 0.08797229],
        [0.89774404, 0.08709427],
        [0.9014468, 0.08503284],
        [1.0, 0.07601531],
    ]
)

pos_ocp = np.array(
    [
        [0.24879728, 4.4],
        [0.26614516, 4.2935653],
        [0.26886763, 4.2768621],
        [0.27159011, 4.2647018],
        [0.27431258, 4.2540312],
        [0.27703505, 4.2449446],
        [0.27975753, 4.2364879],
        [0.28248, 4.2302647],
        [0.28520247, 4.2225528],
        [0.28792495, 4.2182574],
        [0.29064743, 4.213294],
        [0.29336992, 4.2090373],
        [0.29609239, 4.2051239],
        [0.29881487, 4.2012677],
        [0.30153735, 4.1981564],
        [0.30425983, 4.1955218],
        [0.30698231, 4.1931167],
        [0.30970478, 4.1889744],
        [0.31242725, 4.1881533],
        [0.31514973, 4.1865883],
        [0.3178722, 4.1850228],
        [0.32059466, 4.1832285],
        [0.32331714, 4.1808805],
        [0.32603962, 4.1805749],
        [0.32876209, 4.1789522],
        [0.33148456, 4.1768146],
        [0.33420703, 4.1768146],
        [0.3369295, 4.1752872],
        [0.33965197, 4.173111],
        [0.34237446, 4.1726718],
        [0.34509694, 4.1710877],
        [0.34781941, 4.1702285],
        [0.3505419, 4.168797],
        [0.35326438, 4.1669831],
        [0.35598685, 4.1655135],
        [0.35870932, 4.1634517],
        [0.3614318, 4.1598248],
        [0.36415428, 4.1571712],
        [0.36687674, 4.154079],
        [0.36959921, 4.1504135],
        [0.37232169, 4.1466532],
        [0.37504418, 4.1423388],
        [0.37776665, 4.1382346],
        [0.38048913, 4.1338248],
        [0.38321161, 4.1305799],
        [0.38593408, 4.1272392],
        [0.38865655, 4.1228104],
        [0.39137903, 4.1186109],
        [0.39410151, 4.114182],
        [0.39682398, 4.1096005],
        [0.39954645, 4.1046948],
        [0.40226892, 4.1004758],
        [0.4049914, 4.0956464],
        [0.40771387, 4.0909696],
        [0.41043634, 4.0864644],
        [0.41315882, 4.0818448],
        [0.41588129, 4.077683],
        [0.41860377, 4.0733309],
        [0.42132624, 4.0690737],
        [0.42404872, 4.0647216],
        [0.4267712, 4.0608654],
        [0.42949368, 4.0564747],
        [0.43221616, 4.0527525],
        [0.43493864, 4.0492401],
        [0.43766111, 4.0450211],
        [0.44038359, 4.041986],
        [0.44310607, 4.0384736],
        [0.44582856, 4.035171],
        [0.44855103, 4.0320406],
        [0.45127351, 4.0289288],
        [0.453996, 4.02597],
        [0.45671848, 4.0227437],
        [0.45944095, 4.0199757],
        [0.46216343, 4.0175133],
        [0.46488592, 4.0149746],
        [0.46760838, 4.0122066],
        [0.47033085, 4.009954],
        [0.47305333, 4.0075679],
        [0.47577581, 4.0050669],
        [0.47849828, 4.0023184],
        [0.48122074, 3.9995501],
        [0.48394321, 3.9969349],
        [0.48666569, 3.9926589],
        [0.48938816, 3.9889555],
        [0.49211064, 3.9834003],
        [0.4948331, 3.9783037],
        [0.49755557, 3.9755929],
        [0.50027804, 3.9707632],
        [0.50300052, 3.9681098],
        [0.50572298, 3.9635665],
        [0.50844545, 3.9594433],
        [0.51116792, 3.9556634],
        [0.51389038, 3.9521511],
        [0.51661284, 3.9479132],
        [0.51933531, 3.9438281],
        [0.52205777, 3.9400866],
        [0.52478024, 3.9362304],
        [0.52750271, 3.9314201],
        [0.53022518, 3.9283848],
        [0.53294765, 3.9242232],
        [0.53567012, 3.9192028],
        [0.53839258, 3.9166257],
        [0.54111506, 3.9117961],
        [0.54383753, 3.90815],
        [0.54656, 3.9038739],
        [0.54928247, 3.8995597],
        [0.55200494, 3.8959136],
        [0.5547274, 3.8909314],
        [0.55744986, 3.8872662],
        [0.56017233, 3.8831048],
        [0.5628948, 3.8793442],
        [0.56561729, 3.8747628],
        [0.56833976, 3.8702576],
        [0.57106222, 3.8666878],
        [0.57378469, 3.8623927],
        [0.57650716, 3.8581741],
        [0.57922963, 3.854146],
        [0.5819521, 3.8499846],
        [0.58467456, 3.8450022],
        [0.58739702, 3.8422534],
        [0.59011948, 3.8380919],
        [0.59284194, 3.8341596],
        [0.5955644, 3.8309333],
        [0.59828687, 3.8272109],
        [0.60100935, 3.823164],
        [0.60373182, 3.8192315],
        [0.60645429, 3.8159864],
        [0.60917677, 3.8123021],
        [0.61189925, 3.8090379],
        [0.61462172, 3.8071671],
        [0.61734419, 3.8040555],
        [0.62006666, 3.8013639],
        [0.62278914, 3.7970879],
        [0.62551162, 3.7953317],
        [0.62823408, 3.7920673],
        [0.63095656, 3.788383],
        [0.63367903, 3.7855389],
        [0.6364015, 3.7838206],
        [0.63912397, 3.78111],
        [0.64184645, 3.7794874],
        [0.64456893, 3.7769294],
        [0.6472914, 3.773608],
        [0.65001389, 3.7695992],
        [0.65273637, 3.7690265],
        [0.65545884, 3.7662776],
        [0.65818131, 3.7642922],
        [0.66090379, 3.7626889],
        [0.66362625, 3.7603791],
        [0.66634874, 3.7575538],
        [0.66907121, 3.7552056],
        [0.67179369, 3.7533159],
        [0.67451616, 3.7507198],
        [0.67723865, 3.7487535],
        [0.67996113, 3.7471499],
        [0.68268361, 3.7442865],
        [0.68540608, 3.7423012],
        [0.68812855, 3.7400677],
        [0.69085103, 3.7385788],
        [0.6935735, 3.7345319],
        [0.69629597, 3.7339211],
        [0.69901843, 3.7301605],
        [0.7017409, 3.7301033],
        [0.70446338, 3.7278316],
        [0.70718585, 3.7251589],
        [0.70990833, 3.723861],
        [0.71263081, 3.7215703],
        [0.71535328, 3.7191267],
        [0.71807574, 3.7172751],
        [0.72079822, 3.7157097],
        [0.72352069, 3.7130945],
        [0.72624317, 3.7099447],
        [0.72896564, 3.7071004],
        [0.7316881, 3.7045615],
        [0.73441057, 3.703588],
        [0.73713303, 3.70208],
        [0.73985551, 3.7002664],
        [0.74257799, 3.6972122],
        [0.74530047, 3.6952841],
        [0.74802293, 3.6929362],
        [0.7507454, 3.6898055],
        [0.75346787, 3.6890991],
        [0.75619034, 3.686522],
        [0.75891281, 3.6849759],
        [0.76163529, 3.6821697],
        [0.76435776, 3.6808143],
        [0.76708024, 3.6786573],
        [0.7698027, 3.6761947],
        [0.77252517, 3.674763],
        [0.77524765, 3.6712887],
        [0.77797012, 3.6697233],
        [0.78069258, 3.6678908],
        [0.78341506, 3.6652565],
        [0.78613753, 3.6630611],
        [0.78885999, 3.660274],
        [0.79158246, 3.6583652],
        [0.79430494, 3.6554828],
        [0.79702741, 3.6522949],
        [0.79974987, 3.6499848],
        [0.80247234, 3.6470451],
        [0.8051948, 3.6405547],
        [0.80791727, 3.6383405],
        [0.81063974, 3.635076],
        [0.81336221, 3.633549],
        [0.81608468, 3.6322317],
        [0.81880714, 3.6306856],
        [0.82152961, 3.6283948],
        [0.82425208, 3.6268487],
        [0.82697453, 3.6243098],
        [0.829697, 3.6223626],
        [0.83241946, 3.6193655],
        [0.83514192, 3.6177621],
        [0.83786439, 3.6158531],
        [0.84058684, 3.6128371],
        [0.84330931, 3.6118062],
        [0.84603177, 3.6094582],
        [0.84875424, 3.6072438],
        [0.8514767, 3.6049912],
        [0.85419916, 3.6030822],
        [0.85692162, 3.6012688],
        [0.85964409, 3.5995889],
        [0.86236656, 3.5976417],
        [0.86508902, 3.5951984],
        [0.86781149, 3.593843],
        [0.87053395, 3.5916286],
        [0.87325642, 3.5894907],
        [0.87597888, 3.587429],
        [0.87870135, 3.5852909],
        [0.88142383, 3.5834775],
        [0.8841463, 3.5817785],
        [0.88686877, 3.5801177],
        [0.88959124, 3.5778842],
        [0.89231371, 3.5763381],
        [0.8950362, 3.5737801],
        [0.89775868, 3.5721002],
        [0.90048116, 3.5702102],
        [0.90320364, 3.5684922],
        [0.90592613, 3.5672133],
        [1.0, 3.52302167],
    ]
)

from pybamm import exp, constants


def graphite_LGM50_electrolyte_exchange_current_density_Chen2020(
    c_e, c_s_surf, c_n_max, T
):
    m_ref = 6.48e-7  # (A/m2)(m3/mol)**1.5 - includes ref concentrations
    E_r = 35000
    arrhenius = exp(E_r / constants.R * (1 / 298.15 - 1 / T))

    return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_n_max - c_s_surf) ** 0.5


def nmc_LGM50_electrolyte_exchange_current_density_Chen2020(c_e, c_s_surf, c_p_max, T):
    m_ref = 3.42e-6  # (A/m2)(m3/mol)**1.5 - includes ref concentrations
    E_r = 17800
    arrhenius = exp(E_r / constants.R * (1 / 298.15 - 1 / T))

    return m_ref * arrhenius * c_e**0.5 * c_s_surf**0.5 * (c_p_max - c_s_surf) ** 0.5


values = {
    "Negative electrode thickness [m]": 8.52e-05,
    "Separator thickness [m]": 1.2e-05,
    "Positive electrode thickness [m]": 7.56e-05,
    "Electrode height [m]": 0.065,
    "Electrode width [m]": 1.58,
    "Nominal cell capacity [A.h]": 5.0,
    "Typical current [A]": 5.0,
    "Current function [A]": 5.0,
    "Maximum concentration in negative electrode [mol.m-3]": 33133.0,
    "Negative electrode diffusivity [m2.s-1]": 3.3e-14,
    "Negative electrode OCP [V]": ("graphite_LGM50_ocp_Chen2020", neg_ocp),
    "Negative electrode porosity": 0.25,
    "Negative electrode active material volume fraction": 0.75,
    "Negative particle radius [m]": 5.86e-06,
    "Negative electrode Bruggeman coefficient (electrolyte)": 1.5,
    "Negative electrode Bruggeman coefficient (electrode)": 1.5,
    "Negative electrode electrons in reaction": 1.0,
    "Negative electrode exchange-current density [A.m-2]": graphite_LGM50_electrolyte_exchange_current_density_Chen2020,
    "Negative electrode OCP entropic change [V.K-1]": 0.0,
    "Maximum concentration in positive electrode [mol.m-3]": 63104.0,
    "Positive electrode diffusivity [m2.s-1]": 4e-15,
    "Positive electrode OCP [V]": ("nmc_LGM50_ocp_Chen2020", pos_ocp),
    "Positive electrode porosity": 0.335,
    "Positive electrode active material volume fraction": 0.665,
    "Positive particle radius [m]": 5.22e-06,
    "Positive electrode Bruggeman coefficient (electrolyte)": 1.5,
    "Positive electrode Bruggeman coefficient (electrode)": 1.5,
    "Positive electrode electrons in reaction": 1.0,
    "Positive electrode exchange-current density [A.m-2]": nmc_LGM50_electrolyte_exchange_current_density_Chen2020,
    "Positive electrode OCP entropic change [V.K-1]": 0.0,
    "Separator porosity": 0.47,
    "Separator Bruggeman coefficient (electrolyte)": 1.5,
    "Typical electrolyte concentration [mol.m-3]": 1000.0,
    "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.5,
    "Upper voltage cut-off [V]": 4.4,
    "Initial concentration in electrolyte [mol.m-3]": 1000,
    "Initial concentration in negative electrode [mol.m-3]": 29866.0,
    "Initial concentration in positive electrode [mol.m-3]": 17038.0,
    "Initial temperature [K]": 298.15,
}
param = pybamm.ParameterValues(values)
param
[16]:
{'Ambient temperature [K]': 298.15,
 'Boltzmann constant [J.K-1]': 1.380649e-23,
 'Current function [A]': 5.0,
 'Electrode height [m]': 0.065,
 'Electrode width [m]': 1.58,
 'Electron charge [C]': 1.602176634e-19,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'Initial concentration in electrolyte [mol.m-3]': 1000,
 'Initial concentration in negative electrode [mol.m-3]': 29866.0,
 'Initial concentration in positive electrode [mol.m-3]': 17038.0,
 'Initial temperature [K]': 298.15,
 'Lower voltage cut-off [V]': 2.5,
 'Maximum concentration in negative electrode [mol.m-3]': 33133.0,
 'Maximum concentration in positive electrode [mol.m-3]': 63104.0,
 'Negative electrode Bruggeman coefficient (electrode)': 1.5,
 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Negative electrode OCP [V]': ('graphite_LGM50_ocp_Chen2020',
                                ([array([0.        , 0.03129623, 0.03499902, 0.0387018 , 0.04240458,
       0.04610736, 0.04981015, 0.05351292, 0.05721568, 0.06091845,
       0.06462122, 0.06832399, 0.07202675, 0.07572951, 0.07943227,
       0.08313503, 0.08683779, 0.09054054, 0.09424331, 0.09794607,
       0.10164883, 0.10535158, 0.10905434, 0.1127571 , 0.11645985,
       0.12016261, 0.12386536, 0.12756811, 0.13127086, 0.13497362,
       0.13867638, 0.14237913, 0.14608189, 0.14978465, 0.15348741,
       0.15719018, 0.16089294, 0.1645957 , 0.16829847, 0.17200122,
       0.17570399, 0.17940674, 0.1831095 , 0.18681229, 0.19051504,
       0.1942178 , 0.19792056, 0.20162334, 0.2053261 , 0.20902886,
       0.21273164, 0.2164344 , 0.22013716, 0.22383993, 0.2275427 ,
       0.23124547, 0.23494825, 0.23865101, 0.24235377, 0.24605653,
       0.2497593 , 0.25346208, 0.25716486, 0.26086762, 0.26457039,
       0.26827314, 0.2719759 , 0.27567867, 0.27938144, 0.28308421,
       0.28678698, 0.29048974, 0.29419251, 0.29789529, 0.30159806,
       0.30530083, 0.30900361, 0.31270637, 0.31640913, 0.32011189,
       0.32381466, 0.32751744, 0.33122021, 0.33492297, 0.33862575,
       0.34232853, 0.34603131, 0.34973408, 0.35343685, 0.35713963,
       0.36084241, 0.36454517, 0.36824795, 0.37195071, 0.37565348,
       0.37935626, 0.38305904, 0.38676182, 0.3904646 , 0.39416737,
       0.39787015, 0.40157291, 0.40527567, 0.40897844, 0.41268121,
       0.41638398, 0.42008676, 0.42378953, 0.4274923 , 0.43119506,
       0.43489784, 0.43860061, 0.44230338, 0.44600615, 0.44970893,
       0.45341168, 0.45711444, 0.46081719, 0.46451994, 0.46822269,
       0.47192545, 0.47562821, 0.47933098, 0.48303375, 0.48673651,
       0.49043926, 0.49414203, 0.49784482, 0.50154759, 0.50525036,
       0.50895311, 0.51265586, 0.51635861, 0.52006139, 0.52376415,
       0.52746692, 0.53116969, 0.53487245, 0.53857521, 0.54227797,
       0.54598074, 0.5496835 , 0.55338627, 0.55708902, 0.56079178,
       0.56449454, 0.5681973 , 0.57190006, 0.57560282, 0.57930558,
       0.58300835, 0.58671112, 0.59041389, 0.59411664, 0.59781941,
       0.60152218, 0.60522496, 0.60892772, 0.61263048, 0.61633325,
       0.62003603, 0.6237388 , 0.62744156, 0.63114433, 0.63484711,
       0.63854988, 0.64225265, 0.64595543, 0.64965823, 0.653361  ,
       0.65706377, 0.66076656, 0.66446934, 0.66817212, 0.67187489,
       0.67557767, 0.67928044, 0.68298322, 0.686686  , 0.69038878,
       0.69409156, 0.69779433, 0.70149709, 0.70519988, 0.70890264,
       0.7126054 , 0.71630818, 0.72001095, 0.72371371, 0.72741648,
       0.73111925, 0.73482204, 0.7385248 , 0.74222757, 0.74593034,
       0.74963312, 0.75333589, 0.75703868, 0.76074146, 0.76444422,
       0.76814698, 0.77184976, 0.77555253, 0.77925531, 0.78295807,
       0.78666085, 0.79036364, 0.79406641, 0.79776918, 0.80147197,
       0.80517474, 0.80887751, 0.81258028, 0.81628304, 0.81998581,
       0.82368858, 0.82739136, 0.83109411, 0.83479688, 0.83849965,
       0.84220242, 0.84590519, 0.84960797, 0.85331075, 0.85701353,
       0.86071631, 0.86441907, 0.86812186, 0.87182464, 0.87552742,
       0.87923019, 0.88293296, 0.88663573, 0.89033849, 0.89404126,
       0.89774404, 0.9014468 , 1.        ])],
                                 array([1.81772748, 1.0828807 , 0.99593794, 0.90023398, 0.79649431,
       0.73354429, 0.66664314, 0.64137149, 0.59813869, 0.5670836 ,
       0.54746181, 0.53068399, 0.51304734, 0.49394092, 0.47926274,
       0.46065259, 0.45992726, 0.43801501, 0.42438665, 0.41150269,
       0.40033659, 0.38957134, 0.37756538, 0.36292541, 0.34357086,
       0.3406314 , 0.32299468, 0.31379458, 0.30795386, 0.29207319,
       0.28697687, 0.27405477, 0.2670497 , 0.25857493, 0.25265783,
       0.24826777, 0.2414345 , 0.23362778, 0.22956218, 0.22370236,
       0.22181271, 0.22089651, 0.2194268 , 0.21830064, 0.21845333,
       0.21753715, 0.21719357, 0.21635373, 0.21667822, 0.21738444,
       0.21469313, 0.21541846, 0.21465495, 0.2135479 , 0.21392964,
       0.21074206, 0.20873788, 0.20465319, 0.20205732, 0.19774358,
       0.19444147, 0.19190285, 0.18850531, 0.18581399, 0.18327537,
       0.18157659, 0.17814088, 0.17529686, 0.1719375 , 0.16934161,
       0.16756649, 0.16609676, 0.16414985, 0.16260378, 0.16224113,
       0.160027  , 0.15827096, 0.1588054 , 0.15552238, 0.15580869,
       0.15220118, 0.1511132 , 0.14987253, 0.14874637, 0.14678037,
       0.14620776, 0.14555879, 0.14389819, 0.14359279, 0.14242846,
       0.14038612, 0.13882096, 0.13954628, 0.13946992, 0.13780934,
       0.13973714, 0.13698858, 0.13523254, 0.13441178, 0.1352898 ,
       0.13507985, 0.13647321, 0.13601512, 0.13435452, 0.1334765 ,
       0.1348317 , 0.13275118, 0.13286571, 0.13263667, 0.13456447,
       0.13471718, 0.13395369, 0.13448814, 0.1334765 , 0.13298023,
       0.13259849, 0.13338107, 0.13309476, 0.13275118, 0.13443087,
       0.13315202, 0.132713  , 0.1330184 , 0.13278936, 0.13225491,
       0.13317111, 0.13263667, 0.13187316, 0.13265574, 0.13250305,
       0.13324745, 0.13204496, 0.13242669, 0.13233127, 0.13198769,
       0.13254122, 0.13145325, 0.13298023, 0.13168229, 0.1313578 ,
       0.13235036, 0.13120511, 0.13089971, 0.13109058, 0.13082336,
       0.13011713, 0.129869  , 0.12992626, 0.12942998, 0.12796026,
       0.12862831, 0.12656689, 0.12734947, 0.12509716, 0.12110791,
       0.11839751, 0.11244226, 0.11307214, 0.1092165 , 0.10683058,
       0.10433014, 0.10530359, 0.10056993, 0.09950104, 0.09854668,
       0.09921473, 0.09541635, 0.09980643, 0.0986612 , 0.09560722,
       0.09755413, 0.09612258, 0.09430929, 0.09661885, 0.09366032,
       0.09522548, 0.09535909, 0.09316404, 0.09450016, 0.0930877 ,
       0.09343126, 0.0932404 , 0.09350762, 0.09339309, 0.09291591,
       0.09303043, 0.0926296 , 0.0932404 , 0.09261052, 0.09249599,
       0.09240055, 0.09253416, 0.09209515, 0.09234329, 0.09366032,
       0.09333583, 0.09322131, 0.09264868, 0.09253416, 0.09243873,
       0.09230512, 0.09310678, 0.09165615, 0.09159888, 0.09207606,
       0.09175158, 0.09177067, 0.09236237, 0.09241964, 0.09320222,
       0.09199972, 0.09167523, 0.09322131, 0.09190428, 0.09167523,
       0.09285865, 0.09180884, 0.09150345, 0.09186611, 0.0920188 ,
       0.09320222, 0.09131257, 0.09117896, 0.09133166, 0.09089265,
       0.09058725, 0.09051091, 0.09033912, 0.09041547, 0.0911217 ,
       0.0894611 , 0.08999555, 0.08921297, 0.08881213, 0.08797229,
       0.08709427, 0.08503284, 0.07601531]))),
 'Negative electrode OCP entropic change [V.K-1]': 0.0,
 'Negative electrode active material volume fraction': 0.75,
 'Negative electrode diffusivity [m2.s-1]': 3.3e-14,
 'Negative electrode electrons in reaction': 1.0,
 'Negative electrode exchange-current density [A.m-2]': <function graphite_LGM50_electrolyte_exchange_current_density_Chen2020 at 0x7f481dd1f380>,
 'Negative electrode porosity': 0.25,
 'Negative electrode thickness [m]': 8.52e-05,
 'Negative particle radius [m]': 5.86e-06,
 'Nominal cell capacity [A.h]': 5.0,
 'Number of cells connected in series to make a battery': 1.0,
 'Number of electrodes connected in parallel to make a cell': 1.0,
 'Positive electrode Bruggeman coefficient (electrode)': 1.5,
 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Positive electrode OCP [V]': ('nmc_LGM50_ocp_Chen2020',
                                ([array([0.24879728, 0.26614516, 0.26886763, 0.27159011, 0.27431258,
       0.27703505, 0.27975753, 0.28248   , 0.28520247, 0.28792495,
       0.29064743, 0.29336992, 0.29609239, 0.29881487, 0.30153735,
       0.30425983, 0.30698231, 0.30970478, 0.31242725, 0.31514973,
       0.3178722 , 0.32059466, 0.32331714, 0.32603962, 0.32876209,
       0.33148456, 0.33420703, 0.3369295 , 0.33965197, 0.34237446,
       0.34509694, 0.34781941, 0.3505419 , 0.35326438, 0.35598685,
       0.35870932, 0.3614318 , 0.36415428, 0.36687674, 0.36959921,
       0.37232169, 0.37504418, 0.37776665, 0.38048913, 0.38321161,
       0.38593408, 0.38865655, 0.39137903, 0.39410151, 0.39682398,
       0.39954645, 0.40226892, 0.4049914 , 0.40771387, 0.41043634,
       0.41315882, 0.41588129, 0.41860377, 0.42132624, 0.42404872,
       0.4267712 , 0.42949368, 0.43221616, 0.43493864, 0.43766111,
       0.44038359, 0.44310607, 0.44582856, 0.44855103, 0.45127351,
       0.453996  , 0.45671848, 0.45944095, 0.46216343, 0.46488592,
       0.46760838, 0.47033085, 0.47305333, 0.47577581, 0.47849828,
       0.48122074, 0.48394321, 0.48666569, 0.48938816, 0.49211064,
       0.4948331 , 0.49755557, 0.50027804, 0.50300052, 0.50572298,
       0.50844545, 0.51116792, 0.51389038, 0.51661284, 0.51933531,
       0.52205777, 0.52478024, 0.52750271, 0.53022518, 0.53294765,
       0.53567012, 0.53839258, 0.54111506, 0.54383753, 0.54656   ,
       0.54928247, 0.55200494, 0.5547274 , 0.55744986, 0.56017233,
       0.5628948 , 0.56561729, 0.56833976, 0.57106222, 0.57378469,
       0.57650716, 0.57922963, 0.5819521 , 0.58467456, 0.58739702,
       0.59011948, 0.59284194, 0.5955644 , 0.59828687, 0.60100935,
       0.60373182, 0.60645429, 0.60917677, 0.61189925, 0.61462172,
       0.61734419, 0.62006666, 0.62278914, 0.62551162, 0.62823408,
       0.63095656, 0.63367903, 0.6364015 , 0.63912397, 0.64184645,
       0.64456893, 0.6472914 , 0.65001389, 0.65273637, 0.65545884,
       0.65818131, 0.66090379, 0.66362625, 0.66634874, 0.66907121,
       0.67179369, 0.67451616, 0.67723865, 0.67996113, 0.68268361,
       0.68540608, 0.68812855, 0.69085103, 0.6935735 , 0.69629597,
       0.69901843, 0.7017409 , 0.70446338, 0.70718585, 0.70990833,
       0.71263081, 0.71535328, 0.71807574, 0.72079822, 0.72352069,
       0.72624317, 0.72896564, 0.7316881 , 0.73441057, 0.73713303,
       0.73985551, 0.74257799, 0.74530047, 0.74802293, 0.7507454 ,
       0.75346787, 0.75619034, 0.75891281, 0.76163529, 0.76435776,
       0.76708024, 0.7698027 , 0.77252517, 0.77524765, 0.77797012,
       0.78069258, 0.78341506, 0.78613753, 0.78885999, 0.79158246,
       0.79430494, 0.79702741, 0.79974987, 0.80247234, 0.8051948 ,
       0.80791727, 0.81063974, 0.81336221, 0.81608468, 0.81880714,
       0.82152961, 0.82425208, 0.82697453, 0.829697  , 0.83241946,
       0.83514192, 0.83786439, 0.84058684, 0.84330931, 0.84603177,
       0.84875424, 0.8514767 , 0.85419916, 0.85692162, 0.85964409,
       0.86236656, 0.86508902, 0.86781149, 0.87053395, 0.87325642,
       0.87597888, 0.87870135, 0.88142383, 0.8841463 , 0.88686877,
       0.88959124, 0.89231371, 0.8950362 , 0.89775868, 0.90048116,
       0.90320364, 0.90592613, 1.        ])],
                                 array([4.4       , 4.2935653 , 4.2768621 , 4.2647018 , 4.2540312 ,
       4.2449446 , 4.2364879 , 4.2302647 , 4.2225528 , 4.2182574 ,
       4.213294  , 4.2090373 , 4.2051239 , 4.2012677 , 4.1981564 ,
       4.1955218 , 4.1931167 , 4.1889744 , 4.1881533 , 4.1865883 ,
       4.1850228 , 4.1832285 , 4.1808805 , 4.1805749 , 4.1789522 ,
       4.1768146 , 4.1768146 , 4.1752872 , 4.173111  , 4.1726718 ,
       4.1710877 , 4.1702285 , 4.168797  , 4.1669831 , 4.1655135 ,
       4.1634517 , 4.1598248 , 4.1571712 , 4.154079  , 4.1504135 ,
       4.1466532 , 4.1423388 , 4.1382346 , 4.1338248 , 4.1305799 ,
       4.1272392 , 4.1228104 , 4.1186109 , 4.114182  , 4.1096005 ,
       4.1046948 , 4.1004758 , 4.0956464 , 4.0909696 , 4.0864644 ,
       4.0818448 , 4.077683  , 4.0733309 , 4.0690737 , 4.0647216 ,
       4.0608654 , 4.0564747 , 4.0527525 , 4.0492401 , 4.0450211 ,
       4.041986  , 4.0384736 , 4.035171  , 4.0320406 , 4.0289288 ,
       4.02597   , 4.0227437 , 4.0199757 , 4.0175133 , 4.0149746 ,
       4.0122066 , 4.009954  , 4.0075679 , 4.0050669 , 4.0023184 ,
       3.9995501 , 3.9969349 , 3.9926589 , 3.9889555 , 3.9834003 ,
       3.9783037 , 3.9755929 , 3.9707632 , 3.9681098 , 3.9635665 ,
       3.9594433 , 3.9556634 , 3.9521511 , 3.9479132 , 3.9438281 ,
       3.9400866 , 3.9362304 , 3.9314201 , 3.9283848 , 3.9242232 ,
       3.9192028 , 3.9166257 , 3.9117961 , 3.90815   , 3.9038739 ,
       3.8995597 , 3.8959136 , 3.8909314 , 3.8872662 , 3.8831048 ,
       3.8793442 , 3.8747628 , 3.8702576 , 3.8666878 , 3.8623927 ,
       3.8581741 , 3.854146  , 3.8499846 , 3.8450022 , 3.8422534 ,
       3.8380919 , 3.8341596 , 3.8309333 , 3.8272109 , 3.823164  ,
       3.8192315 , 3.8159864 , 3.8123021 , 3.8090379 , 3.8071671 ,
       3.8040555 , 3.8013639 , 3.7970879 , 3.7953317 , 3.7920673 ,
       3.788383  , 3.7855389 , 3.7838206 , 3.78111   , 3.7794874 ,
       3.7769294 , 3.773608  , 3.7695992 , 3.7690265 , 3.7662776 ,
       3.7642922 , 3.7626889 , 3.7603791 , 3.7575538 , 3.7552056 ,
       3.7533159 , 3.7507198 , 3.7487535 , 3.7471499 , 3.7442865 ,
       3.7423012 , 3.7400677 , 3.7385788 , 3.7345319 , 3.7339211 ,
       3.7301605 , 3.7301033 , 3.7278316 , 3.7251589 , 3.723861  ,
       3.7215703 , 3.7191267 , 3.7172751 , 3.7157097 , 3.7130945 ,
       3.7099447 , 3.7071004 , 3.7045615 , 3.703588  , 3.70208   ,
       3.7002664 , 3.6972122 , 3.6952841 , 3.6929362 , 3.6898055 ,
       3.6890991 , 3.686522  , 3.6849759 , 3.6821697 , 3.6808143 ,
       3.6786573 , 3.6761947 , 3.674763  , 3.6712887 , 3.6697233 ,
       3.6678908 , 3.6652565 , 3.6630611 , 3.660274  , 3.6583652 ,
       3.6554828 , 3.6522949 , 3.6499848 , 3.6470451 , 3.6405547 ,
       3.6383405 , 3.635076  , 3.633549  , 3.6322317 , 3.6306856 ,
       3.6283948 , 3.6268487 , 3.6243098 , 3.6223626 , 3.6193655 ,
       3.6177621 , 3.6158531 , 3.6128371 , 3.6118062 , 3.6094582 ,
       3.6072438 , 3.6049912 , 3.6030822 , 3.6012688 , 3.5995889 ,
       3.5976417 , 3.5951984 , 3.593843  , 3.5916286 , 3.5894907 ,
       3.587429  , 3.5852909 , 3.5834775 , 3.5817785 , 3.5801177 ,
       3.5778842 , 3.5763381 , 3.5737801 , 3.5721002 , 3.5702102 ,
       3.5684922 , 3.5672133 , 3.52302167]))),
 'Positive electrode OCP entropic change [V.K-1]': 0.0,
 'Positive electrode active material volume fraction': 0.665,
 'Positive electrode diffusivity [m2.s-1]': 4e-15,
 'Positive electrode electrons in reaction': 1.0,
 'Positive electrode exchange-current density [A.m-2]': <function nmc_LGM50_electrolyte_exchange_current_density_Chen2020 at 0x7f481dd1f240>,
 'Positive electrode porosity': 0.335,
 'Positive electrode thickness [m]': 7.56e-05,
 'Positive particle radius [m]': 5.22e-06,
 'Reference temperature [K]': 298.15,
 'Separator Bruggeman coefficient (electrolyte)': 1.5,
 'Separator porosity': 0.47,
 'Separator thickness [m]': 1.2e-05,
 'Typical current [A]': 5.0,
 'Typical electrolyte concentration [mol.m-3]': 1000.0,
 'Upper voltage cut-off [V]': 4.4}

Here we would have got the same result by doing

[17]:
param_same = pybamm.ParameterValues("Chen2020")
{k: v for k, v in param_same.items() if k in spm.get_parameter_info()}
[17]:
{'Ideal gas constant [J.K-1.mol-1]': 8.314462618,
 'Faraday constant [C.mol-1]': 96485.33212,
 'Negative electrode thickness [m]': 8.52e-05,
 'Separator thickness [m]': 1.2e-05,
 'Positive electrode thickness [m]': 7.56e-05,
 'Electrode height [m]': 0.065,
 'Electrode width [m]': 1.58,
 'Nominal cell capacity [A.h]': 5.0,
 'Current function [A]': 5.0,
 'Maximum concentration in negative electrode [mol.m-3]': 33133.0,
 'Negative electrode diffusivity [m2.s-1]': 3.3e-14,
 'Negative electrode OCP [V]': <function pybamm.input.parameters.lithium_ion.Chen2020.graphite_LGM50_ocp_Chen2020(sto)>,
 'Negative electrode porosity': 0.25,
 'Negative electrode active material volume fraction': 0.75,
 'Negative particle radius [m]': 5.86e-06,
 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Negative electrode Bruggeman coefficient (electrode)': 0,
 'Negative electrode exchange-current density [A.m-2]': <function pybamm.input.parameters.lithium_ion.Chen2020.graphite_LGM50_electrolyte_exchange_current_density_Chen2020(c_e, c_s_surf, c_s_max, T)>,
 'Negative electrode OCP entropic change [V.K-1]': 0.0,
 'Maximum concentration in positive electrode [mol.m-3]': 63104.0,
 'Positive electrode diffusivity [m2.s-1]': 4e-15,
 'Positive electrode OCP [V]': <function pybamm.input.parameters.lithium_ion.Chen2020.nmc_LGM50_ocp_Chen2020(sto)>,
 'Positive electrode porosity': 0.335,
 'Positive electrode active material volume fraction': 0.665,
 'Positive particle radius [m]': 5.22e-06,
 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Positive electrode Bruggeman coefficient (electrode)': 0,
 'Positive electrode exchange-current density [A.m-2]': <function pybamm.input.parameters.lithium_ion.Chen2020.nmc_LGM50_electrolyte_exchange_current_density_Chen2020(c_e, c_s_surf, c_s_max, T)>,
 'Positive electrode OCP entropic change [V.K-1]': 0.0,
 'Separator porosity': 0.47,
 'Separator Bruggeman coefficient (electrolyte)': 1.5,
 'Initial concentration in electrolyte [mol.m-3]': 1000.0,
 '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.5,
 'Upper voltage cut-off [V]': 4.2,
 'Initial concentration in negative electrode [mol.m-3]': 29866.0,
 'Initial concentration in positive electrode [mol.m-3]': 17038.0}

Updating a specific parameter#

Once a parameter set has been defined (either via a dictionary or a pre-built set), single parameters can be updated

Using a constant value:#
[18]:
param.search("Current function [A]")

param.update({"Current function [A]": 4.0})

param["Current function [A]"]
Current function [A]    5.0
[18]:
4.0
Using a function:#
[19]:
def curren_func(time):
    return 1 + pybamm.sin(2 * np.pi * time / 60)


param.update({"Current function [A]": curren_func})

param["Current function [A]"]
[19]:
<function __main__.curren_func(time)>

Plotting parameter functions#

As seen above, functions can be passed as parameter values. These parameter values can then be plotted by using pybamm.plot

Plotting “Current function [A]”#
[20]:
currentfunc = param["Current function [A]"]
time = pybamm.linspace(0, 120, 60)
evaluated = param.evaluate(currentfunc(time))
evaluated = pybamm.Array(evaluated)
pybamm.plot(time, evaluated)
_images/source_examples_notebooks_parameterization_parameterization_47_0.png
[20]:
<Axes: >

Taking another such example:

Plotting “Negative electrode exchange-current density [A.m-2]”#
[21]:
negative_electrode_exchange_current_density = param[
    "Negative electrode exchange-current density [A.m-2]"
]
x = pybamm.linspace(3000, 6000, 100)
c_n_max = param["Maximum concentration in negative electrode [mol.m-3]"]
evaluated = param.evaluate(
    negative_electrode_exchange_current_density(1000, x, c_n_max, 300)
)
evaluated = pybamm.Array(evaluated)
pybamm.plot(x, evaluated)
_images/source_examples_notebooks_parameterization_parameterization_49_0.png
[21]:
<Axes: >

Simulating and solving the model#

Finally we can simulate the model and solve it using pybamm.Simulation and solve respectively.

[22]:
sim = pybamm.Simulation(spm, parameter_values=param)
t_eval = np.arange(0, 3600, 1)
sim.solve(t_eval=t_eval)
sim.plot()
[22]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f481ffd4990>

References#

The relevant papers for this notebook are:

[23]:
pybamm.print_citations()
[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.
[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.
[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.
[4] 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.
[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.
[6] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Callbacks#

WARNING: This is a new, experimental feature, and the API may change in future.

Callbacks provide hooks for users to interact with the different parts of the PyBaMM pipeline, for example to log, save, or visualize outputs of intermediate functions.

A list of available callbacks can be found in the API docs. Any number of callbacks can be provided as a list, and they will each be called in turn in the order provided.

The base class `pybamm.callbacks.Callback <https://docs.pybamm.org/en/latest/source/api/callbacks.html#pybamm.callbacks.Callback>`__ documents the available callback methods, at which point in the pipeline they are called, and what arguments are passed to them.

[1]:
%pip install "pybamm[plot,cite]" -q
import pybamm

model = pybamm.lithium_ion.DFN()
experiment = pybamm.Experiment(
    [
        (
            "Discharge at C/5 for 10 hours or until 3.3 V",
            "Charge at 1 A until 4.1 V",
            "Hold at 4.1 V until 10 mA",
        ),
    ]
    * 3
)
sim = pybamm.Simulation(model, experiment=experiment)
Note: you may need to restart the kernel to use updated packages.

Logging callback#

The LoggingCallback can be used to log the progress of the simulation. The default is to print to stdout (this callback is automatically created if no other LoggingCallback exists)

[2]:
pybamm.set_logging_level("NOTICE")
sim.solve();
2022-03-10 10:17:57.074 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/3 (18.917 ms elapsed) --------------------
2022-03-10 10:17:57.074 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:17:58.139 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:17:58.505 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:17:58.952 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/3 (1.898 s elapsed) --------------------
2022-03-10 10:17:58.953 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:00.203 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:00.464 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:00.675 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/3 (3.620 s elapsed) --------------------
2022-03-10 10:18:00.675 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:01.963 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:02.232 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:02.452 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.620 s

or to a separate file

[3]:
callback = pybamm.callbacks.LoggingCallback("output.log")
sim.solve(callbacks=callback)

# Read the file that has been written, which was saved to callback.logfile
with open(callback.logfile) as f:
    print(f.read())

# Remove the log file
import os

os.remove(callback.logfile)
2022-03-10 10:18:02.483 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/3 (24.357 ms elapsed) --------------------
2022-03-10 10:18:02.483 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:03.799 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:04.065 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:04.394 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/3 (1.935 s elapsed) --------------------
2022-03-10 10:18:04.394 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:05.607 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:05.858 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:06.059 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/3 (3.600 s elapsed) --------------------
2022-03-10 10:18:06.059 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:07.280 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:07.539 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:07.743 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.600 s

Custom callbacks#

Custom callbacks should subclass the class pybamm.callbacks.Callback class, and implement a subset of the callback methods on_<event>, which all take as input the dictionary logs.

[4]:
class CustomCallback(pybamm.callbacks.Callback):
    def on_experiment_end(self, logs):
        print(f"We are at the end of the simulation. Logs are {logs}")
[5]:
# Note the default `LoggingCallback` is also called
sim.solve(callbacks=CustomCallback());
2022-03-10 10:18:07.776 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/3 (24.415 ms elapsed) --------------------
2022-03-10 10:18:07.776 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:09.125 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:09.390 - [NOTICE] callbacks.on_step_start(182): Cycle 1/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:09.714 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/3 (1.963 s elapsed) --------------------
2022-03-10 10:18:09.714 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:11.025 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:11.288 - [NOTICE] callbacks.on_step_start(182): Cycle 2/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:11.498 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/3 (3.747 s elapsed) --------------------
2022-03-10 10:18:11.499 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 1/3: Discharge at C/5 for 10 hours or until 3.3 V
2022-03-10 10:18:12.748 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 2/3: Charge at 1 A until 4.1 V
2022-03-10 10:18:13.007 - [NOTICE] callbacks.on_step_start(182): Cycle 3/3, step 3/3: Hold at 4.1 V until 10 mA
2022-03-10 10:18:13.230 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.747 s
We are at the end of the simulation of the simulation. Logs are {'stopping conditions': {'voltage': None, 'capacity': None}, 'cycle number': (3, 3), 'elapsed time': pybamm.TimerTime(3.7469801669999967), 'step number': (3, 3), 'step operating conditions': 'Hold at 4.1 V until 10 mA', 'termination': 'event: Current cut-off (negative) [A] [experiment]', 'summary variables': {'Minimum measured discharge capacity [A.h]': -0.16806782223941116, 'Maximum measured discharge capacity [A.h]': 0.6889979156579616, 'Measured capacity [A.h]': 0.8570657378973728, 'Minimum voltage [V]': 3.300010000000005, 'Maximum voltage [V]': 4.100000000000016, 'Positive electrode capacity [A.h]': 1.9464430360887066, 'Change in positive electrode capacity [A.h]': 0.0, 'Loss of active material in positive electrode [%]': 1.1102230246251565e-14, 'Change in loss of active material in positive electrode [%]': 0.0, 'Loss of lithium inventory [%]': -2.220446049250313e-14, 'Change in loss of lithium inventory [%]': 0.0, 'Loss of lithium inventory, including electrolyte [%]': 0.0, 'Change in loss of lithium inventory, including electrolyte [%]': 0.0, 'Total lithium [mol]': 0.0799932053645051, 'Change in total lithium [mol]': 0.0, 'Total lithium in electrolyte [mol]': 0.002410514999999987, 'Change in total lithium in electrolyte [mol]': -2.168404344971009e-18, 'Total lithium in positive electrode [mol]': 0.037303833837759315, 'Change in total lithium in positive electrode [mol]': 6.938893903907228e-18, 'Total lithium in particles [mol]': 0.07758269036450512, 'Change in total lithium in particles [mol]': 0.0, 'Total lithium lost [mol]': 0.0, 'Change in total lithium lost [mol]': 0.0, 'Total lithium lost from particles [mol]': -1.3877787807814457e-17, 'Change in total lithium lost from particles [mol]': 0.0, 'Total lithium lost from electrolyte [mol]': 1.2576745200831851e-17, 'Change in total lithium lost from electrolyte [mol]': 2.168404344971009e-18, 'Loss of lithium to SEI [mol]': 0.0, 'Change in loss of lithium to SEI [mol]': 0.0, 'Loss of capacity to SEI [A.h]': 0.0, 'Change in loss of capacity to SEI [A.h]': 0.0, 'Total lithium lost to side reactions [mol]': 0.0, 'Change in total lithium lost to side reactions [mol]': 0.0, 'Total capacity lost to side reactions [A.h]': 0.0, 'Change in total capacity lost to side reactions [A.h]': 0.0, 'Local ECM resistance [Ohm]': -0.1921801399643974, 'Change in local ECM resistance [Ohm]': -0.3385111526792064, 'Negative electrode capacity [A.h]': 1.139331489107912, 'Change in negative electrode capacity [A.h]': 0.0, 'Loss of active material in negative electrode [%]': -2.220446049250313e-14, 'Change in loss of active material in negative electrode [%]': 0.0, 'Total lithium in negative electrode [mol]': 0.0402788565267458, 'Change in total lithium in negative electrode [mol]': -6.938893903907228e-18, 'Loss of lithium to lithium plating [mol]': 0.0, 'Change in loss of lithium to lithium plating [mol]': 0.0, 'Loss of capacity to lithium plating [A.h]': 0.0, 'Change in loss of capacity to lithium plating [A.h]': 0.0, 'x_100': 0.9493038520218161, 'y_100': 0.5126064431891194, 'C': 0.8728195070455337, 'x_0': 0.18322346594476246, 'y_0': 0.9610241419672079, 'Un(x_100)': 0.07517904666112207, 'Un(x_0)': 0.2527477655253842, 'Up(y_100)': 4.175179046661121, 'Up(y_0)': 3.357747765525383, 'Up(y_100) - Un(x_100)': 4.099999999999999, 'Up(y_0) - Un(x_0)': 3.1049999999999986, 'n_Li_100': 0.07758269036450512, 'n_Li_0': 0.07758269036450512, 'n_Li': 0.07758269036450512, 'C_n': 1.139331489107912, 'C_p': 1.9464430360887066, 'C_n * (x_100 - x_0)': 0.8728195070455337, 'C_p * (y_100 - y_0)': 0.8728195070455337, 'Capacity [A.h]': 0.8728195070455337}}

References#

The relevant papers for this notebook are:

[6]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] 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.
[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.
[7] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Custom experiments#

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
Note: you may need to restart the kernel to use updated packages.

Custom steps#

This feature is in development

Custom termination#

Termination of a step can be specified using a few standard strings (e.g. “4.2V” for voltage, “1 A” for current, “C/2” for C-rate), or via a custom termination step. The custom termination step can be specified based on any variable in the model. Below, we show an example where we specify a custom termination step based on keeping the anode potential above 0V, which is a common limit used to avoid lithium plating,

[2]:
# Set up model and parameters
model = pybamm.lithium_ion.DFN()
# add anode potential as a variable
# we use the potential at the separator interface since that is the minimum potential
# during charging (plating is most likely to occur first at the separator interface)
model.variables["Anode potential [V]"] = model.variables[
    "Negative electrode surface potential difference at separator interface [V]"
]
parameter_values = pybamm.ParameterValues("Chen2020")


# Create a custom termination event for the anode potential cut-off at 0.02V
# We use 0.02V instead of 0V to give a safety factor
def anode_potential_cutoff(variables):
    return variables["Anode potential [V]"] - 0.02


# The CustomTermination class takes a name and function
anode_potential_termination = pybamm.step.CustomTermination(
    name="Anode potential cut-off [V]", event_function=anode_potential_cutoff
)

# Provide a list of termination events, each step will stop whenever the first
# termination event is reached
terminations = [anode_potential_termination, "4.2V"]

# Set up multi-step CC experiment with the customer terminations followed
# by a voltage hold
experiment = pybamm.Experiment(
    [
        (
            pybamm.step.c_rate(-1, termination=terminations),
            pybamm.step.c_rate(-0.5, termination=terminations),
            pybamm.step.c_rate(-0.25, termination=terminations),
            "Hold at 4.2V until C/50",
        )
    ]
)

# Set up simulation
sim = pybamm.Simulation(model, parameter_values=parameter_values, experiment=experiment)

# for a charge we start as SOC 0
sim.solve(initial_soc=0)
[2]:
<pybamm.solvers.solution.Solution at 0x11fcd1e10>
[3]:
# Plot
plot = pybamm.QuickPlot(
    sim.solution,
    [
        "Current [A]",
        "Voltage [V]",
        "Anode potential [V]",
    ],
)
plot.plot(0)

# Plot the limits used in the termination events to check they are not surpassed
plot.axes.by_variable("Voltage [V]").axhline(4.2, color="k", linestyle=":")
plot.axes.by_variable("Anode potential [V]").axhline(0.02, color="k", linestyle=":")
[3]:
<matplotlib.lines.Line2D at 0x16e37f9d0>
_images/source_examples_notebooks_simulations_and_experiments_custom-experiments_5_1.png

We can check which events were reached by each step

[4]:
for i, step in enumerate(sim.solution.cycles[0].steps):
    print(f"Step {i}: {step.termination}")
Step 0: event: Anode potential cut-off [V] [experiment]
Step 1: event: Anode potential cut-off [V] [experiment]
Step 2: event: Charge voltage cut-off [V] [experiment]
Step 3: event: C-rate cut-off [experiment]
[5]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[5] 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.
[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.
[7] 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.
[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Experiments with start_time#

This notebook introduces functionality for simulating user case in which the experiment steps are triggered at a certain point in time.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
from datetime import datetime

[notice] A new release of pip is available: 23.0.1 -> 23.1.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

Let’s start defining a model to illustrate this functionality, in this case we choose the SPM

[2]:
model = pybamm.lithium_ion.SPM()

Usually we define an experiment such that each step is triggered when the previous step is completed. For example, in this case we do a 1C discharge for 20 minutes and then a C/3 charge for 10 minutes. The charge step starts after 20 minutes, i.e. once the discharge step is finished.

[3]:
experiment = pybamm.Experiment(
    ["Discharge at 1C for 20 minutes", "Charge at C/3 for 10 minutes"]
)
sim = pybamm.Simulation(model, experiment=experiment)
sim.solve()
sim.plot()
[3]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f8910adb4f0>

However, if we want to represent a realistic user case we might certain experiments to be run at a certain time instead, even if that means cutting short the previous step. In this case we can pass a starting time as a keyword argument in the pybamm.step.string method. The start_time should be passed as a datetime.datetime object.

[4]:
s = pybamm.step.string

experiment = pybamm.Experiment(
    [
        s("Discharge at 1C for 1 hour", start_time=datetime(1, 1, 1, 8, 0, 0)),
        s("Charge at C/3 for 10 minutes", start_time=datetime(1, 1, 1, 8, 30, 0)),
        s("Discharge at C/2 for 30 minutes", start_time=datetime(1, 1, 1, 9, 0, 0)),
        s("Rest for 1 hour"),
    ]
)
sim = pybamm.Simulation(model, experiment=experiment)
sim.solve()
sim.plot()
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7f8910b8e250>

In the example above, we note that the first step (1C discharge) is cut short as the second step (C/3 charge) start time occurs before the end of the first step. On the other hand, an additional resting period is added after the second step as the third step (C/2 discharge) start time is 20 minutes later than the end of the second step. The final step does not have a start time so it is triggered immediately after the previous step. Note that if the argument start_time is used in an experiment, the first step should always have a start_time, otherwise the solver will throw an error.

Note that you can use the datetime.strptime (see the docs for more info) function to convert a string to a datetime object. For example, to start the experiment at 8:30 on the 2nd of January 2023, you can use

[5]:
datetime.strptime("2023-01-02 8:30:00", "%Y-%m-%d %H:%M:%S")
[5]:
datetime.datetime(2023, 1, 2, 8, 30)
[6]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.
[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.
[6] 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.
[7] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Degradation experiments with reference performance tests#

When running degradation experiments in the lab, it is important to use reference performance tests (RPTs) to measure capacity and other key metrics, otherwise the experimental contitions will interfere with the measurement! In PyBaMM, you can run simulations with RPTs using the Experiment class.

[1]:
%pip install pybamm[plot,cite] -q  # install PyBaMM if it is not installed

import pybamm
import matplotlib.pyplot as plt
import os

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

Load a simple degradation model

[2]:
model = pybamm.lithium_ion.SPM({"SEI": "ec reaction limited"})
parameter_values = pybamm.ParameterValues("Mohtat2020")
parameter_values.update({"SEI kinetic rate constant [m.s-1]": 1e-14})

Define three different experiments using the Experiment class: * cccv_experiment is a cycle ageing experiment * charge_experiment charges to full after N ageing cycles * rpt_experiment is a C/3 discharge in this case, but can also contain a charge, GITT, EIS and other procedures

[3]:
N = 10
cccv_experiment = pybamm.Experiment(
    [
        (
            "Charge at 1C until 4.2V",
            "Hold at 4.2V until C/50",
            "Discharge at 1C until 3V",
            "Rest for 1 hour",
        )
    ]
    * N
)
charge_experiment = pybamm.Experiment(
    [
        (
            "Charge at 1C until 4.2V",
            "Hold at 4.2V until C/50",
        )
    ]
)
rpt_experiment = pybamm.Experiment([("Discharge at C/3 until 3V",)])

Run the ageing, charge and RPT experiments in order by feeding the previous solution into the solve command:

[4]:
sim = pybamm.Simulation(
    model, experiment=cccv_experiment, parameter_values=parameter_values
)
cccv_sol = sim.solve()
sim = pybamm.Simulation(
    model, experiment=charge_experiment, parameter_values=parameter_values
)
charge_sol = sim.solve(starting_solution=cccv_sol)
sim = pybamm.Simulation(
    model, experiment=rpt_experiment, parameter_values=parameter_values
)
rpt_sol = sim.solve(starting_solution=charge_sol)

Plot detailed current/voltage data for the RPT cycle only:

[5]:
pybamm.dynamic_plot(rpt_sol.cycles[-1], ["Current [A]", "Voltage [V]"])
[5]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ff13da91760>

PyBaMM’s summary variables track important cell-level degradation veriables at the end of each cycle. The charge and RPT cycles are also counted, making a total of 12 cycles:

[6]:
pybamm.plot_summary_variables(rpt_sol);
_images/source_examples_notebooks_simulations_and_experiments_rpt-experiment_12_0.png

Repeat the procedure four times:

[7]:
cccv_sols = []
charge_sols = []
rpt_sols = []
M = 5
for i in range(M):
    if i != 0:  # skip the first set of ageing cycles because it's already been done
        sim = pybamm.Simulation(
            model, experiment=cccv_experiment, parameter_values=parameter_values
        )
        cccv_sol = sim.solve(starting_solution=rpt_sol)
        sim = pybamm.Simulation(
            model, experiment=charge_experiment, parameter_values=parameter_values
        )
        charge_sol = sim.solve(starting_solution=cccv_sol)
        sim = pybamm.Simulation(
            model, experiment=rpt_experiment, parameter_values=parameter_values
        )
        rpt_sol = sim.solve(starting_solution=charge_sol)
    cccv_sols.append(cccv_sol)
    charge_sols.append(charge_sol)
    rpt_sols.append(rpt_sol)

You can plot any RPT cycle. The last one is chosen here.

[8]:
pybamm.dynamic_plot(rpt_sols[-1].cycles[-1], ["Current [A]", "Voltage [V]"])
[8]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ff12d79b610>

One way of demonstrating how useful RPTs are is to plot the discharge capacity for each cycle. It is convenient to use the final rpt_sol because it also contains all previous simulations.

[9]:
cccv_cycles = []
cccv_capacities = []
rpt_cycles = []
rpt_capacities = []
for i in range(M):
    for j in range(N):
        cccv_cycles.append(i * (N + 2) + j + 1)
        start_capacity = (
            rpt_sol.cycles[i * (N + 2) + j]
            .steps[2]["Discharge capacity [A.h]"]
            .entries[0]
        )
        end_capacity = (
            rpt_sol.cycles[i * (N + 2) + j]
            .steps[2]["Discharge capacity [A.h]"]
            .entries[-1]
        )
        cccv_capacities.append(end_capacity - start_capacity)
    rpt_cycles.append((i + 1) * (N + 2))
    start_capacity = rpt_sol.cycles[(i + 1) * (N + 2) - 1][
        "Discharge capacity [A.h]"
    ].entries[0]
    end_capacity = rpt_sol.cycles[(i + 1) * (N + 2) - 1][
        "Discharge capacity [A.h]"
    ].entries[-1]
    rpt_capacities.append(end_capacity - start_capacity)
plt.scatter(cccv_cycles, cccv_capacities, label="Ageing cycles")
plt.scatter(rpt_cycles, rpt_capacities, label="RPT cycles")
plt.xlabel("Cycle number")
plt.ylabel("Discharge capacity [A.h]")
plt.legend()
[9]:
<matplotlib.legend.Legend at 0x7ff12d616bb0>
_images/source_examples_notebooks_simulations_and_experiments_rpt-experiment_18_1.png

The ageing cycles have a higher discharge rate than the RPT cycles and therefore have a slightly lower discharge capacity. (The charge cycles are not included because they have no discharge capacity.)

Finally, plot the summary variables for the entire experiment run.

[10]:
pybamm.plot_summary_variables(rpt_sol);
_images/source_examples_notebooks_simulations_and_experiments_rpt-experiment_21_0.png

References#

The relevant papers for this notebook are:

[11]:
pybamm.print_citations()
[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.
[2] 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:.
[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.
[4] 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.
[5] 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.
[6] Peyman Mohtat, Suhak Lee, Valentin Sulzer, Jason B. Siegel, and Anna G. Stefanopoulou. Differential Expansion and Voltage Model for Li-ion Batteries at Practical Charging Rates. Journal of The Electrochemical Society, 167(11):110561, 2020. doi:10.1149/1945-7111/aba5d1.
[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.
[8] 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.
[9] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.
[10] Xiao Guang Yang, Yongjun Leng, Guangsheng Zhang, Shanhai Ge, and Chao Yang Wang. Modeling of lithium plating induced aging of lithium-ion batteries: transition from linear to nonlinear aging. Journal of Power Sources, 360:28–40, 2017. doi:10.1016/j.jpowsour.2017.05.110.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Simulating long experiments#

This notebook introduces functionality for simulating experiments over hundreds or even thousands of cycles.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
Note: you may need to restart the kernel to use updated packages.

Simulating long experiments#

In the interest of simplicity and running time, we consider a SPM with SEI effects leading to linear degradation, with parameter values chosen so that the capacity fades by 20% in just a few cycles

[2]:
parameter_values = pybamm.ParameterValues("Mohtat2020")
parameter_values.update({"SEI kinetic rate constant [m.s-1]": 1e-14})
spm = pybamm.lithium_ion.SPM({"SEI": "ec reaction limited"})

We initialize the concentration in each electrode at 100% State of Charge

[3]:
# Calculate stoichiometries at 100% SOC
parameter_values.set_initial_stoichiometries(1);

We can now simulate a single CCCV cycle using the Experiment class (see this notebook for more details)

[4]:
pybamm.set_logging_level("NOTICE")

experiment = pybamm.Experiment(
    [
        (
            "Discharge at 1C until 3V",
            "Rest for 1 hour",
            "Charge at 1C until 4.2V",
            "Hold at 4.2V until C/50",
        )
    ]
)
sim = pybamm.Simulation(spm, experiment=experiment, parameter_values=parameter_values)
sol = sim.solve()
2022-12-04 21:24:29.130 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/1 (24.500 us elapsed) --------------------
2022-12-04 21:24:29.131 - [NOTICE] callbacks.on_step_start(182): Cycle 1/1, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:29.172 - [NOTICE] callbacks.on_step_start(182): Cycle 1/1, step 2/4: Rest for 1 hour
2022-12-04 21:24:29.199 - [NOTICE] callbacks.on_step_start(182): Cycle 1/1, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:29.237 - [NOTICE] callbacks.on_step_start(182): Cycle 1/1, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:29.372 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 241.861 ms

Alternatively, we can simulate many CCCV cycles. Here we simulate either 100 cycles or until the capacity is 80% of the initial capacity, whichever is first. The capacity is calculated by the eSOH model

[5]:
experiment = pybamm.Experiment(
    [
        (
            "Discharge at 1C until 3V",
            "Rest for 1 hour",
            "Charge at 1C until 4.2V",
            "Hold at 4.2V until C/50",
        )
    ]
    * 500,
    termination="80% capacity",
)
sim = pybamm.Simulation(spm, experiment=experiment, parameter_values=parameter_values)
sol = sim.solve()
2022-12-04 21:24:30.209 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/500 (25.250 us elapsed) --------------------
2022-12-04 21:24:30.209 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.248 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.275 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.315 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.460 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.941 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.460 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/500 (251.525 ms elapsed) --------------------
2022-12-04 21:24:30.461 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.486 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.501 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.523 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.566 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.913 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.567 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/500 (357.953 ms elapsed) --------------------
2022-12-04 21:24:30.567 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.593 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.607 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.630 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.674 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.886 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.675 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/500 (465.886 ms elapsed) --------------------
2022-12-04 21:24:30.675 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.700 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.713 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.737 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.781 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.859 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.782 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/500 (573.195 ms elapsed) --------------------
2022-12-04 21:24:30.782 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.807 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.821 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.845 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.894 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.833 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.895 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/500 (686.094 ms elapsed) --------------------
2022-12-04 21:24:30.895 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:30.916 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:30.930 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:30.949 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:30.996 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.807 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:30.996 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/500 (787.595 ms elapsed) --------------------
2022-12-04 21:24:30.997 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.018 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.032 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.051 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.099 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.781 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.100 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/500 (891.219 ms elapsed) --------------------
2022-12-04 21:24:31.100 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.120 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.134 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.153 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.200 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.756 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.201 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/500 (992.032 ms elapsed) --------------------
2022-12-04 21:24:31.201 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.225 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.240 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.263 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.313 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.732 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.313 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/500 (1.104 s elapsed) --------------------
2022-12-04 21:24:31.313 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.337 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.351 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.372 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.420 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.708 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.421 - [NOTICE] callbacks.on_cycle_start(174): Cycle 11/500 (1.212 s elapsed) --------------------
2022-12-04 21:24:31.421 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.443 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.458 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.483 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.539 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.684 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.540 - [NOTICE] callbacks.on_cycle_start(174): Cycle 12/500 (1.331 s elapsed) --------------------
2022-12-04 21:24:31.542 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.565 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.579 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.597 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.644 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.660 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.645 - [NOTICE] callbacks.on_cycle_start(174): Cycle 13/500 (1.436 s elapsed) --------------------
2022-12-04 21:24:31.645 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.665 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.680 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.700 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.751 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.637 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.751 - [NOTICE] callbacks.on_cycle_start(174): Cycle 14/500 (1.543 s elapsed) --------------------
2022-12-04 21:24:31.752 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.774 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.788 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.807 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:31.938 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.614 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:31.938 - [NOTICE] callbacks.on_cycle_start(174): Cycle 15/500 (1.729 s elapsed) --------------------
2022-12-04 21:24:31.939 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:31.959 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:31.972 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:31.991 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.045 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.592 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.046 - [NOTICE] callbacks.on_cycle_start(174): Cycle 16/500 (1.837 s elapsed) --------------------
2022-12-04 21:24:32.046 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.068 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.082 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.103 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.153 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.569 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.154 - [NOTICE] callbacks.on_cycle_start(174): Cycle 17/500 (1.945 s elapsed) --------------------
2022-12-04 21:24:32.154 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.179 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.193 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.215 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.267 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.548 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.268 - [NOTICE] callbacks.on_cycle_start(174): Cycle 18/500 (2.059 s elapsed) --------------------
2022-12-04 21:24:32.268 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.290 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.304 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.326 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.378 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.526 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.379 - [NOTICE] callbacks.on_cycle_start(174): Cycle 19/500 (2.170 s elapsed) --------------------
2022-12-04 21:24:32.380 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.403 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.417 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.436 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.489 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.505 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.489 - [NOTICE] callbacks.on_cycle_start(174): Cycle 20/500 (2.281 s elapsed) --------------------
2022-12-04 21:24:32.490 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.508 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.522 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.540 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.594 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.484 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.595 - [NOTICE] callbacks.on_cycle_start(174): Cycle 21/500 (2.386 s elapsed) --------------------
2022-12-04 21:24:32.595 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.615 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.630 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.649 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.705 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.463 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.706 - [NOTICE] callbacks.on_cycle_start(174): Cycle 22/500 (2.497 s elapsed) --------------------
2022-12-04 21:24:32.707 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.728 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.742 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.762 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.816 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.442 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.817 - [NOTICE] callbacks.on_cycle_start(174): Cycle 23/500 (2.608 s elapsed) --------------------
2022-12-04 21:24:32.817 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.840 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.855 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.876 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:32.932 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.422 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:32.932 - [NOTICE] callbacks.on_cycle_start(174): Cycle 24/500 (2.724 s elapsed) --------------------
2022-12-04 21:24:32.933 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:32.954 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:32.968 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:32.998 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.056 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.402 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.056 - [NOTICE] callbacks.on_cycle_start(174): Cycle 25/500 (2.848 s elapsed) --------------------
2022-12-04 21:24:33.057 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.078 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.093 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.114 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.166 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.382 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.167 - [NOTICE] callbacks.on_cycle_start(174): Cycle 26/500 (2.958 s elapsed) --------------------
2022-12-04 21:24:33.167 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.188 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.203 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.224 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.279 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.362 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.280 - [NOTICE] callbacks.on_cycle_start(174): Cycle 27/500 (3.071 s elapsed) --------------------
2022-12-04 21:24:33.280 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.302 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.316 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.335 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.391 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.343 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.392 - [NOTICE] callbacks.on_cycle_start(174): Cycle 28/500 (3.183 s elapsed) --------------------
2022-12-04 21:24:33.392 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.415 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.429 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.448 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.503 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.324 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.504 - [NOTICE] callbacks.on_cycle_start(174): Cycle 29/500 (3.295 s elapsed) --------------------
2022-12-04 21:24:33.505 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.525 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.539 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.557 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.612 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.305 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.613 - [NOTICE] callbacks.on_cycle_start(174): Cycle 30/500 (3.404 s elapsed) --------------------
2022-12-04 21:24:33.613 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.631 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.645 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.665 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.724 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.286 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.724 - [NOTICE] callbacks.on_cycle_start(174): Cycle 31/500 (3.516 s elapsed) --------------------
2022-12-04 21:24:33.725 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.748 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.766 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.788 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.847 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.267 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.848 - [NOTICE] callbacks.on_cycle_start(174): Cycle 32/500 (3.639 s elapsed) --------------------
2022-12-04 21:24:33.848 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.871 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:33.894 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:33.914 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:33.972 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.249 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:33.972 - [NOTICE] callbacks.on_cycle_start(174): Cycle 33/500 (3.763 s elapsed) --------------------
2022-12-04 21:24:33.972 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:33.995 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.010 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.031 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.091 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.231 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.092 - [NOTICE] callbacks.on_cycle_start(174): Cycle 34/500 (3.883 s elapsed) --------------------
2022-12-04 21:24:34.092 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.114 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.128 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.149 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.208 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.213 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.209 - [NOTICE] callbacks.on_cycle_start(174): Cycle 35/500 (4.000 s elapsed) --------------------
2022-12-04 21:24:34.209 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.231 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.246 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.263 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.420 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.195 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.421 - [NOTICE] callbacks.on_cycle_start(174): Cycle 36/500 (4.212 s elapsed) --------------------
2022-12-04 21:24:34.421 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.444 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.458 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.475 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.535 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.177 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.536 - [NOTICE] callbacks.on_cycle_start(174): Cycle 37/500 (4.327 s elapsed) --------------------
2022-12-04 21:24:34.536 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.558 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.573 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.590 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.650 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.160 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.650 - [NOTICE] callbacks.on_cycle_start(174): Cycle 38/500 (4.442 s elapsed) --------------------
2022-12-04 21:24:34.651 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.668 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.682 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.700 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.763 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.143 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.763 - [NOTICE] callbacks.on_cycle_start(174): Cycle 39/500 (4.555 s elapsed) --------------------
2022-12-04 21:24:34.764 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.782 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.797 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.814 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.875 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.126 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.876 - [NOTICE] callbacks.on_cycle_start(174): Cycle 40/500 (4.667 s elapsed) --------------------
2022-12-04 21:24:34.876 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:34.895 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:34.910 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:34.931 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:34.990 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.109 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:34.991 - [NOTICE] callbacks.on_cycle_start(174): Cycle 41/500 (4.782 s elapsed) --------------------
2022-12-04 21:24:34.991 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.009 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.026 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.046 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.109 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.092 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.110 - [NOTICE] callbacks.on_cycle_start(174): Cycle 42/500 (4.901 s elapsed) --------------------
2022-12-04 21:24:35.110 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.130 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.144 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.163 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.225 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.075 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.225 - [NOTICE] callbacks.on_cycle_start(174): Cycle 43/500 (5.016 s elapsed) --------------------
2022-12-04 21:24:35.225 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.246 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.260 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.281 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.341 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.059 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.342 - [NOTICE] callbacks.on_cycle_start(174): Cycle 44/500 (5.133 s elapsed) --------------------
2022-12-04 21:24:35.342 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.364 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.383 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.403 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.462 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.042 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.463 - [NOTICE] callbacks.on_cycle_start(174): Cycle 45/500 (5.254 s elapsed) --------------------
2022-12-04 21:24:35.463 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.484 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.498 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.514 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.577 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.026 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.584 - [NOTICE] callbacks.on_cycle_start(174): Cycle 46/500 (5.376 s elapsed) --------------------
2022-12-04 21:24:35.593 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.633 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.659 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.675 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.740 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.010 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.741 - [NOTICE] callbacks.on_cycle_start(174): Cycle 47/500 (5.532 s elapsed) --------------------
2022-12-04 21:24:35.741 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.769 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.784 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.800 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.864 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.994 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.864 - [NOTICE] callbacks.on_cycle_start(174): Cycle 48/500 (5.655 s elapsed) --------------------
2022-12-04 21:24:35.864 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:35.881 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:35.896 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:35.914 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:35.983 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.978 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:35.983 - [NOTICE] callbacks.on_cycle_start(174): Cycle 49/500 (5.774 s elapsed) --------------------
2022-12-04 21:24:35.983 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:36.000 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:36.015 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:36.039 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:36.104 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.962 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:36.105 - [NOTICE] callbacks.on_cycle_start(174): Cycle 50/500 (5.896 s elapsed) --------------------
2022-12-04 21:24:36.105 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:36.123 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:36.140 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:36.160 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:36.227 - [NOTICE] callbacks.on_cycle_end(201): Stopping experiment since capacity (3.947 Ah) is below stopping capacity (3.952 Ah).
2022-12-04 21:24:36.229 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 6.019 s
Summary variables#

We can plot standard variables like the current and voltage, but it isn’t very instructive on these timescales

[6]:
sol.plot(["Current [A]", "Voltage [V]"])
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x170561fd0>

Instead, we plot “summary variables”, which show how the battery degrades over time by various metrics. Some of the variables also have “Change in …”, which is how much that variable changes over each cycle. This can be achieved by using plot_summary_variables method of pybamm, which can also be used to compare “summary variables” extracted from 2 or more solutions.

[7]:
sorted(sol.summary_variables.keys())
[7]:
['Capacity [A.h]',
 'Change in local ECM resistance [Ohm]',
 'Change in loss of active material in negative electrode [%]',
 'Change in loss of active material in positive electrode [%]',
 'Change in loss of capacity to SEI [A.h]',
 'Change in loss of capacity to SEI on cracks [A.h]',
 'Change in loss of capacity to lithium plating [A.h]',
 'Change in loss of lithium inventory [%]',
 'Change in loss of lithium inventory, including electrolyte [%]',
 'Change in loss of lithium to SEI [mol]',
 'Change in loss of lithium to SEI on cracks [mol]',
 'Change in loss of lithium to lithium plating [mol]',
 'Change in negative electrode capacity [A.h]',
 'Change in positive electrode capacity [A.h]',
 'Change in throughput capacity [A.h]',
 'Change in throughput energy [W.h]',
 'Change in time [h]',
 'Change in time [s]',
 'Change in total capacity lost to side reactions [A.h]',
 'Change in total lithium [mol]',
 'Change in total lithium in electrolyte [mol]',
 'Change in total lithium in negative electrode [mol]',
 'Change in total lithium in particles [mol]',
 'Change in total lithium in positive electrode [mol]',
 'Change in total lithium lost [mol]',
 'Change in total lithium lost from electrolyte [mol]',
 'Change in total lithium lost from particles [mol]',
 'Change in total lithium lost to side reactions [mol]',
 'Cycle number',
 'Local ECM resistance [Ohm]',
 'Loss of active material in negative electrode [%]',
 'Loss of active material in positive electrode [%]',
 'Loss of capacity to SEI [A.h]',
 'Loss of capacity to SEI on cracks [A.h]',
 'Loss of capacity to lithium plating [A.h]',
 'Loss of lithium inventory [%]',
 'Loss of lithium inventory, including electrolyte [%]',
 'Loss of lithium to SEI [mol]',
 'Loss of lithium to SEI on cracks [mol]',
 'Loss of lithium to lithium plating [mol]',
 'Maximum measured discharge capacity [A.h]',
 'Maximum voltage [V]',
 'Measured capacity [A.h]',
 'Minimum measured discharge capacity [A.h]',
 'Minimum voltage [V]',
 'Negative electrode capacity [A.h]',
 'Negative electrode excess capacity ratio',
 'Positive electrode capacity [A.h]',
 'Positive electrode excess capacity ratio',
 'Q',
 'Q_Li',
 'Q_n',
 'Q_n * (x_100 - x_0)',
 'Q_p',
 'Q_p * (y_0 - y_100)',
 'Throughput capacity [A.h]',
 'Throughput energy [W.h]',
 'Time [h]',
 'Time [s]',
 'Total capacity lost to side reactions [A.h]',
 'Total lithium [mol]',
 'Total lithium in electrolyte [mol]',
 'Total lithium in negative electrode [mol]',
 'Total lithium in particles [mol]',
 'Total lithium in positive electrode [mol]',
 'Total lithium lost [mol]',
 'Total lithium lost from electrolyte [mol]',
 'Total lithium lost from particles [mol]',
 'Total lithium lost to side reactions [mol]',
 'Un(x_0)',
 'Un(x_100)',
 'Up(y_0)',
 'Up(y_0) - Un(x_0)',
 'Up(y_100)',
 'Up(y_100) - Un(x_100)',
 'n_Li',
 'x_0',
 'x_100',
 'x_100 - x_0',
 'y_0',
 'y_0 - y_100',
 'y_100']

The “summary variables” associated with a particular model can also be accessed as a list (which can then be edited) -

[8]:
spm.summary_variables
[8]:
['Time [s]',
 'Time [h]',
 'Throughput capacity [A.h]',
 'Throughput energy [W.h]',
 'Loss of lithium inventory [%]',
 'Loss of lithium inventory, including electrolyte [%]',
 'Total lithium [mol]',
 'Total lithium in electrolyte [mol]',
 'Total lithium in particles [mol]',
 'Total lithium lost [mol]',
 'Total lithium lost from particles [mol]',
 'Total lithium lost from electrolyte [mol]',
 'Loss of lithium to SEI [mol]',
 'Loss of capacity to SEI [A.h]',
 'Total lithium lost to side reactions [mol]',
 'Total capacity lost to side reactions [A.h]',
 'Local ECM resistance [Ohm]',
 'Negative electrode capacity [A.h]',
 'Loss of active material in negative electrode [%]',
 'Total lithium in negative electrode [mol]',
 'Loss of lithium to lithium plating [mol]',
 'Loss of capacity to lithium plating [A.h]',
 'Loss of lithium to SEI on cracks [mol]',
 'Loss of capacity to SEI on cracks [A.h]',
 'Positive electrode capacity [A.h]',
 'Loss of active material in positive electrode [%]',
 'Total lithium in positive electrode [mol]']

Here the only degradation mechanism is one that causes loss of lithium, so we don’t see loss of active material

[9]:
pybamm.plot_summary_variables(sol)
_images/source_examples_notebooks_simulations_and_experiments_simulating-long-experiments_20_0.png
[9]:
array([[<AxesSubplot:xlabel='Cycle number', ylabel='Capacity [A.h]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of lithium inventory [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of capacity to SEI [A.h]'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in negative electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in positive electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='x_100'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='x_0'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_100'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_0'>]], dtype=object)

To suggest additional summary variables, open an issue!

Choosing which cycles to save#

If the simulation contains thousands of cycles, saving each cycle in RAM might not be possible. To get around this, we can use save_at_cycles. If this is an integer n, every nth cycle is saved. If this is a list, all the cycles in the list are saved. The first cycle is always saved.

[10]:
# With integer
sol_int = sim.solve(save_at_cycles=5)
# With list
sol_list = sim.solve(save_at_cycles=[30, 45, 55])
2022-12-04 21:24:37.738 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/500 (18.250 us elapsed) --------------------
2022-12-04 21:24:37.739 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:37.779 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:37.793 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:37.813 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:37.855 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.941 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:37.855 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/500 (117.477 ms elapsed) --------------------
2022-12-04 21:24:37.856 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:37.879 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:37.892 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:37.912 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:37.951 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.913 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:37.952 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/500 (213.869 ms elapsed) --------------------
2022-12-04 21:24:37.952 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:37.977 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:37.991 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.013 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.056 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.886 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.057 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/500 (319.000 ms elapsed) --------------------
2022-12-04 21:24:38.058 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.094 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.115 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.154 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.204 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.859 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.204 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/500 (466.169 ms elapsed) --------------------
2022-12-04 21:24:38.205 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.232 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.249 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.273 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.327 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.833 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.328 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/500 (590.261 ms elapsed) --------------------
2022-12-04 21:24:38.329 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.352 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.369 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.389 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.442 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.807 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.442 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/500 (704.200 ms elapsed) --------------------
2022-12-04 21:24:38.443 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.464 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.478 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.498 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.545 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.781 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.546 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/500 (807.534 ms elapsed) --------------------
2022-12-04 21:24:38.546 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.569 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.584 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.603 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.650 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.756 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.650 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/500 (912.304 ms elapsed) --------------------
2022-12-04 21:24:38.651 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.673 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.688 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.709 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.754 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.732 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.755 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/500 (1.017 s elapsed) --------------------
2022-12-04 21:24:38.756 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.780 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.795 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.817 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.866 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.708 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.867 - [NOTICE] callbacks.on_cycle_start(174): Cycle 11/500 (1.129 s elapsed) --------------------
2022-12-04 21:24:38.867 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.890 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:38.905 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:38.928 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:38.972 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.684 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:38.973 - [NOTICE] callbacks.on_cycle_start(174): Cycle 12/500 (1.235 s elapsed) --------------------
2022-12-04 21:24:38.973 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:38.997 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.010 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.029 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.074 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.660 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.075 - [NOTICE] callbacks.on_cycle_start(174): Cycle 13/500 (1.337 s elapsed) --------------------
2022-12-04 21:24:39.075 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.094 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.109 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.128 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.174 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.637 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.175 - [NOTICE] callbacks.on_cycle_start(174): Cycle 14/500 (1.437 s elapsed) --------------------
2022-12-04 21:24:39.176 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.198 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.211 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.230 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.274 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.614 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.274 - [NOTICE] callbacks.on_cycle_start(174): Cycle 15/500 (1.536 s elapsed) --------------------
2022-12-04 21:24:39.275 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.295 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.309 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.328 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.377 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.592 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.378 - [NOTICE] callbacks.on_cycle_start(174): Cycle 16/500 (1.640 s elapsed) --------------------
2022-12-04 21:24:39.378 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.400 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.414 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.437 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.482 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.569 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.482 - [NOTICE] callbacks.on_cycle_start(174): Cycle 17/500 (1.744 s elapsed) --------------------
2022-12-04 21:24:39.483 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.504 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.518 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.542 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.588 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.548 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.589 - [NOTICE] callbacks.on_cycle_start(174): Cycle 18/500 (1.851 s elapsed) --------------------
2022-12-04 21:24:39.589 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.613 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.627 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.654 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.700 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.526 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.700 - [NOTICE] callbacks.on_cycle_start(174): Cycle 19/500 (1.962 s elapsed) --------------------
2022-12-04 21:24:39.701 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.723 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.736 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.755 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.799 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.505 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.799 - [NOTICE] callbacks.on_cycle_start(174): Cycle 20/500 (2.061 s elapsed) --------------------
2022-12-04 21:24:39.800 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.820 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.833 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.852 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.899 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.484 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.899 - [NOTICE] callbacks.on_cycle_start(174): Cycle 21/500 (2.161 s elapsed) --------------------
2022-12-04 21:24:39.900 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:39.919 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:39.933 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:39.952 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:39.997 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.463 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:39.997 - [NOTICE] callbacks.on_cycle_start(174): Cycle 22/500 (2.259 s elapsed) --------------------
2022-12-04 21:24:39.998 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.017 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.031 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.050 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.092 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.442 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.093 - [NOTICE] callbacks.on_cycle_start(174): Cycle 23/500 (2.355 s elapsed) --------------------
2022-12-04 21:24:40.093 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.114 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.127 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.148 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.194 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.422 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.195 - [NOTICE] callbacks.on_cycle_start(174): Cycle 24/500 (2.457 s elapsed) --------------------
2022-12-04 21:24:40.195 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.217 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.231 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.253 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.299 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.402 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.299 - [NOTICE] callbacks.on_cycle_start(174): Cycle 25/500 (2.561 s elapsed) --------------------
2022-12-04 21:24:40.300 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.322 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.337 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.358 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.413 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.382 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.413 - [NOTICE] callbacks.on_cycle_start(174): Cycle 26/500 (2.675 s elapsed) --------------------
2022-12-04 21:24:40.414 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.439 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.456 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.480 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.526 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.362 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.527 - [NOTICE] callbacks.on_cycle_start(174): Cycle 27/500 (2.789 s elapsed) --------------------
2022-12-04 21:24:40.529 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.553 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.570 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.589 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.635 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.343 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.635 - [NOTICE] callbacks.on_cycle_start(174): Cycle 28/500 (2.897 s elapsed) --------------------
2022-12-04 21:24:40.635 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.658 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.673 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.693 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.737 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.324 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.738 - [NOTICE] callbacks.on_cycle_start(174): Cycle 29/500 (3.000 s elapsed) --------------------
2022-12-04 21:24:40.738 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.757 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.772 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.790 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.831 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.305 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.832 - [NOTICE] callbacks.on_cycle_start(174): Cycle 30/500 (3.094 s elapsed) --------------------
2022-12-04 21:24:40.833 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.851 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.864 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.882 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:40.926 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.286 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:40.927 - [NOTICE] callbacks.on_cycle_start(174): Cycle 31/500 (3.189 s elapsed) --------------------
2022-12-04 21:24:40.927 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:40.946 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:40.960 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:40.980 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.022 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.267 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.023 - [NOTICE] callbacks.on_cycle_start(174): Cycle 32/500 (3.285 s elapsed) --------------------
2022-12-04 21:24:41.023 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.042 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.056 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.078 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.122 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.249 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.123 - [NOTICE] callbacks.on_cycle_start(174): Cycle 33/500 (3.385 s elapsed) --------------------
2022-12-04 21:24:41.123 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.146 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.159 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.179 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.226 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.231 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.226 - [NOTICE] callbacks.on_cycle_start(174): Cycle 34/500 (3.488 s elapsed) --------------------
2022-12-04 21:24:41.227 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.252 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.267 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.288 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.333 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.213 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.334 - [NOTICE] callbacks.on_cycle_start(174): Cycle 35/500 (3.596 s elapsed) --------------------
2022-12-04 21:24:41.334 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.356 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.371 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.389 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.435 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.195 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.436 - [NOTICE] callbacks.on_cycle_start(174): Cycle 36/500 (3.698 s elapsed) --------------------
2022-12-04 21:24:41.436 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.461 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.474 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.492 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.539 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.177 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.540 - [NOTICE] callbacks.on_cycle_start(174): Cycle 37/500 (3.802 s elapsed) --------------------
2022-12-04 21:24:41.540 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.562 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.575 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.594 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.640 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.160 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.641 - [NOTICE] callbacks.on_cycle_start(174): Cycle 38/500 (3.903 s elapsed) --------------------
2022-12-04 21:24:41.641 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.659 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.673 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.692 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.738 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.143 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.738 - [NOTICE] callbacks.on_cycle_start(174): Cycle 39/500 (4.000 s elapsed) --------------------
2022-12-04 21:24:41.739 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.758 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.773 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.791 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.840 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.126 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.843 - [NOTICE] callbacks.on_cycle_start(174): Cycle 40/500 (4.105 s elapsed) --------------------
2022-12-04 21:24:41.848 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.869 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:41.888 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:41.916 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:41.973 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.109 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:41.974 - [NOTICE] callbacks.on_cycle_start(174): Cycle 41/500 (4.236 s elapsed) --------------------
2022-12-04 21:24:41.975 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:41.994 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.009 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.030 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.073 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.092 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.074 - [NOTICE] callbacks.on_cycle_start(174): Cycle 42/500 (4.336 s elapsed) --------------------
2022-12-04 21:24:42.074 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.092 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.108 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.129 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.174 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.075 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.174 - [NOTICE] callbacks.on_cycle_start(174): Cycle 43/500 (4.436 s elapsed) --------------------
2022-12-04 21:24:42.175 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.196 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.211 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.231 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.272 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.059 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.273 - [NOTICE] callbacks.on_cycle_start(174): Cycle 44/500 (4.535 s elapsed) --------------------
2022-12-04 21:24:42.273 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.295 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.309 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.330 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.372 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.042 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.373 - [NOTICE] callbacks.on_cycle_start(174): Cycle 45/500 (4.635 s elapsed) --------------------
2022-12-04 21:24:42.373 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.396 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.410 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.427 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.475 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.026 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.476 - [NOTICE] callbacks.on_cycle_start(174): Cycle 46/500 (4.738 s elapsed) --------------------
2022-12-04 21:24:42.476 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.498 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.519 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.536 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.582 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.010 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.582 - [NOTICE] callbacks.on_cycle_start(174): Cycle 47/500 (4.844 s elapsed) --------------------
2022-12-04 21:24:42.582 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.603 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.617 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.634 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.678 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.994 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.679 - [NOTICE] callbacks.on_cycle_start(174): Cycle 48/500 (4.941 s elapsed) --------------------
2022-12-04 21:24:42.680 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.698 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.711 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.727 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.772 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.978 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.772 - [NOTICE] callbacks.on_cycle_start(174): Cycle 49/500 (5.034 s elapsed) --------------------
2022-12-04 21:24:42.773 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.789 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.804 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.824 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.871 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.962 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:42.871 - [NOTICE] callbacks.on_cycle_start(174): Cycle 50/500 (5.133 s elapsed) --------------------
2022-12-04 21:24:42.871 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:42.890 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:42.904 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:42.924 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:42.977 - [NOTICE] callbacks.on_cycle_end(201): Stopping experiment since capacity (3.947 Ah) is below stopping capacity (3.952 Ah).
2022-12-04 21:24:42.979 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 5.239 s
2022-12-04 21:24:42.979 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/500 (15.583 us elapsed) --------------------
2022-12-04 21:24:42.980 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.020 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.036 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.061 - [NOTICE] callbacks.on_step_start(182): Cycle 1/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.105 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.941 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.106 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/500 (126.469 ms elapsed) --------------------
2022-12-04 21:24:43.107 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.131 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.145 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.166 - [NOTICE] callbacks.on_step_start(182): Cycle 2/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.209 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.913 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.209 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/500 (229.755 ms elapsed) --------------------
2022-12-04 21:24:43.210 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.232 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.246 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.270 - [NOTICE] callbacks.on_step_start(182): Cycle 3/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.315 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.886 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.316 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/500 (336.229 ms elapsed) --------------------
2022-12-04 21:24:43.316 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.341 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.357 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.382 - [NOTICE] callbacks.on_step_start(182): Cycle 4/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.424 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.859 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.425 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/500 (445.575 ms elapsed) --------------------
2022-12-04 21:24:43.426 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.450 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.463 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.485 - [NOTICE] callbacks.on_step_start(182): Cycle 5/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.531 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.833 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.532 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/500 (552.278 ms elapsed) --------------------
2022-12-04 21:24:43.532 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.552 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.572 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.592 - [NOTICE] callbacks.on_step_start(182): Cycle 6/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.640 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.807 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.641 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/500 (661.270 ms elapsed) --------------------
2022-12-04 21:24:43.642 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.666 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.682 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.704 - [NOTICE] callbacks.on_step_start(182): Cycle 7/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.762 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.781 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.763 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/500 (783.208 ms elapsed) --------------------
2022-12-04 21:24:43.763 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.788 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.805 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.832 - [NOTICE] callbacks.on_step_start(182): Cycle 8/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:43.883 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.756 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:43.889 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/500 (909.409 ms elapsed) --------------------
2022-12-04 21:24:43.902 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:43.939 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:43.955 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:43.977 - [NOTICE] callbacks.on_step_start(182): Cycle 9/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.022 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.732 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.023 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/500 (1.043 s elapsed) --------------------
2022-12-04 21:24:44.023 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.046 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.065 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.087 - [NOTICE] callbacks.on_step_start(182): Cycle 10/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.132 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.708 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.132 - [NOTICE] callbacks.on_cycle_start(174): Cycle 11/500 (1.153 s elapsed) --------------------
2022-12-04 21:24:44.132 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.157 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.172 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.194 - [NOTICE] callbacks.on_step_start(182): Cycle 11/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.238 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.684 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.238 - [NOTICE] callbacks.on_cycle_start(174): Cycle 12/500 (1.259 s elapsed) --------------------
2022-12-04 21:24:44.239 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.263 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.277 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.296 - [NOTICE] callbacks.on_step_start(182): Cycle 12/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.342 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.660 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.343 - [NOTICE] callbacks.on_cycle_start(174): Cycle 13/500 (1.363 s elapsed) --------------------
2022-12-04 21:24:44.343 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.363 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.377 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.399 - [NOTICE] callbacks.on_step_start(182): Cycle 13/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.447 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.637 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.448 - [NOTICE] callbacks.on_cycle_start(174): Cycle 14/500 (1.468 s elapsed) --------------------
2022-12-04 21:24:44.448 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.470 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.484 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.504 - [NOTICE] callbacks.on_step_start(182): Cycle 14/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.550 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.614 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.551 - [NOTICE] callbacks.on_cycle_start(174): Cycle 15/500 (1.572 s elapsed) --------------------
2022-12-04 21:24:44.551 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.572 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.586 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.604 - [NOTICE] callbacks.on_step_start(182): Cycle 15/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.651 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.592 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.651 - [NOTICE] callbacks.on_cycle_start(174): Cycle 16/500 (1.672 s elapsed) --------------------
2022-12-04 21:24:44.651 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.674 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.690 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.720 - [NOTICE] callbacks.on_step_start(182): Cycle 16/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.770 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.569 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.772 - [NOTICE] callbacks.on_cycle_start(174): Cycle 17/500 (1.792 s elapsed) --------------------
2022-12-04 21:24:44.774 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.802 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.828 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.851 - [NOTICE] callbacks.on_step_start(182): Cycle 17/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:44.897 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.548 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:44.898 - [NOTICE] callbacks.on_cycle_start(174): Cycle 18/500 (1.918 s elapsed) --------------------
2022-12-04 21:24:44.898 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:44.922 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:44.938 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:44.961 - [NOTICE] callbacks.on_step_start(182): Cycle 18/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.008 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.526 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.009 - [NOTICE] callbacks.on_cycle_start(174): Cycle 19/500 (2.030 s elapsed) --------------------
2022-12-04 21:24:45.010 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.033 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.046 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.065 - [NOTICE] callbacks.on_step_start(182): Cycle 19/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.111 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.505 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.111 - [NOTICE] callbacks.on_cycle_start(174): Cycle 20/500 (2.132 s elapsed) --------------------
2022-12-04 21:24:45.112 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.131 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.145 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.163 - [NOTICE] callbacks.on_step_start(182): Cycle 20/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.212 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.484 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.213 - [NOTICE] callbacks.on_cycle_start(174): Cycle 21/500 (2.233 s elapsed) --------------------
2022-12-04 21:24:45.213 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.235 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.249 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.268 - [NOTICE] callbacks.on_step_start(182): Cycle 21/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.314 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.463 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.315 - [NOTICE] callbacks.on_cycle_start(174): Cycle 22/500 (2.335 s elapsed) --------------------
2022-12-04 21:24:45.315 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.336 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.350 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.370 - [NOTICE] callbacks.on_step_start(182): Cycle 22/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.415 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.442 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.416 - [NOTICE] callbacks.on_cycle_start(174): Cycle 23/500 (2.436 s elapsed) --------------------
2022-12-04 21:24:45.416 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.437 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.450 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.471 - [NOTICE] callbacks.on_step_start(182): Cycle 23/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.690 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.422 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.690 - [NOTICE] callbacks.on_cycle_start(174): Cycle 24/500 (2.711 s elapsed) --------------------
2022-12-04 21:24:45.691 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.715 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.729 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.750 - [NOTICE] callbacks.on_step_start(182): Cycle 24/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.795 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.402 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.795 - [NOTICE] callbacks.on_cycle_start(174): Cycle 25/500 (2.816 s elapsed) --------------------
2022-12-04 21:24:45.796 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.819 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.833 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.855 - [NOTICE] callbacks.on_step_start(182): Cycle 25/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:45.899 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.382 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:45.900 - [NOTICE] callbacks.on_cycle_start(174): Cycle 26/500 (2.920 s elapsed) --------------------
2022-12-04 21:24:45.900 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:45.923 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:45.939 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:45.963 - [NOTICE] callbacks.on_step_start(182): Cycle 26/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.006 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.362 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.006 - [NOTICE] callbacks.on_cycle_start(174): Cycle 27/500 (3.027 s elapsed) --------------------
2022-12-04 21:24:46.007 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.028 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.043 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.061 - [NOTICE] callbacks.on_step_start(182): Cycle 27/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.109 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.343 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.109 - [NOTICE] callbacks.on_cycle_start(174): Cycle 28/500 (3.130 s elapsed) --------------------
2022-12-04 21:24:46.110 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.134 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.147 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.165 - [NOTICE] callbacks.on_step_start(182): Cycle 28/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.209 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.324 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.210 - [NOTICE] callbacks.on_cycle_start(174): Cycle 29/500 (3.230 s elapsed) --------------------
2022-12-04 21:24:46.210 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.233 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.247 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.265 - [NOTICE] callbacks.on_step_start(182): Cycle 29/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.310 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.305 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.311 - [NOTICE] callbacks.on_cycle_start(174): Cycle 30/500 (3.332 s elapsed) --------------------
2022-12-04 21:24:46.312 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.332 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.346 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.364 - [NOTICE] callbacks.on_step_start(182): Cycle 30/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.411 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.286 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.411 - [NOTICE] callbacks.on_cycle_start(174): Cycle 31/500 (3.432 s elapsed) --------------------
2022-12-04 21:24:46.412 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.432 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.445 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.465 - [NOTICE] callbacks.on_step_start(182): Cycle 31/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.508 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.267 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.509 - [NOTICE] callbacks.on_cycle_start(174): Cycle 32/500 (3.529 s elapsed) --------------------
2022-12-04 21:24:46.509 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.528 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.546 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.566 - [NOTICE] callbacks.on_step_start(182): Cycle 32/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.612 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.249 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.613 - [NOTICE] callbacks.on_cycle_start(174): Cycle 33/500 (3.634 s elapsed) --------------------
2022-12-04 21:24:46.613 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.635 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.650 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.670 - [NOTICE] callbacks.on_step_start(182): Cycle 33/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.714 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.231 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.714 - [NOTICE] callbacks.on_cycle_start(174): Cycle 34/500 (3.735 s elapsed) --------------------
2022-12-04 21:24:46.715 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.736 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.749 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.769 - [NOTICE] callbacks.on_step_start(182): Cycle 34/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.814 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.213 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.814 - [NOTICE] callbacks.on_cycle_start(174): Cycle 35/500 (3.835 s elapsed) --------------------
2022-12-04 21:24:46.815 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.837 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.857 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:46.877 - [NOTICE] callbacks.on_step_start(182): Cycle 35/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:46.922 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.195 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:46.926 - [NOTICE] callbacks.on_cycle_start(174): Cycle 36/500 (3.947 s elapsed) --------------------
2022-12-04 21:24:46.928 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:46.952 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:46.986 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.020 - [NOTICE] callbacks.on_step_start(182): Cycle 36/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.094 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.177 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.095 - [NOTICE] callbacks.on_cycle_start(174): Cycle 37/500 (4.115 s elapsed) --------------------
2022-12-04 21:24:47.095 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.120 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.134 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.151 - [NOTICE] callbacks.on_step_start(182): Cycle 37/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.195 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.160 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.196 - [NOTICE] callbacks.on_cycle_start(174): Cycle 38/500 (4.216 s elapsed) --------------------
2022-12-04 21:24:47.196 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.216 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.233 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.252 - [NOTICE] callbacks.on_step_start(182): Cycle 38/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.296 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.143 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.297 - [NOTICE] callbacks.on_cycle_start(174): Cycle 39/500 (4.317 s elapsed) --------------------
2022-12-04 21:24:47.297 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.318 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.333 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.352 - [NOTICE] callbacks.on_step_start(182): Cycle 39/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.396 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.126 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.397 - [NOTICE] callbacks.on_cycle_start(174): Cycle 40/500 (4.417 s elapsed) --------------------
2022-12-04 21:24:47.397 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.418 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.432 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.450 - [NOTICE] callbacks.on_step_start(182): Cycle 40/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.493 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.109 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.494 - [NOTICE] callbacks.on_cycle_start(174): Cycle 41/500 (4.514 s elapsed) --------------------
2022-12-04 21:24:47.494 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.512 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.528 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.551 - [NOTICE] callbacks.on_step_start(182): Cycle 41/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.594 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.092 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.594 - [NOTICE] callbacks.on_cycle_start(174): Cycle 42/500 (4.615 s elapsed) --------------------
2022-12-04 21:24:47.594 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.613 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.627 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.649 - [NOTICE] callbacks.on_step_start(182): Cycle 42/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.693 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.075 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.693 - [NOTICE] callbacks.on_cycle_start(174): Cycle 43/500 (4.714 s elapsed) --------------------
2022-12-04 21:24:47.694 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.716 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.729 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.748 - [NOTICE] callbacks.on_step_start(182): Cycle 43/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.791 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.059 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.792 - [NOTICE] callbacks.on_cycle_start(174): Cycle 44/500 (4.813 s elapsed) --------------------
2022-12-04 21:24:47.793 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.817 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.835 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.854 - [NOTICE] callbacks.on_step_start(182): Cycle 44/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:47.899 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.042 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:47.900 - [NOTICE] callbacks.on_cycle_start(174): Cycle 45/500 (4.920 s elapsed) --------------------
2022-12-04 21:24:47.900 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:47.922 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:47.938 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:47.958 - [NOTICE] callbacks.on_step_start(182): Cycle 45/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.003 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.026 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:48.003 - [NOTICE] callbacks.on_cycle_start(174): Cycle 46/500 (5.024 s elapsed) --------------------
2022-12-04 21:24:48.004 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:48.027 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:48.041 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:48.060 - [NOTICE] callbacks.on_step_start(182): Cycle 46/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.117 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.010 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:48.122 - [NOTICE] callbacks.on_cycle_start(174): Cycle 47/500 (5.142 s elapsed) --------------------
2022-12-04 21:24:48.127 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:48.155 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:48.193 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:48.208 - [NOTICE] callbacks.on_step_start(182): Cycle 47/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.256 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.994 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:48.256 - [NOTICE] callbacks.on_cycle_start(174): Cycle 48/500 (5.277 s elapsed) --------------------
2022-12-04 21:24:48.257 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:48.274 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:48.290 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:48.309 - [NOTICE] callbacks.on_step_start(182): Cycle 48/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.356 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.978 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:48.357 - [NOTICE] callbacks.on_cycle_start(174): Cycle 49/500 (5.378 s elapsed) --------------------
2022-12-04 21:24:48.358 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:48.375 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:48.389 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:48.410 - [NOTICE] callbacks.on_step_start(182): Cycle 49/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.457 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 3.962 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:48.458 - [NOTICE] callbacks.on_cycle_start(174): Cycle 50/500 (5.478 s elapsed) --------------------
2022-12-04 21:24:48.458 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:48.478 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 2/4: Rest for 1 hour
2022-12-04 21:24:48.494 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:48.513 - [NOTICE] callbacks.on_step_start(182): Cycle 50/500, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:48.560 - [NOTICE] callbacks.on_cycle_end(201): Stopping experiment since capacity (3.947 Ah) is below stopping capacity (3.952 Ah).
2022-12-04 21:24:48.561 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 5.580 s
[11]:
sol_int.cycles
[11]:
[<pybamm.solvers.solution.Solution at 0x1729c8130>,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x17299b760>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172a40220>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172b05190>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172c5f2e0>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172d43730>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172bc1ac0>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172f10940>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x172ec28e0>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x1731078b0>,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x1731a2520>]
[12]:
sol_list.cycles
[12]:
[<pybamm.solvers.solution.Solution at 0x17331d670>,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x17330ef10>,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 None,
 <pybamm.solvers.solution.Solution at 0x173482220>,
 None,
 None,
 None,
 None,
 None]

For the cycles that are saved, you can plot as usual (note off-by-1 indexing)

[13]:
sol_list.cycles[44].plot(["Current [A]", "Voltage [V]"])
[13]:
<pybamm.plotting.quick_plot.QuickPlot at 0x172afde20>
[14]:
fig, ax = plt.subplots(1, 2, figsize=(10, 5))
for cycle in sol_int.cycles:
    if cycle is not None:
        t = cycle["Time [h]"].data - cycle["Time [h]"].data[0]
        ax[0].plot(t, cycle["Current [A]"].data)
        ax[0].set_xlabel("Time [h]")
        ax[0].set_title("Current [A]")
        ax[1].plot(t, cycle["Voltage [V]"].data)
        ax[1].set_xlabel("Time [h]")
        ax[1].set_title("Voltage [V]")
_images/source_examples_notebooks_simulations_and_experiments_simulating-long-experiments_29_0.png

All summary variables are always available for every cycle, since these are much less memory-intensive

[15]:
pybamm.plot_summary_variables(sol_list)
_images/source_examples_notebooks_simulations_and_experiments_simulating-long-experiments_31_0.png
[15]:
array([[<AxesSubplot:xlabel='Cycle number', ylabel='Capacity [A.h]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of lithium inventory [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of capacity to SEI [A.h]'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in negative electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in positive electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='x_100'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='x_0'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_100'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_0'>]], dtype=object)

Starting solution#

A simulation can be performed iteratively by using the starting_solution feature. For example, we first solve for 10 cycles

[16]:
experiment = pybamm.Experiment(
    [
        (
            "Discharge at 1C until 3V",
            "Rest for 1 hour",
            "Charge at 1C until 4.2V",
            "Hold at 4.2V until C/50",
        )
    ]
    * 10,
    termination="80% capacity",
)
sim = pybamm.Simulation(spm, experiment=experiment, parameter_values=parameter_values)
sol = sim.solve()
2022-12-04 21:24:50.503 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/10 (22.792 us elapsed) --------------------
2022-12-04 21:24:50.503 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:50.544 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:50.572 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:50.613 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:50.754 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.941 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:50.754 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/10 (251.450 ms elapsed) --------------------
2022-12-04 21:24:50.755 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:50.779 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:50.800 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:50.825 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:50.868 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.913 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:50.869 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/10 (366.027 ms elapsed) --------------------
2022-12-04 21:24:50.869 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:50.893 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:50.909 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:50.934 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:50.988 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.886 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:50.989 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/10 (486.547 ms elapsed) --------------------
2022-12-04 21:24:50.990 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.016 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.031 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.055 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.103 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.859 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.104 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/10 (600.658 ms elapsed) --------------------
2022-12-04 21:24:51.104 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.128 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.143 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.171 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.230 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.833 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.230 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/10 (727.148 ms elapsed) --------------------
2022-12-04 21:24:51.230 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.253 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.267 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.288 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.337 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.807 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.338 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/10 (834.650 ms elapsed) --------------------
2022-12-04 21:24:51.338 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.361 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.381 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.401 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.449 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.781 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.450 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/10 (946.869 ms elapsed) --------------------
2022-12-04 21:24:51.450 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.470 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.484 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.503 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.551 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.756 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.552 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/10 (1.049 s elapsed) --------------------
2022-12-04 21:24:51.552 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.575 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.589 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.611 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.659 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.732 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.660 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/10 (1.157 s elapsed) --------------------
2022-12-04 21:24:51.660 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.682 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.695 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.717 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.766 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.708 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.767 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 1.263 s

If we give sol as the starting solution this will then solve for the next 10 cycles

[17]:
sol2 = sim.solve(starting_solution=sol)
2022-12-04 21:24:51.827 - [NOTICE] callbacks.on_cycle_start(174): Cycle 11/20 (30.792 us elapsed) --------------------
2022-12-04 21:24:51.828 - [NOTICE] callbacks.on_step_start(182): Cycle 11/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.859 - [NOTICE] callbacks.on_step_start(182): Cycle 11/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:51.876 - [NOTICE] callbacks.on_step_start(182): Cycle 11/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:51.900 - [NOTICE] callbacks.on_step_start(182): Cycle 11/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:51.974 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.684 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:51.974 - [NOTICE] callbacks.on_cycle_start(174): Cycle 12/20 (147.399 ms elapsed) --------------------
2022-12-04 21:24:51.975 - [NOTICE] callbacks.on_step_start(182): Cycle 12/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:51.997 - [NOTICE] callbacks.on_step_start(182): Cycle 12/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.010 - [NOTICE] callbacks.on_step_start(182): Cycle 12/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.028 - [NOTICE] callbacks.on_step_start(182): Cycle 12/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.227 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.660 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.227 - [NOTICE] callbacks.on_cycle_start(174): Cycle 13/20 (400.113 ms elapsed) --------------------
2022-12-04 21:24:52.227 - [NOTICE] callbacks.on_step_start(182): Cycle 13/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.245 - [NOTICE] callbacks.on_step_start(182): Cycle 13/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.258 - [NOTICE] callbacks.on_step_start(182): Cycle 13/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.276 - [NOTICE] callbacks.on_step_start(182): Cycle 13/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.326 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.637 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.326 - [NOTICE] callbacks.on_cycle_start(174): Cycle 14/20 (498.938 ms elapsed) --------------------
2022-12-04 21:24:52.326 - [NOTICE] callbacks.on_step_start(182): Cycle 14/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.345 - [NOTICE] callbacks.on_step_start(182): Cycle 14/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.360 - [NOTICE] callbacks.on_step_start(182): Cycle 14/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.378 - [NOTICE] callbacks.on_step_start(182): Cycle 14/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.428 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.614 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.428 - [NOTICE] callbacks.on_cycle_start(174): Cycle 15/20 (601.364 ms elapsed) --------------------
2022-12-04 21:24:52.429 - [NOTICE] callbacks.on_step_start(182): Cycle 15/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.450 - [NOTICE] callbacks.on_step_start(182): Cycle 15/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.463 - [NOTICE] callbacks.on_step_start(182): Cycle 15/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.481 - [NOTICE] callbacks.on_step_start(182): Cycle 15/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.534 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.592 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.534 - [NOTICE] callbacks.on_cycle_start(174): Cycle 16/20 (707.075 ms elapsed) --------------------
2022-12-04 21:24:52.535 - [NOTICE] callbacks.on_step_start(182): Cycle 16/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.558 - [NOTICE] callbacks.on_step_start(182): Cycle 16/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.571 - [NOTICE] callbacks.on_step_start(182): Cycle 16/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.593 - [NOTICE] callbacks.on_step_start(182): Cycle 16/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.645 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.569 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.645 - [NOTICE] callbacks.on_cycle_start(174): Cycle 17/20 (818.355 ms elapsed) --------------------
2022-12-04 21:24:52.646 - [NOTICE] callbacks.on_step_start(182): Cycle 17/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.669 - [NOTICE] callbacks.on_step_start(182): Cycle 17/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.683 - [NOTICE] callbacks.on_step_start(182): Cycle 17/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.709 - [NOTICE] callbacks.on_step_start(182): Cycle 17/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.762 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.548 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.763 - [NOTICE] callbacks.on_cycle_start(174): Cycle 18/20 (935.547 ms elapsed) --------------------
2022-12-04 21:24:52.763 - [NOTICE] callbacks.on_step_start(182): Cycle 18/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.786 - [NOTICE] callbacks.on_step_start(182): Cycle 18/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.799 - [NOTICE] callbacks.on_step_start(182): Cycle 18/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.821 - [NOTICE] callbacks.on_step_start(182): Cycle 18/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.877 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.526 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.878 - [NOTICE] callbacks.on_cycle_start(174): Cycle 19/20 (1.051 s elapsed) --------------------
2022-12-04 21:24:52.878 - [NOTICE] callbacks.on_step_start(182): Cycle 19/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:52.900 - [NOTICE] callbacks.on_step_start(182): Cycle 19/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:52.914 - [NOTICE] callbacks.on_step_start(182): Cycle 19/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:52.933 - [NOTICE] callbacks.on_step_start(182): Cycle 19/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:52.988 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.505 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:52.988 - [NOTICE] callbacks.on_cycle_start(174): Cycle 20/20 (1.161 s elapsed) --------------------
2022-12-04 21:24:52.989 - [NOTICE] callbacks.on_step_start(182): Cycle 20/20, step 1/4: Discharge at 1C until 3V
2022-12-04 21:24:53.007 - [NOTICE] callbacks.on_step_start(182): Cycle 20/20, step 2/4: Rest for 1 hour
2022-12-04 21:24:53.020 - [NOTICE] callbacks.on_step_start(182): Cycle 20/20, step 3/4: Charge at 1C until 4.2V
2022-12-04 21:24:53.039 - [NOTICE] callbacks.on_step_start(182): Cycle 20/20, step 4/4: Hold at 4.2V until C/50
2022-12-04 21:24:53.094 - [NOTICE] callbacks.on_cycle_end(196): Capacity is now 4.484 Ah (originally 4.941 Ah, will stop at 3.952 Ah)
2022-12-04 21:24:53.095 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 1.267 s

We have now simulated 20 cycles

[18]:
len(sol2.cycles)
[18]:
20

References#

The relevant papers for this notebook are:

[19]:
pybamm.print_citations()
[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.
[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.
[3] 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:.
[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.
[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.
[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.
[7] 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.
[8] Peyman Mohtat, Suhak Lee, Valentin Sulzer, Jason B. Siegel, and Anna G. Stefanopoulou. Differential Expansion and Voltage Model for Li-ion Batteries at Practical Charging Rates. Journal of The Electrochemical Society, 167(11):110561, 2020. doi:10.1149/1945-7111/aba5d1.
[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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

A step-by-step look at the Simulation class#

The simplest way to solve a model is to use the Simulation class. This automatically processes the model (setting of parameters, setting up the mesh and discretisation, etc.) for you, and provides built-in functionality for solving and plotting. Changing things such as parameters in handled by passing options to the Simulation, as shown in the Getting Started guides, example notebooks and documentation.

In this notebook we show how to solve a model using a Simulation and compare this to manually handling the different stages of the process, such as setting parameters, ourselves step-by-step.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

Simulation#

The easiest way to get started is to pick a model and create a simulation using that model. For simplicity, we’ll use the SPM with all the default options here.

[2]:
model = pybamm.lithium_ion.SPM()
simulation = pybamm.Simulation(model)

The simulation can then be solved, passing a time interval (in seconds) to integrate over

[3]:
simulation.solve([0, 3600])
[3]:
<pybamm.solvers.solution.Solution at 0x2698c4bb3d0>

and the results plotted

[4]:
simulation.plot()
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x2698c4da370>

Simple!

A GIF of the simulation can also be obtained

[5]:
# using less number of images in the example
# for a smoother GIF use more images
simulation.create_gif(
    number_of_images=5, duration=0.2, output_filename="simulation.gif"
)

Displaying the GIF using markdown - plot

Processing the model step-by-step#

One way of gaining more control over the simulation processing is by passing options, as outlined in the documentation. However, you can also process the model step-by-step yourself. A detailed example of this can be found in the SPM notebook, but here we outline the basic steps.

First we pick a model

[6]:
model = pybamm.lithium_ion.SPM()

Next we must set up the geometry. We’ll use the default geometry for the SPM. In all of the following steps we will also use the default settings provided by the model. For a look at changing these options, see the change settings notebook.

[7]:
geometry = model.default_geometry

Both the model and geometry depend on parameters (such as the electrode thickness, or diffusivity). We’ll use the default model parameters

[8]:
param = model.default_parameter_values

Now that we have picked our parameters we can “process” the model and geometry. This just means we look through the model and geometry for any parameter symbols and replace them with the numeric values (or functions, in the case of parameters that have functional dependence) defined by our parameter values.

[9]:
param.process_model(model)
param.process_geometry(geometry)

Next we must create a mesh on which to solve the discretised equations. This not only depends on the geometry, but also on the type of submesh (e.g. uniformly space) and number of mesh points to use.

[10]:
mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)

Now that we have defined a mesh we can discretise our model. In order to do so we must choose a spatial method. The default for the SPM is the Finite Volume Method. We first define a discretisation, which depends on the mesh and spatial method, and then use this to process our model. This turns the variables in the models into a StateVector, and replaces spatial operators with matrix-vector multiplications, ready to be passed to a time stepping algorithm.

[11]:
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model)
[11]:
<pybamm.models.full_battery_models.lithium_ion.spm.SPM at 0x2699d43f100>

Finally, we pick a solver to step the problem forward in time. We’ll use the default ODE solver for the SPM

[12]:
solver = model.default_solver

We then integrate in time using the solve command, as with the simulation. Note that we now have to pass the model object to the solve command, and that we return the solution object so that we can interact with it later.

[13]:
solution = solver.solve(model, [0, 3600])

We can create the default slider plot by passing the solution object to the dynamic_plot method

[14]:
pybamm.dynamic_plot(solution)
[14]:
<pybamm.plotting.quick_plot.QuickPlot at 0x2699064c760>

References#

The relevant papers for this notebook are:

[15]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Customizing QuickPlot#

This notebook shows how to customize PyBaMM’s QuickPlot, using matplotlib’s style sheets and rcParams

First we define and solve the models

[8]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

models = [pybamm.lithium_ion.SPM(), pybamm.lithium_ion.SPMe(), pybamm.lithium_ion.DFN()]
sims = []
for model in models:
    sim = pybamm.Simulation(model)
    sim.solve([0, 3600])
    sims.append(sim)

[notice] A new release of pip is available: 23.0.1 -> 23.1.2
[notice] To update, run: pip install --upgrade pip
Note: you may need to restart the kernel to use updated packages.

Call the default plots

[9]:
pybamm.dynamic_plot(sims)
[9]:
<pybamm.plotting.quick_plot.QuickPlot at 0x28810b850>

Using style sheets#

The easiest way to customize style is to use one of matplotlib’s available style sheets

[10]:
import matplotlib.pyplot as plt

plt.style.available
[10]:
['Solarize_Light2',
 '_classic_test_patch',
 '_mpl-gallery',
 '_mpl-gallery-nogrid',
 'bmh',
 'classic',
 'dark_background',
 'fast',
 'fivethirtyeight',
 'ggplot',
 'grayscale',
 'seaborn-v0_8',
 'seaborn-v0_8-bright',
 'seaborn-v0_8-colorblind',
 'seaborn-v0_8-dark',
 'seaborn-v0_8-dark-palette',
 'seaborn-v0_8-darkgrid',
 'seaborn-v0_8-deep',
 'seaborn-v0_8-muted',
 'seaborn-v0_8-notebook',
 'seaborn-v0_8-paper',
 'seaborn-v0_8-pastel',
 'seaborn-v0_8-poster',
 'seaborn-v0_8-talk',
 'seaborn-v0_8-ticks',
 'seaborn-v0_8-white',
 'seaborn-v0_8-whitegrid',
 'tableau-colorblind10']

For example we can use the ggplot style from R. In this case, the title fonts are quite large, so we reduce the number of words in a title before a line break

[11]:
plt.style.use("ggplot")
pybamm.settings.max_words_in_line = 3
pybamm.dynamic_plot(sims)
[11]:
<pybamm.plotting.quick_plot.QuickPlot at 0x289590050>

Another good set of style sheets for scientific plots is available by pip installing the SciencePlots package

Further customization using rcParams#

Sometimes we want further customization of a style, without needing to edit the style sheets. For example, we can update the font sizes and plot again.

To change the line colors, we use cycler

[12]:
import matplotlib as mpl
from cycler import cycler

mpl.rcParams["axes.labelsize"] = 12
mpl.rcParams["axes.titlesize"] = 12
mpl.rcParams["xtick.labelsize"] = 12
mpl.rcParams["ytick.labelsize"] = 12
mpl.rcParams["legend.fontsize"] = 12
mpl.rcParams["axes.prop_cycle"] = cycler("color", ["k", "g", "c"])
pybamm.dynamic_plot(sims)
[12]:
<pybamm.plotting.quick_plot.QuickPlot at 0x282c16290>

Very fine customization#

Some customization of the QuickPlot object is possible by passing arguments - see the docs for details

We can also further control the plot by calling plot.fig after the figure has been created, and editing the matplotlib objects. For example, here we move the titles to the ylabel, and move the legend.

[13]:
pybamm.settings.max_words_in_line = 4

plot = pybamm.QuickPlot(sims, figsize=(14, 7))
plot.plot(0.5)  # time in hours

# Move title to ylabel
for ax in plot.fig.axes:
    title = ax.get_title()
    ax.set_title("")
    ax.set_ylabel(title)

# Remove old legend and add a new one in the bottom
leg = plot.fig.get_children()[-1]
leg.set_visible(False)
plot.fig.legend(plot.labels, loc="lower center", ncol=len(plot.labels), fontsize=11)

# Adjust layout
plot.gridspec.tight_layout(plot.fig, rect=[0, 0.04, 1, 1])
_images/source_examples_notebooks_plotting_customize-quick-plot_17_0.png

The figure can then be saved using plot.fig.savefig

References#

The relevant papers for this notebook are:

[14]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Plot voltage components#

In a 1D model, the voltage of a cell is given by the difference in solid-phase potentials at the two edges of the domain

\[V = \left.\phi_\mathrm{s,p}\right\rvert_{x=L} - \left.\phi_\mathrm{s,n}\right\rvert_{x=0}\]

We can decompose this voltage into constituent components by adding and subtracting terms and regrouping:

\[\begin{split}\begin{align*} V =& \left(\left.\phi_\mathrm{s,p}\right\rvert_{x=L} - \bar{\phi}_\mathrm{s,p}\right) &\quad&\text{Positive solid phase ohmic losses} \\ &- \left(\left.\phi_\mathrm{s,n}\right\rvert_{x=L} - \bar{\phi}_\mathrm{s,n}\right) &&\text{Negative solid phase ohmic losses} \\ &+ \bar{\eta}_\mathrm{p} &&\text{Positive reaction overpotential} \\ &- \bar{\eta}_\mathrm{n} &&\text{Negative reaction overpotential} \\ &+ \left(\bar{\phi}_\mathrm{e,p}-\bar{\phi}_\mathrm{e,n}\right) &&\text{Electrolyte overpotential} \\ &+ \left(\overline{U(c_\mathrm{p}^\mathrm{surf})}-U(\bar{c}_\mathrm{p})\right) &&\text{Positive particle overpotential} \\ &- \left(\overline{U(c_\mathrm{n}^\mathrm{surf})}-U(\bar{c}_\mathrm{n})\right) &&\text{Negative particle overpotential} \\ &+ U(\bar{c}_\mathrm{p}) &&\text{Positive bulk open-circuit potential} \\ &- U(\bar{c}_\mathrm{n}) &&\text{Negative bulk open-circuit potential} \end{align*}\end{split}\]

where

\[\eta = \phi_\mathrm{s} - \phi_\mathrm{e} - U(c^\mathrm{surf})\]

and \(\bar{\cdot}\) represents the average value of the variable over its subdomain.

To demonstrate this, we solve a standard DFN model with the Chen2020 parameter set and plot the various potentials

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

model = pybamm.lithium_ion.DFN()

experiment = pybamm.Experiment(["Discharge at 1C until 2.5 V"])

sim = pybamm.Simulation(
    model, experiment=experiment, parameter_values=pybamm.ParameterValues("Chen2020")
)
sol = sim.solve()

sol.plot(
    [
        "Negative electrode bulk open-circuit potential [V]",
        "Positive electrode bulk open-circuit potential [V]",
        "Negative particle concentration overpotential [V]",
        "Positive particle concentration overpotential [V]",
        "X-averaged negative electrode reaction overpotential [V]",
        "X-averaged positive electrode reaction overpotential [V]",
        "X-averaged concentration overpotential [V]",
        "X-averaged electrolyte ohmic losses [V]",
        "X-averaged negative electrode ohmic losses [V]",
        "X-averaged positive electrode ohmic losses [V]",
    ],
)
Note: you may need to restart the kernel to use updated packages.
At t = 472.93 and h = 1.932e-14, the corrector convergence failed repeatedly or with |h| = hmin.
At t = 232.93 and h = 3.07648e-13, the corrector convergence failed repeatedly or with |h| = hmin.
[1]:
<pybamm.plotting.quick_plot.QuickPlot at 0x1217ec4f0>

A more useful visualization is given by the plot_voltage_components function, which can either plot all the individual voltage components

[2]:
pybamm.plot_voltage_components(sol, split_by_electrode=True)
_images/source_examples_notebooks_plotting_plot-voltage-components_5_0.png
[2]:
(<Figure size 800x400 with 1 Axes>, <AxesSubplot: xlabel='Time [h]'>)

or group positive and negative together

[3]:
pybamm.plot_voltage_components(sol)
_images/source_examples_notebooks_plotting_plot-voltage-components_7_0.png
[3]:
(<Figure size 800x400 with 1 Axes>, <AxesSubplot: xlabel='Time [h]'>)

Note that this function further splits the electrolyte overpotential into the contribution from the electrolyte gradient and the contribution from ohmic losses

References#

The relevant papers for this notebook are:

[4]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[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.
[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.
[7] 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.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.
[9] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

DAE solver#

In this notebook, we show some examples of solving a DAE model. For the purposes of this example, we use the CasADi solver, but the syntax remains the same for other solvers

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

Integrating DAEs#

In PyBaMM, a model is solved by calling a solver with solve. This sets up the model to be solved, and then calls the method _integrate, which is specific to each solver. We begin by setting up and discretising a model

[2]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
v = pybamm.Variable("v")
model.rhs = {u: -v}  # du/dt = -v
model.algebraic = {v: 2 * u - v}  # 2*v = u
model.initial_conditions = {u: 1, v: 2}
model.variables = {"u": u, "v": v}

# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model);

Now the model can be solved by calling solver.solve with a specific time vector at which to evaluate the solution

[3]:
# Solve #################################
t_eval = np.linspace(0, 2, 30)
dae_solver = pybamm.CasadiSolver(mode="safe")
solution = dae_solver.solve(model, t_eval)
#########################################

# Extract u and v
t_sol = solution.t
u = solution["u"]
v = solution["v"]

# Plot
t_fine = np.linspace(0, t_eval[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, np.exp(-2 * t_fine), t_sol, u(t_sol), "o")
ax1.set_xlabel("t")
ax1.legend(["exp(-2*t)", "u"], loc="best")

ax2.plot(t_fine, 2 * np.exp(-2 * t_fine), t_sol, v(t_sol), "o")
ax2.set_xlabel("t")
ax2.legend(["2*exp(-2*t)", "v"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_solvers_dae-solver_6_0.png

Note that, where possible, the solver makes use of the mass matrix and jacobian for the model. However, the discretisation or solver will have created the mass matrix and jacobian algorithmically, using the expression tree, so we do not need to calculate and input these manually.

The solution terminates at the final simulation time:

[4]:
solution.termination
[4]:
'final time'
Events#

It is possible to specify events at which a solution should terminate. This is done by adding events to the model.events dictionary. In the following example, we solve the same model as before but add a termination event when v=-0.2.

[5]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
v = pybamm.Variable("v")
model.rhs = {u: -v}  # du/dt = -v
model.algebraic = {v: 2 * u - v}  # 2*v = u
model.initial_conditions = {u: 1, v: 2}
model.events.append(pybamm.Event("v=0.2", v - 0.2))  # adding event here

model.variables = {"u": u, "v": v}

# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model)

# Solve #################################
t_eval = np.linspace(0, 2, 30)
dae_solver = pybamm.CasadiSolver(mode="safe")
solution = dae_solver.solve(model, t_eval)
#########################################

# Extract u and v
t_sol = solution.t
u = solution["u"]
v = solution["v"]

# Plot
t_fine = np.linspace(0, t_eval[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, np.exp(-2 * t_fine), t_sol, u(t_sol), "o")
ax1.set_xlabel("t")
ax1.legend(["exp(-2*t)", "u"], loc="best")

ax2.plot(
    t_fine,
    2 * np.exp(-2 * t_fine),
    t_sol,
    v(t_sol),
    "o",
    t_fine,
    0.2 * np.ones_like(t_fine),
    "k",
)
ax2.set_xlabel("t")
ax2.legend(["2*exp(-2*t)", "v", "v = 0.2"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_solvers_dae-solver_11_0.png

Now the solution terminates because the event has been reached

[6]:
solution.termination
[6]:
'event: v=0.2'

Finding consistent initial conditions#

The solver will fail if initial conditions that are inconsistent with the algebraic equations are provided. However, before solving the DAE solvers automatically use _set_initial_conditions to obtain consistent initial conditions, starting from a guess of bad initial conditions, using a simple root-finding algorithm.

[7]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
v = pybamm.Variable("v")
model.rhs = {u: -v}  # du/dt = -v
model.algebraic = {v: 2 * u - v}  # 2*v = u
model.initial_conditions = {u: 1, v: 1}  # bad initial conditions, solver fixes
model.events.append(pybamm.Event("v=0.2", v - 0.2))
model.variables = {"u": u, "v": v}

# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model)
print(f"y0_guess={model.concatenated_initial_conditions.evaluate().flatten()}")

dae_solver = pybamm.CasadiSolver(mode="safe")
dae_solver.set_up(model)
dae_solver._set_initial_conditions(model, 0, {}, True)
print(f"y0_fixed={model.y0}")
y0_guess=[1. 1.]
y0_fixed=[1, 2]

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

ODE solver#

In this notebook, we show some examples of solving an ODE model. For the purposes of this example, we use the Scipy solver, but the syntax remains the same for other solvers

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

Integrating ODEs#

In PyBaMM, a model is solved by calling a solver with solve. This sets up the model to be solved, and then calls the method _integrate, which is specific to each solver. We begin by setting up and discretising a model

[2]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
v = pybamm.Variable("v")
model.rhs = {u: -v, v: u}
model.initial_conditions = {u: 2, v: 1}
model.variables = {"u": u, "v": v}


# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model);

Now the model can be solved by calling solver.solve with a specific time vector at which to evaluate the solution

[3]:
# Solve ########################
t_eval = np.linspace(0, 5, 30)
ode_solver = pybamm.ScipySolver()
solution = ode_solver.solve(model, t_eval)
################################

# Extract u and v
t_sol = solution.t
u = solution["u"]
v = solution["v"]

# Plot
t_fine = np.linspace(0, t_eval[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, 2 * np.cos(t_fine) - np.sin(t_fine), t_sol, u(t_sol), "o")
ax1.set_xlabel("t")
ax1.legend(["2*cos(t) - sin(t)", "u"], loc="best")

ax2.plot(t_fine, 2 * np.sin(t_fine) + np.cos(t_fine), t_sol, v(t_sol), "o")
ax2.set_xlabel("t")
ax2.legend(["2*sin(t) + cos(t)", "v"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_solvers_ode-solver_6_0.png

Note that, where possible, the solver makes use of the mass matrix and jacobian for the model. However, the discretisation or solver will have created the mass matrix and jacobian algorithmically, using the expression tree, so we do not need to calculate and input these manually.

The solution terminates at the final simulation time:

[4]:
solution.termination
[4]:
'final time'
Events#

It is possible to specify events at which a solution should terminate. This is done by adding events to the model.events dictionary. In the following example, we solve the same model as before but add a termination event when v=-2.

[5]:
# Create model
model = pybamm.BaseModel()
u = pybamm.Variable("u")
v = pybamm.Variable("v")
model.rhs = {u: -v, v: u}
model.initial_conditions = {u: 2, v: 1}
model.events.append(pybamm.Event("v=-2", v + 2))  # New termination event
model.variables = {"u": u, "v": v}

# Discretise using default discretisation
disc = pybamm.Discretisation()
disc.process_model(model)

# Solve ########################
t_eval = np.linspace(0, 5, 30)
ode_solver = pybamm.ScipySolver()
solution = ode_solver.solve(model, t_eval)
################################

# Extract u and v
t_sol = solution.t
u = solution["u"]
v = solution["v"]

# Plot
t_fine = np.linspace(0, t_eval[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(t_fine, 2 * np.cos(t_fine) - np.sin(t_fine), t_sol, u(t_sol), "o")
ax1.set_xlabel("t")
ax1.legend(["2*cos(t) - sin(t)", "u"], loc="best")

ax2.plot(
    t_fine,
    2 * np.sin(t_fine) + np.cos(t_fine),
    t_sol,
    v(t_sol),
    "o",
    t_fine,
    -2 * np.ones_like(t_fine),
    "k",
)
ax2.set_xlabel("t")
ax2.legend(["2*sin(t) + cos(t)", "v", "v = -2"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_solvers_ode-solver_11_0.png

Now the solution terminates because the event has been reached

[6]:
solution.termination
[6]:
'event: v=-2'
[7]:
print("event time: ", solution.t_event, "\nevent state", solution.y_event.flatten())
event time:  [3.78510677]
event state [-0.99995359 -2.        ]

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Speeding up the solvers#

This notebook contains a collection of tips on how to speed up the solvers

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import matplotlib.pyplot as plt
import numpy as np
Note: you may need to restart the kernel to use updated packages.

Choosing a solver#

Since it is very easy to switch which solver is used for the model, we recommend you try different solvers for your particular use case. In general, the CasadiSolver is the fastest.

Once you have found a good solver, you can further improve performance by trying out different values for the method, rtol, and atol arguments. Further options are sometimes available, but are solver specific. See solver API docs for details.

Choosing and optimizing CasadiSolver settings#

Fast mode vs safe mode#

The CasadiSolver comes with a mode argument which can be set to “fast” or “safe” (the third option, “safe without grid”, is experimental and should not be used for now). The “fast” mode is faster but ignores any “events” (such as a voltage cut-off point), while the “safe” mode is slower but does stop at events (with manually implemented “step-and-check” under the hood). Therefore, “fast” mode should be used whenever events are not expected to be hit (for example, when simulating a drive cycle or a constant-current discharge where the time interval is such that the simulation will finish before reaching the voltage cut-off). Conversely, “safe” mode should be used whenever events are important: in particular, when using the Experiment class.

To demonstrate the difference between safe mode and fast mode, consider the following example

[2]:
# Set up model
model = pybamm.lithium_ion.DFN()
param = model.default_parameter_values
cap = param["Nominal cell capacity [A.h]"]
param["Current function [A]"] = cap * pybamm.InputParameter("Crate")
sim = pybamm.Simulation(model, parameter_values=param)

# Set up solvers. Reduce max_num_steps for the fast solver, for faster errors
fast_solver = pybamm.CasadiSolver(
    mode="fast", extra_options_setup={"max_num_steps": 1000}
)
safe_solver = pybamm.CasadiSolver(mode="safe")

Both solvers can solve the model up to 3700 s, but the fast solver ignores the voltage cut-off around 3.1 V

[3]:
safe_sol = sim.solve([0, 3700], solver=safe_solver, inputs={"Crate": 1})
fast_sol = sim.solve([0, 3700], solver=fast_solver, inputs={"Crate": 1})

timer = pybamm.Timer()
print("Safe:", safe_sol.solve_time)
print("Fast:", fast_sol.solve_time)

cutoff = param["Lower voltage cut-off [V]"]
plt.plot(fast_sol["Time [h]"].data, fast_sol["Voltage [V]"].data, "b-", label="Fast")
plt.plot(safe_sol["Time [h]"].data, safe_sol["Voltage [V]"].data, "r-", label="Safe")
plt.plot(
    fast_sol["Time [h]"].data,
    cutoff * np.ones_like(fast_sol["Time [h]"].data),
    "k--",
    label="Voltage cut-off",
)
plt.legend();
Safe: 161.060 ms
Fast: 78.290 ms
_images/source_examples_notebooks_solvers_speed-up-solver_11_1.png

If we increase the integration interval, the safe solver still stops at the same point, but the fast solver fails

[4]:
safe_sol = sim.solve([0, 4500], solver=safe_solver, inputs={"Crate": 1})

print("Safe:", safe_sol.solve_time)

plt.plot(safe_sol["Time [h]"].data, safe_sol["Voltage [V]"].data, "r-", label="Safe")
plt.plot(
    safe_sol["Time [h]"].data,
    cutoff * np.ones_like(safe_sol["Time [h]"].data),
    "k--",
    label="Voltage cut-off",
)
plt.legend()

try:
    sim.solve([0, 4500], solver=fast_solver, inputs={"Crate": 1})
except pybamm.SolverError as e:
    print("Solving fast mode, error occurred:", e.args[0])
At t = 506.167, , mxstep steps taken before reaching tout.
Safe: 7.181 s
Solving fast mode, error occurred: Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:1401:
Error in Function::call for 'F' [IdasInterface] at .../casadi/core/function.cpp:330:
.../casadi/interfaces/sundials/idas_interface.cpp:596: IDASolve returned "IDA_TOO_MUCH_WORK". Consult IDAS documentation.
At t = 4051.62, , mxstep steps taken before reaching tout.
_images/source_examples_notebooks_solvers_speed-up-solver_13_3.png

We can solve with fast mode up to close to this time to understand why the model is failing

[5]:
fast_sol = sim.solve([0, 4049], solver=fast_solver, inputs={"Crate": 1})
fast_sol.plot(
    [
        "Minimum negative particle surface concentration",
        "Electrolyte concentration [mol.m-3]",
        "Maximum positive particle surface concentration",
        "Voltage [V]",
    ],
    time_unit="seconds",
    figsize=(9, 9),
);

In this case, we can see that the reason the solver is failing is that the concentration at the surface of the particles in the positive electrode hit their maximum value of c_max. Since the exchange current density has a term sqrt(c_max-c_s_surf), the square root of a negative number is complex, c_s_surf going above c_max will cause the solver to fail.

As a final note, there are some cases where the “safe” mode prints some warnings. This is linked to how the solver looks for events (sometimes stepping too far), and can be safely ignored if the solution looks sensible.

[6]:
safe_sol_160 = sim.solve([0, 160], solver=safe_solver, inputs={"Crate": 10})
plt.plot(
    safe_sol_160["Time [h]"].data, safe_sol_160["Voltage [V]"].data, "r-", label="Safe"
)
plt.plot(
    safe_sol_160["Time [h]"].data,
    cutoff * np.ones_like(safe_sol_160["Time [h]"].data),
    "k--",
    label="Voltage cut-off",
)
plt.legend();
At t = 159.516 and h = 1.33464e-07, the corrector convergence failed repeatedly or with |h| = hmin.
_images/source_examples_notebooks_solvers_speed-up-solver_18_1.png

Reducing the time interval to [0, 150], we see that the solution is exactly the same, without the warnings

[7]:
safe_sol_150 = sim.solve([0, 150], solver=safe_solver, inputs={"Crate": 10})
plt.plot(
    safe_sol_150["Time [h]"].data,
    safe_sol_150["Voltage [V]"].data,
    "r-",
    label="Safe [0,150]",
)
plt.plot(
    safe_sol_160["Time [h]"].data,
    safe_sol_160["Voltage [V]"].data,
    "b.",
    label="Safe [0,160]",
)
plt.plot(
    safe_sol_150["Time [h]"].data,
    cutoff * np.ones_like(safe_sol_150["Time [h]"].data),
    "k--",
    label="Voltage cut-off",
)
plt.legend();
_images/source_examples_notebooks_solvers_speed-up-solver_20_0.png
[8]:
safe_solver_2 = pybamm.CasadiSolver(mode="safe", dt_max=30)
safe_sol_2 = sim.solve([0, 160], solver=safe_solver_2, inputs={"Crate": 10})
Choosing dt_max to speed up the safe mode#

The parameter dt_max controls how large the steps taken by the CasadiSolver with “safe” mode are when looking for events.

[9]:
for dt_max in [10, 20, 100, 1000, 3700]:
    safe_sol = sim.solve(
        [0, 3600],
        solver=pybamm.CasadiSolver(mode="safe", dt_max=dt_max),
        inputs={"Crate": 1},
    )
    print(
        f"With dt_max={dt_max}, took {safe_sol.solve_time} "
        + f"(integration time: {safe_sol.integration_time})"
    )

fast_sol = sim.solve([0, 3600], solver=fast_solver, inputs={"Crate": 1})
print(
    f"With 'fast' mode, took {fast_sol.solve_time} "
    + f"(integration time: {fast_sol.integration_time})"
)
With dt_max=10, took 575.783 ms (integration time: 508.473 ms)
With dt_max=20, took 575.500 ms (integration time: 510.705 ms)
With dt_max=100, took 316.721 ms (integration time: 275.459 ms)
With dt_max=1000, took 76.646 ms (integration time: 49.294 ms)
With dt_max=3700, took 48.773 ms (integration time: 32.436 ms)
With 'fast' mode, took 42.224 ms (integration time: 32.177 ms)

In general, a larger value of dt_max gives a faster solution, since fewer integrator creations and calls are required.

Below the solution time interval of 36s, the value of dt_max does not affect the solve time, since steps must be at least 36s large. The discrepancy between the solve time and integration time is due to the extra operations recorded by “solve time”, such as creating the integrator. The “fast” solver does not need to do this (it reuses the first one it had already created), so the solve time is much closer to the integration time.

The example above was a case where no events are triggered, so the largest dt_max works well. If we step over events, then it is possible to makes dt_max too large, so that the solver will attempt (and fail) to take large steps past the event, iteratively reducing the step size until it works. For example:

[10]:
for dt_max in [10, 20, 100, 1000, 3600]:
    # Reduce max_num_steps to fail faster
    safe_sol = sim.solve(
        [0, 4500],
        solver=pybamm.CasadiSolver(
            mode="safe", dt_max=dt_max, extra_options_setup={"max_num_steps": 1000}
        ),
        inputs={"Crate": 1},
    )
    print(
        f"With dt_max={dt_max}, took {safe_sol.solve_time} "
        + f"(integration time: {safe_sol.integration_time})"
    )
With dt_max=10, took 504.163 ms (integration time: 419.740 ms)
With dt_max=20, took 504.691 ms (integration time: 421.396 ms)
With dt_max=100, took 286.620 ms (integration time: 238.390 ms)
With dt_max=1000, took 98.500 ms (integration time: 60.880 ms)
At t = 460.712, , mxstep steps taken before reaching tout.
At t = 460.712 and h = 1.26752e-15, the corrector convergence failed repeatedly or with |h| = hmin.
At t = 460.712 and h = 9.51455e-16, the corrector convergence failed repeatedly or with |h| = hmin.
With dt_max=3600, took 645.118 ms (integration time: 32.601 ms)

The integration time with dt_max=3600 remains the fastest, but the solve time is the slowest due to all the failed steps.

Choosing the period for faster experiments#

The “period” argument of the experiments also affects how long the simulations take, for a similar reason to dt_max. Therefore, this argument can be manually tuned to speed up how long an experiment takes to solve.

We start with one cycle of CCCV

[11]:
experiment = pybamm.Experiment(
    [
        "Discharge at C/10 for 10 hours or until 3.3 V",
        "Rest for 1 hour",
        "Charge at 1 A until 4.1 V",
        "Hold at 4.1 V until 50 mA",
        "Rest for 1 hour",
    ]
)
solver = pybamm.CasadiSolver(mode="safe", extra_options_setup={"max_num_steps": 1000})
sim = pybamm.Simulation(model, experiment=experiment, solver=solver)
sol = sim.solve()
print("Took ", sol.solve_time)
Took  693.344 ms

This gives a nice, smooth voltage curve

[12]:
plt.plot(sol["Time [s]"].data, sol["Voltage [V]"].data);
_images/source_examples_notebooks_solvers_speed-up-solver_34_0.png

We can speed up the experiment by increasing the period, but tradeoff is that the resolution of the solution becomes worse

[13]:
experiment = pybamm.Experiment(
    [
        "Discharge at C/10 for 10 hours or until 3.3 V",
        "Rest for 1 hour",
        "Charge at 1 A until 4.1 V",
        "Hold at 4.1 V until 50 mA",
        "Rest for 1 hour",
    ],
    period="10 minutes",
)
sim = pybamm.Simulation(model, experiment=experiment, solver=solver)
sol = sim.solve()
print("Took ", sol.solve_time)
plt.plot(sol["Time [s]"].data, sol["Voltage [V]"].data);
Took  598.237 ms
_images/source_examples_notebooks_solvers_speed-up-solver_36_1.png

If we increase the period too much, the experiment becomes slower as the solver takes more failing steps

[14]:
experiment = pybamm.Experiment(
    [
        "Discharge at C/10 for 10 hours or until 3.3 V",
        "Rest for 1 hour",
        "Charge at 1 A until 4.1 V",
        "Hold at 4.1 V until 50 mA",
        "Rest for 1 hour",
    ],
    period="30 minutes",
)
sim = pybamm.Simulation(model, experiment=experiment, solver=solver)
sol = sim.solve()
print("Took ", sol.solve_time)
plt.plot(sol["Time [s]"].data, sol["Voltage [V]"].data);
At t = 1262.29 and h = 3.51225e-14, the corrector convergence failed repeatedly or with |h| = hmin.
Took  532.042 ms
_images/source_examples_notebooks_solvers_speed-up-solver_38_2.png

We can control the period of individual parts of the experiment to get the fastest solution (again, at the cost of resolution)

[15]:
s = pybamm.step.string
experiment = pybamm.Experiment(
    [
        s("Discharge at C/10 for 10 hours or until 3.3 V", period="5 hours"),
        s("Rest for 1 hour", period="30 minutes"),
        s("Charge at 1 C until 4.1 V", period="10 minutes"),
        s("Hold at 4.1 V until 50 mA", period="10 minutes"),
        s("Rest for 1 hour", period="30 minutes"),
    ],
)
solver = pybamm.CasadiSolver(mode="safe", extra_options_setup={"max_num_steps": 1000})
sim = pybamm.Simulation(model, experiment=experiment, solver=solver)
sol = sim.solve()
print("Took ", sol.solve_time)
plt.plot(sol["Time [s]"].data, sol["Voltage [V]"].data);
Took  257.314 ms
_images/source_examples_notebooks_solvers_speed-up-solver_40_1.png

As you can see, this kind of optimization requires a lot of manual tuning. We are working on ways to make the experiment class more efficient in general.

Changing the time interval#

Finally, in some cases, changing the time interval (either the step size or the final time) may affect whether or not the casadi solver can solve the system. Therefore, if the casadi solver is failing, it may be worth changing the time interval (usually, reducing step size or final time) to see if that allows the solver to solve the model. Unfortunately, we have not yet been able to isolate a minimum working example to demonstrate this effect.

Handling instabilities#

If the solver is taking a lot of steps, possibly failing with a max_steps error, and the error persists with different solvers and options, this suggests a problem with the model itself. This can be due to a few things:

  • A singularity in the model (such as division by zero). Solve up to the time where the model fails, and plot some variables to see if they are going to infinity. You can then narrow down the source of the problem.

  • High model stiffness. Set the scale parameter of variables so that their scaled nominal value (e.g. initial condition) is of order 1, and multiply algebraic equations by appropriate constants so that they are roughly of order 1.

  • Non-differentiable functions (see below)

If none of these fixes work, we are interested in finding out why - please get in touch!

Smooth approximations to non-differentiable functions#

Some functions, such as minimum, maximum, heaviside, and abs, are discontinuous and/or non-differentiable (their derivative is discontinuous). Adaptive solvers can deal with this discontinuity, but will take many more steps close to the discontinuity in order to resolve it. Therefore, using soft approximations instead can reduce the number of steps taken by the solver, and hence the integration time. See this post for more details.

Here is an example using the maximum function. The function maximum(x,1) is continuous but non-differentiable at x=1, where its derivative jumps from 0 to 1. However, we can approximate it using the `softplus function <https://en.wikipedia.org/wiki/Rectifier_(neural_networks)#Softplus>`__, which is smooth everywhere and is sometimes used in neural networks as a smooth approximation to the RELU activation function. The softplus function is given by

\[s(x,y;k) = \frac{\log(\exp(kx)+\exp(ky))}{k},\]

where k is a strictly positive smoothing (or sharpness) parameter. The larger the value of k, the better the approximation but the stiffer the term (exp blows up quickly!). Usually, a value of k=10 is a good middle ground.

In PyBaMM, you can either call the softplus function directly, or change pybamm.settings.max_smoothing to automatically replace all your calls to pybamm.maximum with softplus.

[16]:
x = pybamm.Variable("x")
y = pybamm.Variable("y")

# Normal maximum
print(f"Exact maximum: {pybamm.maximum(x,y)}")

# Softplus
print("Softplus (k=10): ", pybamm.softplus(x, y, 10))

# Changing the setting to call softplus automatically
pybamm.settings.min_max_mode = "soft"
pybamm.settings.min_max_smoothing = 20
print(f"Softplus (k=20): {pybamm.maximum(x,y)}")

# All smoothing parameters can be changed at once
pybamm.settings.set_smoothing_parameters(30)
print(f"Softplus (k=30): {pybamm.maximum(x,y)}")

# Change back
pybamm.settings.set_smoothing_parameters("exact")
print(f"Exact maximum: {pybamm.maximum(x,y)}")
Exact maximum: maximum(x, y)
Softplus (k=10):  0.1 * log(exp(10.0 * x) + exp(10.0 * y))
Softplus (k=20): 0.05 * log(exp(20.0 * x) + exp(20.0 * y))
Softplus (k=30): 0.03333333333333333 * log(exp(30.0 * x) + exp(30.0 * y))
Exact maximum: maximum(x, y)

Note that if both sides are constant then pybamm will use the exact value even if the setting is set to smoothing

[17]:
a = pybamm.InputParameter("a")
pybamm.settings.max_smoothing = 20
# Both inputs are constant so uses exact maximum
print("Exact:", pybamm.maximum(0.999, 1).evaluate())
# One input is not constant (InputParameter) so uses softplus
print("Softplus:", pybamm.maximum(a, 1).evaluate(inputs={"a": 0.999}))
pybamm.settings.set_smoothing_parameters("exact")
Exact: 1.0
Softplus: 1.0

Here is the plot of softplus with different values of k

[18]:
pts = pybamm.linspace(0, 2, 100)

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(pts.evaluate(), pybamm.maximum(pts, 1).evaluate(), lw=2, label="exact")
ax.plot(
    pts.evaluate(),
    pybamm.softplus(pts, 1, 5).evaluate(),
    ":",
    lw=2,
    label="softplus (k=5)",
)
ax.plot(
    pts.evaluate(),
    pybamm.softplus(pts, 1, 10).evaluate(),
    ":",
    lw=2,
    label="softplus (k=10)",
)
ax.plot(
    pts.evaluate(),
    pybamm.softplus(pts, 1, 100).evaluate(),
    ":",
    lw=2,
    label="softplus (k=100)",
)
ax.legend();
_images/source_examples_notebooks_solvers_speed-up-solver_52_0.png

Solving a model with the exact maximum and soft approximation, demonstrates a clear speed-up even for a very simple model

[19]:
model_exact = pybamm.BaseModel()
model_exact.rhs = {x: pybamm.maximum(x, 1)}
model_exact.initial_conditions = {x: 0.5}
model_exact.variables = {"x": x, "max(x,1)": pybamm.maximum(x, 1)}

model_smooth = pybamm.BaseModel()
k = pybamm.InputParameter("k")
model_smooth.rhs = {x: pybamm.softplus(x, 1, k)}
model_smooth.initial_conditions = {x: 0.5}
model_smooth.variables = {"x": x, "max(x,1)": pybamm.softplus(x, 1, k)}


# Exact solution
timer = pybamm.Timer()
time = 0
solver = pybamm.CasadiSolver(mode="fast")
for _ in range(100):
    exact_sol = solver.solve(model_exact, [0, 2])
    # Report integration time, which is the time spent actually doing the integration
    time += exact_sol.integration_time
print("Exact:", time / 100)
sols = [exact_sol]

ks = [5, 10, 100]
solver = pybamm.CasadiSolver(mode="fast")
for k in ks:
    time = 0
    for _ in range(100):
        sol = solver.solve(model_smooth, [0, 2], inputs={"k": k})
        time += sol.integration_time
    print(f"Soft, k={k}:", time / 100)
    sols.append(sol)

pybamm.dynamic_plot(
    sols, ["x", "max(x,1)"], labels=["exact"] + [f"soft (k={k})" for k in ks]
);
Exact: 172.706 us
Soft, k=5: 160.098 us
Soft, k=10: 133.737 us
Soft, k=100: 168.833 us

For the minimum and maximum functions, an alternative smoothing functions (smooth_max, smooth_min) are provided.

\[\textrm{min}(x, y) = 0.5 * (\sqrt((x - y)^2 + \sigma) + (x + y)) \quad , \quad \textrm{max}(x, y) = 0.5 * ((x + y) - \sqrt((x - y)^2 + \sigma))\]

where

\[\sigma = \frac{1}{k^2}\]

For the smooth minimum and maximum functions, the recommended value of k is 100, where the function closely approximates the exact function, but is differentiable.

Changing between the soft, smooth, and exact functions can be done by setting the min_max_mode and the value of k stored in min_max_smoothing

[20]:
x = pybamm.Variable("x")
y = pybamm.Variable("y")

# Normal maximum
print(f"Exact maximum: {pybamm.maximum(x,y)}")

# Smooth plus can be called explicitly
print("Smooth plus (k=100): ", pybamm.smooth_max(x, y, 100))

# Smooth plus and smooth minus will be used when the mode is set to "smooth"
pybamm.settings.min_max_mode = "smooth"
pybamm.settings.min_max_smoothing = 200
print(f"Smooth plus (k=200): {pybamm.maximum(x,y)}")

# Setting the smoothing parameters with set_smoothing_parameters() defaults to softplus
pybamm.settings.set_smoothing_parameters(10)
print(f"Softplus (k=10): {pybamm.maximum(x,y)}")

# Change back
pybamm.settings.set_smoothing_parameters("exact")
print(f"Exact maximum: {pybamm.maximum(x,y)}")
Exact maximum: maximum(x, y)
Smooth plus (k=100):  0.5 * (sqrt(0.0001 + (x - y) ** 2.0) + x + y)
Smooth plus (k=200): 0.5 * (sqrt(2.5e-05 + (x - y) ** 2.0) + x + y)
Softplus (k=10): 0.1 * log(exp(10.0 * x) + exp(10.0 * y))
Exact maximum: maximum(x, y)

Here is the plot of smooth_max with different values of k

[21]:
pts = pybamm.linspace(0, 2, 100)

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(pts.evaluate(), pybamm.maximum(pts, 1).evaluate(), lw=2, label="exact")
ax.plot(
    pts.evaluate(),
    pybamm.smooth_max(pts, 1, 5).evaluate(),
    ":",
    lw=2,
    label="smooth_max (k=5)",
)
ax.plot(
    pts.evaluate(),
    pybamm.smooth_max(pts, 1, 10).evaluate(),
    ":",
    lw=2,
    label="smooth_max (k=10)",
)
ax.plot(
    pts.evaluate(),
    pybamm.smooth_max(pts, 1, 100).evaluate(),
    ":",
    lw=2,
    label="smooth_max (k=100)",
)
ax.legend();
_images/source_examples_notebooks_solvers_speed-up-solver_58_0.png

Solving a model with the exact maximum and smooth approximation, demonstrates a clear speed-up even for a very simple model

[22]:
model_exact = pybamm.BaseModel()
model_exact.rhs = {x: pybamm.maximum(x, 1)}
model_exact.initial_conditions = {x: 0.5}
model_exact.variables = {"x": x, "max(x,1)": pybamm.maximum(x, 1)}

model_smooth = pybamm.BaseModel()
k = pybamm.InputParameter("k")
model_smooth.rhs = {x: pybamm.smooth_max(x, 1, k)}
model_smooth.initial_conditions = {x: 0.5}
model_smooth.variables = {"x": x, "max(x,1)": pybamm.smooth_max(x, 1, k)}


# Exact solution
timer = pybamm.Timer()
time = 0
solver = pybamm.CasadiSolver(mode="fast")
for _ in range(100):
    exact_sol = solver.solve(model_exact, [0, 2])
    # Report integration time, which is the time spent actually doing the integration
    time += exact_sol.integration_time
print("Exact:", time / 100)
sols = [exact_sol]

ks = [10, 50, 100, 1000, 10000]
solver = pybamm.CasadiSolver(mode="fast")
for k in ks:
    time = 0
    for _ in range(100):
        sol = solver.solve(model_smooth, [0, 2], inputs={"k": k})
        time += sol.integration_time
    print(f"Smooth, k={k}:", time / 100)
    sols.append(sol)

pybamm.dynamic_plot(
    sols, ["x", "max(x,1)"], labels=["exact"] + [f"soft (k={k})" for k in ks]
);
Exact: 168.348 us
Smooth, k=10: 149.515 us
Smooth, k=50: 170.092 us
Smooth, k=100: 137.928 us
Smooth, k=1000: 200.991 us
Smooth, k=10000: 175.300 us
Other smooth approximations#

Here are the other smooth approximations for the other non-smooth functions:

[23]:
pybamm.settings.set_smoothing_parameters(10)
print(f"Soft minimum (softminus):\t {pybamm.minimum(x,y)!s}")
print(f"Smooth heaviside (sigmoid):\t {x < y!s}")
print(f"Smooth absolute value: \t\t {abs(x)!s}")
pybamm.settings.min_max_mode = "smooth"
print(f"Smooth minimum:\t\t\t {pybamm.minimum(x,y)!s}")
pybamm.settings.set_smoothing_parameters("exact")
Soft minimum (softminus):        -0.1 * log(exp(-10.0 * x) + exp(-10.0 * y))
Smooth heaviside (sigmoid):      0.5 + 0.5 * tanh(10.0 * (y - x))
Smooth absolute value:           x * (exp(10.0 * x) - exp(-10.0 * x)) / (exp(10.0 * x) + exp(-10.0 * x))
Smooth minimum:                  0.5 * (x + y - sqrt(0.010000000000000002 + (x - y) ** 2.0))

References#

The relevant papers for this notebook are:

[24]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] 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.
[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.
[7] 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.
[8] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Finite Volume Discretisation#

In this notebook, we explain the discretisation process that converts an expression tree, representing a model, to a linear algebra tree that can be evaluated by the solvers.

We use Finite Volumes as an example of a spatial method, since it is the default spatial method for most PyBaMM models. This is a good spatial method for battery problems as it is conservative: for lithium-ion battery models, we can be sure that the total amount of lithium in the system is constant. For more details on the Finite Volume method, see Randall Leveque’s book.

This notebook is structured as follows:

  1. Setting up a discretisation. Overview of the parameters that are passed to the discretisation

  2. Discretisations and spatial methods. Operations that are common to most spatial methods:

    • Discretising a spatial variable (e.g. \(x\))

    • Discretising a variable (e.g. concentration)

  3. Example: Finite Volume operators. Finite Volume implementation of some useful operators:

    • Gradient operator

    • Divergence operator

    • Integral operator

  4. Example: Discretising a simple model. Setting up and solving a simple model, using Finite Volumes as the spatial method

Setting up a Discretisation#

We first import pybamm and some useful other modules, and change our working directory to the root of the PyBaMM folder:

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")
Note: you may need to restart the kernel to use updated packages.

To set up a discretisation, we must create a geometry, mesh this geometry, and then create the discretisation with the appropriate spatial method(s). The easiest way to create a geometry is to the inbuilt battery geometry:

[2]:
parameter_values = pybamm.ParameterValues(
    values={
        "Negative particle radius [m]": 0.5,
        "Positive particle radius [m]": 0.6,
        "Negative electrode thickness [m]": 0.3,
        "Separator thickness [m]": 0.2,
        "Positive electrode thickness [m]": 0.3,
    }
)

geometry = pybamm.battery_geometry()
parameter_values.process_geometry(geometry)

We then use this geometry to create a mesh, which for this example consists of uniform 1D submeshes

[3]:
submesh_types = {
    "negative electrode": pybamm.Uniform1DSubMesh,
    "separator": pybamm.Uniform1DSubMesh,
    "positive electrode": pybamm.Uniform1DSubMesh,
    "negative particle": pybamm.Uniform1DSubMesh,
    "positive particle": pybamm.Uniform1DSubMesh,
    "current collector": pybamm.SubMesh0D,
}

var = pybamm.standard_spatial_vars
var_pts = {var.x_n: 15, var.x_s: 10, var.x_p: 15, var.r_n: 10, var.r_p: 10}
mesh = pybamm.Mesh(geometry, submesh_types, var_pts)

Finally, we can use the mesh to create a discretisation, using Finite Volumes as the spatial method for this example

[4]:
spatial_methods = {
    "macroscale": pybamm.FiniteVolume(),
    "negative particle": pybamm.FiniteVolume(),
    "positive particle": pybamm.FiniteVolume(),
}
disc = pybamm.Discretisation(mesh, spatial_methods)

Discretisations and Spatial Methods#

Spatial Variables#

Spatial variables, such as \(x\) and \(r\), are converted to pybamm.Vector nodes

[5]:
# Set up
macroscale = ["negative electrode", "separator", "positive electrode"]
x_var = pybamm.SpatialVariable("x", domain=macroscale)
r_var = pybamm.SpatialVariable("r", domain=["negative particle"])

# Discretise
x_disc = disc.process_symbol(x_var)
r_disc = disc.process_symbol(r_var)
print(f"x_disc is a {type(x_disc)}")
print(f"r_disc is a {type(r_disc)}")

# Evaluate
x = x_disc.evaluate()
r = r_disc.evaluate()

f, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))

ax1.plot(x, "*")
ax1.set_xlabel("index")
ax1.set_ylabel(r"$x$")

ax2.plot(r, "*")
ax2.set_xlabel("index")
ax2.set_ylabel(r"$r$")

plt.tight_layout()
plt.show()
x_disc is a <class 'pybamm.expression_tree.vector.Vector'>
r_disc is a <class 'pybamm.expression_tree.vector.Vector'>
_images/source_examples_notebooks_spatial_methods_finite-volumes_14_1.png

We define y_macroscale, y_microscale and y_scalar for evaluation and visualisation of results below

[6]:
y_macroscale = x**3 / 3
y_microscale = np.cos(r)
y_scalar = np.array([[5]])

y = np.concatenate([y_macroscale, y_microscale, y_scalar])
Variables#

In this notebook, we will work with three variables u, v, w.

[7]:
u = pybamm.Variable(
    "u", domain=macroscale
)  # u is a variable in the macroscale (e.g. electrolyte potential)
v = pybamm.Variable(
    "v", domain=["negative particle"]
)  # v is a variable in the negative particle (e.g. particle concentration)
w = pybamm.Variable(
    "w"
)  # w is a variable without a domain (e.g. time, average concentration)

variables = [u, v, w]

Before discretising, trying to evaluate the variables raises a NotImplementedError:

[8]:
try:
    u.evaluate()
except NotImplementedError as e:
    print(e)
method self.evaluate() not implemented for symbol u of type <class 'pybamm.expression_tree.variable.Variable'>

For any spatial method, a pybamm.Variable gets converted to a pybamm.StateVector which, when evaluated, takes the appropriate slice of the input vector y.

[9]:
# Pass the list of variables to the discretisation to calculate the slices to be used (order matters here!)
disc.set_variable_slices(variables)

# Discretise the variables
u_disc = disc.process_symbol(u)
v_disc = disc.process_symbol(v)
w_disc = disc.process_symbol(w)

# Print the outcome
print(f"Discretised u is the StateVector {u_disc}")
print(f"Discretised v is the StateVector {v_disc}")
print(f"Discretised w is the StateVector {w_disc}")
Discretised u is the StateVector y[0:40]
Discretised v is the StateVector y[40:50]
Discretised w is the StateVector y[50:51]

Since the variables have been passed to disc in the order [u,v,w], they each read the appropriate part of y when evaluated:

[10]:
x_fine = np.linspace(x[0], x[-1], 1000)
r_fine = np.linspace(r[0], r[-1], 1000)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
ax1.plot(x_fine, x_fine**3 / 3, x, u_disc.evaluate(y=y), "o")
ax1.set_xlabel("x")
ax1.legend(["x^3/3", "u"], loc="best")

ax2.plot(r_fine, np.cos(r_fine), r, v_disc.evaluate(y=y), "o")
ax2.set_xlabel("r")
ax2.legend(["cos(r)", "v"], loc="best")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_spatial_methods_finite-volumes_25_0.png
[11]:
print(f"w = {w_disc.evaluate(y=y)}")
w = [[5.]]

Finite Volume Operators#

Gradient operator#

The gradient operator is converted to a Matrix-StateVector multiplication. In 1D, the gradient operator is equivalent to \(\partial/\partial x\) on the macroscale and \(\partial/\partial r\) on the microscale. In Finite Volumes, we take the gradient of an object on nodes (shape (n,)), which returns an object on the edges (shape (n-1,)).

[12]:
grad_u = pybamm.grad(u)
grad_u_disc = disc.process_symbol(grad_u)
grad_u_disc.render()
@
├── Sparse Matrix (39, 40)
└── y[0:40]

The Matrix in grad_u_disc is the standard [-1,1] sparse matrix, divided by the step sizes dx:

[13]:
macro_mesh = mesh.combine_submeshes(*macroscale)
print("gradient matrix is:\n")
print(
    f"1/dx *\n{macro_mesh.d_nodes[:,np.newaxis] * grad_u_disc.children[0].entries.toarray()}"
)
gradient matrix is:

1/dx *
[[-1.  1.  0. ...  0.  0.  0.]
 [ 0. -1.  1. ...  0.  0.  0.]
 [ 0.  0. -1. ...  0.  0.  0.]
 ...
 [ 0.  0.  0. ...  1.  0.  0.]
 [ 0.  0.  0. ... -1.  1.  0.]
 [ 0.  0.  0. ...  0. -1.  1.]]

When evaluated with y_macroscale=x**3/3, grad_u_disc is equal to x**2 as expected:

[14]:
x_edge = macro_mesh.edges[1:-1]  # note that grad_u_disc is evaluated on the node edges

fig, ax = plt.subplots()
ax.plot(x_fine, x_fine**2, x_edge, grad_u_disc.evaluate(y=y), "o")
ax.set_xlabel("x")
legend = ax.legend(["x^2", "grad(u).evaluate(y=x**3/3)"], loc="best")

plt.show()
_images/source_examples_notebooks_spatial_methods_finite-volumes_33_0.png

Similary, we can create, discretise and evaluate the gradient of v, which is a variable in the negative particles. Note that the syntax for doing this is identical: we do not need to explicitly specify that we want the gradient in r, since this is inferred from the domain of v.

[15]:
v.domain
[15]:
['negative particle']
[16]:
grad_v = pybamm.grad(v)
grad_v_disc = disc.process_symbol(grad_v)
print("grad(v) tree is:\n")
grad_v_disc.render()

micro_mesh = mesh["negative particle"]
print("\n gradient matrix is:\n")
print(
    f"1/dr *\n{micro_mesh.d_nodes[:,np.newaxis] * grad_v_disc.children[0].entries.toarray()}"
)

r_edge = micro_mesh.edges[1:-1]  # note that grad_u_disc is evaluated on the node edges

fig, ax = plt.subplots()
ax.plot(r_fine, -np.sin(r_fine), r_edge, grad_v_disc.evaluate(y=y), "o")
ax.set_xlabel("x")
legend = ax.legend(["-sin(r)", "grad(v).evaluate(y=cos(r))"], loc="best")

plt.show()
grad(v) tree is:

@
├── Sparse Matrix (9, 10)
└── y[40:50]

 gradient matrix is:

1/dr *
[[-1.  1.  0.  0.  0.  0.  0.  0.  0.  0.]
 [ 0. -1.  1.  0.  0.  0.  0.  0.  0.  0.]
 [ 0.  0. -1.  1.  0.  0.  0.  0.  0.  0.]
 [ 0.  0.  0. -1.  1.  0.  0.  0.  0.  0.]
 [ 0.  0.  0.  0. -1.  1.  0.  0.  0.  0.]
 [ 0.  0.  0.  0.  0. -1.  1.  0.  0.  0.]
 [ 0.  0.  0.  0.  0.  0. -1.  1.  0.  0.]
 [ 0.  0.  0.  0.  0.  0.  0. -1.  1.  0.]
 [ 0.  0.  0.  0.  0.  0.  0.  0. -1.  1.]]
_images/source_examples_notebooks_spatial_methods_finite-volumes_36_1.png
Boundary conditions#

If the discretisation is provided with boundary conditions, appropriate ghost nodes are concatenated onto the variable, and a larger gradient matrix is used. The ghost nodes are chosen based on the value of the first/last node in the variable and the boundary condition. For a Dirichlet boundary condition \(u=a\) on the left-hand boundary, we set the value of the left ghost node to be equal to

\[2*a-u[0],\]

where \(u[0]\) is the value of \(u\) in the left-most cell in the domain. Similarly, for a Dirichlet condition \(u=b\) on the right-hand boundary, we set the right ghost node to be

\[2*b-u[-1].\]
[17]:
disc.bcs = {
    u: {
        "left": (pybamm.Scalar(1), "Dirichlet"),
        "right": (pybamm.Scalar(2), "Dirichlet"),
    }
}
grad_u_disc = disc.process_symbol(grad_u)
print("The gradient object is:")
(grad_u_disc.render())
u_eval = grad_u_disc.evaluate(y=y)
dx = np.diff(macro_mesh.nodes)[-1]
print(f"The value of u on the left-hand boundary is {y[0] - dx*u_eval[0]/2}")
print(f"The value of u on the right-hand boundary is {y[1] + dx*u_eval[-1]/2}")
The gradient object is:
+
├── Column vector of length 41
└── @
    ├── Sparse Matrix (41, 40)
    └── y[0:40]
The value of u on the left-hand boundary is [1.]
The value of u on the right-hand boundary is [1.67902865]

For a Neumann boundary condition \(\partial u/\partial x=c\) on the left-hand boundary, we set the value of the left ghost node to be

\[u[0] - c * dx,\]

where \(dx\) is the step size at the left-hand boundary. For a Neumann boundary condition \(\partial u/\partial x=d\) on the right-hand boundary, we set the value of the right ghost node to be

\[u[-1] + d * dx.\]
[20]:
disc.bcs = {
    u: {"left": (pybamm.Scalar(3), "Neumann"), "right": (pybamm.Scalar(4), "Neumann")}
}
grad_u_disc = disc.process_symbol(grad_u)
print("The gradient object is:")
(grad_u_disc.render())
grad_u_eval = grad_u_disc.evaluate(y=y)
print(f"The gradient on the left-hand boundary is {grad_u_eval[0]}")
print(f"The gradient of u on the right-hand boundary is {grad_u_eval[-1]}")
The gradient object is:
+
├── Column vector of length 41
└── @
    ├── Sparse Matrix (41, 40)
    └── y[0:40]
The gradient on the left-hand boundary is [3.]
The gradient of u on the right-hand boundary is [4.]

We can mix the types of the boundary conditions:

[21]:
disc.bcs = {
    u: {"left": (pybamm.Scalar(5), "Dirichlet"), "right": (pybamm.Scalar(6), "Neumann")}
}
grad_u_disc = disc.process_symbol(grad_u)
print("The gradient object is:")
(grad_u_disc.render())
grad_u_eval = grad_u_disc.evaluate(y=y)
u_eval = grad_u_disc.children[1].evaluate(y=y)
print(f"The value of u on the left-hand boundary is {(u_eval[0] + u_eval[1])/2}")
print(f"The gradient on the right-hand boundary is {grad_u_eval[-1]}")
The gradient object is:
+
├── Column vector of length 41
└── @
    ├── Sparse Matrix (41, 40)
    └── y[0:40]
The value of u on the left-hand boundary is [0.00036458]
The gradient on the right-hand boundary is [6.]

Robin boundary conditions can be implemented by specifying a Neumann condition where the flux depends on the variable.

Divergence operator#

Before computing the Divergence operator, we set up Neumann boundary conditions. The behaviour with Dirichlet boundary conditions is very similar.

[22]:
disc.bcs = {
    u: {"left": (pybamm.Scalar(-1), "Neumann"), "right": (pybamm.Scalar(1), "Neumann")}
}

Now we can process div(grad(u)), converting it to a Matrix-Vector multiplication, plus a vector for the boundary conditions. Since we have Neumann boundary conditions, the divergence of an object of size (n+1,) has size (n,), and so div(grad) of an object of size (n,) has size (n,)

[23]:
div_grad_u = pybamm.div(grad_u)
div_grad_u_disc = disc.process_symbol(div_grad_u)
div_grad_u_disc.render()
+
├── Column vector of length 40
└── @
    ├── Sparse Matrix (40, 40)
    └── y[0:40]

The div(grad) matrix is automatically simplified to the well-known [1,-2,1] matrix (divided by the square of the distance between the edges), except in the first and last rows for boundary conditions

[24]:
print("div(grad) matrix is:\n")
print(
    "1/dx^2 * \n{}".format(
        macro_mesh.d_edges[:, np.newaxis] ** 2
        * div_grad_u_disc.right.left.entries.toarray()
    )
)
div(grad) matrix is:

1/dx^2 *
[[-1.  1.  0. ...  0.  0.  0.]
 [ 1. -2.  1. ...  0.  0.  0.]
 [ 0.  1. -2. ...  0.  0.  0.]
 ...
 [ 0.  0.  0. ... -2.  1.  0.]
 [ 0.  0.  0. ...  1. -2.  1.]
 [ 0.  0.  0. ...  0.  1. -1.]]
Integral operator#

Finally, we can define an integral operator, which integrates the variable across the domain specified by the integration variable.

[25]:
int_u = pybamm.Integral(u, x_var)
int_u_disc = disc.process_symbol(int_u)
print(f"int(u) = {int_u_disc.evaluate(y=y)} is approximately equal to 1/12, {1/12}")

# We divide v by r to evaluate the integral more easily
int_v_over_r2 = pybamm.Integral(v / r_var**2, r_var)
int_v_over_r2_disc = disc.process_symbol(int_v_over_r2)
print(
    "int(v/r^2) = {} is approximately equal to 4 * pi * sin(1), {}".format(
        int_v_over_r2_disc.evaluate(y=y), 4 * np.pi * np.sin(1)
    )
)
int(u) = [[0.08330729]] is approximately equal to 1/12, 0.08333333333333333
int(v/r^2) = [[11.07985772]] is approximately equal to 4 * pi * sin(1), 10.574236256325824

The integral operators are also Matrix-Vector multiplications

[26]:
print("int(u):\n")
int_u_disc.render()
print("\nint(v):\n")
int_v_over_r2_disc.render()
int(u):

@
├── Sparse Matrix (1, 40)
└── y[0:40]

int(v):

@
├── Sparse Matrix (1, 10)
└── *
    ├── Column vector of length 10
    └── y[40:50]
[27]:
int_u_disc.children[0].evaluate() / macro_mesh.d_edges
[27]:
matrix([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
         1., 1., 1., 1., 1., 1., 1., 1.]])

Discretising a model#

We can now discretise a whole model. We create, and discretise, a simple model for the concentration in the electrolyte and the concentration in the particles, and discretise it with a single command:

disc.process_model(model)
[28]:
model = pybamm.BaseModel()

c_e = pybamm.Variable("electrolyte concentration", domain=macroscale)
N_e = pybamm.grad(c_e)
c_s = pybamm.Variable("particle concentration", domain=["negative particle"])
N_s = pybamm.grad(c_s)
model.rhs = {c_e: pybamm.div(N_e) - 5, c_s: pybamm.div(N_s)}
model.boundary_conditions = {
    c_e: {"left": (np.cos(0), "Neumann"), "right": (np.cos(10), "Neumann")},
    c_s: {"left": (0, "Neumann"), "right": (-1, "Neumann")},
}
model.initial_conditions = {c_e: 1 + 0.1 * pybamm.sin(10 * x_var), c_s: 1}

# Create a new discretisation and process model
disc2 = pybamm.Discretisation(mesh, spatial_methods)
disc2.process_model(model);

The initial conditions are discretised to vectors, and an array of concatenated initial conditions is created.

[29]:
c_e_0 = model.initial_conditions[c_e].evaluate()
c_s_0 = model.initial_conditions[c_s].evaluate()
y0 = model.concatenated_initial_conditions.evaluate()

fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13, 4))
ax1.plot(x_fine, 1 + 0.1 * np.sin(10 * x_fine), x, c_e_0, "o")
ax1.set_xlabel("x")
ax1.legend(["1+0.1*sin(10*x)", "c_e_0"], loc="best")

ax2.plot(x_fine, np.ones_like(r_fine), r, c_s_0, "o")
ax2.set_xlabel("r")
ax2.legend(["1", "c_s_0"], loc="best")

ax3.plot(y0, "*")
ax3.set_xlabel("index")
ax3.set_ylabel("y0")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_spatial_methods_finite-volumes_62_0.png

The discretised rhs can be evaluated, for example at 0,y0:

[30]:
rhs_c_e = model.rhs[c_e].evaluate(0, y0)
rhs_c_s = model.rhs[c_s].evaluate(0, y0)
rhs = model.concatenated_rhs.evaluate(0, y0)

fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13, 4))
ax1.plot(x_fine, -10 * np.sin(10 * x_fine) - 5, x, rhs_c_e, "o")
ax1.set_xlabel("x")
ax1.set_ylabel("rhs_c_e")
ax1.legend(["1+0.1*sin(10*x)", "c_e_0"], loc="best")

ax2.plot(r, rhs_c_s, "o")
ax2.set_xlabel("r")
ax2.set_ylabel("rhs_c_s")

ax3.plot(rhs, "*")
ax3.set_xlabel("index")
ax3.set_ylabel("rhs")

plt.tight_layout()
plt.show()
_images/source_examples_notebooks_spatial_methods_finite-volumes_64_0.png

The function model.concatenated_rhs is then passed to the solver to solve the model, with initial conditions model.concatenated_initial_conditions.

Upwinding and downwinding#

If a system is advection-dominated (Peclet number greater than around 40), then it is important to use upwinding (if velocity is positive) or downwinding (if velocity is negative) to obtain accurate results. To see this, consider the following model (without upwinding)

[31]:
model = pybamm.BaseModel()

# Define concentration and velocity
c = pybamm.Variable(
    "c", domain=["negative electrode", "separator", "positive electrode"]
)
v = pybamm.PrimaryBroadcastToEdges(
    1, ["negative electrode", "separator", "positive electrode"]
)
model.rhs = {c: -pybamm.div(c * v) + 1}
model.initial_conditions = {c: 0}
model.boundary_conditions = {c: {"left": (0, "Dirichlet")}}
model.variables = {"c": c}


def solve_and_plot(model):
    model_disc = disc.process_model(model, inplace=False)

    t_eval = [0, 100]
    solution = pybamm.CasadiSolver().solve(model_disc, t_eval)

    # plot
    plot = pybamm.QuickPlot(solution, ["c"], spatial_unit="m")
    plot.dynamic_plot()


solve_and_plot(model)

The concentration grows indefinitely, which is clearly an incorrect solution. Instead, we can use upwinding:

[32]:
model.rhs = {c: -pybamm.div(pybamm.upwind(c) * v) + 1}
solve_and_plot(model)

This gives the expected linear steady state from 0 to 1. Similarly, if the velocity is negative, downwinding gives accurate results

[33]:
model.rhs = {c: -pybamm.div(pybamm.downwind(c) * (-v)) + 1}
model.boundary_conditions = {c: {"right": (0, "Dirichlet")}}
solve_and_plot(model)

More advanced concepts#

Since this notebook is only an introduction to the discretisation, we have not covered everything. More advanced concepts, such as the ones below, can be explored by looking into the API docs.

  • Gradient and divergence of microscale variables in the P2D model

  • Indefinite integral

If you would like detailed examples of these operations, please create an issue and we will be happy to help.

References#

The relevant papers for this notebook are:

[34]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Comparisons using the BatchStudy class#

In this notebook, we will be going through the BatchStudy class and will be discussing how different models, experiments, chemistries, etc. can be compared with each other using the same.

Comparing models#

We start by creating a simple script to compare SPM, SPMe and DFN model with the default parameters.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm

# loading up 3 models to compare
dfn = pybamm.lithium_ion.DFN()
spm = pybamm.lithium_ion.SPM()
spme = pybamm.lithium_ion.SPMe()
Note: you may need to restart the kernel to use updated packages.

The BatchStudy class requires a dictionary of models, and all the default values for a given model are used if no additional parameter is passed in.

[2]:
models = {
    "dfn": dfn,
    "spm": spm,
    "spme": spme,
}

# creating a BatchStudy object
batch_study = pybamm.BatchStudy(models=models)

# solving and plotting the comparison
batch_study.solve(t_eval=[0, 3600])
batch_study.plot()
[2]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ffdb8988b80>

BatchStudy by default requires equal number of items in all the dictionaries passed, which can be changed by setting the value of permutations to True. When set True, a cartesian product of all the available items is taken.

For example, here we pass 3 models but only 1 parameter value, hence it is necessary to set permutations to True. Here, the given parameter value is used for all the provided models.

[3]:
# passing parameter_values as a dictionary
parameter_values = {"Chen2020": pybamm.ParameterValues("Chen2020")}

# creating a BatchStudy object and solving the simulation
batch_study = pybamm.BatchStudy(
    models=models, parameter_values=parameter_values, permutations=True
)
batch_study.solve(t_eval=[0, 3600])
batch_study.plot()
[3]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ffdcba8cfa0>

Comparing parameters#

BatchStudy can also be used to compare different things (like affect of changing a parameter’s value) on a single model.

In the following cell, we compare different values of "Curent function [A]" using the Single Paritcle Model with electrolyte.

[4]:
model = {"spme": spme}

# populating a dictionary with 3 same parameter values
parameter_values = {
    "Chen2020_1": pybamm.ParameterValues("Chen2020"),
    "Chen2020_2": pybamm.ParameterValues("Chen2020"),
    "Chen2020_3": pybamm.ParameterValues("Chen2020"),
}

# different values for "Current function [A]"
current_values = [4.5, 4.75, 5]

# changing the value of "Current function [A]" in all the parameter values present in the
# parameter_values dictionary
for k, v, current_value in zip(
    parameter_values.keys(), parameter_values.values(), current_values
):
    v["Current function [A]"] = current_value

# creating a BatchStudy object with permutations set to True to create a cartesian product
batch_study = pybamm.BatchStudy(
    models=model, parameter_values=parameter_values, permutations=True
)
batch_study.solve(t_eval=[0, 3600])

# generating the required labels and plotting
labels = [f"Current function [A]: {current}" for current in current_values]
batch_study.plot(labels=labels)
[4]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ffdccc5bd30>

BatchStudy also includes a create_gif method which can be used to create a GIF of the simulation.

[5]:
# using less number of images in the example
# for a smoother GIF use more images
batch_study.create_gif(number_of_images=5, duration=0.2, output_filename="batch.gif")

Displaying the GIF using markdown-

plot

Using experiments#

Experiments can also be specified for comparisons, and they are also passed as a dictionary (a dictionary of pybamm.Experiment) in the BatchStudy class.

In the next cell, we compare a single experiment, with a single model, but with a varied parameter value.

[6]:
pybamm.set_logging_level("NOTICE")

# using the cccv experiment with 10 cycles
cccv = pybamm.Experiment(
    [
        (
            "Discharge at C/10 for 10 hours or until 3.3 V",
            "Rest for 1 hour",
            "Charge at 1 A until 4.1 V",
            "Hold at 4.1 V until 50 mA",
            "Rest for 1 hour",
        )
    ]
    * 10,
)

# creating the experiment dict
experiment = {"cccv": cccv}

# populating a dictionary with 3 same parameter values (Mohtat2020 chemistry)
parameter_values = {
    "Mohtat2020_1": pybamm.ParameterValues("Mohtat2020"),
    "Mohtat2020_2": pybamm.ParameterValues("Mohtat2020"),
    "Mohtat2020_3": pybamm.ParameterValues("Mohtat2020"),
}

# different values for the parameter "Inner SEI open-circuit potential [V]"
inner_sei_oc_v_values = [2.0e-4, 2.7e-4, 3.4e-4]

# updating the value of "Inner SEI open-circuit potential [V]" in all the dictionary items
for k, v, inner_sei_oc_v in zip(
    parameter_values.keys(), parameter_values.values(), inner_sei_oc_v_values
):
    v.update(
        {"Inner SEI open-circuit potential [V]": inner_sei_oc_v},
    )

# creating a Single Particle Model with "electron-mitigation limited" SEI
model = {"spm": pybamm.lithium_ion.SPM({"SEI": "electron-migration limited"})}

# creating a BatchStudy object with the given experimen, model and parameter_values
batch_study = pybamm.BatchStudy(
    models=model,
    experiments=experiment,
    parameter_values=parameter_values,
    permutations=True,
)

# solving and plotting the result
batch_study.solve(initial_soc=1)

labels = [
    f"Inner SEI open-circuit potential [V]: {inner_sei_oc_v}"
    for inner_sei_oc_v in inner_sei_oc_v_values
]
batch_study.plot(labels=labels)
2022-07-26 16:44:42.414 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/10 (35.637 ms elapsed) --------------------
2022-07-26 16:44:42.415 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:42.565 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:42.594 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:42.684 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:42.726 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:42.921 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/10 (542.652 ms elapsed) --------------------
2022-07-26 16:44:42.922 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:43.047 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:43.063 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:43.128 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:43.142 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:43.234 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/10 (855.739 ms elapsed) --------------------
2022-07-26 16:44:43.235 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:43.356 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:43.371 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:43.435 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:43.449 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:43.554 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/10 (1.175 s elapsed) --------------------
2022-07-26 16:44:43.554 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:43.676 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:43.691 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:43.756 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:43.769 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:44.037 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/10 (1.658 s elapsed) --------------------
2022-07-26 16:44:44.037 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:44.171 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:44.187 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:44.255 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:44.270 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:44.374 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/10 (1.995 s elapsed) --------------------
2022-07-26 16:44:44.374 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:44.503 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:44.518 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:44.588 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:44.601 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:44.710 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/10 (2.331 s elapsed) --------------------
2022-07-26 16:44:44.710 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:44.836 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:44.852 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:44.922 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:44.935 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:45.033 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/10 (2.655 s elapsed) --------------------
2022-07-26 16:44:45.034 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:45.156 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:45.171 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:45.238 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:45.252 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:45.369 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/10 (2.991 s elapsed) --------------------
2022-07-26 16:44:45.370 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:45.512 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:45.529 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:45.596 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:45.610 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:45.711 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/10 (3.333 s elapsed) --------------------
2022-07-26 16:44:45.712 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:45.849 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:45.864 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:45.932 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:45.947 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:46.055 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.333 s
2022-07-26 16:44:46.893 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/10 (36.421 ms elapsed) --------------------
2022-07-26 16:44:46.894 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:47.052 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:47.080 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:47.172 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:47.219 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:47.398 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/10 (542.151 ms elapsed) --------------------
2022-07-26 16:44:47.399 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:47.522 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:47.690 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:47.760 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:47.774 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:47.872 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/10 (1.016 s elapsed) --------------------
2022-07-26 16:44:47.873 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:48.023 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:48.040 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:48.115 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:48.133 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:48.234 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/10 (1.378 s elapsed) --------------------
2022-07-26 16:44:48.234 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:48.365 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:48.379 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:48.446 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:48.460 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:48.557 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/10 (1.700 s elapsed) --------------------
2022-07-26 16:44:48.557 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:48.688 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:48.704 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:48.776 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:48.790 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:48.887 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/10 (2.031 s elapsed) --------------------
2022-07-26 16:44:48.887 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:49.019 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:49.034 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:49.102 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:49.116 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:49.209 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/10 (2.353 s elapsed) --------------------
2022-07-26 16:44:49.210 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:49.335 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:49.350 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:49.416 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:49.430 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:49.533 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/10 (2.677 s elapsed) --------------------
2022-07-26 16:44:49.534 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:49.658 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:49.673 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:49.740 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:49.755 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:49.851 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/10 (2.995 s elapsed) --------------------
2022-07-26 16:44:49.852 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:49.977 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:49.992 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:50.065 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:50.078 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:50.176 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/10 (3.320 s elapsed) --------------------
2022-07-26 16:44:50.176 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:50.307 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:50.324 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:50.420 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:50.445 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:50.542 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.320 s
2022-07-26 16:44:51.360 - [NOTICE] callbacks.on_cycle_start(174): Cycle 1/10 (36.824 ms elapsed) --------------------
2022-07-26 16:44:51.360 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:51.515 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:51.545 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:51.638 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:51.684 - [NOTICE] callbacks.on_step_start(182): Cycle 1/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:52.031 - [NOTICE] callbacks.on_cycle_start(174): Cycle 2/10 (707.971 ms elapsed) --------------------
2022-07-26 16:44:52.031 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:52.156 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:52.171 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:52.238 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:52.251 - [NOTICE] callbacks.on_step_start(182): Cycle 2/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:52.355 - [NOTICE] callbacks.on_cycle_start(174): Cycle 3/10 (1.033 s elapsed) --------------------
2022-07-26 16:44:52.356 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:52.480 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:52.496 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:52.563 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:52.577 - [NOTICE] callbacks.on_step_start(182): Cycle 3/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:52.676 - [NOTICE] callbacks.on_cycle_start(174): Cycle 4/10 (1.353 s elapsed) --------------------
2022-07-26 16:44:52.676 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:52.800 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:52.815 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:52.881 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:52.895 - [NOTICE] callbacks.on_step_start(182): Cycle 4/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:52.989 - [NOTICE] callbacks.on_cycle_start(174): Cycle 5/10 (1.666 s elapsed) --------------------
2022-07-26 16:44:52.990 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:53.114 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:53.129 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:53.196 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:53.210 - [NOTICE] callbacks.on_step_start(182): Cycle 5/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:53.308 - [NOTICE] callbacks.on_cycle_start(174): Cycle 6/10 (1.985 s elapsed) --------------------
2022-07-26 16:44:53.308 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:53.432 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:53.447 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:53.517 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:53.531 - [NOTICE] callbacks.on_step_start(182): Cycle 6/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:53.627 - [NOTICE] callbacks.on_cycle_start(174): Cycle 7/10 (2.305 s elapsed) --------------------
2022-07-26 16:44:53.628 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:53.753 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:53.768 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:53.836 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:53.849 - [NOTICE] callbacks.on_step_start(182): Cycle 7/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:53.945 - [NOTICE] callbacks.on_cycle_start(174): Cycle 8/10 (2.622 s elapsed) --------------------
2022-07-26 16:44:53.945 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:54.072 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:54.086 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:54.160 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:54.173 - [NOTICE] callbacks.on_step_start(182): Cycle 8/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:54.269 - [NOTICE] callbacks.on_cycle_start(174): Cycle 9/10 (2.946 s elapsed) --------------------
2022-07-26 16:44:54.269 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:54.392 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:54.413 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:54.484 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:54.498 - [NOTICE] callbacks.on_step_start(182): Cycle 9/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:54.603 - [NOTICE] callbacks.on_cycle_start(174): Cycle 10/10 (3.281 s elapsed) --------------------
2022-07-26 16:44:54.604 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 1/5: Discharge at C/10 for 10 hours or until 3.3 V
2022-07-26 16:44:54.731 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 2/5: Rest for 1 hour
2022-07-26 16:44:54.747 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 3/5: Charge at 1 A until 4.1 V
2022-07-26 16:44:54.816 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 4/5: Hold at 4.1 V until 50 mA
2022-07-26 16:44:54.829 - [NOTICE] callbacks.on_step_start(182): Cycle 10/10, step 5/5: Rest for 1 hour
2022-07-26 16:44:54.926 - [NOTICE] callbacks.on_experiment_end(222): Finish experiment simulation, took 3.281 s
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x7ffdea372580>

The difference in the individual plots is not very well visible in the above slider plot, but we can access all the simulations created by BatchStudy (batch_study.sims) and pass it to pybamm.plot_summary_variables to plot the summary variables (more details on “summary variables” are available in the `simulating-long-experiments <./simulations_and_experiments/simulating-long-experiments.ipynb>`__ notebook).

Comparing summary variables#

[7]:
pybamm.plot_summary_variables([sim.solution for sim in batch_study.sims])
_images/source_examples_notebooks_batch_study_15_0.png
[7]:
array([[<AxesSubplot:xlabel='Cycle number', ylabel='Capacity [A.h]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of lithium inventory [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of capacity to SEI [A.h]'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in negative electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='Loss of active material in positive electrode [%]'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='x_100'>],
       [<AxesSubplot:xlabel='Cycle number', ylabel='x_0'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_100'>,
        <AxesSubplot:xlabel='Cycle number', ylabel='y_0'>]], dtype=object)

Other than the above examples, the BatchStudy class can be used to compare a lot of different configurations, like models with different SEIs or a model with reversible and irreversible lithium plating, etc.

References#

The relevant papers for this notebook are:

[8]:
pybamm.print_citations()
[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.
[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.
[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.
[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.
[5] 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.
[6] 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.
[7] Peyman Mohtat, Suhak Lee, Valentin Sulzer, Jason B. Siegel, and Anna G. Stefanopoulou. Differential Expansion and Voltage Model for Li-ion Batteries at Practical Charging Rates. Journal of The Electrochemical Society, 167(11):110561, 2020. doi:10.1149/1945-7111/aba5d1.
[8] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). Journal of Open Research Software, 9(1):14, 2021. doi:10.5334/jors.309.
[9] 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.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Changing settings when solving PyBaMM models#

This example notebook showed how to run an SPM battery model, using the default parameters, discretisation and solvers that were defined for that particular model. Naturally we would like the ability to alter these options on a case by case basis, and this notebook gives an example of how to do this, again using the SPM model. In this notebook we explicitly handle all the stages of setting up, processing and solving the model in order to explain them in detail. However, it is often simpler in practice to use the Simulation class, which handles many of the stages automatically, as shown here.

Table of Contents#

  1. Default SPM model

  2. Changing the parameters

  3. Changing the discretisation

  4. Changing the solver

The default SPM model#

Below is the code to define and run the default SPM model included in PyBaMM (if this is unfamiliar to you, please see this notebook for more details)

[17]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")

# create the model
model = pybamm.lithium_ion.SPM()

# set the default model geometry
geometry = model.default_geometry

# set the default model parameters
param = model.default_parameter_values

# set the parameters for the model and the geometry
param.process_model(model)
param.process_geometry(geometry)

# mesh the domains
mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)

# discretise the model equations
disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
disc.process_model(model)

# Solve the model at the given time points
solver = model.default_solver
n = 100
t_eval = np.linspace(0, 3600, n)
solution = solver.solve(model, t_eval)
WARNING: You are using pip version 22.0.4; however, version 22.3.1 is available.
You should consider upgrading via the '/home/mrobins/git/PyBaMM/env/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

To verify the solution, we can look at a plot of the Voltage over time

[18]:
time = solution["Time [h]"].entries
voltage = solution["Voltage [V]"].entries
plt.plot(time, voltage, lw=2, label=model.name)
plt.xlabel("Time [h]", fontsize=15)
plt.ylabel("Voltage [V]", fontsize=15)
plt.show()
_images/source_examples_notebooks_change-settings_4_0.png
Changing the model parameters#

The parameters are defined using the `pybamm.ParameterValues <https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html>`__ class, which takes either a python dictionary or CSV file with the mapping between parameter names and values.

First lets have a look at the default parameters that are included with the SPM model:

[19]:
model.default_parameter_values.items
[19]:
<bound method ParameterValues.items of {'Thermodynamic factor': 1.0,
 'Ambient temperature [K]': 298.15,
 'Bulk solvent concentration [mol.m-3]': 2636.0,
 'Cation transference number': 0.4,
 'Cell cooling surface area [m2]': 0.0569,
 'Cell volume [m3]': 7.8e-06,
 'Current function [A]': 0.680616,
 'EC diffusivity [m2.s-1]': 2e-18,
 'EC initial concentration in electrolyte [mol.m-3]': 4541.0,
 'Edge heat transfer coefficient [W.m-2.K-1]': 0.3,
 'Electrode height [m]': 0.137,
 'Electrode width [m]': 0.207,
 'Electrolyte conductivity [S.m-1]': <function electrolyte_conductivity_Capiglia1999 at 0x7fb36b261160>,
 'Electrolyte diffusivity [m2.s-1]': <function electrolyte_diffusivity_Capiglia1999 at 0x7fb36b2611f0>,
 'Initial concentration in electrolyte [mol.m-3]': 1000.0,
 'Initial concentration in negative electrode [mol.m-3]': 19986.609595075,
 'Initial concentration in positive electrode [mol.m-3]': 30730.7554385565,
 'Initial inner SEI thickness [m]': 2.5e-09,
 'Initial outer SEI thickness [m]': 2.5e-09,
 'Initial temperature [K]': 298.15,
 'Inner SEI electron conductivity [S.m-1]': 8.95e-14,
 'Inner SEI lithium interstitial diffusivity [m2.s-1]': 1e-20,
 'Inner SEI open-circuit potential [V]': 0.1,
 'Inner SEI partial molar volume [m3.mol-1]': 9.585e-05,
 'Inner SEI reaction proportion': 0.5,
 'Lithium interstitial reference concentration [mol.m-3]': 15.0,
 'Lower voltage cut-off [V]': 3.105,
 'Maximum concentration in negative electrode [mol.m-3]': 24983.2619938437,
 'Maximum concentration in positive electrode [mol.m-3]': 51217.9257309275,
 'Negative current collector conductivity [S.m-1]': 59600000.0,
 'Negative current collector density [kg.m-3]': 8954.0,
 'Negative current collector specific heat capacity [J.kg-1.K-1]': 385.0,
 'Negative current collector surface heat transfer coefficient [W.m-2.K-1]': 0.0,
 'Negative current collector thermal conductivity [W.m-1.K-1]': 401.0,
 'Negative current collector thickness [m]': 2.5e-05,
 'Negative electrode Bruggeman coefficient (electrode)': 1.5,
 'Negative electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Negative electrode OCP [V]': <function graphite_mcmb2528_ocp_Dualfoil1998 at 0x7fb36b261670>,
 'Negative electrode OCP entropic change [V.K-1]': <function graphite_entropic_change_Moura2016 at 0x7fb36b261550>,
 'Negative electrode active material volume fraction': 0.6,
 'Negative electrode cation signed stoichiometry': -1.0,
 'Negative electrode charge transfer coefficient': 0.5,
 'Negative electrode conductivity [S.m-1]': 100.0,
 'Negative electrode density [kg.m-3]': 1657.0,
 'Negative electrode diffusivity [m2.s-1]': <function graphite_mcmb2528_diffusivity_Dualfoil1998 at 0x7fb36b2ad040>,
 'Negative electrode double-layer capacity [F.m-2]': 0.2,
 'Negative electrode electrons in reaction': 1.0,
 'Negative electrode exchange-current density [A.m-2]': <function graphite_electrolyte_exchange_current_density_Dualfoil1998 at 0x7fb36b2615e0>,
 'Negative electrode porosity': 0.3,
 'Negative electrode reaction-driven LAM factor [m3.mol-1]': 0.0,
 'Negative electrode specific heat capacity [J.kg-1.K-1]': 700.0,
 'Negative electrode thermal conductivity [W.m-1.K-1]': 1.7,
 'Negative electrode thickness [m]': 0.0001,
 'Negative particle radius [m]': 1e-05,
 'Negative tab centre y-coordinate [m]': 0.06,
 'Negative tab centre z-coordinate [m]': 0.137,
 'Negative tab heat transfer coefficient [W.m-2.K-1]': 10.0,
 'Negative tab width [m]': 0.04,
 'Nominal cell capacity [A.h]': 0.680616,
 'Number of cells connected in series to make a battery': 1.0,
 'Number of electrodes connected in parallel to make a cell': 1.0,
 'Outer SEI open-circuit potential [V]': 0.8,
 'Outer SEI partial molar volume [m3.mol-1]': 9.585e-05,
 'Outer SEI solvent diffusivity [m2.s-1]': 2.5000000000000002e-22,
 'Positive current collector conductivity [S.m-1]': 35500000.0,
 'Positive current collector density [kg.m-3]': 2707.0,
 'Positive current collector specific heat capacity [J.kg-1.K-1]': 897.0,
 'Positive current collector surface heat transfer coefficient [W.m-2.K-1]': 0.0,
 'Positive current collector thermal conductivity [W.m-1.K-1]': 237.0,
 'Positive current collector thickness [m]': 2.5e-05,
 'Positive electrode Bruggeman coefficient (electrode)': 1.5,
 'Positive electrode Bruggeman coefficient (electrolyte)': 1.5,
 'Positive electrode OCP [V]': <function lico2_ocp_Dualfoil1998 at 0x7fb36b2613a0>,
 'Positive electrode OCP entropic change [V.K-1]': <function lico2_entropic_change_Moura2016 at 0x7fb36b261280>,
 'Positive electrode active material volume fraction': 0.5,
 'Positive electrode cation signed stoichiometry': -1.0,
 'Positive electrode charge transfer coefficient': 0.5,
 'Positive electrode conductivity [S.m-1]': 10.0,
 'Positive electrode density [kg.m-3]': 3262.0,
 'Positive electrode diffusivity [m2.s-1]': <function lico2_diffusivity_Dualfoil1998 at 0x7fb36b2614c0>,
 'Positive electrode double-layer capacity [F.m-2]': 0.2,
 'Positive electrode electrons in reaction': 1.0,
 'Positive electrode exchange-current density [A.m-2]': <function lico2_electrolyte_exchange_current_density_Dualfoil1998 at 0x7fb36b261310>,
 'Positive electrode porosity': 0.3,
 'Positive electrode reaction-driven LAM factor [m3.mol-1]': 0.0,
 'Positive electrode specific heat capacity [J.kg-1.K-1]': 700.0,
 'Positive electrode thermal conductivity [W.m-1.K-1]': 2.1,
 'Positive electrode thickness [m]': 0.0001,
 'Positive particle radius [m]': 1e-05,
 'Positive tab centre y-coordinate [m]': 0.147,
 'Positive tab centre z-coordinate [m]': 0.137,
 'Positive tab heat transfer coefficient [W.m-2.K-1]': 10.0,
 'Positive tab width [m]': 0.04,
 'Ratio of lithium moles to SEI moles': 2.0,
 'Reference temperature [K]': 298.15,
 'SEI growth activation energy [J.mol-1]': 0.0,
 'SEI kinetic rate constant [m.s-1]': 1e-12,
 'SEI open-circuit potential [V]': 0.4,
 'SEI reaction exchange current density [A.m-2]': 1.5e-07,
 'SEI resistivity [Ohm.m]': 200000.0,
 'Separator Bruggeman coefficient (electrolyte)': 1.5,
 'Separator density [kg.m-3]': 397.0,
 'Separator porosity': 1.0,
 'Separator specific heat capacity [J.kg-1.K-1]': 700.0,
 'Separator thermal conductivity [W.m-1.K-1]': 0.16,
 'Separator thickness [m]': 2.5e-05,
 'Total heat transfer coefficient [W.m-2.K-1]': 10.0,
 'Typical current [A]': 0.680616,
 'Typical electrolyte concentration [mol.m-3]': 1000.0,
 'Upper voltage cut-off [V]': 4.1,
 'citations': ['Marquis2019']}>
[20]:
format_str = "{:<75}  {:>20}"
print(format_str.format("PARAMETER", "VALUE"))
print("-" * 97)
for key, value in model.default_parameter_values.items():
    try:
        print(format_str.format(key, value))
    except TypeError:
        print(format_str.format(key, value.__str__()))
PARAMETER                                                                                   VALUE
-------------------------------------------------------------------------------------------------
Ratio of lithium moles to SEI moles                                                           2.0
Inner SEI reaction proportion                                                                 0.5
Inner SEI partial molar volume [m3.mol-1]                                               9.585e-05
Outer SEI partial molar volume [m3.mol-1]                                               9.585e-05
SEI reaction exchange current density [A.m-2]                                             1.5e-07
SEI resistivity [Ohm.m]                                                                  200000.0
Outer SEI solvent diffusivity [m2.s-1]                                       2.5000000000000002e-22
Bulk solvent concentration [mol.m-3]                                                       2636.0
Inner SEI open-circuit potential [V]                                                          0.1
Outer SEI open-circuit potential [V]                                                          0.8
Inner SEI electron conductivity [S.m-1]                                                  8.95e-14
Inner SEI lithium interstitial diffusivity [m2.s-1]                                         1e-20
Lithium interstitial reference concentration [mol.m-3]                                       15.0
Initial inner SEI thickness [m]                                                           2.5e-09
Initial outer SEI thickness [m]                                                           2.5e-09
EC initial concentration in electrolyte [mol.m-3]                                          4541.0
EC diffusivity [m2.s-1]                                                                     2e-18
SEI kinetic rate constant [m.s-1]                                                           1e-12
SEI open-circuit potential [V]                                                                0.4
SEI growth activation energy [J.mol-1]                                                        0.0
Negative electrode reaction-driven LAM factor [m3.mol-1]                                      0.0
Positive electrode reaction-driven LAM factor [m3.mol-1]                                      0.0
Negative current collector thickness [m]                                                  2.5e-05
Negative electrode thickness [m]                                                           0.0001
Separator thickness [m]                                                                   2.5e-05
Positive electrode thickness [m]                                                           0.0001
Positive current collector thickness [m]                                                  2.5e-05
Electrode height [m]                                                                        0.137
Electrode width [m]                                                                         0.207
Negative tab width [m]                                                                       0.04
Negative tab centre y-coordinate [m]                                                         0.06
Negative tab centre z-coordinate [m]                                                        0.137
Positive tab width [m]                                                                       0.04
Positive tab centre y-coordinate [m]                                                        0.147
Positive tab centre z-coordinate [m]                                                        0.137
Cell cooling surface area [m2]                                                             0.0569
Cell volume [m3]                                                                          7.8e-06
Negative current collector conductivity [S.m-1]                                        59600000.0
Positive current collector conductivity [S.m-1]                                        35500000.0
Negative current collector density [kg.m-3]                                                8954.0
Positive current collector density [kg.m-3]                                                2707.0
Negative current collector specific heat capacity [J.kg-1.K-1]                              385.0
Positive current collector specific heat capacity [J.kg-1.K-1]                              897.0
Negative current collector thermal conductivity [W.m-1.K-1]                                 401.0
Positive current collector thermal conductivity [W.m-1.K-1]                                 237.0
Nominal cell capacity [A.h]                                                              0.680616
Typical current [A]                                                                      0.680616
Current function [A]                                                                     0.680616
Negative electrode conductivity [S.m-1]                                                     100.0
Maximum concentration in negative electrode [mol.m-3]                            24983.2619938437
Negative electrode diffusivity [m2.s-1]                                      <function graphite_mcmb2528_diffusivity_Dualfoil1998 at 0x7fb36b2ad040>
Negative electrode OCP [V]                                                   <function graphite_mcmb2528_ocp_Dualfoil1998 at 0x7fb36b261670>
Negative electrode porosity                                                                   0.3
Negative electrode active material volume fraction                                            0.6
Negative particle radius [m]                                                                1e-05
Negative electrode Bruggeman coefficient (electrolyte)                                        1.5
Negative electrode Bruggeman coefficient (electrode)                                          1.5
Negative electrode cation signed stoichiometry                                               -1.0
Negative electrode electrons in reaction                                                      1.0
Negative electrode charge transfer coefficient                                                0.5
Negative electrode double-layer capacity [F.m-2]                                              0.2
Negative electrode exchange-current density [A.m-2]                          <function graphite_electrolyte_exchange_current_density_Dualfoil1998 at 0x7fb36b2615e0>
Negative electrode density [kg.m-3]                                                        1657.0
Negative electrode specific heat capacity [J.kg-1.K-1]                                      700.0
Negative electrode thermal conductivity [W.m-1.K-1]                                           1.7
Negative electrode OCP entropic change [V.K-1]                               <function graphite_entropic_change_Moura2016 at 0x7fb36b261550>
Positive electrode conductivity [S.m-1]                                                      10.0
Maximum concentration in positive electrode [mol.m-3]                            51217.9257309275
Positive electrode diffusivity [m2.s-1]                                      <function lico2_diffusivity_Dualfoil1998 at 0x7fb36b2614c0>
Positive electrode OCP [V]                                                   <function lico2_ocp_Dualfoil1998 at 0x7fb36b2613a0>
Positive electrode porosity                                                                   0.3
Positive electrode active material volume fraction                                            0.5
Positive particle radius [m]                                                                1e-05
Positive electrode Bruggeman coefficient (electrolyte)                                        1.5
Positive electrode Bruggeman coefficient (electrode)                                          1.5
Positive electrode cation signed stoichiometry                                               -1.0
Positive electrode electrons in reaction                                                      1.0
Positive electrode charge transfer coefficient                                                0.5
Positive electrode double-layer capacity [F.m-2]                                              0.2
Positive electrode exchange-current density [A.m-2]                          <function lico2_electrolyte_exchange_current_density_Dualfoil1998 at 0x7fb36b261310>
Positive electrode density [kg.m-3]                                                        3262.0
Positive electrode specific heat capacity [J.kg-1.K-1]                                      700.0
Positive electrode thermal conductivity [W.m-1.K-1]                                           2.1
Positive electrode OCP entropic change [V.K-1]                               <function lico2_entropic_change_Moura2016 at 0x7fb36b261280>
Separator porosity                                                                            1.0
Separator Bruggeman coefficient (electrolyte)                                                 1.5
Separator density [kg.m-3]                                                                  397.0
Separator specific heat capacity [J.kg-1.K-1]                                               700.0
Separator thermal conductivity [W.m-1.K-1]                                                   0.16
Typical electrolyte concentration [mol.m-3]                                                1000.0
Initial concentration in electrolyte [mol.m-3]                                             1000.0
Cation transference number                                                                    0.4
Thermodynamic factor                                                                                 1.0
Electrolyte diffusivity [m2.s-1]                                             <function electrolyte_diffusivity_Capiglia1999 at 0x7fb36b2611f0>
Electrolyte conductivity [S.m-1]                                             <function electrolyte_conductivity_Capiglia1999 at 0x7fb36b261160>
Reference temperature [K]                                                                  298.15
Ambient temperature [K]                                                                    298.15
Negative current collector surface heat transfer coefficient [W.m-2.K-1]                      0.0
Positive current collector surface heat transfer coefficient [W.m-2.K-1]                      0.0
Negative tab heat transfer coefficient [W.m-2.K-1]                                           10.0
Positive tab heat transfer coefficient [W.m-2.K-1]                                           10.0
Edge heat transfer coefficient [W.m-2.K-1]                                                    0.3
Total heat transfer coefficient [W.m-2.K-1]                                                  10.0
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]                                                                   3.105
Upper voltage cut-off [V]                                                                     4.1
Initial concentration in negative electrode [mol.m-3]                             19986.609595075
Initial concentration in positive electrode [mol.m-3]                            30730.7554385565
Initial temperature [K]                                                                    298.15
citations                                                                         ['Marquis2019']

Most of the parameters in this list have numerical values. Some have string values, that point to particular python functions within PyBaMM. These denote parameters that vary over time and/or space, in a manner defined by the given python function. For the moment we will ignore these, and focus on altering one of the numerical parameters.

The class `pybamm.ParameterValues <https://docs.pybamm.org/en/latest/source/api/parameters/parameter_values.html>`__ acts like the normal python dict data structure, so you can read or write individual parameters accordingly:

[21]:
variable = "Current function [A]"
old_value = param[variable]
param[variable] = 1.4
new_value = param[variable]
print(variable, "was", old_value)
print(variable, "now is", param[variable])
Current function [A] was 0.680616
Current function [A] now is 1.4

In order to compare solutions with different parameter values, parameters must be changed to InputParameter objects, whose value can then be specified when solving. For example, we can compare the Voltage calculated using both the old and new current values.

[22]:
# Set up
model = pybamm.lithium_ion.SPM()
solver = model.default_solver
param["Current function [A]"] = "[input]"
param.process_model(model)
disc.process_model(model)

# Solution with current = 0.68
old_solution = solver.solve(model, t_eval, inputs={"Current function [A]": 0.68})
old_time = old_solution["Time [h]"].entries
old_voltage = old_solution["Voltage [V]"].entries

# Solution with current = 1.4
new_solution = solver.solve(model, t_eval, inputs={"Current function [A]": 1.4})
new_time = new_solution["Time [h]"].entries
new_voltage = new_solution["Voltage [V]"].entries

plt.plot(old_time, old_voltage, lw=2, label="Current = 0.68")
plt.plot(new_time, new_voltage, lw=2, label="Current = 1.4")
plt.xlabel("Time [h]", fontsize=15)
plt.ylabel("Voltage [V]", fontsize=15)
plt.legend(fontsize=15)
plt.show()
_images/source_examples_notebooks_change-settings_11_0.png

Note that there is a small number of parameters for which this is not possible, since the discretisation depends on them (for example, Negative electrode thickness). In this case you would need to remesh the geometry as the relative length of the electrodes would have altered. This would in turn require you to re-discretise the model equations.

Changing the discretisation#

The chosen spatial discretisation method to use for each domain is passed into the `pybamm.Discretisation <https://docs.pybamm.org/en/latest/source/api/spatial_methods/discretisation.html>`__ class as one of its arguments. The default spatial methods for the SPM class are given as:

[23]:
print(format_str.format("DOMAIN", "DISCRETISED BY"))
print("-" * 82)
for key, value in model.default_spatial_methods.items():
    print(format_str.format(key, value.__class__.__name__))
DOMAIN                                                                             DISCRETISED BY
----------------------------------------------------------------------------------
macroscale                                                                           FiniteVolume
negative particle                                                                    FiniteVolume
positive particle                                                                    FiniteVolume
negative primary particle                                                            FiniteVolume
positive primary particle                                                            FiniteVolume
negative secondary particle                                                          FiniteVolume
positive secondary particle                                                          FiniteVolume
negative particle size                                                               FiniteVolume
positive particle size                                                               FiniteVolume
current collector                                                            ZeroDimensionalSpatialMethod

To change the discretisation for a particular domain, you can update the spatial methods dict with the new discretiation class that you wish to use, for example:

[24]:
spatial_methods = model.default_spatial_methods
spatial_methods["negative particle"] = pybamm.SpectralVolume()

You can also update the submeshes (meshes used for each domain) in a similar way. We’ll generate a spectral mesh to use with our spectral volume method in the negative particle

[25]:
submesh_types = model.default_submesh_types
submesh_types["negative particle"] = pybamm.MeshGenerator(
    pybamm.SpectralVolume1DSubMesh
)

Now that we have set the new discretisation method, we can proceed to re-discretise and then solve the model. Note that in this case we need to regenerate the model using pybamm.lithium_ion.SPM, as the original model information was lost when we discretised it above.

[26]:
# re-generate the model and set parameters (with fixed current function this time)
model = pybamm.lithium_ion.SPM()
solver = model.default_solver
param["Current function [A]"] = 0.68
param.process_model(model)

# re-discretise the model with the new mesh...
mesh = pybamm.Mesh(geometry, submesh_types, model.default_var_pts)
disc = pybamm.Discretisation(mesh, spatial_methods)
disc.process_model(model)

# ... and finally solve it
solution = solver.solve(model, t_eval)
pybamm.QuickPlot(solution).plot(0)
_images/source_examples_notebooks_change-settings_20_0.png
Changing the solver#

Which method you use to integrate the discretised model in time can also be changed. PyBaMM has a number of different solvers available, all of which are described in the documentation.

[27]:
print("Default solver for SPM model:", type(model.default_solver).__name__)
Default solver for SPM model: CasadiSolver

To change this, simply create a new solver using one of the available classes and use it to solve your discretised model

[28]:
new_solver = pybamm.ScipySolver()
new_solution = new_solver.solve(model, t_eval)
pybamm.QuickPlot(new_solution).plot(0)
_images/source_examples_notebooks_change-settings_24_0.png
References#

The relevant papers for this notebook are:

[29]:
pybamm.print_citations()
[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.
[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.
[3] 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.
[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.
[5] 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.
[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.
[7] 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.
[8] Z. J. Wang. Spectral (finite) volume method for conservation laws on unstructured grids. Journal of Computational Physics, 178(1):210–251, 2002. doi:10.1006/jcph.2002.7041.

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

Initializing a model#

Example showing how to initialize a model with another model

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed

import pybamm
import pandas as pd
import os

os.chdir(pybamm.__path__[0] + "/..")
WARNING: You are using pip version 21.0.1; however, version 21.1 is available.
You should consider upgrading via the '/Users/vsulzer/Documents/Energy_storage/PyBaMM/.tox/dev/bin/python -m pip install --upgrade pip' command.
Note: you may need to restart the kernel to use updated packages.

Solve a model with a drive cycle#

Load model

[2]:
model = pybamm.lithium_ion.DFN()

Set up drive cycle

[3]:
# import drive cycle from file
drive_cycle = pd.read_csv(
    "pybamm/input/drive_cycles/US06.csv", comment="#", header=None
).to_numpy()
# create interpolant
param = model.default_parameter_values
current_interpolant = pybamm.Interpolant(drive_cycle[:, 0], drive_cycle[:, 1], pybamm.t)
# set drive cycle
param["Current function [A]"] = current_interpolant

Create and run simulation using the CasadiSolver in “fast” mode, remembering to pass in the updated parameters

[4]:
sim_US06_1 = pybamm.Simulation(
    model, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast")
)
sol_US06_1 = sim_US06_1.solve()

Update initial conditions based on a solution and solve again#

Now pre-charge with CCCV, update the initial conditions, and solve again with the US06 drive cycle

[5]:
experiment = pybamm.Experiment(
    ["Charge at 1 A until 4.1 V", "Hold at 4.1 V until 50 mA"]
)
sim_cccv = pybamm.Simulation(model, experiment=experiment)
sol_cccv = sim_cccv.solve()

# MODEL RE-INITIALIZATION: #############################################################
# Now initialize the model with the solution of the charge, and then discharge with
# the US06 drive cycle
# We could also do this inplace by setting inplace to True, which modifies the original
# model in place
new_model = model.set_initial_conditions_from(sol_cccv, inplace=False)
########################################################################################

sim_US06_2 = pybamm.Simulation(
    new_model, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast")
)
sol_US06_2 = sim_US06_2.solve()

Plot both solutions, we can clearly see the difference now that initial conditions have been updated

[6]:
pybamm.dynamic_plot(
    [sol_US06_1, sol_US06_2], labels=["Default initial conditions", "Fully charged"]
)
[6]:
<pybamm.plotting.quick_plot.QuickPlot at 0x1447078e0>

Initialize using a different model#

We can also initialize the model using the solution of a different model

[7]:
spm = pybamm.lithium_ion.SPM()
sim_spm_cccv = pybamm.Simulation(spm, experiment=experiment)
sol_spm_cccv = sim_spm_cccv.solve()

# MODEL RE-INITIALIZATION: #############################################################
# Now initialize the model with the solution of the charge, and then discharge with
# the US06 drive cycle
# We could also do this inplace by setting inplace to True, which modifies the original
# model in place
new_dfn = model.set_initial_conditions_from(sol_spm_cccv, inplace=False)
########################################################################################

sim_US06_3 = pybamm.Simulation(
    new_dfn, parameter_values=param, solver=pybamm.CasadiSolver(mode="fast")
)
sol_US06_3 = sim_US06_3.solve()

Now the model initialized by the DFN and the model initialized by the SPM give the same solution

[8]:
pybamm.dynamic_plot(
    [sol_US06_1, sol_US06_2, sol_US06_3],
    labels=[
        "Default initial conditions",
        "Fully charged (from DFN)",
        "Fully charged (from SPM)",
    ],
)
[8]:
<pybamm.plotting.quick_plot.QuickPlot at 0x10437c370>

References#

The relevant papers for this notebook are:

[9]:
pybamm.print_citations()
[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.
[2] 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.
[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.
[4] 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.
[5] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

[ ]:

Tip

An interactive online version of this notebook is available, which can be accessed via Open this notebook in Google Colab


Alternatively, you may download this notebook and run it offline.

A look at solution data and processed variables#

Once you have run a simulation the first thing you want to do is have a look at the data. Most of the examples so far have made use of PyBaMM’s handy QuickPlot function but there are other ways to access the data and this notebook will explore them. First off we will generate a standard SPMe model and use QuickPlot to view the default variables.

[1]:
%pip install "pybamm[plot,cite]" -q    # install PyBaMM if it is not installed
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt

os.chdir(pybamm.__path__[0] + "/..")

# load model
model = pybamm.lithium_ion.SPMe()

# set up and solve simulation
simulation = pybamm.Simulation(model)
dt = 90
t_eval = np.arange(0, 3600, dt)  # time in seconds
solution = simulation.solve(t_eval)

quick_plot = pybamm.QuickPlot(solution)
quick_plot.dynamic_plot();
Note: you may need to restart the kernel to use updated packages.

Behind the scenes the QuickPlot classed has created some processed variables which can interpolate the model variables for our solution and has also stored the results for the solution steps

[2]:
solution.data.keys()
[2]:
dict_keys(['Negative particle surface concentration [mol.m-3]', 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Voltage [V]'])
[3]:
solution.data["Negative particle surface concentration [mol.m-3]"].shape
[3]:
(20, 40)
[4]:
solution.t.shape
[4]:
(40,)

Notice that the dictionary keys are in the same order as the subplots in the QuickPlot figure. We can add new processed variables to the solution by simply using it like a dictionary. First let’s find a few more variables to look at. As you will see there are quite a few:

[5]:
keys = list(model.variables.keys())
keys.sort()
print(keys)
['Ambient temperature', 'Ambient temperature [K]', 'Average negative particle concentration', 'Average negative particle concentration [mol.m-3]', 'Average positive particle concentration', 'Average positive particle concentration [mol.m-3]', 'Battery voltage [V]', 'C-rate', 'Cell temperature', 'Cell temperature [K]', 'Change in measured open-circuit voltage', 'Change in measured open-circuit voltage [V]', 'Current [A]', 'Current collector current density', 'Current collector current density [A.m-2]', 'Discharge capacity [A.h]', 'Electrode current density', 'Electrode tortuosity', 'Electrolyte concentration', 'Electrolyte concentration [Molar]', 'Electrolyte concentration [mol.m-3]', 'Electrolyte current density', 'Electrolyte current density [A.m-2]', 'Electrolyte flux', 'Electrolyte flux [mol.m-2.s-1]', 'Electrolyte potential', 'Electrolyte potential [V]', 'Electrolyte tortuosity', 'Exchange current density', 'Exchange current density [A.m-2]', 'Exchange current density per volume [A.m-3]', 'Gradient of electrolyte potential', 'Gradient of negative electrode potential', 'Gradient of negative electrolyte potential', 'Gradient of positive electrode potential', 'Gradient of positive electrolyte potential', 'Gradient of separator electrolyte potential', 'Inner SEI concentration [mol.m-3]', 'Inner SEI interfacial current density', 'Inner SEI interfacial current density [A.m-2]', 'Inner SEI thickness', 'Inner SEI thickness [m]', 'Inner positive electrode SEI concentration [mol.m-3]', 'Inner positive electrode SEI interfacial current density', 'Inner positive electrode SEI interfacial current density [A.m-2]', 'Inner positive electrode SEI thickness', 'Inner positive electrode SEI thickness [m]', 'Interfacial current density', 'Interfacial current density [A.m-2]', 'Interfacial current density per volume [A.m-3]', 'Irreversible electrochemical heating', 'Irreversible electrochemical heating [W.m-3]', 'Leading-order current collector current density', 'Leading-order electrode tortuosity', 'Leading-order electrolyte tortuosity', 'Leading-order negative electrode porosity', 'Leading-order negative electrode tortuosity', 'Leading-order negative electrolyte tortuosity', 'Leading-order porosity', 'Leading-order positive electrode porosity', 'Leading-order positive electrode tortuosity', 'Leading-order positive electrolyte tortuosity', 'Leading-order separator porosity', 'Leading-order separator tortuosity', 'Leading-order x-averaged negative electrode porosity', 'Leading-order x-averaged negative electrode porosity change', 'Leading-order x-averaged negative electrode tortuosity', 'Leading-order x-averaged negative electrolyte tortuosity', 'Leading-order x-averaged positive electrode porosity', 'Leading-order x-averaged positive electrode porosity change', 'Leading-order x-averaged positive electrode tortuosity', 'Leading-order x-averaged positive electrolyte tortuosity', 'Leading-order x-averaged separator porosity', 'Leading-order x-averaged separator porosity change', 'Leading-order x-averaged separator tortuosity', 'Local ECM resistance', 'Local ECM resistance [Ohm]', 'Local voltage', 'Local voltage [V]', 'Loss of lithium to SEI [mol]', 'Loss of lithium to positive electrode SEI [mol]', 'Maximum negative particle concentration', 'Maximum negative particle concentration [mol.m-3]', 'Maximum negative particle surface concentration', 'Maximum negative particle surface concentration [mol.m-3]', 'Maximum positive particle concentration', 'Maximum positive particle concentration [mol.m-3]', 'Maximum positive particle surface concentration', 'Maximum positive particle surface concentration [mol.m-3]', 'Measured battery open-circuit voltage [V]', 'Measured open-circuit voltage', 'Measured open-circuit voltage [V]', 'Minimum negative particle concentration', 'Minimum negative particle concentration [mol.m-3]', 'Minimum negative particle surface concentration', 'Minimum negative particle surface concentration [mol.m-3]', 'Minimum positive particle concentration', 'Minimum positive particle concentration [mol.m-3]', 'Minimum positive particle surface concentration', 'Minimum positive particle surface concentration [mol.m-3]', 'Negative current collector potential', 'Negative current collector potential [V]', 'Negative current collector temperature', 'Negative current collector temperature [K]', 'Negative electrode active material volume fraction', 'Negative electrode active material volume fraction change', 'Negative electrode current density', 'Negative electrode current density [A.m-2]', 'Negative electrode entropic change', 'Negative electrode exchange current density', 'Negative electrode exchange current density [A.m-2]', 'Negative electrode exchange current density per volume [A.m-3]', 'Negative electrode extent of lithiation', 'Negative electrode interfacial current density', 'Negative electrode interfacial current density [A.m-2]', 'Negative electrode interfacial current density per volume [A.m-3]', 'Negative electrode ohmic losses', 'Negative electrode ohmic losses [V]', 'Negative electrode open-circuit potential', 'Negative electrode open-circuit potential [V]', 'Negative electrode oxygen exchange current density', 'Negative electrode oxygen exchange current density [A.m-2]', 'Negative electrode oxygen exchange current density per volume [A.m-3]', 'Negative electrode oxygen interfacial current density', 'Negative electrode oxygen interfacial current density [A.m-2]', 'Negative electrode oxygen interfacial current density per volume [A.m-3]', 'Negative electrode oxygen open-circuit potential', 'Negative electrode oxygen open-circuit potential [V]', 'Negative electrode oxygen reaction overpotential', 'Negative electrode oxygen reaction overpotential [V]', 'Negative electrode porosity', 'Negative electrode porosity change', 'Negative electrode potential', 'Negative electrode potential [V]', 'Negative electrode pressure', 'Negative electrode reaction overpotential', 'Negative electrode reaction overpotential [V]', 'SEI film overpotential', 'SEI film overpotential [V]', 'SEI interfacial current density', 'SEI interfacial current density [A.m-2]', 'Negative electrode surface area to volume ratio', 'Negative electrode surface area to volume ratio [m-1]', 'Negative electrode surface potential difference', 'Negative electrode surface potential difference [V]', 'Negative electrode temperature', 'Negative electrode temperature [K]', 'Negative electrode tortuosity', 'Negative electrode transverse volume-averaged acceleration', 'Negative electrode transverse volume-averaged acceleration [m.s-2]', 'Negative electrode transverse volume-averaged velocity', 'Negative electrode transverse volume-averaged velocity [m.s-2]', 'Negative electrode volume-averaged acceleration', 'Negative electrode volume-averaged acceleration [m.s-1]', 'Negative electrode volume-averaged concentration', 'Negative electrode volume-averaged concentration [mol.m-3]', 'Negative electrode volume-averaged velocity', 'Negative electrode volume-averaged velocity [m.s-1]', 'Negative electrolyte concentration', 'Negative electrolyte concentration [Molar]', 'Negative electrolyte concentration [mol.m-3]', 'Negative electrolyte current density', 'Negative electrolyte current density [A.m-2]', 'Negative electrolyte potential', 'Negative electrolyte potential [V]', 'Negative electrolyte tortuosity', 'Negative particle concentration', 'Negative particle concentration [mol.m-3]', 'Negative particle flux', 'Negative particle radius', 'Negative particle radius [m]', 'Negative particle surface concentration', 'Negative particle surface concentration [mol.m-3]', 'Negative SEI concentration [mol.m-3]', 'Ohmic heating', 'Ohmic heating [W.m-3]', 'Outer SEI concentration [mol.m-3]', 'Outer SEI interfacial current density', 'Outer SEI interfacial current density [A.m-2]', 'Outer SEI thickness', 'Outer SEI thickness [m]', 'Outer positive electrode SEI concentration [mol.m-3]', 'Outer positive electrode SEI interfacial current density', 'Outer positive electrode SEI interfacial current density [A.m-2]', 'Outer positive electrode SEI thickness', 'Outer positive electrode SEI thickness [m]', 'Oxygen exchange current density', 'Oxygen exchange current density [A.m-2]', 'Oxygen exchange current density per volume [A.m-3]', 'Oxygen interfacial current density', 'Oxygen interfacial current density [A.m-2]', 'Oxygen interfacial current density per volume [A.m-3]', 'Porosity', 'Porosity change', 'Positive current collector potential', 'Positive current collector potential [V]', 'Positive current collector temperature', 'Positive current collector temperature [K]', 'Positive electrode active material volume fraction', 'Positive electrode active material volume fraction change', 'Positive electrode current density', 'Positive electrode current density [A.m-2]', 'Positive electrode entropic change', 'Positive electrode exchange current density', 'Positive electrode exchange current density [A.m-2]', 'Positive electrode exchange current density per volume [A.m-3]', 'Positive electrode extent of lithiation', 'Positive electrode interfacial current density', 'Positive electrode interfacial current density [A.m-2]', 'Positive electrode interfacial current density per volume [A.m-3]', 'Positive electrode ohmic losses', 'Positive electrode ohmic losses [V]', 'Positive electrode open-circuit potential', 'Positive electrode open-circuit potential [V]', 'Positive electrode oxygen exchange current density', 'Positive electrode oxygen exchange current density [A.m-2]', 'Positive electrode oxygen exchange current density per volume [A.m-3]', 'Positive electrode oxygen interfacial current density', 'Positive electrode oxygen interfacial current density [A.m-2]', 'Positive electrode oxygen interfacial current density per volume [A.m-3]', 'Positive electrode oxygen open-circuit potential', 'Positive electrode oxygen open-circuit potential [V]', 'Positive electrode oxygen reaction overpotential', 'Positive electrode oxygen reaction overpotential [V]', 'Positive electrode porosity', 'Positive electrode porosity change', 'Positive electrode potential', 'Positive electrode potential [V]', 'Positive electrode pressure', 'Positive electrode reaction overpotential', 'Positive electrode reaction overpotential [V]', 'Positive electrode SEI film overpotential', 'Positive electrode SEI film overpotential [V]', 'Positive electrode SEI interfacial current density', 'Positive electrode SEI interfacial current density [A.m-2]', 'Positive electrode surface area to volume ratio', 'Positive electrode surface area to volume ratio [m-1]', 'Positive electrode surface potential difference', 'Positive electrode surface potential difference [V]', 'Positive electrode temperature', 'Positive electrode temperature [K]', 'Positive electrode tortuosity', 'Positive electrode transverse volume-averaged acceleration', 'Positive electrode transverse volume-averaged acceleration [m.s-2]', 'Positive electrode transverse volume-averaged velocity', 'Positive electrode transverse volume-averaged velocity [m.s-2]', 'Positive electrode volume-averaged acceleration', 'Positive electrode volume-averaged acceleration [m.s-1]', 'Positive electrode volume-averaged concentration', 'Positive electrode volume-averaged concentration [mol.m-3]', 'Positive electrode volume-averaged velocity', 'Positive electrode volume-averaged velocity [m.s-1]', 'Positive electrolyte concentration', 'Positive electrolyte concentration [Molar]', 'Positive electrolyte concentration [mol.m-3]', 'Positive electrolyte current density', 'Positive electrolyte current density [A.m-2]', 'Positive electrolyte potential', 'Positive electrolyte potential [V]', 'Positive electrolyte tortuosity', 'Positive particle concentration', 'Positive particle concentration [mol.m-3]', 'Positive particle flux', 'Positive particle radius', 'Positive particle radius [m]', 'Positive particle surface concentration', 'Positive particle surface concentration [mol.m-3]', 'Positive SEI concentration [mol.m-3]', 'Pressure', 'R-averaged negative particle concentration', 'R-averaged negative particle concentration [mol.m-3]', 'R-averaged positive particle concentration', 'R-averaged positive particle concentration [mol.m-3]', 'Reversible heating', 'Reversible heating [W.m-3]', 'Sei interfacial current density', 'Sei interfacial current density [A.m-2]', 'Sei interfacial current density per volume [A.m-3]', 'Separator electrolyte concentration', 'Separator electrolyte concentration [Molar]', 'Separator electrolyte concentration [mol.m-3]', 'Separator electrolyte potential', 'Separator electrolyte potential [V]', 'Separator porosity', 'Separator porosity change', 'Separator pressure', 'Separator temperature', 'Separator temperature [K]', 'Separator tortuosity', 'Separator transverse volume-averaged acceleration', 'Separator transverse volume-averaged acceleration [m.s-2]', 'Separator transverse volume-averaged velocity', 'Separator transverse volume-averaged velocity [m.s-2]', 'Separator volume-averaged acceleration', 'Separator volume-averaged acceleration [m.s-1]', 'Separator volume-averaged velocity', 'Separator volume-averaged velocity [m.s-1]', 'Sum of electrolyte reaction source terms', 'Sum of interfacial current densities', 'Sum of negative electrode electrolyte reaction source terms', 'Sum of negative electrode interfacial current densities', 'Sum of positive electrode electrolyte reaction source terms', 'Sum of positive electrode interfacial current densities', 'Sum of x-averaged negative electrode electrolyte reaction source terms', 'Sum of x-averaged negative electrode interfacial current densities', 'Sum of x-averaged positive electrode electrolyte reaction source terms', 'Sum of x-averaged positive electrode interfacial current densities', 'Terminal power [W]', 'Voltage', 'Voltage [V]', 'Time', 'Time [h]', 'Time [min]', 'Time [s]', 'Total concentration in electrolyte [mol]', 'Total current density', 'Total current density [A.m-2]', 'Total heating', 'Total heating [W.m-3]', 'Total lithium in negative electrode [mol]', 'Total lithium in positive electrode [mol]', 'Total SEI thickness', 'Total SEI thickness [m]', 'Total positive electrode SEI thickness', 'Total positive electrode SEI thickness [m]', 'Transverse volume-averaged acceleration', 'Transverse volume-averaged acceleration [m.s-2]', 'Transverse volume-averaged velocity', 'Transverse volume-averaged velocity [m.s-2]', 'Volume-averaged Ohmic heating', 'Volume-averaged Ohmic heating [W.m-3]', 'Volume-averaged acceleration', 'Volume-averaged acceleration [m.s-1]', 'Volume-averaged cell temperature', 'Volume-averaged cell temperature [K]', 'Volume-averaged irreversible electrochemical heating', 'Volume-averaged irreversible electrochemical heating[W.m-3]', 'Volume-averaged reversible heating', 'Volume-averaged reversible heating [W.m-3]', 'Volume-averaged total heating', 'Volume-averaged total heating [W.m-3]', 'Volume-averaged velocity', 'Volume-averaged velocity [m.s-1]', 'X-averaged Ohmic heating', 'X-averaged Ohmic heating [W.m-3]', 'X-averaged battery concentration overpotential [V]', 'X-averaged battery electrolyte ohmic losses [V]', 'X-averaged battery open-circuit voltage [V]', 'X-averaged battery reaction overpotential [V]', 'X-averaged battery solid phase ohmic losses [V]', 'X-averaged cell temperature', 'X-averaged cell temperature [K]', 'X-averaged concentration overpotential', 'X-averaged concentration overpotential [V]', 'X-averaged electrolyte concentration', 'X-averaged electrolyte concentration [Molar]', 'X-averaged electrolyte concentration [mol.m-3]', 'X-averaged electrolyte ohmic losses', 'X-averaged electrolyte ohmic losses [V]', 'X-averaged electrolyte overpotential', 'X-averaged electrolyte overpotential [V]', 'X-averaged electrolyte potential', 'X-averaged electrolyte potential [V]', 'X-averaged inner SEI concentration [mol.m-3]', 'X-averaged inner SEI interfacial current density', 'X-averaged inner SEI interfacial current density [A.m-2]', 'X-averaged inner SEI thickness', 'X-averaged inner SEI thickness [m]', 'X-averaged inner positive electrode SEI concentration [mol.m-3]', 'X-averaged inner positive electrode SEI interfacial current density', 'X-averaged inner positive electrode SEI interfacial current density [A.m-2]', 'X-averaged inner positive electrode SEI thickness', 'X-averaged inner positive electrode SEI thickness [m]', 'X-averaged irreversible electrochemical heating', 'X-averaged irreversible electrochemical heating [W.m-3]', 'X-averaged negative electrode active material volume fraction', 'X-averaged negative electrode active material volume fraction change', 'X-averaged negative electrode entropic change', 'X-averaged negative electrode exchange current density', 'X-averaged negative electrode exchange current density [A.m-2]', 'X-averaged negative electrode exchange current density per volume [A.m-3]', 'X-averaged negative electrode extent of lithiation', 'X-averaged negative electrode interfacial current density', 'X-averaged negative electrode interfacial current density [A.m-2]', 'X-averaged negative electrode interfacial current density per volume [A.m-3]', 'X-averaged negative electrode ohmic losses', 'X-averaged negative electrode ohmic losses [V]', 'X-averaged negative electrode open-circuit potential', 'X-averaged negative electrode open-circuit potential [V]', 'X-averaged negative electrode oxygen exchange current density', 'X-averaged negative electrode oxygen exchange current density [A.m-2]', 'X-averaged negative electrode oxygen exchange current density per volume [A.m-3]', 'X-averaged negative electrode oxygen interfacial current density', 'X-averaged negative electrode oxygen interfacial current density [A.m-2]', 'X-averaged negative electrode oxygen interfacial current density per volume [A.m-3]', 'X-averaged negative electrode oxygen open-circuit potential', 'X-averaged negative electrode oxygen open-circuit potential [V]', 'X-averaged negative electrode oxygen reaction overpotential', 'X-averaged negative electrode oxygen reaction overpotential [V]', 'X-averaged negative electrode porosity', 'X-averaged negative electrode porosity change', 'X-averaged negative electrode potential', 'X-averaged negative electrode potential [V]', 'X-averaged negative electrode pressure', 'X-averaged negative electrode reaction overpotential', 'X-averaged negative electrode reaction overpotential [V]', 'X-averaged negative electrode resistance [Ohm.m2]', 'X-averaged SEI concentration [mol.m-3]', 'X-averaged SEI film overpotential', 'X-averaged SEI film overpotential [V]', 'X-averaged SEI interfacial current density', 'X-averaged SEI interfacial current density [A.m-2]', 'X-averaged negative electrode surface area to volume ratio', 'X-averaged negative electrode surface area to volume ratio [m-1]', 'X-averaged negative electrode surface potential difference', 'X-averaged negative electrode surface potential difference [V]', 'X-averaged negative electrode temperature', 'X-averaged negative electrode temperature [K]', 'X-averaged negative electrode tortuosity', 'X-averaged negative electrode total interfacial current density', 'X-averaged negative electrode total interfacial current density [A.m-2]', 'X-averaged negative electrode total interfacial current density per volume [A.m-3]', 'X-averaged negative electrode transverse volume-averaged acceleration', 'X-averaged negative electrode transverse volume-averaged acceleration [m.s-2]', 'X-averaged negative electrode transverse volume-averaged velocity', 'X-averaged negative electrode transverse volume-averaged velocity [m.s-2]', 'X-averaged negative electrode volume-averaged acceleration', 'X-averaged negative electrode volume-averaged acceleration [m.s-1]', 'X-averaged negative electrolyte concentration', 'X-averaged negative electrolyte concentration [mol.m-3]', 'X-averaged negative electrolyte potential', 'X-averaged negative electrolyte potential [V]', 'X-averaged negative electrolyte tortuosity', 'X-averaged negative particle concentration', 'X-averaged negative particle concentration [mol.m-3]', 'X-averaged negative particle flux', 'X-averaged negative particle surface concentration', 'X-averaged negative particle surface concentration [mol.m-3]', 'X-averaged open-circuit voltage', 'X-averaged open-circuit voltage [V]', 'X-averaged outer SEI concentration [mol.m-3]', 'X-averaged outer SEI interfacial current density', 'X-averaged outer SEI interfacial current density [A.m-2]', 'X-averaged outer SEI thickness', 'X-averaged outer SEI thickness [m]', 'X-averaged outer positive electrode SEI concentration [mol.m-3]', 'X-averaged outer positive electrode SEI interfacial current density', 'X-averaged outer positive electrode SEI interfacial current density [A.m-2]', 'X-averaged outer positive electrode SEI thickness', 'X-averaged outer positive electrode SEI thickness [m]', 'X-averaged positive electrode active material volume fraction', 'X-averaged positive electrode active material volume fraction change', 'X-averaged positive electrode entropic change', 'X-averaged positive electrode exchange current density', 'X-averaged positive electrode exchange current density [A.m-2]', 'X-averaged positive electrode exchange current density per volume [A.m-3]', 'X-averaged positive electrode extent of lithiation', 'X-averaged positive electrode interfacial current density', 'X-averaged positive electrode interfacial current density [A.m-2]', 'X-averaged positive electrode interfacial current density per volume [A.m-3]', 'X-averaged positive electrode ohmic losses', 'X-averaged positive electrode ohmic losses [V]', 'X-averaged positive electrode open-circuit potential', 'X-averaged positive electrode open-circuit potential [V]', 'X-averaged positive electrode oxygen exchange current density', 'X-averaged positive electrode oxygen exchange current density [A.m-2]', 'X-averaged positive electrode oxygen exchange current density per volume [A.m-3]', 'X-averaged positive electrode oxygen interfacial current density', 'X-averaged positive electrode oxygen interfacial current density [A.m-2]', 'X-averaged positive electrode oxygen interfacial current density per volume [A.m-3]', 'X-averaged positive electrode oxygen open-circuit potential', 'X-averaged positive electrode oxygen open-circuit potential [V]', 'X-averaged positive electrode oxygen reaction overpotential', 'X-averaged positive electrode oxygen reaction overpotential [V]', 'X-averaged positive electrode porosity', 'X-averaged positive electrode porosity change', 'X-averaged positive electrode potential', 'X-averaged positive electrode potential [V]', 'X-averaged positive electrode pressure', 'X-averaged positive electrode reaction overpotential', 'X-averaged positive electrode reaction overpotential [V]', 'X-averaged positive electrode resistance [Ohm.m2]', 'X-averaged positive electrode SEI concentration [mol.m-3]', 'X-averaged positive electrode SEI film overpotential', 'X-averaged positive electrode SEI film overpotential [V]', 'X-averaged positive electrode SEI interfacial current density', 'X-averaged positive electrode SEI interfacial current density [A.m-2]', 'X-averaged positive electrode surface area to volume ratio', 'X-averaged positive electrode surface area to volume ratio [m-1]', 'X-averaged positive electrode surface potential difference', 'X-averaged positive electrode surface potential difference [V]', 'X-averaged positive electrode temperature', 'X-averaged positive electrode temperature [K]', 'X-averaged positive electrode tortuosity', 'X-averaged positive electrode total interfacial current density', 'X-averaged positive electrode total interfacial current density [A.m-2]', 'X-averaged positive electrode total interfacial current density per volume [A.m-3]', 'X-averaged positive electrode transverse volume-averaged acceleration', 'X-averaged positive electrode transverse volume-averaged acceleration [m.s-2]', 'X-averaged positive electrode transverse volume-averaged velocity', 'X-averaged positive electrode transverse volume-averaged velocity [m.s-2]', 'X-averaged positive electrode volume-averaged acceleration', 'X-averaged positive electrode volume-averaged acceleration [m.s-1]', 'X-averaged positive electrolyte concentration', 'X-averaged positive electrolyte concentration [mol.m-3]', 'X-averaged positive electrolyte potential', 'X-averaged positive electrolyte potential [V]', 'X-averaged positive electrolyte tortuosity', 'X-averaged positive particle concentration', 'X-averaged positive particle concentration [mol.m-3]', 'X-averaged positive particle flux', 'X-averaged positive particle surface concentration', 'X-averaged positive particle surface concentration [mol.m-3]', 'X-averaged reaction overpotential', 'X-averaged reaction overpotential [V]', 'X-averaged reversible heating', 'X-averaged reversible heating [W.m-3]', 'X-averaged SEI film overpotential', 'X-averaged SEI film overpotential [V]', 'X-averaged separator electrolyte concentration', 'X-averaged separator electrolyte concentration [mol.m-3]', 'X-averaged separator electrolyte potential', 'X-averaged separator electrolyte potential [V]', 'X-averaged separator porosity', 'X-averaged separator porosity change', 'X-averaged separator pressure', 'X-averaged separator temperature', 'X-averaged separator temperature [K]', 'X-averaged separator tortuosity', 'X-averaged separator transverse volume-averaged acceleration', 'X-averaged separator transverse volume-averaged acceleration [m.s-2]', 'X-averaged separator transverse volume-averaged velocity', 'X-averaged separator transverse volume-averaged velocity [m.s-2]', 'X-averaged separator volume-averaged acceleration', 'X-averaged separator volume-averaged acceleration [m.s-1]', 'X-averaged solid phase ohmic losses', 'X-averaged solid phase ohmic losses [V]', 'X-averaged total heating', 'X-averaged total heating [W.m-3]', 'X-averaged total SEI thickness', 'X-averaged total SEI thickness [m]', 'X-averaged total positive electrode SEI thickness', 'X-averaged total positive electrode SEI thickness [m]', 'X-averaged volume-averaged acceleration', 'X-averaged volume-averaged acceleration [m.s-1]', 'r_n', 'r_n [m]', 'r_p', 'r_p [m]', 'x', 'x [m]', 'x_n', 'x_n [m]', 'x_p', 'x_p [m]', 'x_s', 'x_s [m]']

If you want to find a particular variable you can search the variables dictionary

[6]:
model.variables.search("time")
Time
Time [h]
Time [min]
Time [s]

We’ll use the time in hours

[7]:
solution["Time [h]"]
[7]:
<pybamm.solvers.processed_variable.ProcessedVariable at 0x7f69966b6210>

This created a new processed variable and stored it on the solution object

[8]:
solution.data.keys()
[8]:
dict_keys(['Negative particle surface concentration [mol.m-3]', 'Electrolyte concentration [mol.m-3]', 'Positive particle surface concentration [mol.m-3]', 'Current [A]', 'Negative electrode potential [V]', 'Electrolyte potential [V]', 'Positive electrode potential [V]', 'Voltage [V]', 'Time [h]'])

We can see the data by simply accessing the entries attribute of the processed variable

[9]:
solution["Time [h]"].entries
[9]:
array([0.   , 0.025, 0.05 , 0.075, 0.1  , 0.125, 0.15 , 0.175, 0.2  ,
       0.225, 0.25 , 0.275, 0.3  , 0.325, 0.35 , 0.375, 0.4  , 0.425,
       0.45 , 0.475, 0.5  , 0.525, 0.55 , 0.575, 0.6  , 0.625, 0.65 ,
       0.675, 0.7  , 0.725, 0.75 , 0.775, 0.8  , 0.825, 0.85 , 0.875,
       0.9  , 0.925, 0.95 , 0.975])

We can also call the method with specified time(s) in SI units of seconds

[10]:
time_in_seconds = np.array([0, 600, 900, 1700, 3000])
[11]:
solution["Time [h]"](time_in_seconds)
[11]:
array([0.        , 0.16666667, 0.25      , 0.47222222, 0.83333333])

If the variable has not already been processed it will be created behind the scenes

[12]:
var = "X-averaged negative electrode temperature [K]"
solution[var](time_in_seconds)
[12]:
array([298.15, 298.15, 298.15, 298.15, 298.15])

In this example the simulation was isothermal, so the temperature remains unchanged.

Saving the solution#

The solution can be saved in a number of ways:

[13]:
# to a pickle file (default)
solution.save_data(
    "outputs.pickle",
    ["Time [h]", "Current [A]", "Voltage [V]", "Electrolyte concentration [mol.m-3]"],
)
# to a matlab file
# need to give variable names without space
solution.save_data(
    "outputs.mat",
    ["Time [h]", "Current [A]", "Voltage [V]", "Electrolyte concentration [mol.m-3]"],
    to_format="matlab",
    short_names={
        "Time [h]": "t",
        "Current [A]": "I",
        "Voltage [V]": "V",
        "Electrolyte concentration [mol.m-3]": "c_e",
    },
)
# to a csv file (time-dependent outputs only, no spatial dependence allowed)
solution.save_data(
    "outputs.csv", ["Time [h]", "Current [A]", "Voltage [V]"], to_format="csv"
)

Stepping the solver#

The previous solution was created in one go with the solve method, but it is also possible to step the solution and look at the results as we go. In doing so, the results are automatically updated at each step.

[14]:
dt = 360
time = 0
end_time = solution["Time [s]"].entries[-1]
step_simulation = pybamm.Simulation(model)
while time < end_time:
    step_solution = step_simulation.step(dt)
    print("Time", time)
    print(step_solution["Voltage [V]"].entries)
    time += dt
Time 0
[3.77047806 3.71250693]
Time 360
[3.77047806 3.71250693 3.68215218]
Time 720
[3.77047806 3.71250693 3.68215218 3.66125574]
Time 1080
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942]
Time 1440
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857]
Time 1800
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857
 3.59709451]
Time 2160
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857
 3.59709451 3.58821334]
Time 2520
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857
 3.59709451 3.58821334 3.58056055]
Time 2880
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857
 3.59709451 3.58821334 3.58056055 3.55158694]
Time 3240
[3.77047806 3.71250693 3.68215218 3.66125574 3.64330942 3.61166857
 3.59709451 3.58821334 3.58056055 3.55158694 3.16842636]

We can plot the voltages and see that the solutions are the same

[15]:
voltage = solution["Voltage [V]"].entries
step_voltage = step_solution["Voltage [V]"].entries
plt.figure()
plt.plot(solution["Time [h]"].entries, voltage, "b-", label="SPMe (continuous solve)")
plt.plot(
    step_solution["Time [h]"].entries, step_voltage, "ro", label="SPMe (stepped solve)"
)
plt.legend()
[15]:
<matplotlib.legend.Legend at 0x7f69964178d0>
_images/source_examples_notebooks_solution-data-and-processed-variables_28_1.png

As a final step, we will clean up the output files created by this notebook:

[ ]:
os.remove("outputs.csv")
os.remove("outputs.mat")
os.remove("outputs.pickle")

References#

The relevant papers for this notebook are:

[16]:
pybamm.print_citations()
[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.
[2] 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.
[3] 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.
[4] Valentin Sulzer, Scott G. Marquis, Robert Timms, Martin Robinson, and S. Jon Chapman. Python Battery Mathematical Modelling (PyBaMM). ECSarXiv. February, 2020. doi:10.1149/osf.io/67ckj.

Version: 24.1

Useful links: Project Home Page | Installation | Source Repository | Issue Tracker | Discussions

PyBaMM (Python Battery Mathematical Modelling) is an open-source battery simulation package written in Python. Our mission is to accelerate battery modelling research by providing open-source tools for multi-institutional, interdisciplinary collaboration. Broadly, PyBaMM consists of

  1. a framework for writing and solving systems of differential equations,

  2. a library of battery models and parameters, and

  3. specialized tools for simulating battery-specific experiments and visualizing the results.

Together, these enable flexible model definitions and fast battery simulations, allowing users to explore the effect of different battery designs and modeling assumptions under a variety of operating scenarios.

User Guide

The user guide is the best place to start learning PyBaMM. It contains an installation guide, an introduction to the main concepts and links to additional tutorials.

Examples

Examples and tutorials can be viewed on the GitHub examples page, which also provides a link to run them online through Google Colab.

API Documentation

The reference guide contains a detailed description of the functions, modules, and objects included in PyBaMM. The reference describes how the methods work and which parameters can be used.

Contributor’s Guide

Contributions to PyBaMM and its development are welcome! If you have ideas for features, bug fixes, models, spatial methods, or solvers, we would love to hear from you.