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.
- shepherd_core/data_models/__init__.py +4 -2
- shepherd_core/data_models/base/content.py +2 -0
- shepherd_core/data_models/content/__init__.py +4 -2
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +3 -3
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +82 -58
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +426 -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/observer_features.py +8 -2
- shepherd_core/data_models/experiment/target_config.py +1 -1
- shepherd_core/data_models/task/emulation.py +9 -6
- shepherd_core/data_models/task/firmware_mod.py +1 -0
- shepherd_core/data_models/task/harvest.py +4 -4
- shepherd_core/data_models/task/observer_tasks.py +5 -2
- shepherd_core/data_models/task/programming.py +1 -0
- shepherd_core/data_models/task/testbed_tasks.py +6 -1
- shepherd_core/decoder_waveform/uart.py +2 -1
- shepherd_core/fw_tools/patcher.py +60 -34
- shepherd_core/fw_tools/validation.py +7 -1
- shepherd_core/inventory/system.py +1 -1
- shepherd_core/reader.py +4 -3
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +27 -26
- shepherd_core/vsource/virtual_harvester_model.py +27 -19
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +17 -13
- 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-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/METADATA +4 -6
- {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/RECORD +44 -32
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/WHEEL +0 -0
- {shepherd_core-2025.6.4.dist-info → shepherd_core-2025.10.1.dist-info}/top_level.txt +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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=
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
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
|
@@ -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.
|
|
21
|
-
from shepherd_core.data_models.content.
|
|
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__(
|
|
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.
|
|
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.
|
|
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.
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
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.
|
|
98
|
-
self.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
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
|
|
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
|
|
@@ -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.
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
17
|
-
from shepherd_core.data_models.content.
|
|
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)
|