shepherd-core 2025.8.1__py3-none-any.whl → 2026.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shepherd_core/config.py +1 -1
- shepherd_core/data_models/__init__.py +8 -4
- shepherd_core/data_models/base/cal_measurement.py +7 -2
- shepherd_core/data_models/base/calibration.py +23 -12
- shepherd_core/data_models/base/content.py +12 -2
- shepherd_core/data_models/base/shepherd.py +13 -4
- shepherd_core/data_models/base/wrapper.py +2 -0
- shepherd_core/data_models/content/__init__.py +8 -4
- shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
- shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
- shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
- shepherd_core/data_models/content/energy_environment.py +341 -23
- shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
- shepherd_core/data_models/content/enum_datatypes.py +109 -0
- shepherd_core/data_models/content/firmware.py +44 -16
- shepherd_core/data_models/content/{virtual_harvester.py → virtual_harvester_config.py} +13 -96
- shepherd_core/data_models/content/{virtual_source.py → virtual_source_config.py} +103 -60
- shepherd_core/data_models/content/virtual_source_fixture.yaml +24 -24
- shepherd_core/data_models/content/virtual_storage_config.py +429 -0
- shepherd_core/data_models/content/virtual_storage_fixture_creator.py +267 -0
- shepherd_core/data_models/content/virtual_storage_fixture_ideal.yaml +637 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lead.yaml +49 -0
- shepherd_core/data_models/content/virtual_storage_fixture_lipo.yaml +735 -0
- shepherd_core/data_models/content/virtual_storage_fixture_mlcc.yaml +200 -0
- shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +151 -0
- shepherd_core/data_models/content/virtual_storage_fixture_super.yaml +150 -0
- shepherd_core/data_models/content/virtual_storage_fixture_tantal.yaml +550 -0
- shepherd_core/data_models/experiment/experiment.py +38 -13
- shepherd_core/data_models/experiment/observer_features.py +17 -4
- shepherd_core/data_models/experiment/target_config.py +56 -8
- shepherd_core/data_models/task/__init__.py +13 -2
- shepherd_core/data_models/task/emulation.py +10 -6
- shepherd_core/data_models/task/firmware_mod.py +3 -1
- shepherd_core/data_models/task/harvest.py +3 -1
- shepherd_core/data_models/task/helper_paths.py +2 -2
- shepherd_core/data_models/task/observer_tasks.py +8 -6
- shepherd_core/data_models/task/programming.py +4 -2
- shepherd_core/data_models/task/testbed_tasks.py +8 -2
- shepherd_core/data_models/testbed/cape.py +2 -0
- shepherd_core/data_models/testbed/gpio.py +2 -0
- shepherd_core/data_models/testbed/mcu.py +2 -0
- shepherd_core/data_models/testbed/observer.py +2 -0
- shepherd_core/data_models/testbed/target.py +7 -5
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +17 -15
- shepherd_core/decoder_waveform/uart.py +1 -1
- shepherd_core/exit_handler.py +22 -0
- shepherd_core/fw_tools/converter.py +2 -2
- shepherd_core/fw_tools/validation.py +1 -1
- shepherd_core/inventory/__init__.py +23 -21
- shepherd_core/inventory/system.py +3 -3
- shepherd_core/logger.py +0 -1
- shepherd_core/reader.py +32 -27
- shepherd_core/testbed_client/cache_path.py +3 -3
- shepherd_core/testbed_client/client_abc_fix.py +14 -3
- shepherd_core/testbed_client/client_web.py +7 -5
- shepherd_core/testbed_client/fixtures.py +7 -7
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/__init__.py +4 -0
- shepherd_core/vsource/virtual_converter_model.py +29 -28
- shepherd_core/vsource/virtual_harvester_model.py +29 -21
- shepherd_core/vsource/virtual_harvester_simulation.py +38 -39
- shepherd_core/vsource/virtual_source_model.py +18 -14
- shepherd_core/vsource/virtual_source_simulation.py +71 -73
- shepherd_core/vsource/virtual_storage_model.py +164 -0
- shepherd_core/vsource/virtual_storage_model_fixed_point_math.py +58 -0
- shepherd_core/vsource/virtual_storage_models_kibam.py +449 -0
- shepherd_core/vsource/virtual_storage_simulator.py +104 -0
- shepherd_core/writer.py +16 -9
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +6 -3
- shepherd_core-2026.2.1.dist-info/RECORD +102 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
- shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
- shepherd_core/data_models/content/firmware_datatype.py +0 -15
- shepherd_core/data_models/virtual_source_doc.txt +0 -207
- shepherd_core-2025.8.1.dist-info/RECORD +0 -83
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
- {shepherd_core-2025.8.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
|
@@ -1,52 +1,370 @@
|
|
|
1
|
-
"""Data-model for recorded
|
|
1
|
+
"""Data-model for recorded Energy-Environments (EEnvs).
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Scalar environment-recordings are called EnergyProfiles (only temporal dimension).
|
|
4
|
+
EnergyEnvironments are metadata representations of spatio-temporal energy-recordings.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- environments hold >= 1 profile / recording
|
|
8
|
+
- allows mapping profiles to individual targets (in target config)
|
|
9
|
+
- emit warning if single EEnv is used more than once to avoid unwanted correlation effects
|
|
10
|
+
- exception to that rule if EEnv allows for it (repetition_ok)
|
|
11
|
+
- checked on local TargetConfig-level and more global on experiment-level
|
|
12
|
+
- avoid funky behavior & hidden mechanics
|
|
13
|
+
- environments can be composed (add single profiles, list of profiles or a 2nd environment)
|
|
14
|
+
- offer structured metadata (dict) for information about the environment
|
|
15
|
+
- access elements similar to list[]-syntax for single items and slices
|
|
16
|
+
|
|
17
|
+
Profiles embed generalized metadata:
|
|
18
|
+
- duration of the recording,
|
|
19
|
+
- maximum harvestable energy,
|
|
20
|
+
- flag to signal a valid recording file,
|
|
21
|
+
- flag to signal that repetitions are okay -> typically used by
|
|
22
|
+
static / synthetic traces that don't cause unwanted correlation effects
|
|
23
|
+
|
|
24
|
+
Typical additional metadata keys for Energy Environments:
|
|
25
|
+
- recording-tool/generation-script,
|
|
26
|
+
- [maximum harvestable energy] -> already hardcoded
|
|
27
|
+
- location (address/GPS),
|
|
28
|
+
- site-description (building/forest),
|
|
29
|
+
- weather,
|
|
30
|
+
- node specific data, like
|
|
31
|
+
- transducer used
|
|
32
|
+
- location within experiment
|
|
33
|
+
|
|
34
|
+
TODO: add TargetConfig-Builder that makes it easier to construct complex scenarios
|
|
35
|
+
see proto_target_config_builder.py
|
|
36
|
+
TODO: find a proper solution for slicing repetitions (consider slice-length)
|
|
37
|
+
or get rid of funky behavior (warning is emitted ATM)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import shutil
|
|
41
|
+
from collections.abc import Mapping
|
|
42
|
+
from collections.abc import Sequence
|
|
43
|
+
from copy import deepcopy
|
|
4
44
|
from pathlib import Path
|
|
45
|
+
from typing import Annotated
|
|
5
46
|
from typing import Any
|
|
47
|
+
from typing import final
|
|
48
|
+
from typing import overload
|
|
6
49
|
|
|
50
|
+
import yaml
|
|
51
|
+
from pydantic import Field
|
|
52
|
+
from pydantic import NonNegativeFloat
|
|
7
53
|
from pydantic import PositiveFloat
|
|
8
54
|
from pydantic import model_validator
|
|
55
|
+
from pydantic import validate_call
|
|
56
|
+
from typing_extensions import Self
|
|
9
57
|
|
|
10
58
|
from shepherd_core.data_models.base.content import ContentModel
|
|
59
|
+
from shepherd_core.data_models.base.content import id_default
|
|
60
|
+
from shepherd_core.data_models.base.shepherd import ShpModel
|
|
61
|
+
from shepherd_core.data_models.base.timezone import local_now
|
|
62
|
+
from shepherd_core.logger import log
|
|
63
|
+
from shepherd_core.reader import Reader
|
|
11
64
|
from shepherd_core.testbed_client import tb_client
|
|
12
65
|
|
|
66
|
+
from .enum_datatypes import EnergyDType
|
|
13
67
|
|
|
14
|
-
class EnergyDType(str, Enum):
|
|
15
|
-
"""Data-Type-Options for energy environments."""
|
|
16
68
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class EnergyEnvironment(ContentModel):
|
|
23
|
-
"""Recording of meta-data representation of a testbed-component."""
|
|
24
|
-
|
|
25
|
-
# General Metadata & Ownership -> ContentModel
|
|
69
|
+
@final
|
|
70
|
+
class EnergyProfile(ShpModel):
|
|
71
|
+
"""Metadata representation of scalar energy-recording."""
|
|
26
72
|
|
|
27
73
|
data_path: Path
|
|
28
74
|
data_type: EnergyDType
|
|
29
|
-
|
|
75
|
+
data_2_copy: bool = True
|
|
30
76
|
""" ⤷ signals that file has to be copied to testbed"""
|
|
31
77
|
|
|
32
78
|
duration: PositiveFloat
|
|
33
|
-
energy_Ws:
|
|
79
|
+
energy_Ws: NonNegativeFloat
|
|
80
|
+
""" ⤷ maximum usable energy """
|
|
34
81
|
valid: bool = False
|
|
82
|
+
""" ⤷ profile is marked invalid by default to:
|
|
83
|
+
- motivate using .from_file(), or
|
|
84
|
+
- easier find manual validity-overrides
|
|
85
|
+
"""
|
|
86
|
+
repetitions_ok: bool = False
|
|
87
|
+
"""⤷ emit no warning if single profile-path is used more than once.
|
|
88
|
+
this protects against unwanted correlation effects.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def export(self, output_path: Path) -> Self:
|
|
92
|
+
"""Copy this EnergyProfile to a new destination."""
|
|
93
|
+
if not self.data_path.exists():
|
|
94
|
+
raise FileNotFoundError("EnergyProfile is not locally available.")
|
|
95
|
+
if output_path.exists():
|
|
96
|
+
if output_path.is_dir():
|
|
97
|
+
file_path = output_path / self.data_path.name
|
|
98
|
+
else:
|
|
99
|
+
raise FileExistsError("Provided export-path exists, but is not a directory")
|
|
100
|
+
else:
|
|
101
|
+
output_path.parent.mkdir(exist_ok=True, parents=True)
|
|
102
|
+
file_path = output_path
|
|
103
|
+
# TODO: offer both, move and copy?
|
|
104
|
+
shutil.copy(self.data_path, file_path)
|
|
105
|
+
return self.model_copy(deep=True, update={"data_path": file_path})
|
|
106
|
+
|
|
107
|
+
def check(self) -> bool:
|
|
108
|
+
"""Check validity of Energy-Profile.
|
|
109
|
+
|
|
110
|
+
Path must exist, be a file, be shepherd-hdf5-format.
|
|
111
|
+
"""
|
|
112
|
+
if not self.data_path.exists():
|
|
113
|
+
log.error(f"EnergyProfile does not exist in '{self.data_path}'.")
|
|
114
|
+
return False
|
|
115
|
+
if not self.data_path.is_file():
|
|
116
|
+
log.error(f"EnergyProfile is not a file ({self.data_path}).")
|
|
117
|
+
return False
|
|
118
|
+
try:
|
|
119
|
+
with Reader(self.data_path) as reader:
|
|
120
|
+
if self.duration != reader.runtime_s:
|
|
121
|
+
log.error(
|
|
122
|
+
f"EnergyProfile duration does not match runtime of file ({self.data_path})."
|
|
123
|
+
)
|
|
124
|
+
return False
|
|
125
|
+
if self.valid != reader.is_valid():
|
|
126
|
+
log.error(
|
|
127
|
+
f"EnergyProfile validity-state does not match file ({self.data_path})."
|
|
128
|
+
)
|
|
129
|
+
return False
|
|
130
|
+
if self.energy_Ws != reader.energy():
|
|
131
|
+
log.error(f"EnergyProfile max energy does not match file ({self.data_path}).")
|
|
132
|
+
return False
|
|
133
|
+
except TypeError:
|
|
134
|
+
log.error(f"EnergyProfile - hdf5-file could not be read ({self.data_path})")
|
|
135
|
+
return False
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
def exists(self) -> bool:
|
|
139
|
+
"""Check if embedded file exists."""
|
|
140
|
+
return self.data_path.exists()
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def derive_from_file(
|
|
144
|
+
cls,
|
|
145
|
+
hdf: Path,
|
|
146
|
+
data_type: EnergyDType | None = None,
|
|
147
|
+
*,
|
|
148
|
+
repetition_ok: bool = False,
|
|
149
|
+
) -> Self:
|
|
150
|
+
"""Use recording to fill in most fields."""
|
|
151
|
+
with Reader(hdf, verbose=False) as reader:
|
|
152
|
+
dtype = data_type or reader.get_datatype()
|
|
153
|
+
if dtype is None:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
"EnergyDType could not be determined from file, please provide it."
|
|
156
|
+
)
|
|
157
|
+
return cls(
|
|
158
|
+
data_path=hdf,
|
|
159
|
+
data_type=dtype,
|
|
160
|
+
data_2_copy=True,
|
|
161
|
+
duration=reader.runtime_s,
|
|
162
|
+
energy_Ws=reader.energy(),
|
|
163
|
+
valid=reader.is_valid(),
|
|
164
|
+
repetitions_ok=repetition_ok,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@final
|
|
169
|
+
class EnergyEnvironment(ContentModel):
|
|
170
|
+
"""Metadata representation of spatio-temporal energy-recording."""
|
|
171
|
+
|
|
172
|
+
# General Metadata & Ownership -> see ContentModel
|
|
173
|
+
|
|
174
|
+
energy_profiles: Annotated[list[EnergyProfile], Field(min_length=1)]
|
|
175
|
+
""" ⤷ list of individual profiles that make up the environment"""
|
|
176
|
+
|
|
177
|
+
metadata: Mapping[str, str | int | float] = {}
|
|
178
|
+
""" ⤷ additional descriptive information
|
|
179
|
+
|
|
180
|
+
Example for solar: (main) light source, weather conditions, indoor
|
|
181
|
+
General: transducer / harvester used, date, time, experiment setup, location, route
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
modifications: Sequence[str] = []
|
|
185
|
+
"""Changes recorded by manipulation-Ops (i.e. addition, slicing)."""
|
|
35
186
|
|
|
36
187
|
# TODO: scale up/down voltage/current
|
|
37
|
-
# TODO: multiple files for one env
|
|
38
188
|
# TODO: mean power as energy/duration
|
|
39
|
-
# TODO: harvester, transducer
|
|
40
189
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
190
|
+
PROFILES_MAX: int = Field(default=128, exclude=True)
|
|
191
|
+
""" ⤷ arbitrary maximum, internal state which controls behavior for repetitions_ok-cases
|
|
192
|
+
|
|
193
|
+
- single item list access is possible as modulo
|
|
194
|
+
- sliced list access repeats profile-list up to max length
|
|
195
|
+
ee[10:] gets (max - 10) items
|
|
196
|
+
"""
|
|
46
197
|
|
|
47
198
|
@model_validator(mode="before")
|
|
48
199
|
@classmethod
|
|
49
200
|
def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
201
|
+
"""Add missing entries of class by querying database."""
|
|
50
202
|
values, _ = tb_client.try_completing_model(cls.__name__, values)
|
|
51
|
-
# TODO: figure out a way to crosscheck type with actual data
|
|
52
203
|
return tb_client.fill_in_user_data(values)
|
|
204
|
+
|
|
205
|
+
def __len__(self) -> int:
|
|
206
|
+
if self.repetitions_ok:
|
|
207
|
+
return self.PROFILES_MAX
|
|
208
|
+
return len(self.energy_profiles)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def duration(self) -> PositiveFloat:
|
|
212
|
+
"""Duration of the recorded environment (minimum of all profiles) in seconds."""
|
|
213
|
+
return min(profile.duration for profile in self.energy_profiles)
|
|
214
|
+
|
|
215
|
+
@property
|
|
216
|
+
def repetitions_ok(self) -> bool:
|
|
217
|
+
"""Emit no warning if single profile-path is used more than once."""
|
|
218
|
+
return all(profile.repetitions_ok for profile in self.energy_profiles)
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def valid(self) -> bool:
|
|
222
|
+
return all(profile.valid for profile in self.energy_profiles)
|
|
223
|
+
|
|
224
|
+
def enforce_validity(self) -> None:
|
|
225
|
+
"""Offer soft validation that can be used by upper classes."""
|
|
226
|
+
msg = f"All EnergyProfiles in EnergyEnvironment {self.name} must be marked valid."
|
|
227
|
+
msg += " False for:"
|
|
228
|
+
for profile in self.energy_profiles:
|
|
229
|
+
if not profile.valid:
|
|
230
|
+
msg += f"\n\t- {profile.data_path}"
|
|
231
|
+
if not self.valid:
|
|
232
|
+
raise ValueError(msg + "\n")
|
|
233
|
+
|
|
234
|
+
@validate_call(validate_return=False)
|
|
235
|
+
def __add__(self, rvalue: ShpModel | list[ShpModel]) -> Self:
|
|
236
|
+
"""Extend this EnergyEnvironment.
|
|
237
|
+
|
|
238
|
+
Possible concatenations:
|
|
239
|
+
- a single EProfile,
|
|
240
|
+
- a list of EnergyProfiles,
|
|
241
|
+
- a second EnergyEnvironment
|
|
242
|
+
"""
|
|
243
|
+
id_new = id_default()
|
|
244
|
+
data: dict[str, Any] = {
|
|
245
|
+
"id": id_new,
|
|
246
|
+
"created": local_now(),
|
|
247
|
+
"updated_last": local_now(),
|
|
248
|
+
}
|
|
249
|
+
if isinstance(rvalue, EnergyProfile):
|
|
250
|
+
data["modifications"] = deepcopy(
|
|
251
|
+
[
|
|
252
|
+
*self.modifications,
|
|
253
|
+
f"EEnv '{self.name}' - added EnergyProfile {rvalue.data_path.stem}, "
|
|
254
|
+
f"ID [{self.id}->{id_new}]",
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
data["energy_profiles"] = deepcopy([*self.energy_profiles, rvalue])
|
|
258
|
+
return self.model_copy(deep=True, update=data)
|
|
259
|
+
if isinstance(rvalue, list):
|
|
260
|
+
if len(rvalue) == 0:
|
|
261
|
+
return self.model_copy(deep=True)
|
|
262
|
+
if isinstance(rvalue[0], EnergyProfile):
|
|
263
|
+
data["modifications"] = deepcopy(
|
|
264
|
+
[
|
|
265
|
+
*self.modifications,
|
|
266
|
+
f"EEnv '{self.name}' - added list of {len(rvalue)} EnergyProfiles, "
|
|
267
|
+
f"ID[{self.id}->{id_new}]",
|
|
268
|
+
]
|
|
269
|
+
)
|
|
270
|
+
data["energy_profiles"] = deepcopy(self.energy_profiles + rvalue)
|
|
271
|
+
return self.model_copy(deep=True, update=data)
|
|
272
|
+
raise ValueError("Addition could not be performed, as types did not match.")
|
|
273
|
+
if isinstance(rvalue, EnergyEnvironment):
|
|
274
|
+
data["modifications"] = deepcopy(
|
|
275
|
+
[
|
|
276
|
+
*self.modifications,
|
|
277
|
+
*rvalue.modifications,
|
|
278
|
+
f"EEnv '{self.name}' - added EEnv {rvalue.name} with {len(rvalue)} entries, "
|
|
279
|
+
f"ID[{self.id}->{id_new}]",
|
|
280
|
+
]
|
|
281
|
+
)
|
|
282
|
+
data["metadata"] = deepcopy({**rvalue.metadata, **self.metadata})
|
|
283
|
+
# ⤷ values of right side are kept in case of key-collision
|
|
284
|
+
data["energy_profiles"] = deepcopy(self.energy_profiles + rvalue.energy_profiles)
|
|
285
|
+
return self.model_copy(deep=True, update=data)
|
|
286
|
+
raise TypeError(
|
|
287
|
+
"Right value of addition must be of type: "
|
|
288
|
+
"EnergyProfile, list[EnergyProfile], EnergyEnvironment."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@overload
|
|
292
|
+
def __getitem__(self, value: int) -> EnergyProfile: ...
|
|
293
|
+
@overload
|
|
294
|
+
def __getitem__(self, value: slice) -> Self: ...
|
|
295
|
+
def __getitem__(self, value):
|
|
296
|
+
"""Select elements from this EEnv similar to list-Ops (slicing, int)."""
|
|
297
|
+
if isinstance(value, int):
|
|
298
|
+
if self.repetitions_ok:
|
|
299
|
+
value = value % len(self.energy_profiles)
|
|
300
|
+
return deepcopy(self.energy_profiles[value])
|
|
301
|
+
if isinstance(value, slice):
|
|
302
|
+
if self.repetitions_ok:
|
|
303
|
+
# bring values into range (out of bounds like -1, 300, ..)
|
|
304
|
+
log.warning("EEnv-Slice-Access with .repetition_ok==True is beta (funky behavior)")
|
|
305
|
+
val_start = value.start % self.PROFILES_MAX if value.start else value.start
|
|
306
|
+
val_stop: int = self.PROFILES_MAX
|
|
307
|
+
if value.stop:
|
|
308
|
+
if value.stop < 0:
|
|
309
|
+
val_stop = value.stop % self.PROFILES_MAX
|
|
310
|
+
else:
|
|
311
|
+
val_stop = min(value.stop, self.PROFILES_MAX)
|
|
312
|
+
|
|
313
|
+
if val_start and val_start > val_stop:
|
|
314
|
+
val_start = val_stop
|
|
315
|
+
else:
|
|
316
|
+
val_start = value.start
|
|
317
|
+
val_stop = value.stop
|
|
318
|
+
|
|
319
|
+
if self.repetitions_ok and val_stop > len(self.energy_profiles):
|
|
320
|
+
# scale profile-list up
|
|
321
|
+
scale = (val_stop // len(self.energy_profiles)) + 1
|
|
322
|
+
profiles = scale * self.energy_profiles
|
|
323
|
+
else:
|
|
324
|
+
profiles = self.energy_profiles
|
|
325
|
+
id_new = id_default()
|
|
326
|
+
slice_new = slice(val_start, val_stop, value.step)
|
|
327
|
+
data: dict[str, Any] = {
|
|
328
|
+
"id": id_new,
|
|
329
|
+
"created": local_now(),
|
|
330
|
+
"updated_last": local_now(),
|
|
331
|
+
"modifications": deepcopy(
|
|
332
|
+
[
|
|
333
|
+
*self.modifications,
|
|
334
|
+
f"EEnv '{self.name}' was sliced with {slice_new}, ID[{self.id}->{id_new}]",
|
|
335
|
+
]
|
|
336
|
+
),
|
|
337
|
+
"energy_profiles": deepcopy(profiles[slice_new]),
|
|
338
|
+
}
|
|
339
|
+
return self.model_copy(deep=True, update=data)
|
|
340
|
+
raise IndexError("Use int or slice when selecting from EEnv")
|
|
341
|
+
|
|
342
|
+
def export(self, output_path: Path) -> None:
|
|
343
|
+
"""Copy local data to new directory and add meta-data-file."""
|
|
344
|
+
if output_path.exists():
|
|
345
|
+
# TODO: elegant but unpractical, must be: empty dir or non-existing dir
|
|
346
|
+
msg = f"Warning: path {output_path} already exists"
|
|
347
|
+
raise FileExistsError(msg)
|
|
348
|
+
output_path.mkdir(parents=True)
|
|
349
|
+
|
|
350
|
+
# Copy data files & update meta-data
|
|
351
|
+
content = self.model_dump(exclude_unset=True, exclude_defaults=True)
|
|
352
|
+
for i_, profile in enumerate(self.energy_profiles):
|
|
353
|
+
# Numbered to avoid collisions. Preserve extensions
|
|
354
|
+
file_name = f"node{i_:03d}{profile.data_path.suffix}"
|
|
355
|
+
profile_new = profile.export(output_path / file_name)
|
|
356
|
+
content["energy_profiles"][i_] = profile_new.model_dump(
|
|
357
|
+
exclude_unset=True, exclude_defaults=True
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Create metadata file
|
|
361
|
+
with (output_path / "eenv.yaml").open("w") as file:
|
|
362
|
+
yaml.safe_dump(content, file, default_flow_style=False, sort_keys=False)
|
|
363
|
+
|
|
364
|
+
def exists(self) -> bool:
|
|
365
|
+
"""Check if embedded files exists."""
|
|
366
|
+
return all(profile.exists() for profile in self.energy_profiles)
|
|
367
|
+
|
|
368
|
+
def check(self) -> bool:
|
|
369
|
+
"""Check validity of embedded Energy-Profile."""
|
|
370
|
+
return all(profile.check() for profile in self.energy_profiles)
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
id: 1001
|
|
4
4
|
name: SolarSunny
|
|
5
5
|
description: MOCKUP! Sunny Day with 4 sq-cm Solar Cell
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
energy_profiles:
|
|
7
|
+
- data_path: eenv/group/user/solar_4h_new.h5
|
|
8
|
+
data_type: ivsample
|
|
9
|
+
duration: 14400
|
|
10
|
+
energy_Ws: 0.5
|
|
11
|
+
valid: true
|
|
12
|
+
repetitions_ok: true # just for tests - not OK for real recordings
|
|
12
13
|
owner: Ingmar
|
|
13
14
|
group: NES Lab
|
|
14
15
|
visible2group: true
|
|
@@ -21,12 +22,13 @@
|
|
|
21
22
|
id: 1002
|
|
22
23
|
name: ThermoelectricWashingMachine
|
|
23
24
|
description: MOCKUP! Energy harvested from side of machine during washing & tumbling
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
energy_profiles:
|
|
26
|
+
- data_path: eenv/group/user/thermoelectric_1h_wash.h5
|
|
27
|
+
data_type: ivcurve
|
|
28
|
+
duration: 3600
|
|
29
|
+
energy_Ws: 0.1
|
|
30
|
+
valid: true
|
|
31
|
+
repetitions_ok: true # just for tests - not OK for real recordings
|
|
30
32
|
owner: Ingmar
|
|
31
33
|
group: NES Lab
|
|
32
34
|
visible2group: true
|
|
@@ -39,12 +41,13 @@
|
|
|
39
41
|
id: 666
|
|
40
42
|
name: nuclear
|
|
41
43
|
description: MOCKUP! Energy harvested
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
energy_profiles:
|
|
45
|
+
- data_path: eenv/group/user/thermoelectric_1h_wash.h5
|
|
46
|
+
data_type: ivcurve
|
|
47
|
+
duration: 3600
|
|
48
|
+
energy_Ws: 0.1
|
|
49
|
+
valid: false # deliberate for testing
|
|
50
|
+
repetitions_ok: true # just for tests - not OK for real recordings
|
|
48
51
|
owner: Ingmar
|
|
49
52
|
group: NES Lab
|
|
50
53
|
visible2group: true
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Separated data-type.
|
|
2
|
+
|
|
3
|
+
Done due to cyclic inheritance.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EnergyDType(str, Enum):
|
|
10
|
+
"""Data-Type-Options for energy environments."""
|
|
11
|
+
|
|
12
|
+
ivtrace = ivsample = ivsamples = "ivsample"
|
|
13
|
+
ivsurface = ivcurve = ivcurves = "ivcurve"
|
|
14
|
+
isc_voc = "isc_voc"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FirmwareDType(str, Enum):
|
|
18
|
+
"""Options for firmware-types."""
|
|
19
|
+
|
|
20
|
+
base64_hex = "hex"
|
|
21
|
+
base64_elf = "elf"
|
|
22
|
+
path_hex = "path_hex"
|
|
23
|
+
path_elf = "path_elf"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HarvestAlgorithmDType(str, Enum):
|
|
27
|
+
"""Options for choosing a harvesting algorithm."""
|
|
28
|
+
|
|
29
|
+
direct = disable = neutral = "neutral"
|
|
30
|
+
"""
|
|
31
|
+
Reads an energy environment as is without selecting a harvesting
|
|
32
|
+
voltage.
|
|
33
|
+
|
|
34
|
+
Used to play "constant-power" energy environments or simple
|
|
35
|
+
"on-off-patterns". Generally, not useful for virtual source
|
|
36
|
+
emulation.
|
|
37
|
+
|
|
38
|
+
Not applicable to real harvesting, only emulation with IVTrace / samples.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
isc_voc = "isc_voc"
|
|
42
|
+
"""
|
|
43
|
+
Short Circuit Current, Open Circuit Voltage.
|
|
44
|
+
|
|
45
|
+
This is not relevant for emulation, but used to configure recording of
|
|
46
|
+
energy environments.
|
|
47
|
+
|
|
48
|
+
This mode samples the two extremes of an IV curve, which may be
|
|
49
|
+
interesting to characterize a transducer/energy environment.
|
|
50
|
+
|
|
51
|
+
Not applicable to emulation - only recordable during harvest-recording ATM.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
ivcurve = ivcurves = ivsurface = "ivcurve"
|
|
55
|
+
"""
|
|
56
|
+
Used during harvesting to record the full IV surface.
|
|
57
|
+
|
|
58
|
+
When configuring the energy environment recording, this algorithm
|
|
59
|
+
records the IV surface by repeatedly recording voltage and current
|
|
60
|
+
while ramping the voltage.
|
|
61
|
+
|
|
62
|
+
Cannot be used as output of emulation.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
constant = cv = "cv"
|
|
66
|
+
"""
|
|
67
|
+
Harvest energy at a fixed predefined voltage ('voltage_mV').
|
|
68
|
+
|
|
69
|
+
For harvesting, this records the IV samples at the specified voltage.
|
|
70
|
+
For emulation, this virtually harvests the IV surface at the specified voltage.
|
|
71
|
+
|
|
72
|
+
In addition to constant voltage harvesting, this can be used together
|
|
73
|
+
with the 'feedback_to_hrv' flag to implement a "Capacitor and Diode"
|
|
74
|
+
topology, where the harvesting voltage depends dynamically on the
|
|
75
|
+
capacitor voltage.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# ci .. constant current -> is this desired?
|
|
79
|
+
|
|
80
|
+
mppt_voc = "mppt_voc"
|
|
81
|
+
"""
|
|
82
|
+
Emulate a harvester with maximum power point (MPP) tracking based on
|
|
83
|
+
open circuit voltage measurements.
|
|
84
|
+
|
|
85
|
+
This MPPT heuristic estimates the MPP as a constant ratio of the open
|
|
86
|
+
circuit voltage.
|
|
87
|
+
|
|
88
|
+
Used in conjunction with 'setpoint_n', 'interval_ms', and 'duration_ms'.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
mppt_po = perturb_observe = "mppt_po"
|
|
92
|
+
"""
|
|
93
|
+
Emulate a harvester with perturb and observe maximum power point
|
|
94
|
+
tracking.
|
|
95
|
+
|
|
96
|
+
This MPPT heuristic adjusts the harvesting voltage by small amounts and
|
|
97
|
+
checks if the power increases. Eventually, the tracking changes the
|
|
98
|
+
direction of adjustments and oscillates around the MPP.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
mppt_opt = optimal = "mppt_opt"
|
|
102
|
+
"""
|
|
103
|
+
A theoretical harvester that identifies the MPP by reading it from the
|
|
104
|
+
IV curve during emulation.
|
|
105
|
+
|
|
106
|
+
Note that this is not possible for real-world harvesting as the system would
|
|
107
|
+
not know the entire IV curve. In that case a very fast and detailed mppt_po is
|
|
108
|
+
used.
|
|
109
|
+
"""
|