/** PMU hook. * * @author Manuel Pitz * @copyright 2014-2022, Institute for Automation of Complex Power Systems, EONERC * @license Apache 2.0 *********************************************************************************/ #include #include 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("frequency", "Hz", SignalType::FLOAT); auto amplSig = std::make_shared("amplitude", "V", SignalType::FLOAT); auto phaseSig = std::make_shared("phase", (angleUnitFactor)?"rad":"deg", SignalType::FLOAT);//angleUnitFactor==1 means rad auto rocofSig = std::make_shared("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(windowSize, 0.0)); else if (windowType == WindowType::FLATTOP) windows.push_back(new dsp::FlattopWindow(windowSize, 0.0)); else if (windowType == WindowType::HAMMING) windows.push_back(new dsp::HammingWindow(windowSize, 0.0)); else if (windowType == WindowType::HANN) windows.push_back(new dsp::HannWindow(windowSize, 0.0)); else if (windowType == WindowType::NUTTAL) windows.push_back(new dsp::NuttallWindow(windowSize, 0.0)); else if (windowType == WindowType::BLACKMAN) windows.push_back(new dsp::BlackmanWindow(windowSize, 0.0)); } windowsTs = new dsp::Window(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", &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); } 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; } PmuHook::Phasor PmuHook::estimatePhasor(dsp::CosineWindow *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 p; } /* namespace node */ } /* namespace villas */