Sizing Lab

Hydrid Storage Architecture and Specification#

Written by Marc Budinger, INSA Toulouse, France

We will consider here an hybrid solutions with super capacities and traction battery packs. This notebook is dedicated :

  • to understand main operating limits of super capacitors and batteries

  • to understand a control architecture enabling to split power between super capacitors and batteries

  • to specify energy storage requirements of the different energy sources.

The storage element selection approach developed here is inspired by the following publication:

Jaafar, A., Sareni, B., Roboam, X., & Thiounn-Guermeur, M. (2010, September). Sizing of a hybrid locomotive based on accumulators and ultracapacitors. In 2010 IEEE Vehicle Power and Propulsion Conference (pp. 1-6). IEEE.[pdf]

Main operating limits of energy storage components#

To enable the selection of energy storage means, it is necessary to understand their main operational limitations. These limits can be representative:

  • rapid deterioration that can develop over an operating cycle, for example one journey or a few journeys over the same day.

  • gradual degradation linked to the lifespan of the component over multiple cycles, months or years, where the accumulation of degradation leads to an irreversible loss of performance.

Questions: Examine the following Figures extract from datasheet of elementary storage cells of supercapacitors or battery (LiFeSO4). Propose selection criteria representative of the main operational limits. Explain how to size a battery taking into account an high number of discharge cycles.

Battery Cell

Ultracapacitor Cell

Simulation of a complete line#

The objective of this section is to propose an evolution of the previous python codes to be able to:

  • simulate the power profile necessary for a complete line comprising several sections.

  • We will define in particular the type of vehicle, the different lengths of sections between 2 stations (Distances vector), the average speed to be ensured (Speeds vector), the presence of charger in station (Chargers vector), the stopping duration at station (StopDuration vector), the ratio between the maximum braking power and the maximum acceleration power (RatioBrakeMax).

  • provide the information necessary for sizing the battery/supercapacity packs that could be added.

  • We assume here an efficiency of the motorization chain of 100%.

  • Each line section will be optimized in order to meet the requirements defined previously and minimize the energy consumed. -The energy flow or the resulting power demand will be shared between battery and supercapacity with control based on frequency sharing of demands: the low frequency power will be provided by the batteries while the high frequency part will be provided by supercapacitors.

  • Indicators useful for sizing will then be generated from these power profiles.

  • take into account the energy that could come from charging stations or catenaries.

  • This version will only implement the consideration of charging stations.

  • Each charging station will provide the power to compensate for the energy of the travel from the last station.

Here we load all the functions and classes defined in the previous notebook.

%run ./01b_CaseStudy_Specification.ipynb
Consumption per passenger: 14.35 kJ/(Pax.km)
../../_images/be177c4884206f9c0847743a521e46fcd0931e9118d5d6b59fb62d0f593b61f8.png ../../_images/d73d733237df3d59676a23dd2bf89bc1f86f652efe33b130dc47b80e732ff742.png
[0.036125972011676784, 1, 0.9638740279883232]
Consumption per passenger: 20.44 kJ/(Pax.km)
../../_images/8fba898326fa1c0e7756ce2c990b52729fc18d452e7756922bf692facb54e0c9.png
     fun: 1.118074884265724
 message: 'Optimization terminated successfully.'
    nfev: 1444
     nit: 31
 success: True
       x: array([0.19050359, 0.28332682, 0.98943093])
Optimal vector : [0.19050359 0.28332682 0.98943093]
0.5450507762670603
Constraints vector : [0.00877090134784597, 0.00020729684908794098, 0.054949223732939645]
Consumption per passenger: 17.22 kJ/(Pax.km)
../../_images/c557e45bd6f85d4ea4bfc1610ba2cf0689b57f264082f3a409d79dbeee0f6146.png
Calculation of energy consumption of different vehicles: 

Tramway :
     fun: 1.2943682600385298
 message: 'Optimization terminated successfully.'
    nfev: 1129
     nit: 24
 success: True
       x: array([0.11939373, 0.38720184, 0.98950586])
