shepherd-core 2025.8.1__py3-none-any.whl → 2025.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shepherd_core/data_models/__init__.py +4 -2
- shepherd_core/data_models/base/content.py +2 -0
- shepherd_core/data_models/content/__init__.py +4 -2
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +3 -3
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +82 -58
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +426 -0
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
- shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
- shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
- shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
- shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
- shepherd_core/data_models/experiment/target_config.py +1 -1
- shepherd_core/data_models/task/emulation.py +1 -1
- shepherd_core/data_models/task/harvest.py +1 -1
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/inventory/system.py +1 -1
- shepherd_core/reader.py +4 -3
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +27 -26
- shepherd_core/vsource/virtual_harvester_model.py +27 -19
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +17 -13
- shepherd_core/vsource/virtual_source_simulation.py +71 -73
- shepherd_core/vsource/virtual_storage_model.py +164 -0
- shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
- shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
- shepherd_core/vsource/virtual_storage_simulator.py +104 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2025.10.1.dist-info}/METADATA +2 -1
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2025.10.1.dist-info}/RECORD +37 -25
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2025.10.1.dist-info}/WHEEL +0 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2025.10.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2025.10.1.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"""Generalized virtual energy storage data models (config).
|
|
2
|
+
|
|
3
|
+
Additions in near future:
|
|
4
|
+
- TODO: DC Bias to improve Capacitor-behavior, p_VOC / LuT
|
|
5
|
+
|
|
6
|
+
Possible Extensions:
|
|
7
|
+
- scale cell-count (determine how to change parameters) as
|
|
8
|
+
2 cell Lipo or 2 cell lead would be advantageous
|
|
9
|
+
- add temperature-component?
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from datetime import timedelta
|
|
17
|
+
from typing import Annotated
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from annotated_types import Ge
|
|
21
|
+
from annotated_types import Gt
|
|
22
|
+
from annotated_types import Le
|
|
23
|
+
from pydantic import Field
|
|
24
|
+
from pydantic import NonNegativeFloat
|
|
25
|
+
from pydantic import PositiveFloat
|
|
26
|
+
from pydantic import model_validator
|
|
27
|
+
from pydantic import validate_call
|
|
28
|
+
from typing_extensions import Self
|
|
29
|
+
|
|
30
|
+
from shepherd_core.config import config
|
|
31
|
+
from shepherd_core.data_models.base.content import ContentModel
|
|
32
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
33
|
+
from shepherd_core.logger import log
|
|
34
|
+
from shepherd_core.testbed_client import tb_client
|
|
35
|
+
|
|
36
|
+
soc_t = Annotated[float, Ge(0.0), Le(1.0)]
|
|
37
|
+
# TODO: adapt V_max in vsrc,
|
|
38
|
+
# TODO: do we need to set initial voltage, or is SoC ok? add V_OC_to_SoC()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class VirtualStorageConfig(ContentModel, title="Config for the virtual energy storage"):
|
|
42
|
+
"""KiBaM Battery model based on two papers.
|
|
43
|
+
|
|
44
|
+
Model An Accurate Electrical Battery Model Capable
|
|
45
|
+
of Predicting Runtime and I-V Performance
|
|
46
|
+
https://rincon-mora.gatech.edu/publicat/jrnls/tec05_batt_mdl.pdf
|
|
47
|
+
|
|
48
|
+
A Hybrid Battery Model Capable of Capturing Dynamic Circuit
|
|
49
|
+
Characteristics and Nonlinear Capacity Effects
|
|
50
|
+
https://digitalcommons.unl.edu/cgi/viewcontent.cgi?article=1210&context=electricalengineeringfacpub
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
SoC_init: soc_t = 0.8
|
|
54
|
+
""" ⤷ State of Charge that is available when emulation starts.
|
|
55
|
+
Allows a proper / fast startup.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
q_As: PositiveFloat
|
|
59
|
+
""" ⤷ Capacity (electrical charge) of Storage."""
|
|
60
|
+
p_VOC: Annotated[Sequence[float], Field(min_length=6, max_length=6)] = [0, 0, 0, 1, 0, 0]
|
|
61
|
+
""" ⤷ Parameters for V_OC-Mapping
|
|
62
|
+
- direct SOC-Mapping by default
|
|
63
|
+
- named a0 to a5 in paper
|
|
64
|
+
"""
|
|
65
|
+
p_Rs: Annotated[Sequence[float], Field(min_length=6, max_length=6)] = [0, 0, 0, 0, 0, 0]
|
|
66
|
+
""" ⤷ Parameters for series-resistance
|
|
67
|
+
- no resistance set by default
|
|
68
|
+
- named b0 to b5 in paper
|
|
69
|
+
"""
|
|
70
|
+
p_RtS: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
|
|
71
|
+
""" ⤷ Parameters for R_transient_S (short-term),
|
|
72
|
+
- no transient active by default
|
|
73
|
+
- named c0 to c2 in paper
|
|
74
|
+
"""
|
|
75
|
+
p_CtS: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
|
|
76
|
+
""" ⤷ Parameters for C_transient_S (short-term)
|
|
77
|
+
- no transient active by default
|
|
78
|
+
- named d0 to d2 in paper
|
|
79
|
+
"""
|
|
80
|
+
p_RtL: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
|
|
81
|
+
""" ⤷ Parameters for R_transient_L (long-term)
|
|
82
|
+
- no transient active by default
|
|
83
|
+
- named e0 to e2 in paper
|
|
84
|
+
"""
|
|
85
|
+
p_CtL: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
|
|
86
|
+
""" ⤷ Parameters for C_transient_L (long-term)
|
|
87
|
+
- no transient active by default
|
|
88
|
+
- named f0 to f2 in paper
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
p_rce: Annotated[float, Gt(0), Le(1.0)] = 1.0
|
|
92
|
+
""" ⤷ Parameter for rate capacity effect
|
|
93
|
+
- Set to 1 to disregard
|
|
94
|
+
- named c in paper
|
|
95
|
+
"""
|
|
96
|
+
kdash: PositiveFloat = sys.float_info.min # TODO: use k directly?
|
|
97
|
+
""" ⤷ Parameter for rate capacity effect
|
|
98
|
+
- temporary component of rate capacity effect, valve in KiBaM (eq 17)
|
|
99
|
+
- k' = k/c(1-c),
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
R_leak_Ohm: PositiveFloat = sys.float_info.max
|
|
103
|
+
""" ⤷ Parameter for self discharge (custom extension)
|
|
104
|
+
- effect is often very small, mostly relevant for some capacitors
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
@validate_call
|
|
109
|
+
def lipo(
|
|
110
|
+
cls,
|
|
111
|
+
q_mAh: PositiveFloat, # TODO: charge is more correct
|
|
112
|
+
SoC_init: soc_t | None = None,
|
|
113
|
+
name: str | None = None,
|
|
114
|
+
description: str | None = None,
|
|
115
|
+
) -> Self:
|
|
116
|
+
"""Modeled after the PL-383562 2C Polymer Lithium-ion Battery.
|
|
117
|
+
|
|
118
|
+
Nominal Voltage 3.7 V
|
|
119
|
+
Nominal Capacity 860 mAh
|
|
120
|
+
Discharge Cutoff 3.0 V
|
|
121
|
+
Charge Cutoff 4.2 V
|
|
122
|
+
Max Discharge 2 C / 1.72 A
|
|
123
|
+
https://www.batteryspace.com/prod-specs/pl383562.pdf
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
name_lmnts: list[str] = ["LiPo", f"{q_mAh:.0f}mAh", "3.7V"]
|
|
127
|
+
if name is not None:
|
|
128
|
+
name_lmnts = [name] # specific name overrules params
|
|
129
|
+
if description is None:
|
|
130
|
+
description = "Model of a LiPo battery (3 to 4.2 V) with adjustable capacity"
|
|
131
|
+
return cls(
|
|
132
|
+
SoC_init=SoC_init if SoC_init else cls.model_fields["SoC_init"].default,
|
|
133
|
+
q_As=q_mAh * 3600 / 1000,
|
|
134
|
+
p_VOC=[-0.852, 63.867, 3.6297, 0.559, 0.51, 0.508],
|
|
135
|
+
p_Rs=[0.1463, 30.27, 0.1037, 0.0584, 0.1747, 0.1288],
|
|
136
|
+
p_RtS=[0.1063, 62.49, 0.0437],
|
|
137
|
+
p_CtS=[-200, 138, 300], # most likely a mistake (d1=-138) in the table/paper!
|
|
138
|
+
p_RtL=[0.0712, 61.4, 0.0288],
|
|
139
|
+
p_CtL=[-3083, 180, 5088],
|
|
140
|
+
# y10 = 2863.3, y20 = 232.66 # unused
|
|
141
|
+
p_rce=0.9248,
|
|
142
|
+
kdash=0.0008,
|
|
143
|
+
R_leak_Ohm=55.9e6 / q_mAh, # from wiki ~ 5 % discharge/month
|
|
144
|
+
# content-fields below
|
|
145
|
+
name="_".join(name_lmnts),
|
|
146
|
+
description=description,
|
|
147
|
+
owner="NES Lab",
|
|
148
|
+
group="NES Lab",
|
|
149
|
+
visible2group=True,
|
|
150
|
+
visible2all=True,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
@validate_call
|
|
155
|
+
def lead_acid(
|
|
156
|
+
cls,
|
|
157
|
+
q_mAh: PositiveFloat,
|
|
158
|
+
SoC_init: soc_t | None = None,
|
|
159
|
+
name: str | None = None,
|
|
160
|
+
description: str | None = None,
|
|
161
|
+
) -> Self:
|
|
162
|
+
"""Modeled after the LEOCH LP12-1.2AH lead acid battery.
|
|
163
|
+
|
|
164
|
+
Nominal Voltage 12 V (6 Cell)
|
|
165
|
+
Nominal Capacity 1.2 Ah
|
|
166
|
+
Discharge Cutoff 10.8 V
|
|
167
|
+
Charge Cutoff 13.5 V
|
|
168
|
+
Max Discharge 15 C / 18 A
|
|
169
|
+
https://www.leoch.com/pdf/reserve-power/agm-vrla/lp-general/LP12-1.2.pdf
|
|
170
|
+
# NOTE: 1 cell has 2.1 V nom, cell-count as param?
|
|
171
|
+
# NOTE: add temperature-component? -5mV/cell/K, also capacity-decrease
|
|
172
|
+
"""
|
|
173
|
+
name_lmnts: list[str] = ["Lead-Acid", f"{q_mAh:.0f}mAh", "12V"]
|
|
174
|
+
if name is not None:
|
|
175
|
+
name_lmnts = [name] # specific name overrules params
|
|
176
|
+
if description is None:
|
|
177
|
+
description = "Model of a 12V lead acid battery with adjustable capacity"
|
|
178
|
+
return cls(
|
|
179
|
+
SoC_init=SoC_init if SoC_init else cls.model_fields["SoC_init"].default,
|
|
180
|
+
q_As=q_mAh * 3600 / 1000,
|
|
181
|
+
p_VOC=[5.429, 117.5, 11.32, 2.706, 2.04, 1.026],
|
|
182
|
+
p_Rs=[1.578, 8.527, 0.7808, -1.887, -2.404, -0.649],
|
|
183
|
+
p_RtS=[2.771, 9.079, 0.22],
|
|
184
|
+
p_CtS=[-2423, 75.14, 55],
|
|
185
|
+
p_RtL=[2.771, 9.079, 0.218],
|
|
186
|
+
# ⤷ first 2 values of p_RtL are identical with p_RtS in table/paper
|
|
187
|
+
# (strange, but plots look fine)
|
|
188
|
+
p_CtL=[-1240, 9.571, 3100],
|
|
189
|
+
# y10 = 2592, y20 = 1728 # unused
|
|
190
|
+
p_rce=0.6,
|
|
191
|
+
kdash=0.0034,
|
|
192
|
+
R_leak_Ohm=174e6 / q_mAh,
|
|
193
|
+
# ⤷ from datasheet - 3-20 % discharge/month for 1.2 Ah, here 5%
|
|
194
|
+
# content-fields below
|
|
195
|
+
name="_".join(name_lmnts),
|
|
196
|
+
description=description,
|
|
197
|
+
owner="NES Lab",
|
|
198
|
+
group="NES Lab",
|
|
199
|
+
visible2group=True,
|
|
200
|
+
visible2all=True,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
@validate_call
|
|
205
|
+
def capacitor(
|
|
206
|
+
cls,
|
|
207
|
+
C_uF: PositiveFloat,
|
|
208
|
+
V_rated: PositiveFloat,
|
|
209
|
+
SoC_init: soc_t | None = None,
|
|
210
|
+
R_series_Ohm: NonNegativeFloat | None = None,
|
|
211
|
+
R_leak_Ohm: PositiveFloat | None = None,
|
|
212
|
+
name: str | None = None,
|
|
213
|
+
description: str | None = None,
|
|
214
|
+
) -> Self:
|
|
215
|
+
name_lmnts: list[str] = ["Capacitor", f"{C_uF:.0f}uF", f"{V_rated:.1f}V"]
|
|
216
|
+
if name is not None:
|
|
217
|
+
name_lmnts = [name] # specific name overrules params
|
|
218
|
+
if description is None:
|
|
219
|
+
description = "Model of an Capacitor with various effects"
|
|
220
|
+
if R_leak_Ohm is not None:
|
|
221
|
+
description += f", R_leak = {R_leak_Ohm / 1e3:.3f} Ohm"
|
|
222
|
+
if R_series_Ohm is not None:
|
|
223
|
+
description += f", R_serial = {R_series_Ohm:.0f} Ohm"
|
|
224
|
+
return cls(
|
|
225
|
+
SoC_init=SoC_init if SoC_init else cls.model_fields["SoC_init"].default,
|
|
226
|
+
q_As=1e-6 * C_uF * V_rated,
|
|
227
|
+
p_VOC=[0, 0, 0, V_rated, 0, 0], # 100% SoC is @V_rated,
|
|
228
|
+
# no transients per default
|
|
229
|
+
p_Rs=[0, 0, R_series_Ohm, 0, 0, 0]
|
|
230
|
+
if R_series_Ohm
|
|
231
|
+
else cls.model_fields["p_Rs"].default, # const series resistance
|
|
232
|
+
R_leak_Ohm=R_leak_Ohm if R_leak_Ohm else cls.model_fields["R_leak_Ohm"].default,
|
|
233
|
+
# content-fields below
|
|
234
|
+
name="_".join(name_lmnts),
|
|
235
|
+
description=description,
|
|
236
|
+
owner="NES Lab",
|
|
237
|
+
group="NES Lab",
|
|
238
|
+
visible2group=True,
|
|
239
|
+
visible2all=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
@model_validator(mode="before")
|
|
243
|
+
@classmethod
|
|
244
|
+
def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
245
|
+
values, chain = tb_client.try_completing_model(cls.__name__, values)
|
|
246
|
+
values = tb_client.fill_in_user_data(values)
|
|
247
|
+
log.debug("vStorage-Inheritances: %s", chain)
|
|
248
|
+
return values
|
|
249
|
+
|
|
250
|
+
@model_validator(mode="after")
|
|
251
|
+
def post_validation(self) -> Self:
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def without_rate_capacity(self) -> Self:
|
|
255
|
+
model_dict = self.model_dump()
|
|
256
|
+
model_dict["p_rce"] = 1
|
|
257
|
+
model_dict["name"] += " no_rate_cap"
|
|
258
|
+
return type(self)(**model_dict)
|
|
259
|
+
|
|
260
|
+
def without_transient_voltages(self) -> Self:
|
|
261
|
+
model_dict = self.model_dump()
|
|
262
|
+
model_dict["p_RtS"] = [0, 0, 0]
|
|
263
|
+
model_dict["p_CtS"] = [0, 0, 0]
|
|
264
|
+
model_dict["p_RtL"] = [0, 0, 0]
|
|
265
|
+
model_dict["p_CtL"] = [0, 0, 0]
|
|
266
|
+
model_dict["name"] += " no_transient_vs"
|
|
267
|
+
return type(self)(**model_dict)
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
@validate_call
|
|
271
|
+
def calc_k(kdash: PositiveFloat, c: Annotated[float, Gt(0), Le(1)]) -> float:
|
|
272
|
+
"""Translate between k & k'.
|
|
273
|
+
|
|
274
|
+
As explained below equation 4 in paper: k' = k / (c * (c - 1))
|
|
275
|
+
"""
|
|
276
|
+
return kdash * c * (1 - c)
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def kdash_(self) -> float:
|
|
280
|
+
return self.k / (self.p_rce * (self.p_rce - 1))
|
|
281
|
+
|
|
282
|
+
@validate_call
|
|
283
|
+
def calc_R_leak_capacitor(
|
|
284
|
+
self,
|
|
285
|
+
duration: timedelta,
|
|
286
|
+
SoC_final: Annotated[float, Ge(0), Le(1)],
|
|
287
|
+
SoC_0: Annotated[float, Ge(0), Le(1)] = 1.0,
|
|
288
|
+
) -> float:
|
|
289
|
+
# based on capacitor discharge: U(t) = U0 * e ^ (-t/RC)
|
|
290
|
+
# Example: 50mAh; SoC from 100 % to 85 % over 30 days => ~1.8 MOhm
|
|
291
|
+
U0 = self.calc_V_OC(SoC_0)
|
|
292
|
+
Ut = self.calc_V_OC(SoC_final)
|
|
293
|
+
return duration.total_seconds() * U0 / (self.q_As * math.log(U0 / Ut))
|
|
294
|
+
|
|
295
|
+
@validate_call
|
|
296
|
+
def calc_R_leak_battery(
|
|
297
|
+
self,
|
|
298
|
+
duration: timedelta,
|
|
299
|
+
SoC_final: Annotated[float, Ge(0), Le(1)],
|
|
300
|
+
SoC_0: Annotated[float, Ge(0), Le(1)] = 1.0,
|
|
301
|
+
) -> float:
|
|
302
|
+
U0 = self.calc_V_OC(SoC_0)
|
|
303
|
+
U1 = self.calc_V_OC(SoC_final)
|
|
304
|
+
current_A = (SoC_0 - SoC_final) * self.q_As / duration.total_seconds()
|
|
305
|
+
return (U0 + U1) / 2 / current_A
|
|
306
|
+
|
|
307
|
+
def calc_V_OC(self, SoC: float) -> float:
|
|
308
|
+
return (
|
|
309
|
+
self.p_VOC[0] * math.pow(math.e, -self.p_VOC[1] * SoC)
|
|
310
|
+
+ self.p_VOC[2]
|
|
311
|
+
+ self.p_VOC[3] * SoC
|
|
312
|
+
- self.p_VOC[4] * SoC**2
|
|
313
|
+
+ self.p_VOC[5] * SoC**3
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
@property
|
|
317
|
+
def capacity_in_uF(self) -> float:
|
|
318
|
+
return 1e6 * self.q_As / self.calc_V_OC(SoC=1.0)
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def charge_in_mAh(self) -> float:
|
|
322
|
+
return 1e3 * self.q_As / 3600
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def V_init(self) -> float:
|
|
326
|
+
return self.calc_V_OC(SoC=self.SoC_init)
|
|
327
|
+
|
|
328
|
+
def calc_R_series(self, SoC: float) -> float:
|
|
329
|
+
return (
|
|
330
|
+
self.p_Rs[0] * math.pow(math.e, -self.p_Rs[1] * SoC)
|
|
331
|
+
+ self.p_Rs[2]
|
|
332
|
+
+ self.p_Rs[3] * SoC
|
|
333
|
+
- self.p_Rs[4] * SoC**2
|
|
334
|
+
+ self.p_Rs[5] * SoC**3
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
def calc_R_transient_S(self, SoC: float) -> float:
|
|
338
|
+
return self.p_RtS[0] * math.pow(math.e, -self.p_RtS[1] * SoC) + self.p_RtS[2]
|
|
339
|
+
|
|
340
|
+
def calc_C_transient_S(self, SoC: float) -> float:
|
|
341
|
+
return self.p_CtS[0] * math.pow(math.e, -self.p_CtS[1] * SoC) + self.p_CtS[2]
|
|
342
|
+
|
|
343
|
+
def calc_R_transient_L(self, SoC: float) -> float:
|
|
344
|
+
return self.p_RtL[0] * math.pow(math.e, -self.p_RtL[1] * SoC) + self.p_RtL[2]
|
|
345
|
+
|
|
346
|
+
def calc_C_transient_L(self, SoC: float) -> float:
|
|
347
|
+
return self.p_CtL[0] * math.pow(math.e, -self.p_CtL[1] * SoC) + self.p_CtL[2]
|
|
348
|
+
|
|
349
|
+
def approximate_SoC(self, V_OC: float) -> float:
|
|
350
|
+
SoC_next = SoC_now = 0.5
|
|
351
|
+
step_size = 0.05
|
|
352
|
+
go_up = True
|
|
353
|
+
match = 5
|
|
354
|
+
counter = 0
|
|
355
|
+
|
|
356
|
+
while match > 0.001:
|
|
357
|
+
SoC_now = SoC_next
|
|
358
|
+
V_OC_now = self.calc_V_OC(SoC_now)
|
|
359
|
+
match_new = abs(V_OC_now / V_OC - 1)
|
|
360
|
+
if match_new > match:
|
|
361
|
+
go_up = not go_up
|
|
362
|
+
if go_up:
|
|
363
|
+
step_size /= 2
|
|
364
|
+
SoC_next += step_size if go_up else -step_size
|
|
365
|
+
match = match_new
|
|
366
|
+
counter += 1
|
|
367
|
+
if counter > 100:
|
|
368
|
+
raise RuntimeError("Could not approximate SoC for given Voltage")
|
|
369
|
+
|
|
370
|
+
return SoC_now
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# constants & custom types
|
|
374
|
+
TIMESTEP_s_DEFAULT: float = 1.0 / config.SAMPLERATE_SPS
|
|
375
|
+
LuT_SIZE_LOG: int = 7
|
|
376
|
+
LuT_SIZE: int = 2**LuT_SIZE_LOG
|
|
377
|
+
u32 = Annotated[int, Field(ge=0, lt=2**32)]
|
|
378
|
+
lut_storage = Annotated[list[u32], Field(min_length=LuT_SIZE, max_length=LuT_SIZE)]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class StoragePRUConfig(ShpModel):
|
|
382
|
+
"""Map settings-list to internal state-vars struct StorageConfig.
|
|
383
|
+
|
|
384
|
+
NOTE:
|
|
385
|
+
- yaml is based on si-units like nA, mV, ms, uF
|
|
386
|
+
- c-code and py-copy is using nA, uV, ns, nF, fW, raw
|
|
387
|
+
- ordering is intentional and in sync with shepherd/commons.h
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
SoC_init_1_n30: u32
|
|
391
|
+
""" ⤷ initial charge of storage """
|
|
392
|
+
Constant_1_per_nA_n60: u32
|
|
393
|
+
""" ⤷ Convert I_charge to delta-SoC with one multiplication."""
|
|
394
|
+
Constant_1_per_uV_n60: u32
|
|
395
|
+
""" ⤷ Leakage - Convert V_OC to delta-SoC with one multiplication.
|
|
396
|
+
Combines prior constant and R_leak, to maximize resolution.
|
|
397
|
+
"""
|
|
398
|
+
LuT_VOC_uV_n8: lut_storage
|
|
399
|
+
"""⤷ ranges from 3.9 uV to 16.7 V"""
|
|
400
|
+
LuT_RSeries_kOhm_n32: lut_storage
|
|
401
|
+
"""⤷ ranges from 233n to 1 kOhm"""
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
@validate_call
|
|
405
|
+
def from_vstorage(
|
|
406
|
+
cls,
|
|
407
|
+
data: VirtualStorageConfig | None,
|
|
408
|
+
dt_s: PositiveFloat = TIMESTEP_s_DEFAULT,
|
|
409
|
+
*,
|
|
410
|
+
optimize_clamp: bool = True,
|
|
411
|
+
) -> Self:
|
|
412
|
+
x_off = 0.5 if optimize_clamp else 0.0
|
|
413
|
+
SoC_min = 1.0 / LuT_SIZE
|
|
414
|
+
if data is None:
|
|
415
|
+
data = VirtualStorageConfig.capacitor(C_uF=100, V_rated=10)
|
|
416
|
+
V_OC_LuT = [data.calc_V_OC(SoC_min * (x + x_off)) for x in range(LuT_SIZE)]
|
|
417
|
+
R_series_LuT = [data.calc_R_series(SoC_min * (x + x_off)) for x in range(LuT_SIZE)]
|
|
418
|
+
Constant_1_per_A: float = dt_s / data.q_As
|
|
419
|
+
Constant_1_per_V: float = Constant_1_per_A / data.R_leak_Ohm
|
|
420
|
+
return cls(
|
|
421
|
+
SoC_init_1_n30=round(2**30 * data.SoC_init),
|
|
422
|
+
Constant_1_per_nA_n60=round((2**60 / 1e9) * Constant_1_per_A),
|
|
423
|
+
Constant_1_per_uV_n60=round((2**60 / 1e6) * Constant_1_per_V),
|
|
424
|
+
LuT_VOC_uV_n8=[round((2**8 * 1e6) * y) for y in V_OC_LuT],
|
|
425
|
+
LuT_RSeries_kOhm_n32=[round((2**32 * 1e-3) * y) for y in R_series_LuT],
|
|
426
|
+
)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Script for generating YAML-fixture for virtual storage."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from virtual_storage_config import VirtualStorageConfig
|
|
8
|
+
|
|
9
|
+
from shepherd_core import local_now
|
|
10
|
+
from shepherd_core.data_models import Wrapper
|
|
11
|
+
from shepherd_core.logger import log
|
|
12
|
+
|
|
13
|
+
dsc_ideal = "Model of an ideal Capacitor (true to spec, no losses)"
|
|
14
|
+
dsc_tantal = "Tantal-Capacitor similar to ideal Model, but with R_leak & R_series"
|
|
15
|
+
dsc_mlcc = "MLCC-Capacitor with R_leak & R_series and planned DC-Bias-Effect"
|
|
16
|
+
dsc_super = "SuperCapacitor with typically 1000 hours / 500 k cycles (not modeled)"
|
|
17
|
+
|
|
18
|
+
# Ideal Capacitor, E6 row 10 to 1000 uF
|
|
19
|
+
# typical voltage-ratings: 2.5, 4.0, 6.3, 10, 16, 20 V
|
|
20
|
+
E6: list[int] = [10, 15, 22, 33, 47, 68, 100, 150, 220, 330, 470, 680, 1000]
|
|
21
|
+
fixture_ideal: list[VirtualStorageConfig] = [
|
|
22
|
+
VirtualStorageConfig.capacitor(C_uF=_v, V_rated=10.0, description=dsc_ideal) for _v in E6
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Tantal Capacitors, E6 row
|
|
26
|
+
# ⤷ verified with AVX TAJB107M006RNJ
|
|
27
|
+
# - Tantal, 100 uF, 6V3, 20%, 1411 package -> 100 uF measured
|
|
28
|
+
# - R_series taken from Datasheet
|
|
29
|
+
# - R_leak = 1 MOhm in datasheet (6.3V/6.3uA), but ~2 MOhm in experiment
|
|
30
|
+
# see https://github.com/orgua/bq_characteristics/tree/main/eval_kit_behavior_var1#capacitor
|
|
31
|
+
fixture_tantal: list[VirtualStorageConfig] = [
|
|
32
|
+
VirtualStorageConfig.capacitor(
|
|
33
|
+
C_uF=10,
|
|
34
|
+
V_rated=6.3,
|
|
35
|
+
R_leak_Ohm=196e6 / 10,
|
|
36
|
+
R_series_Ohm=3.0,
|
|
37
|
+
name="AVX TAJB106x006", # Note: small x is * in datasheet
|
|
38
|
+
description=dsc_tantal,
|
|
39
|
+
),
|
|
40
|
+
VirtualStorageConfig.capacitor(
|
|
41
|
+
C_uF=15,
|
|
42
|
+
V_rated=6.3,
|
|
43
|
+
R_leak_Ohm=196e6 / 15,
|
|
44
|
+
R_series_Ohm=2.0,
|
|
45
|
+
name="AVX TAJB156x006",
|
|
46
|
+
description=dsc_tantal,
|
|
47
|
+
),
|
|
48
|
+
VirtualStorageConfig.capacitor(
|
|
49
|
+
C_uF=22,
|
|
50
|
+
V_rated=6.3,
|
|
51
|
+
R_leak_Ohm=196e6 / 22,
|
|
52
|
+
R_series_Ohm=2.5,
|
|
53
|
+
name="AVX TAJB226x006",
|
|
54
|
+
description=dsc_tantal,
|
|
55
|
+
),
|
|
56
|
+
VirtualStorageConfig.capacitor(
|
|
57
|
+
C_uF=33,
|
|
58
|
+
V_rated=6.3,
|
|
59
|
+
R_leak_Ohm=196e6 / 33,
|
|
60
|
+
R_series_Ohm=2.2,
|
|
61
|
+
name="AVX TAJB336x006",
|
|
62
|
+
description=dsc_tantal,
|
|
63
|
+
),
|
|
64
|
+
VirtualStorageConfig.capacitor(
|
|
65
|
+
C_uF=47,
|
|
66
|
+
V_rated=6.3,
|
|
67
|
+
R_leak_Ohm=196e6 / 47,
|
|
68
|
+
R_series_Ohm=2,
|
|
69
|
+
name="AVX TAJB476x006",
|
|
70
|
+
description=dsc_tantal,
|
|
71
|
+
),
|
|
72
|
+
VirtualStorageConfig.capacitor(
|
|
73
|
+
C_uF=68,
|
|
74
|
+
V_rated=6.3,
|
|
75
|
+
R_leak_Ohm=196e6 / 68,
|
|
76
|
+
R_series_Ohm=0.9,
|
|
77
|
+
name="AVX TAJB686x006",
|
|
78
|
+
description=dsc_tantal,
|
|
79
|
+
),
|
|
80
|
+
VirtualStorageConfig.capacitor(
|
|
81
|
+
C_uF=100,
|
|
82
|
+
V_rated=6.3,
|
|
83
|
+
R_leak_Ohm=196e6 / 100,
|
|
84
|
+
R_series_Ohm=1.7,
|
|
85
|
+
name="AVX TAJB107x006",
|
|
86
|
+
description=dsc_tantal,
|
|
87
|
+
),
|
|
88
|
+
VirtualStorageConfig.capacitor(
|
|
89
|
+
C_uF=150,
|
|
90
|
+
V_rated=6.3,
|
|
91
|
+
R_leak_Ohm=196e6 / 150,
|
|
92
|
+
R_series_Ohm=1.3,
|
|
93
|
+
name="AVX TAJC157x006",
|
|
94
|
+
description=dsc_tantal,
|
|
95
|
+
),
|
|
96
|
+
VirtualStorageConfig.capacitor(
|
|
97
|
+
C_uF=220,
|
|
98
|
+
V_rated=6.3,
|
|
99
|
+
R_leak_Ohm=196e6 / 220,
|
|
100
|
+
R_series_Ohm=1.2,
|
|
101
|
+
name="AVX TAJC227x006",
|
|
102
|
+
description=dsc_tantal,
|
|
103
|
+
),
|
|
104
|
+
VirtualStorageConfig.capacitor(
|
|
105
|
+
C_uF=330,
|
|
106
|
+
V_rated=6.3,
|
|
107
|
+
R_leak_Ohm=196e6 / 330,
|
|
108
|
+
R_series_Ohm=0.5,
|
|
109
|
+
name="AVX TAJC337x006",
|
|
110
|
+
description=dsc_tantal,
|
|
111
|
+
),
|
|
112
|
+
VirtualStorageConfig.capacitor(
|
|
113
|
+
C_uF=470,
|
|
114
|
+
V_rated=6.3,
|
|
115
|
+
R_leak_Ohm=196e6 / 470,
|
|
116
|
+
R_series_Ohm=0.4,
|
|
117
|
+
name="AVX TAJD477x006",
|
|
118
|
+
description=dsc_tantal,
|
|
119
|
+
),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
# MLCC
|
|
123
|
+
# ⤷ verified with Taiyo Yuden JMK316ABJ107ML-T
|
|
124
|
+
# - MLCC, 100uF, 6V3, 20%, X5R, 1206 package -> 74 uF measured
|
|
125
|
+
# - Insulation Resistance (min) 100 MΩ·μF (datasheet), 97.8 MΩ measured
|
|
126
|
+
# https://github.com/orgua/bq_characteristics/tree/main/eval_kit_behavior_var1/data_capacitor
|
|
127
|
+
# BQ25570EVM uses Murata GRM43SR60J107ME20L
|
|
128
|
+
# - MLCC, 100uF, 6V3, 20%, X5R, 1812 package -> 79 uF measured
|
|
129
|
+
# - murata-DB does not know this part, but direct substitute is GRM31CR60J107MEA8
|
|
130
|
+
# https://pim.murata.com/en-global/pim/details/?partNum=GRM31CR60J107MEA8%23
|
|
131
|
+
# TODO: add DC-Bias
|
|
132
|
+
fixture_mlcc: list[VirtualStorageConfig] = [
|
|
133
|
+
VirtualStorageConfig.capacitor(
|
|
134
|
+
C_uF=10, V_rated=6.3, R_leak_Ohm=97.8e6 / 10, description=dsc_mlcc
|
|
135
|
+
),
|
|
136
|
+
VirtualStorageConfig.capacitor(
|
|
137
|
+
C_uF=33, V_rated=6.3, R_leak_Ohm=97.8e6 / 33, description=dsc_mlcc
|
|
138
|
+
),
|
|
139
|
+
VirtualStorageConfig.capacitor(
|
|
140
|
+
C_uF=47, V_rated=6.3, R_leak_Ohm=97.8e6 / 47, description=dsc_mlcc
|
|
141
|
+
),
|
|
142
|
+
VirtualStorageConfig.capacitor(
|
|
143
|
+
C_uF=100, V_rated=6.3, R_leak_Ohm=97.8e6 / 100, description=dsc_mlcc
|
|
144
|
+
),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
# SuperCap
|
|
148
|
+
fixture_super: list[VirtualStorageConfig] = [
|
|
149
|
+
VirtualStorageConfig.capacitor(
|
|
150
|
+
C_uF=25e6,
|
|
151
|
+
V_rated=3.0,
|
|
152
|
+
R_leak_Ohm=3 / 55e-6,
|
|
153
|
+
R_series_Ohm=17e-3,
|
|
154
|
+
name="Maxwell BCAP0025 P300 X11",
|
|
155
|
+
description=dsc_super,
|
|
156
|
+
),
|
|
157
|
+
VirtualStorageConfig.capacitor(
|
|
158
|
+
C_uF=12e6,
|
|
159
|
+
V_rated=6.0,
|
|
160
|
+
R_leak_Ohm=6 / 80e-6,
|
|
161
|
+
R_series_Ohm=90e-3,
|
|
162
|
+
name="Abracon ADCM-S06R0SA126RB",
|
|
163
|
+
description=dsc_super,
|
|
164
|
+
),
|
|
165
|
+
VirtualStorageConfig.capacitor(
|
|
166
|
+
C_uF=7.5e6,
|
|
167
|
+
V_rated=5.5,
|
|
168
|
+
R_leak_Ohm=6 / 78e-6,
|
|
169
|
+
R_series_Ohm=90e-3,
|
|
170
|
+
name="AVX SCMT32F755SRBA0 ",
|
|
171
|
+
description=dsc_super,
|
|
172
|
+
),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
fixture_lipo: list[VirtualStorageConfig] = [
|
|
176
|
+
# LiPo-Coin-cells 5.4mm, https://www.lipobatteries.net/3-8v-rechargeable-mini-button-lipo-coin-cell-battery/
|
|
177
|
+
VirtualStorageConfig.lipo(
|
|
178
|
+
q_mAh=80, name="LPM1254", description="LiPo-Coin-Cell 80mAh, 1 cell, w=12mm, d=5.4mm"
|
|
179
|
+
),
|
|
180
|
+
VirtualStorageConfig.lipo(
|
|
181
|
+
q_mAh=65, name="LPM1154", description="LiPo-Coin-Cell 65mAh, 1 cell, w=11mm, d=5.4mm"
|
|
182
|
+
),
|
|
183
|
+
VirtualStorageConfig.lipo(
|
|
184
|
+
q_mAh=50, name="LPM1054", description="LiPo-Coin-Cell 50mAh, 1 cell, w=10mm, d=5.4mm"
|
|
185
|
+
),
|
|
186
|
+
VirtualStorageConfig.lipo(
|
|
187
|
+
q_mAh=40, name="LPM0954", description="LiPo-Coin-Cell 40mAh, 1 cell, w=9mm, d=5.4mm"
|
|
188
|
+
),
|
|
189
|
+
VirtualStorageConfig.lipo(
|
|
190
|
+
q_mAh=35, name="LPM0854", description="LiPo-Coin-Cell 35mAh, 1 cell, w=8mm, d=5.4mm"
|
|
191
|
+
),
|
|
192
|
+
# LiPo-Coin-cells 4.0mm, https://www.lipobatteries.net/3-8v-rechargeable-mini-button-lipo-coin-cell-battery/
|
|
193
|
+
VirtualStorageConfig.lipo(
|
|
194
|
+
q_mAh=55, name="LPM1240", description="LiPo-Coin-Cell 55mAh, 1 cell, w=12mm, d=4.0mm"
|
|
195
|
+
),
|
|
196
|
+
VirtualStorageConfig.lipo(
|
|
197
|
+
q_mAh=45, name="LPM1140", description="LiPo-Coin-Cell 45mAh, 1 cell, w=11mm, d=4.0mm"
|
|
198
|
+
),
|
|
199
|
+
VirtualStorageConfig.lipo(
|
|
200
|
+
q_mAh=35, name="LPM1040", description="LiPo-Coin-Cell 35mAh, 1 cell, w=10mm, d=4.0mm"
|
|
201
|
+
),
|
|
202
|
+
VirtualStorageConfig.lipo(
|
|
203
|
+
q_mAh=30, name="LPM0940", description="LiPo-Coin-Cell 30mAh, 1 cell, w=9mm, d=4.0mm"
|
|
204
|
+
),
|
|
205
|
+
VirtualStorageConfig.lipo(
|
|
206
|
+
q_mAh=18, name="LPM0840", description="LiPo-Coin-Cell 18mAh, 1 cell, w=8mm, d=4.0mm"
|
|
207
|
+
),
|
|
208
|
+
# small LiPos, https://www.lipobatteries.net/lipo-batteries-within-100mah/
|
|
209
|
+
VirtualStorageConfig.lipo(
|
|
210
|
+
q_mAh=12, name="LP151020", description="LiPo-Pouch 12mAh, 1 cell, 20x10x1.5mm"
|
|
211
|
+
),
|
|
212
|
+
VirtualStorageConfig.lipo(
|
|
213
|
+
q_mAh=15, name="LP251212", description="LiPo-Pouch 15mAh, 1 cell, 12x12x2.5mm"
|
|
214
|
+
),
|
|
215
|
+
VirtualStorageConfig.lipo(
|
|
216
|
+
q_mAh=22, name="LP500522", description="LiPo-Pouch 20mAh, 1 cell, 22x05x5.0mm"
|
|
217
|
+
),
|
|
218
|
+
VirtualStorageConfig.lipo(
|
|
219
|
+
q_mAh=22, name="LP271015", description="LiPo-Pouch 22mAh, 1 cell, 15x10x2.7mm"
|
|
220
|
+
),
|
|
221
|
+
# Example from the paper
|
|
222
|
+
VirtualStorageConfig.lipo(q_mAh=860, name="PL-383562"),
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
fixture_lead: list[VirtualStorageConfig] = [
|
|
226
|
+
# Example from the paper
|
|
227
|
+
VirtualStorageConfig.lead_acid(q_mAh=1200, name="LEOCH_LP12-1.2AH"),
|
|
228
|
+
]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
path_here = Path(__file__).parent.absolute()
|
|
233
|
+
path_db = path_here # .parent / "shepherd_core/shepherd_core/data_models/content"
|
|
234
|
+
|
|
235
|
+
if not path_db.exists() or not path_db.is_dir():
|
|
236
|
+
log.error("Path to db must exist and be a directory!")
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
fixtures: dict[str, list[VirtualStorageConfig]] = {
|
|
240
|
+
"ideal": fixture_ideal,
|
|
241
|
+
"tantal": fixture_tantal,
|
|
242
|
+
"mlcc": fixture_mlcc,
|
|
243
|
+
"super": fixture_super,
|
|
244
|
+
"lipo": fixture_lipo,
|
|
245
|
+
"lead": fixture_lead,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for name, fixture in fixtures.items():
|
|
249
|
+
file_path = path_db / f"virtual_storage_fixture_{name}.yaml"
|
|
250
|
+
if file_path.exists():
|
|
251
|
+
log.warning("File %s already exists! -> will skip", file_path.name)
|
|
252
|
+
models_wrap = []
|
|
253
|
+
for model in fixture:
|
|
254
|
+
model_dict = (
|
|
255
|
+
model.model_dump()
|
|
256
|
+
) # exclude_unset=True, exclude_defaults=True, include={"id",})
|
|
257
|
+
model_wrap = Wrapper(
|
|
258
|
+
datatype=type(model).__name__,
|
|
259
|
+
created=local_now(),
|
|
260
|
+
comment=f"created by script '{Path(__file__).name}'",
|
|
261
|
+
parameters=model_dict,
|
|
262
|
+
)
|
|
263
|
+
models_wrap.append(model_wrap.model_dump(exclude_unset=True, exclude_defaults=True))
|
|
264
|
+
|
|
265
|
+
models_yaml = yaml.safe_dump(models_wrap, default_flow_style=False, sort_keys=False)
|
|
266
|
+
with file_path.open("w") as f:
|
|
267
|
+
f.write(models_yaml)
|