pqopen-lib 0.8.5__tar.gz → 0.9.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pqopen-lib
3
- Version: 0.8.5
3
+ Version: 0.9.1
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._harm_fft_resample_size,
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._harm_fft_resample_size,
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
- 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)]
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,16 @@ 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
+ if phase._u_fft_corr_array is not None:
530
+ corr_freq_idx = np.linspace(0, phase._u_fft_corr_array.size, u_real_fft.shape[0], endpoint=False).astype(np.int32)
531
+ u_real_fft *= phase._u_fft_corr_array[corr_freq_idx]
532
+ u_Pxx = (np.abs(u_real_fft) ** 2) / np.sum(self._hf_1khz_window ** 2)
533
+ u_Pxx /= self._harm_fft_resample_size * 0.5
534
+ u_hf_1khz = pq.calc_hf_1khz_band(u_Pxx, self._samplerate)
535
+ output_channel.put_data_single(stop_sidx, u_hf_1khz)
514
536
 
515
537
  if phase._i_channel:
516
538
  i_values = phase._i_channel.read_data_by_index(start_sidx, stop_sidx)
@@ -810,6 +832,10 @@ class PowerPhase(object):
810
832
  self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
811
833
  self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
812
834
 
835
+ if "hf_1khz_band_calculation" in features and features["hf_1khz_band_calculation"]:
836
+ num_bands = features["hf_1khz_band_calculation"]
837
+ 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")
838
+
813
839
  # Create Current Channels
814
840
  if self._i_channel:
815
841
  self._calc_channels["one_period"]["current"] = {}
@@ -12,7 +12,7 @@ packages = ["pqopen"]
12
12
 
13
13
  [project]
14
14
  name = "pqopen-lib"
15
- version = "0.8.5"
15
+ version = "0.9.1"
16
16
  dependencies = [
17
17
  "numpy",
18
18
  "daqopen-lib >= 0.7.4",
@@ -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 = 1000
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 = 1000
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