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
|
@@ -16,7 +16,7 @@ Compromises:
|
|
|
16
16
|
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
from shepherd_core.data_models.content.
|
|
19
|
+
from shepherd_core.data_models.content.virtual_harvester_config import HarvesterPRUConfig
|
|
20
20
|
from shepherd_core.logger import log
|
|
21
21
|
|
|
22
22
|
|
|
@@ -64,33 +64,34 @@ class VirtualHarvesterModel:
|
|
|
64
64
|
self.voltage_step_x4_uV: int = 4 * self._cfg.voltage_step_uV
|
|
65
65
|
self.age_max: int = 2 * self._cfg.window_size
|
|
66
66
|
|
|
67
|
+
self.voc_now: int = self._cfg.voltage_max_uV
|
|
68
|
+
self.voc_nxt: int = self._cfg.voltage_max_uV
|
|
69
|
+
self.voc_min: int = max(1000, self._cfg.voltage_min_uV)
|
|
70
|
+
|
|
71
|
+
self.lin_extrapolation: bool = bool(self._cfg.hrv_mode & (2**2))
|
|
72
|
+
|
|
67
73
|
# INIT static vars: CV
|
|
68
74
|
self.voltage_last: int = 0
|
|
69
75
|
self.current_last: int = 0
|
|
70
|
-
self.compare_last: int = 0
|
|
71
|
-
self.lin_extrapolation: bool = bool(self._cfg.hrv_mode & (2**2))
|
|
72
|
-
self.current_delta: int = 0
|
|
73
76
|
self.voltage_delta: int = 0
|
|
77
|
+
self.current_delta: int = 0
|
|
78
|
+
self.compare_last: int = 0
|
|
74
79
|
|
|
75
80
|
# INIT static vars: VOC
|
|
76
81
|
self.age_now: int = 0
|
|
77
|
-
self.voc_now: int = self._cfg.voltage_max_uV
|
|
78
82
|
self.age_nxt: int = 0
|
|
79
|
-
self.voc_nxt: int = self._cfg.voltage_max_uV
|
|
80
|
-
self.voc_min: int = max(1000, self._cfg.voltage_min_uV)
|
|
81
83
|
|
|
82
84
|
# INIT static vars: PO
|
|
83
|
-
# already done: interval step
|
|
84
85
|
self.power_last: int = 0
|
|
85
86
|
|
|
86
87
|
# INIT static vars: OPT
|
|
87
|
-
# already done: age_now, age_nxt
|
|
88
|
-
self.power_now: int = 0
|
|
88
|
+
# already done in VOC: age_now, age_nxt
|
|
89
89
|
self.voltage_now: int = 0
|
|
90
90
|
self.current_now: int = 0
|
|
91
|
-
self.power_nxt: int = 0
|
|
92
91
|
self.voltage_nxt: int = 0
|
|
93
92
|
self.current_nxt: int = 0
|
|
93
|
+
self.power_now: int = 0
|
|
94
|
+
self.power_nxt: int = 0
|
|
94
95
|
|
|
95
96
|
def ivcurve_sample(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
|
|
96
97
|
if self._cfg.window_size <= 1:
|
|
@@ -116,19 +117,25 @@ class VirtualHarvesterModel:
|
|
|
116
117
|
if distance_now < distance_last and distance_now < self.voltage_step_x4_uV:
|
|
117
118
|
self.voltage_hold = _voltage_uV
|
|
118
119
|
self.current_hold = _current_nA
|
|
119
|
-
self.current_delta = _current_nA - self.current_last
|
|
120
120
|
self.voltage_delta = _voltage_uV - self.voltage_last
|
|
121
|
-
|
|
121
|
+
self.current_delta = _current_nA - self.current_last
|
|
122
122
|
elif distance_last < distance_now and distance_last < self.voltage_step_x4_uV:
|
|
123
123
|
self.voltage_hold = self.voltage_last
|
|
124
124
|
self.current_hold = self.current_last
|
|
125
|
-
self.current_delta = _current_nA - self.current_last
|
|
126
125
|
self.voltage_delta = _voltage_uV - self.voltage_last
|
|
126
|
+
self.current_delta = _current_nA - self.current_last
|
|
127
127
|
elif self.lin_extrapolation:
|
|
128
128
|
# apply the proper delta if needed
|
|
129
|
+
# TODO: C-Code differs here slightly, but only to handle unsigned int
|
|
129
130
|
if (self.voltage_hold < self.voltage_set_uV) == (self.voltage_delta > 0):
|
|
130
|
-
self.voltage_hold
|
|
131
|
-
|
|
131
|
+
if self.voltage_hold > -self.voltage_delta:
|
|
132
|
+
self.voltage_hold += self.voltage_delta
|
|
133
|
+
else:
|
|
134
|
+
self.voltage_hold = 0
|
|
135
|
+
if self.current_hold > -self.current_delta:
|
|
136
|
+
self.current_hold += self.current_delta
|
|
137
|
+
else:
|
|
138
|
+
self.current_hold = 0
|
|
132
139
|
else:
|
|
133
140
|
if self.voltage_hold > self.voltage_delta:
|
|
134
141
|
self.voltage_hold -= self.voltage_delta
|
|
@@ -145,7 +152,7 @@ class VirtualHarvesterModel:
|
|
|
145
152
|
return self.voltage_hold, self.current_hold
|
|
146
153
|
|
|
147
154
|
def ivcurve_2_mppt_voc(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
|
|
148
|
-
self.interval_step
|
|
155
|
+
self.interval_step += 1
|
|
149
156
|
if self.interval_step >= self._cfg.interval_n:
|
|
150
157
|
self.interval_step = 0
|
|
151
158
|
self.age_nxt += 1
|
|
@@ -168,14 +175,15 @@ class VirtualHarvesterModel:
|
|
|
168
175
|
|
|
169
176
|
_voltage_uV, _current_nA = self.ivcurve_2_cv(_voltage_uV, _current_nA)
|
|
170
177
|
if self.interval_step < self._cfg.duration_n:
|
|
171
|
-
self.voltage_set_uV = self.
|
|
178
|
+
self.voltage_set_uV = self._cfg.voltage_max_uV
|
|
179
|
+
_current_nA = 0
|
|
172
180
|
elif self.interval_step == self._cfg.duration_n:
|
|
173
181
|
self.voltage_set_uV = int(self.voc_now * self._cfg.setpoint_n8 / 256)
|
|
174
|
-
|
|
182
|
+
_current_nA = 0
|
|
175
183
|
return _voltage_uV, _current_nA
|
|
176
184
|
|
|
177
185
|
def ivcurve_2_mppt_po(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
|
|
178
|
-
self.interval_step
|
|
186
|
+
self.interval_step += 1
|
|
179
187
|
if self.interval_step >= self._cfg.interval_n:
|
|
180
188
|
self.interval_step = 0
|
|
181
189
|
|
|
@@ -211,7 +219,7 @@ class VirtualHarvesterModel:
|
|
|
211
219
|
self.voltage_set_uV = self._cfg.voltage_min_uV
|
|
212
220
|
self.is_rising = True
|
|
213
221
|
self.volt_step_uV = self._cfg.voltage_step_uV
|
|
214
|
-
if self.voltage_set_uV
|
|
222
|
+
if self.voltage_set_uV < self._cfg.voltage_step_uV:
|
|
215
223
|
self.voltage_set_uV = self._cfg.voltage_step_uV
|
|
216
224
|
self.is_rising = True
|
|
217
225
|
self.volt_step_uV = self._cfg.voltage_step_uV
|
|
@@ -13,8 +13,8 @@ from pathlib import Path
|
|
|
13
13
|
from tqdm import tqdm
|
|
14
14
|
|
|
15
15
|
from shepherd_core.data_models.base.calibration import CalibrationHarvester
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
17
|
-
from shepherd_core.data_models.content.
|
|
16
|
+
from shepherd_core.data_models.content.virtual_harvester_config import HarvesterPRUConfig
|
|
17
|
+
from shepherd_core.data_models.content.virtual_harvester_config import VirtualHarvesterConfig
|
|
18
18
|
from shepherd_core.reader import Reader
|
|
19
19
|
from shepherd_core.writer import Writer
|
|
20
20
|
|
|
@@ -28,45 +28,44 @@ def simulate_harvester(
|
|
|
28
28
|
|
|
29
29
|
Fn return the harvested energy.
|
|
30
30
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
with ExitStack() as stack:
|
|
32
|
+
file_inp = Reader(path_input, verbose=False)
|
|
33
|
+
stack.enter_context(file_inp)
|
|
34
|
+
cal_inp = file_inp.get_calibration_data()
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
if path_output:
|
|
37
|
+
cal_hrv = CalibrationHarvester()
|
|
38
|
+
file_out = Writer(
|
|
39
|
+
path_output, cal_data=cal_hrv, mode="harvester", verbose=False, force_overwrite=True
|
|
40
|
+
)
|
|
41
|
+
stack.enter_context(file_out)
|
|
42
|
+
cal_out = file_out.get_calibration_data()
|
|
43
|
+
file_out.store_hostname("hrv_sim_" + config.name)
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
hrv_pru = HarvesterPRUConfig.from_vhrv(
|
|
46
|
+
config,
|
|
47
|
+
for_emu=True,
|
|
48
|
+
dtype_in=file_inp.get_datatype(),
|
|
49
|
+
window_size=file_inp.get_window_samples(),
|
|
50
|
+
voltage_step_V=file_inp.get_voltage_step(),
|
|
51
|
+
)
|
|
52
|
+
hrv = VirtualHarvesterModel(hrv_pru)
|
|
53
|
+
e_out_Ws = 0.0
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
for t_, v_inp, i_inp in tqdm(
|
|
56
|
+
file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
|
|
57
|
+
):
|
|
58
|
+
v_uV = cal_inp.voltage.raw_to_si(v_inp) * 1e6
|
|
59
|
+
i_nA = cal_inp.current.raw_to_si(i_inp) * 1e9
|
|
60
|
+
length = min(v_uV.size, i_nA.size)
|
|
61
|
+
for n_ in range(length):
|
|
62
|
+
v_uV[n_], i_nA[n_] = hrv.ivcurve_sample(
|
|
63
|
+
_voltage_uV=int(v_uV[n_]), _current_nA=int(i_nA[n_])
|
|
64
|
+
)
|
|
65
|
+
e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
|
|
66
|
+
if path_output:
|
|
67
|
+
v_out = cal_out.voltage.si_to_raw(v_uV / 1e6)
|
|
68
|
+
i_out = cal_out.current.si_to_raw(i_nA / 1e9)
|
|
69
|
+
file_out.append_iv_data_raw(t_, v_out, i_out)
|
|
70
70
|
|
|
71
|
-
stack.close()
|
|
72
71
|
return e_out_Ws
|
|
@@ -11,10 +11,11 @@ NOTE: DO NOT OPTIMIZE -> stay close to original code-base
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from shepherd_core.data_models.base.calibration import CalibrationEmulator
|
|
14
|
-
from shepherd_core.data_models.content.
|
|
15
|
-
from shepherd_core.data_models.content.
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
17
|
-
from shepherd_core.data_models.content.
|
|
14
|
+
from shepherd_core.data_models.content.enum_datatypes import EnergyDType
|
|
15
|
+
from shepherd_core.data_models.content.virtual_harvester_config import HarvesterPRUConfig
|
|
16
|
+
from shepherd_core.data_models.content.virtual_source_config import ConverterPRUConfig
|
|
17
|
+
from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
|
|
18
|
+
from shepherd_core.data_models.content.virtual_storage_config import StoragePRUConfig
|
|
18
19
|
|
|
19
20
|
from .virtual_converter_model import PruCalibration
|
|
20
21
|
from .virtual_converter_model import VirtualConverterModel
|
|
@@ -36,25 +37,28 @@ class VirtualSourceModel:
|
|
|
36
37
|
) -> None:
|
|
37
38
|
self._cal_emu: CalibrationEmulator = cal_emu
|
|
38
39
|
self._cal_pru: PruCalibration = PruCalibration(cal_emu)
|
|
39
|
-
|
|
40
40
|
self.cfg_src = VirtualSourceConfig() if vsrc is None else vsrc
|
|
41
|
-
cnv_config = ConverterPRUConfig.from_vsrc(
|
|
42
|
-
self.cfg_src,
|
|
43
|
-
dtype_in=dtype_in,
|
|
44
|
-
log_intermediate_node=log_intermediate,
|
|
45
|
-
)
|
|
46
|
-
self.cnv: VirtualConverterModel = VirtualConverterModel(cnv_config, self._cal_pru)
|
|
47
41
|
|
|
48
|
-
|
|
42
|
+
# init Harvester
|
|
43
|
+
cfg_hrv = HarvesterPRUConfig.from_vhrv(
|
|
49
44
|
self.cfg_src.harvester,
|
|
50
45
|
for_emu=True,
|
|
51
46
|
dtype_in=dtype_in,
|
|
52
47
|
window_size=window_size,
|
|
53
48
|
voltage_step_V=voltage_step_V,
|
|
54
49
|
)
|
|
50
|
+
self.hrv: VirtualHarvesterModel = VirtualHarvesterModel(cfg_hrv)
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
# init Converters
|
|
53
|
+
cfg_cnv = ConverterPRUConfig.from_vsrc(
|
|
54
|
+
self.cfg_src,
|
|
55
|
+
dtype_in=dtype_in,
|
|
56
|
+
log_intermediate_node=log_intermediate,
|
|
57
|
+
)
|
|
58
|
+
cfg_storage = StoragePRUConfig.from_vstorage(self.cfg_src.storage, optimize_clamp=True)
|
|
59
|
+
self.cnv: VirtualConverterModel = VirtualConverterModel(cfg_cnv, self._cal_pru, cfg_storage)
|
|
57
60
|
|
|
61
|
+
# state for simulation
|
|
58
62
|
self.W_inp_fWs: float = 0.0
|
|
59
63
|
self.W_out_fWs: float = 0.0
|
|
60
64
|
|
|
@@ -74,8 +78,8 @@ class VirtualSourceModel:
|
|
|
74
78
|
|
|
75
79
|
# fake ADC read
|
|
76
80
|
A_out_raw = self._cal_emu.adc_C_A.si_to_raw(I_out_nA * 10**-9)
|
|
77
|
-
|
|
78
81
|
P_out_fW = self.cnv.calc_out_power(A_out_raw)
|
|
82
|
+
|
|
79
83
|
V_mid_uV = self.cnv.update_cap_storage()
|
|
80
84
|
V_out_raw = self.cnv.update_states_and_output()
|
|
81
85
|
V_out_uV = int(self._cal_emu.dac_V_A.raw_to_si(V_out_raw) * 10**6)
|
|
@@ -14,7 +14,7 @@ import numpy as np
|
|
|
14
14
|
from tqdm import tqdm
|
|
15
15
|
|
|
16
16
|
from shepherd_core.data_models.base.calibration import CalibrationEmulator
|
|
17
|
-
from shepherd_core.data_models.content.
|
|
17
|
+
from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
|
|
18
18
|
from shepherd_core.logger import log
|
|
19
19
|
from shepherd_core.reader import Reader
|
|
20
20
|
from shepherd_core.writer import Writer
|
|
@@ -35,81 +35,79 @@ def simulate_source(
|
|
|
35
35
|
|
|
36
36
|
FN returns the consumed energy of the target.
|
|
37
37
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
with ExitStack() as stack:
|
|
39
|
+
file_inp = Reader(path_input, verbose=False)
|
|
40
|
+
stack.enter_context(file_inp)
|
|
41
|
+
cal_emu = CalibrationEmulator()
|
|
42
|
+
cal_inp = file_inp.get_calibration_data()
|
|
43
|
+
|
|
44
|
+
if path_output:
|
|
45
|
+
file_out = Writer(
|
|
46
|
+
path_output, cal_data=cal_emu, mode="emulator", verbose=False, force_overwrite=True
|
|
47
|
+
)
|
|
48
|
+
stack.enter_context(file_out)
|
|
49
|
+
file_out.store_hostname("emu_sim_" + config.name)
|
|
50
|
+
file_out.store_config(config.model_dump())
|
|
51
|
+
cal_out = file_out.get_calibration_data()
|
|
52
|
+
|
|
53
|
+
src = VirtualSourceModel(
|
|
54
|
+
config,
|
|
55
|
+
cal_emu,
|
|
56
|
+
dtype_in=file_inp.get_datatype(),
|
|
57
|
+
log_intermediate=False,
|
|
58
|
+
window_size=file_inp.get_window_samples(),
|
|
59
|
+
voltage_step_V=file_inp.get_voltage_step(),
|
|
47
60
|
)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
i_out_nA = 0
|
|
62
|
-
e_out_Ws = 0.0
|
|
63
|
-
if monitor_internals and path_output:
|
|
64
|
-
stats_sample = 0
|
|
65
|
-
stats_internal = np.empty((round(file_inp.runtime_s * file_inp.samplerate_sps), 11))
|
|
66
|
-
try:
|
|
67
|
-
# keep dependencies low
|
|
68
|
-
from matplotlib import pyplot as plt # noqa: PLC0415
|
|
69
|
-
except ImportError:
|
|
70
|
-
log.warning("Matplotlib not installed, plotting of internals disabled")
|
|
61
|
+
i_out_nA = 0
|
|
62
|
+
e_out_Ws = 0.0
|
|
63
|
+
if monitor_internals and path_output:
|
|
64
|
+
stats_sample = 0
|
|
65
|
+
stats_internal = np.empty((round(file_inp.runtime_s * file_inp.samplerate_sps), 11))
|
|
66
|
+
try:
|
|
67
|
+
# keep dependencies low
|
|
68
|
+
from matplotlib import pyplot as plt # noqa: PLC0415
|
|
69
|
+
except ImportError:
|
|
70
|
+
log.warning("Matplotlib not installed, plotting of internals disabled")
|
|
71
|
+
stats_internal = None
|
|
72
|
+
else:
|
|
71
73
|
stats_internal = None
|
|
72
|
-
else:
|
|
73
|
-
stats_internal = None
|
|
74
|
-
|
|
75
|
-
for _t, v_inp, i_inp in tqdm(
|
|
76
|
-
file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
|
|
77
|
-
):
|
|
78
|
-
v_uV = 1e6 * cal_inp.voltage.raw_to_si(v_inp)
|
|
79
|
-
i_nA = 1e9 * cal_inp.current.raw_to_si(i_inp)
|
|
80
|
-
|
|
81
|
-
for _n in range(len(_t)):
|
|
82
|
-
v_uV[_n] = src.iterate_sampling(
|
|
83
|
-
V_inp_uV=int(v_uV[_n]),
|
|
84
|
-
I_inp_nA=int(i_nA[_n]),
|
|
85
|
-
I_out_nA=i_out_nA,
|
|
86
|
-
)
|
|
87
|
-
i_out_nA = target.step(int(v_uV[_n]), pwr_good=src.cnv.get_power_good())
|
|
88
|
-
i_nA[_n] = i_out_nA
|
|
89
|
-
|
|
90
|
-
if stats_internal is not None:
|
|
91
|
-
stats_internal[stats_sample] = [
|
|
92
|
-
_t[_n] * 1e-9, # s
|
|
93
|
-
src.hrv.voltage_hold * 1e-6,
|
|
94
|
-
src.cnv.V_input_request_uV * 1e-6, # V
|
|
95
|
-
src.hrv.voltage_set_uV * 1e-6,
|
|
96
|
-
src.cnv.V_mid_uV * 1e-6,
|
|
97
|
-
src.hrv.current_hold * 1e-6, # mA
|
|
98
|
-
src.hrv.current_delta * 1e-6,
|
|
99
|
-
i_out_nA * 1e-6,
|
|
100
|
-
src.cnv.P_inp_fW * 1e-12, # mW
|
|
101
|
-
src.cnv.P_out_fW * 1e-12,
|
|
102
|
-
src.cnv.get_power_good(),
|
|
103
|
-
]
|
|
104
|
-
stats_sample += 1
|
|
105
|
-
|
|
106
|
-
e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
|
|
107
|
-
if path_output:
|
|
108
|
-
v_out = cal_out.voltage.si_to_raw(1e-6 * v_uV)
|
|
109
|
-
i_out = cal_out.current.si_to_raw(1e-9 * i_nA)
|
|
110
|
-
file_out.append_iv_data_raw(_t, v_out, i_out)
|
|
111
74
|
|
|
112
|
-
|
|
75
|
+
for t_, v_inp, i_inp in tqdm(
|
|
76
|
+
file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
|
|
77
|
+
):
|
|
78
|
+
v_uV = 1e6 * cal_inp.voltage.raw_to_si(v_inp)
|
|
79
|
+
i_nA = 1e9 * cal_inp.current.raw_to_si(i_inp)
|
|
80
|
+
|
|
81
|
+
for n_ in range(len(t_)):
|
|
82
|
+
v_uV[n_] = src.iterate_sampling(
|
|
83
|
+
V_inp_uV=int(v_uV[n_]),
|
|
84
|
+
I_inp_nA=int(i_nA[n_]),
|
|
85
|
+
I_out_nA=i_out_nA,
|
|
86
|
+
)
|
|
87
|
+
i_out_nA = target.step(int(v_uV[n_]), pwr_good=src.cnv.get_power_good())
|
|
88
|
+
i_nA[n_] = i_out_nA
|
|
89
|
+
|
|
90
|
+
if stats_internal is not None:
|
|
91
|
+
stats_internal[stats_sample] = [
|
|
92
|
+
t_[n_] * 1e-9, # s
|
|
93
|
+
src.hrv.voltage_hold * 1e-6,
|
|
94
|
+
src.cnv.V_input_request_uV * 1e-6, # V
|
|
95
|
+
src.hrv.voltage_set_uV * 1e-6,
|
|
96
|
+
src.cnv.V_mid_uV * 1e-6,
|
|
97
|
+
src.hrv.current_hold * 1e-6, # mA
|
|
98
|
+
src.hrv.current_delta * 1e-6,
|
|
99
|
+
i_out_nA * 1e-6,
|
|
100
|
+
src.cnv.P_inp_fW * 1e-12, # mW
|
|
101
|
+
src.cnv.P_out_fW * 1e-12,
|
|
102
|
+
src.cnv.get_power_good(),
|
|
103
|
+
]
|
|
104
|
+
stats_sample += 1
|
|
105
|
+
|
|
106
|
+
e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
|
|
107
|
+
if path_output:
|
|
108
|
+
v_out = cal_out.voltage.si_to_raw(1e-6 * v_uV)
|
|
109
|
+
i_out = cal_out.current.si_to_raw(1e-9 * i_nA)
|
|
110
|
+
file_out.append_iv_data_raw(t_, v_out, i_out)
|
|
113
111
|
|
|
114
112
|
if stats_internal is not None:
|
|
115
113
|
stats_internal = stats_internal[:stats_sample, :]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""this is ported py-version of the pru-code.
|
|
2
|
+
|
|
3
|
+
Goals:
|
|
4
|
+
|
|
5
|
+
- stay close to original code-base (fixed-point integer math)
|
|
6
|
+
- offer a comparison for the tests
|
|
7
|
+
- step 1 to a virtualization of emulation
|
|
8
|
+
|
|
9
|
+
NOTE1: DO NOT OPTIMIZE -> stay close to original c-code-base
|
|
10
|
+
|
|
11
|
+
Compromises:
|
|
12
|
+
|
|
13
|
+
- Py has to map the settings-list to internal vars -> is kernel-task
|
|
14
|
+
|
|
15
|
+
Expected deviations:
|
|
16
|
+
|
|
17
|
+
- lead charge ramp maxes out early on cell-voltage (max of V_uV_n8 is 16.78 V)
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from pydantic import PositiveFloat
|
|
22
|
+
from pydantic import validate_call
|
|
23
|
+
|
|
24
|
+
from shepherd_core import log
|
|
25
|
+
from shepherd_core.data_models.content.virtual_storage_config import LuT_SIZE
|
|
26
|
+
from shepherd_core.data_models.content.virtual_storage_config import LuT_SIZE_LOG
|
|
27
|
+
from shepherd_core.data_models.content.virtual_storage_config import StoragePRUConfig
|
|
28
|
+
from shepherd_core.data_models.content.virtual_storage_config import TIMESTEP_s_DEFAULT
|
|
29
|
+
from shepherd_core.data_models.content.virtual_storage_config import VirtualStorageConfig
|
|
30
|
+
from shepherd_core.data_models.content.virtual_storage_config import soc_t
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ModelStorage:
|
|
34
|
+
"""Abstract base class for storage models."""
|
|
35
|
+
|
|
36
|
+
def step(self, I_charge_A: float) -> tuple[float, float, float, float]: ...
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def u32s(i: float) -> int:
|
|
40
|
+
"""Guard to supervise calculated model-states."""
|
|
41
|
+
if i >= 2**32:
|
|
42
|
+
log.warning("u32-overflow")
|
|
43
|
+
if i < 0:
|
|
44
|
+
log.warning("u32-underflow")
|
|
45
|
+
return int(min(max(i, 0), 2**32 - 1))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def u64s(i: float) -> int:
|
|
49
|
+
"""Guard to supervise calculated model-states."""
|
|
50
|
+
if i >= 2**64:
|
|
51
|
+
log.warning("u64-overflow")
|
|
52
|
+
if i < 0:
|
|
53
|
+
log.warning("u64-underflow")
|
|
54
|
+
return int(min(max(i, 0), 2**64 - 1))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VirtualStorageModelPRU:
|
|
58
|
+
"""Ported python version of the pru vStorage.
|
|
59
|
+
|
|
60
|
+
This model should behave like ModelKiBaMSimple
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
SoC_MAX_1_n62: int = 2**62 - 1
|
|
64
|
+
SoC_TO_POS_DIV: int = 2 ** (62 - LuT_SIZE_LOG)
|
|
65
|
+
|
|
66
|
+
@validate_call
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
cfg: StoragePRUConfig,
|
|
70
|
+
SoC_init: soc_t | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
self.cfg_pru = cfg
|
|
73
|
+
# state
|
|
74
|
+
SoC_1_n30: float = 2**30 * SoC_init if SoC_init is not None else self.cfg_pru.SoC_init_1_n30
|
|
75
|
+
self.SoC_1_n62 = round(2**32 * SoC_1_n30)
|
|
76
|
+
self.V_OC_uV_n8 = self.cfg_pru.LuT_VOC_uV_n8[self.pos_LuT(self.SoC_1_n62)]
|
|
77
|
+
|
|
78
|
+
def pos_LuT(self, SoC_1_n62: int) -> int:
|
|
79
|
+
pos = u32s(SoC_1_n62 // self.SoC_TO_POS_DIV)
|
|
80
|
+
if pos >= LuT_SIZE:
|
|
81
|
+
pos = LuT_SIZE - 1
|
|
82
|
+
return pos
|
|
83
|
+
|
|
84
|
+
def calc_V_OC_uV(self) -> int:
|
|
85
|
+
pos_LuT = self.pos_LuT(self.SoC_1_n62)
|
|
86
|
+
return round(self.cfg_pru.LuT_VOC_uV_n8[pos_LuT] // 2**8)
|
|
87
|
+
|
|
88
|
+
def step(self, I_delta_nA_n4: float, *, is_charging: bool) -> float:
|
|
89
|
+
"""Calculate the battery SoC & cell-voltage after drawing a current over a time-step.
|
|
90
|
+
|
|
91
|
+
Note: 3x u64 multiplications
|
|
92
|
+
"""
|
|
93
|
+
dSoC_leak_1_n62 = u64s((self.V_OC_uV_n8 // 2**6) * self.cfg_pru.Constant_1_per_uV_n60)
|
|
94
|
+
if self.SoC_1_n62 >= dSoC_leak_1_n62:
|
|
95
|
+
self.SoC_1_n62 = u64s(self.SoC_1_n62 - dSoC_leak_1_n62)
|
|
96
|
+
else:
|
|
97
|
+
self.SoC_1_n62 = 0
|
|
98
|
+
|
|
99
|
+
dSoC_1_n62 = u64s(I_delta_nA_n4 * self.cfg_pru.Constant_1_per_nA_n60 // (2**2))
|
|
100
|
+
if is_charging:
|
|
101
|
+
self.SoC_1_n62 = u64s(self.SoC_1_n62 + dSoC_1_n62)
|
|
102
|
+
self.SoC_1_n62 = min(self.SoC_MAX_1_n62, self.SoC_1_n62)
|
|
103
|
+
elif self.SoC_1_n62 > dSoC_1_n62:
|
|
104
|
+
self.SoC_1_n62 = u64s(self.SoC_1_n62 - dSoC_1_n62)
|
|
105
|
+
else:
|
|
106
|
+
self.SoC_1_n62 = 0
|
|
107
|
+
|
|
108
|
+
pos_LuT = self.pos_LuT(self.SoC_1_n62)
|
|
109
|
+
self.V_OC_uV_n8 = self.cfg_pru.LuT_VOC_uV_n8[pos_LuT]
|
|
110
|
+
# TODO: is interpolation possible?
|
|
111
|
+
R_series_kOhm_n32 = self.cfg_pru.LuT_RSeries_kOhm_n32[pos_LuT]
|
|
112
|
+
V_delta_uV_n8 = u32s(u64s(I_delta_nA_n4 * R_series_kOhm_n32) // 2**28)
|
|
113
|
+
|
|
114
|
+
if is_charging:
|
|
115
|
+
V_cell_uV_n8 = u32s(self.V_OC_uV_n8 + V_delta_uV_n8)
|
|
116
|
+
elif self.V_OC_uV_n8 > V_delta_uV_n8:
|
|
117
|
+
V_cell_uV_n8 = u32s(self.V_OC_uV_n8 - V_delta_uV_n8)
|
|
118
|
+
else:
|
|
119
|
+
V_cell_uV_n8 = 0
|
|
120
|
+
|
|
121
|
+
if self.SoC_1_n62 == 0:
|
|
122
|
+
return 0 # cell voltage breaks down
|
|
123
|
+
return V_cell_uV_n8 // 2**8 # u32 uV
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class VirtualStorageModel(VirtualStorageModelPRU, ModelStorage):
|
|
127
|
+
"""Higher level Model that can run on a coarser timebase.
|
|
128
|
+
|
|
129
|
+
This model should behave like ModelKiBaMSimple
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
@validate_call
|
|
133
|
+
def __init__(
|
|
134
|
+
self,
|
|
135
|
+
cfg: VirtualStorageConfig,
|
|
136
|
+
SoC_init: soc_t | None = None,
|
|
137
|
+
dt_s: PositiveFloat = TIMESTEP_s_DEFAULT,
|
|
138
|
+
*,
|
|
139
|
+
optimize_clamp: bool = True,
|
|
140
|
+
) -> None:
|
|
141
|
+
# metadata for simulator
|
|
142
|
+
self.cfg: VirtualStorageConfig = cfg
|
|
143
|
+
self.dt_s = dt_s
|
|
144
|
+
# prepare PRU-Model
|
|
145
|
+
cfg_pru = StoragePRUConfig.from_vstorage(
|
|
146
|
+
cfg, TIMESTEP_s_DEFAULT, optimize_clamp=optimize_clamp
|
|
147
|
+
)
|
|
148
|
+
super().__init__(cfg_pru, SoC_init=SoC_init)
|
|
149
|
+
|
|
150
|
+
# just for simulation
|
|
151
|
+
self.steps_per_frame = round(dt_s / TIMESTEP_s_DEFAULT)
|
|
152
|
+
|
|
153
|
+
def step(self, I_charge_A: float) -> tuple[float, float, float, float]:
|
|
154
|
+
"""Slower outer step with step-size of simulation."""
|
|
155
|
+
I_delta_nA_n4 = abs(2**4 * (1e9 * I_charge_A))
|
|
156
|
+
is_charging = I_charge_A >= 0
|
|
157
|
+
for _ in range(self.steps_per_frame - 1):
|
|
158
|
+
super().step(I_delta_nA_n4, is_charging=is_charging)
|
|
159
|
+
V_cell_uV = super().step(I_delta_nA_n4, is_charging=is_charging)
|
|
160
|
+
# code below just for simulation
|
|
161
|
+
V_OC = (1e-6 / 2**8) * self.V_OC_uV_n8
|
|
162
|
+
V_cell = 1e-6 * V_cell_uV
|
|
163
|
+
SoC = (1.0 / 2**62) * self.SoC_1_n62
|
|
164
|
+
return V_OC, V_cell, SoC, SoC
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Playground to determine the best integer-math and fit for constants."""
|
|
2
|
+
|
|
3
|
+
from itertools import product
|
|
4
|
+
|
|
5
|
+
from shepherd_core import log
|
|
6
|
+
from shepherd_core.data_models.content.virtual_storage_config import LuT_SIZE_LOG
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def u32l(i: float) -> int:
|
|
10
|
+
"""Guard to supervise calculated model-states."""
|
|
11
|
+
if i >= 2**32:
|
|
12
|
+
log.warning("u32-overflow (%d)", i)
|
|
13
|
+
if i < 0:
|
|
14
|
+
log.warning("u32-underflow (%d)", i)
|
|
15
|
+
return round(min(max(i, 0), 2**32 - 1))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# #### I_charge-to-dSoC #####
|
|
19
|
+
# Goal: Allow ~1 uF Capacitor to 800 mAh battery
|
|
20
|
+
|
|
21
|
+
dt_s = 10e-6
|
|
22
|
+
qs_As = [1e-6 * 1 * 2.0, 1e-6 * 5 * 2.5, 1e-6 * 10 * 3.6, 800 * 3.6]
|
|
23
|
+
Constant_1u_per_nA_n40 = [u32l((2**40 / 1e3) * dt_s / q_As) for q_As in qs_As]
|
|
24
|
+
Constant_1_per_nA_n60 = [u32l((2**60 / 1e9) * dt_s / q_As) for q_As in qs_As]
|
|
25
|
+
|
|
26
|
+
# #### SoC-to-position #####
|
|
27
|
+
# Goal: As-simple-as-possible
|
|
28
|
+
|
|
29
|
+
SoCs_1u_n32 = [round(x / 10 * 1e6 * 2**32) for x in range(11)]
|
|
30
|
+
LUT_SIZE = 128
|
|
31
|
+
|
|
32
|
+
# First Approach (1 Division in pos-calc -> impossible for PRU)
|
|
33
|
+
SoC_min_1u = round(1e6 / LUT_SIZE)
|
|
34
|
+
positions1 = [int(SoC_1u_n32 / SoC_min_1u / 2**32) for SoC_1u_n32 in SoCs_1u_n32]
|
|
35
|
+
|
|
36
|
+
# Second Approach
|
|
37
|
+
SoC_min = 1.0 / LUT_SIZE
|
|
38
|
+
inv_SoC_min_1M_n32 = round(2**32 / 1e6 / SoC_min) # 1M / SoC_min
|
|
39
|
+
positions2 = [
|
|
40
|
+
int(int(SoC_1u_n32 / 2**32) * inv_SoC_min_1M_n32 / 2**32) for SoC_1u_n32 in SoCs_1u_n32
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# third approach
|
|
44
|
+
SoCs_1_n62 = [round(x / 10 * 2**62) for x in range(11)]
|
|
45
|
+
positions3 = [int(int(SoC_1_n62 / 2**32) * LUT_SIZE / 2**30) for SoC_1_n62 in SoCs_1_n62]
|
|
46
|
+
|
|
47
|
+
# final approach (upper u32 & rest of shift)
|
|
48
|
+
positions4 = [u32l(SoC_1_n62 // 2 ** (62 - LuT_SIZE_LOG)) for SoC_1_n62 in SoCs_1_n62]
|
|
49
|
+
|
|
50
|
+
# #### R_Leak-to-dSoC #####
|
|
51
|
+
# Goal: biggest possible dynamic range
|
|
52
|
+
|
|
53
|
+
Rs_leak_Ohm = [1e3, 10e3, 100e3, 1e6, 10e6]
|
|
54
|
+
Constants_1_per_V = [dt_s / q_As / R_leak_Ohm for q_As, R_leak_Ohm in product(qs_As, Rs_leak_Ohm)]
|
|
55
|
+
Constants_1u_per_uV_n40 = [u32l((2**40) * c_1_per_V) for c_1_per_V in Constants_1_per_V]
|
|
56
|
+
Constants_1_per_uV_n60 = [u32l((2**60 / 1e6) * c_1_per_V) for c_1_per_V in Constants_1_per_V]
|
|
57
|
+
|
|
58
|
+
print("done") # noqa: T201
|