Consumption per passenger: 20.18 kJ/(Pax.km)
----
Trolleybus :
     fun: 1.1396852632064673
 message: 'Optimization terminated successfully.'
    nfev: 1309
     nit: 28
 success: True
       x: array([0.07943326, 0.61789106, 0.98859802])
Consumption per passenger: 33.77 kJ/(Pax.km)
----
Bus :
     fun: 1.1554437831959297
 message: 'Optimization terminated successfully.'
    nfev: 1579
     nit: 34
 success: True
       x: array([0.04689676, 0.75507474, 0.98998498])
Consumption per passenger: 35.30 kJ/(Pax.km)
----
Car :
     fun: 1.522255018926166
 message: 'Optimization terminated successfully.'
    nfev: 1579
     nit: 34
 success: True
       x: array([4.26520509e-04, 9.98816721e-01, 9.81429487e-01])
Consumption per passenger: 102.08 kJ/(Pax.km)
../../_images/65c46b24f0826e9cce5503ae417e837d37bc25d4db31da23296bfb928c23858e.png

Here we define a line class with all the functionalities described just before.

from scipy import signal

class line():
    def __init__(self,Vehicle, Distances,Speeds, Chargers, StopDuration, RatioBrakeMax):
        i=0
        self.Section=[]
        self.Chargers =  Chargers # Boolean vector (True = Charger, False = No Charger , at end of te section)
        self.StopDuration = StopDuration # [s] station stop duration (scalar)
        self.RatioBrakeMax = RatioBrakeMax # [-] Ratio between max braking power / max acceleration power
        
        # initialization of  transient evolution (vectors)
        self.PowerStorage= [] # Transient evolution of requested power
        self.GlobalTime=[] # Time vector for plot and energy integration
        self.GlobalNRJStorage=[] # Transient evolution of energy
        self.PowerLF = [] # Transient evolution of power  (Low Frequency)
        self.PowerHF = [] # Transient evolution of power  (high Frequency)
        self.LFNRJStorage=[] # Time vector for plot and energy integration (Low Frequency)
        self.HFNRJStorage=[] # Time vector for plot and energy integration (High Frequency)

        self.TotalLineDistance =  sum(Distances)
        self.dt=0.25 # Time step for numerical integration
        
        # print characteristic of each section
        for d,s,c in zip(Distances,Speeds,Chargers):
            print("Section %.i: %.i m at %.2f m/s %s charger"%(i+1,d,s, "whith" if c else "without"))
            self.Section=self.Section+[OptimSection(Vehicle,d,s,self.RatioBrakeMax,self.dt)]
            i=i+1
    
    # Optimization loop of each section of the line
    def optimLine(self):
        X=[0.1,1,0.9]
        for i in range(len(self.Section)):
            self.Section[i].optimizeG(X)
            self.Section[i].plot()   
    
    # Power vector concatenation 
    def CalculPowerStorage(self):
        NRJ = 0
        self.PowerStorage= []
        self.GlobalTime= []
        dt=self.dt # [s] pas de temps pour l'integration
        
        # Power vector build thanks concatenation 
        for i in range(len(self.Section)):
            print(i)
            NRJ=NRJ+self.Section[i].NRJsection[-1] # we add here the energy consummed on the section
            
            self.PowerStorage = self.PowerStorage + self.Section[i].psection
            
            # Chargers effect
            if (self.Chargers[i] == True and i<(len(self.Section)-1)):
                tcharge=NRJ/self.Section[i].Vehicle.Pmax # Charging time caculation function of energy  
            else:
                tcharge=0
            if (tcharge>=self.StopDuration):
                self.PowerStorage = self.PowerStorage + [-self.Section[i].Vehicle.Pmax]*int(tcharge/dt)
                NRJ=0
            else:
                if (i<(len(self.Section)-1)):
                    self.PowerStorage = self.PowerStorage + [-self.Section[i].Vehicle.Pmax]*int(tcharge/dt)
                    self.PowerStorage = self.PowerStorage + [0]*int((self.StopDuration-tcharge)/dt)
                if (self.Chargers[i] == True) :
                    NRJ=0  
        
        # Time vector
        t=0
        for i in range(len(self.PowerStorage)):
            self.GlobalTime = self.GlobalTime + [t]
            t = t + dt
            
    # Filtering of total power in order to generate LF and HF pwoers
    def FilterPower(self, omega):
        TF=signal.TransferFunction([1], [1/omega**2, 2*1/omega, 1])
        time, self.PowerLF, state = signal.lsim(TF, self.PowerStorage , self.GlobalTime)
        self.PowerHF = self.PowerStorage -  self.PowerLF
        
    # NRJ vector integration from power vectors
    def IntegrateNRJ(self):
        t=0
        NRJtotal=0
        NRJHF=0
        NRJLF=0
        #NRJTotalAging=0
        #NRJLFAging=0
        self.HFNRJStorage = []
        self.GlobalNRJStorage = []
        self.LFNRJStorage = []
        dt=self.dt
        
        for i in range(len(self.PowerStorage)):
            self.GlobalNRJStorage = self.GlobalNRJStorage + [NRJtotal]
            self.HFNRJStorage = self.HFNRJStorage + [NRJHF]
            self.LFNRJStorage = self.LFNRJStorage + [NRJLF]
            #self.TotalNRJAging = self.TotalNRJAging + [NRJTotalAging]
            #self.LFNRJAging = self.LFNRJAging + [NRJLFAging]
        
            t = t + dt
            NRJtotal = NRJtotal+(self.PowerStorage[i])*dt
            NRJHF = NRJHF+(self.PowerHF[i])*dt
            NRJLF = NRJLF+(self.PowerLF[i])*dt
        
        PmaxHF = max(abs(min(self.PowerHF)),max(self.PowerHF))/1e3 # kW
        PmaxLF = max(self.PowerLF)/1e3 # kW 
        PmaxBrakeLF = abs(min(self.PowerLF))/1e3 # kW, Max power braking
        NRJHF = (max(self.HFNRJStorage) - min(self.HFNRJStorage))/3600/1e3 # NRJ en kWh
        NRJLF = (max(self.LFNRJStorage) - min(self.LFNRJStorage))/3600/1e3 # NRJ en kWh
        
        return PmaxHF, PmaxLF, PmaxBrakeLF, NRJHF, NRJLF
          
    
    
                
    # Main results plot
    def plot(self):
        fig, axs = plt.subplots(2,1)
        axs[0].plot(self.GlobalTime,self.PowerStorage,'b-',label='Total')
        axs[0].plot(self.GlobalTime,self.PowerLF,'r-.',label='LF')
        axs[0].plot(self.GlobalTime,self.PowerHF,'g-.',label='HF')
        axs[0].set_ylabel("Power (W)")
        axs[0].legend(bbox_to_anchor=(1.05, 1.0), loc='upper left')
        axs[0].grid()
        axs[1].plot(self.GlobalTime,self.GlobalNRJStorage,'b-',label='Total')
        axs[1].plot(self.GlobalTime,self.LFNRJStorage,'r-.',label='LF')
        axs[1].plot(self.GlobalTime,self.HFNRJStorage,'g-.',label='HF')
        axs[1].set_ylabel("Energy (J)")
        axs[1].legend(bbox_to_anchor=(1.05, 1.0), loc='upper left')
        axs[1].grid()
              
        axs[1].set_xlabel('Time (s)') 
        
        fig.tight_layout()            
        
        
            

