shepherd-core 2024.7.4__py3-none-any.whl → 2024.8.2__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.
@@ -51,7 +51,7 @@ class CalibrationPair(ShpModel):
51
51
 
52
52
  gain: PositiveFloat
53
53
  offset: float = 0
54
- # TODO: add unit
54
+ unit: Optional[str] = None # TODO: add units when used
55
55
 
56
56
  def raw_to_si(self, values_raw: Calc_t, *, allow_negative: bool = True) -> Calc_t:
57
57
  """Convert between physical units and raw unsigned integers."""
@@ -78,14 +78,11 @@ class CalibrationPair(ShpModel):
78
78
  return values_raw
79
79
 
80
80
  @classmethod
81
- def from_fn(cls, fn: Callable) -> Self:
81
+ def from_fn(cls, fn: Callable, unit: Optional[str] = None) -> Self:
82
82
  """Probe linear function to determine scaling values."""
83
83
  offset = fn(0, limited=False)
84
84
  gain_inv = fn(1.0, limited=False) - offset
85
- return cls(
86
- gain=1.0 / float(gain_inv),
87
- offset=-float(offset) / gain_inv,
88
- )
85
+ return cls(gain=1.0 / float(gain_inv), offset=-float(offset) / gain_inv, unit=unit)
89
86
 
90
87
 
91
88
  cal_hrv_legacy = { # legacy translator
@@ -99,10 +96,10 @@ cal_hrv_legacy = { # legacy translator
99
96
  class CalibrationHarvester(ShpModel):
100
97
  """Container for all calibration-pairs for that device."""
101
98
 
102
- dac_V_Hrv: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw)
103
- dac_V_Sim: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw)
104
- adc_V_Sense: CalibrationPair = CalibrationPair.from_fn(adc_voltage_to_raw)
105
- adc_C_Hrv: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw)
99
+ dac_V_Hrv: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
100
+ dac_V_Sim: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
101
+ adc_V_Sense: CalibrationPair = CalibrationPair.from_fn(adc_voltage_to_raw, unit="V")
102
+ adc_C_Hrv: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
106
103
 
107
104
  def export_for_sysfs(self) -> dict:
108
105
  """Convert and write the essential data.
@@ -143,10 +140,10 @@ class CalibrationEmulator(ShpModel):
143
140
  Differentiates between both target-ports A/B.
144
141
  """
145
142
 
146
- dac_V_A: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw)
147
- dac_V_B: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw)
148
- adc_C_A: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw)
149
- adc_C_B: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw)
143
+ dac_V_A: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
144
+ dac_V_B: CalibrationPair = CalibrationPair.from_fn(dac_voltage_to_raw, unit="V")
145
+ adc_C_A: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
146
+ adc_C_B: CalibrationPair = CalibrationPair.from_fn(adc_current_to_raw, unit="A")
150
147
 
151
148
  def export_for_sysfs(self) -> dict:
152
149
  """Convert and write the essential data.
@@ -233,10 +230,11 @@ class CalibrationCape(ShpModel):
233
230
 
234
231
  """
235
232
  dv = cls().model_dump(include={"harvester", "emulator"})
236
- lw = list(dict_generator(dv))
237
- values = struct.unpack(">" + len(lw) * "d", data)
233
+ lw1 = list(dict_generator(dv))
234
+ lw2 = [elem for elem in lw1 if isinstance(elem[-1], float)]
235
+ values = struct.unpack(">" + len(lw2) * "d", data)
238
236
  # ⤷ X => double float, big endian
239
- for _i, walk in enumerate(lw):
237
+ for _i, walk in enumerate(lw2):
240
238
  # hardcoded fixed depth ... bad but easy
241
239
  dv[walk[0]][walk[1]][walk[2]] = float(values[_i])
242
240
  dv["cape"] = cape
@@ -252,18 +250,18 @@ class CalibrationCape(ShpModel):
252
250
 
253
251
  """
254
252
  lw = list(dict_generator(self.model_dump(include={"harvester", "emulator"})))
255
- values = [walk[-1] for walk in lw]
256
- return struct.pack(">" + len(lw) * "d", *values)
253
+ values = [walk[-1] for walk in lw if isinstance(walk[-1], float)]
254
+ return struct.pack(">" + len(values) * "d", *values)
257
255
 
258
256
 
259
257
  class CalibrationSeries(ShpModel):
260
258
  """Cal-Data for a typical recording of a testbed experiment."""
261
259
 
262
- voltage: CalibrationPair = CalibrationPair(gain=3 * 1e-9)
260
+ voltage: CalibrationPair = CalibrationPair(gain=3 * 1e-9, unit="V")
263
261
  # ⤷ default allows 0 - 12 V in 3 nV-Steps
264
- current: CalibrationPair = CalibrationPair(gain=250 * 1e-12)
262
+ current: CalibrationPair = CalibrationPair(gain=250 * 1e-12, unit="A")
265
263
  # ⤷ default allows 0 - 1 A in 250 pA - Steps
266
- time: CalibrationPair = CalibrationPair(gain=1e-9)
264
+ time: CalibrationPair = CalibrationPair(gain=1e-9, unit="s")
267
265
  # ⤷ default = nanoseconds
268
266
 
269
267
  @classmethod
@@ -149,20 +149,27 @@ class VirtualHarvesterConfig(ContentModel, title="Config for the Harvester"):
149
149
 
150
150
  def calc_window_size(
151
151
  self,
152
- dtype_in: Optional[EnergyDType] = EnergyDType.ivsample,
152
+ dtype_in: Optional[EnergyDType] = None,
153
153
  *,
154
154
  for_emu: bool,
155
155
  ) -> int:
156
- if for_emu:
157
- if dtype_in == EnergyDType.ivcurve:
158
- return self.samples_n * (1 + self.wait_cycles)
159
- if dtype_in == EnergyDType.ivsample:
160
- return 0
161
- # isc_voc: 2 * (1 + wait_cycles), noqa
162
- raise NotImplementedError
156
+ if not for_emu:
157
+ # TODO: should be named 'for_ivcurve_recording'
158
+ # TODO: add extra variable to distinguish step_count
159
+ # and window_size (currently mixed together)
160
+ # only used by ivcurve algo (in ADC-Mode)
161
+ return self.samples_n
163
162
 
164
- # only used by ivcurve algo (in ADC-Mode)
165
- return self.samples_n
163
+ if dtype_in is None:
164
+ dtype_in = self.get_datatype()
165
+
166
+ if dtype_in == EnergyDType.ivcurve:
167
+ return self.samples_n * (1 + self.wait_cycles)
168
+ if dtype_in == EnergyDType.ivsample:
169
+ return 0
170
+ if dtype_in == EnergyDType.isc_voc:
171
+ return 2 * (1 + self.wait_cycles)
172
+ raise NotImplementedError
166
173
 
167
174
 
168
175
  u32 = Annotated[int, Field(ge=0, lt=2**32)]
@@ -234,7 +241,7 @@ class HarvesterPRUConfig(ShpModel):
234
241
  dtype_in = EnergyDType[dtype_in]
235
242
  if for_emu and dtype_in not in {EnergyDType.ivsample, EnergyDType.ivcurve}:
236
243
  raise NotImplementedError
237
- # TODO: use dtype properly in shepherd
244
+
238
245
  interval_ms, duration_ms = data.calc_timings_ms(for_emu=for_emu)
239
246
  return cls(
240
247
  algorithm=data.calc_algorithm_num(for_emu=for_emu),
@@ -19,13 +19,13 @@
19
19
  id: 1010
20
20
  name: ivcurve
21
21
  description: Postpone harvesting by sampling ivcurves (voltage stepped as sawtooth-wave)
22
- comment: ~200 Hz
22
+ comment: ~110 Hz, Between 50 & 60 Hz line-frequency to avoid standing waves
23
23
  inherit_from: neutral
24
24
  algorithm: ivcurve
25
- samples_n: 250
25
+ samples_n: 909
26
26
  voltage_min_mV: 0
27
27
  voltage_max_mV: 5000
28
- wait_cycles: 1 # results in 200 Hz (= 100kHz /(2*250))
28
+ wait_cycles: 0
29
29
  rising: false # downward sawtooth seems to have advantages for solar cells
30
30
  # todo: also add switch for sawtooth- vs triangle-wave?
31
31
  # todo: could also include a version with dynamic upper-boundary, varied if voc is reached very early
@@ -38,20 +38,17 @@
38
38
 
39
39
  - datatype: VirtualHarvesterConfig
40
40
  parameters:
41
- id: 1012
42
- name: iv1000
43
- comment: Name relates to curves per second
41
+ id: 1013
42
+ name: iv110 # synonym
44
43
  inherit_from: ivcurve
45
- samples_n: 100
46
- wait_cycles: 0
47
44
 
48
45
  - datatype: VirtualHarvesterConfig
49
46
  parameters:
50
- id: 1013
51
- name: iv110
52
- comment: Between 50 & 60 Hz line-frequency to avoid standing waves
47
+ id: 1012
48
+ name: iv1000
49
+ comment: Name relates to curves per second
53
50
  inherit_from: ivcurve
54
- samples_n: 909
51
+ samples_n: 100
55
52
  wait_cycles: 0
56
53
 
57
54
  - datatype: VirtualHarvesterConfig
@@ -61,7 +58,7 @@
61
58
  description: Postpone harvesting by sampling short circuit current & open circuit voltage
62
59
  inherit_from: neutral
63
60
  algorithm: isc_voc
64
- wait_cycles: 1 # results in 25 kHz (isc, wait, voc, wait)
61
+ wait_cycles: 4 # results in 10 kHz (isc, wait, voc, wait)
65
62
 
66
63
  - datatype: VirtualHarvesterConfig
67
64
  parameters:
@@ -76,6 +76,8 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
76
76
  # final (always last) stage to compensate undetectable current spikes
77
77
  # when enabling power for target
78
78
  C_output_uF: Annotated[float, Field(ge=0, le=4.29e6)] = 1.0
79
+ # TODO: C_output is handled internally as delta-V, but should be a I_transient
80
+ # that makes it visible in simulation as additional i_out_drain
79
81
 
80
82
  # Extra
81
83
  V_output_log_gpio_threshold_mV: Annotated[float, Field(ge=0, le=4.29e6)] = 1_400
@@ -93,7 +95,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
93
95
  # influence of cap-voltage is not implemented
94
96
  LUT_input_V_min_log2_uV: Annotated[int, Field(ge=0, le=20)] = 0
95
97
  # ⤷ 2^7 = 128 uV -> LUT[0][:] is for inputs < 128 uV
96
- LUT_input_I_min_log2_nA: Annotated[int, Field(ge=0, le=20)] = 0
98
+ LUT_input_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
97
99
  # ⤷ 2^8 = 256 nA -> LUT[:][0] is for inputs < 256 nA
98
100
 
99
101
  # Buck Converter
@@ -103,7 +105,7 @@ class VirtualSourceConfig(ContentModel, title="Config for the virtual Source"):
103
105
 
104
106
  LUT_output_efficiency: LUT1D = 12 * [1.00]
105
107
  # ⤷ array[12] depending on output_current
106
- LUT_output_I_min_log2_nA: Annotated[int, Field(ge=0, le=20)] = 0
108
+ LUT_output_I_min_log2_nA: Annotated[int, Field(ge=1, le=20)] = 1
107
109
  # ⤷ 2^8 = 256 nA -> LUT[0] is for inputs < 256 nA, see notes on LUT_input for explanation
108
110
 
109
111
  @model_validator(mode="before")
@@ -318,8 +320,8 @@ class ConverterPRUConfig(ShpModel):
318
320
  V_buck_drop_uV=round(data.V_buck_drop_mV * 1e3),
319
321
  # LUTs
320
322
  LUT_input_V_min_log2_uV=data.LUT_input_V_min_log2_uV,
321
- LUT_input_I_min_log2_nA=data.LUT_input_I_min_log2_nA,
322
- LUT_output_I_min_log2_nA=data.LUT_output_I_min_log2_nA,
323
+ LUT_input_I_min_log2_nA=data.LUT_input_I_min_log2_nA - 1, # sub-1 due to later log2-op
324
+ LUT_output_I_min_log2_nA=data.LUT_output_I_min_log2_nA - 1, # sub-1 due to later log2
323
325
  LUT_inp_efficiency_n8=[
324
326
  [min(255, round(256 * ival)) for ival in il] for il in data.LUT_input_efficiency
325
327
  ],
@@ -15,7 +15,7 @@
15
15
  interval_startup_delay_drain_ms: 0
16
16
 
17
17
  harvester:
18
- name: mppt_opt # harvester only active if input is "ivcurves"
18
+ name: mppt_opt # harvester only active if input is "ivcurve"
19
19
 
20
20
  V_input_max_mV: 10000
21
21
  I_input_max_mA: 4200
@@ -56,16 +56,18 @@
56
56
  [ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00 ],
57
57
  [ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00 ],
58
58
  [ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00 ],
59
- ] # input-array[12][12] depending on array[inp_voltage][log(inp_current)], influence of cap-voltage is not implemented
59
+ ]
60
+ # input-array[12][12] depending on array[inp_voltage][log(inp_current)],
61
+ # influence of cap-voltage is not implemented
60
62
  LUT_input_V_min_log2_uV: 0 # 2^7 = 128 uV -> array[0] is for inputs < 128 uV
61
- LUT_input_I_min_log2_nA: 0 # 2^8 = 256 nA -> array[0] is for inputs < 256 nA
63
+ LUT_input_I_min_log2_nA: 1 # 2^8 = 256 nA -> array[0] is for inputs < 256 nA
62
64
 
63
65
  # Buck-converter
64
66
  V_output_mV: 2400
65
67
  V_buck_drop_mV: 0.0 # simulate LDO min voltage differential or output-diode
66
68
 
67
69
  LUT_output_efficiency: [ 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00, 1.00 ] # array[12] depending on output_current
68
- LUT_output_I_min_log2_nA: 0 # 2^8 = 256 nA -> array[0] is for inputs < 256 nA, see notes on LUT_input for explanation
70
+ LUT_output_I_min_log2_nA: 1 # 2^8 = 256 nA -> array[0] is for inputs < 256 nA, see notes on LUT_input for explanation
69
71
 
70
72
  owner: Ingmar
71
73
  group: NES Lab
@@ -114,7 +116,7 @@
114
116
  parameters:
115
117
  id: 1020
116
118
  name: BQ25504
117
- description: TI BQ25504 with integrated boost-converter
119
+ description: TI BQ25504 with integrated boost-converter. Values are taken from the DK-Board.
118
120
  inherit_from: neutral # to complete undefined vars
119
121
  enable_boost: true # if false -> v_intermediate = v_input, output-switch-hysteresis is still usable
120
122
 
@@ -124,16 +126,16 @@
124
126
  V_input_max_mV: 3000
125
127
  I_input_max_mA: 100
126
128
 
127
- C_intermediate_uF: 22.0 # primary storage-Cap
129
+ C_intermediate_uF: 100.0 # primary storage-Cap
128
130
  V_intermediate_init_mV: 3000 # allow a proper / fast startup
129
131
  I_intermediate_leak_nA: 330
130
132
 
131
- V_intermediate_enable_threshold_mV: 2600 # -> target gets connected (hysteresis-combo with next value)
132
- V_intermediate_disable_threshold_mV: 2300 # -> target gets disconnected
133
+ V_intermediate_enable_threshold_mV: 1000 # -> target gets connected (hysteresis-combo with next value)
134
+ V_intermediate_disable_threshold_mV: 0 # -> target gets disconnected
133
135
  interval_check_thresholds_ms: 64.0 # some BQs check every 64 ms if output should be disconnected
134
136
 
135
137
  V_pwr_good_enable_threshold_mV: 2800 # target is informed by pwr-good on output-pin (hysteresis) -> for intermediate voltage
136
- V_pwr_good_disable_threshold_mV: 2400
138
+ V_pwr_good_disable_threshold_mV: 2340
137
139
  immediate_pwr_good_signal: false # 1: activate instant schmitt-trigger, 0: stay in interval for checking thresholds
138
140
 
139
141
  # Boost Converter
shepherd_core/reader.py CHANGED
@@ -84,6 +84,7 @@ class Reader:
84
84
 
85
85
  # init stats
86
86
  self.runtime_s: float = 0
87
+ self.buffers_n: int = 0
87
88
  self.file_size: int = 0
88
89
  self.data_rate: float = 0
89
90
 
@@ -170,12 +171,16 @@ class Reader:
170
171
  def _refresh_file_stats(self) -> None:
171
172
  """Update internal states, helpful after resampling or other changes in data-group."""
172
173
  self.h5file.flush()
173
- if (self.ds_time.shape[0] > 1) and (self.ds_time[1] != self.ds_time[0]):
174
- # this assumes isochronal sampling
175
- self.sample_interval_s = self._cal.time.raw_to_si(self.ds_time[1] - self.ds_time[0])
174
+ sample_count = self.ds_time.shape[0]
175
+ duration_raw = self.ds_time[sample_count - 1] - self.ds_time[0] if sample_count > 0 else 0
176
+ if (sample_count > 0) and (duration_raw > 0):
177
+ # this assumes iso-chronous sampling
178
+ duration_s = self._cal.time.raw_to_si(duration_raw)
179
+ self.sample_interval_s = duration_s / sample_count
176
180
  self.sample_interval_ns = int(10**9 * self.sample_interval_s)
177
- self.samplerate_sps = max(int(10**9 // self.sample_interval_ns), 1)
181
+ self.samplerate_sps = max(int((sample_count - 1) / duration_s), 1)
178
182
  self.runtime_s = round(self.ds_voltage.shape[0] / self.samplerate_sps, 1)
183
+ self.buffers_n = int(self.ds_voltage.shape[0] // self.samples_per_buffer)
179
184
  if isinstance(self.file_path, Path):
180
185
  self.file_size = self.file_path.stat().st_size
181
186
  else:
shepherd_core/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Separated string avoids circular imports."""
2
2
 
