pqopen-lib 0.7.9__tar.gz → 0.8.0__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.9 → pqopen_lib-0.8.0}/PKG-INFO +1 -1
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/powersystem.py +91 -9
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/zcd.py +3 -5
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pyproject.toml +1 -1
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/powersystem-test.py +33 -2
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/zcd-test.py +1 -1
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/.gitignore +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/LICENSE +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/README.md +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/__init__.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/eventdetector.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/goertzel.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/helper.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/powerquality.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/pqopen/storagecontroller.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/data_files/event_data_level_low.csv +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/eventdetector-test.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/goertzel-test.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/helper-test.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/powerquality-test.py +0 -0
- {pqopen_lib-0.7.9 → pqopen_lib-0.8.0}/test/storagecontroller-test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pqopen-lib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -89,7 +89,8 @@ class PowerSystem(object):
|
|
|
89
89
|
"debug_channels": False,
|
|
90
90
|
"energy_channels": {},
|
|
91
91
|
"one_period_fundamental": 0,
|
|
92
|
-
"rms_trapz_rule": False
|
|
92
|
+
"rms_trapz_rule": False,
|
|
93
|
+
"pmu_calculation": False}
|
|
93
94
|
self._prepare_calc_channels()
|
|
94
95
|
self.output_channels: Dict[str, DataChannelBuffer] = {}
|
|
95
96
|
self._last_processed_sidx = 0
|
|
@@ -109,7 +110,8 @@ class PowerSystem(object):
|
|
|
109
110
|
self._calc_channels = {"half_period": {"voltage": {}, "current": {}, "power": {}, "_debug": {}},
|
|
110
111
|
"one_period": {"voltage": {}, "current": {}, "power": {}, "_debug": {}},
|
|
111
112
|
"one_period_ovlp": {"voltage": {}, "current": {}, "power": {}, "_debug": {}},
|
|
112
|
-
"multi_period": {"voltage": {}, "current": {}, "power": {}, "energy": {}, "_debug": {}}
|
|
113
|
+
"multi_period": {"voltage": {}, "current": {}, "power": {}, "energy": {}, "_debug": {}},
|
|
114
|
+
"pmu": {"voltage": {}, "current": {}, "power": {}, "_debug": {}}}
|
|
113
115
|
|
|
114
116
|
def add_phase(self, u_channel: AcqBuffer, i_channel: AcqBuffer = None, name: str = ""):
|
|
115
117
|
"""
|
|
@@ -230,6 +232,19 @@ class PowerSystem(object):
|
|
|
230
232
|
"""
|
|
231
233
|
self._features["rms_trapz_rule"] = True
|
|
232
234
|
|
|
235
|
+
def enable_pmu_calculation(self):
|
|
236
|
+
"""
|
|
237
|
+
Enables the calculation of equidistant PMU values
|
|
238
|
+
"""
|
|
239
|
+
if not self._features["nper_abs_time_sync"]:
|
|
240
|
+
logger.warning("To enable pmu_calculation, nper_abs_time_sync must be enabled first.")
|
|
241
|
+
return False
|
|
242
|
+
self._features["pmu_calculation"] = True
|
|
243
|
+
self._channel_update_needed = True
|
|
244
|
+
self._pmu_last_processed_sidx = 0
|
|
245
|
+
self._pmu_last_processed_ts_us = 0
|
|
246
|
+
self._pmu_time_increment_us = int(1_000_000 / self.nominal_frequency)
|
|
247
|
+
|
|
233
248
|
def _resync_nper_abs_time(self, zc_idx: int):
|
|
234
249
|
if not self._features["nper_abs_time_sync"]:
|
|
235
250
|
return None
|
|
@@ -278,6 +293,8 @@ class PowerSystem(object):
|
|
|
278
293
|
self._calc_channels["multi_period"]["energy"]["w_pos"].last_sample_value = self._features["energy_channels"]["energy_counters"].get("W_pos", 0.0)
|
|
279
294
|
self._calc_channels["multi_period"]["energy"]["w_neg"] = DataChannelBuffer('W_neg', agg_type='max', unit="Wh", dtype=np.float64)
|
|
280
295
|
self._calc_channels["multi_period"]["energy"]["w_neg"].last_sample_value = self._features["energy_channels"]["energy_counters"].get("W_neg", 0.0)
|
|
296
|
+
if self._features["pmu_calculation"]:
|
|
297
|
+
self._calc_channels["pmu"]["power"]["freq"] = DataChannelBuffer('Freq_pmu', agg_type='mean', unit="Hz")
|
|
281
298
|
|
|
282
299
|
for agg_interval, phys_types in self._calc_channels.items():
|
|
283
300
|
for phys_type, calc_type in phys_types.items():
|
|
@@ -340,6 +357,9 @@ class PowerSystem(object):
|
|
|
340
357
|
self._process_multi_period(self._zero_crossings[-self.nper - 1], self._zero_crossings[-1])
|
|
341
358
|
self._resync_nper_abs_time(-1)
|
|
342
359
|
self._process_fluctuation_calc(self._zero_crossings[-self.nper - 1], self._zero_crossings[-1])
|
|
360
|
+
|
|
361
|
+
# Process fixed-time (PMU) channels
|
|
362
|
+
self._process_pmu_calc(self._zero_crossings[-1])
|
|
343
363
|
|
|
344
364
|
self._last_processed_sidx = stop_acq_sidx
|
|
345
365
|
|
|
@@ -373,6 +393,13 @@ class PowerSystem(object):
|
|
|
373
393
|
u_values = phase._u_channel.read_data_by_index(phase_period_start_sidx, phase_period_stop_sidx)
|
|
374
394
|
if self._features["mains_signaling_tracer"]:
|
|
375
395
|
msv_edge, msv_value = phase._mains_signaling_tracer.process(u_values[::10])
|
|
396
|
+
if self._features["one_period_fundamental"]:
|
|
397
|
+
# Use sample-discrete frequency, not the exact one for full cycle
|
|
398
|
+
u_values_sync = phase._u_channel.read_data_by_index(period_start_sidx, period_stop_sidx)
|
|
399
|
+
self._fund_freq_list = np.roll(self._fund_freq_list,-1)
|
|
400
|
+
self._fund_freq_list[-1] = self._samplerate/len(u_values_sync)
|
|
401
|
+
mean_freq = self._fund_freq_list[self._fund_freq_list>0].mean()
|
|
402
|
+
fund_amp, fund_phase = calc_single_freq(u_values_sync, mean_freq, self._samplerate)
|
|
376
403
|
for phys_type, output_channel in phase._calc_channels["one_period"]["voltage"].items():
|
|
377
404
|
if phys_type == "trms":
|
|
378
405
|
if self._features["rms_trapz_rule"]:
|
|
@@ -399,12 +426,10 @@ class PowerSystem(object):
|
|
|
399
426
|
if phys_type == "slope":
|
|
400
427
|
output_channel.put_data_single(phase_period_stop_sidx, np.abs(np.diff(u_values)).max())
|
|
401
428
|
if phys_type == "fund_rms":
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
fund_amp, fund_phase = calc_single_freq(u_values, mean_freq, self._samplerate)
|
|
407
|
-
output_channel.put_data_single(phase_period_stop_sidx, fund_amp)
|
|
429
|
+
output_channel.put_data_single(period_stop_sidx, fund_amp)
|
|
430
|
+
if phys_type == "fund_phi":
|
|
431
|
+
output_channel.put_data_single(period_stop_sidx, pq.normalize_phi(np.rad2deg(fund_phase+np.pi/2)))
|
|
432
|
+
|
|
408
433
|
for phys_type, output_channel in phase._calc_channels["half_period"]["voltage"].items():
|
|
409
434
|
if phys_type == "trms":
|
|
410
435
|
# First half period
|
|
@@ -571,6 +596,52 @@ class PowerSystem(object):
|
|
|
571
596
|
self._pst_last_calc_sidx = stop_sidx
|
|
572
597
|
self._pst_next_round_ts = floor_timestamp(stop_ts, self._pst_interval_sec, ts_resolution="us")+self._pst_interval_sec*1_000_000
|
|
573
598
|
|
|
599
|
+
def _process_pmu_calc(self, stop_sidx: int):
|
|
600
|
+
"""
|
|
601
|
+
Process data to calculate PMU (Phasor Measurement Unit) parameters.
|
|
602
|
+
|
|
603
|
+
Parameters:
|
|
604
|
+
stop_sidx: Stop sample index for calculation
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
None
|
|
608
|
+
"""
|
|
609
|
+
if not self._features["pmu_calculation"]:
|
|
610
|
+
return None
|
|
611
|
+
# Read timestamps
|
|
612
|
+
start_sidx = int(self._pmu_last_processed_sidx - 1.5*self._samplerate / self.nominal_frequency)
|
|
613
|
+
start_sidx = max(0, start_sidx)
|
|
614
|
+
ts_us = self._time_channel.read_data_by_index(start_sidx, stop_sidx)
|
|
615
|
+
first_pmu_ts = (self._pmu_last_processed_ts_us + self._pmu_time_increment_us) if self._pmu_last_processed_ts_us > 0 else ts_us[0] - ts_us[0] % self._pmu_time_increment_us + self._pmu_time_increment_us
|
|
616
|
+
last_pmu_ts = ts_us[-1] - (ts_us[-1] % self._pmu_time_increment_us) - self._pmu_time_increment_us # convert until n-1
|
|
617
|
+
logger.debug(f"first_pmu_ts: {first_pmu_ts:d}, last_pmu_ts: {last_pmu_ts:d}, self._pmu_time_increment_us: {self._pmu_time_increment_us:d}")
|
|
618
|
+
wanted_pmu_ts = np.arange(first_pmu_ts, last_pmu_ts+1, self._pmu_time_increment_us, dtype=np.int64)
|
|
619
|
+
wanted_pmu_sidx = [int(np.searchsorted(ts_us, pmu_ts)+start_sidx) for pmu_ts in wanted_pmu_ts]
|
|
620
|
+
wanted_pmu_ts_map = dict(zip(wanted_pmu_sidx, wanted_pmu_ts))
|
|
621
|
+
real_pmu_ts_map = {pmu_sidx: int(ts_us[pmu_sidx - start_sidx]) for pmu_sidx in wanted_pmu_sidx}
|
|
622
|
+
ovlp_window_start = max(0, int(wanted_pmu_sidx[0] - 1.5*self._samplerate / self.nominal_frequency))
|
|
623
|
+
freq, sample_indices = self._calc_channels["one_period"]["power"]["freq"].read_data_by_acq_sidx(ovlp_window_start, stop_sidx)
|
|
624
|
+
|
|
625
|
+
if len(sample_indices) == 0:
|
|
626
|
+
return None
|
|
627
|
+
for phase in self._phases:
|
|
628
|
+
u_raw = phase._u_channel.read_data_by_index(ovlp_window_start, stop_sidx)
|
|
629
|
+
for pmu_sidx in wanted_pmu_sidx:
|
|
630
|
+
# Search for previous zc
|
|
631
|
+
zc_idx = np.searchsorted(sample_indices, pmu_sidx,side="left")
|
|
632
|
+
if zc_idx == len(sample_indices):
|
|
633
|
+
zc_idx -= 1
|
|
634
|
+
local_stop_idx = max(0, pmu_sidx - ovlp_window_start)
|
|
635
|
+
local_start_idx = max(0, local_stop_idx - int(np.round(self._samplerate/freq[zc_idx])))
|
|
636
|
+
fund_amp, fund_phase = calc_single_freq(u_raw[local_start_idx:local_stop_idx], freq[zc_idx], self._samplerate)
|
|
637
|
+
frac_sidx_phi_offset = (wanted_pmu_ts_map[pmu_sidx] - real_pmu_ts_map[pmu_sidx])/1e6*2*np.pi*freq[zc_idx]
|
|
638
|
+
phase._calc_channels["pmu"]["voltage"]["rms"].put_data_single(pmu_sidx, fund_amp)
|
|
639
|
+
phase._calc_channels["pmu"]["voltage"]["phi"].put_data_single(pmu_sidx, pq.normalize_phi(np.rad2deg(fund_phase+np.pi/2+frac_sidx_phi_offset)))
|
|
640
|
+
# TODO: Add Frequency PMU Channel as well??
|
|
641
|
+
# TODO: Add current channels?
|
|
642
|
+
self._pmu_last_processed_sidx = stop_sidx
|
|
643
|
+
self._pmu_last_processed_ts_us = last_pmu_ts
|
|
644
|
+
|
|
574
645
|
def _detect_zero_crossings(self, start_acq_sidx: int, stop_acq_sidx: int) -> List[float]:
|
|
575
646
|
"""
|
|
576
647
|
Detects zero crossings in the signal.
|
|
@@ -671,12 +742,13 @@ class PowerPhase(object):
|
|
|
671
742
|
Parameters:
|
|
672
743
|
features: Dict of features
|
|
673
744
|
"""
|
|
674
|
-
self._calc_channels = {"half_period": {}, "one_period": {}, "one_period_ovlp": {}, "multi_period": {}}
|
|
745
|
+
self._calc_channels = {"half_period": {}, "one_period": {}, "one_period_ovlp": {}, "multi_period": {}, "pmu": {}}
|
|
675
746
|
# Create Voltage Channels
|
|
676
747
|
self._calc_channels["half_period"]["voltage"] = {}
|
|
677
748
|
self._calc_channels["one_period"]["voltage"] = {}
|
|
678
749
|
self._calc_channels["one_period_ovlp"]["voltage"] = {}
|
|
679
750
|
self._calc_channels["multi_period"]["voltage"] = {}
|
|
751
|
+
self._calc_channels["pmu"]["voltage"] = {}
|
|
680
752
|
self._calc_channels["half_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_hp_rms'.format(self.name), agg_type='rms', unit="V")
|
|
681
753
|
self._calc_channels["one_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_1p_rms'.format(self.name), agg_type='rms', unit="V")
|
|
682
754
|
self._calc_channels["one_period"]["voltage"]["slope"] = DataChannelBuffer('U{:s}_1p_slope'.format(self.name), agg_type='max', unit="V/s")
|
|
@@ -706,6 +778,11 @@ class PowerPhase(object):
|
|
|
706
778
|
|
|
707
779
|
if "one_period_fundamental" in features and features["one_period_fundamental"] > 0:
|
|
708
780
|
self._calc_channels["one_period"]["voltage"]["fund_rms"] = DataChannelBuffer('U{:s}_1p_H1_rms'.format(self.name), agg_type='rms', unit="V")
|
|
781
|
+
self._calc_channels["one_period"]["voltage"]["fund_phi"] = DataChannelBuffer('U{:s}_1p_H1_phi'.format(self.name), agg_type='phi', unit="°")
|
|
782
|
+
|
|
783
|
+
if "pmu_calculation" in features and features["pmu_calculation"]:
|
|
784
|
+
self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
|
|
785
|
+
self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
|
|
709
786
|
|
|
710
787
|
# Create Current Channels
|
|
711
788
|
if self._i_channel:
|
|
@@ -713,6 +790,7 @@ class PowerPhase(object):
|
|
|
713
790
|
self._calc_channels["multi_period"]["current"] = {}
|
|
714
791
|
self._calc_channels["one_period"]["current"]["trms"] = DataChannelBuffer('I{:s}_1p_rms'.format(self.name), agg_type='rms', unit="A")
|
|
715
792
|
self._calc_channels["multi_period"]["current"]["trms"] = DataChannelBuffer('I{:s}_rms'.format(self.name), agg_type='rms', unit="A")
|
|
793
|
+
self._calc_channels["pmu"]["current"] = {}
|
|
716
794
|
self._calc_channels["one_period"]["power"] = {}
|
|
717
795
|
self._calc_channels["multi_period"]["power"] = {}
|
|
718
796
|
|
|
@@ -725,6 +803,10 @@ class PowerPhase(object):
|
|
|
725
803
|
self._calc_channels["multi_period"]["power"]['p_fund_mag'] = DataChannelBuffer('P{:s}_H1'.format(self.name), agg_type='mean', unit="W")
|
|
726
804
|
self._calc_channels["multi_period"]["power"]['q_fund_mag'] = DataChannelBuffer('Q{:s}_H1'.format(self.name), agg_type='mean', unit="var")
|
|
727
805
|
|
|
806
|
+
if "pmu_calculation" in features and features["pmu_calculation"]:
|
|
807
|
+
self._calc_channels["pmu"]["current"]["rms"] = DataChannelBuffer('I{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="A")
|
|
808
|
+
self._calc_channels["pmu"]["current"]["phi"] = DataChannelBuffer('I{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
|
|
809
|
+
|
|
728
810
|
# Create Power Channels
|
|
729
811
|
self._calc_channels["one_period"]["power"]['p_avg'] = DataChannelBuffer('P{:s}_1p'.format(self.name), agg_type='mean', unit="W")
|
|
730
812
|
self._calc_channels["multi_period"]["power"]['p_avg'] = DataChannelBuffer('P{:s}'.format(self.name), agg_type='mean', unit="W")
|
|
@@ -40,8 +40,7 @@ class ZeroCrossDetector:
|
|
|
40
40
|
self.samplerate = samplerate
|
|
41
41
|
|
|
42
42
|
# Design a Butterworth low-pass filter
|
|
43
|
-
|
|
44
|
-
self._filter_coeff = signal.iirfilter(2, normal_cutoff, btype='lowpass', ftype='butter')
|
|
43
|
+
self._filter_coeff = signal.iirfilter(2, self.f_cutoff, btype='lowpass', ftype='butter', fs=self.samplerate)
|
|
45
44
|
self._filter_zi = np.zeros(len(self._filter_coeff[0]) - 1)
|
|
46
45
|
|
|
47
46
|
self._last_filtered_sample = 0
|
|
@@ -51,9 +50,8 @@ class ZeroCrossDetector:
|
|
|
51
50
|
self._last_zc_n_val = None
|
|
52
51
|
|
|
53
52
|
# Calculate the filter delay in samples
|
|
54
|
-
w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1],
|
|
55
|
-
|
|
56
|
-
self.filter_delay_samples = np.angle(h)[0] / (2 * np.pi) * self.samplerate / self.f_cutoff
|
|
53
|
+
w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1], worN=[self.f_cutoff], fs=self.samplerate)
|
|
54
|
+
self.filter_delay_samples = np.angle(h)[0] / (2 * np.pi) * self.samplerate / self.f_cutoff - 1 # due to adding sample in front for continuity
|
|
57
55
|
self.filtered_data = []
|
|
58
56
|
|
|
59
57
|
def process(self, data: np.ndarray)-> list:
|
|
@@ -527,7 +527,7 @@ class TestPowerSystemNperSync(unittest.TestCase):
|
|
|
527
527
|
self.power_system.process()
|
|
528
528
|
|
|
529
529
|
u_rms, sidx = self.power_system.output_channels["U1_rms"].read_data_by_acq_sidx(0, u_values.size)
|
|
530
|
-
self.assertAlmostEqual(sidx[5*5],5.
|
|
530
|
+
self.assertAlmostEqual(sidx[5*5],5.22*self.power_system._samplerate, places=-1)
|
|
531
531
|
|
|
532
532
|
# def test_fractional_freq(self):
|
|
533
533
|
# abs_ts_start = datetime.datetime(2024,1,1,0,0,5, tzinfo=datetime.UTC).timestamp()
|
|
@@ -570,7 +570,7 @@ class TestPowerSystemFluctuation(unittest.TestCase):
|
|
|
570
570
|
self.time_channel.put_data((t[blk_idx*blocksize:(blk_idx+1)*blocksize]+abs_ts_start)*1e6)
|
|
571
571
|
self.power_system.process()
|
|
572
572
|
self.assertAlmostEqual(self.power_system.output_channels["U1_pst"].last_sample_value, 0, places=1)
|
|
573
|
-
self.assertEqual(self.power_system.output_channels["U1_pst"].last_sample_acq_sidx,
|
|
573
|
+
self.assertEqual(self.power_system.output_channels["U1_pst"].last_sample_acq_sidx, int(self.power_system._samplerate*(61+0.02)))
|
|
574
574
|
|
|
575
575
|
def test_steady_state_600s(self):
|
|
576
576
|
self.power_system.enable_fluctuation_calculation(nominal_voltage=230, pst_interval_sec=600)
|
|
@@ -586,6 +586,37 @@ def test_steady_state_600s(self):
|
|
|
586
586
|
self.assertAlmostEqual(self.power_system.output_channels["U1_pst"].last_sample_value, 0, places=1)
|
|
587
587
|
self.assertEqual(self.power_system.output_channels["U1_pst"].last_sample_acq_sidx, np.round(self.power_system._samplerate*621))
|
|
588
588
|
|
|
589
|
+
class TestPowerSystemPMU(unittest.TestCase):
|
|
590
|
+
def setUp(self):
|
|
591
|
+
self.u_channel = AcqBuffer(name="U1")
|
|
592
|
+
self.time_channel = AcqBuffer(dtype=np.int64)
|
|
593
|
+
|
|
594
|
+
# Create PowerSystem instance
|
|
595
|
+
self.power_system = PowerSystem(
|
|
596
|
+
zcd_channel=self.u_channel,
|
|
597
|
+
input_samplerate=5555.555,
|
|
598
|
+
zcd_threshold=1
|
|
599
|
+
)
|
|
600
|
+
# Add Phase
|
|
601
|
+
self.power_system.add_phase(u_channel=self.u_channel)
|
|
602
|
+
self.power_system.enable_nper_abs_time_sync(self.time_channel)
|
|
603
|
+
self.power_system.enable_one_period_fundamental(1)
|
|
604
|
+
self.power_system.enable_pmu_calculation()
|
|
605
|
+
|
|
606
|
+
def test_simple(self):
|
|
607
|
+
abs_ts_start = datetime.datetime(2025,1,1,0,0,0, tzinfo=datetime.UTC).timestamp()
|
|
608
|
+
t = np.arange(0, 1, 1/self.power_system._samplerate)
|
|
609
|
+
u_values = 230*np.sqrt(2)*np.sin(2*np.pi*50*t)
|
|
610
|
+
|
|
611
|
+
blocksize = 1000
|
|
612
|
+
for blk_idx in range(t.size // blocksize):
|
|
613
|
+
self.u_channel.put_data(u_values[blk_idx*blocksize:(blk_idx+1)*blocksize])
|
|
614
|
+
self.time_channel.put_data((t[blk_idx*blocksize:(blk_idx+1)*blocksize]+abs_ts_start)*1e6)
|
|
615
|
+
self.power_system.process()
|
|
616
|
+
self.assertAlmostEqual(self.power_system.output_channels["U1_pmu_rms"].last_sample_value, 230, places=0)
|
|
617
|
+
self.assertAlmostEqual(self.power_system.output_channels["U1_pmu_phi"].last_sample_value, 0, places=0)
|
|
618
|
+
#self.assertEqual(self.power_system.output_channels["U1_pmu_rms"].last_sample_acq_sidx, np.round(self.power_system._samplerate*(61+0.02)))
|
|
619
|
+
|
|
589
620
|
|
|
590
621
|
if __name__ == "__main__":
|
|
591
622
|
unittest.main()
|
|
@@ -44,7 +44,7 @@ class TestZeroCrossDetector(unittest.TestCase):
|
|
|
44
44
|
sine_wave = self.generate_sine_wave(freq=freq, duration=duration)
|
|
45
45
|
|
|
46
46
|
zero_crossings = self.detector.process(sine_wave)
|
|
47
|
-
self.assertAlmostEqual(self.detector.filter_delay_samples, -
|
|
47
|
+
self.assertAlmostEqual(self.detector.filter_delay_samples, -6)
|
|
48
48
|
|
|
49
49
|
# Expected zero-crossings: 5 Hz * 1 positive crossing per cycle * 1 second
|
|
50
50
|
expected_crossings = np.arange(start=1, stop=6)*self.samplerate/freq
|
|
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
|