shepherd-core 2023.11.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 +13 -11
- shepherd_core/data_models/base/shepherd.py +4 -6
- 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 +31 -53
- shepherd_core/data_models/experiment/experiment.py +13 -18
- shepherd_core/data_models/experiment/observer_features.py +9 -15
- shepherd_core/data_models/experiment/target_config.py +4 -11
- shepherd_core/data_models/task/__init__.py +2 -6
- shepherd_core/data_models/task/emulation.py +7 -14
- shepherd_core/data_models/task/firmware_mod.py +1 -3
- shepherd_core/data_models/task/harvest.py +1 -3
- 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 +7 -23
- shepherd_core/data_models/testbed/target.py +1 -1
- shepherd_core/data_models/testbed/testbed.py +2 -4
- shepherd_core/decoder_waveform/uart.py +9 -26
- shepherd_core/fw_tools/__init__.py +1 -3
- shepherd_core/fw_tools/converter.py +4 -12
- shepherd_core/fw_tools/patcher.py +4 -12
- shepherd_core/fw_tools/validation.py +1 -2
- shepherd_core/inventory/__init__.py +53 -9
- shepherd_core/inventory/system.py +10 -5
- shepherd_core/logger.py +1 -3
- shepherd_core/reader.py +45 -57
- shepherd_core/testbed_client/cache_path.py +15 -0
- shepherd_core/testbed_client/client.py +7 -19
- shepherd_core/testbed_client/fixtures.py +8 -15
- shepherd_core/testbed_client/user_model.py +7 -7
- shepherd_core/vsource/virtual_converter_model.py +5 -15
- shepherd_core/vsource/virtual_harvester_model.py +2 -3
- shepherd_core/vsource/virtual_source_model.py +3 -6
- shepherd_core/writer.py +16 -24
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +50 -38
- shepherd_core-2024.4.1.dist-info/RECORD +64 -0
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
- {shepherd_core-2023.11.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.11.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 -288
- tests/data_models/test_examples.py +0 -48
- tests/data_models/test_experiment_models.py +0 -279
- 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 -22
- tests/test_cal_hw.py +0 -38
- 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 -51
- tests/vsource/test_converter.py +0 -165
- tests/vsource/test_harvester.py +0 -79
- tests/vsource/test_z.py +0 -5
- {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/zip-safe +0 -0
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
|
|
@@ -95,9 +96,7 @@ class Reader:
|
|
|
95
96
|
self.h5file = h5py.File(self.file_path, "r") # = readonly
|
|
96
97
|
self._reader_opened = True
|
|
97
98
|
except OSError as _xcp:
|
|
98
|
-
raise TypeError(
|
|
99
|
-
f"Unable to open HDF5-File '{self.file_path.name}'"
|
|
100
|
-
) from _xcp
|
|
99
|
+
raise TypeError(f"Unable to open HDF5-File '{self.file_path.name}'") from _xcp
|
|
101
100
|
|
|
102
101
|
if self.is_valid():
|
|
103
102
|
self._logger.debug("File is available now")
|
|
@@ -109,9 +108,7 @@ class Reader:
|
|
|
109
108
|
)
|
|
110
109
|
|
|
111
110
|
if not isinstance(self.h5file, h5py.File):
|
|
112
|
-
raise TypeError(
|
|
113
|
-
"Type of opened file is not h5py.File, for %s", self.file_path.name
|
|
114
|
-
)
|
|
111
|
+
raise TypeError("Type of opened file is not h5py.File, for %s", self.file_path.name)
|
|
115
112
|
|
|
116
113
|
self.ds_time: h5py.Dataset = self.h5file["data"]["time"]
|
|
117
114
|
self.ds_voltage: h5py.Dataset = self.h5file["data"]["voltage"]
|
|
@@ -120,9 +117,7 @@ class Reader:
|
|
|
120
117
|
# retrieve cal-data
|
|
121
118
|
if not hasattr(self, "_cal"):
|
|
122
119
|
cal_dict = CalibrationSeries().model_dump()
|
|
123
|
-
for ds, param in product(
|
|
124
|
-
["current", "voltage", "time"], ["gain", "offset"]
|
|
125
|
-
):
|
|
120
|
+
for ds, param in product(["current", "voltage", "time"], ["gain", "offset"]):
|
|
126
121
|
cal_dict[ds][param] = self.h5file["data"][ds].attrs[param]
|
|
127
122
|
self._cal = CalibrationSeries(**cal_dict)
|
|
128
123
|
|
|
@@ -164,15 +159,14 @@ class Reader:
|
|
|
164
159
|
)
|
|
165
160
|
|
|
166
161
|
def _refresh_file_stats(self) -> None:
|
|
167
|
-
"""
|
|
162
|
+
"""Update internal states, helpful after resampling or other changes in data-group"""
|
|
168
163
|
self.h5file.flush()
|
|
169
164
|
if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
)
|
|
165
|
+
# this assumes isochronal sampling
|
|
166
|
+
self.sample_interval_s = self._cal.time.raw_to_si(self.ds_time[1] - self.ds_time[0])
|
|
173
167
|
self.sample_interval_ns = int(10**9 * self.sample_interval_s)
|
|
174
168
|
self.samplerate_sps = max(int(10**9 // self.sample_interval_ns), 1)
|
|
175
|
-
self.runtime_s = round(self.
|
|
169
|
+
self.runtime_s = round(self.ds_voltage.shape[0] / self.samplerate_sps, 1)
|
|
176
170
|
if isinstance(self.file_path, Path):
|
|
177
171
|
self.file_size = self.file_path.stat().st_size
|
|
178
172
|
else:
|
|
@@ -185,35 +179,40 @@ class Reader:
|
|
|
185
179
|
end_n: Optional[int] = None,
|
|
186
180
|
*,
|
|
187
181
|
is_raw: bool = False,
|
|
182
|
+
omit_ts: bool = False,
|
|
188
183
|
) -> Generator[tuple, None, None]:
|
|
189
184
|
"""Generator that reads the specified range of buffers from the hdf5 file.
|
|
190
185
|
can be configured on first call
|
|
191
|
-
TODO: reconstruct - start/end mark samples
|
|
192
|
-
|
|
186
|
+
TODO: reconstruct - start/end mark samples &
|
|
187
|
+
each call can request a certain number of samples
|
|
193
188
|
|
|
194
189
|
Args:
|
|
190
|
+
----
|
|
195
191
|
:param start_n: (int) Index of first buffer to be read
|
|
196
192
|
:param end_n: (int) Index of last buffer to be read
|
|
197
193
|
:param is_raw: (bool) output original data, not transformed to SI-Units
|
|
194
|
+
:param omit_ts: (bool) optimize reading if timestamp is never used
|
|
198
195
|
Yields: Buffers between start and end (tuple with time, voltage, current)
|
|
196
|
+
|
|
199
197
|
"""
|
|
200
198
|
if end_n is None:
|
|
201
|
-
end_n = int(self.
|
|
199
|
+
end_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
|
|
202
200
|
self._logger.debug("Reading blocks %d to %d from source-file", start_n, end_n)
|
|
203
201
|
_raw = is_raw
|
|
202
|
+
_wts = not omit_ts
|
|
204
203
|
|
|
205
204
|
for i in range(start_n, end_n):
|
|
206
205
|
idx_start = i * self.samples_per_buffer
|
|
207
206
|
idx_end = idx_start + self.samples_per_buffer
|
|
208
207
|
if _raw:
|
|
209
208
|
yield (
|
|
210
|
-
self.ds_time[idx_start:idx_end],
|
|
209
|
+
self.ds_time[idx_start:idx_end] if _wts else None,
|
|
211
210
|
self.ds_voltage[idx_start:idx_end],
|
|
212
211
|
self.ds_current[idx_start:idx_end],
|
|
213
212
|
)
|
|
214
213
|
else:
|
|
215
214
|
yield (
|
|
216
|
-
self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]),
|
|
215
|
+
self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]) if _wts else None,
|
|
217
216
|
self._cal.voltage.raw_to_si(self.ds_voltage[idx_start:idx_end]),
|
|
218
217
|
self._cal.current.raw_to_si(self.ds_current[idx_start:idx_end]),
|
|
219
218
|
)
|
|
@@ -254,7 +253,7 @@ class Reader:
|
|
|
254
253
|
return None
|
|
255
254
|
|
|
256
255
|
def get_hrv_config(self) -> dict:
|
|
257
|
-
"""
|
|
256
|
+
"""Essential info for harvester
|
|
258
257
|
:return: config-dict directly for vHarvester to be used during emulation
|
|
259
258
|
"""
|
|
260
259
|
return {
|
|
@@ -263,7 +262,7 @@ class Reader:
|
|
|
263
262
|
}
|
|
264
263
|
|
|
265
264
|
def is_valid(self) -> bool:
|
|
266
|
-
"""
|
|
265
|
+
"""Checks file for plausibility
|
|
267
266
|
|
|
268
267
|
:return: state of validity
|
|
269
268
|
"""
|
|
@@ -339,20 +338,20 @@ class Reader:
|
|
|
339
338
|
self.file_path.name,
|
|
340
339
|
)
|
|
341
340
|
# same length of datasets:
|
|
342
|
-
|
|
343
|
-
for dset in ["current", "
|
|
341
|
+
ds_volt_size = self.h5file["data"]["voltage"].shape[0]
|
|
342
|
+
for dset in ["current", "time"]:
|
|
344
343
|
ds_size = self.h5file["data"][dset].shape[0]
|
|
345
|
-
if
|
|
344
|
+
if ds_volt_size != ds_size:
|
|
346
345
|
self._logger.warning(
|
|
347
346
|
"[FileValidation] dataset '%s' has different size (=%d), "
|
|
348
347
|
"compared to time-ds (=%d), in '%s'",
|
|
349
348
|
dset,
|
|
350
349
|
ds_size,
|
|
351
|
-
|
|
350
|
+
ds_volt_size,
|
|
352
351
|
self.file_path.name,
|
|
353
352
|
)
|
|
354
353
|
# dataset-length should be multiple of buffersize
|
|
355
|
-
remaining_size =
|
|
354
|
+
remaining_size = ds_volt_size % self.samples_per_buffer
|
|
356
355
|
if remaining_size != 0:
|
|
357
356
|
self._logger.warning(
|
|
358
357
|
"[FileValidation] datasets are not aligned with buffer-size in '%s'",
|
|
@@ -391,7 +390,7 @@ class Reader:
|
|
|
391
390
|
return True
|
|
392
391
|
|
|
393
392
|
def __getitem__(self, key: str) -> Any:
|
|
394
|
-
"""
|
|
393
|
+
"""Returns attribute or (if none found) a handle for a group or dataset (if found)
|
|
395
394
|
|
|
396
395
|
:param key: attribute, group, dataset
|
|
397
396
|
:return: value of that key, or handle of object
|
|
@@ -403,16 +402,16 @@ class Reader:
|
|
|
403
402
|
raise KeyError
|
|
404
403
|
|
|
405
404
|
def energy(self) -> float:
|
|
406
|
-
"""
|
|
405
|
+
"""Determine the recorded energy of the trace
|
|
407
406
|
# multiprocessing: https://stackoverflow.com/a/71898911
|
|
408
407
|
# -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool
|
|
409
408
|
|
|
410
409
|
:return: sampled energy in Ws (watt-seconds)
|
|
411
410
|
"""
|
|
412
|
-
iterations = math.ceil(self.
|
|
411
|
+
iterations = math.ceil(self.ds_voltage.shape[0] / self.max_elements)
|
|
413
412
|
job_iter = trange(
|
|
414
413
|
0,
|
|
415
|
-
self.
|
|
414
|
+
self.ds_voltage.shape[0],
|
|
416
415
|
self.max_elements,
|
|
417
416
|
desc="energy",
|
|
418
417
|
leave=False,
|
|
@@ -420,7 +419,7 @@ class Reader:
|
|
|
420
419
|
)
|
|
421
420
|
|
|
422
421
|
def _calc_energy(idx_start: int) -> float:
|
|
423
|
-
idx_stop = min(idx_start + self.max_elements, self.
|
|
422
|
+
idx_stop = min(idx_start + self.max_elements, self.ds_voltage.shape[0])
|
|
424
423
|
vol_v = self._cal.voltage.raw_to_si(self.ds_voltage[idx_start:idx_stop])
|
|
425
424
|
cur_a = self._cal.current.raw_to_si(self.ds_current[idx_start:idx_stop])
|
|
426
425
|
return (vol_v[:] * cur_a[:]).sum() * self.sample_interval_s
|
|
@@ -431,7 +430,7 @@ class Reader:
|
|
|
431
430
|
def _dset_statistics(
|
|
432
431
|
self, dset: h5py.Dataset, cal: Optional[CalibrationPair] = None
|
|
433
432
|
) -> Dict[str, float]:
|
|
434
|
-
"""
|
|
433
|
+
"""Some basic stats for a provided dataset
|
|
435
434
|
:param dset: dataset to evaluate
|
|
436
435
|
:param cal: calibration (if wanted)
|
|
437
436
|
:return: dict with entries for mean, min, max, std
|
|
@@ -439,9 +438,7 @@ class Reader:
|
|
|
439
438
|
si_converted = True
|
|
440
439
|
if not isinstance(cal, CalibrationPair):
|
|
441
440
|
if "gain" in dset.attrs and "offset" in dset.attrs:
|
|
442
|
-
cal = CalibrationPair(
|
|
443
|
-
gain=dset.attrs["gain"], offset=dset.attrs["offset"]
|
|
444
|
-
)
|
|
441
|
+
cal = CalibrationPair(gain=dset.attrs["gain"], offset=dset.attrs["offset"])
|
|
445
442
|
else:
|
|
446
443
|
cal = CalibrationPair(gain=1)
|
|
447
444
|
si_converted = False
|
|
@@ -459,8 +456,7 @@ class Reader:
|
|
|
459
456
|
return [np.mean(data), np.min(data), np.max(data), np.std(data)]
|
|
460
457
|
|
|
461
458
|
stats_list = [
|
|
462
|
-
_calc_statistics(cal.raw_to_si(dset[i : i + self.max_elements]))
|
|
463
|
-
for i in job_iter
|
|
459
|
+
_calc_statistics(cal.raw_to_si(dset[i : i + self.max_elements])) for i in job_iter
|
|
464
460
|
]
|
|
465
461
|
if len(stats_list) < 1:
|
|
466
462
|
return {}
|
|
@@ -476,7 +472,7 @@ class Reader:
|
|
|
476
472
|
return stats
|
|
477
473
|
|
|
478
474
|
def _data_timediffs(self) -> List[float]:
|
|
479
|
-
"""
|
|
475
|
+
"""Calculate list of (unique) time-deltas between buffers [s]
|
|
480
476
|
-> optimized version that only looks at the start of each buffer
|
|
481
477
|
|
|
482
478
|
:return: list of (unique) time-deltas between buffers [s]
|
|
@@ -507,7 +503,7 @@ class Reader:
|
|
|
507
503
|
return list(diffs)
|
|
508
504
|
|
|
509
505
|
def check_timediffs(self) -> bool:
|
|
510
|
-
"""
|
|
506
|
+
"""Validate equal time-deltas
|
|
511
507
|
-> unexpected time-jumps hint at a corrupted file or faulty measurement
|
|
512
508
|
|
|
513
509
|
:return: True if OK
|
|
@@ -519,9 +515,7 @@ class Reader:
|
|
|
519
515
|
)
|
|
520
516
|
return (len(diffs) <= 1) and diffs[0] == round(0.1 / self.samples_per_buffer, 6)
|
|
521
517
|
|
|
522
|
-
def count_errors_in_log(
|
|
523
|
-
self, group_name: str = "sheep", min_level: int = 40
|
|
524
|
-
) -> int:
|
|
518
|
+
def count_errors_in_log(self, group_name: str = "sheep", min_level: int = 40) -> int:
|
|
525
519
|
if group_name not in self.h5file:
|
|
526
520
|
return 0
|
|
527
521
|
if "level" not in self.h5file["sheep"]:
|
|
@@ -538,7 +532,7 @@ class Reader:
|
|
|
538
532
|
*,
|
|
539
533
|
minimal: bool = False,
|
|
540
534
|
) -> Dict[str, dict]:
|
|
541
|
-
"""
|
|
535
|
+
"""Recursive FN to capture the structure of the file
|
|
542
536
|
:param node: starting node, leave free to go through whole file
|
|
543
537
|
:param minimal: just provide a bare tree (much faster)
|
|
544
538
|
:return: structure of that node with everything inside it
|
|
@@ -589,7 +583,7 @@ class Reader:
|
|
|
589
583
|
return metadata
|
|
590
584
|
|
|
591
585
|
def save_metadata(self, node: Union[h5py.Dataset, h5py.Group, None] = None) -> dict:
|
|
592
|
-
"""
|
|
586
|
+
"""Get structure of file and dump content to yaml-file with same name as original
|
|
593
587
|
|
|
594
588
|
:param node: starting node, leave free to go through whole file
|
|
595
589
|
:return: structure of that node with everything inside it
|
|
@@ -599,9 +593,7 @@ class Reader:
|
|
|
599
593
|
if yaml_path.exists():
|
|
600
594
|
self._logger.info("File already exists, will skip '%s'", yaml_path.name)
|
|
601
595
|
return {}
|
|
602
|
-
metadata = self.get_metadata(
|
|
603
|
-
node
|
|
604
|
-
) # {"h5root": self.get_metadata(self.h5file)}
|
|
596
|
+
metadata = self.get_metadata(node) # {"h5root": self.get_metadata(self.h5file)}
|
|
605
597
|
with yaml_path.open("w", encoding="utf-8-sig") as yfd:
|
|
606
598
|
yaml.safe_dump(metadata, yfd, default_flow_style=False, sort_keys=False)
|
|
607
599
|
else:
|
|
@@ -620,7 +612,7 @@ class Reader:
|
|
|
620
612
|
|
|
621
613
|
@staticmethod
|
|
622
614
|
def get_filter_for_redundant_states(data: np.ndarray) -> np.ndarray:
|
|
623
|
-
"""
|
|
615
|
+
"""Input is 1D state-vector, kep only first from identical & sequential states
|
|
624
616
|
algo: create an offset-by-one vector and compare against original
|
|
625
617
|
"""
|
|
626
618
|
if len(data.shape) > 1:
|
|
@@ -637,9 +629,7 @@ class Reader:
|
|
|
637
629
|
gpio_vs = self.h5file["gpio"]["value"]
|
|
638
630
|
|
|
639
631
|
if name is None:
|
|
640
|
-
descriptions = yaml.safe_load(
|
|
641
|
-
self.h5file["gpio"]["value"].attrs["description"]
|
|
642
|
-
)
|
|
632
|
+
descriptions = yaml.safe_load(self.h5file["gpio"]["value"].attrs["description"])
|
|
643
633
|
pin_dict = {value["name"]: key for key, value in descriptions.items()}
|
|
644
634
|
else:
|
|
645
635
|
pin_dict = {name: self.get_gpio_pin_num(name)}
|
|
@@ -655,9 +645,7 @@ class Reader:
|
|
|
655
645
|
)
|
|
656
646
|
return waveforms
|
|
657
647
|
|
|
658
|
-
def waveform_to_csv(
|
|
659
|
-
self, pin_name: str, pin_wf: np.ndarray, separator: str = ","
|
|
660
|
-
) -> None:
|
|
648
|
+
def waveform_to_csv(self, pin_name: str, pin_wf: np.ndarray, separator: str = ",") -> None:
|
|
661
649
|
path_csv = self.file_path.with_suffix(f".waveform.{pin_name}.csv")
|
|
662
650
|
if path_csv.exists():
|
|
663
651
|
self._logger.info("File already exists, will skip '%s'", path_csv.name)
|
|
@@ -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"
|
|
@@ -18,9 +18,7 @@ from .user_model import User
|
|
|
18
18
|
class TestbedClient:
|
|
19
19
|
_instance: Optional[Self] = None
|
|
20
20
|
|
|
21
|
-
def __init__(
|
|
22
|
-
self, server: Optional[str] = None, token: Union[str, Path, None] = None
|
|
23
|
-
) -> None:
|
|
21
|
+
def __init__(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> None:
|
|
24
22
|
if not hasattr(self, "_token"):
|
|
25
23
|
self._token: str = "null"
|
|
26
24
|
self._server: Optional[str] = testbed_server_default
|
|
@@ -42,11 +40,8 @@ class TestbedClient:
|
|
|
42
40
|
TestbedClient._instance = None
|
|
43
41
|
|
|
44
42
|
@validate_call
|
|
45
|
-
def connect(
|
|
46
|
-
|
|
47
|
-
) -> bool:
|
|
48
|
-
"""
|
|
49
|
-
server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
|
|
43
|
+
def connect(self, server: Optional[str] = None, token: Union[str, Path, None] = None) -> bool:
|
|
44
|
+
"""server: either "local" to use demo-fixtures or something like "https://HOST:PORT"
|
|
50
45
|
token: your account validation
|
|
51
46
|
"""
|
|
52
47
|
if isinstance(token, Path):
|
|
@@ -74,9 +69,7 @@ class TestbedClient:
|
|
|
74
69
|
parameters=data.model_dump(),
|
|
75
70
|
)
|
|
76
71
|
if self._connected:
|
|
77
|
-
r = self._req.post(
|
|
78
|
-
self._server + "/add", data=wrap.model_dump_json(), timeout=2
|
|
79
|
-
)
|
|
72
|
+
r = self._req.post(self._server + "/add", data=wrap.model_dump_json(), timeout=2)
|
|
80
73
|
r.raise_for_status()
|
|
81
74
|
else:
|
|
82
75
|
self._fixtures.insert_model(wrap)
|
|
@@ -126,7 +119,7 @@ class TestbedClient:
|
|
|
126
119
|
return self._fixtures[model_type].inheritance(values)
|
|
127
120
|
|
|
128
121
|
def try_completing_model(self, model_type: str, values: dict) -> (dict, list):
|
|
129
|
-
"""
|
|
122
|
+
"""Init by name/id, for none existing instances raise Exception"""
|
|
130
123
|
if len(values) == 1 and next(iter(values.keys())) in {"id", "name"}:
|
|
131
124
|
value = next(iter(values.values()))
|
|
132
125
|
if (
|
|
@@ -134,16 +127,11 @@ class TestbedClient:
|
|
|
134
127
|
and value.lower() in self._fixtures[model_type].elements_by_name
|
|
135
128
|
):
|
|
136
129
|
values = self.query_item(model_type, name=value)
|
|
137
|
-
elif (
|
|
138
|
-
isinstance(value, int)
|
|
139
|
-
and value in self._fixtures[model_type].elements_by_id
|
|
140
|
-
):
|
|
130
|
+
elif isinstance(value, int) and value in self._fixtures[model_type].elements_by_id:
|
|
141
131
|
# TODO: still depending on _fixture
|
|
142
132
|
values = self.query_item(model_type, uid=value)
|
|
143
133
|
else:
|
|
144
|
-
raise ValueError(
|
|
145
|
-
f"Query {model_type} by name / ID failed - " f"{values} is unknown!"
|
|
146
|
-
)
|
|
134
|
+
raise ValueError(f"Query {model_type} by name / ID failed - {values} is unknown!")
|
|
147
135
|
return self.try_inheritance(model_type, values)
|
|
148
136
|
|
|
149
137
|
def fill_in_user_data(self, values: dict) -> dict:
|
|
@@ -18,6 +18,7 @@ from ..data_models.base.timezone import local_now
|
|
|
18
18
|
from ..data_models.base.timezone import local_tz
|
|
19
19
|
from ..data_models.base.wrapper import Wrapper
|
|
20
20
|
from ..logger import logger
|
|
21
|
+
from .cache_path import cache_user_path
|
|
21
22
|
|
|
22
23
|
# Proposed field-name:
|
|
23
24
|
# - inheritance
|
|
@@ -148,16 +149,12 @@ class Fixture:
|
|
|
148
149
|
def query_id(self, _id: int) -> dict:
|
|
149
150
|
if isinstance(_id, int) and _id in self.elements_by_id:
|
|
150
151
|
return self.elements_by_id[_id]
|
|
151
|
-
raise ValueError(
|
|
152
|
-
f"Initialization of {self.model_type} by ID failed - {_id} is unknown!"
|
|
153
|
-
)
|
|
152
|
+
raise ValueError(f"Initialization of {self.model_type} by ID failed - {_id} is unknown!")
|
|
154
153
|
|
|
155
154
|
def query_name(self, name: str) -> dict:
|
|
156
155
|
if isinstance(name, str) and name.lower() in self.elements_by_name:
|
|
157
156
|
return self.elements_by_name[name.lower()]
|
|
158
|
-
raise ValueError(
|
|
159
|
-
f"Initialization of {self.model_type} by name failed - {name} is unknown!"
|
|
160
|
-
)
|
|
157
|
+
raise ValueError(f"Initialization of {self.model_type} by name failed - {name} is unknown!")
|
|
161
158
|
|
|
162
159
|
|
|
163
160
|
def file_older_than(file: Path, delta: timedelta) -> bool:
|
|
@@ -172,22 +169,17 @@ class Fixtures:
|
|
|
172
169
|
suffix = ".yaml"
|
|
173
170
|
|
|
174
171
|
@validate_call
|
|
175
|
-
def __init__(
|
|
176
|
-
self, file_path: Optional[Path] = None, *, reset: bool = False
|
|
177
|
-
) -> None:
|
|
172
|
+
def __init__(self, file_path: Optional[Path] = None, *, reset: bool = False) -> None:
|
|
178
173
|
if file_path is None:
|
|
179
174
|
self.file_path = Path(__file__).parent.parent.resolve() / "data_models"
|
|
180
175
|
else:
|
|
181
176
|
self.file_path = file_path
|
|
182
177
|
self.components: Dict[str, Fixture] = {}
|
|
183
|
-
save_path =
|
|
178
|
+
save_path = cache_user_path / "fixtures.pickle"
|
|
184
179
|
|
|
185
|
-
if (
|
|
186
|
-
save_path.exists()
|
|
187
|
-
and not file_older_than(save_path, timedelta(hours=24))
|
|
188
|
-
and not reset
|
|
189
|
-
):
|
|
180
|
+
if save_path.exists() and not file_older_than(save_path, timedelta(hours=24)) and not reset:
|
|
190
181
|
# speedup
|
|
182
|
+
# TODO: also add version as criterion
|
|
191
183
|
with save_path.open("rb", buffering=-1) as fd:
|
|
192
184
|
self.components = pickle.load(fd) # noqa: S301
|
|
193
185
|
logger.debug(" -> found & used pickled fixtures")
|
|
@@ -202,6 +194,7 @@ class Fixtures:
|
|
|
202
194
|
for file in files:
|
|
203
195
|
self.insert_file(file)
|
|
204
196
|
|
|
197
|
+
save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
198
|
with save_path.open("wb", buffering=-1) as fd:
|
|
206
199
|
pickle.dump(self.components, fd)
|
|
207
200
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import secrets
|
|
2
2
|
from hashlib import pbkdf2_hmac
|
|
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 EmailStr
|
|
6
9
|
from pydantic import Field
|
|
7
10
|
from pydantic import SecretBytes
|
|
@@ -11,17 +14,13 @@ from pydantic import model_validator
|
|
|
11
14
|
from pydantic import validate_call
|
|
12
15
|
from typing_extensions import Annotated
|
|
13
16
|
|
|
14
|
-
from ..data_models.base.content import IdInt
|
|
15
17
|
from ..data_models.base.content import NameStr
|
|
16
18
|
from ..data_models.base.content import SafeStr
|
|
17
|
-
from ..data_models.base.content import id_default
|
|
18
19
|
from ..data_models.base.shepherd import ShpModel
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@validate_call
|
|
22
|
-
def hash_password(
|
|
23
|
-
pw: Annotated[str, StringConstraints(min_length=20, max_length=100)]
|
|
24
|
-
) -> bytes:
|
|
23
|
+
def hash_password(pw: Annotated[str, StringConstraints(min_length=20, max_length=100)]) -> bytes:
|
|
25
24
|
# TODO: add salt of testbed -> this fn should be part of Testbed-Object
|
|
26
25
|
# NOTE: 1M Iterations need 25s on beaglebone
|
|
27
26
|
return pbkdf2_hmac(
|
|
@@ -36,9 +35,10 @@ def hash_password(
|
|
|
36
35
|
class User(ShpModel):
|
|
37
36
|
"""meta-data representation of a testbed-component (physical object)"""
|
|
38
37
|
|
|
39
|
-
id:
|
|
38
|
+
# id: UUID4 = Field( # TODO db-migration - temp fix for documentation
|
|
39
|
+
id: Union[UUID4, int] = Field(
|
|
40
40
|
description="Unique ID",
|
|
41
|
-
default_factory=
|
|
41
|
+
default_factory=uuid4,
|
|
42
42
|
)
|
|
43
43
|
name: NameStr
|
|
44
44
|
description: Optional[SafeStr] = None
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
NOTE: DO NOT OPTIMIZE -> stay close to original code-base
|
|
7
7
|
|
|
8
8
|
Compromises:
|
|
9
|
-
- bitshifted values (
|
|
9
|
+
- bitshifted values (i.e. _n28) are converted to float without shift
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
@@ -52,9 +52,7 @@ class VirtualConverterModel:
|
|
|
52
52
|
self.V_input_uV: float = 0.0
|
|
53
53
|
self.P_inp_fW: float = 0.0
|
|
54
54
|
self.P_out_fW: float = 0.0
|
|
55
|
-
self.interval_startup_disabled_drain_n: int =
|
|
56
|
-
self._cfg.interval_startup_delay_drain_n
|
|
57
|
-
)
|
|
55
|
+
self.interval_startup_disabled_drain_n: int = self._cfg.interval_startup_delay_drain_n
|
|
58
56
|
|
|
59
57
|
# container for the stored energy
|
|
60
58
|
self.V_mid_uV: float = self._cfg.V_intermediate_init_uV
|
|
@@ -71,12 +69,8 @@ class VirtualConverterModel:
|
|
|
71
69
|
|
|
72
70
|
# prepare hysteresis-thresholds
|
|
73
71
|
self.dV_enable_output_uV: float = self._cfg.dV_enable_output_uV
|
|
74
|
-
self.V_enable_output_threshold_uV: float =
|
|
75
|
-
|
|
76
|
-
)
|
|
77
|
-
self.V_disable_output_threshold_uV: float = (
|
|
78
|
-
self._cfg.V_disable_output_threshold_uV
|
|
79
|
-
)
|
|
72
|
+
self.V_enable_output_threshold_uV: float = self._cfg.V_enable_output_threshold_uV
|
|
73
|
+
self.V_disable_output_threshold_uV: float = self._cfg.V_disable_output_threshold_uV
|
|
80
74
|
|
|
81
75
|
if self.dV_enable_output_uV > self.V_enable_output_threshold_uV:
|
|
82
76
|
self.V_enable_output_threshold_uV = self.dV_enable_output_uV
|
|
@@ -163,11 +157,7 @@ class VirtualConverterModel:
|
|
|
163
157
|
|
|
164
158
|
if self.V_mid_uV > self._cfg.V_intermediate_max_uV:
|
|
165
159
|
self.V_mid_uV = self._cfg.V_intermediate_max_uV
|
|
166
|
-
if (
|
|
167
|
-
(not self.enable_boost)
|
|
168
|
-
and (self.P_inp_fW > 0.0)
|
|
169
|
-
and (self.V_mid_uV > self.V_input_uV)
|
|
170
|
-
):
|
|
160
|
+
if (not self.enable_boost) and (self.P_inp_fW > 0.0) and (self.V_mid_uV > self.V_input_uV):
|
|
171
161
|
# TODO: obfuscated - no "direct connection"?
|
|
172
162
|
self.V_mid_uV = self.V_input_uV
|
|
173
163
|
elif self.V_mid_uV < 1:
|
|
@@ -11,6 +11,7 @@ Compromises:
|
|
|
11
11
|
- Python has no static vars -> FName_reset is handling the class-vars
|
|
12
12
|
|
|
13
13
|
"""
|
|
14
|
+
|
|
14
15
|
from typing import Tuple
|
|
15
16
|
|
|
16
17
|
from ..data_models.content.virtual_harvester import HarvesterPRUConfig
|
|
@@ -93,9 +94,7 @@ class VirtualHarvesterModel:
|
|
|
93
94
|
if distance_now < distance_last and distance_now < self.voltage_step_x4_uV:
|
|
94
95
|
self.voltage_hold = _voltage_uV
|
|
95
96
|
self.current_hold = _current_nA
|
|
96
|
-
elif
|
|
97
|
-
distance_last < distance_now and distance_last < self.voltage_step_x4_uV
|
|
98
|
-
):
|
|
97
|
+
elif distance_last < distance_now and distance_last < self.voltage_step_x4_uV:
|
|
99
98
|
self.voltage_hold = self.voltage_last
|
|
100
99
|
self.current_hold = self.current_last
|
|
101
100
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
NOTE: DO NOT OPTIMIZE -> stay close to original code-base
|
|
7
7
|
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from typing import Optional
|
|
10
11
|
|
|
11
12
|
from ..data_models import CalibrationEmulator
|
|
@@ -37,9 +38,7 @@ class VirtualSourceModel:
|
|
|
37
38
|
cnv_config = ConverterPRUConfig.from_vsrc(
|
|
38
39
|
self.cfg_src, log_intermediate_node=log_intermediate
|
|
39
40
|
)
|
|
40
|
-
self.cnv: VirtualConverterModel = VirtualConverterModel(
|
|
41
|
-
cnv_config, self._cal_pru
|
|
42
|
-
)
|
|
41
|
+
self.cnv: VirtualConverterModel = VirtualConverterModel(cnv_config, self._cal_pru)
|
|
43
42
|
|
|
44
43
|
hrv_config = HarvesterPRUConfig.from_vhrv(
|
|
45
44
|
self.cfg_src.harvester,
|
|
@@ -53,9 +52,7 @@ class VirtualSourceModel:
|
|
|
53
52
|
self.W_inp_fWs: float = 0.0
|
|
54
53
|
self.W_out_fWs: float = 0.0
|
|
55
54
|
|
|
56
|
-
def iterate_sampling(
|
|
57
|
-
self, V_inp_uV: int = 0, I_inp_nA: int = 0, I_out_nA: int = 0
|
|
58
|
-
) -> int:
|
|
55
|
+
def iterate_sampling(self, V_inp_uV: int = 0, I_inp_nA: int = 0, I_out_nA: int = 0) -> int:
|
|
59
56
|
"""TEST-SIMPLIFICATION - code below is not part of pru-code,
|
|
60
57
|
but in part sample_emulator() in sampling.c
|
|
61
58
|
|