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.
- shepherd_core/config.py +1 -1
- shepherd_core/data_models/__init__.py +8 -4
- shepherd_core/data_models/base/cal_measurement.py +7 -2
- shepherd_core/data_models/base/calibration.py +23 -12
- shepherd_core/data_models/base/content.py +12 -2
- shepherd_core/data_models/base/shepherd.py +13 -4
- shepherd_core/data_models/base/wrapper.py +2 -0
- shepherd_core/data_models/content/__init__.py +8 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
- shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
- shepherd_core/data_models/content/energy_environment.py +341 -23
- shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +44 -16
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +429 -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/experiment.py +38 -13
- shepherd_core/data_models/experiment/observer_features.py +17 -4
- shepherd_core/data_models/experiment/target_config.py +56 -8
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +10 -6
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +3 -1
- shepherd_core/data_models/task/helper_paths.py +2 -2
- shepherd_core/data_models/task/observer_tasks.py +8 -6
- shepherd_core/data_models/task/programming.py +4 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -2
- shepherd_core/data_models/testbed/cape.py +2 -0
- shepherd_core/data_models/testbed/gpio.py +2 -0
- shepherd_core/data_models/testbed/mcu.py +2 -0
- shepherd_core/data_models/testbed/observer.py +2 -0
- shepherd_core/data_models/testbed/target.py +7 -5
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +17 -15
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/exit_handler.py +22 -0
- shepherd_core/fw_tools/converter.py +2 -2
- shepherd_core/fw_tools/validation.py +1 -1
- shepherd_core/inventory/__init__.py +23 -21
- shepherd_core/inventory/system.py +3 -3
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +32 -27
- shepherd_core/testbed_client/cache_path.py +3 -3
- shepherd_core/testbed_client/client_abc_fix.py +14 -3
- shepherd_core/testbed_client/client_web.py +7 -5
- shepherd_core/testbed_client/fixtures.py +7 -7
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +29 -28
- shepherd_core/vsource/virtual_harvester_model.py +29 -21
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +18 -14
- 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/writer.py +16 -9
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- shepherd_core-2025.8.1.dist-info/RECORD +0 -83
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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 #
|
|
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 #
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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 #
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
storage:
|
|
202
|
+
name: Capacitor_100uF_6.3V
|
|
203
|
+
SoC_init: 0.66 # allow a proper / fast startup
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
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 #
|
|
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
|
|
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
|
+
)
|