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

Add files from Micah Black on SoC refactor #86

Merged
merged 1 commit into from
Nov 7, 2020
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
2 changes: 1 addition & 1 deletion displays/SOC_velocity_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))

from optimization.car_model import Car
from soc.SoCEstimation import CoulombCounter
from soc.soc_deprecated.SoCEstimation import CoulombCounter

def calculate_SOC_values(v_profile, e_profile, distance, initial_soc, min_speed=None, max_speed=None):
if min_speed is not None and max_speed is not None:
Expand Down
4 changes: 1 addition & 3 deletions displays/tests/test_SOC_velocity_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import pytest
import mock
from displays.SOC_velocity_graph import calculate_SOC_values
from matplotlib.testing.decorators import check_figures_equal
import matplotlib.pyplot as plt

def test_calculate_SOC_values_zero_values():
velocities = [0, 0, 0]
Expand Down Expand Up @@ -41,7 +39,7 @@ def test_calculate_SOC_values_zero_length_inputs():
calculate_SOC_values([], [], [], 1)

def test_calculate_SOC_values_single_element_inputs():
with mock.patch("soc.SoCEstimation.CoulombCounter.get_soc", return_value=0.9):
with mock.patch("soc.soc_deprecated.SoCEstimation.CoulombCounter.get_soc", return_value=0.9):
expected_result = [1, 0.9]
assert(expected_result == (calculate_SOC_values([1], [(1, 1)], [1], 1)))

Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pytest==5.0.1
flake8
haversine
requests
mock==4.0.1
mock==4.0.1
sklearn
78 changes: 78 additions & 0 deletions soc/BatTestPlot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import matplotlib.pyplot as plt
import numpy as np
import itertools
from sklearn import datasets, linear_model, metrics
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline

# fName = "D:\Documents\MSXIV\MSXIV\Strategy\CellDataFileTestMJ1.txt"
# fName = r"C:\Users\micke\Documents\MSXIV\Strategy\CellDataFileTestMJ1.txt"

import sys
import os.path
sys.path.append(os.path.dirname(__file__))
fName = 'CellDataFileTestMJ1.txt'

class BatTestPlot:
def __init__(self):
#includes 0.050 cell and 0.035 cable, from tests done on single cell
self.IR = 0.085

def plot_SoCOCV(self):
print ("plotting SoC OCV")

x = np.arange(0,10,0.2)
y = np.sin(x)

socfig, ax = plt.subplots()
ax.plot(x,y)
plt.title('MJ1 SoC OCV @3.5A')
plt.show()

def read_data(self, filename):
#voltage, capacity = np.loadtxt(filename, delimiter = '\t', skiprows = 1, usecols = (1,7), unpack = True)
with open(filename) as f_in:
voltage, current, soc = np.loadtxt(itertools.islice(f_in, 1, None, 2), delimiter = '\t', usecols = (1,3,9), unpack = True)

v_ocv = voltage + self.IR * current

fig, ax = plt.subplots()

ax.plot(soc, voltage, 'bo',label = 'v')
ax.plot(soc, v_ocv, 'm-', label = 'ocv')

soc = soc.reshape(-1,1)
voltage = voltage.reshape(-1,1)

#model = linear_model.LinearRegression()
#model.fit(soc, voltage)

#voltage_test = model.predict(soc)

#ax.plot(soc, voltage_test, 'r-')

#print('Coefficients: \n', model.coef_)
#print('Variance Score: %.2f' % metrics.r2_score(voltage, voltage_test))

poly_model = make_pipeline(PolynomialFeatures(5), linear_model.Ridge())
poly_model.fit(soc, v_ocv)

print("coefficients:")
print(poly_model[1].coef_)
print("Intercept:")
print(poly_model[1].intercept_)

poly_voltage_plot = poly_model.predict(soc)
ax.plot(soc, poly_voltage_plot, 'g-', label = 'v_prediction', linewidth = 6)

plt.title('Soc vs Voltage LG MJ1 @3A DSC')
plt.legend(loc = 'lower right')
plt.grid(True)
plt.show()


print ("Started")

test = BatTestPlot()
test.read_data(fName)
#test.plot_SoCOCV()
4,006 changes: 4,006 additions & 0 deletions soc/CellDataFileTestMJ1.txt

Large diffs are not rendered by default.

