Detect and classify power-quality events in voltage recordings, following IEEE Std 1159.
Point it at a CSV of voltage measurements and it tells you what went wrong on the line: every sag, swell and interruption, how deep, how long, and what the standard calls it.
$ pqa analyse recording.csv
Recording : recording.csv
Duration : 2.000 s at 10,000 Hz
Frequency : 50.001 Hz
THD : 4.02 %
2 event(s) detected:
# CATEGORY START DURATION EXTREME
------------------------------------------------------------
1 instantaneous sag 0.300s 0.250s 0.650pu
2 instantaneous swell 1.100s 0.080s 1.250pu
Voltage sags are the most common power-quality disturbance and the most expensive: a dip to 70% lasting a tenth of a second is invisible to a person but will drop out a variable-speed drive and stop a production line. On weak grids they are constant.
Utilities have instruments that log raw waveforms. Turning those logs into "you had eleven momentary sags this week, three of them below 0.5 pu" is the part that usually happens by hand in a spreadsheet. This does it in one command.
git clone https://github.com/IjootAnthony/pq-analyser
cd pq-analyser
pip install -e ".[dev,plot]"Generate a synthetic recording with known events, then analyse it:
pqa demo --output demo.csv
pqa analyse demo.csvAnalyse your own recording. If the file is in volts rather than per-unit, give the nominal voltage:
pqa analyse field_data.csv --nominal-voltage 230 --format json --output report.jsonOr use it as a library:
from pqa import load_csv, analyse
result = analyse(load_csv("recording.csv", nominal_voltage=230))
for event in result.events:
print(event.kind, event.duration_s, event.extreme_pu)| Stage | Module | What happens |
|---|---|---|
| Load | io.py |
Read CSV, detect columns, check sampling is uniform, convert to per-unit |
| Frequency | signal.py |
Estimate the fundamental from the FFT peak with parabolic interpolation |
| Envelope | signal.py |
Sliding half-cycle RMS, one sample at a time |
| Detect | events.py |
Find regions outside the normal band, with hysteresis |
| Classify | classify.py |
Map magnitude and duration onto IEEE 1159 categories |
| Report | report.py |
Text, JSON or CSV |
Two decisions worth explaining:
Everything is per-unit. 1.0 pu is nominal. The same thresholds then apply to a 230 V outlet and an 11 kV feeder without changing a line of code.
The frequency is measured, not assumed. The RMS window is sized from the fundamental. On a grid running at 49.2 Hz, a window sized for 50 Hz leaves a ripple on the envelope that looks like a train of small events. Measuring first removes them.
pytestEvery test runs against a synthetic signal whose correct answer is known exactly. When a test injects a sag to 0.70 pu lasting 0.300 s, it asserts the detector reports 0.70 pu and 0.300 s — not that the result is merely non-empty. That is only possible because the signal was generated rather than measured, which is why synth.py exists and why it is tested itself.
Deliberately not included: real-time streaming, three-phase unbalance, flicker (IEC 61000-4-15), transient classification, GUI.
MIT