shepherd-core 2025.5.3__py3-none-any.whl → 2025.6.2__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 (49) hide show
  1. shepherd_core/__init__.py +2 -2
  2. shepherd_core/commons.py +3 -5
  3. shepherd_core/config.py +34 -0
  4. shepherd_core/data_models/__init__.py +1 -1
  5. shepherd_core/data_models/base/calibration.py +13 -8
  6. shepherd_core/data_models/base/shepherd.py +28 -11
  7. shepherd_core/data_models/base/wrapper.py +4 -4
  8. shepherd_core/data_models/content/energy_environment.py +1 -1
  9. shepherd_core/data_models/content/firmware.py +13 -8
  10. shepherd_core/data_models/content/virtual_harvester.py +13 -13
  11. shepherd_core/data_models/content/virtual_source.py +41 -33
  12. shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
  13. shepherd_core/data_models/experiment/experiment.py +28 -18
  14. shepherd_core/data_models/experiment/observer_features.py +32 -13
  15. shepherd_core/data_models/experiment/target_config.py +17 -7
  16. shepherd_core/data_models/task/__init__.py +8 -4
  17. shepherd_core/data_models/task/emulation.py +52 -30
  18. shepherd_core/data_models/task/firmware_mod.py +15 -6
  19. shepherd_core/data_models/task/harvest.py +19 -13
  20. shepherd_core/data_models/task/helper_paths.py +15 -0
  21. shepherd_core/data_models/task/observer_tasks.py +20 -18
  22. shepherd_core/data_models/task/programming.py +10 -4
  23. shepherd_core/data_models/task/testbed_tasks.py +16 -7
  24. shepherd_core/data_models/testbed/cape_fixture.yaml +1 -1
  25. shepherd_core/data_models/testbed/observer.py +1 -1
  26. shepherd_core/data_models/testbed/observer_fixture.yaml +2 -2
  27. shepherd_core/data_models/testbed/target.py +1 -1
  28. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  29. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  30. shepherd_core/data_models/testbed/testbed.py +8 -9
  31. shepherd_core/decoder_waveform/uart.py +7 -7
  32. shepherd_core/fw_tools/patcher.py +13 -14
  33. shepherd_core/fw_tools/validation.py +2 -2
  34. shepherd_core/inventory/system.py +3 -5
  35. shepherd_core/logger.py +3 -3
  36. shepherd_core/reader.py +9 -2
  37. shepherd_core/testbed_client/cache_path.py +1 -1
  38. shepherd_core/testbed_client/client_web.py +2 -2
  39. shepherd_core/testbed_client/fixtures.py +5 -5
  40. shepherd_core/version.py +1 -1
  41. shepherd_core/vsource/virtual_harvester_model.py +2 -2
  42. shepherd_core/vsource/virtual_source_simulation.py +2 -2
  43. shepherd_core/writer.py +2 -2
  44. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/METADATA +12 -12
  45. shepherd_core-2025.6.2.dist-info/RECORD +83 -0
  46. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/WHEEL +1 -1
  47. shepherd_core-2025.5.3.dist-info/RECORD +0 -81
  48. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/top_level.txt +0 -0
  49. {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py CHANGED
@@ -17,7 +17,7 @@ from .data_models.task.emulation import Compression
17
17
  from .inventory import Inventory
18
18
  from .logger import get_verbose_level
19
19
  from .logger import increase_verbose_level
20
- from .logger import logger
20
+ from .logger import log
21
21
  from .reader import Reader
22
22
  from .testbed_client.client_web import WebClient
23
23
  from .version import version
@@ -41,5 +41,5 @@ __all__ = [
41
41
  "increase_verbose_level",
42
42
  "local_now",
43
43
  "local_tz",
44
- "logger",
44
+ "log",
45
45
  ]
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()
@@ -3,7 +3,7 @@
3
3
  Public models are directly referenced here and are usable like:
4
4
 
5
5
  '''python
6
- from shepherd-core import data_models
6
+ from shepherd_core import data_models
7
7
 
8
8
  cdata = data_models.CapeData(serial_number="A123")
9
9
  '''
@@ -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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import hashlib
4
4
  import pathlib
5
+ import pickle
5
6
  from collections.abc import Generator
6
7
  from datetime import timedelta
7
8
  from ipaddress import IPv4Address
@@ -126,10 +127,12 @@ class ShpModel(BaseModel):
126
127
  comment: Optional[str] = None,
127
128
  *,
128
129
  minimal: bool = True,
130
+ use_pickle: bool = False,
129
131
  ) -> Path:
