shepherd-data 2022.9.3__py3-none-any.whl → 2023.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shepherd_data/__init__.py +2 -1
- shepherd_data/calibration.py +17 -9
- shepherd_data/cli.py +24 -12
- shepherd_data/ivonne.py +35 -27
- shepherd_data/mppt.py +12 -7
- shepherd_data/reader.py +118 -77
- shepherd_data/writer.py +111 -95
- {shepherd_data-2022.9.3.dist-info → shepherd_data-2023.3.1.dist-info}/METADATA +49 -31
- shepherd_data-2023.3.1.dist-info/RECORD +14 -0
- {shepherd_data-2022.9.3.dist-info → shepherd_data-2023.3.1.dist-info}/WHEEL +1 -1
- shepherd_data-2023.3.1.dist-info/entry_points.txt +2 -0
- shepherd_data-2022.9.3.dist-info/RECORD +0 -14
- shepherd_data-2022.9.3.dist-info/entry_points.txt +0 -2
- {shepherd_data-2022.9.3.dist-info → shepherd_data-2023.3.1.dist-info}/LICENSE +0 -0
- {shepherd_data-2022.9.3.dist-info → shepherd_data-2023.3.1.dist-info}/top_level.txt +0 -0
- {shepherd_data-2022.9.3.dist-info → shepherd_data-2023.3.1.dist-info}/zip-safe +0 -0
shepherd_data/__init__.py
CHANGED
shepherd_data/calibration.py
CHANGED
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
"""
|
|
4
4
|
from typing import Dict
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TypeVar
|
|
6
6
|
|
|
7
7
|
import numpy as np
|
|
8
|
+
from numpy.typing import NDArray
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
cal_default: Dict[str, dict] = {
|
|
10
|
+
cal_default: Dict[str, Dict[str, float]] = {
|
|
11
11
|
"voltage": {"gain": 3 * 1e-9, "offset": 0.0}, # allows 0 - 12 V in 3 nV-Steps
|
|
12
12
|
"current": {"gain": 250 * 1e-12, "offset": 0.0}, # allows 0 - 1 A in 250 pA - Steps
|
|
13
13
|
"time": {"gain": 1e-9, "offset": 0.0},
|
|
14
|
+
# SI-value [SI-Unit] = raw-value * gain + offset
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
T_calc = TypeVar("T_calc", NDArray[np.float64], float)
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
) -> Union[np.ndarray, float]:
|
|
19
|
+
|
|
20
|
+
def raw_to_si(values_raw: T_calc, cal: Dict[str, float]) -> T_calc:
|
|
20
21
|
"""Helper to convert between physical units and raw unsigned integers
|
|
21
22
|
|
|
22
23
|
:param values_raw: number or numpy array with raw values
|
|
@@ -24,11 +25,15 @@ def raw_to_si(
|
|
|
24
25
|
:return: converted number or array
|
|
25
26
|
"""
|
|
26
27
|
values_si = values_raw * cal["gain"] + cal["offset"]
|
|
27
|
-
values_si
|
|
28
|
+
if isinstance(values_si, np.ndarray):
|
|
29
|
+
values_si[values_si < 0.0] = 0.0
|
|
30
|
+
# if pyright still complains, cast with .astype(float)
|
|
31
|
+
else:
|
|
32
|
+
values_si = float(max(values_si, 0.0))
|
|
28
33
|
return values_si
|
|
29
34
|
|
|
30
35
|
|
|
31
|
-
def si_to_raw(values_si:
|
|
36
|
+
def si_to_raw(values_si: T_calc, cal: Dict[str, float]) -> T_calc:
|
|
32
37
|
"""Helper to convert between physical units and raw unsigned integers
|
|
33
38
|
|
|
34
39
|
:param values_si: number or numpy array with values in physical units
|
|
@@ -36,5 +41,8 @@ def si_to_raw(values_si: Union[np.ndarray, float], cal: dict) -> Union[np.ndarra
|
|
|
36
41
|
:return: converted number or array
|
|
37
42
|
"""
|
|
38
43
|
values_raw = (values_si - cal["offset"]) / cal["gain"]
|
|
39
|
-
values_raw
|
|
44
|
+
if isinstance(values_raw, np.ndarray):
|
|
45
|
+
values_raw[values_raw < 0.0] = 0.0
|
|
46
|
+
else:
|
|
47
|
+
values_raw = max(values_raw, 0.0)
|
|
40
48
|
return values_raw
|
shepherd_data/cli.py
CHANGED
|
@@ -6,7 +6,7 @@ import os
|
|
|
6
6
|
import sys
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import List
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Optional
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ logger = logging.getLogger("SHPData.cli")
|
|
|
18
18
|
verbose_level = 2
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def config_logger(verbose: int) ->
|
|
21
|
+
def config_logger(verbose: int) -> None:
|
|
22
22
|
# TODO: put in __init__, provide logger, ditch global var
|
|
23
23
|
if verbose == 0:
|
|
24
24
|
logger.setLevel(logging.ERROR)
|
|
@@ -61,8 +61,8 @@ def path_to_flist(data_path: Path) -> List[Path]:
|
|
|
61
61
|
default=0,
|
|
62
62
|
help="4 Levels [0..3](Error, Warning, Info, Debug)",
|
|
63
63
|
)
|
|
64
|
-
@click.pass_context
|
|
65
|
-
def cli(ctx, verbose: int):
|
|
64
|
+
@click.pass_context # TODO: is the ctx-type correct?
|
|
65
|
+
def cli(ctx: click.Context, verbose: int) -> None:
|
|
66
66
|
"""Shepherd: Synchronized Energy Harvesting Emulator and Recorder
|
|
67
67
|
|
|
68
68
|
Args:
|
|
@@ -79,7 +79,7 @@ def cli(ctx, verbose: int):
|
|
|
79
79
|
|
|
80
80
|
@cli.command(short_help="Validates a file or directory containing shepherd-recordings")
|
|
81
81
|
@click.argument("in_data", type=click.Path(exists=True, resolve_path=True))
|
|
82
|
-
def validate(in_data):
|
|
82
|
+
def validate(in_data: Path) -> None:
|
|
83
83
|
"""Validates a file or directory containing shepherd-recordings"""
|
|
84
84
|
files = path_to_flist(in_data)
|
|
85
85
|
valid_dir = True
|
|
@@ -111,7 +111,7 @@ def validate(in_data):
|
|
|
111
111
|
type=click.STRING,
|
|
112
112
|
help="Set an individual csv-separator",
|
|
113
113
|
)
|
|
114
|
-
def extract(in_data, ds_factor, separator):
|
|
114
|
+
def extract(in_data: Path, ds_factor: float, separator: str) -> None:
|
|
115
115
|
"""Extracts recorded IVSamples and stores it to csv"""
|
|
116
116
|
files = path_to_flist(in_data)
|
|
117
117
|
if not isinstance(ds_factor, (float, int)) or ds_factor < 1:
|
|
@@ -160,7 +160,7 @@ def extract(in_data, ds_factor, separator):
|
|
|
160
160
|
type=click.STRING,
|
|
161
161
|
help="Set an individual csv-separator",
|
|
162
162
|
)
|
|
163
|
-
def extract_meta(in_data, separator):
|
|
163
|
+
def extract_meta(in_data: Path, separator: str) -> None:
|
|
164
164
|
"""Extracts metadata and logs from file or directory containing shepherd-recordings"""
|
|
165
165
|
files = path_to_flist(in_data)
|
|
166
166
|
for file in files:
|
|
@@ -197,14 +197,16 @@ def extract_meta(in_data, separator):
|
|
|
197
197
|
@click.option(
|
|
198
198
|
"--sample-rate",
|
|
199
199
|
"-r",
|
|
200
|
-
type=
|
|
200
|
+
type=click.INT,
|
|
201
201
|
help="Alternative Input to determine a downsample-factor (Choose One)",
|
|
202
202
|
)
|
|
203
|
-
def downsample(
|
|
203
|
+
def downsample(
|
|
204
|
+
in_data: Path, ds_factor: Optional[float], sample_rate: Optional[int]
|
|
205
|
+
) -> None:
|
|
204
206
|
"""Creates an array of downsampling-files from file
|
|
205
207
|
or directory containing shepherd-recordings"""
|
|
206
208
|
if ds_factor is None and sample_rate is not None and sample_rate >= 1:
|
|
207
|
-
ds_factor = int(Reader.
|
|
209
|
+
ds_factor = int(Reader.samplerate_sps_default / sample_rate)
|
|
208
210
|
if isinstance(ds_factor, (float, int)) and ds_factor >= 1:
|
|
209
211
|
ds_list = [ds_factor]
|
|
210
212
|
else:
|
|
@@ -279,7 +281,14 @@ def downsample(in_data, ds_factor, sample_rate):
|
|
|
279
281
|
is_flag=True,
|
|
280
282
|
help="Plot all files (in directory) into one Multiplot",
|
|
281
283
|
)
|
|
282
|
-
def plot(
|
|
284
|
+
def plot(
|
|
285
|
+
in_data: Path,
|
|
286
|
+
start: Optional[float],
|
|
287
|
+
end: Optional[float],
|
|
288
|
+
width: int,
|
|
289
|
+
height: int,
|
|
290
|
+
multiplot: bool,
|
|
291
|
+
) -> None:
|
|
283
292
|
"""Plots IV-trace from file or directory containing shepherd-recordings"""
|
|
284
293
|
files = path_to_flist(in_data)
|
|
285
294
|
multiplot = multiplot and len(files) > 1
|
|
@@ -294,7 +303,10 @@ def plot(in_data, start: float, end: float, width: int, height: int, multiplot:
|
|
|
294
303
|
if multiplot:
|
|
295
304
|
logger.info("Got %d datasets to plot", len(data))
|
|
296
305
|
mpl_path = Reader.multiplot_to_file(data, in_data, width, height)
|
|
297
|
-
|
|
306
|
+
if mpl_path:
|
|
307
|
+
logger.info("Plot generated and saved to '%s'", mpl_path.name)
|
|
308
|
+
else:
|
|
309
|
+
logger.info("Plot not generated, path was already in use.")
|
|
298
310
|
|
|
299
311
|
|
|
300
312
|
if __name__ == "__main__":
|
shepherd_data/ivonne.py
CHANGED
|
@@ -9,7 +9,7 @@ import math
|
|
|
9
9
|
import os
|
|
10
10
|
import pickle # noqa: S403
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import
|
|
12
|
+
from typing import Optional
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
import pandas as pd
|
|
@@ -34,27 +34,30 @@ def get_isc(coeffs: pd.DataFrame):
|
|
|
34
34
|
class Reader:
|
|
35
35
|
"""container for converters to shepherd-data"""
|
|
36
36
|
|
|
37
|
-
samplerate_sps: int = 50
|
|
38
|
-
sample_interval_ns: int = int(10**9 // samplerate_sps)
|
|
39
|
-
sample_interval_s: float = 1 / samplerate_sps
|
|
40
|
-
|
|
41
|
-
runtime_s: float = None
|
|
42
|
-
file_size: int = None
|
|
43
|
-
data_rate: float = None
|
|
44
|
-
|
|
45
|
-
_df: pd.DataFrame = None
|
|
46
|
-
|
|
47
37
|
_logger: logging.Logger = logging.getLogger("SHPData.IVonne.Reader")
|
|
48
38
|
|
|
49
39
|
def __init__(
|
|
50
|
-
self,
|
|
40
|
+
self,
|
|
41
|
+
file_path: Path,
|
|
42
|
+
samplerate_sps: Optional[int] = None,
|
|
43
|
+
verbose: bool = True,
|
|
51
44
|
):
|
|
52
45
|
self._logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
|
53
46
|
|
|
54
47
|
self.file_path = Path(file_path)
|
|
48
|
+
self.samplerate_sps: int = 50
|
|
55
49
|
if samplerate_sps is not None:
|
|
56
50
|
self.samplerate_sps = samplerate_sps
|
|
57
51
|
|
|
52
|
+
self.sample_interval_ns: int = int(10**9 // self.samplerate_sps)
|
|
53
|
+
self.sample_interval_s: float = 1 / self.samplerate_sps
|
|
54
|
+
|
|
55
|
+
self.runtime_s: float = 0
|
|
56
|
+
self.file_size: int = 0
|
|
57
|
+
self.data_rate: float = 0
|
|
58
|
+
|
|
59
|
+
self._df: Optional[pd.DataFrame] = None
|
|
60
|
+
|
|
58
61
|
def __enter__(self):
|
|
59
62
|
if not self.file_path.exists():
|
|
60
63
|
raise FileNotFoundError(
|
|
@@ -75,10 +78,12 @@ class Reader:
|
|
|
75
78
|
)
|
|
76
79
|
return self
|
|
77
80
|
|
|
78
|
-
def __exit__(self, *exc):
|
|
81
|
+
def __exit__(self, *exc): # type: ignore
|
|
79
82
|
pass
|
|
80
83
|
|
|
81
|
-
def _refresh_file_stats(self) ->
|
|
84
|
+
def _refresh_file_stats(self) -> None:
|
|
85
|
+
if self._df is None:
|
|
86
|
+
raise RuntimeError("IVonne Context was not entered - file not open!")
|
|
82
87
|
self.runtime_s = round(self._df.shape[0] / self.samplerate_sps, 3)
|
|
83
88
|
self.file_size = self.file_path.stat().st_size
|
|
84
89
|
self.data_rate = self.file_size / self.runtime_s if self.runtime_s > 0 else 0
|
|
@@ -88,8 +93,8 @@ class Reader:
|
|
|
88
93
|
shp_output: Path,
|
|
89
94
|
v_max: float = 5.0,
|
|
90
95
|
pts_per_curve: int = 1000,
|
|
91
|
-
duration_s: float = None,
|
|
92
|
-
) ->
|
|
96
|
+
duration_s: Optional[float] = None,
|
|
97
|
+
) -> None:
|
|
93
98
|
"""Transforms previously recorded parameters to shepherd hdf database with IV curves.
|
|
94
99
|
Shepherd should work with IV 'surfaces', where we have a stream of IV curves
|
|
95
100
|
|
|
@@ -98,6 +103,8 @@ class Reader:
|
|
|
98
103
|
:param pts_per_curve: Number of sampling points of the prototype curve
|
|
99
104
|
:param duration_s: time to stop in seconds, counted from beginning
|
|
100
105
|
"""
|
|
106
|
+
if self._df is None:
|
|
107
|
+
raise RuntimeError("IVonne Context was not entered - file not open!")
|
|
101
108
|
if isinstance(duration_s, (float, int)) and self.runtime_s > duration_s:
|
|
102
109
|
self._logger.info(" -> gets trimmed to %s s", duration_s)
|
|
103
110
|
df_elements_n = min(
|
|
@@ -115,7 +122,6 @@ class Reader:
|
|
|
115
122
|
with Writer(
|
|
116
123
|
shp_output, datatype="ivcurve", window_samples=pts_per_curve
|
|
117
124
|
) as sfw:
|
|
118
|
-
|
|
119
125
|
sfw.set_hostname("IVonne")
|
|
120
126
|
curve_interval_us = round(sfw.sample_interval_ns * pts_per_curve / 1000)
|
|
121
127
|
up_factor = self.sample_interval_ns // sfw.sample_interval_ns
|
|
@@ -125,7 +131,7 @@ class Reader:
|
|
|
125
131
|
for idx in job_iter:
|
|
126
132
|
idx_top = min(idx + max_elements, df_elements_n)
|
|
127
133
|
df_slice = self._df.iloc[idx : idx_top + 1].copy()
|
|
128
|
-
df_slice
|
|
134
|
+
df_slice["timestamp"] = pd.TimedeltaIndex(
|
|
129
135
|
data=df_slice["time"], unit="s"
|
|
130
136
|
)
|
|
131
137
|
df_slice = df_slice.set_index("timestamp")
|
|
@@ -153,9 +159,9 @@ class Reader:
|
|
|
153
159
|
self,
|
|
154
160
|
shp_output: Path,
|
|
155
161
|
v_max: float = 5.0,
|
|
156
|
-
duration_s: float = None,
|
|
157
|
-
tracker: MPPTracker = None,
|
|
158
|
-
) ->
|
|
162
|
+
duration_s: Optional[float] = None,
|
|
163
|
+
tracker: Optional[MPPTracker] = None,
|
|
164
|
+
) -> None:
|
|
159
165
|
"""Transforms shepherd IV curves to shepherd IV traces.
|
|
160
166
|
|
|
161
167
|
For the 'buck' and 'buck-boost' modes, shepherd takes voltage and current traces.
|
|
@@ -173,6 +179,8 @@ class Reader:
|
|
|
173
179
|
:param duration_s: time to stop in seconds, counted from beginning
|
|
174
180
|
:param tracker: VOC or OPT
|
|
175
181
|
"""
|
|
182
|
+
if self._df is None:
|
|
183
|
+
raise RuntimeError("IVonne Context was not entered - file not open!")
|
|
176
184
|
if isinstance(duration_s, (float, int)) and self.runtime_s > duration_s:
|
|
177
185
|
self._logger.info(" -> gets trimmed to %s s", duration_s)
|
|
178
186
|
df_elements_n = min(
|
|
@@ -191,7 +199,6 @@ class Reader:
|
|
|
191
199
|
)
|
|
192
200
|
|
|
193
201
|
with Writer(shp_output, datatype="ivsample") as sfw:
|
|
194
|
-
|
|
195
202
|
sfw.set_hostname("IVonne")
|
|
196
203
|
interval_us = round(sfw.sample_interval_ns / 1000)
|
|
197
204
|
up_factor = self.sample_interval_ns // sfw.sample_interval_ns
|
|
@@ -206,7 +213,7 @@ class Reader:
|
|
|
206
213
|
df_slice.loc[:, "voc"] = get_voc(df_slice)
|
|
207
214
|
df_slice.loc[df_slice["voc"] >= v_max, "voc"] = v_max
|
|
208
215
|
df_slice = tracker.process(df_slice)
|
|
209
|
-
df_slice
|
|
216
|
+
df_slice["timestamp"] = pd.TimedeltaIndex(
|
|
210
217
|
data=df_slice["time"], unit="s"
|
|
211
218
|
)
|
|
212
219
|
df_slice = df_slice[["time", "v", "i", "timestamp"]].set_index(
|
|
@@ -228,14 +235,16 @@ class Reader:
|
|
|
228
235
|
self,
|
|
229
236
|
shp_output: Path,
|
|
230
237
|
v_max: float = 5.0,
|
|
231
|
-
duration_s: float = None,
|
|
232
|
-
) ->
|
|
238
|
+
duration_s: Optional[float] = None,
|
|
239
|
+
) -> None:
|
|
233
240
|
"""Transforms ivonne-parameters to upsampled version for shepherd
|
|
234
241
|
|
|
235
242
|
:param shp_output: Path where the resulting hdf file shall be stored
|
|
236
243
|
:param v_max: Maximum voltage supported by shepherd
|
|
237
244
|
:param duration_s: time to stop in seconds, counted from beginning
|
|
238
245
|
"""
|
|
246
|
+
if self._df is None:
|
|
247
|
+
raise RuntimeError("IVonne Context was not entered - file not open!")
|
|
239
248
|
if isinstance(duration_s, (float, int)) and self.runtime_s > duration_s:
|
|
240
249
|
self._logger.info(" -> gets trimmed to %s s", duration_s)
|
|
241
250
|
df_elements_n = min(
|
|
@@ -249,7 +258,6 @@ class Reader:
|
|
|
249
258
|
return
|
|
250
259
|
|
|
251
260
|
with Writer(shp_output, datatype="isc_voc") as sfw:
|
|
252
|
-
|
|
253
261
|
sfw.set_hostname("IVonne")
|
|
254
262
|
interval_us = round(sfw.sample_interval_ns / 1000)
|
|
255
263
|
up_factor = self.sample_interval_ns // sfw.sample_interval_ns
|
|
@@ -264,7 +272,7 @@ class Reader:
|
|
|
264
272
|
df_slice.loc[:, "voc"] = get_voc(df_slice)
|
|
265
273
|
df_slice.loc[df_slice["voc"] >= v_max, "voc"] = v_max
|
|
266
274
|
df_slice.loc[:, "isc"] = get_isc(df_slice)
|
|
267
|
-
df_slice
|
|
275
|
+
df_slice["timestamp"] = pd.TimedeltaIndex(
|
|
268
276
|
data=df_slice["time"], unit="s"
|
|
269
277
|
)
|
|
270
278
|
df_slice = df_slice[["time", "voc", "isc", "timestamp"]].set_index(
|
shepherd_data/mppt.py
CHANGED
|
@@ -5,8 +5,10 @@ Might be exchanged by shepherds py-model of pru-harvesters
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
|
+
from .calibration import T_calc
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
def iv_model(voltages: T_calc, coeffs: pd.Series) -> T_calc:
|
|
10
12
|
"""Simple diode based model of a solar panel IV curve.
|
|
11
13
|
|
|
12
14
|
Args:
|
|
@@ -16,15 +18,18 @@ def iv_model(voltages: np.ndarray, coeffs: pd.DataFrame) -> np.ndarray:
|
|
|
16
18
|
Returns:
|
|
17
19
|
Solar current at given load voltage
|
|
18
20
|
"""
|
|
19
|
-
currents = coeffs["a"] - coeffs["b"] * (
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
currents = float(coeffs["a"]) - float(coeffs["b"]) * (
|
|
22
|
+
np.exp(float(coeffs["c"]) * voltages) - 1.0
|
|
23
|
+
)
|
|
24
|
+
if isinstance(currents, np.ndarray):
|
|
25
|
+
currents[currents < 0.0] = 0.0
|
|
22
26
|
else:
|
|
23
|
-
currents = max(0, currents)
|
|
27
|
+
currents = max(0.0, currents)
|
|
28
|
+
|
|
24
29
|
return currents
|
|
25
30
|
|
|
26
31
|
|
|
27
|
-
def find_oc(v_arr, i_arr, ratio: float = 0.05):
|
|
32
|
+
def find_oc(v_arr: np.ndarray, i_arr: np.ndarray, ratio: float = 0.05):
|
|
28
33
|
"""Approximates opencircuit voltage.
|
|
29
34
|
|
|
30
35
|
Searches last current value that is above a certain ratio of the short-circuit
|
|
@@ -51,7 +56,7 @@ class MPPTracker:
|
|
|
51
56
|
:param coeffs: ivonne coefficients
|
|
52
57
|
:return:
|
|
53
58
|
"""
|
|
54
|
-
|
|
59
|
+
return pd.DataFrame()
|
|
55
60
|
|
|
56
61
|
|
|
57
62
|
class OpenCircuitTracker(MPPTracker):
|