pqopen-lib 0.8.5__tar.gz → 0.9.0__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.5 → pqopen_lib-0.9.0}/PKG-INFO +1 -1
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/powerquality.py +13 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/powersystem.py +31 -4
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pyproject.toml +1 -1
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/powerquality-test.py +32 -4
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/powersystem-test.py +19 -1
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/.gitignore +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/LICENSE +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/README.md +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/__init__.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/auxcalc.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/eventdetector.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/helper.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/storagecontroller.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/pqopen/zcd.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/data_files/event_data_level_low.csv +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/eventdetector-test.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/goertzel-test.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/helper-test.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/storagecontroller-test.py +0 -0
- {pqopen_lib-0.8.5 → pqopen_lib-0.9.0}/test/zcd-test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pqopen-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
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
|
|
@@ -63,6 +63,19 @@ def calc_thd(harm_rms: np.ndarray, max_harmonic: int = 40, min_harmonic: int = 2
|
|
|
63
63
|
thd = np.sqrt(np.sum(np.power(harm_rms[min_harmonic:max_harmonic+1]/fund_rms, 2)))*100
|
|
64
64
|
return thd
|
|
65
65
|
|
|
66
|
+
def calc_hf_1khz_band(fft_data: np.ndarray, samplerate: float):
|
|
67
|
+
fft_data_abs = np.abs(fft_data)
|
|
68
|
+
freq_resolution = 0.5*samplerate / fft_data_abs.shape[0]
|
|
69
|
+
freq_steps = np.arange(-500, (samplerate/2) + 1500 - (samplerate/2) % 1000, 1000)
|
|
70
|
+
freq_steps[0] = 0
|
|
71
|
+
freq_steps[-1] = samplerate/2
|
|
72
|
+
freq_band_idx = (freq_steps / freq_resolution).astype(int)
|
|
73
|
+
hf_1khz_band_rms = []
|
|
74
|
+
for band_idx in range(freq_band_idx.shape[0] - 1):
|
|
75
|
+
hf_1khz_band_rms.append(np.sqrt(np.sum(fft_data_abs[freq_band_idx[band_idx]:freq_band_idx[band_idx+1]])))
|
|
76
|
+
|
|
77
|
+
return np.array(hf_1khz_band_rms)
|
|
78
|
+
|
|
66
79
|
def resample_and_fft(data: np.ndarray, resample_size: int = None) -> np.ndarray:
|
|
67
80
|
"""
|
|
68
81
|
Resample the input data to a specified size and compute its FFT.
|
|
@@ -24,6 +24,7 @@ from typing import List, Dict
|
|
|
24
24
|
import logging
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
import json
|
|
27
|
+
from scipy.signal import get_window
|
|
27
28
|
|
|
28
29
|
from daqopen.channelbuffer import AcqBuffer, DataChannelBuffer
|
|
29
30
|
from pqopen.zcd import ZeroCrossDetector
|
|
@@ -91,7 +92,8 @@ class PowerSystem(object):
|
|
|
91
92
|
"energy_channels": {},
|
|
92
93
|
"one_period_fundamental": 0,
|
|
93
94
|
"rms_trapz_rule": False,
|
|
94
|
-
"pmu_calculation": False
|
|
95
|
+
"pmu_calculation": False,
|
|
96
|
+
"hf_1khz_band_calculation": False}
|
|
95
97
|
self._prepare_calc_channels()
|
|
96
98
|
self.output_channels: Dict[str, DataChannelBuffer] = {}
|
|
97
99
|
self._last_processed_sidx = 0
|
|
@@ -246,6 +248,15 @@ class PowerSystem(object):
|
|
|
246
248
|
self._pmu_last_processed_ts_us = 0
|
|
247
249
|
self._pmu_time_increment_us = int(1_000_000 / self.nominal_frequency)
|
|
248
250
|
|
|
251
|
+
def enable_hf_1khz_band_calculation(self):
|
|
252
|
+
"""
|
|
253
|
+
Enables the calculation of wide band analysis with 1 kHz bands (non standard)
|
|
254
|
+
"""
|
|
255
|
+
num_bands = int(self._samplerate*0.5 / 1000) + 1
|
|
256
|
+
self._features["hf_1khz_band_calculation"] = num_bands
|
|
257
|
+
self._hf_1khz_window = get_window("hann", self._harm_fft_resample_size)
|
|
258
|
+
self._channel_update_needed = True
|
|
259
|
+
|
|
249
260
|
def _resync_nper_abs_time(self, zc_idx: int):
|
|
250
261
|
if not self._features["nper_abs_time_sync"]:
|
|
251
262
|
return None
|
|
@@ -305,11 +316,11 @@ class PowerSystem(object):
|
|
|
305
316
|
if self._features["harmonics"]:
|
|
306
317
|
for phase in self._phases:
|
|
307
318
|
if phase._u_channel.freq_response:
|
|
308
|
-
phase._u_fft_corr_array = create_fft_corr_array(self.
|
|
319
|
+
phase._u_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
|
|
309
320
|
self._samplerate/2,
|
|
310
321
|
phase._u_channel.freq_response)
|
|
311
322
|
if phase._i_channel is not None and phase._i_channel.freq_response:
|
|
312
|
-
phase._i_fft_corr_array = create_fft_corr_array(self.
|
|
323
|
+
phase._i_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
|
|
313
324
|
self._samplerate/2,
|
|
314
325
|
phase._i_channel.freq_response)
|
|
315
326
|
|
|
@@ -482,7 +493,8 @@ class PowerSystem(object):
|
|
|
482
493
|
data_fft_U = pq.resample_and_fft(u_values, self._harm_fft_resample_size)
|
|
483
494
|
if phase._u_fft_corr_array is not None:
|
|
484
495
|
resample_factor = min(self._harm_fft_resample_size / u_values.size, 1)
|
|
485
|
-
|
|
496
|
+
corr_freq_idx = np.linspace(0, self._samplerate*0.5*resample_factor*self.nper/self.nominal_frequency, self._harm_fft_resample_size//2+1).astype(np.int32)
|
|
497
|
+
data_fft_U *= phase._u_fft_corr_array[corr_freq_idx]
|
|
486
498
|
u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
487
499
|
u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
|
|
488
500
|
if phase._number == 1: # use phase 1 angle as reference
|
|
@@ -511,6 +523,17 @@ class PowerSystem(object):
|
|
|
511
523
|
output_channel.put_data_single(stop_sidx, u_rms)
|
|
512
524
|
if phys_type == "over":
|
|
513
525
|
output_channel.put_data_single(stop_sidx, u_rms)
|
|
526
|
+
if phys_type == "hf_1khz_rms":
|
|
527
|
+
u_values_windowed = u_values[:self._harm_fft_resample_size]*self._hf_1khz_window
|
|
528
|
+
u_real_fft = np.fft.rfft(u_values_windowed)
|
|
529
|
+
#u_real_fft = np.fft.rfft(u_values_windowed)
|
|
530
|
+
if phase._u_fft_corr_array is not None:
|
|
531
|
+
corr_freq_idx = np.linspace(0, self._samplerate*0.5, u_real_fft.shape[0]).astype(np.int32)
|
|
532
|
+
u_real_fft *= phase._u_fft_corr_array[corr_freq_idx]
|
|
533
|
+
u_Pxx = (np.abs(u_real_fft) ** 2) / np.sum(self._hf_1khz_window ** 2)
|
|
534
|
+
u_Pxx /= self._harm_fft_resample_size * 0.5
|
|
535
|
+
u_hf_1khz = pq.calc_hf_1khz_band(u_Pxx, self._samplerate)
|
|
536
|
+
output_channel.put_data_single(stop_sidx, u_hf_1khz)
|
|
514
537
|
|
|
515
538
|
if phase._i_channel:
|
|
516
539
|
i_values = phase._i_channel.read_data_by_index(start_sidx, stop_sidx)
|
|
@@ -810,6 +833,10 @@ class PowerPhase(object):
|
|
|
810
833
|
self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
|
|
811
834
|
self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
|
|
812
835
|
|
|
836
|
+
if "hf_1khz_band_calculation" in features and features["hf_1khz_band_calculation"]:
|
|
837
|
+
num_bands = features["hf_1khz_band_calculation"]
|
|
838
|
+
self._calc_channels["multi_period"]["voltage"]["hf_1khz_rms"] = DataChannelBuffer('U{:s}_HF_1kHz_rms'.format(self.name), sample_dimension=num_bands, agg_type='rms', unit="V")
|
|
839
|
+
|
|
813
840
|
# Create Current Channels
|
|
814
841
|
if self._i_channel:
|
|
815
842
|
self._calc_channels["one_period"]["current"] = {}
|
|
@@ -8,16 +8,17 @@ sys.path.append(os.path.dirname(SCRIPT_DIR))
|
|
|
8
8
|
|
|
9
9
|
import pqopen.powerquality as pq
|
|
10
10
|
from daqopen.channelbuffer import DataChannelBuffer
|
|
11
|
+
from pqopen.helper import create_fft_corr_array
|
|
11
12
|
|
|
12
13
|
class TestPowerPowerQualityHarmonic(unittest.TestCase):
|
|
13
14
|
def setUp(self):
|
|
14
15
|
...
|
|
15
16
|
|
|
16
17
|
def test_simple(self):
|
|
17
|
-
samplerate =
|
|
18
|
+
samplerate = 5000
|
|
18
19
|
f_fund = 50.0
|
|
19
20
|
num_periods = 10
|
|
20
|
-
t = np.linspace(0, 0.2, samplerate, endpoint=False)
|
|
21
|
+
t = np.linspace(0, 0.2, int(samplerate*0.2), endpoint=False)
|
|
21
22
|
values = np.sqrt(2)*np.sin(2*np.pi*f_fund*t) + 0.1*np.sqrt(2)*np.sin(2*np.pi*2*f_fund*t + 45*np.pi/2)
|
|
22
23
|
expected_v_h_rms = [0, 1, 0.1, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
23
24
|
|
|
@@ -29,10 +30,10 @@ class TestPowerPowerQualityHarmonic(unittest.TestCase):
|
|
|
29
30
|
self.assertAlmostEqual(v_h_phi[2], -90+45*2, places=2)
|
|
30
31
|
|
|
31
32
|
def test_advanced(self):
|
|
32
|
-
samplerate =
|
|
33
|
+
samplerate = 5000
|
|
33
34
|
f_fund = 53.0
|
|
34
35
|
num_periods = 10
|
|
35
|
-
t = np.linspace(0, num_periods/f_fund, samplerate, endpoint=False)
|
|
36
|
+
t = np.linspace(0, num_periods/f_fund, int(samplerate*num_periods/f_fund), endpoint=False)
|
|
36
37
|
values = (1.0*np.sqrt(2)*np.sin(2*np.pi*1.0*f_fund*t) +
|
|
37
38
|
0.1*np.sqrt(2)*np.sin(2*np.pi*1.5*f_fund*t + 45*np.pi/2)+
|
|
38
39
|
0.1*np.sqrt(2)*np.sin(2*np.pi*2.0*f_fund*t + 45*np.pi/2))
|
|
@@ -53,6 +54,33 @@ class TestPowerPowerQualityHarmonic(unittest.TestCase):
|
|
|
53
54
|
self.assertAlmostEqual(v_h_phi[2], -90+45*2, places=2)
|
|
54
55
|
self.assertAlmostEqual(v_thd, expected_v_thd, places=1)
|
|
55
56
|
|
|
57
|
+
def test_freq_response_correction(self):
|
|
58
|
+
samplerate = 5000
|
|
59
|
+
nominal_frequency = 50
|
|
60
|
+
f_fund = 45
|
|
61
|
+
num_periods = 10
|
|
62
|
+
harm_fft_resample_size = 512
|
|
63
|
+
t = np.linspace(0, num_periods/f_fund, int(samplerate*num_periods/f_fund), endpoint=False)
|
|
64
|
+
values = (1.0*np.sqrt(2)*np.sin(2*np.pi*1.0*f_fund*t) +
|
|
65
|
+
0.1*np.sqrt(2)*np.sin(2*np.pi*1.5*f_fund*t + 45*np.pi/2)+
|
|
66
|
+
0.1*np.sqrt(2)*np.sin(2*np.pi*2.0*f_fund*t + 45*np.pi/2)+
|
|
67
|
+
0.1*np.sqrt(2)*np.sin(2*np.pi*5.0*f_fund*t + 45*np.pi/2))
|
|
68
|
+
freq_response = ((50, 1.0), (200, 1.0), (225, 0.5))
|
|
69
|
+
expected_v_h_rms = [0, 1, 0.1, 0, 0, 0.2, 0, 0, 0, 0, 0]
|
|
70
|
+
|
|
71
|
+
v_fft = pq.resample_and_fft(values, resample_size=harm_fft_resample_size)
|
|
72
|
+
resample_factor = v_fft.shape[0]*2 / values.shape[0]
|
|
73
|
+
|
|
74
|
+
v_fft_corr_array = create_fft_corr_array(int((samplerate / nominal_frequency) * num_periods / 2),
|
|
75
|
+
samplerate*0.5,
|
|
76
|
+
freq_response)
|
|
77
|
+
|
|
78
|
+
corr_array_to_apply = v_fft_corr_array[np.linspace(0, samplerate*0.5*resample_factor*num_periods/nominal_frequency, harm_fft_resample_size//2+1).astype(np.int32)]
|
|
79
|
+
v_fft *= corr_array_to_apply
|
|
80
|
+
v_h_rms, v_h_phi = pq.calc_harmonics(v_fft, 10, 10)
|
|
81
|
+
|
|
82
|
+
self.assertIsNone(np.testing.assert_allclose(v_h_rms, expected_v_h_rms, atol=0.01))
|
|
83
|
+
|
|
56
84
|
|
|
57
85
|
class TestPowerPowerQualityFlicker(unittest.TestCase):
|
|
58
86
|
def setUp(self):
|
|
@@ -273,7 +273,7 @@ class TestPowerSystemCalculation(unittest.TestCase):
|
|
|
273
273
|
|
|
274
274
|
class TestPowerSystemCalculationFreqResponse(unittest.TestCase):
|
|
275
275
|
def setUp(self):
|
|
276
|
-
self.u_channel = AcqBuffer(freq_response=((50,1.0), (100,0.9)))
|
|
276
|
+
self.u_channel = AcqBuffer(freq_response=((50,1.0), (100,0.9), (1000, 0.9), (2000, 0.7)))
|
|
277
277
|
self.i_channel = AcqBuffer()
|
|
278
278
|
|
|
279
279
|
# Create PowerSystem instance
|
|
@@ -305,6 +305,24 @@ class TestPowerSystemCalculationFreqResponse(unittest.TestCase):
|
|
|
305
305
|
u_msv_rms, _ = self.power_system.output_channels["U1_msv_rms"].read_data_by_acq_sidx(0, u_values.size)
|
|
306
306
|
self.assertIsNone(np.testing.assert_allclose(u_msv_rms, expected_u_msv_rms, rtol=0.01))
|
|
307
307
|
|
|
308
|
+
def test_hf_1khz_band_calc(self):
|
|
309
|
+
t = np.linspace(0, 1, int(self.power_system._samplerate), endpoint=False)
|
|
310
|
+
u_values = (np.sqrt(2)*np.sin(2*np.pi*50*t) +
|
|
311
|
+
0.2*np.sqrt(2)*np.sin(2*np.pi*1000*t) +
|
|
312
|
+
0.1*np.sqrt(2)*np.sin(2*np.pi*2000*t))
|
|
313
|
+
i_values = 2*np.sqrt(2)*np.sin(2*np.pi*50*t+60*np.pi/180) # cos_phi = 0.5
|
|
314
|
+
|
|
315
|
+
expected_u_hf_rms = np.array([1, 0.2, 0.1, 0, 0, 0])
|
|
316
|
+
|
|
317
|
+
self.power_system.enable_hf_1khz_band_calculation()
|
|
318
|
+
self.u_channel.put_data(u_values)
|
|
319
|
+
self.i_channel.put_data(i_values)
|
|
320
|
+
self.power_system.process()
|
|
321
|
+
|
|
322
|
+
# Check Voltage
|
|
323
|
+
u_hf_rms, _ = self.power_system.output_channels["U1_HF_1kHz_rms"].read_data_by_acq_sidx(0, u_values.size)
|
|
324
|
+
self.assertIsNone(np.testing.assert_allclose(u_hf_rms[3,:], expected_u_hf_rms, atol=0.01))
|
|
325
|
+
|
|
308
326
|
class TestPowerSystemCalculationThreePhase(unittest.TestCase):
|
|
309
327
|
def setUp(self):
|
|
310
328
|
self.u1_channel = AcqBuffer()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|