pqopen-lib 0.8.1__tar.gz → 0.8.3__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.1 → pqopen_lib-0.8.3}/PKG-INFO +2 -2
- pqopen_lib-0.8.1/pqopen/goertzel.py → pqopen_lib-0.8.3/pqopen/auxcalc.py +24 -1
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/helper.py +17 -1
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/powerquality.py +7 -31
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/powersystem.py +22 -15
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pyproject.toml +2 -2
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/goertzel-test.py +1 -1
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/helper-test.py +28 -1
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/powerquality-test.py +2 -2
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/powersystem-test.py +34 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/storagecontroller-test.py +1 -1
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/.gitignore +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/LICENSE +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/README.md +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/__init__.py +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/eventdetector.py +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/storagecontroller.py +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/pqopen/zcd.py +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/data_files/event_data_level_low.csv +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/test/eventdetector-test.py +0 -0
- {pqopen_lib-0.8.1 → pqopen_lib-0.8.3}/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.3
|
|
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,
|
|
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,
|
|
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,10 @@ 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
|
+
resample_factor = min(self._harm_fft_resample_size / u_values.size, 1)
|
|
485
|
+
data_fft_U *= phase._u_fft_corr_array[np.linspace(0, self._harm_fft_resample_size*resample_factor, self._harm_fft_resample_size//2+1).astype(np.int32)]
|
|
481
486
|
u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
482
487
|
u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
483
488
|
if phase._number == 1: # use phase 1 angle as reference
|
|
@@ -729,6 +734,8 @@ class PowerPhase(object):
|
|
|
729
734
|
self._calc_channels = {}
|
|
730
735
|
self._voltage_fluctuation_processor: pq.VoltageFluctuation = None
|
|
731
736
|
self._mains_signaling_tracer: pq.MainsSignalingVoltageTracer = None
|
|
737
|
+
self._u_fft_corr_array = None
|
|
738
|
+
self._i_fft_corr_array = None
|
|
732
739
|
|
|
733
740
|
def update_calc_channels(self, features: dict):
|
|
734
741
|
"""
|
|
@@ -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
|
|
File without changes
|