shepherd-core 2025.5.3__py3-none-any.whl → 2025.6.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 (37) hide show
  1. shepherd_core/commons.py +3 -5
  2. shepherd_core/config.py +34 -0
  3. shepherd_core/data_models/base/calibration.py +13 -8
  4. shepherd_core/data_models/base/wrapper.py +4 -4
  5. shepherd_core/data_models/content/energy_environment.py +1 -1
  6. shepherd_core/data_models/content/firmware.py +10 -5
  7. shepherd_core/data_models/content/virtual_harvester.py +10 -10
  8. shepherd_core/data_models/content/virtual_source.py +36 -28
  9. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
  10. shepherd_core/data_models/experiment/experiment.py +28 -18
  11. shepherd_core/data_models/experiment/observer_features.py +30 -11
  12. shepherd_core/data_models/experiment/target_config.py +17 -7
  13. shepherd_core/data_models/task/emulation.py +38 -25
  14. shepherd_core/data_models/task/firmware_mod.py +1 -1
  15. shepherd_core/data_models/task/harvest.py +14 -13
  16. shepherd_core/data_models/task/observer_tasks.py +4 -3
  17. shepherd_core/data_models/task/programming.py +2 -2
  18. shepherd_core/data_models/task/testbed_tasks.py +4 -7
  19. shepherd_core/data_models/testbed/cape_fixture.yaml +1 -1
  20. shepherd_core/data_models/testbed/observer.py +1 -1
  21. shepherd_core/data_models/testbed/observer_fixture.yaml +2 -2
  22. shepherd_core/data_models/testbed/target.py +1 -1
  23. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  24. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  25. shepherd_core/data_models/testbed/testbed.py +8 -9
  26. shepherd_core/fw_tools/patcher.py +7 -8
  27. shepherd_core/inventory/system.py +1 -3
  28. shepherd_core/reader.py +9 -2
  29. shepherd_core/testbed_client/cache_path.py +1 -1
  30. shepherd_core/testbed_client/client_web.py +2 -2
  31. shepherd_core/version.py +1 -1
  32. shepherd_core/writer.py +2 -2
  33. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/METADATA +12 -12
  34. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/RECORD +37 -36
  35. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/WHEEL +1 -1
  36. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/top_level.txt +0 -0
  37. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.1.dist-info}/zip-safe +0 -0
shepherd_core/commons.py CHANGED
@@ -1,8 +1,6 @@
1
1
  """Container for commonly shared constants."""
2
2
 
3
- SAMPLERATE_SPS_DEFAULT: int = 100_000
3
+ from .config import config
4
4
 
5
- UID_NAME: str = "SHEPHERD_NODE_ID"
6
- UID_SIZE: int = 2
7
-
8
- TESTBED_SERVER_URI: str = "http://127.0.0.1:8000/shepherd"
5
+ # TODO: deprecated - replace with config in subprojects and then remove here
6
+ SAMPLERATE_SPS_DEFAULT: int = config.SAMPLERATE_SPS
@@ -0,0 +1,34 @@
1
+ """Container for a common configuration.
2
+
3
+ This can be adapted by the user by importing 'config' and changing its variables.
4
+ """
5
+
6
+ from pydantic import BaseModel
7
+ from pydantic import HttpUrl
8
+
9
+
10
+ class ConfigDefault(BaseModel):
11
+ """Container for a common configuration."""
12
+
13
+ __slots__ = ()
14
+
15
+ TESTBED: str = "shepherd_tud_nes"
16
+ """name of the testbed to validate against - if enabled - see switch below"""
17
+ VALIDATE_INFRA: bool = True
18
+ """switch to turn on / off deep validation of data models also considering the current
19
+ layout & infrastructure of the testbed.
20
+ """
21
+
22
+ SAMPLERATE_SPS: int = 100_000
23
+ """Rate of IV-Recording of the testbed."""
24
+
25
+ UID_NAME: str = "SHEPHERD_NODE_ID"
26
+ """Variable name to patch in ELF-file"""
27
+ UID_SIZE: int = 2
28
+ """Variable size in Byte"""
29
+
30
+ TESTBED_SERVER: HttpUrl = "https://shepherd.cfaed.tu-dresden.de:8000/"
31
+ """Server that holds up to date testbed fixtures"""
32
+
33
+
34
+ config = ConfigDefault()
@@ -95,14 +95,19 @@ cal_hrv_legacy = { # legacy translator
95
95
  "adc_voltage": "adc_V_Sense", # datalog voltage
96
96
  }
97
97
 
98
+ # defaults (pre-init complex types)
99
+ cal_pair_dac_V = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
100
+ cal_pair_adc_V = CalibrationPair.from_fn(adc_voltage_to_raw, unit="V")
101
+ cal_pair_adc_C = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
102
+
98
103
 
99
104
  class CalibrationHarvester(ShpModel):
100
105
  """Container for all calibration-pairs for that device."""
101
106
 
102
- dac_V_Hrv: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
103
- dac_V_Sim: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
104
- adc_V_Sense: CalibrationPair = CalibrationPair.from_fn(adc_voltage_to_raw, unit="V")
105
- adc_C_Hrv: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
107
+ dac_V_Hrv: CalibrationPair = cal_pair_dac_V
108
+ dac_V_Sim: CalibrationPair = cal_pair_dac_V
109
+ adc_V_Sense: CalibrationPair = cal_pair_adc_V
110
+ adc_C_Hrv: CalibrationPair = cal_pair_adc_C
106
111
 
107
112
  def export_for_sysfs(self) -> dict:
108
113
  """Convert and write the essential data.
@@ -143,10 +148,10 @@ class CalibrationEmulator(ShpModel):
143
148
  Differentiates between both target-ports A/B.
144
149
  """
145
150
 
146
- dac_V_A: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
147
- dac_V_B: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
148
- adc_C_A: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
149
- adc_C_B: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
151
+ dac_V_A: CalibrationPair = cal_pair_dac_V
152
+ dac_V_B: CalibrationPair = cal_pair_dac_V
153
+ adc_C_A: CalibrationPair = cal_pair_adc_C
154
+ adc_C_B: CalibrationPair = cal_pair_adc_C
150
155
 
151
156
  def export_for_sysfs(self) -> dict:
152
157
  """Convert and write the essential data.
@@ -17,11 +17,11 @@ class Wrapper(BaseModel):
17
17
  """Generalized web- & file-interface for all models with dynamic typecasting."""
18
18
 
19
19
  datatype: str
20
- # ⤷ model-name
20
+ """ ⤷ model-name"""
21
21
  comment: Optional[SafeStrClone] = None
22
22
  created: Optional[datetime] = None
23
- # ⤷ Optional metadata
23
+ """ ⤷ Optional metadata"""
24
24
  lib_ver: Optional[str] = version
25
- # ⤷ for debug-purposes and later comp-checks
25
+ """ ⤷ for debug-purposes and later compatibility-checks"""
26
26
  parameters: dict
27
- # ⤷ ShpModel
27
+ """ ⤷ ShpModel"""
@@ -28,7 +28,7 @@ class EnergyEnvironment(ContentModel):
28
28
  data_path: Path
29
29
  data_type: EnergyDType
30
30
  data_local: bool = True
31
- # ⤷ signals that file has to be copied to testbed
31
+ """ ⤷ signals that file has to be copied to testbed"""
32
32
 
33
33
  duration: PositiveFloat
34
34
  energy_Ws: PositiveFloat
@@ -62,7 +62,7 @@ class Firmware(ContentModel, title="Firmware of Target"):
62
62
  data_type: FirmwareDType
63
63
  data_hash: Optional[str] = None
64
64
  data_local: bool = True
65
- # ⤷ signals that file has to be copied to testbed
65
+ """ ⤷ signals that file has to be copied to testbed"""
66
66
 
67
67
  @model_validator(mode="before")
68
68
  @classmethod
@@ -125,12 +125,17 @@ class Firmware(ContentModel, title="Firmware of Target"):
125
125
  if "mcu" not in kwargs:
126
126
  kwargs["mcu"] = arch_to_mcu[arch]
127
127
 
128
+ # verification of ELF - warn if something is off
129
+ # -> adds ARCH if it is able to derive
128
130
  if kwargs["data_type"] == FirmwareDType.base64_elf:
129
131
  arch = fw_tools.read_arch(file)
130
- if "msp430" in arch and not fw_tools.is_elf_msp430(file):
131
- raise ValueError("File is not a ELF for msp430")
132
- if ("nrf52" in arch or "arm" in arch) and not fw_tools.is_elf_nrf52(file):
133
- raise ValueError("File is not a ELF for nRF52")
132
+ try:
133
+ if "msp430" in arch and not fw_tools.is_elf_msp430(file):
134
+ raise ValueError("File is not a ELF for msp430")
135
+ if ("nrf52" in arch or "arm" in arch) and not fw_tools.is_elf_nrf52(file):
136
+ raise ValueError("File is not a ELF for nRF52")
137
+ except RuntimeError:
138
+ logger.warning("ObjCopy not found -> Arch of Firmware can't be verified")
134
139
  logger.debug("ELF-File '%s' has arch: %s", file.name, arch)
135
140
  if "mcu" not in kwargs:
136
141
  kwargs["mcu"] = arch_to_mcu[arch]
@@ -10,7 +10,7 @@ from pydantic import Field
10
10
  from pydantic import model_validator
11
11
  from typing_extensions import Self
12
12
 
13
- from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
13
+ from shepherd_core.config import config
14
14
  from shepherd_core.data_models.base.calibration import CalibrationHarvester
15
15
  from shepherd_core.data_models.base.content import ContentModel
16
16
  from shepherd_core.data_models.base.shepherd import ShpModel
@@ -363,9 +363,9 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
363
363
  def calc_timings_ms(self, *, for_emu: bool) -> tuple[float, float]:
364
364
  """factor-in model-internal timing-constraints."""
365
365
  window_length = self.samples_n * (1 + self.wait_cycles)
366
- time_min_ms = (1 + self.wait_cycles) * 1_000 / SAMPLERATE_SPS_DEFAULT
366
+ time_min_ms = (1 + self.wait_cycles) * 1_000 / config.SAMPLERATE_SPS
367
367
  if for_emu:
368
- window_ms = window_length * 1_000 / SAMPLERATE_SPS_DEFAULT
368
+ window_ms = window_length * 1_000 / config.SAMPLERATE_SPS
369
369
  time_min_ms = max(time_min_ms, window_ms)
370
370
 
371
371
  interval_ms = min(max(self.interval_ms, time_min_ms), 1_000_000)
@@ -453,16 +453,16 @@ class HarvesterPRUConfig(ShpModel):
453
453
  voltage_min_uV: u32
454
454
  voltage_max_uV: u32
455
455
  voltage_step_uV: u32
456
- # ⤷ for window-based algo like ivcurve
456
+ """ ⤷ for window-based algo like ivcurve"""
457
457
  current_limit_nA: u32
458
- # ⤷ lower bound to detect zero current
458
+ """ ⤷ lower bound to detect zero current"""
459
459
  setpoint_n8: u32
460
460
  interval_n: u32
461
- # ⤷ between measurements
461
+ """ ⤷ between measurements"""
462
462
  duration_n: u32
463
- # ⤷ of measurement
463
+ """ ⤷ of measurement"""
464
464
  wait_cycles_n: u32
465
- # ⤷ for DAC to settle
465
+ """ ⤷ for DAC to settle"""
466
466
 
467
467
  @classmethod
468
468
  def from_vhrv(
@@ -517,7 +517,7 @@ class HarvesterPRUConfig(ShpModel):
517
517
  voltage_step_uV=round(voltage_step_mV * 10**3),
518
518
  current_limit_nA=round(data.current_limit_uA * 10**3),
519
519
  setpoint_n8=round(min(255, data.setpoint_n * 2**8)),
520
- interval_n=round(interval_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
521
- duration_n=round(duration_ms * SAMPLERATE_SPS_DEFAULT * 1e-3),
520
+ interval_n=round(interval_ms * config.SAMPLERATE_SPS * 1e-3),
521
+ duration_n=round(duration_ms * config.SAMPLERATE_SPS * 1e-3),
522
522
  wait_cycles_n=data.wait_cycles,
523
523
  )
@@ -7,7 +7,7 @@ from pydantic import Field
7
7
  from pydantic import model_validator
8
8
  from typing_extensions import Self
9
9
 
10
- from shepherd_core.commons import SAMPLERATE_SPS_DEFAULT
10
+ from shepherd_core.config import config
11
11
  from shepherd_core.data_models.base.content import ContentModel
12
12
  from shepherd_core.data_models.base.shepherd import ShpModel
13
13
  from shepherd_core.logger import logger
@@ -23,6 +23,9 @@ NormedNum = Annotated[float, Field(ge=0.0, le=1.0)]
23
23
  LUT1D = Annotated[list[NormedNum], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
24
24
  LUT2D = Annotated[list[LUT1D], Field(min_length=LUT_SIZE, max_length=LUT_SIZE)]
25
25
 
26
+ # defaults (pre-init complex types for improved perf) TODO: is documentation still fine?
27
+ vhrv_mppt_opt = VirtualHarvesterConfig(name="mppt_opt")
28
+
26
29
 
27
30
  class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
28
31
  """The vSrc uses the energy environment (file) for supplying the Target Node.
