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
@@ -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 .firmware_datatype import FirmwareDType
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
- data_local: bool = True
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
- _type = values.get("data_type")
71
- if _type in {
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
- _hash = fw_tools.base64_to_hash(values.get("data"))
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 _hash != values.get("data_hash"):
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 _type in {
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["data_local"] = False
110
+ kwargs["data_2_copy"] = False
109
111
  else:
110
112
  kwargs["data"] = Path(file).as_posix()
111
- kwargs["data_local"] = True
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, path: Path | None = None) -> bool:
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 path is not None and path.is_file():
153
- hash_new = fw_tools.file_to_hash(path)
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
- else:
156
- hash_new = fw_tools.base64_to_hash(self.data)
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 = file / self.name
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 .energy_environment import EnergyDType
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: AlgorithmDType
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
- _ratio = (duration_ms / interval_ms) / (self.duration_ms / self.interval_ms)
373
- if (_ratio - 1) > 0.1:
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
- _ratio,
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 .energy_environment import EnergyDType
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 if SoC_init else cls.model_fields["SoC_init"].default,
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 if SoC_init else cls.model_fields["SoC_init"].default,
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 if SoC_init else cls.model_fields["SoC_init"].default,
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 if R_leak_Ohm else cls.model_fields["R_leak_Ohm"].default,
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=_v, V_rated=10.0, description=dsc_ideal) for _v in E6
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"XP {cfg1.name}, self-discharge, "
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"XP {cfg1.name}, self-discharge, "
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"XP Tantal AVX, self-discharge {duration.total_seconds()} s")
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"XP MLCC Tayo, self-discharge {duration.total_seconds()} s")
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 _config in configs:
66
- for _id in _config.target_IDs:
67
- target_ids.append(_id)
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=_id)
77
+ Target(id=id_)
70
78
  # ⤷ this can raise exception for non-existing targets
71
- if _config.custom_IDs is not None:
72
- custom_ids = custom_ids + _config.custom_IDs[: len(_config.target_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 = custom_ids + _config.target_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 = [_id for _config in configs for _id in _config.target_IDs]
86
- obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
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 [_id for _config in self.target_configs for _id in _config.target_IDs]
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 _config in self.target_configs:
97
- if target_id in _config.target_IDs:
98
- return _config
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 storage capacitor instead of output (good for V_out = const)
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 - power @ 100 kHz is not recommended
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 {_ev.gpio for _ev in self.events}
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