shepherd-core 2025.6.4__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.
Files changed (45) hide show
  1. shepherd_core/data_models/__init__.py +4 -2
  2. shepherd_core/data_models/base/content.py +2 -0
  3. shepherd_core/data_models/content/__init__.py +4 -2
  4. shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +3 -3
  5. shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +82 -58
  6. shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
  7. shepherd_core/data_models/content/virtual_storage_config.py +426 -0
  8. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
  9. shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
  10. shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
  11. shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
  12. shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
  13. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
  14. shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
  15. shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
  16. shepherd_core/data_models/experiment/observer_features.py +8 -2
  17. shepherd_core/data_models/experiment/target_config.py +1 -1
  18. shepherd_core/data_models/task/emulation.py +9 -6
  19. shepherd_core/data_models/task/firmware_mod.py +1 -0
  20. shepherd_core/data_models/task/harvest.py +4 -4
  21. shepherd_core/data_models/task/observer_tasks.py +5 -2
  22. shepherd_core/data_models/task/programming.py +1 -0
  23. shepherd_core/data_models/task/testbed_tasks.py +6 -1
  24. shepherd_core/decoder_waveform/uart.py +2 -1
  25. shepherd_core/fw_tools/patcher.py +60 -34
  26. shepherd_core/fw_tools/validation.py +7 -1
  27. shepherd_core/inventory/system.py +1 -1
  28. shepherd_core/reader.py +4 -3
  29. shepherd_core/version.py +1 -1
  30. shepherd_core/vsource/__init__.py +4 -0
  31. shepherd_core/vsource/virtual_converter_model.py +27 -26
  32. shepherd_core/vsource/virtual_harvester_model.py +27 -19
  33. shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
  34. shepherd_core/vsource/virtual_source_model.py +17 -13
  35. shepherd_core/vsource/virtual_source_simulation.py +71 -73
  36. shepherd_core/vsource/virtual_storage_model.py +164 -0
  37. shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
  38. shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
  39. shepherd_core/vsource/virtual_storage_simulator.py +104 -0
  40. {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/METADATA +4 -6
  41. {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/RECORD +44 -32
  42. shepherd_core/data_models/virtual_source_doc.txt +0 -207
  43. {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/WHEEL +0 -0
  44. {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/top_level.txt +0 -0
  45. {shepherd_core-2025.6.4.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)