@@ -41,46 +44,48 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
41
44
  # General Metadata & Ownership -> ContentModel
42
45
 
43
46
  enable_boost: bool = False
44
- # ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
47
+ """ ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
45
48
  enable_buck: bool = False
46
- # ⤷ if false -> v_output = v_intermediate
49
+ """ ⤷ if false -> v_output = v_intermediate"""
47
50
  enable_feedback_to_hrv: bool = False
48
- # src can control a cv-harvester for ivcurve
51
+ """ src can control a cv-harvester for ivcurve"""
49
52
 
50
53
  interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
51
54
 
52
- harvester: VirtualHarvesterConfig = VirtualHarvesterConfig(name="mppt_opt")
55
+ harvester: VirtualHarvesterConfig = vhrv_mppt_opt
53
56
 
54
57
  V_input_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
55
58
  I_input_max_mA: Annotated[float, Field(ge=0, le=4.29e3)] = 4_200
56
59
  V_input_drop_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 0
57
- # ⤷ simulate input-diode
60
+ """ ⤷ simulate input-diode"""
58
61
  R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
59
- # ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]
62
+ """ ⤷ resistance only active with disabled boost, range [1 mOhm; 1MOhm]"""
60
63
 
61
64
  # primary storage-Cap
62
65
  C_intermediate_uF: Annotated[float, Field(ge=0, le=100_000)] = 0
63
66
  V_intermediate_init_mV: Annotated[float, Field(ge=0, le=10_000)] = 3_000
64
- # ⤷ allow a proper / fast startup
67
+ """ ⤷ allow a proper / fast startup"""
65
68
  I_intermediate_leak_nA: Annotated[float, Field(ge=0, le=4.29e9)] = 0
66
69
 
67
70
  V_intermediate_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 1
68
- # ⤷ target gets connected (hysteresis-combo with next value)
71
+ """ ⤷ target gets connected (hysteresis-combo with next value)"""
69
72
  V_intermediate_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
70
- # ⤷ target gets disconnected
73
+ """ ⤷ target gets disconnected"""
71
74
  interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
72
- # ⤷ some ICs (BQ) check every 64 ms if output should be disconnected
75
+ """ ⤷ some ICs (BQ) check every 64 ms if output should be disconnected"""
73
76
  # TODO: add intervals for input-disable, output-disable & power-good-signal
74
77
 
75
78
  # pwr-good: target is informed on output-pin (hysteresis) -> for intermediate voltage
76
79
  V_pwr_good_enable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2_800
77
80
  V_pwr_good_disable_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 2200
78
81
  immediate_pwr_good_signal: bool = True
79
- # ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
82
+ """ ⤷ 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds"""
80
83
 
81
- # final (always last) stage to compensate undetectable current spikes
82
- # when enabling power for target
83
84
  C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
85
+ """
86
+ final (always last) stage to compensate undetectable current spikes when
87
+ enabling power for target
88
+ """
84
89
  # TODO: C_output is handled internally as delta-V, but should be a I_transient
85
90
  # that makes it visible in simulation as additional i_out_drain
86
91
  # TODO: potential weakness, ACD lowpass is capturing transient,
@@ -88,32 +93,35 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
88
93
 
89
94
  # Extra
90
95
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
91
- # ⤷ min voltage needed to enable recording changes in gpio-bank
96
+ """ ⤷ min voltage needed to enable recording changes in gpio-bank"""
92
97
 
93
98
  # Boost Converter
94
99
  V_input_boost_threshold_mV: Annotated[float, Field(ge=0, le=10_000)] = 0
95
- # ⤷ min input-voltage for the boost converter to work
100
+ """ ⤷ min input-voltage for the boost converter to work"""
96
101
  V_intermediate_max_mV: Annotated[float, Field(ge=0, le=10_000)] = 10_000
97
- # ⤷ boost converter shuts off
102
+ """ ⤷ boost converter shuts off"""
98
103
 
99
104
  LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
100
- # ⤷ rows are current -> first row a[V=0][:]
101
- # input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
102
- # influence of cap-voltage is not implemented
105
+ """ ⤷ rows are current -> first row a[V=0][:]
106
+
107
+ input-LUT[12][12] depending on array[inp_voltage][log(inp_current)],
108
+ influence of cap-voltage is not implemented
109
+ """
110
+
103
111
  LUT_input_V_min_log2_uV: Annotated[int, Field(ge=0, le=20)] = 0
104
- # ⤷ 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV
112
+ """i.e. 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV"""
105
113
  LUT_input_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
106
- # ⤷ 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA
114
+ """i.e. 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA"""
107
115
 
108
116
  # Buck Converter
109
117
  V_output_mV: Annotated[float, Field(ge=0, le=5_000)] = 2_400
110
118
  V_buck_drop_mV: Annotated[float, Field(ge=0, le=5_000)] = 0
111
- # ⤷ simulate LDO / diode min voltage differential or output-diode
119
+ """ ⤷ simulate LDO / diode min voltage differential or output-diode"""
112
120
 
113
121
  LUT_output_efficiency: LUT1D = 12 * [1.00]
114
- # ⤷ array[12] depending on output_current
122
+ """ ⤷ array[12] depending on output_current"""
115
123
  LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
116
- # ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation
124
+ """ ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation"""
117
125
 
118
126
  @model_validator(mode="before")
119
127
  @classmethod
@@ -242,7 +250,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
242
250
  dV[uV] = constant[us/nF] * current[nA] = constant[us*V/nAs] * current[nA]
243
251
  """
