diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index f77935130..e3d3dfebc 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -35,28 +35,40 @@ namespace GridKit /// Internal variables of a `Ieeest` enum class IeeestInternalVariables : 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) + X1, ///< \f$x_1\f$ Notch filter signal state [p.u.], differential for \f$n\ge1\f$ + X2, ///< \f$x_2\f$ First derivative of the filtered signal [p.u./sec], differential for \f$n\ge2\f$ + X3, ///< \f$x_3\f$ Second derivative of the filtered signal [p.u./sec^2], differential for \f$n\ge3\f$ + X4, ///< \f$x_4\f$ Third derivative of the filtered signal [p.u./sec^3], differential for \f$n=4\f$ + X5, ///< \f$x_5\f$ Lead-lag 1 state [p.u.] + X6, ///< \f$x_6\f$ Lead-lag 2 state [p.u.] + X7, ///< \f$x_7\f$ Washout state [p.u.] + V4, ///< \f$v_4\f$ Notch filter output [p.u.] + V5, ///< \f$v_5\f$ Lead-lag 1 output [p.u.] + V6, ///< \f$v_6\f$ Lead-lag 2 output [p.u.] + V7, ///< \f$v_7\f$ Unlimited stabilizer signal [p.u.] + VSS, ///< \f$V_{ss}\f$ Limited stabilizer signal, the model output [p.u.] MAXIMUM, }; /// External variables of a `Ieeest` enum class IeeestExternalVariables : size_t { - U, ///< Stabilizer input signal + U, ///< \f$u\f$ Stabilizer input signal [p.u.] MAXIMUM, }; + /** + * @brief IEEE type ST power system stabilizer (IEEEST). + * + * A selectable-order notch filter, two lead-lag blocks, a washout, and an + * output limiter. The notch order \f$n\in\{0,1,2,3,4\}\f$ is the degree of + * the expanded denominator derived from \f$A_1,\ldots,A_4\f$. + * + * @tparam scalar_type Plain real or differentiable scalar type. + * @tparam index_type Integer index type. + * + * @see IeeestData + */ template class Ieeest : public Component { @@ -139,27 +151,22 @@ namespace GridKit RealT Vcu_{0}; RealT Tdelay_{0}; + /// Expanded notch denominator \f$a_0 + a_1 s + a_2 s^2 + a_3 s^3 + a_4 s^4\f$ 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}; + /// Order indicators \f$O_k\f$, one when the notch order is at least \f$k\f$ + RealT O1_{0}; + RealT O2_{0}; + RealT O3_{0}; + + // A denominator time constant that is not positive bypasses its block. + RealT bypass_T2_{0}; + RealT bypass_T4_{0}; + RealT bypass_T6_{0}; ComponentSignals signals_; @@ -167,6 +174,7 @@ namespace GridKit void initializeParameters(const ModelDataT& data); void initializeMonitor(); + void setDerivedParameters(); std::vector ws_; std::vector ws_indices_; diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestData.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestData.hpp index 139e928af..8d8ce91ac 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestData.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestData.hpp @@ -19,65 +19,61 @@ namespace GridKit */ enum class IeeestParameters { - A1, ///< Notch filter denominator coefficient - A2, ///< Notch filter denominator coefficient - A3, ///< Notch filter denominator coefficient - A4, ///< Notch filter denominator coefficient - A5, ///< Notch filter numerator coefficient - A6, ///< Notch filter numerator coefficient - T1, ///< Lead-lag 1 numerator time constant - T2, ///< Lead-lag 1 denominator time constant - T3, ///< Lead-lag 2 numerator time constant - T4, ///< Lead-lag 2 denominator time constant - T5, ///< Washout numerator time constant - T6, ///< Washout denominator time constant - Ks, ///< Stabilizer gain - Lsmin, ///< Minimum stabilizer output limit - Lsmax, ///< Maximum stabilizer output limit - Vcl, ///< Lower input cutout threshold (not modeled) - Vcu, ///< Upper input cutout threshold (not modeled) - Tdelay, ///< Input time delay (not modeled) + A1, ///< \f$A_1\f$ Notch filter denominator coefficient [sec] + A2, ///< \f$A_2\f$ Notch filter denominator coefficient [sec^2] + A3, ///< \f$A_3\f$ Notch filter denominator coefficient [sec] + A4, ///< \f$A_4\f$ Notch filter denominator coefficient [sec^2] + A5, ///< \f$A_5\f$ Notch filter numerator coefficient [sec] + A6, ///< \f$A_6\f$ Notch filter numerator coefficient [sec^2] + T1, ///< \f$T_1\f$ Lead-lag 1 numerator time constant [sec] + T2, ///< \f$T_2\f$ Lead-lag 1 denominator time constant [sec] + T3, ///< \f$T_3\f$ Lead-lag 2 numerator time constant [sec] + T4, ///< \f$T_4\f$ Lead-lag 2 denominator time constant [sec] + T5, ///< \f$T_5\f$ Washout numerator time constant [sec] + T6, ///< \f$T_6\f$ Washout denominator time constant [sec] + Ks, ///< \f$K_s\f$ Stabilizer gain [p.u.] + Lsmin, ///< \f$L_s^{\min}\f$ Minimum stabilizer output limit [p.u.] + Lsmax, ///< \f$L_s^{\max}\f$ Maximum stabilizer output limit [p.u.] + Vcl, ///< \f$V_{cl}\f$ Lower input cutout threshold [p.u.], accepted and unused + Vcu, ///< \f$V_{cu}\f$ Upper input cutout threshold [p.u.], accepted and unused + Tdelay, ///< \f$T_{delay}\f$ Input time delay [sec], accepted and unused }; - /** - * @brief Bus keys for IEEEST Stabilizer model. - */ + /// Buses for the IEEEST Stabilizer model. The stabilizer has no + /// electrical terminal. enum class IeeestBuses : size_t { SIZE }; - /** - * @brief Signal input keys for IEEEST Stabilizer model. - */ + /// Signal inputs for the IEEEST Stabilizer model. enum class IeeestSignalInputs : size_t { - input, ///< Unique ID of the stabilizer input signal + input, ///< \f$u\f$ Required stabilizer input signal ID SIZE }; - /** - * @brief Signal output keys for IEEEST Stabilizer model. - */ + /// Signal outputs for the IEEEST Stabilizer model. enum class IeeestSignalOutputs : size_t { - output, ///< Unique ID of the stabilizer output signal + output, ///< \f$V_{ss}\f$ Optional stabilizer output signal ID SIZE }; - /** - * @brief Monitorable variables for IEEEST Stabilizer model. - */ + /// Variables available through the monitor interface. enum class IeeestMonitorableVariables { - vss, ///< Stabilizer output (limited signal) + vss, ///< \f$V_{ss}\f$ Stabilizer output after the limiter [p.u.] }; /** - * @brief Contains modeling data for a IEEEST Stabilizer model. + * @brief Model data for the IEEEST Stabilizer: parameter values, the + * input and output signals, and monitored-variable selections. * * @tparam real_type Real parameter data type * @tparam index_type Integer parameter data type + * + * @see Ieeest */ template struct IeeestData : public ComponentData Ieeest::Ieeest() { - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); } + /** + * @brief Construct an IEEEST stabilizer from model data. + * + * @param[in] data Parameters and monitored-variable selections. + */ template Ieeest::Ieeest(const ModelDataT& data) : monitor_(std::make_unique(data)) { initializeParameters(data); initializeMonitor(); - size_ = 12; + size_ = static_cast(IeeestInternalVariables::MAXIMUM); } template @@ -119,31 +127,26 @@ namespace GridKit Tdelay_ = std::get(data.parameters.at(Parameter::Tdelay)); } + setDerivedParameters(); + } + + template + void Ieeest::setDerivedParameters() + { 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_; + O1_ = static_cast(a1_ + a2_ + a3_ + a4_ > 0.0); + O2_ = static_cast(a2_ + a3_ + a4_ > 0.0); + O3_ = static_cast(a3_ + a4_ > 0.0); - use_T6_block_ = static_cast(T6_ != 0.0); - bypass_T6_block_ = 1.0 - use_T6_block_; + // A denominator time constant that is not positive bypasses its block. + bypass_T2_ = static_cast(T2_ <= 0.0); + bypass_T4_ = static_cast(T4_ <= 0.0); + bypass_T6_ = static_cast(T6_ <= 0.0); } template @@ -188,6 +191,14 @@ namespace GridKit return 0; } + /** + * @brief Validate the IEEEST configuration. + * + * Checks that the required input signal has a linked source and that + * the filter constants are valid + * + * @return Number of configuration errors, zero when valid. + */ template int Ieeest::verify() const { @@ -207,18 +218,47 @@ namespace GridKit ret += 1; } - if (a4_ == 0 && a3_ == 0 && a2_ == 0 && a1_ != 0) + if (A1_ < 0.0 || A2_ < 0.0 || A3_ < 0.0 || A4_ < 0.0) { - Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; + Log::error() << "Ieeest: filter constants A1-A4 must be nonnegative\n"; + ret += 1; + } + + if (T2_ < 0.0 || T4_ < 0.0 || T6_ < 0.0) + { + Log::error() << "Ieeest: time constants T2, T4, and T6 must be nonnegative\n"; ret += 1; } return ret; } + /** + * @brief Initialize IEEEST to steady state at the input operating point. + * + * The notch filter and both lead-lag blocks have unity DC gain, so every + * block output settles at the input \f$u_0\f$ while the washout removes + * the DC component. The initial residual therefore vanishes for any + * \f$u_0\f$ and the stabilizer comes online at rest. + * + * @pre allocate() has completed and the component driving the input + * signal has been initialized. + * @post All internal derivatives are zero and the initial residual is + * zero up to the smoothing error of the output limiter. + */ template int Ieeest::initialize() { + const auto X1 = static_cast(IeeestInternalVariables::X1); + 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); + auto* y = y_.getData(); auto* yp = yp_.getData(); @@ -228,6 +268,28 @@ 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(); + } + + // The chain states above x1 hold derivatives of the filtered signal and + // stay at zero. + y[X1] = u; + y[V4] = u; + y[X5] = u; + y[V5] = u; + y[X6] = u; + y[V6] = u; + y[X7] = u; + + // A bypassed washout is a plain gain and passes the input through. + y[V7] = bypass_T6_ * Ks_ * u; + y[VSS] = Math::clamp(y[V7], Lsmin_, Lsmax_); + y_.setDataUpdated(); yp_.setDataUpdated(); @@ -237,13 +299,13 @@ namespace GridKit 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] = (O1_ > 0.0); + tag_[1] = (O2_ > 0.0); + tag_[2] = (O3_ > 0.0); + tag_[3] = (a4_ > 0.0); + tag_[4] = (T2_ > 0.0); + tag_[5] = (T4_ > 0.0); + tag_[6] = (T6_ > 0.0); tag_[7] = false; tag_[8] = false; tag_[9] = false; @@ -303,19 +365,20 @@ 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_; + // Chain rows up to the notch order, then one accumulated sum row that + // defines the rate of the highest active state. An inactive chain row + // loses its derivative and pins the next state instead. + f[0] = -O1_ * x1_dot + x2; + f[1] = -O2_ * x2_dot + x3; + f[2] = -O3_ * x3_dot + x4; + f[3] = -a0_ * x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 - a4_ * x4_dot + 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] = -T2_ * (v5 - x5) + T1_ * (v4 - x5) + bypass_T2_ * (v4 - v5); + f[9] = -T4_ * (v6 - x6) + T3_ * (v5 - x6) + bypass_T4_ * (v5 - v6); + f[10] = -T6_ * v7 + Ks_ * T5_ * (v6 - x7) + bypass_T6_ * (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..1a7313e4f 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -1,7 +1,18 @@ # **IEEE Stabilizer Model (IEEEST)** -Standard IEEE power system stabilizer: 4th-order notch filter, two lead–lag -blocks, washout, and output limiter. +Standard IEEE power system stabilizer: a selectable-order notch filter, two +lead–lag blocks, washout, and output limiter. + +## Notes + +- The notch order $n$ is the degree of the expanded denominator, so $n$ is the + largest index with $a_n \ne 0$. Order $0$ bypasses the filter. +- The numerator is truncated to the selected order. $A_5$ and $A_6$ are + inactive for $n=0$, and $A_6$ is inactive for $n=1$. +- A zero denominator time constant bypasses its block. $T_2=0$ gives + $v_5=v_4$, $T_4=0$ gives $v_6=v_5$, and $T_6=0$ gives $v_7=K_s v_6$. +- $V_{cl}$, $V_{cu}$, and $T_{delay}$ are accepted for input-format + compatibility but are not modeled. ## Block Diagram @@ -29,11 +40,7 @@ $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 +### Model Derived Parameters ```math \begin{aligned} @@ -45,18 +52,31 @@ a_4 &= A_2 A_4 \end{aligned} ``` +The order indicators select the active notch states: + +```math +O_k = \begin{cases} + 1 & n \ge k \\ + 0 & n < k +\end{cases}, +\qquad k = 1, 2, 3 +``` + ## Model Variables ### Internal Variables #### Differential -Symbol | Units | Description -----------------------|--------|------------ -$x_1, x_2, x_3, x_4$ | [-] | Notch filter states -$x_5$ | [-] | Lead–lag 1 state -$x_6$ | [-] | Lead–lag 2 state -$x_7$ | [-] | Washout state +Symbol | Units | Description | Note +-------|-------------|--------------------------------------|------ +$x_1$ | [p.u.] | Notch filter signal state | Differential for $n\ge1$ +$x_2$ | [p.u./sec] | First derivative of filtered signal | Differential for $n\ge2$ +$x_3$ | [p.u./sec²] | Second derivative of filtered signal | Differential for $n\ge3$ +$x_4$ | [p.u./sec³] | Third derivative of filtered signal | Differential for $n=4$ +$x_5$ | [p.u.] | Lead–lag 1 state | +$x_6$ | [p.u.] | Lead–lag 2 state | +$x_7$ | [p.u.] | Washout state | #### Algebraic @@ -80,12 +100,16 @@ $u$ | [p.u.] | Stabilizer input signal ### Differential Equations +Only the notch states $x_1,\ldots,x_n$ are differential. The accumulated +fourth row defines $x_{n+1}$ algebraically as the rate of $x_n$, and each +chain row above the order instead pins its unused state, $x_{i+1} = 0$. + ```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 &= -O_1 \dot{x}_1 + x_2 \\ +0 &= -O_2 \dot{x}_2 + x_3 \\ +0 &= -O_3 \dot{x}_3 + x_4 \\ +0 &= -a_0 x_1 - a_1 x_2 - a_2 x_3 - a_3 x_4 - a_4 \dot{x}_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 @@ -94,6 +118,10 @@ $u$ | [p.u.] | Stabilizer input signal ### Algebraic Equations +The numerator $1 + A_5 s + A_6 s^2$ is applied to the filtered signal through +its derivative states. Numerator terms above the notch order vanish with the +pinned states. + ```math \begin{aligned} 0 &= -v_4 + x_1 + A_5 x_2 + A_6 x_3 \\ @@ -109,6 +137,39 @@ 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$. +### Input Initialization + +```math +\begin{aligned} + u_0 &\leftarrow \text{stabilizer input signal} +\end{aligned} +``` + +### Internal Initialization + +The notch filter and both lead–lag blocks have unity DC gain, so every block +output settles at $u_0$ while the washout removes the DC component. The +initial residual therefore vanishes for any $u_0$, and the stabilizer comes +online at rest. + +```math +\begin{aligned} + x_1 &\leftarrow u_0 \\ + x_i &\leftarrow 0 + \quad i=2,\ldots,4 \\ + v_4, x_5, v_5, x_6, v_6, x_7 &\leftarrow u_0 \\ + v_7 &\leftarrow 0 \\ + V_{ss} &\leftarrow \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) \\ + \dot{x}_i &\leftarrow 0 +\end{aligned} +``` + +### Output Initialization + +None. + +## Monitorable Outputs + +Output | Units | Description +-------|--------|------------ +`vss` | [p.u.] | Limited stabilizer signal $V_{ss}$ diff --git a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp index 14cd9cd88..3e73925a0 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -129,7 +129,7 @@ namespace GridKit 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.0878, // f[3]: -a0*x1 - a1*x2 - a2*x3 - a3*x4 - a4*x4_dot + u 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 @@ -163,6 +163,37 @@ namespace GridKit return success.report(__func__); } + /** + * @brief verify() rejects negative filter constants and time constants. + * + * The order and bypass masks compare against zero from above, so the + * nonnegativity of A1-A4 and T2, T4, T6 is a validated invariant. One + * error is expected from each violated group. + */ + TestOutcome verifyNegativeParameters() + { + 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 = makeTestData(); + data.parameters[Params::A3] = -0.3; + data.parameters[Params::T6] = -5.0; + + PhasorDynamics::Stabilizer::Ieeest stab(data); + stab.getSignals().template attachSignalNode(&u_node); + stab.allocate(); + + success *= (stab.verify() == 2); + + return success.report(__func__); + } + #ifdef GRIDKIT_ENABLE_ENZYME /** * @brief Compare DependencyTracking Jacobian against Enzyme Jacobian. @@ -230,6 +261,10 @@ namespace GridKit std::vector residual_y(residual_y_view.getData(), residual_y_view.getData() + residual_y_view.getSize()); // --- d/dy': tag derivatives as independent --- + // Untag u before initialize(), which seeds the states from it, so that + // only the derivatives carry dependencies through this pass. + u_value = 0.5; + stab.initialize(); auto* yp = stab.yp().getData(); for (size_t i = 0; i < stab.size(); ++i) @@ -237,7 +272,6 @@ namespace GridKit yp[i].setVariableNumber(i); } - u_value = 0.5; setStatePointDep(stab); stab.evaluateResidual(); diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index c1999c24a..66d4d8574 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -9,6 +9,7 @@ int main() result += test.constructor(); result += test.zeroInitialResidual(); result += test.residual(); + result += test.verifyNegativeParameters(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); #endif