shepherd-core 2025.10.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 +4 -2
- 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 +10 -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 +4 -2
- 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_config.py +10 -93
- shepherd_core/data_models/content/virtual_source_config.py +21 -2
- shepherd_core/data_models/content/virtual_storage_config.py +7 -4
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +1 -1
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +4 -4
- 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 +55 -7
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +9 -5
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +2 -0
- 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/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 +2 -2
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +29 -25
- 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/virtual_converter_model.py +2 -2
- shepherd_core/vsource/virtual_harvester_model.py +2 -2
- shepherd_core/vsource/virtual_harvester_simulation.py +5 -5
- shepherd_core/vsource/virtual_source_model.py +1 -1
- shepherd_core/vsource/virtual_source_simulation.py +9 -9
- shepherd_core/vsource/virtual_storage_models_kibam.py +3 -3
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +5 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.10.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-2025.10.1.dist-info/RECORD +0 -95
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
|
@@ -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
|
|
@@ -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
|
)
|
|
@@ -315,11 +317,11 @@ class Reader:
|
|
|
315
317
|
self.get_config().get("virtual_harvester", {}).get("voltage_step_mV", None)
|
|
316
318
|
)
|
|
317
319
|
if voltage_step is not None: # convert mV to V
|
|
318
|
-
voltage_step
|
|
320
|
+
voltage_step *= 1e-3
|
|
319
321
|
if voltage_step is None:
|
|
320
322
|
dsv = self._cal.voltage.raw_to_si(self.ds_voltage[0:2000])
|
|
321
323
|
diffs_np = np.unique(dsv[1:] - dsv[0:-1], return_counts=False)
|
|
322
|
-
diffs_ls = [
|
|
324
|
+
diffs_ls = [e_ for e_ in list(np.array(diffs_np)) if e_ > 0]
|
|
323
325
|
# static voltages have 0 steps, so
|
|
324
326
|
if len(diffs_ls) == 0:
|
|
325
327
|
self._logger.warning("Voltage-Step could not be determined from source-material")
|
|
@@ -457,11 +459,11 @@ class Reader:
|
|
|
457
459
|
"[FileValidation] Hostname was not set in '%s'", self.file_path.name
|
|
458
460
|
)
|
|
459
461
|
# errors during execution
|
|
460
|
-
|
|
461
|
-
if
|
|
462
|
+
err_ = self.count_errors_in_log()
|
|
463
|
+
if err_ > 0:
|
|
462
464
|
self._logger.warning(
|
|
463
465
|
"[FileValidation] Sheep reported %d errors during execution -> check logs in '%s'",
|
|
464
|
-
|
|
466
|
+
err_,
|
|
465
467
|
self.file_path.name,
|
|
466
468
|
)
|
|
467
469
|
return True
|
|
@@ -484,6 +486,8 @@ class Reader:
|
|
|
484
486
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
485
487
|
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
|
|
486
488
|
|
|
489
|
+
TODO: add optional duration argument to allow calculating mean energy of a spatial EEnv
|
|
490
|
+
|
|
487
491
|
:return: sampled energy in Ws (watt-seconds)
|
|
488
492
|
"""
|
|
489
493
|
iterations = math.ceil(self.samples_n / self.max_elements)
|
|
@@ -603,11 +607,11 @@ class Reader:
|
|
|
603
607
|
return 0
|
|
604
608
|
if "level" not in self.h5file[group_name]:
|
|
605
609
|
return 0
|
|
606
|
-
|
|
607
|
-
if
|
|
610
|
+
lvl_ = self.h5file[group_name]["level"]
|
|
611
|
+
if lvl_.shape[0] < 1:
|
|
608
612
|
return 0
|
|
609
|
-
|
|
610
|
-
return len(
|
|
613
|
+
items_ = [1 for x_ in lvl_[:] if x_ >= min_level]
|
|
614
|
+
return len(items_)
|
|
611
615
|
|
|
612
616
|
def get_metadata(
|
|
613
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
|
@@ -35,7 +35,7 @@ class PruCalibration:
|
|
|
35
35
|
negative_residue_nA = 0
|
|
36
36
|
|
|
37
37
|
def __init__(self, cal_emu: CalibrationEmulator | None = None) -> None:
|
|
38
|
-
self.cal = cal_emu
|
|
38
|
+
self.cal = cal_emu or CalibrationEmulator()
|
|
39
39
|
|
|
40
40
|
def conv_adc_raw_to_nA(self, current_raw: int) -> float:
|
|
41
41
|
I_nA = self.cal.adc_C_A.raw_to_si(current_raw) * (10**9)
|
|
@@ -44,7 +44,7 @@ class PruCalibration:
|
|
|
44
44
|
I_nA -= self.negative_residue_nA
|
|
45
45
|
self.negative_residue_nA = 0
|
|
46
46
|
else:
|
|
47
|
-
self.negative_residue_nA
|
|
47
|
+
self.negative_residue_nA -= I_nA
|
|
48
48
|
self.negative_residue_nA = min(self.negative_residue_nA, self.RESIDUE_MAX_nA)
|
|
49
49
|
I_nA = 0
|
|
50
50
|
return I_nA
|
|
@@ -152,7 +152,7 @@ class VirtualHarvesterModel:
|
|
|
152
152
|
return self.voltage_hold, self.current_hold
|
|
153
153
|
|
|
154
154
|
def ivcurve_2_mppt_voc(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
|
|
155
|
-
self.interval_step
|
|
155
|
+
self.interval_step += 1
|
|
156
156
|
if self.interval_step >= self._cfg.interval_n:
|
|
157
157
|
self.interval_step = 0
|
|
158
158
|
self.age_nxt += 1
|
|
@@ -183,7 +183,7 @@ class VirtualHarvesterModel:
|
|
|
183
183
|
return _voltage_uV, _current_nA
|
|
184
184
|
|
|
185
185
|
def ivcurve_2_mppt_po(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
|
|
186
|
-
self.interval_step
|
|
186
|
+
self.interval_step += 1
|
|
187
187
|
if self.interval_step >= self._cfg.interval_n:
|
|
188
188
|
self.interval_step = 0
|
|
189
189
|
|
|
@@ -52,20 +52,20 @@ def simulate_harvester(
|
|
|
52
52
|
hrv = VirtualHarvesterModel(hrv_pru)
|
|
53
53
|
e_out_Ws = 0.0
|
|
54
54
|
|
|
55
|
-
for
|
|
55
|
+
for t_, v_inp, i_inp in tqdm(
|
|
56
56
|
file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
|
|
57
57
|
):
|
|
58
58
|
v_uV = cal_inp.voltage.raw_to_si(v_inp) * 1e6
|
|
59
59
|
i_nA = cal_inp.current.raw_to_si(i_inp) * 1e9
|
|
60
60
|
length = min(v_uV.size, i_nA.size)
|
|
61
|
-
for
|
|
62
|
-
v_uV[
|
|
63
|
-
_voltage_uV=int(v_uV[
|
|
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
64
|
)
|
|
65
65
|
e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
|
|
66
66
|
if path_output:
|
|
67
67
|
v_out = cal_out.voltage.si_to_raw(v_uV / 1e6)
|
|
68
68
|
i_out = cal_out.current.si_to_raw(i_nA / 1e9)
|
|
69
|
-
file_out.append_iv_data_raw(
|
|
69
|
+
file_out.append_iv_data_raw(t_, v_out, i_out)
|
|
70
70
|
|
|
71
71
|
return e_out_Ws
|
|
@@ -11,7 +11,7 @@ NOTE: DO NOT OPTIMIZE -> stay close to original code-base
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from shepherd_core.data_models.base.calibration import CalibrationEmulator
|
|
14
|
-
from shepherd_core.data_models.content.
|
|
14
|
+
from shepherd_core.data_models.content.enum_datatypes import EnergyDType
|
|
15
15
|
from shepherd_core.data_models.content.virtual_harvester_config import HarvesterPRUConfig
|
|
16
16
|
from shepherd_core.data_models.content.virtual_source_config import ConverterPRUConfig
|
|
17
17
|
from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
|
|
@@ -72,24 +72,24 @@ def simulate_source(
|
|
|
72
72
|
else:
|
|
73
73
|
stats_internal = None
|
|
74
74
|
|
|
75
|
-
for
|
|
75
|
+
for t_, v_inp, i_inp in tqdm(
|
|
76
76
|
file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
|
|
77
77
|
):
|
|
78
78
|
v_uV = 1e6 * cal_inp.voltage.raw_to_si(v_inp)
|
|
79
79
|
i_nA = 1e9 * cal_inp.current.raw_to_si(i_inp)
|
|
80
80
|
|
|
81
|
-
for
|
|
82
|
-
v_uV[
|
|
83
|
-
V_inp_uV=int(v_uV[
|
|
84
|
-
I_inp_nA=int(i_nA[
|
|
81
|
+
for n_ in range(len(t_)):
|
|
82
|
+
v_uV[n_] = src.iterate_sampling(
|
|
83
|
+
V_inp_uV=int(v_uV[n_]),
|
|
84
|
+
I_inp_nA=int(i_nA[n_]),
|
|
85
85
|
I_out_nA=i_out_nA,
|
|
86
86
|
)
|
|
87
|
-
i_out_nA = target.step(int(v_uV[
|
|
88
|
-
i_nA[
|
|
87
|
+
i_out_nA = target.step(int(v_uV[n_]), pwr_good=src.cnv.get_power_good())
|
|
88
|
+
i_nA[n_] = i_out_nA
|
|
89
89
|
|
|
90
90
|
if stats_internal is not None:
|
|
91
91
|
stats_internal[stats_sample] = [
|
|
92
|
-
|
|
92
|
+
t_[n_] * 1e-9, # s
|
|
93
93
|
src.hrv.voltage_hold * 1e-6,
|
|
94
94
|
src.cnv.V_input_request_uV * 1e-6, # V
|
|
95
95
|
src.hrv.voltage_set_uV * 1e-6,
|
|
@@ -107,7 +107,7 @@ def simulate_source(
|
|
|
107
107
|
if path_output:
|
|
108
108
|
v_out = cal_out.voltage.si_to_raw(1e-6 * v_uV)
|
|
109
109
|
i_out = cal_out.current.si_to_raw(1e-9 * i_nA)
|
|
110
|
-
file_out.append_iv_data_raw(
|
|
110
|
+
file_out.append_iv_data_raw(t_, v_out, i_out)
|
|
111
111
|
|
|
112
112
|
if stats_internal is not None:
|
|
113
113
|
stats_internal = stats_internal[:stats_sample, :]
|
|
@@ -161,7 +161,7 @@ class ModelKiBaM(ModelStorage):
|
|
|
161
161
|
|
|
162
162
|
# Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
|
|
163
163
|
# ⤷ MODIFIED: clamp both SoC to 0..1
|
|
164
|
-
self.SoC
|
|
164
|
+
self.SoC -= 1 / self.cfg.q_As * (I_cell * self.dt_s)
|
|
165
165
|
self.SoC = min(max(self.SoC, 0.0), 1.0)
|
|
166
166
|
SoC_eff = self.SoC - 1 / self.cfg.q_As * self.C_unavailable
|
|
167
167
|
SoC_eff = max(SoC_eff, 0.0)
|
|
@@ -278,7 +278,7 @@ class ModelKiBaMPlus(ModelStorage):
|
|
|
278
278
|
|
|
279
279
|
# Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
|
|
280
280
|
# ⤷ MODIFIED: clamp both SoC to 0..1
|
|
281
|
-
self.SoC
|
|
281
|
+
self.SoC -= (I_cell + I_leak) * self.dt_s / self.cfg.q_As
|
|
282
282
|
self.SoC = min(max(self.SoC, 0.0), 1.0)
|
|
283
283
|
SoC_eff = self.SoC - 1 / self.cfg.q_As * self.C_unavailable
|
|
284
284
|
SoC_eff = min(max(SoC_eff, 0.0), 1.0)
|
|
@@ -380,7 +380,7 @@ class ModelKiBaMSimple(ModelStorage):
|
|
|
380
380
|
|
|
381
381
|
# Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
|
|
382
382
|
# = SoC - 1 / C * (i_cell * dt)
|
|
383
|
-
self.SoC
|
|
383
|
+
self.SoC -= (I_cell + I_leak) * self.Constant_s_per_As
|
|
384
384
|
SoC_eff = self.SoC = min(max(self.SoC, 0.0), 1.0)
|
|
385
385
|
# ⤷ MODIFIED: removed term due to omission of rate capacity effect
|
|
386
386
|
# ⤷ MODIFIED: clamp SoC to 0..1
|
shepherd_core/writer.py
CHANGED
|
@@ -22,7 +22,7 @@ from .config import config
|
|
|
22
22
|
from .data_models.base.calibration import CalibrationEmulator as CalEmu
|
|
23
23
|
from .data_models.base.calibration import CalibrationHarvester as CalHrv
|
|
24
24
|
from .data_models.base.calibration import CalibrationSeries as CalSeries
|
|
25
|
-
from .data_models.content.
|
|
25
|
+
from .data_models.content.enum_datatypes import EnergyDType
|
|
26
26
|
from .data_models.task import Compression
|
|
27
27
|
from .data_models.task.emulation import c_translate
|
|
28
28
|
from .reader import Reader
|
|
@@ -116,11 +116,12 @@ class Writer(Reader):
|
|
|
116
116
|
|
|
117
117
|
if not hasattr(self, "_logger"):
|
|
118
118
|
self._logger: logging.Logger = logging.getLogger("SHPCore.Writer")
|
|
119
|
+
self._logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
119
120
|
# -> logger gets configured in reader()
|
|
120
121
|
|
|
121
122
|
if self._modify or force_overwrite or not file_path.exists():
|
|
122
123
|
file_path = file_path.resolve()
|
|
123
|
-
self._logger.
|
|
124
|
+
self._logger.debug("Storing data to '%s'", file_path)
|
|
124
125
|
elif file_path.exists() and not file_path.is_file():
|
|
125
126
|
msg = f"Path is not a file ({file_path})"
|
|
126
127
|
raise TypeError(msg)
|
|
@@ -154,22 +155,22 @@ class Writer(Reader):
|
|
|
154
155
|
if "mode" not in self.h5file.attrs:
|
|
155
156
|
self.h5file.attrs["mode"] = self.MODE_DEFAULT
|
|
156
157
|
|
|
157
|
-
|
|
158
|
+
dtypes_ = self.MODE_TO_DTYPE[self.get_mode()]
|
|
158
159
|
|
|
159
160
|
# Handle Datatype
|
|
160
161
|
if isinstance(datatype, str):
|
|
161
162
|
datatype = EnergyDType[datatype]
|
|
162
|
-
if isinstance(datatype, EnergyDType) and datatype not in
|
|
163
|
-
msg = f"Can't handle value '{datatype}' of datatype (choose one of {
|
|
163
|
+
if isinstance(datatype, EnergyDType) and datatype not in dtypes_:
|
|
164
|
+
msg = f"Can't handle value '{datatype}' of datatype (choose one of {dtypes_})"
|
|
164
165
|
raise ValueError(msg)
|
|
165
166
|
|
|
166
167
|
if isinstance(datatype, EnergyDType):
|
|
167
168
|
self.h5file["data"].attrs["datatype"] = datatype.name
|
|
168
169
|
if "datatype" not in self.h5file["data"].attrs:
|
|
169
170
|
self.h5file["data"].attrs["datatype"] = self.DATATYPE_DEFAULT.name
|
|
170
|
-
if self.get_datatype() not in
|
|
171
|
+
if self.get_datatype() not in dtypes_:
|
|
171
172
|
msg = (
|
|
172
|
-
f"Can't handle value '{self.get_datatype()}' of datatype (choose one of {
|
|
173
|
+
f"Can't handle value '{self.get_datatype()}' of datatype (choose one of {dtypes_})"
|
|
173
174
|
)
|
|
174
175
|
raise ValueError(msg)
|
|
175
176
|
|
|
@@ -183,6 +184,12 @@ class Writer(Reader):
|
|
|
183
184
|
raise ValueError("Window Size argument needed for ivcurve-Datatype")
|
|
184
185
|
|
|
185
186
|
# Handle Cal
|
|
187
|
+
if isinstance(cal_data, CalEmu):
|
|
188
|
+
msg = (
|
|
189
|
+
"Writer got a CalibrationEmulator()-object without information "
|
|
190
|
+
"about the TargetPort. Possibly wrong cal-data stored in hdf5!"
|
|
191
|
+
)
|
|
192
|
+
self._logger.warning(msg)
|
|
186
193
|
if isinstance(cal_data, (CalEmu, CalHrv)):
|
|
187
194
|
cal_data = CalSeries.from_cal(cal_data)
|
|
188
195
|
|
|
@@ -215,7 +222,7 @@ class Writer(Reader):
|
|
|
215
222
|
) -> None:
|
|
216
223
|
self._align()
|
|
217
224
|
self._refresh_file_stats()
|
|
218
|
-
self._logger.
|
|
225
|
+
self._logger.debug(
|
|
219
226
|
"closing hdf5 file, %.1f s iv-data, size = %.3f MiB, rate = %.0f KiB/s",
|
|
220
227
|
self.runtime_s,
|
|
221
228
|
self.file_size / 2**20,
|
|
@@ -296,7 +303,7 @@ class Writer(Reader):
|
|
|
296
303
|
timestamp = int(timestamp)
|
|
297
304
|
if isinstance(timestamp, int):
|
|
298
305
|
time_series_ns = self.sample_interval_ns * np.arange(len_new).astype("u8")
|
|
299
|
-
timestamp
|
|
306
|
+
timestamp += time_series_ns
|
|
300
307
|
if isinstance(timestamp, np.ndarray):
|
|
301
308
|
len_new = min(len_new, timestamp.size)
|
|
302
309
|
else:
|