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
@@ -34,7 +34,8 @@ class TestbedTasks(ShpModel):
34
34
  tb = Testbed() # this will query the first (and only) entry of client
35
35
 
36
36
  tgt_ids = xp.get_target_ids()
37
- obs_tasks = [ObserverTasks.from_xp(xp, tb, _id) for _id in tgt_ids]
37
+ xp_folder = xp.folder_name()
38
+ obs_tasks = [ObserverTasks.from_xp(xp, xp_folder, tb, _id) for _id in tgt_ids]
38
39
  return cls(
39
40
  name=xp.name,
40
41
  observer_tasks=obs_tasks,
@@ -46,6 +47,9 @@ class TestbedTasks(ShpModel):
46
47
  return tasks
47
48
  return None
48
49
 
50
+ def get_observers(self) -> set[str]:
51
+ return {tasks.observer for tasks in self.observer_tasks}
52
+
49
53
  def get_output_paths(self) -> dict[str, Path]:
50
54
  # TODO: computed field preferred, but they don't work here, as
51
55
  # - they are always stored in yaml despite "repr=False"
@@ -56,6 +60,7 @@ class TestbedTasks(ShpModel):
56
60
  return values
57
61
 
58
62
  def is_contained(self) -> bool:
63
+ """Limit paths to allowed directories."""
59
64
  paths_allowed: AbstractSet[PurePosixPath] = {
60
65
  PurePosixPath("/var/shepherd/"),
61
66
  PurePosixPath("/tmp/"), # noqa: S108
@@ -50,6 +50,7 @@ class Uart:
50
50
  def __init__(
51
51
  self,
52
52
  content: Path | np.ndarray,
53
+ *,
53
54
  baud_rate: int | None = None,
54
55
  frame_length: int | None = 8,
55
56
  inversion: bool | None = None,
@@ -155,7 +156,7 @@ class Uart:
155
156
  """Analyze bit-state during long pauses (unchanged states).
156
157
 
157
158
  - pause should be HIGH for non-inverted mode (default)
158
- - assumes max frame size of 64 bit + x for safety
159
+ - assumes maximum frame size of 64 bit + x for safety
159
160
  """
160
161
  events = self.events_sig[:1000, :] # speedup for large datasets
161
162
  pauses = events[:, 2] > 80
@@ -1,6 +1,8 @@
1
1
  """Read and modify symbols in ELF-files."""
2
2
 
3
+ import shutil
3
4
  from pathlib import Path
5
+ from tempfile import TemporaryDirectory
4
6
  from typing import Annotated
5
7
 
6
8
  from pydantic import Field
@@ -28,22 +30,27 @@ def find_symbol(file_elf: Path, symbol: str) -> bool:
28
30
  return False
29
31
  if ELF is None:
30
32
  raise RuntimeError(elf_error_text)
31
- elf = ELF(path=file_elf)
32
- try:
33
- addr = elf.symbols[symbol]
34
- except KeyError:
35
- addr = None
36
- if addr is None:
37
- log.debug("Symbol '%s' not found in ELF-File %s", symbol, file_elf.name)
38
- return False
39
- log.debug(
40
- "Symbol '%s' found in ELF-File %s, arch=%s, order=%s",
41
- symbol,
42
- file_elf.name,
43
- elf.arch,
44
- elf.endian,
45
- )
46
- elf.close()
33
+ with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
34
+ # switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
35
+ file_tmp = Path(tmp) / file_elf.name
36
+ shutil.copy(file_elf, file_tmp)
37
+ elf = ELF(path=file_tmp)
38
+ try:
39
+ addr = elf.symbols[symbol]
40
+ except KeyError:
41
+ addr = None
42
+ if addr is None:
43
+ elf.close() # better be safe
44
+ log.debug("Symbol '%s' not found in ELF-File %s", symbol, file_elf.name)
45
+ return False
46
+ log.debug(
47
+ "Symbol '%s' found in ELF-File %s, arch=%s, order=%s",
48
+ symbol,
49
+ file_elf.name,
50
+ elf.arch,
51
+ elf.endian,
52
+ )
53
+ elf.close()
47
54
  return True
48
55
 
49
56
 
@@ -60,8 +67,9 @@ def read_symbol(file_elf: Path, symbol: str, length: int) -> int | None:
60
67
  elf = ELF(path=file_elf)
61
68
  addr = elf.symbols[symbol]
62
69
  value_raw = elf.read(address=addr, count=length)[-length:]
70
+ endian = elf.endian
63
71
  elf.close()
64
- return int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
72
+ return int.from_bytes(bytes=value_raw, byteorder=endian, signed=False)
65
73
 
66
74
 
67
75
  def read_uid(file_elf: Path) -> int | None:
@@ -76,8 +84,11 @@ def read_arch(file_elf: Path) -> str | None:
76
84
  if ELF is None:
77
85
  raise RuntimeError(elf_error_text)
78
86
  elf = ELF(path=file_elf)
79
- if "exec" in elf.elftype.lower():
80
- return elf.arch.lower()
87
+ elf_type = elf.elftype.lower()
88
+ elf_arch = elf.arch.lower()
89
+ elf.close()
90
+ if "exec" in elf_type:
91
+ return elf_arch
81
92
  log.error("ELF is not Executable")
82
93
  return None
83
94
 
@@ -102,22 +113,37 @@ def modify_symbol_value(
102
113
  return None
103
114
  if ELF is None:
104
115
  raise RuntimeError(elf_error_text)
105
- elf = ELF(path=file_elf)
106
- addr = elf.symbols[symbol]
107
- value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
108
- # ⤷ cutting needed -> msp produces 4b instead of 2
109
- value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
110
- value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
111
-
112
- try:
113
- elf.write(address=addr, data=value_raw)
114
- except AttributeError:
115
- log.warning("ELF-Modifier failed @%s for symbol '%s'", f"0x{addr:X}", symbol)
116
- return None
116
+ with TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
117
+ # switcheroo that also prevents windows bug (overwrite fails)
118
+ file_tmp = Path(tmp) / file_elf.name
119
+ shutil.copy(file_elf, file_tmp)
120
+
121
+ elf = ELF(path=file_elf)
122
+ addr = elf.symbols[symbol]
123
+ value_raw = elf.read(address=addr, count=config.UID_SIZE)[-config.UID_SIZE :]
124
+ # ⤷ cutting needed -> msp produces 4b instead of 2
125
+ value_old = int.from_bytes(bytes=value_raw, byteorder=elf.endian, signed=False)
126
+ value_raw = value.to_bytes(length=config.UID_SIZE, byteorder=elf.endian, signed=False)
127
+
128
+ try:
129
+ elf.write(address=addr, data=value_raw)
130
+ except AttributeError:
131
+ log.warning("ELF-Modifier failed @%s for symbol '%s'", f"0x{addr:X}", symbol)
132
+ elf.close()
133
+ return None
134
+
135
+ file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
136
+ try:
137
+ file_new.unlink(missing_ok=True)
138
+ except PermissionError:
139
+ elf.close()
140
+ log.error(
141
+ "Failed to overwrite file, because it's somehow still in use (typical for WinOS)."
142
+ )
143
+ return None
144
+ elf.save(path=file_new)
145
+ elf.close()
117
146
 
118
- file_new = file_elf if overwrite else file_elf.with_stem(file_elf.stem + "_" + str(value))
119
- elf.save(path=file_new)
120
- elf.close()
121
147
  log.debug(
122
148
  "Value of Symbol '%s' modified: %s -> %s @%s",
123
149
  symbol,
@@ -5,6 +5,7 @@ TODO: Work in Progress.
5
5
  - detection-functions that register in main validator.
6
6
  """
7
7
 
8
+ import shutil
8
9
  import tempfile
9
10
  from pathlib import Path
10
11
 
@@ -92,7 +93,12 @@ def is_elf(file: Path) -> bool:
92
93
  if not file.is_file():
93
94
  return False
94
95
  try:
95
- _ = ELF(path=file)
96
+ with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmp:
97
+ # switcheroo that might prevent windows bug - overwrite fails in modify_symbol_value()
98
+ file_tmp = Path(tmp) / file.name
99
+ shutil.copy(file, file_tmp)
100
+ elf = ELF(path=file_tmp)
101
+ elf.close()
96
102
  except ELFError:
97
103
  log.debug("File %s is not ELF - Magic number does not match", file.name)
98
104
  return False
@@ -21,7 +21,7 @@ except ImportError:
21
21
  psutil = None
22
22
 
23
23
  from pydantic import ConfigDict
24
- from pydantic.types import PositiveInt
24
+ from pydantic import PositiveInt
25
25
 
26
26
  from shepherd_core.data_models import ShpModel
27
27
 
shepherd_core/reader.py CHANGED
@@ -314,16 +314,17 @@ class Reader:
314
314
  voltage_step: float | None = (
315
315
  self.get_config().get("virtual_harvester", {}).get("voltage_step_mV", None)
316
316
  )
317
+ if voltage_step is not None: # convert mV to V
318
+ voltage_step = 1e-3 * voltage_step
317
319
  if voltage_step is None:
318
- dsv = self.ds_voltage[0:2000]
320
+ dsv = self._cal.voltage.raw_to_si(self.ds_voltage[0:2000])
319
321
  diffs_np = np.unique(dsv[1:] - dsv[0:-1], return_counts=False)
320
322
  diffs_ls = [_e for _e in list(np.array(diffs_np)) if _e > 0]
321
323
  # static voltages have 0 steps, so
322
324
  if len(diffs_ls) == 0:
325
+ self._logger.warning("Voltage-Step could not be determined from source-material")
323
326
  return None # or is 0 better? that may provoke div0
324
327
  voltage_step = min(diffs_ls)
325
- if voltage_step is not None:
326
- voltage_step = 1e-3 * voltage_step
327
328
  return voltage_step
328
329
 
329
330
  def get_hrv_config(self) -> dict:
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2025.06.4"
3
+ version: str = "2025.10.1"
@@ -9,15 +9,19 @@ from .virtual_harvester_model import VirtualHarvesterModel
9
9
  from .virtual_harvester_simulation import simulate_harvester
10
10
  from .virtual_source_model import VirtualSourceModel
11
11
  from .virtual_source_simulation import simulate_source
12
+ from .virtual_storage_model import VirtualStorageModel
13
+ from .virtual_storage_simulator import StorageSimulator
12
14
 
13
15
  __all__ = [
14
16
  "ConstantCurrentTarget",
15
17
  "ConstantPowerTarget",
16
18
  "PruCalibration",
17
19
  "ResistiveTarget",
20
+ "StorageSimulator",
18
21
  "VirtualConverterModel",
19
22
  "VirtualHarvesterModel",
20
23
  "VirtualSourceModel",
24
+ "VirtualStorageModel",
21
25
  "simulate_harvester",
22
26
  "simulate_source",
23
27
  ]
@@ -17,8 +17,11 @@ Compromises:
17
17
  import math
18
18
 
19
19
  from shepherd_core.data_models import CalibrationEmulator
20
- from shepherd_core.data_models.content.virtual_source import LUT_SIZE
21
- from shepherd_core.data_models.content.virtual_source import ConverterPRUConfig
20
+ from shepherd_core.data_models.content.virtual_source_config import LUT_SIZE
21
+ from shepherd_core.data_models.content.virtual_source_config import ConverterPRUConfig
22
+ from shepherd_core.data_models.content.virtual_storage_config import StoragePRUConfig
23
+
24
+ from .virtual_storage_model import VirtualStorageModelPRU
22
25
 
23
26
 
24
27
  class PruCalibration:
@@ -59,13 +62,16 @@ class PruCalibration:
59
62
  class VirtualConverterModel:
60
63
  """Ported python version of the pru vCnv."""
61
64
 
62
- def __init__(self, cfg: ConverterPRUConfig, cal: PruCalibration) -> None:
65
+ def __init__(
66
+ self, cfg: ConverterPRUConfig, cal: PruCalibration, storage_cfg: StoragePRUConfig
67
+ ) -> None:
63
68
  self._cal: PruCalibration = cal
64
69
  self._cfg: ConverterPRUConfig = cfg
65
70
 
71
+ self.storage = VirtualStorageModelPRU(storage_cfg)
72
+
66
73
  # simplifications for python
67
74
  self.R_input_kOhm = float(self._cfg.R_input_kOhm_n22) / 2**22
68
- self.Constant_us_per_nF = float(self._cfg.Constant_us_per_nF_n28) / 2**28
69
75
 
70
76
  # boost internal state
71
77
  self.V_input_uV: float = 0.0
@@ -74,7 +80,7 @@ class VirtualConverterModel:
74
80
  self.interval_startup_disabled_drain_n: int = self._cfg.interval_startup_delay_drain_n
75
81
 
76
82
  # container for the stored energy
77
- self.V_mid_uV: float = self._cfg.V_intermediate_init_uV
83
+ self.V_mid_uV: float = self.storage.calc_V_OC_uV()
78
84
 
79
85
  # buck internal state
80
86
  self.enable_storage: bool = (int(self._cfg.converter_mode) & 0b0001) > 0
@@ -83,19 +89,19 @@ class VirtualConverterModel:
83
89
  self.enable_log_mid: bool = (int(self._cfg.converter_mode) & 0b1000) > 0
84
90
  # back-channel to hrv
85
91
  self.feedback_to_hrv: bool = (int(self._cfg.converter_mode) & 0b1_0000) > 0
86
- self.V_input_request_uV: int = self._cfg.V_intermediate_init_uV
92
+ self.V_input_request_uV: int = int(self.V_mid_uV)
87
93
 
88
94
  self.V_out_dac_uV: float = self._cfg.V_output_uV
89
95
  self.V_out_dac_raw: int = self._cal.conv_uV_to_dac_raw(self._cfg.V_output_uV)
90
96
  self.power_good: bool = True
91
97
 
92
98
  # prepare hysteresis-thresholds
93
- self.dV_enable_output_uV: float = self._cfg.dV_enable_output_uV
94
- self.V_enable_output_threshold_uV: float = self._cfg.V_enable_output_threshold_uV
95
- self.V_disable_output_threshold_uV: float = self._cfg.V_disable_output_threshold_uV
99
+ self.dV_mid_enable_output_uV: float = self._cfg.dV_mid_enable_output_uV
100
+ self.V_mid_enable_output_threshold_uV: float = self._cfg.V_mid_enable_output_threshold_uV
101
+ self.V_mid_disable_output_threshold_uV: float = self._cfg.V_mid_disable_output_threshold_uV
96
102
 
97
- self.V_enable_output_threshold_uV = max(
98
- self.dV_enable_output_uV, self.V_enable_output_threshold_uV
103
+ self.V_mid_enable_output_threshold_uV = max(
104
+ self.dV_mid_enable_output_uV, self.V_mid_enable_output_threshold_uV
99
105
  )
100
106
 
101
107
  # pulled from update_states_and_output() due to easier static init
@@ -125,7 +131,7 @@ class VirtualConverterModel:
125
131
  input_voltage_uV = 0.0
126
132
  # TODO: vdrop in case of v_input > v_storage (non-boost)
127
133
  elif self.enable_storage:
128
- # no boost, but cap, for ie. diode+cap (+resistor)
134
+ # no boost, but cap, for i.e. diode+cap (+resistor)
129
135
  V_diff_uV = (
130
136
  (input_voltage_uV - self.V_mid_uV) if (input_voltage_uV >= self.V_mid_uV) else 0
131
137
  )
@@ -162,14 +168,14 @@ class VirtualConverterModel:
162
168
  current_adc_raw = max(0, current_adc_raw)
163
169
  current_adc_raw = min((2**18) - 1, current_adc_raw)
164
170
 
165
- P_leak_fW = self.V_mid_uV * self._cfg.I_intermediate_leak_nA
171
+ # TODO: remove P_leak in C-Code
166
172
  I_out_nA = self._cal.conv_adc_raw_to_nA(current_adc_raw)
167
173
  if self.enable_buck: # noqa: SIM108
168
174
  eta_inv_out = self.get_output_inv_efficiency(I_out_nA)
169
175
  else:
170
176
  eta_inv_out = 1.0
171
177
 
172
- self.P_out_fW = eta_inv_out * self.V_out_dac_uV * I_out_nA + P_leak_fW
178
+ self.P_out_fW = eta_inv_out * self.V_out_dac_uV * I_out_nA
173
179
 
174
180
  if self.interval_startup_disabled_drain_n > 0:
175
181
  self.interval_startup_disabled_drain_n -= 1
@@ -177,17 +183,12 @@ class VirtualConverterModel:
177
183
 
178
184
  return round(self.P_out_fW) # Python-specific, added for easier testing
179
185
 
180
- # TODO: add range-checks for add, sub Ops
181
186
  def update_cap_storage(self) -> int:
182
- # TODO: this calculation is wrong for everything beside boost-cnv
183
187
  if self.enable_storage:
184
- V_mid_prot_uV = max(1.0, self.V_mid_uV)
185
- P_sum_fW = self.P_inp_fW - self.P_out_fW
186
- I_mid_nA = P_sum_fW / V_mid_prot_uV
187
- dV_mid_uV = I_mid_nA * self.Constant_us_per_nF
188
- self.V_mid_uV += dV_mid_uV
189
-
190
- self.V_mid_uV = min(self.V_mid_uV, self._cfg.V_intermediate_max_uV)
188
+ I_delta_nA = abs((self.P_inp_fW - self.P_out_fW) / self.V_mid_uV)
189
+ is_charging: bool = self.P_inp_fW >= self.P_out_fW
190
+ self.V_mid_uV = self.storage.step(2**4 * I_delta_nA, is_charging=is_charging)
191
+ self.V_mid_uV = min(self.V_mid_uV, self._cfg.V_mid_max_uV)
191
192
  self.V_mid_uV = max(self.V_mid_uV, 1)
192
193
  return round(self.V_mid_uV) # Python-specific, added for easier testing
193
194
 
@@ -200,11 +201,11 @@ class VirtualConverterModel:
200
201
  if check_thresholds:
201
202
  self.sample_count = 0
202
203
  if self.is_outputting:
203
- if V_mid_uV_now < self.V_disable_output_threshold_uV:
204
+ if V_mid_uV_now < self.V_mid_disable_output_threshold_uV:
204
205
  self.is_outputting = False
205
- elif V_mid_uV_now >= self.V_enable_output_threshold_uV:
206
+ elif V_mid_uV_now >= self.V_mid_enable_output_threshold_uV:
206
207
  self.is_outputting = True
207
- self.V_mid_uV -= self.dV_enable_output_uV
208
+ self.V_mid_uV -= self.dV_mid_enable_output_uV
208
209
 
209
210
  if check_thresholds or self._cfg.immediate_pwr_good_signal:
210
211
  # generate power-good-signal
@@ -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
@@ -168,10 +175,11 @@ 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]:
@@ -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
@@ -12,9 +12,10 @@ NOTE: DO NOT OPTIMIZE -> stay close to original code-base
12
12
 
13
13
  from shepherd_core.data_models.base.calibration import CalibrationEmulator
14
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
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)