diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Stabilizer/CMakeLists.txt index e0d818124..5abcdb07a 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Stabilizer/CMakeLists.txt @@ -1 +1,16 @@ +# [[ +# Author(s): +# - Luke Lowery +#]] + +gridkit_add_library( + phasor_dynamics_stabilizer + INTERFACE_TARGET + HEADERS StabilizerFactory.hpp + LINK_LIBRARIES INTERFACE GridKit::phasor_dynamics_core) + +target_link_libraries( + phasor_dynamics_components + INTERFACE GridKit::phasor_dynamics_stabilizer) + add_subdirectory(IEEEST) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp index 045fac991..8fb8b5bd4 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp @@ -17,8 +17,8 @@ namespace GridKit * * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { Log::misc() << "Evaluate Jacobian for Ieeest..." << std::endl; Log::misc() << "Jacobian evaluation not implemented!" << std::endl; @@ -26,8 +26,16 @@ namespace GridKit } // Available template instantiations - template class Ieeest; - template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index 4ef2dcb37..baf136cfa 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -6,6 +6,12 @@ #pragma once +#include +#include +#include +#include + +#include #include #include #include @@ -32,24 +38,164 @@ namespace GridKit { namespace Stabilizer { - /// Internal variables of a `Ieeest` - enum class IeeestInternalVariables : size_t + /** + * @brief Combined denominator coefficients of the IEEEST notch filter. + * + * The two second-order denominator factors expand into a single quartic + * with coefficients a1..a4. + */ + template + inline std::array notchCoefficients(real_type A1, + real_type A2, + real_type A3, + real_type A4) { - X1, ///< Notch filter state 1 - X2, ///< Notch filter state 2 - X3, ///< Notch filter state 3 - X4, ///< Notch filter state 4 - X5, ///< Lead-lag 1 state - X6, ///< Lead-lag 2 state - X7, ///< Washout state - V4, ///< Notch filter output - V5, ///< Lead-lag 1 output - V6, ///< Lead-lag 2 output - V7, ///< Unlimited stabilizer signal - VSS, ///< Limited stabilizer signal (model output) - MAXIMUM, + return {A1 + A3, + A2 + A4 + A1 * A3, + A1 * A4 + A2 * A3, + A2 * A4}; + } + + /** + * @brief Notch-filter order implied by the combined denominator + * coefficients. + * + * Structural coefficients are exact data-file literals, so the + * comparisons against zero are intentionally exact. + */ + template + inline size_t notchOrder(real_type a1, real_type a2, real_type a3, real_type a4) + { + size_t order = 0; + + if (a1 != ZERO) + { + order = 1; + } + if (a2 != ZERO) + { + order = 2; + } + if (a3 != ZERO) + { + order = 3; + } + if (a4 != ZERO) + { + order = 4; + } + + return order; + } + + /// Internal variable layout of a `Ieeest` by notch-filter order + template + struct IeeestVariables; + + template <> + struct IeeestVariables<0> + { + /// Internal variables of a zeroth-order `Ieeest` + enum class InternalVariables : size_t + { + X5, ///< Lead-lag 1 state + X6, ///< Lead-lag 2 state + X7, ///< Washout state + V4, ///< Notch filter output + V5, ///< Lead-lag 1 output + V6, ///< Lead-lag 2 output + V7, ///< Unlimited stabilizer signal + VSS, ///< Limited stabilizer signal (model output) + MAXIMUM, + }; + }; + + template <> + struct IeeestVariables<1> + { + /// Internal variables of a first-order `Ieeest` + enum class InternalVariables : size_t + { + X1, ///< Notch filter state 1 + X5, ///< Lead-lag 1 state + X6, ///< Lead-lag 2 state + X7, ///< Washout state + V4, ///< Notch filter output + V5, ///< Lead-lag 1 output + V6, ///< Lead-lag 2 output + V7, ///< Unlimited stabilizer signal + VSS, ///< Limited stabilizer signal (model output) + MAXIMUM, + }; + }; + + template <> + struct IeeestVariables<2> + { + /// Internal variables of a second-order `Ieeest` + enum class InternalVariables : size_t + { + X1, ///< Notch filter state 1 + X2, ///< Notch filter state 2 + X5, ///< Lead-lag 1 state + X6, ///< Lead-lag 2 state + X7, ///< Washout state + V4, ///< Notch filter output + V5, ///< Lead-lag 1 output + V6, ///< Lead-lag 2 output + V7, ///< Unlimited stabilizer signal + VSS, ///< Limited stabilizer signal (model output) + MAXIMUM, + }; }; + template <> + struct IeeestVariables<3> + { + /// Internal variables of a third-order `Ieeest` + enum class InternalVariables : size_t + { + X1, ///< Notch filter state 1 + X2, ///< Notch filter state 2 + X3, ///< Notch filter state 3 + X5, ///< Lead-lag 1 state + X6, ///< Lead-lag 2 state + X7, ///< Washout state + V4, ///< Notch filter output + V5, ///< Lead-lag 1 output + V6, ///< Lead-lag 2 output + V7, ///< Unlimited stabilizer signal + VSS, ///< Limited stabilizer signal (model output) + MAXIMUM, + }; + }; + + template <> + struct IeeestVariables<4> + { + /// Internal variables of a fourth-order `Ieeest` + enum class InternalVariables : size_t + { + X1, ///< Notch filter state 1 + X2, ///< Notch filter state 2 + X3, ///< Notch filter state 3 + X4, ///< Notch filter state 4 + X5, ///< Lead-lag 1 state + X6, ///< Lead-lag 2 state + X7, ///< Washout state + V4, ///< Notch filter output + V5, ///< Lead-lag 1 output + V6, ///< Lead-lag 2 output + V7, ///< Unlimited stabilizer signal + VSS, ///< Limited stabilizer signal (model output) + MAXIMUM, + }; + }; + + /// Internal variables of a `Ieeest` of the given notch-filter order + template + using IeeestInternalVariables = typename IeeestVariables::InternalVariables; + /// External variables of a `Ieeest` enum class IeeestExternalVariables : size_t { @@ -57,9 +203,11 @@ namespace GridKit MAXIMUM, }; - template + template class Ieeest : public Component { + static_assert(order <= 4, "Ieeest notch filter order must be in [0, 4]"); + using Component::gridkit_component_id_; using Component::alpha_; using Component::f_; @@ -71,7 +219,6 @@ namespace GridKit using Component::y_; using Component::yp_; using Component::wb_; - using Component::h_; using Component::J_rows_buffer_; using Component::J_cols_buffer_; using Component::J_vals_buffer_; @@ -103,7 +250,7 @@ namespace GridKit auto getSignals() -> ComponentSignals, IeeestExternalVariables>& { return signals_; @@ -119,6 +266,8 @@ namespace GridKit ScalarT*); private: + static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); + RealT A1_{0}; RealT A2_{0}; RealT A3_{0}; @@ -138,34 +287,20 @@ namespace GridKit RealT Vcu_{0}; RealT Tdelay_{0}; - RealT a0_{1}; RealT a1_{0}; RealT a2_{0}; RealT a3_{0}; RealT a4_{0}; - // Precomputed masks and safe inverse coefficients for branch-free degenerate paths. - RealT use_notch_{0}; - RealT bypass_notch_{1}; - RealT use_4th_order_{0}; - RealT use_3rd_order_{0}; - RealT use_2nd_order_{0}; - RealT safe_inv_a4_{0}; - RealT safe_inv_a3_{0}; - RealT safe_inv_a2_{0}; - RealT use_T2_block_{1}; - RealT bypass_T2_block_{0}; - RealT use_T4_block_{1}; - RealT bypass_T4_block_{0}; - RealT use_T6_block_{1}; - RealT bypass_T6_block_{0}; - - ComponentSignals signals_; + IdxT parameter_error_count_{0}; + + ComponentSignals, IeeestExternalVariables> signals_; std::unique_ptr monitor_; void initializeParameters(const ModelDataT& data); void initializeMonitor(); + void setDerivedParameters(); std::vector ws_; std::vector ws_indices_; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp index 083b5c7e2..fe28e860e 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp @@ -17,8 +17,8 @@ namespace GridKit * * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { Log::misc() << "Evaluate Jacobian for Ieeest..." << std::endl; Log::misc() << "Jacobian evaluation not implemented!" << std::endl; @@ -26,8 +26,16 @@ namespace GridKit } // Available template instantiations - template class Ieeest; - template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp index a1ff9f40c..d3afc30d6 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp @@ -19,10 +19,11 @@ namespace GridKit * * @tparam ScalarT - Scalar data type * @tparam IdxT - Index data type + * @tparam order - Notch filter order * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { Log::misc() << "Evaluate Jacobian for Ieeest..." << std::endl; Log::misc() << "Jacobian evaluation is experimental!" << std::endl; @@ -42,7 +43,7 @@ namespace GridKit nnz_ = 0; - GridKit::Enzyme::Sparse::DfDy, + GridKit::Enzyme::Sparse::DfDy, GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, f_.size(), y_.size(), @@ -57,7 +58,7 @@ namespace GridKit J_vals_buffer_, nnz_); - GridKit::Enzyme::Sparse::DfDyp, + GridKit::Enzyme::Sparse::DfDyp, GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, f_.size(), y_.size(), @@ -73,7 +74,7 @@ namespace GridKit J_vals_buffer_, nnz_); - GridKit::Enzyme::Sparse::DfDws, + GridKit::Enzyme::Sparse::DfDws, GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, f_.size(), ws_.size(), @@ -94,8 +95,16 @@ namespace GridKit } // Available template instantiations - template class Ieeest; - template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index 302e5bc63..b75028c48 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -6,8 +6,9 @@ * @brief Definition of the IEEEST Power System Stabilizer. */ -#include +#include #include +#include #include #include @@ -23,141 +24,106 @@ namespace GridKit { using Log = ::GridKit::Utilities::Logger; - template - Ieeest::Ieeest() + template + Ieeest::Ieeest() { - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); + setDerivedParameters(); } - template - Ieeest::Ieeest(const ModelDataT& data) + template + Ieeest::Ieeest(const ModelDataT& data) : monitor_(std::make_unique(data)) { initializeParameters(data); initializeMonitor(); - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); + setDerivedParameters(); } - template - Ieeest::~Ieeest() + template + Ieeest::~Ieeest() { } - template - void Ieeest::initializeParameters(const ModelDataT& data) + template + void Ieeest::setDerivedParameters() { - using Parameter = typename ModelDataT::Parameters; - if (data.parameters.contains(Parameter::A1)) - { - A1_ = std::get(data.parameters.at(Parameter::A1)); - } - if (data.parameters.contains(Parameter::A2)) - { - A2_ = std::get(data.parameters.at(Parameter::A2)); - } - if (data.parameters.contains(Parameter::A3)) - { - A3_ = std::get(data.parameters.at(Parameter::A3)); - } - if (data.parameters.contains(Parameter::A4)) - { - A4_ = std::get(data.parameters.at(Parameter::A4)); - } - if (data.parameters.contains(Parameter::A5)) - { - A5_ = std::get(data.parameters.at(Parameter::A5)); - } - if (data.parameters.contains(Parameter::A6)) - { - A6_ = std::get(data.parameters.at(Parameter::A6)); - } - if (data.parameters.contains(Parameter::T1)) - { - T1_ = std::get(data.parameters.at(Parameter::T1)); - } - if (data.parameters.contains(Parameter::T2)) - { - T2_ = std::get(data.parameters.at(Parameter::T2)); - } - if (data.parameters.contains(Parameter::T3)) - { - T3_ = std::get(data.parameters.at(Parameter::T3)); - } - if (data.parameters.contains(Parameter::T4)) - { - T4_ = std::get(data.parameters.at(Parameter::T4)); - } - if (data.parameters.contains(Parameter::T5)) - { - T5_ = std::get(data.parameters.at(Parameter::T5)); - } - if (data.parameters.contains(Parameter::T6)) - { - T6_ = std::get(data.parameters.at(Parameter::T6)); - } - if (data.parameters.contains(Parameter::Ks)) - { - Ks_ = std::get(data.parameters.at(Parameter::Ks)); - } - if (data.parameters.contains(Parameter::Lsmin)) - { - Lsmin_ = std::get(data.parameters.at(Parameter::Lsmin)); - } - if (data.parameters.contains(Parameter::Lsmax)) - { - Lsmax_ = std::get(data.parameters.at(Parameter::Lsmax)); - } - if (data.parameters.contains(Parameter::Vcl)) - { - Vcl_ = std::get(data.parameters.at(Parameter::Vcl)); - } - if (data.parameters.contains(Parameter::Vcu)) - { - Vcu_ = std::get(data.parameters.at(Parameter::Vcu)); - } - if (data.parameters.contains(Parameter::Tdelay)) - { - Tdelay_ = std::get(data.parameters.at(Parameter::Tdelay)); - } + T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); + T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); + T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); - a0_ = 1; - a1_ = A1_ + A3_; - a2_ = A2_ + A4_ + A1_ * A3_; - a3_ = A1_ * A4_ + A2_ * A3_; - a4_ = A2_ * A4_; + const auto a = notchCoefficients(A1_, A2_, A3_, A4_); - // Precompute masks and safe inverse coefficients so the residual stays branch-free. - use_notch_ = static_cast(a2_ != 0.0 || a3_ != 0.0 || a4_ != 0.0); - bypass_notch_ = 1.0 - use_notch_; + a1_ = a[0]; + a2_ = a[1]; + a3_ = a[2]; + a4_ = a[3]; + } - use_4th_order_ = static_cast(a4_ != 0.0); - use_3rd_order_ = static_cast(a4_ == 0.0 && a3_ != 0.0); - use_2nd_order_ = static_cast(a4_ == 0.0 && a3_ == 0.0 && a2_ != 0.0); - safe_inv_a4_ = use_4th_order_ / (a4_ + (1.0 - use_4th_order_)); - safe_inv_a3_ = use_3rd_order_ / (a3_ + (1.0 - use_3rd_order_)); - safe_inv_a2_ = use_2nd_order_ / (a2_ + (1.0 - use_2nd_order_)); + template + void Ieeest::initializeParameters(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; - use_T2_block_ = static_cast(T2_ != 0.0); - bypass_T2_block_ = 1.0 - use_T2_block_; + parameter_error_count_ = 0; - use_T4_block_ = static_cast(T4_ != 0.0); - bypass_T4_block_ = 1.0 - use_T4_block_; + auto load_real = [&](auto key, RealT& target, const char* name) + { + if (!data.parameters.contains(key)) + { + return; + } - use_T6_block_ = static_cast(T6_ != 0.0); - bypass_T6_block_ = 1.0 - use_T6_block_; + 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); + } + else + { + Log::error() << "Ieeest: parameter '" << name << "' must be numeric\n"; + ++parameter_error_count_; + } + }; + + load_real(Params::A1, A1_, "A1"); + load_real(Params::A2, A2_, "A2"); + load_real(Params::A3, A3_, "A3"); + load_real(Params::A4, A4_, "A4"); + load_real(Params::A5, A5_, "A5"); + load_real(Params::A6, A6_, "A6"); + load_real(Params::T1, T1_, "T1"); + load_real(Params::T2, T2_, "T2"); + load_real(Params::T3, T3_, "T3"); + load_real(Params::T4, T4_, "T4"); + load_real(Params::T5, T5_, "T5"); + load_real(Params::T6, T6_, "T6"); + load_real(Params::Ks, Ks_, "Ks"); + load_real(Params::Lsmin, Lsmin_, "Lsmin"); + load_real(Params::Lsmax, Lsmax_, "Lsmax"); + load_real(Params::Vcl, Vcl_, "Vcl"); + load_real(Params::Vcu, Vcu_, "Vcu"); + load_real(Params::Tdelay, Tdelay_, "Tdelay"); } - template - int Ieeest::setGridKitComponentID(IdxT component_id) + template + int Ieeest::setGridKitComponentID(IdxT component_id) { gridkit_component_id_ = component_id; return 0; } - template - int Ieeest::allocate() + template + int Ieeest::allocate() { + size_ = static_cast(IeeestInternalVariables::MAXIMUM); auto size = static_cast(size_); + f_.resize(size); y_.resize(size); yp_.resize(size); @@ -166,10 +132,9 @@ namespace GridKit variable_indices_.resize(size); residual_indices_.resize(size); - ws_.resize(1); - ws_indices_.resize(1); - ws_[0] = 0.0; - ws_indices_[0] = INVALID_INDEX; + auto signal_size = static_cast(IeeestExternalVariables::MAXIMUM); + ws_.resize(signal_size); + ws_indices_.resize(signal_size); for (IdxT j = 0; j < size_; ++j) { @@ -177,19 +142,21 @@ namespace GridKit this->setResidualIndex(j, j); } - if (signals_.template isAssigned()) + constexpr auto VSS = IeeestInternalVariables::VSS; + if (signals_.template isAssigned()) { - signals_.template getSignalNode()->set( - &y_[11], &(this->getVariableIndex(11))); + signals_.template getSignalNode()->set( + &y_[static_cast(VSS)], + &(this->getVariableIndex(static_cast(VSS)))); } return 0; } - template - int Ieeest::verify() const + template + int Ieeest::verify() const { - int ret = 0; + int ret = static_cast(parameter_error_count_); if (signals_.template isAttached()) { @@ -205,42 +172,107 @@ namespace GridKit ret += 1; } - if (a4_ == 0 && a3_ == 0 && a2_ == 0 && a1_ != 0) + const size_t derived_order = notchOrder(a1_, a2_, a3_, a4_); + if (derived_order != order) { - Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; + Log::error() << "Ieeest: parameters imply a notch filter of order " << derived_order + << " for a model instantiated with order " << order << "\n"; ret += 1; } + if constexpr (order == 0) + { + if (A5_ != ZERO || A6_ != ZERO) + { + Log::error() << "Ieeest: unsupported zeroth-order notch filter with nonzero numerator\n"; + ret += 1; + } + } + + if constexpr (order == 1) + { + if (A6_ != ZERO) + { + Log::error() << "Ieeest: unsupported first-order notch filter with second-order numerator\n"; + ret += 1; + } + } + return ret; } - template - int Ieeest::initialize() + template + int Ieeest::initialize() { - for (IdxT i = 0; i < size_; ++i) + if (verify() > 0) { - y_[static_cast(i)] = 0.0; - yp_[static_cast(i)] = 0.0; + Log::error() << "Ieeest: cannot initialize with invalid configuration\n"; + return 1; } + const auto X5 = static_cast(IeeestInternalVariables::X5); + const auto X6 = static_cast(IeeestInternalVariables::X6); + const auto X7 = static_cast(IeeestInternalVariables::X7); + const auto V4 = static_cast(IeeestInternalVariables::V4); + const auto V5 = static_cast(IeeestInternalVariables::V5); + const auto V6 = static_cast(IeeestInternalVariables::V6); + const auto V7 = static_cast(IeeestInternalVariables::V7); + const auto VSS = static_cast(IeeestInternalVariables::VSS); + const auto U = static_cast(IeeestExternalVariables::U); + + std::fill(y_.begin(), y_.end(), ZERO); + std::fill(yp_.begin(), yp_.end(), ZERO); + + const ScalarT u = signals_.template readExternalVariable(); + ws_[U] = u; + ws_indices_[U] = signals_.template readExternalVariableIndex(); + + // Chain states x2..xN hold successive derivatives of the filtered + // signal and remain at zero from the fill above. + if constexpr (order >= 1) + { + const auto X1 = static_cast(IeeestInternalVariables::X1); + + y_[X1] = u; + } + + y_[X5] = u; + y_[X6] = u; + y_[X7] = u; + y_[V4] = u; + y_[V5] = u; + y_[V6] = u; + y_[V7] = ZERO; + y_[VSS] = Math::clamp(y_[V7], Lsmin_, Lsmax_); + return 0; } - template - int Ieeest::tagDifferentiable() + template + int Ieeest::tagDifferentiable() { - tag_[0] = true; - tag_[1] = true; - tag_[2] = true; - tag_[3] = true; - tag_[4] = (T2_ != 0.0); - tag_[5] = (T4_ != 0.0); - tag_[6] = (T6_ != 0.0); - tag_[7] = false; - tag_[8] = false; - tag_[9] = false; - tag_[10] = false; - tag_[11] = false; + std::fill(tag_.begin(), tag_.end(), false); + + if constexpr (order >= 1) + { + tag_[static_cast(IeeestInternalVariables::X1)] = true; + } + if constexpr (order >= 2) + { + tag_[static_cast(IeeestInternalVariables::X2)] = true; + } + if constexpr (order >= 3) + { + tag_[static_cast(IeeestInternalVariables::X3)] = true; + } + if constexpr (order >= 4) + { + tag_[static_cast(IeeestInternalVariables::X4)] = true; + } + + tag_[static_cast(IeeestInternalVariables::X5)] = true; + tag_[static_cast(IeeestInternalVariables::X6)] = true; + tag_[static_cast(IeeestInternalVariables::X7)] = true; return 0; } @@ -257,69 +289,152 @@ namespace GridKit * This represents a "noise" level close to zero for which pure relative * error cannot be used. */ - template - int Ieeest::setAbsoluteTolerance(RealT rel_tol) + template + int Ieeest::setAbsoluteTolerance(RealT rel_tol) { std::fill(abs_tol_.begin(), abs_tol_.end(), rel_tol); return 0; } - template - __attribute__((always_inline)) inline int Ieeest::evaluateInternalResidual( + template + __attribute__((always_inline)) inline int Ieeest::evaluateInternalResidual( const ScalarT* y, const ScalarT* yp, [[maybe_unused]] const ScalarT* wb, const ScalarT* ws, ScalarT* f) { - ScalarT x1 = y[0]; - ScalarT x2 = y[1]; - ScalarT x3 = y[2]; - ScalarT x4 = y[3]; - ScalarT x5 = y[4]; - ScalarT x6 = y[5]; - ScalarT x7 = y[6]; - ScalarT v4 = y[7]; - ScalarT v5 = y[8]; - ScalarT v6 = y[9]; - ScalarT v7 = y[10]; - ScalarT vss = y[11]; - - ScalarT x1_dot = yp[0]; - ScalarT x2_dot = yp[1]; - ScalarT x3_dot = yp[2]; - ScalarT x4_dot = yp[3]; - ScalarT x5_dot = yp[4]; - ScalarT x6_dot = yp[5]; - ScalarT x7_dot = yp[6]; - - ScalarT u = ws[0]; - - f[0] = -x1_dot + use_notch_ * x2; - f[1] = -x2_dot + (use_4th_order_ + use_3rd_order_) * x3 - + use_2nd_order_ * (-a0_ * x1 - a1_ * x2 + u) * safe_inv_a2_; - f[2] = -x3_dot + use_4th_order_ * x4 - + use_3rd_order_ * (-a0_ * x1 - a1_ * x2 - a2_ * x3 + u) * safe_inv_a3_; - f[3] = -x4_dot + use_4th_order_ * (-a0_ * x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u) * safe_inv_a4_; - f[4] = -T2_ * x5_dot - x5 + v4; - f[5] = -T4_ * x6_dot - x6 + v5; - f[6] = -T6_ * x7_dot - x7 + v6; - f[7] = -v4 + bypass_notch_ * u + use_notch_ * (x1 + A5_ * x2 + (use_4th_order_ + use_3rd_order_) * A6_ * x3); - f[8] = use_T2_block_ * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + bypass_T2_block_ * (v4 - v5); - f[9] = use_T4_block_ * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + bypass_T4_block_ * (v5 - v6); - f[10] = use_T6_block_ * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + bypass_T6_block_ * (Ks_ * v6 - v7); - f[11] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); + const auto X5 = static_cast(IeeestInternalVariables::X5); + const auto X6 = static_cast(IeeestInternalVariables::X6); + const auto X7 = static_cast(IeeestInternalVariables::X7); + const auto V4 = static_cast(IeeestInternalVariables::V4); + const auto V5 = static_cast(IeeestInternalVariables::V5); + const auto V6 = static_cast(IeeestInternalVariables::V6); + const auto V7 = static_cast(IeeestInternalVariables::V7); + const auto VSS = static_cast(IeeestInternalVariables::VSS); + const auto U = static_cast(IeeestExternalVariables::U); + + const ScalarT x5 = y[X5]; + const ScalarT x6 = y[X6]; + const ScalarT x7 = y[X7]; + const ScalarT v4 = y[V4]; + const ScalarT v5 = y[V5]; + const ScalarT v6 = y[V6]; + const ScalarT v7 = y[V7]; + const ScalarT vss = y[VSS]; + + const ScalarT x5_dot = yp[X5]; + const ScalarT x6_dot = yp[X6]; + const ScalarT x7_dot = yp[X7]; + + const ScalarT u = ws[U]; + + // Notch filter -- order-specific realization + if constexpr (order == 0) + { + f[V4] = -v4 + u; + } + else if constexpr (order == 1) + { + const auto X1 = static_cast(IeeestInternalVariables::X1); + + const ScalarT x1 = y[X1]; + const ScalarT x1_dot = yp[X1]; + + const ScalarT x1_rhs = (u - x1) / a1_; + + f[X1] = -x1_dot + x1_rhs; + f[V4] = -v4 + x1 + A5_ * x1_rhs; + } + else if constexpr (order == 2) + { + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + + const ScalarT x1 = y[X1]; + const ScalarT x2 = y[X2]; + const ScalarT x1_dot = yp[X1]; + const ScalarT x2_dot = yp[X2]; + + const ScalarT x2_rhs = (u - x1 - a1_ * x2) / a2_; + + f[X1] = -x1_dot + x2; + f[X2] = -x2_dot + x2_rhs; + f[V4] = -v4 + x1 + A5_ * x2 + A6_ * x2_rhs; + } + else if constexpr (order == 3) + { + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + const auto X3 = static_cast(IeeestInternalVariables::X3); + + const ScalarT x1 = y[X1]; + const ScalarT x2 = y[X2]; + const ScalarT x3 = y[X3]; + const ScalarT x1_dot = yp[X1]; + const ScalarT x2_dot = yp[X2]; + const ScalarT x3_dot = yp[X3]; + + const ScalarT x3_rhs = (u - x1 - a1_ * x2 - a2_ * x3) / a3_; + + f[X1] = -x1_dot + x2; + f[X2] = -x2_dot + x3; + f[X3] = -x3_dot + x3_rhs; + f[V4] = -v4 + x1 + A5_ * x2 + A6_ * x3; + } + else + { + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + const auto X3 = static_cast(IeeestInternalVariables::X3); + const auto X4 = static_cast(IeeestInternalVariables::X4); + + const ScalarT x1 = y[X1]; + const ScalarT x2 = y[X2]; + const ScalarT x3 = y[X3]; + const ScalarT x4 = y[X4]; + const ScalarT x1_dot = yp[X1]; + const ScalarT x2_dot = yp[X2]; + const ScalarT x3_dot = yp[X3]; + const ScalarT x4_dot = yp[X4]; + + const ScalarT x4_rhs = (u - x1 - a1_ * x2 - a2_ * x3 - a3_ * x4) / a4_; + + f[X1] = -x1_dot + x2; + f[X2] = -x2_dot + x3; + f[X3] = -x3_dot + x4; + f[X4] = -x4_dot + x4_rhs; + f[V4] = -v4 + x1 + A5_ * x2 + A6_ * x3; + } + + // Lead-lags and washout -- shared across all orders + const ScalarT x5_rhs = (v4 - x5) / T2_; + const ScalarT x6_rhs = (v5 - x6) / T4_; + const ScalarT x7_rhs = (v6 - x7) / T6_; + + f[X5] = -x5_dot + x5_rhs; + f[X6] = -x6_dot + x6_rhs; + f[X7] = -x7_dot + x7_rhs; + f[V5] = -v5 + x5 + T1_ * x5_rhs; + f[V6] = -v6 + x6 + T3_ * x6_rhs; + f[V7] = -v7 + Ks_ * T5_ * x7_rhs; + f[VSS] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); return 0; } - template - int Ieeest::evaluateResidual() + template + int Ieeest::evaluateResidual() { + const auto U = static_cast(IeeestExternalVariables::U); + + ws_[U] = ZERO; + ws_indices_[U] = INVALID_INDEX; + if (signals_.template isAttached()) { - ws_[0] = signals_.template readExternalVariable(); - ws_indices_[0] = signals_.template readExternalVariableIndex(); + ws_[U] = signals_.template readExternalVariable(); + ws_indices_[U] = signals_.template readExternalVariableIndex(); } evaluateInternalResidual(y_.data(), yp_.data(), wb_.data(), ws_.data(), f_.data()); @@ -327,18 +442,21 @@ namespace GridKit return 0; } - template - const Model::VariableMonitorBase* Ieeest::getMonitor() const + template + const Model::VariableMonitorBase* Ieeest::getMonitor() const { return monitor_.get(); } - template - void Ieeest::initializeMonitor() + template + void Ieeest::initializeMonitor() { using Variable = typename ModelDataT::MonitorableVariables; + + constexpr auto VSS = static_cast(IeeestInternalVariables::VSS); + monitor_->set(Variable::vss, [this] - { return y_[11]; }); + { return y_[VSS]; }); } } // namespace Stabilizer diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index e5765ba1a..491c60885 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -3,6 +3,11 @@ Standard IEEE power system stabilizer: 4th-order notch filter, two lead–lag blocks, washout, and output limiter. +## Notes + +- $V_{\mathrm{cl}}$, $V_{\mathrm{cu}}$, and $T_{\mathrm{delay}}$ are accepted for input-format + compatibility but are not modeled. + ## Block Diagram ![](../../../../../docs/Figures/stabilizer_ieeest_diagram.png) @@ -11,70 +16,101 @@ Figure 1: Stabilizer IEEEST model. Figure courtesy of [PowerWorld](https://www.p ## Model Parameters -Symbol | Units | Description | Typical Value -------------|--------|--------------------------------------|-------------- -$A_1$ | [s] | Notch denominator coefficient | 1.013 -$A_2$ | [s²] | Notch denominator coefficient | 0.013 -$A_3$ | [s] | Notch denominator coefficient | 0.0 -$A_4$ | [s²] | Notch denominator coefficient | 0.0 -$A_5$ | [s] | Notch numerator coefficient | 1.013 -$A_6$ | [s²] | Notch numerator coefficient | 0.113 -$T_1$ | [s] | Lead–lag 1 numerator time constant | 0.0 -$T_2$ | [s] | Lead–lag 1 denominator time constant | 0.02 -$T_3$ | [s] | Lead–lag 2 numerator time constant | 0.0 -$T_4$ | [s] | Lead–lag 2 denominator time constant | 0.0 -$T_5$ | [s] | Washout numerator time constant | 1.65 -$T_6$ | [s] | Washout denominator time constant | 1.65 -$K_s$ | [p.u.] | Stabilizer gain | 3.0 -$L_s^{\min}$ | [p.u.] | Minimum stabilizer output limit | -0.1 -$L_s^{\max}$ | [p.u.] | Maximum stabilizer output limit | 0.1 - -The IEEE 421.5 IEEEST also defines a cutout window ($V_{cl}$, $V_{cu}$) and an -input delay ($T_{delay}$). These parameters are accepted for input-format -compatibility but are not modeled here. - -### Derived Parameters +Symbol | Units | JSON | Description | Typical Value | Note +------------------------|----------|----------|--------------------------------------|---------------|------ +$A_1$ | [sec] | `A1` | Notch denominator coefficient | 1.013 | +$A_2$ | [sec²] | `A2` | Notch denominator coefficient | 0.013 | +$A_3$ | [sec] | `A3` | Notch denominator coefficient | 0.0 | +$A_4$ | [sec²] | `A4` | Notch denominator coefficient | 0.0 | +$A_5$ | [sec] | `A5` | Notch numerator coefficient | 1.013 | +$A_6$ | [sec²] | `A6` | Notch numerator coefficient | 0.113 | +$T_1$ | [sec] | `T1` | Lead–lag 1 numerator time constant | 0.0 | +$T_2$ | [sec] | `T2` | Lead–lag 1 denominator time constant | 0.02 | +$T_3$ | [sec] | `T3` | Lead–lag 2 numerator time constant | 0.0 | +$T_4$ | [sec] | `T4` | Lead–lag 2 denominator time constant | 0.0 | +$T_5$ | [sec] | `T5` | Washout numerator time constant | 1.65 | +$T_6$ | [sec] | `T6` | Washout denominator time constant | 1.65 | +$K_s$ | [p.u.] | `Ks` | Stabilizer gain | 3.0 | +$L_s^{\min}$ | [p.u.] | `Lsmin` | Minimum stabilizer output limit | -0.1 | +$L_s^{\max}$ | [p.u.] | `Lsmax` | Maximum stabilizer output limit | 0.1 | +$V_{\mathrm{cl}}$ | [p.u.] | `Vcl` | Lower input cutout threshold | 0.0 | Accepted but not modeled +$V_{\mathrm{cu}}$ | [p.u.] | `Vcu` | Upper input cutout threshold | 0.0 | Accepted but not modeled +$T_{\mathrm{delay}}$ | [sec] | `Tdelay` | Input delay | 0.0 | Accepted but not modeled + +### Parameter Validation + +IEEEST denominator time constants are conditioned and unsupported notch forms are rejected by the following checks. Let $\epsilon_T=10^{-3}$. + +```math +\begin{aligned} + T &\leftarrow \max\!\left(T, \epsilon_T\right) + \quad T\in\{T_2,T_4,T_6\} \\ + A_6 &= 0 + \quad\text{when}\quad + n = 1 +\end{aligned} +``` + +### Model Derived Parameters ```math \begin{aligned} -a_0 &= 1 \\ -a_1 &= A_1 + A_3 \\ -a_2 &= A_2 + A_4 + A_1 A_3 \\ -a_3 &= A_1 A_4 + A_2 A_3 \\ -a_4 &= A_2 A_4 + a_1 &= A_1 + A_3 \\ + a_2 &= A_2 + A_4 + A_1 A_3 \\ + a_3 &= A_1 A_4 + A_2 A_3 \\ + a_4 &= A_2 A_4 \\ + n &= + \begin{cases} + 4 & a_4 \ne 0 \\ + 3 & a_4 = 0,\ a_3 \ne 0 \\ + 2 & a_4 = a_3 = 0,\ a_2 \ne 0 \\ + 1 & a_4 = a_3 = a_2 = 0,\ a_1 \ne 0 \\ + 0 & a_4 = a_3 = a_2 = a_1 = 0 + \end{cases} \end{aligned} ``` +## Model Ports + +Name | Port | Init | Description +---------|--------|-------|------ +`input` | Input | Known | Stabilizer input signal +`output` | Output | Known | Stabilizer output signal + ## Model Variables ### Internal Variables #### Differential -Symbol | Units | Description -----------------------|--------|------------ -$x_1, x_2, x_3, x_4$ | [-] | Notch filter states -$x_5$ | [-] | Lead–lag 1 state -$x_6$ | [-] | Lead–lag 2 state -$x_7$ | [-] | Washout state +Symbol | Units | Description | Note +----------------------|--------|-----------------------|------ +$x_1, x_2, x_3, x_4$ | [-] | Notch filter states | States 1–4 in Fig. 1 +$x_5$ | [-] | Lead–lag 1 state | State 5 in Fig. 1 +$x_6$ | [-] | Lead–lag 2 state | State 6 in Fig. 1 +$x_7$ | [-] | Washout state | State 7 in Fig. 1 #### Algebraic -Symbol | Units | Description ------------|--------|------------ -$v_4$ | [p.u.] | Notch filter output -$v_5$ | [p.u.] | Lead–lag 1 output -$v_6$ | [p.u.] | Lead–lag 2 output -$v_7$ | [p.u.] | Unlimited stabilizer signal -$V_{ss}$ | [p.u.] | Limited stabilizer signal (model output) +Symbol | Units | Description | Note +---------------------|--------|------------------------------------------|------ +$v_4$ | [p.u.] | Notch filter output | +$v_5$ | [p.u.] | Lead–lag 1 output | +$v_6$ | [p.u.] | Lead–lag 2 output | +$v_7$ | [p.u.] | Unlimited stabilizer signal | +$V_{\mathrm{ss}}$ | [p.u.] | Limited stabilizer signal (model output) | ### External Variables +#### Differential + +None. + #### Algebraic -Symbol | Units | Description --------|--------|------------ -$u$ | [p.u.] | Stabilizer input signal +Symbol | Units | Type | Description | Note +-------|--------|-------|-------------------------|------ +$u$ | [p.u.] | Known | Stabilizer input signal | ## Model Equations @@ -82,13 +118,50 @@ $u$ | [p.u.] | Stabilizer input signal ```math \begin{aligned} -0 &= -\dot{x}_1 + x_2 \\ -0 &= -\dot{x}_2 + x_3 \\ -0 &= -\dot{x}_3 + x_4 \\ -0 &= -\dot{x}_4 - \dfrac{a_0}{a_4}x_1 - \dfrac{a_1}{a_4}x_2 - \dfrac{a_2}{a_4}x_3 - \dfrac{a_3}{a_4}x_4 + \dfrac{1}{a_4}u \\ -0 &= -T_2 \dot{x}_5 - x_5 + v_4 \\ -0 &= -T_4 \dot{x}_6 - x_6 + v_5 \\ -0 &= -T_6 \dot{x}_7 - x_7 + v_6 + 0 &= + -\dot{x}_1 + + \begin{cases} + 0, & n = 0 \\ + \dfrac{1}{a_1}\left(-x_1 + u\right), & n = 1 \\ + x_2, & n = 2,3,4 + \end{cases} \\ + 0 &= + -\dot{x}_2 + + \begin{cases} + 0, + & n = 0,1 \\ + \dfrac{1}{a_2}\left(-x_1 - a_1x_2 + u\right), + & n = 2 \\ + x_3, + & n = 3,4 + \end{cases} \\ + 0 &= + -\dot{x}_3 + + \begin{cases} + 0, + & n = 0,1,2 \\ + \dfrac{1}{a_3}\left(-x_1 - a_1x_2 - a_2x_3 + u\right), + & n = 3 \\ + x_4, + & n = 4 + \end{cases} \\ + 0 &= + -\dot{x}_4 + + \begin{cases} + 0, + & n = 0,1,2,3 \\ + \dfrac{1}{a_4}\left(-x_1 - a_1x_2 - a_2x_3 - a_3x_4 + u\right), + & n = 4 + \end{cases} \\ + 0 &= + -\dot{x}_5 + + \dfrac{1}{T_2}\left(v_4 - x_5\right) \\ + 0 &= + -\dot{x}_6 + + \dfrac{1}{T_4}\left(v_5 - x_6\right) \\ + 0 &= + -\dot{x}_7 + + \dfrac{1}{T_6}\left(v_6 - x_7\right) \end{aligned} ``` @@ -96,19 +169,59 @@ $u$ | [p.u.] | Stabilizer input signal ```math \begin{aligned} -0 &= -v_4 + x_1 + A_5 x_2 + A_6 x_3 \\ -0 &= -T_2(v_5 - x_5) + T_1(v_4 - x_5) \\ -0 &= -T_4(v_6 - x_6) + T_3(v_5 - x_6) \\ -0 &= -T_6 v_7 + K_s T_5(v_6 - x_7) \\ -0 &= -V_{ss} + \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) + 0 &= + -v_4 + + \begin{cases} + u, + & n = 0 \\ + x_1 + \dfrac{A_5}{a_1}\left(-x_1 + u\right), + & n = 1 \\ + x_1 + A_5x_2 + + \dfrac{A_6}{a_2}\left(-x_1 - a_1x_2 + u\right), + & n = 2 \\ + x_1 + A_5x_2 + A_6x_3, + & n = 3,4 + \end{cases} \\ + 0 &= -v_5 + x_5 + \dfrac{T_1}{T_2}\left(v_4 - x_5\right) \\ + 0 &= -v_6 + x_6 + \dfrac{T_3}{T_4}\left(v_5 - x_6\right) \\ + 0 &= -v_7 + K_s\dfrac{T_5}{T_6}\left(v_6 - x_7\right) \\ + 0 &= + -V_{\mathrm{ss}} + + \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) \end{aligned} ``` The output limiter uses GridKit's smooth -[Clamp](../../../../CommonMath.md#derived-functions). +[clamp](../../../../CommonMath.md#derived-functions). ## Initialization -All states and their derivatives initialize to zero. The stabilizer comes -online at rest and produces signal only in response to deviations in the input -$u$. +### Input Initialization + +```math +\begin{aligned} + u &\leftarrow \text{stabilizer input signal} +\end{aligned} +``` + +### Internal Initialization + +```math +\begin{aligned} + x_{1,0} &= v_{4,0} = x_{5,0} = v_{5,0} = x_{6,0} = v_{6,0} = x_{7,0} = u_0 \\ + x_{2,0} &= x_{3,0} = x_{4,0} = 0 \\ + v_{7,0} &= 0 \\ + V_{\mathrm{ss},0} &= \text{clamp}(v_{7,0}, L_s^{\min}, L_s^{\max}) \\ + \dot{x}_{i,0} &= 0 \quad i\in\{1,\ldots,7\} +\end{aligned} +``` + +### Output Initialization + +None. + +## Monitorable Outputs + +Output | Units | Description | Note +-------|--------|---------------------------|------ +`vss` | [p.u.] | Limited stabilizer signal | Exported through `output` when assigned diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/StabilizerFactory.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/StabilizerFactory.hpp new file mode 100644 index 000000000..369b9a268 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Stabilizer/StabilizerFactory.hpp @@ -0,0 +1,126 @@ +/** + * @file StabilizerFactory.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Factory for constructing stabilizer models from modeling data. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Stabilizer + { + /** + * @brief Creates stabilizer components of the concrete order implied by + * their modeling data. + * + * The notch-filter order of a `Ieeest` is a template parameter, while + * modeling data determines the order at runtime. This factory resolves + * the runtime decision to the matching `Ieeest` instantiation and wires + * the input/output signal nodes. + */ + template + class StabilizerFactory + { + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename Component::RealT; + using ComponentT = Component; + using SignalT = SignalNode; + using IeeestDataT = IeeestData; + + StabilizerFactory() = delete; + + /** + * @brief Create the `Ieeest` of the order implied by `data`. + * + * @param[in] data IEEEST modeling data; the notch-filter order is + * derived from its A1..A4 parameters. + * @param[in] input Stabilizer input signal node; may be `nullptr`. + * @param[in] output Stabilizer output signal node; may be `nullptr`. + * @return Newly allocated stabilizer; the caller assumes ownership. + */ + static ComponentT* create(const IeeestDataT& data, SignalT* input, SignalT* output) + { + using Params = typename IeeestDataT::Parameters; + + const RealT A1 = readParameter(data, Params::A1); + const RealT A2 = readParameter(data, Params::A2); + const RealT A3 = readParameter(data, Params::A3); + const RealT A4 = readParameter(data, Params::A4); + + const auto a = notchCoefficients(A1, A2, A3, A4); + + switch (notchOrder(a[0], a[1], a[2], a[3])) + { + case 0: + return createIeeest<0>(data, input, output); + case 1: + return createIeeest<1>(data, input, output); + case 2: + return createIeeest<2>(data, input, output); + case 3: + return createIeeest<3>(data, input, output); + default: + // notchOrder yields at most 4 + return createIeeest<4>(data, input, output); + } + } + + private: + /// Create a `Ieeest` of compile-time order `order` and wire its signal nodes + template + static ComponentT* createIeeest(const IeeestDataT& data, SignalT* input, SignalT* output) + { + auto* stabilizer = new Ieeest(data); + + if (input != nullptr) + { + stabilizer->getSignals().template attachSignalNode(input); + } + + if (output != nullptr) + { + constexpr auto VSS = IeeestInternalVariables::VSS; + stabilizer->getSignals().template assignSignalNode(output); + } + + return stabilizer; + } + + /// Read a real-valued parameter from `data`, defaulting to zero + static RealT readParameter(const IeeestDataT& data, typename IeeestDataT::Parameters key) + { + if (!data.parameters.contains(key)) + { + return ZERO; + } + + const auto& value = data.parameters.at(key); + if (const auto* real_value = std::get_if(&value)) + { + return *real_value; + } + if (const auto* index_value = std::get_if(&value)) + { + return static_cast(*index_value); + } + + return ZERO; + } + }; + + } // namespace Stabilizer + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index b020f8f72..602be6374 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -322,23 +323,19 @@ namespace GridKit // Add IEEEST stabilizers for (const auto& stabdata : data.stabilizer) { - auto* stabilizer = new Ieeest(stabdata); - + SignalT* input = nullptr; if (stabdata.signal_inputs.contains(IeeestSignalInputs::input)) { - IdxT input = stabdata.signal_inputs.at(IeeestSignalInputs::input); - constexpr auto U = IeeestExternalVariables::U; - stabilizer->getSignals().template attachSignalNode(getSignal(input)); + input = getSignal(stabdata.signal_inputs.at(IeeestSignalInputs::input)); } + SignalT* output = nullptr; if (stabdata.signal_outputs.contains(IeeestSignalOutputs::output)) { - IdxT output = stabdata.signal_outputs.at(IeeestSignalOutputs::output); - constexpr auto VSS = IeeestInternalVariables::VSS; - stabilizer->getSignals().template assignSignalNode(getSignal(output)); + output = getSignal(stabdata.signal_outputs.at(IeeestSignalOutputs::output)); } - addComponent(stabilizer); + addComponent(StabilizerFactory::create(stabdata, input, output)); } // Add constant signal sources diff --git a/GridKit/Model/VariableMonitorImpl.hpp b/GridKit/Model/VariableMonitorImpl.hpp index 682692c3c..5771eb3df 100644 --- a/GridKit/Model/VariableMonitorImpl.hpp +++ b/GridKit/Model/VariableMonitorImpl.hpp @@ -24,22 +24,23 @@ namespace GridKit * each monitored value without a line break. That way other monitors can * append likewise on the same line, and the line can be ended by the control * monitor. + * + * @tparam eval_type Monitored component type, expected to expose `ScalarT` + * and `IdxT` type aliases + * @tparam model_data_type Data class template expected to have a + * MonitorableVariables enum */ - template typename eval_type, - template typename model_data_type> - class VariableMonitor, model_data_type> - : public VariableMonitorBase + template typename model_data_type> + class VariableMonitor : public VariableMonitorBase { template friend class VariableMonitorController; public: /// Underlying scalar value type - using ScalarT = scalar_type; + using ScalarT = typename eval_type::ScalarT; /// Index type - using IdxT = index_type; + using IdxT = typename eval_type::IdxT; /// Underlying real value type using RealT = typename GridKit::ScalarTraits::RealT; /// Type of (EvalT)Data class expected to have MonitorableVariables enum diff --git a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp index 15b64876f..102d39356 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -1,14 +1,19 @@ #pragma once +#include +#include #include #include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -22,6 +27,10 @@ namespace GridKit { public: using RealT = typename PhasorDynamics::Component::RealT; + using DataT = PhasorDynamics::Stabilizer::IeeestData; + + template + using InternalVariables = PhasorDynamics::Stabilizer::IeeestInternalVariables; StabilizerIeeestTests() = default; ~StabilizerIeeestTests() = default; @@ -30,74 +39,146 @@ namespace GridKit { TestStatus success = true; - auto data = makeTestData(); - auto* stab = new PhasorDynamics::Stabilizer::Ieeest(data); - - success *= (stab != nullptr); - success *= (stab->getMonitor() != nullptr); + { + // Zeroth order: no notch states, only X5..X7 and V4..VSS. + PhasorDynamics::Stabilizer::Ieeest model; + success *= (model.size() == 8); + success *= (model.getMonitor() == nullptr); + } - delete stab; + success *= checkConstructedSize<0>(); + success *= checkConstructedSize<1>(); + success *= checkConstructedSize<2>(); + success *= checkConstructedSize<3>(); + success *= checkConstructedSize<4>(); return success.report(__func__); } - /** - * @brief All states initialize to zero (stabilizer at rest). - * With u = 0, all residuals should be zero. - */ - TestOutcome zeroInitialResidual() + template + TestOutcome init() { TestStatus success = true; - // Create signal nodes for input (u) and output (Vss) - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.0}; - IdxT u_index = 12; // beyond internal variables - ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + using Params = PhasorDynamics::Stabilizer::IeeestParameters; - // Link signal nodes to backing storage - u_node.set(&u_value, &u_index); - vss_node.set(&vss_value, &vss_index); + struct InitCase + { + RealT T6; + RealT Ks; + ScalarT u; + RealT Lsmin; + RealT Lsmax; + ScalarT expected_v7; + ScalarT expected_vss; + }; - auto data = makeTestData(); - PhasorDynamics::Stabilizer::Ieeest stab(data); + // The smooth clamp only approximates the hard limits, hence the + // looser tolerance on the limited output. + const auto loose_tol = static_cast(1.0e-4); + const std::vector cases = { + {0.0, 0.0, 0.25, -1.0, 1.0, 0.0, 0.0}, + {0.0, 4.0, 0.25, 0.2, 0.6, 0.0, 0.2}, + {5.0, 3.0, 0.25, -1.0, 1.0, 0.0, 0.0}, + }; - // Wire: stabilizer reads u_node as input, writes vss_node as output - stab.getSignals().template attachSignalNode(&u_node); - stab.getSignals().template assignSignalNode(&vss_node); + const auto V7 = static_cast(InternalVariables::V7); + const auto VSS = static_cast(InternalVariables::VSS); - stab.allocate(); - success *= (stab.verify() == 0); - stab.initialize(); - stab.evaluateResidual(); + for (const auto& test : cases) + { + PhasorDynamics::SignalNode u_node; + PhasorDynamics::SignalNode vss_node; + ScalarT u_value{test.u}; + IdxT u_index{12}; + ScalarT vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; + + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); + + auto data = makeOrderData(); + data.parameters[Params::T6] = test.T6; + data.parameters[Params::Ks] = test.Ks; + data.parameters[Params::Lsmin] = test.Lsmin; + data.parameters[Params::Lsmax] = test.Lsmax; + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); + + model.allocate(); + success *= (model.verify() == 0); + model.initialize(); + + success *= vss_node.linked(); + success *= (vss_node.getVariableIndex() == static_cast(VSS)); + success *= isEqual(model.y()[V7], test.expected_v7, tol_); + success *= isEqual(model.y()[VSS], test.expected_vss, loose_tol); + success *= isEqual(vss_node.read(), test.expected_vss, loose_tol); + } + + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); + } + + template + TestOutcome zeroInitialResidual() + { + TestStatus success = true; + + using Params = PhasorDynamics::Stabilizer::IeeestParameters; - auto tol = 10 * std::numeric_limits::epsilon(); - const auto& f = stab.getResidual(); - for (size_t i = 0; i < f.size(); ++i) + // The second case exercises the TIME_CONSTANT_MINIMUM flooring. + const std::vector> time_constant_cases = { + {1.0, 1.0, 5.0}, + {0.0, 0.0, 0.0}, + }; + + for (const auto& T : time_constant_cases) { - if (!isEqual(f[i], 0.0, tol)) + PhasorDynamics::SignalNode u_node; + PhasorDynamics::SignalNode vss_node; + ScalarT u_value{0.25}; + IdxT u_index{12}; + ScalarT vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; + + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); + + auto data = makeOrderData(); + data.parameters[Params::T2] = T[0]; + data.parameters[Params::T4] = T[1]; + data.parameters[Params::T6] = T[2]; + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); + + model.allocate(); + success *= (model.verify() == 0); + success *= (model.initialize() == 0); + success *= (model.evaluateResidual() == 0); + + // The smooth clamp keeps the VSS row only approximately zero. + const auto loose_tol = static_cast(1.0e-4); + for (size_t i = 0; i < model.getResidual().size(); ++i) { - std::cout << "Non-zero residual at index " << i << ": " << f[i] << "\n"; - success = false; + if (!isEqual(model.getResidual()[i], static_cast(0.0), loose_tol)) + { + std::cout << "Nonzero initial residual at row " << i << ": " + << std::setprecision(15) << model.getResidual()[i] << "\n"; + success = false; + } } } - // Verify output signal is linked and reads the correct value - success *= vss_node.linked(); - success *= (vss_node.getVariableIndex() == 11); - success *= isEqual(vss_node.read(), static_cast(0.0), tol); - - return success.report(__func__); + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); } - /** - * @brief Residual evaluation against hand-computed answer key. - * - * Sets specific y/yp values and verifies residuals match - * pre-computed expected values. See plan for derivation. - */ + template TestOutcome residual() { TestStatus success = true; @@ -105,229 +186,500 @@ namespace GridKit PhasorDynamics::SignalNode u_node; PhasorDynamics::SignalNode vss_node; ScalarT u_value{0.5}; - IdxT u_index = 12; + IdxT u_index{12}; ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + IdxT vss_index{INVALID_INDEX}; u_node.set(&u_value, &u_index); vss_node.set(&vss_value, &vss_index); - auto data = makeTestData(); - PhasorDynamics::Stabilizer::Ieeest stab(data); - - stab.getSignals().template attachSignalNode(&u_node); - stab.getSignals().template assignSignalNode(&vss_node); - - stab.allocate(); - stab.initialize(); - setStatePoint(stab); - stab.evaluateResidual(); - - // Hand-computed answer key (see plan for full derivation) - const std::vector res_answer = { - 0.19, // f[0]: -x1_dot + x2 - 0.28, // f[1]: -x2_dot + x3 - 0.37, // f[2]: -x3_dot + x4 - 1.0975, // f[3]: -x4_dot + (-a0*x1 - a1*x2 - a2*x3 - a3*x4 + u) / a4 - 0.25, // f[4]: -T2*x5_dot - x5 + v4 - 0.24, // f[5]: -T4*x6_dot - x6 + v5 - -0.05, // f[6]: -T6*x7_dot - x7 + v6 - -0.42, // f[7]: -v4 + x1 + A5*x2 + A6*x3 - -0.25, // f[8]: -T2*(v5 - x5) + T1*(v4 - x5) - -0.31, // f[9]: -T4*(v6 - x6) + T3*(v5 - x6) - 5.75, // f[10]: -T6*v7 + Ks*T5*(v6 - x7) - 0.0, // f[11]: limiter (v7=0.05 within [-0.1, 0.1]) - }; + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); + + model.allocate(); + model.initialize(); + + const auto y_values = stateValues(); + const auto yp_values = derivativeValues(); + for (size_t i = 0; i < y_values.size(); ++i) + { + model.y()[i] = y_values[i]; + model.yp()[i] = yp_values[i]; + } + + model.evaluateResidual(); - // Looser tolerance for f[11] — Math::clamp is a smooth ramp approximation. + // The smooth clamp on the VSS row carries approximation error, so + // that row is compared with a looser tolerance. + const auto VSS = static_cast(InternalVariables::VSS); const auto loose_tol = static_cast(1.0e-4); - auto& residual = stab.getResidual(); + const auto expected = expectedResidual(); + for (size_t i = 0; i < expected.size(); ++i) + { + const auto test_tol = (i == VSS) ? loose_tol : tol_; + if (!isEqual(model.getResidual()[i], expected[i], test_tol)) + { + std::cout << "Incorrect residual for order " << order + << " row " << i << ": " + << std::setprecision(15) << model.getResidual()[i] + << " != " << expected[i] << "\n"; + success = false; + } + } - for (size_t i = 0; i < res_answer.size(); ++i) + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); + } + + template + TestOutcome tags() + { + TestStatus success = true; + + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + model.getSignals().template attachSignalNode(&u_node); + + model.allocate(); + + for (size_t i = 0; i < model.tag().size(); ++i) { - auto test_tol = (i == 11) ? loose_tol : static_cast(10 * std::numeric_limits::epsilon()); - if (!isEqual(residual[i], res_answer[i], test_tol)) + if (model.tag()[i]) { - std::cout << "Incorrect result for residual " << i << ": " - << std::setprecision(15) << residual[i] - << " != " << res_answer[i] << "\n"; + std::cout << "Differential tag set before tagDifferentiable at row " << i << "\n"; success = false; } } - // Verify output signal reads the stabilizer output - success *= isEqual(vss_node.read(), static_cast(0.05), loose_tol); + model.tagDifferentiable(); - return success.report(__func__); + const auto X7 = static_cast(InternalVariables::X7); + for (size_t i = 0; i < model.tag().size(); ++i) + { + const bool expected = (i <= X7); + if (model.tag()[i] != expected) + { + std::cout << "Incorrect differential tag at row " << i << "\n"; + success = false; + } + } + + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); } -#ifdef GRIDKIT_ENABLE_ENZYME - /** - * @brief Compare DependencyTracking Jacobian against Enzyme Jacobian. - */ - TestOutcome jacobian() + TestOutcome verify() { TestStatus success = true; + using Params = PhasorDynamics::Stabilizer::IeeestParameters; - auto data = makeTestData(); + // Missing input signal fails verification. + { + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); + model.allocate(); + success *= (model.verify() != 0); + } - std::vector - dependency_tracking_jacobian = DependencyTrackingJacobian(data); + // Attached but unlinked input signal fails verification. + { + PhasorDynamics::SignalNode u_node; + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); + } - std::vector - enzyme_jacobian = EnzymeJacobian(data); + // Parameters implying a different order fail verification. + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); + } - // Compare DependencyTracking dependencies to Enzyme's - auto tol = 10 * std::numeric_limits::epsilon(); - for (size_t i = 0; i < dependency_tracking_jacobian.size(); ++i) + // First-order notch with a second-order numerator fails verification. + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + auto data = makeOrderData<1>(); + data.parameters[Params::A6] = static_cast(0.6); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); + } + + // First-order notch with a first-order numerator verifies. + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<1>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() == 0); + } + + // Zeroth-order notch with a nonzero numerator fails verification. + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + auto data = makeOrderData<0>(); + data.parameters[Params::A5] = static_cast(0.5); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); + } + + // Zeroth-order pass-through verifies. { - success *= (GridKit::Testing::isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol)); + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<0>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() == 0); } return success.report(__func__); } - private: - std::vector DependencyTrackingJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) + TestOutcome factory() + { + TestStatus success = true; + + success *= checkFactoryOrder<0>(); + success *= checkFactoryOrder<1>(); + success *= checkFactoryOrder<2>(); + success *= checkFactoryOrder<3>(); + success *= checkFactoryOrder<4>(); + + // A null output node is allowed. + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.5}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + auto* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeOrderData<4>(), &u_node, nullptr); + stabilizer->allocate(); + success *= (stabilizer->verify() == 0); + delete stabilizer; + } + + // A null input node constructs, but fails verification. + { + PhasorDynamics::SignalNode vss_node; + ScalarT vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; + vss_node.set(&vss_value, &vss_index); + + auto* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeOrderData<4>(), nullptr, &vss_node); + stabilizer->allocate(); + success *= (stabilizer->verify() != 0); + delete stabilizer; + } + + return success.report(__func__); + } + + /// Symmetric notch filter (A1 = A3 = 0): a standard fourth-order + /// configuration with zero interior denominator coefficients + /// (a1 = a3 = 0) + /// + /// The Enzyme-vs-dependency-tracking Jacobian comparison is not run for + /// this configuration: Enzyme's `sparse_store` drops exact-zero entries + /// (the a1/a3 columns) while dependency tracking retains them, so the + /// key sets legitimately differ. + TestOutcome symmetricNotch() { - using DepVar = DependencyTracking::Variable; + TestStatus success = true; - // Set up signal nodes with DependencyTracking scalar type - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - DepVar u_value{0.5}; - IdxT u_index = 12; - DepVar vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + PhasorDynamics::SignalNode u_node; + PhasorDynamics::SignalNode vss_node; + ScalarT u_value{0.5}; + IdxT u_index{12}; + ScalarT vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; u_node.set(&u_value, &u_index); vss_node.set(&vss_value, &vss_index); - PhasorDynamics::Stabilizer::Ieeest stab(ieeestdata); - stab.getSignals().template attachSignalNode(&u_node); - stab.getSignals().template assignSignalNode(&vss_node); + auto* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeSymmetricNotchData(), &u_node, &vss_node); - stab.allocate(); - stab.initialize(); + // a = (0, A2 + A4, 0, A2 * A4): the factory must dispatch to order 4. + success *= (stabilizer->size() == static_cast(InternalVariables<4>::MAXIMUM)); + success *= (stabilizer->allocate() == 0); + success *= (stabilizer->verify() == 0); + success *= (stabilizer->initialize() == 0); + success *= (stabilizer->evaluateResidual() == 0); - // --- d/dy: tag internal variables as independent --- - for (size_t i = 0; i < stab.size(); ++i) + const auto y_values = stateValues<4>(); + const auto yp_values = derivativeValues<4>(); + for (size_t i = 0; i < y_values.size(); ++i) { - stab.y()[i].setVariableNumber(i); + stabilizer->y()[i] = y_values[i]; + stabilizer->yp()[i] = yp_values[i]; } - // Tag external signal u as an additional independent variable - u_value.setVariableNumber(stab.size()); - u_value.setValue(0.5); - setStatePointDep(stab); + stabilizer->evaluateResidual(); - stab.evaluateResidual(); - std::vector residual_y = stab.getResidual(); + // Derived with the a1 and a3 residual terms dropped: + // x4_rhs = (0.5 - 0.1 - 0.6 * 0.3) / 0.08 = 2.75 + const std::vector expected = + {0.19, 0.28, 0.37, 2.71, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}; - // --- d/dy': tag derivatives as independent --- - stab.initialize(); - for (size_t i = 0; i < stab.size(); ++i) + // The smooth clamp on the VSS row carries approximation error, so + // that row is compared with a looser tolerance. + const auto VSS = static_cast(InternalVariables<4>::VSS); + const auto loose_tol = static_cast(1.0e-4); + for (size_t i = 0; i < expected.size(); ++i) { - stab.yp()[i].setVariableNumber(i); + const auto test_tol = (i == VSS) ? loose_tol : tol_; + if (!isEqual(stabilizer->getResidual()[i], expected[i], test_tol)) + { + std::cout << "Incorrect symmetric-notch residual row " << i << ": " + << std::setprecision(15) << stabilizer->getResidual()[i] + << " != " << expected[i] << "\n"; + success = false; + } } - u_value = 0.5; - setStatePointDep(stab); + delete stabilizer; + return success.report(__func__); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + template + TestOutcome jacobian() + { + TestStatus success = true; + using DepVar = DependencyTracking::Variable; + + // The input signal takes the first global index after the model block + // so the dependency-tracking and Enzyme variable numbers align. + const IdxT u_index_value = static_cast(InternalVariables::MAXIMUM); - stab.evaluateResidual(); - std::vector residual_yp = stab.getResidual(); + const auto y_values = stateValues(); + const auto yp_values = derivativeValues(); - // Print dependencies for debugging - for (size_t i = 0; i < residual_y.size(); ++i) - { - std::cout << i << "th residual, y: "; - (residual_y[i]).print(std::cout); - std::cout << "\n"; - std::cout << i << "th residual, yp: "; - (residual_yp[i]).print(std::cout); - std::cout << "\n"; - } + std::vector dependency_tracking_jacobian; - // Merge d/dy and d/dy' into a single dependency map - std::vector dependencies(residual_y.size()); - for (IdxT i = 0; i < residual_y.size(); ++i) { - auto dependency_y = (residual_y[i]).getDependencies(); - auto dependency_yp = (residual_yp[i]).getDependencies(); + PhasorDynamics::SignalNode u_node; + PhasorDynamics::SignalNode vss_node; + DepVar u_value{0.5}; + IdxT u_index{u_index_value}; + DepVar vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; + + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); - for (const auto& pair_y : dependency_y) + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); + + model.allocate(); + model.initialize(); + + for (size_t i = 0; i < model.size(); ++i) + { + model.y()[i].setVariableNumber(i); + } + u_value.setVariableNumber(model.size()); + u_value.setValue(0.5); + + for (size_t i = 0; i < y_values.size(); ++i) + { + model.y()[i].setValue(y_values[i]); + } + for (size_t i = 0; i < yp_values.size(); ++i) + { + model.yp()[i].setValue(yp_values[i]); + } + + model.evaluateResidual(); + std::vector residual_y = model.getResidual(); + + model.initialize(); + for (size_t i = 0; i < model.size(); ++i) + { + model.y()[i] = model.y()[i].getValue(); + model.yp()[i].setVariableNumber(i); + } + u_value = 0.5; + + for (size_t i = 0; i < y_values.size(); ++i) { - auto it_yp = dependency_yp.find(pair_y.first); - if (it_yp != dependency_yp.end()) + model.y()[i].setValue(y_values[i]); + } + for (size_t i = 0; i < yp_values.size(); ++i) + { + model.yp()[i].setValue(yp_values[i]); + } + + model.evaluateResidual(); + std::vector residual_yp = model.getResidual(); + + dependency_tracking_jacobian.resize(residual_y.size()); + for (size_t i = 0; i < residual_y.size(); ++i) + { + auto dependency_y = residual_y[i].getDependencies(); + auto dependency_yp = residual_yp[i].getDependencies(); + + for (const auto& pair_y : dependency_y) { - dependencies[i].insert(std::make_pair(pair_y.first, pair_y.second + it_yp->second)); + auto it_yp = dependency_yp.find(pair_y.first); + if (it_yp != dependency_yp.end()) + { + dependency_tracking_jacobian[i].insert(std::make_pair(pair_y.first, pair_y.second + it_yp->second)); + } + else + { + dependency_tracking_jacobian[i].insert(std::make_pair(pair_y.first, pair_y.second)); + } } - else + + for (const auto& pair_yp : dependency_yp) { - dependencies[i].insert(std::make_pair(pair_y.first, pair_y.second)); + if (!dependency_y.contains(pair_yp.first)) + { + dependency_tracking_jacobian[i].insert(std::make_pair(pair_yp.first, pair_yp.second)); + } } } + } + + std::vector enzyme_jacobian; + + { + PhasorDynamics::SignalNode u_node; + PhasorDynamics::SignalNode vss_node; + ScalarT u_value{0.5}; + IdxT u_index{u_index_value}; + ScalarT vss_value{0.0}; + IdxT vss_index{INVALID_INDEX}; + + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); + + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); - // Insert yp dependencies that did not exist in the y dependencies - for (const auto& pair_yp : dependency_yp) + model.allocate(); + model.initialize(); + + for (size_t i = 0; i < y_values.size(); ++i) { - if (!dependency_y.contains(pair_yp.first)) - { - dependencies[i].insert(std::make_pair(pair_yp.first, pair_yp.second)); - } + model.y()[i] = y_values[i]; + model.yp()[i] = yp_values[i]; } + + model.updateTime(0.0, 1.0); + model.evaluateResidual(); + model.evaluateJacobian(); + model.constructCsr(); + auto model_jacobian = model.getCsrJacobian(); + enzyme_jacobian = GridKit::Testing::MapFromCsr(model_jacobian); + } + + for (size_t i = 0; i < dependency_tracking_jacobian.size(); ++i) + { + success *= GridKit::Testing::isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol_); } - return dependencies; + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); + } +#endif + + private: + static constexpr ScalarT tol_ = 10 * std::numeric_limits::epsilon(); + + static std::string orderedName(const char* funcname, size_t order) + { + return std::string(funcname) + " (order " + std::to_string(order) + ")"; } - std::vector EnzymeJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) + template + bool checkConstructedSize() + { + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + + bool success = (model.size() == static_cast(InternalVariables::MAXIMUM)); + // `order` notch states + X5..X7 + V4..V7 + VSS + success = success && (model.size() == static_cast(order + 8)); + success = success && (model.getMonitor() != nullptr); + + return success; + } + + template + bool checkFactoryOrder() { PhasorDynamics::SignalNode u_node; PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.5}; - IdxT u_index = 12; + ScalarT u_value{0.25}; + IdxT u_index{12}; ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + IdxT vss_index{INVALID_INDEX}; u_node.set(&u_value, &u_index); vss_node.set(&vss_value, &vss_index); - PhasorDynamics::Stabilizer::Ieeest stab(ieeestdata); - stab.getSignals().template attachSignalNode(&u_node); - stab.getSignals().template assignSignalNode(&vss_node); + auto* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeOrderData(), &u_node, &vss_node); - stab.allocate(); - stab.initialize(); - setStatePoint(stab); + bool success = (stabilizer->size() == static_cast(InternalVariables::MAXIMUM)); + success = success && (stabilizer->allocate() == 0); + success = success && (stabilizer->verify() == 0); + success = success && (stabilizer->initialize() == 0); + success = success && vss_node.linked(); + success = success && (vss_node.getVariableIndex() == static_cast(InternalVariables::VSS)); - stab.updateTime(0.0, 1.0); // alpha = 1.0 to verify d/dy' term - - stab.evaluateResidual(); - stab.evaluateJacobian(); - stab.constructCsr(); - auto model_jacobian = stab.getCsrJacobian(); - std::cout << "Sparse Csr Matrix: Ieeest Jacobian\n"; - model_jacobian->print(); + if (!success) + { + std::cout << "Factory checks failed for order " << order << "\n"; + } - return GridKit::Testing::MapFromCsr(model_jacobian); + delete stabilizer; + return success; } -#endif - - private: - static constexpr ScalarT tol_ = 10 * std::numeric_limits::epsilon(); - /** - * @brief Standard IEEEST parameter set for all tests. - * Derived: a0=1, a1=0.4, a2=0.63, a3=0.1, a4=0.08 - */ - auto makeTestData() -> PhasorDynamics::Stabilizer::IeeestData + auto makeData() -> DataT { using Params = PhasorDynamics::Stabilizer::IeeestParameters; - PhasorDynamics::Stabilizer::IeeestData data; + DataT data; data.device_class = "stabilizer"; data.disambiguation_string = "ieeest_test"; data.monitored_variables.insert(PhasorDynamics::Stabilizer::IeeestMonitorableVariables::vss); @@ -354,60 +706,134 @@ namespace GridKit return data; } - /** - * @brief Set a non-trivial operating point for residual/Jacobian tests. - * Avoids zeros and ones to catch coefficient errors. - */ - void setStatePoint(PhasorDynamics::Stabilizer::Ieeest& stab) + /// Base data with A1 = A3 = 0: still fourth order, with a1 = a3 = 0 + auto makeSymmetricNotchData() -> DataT { - stab.y()[0] = 0.1; // x1 - stab.y()[1] = 0.2; // x2 - stab.y()[2] = 0.3; // x3 - stab.y()[3] = 0.4; // x4 - stab.y()[4] = 0.5; // x5 - stab.y()[5] = 0.6; // x6 - stab.y()[6] = 0.7; // x7 - stab.y()[7] = 0.8; // v4 - stab.y()[8] = 0.9; // v5 - stab.y()[9] = 1.0; // v6 - stab.y()[10] = 0.05; // v7 (within limiter range) - stab.y()[11] = 0.05; // Vss (model output) - - stab.yp()[0] = 0.01; // x1_dot - stab.yp()[1] = 0.02; // x2_dot - stab.yp()[2] = 0.03; // x3_dot - stab.yp()[3] = 0.04; // x4_dot - stab.yp()[4] = 0.05; // x5_dot - stab.yp()[5] = 0.06; // x6_dot - stab.yp()[6] = 0.07; // x7_dot + using Params = PhasorDynamics::Stabilizer::IeeestParameters; + + auto data = makeData(); + data.parameters[Params::A1] = static_cast(0.0); + data.parameters[Params::A3] = static_cast(0.0); + + return data; + } + + /// Base data edited so the derived notch-filter order equals `order` + template + auto makeOrderData() -> DataT + { + using Params = PhasorDynamics::Stabilizer::IeeestParameters; + + auto data = makeData(); + + if constexpr (order == 3) + { + data.parameters[Params::A4] = static_cast(0.0); + } + else if constexpr (order == 2) + { + data.parameters[Params::A3] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + } + else if constexpr (order == 1) + { + data.parameters[Params::A2] = static_cast(0.0); + data.parameters[Params::A3] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + data.parameters[Params::A6] = static_cast(0.0); + } + else if constexpr (order == 0) + { + data.parameters[Params::A1] = static_cast(0.0); + data.parameters[Params::A2] = static_cast(0.0); + data.parameters[Params::A3] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + data.parameters[Params::A5] = static_cast(0.0); + data.parameters[Params::A6] = static_cast(0.0); + } + + return data; + } + + /// Test state: every variable keeps its named value at every order + /// (x1..x4 = 0.1..0.4, x5..x7 = 0.5..0.7, v4..v6 = 0.8..1.0, + /// v7 = vss = 0.05), so shared residual rows match across orders. + template + static std::vector stateValues() + { + if constexpr (order == 0) + { + return {0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.05, 0.05}; + } + else if constexpr (order == 1) + { + return {0.1, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.05, 0.05}; + } + else if constexpr (order == 2) + { + return {0.1, 0.2, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.05, 0.05}; + } + else if constexpr (order == 3) + { + return {0.1, 0.2, 0.3, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.05, 0.05}; + } + else + { + return {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 0.05, 0.05}; + } + } + + /// Test state derivatives (differential block only; algebraic rows zero) + template + static std::vector derivativeValues() + { + if constexpr (order == 0) + { + return {0.05, 0.06, 0.07, 0.0, 0.0, 0.0, 0.0, 0.0}; + } + else if constexpr (order == 1) + { + return {0.01, 0.05, 0.06, 0.07, 0.0, 0.0, 0.0, 0.0, 0.0}; + } + else if constexpr (order == 2) + { + return {0.01, 0.02, 0.05, 0.06, 0.07, 0.0, 0.0, 0.0, 0.0, 0.0}; + } + else if constexpr (order == 3) + { + return {0.01, 0.02, 0.03, 0.05, 0.06, 0.07, 0.0, 0.0, 0.0, 0.0, 0.0}; + } + else + { + return {0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.0, 0.0, 0.0, 0.0, 0.0}; + } } - /** - * @brief Set the same operating point for DependencyTracking variables. - * Uses setValue() to set the numeric value while preserving dependency info. - */ - void setStatePointDep(PhasorDynamics::Stabilizer::Ieeest& stab) + /// Expected residuals for `stateValues`/`derivativeValues` with + /// `makeOrderData` and u = 0.5, derived from the model equations + template + static std::vector expectedResidual() { - stab.y()[0].setValue(0.1); - stab.y()[1].setValue(0.2); - stab.y()[2].setValue(0.3); - stab.y()[3].setValue(0.4); - stab.y()[4].setValue(0.5); - stab.y()[5].setValue(0.6); - stab.y()[6].setValue(0.7); - stab.y()[7].setValue(0.8); - stab.y()[8].setValue(0.9); - stab.y()[9].setValue(1.0); - stab.y()[10].setValue(0.05); - stab.y()[11].setValue(0.05); - - stab.yp()[0].setValue(0.01); - stab.yp()[1].setValue(0.02); - stab.yp()[2].setValue(0.03); - stab.yp()[3].setValue(0.04); - stab.yp()[4].setValue(0.05); - stab.yp()[5].setValue(0.06); - stab.yp()[6].setValue(0.07); + if constexpr (order == 0) + { + return {0.25, 0.24, -0.01, -0.3, -0.25, -0.31, 1.15, 0.0}; + } + else if constexpr (order == 1) + { + return {3.99, 0.25, 0.24, -0.01, 1.3, -0.25, -0.31, 1.15, 0.0}; + } + else if constexpr (order == 2) + { + return {0.19, 1.88, 0.25, 0.24, -0.01, 0.54, -0.25, -0.31, 1.15, 0.0}; + } + else if constexpr (order == 3) + { + return {0.19, 0.28, 4.153333333333333, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}; + } + else + { + return {0.19, 0.28, 0.37, 1.0975, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}; + } } }; // class StabilizerIeeestTests diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index c1999c24a..888060d6a 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -7,10 +7,41 @@ int main() GridKit::Testing::StabilizerIeeestTests test; result += test.constructor(); - result += test.zeroInitialResidual(); - result += test.residual(); + + result += test.init<0>(); + result += test.init<1>(); + result += test.init<2>(); + result += test.init<3>(); + result += test.init<4>(); + + result += test.zeroInitialResidual<0>(); + result += test.zeroInitialResidual<1>(); + result += test.zeroInitialResidual<2>(); + result += test.zeroInitialResidual<3>(); + result += test.zeroInitialResidual<4>(); + + result += test.residual<0>(); + result += test.residual<1>(); + result += test.residual<2>(); + result += test.residual<3>(); + result += test.residual<4>(); + + result += test.tags<0>(); + result += test.tags<1>(); + result += test.tags<2>(); + result += test.tags<3>(); + result += test.tags<4>(); + + result += test.verify(); + result += test.factory(); + result += test.symmetricNotch(); + #ifdef GRIDKIT_ENABLE_ENZYME - result += test.jacobian(); + result += test.jacobian<0>(); + result += test.jacobian<1>(); + result += test.jacobian<2>(); + result += test.jacobian<3>(); + result += test.jacobian<4>(); #endif return result.summary();