shepherd-core 2024.8.2__py3-none-any.whl → 2024.11.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.
@@ -34,6 +34,9 @@ class EnergyEnvironment(ContentModel):
34
34
  valid: bool = False
35
35
 
36
36
  # TODO: scale up/down voltage/current
37
+ # TODO: multiple files for one env
38
+ # TODO: mean power as energy/duration
39
+ # TODO: harvester, transducer
37
40
 
38
41
  # additional descriptive metadata, TODO: these are very solar-centered -> generalize
39
42
  light_source: Optional[str] = None
@@ -21,6 +21,7 @@ from .energy_environment import EnergyDType
21
21
  class AlgorithmDType(str, Enum):
22
22
  """Options for choosing a harvesting algorithm."""
23
23
 
24
+ direct = disable = neutral = "neutral"
24
25
  isc_voc = "isc_voc"
25
26
  ivcurve = ivcurves = ("ivcurve",)
26
27
  constant = cv = "cv"
@@ -62,6 +63,9 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
62
63
  # ⤷ of (open voltage) measurement
63
64
  rising: bool = True
64
65
  # ⤷ direction of sawtooth
66
+ enable_linear_extrapolation: bool = True
67
+ # ⤷ improves slow cv-algo that is base of most ivcurve-harvesters
68
+ # (update-freq dependent on window-size)
65
69
 
66
70
  # Underlying recorder
67
71
  wait_cycles: Annotated[int, Field(ge=0, le=100)] = 1
@@ -107,7 +111,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
107
111
  return self
108
112
 
109
113
  def calc_hrv_mode(self, *, for_emu: bool) -> int:
110
- return 1 * int(for_emu) + 2 * self.rising
114
+ return 1 * int(for_emu) + 2 * self.rising + 4 * self.enable_linear_extrapolation
111
115
 
112
116
  def calc_algorithm_num(self, *, for_emu: bool) -> int:
113
117
  num = algo_to_num.get(self.algorithm)
@@ -117,7 +121,7 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
117
121
  f"current usage = {self.algorithm}"
118
122
  )
119
123
  raise ValueError(msg)
