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
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Annotated
|
|
8
8
|
from typing import Any
|
|
9
9
|
from typing import TypedDict
|
|
10
|
+
from typing import final
|
|
10
11
|
|
|
11
12
|
from pydantic import StringConstraints
|
|
12
13
|
from pydantic import model_validator
|
|
@@ -20,7 +21,7 @@ from shepherd_core.data_models.testbed.mcu import MCU
|
|
|
20
21
|
from shepherd_core.logger import log
|
|
21
22
|
from shepherd_core.testbed_client import tb_client
|
|
22
23
|
|
|
23
|
-
from .
|
|
24
|
+
from .enum_datatypes import FirmwareDType
|
|
24
25
|
|
|
25
26
|
suffix_to_DType: dict = {
|
|
26
27
|
# derived from wikipedia
|
|
@@ -49,17 +50,18 @@ arch_to_mcu: dict = {
|
|
|
49
50
|
FirmwareStr = Annotated[str, StringConstraints(min_length=3, max_length=8_000_000)]
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
@final
|
|
52
54
|
class Firmware(ContentModel, title="Firmware of Target"):
|
|
53
55
|
"""meta-data representation of a data-component."""
|
|
54
56
|
|
|
55
|
-
# General Metadata & Ownership -> ContentModel
|
|
57
|
+
# General Metadata & Ownership -> see ContentModel
|
|
56
58
|
|
|
57
59
|
mcu: MCU
|
|
58
60
|
|
|
59
61
|
data: FirmwareStr | Path
|
|
60
62
|
data_type: FirmwareDType
|
|
61
63
|
data_hash: str | None = None
|
|
62
|
-
|
|
64
|
+
data_2_copy: bool = True
|
|
63
65
|
""" ⤷ signals that file has to be copied to testbed"""
|
|
64
66
|
|
|
65
67
|
@model_validator(mode="before")
|
|
@@ -67,18 +69,18 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
67
69
|
def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
68
70
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
69
71
|
# crosscheck type with actual data
|
|
70
|
-
|
|
71
|
-
if
|
|
72
|
+
dtype = values.get("data_type")
|
|
73
|
+
if dtype in {
|
|
72
74
|
FirmwareDType.base64_hex,
|
|
73
75
|
FirmwareDType.base64_elf,
|
|
74
76
|
}:
|
|
75
77
|
try:
|
|
76
|
-
|
|
78
|
+
dhash = fw_tools.base64_to_hash(values.get("data"))
|
|
77
79
|
except ValueError:
|
|
78
80
|
raise ValueError("Embedded Firmware seems to be faulty") from None
|
|
79
|
-
if values.get("data_hash") is not None and
|
|
81
|
+
if values.get("data_hash") is not None and dhash != values.get("data_hash"):
|
|
80
82
|
raise ValueError("Embedded Firmware and Hash do not match!")
|
|
81
|
-
elif
|
|
83
|
+
elif dtype in {
|
|
82
84
|
FirmwareDType.path_hex,
|
|
83
85
|
FirmwareDType.path_elf,
|
|
84
86
|
}:
|
|
@@ -105,10 +107,10 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
105
107
|
kwargs["data_hash"] = fw_tools.file_to_hash(file)
|
|
106
108
|
if embed:
|
|
107
109
|
kwargs["data"] = fw_tools.file_to_base64(file)
|
|
108
|
-
kwargs["
|
|
110
|
+
kwargs["data_2_copy"] = False
|
|
109
111
|
else:
|
|
110
112
|
kwargs["data"] = Path(file).as_posix()
|
|
111
|
-
kwargs["
|
|
113
|
+
kwargs["data_2_copy"] = True
|
|
112
114
|
|
|
113
115
|
if "data_type" not in kwargs:
|
|
114
116
|
kwargs["data_type"] = suffix_to_DType[file.suffix.lower()]
|
|
@@ -145,16 +147,22 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
145
147
|
kwargs["name"] = file.name
|
|
146
148
|
return cls(**kwargs)
|
|
147
149
|
|
|
148
|
-
def compare_hash(self,
|
|
150
|
+
def compare_hash(self, data: Path | str | None = None) -> bool:
|
|
149
151
|
if self.data_hash is None:
|
|
150
152
|
return True
|
|
151
153
|
|
|
152
|
-
if
|
|
153
|
-
|
|
154
|
+
if data is None:
|
|
155
|
+
# use included data if nothing is provided
|
|
156
|
+
data = self.data
|
|
157
|
+
|
|
158
|
+
if isinstance(data, Path) and data.is_file():
|
|
159
|
+
hash_new = fw_tools.file_to_hash(data)
|
|
154
160
|
match = self.data_hash == hash_new
|
|
155
|
-
|
|
156
|
-
hash_new = fw_tools.base64_to_hash(
|
|
161
|
+
elif isinstance(data, str):
|
|
162
|
+
hash_new = fw_tools.base64_to_hash(data)
|
|
157
163
|
match = self.data_hash == hash_new
|
|
164
|
+
else:
|
|
165
|
+
match = False
|
|
158
166
|
|
|
159
167
|
if not match:
|
|
160
168
|
log.warning("FW-Hash does not match with stored value!")
|
|
@@ -169,7 +177,27 @@ class Firmware(ContentModel, title="Firmware of Target"):
|
|
|
169
177
|
- if provided path is a directory, the firmware-name is used
|
|
170
178
|
"""
|
|
171
179
|
if file.is_dir():
|
|
172
|
-
file
|
|
180
|
+
file /= self.name
|
|
173
181
|
file_new = fw_tools.extract_firmware(self.data, self.data_type, file)
|
|
174
182
|
self.compare_hash(file_new)
|
|
175
183
|
return file_new
|
|
184
|
+
|
|
185
|
+
def exists(self) -> bool:
|
|
186
|
+
"""Check if embedded file exists."""
|
|
187
|
+
if self.data_type in [FirmwareDType.path_hex, FirmwareDType.path_elf]:
|
|
188
|
+
if not isinstance(self.data, Path):
|
|
189
|
+
raise ValueError("Firmware.data is not a Path (but type-property claims so)")
|
|
190
|
+
return self.data.exists()
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
def check(self) -> bool:
|
|
194
|
+
"""Check if embedded file is still valid or unchanged."""
|
|
195
|
+
valid = True
|
|
196
|
+
if self.data_type in [FirmwareDType.path_hex, FirmwareDType.path_elf]:
|
|
197
|
+
valid &= isinstance(self.data, Path) and self.data.exists()
|
|
198
|
+
if self.data_type in [FirmwareDType.base64_elf, FirmwareDType.base64_hex]:
|
|
199
|
+
valid &= isinstance(self.data, str)
|
|
200
|
+
# TODO: could also begin unpacking base64
|
|
201
|
+
# TODO: could also verify hex, elf
|
|
202
|
+
|
|
203
|
+
return valid & self.compare_hash()
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Generalized energy harvester data models."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Mapping
|
|
4
|
-
from enum import Enum
|
|
5
4
|
from typing import Annotated
|
|
6
5
|
from typing import Any
|
|
6
|
+
from typing import final
|
|
7
7
|
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
from pydantic import model_validator
|
|
@@ -16,95 +16,11 @@ from shepherd_core.data_models.base.shepherd import ShpModel
|
|
|
16
16
|
from shepherd_core.logger import log
|
|
17
17
|
from shepherd_core.testbed_client import tb_client
|
|
18
18
|
|
|
19
|
-
from .
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AlgorithmDType(str, Enum):
|
|
23
|
-
"""Options for choosing a harvesting algorithm."""
|
|
24
|
-
|
|
25
|
-
direct = disable = neutral = "neutral"
|
|
26
|
-
"""
|
|
27
|
-
Reads an energy environment as is without selecting a harvesting
|
|
28
|
-
voltage.
|
|
29
|
-
|
|
30
|
-
Used to play "constant-power" energy environments or simple
|
|
31
|
-
"on-off-patterns". Generally, not useful for virtual source
|
|
32
|
-
emulation.
|
|
33
|
-
|
|
34
|
-
Not applicable to real harvesting, only emulation with IVTrace / samples.
|
|
35
|
-
"""
|
|
36
|
-
|
|
37
|
-
isc_voc = "isc_voc"
|
|
38
|
-
"""
|
|
39
|
-
Short Circuit Current, Open Circuit Voltage.
|
|
40
|
-
|
|
41
|
-
This is not relevant for emulation, but used to configure recording of
|
|
42
|
-
energy environments.
|
|
43
|
-
|
|
44
|
-
This mode samples the two extremes of an IV curve, which may be
|
|
45
|
-
interesting to characterize a transducer/energy environment.
|
|
46
|
-
|
|
47
|
-
Not applicable to emulation - only recordable during harvest-recording ATM.
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
ivcurve = ivcurves = ivsurface = "ivcurve"
|
|
51
|
-
"""
|
|
52
|
-
Used during harvesting to record the full IV surface.
|
|
53
|
-
|
|
54
|
-
When configuring the energy environment recording, this algorithm
|
|
55
|
-
records the IV surface by repeatedly recording voltage and current
|
|
56
|
-
while ramping the voltage.
|
|
57
|
-
|
|
58
|
-
Cannot be used as output of emulation.
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
constant = cv = "cv"
|
|
62
|
-
"""
|
|
63
|
-
Harvest energy at a fixed predefined voltage ('voltage_mV').
|
|
64
|
-
|
|
65
|
-
For harvesting, this records the IV samples at the specified voltage.
|
|
66
|
-
For emulation, this virtually harvests the IV surface at the specified voltage.
|
|
67
|
-
|
|
68
|
-
In addition to constant voltage harvesting, this can be used together
|
|
69
|
-
with the 'feedback_to_hrv' flag to implement a "Capacitor and Diode"
|
|
70
|
-
topology, where the harvesting voltage depends dynamically on the
|
|
71
|
-
capacitor voltage.
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
-
# ci .. constant current -> is this desired?
|
|
75
|
-
|
|
76
|
-
mppt_voc = "mppt_voc"
|
|
77
|
-
"""
|
|
78
|
-
Emulate a harvester with maximum power point (MPP) tracking based on
|
|
79
|
-
open circuit voltage measurements.
|
|
80
|
-
|
|
81
|
-
This MPPT heuristic estimates the MPP as a constant ratio of the open
|
|
82
|
-
circuit voltage.
|
|
83
|
-
|
|
84
|
-
Used in conjunction with 'setpoint_n', 'interval_ms', and 'duration_ms'.
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
mppt_po = perturb_observe = "mppt_po"
|
|
88
|
-
"""
|
|
89
|
-
Emulate a harvester with perturb and observe maximum power point
|
|
90
|
-
tracking.
|
|
91
|
-
|
|
92
|
-
This MPPT heuristic adjusts the harvesting voltage by small amounts and
|
|
93
|
-
checks if the power increases. Eventually, the tracking changes the
|
|
94
|
-
direction of adjustments and oscillates around the MPP.
|
|
95
|
-
"""
|
|
96
|
-
|
|
97
|
-
mppt_opt = optimal = "mppt_opt"
|
|
98
|
-
"""
|
|
99
|
-
A theoretical harvester that identifies the MPP by reading it from the
|
|
100
|
-
IV curve during emulation.
|
|
101
|
-
|
|
102
|
-
Note that this is not possible for real-world harvesting as the system would
|
|
103
|
-
not know the entire IV curve. In that case a very fast and detailed mppt_po is
|
|
104
|
-
used.
|
|
105
|
-
"""
|
|
19
|
+
from .enum_datatypes import EnergyDType
|
|
20
|
+
from .enum_datatypes import HarvestAlgorithmDType
|
|
106
21
|
|
|
107
22
|
|
|
23
|
+
@final
|
|
108
24
|
class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
109
25
|
"""The virtual harvester configuration characterizes usage of an energy environment.
|
|
110
26
|
|
|
@@ -150,9 +66,9 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
150
66
|
storage.
|
|
151
67
|
"""
|
|
152
68
|
|
|
153
|
-
# General Metadata & Ownership -> ContentModel
|
|
69
|
+
# General Metadata & Ownership -> see ContentModel
|
|
154
70
|
|
|
155
|
-
algorithm:
|
|
71
|
+
algorithm: HarvestAlgorithmDType
|
|
156
72
|
"""The algorithm determines how the harvester chooses the harvesting voltage.
|
|
157
73
|
"""
|
|
158
74
|
|
|
@@ -369,12 +285,12 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
369
285
|
|
|
370
286
|
interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
|
|
371
287
|
duration_ms = min(max(self.duration_ms, time_min_ms), interval_ms)
|
|
372
|
-
|
|
373
|
-
if (
|
|
288
|
+
ratio = (duration_ms / interval_ms) / (self.duration_ms / self.interval_ms)
|
|
289
|
+
if (ratio - 1) > 0.1:
|
|
374
290
|
log.debug(
|
|
375
291
|
"Ratio between interval & duration has changed "
|
|
376
292
|
"more than 10%% due to constraints (%.4f)",
|
|
377
|
-
|
|
293
|
+
ratio,
|
|
378
294
|
)
|
|
379
295
|
return interval_ms, duration_ms
|
|
380
296
|
|
|
@@ -436,6 +352,7 @@ ALGO_TO_DTYPE: Mapping[str, EnergyDType] = {
|
|
|
436
352
|
}
|
|
437
353
|
|
|
438
354
|
|
|
355
|
+
@final
|
|
439
356
|
class HarvesterPRUConfig(ShpModel):
|
|
440
357
|
"""Map settings-list to internal state-vars struct HarvesterConfig for PRU.
|
|
441
358
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Annotated
|
|
4
4
|
from typing import Any
|
|
5
|
+
from typing import final
|
|
5
6
|
|
|
6
7
|
from pydantic import Field
|
|
7
8
|
from pydantic import model_validator
|
|
@@ -13,7 +14,7 @@ from shepherd_core.data_models.base.shepherd import ShpModel
|
|
|
13
14
|
from shepherd_core.logger import log
|
|
14
15
|
from shepherd_core.testbed_client import tb_client
|
|
15
16
|
|
|
16
|
-
from .
|
|
17
|
+
from .enum_datatypes import EnergyDType
|
|
17
18
|
from .virtual_harvester_config import HarvesterPRUConfig
|
|
18
19
|
from .virtual_harvester_config import VirtualHarvesterConfig
|
|
19
20
|
from .virtual_storage_config import VirtualStorageConfig
|
|
@@ -28,6 +29,7 @@ LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
|
28
29
|
vhrv_mppt_opt = VirtualHarvesterConfig(name="mppt_opt")
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@final
|
|
31
33
|
class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
32
34
|
"""The vSrc uses the energy environment (file) for supplying the Target Node.
|
|
33
35
|
|
|
@@ -42,7 +44,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
42
44
|
|
|
43
45
|
# TODO: I,V,R should be in regular unit (V, A, Ohm)
|
|
44
46
|
|
|
45
|
-
# General Metadata & Ownership -> ContentModel
|
|
47
|
+
# General Metadata & Ownership -> see ContentModel
|
|
46
48
|
|
|
47
49
|
enable_boost: bool = False
|
|
48
50
|
""" ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
|
|
@@ -165,6 +167,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
165
167
|
- the converter always turns on with "V_storage_enable_threshold_uV".
|
|
166
168
|
|
|
167
169
|
TODO: currently neglecting delay after disabling converter, boost
|
|
170
|
+
TODO: warn and explain when altering config due to boundaries (transparency)
|
|
168
171
|
only has simpler formula, second enabling when V_Cap >= V_out
|
|
169
172
|
|
|
170
173
|
Math behind this calculation:
|
|
@@ -292,6 +295,7 @@ lut_i = Annotated[
|
|
|
292
295
|
lut_o = Annotated[list[u32], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
|
|
293
296
|
|
|
294
297
|
|
|
298
|
+
@final
|
|
295
299
|
class ConverterPRUConfig(ShpModel):
|
|
296
300
|
"""Map settings-list to internal state-vars struct ConverterConfig.
|
|
297
301
|
|
|
@@ -386,3 +390,18 @@ class ConverterPRUConfig(ShpModel):
|
|
|
386
390
|
for value in data.LUT_output_efficiency
|
|
387
391
|
],
|
|
388
392
|
)
|
|
393
|
+
|
|
394
|
+
def storage_is_enabled(self) -> bool:
|
|
395
|
+
return bool(self.converter_mode & 1)
|
|
396
|
+
|
|
397
|
+
def boost_is_enabled(self) -> bool:
|
|
398
|
+
return bool(self.converter_mode & 2)
|
|
399
|
+
|
|
400
|
+
def buck_is_enabled(self) -> bool:
|
|
401
|
+
return bool(self.converter_mode & 4)
|
|
402
|
+
|
|
403
|
+
def logging_intermediate_node_is_enabled(self) -> bool:
|
|
404
|
+
return bool(self.converter_mode & 8)
|
|
405
|
+
|
|
406
|
+
def feedback_is_enabled(self) -> bool:
|
|
407
|
+
return bool(self.converter_mode & 16)
|
|
@@ -16,6 +16,7 @@ from collections.abc import Sequence
|
|
|
16
16
|
from datetime import timedelta
|
|
17
17
|
from typing import Annotated
|
|
18
18
|
from typing import Any
|
|
19
|
+
from typing import final
|
|
19
20
|
|
|
20
21
|
from annotated_types import Ge
|
|
21
22
|
from annotated_types import Gt
|
|
@@ -38,6 +39,7 @@ soc_t = Annotated[float, Ge(0.0), Le(1.0)]
|
|
|
38
39
|
# TODO: do we need to set initial voltage, or is SoC ok? add V_OC_to_SoC()
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
@final
|
|
41
43
|
class VirtualStorageConfig(ContentModel, title="Config for the virtual energy storage"):
|
|
42
44
|
"""KiBaM Battery model based on two papers.
|
|
43
45
|
|
|
@@ -129,7 +131,7 @@ class VirtualStorageConfig(ContentModel, title="Config for the virtual energy st
|
|
|
129
131
|
if description is None:
|
|
130
132
|
description = "Model of a LiPo battery (3 to 4.2 V) with adjustable capacity"
|
|
131
133
|
return cls(
|
|
132
|
-
SoC_init=SoC_init
|
|
134
|
+
SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
|
|
133
135
|
q_As=q_mAh * 3600 / 1000,
|
|
134
136
|
p_VOC=[-0.852, 63.867, 3.6297, 0.559, 0.51, 0.508],
|
|
135
137
|
p_Rs=[0.1463, 30.27, 0.1037, 0.0584, 0.1747, 0.1288],
|
|
@@ -176,7 +178,7 @@ class VirtualStorageConfig(ContentModel, title="Config for the virtual energy st
|
|
|
176
178
|
if description is None:
|
|
177
179
|
description = "Model of a 12V lead acid battery with adjustable capacity"
|
|
178
180
|
return cls(
|
|
179
|
-
SoC_init=SoC_init
|
|
181
|
+
SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
|
|
180
182
|
q_As=q_mAh * 3600 / 1000,
|
|
181
183
|
p_VOC=[5.429, 117.5, 11.32, 2.706, 2.04, 1.026],
|
|
182
184
|
p_Rs=[1.578, 8.527, 0.7808, -1.887, -2.404, -0.649],
|
|
@@ -222,14 +224,14 @@ class VirtualStorageConfig(ContentModel, title="Config for the virtual energy st
|
|
|
222
224
|
if R_series_Ohm is not None:
|
|
223
225
|
description += f", R_serial = {R_series_Ohm:.0f} Ohm"
|
|
224
226
|
return cls(
|
|
225
|
-
SoC_init=SoC_init
|
|
227
|
+
SoC_init=SoC_init or cls.model_fields["SoC_init"].default,
|
|
226
228
|
q_As=1e-6 * C_uF * V_rated,
|
|
227
229
|
p_VOC=[0, 0, 0, V_rated, 0, 0], # 100% SoC is @V_rated,
|
|
228
230
|
# no transients per default
|
|
229
231
|
p_Rs=[0, 0, R_series_Ohm, 0, 0, 0]
|
|
230
232
|
if R_series_Ohm
|
|
231
233
|
else cls.model_fields["p_Rs"].default, # const series resistance
|
|
232
|
-
R_leak_Ohm=R_leak_Ohm
|
|
234
|
+
R_leak_Ohm=R_leak_Ohm or cls.model_fields["R_leak_Ohm"].default,
|
|
233
235
|
# content-fields below
|
|
234
236
|
name="_".join(name_lmnts),
|
|
235
237
|
description=description,
|
|
@@ -378,6 +380,7 @@ u32 = Annotated[int, Field(ge=0, lt=2**32)]
|
|
|
378
380
|
lut_storage = Annotated[list[u32], Field(min_length=LuT_SIZE, max_length=LuT_SIZE)]
|
|
379
381
|
|
|
380
382
|
|
|
383
|
+
@final
|
|
381
384
|
class StoragePRUConfig(ShpModel):
|
|
382
385
|
"""Map settings-list to internal state-vars struct StorageConfig.
|
|
383
386
|
|
|
@@ -19,7 +19,7 @@ dsc_super = "SuperCapacitor with typically 1000 hours / 500 k cycles (not modele
|
|
|
19
19
|
# typical voltage-ratings: 2.5, 4.0, 6.3, 10, 16, 20 V
|
|
20
20
|
E6: list[int] = [10, 15, 22, 33, 47, 68, 100, 150, 220, 330, 470, 680, 1000]
|
|
21
21
|
fixture_ideal: list[VirtualStorageConfig] = [
|
|
22
|
-
VirtualStorageConfig.capacitor(C_uF=
|
|
22
|
+
VirtualStorageConfig.capacitor(C_uF=v_, V_rated=10.0, description=dsc_ideal) for v_ in E6
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
# Tantal Capacitors, E6 row
|
|
@@ -46,7 +46,7 @@ def experiment_self_discharge_lead_acid() -> None:
|
|
|
46
46
|
|
|
47
47
|
sim.run(fn=step, duration_s=duration.total_seconds())
|
|
48
48
|
sim.plot(
|
|
49
|
-
f"
|
|
49
|
+
f"Experiment {cfg1.name}, self-discharge, "
|
|
50
50
|
f"SoC {SoC_init:.3f} to {SoC_final:.3f} in {duration.total_seconds()} s"
|
|
51
51
|
)
|
|
52
52
|
|
|
@@ -83,7 +83,7 @@ def experiment_self_discharge_lipo() -> None:
|
|
|
83
83
|
|
|
84
84
|
sim.run(fn=step, duration_s=duration.total_seconds())
|
|
85
85
|
sim.plot(
|
|
86
|
-
f"
|
|
86
|
+
f"Experiment {cfg1.name}, self-discharge, "
|
|
87
87
|
f"SoC {SoC_init:.3f} to {SoC_final:.3f} in {duration.total_seconds()} s"
|
|
88
88
|
)
|
|
89
89
|
|
|
@@ -112,7 +112,7 @@ def experiment_self_discharge_tantal_avx() -> None:
|
|
|
112
112
|
return 0
|
|
113
113
|
|
|
114
114
|
sim.run(fn=step, duration_s=duration.total_seconds())
|
|
115
|
-
sim.plot(f"
|
|
115
|
+
sim.plot(f"Experiment Tantal AVX, self-discharge {duration.total_seconds()} s")
|
|
116
116
|
|
|
117
117
|
|
|
118
118
|
def experiment_self_discharge_mlcc_tayo() -> None:
|
|
@@ -141,7 +141,7 @@ def experiment_self_discharge_mlcc_tayo() -> None:
|
|
|
141
141
|
return 0
|
|
142
142
|
|
|
143
143
|
sim.run(fn=step, duration_s=duration.total_seconds())
|
|
144
|
-
sim.plot(f"
|
|
144
|
+
sim.plot(f"Experiment MLCC Tayo, self-discharge {duration.total_seconds()} s")
|
|
145
145
|
|
|
146
146
|
|
|
147
147
|
if __name__ == "__main__":
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from collections.abc import Iterable
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from datetime import timedelta
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
from typing import Annotated
|
|
8
|
+
from typing import final
|
|
7
9
|
|
|
8
10
|
from pydantic import Field
|
|
9
11
|
from pydantic import model_validator
|
|
@@ -16,15 +18,20 @@ from shepherd_core.data_models.base.shepherd import ShpModel
|
|
|
16
18
|
from shepherd_core.data_models.base.timezone import local_now
|
|
17
19
|
from shepherd_core.data_models.testbed.target import Target
|
|
18
20
|
from shepherd_core.data_models.testbed.testbed import Testbed
|
|
21
|
+
from shepherd_core.logger import log
|
|
19
22
|
from shepherd_core.version import version
|
|
20
23
|
|
|
21
24
|
from .observer_features import SystemLogging
|
|
22
25
|
from .target_config import TargetConfig
|
|
23
26
|
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
24
30
|
# defaults (pre-init complex types)
|
|
25
31
|
sys_log_all = SystemLogging() # = all active
|
|
26
32
|
|
|
27
33
|
|
|
34
|
+
@final
|
|
28
35
|
class Experiment(ShpModel, title="Config of an Experiment"):
|
|
29
36
|
"""Config for experiments on the testbed emulating energy environments for target nodes."""
|
|
30
37
|
|
|
@@ -54,6 +61,7 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
54
61
|
def post_validation(self) -> Self:
|
|
55
62
|
self._validate_observers(self.target_configs)
|
|
56
63
|
self._validate_targets(self.target_configs)
|
|
64
|
+
self._validate_eenvs(self.target_configs)
|
|
57
65
|
if self.duration and self.duration.total_seconds() < 0:
|
|
58
66
|
raise ValueError("Duration of experiment can't be negative.")
|
|
59
67
|
return self
|
|
@@ -62,16 +70,16 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
62
70
|
def _validate_targets(configs: Iterable[TargetConfig]) -> None:
|
|
63
71
|
target_ids: list[int] = []
|
|
64
72
|
custom_ids: list[int] = []
|
|
65
|
-
for
|
|
66
|
-
for
|
|
67
|
-
target_ids.append(
|
|
73
|
+
for config_ in configs:
|
|
74
|
+
for id_ in config_.target_IDs:
|
|
75
|
+
target_ids.append(id_)
|
|
68
76
|
if config.VALIDATE_INFRA:
|
|
69
|
-
Target(id=
|
|
77
|
+
Target(id=id_)
|
|
70
78
|
# ⤷ this can raise exception for non-existing targets
|
|
71
|
-
if
|
|
72
|
-
custom_ids
|
|
79
|
+
if config_.custom_IDs is not None:
|
|
80
|
+
custom_ids += config_.custom_IDs[: len(config_.target_IDs)]
|
|
73
81
|
else:
|
|
74
|
-
custom_ids
|
|
82
|
+
custom_ids += config_.target_IDs
|
|
75
83
|
if len(target_ids) > len(set(target_ids)):
|
|
76
84
|
raise ValueError("Target-ID used more than once in Experiment!")
|
|
77
85
|
if len(target_ids) > len(set(custom_ids)):
|
|
@@ -82,20 +90,37 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
82
90
|
if not config.VALIDATE_INFRA:
|
|
83
91
|
return
|
|
84
92
|
testbed = Testbed()
|
|
85
|
-
target_ids = [
|
|
86
|
-
obs_ids = [testbed.get_observer(
|
|
93
|
+
target_ids = [id_ for config_ in configs for id_ in config_.target_IDs]
|
|
94
|
+
obs_ids = [testbed.get_observer(id_).id for id_ in target_ids]
|
|
87
95
|
if len(target_ids) > len(set(obs_ids)):
|
|
88
96
|
raise ValueError(
|
|
89
97
|
"Observer is used more than once in Experiment -> only 1 target per observer!"
|
|
90
98
|
)
|
|
91
99
|
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _validate_eenvs(configs: Iterable[TargetConfig]) -> None:
|
|
102
|
+
"""Make sure eenvs are usable."""
|
|
103
|
+
# TODO: these individual validations should go to class itself (decoupling)
|
|
104
|
+
# TODO: data_2_copy means the data itself must be locally available
|
|
105
|
+
paths_all: list[Path] = [
|
|
106
|
+
path
|
|
107
|
+
for cfg in configs
|
|
108
|
+
for path in cfg.get_critical_paths(warn_reuse=False)
|
|
109
|
+
# direct warning inside cfg is disabled here as it is done during cfg.__init__()
|
|
110
|
+
]
|
|
111
|
+
if len(paths_all) != len(set(paths_all)):
|
|
112
|
+
log.warning(
|
|
113
|
+
"Detected re-usage of non-repeatable EnergyProfiles "
|
|
114
|
+
"in Experiment across TargetConfigs"
|
|
115
|
+
)
|
|
116
|
+
|
|
92
117
|
def get_target_ids(self) -> list:
|
|
93
|
-
return [
|
|
118
|
+
return [id_ for config_ in self.target_configs for id_ in config_.target_IDs]
|
|
94
119
|
|
|
95
120
|
def get_target_config(self, target_id: int) -> TargetConfig:
|
|
96
|
-
for
|
|
97
|
-
if target_id in
|
|
98
|
-
return
|
|
121
|
+
for config_ in self.target_configs:
|
|
122
|
+
if target_id in config_.target_IDs:
|
|
123
|
+
return config_
|
|
99
124
|
# gets already caught in target_config - but keep:
|
|
100
125
|
msg = f"Target-ID {target_id} was not found in Experiment '{self.name}'"
|
|
101
126
|
raise ValueError(msg)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from datetime import timedelta
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from typing import Annotated
|
|
6
|
+
from typing import final
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
from annotated_types import Interval
|
|
@@ -19,12 +20,17 @@ from shepherd_core.logger import log
|
|
|
19
20
|
zero_duration = timedelta(seconds=0)
|
|
20
21
|
|
|
21
22
|
|
|
23
|
+
@final
|
|
22
24
|
class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
23
|
-
"""Configuration for recording the Power-Consumption of the Target Nodes.
|
|
25
|
+
"""Configuration for recording the Power-Consumption of the Target Nodes.
|
|
26
|
+
|
|
27
|
+
With the default configuration voltage and current are sampled with 100 kHz.
|
|
28
|
+
"""
|
|
24
29
|
|
|
25
30
|
intermediate_voltage: bool = False
|
|
26
31
|
"""
|
|
27
|
-
⤷ for EMU: record
|
|
32
|
+
⤷ for EMU: record output-path of intermediate energy storage (capacitor, battery)
|
|
33
|
+
instead of direct target voltage-output (good for V_out = const)
|
|
28
34
|
this also includes current!
|
|
29
35
|
"""
|
|
30
36
|
# time
|
|
@@ -38,7 +44,8 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
|
|
|
38
44
|
# further processing of IV-Samples
|
|
39
45
|
only_power: bool = False
|
|
40
46
|
""" ⤷ reduce file-size by calculating power and automatically discard I&V
|
|
41
|
-
Caution: increases cpu-utilization on observer
|
|
47
|
+
Caution: increases cpu-utilization on observer
|
|
48
|
+
sampling power @ 100 kHz is not recommended
|
|
42
49
|
"""
|
|
43
50
|
samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000
|
|
44
51
|
""" ⤷ reduce file-size by re-sampling (mean over x samples)
|
|
@@ -107,6 +114,7 @@ STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO = (1, 1.5, 2)
|
|
|
107
114
|
STOPBITS = (STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO)
|
|
108
115
|
|
|
109
116
|
|
|
117
|
+
@final
|
|
110
118
|
class UartLogging(ShpModel, title="Config for UART Logging"):
|
|
111
119
|
"""Configuration for recording UART-Output of the Target Nodes.
|
|
112
120
|
|
|
@@ -139,6 +147,7 @@ GpioList = Annotated[list[GpioInt], Field(min_length=1, max_length=18)]
|
|
|
139
147
|
all_gpio = list(range(18))
|
|
140
148
|
|
|
141
149
|
|
|
150
|
+
@final
|
|
142
151
|
class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
143
152
|
"""Configuration for recording the GPIO-Output of the Target Nodes.
|
|
144
153
|
|
|
@@ -194,6 +203,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
194
203
|
return mask
|
|
195
204
|
|
|
196
205
|
|
|
206
|
+
@final
|
|
197
207
|
class GpioLevel(str, Enum):
|
|
198
208
|
"""Options for setting the gpio-level or state."""
|
|
199
209
|
|
|
@@ -202,6 +212,7 @@ class GpioLevel(str, Enum):
|
|
|
202
212
|
toggle = "X" # TODO: not the smartest decision for writing a converter
|
|
203
213
|
|
|
204
214
|
|
|
215
|
+
@final
|
|
205
216
|
class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
206
217
|
"""Configuration for a single GPIO-Event (Actuation)."""
|
|
207
218
|
|
|
@@ -228,6 +239,7 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
|
228
239
|
return np.arange(self.delay, stop, self.period)
|
|
229
240
|
|
|
230
241
|
|
|
242
|
+
@final
|
|
231
243
|
class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
232
244
|
"""Configuration for a GPIO-Actuation-Sequence."""
|
|
233
245
|
|
|
@@ -242,9 +254,10 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
|
242
254
|
raise ValueError(msg)
|
|
243
255
|
|
|
244
256
|
def get_gpios(self) -> set:
|
|
245
|
-
return {
|
|
257
|
+
return {ev_.gpio for ev_ in self.events}
|
|
246
258
|
|
|
247
259
|
|
|
260
|
+
@final
|
|
248
261
|
class SystemLogging(ShpModel, title="Config for System-Logging"):
|
|
249
262
|
"""Configuration for recording Debug-Output of the Observers System-Services."""
|
|
250
263
|
|