244
252
  C_cap_uF = max(self.C_intermediate_uF, 0.001)
245
- return int((10**3 * (2**28)) // (C_cap_uF * SAMPLERATE_SPS_DEFAULT))
253
+ return int((10**3 * (2**28)) // (C_cap_uF * config.SAMPLERATE_SPS))
246
254
 
247
255
 
248
256
  u32 = Annotated[int, Field(ge=0, lt=2**32)]
@@ -317,7 +325,7 @@ class ConverterPRUConfig(ShpModel):
317
325
  dtype_in, log_intermediate_node=log_intermediate_node
318
326
  ),
319
327
  interval_startup_delay_drain_n=round(
320
- data.interval_startup_delay_drain_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
328
+ data.interval_startup_delay_drain_ms * config.SAMPLERATE_SPS * 1e-3
321
329
  ),
322
330
  V_input_max_uV=round(data.V_input_max_mV * 1e3),
323
331
  I_input_max_nA=round(data.I_input_max_mA * 1e6),
@@ -330,7 +338,7 @@ class ConverterPRUConfig(ShpModel):
330
338
  V_disable_output_threshold_uV=round(states["V_disable_output_threshold_mV"] * 1e3),
331
339
  dV_enable_output_uV=round(states["dV_enable_output_mV"] * 1e3),
332
340
  interval_check_thresholds_n=round(
333
- data.interval_check_thresholds_ms * SAMPLERATE_SPS_DEFAULT * 1e-3
341
+ data.interval_check_thresholds_ms * config.SAMPLERATE_SPS * 1e-3
334
342
  ),
335
343
  V_pwr_good_enable_threshold_uV=round(data.V_pwr_good_enable_threshold_mV * 1e3),
336
344
  V_pwr_good_disable_threshold_uV=round(data.V_pwr_good_disable_threshold_mV * 1e3),
@@ -1,6 +1,6 @@
1
1
  # info:
2
2
  # - compendium of all parameters & description
3
- # - base for neutral fallback values if provided yml is sparse
3
+ # - base for neutral fallback values if provided yaml is sparse
4
4
  # - -> it is encouraged to omit redundant parameters
5
5
  ---
6
6
  - datatype: VirtualSourceConfig
@@ -11,11 +11,12 @@ from pydantic import model_validator
11
11
  from typing_extensions import Self
12
12
  from typing_extensions import deprecated
13
13
 
14
+ from shepherd_core.config import config
14
15
  from shepherd_core.data_models.base.content import IdInt
15
16
  from shepherd_core.data_models.base.content import NameStr
16
17
  from shepherd_core.data_models.base.content import SafeStr
17
- from shepherd_core.data_models.base.content import id_default
18
18
  from shepherd_core.data_models.base.shepherd import ShpModel
19
+ from shepherd_core.data_models.base.timezone import local_now
19
20
  from shepherd_core.data_models.testbed.target import Target
20
21
  from shepherd_core.data_models.testbed.testbed import Testbed
21
22
  from shepherd_core.version import version
@@ -23,33 +24,28 @@ from shepherd_core.version import version
23
24
  from .observer_features import SystemLogging
24
25
  from .target_config import TargetConfig
25
26
 
27
+ # defaults (pre-init complex types)
28
+ sys_log_all = SystemLogging() # = all active
29
+
26
30
 
27
31
  class Experiment(ShpModel, title="Config of an Experiment"):
28
32
  """Config for experiments on the testbed emulating energy environments for target nodes."""
29
33
 
30
34
  # General Properties
31
- id: int = Field(description="Unique ID", default_factory=id_default)
32
- # ⤷ TODO: automatic ID is problematic for identification by hash
33
-
34
35
  name: NameStr
35
36
  description: Annotated[
36
37
  Optional[SafeStr], Field(description="Required for public instances")
37
38
  ] = None
38
39
  comment: Optional[SafeStr] = None
39
- created: datetime = Field(default_factory=datetime.now)
40
-
41
- # Ownership & Access
42
- owner_id: Optional[IdInt] = None
43
40
 
44
41
  # feedback
45
- email_results: bool = False
42
+ email_results: bool = True
46
43
 
47
- sys_logging: SystemLogging = SystemLogging() # = all active
44
+ sys_logging: SystemLogging = sys_log_all
48
45
 
49
46
  # schedule
50
47
  time_start: Optional[datetime] = None # = ASAP
51
48
  duration: Optional[timedelta] = None # = till EOF
52
- abort_on_error: Annotated[bool, deprecated("has no effect")] = False
53
49
 
54
50
  # targets
55
51
  target_configs: Annotated[list[TargetConfig], Field(min_length=1, max_length=128)]
@@ -57,13 +53,16 @@ class Experiment(ShpModel, title="Config of an Experiment"):
57
53
  # debug
58
54
  lib_ver: Optional[str] = version
59
55
 
56
+ # deprecated fields, TODO: remove before public release
57
+ id: Annotated[Optional[int], deprecated("not needed")] = None
58
+ created: Annotated[Optional[datetime], deprecated("not needed")] = None
59
+ abort_on_error: Annotated[bool, deprecated("has no effect")] = False
60
+ owner_id: Annotated[Optional[IdInt], deprecated("not needed")] = None
61
+
60
62
  @model_validator(mode="after")
61
63
  def post_validation(self) -> Self:
62
- # TODO: only do deep validation with active connection to TB-client
63
- # or with cached fixtures
64
- testbed = Testbed() # this will query the first (and only) entry of client
64
+ self._validate_observers(self.target_configs)
65
65
  self._validate_targets(self.target_configs)
66
- self._validate_observers(self.target_configs, testbed)
67
66
  if self.duration and self.duration.total_seconds() < 0:
68
67
  raise ValueError("Duration of experiment can't be negative.")
69
68
  return self
@@ -75,8 +74,9 @@ class Experiment(ShpModel, title="Config of an Experiment"):
75
74
  for _config in configs:
76
75
  for _id in _config.target_IDs:
77
76
  target_ids.append(_id)
78
- Target(id=_id)
79
- # ⤷ this can raise exception for non-existing targets
77
+ if config.VALIDATE_INFRA:
78
+ Target(id=_id)
79
+ # ⤷ this can raise exception for non-existing targets
80
80
  if _config.custom_IDs is not None:
81
81
  custom_ids = custom_ids + _config.custom_IDs[: len(_config.target_IDs)]
82
82
  else:
@@ -87,7 +87,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
87
87
  raise ValueError("Custom Target-ID are faulty (some form of id-collisions)!")
88
88
 
89
89
  @staticmethod
90
- def _validate_observers(configs: Iterable[TargetConfig], testbed: Testbed) -> None:
90
+ def _validate_observers(configs: Iterable[TargetConfig]) -> None:
91
+ if not config.VALIDATE_INFRA:
92
+ return
93
+ testbed = Testbed()
91
94
  target_ids = [_id for _config in configs for _id in _config.target_IDs]
92
95
  obs_ids = [testbed.get_observer(_id).id for _id in target_ids]
93
96
  if len(target_ids) > len(set(obs_ids)):
@@ -105,3 +108,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
105
108
  # gets already caught in target_config - but keep:
106
109
  msg = f"Target-ID {target_id} was not found in Experiment '{self.name}'"
107
110
  raise ValueError(msg)
111
+
112
+ def folder_name(self, custom_date: Optional[datetime] = None) -> str:
113
+ date = custom_date if custom_date is not None else self.time_start
114
+ timestamp = local_now() if date is None else date
115
+ timestrng = timestamp.strftime("%Y-%m-%d_%H-%M-%S")
116
+ # ⤷ closest to ISO 8601, avoids ":"
117
+ return f"{timestrng}_{self.name.replace(' ', '_')}"
@@ -17,6 +17,9 @@ from shepherd_core import logger
17
17
  from shepherd_core.data_models.base.shepherd import ShpModel
18
18
  from shepherd_core.data_models.testbed.gpio import GPIO
19
19
 
20
+ # defaults (pre-init complex types)
21
+ zero_duration = timedelta(seconds=0)
22
+
20
23
 
21
24
  class PowerTracing(ShpModel, title="Config for Power-Tracing"):
22
25
  """Configuration for recording the Power-Consumption of the Target Nodes.
@@ -25,19 +28,27 @@ class PowerTracing(ShpModel, title="Config for Power-Tracing"):
25
28
  """
26
29
 
27
30
  intermediate_voltage: bool = False
28
- # ⤷ for EMU: record storage capacitor instead of output (good for V_out = const)
29
- # this also includes current!
30
-
31
+ """
32
+ for EMU: record storage capacitor instead of output (good for V_out = const)
33
+ this also includes current!
34
+ """
31
35
  # time
32
- delay: timedelta = timedelta(seconds=0)
36
+ delay: timedelta = zero_duration
37
+ """start recording after experiment started"""
33
38
  duration: Optional[timedelta] = None # till EOF
39
+ """duration of recording after delay starts the process.
40
+
41
+ default is None, recording till EOF"""
34
42
 
35
43
  # post-processing
36
44
  calculate_power: bool = False
37
- samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000 # down-sample
45
+ """ reduce file-size by calculating power -> not implemented ATM"""
46
+ samplerate: Annotated[int, Field(ge=10, le=100_000)] = 100_000
47
+ """ ⤷ reduce file-size by down-sampling -> not implemented ATM"""
38
48
  discard_current: bool = False
49
+ """ ⤷ reduce file-size by omitting current -> not implemented ATM"""
39
50
  discard_voltage: bool = False
40
- # ⤷ reduce file-size by omitting current / voltage
51
+ """ ⤷ reduce file-size by omitting voltage -> not implemented ATM"""
41
52
 
42
53
  @model_validator(mode="after")
43
54
  def post_validation(self) -> Self:
@@ -163,11 +174,12 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
163
174
  """
164
175
 
165
176
  # time
166
- delay: timedelta = timedelta(seconds=0)
177
+ delay: timedelta = zero_duration
167
178
  duration: Optional[timedelta] = None # till EOF
168
179
 
169
180
  # post-processing,
170
181
  uart_decode: bool = False
182
+ """Automatic decoding from gpio-trace not implemented ATM."""
171
183
  uart_pin: GPIO = GPIO(name="GPIO8")
172
184
  uart_baudrate: Annotated[int, Field(ge=2_400, le=1_152_000)] = 115_200
173
185
 
@@ -205,12 +217,14 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
205
217
  """Configuration for a single GPIO-Event (Actuation)."""
206
218
 
207
219
  delay: PositiveFloat
208
- # ⤷ from start_time
209
- # ⤷ resolution 10 us (guaranteed, but finer steps are possible)
220
+ """ ⤷ from start_time
221
+
222
+ - resolution 10 us (guaranteed, but finer steps are possible)
223
+ """
210
224
  gpio: GPIO
211
225
  level: GpioLevel
212
226
  period: Annotated[float, Field(ge=10e-6)] = 1
213
- # ⤷ time base of periodicity in s
227
+ """ ⤷ time base of periodicity in s"""
214
228
  count: Annotated[int, Field(ge=1, le=4096)] = 1
215
229
 
216
230
  @model_validator(mode="after")
@@ -233,6 +247,11 @@ class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
233
247
 
234
248
  events: Annotated[list[GpioEvent], Field(min_length=1, max_length=1024)]
235
249
 
250
+ @model_validator(mode="after")
251
+ def post_validation(self) -> Self:
252
+ msg = "not implemented ATM"
253
+ raise ValueError(msg)
254
+
236
255
  def get_gpios(self) -> set:
237
256
  return {_ev.gpio for _ev in self.events}
238
257
 
@@ -245,7 +264,7 @@ class SystemLogging(ShpModel, title="Config for System-Logging"):
245
264
  sheep: bool = True
246
265
  sys_util: bool = True
247
266
 
248
- # TODO: remove lines below in 2026
267
+ # deprecated, TODO: remove lines below before public release
249
268
  dmesg: Annotated[bool, deprecated("for sheep v0.9.0+, use 'kernel' instead")] = True
250
269
  ptp: Annotated[bool, deprecated("for sheep v0.9.0+, use 'time_sync' instead")] = True
251
270
  shepherd: Annotated[bool, deprecated("for sheep v0.9.0+, use 'sheep' instead")] = True
@@ -20,26 +20,36 @@ from .observer_features import GpioTracing
20
20
  from .observer_features import PowerTracing
21
21
  from .observer_features import UartLogging
22
22
 
23
+ # defaults (pre-init complex types)
24
+ vsrc_neutral = VirtualSourceConfig(name="neutral")
25
+
23
26
 
24
27
  class TargetConfig(ShpModel, title="Target Config"):
25
28
  """Configuration related to Target Nodes (DuT)."""
26
29
 
27
30
  target_IDs: Annotated[list[IdInt], Field(min_length=1, max_length=128)]
28
31
  custom_IDs: Optional[Annotated[list[IdInt16], Field(min_length=1, max_length=128)]] = None
29
- # ⤷ will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware
30
- # if no custom ID is provided, the original ID of target is used
32
+ """custom ID will replace 'const uint16_t SHEPHERD_NODE_ID' in firmware.
33
+
34
+ if no custom ID is provided, the original ID of target is used
35
+ """
31
36
 
32
- energy_env: EnergyEnvironment # alias: input
33
- virtual_source: VirtualSourceConfig = VirtualSourceConfig(name="neutral")
37
+ energy_env: EnergyEnvironment
38
+ """ input for the virtual source """
39
+ virtual_source: VirtualSourceConfig = vsrc_neutral
34
40
  target_delays: Optional[
35
41
  Annotated[list[Annotated[int, Field(ge=0)]], Field(min_length=1, max_length=128)]
36
42
  ] = None
37
- # ⤷ individual starting times -> allows to use the same environment
38
- # TODO: delays not used ATM
43
+ """ ⤷ individual starting times
44
+
45
+ - allows to use the same environment
46
+ - not implemented ATM
47
+ """
39
48
 
40
49
  firmware1: Firmware
50
+ """ ⤷ omitted FW gets set to neutral deep-sleep"""
41
51
  firmware2: Optional[Firmware] = None
42
- # ⤷ omitted FW gets set to neutral deep-sleep
52
+ """ ⤷ omitted FW gets set to neutral deep-sleep"""
43
53
 
44
54
  power_tracing: Optional[PowerTracing] = None
45
55
  gpio_tracing: Optional[GpioTracing] = None