Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port lithium_ion_battery.m to Python #1263

Merged
merged 2 commits into from
Apr 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
"""
This example calculates the cell voltage of a lithium-ion battery at
given temperature, pressure, current, and range of state of charge (SOC).

The thermodynamics are based on a graphite anode and a LiCoO2 cathode,
modeled using the 'BinarySolutionTabulatedThermo' class.
Further required cell parameters are the electrolyte ionic resistance, the
stoichiometry ranges of the active materials (electrode balancing), and the
surface area of the active materials.

The functionality of this example is presented in greater detail in a jupyter
notebook as well as the reference (which also describes the derivation of the
'BinarySolutionTabulatedThermo' class):

Reference:
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

Requires: cantera >= 2.6.0, matplotlib >= 2.0
Keywords: surface chemistry, kinetics, electrochemistry, battery, plotting
"""

import cantera as ct
import numpy as np

# Parameters
samples = 101
soc = np.linspace(0., 1., samples) # [-] Input state of charge (0...1)
current = -1 # [A] Externally-applied current, negative for discharge
T = 293 # [K] Temperature
P = ct.one_atm # [Pa] Pressure

# Cell properties
input_file = "lithium_ion_battery.yaml" # Cantera input file name
R_electrolyte = 0.0384 # [Ohm] Electrolyte resistance
area_cathode = 1.1167 # [m^2] Cathode total active material surface area
area_anode = 0.7824 # [m^2] Anode total active material surface area

# Electrode balancing: The "balancing" of the electrodes relates the chemical
# composition (lithium mole fraction in the active materials) to the macroscopic
# cell-level state of charge.
X_Li_anode_0 = 0.01 # [-] anode Li mole fraction at SOC = 0
X_Li_anode_1 = 0.75 # [-] anode Li mole fraction at SOC = 100
X_Li_cathode_0 = 0.99 # [-] cathode Li mole fraction at SOC = 0
X_Li_cathode_1 = 0.49 # [-] cathode Li mole fraction at SOC = 100

# Calculate mole fractions from SOC
X_Li_anode = (X_Li_anode_1 - X_Li_anode_0) * soc + X_Li_anode_0
X_Li_cathode = (X_Li_cathode_0 - X_Li_cathode_1) * (1 - soc) + X_Li_cathode_1

# Import all Cantera phases
anode = ct.Solution(input_file, "anode")
cathode = ct.Solution(input_file, "cathode")
metal = ct.Solution(input_file, "electron")
electrolyte = ct.Solution(input_file, "electrolyte")
anode_int = ct.Interface(
input_file, "edge_anode_electrolyte", adjacent=[anode, metal, electrolyte])
cathode_int = ct.Interface(
input_file, "edge_cathode_electrolyte", adjacent=[cathode, metal, electrolyte])

# Set the temperatures and pressures of all phases
for phase in [anode, cathode, metal, electrolyte, anode_int, cathode_int]:
phase.TP = T, P


# Root finding function
def newton_solve(f, xstart, C=0.0):
"""
Solve f(x) = C by Newton iteration using initial guess *xstart*
"""
f0 = f(xstart) - C
x0 = xstart
dx = 1.0e-6
n = 0
while n < 200:
ff = f(x0 + dx) - C
dfdx = (ff - f0) / dx
step = - f0 / dfdx

# avoid taking steps too large
if abs(step) > 0.1:
step = 0.1 * step / abs(step)

x0 += step
emax = 0.00001 # 0.01 mV tolerance
if abs(f0) < emax and n > 8:
return x0
f0 = f(x0) - C
n += 1
raise Exception("no root!")


# This function returns the Cantera calculated anode current (in A)
def anode_current(phi_s, phi_l, X_Li_anode):
"""
Current from the anode as a function of anode potential relative to
electrolyte.
"""
# Set the active material mole fraction
anode.X = {"Li[anode]": X_Li_anode, "V[anode]": 1 - X_Li_anode}

# Set the electrode and electrolyte potential
metal.electric_potential = phi_s
electrolyte.electric_potential = phi_l

# Get the net reaction rate at the anode-side interface
# Reaction according to input file:
# Li+[electrolyte] + V[anode] + electron <=> Li[anode]
r = anode_int.net_rates_of_progress # [kmol/m2/s]

# Calculate the current. Should be negative for cell discharge.
return r * ct.faraday * area_anode


# This function returns the Cantera calculated cathode current (in A)
def cathode_current(phi_s, phi_l, X_Li_cathode):
"""
Current to the cathode as a function of cathode potential relative to electrolyte
"""
# Set the active material mole fractions
cathode.X = {"Li[cathode]": X_Li_cathode, "V[cathode]": 1 - X_Li_cathode}

# Set the electrode and electrolyte potential
metal.electric_potential = phi_s
electrolyte.electric_potential = phi_l

# Get the net reaction rate at the cathode-side interface
# Reaction according to input file:
# Li+[electrolyte] + V[cathode] + electron <=> Li[cathode]
r = cathode_int.net_rates_of_progress # [kmol/m2/s]

# Calculate the current. Should be negative for cell discharge.
return - r * ct.faraday * area_cathode


# Calculate cell voltage, separately for each entry of the input vectors
V_cell = np.zeros_like(soc)
phi_l_anode = 0
phi_s_cathode = 0
for i in range(samples):
# Set anode electrode potential to 0
phi_s_anode = 0

# Calculate anode electrolyte potential
phi_l_anode = newton_solve(
lambda E: anode_current(phi_s_anode, E, X_Li_anode[i]),
phi_l_anode, C=current)

