Skip to content

Instantly share code, notes, and snippets.

@asilichenko
Last active July 24, 2024 20:06
Show Gist options
  • Save asilichenko/342613acfcdbc697a07831bb4a82d4ea to your computer and use it in GitHub Desktop.
Save asilichenko/342613acfcdbc697a07831bb4a82d4ea to your computer and use it in GitHub Desktop.
Li-Ion Open Circuit Voltage (OCV) by Depth of Discharge (DOD) lookup table generator
# MIT License
#
# Copyright (c) 2024 Oleksii Sylichenko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import CubicSpline
OCV_MIN = 2500
OCV_MAX = 4200
OCV_RANGE_SIZE = OCV_MAX - OCV_MIN
"""
Control points were taken as coordinates from the image:
https://www.researchgate.net/figure/OCV-model-of-a-Li-ion-battery_fig3_363656090
"""
CONTROL_POINTS = [
(29.1879, 20.1961),
(30.5088, 66.3425),
(31.2498, 76.2909),
(31.9130, 83.2655),
(32.5299, 87.3595),
(33.9846, 95.5851),
(36.1870, 105.5144),
(38.0097, 112.8053),
(41.9811, 124.2968),
(44.4734, 129.1133),
(47.0482, 132.2671),
(51.5661, 134.4481),
(56.9215, 135.8484),
(63.2201, 138.8181),
(70.1136, 141.7001),
(81.6601, 147.8047),
(89.0599, 151.5826),
(96.6546, 155.3993),
(119.4749, 162.9783),
(132.2495, 166.6393),
(143.3493, 169.7940),
(155.2145, 174.1227),
(163.6854, 177.6279),
(181.7935, 185.6167),
(186.0398, 187.4198),
(192.5039, 190.8367),
(203.4541, 197.6201),
(212.9636, 200.4438),
(232.1327, 207.2311),
(239.3768, 211.0869),
(245.9977, 215.2542),
(250.6704, 217.5304),
(254.3010, 219.1568),
(262.9939, 222.1292),
(278.5022, 223.980),
(292.8864, 227.0085),
(306.1642, 238.8362)
]
"""
These lookup points were chosen to align with
Texas Instruments' “Theory and Implementation of Impedance Track”
See: https://www.ti.com/lit/an/slua364b/slua364b.pdf
"""
LOOKUP_DOD_POINTS = [
0, # 0 %
1 / 9, # 11.11 %
2 / 9, # 22.22 %
3 / 9, # 33.33 %
4 / 9, # 44.44 %
5 / 9, # 55.55 %
6 / 9, # 66.66 %
7 / 9, # 77.77 %
7 / 9 + (2 / 9 / 7), # 80.95 %
7 / 9 + (2 / 9 / 7) * 2, # 84.13 %
7 / 9 + (2 / 9 / 7) * 3, # 87.30 %
7 / 9 + (2 / 9 / 7) * 4, # 90.48 %
7 / 9 + (2 / 9 / 7) * 5, # 93.65 %
7 / 9 + (2 / 9 / 7) * 6, # 96.83 %
7 / 9 + (2 / 9 / 7) * 7, # 100 %
]
LOOKUP_DOD_1PERCENT_STEP = np.arange(0, 101, 1) / 100
def make_spline(control_points, num=1000):
"""Creates a spline that passes through the given control points with a specified number of points in the spline.
:param control_points: Nd-array of control points.
:param num: The number of points to generate for the spline.
:return: Two nd-arrays - an array of x coordinates and an array of y coordinates for the spline.
"""
x_points = control_points[:, 0]
y_points = control_points[:, 1]
spline = CubicSpline(x_points, y_points)
# create `num` evenly spaced points over the range of x-coordinates
spline_x = np.linspace(min(x_points), max(x_points), num)
spline_y = spline(spline_x)
return spline_x, spline_y
def coordinates_to_values(x_arr, y_arr):
"""Maps x, y coordinates to DOD, Voltage values.
:param x_arr: Nd-array of x-coordinates.
:param y_arr: Nd-array of y-coordinates.
:return: Two nd-arrays - dod and voltage values.
"""
x_min = min(x_arr)
x_max = max(x_arr)
x_range_size = x_max - x_min
y_min = min(y_arr)
y_max = max(y_arr)
y_range_size = y_max - y_min
dod_arr = []
voltage_arr = []
for x, y in zip(x_arr, y_arr):
dod = x - x_min
dod /= x_range_size
dod = 1 - dod # SOC -> DOD
dod_arr.append(dod)
voltage = y - y_min
voltage /= y_range_size
voltage *= OCV_RANGE_SIZE
voltage += OCV_MIN
voltage_arr.append(voltage)
return dod_arr, voltage_arr
def make_lookup(dod_arr, voltage_arr, dod_points):
spline = CubicSpline(dod_arr, voltage_arr)
lookup_dod = np.array(dod_points)
lookup_voltage = spline(lookup_dod)
return lookup_dod, lookup_voltage
def configure_plot(x_arr, y_arr, x_label, y_label):
plt.figure()
plt.xlim(min(x_arr), max(x_arr))
plt.ylim(min(y_arr), max(y_arr))
plt.xlabel(x_label)
plt.ylabel(y_label)
plt.tight_layout()
def plot_coordinates(control_points, spline_x, spline_y):
"""Plots control points and a spline curve over them.
:param control_points: Nd-array of coordinates for the control points.
:param spline_x: An array of x coordinates for the spline curve.
:param spline_y: An array of y coordinates for the spline curve.
"""
control_points_x = control_points[:, 0]
control_points_y = control_points[:, 1]
configure_plot(control_points_x, control_points_y, 'SOC (x)', 'Voltage (y)')
plt.plot(spline_x, spline_y, 'r-', label='Spline')
plt.plot(control_points_x, control_points_y,
linestyle='none', marker='o',
markerfacecolor='none', markeredgecolor='black',
label='Control Points')
plt.legend()
def plot_values(dod_arr, voltage_arr):
"""Plots an OCV by DOD curve.
DOD values should be in ascending order.
:param dod_arr: An array of DOD values.
:param voltage_arr: An array of Voltage values.
"""
configure_plot(dod_arr, voltage_arr, 'DOD', 'Voltage (mV)')
plt.plot(dod_arr, voltage_arr, '-', label='OCV')
plt.legend()
def plot_lookup(dod_arr, voltage_arr, lookup_dod, lookup_voltage, color='green', label='Lookup OCV'):
"""Plots an OCV by DOD curve in the background,
and lookup points with an approximation curve on top.
:param dod_arr: Nd-array of DOD values of a spline.
:param voltage_arr: Nd-array of OCV values of a spline.
:param lookup_dod: Nd-array of lookup DOD values.
:param lookup_voltage: Nd-array of lookup OCV values.
:param color: Color of the series.
:param label: Label for lookup series.
"""
configure_plot(dod_arr, voltage_arr, 'DOD', 'Voltage (mV)')
plt.plot(dod_arr, voltage_arr, '--', color='grey', label='OCV')
plt.plot(
lookup_dod, lookup_voltage, color=color, label=label,
marker='o', markerfacecolor='none', markeredgecolor='black'
)
plt.legend()
def print_values(dod_arr, voltage_arr, fmt=''):
separator = '\t' if '\t' in fmt else ';'
print(f'DOD0{separator}OCV')
for dod, voltage in zip(dod_arr, voltage_arr):
if 'int' in fmt:
dod = int(dod * 100)
voltage = round(voltage)
line = f'{dod}{separator}{voltage}'
if ',' in fmt:
line = line.replace('.', ',')
print(line)
if __name__ == '__main__':
_control_points = np.array(CONTROL_POINTS)
# Control points and spline
_spline_x, _spline_y = make_spline(_control_points)
plot_coordinates(_control_points, _spline_x, _spline_y)
# OCV by DOD
_dod_arr, _voltage_arr = coordinates_to_values(_spline_x, _spline_y)
_dod_arr.reverse()
_voltage_arr.reverse()
plot_values(_dod_arr, _voltage_arr)
# Lookup values
_lookup_dod, _lookup_voltage = make_lookup(_dod_arr, _voltage_arr, LOOKUP_DOD_POINTS)
plot_lookup(_dod_arr, _voltage_arr, _lookup_dod, _lookup_voltage)
# Lookup with step 1%
_lookup_dod_1, _lookup_voltage_1 = make_lookup(_dod_arr, _voltage_arr, LOOKUP_DOD_1PERCENT_STEP)
plot_lookup(_dod_arr, _voltage_arr, _lookup_dod_1, _lookup_voltage_1,
color='darkorange', label='Lookup with step 1%')
print_values(_dod_arr, _voltage_arr)
print()
print_values(_lookup_dod, _lookup_voltage, '\t,')
print()
print_values(_lookup_dod_1, _lookup_voltage_1, 'int\t')
plt.show()
@asilichenko
Copy link
Author

asilichenko commented Jul 24, 2024

Control points and spline:
Figure 1

OCV by DOD:
Figure 2

Lookup values:
Figure 3

Lookup with step 1%:
Figure 4

Generated data (1000 points): https://docs.google.com/spreadsheets/d/1osN5bf89-xLXIydQKgJzRk5vHEYXHxaP2hoR2wP83jY/edit?usp=sharing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment