From 231bbb4b1b03f42e856fdd92cca611dc8d38e37a Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sun, 17 May 2026 14:41:26 -0500 Subject: [PATCH 1/3] REGCA skeleton --- GridKit/Model/PhasorDynamics/CMakeLists.txt | 1 + .../Model/PhasorDynamics/ComponentLibrary.hpp | 1 + .../PhasorDynamics/Converter/CMakeLists.txt | 6 + .../Converter/REGCA/CMakeLists.txt | 49 +++ .../PhasorDynamics/Converter/REGCA/Regca.cpp | 27 ++ .../PhasorDynamics/Converter/REGCA/Regca.hpp | 168 ++++++++++ .../Converter/REGCA/RegcaData.hpp | 77 +++++ .../REGCA/RegcaDependencyTracking.cpp | 27 ++ .../Converter/REGCA/RegcaEnzyme.cpp | 31 ++ .../Converter/REGCA/RegcaImpl.hpp | 303 ++++++++++++++++++ GridKit/Model/PhasorDynamics/INPUT_FORMAT.md | 32 ++ GridKit/Model/PhasorDynamics/SystemModel.hpp | 31 ++ .../Model/PhasorDynamics/SystemModelData.hpp | 3 + .../SystemModelDataJSONParser.hpp | 6 + tests/UnitTests/PhasorDynamics/CMakeLists.txt | 8 + .../PhasorDynamics/ConverterRegcaTests.hpp | 232 ++++++++++++++ .../SystemSingleComponentTests.hpp | 24 ++ .../PhasorDynamics/runConverterRegcaTests.cpp | 16 + .../runSystemSingleComponentTests.cpp | 1 + 19 files changed, 1043 insertions(+) create mode 100644 GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/CMakeLists.txt create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaDependencyTracking.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp create mode 100644 GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp create mode 100644 tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp create mode 100644 tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp diff --git a/GridKit/Model/PhasorDynamics/CMakeLists.txt b/GridKit/Model/PhasorDynamics/CMakeLists.txt index b586b2fd7..7c4715b50 100644 --- a/GridKit/Model/PhasorDynamics/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/CMakeLists.txt @@ -35,6 +35,7 @@ target_link_libraries(phasor_dynamics_components_dependency_tracking add_subdirectory(Branch) add_subdirectory(Bus) add_subdirectory(BusFault) +add_subdirectory(Converter) add_subdirectory(Exciter) add_subdirectory(Governor) add_subdirectory(Load) diff --git a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp index b44819ed5..30bae6353 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include diff --git a/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt new file mode 100644 index 000000000..cafb2cc36 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/CMakeLists.txt @@ -0,0 +1,6 @@ +# [[ +# Author(s): +# - Luke Lowery +# ]] + +add_subdirectory(REGCA) diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Converter/REGCA/CMakeLists.txt new file mode 100644 index 000000000..9e9b582d7 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/CMakeLists.txt @@ -0,0 +1,49 @@ +# [[ +# Author(s): +# - Luke Lowery +# ]] + +set(_install_headers + Regca.hpp + RegcaData.hpp) + +if(GRIDKIT_ENABLE_ENZYME) + gridkit_add_library(phasor_dynamics_converter_regca + SOURCES + RegcaEnzyme.cpp + HEADERS + ${_install_headers} + INCLUDE_DIRECTORIES + PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC GridKit::phasor_dynamics_core + PUBLIC GridKit::phasor_dynamics_signal + PRIVATE ClangEnzymeFlags + COMPILE_OPTIONS + PRIVATE -mllvm -enzyme-auto-sparsity=1 -fno-math-errno) +else() + gridkit_add_library(phasor_dynamics_converter_regca + SOURCES + Regca.cpp + HEADERS + ${_install_headers} + INCLUDE_DIRECTORIES + PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC GridKit::phasor_dynamics_core + PUBLIC GridKit::phasor_dynamics_signal) +endif() + +gridkit_add_library(phasor_dynamics_converter_regca_dependency_tracking + SOURCES + RegcaDependencyTracking.cpp + INCLUDE_DIRECTORIES + PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC GridKit::phasor_dynamics_core + PUBLIC GridKit::phasor_dynamics_signal_dependency_tracking) + +target_link_libraries(phasor_dynamics_components + INTERFACE GridKit::phasor_dynamics_converter_regca) +target_link_libraries(phasor_dynamics_components_dependency_tracking + INTERFACE GridKit::phasor_dynamics_converter_regca_dependency_tracking) diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.cpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.cpp new file mode 100644 index 000000000..b018c4bc3 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.cpp @@ -0,0 +1,27 @@ +/** + * @file Regca.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Non-Enzyme instantiation for the REGCA converter model. + */ + +#include "RegcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Regca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Regca..." << std::endl; + Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + return 0; + } + + template class Regca; + template class Regca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp new file mode 100644 index 000000000..cbafdc7d8 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp @@ -0,0 +1,168 @@ +/** + * @file Regca.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Declaration of the REGCA phasor-dynamics converter model. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + template + class BusBase; + + template + class SignalNode; + } // namespace PhasorDynamics +} // namespace GridKit + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + /// Internal variables of a `Regca` + enum class RegcaInternalVariables : size_t + { + VM, ///< Filtered terminal voltage + IQ, ///< Reactive-current state + IP, ///< Active-current state + VT, ///< Terminal voltage magnitude + II, ///< Imaginary injected current + IQEXTRA, ///< HVRCM extra reactive current + IL, ///< LVPL upper-limit current curve + IR, ///< Real injected current + LP, ///< Active-current lower rate bound + UP, ///< Active-current upper rate bound + MAXIMUM, + }; + + /// External variables of a `Regca` + enum class RegcaExternalVariables : size_t + { + IPCMD, ///< Active-current command signal + IQCMD, ///< Reactive-current command signal + MAXIMUM, + }; + + template + class Regca : public Component + { + using Component::gridkit_component_id_; + using Component::alpha_; + using Component::f_; + using Component::h_; + using Component::J_; + using Component::J_cols_buffer_; + using Component::J_rows_buffer_; + using Component::J_vals_buffer_; + using Component::mva_system_base_; + using Component::nnz_; + using Component::residual_indices_; + using Component::size_; + using Component::tag_; + using Component::time_; + using Component::variable_indices_; + using Component::wb_; + using Component::y_; + using Component::yp_; + + public: + using RealT = typename Component::RealT; + using bus_type = BusBase; + using signal_type = SignalNode; + using model_data_type = RegcaData; + using MonitorT = Model::VariableMonitor; + + Regca(bus_type* bus); + Regca(bus_type* bus, const model_data_type& data); + ~Regca(); + + int setGridKitComponentID(IdxT) override final; + int allocate() override final; + int verify() const override final; + int initialize() override final; + int tagDifferentiable() override final; + int evaluateResidual() override final; + int evaluateJacobian() override final; + + auto getSignals() + -> ComponentSignals& + { + return signals_; + } + + const Model::VariableMonitorBase* getMonitor() const override; + + __attribute__((always_inline)) inline int evaluateInternalResidual( + ScalarT*, ScalarT*, ScalarT*, ScalarT*, ScalarT*); + __attribute__((always_inline)) inline int evaluateBusResidual( + ScalarT*, ScalarT*, ScalarT*, ScalarT*); + + private: + void initializeParameters(const model_data_type& data); + void initializeMonitor(); + + ScalarT& Vr() + { + return bus_->Vr(); + } + + ScalarT& Vi() + { + return bus_->Vi(); + } + + ScalarT& Ir() + { + return bus_->Ir(); + } + + ScalarT& Ii() + { + return bus_->Ii(); + } + + bus_type* bus_{nullptr}; + + RealT P0_{0}; + RealT Q0_{0}; + RealT Sconv_{0}; + RealT Tg_{0}; + RealT TM_{0}; + RealT Rqmax_{0}; + RealT Rqmin_{0}; + RealT Rpmax_{0}; + bool sL_{false}; + RealT IL1_{0}; + RealT VL0_{0}; + RealT VL1_{0}; + RealT VA0_{0}; + RealT VA1_{0}; + RealT Vhvmax_{0}; + IdxT bus_id_{0}; + + ComponentSignals signals_; + std::unique_ptr monitor_; + + std::vector ws_; + std::vector ws_indices_; + }; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp new file mode 100644 index 000000000..29f8530fe --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp @@ -0,0 +1,77 @@ +/** + * @file RegcaData.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Modeling data for the REGCA converter model. + */ + +#pragma once + +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + /// Parameter keys for the REGCA converter model. + enum class RegcaParameters + { + P0, ///< Initial active power injection on system base + Q0, ///< Initial reactive power injection on system base + Sconv, ///< Converter/model power base + Tg, ///< Converter current-control lag time constant + TM, ///< Terminal voltage sensor time constant + Rqmax, ///< Reactive-current recovery positive rate limit + Rqmin, ///< Reactive-current recovery negative rate limit + Rpmax, ///< Active-current magnitude recovery rate limit + sL, ///< LVPL switch + IL1, ///< LVPL upper-current ceiling + VL0, ///< LVPL zero-crossing voltage + VL1, ///< LVPL upper breakpoint voltage + VA0, ///< LVACM lower breakpoint voltage + VA1, ///< LVACM upper breakpoint voltage + Vhvmax ///< Terminal-voltage ceiling for HV reactive management + }; + + /// Ports for the REGCA converter model. + enum class RegcaPorts + { + bus, ///< Terminal bus ID + ipcmd, ///< Optional active-current command signal ID + iqcmd ///< Optional reactive-current command signal ID + }; + + /// Variables available through the monitor interface. + enum class RegcaMonitorableVariables + { + ir, ///< Real current injection on converter base + ii, ///< Imaginary current injection on converter base + p, ///< Active power injection on system base + q, ///< Reactive power injection on system base + vt, ///< Terminal voltage magnitude + vm, ///< Filtered terminal voltage + ip, ///< Active-current state + iq, ///< Reactive-current state + iqextra, ///< HVRCM extra reactive current + il, ///< LVPL upper-limit current curve + lp, ///< Active-current lower rate bound + up ///< Active-current upper rate bound + }; + + template + struct RegcaData : public ComponentData + { + RegcaData() = default; + + using Parameters = RegcaParameters; + using Ports = RegcaPorts; + using MonitorableVariables = RegcaMonitorableVariables; + }; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaDependencyTracking.cpp new file mode 100644 index 000000000..20d688688 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaDependencyTracking.cpp @@ -0,0 +1,27 @@ +/** + * @file RegcaDependencyTracking.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Dependency-tracking instantiations for the REGCA converter model. + */ + +#include "RegcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Regca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Regca..." << std::endl; + Log::misc() << "Jacobian evaluation is not implemented!" << std::endl; + return 0; + } + + template class Regca; + template class Regca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp new file mode 100644 index 000000000..7ad8ee7fc --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp @@ -0,0 +1,31 @@ +/** + * @file RegcaEnzyme.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Enzyme sparse Jacobian for the REGCA converter model. + */ + +#include + +#include "RegcaImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + template + int Regca::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Regca..." << std::endl; + Log::misc() << "Jacobian evaluation is experimental!" << std::endl; + + J_.zeroMatrix(); + return 0; + } + + template class Regca; + template class Regca; + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp new file mode 100644 index 000000000..6598ea24e --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp @@ -0,0 +1,303 @@ +/** + * @file RegcaImpl.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Definition of the REGCA phasor-dynamics converter model. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Converter + { + using Log = ::GridKit::Utilities::Logger; + + template + Regca::Regca(bus_type* bus) + : bus_(bus) + { + size_ = static_cast(RegcaInternalVariables::MAXIMUM); + } + + template + Regca::Regca(bus_type* bus, const model_data_type& data) + : bus_(bus), + monitor_(std::make_unique(data)) + { + initializeParameters(data); + initializeMonitor(); + size_ = static_cast(RegcaInternalVariables::MAXIMUM); + } + + template + Regca::~Regca() + { + } + + template + void Regca::initializeParameters(const model_data_type& data) + { + using Params = typename model_data_type::Parameters; + using Ports = typename model_data_type::Ports; + + auto loadReal = [&](auto key, RealT& target) + { + if (!data.parameters.contains(key)) + { + return; + } + + const auto& value = data.parameters.at(key); + if (const auto* real_value = std::get_if(&value)) + { + target = *real_value; + } + else if (const auto* index_value = std::get_if(&value)) + { + target = static_cast(*index_value); + } + }; + + auto loadBool = [&](auto key, bool& target) + { + if (!data.parameters.contains(key)) + { + return; + } + + const auto& value = data.parameters.at(key); + if (const auto* bool_value = std::get_if(&value)) + { + target = *bool_value; + } + else if (const auto* index_value = std::get_if(&value)) + { + target = (*index_value != 0); + } + }; + + loadReal(Params::P0, P0_); + loadReal(Params::Q0, Q0_); + loadReal(Params::Sconv, Sconv_); + loadReal(Params::Tg, Tg_); + loadReal(Params::TM, TM_); + loadReal(Params::Rqmax, Rqmax_); + loadReal(Params::Rqmin, Rqmin_); + loadReal(Params::Rpmax, Rpmax_); + loadBool(Params::sL, sL_); + loadReal(Params::IL1, IL1_); + loadReal(Params::VL0, VL0_); + loadReal(Params::VL1, VL1_); + loadReal(Params::VA0, VA0_); + loadReal(Params::VA1, VA1_); + loadReal(Params::Vhvmax, Vhvmax_); + + if (data.ports.contains(Ports::bus)) + { + bus_id_ = data.ports.at(Ports::bus); + } + } + + template + const Model::VariableMonitorBase* Regca::getMonitor() const + { + return monitor_.get(); + } + + template + void Regca::initializeMonitor() + { + using Variable = typename model_data_type::MonitorableVariables; + auto index = [](RegcaInternalVariables variable) + { + return static_cast(variable); + }; + + monitor_->set(Variable::ir, [this, index] + { return y_[index(RegcaInternalVariables::IR)]; }); + monitor_->set(Variable::ii, [this, index] + { return y_[index(RegcaInternalVariables::II)]; }); + monitor_->set(Variable::p, [] + { return ScalarT{0}; }); + monitor_->set(Variable::q, [] + { return ScalarT{0}; }); + monitor_->set(Variable::vt, [this, index] + { return y_[index(RegcaInternalVariables::VT)]; }); + monitor_->set(Variable::vm, [this, index] + { return y_[index(RegcaInternalVariables::VM)]; }); + monitor_->set(Variable::ip, [this, index] + { return y_[index(RegcaInternalVariables::IP)]; }); + monitor_->set(Variable::iq, [this, index] + { return y_[index(RegcaInternalVariables::IQ)]; }); + monitor_->set(Variable::iqextra, [this, index] + { return y_[index(RegcaInternalVariables::IQEXTRA)]; }); + monitor_->set(Variable::il, [this, index] + { return y_[index(RegcaInternalVariables::IL)]; }); + monitor_->set(Variable::lp, [this, index] + { return y_[index(RegcaInternalVariables::LP)]; }); + monitor_->set(Variable::up, [this, index] + { return y_[index(RegcaInternalVariables::UP)]; }); + } + + template + int Regca::setGridKitComponentID(IdxT component_id) + { + gridkit_component_id_ = component_id; + return 0; + } + + template + int Regca::allocate() + { + size_ = static_cast(RegcaInternalVariables::MAXIMUM); + auto size = static_cast(size_); + + f_.assign(size, ScalarT{0}); + y_.assign(size, ScalarT{0}); + yp_.assign(size, ScalarT{0}); + tag_.assign(size, false); + variable_indices_.resize(size); + residual_indices_.resize(size); + + wb_.assign(2, ScalarT{0}); + h_.assign(2, ScalarT{0}); + + auto signal_size = static_cast(RegcaExternalVariables::MAXIMUM); + ws_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); + + for (IdxT j = 0; j < size_; ++j) + { + this->setVariableIndex(j, j); + this->setResidualIndex(j, j); + } + + return 0; + } + + template + int Regca::verify() const + { + int ret = 0; + + if (bus_ == nullptr) + { + Log::error() << "Regca: bus pointer is null\n"; + ret += 1; + } + + if (signals_.template isAttached()) + { + if (!signals_.template isLinked()) + { + Log::error() << "Regca: ipcmd signal attached with no linked source\n"; + ret += 1; + } + } + + if (signals_.template isAttached()) + { + if (!signals_.template isLinked()) + { + Log::error() << "Regca: iqcmd signal attached with no linked source\n"; + ret += 1; + } + } + + return ret; + } + + template + int Regca::initialize() + { + std::fill(y_.begin(), y_.end(), ScalarT{0}); + std::fill(yp_.begin(), yp_.end(), ScalarT{0}); + return 0; + } + + template + int Regca::tagDifferentiable() + { + std::fill(tag_.begin(), tag_.end(), false); + tag_[static_cast(RegcaInternalVariables::VM)] = true; + tag_[static_cast(RegcaInternalVariables::IQ)] = true; + tag_[static_cast(RegcaInternalVariables::IP)] = true; + return 0; + } + + template + __attribute__((always_inline)) inline int Regca::evaluateInternalResidual( + [[maybe_unused]] ScalarT* y, + [[maybe_unused]] ScalarT* yp, + [[maybe_unused]] ScalarT* wb, + [[maybe_unused]] ScalarT* ws, + ScalarT* f) + { + for (IdxT i = 0; i < size_; ++i) + { + f[static_cast(i)] = ScalarT{0}; + } + + return 0; + } + + template + __attribute__((always_inline)) inline int Regca::evaluateBusResidual( + [[maybe_unused]] ScalarT* y, + [[maybe_unused]] ScalarT* yp, + [[maybe_unused]] ScalarT* wb, + ScalarT* h) + { + h[0] = ScalarT{0}; + h[1] = ScalarT{0}; + return 0; + } + + template + int Regca::evaluateResidual() + { + std::fill(ws_.begin(), ws_.end(), ScalarT{0}); + std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); + + if (signals_.template isAttached()) + { + const auto index = static_cast(RegcaExternalVariables::IPCMD); + ws_[index] = signals_.template readExternalVariable(); + ws_indices_[index] = + signals_.template readExternalVariableIndex(); + } + + if (signals_.template isAttached()) + { + const auto index = static_cast(RegcaExternalVariables::IQCMD); + ws_[index] = signals_.template readExternalVariable(); + ws_indices_[index] = + signals_.template readExternalVariableIndex(); + } + + wb_[0] = Vr(); + wb_[1] = Vi(); + + evaluateInternalResidual(y_.data(), yp_.data(), wb_.data(), ws_.data(), f_.data()); + evaluateBusResidual(y_.data(), yp_.data(), wb_.data(), h_.data()); + + Ir() += h_[0]; + Ii() += h_[1]; + + return 0; + } + } // namespace Converter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index 72d732a0d..fdbc7cd3a 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -144,6 +144,7 @@ are specified: `Genrou` | 6th order machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqop`, `Tqopp`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xqp`, `Xqpp`, `Xl`, `S10`, `S12`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed` `Gensal` | 5th order salient-pole machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqopp`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xl`, `S10`, `S12`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed`, `Eqp`, `psidp`, `psiqpp`, `psidpp`, `vd`, `vq`, `te`, `id`, `iq` `GenClassical`| the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega` + `Regca` | WECC REGCA renewable generator/converter skeleton | `bus`, `ipcmd`\*, `iqcmd`\* | `P0`, `Q0`, `Sconv`, `Tg`, `TM`, `Rqmax`, `Rqmin`, `Rpmax`, `sL`, `IL1`, `VL0`, `VL1`, `VA0`, `VA1`, `Vhvmax` | `ir`, `ii`, `p`, `q`, `vt`, `vm`, `ip`, `iq`, `iqextra`, `il`, `lp`, `up` `Tgov1 ` | the TGOV1 governor model | `pmech`, `speed` | `R`, `T1`, `T2`, `T3`, `Pvmax`, `Pvmin`, `Dt` | `none` `Ieeet1` | the IEEET1 exciter model | `bus`, `speed`, `efd`, `vs`\* | `Tr`, `Ka`, `Ta`, `Ke`, `Te`, `Kf`, `Tf`, `Vrmin`, `Vrmax`, `E1`, `E2`, `Se1`, `Se2`, `Ispdlim` | `efd`, `ksat` `SexsPti` | the SEXS-PTI simplified exciter model | `bus`, `efd`, `vs`\* | `Ta`, `Tb`, `Te`, `K`, `Efdmax`, `Efdmin` | `efd` @@ -153,6 +154,37 @@ are specified: Ports marked with \* are optional and, if missing, will be assumed to be connected to a constant value. This list is subject to change. +For `Regca`, `ipcmd` and `iqcmd` are optional in the skeleton implementation +and default to constant zero command inputs when omitted. + +Compact `Regca` device example: + +```json +{ + "class": "Regca", + "ports": { "bus": 1, "ipcmd": 10, "iqcmd": 11 }, + "id": "CV1", + "params": { + "P0": 1.0, + "Q0": 0.0, + "Sconv": 100.0, + "Tg": 0.02, + "TM": 0.02, + "Rqmax": 999.0, + "Rqmin": -999.0, + "Rpmax": 999.0, + "sL": true, + "IL1": 1.1, + "VL0": 0.4, + "VL1": 0.9, + "VA0": 0.4, + "VA1": 0.9, + "Vhvmax": 1.2 + }, + "mon": ["ir", "ii"] +} +``` + ## Example File for a 2-Bus System diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index 43cb02cf6..1b738c170 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -82,6 +82,7 @@ namespace GridKit using namespace Governor; using namespace Exciter; using namespace Stabilizer; + using namespace Converter; // Set system model tolerances rel_tol_ = 1e-7; @@ -108,6 +109,36 @@ namespace GridKit addSignal(signal); } + // Add REGCA converters + for (const auto& regcadata : data.regca) + { + using DataT = typename SystemModelData::RegcaDataT; + + IdxT bus_index = 0; + if (regcadata.ports.contains(DataT::Ports::bus)) + { + bus_index = regcadata.ports.at(DataT::Ports::bus); + } + + auto* regca = new Converter::Regca(getBus(bus_index), regcadata); + + if (regcadata.ports.contains(DataT::Ports::ipcmd)) + { + const IdxT ipcmd = regcadata.ports.at(DataT::Ports::ipcmd); + regca->getSignals().template attachSignalNode( + getSignal(ipcmd)); + } + + if (regcadata.ports.contains(DataT::Ports::iqcmd)) + { + const IdxT iqcmd = regcadata.ports.at(DataT::Ports::iqcmd); + regca->getSignals().template attachSignalNode( + getSignal(iqcmd)); + } + + addComponent(regca); + } + // Add branches for (const auto& branchdata : data.branch) { diff --git a/GridKit/Model/PhasorDynamics/SystemModelData.hpp b/GridKit/Model/PhasorDynamics/SystemModelData.hpp index a655d3b59..a7648f1a5 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelData.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelData.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ namespace GridKit using BranchDataT = BranchData; using BusDataT = BusData; using BusFaultDataT = BusFaultData; + using RegcaDataT = Converter::RegcaData; using Tgov1DataT = Governor::Tgov1Data; using Ieeet1DataT = Exciter::Ieeet1Data; using SexsPtiDataT = Exciter::SexsPtiData; @@ -88,6 +90,7 @@ namespace GridKit std::vector bus; ///< Buses within the model std::vector branch; ///< Branches within the model std::vector bus_fault; ///< Bus faults within the model + std::vector regca; ///< REGCA converter instances within the model std::vector genrou; ///< GENROU instances within the model std::vector gensal; ///< GENSAL instances within the model std::vector genclassical; ///< Classical generator instances within the model diff --git a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp index 0a65042d5..60a19931c 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp @@ -130,6 +130,12 @@ namespace GridKit raw_component.get_to(loadzip); sm.loadzip.push_back(loadzip); } + else if (kind == "Regca") + { + typename SystemModelData::RegcaDataT regca; + raw_component.get_to(regca); + sm.regca.push_back(regca); + } else if (kind == "Tgov1") { typename SystemModelData::Tgov1DataT gov; diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 8c7df4892..192321fac 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -70,6 +70,12 @@ target_link_libraries(test_phasor_exciter_sexspti GridKit::definitions GridKit::phasor_dynamics_components_dependency_tracking GridKit::testing) +add_executable(test_phasor_converter_regca runConverterRegcaTests.cpp) +target_link_libraries(test_phasor_converter_regca GridKit::definitions + GridKit::phasor_dynamics_components + GridKit::phasor_dynamics_components_dependency_tracking + GridKit::testing) + add_executable(test_phasor_stabilizer_ieeest runStabilizerIeeestTests.cpp) target_link_libraries(test_phasor_stabilizer_ieeest GridKit::definitions GridKit::phasor_dynamics_stabilizer_ieeest @@ -107,6 +113,7 @@ add_test(NAME PhasorDynamicsGovernorTgov1Test COMMAND test_phasor_governor_tgov1 add_test(NAME PhasorDynamicsExciterIeeet1Test COMMAND test_phasor_exciter_ieeet1) add_test(NAME PhasorDynamicsGensalTest COMMAND test_phasor_gensal) add_test(NAME PhasorDynamicsExciterSexsPtiTest COMMAND test_phasor_exciter_sexspti) +add_test(NAME PhasorDynamicsConverterRegcaTest COMMAND test_phasor_converter_regca) add_test(NAME PhasorDynamicsStabilizerIeeestTest COMMAND test_phasor_stabilizer_ieeest) add_test(NAME PhasorDynamicsGenClassicalTest COMMAND test_phasor_gen_classical) add_test(NAME PhasorDynamicsLoadTest COMMAND test_phasor_load) @@ -124,6 +131,7 @@ install(TARGETS test_phasor_bus test_phasor_exciter_ieeet1 test_phasor_gensal test_phasor_exciter_sexspti + test_phasor_converter_regca test_phasor_stabilizer_ieeest test_phasor_gen_classical test_phasor_system diff --git a/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp new file mode 100644 index 000000000..f74da86e6 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp @@ -0,0 +1,232 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class ConverterRegcaTests + { + public: + using RealT = typename PhasorDynamics::Component::RealT; + + ConverterRegcaTests() = default; + ~ConverterRegcaTests() = default; + + static constexpr ScalarT kTol = static_cast(1.0e-14); + + TestOutcome constructor() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + + PhasorDynamics::Converter::Regca minimal(&bus); + success *= (minimal.size() == static_cast(PhasorDynamics::Converter::RegcaInternalVariables::MAXIMUM)); + success *= (minimal.getMonitor() == nullptr); + + auto data = makeTestData(); + PhasorDynamics::Converter::Regca from_data(&bus, data); + success *= (from_data.size() == static_cast(PhasorDynamics::Converter::RegcaInternalVariables::MAXIMUM)); + success *= (from_data.getMonitor() != nullptr); + + return success.report(__func__); + } + + TestOutcome lifecycle() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + bus.evaluateResidual(); + + auto data = makeTestData(); + PhasorDynamics::Converter::Regca regca(&bus, data); + success *= (regca.allocate() == 0); + success *= (regca.initialize() == 0); + success *= (regca.tagDifferentiable() == 0); + success *= (regca.evaluateResidual() == 0); + success *= (regca.evaluateJacobian() == 0); + + const auto& y = regca.y(); + const auto& yp = regca.yp(); + const auto& f = regca.getResidual(); + for (size_t i = 0; i < static_cast(regca.size()); ++i) + { + success *= isEqual(y[i], static_cast(0.0), kTol); + success *= isEqual(yp[i], static_cast(0.0), kTol); + success *= isEqual(f[i], static_cast(0.0), kTol); + } + + using Vars = PhasorDynamics::Converter::RegcaInternalVariables; + for (size_t i = 0; i < static_cast(Vars::MAXIMUM); ++i) + { + const bool expected = (i == static_cast(Vars::VM)) + || (i == static_cast(Vars::IQ)) + || (i == static_cast(Vars::IP)); + success *= (regca.tag()[i] == expected); + } + + success *= isEqual(bus.Ir(), static_cast(0.0), kTol); + success *= isEqual(bus.Ii(), static_cast(0.0), kTol); + + return success.report(__func__); + } + + TestOutcome signalVerification() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + PhasorDynamics::Converter::Regca regca(&bus, makeTestData()); + + PhasorDynamics::SignalNode ipcmd_node; + PhasorDynamics::SignalNode iqcmd_node; + ScalarT ipcmd_value{0.25}; + ScalarT iqcmd_value{-0.10}; + IdxT ipcmd_index = 21; + IdxT iqcmd_index = 22; + + regca.getSignals().template attachSignalNode(&ipcmd_node); + success *= (regca.verify() > 0); + + ipcmd_node.set(&ipcmd_value, &ipcmd_index); + success *= (regca.verify() == 0); + + regca.getSignals().template attachSignalNode(&iqcmd_node); + success *= (regca.verify() > 0); + + iqcmd_node.set(&iqcmd_value, &iqcmd_index); + success *= (regca.verify() == 0); + + return success.report(__func__); + } + + TestOutcome nullBusVerification() + { + TestStatus success = true; + + PhasorDynamics::Converter::Regca regca(nullptr); + success *= (regca.verify() > 0); + + return success.report(__func__); + } + + TestOutcome jsonParseAndSystemAssembly() + { + TestStatus success = true; + + std::istringstream input(R"json( +{ + "header": { + "format_version": 0, + "format_revision": 1, + "case_name": "REGCA skeleton", + "case_description": "REGCA parser smoke test", + "case_comments": "", + "freq_base": 60.0, + "va_base": 100000000.0 + }, + "buses": [ + { "number": 1, "class": "bus", "name": "Bus 1", "init": { "Vr": 1.0, "Vi": 0.0 }, "v_base": 1.0 } + ], + "devices": [ + { + "class": "Regca", + "ports": { "bus": 1 }, + "id": "CV1", + "params": { + "P0": 1.0, + "Q0": 0.0, + "Sconv": 100, + "Tg": 0.02, + "TM": 0.02, + "Rqmax": 999.0, + "Rqmin": -999.0, + "Rpmax": 999.0, + "sL": true, + "IL1": 1.1, + "VL0": 0.4, + "VL1": 0.9, + "VA0": 0.4, + "VA1": 0.9, + "Vhvmax": 1.2 + }, + "mon": ["ir", "ii"] + } + ] +} +)json"); + + auto data = PhasorDynamics::parseSystemModelData(input); + success *= (data.regca.size() == 1); + success *= (data.regca[0].device_class == "Regca"); + success *= (data.regca[0].ports.at(PhasorDynamics::Converter::RegcaPorts::bus) == 1); + success *= (std::get_if(&data.regca[0].parameters.at(PhasorDynamics::Converter::RegcaParameters::Sconv)) != nullptr); + success *= (std::get_if(&data.regca[0].parameters.at(PhasorDynamics::Converter::RegcaParameters::sL)) != nullptr); + + PhasorDynamics::SystemModel system(data); + success *= (system.allocate() == 0); + success *= (system.initialize() == 0); + success *= (system.tagDifferentiable() == 0); + success *= (system.evaluateResidual() == 0); + success *= (system.evaluateJacobian() == 0); + success *= (system.size() == 12); + success *= isEqual(system.getResidual()[0], 0.0, static_cast(kTol)); + success *= isEqual(system.getResidual()[1], 0.0, static_cast(kTol)); + + return success.report(__func__); + } + + private: + auto makeTestData() -> PhasorDynamics::Converter::RegcaData + { + using Params = PhasorDynamics::Converter::RegcaParameters; + using Ports = PhasorDynamics::Converter::RegcaPorts; + using Mon = PhasorDynamics::Converter::RegcaMonitorableVariables; + + PhasorDynamics::Converter::RegcaData data; + data.device_class = "Regca"; + data.disambiguation_string = "regca_test"; + data.ports[Ports::bus] = 1; + data.monitored_variables.insert(Mon::ir); + data.monitored_variables.insert(Mon::ii); + + data.parameters[Params::P0] = static_cast(1.0); + data.parameters[Params::Q0] = static_cast(0.0); + data.parameters[Params::Sconv] = static_cast(100); + data.parameters[Params::Tg] = static_cast(0.02); + data.parameters[Params::TM] = static_cast(0.02); + data.parameters[Params::Rqmax] = static_cast(999.0); + data.parameters[Params::Rqmin] = static_cast(-999.0); + data.parameters[Params::Rpmax] = static_cast(999.0); + data.parameters[Params::sL] = true; + data.parameters[Params::IL1] = static_cast(1.1); + data.parameters[Params::VL0] = static_cast(0.4); + data.parameters[Params::VL1] = static_cast(0.9); + data.parameters[Params::VA0] = static_cast(0.4); + data.parameters[Params::VA1] = static_cast(0.9); + data.parameters[Params::Vhvmax] = static_cast(1.2); + + return data; + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index c32df9f58..790d7048a 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -166,6 +166,30 @@ namespace GridKit return success.report(__func__); } + TestOutcome regca() + { + TestStatus success = true; + + PhasorDynamics::SystemModel* system = new PhasorDynamics::SystemModel(); + + PhasorDynamics::BusInfinite bus; + system->addBus(&bus); + + PhasorDynamics::Converter::Regca regca(&bus); + system->addComponent(®ca); + + success *= system->allocate() == 0; + success *= system->initialize() == 0; + success *= system->evaluateResidual() == 0; + success *= system->evaluateJacobian() == 0; + success *= system->size() == regca.size(); + + delete system; + system = nullptr; + + return success.report(__func__); + } + TestOutcome genrou() { TestStatus success = true; diff --git a/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp new file mode 100644 index 000000000..ae8462396 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp @@ -0,0 +1,16 @@ +#include "ConverterRegcaTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + + GridKit::Testing::ConverterRegcaTests test; + + result += test.constructor(); + result += test.lifecycle(); + result += test.signalVerification(); + result += test.nullBusVerification(); + result += test.jsonParseAndSystemAssembly(); + + return result.summary(); +} diff --git a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp index fc4f07e80..2eb8cecb8 100644 --- a/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSystemSingleComponentTests.cpp @@ -14,6 +14,7 @@ int main() result += test.ieeet1(); result += test.load(); result += test.loadZIP(); + result += test.regca(); result += test.genrou(); result += test.genClassical(); result += test.tgov1(); From 364724170a94571c57e4c51c58586fdb15711ac8 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Sun, 17 May 2026 17:21:56 -0500 Subject: [PATCH 2/3] REGCA Implementation and Tests --- .../PhasorDynamics/Converter/REGCA/README.md | 12 +- .../PhasorDynamics/Converter/REGCA/Regca.hpp | 29 +- .../Converter/REGCA/RegcaData.hpp | 30 +- .../Converter/REGCA/RegcaEnzyme.cpp | 79 +++ .../Converter/REGCA/RegcaImpl.hpp | 290 ++++++++-- GridKit/Model/PhasorDynamics/INPUT_FORMAT.md | 34 +- .../PhasorDynamics/ConverterRegcaTests.hpp | 494 ++++++++++++++++-- .../SystemSingleComponentTests.hpp | 31 +- .../PhasorDynamics/runConverterRegcaTests.cpp | 9 + 9 files changed, 875 insertions(+), 133 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/README.md b/GridKit/Model/PhasorDynamics/Converter/REGCA/README.md index ea352c520..c145165a7 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/README.md +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/README.md @@ -25,7 +25,7 @@ Symbol | Units | Description ---------------------------------|----------|-------------------------------------------------------|---------------|------ $P_{\mathrm{0}}$ | [p.u.] | Initial active power injection | | On system base $Q_{\mathrm{0}}$ | [p.u.] | Initial reactive power injection | | On system base -$S^{\mathrm{conv}}$ | [MVA] | Converter/model power base | TBD | +$S^{\text{base}}$ | [MVA] | REGCA model power base | TBD | JSON key: `mva_base` $T_{\mathrm{g}}$ | [sec] | Converter current-control lag time constant | TBD | $T_M$ | [sec] | Terminal voltage sensor time constant | TBD | Block name: `Tfltr` $R_{\mathrm{q}}^{\max}$ | [p.u./s] | Reactive-current recovery positive rate limit | TBD | Block name: `Iqrmax` @@ -45,7 +45,7 @@ Implementations should reject or report invalid parameter sets: ```math \begin{aligned} - S^{\mathrm{conv}} &> 0 & + S^{\text{base}} &> 0 & T_{\mathrm{g}} &> 0 & T_M &> 0 \\ R_{\mathrm{p}}^{\max} &> 0 & @@ -241,8 +241,8 @@ REGCA currents: ```math \begin{aligned} - I_{\mathrm{r}}^{\mathrm{inj}} &:= I_{\mathrm{r}}\dfrac{S^{\mathrm{conv}}}{S^{\mathrm{sys}}} \\ - I_{\mathrm{i}}^{\mathrm{inj}} &:= I_{\mathrm{i}}\dfrac{S^{\mathrm{conv}}}{S^{\mathrm{sys}}} + I_{\mathrm{r}}^{\mathrm{inj}} &:= I_{\mathrm{r}}\dfrac{S^{\text{base}}}{S^{\text{sys}}} \\ + I_{\mathrm{i}}^{\mathrm{inj}} &:= I_{\mathrm{i}}\dfrac{S^{\text{base}}}{S^{\text{sys}}} \end{aligned} ``` @@ -257,9 +257,9 @@ steady-state initial values: \begin{aligned} V_T &= \sqrt{V_\mathrm{r}^2 + V_\mathrm{i}^2} \\ I_\mathrm{r0} &= \dfrac{P_0 V_\mathrm{r} + Q_0 V_\mathrm{i}}{V_T^2} - \dfrac{S^\mathrm{sys}}{S^\mathrm{conv}} \\ + \dfrac{S^{\text{sys}}}{S^{\text{base}}} \\ I_\mathrm{i0} &= \dfrac{P_0 V_\mathrm{i} - Q_0 V_\mathrm{r}}{V_T^2} - \dfrac{S^\mathrm{sys}}{S^\mathrm{conv}} \\ + \dfrac{S^{\text{sys}}}{S^{\text{base}}} \\ V_{M0} &= V_T \\ I_{L0} &= \text{linseg}(V_T;\ V_{L0},\ V_{L1},\ I_{L1}) \\ I_\mathrm{p0} &= \dfrac{I_\mathrm{r0}} diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp index cbafdc7d8..8366e3cf2 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp @@ -68,12 +68,12 @@ namespace GridKit using Component::J_cols_buffer_; using Component::J_rows_buffer_; using Component::J_vals_buffer_; - using Component::mva_system_base_; using Component::nnz_; using Component::residual_indices_; using Component::size_; using Component::tag_; using Component::time_; + using Component::va_system_base_; using Component::variable_indices_; using Component::wb_; using Component::y_; @@ -117,6 +117,20 @@ namespace GridKit private: void initializeParameters(const model_data_type& data); void initializeMonitor(); + void setDerivedParameters(); + + ScalarT toComponentBase(ScalarT value) const + { + return value * va_system_base_ / va_converter_base_; + } + + ScalarT toSystemBase(ScalarT value) const + { + return value / toComponentBase(static_cast(ONE)); + } + + ScalarT activeCurrentLowerRateBound(ScalarT ip) const; + ScalarT activeCurrentUpperRateBound(ScalarT ip, ScalarT il) const; ScalarT& Vr() { @@ -142,7 +156,7 @@ namespace GridKit RealT P0_{0}; RealT Q0_{0}; - RealT Sconv_{0}; + RealT mva_base_{0}; RealT Tg_{0}; RealT TM_{0}; RealT Rqmax_{0}; @@ -157,6 +171,17 @@ namespace GridKit RealT Vhvmax_{0}; IdxT bus_id_{0}; + IdxT parameter_error_count_{0}; + RealT Mp_{0}; + RealT va_converter_base_{0}; + RealT use_lvpl_{0}; + RealT bypass_lvpl_{1}; + RealT iq_use_upper_{0}; + RealT iq_use_lower_{1}; + + ScalarT ipcmd_set_{0}; + ScalarT iqcmd_set_{0}; + ComponentSignals signals_; std::unique_ptr monitor_; diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp index 29f8530fe..c05700aff 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaData.hpp @@ -17,21 +17,21 @@ namespace GridKit /// Parameter keys for the REGCA converter model. enum class RegcaParameters { - P0, ///< Initial active power injection on system base - Q0, ///< Initial reactive power injection on system base - Sconv, ///< Converter/model power base - Tg, ///< Converter current-control lag time constant - TM, ///< Terminal voltage sensor time constant - Rqmax, ///< Reactive-current recovery positive rate limit - Rqmin, ///< Reactive-current recovery negative rate limit - Rpmax, ///< Active-current magnitude recovery rate limit - sL, ///< LVPL switch - IL1, ///< LVPL upper-current ceiling - VL0, ///< LVPL zero-crossing voltage - VL1, ///< LVPL upper breakpoint voltage - VA0, ///< LVACM lower breakpoint voltage - VA1, ///< LVACM upper breakpoint voltage - Vhvmax ///< Terminal-voltage ceiling for HV reactive management + P0, ///< Initial active power injection on system base + Q0, ///< Initial reactive power injection on system base + mva_base, ///< MVA base of the REGCA model + Tg, ///< Converter current-control lag time constant + TM, ///< Terminal voltage sensor time constant + Rqmax, ///< Reactive-current recovery positive rate limit + Rqmin, ///< Reactive-current recovery negative rate limit + Rpmax, ///< Active-current magnitude recovery rate limit + sL, ///< LVPL switch + IL1, ///< LVPL upper-current ceiling + VL0, ///< LVPL zero-crossing voltage + VL1, ///< LVPL upper breakpoint voltage + VA0, ///< LVACM lower breakpoint voltage + VA1, ///< LVACM upper breakpoint voltage + Vhvmax ///< Terminal-voltage ceiling for HV reactive management }; /// Ports for the REGCA converter model. diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp index 7ad8ee7fc..8bfb167d7 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaEnzyme.cpp @@ -21,6 +21,85 @@ namespace GridKit Log::misc() << "Jacobian evaluation is experimental!" << std::endl; J_.zeroMatrix(); + if (J_rows_buffer_ == nullptr) + { + // Reserve space for a dense internal block. Enzyme's sparse storage + // keeps only structural nonzeros for each differentiated block. + J_rows_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_cols_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_vals_buffer_ = new RealT[static_cast(size_) * static_cast(size_)]; + } + + using RegcaT = GridKit::PhasorDynamics::Converter::Regca; + using Fn = GridKit::Enzyme::Sparse::MemberFunctions; + + GridKit::Enzyme::Sparse::DfDy::eval(this, + f_.size(), + y_.size(), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + alpha_, + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDwb::eval(this, + f_.size(), + static_cast(bus_->size()), + (this->getResidualIndices()).data(), + (bus_->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDws::eval(this, + f_.size(), + ws_.size(), + (this->getResidualIndices()).data(), + ws_indices_.data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DhDy::eval(this, + static_cast(bus_->size()), + y_.size(), + (bus_->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); return 0; } diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp index 6598ea24e..1300adec4 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -46,16 +47,45 @@ namespace GridKit { } + template + void Regca::setDerivedParameters() + { + Mp_ = static_cast(100.0) * Rpmax_; + use_lvpl_ = sL_ ? ONE : ZERO; + bypass_lvpl_ = ONE - use_lvpl_; + iq_use_upper_ = (Q0_ > ZERO) ? ONE : ZERO; + iq_use_lower_ = ONE - iq_use_upper_; + va_converter_base_ = mva_base_ * static_cast(1.0e6); + } + + template + ScalarT Regca::activeCurrentLowerRateBound(ScalarT ip) const + { + return -Rpmax_ - (Mp_ - Rpmax_) * Math::sigmoid(ip); + } + + template + ScalarT Regca::activeCurrentUpperRateBound(ScalarT ip, ScalarT il) const + { + const ScalarT sigma_ip = Math::sigmoid(ip); + return Mp_ * (ONE - sigma_ip) + + Rpmax_ * sigma_ip * (bypass_lvpl_ + use_lvpl_ * Math::sigmoid(il - ip)); + } + template void Regca::initializeParameters(const model_data_type& data) { using Params = typename model_data_type::Parameters; using Ports = typename model_data_type::Ports; - auto loadReal = [&](auto key, RealT& target) + parameter_error_count_ = 0; + + auto loadRequiredReal = [&](auto key, RealT& target, const char* name) { if (!data.parameters.contains(key)) { + Log::error() << "Regca: missing required parameter '" << name << "'\n"; + ++parameter_error_count_; return; } @@ -68,12 +98,19 @@ namespace GridKit { target = static_cast(*index_value); } + else + { + Log::error() << "Regca: parameter '" << name << "' must be numeric\n"; + ++parameter_error_count_; + } }; - auto loadBool = [&](auto key, bool& target) + auto loadRequiredSwitch = [&](auto key, bool& target, const char* name) { if (!data.parameters.contains(key)) { + Log::error() << "Regca: missing required parameter '" << name << "'\n"; + ++parameter_error_count_; return; } @@ -82,32 +119,40 @@ namespace GridKit { target = *bool_value; } - else if (const auto* index_value = std::get_if(&value)) + else if (const auto* index_value = std::get_if(&value); + index_value && (*index_value == 0 || *index_value == 1)) + { + target = (*index_value == 1); + } + else { - target = (*index_value != 0); + Log::error() << "Regca: parameter '" << name << "' must be bool or 0/1\n"; + ++parameter_error_count_; } }; - loadReal(Params::P0, P0_); - loadReal(Params::Q0, Q0_); - loadReal(Params::Sconv, Sconv_); - loadReal(Params::Tg, Tg_); - loadReal(Params::TM, TM_); - loadReal(Params::Rqmax, Rqmax_); - loadReal(Params::Rqmin, Rqmin_); - loadReal(Params::Rpmax, Rpmax_); - loadBool(Params::sL, sL_); - loadReal(Params::IL1, IL1_); - loadReal(Params::VL0, VL0_); - loadReal(Params::VL1, VL1_); - loadReal(Params::VA0, VA0_); - loadReal(Params::VA1, VA1_); - loadReal(Params::Vhvmax, Vhvmax_); + loadRequiredReal(Params::P0, P0_, "P0"); + loadRequiredReal(Params::Q0, Q0_, "Q0"); + loadRequiredReal(Params::mva_base, mva_base_, "mva_base"); + loadRequiredReal(Params::Tg, Tg_, "Tg"); + loadRequiredReal(Params::TM, TM_, "TM"); + loadRequiredReal(Params::Rqmax, Rqmax_, "Rqmax"); + loadRequiredReal(Params::Rqmin, Rqmin_, "Rqmin"); + loadRequiredReal(Params::Rpmax, Rpmax_, "Rpmax"); + loadRequiredSwitch(Params::sL, sL_, "sL"); + loadRequiredReal(Params::IL1, IL1_, "IL1"); + loadRequiredReal(Params::VL0, VL0_, "VL0"); + loadRequiredReal(Params::VL1, VL1_, "VL1"); + loadRequiredReal(Params::VA0, VA0_, "VA0"); + loadRequiredReal(Params::VA1, VA1_, "VA1"); + loadRequiredReal(Params::Vhvmax, Vhvmax_, "Vhvmax"); if (data.ports.contains(Ports::bus)) { bus_id_ = data.ports.at(Ports::bus); } + + setDerivedParameters(); } template @@ -129,10 +174,12 @@ namespace GridKit { return y_[index(RegcaInternalVariables::IR)]; }); monitor_->set(Variable::ii, [this, index] { return y_[index(RegcaInternalVariables::II)]; }); - monitor_->set(Variable::p, [] - { return ScalarT{0}; }); - monitor_->set(Variable::q, [] - { return ScalarT{0}; }); + monitor_->set(Variable::p, [this, index] + { return toSystemBase(Vr() * y_[index(RegcaInternalVariables::IR)] + + Vi() * y_[index(RegcaInternalVariables::II)]); }); + monitor_->set(Variable::q, [this, index] + { return toSystemBase(Vi() * y_[index(RegcaInternalVariables::IR)] + - Vr() * y_[index(RegcaInternalVariables::II)]); }); monitor_->set(Variable::vt, [this, index] { return y_[index(RegcaInternalVariables::VT)]; }); monitor_->set(Variable::vm, [this, index] @@ -190,7 +237,16 @@ namespace GridKit template int Regca::verify() const { - int ret = 0; + int ret = static_cast(parameter_error_count_); + + auto check = [&](bool condition, const char* message) + { + if (!condition) + { + Log::error() << "Regca: " << message << '\n'; + ret += 1; + } + }; if (bus_ == nullptr) { @@ -198,6 +254,16 @@ namespace GridKit ret += 1; } + check(mva_base_ > ZERO, "mva_base must be positive"); + check(Tg_ > ZERO, "Tg must be positive"); + check(TM_ > ZERO, "TM must be positive"); + check(Rpmax_ > ZERO, "Rpmax must be positive"); + check(Rqmin_ < ZERO && ZERO < Rqmax_, "Rqmin < 0 < Rqmax is required"); + check(IL1_ >= ZERO, "IL1 must be non-negative"); + check(ZERO <= VL0_ && VL0_ < VL1_, "VL0/VL1 must satisfy 0 <= VL0 < VL1"); + check(ZERO <= VA0_ && VA0_ < VA1_, "VA0/VA1 must satisfy 0 <= VA0 < VA1"); + check(Vhvmax_ > ZERO, "Vhvmax must be positive"); + if (signals_.template isAttached()) { if (!signals_.template isLinked()) @@ -222,8 +288,87 @@ namespace GridKit template int Regca::initialize() { - std::fill(y_.begin(), y_.end(), ScalarT{0}); - std::fill(yp_.begin(), yp_.end(), ScalarT{0}); + if (bus_ == nullptr) + { + Log::error() << "Regca: cannot initialize with null bus\n"; + return 1; + } + + if (parameter_error_count_ > 0 || mva_base_ <= ZERO || Tg_ <= ZERO + || TM_ <= ZERO || Rpmax_ <= ZERO + || !(Rqmin_ < ZERO && ZERO < Rqmax_) + || IL1_ < ZERO || !(ZERO <= VL0_ && VL0_ < VL1_) + || !(ZERO <= VA0_ && VA0_ < VA1_) || Vhvmax_ <= ZERO) + { + Log::error() << "Regca: cannot initialize with invalid parameters\n"; + return 1; + } + + const auto VM = static_cast(RegcaInternalVariables::VM); + const auto IQ = static_cast(RegcaInternalVariables::IQ); + const auto IP = static_cast(RegcaInternalVariables::IP); + const auto VT = static_cast(RegcaInternalVariables::VT); + const auto II = static_cast(RegcaInternalVariables::II); + const auto IQEXTRA = static_cast(RegcaInternalVariables::IQEXTRA); + const auto IL = static_cast(RegcaInternalVariables::IL); + const auto IR = static_cast(RegcaInternalVariables::IR); + const auto LP = static_cast(RegcaInternalVariables::LP); + const auto UP = static_cast(RegcaInternalVariables::UP); + + const ScalarT vr = Vr(); + const ScalarT vi = Vi(); + const ScalarT vt2 = vr * vr + vi * vi; + const ScalarT vt = std::sqrt(vt2); + + if (vt <= ZERO) + { + Log::error() << "Regca: terminal voltage magnitude must be positive at initialization\n"; + return 1; + } + + if (vt >= Vhvmax_) + { + Log::error() << "Regca: terminal voltage magnitude must be less than Vhvmax at initialization\n"; + return 1; + } + + const ScalarT ir0 = toComponentBase((P0_ * vr + Q0_ * vi) / vt2); + const ScalarT ii0 = toComponentBase((P0_ * vi - Q0_ * vr) / vt2); + + const ScalarT lvacm = Math::linseg(vt, VA0_, VA1_, ONE); + if (ir0 != ZERO && (vt <= VA0_ || lvacm <= ZERO) ) + { + Log::error() << "Regca: LVACM gain is zero with nonzero initial active current\n"; + return 1; + } + + y_[VM] = vt; + y_[VT] = vt; + y_[IL] = Math::linseg(vt, VL0_, VL1_, IL1_); + y_[IR] = ir0; + y_[II] = ii0; + y_[IP] = ir0 / lvacm; + y_[IQEXTRA] = ZERO; + y_[IQ] = -ii0; + y_[LP] = activeCurrentLowerRateBound(y_[IP]); + y_[UP] = activeCurrentUpperRateBound(y_[IP], y_[IL]); + + ipcmd_set_ = y_[IP]; + iqcmd_set_ = y_[IQ]; + + if (signals_.template isAttached() + && signals_.template isLinked()) + { + signals_.template writeExternalVariable(ipcmd_set_); + } + + if (signals_.template isAttached() + && signals_.template isLinked()) + { + signals_.template writeExternalVariable(iqcmd_set_); + } + + std::fill(yp_.begin(), yp_.end(), ZERO); return 0; } @@ -239,51 +384,106 @@ namespace GridKit template __attribute__((always_inline)) inline int Regca::evaluateInternalResidual( - [[maybe_unused]] ScalarT* y, - [[maybe_unused]] ScalarT* yp, - [[maybe_unused]] ScalarT* wb, - [[maybe_unused]] ScalarT* ws, - ScalarT* f) + ScalarT* y, + ScalarT* yp, + ScalarT* wb, + ScalarT* ws, + ScalarT* f) { - for (IdxT i = 0; i < size_; ++i) - { - f[static_cast(i)] = ScalarT{0}; - } + const auto VM = static_cast(RegcaInternalVariables::VM); + const auto IQ = static_cast(RegcaInternalVariables::IQ); + const auto IP = static_cast(RegcaInternalVariables::IP); + const auto VT = static_cast(RegcaInternalVariables::VT); + const auto II = static_cast(RegcaInternalVariables::II); + const auto IQEXTRA = static_cast(RegcaInternalVariables::IQEXTRA); + const auto IL = static_cast(RegcaInternalVariables::IL); + const auto IR = static_cast(RegcaInternalVariables::IR); + const auto LP = static_cast(RegcaInternalVariables::LP); + const auto UP = static_cast(RegcaInternalVariables::UP); + + const auto IPCMD = static_cast(RegcaExternalVariables::IPCMD); + const auto IQCMD = static_cast(RegcaExternalVariables::IQCMD); + + const ScalarT vm = y[VM]; + const ScalarT iq = y[IQ]; + const ScalarT ip = y[IP]; + const ScalarT vt = y[VT]; + const ScalarT ii = y[II]; + const ScalarT iqextra = y[IQEXTRA]; + const ScalarT il = y[IL]; + const ScalarT ir = y[IR]; + const ScalarT lp = y[LP]; + const ScalarT up = y[UP]; + + const ScalarT vm_dot = yp[VM]; + const ScalarT iq_dot = yp[IQ]; + const ScalarT ip_dot = yp[IP]; + + const ScalarT vr = wb[0]; + const ScalarT vi = wb[1]; + + const ScalarT ipcmd = ws[IPCMD]; + const ScalarT iqcmd = ws[IQCMD]; + + const ScalarT fq = (iqcmd - iq) / Tg_; + const ScalarT fp = (ipcmd - ip) / Tg_; + + const ScalarT iq_rate = + iq_use_upper_ * (fq - Math::ramp(fq - Rqmax_)) + + iq_use_lower_ * (fq + Math::ramp(Rqmin_ - fq)); + + const ScalarT ip_rate = lp + Math::ramp(fp - lp) - Math::ramp(fp - up); + + f[VM] = -vm_dot + (vt - vm) / TM_; + f[IQ] = -iq_dot + iq_rate; + f[IP] = -ip_dot + ip_rate; + f[VT] = vt * vt - vr * vr - vi * vi; + f[II] = -ii - iq + iqextra; + f[IQEXTRA] = -iqextra + Math::ramp(iqextra - (Vhvmax_ - vt)); + f[IL] = -il + Math::linseg(vm, VL0_, VL1_, IL1_); + f[IR] = -ir + ip * Math::linseg(vt, VA0_, VA1_, ONE); + f[LP] = -lp + activeCurrentLowerRateBound(ip); + f[UP] = -up + activeCurrentUpperRateBound(ip, il); return 0; } template __attribute__((always_inline)) inline int Regca::evaluateBusResidual( - [[maybe_unused]] ScalarT* y, + ScalarT* y, [[maybe_unused]] ScalarT* yp, [[maybe_unused]] ScalarT* wb, ScalarT* h) { - h[0] = ScalarT{0}; - h[1] = ScalarT{0}; + const auto II = static_cast(RegcaInternalVariables::II); + const auto IR = static_cast(RegcaInternalVariables::IR); + + h[0] = toSystemBase(y[IR]); + h[1] = toSystemBase(y[II]); return 0; } template int Regca::evaluateResidual() { - std::fill(ws_.begin(), ws_.end(), ScalarT{0}); + const auto IPCMD = static_cast(RegcaExternalVariables::IPCMD); + const auto IQCMD = static_cast(RegcaExternalVariables::IQCMD); + + ws_[IPCMD] = ipcmd_set_; + ws_[IQCMD] = iqcmd_set_; std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); if (signals_.template isAttached()) { - const auto index = static_cast(RegcaExternalVariables::IPCMD); - ws_[index] = signals_.template readExternalVariable(); - ws_indices_[index] = + ws_[IPCMD] = signals_.template readExternalVariable(); + ws_indices_[IPCMD] = signals_.template readExternalVariableIndex(); } if (signals_.template isAttached()) { - const auto index = static_cast(RegcaExternalVariables::IQCMD); - ws_[index] = signals_.template readExternalVariable(); - ws_indices_[index] = + ws_[IQCMD] = signals_.template readExternalVariable(); + ws_indices_[IQCMD] = signals_.template readExternalVariableIndex(); } diff --git a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index fdbc7cd3a..93a675d5b 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -144,7 +144,7 @@ are specified: `Genrou` | 6th order machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqop`, `Tqopp`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xqp`, `Xqpp`, `Xl`, `S10`, `S12`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed` `Gensal` | 5th order salient-pole machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Tdop`, `Tdopp`, `Tqopp`, `Xd`, `Xdp`, `Xdpp`, `Xq`, `Xl`, `S10`, `S12`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega`, `speed`, `Eqp`, `psidp`, `psiqpp`, `psidpp`, `vd`, `vq`, `te`, `id`, `iq` `GenClassical`| the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva_base` | `ir`, `ii`, `p`, `q`, `delta`, `omega` - `Regca` | WECC REGCA renewable generator/converter skeleton | `bus`, `ipcmd`\*, `iqcmd`\* | `P0`, `Q0`, `Sconv`, `Tg`, `TM`, `Rqmax`, `Rqmin`, `Rpmax`, `sL`, `IL1`, `VL0`, `VL1`, `VA0`, `VA1`, `Vhvmax` | `ir`, `ii`, `p`, `q`, `vt`, `vm`, `ip`, `iq`, `iqextra`, `il`, `lp`, `up` + `Regca` | WECC REGCA renewable generator/converter model | `bus`, `ipcmd`\*, `iqcmd`\* | `P0`, `Q0`, `mva_base`, `Tg`, `TM`, `Rqmax`, `Rqmin`, `Rpmax`, `sL`, `IL1`, `VL0`, `VL1`, `VA0`, `VA1`, `Vhvmax` | `ir`, `ii`, `p`, `q`, `vt`, `vm`, `ip`, `iq`, `iqextra`, `il`, `lp`, `up` `Tgov1 ` | the TGOV1 governor model | `pmech`, `speed` | `R`, `T1`, `T2`, `T3`, `Pvmax`, `Pvmin`, `Dt` | `none` `Ieeet1` | the IEEET1 exciter model | `bus`, `speed`, `efd`, `vs`\* | `Tr`, `Ka`, `Ta`, `Ke`, `Te`, `Kf`, `Tf`, `Vrmin`, `Vrmax`, `E1`, `E2`, `Se1`, `Se2`, `Ispdlim` | `efd`, `ksat` `SexsPti` | the SEXS-PTI simplified exciter model | `bus`, `efd`, `vs`\* | `Ta`, `Tb`, `Te`, `K`, `Efdmax`, `Efdmin` | `efd` @@ -154,38 +154,6 @@ are specified: Ports marked with \* are optional and, if missing, will be assumed to be connected to a constant value. This list is subject to change. -For `Regca`, `ipcmd` and `iqcmd` are optional in the skeleton implementation -and default to constant zero command inputs when omitted. - -Compact `Regca` device example: - -```json -{ - "class": "Regca", - "ports": { "bus": 1, "ipcmd": 10, "iqcmd": 11 }, - "id": "CV1", - "params": { - "P0": 1.0, - "Q0": 0.0, - "Sconv": 100.0, - "Tg": 0.02, - "TM": 0.02, - "Rqmax": 999.0, - "Rqmin": -999.0, - "Rpmax": 999.0, - "sL": true, - "IL1": 1.1, - "VL0": 0.4, - "VL1": 0.9, - "VA0": 0.4, - "VA1": 0.9, - "Vhvmax": 1.2 - }, - "mon": ["ir", "ii"] -} -``` - - ## Example File for a 2-Bus System ```json diff --git a/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp index f74da86e6..99fec8044 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp @@ -1,10 +1,12 @@ #pragma once +#include #include -#include #include #include +#include +#include #include #include #include @@ -14,6 +16,7 @@ #include #include #include +#include namespace GridKit { @@ -28,7 +31,7 @@ namespace GridKit ConverterRegcaTests() = default; ~ConverterRegcaTests() = default; - static constexpr ScalarT kTol = static_cast(1.0e-14); + static constexpr ScalarT kTol = static_cast(1.0e-10); TestOutcome constructor() { @@ -39,11 +42,42 @@ namespace GridKit PhasorDynamics::Converter::Regca minimal(&bus); success *= (minimal.size() == static_cast(PhasorDynamics::Converter::RegcaInternalVariables::MAXIMUM)); success *= (minimal.getMonitor() == nullptr); + success *= (minimal.verify() > 0); auto data = makeTestData(); PhasorDynamics::Converter::Regca from_data(&bus, data); success *= (from_data.size() == static_cast(PhasorDynamics::Converter::RegcaInternalVariables::MAXIMUM)); success *= (from_data.getMonitor() != nullptr); + success *= (from_data.verify() == 0); + + return success.report(__func__); + } + + TestOutcome parameterValidation() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + + auto missing = makeTestData(); + missing.parameters.erase(PhasorDynamics::Converter::RegcaParameters::Tg); + PhasorDynamics::Converter::Regca missing_model(&bus, missing); + success *= (missing_model.verify() > 0); + + auto bad_switch = makeTestData(); + bad_switch.parameters[PhasorDynamics::Converter::RegcaParameters::sL] = static_cast(2); + PhasorDynamics::Converter::Regca bad_switch_model(&bus, bad_switch); + success *= (bad_switch_model.verify() > 0); + + success *= invalidParameterCase(bus, Params::mva_base, static_cast(0.0)); + success *= invalidParameterCase(bus, Params::Tg, static_cast(0.0)); + success *= invalidParameterCase(bus, Params::TM, static_cast(0.0)); + success *= invalidParameterCase(bus, Params::Rpmax, static_cast(0.0)); + success *= invalidParameterCase(bus, Params::Rqmin, static_cast(0.0)); + success *= invalidParameterCase(bus, Params::IL1, static_cast(-0.1)); + success *= invalidParameterCase(bus, Params::VL1, static_cast(0.3)); + success *= invalidParameterCase(bus, Params::VA1, static_cast(0.3)); + success *= invalidParameterCase(bus, Params::Vhvmax, static_cast(0.0)); return success.report(__func__); } @@ -65,14 +99,10 @@ namespace GridKit success *= (regca.evaluateResidual() == 0); success *= (regca.evaluateJacobian() == 0); - const auto& y = regca.y(); - const auto& yp = regca.yp(); - const auto& f = regca.getResidual(); for (size_t i = 0; i < static_cast(regca.size()); ++i) { - success *= isEqual(y[i], static_cast(0.0), kTol); - success *= isEqual(yp[i], static_cast(0.0), kTol); - success *= isEqual(f[i], static_cast(0.0), kTol); + success *= isEqual(regca.yp()[i], static_cast(0.0), kTol); + success *= isEqual(regca.getResidual()[i], static_cast(0.0), kTol); } using Vars = PhasorDynamics::Converter::RegcaInternalVariables; @@ -84,12 +114,102 @@ namespace GridKit success *= (regca.tag()[i] == expected); } - success *= isEqual(bus.Ir(), static_cast(0.0), kTol); + success *= isEqual(bus.Ir(), static_cast(1.0), kTol); success *= isEqual(bus.Ii(), static_cast(0.0), kTol); return success.report(__func__); } + TestOutcome steadyStateInitializationGolden() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(0.8, 0.6); + bus.allocate(); + bus.initialize(); + + auto data = makeGoldenTestData(static_cast(0.2), true); + + PhasorDynamics::Converter::Regca regca(&bus, data); + regca.allocate(); + success *= (regca.initialize() == 0); + + const std::vector expected_y = { + static_cast(1.0), + static_cast(-0.44), + static_cast(0.92), + static_cast(1.0), + static_cast(0.44), + static_cast(0.0), + static_cast(1.1), + static_cast(0.92), + static_cast(-70.0), + static_cast(0.7)}; + + success *= vectorMatches(regca.y(), expected_y, "REGCA initialization state"); + for (size_t i = 0; i < static_cast(regca.size()); ++i) + { + success *= isEqual(regca.yp()[i], static_cast(0.0), kTol); + } + + return success.report(__func__); + } + + TestOutcome attachedSignalInitialization() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + bus.allocate(); + bus.initialize(); + + auto data = makeTestData(); + PhasorDynamics::Converter::Regca regca(&bus, data); + + PhasorDynamics::SignalNode ipcmd_node; + PhasorDynamics::SignalNode iqcmd_node; + ScalarT ipcmd_value{0.0}; + ScalarT iqcmd_value{0.0}; + IdxT ipcmd_index = 21; + IdxT iqcmd_index = 22; + + ipcmd_node.set(&ipcmd_value, &ipcmd_index); + iqcmd_node.set(&iqcmd_value, &iqcmd_index); + regca.getSignals().template attachSignalNode(&ipcmd_node); + regca.getSignals().template attachSignalNode(&iqcmd_node); + + regca.allocate(); + success *= (regca.initialize() == 0); + + success *= isEqual(ipcmd_value, static_cast(1.0), kTol); + success *= isEqual(iqcmd_value, static_cast(0.0), kTol); + + return success.report(__func__); + } + + TestOutcome invalidInitialization() + { + TestStatus success = true; + + auto data = makeTestData(); + + PhasorDynamics::Bus high_voltage_bus(1.3, 0.0); + high_voltage_bus.allocate(); + high_voltage_bus.initialize(); + PhasorDynamics::Converter::Regca high_voltage_regca(&high_voltage_bus, data); + high_voltage_regca.allocate(); + success *= (high_voltage_regca.initialize() > 0); + + PhasorDynamics::Bus low_voltage_bus(0.2, 0.0); + low_voltage_bus.allocate(); + low_voltage_bus.initialize(); + PhasorDynamics::Converter::Regca low_voltage_regca(&low_voltage_bus, data); + low_voltage_regca.allocate(); + success *= (low_voltage_regca.initialize() > 0); + + return success.report(__func__); + } + TestOutcome signalVerification() { TestStatus success = true; @@ -123,12 +243,72 @@ namespace GridKit { TestStatus success = true; - PhasorDynamics::Converter::Regca regca(nullptr); + PhasorDynamics::Converter::Regca regca(nullptr, makeTestData()); success *= (regca.verify() > 0); return success.report(__func__); } + TestOutcome residualGoldenVectors() + { + TestStatus success = true; + + const std::vector positive_q_lvpl = { + static_cast(0.29), + static_cast(0.52), + static_cast(0.21999997439919405), + static_cast(-0.0046000000000000485), + static_cast(0.05000000000000002), + static_cast(-0.03), + static_cast(0.29199937917427254), + static_cast(0.3499999999675074), + static_cast(-69.6), + static_cast(-0.2999999999999803)}; + + const std::vector nonpositive_q_no_lvpl = { + static_cast(0.29), + static_cast(1.5200000000000002), + static_cast(0.21999997439919405), + static_cast(-0.0046000000000000485), + static_cast(0.05000000000000002), + static_cast(-0.03), + static_cast(0.29199937917427254), + static_cast(0.3499999999675074), + static_cast(-69.6), + static_cast(0.39999999999999997)}; + + success *= residualGoldenVectorCase(static_cast(0.2), true, positive_q_lvpl); + success *= residualGoldenVectorCase(static_cast(-0.2), false, nonpositive_q_no_lvpl); + + return success.report(__func__); + } + + TestOutcome busInjection() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(0.8, 0.6); + bus.allocate(); + bus.initialize(); + + auto data = makeTestData(); + data.parameters[PhasorDynamics::Converter::RegcaParameters::P0] = static_cast(0.8); + data.parameters[PhasorDynamics::Converter::RegcaParameters::Q0] = static_cast(-0.1); + data.parameters[PhasorDynamics::Converter::RegcaParameters::mva_base] = static_cast(50.0); + + PhasorDynamics::Converter::Regca regca(&bus, data); + regca.allocate(); + success *= (regca.initialize() == 0); + + bus.evaluateResidual(); + success *= (regca.evaluateResidual() == 0); + + success *= isEqual(bus.Ir(), static_cast(0.58), kTol); + success *= isEqual(bus.Ii(), static_cast(0.56), kTol); + + return success.report(__func__); + } + TestOutcome jsonParseAndSystemAssembly() { TestStatus success = true; @@ -138,8 +318,8 @@ namespace GridKit "header": { "format_version": 0, "format_revision": 1, - "case_name": "REGCA skeleton", - "case_description": "REGCA parser smoke test", + "case_name": "REGCA full model", + "case_description": "REGCA parser behavior test", "case_comments": "", "freq_base": 60.0, "va_base": 100000000.0 @@ -155,7 +335,7 @@ namespace GridKit "params": { "P0": 1.0, "Q0": 0.0, - "Sconv": 100, + "mva_base": 100, "Tg": 0.02, "TM": 0.02, "Rqmax": 999.0, @@ -169,7 +349,7 @@ namespace GridKit "VA1": 0.9, "Vhvmax": 1.2 }, - "mon": ["ir", "ii"] + "mon": ["ir", "ii", "p", "q"] } ] } @@ -179,7 +359,7 @@ namespace GridKit success *= (data.regca.size() == 1); success *= (data.regca[0].device_class == "Regca"); success *= (data.regca[0].ports.at(PhasorDynamics::Converter::RegcaPorts::bus) == 1); - success *= (std::get_if(&data.regca[0].parameters.at(PhasorDynamics::Converter::RegcaParameters::Sconv)) != nullptr); + success *= (std::get_if(&data.regca[0].parameters.at(PhasorDynamics::Converter::RegcaParameters::mva_base)) != nullptr); success *= (std::get_if(&data.regca[0].parameters.at(PhasorDynamics::Converter::RegcaParameters::sL)) != nullptr); PhasorDynamics::SystemModel system(data); @@ -189,18 +369,49 @@ namespace GridKit success *= (system.evaluateResidual() == 0); success *= (system.evaluateJacobian() == 0); success *= (system.size() == 12); - success *= isEqual(system.getResidual()[0], 0.0, static_cast(kTol)); + success *= isEqual(system.getResidual()[0], 1.0, static_cast(kTol)); success *= isEqual(system.getResidual()[1], 0.0, static_cast(kTol)); + for (size_t i = 2; i < system.getResidual().size(); ++i) + { + success *= isEqual(system.getResidual()[i], 0.0, static_cast(kTol)); + } + + return success.report(__func__); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome jacobian() + { + TestStatus success = true; + + auto dependency_tracking_jacobian = DependencyTrackingJacobian(); + auto enzyme_jacobian = EnzymeJacobian(); + + success *= (dependency_tracking_jacobian.size() == enzyme_jacobian.size()); + const auto nrows = std::min(dependency_tracking_jacobian.size(), enzyme_jacobian.size()); + for (size_t i = 0; i < nrows; ++i) + { + success *= isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], static_cast(1.0e-8)); + } return success.report(__func__); } +#endif private: + using Params = PhasorDynamics::Converter::RegcaParameters; + using Vars = PhasorDynamics::Converter::RegcaInternalVariables; + using Ext = PhasorDynamics::Converter::RegcaExternalVariables; + + static size_t index(Vars variable) + { + return static_cast(variable); + } + auto makeTestData() -> PhasorDynamics::Converter::RegcaData { - using Params = PhasorDynamics::Converter::RegcaParameters; - using Ports = PhasorDynamics::Converter::RegcaPorts; - using Mon = PhasorDynamics::Converter::RegcaMonitorableVariables; + using Ports = PhasorDynamics::Converter::RegcaPorts; + using Mon = PhasorDynamics::Converter::RegcaMonitorableVariables; PhasorDynamics::Converter::RegcaData data; data.device_class = "Regca"; @@ -209,24 +420,247 @@ namespace GridKit data.monitored_variables.insert(Mon::ir); data.monitored_variables.insert(Mon::ii); - data.parameters[Params::P0] = static_cast(1.0); - data.parameters[Params::Q0] = static_cast(0.0); - data.parameters[Params::Sconv] = static_cast(100); - data.parameters[Params::Tg] = static_cast(0.02); - data.parameters[Params::TM] = static_cast(0.02); - data.parameters[Params::Rqmax] = static_cast(999.0); - data.parameters[Params::Rqmin] = static_cast(-999.0); - data.parameters[Params::Rpmax] = static_cast(999.0); - data.parameters[Params::sL] = true; + data.parameters[Params::P0] = static_cast(1.0); + data.parameters[Params::Q0] = static_cast(0.0); + data.parameters[Params::mva_base] = static_cast(100); + data.parameters[Params::Tg] = static_cast(0.02); + data.parameters[Params::TM] = static_cast(0.02); + data.parameters[Params::Rqmax] = static_cast(999.0); + data.parameters[Params::Rqmin] = static_cast(-999.0); + data.parameters[Params::Rpmax] = static_cast(999.0); + data.parameters[Params::sL] = true; + data.parameters[Params::IL1] = static_cast(1.1); + data.parameters[Params::VL0] = static_cast(0.4); + data.parameters[Params::VL1] = static_cast(0.9); + data.parameters[Params::VA0] = static_cast(0.4); + data.parameters[Params::VA1] = static_cast(0.9); + data.parameters[Params::Vhvmax] = static_cast(1.2); + + return data; + } + + auto makeGoldenTestData(RealT q0, bool use_lvpl) -> PhasorDynamics::Converter::RegcaData + { + auto data = makeTestData(); + data.parameters[Params::Q0] = q0; + data.parameters[Params::Tg] = static_cast(0.2); + data.parameters[Params::TM] = static_cast(0.4); + data.parameters[Params::Rqmax] = static_cast(0.5); + data.parameters[Params::Rqmin] = static_cast(-0.6); + data.parameters[Params::Rpmax] = static_cast(0.7); + data.parameters[Params::sL] = use_lvpl; data.parameters[Params::IL1] = static_cast(1.1); data.parameters[Params::VL0] = static_cast(0.4); data.parameters[Params::VL1] = static_cast(0.9); data.parameters[Params::VA0] = static_cast(0.4); data.parameters[Params::VA1] = static_cast(0.9); - data.parameters[Params::Vhvmax] = static_cast(1.2); - + data.parameters[Params::Vhvmax] = static_cast(1.3); return data; } + + bool invalidParameterCase(PhasorDynamics::Bus& bus, Params param, RealT value) + { + auto data = makeTestData(); + data.parameters[param] = value; + PhasorDynamics::Converter::Regca model(&bus, data); + return model.verify() > 0; + } + + bool vectorMatches(const std::vector& actual, + const std::vector& expected, + const char* label) const + { + bool success = (actual.size() == expected.size()); + const auto n = std::min(actual.size(), expected.size()); + for (size_t i = 0; i < n; ++i) + { + if (!isEqual(actual[i], expected[i], kTol)) + { + std::cout << label << " mismatch at row " << i << ": " + << actual[i] << " != " << expected[i] << "\n"; + success = false; + } + } + return success; + } + + void setGoldenResidualState(PhasorDynamics::Converter::Regca& regca) + { + regca.y()[index(Vars::VM)] = static_cast(0.86); + regca.y()[index(Vars::IQ)] = static_cast(-0.2); + regca.y()[index(Vars::IP)] = static_cast(0.85); + regca.y()[index(Vars::VT)] = static_cast(0.98); + regca.y()[index(Vars::II)] = static_cast(0.18); + regca.y()[index(Vars::IQEXTRA)] = static_cast(0.03); + regca.y()[index(Vars::IL)] = static_cast(0.72); + regca.y()[index(Vars::IR)] = static_cast(0.5); + regca.y()[index(Vars::LP)] = static_cast(-0.4); + regca.y()[index(Vars::UP)] = static_cast(0.3); + + regca.yp()[index(Vars::VM)] = static_cast(0.01); + regca.yp()[index(Vars::IQ)] = static_cast(-0.02); + regca.yp()[index(Vars::IP)] = static_cast(0.03); + } + + bool residualGoldenVectorCase(RealT q0, bool use_lvpl, const std::vector& expected) + { + bool success = true; + + PhasorDynamics::Bus bus(0.95, 0.25); + bus.allocate(); + bus.initialize(); + + auto data = makeGoldenTestData(q0, use_lvpl); + PhasorDynamics::Converter::Regca regca(&bus, data); + regca.allocate(); + + setGoldenResidualState(regca); + + PhasorDynamics::SignalNode ipcmd_node; + PhasorDynamics::SignalNode iqcmd_node; + ScalarT ipcmd_value{0.9}; + ScalarT iqcmd_value{0.1}; + IdxT ipcmd_index = 21; + IdxT iqcmd_index = 22; + ipcmd_node.set(&ipcmd_value, &ipcmd_index); + iqcmd_node.set(&iqcmd_value, &iqcmd_index); + regca.getSignals().template attachSignalNode(&ipcmd_node); + regca.getSignals().template attachSignalNode(&iqcmd_node); + + bus.evaluateResidual(); + regca.evaluateResidual(); + + success *= vectorMatches(regca.getResidual(), expected, "REGCA residual"); + + return success; + } + +#ifdef GRIDKIT_ENABLE_ENZYME + void setJacobianState(PhasorDynamics::Converter::Regca& regca, + PhasorDynamics::Bus& bus) + { + bus.y()[0] = static_cast(0.95); + bus.y()[1] = static_cast(0.25); + + setGoldenResidualState(regca); + } + + void setJacobianStateDep( + PhasorDynamics::Converter::Regca& regca, + PhasorDynamics::Bus& bus) + { + bus.y()[0].setValue(0.95); + bus.y()[1].setValue(0.25); + + regca.y()[index(Vars::VM)].setValue(0.86); + regca.y()[index(Vars::IQ)].setValue(-0.2); + regca.y()[index(Vars::IP)].setValue(0.85); + regca.y()[index(Vars::VT)].setValue(0.98); + regca.y()[index(Vars::II)].setValue(0.18); + regca.y()[index(Vars::IQEXTRA)].setValue(0.03); + regca.y()[index(Vars::IL)].setValue(0.72); + regca.y()[index(Vars::IR)].setValue(0.5); + regca.y()[index(Vars::LP)].setValue(-0.4); + regca.y()[index(Vars::UP)].setValue(0.3); + + regca.yp()[index(Vars::VM)].setValue(0.01); + regca.yp()[index(Vars::IQ)].setValue(-0.02); + regca.yp()[index(Vars::IP)].setValue(0.03); + } + + std::vector DependencyTrackingJacobian() + { + using DepVar = DependencyTracking::Variable; + + auto data = makeGoldenTestData(static_cast(0.2), true); + + PhasorDynamics::Bus bus(DepVar{0.95}, DepVar{0.25}); + PhasorDynamics::Converter::Regca regca(&bus, data); + + PhasorDynamics::SignalNode ipcmd_node; + PhasorDynamics::SignalNode iqcmd_node; + DepVar ipcmd_value{0.9}; + DepVar iqcmd_value{0.1}; + IdxT ipcmd_index = static_cast(regca.size() + bus.size()); + IdxT iqcmd_index = static_cast(regca.size() + bus.size() + 1); + + bus.allocate(); + regca.allocate(); + bus.initialize(); + setJacobianStateDep(regca, bus); + + for (IdxT i = 0; i < regca.size(); ++i) + { + regca.y()[static_cast(i)].setVariableNumber(i); + regca.yp()[static_cast(i)].setVariableNumber(i); + } + for (IdxT i = 0; i < bus.size(); ++i) + { + bus.y()[static_cast(i)].setVariableNumber(i + regca.size()); + } + ipcmd_value.setVariableNumber(ipcmd_index); + iqcmd_value.setVariableNumber(iqcmd_index); + + ipcmd_node.set(&ipcmd_value, &ipcmd_index); + iqcmd_node.set(&iqcmd_value, &iqcmd_index); + regca.getSignals().template attachSignalNode(&ipcmd_node); + regca.getSignals().template attachSignalNode(&iqcmd_node); + + bus.evaluateResidual(); + regca.evaluateResidual(); + + std::vector dependencies( + static_cast(regca.size() + bus.size())); + for (IdxT i = 0; i < regca.size(); ++i) + { + dependencies[static_cast(i)] = regca.getResidual()[static_cast(i)].getDependencies(); + } + dependencies[static_cast(regca.size())] = bus.Ir().getDependencies(); + dependencies[static_cast(regca.size() + 1)] = bus.Ii().getDependencies(); + + return dependencies; + } + + std::vector EnzymeJacobian() + { + auto data = makeGoldenTestData(static_cast(0.2), true); + + PhasorDynamics::Bus bus(0.95, 0.25); + PhasorDynamics::Converter::Regca regca(&bus, data); + + PhasorDynamics::SignalNode ipcmd_node; + PhasorDynamics::SignalNode iqcmd_node; + ScalarT ipcmd_value{0.9}; + ScalarT iqcmd_value{0.1}; + IdxT ipcmd_index = static_cast(regca.size() + bus.size()); + IdxT iqcmd_index = static_cast(regca.size() + bus.size() + 1); + + bus.allocate(); + regca.allocate(); + for (IdxT i = 0; i < bus.size(); ++i) + { + bus.setVariableIndex(i, i + regca.size()); + bus.setResidualIndex(i, i + regca.size()); + } + + bus.initialize(); + setJacobianState(regca, bus); + regca.updateTime(0.0, 1.0); + + ipcmd_node.set(&ipcmd_value, &ipcmd_index); + iqcmd_node.set(&iqcmd_value, &iqcmd_index); + regca.getSignals().template attachSignalNode(&ipcmd_node); + regca.getSignals().template attachSignalNode(&iqcmd_node); + + bus.evaluateResidual(); + regca.evaluateResidual(); + regca.evaluateJacobian(); + + auto model_jacobian = regca.getJacobian(); + model_jacobian.deduplicate(); + return MapFromCOO(model_jacobian); + } +#endif }; } // namespace Testing } // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 790d7048a..80cab45a4 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -172,10 +172,10 @@ namespace GridKit PhasorDynamics::SystemModel* system = new PhasorDynamics::SystemModel(); - PhasorDynamics::BusInfinite bus; + PhasorDynamics::BusInfinite bus(1.0, 0.0); system->addBus(&bus); - PhasorDynamics::Converter::Regca regca(&bus); + PhasorDynamics::Converter::Regca regca(&bus, makeRegcaData()); system->addComponent(®ca); success *= system->allocate() == 0; @@ -190,6 +190,33 @@ namespace GridKit return success.report(__func__); } + private: + auto makeRegcaData() -> PhasorDynamics::Converter::RegcaData + { + using Params = PhasorDynamics::Converter::RegcaParameters; + + PhasorDynamics::Converter::RegcaData data; + data.device_class = "Regca"; + data.disambiguation_string = "regca_test"; + data.parameters[Params::P0] = static_cast(1.0); + data.parameters[Params::Q0] = static_cast(0.0); + data.parameters[Params::mva_base] = static_cast(100.0); + data.parameters[Params::Tg] = static_cast(0.02); + data.parameters[Params::TM] = static_cast(0.02); + data.parameters[Params::Rqmax] = static_cast(999.0); + data.parameters[Params::Rqmin] = static_cast(-999.0); + data.parameters[Params::Rpmax] = static_cast(999.0); + data.parameters[Params::sL] = true; + data.parameters[Params::IL1] = static_cast(1.1); + data.parameters[Params::VL0] = static_cast(0.4); + data.parameters[Params::VL1] = static_cast(0.9); + data.parameters[Params::VA0] = static_cast(0.4); + data.parameters[Params::VA1] = static_cast(0.9); + data.parameters[Params::Vhvmax] = static_cast(1.2); + return data; + } + + public: TestOutcome genrou() { TestStatus success = true; diff --git a/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp b/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp index ae8462396..444cee1cf 100644 --- a/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runConverterRegcaTests.cpp @@ -7,10 +7,19 @@ int main() GridKit::Testing::ConverterRegcaTests test; result += test.constructor(); + result += test.parameterValidation(); result += test.lifecycle(); + result += test.steadyStateInitializationGolden(); + result += test.attachedSignalInitialization(); + result += test.invalidInitialization(); result += test.signalVerification(); result += test.nullBusVerification(); + result += test.residualGoldenVectors(); + result += test.busInjection(); result += test.jsonParseAndSystemAssembly(); +#ifdef GRIDKIT_ENABLE_ENZYME + result += test.jacobian(); +#endif return result.summary(); } From 75cbc714f98bc6aa0e679496eac7127b93e19efd Mon Sep 17 00:00:00 2001 From: lukelowry Date: Tue, 26 May 2026 14:15:03 -0500 Subject: [PATCH 3/3] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51bec72ed..1a169e51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ - Added node objects to `PowerElectronics` module & updated all examples to make use of them. - Separated internal and external residuals of `PowerElectronics` models. - Added `CliArgs` class for better management of command-line options. +- Added `REGCA` converter model implementation for PhasorDynamics. ## v0.1