# Calculate cathode electrolyte potential
phi_l_cathode = phi_l_anode + current * R_electrolyte

# Calculate cathode electrode potential
phi_s_cathode = newton_solve(
lambda E: cathode_current(E, phi_l_cathode, X_Li_cathode[i]),
phi_s_cathode, C=current)

# Calculate cell voltage
V_cell[i] = phi_s_cathode - phi_s_anode

try:
import matplotlib.pyplot as plt

# Plot the cell voltage, as a function of the state of charge
plt.plot(soc * 100, V_cell)
plt.xlabel("State of charge / %")
plt.ylabel("Cell voltage / V")
plt.show()

except ImportError:
print("Install matplotlib to plot the outputs")
100 changes: 100 additions & 0 deletions interfaces/cython/cantera/test/test_kinetics.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,106 @@ class TestSofcKinetics3(TestSofcKinetics):
_mech = "sofc.cti"


class TestLithiumIonBatteryKinetics(utilities.CanteraTest):
""" Test based on lithium_ion_battery.py """
_mech = "lithium_ion_battery.yaml"

def test_lithium_ion_battery(self):
mech = self._mech
samples = 11
soc = np.linspace(0., 1., samples) # [-] Input state of charge (0...1)
current = -1 # [A] Externally-applied current, negative for discharge
T = 293 # T in K
P = ct.one_atm
R_electrolyte = 0.0384 # [Ohm] Electrolyte resistance
area_cathode = 1.1167 # [m^2] Cathode total active material surface area
area_anode = 0.7824 # [m^2] Anode total active material surface area

# Calculate mole fractions from SOC
X_Li_anode_0 = 0.01 # [-] anode Li mole fraction at SOC = 0
X_Li_anode_1 = 0.75 # [-] anode Li mole fraction at SOC = 100
X_Li_cathode_0 = 0.99 # [-] cathode Li mole fraction at SOC = 0
X_Li_cathode_1 = 0.49 # [-] cathode Li mole fraction at SOC = 100
X_Li_anode = (X_Li_anode_1 - X_Li_anode_0) * soc + X_Li_anode_0
X_Li_cathode = (X_Li_cathode_0 - X_Li_cathode_1) * (1 - soc) + X_Li_cathode_1

def newton_solve(f, xstart, C=0.0):
""" Solve f(x) = C by Newton iteration. """
x0 = xstart
dx = 1.0e-6
n = 0
while True:
n += 1
f0 = f(x0) - C
x0 -= f0 / (f(x0 + dx) - C - f0) * dx
if n > 1000:
raise Exception('No convergence in Newton solve')
if abs(f0) < 0.00001:
return x0

# Phases
anode, cathode, metal, electrolyte = ct.import_phases(
mech, ["anode", "cathode", "electron", "electrolyte"])
anode_int = ct.Interface(
mech, "edge_anode_electrolyte", adjacent=[anode, metal, electrolyte])
cathode_int = ct.Interface(
mech, "edge_cathode_electrolyte", adjacent=[cathode, metal, electrolyte])

# initialization
for phase in [anode, cathode, metal, electrolyte, anode_int, cathode_int]:
phase.TP = T, P

# This function returns the Cantera calculated anode current
def anode_current(phi_s, phi_l, X_Li_anode):
# Set mole fraction and electrode and electrolyte potential
anode.X = {"Li[anode]": X_Li_anode, "V[anode]": 1 - X_Li_anode}
metal.electric_potential = phi_s
electrolyte.electric_potential = phi_l

# Calculate the current.
return ct.faraday * anode_int.net_rates_of_progress * area_anode

# This function returns the Cantera calculated cathode current
def cathode_current(phi_s, phi_l, X_Li_cathode):
# Set mole fraction and electrode and electrolyte potential
cathode.X = {"Li[cathode]": X_Li_cathode, "V[cathode]": 1 - X_Li_cathode}
metal.electric_potential = phi_s
electrolyte.electric_potential = phi_l

# Calculate the current. Should be negative for cell discharge.
return - ct.faraday * cathode_int.net_rates_of_progress * area_cathode

# Calculate cell voltage, separately for each entry of the input vectors
data = []
phi_l_anode = 0
phi_s_cathode = 0
for i in range(samples):
# Calculate anode electrolyte potential
phi_s_anode = 0
phi_l_anode = newton_solve(
lambda E: anode_current(phi_s_anode, E, X_Li_anode[i]),
phi_l_anode, C=current)

# Calculate cathode electrode potential
phi_l_cathode = phi_l_anode + current * R_electrolyte
phi_s_cathode = newton_solve(
lambda E: cathode_current(E, phi_l_cathode, X_Li_cathode[i]),
phi_s_cathode, C=current)

# Calculate cell voltage
data.append(phi_s_cathode - phi_s_anode)

data = np.array(data).ravel()
ref = np.genfromtxt(self.test_data_path / "lithium-ion-battery-test.csv")
assert np.allclose(data, ref, rtol=1e-7)


@pytest.mark.usefixtures("allow_deprecated")
class TestLithiumIonBatteryKinetics2(TestLithiumIonBatteryKinetics):
""" Test using legacy framework; included to retain coverage """
_mech = "lithium_ion_battery.cti"


class TestDuplicateReactions(utilities.CanteraTest):
infile = 'duplicate-reactions.yaml'

Expand Down
11 changes: 11 additions & 0 deletions test/data/lithium-ion-battery-test.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
2.65335507
3.57418843
3.64605784
3.68174416
3.70084855
3.71586159
3.75736585
3.81434722
3.868883
3.91854252
4.01938813