pqopen-lib 0.8.0__tar.gz → 0.8.2__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.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
@@ -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 = [
@@ -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
@@ -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
@@ -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
  """
@@ -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
@@ -12,10 +12,10 @@ packages = ["pqopen"]
12
12
 
13
13
  [project]
14
14
  name = "pqopen-lib"
15
- version = "0.8.0"
15
+ version = "0.8.2"
16
16
  dependencies = [
17
17
  "numpy",
18
- "daqopen-lib",
18
+ "daqopen-lib >= 0.7.4",
19
19
  "scipy"
20
20
  ]
21
21
 
@@ -6,7 +6,7 @@ import numpy as np
6
6
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
7
  sys.path.append(os.path.dirname(SCRIPT_DIR))
8
8
 
9
- from pqopen.goertzel import calc_single_freq
9
+ from pqopen.auxcalc import calc_single_freq
10
10
 
11
11
  class TestSingleFrequency(unittest.TestCase):
12
12
  def test_fund_only(self):
@@ -1,12 +1,13 @@
1
1
  import unittest
2
2
  import sys
3
3
  import os
4
+ import numpy as np
4
5
  from datetime import datetime
5
6
 
6
7
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
7
8
  sys.path.append(os.path.dirname(SCRIPT_DIR))
8
9
 
9
- from pqopen.helper import floor_timestamp, JsonDecimalLimiter
10
+ from pqopen.helper import floor_timestamp, JsonDecimalLimiter, create_harm_corr_array, create_fft_corr_array
10
11
 
11
12
  class TestFloorTimestamp(unittest.TestCase):
12
13
 
@@ -100,5 +101,31 @@ class TestLimitDecimalPlaces(unittest.TestCase):
100
101
  input_json = '{"sci": 1.23e10}'
101
102
  self.assertEqual(float_limiter.process(input_json), input_json)
102
103
 
104
+ class TestHarmCorrCreater(unittest.TestCase):
105
+
106
+ def test_harmonic(self):
107
+ freq_response = ((50, 1.0), (100, 0.9))
108
+ expected_corr_factors = 1/np.array([1, 1, 0.9, 0.9, 0.9, 0.9])
109
+ corr_factors = create_harm_corr_array(50, 5, freq_response)
110
+
111
+ self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
112
+
113
+ def test_interharmonic(self):
114
+ freq_response = ((50, 1.0), (100, 0.9))
115
+ expected_corr_factors = 1/np.array([1, 0.95, 0.9, 0.9, 0.9, 0.9])
116
+ corr_factors = create_harm_corr_array(50, 5, freq_response, interharm=True)
117
+
118
+ self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
119
+
120
+ class TestFftCorrCreater(unittest.TestCase):
121
+
122
+ def test_small(self):
123
+ freq_response = ((50, 1.0), (100, 0.9))
124
+
125
+ expected_corr_factors = 1/np.array([1, 1, 0.9, 0.9, 0.9, 0.9])
126
+ corr_factors = create_fft_corr_array(6, 250, freq_response)
127
+
128
+ self.assertIsNone(np.testing.assert_array_almost_equal(corr_factors, expected_corr_factors))
129
+
103
130
  if __name__ == '__main__':
104
131
  unittest.main()
@@ -69,7 +69,7 @@ class TestPowerPowerQualityFlicker(unittest.TestCase):
69
69
 
70
70
  blocksize = 1000
71
71
  for blk_idx in range(t.size // blocksize):
72
- hp_data = 230*np.ones(samplerate//blocksize*f_fund*2)
72
+ hp_data = 230*np.ones((f_fund*2*blocksize)//samplerate)
73
73
  voltage_fluctuation.process(blk_idx*blocksize, hp_data, u_values[blk_idx*blocksize:(blk_idx+1)*blocksize])
74
74
  pst = voltage_fluctuation.calc_pst(0, duration*samplerate)
75
75
 
@@ -90,7 +90,7 @@ class TestPowerPowerQualityFlicker(unittest.TestCase):
90
90
 
91
91
  blocksize = 1000
92
92
  for blk_idx in range(t.size // blocksize):
93
- hp_data = 230*np.ones(samplerate//blocksize*f_fund*2)
93
+ hp_data = 230*np.ones((f_fund*2*blocksize)//samplerate)
94
94
  voltage_fluctuation.process(blk_idx*blocksize, hp_data, u_values[blk_idx*blocksize:(blk_idx+1)*blocksize])
95
95
  pinst_1s, _ = voltage_fluctuation._pinst_channel.read_data_by_acq_sidx((duration-1)*samplerate, duration*samplerate)
96
96
 
@@ -249,6 +249,40 @@ class TestPowerSystemCalculation(unittest.TestCase):
249
249
  freq, _ = self.power_system.output_channels["Freq"].read_data_by_acq_sidx(0, u_values.size)
250
250
  self.assertIsNone(np.testing.assert_array_almost_equal(freq[1:], expected_freq, 3))
251
251
 
252
+ class TestPowerSystemCalculationFreqResponse(unittest.TestCase):
253
+ def setUp(self):
254
+ self.u_channel = AcqBuffer(freq_response=((50,1.0), (100,0.9)))
255
+ self.i_channel = AcqBuffer()
256
+
257
+ # Create PowerSystem instance
258
+ self.power_system = PowerSystem(
259
+ zcd_channel=self.u_channel,
260
+ input_samplerate=10000.0,
261
+ zcd_threshold=0.1
262
+ )
263
+ # Add Phase
264
+ self.power_system.add_phase(u_channel=self.u_channel, i_channel=self.i_channel)
265
+
266
+ def test_multi_period_calc_harmonic_msv(self):
267
+ t = np.linspace(0, 1, int(self.power_system._samplerate), endpoint=False)
268
+ u_values = np.sqrt(2)*np.sin(2*np.pi*50*t) + 0.09*np.sqrt(2)*np.sin(2*np.pi*150*t) + 0.009*np.sqrt(2)*np.sin(2*np.pi*375*t)
269
+ i_values = 2*np.sqrt(2)*np.sin(2*np.pi*50*t+60*np.pi/180) # cos_phi = 0.5
270
+
271
+ expected_u_h3_rms = np.array(np.zeros(4)) + 0.1
272
+ expected_u_msv_rms = np.array(np.zeros(4)) + 0.01
273
+
274
+ self.power_system.enable_harmonic_calculation(10)
275
+ self.power_system.enable_mains_signaling_calculation(375)
276
+ self.u_channel.put_data(u_values)
277
+ self.i_channel.put_data(i_values)
278
+ self.power_system.process()
279
+
280
+ # Check Voltage
281
+ u_h_rms, _ = self.power_system.output_channels["U1_H_rms"].read_data_by_acq_sidx(0, u_values.size)
282
+ self.assertIsNone(np.testing.assert_allclose(u_h_rms[:,3], expected_u_h3_rms, rtol=0.01))
283
+ u_msv_rms, _ = self.power_system.output_channels["U1_msv_rms"].read_data_by_acq_sidx(0, u_values.size)
284
+ self.assertIsNone(np.testing.assert_allclose(u_msv_rms, expected_u_msv_rms, rtol=0.01))
285
+
252
286
  class TestPowerSystemCalculationThreePhase(unittest.TestCase):
253
287
  def setUp(self):
254
288
  self.u1_channel = AcqBuffer()
@@ -88,7 +88,7 @@ class TestStorageController(unittest.TestCase):
88
88
 
89
89
  self.storage_controller.process_events(events)
90
90
 
91
- self.assertEqual(storage_endpoint._event_list[0].start_ts, 0.01)
91
+ self.assertAlmostEqual(storage_endpoint._event_list[0].start_ts, 0.01)
92
92
 
93
93
  def test_one_storageplan_series_slow(self):
94
94
  start_timestamp = int(1000000000*1e6)
File without changes
File without changes
File without changes
File without changes