2022-06-01 18:15:29 +02:00
|
|
|
/* PMU hook.
|
|
|
|
*
|
|
|
|
* Author: Manuel Pitz <manuel.pitz@eonerc.rwth-aachen.de>
|
|
|
|
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
|
2022-07-04 18:20:03 +02:00
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
2022-06-01 18:15:29 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include <villas/hooks/pmu.hpp>
|
|
|
|
#include <villas/timing.hpp>
|
|
|
|
|
|
|
|
namespace villas {
|
|
|
|
namespace node {
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
PmuHook::PmuHook(Path *p, Node *n, int fl, int prio, bool en)
|
|
|
|
: MultiSignalHook(p, n, fl, prio, en), windows(), windowsTs(),
|
|
|
|
timeAlignType(TimeAlign::CENTER), windowType(WindowType::NONE),
|
|
|
|
sampleRate(1), phasorRate(1.0), nominalFreq(1.0), numberPlc(1.),
|
|
|
|
windowSize(1), channelNameEnable(true), angleUnitFactor(1.0),
|
|
|
|
lastSequence(0), nextRun({0}), init(false), initSampleCount(0),
|
|
|
|
phaseOffset(0.0), amplitudeOffset(0.0), frequencyOffset(0.0),
|
|
|
|
rocofOffset(0.0) {}
|
|
|
|
|
|
|
|
void PmuHook::prepare() {
|
|
|
|
MultiSignalHook::prepare();
|
|
|
|
|
|
|
|
signals->clear();
|
|
|
|
for (unsigned i = 0; i < signalIndices.size(); i++) {
|
|
|
|
// Add signals
|
|
|
|
auto freqSig =
|
|
|
|
std::make_shared<Signal>("frequency", "Hz", SignalType::FLOAT);
|
|
|
|
auto amplSig =
|
|
|
|
std::make_shared<Signal>("amplitude", "V", SignalType::FLOAT);
|
|
|
|
auto phaseSig = std::make_shared<Signal>(
|
|
|
|
"phase", (angleUnitFactor) ? "rad" : "deg",
|
|
|
|
SignalType::FLOAT); //angleUnitFactor==1 means rad
|
|
|
|
auto rocofSig =
|
|
|
|
std::make_shared<Signal>("rocof", "Hz/s", SignalType::FLOAT);
|
|
|
|
|
|
|
|
if (!freqSig || !amplSig || !phaseSig || !rocofSig)
|
|
|
|
throw RuntimeError("Failed to create new signals");
|
|
|
|
|
|
|
|
if (channelNameEnable) {
|
|
|
|
auto suffix = fmt::format("_{}", signalNames[i]);
|
|
|
|
|
|
|
|
freqSig->name += suffix;
|
|
|
|
amplSig->name += suffix;
|
|
|
|
phaseSig->name += suffix;
|
|
|
|
rocofSig->name += suffix;
|
|
|
|
}
|
|
|
|
|
|
|
|
signals->push_back(freqSig);
|
|
|
|
signals->push_back(amplSig);
|
|
|
|
signals->push_back(phaseSig);
|
|
|
|
signals->push_back(rocofSig);
|
|
|
|
|
|
|
|
lastPhasors.push_back({0., 0., 0., 0.});
|
|
|
|
}
|
|
|
|
|
|
|
|
windowSize = ceil(sampleRate * numberPlc / nominalFreq);
|
|
|
|
|
|
|
|
for (unsigned i = 0; i < signalIndices.size(); i++) {
|
|
|
|
if (windowType == WindowType::NONE)
|
|
|
|
windows.push_back(new dsp::RectangularWindow<double>(windowSize, 0.0));
|
|
|
|
else if (windowType == WindowType::FLATTOP)
|
|
|
|
windows.push_back(new dsp::FlattopWindow<double>(windowSize, 0.0));
|
|
|
|
else if (windowType == WindowType::HAMMING)
|
|
|
|
windows.push_back(new dsp::HammingWindow<double>(windowSize, 0.0));
|
|
|
|
else if (windowType == WindowType::HANN)
|
|
|
|
windows.push_back(new dsp::HannWindow<double>(windowSize, 0.0));
|
|
|
|
else if (windowType == WindowType::NUTTAL)
|
|
|
|
windows.push_back(new dsp::NuttallWindow<double>(windowSize, 0.0));
|
|
|
|
else if (windowType == WindowType::BLACKMAN)
|
|
|
|
windows.push_back(new dsp::BlackmanWindow<double>(windowSize, 0.0));
|
|
|
|
}
|
|
|
|
|
|
|
|
windowsTs = new dsp::Window<timespec>(windowSize, timespec{0});
|
2022-06-01 18:15:29 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
void PmuHook::parse(json_t *json) {
|
|
|
|
MultiSignalHook::parse(json);
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
const char *windowTypeC = nullptr;
|
|
|
|
const char *angleUnitC = nullptr;
|
|
|
|
const char *timeAlignC = nullptr;
|
|
|
|
|
|
|
|
json_error_t err;
|
|
|
|
|
|
|
|
assert(state != State::STARTED);
|
|
|
|
|
|
|
|
Hook::parse(json);
|
|
|
|
|
|
|
|
ret = json_unpack_ex(
|
|
|
|
json, &err, 0,
|
|
|
|
"{ s?: i, s?: F, s?: F, s?: F, s?: s, s?: s, s?: b, s?: s, s?: F, s?: F, "
|
|
|
|
"s?: F, s?: F}",
|
|
|
|
"sample_rate", &sampleRate, "dft_rate", &phasorRate, "nominal_freq",
|
|
|
|
&nominalFreq, "number_plc", &numberPlc, "window_type", &windowTypeC,
|
|
|
|
"angle_unit", &angleUnitC, "add_channel_name", &channelNameEnable,
|
|
|
|
"timestamp_align", &timeAlignC, "phase_offset", &phaseOffset,
|
|
|
|
"amplitude_offset", &litudeOffset, "frequency_offset",
|
|
|
|
&frequencyOffset, "rocof_offset", &rocofOffset);
|
|
|
|
|
|
|
|
if (ret)
|
|
|
|
throw ConfigError(json, err, "node-config-hook-pmu");
|
|
|
|
|
|
|
|
if (sampleRate <= 0)
|
|
|
|
throw ConfigError(json, "node-config-hook-pmu-sample_rate",
|
|
|
|
"Sample rate cannot be less than 0 tried to set {}",
|
|
|
|
sampleRate);
|
|
|
|
|
|
|
|
if (phasorRate <= 0)
|
|
|
|
throw ConfigError(json, "node-config-hook-pmu-phasor_rate",
|
|
|
|
"Phasor rate cannot be less than 0 tried to set {}",
|
|
|
|
phasorRate);
|
|
|
|
|
|
|
|
if (nominalFreq <= 0)
|
|
|
|
throw ConfigError(json, "node-config-hook-pmu-nominal_freq",
|
|
|
|
"Nominal frequency cannot be less than 0 tried to set {}",
|
|
|
|
nominalFreq);
|
|
|
|
|
|
|
|
if (numberPlc <= 0)
|
|
|
|
throw ConfigError(
|
|
|
|
json, "node-config-hook-pmu-number_plc",
|
|
|
|
"Number of power line cycles cannot be less than 0 tried to set {}",
|
|
|
|
numberPlc);
|
|
|
|
|
|
|
|
if (!windowTypeC)
|
|
|
|
logger->info("No Window type given, assume no windowing");
|
|
|
|
else if (strcmp(windowTypeC, "flattop") == 0)
|
|
|
|
windowType = WindowType::FLATTOP;
|
|
|
|
else if (strcmp(windowTypeC, "hamming") == 0)
|
|
|
|
windowType = WindowType::HAMMING;
|
|
|
|
else if (strcmp(windowTypeC, "hann") == 0)
|
|
|
|
windowType = WindowType::HANN;
|
|
|
|
else if (strcmp(windowTypeC, "nuttal") == 0)
|
|
|
|
windowType = WindowType::NUTTAL;
|
|
|
|
else if (strcmp(windowTypeC, "blackman") == 0)
|
|
|
|
windowType = WindowType::BLACKMAN;
|
|
|
|
else
|
|
|
|
throw ConfigError(json, "node-config-hook-pmu-window-type",
|
|
|
|
"Invalid window type: {}", windowTypeC);
|
|
|
|
|
|
|
|
if (!angleUnitC)
|
|
|
|
logger->info("No angle type given, assume rad");
|
|
|
|
else if (strcmp(angleUnitC, "rad") == 0)
|
|
|
|
angleUnitFactor = 1;
|
|
|
|
else if (strcmp(angleUnitC, "degree") == 0)
|
|
|
|
angleUnitFactor = 180 / M_PI;
|
|
|
|
else
|
|
|
|
throw ConfigError(json, "node-config-hook-pmu-angle-unit",
|
|
|
|
"Angle unit {} not recognized", angleUnitC);
|
|
|
|
|
|
|
|
if (!timeAlignC)
|
|
|
|
logger->info("No timestamp alignment defined. Assume alignment center");
|
|
|
|
else if (strcmp(timeAlignC, "left") == 0)
|
|
|
|
timeAlignType = TimeAlign::LEFT;
|
|
|
|
else if (strcmp(timeAlignC, "center") == 0)
|
|
|
|
timeAlignType = TimeAlign::CENTER;
|
|
|
|
else if (strcmp(timeAlignC, "right") == 0)
|
|
|
|
timeAlignType = TimeAlign::RIGHT;
|
|
|
|
else
|
|
|
|
throw ConfigError(json, "node-config-hook-dft-timestamp-alignment",
|
|
|
|
"Timestamp alignment {} not recognized", timeAlignC);
|
2022-06-01 18:15:29 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
Hook::Reason PmuHook::process(struct Sample *smp) {
|
|
|
|
assert(state == State::STARTED);
|
|
|
|
|
|
|
|
initSampleCount++;
|
|
|
|
|
|
|
|
if (smp->sequence - lastSequence > 1)
|
|
|
|
logger->warn("Calculation is not Realtime. {} sampled missed",
|
|
|
|
smp->sequence - lastSequence);
|
|
|
|
lastSequence = smp->sequence;
|
|
|
|
|
|
|
|
if (!init && initSampleCount > windowSize)
|
|
|
|
init = true;
|
|
|
|
|
|
|
|
timespec timeDiff = time_diff(&nextRun, &smp->ts.origin);
|
|
|
|
double tmpTimeDiff = time_to_double(&timeDiff);
|
|
|
|
bool run = false;
|
|
|
|
if (tmpTimeDiff > 0. && init)
|
|
|
|
run = true;
|
|
|
|
|
|
|
|
Status phasorStatus = Status::VALID;
|
|
|
|
timespec phasorTimestamp = {0};
|
|
|
|
if (run) {
|
|
|
|
for (unsigned i = 0; i < signalIndices.size(); i++) {
|
|
|
|
lastPhasors[i] = estimatePhasor(windows[i], lastPhasors[i]);
|
|
|
|
if (lastPhasors[i].valid != Status::VALID)
|
|
|
|
phasorStatus = Status::INVALID;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Align time tag
|
|
|
|
double currentTimeTag = time_to_double(&smp->ts.origin);
|
|
|
|
double alignedTime = currentTimeTag - fmod(currentTimeTag, 1 / phasorRate);
|
|
|
|
nextRun = time_from_double(alignedTime + 1 / phasorRate);
|
|
|
|
|
|
|
|
size_t tsPos = 0;
|
|
|
|
if (timeAlignType == TimeAlign::RIGHT)
|
|
|
|
tsPos = windowSize;
|
|
|
|
else if (timeAlignType == TimeAlign::LEFT)
|
|
|
|
tsPos = 0;
|
|
|
|
else if (timeAlignType == TimeAlign::CENTER)
|
|
|
|
tsPos = windowSize / 2;
|
|
|
|
phasorTimestamp = (*windowsTs)[tsPos];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update sample memory
|
|
|
|
unsigned i = 0;
|
|
|
|
for (auto index : signalIndices)
|
|
|
|
windows[i++]->update(smp->data[index].f);
|
|
|
|
windowsTs->update(smp->ts.origin);
|
|
|
|
|
|
|
|
// Make sure to update phasors after window update but estimate them before
|
|
|
|
if (run) {
|
|
|
|
for (unsigned i = 0; i < signalIndices.size(); i++) {
|
|
|
|
smp->data[i * 4 + 0].f =
|
|
|
|
lastPhasors[i].frequency + frequencyOffset; // Frequency
|
|
|
|
smp->data[i * 4 + 1].f = (lastPhasors[i].amplitude / pow(2, 0.5)) +
|
|
|
|
amplitudeOffset; // Amplitude
|
|
|
|
smp->data[i * 4 + 2].f =
|
|
|
|
(lastPhasors[i].phase * 180 / M_PI) + phaseOffset; // Phase
|
|
|
|
smp->data[i * 4 + 3].f = lastPhasors[i].rocof + rocofOffset; /* ROCOF */
|
|
|
|
;
|
|
|
|
}
|
|
|
|
smp->ts.origin = phasorTimestamp;
|
|
|
|
|
|
|
|
smp->length = signalIndices.size() * 4;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!run || phasorStatus != Status::VALID)
|
|
|
|
return Reason::SKIP_SAMPLE;
|
|
|
|
|
|
|
|
return Reason::OK;
|
2022-06-01 18:15:29 +02:00
|
|
|
}
|
|
|
|
|
2023-09-07 11:46:39 +02:00
|
|
|
PmuHook::Phasor PmuHook::estimatePhasor(dsp::CosineWindow<double> *window,
|
|
|
|
Phasor lastPhasor) {
|
|
|
|
return {0., 0., 0., 0., Status::INVALID};
|
2022-06-01 18:15:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Register hook
|
|
|
|
static char n[] = "pmu";
|
|
|
|
static char d[] = "This hook estimates a phsor";
|
2023-09-07 11:46:39 +02:00
|
|
|
static HookPlugin<PmuHook, n, d,
|
|
|
|
(int)Hook::Flags::NODE_READ | (int)Hook::Flags::NODE_WRITE |
|
|
|
|
(int)Hook::Flags::PATH>
|
|
|
|
p;
|
2022-06-01 18:15:29 +02:00
|
|
|
|
2023-08-28 09:34:02 +02:00
|
|
|
} // namespace node
|
|
|
|
} // namespace villas
|