Example of a line definition#

We can now use this new class to define a bus transport line with the following requirements:

  • distances: 700, 500, 400, 700, 3000, 300, 300, 300, 300, 300 m/s

  • mean speed: 7, 7, 7, 5, 5, 5, 5, 5 m/s

  • one final charger

ToulouseC=line(Bus,[700,500,400,700,300,300,300,300,300],[7, 7, 7,5,5,5,5,5],[False,False,False,False,False,False,False,True], 20, 0.6)
#ToulouseC=line(Bus,[700,500,400],[7, 7, 7],[False,False,True], 20, 0.6)
Section 1: 700 m at 7.00 m/s without charger
Section 2: 500 m at 7.00 m/s without charger
Section 3: 400 m at 7.00 m/s without charger
Section 4: 700 m at 5.00 m/s without charger
Section 5: 300 m at 5.00 m/s without charger
Section 6: 300 m at 5.00 m/s without charger
Section 7: 300 m at 5.00 m/s without charger
Section 8: 300 m at 5.00 m/s whith charger

Each speed profil section can be optimized.

ToulouseC.optimLine()
     fun: 1.0914466153457383
 message: 'Optimization terminated successfully.'
    nfev: 1534
     nit: 33
 success: True
       x: array([0.06626781, 0.72656115, 0.9892015 ])
     fun: 1.0945237778934926
 message: 'Optimization terminated successfully.'
    nfev: 1264
     nit: 27
 success: True
       x: array([0.09229909, 0.68985732, 0.98648391])
     fun: 1.1043612789972144
 message: 'Optimization terminated successfully.'
    nfev: 1264
     nit: 27
 success: True
       x: array([0.14202399, 0.61175796, 0.96930333])
     fun: 1.0537924920275576
 message: 'Optimization terminated successfully.'
    nfev: 2974
     nit: 65
 success: True
       x: array([0.02597591, 0.89831639, 0.98998913])
     fun: 1.0638753299209696
 message: 'Optimization terminated successfully.'
    nfev: 1309
     nit: 28
 success: True
       x: array([0.06853041, 0.74479909, 0.98965947])
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[4], line 1
----> 1 ToulouseC.optimLine()

Cell In[2], line 33, in line.optimLine(self)
     31 X=[0.1,1,0.9]
     32 for i in range(len(self.Section)):
---> 33     self.Section[i].optimizeG(X)
     34     self.Section[i].plot()

File /tmp/ipykernel_3424/2875550521.py:45, in OptimSection.optimizeG(self, x0)
     43 def optimizeG(self,x0):
     44     Xbound= [(0.0, 0.3), (0.1, 1), (0.5,0.99)]
---> 45     res=differential_evolution(func=self.objectifG, x0=x0, bounds=Xbound)
     46     print(res)
     47     return res.x

File /opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:392, in differential_evolution(func, bounds, args, strategy, maxiter, popsize, tol, mutation, recombination, seed, callback, disp, polish, init, atol, updating, workers, constraints, x0, integrality, vectorized)
    375 # using a context manager means that any created Pool objects are
    376 # cleared up.
    377 with DifferentialEvolutionSolver(func, bounds, args=args,
    378                                  strategy=strategy,
    379                                  maxiter=maxiter,
   (...)
    390                                  integrality=integrality,
    391                                  vectorized=vectorized) as solver:
--> 392     ret = solver.solve()
    394 return ret

File /opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:993, in DifferentialEvolutionSolver.solve(self)
    990 for nit in range(1, self.maxiter + 1):
    991     # evolve the population by a generation
    992     try:
--> 993         next(self)
    994     except StopIteration:
    995         warning_flag = True

File /opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/scipy/optimize/_differentialevolution.py:1379, in DifferentialEvolutionSolver.__next__(self)
   1377     feasible = True
   1378     cv = np.atleast_2d([0.])
-> 1379     energy = self.func(parameters)
   1380     self._nfev += 1
   1382 # compare trial and population member

File /opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/scipy/_lib/_util.py:407, in _FunctionWrapper.__call__(self, x)
    406 def __call__(self, x):
--> 407     return self.f(x, *self.args)

File /tmp/ipykernel_3424/2875550521.py:29, in OptimSection.objectifG(self, x)
     28 def objectifG(self, x):
---> 29     self.solver(x)
     30     NRJmin = self.xsection[-1]*self.Vehicle.Weight*self.g*self.Vehicle.Crr
     31     pen=0

File /tmp/ipykernel_3424/3938155709.py:85, in SimulSection.solver(self, x)
     82 self.Fsection = self.Fsection + [F]    
     84 # Euler integration de la postion y[0] et de la vitesse y[1]
---> 85 y = y + dt * np.array(dydt)
     87 t = t + dt
     88 NRJ = NRJ+y[1]*F*dt

