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.
Files changed (69) hide show
  1. shepherd_core/config.py +1 -1
  2. shepherd_core/data_models/__init__.py +4 -2
  3. shepherd_core/data_models/base/cal_measurement.py +7 -2
  4. shepherd_core/data_models/base/calibration.py +23 -12
  5. shepherd_core/data_models/base/content.py +10 -2
  6. shepherd_core/data_models/base/shepherd.py +13 -4
  7. shepherd_core/data_models/base/wrapper.py +2 -0
  8. shepherd_core/data_models/content/__init__.py +4 -2
  9. shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
  10. shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
  11. shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
  12. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
  13. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
  14. shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
  15. shepherd_core/data_models/content/energy_environment.py +341 -23
  16. shepherd_core/data_models/content/energy_environment_fixture.yaml +21 -18
  17. shepherd_core/data_models/content/enum_datatypes.py +109 -0
  18. shepherd_core/data_models/content/firmware.py +44 -16
  19. shepherd_core/data_models/content/virtual_harvester_config.py +10 -93
  20. shepherd_core/data_models/content/virtual_source_config.py +21 -2
  21. shepherd_core/data_models/content/virtual_storage_config.py +7 -4
  22. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +1 -1
  23. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +4 -4
  24. shepherd_core/data_models/experiment/experiment.py +38 -13
  25. shepherd_core/data_models/experiment/observer_features.py +17 -4
  26. shepherd_core/data_models/experiment/target_config.py +55 -7
  27. shepherd_core/data_models/task/__init__.py +13 -2
  28. shepherd_core/data_models/task/emulation.py +9 -5
  29. shepherd_core/data_models/task/firmware_mod.py +3 -1
  30. shepherd_core/data_models/task/harvest.py +2 -0
  31. shepherd_core/data_models/task/helper_paths.py +2 -2
  32. shepherd_core/data_models/task/observer_tasks.py +8 -6
  33. shepherd_core/data_models/task/programming.py +4 -2
  34. shepherd_core/data_models/task/testbed_tasks.py +8 -2
  35. shepherd_core/data_models/testbed/cape.py +2 -0
  36. shepherd_core/data_models/testbed/gpio.py +2 -0
  37. shepherd_core/data_models/testbed/mcu.py +2 -0
  38. shepherd_core/data_models/testbed/observer.py +2 -0
  39. shepherd_core/data_models/testbed/target.py +7 -5
  40. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  41. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  42. shepherd_core/data_models/testbed/testbed.py +17 -15
  43. shepherd_core/exit_handler.py +22 -0
  44. shepherd_core/fw_tools/converter.py +2 -2
  45. shepherd_core/fw_tools/validation.py +1 -1
  46. shepherd_core/inventory/__init__.py +23 -21
  47. shepherd_core/inventory/system.py +2 -2
  48. shepherd_core/logger.py +0 -1
  49. shepherd_core/reader.py +29 -25
  50. shepherd_core/testbed_client/cache_path.py +3 -3
  51. shepherd_core/testbed_client/client_abc_fix.py +14 -3
  52. shepherd_core/testbed_client/client_web.py +7 -5
  53. shepherd_core/testbed_client/fixtures.py +7 -7
  54. shepherd_core/version.py +1 -1
  55. shepherd_core/vsource/virtual_converter_model.py +2 -2
  56. shepherd_core/vsource/virtual_harvester_model.py +2 -2
  57. shepherd_core/vsource/virtual_harvester_simulation.py +5 -5
  58. shepherd_core/vsource/virtual_source_model.py +1 -1
  59. shepherd_core/vsource/virtual_source_simulation.py +9 -9
  60. shepherd_core/vsource/virtual_storage_models_kibam.py +3 -3
  61. shepherd_core/writer.py +16 -9
  62. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/METADATA +5 -3
  63. shepherd_core-2026.2.1.dist-info/RECORD +102 -0
  64. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/WHEEL +1 -1
  65. shepherd_core-2026.2.1.dist-info/licenses/LICENSE +21 -0
  66. shepherd_core/data_models/content/firmware_datatype.py +0 -15
  67. shepherd_core-2025.10.1.dist-info/RECORD +0 -95
  68. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/top_level.txt +0 -0
  69. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.1.dist-info}/zip-safe +0 -0
@@ -1,52 +1,370 @@
1
- """Data-model for recorded eEnvs."""
1
+ """Data-model for recorded Energy-Environments (EEnvs).
2
2
 
3
- from enum import Enum
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
- ivtrace = ivsample = ivsamples = "ivsample"
18
- ivsurface = ivcurve = ivcurves = "ivcurve"
19
- isc_voc = "isc_voc"
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
- data_local: bool = True
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: PositiveFloat
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
- # additional descriptive metadata, TODO: these are very solar-centered -> generalize
42
- light_source: str | None = None
43
- weather_conditions: str | None = None
44
- indoor: bool | None = None
45
- location: str | None = None
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
- data_path: eenv/group/user/solar_4h_new.h5
7
- data_type: ivsample
8
- duration: 14400
9
- energy_Ws: 0.5
10
- valid: true
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
- data_path: eenv/group/user/thermoelectric_1h_wash.h5
25
- data_type: ivcurve
26
- duration: 3600
27
- energy_Ws: 0.1
28
- valid: true
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
- data_path: eenv/group/user/thermoelectric_1h_wash.h5
43
- data_type: ivcurve
44
- duration: 3600
45
- energy_Ws: 0.1
46
- valid: false
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
+ """