pqopen-lib 0.7.6__tar.gz → 0.7.7__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.7.6
3
+ Version: 0.7.7
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
@@ -13,6 +13,8 @@ Requires-Python: >=3.11
13
13
  Requires-Dist: daqopen-lib
14
14
  Requires-Dist: numpy
15
15
  Requires-Dist: scipy
16
+ Provides-Extra: numba
17
+ Requires-Dist: numba; extra == 'numba'
16
18
  Description-Content-Type: text/markdown
17
19
 
18
20
  # pqopen-lib
@@ -0,0 +1,58 @@
1
+ import numpy as np
2
+ try:
3
+ from numba import njit, float32, float64
4
+ except ImportError:
5
+ # Dummy-Decorator if numba not available
6
+ def njit(func=None, **kwargs):
7
+ if func is None:
8
+ return lambda f: f
9
+ return func
10
+ float32 = float64 = None
11
+
12
+ @njit
13
+ def calc_single_freq(x: np.array, f_hz: float, fs: float) -> tuple[float, float]:
14
+ """Compute the amplitude and phase of a specific frequency component
15
+ in a signal using the Goertzel algorithm.
16
+
17
+ Parameters:
18
+ x: Input signal (time-domain samples).
19
+ f_hz: Target frequency in Hertz (Hz).
20
+ fs: Sampling frequency of the signal in Hertz (Hz).
21
+
22
+ Returns:
23
+ tuple:
24
+ amp: Amplitude of the frequency component.
25
+ phase: Phase of the frequency component in radians.
26
+ """
27
+ N = len(x) # Number of samples
28
+ k = (f_hz * N) / fs # Corresponding DFT bin index (can be fractional)
29
+ w = 2 * np.pi * k / N # Angular frequency for the bin
30
+ cw = np.cos(w) # Cosine component
31
+ c = 2 * cw # Multiplier used in recurrence relation
32
+ sw = np.sin(w) # Sine component
33
+ z1, z2 = 0, 0 # Initialize state variables
34
+
35
+ # Recursive filter loop
36
+ for n in range(N):
37
+ z0 = x[n] + c * z1 - z2 # Apply recurrence relation
38
+ z2 = z1 # Shift states
39
+ z1 = z0
40
+
41
+ # Compute real and imaginary parts of the result
42
+ ip = cw * z1 - z2 # In-phase (real) component
43
+ qp = sw * z1 # Quadrature (imaginary) component
44
+
45
+ # Compute amplitude and phase of the frequency component
46
+ amp = np.sqrt((ip**2 + qp**2)/2) / (N / 2)
47
+ phase = np.arctan2(qp, ip)
48
+
49
+ return amp, phase
50
+
51
+ # >>>>>>> Pre-Compile for float32 und float64 <<<<<<<
52
+ if float32 is not None and float64 is not None:
53
+ sigs = [
54
+ "(float32[:], float32, float32)",
55
+ "(float64[:], float64, float64)"
56
+ ]
57
+ for sig in sigs:
58
+ calc_single_freq.compile(sig)
@@ -29,7 +29,7 @@ from daqopen.channelbuffer import AcqBuffer, DataChannelBuffer
29
29
  from pqopen.zcd import ZeroCrossDetector
30
30
  import pqopen.powerquality as pq
31
31
  from pqopen.helper import floor_timestamp
32
-
32
+ from pqopen.goertzel import calc_single_freq
33
33
  logger = logging.getLogger(__name__)
34
34
 
35
35
  class PowerSystem(object):
@@ -87,7 +87,8 @@ class PowerSystem(object):
87
87
  "under_over_deviation": 0,
88
88
  "mains_signaling_tracer": {},
89
89
  "debug_channels": False,
90
- "energy_channels": {}}
90
+ "energy_channels": {},
91
+ "one_period_fundamental": False}
91
92
  self._prepare_calc_channels()
92
93
  self.output_channels: Dict[str, DataChannelBuffer] = {}
93
94
  self._last_processed_sidx = 0
@@ -213,6 +214,13 @@ class PowerSystem(object):
213
214
  self._features["energy_channels"] = {"persist_file": persist_file, "energy_counters": energy_counters}
214
215
  self._channel_update_needed = True
215
216
 
