shepherd-core 2025.10.1__py3-none-any.whl → 2026.2.3__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 (69) hide show
  1. shepherd_core/config.py +1 -1
  2. shepherd_core/data_models/__init__.py +4 -2
  3. shepherd_core/data_models/base/cal_measurement.py +7 -2
  4. shepherd_core/data_models/base/calibration.py +23 -12
  5. shepherd_core/data_models/base/content.py +10 -2
  6. shepherd_core/data_models/base/shepherd.py +13 -4
  7. shepherd_core/data_models/base/wrapper.py +2 -0
  8. shepherd_core/data_models/content/__init__.py +4 -2
  9. shepherd_core/data_models/content/_external_fixtures.yaml +104 -96
  10. shepherd_core/data_models/content/_metadata_eenvs_bonito.yaml +436 -0
  11. shepherd_core/data_models/content/_metadata_eenvs_synthetic_multivariate_random_walk.yaml +164 -0
  12. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_markov.yaml +3280 -0
  13. shepherd_core/data_models/content/_metadata_eenvs_synthetic_on_off_windows.yaml +3260 -0
  14. shepherd_core/data_models/content/_metadata_eenvs_synthetic_static.yaml +450 -0
  15. shepherd_core/data_models/content/energy_environment.py +341 -23
  16. shepherd_core/data_models/content/energy_environment_fixture.yaml +24 -18
  17. shepherd_core/data_models/content/enum_datatypes.py +109 -0
  18. shepherd_core/data_models/content/firmware.py +50 -18
  19. shepherd_core/data_models/content/virtual_harvester_config.py +10 -93
  20. shepherd_core/data_models/content/virtual_source_config.py +21 -2
  21. shepherd_core/data_models/content/virtual_storage_config.py +7 -4
  22. shepherd_core/data_models/content/virtual_storage_fixture_creator.py +1 -1
  23. shepherd_core/data_models/content/virtual_storage_fixture_param_experiments.py +4 -4
  24. shepherd_core/data_models/experiment/experiment.py +38 -13
  25. shepherd_core/data_models/experiment/observer_features.py +17 -4
  26. shepherd_core/data_models/experiment/target_config.py +55 -7
  27. shepherd_core/data_models/task/__init__.py +13 -2
  28. shepherd_core/data_models/task/emulation.py +9 -5
  29. shepherd_core/data_models/task/firmware_mod.py +3 -1
  30. shepherd_core/data_models/task/harvest.py +2 -0
  31. shepherd_core/data_models/task/helper_paths.py +2 -2
  32. shepherd_core/data_models/task/observer_tasks.py +8 -6
  33. shepherd_core/data_models/task/programming.py +4 -2
  34. shepherd_core/data_models/task/testbed_tasks.py +8 -2
  35. shepherd_core/data_models/testbed/cape.py +2 -0
  36. shepherd_core/data_models/testbed/gpio.py +2 -0
  37. shepherd_core/data_models/testbed/mcu.py +2 -0
  38. shepherd_core/data_models/testbed/observer.py +2 -0
  39. shepherd_core/data_models/testbed/target.py +7 -5
  40. shepherd_core/data_models/testbed/target_fixture.old1 +1 -1
  41. shepherd_core/data_models/testbed/target_fixture.yaml +1 -1
  42. shepherd_core/data_models/testbed/testbed.py +17 -15
  43. shepherd_core/exit_handler.py +22 -0
  44. shepherd_core/fw_tools/converter.py +2 -2
  45. shepherd_core/fw_tools/validation.py +1 -1
  46. shepherd_core/inventory/__init__.py +23 -21
  47. shepherd_core/inventory/system.py +2 -2
  48. shepherd_core/logger.py +0 -1
  49. shepherd_core/reader.py +29 -25
  50. shepherd_core/testbed_client/cache_path.py +3 -3
  51. shepherd_core/testbed_client/client_abc_fix.py +14 -3
  52. shepherd_core/testbed_client/client_web.py +7 -5
  53. shepherd_core/testbed_client/fixtures.py +7 -7
  54. shepherd_core/version.py +1 -1
  55. shepherd_core/vsource/virtual_converter_model.py +2 -2
  56. shepherd_core/vsource/virtual_harvester_model.py +2 -2
  57. shepherd_core/vsource/virtual_harvester_simulation.py +5 -5
  58. shepherd_core/vsource/virtual_source_model.py +1 -1
  59. shepherd_core/vsource/virtual_source_simulation.py +9 -9
  60. shepherd_core/vsource/virtual_storage_models_kibam.py +3 -3
  61. shepherd_core/writer.py +16 -9
  62. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/METADATA +5 -3
  63. shepherd_core-2026.2.3.dist-info/RECORD +102 -0
  64. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/WHEEL +1 -1
  65. shepherd_core-2026.2.3.dist-info/licenses/LICENSE +21 -0
  66. shepherd_core/data_models/content/firmware_datatype.py +0 -15
  67. shepherd_core-2025.10.1.dist-info/RECORD +0 -95
  68. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/top_level.txt +0 -0
  69. {shepherd_core-2025.10.1.dist-info → shepherd_core-2026.2.3.dist-info}/zip-safe +0 -0
