55import numpy as np
66from loguru import logger
77
8+ from vessim .signal import Signal
9+
810
911class Storage (ABC ):
1012 @abstractmethod
@@ -41,7 +43,126 @@ def state(self) -> dict:
4143 return {}
4244
4345
44- class SimpleBattery (Storage ):
46+ class BatteryDegradation (ABC ):
47+ @abstractmethod
48+ def update (self , soc : float , duration : int ) -> float :
49+ """Calculate degradation based on state-of-charge after specified duration.
50+
51+ Args:
52+ soc: The battery's SoC after the duration.
53+ duration: Duration in seconds over which the battery reached the given SoC.
54+
55+ Returns:
56+ The newly calculated relative discharge capacity.
57+ """
58+ pass
59+
60+ @abstractmethod
61+ def q (self ) -> float :
62+ """Returns the relative discharge capacity (q) of the battery.
63+
64+ Values should range between 0 and 1.
65+ """
66+ pass
67+
68+ def state (self ) -> dict :
69+ """Returns information about the current state of the degradation. Can be overriden."""
70+ return {"q" : self .q ()}
71+
72+
73+ try :
74+ from blast .models import BatteryDegradationModel
75+
76+ class ModelDegradation (BatteryDegradation ):
77+ """Battery degradation as modeled by a BLAST-Lite model.
78+
79+ Args:
80+ model: BLAST-Lite degradation model.
81+ temp: Battery temperature signal in Celsius. Should start at 00:00:00.
82+ sample_size: Number of battery SoC samples to take before updating degradation model.
83+ """
84+
85+ def __init__ (
86+ self ,
87+ model : BatteryDegradationModel ,
88+ temp : Signal ,
89+ sample_size : int ,
90+ initial_soc : float = 0 ,
91+ ) -> None :
92+ self .model = model
93+ self .temp = temp
94+ self .sample_size = sample_size
95+ self .t = np .datetime64 (0 , "s" )
96+ self ._q = 1
97+
98+ self .t_secs = np .zeros (sample_size )
99+ self .soc = np .zeros (sample_size )
100+ self .T_celsius = np .zeros (sample_size )
101+ self .t_secs [0 ] = 0
102+ self .soc [0 ] = initial_soc
103+ self .T_celsius [0 ] = temp .now (self .t )
104+ self .samples = 1
105+
106+ def update (self , soc : float , duration : int ) -> float :
107+ dt = np .timedelta64 (duration , "s" )
108+ self .t += dt
109+
110+ if self .samples > 0 and self .samples % self .sample_size == 0 :
111+ self .model .update_battery_state (
112+ self .t_secs ,
113+ self .soc ,
114+ self .T_celsius ,
115+ )
116+ self ._q = self .model .outputs ["q" ][- 1 ]
117+ self .samples = 0
118+
119+ self .t_secs [self .samples ] = self .t_secs [self .samples - 1 ] + duration
120+ self .soc [self .samples ] = soc
121+ self .T_celsius [self .samples ] = self .temp .now (self .t )
122+ self .samples += 1
123+ return self ._q
124+
125+ def q (self ) -> float :
126+ return self ._q
127+
128+ def state (self ) -> dict :
129+ s = super ().state ()
130+ s .update ({"temp" : self .temp .now (self .t )})
131+ return s
132+ except ImportError :
133+ pass
134+
135+
136+ class Battery (Storage ):
137+ def __init__ (self , deg : Optional [BatteryDegradation ] = None ) -> None :
138+ self .deg = deg
139+
140+ def update (self , power : float , duration : int ) -> float :
141+ total_power = self ._update (power , duration )
142+ if self .deg is not None :
143+ q = self .deg .update (self .soc (), duration )
144+ self .degrade_to (q )
145+ return total_power
146+
147+ @abstractmethod
148+ def _update (self , power : float , duration : int ) -> float :
149+ pass
150+
151+ @abstractmethod
152+ def degrade_to (self , q : float ) -> None :
153+ pass
154+
155+ def state (self ) -> dict :
156+ s = self ._state ()
157+ if self .deg is not None :
158+ s .update (self .deg .state ())
159+ return s
160+
161+ def _state (self ) -> dict :
162+ return {}
163+
164+
165+ class SimpleBattery (Battery ):
45166 """(Way too) simple battery.
46167
47168 Args:
@@ -62,7 +183,10 @@ def __init__(
62183 initial_soc : float = 0 ,
63184 min_soc : float = 0 ,
64185 c_rate : Optional [float ] = None ,
186+ deg : Optional [BatteryDegradation ] = None ,
65187 ):
188+ super ().__init__ (deg )
189+ self .initial_capacity = capacity
66190 self .capacity = capacity
67191 assert 0 <= initial_soc <= 1 , "Invalid initial state-of-charge. Has to be between 0 and 1."
68192 self .charge_level = capacity * initial_soc
@@ -71,7 +195,7 @@ def __init__(
71195 self .min_soc = min_soc
72196 self .c_rate = c_rate
73197
74- def update (self , power : float , duration : int ) -> float :
198+ def _update (self , power : float , duration : int ) -> float :
75199 """Charges the battery with specific power for a duration.
76200
77201 Updates batteries energy level according to power that is fed to/ drawn from the battery.
@@ -123,21 +247,28 @@ def update(self, power: float, duration: int) -> float:
123247
124248 return charged_energy
125249
250+ def degrade_to (self , q : float ) -> None :
251+ new_capacity = q * self .initial_capacity
252+ r = new_capacity / self .capacity
253+ self .capacity = new_capacity
254+ self .charge_level *= r
255+
126256 def soc (self ) -> float :
127257 return self ._soc
128258
129- def state (self ) -> dict :
259+ def _state (self ) -> dict :
130260 """Returns state information of the battery as a dict."""
131261 return {
132262 "soc" : self ._soc ,
133263 "charge_level" : self .charge_level ,
264+ "initial_capacity" : self .initial_capacity ,
134265 "capacity" : self .capacity ,
135266 "min_soc" : self .min_soc ,
136267 "c_rate" : self .c_rate ,
137268 }
138269
139270
140- class ClcBattery (Storage ):
271+ class ClcBattery (Battery ):
141272 """Implementation of the C-L-C Battery model for lithium-ion batteries.
142273
143274 This class implements the C-L-C model as described in:
@@ -196,16 +327,19 @@ def __init__(
196327 eta_c : float = 0.978 ,
197328 discharging_current_cutoff : float = - 0.05 ,
198329 charging_current_cutoff : float = 0.05 ,
330+ deg : Optional [BatteryDegradation ] = None ,
199331 ) -> None :
332+ super ().__init__ (deg )
200333 assert number_of_cells > 0 , "There has to be a positive number of cells."
201334 self .number_of_cells = number_of_cells
202335 self .u_1 = u_1
203336 self .v_1 = v_1
204337 self .u_2 = u_2
205338 self .v_2 = v_2
339+ self .initial_v2 = v_2
206340 assert 0 <= initial_soc <= 1 , "Invalid initial state-of-charge. Has to be between 0 and 1."
207341 self ._soc = initial_soc
208- self .charge_level = self .v_2 * initial_soc # Charge level of one cell
342+ self .charge_level = self .v_2 * initial_soc # Charge level of one cell
209343 assert 0 <= min_soc <= 1 , "Invalid minimum state-of-charge. Has to be between 0 and 1."
210344 self .min_soc = min_soc
211345 self .nom_voltage = nom_voltage
@@ -221,7 +355,7 @@ def __init__(
221355 def soc (self ) -> float :
222356 return self ._soc
223357
224- def update (self , power : float , duration : int ) -> float :
358+ def _update (self , power : float , duration : int ) -> float :
225359 if duration <= 0.0 :
226360 raise ValueError ("Duration needs to be a positive value" )
227361
@@ -232,6 +366,12 @@ def update(self, power: float, duration: int) -> float:
232366 else :
233367 return 0
234368
369+ def degrade_to (self , q : float ) -> None :
370+ new_capacity = q * self .initial_v2
371+ r = new_capacity / self .v_2
372+ self .v_2 = new_capacity
373+ self .charge_level *= r
374+
235375 def charge (self , power : float , duration : int ) -> float :
236376 # Apply charging power limits
237377 max_power = (
@@ -278,10 +418,11 @@ def discharge(self, power: float, duration: int) -> float:
278418 self ._soc = self .charge_level / self .v_2
279419 return power * duration
280420
281- def state (self ) -> dict :
421+ def _state (self ) -> dict :
282422 return {
283423 "soc" : self ._soc ,
284424 "charge_level" : self .charge_level * self .number_of_cells ,
425+ "initial_capacity" : self .initial_v2 * self .number_of_cells ,
285426 "capacity" : self .v_2 * self .number_of_cells ,
286- "min_soc" : self .min_soc
427+ "min_soc" : self .min_soc ,
287428 }
0 commit comments