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.
Files changed (99) hide show
  1. shepherd_core/__init__.py +3 -2
  2. shepherd_core/data_models/base/calibration.py +3 -1
  3. shepherd_core/data_models/base/content.py +13 -11
  4. shepherd_core/data_models/base/shepherd.py +4 -6
  5. shepherd_core/data_models/content/energy_environment.py +1 -1
  6. shepherd_core/data_models/content/virtual_harvester.py +1 -1
  7. shepherd_core/data_models/content/virtual_source.py +31 -53
  8. shepherd_core/data_models/experiment/experiment.py +13 -18
  9. shepherd_core/data_models/experiment/observer_features.py +9 -15
  10. shepherd_core/data_models/experiment/target_config.py +4 -11
  11. shepherd_core/data_models/task/__init__.py +2 -6
  12. shepherd_core/data_models/task/emulation.py +7 -14
  13. shepherd_core/data_models/task/firmware_mod.py +1 -3
  14. shepherd_core/data_models/task/harvest.py +1 -3
  15. shepherd_core/data_models/task/observer_tasks.py +1 -1
  16. shepherd_core/data_models/task/testbed_tasks.py +7 -3
  17. shepherd_core/data_models/testbed/cape.py +1 -1
  18. shepherd_core/data_models/testbed/gpio.py +1 -1
  19. shepherd_core/data_models/testbed/mcu.py +1 -1
  20. shepherd_core/data_models/testbed/observer.py +7 -23
  21. shepherd_core/data_models/testbed/target.py +1 -1
  22. shepherd_core/data_models/testbed/testbed.py +2 -4
  23. shepherd_core/decoder_waveform/uart.py +9 -26
  24. shepherd_core/fw_tools/__init__.py +1 -3
  25. shepherd_core/fw_tools/converter.py +4 -12
  26. shepherd_core/fw_tools/patcher.py +4 -12
  27. shepherd_core/fw_tools/validation.py +1 -2
  28. shepherd_core/inventory/__init__.py +53 -9
  29. shepherd_core/inventory/system.py +10 -5
  30. shepherd_core/logger.py +1 -3
  31. shepherd_core/reader.py +45 -57
  32. shepherd_core/testbed_client/cache_path.py +15 -0
  33. shepherd_core/testbed_client/client.py +7 -19
  34. shepherd_core/testbed_client/fixtures.py +8 -15
  35. shepherd_core/testbed_client/user_model.py +7 -7
  36. shepherd_core/vsource/virtual_converter_model.py +5 -15
  37. shepherd_core/vsource/virtual_harvester_model.py +2 -3
  38. shepherd_core/vsource/virtual_source_model.py +3 -6
  39. shepherd_core/writer.py +16 -24
  40. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/METADATA +50 -38
  41. shepherd_core-2024.4.1.dist-info/RECORD +64 -0
  42. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/WHEEL +1 -1
  43. {shepherd_core-2023.11.1.dist-info → shepherd_core-2024.4.1.dist-info}/top_level.txt +0 -1
  44. shepherd_core/data_models/content/_external_fixtures.yaml +0 -394
  45. shepherd_core/data_models/content/energy_environment_fixture.yaml +0 -50
  46. shepherd_core/data_models/content/virtual_harvester_fixture.yaml +0 -159
  47. shepherd_core/data_models/content/virtual_source_fixture.yaml +0 -229
  48. shepherd_core/data_models/testbed/cape_fixture.yaml +0 -94
  49. shepherd_core/data_models/testbed/gpio_fixture.yaml +0 -166
  50. shepherd_core/data_models/testbed/mcu_fixture.yaml +0 -19
  51. shepherd_core/data_models/testbed/observer_fixture.yaml +0 -220
  52. shepherd_core/data_models/testbed/target_fixture.yaml +0 -137
  53. shepherd_core/data_models/testbed/testbed_fixture.yaml +0 -25
  54. shepherd_core-2023.11.1.dist-info/RECORD +0 -117
  55. tests/__init__.py +0 -0
  56. tests/conftest.py +0 -64
  57. tests/data_models/__init__.py +0 -0
  58. tests/data_models/conftest.py +0 -14
  59. tests/data_models/example_cal_data.yaml +0 -31
  60. tests/data_models/example_cal_data_faulty.yaml +0 -29
  61. tests/data_models/example_cal_meas.yaml +0 -178
  62. tests/data_models/example_cal_meas_faulty1.yaml +0 -142
  63. tests/data_models/example_cal_meas_faulty2.yaml +0 -136
  64. tests/data_models/example_config_emulator.yaml +0 -41
  65. tests/data_models/example_config_experiment.yaml +0 -16
  66. tests/data_models/example_config_experiment_alternative.yaml +0 -14
  67. tests/data_models/example_config_harvester.yaml +0 -15
  68. tests/data_models/example_config_testbed.yaml +0 -26
  69. tests/data_models/example_config_virtsource.yaml +0 -78
  70. tests/data_models/test_base_models.py +0 -205
  71. tests/data_models/test_content_fixtures.py +0 -41
  72. tests/data_models/test_content_models.py +0 -288
  73. tests/data_models/test_examples.py +0 -48
  74. tests/data_models/test_experiment_models.py +0 -279
  75. tests/data_models/test_task_generation.py +0 -52
  76. tests/data_models/test_task_models.py +0 -131
  77. tests/data_models/test_testbed_fixtures.py +0 -47
  78. tests/data_models/test_testbed_models.py +0 -187
  79. tests/decoder_waveform/__init__.py +0 -0
  80. tests/decoder_waveform/test_decoder.py +0 -34
  81. tests/fw_tools/__init__.py +0 -0
  82. tests/fw_tools/conftest.py +0 -5
  83. tests/fw_tools/test_converter.py +0 -76
  84. tests/fw_tools/test_patcher.py +0 -66
  85. tests/fw_tools/test_validation.py +0 -56
  86. tests/inventory/__init__.py +0 -0
  87. tests/inventory/test_inventory.py +0 -22
  88. tests/test_cal_hw.py +0 -38
  89. tests/test_examples.py +0 -40
  90. tests/test_logger.py +0 -15
  91. tests/test_reader.py +0 -283
  92. tests/test_writer.py +0 -169
  93. tests/testbed_client/__init__.py +0 -0
  94. tests/vsource/__init__.py +0 -0
  95. tests/vsource/conftest.py +0 -51
  96. tests/vsource/test_converter.py +0 -165
  97. tests/vsource/test_harvester.py +0 -79
  98. tests/vsource/test_z.py +0 -5
  99. {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
- Reader-Baseclass
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
- """update internal states, helpful after resampling or other changes in data-group"""
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
- self.sample_interval_s = self._cal.time.raw_to_si(
171
- self.ds_time[1] - self.ds_time[0]
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.ds_time.shape[0] / self.samplerate_sps, 1)
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 and
192
- each call can request a certain number of samples
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.ds_time.shape[0] // self.samples_per_buffer)
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
- """essential info for harvester
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
- """checks file for plausibility
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
- ds_time_size = self.h5file["data"]["time"].shape[0]
343
- for dset in ["current", "voltage"]:
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 ds_time_size != ds_size:
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
- ds_time_size,
350
+ ds_volt_size,
352
351
  self.file_path.name,
353
352
  )
354
353
  # dataset-length should be multiple of buffersize
355
- remaining_size = ds_time_size % self.samples_per_buffer
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
- """returns attribute or (if none found) a handle for a group or dataset (if found)
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
- """determine the recorded energy of the trace
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.ds_time.shape[0] / self.max_elements)
411
+ iterations = math.ceil(self.ds_voltage.shape[0] / self.max_elements)
413
412
  job_iter = trange(
414
413
  0,
415
- self.ds_time.shape[0],
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.ds_time.shape[0])
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
- """some basic stats for a provided dataset
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
- """calculate list of (unique) time-deltas between buffers [s]
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
- """validate equal time-deltas
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
- """recursive FN to capture the structure of the file
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
- """get structure of file and dump content to yaml-file with same name as original
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
- """input is 1D state-vector, kep only first from identical & sequential states
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
- self, server: Optional[str] = None, token: Union[str, Path, None] = None
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
- """init by name/id, for none existing instances raise Exception"""
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 = self.file_path / "fixtures.pickle"
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: IdInt = Field( # noqa: A003
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=id_default,
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 (ie. _n28) are converted to float without shift
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
- self._cfg.V_enable_output_threshold_uV
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