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.

EIS Simulation#

Electrochemical Impedance Spectroscopy (EIS) is a technique for characterising battery dynamics by applying a small sinusoidal perturbation at different frequencies and measuring the resulting impedance. PyBaMM provides EISSimulation, a frequency-domain solver that computes impedance spectra directly — much faster than running full time-domain simulations at each frequency.

In this notebook we will:

  1. Run a basic EIS simulation and inspect the solution

  2. Access and plot impedance data (Nyquist plots)

  3. Compare impedance at different states of charge (SOC)

  4. Compare impedance across different models

  5. Save EIS data to file

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

import pybamm

Running a basic EIS simulation#

To run an EIS simulation we need a model with the "surface form" option set to "differential". We pass the model to pybamm.EISSimulation, define the frequencies of interest, and call solve:

[2]:
model = pybamm.lithium_ion.SPM(options={"surface form": "differential"})
eis_sim = pybamm.EISSimulation(model)

frequencies = np.logspace(-4, 4, 30)  # 10^-4 to 10^4 Hz
result = eis_sim.solve(frequencies)
result
[2]:
<pybamm.solvers.solution.EISSolution at 0x11f379fd0>

The solve method returns an EISSolution object. Like the time-series Solution, it is also a SolutionBase instance, stores timing information, and is accessible via the solution property on the simulation — the same pattern as Simulation.solution:

[3]:
print(f"isinstance EISSolution:  {isinstance(result, pybamm.EISSolution)}")
print(f"isinstance SolutionBase: {isinstance(result, pybamm.SolutionBase)}")
print(f"Set-up time: {result.set_up_time}")
print(f"Solve time:  {result.solve_time}")
print(f"Same as eis_sim.solution: {result is eis_sim.solution}")
isinstance EISSolution:  True
isinstance SolutionBase: True
Set-up time: 8.201 ms
Solve time:  61.040 ms
Same as eis_sim.solution: True

Accessing solution data#

The EISSolution supports dict-style access, just like the time-series Solution. You can use result["Variable name"] to get data arrays:

[4]:
# Dict-style access — same API as Solution
print("Frequencies (Hz):\n", result["Frequency [Hz]"])
print("\nComplex impedance:\n", result["Impedance [Ohm]"])
print("\nReal part:\n", result["Z_re [Ohm]"])
print("\nImaginary part:\n", result["Z_im [Ohm]"])

# Convenience properties also available
print("\nVia property:", result.impedance[:3], "...")
Frequencies (Hz):
 [1.00000000e-04 1.88739182e-04 3.56224789e-04 6.72335754e-04
 1.26896100e-03 2.39502662e-03 4.52035366e-03 8.53167852e-03
 1.61026203e-02 3.03919538e-02 5.73615251e-02 1.08263673e-01
 2.04335972e-01 3.85662042e-01 7.27895384e-01 1.37382380e+00
 2.59294380e+00 4.89390092e+00 9.23670857e+00 1.74332882e+01
 3.29034456e+01 6.21016942e+01 1.17210230e+02 2.21221629e+02
 4.17531894e+02 7.88046282e+02 1.48735211e+03 2.80721620e+03
 5.29831691e+03 1.00000000e+04]

Complex impedance:
 [1.00563333e-01-0.18918861j 1.00541067e-01-0.10051692j
 1.00465109e-01-0.05376932j 1.00228638e-01-0.02937809j
 9.96259133e-02-0.01692566j 9.84603512e-02-0.01065627j
 9.68969090e-02-0.00720148j 9.54832027e-02-0.00501148j
 9.43678954e-02-0.00360485j 9.34972143e-02-0.00270308j
 9.28238758e-02-0.0021521j  9.23232216e-02-0.00190189j
 9.19865376e-02-0.00202381j 9.17769471e-02-0.00275431j
 9.15614093e-02-0.00454072j 9.10077709e-02-0.00815556j
 8.91552787e-02-0.01487124j 8.31767712e-02-0.02598032j
 6.74091734e-02-0.03910917j 4.13844737e-02-0.04305452j
 1.97116176e-02-0.03311025j 9.53267228e-03-0.02097056j
 5.33499888e-03-0.01308254j 2.76220826e-03-0.00841996j
 1.08428400e-03-0.00512891j 3.45241881e-04-0.00288328j
 1.00777321e-04-0.00155718j 2.86117955e-05-0.00082968j
 8.05768263e-06-0.00044029j 2.26400268e-06-0.00023339j]

