diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp index a0746bb85..70b1d401b 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp @@ -60,7 +60,10 @@ namespace GridKit OMEGA, ///< Generator speed deviation VREAL, ///< Real bus voltage VIMAG, ///< Imaginary bus voltage + VREF, ///< Voltage reference VS, ///< Stabilizer output signal + VUEL, ///< Under-excitation limiter signal + VOEL, ///< Over-excitation limiter signal MAXIMUM, }; @@ -153,11 +156,15 @@ namespace GridKit RealT SA_{0}; RealT SB_{0}; - // External Variables that don't have models yet. - // They are constants until then. - ScalarT vref_{0}; // (Setpoint voltage, can be different from terminal voltage) - ScalarT vUEL_{0}; - ScalarT vOEL_{0}; + // Runtime connection masks keep the summing junction Enzyme sparse-solvable + RealT uel_on_{0}; + RealT oel_on_{0}; + + ScalarT omega_set_{0}; + ScalarT vref_set_{0}; + ScalarT vs_set_{0}; + ScalarT vuel_set_{0}; + ScalarT voel_set_{0}; /// Component signal extension ComponentSignals signals_; diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp index d9e1f783d..5061792c7 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp @@ -45,7 +45,10 @@ namespace GridKit enum class Ieeet1SignalInputs : size_t { speed, ///< Unique ID of the generator speed signal + vref, ///< Unique ID of the voltage reference signal (optional) vs, ///< Unique ID of the stabilizer output signal (optional) + vuel, ///< Unique ID of the under-excitation limiter signal (optional) + voel, ///< Unique ID of the over-excitation limiter signal (optional) SIZE }; diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp index 8395b275a..4a124cd1d 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp @@ -100,12 +100,9 @@ namespace GridKit wb_.resize(2); // Resize signal variable data - ws_.resize(2); - ws_indices_.resize(2); - ws_[0] = 0.0; - ws_indices_[0] = INVALID_INDEX; - ws_[1] = 0.0; - ws_indices_[1] = INVALID_INDEX; + const auto signal_size = static_cast(Ieeet1ExternalVariables::MAXIMUM); + ws_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); // Set output signals if (signals_.template isAssigned()) @@ -124,9 +121,6 @@ namespace GridKit template int Ieeet1::verify() const { - static constexpr auto OMEGA = Ieeet1ExternalVariables::OMEGA; - static constexpr auto VS = Ieeet1ExternalVariables::VS; - int ret = 0; auto check = [&](bool condition, const char* message) @@ -157,23 +151,22 @@ namespace GridKit check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently"); } - if (signals_.template isAttached()) + auto check_attached_signal = + [&](const char* name) { - if (!signals_.template isLinked()) + if (signals_.template isAttached() + && !signals_.template isLinked()) { - Log::error() << "Ieeet1: omega signal attached with no linked generator\n"; + Log::error() << "Ieeet1: " << name << " signal attached with no linked source\n"; ret += 1; } - } + }; - if (signals_.template isAttached()) - { - if (!signals_.template isLinked()) - { - Log::error() << "Ieeet1: VS signal attached with no linked source\n"; - ret += 1; - } - } + check_attached_signal.template operator()("speed"); + check_attached_signal.template operator()("vref"); + check_attached_signal.template operator()("vs"); + check_attached_signal.template operator()("vuel"); + check_attached_signal.template operator()("voel"); return ret; } @@ -187,9 +180,10 @@ namespace GridKit * Inputs: * - EFD assigned by the generator. * - Bus voltage, used to form the sensed terminal voltage magnitude. - * - Attached external signals (omega, V_S) + * - Attached external signals (omega, V_S, V_UEL, V_OEL) * * Enabled saturation is included via ksat computed from efdp and SA, SB. + * The resolved V_ref is written to an attached vref signal. * * @warning IEEE Std 421.5-2016 states: “In some programs, if * \f$K_{E}\f$ is entered as zero, \f$K_{E}\f$ is automatically @@ -226,15 +220,30 @@ namespace GridKit efd0 = y[7]; ///<- generator needs to be initialized first } - ScalarT omega{0}; - ScalarT vs{0}; - if (signals_.template isAttached()) + auto read_attached = [&]() -> ScalarT + { + if (signals_.template isAttached()) + { + return signals_.template readExternalVariable(); + } + return ScalarT{0}; + }; + + const ScalarT omega = read_attached.template operator()(); + const ScalarT vs = read_attached.template operator()(); + const ScalarT vuel = read_attached.template operator()(); + const ScalarT voel = read_attached.template operator()(); + + uel_on_ = ZERO; + if (signals_.template isAttached()) { - omega = signals_.template readExternalVariable(); + uel_on_ = ONE; } - if (signals_.template isAttached()) + + oel_on_ = ZERO; + if (signals_.template isAttached()) { - vs = signals_.template readExternalVariable(); + oel_on_ = ONE; } // Terminal Voltage @@ -266,7 +275,7 @@ namespace GridKit ScalarT vf{0}; ScalarT vfx = (Kf_ / Tf_) * efdp; - vref_ = Ec + vtr + vf - vUEL_ - vOEL_ - vs; + const ScalarT vref = Ec + vtr + vf - vs - uel_on_ * vuel - oel_on_ * voel; y[0] = Ec; // y0 - vts - Sensed term volt y[1] = vr; // y1 - vr - Voltage reg @@ -283,6 +292,17 @@ namespace GridKit yp[i] = 0.0; } + omega_set_ = omega; + vref_set_ = vref; + vs_set_ = vs; + vuel_set_ = vuel; + voel_set_ = voel; + + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable(vref_set_); + } + y_.setDataUpdated(); yp_.setDataUpdated(); @@ -341,6 +361,12 @@ namespace GridKit const ScalarT* ws, ScalarT* f) { + const auto OMEGA = static_cast(Ieeet1ExternalVariables::OMEGA); + const auto VREF = static_cast(Ieeet1ExternalVariables::VREF); + const auto VS = static_cast(Ieeet1ExternalVariables::VS); + const auto VUEL = static_cast(Ieeet1ExternalVariables::VUEL); + const auto VOEL = static_cast(Ieeet1ExternalVariables::VOEL); + // Read bus voltage components ScalarT vreal = wb[0]; ScalarT vimag = wb[1]; @@ -364,8 +390,11 @@ namespace GridKit ScalarT vfx_dot = yp[3]; // Set signal variable aliases - ScalarT omega = ws[0]; - ScalarT vs_signal = ws[1]; + ScalarT omega = ws[OMEGA]; + ScalarT vref = ws[VREF]; + ScalarT vs = ws[VS]; + ScalarT vuel = ws[VUEL]; + ScalarT voel = ws[VOEL]; // The 'pre-limit' derivative of Vr. ScalarT func = (-vr + Ka_ * vtr) / Ta_; @@ -377,7 +406,7 @@ namespace GridKit f[3] = -vfx_dot + vf / Tf_; // Internal Algebraic Equations - f[4] = -vts + vref_ + vUEL_ + vOEL_ + vs_signal - vtr - vf; + f[4] = -vts + vref + vs + uel_on_ * vuel + oel_on_ * voel - vtr - vf; f[5] = -Tf_ * (vf + vfx) + Kf_ * efdp; f[6] = -ve + ksat; f[7] = -efd + efdp + omega * efdp * Ispdlim_; @@ -393,21 +422,24 @@ namespace GridKit template int Ieeet1::evaluateResidual() { - // Set input variables. - if (signals_.template isAttached()) + // Attached signals are read live; unattached ones keep the latched value. + auto read_signal = [&](const ScalarT& latched) { - ws_[0] = signals_.template readExternalVariable(); - ws_indices_[0] = signals_.template readExternalVariableIndex(); - } + const auto index = static_cast(variable); + ws_[index] = latched; + ws_indices_[index] = INVALID_INDEX; + if (signals_.template isAttached()) + { + ws_[index] = signals_.template readExternalVariable(); + ws_indices_[index] = signals_.template readExternalVariableIndex(); + } + }; - // VS signal (stabilizer output, optional) - ws_[1] = 0.0; - ws_indices_[1] = INVALID_INDEX; - if (signals_.template isAttached()) - { - ws_[1] = signals_.template readExternalVariable(); - ws_indices_[1] = signals_.template readExternalVariableIndex(); - } + read_signal.template operator()(omega_set_); + read_signal.template operator()(vref_set_); + read_signal.template operator()(vs_set_); + read_signal.template operator()(vuel_set_); + read_signal.template operator()(voel_set_); // Bus voltages wb_[0] = bus_->Vr(); diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/README.md b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/README.md index b5d7d3a6d..1e928dc56 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/README.md +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/README.md @@ -34,6 +34,8 @@ $I_{\mathrm{spdlim}}$ | [binary] | Speed limit flag indicator | 0 | ### Parameter Validation Invalid IEEET1 parameter sets are rejected by the following checks. Let $\epsilon_T=10^{-3}$. +Time constants below $\epsilon_T$ are raised to $\epsilon_T$ and logged as a warning; +every other condition is a configuration error. ```math \begin{aligned} @@ -135,6 +137,15 @@ K_E^{\mathrm{eff}} Thus $K_E^{\mathrm{eff}}$ is the resolved value of the same exciter coefficient, not an additional model input. +## Model Ports + +Name | Port | Init | Description +--------|--------|-------|------------ +`bus` | Bus | Known | Terminal bus voltage +`speed` | Input | Known | Optional machine speed deviation; defaults to zero +`vs` | Input | Known | Optional stabilizer input signal; defaults to zero +`efd` | Output | Known | Field-voltage output seeded by the machine + ## Model Variables ### Internal Variables @@ -145,7 +156,7 @@ Symbol | Units | Description | Note ----------|--------|------------------------------------|------- $V_{ts}$ | [p.u.] | Sensed terminal voltage | $V_R$ | [p.u.] | Voltage regulator | -$E_{fd}'$ | [p.u.] | Field-current pre-speed multiplier | +$E_{fd}'$ | [p.u.] | Field voltage before the speed multiplier | $V_{fx}$ | [p.u.] | Exciter feedback internal state | @@ -162,11 +173,17 @@ $k_\text{sat}$ | [p.u.] | Scaled-quadratic saturation contribution | $E_{fd}'S( ### External Variables +#### Differential + +None. + +#### Algebraic + Symbol | Units | Description | Note ----------------|--------|-----------------------------------|------- $V_r$ | [p.u.] | Real bus voltage component | $V_i$ | [p.u.] | Imaginary bus voltage component | -$V_\text{ref}$ | [p.u.] | Reference terminal voltage | +$V_\text{ref}$ | [p.u.] | Reference terminal voltage | Set during initialization; constant thereafter $V_{UEL}$ | [p.u.] | Input from under excitation limiter | Constant zero until modeled $V_{OEL}$ | [p.u.] | Input from over excitation limiter | Constant zero until modeled $V_S$ | [p.u.] | Input from stabilizer controller | Optional, defaults to zero @@ -175,7 +192,9 @@ $\omega$ | [p.u.] | Machine speed deviation | Opti ## Model Equations -### Differential Equations +### Internal Equations + +#### Differential For readability, define the pre-limit derivative of $V_R$ and voltage-sensing input: @@ -201,7 +220,7 @@ The IEEET1 differential equations, as derived from the model diagram, are: CommonMath defines the smooth [Anti-Windup](../../../../CommonMath.md#antiwindup) target and approximation. -### Algebraic Equations +#### Algebraic The algebraic equations of the exciter. ```math @@ -214,13 +233,14 @@ The algebraic equations of the exciter. \end{aligned} ``` -Here $q$ is GridKit's [Quadratic Ramp](../../../../CommonMath.md#primitives). +Here $q$ is GridKit's [Quadratic Ramp](../../../../CommonMath.md#quadratic-ramp). + +### External Equations +None. ## Initialization -The implementation first applies $T \leftarrow \max(T, 10^{-3})$ for -$T \in \{T_R, T_A, T_E, T_F\}$. This should be replaced with a structural template change in the future. The machine initializes $E_{fd}$ first. IEEET1 reads that value, along with any attached $\omega$ and $V_S$, and solves the steady-state algebraic chain so all residuals vanish with @@ -246,9 +266,9 @@ with the current input values. All internal derivatives initialize to zero. -## Monitorable Variables +## Monitors -Variable | Units | Description | Note +Monitor | Units | Description | Note ---------|--------|-----------------------------------|------ `efd` | [p.u.] | Field winding voltage | `ksat` | [p.u.] | Scaled-quadratic saturation contribution | $S_B\,q(E_{fd}'-S_A)$ diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/README.md b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/README.md index ba79674d6..7aed72c29 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/README.md +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/README.md @@ -23,6 +23,32 @@ PowerWorld/PSS/E SEXS_PTI data often gives $T_A/T_B$ as a ratio. GridKit stores $T_A$ and $T_B$ separately, so convert ratio-format data with $T_A = (T_A/T_B)T_B$ before passing parameters to the model. +All six parameters are required; there are no defaults. + +### Parameter Validation + +Invalid SEXS-PTI parameter sets are rejected by the following checks: + +```math +\begin{aligned} + T_A &\ge 0 \\ + T_B, T_E, K &> 0 \\ + E_{fd}^{\min} &< E_{fd}^{\max} +\end{aligned} +``` + +### Model Derived Parameters + +None. + +## Model Ports + +Name | Port | Init | Description +------|--------|-------|------------ +`bus` | Bus | Known | Terminal bus voltage +`vs` | Input | Known | Optional stabilizer input signal; defaults to zero +`efd` | Output | Known | Required field-voltage output seeded by the machine + ## Model Variables ### Internal Variables @@ -50,7 +76,8 @@ None. Symbol | Units | Description | Note ----------------|--------|----------------------------------------------|----- -$E_C$ | [p.u.] | Compensated machine terminal voltage magnitude | Computed from bus voltage +$V_r$ | [p.u.] | Terminal voltage, real component | Bus input +$V_i$ | [p.u.] | Terminal voltage, imaginary component | Bus input $V_{ref}$ | [p.u.] | Reference voltage | Set during initialization $V_S$ | [p.u.] | Stabilizer output | Optional, defaults to zero $V_{OEL}$ | [p.u.] | Over-excitation limiter signal | Constant zero until modeled @@ -58,7 +85,15 @@ $V_{UEL}$ | [p.u.] | Under-excitation limiter signal | Consta ## Model Equations -### Differential Equations +Define the compensated terminal voltage magnitude for readability: + +```math +E_C = \sqrt{V_r^2+V_i^2}. +``` + +### Internal Equations + +#### Differential The SEXS-PTI differential equations, as derived from the model diagram. Define the pre-limit derivative of $E_{fd}$ @@ -84,7 +119,7 @@ so that $\dot E_{fd}$ can be written in piecewise form compactly. In simulation the piecewise form above is replaced with a smooth approximation where $\phi$ is GridKit's smooth anti-windup indicator. See [CommonMath: Anti-Windup Indicator](../../../../CommonMath.md#antiwindup) for its definition, behavior, and design rationale. -### Algebraic Equations +#### Algebraic ```math \begin{aligned} @@ -92,6 +127,10 @@ In simulation the piecewise form above is replaced with a smooth approximation w \end{aligned} ``` +### External Equations + +None. + ## Initialization The generator initializes the EFD signal first. SEXS-PTI then reads that value @@ -99,7 +138,6 @@ as $E_{fd,0}$ and assumes steady state with $V_S=V_{OEL}=V_{UEL}=0$: ```math \begin{aligned} -E_C &= \sqrt{V_r^2+V_i^2} \\ V_{tr,0} &= \dfrac{E_{fd,0}}{K} \\ V_{R,0} &= (T_A - T_B)V_{tr,0} \\ V_{ref} &= E_C + V_{tr,0} @@ -107,3 +145,9 @@ V_{ref} &= E_C + V_{tr,0} ``` All derivatives initialize to zero. + +## Monitors + +Monitor | Units | Description | Note +--------|--------|----------------------|------ +`efd` | [p.u.] | Field-voltage output | $E_{fd}$ diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp index b1268515e..8574248f2 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp @@ -47,7 +47,10 @@ namespace GridKit /// External variables of a `SexsPti`. enum class SexsPtiExternalVariables : size_t { - VS, ///< Stabilizer output signal + VREF, ///< Voltage reference + VS, ///< Stabilizer output signal + VUEL, ///< Under-excitation limiter signal + VOEL, ///< Over-excitation limiter signal MAXIMUM, }; @@ -120,9 +123,14 @@ namespace GridKit int missing_param_count_{0}; - ScalarT vref_{0}; - ScalarT vOEL_{0}; - ScalarT vUEL_{0}; + // Runtime connection masks keep the summing junction Enzyme sparse-solvable + RealT uel_on_{0}; + RealT oel_on_{0}; + + ScalarT vref_set_{0}; + ScalarT vs_set_{0}; + ScalarT vuel_set_{0}; + ScalarT voel_set_{0}; ComponentSignals signals_; diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiData.hpp b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiData.hpp index db70477a5..6f21d5255 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiData.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiData.hpp @@ -35,7 +35,10 @@ namespace GridKit /// Signal inputs for the SEXS-PTI exciter model. enum class SexsPtiSignalInputs : size_t { - vs, ///< Unique ID of the optional stabilizer output signal + vref, ///< Unique ID of the optional voltage reference signal + vs, ///< Unique ID of the optional stabilizer output signal + vuel, ///< Unique ID of the optional under-excitation limiter signal + voel, ///< Unique ID of the optional over-excitation limiter signal SIZE }; diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp index 8b8e53a11..148dd06f7 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp @@ -76,10 +76,9 @@ namespace GridKit wb_.resize(2); - ws_.resize(1); - ws_indices_.resize(1); - ws_[0] = 0.0; - ws_indices_[0] = INVALID_INDEX; + const auto signal_size = static_cast(SexsPtiExternalVariables::MAXIMUM); + ws_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); if (signals_.template isAssigned()) { @@ -134,14 +133,21 @@ namespace GridKit ret += 1; } - if (signals_.template isAttached()) + auto check_attached_signal = + [&](const char* name) { - if (!signals_.template isLinked()) + if (signals_.template isAttached() + && !signals_.template isLinked()) { - Log::error() << "SexsPti: VS signal attached with no linked source\n"; + Log::error() << "SexsPti: " << name << " signal attached with no linked source\n"; ret += 1; } - } + }; + + check_attached_signal.template operator()("vref"); + check_attached_signal.template operator()("vs"); + check_attached_signal.template operator()("vuel"); + check_attached_signal.template operator()("voel"); return ret; } @@ -158,13 +164,37 @@ namespace GridKit efd0 = y[1]; } + auto read_attached = [&]() -> ScalarT + { + if (signals_.template isAttached()) + { + return signals_.template readExternalVariable(); + } + return ScalarT{0}; + }; + + const ScalarT vs = read_attached.template operator()(); + const ScalarT vuel = read_attached.template operator()(); + const ScalarT voel = read_attached.template operator()(); + + uel_on_ = ZERO; + if (signals_.template isAttached()) + { + uel_on_ = ONE; + } + + oel_on_ = ZERO; + if (signals_.template isAttached()) + { + oel_on_ = ONE; + } + ScalarT vreal = bus_->Vr(); ScalarT vimag = bus_->Vi(); ScalarT Ec = std::sqrt(vreal * vreal + vimag * vimag); ScalarT vtr = efd0 / K_; ScalarT vr = (Ta_ - Tb_) * vtr; - - vref_ = Ec + vtr; + ScalarT vref = Ec + vtr - vs - uel_on_ * vuel - oel_on_ * voel; y[0] = vr; y[1] = efd0; @@ -175,6 +205,16 @@ namespace GridKit yp[static_cast(i)] = 0.0; } + vref_set_ = vref; + vs_set_ = vs; + vuel_set_ = vuel; + voel_set_ = voel; + + if (signals_.template isAttached()) + { + signals_.template writeExternalVariable(vref_set_); + } + y_.setDataUpdated(); yp_.setDataUpdated(); @@ -218,20 +258,28 @@ namespace GridKit const ScalarT* ws, ScalarT* f) { + const auto VREF = static_cast(SexsPtiExternalVariables::VREF); + const auto VS = static_cast(SexsPtiExternalVariables::VS); + const auto VUEL = static_cast(SexsPtiExternalVariables::VUEL); + const auto VOEL = static_cast(SexsPtiExternalVariables::VOEL); + ScalarT vr = y[0]; ScalarT efd = y[1]; ScalarT vtr = y[2]; ScalarT vr_dot = yp[0]; ScalarT efd_dot = yp[1]; - ScalarT Ec = std::sqrt(wb[0] * wb[0] + wb[1] * wb[1]); - ScalarT vs = ws[0]; + ScalarT Ec = std::sqrt(wb[0] * wb[0] + wb[1] * wb[1]); + ScalarT vref = ws[VREF]; + ScalarT vs = ws[VS]; + ScalarT vuel = ws[VUEL]; + ScalarT voel = ws[VOEL]; ScalarT func = (-efd + (K_ / Tb_) * (-vr + Ta_ * vtr)) / Te_; f[0] = -vr_dot + (-vr + Ta_ * vtr) / Tb_ - vtr; f[1] = -efd_dot + Math::antiwindup(efd, func, Efdmin_, Efdmax_); - f[2] = -vtr - Ec + vref_ + vs + vOEL_ + vUEL_; + f[2] = -vtr - Ec + vref + vs + uel_on_ * vuel + oel_on_ * voel; return 0; } @@ -239,13 +287,23 @@ namespace GridKit template int SexsPti::evaluateResidual() { - ws_[0] = 0.0; - ws_indices_[0] = INVALID_INDEX; - if (signals_.template isAttached()) + // Attached signals are read live; unattached ones keep the latched value. + auto read_signal = [&](const ScalarT& latched) { - ws_[0] = signals_.template readExternalVariable(); - ws_indices_[0] = signals_.template readExternalVariableIndex(); - } + const auto index = static_cast(variable); + ws_[index] = latched; + ws_indices_[index] = INVALID_INDEX; + if (signals_.template isAttached()) + { + ws_[index] = signals_.template readExternalVariable(); + ws_indices_[index] = signals_.template readExternalVariableIndex(); + } + }; + + read_signal.template operator()(vref_set_); + read_signal.template operator()(vs_set_); + read_signal.template operator()(vuel_set_); + read_signal.template operator()(voel_set_); wb_[0] = bus_->Vr(); wb_[1] = bus_->Vi(); diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 8e640eac1..6312d350e 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -486,6 +486,13 @@ namespace GridKit exciter->getSignals().template attachSignalNode(getSignal(speed)); } + if (excitedata.signal_inputs.contains(Ieeet1SignalInputs::vref)) + { + IdxT vref = excitedata.signal_inputs.at(Ieeet1SignalInputs::vref); + constexpr auto VREF = Ieeet1ExternalVariables::VREF; + exciter->getSignals().template attachSignalNode(getSignal(vref)); + } + if (excitedata.signal_outputs.contains(Ieeet1SignalOutputs::efd)) { IdxT efd = excitedata.signal_outputs.at(Ieeet1SignalOutputs::efd); @@ -500,6 +507,20 @@ namespace GridKit exciter->getSignals().template attachSignalNode(getSignal(vs)); } + if (excitedata.signal_inputs.contains(Ieeet1SignalInputs::vuel)) + { + IdxT vuel = excitedata.signal_inputs.at(Ieeet1SignalInputs::vuel); + constexpr auto VUEL = Ieeet1ExternalVariables::VUEL; + exciter->getSignals().template attachSignalNode(getSignal(vuel)); + } + + if (excitedata.signal_inputs.contains(Ieeet1SignalInputs::voel)) + { + IdxT voel = excitedata.signal_inputs.at(Ieeet1SignalInputs::voel); + constexpr auto VOEL = Ieeet1ExternalVariables::VOEL; + exciter->getSignals().template attachSignalNode(getSignal(voel)); + } + addComponent(exciter); } @@ -561,6 +582,13 @@ namespace GridKit auto* exciter = new SexsPti(getBus(bus_index), excitedata); + if (excitedata.signal_inputs.contains(SexsPtiSignalInputs::vref)) + { + IdxT vref = excitedata.signal_inputs.at(SexsPtiSignalInputs::vref); + constexpr auto VREF = SexsPtiExternalVariables::VREF; + exciter->getSignals().template attachSignalNode(getSignal(vref)); + } + if (excitedata.signal_outputs.contains(SexsPtiSignalOutputs::efd)) { IdxT efd = excitedata.signal_outputs.at(SexsPtiSignalOutputs::efd); @@ -575,6 +603,20 @@ namespace GridKit exciter->getSignals().template attachSignalNode(getSignal(vs)); } + if (excitedata.signal_inputs.contains(SexsPtiSignalInputs::vuel)) + { + IdxT vuel = excitedata.signal_inputs.at(SexsPtiSignalInputs::vuel); + constexpr auto VUEL = SexsPtiExternalVariables::VUEL; + exciter->getSignals().template attachSignalNode(getSignal(vuel)); + } + + if (excitedata.signal_inputs.contains(SexsPtiSignalInputs::voel)) + { + IdxT voel = excitedata.signal_inputs.at(SexsPtiSignalInputs::voel); + constexpr auto VOEL = SexsPtiExternalVariables::VOEL; + exciter->getSignals().template attachSignalNode(getSignal(voel)); + } + addComponent(exciter); } diff --git a/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp b/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp index 8da491c44..edae57cc1 100644 --- a/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp +++ b/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp @@ -28,6 +28,9 @@ namespace GridKit ExciterIeeet1Tests() = default; ~ExciterIeeet1Tests() = default; + static constexpr RealT kTol = + static_cast(4.0) * std::numeric_limits::epsilon(); + TestOutcome constructor() { TestStatus success = true; @@ -204,6 +207,56 @@ namespace GridKit return success.report(__func__); } + TestOutcome vrefAndUelPorts() + { + TestStatus success = true; + + auto data = makeTestData(); + + PhasorDynamics::Bus bus(3.0, 4.0); + PhasorDynamics::Exciter::Ieeet1 exciter(&bus, data); + PhasorDynamics::SignalNode efd_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode vuel_node; + ScalarT efd_value{0.0}; + ScalarT vref_value{0.0}; + ScalarT vuel_value{-0.4}; + IdxT efd_index = INVALID_INDEX; + IdxT vref_index = 11; + IdxT vuel_index = 12; + + efd_node.set(&efd_value, &efd_index); + vref_node.set(&vref_value, &vref_index); + vuel_node.set(&vuel_value, &vuel_index); + exciter.getSignals() + .template assignSignalNode(&efd_node); + exciter.getSignals() + .template attachSignalNode(&vref_node); + exciter.getSignals() + .template attachSignalNode(&vuel_node); + + bus.allocate(); + exciter.allocate(); + + bus.initialize(); + efd_node.init(1.2); + success *= (exciter.initialize() == 0); + exciter.evaluateResidual(); + + const auto* y = exciter.y().getData(); + const auto* f = exciter.getResidual().getData(); + + // vref = vts + vtr + vf - vuel; the masked vuel then enters with unit gain + success *= isEqual(vref_node.read(), y[0] + y[4] + y[5] - vuel_value, kTol); + success *= isEqual(f[4], static_cast(0.0), kTol); + + vuel_value = -0.3; + exciter.evaluateResidual(); + success *= isEqual(f[4], static_cast(0.1), kTol); + + return success.report(__func__); + } + #ifdef GRIDKIT_ENABLE_ENZYME /** * @brief Checks Jacobian evaluation. diff --git a/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp b/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp index 6909a7ad8..14b2d83d9 100644 --- a/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp @@ -124,8 +124,57 @@ namespace GridKit exciter.initialize(); exciter.evaluateResidual(); + // vref absorbs the attached vs at initialization; later changes pass through const auto* f = exciter.getResidual().getData(); - success *= isEqual(f[2], vs_value, kTol); + success *= isEqual(f[2], static_cast(0.0), kTol); + + vs_value = 0.35; + exciter.evaluateResidual(); + success *= isEqual(f[2], static_cast(0.1), kTol); + + return success.report(__func__); + } + + TestOutcome vrefAndUelPorts() + { + TestStatus success = true; + + PhasorDynamics::Bus bus(3.0, 4.0); + bus.allocate(); + bus.initialize(); + + PhasorDynamics::SignalNode efd_node; + PhasorDynamics::SignalNode vref_node; + PhasorDynamics::SignalNode vuel_node; + ScalarT efd_value{0.0}; + ScalarT vref_value{0.0}; + ScalarT vuel_value{-0.4}; + IdxT efd_index = INVALID_INDEX; + IdxT vref_index = 5; + IdxT vuel_index = 6; + efd_node.set(&efd_value, &efd_index); + vref_node.set(&vref_value, &vref_index); + vuel_node.set(&vuel_value, &vuel_index); + + auto data = makeTestData(); + PhasorDynamics::Exciter::SexsPti exciter(&bus, data); + exciter.getSignals().template assignSignalNode(&efd_node); + exciter.getSignals().template attachSignalNode(&vref_node); + exciter.getSignals().template attachSignalNode(&vuel_node); + + exciter.allocate(); + efd_node.init(1.2); + success *= (exciter.initialize() == 0); + exciter.evaluateResidual(); + + // vref = Ec + vtr - vuel = 5 + 0.12 + 0.4; the masked vuel then enters with unit gain + const auto* f = exciter.getResidual().getData(); + success *= isEqual(vref_node.read(), static_cast(5.52), kTol); + success *= isEqual(f[2], static_cast(0.0), kTol); + + vuel_value = -0.3; + exciter.evaluateResidual(); + success *= isEqual(f[2], static_cast(0.1), kTol); return success.report(__func__); } diff --git a/tests/UnitTests/PhasorDynamics/runExciterIeeet1Tests.cpp b/tests/UnitTests/PhasorDynamics/runExciterIeeet1Tests.cpp index 997e28a3a..898e3788a 100644 --- a/tests/UnitTests/PhasorDynamics/runExciterIeeet1Tests.cpp +++ b/tests/UnitTests/PhasorDynamics/runExciterIeeet1Tests.cpp @@ -10,6 +10,7 @@ int main() result += test.zeroInitialResidual(); result += test.zeroTimeConstantsAndDisabledSaturation(); result += test.invalidSaturationParameters(); + result += test.vrefAndUelPorts(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); #endif diff --git a/tests/UnitTests/PhasorDynamics/runExciterSexsPtiTests.cpp b/tests/UnitTests/PhasorDynamics/runExciterSexsPtiTests.cpp index eaf79d456..f46a0ae0f 100644 --- a/tests/UnitTests/PhasorDynamics/runExciterSexsPtiTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runExciterSexsPtiTests.cpp @@ -9,6 +9,7 @@ int main() result += test.constructor(); result += test.zeroInitialResidual(); result += test.vsSignal(); + result += test.vrefAndUelPorts(); result += test.antiWindupLimiter(); result += test.parameterValidation(); result += test.systemAssembly();