KeyboardInterrupt: 
../../_images/31d234d721beff767b8a4c1d1acf1bf47aef066ad4c414bc2e3c4a3829bae59b.png ../../_images/ed9037909bc9521d2eb7096f0ed76c8e6b5c24a221b1eb4d7f9b33d1c1020be6.png ../../_images/d9e8934431a6668ab772b4acf0932d486a685eb5cfa4f95075e841399effb382.png ../../_images/9a528a292cf615704ed9b69b1a7061c2fe5999fba09f016bda767edec1f53773.png

A time vector of evolution of the power required at each section or supplied to each charger is constructed.

ToulouseC.CalculPowerStorage()
0
1
2
3
4
5
6
7

Hybrid storage system sizing#

The energy flow or the resulting power demand will be shared between battery and supercapacity with control based on frequency sharing of demands. The Figure below show how the low frequency power will be provided by the batteries while the high frequency part will be provided by supercapacitors.

Sizing Stategy

The cutoff frequency defines the power sharing and has a strong influence on the sizing of the storage elements. The following code analyzes this power sharing by varying this cutoff frequency.

Questions: Explain the sizing criteria implemented here to evaluate the mass or CO2 impact of batteries and supercapacitors.

omegaV=np.logspace(-5,2,50)
MassStorageV=[]
MassSC=[]
MassLFPAging=[]
MassLFPNRJ=[]
MassLFPPow=[]
CO2Total=[]

# Hypothese
Targetkm = 250e3 # [km] durée de vie du vehicule

# Energie massique des supercapacités
# https://1188159.fs1.hubspotusercontent-na1.net/hubfs/1188159/02-DS-220909-SKELCAP-CELLS-1F.pdf
# chez Skeleton
WmassSC=6.8*0.75 # [Wh/kg] on suppose pouvoir recuperer 75% de l'energie stockée
PmassSC=860/4.3*6.8*0.75 # [W/kg]

# Energie massique des batteries
# LFP
WmassLFP= 100 # [Wh/kg] les LFP peuvent pratiquement etre dechargé a 100%
PmassLFP=3*100 # [W/kg]  puissance massique en decharge à 3 C
PBmassLFP=1*100 # [W/kg]  puissance massique en decharge à 1 C
Ncycle = 3000 # [-] nb de cycle de decharge a 100%

# Bilan carbone
CO2SC = 39 # kgCO2eq/kg d'ecoInvent
CO2LFP = 11 # kgCO2eq/kg d'ecoInvent

for omega in omegaV:
    ToulouseC.FilterPower(omega)
    PmaxHF, PmaxLF, PmaxBrake, NRJHF, NRJLF = ToulouseC.IntegrateNRJ()
    
    Nc=Targetkm*1000/ToulouseC.TotalLineDistance # Number of cycles for global lifetime 
    DoD=(Nc/3365)**(-1/2.26) # DoD calculation for Target Km
    MassStorageV = MassStorageV + [max(NRJHF/WmassSC*1e3, PmaxHF/PmassSC*1e3)
                                   +max(NRJLF/DoD/WmassLFP*1e3,
                                       PmaxLF/PmassLFP*1e3, PmaxBrake/PBmassLFP*1e3)] 
    MassSC = MassSC + [max(NRJHF/WmassSC*1e3, PmaxHF/PmassSC*1e3)] 
    MassLFPNRJ = MassLFPNRJ + [NRJLF/WmassLFP*1e3] 
    
    MassLFPAging = MassLFPAging + [NRJLF/DoD/WmassLFP*1e3] 
    MassLFPPow = MassLFPPow + [PmaxBrake/PBmassLFP*1e3]
    CO2Total = CO2Total + [max(NRJHF/WmassSC*1e3, PmaxHF/PmassSC*1e3)*CO2SC+
                           max(NRJLF/DoD/WmassLFP*1e3, PmaxLF/PmassLFP*1e3, PmaxBrake/PBmassLFP*1e3)*CO2LFP]

The following figures represent the overall mass of the solutions according to the power sharing achieved. A simple CO2 impact is also estimated.