130
132
  """Store data to yaml in a wrapper.
131
133
 
132
134
  minimal: stores minimal set (filters out unset & default parameters)
135
+ pickle: uses pickle to serialize data, on BBB >100x faster for large files
133
136
  comment: documentation.
134
137
  """
135
138
  model_dict = self.model_dump(exclude_unset=minimal)
@@ -139,25 +142,39 @@ class ShpModel(BaseModel):
139
142
  created=local_now(),
140
143
  parameters=model_dict,
141
144
  )
142
- model_yaml = yaml.safe_dump(
143
- model_wrap.model_dump(exclude_unset=minimal, exclude_defaults=minimal),
144
- default_flow_style=False,
145
- sort_keys=False,
146
- )
145
+ if use_pickle:
146
+ model_serial = pickle.dumps(model_dict, fix_imports=True)
147
+ model_path = Path(path).resolve().with_suffix(".pickle")
148
+ else:
149
+ # TODO: x64 windows supports CSafeLoader/dumper,
150
+ # there are examples that replace load if avail
151
+ model_serial = yaml.safe_dump(
152
+ model_wrap.model_dump(exclude_unset=minimal, exclude_defaults=minimal),
153
+ default_flow_style=False,
154
+ sort_keys=False,
155
+ )
156
+ model_path = Path(path).resolve().with_suffix(".yaml")
147
157
  # TODO: handle directory
148
- model_path = Path(path).resolve().with_suffix(".yaml")
158
+
149
159
  if not model_path.parent.exists():
150
160
  model_path.parent.mkdir(parents=True)
151
161
  with model_path.open("w") as f:
152
- f.write(model_yaml)
162
+ f.write(model_serial)
153
163
  return model_path
154
164
 
155
165
  @classmethod
156
166
  def from_file(cls, path: Union[str, Path]) -> Self:
157
- """Load from yaml."""
158
- with Path(path).open() as shp_file:
159
- shp_dict = yaml.safe_load(shp_file)
160
- shp_wrap = Wrapper(**shp_dict)
167
+ """Load from YAML or pickle file."""
168
+ path: Path = Path(path)
169
+ if not Path(path).exists():
170
+ raise FileNotFoundError
171
+ if path.suffix.lower() == ".pickle":
172
+ with Path(path).open("rb") as shp_file:
173
+ shp_wrap = pickle.load(shp_file, fix_imports=True) # noqa: S301
174
+ else:
175
+ with Path(path).open() as shp_file:
176
+ shp_dict = yaml.safe_load(shp_file)
177
+ shp_wrap = Wrapper(**shp_dict)
161
178
  if shp_wrap.datatype != cls.__name__:
162
179
  raise ValueError("Model in file does not match the requirement")
163
180
  return cls(**shp_wrap.parameters)
@@ -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
@@ -19,7 +19,7 @@ from typing_extensions import Unpack
19
19
  from shepherd_core import fw_tools
20
20
  from shepherd_core.data_models.base.content import ContentModel
21
21
  from shepherd_core.data_models.testbed.mcu import MCU
22
- from shepherd_core.logger import logger
22
+ from shepherd_core.logger import log
23
23
  from shepherd_core.testbed_client import tb_client
24
24
 
25
25
  from .firmware_datatype import FirmwareDType
@@ -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,13 +125,18 @@ 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")
134
- logger.debug("ELF-File '%s' has arch: %s", file.name, arch)
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
+ log.warning("ObjCopy not found -> Arch of Firmware can't be verified")
139
+ log.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]
137
142
 
@@ -154,7 +159,7 @@ class Firmware(ContentModel, title="Firmware of Target"):
154
159
  match = self.data_hash == hash_new
155
160
 
156
161
  if not match:
157
- logger.warning("FW-Hash does not match with stored value!")
162
+ log.warning("FW-Hash does not match with stored value!")
158
163
  # TODO: it might be more appropriate to raise here
159
164
  return match
160
165
 
@@ -10,11 +10,11 @@ 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
17
- from shepherd_core.logger import logger
17
+ from shepherd_core.logger import log
18
18
  from shepherd_core.testbed_client import tb_client
19
19
 
20
20
  from .energy_environment import EnergyDType
@@ -310,7 +310,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
310
310
  if values["name"] == "neutral":
311
311
  # TODO: same test is later done in calc_algorithm_num() again
312
312
  raise ValueError("Resulting Harvester can't be neutral")
313
- logger.debug("VHrv-Inheritances: %s", chain)
313
+ log.debug("VHrv-Inheritances: %s", chain)
314
314
 
315
315
  # post corrections -> should be in separate validator
316
316
  cal = CalibrationHarvester() # TODO: as argument?
@@ -363,16 +363,16 @@ 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)
372
372
  duration_ms = min(max(self.duration_ms, time_min_ms), interval_ms)
373
373
  _ratio = (duration_ms / interval_ms) / (self.duration_ms / self.interval_ms)
374
374
  if (_ratio - 1) > 0.1:
375
- logger.debug(
375
+ log.debug(
376
376
  "Ratio between interval & duration has changed "
377
377
  "more than 10%% due to constraints (%.4f)",
378
378
  _ratio,
@@ -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,10 +7,10 @@ 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
- from shepherd_core.logger import logger
13
+ from shepherd_core.logger import log
14
14
  from shepherd_core.testbed_client import tb_client
15
15
 
16
16
  from .energy_environment import EnergyDType
@@ -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,39 +93,42 @@ 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
120
128
  def query_database(cls, values: dict[str, Any]) -> dict[str, Any]:
121
129
  values, chain = tb_client.try_completing_model(cls.__name__, values)
122
130
  values = tb_client.fill_in_user_data(values)
123
- logger.debug("VSrc-Inheritances: %s", chain)
131
+ log.debug("VSrc-Inheritances: %s", chain)
124
132
  return values
125
133
 
126
134
  @model_validator(mode="after")
@@ -179,7 +187,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
179
187
  if not (isinstance(dV_output_en_thrs_mV, (int, float)) and (dV_output_en_thrs_mV >= 0)):
180
188
  dV_output_en_thrs_mV = 0
181
189
  if not (isinstance(dV_output_imed_low_mV, (int, float)) and (dV_output_imed_low_mV >= 0)):
182
- logger.warning("VSrc: C_output shouldn't be larger than C_intermediate")
190
+ log.warning("VSrc: C_output shouldn't be larger than C_intermediate")
183
191
  dV_output_imed_low_mV = 0
184
192
 
185
193
  # decide which hysteresis-thresholds to use for buck-converter
@@ -216,7 +224,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
216
224
  enable_storage = self.C_intermediate_uF > 0
217
225
  enable_boost = self.enable_boost and enable_storage
218
226
  if enable_boost != self.enable_boost:
219
- logger.warning("VSrc - boost was disabled due to missing storage capacitor!")
227
+ log.warning("VSrc - boost was disabled due to missing storage capacitor!")
220
228
  enable_feedback = (
221
229
  self.enable_feedback_to_hrv
222
230
  and enable_storage
@@ -227,7 +235,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
227
235
  reason = "enabled boost, " if enable_boost else ""
228
236
  reason += "" if dtype_in == EnergyDType.ivcurve else "input not ivcurve, "
229
237
  reason += "" if enable_storage else "no storage capacitor"
230
- logger.warning("VSRC - feedback to harvester was disabled! Reasons: %s", reason)
238
+ log.warning("VSRC - feedback to harvester was disabled! Reasons: %s", reason)
231
239
  return (
232
240
  1 * int(enable_storage)
233
241
  + 2 * int(enable_boost)
@@ -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(' ', '_')}"