Skip to content

Commit 49692d3

Browse files
committed
add battery degradation
reset samples counter after degradation update
1 parent 9960f24 commit 49692d3

File tree

3 files changed

+164
-10
lines changed

3 files changed

+164
-10
lines changed

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ requests = {version = "^2.26.0", optional = true}
5353
fastapi = {version = "^0.104.0", optional = true}
5454
uvicorn = {version = "^0.23.0", optional = true}
5555

56+
# Optional dependencies (battery degradation)
57+
blast-lite = {version = "^1.0.5", optional = true, python = ">=3.9"}
58+
5659
[tool.poetry.extras]
5760
sil = ["requests", "fastapi", "uvicorn"]
61+
model-deg = ["blast-lite"]
5862

5963
[tool.poetry.group.dev]
6064
optional = true
@@ -131,4 +135,4 @@ filterwarnings = [
131135
"error",
132136
# https://github.com/dateutil/dateutil/issues/1314
133137
"ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz",
134-
]
138+
]

vessim/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from vessim.cosim import Microgrid, Environment
66
from vessim.policy import MicrogridPolicy, DefaultMicrogridPolicy
77
from vessim.signal import Signal, HistoricalSignal, MockSignal, CollectorSignal
8-
from vessim.storage import Storage, SimpleBattery, ClcBattery
8+
from vessim.storage import Storage, Battery, BatteryDegradation, SimpleBattery, ClcBattery
99

1010
__all__ = [
1111
"ActorBase",
@@ -22,10 +22,19 @@
2222
"Signal",
2323
"HistoricalSignal",
2424
"Storage",
25+
"Battery",
26+
"BatteryDegradation",
2527
"ClcBattery",
2628
"SimpleBattery",
2729
]
2830

31+
try:
32+
from vessim.storage import ModelDegradation # noqa: F401
33+
34+
__all__.extend(["ModelDegradation"])
35+
except ImportError:
36+
pass
37+
2938
try:
3039
from vessim.sil import Broker, SilController, WatttimeSignal, get_latest_event # noqa: F401
3140

vessim/storage.py

Lines changed: 149 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import numpy as np
66
from loguru import logger
77

8+
from vessim.signal import Signal
9+
810

911
class 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

Comments
 (0)