Binary file added soc/Efficiency-and-Power-Loss-vs-Power-Draw.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
112 changes: 100 additions & 12 deletions soc/PackEfficiency.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
#this is programmed in notepad++ and not tested.

import matplotlib.pyplot as plt
import numpy as np
import cmath

import sys
import os.path
sys.path.append(os.path.dirname(__file__))
from SoC_OCV import SoC_OCV

class PackEfficiency:
'''
resistance of the pack in ohms
Expand All @@ -18,27 +23,110 @@ class PackEfficiency:
power loss in a pack is due primarily to ohmic heating
'''


def __init__(self):
self._pack_resistance = 0.055
self.pack_resistance = 0.055

#initialize SoC-OCV model curve
self.soc_ocv_curve = SoC_OCV()

#if we want say 1000W at the motors, we will need to draw more power (1050W) from the cells
def draw_power(self, power_outside_pack_W):
pack_ocv = get_pack_ocv() #from SoC-OCV curve or telemetry, assume cells are balanced
def draw_power(self, power_outside_pack_W, soc, max = 100): #max is the maximum value for 100% SoC
if power_outside_pack_W == 0:
return (0, 1)

#pack_ocv = 36*4 #from SoC-OCV curve or telemetry, assume cells are balanced
pack_ocv = self.soc_ocv_curve.get_cell_ocv(soc, max) * 36
print("Pack OCV: {}".format(pack_ocv))

#solve quadratic equation to find current
#I^2Ri - IVo + Po = 0
current_draw = (pack_ocv - cmath.sqrt((pack_ocv ** 2) - (4 * self._pack_resistance * power_outside_pack_W))) / (2 * (self._pack_resistance))
current_draw = (pack_ocv - cmath.sqrt((pack_ocv ** 2) -
(4 * self.pack_resistance * power_outside_pack_W))) \
/ (2 * self.pack_resistance)
print("Current Draw: {}".format(current_draw))

#this power loss is wasted as heat through the resistive elements
power_loss = (current_draw ** 2) * self._pack_resistance
power_loss = (current_draw**2)*self.pack_resistance

#the power drawn from the cell itself (before resistive losses from internal resistance), the actual power draw for SoC estimation
power_inside_pack = power_outside_pack_W + power_loss

#1 = 100% efficiency
efficiency = (power_inside_pack - power_loss) / power_inside_pack
#discharging efficiency - useable energy / given energy
if power_outside_pack_W > 0:
efficiency = (power_inside_pack - power_loss) / power_inside_pack
#charging efficiency - power loss will be negative in this case - useable energy / given energy
else:
efficiency = (power_inside_pack) / power_outside_pack_W

return (power_inside_pack, efficiency)


