pqopen-lib 0.8.0__py3-none-any.whl → 0.8.2__py3-none-any.whl

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.
@@ -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 = [
pqopen/helper.py CHANGED
@@ -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
pqopen/powerquality.py CHANGED
@@ -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.ceil(np.log2(data.size)))
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,_ = signal.lfilter(self.stage0_tp_filter_coeff[0], self.stage0_tp_filter_coeff[1], raw_data, zi=self.stage0_tp_filter_zi)
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,_ = signal.lfilter(self.stage1_tp_filter_coeff[0], self.stage1_tp_filter_coeff[1], hp_data, zi=self.stage1_tp_filter_zi)
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,_ = signal.lfilter(self.stage3_hp_filter_coeff[0], self.stage3_hp_filter_coeff[1], stage2_output, zi=self.stage3_hp_filter_zi)
175
- self.stage3_hp_filter_zi = signal.lfiltic(self.stage3_hp_filter_coeff[0],
176
- self.stage3_hp_filter_coeff[1],
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,_ = signal.lfilter(self.stage4_tp_filter_coeff[0], self.stage4_tp_filter_coeff[1], stage3_output, zi=self.stage4_tp_filter_zi)
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
pqopen/powersystem.py CHANGED
@@ -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.goertzel import calc_single_freq
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//2+1,
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//2+1,
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
- add_start_idx_sample = 1 if self._last_zc_frac < 0 else 0
407
- add_stop_idx_sample = 1 if actual_zc_frac > 0 else 0
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,9 @@ 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
+ data_fft_U *= phase._u_fft_corr_array
481
485
  u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
482
486
  u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
483
487
  if phase._number == 1: # use phase 1 angle as reference
@@ -672,13 +676,8 @@ class PowerSystem(object):
672
676
  self._calculation_mode = "FALLBACK"
673
677
  else:
674
678
  self._calculation_mode = "NORMAL"
675
-
676
- # Remove Non monotonic rising zero crossings
677
- filtered_zero_crossings = []
678
- for idx,zc in enumerate(zero_crossings):
679
- if (int(zc)+start_acq_sidx) >= self._zero_crossings[-1] or np.isnan(self._zero_crossings[-1]):
680
- filtered_zero_crossings.append(zc)
681
- return filtered_zero_crossings
679
+
680
+ return zero_crossings
682
681
 
683
682
  def get_aggregated_data(self, start_acq_sidx: int, stop_acq_sidx: int) -> dict:
684
683
  """
@@ -734,6 +733,8 @@ class PowerPhase(object):
734
733
  self._calc_channels = {}
735
734
  self._voltage_fluctuation_processor: pq.VoltageFluctuation = None
736
735
  self._mains_signaling_tracer: pq.MainsSignalingVoltageTracer = None
736
+ self._u_fft_corr_array = None
737
+ self._i_fft_corr_array = None
737
738
 
738
739
  def update_calc_channels(self, features: dict):
739
740
  """
pqopen/zcd.py CHANGED
@@ -48,6 +48,7 @@ class ZeroCrossDetector:
48
48
  self._last_zc_p = None
49
49
  self._last_zc_n = None
50
50
  self._last_zc_n_val = None
51
+ self._last_zc = None
51
52
 
52
53
  # Calculate the filter delay in samples
53
54
  w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1], worN=[self.f_cutoff], fs=self.samplerate)
@@ -111,9 +112,15 @@ class ZeroCrossDetector:
111
112
  k = (y2 - y1) / (x2 - x1)
112
113
  d = y1 - k * x1
113
114
  real_zc = -d/k
114
- #real_zc = (threshold_p_cross[p_idx] - threshold_n_cross[n_idx] + 1) / 2 + threshold_n_cross[n_idx]
115
- zero_crossings.append(real_zc + self.filter_delay_samples)
116
- last_used_p_idx = p_idx
115
+ if np.isnan(real_zc):
116
+ logger.warning("Detection Error: real_zc is NaN, ignoring")
117
+ else:
118
+ if self._last_zc and (real_zc <= self._last_zc):
119
+ logger.warning("Detected ZC before last one, ignoring")
120
+ else:
121
+ zero_crossings.append(real_zc + self.filter_delay_samples)
122
+ last_used_p_idx = p_idx
123
+ self._last_zc = real_zc
117
124
 
118
125
  # Update the last negative threshold-crossing for the next block
119
126
  if last_used_p_idx < len(threshold_p_cross) and len(threshold_n_cross) > 0:
@@ -126,4 +133,8 @@ class ZeroCrossDetector:
126
133
  else:
127
134
  self._last_zc_n = None
128
135
 
136
+ # Update Last Valid ZC Index
137
+ if self._last_zc:
138
+ self._last_zc -= len(data)
139
+
129
140
  return zero_crossings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pqopen-lib
3
- Version: 0.8.0
3
+ Version: 0.8.2
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
@@ -0,0 +1,12 @@
1
+ pqopen/__init__.py,sha256=sMVEOm5j6AZYnnEox5PHOUyZlL5TJjpsNMm5ATLx6ec,329
2
+ pqopen/auxcalc.py,sha256=P11Nu9pgJRoPYZDjLk-mXI6Ha02LoTH5bS9FdFTvC9M,2733
3
+ pqopen/eventdetector.py,sha256=NKZU7GbeorZdkYu3ET4lhMaeynw70GhIGO2p1xH4aTA,11962
4
+ pqopen/helper.py,sha256=0msrm6i1v8jj2Z5X8F7wDEW0KD5i91RBNZwPJC05YrA,2533
5
+ pqopen/powerquality.py,sha256=dRVCedWa1QJKHgdiYoIIdvhH_p40cwpgeUePO5u1j28,15953
6
+ pqopen/powersystem.py,sha256=R9nOIv5Qpuc1JPDzP67NGP9LNqB74unpY85yS1CuWPg,48921
7
+ pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
+ pqopen/zcd.py,sha256=jN4jkGgFNBWQWRebefLc1GjUKm1xdYQXav_ajYfkuo4,6054
9
+ pqopen_lib-0.8.2.dist-info/METADATA,sha256=tdCcqLVphJ4slcPgOe9gofVITNC2k4qNMnPwuViNCAE,4787
10
+ pqopen_lib-0.8.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ pqopen_lib-0.8.2.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
+ pqopen_lib-0.8.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,12 +0,0 @@
1
- pqopen/__init__.py,sha256=sMVEOm5j6AZYnnEox5PHOUyZlL5TJjpsNMm5ATLx6ec,329
2
- pqopen/eventdetector.py,sha256=NKZU7GbeorZdkYu3ET4lhMaeynw70GhIGO2p1xH4aTA,11962
3
- pqopen/goertzel.py,sha256=JhehuEj-xNnwQEh4Ti8dXiGvfDmxbdEoaxIsFswZNcM,2008
4
- pqopen/helper.py,sha256=bM_wDck5OfeEy96U_2FjCwZuLZRPYyKVeiYD3-vzP6M,1761
5
- pqopen/powerquality.py,sha256=Qwyaj7BQqQPRivei140mv-Leh6u9uIQzViKLOY7bHyw,17877
6
- pqopen/powersystem.py,sha256=unaQLrCb5V1l6kjX6R186MflAV9BsWVeJd_Sc8azsq8,48895
7
- pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
- pqopen/zcd.py,sha256=cqGURF6dtvdYanI-vby95Gn9Kvh8F4cfKCqUFlyM15Y,5678
9
- pqopen_lib-0.8.0.dist-info/METADATA,sha256=pvr3sXmlAPcGiEYp9KEqgcrZ1-qKEFkVrgxvRQQKB5E,4780
10
- pqopen_lib-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- pqopen_lib-0.8.0.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
- pqopen_lib-0.8.0.dist-info/RECORD,,