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
@@ -16,7 +16,7 @@ Compromises:
16
16
 
17
17
  """
18
18
 
19
- from shepherd_core.data_models.content.virtual_harvester import HarvesterPRUConfig
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
- # TODO: voltage_delta is static
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 += self.voltage_delta
131
- self.current_hold += self.current_delta
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 = self.interval_step + 1
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.voc_now
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 = self.interval_step + 1
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 <= self._cfg.voltage_step_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.virtual_harvester import HarvesterPRUConfig
17
- from shepherd_core.data_models.content.virtual_harvester import VirtualHarvesterConfig
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
- stack = ExitStack()
32
- file_inp = Reader(path_input, verbose=False)
33
- stack.enter_context(file_inp)
34
- cal_inp = file_inp.get_calibration_data()
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
- 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)
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
- 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
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
- 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)
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.energy_environment import EnergyDType
15
- from shepherd_core.data_models.content.virtual_harvester import HarvesterPRUConfig
16
- from shepherd_core.data_models.content.virtual_source import ConverterPRUConfig
17
- from shepherd_core.data_models.content.virtual_source import VirtualSourceConfig
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
- hrv_config = HarvesterPRUConfig.from_vhrv(
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
- self.hrv: VirtualHarvesterModel = VirtualHarvesterModel(hrv_config)
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.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 # 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
- 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