{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Lithium Ion Battery\n",
"In this example, we will illustrate how to calculate the open circuit voltage (voltage when the external applied current is 0) for a lithium ion battery as a function of anode and cathode lithium content."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The thermodynamics are based on a graphite anode and a LiCoO2 cathode (the typical/standard active materials for commercial batteries, as of 2019), and are modeled using the 'BinarySolutionTabulatedThermo' class.\n",
"\n",
"For the sake of simplicity, we're going to assume that the anode and cathode capacities are perfectly balanced (i.e. if the cathode lithium content is X percent of its max possible (i.e. its capacity), then we will assume that the anode is at 1-X percent. Without loss of generality, we will define the anode composition.\n",
"\n",
"The routine below returns the cell voltage (in Volt) of a lithium-ion cell for a given cell current and active material lithium stoichiometries.\n",
"\n",
"Note that the function 'E_cell' below has even greater capabilities than what we use, here. It calculates the steady state cell voltage, at a given composition and cell current, for a given electrolyte ionic resistance. This functionality is presented in greater detail in the reference (which also describes the derivation of the BinarySolutionTabulatedThermo class): \n",
"\n",
">M. Mayur, S. C. DeCaluwe, B. L. Kee, W. G. Bessler, “Modeling and simulation of the thermodynamics of lithium-ion battery intercalation materials in the open-source software Cantera,” _Electrochim. Acta_ 323, 134797 (2019), https://doi.org/10.1016/j.electacta.2019.134797\n",
"\n",
"In the future, this example may be developed further to demonstrate simulating charge-discharge of the lithium ion battery.\n",
"\n",
"\n",
"Other than the typical Cantera dependencies, plotting functions require that you have matplotlib installed, and finding the electrode potentials uses Scipy's fsolve. See https://matplotlib.org/ and https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.fsolve.html for additional info.\n",
"\n",
"The open circuit (i.e. equilibrium) voltage, here, is calculated via two means: kinetically and thermodynamically.\n",
"\n",
"The system here, can be thought of as consisting of two particles--anode and cathode--connected by a liquid electrolyte:\n",
"\n",
"\n",
"\n",
"### Kinetic equilibrium calculations\n",
"In the kinetic equilibrium problem, steady state is achieved when the faradaic current at each electrode (anode and cathode) surface and the ionic current through the electrolyte are all zero. There are, essentially, four relevant electric potentials which must be determined:\n",
"\n",
"- $\\phi_{\\rm anode}$: electric potential of graphite anode.\n",
"- $\\phi_{\\rm elyte,\\, anode}$: electric potential of electrolyte at anode interface.\n",
"- $\\phi_{\\rm elyte,\\, cathode}$: electric potential of electrolyte at cathode interface.\n",
"- $\\phi_{\\rm cathode}$: electric potential of LCO cathode.\n",
"\n",
"Setting one of these four to the reference potential of zero (because it is the difference in electric potential which drives currents, the actual potential values are irrelevant. Let's assume the anode electric potential is zero), there is only one distribution of electric potentials across the cell such that the current is invariant across the cell. I.e. we want the potentials such that:\n",
"\n",
"\\begin{equation}\n",
"i_{\\rm Far,\\, anode} = i_{\\rm ionic} = i_{\\rm Far,\\,cathode}= i_{\\rm app}\n",
"\\end{equation}\n",
"\n",
"where $i_{\\rm app}$ is the user input for the applied current. For this example, we assume an applied current of 0, to calculate the equilibrium voltage.\n",
"\n",
"#### Faradaic current\n",
"The Faradaic current for this model is calculated using Butler-Volmer kinetics. For a Li-ion battery, this is:\n",
"\n",
"\\begin{equation}\n",
"i = S_{\\rm elde}i_\\circ\\left[\\exp\\left(\\frac{F\\beta\\eta}{RT}\\right) - \\exp\\left(-\\frac{F(1-\\beta)\\eta}{RT}\\right) \\right]\n",
"\\end{equation}\n",
"\n",
"where $S_{\\rm elde}$ is the specific surface area of the electrode in question, $F$ is Faraday's constant, $\\beta$ is the charge-transfer symmetry parameter, $R$ the universal gas constant, $T$ the temperature, and $\\eta$ the overpotential, which is the electric potential difference between the electrode and electrolyte, $\\Delta \\phi = \\phi_{\\rm elde} - \\phi_{\\rm elyte}$, relative to that at equilibrium, $\\Delta \\phi_{\\rm eq}$:\n",
"\n",
"\\begin{equation}\n",
"\\eta = \\Delta \\phi - \\Delta \\phi_{\\rm eq}\n",
"\\end{equation}\n",
"\n",
"$i_\\circ$ is known as the \"exchange current density,\" which is equal to the rate of the forward and reverse current at equilibrium (which are equal). $i_\\circ$ and $\\beta$ are provided as user inputs in the cti file. At any particular state, (user-specified electric potentials, pressure, temperature, and chemical compositions), Cantera calculates $\\eta$ as part of the routine to evaluate reaction rates of progress $\\left(\\dot{q} = \\frac{i_{\\rm Far}}{F}\\right)$. The user simply sets the state values mentioned above.\n",
"\n",
"#### Ionic current\n",
"The electrolyte is modeled as a resistor with user-defined ionic resistance $R_{\\rm io}$, and hence the ionic current is calculated as:\n",
"\n",
"\\begin{equation}\n",
"i_{\\rm ionic} = \\frac{\\phi_{\\rm elyte,\\,ca} - \\phi_{\\rm elyte,\\,an}}{R_{\\rm io}}\n",
"\\end{equation}\n",
"\n",
"where positive current is defined as delivering Li$^+$ to the anode interface. Given $i_{\\rm app}$, this equation can be inverted, to calculate the electric potential of the electrolyte at the cathode interface, relative to that at the anode interface:\n",
"\n",
"\\begin{equation}\n",
"\\phi_{\\rm elyte,\\,ca} = \\phi_{\\rm elyte,\\,an} + R_{\\rm io}i_{\\rm app} \n",
"\\end{equation}\n",
"\n",
"Again: in this example, $i_{\\rm app} = 0$ and hence the two electric potential values in the electrolyte are equal.\n",
"\n",
"#### Numerical routine\n",
"For the kinetic routine, there are three processes to determine the cell voltage $\\phi_{\\rm cathode} - \\phi_{\\rm anode}$ which corresponds to the user-provided $i_{\\rm app}$:\n",
"1. Determine the $\\phi_{\\rm elyte,\\,anode}$ value which corresponds to $i_{\\rm app}$, given $X_{\\rm Li, anode}$, the percentage of Li in the anode active material.\n",
"2. Determine $\\phi_{\\rm elyte,\\,cathode}$, given $\\phi_{\\rm elyte,\\,anode}$ and $i_{\\rm app}$.\n",
"3. Determine the $\\phi_{\\rm cathode}$ which corresponds to $i_{\\rm app}$, given $\\phi_{\\rm elyte,\\,cathode}$ and $X_{\\rm Li, anode}$, the percentage of Li in the anode active material.\n",
"\n",
"The routines below are written generally such that an interested user may set $i_{\\rm app}$ to any value of interest.\n",
"\n",
"#### Import necessary packages:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Runnning Cantera version: 3.0.0\n"
]
}
],
"source": [
"import cantera as ct\n",
"\n",
"print(f\"Runnning Cantera version: {ct.__version__}\")"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"from scipy.optimize import fsolve\n",
"\n",
"# Used for timing our calculations:\n",
"import time\n",
"\n",
"# Plotting:\n",
"%matplotlib inline\n",
"%config InlineBackend.figure_formats = [\"svg\"]\n",
"import matplotlib.pyplot as plt\n",
"\n",
"plt.rcParams[\"axes.labelsize\"] = 16\n",
"plt.rcParams[\"xtick.labelsize\"] = 12\n",
"plt.rcParams[\"ytick.labelsize\"] = 12\n",
"plt.rcParams[\"figure.autolayout\"] = True\n",
"plt.rcParams[\"figure.dpi\"] = 120"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define the phases\n",
"\n",
"The phase thermodynamics are defined according to experimentally-measured open circuit voltage values, as desscribed in the reference provided above:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"input_file = \"../data/lithium_ion_battery.yaml\"\n",
"anode = ct.Solution(input_file, \"anode\")\n",
"cathode = ct.Solution(input_file, \"cathode\")\n",
"# The 'elde' electrode phase is needed as a source/sink for electrons:\n",
"elde = ct.Solution(input_file, \"electron\")\n",
"elyte = ct.Solution(input_file, \"electrolyte\")\n",
"anode_interface = ct.Interface(\n",
" input_file, \"edge_anode_electrolyte\", [anode, elde, elyte]\n",
")\n",
"cathode_interface = ct.Interface(\n",
" input_file, \"edge_cathode_electrolyte\", [cathode, elde, elyte]\n",
");"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Define battery conditions : temperature, pressure, stoichiometry, electrolyte resistance:\n",
"\n",
"Inputs are:\n",
"- Stoichiometries X_Li_ca and X_Li_an [-] (can be vectors)\n",
"- Temperature T [K]\n",
"- Pressure P [Pa]\n",
"- Externally-applied current i_app [A]\n",
"- Electrolyte resistance R_elyte [Ohm]\n",
"- Anode total surface area S_an [m^2]\n",
"- Cathode total surface area S_ca [m^2]\n"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Array of lithium mole fractions in the anode:\n",
"X_Li_an = np.arange(0.005, 0.995, 0.02)\n",
"# Assume that the cathode and anode capacities are balanced:\n",
"X_Li_ca = 1.0 - X_Li_an\n",
"\n",
"# I_app = 0: Open circuit\n",
"I_app = 0.0\n",
"\n",
"# At zero current, electrolyte resistance is irrelevant:\n",
"R_elyte = 0.0\n",
"\n",
"# Temperature and pressure\n",
"T = 300 # K\n",
"P = ct.one_atm\n",
"\n",
"F = ct.faraday\n",
"\n",
"# [m^2] Cathode total active material surface area\n",
"S_ca = 1.1167\n",
"\n",
"S_an = 0.7824; # [m^2] Anode total active material surface area"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Set phase temperatures and pressures:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"phases = [anode, elde, elyte, cathode, anode_interface, cathode_interface]\n",
"for ph in phases:\n",
" ph.TP = T, P"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Helper Functions:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"def anode_curr(phi_l, I_app, phi_s, X_Li_an):\n",
"\n",
" # Set the active material mole fraction\n",
" anode.X = f\"Li[anode]:{X_Li_an}, V[anode]:{1 - X_Li_an}\"\n",
"\n",
" # Set the electrode and electrolyte potential\n",
" elde.electric_potential = phi_s\n",
" elyte.electric_potential = phi_l\n",
"\n",
" # Get the net product rate of electrons in the anode (per m2^ interface)\n",
" r_elec = anode_interface.get_net_production_rates(elde)\n",
"\n",
" anode_current = r_elec * ct.faraday * S_an\n",
" diff = I_app + anode_current\n",
"\n",
" return diff\n",
"\n",
"\n",
"def cathode_curr(phi_s, I_app, phi_l, X_Li_ca):\n",
"\n",
" # Set the active material mole fractions\n",
" cathode.X = f\"Li[cathode]:{X_Li_ca}, V[cathode]:{1 - X_Li_ca}\"\n",
"\n",
" # Set the electrode and electrolyte potential\n",
" elde.electric_potential = phi_s\n",
" elyte.electric_potential = phi_l\n",
"\n",
" # Get the net product rate of electrons in the cathode (per m2^ interface)\n",
" r_elec = cathode_interface.get_net_production_rates(elde)\n",
"\n",
" cathode_current = r_elec * ct.faraday * S_an\n",
" diff = I_app - cathode_current\n",
"\n",
" return diff"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Run the calculations for all stoichiometries:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"49 cell voltages calculated in 1.84e-01 seconds.\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"C:\\Users\\Niclas\\AppData\\Local\\Temp\\ipykernel_21244\\462783244.py:8: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n",
" elyte.electric_potential = phi_l\n",
"C:\\Users\\Niclas\\AppData\\Local\\Temp\\ipykernel_21244\\462783244.py:25: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n",
" elde.electric_potential = phi_s\n",
"C:\\Users\\Niclas\\AppData\\Local\\Temp\\ipykernel_21244\\462783244.py:26: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n",
" elyte.electric_potential = phi_l\n",
"C:\\Users\\Niclas\\AppData\\Local\\Temp\\ipykernel_21244\\3503167752.py:21: DeprecationWarning: Conversion of an array with ndim > 0 to a scalar is deprecated, and will error in future. Ensure you extract a single element from your array before performing this operation. (Deprecated NumPy 1.25.)\n",
" E_cell_kin[i] = phi_s_ca - phi_s_an\n"
]
}
],
"source": [
"# Tic\n",
"t0 = time.time()\n",
"\n",
"# Initialize array of OCVs:\n",
"E_cell_kin = np.zeros_like(X_Li_ca)\n",
"\n",
"for i, X_an in enumerate(X_Li_an):\n",
" # Set anode electrode potential to 0:\n",
" phi_s_an = 0\n",
" E_init = 3.0\n",
"\n",
" phi_l_an = fsolve(anode_curr, E_init, args=(I_app, phi_s_an, X_an))\n",
"\n",
" # Calculate electrolyte potential at cathode interface:\n",
" phi_l_ca = phi_l_an + I_app * R_elyte\n",
"\n",
" # Calculate cathode electrode potential\n",
" phi_s_ca = fsolve(cathode_curr, E_init, args=(I_app, phi_l_ca, X_Li_ca[i]))\n",
"\n",
" # Calculate cell voltage\n",
" E_cell_kin[i] = phi_s_ca - phi_s_an\n",
"\n",
"\n",
"# Toc\n",
"t1 = time.time()\n",
"print(f\"{i:d} cell voltages calculated in {t1 - t0:3.2e} seconds.\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Plot cell voltage, as a function of the cathode stoichiometry:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"data": {
"image/svg+xml": [
"\n",
"\n",
"\n"
],
"text/plain": [
"