3
- version: str = "2024.7.4"
3
+ version: str = "2024.8.2"
@@ -1,13 +1,23 @@
1
1
  """Simulation model of the virtual source."""
2
2
 
3
+ from .target_model import ConstantCurrentTarget
4
+ from .target_model import ConstantPowerTarget
5
+ from .target_model import ResistiveTarget
3
6
  from .virtual_converter_model import PruCalibration
4
7
  from .virtual_converter_model import VirtualConverterModel
5
8
  from .virtual_harvester_model import VirtualHarvesterModel
9
+ from .virtual_harvester_simulation import simulate_harvester
6
10
  from .virtual_source_model import VirtualSourceModel
11
+ from .virtual_source_simulation import simulate_source
7
12
 
8
13
  __all__ = [
9
14
  "PruCalibration",
10
15
  "VirtualConverterModel",
11
16
  "VirtualHarvesterModel",
12
17
  "VirtualSourceModel",
18
+ "simulate_harvester",
19
+ "simulate_source",
20
+ "ResistiveTarget",
21
+ "ConstantCurrentTarget",
22
+ "ConstantPowerTarget",
13
23
  ]
@@ -0,0 +1,62 @@
1
+ """Virtual targets with different characteristics.
2
+
3
+ TODO: add more targets
4
+ - diode
5
+ - constant power
6
+ - constant current
7
+ - msp430 (const I)
8
+ - nRF (constant power due to regulator)
9
+ - riotee
10
+ """
11
+
12
+ from abc import ABC
13
+ from abc import abstractmethod
14
+
15
+
16
+ class TargetABC(ABC):
17
+ """Abstract base class for all targets."""
18
+
19
+ @abstractmethod
20
+ def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
21
+ """Calculate one time step and return drawn current in nA."""
22
+
23
+
24
+ class ResistiveTarget(TargetABC):
25
+ """Predictable target for matching the real world."""
26
+
27
+ def __init__(self, resistance_Ohm: float, *, controlled: bool = False) -> None:
28
+ if resistance_Ohm <= 1e-3:
29
+ raise ValueError("Resistance must be greater than 1 mOhm.")
30
+ self.r_kOhm = 1e-3 * resistance_Ohm
31
+ self.ctrl = controlled
32
+
33
+ def step(self, voltage_uV: int, *, pwr_good: bool) -> float:
34
+ if pwr_good or not self.ctrl:
35
+ return voltage_uV / self.r_kOhm # = nA
36
+ return 0
37
+
38
+
39
+ class ConstantCurrentTarget(TargetABC):
40
+ """Recreate simple MCU without integrated regulator."""
41
+
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:
44
+ 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
47
+
48
+ 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
50
+
51
+
52
+ class ConstantPowerTarget(TargetABC):
53
+ """Recreate MCU with integrated regulator."""
54
+
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:
57
+ 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
60
+
61
+ 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
@@ -25,12 +25,28 @@ from ..data_models.content.virtual_source import ConverterPRUConfig
25
25
  class PruCalibration:
