pqopen-lib 0.7.9__py3-none-any.whl → 0.8.1__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.
pqopen/powersystem.py CHANGED
@@ -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
- # Use sample-discrete frequency, not the exact one for full cycle
403
- self._fund_freq_list = np.roll(self._fund_freq_list,-1)
404
- self._fund_freq_list[-1] = self._samplerate/len(u_values)
405
- mean_freq = self._fund_freq_list[self._fund_freq_list>0].mean()
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.
@@ -601,13 +672,8 @@ class PowerSystem(object):
601
672
  self._calculation_mode = "FALLBACK"
602
673
  else:
603
674
  self._calculation_mode = "NORMAL"
604
-
605
- # Remove Non monotonic rising zero crossings
606
- filtered_zero_crossings = []
607
- for idx,zc in enumerate(zero_crossings):
608
- if (int(zc)+start_acq_sidx) >= self._zero_crossings[-1] or np.isnan(self._zero_crossings[-1]):
609
- filtered_zero_crossings.append(zc)
610
- return filtered_zero_crossings
675
+
676
+ return zero_crossings
611
677
 
612
678
  def get_aggregated_data(self, start_acq_sidx: int, stop_acq_sidx: int) -> dict:
613
679
  """
@@ -671,12 +737,13 @@ class PowerPhase(object):
671
737
  Parameters:
672
738
  features: Dict of features
673
739
  """
674
- self._calc_channels = {"half_period": {}, "one_period": {}, "one_period_ovlp": {}, "multi_period": {}}
740
+ self._calc_channels = {"half_period": {}, "one_period": {}, "one_period_ovlp": {}, "multi_period": {}, "pmu": {}}
675
741
  # Create Voltage Channels
676
742
  self._calc_channels["half_period"]["voltage"] = {}
677
743
  self._calc_channels["one_period"]["voltage"] = {}
678
744
  self._calc_channels["one_period_ovlp"]["voltage"] = {}
679
745
  self._calc_channels["multi_period"]["voltage"] = {}
746
+ self._calc_channels["pmu"]["voltage"] = {}
680
747
  self._calc_channels["half_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_hp_rms'.format(self.name), agg_type='rms', unit="V")
681
748
  self._calc_channels["one_period"]["voltage"]["trms"] = DataChannelBuffer('U{:s}_1p_rms'.format(self.name), agg_type='rms', unit="V")
682
749
  self._calc_channels["one_period"]["voltage"]["slope"] = DataChannelBuffer('U{:s}_1p_slope'.format(self.name), agg_type='max', unit="V/s")
@@ -706,6 +773,11 @@ class PowerPhase(object):
706
773
 
707
774
  if "one_period_fundamental" in features and features["one_period_fundamental"] > 0:
708
775
  self._calc_channels["one_period"]["voltage"]["fund_rms"] = DataChannelBuffer('U{:s}_1p_H1_rms'.format(self.name), agg_type='rms', unit="V")
776
+ self._calc_channels["one_period"]["voltage"]["fund_phi"] = DataChannelBuffer('U{:s}_1p_H1_phi'.format(self.name), agg_type='phi', unit="°")
777
+
778
+ if "pmu_calculation" in features and features["pmu_calculation"]:
779
+ self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
780
+ self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
709
781
 
710
782
  # Create Current Channels
711
783
  if self._i_channel:
@@ -713,6 +785,7 @@ class PowerPhase(object):
713
785
  self._calc_channels["multi_period"]["current"] = {}
714
786
  self._calc_channels["one_period"]["current"]["trms"] = DataChannelBuffer('I{:s}_1p_rms'.format(self.name), agg_type='rms', unit="A")
715
787
  self._calc_channels["multi_period"]["current"]["trms"] = DataChannelBuffer('I{:s}_rms'.format(self.name), agg_type='rms', unit="A")
788
+ self._calc_channels["pmu"]["current"] = {}
716
789
  self._calc_channels["one_period"]["power"] = {}
717
790
  self._calc_channels["multi_period"]["power"] = {}
718
791
 
@@ -725,6 +798,10 @@ class PowerPhase(object):
725
798
  self._calc_channels["multi_period"]["power"]['p_fund_mag'] = DataChannelBuffer('P{:s}_H1'.format(self.name), agg_type='mean', unit="W")
726
799
  self._calc_channels["multi_period"]["power"]['q_fund_mag'] = DataChannelBuffer('Q{:s}_H1'.format(self.name), agg_type='mean', unit="var")
727
800
 
801
+ if "pmu_calculation" in features and features["pmu_calculation"]:
802
+ self._calc_channels["pmu"]["current"]["rms"] = DataChannelBuffer('I{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="A")
803
+ self._calc_channels["pmu"]["current"]["phi"] = DataChannelBuffer('I{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
804
+
728
805
  # Create Power Channels
729
806
  self._calc_channels["one_period"]["power"]['p_avg'] = DataChannelBuffer('P{:s}_1p'.format(self.name), agg_type='mean', unit="W")
730
807
  self._calc_channels["multi_period"]["power"]['p_avg'] = DataChannelBuffer('P{:s}'.format(self.name), agg_type='mean', unit="W")
pqopen/zcd.py CHANGED
@@ -40,8 +40,7 @@ class ZeroCrossDetector:
40
40
  self.samplerate = samplerate
41
41
 
42
42
  # Design a Butterworth low-pass filter
43
- normal_cutoff = self.f_cutoff / (0.5 * self.samplerate)
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
@@ -49,11 +48,11 @@ class ZeroCrossDetector:
49
48
  self._last_zc_p = None
50
49
  self._last_zc_n = None
51
50
  self._last_zc_n_val = None
51
+ self._last_zc = None
52
52
 
53
53
  # Calculate the filter delay in samples
54
- w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1],
55
- [2 * np.pi * self.f_cutoff / self.samplerate])
56
- self.filter_delay_samples = np.angle(h)[0] / (2 * np.pi) * self.samplerate / self.f_cutoff
54
+ w, h = signal.freqz(self._filter_coeff[0], self._filter_coeff[1], worN=[self.f_cutoff], fs=self.samplerate)
55
+ 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
56
  self.filtered_data = []
58
57
 
59
58
  def process(self, data: np.ndarray)-> list:
@@ -113,9 +112,15 @@ class ZeroCrossDetector:
113
112
  k = (y2 - y1) / (x2 - x1)
114
113
  d = y1 - k * x1
115
114
  real_zc = -d/k
116
- #real_zc = (threshold_p_cross[p_idx] - threshold_n_cross[n_idx] + 1) / 2 + threshold_n_cross[n_idx]
117
- zero_crossings.append(real_zc + self.filter_delay_samples)
118
- 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
119
124
 
120
125
  # Update the last negative threshold-crossing for the next block
121
126
  if last_used_p_idx < len(threshold_p_cross) and len(threshold_n_cross) > 0:
@@ -128,4 +133,8 @@ class ZeroCrossDetector:
128
133
  else:
129
134
  self._last_zc_n = None
130
135
 
136
+ # Update Last Valid ZC Index
137
+ if self._last_zc:
138
+ self._last_zc -= len(data)
139
+
131
140
  return zero_crossings
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pqopen-lib
3
- Version: 0.7.9
3
+ Version: 0.8.1
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
@@ -3,10 +3,10 @@ pqopen/eventdetector.py,sha256=NKZU7GbeorZdkYu3ET4lhMaeynw70GhIGO2p1xH4aTA,11962
3
3
  pqopen/goertzel.py,sha256=JhehuEj-xNnwQEh4Ti8dXiGvfDmxbdEoaxIsFswZNcM,2008
4
4
  pqopen/helper.py,sha256=bM_wDck5OfeEy96U_2FjCwZuLZRPYyKVeiYD3-vzP6M,1761
5
5
  pqopen/powerquality.py,sha256=Qwyaj7BQqQPRivei140mv-Leh6u9uIQzViKLOY7bHyw,17877
6
- pqopen/powersystem.py,sha256=JC7GI_1F7T3RlFHtnbw3UxGGVKV5Q7TlpiR0vzPbWYk,43610
6
+ pqopen/powersystem.py,sha256=5gZcZem_mNRqezjC67qlCePht6L8CraPtQNsjPgP7C4,48581
7
7
  pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
- pqopen/zcd.py,sha256=ueq-a59gOFFeIUzI61vO4G5sSuBDAm30Y_FfGgwDPV0,5704
9
- pqopen_lib-0.7.9.dist-info/METADATA,sha256=IUKnOMFcjwyDCOEe5hBS7ZtrLs19me75EjLWCZZkwp4,4780
10
- pqopen_lib-0.7.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- pqopen_lib-0.7.9.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
- pqopen_lib-0.7.9.dist-info/RECORD,,
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,,