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
@@ -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.virtual_source import VirtualSourceConfig
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
- stack = ExitStack()
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
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
- 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(),
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
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
- stack.close()
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