/* RMS hook. * * Author: Manuel Pitz * SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University * SPDX-License-Identifier: Apache-2.0 */ #include #include namespace villas { namespace node { /* Concept: Based on RMS Hook * * For each window, calculate integrals for U, I, U*I * Formulas from: https://de.wikipedia.org/wiki/Scheinleistung * Calculate S and P from these */ class PowerHook : public MultiSignalHook { protected: enum class TimeAlign { LEFT, CENTER, RIGHT, }; struct PowerPairing { int voltageIndex; int currentIndex; }; struct PairingsStr { std::string voltage; std::string current; }; std::vector pairingsStr; std::vector> smpMemory; std::vector pairings; std::vector smpMemoryTs; std::vector accumulator_u; // Integral over u std::vector accumulator_i; // Integral over I std::vector accumulator_ui; // Integral over U*I unsigned windowSize; uint64_t smpMemoryPosition; bool calcActivePower; bool calcReactivePower; bool caclApparentPower; bool calcCosPhi; bool channelNameEnable; // Rename the output values with channel name or only descriptive name double angleUnitFactor; enum TimeAlign timeAlignType; public: PowerHook(Path *p, Node *n, int fl, int prio, bool en = true) : MultiSignalHook(p, n, fl, prio, en), smpMemory(), pairings(), smpMemoryTs(), windowSize(0), smpMemoryPosition(0), calcActivePower(true), calcReactivePower(true), caclApparentPower(true), calcCosPhi(true), channelNameEnable(false), angleUnitFactor(1), timeAlignType(TimeAlign::CENTER) {} virtual void prepare() { MultiSignalHook::prepare(); for (unsigned i = 0; i < pairingsStr.size(); i++) { PowerPairing p = { .voltageIndex = signals->getIndexByName(pairingsStr[i].voltage), .currentIndex = signals->getIndexByName(pairingsStr[i].current)}; if (p.currentIndex == -1) throw RuntimeError("Pairings signal name not recognized {}", pairingsStr[i].current); if (p.voltageIndex == -1) throw RuntimeError("Pairings signal name not recognized {}", pairingsStr[i].voltage); pairings.push_back(p); } if ((pairingsStr.size() * 2) != signalIndices.size()) throw RuntimeError("Number of signals and parings don not match!"); // Check Signal Data Type for (auto index : signalIndices) { auto origSig = signals->getByIndex(index); // Check that signal has float type if (origSig->type != SignalType::FLOAT) throw RuntimeError( "The power hook can only operate on signals of type float!"); } signals->clear(); for (unsigned i = 0; i < signalIndices.size(); i++) { std::string suffix = ""; if (channelNameEnable) suffix = fmt::format("_{}", signalNames[i]); // Add signals if (calcActivePower) { auto activeSig = std::make_shared("active", "W", SignalType::FLOAT); activeSig->name += suffix; if (!activeSig) throw RuntimeError("Failed to create new signals"); signals->push_back(activeSig); } if (calcReactivePower) { auto reactiveSig = std::make_shared("reactive", "Var", SignalType::FLOAT); reactiveSig->name += suffix; if (!reactiveSig) throw RuntimeError("Failed to create new signals"); signals->push_back(reactiveSig); } if (caclApparentPower) { auto apparentSig = std::make_shared("apparent", "VA", SignalType::FLOAT); apparentSig->name += suffix; if (!apparentSig) throw RuntimeError("Failed to create new signals"); signals->push_back(apparentSig); } if (calcCosPhi) { auto cosPhiSig = std::make_shared("cos_phi", "deg", SignalType::FLOAT); cosPhiSig->name += suffix; if (!cosPhiSig) throw RuntimeError("Failed to create new signals"); signals->push_back(cosPhiSig); } } // Initialize sampMemory to 0 smpMemory.clear(); for (unsigned i = 0; i < signalIndices.size(); i++) smpMemory.emplace_back(windowSize, 0.0); smpMemoryTs.clear(); for (unsigned i = 0; i < windowSize; i++) smpMemoryTs.push_back({0}); // Init empty accumulators for each pairing for (size_t i = 0; i < pairings.size(); i++) { accumulator_i.push_back(0.0); accumulator_u.push_back(0.0); accumulator_ui.push_back(0.0); } // Signal state prepared state = State::PREPARED; } // Read configuration JSON and configure hook accordingly virtual void parse(json_t *json) { // Ensure hook is not yet running assert(state != State::STARTED); int result = 0; const char *timeAlignC = nullptr; const char *angleUnitC = nullptr; json_error_t json_error; int windowSizeIn = 0; // Size of window in samples json_t *json_pairings = nullptr; MultiSignalHook::parse(json); result = json_unpack_ex( json, &json_error, 0, "{ s: i , s: o, s?: b, s?: b, s?: b, s?: b, s?: b, s? : s, s? : s }", "window_size", &windowSizeIn, "pairings", &json_pairings, "add_channel_name", &channelNameEnable, "active_power", &calcActivePower, "reactive_power", &calcReactivePower, "apparent_power", &caclApparentPower, "cos_phi", &calcCosPhi, "timestamp_align", &timeAlignC, "angle_unit", &angleUnitC); if (result) throw ConfigError(json, json_error, "node-config-hook-power"); if (windowSizeIn < 1) throw ConfigError(json, "node-config-hook-power", "Window size must be greater 0 but is set to {}", windowSizeIn); windowSize = (unsigned)windowSizeIn; 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); 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-dft-angle-unit", "Angle unit {} not recognized", angleUnitC); // Pairings if (!json_is_array(json_pairings)) throw ConfigError(json_pairings, "node-config-hook-power", "Pairings are expected as json array"); size_t i = 0; json_t *json_pairings_value; json_array_foreach(json_pairings, i, json_pairings_value) { const char *voltageNameC = nullptr; const char *currentNameC = nullptr; json_unpack_ex(json_pairings_value, &json_error, 0, "{ s: s, s: s}", "voltage", &voltageNameC, "current", ¤tNameC); pairingsStr.push_back( (PairingsStr){.voltage = voltageNameC, .current = currentNameC}); } state = State::PARSED; } // This function does the actual processing of the hook when a new sample is passed through. virtual Hook::Reason process(struct Sample *smp) { assert(state == State::STARTED); uint sigIndex = 0; //used to set the pos of value in output sampel array smpMemoryTs[smpMemoryPosition % windowSize] = smp->ts.origin; // Loop over all pairings for (size_t i = 0; i < pairings.size(); i++) { auto pair = pairings[i]; // Update U integral double oldValueU = smpMemory[pair.voltageIndex][smpMemoryPosition % windowSize]; double newValueU = smp->data[pair.voltageIndex].f; smpMemory[pair.voltageIndex][smpMemoryPosition % windowSize] = newValueU; // Save for later accumulator_u[i] -= oldValueU * oldValueU; accumulator_u[i] += newValueU * newValueU; // Update I integral double newValueI = smp->data[pair.currentIndex].f; double oldValueI = smpMemory[pair.currentIndex][smpMemoryPosition % windowSize]; smpMemory[pair.currentIndex][smpMemoryPosition % windowSize] = newValueI; // Save for later accumulator_i[i] -= oldValueI * oldValueI; accumulator_i[i] += newValueI * newValueI; // Update UI Integral accumulator_ui[i] -= oldValueI * oldValueU; accumulator_ui[i] += newValueI * newValueU; // Calc active power power double P = (1.0 / windowSize) * accumulator_ui[i]; // Calc apparent power double S = (1.0 / windowSize) * pow(accumulator_i[i] * accumulator_u[i], 0.5); // Calc reactive power double Q = pow(S * S - P * P, 0.5); // Calc cos phi double PHI = atan2(Q, P) * angleUnitFactor; if (smpMemoryPosition >= windowSize) { // Write to samples if (calcActivePower) smp->data[sigIndex++].f = P; if (calcReactivePower) smp->data[sigIndex++].f = Q; if (caclApparentPower) smp->data[sigIndex++].f = S; if (calcCosPhi) smp->data[sigIndex++].f = PHI; } } smp->length = sigIndex; if (smpMemoryPosition >= windowSize) { unsigned tsPos = 0; if (timeAlignType == TimeAlign::RIGHT) tsPos = smpMemoryPosition; else if (timeAlignType == TimeAlign::LEFT) tsPos = smpMemoryPosition - windowSize; else if (timeAlignType == TimeAlign::CENTER) tsPos = smpMemoryPosition - (windowSize / 2); smp->ts.origin = smpMemoryTs[tsPos % windowSize]; } if (smpMemoryPosition >= 2 * windowSize) //reset smpMemPos if greater than twice the window. Important to handle init smpMemoryPosition = windowSize; smpMemoryPosition++; // Move write head for sample history foreward by one if (windowSize < smpMemoryPosition) return Reason::OK; return Reason::SKIP_SAMPLE; } }; // Register hook static char n[] = "power"; static char d[] = "This hook calculates the Active and Reactive Power for a given signal "; static HookPlugin p; } // namespace node } // namespace villas