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.
- shepherd_core/__init__.py +2 -2
- shepherd_core/commons.py +3 -5
- shepherd_core/config.py +34 -0
- shepherd_core/data_models/__init__.py +1 -1
- shepherd_core/data_models/base/calibration.py +13 -8
- shepherd_core/data_models/base/shepherd.py +28 -11
- shepherd_core/data_models/base/wrapper.py +4 -4
- shepherd_core/data_models/content/energy_environment.py +1 -1
- shepherd_core/data_models/content/firmware.py +13 -8
- shepherd_core/data_models/content/virtual_harvester.py +13 -13
- shepherd_core/data_models/content/virtual_source.py +41 -33
- shepherd_core/data_models/content/virtual_source_fixture.yaml +1 -1
- shepherd_core/data_models/experiment/experiment.py +28 -18
- shepherd_core/data_models/experiment/observer_features.py +32 -13
- shepherd_core/data_models/experiment/target_config.py +17 -7
- shepherd_core/data_models/task/__init__.py +8 -4
- shepherd_core/data_models/task/emulation.py +52 -30
- shepherd_core/data_models/task/firmware_mod.py +15 -6
- shepherd_core/data_models/task/harvest.py +19 -13
- shepherd_core/data_models/task/helper_paths.py +15 -0
- shepherd_core/data_models/task/observer_tasks.py +20 -18
- shepherd_core/data_models/task/programming.py +10 -4
- shepherd_core/data_models/task/testbed_tasks.py +16 -7
- shepherd_core/data_models/testbed/cape_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/observer.py +1 -1
- shepherd_core/data_models/testbed/observer_fixture.yaml +2 -2
- shepherd_core/data_models/testbed/target.py +1 -1
- shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
- shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
- shepherd_core/data_models/testbed/testbed.py +8 -9
- shepherd_core/decoder_waveform/uart.py +7 -7
- shepherd_core/fw_tools/patcher.py +13 -14
- shepherd_core/fw_tools/validation.py +2 -2
- shepherd_core/inventory/system.py +3 -5
- shepherd_core/logger.py +3 -3
- shepherd_core/reader.py +9 -2
- shepherd_core/testbed_client/cache_path.py +1 -1
- shepherd_core/testbed_client/client_web.py +2 -2
- shepherd_core/testbed_client/fixtures.py +5 -5
- shepherd_core/version.py +1 -1
- shepherd_core/vsource/virtual_harvester_model.py +2 -2
- shepherd_core/vsource/virtual_source_simulation.py +2 -2
- shepherd_core/writer.py +2 -2
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/METADATA +12 -12
- shepherd_core-2025.6.2.dist-info/RECORD +83 -0
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/WHEEL +1 -1
- shepherd_core-2025.5.3.dist-info/RECORD +0 -81
- {shepherd_core-2025.5.3.dist-info → shepherd_core-2025.6.2.dist-info}/top_level.txt +0 -0
- {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
|
|
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
|
-
"
|
|
44
|
+
"log",
|
|
45
45
|
]
|
shepherd_core/commons.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
"""Container for commonly shared constants."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from .config import config
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
shepherd_core/config.py
ADDED
|
@@ -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 =
|
|
103
|
-
dac_V_Sim: CalibrationPair =
|
|
104
|
-
adc_V_Sense: CalibrationPair =
|
|
105
|
-
adc_C_Hrv: CalibrationPair =
|
|
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 =
|
|
147
|
-
dac_V_B: CalibrationPair =
|
|
148
|
-
adc_C_A: CalibrationPair =
|
|
149
|
-
adc_C_B: CalibrationPair =
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
20
|
+
""" ⤷ model-name"""
|
|
21
21
|
comment: Optional[SafeStrClone] = None
|
|
22
22
|
created: Optional[datetime] = None
|
|
23
|
-
|
|
23
|
+
""" ⤷ Optional metadata"""
|
|
24
24
|
lib_ver: Optional[str] = version
|
|
25
|
-
|
|
25
|
+
""" ⤷ for debug-purposes and later compatibility-checks"""
|
|
26
26
|
parameters: dict
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 /
|
|
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 /
|
|
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
|
-
|
|
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
|
-
|
|
456
|
+
""" ⤷ for window-based algo like ivcurve"""
|
|
457
457
|
current_limit_nA: u32
|
|
458
|
-
|
|
458
|
+
""" ⤷ lower bound to detect zero current"""
|
|
459
459
|
setpoint_n8: u32
|
|
460
460
|
interval_n: u32
|
|
461
|
-
|
|
461
|
+
""" ⤷ between measurements"""
|
|
462
462
|
duration_n: u32
|
|
463
|
-
|
|
463
|
+
""" ⤷ of measurement"""
|
|
464
464
|
wait_cycles_n: u32
|
|
465
|
-
|
|
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 *
|
|
521
|
-
duration_n=round(duration_ms *
|
|
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.
|
|
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
|
|
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
|
-
|
|
47
|
+
""" ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable"""
|
|
45
48
|
enable_buck: bool = False
|
|
46
|
-
|
|
49
|
+
""" ⤷ if false -> v_output = v_intermediate"""
|
|
47
50
|
enable_feedback_to_hrv: bool = False
|
|
48
|
-
|
|
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 =
|
|
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
|
-
|
|
60
|
+
""" ⤷ simulate input-diode"""
|
|
58
61
|
R_input_mOhm: Annotated[float, Field(ge=0, le=4.29e6)] = 0
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
""" ⤷ target gets disconnected"""
|
|
71
74
|
interval_check_thresholds_ms: Annotated[float, Field(ge=0, le=4.29e3)] = 0
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
+
""" ⤷ boost converter shuts off"""
|
|
98
103
|
|
|
99
104
|
LUT_input_efficiency: LUT2D = 12 * [12 * [1.00]]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
""" ⤷ simulate LDO / diode min voltage differential or output-diode"""
|
|
112
120
|
|
|
113
121
|
LUT_output_efficiency: LUT1D = 12 * [1.00]
|
|
114
|
-
|
|
122
|
+
""" ⤷ array[12] depending on output_current"""
|
|
115
123
|
LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 *
|
|
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 *
|
|
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 *
|
|
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
|
|
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 =
|
|
42
|
+
email_results: bool = True
|
|
46
43
|
|
|
47
|
-
sys_logging: SystemLogging =
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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]
|
|
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(' ', '_')}"
|