shepherd-core 2023.12.1__py3-none-any.whl → 2024.4.2__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/__init__.py +5 -4
- shepherd_core/calibration_hw_def.py +9 -1
- shepherd_core/commons.py +2 -0
- shepherd_core/data_models/__init__.py +11 -0
- shepherd_core/data_models/base/__init__.py +4 -1
- shepherd_core/data_models/base/cal_measurement.py +18 -6
- shepherd_core/data_models/base/calibration.py +41 -16
- shepherd_core/data_models/base/content.py +20 -5
- shepherd_core/data_models/base/shepherd.py +23 -12
- shepherd_core/data_models/base/timezone.py +5 -0
- shepherd_core/data_models/base/wrapper.py +3 -3
- shepherd_core/data_models/content/__init__.py +5 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +32 -16
- shepherd_core/data_models/content/energy_environment.py +7 -5
- shepherd_core/data_models/content/energy_environment_fixture.yaml +3 -0
- shepherd_core/data_models/content/firmware.py +12 -5
- shepherd_core/data_models/content/firmware_datatype.py +7 -0
- shepherd_core/data_models/content/virtual_harvester.py +25 -20
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +1 -0
- shepherd_core/data_models/content/virtual_source.py +40 -23
- shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -0
- shepherd_core/data_models/experiment/__init__.py +5 -4
- shepherd_core/data_models/experiment/experiment.py +16 -15
- shepherd_core/data_models/experiment/observer_features.py +18 -12
- shepherd_core/data_models/experiment/target_config.py +11 -7
- shepherd_core/data_models/readme.md +88 -0
- shepherd_core/data_models/task/__init__.py +10 -3
- shepherd_core/data_models/task/emulation.py +9 -6
- shepherd_core/data_models/task/firmware_mod.py +4 -2
- shepherd_core/data_models/task/harvest.py +5 -4
- shepherd_core/data_models/task/observer_tasks.py +4 -2
- shepherd_core/data_models/task/programming.py +3 -1
- shepherd_core/data_models/task/testbed_tasks.py +10 -4
- shepherd_core/data_models/testbed/__init__.py +5 -2
- shepherd_core/data_models/testbed/cape.py +8 -6
- shepherd_core/data_models/testbed/gpio.py +11 -9
- shepherd_core/data_models/testbed/mcu.py +10 -10
- shepherd_core/data_models/testbed/observer.py +10 -5
- shepherd_core/data_models/testbed/observer_fixture.yaml +23 -22
- shepherd_core/data_models/testbed/target.py +5 -3
- shepherd_core/data_models/testbed/target_fixture.yaml +11 -11
- shepherd_core/data_models/testbed/testbed.py +6 -3
- shepherd_core/decoder_waveform/__init__.py +2 -0
- shepherd_core/decoder_waveform/uart.py +44 -25
- shepherd_core/fw_tools/__init__.py +2 -0
- shepherd_core/fw_tools/converter.py +20 -9
- shepherd_core/fw_tools/converter_elf.py +3 -0
- shepherd_core/fw_tools/patcher.py +16 -4
- shepherd_core/fw_tools/validation.py +25 -5
- shepherd_core/inventory/__init__.py +66 -6
- shepherd_core/inventory/python.py +4 -0
- shepherd_core/inventory/system.py +13 -1
- shepherd_core/inventory/target.py +4 -0
- shepherd_core/logger.py +5 -0
- shepherd_core/reader.py +44 -26
- shepherd_core/testbed_client/__init__.py +2 -0
- shepherd_core/testbed_client/cache_path.py +17 -0
- shepherd_core/testbed_client/client.py +14 -8
- shepherd_core/testbed_client/fixtures.py +30 -11
- shepherd_core/testbed_client/user_model.py +13 -6
- shepherd_core/vsource/__init__.py +2 -0
- shepherd_core/vsource/virtual_converter_model.py +11 -4
- shepherd_core/vsource/virtual_harvester_model.py +8 -1
- shepherd_core/vsource/virtual_source_model.py +10 -5
- shepherd_core/writer.py +28 -20
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/METADATA +50 -34
- shepherd_core-2024.4.2.dist-info/RECORD +75 -0
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/WHEEL +1 -1
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/top_level.txt +0 -1
- shepherd_core-2023.12.1.dist-info/RECORD +0 -117
- tests/__init__.py +0 -0
- tests/conftest.py +0 -64
- tests/data_models/__init__.py +0 -0
- tests/data_models/conftest.py +0 -14
- tests/data_models/example_cal_data.yaml +0 -31
- tests/data_models/example_cal_data_faulty.yaml +0 -29
- tests/data_models/example_cal_meas.yaml +0 -178
- tests/data_models/example_cal_meas_faulty1.yaml +0 -142
- tests/data_models/example_cal_meas_faulty2.yaml +0 -136
- tests/data_models/example_config_emulator.yaml +0 -41
- tests/data_models/example_config_experiment.yaml +0 -16
- tests/data_models/example_config_experiment_alternative.yaml +0 -14
- tests/data_models/example_config_harvester.yaml +0 -15
- tests/data_models/example_config_testbed.yaml +0 -26
- tests/data_models/example_config_virtsource.yaml +0 -78
- tests/data_models/test_base_models.py +0 -205
- tests/data_models/test_content_fixtures.py +0 -41
- tests/data_models/test_content_models.py +0 -282
- tests/data_models/test_examples.py +0 -48
- tests/data_models/test_experiment_models.py +0 -277
- tests/data_models/test_task_generation.py +0 -52
- tests/data_models/test_task_models.py +0 -131
- tests/data_models/test_testbed_fixtures.py +0 -47
- tests/data_models/test_testbed_models.py +0 -187
- tests/decoder_waveform/__init__.py +0 -0
- tests/decoder_waveform/test_decoder.py +0 -34
- tests/fw_tools/__init__.py +0 -0
- tests/fw_tools/conftest.py +0 -5
- tests/fw_tools/test_converter.py +0 -76
- tests/fw_tools/test_patcher.py +0 -66
- tests/fw_tools/test_validation.py +0 -56
- tests/inventory/__init__.py +0 -0
- tests/inventory/test_inventory.py +0 -20
- tests/test_cal_hw.py +0 -34
- tests/test_examples.py +0 -40
- tests/test_logger.py +0 -15
- tests/test_reader.py +0 -283
- tests/test_writer.py +0 -169
- tests/testbed_client/__init__.py +0 -0
- tests/vsource/__init__.py +0 -0
- tests/vsource/conftest.py +0 -49
- tests/vsource/test_converter.py +0 -161
- tests/vsource/test_harvester.py +0 -73
- tests/vsource/test_z.py +0 -5
- /shepherd_core/data_models/{doc_virtual_source.py → virtual_source_doc.txt} +0 -0
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.2.dist-info}/zip-safe +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Helper-functions for firmware-validation.
|
|
2
2
|
|
|
3
|
+
TODO: Work in Progress.
|
|
4
|
+
- Each arch should have its own file and
|
|
5
|
+
- detection-functions that register in main validator.
|
|
3
6
|
"""
|
|
7
|
+
|
|
4
8
|
import tempfile
|
|
5
9
|
from pathlib import Path
|
|
6
10
|
|
|
@@ -26,6 +30,7 @@ except ImportError as e:
|
|
|
26
30
|
|
|
27
31
|
@validate_call
|
|
28
32
|
def is_hex(file: Path) -> bool:
|
|
33
|
+
"""Check if file is containing intel-hex data."""
|
|
29
34
|
try:
|
|
30
35
|
_ = IntelHex(file.as_posix())
|
|
31
36
|
except ValueError: # parsing
|
|
@@ -36,7 +41,9 @@ def is_hex(file: Path) -> bool:
|
|
|
36
41
|
|
|
37
42
|
|
|
38
43
|
def is_hex_msp430(file: Path) -> bool:
|
|
39
|
-
"""
|
|
44
|
+
"""Try to detect specifics for that MCU.
|
|
45
|
+
|
|
46
|
+
Observations:
|
|
40
47
|
- addresses begin at 0x4000
|
|
41
48
|
- value @0xFFFE (IVT) is start_address (of pgm-code)
|
|
42
49
|
"""
|
|
@@ -57,7 +64,9 @@ def is_hex_msp430(file: Path) -> bool:
|
|
|
57
64
|
|
|
58
65
|
|
|
59
66
|
def is_hex_nrf52(file: Path) -> bool:
|
|
60
|
-
"""
|
|
67
|
+
"""Try to detect specifics for that MCU.
|
|
68
|
+
|
|
69
|
+
Observations:
|
|
61
70
|
- addresses begin at 0x0
|
|
62
71
|
- only one segment (.get_segments), todo
|
|
63
72
|
"""
|
|
@@ -80,6 +89,7 @@ def is_hex_nrf52(file: Path) -> bool:
|
|
|
80
89
|
|
|
81
90
|
@validate_call
|
|
82
91
|
def is_elf(file: Path) -> bool:
|
|
92
|
+
"""Check if file is an ELF file."""
|
|
83
93
|
if ELF is None:
|
|
84
94
|
raise RuntimeError(elf_error_text)
|
|
85
95
|
if not file.is_file():
|
|
@@ -92,7 +102,11 @@ def is_elf(file: Path) -> bool:
|
|
|
92
102
|
return True
|
|
93
103
|
|
|
94
104
|
|
|
95
|
-
def is_elf_msp430(file: Path) -> bool:
|
|
105
|
+
def is_elf_msp430(file: Path) -> bool:
|
|
106
|
+
"""Check if file is an ELF for that MCU.
|
|
107
|
+
|
|
108
|
+
TODO: allow detection without conversion
|
|
109
|
+
"""
|
|
96
110
|
if is_elf(file):
|
|
97
111
|
with tempfile.TemporaryDirectory() as path:
|
|
98
112
|
file_hex = Path(path) / "file.hex"
|
|
@@ -103,7 +117,11 @@ def is_elf_msp430(file: Path) -> bool: # TODO: allow detection without conversi
|
|
|
103
117
|
return False
|
|
104
118
|
|
|
105
119
|
|
|
106
|
-
def is_elf_nrf52(file: Path) -> bool:
|
|
120
|
+
def is_elf_nrf52(file: Path) -> bool:
|
|
121
|
+
"""Check if file is an ELF for that MCU.
|
|
122
|
+
|
|
123
|
+
TODO: allow detection without conversion
|
|
124
|
+
"""
|
|
107
125
|
if is_elf(file):
|
|
108
126
|
with tempfile.TemporaryDirectory() as path:
|
|
109
127
|
file_hex = Path(path) / "file.hex"
|
|
@@ -115,6 +133,7 @@ def is_elf_nrf52(file: Path) -> bool: # TODO: allow detection without conversio
|
|
|
115
133
|
|
|
116
134
|
|
|
117
135
|
def determine_type(file: Path) -> FirmwareDType:
|
|
136
|
+
"""Figure out file-type (hex or elf)."""
|
|
118
137
|
if not file.is_file():
|
|
119
138
|
raise ValueError("Fn needs an existing file as input")
|
|
120
139
|
if is_hex(file):
|
|
@@ -125,6 +144,7 @@ def determine_type(file: Path) -> FirmwareDType:
|
|
|
125
144
|
|
|
126
145
|
|
|
127
146
|
def determine_arch(file: Path) -> str:
|
|
147
|
+
"""Figure out arch (msp430 or nrf52)."""
|
|
128
148
|
file_t = determine_type(file)
|
|
129
149
|
if file_t == FirmwareDType.path_elf:
|
|
130
150
|
if is_elf_nrf52(file):
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
"""Creates an overview for shepherd-host-machines
|
|
1
|
+
"""Creates an overview for shepherd-host-machines.
|
|
2
|
+
|
|
3
|
+
This will collect:
|
|
2
4
|
- relevant software-versions
|
|
3
5
|
- system-parameters
|
|
4
|
-
- hardware-config
|
|
6
|
+
- hardware-config.
|
|
5
7
|
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from datetime import timedelta
|
|
6
11
|
from pathlib import Path
|
|
7
12
|
from typing import List
|
|
8
13
|
|
|
@@ -25,25 +30,39 @@ __all__ = [
|
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
28
|
-
|
|
33
|
+
"""Complete inventory for one device.
|
|
34
|
+
|
|
35
|
+
Has all child-parameters.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
hostname: str
|
|
39
|
+
created: datetime
|
|
29
40
|
|
|
30
41
|
@classmethod
|
|
31
42
|
def collect(cls) -> Self:
|
|
32
43
|
# one by one for more precise error messages
|
|
33
|
-
|
|
44
|
+
# NOTE: system is first, as it must take a precise timestamp
|
|
34
45
|
sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
46
|
+
pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
35
47
|
tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
36
48
|
model = {**pid, **sid, **tid}
|
|
49
|
+
# make important metadata available at root level
|
|
50
|
+
model["created"] = sid["timestamp"]
|
|
51
|
+
model["hostname"] = sid["hostname"]
|
|
37
52
|
return cls(**model)
|
|
38
53
|
|
|
39
54
|
|
|
40
55
|
class InventoryList(ShpModel):
|
|
56
|
+
"""Collection of inventories for several devices."""
|
|
57
|
+
|
|
41
58
|
elements: Annotated[List[Inventory], Field(min_length=1)]
|
|
42
59
|
|
|
43
60
|
def to_csv(self, path: Path) -> None:
|
|
44
|
-
"""
|
|
61
|
+
"""Generate a CSV.
|
|
62
|
+
|
|
63
|
+
TODO: pretty messed up (raw lists and dicts for sub-elements)
|
|
45
64
|
numpy.savetxt -> too basic
|
|
46
|
-
np.concatenate(content).reshape((len(content), len(content[0])))
|
|
65
|
+
np.concatenate(content).reshape((len(content), len(content[0]))).
|
|
47
66
|
"""
|
|
48
67
|
if path.is_dir():
|
|
49
68
|
path = path / "inventory.yaml"
|
|
@@ -53,3 +72,44 @@ class InventoryList(ShpModel):
|
|
|
53
72
|
content = list(item.model_dump().values())
|
|
54
73
|
content = ["" if value is None else str(value) for value in content]
|
|
55
74
|
fd.write(", ".join(content) + "\r\n")
|
|
75
|
+
|
|
76
|
+
def warn(self) -> dict:
|
|
77
|
+
warnings = {}
|
|
78
|
+
ts_earl = min([_e.created.timestamp() for _e in self.elements])
|
|
79
|
+
for _e in self.elements:
|
|
80
|
+
if _e.uptime > timedelta(hours=30).total_seconds():
|
|
81
|
+
warnings["uptime"] = f"[{self.hostname}] restart is recommended"
|
|
82
|
+
if (_e.created.timestamp() - ts_earl) > 10:
|
|
83
|
+
warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
|
|
84
|
+
|
|
85
|
+
# turn dict[hostname][type] = val
|
|
86
|
+
# to dict[type][val] = list[hostnames]
|
|
87
|
+
_inp = {
|
|
88
|
+
_e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
|
|
89
|
+
for _e in self.elements
|
|
90
|
+
}
|
|
91
|
+
result = {}
|
|
92
|
+
for _host, _types in _inp.items():
|
|
93
|
+
for _type, _val in _types.items():
|
|
94
|
+
if _type not in result:
|
|
95
|
+
result[_type] = {}
|
|
96
|
+
if _val not in result[_type]:
|
|
97
|
+
result[_type][_val] = []
|
|
98
|
+
result[_type][_val].append(_host)
|
|
99
|
+
rescnt = {_key: len(_val) for _key, _val in result.items()}
|
|
100
|
+
t_unique = [
|
|
101
|
+
"h5py",
|
|
102
|
+
"numpy",
|
|
103
|
+
"pydantic",
|
|
104
|
+
"python",
|
|
105
|
+
"shepherd_core",
|
|
106
|
+
"shepherd_sheep",
|
|
107
|
+
"yaml",
|
|
108
|
+
"zstandard",
|
|
109
|
+
]
|
|
110
|
+
for _key in t_unique:
|
|
111
|
+
if rescnt[_key] > 1:
|
|
112
|
+
warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
|
|
113
|
+
|
|
114
|
+
# TODO: finish with more potential warnings
|
|
115
|
+
return warnings
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Python related inventory model."""
|
|
2
|
+
|
|
1
3
|
import platform
|
|
2
4
|
from contextlib import suppress
|
|
3
5
|
from importlib import import_module
|
|
@@ -10,6 +12,8 @@ from ..data_models import ShpModel
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class PythonInventory(ShpModel):
|
|
15
|
+
"""Python related inventory model."""
|
|
16
|
+
|
|
13
17
|
# program versions
|
|
14
18
|
h5py: Optional[str] = None
|
|
15
19
|
numpy: Optional[str] = None
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
"""System / OS related inventory model."""
|
|
2
|
+
|
|
1
3
|
import platform
|
|
2
4
|
import subprocess
|
|
3
5
|
import time
|
|
4
6
|
from contextlib import suppress
|
|
7
|
+
from datetime import datetime
|
|
5
8
|
from typing import Optional
|
|
6
9
|
|
|
7
10
|
from typing_extensions import Self
|
|
8
11
|
|
|
9
|
-
from .. import
|
|
12
|
+
from ..data_models.base.timezone import local_now
|
|
13
|
+
from ..logger import logger
|
|
10
14
|
|
|
11
15
|
try:
|
|
12
16
|
import psutil
|
|
@@ -20,8 +24,13 @@ from ..data_models import ShpModel
|
|
|
20
24
|
|
|
21
25
|
|
|
22
26
|
class SystemInventory(ShpModel):
|
|
27
|
+
"""System / OS related inventory model."""
|
|
28
|
+
|
|
23
29
|
uptime: PositiveInt
|
|
24
30
|
# ⤷ seconds
|
|
31
|
+
timestamp: datetime
|
|
32
|
+
# time_delta: timedelta = timedelta(0) # noqa: ERA001
|
|
33
|
+
# ⤷ lag behind earliest observer, TODO: wrong place
|
|
25
34
|
|
|
26
35
|
system: str
|
|
27
36
|
release: str
|
|
@@ -43,6 +52,8 @@ class SystemInventory(ShpModel):
|
|
|
43
52
|
|
|
44
53
|
@classmethod
|
|
45
54
|
def collect(cls) -> Self:
|
|
55
|
+
ts = local_now()
|
|
56
|
+
|
|
46
57
|
if psutil is None:
|
|
47
58
|
ifs2 = {}
|
|
48
59
|
uptime = 0
|
|
@@ -58,6 +69,7 @@ class SystemInventory(ShpModel):
|
|
|
58
69
|
|
|
59
70
|
model_dict = {
|
|
60
71
|
"uptime": round(uptime),
|
|
72
|
+
"timestamp": ts,
|
|
61
73
|
"system": platform.system(),
|
|
62
74
|
"release": platform.release(),
|
|
63
75
|
"version": platform.version(),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Hardware related inventory model."""
|
|
2
|
+
|
|
1
3
|
from typing import List
|
|
2
4
|
from typing import Optional
|
|
3
5
|
|
|
@@ -8,6 +10,8 @@ from ..data_models import ShpModel
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class TargetInventory(ShpModel):
|
|
13
|
+
"""Hardware related inventory model."""
|
|
14
|
+
|
|
11
15
|
cape: Optional[str] = None
|
|
12
16
|
targets: List[str] = [] # noqa: RUF012
|
|
13
17
|
|
shepherd_core/logger.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Log handler of shepherd."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import logging.handlers
|
|
3
5
|
from typing import Union
|
|
@@ -12,10 +14,12 @@ verbose_level: int = 2
|
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
def get_verbose_level() -> int:
|
|
17
|
+
"""Get log level of shepherd."""
|
|
15
18
|
return verbose_level
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose: int) -> None:
|
|
22
|
+
"""Set log level of shepherd."""
|
|
19
23
|
if verbose == 0:
|
|
20
24
|
log_.setLevel(logging.ERROR)
|
|
21
25
|
logging.basicConfig(level=logging.ERROR)
|
|
@@ -39,6 +43,7 @@ def set_log_verbose_level(log_: Union[logging.Logger, logging.Handler], verbose:
|
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
def increase_verbose_level(verbose: int) -> None:
|
|
46
|
+
"""Increase log level of shepherd."""
|
|
42
47
|
global verbose_level # noqa: PLW0603
|
|
43
48
|
if verbose >= verbose_level:
|
|
44
49
|
verbose_level = min(max(verbose, 0), 3)
|
shepherd_core/reader.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""Reader-Baseclass."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
4
5
|
import contextlib
|
|
5
6
|
import errno
|
|
6
7
|
import logging
|
|
@@ -8,7 +9,7 @@ import math
|
|
|
8
9
|
import os
|
|
9
10
|
from itertools import product
|
|
10
11
|
from pathlib import Path
|
|
11
|
-
from
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
12
13
|
from typing import Any
|
|
13
14
|
from typing import ClassVar
|
|
14
15
|
from typing import Dict
|
|
@@ -31,13 +32,18 @@ from .data_models.base.calibration import CalibrationSeries
|
|
|
31
32
|
from .data_models.content.energy_environment import EnergyDType
|
|
32
33
|
from .decoder_waveform import Uart
|
|
33
34
|
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from types import TracebackType
|
|
37
|
+
|
|
34
38
|
|
|
35
39
|
class Reader:
|
|
36
40
|
"""Sequentially Reads shepherd-data from HDF5 file.
|
|
37
41
|
|
|
38
42
|
Args:
|
|
43
|
+
----
|
|
39
44
|
file_path: Path of hdf5 file containing shepherd data with iv-samples, iv-curves or isc&voc
|
|
40
45
|
verbose: more debug-info during usage, 'None' skips the setter
|
|
46
|
+
|
|
41
47
|
"""
|
|
42
48
|
|
|
43
49
|
samples_per_buffer: int = 10_000
|
|
@@ -95,7 +101,8 @@ class Reader:
|
|
|
95
101
|
self.h5file = h5py.File(self.file_path, "r") # = readonly
|
|
96
102
|
self._reader_opened = True
|
|
97
103
|
except OSError as _xcp:
|
|
98
|
-
|
|
104
|
+
msg = f"Unable to open HDF5-File '{self.file_path.name}'"
|
|
105
|
+
raise TypeError(msg) from _xcp
|
|
99
106
|
|
|
100
107
|
if self.is_valid():
|
|
101
108
|
self._logger.debug("File is available now")
|
|
@@ -158,7 +165,7 @@ class Reader:
|
|
|
158
165
|
)
|
|
159
166
|
|
|
160
167
|
def _refresh_file_stats(self) -> None:
|
|
161
|
-
"""
|
|
168
|
+
"""Update internal states, helpful after resampling or other changes in data-group."""
|
|
162
169
|
self.h5file.flush()
|
|
163
170
|
if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
|
|
164
171
|
# this assumes isochronal sampling
|
|
@@ -180,17 +187,20 @@ class Reader:
|
|
|
180
187
|
is_raw: bool = False,
|
|
181
188
|
omit_ts: bool = False,
|
|
182
189
|
) -> Generator[tuple, None, None]:
|
|
183
|
-
"""
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
"""Read the specified range of buffers from the hdf5 file.
|
|
191
|
+
|
|
192
|
+
Generator - can be configured on first call
|
|
193
|
+
TODO: reconstruct - start/end mark samples &
|
|
194
|
+
each call can request a certain number of samples.
|
|
187
195
|
|
|
188
196
|
Args:
|
|
197
|
+
----
|
|
189
198
|
:param start_n: (int) Index of first buffer to be read
|
|
190
199
|
:param end_n: (int) Index of last buffer to be read
|
|
191
200
|
:param is_raw: (bool) output original data, not transformed to SI-Units
|
|
192
201
|
:param omit_ts: (bool) optimize reading if timestamp is never used
|
|
193
202
|
Yields: Buffers between start and end (tuple with time, voltage, current)
|
|
203
|
+
|
|
194
204
|
"""
|
|
195
205
|
if end_n is None:
|
|
196
206
|
end_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
|
|
@@ -215,7 +225,7 @@ class Reader:
|
|
|
215
225
|
)
|
|
216
226
|
|
|
217
227
|
def get_calibration_data(self) -> CalibrationSeries:
|
|
218
|
-
"""
|
|
228
|
+
"""Read calibration-data from hdf5 file.
|
|
219
229
|
|
|
220
230
|
:return: Calibration data as CalibrationSeries object
|
|
221
231
|
"""
|
|
@@ -245,12 +255,14 @@ class Reader:
|
|
|
245
255
|
try:
|
|
246
256
|
if "datatype" in self.h5file["data"].attrs:
|
|
247
257
|
return EnergyDType[self.h5file["data"].attrs["datatype"]]
|
|
248
|
-
return None
|
|
249
258
|
except KeyError:
|
|
250
259
|
return None
|
|
260
|
+
else:
|
|
261
|
+
return None
|
|
251
262
|
|
|
252
263
|
def get_hrv_config(self) -> dict:
|
|
253
|
-
"""
|
|
264
|
+
"""Essential info for harvester.
|
|
265
|
+
|
|
254
266
|
:return: config-dict directly for vHarvester to be used during emulation
|
|
255
267
|
"""
|
|
256
268
|
return {
|
|
@@ -259,7 +271,7 @@ class Reader:
|
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
def is_valid(self) -> bool:
|
|
262
|
-
"""
|
|
274
|
+
"""Check file for plausibility, validity / correctness.
|
|
263
275
|
|
|
264
276
|
:return: state of validity
|
|
265
277
|
"""
|
|
@@ -387,7 +399,7 @@ class Reader:
|
|
|
387
399
|
return True
|
|
388
400
|
|
|
389
401
|
def __getitem__(self, key: str) -> Any:
|
|
390
|
-
"""
|
|
402
|
+
"""Query attribute or (if none found) a handle for a group or dataset (if found).
|
|
391
403
|
|
|
392
404
|
:param key: attribute, group, dataset
|
|
393
405
|
:return: value of that key, or handle of object
|
|
@@ -399,9 +411,10 @@ class Reader:
|
|
|
399
411
|
raise KeyError
|
|
400
412
|
|
|
401
413
|
def energy(self) -> float:
|
|
402
|
-
"""
|
|
414
|
+
"""Determine the recorded energy of the trace.
|
|
415
|
+
|
|
403
416
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
404
|
-
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
|
|
417
|
+
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
|
|
405
418
|
|
|
406
419
|
:return: sampled energy in Ws (watt-seconds)
|
|
407
420
|
"""
|
|
@@ -427,7 +440,8 @@ class Reader:
|
|
|
427
440
|
def _dset_statistics(
|
|
428
441
|
self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
|
|
429
442
|
) -> Dict[str, float]:
|
|
430
|
-
"""
|
|
443
|
+
"""Create basic stats for a provided dataset.
|
|
444
|
+
|
|
431
445
|
:param dset: dataset to evaluate
|
|
432
446
|
:param cal: calibration (if wanted)
|
|
433
447
|
:return: dict with entries for mean, min, max, std
|
|
@@ -469,8 +483,9 @@ class Reader:
|
|
|
469
483
|
return stats
|
|
470
484
|
|
|
471
485
|
def _data_timediffs(self) -> List[float]:
|
|
472
|
-
"""
|
|
473
|
-
|
|
486
|
+
"""Calculate list of unique time-deltas [s] between buffers.
|
|
487
|
+
|
|
488
|
+
Optimized version that only looks at the start of each buffer.
|
|
474
489
|
|
|
475
490
|
:return: list of (unique) time-deltas between buffers [s]
|
|
476
491
|
"""
|
|
@@ -500,8 +515,9 @@ class Reader:
|
|
|
500
515
|
return list(diffs)
|
|
501
516
|
|
|
502
517
|
def check_timediffs(self) -> bool:
|
|
503
|
-
"""
|
|
504
|
-
|
|
518
|
+
"""Validate equal time-deltas.
|
|
519
|
+
|
|
520
|
+
Unexpected time-jumps hint at a corrupted file or faulty measurement.
|
|
505
521
|
|
|
506
522
|
:return: True if OK
|
|
507
523
|
"""
|
|
@@ -529,7 +545,8 @@ class Reader:
|
|
|
529
545
|
*,
|
|
530
546
|
minimal: bool = False,
|
|
531
547
|
) -> Dict[str, dict]:
|
|
532
|
-
"""
|
|
548
|
+
"""Recursive FN to capture the structure of the file.
|
|
549
|
+
|
|
533
550
|
:param node: starting node, leave free to go through whole file
|
|
534
551
|
:param minimal: just provide a bare tree (much faster)
|
|
535
552
|
:return: structure of that node with everything inside it
|
|
@@ -580,7 +597,7 @@ class Reader:
|
|
|
580
597
|
return metadata
|
|
581
598
|
|
|
582
599
|
def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
|
|
583
|
-
"""
|
|
600
|
+
"""Get structure of file and dump content to yaml-file with same name as original.
|
|
584
601
|
|
|
585
602
|
:param node: starting node, leave free to go through whole file
|
|
586
603
|
:return: structure of that node with everything inside it
|
|
@@ -609,8 +626,9 @@ class Reader:
|
|
|
609
626
|
|
|
610
627
|
@staticmethod
|
|
611
628
|
def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
|
|
612
|
-
"""
|
|
613
|
-
|
|
629
|
+
"""Input is 1D state-vector, kep only first from identical & sequential states.
|
|
630
|
+
|
|
631
|
+
Algo: create an offset-by-one vector and compare against original.
|
|
614
632
|
"""
|
|
615
633
|
if len(data.shape) > 1:
|
|
616
634
|
ValueError("Array must be 1D")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Generalized path for the file-cache."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
|
|
8
|
+
_value = os.environ.get(variable_name)
|
|
9
|
+
if _value is None or _value == "":
|
|
10
|
+
return default_path
|
|
11
|
+
return Path(_value)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
user_path = Path("~").expanduser()
|
|
15
|
+
|
|
16
|
+
cache_xdg_path = _get_xdg_path("XDG_CACHE_HOME", user_path / ".cache")
|
|
17
|
+
cache_user_path = cache_xdg_path / "shepherd_datalib"
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Client-Class to access a testbed instance."""
|
|
2
|
+
|
|
1
3
|
from importlib import import_module
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
from typing import Optional
|
|
@@ -16,6 +18,8 @@ from .user_model import User
|
|
|
16
18
|
|
|
17
19
|
|
|
18
20
|
class TestbedClient:
|
|
21
|
+
"""Client-Class to access a testbed instance."""
|
|
22
|
+
|
|
19
23
|
_instance: Optional[Self] = None
|
|
20
24
|
|
|
21
25
|
def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
|
|
@@ -41,9 +45,10 @@ class TestbedClient:
|
|
|
41
45
|
|
|
42
46
|
@validate_call
|
|
43
47
|
def connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
|
|
44
|
-
"""
|
|
48
|
+
"""Establish connection to testbed-server.
|
|
49
|
+
|
|
45
50
|
server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
|
|
46
|
-
token: your account validation
|
|
51
|
+
token: your account validation.
|
|
47
52
|
"""
|
|
48
53
|
if isinstance(token, Path):
|
|
49
54
|
with token.resolve().open() as file:
|
|
@@ -78,19 +83,19 @@ class TestbedClient:
|
|
|
78
83
|
|
|
79
84
|
def query_ids(self, model_type: str) -> list:
|
|
80
85
|
if self._connected:
|
|
81
|
-
raise
|
|
86
|
+
raise NotImplementedError("TODO")
|
|
82
87
|
return list(self._fixtures[model_type].elements_by_id.keys())
|
|
83
88
|
|
|
84
89
|
def query_names(self, model_type: str) -> list:
|
|
85
90
|
if self._connected:
|
|
86
|
-
raise
|
|
91
|
+
raise NotImplementedError("TODO")
|
|
87
92
|
return list(self._fixtures[model_type].elements_by_name.keys())
|
|
88
93
|
|
|
89
94
|
def query_item(
|
|
90
95
|
self, model_type: str, uid: Optional[int] = None, name: Optional[str] = None
|
|
91
96
|
) -> dict:
|
|
92
97
|
if self._connected:
|
|
93
|
-
raise
|
|
98
|
+
raise NotImplementedError("TODO")
|
|
94
99
|
if uid is not None:
|
|
95
100
|
return self._fixtures[model_type].query_id(uid)
|
|
96
101
|
if name is not None:
|
|
@@ -116,11 +121,11 @@ class TestbedClient:
|
|
|
116
121
|
|
|
117
122
|
def try_inheritance(self, model_type: str, values: dict) -> (dict, list):
|
|
118
123
|
if self._connected:
|
|
119
|
-
raise
|
|
124
|
+
raise NotImplementedError("TODO")
|
|
120
125
|
return self._fixtures[model_type].inheritance(values)
|
|
121
126
|
|
|
122
127
|
def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
|
|
123
|
-
"""
|
|
128
|
+
"""Init by name/id, for none existing instances raise Exception."""
|
|
124
129
|
if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
|
|
125
130
|
value = next(iter(values.values()))
|
|
126
131
|
if (
|
|
@@ -132,7 +137,8 @@ class TestbedClient:
|
|
|
132
137
|
# TODO: still depending on _fixture
|
|
133
138
|
values = self.query_item(model_type, uid=value)
|
|
134
139
|
else:
|
|
135
|
-
|
|
140
|
+
msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
|
|
141
|
+
raise ValueError(msg)
|
|
136
142
|
return self.try_inheritance(model_type, values)
|
|
137
143
|
|
|
138
144
|
def fill_in_user_data(self, values: dict) -> dict:
|