shepherd-core 2023.12.1__py3-none-any.whl → 2024.4.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.
- shepherd_core/__init__.py +3 -2
- shepherd_core/data_models/base/calibration.py +3 -1
- shepherd_core/data_models/base/content.py +11 -4
- shepherd_core/data_models/base/shepherd.py +2 -0
- shepherd_core/data_models/content/energy_environment.py +1 -1
- shepherd_core/data_models/content/virtual_harvester.py +1 -1
- shepherd_core/data_models/content/virtual_source.py +18 -13
- shepherd_core/data_models/experiment/experiment.py +10 -10
- shepherd_core/data_models/experiment/observer_features.py +5 -6
- shepherd_core/data_models/task/observer_tasks.py +1 -1
- shepherd_core/data_models/task/testbed_tasks.py +7 -3
- shepherd_core/data_models/testbed/cape.py +1 -1
- shepherd_core/data_models/testbed/gpio.py +1 -1
- shepherd_core/data_models/testbed/mcu.py +1 -1
- shepherd_core/data_models/testbed/observer.py +1 -1
- shepherd_core/data_models/testbed/target.py +1 -1
- shepherd_core/data_models/testbed/testbed.py +1 -1
- shepherd_core/decoder_waveform/uart.py +1 -0
- shepherd_core/fw_tools/validation.py +1 -2
- shepherd_core/inventory/__init__.py +51 -1
- shepherd_core/inventory/system.py +8 -0
- shepherd_core/reader.py +19 -16
- shepherd_core/testbed_client/cache_path.py +15 -0
- shepherd_core/testbed_client/client.py +2 -3
- shepherd_core/testbed_client/fixtures.py +4 -1
- shepherd_core/testbed_client/user_model.py +6 -4
- shepherd_core/vsource/virtual_converter_model.py +1 -1
- shepherd_core/vsource/virtual_harvester_model.py +1 -0
- shepherd_core/vsource/virtual_source_model.py +1 -0
- shepherd_core/writer.py +5 -2
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +49 -34
- shepherd_core-2024.4.1.dist-info/RECORD +64 -0
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
- shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
- shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
- shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
- shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
- shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
- shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
- shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
- shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
- shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
- shepherd_core-2023.12.1.dist-info/RECORD +0 -117
- tests/__init__.py +0 -0
- tests/conftest.py +0 -64
- tests/data_models/__init__.py +0 -0
- tests/data_models/conftest.py +0 -14
- tests/data_models/example_cal_data.yaml +0 -31
- tests/data_models/example_cal_data_faulty.yaml +0 -29
- tests/data_models/example_cal_meas.yaml +0 -178
- tests/data_models/example_cal_meas_faulty1.yaml +0 -142
- tests/data_models/example_cal_meas_faulty2.yaml +0 -136
- tests/data_models/example_config_emulator.yaml +0 -41
- tests/data_models/example_config_experiment.yaml +0 -16
- tests/data_models/example_config_experiment_alternative.yaml +0 -14
- tests/data_models/example_config_harvester.yaml +0 -15
- tests/data_models/example_config_testbed.yaml +0 -26
- tests/data_models/example_config_virtsource.yaml +0 -78
- tests/data_models/test_base_models.py +0 -205
- tests/data_models/test_content_fixtures.py +0 -41
- tests/data_models/test_content_models.py +0 -282
- tests/data_models/test_examples.py +0 -48
- tests/data_models/test_experiment_models.py +0 -277
- tests/data_models/test_task_generation.py +0 -52
- tests/data_models/test_task_models.py +0 -131
- tests/data_models/test_testbed_fixtures.py +0 -47
- tests/data_models/test_testbed_models.py +0 -187
- tests/decoder_waveform/__init__.py +0 -0
- tests/decoder_waveform/test_decoder.py +0 -34
- tests/fw_tools/__init__.py +0 -0
- tests/fw_tools/conftest.py +0 -5
- tests/fw_tools/test_converter.py +0 -76
- tests/fw_tools/test_patcher.py +0 -66
- tests/fw_tools/test_validation.py +0 -56
- tests/inventory/__init__.py +0 -0
- tests/inventory/test_inventory.py +0 -20
- tests/test_cal_hw.py +0 -34
- tests/test_examples.py +0 -40
- tests/test_logger.py +0 -15
- tests/test_reader.py +0 -283
- tests/test_writer.py +0 -169
- tests/testbed_client/__init__.py +0 -0
- tests/vsource/__init__.py +0 -0
- tests/vsource/conftest.py +0 -49
- tests/vsource/test_converter.py +0 -161
- tests/vsource/test_harvester.py +0 -73
- tests/vsource/test_z.py +0 -5
- {shepherd_core-2023.12.1.dist-info → shepherd_core-2024.4.1.dist-info}/zip-safe +0 -0
shepherd_core/__init__.py
CHANGED
|
@@ -4,6 +4,7 @@ Provides classes for storing and retrieving sampled IV data to/from
|
|
|
4
4
|
HDF5 files.
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from .data_models.base.calibration import Calc_t
|
|
8
9
|
from .data_models.base.calibration import CalibrationCape
|
|
9
10
|
from .data_models.base.calibration import CalibrationEmulator
|
|
@@ -12,7 +13,7 @@ from .data_models.base.calibration import CalibrationPair
|
|
|
12
13
|
from .data_models.base.calibration import CalibrationSeries
|
|
13
14
|
from .data_models.base.timezone import local_now
|
|
14
15
|
from .data_models.base.timezone import local_tz
|
|
15
|
-
from .data_models.task import Compression
|
|
16
|
+
from .data_models.task.emulation import Compression
|
|
16
17
|
from .inventory import Inventory
|
|
17
18
|
from .logger import get_verbose_level
|
|
18
19
|
from .logger import increase_verbose_level
|
|
@@ -22,7 +23,7 @@ from .testbed_client.client import TestbedClient
|
|
|
22
23
|
from .testbed_client.client import tb_client
|
|
23
24
|
from .writer import Writer
|
|
24
25
|
|
|
25
|
-
__version__ = "
|
|
26
|
+
__version__ = "2024.04.1"
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
28
29
|
"Reader",
|
|
@@ -165,7 +165,7 @@ class CapeData(ShpModel):
|
|
|
165
165
|
`See<https://github.com/beagleboard/beaglebone-black/wiki/System-Reference-Manual#824_EEPROM_Data_Format>`_
|
|
166
166
|
"""
|
|
167
167
|
|
|
168
|
-
header: conbytes(max_length=4) = b"\
|
|
168
|
+
header: conbytes(max_length=4) = b"\xaa\x55\x33\xee"
|
|
169
169
|
eeprom_revision: constr(max_length=2) = "A2"
|
|
170
170
|
board_name: constr(max_length=32) = "BeagleBone SHEPHERD2 Cape"
|
|
171
171
|
version: constr(max_length=4) = "24B0"
|
|
@@ -207,6 +207,7 @@ class CalibrationCape(ShpModel):
|
|
|
207
207
|
cape: data can be supplied
|
|
208
208
|
Returns:
|
|
209
209
|
CalibrationCape object with extracted calibration data.
|
|
210
|
+
|
|
210
211
|
"""
|
|
211
212
|
dv = cls().model_dump(include={"harvester", "emulator"})
|
|
212
213
|
lw = list(dict_generator(dv))
|
|
@@ -225,6 +226,7 @@ class CalibrationCape(ShpModel):
|
|
|
225
226
|
Returns
|
|
226
227
|
-------
|
|
227
228
|
Byte string representation of calibration values.
|
|
229
|
+
|
|
228
230
|
"""
|
|
229
231
|
lw = list(dict_generator(self.model_dump(include={"harvester", "emulator"})))
|
|
230
232
|
values = [walk[-1] for walk in lw]
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from typing import Union
|
|
5
|
+
from uuid import uuid4
|
|
4
6
|
|
|
7
|
+
from pydantic import UUID4
|
|
5
8
|
from pydantic import Field
|
|
6
9
|
from pydantic import StringConstraints
|
|
7
10
|
from pydantic import model_validator
|
|
@@ -14,30 +17,34 @@ from .timezone import local_now
|
|
|
14
17
|
# constr -> to_lower=True, max_length=16, regex=r"^[\w]+$"
|
|
15
18
|
# ⤷ Regex = AlphaNum
|
|
16
19
|
IdInt = Annotated[int, Field(ge=0, lt=2**128)]
|
|
17
|
-
NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r
|
|
20
|
+
NameStr = Annotated[str, StringConstraints(max_length=32, pattern=r"^[^<>:;,?\"\*|\/\\]+$")]
|
|
18
21
|
# ⤷ Regex = FileSystem-Compatible ASCII
|
|
19
22
|
SafeStr = Annotated[str, StringConstraints(pattern=r"^[ -~]+$")]
|
|
20
23
|
# ⤷ Regex = All Printable ASCII-Characters with Space
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
def id_default() -> int:
|
|
27
|
+
# note: IdInt has space for 128 bit, so 128/4 = 32 hex-chars
|
|
24
28
|
time_stamp = str(local_now()).encode("utf-8")
|
|
25
|
-
time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-
|
|
29
|
+
time_hash = hashlib.sha3_224(time_stamp).hexdigest()[-32:]
|
|
26
30
|
return int(time_hash, 16)
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
class ContentModel(ShpModel):
|
|
30
34
|
# General Properties
|
|
31
|
-
id:
|
|
35
|
+
# id: UUID4 = Field( # TODO: db-migration - temp fix for documentation
|
|
36
|
+
id: Union[UUID4, int] = Field(
|
|
32
37
|
description="Unique ID",
|
|
33
|
-
default_factory=
|
|
38
|
+
default_factory=uuid4,
|
|
34
39
|
)
|
|
35
40
|
name: NameStr
|
|
36
41
|
description: Annotated[Optional[SafeStr], Field(description="Required when public")] = None
|
|
37
42
|
comment: Optional[SafeStr] = None
|
|
38
43
|
created: datetime = Field(default_factory=datetime.now)
|
|
44
|
+
updated_last: datetime = Field(default_factory=datetime.now)
|
|
39
45
|
|
|
40
46
|
# Ownership & Access
|
|
47
|
+
# TODO: remove owner & group, only needed for DB
|
|
41
48
|
owner: NameStr
|
|
42
49
|
group: Annotated[NameStr, Field(description="University or Subgroup")]
|
|
43
50
|
visible2group: bool = False
|
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
from typing import Generator
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from typing import Union
|
|
10
|
+
from uuid import UUID
|
|
10
11
|
|
|
11
12
|
import yaml
|
|
12
13
|
from pydantic import BaseModel
|
|
@@ -39,6 +40,7 @@ yaml.add_representer(pathlib.WindowsPath, path2str, SafeDumper)
|
|
|
39
40
|
yaml.add_representer(pathlib.Path, path2str, SafeDumper)
|
|
40
41
|
yaml.add_representer(timedelta, time2int, SafeDumper)
|
|
41
42
|
yaml.add_representer(IPv4Address, generic2str, SafeDumper)
|
|
43
|
+
yaml.add_representer(UUID, generic2str, SafeDumper)
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
class ShpModel(BaseModel):
|
|
@@ -76,7 +76,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
|
|
|
76
76
|
logger.debug("VHrv-Inheritances: %s", chain)
|
|
77
77
|
|
|
78
78
|
# post corrections -> should be in separate validator
|
|
79
|
-
cal = CalibrationHarvester() #
|
|
79
|
+
cal = CalibrationHarvester() # TODO: as argument?
|
|
80
80
|
c_limit = values.get("current_limit_uA", 50_000) # cls.current_limit_uA)
|
|
81
81
|
values["current_limit_uA"] = max(10**6 * cal.adc_C_Hrv.raw_to_si(4), c_limit)
|
|
82
82
|
|
|
@@ -25,12 +25,13 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
25
25
|
for supplying the Target Node during the experiment.
|
|
26
26
|
If not already done, the energy will be harvested and then converted.
|
|
27
27
|
The converter-stage is software defined and offers:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
TODO: I,V,R should be in regular unit (V, A, Ohm)
|
|
28
|
+
- buck-boost-combinations,
|
|
29
|
+
- a simple diode + resistor and
|
|
30
|
+
- an intermediate buffer capacitor.
|
|
32
31
|
"""
|
|
33
32
|
|
|
33
|
+
# TODO: I,V,R should be in regular unit (V, A, Ohm)
|
|
34
|
+
|
|
34
35
|
# General Metadata & Ownership -> ContentModel
|
|
35
36
|
|
|
36
37
|
enable_boost: bool = False
|
|
@@ -123,14 +124,17 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
123
124
|
only has simpler formula, second enabling when V_Cap >= V_out
|
|
124
125
|
|
|
125
126
|
Math behind this calculation:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
convert
|
|
132
|
-
|
|
133
|
-
|
|
127
|
+
|
|
128
|
+
- Energy-Change Storage Cap -> E_new = E_old - E_output
|
|
129
|
+
- with Energy of a Cap -> E_x = C_x * V_x^2 / 2
|
|
130
|
+
- combine formulas -> C_store * V_store_new^2 / 2 =
|
|
131
|
+
C_store * V_store_old^2 / 2 - C_out * V_out^2 / 2
|
|
132
|
+
- convert formula to V_new -> V_store_new^2 =
|
|
133
|
+
V_store_old^2 - (C_out / C_store) * V_out^2
|
|
134
|
+
- convert into dV -> dV = V_store_new - V_store_old
|
|
135
|
+
- in case of V_cap = V_out -> dV = V_store_old * (sqrt(1 - C_out / C_store) - 1)
|
|
136
|
+
|
|
137
|
+
Note: dV values will be reversed (negated), because dV is always negative (Voltage drop)
|
|
134
138
|
"""
|
|
135
139
|
values = {}
|
|
136
140
|
if self.C_intermediate_uF > 0 and self.C_output_uF > 0:
|
|
@@ -186,8 +190,9 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
|
|
|
186
190
|
|
|
187
191
|
def calc_converter_mode(self, *, log_intermediate_node: bool) -> int:
|
|
188
192
|
"""Assembles bitmask from discrete values
|
|
193
|
+
|
|
189
194
|
log_intermediate_node: record / log virtual intermediate (cap-)voltage and
|
|
190
|
-
|
|
195
|
+
-current (out) instead of output-voltage and -current
|
|
191
196
|
"""
|
|
192
197
|
enable_storage = self.C_intermediate_uF > 0
|
|
193
198
|
enable_boost = self.enable_boost and enable_storage
|
|
@@ -2,8 +2,10 @@ from datetime import datetime
|
|
|
2
2
|
from datetime import timedelta
|
|
3
3
|
from typing import List
|
|
4
4
|
from typing import Optional
|
|
5
|
+
from typing import Union
|
|
6
|
+
from uuid import uuid4
|
|
5
7
|
|
|
6
|
-
from pydantic import
|
|
8
|
+
from pydantic import UUID4
|
|
7
9
|
from pydantic import Field
|
|
8
10
|
from pydantic import model_validator
|
|
9
11
|
from typing_extensions import Annotated
|
|
@@ -12,7 +14,6 @@ from typing_extensions import Self
|
|
|
12
14
|
from ..base.content import IdInt
|
|
13
15
|
from ..base.content import NameStr
|
|
14
16
|
from ..base.content import SafeStr
|
|
15
|
-
from ..base.content import id_default
|
|
16
17
|
from ..base.shepherd import ShpModel
|
|
17
18
|
from ..testbed.target import Target
|
|
18
19
|
from ..testbed.testbed import Testbed
|
|
@@ -26,11 +27,10 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
29
|
# General Properties
|
|
29
|
-
id:
|
|
30
|
-
|
|
31
|
-
default_factory=id_default,
|
|
32
|
-
)
|
|
30
|
+
# id: UUID4 ... # TODO db-migration - temp fix for documentation
|
|
31
|
+
id: Union[UUID4, int] = Field(default_factory=uuid4)
|
|
33
32
|
# ⤷ TODO: automatic ID is problematic for identification by hash
|
|
33
|
+
|
|
34
34
|
name: NameStr
|
|
35
35
|
description: Annotated[
|
|
36
36
|
Optional[SafeStr], Field(description="Required for public instances")
|
|
@@ -38,12 +38,12 @@ class Experiment(ShpModel, title="Config of an Experiment"):
|
|
|
38
38
|
comment: Optional[SafeStr] = None
|
|
39
39
|
created: datetime = Field(default_factory=datetime.now)
|
|
40
40
|
|
|
41
|
-
# Ownership & Access
|
|
42
|
-
owner_id: Optional[IdInt] =
|
|
41
|
+
# Ownership & Access
|
|
42
|
+
owner_id: Optional[IdInt] = None
|
|
43
43
|
|
|
44
44
|
# feedback
|
|
45
|
-
email_results:
|
|
46
|
-
|
|
45
|
+
email_results: bool = False
|
|
46
|
+
|
|
47
47
|
sys_logging: SystemLogging = SystemLogging(dmesg=True, ptp=True, shepherd=True)
|
|
48
48
|
|
|
49
49
|
# schedule
|
|
@@ -66,7 +66,7 @@ class GpioTracing(ShpModel, title="Config for GPIO-Tracing"):
|
|
|
66
66
|
|
|
67
67
|
# post-processing,
|
|
68
68
|
uart_decode: bool = False
|
|
69
|
-
#
|
|
69
|
+
# TODO: quickfix - uart-log currently done online in userspace
|
|
70
70
|
# NOTE: gpio-tracing currently shows rather big - but rare - "blind" windows (~1-4us)
|
|
71
71
|
uart_pin: GPIO = GPIO(name="GPIO8")
|
|
72
72
|
uart_baudrate: Annotated[int, Field(ge=2_400, le=921_600)] = 115_200
|
|
@@ -113,11 +113,10 @@ class GpioEvent(ShpModel, title="Config for a GPIO-Event"):
|
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
class GpioActuation(ShpModel, title="Config for GPIO-Actuation"):
|
|
116
|
-
"""Configuration for a GPIO-Actuation-Sequence
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
"""
|
|
116
|
+
"""Configuration for a GPIO-Actuation-Sequence"""
|
|
117
|
+
|
|
118
|
+
# TODO: not implemented ATM - decide if pru control sys-gpio or
|
|
119
|
+
# TODO: not implemented ATM - reverses pru-gpio (preferred if possible)
|
|
121
120
|
|
|
122
121
|
events: Annotated[List[GpioEvent], Field(min_length=1, max_length=1024)]
|
|
123
122
|
|
|
@@ -21,7 +21,7 @@ class ObserverTasks(ShpModel):
|
|
|
21
21
|
"""Collection of tasks for selected observer included in experiment"""
|
|
22
22
|
|
|
23
23
|
observer: NameStr
|
|
24
|
-
owner_id: IdInt
|
|
24
|
+
owner_id: Optional[IdInt] # TODO: set to optional for now, shouldn't be
|
|
25
25
|
|
|
26
26
|
# PRE PROCESS
|
|
27
27
|
time_prep: datetime # TODO: should be optional
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import List
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from pydantic import EmailStr
|
|
5
4
|
from pydantic import Field
|
|
6
5
|
from pydantic import validate_call
|
|
7
6
|
from typing_extensions import Annotated
|
|
8
7
|
from typing_extensions import Self
|
|
9
8
|
|
|
9
|
+
from ..base.content import IdInt
|
|
10
10
|
from ..base.content import NameStr
|
|
11
11
|
from ..base.shepherd import ShpModel
|
|
12
12
|
from ..experiment.experiment import Experiment
|
|
@@ -21,7 +21,10 @@ class TestbedTasks(ShpModel):
|
|
|
21
21
|
observer_tasks: Annotated[List[ObserverTasks], Field(min_length=1, max_length=128)]
|
|
22
22
|
|
|
23
23
|
# POST PROCESS
|
|
24
|
-
|
|
24
|
+
email_results: bool = False
|
|
25
|
+
owner_id: Optional[IdInt]
|
|
26
|
+
# TODO: had real email previously, does it really need these at all?
|
|
27
|
+
# DB stores experiment and knows when to email
|
|
25
28
|
|
|
26
29
|
@classmethod
|
|
27
30
|
@validate_call
|
|
@@ -34,7 +37,8 @@ class TestbedTasks(ShpModel):
|
|
|
34
37
|
return cls(
|
|
35
38
|
name=xp.name,
|
|
36
39
|
observer_tasks=obs_tasks,
|
|
37
|
-
|
|
40
|
+
email_results=xp.email_results,
|
|
41
|
+
owner_id=xp.owner_id,
|
|
38
42
|
)
|
|
39
43
|
|
|
40
44
|
def get_observer_tasks(self, observer: str) -> Optional[ObserverTasks]:
|
|
@@ -26,7 +26,7 @@ class Direction(str, Enum):
|
|
|
26
26
|
class GPIO(ShpModel, title="GPIO of Observer Node"):
|
|
27
27
|
"""meta-data representation of a testbed-component"""
|
|
28
28
|
|
|
29
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
30
30
|
name: NameStr
|
|
31
31
|
description: Optional[SafeStr] = None
|
|
32
32
|
comment: Optional[SafeStr] = None
|
|
@@ -26,7 +26,7 @@ class ProgrammerProtocol(str, Enum):
|
|
|
26
26
|
class MCU(ShpModel, title="Microcontroller of the Target Node"):
|
|
27
27
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
28
28
|
|
|
29
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
30
30
|
name: NameStr
|
|
31
31
|
description: SafeStr
|
|
32
32
|
comment: Optional[SafeStr] = None
|
|
@@ -26,7 +26,7 @@ MACStr = Annotated[
|
|
|
26
26
|
class Observer(ShpModel, title="Shepherd-Sheep"):
|
|
27
27
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
28
28
|
|
|
29
|
-
id: IdInt
|
|
29
|
+
id: IdInt
|
|
30
30
|
name: NameStr
|
|
31
31
|
description: SafeStr
|
|
32
32
|
comment: Optional[SafeStr] = None
|
|
@@ -21,7 +21,7 @@ MCUPort = Annotated[int, Field(ge=1, le=2)]
|
|
|
21
21
|
class Target(ShpModel, title="Target Node (DuT)"):
|
|
22
22
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
23
23
|
|
|
24
|
-
id: IdInt
|
|
24
|
+
id: IdInt
|
|
25
25
|
name: NameStr
|
|
26
26
|
version: NameStr
|
|
27
27
|
description: SafeStr
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
- system-parameters
|
|
4
4
|
- hardware-config
|
|
5
5
|
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from datetime import timedelta
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from typing import List
|
|
8
11
|
|
|
@@ -26,14 +29,20 @@ __all__ = [
|
|
|
26
29
|
|
|
27
30
|
class Inventory(PythonInventory, SystemInventory, TargetInventory):
|
|
28
31
|
# has all child-parameters
|
|
32
|
+
hostname: str
|
|
33
|
+
created: datetime
|
|
29
34
|
|
|
30
35
|
@classmethod
|
|
31
36
|
def collect(cls) -> Self:
|
|
32
37
|
# one by one for more precise error messages
|
|
33
|
-
|
|
38
|
+
# NOTE: system is first, as it must take a precise timestamp
|
|
34
39
|
sid = SystemInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
40
|
+
pid = PythonInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
35
41
|
tid = TargetInventory.collect().model_dump(exclude_unset=True, exclude_defaults=True)
|
|
36
42
|
model = {**pid, **sid, **tid}
|
|
43
|
+
# make important metadata available at root level
|
|
44
|
+
model["created"] = sid["timestamp"]
|
|
45
|
+
model["hostname"] = sid["hostname"]
|
|
37
46
|
return cls(**model)
|
|
38
47
|
|
|
39
48
|
|
|
@@ -53,3 +62,44 @@ class InventoryList(ShpModel):
|
|
|
53
62
|
content = list(item.model_dump().values())
|
|
54
63
|
content = ["" if value is None else str(value) for value in content]
|
|
55
64
|
fd.write(", ".join(content) + "\r\n")
|
|
65
|
+
|
|
66
|
+
def warn(self) -> dict:
|
|
67
|
+
warnings = {}
|
|
68
|
+
ts_earl = min([_e.created.timestamp() for _e in self.elements])
|
|
69
|
+
for _e in self.elements:
|
|
70
|
+
if _e.uptime > timedelta(hours=30).total_seconds():
|
|
71
|
+
warnings["uptime"] = f"[{self.hostname}] restart is recommended"
|
|
72
|
+
if (_e.created.timestamp() - ts_earl) > 10:
|
|
73
|
+
warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
|
|
74
|
+
|
|
75
|
+
# turn dict[hostname][type] = val
|
|
76
|
+
# to dict[type][val] = list[hostnames]
|
|
77
|
+
_inp = {
|
|
78
|
+
_e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
|
|
79
|
+
for _e in self.elements
|
|
80
|
+
}
|
|
81
|
+
result = {}
|
|
82
|
+
for _host, _types in _inp.items():
|
|
83
|
+
for _type, _val in _types.items():
|
|
84
|
+
if _type not in result:
|
|
85
|
+
result[_type] = {}
|
|
86
|
+
if _val not in result[_type]:
|
|
87
|
+
result[_type][_val] = []
|
|
88
|
+
result[_type][_val].append(_host)
|
|
89
|
+
rescnt = {_key: len(_val) for _key, _val in result.items()}
|
|
90
|
+
t_unique = [
|
|
91
|
+
"h5py",
|
|
92
|
+
"numpy",
|
|
93
|
+
"pydantic",
|
|
94
|
+
"python",
|
|
95
|
+
"shepherd_core",
|
|
96
|
+
"shepherd_sheep",
|
|
97
|
+
"yaml",
|
|
98
|
+
"zstandard",
|
|
99
|
+
]
|
|
100
|
+
for _key in t_unique:
|
|
101
|
+
if rescnt[_key] > 1:
|
|
102
|
+
warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
|
|
103
|
+
|
|
104
|
+
# TODO: finish with more potential warnings
|
|
105
|
+
return warnings
|
|
@@ -2,10 +2,12 @@ import platform
|
|
|
2
2
|
import subprocess
|
|
3
3
|
import time
|
|
4
4
|
from contextlib import suppress
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
7
8
|
from typing_extensions import Self
|
|
8
9
|
|
|
10
|
+
from .. import local_now
|
|
9
11
|
from .. import logger
|
|
10
12
|
|
|
11
13
|
try:
|
|
@@ -22,6 +24,9 @@ from ..data_models import ShpModel
|
|
|
22
24
|
class SystemInventory(ShpModel):
|
|
23
25
|
uptime: PositiveInt
|
|
24
26
|
# ⤷ seconds
|
|
27
|
+
timestamp: datetime
|
|
28
|
+
# time_delta: timedelta = timedelta(0) # noqa: ERA001
|
|
29
|
+
# ⤷ lag behind earliest observer, TODO: wrong place
|
|
25
30
|
|
|
26
31
|
system: str
|
|
27
32
|
release: str
|
|
@@ -43,6 +48,8 @@ class SystemInventory(ShpModel):
|
|
|
43
48
|
|
|
44
49
|
@classmethod
|
|
45
50
|
def collect(cls) -> Self:
|
|
51
|
+
ts = local_now()
|
|
52
|
+
|
|
46
53
|
if psutil is None:
|
|
47
54
|
ifs2 = {}
|
|
48
55
|
uptime = 0
|
|
@@ -58,6 +65,7 @@ class SystemInventory(ShpModel):
|
|
|
58
65
|
|
|
59
66
|
model_dict = {
|
|
60
67
|
"uptime": round(uptime),
|
|
68
|
+
"timestamp": ts,
|
|
61
69
|
"system": platform.system(),
|
|
62
70
|
"release": platform.release(),
|
|
63
71
|
"version": platform.version(),
|
shepherd_core/reader.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
"""
|
|
1
|
+
"""Reader-Baseclass"""
|
|
2
|
+
|
|
4
3
|
import contextlib
|
|
5
4
|
import errno
|
|
6
5
|
import logging
|
|
@@ -36,8 +35,10 @@ class Reader:
|
|
|
36
35
|
"""Sequentially Reads shepherd-data from HDF5 file.
|
|
37
36
|
|
|
38
37
|
Args:
|
|
38
|
+
----
|
|
39
39
|
file_path: Path of hdf5 file containing shepherd data with iv-samples, iv-curves or isc&voc
|
|
40
40
|
verbose: more debug-info during usage, 'None' skips the setter
|
|
41
|
+
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
samples_per_buffer: int = 10_000
|
|
@@ -158,7 +159,7 @@ class Reader:
|
|
|
158
159
|
)
|
|
159
160
|
|
|
160
161
|
def _refresh_file_stats(self) -> None:
|
|
161
|
-
"""
|
|
162
|
+
"""Update internal states, helpful after resampling or other changes in data-group"""
|
|
162
163
|
self.h5file.flush()
|
|
163
164
|
if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
|
|
164
165
|
# this assumes isochronal sampling
|
|
@@ -182,15 +183,17 @@ class Reader:
|
|
|
182
183
|
) -> Generator[tuple, None, None]:
|
|
183
184
|
"""Generator that reads the specified range of buffers from the hdf5 file.
|
|
184
185
|
can be configured on first call
|
|
185
|
-
TODO: reconstruct - start/end mark samples
|
|
186
|
-
|
|
186
|
+
TODO: reconstruct - start/end mark samples &
|
|
187
|
+
each call can request a certain number of samples
|
|
187
188
|
|
|
188
189
|
Args:
|
|
190
|
+
----
|
|
189
191
|
:param start_n: (int) Index of first buffer to be read
|
|
190
192
|
:param end_n: (int) Index of last buffer to be read
|
|
191
193
|
:param is_raw: (bool) output original data, not transformed to SI-Units
|
|
192
194
|
:param omit_ts: (bool) optimize reading if timestamp is never used
|
|
193
195
|
Yields: Buffers between start and end (tuple with time, voltage, current)
|
|
196
|
+
|
|
194
197
|
"""
|
|
195
198
|
if end_n is None:
|
|
196
199
|
end_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
|
|
@@ -250,7 +253,7 @@ class Reader:
|
|
|
250
253
|
return None
|
|
251
254
|
|
|
252
255
|
def get_hrv_config(self) -> dict:
|
|
253
|
-
"""
|
|
256
|
+
"""Essential info for harvester
|
|
254
257
|
:return: config-dict directly for vHarvester to be used during emulation
|
|
255
258
|
"""
|
|
256
259
|
return {
|
|
@@ -259,7 +262,7 @@ class Reader:
|
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
def is_valid(self) -> bool:
|
|
262
|
-
"""
|
|
265
|
+
"""Checks file for plausibility
|
|
263
266
|
|
|
264
267
|
:return: state of validity
|
|
265
268
|
"""
|
|
@@ -387,7 +390,7 @@ class Reader:
|
|
|
387
390
|
return True
|
|
388
391
|
|
|
389
392
|
def __getitem__(self, key: str) -> Any:
|
|
390
|
-
"""
|
|
393
|
+
"""Returns attribute or (if none found) a handle for a group or dataset (if found)
|
|
391
394
|
|
|
392
395
|
:param key: attribute, group, dataset
|
|
393
396
|
:return: value of that key, or handle of object
|
|
@@ -399,7 +402,7 @@ class Reader:
|
|
|
399
402
|
raise KeyError
|
|
400
403
|
|
|
401
404
|
def energy(self) -> float:
|
|
402
|
-
"""
|
|
405
|
+
"""Determine the recorded energy of the trace
|
|
403
406
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
404
407
|
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
|
|
405
408
|
|
|
@@ -427,7 +430,7 @@ class Reader:
|
|
|
427
430
|
def _dset_statistics(
|
|
428
431
|
self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
|
|
429
432
|
) -> Dict[str, float]:
|
|
430
|
-
"""
|
|
433
|
+
"""Some basic stats for a provided dataset
|
|
431
434
|
:param dset: dataset to evaluate
|
|
432
435
|
:param cal: calibration (if wanted)
|
|
433
436
|
:return: dict with entries for mean, min, max, std
|
|
@@ -469,7 +472,7 @@ class Reader:
|
|
|
469
472
|
return stats
|
|
470
473
|
|
|
471
474
|
def _data_timediffs(self) -> List[float]:
|
|
472
|
-
"""
|
|
475
|
+
"""Calculate list of (unique) time-deltas between buffers [s]
|
|
473
476
|
-> optimized version that only looks at the start of each buffer
|
|
474
477
|
|
|
475
478
|
:return: list of (unique) time-deltas between buffers [s]
|
|
@@ -500,7 +503,7 @@ class Reader:
|
|
|
500
503
|
return list(diffs)
|
|
501
504
|
|
|
502
505
|
def check_timediffs(self) -> bool:
|
|
503
|
-
"""
|
|
506
|
+
"""Validate equal time-deltas
|
|
504
507
|
-> unexpected time-jumps hint at a corrupted file or faulty measurement
|
|
505
508
|
|
|
506
509
|
:return: True if OK
|
|
@@ -529,7 +532,7 @@ class Reader:
|
|
|
529
532
|
*,
|
|
530
533
|
minimal: bool = False,
|
|
531
534
|
) -> Dict[str, dict]:
|
|
532
|
-
"""
|
|
535
|
+
"""Recursive FN to capture the structure of the file
|
|
533
536
|
:param node: starting node, leave free to go through whole file
|
|
534
537
|
:param minimal: just provide a bare tree (much faster)
|
|
535
538
|
:return: structure of that node with everything inside it
|
|
@@ -580,7 +583,7 @@ class Reader:
|
|
|
580
583
|
return metadata
|
|
581
584
|
|
|
582
585
|
def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
|
|
583
|
-
"""
|
|
586
|
+
"""Get structure of file and dump content to yaml-file with same name as original
|
|
584
587
|
|
|
585
588
|
:param node: starting node, leave free to go through whole file
|
|
586
589
|
:return: structure of that node with everything inside it
|
|
@@ -609,7 +612,7 @@ class Reader:
|
|
|
609
612
|
|
|
610
613
|
@staticmethod
|
|
611
614
|
def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
|
|
612
|
-
"""
|
|
615
|
+
"""Input is 1D state-vector, kep only first from identical & sequential states
|
|
613
616
|
algo: create an offset-by-one vector and compare against original
|
|
614
617
|
"""
|
|
615
618
|
if len(data.shape) > 1:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
|
|
6
|
+
_value = os.environ.get(variable_name)
|
|
7
|
+
if _value is None or _value == "":
|
|
8
|
+
return default_path
|
|
9
|
+
return Path(_value)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
user_path = Path("~").expanduser()
|
|
13
|
+
|
|
14
|
+
cache_xdg_path = _get_xdg_path("XDG_CACHE_HOME", user_path / ".cache")
|
|
15
|
+
cache_user_path = cache_xdg_path / "shepherd_datalib"
|