pqopen-lib 0.7.6__tar.gz → 0.7.8__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.7.6 → pqopen_lib-0.7.8}/PKG-INFO +3 -1
- pqopen_lib-0.7.8/pqopen/goertzel.py +58 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/powersystem.py +17 -2
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pyproject.toml +5 -1
- pqopen_lib-0.7.8/test/goertzel-test.py +32 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/.gitignore +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/LICENSE +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/README.md +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/__init__.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/eventdetector.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/helper.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/powerquality.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/storagecontroller.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/pqopen/zcd.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/data_files/event_data_level_low.csv +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/eventdetector-test.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/helper-test.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/powerquality-test.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/powersystem-test.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/storagecontroller-test.py +0 -0
- {pqopen_lib-0.7.6 → pqopen_lib-0.7.8}/test/zcd-test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pqopen-lib
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.8
|
|
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
|
|
@@ -665,6 +677,9 @@ class PowerPhase(object):
|
|
|
665
677
|
self._calc_channels["one_period"]["voltage"]["msv_mag"] = DataChannelBuffer('U{:s}_1p_msv'.format(self.name), agg_type='max', unit="V")
|
|
666
678
|
self._calc_channels["one_period"]["voltage"]["msv_bit"] = DataChannelBuffer('U{:s}_msv_bit'.format(self.name), unit="")
|
|
667
679
|
|
|
680
|
+
if "one_period_fundamental" in features and features["one_period_fundamental"]:
|
|
681
|
+
self._calc_channels["one_period"]["voltage"]["fund_rms"] = DataChannelBuffer('U{:s}_1p_H1_rms'.format(self.name), agg_type='rms', unit="V")
|
|
682
|
+
|
|
668
683
|
# Create Current Channels
|
|
669
684
|
if self._i_channel:
|
|
670
685
|
self._calc_channels["one_period"]["current"] = {}
|
|
@@ -12,12 +12,13 @@ packages = ["pqopen"]
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "pqopen-lib"
|
|
15
|
-
version = "0.7.
|
|
15
|
+
version = "0.7.8"
|
|
16
16
|
dependencies = [
|
|
17
17
|
"numpy",
|
|
18
18
|
"daqopen-lib",
|
|
19
19
|
"scipy"
|
|
20
20
|
]
|
|
21
|
+
|
|
21
22
|
authors = [
|
|
22
23
|
{ name="Michael Oberhofer", email="michael@daqopen.com" },
|
|
23
24
|
]
|
|
@@ -30,6 +31,9 @@ classifiers = [
|
|
|
30
31
|
"Operating System :: OS Independent",
|
|
31
32
|
]
|
|
32
33
|
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
numba = ["numba"]
|
|
36
|
+
|
|
33
37
|
[project.urls]
|
|
34
38
|
Homepage = "https://github.com/DaqOpen/pqopen-lib"
|
|
35
39
|
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
|
|
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
|