120
- if num < algo_to_num["isc_voc"]:
124
+ if not for_emu and num < algo_to_num["isc_voc"]:
121
125
  msg = (
122
126
  f"[{self.name}] Select valid harvest-algorithm for harvester, "
123
127
  f"current usage = {self.algorithm}"
@@ -177,7 +181,7 @@ u32 = Annotated[int, Field(ge=0, lt=2**32)]
177
181
 
178
182
  # Currently implemented harvesters
179
183
  # NOTE: numbers have meaning and will be tested ->
180
- # - harvesting on "neutral" is not possible
184
+ # - harvesting on "neutral" is not possible - direct pass-through
181
185
  # - emulation with "ivcurve" or lower is also resulting in Error
182
186
  # - "_opt" has its own algo for emulation, but is only a fast mppt_po for harvesting
183
187
  algo_to_num = {
@@ -192,6 +196,7 @@ algo_to_num = {
192
196
  }
193
197
 
194
198
  algo_to_dtype = {
199
+ "neutral": EnergyDType.ivsample,
195
200
  "isc_voc": EnergyDType.isc_voc,
196
201
  "ivcurve": EnergyDType.ivcurve,
197
202
  "cv": EnergyDType.ivsample,
@@ -3,10 +3,13 @@
3
3
  # - look into the implementation to see which parameters are used
4
4
  # - base for neutral fallback values if provided yml is sparse
5
5
  # - -> it is encouraged to omit redundant parameters in your own implementation
6
+ # - general rule: 0<id<2000 harvest from transducers, 2000<=id harvest from ivcurve
7
+
6
8
  - datatype: VirtualHarvesterConfig
7
9
  parameters:
8
10
  id: 1000
9
11
  name: neutral
12
+ enable_linear_extrapolation: true
10
13
  owner: Ingmar
11
14
  group: NES Lab
12
15
  visible2group: true
@@ -16,7 +19,7 @@
16
19
 
17
20
  - datatype: VirtualHarvesterConfig
18
21
  parameters:
19
- id: 1010
22
+ id: 1100
20
23
  name: ivcurve
21
24
  description: Postpone harvesting by sampling ivcurves (voltage stepped as sawtooth-wave)
22
25
  comment: ~110 Hz, Between 50 & 60 Hz line-frequency to avoid standing waves
@@ -32,19 +35,19 @@
32
35
 
33
36
  - datatype: VirtualHarvesterConfig
34
37
  parameters:
35
- id: 1011
38
+ id: 1101
36
39
  name: ivcurves # synonym
37
40
  inherit_from: ivcurve
38
41
 
39
42
  - datatype: VirtualHarvesterConfig
40
43
  parameters:
41
- id: 1013
44
+ id: 1103
42
45
  name: iv110 # synonym
43
46
  inherit_from: ivcurve
44
47
 
45
48
  - datatype: VirtualHarvesterConfig
46
49
  parameters:
47
- id: 1012
50
+ id: 1102
48
51
  name: iv1000
49
52
  comment: Name relates to curves per second
50
53
  inherit_from: ivcurve
@@ -53,7 +56,7 @@
53
56
 
54
57
  - datatype: VirtualHarvesterConfig
55
58
  parameters:
56
- id: 1020
59
+ id: 1200
57
60
  name: isc_voc
58
61
  description: Postpone harvesting by sampling short circuit current & open circuit voltage
59
62
  inherit_from: neutral
@@ -62,7 +65,7 @@
62
65
 
63
66
  - datatype: VirtualHarvesterConfig
64
67
  parameters:
65
- id: 1030
68
+ id: 2100
66
69
  name: cv20
67
70
  description: Harvesting with constant Voltage
68
71
  inherit_from: neutral
@@ -71,28 +74,28 @@
71
74
 
72
75
  - datatype: VirtualHarvesterConfig
73
76
  parameters:
74
- id: 1031
77
+ id: 2101
75
78
  name: cv24
76
79
  inherit_from: cv20
77
80
  voltage_mV: 2400
78
81
 
79
82
  - datatype: VirtualHarvesterConfig
80
83
  parameters:
81
- id: 1032
84
+ id: 2102
82
85
  name: cv33
83
86
  inherit_from: cv20
84
87
  voltage_mV: 3300
85
88
 
86
89
  - datatype: VirtualHarvesterConfig
87
90
  parameters:
88
- id: 1032
91
+ id: 2102
89
92
  name: cv10
90
93
  inherit_from: cv20
91
94
  voltage_mV: 1000
92
95
 
93
96
  - datatype: VirtualHarvesterConfig
94
97
  parameters:
95
- id: 1040
98
+ id: 2200
96
99
  name: mppt_voc
97
100
  description: MPPT based on open circuit voltage for solar
98
101
  inherit_from: neutral
@@ -104,7 +107,7 @@
104
107
 
105
108
  - datatype: VirtualHarvesterConfig
106
109
  parameters:
107
- id: 1041
110
+ id: 2201
108
111
  name: mppt_bq
109
112
  description: MPPT of TI BQ-Converters for solar
110
113
  inherit_from: mppt_voc
@@ -114,7 +117,7 @@
114
117
 
115
118
  - datatype: VirtualHarvesterConfig
116
119
  parameters:
117
- id: 1042
120
+ id: 2202
118
121
  name: mppt_bqt
119
122
  description: MPPT of TI BQ-Converters for thermoelectric
120
123
  inherit_from: mppt_voc
@@ -124,19 +127,19 @@
124
127
 
125
128
  - datatype: VirtualHarvesterConfig
126
129
  parameters:
127
- id: 1043
130
+ id: 2203
128
131
  name: mppt_bq_solar # explicit naming
129
132
  inherit_from: mppt_bq
130
133
 
131
134
  - datatype: VirtualHarvesterConfig
132
135
  parameters:
133
- id: 1044
136
+ id: 2204
134
137
  name: mppt_bq_thermoelectric # explicit naming
135
138
  inherit_from: mppt_bqt
136
139
 
137
140
  - datatype: VirtualHarvesterConfig
138
141
  parameters:
139
- id: 1045
142
+ id: 2205
140
143
  name: mppt_po
141
144
  description: MPPT based on perturb & observe algorithm
142
145
  inherit_from: neutral
@@ -148,10 +151,18 @@
148
151
 
149
152
  - datatype: VirtualHarvesterConfig
150
153
  parameters:
151
- id: 1046
154
+ id: 2206
152
155
  name: mppt_opt
153
156
  description: Power-Optimum with very fast PO-Variant (harvesting) or special max-pwr-picker (emulator / ivcurve)
154
157
  inherit_from: mppt_po
155
158
  algorithm: mppt_opt
156
159
  voltage_step_mV: 1
157
160
  interval_ms: 0.01
161
+
162
+ - datatype: VirtualHarvesterConfig
163
+ parameters:
164
+ id: 3000
165
+ name: direct
166
+ description: Disables harvesting, even for ivcurve-input
167
+ inherit_from: neutral
168
+ algorithm: neutral
@@ -12,6 +12,7 @@ from ...logger import logger
12
12
  from ...testbed_client import tb_client
13
13
  from ..base.content import ContentModel
14
14
  from ..base.shepherd import ShpModel
15
+ from .energy_environment import EnergyDType
15
16
  from .virtual_harvester import HarvesterPRUConfig
16
17
  from .virtual_harvester import VirtualHarvesterConfig
17
18
 
@@ -42,6 +43,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
42
43
  # ⤷ if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
43
44
  enable_buck: bool = False
44
45
  # ⤷ if false -> v_output = v_intermediate
46
+ enable_feedback_to_hrv: bool = False
47
+ # src can control a cv-harvester for ivcurve
45
48
 
46
49
  interval_startup_delay_drain_ms: Annotated[float, Field(ge=0, le=10_000)] = 0
47
50
 
@@ -200,7 +203,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
200
203
  values["V_disable_output_threshold_mV"] = self.V_intermediate_disable_threshold_mV
201
204
  return values
202
205
 
203
- def calc_converter_mode(self, *, log_intermediate_node: bool) -> int:
206
+ def calc_converter_mode(self, dtype_in: EnergyDType, *, log_intermediate_node: bool) -> int:
204
207
  """Assembles bitmask from discrete values.
205
208
 
206
209
  log_intermediate_node: record / log virtual intermediate (cap-)voltage and
@@ -208,11 +211,25 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
208
211
  """
209
212
  enable_storage = self.C_intermediate_uF > 0
210
213
  enable_boost = self.enable_boost and enable_storage
214
+ if enable_boost != self.enable_boost:
215
+ logger.warning("VSrc - boost was disabled due to missing storage capacitor!")
216
+ enable_feedback = (
217
+ self.enable_feedback_to_hrv
218
+ and enable_storage
219
+ and not enable_boost
220
+ and dtype_in == EnergyDType.ivcurve
221
+ )
222
+ if enable_feedback != self.enable_feedback_to_hrv:
223
+ reason = "enabled boost, " if enable_boost else ""
224
+ reason += "" if dtype_in == EnergyDType.ivcurve else "input not ivcurve, "
225
+ reason += "" if enable_storage else "no storage capacitor"
226
+ logger.warning("VSRC - feedback to harvester was disabled! Reasons: %s", reason)
211
227
  return (
212
228
  1 * int(enable_storage)
213
229
  + 2 * int(enable_boost)
214
230
  + 4 * int(self.enable_buck)
215
231
  + 8 * int(log_intermediate_node)
232
+ + 16 * int(enable_feedback)
216
233
  )
217
234
 
218
235
  def calc_cap_constant_us_per_nF_n28(self) -> int:
@@ -285,13 +302,16 @@ class ConverterPRUConfig(ShpModel):
285
302
  def from_vsrc(
286
303
  cls,
287
304
  data: VirtualSourceConfig,
305
+ dtype_in: EnergyDType = EnergyDType.ivsample,
288
306
  *,
289
307
  log_intermediate_node: bool = False,
290
308
  ) -> Self:
291
309
  states = data.calc_internal_states()
292
310
  return cls(
293
311
  # General
294
- converter_mode=data.calc_converter_mode(log_intermediate_node=log_intermediate_node),
312
+ converter_mode=data.calc_converter_mode(
313
+ dtype_in, log_intermediate_node=log_intermediate_node
314
+ ),
295
315
  interval_startup_delay_drain_n=round(
296
316
  data.interval_startup_delay_drain_ms * samplerate_sps_default * 1e-3
297
317
  ),
@@ -11,6 +11,7 @@
11
11
  # General Config
12
12
  enable_boost: false # if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
13
13
  enable_buck: false # if false -> v_output = v_intermediate
14
+ enable_feedback_to_hrv: false # src can control a cv-harvester for ivcurve
14
15
 
15
16
  interval_startup_delay_drain_ms: 0
16
17
 
@@ -26,18 +27,21 @@
26
27
  V_intermediate_init_mV: 3000 # allow a proper / fast startup
27
28
  I_intermediate_leak_nA: 0.0
28
29
 
30
+ # Output-Switch with comparator and hysteresis
29
31
  V_intermediate_enable_threshold_mV: 1 # -> target gets connected (hysteresis-combo with next value)
30
32
  V_intermediate_disable_threshold_mV: 0 # -> target gets disconnected
31
33
  interval_check_thresholds_ms: 0.0 # some BQs check every 64 ms if output should be disconnected
32
34
 
35
+ # Power-Good signal from comparator and hysteresis
33
36
  V_pwr_good_enable_threshold_mV: 2800 # target is informed by pwr-good on output-pin (hysteresis) -> for intermediate voltage
34
37
  V_pwr_good_disable_threshold_mV: 2200
35
38
  immediate_pwr_good_signal: true # 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
36
39
 
37
- C_output_uF: 1.0 # final (always last) stage to compensate undetectable current spikes when enabling power for target
40
+ C_output_uF: 1.0 # final (always last) stage to compensate transient current spikes when enabling power for target
38
41
 
39
42
  # Extra
40
43
  V_output_log_gpio_threshold_mV: 1400 # min voltage needed to enable recording changes in gpio-bank
44
+ # TODO: actually disable gpio below that
41
45
 
42
46
  # Boost Converter
43
47
  V_input_boost_threshold_mV: 0.0 # min input-voltage for the boost converter to work
@@ -81,6 +85,8 @@
81
85
  id: 1010
82
86
  name: direct
83
87
  inherit_from: neutral
88
+ harvester:
89
+ name: direct # even disables harvesting of ivcurve
84
90
  # Note: current input has no influence
85
91
 
86
92
  - datatype: VirtualSourceConfig
@@ -90,7 +96,13 @@
90
96
  description: Simple Converter based on diode and buffer capacitor
91
97
  inherit_from: neutral
92
98
  V_input_drop_mV: 300 # simulate input-diode
93
- C_intermediate_uF: 10 # primary storage-Cap
99
+ C_intermediate_uF: 47 # primary storage-Cap
100
+ harvester:
101
+ name: cv20
102
+ enable_feedback_to_hrv: true # src can control a cv-harvester for ivcurve
103
+ V_intermediate_enable_threshold_mV: 2000
104
+ V_intermediate_disable_threshold_mV: 1800 # nRF draw ~0.5 mA below that point
105
+ # TODO: put switch-output into special nRF Version
94
106
 
95
107
  - datatype: VirtualSourceConfig
96
108
  parameters:
@@ -230,3 +242,5 @@
230
242
  id: 1033
231
243
  name: default
232
244
  inherit_from: BQ25570s
245
+
246
+ # TODO: add some generic boost-converters with mppt_po, _voc and more
@@ -33,7 +33,8 @@ class Compression(str, Enum):
33
33
  lzf = default = "lzf" # not native hdf5
34
34
  gzip1 = gzip = 1 # higher compr & load
35
35
  null = None
36
- # NOTE: changed to lzf as BBB needs every straw it can get
36
+ # NOTE: lzf & external file-compression (xz or zstd) work better than gzip
37
+ # -> even with additional compression
37
38
 
38
39
 
39
40
  compressions_allowed: list = [None, "lzf", 1]
shepherd_core/reader.py CHANGED
@@ -76,7 +76,7 @@ class Reader:
76
76
 
77
77
  if not hasattr(self, "samplerate_sps"):
78
78
  self.samplerate_sps: int = samplerate_sps_default
79
- self.sample_interval_ns: int = int(10**9 // self.samplerate_sps)
79
+ self.sample_interval_ns: int = round(10**9 // self.samplerate_sps)
80
80
  self.sample_interval_s: float = 1 / self.samplerate_sps
81
81
 
82
82
  self.max_elements: int = 40 * self.samplerate_sps
@@ -177,8 +177,8 @@ class Reader:
177
177
  # this assumes iso-chronous sampling
178
178
  duration_s = self._cal.time.raw_to_si(duration_raw)
179
179
  self.sample_interval_s = duration_s / sample_count
180
- self.sample_interval_ns = int(10**9 * self.sample_interval_s)
181
- self.samplerate_sps = max(int((sample_count - 1) / duration_s), 1)
180
+ self.sample_interval_ns = round(10**9 * self.sample_interval_s)
181
+ self.samplerate_sps = max(round((sample_count - 1) / duration_s), 1)
182
182
  self.runtime_s = round(self.ds_voltage.shape[0] / self.samplerate_sps, 1)
183
183
  self.buffers_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
184
184
  if isinstance(self.file_path, Path):
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2024.8.2"
3
+ version: str = "2024.11.1"
@@ -9,8 +9,10 @@ TODO: add more targets
9
9
  - riotee
10
10
  """
11
11
 
12
+ import math
12
13
  from abc import ABC
13
14
  from abc import abstractmethod
15
+ from contextlib import suppress
14
16
 
15
17
 
16
18
  class TargetABC(ABC):
@@ -22,41 +24,118 @@ class TargetABC(ABC):
22
24
 
23
25
 
24
26
  class ResistiveTarget(TargetABC):
25
- """Predictable target for matching the real world."""
27
+ """Predictable target with linear behavior."""
26
28
 
27
- def __init__(self, resistance_Ohm: float, *, controlled: bool = False) -> None:
28
- if resistance_Ohm <= 1e-3:
29
+ def __init__(self, R_Ohm: float, *, controlled: bool = False) -> None:
30
+ if R_Ohm <= 1e-3:
29
31
  raise ValueError("Resistance must be greater than 1 mOhm.")
30
- self.r_kOhm = 1e-3 * resistance_Ohm
32
+ self.R_kOhm = 1e-3 * R_Ohm
31
33
  self.ctrl = controlled
32
34
 
33
35
  def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
34
36
  if pwr_good or not self.ctrl:
35
- return voltage_uV / self.r_kOhm # = nA
37
+ return voltage_uV / self.R_kOhm # = nA
38
+ return 0
39
+
40
+
41
+ class DiodeTarget(TargetABC):
42
+ """Emulate a diode and current limiting resistor in series.
43
+
44
+ Good for modeling a debug-diode that burns energy.
45
+ It uses Shockley Diode Equation to estimate a model for diode
46
+ I_D = I_S * ( e ^ ( U_D / n*U_T ) - 1 )
47
+
48
+ diode of shepherd target:
49
+ d1 = DiodeTarget(V_forward_V = 2.0, I_forward_A = 20e-3, R_Ohm = 100)
50
+
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ V_forward_V: float,
56
+ I_forward_A: float,
57
+ R_Ohm: float,
58
+ *,
59
+ controlled: bool = False,
60
+ ) -> None:
61
+ if R_Ohm <= 1e-3:
62
+ raise ValueError("Resistance must be greater than 1 mOhm.")
63
+ if V_forward_V <= 0.2:
64
+ raise ValueError("Forward-Voltage of diode must be greater than 200 mV.")
65
+ if I_forward_A <= 0:
66
+ raise ValueError("Forward-current of diode must be greater than 0 A.")
67
+
68
+ k = 1.380649e-23 # boltzmann
69
+ q = 1.6021766e-19 # elementary charge
70
+ TJ = 100 + 273.15 # junction temperature
71
+
72
+ V_T = k * TJ / q # thermal voltage
73
+ n = 2 # ideality factor
74
+ self.c1 = V_T * n
75
+ # NOTE: math.expm1(x) = math.exp(x) - 1 = e^x -1
76
+ self.I_S = I_forward_A / math.expm1(V_forward_V / self.c1) # scale current
77
+ self.R_Ohm = R_Ohm
78
+ self.ctrl = controlled
79
+
80
+ def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
81
+ if pwr_good or not self.ctrl:
82
+ V_CC = voltage_uV * 1e-6
83
+ V_D = V_CC / 2
84
+ I_R = I_D = 0
85
+ # there is no direct formular, but this iteration converges fast
86
+ for _ in range(10):
87
+ # low voltages tend to produce log(x<0)=err
88
+ with suppress(ValueError):
89
+ V_D = self.c1 * math.log(1 + (V_CC - V_D) / (self.R_Ohm * self.I_S))
90
+ # both currents are positive and should be identical
91
+ I_R = max(0.0, (V_CC - V_D) / self.R_Ohm)
92
+ I_D = max(0.0, self.I_S * math.expm1(V_D / self.c1))
93
+ with suppress(ZeroDivisionError):
94
+ if abs(I_R / I_D - 1) < 1e-6:
95
+ break
96
+ # take mean of both currents and determine a new V_D
97
+ V_D = V_CC - self.R_Ohm * (I_R + I_D) / 2
98
+ return 1e9 * (I_R + I_D) / 2 # = nA
36
99
  return 0
37
100
 
38
101
 
39
102
  class ConstantCurrentTarget(TargetABC):
40
- """Recreate simple MCU without integrated regulator."""
103
+ """Recreate simple MCU without integrated regulator, i.e. msp430."""
41
104
 
42
- def __init__(self, i_active_A: float, i_sleep_A: float = 0) -> None:
43
- if i_active_A <= 0 or i_sleep_A <= 0:
105
+ def __init__(self, I_active_A: float, I_sleep_A: float = 0) -> None:
106
+ if I_active_A <= 0 or I_sleep_A < 0:
44
107
  raise ValueError("Current must be greater than 0.")
45
- self.i_active_nA = 1e9 * i_active_A
46
- self.i_sleep_nA = 1e9 * i_sleep_A
108
+ self.I_active_nA = 1e9 * I_active_A
109
+ self.I_sleep_nA = 1e9 * I_sleep_A
47
110
 
48
111
  def step(self, voltage_uV: int, *, pwr_good: bool) -> float: # noqa: ARG002
49
- return self.i_active_nA if pwr_good else self.i_sleep_nA
112
+ return self.I_active_nA if pwr_good else self.I_sleep_nA
50
113
 
51
114
 
52
115
  class ConstantPowerTarget(TargetABC):
53
- """Recreate MCU with integrated regulator."""
116
+ """Recreate MCU with integrated regulator, i.e. nRF52."""
54
117
 
55
- def __init__(self, p_active_W: float, p_sleep_W: float = 0) -> None:
56
- if p_active_W <= 0 or p_sleep_W <= 0:
118
+ def __init__(self, P_active_W: float, P_sleep_W: float = 0) -> None:
119
+ if P_active_W <= 0 or P_sleep_W < 0:
57
120
  raise ValueError("Power must be greater than 0.")
58
- self.p_active_fW = 1e15 * p_active_W
59
- self.p_sleep_fW = 1e15 * p_sleep_W
121
+ self.P_active_fW = 1e15 * P_active_W
122
+ self.P_sleep_fW = 1e15 * P_sleep_W
60
123
 
61
124
  def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
62
- return (self.p_active_fW if pwr_good else self.p_sleep_fW) / voltage_uV # = nA
125
+ return (self.P_active_fW if pwr_good else self.P_sleep_fW) / voltage_uV # = nA
126
+
127
+
128
+ # exemplary instantiations
129
+
130
+ diode_target_burn = DiodeTarget(V_forward_V=2.0, I_forward_A=20e-3, R_Ohm=100)
131
+ mcu_msp430fr = ConstantCurrentTarget(I_active_A=16 * 118e-6, I_sleep_A=350e-9)
132
+ mcu_msp_deep_sleep = ConstantCurrentTarget(45e-9, 45e-9)
133
+ # TODO: writing FRAM
134
+ mcu_nrf52840 = ConstantPowerTarget(P_active_W=3 * 3e-3, P_sleep_W=3 * 0.97e-6)
135
+ mcu_nrf_tx_8dBm = ConstantPowerTarget(P_active_W=3.0 * 16.40e-3)
136
+ mcu_nrf_tx_0dBm = ConstantPowerTarget(P_active_W=3.0 * 6.40e-3)
137
+ mcu_nrf_rx = ConstantPowerTarget(P_active_W=3.0 * 6.26e-3)
138
+ # data based on PS1.10
139
+ # - TX 8 dBm -> 6.31 mW -> DS shows 16.40 mA @ 3V (DC/DC)
140
+ # - TX 0 dBm -> 1 mW -> 6.40 mA
141
+ # - RX -> 6.26 mA
@@ -43,21 +43,18 @@ class PruCalibration:
43
43
  self.negative_residue_nA = 0
44
44
  else:
45
45
  self.negative_residue_nA = self.negative_residue_nA - I_nA
46
- if self.negative_residue_nA > self.RESIDUE_MAX_nA:
47
- self.negative_residue_nA = self.RESIDUE_MAX_nA
46
+ self.negative_residue_nA = min(self.negative_residue_nA, self.RESIDUE_MAX_nA)
48
47
  I_nA = 0
49
48
  return I_nA
50
49
 
51
50
  @staticmethod
52
- def conv_adc_raw_to_uV(voltage_raw: int) -> float:
51
+ def conv_adc_raw_to_uV(voltage_raw: int) -> None:
53
52
  msg = f"This Fn should not been used (val={voltage_raw})"
54
53
  raise RuntimeError(msg)
55
54
 
56
55
  def conv_uV_to_dac_raw(self, voltage_uV: float) -> int:
57
56
  dac_raw = self.cal.dac_V_A.si_to_raw(float(voltage_uV) / (10**6))
58
- if dac_raw > (2**16) - 1:
59
- dac_raw = (2**16) - 1
60
- return dac_raw
57
+ return min(dac_raw, (2**16) - 1)
61
58
 
62
59
 
63
60
  class VirtualConverterModel:
@@ -85,6 +82,9 @@ class VirtualConverterModel:
85
82
  self.enable_boost: bool = (int(self._cfg.converter_mode) & 0b0010) > 0
86
83
  self.enable_buck: bool = (int(self._cfg.converter_mode) & 0b0100) > 0
87
84
  self.enable_log_mid: bool = (int(self._cfg.converter_mode) & 0b1000) > 0
85
+ # back-channel to hrv
86
+ self.feedback_to_hrv: bool = (int(self._cfg.converter_mode) & 0b1_0000) > 0
87
+ self.V_input_request_uV: int = self._cfg.V_intermediate_init_uV
88
88
 
89
89
  self.V_out_dac_uV: float = self._cfg.V_output_uV
90
90
  self.V_out_dac_raw: int = self._cal.conv_uV_to_dac_raw(self._cfg.V_output_uV)
@@ -95,12 +95,13 @@ class VirtualConverterModel:
95
95
  self.V_enable_output_threshold_uV: float = self._cfg.V_enable_output_threshold_uV
96
96
  self.V_disable_output_threshold_uV: float = self._cfg.V_disable_output_threshold_uV
97
97
 
98
- if self.dV_enable_output_uV > self.V_enable_output_threshold_uV:
99
- self.V_enable_output_threshold_uV = self.dV_enable_output_uV
98
+ self.V_enable_output_threshold_uV = max(
99
+ self.dV_enable_output_uV, self.V_enable_output_threshold_uV
100
+ )
100
101
 
101
102
  # pulled from update_states_and_output() due to easier static init
102
103
  self.sample_count: int = 0xFFFFFFF0
103
- self.is_outputting: bool = True
104
+ self.is_outputting: bool = False
104
105
  self.vsource_skip_gpio_logging: bool = False
105
106
 
106
107
  def calc_inp_power(self, input_voltage_uV: float, input_current_nA: float) -> int:
@@ -114,31 +115,40 @@ class VirtualConverterModel:
114
115
  else:
115
116
  input_voltage_uV = 0.0
116
117
 
117
- if input_voltage_uV > self._cfg.V_input_max_uV:
118
- input_voltage_uV = self._cfg.V_input_max_uV
118
+ input_voltage_uV = min(input_voltage_uV, self._cfg.V_input_max_uV)
119
119
 
120
- if input_current_nA > self._cfg.I_input_max_nA:
121
- input_current_nA = self._cfg.I_input_max_nA
120
+ input_current_nA = min(input_current_nA, self._cfg.I_input_max_nA)
122
121
 
123
122
  self.V_input_uV = input_voltage_uV
124
123
 
125
124
  if self.enable_boost:
126
125
  if input_voltage_uV < self._cfg.V_input_boost_threshold_uV:
127
126
  input_voltage_uV = 0.0
128
- elif not self.enable_storage:
129
- # direct connection
130
- self.V_mid_uV = input_voltage_uV
131
- input_voltage_uV = 0.0
132
- # input current (& power) is not evaluated
133
- elif input_voltage_uV > self.V_mid_uV:
134
- V_diff_uV = input_voltage_uV - self.V_mid_uV
135
- V_drop_uV = input_current_nA * self.R_input_kOhm
136
- if V_drop_uV > V_diff_uV:
127
+ # TODO: vdrop in case of v_input > v_storage (non-boost)
128
+ elif self.enable_storage:
129
+ # no boost, but cap, for ie. diode+cap (+resistor)
130
+ V_diff_uV = (
131
+ (input_voltage_uV - self.V_mid_uV) if (input_voltage_uV >= self.V_mid_uV) else 0
132
+ )
133
+ V_res_drop_uV = input_current_nA * self.R_input_kOhm
134
+ if V_res_drop_uV > V_diff_uV:
137
135
  input_voltage_uV = self.V_mid_uV
138
136
  else:
139
- input_voltage_uV -= V_drop_uV
137
+ input_voltage_uV -= V_res_drop_uV
138
+
139
+ # IF input==ivcurve request new CV
140
+ if self.feedback_to_hrv:
141
+ self.V_input_request_uV = self.V_mid_uV + V_res_drop_uV + self._cfg.V_input_drop_uV
142
+ elif input_voltage_uV < self.V_mid_uV:
143
+ # without feedback there is no usable energy here
144
+ input_voltage_uV = 0
140
145
  else:
146
+ # direct connection
147
+ # modifying V_mid here is not clean, but simpler
148
+ # -> V_mid is needed in calc_out, before cap is updated
149
+ self.V_mid_uV = input_voltage_uV
141
150
  input_voltage_uV = 0.0
151
+ # ⤷ input will not be evaluated
142
152
 
143
153
  if self.enable_boost:
144
154
  eta_inp = self.get_input_efficiency(input_voltage_uV, input_current_nA)
@@ -170,6 +180,7 @@ class VirtualConverterModel:
170
180
 
171
181
  # TODO: add range-checks for add, sub Ops
172
182
  def update_cap_storage(self) -> int:
183
+ # TODO: this calculation is wrong for everything beside boost-cnv
173
184
  if self.enable_storage:
174
185
  V_mid_prot_uV = max(1.0, self.V_mid_uV)
175
186
  P_sum_fW = self.P_inp_fW - self.P_out_fW
@@ -177,34 +188,31 @@ class VirtualConverterModel:
177
188
  dV_mid_uV = I_mid_nA * self.Constant_us_per_nF
178
189
  self.V_mid_uV += dV_mid_uV
179
190
 
180
- if self.V_mid_uV > self._cfg.V_intermediate_max_uV:
181
- self.V_mid_uV = self._cfg.V_intermediate_max_uV
182
- if (not self.enable_boost) and (self.P_inp_fW > 0.0) and (self.V_mid_uV > self.V_input_uV):
183
- # TODO: obfuscated - no "direct connection"?
184
- self.V_mid_uV = self.V_input_uV
185
- elif self.V_mid_uV < 1:
186
- self.V_mid_uV = 1
191
+ self.V_mid_uV = min(self.V_mid_uV, self._cfg.V_intermediate_max_uV)
192
+ self.V_mid_uV = max(self.V_mid_uV, 1)
187
193
  return round(self.V_mid_uV) # Python-specific, added for easier testing
188
194
 
189
195
  def update_states_and_output(self) -> int:
190
196
  self.sample_count += 1
191
197
  check_thresholds = self.sample_count >= self._cfg.interval_check_thresholds_n
198
+ V_mid_uV_now = self.V_mid_uV
199
+ # copy avoids not enabling pwr_good (due to large dV_enable_output_uV)
192
200
 
193
201
  if check_thresholds:
194
202
  self.sample_count = 0
195
203
  if self.is_outputting:
196
- if self.V_mid_uV < self.V_disable_output_threshold_uV:
204
+ if V_mid_uV_now < self.V_disable_output_threshold_uV:
197
205
  self.is_outputting = False
198
- elif self.V_mid_uV >= self.V_enable_output_threshold_uV:
206
+ elif V_mid_uV_now >= self.V_enable_output_threshold_uV:
199
207
  self.is_outputting = True
200
208
  self.V_mid_uV -= self.dV_enable_output_uV
201
209
 
202
210
  if check_thresholds or self._cfg.immediate_pwr_good_signal:
203
211
  # generate power-good-signal
204
212
  if self.power_good:
205
- if self.V_mid_uV <= self._cfg.V_pwr_good_disable_threshold_uV:
213
+ if V_mid_uV_now <= self._cfg.V_pwr_good_disable_threshold_uV:
206
214
  self.power_good = False
207
- elif self.V_mid_uV >= self._cfg.V_pwr_good_enable_threshold_uV:
215
+ elif V_mid_uV_now >= self._cfg.V_pwr_good_enable_threshold_uV:
208
216
  self.power_good = self.is_outputting
209
217
  # set batok pin to state ... TODO?
210
218
 
@@ -270,7 +278,7 @@ class VirtualConverterModel:
270
278
  def get_power_good(self) -> bool:
271
279
  return self.power_good
272
280
 
273
- def get_I_mod_out_nA(self) -> float:
281
+ def get_I_mid_out_nA(self) -> float:
274
282
  return self.P_out_fW / self.V_mid_uV
275
283
 
276
284
  def get_state_log_intermediate(self) -> bool:
@@ -19,6 +19,7 @@ Compromises:
19
19
  from typing import Tuple
20
20
 
21
21
  from ..data_models.content.virtual_harvester import HarvesterPRUConfig
22
+ from ..logger import logger
22
23
 
23
24
 
24
25
  class VirtualHarvesterModel:
@@ -35,12 +36,20 @@ class VirtualHarvesterModel:
35
36
 
36
37
  # INIT global vars: shared states
37
38
  self.voltage_set_uV: int = self._cfg.voltage_uV + 1
39
+
40
+ self.is_emu: bool = bool(self._cfg.hrv_mode & (2**0))
41
+ if not self.is_emu:
42
+ logger.warning(
43
+ "This VSrc-config is not meant for emulation-mode -> activate 'is_emu' flag."
44
+ )
45
+
38
46
  if self._cfg.interval_n > 2 * self._cfg.window_size:
39
47
  self.interval_step = self._cfg.interval_n - (2 * self._cfg.window_size)
40
48
  else:
41
49
  self.interval_step = 2**30
42
50
  # ⤷ intake two ivcurves before overflow / reset
43
- self.is_rising: bool = (self._cfg.hrv_mode & (2**1)) != 0
51
+
52
+ self.is_rising: bool = bool(self._cfg.hrv_mode & (2**1))
44
53
 
45
54
  # PO-Relevant, iv & adc
46
55
  self.volt_step_uV: int = self._cfg.voltage_step_uV
@@ -54,13 +63,16 @@ class VirtualHarvesterModel:
54
63
  # globals for iv_cv
55
64
  self.voltage_hold: int = 0
56
65
  self.current_hold: int = 0
57
- self.voltage_step_x4_uV: int = self._cfg.voltage_step_uV * 4
66
+ self.voltage_step_x4_uV: int = 4 * self._cfg.voltage_step_uV
58
67
  self.age_max: int = 2 * self._cfg.window_size
59
68
 
60
69
  # INIT static vars: CV
61
70
  self.voltage_last: int = 0
62
71
  self.current_last: int = 0
63
72
  self.compare_last: int = 0
73
+ self.lin_extrapolation: bool = bool(self._cfg.hrv_mode & (2**2))
74
+ self.current_delta: int = 0
75
+ self.voltage_delta: int = 0
64
76
 
65
77
  # INIT static vars: VOC
66
78
  self.age_now: int = 0
@@ -106,9 +118,28 @@ class VirtualHarvesterModel:
106
118
  if distance_now < distance_last and distance_now < self.voltage_step_x4_uV:
107
119
  self.voltage_hold = _voltage_uV
108
120
  self.current_hold = _current_nA
121
+ self.current_delta = _current_nA - self.current_last
122
+ self.voltage_delta = _voltage_uV - self.voltage_last
123
+ # TODO: voltage_delta is static
109
124
  elif distance_last < distance_now and distance_last < self.voltage_step_x4_uV:
110
125
  self.voltage_hold = self.voltage_last
111
126
  self.current_hold = self.current_last
127
+ self.current_delta = _current_nA - self.current_last
128
+ self.voltage_delta = _voltage_uV - self.voltage_last
129
+ elif self.lin_extrapolation:
130
+ # apply the proper delta if needed
131
+ if (self.voltage_hold < self.voltage_set_uV) == (self.voltage_delta > 0):
132
+ self.voltage_hold += self.voltage_delta
133
+ self.current_hold += self.current_delta
134
+ else:
135
+ if self.voltage_hold > self.voltage_delta:
136
+ self.voltage_hold -= self.voltage_delta
137
+ else:
138
+ self.voltage_hold = 0
139
+ if self.current_hold > self.current_delta:
140
+ self.current_hold -= self.current_delta
141
+ else:
142
+ self.current_hold = 0
112
143
 
113
144
  self.voltage_last = _voltage_uV
114
145
  self.current_last = _current_nA
@@ -139,8 +170,9 @@ class VirtualHarvesterModel:
139
170
 
140
171
  _voltage_uV, _current_nA = self.ivcurve_2_cv(_voltage_uV, _current_nA)
141
172
  if self.interval_step < self._cfg.duration_n:
173
+ self.voltage_set_uV = self.voc_now
174
+ elif self.interval_step == self._cfg.duration_n:
142
175
  self.voltage_set_uV = int(self.voc_now * self._cfg.setpoint_n8 / 256)
143
- _current_nA = 0
144
176
 
145
177
  return _voltage_uV, _current_nA
146
178
 
@@ -194,7 +226,7 @@ class VirtualHarvesterModel:
194
226
 
195
227
  power_fW = _voltage_uV * _current_nA
196
228
  if (
197
- (power_fW > self.power_nxt)
229
+ (power_fW >= self.power_nxt)
198
230
  and (_voltage_uV >= self._cfg.voltage_min_uV)
199
231
  and (_voltage_uV <= self._cfg.voltage_max_uV)
200
232
  ):
@@ -39,7 +39,9 @@ class VirtualSourceModel:
39
39
 
40
40
  self.cfg_src = VirtualSourceConfig() if vsrc is None else vsrc
41
41
  cnv_config = ConverterPRUConfig.from_vsrc(
42
- self.cfg_src, log_intermediate_node=log_intermediate
42
+ self.cfg_src,
43
+ dtype_in=dtype_in,
44
+ log_intermediate_node=log_intermediate,
43
45
  )
44
46
  self.cnv: VirtualConverterModel = VirtualConverterModel(cnv_config, self._cal_pru)
45
47
 
@@ -80,4 +82,8 @@ class VirtualSourceModel:
80
82
  self.W_inp_fWs += P_inp_fW
81
83
  self.W_out_fWs += P_out_fW
82
84
 
85
+ # feedback path - important for boost-less circuits
86
+ if self.cnv.feedback_to_hrv:
87
+ self.hrv.voltage_set_uV = self.cnv.V_input_request_uV
88
+
83
89
  return V_mid_uV if self.cnv.get_state_log_intermediate() else V_out_uV
@@ -11,12 +11,14 @@ from contextlib import ExitStack
11
11
  from pathlib import Path
12
12
  from typing import Optional
13
13
 
14
+ import numpy as np
14
15
  from tqdm import tqdm
15
16
 
16
17
  from .. import CalibrationEmulator
17
18
  from .. import Reader
18
19
  from .. import Writer
19
20
  from ..data_models import VirtualSourceConfig
21
+ from ..logger import logger
20
22
  from . import VirtualSourceModel
21
23
  from .target_model import TargetABC
22
24
 
@@ -26,6 +28,8 @@ def simulate_source(
26
28
  target: TargetABC,
27
29
  path_input: Path,
28
30
  path_output: Optional[Path] = None,
31
+ *,
32
+ monitor_internals: bool = False,
29
33
  ) -> float:
30
34
  """Simulate behavior of virtual source algorithms.
31
35
 
@@ -47,10 +51,25 @@ def simulate_source(
47
51
  cal_out = file_out.get_calibration_data()
48
52
 
49
53
  src = VirtualSourceModel(
50
- config, cal_emu, log_intermediate=False, window_size=file_inp.get_window_samples()
54
+ config,
55
+ cal_emu,
56
+ dtype_in=file_inp.get_datatype(),
57
+ log_intermediate=False,
58
+ window_size=file_inp.get_window_samples(),
51
59
  )
52
60
  i_out_nA = 0
53
61
  e_out_Ws = 0.0
62
+ if monitor_internals and path_output:
63
+ stats_sample = 0
64
+ stats_internal = np.empty((round(file_inp.runtime_s * file_inp.samplerate_sps), 11))
65
+ try:
66
+ # keep dependencies low
67
+ from matplotlib import pyplot as plt
68
+ except ImportError:
69
+ logger.warning("Matplotlib not installed, plotting of internals disabled")
70
+ stats_internal = None
71
+ else:
72
+ stats_internal = None
54
73
 
55
74
  for _t, v_inp, i_inp in tqdm(
56
75
  file_inp.read_buffers(is_raw=True), total=file_inp.buffers_n, desc="Buffers", leave=False
@@ -66,7 +85,22 @@ def simulate_source(
66
85
  )
67
86
  i_out_nA = target.step(int(v_uV[_n]), pwr_good=src.cnv.get_power_good())
68
87
  i_nA[_n] = i_out_nA
69
- # TODO: src.cnv.get_I_mod_out_nA() has more internal drains
88
+
89
+ if stats_internal is not None:
90
+ stats_internal[stats_sample] = [
91
+ _t[_n] * 1e-9, # s
92
+ src.hrv.voltage_hold * 1e-6,
93
+ src.cnv.V_input_request_uV * 1e-6, # V
94
+ src.hrv.voltage_set_uV * 1e-6,
95
+ src.cnv.V_mid_uV * 1e-6,
96
+ src.hrv.current_hold * 1e-6, # mA
97
+ src.hrv.current_delta * 1e-6,
98
+ i_out_nA * 1e-6,
99
+ src.cnv.P_inp_fW * 1e-12, # mW
100
+ src.cnv.P_out_fW * 1e-12,
101
+ src.cnv.get_power_good(),
102
+ ]
103
+ stats_sample += 1
70
104
 
71
105
  e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
72
106
  if path_output:
@@ -75,4 +109,36 @@ def simulate_source(
75
109
  file_out.append_iv_data_raw(_t, v_out, i_out)
76
110
 
77
111
  stack.close()
112
+
113
+ if stats_internal is not None:
114
+ stats_internal = stats_internal[:stats_sample, :]
115
+ fig, axs = plt.subplots(4, 1, sharex="all", figsize=(20, 4 * 6), layout="tight")
116
+ fig.suptitle(f"VSrc-Sim with {config.name}, Inp={path_input.name}, E={e_out_Ws} Ws")
117
+ axs[0].set_ylabel("Voltages [V]")
118
+ axs[0].plot(stats_internal[:, 0], stats_internal[:, 1:5])
119
+ axs[0].legend(["V_cv_hold", "V_inp_Req", "V_cv_set", "V_cap"], loc="upper right")
120
+
121
+ axs[1].set_ylabel("Current [mA]")
122
+ axs[1].plot(stats_internal[:, 0], stats_internal[:, 5:8])
123
+ axs[1].legend(["C_cv_hold", "C_cv_delta", "C_out"], loc="upper right")
124
+
125
+ axs[2].set_ylabel("Power [mW]")
126
+ axs[2].plot(stats_internal[:, 0:1], stats_internal[:, 8:10])
127
+ axs[2].legend(["P_inp", "P_out"], loc="upper right")
128
+
129
+ axs[3].set_ylabel("PwrGood [n]")
130
+ axs[3].plot(stats_internal[:, 0], stats_internal[:, 10])
131
+ axs[3].legend(["PwrGood"], loc="upper right")
132
+
133
+ axs[3].set_xlabel("Runtime [s]")
134
+
135
+ for ax in axs:
136
+ # deactivates offset-creation for ax-ticks
137
+ ax.get_yaxis().get_major_formatter().set_useOffset(False)
138
+ ax.get_xaxis().get_major_formatter().set_useOffset(False)
139
+
140
+ plt.savefig(path_output.with_suffix(".png"))
141
+ plt.close(fig)
142
+ plt.clf()
143
+
78
144
  return e_out_Ws
shepherd_core/writer.py CHANGED
@@ -94,7 +94,7 @@ class Writer(Reader):
94
94
 
95
95
  comp_default: int = 1
96
96
  mode_default: str = "harvester"
97
- datatype_default: str = EnergyDType.ivsample
97
+ datatype_default: EnergyDType = EnergyDType.ivsample
98
98
 
99
99
  _chunk_shape: tuple = (Reader.samples_per_buffer,)
100
100
 
@@ -137,79 +137,72 @@ class Writer(Reader):
137
137
  self.file_path.name,
138
138
  )
139
139
 
140
+ # open file
141
+ if self._modify:
142
+ self.h5file = h5py.File(self.file_path, "r+") # = rw
143
+ else:
144
+ if not self.file_path.parent.exists():
145
+ self.file_path.parent.mkdir(parents=True)
146
+ self.h5file = h5py.File(self.file_path, "w")
147
+ # ⤷ write, truncate if exist
148
+ self._create_skeleton()
149
+
150
+ # Handle Mode
140
151
  if isinstance(mode, str) and mode not in self.mode_dtype_dict:
141
152
  msg = f"Can't handle mode '{mode}' (choose one of {self.mode_dtype_dict})"
142
153
  raise ValueError(msg)
143
154
 
144
- _dtypes = self.mode_dtype_dict[mode or self.mode_default]
155
+ if mode is not None:
156
+ self.h5file.attrs["mode"] = mode
157
+ if "mode" not in self.h5file.attrs:
158
+ self.h5file.attrs["mode"] = self.mode_default
159
+
160
+ _dtypes = self.mode_dtype_dict[self.get_mode()]
161
+
162
+ # Handle Datatype
145
163
  if isinstance(datatype, str):
146
164
  datatype = EnergyDType[datatype]
147
165
  if isinstance(datatype, EnergyDType) and datatype not in _dtypes:
148
166
  msg = f"Can't handle value '{datatype}' of datatype (choose one of {_dtypes})"
149
167
  raise ValueError(msg)
150
168
 
151
- if self._modify:
152
- if mode:
153
- self._mode = mode
154
- if not hasattr(self, "_mode"):
155
- self._mode = self.mode_default
156
- if datatype:
157
- self._datatype = datatype
158
- if not hasattr(self, "_datatype"):
159
- self._datatype = self.datatype_default
160
- if window_samples:
161
- self._window_samples = window_samples
162
- if not hasattr(self, "_window_samples"):
163
- self._window_samples = 0
164
- else:
165
- self._mode = mode if isinstance(mode, str) else self.mode_default
166
- self._datatype = (
167
- datatype if isinstance(datatype, EnergyDType) else self.datatype_default
169
+ if isinstance(datatype, EnergyDType):
170
+ self.h5file["data"].attrs["datatype"] = datatype.name
171
+ if "datatype" not in self.h5file["data"].attrs:
172
+ self.h5file["data"].attrs["datatype"] = self.datatype_default.name
173
+ if self.get_datatype() not in _dtypes:
174
+ msg = (
175
+ f"Can't handle value '{self.get_datatype()}' of datatype (choose one of {_dtypes})"
168
176
  )
169
- self._window_samples = window_samples if isinstance(window_samples, int) else 0
177
+ raise ValueError(msg)
170
178
 
179
+ # Handle Window_samples
180
+ if window_samples is not None:
181
+ self.h5file["data"].attrs["window_samples"] = window_samples
182
+ if "window_samples" not in self.h5file["data"].attrs:
183
+ self.h5file["data"].attrs["window_samples"] = 0
184
+
185
+ if datatype == EnergyDType.ivcurve and self.get_window_samples() < 1:
186
+ raise ValueError("Window Size argument needed for ivcurve-Datatype")
187
+
188
+ # Handle Cal
171
189
  if isinstance(cal_data, (CalEmu, CalHrv)):
172
- self._cal = CalSeries.from_cal(cal_data)
173
- elif isinstance(cal_data, CalSeries):
174
- self._cal = cal_data
175
- else:
176
- self._cal = CalSeries()
190
+ cal_data = CalSeries.from_cal(cal_data)
177
191
 
178
- # open file
179
- if self._modify:
180
- self.h5file = h5py.File(self.file_path, "r+") # = rw
192
+ if isinstance(cal_data, CalSeries):
193
+ for ds, param in product(["current", "voltage", "time"], ["gain", "offset"]):
194
+ self.h5file["data"][ds].attrs[param] = cal_data[ds][param]
181
195
  else:
182
- if not self.file_path.parent.exists():
183
- self.file_path.parent.mkdir(parents=True)
184
- self.h5file = h5py.File(self.file_path, "w")
185
- # write, truncate if exist
186
- self._create_skeleton()
196
+ # check if there are unset cal-values and set them to default
197
+ cal_data = CalSeries()
198
+ for ds, param in product(["current", "voltage", "time"], ["gain", "offset"]):
199
+ if param not in self.h5file["data"][ds].attrs:
200
+ self.h5file["data"][ds].attrs[param] = cal_data[ds][param]
187
201
 
188
202
  # show key parameters for h5-performance
189
203
  settings = list(self.h5file.id.get_access_plist().get_cache())
190
204
  self._logger.debug("H5Py Cache_setting=%s (_mdc, _nslots, _nbytes, _w0)", settings)
191
205
 
192
- # Store the mode in order to allow user to differentiate harvesting vs emulation data
193
- if isinstance(self._mode, str) and self._mode in self.mode_dtype_dict:
194
- self.h5file.attrs["mode"] = self._mode
195
-
196
- if (
197
- isinstance(self._datatype, EnergyDType)
198
- and self._datatype in self.mode_dtype_dict[self.get_mode()]
199
- ):
200
- self.h5file["data"].attrs["datatype"] = self._datatype.name
201
- elif not self._modify:
202
- self._logger.error("datatype invalid? '%s' not written", self._datatype)
203
-
204
- if isinstance(self._window_samples, int):
205
- self.h5file["data"].attrs["window_samples"] = self._window_samples
206
- if datatype == EnergyDType.ivcurve and (self._window_samples in {None, 0}):
207
- raise ValueError("Window Size argument needed for ivcurve-Datatype")
208
-
209
- # include cal-data
210
- for ds, param in product(["current", "voltage", "time"], ["gain", "offset"]):
211
- self.h5file["data"][ds].attrs[param] = self._cal[ds][param]
212
-
213
206
  super().__init__(file_path=None, verbose=verbose)
214
207
 
215
208
  def __enter__(self) -> Self:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shepherd_core
3
- Version: 2024.8.2
3
+ Version: 2024.11.1
4
4
  Summary: Programming- and CLI-Interface for the h5-dataformat of the Shepherd-Testbed
5
5
  Author-email: Ingmar Splitt <ingmar.splitt@tu-dresden.de>
6
6
  Maintainer-email: Ingmar Splitt <ingmar.splitt@tu-dresden.de>
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.9
23
23
  Classifier: Programming Language :: Python :: 3.10
24
24
  Classifier: Programming Language :: Python :: 3.11
25
25
  Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
26
27
  Classifier: License :: OSI Approved :: MIT License
27
28
  Classifier: Operating System :: OS Independent
28
29
  Classifier: Natural Language :: English
@@ -119,11 +120,11 @@ These pydantic data-models are used throughout all shepherd interfaces. Users ca
119
120
 
120
121
  ## Compatibility
121
122
 
122
- | OS | PyVersion | Comment |
123
- |---------|--------------|--------------------------------------------|
124
- | Ubuntu | 3.8 - 3.12 | |
125
- | Windows | 3.8 - 3.12 | no support for elf and hex-conversions yet |
126
- | MacOS | 3.8 - 3.12 | hex-conversion missing |
123
+ | OS | PyVersion | Comment |
124
+ |---------|------------|--------------------------------------------|
125
+ | Ubuntu | 3.8 - 3.13 | |
126
+ | Windows | 3.8 - 3.13 | no support for elf and hex-conversions yet |
127
+ | MacOS | 3.8 - 3.13 | hex-conversion missing |
127
128
 
128
129
  Notes:
129
130
  - hex-conversion needs a working and accessible objcopy
@@ -147,7 +148,7 @@ For bleeding-edge-features or dev-work it is possible to install directly from G
147
148
  ```Shell
148
149
  pip install git+https://github.com/orgua/shepherd-datalib.git@dev#subdirectory=shepherd_core -U
149
150
  # and on sheep with newer debian
150
- pip install git+https://github.com/orgua/shepherd-datalib.git@dev#subdirectory=shepherd_core -U --break-system-packages
151
+ sudo pip install git+https://github.com/orgua/shepherd-datalib.git@dev#subdirectory=shepherd_core -U --break-system-packages
151
152
  ```
152
153
 
153
154
  If you are working with ``.elf``-files (embedding into experiments) you make "objcopy" accessible to python. In Ubuntu, you can either install ``build-essential`` or ``binutils-$ARCH`` with arch being ``msp430`` or ``arm-none-eabi`` for the nRF52.
@@ -2,9 +2,9 @@ shepherd_core/__init__.py,sha256=QyqENyf508XfZQ4vDU5o6UL9rmIqkf8kzwgTF9XU1-Y,127
2
2
  shepherd_core/calibration_hw_def.py,sha256=_nMzgNzSnYyqcLnVCGd4tfA2e0avUXbccjmNpFhiDgo,2830
3
3
  shepherd_core/commons.py,sha256=vymKXWcy_1bz7ChzzEATUkJ4p3czCzjIdsSehVjJOY8,218
4
4
  shepherd_core/logger.py,sha256=4Q4hTI-nccOZ1_A68fo4UEctfu3pJx3IeHfa9VuDDEo,1804
5
- shepherd_core/reader.py,sha256=xpzwt1JjubNat0PNwdzmBJltLPOmvCyQGwcC-bq83ZI,27052
6
- shepherd_core/version.py,sha256=2UChDgJ4n71vSZH0_L2ArDetMFRZd-5aZO5ppY2Yhrg,75
7
- shepherd_core/writer.py,sha256=t53fXkYUWHhYnOFnHCoMWwUADkgYydy9Dg66PiqPa68,14946
5
+ shepherd_core/reader.py,sha256=Sg7s5UfV01CejI3_nolG7qrSDUBenmt1WPo2BPJee8o,27058
6
+ shepherd_core/version.py,sha256=_cAEEjp9MalkLrnbibrPNp6zEr3U5KIC-gmZvPG5QqA,76
7
+ shepherd_core/writer.py,sha256=GMR-7vkOgpTNPoknBWsRsC7-b7iGMtT60-KtSKunNe8,14636
8
8
  shepherd_core/data_models/__init__.py,sha256=IVjKbT2Ilz5bev325EvAuuhd9LfQgQ1u7qKo6dhVA2k,1866
9
9
  shepherd_core/data_models/readme.md,sha256=1bdfEypY_0NMhXLxOPRnLAsFca0HuHdq7_01yEWxvUs,2470
10
10
  shepherd_core/data_models/virtual_source_doc.txt,sha256=KizMcfGKj7BnHIbaJHT7KeTF01SV__UXv01qV_DGHSs,6057
@@ -17,20 +17,20 @@ shepherd_core/data_models/base/timezone.py,sha256=2T6E46hJ1DAvmqKfu6uIgCK3RSoAKj
17
17
  shepherd_core/data_models/base/wrapper.py,sha256=Izp17HFCKNAS3TnWcPn3MM9fWdc3A-F7eDyAsYlyWCw,755
18
18
  shepherd_core/data_models/content/__init__.py,sha256=wVa5lw6bS-fBgeo-SWydg6rw8AsScxqNgDo81dzteaE,537
19
19
  shepherd_core/data_models/content/_external_fixtures.yaml,sha256=0CH7YSWT_hzL-jcg4JjgN9ryQOzbS8S66_pd6GbMnHw,12259
20
- shepherd_core/data_models/content/energy_environment.py,sha256=bXInmHzlRjBAt7mitig35V-zCfj98ZvGEBio0miSxRg,1425
20
+ shepherd_core/data_models/content/energy_environment.py,sha256=WuXMkKqnibGzM2WeW1_m2DAsc0fDqE9CkBYYPSw-7eA,1540
21
21
  shepherd_core/data_models/content/energy_environment_fixture.yaml,sha256=UBXTdGT7MK98zx5w_RBCu-f9uNCKxRgiFBQFbmDUxPc,1301
22
22
  shepherd_core/data_models/content/firmware.py,sha256=MyEiaP6bkOm7i_oihDXTxHC7ajc5aqiIDLn7mhap6YY,5722
23
23
  shepherd_core/data_models/content/firmware_datatype.py,sha256=XPU9LOoT3h5qFOlE8WU0vAkw-vymNxzor9kVFyEqsWg,255
24
- shepherd_core/data_models/content/virtual_harvester.py,sha256=52HNac5k_GBCAhT2jBgu8oIqnvwMN3WMVmsAJrrxVRo,9678
25
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml,sha256=1u-V9RvFhbU74wEUv-BMQugh7OdzBE1SvhVe1v0DN-8,4251
26
- shepherd_core/data_models/content/virtual_source.py,sha256=2ebNI1CvZgcfY5nz9P9p-Vr26E9l9bli9YeEd6_7yBY,14364
27
- shepherd_core/data_models/content/virtual_source_fixture.yaml,sha256=8Y1HwGRUPKTA6xwI74MFXFYUT018aI7WRlkPIhODwcQ,10525
24
+ shepherd_core/data_models/content/virtual_harvester.py,sha256=MXmSJ_nRp1mSzxfTNk60o9h5Yrp2lFMbLphUVSnNeNc,9999
25
+ shepherd_core/data_models/content/virtual_harvester_fixture.yaml,sha256=LZe5ue1xYhXZwB3a32sva-L4uKhkQA5AtG9JzW4B2hQ,4564
26
+ shepherd_core/data_models/content/virtual_source.py,sha256=PPAphxEXvgMM7OVZ2dBkYAvJQkmj5Kb2BYFogVUs7B8,15354
27
+ shepherd_core/data_models/content/virtual_source_fixture.yaml,sha256=1o-31mGgn7eyCNidKoOUp9vZh3K4Al0kJgmz54Q2DAE,11191
28
28
  shepherd_core/data_models/experiment/__init__.py,sha256=9TE9_aSnCNRhagsIWLTE8XkyjyMGB7kEGdswl-296v0,645
29
29
  shepherd_core/data_models/experiment/experiment.py,sha256=wnn6T3czuh4rz6OSYtMltCTbRpPX55TLVAtQcKO7Uhg,4044
30
30
  shepherd_core/data_models/experiment/observer_features.py,sha256=qxnb7anuQz9ZW5IUlPdUXYPIl5U7O9uXkJqZtMnAb0Y,5156
31
31
  shepherd_core/data_models/experiment/target_config.py,sha256=XIsjbbo7yn_A4q3GMxWbiNzEGA0Kk5gH7-XfQQ7Kg0E,3674
32
32
  shepherd_core/data_models/task/__init__.py,sha256=rZLbgqX-dTWY4026-bqW-IWVHbA6C_xP9y0aeRze8FY,3374
33
- shepherd_core/data_models/task/emulation.py,sha256=z3BEqERy2m3_rxqKo6w89sSnBQhFwHDEdCgwJh2XUOQ,6376
33
+ shepherd_core/data_models/task/emulation.py,sha256=tLb5auHOgdoG-e4hFljAYT49z7lMEaiimOy4UVZONi4,6440
34
34
  shepherd_core/data_models/task/firmware_mod.py,sha256=Rw_TA1ykQ7abUd_U0snqZlpZyrS8Nx6f4BEax1Xnji0,2818
35
35
  shepherd_core/data_models/task/harvest.py,sha256=HHnqWwRsJupaZJxuohs7NrK6VaDyoRzGOaG2h9y3s1Y,3360
36
36
  shepherd_core/data_models/task/observer_tasks.py,sha256=XlH_-EGRrdodTn0c2pjGvpcauc0a9NOnLhysKw8iRwk,3511
@@ -67,14 +67,14 @@ shepherd_core/testbed_client/client_web.py,sha256=iMh5T91152uugbFsqr2vvxLser0KIo
67
67
  shepherd_core/testbed_client/fixtures.py,sha256=4Uk583R4r6I5IB78HxOn-9UNH3sbFha7OPEdcSXvMCU,9939
68
68
  shepherd_core/testbed_client/user_model.py,sha256=5M3vWkAGBwdGDUYAanAjrZwpzMBlh3XLOVvNYWiLmms,2107
69
69
  shepherd_core/vsource/__init__.py,sha256=GVB-FwuO2mvM15mGX9EQC1lbUmHMLmUEFGYkGmIngPM,771
70
- shepherd_core/vsource/target_model.py,sha256=J_8-MHs_gIMcE3ZYTgoFsrO6VShR6SFJ5CA3lwuPC2U,2049
71
- shepherd_core/vsource/virtual_converter_model.py,sha256=sQkJSj-7CVabHvXqk6C3cbLmztSSsdrSU3WgYr4h30E,11067
72
- shepherd_core/vsource/virtual_harvester_model.py,sha256=ROR8vtKeM2WTnogV68TKBOu0zRVwOwQj_q67hW_qtpQ,8297
70
+ shepherd_core/vsource/target_model.py,sha256=LaB5ppi2-IIpIepDqDvOliR-BupzccJl44yRxjlF-ms,5113
71
+ shepherd_core/vsource/virtual_converter_model.py,sha256=3TyxphUMunoGhMda7AWCHZQU8pjRSvxB-9R8lfZFnok,11592
72
+ shepherd_core/vsource/virtual_harvester_model.py,sha256=GyA0uGl3r42t5c4roYtEaj22b0-b5DAHUr2e9DuNn-c,9765
73
73
  shepherd_core/vsource/virtual_harvester_simulation.py,sha256=EiBrvmc6D2N7Z0DqFBWPdRJK6hR8Q3iQaj7EYP9pLA0,2405
74
- shepherd_core/vsource/virtual_source_model.py,sha256=fjN8myTY3I_LpikF_aGAcxes3RGu1GP23P7XKC_UIyA,2737
75
- shepherd_core/vsource/virtual_source_simulation.py,sha256=swopkXe60LozBkp_-e1WCla-eVCG2klTFB5bLAUj51I,2528
76
- shepherd_core-2024.8.2.dist-info/METADATA,sha256=oDQiB1H6NM3zW-SuQp0uZ1Ap366o5syvqE-5RLWrIws,7771
77
- shepherd_core-2024.8.2.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
78
- shepherd_core-2024.8.2.dist-info/top_level.txt,sha256=wy-t7HRBrKARZxa-Y8_j8d49oVHnulh-95K9ikxVhew,14
79
- shepherd_core-2024.8.2.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
80
- shepherd_core-2024.8.2.dist-info/RECORD,,
74
+ shepherd_core/vsource/virtual_source_model.py,sha256=-JSYUfsnYlNo5RfPBhx2G33fo5AjSeFSf2O6unroyFw,2945
75
+ shepherd_core/vsource/virtual_source_simulation.py,sha256=k1v2zpNdJTqiO9uY8TXaq-IUKK6m5l-LEWebYva0skk,5088
76
+ shepherd_core-2024.11.1.dist-info/METADATA,sha256=TPLadz0PyPjY4RxradpDtestEq_6U2ILu9tVNIIG7q0,7818
77
+ shepherd_core-2024.11.1.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
78
+ shepherd_core-2024.11.1.dist-info/top_level.txt,sha256=wy-t7HRBrKARZxa-Y8_j8d49oVHnulh-95K9ikxVhew,14
79
+ shepherd_core-2024.11.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
80
+ shepherd_core-2024.11.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (72.1.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5