Real part:
 [1.00563333e-01 1.00541067e-01 1.00465109e-01 1.00228638e-01
 9.96259133e-02 9.84603512e-02 9.68969090e-02 9.54832027e-02
 9.43678954e-02 9.34972143e-02 9.28238758e-02 9.23232216e-02
 9.19865376e-02 9.17769471e-02 9.15614093e-02 9.10077709e-02
 8.91552787e-02 8.31767712e-02 6.74091734e-02 4.13844737e-02
 1.97116176e-02 9.53267228e-03 5.33499888e-03 2.76220826e-03
 1.08428400e-03 3.45241881e-04 1.00777321e-04 2.86117955e-05
 8.05768263e-06 2.26400268e-06]

Imaginary part:
 [-0.18918861 -0.10051692 -0.05376932 -0.02937809 -0.01692566 -0.01065627
 -0.00720148 -0.00501148 -0.00360485 -0.00270308 -0.0021521  -0.00190189
 -0.00202381 -0.00275431 -0.00454072 -0.00815556 -0.01487124 -0.02598032
 -0.03910917 -0.04305452 -0.03311025 -0.02097056 -0.01308254 -0.00841996
 -0.00512891 -0.00288328 -0.00155718 -0.00082968 -0.00044029 -0.00023339]

Via property: [0.10056333-0.18918861j 0.10054107-0.10051692j 0.10046511-0.05376932j] ...

Nyquist plots#

The standard way to visualise EIS data is a Nyquist plot, which shows the real part of impedance on the x-axis and the negative imaginary part on the y-axis. EISSolution has a built-in method for this:

[5]:
result.nyquist_plot(show_plot=False)
plt.show()
../../../../_images/source_examples_notebooks_simulations_and_experiments_eis-simulation_9_0.png

You can also build custom plots directly from the solution data:

[6]:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

# Nyquist plot from dict-style access
axes[0].plot(result["Z_re [Ohm]"], -result["Z_im [Ohm]"], "o-")
axes[0].set_xlabel(r"$Z_\mathrm{Re}$ [Ohm]")
axes[0].set_ylabel(r"$-Z_\mathrm{Im}$ [Ohm]")
axes[0].set_title("Nyquist plot")
axes[0].set_aspect("equal")

# Bode-style: magnitude vs frequency
Z = result["Impedance [Ohm]"]
axes[1].loglog(result["Frequency [Hz]"], np.abs(Z), "o-")
axes[1].set_xlabel("Frequency [Hz]")
axes[1].set_ylabel("|Z| [Ohm]")
axes[1].set_title("Bode magnitude")

fig.tight_layout()
plt.show()
../../../../_images/source_examples_notebooks_simulations_and_experiments_eis-simulation_11_0.png

SOC sweep#

Battery impedance changes with the state of charge. We can sweep SOC by passing initial_soc to solve:

[7]:
model = pybamm.lithium_ion.SPM(options={"surface form": "differential"})
parameter_values = pybamm.ParameterValues("Chen2020")
eis_sim = pybamm.EISSimulation(model, parameter_values=parameter_values)

frequencies = np.logspace(-4, 4, 30)
_, ax = plt.subplots()

for soc in [0.2, 0.5, 0.9]:
    result = eis_sim.solve(frequencies, initial_soc=soc)
    ax.plot(
        result["Z_re [Ohm]"],
        -result["Z_im [Ohm]"],
        "o",
        label=f"SOC = {soc}",
    )

ax.set_xlabel(r"$Z_\mathrm{Re}$ [Ohm]")
ax.set_ylabel(r"$-Z_\mathrm{Im}$ [Ohm]")
ax.set_aspect("equal")
ax.legend()
plt.show()
../../../../_images/source_examples_notebooks_simulations_and_experiments_eis-simulation_13_0.png

Comparing models#

We can compare the impedance predicted by different electrochemical models. All models must use "surface form": "differential" for EIS:

[8]:
models = {
    "SPM": pybamm.lithium_ion.SPM(options={"surface form": "differential"}),
    "SPMe": pybamm.lithium_ion.SPMe(options={"surface form": "differential"}),
    "DFN": pybamm.lithium_ion.DFN(options={"surface form": "differential"}),
}

frequencies = np.logspace(-4, 4, 30)
_, ax = plt.subplots()

for name, model in models.items():
    eis_sim = pybamm.EISSimulation(model)
    result = eis_sim.solve(frequencies)
    ax.plot(
        result["Z_re [Ohm]"],
        -result["Z_im [Ohm]"],
        "o",
        label=name,
    )

ax.set_xlabel(r"$Z_\mathrm{Re}$ [Ohm]")
ax.set_ylabel(r"$-Z_\mathrm{Im}$ [Ohm]")
ax.set_aspect("equal")
ax.legend()
plt.show()
../../../../_images/source_examples_notebooks_simulations_and_experiments_eis-simulation_15_0.png

Saving EIS data#

