1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00
VILLASnode/lib/hooks/pmu.cpp

252 lines
8 KiB
C++
Raw Permalink Normal View History

2022-06-01 18:15:29 +02:00
/** PMU hook.
*
* @author Manuel Pitz <manuel.pitz@eonerc.rwth-aachen.de>
* @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC
2022-07-04 18:20:03 +02:00
* @license Apache 2.0
2022-06-01 18:15:29 +02:00
*********************************************************************************/
#include <villas/hooks/pmu.hpp>
#include <villas/timing.hpp>
namespace villas {
namespace node {
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});
}
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", &amplitudeOffset,
"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);
}
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;
2022-06-01 18:15:29 +02:00
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;
}
PmuHook::Phasor PmuHook::estimatePhasor(dsp::CosineWindow<double> *window, Phasor lastPhasor)
{
return {0., 0., 0., 0., Status::INVALID};
}
/* Register hook */
static char n[] = "pmu";
static char d[] = "This hook estimates a phsor";
static HookPlugin<PmuHook, n, d, (int) Hook::Flags::NODE_READ | (int) Hook::Flags::NODE_WRITE | (int) Hook::Flags::PATH> p;
} /* namespace node */
} /* namespace villas */