@@ -10,6 +10,7 @@ from datetime import datetime
10
10
  from datetime import timedelta
11
11
  from pathlib import Path
12
12
  from typing import Annotated
13
+ from typing import final
13
14
 
14
15
  from pydantic import Field
15
16
  from typing_extensions import Self
@@ -29,6 +30,7 @@ __all__ = [
29
30
  ]
30
31
 
31
32
 
33
+ @final
32
34
  class Inventory(PythonInventory, SystemInventory, TargetInventory):
33
35
  """Complete inventory for one device.
34
36
 
@@ -65,7 +67,7 @@ class InventoryList(ShpModel):
65
67
  np.concatenate(content).reshape((len(content), len(content[0]))).
66
68
  """
67
69
  if path.is_dir():
68
- path = path / "inventory.yaml"
70
+ path /= "inventory.yaml"
69
71
  with path.resolve().open("w") as fd:
70
72
  fd.write(", ".join(self.elements[0].model_dump().keys()) + "\r\n")
71
73
  for item in self.elements:
@@ -75,28 +77,28 @@ class InventoryList(ShpModel):
75
77
 
76
78
  def warn(self) -> dict:
77
79
  warnings = {}
78
- ts_earl = min([_e.created.timestamp() for _e in self.elements])
79
- for _e in self.elements:
80
- if _e.uptime > timedelta(hours=30).total_seconds():
81
- warnings["uptime"] = f"[{self.hostname}] restart is recommended"
82
- if (_e.created.timestamp() - ts_earl) > 10:
83
- warnings["time_delta"] = f"[{self.hostname}] time-sync has failed"
80
+ ts_earl = min(e_.created.timestamp() for e_ in self.elements)
81
+ for e_ in self.elements:
82
+ if e_.uptime > timedelta(hours=30).total_seconds():
83
+ warnings["uptime"] = f"[{e_.hostname}] restart is recommended"
84
+ if (e_.created.timestamp() - ts_earl) > 10:
85
+ warnings["time_delta"] = f"[{e_.hostname}] time-sync has failed"
84
86
 
85
87
  # turn dict[hostname][type] = val
86
88
  # to dict[type][val] = list[hostnames]
87
- _inp = {
88
- _e.hostname: _e.model_dump(exclude_unset=True, exclude_defaults=True)
89
- for _e in self.elements
89
+ inp_ = {
90
+ e_.hostname: e_.model_dump(exclude_unset=True, exclude_defaults=True)
91
+ for e_ in self.elements
90
92
  }
91
93
  result = {}
92
- for _host, _types in _inp.items():
93
- for _type, _val in _types.items():
94
- if _type not in result:
95
- result[_type] = {}
96
- if _val not in result[_type]:
97
- result[_type][_val] = []
98
- result[_type][_val].append(_host)
99
- rescnt = {_key: len(_val) for _key, _val in result.items()}
94
+ for host_, types_ in inp_.items():
95
+ for type_, val_ in types_.items():
96
+ if type_ not in result:
97
+ result[type_] = {}
98
+ if val_ not in result[type_]:
99
+ result[type_][val_] = []
100
+ result[type_][val_].append(host_)
101
+ rescnt = {key_: len(val_) for key_, val_ in result.items()}
100
102
  t_unique = [
101
103
  "h5py",
102
104
  "numpy",
@@ -107,9 +109,9 @@ class InventoryList(ShpModel):
107
109
  "yaml",
108
110
  "zstandard",
109
111
  ]
110
- for _key in t_unique:
111
- if rescnt[_key] > 1:
112
- warnings[_key] = f"[{_key}] VersionMismatch - {result[_key]}"
112
+ for key_ in t_unique:
113
+ if rescnt[key_] > 1:
114
+ warnings[key_] = f"[{key_}] VersionMismatch - {result[key_]}"
113
115
 
114
116
  # TODO: finish with more potential warnings
115
117
  return warnings
@@ -63,12 +63,12 @@ class SystemInventory(ShpModel):
63
63
  uptime = 0
64
64
  log.warning(
65
65
  "Inventory-Parameters will be missing. "
66
- "Please install functionality with "
66
+ "Please install functionality with i.e."
67
67
  "'pip install shepherd_core[inventory] -U' first"
68
68
  )
69
69
  else:
70
70
  ifs1 = psutil.net_if_addrs().items()
71
- ifs2 = {name: (_if[1].address, _if[0].address) for name, _if in ifs1 if len(_if) > 1}
71
+ ifs2 = {name: (if_[1].address, if_[0].address) for name, if_ in ifs1 if len(if_) > 1}
72
72
  uptime = time.time() - psutil.boot_time()
73
73
 
74
74
  fs_cmd = ["/usr/bin/df", "-h", "/"]
shepherd_core/logger.py CHANGED
@@ -1,7 +1,6 @@
1
1
  """Log handler of shepherd."""
2
2
 
3
3
  import logging
4
- import logging.handlers
5
4
 
6
5
  import chromalog
7
6
 
shepherd_core/reader.py CHANGED
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from types import MappingProxyType
14
14
  from typing import TYPE_CHECKING
15
15
  from typing import Any
16
+ from typing import final
16
17
 
17
18
  import h5py
18
19
  import numpy as np
@@ -26,7 +27,7 @@ from .config import config
26
27
  from .data_models.base.calibration import CalibrationPair
27
28
  from .data_models.base.calibration import CalibrationSeries
28
29
  from .data_models.base.timezone import local_tz
29
- from .data_models.content.energy_environment import EnergyDType
30
+ from .data_models.content.enum_datatypes import EnergyDType
30
31
  from .decoder_waveform import Uart
31
32
 
32
33
  if TYPE_CHECKING:
@@ -46,7 +47,7 @@ class Reader:
46
47
 
47
48
  """
48
49
 
49
- CHUNK_SAMPLES_N: int = 10_000
50
+ CHUNK_SAMPLES_N: final[int] = 10_000
50
51
 
51
52
  MODE_TO_DTYPE: Mapping[str, Sequence[EnergyDType]] = MappingProxyType(
52
53
  {
@@ -101,9 +102,9 @@ class Reader:
101
102
  try:
102
103
  self.h5file = h5py.File(self.file_path, "r") # = readonly
103
104
  self._reader_opened = True
104
- except OSError as _xcp:
105
+ except OSError as xcp:
105
106
  msg = f"Unable to open HDF5-File '{self.file_path.name}'"
106
- raise TypeError(msg) from _xcp
107
+ raise TypeError(msg) from xcp
107
108
 
108
109
  if self.is_valid():
109
110
  self._logger.debug("File is available now")
@@ -209,13 +210,14 @@ class Reader:
209
210
  Generator - can be configured on first call
210
211
 
211
212
  Args:
212
- ----
213
- :param start_n: (int) Index of first chunk to be read
214
- :param end_n: (int) Index of last chunk to be read
215
- :param n_samples_per_chunk: (int) allows changing
216
- :param is_raw: (bool) output original data, not transformed to SI-Units
217
- :param omit_timestamps: (bool) optimize reading if timestamp is never used
218
- Yields: chunks between start and end (tuple with time, voltage, current)
213
+ start_n: (int) Index of first chunk to be read
214
+ end_n: (int) Index of last chunk to be read
215
+ n_samples_per_chunk: (int) allows changing
216
+ is_raw: (bool) output original data, not transformed to SI-Units
217
+ omit_timestamps: (bool) optimize reading if timestamp is never used
218
+
219
+ Yields:
220
+ chunks between start and end (tuple with time, voltage, current)
219
221
 
220
222
  """
221
223
  if n_samples_per_chunk is None:
@@ -223,21 +225,21 @@ class Reader:
223
225
  end_max = int(self.samples_n // n_samples_per_chunk)
224
226
  end_n = end_max if end_n is None else min(end_n, end_max)
225
227
  self._logger.debug("Reading chunk %d to %d from source-file", start_n, end_n)
226
- _raw = is_raw
227
- _wts = not omit_timestamps
228
+ raw_ = is_raw
229
+ wts_ = not omit_timestamps
228
230
 
229
231
  for i in range(start_n, end_n):
230
232
  idx_start = i * n_samples_per_chunk
231
233
  idx_end = idx_start + n_samples_per_chunk
232
- if _raw:
234
+ if raw_:
233
235
  yield (
234
- self.ds_time[idx_start:idx_end] if _wts else None,
236
+ self.ds_time[idx_start:idx_end] if wts_ else None,
235
237
  self.ds_voltage[idx_start:idx_end],
236
238
  self.ds_current[idx_start:idx_end],
237
239
  )
238
240
  else:
239
241
  yield (
240
- self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]) if _wts else None,
242
+ self._cal.time.raw_to_si(self.ds_time[idx_start:idx_end]) if wts_ else None,
241
243
  self._cal.voltage.raw_to_si(self.ds_voltage[idx_start:idx_end]),
242
244
  self._cal.current.raw_to_si(self.ds_current[idx_start:idx_end]),
243
245
  )
@@ -315,11 +317,11 @@ class Reader:
315
317
  self.get_config().get("virtual_harvester", {}).get("voltage_step_mV", None)
316
318
  )
317
319
  if voltage_step is not None: # convert mV to V
318
- voltage_step = 1e-3 * voltage_step
320
+ voltage_step *= 1e-3
319
321
  if voltage_step is None:
320
322
  dsv = self._cal.voltage.raw_to_si(self.ds_voltage[0:2000])
321
323
  diffs_np = np.unique(dsv[1:] - dsv[0:-1], return_counts=False)
322
- diffs_ls = [_e for _e in list(np.array(diffs_np)) if _e > 0]
324
+ diffs_ls = [e_ for e_ in list(np.array(diffs_np)) if e_ > 0]
323
325
  # static voltages have 0 steps, so
324
326
  if len(diffs_ls) == 0:
325
327
  self._logger.warning("Voltage-Step could not be determined from source-material")
@@ -457,11 +459,11 @@ class Reader:
457
459
  "[FileValidation] Hostname was not set in '%s'", self.file_path.name
458
460
  )
459
461
  # errors during execution
460
- _err = self.count_errors_in_log()
461
- if _err > 0:
462
+ err_ = self.count_errors_in_log()
463
+ if err_ > 0:
462
464
  self._logger.warning(
463
465
  "[FileValidation] Sheep reported %d errors during execution -> check logs in '%s'",
464
- _err,
466
+ err_,
465
467
  self.file_path.name,
466
468
  )
467
469
  return True
@@ -484,6 +486,8 @@ class Reader:
484
486
  # multiprocessing: https://stackoverflow.com/a/71898911
485
487
  # -> failed with multiprocessing.pool and pathos.multiprocessing.ProcessPool.
486
488
 
489
+ TODO: add optional duration argument to allow calculating mean energy of a spatial EEnv
490
+
487
491
  :return: sampled energy in Ws (watt-seconds)
488
492
  """
489
493
  iterations = math.ceil(self.samples_n / self.max_elements)
@@ -603,11 +607,11 @@ class Reader:
603
607
  return 0
604
608
  if "level" not in self.h5file[group_name]:
605
609
  return 0
606
- _lvl = self.h5file[group_name]["level"]
607
- if _lvl.shape[0] < 1:
610
+ lvl_ = self.h5file[group_name]["level"]
611
+ if lvl_.shape[0] < 1:
608
612
  return 0
609
- _items = [1 for _x in _lvl[:] if _x >= min_level]
610
- return len(_items)
613
+ items_ = [1 for x_ in lvl_[:] if x_ >= min_level]
614
+ return len(items_)
611
615
 
612
616
  def get_metadata(
613
617
  self,
@@ -5,10 +5,10 @@ from pathlib import Path
5
5
 
6
6
 
7
7
  def _get_xdg_path(variable_name: str, default_path: Path) -> Path:
8
- _value = os.environ.get(variable_name)
9
- if _value is None or _value == "":
8
+ value_ = os.environ.get(variable_name)
9
+ if not value_: # catches None and ""
10
10
  return default_path
11
- return Path(_value)
11
+ return Path(value_)
12
12
 
13
13
 
14
14
  user_path = Path("~").expanduser()
@@ -18,9 +18,11 @@ TODO: Comfort functions missing
18
18
  from abc import ABC
19
19
  from abc import abstractmethod
20
20
  from typing import Any
21
+ from typing import final
21
22
 
22
23
  from shepherd_core.data_models.base.shepherd import ShpModel
23
24
  from shepherd_core.data_models.base.wrapper import Wrapper
25
+ from shepherd_core.logger import log
24
26
 
25
27
  from .fixtures import Fixtures
26
28
 
@@ -58,6 +60,7 @@ class AbcClient(ABC):
58
60
  # TODO: maybe internal? yes
59
61
  pass
60
62
 
63
+ @final
61
64
  def try_completing_model(
62
65
  self, model_type: str, values: dict[str, Any]
63
66
  ) -> tuple[dict[str, Any], list[str]]:
@@ -71,14 +74,18 @@ class AbcClient(ABC):
71
74
  except ValueError as err:
72
75
  msg = f"Query {model_type} by name / ID failed - {values} is unknown!"
73
76
  raise ValueError(msg) from err
77
+ except KeyError:
78
+ log.error(f"Query failed - model-type {model_type} is unknown")
79
+ return values, []
74
80
  return self.try_inheritance(model_type, values)
75
81
 
76
82
  @abstractmethod
77
83
  def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
78
- # TODO: is it really helpful and needed?
84
+ # TODO: is it really needed and helpful?
79
85
  pass
80
86
 
81
87
 
88
+ @final
82
89
  class FixturesClient(AbcClient):
83
90
  """Client-Class to access the file based fixtures."""
84
91
 
@@ -110,12 +117,16 @@ class FixturesClient(AbcClient):
110
117
  def try_inheritance(
111
118
  self, model_type: str, values: dict[str, Any]
112
119
  ) -> tuple[dict[str, Any], list[str]]:
113
- return self._fixtures[model_type].inheritance(values)
120
+ try:
121
+ return self._fixtures[model_type].inheritance(values)
122
+ except KeyError:
123
+ log.error(f"Query failed - model-type {model_type} is unknown")
124
+ return values, []
114
125
 
115
126
  def fill_in_user_data(self, values: dict[str, Any]) -> dict[str, Any]:
116
127
  """Add fake user-data when offline-client is used.
