shepherd-core 2025.8.1__py3-none-any.whl → 2026.2.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 (82) hide show
  1. shepherd_core/config.py +1 -1
  2. shepherd_core/data_models/__init__.py +8 -4
  3. shepherd_core/data_models/base/cal_measurement.py +7 -2
  4. shepherd_core/data_models/base/calibration.py +23 -12
  5. shepherd_core/data_models/base/content.py +12 -2
  6. shepherd_core/data_models/base/shepherd.py +13 -4
  7. shepherd_core/data_models/base/wrapper.py +2 -0
  8. shepherd_core/data_models/content/__init__.py +8 -4
  9. shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
  10. shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
  11. shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
  12. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
  13. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
  14. shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
  15. shepherd_core/data_models/content/energy_environment.py +341 -23
  16. shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
  17. shepherd_core/data_models/content/enum_datatypes.py +109 -0
  18. shepherd_core/data_models/content/firmware.py +44 -16
  19. shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
  20. shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
  21. shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
  22. shepherd_core/data_models/content/virtual_storage_config.py +429 -0
  23. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
  24. shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
  25. shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
  26. shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
  27. shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
  28. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
  29. shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
  30. shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
  31. shepherd_core/data_models/experiment/experiment.py +38 -13
  32. shepherd_core/data_models/experiment/observer_features.py +17 -4
  33. shepherd_core/data_models/experiment/target_config.py +56 -8
  34. shepherd_core/data_models/task/__init__.py +13 -2
  35. shepherd_core/data_models/task/emulation.py +10 -6
  36. shepherd_core/data_models/task/firmware_mod.py +3 -1
  37. shepherd_core/data_models/task/harvest.py +3 -1
  38. shepherd_core/data_models/task/helper_paths.py +2 -2
  39. shepherd_core/data_models/task/observer_tasks.py +8 -6
  40. shepherd_core/data_models/task/programming.py +4 -2
  41. shepherd_core/data_models/task/testbed_tasks.py +8 -2
  42. shepherd_core/data_models/testbed/cape.py +2 -0
  43. shepherd_core/data_models/testbed/gpio.py +2 -0
  44. shepherd_core/data_models/testbed/mcu.py +2 -0
  45. shepherd_core/data_models/testbed/observer.py +2 -0
  46. shepherd_core/data_models/testbed/target.py +7 -5
  47. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  48. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  49. shepherd_core/data_models/testbed/testbed.py +17 -15
  50. shepherd_core/decoder_waveform/uart.py +1 -1
  51. shepherd_core/exit_handler.py +22 -0
  52. shepherd_core/fw_tools/converter.py +2 -2
  53. shepherd_core/fw_tools/validation.py +1 -1
  54. shepherd_core/inventory/__init__.py +23 -21
  55. shepherd_core/inventory/system.py +3 -3
  56. shepherd_core/logger.py +0 -1
  57. shepherd_core/reader.py +32 -27
  58. shepherd_core/testbed_client/cache_path.py +3 -3
  59. shepherd_core/testbed_client/client_abc_fix.py +14 -3
  60. shepherd_core/testbed_client/client_web.py +7 -5
  61. shepherd_core/testbed_client/fixtures.py +7 -7
  62. shepherd_core/version.py +1 -1
  63. shepherd_core/vsource/__init__.py +4 -0
  64. shepherd_core/vsource/virtual_converter_model.py +29 -28
  65. shepherd_core/vsource/virtual_harvester_model.py +29 -21
  66. shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
  67. shepherd_core/vsource/virtual_source_model.py +18 -14
  68. shepherd_core/vsource/virtual_source_simulation.py +71 -73
  69. shepherd_core/vsource/virtual_storage_model.py +164 -0
  70. shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
  71. shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
  72. shepherd_core/vsource/virtual_storage_simulator.py +104 -0
  73. shepherd_core/writer.py +16 -9
  74. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
  75. shepherd_core-2026.2.1.dist-info/RECORD +102 -0
  76. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
  77. shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
  78. shepherd_core/data_models/content/firmware_datatype.py +0 -15
  79. shepherd_core/data_models/virtual_source_doc.txt +0 -207
  80. shepherd_core-2025.8.1.dist-info/RECORD +0 -83
  81. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
  82. {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
@@ -23,13 +23,11 @@
23
23
  V_input_drop_mV: 0.0 # simulate input-diode
24
24
  R_input_mOhm: 0.0 # resistance only active with disabled boost, range [1 mOhm; 1MOhm]
25
25
 
26
- C_intermediate_uF: 0.0 # primary storage-Cap
27
- V_intermediate_init_mV: 3000 # allow a proper / fast startup
28
- I_intermediate_leak_nA: 0.0
26
+ storage: null # so storage is disabled (null is None in YAML)
29
27
 
30
28
  # Output-Switch with comparator and hysteresis
31
- V_intermediate_enable_threshold_mV: 1 # -> target gets connected (hysteresis-combo with next value)
32
- V_intermediate_disable_threshold_mV: 0 # -> target gets disconnected
29
+ V_intermediate_enable_output_threshold_mV: 1 # -> target gets connected (hysteresis-combo with next value)
30
+ V_intermediate_disable_output_threshold_mV: 0 # -> target gets disconnected
33
31
  interval_check_thresholds_ms: 0.0 # some BQs check every 64 ms if output should be disconnected
34
32
 
35
33
  # Power-Good signal from comparator and hysteresis
@@ -40,11 +38,11 @@
40
38
  C_output_uF: 1.0 # final (always last) stage to compensate transient current spikes when enabling power for target
41
39
 
42
40
  # Extra
43
- V_output_log_gpio_threshold_mV: 1400 # min voltage needed to enable recording changes in gpio-bank
41
+ V_output_log_gpio_threshold_mV: 1400 # minimum voltage needed to enable recording changes in gpio-bank
44
42
  # TODO: actually disable gpio below that
45
43
 
46
44
  # Boost Converter
47
- V_input_boost_threshold_mV: 0.0 # min input-voltage for the boost converter to work
45
+ V_input_boost_threshold_mV: 0.0 # minimum input-voltage for the boost converter to work
48
46
  V_intermediate_max_mV: 10000 # -> boost converter shuts off
49
47
 
50
48
  LUT_input_efficiency: [
@@ -68,7 +66,7 @@
68
66
 
69
67
  # Buck-converter
70
68
  V_output_mV: 2400
71
- V_buck_drop_mV: 0.0 # simulate LDO min voltage differential or output-diode
69
+ V_buck_drop_mV: 0.0 # simulate LDO minimum voltage differential or output-diode
72
70
 
73
71
  LUT_output_efficiency: [ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00 ] # array[12] depending on output_current
74
72
  LUT_output_I_min_log2_nA: 1 # 2^8 = 256 nA -> array[0] is for inputs < 256 nA, see notes on LUT_input for explanation
@@ -96,12 +94,13 @@
96
94
  description: Simple Converter based on diode and storage capacitor
97
95
  inherit_from: neutral
98
96
  V_input_drop_mV: 300 # simulate input-diode
99
- C_intermediate_uF: 47 # primary storage-Cap
97
+ storage:
98
+ name: Capacitor_47uF_6.3V
100
99
  harvester:
101
100
  name: cv20
102
101
  enable_feedback_to_hrv: true # src can control a cv-harvester for ivcurve
103
- V_intermediate_enable_threshold_mV: 2000
104
- V_intermediate_disable_threshold_mV: 1800 # nRF draw ~0.5 mA below that point
102
+ V_intermediate_enable_output_threshold_mV: 2000
103
+ V_intermediate_disable_output_threshold_mV: 1800 # nRF draw ~0.5 mA below that point
105
104
  # TODO: put switch-output into special nRF Version
106
105
 
107
106
  - datatype: VirtualSourceConfig
@@ -138,12 +137,13 @@
138
137
  V_input_max_mV: 3000
139
138
  I_input_max_mA: 100
140
139
 
141
- C_intermediate_uF: 100.0 # primary storage-Cap
142
- V_intermediate_init_mV: 3000 # allow a proper / fast startup
143
- I_intermediate_leak_nA: 330
140
+ storage:
141
+ name: Capacitor_100uF_6.3V
142
+ SoC_init: 0.66 # allow a proper / fast startup
143
+ R_leak_Ohm: 19.0e6 # ~ 330 nA leakage at 6.3 V
144
144
 
145
- V_intermediate_enable_threshold_mV: 1000 # -> target gets connected (hysteresis-combo with next value)
146
- V_intermediate_disable_threshold_mV: 0 # -> target gets disconnected
145
+ V_intermediate_enable_output_threshold_mV: 1000 # -> target gets connected (hysteresis-combo with next value)
146
+ V_intermediate_disable_output_threshold_mV: 0 # -> target gets disconnected
147
147
  interval_check_thresholds_ms: 64.0 # some BQs check every 64 ms if output should be disconnected
148
148
 
149
149
  V_pwr_good_enable_threshold_mV: 2800 # target is informed by pwr-good on output-pin (hysteresis) -> for intermediate voltage
@@ -151,7 +151,7 @@
151
151
  immediate_pwr_good_signal: false # 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
152
152
 
153
153
  # Boost Converter
154
- V_input_boost_threshold_mV: 130 # min input-voltage for the boost converter to work
154
+ V_input_boost_threshold_mV: 130 # minimum input-voltage for the boost converter to work
155
155
  V_intermediate_max_mV: 3600 # -> boost converter shuts off
156
156
 
157
157
  LUT_input_efficiency: [
@@ -198,12 +198,12 @@
198
198
  V_input_max_mV: 5100
199
199
  I_input_max_mA: 100
200
200
 
201
- C_intermediate_uF: 33.0 # primary storage-Cap
202
- V_intermediate_init_mV: 3400 # allow a proper / fast startup
203
- I_intermediate_leak_nA: 0.0
201
+ storage:
202
+ name: Capacitor_100uF_6.3V
203
+ SoC_init: 0.66 # allow a proper / fast startup
204
204
 
205
- V_intermediate_enable_threshold_mV: 3000 # -> target gets connected (hysteresis-combo with next value)
206
- V_intermediate_disable_threshold_mV: 2400 # -> target gets disconnected
205
+ V_intermediate_enable_output_threshold_mV: 3000 # -> target gets connected (hysteresis-combo with next value)
206
+ V_intermediate_disable_output_threshold_mV: 2400 # -> target gets disconnected
207
207
  interval_check_thresholds_ms: 64.0 # some BQs check every 64 ms if output should be disconnected
208
208
 
209
209
  V_pwr_good_enable_threshold_mV: 3000 # target is informed by pwr-good on output-pin (hysteresis) -> for intermediate voltage
@@ -213,12 +213,12 @@
213
213
  C_output_uF: 1.0 # final (always last) stage to compensate undetectable current spikes when enabling power for target
214
214
 
215
215
  # Boost Converter
216
- V_input_boost_threshold_mV: 100.0 # min input-voltage for the boost converter to work
216
+ V_input_boost_threshold_mV: 100.0 # minimum input-voltage for the boost converter to work
217
217
  V_intermediate_max_mV: 5500 # -> boost converter shuts off
218
218
 
219
219
  # Buck Converter
220
220
  V_output_mV: 2200
221
- V_buck_drop_mV: 200.0 # simulate LDO min voltage differential or output-diode
221
+ V_buck_drop_mV: 200.0 # simulate LDO minimum voltage differential or output-diode
222
222
 
223
223
  # <1u 1u 2u 4u 8u 16u 32u 64u 128u 256u 512u >1m
224
224
  LUT_output_efficiency: [ 0.40, 0.50, 0.60, 0.73, 0.82, 0.86, 0.88, 0.90, 0.91, 0.92, 0.93, 0.92] # array[12] depending on output_current
@@ -0,0 +1,429 @@
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
+ from typing import final
20
+
21
+ from annotated_types import Ge
22
+ from annotated_types import Gt
23
+ from annotated_types import Le
24
+ from pydantic import Field
25
+ from pydantic import NonNegativeFloat
26
+ from pydantic import PositiveFloat
27
+ from pydantic import model_validator
28
+ from pydantic import validate_call
29
+ from typing_extensions import Self
30
+
31
+ from shepherd_core.config import config
32
+ from shepherd_core.data_models.base.content import ContentModel
33
+ from shepherd_core.data_models.base.shepherd import ShpModel
34
+ from shepherd_core.logger import log
35
+ from shepherd_core.testbed_client import tb_client
36
+
37
+ soc_t = Annotated[float, Ge(0.0), Le(1.0)]
38
+ # TODO: adapt V_max in vsrc,
39
+ # TODO: do we need to set initial voltage, or is SoC ok? add V_OC_to_SoC()
40
+
41
+
42
+ @final
43
+ class VirtualStorageConfig(ContentModel, title="Config for the virtual energy storage"):
44
+ """KiBaM Battery model based on two papers.
45
+
46
+ Model An Accurate Electrical Battery Model Capable
47
+ of Predicting Runtime and I-V Performance
48
+ https://rincon-mora.gatech.edu/publicat/jrnls/tec05_batt_mdl.pdf
49
+
50
+ A Hybrid Battery Model Capable of Capturing Dynamic Circuit
51
+ Characteristics and Nonlinear Capacity Effects
52
+ https://digitalcommons.unl.edu/cgi/viewcontent.cgi?article=1210&context=electricalengineeringfacpub
53
+ """
54
+
55
+ SoC_init: soc_t = 0.8
56
+ """ ⤷ State of Charge that is available when emulation starts.
57
+ Allows a proper / fast startup.
58
+ """
59
+
60
+ q_As: PositiveFloat
61
+ """ ⤷ Capacity (electrical charge) of Storage."""
62
+ p_VOC: Annotated[Sequence[float], Field(min_length=6, max_length=6)] = [0, 0, 0, 1, 0, 0]
63
+ """ ⤷ Parameters for V_OC-Mapping
64
+ - direct SOC-Mapping by default
65
+ - named a0 to a5 in paper
66
+ """
67
+ p_Rs: Annotated[Sequence[float], Field(min_length=6, max_length=6)] = [0, 0, 0, 0, 0, 0]
68
+ """ ⤷ Parameters for series-resistance
69
+ - no resistance set by default
70
+ - named b0 to b5 in paper
71
+ """
72
+ p_RtS: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
73
+ """ ⤷ Parameters for R_transient_S (short-term),
74
+ - no transient active by default
75
+ - named c0 to c2 in paper
76
+ """
77
+ p_CtS: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
78
+ """ ⤷ Parameters for C_transient_S (short-term)
79
+ - no transient active by default
80
+ - named d0 to d2 in paper
81
+ """
82
+ p_RtL: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
83
+ """ ⤷ Parameters for R_transient_L (long-term)
84
+ - no transient active by default
85
+ - named e0 to e2 in paper
86
+ """
87
+ p_CtL: Annotated[Sequence[float], Field(min_length=3, max_length=3)] = [0, 0, 0]
88
+ """ ⤷ Parameters for C_transient_L (long-term)
89
+ - no transient active by default
90
+ - named f0 to f2 in paper
91
+ """
92
+
93
+ p_rce: Annotated[float, Gt(0), Le(1.0)] = 1.0
94
+ """ ⤷ Parameter for rate capacity effect
95
+ - Set to 1 to disregard
96
+ - named c in paper
97
+ """
98
+ kdash: PositiveFloat = sys.float_info.min # TODO: use k directly?
99
+ """ ⤷ Parameter for rate capacity effect
100
+ - temporary component of rate capacity effect, valve in KiBaM (eq 17)
101
+ - k' = k/c(1-c),
102
+ """
103
+
104
+ R_leak_Ohm: PositiveFloat = sys.float_info.max
105
+ """ ⤷ Parameter for self discharge (custom extension)
106
+ - effect is often very small, mostly relevant for some capacitors
107
+ """
108
+
109
+ @classmethod
110
+ @validate_call
111
+ def lipo(
112
+ cls,
113
+ q_mAh: PositiveFloat, # TODO: charge is more correct
114
+ SoC_init: soc_t | None = None,
115
+ name: str | None = None,
116
+ description: str | None = None,
117
+ ) -> Self:
118
+ """Modeled after the PL-383562 2C Polymer Lithium-ion Battery.
119
+
120
+ Nominal Voltage 3.7 V
121
+ Nominal Capacity 860 mAh
122
+ Discharge Cutoff 3.0 V
123
+ Charge Cutoff 4.2 V
124
+ Max Discharge 2 C / 1.72 A
125
+ https://www.batteryspace.com/prod-specs/pl383562.pdf
126
+
127
+ """
128
+ name_lmnts: list[str] = ["LiPo", f"{q_mAh:.0f}mAh", "3.7V"]
129
+ if name is not None:
130
+ name_lmnts = [name] # specific name overrules params
131
+ if description is None:
132
+ description = "Model of a LiPo battery (3 to 4.2 V) with adjustable capacity"
133
+ return cls(
134
+ SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
135
+ q_As=q_mAh * 3600 / 1000,
136
+ p_VOC=[-0.852, 63.867, 3.6297, 0.559, 0.51, 0.508],
137
+ p_Rs=[0.1463, 30.27, 0.1037, 0.0584, 0.1747, 0.1288],
138
+ p_RtS=[0.1063, 62.49, 0.0437],
139
+ p_CtS=[-200, 138, 300], # most likely a mistake (d1=-138) in the table/paper!
140
+ p_RtL=[0.0712, 61.4, 0.0288],
141
+ p_CtL=[-3083, 180, 5088],
142
+ # y10 = 2863.3, y20 = 232.66 # unused
143
+ p_rce=0.9248,
144
+ kdash=0.0008,
145
+ R_leak_Ohm=55.9e6 / q_mAh, # from wiki ~ 5 % discharge/month
146
+ # content-fields below
147
+ name="_".join(name_lmnts),
148
+ description=description,
149
+ owner="NES Lab",
150
+ group="NES Lab",
151
+ visible2group=True,
152
+ visible2all=True,
153
+ )
154
+
155
+ @classmethod
156
+ @validate_call
157
+ def lead_acid(
158
+ cls,
159
+ q_mAh: PositiveFloat,
160
+ SoC_init: soc_t | None = None,
161
+ name: str | None = None,
162
+ description: str | None = None,
163
+ ) -> Self:
164
+ """Modeled after the LEOCH LP12-1.2AH lead acid battery.
165
+
166
+ Nominal Voltage 12 V (6 Cell)
167
+ Nominal Capacity 1.2 Ah
168
+ Discharge Cutoff 10.8 V
169
+ Charge Cutoff 13.5 V
170
+ Max Discharge 15 C / 18 A
171
+ https://www.leoch.com/pdf/reserve-power/agm-vrla/lp-general/LP12-1.2.pdf
172
+ # NOTE: 1 cell has 2.1 V nom, cell-count as param?
173
+ # NOTE: add temperature-component? -5mV/cell/K, also capacity-decrease
174
+ """
175
+ name_lmnts: list[str] = ["Lead-Acid", f"{q_mAh:.0f}mAh", "12V"]
176
+ if name is not None:
177
+ name_lmnts = [name] # specific name overrules params
178
+ if description is None:
179
+ description = "Model of a 12V lead acid battery with adjustable capacity"
180
+ return cls(
181
+ SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
182
+ q_As=q_mAh * 3600 / 1000,
183
+ p_VOC=[5.429, 117.5, 11.32, 2.706, 2.04, 1.026],
184
+ p_Rs=[1.578, 8.527, 0.7808, -1.887, -2.404, -0.649],
185
+ p_RtS=[2.771, 9.079, 0.22],
186
+ p_CtS=[-2423, 75.14, 55],
187
+ p_RtL=[2.771, 9.079, 0.218],
188
+ # ⤷ first 2 values of p_RtL are identical with p_RtS in table/paper
189
+ # (strange, but plots look fine)
190
+ p_CtL=[-1240, 9.571, 3100],
191
+ # y10 = 2592, y20 = 1728 # unused
192
+ p_rce=0.6,
193
+ kdash=0.0034,
194
+ R_leak_Ohm=174e6 / q_mAh,
195
+ # ⤷ from datasheet - 3-20 % discharge/month for 1.2 Ah, here 5%
196
+ # content-fields below
197
+ name="_".join(name_lmnts),
198
+ description=description,
199
+ owner="NES Lab",
200
+ group="NES Lab",
201
+ visible2group=True,
202
+ visible2all=True,
203
+ )
204
+
205
+ @classmethod
206
+ @validate_call
207
+ def capacitor(
208
+ cls,
209
+ C_uF: PositiveFloat,
210
+ V_rated: PositiveFloat,
211
+ SoC_init: soc_t | None = None,
212
+ R_series_Ohm: NonNegativeFloat | None = None,
213
+ R_leak_Ohm: PositiveFloat | None = None,
214
+ name: str | None = None,
215
+ description: str | None = None,
216
+ ) -> Self:
217
+ name_lmnts: list[str] = ["Capacitor", f"{C_uF:.0f}uF", f"{V_rated:.1f}V"]
218
+ if name is not None:
219
+ name_lmnts = [name] # specific name overrules params
220
+ if description is None:
221
+ description = "Model of an Capacitor with various effects"
222
+ if R_leak_Ohm is not None:
223
+ description += f", R_leak = {R_leak_Ohm / 1e3:.3f} Ohm"
224
+ if R_series_Ohm is not None:
225
+ description += f", R_serial = {R_series_Ohm:.0f} Ohm"
226
+ return cls(
227
+ SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
228
+ q_As=1e-6 * C_uF * V_rated,
229
+ p_VOC=[0, 0, 0, V_rated, 0, 0], # 100% SoC is @V_rated,
230
+ # no transients per default
231
+ p_Rs=[0, 0, R_series_Ohm, 0, 0, 0]
232
+ if R_series_Ohm
233
+ else cls.model_fields["p_Rs"].default, # const series resistance
234
+ R_leak_Ohm=R_leak_Ohm or cls.model_fields["R_leak_Ohm"].default,
235
+ # content-fields below
236
+ name="_".join(name_lmnts),
237
+ description=description,
238
+ owner="NES Lab",
239
+ group="NES Lab",
240
+ visible2group=True,
241
+ visible2all=True,
242
+ )
243
+
244
+ @model_validator(mode="before")
245
+ @classmethod
246
+ def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
247
+ values, chain = tb_client.try_completing_model(cls.__name__, values)
248
+ values = tb_client.fill_in_user_data(values)
249
+ log.debug("vStorage-Inheritances: %s", chain)
250
+ return values
251
+
252
+ @model_validator(mode="after")
253
+ def post_validation(self) -> Self:
254
+ return self
255
+
256
+ def without_rate_capacity(self) -> Self:
257
+ model_dict = self.model_dump()
258
+ model_dict["p_rce"] = 1
259
+ model_dict["name"] += " no_rate_cap"
260
+ return type(self)(**model_dict)
261
+
262
+ def without_transient_voltages(self) -> Self:
263
+ model_dict = self.model_dump()
264
+ model_dict["p_RtS"] = [0, 0, 0]
265
+ model_dict["p_CtS"] = [0, 0, 0]
266
+ model_dict["p_RtL"] = [0, 0, 0]
267
+ model_dict["p_CtL"] = [0, 0, 0]
268
+ model_dict["name"] += " no_transient_vs"
269
+ return type(self)(**model_dict)
270
+
271
+ @staticmethod
272
+ @validate_call
273
+ def calc_k(kdash: PositiveFloat, c: Annotated[float, Gt(0), Le(1)]) -> float:
274
+ """Translate between k & k'.
275
+
276
+ As explained below equation 4 in paper: k' = k / (c * (c - 1))
277
+ """
278
+ return kdash * c * (1 - c)
279
+
280
+ @property
281
+ def kdash_(self) -> float:
282
+ return self.k / (self.p_rce * (self.p_rce - 1))
283
+
284
+ @validate_call
285
+ def calc_R_leak_capacitor(
286
+ self,
287
+ duration: timedelta,
288
+ SoC_final: Annotated[float, Ge(0), Le(1)],
289
+ SoC_0: Annotated[float, Ge(0), Le(1)] = 1.0,
290
+ ) -> float:
291
+ # based on capacitor discharge: U(t) = U0 * e ^ (-t/RC)
292
+ # Example: 50mAh; SoC from 100 % to 85 % over 30 days => ~1.8 MOhm
293
+ U0 = self.calc_V_OC(SoC_0)
294
+ Ut = self.calc_V_OC(SoC_final)
295
+ return duration.total_seconds() * U0 / (self.q_As * math.log(U0 / Ut))
296
+
297
+ @validate_call
298
+ def calc_R_leak_battery(
299
+ self,
300
+ duration: timedelta,
301
+ SoC_final: Annotated[float, Ge(0), Le(1)],
302
+ SoC_0: Annotated[float, Ge(0), Le(1)] = 1.0,
303
+ ) -> float:
304
+ U0 = self.calc_V_OC(SoC_0)
305
+ U1 = self.calc_V_OC(SoC_final)
306
+ current_A = (SoC_0 - SoC_final) * self.q_As / duration.total_seconds()
307
+ return (U0 + U1) / 2 / current_A
308
+
309
+ def calc_V_OC(self, SoC: float) -> float:
310
+ return (
311
+ self.p_VOC[0] * math.pow(math.e, -self.p_VOC[1] * SoC)
312
+ + self.p_VOC[2]
313
+ + self.p_VOC[3] * SoC
314
+ - self.p_VOC[4] * SoC**2
315
+ + self.p_VOC[5] * SoC**3
316
+ )
317
+
318
+ @property
319
+ def capacity_in_uF(self) -> float:
320
+ return 1e6 * self.q_As / self.calc_V_OC(SoC=1.0)
321
+
322
+ @property
323
+ def charge_in_mAh(self) -> float:
324
+ return 1e3 * self.q_As / 3600
325
+
326
+ @property
327
+ def V_init(self) -> float:
328
+ return self.calc_V_OC(SoC=self.SoC_init)
329
+
330
+ def calc_R_series(self, SoC: float) -> float:
331
+ return (
332
+ self.p_Rs[0] * math.pow(math.e, -self.p_Rs[1] * SoC)
333
+ + self.p_Rs[2]
334
+ + self.p_Rs[3] * SoC
335
+ - self.p_Rs[4] * SoC**2
336
+ + self.p_Rs[5] * SoC**3
337
+ )
338
+
339
+ def calc_R_transient_S(self, SoC: float) -> float:
340
+ return self.p_RtS[0] * math.pow(math.e, -self.p_RtS[1] * SoC) + self.p_RtS[2]
341
+
342
+ def calc_C_transient_S(self, SoC: float) -> float:
343
+ return self.p_CtS[0] * math.pow(math.e, -self.p_CtS[1] * SoC) + self.p_CtS[2]
344
+
345
+ def calc_R_transient_L(self, SoC: float) -> float:
346
+ return self.p_RtL[0] * math.pow(math.e, -self.p_RtL[1] * SoC) + self.p_RtL[2]
347
+
348
+ def calc_C_transient_L(self, SoC: float) -> float:
349
+ return self.p_CtL[0] * math.pow(math.e, -self.p_CtL[1] * SoC) + self.p_CtL[2]
350
+
351
+ def approximate_SoC(self, V_OC: float) -> float:
352
+ SoC_next = SoC_now = 0.5
353
+ step_size = 0.05
354
+ go_up = True
355
+ match = 5
356
+ counter = 0
357
+
358
+ while match > 0.001:
359
+ SoC_now = SoC_next
360
+ V_OC_now = self.calc_V_OC(SoC_now)
361
+ match_new = abs(V_OC_now / V_OC - 1)
362
+ if match_new > match:
363
+ go_up = not go_up
364
+ if go_up:
365
+ step_size /= 2
366
+ SoC_next += step_size if go_up else -step_size
367
+ match = match_new
368
+ counter += 1
369
+ if counter > 100:
370
+ raise RuntimeError("Could not approximate SoC for given Voltage")
371
+
372
+ return SoC_now
373
+
374
+
375
+ # constants & custom types
376
+ TIMESTEP_s_DEFAULT: float = 1.0 / config.SAMPLERATE_SPS
377
+ LuT_SIZE_LOG: int = 7
378
+ LuT_SIZE: int = 2**LuT_SIZE_LOG
379
+ u32 = Annotated[int, Field(ge=0, lt=2**32)]
380
+ lut_storage = Annotated[list[u32], Field(min_length=LuT_SIZE, max_length=LuT_SIZE)]
381
+
382
+
383
+ @final
384
+ class StoragePRUConfig(ShpModel):
385
+ """Map settings-list to internal state-vars struct StorageConfig.
386
+
387
+ NOTE:
388
+ - yaml is based on si-units like nA, mV, ms, uF
389
+ - c-code and py-copy is using nA, uV, ns, nF, fW, raw
390
+ - ordering is intentional and in sync with shepherd/commons.h
391
+ """
392
+
393
+ SoC_init_1_n30: u32
394
+ """ ⤷ initial charge of storage """
395
+ Constant_1_per_nA_n60: u32
396
+ """ ⤷ Convert I_charge to delta-SoC with one multiplication."""
397
+ Constant_1_per_uV_n60: u32
398
+ """ ⤷ Leakage - Convert V_OC to delta-SoC with one multiplication.
399
+ Combines prior constant and R_leak, to maximize resolution.
400
+ """
401
+ LuT_VOC_uV_n8: lut_storage
402
+ """⤷ ranges from 3.9 uV to 16.7 V"""
403
+ LuT_RSeries_kOhm_n32: lut_storage
404
+ """⤷ ranges from 233n to 1 kOhm"""
405
+
406
+ @classmethod
407
+ @validate_call
408
+ def from_vstorage(
409
+ cls,
410
+ data: VirtualStorageConfig | None,
411
+ dt_s: PositiveFloat = TIMESTEP_s_DEFAULT,
412
+ *,
413
+ optimize_clamp: bool = True,
414
+ ) -> Self:
415
+ x_off = 0.5 if optimize_clamp else 0.0
416
+ SoC_min = 1.0 / LuT_SIZE
417
+ if data is None:
418
+ data = VirtualStorageConfig.capacitor(C_uF=100, V_rated=10)
419
+ V_OC_LuT = [data.calc_V_OC(SoC_min * (x + x_off)) for x in range(LuT_SIZE)]
420
+ R_series_LuT = [data.calc_R_series(SoC_min * (x + x_off)) for x in range(LuT_SIZE)]
421
+ Constant_1_per_A: float = dt_s / data.q_As
422
+ Constant_1_per_V: float = Constant_1_per_A / data.R_leak_Ohm
423
+ return cls(
424
+ SoC_init_1_n30=round(2**30 * data.SoC_init),
425
+ Constant_1_per_nA_n60=round((2**60 / 1e9) * Constant_1_per_A),
426
+ Constant_1_per_uV_n60=round((2**60 / 1e6) * Constant_1_per_V),
427
+ LuT_VOC_uV_n8=[round((2**8 * 1e6) * y) for y in V_OC_LuT],
428
+ LuT_RSeries_kOhm_n32=[round((2**32 * 1e-3) * y) for y in R_series_LuT],
429
+ )