pqopen-lib 0.8.4__py3-none-any.whl → 0.9.0__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/powerquality.py CHANGED
@@ -63,6 +63,19 @@ def calc_thd(harm_rms: np.ndarray, max_harmonic: int = 40, min_harmonic: int = 2
63
63
  thd = np.sqrt(np.sum(np.power(harm_rms[min_harmonic:max_harmonic+1]/fund_rms, 2)))*100
64
64
  return thd
65
65
 
66
+ def calc_hf_1khz_band(fft_data: np.ndarray, samplerate: float):
67
+ fft_data_abs = np.abs(fft_data)
68
+ freq_resolution = 0.5*samplerate / fft_data_abs.shape[0]
69
+ freq_steps = np.arange(-500, (samplerate/2) + 1500 - (samplerate/2) % 1000, 1000)
70
+ freq_steps[0] = 0
71
+ freq_steps[-1] = samplerate/2
72
+ freq_band_idx = (freq_steps / freq_resolution).astype(int)
73
+ hf_1khz_band_rms = []
74
+ for band_idx in range(freq_band_idx.shape[0] - 1):
75
+ hf_1khz_band_rms.append(np.sqrt(np.sum(fft_data_abs[freq_band_idx[band_idx]:freq_band_idx[band_idx+1]])))
76
+
77
+ return np.array(hf_1khz_band_rms)
78
+
66
79
  def resample_and_fft(data: np.ndarray, resample_size: int = None) -> np.ndarray:
67
80
  """
68
81
  Resample the input data to a specified size and compute its FFT.
pqopen/powersystem.py CHANGED
@@ -24,6 +24,7 @@ from typing import List, Dict
24
24
  import logging
25
25
  from pathlib import Path
26
26
  import json
27
+ from scipy.signal import get_window
27
28
 
28
29
  from daqopen.channelbuffer import AcqBuffer, DataChannelBuffer
29
30
  from pqopen.zcd import ZeroCrossDetector
@@ -91,7 +92,8 @@ class PowerSystem(object):
91
92
  "energy_channels": {},
92
93
  "one_period_fundamental": 0,
93
94
  "rms_trapz_rule": False,
94
- "pmu_calculation": False}
95
+ "pmu_calculation": False,
96
+ "hf_1khz_band_calculation": False}
95
97
  self._prepare_calc_channels()
96
98
  self.output_channels: Dict[str, DataChannelBuffer] = {}
97
99
  self._last_processed_sidx = 0
@@ -246,6 +248,15 @@ class PowerSystem(object):
246
248
  self._pmu_last_processed_ts_us = 0
247
249
  self._pmu_time_increment_us = int(1_000_000 / self.nominal_frequency)
248
250
 
251
+ def enable_hf_1khz_band_calculation(self):
252
+ """
253
+ Enables the calculation of wide band analysis with 1 kHz bands (non standard)
254
+ """
255
+ num_bands = int(self._samplerate*0.5 / 1000) + 1
256
+ self._features["hf_1khz_band_calculation"] = num_bands
257
+ self._hf_1khz_window = get_window("hann", self._harm_fft_resample_size)
258
+ self._channel_update_needed = True
259
+
249
260
  def _resync_nper_abs_time(self, zc_idx: int):
250
261
  if not self._features["nper_abs_time_sync"]:
251
262
  return None
@@ -305,11 +316,11 @@ class PowerSystem(object):
305
316
  if self._features["harmonics"]:
306
317
  for phase in self._phases:
307
318
  if phase._u_channel.freq_response:
308
- phase._u_fft_corr_array = create_fft_corr_array(self._harm_fft_resample_size,
319
+ phase._u_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
309
320
  self._samplerate/2,
310
321
  phase._u_channel.freq_response)
311
322
  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,
323
+ phase._i_fft_corr_array = create_fft_corr_array(int((self._samplerate / self.nominal_frequency) * self.nper / 2),
313
324
  self._samplerate/2,
314
325
  phase._i_channel.freq_response)
315
326
 
@@ -482,7 +493,8 @@ class PowerSystem(object):
482
493
  data_fft_U = pq.resample_and_fft(u_values, self._harm_fft_resample_size)
483
494
  if phase._u_fft_corr_array is not None:
484
495
  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)]
496
+ corr_freq_idx = np.linspace(0, self._samplerate*0.5*resample_factor*self.nper/self.nominal_frequency, self._harm_fft_resample_size//2+1).astype(np.int32)
497
+ data_fft_U *= phase._u_fft_corr_array[corr_freq_idx]
486
498
  u_h_mag, u_h_phi = pq.calc_harmonics(data_fft_U, self.nper, self._features["harmonics"])
487
499
  u_ih_mag = pq.calc_interharmonics(data_fft_U, self.nper, self._features["harmonics"])
488
500
  if phase._number == 1: # use phase 1 angle as reference
@@ -511,6 +523,17 @@ class PowerSystem(object):
511
523
  output_channel.put_data_single(stop_sidx, u_rms)
512
524
  if phys_type == "over":
513
525
  output_channel.put_data_single(stop_sidx, u_rms)
526
+ if phys_type == "hf_1khz_rms":
527
+ u_values_windowed = u_values[:self._harm_fft_resample_size]*self._hf_1khz_window
528
+ u_real_fft = np.fft.rfft(u_values_windowed)
529
+ #u_real_fft = np.fft.rfft(u_values_windowed)
530
+ if phase._u_fft_corr_array is not None:
531
+ corr_freq_idx = np.linspace(0, self._samplerate*0.5, u_real_fft.shape[0]).astype(np.int32)
532
+ u_real_fft *= phase._u_fft_corr_array[corr_freq_idx]
533
+ u_Pxx = (np.abs(u_real_fft) ** 2) / np.sum(self._hf_1khz_window ** 2)
534
+ u_Pxx /= self._harm_fft_resample_size * 0.5
535
+ u_hf_1khz = pq.calc_hf_1khz_band(u_Pxx, self._samplerate)
536
+ output_channel.put_data_single(stop_sidx, u_hf_1khz)
514
537
 
515
538
  if phase._i_channel:
516
539
  i_values = phase._i_channel.read_data_by_index(start_sidx, stop_sidx)
@@ -676,6 +699,14 @@ class PowerSystem(object):
676
699
  self._last_known_freq = self.nominal_frequency
677
700
  self._calculation_mode = "FALLBACK"
678
701
  elif self._calculation_mode == "FALLBACK":
702
+ remaining_virtual_zc = []
703
+ last_rel_zc = self._zero_crossings[-1] - start_acq_sidx
704
+ zc_gap = zero_crossings[0] - last_rel_zc
705
+ num_zc_to_fill = int(zc_gap / (self._samplerate/self._last_known_freq)) + 1
706
+ virtual_zc_interval = int(zc_gap / num_zc_to_fill)
707
+ remaining_virtual_zc = [last_rel_zc + i*virtual_zc_interval for i in range(1,num_zc_to_fill)]
708
+ logger.debug("Finishing Fallback: Added virtual zero crossing: idx=" + ",".join([f"{virtual_zc:.1f}" for virtual_zc in remaining_virtual_zc]))
709
+ zero_crossings = remaining_virtual_zc + zero_crossings
679
710
  self._calculation_mode = "NORMAL"
680
711
  else:
681
712
  self._calculation_mode = "NORMAL"
@@ -698,6 +729,21 @@ class PowerSystem(object):
698
729
  output_values[ch_name] = ch_data
699
730
  return output_values
700
731
 
732
+ def get_channel_info(self) -> dict:
733
+ channel_info = {}
734
+ for phase in self._phases:
735
+ for calc_interval, interval_group in phase._calc_channels.items():
736
+ for derived_type, phys_group in interval_group.items():
737
+ for phys_type, channel in phys_group.items():
738
+ channel_info[channel.name] = {
739
+ "unit": channel.unit,
740
+ "phys_type": phys_type,
741
+ "derived_type": derived_type,
742
+ "calc_interval": calc_interval,
743
+ "phase": phase.name
744
+ }
745
+ return channel_info
746
+
701
747
  def __del__(self):
702
748
  if self._features["energy_channels"]:
703
749
  w_pos_value = float(self._calc_channels["multi_period"]["energy"]["w_pos"].last_sample_value)
@@ -787,6 +833,10 @@ class PowerPhase(object):
787
833
  self._calc_channels["pmu"]["voltage"]["rms"] = DataChannelBuffer('U{:s}_pmu_rms'.format(self.name), agg_type='rms', unit="V")
788
834
  self._calc_channels["pmu"]["voltage"]["phi"] = DataChannelBuffer('U{:s}_pmu_phi'.format(self.name), agg_type='phi', unit="°")
789
835
 
836
+ if "hf_1khz_band_calculation" in features and features["hf_1khz_band_calculation"]:
837
+ num_bands = features["hf_1khz_band_calculation"]
838
+ self._calc_channels["multi_period"]["voltage"]["hf_1khz_rms"] = DataChannelBuffer('U{:s}_HF_1kHz_rms'.format(self.name), sample_dimension=num_bands, agg_type='rms', unit="V")
839
+
790
840
  # Create Current Channels
791
841
  if self._i_channel:
792
842
  self._calc_channels["one_period"]["current"] = {}
@@ -57,11 +57,13 @@ import numpy as np
57
57
  from pqopen.helper import floor_timestamp, JsonDecimalLimiter
58
58
  from pqopen.eventdetector import Event
59
59
  from daqopen.channelbuffer import DataChannelBuffer, AcqBuffer
60
+ from daqopen.daqinfo import DaqInfo
60
61
  from pathlib import Path
61
62
  from typing import List, Dict
62
63
  import logging
63
64
  import json
64
65
  import gzip
66
+ import time
65
67
  import paho.mqtt.client as mqtt
66
68
 
67
69
 
@@ -91,7 +93,7 @@ class StorageEndpoint(object):
91
93
  """
92
94
  Writes aggregated data to the storage endpoint.
93
95
 
94
- Args:
96
+ Parameters:
95
97
  data: The aggregated data to store.
96
98
  timestamp_us: The timestamp in microseconds for the aggregated data.
97
99
  interval_seconds: The aggregation interval in seconds.
@@ -101,6 +103,17 @@ class StorageEndpoint(object):
101
103
  def write_event(self, event: Event):
102
104
  pass
103
105
 
106
+ def write_measurement_config(self, m_config: dict, daq_info: DaqInfo, channe_info: dict, **kwargs):
107
+ """
108
+ Writes measurement config to the storage endpoint.
109
+
110
+ Parameters:
111
+ m_config: The measurement config to store.
112
+ daq_info: The daq info to store.
113
+ channel_info: The channel's specific infos
114
+ """
115
+ pass
116
+
104
117
  class StoragePlan(object):
105
118
  """Defines a plan for storing data with specified intervals and channels."""
106
119
 
@@ -190,6 +203,9 @@ class StoragePlan(object):
190
203
  if self._store_events_enabled:
191
204
  self.storage_endpoint.write_event(event, **self._additional_config)
192
205
 
206
+ def store_measurement_config(self, m_config: dict, daq_info: DaqInfo, channel_info: dict):
207
+ self.storage_endpoint.write_measurement_config(m_config, daq_info, channel_info, **self._additional_config)
208
+
193
209
  class StorageController(object):
194
210
  """Manages multiple storage plans and processes data for storage."""
195
211
 
@@ -304,7 +320,10 @@ class StorageController(object):
304
320
  available_channels: dict,
305
321
  measurement_id: str,
306
322
  device_id: str,
307
- start_timestamp_us: int):
323
+ start_timestamp_us: int,
324
+ m_config: dict | None = None,
325
+ daq_info: DaqInfo | None = None,
326
+ channel_info: dict| None = None):
308
327
  """
309
328
  Setup endpoints and storage plans from config
310
329
 
@@ -319,7 +338,7 @@ class StorageController(object):
319
338
  Raises:
320
339
  NotImplementedError: If a not implemented endpoint will be configured
321
340
  """
322
- self._configured_eps = {}
341
+ self._configured_eps: dict[str, StorageEndpoint] = {}
323
342
  for ep_type, ep_config in endpoints.items():
324
343
  if ep_type == "csv":
325
344
  csv_storage_endpoint = CsvStorageEndpoint("csv", measurement_id, ep_config.get("data_dir", "/tmp/"))
@@ -375,8 +394,11 @@ class StorageController(object):
375
394
  else:
376
395
  logger.warning(f"Channel {channel} not available for storing")
377
396
  self.add_storage_plan(storage_plan)
378
-
379
-
397
+ # Initially store the measurement config if configured
398
+ if sp_config.get("store_config", False) and m_config and daq_info and channel_info:
399
+ storage_plan.store_measurement_config(m_config=m_config,
400
+ daq_info=daq_info,
401
+ channel_info=channel_info)
380
402
 
381
403
  class TestStorageEndpoint(StorageEndpoint):
382
404
  """A implementation of StorageEndpoint for testing purposes."""
@@ -499,6 +521,27 @@ class MqttStorageEndpoint(StorageEndpoint):
499
521
  self._client.publish(topic_prefix + f"/{self._device_id:s}/event/json",
500
522
  json_item.encode('utf-8'), qos=2)
501
523
 
524
+ def write_measurement_config(self, m_config: dict, daq_info: DaqInfo, channel_info: dict, **kwargs):
525
+ """
526
+ Write measurement config
527
+ """
528
+ m_config_object = {
529
+ "type": "m_config",
530
+ "measurement_uuid": self.measurement_id,
531
+ "timestamp": time.time(),
532
+ "m_config": m_config,
533
+ "daq_info": daq_info.to_dict(),
534
+ "channel_info": channel_info
535
+ }
536
+ json_item = json.dumps(m_config_object)
537
+ topic_prefix = kwargs.get("mqtt_topic_prefix", self._topic_prefix)
538
+ if self._compression:
539
+ self._client.publish(topic_prefix + f"/{self._device_id:s}/m_config/gjson",
540
+ gzip.compress(json_item.encode('utf-8')), qos=2)
541
+ else:
542
+ self._client.publish(topic_prefix + f"/{self._device_id:s}/m_config/json",
543
+ json_item.encode('utf-8'), qos=2)
544
+
502
545
  class HomeAssistantStorageEndpoint(StorageEndpoint):
503
546
  """Represents a MQTT endpoint (MQTT) for HomeAssistant."""
504
547
  def __init__(self,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pqopen-lib
3
- Version: 0.8.4
3
+ Version: 0.9.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
@@ -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=Cg7rwXOo3S63idE4XmrlC8CXHgGHbqI6-Urmhlg6ySc,16558
6
+ pqopen/powersystem.py,sha256=1Fi5SbS4PsUNz3hANjJBzTNtuM5bvQ6Oivae80K6RXI,52415
7
+ pqopen/storagecontroller.py,sha256=8JkeRX3HDWGFR40wO1ejxctQp53eMgEtsJMbOl7ea1Y,33468
8
+ pqopen/zcd.py,sha256=olJZsHRd1CjU65vysc2emR5wFGMkayIWdUEDNE8jfIc,6253
9
+ pqopen_lib-0.9.0.dist-info/METADATA,sha256=-UoUljXNQMLQUuR0GE_bBZpBY_sdBl5k5G1ZZ8GCwOw,4787
10
+ pqopen_lib-0.9.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
+ pqopen_lib-0.9.0.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
+ pqopen_lib-0.9.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
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=BDB-Xf-xe3UYVBUyX2gjKXnWpeZWTQTaLDSGCvtmGZ4,49256
7
- pqopen/storagecontroller.py,sha256=AhVaPgIh4CBKKMJm9Ja0dOjVd4dLppWprxeeg3vrmAk,31266
8
- pqopen/zcd.py,sha256=olJZsHRd1CjU65vysc2emR5wFGMkayIWdUEDNE8jfIc,6253
9
- pqopen_lib-0.8.4.dist-info/METADATA,sha256=hC1fidnpdel5XKb8T5zDJ6H3zidLNtTTJuhnYnKAVQE,4787
10
- pqopen_lib-0.8.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
11
- pqopen_lib-0.8.4.dist-info/licenses/LICENSE,sha256=yhYwu9dioytbAvNQa0UBwaBVcALqiOoBViEs4HLW6aU,1064
12
- pqopen_lib-0.8.4.dist-info/RECORD,,