pqopen-lib 0.8.1__py3-none-any.whl → 0.8.3__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,
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,
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,10 @@ 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
+ 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)]
481
486
  u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
482
487
  u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
483
488
  if phase._number == 1: # use phase 1 angle as reference
@@ -729,6 +734,8 @@ class PowerPhase(object):
729
734
  self._calc_channels = {}
730
735
  self._voltage_fluctuation_processor: pq.VoltageFluctuation = None
731
736
  self._mains_signaling_tracer: pq.MainsSignalingVoltageTracer = None
737
+ self._u_fft_corr_array = None
738
+ self._i_fft_corr_array = None
732
739
 
733
740
  def update_calc_channels(self, features: dict):
734
741
  """
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pqopen-lib
3
- Version: 0.8.1
3
+ Version: 0.8.3
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=ta73R9XPZiEKkHtgwZVfoXBPwIv0NNtUhRfwRFStA-0,49117
7
+ pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
+ pqopen/zcd.py,sha256=jN4jkGgFNBWQWRebefLc1GjUKm1xdYQXav_ajYfkuo4,6054
9
+ pqopen_lib-0.8.3.dist-info/METADATA,sha256=ClKfxsmOIQGpiIvOrZytvIb8CDTcUvzYvbnBftjbyAQ,4787
10
+ pqopen_lib-0.8.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ pqopen_lib-0.8.3.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
+ pqopen_lib-0.8.3.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=5gZcZem_mNRqezjC67qlCePht6L8CraPtQNsjPgP7C4,48581
7
- pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
- pqopen/zcd.py,sha256=jN4jkGgFNBWQWRebefLc1GjUKm1xdYQXav_ajYfkuo4,6054
9
- pqopen_lib-0.8.1.dist-info/METADATA,sha256=XSvdYAXMzrjm_FH1qEB5QHoYfsBk3TDldRj-pqbuEGM,4780
10
- pqopen_lib-0.8.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- pqopen_lib-0.8.1.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
- pqopen_lib-0.8.1.dist-info/RECORD,,