117
128
 
118
- Hotfix until WebClient is working.
129
+ Workaround until WebClient is working.
119
130
  """
120
131
  if values.get("owner") is None:
121
132
  values["owner"] = "unknown"
@@ -3,6 +3,7 @@
3
3
  from importlib import import_module
4
4
  from pathlib import Path
5
5
  from typing import Any
6
+ from typing import final
6
7
 
7
8
  from pydantic import validate_call
8
9
 
@@ -14,6 +15,7 @@ from .client_abc_fix import AbcClient
14
15
  from .user_model import User
15
16
 
16
17
 
18
+ @final
17
19
  class WebClient(AbcClient):
18
20
  """Client-Class to access a testbed instance over the web.
19
21
 
@@ -125,26 +127,26 @@ class WebClient(AbcClient):
125
127
  return False
126
128
 
127
129
  def submit_experiment(self, xp: ShpModel) -> str:
128
- """Transmit XP to server to validate its feasibility.
130
+ """Transmit experiment to server to validate its feasibility.
129
131
 
130
132
  - Experiment will be added to DB (if not present)
131
- - if the same experiment is resubmitted it will just return the ID of that XP
133
+ - if the same experiment is resubmitted it will just return the ID of that experiment
132
134
  - Experiment will be validated by converting it into a task-set (additional validation)
133
135
  - optional: the scheduler should validate there are no time-collisions
134
136
 
135
137
  Will return an ID if valid, otherwise an empty string.
136
138
  TODO: maybe its better to throw specific errors if validation fails
137
- TODO: is it better to include these experiment-related FNs in Xp-Class?
139
+ TODO: is it better to include these experiment-related FNs in experiment-Class?
138
140
  TODO: Experiment-typehint for argument triggers circular import
139
141
  """
140
142
  raise NotImplementedError("TODO")
141
143
 
142
144
  def schedule_experiment(self, id_xp: str) -> bool:
143
- """Enqueue XP on testbed."""
145
+ """Enqueue experiment on testbed."""
144
146
  raise NotImplementedError("TODO")
145
147
 
146
148
  def get_experiment_status(self, id_xp: str) -> str:
147
- """Ask server about current state of XP.
149
+ """Ask server about current state of experiment.
148
150
 
149
151
  - after valid submission: disabled / deactivated
150
152
  - after scheduling: scheduled
@@ -48,10 +48,10 @@ class Fixture:
48
48
  if "name" not in data.parameters:
49
49
  return
50
50
  name = str(data.parameters["name"]).lower()
51
- _id = data.parameters["id"]
51
+ id_ = data.parameters["id"]
52
52
  data_model = data.parameters
53
53
  self.elements_by_name[name] = data_model
54
- self.elements_by_id[_id] = data_model
54
+ self.elements_by_id[id_] = data_model
55
55
  # update iterator
56
56
  self._iter_list: list[dict[str, Any]] = list(self.elements_by_name.values())
57
57
 
@@ -84,7 +84,7 @@ class Fixture:
84
84
  return self.elements_by_name.keys()
85
85
 
86
86
  def refs(self) -> dict:
87
- return {_i["id"]: _i["name"] for _i in self.elements_by_id.values()}
87
+ return {i_["id"]: i_["name"] for i_ in self.elements_by_id.values()}
88
88
 
89
89
  def inheritance(
90
90
  self, values: dict[str, Any], chain: list[str] | None = None
@@ -123,8 +123,8 @@ class Fixture:
123
123
  post_process = True
124
124
 
125
125
  elif values.get("id") in self.elements_by_id:
126
- _id = values["id"]
127
- fixture_base = copy.copy(self.elements_by_id[_id])
126
+ id_ = values["id"]
127
+ fixture_base = copy.copy(self.elements_by_id[id_])
128
128
  post_process = True
129
129
 
130
130
  if post_process:
@@ -218,7 +218,7 @@ class Fixtures:
218
218
 
219
219
  @validate_call
220
220
  def insert_file(self, file: Path) -> None:
221
- with file.open() as fd:
221
+ with file.open(encoding="utf-8-sig") as fd:
222
222
  fixtures = yaml.safe_load(fd)
223
223
  for fixture in fixtures:
224
224
  if not isinstance(fixture, dict):
@@ -237,7 +237,7 @@ class Fixtures:
237
237
  if key in self.components:
238
238
  return self.components[key]
239
239
  msg = f"Component '{key}' not found!"
240
- raise ValueError(msg)
240
+ raise KeyError(msg)
241
241
 
242
242
  def keys(self) -> Iterable[str]:
243
243
  return self.components.keys()
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2025.10.1"
3
+ version: str = "2026.02.3"
@@ -35,7 +35,7 @@ class PruCalibration:
35
35
  negative_residue_nA = 0
36
36
 
37
37
  def __init__(self, cal_emu: CalibrationEmulator | None = None) -> None:
38
- self.cal = cal_emu if cal_emu else CalibrationEmulator()
38
+ self.cal = cal_emu or CalibrationEmulator()
39
39
 
40
40
  def conv_adc_raw_to_nA(self, current_raw: int) -> float:
41
41
  I_nA = self.cal.adc_C_A.raw_to_si(current_raw) * (10**9)
@@ -44,7 +44,7 @@ class PruCalibration:
44
44
  I_nA -= self.negative_residue_nA
45
45
  self.negative_residue_nA = 0
46
46
  else:
47
- self.negative_residue_nA = self.negative_residue_nA - I_nA
47
+ self.negative_residue_nA -= I_nA
48
48
  self.negative_residue_nA = min(self.negative_residue_nA, self.RESIDUE_MAX_nA)
49
49
  I_nA = 0
50
50
  return I_nA
@@ -152,7 +152,7 @@ class VirtualHarvesterModel:
152
152
  return self.voltage_hold, self.current_hold
153
153
 
154
154
  def ivcurve_2_mppt_voc(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
155
- self.interval_step = self.interval_step + 1
155
+ self.interval_step += 1
156
156
  if self.interval_step >= self._cfg.interval_n:
157
157
  self.interval_step = 0
158
158
  self.age_nxt += 1
@@ -183,7 +183,7 @@ class VirtualHarvesterModel:
183
183
  return _voltage_uV, _current_nA
184
184
 
185
185
  def ivcurve_2_mppt_po(self, _voltage_uV: int, _current_nA: int) -> tuple[int, int]:
186
- self.interval_step = self.interval_step + 1
186
+ self.interval_step += 1
187
187
  if self.interval_step >= self._cfg.interval_n:
188
188
  self.interval_step = 0
189
189
 
@@ -52,20 +52,20 @@ def simulate_harvester(
52
52
  hrv = VirtualHarvesterModel(hrv_pru)
53
53
  e_out_Ws = 0.0
54
54
 
55
- for _t, v_inp, i_inp in tqdm(
55
+ for t_, v_inp, i_inp in tqdm(
56
56
  file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
57
57
  ):
58
58
  v_uV = cal_inp.voltage.raw_to_si(v_inp) * 1e6
59
59
  i_nA = cal_inp.current.raw_to_si(i_inp) * 1e9
60
60
  length = min(v_uV.size, i_nA.size)
61
- for _n in range(length):
62
- v_uV[_n], i_nA[_n] = hrv.ivcurve_sample(
63
- _voltage_uV=int(v_uV[_n]), _current_nA=int(i_nA[_n])
61
+ for n_ in range(length):
62
+ v_uV[n_], i_nA[n_] = hrv.ivcurve_sample(
63
+ _voltage_uV=int(v_uV[n_]), _current_nA=int(i_nA[n_])
64
64
  )
65
65
  e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
66
66
  if path_output:
67
67
  v_out = cal_out.voltage.si_to_raw(v_uV / 1e6)
68
68
  i_out = cal_out.current.si_to_raw(i_nA / 1e9)
69
- file_out.append_iv_data_raw(_t, v_out, i_out)
69
+ file_out.append_iv_data_raw(t_, v_out, i_out)
70
70
 
71
71
  return e_out_Ws
@@ -11,7 +11,7 @@ NOTE: DO NOT OPTIMIZE -> stay close to original code-base
11
11
  """
12
12
 
13
13
  from shepherd_core.data_models.base.calibration import CalibrationEmulator