26
26
  """part of calibration.h."""
27
27
 
28
+ # negative residue compensation - compensate for noise around 0
29
+ # -> current uint-design cuts away negative part and leads to biased mean()
30
+ NOISE_ESTIMATE_nA: int = 2000
31
+ RESIDUE_SIZE_FACTOR: int = 30
32
+ RESIDUE_MAX_nA: int = NOISE_ESTIMATE_nA * RESIDUE_SIZE_FACTOR
33
+ negative_residue_nA = 0
34
+
28
35
  def __init__(self, cal_emu: Optional[CalibrationEmulator] = None) -> None:
29
36
  self.cal = cal_emu if cal_emu else CalibrationEmulator()
30
37
 
31
38
  def conv_adc_raw_to_nA(self, current_raw: int) -> float:
32
- return self.cal.adc_C_A.raw_to_si(current_raw) * (10**9)
33
- # TODO: add feature "negative residue compensation" to here
39
+ I_nA = self.cal.adc_C_A.raw_to_si(current_raw) * (10**9)
40
+ if self.cal.adc_C_A.offset < 0:
41
+ if I_nA > self.negative_residue_nA:
42
+ I_nA -= self.negative_residue_nA
43
+ self.negative_residue_nA = 0
44
+ else:
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
48
+ I_nA = 0
49
+ return I_nA
34
50
 
35
51
  @staticmethod
36
52
  def conv_adc_raw_to_uV(voltage_raw: int) -> float:
@@ -88,10 +104,11 @@ class VirtualConverterModel:
88
104
  self.vsource_skip_gpio_logging: bool = False
89
105
 
90
106
  def calc_inp_power(self, input_voltage_uV: float, input_current_nA: float) -> int:
91
- # Next 2 lines are Python-specific
107
+ # Next 2 lines are Python-specific (model unsigned int)
92
108
  input_voltage_uV = max(0.0, input_voltage_uV)
93
109
  input_current_nA = max(0.0, input_current_nA)
94
110
 
111
+ # Input diode
95
112
  if input_voltage_uV > self._cfg.V_input_drop_uV:
96
113
  input_voltage_uV -= self._cfg.V_input_drop_uV
97
114
  else:
@@ -108,8 +125,6 @@ class VirtualConverterModel:
108
125
  if self.enable_boost:
109
126
  if input_voltage_uV < self._cfg.V_input_boost_threshold_uV:
110
127
  input_voltage_uV = 0.0
111
- if input_voltage_uV > self.V_mid_uV:
112
- input_voltage_uV = self.V_mid_uV
113
128
  elif not self.enable_storage:
114
129
  # direct connection
115
130
  self.V_mid_uV = input_voltage_uV
@@ -134,7 +149,7 @@ class VirtualConverterModel:
134
149
  return round(self.P_inp_fW) # Python-specific, added for easier testing
135
150
 
136
151
  def calc_out_power(self, current_adc_raw: int) -> int:
137
- # Next 2 lines are Python-specific
152
+ # Next 2 lines are Python-specific (model unsigned int)
138
153
  current_adc_raw = max(0, current_adc_raw)
139
154
  current_adc_raw = min((2**18) - 1, current_adc_raw)
140
155
 
@@ -35,7 +35,11 @@ class VirtualHarvesterModel:
35
35
 
36
36
  # INIT global vars: shared states
37
37
  self.voltage_set_uV: int = self._cfg.voltage_uV + 1
38
- self.interval_step: int = 2**30
38
+ if self._cfg.interval_n > 2 * self._cfg.window_size:
39
+ self.interval_step = self._cfg.interval_n - (2 * self._cfg.window_size)
40
+ else:
41
+ self.interval_step = 2**30
42
+ # ⤷ intake two ivcurves before overflow / reset
39
43
  self.is_rising: bool = (self._cfg.hrv_mode & (2**1)) != 0
40
44
 
41
45
  # PO-Relevant, iv & adc
@@ -51,6 +55,7 @@ class VirtualHarvesterModel:
51
55
  self.voltage_hold: int = 0
52
56
  self.current_hold: int = 0
53
57
  self.voltage_step_x4_uV: int = self._cfg.voltage_step_uV * 4
58
+ self.age_max: int = 2 * self._cfg.window_size
54
59
 
55
60
  # INIT static vars: CV
56
61
  self.voltage_last: int = 0
@@ -59,9 +64,10 @@ class VirtualHarvesterModel:
59
64
 
60
65
  # INIT static vars: VOC
61
66
  self.age_now: int = 0
62
- self.voc_now: int = 0
67
+ self.voc_now: int = self._cfg.voltage_max_uV
63
68
  self.age_nxt: int = 0
64
- self.voc_nxt: int = 0
69
+ self.voc_nxt: int = self._cfg.voltage_max_uV
70
+ self.voc_min: int = max(1000, self._cfg.voltage_min_uV)
65
71
 
66
72
  # INIT static vars: PO
67
73
  # already done: interval step
@@ -110,20 +116,22 @@ class VirtualHarvesterModel:
110
116
  return self.voltage_hold, self.current_hold
111
117
 
112
118
  def ivcurve_2_mppt_voc(self, _voltage_uV: int, _current_nA: int) -> Tuple[int, int]:
113
- self.interval_step = (self.interval_step + 1) % self._cfg.interval_n
119
+ self.interval_step = self.interval_step + 1
120
+ if self.interval_step >= self._cfg.interval_n:
121
+ self.interval_step = 0
114
122
  self.age_nxt += 1
115
123
  self.age_now += 1
116
124
 
117
125
  if (
118
126
  (_current_nA < self._cfg.current_limit_nA)
119
127
  and (_voltage_uV < self.voc_nxt)
120
- and (_voltage_uV >= self._cfg.voltage_min_uV)
128
+ and (_voltage_uV >= self.voc_min)
121
129
  and (_voltage_uV <= self._cfg.voltage_max_uV)
122
130
  ):
123
131
  self.voc_nxt = _voltage_uV
124
132
  self.age_nxt = 0
125
133
 
126
- if (self.age_now > self._cfg.window_size) or (self.voc_nxt <= self.voc_now):
134
+ if (self.age_now > self.age_max) or (self.voc_nxt <= self.voc_now):
127
135
  self.age_now = self.age_nxt
128
136
  self.voc_now = self.voc_nxt
129
137
  self.age_nxt = 0
@@ -137,7 +145,9 @@ class VirtualHarvesterModel:
137
145
  return _voltage_uV, _current_nA
138
146
 
139
147
  def ivcurve_2_mppt_po(self, _voltage_uV: int, _current_nA: int) -> Tuple[int, int]:
140
- self.interval_step = (self.interval_step + 1) % self._cfg.interval_n
148
+ self.interval_step = self.interval_step + 1
149
+ if self.interval_step >= self._cfg.interval_n:
150
+ self.interval_step = 0
141
151
 
142
152
  _voltage_uV, _current_nA = self.ivcurve_2_cv(_voltage_uV, _current_nA)
143
153
 
@@ -193,7 +203,7 @@ class VirtualHarvesterModel:
193
203
  self.voltage_nxt = _voltage_uV
194
204
  self.current_nxt = _current_nA
195
205
 
196
- if (self.age_now > self._cfg.window_size) or (self.power_nxt >= self.power_now):
206
+ if (self.age_now > self.age_max) or (self.power_nxt >= self.power_now):
197
207
  self.age_now = self.age_nxt
198
208
  self.power_now = self.power_nxt
199
209
  self.voltage_now = self.voltage_nxt
@@ -0,0 +1,71 @@
1
+ """Simulate behavior of virtual harvester algorithms.
2
+
3
+ The simulation recreates an observer-cape and the virtual harvester
4
+ - input = hdf5-file with an ivcurve
5
+ - output = optional as hdf5-file
6
+
7
+ The output file can be analyzed and plotted with shepherds tool suite.
8
+ """
9
+
10
+ from contextlib import ExitStack
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from tqdm import tqdm
15
+
16
+ from .. import CalibrationHarvester
17
+ from .. import Reader
18
+ from .. import Writer
19
+ from ..data_models.content.virtual_harvester import HarvesterPRUConfig
20
+ from ..data_models.content.virtual_harvester import VirtualHarvesterConfig
21
+ from .virtual_harvester_model import VirtualHarvesterModel
22
+
23
+
24
+ def simulate_harvester(
25
+ config: VirtualHarvesterConfig, path_input: Path, path_output: Optional[Path] = None
26
+ ) -> float:
27
+ """Simulate behavior of virtual harvester algorithms.
28
+
29
+ Fn return the harvested energy.
30
+ """
31
+ stack = ExitStack()
32
+ file_inp = Reader(path_input, verbose=False)
33
+ stack.enter_context(file_inp)
34
+ cal_inp = file_inp.get_calibration_data()
35
+
36
+ if path_output:
37
+ cal_hrv = CalibrationHarvester()
38
+ file_out = Writer(
39
+ path_output, cal_data=cal_hrv, mode="harvester", verbose=False, force_overwrite=True
40
+ )
41
+ stack.enter_context(file_out)
42
+ cal_out = file_out.get_calibration_data()
43
+ file_out.store_hostname("hrv_sim_" + config.name)
44
+
45
+ hrv_pru = HarvesterPRUConfig.from_vhrv(
46
+ config,
47
+ for_emu=True,
48
+ dtype_in=file_inp.get_datatype(),
49
+ window_size=file_inp.get_window_samples(),
50
+ )
51
+ hrv = VirtualHarvesterModel(hrv_pru)
52
+ e_out_Ws = 0.0
53
+
54
+ for _t, v_inp, i_inp in tqdm(
55
+ file_inp.read_buffers(is_raw=True), total=file_inp.buffers_n, desc="Buffers", leave=False
56
+ ):
57
+ v_uV = cal_inp.voltage.raw_to_si(v_inp) * 1e6
58
+ i_nA = cal_inp.current.raw_to_si(i_inp) * 1e9
59
+ length = min(v_uV.size, i_nA.size)
60
+ for _n in range(length):
61
+ v_uV[_n], i_nA[_n] = hrv.ivcurve_sample(
62
+ _voltage_uV=int(v_uV[_n]), _current_nA=int(i_nA[_n])
63
+ )
64
+ e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
65
+ if path_output:
66
+ v_out = cal_out.voltage.si_to_raw(v_uV / 1e6)
67
+ i_out = cal_out.current.si_to_raw(i_nA / 1e9)
68
+ file_out.append_iv_data_raw(_t, v_out, i_out)
69
+
70
+ stack.close()
71
+ return e_out_Ws
@@ -0,0 +1,78 @@
1
+ """Simulate behavior of virtual source algorithms.
2
+
3
+ The simulation recreates an observer-cape, the virtual Source and a virtual target
4
+ - input = hdf5-file with a harvest-recording
5
+ - output = optional as hdf5-file
6
+
7
+ The output file can be analyzed and plotted with shepherds tool suite.
8
+ """
9
+
10
+ from contextlib import ExitStack
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from tqdm import tqdm
15
+
16
+ from .. import CalibrationEmulator
17
+ from .. import Reader
18
+ from .. import Writer
19
+ from ..data_models import VirtualSourceConfig
20
+ from . import VirtualSourceModel
21
+ from .target_model import TargetABC
22
+
23
+
24
+ def simulate_source(
25
+ config: VirtualSourceConfig,
26
+ target: TargetABC,
27
+ path_input: Path,
28
+ path_output: Optional[Path] = None,
29
+ ) -> float:
30
+ """Simulate behavior of virtual source algorithms.
31
+
32
+ FN returns the consumed energy of the target.
33
+ """
34
+ stack = ExitStack()
35
+ file_inp = Reader(path_input, verbose=False)
36
+ stack.enter_context(file_inp)
37
+ cal_emu = CalibrationEmulator()
38
+ cal_inp = file_inp.get_calibration_data()
39
+
40
+ if path_output:
41
+ file_out = Writer(
42
+ path_output, cal_data=cal_emu, mode="emulator", verbose=False, force_overwrite=True
43
+ )
44
+ stack.enter_context(file_out)
45
+ file_out.store_hostname("emu_sim_" + config.name)
46
+ file_out.store_config(config.model_dump())
47
+ cal_out = file_out.get_calibration_data()
48
+
49
+ src = VirtualSourceModel(
50
+ config, cal_emu, log_intermediate=False, window_size=file_inp.get_window_samples()
51
+ )
52
+ i_out_nA = 0
53
+ e_out_Ws = 0.0
54
+
55
+ for _t, v_inp, i_inp in tqdm(
56
+ file_inp.read_buffers(is_raw=True), total=file_inp.buffers_n, desc="Buffers", leave=False
57
+ ):
58
+ v_uV = 1e6 * cal_inp.voltage.raw_to_si(v_inp)
59
+ i_nA = 1e9 * cal_inp.current.raw_to_si(i_inp)
60
+
61
+ for _n in range(len(_t)):
62
+ v_uV[_n] = src.iterate_sampling(
63
+ V_inp_uV=int(v_uV[_n]),
64
+ I_inp_nA=int(i_nA[_n]),
65
+ I_out_nA=i_out_nA,
66
+ )
67
+ i_out_nA = target.step(int(v_uV[_n]), pwr_good=src.cnv.get_power_good())
68
+ i_nA[_n] = i_out_nA
69
+ # TODO: src.cnv.get_I_mod_out_nA() has more internal drains
70
+
71
+ e_out_Ws += (v_uV * i_nA).sum() * 1e-15 * file_inp.sample_interval_s
72
+ if path_output:
73
+ v_out = cal_out.voltage.si_to_raw(1e-6 * v_uV)
74
+ i_out = cal_out.current.si_to_raw(1e-9 * i_nA)
75
+ file_out.append_iv_data_raw(_t, v_out, i_out)
76
+
77
+ stack.close()
78
+ return e_out_Ws
shepherd_core/writer.py CHANGED
@@ -149,9 +149,18 @@ class Writer(Reader):
149
149
  raise ValueError(msg)
150
150
 
151
151
  if self._modify:
152
- self._mode = mode
153
- self._datatype = datatype
154
- self._window_samples = window_samples
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
155
164
  else:
156
165
  self._mode = mode if isinstance(mode, str) else self.mode_default
157
166
  self._datatype = (
@@ -335,6 +344,7 @@ class Writer(Reader):
335
344
  current: ndarray in physical-unit A
336
345
 
337
346
  """
347
+ # TODO: make timestamp optional to add it raw, OR unify append with granular raw-switch
338
348
  timestamp = self._cal.time.si_to_raw(timestamp)
339
349
  voltage = self._cal.voltage.si_to_raw(voltage)
340
350
  current = self._cal.current.si_to_raw(current)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: shepherd_core
3
- Version: 2024.7.4
3
+ Version: 2024.8.2
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>
@@ -2,15 +2,15 @@ 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=9BuArqou5pmPKUrJH9oiPYlU1DkMxUScL4nftDJuFIs,26790
6
- shepherd_core/version.py,sha256=nuk8f6h1RBadV7-koFp4yLk1jExaUHnFqMTtxlKZyCo,75
7
- shepherd_core/writer.py,sha256=xcLCw-YokKaN8TrkwD0IjRmn8xZU0Q8wwWp_1K8JFVY,14475
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
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
11
11
  shepherd_core/data_models/base/__init__.py,sha256=PSJ6acWViqBm0Eiom8DIgKfFVrp5lzYr8OsDvP79vwI,94
12
12
  shepherd_core/data_models/base/cal_measurement.py,sha256=YScPG7QLynbUHdjcznYqU8O5KRh0XiROGxGSk9BETMk,3357
13
- shepherd_core/data_models/base/calibration.py,sha256=iy04ajChReqrRNLoNDEH01CM-iCvb5XepBIlyFSWeII,10466
13
+ shepherd_core/data_models/base/calibration.py,sha256=k5VwAK_cwr0a6QY82Lw-9uHth5KUReFOakuOTzghoso,10725
14
14
  shepherd_core/data_models/base/content.py,sha256=13j7GSgT73xn27jgDP508thUEJR4U-nCb5n7CJ50c9Y,2463
15
15
  shepherd_core/data_models/base/shepherd.py,sha256=DNrx59o1VBuy_liJuUzZRzmTTYB73D_pUWiNyMQyjYY,6112
16
16
  shepherd_core/data_models/base/timezone.py,sha256=2T6E46hJ1DAvmqKfu6uIgCK3RSoAKjGXRyzYNaqKyjY,665
@@ -21,10 +21,10 @@ shepherd_core/data_models/content/energy_environment.py,sha256=bXInmHzlRjBAt7mit
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=5eEHAZrgHPHZlTxDGaJrckDQgupFNC3Zax67EcCSqR8,9448
25
- shepherd_core/data_models/content/virtual_harvester_fixture.yaml,sha256=-IRyoQU0HXCEtIIcFmkFdz4snLB7bjFFqNcFVGSMiSA,4332
26
- shepherd_core/data_models/content/virtual_source.py,sha256=aoD8oam1POid0JG2ppttPA_Jl3y4ko5FNqzoaNKyBD8,14142
27
- shepherd_core/data_models/content/virtual_source_fixture.yaml,sha256=kx_lpBx0bLKqEHxS09GTnk8kuSbhuGhLgKHeaM6UviE,10481
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
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
@@ -66,12 +66,15 @@ shepherd_core/testbed_client/client_abc_fix.py,sha256=BsSkpvJHURRejlS-YPF1f6QRPC
66
66
  shepherd_core/testbed_client/client_web.py,sha256=iMh5T91152uugbFsqr2vvxLser0KIo5g426dp_6QWUE,5774
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
- shepherd_core/vsource/__init__.py,sha256=dS33KYLq5GQ9_D8HfdP8iWSocWTghCi2ZZG2AJWNfaM,391
70
- shepherd_core/vsource/virtual_converter_model.py,sha256=ZSoWVLfRmFEjeCNoQCg3BctzhdfayINUBDU_AJK1CR0,10404
71
- shepherd_core/vsource/virtual_harvester_model.py,sha256=wCbFfsqDRC5Rfu8qANkmkP9XGJOPHJY9-iSnI850JI4,7817
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
73
+ shepherd_core/vsource/virtual_harvester_simulation.py,sha256=EiBrvmc6D2N7Z0DqFBWPdRJK6hR8Q3iQaj7EYP9pLA0,2405
72
74
  shepherd_core/vsource/virtual_source_model.py,sha256=fjN8myTY3I_LpikF_aGAcxes3RGu1GP23P7XKC_UIyA,2737
73
- shepherd_core-2024.7.4.dist-info/METADATA,sha256=zFRCTLQEf-XN8Szx8fxb2JwCA6H0F26oUU5UMgWxVaA,7771
74
- shepherd_core-2024.7.4.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91
75
- shepherd_core-2024.7.4.dist-info/top_level.txt,sha256=wy-t7HRBrKARZxa-Y8_j8d49oVHnulh-95K9ikxVhew,14
76
- shepherd_core-2024.7.4.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
77
- shepherd_core-2024.7.4.dist-info/RECORD,,
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,,