plt.plot(omegaV, MassStorageV, 'g^', label='Total')
plt.plot(omegaV, MassSC, 'yx', label='SuperCap')
plt.plot(omegaV, MassLFPNRJ, 'bo', label='LFP NRJ')
plt.plot(omegaV, MassLFPPow, 'bx', label='LFP Power (Brake)')
plt.plot(omegaV, MassLFPAging, 'ro', label='LFP Aging')
plt.xscale('log') 
plt.ylabel('Weight (kg)')
plt.xlabel('Cut off angular frequency (rad/s)')
plt.legend()
plt.show()
plt.plot(omegaV, CO2Total, 'g^', label='Total')
plt.xscale('log') 
plt.ylabel('CO2  (kgCO2eq)')
plt.xlabel('Cut off angular frequency (rad/s)')
plt.legend()
plt.show()
../../_images/554755f19dcc6c0584715f13f7bccc6730627650ee4108891765b0229e0d1b7b.png ../../_images/a487b4b7f4a7ca950443997ec662057d727f5f29c3f11ea28c4427a6a4db6c8b.png

A Pareto front can help find a solution achieving a compromise between 2 objectives.

# Pareto Front

plt.scatter(MassStorageV, CO2Total, c=np.log10(omegaV))
plt.xlabel('Weight (kg)')
plt.ylabel('CO2  (kgCO2eq)')
plt.colorbar()
plt.title('Cut off angular frequency influence on Pareto Front')
plt.show()
../../_images/1296176c0e113bf915e565d5df7e0b3f58e56bfc964a70f4561f243ea6bd40b6.png
ToulouseC.FilterPower(0.4)
PmaxHF, PmaxLF, PmaxBrakeLF, NRJHF, NRJLF=ToulouseC.IntegrateNRJ()

Nc=Targetkm*1000/ToulouseC.TotalLineDistance # Number of cycles for global lifetime 
DoD=(Nc/3365)**(-1/2.26) # DoD calculation for Target Km

print("Super Capacitor:")
print("Pmax: %.2f kW"%PmaxHF)
print("NRJ:  %.2f kWh"%NRJHF)
print("Mass: % .1f kg"%(max(NRJHF/WmassSC*1e3, PmaxHF/PmassSC*1e3)))
      
print("---")
print("Traction battery:")
print("Pmax discharge: %.2f kW"%PmaxLF)
print("Pmax charge: %.2f kW"%PmaxBrakeLF)
print("NRJ: %.2f kWh"%(NRJLF/DoD))
print("NRJ one travel: %.2f kWh"%(NRJLF))
print("Mass: % .1f kg"%(max(NRJLF/DoD/WmassLFP*1e3,
                                       PmaxLF/PmassLFP*1e3, PmaxBrakeLF/PBmassLFP*1e3)))  
print("Mass (brake criteria): % .1f kg"%(PmaxLF/PmassLFP*1e3))  


ToulouseC.plot()
print("---")
Super Capacitor:
Pmax: 118.02 kW
NRJ:  0.22 kWh
Mass:  115.7 kg
---
Traction battery:
Pmax discharge: 105.01 kW
Pmax charge: 32.22 kW
NRJ: 11.27 kWh
NRJ one travel: 3.02 kWh
Mass:  350.0 kg
Mass (brake criteria):  350.0 kg
---
../../_images/6e5a5ffd9833af03ca4fc940fdb7abed3ffdaa3fd2d3c7e9db5022245001a562.png

Labwork and homework#

Your objective is to specify the hybrid storage system of an electric bus for doubling line 78 between the IUT Rangueil and MFJA stations. The characteristics of the bus are here
BlueBus A example of time table of the line 78 is here
L78 We will assume a round trip in 20 min, charge at the ends of the lines included.

Modify the present notebooks in order to set up a technical justification report : starting from the need (journey, vehicle size, frequency of journeys), setting up the effort/speed/power profiles, the power distribution in the hybrid storage system, the preliminary sizing and the specification of the main components.

Adapt and complete the sizing process in order to take into account the global efficiency of the converters and storage elments (assumed to be equal to 80%).

Sizing process

Propose compatible technological storage packs and specify the DC/DC converters (DC bus of 400 V).

Provide an electrical architecture diagram (possible software) summarizing your main choices:

  • representing the different sources and load of the network.

  • making it possible to standardize the DC/DC converters used.

  • allowing reduced functionality to be maintained in the event of a fault on part of the storage elements