return power_inside_pack
#Make sure we get values that make sense
class Test_Pack_Efficiency:
def __init__(self):
self.packEfficiency = PackEfficiency()
def test(self):
power, efficiency = self.packEfficiency.draw_power(100, 100)
print("100W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(500, 90)
print("500W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(5000, 80)
print("5000W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(20000, 10)
print("20000W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(0, 10)
print("0W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(-500, 50)
print("-500W Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(-5000, 80)
print("-5000W Test: {} Eff: {}".format(power, efficiency))

power, efficiency = self.packEfficiency.draw_power(20000, 100)
print("\n\n20000W 100% Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(20000, 50)
print("20000W 50% Test: {} Eff: {}".format(power, efficiency))
power, efficiency = self.packEfficiency.draw_power(20000, 0)
print("20000W 0% Test: {} Eff: {}".format(power, efficiency))


def get_pack_ocv():
return 0.0
'''
#Testing Code
print ("Testing Started")
pe = Test_Pack_Efficiency()
pe.test()
'''

if __name__ == "__main__":
#plot efficiency and power loss vs power draw
#make sure we get reasonable values

pet = Test_Pack_Efficiency()
pet.test()

pe = PackEfficiency()

p_array = []
e_array = []
w_array = []

for i in range(0, 20000, 1000):
power, efficiency = pe.draw_power(i, 60)
p_array.append(power)
e_array.append(efficiency)
w_array.append(power - i)

print(p_array)
print(e_array)

fig, ax = plt.subplots()
ax.plot(p_array, e_array, 'b-',label = '%e')
plt.ylabel('Efficiency')
plt.xlabel('Power Draw')

ax2 = ax.twinx()
ax2.set_ylabel('Power Loss')
ax2.plot(p_array, w_array)

plt.show()

Binary file added soc/SoC-OCV-Python-Test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added soc/SoC-OCV-Verification-desmos-graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 72 additions & 24 deletions soc/SoCEstimation.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
#Cell SOC Estimation class - Coulomb counting by energy (Amp draw over telemetry)
#Cell SOC Estimation class - Coulomb counting by pack data available
#Based off Karl's coulomb counting method
#https://uwmidsun.atlassian.net/wiki/spaces/~karlding/pages/620036100/BMS+SOC+Algorithm+v1.0.0+aka.+We+live+in+a+SOCiety
#not really a coulomb counter, more of an energy meter.

#how is this being accessed, etc??
#Will it be different when used on telemetry data / modelling
#this is programmed in notepad++ and not tested.

#This class operates on the assumptions that our pack is perfectly balanced, and there is no evident capacity fade
#SoC returned is between 0 (0%) and 1 (100%)

import sys
import os.path
sys.path.append(os.path.dirname(__file__))
from PackEfficiency import PackEfficiency

class CoulombCounter:
'''
Expand All @@ -15,37 +19,81 @@ class CoulombCounter:
3.635V nominal
'''
def __init__(self):
self._energy_available = 0
self._pack_energy = 36 * 36 * 3.45 * 3.635
self._SoC = self._energy_available / self._pack_energy
self.energy_available = 0
self.pack_energy = 36 * 36 * 3.45 *3.635 #36S * 36P * 3.45Ah * 3.635V = 16 252.812 Wh
self.SoC = self.energy_available / self.pack_energy
self.maxSoC = 1 #the value of self.SoC when at 100% full
self.PackEff = PackEfficiency()
#cell ocv is available with PackEff.soc_ocv_curve.get_cell_ocv(self.SoC, max = 1)

def set_SoC(self, soc):
self._SoC = soc
self._energy_available = soc * self._pack_energy
self.SoC = soc
self.energy_available = soc * self.pack_energy

def set_energy(self, energy):
self._energy_available = energy
self._SoC = self._energy_available / self._pack_energy
self.energy_available = energy
self.SoC = self.energy_available / self.pack_energy

def get_soc(self):
return self._SoC
return self.SoC

def _print(self):
print("SoC: {} Energy Remaining: {} Voltage: {}".format(self.SoC, self.energy_available, self.PackEff.soc_ocv_curve.get_cell_ocv(self.SoC, max = self.maxSoC)*36))

#given telemetry updates, discharge the cells
def telemetry_discharge(self, telemetry_pack_current, telemetry_pack_voltage, telemetry_interval_ms):
self.discharge(self, telemetry_pack_current * telemetry_pack_voltage, telemetry_interval_ms*1000)

#use a certain amount of power for a certain amount of time
#dirOUT true for discharge, false for charge
def discharge(self, power_W, time_S, dirOUT):
#total energy in kWh
energy = power_W * time_S / 3600 / 1000

if dirOUT:
#discharge
self._energy_available -= energy
else:
#charge
self._energy_available += energy
def discharge(self, power_W, time_S, dirOUT = True):
if not dirOUT:
power_W *= -1

#power calculation taking the power loss into account
power_total, efficiency = self.PackEff.draw_power(power_W, self.SoC, max = self.maxSoC)
print(power_total)

#total energy in Wh
energy = power_total * time_S/3600

#discharge
self.energy_available -= energy

#update SoC
self._SoC = self._energy_available / self._pack_energy
self.SoC = self.energy_available / self.pack_energy



class TestCoulombCounter:
def __init__(self):
self.CC = CoulombCounter()
self.CC._print()

def test(self):
self.CC.set_SoC(1)
self.CC._print()
self.CC.set_SoC(0.75)
self.CC._print()

def test2(self):
self.CC.set_SoC(0.9)
self.CC._print()
for x in range(4):
self.CC._print()
self.CC.discharge(20000, 500)
self.CC._print()

#expected without other power loss
print("Expected without Power loss")
print(0.9 * self.CC.pack_energy - 20000 * 500 / 3600 * 4)

#next step: plot this against another discharge curve
if __name__ == "__main__":
#print out to console and make sure we get reasonable values
print ("Starting")
TCC = TestCoulombCounter()
print("\n\n Test1: \n")
TCC.test()
print("\n\n Test 2: \n")
TCC.test2()
Binary file added soc/SoC_OCV-corrected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading