shepherd-core 2025.8.1__py3-none-any.whl → 2026.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shepherd_core/config.py +1 -1
- shepherd_core/data_models/__init__.py +8 -4
- shepherd_core/data_models/base/cal_measurement.py +7 -2
- shepherd_core/data_models/base/calibration.py +23 -12
- shepherd_core/data_models/base/content.py +12 -2
- shepherd_core/data_models/base/shepherd.py +13 -4
- shepherd_core/data_models/base/wrapper.py +2 -0
- shepherd_core/data_models/content/__init__.py +8 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
- shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
- shepherd_core/data_models/content/energy_environment.py +341 -23
- shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +44 -16
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +429 -0
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
- shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
- shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
- shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
- shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
- shepherd_core/data_models/experiment/experiment.py +38 -13
- shepherd_core/data_models/experiment/observer_features.py +17 -4
- shepherd_core/data_models/experiment/target_config.py +56 -8
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +10 -6
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +3 -1
- shepherd_core/data_models/task/helper_paths.py +2 -2
- shepherd_core/data_models/task/observer_tasks.py +8 -6
- shepherd_core/data_models/task/programming.py +4 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -2
- shepherd_core/data_models/testbed/cape.py +2 -0
- shepherd_core/data_models/testbed/gpio.py +2 -0
- shepherd_core/data_models/testbed/mcu.py +2 -0
- shepherd_core/data_models/testbed/observer.py +2 -0
- shepherd_core/data_models/testbed/target.py +7 -5
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +17 -15
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/exit_handler.py +22 -0
- shepherd_core/fw_tools/converter.py +2 -2
- shepherd_core/fw_tools/validation.py +1 -1
- shepherd_core/inventory/__init__.py +23 -21
- shepherd_core/inventory/system.py +3 -3
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +32 -27
- shepherd_core/testbed_client/cache_path.py +3 -3
- shepherd_core/testbed_client/client_abc_fix.py +14 -3
- shepherd_core/testbed_client/client_web.py +7 -5
- shepherd_core/testbed_client/fixtures.py +7 -7
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +29 -28
- shepherd_core/vsource/virtual_harvester_model.py +29 -21
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +18 -14
- shepherd_core/vsource/virtual_source_simulation.py +71 -73
- shepherd_core/vsource/virtual_storage_model.py +164 -0
- shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
- shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
- shepherd_core/vsource/virtual_storage_simulator.py +104 -0
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- shepherd_core-2025.8.1.dist-info/RECORD +0 -83
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Generalized exit handler for Shepherd."""
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from types import FrameType
|
|
7
|
+
|
|
8
|
+
from .logger import log
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def exit_gracefully(_signum: int, _frame: FrameType | None) -> None:
|
|
12
|
+
"""Usual exit handler for single-processing applications."""
|
|
13
|
+
log.warning("Exiting!")
|
|
14
|
+
sys.exit(0)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def activate_exit_handler(custom: Callable = exit_gracefully) -> None:
|
|
18
|
+
"""Register the provided exit handler or use the default one."""
|
|
19
|
+
signal.signal(signal.SIGTERM, custom)
|
|
20
|
+
signal.signal(signal.SIGINT, custom)
|
|
21
|
+
if hasattr(signal, "SIGALRM"):
|
|
22
|
+
signal.signal(signal.SIGALRM, custom)
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
import zstandard as zstd
|
|
9
9
|
from pydantic import validate_call
|
|
10
10
|
|
|
11
|
-
from shepherd_core.data_models.content.
|
|
11
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
12
12
|
|
|
13
13
|
from .converter_elf import elf_to_hex
|
|
14
14
|
from .validation import is_elf
|
|
@@ -98,7 +98,7 @@ def extract_firmware(data: str | Path, data_type: FirmwareDType, file_path: Path
|
|
|
98
98
|
elif data_type == FirmwareDType.path_hex:
|
|
99
99
|
file = file_path.with_suffix(".hex")
|
|
100
100
|
else:
|
|
101
|
-
msg = "FW-Extraction failed due to unknown datatype '{data_type}'"
|
|
101
|
+
msg = f"FW-Extraction failed due to unknown datatype '{data_type}'"
|
|
102
102
|
raise ValueError(msg)
|
|
103
103
|
if not file.parent.exists():
|
|
104
104
|
file.parent.mkdir(parents=True)
|
|
@@ -13,7 +13,7 @@ from intelhex import IntelHex
|
|
|
13
13
|
from intelhex import IntelHexError
|
|
14
14
|
from pydantic import validate_call
|
|
15
15
|
|
|
16
|
-
from shepherd_core.data_models.content.
|
|
16
|
+
from shepherd_core.data_models.content.enum_datatypes import FirmwareDType
|
|
17
17
|
from shepherd_core.logger import log
|
|
18
18
|
|
|
19
19
|
from .converter_elf import elf_to_hex
|
|
@@ -10,6 +10,7 @@ from datetime import datetime
|
|
|
10
10
|
from datetime import timedelta
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Annotated
|
|
13
|
+
from typing import final
|
|
13
14
|
|
|
14
15
|
from pydantic import Field
|
|
15
16
|
from typing_extensions import Self
|
|
@@ -29,6 +30,7 @@ __all__ = [
|
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
@final
|
|
32
34
|
class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
33
35
|
"""Complete inventory for one device.
|
|
34
36
|
|
|
@@ -65,7 +67,7 @@ class InventoryList(ShpModel):
|
|
|
65
67
|
np.concatenate(content).reshape((len(content), len(content[0]))).
|
|
66
68
|
"""
|
|
67
69
|
if path.is_dir():
|
|
68
|
-
path
|
|
70
|
+
path /= "inventory.yaml"
|
|
69
71
|
with path.resolve().open("w") as fd:
|
|
70
72
|
fd.write(", ".join(self.elements[0].model_dump().keys()) + "\r\n")
|
|
71
73
|
for item in self.elements:
|
|
@@ -75,28 +77,28 @@ class InventoryList(ShpModel):
|
|
|
75
77
|
|
|
76
78
|
def warn(self) -> dict:
|
|
77
79
|
warnings = {}
|
|
78
|
-
ts_earl = min(
|
|
79
|
-
for
|
|
80
|
-
if
|
|
81
|
-
warnings["uptime"] = f"[{
|
|
82
|
-
if (
|
|
83
|
-
warnings["time_delta"] = f"[{
|
|
80
|
+
ts_earl = min(e_.created.timestamp() for e_ in self.elements)
|
|
81
|
+
for e_ in self.elements:
|
|
82
|
+
if e_.uptime > timedelta(hours=30).total_seconds():
|
|
83
|
+
warnings["uptime"] = f"[{e_.hostname}] restart is recommended"
|
|
84
|
+
if (e_.created.timestamp() - ts_earl) > 10:
|
|
85
|
+
warnings["time_delta"] = f"[{e_.hostname}] time-sync has failed"
|
|
84
86
|
|
|
85
87
|
# turn dict[hostname][type] = val
|
|
86
88
|
# to dict[type][val] = list[hostnames]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for
|
|
89
|
+
inp_ = {
|
|
90
|
+
e_.hostname: e_.model_dump(exclude_unset=True, exclude_defaults=True)
|
|
91
|
+
for e_ in self.elements
|
|
90
92
|
}
|
|
91
93
|
result = {}
|
|
92
|
-
for
|
|
93
|
-
for
|
|
94
|
-
if
|
|
95
|
-
result[
|
|
96
|
-
if
|
|
97
|
-
result[
|
|
98
|
-
result[
|
|
99
|
-
rescnt = {
|
|
94
|
+
for host_, types_ in inp_.items():
|
|
95
|
+
for type_, val_ in types_.items():
|
|
96
|
+
if type_ not in result:
|
|
97
|
+
result[type_] = {}
|
|
98
|
+
if val_ not in result[type_]:
|
|
99
|
+
result[type_][val_] = []
|
|
100
|
+
result[type_][val_].append(host_)
|
|
101
|
+
rescnt = {key_: len(val_) for key_, val_ in result.items()}
|
|
100
102
|
t_unique = [
|
|
101
103
|
"h5py",
|
|
102
104
|
"numpy",
|
|
@@ -107,9 +109,9 @@ class InventoryList(ShpModel):
|
|
|
107
109
|
"yaml",
|
|
108
110
|
"zstandard",
|
|
109
111
|
]
|
|
110
|
-
for
|
|
111
|
-
if rescnt[
|
|
112
|
-
warnings[
|
|
112
|
+
for key_ in t_unique:
|
|
113
|
+
if rescnt[key_] > 1:
|
|
114
|
+
warnings[key_] = f"[{key_}] VersionMismatch - {result[key_]}"
|
|
113
115
|
|
|
114
116
|
# TODO: finish with more potential warnings
|
|
115
117
|
return warnings
|
|
@@ -21,7 +21,7 @@ except ImportError:
|
|
|
21
21
|
psutil = None
|
|
22
22
|
|
|
23
23
|
from pydantic import ConfigDict
|
|
24
|
-
from pydantic
|
|
24
|
+
from pydantic import PositiveInt
|
|
25
25
|
|
|
26
26
|
from shepherd_core.data_models import ShpModel
|
|
27
27
|
|
|
@@ -63,12 +63,12 @@ class SystemInventory(ShpModel):
|
|
|
63
63
|
uptime = 0
|
|
64
64
|
log.warning(
|
|
65
65
|
"Inventory-Parameters will be missing. "
|
|
66
|
-
"Please install functionality with "
|
|
66
|
+
"Please install functionality with i.e."
|
|
67
67
|
"'pip install shepherd_core[inventory] -U' first"
|
|
68
68
|
)
|
|
69
69
|
else:
|
|
70
70
|
ifs1 = psutil.net_if_addrs().items()
|
|
71
|
-
ifs2 = {name: (
|
|
71
|
+
ifs2 = {name: (if_[1].address, if_[0].address) for name, if_ in ifs1 if len(if_) > 1}
|
|
72
72
|
uptime = time.time() - psutil.boot_time()
|
|
73
73
|
|
|
74
74
|
fs_cmd = ["/usr/bin/df", "-h", "/"]
|
shepherd_core/logger.py
CHANGED
shepherd_core/reader.py
CHANGED
|
@@ -13,6 +13,7 @@ from pathlib import Path
|
|
|
13
13
|
from types import MappingProxyType
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
from typing import Any
|
|
16
|
+
from typing import final
|
|
16
17
|
|
|
17
18
|
import h5py
|
|
18
19
|
import numpy as np
|
|
@@ -26,7 +27,7 @@ from .config import config
|
|
|
26
27
|
from .data_models.base.calibration import CalibrationPair
|
|
27
28
|
from .data_models.base.calibration import CalibrationSeries
|
|
28
29
|
from .data_models.base.timezone import local_tz
|
|
29
|
-
from .data_models.content.
|
|
30
|
+
from .data_models.content.enum_datatypes import EnergyDType
|
|
30
31
|
from .decoder_waveform import Uart
|
|
31
32
|
|
|
32
33
|
if TYPE_CHECKING:
|
|
@@ -46,7 +47,7 @@ class Reader:
|
|
|
46
47
|
|
|
47
48
|
"""
|
|
48
49
|
|
|
49
|
-
CHUNK_SAMPLES_N: int = 10_000
|
|
50
|
+
CHUNK_SAMPLES_N: final[int] = 10_000
|
|
50
51
|
|
|
51
52
|
MODE_TO_DTYPE: Mapping[str, Sequence[EnergyDType]] = MappingProxyType(
|
|
52
53
|
{
|
|
@@ -101,9 +102,9 @@ class Reader:
|
|
|
101
102
|
try:
|
|
102
103
|
self.h5file = h5py.File(self.file_path, "r") # = readonly
|
|
103
104
|
self._reader_opened = True
|
|
104
|
-
except OSError as
|
|
105
|
+
except OSError as xcp:
|
|
105
106
|
msg = f"Unable to open HDF5-File '{self.file_path.name}'"
|
|
106
|
-
raise TypeError(msg) from
|
|
107
|
+
raise TypeError(msg) from xcp
|
|
107
108
|
|
|
108
109
|
if self.is_valid():
|
|
109
110
|
self._logger.debug("File is available now")
|
|
@@ -209,13 +210,14 @@ class Reader:
|
|
|
209
210
|
Generator - can be configured on first call
|
|
210
211
|
|
|
211
212
|
Args:
|
|
212
|
-
|
|
213
|
-
:
|
|
214
|
-
:
|
|
215
|
-
:
|
|
216
|
-
:
|
|
217
|
-
|
|
218
|
-
Yields:
|
|
213
|
+
start_n: (int) Index of first chunk to be read
|
|
214
|
+
end_n: (int) Index of last chunk to be read
|
|
215
|
+
n_samples_per_chunk: (int) allows changing
|
|
216
|
+
is_raw: (bool) output original data, not transformed to SI-Units
|
|
217
|
+
omit_timestamps: (bool) optimize reading if timestamp is never used
|
|
218
|
+
|
|
219
|
+
Yields:
|
|
220
|
+
chunks between start and end (tuple with time, voltage, current)
|
|
219
221
|
|
|
220
222
|
"""
|
|
221
223
|
if n_samples_per_chunk is None:
|
|
@@ -223,21 +225,21 @@ class Reader:
|
|
|
223
225
|
end_max = int(self.samples_n // n_samples_per_chunk)
|
|
224
226
|
end_n = end_max if end_n is None else min(end_n, end_max)
|
|
225
227
|
self._logger.debug("Reading chunk %d to %d from source-file", start_n, end_n)
|
|
226
|
-
|
|
227
|
-
|
|
228
|
+
raw_ = is_raw
|
|
229
|
+
wts_ = not omit_timestamps
|
|
228
230
|
|
|
229
231
|
for i in range(start_n, end_n):
|
|
230
232
|
idx_start = i * n_samples_per_chunk
|
|
231
233
|
idx_end = idx_start + n_samples_per_chunk
|
|
232
|
-
if
|
|
234
|
+
if raw_:
|
|
233
235
|
yield (
|
|
234
|
-
self.ds_time[idx_start:idx_end] if
|
|
236
|
+
self.ds_time[idx_start:idx_end] if wts_ else None,
|
|
235
237
|
self.ds_voltage[idx_start:idx_end],
|
|
236
238
|
self.ds_current[idx_start:idx_end],
|
|
237
239
|
)
|
|
238
240
|
else:
|
|
239
241
|
yield (
|
|
240
|
-
self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]) if
|
|
242
|
+
self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]) if wts_ else None,
|
|
241
243
|
self._cal.voltage.raw_to_si(self.ds_voltage[idx_start:idx_end]),
|
|
242
244
|
self._cal.current.raw_to_si(self.ds_current[idx_start:idx_end]),
|
|
243
245
|
)
|
|
@@ -314,16 +316,17 @@ class Reader:
|
|
|
314
316
|
voltage_step: float | None = (
|
|
315
317
|
self.get_config().get("virtual_harvester", {}).get("voltage_step_mV", None)
|
|
316
318
|
)
|
|
319
|
+
if voltage_step is not None: # convert mV to V
|
|
320
|
+
voltage_step *= 1e-3
|
|
317
321
|
if voltage_step is None:
|
|
318
|
-
dsv = self.ds_voltage[0:2000]
|
|
322
|
+
dsv = self._cal.voltage.raw_to_si(self.ds_voltage[0:2000])
|
|
319
323
|
diffs_np = np.unique(dsv[1:] - dsv[0:-1], return_counts=False)
|
|
320
|
-
diffs_ls = [
|
|
324
|
+
diffs_ls = [e_ for e_ in list(np.array(diffs_np)) if e_ > 0]
|
|
321
325
|
# static voltages have 0 steps, so
|
|
322
326
|
if len(diffs_ls) == 0:
|
|
327
|
+
self._logger.warning("Voltage-Step could not be determined from source-material")
|
|
323
328
|
return None # or is 0 better? that may provoke div0
|
|
324
329
|
voltage_step = min(diffs_ls)
|
|
325
|
-
if voltage_step is not None:
|
|
326
|
-
voltage_step = 1e-3 * voltage_step
|
|
327
330
|
return voltage_step
|
|
328
331
|
|
|
329
332
|
def get_hrv_config(self) -> dict:
|
|
@@ -456,11 +459,11 @@ class Reader:
|
|
|
456
459
|
"[FileValidation] Hostname was not set in '%s'", self.file_path.name
|
|
457
460
|
)
|
|
458
461
|
# errors during execution
|
|
459
|
-
|
|
460
|
-
if
|
|
462
|
+
err_ = self.count_errors_in_log()
|
|
463
|
+
if err_ > 0:
|
|
461
464
|
self._logger.warning(
|
|
462
465
|
"[FileValidation] Sheep reported %d errors during execution -> check logs in '%s'",
|
|
463
|
-
|
|
466
|
+
err_,
|
|
464
467
|
self.file_path.name,
|
|
465
468
|
)
|
|
466
469
|
return True
|
|
@@ -483,6 +486,8 @@ class Reader:
|
|
|
483
486
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
484
487
|
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
|
|
485
488
|
|
|
489
|
+
TODO: add optional duration argument to allow calculating mean energy of a spatial EEnv
|
|
490
|
+
|
|
486
491
|
:return: sampled energy in Ws (watt-seconds)
|
|
487
492
|
"""
|
|
488
493
|
iterations = math.ceil(self.samples_n / self.max_elements)
|
|
@@ -602,11 +607,11 @@ class Reader:
|
|
|
602
607
|
return 0
|
|
603
608
|
if "level" not in self.h5file[group_name]:
|
|
604
609
|
return 0
|
|
605
|
-
|
|
606
|
-
if
|
|
610
|
+
lvl_ = self.h5file[group_name]["level"]
|
|
611
|
+
if lvl_.shape[0] < 1:
|
|
607
612
|
return 0
|
|
608
|
-
|
|
609
|
-
return len(
|
|
613
|
+
items_ = [1 for x_ in lvl_[:] if x_ >= min_level]
|
|
614
|
+
return len(items_)
|
|
610
615
|
|
|
611
616
|
def get_metadata(
|
|
612
617
|
self,
|
|
@@ -5,10 +5,10 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
|
|
8
|
-
|
|
9
|
-
if
|
|
8
|
+
value_ = os.environ.get(variable_name)
|
|
9
|
+
if not value_: # catches None and ""
|
|
10
10
|
return default_path
|
|
11
|
-
return Path(
|
|
11
|
+
return Path(value_)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
user_path = Path("~").expanduser()
|
|
@@ -18,9 +18,11 @@ TODO: Comfort functions missing
|
|
|
18
18
|
from abc import ABC
|
|
19
19
|
from abc import abstractmethod
|
|
20
20
|
from typing import Any
|
|
21
|
+
from typing import final
|
|
21
22
|
|
|
22
23
|
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
23
24
|
from shepherd_core.data_models.base.wrapper import Wrapper
|
|
25
|
+
from shepherd_core.logger import log
|
|
24
26
|
|
|
25
27
|
from .fixtures import Fixtures
|
|
26
28
|
|
|
@@ -58,6 +60,7 @@ class AbcClient(ABC):
|
|
|
58
60
|
# TODO: maybe internal? yes
|
|
59
61
|
pass
|
|
60
62
|
|
|
63
|
+
@final
|
|
61
64
|
def try_completing_model(
|
|
62
65
|
self, model_type: str, values: dict[str, Any]
|
|
63
66
|
) -> tuple[dict[str, Any], list[str]]:
|
|
@@ -71,14 +74,18 @@ class AbcClient(ABC):
|
|
|
71
74
|
except ValueError as err:
|
|
72
75
|
msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
|
|
73
76
|
raise ValueError(msg) from err
|
|
77
|
+
except KeyError:
|
|
78
|
+
log.error(f"Query failed - model-type {model_type} is unknown")
|
|
79
|
+
return values, []
|
|
74
80
|
return self.try_inheritance(model_type, values)
|
|
75
81
|
|
|
76
82
|
@abstractmethod
|
|
77
83
|
def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
-
# TODO: is it really
|
|
84
|
+
# TODO: is it really needed and helpful?
|
|
79
85
|
pass
|
|
80
86
|
|
|
81
87
|
|
|
88
|
+
@final
|
|
82
89
|
class FixturesClient(AbcClient):
|
|
83
90
|
"""Client-Class to access the file based fixtures."""
|
|
84
91
|
|
|
@@ -110,12 +117,16 @@ class FixturesClient(AbcClient):
|
|
|
110
117
|
def try_inheritance(
|
|
111
118
|
self, model_type: str, values: dict[str, Any]
|
|
112
119
|
) -> tuple[dict[str, Any], list[str]]:
|
|
113
|
-
|
|
120
|
+
try:
|
|
121
|
+
return self._fixtures[model_type].inheritance(values)
|
|
122
|
+
except KeyError:
|
|
123
|
+
log.error(f"Query failed - model-type {model_type} is unknown")
|
|
124
|
+
return values, []
|
|
114
125
|
|
|
115
126
|
def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
|
|
116
127
|
"""Add fake user-data when offline-client is used.
|
|
117
128
|
|
|
118
|
-
|
|
129
|
+
Workaround until WebClient is working.
|
|
119
130
|
"""
|
|
120
131
|
if values.get("owner") is None:
|
|
121
132
|
values["owner"] = "unknown"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from importlib import import_module
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
from pydantic import validate_call
|
|
8
9
|
|
|
@@ -14,6 +15,7 @@ from .client_abc_fix import AbcClient
|
|
|
14
15
|
from .user_model import User
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@final
|
|
17
19
|
class WebClient(AbcClient):
|
|
18
20
|
"""Client-Class to access a testbed instance over the web.
|
|
19
21
|
|
|
@@ -125,26 +127,26 @@ class WebClient(AbcClient):
|
|
|
125
127
|
return False
|
|
126
128
|
|
|
127
129
|
def submit_experiment(self, xp: ShpModel) -> str:
|
|
128
|
-
"""Transmit
|
|
130
|
+
"""Transmit experiment to server to validate its feasibility.
|
|
129
131
|
|
|
130
132
|
- Experiment will be added to DB (if not present)
|
|
131
|
-
- if the same experiment is resubmitted it will just return the ID of that
|
|
133
|
+
- if the same experiment is resubmitted it will just return the ID of that experiment
|
|
132
134
|
- Experiment will be validated by converting it into a task-set (additional validation)
|
|
133
135
|
- optional: the scheduler should validate there are no time-collisions
|
|
134
136
|
|
|
135
137
|
Will return an ID if valid, otherwise an empty string.
|
|
136
138
|
TODO: maybe its better to throw specific errors if validation fails
|
|
137
|
-
TODO: is it better to include these experiment-related FNs in
|
|
139
|
+
TODO: is it better to include these experiment-related FNs in experiment-Class?
|
|
138
140
|
TODO: Experiment-typehint for argument triggers circular import
|
|
139
141
|
"""
|
|
140
142
|
raise NotImplementedError("TODO")
|
|
141
143
|
|
|
142
144
|
def schedule_experiment(self, id_xp: str) -> bool:
|
|
143
|
-
"""Enqueue
|
|
145
|
+
"""Enqueue experiment on testbed."""
|
|
144
146
|
raise NotImplementedError("TODO")
|
|
145
147
|
|
|
146
148
|
def get_experiment_status(self, id_xp: str) -> str:
|
|
147
|
-
"""Ask server about current state of
|
|
149
|
+
"""Ask server about current state of experiment.
|
|
148
150
|
|
|
149
151
|
- after valid submission: disabled / deactivated
|
|
150
152
|
- after scheduling: scheduled
|
|
@@ -48,10 +48,10 @@ class Fixture:
|
|
|
48
48
|
if "name" not in data.parameters:
|
|
49
49
|
return
|
|
50
50
|
name = str(data.parameters["name"]).lower()
|
|
51
|
-
|
|
51
|
+
id_ = data.parameters["id"]
|
|
52
52
|
data_model = data.parameters
|
|
53
53
|
self.elements_by_name[name] = data_model
|
|
54
|
-
self.elements_by_id[
|
|
54
|
+
self.elements_by_id[id_] = data_model
|
|
55
55
|
# update iterator
|
|
56
56
|
self._iter_list: list[dict[str, Any]] = list(self.elements_by_name.values())
|
|
57
57
|
|
|
@@ -84,7 +84,7 @@ class Fixture:
|
|
|
84
84
|
return self.elements_by_name.keys()
|
|
85
85
|
|
|
86
86
|
def refs(self) -> dict:
|
|
87
|
-
return {
|
|
87
|
+
return {i_["id"]: i_["name"] for i_ in self.elements_by_id.values()}
|
|
88
88
|
|
|
89
89
|
def inheritance(
|
|
90
90
|
self, values: dict[str, Any], chain: list[str] | None = None
|
|
@@ -123,8 +123,8 @@ class Fixture:
|
|
|
123
123
|
post_process = True
|
|
124
124
|
|
|
125
125
|
elif values.get("id") in self.elements_by_id:
|
|
126
|
-
|
|
127
|
-
fixture_base = copy.copy(self.elements_by_id[
|
|
126
|
+
id_ = values["id"]
|
|
127
|
+
fixture_base = copy.copy(self.elements_by_id[id_])
|
|
128
128
|
post_process = True
|
|
129
129
|
|
|
130
130
|
if post_process:
|
|
@@ -218,7 +218,7 @@ class Fixtures:
|
|
|
218
218
|
|
|
219
219
|
@validate_call
|
|
220
220
|
def insert_file(self, file: Path) -> None:
|
|
221
|
-
with file.open() as fd:
|
|
221
|
+
with file.open(encoding="utf-8-sig") as fd:
|
|
222
222
|
fixtures = yaml.safe_load(fd)
|
|
223
223
|
for fixture in fixtures:
|
|
224
224
|
if not isinstance(fixture, dict):
|
|
@@ -237,7 +237,7 @@ class Fixtures:
|
|
|
237
237
|
if key in self.components:
|
|
238
238
|
return self.components[key]
|
|
239
239
|
msg = f"Component '{key}' not found!"
|
|
240
|
-
raise
|
|
240
|
+
raise KeyError(msg)
|
|
241
241
|
|
|
242
242
|
def keys(self) -> Iterable[str]:
|
|
243
243
|
return self.components.keys()
|
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:
|
|
@@ -32,7 +35,7 @@ class PruCalibration:
|
|
|
32
35
|
negative_residue_nA = 0
|
|
33
36
|
|
|
34
37
|
def __init__(self, cal_emu: CalibrationEmulator | None = None) -> None:
|
|
35
|
-
self.cal = cal_emu
|
|
38
|
+
self.cal = cal_emu or CalibrationEmulator()
|
|
36
39
|
|
|
37
40
|
def conv_adc_raw_to_nA(self, current_raw: int) -> float:
|
|
38
41
|
I_nA = self.cal.adc_C_A.raw_to_si(current_raw) * (10**9)
|
|
@@ -41,7 +44,7 @@ class PruCalibration:
|
|
|
41
44
|
I_nA -= self.negative_residue_nA
|
|
42
45
|
self.negative_residue_nA = 0
|
|
43
46
|
else:
|
|
44
|
-
self.negative_residue_nA
|
|
47
|
+
self.negative_residue_nA -= I_nA
|
|
45
48
|
self.negative_residue_nA = min(self.negative_residue_nA, self.RESIDUE_MAX_nA)
|
|
46
49
|
I_nA = 0
|
|
47
50
|
return I_nA
|
|
@@ -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
|