From 01a5c8e175de27213b799067108c3240dfdd93cd Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 27 Aug 2026 05:13:57 -0700 Subject: [PATCH 1/3] add the ieeest stabilizer model --- docs/developer-guide/andes-compatibility.md | 37 +- src/fileInput/gridDynReadDYR.cpp | 50 ++ src/griddyn/CMakeLists.txt | 2 + src/griddyn/stabilizers/Stabilizer.cpp | 3 + src/griddyn/stabilizers/StabilizerIEEEST.cpp | 689 ++++++++++++++++++ src/griddyn/stabilizers/StabilizerIEEEST.h | 177 +++++ test/andesTests/testAndesDyrReader.cpp | 53 ++ test/componentTests/testStabilizers.cpp | 150 ++++ test/test_files/andes_tests/ieee14_ieeest.dyr | 2 + 9 files changed, 1160 insertions(+), 3 deletions(-) create mode 100644 src/griddyn/stabilizers/StabilizerIEEEST.cpp create mode 100644 src/griddyn/stabilizers/StabilizerIEEEST.h create mode 100644 test/test_files/andes_tests/ieee14_ieeest.dyr diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index 6f069cbc3..04a3351c8 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -85,7 +85,7 @@ adapters still have attachment, parameter, equation, and validation gaps. | `GAST` | `GAST` | None exact | **No direct analogue.** Add a gas-turbine governor implementation and DYR adapter. | | `GGOV1` | `TGOV1` | `GovernorTgov1` | **Planned compatibility approximation.** ANDES currently retains only `R` when converting this record. If reproduced, emit a diagnostic and keep exact GGOV1 support as a separate task. | | `ST2CUT` | `ST2CUT` | `StabilizerST2CUT` | **Partial.** Exact frozen-ANDES local modes 0/1/3/4/5, dual transducers, washout/lag, three lead-lag stages, output limits, voltage gating, DYR mapping, generator/exciter coupling, and dynamic load-step coverage are implemented. Remote `BUSR` inputs and ANDES modes 2/6 require cross-bus/frequency-derivative measurement routing and are rejected rather than approximated. | -| `IEEEST` | `IEEEST` | Base `Stabilizer` only | **No direct analogue.** The base class is not a usable PSS implementation; add the model and its generator/exciter signal connections. | +| `IEEEST` | `IEEEST` | `StabilizerIEEEST` | **Partial.** Exact frozen-ANDES local modes 0/1/3/4/5, filter/lead-lag/washout zero-bypass semantics, output limits, voltage gate, DYR mapping, and GENROU/exciter coupling are implemented. Modes 2/6 and nonzero `BUSR` require unavailable cross-bus/frequency-derivative measurement routing and are rejected rather than approximated. A captured ANDES trajectory remains open. | | `REGCA1`, `REECA1`, `REECB1`, `REPCA1` | Same named ANDES models | `GenModelInverter` is not equivalent | **No direct analogue.** Add the coordinated renewable generator, electrical-control, and plant-control chain rather than flattening these records into the generic inverter. | | `WTDTA1`, `WTARA1`, `WTPTA1`, `WTTQA1` | Same named ANDES models | None | **No direct analogue.** Add drive-train, aerodynamic, pitch, and torque-control submodels with their shared interfaces. | | `Toggle`, `Fault` | ANDES event models | GridDyn event/action and fault mechanisms | **Partial conceptually.** Define DYR record schemas and translate target resolution, timing, status changes, and fault clearing semantics; no adapter exists. | @@ -177,7 +177,7 @@ seconds, and continuing through 2.0 seconds. The five controller chains are: | 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | | 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Implemented | PR 2 | | 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Models/adapters implemented; trajectories open | PR 3 | -| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | `IEEEG1` implemented; `ST2CUT` partial; `IEEEST` planned | PR 4 | +| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | `IEEEG1` implemented; `ST2CUT` and `IEEEST` partial | PR 4 | | 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | PRs must merge in this order. Each PR must pass without relying on production @@ -507,6 +507,36 @@ transitions, GENROU plus exciter coupling, and a load-step dynamic run whose ST2CUT states must evolve. A captured ANDES trajectory and support for the remote/derivative measurement modes remain open. +#### IEEEST implementation and parameter contract + +`StabilizerIEEEST` follows frozen ANDES v2.0.0 +`andes/models/pss/ieeest.py`. Its DYR adapter uses the exact frozen +`psse-dyr.yaml` order `BUS, ID, MODE, BUSR, A1, A2, A3, A4, A5, A6, T1, +T2, T3, T4, T5, T6, KS, LSMAX, LSMIN, VCU, VCL`. The detailed, buildable +Doxygen equations and source references are in +`src/griddyn/stabilizers/StabilizerIEEEST.h`. In summary, the selected signal +passes through the second-order lag `F1`, second-order lead-lag `F2`, two +lead-lag stages, gain `KS`, and ANDES `WashoutOrLag`, before the +`[LSMIN, LSMAX]` output limit and initialized-terminal-voltage-relative +`[VCL, VCU]` gate. Zero `VCU`/`VCL` maps to `+999`/`-999`, as in ANDES. + +The exact ANDES `zero_out` forms are retained: `A4=0` bypasses `F2`, `T2=0` +bypasses the first lead-lag, and `T4=0` bypasses the second lead-lag. `A2=0` +is represented as the exact first-order `F1` reduction when `A1>0`, or as a +bypass when both are zero; no singular dummy state is allocated. The actual +IEEE 14 IEEEST record exercises these reductions. `T5=0` selects ANDES's +low-pass output rather than the washout output. + +GridDyn currently provides local rotor speed, terminal voltage, mechanical +power, and machine electrical torque/power. Therefore local modes 0, 1, 3, 4, +and 5 are implemented and tested. ANDES mode 2 (bus-frequency), mode 6 +(voltage derivative), and nonzero `BUSR` require a general cross-bus +measurement-routing interface and are rejected rather than silently +substituted. Focused tests cover DYR mapping, initialization and perturbed +equations, analytic-Jacobian finite differences, limiter/gate roots, bypass +semantics, GENROU plus exciter coupling, and a load-step dynamic run. A +captured ANDES trajectory remains open. + Merge gate: a no-event IEEE 14 controller subset loads all five governors and all three stabilizers with no unsupported-record diagnostics; each controller also passes its independent minimized numerical comparison. Do not add the @@ -707,7 +737,8 @@ it also needs native-input mapping, initialization, and a trajectory test. | `ESST3A` | `ExciterESST3A`; exact GENROU path plus documented reduced-order synchronous-machine signal approximations. | Implemented | | `EXST1` | `ExciterEXST1`; positive-time-constant equations and DYR mapping, with the documented corrected regulator-output limiter; zero-time-constant blocks, native import, and a captured trajectory remain. | Implemented | | `IEEEX1`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | -| `IEEEST`, `ST2CUT` | Base `Stabilizer` is not a functional equivalent; implement both models and signal connections. | No direct analogue | +| `ST2CUT` | `StabilizerST2CUT`; local input modes, exact filters, limits, DYR mapping, and dynamic load-step coverage are implemented. Remote/frequency-derivative measurements and captured ANDES trajectories remain. | Partial | +| `IEEEST` | `StabilizerIEEEST`; local input modes, exact ANDES zero-bypass filters, limits, DYR mapping, and dynamic load-step coverage are implemented. Remote/frequency-derivative measurements and captured trajectories remain. | Partial | | `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | | `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | | `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | diff --git a/src/fileInput/gridDynReadDYR.cpp b/src/fileInput/gridDynReadDYR.cpp index 52a516133..ba903da05 100644 --- a/src/fileInput/gridDynReadDYR.cpp +++ b/src/fileInput/gridDynReadDYR.cpp @@ -20,6 +20,7 @@ #include "griddyn/Stabilizer.h" #include "griddyn/generators/DynamicGenerator.h" #include "griddyn/governors/GovernorIeeeG1.h" +#include "griddyn/stabilizers/StabilizerIEEEST.h" #include "griddyn/stabilizers/StabilizerST2CUT.h" #include #include @@ -35,6 +36,7 @@ namespace { void loadEXST1(CoreObject* parentObject, stringVec& tokens); void loadTGOV1(CoreObject* parentObject, stringVec& tokens); void loadIEEEG1(CoreObject* parentObject, stringVec& tokens); + void loadIEEEST(CoreObject* parentObject, stringVec& tokens); void loadST2CUT(CoreObject* parentObject, stringVec& tokens); void loadEXDC2(CoreObject* parentObject, stringVec& tokens); void loadSEXS(CoreObject* parentObject, stringVec& tokens); @@ -90,6 +92,8 @@ void loadDyr(CoreObject* parentObject, loadTGOV1(parentObject, lineTokens); } else if (type == "'IEEEG1'") { loadIEEEG1(parentObject, lineTokens); + } else if (type == "'IEEEST'") { + loadIEEEST(parentObject, lineTokens); } else if (type == "'ST2CUT'") { loadST2CUT(parentObject, lineTokens); } else if (type == "'SEXS'") { @@ -392,6 +396,9 @@ namespace { const int busId = std::stoi(tokens[0]); const auto* bus = static_cast(parentObject->findByUserID("bus", busId)); const int genId = std::stoi(tokens[2]); + if ((bus == nullptr) || (genId <= 0)) { + throw InvalidParameterValue("ST2CUT generator identity"); + } auto* generator = dynamic_cast(bus->getGen(genId - 1)); if (generator == nullptr) { throw InvalidParameterValue("ST2CUT requires a dynamic generator"); @@ -424,6 +431,49 @@ namespace { stabilizer->set("vcl", params[22]); generator->add(stabilizer); } + + void loadIEEEST(CoreObject* parentObject, stringVec& tokens) + { + if (tokens.size() != 22U) { + throw InvalidParameterValue("IEEEST DYR record must contain 22 fields"); + } + const int busId = std::stoi(tokens[0]); + const auto* bus = static_cast(parentObject->findByUserID("bus", busId)); + const int genId = std::stoi(tokens[2]); + if ((bus == nullptr) || (genId <= 0)) { + throw InvalidParameterValue("IEEEST generator identity"); + } + auto* generator = dynamic_cast(bus->getGen(genId - 1)); + if (generator == nullptr) { + throw InvalidParameterValue("IEEEST requires a dynamic generator"); + } + + const auto params = gmlc::utilities::str2vector(tokens, kNullVal); + auto* stabilizer = new stabilizers::StabilizerIEEEST(); + // Exact frozen ANDES psse-dyr.yaml order after BUS and ID: + // MODE, BUSR, A1, A2, A3, A4, A5, A6, T1, T2, T3, T4, + // T5, T6, KS, LSMAX, LSMIN, VCU, VCL. + stabilizer->set("mode", params[3]); + stabilizer->set("busr", params[4]); + stabilizer->set("a1", params[5]); + stabilizer->set("a2", params[6]); + stabilizer->set("a3", params[7]); + stabilizer->set("a4", params[8]); + stabilizer->set("a5", params[9]); + stabilizer->set("a6", params[10]); + stabilizer->set("t1", params[11]); + stabilizer->set("t2", params[12]); + stabilizer->set("t3", params[13]); + stabilizer->set("t4", params[14]); + stabilizer->set("t5", params[15]); + stabilizer->set("t6", params[16]); + stabilizer->set("ks", params[17]); + stabilizer->set("lsmax", params[18]); + stabilizer->set("lsmin", params[19]); + stabilizer->set("vcu", params[20]); + stabilizer->set("vcl", params[21]); + generator->add(stabilizer); + } } // namespace } // namespace griddyn diff --git a/src/griddyn/CMakeLists.txt b/src/griddyn/CMakeLists.txt index d2839003d..ab08cb715 100644 --- a/src/griddyn/CMakeLists.txt +++ b/src/griddyn/CMakeLists.txt @@ -81,6 +81,7 @@ set(gridSource_sources set(exciter_headers Exciter.h Stabilizer.h + stabilizers/StabilizerIEEEST.h stabilizers/StabilizerST2CUT.h exciters/ExciterDC1A.h exciters/ExciterDC2A.h @@ -102,6 +103,7 @@ set(exciter_sources exciters/ExciterIEEEtype2.cpp exciters/ExciterSEXS.cpp stabilizers/Stabilizer.cpp + stabilizers/StabilizerIEEEST.cpp stabilizers/StabilizerST2CUT.cpp ) diff --git a/src/griddyn/stabilizers/Stabilizer.cpp b/src/griddyn/stabilizers/Stabilizer.cpp index 367290193..152418a6a 100644 --- a/src/griddyn/stabilizers/Stabilizer.cpp +++ b/src/griddyn/stabilizers/Stabilizer.cpp @@ -8,6 +8,7 @@ #include "../Generator.h" #include "../GridBus.h" +#include "StabilizerIEEEST.h" #include "StabilizerST2CUT.h" #include "core/CoreObjectTemplates.hpp" #include "core/ObjectFactoryTemplates.hpp" @@ -20,6 +21,8 @@ static const TypeFactory STABILIZER_FACTORY("pss", std::to_array({"basic"})); static ChildTypeFactory gSt2cutFactory("pss", std::to_array({"st2cut"})); +static ChildTypeFactory + gIeeestFactory("pss", std::to_array({"ieeest"})); Stabilizer::Stabilizer(const std::string& objName): GridSubModel(objName), mp_Tw(0.0), mp_Teps(0.0), mp_Kw(0.0), mp_Kp(0.0), mp_Kv(0.0), diff --git a/src/griddyn/stabilizers/StabilizerIEEEST.cpp b/src/griddyn/stabilizers/StabilizerIEEEST.cpp new file mode 100644 index 000000000..0ed472beb --- /dev/null +++ b/src/griddyn/stabilizers/StabilizerIEEEST.cpp @@ -0,0 +1,689 @@ +/* + * Copyright (c) 2014-2026, Lawrence Livermore National Security + * See the top-level NOTICE for additional details. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include "StabilizerIEEEST.h" + +#include "core/CoreExceptions.h" +#include "core/CoreObjectTemplates.hpp" +#include "utilities/MatrixData.hpp" +#include +#include +#include +#include + +namespace griddyn::stabilizers { +namespace { + constexpr double inputValidityLimit = 1e20; + constexpr index_t maxDifferentialStates = 7; +} // namespace + +StabilizerIEEEST::StabilizerIEEEST(const std::string& objName): Stabilizer(objName) +{ + m_inputSize = pssInputCount; + m_outputSize = 1; +} + +CoreObject* StabilizerIEEEST::clone(CoreObject* obj) const +{ + auto* stabilizerClone = cloneBase(this, obj); + if (stabilizerClone == nullptr) { + return obj; + } + stabilizerClone->mode = mode; + stabilizerClone->remoteBus = remoteBus; + stabilizerClone->A1 = A1; + stabilizerClone->A2 = A2; + stabilizerClone->A3 = A3; + stabilizerClone->A4 = A4; + stabilizerClone->A5 = A5; + stabilizerClone->A6 = A6; + stabilizerClone->T1 = T1; + stabilizerClone->T2 = T2; + stabilizerClone->T3 = T3; + stabilizerClone->T4 = T4; + stabilizerClone->T5 = T5; + stabilizerClone->T6 = T6; + stabilizerClone->Ks = Ks; + stabilizerClone->Lsmax = Lsmax; + stabilizerClone->Lsmin = Lsmin; + stabilizerClone->Vcu = Vcu; + stabilizerClone->Vcl = Vcl; + return stabilizerClone; +} + +bool StabilizerIEEEST::supportedMode(int modeValue) +{ + return (modeValue == 0) || (modeValue == 1) || (modeValue == 3) || (modeValue == 4) || + (modeValue == 5); +} + +void StabilizerIEEEST::dynObjectInitializeA(CoreTime /*time0*/, std::uint32_t /*flags*/) +{ + const std::array parameters{ + A1, A2, A3, A4, A5, A6, T1, T2, T3, T4, T5, T6, Ks, Lsmax, Lsmin, Vcu, Vcl}; + if (!std::all_of(parameters.begin(), + parameters.end(), + [](double value) { return std::isfinite(value); }) || + !supportedMode(mode) || (remoteBus != 0) || (A1 < 0.0) || (A2 < 0.0) || (A4 < 0.0) || + (T1 < 0.0) || (T2 < 0.0) || (T3 < 0.0) || (T4 < 0.0) || (T5 < 0.0) || (T6 <= 0.0) || + (Lsmax < Lsmin) || (Vcu < Vcl)) { + throw InvalidParameterValue("IEEEST modes, time constants, or limits"); + } + + f1xState = kNullLocation; + f1yState = kNullLocation; + f2xState = kNullLocation; + f2yState = kNullLocation; + ll1State = kNullLocation; + ll2State = kNullLocation; + washoutState = kNullLocation; + index_t stateCount = 0; + if (A2 > 0.0) { + f1xState = stateCount++; + f1yState = stateCount++; + } else if (A1 > 0.0) { + f1yState = stateCount++; + } + if (A4 > 0.0) { + f2xState = stateCount++; + f2yState = stateCount++; + } + if (T2 > 0.0) { + ll1State = stateCount++; + } + if (T4 > 0.0) { + ll2State = stateCount++; + } + washoutState = stateCount++; + + offsets.local().local.algSize = 1; + offsets.local().local.diffSize = stateCount; + offsets.local().local.algRoots = 4; + offsets.local().local.jacSize = 8 * stateCount + 8; +} + +void StabilizerIEEEST::dynObjectInitializeB(const IOdata& inputs, + const IOdata& /*desiredOutput*/, + IOdata& /*fieldSet*/) +{ + if (inputs.size() < pssInputCount) { + throw InvalidParameterValue("IEEEST controller inputs"); + } + const auto validInput = [&inputs](index_t index) { + return std::isfinite(inputs[index]) && (std::abs(inputs[index]) < inputValidityLimit); + }; + const bool signalAvailable = (mode == 0) || ((mode == 1) && validInput(pssOmegaInLocation)) || + ((mode == 3) && validInput(pssElectricalPowerInLocation)) || + ((mode == 4) && validInput(pssPmechInLocation)) || + ((mode == 5) && validInput(pssVoltageInLocation)); + if (!validInput(pssVoltageInLocation) || !signalAvailable) { + throw InvalidParameterValue("IEEEST controller inputs"); + } + initialVoltage = inputs[pssVoltageInLocation]; + initialPmech = (mode == 4) ? inputs[pssPmechInLocation] : 0.0; + const double input = selectedInput(inputs); + double* state = m_state.data() + 1; + if (f1xState != kNullLocation) { + state[f1xState] = 0.0; + } + if (f1yState != kNullLocation) { + state[f1yState] = input; + } + const auto filterOne = f1Output(state, input); + if (f2xState != kNullLocation) { + state[f2xState] = 0.0; + state[f2yState] = filterOne.value; + } + const auto filterTwo = f2Output(state, filterOne); + if (ll1State != kNullLocation) { + state[ll1State] = filterTwo.value; + } + const auto firstLeadLag = leadLagOutput(state, filterTwo, ll1State, T1, T2); + if (ll2State != kNullLocation) { + state[ll2State] = firstLeadLag.value; + } + const auto secondLeadLag = leadLagOutput(state, firstLeadLag, ll2State, T3, T4); + state[washoutState] = Ks * secondLeadLag.value; + m_state[0] = 0.0; + std::fill(m_dstate_dt.begin(), m_dstate_dt.end(), 0.0); + updateLimitFlags(inputs, state); +} + +double StabilizerIEEEST::selectedInput(const IOdata& inputs) const +{ + switch (mode) { + case 0: + return 0.0; + case 1: + return inputs[pssOmegaInLocation] - 1.0; + case 3: + return inputs[pssElectricalPowerInLocation]; + case 4: + return inputs[pssPmechInLocation] - initialPmech; + case 5: + return inputs[pssVoltageInLocation]; + default: + return 0.0; + } +} + +StabilizerIEEEST::LinearValue StabilizerIEEEST::f1Output(const double state[], double input) const +{ + LinearValue output; + if (f1yState == kNullLocation) { + output.value = input; + output.inputGain = 1.0; + } else { + output.value = state[f1yState]; + output.stateGain[f1yState] = 1.0; + } + return output; +} + +StabilizerIEEEST::LinearValue StabilizerIEEEST::f2Output(const double state[], + const LinearValue& input) const +{ + if (f2xState == kNullLocation) { + return input; + } + const double scale = A6 / A4; + LinearValue output = input; + output.value = state[f2yState] + (A5 * state[f2xState]) + + (scale * (input.value - state[f2yState] - (A3 * state[f2xState]))); + for (index_t index = 0; index < maxDifferentialStates; ++index) { + output.stateGain[index] *= scale; + } + output.inputGain *= scale; + output.stateGain[f2xState] += A5 - (A3 * scale); + output.stateGain[f2yState] += 1.0 - scale; + return output; +} + +StabilizerIEEEST::LinearValue StabilizerIEEEST::leadLagOutput(const double state[], + const LinearValue& input, + index_t stateIndex, + double leadTime, + double lagTime) +{ + if (stateIndex == kNullLocation) { + return input; + } + const double scale = leadTime / lagTime; + LinearValue output = input; + output.value = state[stateIndex] + (scale * (input.value - state[stateIndex])); + for (index_t index = 0; index < maxDifferentialStates; ++index) { + output.stateGain[index] *= scale; + } + output.inputGain *= scale; + output.stateGain[stateIndex] += 1.0 - scale; + return output; +} + +StabilizerIEEEST::LinearValue StabilizerIEEEST::cascadeOutput(const double state[], + const IOdata& inputs) const +{ + LinearValue signal; + signal.value = selectedInput(inputs); + signal.inputGain = 1.0; + const auto filterOne = f1Output(state, signal.value); + const auto filterTwo = f2Output(state, filterOne); + const auto firstLeadLag = leadLagOutput(state, filterTwo, ll1State, T1, T2); + const auto secondLeadLag = leadLagOutput(state, firstLeadLag, ll2State, T3, T4); + LinearValue output = secondLeadLag; + const double input = Ks * secondLeadLag.value; + if (T5 <= 0.0) { + output.value = state[washoutState]; + output.stateGain.fill(0.0); + output.stateGain[washoutState] = 1.0; + output.inputGain = 0.0; + return output; + } + const double scale = T5 / T6; + output.value = scale * (input - state[washoutState]); + for (index_t index = 0; index < maxDifferentialStates; ++index) { + output.stateGain[index] *= Ks * scale; + } + output.inputGain *= Ks * scale; + output.stateGain[washoutState] -= scale; + return output; +} + +bool StabilizerIEEEST::voltageEnabled(const IOdata& inputs) const +{ + return (inputs[pssVoltageInLocation] >= initialVoltage + Vcl) && + (inputs[pssVoltageInLocation] <= initialVoltage + Vcu); +} + +int StabilizerIEEEST::outputLimitStatus(const double state[], const IOdata& inputs) const +{ + const double outputValue = cascadeOutput(state, inputs).value; + if (outputValue >= Lsmax) { + return 1; + } + return (outputValue <= Lsmin) ? -1 : 0; +} + +double StabilizerIEEEST::output(const double state[], const IOdata& inputs) const +{ + if (!voltageEnabled(inputs)) { + return 0.0; + } + return std::clamp(cascadeOutput(state, inputs).value, Lsmin, Lsmax); +} + +bool StabilizerIEEEST::updateLimitFlags(const IOdata& inputs, const double state[]) +{ + const int limitStatus = outputLimitStatus(state, inputs); + const bool gated = !voltageEnabled(inputs); + const bool changed = (opFlags[OUTPUT_LIMITED] != (limitStatus != 0)) || + (opFlags[OUTPUT_LIMIT_HIGH] != (limitStatus > 0)) || (opFlags[VOLTAGE_GATED] != gated); + opFlags.set(OUTPUT_LIMITED, limitStatus != 0); + opFlags.set(OUTPUT_LIMIT_HIGH, limitStatus > 0); + opFlags.set(VOLTAGE_GATED, gated); + return changed; +} + +void StabilizerIEEEST::residual(const IOdata& inputs, + const StateData& stateData, + double resid[], + const SolverMode& sMode) +{ + const auto locations = offsets.getLocations(stateData, resid, sMode, this); + if (hasAlgebraic(sMode)) { + locations.destLoc[0] = output(locations.diffStateLoc, inputs) - locations.algStateLoc[0]; + } + if (!hasDifferential(sMode)) { + return; + } + derivative(inputs, stateData, resid, sMode); + for (index_t index = 0; index < locations.diffSize; ++index) { + locations.destDiffLoc[index] -= locations.dstateLoc[index]; + } +} + +void StabilizerIEEEST::derivative(const IOdata& inputs, + const StateData& stateData, + double deriv[], + const SolverMode& sMode) +{ + if (!hasDifferential(sMode)) { + return; + } + const auto locations = offsets.getLocations(stateData, deriv, sMode, this); + const double* state = locations.diffStateLoc; + double* stateDerivative = locations.destDiffLoc; + const double input = selectedInput(inputs); + const auto filterOne = f1Output(state, input); + if (f1xState != kNullLocation) { + stateDerivative[f1xState] = (input - state[f1yState] - (A1 * state[f1xState])) / A2; + stateDerivative[f1yState] = state[f1xState]; + } else if (f1yState != kNullLocation) { + stateDerivative[f1yState] = (input - state[f1yState]) / A1; + } + const auto filterTwo = f2Output(state, filterOne); + if (f2xState != kNullLocation) { + stateDerivative[f2xState] = + (filterOne.value - state[f2yState] - (A3 * state[f2xState])) / A4; + stateDerivative[f2yState] = state[f2xState]; + } + const auto firstLeadLag = leadLagOutput(state, filterTwo, ll1State, T1, T2); + if (ll1State != kNullLocation) { + stateDerivative[ll1State] = (filterTwo.value - state[ll1State]) / T2; + } + const auto secondLeadLag = leadLagOutput(state, firstLeadLag, ll2State, T3, T4); + if (ll2State != kNullLocation) { + stateDerivative[ll2State] = (firstLeadLag.value - state[ll2State]) / T4; + } + stateDerivative[washoutState] = ((Ks * secondLeadLag.value) - state[washoutState]) / T6; +} + +void StabilizerIEEEST::jacobianElements(const IOdata& inputs, + const StateData& stateData, + MatrixData& matrixData, + const IOlocs& inputLocs, + const SolverMode& sMode) +{ + const auto locations = offsets.getLocations(stateData, sMode, this); + const index_t refAlg = locations.algOffset; + const index_t refDiff = locations.diffOffset; + const double* state = locations.diffStateLoc; + const auto addInput = [&matrixData, &inputLocs, this](index_t row, double value) { + if (mode == 1) { + matrixData.assignCheckCol(row, inputLocs[pssOmegaInLocation], value); + } else if (mode == 3) { + matrixData.assignCheckCol(row, inputLocs[pssElectricalPowerInLocation], value); + } else if (mode == 4) { + matrixData.assignCheckCol(row, inputLocs[pssPmechInLocation], value); + } else if (mode == 5) { + matrixData.assignCheckCol(row, inputLocs[pssVoltageInLocation], value); + } + }; + const auto addLinear = + [&matrixData, &addInput, refDiff](index_t row, const LinearValue& value, double scale) { + for (index_t index = 0; index < maxDifferentialStates; ++index) { + if (value.stateGain[index] != 0.0) { + matrixData.assign(row, refDiff + index, scale * value.stateGain[index]); + } + } + if (value.inputGain != 0.0) { + addInput(row, scale * value.inputGain); + } + }; + + const auto outputValue = cascadeOutput(state, inputs); + if (hasAlgebraic(sMode)) { + matrixData.assign(refAlg, refAlg, -1.0); + if (voltageEnabled(inputs) && (outputLimitStatus(state, inputs) == 0)) { + addLinear(refAlg, outputValue, 1.0); + } + } + if (!hasDifferential(sMode)) { + return; + } + + LinearValue signal; + signal.value = selectedInput(inputs); + signal.inputGain = 1.0; + const auto filterOne = f1Output(state, signal.value); + if (f1xState != kNullLocation) { + addLinear(refDiff + f1xState, signal, 1.0 / A2); + matrixData.assign(refDiff + f1xState, refDiff + f1xState, ((-A1 / A2) - stateData.cj)); + matrixData.assign(refDiff + f1xState, refDiff + f1yState, -1.0 / A2); + matrixData.assign(refDiff + f1yState, refDiff + f1xState, 1.0); + matrixData.assign(refDiff + f1yState, refDiff + f1yState, -stateData.cj); + } else if (f1yState != kNullLocation) { + addLinear(refDiff + f1yState, signal, 1.0 / A1); + matrixData.assign(refDiff + f1yState, refDiff + f1yState, ((-1.0 / A1) - stateData.cj)); + } + + const auto filterTwo = f2Output(state, filterOne); + if (f2xState != kNullLocation) { + addLinear(refDiff + f2xState, filterOne, 1.0 / A4); + matrixData.assign(refDiff + f2xState, refDiff + f2xState, ((-A3 / A4) - stateData.cj)); + matrixData.assign(refDiff + f2xState, refDiff + f2yState, -1.0 / A4); + matrixData.assign(refDiff + f2yState, refDiff + f2xState, 1.0); + matrixData.assign(refDiff + f2yState, refDiff + f2yState, -stateData.cj); + } + const auto firstLeadLag = leadLagOutput(state, filterTwo, ll1State, T1, T2); + if (ll1State != kNullLocation) { + addLinear(refDiff + ll1State, filterTwo, 1.0 / T2); + matrixData.assign(refDiff + ll1State, refDiff + ll1State, ((-1.0 / T2) - stateData.cj)); + } + const auto secondLeadLag = leadLagOutput(state, firstLeadLag, ll2State, T3, T4); + if (ll2State != kNullLocation) { + addLinear(refDiff + ll2State, firstLeadLag, 1.0 / T4); + matrixData.assign(refDiff + ll2State, refDiff + ll2State, ((-1.0 / T4) - stateData.cj)); + } + addLinear(refDiff + washoutState, secondLeadLag, Ks / T6); + matrixData.assign(refDiff + washoutState, refDiff + washoutState, ((-1.0 / T6) - stateData.cj)); +} + +void StabilizerIEEEST::timestep(CoreTime time, const IOdata& inputs, const SolverMode& /*sMode*/) +{ + derivative(inputs, emptyStateData, m_dstate_dt.data(), cLocalSolverMode); + const double timeStep = time - prevTime; + for (index_t index = 0; index < offsets.local().local.diffSize; ++index) { + m_state[index + 1] += timeStep * m_dstate_dt[index + 1]; + } + m_state[0] = output(m_state.data() + 1, inputs); + updateLimitFlags(inputs, m_state.data() + 1); + prevTime = time; +} + +void StabilizerIEEEST::rootTest(const IOdata& inputs, + const StateData& stateData, + double roots[], + const SolverMode& sMode) +{ + const auto locations = offsets.getLocations(stateData, sMode, this); + const index_t rootOffset = offsets.getRootOffset(sMode); + const double unlimited = cascadeOutput(locations.diffStateLoc, inputs).value; + roots[rootOffset] = Lsmax - unlimited; + roots[rootOffset + 1] = unlimited - Lsmin; + roots[rootOffset + 2] = inputs[pssVoltageInLocation] - (initialVoltage + Vcl); + roots[rootOffset + 3] = (initialVoltage + Vcu) - inputs[pssVoltageInLocation]; +} + +void StabilizerIEEEST::rootTrigger(CoreTime /*time*/, + const IOdata& inputs, + const std::vector& rootMask, + const SolverMode& sMode) +{ + const index_t rootOffset = offsets.getRootOffset(sMode); + if ((rootMask[rootOffset] == 0) && (rootMask[rootOffset + 1] == 0) && + (rootMask[rootOffset + 2] == 0) && (rootMask[rootOffset + 3] == 0)) { + return; + } + if (updateLimitFlags(inputs, m_state.data() + 1)) { + alert(this, JAC_COUNT_CHANGE); + } +} + +ChangeCode StabilizerIEEEST::rootCheck(const IOdata& inputs, + const StateData& /*stateData*/, + const SolverMode& /*sMode*/, + CheckLevel /*level*/) +{ + if (updateLimitFlags(inputs, m_state.data() + 1)) { + alert(this, JAC_COUNT_CHANGE); + return ChangeCode::JACOBIAN_CHANGE; + } + return ChangeCode::NO_CHANGE; +} + +stringVec StabilizerIEEEST::localStateNames() const +{ + stringVec names{"vs"}; + if (f1xState != kNullLocation) { + names.emplace_back("f1x"); + } + if (f1yState != kNullLocation) { + names.emplace_back("f1y"); + } + if (f2xState != kNullLocation) { + names.emplace_back("f2x"); + names.emplace_back("f2y"); + } + if (ll1State != kNullLocation) { + names.emplace_back("ll1"); + } + if (ll2State != kNullLocation) { + names.emplace_back("ll2"); + } + names.emplace_back("wo"); + return names; +} + +index_t StabilizerIEEEST::findIndex(std::string_view field, const SolverMode& sMode) const +{ + if ((field == "vss") || (field == "vs")) { + return getOutputLoc(sMode, 0); + } + return kInvalidLocation; +} + +void StabilizerIEEEST::set(std::string_view param, std::string_view val) +{ + Stabilizer::set(param, val); +} + +void StabilizerIEEEST::set(std::string_view param, double val, units::unit unitType) +{ + const auto finite = [val](const char* parameterName) { + if (!std::isfinite(val)) { + throw InvalidParameterValue(std::string("IEEEST ") + parameterName + " must be finite"); + } + }; + if (param == "mode") { + if (!std::isfinite(val) || (std::floor(val) != val) || + !supportedMode(static_cast(val))) { + throw InvalidParameterValue("IEEEST MODE is unsupported"); + } + mode = static_cast(val); + } else if (param == "busr") { + if (!std::isfinite(val) || (std::floor(val) != val) || (val != 0.0)) { + throw InvalidParameterValue("IEEEST remote BUSR is unsupported"); + } + remoteBus = 0; + } else if (param == "a1") { + finite("A1"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST A1 must be nonnegative"); + } + A1 = val; + } else if (param == "a2") { + finite("A2"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST A2 must be nonnegative"); + } + A2 = val; + } else if (param == "a3") { + finite("A3"); + A3 = val; + } else if (param == "a4") { + finite("A4"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST A4 must be nonnegative"); + } + A4 = val; + } else if (param == "a5") { + finite("A5"); + A5 = val; + } else if (param == "a6") { + finite("A6"); + A6 = val; + } else if (param == "t1") { + finite("T1"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST T1 must be nonnegative"); + } + T1 = val; + } else if (param == "t2") { + finite("T2"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST T2 must be nonnegative"); + } + T2 = val; + } else if (param == "t3") { + finite("T3"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST T3 must be nonnegative"); + } + T3 = val; + } else if (param == "t4") { + finite("T4"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST T4 must be nonnegative"); + } + T4 = val; + } else if (param == "t5") { + finite("T5"); + if (val < 0.0) { + throw InvalidParameterValue("IEEEST T5 must be nonnegative"); + } + T5 = val; + } else if (param == "t6") { + finite("T6"); + if (val <= 0.0) { + throw InvalidParameterValue("IEEEST T6 must be positive"); + } + T6 = val; + } else if (param == "ks") { + finite("KS"); + Ks = val; + } else if ((param == "lsmax") || (param == "vmax")) { + finite("LSMAX"); + if (val < Lsmin) { + throw InvalidParameterValue("IEEEST LSMAX must not be less than LSMIN"); + } + Lsmax = val; + } else if ((param == "lsmin") || (param == "vmin")) { + finite("LSMIN"); + if (val > Lsmax) { + throw InvalidParameterValue("IEEEST LSMIN must not exceed LSMAX"); + } + Lsmin = val; + } else if (param == "vcu") { + finite("VCU"); + const double mappedValue = (val == 0.0) ? 999.0 : val; + if (mappedValue < Vcl) { + throw InvalidParameterValue("IEEEST VCU must not be less than VCL"); + } + Vcu = mappedValue; + } else if (param == "vcl") { + finite("VCL"); + const double mappedValue = (val == 0.0) ? -999.0 : val; + if (mappedValue > Vcu) { + throw InvalidParameterValue("IEEEST VCL must not exceed VCU"); + } + Vcl = mappedValue; + } else { + Stabilizer::set(param, val, unitType); + } +} + +double StabilizerIEEEST::get(std::string_view param, units::unit unitType) const +{ + if (param == "mode") { + return mode; + } + if (param == "busr") { + return remoteBus; + } + if (param == "a1") { + return A1; + } + if (param == "a2") { + return A2; + } + if (param == "a3") { + return A3; + } + if (param == "a4") { + return A4; + } + if (param == "a5") { + return A5; + } + if (param == "a6") { + return A6; + } + if (param == "t1") { + return T1; + } + if (param == "t2") { + return T2; + } + if (param == "t3") { + return T3; + } + if (param == "t4") { + return T4; + } + if (param == "t5") { + return T5; + } + if (param == "t6") { + return T6; + } + if (param == "ks") { + return Ks; + } + if ((param == "lsmax") || (param == "vmax")) { + return Lsmax; + } + if ((param == "lsmin") || (param == "vmin")) { + return Lsmin; + } + if (param == "vcu") { + return Vcu; + } + if (param == "vcl") { + return Vcl; + } + return Stabilizer::get(param, unitType); +} +} // namespace griddyn::stabilizers diff --git a/src/griddyn/stabilizers/StabilizerIEEEST.h b/src/griddyn/stabilizers/StabilizerIEEEST.h new file mode 100644 index 000000000..f50f841b4 --- /dev/null +++ b/src/griddyn/stabilizers/StabilizerIEEEST.h @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2014-2026, Lawrence Livermore National Security + * See the top-level NOTICE for additional details. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ + +#pragma once + +#include "../Stabilizer.h" +#include +#include +#include + +namespace griddyn::stabilizers { +/** + * @brief PSS/E IEEEST single-input power-system stabilizer. + * + * Implements frozen ANDES v2.0.0 `andes/models/pss/ieeest.py`, using the + * PSS/E DYR schema in `andes/io/psse-dyr.yaml`. The selected input @f$u@f$ + * passes through the following transfer-function cascade: + * + * @f[ + * F_1(s)=\frac{1}{1+A_1s+A_2s^2},\quad + * F_2(s)=\frac{1+A_5s+A_6s^2}{1+A_3s+A_4s^2}, + * @f] + * @f[ + * L_1(s)=\frac{1+T_1s}{1+T_2s},\quad + * L_2(s)=\frac{1+T_3s}{1+T_4s},\quad + * W(s)=\frac{K_ST_5s}{1+T_6s}. + * @f] + * + * The final output is @f$V_{SS}=\operatorname{limit}_{[LSMIN,LSMAX]}(W)@f$ + * when @f$V_{t0}+VCL\leq V_t\leq V_{t0}+VCU@f$, and zero otherwise. The + * limiter is purely an output hard limit: it does not apply anti-windup to + * the filter states. + * + * For positive second-order denominators, the realization follows ANDES + * exactly. For example, @f$F_1@f$ uses + * @f[ + * A_2\dot{x}_{F1}=u-y_{F1}-A_1x_{F1},\qquad \dot{y}_{F1}=x_{F1}, + * @f] + * and @f$F_2@f$ uses + * @f[ + * A_4\dot{x}_{F2}=u-y_{F2}-A_3x_{F2},\quad \dot{y}_{F2}=x_{F2}, + * @f] + * @f[ + * F_2=y_{F2}+A_5x_{F2}+\frac{A_6}{A_4} + * (u-y_{F2}-A_3x_{F2}). + * @f] + * Each active first-order lead-lag is realized as + * @f$T_b\dot{x}=u-x@f$ and + * @f$y=x+(T_a/T_b)(u-x)@f$. The washout state satisfies + * @f$T_6\dot{x}_W=K_SL_2-x_W@f$ with output + * @f$W=T_5(K_SL_2-x_W)/T_6@f$; if @f$T_5=0@f$, frozen ANDES selects the + * low-pass output @f$W=x_W@f$. + * + * Frozen ANDES `zero_out` semantics are retained: @f$A_4\leq0@f$ bypasses + * @f$F_2@f$, @f$T_2\leq0@f$ bypasses @f$L_1@f$, and @f$T_4\leq0@f$ + * bypasses @f$L_2@f$. @f$A_2=0@f$ reduces @f$F_1@f$ to its first-order form + * when @f$A_1>0@f$, or bypasses it when @f$A_1=0@f$. These reductions avoid + * allocating singular dummy states while preserving the same transfer + * functions and initialized outputs. + * + * @par References + * - ANDES v2.0.0 `andes/models/pss/ieeest.py` and `andes/core/block.py` + * (`Lag2ndOrd`, `LeadLag2ndOrd`, `LeadLag`, and `WashoutOrLag`). + * - ANDES v2.0.0 `andes/io/psse-dyr.yaml`, IEEEST schema. + * - Siemens PTI, PSS/E Model Library, IEEEST model. + * + * GridDyn currently supports local MODE values 0, 1, 3, 4, and 5: disabled, + * rotor-speed deviation, electrical power, accelerating power, and terminal + * voltage. MODE 2 (bus frequency), MODE 6 (voltage derivative), and nonzero + * BUSR require measurement routing not yet available in GridDyn and are + * rejected rather than approximated. + */ +class StabilizerIEEEST: public Stabilizer { + public: + enum IEEESTFlags { + OUTPUT_LIMITED = OBJECT_FLAG5, + OUTPUT_LIMIT_HIGH = OBJECT_FLAG6, + VOLTAGE_GATED = OBJECT_FLAG7, + }; + + protected: + int mode = 1; + int remoteBus = 0; + double A1 = 1.0; + double A2 = 1.0; + double A3 = 1.0; + double A4 = 1.0; + double A5 = 1.0; + double A6 = 1.0; + double T1 = 1.0; + double T2 = 1.0; + double T3 = 1.0; + double T4 = 1.0; + double T5 = 1.0; + double T6 = 1.0; + double Ks = 1.0; + double Lsmax = 0.3; + double Lsmin = -0.3; + double Vcu = 999.0; + double Vcl = -999.0; + double initialVoltage = 1.0; + double initialPmech = 0.0; + + index_t f1xState = kNullLocation; + index_t f1yState = kNullLocation; + index_t f2xState = kNullLocation; + index_t f2yState = kNullLocation; + index_t ll1State = kNullLocation; + index_t ll2State = kNullLocation; + index_t washoutState = kNullLocation; + + public: + explicit StabilizerIEEEST(const std::string& objName = "pssIEEEST_#"); + CoreObject* clone(CoreObject* obj = nullptr) const override; + + void dynObjectInitializeA(CoreTime time0, std::uint32_t flags) override; + void dynObjectInitializeB(const IOdata& inputs, + const IOdata& desiredOutput, + IOdata& fieldSet) override; + void set(std::string_view param, std::string_view val) override; + void set(std::string_view param, double val, units::unit unitType = units::defunit) override; + double get(std::string_view param, units::unit unitType = units::defunit) const override; + stringVec localStateNames() const override; + index_t findIndex(std::string_view field, const SolverMode& sMode) const override; + void residual(const IOdata& inputs, + const StateData& stateData, + double resid[], + const SolverMode& sMode) override; + void derivative(const IOdata& inputs, + const StateData& stateData, + double deriv[], + const SolverMode& sMode) override; + void jacobianElements(const IOdata& inputs, + const StateData& stateData, + MatrixData& matrixData, + const IOlocs& inputLocs, + const SolverMode& sMode) override; + void timestep(CoreTime time, const IOdata& inputs, const SolverMode& sMode) override; + void rootTest(const IOdata& inputs, + const StateData& stateData, + double roots[], + const SolverMode& sMode) override; + void rootTrigger(CoreTime time, + const IOdata& inputs, + const std::vector& rootMask, + const SolverMode& sMode) override; + ChangeCode rootCheck(const IOdata& inputs, + const StateData& stateData, + const SolverMode& sMode, + CheckLevel level) override; + + private: + struct LinearValue { + double value = 0.0; + std::array stateGain{}; + double inputGain = 0.0; + }; + + [[nodiscard]] double selectedInput(const IOdata& inputs) const; + [[nodiscard]] LinearValue f1Output(const double state[], double input) const; + [[nodiscard]] LinearValue f2Output(const double state[], const LinearValue& input) const; + [[nodiscard]] static LinearValue leadLagOutput(const double state[], + const LinearValue& input, + index_t stateIndex, + double leadTime, + double lagTime); + [[nodiscard]] LinearValue cascadeOutput(const double state[], const IOdata& inputs) const; + [[nodiscard]] bool voltageEnabled(const IOdata& inputs) const; + [[nodiscard]] int outputLimitStatus(const double state[], const IOdata& inputs) const; + [[nodiscard]] double output(const double state[], const IOdata& inputs) const; + bool updateLimitFlags(const IOdata& inputs, const double state[]); + static bool supportedMode(int modeValue); +}; +} // namespace griddyn::stabilizers diff --git a/test/andesTests/testAndesDyrReader.cpp b/test/andesTests/testAndesDyrReader.cpp index fd7d3ce65..2b9a294a7 100644 --- a/test/andesTests/testAndesDyrReader.cpp +++ b/test/andesTests/testAndesDyrReader.cpp @@ -17,6 +17,7 @@ #include "griddyn/genmodels/GenModelGENROU.h" #include "griddyn/governors/GovernorIeeeG1.h" #include "griddyn/governors/GovernorTgov1.h" +#include "griddyn/stabilizers/StabilizerIEEEST.h" #include "griddyn/stabilizers/StabilizerST2CUT.h" #include #include @@ -391,6 +392,51 @@ TEST(AndesDyrReaderTests, MapsSt2cutParametersAndCouplesToExciters) EXPECT_EQ(runJacobianCheck(simulation, griddyn::cDaeSolverMode, false), 0); } +TEST(AndesDyrReaderTests, MapsIeeestParametersAndCouplesToExciter) +{ + auto simulation = std::make_unique(); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14.raw")); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_genrou.dyr")); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_esst3a.dyr")); + griddyn::loadFile(simulation.get(), makeAndesTestPath("ieee14_ieeest.dyr")); + + auto* bus = dynamic_cast(simulation->findByUserID("bus", 3)); + ASSERT_NE(bus, nullptr); + auto* generator = dynamic_cast(bus->getGen(0)); + ASSERT_NE(generator, nullptr); + ASSERT_NE(dynamic_cast(generator->find("exciter")), nullptr); + auto* stabilizer = + dynamic_cast(generator->find("pss")); + ASSERT_NE(stabilizer, nullptr); + + const std::pair expected[]{{"mode", 3.0}, + {"busr", 0.0}, + {"a1", 0.0}, + {"a2", 0.0}, + {"a3", 0.0}, + {"a4", 0.0}, + {"a5", 0.0}, + {"a6", 0.0}, + {"t1", 0.0}, + {"t2", 0.0}, + {"t3", 0.0}, + {"t4", 0.75}, + {"t5", 1.0}, + {"t6", 4.2}, + {"ks", -2.0}, + {"lsmax", 0.1}, + {"lsmin", -0.1}, + {"vcu", 999.0}, + {"vcl", -999.0}}; + for (const auto& parameter : expected) { + EXPECT_DOUBLE_EQ(stabilizer->get(parameter.first), parameter.second) << parameter.first; + } + + ASSERT_EQ(simulation->dynInitialize(), 0); + EXPECT_EQ(runResidualCheck(simulation, griddyn::cDaeSolverMode, false), 0); + EXPECT_EQ(runJacobianCheck(simulation, griddyn::cDaeSolverMode, false), 0); +} + TEST(AndesDynamicTests, Tgov1RespondsToLoadStep) { const auto finalState = runLoadStepCase({"ieee14_tgov1.dyr"}); @@ -422,6 +468,13 @@ TEST(AndesDynamicTests, St2cutRespondsToLoadStep) EXPECT_FALSE(finalState.empty()); } +TEST(AndesDynamicTests, IeeestRespondsToLoadStep) +{ + const auto finalState = + runLoadStepCase({"ieee14_esst3a.dyr", "ieee14_exst1.dyr", "ieee14_ieeest.dyr"}); + EXPECT_FALSE(finalState.empty()); +} + TEST(AndesDynamicTests, CombinedControllersRespondToLoadStep) { const auto finalState = runLoadStepCase( diff --git a/test/componentTests/testStabilizers.cpp b/test/componentTests/testStabilizers.cpp index 94979af5b..d1e04c53a 100644 --- a/test/componentTests/testStabilizers.cpp +++ b/test/componentTests/testStabilizers.cpp @@ -8,6 +8,7 @@ #include "core/ObjectFactory.hpp" #include "griddyn/Stabilizer.h" #include "griddyn/generators/DynamicGenerator.h" +#include "griddyn/stabilizers/StabilizerIEEEST.h" #include "griddyn/stabilizers/StabilizerST2CUT.h" #include #include @@ -40,8 +41,141 @@ void configureSt2cut(stabilizers::StabilizerST2CUT& stabilizer) stabilizer.set("vcu", 0.1); stabilizer.set("vcl", -0.1); } + +void configureIeeest(stabilizers::StabilizerIEEEST& stabilizer) +{ + stabilizer.set("mode", 1.0); + stabilizer.set("busr", 0.0); + stabilizer.set("a1", 0.2); + stabilizer.set("a2", 0.1); + stabilizer.set("a3", 0.3); + stabilizer.set("a4", 0.2); + stabilizer.set("a5", 0.05); + stabilizer.set("a6", 0.02); + stabilizer.set("t1", 0.1); + stabilizer.set("t2", 0.2); + stabilizer.set("t3", 0.15); + stabilizer.set("t4", 0.3); + stabilizer.set("t5", 0.6); + stabilizer.set("t6", 1.2); + stabilizer.set("ks", 2.0); + stabilizer.set("lsmax", 0.05); + stabilizer.set("lsmin", -0.05); + stabilizer.set("vcu", 0.1); + stabilizer.set("vcl", -0.1); +} } // namespace +TEST(StabilizerModelTests, IeeestMatchesAndesInitializationAndPerturbedEquations) +{ + stabilizers::StabilizerIEEEST stabilizer; + configureIeeest(stabilizer); + stabilizer.dynInitializeA(0.0, 0); + IOdata inputs{1.0, 1.0, 0.8, 0.7}; + IOdata fieldSet; + stabilizer.dynInitializeB(inputs, {0.0}, fieldSet); + + const auto& initialized = stabilizer.getStates(); + ASSERT_EQ(initialized.size(), 8U); + for (const auto value : initialized) { + EXPECT_NEAR(value, 0.0, 1e-14); + } + + // State order is [VSS, F1_x, F1_y, F2_x, F2_y, LL1_x, LL2_x, WO_x]. + // These are direct evaluations of frozen ANDES IEEEST equations. + std::vector state{0.0, 0.02, -0.01, 0.03, -0.02, 0.04, -0.03, 0.05}; + std::vector stateDerivative(state.size(), 0.0); + stabilizer.setState(0.0, state.data(), stateDerivative.data(), cLocalSolverMode); + inputs[pssOmegaInLocation] = 1.01; + + std::vector derivative(state.size(), 0.0); + stabilizer.derivative(inputs, emptyStateData, derivative.data(), cLocalSolverMode); + EXPECT_NEAR(derivative[1], 0.16, 1e-14); + EXPECT_NEAR(derivative[2], 0.02, 1e-14); + EXPECT_NEAR(derivative[3], 0.005, 1e-14); + EXPECT_NEAR(derivative[4], 0.03, 1e-14); + EXPECT_NEAR(derivative[5], -0.292, 1e-14); + EXPECT_NEAR(derivative[6], 0.136, 1e-14); + EXPECT_NEAR(derivative[7], -0.057666666666666665, 1e-14); + + std::vector residual(state.size(), 0.0); + stabilizer.residual(inputs, emptyStateData, residual.data(), cLocalSolverMode); + EXPECT_NEAR(residual[0], -0.0346, 1e-14); + for (std::size_t index = 1; index < residual.size(); ++index) { + EXPECT_NEAR(residual[index], derivative[index], 1e-14) << index; + } +} + +TEST(StabilizerModelTests, IeeestLimitsVoltageGateAndRetainsAndesZeroBypasses) +{ + stabilizers::StabilizerIEEEST stabilizer; + configureIeeest(stabilizer); + stabilizer.set("lsmax", 0.03); + stabilizer.set("lsmin", -0.03); + stabilizer.dynInitializeA(0.0, 0); + stabilizer.setRootOffset(0, cLocalSolverMode); + IOdata inputs{1.01, 1.0, 0.8, 0.7}; + IOdata fieldSet; + stabilizer.dynInitializeB(inputs, {0.0}, fieldSet); + std::vector state{0.0, 0.02, -0.01, 0.03, -0.02, 0.04, -0.03, 0.05}; + std::vector stateDerivative(state.size(), 0.0); + stabilizer.setState(0.0, state.data(), stateDerivative.data(), cLocalSolverMode); + + std::vector residual(state.size(), 0.0); + stabilizer.residual(inputs, emptyStateData, residual.data(), cLocalSolverMode); + EXPECT_NEAR(residual[0], -0.03, 1e-14); + std::vector roots(4, 0.0); + stabilizer.rootTest(inputs, emptyStateData, roots.data(), cLocalSolverMode); + EXPECT_LT(roots[1], 0.0); + stabilizer.rootTrigger(0.0, inputs, {0, 1, 0, 0}, cLocalSolverMode); + EXPECT_TRUE(stabilizer.checkFlag(stabilizers::StabilizerIEEEST::OUTPUT_LIMITED)); + EXPECT_FALSE(stabilizer.checkFlag(stabilizers::StabilizerIEEEST::OUTPUT_LIMIT_HIGH)); + + inputs[pssVoltageInLocation] = 1.2; + stabilizer.residual(inputs, emptyStateData, residual.data(), cLocalSolverMode); + EXPECT_NEAR(residual[0], 0.0, 1e-14); + stabilizer.rootTrigger(0.0, inputs, {0, 0, 0, 1}, cLocalSolverMode); + EXPECT_TRUE(stabilizer.checkFlag(stabilizers::StabilizerIEEEST::VOLTAGE_GATED)); + + stabilizers::StabilizerIEEEST bypass; + bypass.set("mode", 3.0); + bypass.set("a1", 0.0); + bypass.set("a2", 0.0); + bypass.set("a4", 0.0); + bypass.set("t2", 0.0); + bypass.set("t3", 0.0); + bypass.set("t4", 0.75); + bypass.set("t5", 1.0); + bypass.set("t6", 4.2); + bypass.set("ks", -2.0); + bypass.dynInitializeA(0.0, 0); + IOdata bypassFields; + bypass.dynInitializeB({1.0, 1.0, 0.8, 0.7}, {0.0}, bypassFields); + EXPECT_EQ(bypass.getStates().size(), 3U); + EXPECT_NEAR(bypass.getStates()[1], 0.7, 1e-14); + EXPECT_NEAR(bypass.getStates()[2], -1.4, 1e-14); + + EXPECT_ANY_THROW(stabilizer.set("mode", 2.0)); + EXPECT_ANY_THROW(stabilizer.set("busr", 9.0)); + EXPECT_ANY_THROW(stabilizer.set("t6", 0.0)); +} + +TEST(StabilizerModelTests, IeeestFactoryCloneAndParameterValidation) +{ + auto factory = CoreObjectFactory::instance(); + std::unique_ptr object(factory->createObject("pss", "ieeest")); + auto* stabilizer = dynamic_cast(object.get()); + ASSERT_NE(stabilizer, nullptr); + stabilizer->set("ks", 2.5); + std::unique_ptr clonedObject(stabilizer->clone()); + auto* clone = dynamic_cast(clonedObject.get()); + ASSERT_NE(clone, nullptr); + EXPECT_DOUBLE_EQ(clone->get("ks"), 2.5); + + EXPECT_ANY_THROW(stabilizer->set("a1", -0.1)); + EXPECT_ANY_THROW(stabilizer->set("lsmin", 0.4)); +} + TEST(StabilizerModelTests, St2cutMatchesAndesInitializationAndPerturbedEquations) { stabilizers::StabilizerST2CUT stabilizer; @@ -148,3 +282,19 @@ TEST_F(StabilizerTests, St2cutAnalyticJacobianMatchesFiniteDifferencesWhenAttach EXPECT_EQ(runResidualCheck(gds, cDaeSolverMode, false), 0); EXPECT_EQ(runJacobianCheck(gds, cDaeSolverMode, false), 0); } + +TEST_F(StabilizerTests, IeeestAnalyticJacobianMatchesFiniteDifferencesWhenAttached) +{ + gds = readSimXMLFile(std::string(GRIDDYN_TEST_DIRECTORY "/genmodel_tests/test_model1.xml")); + auto* generator = dynamic_cast(gds->getGen(0)); + ASSERT_NE(generator, nullptr); + auto* stabilizer = new stabilizers::StabilizerIEEEST(); + configureIeeest(*stabilizer); + stabilizer->set("lsmax", 2.0); + stabilizer->set("lsmin", -2.0); + generator->add(stabilizer); + + ASSERT_EQ(gds->dynInitialize(), 0); + EXPECT_EQ(runResidualCheck(gds, cDaeSolverMode, false), 0); + EXPECT_EQ(runJacobianCheck(gds, cDaeSolverMode, false), 0); +} diff --git a/test/test_files/andes_tests/ieee14_ieeest.dyr b/test/test_files/andes_tests/ieee14_ieeest.dyr new file mode 100644 index 000000000..fff2cf7a2 --- /dev/null +++ b/test/test_files/andes_tests/ieee14_ieeest.dyr @@ -0,0 +1,2 @@ + 3 'IEEEST' 1 3 0 0.0000 0.0000 0.0000 0.0000 0.0000 0.0000 + 0.0000 0.0000 0.0000 0.75000 1.0000 4.2000 -2.0000 0.10000 -0.10000 0.0000 0.0000 / From 05f1c5d75a224e5d037c71e96f4cda7019bc2d6f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:31:45 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/developer-guide/andes-compatibility.md | 66 ++++++++++----------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/developer-guide/andes-compatibility.md b/docs/developer-guide/andes-compatibility.md index 04a3351c8..4a1d7691f 100644 --- a/docs/developer-guide/andes-compatibility.md +++ b/docs/developer-guide/andes-compatibility.md @@ -85,7 +85,7 @@ adapters still have attachment, parameter, equation, and validation gaps. | `GAST` | `GAST` | None exact | **No direct analogue.** Add a gas-turbine governor implementation and DYR adapter. | | `GGOV1` | `TGOV1` | `GovernorTgov1` | **Planned compatibility approximation.** ANDES currently retains only `R` when converting this record. If reproduced, emit a diagnostic and keep exact GGOV1 support as a separate task. | | `ST2CUT` | `ST2CUT` | `StabilizerST2CUT` | **Partial.** Exact frozen-ANDES local modes 0/1/3/4/5, dual transducers, washout/lag, three lead-lag stages, output limits, voltage gating, DYR mapping, generator/exciter coupling, and dynamic load-step coverage are implemented. Remote `BUSR` inputs and ANDES modes 2/6 require cross-bus/frequency-derivative measurement routing and are rejected rather than approximated. | -| `IEEEST` | `IEEEST` | `StabilizerIEEEST` | **Partial.** Exact frozen-ANDES local modes 0/1/3/4/5, filter/lead-lag/washout zero-bypass semantics, output limits, voltage gate, DYR mapping, and GENROU/exciter coupling are implemented. Modes 2/6 and nonzero `BUSR` require unavailable cross-bus/frequency-derivative measurement routing and are rejected rather than approximated. A captured ANDES trajectory remains open. | +| `IEEEST` | `IEEEST` | `StabilizerIEEEST` | **Partial.** Exact frozen-ANDES local modes 0/1/3/4/5, filter/lead-lag/washout zero-bypass semantics, output limits, voltage gate, DYR mapping, and GENROU/exciter coupling are implemented. Modes 2/6 and nonzero `BUSR` require unavailable cross-bus/frequency-derivative measurement routing and are rejected rather than approximated. A captured ANDES trajectory remains open. | | `REGCA1`, `REECA1`, `REECB1`, `REPCA1` | Same named ANDES models | `GenModelInverter` is not equivalent | **No direct analogue.** Add the coordinated renewable generator, electrical-control, and plant-control chain rather than flattening these records into the generic inverter. | | `WTDTA1`, `WTARA1`, `WTPTA1`, `WTTQA1` | Same named ANDES models | None | **No direct analogue.** Add drive-train, aerodynamic, pitch, and torque-control submodels with their shared interfaces. | | `Toggle`, `Fault` | ANDES event models | GridDyn event/action and fault mechanisms | **Partial conceptually.** Define DYR record schemas and translate target resolution, timing, status changes, and fault clearing semantics; no adapter exists. | @@ -171,14 +171,14 @@ seconds, and continuing through 2.0 seconds. The five controller chains are: ### PR status and dependency order -| PR | Deliverable | Status | Depends on | -| --- | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------- | -| 1 | Complete GENROU equations, saturation utility, DYR mapping, and five-machine initialization reference | Current changes; treat as complete after merge | none | -| 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | -| 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Implemented | PR 2 | -| 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Models/adapters implemented; trajectories open | PR 3 | -| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | `IEEEG1` implemented; `ST2CUT` and `IEEEST` partial | PR 4 | -| 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | +| PR | Deliverable | Status | Depends on | +| --- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | ---------- | +| 1 | Complete GENROU equations, saturation utility, DYR mapping, and five-machine initialization reference | Current changes; treat as complete after merge | none | +| 2 | Robust RAW/DYR identity and base handling, IEEE 14 power-flow parity, and a controller-free GENROU trajectory | Planned | PR 1 | +| 3 | Generator/controller signal plumbing, PSS-to-exciter routing, and validated `TGOV1` | Implemented | PR 2 | +| 4 | Complete `ESST3A` and `EXST1` models plus DYR adapters | Models/adapters implemented; trajectories open | PR 3 | +| 5 | Complete `IEEEG1`, `ST2CUT`, and `IEEEST` models plus DYR adapters | `IEEEG1` implemented; `ST2CUT` and `IEEEST` partial | PR 4 | +| 6 | DYR `Toggle`, complete IEEE 14 initialization/equilibrium, and the two-second trajectory regression | Planned | PR 5 | PRs must merge in this order. Each PR must pass without relying on production code from a later PR. Minimized fixtures and test-only probe models are @@ -720,31 +720,31 @@ can be split and checked off as soon as models in the same family diverge. A model is not compatible merely because a similarly named GridDyn model exists: it also needs native-input mapping, initialization, and a trajectory test. -| ANDES model(s) | GridDyn mapping / next action | Status | -| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial | -| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial | -| `TGOV1` | `GovernorTgov1`; ANDES-equation, DYR-order, limiter, initialization, Jacobian, and isolated trajectory regressions are complete, including a captured IEEE 14 GENROU+TGOV1 `Alter(pref0)` trajectory. | Implemented | -| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial | -| `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial | -| `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial | -| `ACE`, `ACEc`, `COI` | Map area-control and center-of-inertia services. | Untriaged | -| `PLBVFU1`, `IEEEVC` | No direct voltage-compensator/playback analogue; define exciter input and playback interfaces. | No direct analogue | -| `IEEEG1` | `GovernorIeeeG1`; frozen equations, limits, DYR mapping, one-/two-machine connections, initialization, and Jacobian checks are complete; unequal MBASE, native import, and disturbed trajectory remain. | Implemented | -| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Existing hydro/reheat/steam classes are candidates only; each needs an equation audit or a new model. | Planned | -| `SHAFT5` | Multi-mass shaft model; map states and mechanical interfaces. | Untriaged | -| `ESDC1A`, `ESDC2A`, `SEXS`, `IEEET1`, `IEEET3` | Existing DC/IEEE/SEXS exciters are candidates; complete model-specific audits and DYR/native mappings. | Partial | -| `ESST3A` | `ExciterESST3A`; exact GENROU path plus documented reduced-order synchronous-machine signal approximations. | Implemented | -| `EXST1` | `ExciterEXST1`; positive-time-constant equations and DYR mapping, with the documented corrected regulator-output limiter; zero-time-constant blocks, native import, and a captured trajectory remain. | Implemented | -| `IEEEX1`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | -| `ST2CUT` | `StabilizerST2CUT`; local input modes, exact filters, limits, DYR mapping, and dynamic load-step coverage are implemented. Remote/frequency-derivative measurements and captured ANDES trajectories remain. | Partial | +| ANDES model(s) | GridDyn mapping / next action | Status | +| --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `GENCLS` | `GenModelClassical`; parameter and trajectory comparison required. | Partial | +| `GENROU` | `GenModelGENROU` equations, DYR mapping, and five-machine initialization match ANDES references; fix exact machine identity/base handling, native ANDES import, and trajectory parity. | Partial | +| `TGOV1` | `GovernorTgov1`; ANDES-equation, DYR-order, limiter, initialization, Jacobian, and isolated trajectory regressions are complete, including a captured IEEE 14 GENROU+TGOV1 `Alter(pref0)` trajectory. | Implemented | +| `EXDC2` | `ExciterDC2A` candidate; add omitted transducer/switch/saturation behavior and prove equation equivalence. | Partial | +| `ZIP`, `FLoad` | GridDyn static/dynamic load models; identify exact parameter and frequency-response equivalence. | Partial | +| `Motor3`, `Motor5` | `MotorLoad3`, `MotorLoad5`; parameter mapping and trajectory comparisons required. | Partial | +| `ACE`, `ACEc`, `COI` | Map area-control and center-of-inertia services. | Untriaged | +| `PLBVFU1`, `IEEEVC` | No direct voltage-compensator/playback analogue; define exciter input and playback interfaces. | No direct analogue | +| `IEEEG1` | `GovernorIeeeG1`; frozen equations, limits, DYR mapping, one-/two-machine connections, initialization, and Jacobian checks are complete; unequal MBASE, native import, and disturbed trajectory remain. | Implemented | +| `TG2`, `TGOV1DB`, `TGOV1N`, `TGOV1NDB`, `IEESGO`, `GAST`, `HYGOV`, `HYGOVDB`, `HYGOV4` | Existing hydro/reheat/steam classes are candidates only; each needs an equation audit or a new model. | Planned | +| `SHAFT5` | Multi-mass shaft model; map states and mechanical interfaces. | Untriaged | +| `ESDC1A`, `ESDC2A`, `SEXS`, `IEEET1`, `IEEET3` | Existing DC/IEEE/SEXS exciters are candidates; complete model-specific audits and DYR/native mappings. | Partial | +| `ESST3A` | `ExciterESST3A`; exact GENROU path plus documented reduced-order synchronous-machine signal approximations. | Implemented | +| `EXST1` | `ExciterEXST1`; positive-time-constant equations and DYR mapping, with the documented corrected regulator-output limiter; zero-time-constant blocks, native import, and a captured trajectory remain. | Implemented | +| `IEEEX1`, `EXAC1`, `EXAC2`, `EXAC4`, `ESST4B`, `AC8B`, `ESAC1A`, `ESST1A`, `ESAC5A` | No exact named GridDyn implementations; implement individually rather than mapping by family name. | No direct analogue | +| `ST2CUT` | `StabilizerST2CUT`; local input modes, exact filters, limits, DYR mapping, and dynamic load-step coverage are implemented. Remote/frequency-derivative measurements and captured ANDES trajectories remain. | Partial | | `IEEEST` | `StabilizerIEEEST`; local input modes, exact ANDES zero-bypass filters, limits, DYR mapping, and dynamic load-step coverage are implemented. Remote/frequency-derivative measurements and captured trajectories remain. | Partial | -| `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | -| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | -| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | -| `PVD1`, `ESD1`, `EV1`, `EV2`, `DGPRCT1`, `DGPRCTExt` | Distributed energy-resource and protection models. | Untriaged | -| `Fault`, `Alter`, `TimeSeries`, `Toggle` | Event/action semantics and time-series input mapping. | Untriaged | -| `Summary`, `Output` | Reporting configuration; define output-channel mapping after model compatibility. | Untriaged | +| `BusFreq`, `BusROCOF`, `PMU`, `PLL1`, `PLL2`, `FreqDiv` | Measurement and frequency-estimation models. | Untriaged | +| `REGCA1`, `REGCP1`, `REECA1`, `REECA1E`, `REECA1G`, `REECB1`, `REPCA1`, `REGCV1`, `REGCV2`, `REGF1`, `REGF2`, `REGF3` | Generic `GenModelInverter` is insufficient; add composable generator, electrical, plant, and frequency controls. | No direct analogue | +| `WTDTA1`, `WTDS`, `WTARA1`, `WTPTA1`, `WTTQA1`, `WTARV1` | Add wind-turbine drive-train, aerodynamic, pitch, torque, and renewable-voltage submodels and interfaces. | No direct analogue | +| `PVD1`, `ESD1`, `EV1`, `EV2`, `DGPRCT1`, `DGPRCTExt` | Distributed energy-resource and protection models. | Untriaged | +| `Fault`, `Alter`, `TimeSeries`, `Toggle` | Event/action semantics and time-series input mapping. | Untriaged | +| `Summary`, `Output` | Reporting configuration; define output-channel mapping after model compatibility. | Untriaged | ## Numerical-regression policy From 8bb9b2925f331edcbb0dc220aff7858ac46b46bc Mon Sep 17 00:00:00 2001 From: Philip Top Date: Thu, 27 Aug 2026 06:03:52 -0700 Subject: [PATCH 3/3] fix code warnings --- src/griddyn/stabilizers/StabilizerIEEEST.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/griddyn/stabilizers/StabilizerIEEEST.cpp b/src/griddyn/stabilizers/StabilizerIEEEST.cpp index 0ed472beb..c58dfe2bb 100644 --- a/src/griddyn/stabilizers/StabilizerIEEEST.cpp +++ b/src/griddyn/stabilizers/StabilizerIEEEST.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace griddyn::stabilizers { namespace { @@ -102,7 +103,7 @@ void StabilizerIEEEST::dynObjectInitializeA(CoreTime /*time0*/, std::uint32_t /* offsets.local().local.algSize = 1; offsets.local().local.diffSize = stateCount; offsets.local().local.algRoots = 4; - offsets.local().local.jacSize = 8 * stateCount + 8; + offsets.local().local.jacSize = (8 * stateCount) + 8; } void StabilizerIEEEST::dynObjectInitializeB(const IOdata& inputs,