14
- from shepherd_core.data_models.content.energy_environment import EnergyDType
14
+ from shepherd_core.data_models.content.enum_datatypes import EnergyDType
15
15
  from shepherd_core.data_models.content.virtual_harvester_config import HarvesterPRUConfig
16
16
  from shepherd_core.data_models.content.virtual_source_config import ConverterPRUConfig
17
17
  from shepherd_core.data_models.content.virtual_source_config import VirtualSourceConfig
@@ -72,24 +72,24 @@ def simulate_source(
72
72
  else:
73
73
  stats_internal = None
74
74
 
75
- for _t, v_inp, i_inp in tqdm(
75
+ for t_, v_inp, i_inp in tqdm(
76
76
  file_inp.read(is_raw=True), total=file_inp.chunks_n, desc="Chunk", leave=False
77
77
  ):
78
78
  v_uV = 1e6 * cal_inp.voltage.raw_to_si(v_inp)
79
79
  i_nA = 1e9 * cal_inp.current.raw_to_si(i_inp)
80
80
 
81
- for _n in range(len(_t)):
82
- v_uV[_n] = src.iterate_sampling(
83
- V_inp_uV=int(v_uV[_n]),
84
- I_inp_nA=int(i_nA[_n]),
81
+ for n_ in range(len(t_)):
82
+ v_uV[n_] = src.iterate_sampling(
83
+ V_inp_uV=int(v_uV[n_]),
84
+ I_inp_nA=int(i_nA[n_]),
85
85
  I_out_nA=i_out_nA,
86
86
  )
87
- i_out_nA = target.step(int(v_uV[_n]), pwr_good=src.cnv.get_power_good())
88
- i_nA[_n] = i_out_nA
87
+ i_out_nA = target.step(int(v_uV[n_]), pwr_good=src.cnv.get_power_good())
88
+ i_nA[n_] = i_out_nA
89
89
 
90
90
  if stats_internal is not None:
91
91
  stats_internal[stats_sample] = [
92
- _t[_n] * 1e-9, # s
92
+ t_[n_] * 1e-9, # s
93
93
  src.hrv.voltage_hold * 1e-6,
94
94
  src.cnv.V_input_request_uV * 1e-6, # V
95
95
  src.hrv.voltage_set_uV * 1e-6,
@@ -107,7 +107,7 @@ def simulate_source(
107
107
  if path_output:
108
108
  v_out = cal_out.voltage.si_to_raw(1e-6 * v_uV)
109
109
  i_out = cal_out.current.si_to_raw(1e-9 * i_nA)
110
- file_out.append_iv_data_raw(_t, v_out, i_out)
110
+ file_out.append_iv_data_raw(t_, v_out, i_out)
111
111
 
112
112
  if stats_internal is not None:
113
113
  stats_internal = stats_internal[:stats_sample, :]
@@ -161,7 +161,7 @@ class ModelKiBaM(ModelStorage):
161
161
 
162
162
  # Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
163
163
  # ⤷ MODIFIED: clamp both SoC to 0..1
164
- self.SoC = self.SoC - 1 / self.cfg.q_As * (I_cell * self.dt_s)
164
+ self.SoC -= 1 / self.cfg.q_As * (I_cell * self.dt_s)
165
165
  self.SoC = min(max(self.SoC, 0.0), 1.0)
166
166
  SoC_eff = self.SoC - 1 / self.cfg.q_As * self.C_unavailable
167
167
  SoC_eff = max(SoC_eff, 0.0)
@@ -278,7 +278,7 @@ class ModelKiBaMPlus(ModelStorage):
278
278
 
279
279
  # Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
280
280
  # ⤷ MODIFIED: clamp both SoC to 0..1
281
- self.SoC = self.SoC - (I_cell + I_leak) * self.dt_s / self.cfg.q_As
281
+ self.SoC -= (I_cell + I_leak) * self.dt_s / self.cfg.q_As
282
282
  self.SoC = min(max(self.SoC, 0.0), 1.0)
283
283
  SoC_eff = self.SoC - 1 / self.cfg.q_As * self.C_unavailable
284
284
  SoC_eff = min(max(SoC_eff, 0.0), 1.0)
@@ -380,7 +380,7 @@ class ModelKiBaMSimple(ModelStorage):
380
380
 
381
381
  # Step 2: Calculate SoC after dt (equation 6; modified for discrete operation)
382
382
  # = SoC - 1 / C * (i_cell * dt)
383
- self.SoC = self.SoC - (I_cell + I_leak) * self.Constant_s_per_As
383
+ self.SoC -= (I_cell + I_leak) * self.Constant_s_per_As
384
384
  SoC_eff = self.SoC = min(max(self.SoC, 0.0), 1.0)
385
385
  # ⤷ MODIFIED: removed term due to omission of rate capacity effect
386
386
  # ⤷ MODIFIED: clamp SoC to 0..1
shepherd_core/writer.py CHANGED
@@ -22,7 +22,7 @@ from .config import config
22
22
  from .data_models.base.calibration import CalibrationEmulator as CalEmu
23
23
  from .data_models.base.calibration import CalibrationHarvester as CalHrv
24
24
  from .data_models.base.calibration import CalibrationSeries as CalSeries
25
- from .data_models.content.energy_environment import EnergyDType
25
+ from .data_models.content.enum_datatypes import EnergyDType
26
26
  from .data_models.task import Compression
27
27
  from .data_models.task.emulation import c_translate
28
28
  from .reader import Reader
@@ -116,11 +116,12 @@ class Writer(Reader):
116
116
 
117
117
  if not hasattr(self, "_logger"):
118
118
  self._logger: logging.Logger = logging.getLogger("SHPCore.Writer")
119
+ self._logger.setLevel(logging.DEBUG if verbose else logging.INFO)
119
120
  # -> logger gets configured in reader()
120
121
 
121
122
  if self._modify or force_overwrite or not file_path.exists():
122
123
  file_path = file_path.resolve()
123
- self._logger.info("Storing data to '%s'", file_path)
124
+ self._logger.debug("Storing data to '%s'", file_path)
124
125
  elif file_path.exists() and not file_path.is_file():
125
126
  msg = f"Path is not a file ({file_path})"
126
127
  raise TypeError(msg)
@@ -154,22 +155,22 @@ class Writer(Reader):
154
155
  if "mode" not in self.h5file.attrs:
155
156
  self.h5file.attrs["mode"] = self.MODE_DEFAULT
156
157
 
157
- _dtypes = self.MODE_TO_DTYPE[self.get_mode()]
158
+ dtypes_ = self.MODE_TO_DTYPE[self.get_mode()]
158
159
 
159
160
  # Handle Datatype
160
161
  if isinstance(datatype, str):
161
162
  datatype = EnergyDType[datatype]
162
- if isinstance(datatype, EnergyDType) and datatype not in _dtypes:
163
- msg = f"Can't handle value '{datatype}' of datatype (choose one of {_dtypes})"
163
+ if isinstance(datatype, EnergyDType) and datatype not in dtypes_:
164
+ msg = f"Can't handle value '{datatype}' of datatype (choose one of {dtypes_})"
164
165
  raise ValueError(msg)
165
166
 
166
167
  if isinstance(datatype, EnergyDType):
167
168
  self.h5file["data"].attrs["datatype"] = datatype.name
168
169
  if "datatype" not in self.h5file["data"].attrs:
169
170
  self.h5file["data"].attrs["datatype"] = self.DATATYPE_DEFAULT.name
170
- if self.get_datatype() not in _dtypes:
171
+ if self.get_datatype() not in dtypes_:
171
172
  msg = (
172
- f"Can't handle value '{self.get_datatype()}' of datatype (choose one of {_dtypes})"
173
+ f"Can't handle value '{self.get_datatype()}' of datatype (choose one of {dtypes_})"
173
174
  )
174
175
  raise ValueError(msg)
175
176
 
@@ -183,6 +184,12 @@ class Writer(Reader):
183
184
  raise ValueError("Window Size argument needed for ivcurve-Datatype")
184
185
 
185
186
  # Handle Cal
187
+ if isinstance(cal_data, CalEmu):
188
+ msg = (
189
+ "Writer got a CalibrationEmulator()-object without information "
190
+ "about the TargetPort. Possibly wrong cal-data stored in hdf5!"
191
+ )
192
+ self._logger.warning(msg)
186
193
  if isinstance(cal_data, (CalEmu, CalHrv)):
187
194
  cal_data = CalSeries.from_cal(cal_data)
188
195
 
@@ -215,7 +222,7 @@ class Writer(Reader):
215
222
  ) -> None:
216
223
  self._align()
217
224
  self._refresh_file_stats()
218
- self._logger.info(
225
+ self._logger.debug(
219
226
  "closing hdf5 file, %.1f s iv-data, size = %.3f MiB, rate = %.0f KiB/s",
220
227
  self.runtime_s,
221
228
  self.file_size / 2**20,
@@ -296,7 +303,7 @@ class Writer(Reader):
296
303
  timestamp = int(timestamp)
297
304
  if isinstance(timestamp, int):
298
305
  time_series_ns = self.sample_interval_ns * np.arange(len_new).astype("u8")
299
- timestamp = timestamp + time_series_ns
306
+ timestamp += time_series_ns
300
307
  if isinstance(timestamp, np.ndarray):
301
308
  len_new = min(len_new, timestamp.size)
302
309
  else: