pqopen-lib 0.8.0__tar.gz → 0.8.2__tar.gz
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.
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/PKG-INFO +2 -2
- pqopen_lib-0.8.0/pqopen/goertzel.py → pqopen_lib-0.8.2/pqopen/auxcalc.py +24 -1
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/helper.py +17 -1
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/powerquality.py +7 -31
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/powersystem.py +23 -22
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/zcd.py +14 -3
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pyproject.toml +2 -2
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/goertzel-test.py +1 -1
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/helper-test.py +28 -1
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/powerquality-test.py +2 -2
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/powersystem-test.py +34 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/storagecontroller-test.py +1 -1
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/.gitignore +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/LICENSE +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/README.md +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/__init__.py +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/eventdetector.py +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/pqopen/storagecontroller.py +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/data_files/event_data_level_low.csv +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/eventdetector-test.py +0 -0
- {pqopen_lib-0.8.0 → pqopen_lib-0.8.2}/test/zcd-test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pqopen-lib
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: A power quality processing library for calculating parameters from waveform data
|
|
5
5
|
Project-URL: Homepage, https://github.com/DaqOpen/pqopen-lib
|
|
6
6
|
Project-URL: Issues, https://github.com/DaqOpen/pqopen-lib/issues
|
|
@@ -10,7 +10,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Requires-Python: >=3.11
|
|
13
|
-
Requires-Dist: daqopen-lib
|
|
13
|
+
Requires-Dist: daqopen-lib>=0.7.4
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: scipy
|
|
16
16
|
Provides-Extra: numba
|
|
@@ -9,7 +9,7 @@ except ImportError:
|
|
|
9
9
|
return func
|
|
10
10
|
float32 = float64 = None
|
|
11
11
|
|
|
12
|
-
@njit
|
|
12
|
+
@njit(cache=True)
|
|
13
13
|
def calc_single_freq(x: np.array, f_hz: float, fs: float) -> tuple[float, float]:
|
|
14
14
|
"""Compute the amplitude and phase of a specific frequency component
|
|
15
15
|
in a signal using the Goertzel algorithm.
|
|
@@ -48,6 +48,29 @@ def calc_single_freq(x: np.array, f_hz: float, fs: float) -> tuple[float, float]
|
|
|
48
48
|
|
|
49
49
|
return amp, phase
|
|
50
50
|
|
|
51
|
+
def calc_rms_trapz(values: np.array, start_frac: float, end_frac: float, frequency: float, samplerate: float):
|
|
52
|
+
u2 = values * values
|
|
53
|
+
s = 0.0
|
|
54
|
+
s += 0.5 * (u2[0] + u2[1]) * start_frac # left edge
|
|
55
|
+
s += 0.5*u2[1:-2].sum() + 0.5*u2[2:-1].sum() # middle
|
|
56
|
+
s += 0.5 * (u2[-2] + u2[-1]) * end_frac # right edge
|
|
57
|
+
return (s * frequency / samplerate) ** 0.5
|
|
58
|
+
|
|
59
|
+
@njit(fastmath=True)
|
|
60
|
+
def fast_interp(y, out_len):
|
|
61
|
+
n = y.size
|
|
62
|
+
scale = (n - 1) / out_len
|
|
63
|
+
out = np.empty(out_len, dtype=y.dtype)
|
|
64
|
+
for i in range(out_len):
|
|
65
|
+
x = i * scale
|
|
66
|
+
j = int(x)
|
|
67
|
+
t = x - j
|
|
68
|
+
if j+1 < n:
|
|
69
|
+
out[i] = (1-t)*y[j] + t*y[j+1]
|
|
70
|
+
else:
|
|
71
|
+
out[i] = y[-1]
|
|
72
|
+
return out
|
|
73
|
+
|
|
51
74
|
# >>>>>>> Pre-Compile for float32 und float64 <<<<<<<
|
|
52
75
|
if float32 is not None and float64 is not None:
|
|
53
76
|
sigs = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
import re
|
|
3
|
+
import numpy as np
|
|
3
4
|
|
|
4
5
|
def floor_timestamp(timestamp: float | int, interval_seconds: int, ts_resolution: str = "us"):
|
|
5
6
|
"""Floor eines Zeitstempels auf ein gegebenes Intervall."""
|
|
@@ -44,4 +45,19 @@ class JsonDecimalLimiter(object):
|
|
|
44
45
|
return self._float_pattern.sub(self._round_float_match, json_string)
|
|
45
46
|
|
|
46
47
|
def _round_float_match(self, m):
|
|
47
|
-
return format(round(float(m.group()), self._decimal_places), f'.{self._decimal_places}f')
|
|
48
|
+
return format(round(float(m.group()), self._decimal_places), f'.{self._decimal_places}f')
|
|
49
|
+
|
|
50
|
+
def create_harm_corr_array(nom_frequency: float, num_harmonics: int, freq_response: tuple, interharm=False):
|
|
51
|
+
if interharm:
|
|
52
|
+
freqs = np.arange(0.5*nom_frequency, nom_frequency*(num_harmonics+1.5), nom_frequency)
|
|
53
|
+
else:
|
|
54
|
+
freqs = np.arange(0, nom_frequency*(num_harmonics+1), nom_frequency)
|
|
55
|
+
freq_response_arr = np.array(freq_response)
|
|
56
|
+
freq_corr = 1/np.interp(freqs, freq_response_arr[:,0], freq_response_arr[:,1])
|
|
57
|
+
return freq_corr
|
|
58
|
+
|
|
59
|
+
def create_fft_corr_array(target_size: int, freq_nyq: float, freq_response: tuple):
|
|
60
|
+
freqs = np.linspace(0, freq_nyq, target_size)
|
|
61
|
+
freq_response_arr = np.array(freq_response)
|
|
62
|
+
freq_corr = 1/np.interp(freqs, freq_response_arr[:,0], freq_response_arr[:,1])
|
|
63
|
+
return freq_corr
|
|
@@ -76,7 +76,7 @@ def resample_and_fft(data: np.ndarray, resample_size: int = None) -> np.ndarray:
|
|
|
76
76
|
np.ndarray: The FFT of the resampled data, scaled by sqrt(2) and normalized by the resample size.
|
|
77
77
|
"""
|
|
78
78
|
if not resample_size:
|
|
79
|
-
resample_size = 2**int(np.
|
|
79
|
+
resample_size = 2**int(np.floor(np.log2(data.size)))
|
|
80
80
|
x_data = np.arange(len(data))
|
|
81
81
|
x = np.linspace(0, len(data), resample_size, endpoint=False)
|
|
82
82
|
data_resampled = np.interp(x, x_data, data)
|
|
@@ -149,11 +149,7 @@ class VoltageFluctuation(object):
|
|
|
149
149
|
def process(self, start_sidx: int, hp_data: np.ndarray, raw_data: np.ndarray):
|
|
150
150
|
"""
|
|
151
151
|
"""
|
|
152
|
-
stage0_tp_filtered_data,
|
|
153
|
-
self.stage0_tp_filter_zi = signal.lfiltic(self.stage0_tp_filter_coeff[0],
|
|
154
|
-
self.stage0_tp_filter_coeff[1],
|
|
155
|
-
stage0_tp_filtered_data[-3:][::-1],
|
|
156
|
-
raw_data[-3:][::-1])
|
|
152
|
+
stage0_tp_filtered_data, self.stage0_tp_filter_zi = signal.lfilter(self.stage0_tp_filter_coeff[0], self.stage0_tp_filter_coeff[1], raw_data, zi=self.stage0_tp_filter_zi)
|
|
157
153
|
|
|
158
154
|
samples_skip_next_start = len(stage0_tp_filtered_data) % self._calc_samplerate_decimation
|
|
159
155
|
stage0_tp_filtered_data = stage0_tp_filtered_data[self._next_reduction_start_idx::self._calc_samplerate_decimation]
|
|
@@ -161,37 +157,17 @@ class VoltageFluctuation(object):
|
|
|
161
157
|
self._next_reduction_start_idx += self._calc_samplerate_decimation - samples_skip_next_start
|
|
162
158
|
if self._next_reduction_start_idx >= self._calc_samplerate_decimation:
|
|
163
159
|
self._next_reduction_start_idx %= self._calc_samplerate_decimation
|
|
164
|
-
stage1_tp_filtered_data,
|
|
165
|
-
self.stage1_tp_filter_zi = signal.lfiltic(self.stage1_tp_filter_coeff[0],
|
|
166
|
-
self.stage1_tp_filter_coeff[1],
|
|
167
|
-
stage1_tp_filtered_data[-2:][::-1],
|
|
168
|
-
hp_data[-2:][::-1])
|
|
160
|
+
stage1_tp_filtered_data, self.stage1_tp_filter_zi = signal.lfilter(self.stage1_tp_filter_coeff[0], self.stage1_tp_filter_coeff[1], hp_data, zi=self.stage1_tp_filter_zi)
|
|
169
161
|
blk_size = int(len(stage0_tp_filtered_data)/len(hp_data))
|
|
170
162
|
for idx,val in enumerate(stage1_tp_filtered_data[:-1]):
|
|
171
163
|
stage0_tp_filtered_data[idx*blk_size:(idx+1)*blk_size] /= val
|
|
172
164
|
stage0_tp_filtered_data[(idx+1)*blk_size:] /= stage1_tp_filtered_data[-1]
|
|
173
165
|
stage2_output = np.power(stage0_tp_filtered_data, 2)
|
|
174
|
-
stage3_hp_filtered_data,
|
|
175
|
-
self.
|
|
176
|
-
|
|
177
|
-
stage3_hp_filtered_data[-2:][::-1],
|
|
178
|
-
stage2_output[-2:][::-1])
|
|
179
|
-
stage3_tp_filtered_data,_ = signal.lfilter(self.stage3_tp_filter_coeff[0], self.stage3_tp_filter_coeff[1], stage3_hp_filtered_data, zi=self.stage3_tp_filter_zi)
|
|
180
|
-
self.stage3_tp_filter_zi = signal.lfiltic(self.stage3_tp_filter_coeff[0],
|
|
181
|
-
self.stage3_tp_filter_coeff[1],
|
|
182
|
-
stage3_tp_filtered_data[-7:][::-1],
|
|
183
|
-
stage3_hp_filtered_data[-7:][::-1])
|
|
184
|
-
stage3_weight_filtered_data,_ = signal.lfilter(self.stage3_weight_filter_coeff[0], self.stage3_weight_filter_coeff[1], stage3_tp_filtered_data, zi=self.stage3_weight_filter_zi)
|
|
185
|
-
self.stage3_weight_filter_zi = signal.lfiltic(self.stage3_weight_filter_coeff[0],
|
|
186
|
-
self.stage3_weight_filter_coeff[1],
|
|
187
|
-
stage3_weight_filtered_data[-len(self.stage3_weight_filter_coeff[1])-1:][::-1],
|
|
188
|
-
stage3_tp_filtered_data[-len(self.stage3_weight_filter_coeff[1])-1:][::-1])
|
|
166
|
+
stage3_hp_filtered_data, self.stage3_hp_filter_zi = signal.lfilter(self.stage3_hp_filter_coeff[0], self.stage3_hp_filter_coeff[1], stage2_output, zi=self.stage3_hp_filter_zi)
|
|
167
|
+
stage3_tp_filtered_data, self.stage3_tp_filter_zi = signal.lfilter(self.stage3_tp_filter_coeff[0], self.stage3_tp_filter_coeff[1], stage3_hp_filtered_data, zi=self.stage3_tp_filter_zi)
|
|
168
|
+
stage3_weight_filtered_data, self.stage3_weight_filter_zi = signal.lfilter(self.stage3_weight_filter_coeff[0], self.stage3_weight_filter_coeff[1], stage3_tp_filtered_data, zi=self.stage3_weight_filter_zi)
|
|
189
169
|
stage3_output = np.power(stage3_weight_filtered_data, 2)
|
|
190
|
-
stage4_tp_filtered_data,
|
|
191
|
-
self.stage4_tp_filter_zi = signal.lfiltic(self.stage4_tp_filter_coeff[0],
|
|
192
|
-
self.stage4_tp_filter_coeff[1],
|
|
193
|
-
stage4_tp_filtered_data[-2:][::-1],
|
|
194
|
-
stage3_output[-2:][::-1])
|
|
170
|
+
stage4_tp_filtered_data, self.stage4_tp_filter_zi = signal.lfilter(self.stage4_tp_filter_coeff[0], self.stage4_tp_filter_coeff[1], stage3_output, zi=self.stage4_tp_filter_zi)
|
|
195
171
|
# Append buffer since steady state
|
|
196
172
|
self.processed_samples += len(raw_data)
|
|
197
173
|
# Steady State after approx. 20 Seconds
|
|
@@ -28,8 +28,8 @@ import json
|
|
|
28
28
|
from daqopen.channelbuffer import AcqBuffer, DataChannelBuffer
|
|
29
29
|
from pqopen.zcd import ZeroCrossDetector
|
|
30
30
|
import pqopen.powerquality as pq
|
|
31
|
-
from pqopen.helper import floor_timestamp
|
|
32
|
-
from pqopen.
|
|
31
|
+
from pqopen.helper import floor_timestamp, create_fft_corr_array
|
|
32
|
+
from pqopen.auxcalc import calc_single_freq, calc_rms_trapz
|
|
33
33
|
logger = logging.getLogger(__name__)
|
|
34
34
|
|
|
35
35
|
class PowerSystem(object):
|
|
@@ -78,6 +78,7 @@ class PowerSystem(object):
|
|
|
78
78
|
self._zcd_minimum_frequency = zcd_minimum_freq
|
|
79
79
|
self.nominal_frequency = nominal_frequency
|
|
80
80
|
self.nper = nper
|
|
81
|
+
self._harm_fft_resample_size = 2**int(np.floor(np.log2((self._samplerate / self.nominal_frequency) * self.nper)))
|
|
81
82
|
self._nominal_voltage = None
|
|
82
83
|
self._phases: List[PowerPhase] = []
|
|
83
84
|
self._features = {"harmonics": 0,
|
|
@@ -301,6 +302,17 @@ class PowerSystem(object):
|
|
|
301
302
|
tmp = {channel.name: channel for channel in calc_type.values()}
|
|
302
303
|
self.output_channels.update(tmp)
|
|
303
304
|
|
|
305
|
+
if self._features["harmonics"]:
|
|
306
|
+
for phase in self._phases:
|
|
307
|
+
if phase._u_channel.freq_response:
|
|
308
|
+
phase._u_fft_corr_array = create_fft_corr_array(self._harm_fft_resample_size//2+1,
|
|
309
|
+
self._samplerate/2,
|
|
310
|
+
phase._u_channel.freq_response)
|
|
311
|
+
if phase._i_channel is not None and phase._i_channel.freq_response:
|
|
312
|
+
phase._i_fft_corr_array = create_fft_corr_array(self._harm_fft_resample_size//2+1,
|
|
313
|
+
self._samplerate/2,
|
|
314
|
+
phase._i_channel.freq_response)
|
|
315
|
+
|
|
304
316
|
if self._features["fluctuation"]:
|
|
305
317
|
for phase in self._phases:
|
|
306
318
|
phase._voltage_fluctuation_processor = pq.VoltageFluctuation(samplerate=self._samplerate,
|
|
@@ -403,18 +415,8 @@ class PowerSystem(object):
|
|
|
403
415
|
for phys_type, output_channel in phase._calc_channels["one_period"]["voltage"].items():
|
|
404
416
|
if phys_type == "trms":
|
|
405
417
|
if self._features["rms_trapz_rule"]:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
u_trapz_values = phase._u_channel.read_data_by_index(phase_period_start_sidx-add_start_idx_sample, phase_period_stop_sidx+add_stop_idx_sample)
|
|
409
|
-
sample_points = np.arange(len(u_values), dtype=np.float64)
|
|
410
|
-
if add_start_idx_sample:
|
|
411
|
-
u_trapz_values[0] = (u_trapz_values[1] - u_trapz_values[0])*(1+self._last_zc_frac) + u_trapz_values[0]
|
|
412
|
-
sample_points = np.insert(sample_points,0,self._last_zc_frac)
|
|
413
|
-
if add_stop_idx_sample:
|
|
414
|
-
u_trapz_values[-1] = (u_trapz_values[-1] - u_trapz_values[-2])*self._last_zc_frac + u_trapz_values[-2]
|
|
415
|
-
sample_points = np.insert(sample_points,len(sample_points),sample_points[-1]+actual_zc_frac)
|
|
416
|
-
integral = np.trapezoid(np.power(u_trapz_values,2), sample_points)
|
|
417
|
-
u_rms = np.sqrt(integral * frequency / self._samplerate)
|
|
418
|
+
u_trapz_values = phase._u_channel.read_data_by_index(phase_period_start_sidx-1, phase_period_stop_sidx+1)
|
|
419
|
+
u_rms = calc_rms_trapz(u_trapz_values, self._last_zc_frac, actual_zc_frac, frequency, self._samplerate)
|
|
418
420
|
else:
|
|
419
421
|
u_rms = np.sqrt(np.mean(np.power(u_values, 2)))
|
|
420
422
|
output_channel.put_data_single(phase_period_stop_sidx, u_rms)
|
|
@@ -477,7 +479,9 @@ class PowerSystem(object):
|
|
|
477
479
|
u_values = phase._u_channel.read_data_by_index(start_sidx, stop_sidx)
|
|
478
480
|
u_rms = np.sqrt(np.mean(np.power(u_values, 2)))
|
|
479
481
|
if self._features["harmonics"]:
|
|
480
|
-
data_fft_U = pq.resample_and_fft(u_values)
|
|
482
|
+
data_fft_U = pq.resample_and_fft(u_values, self._harm_fft_resample_size)
|
|
483
|
+
if phase._u_fft_corr_array is not None:
|
|
484
|
+
data_fft_U *= phase._u_fft_corr_array
|
|
481
485
|
u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
482
486
|
u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
483
487
|
if phase._number == 1: # use phase 1 angle as reference
|
|
@@ -672,13 +676,8 @@ class PowerSystem(object):
|
|
|
672
676
|
self._calculation_mode = "FALLBACK"
|
|
673
677
|
else:
|
|
674
678
|
self._calculation_mode = "NORMAL"
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
filtered_zero_crossings = []
|
|
678
|
-
for idx,zc in enumerate(zero_crossings):
|
|
679
|
-
if (int(zc)+start_acq_sidx) >= self._zero_crossings[-1] or np.isnan(self._zero_crossings[-1]):
|
|
680
|
-
filtered_zero_crossings.append(zc)
|
|
681
|
-
return filtered_zero_crossings
|
|
679
|
+
|
|
680
|
+
return zero_crossings
|
|
682
681
|
|
|
683
682
|
def get_aggregated_data(self, start_acq_sidx: int, stop_acq_sidx: int) -> dict:
|
|
684
683
|
"""
|
|
@@ -734,6 +733,8 @@ class PowerPhase(object):
|
|
|
734
733
|
self._calc_channels = {}
|
|
735
734
|
self._voltage_fluctuation_processor: pq.VoltageFluctuation = None
|
|
736
735
|
self._mains_signaling_tracer: pq.MainsSignalingVoltageTracer = None
|
|
736
|
+
self._u_fft_corr_array = None
|
|
737
|
+
self._i_fft_corr_array = None
|
|
737
738
|
|
|
738
739
|
def update_calc_channels(self, features: dict):
|
|
739
740
|
"""
|
|
@@ -48,6 +48,7 @@ class ZeroCrossDetector:
|
|
|
48
48
|
self._last_zc_p = None
|
|
49
49
|
self._last_zc_n = None
|
|
50
50
|
self._last_zc_n_val = None
|
|
51
|
+
self._last_zc = None
|
|
51
52
|
|
|
52
53
|
# Calculate the filter delay in samples
|
|
53
54
|
w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1], worN=[self.f_cutoff], fs=self.samplerate)
|
|
@@ -111,9 +112,15 @@ class ZeroCrossDetector:
|
|
|
111
112
|
k = (y2 - y1) / (x2 - x1)
|
|
112
113
|
d = y1 - k * x1
|
|
113
114
|
real_zc = -d/k
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
if np.isnan(real_zc):
|
|
116
|
+
logger.warning("Detection Error: real_zc is NaN, ignoring")
|
|
117
|
+
else:
|
|
118
|
+
if self._last_zc and (real_zc <= self._last_zc):
|
|
119
|
+
logger.warning("Detected ZC before last one, ignoring")
|
|
120
|
+
else:
|
|
121
|
+
zero_crossings.append(real_zc + self.filter_delay_samples)
|
|
122
|
+
last_used_p_idx = p_idx
|
|
123
|
+
self._last_zc = real_zc
|
|
117
124
|
|
|
118
125
|
# Update the last negative threshold-crossing for the next block
|
|
119
126
|
if last_used_p_idx < len(threshold_p_cross) and len(threshold_n_cross) > 0:
|
|
@@ -126,4 +133,8 @@ class ZeroCrossDetector:
|
|
|
126
133
|
else:
|
|
127
134
|
self._last_zc_n = None
|
|
128
135
|
|
|
136
|
+
# Update Last Valid ZC Index
|
|
137
|
+
if self._last_zc:
|
|
138
|
+
self._last_zc -= len(data)
|
|
139
|
+
|
|
129
140
|
return zero_crossings
|
|
@@ -6,7 +6,7 @@ import numpy as np
|
|
|
6
6
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
7
7
|
sys.path.append(os.path.dirname(SCRIPT_DIR))
|
|
8
8
|
|
|
9
|
-
from pqopen.
|
|
9
|
+
from pqopen.auxcalc import calc_single_freq
|
|
10
10
|
|
|
11
11
|
class TestSingleFrequency(unittest.TestCase):
|
|
12
12
|
def test_fund_only(self):
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
import sys
|
|
3
3
|
import os
|
|
4
|
+
import numpy as np
|
|
4
5
|
from datetime import datetime
|
|
5
6
|
|
|
6
7
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
7
8
|
sys.path.append(os.path.dirname(SCRIPT_DIR))
|
|
8
9
|
|
|
9
|
-
from pqopen.helper import floor_timestamp, JsonDecimalLimiter
|
|
10
|
+
from pqopen.helper import floor_timestamp, JsonDecimalLimiter, create_harm_corr_array, create_fft_corr_array
|
|
10
11
|
|
|
11
12
|
class TestFloorTimestamp(unittest.TestCase):
|
|
12
13
|
|
|
@@ -100,5 +101,31 @@ class TestLimitDecimalPlaces(unittest.TestCase):
|
|
|
100
101
|
input_json = '{"sci": 1.23e10}'
|
|
101
102
|
self.assertEqual(float_limiter.process(input_json), input_json)
|
|
102
103
|
|
|
104
|
+
class TestHarmCorrCreater(unittest.TestCase):
|
|
105
|
+
|
|
106
|
+
def test_harmonic(self):
|
|
107
|
+
freq_response = ((50, 1.0), (100, 0.9))
|
|
108
|
+
expected_corr_factors = 1/np.array([1, 1, 0.9, 0.9, 0.9, 0.9])
|
|
109
|
+
corr_factors = create_harm_corr_array(50, 5, freq_response)
|
|
110
|
+
|
|
111
|
+
self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
|
|
112
|
+
|
|
113
|
+
def test_interharmonic(self):
|
|
114
|
+
freq_response = ((50, 1.0), (100, 0.9))
|
|
115
|
+
expected_corr_factors = 1/np.array([1, 0.95, 0.9, 0.9, 0.9, 0.9])
|
|
116
|
+
corr_factors = create_harm_corr_array(50, 5, freq_response, interharm=True)
|
|
117
|
+
|
|
118
|
+
self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
|
|
119
|
+
|
|
120
|
+
class TestFftCorrCreater(unittest.TestCase):
|
|
121
|
+
|
|
122
|
+
def test_small(self):
|
|
123
|
+
freq_response = ((50, 1.0), (100, 0.9))
|
|
124
|
+
|
|
125
|
+
expected_corr_factors = 1/np.array([1, 1, 0.9, 0.9, 0.9, 0.9])
|
|
126
|
+
corr_factors = create_fft_corr_array(6, 250, freq_response)
|
|
127
|
+
|
|
128
|
+
self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
|
|
129
|
+
|
|
103
130
|
if __name__ == '__main__':
|
|
104
131
|
unittest.main()
|
|
@@ -69,7 +69,7 @@ class TestPowerPowerQualityFlicker(unittest.TestCase):
|
|
|
69
69
|
|
|
70
70
|
blocksize = 1000
|
|
71
71
|
for blk_idx in range(t.size // blocksize):
|
|
72
|
-
hp_data = 230*np.ones(
|
|
72
|
+
hp_data = 230*np.ones((f_fund*2*blocksize)//samplerate)
|
|
73
73
|
voltage_fluctuation.process(blk_idx*blocksize, hp_data, u_values[blk_idx*blocksize:(blk_idx+1)*blocksize])
|
|
74
74
|
pst = voltage_fluctuation.calc_pst(0, duration*samplerate)
|
|
75
75
|
|
|
@@ -90,7 +90,7 @@ class TestPowerPowerQualityFlicker(unittest.TestCase):
|
|
|
90
90
|
|
|
91
91
|
blocksize = 1000
|
|
92
92
|
for blk_idx in range(t.size // blocksize):
|
|
93
|
-
hp_data = 230*np.ones(
|
|
93
|
+
hp_data = 230*np.ones((f_fund*2*blocksize)//samplerate)
|
|
94
94
|
voltage_fluctuation.process(blk_idx*blocksize, hp_data, u_values[blk_idx*blocksize:(blk_idx+1)*blocksize])
|
|
95
95
|
pinst_1s, _ = voltage_fluctuation._pinst_channel.read_data_by_acq_sidx((duration-1)*samplerate, duration*samplerate)
|
|
96
96
|
|
|
@@ -249,6 +249,40 @@ class TestPowerSystemCalculation(unittest.TestCase):
|
|
|
249
249
|
freq, _ = self.power_system.output_channels["Freq"].read_data_by_acq_sidx(0, u_values.size)
|
|
250
250
|
self.assertIsNone(np.testing.assert_array_almost_equal(freq[1:], expected_freq, 3))
|
|
251
251
|
|
|
252
|
+
class TestPowerSystemCalculationFreqResponse(unittest.TestCase):
|
|
253
|
+
def setUp(self):
|
|
254
|
+
self.u_channel = AcqBuffer(freq_response=((50,1.0), (100,0.9)))
|
|
255
|
+
self.i_channel = AcqBuffer()
|
|
256
|
+
|
|
257
|
+
# Create PowerSystem instance
|
|
258
|
+
self.power_system = PowerSystem(
|
|
259
|
+
zcd_channel=self.u_channel,
|
|
260
|
+
input_samplerate=10000.0,
|
|
261
|
+
zcd_threshold=0.1
|
|
262
|
+
)
|
|
263
|
+
# Add Phase
|
|
264
|
+
self.power_system.add_phase(u_channel=self.u_channel, i_channel=self.i_channel)
|
|
265
|
+
|
|
266
|
+
def test_multi_period_calc_harmonic_msv(self):
|
|
267
|
+
t = np.linspace(0, 1, int(self.power_system._samplerate), endpoint=False)
|
|
268
|
+
u_values = np.sqrt(2)*np.sin(2*np.pi*50*t) + 0.09*np.sqrt(2)*np.sin(2*np.pi*150*t) + 0.009*np.sqrt(2)*np.sin(2*np.pi*375*t)
|
|
269
|
+
i_values = 2*np.sqrt(2)*np.sin(2*np.pi*50*t+60*np.pi/180) # cos_phi = 0.5
|
|
270
|
+
|
|
271
|
+
expected_u_h3_rms = np.array(np.zeros(4)) + 0.1
|
|
272
|
+
expected_u_msv_rms = np.array(np.zeros(4)) + 0.01
|
|
273
|
+
|
|
274
|
+
self.power_system.enable_harmonic_calculation(10)
|
|
275
|
+
self.power_system.enable_mains_signaling_calculation(375)
|
|
276
|
+
self.u_channel.put_data(u_values)
|
|
277
|
+
self.i_channel.put_data(i_values)
|
|
278
|
+
self.power_system.process()
|
|
279
|
+
|
|
280
|
+
# Check Voltage
|
|
281
|
+
u_h_rms, _ = self.power_system.output_channels["U1_H_rms"].read_data_by_acq_sidx(0, u_values.size)
|
|
282
|
+
self.assertIsNone(np.testing.assert_allclose(u_h_rms[:,3], expected_u_h3_rms, rtol=0.01))
|
|
283
|
+
u_msv_rms, _ = self.power_system.output_channels["U1_msv_rms"].read_data_by_acq_sidx(0, u_values.size)
|
|
284
|
+
self.assertIsNone(np.testing.assert_allclose(u_msv_rms, expected_u_msv_rms, rtol=0.01))
|
|
285
|
+
|
|
252
286
|
class TestPowerSystemCalculationThreePhase(unittest.TestCase):
|
|
253
287
|
def setUp(self):
|
|
254
288
|
self.u1_channel = AcqBuffer()
|
|
@@ -88,7 +88,7 @@ class TestStorageController(unittest.TestCase):
|
|
|
88
88
|
|
|
89
89
|
self.storage_controller.process_events(events)
|
|
90
90
|
|
|
91
|
-
self.
|
|
91
|
+
self.assertAlmostEqual(storage_endpoint._event_list[0].start_ts, 0.01)
|
|
92
92
|
|
|
93
93
|
def test_one_storageplan_series_slow(self):
|
|
94
94
|
start_timestamp = int(1000000000*1e6)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|