217
+ def enable_one_period_fundamental(self):
218
+ """
219
+ Enables the calculation of one (single) period fundamental values
220
+ """
221
+ self._features["one_period_fundamental"] = True
222
+ self._channel_update_needed = True
223
+
216
224
  def _resync_nper_abs_time(self, zc_idx: int):
217
225
  if not self._features["nper_abs_time_sync"]:
218
226
  return None
@@ -366,6 +374,10 @@ class PowerSystem(object):
366
374
  output_channel.put_data_single(phase_period_stop_sidx, msv_value)
367
375
  if phys_type == "slope":
368
376
  output_channel.put_data_single(phase_period_stop_sidx, np.abs(np.diff(u_values)).max())
377
+ if phys_type == "fund_rms":
378
+ # Use sample-discrete frequency, not the exact one for full cycle
379
+ fund_amp, fund_phase = calc_single_freq(u_values, self._samplerate/len(u_values), self._samplerate)
380
+ output_channel.put_data_single(phase_period_stop_sidx, fund_amp)
369
381
  for phys_type, output_channel in phase._calc_channels["half_period"]["voltage"].items():
370
382
  if phys_type == "trms":
371
383
  # First half period
@@ -641,6 +653,7 @@ class PowerPhase(object):
641
653
  self._calc_channels["half_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_hp_rms'.format(self.name), agg_type='rms', unit="V")
642
654
  self._calc_channels["one_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_1p_rms'.format(self.name), agg_type='rms', unit="V")
643
655
  self._calc_channels["one_period"]["voltage"]["slope"] = DataChannelBuffer('U{:s}_1p_slope'.format(self.name), agg_type='max', unit="V/s")
656
+ self._calc_channels["one_period"]["voltage"]["fund_rms"] = DataChannelBuffer('U{:s}_1p_H1_rms'.format(self.name), agg_type='rms', unit="V")
644
657
  self._calc_channels["one_period_ovlp"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_1p_hp_rms'.format(self.name), agg_type='rms', unit="V")
645
658
  self._calc_channels["multi_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_rms'.format(self.name), agg_type='rms', unit="V")
646
659
 
@@ -12,7 +12,7 @@ packages = ["pqopen"]
12
12
 
13
13
  [project]
14
14
  name = "pqopen-lib"
15
- version = "0.7.6"
15
+ version = "0.7.7"
16
16
  dependencies = [
17
17
  "numpy",
18
18
  "daqopen-lib",
@@ -30,6 +30,9 @@ classifiers = [
30
30
  "Operating System :: OS Independent",
31
31
  ]
32
32
 
33
+ [project.optional-dependencies]
34
+ numba = ["numba"]
35
+
33
36
  [project.urls]
34
37
  Homepage = "https://github.com/DaqOpen/pqopen-lib"
35
38
  Issues = "https://github.com/DaqOpen/pqopen-lib/issues"
@@ -0,0 +1,32 @@
1
+ import unittest
2
+ import sys
3
+ import os
4
+ import numpy as np
5
+
6
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
+ sys.path.append(os.path.dirname(SCRIPT_DIR))
8
+
9
+ from pqopen.goertzel import calc_single_freq
10
+
11
+ class TestSingleFrequency(unittest.TestCase):
12
+ def test_fund_only(self):
13
+ fs = 100
14
+ t = np.arange(0, 1, 1/fs)
15
+ u = np.sqrt(2)*np.sin(2*np.pi*t)
16
+ amp, phase = calc_single_freq(u, 1, fs)
17
+
18
+ self.assertAlmostEqual(1, amp)
19
+ self.assertAlmostEqual(-90*np.pi/180, phase)
20
+
21
+ def test_mixed(self):
22
+ fs = 100
23
+ t = np.arange(0, 1, 1/fs)
24
+ u = np.sqrt(2)*np.sin(2*np.pi*t)
25
+ u += 0.5*np.sqrt(2)*np.sin(2*3*np.pi*t)
26
+ amp, phase = calc_single_freq(u, 1, fs)
27
+
28
+ self.assertAlmostEqual(1, amp)
29
+ self.assertAlmostEqual(-90*np.pi/180, phase)
30
+
31
+ if __name__ == "__main__":
32
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes