diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c31a6e41..0fdc82a38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Added component model developer checklist to a README file. - Added `IEEEST` Stabilizer Model - Added `SEXS-PTI` Exciter Model +- Added `ESDC1A` Exciter Model - Added `GENSAL` Machine Model - Added 200 Bus Synthetic Illinois Case - Added node objects to `PowerElectronics` module & updated all examples to make use of them. diff --git a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp index e48a8a9c5..84cd57961 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include diff --git a/GridKit/Model/PhasorDynamics/Exciter/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Exciter/CMakeLists.txt index 1120e20c2..7e5e4b9e3 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Exciter/CMakeLists.txt @@ -3,5 +3,6 @@ # - Luke Lowery # ]] +add_subdirectory(ESDC1A) add_subdirectory(IEEET1) add_subdirectory(SEXS-PTI) diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/CMakeLists.txt new file mode 100644 index 000000000..53807c5df --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/CMakeLists.txt @@ -0,0 +1,46 @@ +# [[ +# Author(s): +# - Luke Lowery +# ]] + +set(_install_headers Esdc1a.hpp Esdc1aData.hpp) + +if(GRIDKIT_ENABLE_ENZYME) + gridkit_add_library( + phasor_dynamics_exciter_esdc1a + SOURCES Esdc1aEnzyme.cpp + HEADERS ${_install_headers} + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES + PUBLIC + GridKit::phasor_dynamics_core + PUBLIC + GridKit::phasor_dynamics_signal + PRIVATE + ClangEnzymeFlags + COMPILE_OPTIONS + PRIVATE + -mllvm + -enzyme-auto-sparsity=1 + -fno-math-errno) +else() + gridkit_add_library( + phasor_dynamics_exciter_esdc1a + SOURCES Esdc1a.cpp + HEADERS ${_install_headers} + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES GridKit::phasor_dynamics_core GridKit::phasor_dynamics_signal) +endif() + +gridkit_add_library( + phasor_dynamics_exciter_esdc1a_dependency_tracking + SOURCES Esdc1aDependencyTracking.cpp + INCLUDE_DIRECTORIES PRIVATE ${GRIDKIT_THIRD_PARTY_DIR}/magic-enum/include + LINK_LIBRARIES GridKit::phasor_dynamics_core GridKit::phasor_dynamics_signal_dependency_tracking) + +target_link_libraries( + phasor_dynamics_components + INTERFACE GridKit::phasor_dynamics_exciter_esdc1a) +target_link_libraries( + phasor_dynamics_components_dependency_tracking + INTERFACE GridKit::phasor_dynamics_exciter_esdc1a_dependency_tracking) diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.cpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.cpp new file mode 100644 index 000000000..6c5c0f89c --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.cpp @@ -0,0 +1,27 @@ +/** + * @file Esdc1a.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Non-Enzyme instantiation for the ESDC1A exciter model. + */ + +#include "Esdc1aImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Exciter + { + template + int Esdc1a::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Esdc1a..." << std::endl; + Log::misc() << "Jacobian evaluation not implemented!" << std::endl; + return 0; + } + + template class Esdc1a; + template class Esdc1a; + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp new file mode 100644 index 000000000..d401eac00 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp @@ -0,0 +1,152 @@ +/** + * @file Esdc1a.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Declaration of the ESDC1A exciter model. + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + template + class BusBase; + + template + class SignalNode; + + namespace Exciter + { + /// Internal variables of an `Esdc1a`. + enum class Esdc1aInternalVariables : size_t + { + EFDP, ///< Field-voltage state before optional speed multiplier + VC, ///< Sensed compensated voltage + VR, ///< Voltage-regulator output + VF, ///< Stabilizing feedback output + XLL, ///< Lead-lag state + EV, ///< Voltage-regulator input error + VLL, ///< Lead-lag block output + VHV, ///< High-value gate output + SE, ///< Saturation coefficient + VFE, ///< Exciter feedback signal + EFD, ///< Field-voltage output + MAXIMUM, + }; + + /// External variables of an `Esdc1a`. + enum class Esdc1aExternalVariables : size_t + { + OMEGA, ///< Machine speed deviation + VS, ///< Stabilizer input signal + VUEL, ///< Under-excitation limiter input + MAXIMUM, + }; + + template + class Esdc1a : public Component + { + using Component::alpha_; + using Component::f_; + using Component::gridkit_component_id_; + using Component::J_; + using Component::J_cols_buffer_; + using Component::J_rows_buffer_; + using Component::J_vals_buffer_; + using Component::residual_indices_; + using Component::size_; + using Component::tag_; + using Component::variable_indices_; + using Component::wb_; + using Component::y_; + using Component::yp_; + + public: + using RealT = typename Component::RealT; + using bus_type = BusBase; + using signal_type = SignalNode; + using model_data_type = Esdc1aData; + using MonitorT = Model::VariableMonitor; + + Esdc1a(bus_type* bus); + Esdc1a(bus_type* bus, const model_data_type& data); + ~Esdc1a(); + + int setGridKitComponentID(IdxT) override final; + int allocate() override final; + int verify() const override final; + int initialize() override final; + int tagDifferentiable() override final; + int evaluateResidual() override final; + int evaluateJacobian() override final; + + auto getSignals() + -> ComponentSignals& + { + return signals_; + } + + const Model::VariableMonitorBase* getMonitor() const override; + + __attribute__((always_inline)) inline int evaluateInternalResidual( + ScalarT*, ScalarT*, ScalarT*, ScalarT*, ScalarT*); + + private: + void initModelParams(const model_data_type& data); + void setDerivedParams(); + void initializeMonitor(); + + bus_type* bus_{nullptr}; + + RealT Tr_{0.0}; + RealT Ka_{40.0}; + RealT Ta_{0.1}; + RealT Tb_{0.0}; + RealT Tc_{0.0}; + RealT Vrmax_{1.0}; + RealT Vrmin_{-1.0}; + RealT Ke_{0.1}; + RealT Te_{0.5}; + RealT Kf_{0.05}; + RealT Tf1_{0.7}; + RealT spdmlt_{0.0}; + RealT E1_{2.8}; + RealT Se1_{0.08}; + RealT E2_{3.7}; + RealT Se2_{0.33}; + IdxT UEL_{0}; + RealT exclim_{1.0}; + + RealT sUEL_{0}; + RealT sUELoff_{1}; + RealT slim_{0}; + RealT slim_off_{1}; + RealT use_lead_lag_{0}; + RealT bypass_lead_lag_{1}; + RealT SA_{0}; + RealT SB_{0}; + + ScalarT vref_{0}; + + ComponentSignals signals_; + std::unique_ptr monitor_; + + std::vector ws_; + std::vector ws_indices_; + }; + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aData.hpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aData.hpp new file mode 100644 index 000000000..0c6a23797 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aData.hpp @@ -0,0 +1,76 @@ +/** + * @file Esdc1aData.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Modeling data for the ESDC1A exciter model. + */ + +#pragma once + +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Exciter + { + /// Parameter keys for the ESDC1A exciter model. + enum class Esdc1aParameters + { + Tr, ///< Transducer time constant + Ka, ///< Voltage-regulator gain + Ta, ///< Voltage-regulator time constant + Tb, ///< Lead-lag denominator time constant + Tc, ///< Lead-lag numerator time constant + Vrmax, ///< Maximum voltage-regulator output + Vrmin, ///< Minimum voltage-regulator output + Ke, ///< Exciter field-resistance line-slope margin + Te, ///< Exciter field time constant + Kf, ///< Stabilizing feedback gain + Tf1, ///< Feedback lead time constant + Spdmlt, ///< Speed multiplier flag + E1, ///< First saturation voltage point + Se1, ///< Saturation value at E1 + E2, ///< Second saturation voltage point + Se2, ///< Saturation value at E2 + UEL, ///< UEL input-location selector + exclim ///< Exciter feedback lower-limit flag + }; + + /// Ports for the ESDC1A exciter model. + enum class Esdc1aPorts + { + bus, ///< Unique ID of the terminal bus + speed, ///< Unique ID of the generator speed-deviation signal + efd, ///< Unique ID of the output EFD signal + vs, ///< Unique ID of the optional stabilizer input signal + vuel ///< Unique ID of the optional UEL input signal + }; + + /// Variables available through the monitor interface. + enum class Esdc1aMonitorableVariables + { + efd, ///< Field-voltage output + vc, ///< Sensed compensated voltage + vr, ///< Voltage-regulator output + vf, ///< Stabilizing feedback state + se, ///< Saturation coefficient + vfe ///< Exciter feedback signal + }; + + template + struct Esdc1aData : public ComponentData + { + Esdc1aData() = default; + + using Parameters = Esdc1aParameters; + using Ports = Esdc1aPorts; + using MonitorableVariables = Esdc1aMonitorableVariables; + }; + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aDependencyTracking.cpp new file mode 100644 index 000000000..c7c25aec6 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aDependencyTracking.cpp @@ -0,0 +1,27 @@ +/** + * @file Esdc1aDependencyTracking.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Dependency-tracking instantiations for the ESDC1A exciter model. + */ + +#include "Esdc1aImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Exciter + { + template + int Esdc1a::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Esdc1a..." << std::endl; + Log::misc() << "Jacobian evaluation not implemented!" << std::endl; + return 0; + } + + template class Esdc1a; + template class Esdc1a; + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aEnzyme.cpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aEnzyme.cpp new file mode 100644 index 000000000..96a7837bd --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aEnzyme.cpp @@ -0,0 +1,93 @@ +/** + * @file Esdc1aEnzyme.cpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Enzyme sparse Jacobian for the ESDC1A exciter model. + */ + +#include + +#include "Esdc1aImpl.hpp" + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Exciter + { + template + int Esdc1a::evaluateJacobian() + { + Log::misc() << "Evaluate Jacobian for Esdc1a..." << std::endl; + Log::misc() << "Jacobian evaluation is experimental!" << std::endl; + + J_.zeroMatrix(); + if (J_rows_buffer_ == nullptr) + { + J_rows_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_cols_buffer_ = new IdxT[static_cast(size_) * static_cast(size_)]; + J_vals_buffer_ = new RealT[static_cast(size_) * static_cast(size_)]; + } + + using ModelT = GridKit::PhasorDynamics::Exciter::Esdc1a; + using Fn = GridKit::Enzyme::Sparse::MemberFunctions; + + GridKit::Enzyme::Sparse::DfDy::eval(this, + f_.size(), + y_.size(), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + alpha_, + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDwb::eval(this, + f_.size(), + static_cast(bus_->size()), + (this->getResidualIndices()).data(), + (bus_->getVariableIndices()).data(), + y_.data(), + yp_.data(), + (bus_->y()).data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + GridKit::Enzyme::Sparse::DfDws::eval(this, + f_.size(), + ws_.size(), + (this->getResidualIndices()).data(), + ws_indices_.data(), + y_.data(), + yp_.data(), + wb_.data(), + ws_.data(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + J_); + + return 0; + } + + template class Esdc1a; + template class Esdc1a; + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp new file mode 100644 index 000000000..87d648eb6 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp @@ -0,0 +1,480 @@ +/** + * @file Esdc1aImpl.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Definition of the ESDC1A exciter model. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Exciter + { + using Log = ::GridKit::Utilities::Logger; + + template + Esdc1a::Esdc1a(bus_type* bus) + : bus_(bus) + { + setDerivedParams(); + size_ = static_cast(Esdc1aInternalVariables::MAXIMUM); + } + + template + Esdc1a::Esdc1a(bus_type* bus, const model_data_type& data) + : bus_(bus), + monitor_(std::make_unique(data)) + { + initModelParams(data); + setDerivedParams(); + initializeMonitor(); + size_ = static_cast(Esdc1aInternalVariables::MAXIMUM); + } + + template + Esdc1a::~Esdc1a() + { + } + + template + void Esdc1a::initModelParams(const model_data_type& data) + { + using Params = typename model_data_type::Parameters; + + auto loadReal = [&](auto param, RealT& member) + { + if (data.parameters.contains(param)) + { + member = std::get(data.parameters.at(param)); + } + }; + + auto loadIndex = [&](auto param, IdxT& member) + { + if (data.parameters.contains(param)) + { + member = std::get(data.parameters.at(param)); + } + }; + + loadReal(Params::Tr, Tr_); + loadReal(Params::Ka, Ka_); + loadReal(Params::Ta, Ta_); + loadReal(Params::Tb, Tb_); + loadReal(Params::Tc, Tc_); + loadReal(Params::Vrmax, Vrmax_); + loadReal(Params::Vrmin, Vrmin_); + loadReal(Params::Ke, Ke_); + loadReal(Params::Te, Te_); + loadReal(Params::Kf, Kf_); + loadReal(Params::Tf1, Tf1_); + loadReal(Params::Spdmlt, spdmlt_); + loadReal(Params::E1, E1_); + loadReal(Params::Se1, Se1_); + loadReal(Params::E2, E2_); + loadReal(Params::Se2, Se2_); + loadIndex(Params::UEL, UEL_); + loadReal(Params::exclim, exclim_); + } + + template + void Esdc1a::setDerivedParams() + { + sUEL_ = ZERO; + if (UEL_ >= static_cast(2)) + { + sUEL_ = ONE; + } + sUELoff_ = ONE - sUEL_; + slim_ = exclim_; + slim_off_ = ONE - slim_; + + use_lead_lag_ = ZERO; + if (Tb_ > ZERO) + { + use_lead_lag_ = ONE; + } + bypass_lead_lag_ = ONE - use_lead_lag_; + + if (Se1_ == ZERO && Se2_ == ZERO) + { + SA_ = ZERO; + SB_ = ZERO; + return; + } + if (E1_ <= ZERO || E2_ <= ZERO || E1_ == E2_ + || Se1_ <= ZERO || Se2_ <= ZERO || Se1_ == Se2_) + { + SA_ = ZERO; + SB_ = ZERO; + return; + } + + const RealT C = std::sqrt(Se2_ / Se1_); + SA_ = (C * E1_ - E2_) / (C - ONE); + SB_ = Se1_ / ((E1_ - SA_) * (E1_ - SA_)); + } + + template + const Model::VariableMonitorBase* Esdc1a::getMonitor() const + { + return monitor_.get(); + } + + template + void Esdc1a::initializeMonitor() + { + using Variable = typename model_data_type::MonitorableVariables; + auto index = [](Esdc1aInternalVariables variable) + { + return static_cast(variable); + }; + + monitor_->set(Variable::efd, [this, index] + { return y_[index(Esdc1aInternalVariables::EFD)]; }); + monitor_->set(Variable::vc, [this, index] + { return y_[index(Esdc1aInternalVariables::VC)]; }); + monitor_->set(Variable::vr, [this, index] + { return y_[index(Esdc1aInternalVariables::VR)]; }); + monitor_->set(Variable::vf, [this, index] + { return y_[index(Esdc1aInternalVariables::VF)]; }); + monitor_->set(Variable::se, [this, index] + { return y_[index(Esdc1aInternalVariables::SE)]; }); + monitor_->set(Variable::vfe, [this, index] + { return y_[index(Esdc1aInternalVariables::VFE)]; }); + } + + template + int Esdc1a::setGridKitComponentID(IdxT component_id) + { + gridkit_component_id_ = component_id; + return 0; + } + + template + int Esdc1a::allocate() + { + size_ = static_cast(Esdc1aInternalVariables::MAXIMUM); + auto size = static_cast(size_); + + f_.assign(size, ScalarT{0}); + y_.assign(size, ScalarT{0}); + yp_.assign(size, ScalarT{0}); + tag_.assign(size, false); + variable_indices_.resize(size); + residual_indices_.resize(size); + + wb_.assign(2, ScalarT{0}); + + auto signal_size = static_cast(Esdc1aExternalVariables::MAXIMUM); + ws_.assign(signal_size, ScalarT{0}); + ws_indices_.assign(signal_size, INVALID_INDEX); + + for (IdxT j = 0; j < size_; ++j) + { + this->setVariableIndex(j, j); + this->setResidualIndex(j, j); + } + + if (signals_.template isAssigned()) + { + signals_.template getSignalNode()->set( + &y_[static_cast(Esdc1aInternalVariables::EFD)], + &(this->getVariableIndex(static_cast(Esdc1aInternalVariables::EFD)))); + } + + return 0; + } + + template + int Esdc1a::verify() const + { + int ret = 0; + + auto check = [&](bool condition, const char* message) + { + if (!condition) + { + Log::error() << "Esdc1a: " << message << '\n'; + ret += 1; + } + }; + + if (bus_ == nullptr) + { + Log::error() << "Esdc1a: bus pointer is null\n"; + ret += 1; + } + + check(Ka_ > ZERO, "Ka must be positive"); + check(Tr_ >= ZERO, "Tr must be non-negative"); + check(Ta_ > ZERO, "Ta must be positive"); + check(Tb_ >= ZERO, "Tb must be non-negative"); + check(Tc_ >= ZERO, "Tc must be non-negative"); + check(Tb_ > ZERO || Tc_ == ZERO, "Tc must be zero when Tb is zero"); + check(Te_ > ZERO, "Te must be positive"); + check(Tf1_ >= ZERO, "Tf1 must be non-negative"); + check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax"); + check(spdmlt_ == ZERO || spdmlt_ == ONE, "Spdmlt must be 0 or 1"); + check(exclim_ == ZERO || exclim_ == ONE, "exclim must be 0 or 1"); + check(static_cast(UEL_) >= ZERO && UEL_ <= static_cast(3), + "UEL must be 0, 1, 2, or 3"); + + if (Se1_ == ZERO && Se2_ == ZERO) + { + // Saturation disabled. + } + else + { + check(E1_ > ZERO, "E1 must be positive when saturation is enabled"); + check(E2_ > ZERO, "E2 must be positive when saturation is enabled"); + check(E1_ != E2_, "E1 and E2 must differ when saturation is enabled"); + check(Se1_ > ZERO, "Se1 must be positive when saturation is enabled"); + check(Se2_ > ZERO, "Se2 must be positive when saturation is enabled"); + check(Se1_ != Se2_, "Se1 and Se2 must differ when saturation is enabled"); + } + + if (!signals_.template isAssigned()) + { + Log::error() << "Esdc1a: required EFD signal is not assigned\n"; + ret += 1; + } + + if (spdmlt_ == ONE + && !signals_.template isAttached()) + { + Log::error() << "Esdc1a: speed signal is required when Spdmlt is enabled\n"; + ret += 1; + } + + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Esdc1a: speed signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Esdc1a: VS signal attached with no linked source\n"; + ret += 1; + } + if (signals_.template isAttached() + && !signals_.template isLinked()) + { + Log::error() << "Esdc1a: VUEL signal attached with no linked source\n"; + ret += 1; + } + + return ret; + } + + template + int Esdc1a::initialize() + { + const auto EFDP = static_cast(Esdc1aInternalVariables::EFDP); + const auto VC = static_cast(Esdc1aInternalVariables::VC); + const auto VR = static_cast(Esdc1aInternalVariables::VR); + const auto VF = static_cast(Esdc1aInternalVariables::VF); + const auto XLL = static_cast(Esdc1aInternalVariables::XLL); + const auto EV = static_cast(Esdc1aInternalVariables::EV); + const auto VLL = static_cast(Esdc1aInternalVariables::VLL); + const auto VHV = static_cast(Esdc1aInternalVariables::VHV); + const auto SE = static_cast(Esdc1aInternalVariables::SE); + const auto VFE = static_cast(Esdc1aInternalVariables::VFE); + const auto EFD = static_cast(Esdc1aInternalVariables::EFD); + + ScalarT omega0{ZERO}; + if (signals_.template isAttached()) + { + omega0 = signals_.template readExternalVariable(); + } + + ScalarT vs0{ZERO}; + if (signals_.template isAttached()) + { + vs0 = signals_.template readExternalVariable(); + } + + ScalarT vuel0{ZERO}; + if (signals_.template isAttached()) + { + vuel0 = signals_.template readExternalVariable(); + } + + const ScalarT denom = ONE + spdmlt_ * omega0; + if (denom == ZERO) + { + Log::error() << "Esdc1a: speed multiplier denominator is zero at initialization\n"; + return 1; + } + + const ScalarT Ec0 = std::sqrt(bus_->Vr() * bus_->Vr() + bus_->Vi() * bus_->Vi()); + + const ScalarT efd0 = y_[EFD]; + const ScalarT efdp0 = efd0 / denom; + const ScalarT se0 = SB_ * Math::qramp(efdp0 - SA_); + const ScalarT vfe0 = slim_off_ * (Ke_ + se0) * efdp0 + + slim_ * Math::ramp((Ke_ + se0) * efdp0); + const ScalarT vr0 = vfe0; + const ScalarT vhv0 = vr0 / Ka_; + const ScalarT vc0 = Ec0; + const ScalarT vf0 = ScalarT{ZERO}; + const ScalarT xll0 = vhv0; + const ScalarT vll0 = vhv0; + const ScalarT ev0 = vhv0; + + if (vr0 < Vrmin_ || vr0 > Vrmax_) + { + Log::error() << "Esdc1a: initialized VR is outside limits\n"; + return 1; + } + if (sUEL_ == ZERO && vhv0 < vuel0) + { + Log::error() << "Esdc1a: high-value gate active at initialization\n"; + return 1; + } + + vref_ = ev0 + vc0 + vf0 - vs0 - sUEL_ * vuel0; + + y_[EFDP] = efdp0; + y_[VC] = vc0; + y_[VR] = vr0; + y_[VF] = vf0; + y_[XLL] = xll0; + y_[EV] = ev0; + y_[VLL] = vll0; + y_[VHV] = vhv0; + y_[SE] = se0; + y_[VFE] = vfe0; + y_[EFD] = efd0; + + std::fill(yp_.begin(), yp_.end(), ZERO); + return 0; + } + + template + int Esdc1a::tagDifferentiable() + { + std::fill(tag_.begin(), tag_.end(), false); + tag_[static_cast(Esdc1aInternalVariables::EFDP)] = true; + tag_[static_cast(Esdc1aInternalVariables::VC)] = (Tr_ > ZERO); + tag_[static_cast(Esdc1aInternalVariables::VR)] = true; + tag_[static_cast(Esdc1aInternalVariables::VF)] = (Tf1_ > ZERO); + tag_[static_cast(Esdc1aInternalVariables::XLL)] = (Tb_ > ZERO); + return 0; + } + + template + __attribute__((always_inline)) inline int Esdc1a::evaluateInternalResidual( + ScalarT* y, + ScalarT* yp, + ScalarT* wb, + ScalarT* ws, + ScalarT* f) + { + const auto EFDP = static_cast(Esdc1aInternalVariables::EFDP); + const auto VC = static_cast(Esdc1aInternalVariables::VC); + const auto VR = static_cast(Esdc1aInternalVariables::VR); + const auto VF = static_cast(Esdc1aInternalVariables::VF); + const auto XLL = static_cast(Esdc1aInternalVariables::XLL); + const auto EV = static_cast(Esdc1aInternalVariables::EV); + const auto VLL = static_cast(Esdc1aInternalVariables::VLL); + const auto VHV = static_cast(Esdc1aInternalVariables::VHV); + const auto SE = static_cast(Esdc1aInternalVariables::SE); + const auto VFE = static_cast(Esdc1aInternalVariables::VFE); + const auto EFD = static_cast(Esdc1aInternalVariables::EFD); + + const auto OMEGA = static_cast(Esdc1aExternalVariables::OMEGA); + const auto VS = static_cast(Esdc1aExternalVariables::VS); + const auto VUEL = static_cast(Esdc1aExternalVariables::VUEL); + + const ScalarT efdp = y[EFDP]; + const ScalarT vc = y[VC]; + const ScalarT vr = y[VR]; + const ScalarT vf = y[VF]; + const ScalarT xll = y[XLL]; + const ScalarT ev = y[EV]; + const ScalarT vll = y[VLL]; + const ScalarT vhv = y[VHV]; + const ScalarT se = y[SE]; + const ScalarT vfe = y[VFE]; + const ScalarT efd = y[EFD]; + + const ScalarT omega = ws[OMEGA]; + const ScalarT vs = ws[VS]; + const ScalarT vuel = ws[VUEL]; + + const ScalarT Ec = std::sqrt(wb[0] * wb[0] + wb[1] * wb[1]); + + f[EFDP] = -Te_ * yp[EFDP] + vr - vfe; + f[VC] = -Tr_ * yp[VC] - vc + Ec; + f[VR] = -Ta_ * yp[VR] + Math::antiwindup(vr, -vr + Ka_ * vhv, Vrmin_, Vrmax_); + f[VF] = -Te_ * Tf1_ * yp[VF] - Te_ * vf + Kf_ * (vr - vfe); + f[XLL] = -Tb_ * yp[XLL] - xll + ev; + + // Keep the EV equation grouped this way to avoid an Enzyme sparse-lowering compile failure. + const ScalarT ref_err = vref_ - ev; + const ScalarT sig_err = vs - vc; + const ScalarT lim_err = sUEL_ * vuel - vf; + f[EV] = ref_err + sig_err + lim_err; + f[VLL] = use_lead_lag_ * (-Tb_ * (vll - xll) + Tc_ * (ev - xll)) + + bypass_lead_lag_ * (-vll + ev); + f[VHV] = -vhv + sUEL_ * vll + sUELoff_ * Math::max(vll, vuel); + f[SE] = -se + SB_ * Math::qramp(efdp - SA_); + f[VFE] = -vfe + slim_off_ * (Ke_ + se) * efdp + slim_ * Math::ramp((Ke_ + se) * efdp); + f[EFD] = -efd + (ONE + spdmlt_ * omega) * efdp; + + return 0; + } + + template + int Esdc1a::evaluateResidual() + { + const auto OMEGA = static_cast(Esdc1aExternalVariables::OMEGA); + const auto VS = static_cast(Esdc1aExternalVariables::VS); + const auto VUEL = static_cast(Esdc1aExternalVariables::VUEL); + + std::fill(ws_.begin(), ws_.end(), ScalarT{ZERO}); + std::fill(ws_indices_.begin(), ws_indices_.end(), INVALID_INDEX); + + if (signals_.template isAttached()) + { + ws_[OMEGA] = signals_.template readExternalVariable(); + ws_indices_[OMEGA] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[VS] = signals_.template readExternalVariable(); + ws_indices_[VS] = + signals_.template readExternalVariableIndex(); + } + if (signals_.template isAttached()) + { + ws_[VUEL] = signals_.template readExternalVariable(); + ws_indices_[VUEL] = + signals_.template readExternalVariableIndex(); + } + + wb_[0] = bus_->Vr(); + wb_[1] = bus_->Vi(); + + evaluateInternalResidual(y_.data(), yp_.data(), wb_.data(), ws_.data(), f_.data()); + return 0; + } + } // namespace Exciter + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/README.md b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/README.md new file mode 100644 index 000000000..1161ff2a7 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/README.md @@ -0,0 +1,221 @@ +# **Exciter Model (ESDC1A)** + +## Block Diagram + +Standard model of the ESDC1A Exciter. + +Notes: +- Internal voltage signals are on model base unless otherwise stated. +- The source diagram labels the optional multiplier input as `Speed`; GridKit uses machine speed deviation, so the enabled multiplier is $1+\omega$. +- The PowerWorld parameter table names the UEL selector `UEL`; the block diagram routes `UEL >= 2` through the input-error summing junction and `UEL < 2` through the high-value gate. + +
+ + + Figure 1: Exciter ESDC1A model. Figure courtesy of [PowerWorld](https://www.powerworld.com/WebHelp/) +
+ +## Model Parameters + +Symbol | Units | Description | Typical Value | Note +------------------------------------|----------|---------------------------------------------------------|---------------|------ +$T_R$ | [sec] | Transducer time constant | 0 | Block name: `Tr`; if zero, $V_C$ is algebraic +$K_A$ | [p.u.] | Voltage-regulator gain | 40 | Block name: `Ka` +$T_A$ | [sec] | Voltage-regulator time constant | 0.1 | Block name: `Ta` +$T_B$ | [sec] | Lag time constant for the voltage-regulator input lead-lag block | 0 | Block name: `Tb`; if $T_B=T_C=0$, GridKit treats the lead-lag block as bypassed +$T_C$ | [sec] | Lead time constant for the voltage-regulator input lead-lag block | 0 | Block name: `Tc`; must be zero when $T_B=0$ +$V_R^{\max}$ | [p.u.] | Maximum voltage-regulator output | 1 | Block name: `Vrmax` +$V_R^{\min}$ | [p.u.] | Minimum voltage-regulator output | -1 | Block name: `Vrmin` +$K_E$ | [p.u.] | Exciter field-resistance line-slope margin | 0.1 | Block name: `Ke` +$T_E$ | [sec] | Exciter time constant | 0.5 | Block name: `Te` +$K_F$ | [p.u.] | Stabilizing feedback gain | 0.05 | Block name: `Kf` +$T_{F1}$ | [sec] | Feedback lead time constant | 0.7 | Block name: `Tf1` +$s_{\mathrm{spd}}$ | [binary] | Speed multiplier flag | 0 | Block name: `Spdmlt`; 1 enables the speed multiplier +$E_1$ | [p.u.] | First saturation voltage point | 2.8 | Block name: `E1` +$S_E(E_1)$ | [p.u.] | Saturation value at $E_1$ | 0.08 | Block name: `Se1` +$E_2$ | [p.u.] | Second saturation voltage point | 3.7 | Block name: `E2` +$S_E(E_2)$ | [p.u.] | Saturation value at $E_2$ | 0.33 | Block name: `Se2` +$I_{\mathrm{UEL}}$ | [integer] | Under-excitation limiter input-location selector | 0 | Block name: `UEL`; 0/1 = HV gate input, 2/3 = input-error summing junction +$s_{\mathrm{lim}}$ | [binary] | Exciter feedback lower-limit flag | 1 | Block name: `exclim`; 1 enables the zero lower limit on $V_{\mathrm{FE}}$ + +### Parameter Validation + +Invalid ESDC1A parameter sets are rejected by the following checks. Source data may apply PowerWorld-style autocorrections before these equations are evaluated. + +The required checks are: + +```math +\begin{aligned} + &K_A > 0 \\ + &T_R \ge 0,\quad T_A > 0,\quad T_B \ge 0,\quad T_C \ge 0,\quad T_E > 0,\quad T_{F1} \ge 0 \\ + &T_B > 0\quad\text{or}\quad(T_B = 0\ \text{and}\ T_C = 0) \\ + &V_R^{\min} \le V_R^{\max} \\ + &s_{\mathrm{spd}}, s_{\mathrm{lim}} \in \{0,1\} \\ + &I_{\mathrm{UEL}} \in \{0,1,2,3\} +\end{aligned} +``` + +The saturation points are either disabled together, + +```math +\begin{aligned} + S_E(E_1) = 0,\quad S_E(E_2) = 0 +\end{aligned} +``` + +or define a valid two-point quadratic saturation fit: + +```math +\begin{aligned} + &E_1 > 0,\quad E_2 > 0,\quad E_1 \ne E_2 \\ + &S_E(E_1) > 0,\quad S_E(E_2) > 0,\quad S_E(E_1) \ne S_E(E_2) +\end{aligned} +``` + +### Model Derived Parameters + +The UEL routing flag and off-mode flag complements are: + +```math +\begin{aligned} + s_{\mathrm{UEL}} &= + \begin{cases} + 1 & I_{\mathrm{UEL}} \ge 2 \\ + 0 & I_{\mathrm{UEL}} < 2 + \end{cases} \\ + s_{\mathrm{UEL}}^{\mathrm{off}} &= 1 - s_{\mathrm{UEL}} \\ + s_{\mathrm{lim}}^{\mathrm{off}} &= 1 - s_{\mathrm{lim}} +\end{aligned} +``` + +The PowerWorld default $T_B=T_C=0$ is treated as a lead-lag bypass. + +The saturation curve is fitted from the two supplied saturation points. If both saturation factors are zero, use $S_A=0$ and $S_B=0$. Otherwise: + +```math +\begin{aligned} + C &= \sqrt{\dfrac{S_E(E_2)}{S_E(E_1)}} \\ + S_A &= \dfrac{C E_1 - E_2}{C - 1} \\ + S_B &= \dfrac{S_E(E_1)}{(E_1 - S_A)^2} +\end{aligned} +``` + +## Model Variables + +### Internal Variables + +#### Differential + +Symbol | Units | Description | Note +------------------------------------|--------|---------------------------------------------------------|------ +$E_{\mathrm{fd}}'$ | [p.u.] | Field-voltage state before optional speed multiplier | State 1 in Fig. 1; source label: `EFD` +$V_C$ | [p.u.] | Sensed compensated voltage | State 2 in Fig. 1; source label: `Sensed Vt` +$V_R$ | [p.u.] | Voltage-regulator output | State 3 in Fig. 1; source label: `VR` +$V_F$ | [p.u.] | Stabilizing feedback washout output | State 4 in Fig. 1; source label: `VF`; algebraic when $T_{F1}=0$ +$x_{\mathrm{LL}}$ | [p.u.] | Lead-lag block state | State 5 in Fig. 1; source label: `Lead-Lag` + +#### Algebraic + +Symbol | Units | Description | Note +------------------------------------|----------|---------------------------------------------------------|------ +$e_V$ | [p.u.] | Voltage-regulator input error before lead-lag block | Includes voltage reference, sensed voltage, stabilizing feedback, stabilizer input, and selected UEL input +$V_{\mathrm{LL}}$ | [p.u.] | Lead-lag block output | Input to high-value gate +$V_{\mathrm{HV}}$ | [p.u.] | High-value gate output | Input to voltage regulator +$S_E$ | [p.u.] | Saturation coefficient evaluated at $E_{\mathrm{fd}}'$ | Uses derived saturation curve +$V_{\mathrm{FE}}$ | [p.u.] | Exciter feedback signal after optional lower limit | Lower limited at zero when $s_{\mathrm{lim}}=1$ +$E_{\mathrm{fd}}$ | [p.u.] | Field-voltage output | Output after optional speed multiplier + +### External Variables + +#### Differential + +None. + +#### Algebraic + +Symbol | Units | Description | Note +------------------------------------|--------|---------------------------------------------------------|------ +$E_C$ | [p.u.] | Compensated terminal voltage magnitude | Source label: `EC` +$V_{\mathrm{ref}}$ | [p.u.] | Voltage-control reference | Source label: `VREF` +$V_S$ | [p.u.] | Stabilizer input signal | Source label: `VS`; optional, defaults to zero +$V_{\mathrm{UEL}}$ | [p.u.] | Under-excitation limiter input | Source label: `VUEL`; optional, defaults to zero +$\omega$ | [p.u.] | Machine speed deviation | Source label: `Speed`; optional when $s_{\mathrm{spd}}=0$ + +## Model Equations + +### Differential Equations + +```math +\begin{aligned} + 0 &= -T_R\dot V_C - V_C + E_C \\ + 0 &= -T_B\dot x_{\mathrm{LL}} - x_{\mathrm{LL}} + e_V \\ + 0 &= -T_A\dot V_R + + \text{antiwindup}\!\left( + V_R, + -V_R + K_A V_{\mathrm{HV}}, + V_R^{\min}, + V_R^{\max} + \right) \\ + 0 &= -T_E\dot E_{\mathrm{fd}}' + V_R - V_{\mathrm{FE}} \\ + 0 &= -T_E T_{F1}\dot V_F - T_E V_F + K_F(V_R - V_{\mathrm{FE}}) +\end{aligned} +``` + +CommonMath defines the [Anti-Windup](../../../../CommonMath.md#anti-windup-indicator) target and smooth approximation. + +### Algebraic Equations + +The algebraic targets use CommonMath helper notation where applicable: + +```math +\begin{aligned} + 0 &= -e_V + V_{\mathrm{ref}} + V_S + s_{\mathrm{UEL}}V_{\mathrm{UEL}} - V_C - V_F \\ + 0 &= -T_B(V_{\mathrm{LL}} - x_{\mathrm{LL}}) + T_C(e_V - x_{\mathrm{LL}}) \\ + 0 &= -V_{\mathrm{HV}} + + s_{\mathrm{UEL}}V_{\mathrm{LL}} + + s_{\mathrm{UEL}}^{\mathrm{off}}\text{max}(V_{\mathrm{LL}}, V_{\mathrm{UEL}}) \\ + 0 &= -S_E + S_B\,q(E_{\mathrm{fd}}' - S_A) \\ + 0 &= -V_{\mathrm{FE}} + + s_{\mathrm{lim}}^{\mathrm{off}}(K_E + S_E)E_{\mathrm{fd}}' + + s_{\mathrm{lim}}\rho\!\left((K_E + S_E)E_{\mathrm{fd}}'\right) \\ + 0 &= -E_{\mathrm{fd}} + \left(1 + s_{\mathrm{spd}}\omega\right)E_{\mathrm{fd}}' +\end{aligned} +``` + +CommonMath defines the helper targets and smooth approximations for [max](../../../../CommonMath.md#derived-functions) and the primitives [ramp and quadratic ramp](../../../../CommonMath.md#primitives) $\rho$ and $q$. + +When the PowerWorld default $T_B=T_C=0$ is used, GridKit bypasses the lead-lag block so $V_{\mathrm{LL}}=e_V$. + +## Initialization + +The machine initializes $E_{\mathrm{fd}}$ first. For a standard unsaturated start, ESDC1A reads that value along with attached $\omega$, $E_C$, $V_S$, and $V_{\mathrm{UEL}}$, sets all internal derivatives to zero, and evaluates: + +```math +\begin{aligned} + E_{\mathrm{fd},0}' &= \dfrac{E_{\mathrm{fd},0}}{1 + s_{\mathrm{spd}}\omega_0} \\ + S_{E,0} &= S_B\,q(E_{\mathrm{fd},0}' - S_A) \\ + V_{\mathrm{FE},0} + &= s_{\mathrm{lim}}^{\mathrm{off}}(K_E + S_{E,0})E_{\mathrm{fd},0}' + + s_{\mathrm{lim}}\rho\!\left((K_E + S_{E,0})E_{\mathrm{fd},0}'\right) \\ + V_{R,0} &= V_{\mathrm{FE},0} \\ + V_{\mathrm{HV},0} &= \dfrac{V_{R,0}}{K_A} \\ + V_{C,0} &= E_{C,0} \\ + V_{F,0} &= 0 \\ + x_{\mathrm{LL},0} &= V_{\mathrm{LL},0} = e_{V,0} = V_{\mathrm{HV},0} \\ + V_{\mathrm{ref},0} + &= e_{V,0} + V_{C,0} + V_{F,0} - V_{S,0} - s_{\mathrm{UEL}}V_{\mathrm{UEL},0} +\end{aligned} +``` + +This closed-form start requires $1 + s_{\mathrm{spd}}\omega_0 \ne 0$, $V_R^{\min} \le V_{R,0} \le V_R^{\max}$, and, when $s_{\mathrm{UEL}}=0$, $V_{\mathrm{HV},0} \ge V_{\mathrm{UEL},0}$. Saturated voltage-regulator starts and active high-value-gate starts are outside these closed-form equations. + +## Model Outputs + +Output | Units | Description | Note +----------------|--------|-------------------------------------|------ +`efd` | [p.u.] | Field-voltage output | $E_{\mathrm{fd}}$ +`vc` | [p.u.] | Sensed compensated voltage | $V_C$ +`vr` | [p.u.] | Voltage-regulator output | $V_R$ +`vf` | [p.u.] | Stabilizing feedback state | $V_F$ +`se` | [p.u.] | Saturation coefficient | $S_E$ +`vfe` | [p.u.] | Exciter feedback signal | $V_{\mathrm{FE}}$ diff --git a/GridKit/Model/PhasorDynamics/Exciter/README.md b/GridKit/Model/PhasorDynamics/Exciter/README.md index 81144066b..e0ccf716c 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/README.md +++ b/GridKit/Model/PhasorDynamics/Exciter/README.md @@ -1,7 +1,7 @@ # **Exciter Models** > [!NOTE] -> IEEET1 and SEXS-PTI exciters are currently implemented. +> EXDC1 is not currently implemented. ## Introduction @@ -14,4 +14,5 @@ device internal voltage. There are a few standard Exciter models - IEEE Type 1 Excitation Model (See [IEEET1](IEEET1/README.md)) - IEEE DC1 Excitation Model (See [EXDC1](EXDC1/README.md)) +- IEEE DC1A Excitation Model (See [ESDC1A](ESDC1A/README.md)) - Simplified Excitation System Model (See [SEXS-PTI](SEXS-PTI/README.md)) diff --git a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md index 32df36cdc..c7499f6f0 100644 --- a/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md +++ b/GridKit/Model/PhasorDynamics/INPUT_FORMAT.md @@ -146,6 +146,7 @@ are specified: `GenClassical` | the classical machine model | `bus`, `pmech`\*, `speed`\*, `efd`\* | `p0`, `q0`, `H`, `D`, `Ra`, `Xdp`, `mva` | `ir`, `ii`, `p`, `q`, `delta`, `omega` `Tgov1 ` | the TGOV1 governor model | `pmech`, `speed` | `R`, `T1`, `T2`, `T3`, `Pvmax`, `Pvmin`, `Dt` | `none` `Ieeet1` | the IEEET1 exciter model | `bus`, `speed`, `efd`, `vs`\* | `Tr`, `Ka`, `Ta`, `Ke`, `Te`, `Kf`, `Tf`, `Vrmin`, `Vrmax`, `E1`, `E2`, `Se1`, `Se2`, `Ispdlim` | `efd`, `ksat` + `Esdc1a` | the ESDC1A exciter model | `bus`, `efd`, `speed`\*, `vs`\*, `vuel`\* | `Tr`, `Ka`, `Ta`, `Tb`, `Tc`, `Vrmax`, `Vrmin`, `Ke`, `Te`, `Kf`, `Tf1`, `Spdmlt`, `E1`, `Se1`, `E2`, `Se2`, `UEL`, `exclim` | `efd`, `vc`, `vr`, `vf`, `se`, `vfe` `SexsPti` | the SEXS-PTI simplified exciter model | `bus`, `efd`, `vs`\* | `Ta`, `Tb`, `Te`, `K`, `Efdmax`, `Efdmin` | `efd` `Ieeest` | the IEEEST stabilizer model | `input`, `output` | `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `T1`, `T2`, `T3`, `T4`, `T5`, `T6`, `Ks`, `Lsmin`, `Lsmax`, `Vcl`, `Vcu`, `Tdelay` | `vss` `BusFault` | simple impedance-based fault at a bus | `bus`, `status`\* | `state0`, `R`, `X` | `state`, `ir`, `ii` diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index d7b4cc9d7..1d226cfb5 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -328,6 +328,43 @@ namespace GridKit addComponent(exciter); } + for (const auto& excitedata : data.esdc1a) + { + IdxT bus_index = 0; + if (excitedata.ports.contains(Esdc1aData::Ports::bus)) + { + bus_index = excitedata.ports.at(Esdc1aData::Ports::bus); + } + + auto* exciter = new Esdc1a(getBus(bus_index), excitedata); + + if (excitedata.ports.contains(Esdc1aData::Ports::speed)) + { + IdxT speed = excitedata.ports.at(Esdc1aData::Ports::speed); + exciter->getSignals().template attachSignalNode(getSignal(speed)); + } + + if (excitedata.ports.contains(Esdc1aData::Ports::efd)) + { + IdxT efd = excitedata.ports.at(Esdc1aData::Ports::efd); + exciter->getSignals().template assignSignalNode(getSignal(efd)); + } + + if (excitedata.ports.contains(Esdc1aData::Ports::vs)) + { + IdxT vs = excitedata.ports.at(Esdc1aData::Ports::vs); + exciter->getSignals().template attachSignalNode(getSignal(vs)); + } + + if (excitedata.ports.contains(Esdc1aData::Ports::vuel)) + { + IdxT vuel = excitedata.ports.at(Esdc1aData::Ports::vuel); + exciter->getSignals().template attachSignalNode(getSignal(vuel)); + } + + addComponent(exciter); + } + for (const auto& excitedata : data.sexspti) { IdxT bus_index = 0; diff --git a/GridKit/Model/PhasorDynamics/SystemModelData.hpp b/GridKit/Model/PhasorDynamics/SystemModelData.hpp index d4deef66e..67b765c20 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelData.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelData.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ namespace GridKit using BusToSignalAdapterDataT = BusToSignalAdapterData; using BusFaultDataT = BusFaultData; using Tgov1DataT = Governor::Tgov1Data; + using Esdc1aDataT = Exciter::Esdc1aData; using Ieeet1DataT = Exciter::Ieeet1Data; using SexsPtiDataT = Exciter::SexsPtiData; using IeeestDataT = Stabilizer::IeeestData; @@ -99,6 +101,7 @@ namespace GridKit std::vector load; ///< Loads within the model std::vector loadzip; ///< Loads within the model std::vector gov; ///< Governors within the model + std::vector esdc1a; ///< ESDC1A exciters within the model std::vector exciter; ///< Exciters within the model std::vector sexspti; ///< SEXS-PTI exciters within the model std::vector stabilizer; ///< Stabilizers within the model diff --git a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp index 00a6278c2..0b882c9df 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelDataJSONParser.hpp @@ -148,6 +148,12 @@ namespace GridKit raw_component.get_to(exciter); sm.exciter.push_back(exciter); } + else if (kind == "Esdc1a") + { + typename SystemModelData::Esdc1aDataT exciter; + raw_component.get_to(exciter); + sm.esdc1a.push_back(exciter); + } else if (kind == "SexsPti") { typename SystemModelData::SexsPtiDataT exciter; diff --git a/docs/Figures/PhasorDynamics/ESDC1A_diagram.png b/docs/Figures/PhasorDynamics/ESDC1A_diagram.png new file mode 100644 index 000000000..c77a100a6 Binary files /dev/null and b/docs/Figures/PhasorDynamics/ESDC1A_diagram.png differ diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 932cb13b8..451f18966 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -86,6 +86,14 @@ target_link_libraries( GridKit::phasor_dynamics_components_dependency_tracking GridKit::testing) +add_executable(test_phasor_exciter_esdc1a runExciterEsdc1aTests.cpp) +target_link_libraries( + test_phasor_exciter_esdc1a + GridKit::definitions + GridKit::phasor_dynamics_components + GridKit::phasor_dynamics_components_dependency_tracking + GridKit::testing) + add_executable(test_phasor_exciter_sexspti runExciterSexsPtiTests.cpp) target_link_libraries( test_phasor_exciter_sexspti @@ -136,6 +144,7 @@ add_test(NAME PhasorDynamicsBranchTest COMMAND test_phasor_branch) add_test(NAME PhasorDynamicsGenrouTest COMMAND test_phasor_genrou) add_test(NAME PhasorDynamicsGovernorTgov1Test COMMAND test_phasor_governor_tgov1) add_test(NAME PhasorDynamicsExciterIeeet1Test COMMAND test_phasor_exciter_ieeet1) +add_test(NAME PhasorDynamicsExciterEsdc1aTest COMMAND test_phasor_exciter_esdc1a) add_test(NAME PhasorDynamicsGensalTest COMMAND test_phasor_gensal) add_test(NAME PhasorDynamicsExciterSexsPtiTest COMMAND test_phasor_exciter_sexspti) add_test(NAME PhasorDynamicsStabilizerIeeestTest COMMAND test_phasor_stabilizer_ieeest) @@ -157,6 +166,7 @@ install( test_phasor_genrou test_phasor_governor_tgov1 test_phasor_exciter_ieeet1 + test_phasor_exciter_esdc1a test_phasor_gensal test_phasor_exciter_sexspti test_phasor_stabilizer_ieeest diff --git a/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp b/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp new file mode 100644 index 000000000..a2d6a5040 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp @@ -0,0 +1,585 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class ExciterEsdc1aTests + { + public: + using RealT = typename PhasorDynamics::Component::RealT; + + ExciterEsdc1aTests() = default; + ~ExciterEsdc1aTests() = default; + + static constexpr ScalarT kTol = static_cast(1.0e-12); + + TestOutcome constructor() + { + using namespace PhasorDynamics::Exciter; + + TestStatus success = true; + + PhasorDynamics::Bus bus(3.0, 4.0); + + Esdc1a default_exciter(&bus); + success *= (default_exciter.size() == static_cast(Esdc1aInternalVariables::MAXIMUM)); + success *= (default_exciter.getMonitor() == nullptr); + + auto data = makeDefaultData(); + data.monitored_variables.insert(Esdc1aMonitorableVariables::efd); + Esdc1a data_exciter(&bus, data); + success *= (data_exciter.size() == static_cast(Esdc1aInternalVariables::MAXIMUM)); + success *= (data_exciter.getMonitor() != nullptr); + + PhasorDynamics::SignalNode efd_node; + ScalarT efd_value{0.0}; + IdxT efd_index = INVALID_INDEX; + efd_node.set(&efd_value, &efd_index); + + data_exciter.getSignals().template assignSignalNode(&efd_node); + data_exciter.allocate(); + data_exciter.tagDifferentiable(); + + success *= (data_exciter.verify() == 0); + success *= (data_exciter.tag()[idx(Esdc1aInternalVariables::EFDP)] == true); + success *= (data_exciter.tag()[idx(Esdc1aInternalVariables::VC)] == false); + success *= (data_exciter.tag()[idx(Esdc1aInternalVariables::VR)] == true); + success *= (data_exciter.tag()[idx(Esdc1aInternalVariables::VF)] == true); + success *= (data_exciter.tag()[idx(Esdc1aInternalVariables::XLL)] == false); + + return success.report(__func__); + } + + TestOutcome zeroInitialResidual() + { + using namespace PhasorDynamics::Exciter; + + TestStatus success = true; + + Fixture fixture(makeDefaultData()); + success *= fixture.allocateAndInitialize(1.2); + + const auto& f = fixture.exciter.getResidual(); + for (size_t i = 0; i < f.size(); ++i) + { + if (!isEqual(f[i], static_cast(0.0), kTol)) + { + std::cout << "Non-zero ESDC1A residual at index " << i << ": " << f[i] << "\n"; + success = false; + } + } + + success *= fixture.efd_node.linked(); + success *= (fixture.efd_node.getVariableIndex() + == static_cast(idx(Esdc1aInternalVariables::EFD))); + success *= isEqual(fixture.efd_node.read(), static_cast(1.2), kTol); + + return success.report(__func__); + } + + TestOutcome blockDiagramSemantics() + { + TestStatus success = true; + + success *= voltageErrorSummingJunction(); + success *= speedMultiplierSelector(); + success *= leadLagSelector(); + success *= uelRoutingSelector(); + success *= exciterFeedbackLimiter(); + + return success.report(__func__); + } + + TestOutcome parameterValidation() + { + using namespace PhasorDynamics::Exciter; + + TestStatus success = true; + + PhasorDynamics::Bus bus(1.0, 0.0); + PhasorDynamics::SignalNode efd_node; + ScalarT efd_value{0.0}; + IdxT efd_index = INVALID_INDEX; + efd_node.set(&efd_value, &efd_index); + + auto valid = makeDefaultData(); + Esdc1a valid_model(&bus, valid); + valid_model.getSignals().template assignSignalNode(&efd_node); + valid_model.allocate(); + success *= (valid_model.verify() == 0); + + auto invalid_ta = makeDefaultData(); + invalid_ta.parameters[Esdc1aParameters::Ta] = 0.0; + Esdc1a invalid_ta_model(&bus, invalid_ta); + invalid_ta_model.getSignals().template assignSignalNode(&efd_node); + invalid_ta_model.allocate(); + success *= (invalid_ta_model.verify() > 0); + + auto invalid_lead_lag = makeDefaultData(); + invalid_lead_lag.parameters[Esdc1aParameters::Tc] = 0.1; + Esdc1a invalid_lead_lag_model(&bus, invalid_lead_lag); + invalid_lead_lag_model.getSignals().template assignSignalNode(&efd_node); + invalid_lead_lag_model.allocate(); + success *= (invalid_lead_lag_model.verify() > 0); + + auto missing_efd = makeDefaultData(); + Esdc1a missing_efd_model(&bus, missing_efd); + missing_efd_model.allocate(); + success *= (missing_efd_model.verify() > 0); + + auto missing_speed = makeDefaultData(); + missing_speed.parameters[Esdc1aParameters::Spdmlt] = 1.0; + Esdc1a missing_speed_model(&bus, missing_speed); + missing_speed_model.getSignals().template assignSignalNode(&efd_node); + missing_speed_model.allocate(); + success *= (missing_speed_model.verify() > 0); + + return success.report(__func__); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome jacobianStructureAndValues() + { + TestStatus success = true; + + const auto tol = static_cast(1.0e-9); + + auto data = makeDefaultData(); + data.parameters[Params::Spdmlt] = 1.0; + data.parameters[Params::UEL] = static_cast(2); + + auto dependency_tracking_jacobian = dependencyTrackingJacobian(data); + auto enzyme_jacobian = enzymeJacobian(data); + + success *= (dependency_tracking_jacobian.size() == enzyme_jacobian.size()); + for (size_t i = 0; i < dependency_tracking_jacobian.size(); ++i) + { + success *= isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol); + } + + return success.report(__func__); + } +#endif + + private: + using Internal = PhasorDynamics::Exciter::Esdc1aInternalVariables; + using External = PhasorDynamics::Exciter::Esdc1aExternalVariables; + using Params = PhasorDynamics::Exciter::Esdc1aParameters; + using DataT = PhasorDynamics::Exciter::Esdc1aData; + + static size_t idx(Internal variable) + { + return static_cast(variable); + } + + auto makeDefaultData() -> DataT + { + DataT data; + data.device_class = "exciter"; + data.disambiguation_string = "esdc1a_test"; + + data.parameters[Params::Tr] = 0.0; + data.parameters[Params::Ka] = 40.0; + data.parameters[Params::Ta] = 0.1; + data.parameters[Params::Tb] = 0.0; + data.parameters[Params::Tc] = 0.0; + data.parameters[Params::Vrmax] = 1.0; + data.parameters[Params::Vrmin] = -1.0; + data.parameters[Params::Ke] = 0.1; + data.parameters[Params::Te] = 0.5; + data.parameters[Params::Kf] = 0.05; + data.parameters[Params::Tf1] = 0.7; + data.parameters[Params::Spdmlt] = 0.0; + data.parameters[Params::E1] = 2.8; + data.parameters[Params::Se1] = 0.08; + data.parameters[Params::E2] = 3.7; + data.parameters[Params::Se2] = 0.33; + data.parameters[Params::UEL] = static_cast(0); + data.parameters[Params::exclim] = 1.0; + + return data; + } + + struct Fixture + { + using BusT = PhasorDynamics::Bus; + using SignalT = PhasorDynamics::SignalNode; + using ExciterT = PhasorDynamics::Exciter::Esdc1a; + + DataT data; + BusT bus; + SignalT efd_node; + SignalT omega_node; + SignalT vs_node; + SignalT vuel_node; + + ScalarT efd_value{0.0}; + ScalarT omega_value{0.0}; + ScalarT vs_value{0.0}; + ScalarT vuel_value{-2.0}; + + IdxT efd_index{INVALID_INDEX}; + IdxT omega_index{20}; + IdxT vs_index{21}; + IdxT vuel_index{22}; + + ExciterT exciter; + + explicit Fixture(const DataT& data_in) + : data(data_in), + bus(3.0, 4.0), + exciter(&bus, data) + { + efd_node.set(&efd_value, &efd_index); + omega_node.set(&omega_value, &omega_index); + vs_node.set(&vs_value, &vs_index); + vuel_node.set(&vuel_value, &vuel_index); + + exciter.getSignals().template assignSignalNode(&efd_node); + exciter.getSignals().template attachSignalNode(&omega_node); + exciter.getSignals().template attachSignalNode(&vs_node); + exciter.getSignals().template attachSignalNode(&vuel_node); + } + + bool allocateAndInitialize(ScalarT efd0) + { + bus.allocate(); + bus.initialize(); + exciter.allocate(); + efd_node.init(efd0); + return exciter.verify() == 0 + && exciter.initialize() == 0 + && exciter.evaluateResidual() == 0; + } + }; + + bool voltageErrorSummingJunction() + { + Fixture fixture(makeDefaultData()); + if (!fixture.allocateAndInitialize(1.2)) + { + return false; + } + + fixture.vs_value += 0.1; + fixture.exciter.evaluateResidual(); + bool success = fixture.exciter.getResidual()[idx(Internal::EV)] > static_cast(0.0); + + fixture.vs_value -= 0.1; + fixture.exciter.y()[idx(Internal::VC)] += 0.1; + fixture.exciter.evaluateResidual(); + success = success && fixture.exciter.getResidual()[idx(Internal::EV)] < static_cast(0.0); + + fixture.exciter.y()[idx(Internal::VC)] -= 0.1; + fixture.exciter.y()[idx(Internal::VF)] += 0.1; + fixture.exciter.evaluateResidual(); + success = success && fixture.exciter.getResidual()[idx(Internal::EV)] < static_cast(0.0); + + return success; + } + + bool speedMultiplierSelector() + { + auto disabled_data = makeDefaultData(); + Fixture disabled(disabled_data); + if (!disabled.allocateAndInitialize(1.2)) + { + return false; + } + + disabled.omega_value = 0.05; + disabled.exciter.evaluateResidual(); + bool success = isEqual(disabled.exciter.getResidual()[idx(Internal::EFD)], + static_cast(0.0), + kTol); + + auto enabled_data = makeDefaultData(); + enabled_data.parameters[Params::Spdmlt] = 1.0; + Fixture enabled(enabled_data); + if (!enabled.allocateAndInitialize(1.2)) + { + return false; + } + + enabled.omega_value = 0.05; + enabled.exciter.evaluateResidual(); + success = success && enabled.exciter.getResidual()[idx(Internal::EFD)] > static_cast(0.0); + + return success; + } + + bool leadLagSelector() + { + Fixture bypass(makeDefaultData()); + if (!bypass.allocateAndInitialize(1.2)) + { + return false; + } + + bypass.exciter.y()[idx(Internal::VLL)] += 0.1; + bypass.exciter.evaluateResidual(); + bool success = bypass.exciter.getResidual()[idx(Internal::VLL)] < static_cast(0.0); + + auto active_data = makeDefaultData(); + active_data.parameters[Params::Tb] = 0.5; + active_data.parameters[Params::Tc] = 0.2; + Fixture active(active_data); + if (!active.allocateAndInitialize(1.2)) + { + return false; + } + + active.exciter.y()[idx(Internal::EV)] += 0.1; + active.exciter.evaluateResidual(); + success = success && active.exciter.getResidual()[idx(Internal::VLL)] > static_cast(0.0); + + active.exciter.y()[idx(Internal::EV)] -= 0.1; + active.exciter.y()[idx(Internal::VLL)] += 0.1; + active.exciter.evaluateResidual(); + success = success && active.exciter.getResidual()[idx(Internal::VLL)] < static_cast(0.0); + + return success; + } + + bool uelRoutingSelector() + { + Fixture hv_gate(makeDefaultData()); + if (!hv_gate.allocateAndInitialize(1.2)) + { + return false; + } + + hv_gate.vuel_value = hv_gate.exciter.y()[idx(Internal::VLL)] + 0.1; + hv_gate.exciter.evaluateResidual(); + bool success = hv_gate.exciter.getResidual()[idx(Internal::VHV)] > static_cast(0.0); + success = success && isEqual(hv_gate.exciter.getResidual()[idx(Internal::EV)], static_cast(0.0), kTol); + + auto sum_data = makeDefaultData(); + sum_data.parameters[Params::UEL] = static_cast(2); + Fixture sum_junction(sum_data); + sum_junction.vuel_value = 0.0; + if (!sum_junction.allocateAndInitialize(1.2)) + { + return false; + } + + sum_junction.vuel_value = 0.1; + sum_junction.exciter.evaluateResidual(); + success = success && sum_junction.exciter.getResidual()[idx(Internal::EV)] > static_cast(0.0); + success = success && isEqual(sum_junction.exciter.getResidual()[idx(Internal::VHV)], static_cast(0.0), kTol); + + return success; + } + + bool exciterFeedbackLimiter() + { + auto limited_data = makeDefaultData(); + limited_data.parameters[Params::Ke] = -0.2; + limited_data.parameters[Params::Se1] = 0.0; + limited_data.parameters[Params::Se2] = 0.0; + limited_data.parameters[Params::exclim] = 1.0; + Fixture limited(limited_data); + if (!limited.allocateAndInitialize(1.2)) + { + return false; + } + + limited.exciter.y()[idx(Internal::EFDP)] = 1.0; + limited.exciter.y()[idx(Internal::SE)] = 0.0; + limited.exciter.y()[idx(Internal::VFE)] = 0.0; + limited.exciter.evaluateResidual(); + bool success = std::abs(limited.exciter.getResidual()[idx(Internal::VFE)]) < kTol; + + auto unlimited_data = limited_data; + unlimited_data.parameters[Params::exclim] = 0.0; + Fixture unlimited(unlimited_data); + if (!unlimited.allocateAndInitialize(1.2)) + { + return false; + } + + unlimited.exciter.y()[idx(Internal::EFDP)] = 1.0; + unlimited.exciter.y()[idx(Internal::SE)] = 0.0; + unlimited.exciter.y()[idx(Internal::VFE)] = 0.0; + unlimited.exciter.evaluateResidual(); + success = success && unlimited.exciter.getResidual()[idx(Internal::VFE)] < static_cast(0.0); + + return success; + } + +#ifdef GRIDKIT_ENABLE_ENZYME + std::vector + dependencyTrackingJacobian(const DataT& data) + { + using Variable = DependencyTracking::Variable; + + PhasorDynamics::Bus bus(Variable{3.0}, Variable{4.0}); + PhasorDynamics::SignalNode efd_node; + PhasorDynamics::SignalNode omega_node; + PhasorDynamics::SignalNode vs_node; + PhasorDynamics::SignalNode vuel_node; + + Variable efd_value{0.0}; + Variable omega_value{0.0}; + Variable vs_value{0.0}; + Variable vuel_value{0.0}; + + IdxT efd_index = INVALID_INDEX; + IdxT omega_index = 13; + IdxT vs_index = 14; + IdxT vuel_index = 15; + + efd_node.set(&efd_value, &efd_index); + omega_node.set(&omega_value, &omega_index); + vs_node.set(&vs_value, &vs_index); + vuel_node.set(&vuel_value, &vuel_index); + + PhasorDynamics::Exciter::Esdc1a exciter(&bus, data); + exciter.getSignals().template assignSignalNode(&efd_node); + exciter.getSignals().template attachSignalNode(&omega_node); + exciter.getSignals().template attachSignalNode(&vs_node); + exciter.getSignals().template attachSignalNode(&vuel_node); + + bus.allocate(); + exciter.allocate(); + bus.initialize(); + efd_node.init(Variable{1.2}); + exciter.initialize(); + + for (size_t i = 0; i < exciter.size(); ++i) + { + exciter.y()[i].setVariableNumber(i); + } + for (size_t i = 0; i < bus.size(); ++i) + { + bus.y()[i].setVariableNumber(i + exciter.size()); + } + omega_value.setVariableNumber(13); + vs_value.setVariableNumber(14); + vuel_value.setVariableNumber(15); + + bus.evaluateResidual(); + exciter.evaluateResidual(); + auto residual_y = exciter.getResidual(); + + omega_value = 0.0; + vs_value = 0.0; + vuel_value = 0.0; + bus.initialize(); + efd_node.init(Variable{1.2}); + exciter.initialize(); + + for (size_t i = 0; i < exciter.size(); ++i) + { + exciter.yp()[i].setVariableNumber(i); + } + + bus.evaluateResidual(); + exciter.evaluateResidual(); + auto residual_yp = exciter.getResidual(); + + std::vector dependencies(residual_y.size()); + for (IdxT i = 0; i < residual_y.size(); ++i) + { + auto dependency_y = residual_y[static_cast(i)].getDependencies(); + auto dependency_yp = residual_yp[static_cast(i)].getDependencies(); + + for (const auto& pair_y : dependency_y) + { + auto index_y = pair_y.first; + auto value_y = pair_y.second; + auto it_yp = dependency_yp.find(index_y); + if (it_yp != dependency_yp.end()) + { + dependencies[static_cast(i)].insert(std::make_pair(index_y, value_y + it_yp->second)); + } + else + { + dependencies[static_cast(i)].insert(pair_y); + } + } + + for (const auto& pair_yp : dependency_yp) + { + if (dependency_y.find(pair_yp.first) == dependency_y.end()) + { + dependencies[static_cast(i)].insert(pair_yp); + } + } + } + + return dependencies; + } + + std::vector + enzymeJacobian(const DataT& data) + { + PhasorDynamics::Bus bus(3.0, 4.0); + PhasorDynamics::SignalNode efd_node; + PhasorDynamics::SignalNode omega_node; + PhasorDynamics::SignalNode vs_node; + PhasorDynamics::SignalNode vuel_node; + + ScalarT efd_value{0.0}; + ScalarT omega_value{0.0}; + ScalarT vs_value{0.0}; + ScalarT vuel_value{0.0}; + + IdxT efd_index = INVALID_INDEX; + IdxT omega_index = 13; + IdxT vs_index = 14; + IdxT vuel_index = 15; + + efd_node.set(&efd_value, &efd_index); + omega_node.set(&omega_value, &omega_index); + vs_node.set(&vs_value, &vs_index); + vuel_node.set(&vuel_value, &vuel_index); + + PhasorDynamics::Exciter::Esdc1a exciter(&bus, data); + exciter.getSignals().template assignSignalNode(&efd_node); + exciter.getSignals().template attachSignalNode(&omega_node); + exciter.getSignals().template attachSignalNode(&vs_node); + exciter.getSignals().template attachSignalNode(&vuel_node); + + bus.allocate(); + exciter.allocate(); + bus.initialize(); + efd_node.init(1.2); + exciter.initialize(); + exciter.updateTime(0.0, 1.0); + + for (size_t i = 0; i < bus.size(); ++i) + { + bus.setVariableIndex(i, static_cast(i + exciter.size())); + bus.setResidualIndex(i, static_cast(i + exciter.size())); + } + + bus.evaluateResidual(); + exciter.evaluateResidual(); + exciter.evaluateJacobian(); + + auto& model_jacobian = exciter.getJacobian(); + model_jacobian.deduplicate(); + + return MapFromCOO(model_jacobian); + } +#endif + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/runExciterEsdc1aTests.cpp b/tests/UnitTests/PhasorDynamics/runExciterEsdc1aTests.cpp new file mode 100644 index 000000000..04c40b955 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runExciterEsdc1aTests.cpp @@ -0,0 +1,18 @@ +#include "ExciterEsdc1aTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + + GridKit::Testing::ExciterEsdc1aTests test; + + result += test.constructor(); + result += test.zeroInitialResidual(); + result += test.blockDiagramSemantics(); + result += test.parameterValidation(); +#ifdef GRIDKIT_ENABLE_ENZYME + result += test.jacobianStructureAndValues(); +#endif + + return result.summary(); +}