From ac6e31937ee857c156982f396cab3e6556505c77 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 8 Jun 2026 04:44:18 -0500 Subject: [PATCH 1/6] Fix bad parameter initialization and polish docs --- .../Stabilizer/IEEEST/Ieeest.hpp | 18 +- .../Stabilizer/IEEEST/IeeestImpl.hpp | 105 +-- .../Stabilizer/IEEEST/README.md | 140 +++- .../PhasorDynamics/StabilizerIeeestTests.hpp | 620 +++++++++--------- .../runStabilizerIeeestTests.cpp | 4 +- 5 files changed, 497 insertions(+), 390 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index 4ef2dcb37..399398ae8 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -138,34 +138,18 @@ 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_; 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/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index 302e5bc63..c6d277ee9 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -43,6 +43,15 @@ namespace GridKit { } + template + void Ieeest::setDerivedParameters() + { + a1_ = A1_ + A3_; + a2_ = A2_ + A4_ + A1_ * A3_; + a3_ = A1_ * A4_ + A2_ * A3_; + a4_ = A2_ * A4_; + } + template void Ieeest::initializeParameters(const ModelDataT& data) { @@ -120,31 +129,7 @@ namespace GridKit Tdelay_ = std::get(data.parameters.at(Parameter::Tdelay)); } - a0_ = 1; - a1_ = A1_ + A3_; - a2_ = A2_ + A4_ + A1_ * A3_; - a3_ = A1_ * A4_ + A2_ * A3_; - a4_ = A2_ * 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_; - - 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_)); - - use_T2_block_ = static_cast(T2_ != 0.0); - bypass_T2_block_ = 1.0 - use_T2_block_; - - use_T4_block_ = static_cast(T4_ != 0.0); - bypass_T4_block_ = 1.0 - use_T4_block_; - - use_T6_block_ = static_cast(T6_ != 0.0); - bypass_T6_block_ = 1.0 - use_T6_block_; + setDerivedParameters(); } template @@ -183,6 +168,8 @@ namespace GridKit &y_[11], &(this->getVariableIndex(11))); } + tagDifferentiable(); + return 0; } @@ -205,7 +192,7 @@ namespace GridKit ret += 1; } - if (a4_ == 0 && a3_ == 0 && a2_ == 0 && a1_ != 0) + if (a2_ == ZERO && a3_ == ZERO && a4_ == ZERO && a1_ != ZERO) { Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; ret += 1; @@ -223,19 +210,53 @@ namespace GridKit yp_[static_cast(i)] = 0.0; } + ScalarT u{0.0}; + if (signals_.template isAttached()) + { + u = signals_.template readExternalVariable(); + ws_[0] = u; + ws_indices_[0] = signals_.template readExternalVariableIndex(); + } + + const ScalarT zero{0.0}; + const ScalarT x1 = u; + const ScalarT x2 = zero; + const ScalarT x3 = zero; + const ScalarT x4 = zero; + const ScalarT v4 = x1 + A5_ * x2 + A6_ * x3; + const ScalarT x5 = v4; + const ScalarT v5 = v4; + const ScalarT x6 = v5; + const ScalarT v6 = v5; + const ScalarT x7 = v6; + const ScalarT v7 = (T6_ == ZERO) ? Ks_ * v6 : zero; + + y_[0] = x1; + y_[1] = x2; + y_[2] = x3; + y_[3] = x4; + y_[4] = x5; + y_[5] = x6; + y_[6] = x7; + y_[7] = v4; + y_[8] = v5; + y_[9] = v6; + y_[10] = v7; + y_[11] = Math::clamp(v7, Lsmin_, Lsmax_); + return 0; } 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_[0] = (a2_ != ZERO || a3_ != ZERO || a4_ != ZERO); + tag_[1] = tag_[0]; + tag_[2] = (a3_ != ZERO || a4_ != ZERO); + tag_[3] = (a4_ != ZERO); + tag_[4] = (T2_ != ZERO); + tag_[5] = (T4_ != ZERO); + tag_[6] = (T6_ != ZERO); tag_[7] = false; tag_[8] = false; tag_[9] = false; @@ -295,19 +316,17 @@ namespace GridKit 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[0] = -tag_[0] * x1_dot + x2; + f[1] = -tag_[1] * x2_dot + x3; + f[2] = -tag_[2] * x3_dot + x4; + f[3] = -a4_ * x4_dot - x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u; 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[7] = -v4 + x1 + A5_ * x2 + A6_ * x3; + f[8] = tag_[4] * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + (1 - tag_[4]) * (v4 - v5); + f[9] = tag_[5] * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + (1 - tag_[5]) * (v5 - v6); + f[10] = tag_[6] * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + (1 - tag_[6]) * (Ks_ * v6 - v7); f[11] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); return 0; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index e5765ba1a..d1d5a8870 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -3,6 +3,12 @@ Standard IEEE power system stabilizer: 4th-order notch filter, two lead–lag blocks, washout, and output limiter. +Notes: +- $V_{cl}$, $V_{cu}$, and $T_{delay}$ are accepted for input-format + compatibility but are not modeled. +- A zero denominator time constant bypasses its corresponding lead–lag or + washout block. + ## Block Diagram ![](../../../../../docs/Figures/stabilizer_ieeest_diagram.png) @@ -11,33 +17,43 @@ 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$ | [s] | `A1` | Notch denominator coefficient | 1.013 | +$A_2$ | [s²] | `A2` | Notch denominator coefficient | 0.013 | +$A_3$ | [s] | `A3` | Notch denominator coefficient | 0.0 | +$A_4$ | [s²] | `A4` | Notch denominator coefficient | 0.0 | +$A_5$ | [s] | `A5` | Notch numerator coefficient | 1.013 | +$A_6$ | [s²] | `A6` | Notch numerator coefficient | 0.113 | +$T_1$ | [s] | `T1` | Lead–lag 1 numerator time constant | 0.0 | +$T_2$ | [s] | `T2` | Lead–lag 1 denominator time constant | 0.02 | +$T_3$ | [s] | `T3` | Lead–lag 2 numerator time constant | 0.0 | +$T_4$ | [s] | `T4` | Lead–lag 2 denominator time constant | 0.0 | +$T_5$ | [s] | `T5` | Washout numerator time constant | 1.65 | +$T_6$ | [s] | `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_{cl}$ | [p.u.] | `Vcl` | Lower input cutout threshold | 0.0 | Accepted but not modeled +$V_{cu}$ | [p.u.] | `Vcu` | Upper input cutout threshold | 0.0 | Accepted but not modeled +$T_\text{delay}$ | [s] | `Tdelay` | Input delay | 0.0 | Accepted but not modeled + +### Parameter Validation + +The fixed realization rejects a first-order-only notch denominator: + +```math +\begin{aligned} + a_2 \ne 0 \lor a_3 \ne 0 \lor a_4 \ne 0 \lor a_1 = 0 +\end{aligned} +``` + +### Model Derived Parameters + +The notch-filter denominator expands to: ```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 \\ @@ -45,6 +61,24 @@ a_4 &= A_2 A_4 \end{aligned} ``` +The binary DAE selectors choose the active notch-filter order: + +```math +\begin{aligned} +\delta_1 &= \delta_2 = +\begin{cases} +1 & a_2 \ne 0 \lor a_3 \ne 0 \lor a_4 \ne 0 \\ +0 & \text{otherwise} +\end{cases} +\\ +\delta_3 &= +\begin{cases} +1 & a_3 \ne 0 \lor a_4 \ne 0 \\ +0 & \text{otherwise} +\end{cases} +\end{aligned} +``` + ## Model Variables ### Internal Variables @@ -58,6 +92,9 @@ $x_5$ | [-] | Lead–lag 1 state $x_6$ | [-] | Lead–lag 2 state $x_7$ | [-] | Washout state +For reduced-order notch filters, unused notch states remain in the fixed +component state vector and are pinned by algebraic residuals. + #### Algebraic Symbol | Units | Description @@ -82,10 +119,10 @@ $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 &= -\delta_1\dot{x}_1 + x_2 \\ +0 &= -\delta_2\dot{x}_2 + x_3 \\ +0 &= -\delta_3\dot{x}_3 + x_4 \\ +0 &= -a_4\dot{x}_4 - x_1 - a_1x_2 - a_2x_3 - a_3x_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 @@ -96,10 +133,22 @@ $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_4 + x_1 + A_5x_2 + A_6x_3 \\ +0 &= +\begin{cases} +-T_2(v_5 - x_5) + T_1(v_4 - x_5) & T_2 \ne 0 \\ +v_4 - v_5 & T_2 = 0 +\end{cases} \\ +0 &= +\begin{cases} +-T_4(v_6 - x_6) + T_3(v_5 - x_6) & T_4 \ne 0 \\ +v_5 - v_6 & T_4 = 0 +\end{cases} \\ +0 &= +\begin{cases} +-T_6 v_7 + K_s T_5(v_6 - x_7) & T_6 \ne 0 \\ +K_s v_6 - v_7 & T_6 = 0 +\end{cases} \\ 0 &= -V_{ss} + \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) \end{aligned} ``` @@ -109,6 +158,27 @@ The output limiter uses GridKit's smooth ## 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$. +States and derivatives initialize to the steady state implied by the attached +input $u$: + +```math +\begin{aligned} +x_1 &= v_4 = x_5 = v_5 = x_6 = v_6 = x_7 = u \\ +x_2 &= x_3 = x_4 = 0 \\ +v_7 &= +\begin{cases} +K_su & T_6 = 0 \\ +0 & \text{otherwise} +\end{cases} +& +V_{ss} &= \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) +\end{aligned} +``` + +All internal derivatives initialize to zero. + +## Model Outputs + +Output | Units | Description | Note +-------|--------|---------------------------|----- +`vss` | [p.u.] | Limited stabilizer signal | Exported through `output` when assigned diff --git a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp index 15b64876f..e960ae308 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include #include +#include #include #include @@ -22,312 +24,400 @@ namespace GridKit { public: using RealT = typename PhasorDynamics::Component::RealT; + using DataT = PhasorDynamics::Stabilizer::IeeestData; StabilizerIeeestTests() = default; ~StabilizerIeeestTests() = default; - TestOutcome constructor() + TestOutcome init() { TestStatus success = true; - auto data = makeTestData(); - auto* stab = new PhasorDynamics::Stabilizer::Ieeest(data); - - success *= (stab != nullptr); - success *= (stab->getMonitor() != nullptr); - - delete stab; + using Params = PhasorDynamics::Stabilizer::IeeestParameters; - return success.report(__func__); - } + struct InitCase + { + RealT T6; + RealT Ks; + ScalarT u; + RealT Lsmin; + RealT Lsmax; + ScalarT raw_v7; + ScalarT expected_vss; + }; - /** - * @brief All states initialize to zero (stabilizer at rest). - * With u = 0, all residuals should be zero. - */ - TestOutcome zeroInitialResidual() - { - TestStatus success = true; + 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, 1.0, 0.25, -1.0, 1.0, 0.25, 0.25}, + {0.0, 2.0, 0.25, -1.0, 1.0, 0.50, 0.50}, + {0.0, 4.0, 0.25, -1.0, 0.6, 1.00, 0.60}, + {5.0, 3.0, 0.25, -1.0, 1.0, 0.00, 0.00}, + }; - // 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; - - // Link signal nodes to backing storage - u_node.set(&u_value, &u_index); - vss_node.set(&vss_value, &vss_index); - - auto data = makeTestData(); - PhasorDynamics::Stabilizer::Ieeest stab(data); - - // Wire: stabilizer reads u_node as input, writes vss_node as output - stab.getSignals().template attachSignalNode(&u_node); - stab.getSignals().template assignSignalNode(&vss_node); - - stab.allocate(); - success *= (stab.verify() == 0); - stab.initialize(); - stab.evaluateResidual(); - - auto tol = 10 * std::numeric_limits::epsilon(); - const auto& f = stab.getResidual(); - for (size_t i = 0; i < f.size(); ++i) + for (const auto& test : cases) { - if (!isEqual(f[i], 0.0, tol)) - { - std::cout << "Non-zero residual at index " << i << ": " << f[i] << "\n"; - success = false; - } + 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 = makeData(); + 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_node); + + model.allocate(); + success *= (model.verify() == 0); + model.initialize(); + + success *= vss_node.linked(); + success *= (vss_node.getVariableIndex() == 11); + success *= isEqual(model.y()[10], test.raw_v7, tol_); + success *= isEqual(model.y()[11], test.expected_vss, loose_tol); + success *= isEqual(vss_node.read(), test.expected_vss, loose_tol); } - // 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__); } - /** - * @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. - */ TestOutcome residual() { TestStatus success = true; - 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); - - 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]) + using Params = PhasorDynamics::Stabilizer::IeeestParameters; + + struct ResidualCase + { + const char* name; + std::function edit; + const std::vector expected; }; - // Looser tolerance for f[11] — Math::clamp is a smooth ramp approximation. - const auto loose_tol = static_cast(1.0e-4); - auto& residual = stab.getResidual(); + const std::vector cases = { + {"baseline", + [](DataT&) {}, + {0.19, 0.28, 0.37, 0.0878, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, + {"a4_zero", + [](DataT& data) + { + data.parameters[Params::A4] = static_cast(0.0); + }, + {0.19, 0.28, 0.37, 0.227, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, + {"a3_a4_zero", + [](DataT& data) + { + data.parameters[Params::A3] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + }, + {0.19, 0.28, 0.40, 0.32, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, + {"time_zero", + [](DataT& data) + { + data.parameters[Params::T2] = static_cast(0.0); + data.parameters[Params::T4] = static_cast(0.0); + data.parameters[Params::T6] = static_cast(0.0); + }, + {0.19, 0.28, 0.37, 0.0878, 0.30, 0.30, 0.30, -0.42, -0.10, -0.10, 9.95, 0.0}}, + }; - for (size_t i = 0; i < res_answer.size(); ++i) + const auto loose_tol = static_cast(1.0e-4); + for (const auto& test : cases) { - auto test_tol = (i == 11) ? loose_tol : static_cast(10 * std::numeric_limits::epsilon()); - if (!isEqual(residual[i], res_answer[i], test_tol)) + 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); + + auto data = makeData(); + test.edit(data); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode(&vss_node); + + model.allocate(); + model.initialize(); + + model.y()[0] = 0.1; + model.y()[1] = 0.2; + model.y()[2] = 0.3; + model.y()[3] = 0.4; + model.y()[4] = 0.5; + model.y()[5] = 0.6; + model.y()[6] = 0.7; + model.y()[7] = 0.8; + model.y()[8] = 0.9; + model.y()[9] = 1.0; + model.y()[10] = 0.05; + model.y()[11] = 0.05; + + model.yp()[0] = 0.01; + model.yp()[1] = 0.02; + model.yp()[2] = 0.03; + model.yp()[3] = 0.04; + model.yp()[4] = 0.05; + model.yp()[5] = 0.06; + model.yp()[6] = 0.07; + + model.evaluateResidual(); + + for (size_t i = 0; i < test.expected.size(); ++i) { - std::cout << "Incorrect result for residual " << i << ": " - << std::setprecision(15) << residual[i] - << " != " << res_answer[i] << "\n"; - success = false; + auto test_tol = (i == 11) ? loose_tol : tol_; + if (!isEqual(model.getResidual()[i], test.expected[i], test_tol)) + { + std::cout << "Incorrect residual for " << test.name + << " row " << i << ": " + << std::setprecision(15) << model.getResidual()[i] + << " != " << test.expected[i] << "\n"; + success = false; + } } } - // Verify output signal reads the stabilizer output - success *= isEqual(vss_node.read(), static_cast(0.05), loose_tol); - return success.report(__func__); } -#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(); - - std::vector - dependency_tracking_jacobian = DependencyTrackingJacobian(data); + { + PhasorDynamics::Stabilizer::Ieeest model(makeData()); + model.allocate(); + success *= (model.verify() != 0); + } - std::vector - enzyme_jacobian = EnzymeJacobian(data); + { + PhasorDynamics::SignalNode u_node; + PhasorDynamics::Stabilizer::Ieeest model(makeData()); + 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) { - 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); + + auto data = makeData(); + data.parameters[Params::A1] = static_cast(1.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); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); } return success.report(__func__); } - private: - std::vector DependencyTrackingJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome jacobian() { - using DepVar = DependencyTracking::Variable; - - // 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; - - 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); - - stab.allocate(); - stab.initialize(); - - // --- d/dy: tag internal variables as independent --- - for (size_t i = 0; i < stab.size(); ++i) - { - stab.y()[i].setVariableNumber(i); - } - // Tag external signal u as an additional independent variable - u_value.setVariableNumber(stab.size()); - u_value.setValue(0.5); - - setStatePointDep(stab); + TestStatus success = true; + using DepVar = DependencyTracking::Variable; - stab.evaluateResidual(); - std::vector residual_y = stab.getResidual(); + std::vector dependency_tracking_jacobian; - // --- d/dy': tag derivatives as independent --- - stab.initialize(); - for (size_t i = 0; i < stab.size(); ++i) { - stab.yp()[i].setVariableNumber(i); - } + 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}; - u_value = 0.5; - setStatePointDep(stab); + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); - stab.evaluateResidual(); - std::vector residual_yp = stab.getResidual(); - - // 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"; - } + PhasorDynamics::Stabilizer::Ieeest model(makeData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode(&vss_node); - // 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(); + model.allocate(); + model.initialize(); - for (const auto& pair_y : dependency_y) + for (size_t i = 0; i < model.size(); ++i) { - auto it_yp = dependency_yp.find(pair_y.first); - if (it_yp != dependency_yp.end()) - { - dependencies[i].insert(std::make_pair(pair_y.first, pair_y.second + it_yp->second)); - } - else + model.y()[i].setVariableNumber(i); + } + u_value.setVariableNumber(model.size()); + u_value.setValue(0.5); + + model.y()[0].setValue(0.1); + model.y()[1].setValue(0.2); + model.y()[2].setValue(0.3); + model.y()[3].setValue(0.4); + model.y()[4].setValue(0.5); + model.y()[5].setValue(0.6); + model.y()[6].setValue(0.7); + model.y()[7].setValue(0.8); + model.y()[8].setValue(0.9); + model.y()[9].setValue(1.0); + model.y()[10].setValue(0.05); + model.y()[11].setValue(0.05); + + model.yp()[0].setValue(0.01); + model.yp()[1].setValue(0.02); + model.yp()[2].setValue(0.03); + model.yp()[3].setValue(0.04); + model.yp()[4].setValue(0.05); + model.yp()[5].setValue(0.06); + model.yp()[6].setValue(0.07); + + 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; + + model.y()[0].setValue(0.1); + model.y()[1].setValue(0.2); + model.y()[2].setValue(0.3); + model.y()[3].setValue(0.4); + model.y()[4].setValue(0.5); + model.y()[5].setValue(0.6); + model.y()[6].setValue(0.7); + model.y()[7].setValue(0.8); + model.y()[8].setValue(0.9); + model.y()[9].setValue(1.0); + model.y()[10].setValue(0.05); + model.y()[11].setValue(0.05); + + model.yp()[0].setValue(0.01); + model.yp()[1].setValue(0.02); + model.yp()[2].setValue(0.03); + model.yp()[3].setValue(0.04); + model.yp()[4].setValue(0.05); + model.yp()[5].setValue(0.06); + model.yp()[6].setValue(0.07); + + model.evaluateResidual(); + std::vector residual_yp = model.getResidual(); + + dependency_tracking_jacobian.resize(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(); + + for (const auto& pair_y : dependency_y) { - dependencies[i].insert(std::make_pair(pair_y.first, pair_y.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)); + } } - } - // Insert yp dependencies that did not exist in the y dependencies - for (const auto& pair_yp : dependency_yp) - { - if (!dependency_y.contains(pair_yp.first)) + for (const auto& pair_yp : dependency_yp) { - dependencies[i].insert(std::make_pair(pair_yp.first, pair_yp.second)); + if (!dependency_y.contains(pair_yp.first)) + { + dependency_tracking_jacobian[i].insert(std::make_pair(pair_yp.first, pair_yp.second)); + } } } } - return dependencies; - } + std::vector enzyme_jacobian; - std::vector EnzymeJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) - { - 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); - - stab.allocate(); - stab.initialize(); - setStatePoint(stab); - - 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(); - - return GridKit::Testing::MapFromCsr(model_jacobian); + { + 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 model(makeData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode(&vss_node); + + model.allocate(); + model.initialize(); + + model.y()[0] = 0.1; + model.y()[1] = 0.2; + model.y()[2] = 0.3; + model.y()[3] = 0.4; + model.y()[4] = 0.5; + model.y()[5] = 0.6; + model.y()[6] = 0.7; + model.y()[7] = 0.8; + model.y()[8] = 0.9; + model.y()[9] = 1.0; + model.y()[10] = 0.05; + model.y()[11] = 0.05; + + model.yp()[0] = 0.01; + model.yp()[1] = 0.02; + model.yp()[2] = 0.03; + model.yp()[3] = 0.04; + model.yp()[4] = 0.05; + model.yp()[5] = 0.06; + model.yp()[6] = 0.07; + + 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 success.report(__func__); } #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); @@ -353,62 +443,6 @@ 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) - { - 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 - } - - /** - * @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) - { - 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); - } }; // class StabilizerIeeestTests } // namespace Testing diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index c1999c24a..2a76a60b0 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -6,9 +6,9 @@ int main() GridKit::Testing::StabilizerIeeestTests test; - result += test.constructor(); - result += test.zeroInitialResidual(); + result += test.init(); result += test.residual(); + result += test.verify(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); #endif From 2b37a48cc6af88f8c0cdf19902aed1e22b3185a1 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 8 Jun 2026 07:54:37 -0500 Subject: [PATCH 2/6] Update Docs --- .../Stabilizer/IEEEST/README.md | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index d1d5a8870..a0ea1064d 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -4,7 +4,7 @@ Standard IEEE power system stabilizer: 4th-order notch filter, two lead–lag blocks, washout, and output limiter. Notes: -- $V_{cl}$, $V_{cu}$, and $T_{delay}$ are accepted for input-format +- $V_{\mathrm{cl}}$, $V_{\mathrm{cu}}$, and $T_{\mathrm{delay}}$ are accepted for input-format compatibility but are not modeled. - A zero denominator time constant bypasses its corresponding lead–lag or washout block. @@ -17,26 +17,26 @@ Figure 1: Stabilizer IEEEST model. Figure courtesy of [PowerWorld](https://www.p ## Model Parameters -Symbol | Units | JSON | Description | Typical Value | Note --------------|--------|----------|--------------------------------------|---------------|------ -$A_1$ | [s] | `A1` | Notch denominator coefficient | 1.013 | -$A_2$ | [s²] | `A2` | Notch denominator coefficient | 0.013 | -$A_3$ | [s] | `A3` | Notch denominator coefficient | 0.0 | -$A_4$ | [s²] | `A4` | Notch denominator coefficient | 0.0 | -$A_5$ | [s] | `A5` | Notch numerator coefficient | 1.013 | -$A_6$ | [s²] | `A6` | Notch numerator coefficient | 0.113 | -$T_1$ | [s] | `T1` | Lead–lag 1 numerator time constant | 0.0 | -$T_2$ | [s] | `T2` | Lead–lag 1 denominator time constant | 0.02 | -$T_3$ | [s] | `T3` | Lead–lag 2 numerator time constant | 0.0 | -$T_4$ | [s] | `T4` | Lead–lag 2 denominator time constant | 0.0 | -$T_5$ | [s] | `T5` | Washout numerator time constant | 1.65 | -$T_6$ | [s] | `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_{cl}$ | [p.u.] | `Vcl` | Lower input cutout threshold | 0.0 | Accepted but not modeled -$V_{cu}$ | [p.u.] | `Vcu` | Upper input cutout threshold | 0.0 | Accepted but not modeled -$T_\text{delay}$ | [s] | `Tdelay` | Input delay | 0.0 | Accepted but not modeled +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 @@ -85,33 +85,37 @@ The binary DAE selectors choose the active notch-filter order: #### 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 For reduced-order notch filters, unused notch states remain in the fixed component state vector and are pinned by algebraic residuals. #### 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 | Description | Note +-------|--------|-------------------------|------ +$u$ | [p.u.] | Stabilizer input signal | ## Model Equations @@ -149,17 +153,19 @@ v_5 - v_6 & T_4 = 0 -T_6 v_7 + K_s T_5(v_6 - x_7) & T_6 \ne 0 \\ K_s v_6 - v_7 & T_6 = 0 \end{cases} \\ -0 &= -V_{ss} + \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) +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 -States and derivatives initialize to the steady state implied by the attached -input $u$: +Initialization is performed by evaluating the steady-state residuals in +dependency order. Let subscript $0$ denote initial values and set all internal +derivatives to zero. States and derivatives initialize to the steady state +implied by the attached input $u$: ```math \begin{aligned} @@ -171,7 +177,7 @@ K_su & T_6 = 0 \\ 0 & \text{otherwise} \end{cases} & -V_{ss} &= \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) +V_{\mathrm{ss}} &= \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) \end{aligned} ``` @@ -180,5 +186,5 @@ All internal derivatives initialize to zero. ## Model Outputs Output | Units | Description | Note --------|--------|---------------------------|----- +-------|--------|---------------------------|------ `vss` | [p.u.] | Limited stabilizer signal | Exported through `output` when assigned From da850e39f8a9e4529bcc9bf9b746ea9af752fdab Mon Sep 17 00:00:00 2001 From: Wyatt Lowery Date: Wed, 24 Jun 2026 19:23:11 -0500 Subject: [PATCH 3/6] isEqual change and tidy --- .../Stabilizer/IEEEST/IeeestImpl.hpp | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index c6d277ee9..a459302fe 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace GridKit @@ -22,6 +23,7 @@ namespace GridKit namespace Stabilizer { using Log = ::GridKit::Utilities::Logger; + using Testing::isEqual; template Ieeest::Ieeest() @@ -192,7 +194,7 @@ namespace GridKit ret += 1; } - if (a2_ == ZERO && a3_ == ZERO && a4_ == ZERO && a1_ != ZERO) + if (isEqual(a2_, ZERO) && isEqual(a3_, ZERO) && isEqual(a4_, ZERO) && !isEqual(a1_, ZERO)) { Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; ret += 1; @@ -229,7 +231,7 @@ namespace GridKit const ScalarT x6 = v5; const ScalarT v6 = v5; const ScalarT x7 = v6; - const ScalarT v7 = (T6_ == ZERO) ? Ks_ * v6 : zero; + const ScalarT v7 = isEqual(T6_, ZERO) ? Ks_ * v6 : zero; y_[0] = x1; y_[1] = x2; @@ -250,13 +252,13 @@ namespace GridKit template int Ieeest::tagDifferentiable() { - tag_[0] = (a2_ != ZERO || a3_ != ZERO || a4_ != ZERO); + tag_[0] = (!isEqual(a2_, ZERO) || !isEqual(a3_, ZERO) || !isEqual(a4_, ZERO)); tag_[1] = tag_[0]; - tag_[2] = (a3_ != ZERO || a4_ != ZERO); - tag_[3] = (a4_ != ZERO); - tag_[4] = (T2_ != ZERO); - tag_[5] = (T4_ != ZERO); - tag_[6] = (T6_ != ZERO); + tag_[2] = (!isEqual(a3_, ZERO) || !isEqual(a4_, ZERO)); + tag_[3] = (!isEqual(a4_, ZERO)); + tag_[4] = (!isEqual(T2_, ZERO)); + tag_[5] = (!isEqual(T4_, ZERO)); + tag_[6] = (!isEqual(T6_, ZERO)); tag_[7] = false; tag_[8] = false; tag_[9] = false; @@ -316,17 +318,26 @@ namespace GridKit ScalarT u = ws[0]; - f[0] = -tag_[0] * x1_dot + x2; - f[1] = -tag_[1] * x2_dot + x3; - f[2] = -tag_[2] * x3_dot + x4; + const ScalarT zero{ZERO}; + const ScalarT one{ONE}; + const ScalarT dx1 = tag_[0] ? one : zero; + const ScalarT dx2 = tag_[1] ? one : zero; + const ScalarT dx3 = tag_[2] ? one : zero; + const ScalarT t2 = tag_[4] ? one : zero; + const ScalarT t4 = tag_[5] ? one : zero; + const ScalarT t6 = tag_[6] ? one : zero; + + f[0] = -dx1 * x1_dot + x2; + f[1] = -dx2 * x2_dot + x3; + f[2] = -dx3 * x3_dot + x4; f[3] = -a4_ * x4_dot - x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u; f[4] = -T2_ * x5_dot - x5 + v4; f[5] = -T4_ * x6_dot - x6 + v5; f[6] = -T6_ * x7_dot - x7 + v6; f[7] = -v4 + x1 + A5_ * x2 + A6_ * x3; - f[8] = tag_[4] * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + (1 - tag_[4]) * (v4 - v5); - f[9] = tag_[5] * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + (1 - tag_[5]) * (v5 - v6); - f[10] = tag_[6] * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + (1 - tag_[6]) * (Ks_ * v6 - v7); + f[8] = t2 * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + (one - t2) * (v4 - v5); + f[9] = t4 * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + (one - t4) * (v5 - v6); + f[10] = t6 * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + (one - t6) * (Ks_ * v6 - v7); f[11] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); return 0; From bb61eadf4d21453414bc178ef63cda13bb29385a Mon Sep 17 00:00:00 2001 From: lukelowry Date: Fri, 26 Jun 2026 07:43:08 -0500 Subject: [PATCH 4/6] Hessenberg form --- .../Stabilizer/IEEEST/Ieeest.hpp | 22 + .../Stabilizer/IEEEST/IeeestImpl.hpp | 409 ++++++++++-------- .../Stabilizer/IEEEST/README.md | 175 ++++---- .../PhasorDynamics/StabilizerIeeestTests.hpp | 133 +++++- .../runStabilizerIeeestTests.cpp | 1 + 5 files changed, 470 insertions(+), 270 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index 399398ae8..1162eee26 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -6,6 +6,10 @@ #pragma once +#include +#include +#include + #include #include #include @@ -119,6 +123,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}; @@ -131,6 +137,9 @@ namespace GridKit RealT T4_{1}; RealT T5_{0}; RealT T6_{1}; + RealT T2_inv_{1}; + RealT T4_inv_{1}; + RealT T6_inv_{1}; RealT Ks_{1}; RealT Lsmin_{-0.1}; RealT Lsmax_{0.1}; @@ -143,6 +152,19 @@ namespace GridKit RealT a3_{0}; RealT a4_{0}; + IdxT order_{0}; + RealT s0_{1}; + RealT s1_{0}; + RealT s2_{0}; + RealT s3_{0}; + RealT s4_{0}; + RealT a1_inv_{0}; + RealT a2_inv_{0}; + RealT a3_inv_{0}; + RealT a4_inv_{0}; + + IdxT parameter_error_count_{0}; + ComponentSignals signals_; std::unique_ptr monitor_; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index a459302fe..785af0826 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -6,14 +6,14 @@ * @brief Definition of the IEEEST Power System Stabilizer. */ -#include +#include #include +#include #include #include #include #include -#include #include namespace GridKit @@ -23,12 +23,12 @@ namespace GridKit namespace Stabilizer { using Log = ::GridKit::Utilities::Logger; - using Testing::isEqual; template Ieeest::Ieeest() { - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); + setDerivedParameters(); } template @@ -37,7 +37,8 @@ namespace GridKit { initializeParameters(data); initializeMonitor(); - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); + setDerivedParameters(); } template @@ -48,90 +49,96 @@ namespace GridKit template void Ieeest::setDerivedParameters() { + T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); + T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); + T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); + T2_inv_ = ONE / T2_; + T4_inv_ = ONE / T4_; + T6_inv_ = ONE / T6_; + a1_ = A1_ + A3_; a2_ = A2_ + A4_ + A1_ * A3_; a3_ = A1_ * A4_ + A2_ * A3_; a4_ = A2_ * A4_; - } - template - void Ieeest::initializeParameters(const ModelDataT& data) - { - 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)) + order_ = 0; + if (a1_ != ZERO) { - Ks_ = std::get(data.parameters.at(Parameter::Ks)); + order_ = 1; } - if (data.parameters.contains(Parameter::Lsmin)) + if (a2_ != ZERO) { - Lsmin_ = std::get(data.parameters.at(Parameter::Lsmin)); + order_ = 2; } - if (data.parameters.contains(Parameter::Lsmax)) + if (a3_ != ZERO) { - Lsmax_ = std::get(data.parameters.at(Parameter::Lsmax)); + order_ = 3; } - if (data.parameters.contains(Parameter::Vcl)) + if (a4_ != ZERO) { - Vcl_ = std::get(data.parameters.at(Parameter::Vcl)); + order_ = 4; } - if (data.parameters.contains(Parameter::Vcu)) - { - Vcu_ = std::get(data.parameters.at(Parameter::Vcu)); - } - if (data.parameters.contains(Parameter::Tdelay)) + + s0_ = (order_ == 0) ? ONE : ZERO; + s1_ = (order_ == 1) ? ONE : ZERO; + s2_ = (order_ == 2) ? ONE : ZERO; + s3_ = (order_ == 3) ? ONE : ZERO; + s4_ = (order_ == 4) ? ONE : ZERO; + + a1_inv_ = (order_ == 1) ? ONE / a1_ : ZERO; + a2_inv_ = (order_ == 2) ? ONE / a2_ : ZERO; + a3_inv_ = (order_ == 3) ? ONE / a3_ : ZERO; + a4_inv_ = (order_ == 4) ? ONE / a4_ : ZERO; + } + + template + void Ieeest::initializeParameters(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; + + parameter_error_count_ = 0; + + auto load_real = [&](auto key, RealT& target, const char* name) { - Tdelay_ = std::get(data.parameters.at(Parameter::Tdelay)); - } + if (!data.parameters.contains(key)) + { + return; + } - setDerivedParameters(); + 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 @@ -144,19 +151,20 @@ namespace GridKit template int Ieeest::allocate() { + size_ = static_cast(IeeestInternalVariables::MAXIMUM); auto size = static_cast(size_); - f_.resize(size); - y_.resize(size); - yp_.resize(size); - tag_.resize(size); - abs_tol_.resize(size); + + f_.assign(size, ScalarT{0}); + y_.assign(size, ScalarT{0}); + yp_.assign(size, ScalarT{0}); + tag_.assign(size, false); + abs_tol_.assign(size, ScalarT{0}); 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_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); for (IdxT j = 0; j < size_; ++j) { @@ -167,7 +175,8 @@ namespace GridKit if (signals_.template isAssigned()) { signals_.template getSignalNode()->set( - &y_[11], &(this->getVariableIndex(11))); + &y_[static_cast(IeeestInternalVariables::VSS)], + &(this->getVariableIndex(static_cast(IeeestInternalVariables::VSS)))); } tagDifferentiable(); @@ -178,7 +187,7 @@ namespace GridKit template int Ieeest::verify() const { - int ret = 0; + int ret = static_cast(parameter_error_count_); if (signals_.template isAttached()) { @@ -194,9 +203,9 @@ namespace GridKit ret += 1; } - if (isEqual(a2_, ZERO) && isEqual(a3_, ZERO) && isEqual(a4_, ZERO) && !isEqual(a1_, ZERO)) + if (order_ == 1 && A6_ != ZERO) { - Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; + Log::error() << "Ieeest: unsupported first-order notch filter with second-order numerator\n"; ret += 1; } @@ -206,45 +215,45 @@ namespace GridKit 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; } - ScalarT u{0.0}; - if (signals_.template isAttached()) - { - u = signals_.template readExternalVariable(); - ws_[0] = u; - ws_indices_[0] = signals_.template readExternalVariableIndex(); - } - - const ScalarT zero{0.0}; - const ScalarT x1 = u; - const ScalarT x2 = zero; - const ScalarT x3 = zero; - const ScalarT x4 = zero; - const ScalarT v4 = x1 + A5_ * x2 + A6_ * x3; - const ScalarT x5 = v4; - const ScalarT v5 = v4; - const ScalarT x6 = v5; - const ScalarT v6 = v5; - const ScalarT x7 = v6; - const ScalarT v7 = isEqual(T6_, ZERO) ? Ks_ * v6 : zero; - - y_[0] = x1; - y_[1] = x2; - y_[2] = x3; - y_[3] = x4; - y_[4] = x5; - y_[5] = x6; - y_[6] = x7; - y_[7] = v4; - y_[8] = v5; - y_[9] = v6; - y_[10] = v7; - y_[11] = Math::clamp(v7, Lsmin_, Lsmax_); + 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 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(); + + y_[X1] = u; + y_[X2] = ZERO; + y_[X3] = ZERO; + y_[X4] = ZERO; + 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; } @@ -252,18 +261,19 @@ namespace GridKit template int Ieeest::tagDifferentiable() { - tag_[0] = (!isEqual(a2_, ZERO) || !isEqual(a3_, ZERO) || !isEqual(a4_, ZERO)); - tag_[1] = tag_[0]; - tag_[2] = (!isEqual(a3_, ZERO) || !isEqual(a4_, ZERO)); - tag_[3] = (!isEqual(a4_, ZERO)); - tag_[4] = (!isEqual(T2_, ZERO)); - tag_[5] = (!isEqual(T4_, ZERO)); - tag_[6] = (!isEqual(T6_, ZERO)); - tag_[7] = false; - tag_[8] = false; - tag_[9] = false; - tag_[10] = false; - tag_[11] = false; + auto index = [](IeeestInternalVariables variable) + { + return static_cast(variable); + }; + + std::fill(tag_.begin(), tag_.end(), false); + tag_[index(IeeestInternalVariables::X1)] = true; + tag_[index(IeeestInternalVariables::X2)] = true; + tag_[index(IeeestInternalVariables::X3)] = true; + tag_[index(IeeestInternalVariables::X4)] = true; + tag_[index(IeeestInternalVariables::X5)] = true; + tag_[index(IeeestInternalVariables::X6)] = true; + tag_[index(IeeestInternalVariables::X7)] = true; return 0; } @@ -295,50 +305,73 @@ namespace GridKit 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]; - - const ScalarT zero{ZERO}; - const ScalarT one{ONE}; - const ScalarT dx1 = tag_[0] ? one : zero; - const ScalarT dx2 = tag_[1] ? one : zero; - const ScalarT dx3 = tag_[2] ? one : zero; - const ScalarT t2 = tag_[4] ? one : zero; - const ScalarT t4 = tag_[5] ? one : zero; - const ScalarT t6 = tag_[6] ? one : zero; - - f[0] = -dx1 * x1_dot + x2; - f[1] = -dx2 * x2_dot + x3; - f[2] = -dx3 * x3_dot + x4; - f[3] = -a4_ * x4_dot - x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u; - f[4] = -T2_ * x5_dot - x5 + v4; - f[5] = -T4_ * x6_dot - x6 + v5; - f[6] = -T6_ * x7_dot - x7 + v6; - f[7] = -v4 + x1 + A5_ * x2 + A6_ * x3; - f[8] = t2 * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + (one - t2) * (v4 - v5); - f[9] = t4 * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + (one - t4) * (v5 - v6); - f[10] = t6 * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + (one - t6) * (Ks_ * v6 - v7); - f[11] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); + 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 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 x1 = y[X1]; + const ScalarT x2 = y[X2]; + const ScalarT x3 = y[X3]; + const ScalarT x4 = y[X4]; + 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 x1_dot = yp[X1]; + const ScalarT x2_dot = yp[X2]; + const ScalarT x3_dot = yp[X3]; + const ScalarT x4_dot = yp[X4]; + const ScalarT x5_dot = yp[X5]; + const ScalarT x6_dot = yp[X6]; + const ScalarT x7_dot = yp[X7]; + + const ScalarT u = ws[U]; + + const RealT s0 = s0_; + const RealT s1 = s1_; + const RealT s2 = s2_; + const RealT s3 = s3_; + const RealT s4 = s4_; + + const ScalarT x1_rhs = (-x1 + u) * a1_inv_; + const ScalarT x2_rhs = (-x1 - a1_ * x2 + u) * a2_inv_; + const ScalarT x3_rhs = (-x1 - a1_ * x2 - a2_ * x3 + u) * a3_inv_; + const ScalarT x4_rhs = (-x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u) * a4_inv_; + const ScalarT x5_rhs = (v4 - x5) * T2_inv_; + const ScalarT x6_rhs = (v5 - x6) * T4_inv_; + const ScalarT x7_rhs = (v6 - x7) * T6_inv_; + + f[X1] = -x1_dot + s1 * x1_rhs + (s2 + s3 + s4) * x2; + f[X2] = -x2_dot + s2 * x2_rhs + (s3 + s4) * x3; + f[X3] = -x3_dot + s3 * x3_rhs + s4 * x4; + f[X4] = -x4_dot + s4 * x4_rhs; + f[X5] = -x5_dot + x5_rhs; + f[X6] = -x6_dot + x6_rhs; + f[X7] = -x7_dot + x7_rhs; + + f[V4] = -v4 + s0 * u + + s1 * (x1 + A5_ * x1_rhs) + + s2 * (x1 + A5_ * x2 + A6_ * x2_rhs) + + (s3 + s4) * (x1 + A5_ * x2 + A6_ * x3); + 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; } @@ -346,10 +379,15 @@ namespace GridKit template int Ieeest::evaluateResidual() { + const auto U = static_cast(IeeestExternalVariables::U); + + std::fill(ws_.begin(), ws_.end(), ZERO); + std::fill(ws_indices_.begin(), ws_indices_.end(), 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()); @@ -367,8 +405,13 @@ namespace GridKit void Ieeest::initializeMonitor() { using Variable = typename ModelDataT::MonitorableVariables; - monitor_->set(Variable::vss, [this] - { return y_[11]; }); + auto index = [](IeeestInternalVariables variable) + { + return static_cast(variable); + }; + + monitor_->set(Variable::vss, [this, index] + { return y_[index(IeeestInternalVariables::VSS)]; }); } } // namespace Stabilizer diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index a0ea1064d..cdae661d3 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -6,8 +6,6 @@ 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. -- A zero denominator time constant bypasses its corresponding lead–lag or - washout block. ## Block Diagram @@ -40,42 +38,34 @@ $T_{\mathrm{delay}}$ | [sec] | `Tdelay` | Input delay ### Parameter Validation -The fixed realization rejects a first-order-only notch denominator: +IEEEST denominator time constants are conditioned and unsupported notch forms are rejected by the following checks. Let $\epsilon_T=10^{-3}$. ```math \begin{aligned} - a_2 \ne 0 \lor a_3 \ne 0 \lor a_4 \ne 0 \lor a_1 = 0 + 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 -The notch-filter denominator expands to: - -```math -\begin{aligned} -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 -\end{aligned} -``` - -The binary DAE selectors choose the active notch-filter order: - ```math \begin{aligned} -\delta_1 &= \delta_2 = -\begin{cases} -1 & a_2 \ne 0 \lor a_3 \ne 0 \lor a_4 \ne 0 \\ -0 & \text{otherwise} -\end{cases} -\\ -\delta_3 &= -\begin{cases} -1 & a_3 \ne 0 \lor a_4 \ne 0 \\ -0 & \text{otherwise} -\end{cases} + 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} ``` @@ -92,9 +82,6 @@ $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 -For reduced-order notch filters, unused notch states remain in the fixed -component state vector and are pinned by algebraic residuals. - #### Algebraic Symbol | Units | Description | Note @@ -113,9 +100,9 @@ None. #### Algebraic -Symbol | Units | Description | Note --------|--------|-------------------------|------ -$u$ | [p.u.] | Stabilizer input signal | +Symbol | Units | Type | Description | Note +-------|--------|-------|-------------------------|------ +$u$ | [p.u.] | Known | Stabilizer input signal | ## Model Equations @@ -123,13 +110,50 @@ $u$ | [p.u.] | Stabilizer input signal | ```math \begin{aligned} -0 &= -\delta_1\dot{x}_1 + x_2 \\ -0 &= -\delta_2\dot{x}_2 + x_3 \\ -0 &= -\delta_3\dot{x}_3 + x_4 \\ -0 &= -a_4\dot{x}_4 - x_1 - a_1x_2 - a_2x_3 - a_3x_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} ``` @@ -137,23 +161,25 @@ $u$ | [p.u.] | Stabilizer input signal | ```math \begin{aligned} -0 &= -v_4 + x_1 + A_5x_2 + A_6x_3 \\ -0 &= -\begin{cases} --T_2(v_5 - x_5) + T_1(v_4 - x_5) & T_2 \ne 0 \\ -v_4 - v_5 & T_2 = 0 -\end{cases} \\ -0 &= -\begin{cases} --T_4(v_6 - x_6) + T_3(v_5 - x_6) & T_4 \ne 0 \\ -v_5 - v_6 & T_4 = 0 -\end{cases} \\ -0 &= -\begin{cases} --T_6 v_7 + K_s T_5(v_6 - x_7) & T_6 \ne 0 \\ -K_s v_6 - v_7 & T_6 = 0 -\end{cases} \\ -0 &= -V_{\mathrm{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} ``` @@ -162,28 +188,31 @@ The output limiter uses GridKit's smooth ## Initialization -Initialization is performed by evaluating the steady-state residuals in -dependency order. Let subscript $0$ denote initial values and set all internal -derivatives to zero. States and derivatives initialize to the steady state -implied by the attached input $u$: +### External Priors ```math \begin{aligned} -x_1 &= v_4 = x_5 = v_5 = x_6 = v_6 = x_7 = u \\ -x_2 &= x_3 = x_4 = 0 \\ -v_7 &= -\begin{cases} -K_su & T_6 = 0 \\ -0 & \text{otherwise} -\end{cases} -& -V_{\mathrm{ss}} &= \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) + u_0 &\leftarrow \text{stabilizer input signal} \end{aligned} ``` -All internal derivatives initialize to zero. +### 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} +``` + +### External Solved + +None. -## Model Outputs +## Monitorable Outputs Output | Units | Description | Note -------|--------|---------------------------|------ diff --git a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp index e960ae308..82fcf27db 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -42,17 +42,15 @@ namespace GridKit ScalarT u; RealT Lsmin; RealT Lsmax; - ScalarT raw_v7; + ScalarT expected_v7; ScalarT expected_vss; }; 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, 1.0, 0.25, -1.0, 1.0, 0.25, 0.25}, - {0.0, 2.0, 0.25, -1.0, 1.0, 0.50, 0.50}, - {0.0, 4.0, 0.25, -1.0, 0.6, 1.00, 0.60}, - {5.0, 3.0, 0.25, -1.0, 1.0, 0.00, 0.00}, + {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}, }; for (const auto& test : cases) @@ -83,7 +81,7 @@ namespace GridKit success *= vss_node.linked(); success *= (vss_node.getVariableIndex() == 11); - success *= isEqual(model.y()[10], test.raw_v7, tol_); + success *= isEqual(model.y()[10], test.expected_v7, tol_); success *= isEqual(model.y()[11], test.expected_vss, loose_tol); success *= isEqual(vss_node.read(), test.expected_vss, loose_tol); } @@ -107,31 +105,44 @@ namespace GridKit const std::vector cases = { {"baseline", [](DataT&) {}, - {0.19, 0.28, 0.37, 0.0878, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, + {0.19, 0.28, 0.37, 1.0975, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}}, {"a4_zero", [](DataT& data) { data.parameters[Params::A4] = static_cast(0.0); }, - {0.19, 0.28, 0.37, 0.227, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, + {0.19, 0.28, 4.153333333333333, -0.04, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}}, {"a3_a4_zero", [](DataT& data) { data.parameters[Params::A3] = static_cast(0.0); data.parameters[Params::A4] = static_cast(0.0); }, - {0.19, 0.28, 0.40, 0.32, 0.25, 0.24, -0.05, -0.42, -0.25, -0.31, 5.75, 0.0}}, - {"time_zero", + {0.19, 1.88, -0.03, -0.04, 0.25, 0.24, -0.01, 0.54, -0.25, -0.31, 1.15, 0.0}}, + {"notch_bypass", [](DataT& data) { - data.parameters[Params::T2] = static_cast(0.0); - data.parameters[Params::T4] = static_cast(0.0); - data.parameters[Params::T6] = static_cast(0.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); }, - {0.19, 0.28, 0.37, 0.0878, 0.30, 0.30, 0.30, -0.42, -0.10, -0.10, 9.95, 0.0}}, + {-0.01, -0.02, -0.03, -0.04, 0.25, 0.24, -0.01, -0.3, -0.25, -0.31, 1.15, 0.0}}, + {"first_order", + [](DataT& data) + { + 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); + }, + {3.99, -0.02, -0.03, -0.04, 0.25, 0.24, -0.01, 1.3, -0.25, -0.31, 1.15, 0.0}}, }; const auto loose_tol = static_cast(1.0e-4); + for (const auto& test : cases) { PhasorDynamics::SignalNode u_node; @@ -191,6 +202,81 @@ namespace GridKit } } + { + 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); + + auto data = makeData(); + data.parameters[Params::A2] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + data.parameters[Params::T2] = static_cast(0.0); + data.parameters[Params::T4] = static_cast(0.0); + data.parameters[Params::T6] = static_cast(0.0); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode(&vss_node); + + model.allocate(); + success *= (model.verify() == 0); + success *= (model.initialize() == 0); + success *= (model.evaluateResidual() == 0); + + const auto loose_tol = static_cast(1.0e-4); + for (size_t i = 0; i < model.getResidual().size(); ++i) + { + if (!isEqual(model.getResidual()[i], static_cast(0.0), loose_tol)) + { + std::cout << "Conditioned-constant residual row " << i << " is " + << std::setprecision(15) << model.getResidual()[i] << "\n"; + success = false; + } + } + } + + return success.report(__func__); + } + + TestOutcome tags() + { + TestStatus success = true; + + using Params = PhasorDynamics::Stabilizer::IeeestParameters; + + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + auto data = makeData(); + data.parameters[Params::A2] = static_cast(0.0); + data.parameters[Params::A4] = static_cast(0.0); + data.parameters[Params::T2] = static_cast(0.0); + data.parameters[Params::T4] = static_cast(0.0); + data.parameters[Params::T6] = static_cast(0.0); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + + model.allocate(); + + for (size_t i = 0; i < model.tag().size(); ++i) + { + const bool expected = (i <= static_cast(PhasorDynamics::Stabilizer::IeeestInternalVariables::X7)); + if (model.tag()[i] != expected) + { + std::cout << "Incorrect differential tag at row " << i << "\n"; + success = false; + } + } + return success.report(__func__); } @@ -231,6 +317,25 @@ namespace GridKit success *= (model.verify() != 0); } + { + PhasorDynamics::SignalNode u_node; + ScalarT u_value{0.0}; + IdxT u_index{12}; + u_node.set(&u_value, &u_index); + + auto data = makeData(); + data.parameters[Params::A1] = static_cast(1.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::A6] = static_cast(0.0); + + PhasorDynamics::Stabilizer::Ieeest model(data); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() == 0); + } + return success.report(__func__); } diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index 2a76a60b0..2a8f60f89 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -8,6 +8,7 @@ int main() result += test.init(); result += test.residual(); + result += test.tags(); result += test.verify(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); From 9fb15faacf42426a67e233aa46c849cbce205419 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Thu, 2 Jul 2026 19:25:32 -0500 Subject: [PATCH 5/6] rebased IEEEST --- .../PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp | 3 --- .../Stabilizer/IEEEST/IeeestImpl.hpp | 15 ++++++--------- .../PhasorDynamics/Stabilizer/IEEEST/README.md | 16 ++++++++++++---- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index 1162eee26..01784dc4b 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -137,9 +137,6 @@ namespace GridKit RealT T4_{1}; RealT T5_{0}; RealT T6_{1}; - RealT T2_inv_{1}; - RealT T4_inv_{1}; - RealT T6_inv_{1}; RealT Ks_{1}; RealT Lsmin_{-0.1}; RealT Lsmax_{0.1}; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index 785af0826..9998be4b0 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -49,12 +49,9 @@ namespace GridKit template void Ieeest::setDerivedParameters() { - T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); - T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); - T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); - T2_inv_ = ONE / T2_; - T4_inv_ = ONE / T4_; - T6_inv_ = ONE / T6_; + T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); + T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); + T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); a1_ = A1_ + A3_; a2_ = A2_ + A4_ + A1_ * A3_; @@ -352,9 +349,9 @@ namespace GridKit const ScalarT x2_rhs = (-x1 - a1_ * x2 + u) * a2_inv_; const ScalarT x3_rhs = (-x1 - a1_ * x2 - a2_ * x3 + u) * a3_inv_; const ScalarT x4_rhs = (-x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u) * a4_inv_; - const ScalarT x5_rhs = (v4 - x5) * T2_inv_; - const ScalarT x6_rhs = (v5 - x6) * T4_inv_; - const ScalarT x7_rhs = (v6 - x7) * T6_inv_; + const ScalarT x5_rhs = (v4 - x5) / T2_; + const ScalarT x6_rhs = (v5 - x6) / T4_; + const ScalarT x7_rhs = (v6 - x7) / T6_; f[X1] = -x1_dot + s1 * x1_rhs + (s2 + s3 + s4) * x2; f[X2] = -x2_dot + s2 * x2_rhs + (s3 + s4) * x3; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index cdae661d3..491c60885 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -3,7 +3,8 @@ Standard IEEE power system stabilizer: 4th-order notch filter, two lead–lag blocks, washout, and output limiter. -Notes: +## Notes + - $V_{\mathrm{cl}}$, $V_{\mathrm{cu}}$, and $T_{\mathrm{delay}}$ are accepted for input-format compatibility but are not modeled. @@ -69,6 +70,13 @@ IEEEST denominator time constants are conditioned and unsupported notch forms ar \end{aligned} ``` +## Model Ports + +Name | Port | Init | Description +---------|--------|-------|------ +`input` | Input | Known | Stabilizer input signal +`output` | Output | Known | Stabilizer output signal + ## Model Variables ### Internal Variables @@ -188,11 +196,11 @@ The output limiter uses GridKit's smooth ## Initialization -### External Priors +### Input Initialization ```math \begin{aligned} - u_0 &\leftarrow \text{stabilizer input signal} + u &\leftarrow \text{stabilizer input signal} \end{aligned} ``` @@ -208,7 +216,7 @@ The output limiter uses GridKit's smooth \end{aligned} ``` -### External Solved +### Output Initialization None. From 7336d1fedd1b6ccf77da9c401750db7690cd15c8 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 6 Jul 2026 08:56:55 -0500 Subject: [PATCH 6/6] tempalte parameter for order implemetnations --- .../PhasorDynamics/Stabilizer/CMakeLists.txt | 15 + .../Stabilizer/IEEEST/Ieeest.cpp | 16 +- .../Stabilizer/IEEEST/Ieeest.hpp | 192 ++++- .../IEEEST/IeeestDependencyTracking.cpp | 16 +- .../Stabilizer/IEEEST/IeeestEnzyme.cpp | 23 +- .../Stabilizer/IEEEST/IeeestImpl.hpp | 342 +++++---- .../Stabilizer/StabilizerFactory.hpp | 126 +++ .../Model/PhasorDynamics/SystemModelImpl.hpp | 15 +- GridKit/Model/VariableMonitorImpl.hpp | 17 +- .../PhasorDynamics/StabilizerIeeestTests.hpp | 725 ++++++++++++------ .../runStabilizerIeeestTests.cpp | 38 +- 11 files changed, 1093 insertions(+), 432 deletions(-) create mode 100644 GridKit/Model/PhasorDynamics/Stabilizer/StabilizerFactory.hpp 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 01784dc4b..baf136cfa 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -6,10 +6,12 @@ #pragma once +#include #include #include #include +#include #include #include #include @@ -36,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 { @@ -61,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_; @@ -75,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_; @@ -107,7 +250,7 @@ namespace GridKit auto getSignals() -> ComponentSignals, IeeestExternalVariables>& { return signals_; @@ -149,20 +292,9 @@ namespace GridKit RealT a3_{0}; RealT a4_{0}; - IdxT order_{0}; - RealT s0_{1}; - RealT s1_{0}; - RealT s2_{0}; - RealT s3_{0}; - RealT s4_{0}; - RealT a1_inv_{0}; - RealT a2_inv_{0}; - RealT a3_inv_{0}; - RealT a4_inv_{0}; - IdxT parameter_error_count_{0}; - ComponentSignals signals_; + ComponentSignals, IeeestExternalVariables> signals_; std::unique_ptr monitor_; 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 9998be4b0..d53e8c4ad 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -24,72 +24,45 @@ namespace GridKit { using Log = ::GridKit::Utilities::Logger; - template - Ieeest::Ieeest() + template + Ieeest::Ieeest() { - size_ = static_cast(IeeestInternalVariables::MAXIMUM); + 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_ = static_cast(IeeestInternalVariables::MAXIMUM); + size_ = static_cast(IeeestInternalVariables::MAXIMUM); setDerivedParameters(); } - template - Ieeest::~Ieeest() + template + Ieeest::~Ieeest() { } - template - void Ieeest::setDerivedParameters() + template + void Ieeest::setDerivedParameters() { T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); - a1_ = A1_ + A3_; - a2_ = A2_ + A4_ + A1_ * A3_; - a3_ = A1_ * A4_ + A2_ * A3_; - a4_ = A2_ * A4_; + const auto a = notchCoefficients(A1_, A2_, A3_, A4_); - order_ = 0; - if (a1_ != ZERO) - { - order_ = 1; - } - if (a2_ != ZERO) - { - order_ = 2; - } - if (a3_ != ZERO) - { - order_ = 3; - } - if (a4_ != ZERO) - { - order_ = 4; - } - - s0_ = (order_ == 0) ? ONE : ZERO; - s1_ = (order_ == 1) ? ONE : ZERO; - s2_ = (order_ == 2) ? ONE : ZERO; - s3_ = (order_ == 3) ? ONE : ZERO; - s4_ = (order_ == 4) ? ONE : ZERO; - - a1_inv_ = (order_ == 1) ? ONE / a1_ : ZERO; - a2_inv_ = (order_ == 2) ? ONE / a2_ : ZERO; - a3_inv_ = (order_ == 3) ? ONE / a3_ : ZERO; - a4_inv_ = (order_ == 4) ? ONE / a4_ : ZERO; + a1_ = a[0]; + a2_ = a[1]; + a3_ = a[2]; + a4_ = a[3]; } - template - void Ieeest::initializeParameters(const ModelDataT& data) + template + void Ieeest::initializeParameters(const ModelDataT& data) { using Params = typename ModelDataT::Parameters; @@ -138,17 +111,17 @@ namespace GridKit 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); + size_ = static_cast(IeeestInternalVariables::MAXIMUM); auto size = static_cast(size_); f_.assign(size, ScalarT{0}); @@ -169,20 +142,19 @@ namespace GridKit this->setResidualIndex(j, j); } - if (signals_.template isAssigned()) + constexpr auto VSS = IeeestInternalVariables::VSS; + if (signals_.template isAssigned()) { - signals_.template getSignalNode()->set( - &y_[static_cast(IeeestInternalVariables::VSS)], - &(this->getVariableIndex(static_cast(IeeestInternalVariables::VSS)))); + signals_.template getSignalNode()->set( + &y_[static_cast(VSS)], + &(this->getVariableIndex(static_cast(VSS)))); } - tagDifferentiable(); - return 0; } - template - int Ieeest::verify() const + template + int Ieeest::verify() const { int ret = static_cast(parameter_error_count_); @@ -200,17 +172,37 @@ namespace GridKit ret += 1; } - if (order_ == 1 && A6_ != ZERO) + const size_t derived_order = notchOrder(a1_, a2_, a3_, a4_); + if (derived_order != order) { - Log::error() << "Ieeest: unsupported first-order notch filter with second-order numerator\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() { if (verify() > 0) { @@ -218,18 +210,14 @@ namespace GridKit return 1; } - 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 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 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); @@ -239,10 +227,15 @@ namespace GridKit ws_[U] = u; ws_indices_[U] = signals_.template readExternalVariableIndex(); - y_[X1] = u; - y_[X2] = ZERO; - y_[X3] = ZERO; - y_[X4] = ZERO; + // 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; @@ -255,22 +248,31 @@ namespace GridKit return 0; } - template - int Ieeest::tagDifferentiable() + template + int Ieeest::tagDifferentiable() { - auto index = [](IeeestInternalVariables variable) + std::fill(tag_.begin(), tag_.end(), false); + + if constexpr (order >= 1) { - return static_cast(variable); - }; + 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; + } - std::fill(tag_.begin(), tag_.end(), false); - tag_[index(IeeestInternalVariables::X1)] = true; - tag_[index(IeeestInternalVariables::X2)] = true; - tag_[index(IeeestInternalVariables::X3)] = true; - tag_[index(IeeestInternalVariables::X4)] = true; - tag_[index(IeeestInternalVariables::X5)] = true; - tag_[index(IeeestInternalVariables::X6)] = true; - tag_[index(IeeestInternalVariables::X7)] = true; + tag_[static_cast(IeeestInternalVariables::X5)] = true; + tag_[static_cast(IeeestInternalVariables::X6)] = true; + tag_[static_cast(IeeestInternalVariables::X7)] = true; return 0; } @@ -287,39 +289,31 @@ 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) { - 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 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 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 x1 = y[X1]; - const ScalarT x2 = y[X2]; - const ScalarT x3 = y[X3]; - const ScalarT x4 = y[X4]; const ScalarT x5 = y[X5]; const ScalarT x6 = y[X6]; const ScalarT x7 = y[X7]; @@ -329,42 +323,98 @@ namespace GridKit const ScalarT v7 = y[V7]; const ScalarT vss = y[VSS]; - 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 x5_dot = yp[X5]; const ScalarT x6_dot = yp[X6]; const ScalarT x7_dot = yp[X7]; const ScalarT u = ws[U]; - const RealT s0 = s0_; - const RealT s1 = s1_; - const RealT s2 = s2_; - const RealT s3 = s3_; - const RealT s4 = s4_; + // 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_rhs = (-x1 + u) * a1_inv_; - const ScalarT x2_rhs = (-x1 - a1_ * x2 + u) * a2_inv_; - const ScalarT x3_rhs = (-x1 - a1_ * x2 - a2_ * x3 + u) * a3_inv_; - const ScalarT x4_rhs = (-x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u) * a4_inv_; + 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[X1] = -x1_dot + s1 * x1_rhs + (s2 + s3 + s4) * x2; - f[X2] = -x2_dot + s2 * x2_rhs + (s3 + s4) * x3; - f[X3] = -x3_dot + s3 * x3_rhs + s4 * x4; - f[X4] = -x4_dot + s4 * x4_rhs; - f[X5] = -x5_dot + x5_rhs; - f[X6] = -x6_dot + x6_rhs; - f[X7] = -x7_dot + x7_rhs; - - f[V4] = -v4 + s0 * u - + s1 * (x1 + A5_ * x1_rhs) - + s2 * (x1 + A5_ * x2 + A6_ * x2_rhs) - + (s3 + s4) * (x1 + A5_ * x2 + A6_ * x3); + 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; @@ -373,8 +423,8 @@ namespace GridKit return 0; } - template - int Ieeest::evaluateResidual() + template + int Ieeest::evaluateResidual() { const auto U = static_cast(IeeestExternalVariables::U); @@ -392,23 +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; - auto index = [](IeeestInternalVariables variable) - { - return static_cast(variable); - }; - monitor_->set(Variable::vss, [this, index] - { return y_[index(IeeestInternalVariables::VSS)]; }); + constexpr auto VSS = static_cast(IeeestInternalVariables::VSS); + + monitor_->set(Variable::vss, [this] + { return y_[VSS]; }); } } // namespace Stabilizer 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 82fcf27db..102d39356 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -1,9 +1,11 @@ #pragma once -#include +#include +#include #include #include #include +#include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -26,9 +29,33 @@ namespace GridKit using RealT = typename PhasorDynamics::Component::RealT; using DataT = PhasorDynamics::Stabilizer::IeeestData; + template + using InternalVariables = PhasorDynamics::Stabilizer::IeeestInternalVariables; + StabilizerIeeestTests() = default; ~StabilizerIeeestTests() = default; + TestOutcome constructor() + { + TestStatus success = true; + + { + // Zeroth order: no notch states, only X5..X7 and V4..VSS. + PhasorDynamics::Stabilizer::Ieeest model; + success *= (model.size() == 8); + success *= (model.getMonitor() == nullptr); + } + + success *= checkConstructedSize<0>(); + success *= checkConstructedSize<1>(); + success *= checkConstructedSize<2>(); + success *= checkConstructedSize<3>(); + success *= checkConstructedSize<4>(); + + return success.report(__func__); + } + + template TestOutcome init() { TestStatus success = true; @@ -46,6 +73,8 @@ namespace GridKit ScalarT expected_vss; }; + // 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}, @@ -53,6 +82,9 @@ namespace GridKit {5.0, 3.0, 0.25, -1.0, 1.0, 0.0, 0.0}, }; + const auto V7 = static_cast(InternalVariables::V7); + const auto VSS = static_cast(InternalVariables::VSS); + for (const auto& test : cases) { PhasorDynamics::SignalNode u_node; @@ -65,89 +97,49 @@ namespace GridKit u_node.set(&u_value, &u_index); vss_node.set(&vss_value, &vss_index); - auto data = makeData(); + 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); + PhasorDynamics::Stabilizer::Ieeest model(data); model.getSignals().template attachSignalNode(&u_node); - model.getSignals().template assignSignalNode(&vss_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); model.allocate(); success *= (model.verify() == 0); model.initialize(); success *= vss_node.linked(); - success *= (vss_node.getVariableIndex() == 11); - success *= isEqual(model.y()[10], test.expected_v7, tol_); - success *= isEqual(model.y()[11], test.expected_vss, loose_tol); + 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); } - return success.report(__func__); + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); } - TestOutcome residual() + template + TestOutcome zeroInitialResidual() { TestStatus success = true; using Params = PhasorDynamics::Stabilizer::IeeestParameters; - struct ResidualCase - { - const char* name; - std::function edit; - const std::vector expected; - }; - - const std::vector cases = { - {"baseline", - [](DataT&) {}, - {0.19, 0.28, 0.37, 1.0975, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}}, - {"a4_zero", - [](DataT& data) - { - data.parameters[Params::A4] = static_cast(0.0); - }, - {0.19, 0.28, 4.153333333333333, -0.04, 0.25, 0.24, -0.01, -0.42, -0.25, -0.31, 1.15, 0.0}}, - {"a3_a4_zero", - [](DataT& data) - { - data.parameters[Params::A3] = static_cast(0.0); - data.parameters[Params::A4] = static_cast(0.0); - }, - {0.19, 1.88, -0.03, -0.04, 0.25, 0.24, -0.01, 0.54, -0.25, -0.31, 1.15, 0.0}}, - {"notch_bypass", - [](DataT& data) - { - 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); - }, - {-0.01, -0.02, -0.03, -0.04, 0.25, 0.24, -0.01, -0.3, -0.25, -0.31, 1.15, 0.0}}, - {"first_order", - [](DataT& data) - { - 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); - }, - {3.99, -0.02, -0.03, -0.04, 0.25, 0.24, -0.01, 1.3, -0.25, -0.31, 1.15, 0.0}}, + // 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}, }; - const auto loose_tol = static_cast(1.0e-4); - - for (const auto& test : cases) + for (const auto& T : time_constant_cases) { PhasorDynamics::SignalNode u_node; PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.5}; + ScalarT u_value{0.25}; IdxT u_index{12}; ScalarT vss_value{0.0}; IdxT vss_index{INVALID_INDEX}; @@ -155,121 +147,121 @@ namespace GridKit u_node.set(&u_value, &u_index); vss_node.set(&vss_value, &vss_index); - auto data = makeData(); - test.edit(data); + 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); + PhasorDynamics::Stabilizer::Ieeest model(data); model.getSignals().template attachSignalNode(&u_node); - model.getSignals().template assignSignalNode(&vss_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); model.allocate(); - model.initialize(); - - model.y()[0] = 0.1; - model.y()[1] = 0.2; - model.y()[2] = 0.3; - model.y()[3] = 0.4; - model.y()[4] = 0.5; - model.y()[5] = 0.6; - model.y()[6] = 0.7; - model.y()[7] = 0.8; - model.y()[8] = 0.9; - model.y()[9] = 1.0; - model.y()[10] = 0.05; - model.y()[11] = 0.05; - - model.yp()[0] = 0.01; - model.yp()[1] = 0.02; - model.yp()[2] = 0.03; - model.yp()[3] = 0.04; - model.yp()[4] = 0.05; - model.yp()[5] = 0.06; - model.yp()[6] = 0.07; - - model.evaluateResidual(); + success *= (model.verify() == 0); + success *= (model.initialize() == 0); + success *= (model.evaluateResidual() == 0); - for (size_t i = 0; i < test.expected.size(); ++i) + // 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) { - auto test_tol = (i == 11) ? loose_tol : tol_; - if (!isEqual(model.getResidual()[i], test.expected[i], test_tol)) + if (!isEqual(model.getResidual()[i], static_cast(0.0), loose_tol)) { - std::cout << "Incorrect residual for " << test.name - << " row " << i << ": " - << std::setprecision(15) << model.getResidual()[i] - << " != " << test.expected[i] << "\n"; + std::cout << "Nonzero initial residual at row " << i << ": " + << std::setprecision(15) << model.getResidual()[i] << "\n"; success = false; } } } - { - 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}; + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); + } - u_node.set(&u_value, &u_index); - vss_node.set(&vss_value, &vss_index); + template + TestOutcome residual() + { + TestStatus success = true; - auto data = makeData(); - data.parameters[Params::A2] = static_cast(0.0); - data.parameters[Params::A4] = static_cast(0.0); - data.parameters[Params::T2] = static_cast(0.0); - data.parameters[Params::T4] = static_cast(0.0); - data.parameters[Params::T6] = static_cast(0.0); + 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}; - PhasorDynamics::Stabilizer::Ieeest model(data); - model.getSignals().template attachSignalNode(&u_node); - model.getSignals().template assignSignalNode(&vss_node); + u_node.set(&u_value, &u_index); + vss_node.set(&vss_value, &vss_index); - model.allocate(); - success *= (model.verify() == 0); - success *= (model.initialize() == 0); - success *= (model.evaluateResidual() == 0); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); - const auto loose_tol = static_cast(1.0e-4); - for (size_t i = 0; i < model.getResidual().size(); ++i) + 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(); + + // 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); + 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)) { - if (!isEqual(model.getResidual()[i], static_cast(0.0), loose_tol)) - { - std::cout << "Conditioned-constant residual row " << i << " is " - << std::setprecision(15) << model.getResidual()[i] << "\n"; - success = false; - } + std::cout << "Incorrect residual for order " << order + << " row " << i << ": " + << std::setprecision(15) << model.getResidual()[i] + << " != " << expected[i] << "\n"; + success = false; } } - return success.report(__func__); + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); } + template TestOutcome tags() { TestStatus success = true; - using Params = PhasorDynamics::Stabilizer::IeeestParameters; - PhasorDynamics::SignalNode u_node; ScalarT u_value{0.0}; IdxT u_index{12}; u_node.set(&u_value, &u_index); - auto data = makeData(); - data.parameters[Params::A2] = static_cast(0.0); - data.parameters[Params::A4] = static_cast(0.0); - data.parameters[Params::T2] = static_cast(0.0); - data.parameters[Params::T4] = static_cast(0.0); - data.parameters[Params::T6] = static_cast(0.0); - - PhasorDynamics::Stabilizer::Ieeest model(data); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); model.getSignals().template attachSignalNode(&u_node); model.allocate(); for (size_t i = 0; i < model.tag().size(); ++i) { - const bool expected = (i <= static_cast(PhasorDynamics::Stabilizer::IeeestInternalVariables::X7)); + if (model.tag()[i]) + { + std::cout << "Differential tag set before tagDifferentiable at row " << i << "\n"; + success = false; + } + } + + model.tagDifferentiable(); + + 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"; @@ -277,7 +269,8 @@ namespace GridKit } } - return success.report(__func__); + const std::string name = orderedName(__func__, order); + return success.report(name.c_str()); } TestOutcome verify() @@ -285,52 +278,88 @@ namespace GridKit TestStatus success = true; using Params = PhasorDynamics::Stabilizer::IeeestParameters; + // Missing input signal fails verification. { - PhasorDynamics::Stabilizer::Ieeest model(makeData()); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); model.allocate(); success *= (model.verify() != 0); } + // Attached but unlinked input signal fails verification. { - PhasorDynamics::SignalNode u_node; - PhasorDynamics::Stabilizer::Ieeest model(makeData()); + PhasorDynamics::SignalNode u_node; + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); model.getSignals().template attachSignalNode(&u_node); model.allocate(); success *= (model.verify() != 0); } + // 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); - auto data = makeData(); - data.parameters[Params::A1] = static_cast(1.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); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<4>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() != 0); + } + + // 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); + 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); - auto data = makeData(); - data.parameters[Params::A1] = static_cast(1.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::A6] = static_cast(0.0); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData<1>()); + model.getSignals().template attachSignalNode(&u_node); + model.allocate(); + success *= (model.verify() == 0); + } - PhasorDynamics::Stabilizer::Ieeest model(data); + // 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. + { + 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); @@ -339,28 +368,144 @@ namespace GridKit return success.report(__func__); } + 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() + { + TestStatus success = true; + + 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); + + auto* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeSymmetricNotchData(), &u_node, &vss_node); + + // 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); + + const auto y_values = stateValues<4>(); + const auto yp_values = derivativeValues<4>(); + for (size_t i = 0; i < y_values.size(); ++i) + { + stabilizer->y()[i] = y_values[i]; + stabilizer->yp()[i] = yp_values[i]; + } + + stabilizer->evaluateResidual(); + + // 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}; + + // 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) + { + 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; + } + } + + 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); + + const auto y_values = stateValues(); + const auto yp_values = derivativeValues(); + std::vector dependency_tracking_jacobian; { PhasorDynamics::SignalNode u_node; PhasorDynamics::SignalNode vss_node; DepVar u_value{0.5}; - IdxT u_index{12}; + 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); - PhasorDynamics::Stabilizer::Ieeest model(makeData()); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); model.getSignals().template attachSignalNode(&u_node); - model.getSignals().template assignSignalNode(&vss_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); model.allocate(); model.initialize(); @@ -372,26 +517,14 @@ namespace GridKit u_value.setVariableNumber(model.size()); u_value.setValue(0.5); - model.y()[0].setValue(0.1); - model.y()[1].setValue(0.2); - model.y()[2].setValue(0.3); - model.y()[3].setValue(0.4); - model.y()[4].setValue(0.5); - model.y()[5].setValue(0.6); - model.y()[6].setValue(0.7); - model.y()[7].setValue(0.8); - model.y()[8].setValue(0.9); - model.y()[9].setValue(1.0); - model.y()[10].setValue(0.05); - model.y()[11].setValue(0.05); - - model.yp()[0].setValue(0.01); - model.yp()[1].setValue(0.02); - model.yp()[2].setValue(0.03); - model.yp()[3].setValue(0.04); - model.yp()[4].setValue(0.05); - model.yp()[5].setValue(0.06); - model.yp()[6].setValue(0.07); + 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(); @@ -404,32 +537,20 @@ namespace GridKit } u_value = 0.5; - model.y()[0].setValue(0.1); - model.y()[1].setValue(0.2); - model.y()[2].setValue(0.3); - model.y()[3].setValue(0.4); - model.y()[4].setValue(0.5); - model.y()[5].setValue(0.6); - model.y()[6].setValue(0.7); - model.y()[7].setValue(0.8); - model.y()[8].setValue(0.9); - model.y()[9].setValue(1.0); - model.y()[10].setValue(0.05); - model.y()[11].setValue(0.05); - - model.yp()[0].setValue(0.01); - model.yp()[1].setValue(0.02); - model.yp()[2].setValue(0.03); - model.yp()[3].setValue(0.04); - model.yp()[4].setValue(0.05); - model.yp()[5].setValue(0.06); - model.yp()[6].setValue(0.07); + 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_yp = model.getResidual(); dependency_tracking_jacobian.resize(residual_y.size()); - for (IdxT i = 0; i < residual_y.size(); ++i) + for (size_t i = 0; i < residual_y.size(); ++i) { auto dependency_y = residual_y[i].getDependencies(); auto dependency_yp = residual_yp[i].getDependencies(); @@ -463,40 +584,25 @@ namespace GridKit PhasorDynamics::SignalNode u_node; PhasorDynamics::SignalNode vss_node; ScalarT u_value{0.5}; - IdxT u_index{12}; + 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(makeData()); + PhasorDynamics::Stabilizer::Ieeest model(makeOrderData()); model.getSignals().template attachSignalNode(&u_node); - model.getSignals().template assignSignalNode(&vss_node); + model.getSignals().template assignSignalNode::VSS>(&vss_node); model.allocate(); model.initialize(); - model.y()[0] = 0.1; - model.y()[1] = 0.2; - model.y()[2] = 0.3; - model.y()[3] = 0.4; - model.y()[4] = 0.5; - model.y()[5] = 0.6; - model.y()[6] = 0.7; - model.y()[7] = 0.8; - model.y()[8] = 0.9; - model.y()[9] = 1.0; - model.y()[10] = 0.05; - model.y()[11] = 0.05; - - model.yp()[0] = 0.01; - model.yp()[1] = 0.02; - model.yp()[2] = 0.03; - model.yp()[3] = 0.04; - model.yp()[4] = 0.05; - model.yp()[5] = 0.06; - model.yp()[6] = 0.07; + for (size_t i = 0; i < y_values.size(); ++i) + { + model.y()[i] = y_values[i]; + model.yp()[i] = yp_values[i]; + } model.updateTime(0.0, 1.0); model.evaluateResidual(); @@ -511,13 +617,64 @@ namespace GridKit success *= GridKit::Testing::isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol_); } - return success.report(__func__); + 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) + ")"; + } + + 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.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* stabilizer = + PhasorDynamics::Stabilizer::StabilizerFactory::create(makeOrderData(), &u_node, &vss_node); + + 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)); + + if (!success) + { + std::cout << "Factory checks failed for order " << order << "\n"; + } + + delete stabilizer; + return success; + } + auto makeData() -> DataT { using Params = PhasorDynamics::Stabilizer::IeeestParameters; @@ -548,6 +705,136 @@ namespace GridKit return data; } + + /// Base data with A1 = A3 = 0: still fourth order, with a1 = a3 = 0 + auto makeSymmetricNotchData() -> DataT + { + 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}; + } + } + + /// Expected residuals for `stateValues`/`derivativeValues` with + /// `makeOrderData` and u = 0.5, derived from the model equations + template + static std::vector expectedResidual() + { + 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 } // namespace Testing diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index 2a8f60f89..888060d6a 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -6,12 +6,42 @@ int main() GridKit::Testing::StabilizerIeeestTests test; - result += test.init(); - result += test.residual(); - result += test.tags(); + result += test.constructor(); + + 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();