The EISSolution supports saving data in several formats. The data property gives a dict-like view of all variables (same interface as Solution.data):

[9]:
# .data returns a dict-like view, .get_data_dict() returns a plain dict for export
print("Keys in data:", list(result.data.keys()))
result.get_data_dict()
Keys in data: ['Frequency [Hz]', 'Impedance [Ohm]', 'Z_re [Ohm]', 'Z_im [Ohm]']
[9]:
{'Frequency [Hz]': array([1.00000000e-04, 1.88739182e-04, 3.56224789e-04, 6.72335754e-04,
        1.26896100e-03, 2.39502662e-03, 4.52035366e-03, 8.53167852e-03,
        1.61026203e-02, 3.03919538e-02, 5.73615251e-02, 1.08263673e-01,
        2.04335972e-01, 3.85662042e-01, 7.27895384e-01, 1.37382380e+00,
        2.59294380e+00, 4.89390092e+00, 9.23670857e+00, 1.74332882e+01,
        3.29034456e+01, 6.21016942e+01, 1.17210230e+02, 2.21221629e+02,
        4.17531894e+02, 7.88046282e+02, 1.48735211e+03, 2.80721620e+03,
        5.29831691e+03, 1.00000000e+04]),
 'Z_re [Ohm]': array([0.11670175, 0.11623164, 0.11578165, 0.11531082, 0.11442335,
        0.11262009, 0.10968718, 0.10644623, 0.10408134, 0.1026827 ,
        0.10180478, 0.10121347, 0.10083789, 0.10061397, 0.1003949 ,
        0.09984292, 0.09799588, 0.09202418, 0.0762611 , 0.05023747,
        0.02856141, 0.01836863, 0.01412147, 0.01138017, 0.00918837,
        0.0072826 , 0.00549163, 0.00416697, 0.00322355, 0.00252515]),
 'Z_im [Ohm]': array([-0.1899683 , -0.10144159, -0.05469772, -0.03043038, -0.01840321,
        -0.01288427, -0.01016257, -0.00787576, -0.00561705, -0.00391433,
        -0.00285538, -0.00230256, -0.00224554, -0.00287343, -0.00460296,
        -0.00818769, -0.01489119, -0.02600462, -0.03915515, -0.04314468,
        -0.03328298, -0.02129734, -0.01369429, -0.00953687, -0.00701784,
        -0.00551958, -0.00431914, -0.00323463, -0.00241477, -0.00180293])}

Save to CSV or JSON:

[10]:
result.save_data("eis_data.csv", to_format="csv")
result.save_data("eis_data.json", to_format="json")

Save the entire solution object using pickle (can be reloaded with pybamm.load):

[11]:
result.save("eis_solution.pkl")
loaded = pybamm.load("eis_solution.pkl")
print("Loaded frequencies:", loaded.frequencies[:5], "...")
print("Loaded impedance: ", loaded.impedance[:5], "...")
Loaded frequencies: [0.0001     0.00018874 0.00035622 0.00067234 0.00126896] ...
Loaded impedance:  [0.11670175-0.1899683j  0.11623164-0.10144159j 0.11578165-0.05469772j
 0.11531082-0.03043038j 0.11442335-0.01840321j] ...

Clean up the files we saved:

[12]:
import os

os.remove("eis_data.csv")
os.remove("eis_data.json")
os.remove("eis_solution.pkl")

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] Von DAG Bruggeman. Berechnung verschiedener physikalischer konstanten von heterogenen substanzen. i. dielektrizitätskonstanten und leitfähigkeiten der mischkörper aus isotropen substanzen. Annalen der physik, 416(7):636–664, 1935.
[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] Noël Hallemans, Nicola E Courtier, Colin P Please, Brady Planden, Rishit Dhoot, Robert Timms, S Jon Chapman, David Howey, and Stephen R Duncan. Physics-based battery model parametrisation from impedance data. J. Electrochem. Soc., 172(6):060507, jun 2025.
[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] Alan C. Hindmarsh. The PVODE and IDA algorithms. Technical Report, Lawrence Livermore National Lab., CA (US), 2000. doi:10.2172/802599.
[8] Alan C. Hindmarsh, Peter N. Brown, Keith E. Grant, Steven L. Lee, Radu Serban, Dan E. Shumaker, and Carol S. Woodward. SUNDIALS: Suite of nonlinear and differential/algebraic equation solvers. ACM Transactions on Mathematical Software (TOMS), 31(3):363–396, 2005. doi:10.1145/1089014.1089020.
[9] 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.
[10] 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.
[11] 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.
[12] 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.
[13] Andrew Weng, Jason B Siegel, and Anna Stefanopoulou. Differential voltage analysis for battery manufacturing process control. arXiv preprint arXiv:2303.07088, 2023.