From 6d68902b5536b7a0969bf3a3133ba3d03c00e286 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 8 Jun 2026 04:44:18 -0500 Subject: [PATCH 1/2] 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 | 617 +++++++++--------- .../runStabilizerIeeestTests.cpp | 4 +- 5 files changed, 496 insertions(+), 388 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index a077d3c12..33a7efb54 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -139,34 +139,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 5d86b0f7c..45a301ac4 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) { @@ -119,31 +128,7 @@ namespace GridKit Tdelay_ = std::get(data.parameters.at(ModelDataT::Parameters::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 @@ -182,6 +167,8 @@ namespace GridKit &y_[11], &(this->getVariableIndex(11))); } + tagDifferentiable(); + return 0; } @@ -204,7 +191,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; @@ -222,19 +209,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; @@ -294,19 +315,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 0c1c77d66..73dd8693c 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,401 @@ 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(); + PhasorDynamics::Stabilizer::Ieeest model(makeData()); + model.getSignals().template attachSignalNode(&u_node); + model.getSignals().template assignSignalNode(&vss_node); - // 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"; - } + model.allocate(); + model.initialize(); - // 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(); - - 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(); + { + 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(); + + auto model_jacobian = model.getJacobian(); + model_jacobian.deduplicate(); + enzyme_jacobian = GridKit::Testing::MapFromCOO(model_jacobian); + } - auto model_jacobian = stab.getJacobian(); - model_jacobian.deduplicate(); - model_jacobian.printMatrix("IEEEST 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 GridKit::Testing::MapFromCOO(model_jacobian); + 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 +444,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 509506cc6765a8610a3cba5e701f449694ad2699 Mon Sep 17 00:00:00 2001 From: lukelowry Date: Mon, 8 Jun 2026 07:54:37 -0500 Subject: [PATCH 2/2] 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