diff --git a/CHANGELOG.md b/CHANGELOG.md index ed853112cd..34deae83fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Removed `BusToSignalAdapter`. Prefer `BusSignalVoltageOut` or `BusSignalVoltageIn`. - Added `Bus::setFault` to apply or clear a fault to ground directly at a bus. - Removed the `BusFault` class in favor of `Bus::setFault`. +- `verify()` returns `Model::ConfigurationChecks` listing every configuration problem instead of an error count. +- Added `Model::ParameterReader` ## v0.2 diff --git a/GridKit/Model/CMakeLists.txt b/GridKit/Model/CMakeLists.txt index af94b489f4..a28d08def9 100644 --- a/GridKit/Model/CMakeLists.txt +++ b/GridKit/Model/CMakeLists.txt @@ -9,5 +9,10 @@ add_subdirectory(PhasorDynamics) add_subdirectory(PowerFlow) add_subdirectory(PowerElectronics) -install(FILES Evaluator.hpp VariableMonitor.hpp VariableMonitorController.hpp - DESTINATION include/GridKit/Model) +install( + FILES ConfigurationChecks.hpp + Evaluator.hpp + ParameterReader.hpp + VariableMonitor.hpp + VariableMonitorController.hpp + DESTINATION include/GridKit/Model) diff --git a/GridKit/Model/ConfigurationChecks.hpp b/GridKit/Model/ConfigurationChecks.hpp new file mode 100644 index 0000000000..9cc55df1e5 --- /dev/null +++ b/GridKit/Model/ConfigurationChecks.hpp @@ -0,0 +1,68 @@ +/** + * @file ConfigurationChecks.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Collects the configuration problems found by a model. + */ + +#pragma once + +#include +#include +#include +#include + +namespace GridKit +{ + namespace Model + { + /** + * @brief Collects the configuration problems found by a model. + * + * A model's verify() fills one instance with a message for every + * condition that does not hold and returns it. The caller decides how + * the messages are reported; passed() is true when there are none. + */ + class [[nodiscard]] ConfigurationChecks + { + public: + ConfigurationChecks() = default; + + /// @param[in] model Model name that prefixes every recorded message. + explicit ConfigurationChecks(std::string_view model) + : prefix_(std::format("{}: ", model)) + { + } + + /// Record a problem. + void fail(std::string_view message) + { + errors_.push_back(prefix_ + std::string(message)); + } + + /// Record the message when the condition does not hold. + void check(bool condition, std::string_view message) + { + if (!condition) + { + fail(message); + } + } + + /// True when no check has failed. + bool passed() const + { + return errors_.empty(); + } + + /// Every recorded message, in the order the checks ran. + const std::vector& errors() const + { + return errors_; + } + + private: + std::string prefix_; + std::vector errors_; + }; + } // namespace Model +} // namespace GridKit diff --git a/GridKit/Model/ParameterReader.hpp b/GridKit/Model/ParameterReader.hpp new file mode 100644 index 0000000000..1d0cac7c3f --- /dev/null +++ b/GridKit/Model/ParameterReader.hpp @@ -0,0 +1,154 @@ +/** + * @file ParameterReader.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Typed parameter loading from model data containers. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace GridKit +{ + namespace Model + { + /** + * @brief Reads typed parameters out of a model data container. + * + * A read method returns the provided value or std::nullopt when the + * parameter is omitted. The model chooses a default or requires a value. + * Invalid values are rejected with std::invalid_argument naming the + * model and parameter. Real parameters accept integer values and must + * be finite. + * + * @tparam ModelDataT A model data container satisfying + * PhasorDynamics::ModelData. + */ + template + class ParameterReader + { + public: + using RealT = typename ModelDataT::RealT; + using IdxT = typename ModelDataT::IdxT; + using ParametersT = typename ModelDataT::Parameters; + + /** + * @param[in] data Model data container to read from. + * @param[in] model Model name used in rejection messages. + */ + ParameterReader(const ModelDataT& data, const char* model) + : parameters_(data.parameters), + model_(model) + { + } + + /// Read an optional real parameter. + [[nodiscard]] std::optional readReal(ParametersT key) const + { + const auto* value = find(key); + if (value == nullptr) + { + return std::nullopt; + } + + RealT real{}; + if (const auto* real_value = std::get_if(value)) + { + real = *real_value; + } + else if (const auto* integer_value = std::get_if(value)) + { + real = static_cast(*integer_value); + } + else + { + reject(key, "must be numeric"); + } + + if (!std::isfinite(real)) + { + reject(key, "must be finite"); + } + + return real; + } + + /// Read an optional non-negative real parameter. + [[nodiscard]] std::optional readNonNegative(ParametersT key) const + { + auto value = readReal(key); + if (value && *value < ZERO) + { + reject(key, "must be non-negative"); + } + return value; + } + + /// Read an optional boolean parameter. + [[nodiscard]] std::optional readBool(ParametersT key) const + { + return readExact(key, "must be boolean"); + } + + /// Read an optional integer parameter. + [[nodiscard]] std::optional readInteger(ParametersT key) const + { + return readExact(key, "must be an integer"); + } + + private: + using ParameterMapT = decltype(ModelDataT::parameters); + using ValueT = typename ParameterMapT::mapped_type; + + /// The provided value, or nullptr when the parameter is omitted. + const ValueT* find(ParametersT key) const + { + const auto entry = parameters_.find(key); + if (entry == parameters_.end()) + { + return nullptr; + } + return &entry->second; + } + + /// Read an optional parameter that must hold exactly a T. + template + [[nodiscard]] std::optional readExact(ParametersT key, std::string_view reason) const + { + const auto* value = find(key); + if (value == nullptr) + { + return std::nullopt; + } + + const auto* exact_value = std::get_if(value); + if (exact_value == nullptr) + { + reject(key, reason); + } + + return *exact_value; + } + + /// Reject the model data with a message naming the model and parameter. + [[noreturn]] void reject(ParametersT key, std::string_view reason) const + { + throw std::invalid_argument( + std::format("{}: parameter '{}' {}", model_, magic_enum::enum_name(key), reason)); + } + + const ParameterMapT& parameters_; + const char* model_; + }; + } // namespace Model +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp index 82a5144a21..022dc16ea9 100644 --- a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp @@ -72,7 +72,8 @@ namespace GridKit virtual int setAbsoluteTolerance(RealT rel_tol) override final; virtual int evaluateResidual() override final; virtual int evaluateJacobian() override final; - virtual int verify() const override final; + + virtual Model::ConfigurationChecks verify() const override final; void setR(RealT R) { diff --git a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp index f7b1391f17..e2d5f3911c 100644 --- a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp @@ -154,34 +154,27 @@ namespace GridKit } template - int Branch::verify() const + Model::ConfigurationChecks Branch::verify() const { - int ret = parameter_error_count_; + Model::ConfigurationChecks checks("Branch"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Branch: " << message << '\n'; - ret += 1; - } - }; - - check(bus1_ != nullptr, "bus1 pointer is null"); - check(bus2_ != nullptr, "bus2 pointer is null"); - - check(std::isfinite(R_), "R must be finite"); - check(std::isfinite(X_), "X must be finite"); - check(std::isfinite(G_), "G must be finite"); - check(std::isfinite(B_), "B must be finite"); - check(std::isfinite(Gmag_), "Gmag must be finite"); - check(std::isfinite(Bmag_), "Bmag must be finite"); - check(std::isfinite(tap_), "tap must be finite"); - check(std::isfinite(phase_), "phase must be finite"); - check(R_ * R_ + X_ * X_ > RealT{0.0}, "R and X cannot both be zero"); - check(tap_ > RealT{0.0}, "tap must be positive"); - - return ret; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); + + checks.check(bus1_ != nullptr, "bus1 pointer is null"); + checks.check(bus2_ != nullptr, "bus2 pointer is null"); + + checks.check(std::isfinite(R_), "R must be finite"); + checks.check(std::isfinite(X_), "X must be finite"); + checks.check(std::isfinite(G_), "G must be finite"); + checks.check(std::isfinite(B_), "B must be finite"); + checks.check(std::isfinite(Gmag_), "Gmag must be finite"); + checks.check(std::isfinite(Bmag_), "Bmag must be finite"); + checks.check(std::isfinite(tap_), "tap must be finite"); + checks.check(std::isfinite(phase_), "phase must be finite"); + checks.check(R_ * R_ + X_ * X_ > RealT{0.0}, "R and X cannot both be zero"); + checks.check(tap_ > RealT{0.0}, "tap must be positive"); + + return checks; } template diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp index eb39bad52e..ac0f7ee800 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageIn.hpp @@ -24,7 +24,7 @@ namespace GridKit * components _Vr_ and _Vi_ are read directly from signal inlets * `vr` and `vi` whenever Vr() or Vi() is called; the bus stores no * voltage of its own and never modifies it. Both voltage inlets are - * mandatory: verify() logs an error and throws for an inlet that is not + * mandatory: verify() reports an error for an inlet that is not * connected to a linked signal, and reading the voltage through an * unlinked inlet throws. No default voltage is ever used. The bus has * no unknowns and no equations (size() == 0, like @ref BusInfinite). @@ -70,13 +70,14 @@ namespace GridKit virtual int setBusID(IdxT) override final; virtual int allocate() override final; - virtual int verify() const override final; virtual int tagDifferentiable() override final; virtual int setAbsoluteTolerance(RealT rel_tol) override final; virtual int initialize() override final; virtual int evaluateResidual() override final; virtual int evaluateJacobian() override final; + virtual Model::ConfigurationChecks verify() const override final; + virtual BusTypeT BusType() const override final { return BusTypeT::SIGNAL_VOLTAGE_IN; diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp index 0ca026ebb9..ac74072e3b 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/BusSignalVoltageInImpl.hpp @@ -5,6 +5,7 @@ */ #include +#include #include #include @@ -112,29 +113,23 @@ namespace GridKit * Both voltage inlets `vr` and `vi` are mandatory, since the bus has no * voltage of its own and no default value is allowed. * - * @throws std::runtime_error if any port fails the check. Each problem - * is logged before throwing. - * - * @return 0 (an error is reported by throwing). + * @return The configuration checks; passed() when valid. */ template - int BusSignalVoltageIn::verify() const + Model::ConfigurationChecks BusSignalVoltageIn::verify() const { - int errors = 0; + Model::ConfigurationChecks checks("BusSignalVoltageIn"); auto check_input = [&](const char* name) { const auto& port = ports_.in.template port(); if (!port.connected()) { - Log::error() << "BusSignalVoltageIn: " << name - << " signal inlet is not connected; a default voltage is not allowed\n"; - errors += 1; + checks.fail(std::format("{} signal inlet is not connected; a default voltage is not allowed", name)); } else if (!port.linked()) { - Log::error() << "BusSignalVoltageIn: " << name << " signal attached with no linked source\n"; - errors += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -143,9 +138,7 @@ namespace GridKit const auto& port = ports_.out.template port(); if (port.connected() && !port.linked()) { - Log::error() << "BusSignalVoltageIn: " << name - << " signal attached but not linked; connect signal ports before allocate()\n"; - errors += 1; + checks.fail(std::format("{} signal attached but not linked; connect signal ports before allocate()", name)); } }; @@ -154,12 +147,7 @@ namespace GridKit check_output.template operator()("Ir"); check_output.template operator()("Ii"); - if (errors > 0) - { - throw std::runtime_error("BusSignalVoltageIn: signal ports are not correctly connected"); - } - - return 0; + return checks; } /** diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md index ce228b85c0..670e40c77e 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageIn/README.md @@ -9,8 +9,8 @@ The bus stores no voltage of its own and has no unknowns and no equations. - Signal ports must be connected before `allocate()` is called. The signal outlets are linked to the current sums in `allocate()`. -- Both voltage inlets are mandatory. `verify()` logs each problem and - throws if a voltage inlet is not connected or not linked, or if a +- Both voltage inlets are mandatory. `verify()` reports an error if a + voltage inlet is not connected or not linked, or if a connected outlet is not linked. Reading the voltage through an unlinked inlet throws. No default voltage is ever used. - The current sums are complete only after all attached components have diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp index f3c9855c8d..553fb73b65 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOut.hpp @@ -23,7 +23,7 @@ namespace GridKit * - publishes _Vr_ and _Vi_ on signal outlets `vr` and `vi`, and * - sets its residuals f[0] and f[1] to the current injections read from * signal inlets `ir` and `ii`, respectively. Both inlets are - * mandatory: verify() throws if either is not connected to a linked + * mandatory: verify() reports an error if either is not connected to a linked * signal, and no default value is ever used. * * Components attached to the bus directly (without signals) keep adding @@ -67,13 +67,14 @@ namespace GridKit virtual int setBusID(IdxT) override final; virtual int allocate() override final; - virtual int verify() const override final; virtual int tagDifferentiable() override final; virtual int setAbsoluteTolerance(RealT rel_tol) override final; virtual int initialize() override final; virtual int evaluateResidual() override final; virtual int evaluateJacobian() override final; + virtual Model::ConfigurationChecks verify() const override final; + virtual BusTypeT BusType() const override final { return BusTypeT::SIGNAL_VOLTAGE_OUT; diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp index 105526ae51..d1e3e5b88c 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/BusSignalVoltageOutImpl.hpp @@ -5,6 +5,7 @@ */ #include +#include #include #include @@ -131,29 +132,23 @@ namespace GridKit * Both current inlets `ir` and `ii` are mandatory, since the residuals * are set from them and no default value is allowed. * - * @throws std::runtime_error if any port fails the check. Each problem - * is logged before throwing. - * - * @return 0 (an error is reported by throwing). + * @return The configuration checks; passed() when valid. */ template - int BusSignalVoltageOut::verify() const + Model::ConfigurationChecks BusSignalVoltageOut::verify() const { - int errors = 0; + Model::ConfigurationChecks checks("BusSignalVoltageOut"); auto check_input = [&](const char* name) { const auto& port = ports_.in.template port(); if (!port.connected()) { - Log::error() << "BusSignalVoltageOut: " << name - << " signal inlet is not connected; a default current is not allowed\n"; - errors += 1; + checks.fail(std::format("{} signal inlet is not connected; a default current is not allowed", name)); } else if (!port.linked()) { - Log::error() << "BusSignalVoltageOut: " << name << " signal attached with no linked source\n"; - errors += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -162,9 +157,7 @@ namespace GridKit const auto& port = ports_.out.template port(); if (port.connected() && !port.linked()) { - Log::error() << "BusSignalVoltageOut: " << name - << " signal attached but not linked; connect signal ports before allocate()\n"; - errors += 1; + checks.fail(std::format("{} signal attached but not linked; connect signal ports before allocate()", name)); } }; @@ -173,12 +166,7 @@ namespace GridKit check_output.template operator()("Vr"); check_output.template operator()("Vi"); - if (errors > 0) - { - throw std::runtime_error("BusSignalVoltageOut: signal ports are not correctly connected"); - } - - return 0; + return checks; } /** @@ -247,7 +235,7 @@ namespace GridKit * * Residuals f[0] and f[1] are set to the values read from the `ir` and * `ii` signal inlets, respectively. Both inlets are mandatory; verify() - * throws if either is not connected to a linked signal, and no default + * reports an error if either is not connected to a linked signal, and no default * value is used here. Components attached to the bus add their currents * afterwards. * diff --git a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md index 5224654e2a..83642e8d0d 100644 --- a/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md +++ b/GridKit/Model/PhasorDynamics/Bus/BusSignalVoltageOut/README.md @@ -11,8 +11,8 @@ the bus residual is evaluated. - Signal ports must be connected before `allocate()` is called. The signal outlets are linked to the bus voltage variables and their system indices in `allocate()`. -- Both current inlets are mandatory. `verify()` logs each problem and - throws if a current inlet is not connected or not linked, or if a +- Both current inlets are mandatory. `verify()` reports an error if a + current inlet is not connected or not linked, or if a connected outlet is not linked. No default current is ever used. - Current entering the bus has positive sign. diff --git a/GridKit/Model/PhasorDynamics/BusBase.hpp b/GridKit/Model/PhasorDynamics/BusBase.hpp index cd447b6597..6dec007193 100644 --- a/GridKit/Model/PhasorDynamics/BusBase.hpp +++ b/GridKit/Model/PhasorDynamics/BusBase.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -40,9 +41,10 @@ namespace GridKit virtual ~BusBase(); - virtual int verify() const + /// Report every configuration problem; passed() when the bus can initialize. + virtual Model::ConfigurationChecks verify() const { - return 0; + return {}; } IdxT size() override final diff --git a/GridKit/Model/PhasorDynamics/Component.hpp b/GridKit/Model/PhasorDynamics/Component.hpp index 4bc4051ad6..25239263aa 100644 --- a/GridKit/Model/PhasorDynamics/Component.hpp +++ b/GridKit/Model/PhasorDynamics/Component.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -61,7 +62,11 @@ namespace GridKit } } - virtual int verify() const = 0; + /// Report configuration problems. Override when the component has checks. + virtual Model::ConfigurationChecks verify() const + { + return {}; + } IdxT size() override final { @@ -280,6 +285,21 @@ namespace GridKit } protected: + /** + * @brief Log every problem verify() reports. + * + * @return true when the configuration has no problems. + */ + [[nodiscard]] bool verifyAndLog() const + { + const auto checks = verify(); + for (const auto& error : checks.errors()) + { + Log::error() << error << '\n'; + } + return checks.passed(); + } + void setComponentBase(RealT va_component_base) { va_component_base_ = va_component_base; diff --git a/GridKit/Model/PhasorDynamics/Controller/REECB/Reecb.hpp b/GridKit/Model/PhasorDynamics/Controller/REECB/Reecb.hpp index 6ec4bb77cf..20e9e618be 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REECB/Reecb.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REECB/Reecb.hpp @@ -116,13 +116,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Controller/REECB/ReecbImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REECB/ReecbImpl.hpp index 5917d14b22..bec915f642 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REECB/ReecbImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REECB/ReecbImpl.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -158,113 +159,105 @@ namespace GridKit * attached optional signals. Operating-point feasibility is checked by * initialize(). * - * @return Number of configuration errors; zero when valid. + * @return The configuration checks; passed() when valid. */ template - int Reecb::verify() const + Model::ConfigurationChecks Reecb::verify() const { - int ret = static_cast(parameter_error_count_); + Model::ConfigurationChecks checks("Reecb"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Reecb: " << message << '\n'; - ret += 1; - } - }; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); - check(bus_ != nullptr, "terminal bus is required"); + checks.check(bus_ != nullptr, "terminal bus is required"); const bool valid_component_base = std::isfinite(va_component_base_) && va_component_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - check(valid_component_base, "component power base must be finite and positive"); - check(valid_system_base, "system power base must be finite and positive"); + checks.check(valid_component_base, "component power base must be finite and positive"); + checks.check(valid_system_base, "system power base must be finite and positive"); if (valid_component_base && valid_system_base) { const RealT system_to_component = va_system_base_ / va_component_base_; const RealT component_to_system = va_component_base_ / va_system_base_; - check(std::isfinite(system_to_component) - && system_to_component > ZERO - && std::isfinite(component_to_system) - && component_to_system > ZERO, - "system/component power-base conversion ratios must be finite and positive"); + checks.check(std::isfinite(system_to_component) + && system_to_component > ZERO + && std::isfinite(component_to_system) + && component_to_system > ZERO, + "system/component power-base conversion ratios must be finite and positive"); } - check(std::isfinite(Trv_), "Trv must be finite"); - check(std::isfinite(Tp_), "Tp must be finite"); - check(std::isfinite(Vref0_), "Vref0 must be finite"); + checks.check(std::isfinite(Trv_), "Trv must be finite"); + checks.check(std::isfinite(Tp_), "Tp must be finite"); + checks.check(std::isfinite(Vref0_), "Vref0 must be finite"); const bool finite_voltage_thresholds = std::isfinite(Vdip_) && std::isfinite(Vup_); - check(finite_voltage_thresholds, "Vdip and Vup must be finite"); + checks.check(finite_voltage_thresholds, "Vdip and Vup must be finite"); if (finite_voltage_thresholds) { - check(Vdip_ < Vup_, "Vdip must be less than Vup"); + checks.check(Vdip_ < Vup_, "Vdip must be less than Vup"); } const bool finite_voltage_deadband = std::isfinite(dbd1_) && std::isfinite(dbd2_); - check(finite_voltage_deadband, "dbd1 and dbd2 must be finite"); + checks.check(finite_voltage_deadband, "dbd1 and dbd2 must be finite"); if (finite_voltage_deadband) { - check(dbd1_ <= ZERO && ZERO <= dbd2_, "dbd1 <= 0 <= dbd2 is required"); + checks.check(dbd1_ <= ZERO && ZERO <= dbd2_, "dbd1 <= 0 <= dbd2 is required"); } - check(std::isfinite(kqv_) && kqv_ >= ZERO, "kqv must be finite and non-negative"); + checks.check(std::isfinite(kqv_) && kqv_ >= ZERO, "kqv must be finite and non-negative"); const bool finite_injection_limits = std::isfinite(Iql1_) && std::isfinite(Iqh1_); - check(finite_injection_limits, "Iql1 and Iqh1 must be finite"); + checks.check(finite_injection_limits, "Iql1 and Iqh1 must be finite"); if (finite_injection_limits) { - check(Iql1_ <= Iqh1_, "Iql1 must be less than or equal to Iqh1"); + checks.check(Iql1_ <= Iqh1_, "Iql1 must be less than or equal to Iqh1"); } const bool finite_reactive_limits = std::isfinite(Qmin_) && std::isfinite(Qmax_); - check(finite_reactive_limits, "Qmin and Qmax must be finite"); + checks.check(finite_reactive_limits, "Qmin and Qmax must be finite"); if (finite_reactive_limits) { - check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); + checks.check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); } - check(std::isfinite(Kqp_) && Kqp_ >= ZERO, "Kqp must be finite and non-negative"); - check(std::isfinite(Kqi_) && Kqi_ >= ZERO, "Kqi must be finite and non-negative"); + checks.check(std::isfinite(Kqp_) && Kqp_ >= ZERO, "Kqp must be finite and non-negative"); + checks.check(std::isfinite(Kqi_) && Kqi_ >= ZERO, "Kqi must be finite and non-negative"); const bool finite_voltage_limits = std::isfinite(Vmin_) && std::isfinite(Vmax_); - check(finite_voltage_limits, "Vmin and Vmax must be finite"); + checks.check(finite_voltage_limits, "Vmin and Vmax must be finite"); if (finite_voltage_limits) { - check(Vmin_ <= Vmax_, "Vmin must be less than or equal to Vmax"); + checks.check(Vmin_ <= Vmax_, "Vmin must be less than or equal to Vmax"); } - check(std::isfinite(Kvp_) && Kvp_ >= ZERO, "Kvp must be finite and non-negative"); - check(std::isfinite(Kvi_) && Kvi_ >= ZERO, "Kvi must be finite and non-negative"); - check(std::isfinite(Tiq_), "Tiq must be finite"); - check(std::isfinite(Tpord_), "Tpord must be finite"); + checks.check(std::isfinite(Kvp_) && Kvp_ >= ZERO, "Kvp must be finite and non-negative"); + checks.check(std::isfinite(Kvi_) && Kvi_ >= ZERO, "Kvi must be finite and non-negative"); + checks.check(std::isfinite(Tiq_), "Tiq must be finite"); + checks.check(std::isfinite(Tpord_), "Tpord must be finite"); const bool finite_ramp_limits = std::isfinite(dPmin_) && std::isfinite(dPmax_); - check(finite_ramp_limits, "dPmin and dPmax must be finite"); + checks.check(finite_ramp_limits, "dPmin and dPmax must be finite"); if (finite_ramp_limits) { - check(dPmin_ < ZERO && ZERO < dPmax_, "dPmin < 0 < dPmax is required"); + checks.check(dPmin_ < ZERO && ZERO < dPmax_, "dPmin < 0 < dPmax is required"); } const bool finite_active_limits = std::isfinite(Pmin_) && std::isfinite(Pmax_); - check(finite_active_limits, "Pmin and Pmax must be finite"); + checks.check(finite_active_limits, "Pmin and Pmax must be finite"); if (finite_active_limits) { - check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); + checks.check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); } - check(std::isfinite(Imax_) && Imax_ > ZERO, "Imax must be finite and positive"); + checks.check(std::isfinite(Imax_) && Imax_ > ZERO, "Imax must be finite and positive"); auto check_optional_signal = [&](const char* name) { if (ports_.in.template port().connected() && !ports_.in.template port().linked()) { - Log::error() << "Reecb: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -274,7 +267,7 @@ namespace GridKit check_optional_signal.template operator()("pfaref"); check_optional_signal.template operator()("pref"); - return ret; + return checks; } /** @@ -303,7 +296,7 @@ namespace GridKit return 1; } - if (verify() > 0) + if (!this->verifyAndLog()) { Log::error() << "Reecb: cannot initialize with invalid configuration\n"; return 1; diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp index 16f9af8de6..78a7bae669 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/Repca.hpp @@ -121,13 +121,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp index 1e266055db..e61dc0d579 100644 --- a/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Controller/REPCA/RepcaImpl.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -142,23 +143,16 @@ namespace GridKit * bus, required measurement signals, and attached optional reference * signals. Command-output assignment is optional. * - * @return Number of configuration errors; zero when valid. + * @return The configuration checks; passed() when valid. */ template - int Repca::verify() const + Model::ConfigurationChecks Repca::verify() const { - int ret = static_cast(parameter_error_count_); + Model::ConfigurationChecks checks("Repca"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Repca: " << message << '\n'; - ret += 1; - } - }; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); - check(bus_ != nullptr, "regulated bus is required"); + checks.check(bus_ != nullptr, "regulated bus is required"); const bool valid_component_base = std::isfinite(mva_base_) && mva_base_ > ZERO @@ -166,44 +160,42 @@ namespace GridKit && va_component_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - check(valid_component_base, - "mva must define a finite positive component power base"); - check(valid_system_base, "system power base must be finite and positive"); + checks.check(valid_component_base, + "mva must define a finite positive component power base"); + checks.check(valid_system_base, "system power base must be finite and positive"); if (valid_component_base && valid_system_base) { const RealT system_to_component = va_system_base_ / va_component_base_; const RealT component_to_system = va_component_base_ / va_system_base_; - check(std::isfinite(system_to_component) - && system_to_component > ZERO - && std::isfinite(component_to_system) - && component_to_system > ZERO, - "system/component power-base conversion ratios must be finite and positive"); + checks.check(std::isfinite(system_to_component) + && system_to_component > ZERO + && std::isfinite(component_to_system) + && component_to_system > ZERO, + "system/component power-base conversion ratios must be finite and positive"); } - check(dbdlow_ <= ZERO && ZERO <= dbdupper_, - "dbdlow <= 0 <= dbdupper is required"); - check(emin_ <= ZERO && ZERO <= emax_, - "emin <= 0 <= emax is required"); - check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); - check(fdbd1_ <= ZERO && ZERO <= fdbd2_, - "fdbd1 <= 0 <= fdbd2 is required"); - check(Ddn_ >= ZERO, "Ddn must be non-negative"); - check(Dup_ >= ZERO, "Dup must be non-negative"); - check(femin_ <= ZERO && ZERO <= femax_, - "femin <= 0 <= femax is required"); - check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); + checks.check(dbdlow_ <= ZERO && ZERO <= dbdupper_, + "dbdlow <= 0 <= dbdupper is required"); + checks.check(emin_ <= ZERO && ZERO <= emax_, + "emin <= 0 <= emax is required"); + checks.check(Qmin_ <= Qmax_, "Qmin must be less than or equal to Qmax"); + checks.check(fdbd1_ <= ZERO && ZERO <= fdbd2_, + "fdbd1 <= 0 <= fdbd2 is required"); + checks.check(Ddn_ >= ZERO, "Ddn must be non-negative"); + checks.check(Dup_ >= ZERO, "Dup must be non-negative"); + checks.check(femin_ <= ZERO && ZERO <= femax_, + "femin <= 0 <= femax is required"); + checks.check(Pmin_ <= Pmax_, "Pmin must be less than or equal to Pmax"); auto check_required_signal = [&](const char* name) { if (!ports_.in.template port().connected()) { - Log::error() << "Repca: " << name << " signal is required\n"; - ret += 1; + checks.fail(std::format("{} signal is required", name)); } else if (!ports_.in.template port().linked()) { - Log::error() << "Repca: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -217,8 +209,7 @@ namespace GridKit if (ports_.in.template port().connected() && !ports_.in.template port().linked()) { - Log::error() << "Repca: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -228,7 +219,7 @@ namespace GridKit check_optional_signal.template operator()("freq"); check_optional_signal.template operator()("freqref"); - return ret; + return checks; } /** @@ -282,7 +273,7 @@ namespace GridKit return 1; } - if (verify() > 0) + if (!this->verifyAndLog()) { Log::error() << "Repca: cannot initialize with invalid configuration\n"; return 1; diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp index bb3ab989c4..61f23966d6 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/Regca.hpp @@ -104,13 +104,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp index cf585b6bd0..43d5b6a144 100644 --- a/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Converter/REGCA/RegcaImpl.hpp @@ -367,43 +367,34 @@ namespace GridKit * terminal bus, and checks that attached command ports have linked sources. * Operating-point admissibility is checked by initialize(). * - * @return Number of configuration errors, zero when valid. + * @return The configuration checks; passed() when valid. */ template - int Regca::verify() const + Model::ConfigurationChecks Regca::verify() const { - int ret = static_cast(parameter_error_count_); + Model::ConfigurationChecks checks("Regca"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Regca: " << message << '\n'; - ret += 1; - } - }; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); if (bus_ == nullptr) { - Log::error() << "Regca: bus pointer is null\n"; - ret += 1; + checks.fail("bus pointer is null"); } - check(mva_base_ > ZERO, "mva must be positive"); - check(Rpmax_ >= ZERO, "Rpmax must be non-negative"); - check(IL1_ >= ZERO, "IL1 must be non-negative"); - check(std::isfinite(Khv_) && Khv_ >= ZERO, - "Khv must be finite and non-negative"); - check(ZERO <= VL0_ && VL0_ < VL1_, "VL0/VL1 must satisfy 0 <= VL0 < VL1"); - check(ZERO <= VA0_ && VA0_ < VA1_ && VA1_ < Vhvmax_, - "VA0/VA1/Vhvmax must satisfy 0 <= VA0 < VA1 < Vhvmax"); + checks.check(mva_base_ > ZERO, "mva must be positive"); + checks.check(Rpmax_ >= ZERO, "Rpmax must be non-negative"); + checks.check(IL1_ >= ZERO, "IL1 must be non-negative"); + checks.check(std::isfinite(Khv_) && Khv_ >= ZERO, + "Khv must be finite and non-negative"); + checks.check(ZERO <= VL0_ && VL0_ < VL1_, "VL0/VL1 must satisfy 0 <= VL0 < VL1"); + checks.check(ZERO <= VA0_ && VA0_ < VA1_ && VA1_ < Vhvmax_, + "VA0/VA1/Vhvmax must satisfy 0 <= VA0 < VA1 < Vhvmax"); if (ports_.in.template port()) { if (!ports_.in.template port().linked()) { - Log::error() << "Regca: ipcmd signal attached with no linked source\n"; - ret += 1; + checks.fail("ipcmd signal attached with no linked source"); } } @@ -411,12 +402,11 @@ namespace GridKit { if (!ports_.in.template port().linked()) { - Log::error() << "Regca: iqcmd signal attached with no linked source\n"; - ret += 1; + checks.fail("iqcmd signal attached with no linked source"); } } - return ret; + return checks; } /** diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp index 6d70cb1a94..7e9fba6328 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1a.hpp @@ -99,13 +99,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; @@ -167,8 +168,6 @@ namespace GridKit RealT SB_{0}; RealT Ke_eff_{Ke_}; - IdxT parameter_error_count_{0}; - ScalarT omega_set_{0}; ScalarT vref_set_{0}; ScalarT vs_set_{0}; diff --git a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp index 505fc83d7f..086f11ae56 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/ESDC1A/Esdc1aImpl.hpp @@ -8,8 +8,10 @@ #include #include -#include +#include +#include +#include #include #include #include @@ -135,37 +137,27 @@ namespace GridKit /** * @brief Validate the ESDC1A configuration * - * Checks parameter-loading errors, static parameter relationships, - * terminal-bus association, the required field-voltage output, and - * attached external signals. Seed feasibility is operating-point - * dependent and is checked by initialize(). + * Checks static parameter relationships, terminal-bus association, the + * required field-voltage output, and attached external signals. Seed + * feasibility is operating-point dependent and is checked by + * initialize(). * - * @return Number of configuration errors; zero when valid. + * @return The configuration checks; passed() when valid. */ template - int Esdc1a::verify() const + Model::ConfigurationChecks Esdc1a::verify() const { - int ret = static_cast(parameter_error_count_); - - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Esdc1a: " << message << '\n'; - ret += 1; - } - }; + Model::ConfigurationChecks checks("Esdc1a"); if (bus_ == nullptr) { - Log::error() << "Esdc1a: bus pointer is null\n"; - ret += 1; + checks.fail("bus pointer is null"); } - check(Ka_ > ZERO, "Ka must be positive"); - check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax"); - check(UEL_ >= static_cast(0) && UEL_ <= static_cast(3), - "UEL must be 0, 1, 2, or 3"); + checks.check(Ka_ > ZERO, "Ka must be positive"); + checks.check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax"); + checks.check(UEL_ >= static_cast(0) && UEL_ <= static_cast(3), + "UEL must be 0, 1, 2, or 3"); // Model data uses an exact zero to mean "saturation bypassed", so // this is an exact comparison by intent rather than a tolerance test. @@ -174,25 +166,23 @@ namespace GridKit if (!saturation_disabled) { - check(E1_ > ZERO, "E1 must be positive when saturation is enabled"); - check(E2_ > ZERO, "E2 must be positive when saturation is enabled"); - check(Se1_ >= ZERO, "Se1 must be non-negative when saturation is enabled"); - check(Se2_ >= ZERO, "Se2 must be non-negative when saturation is enabled"); + checks.check(E1_ > ZERO, "E1 must be positive when saturation is enabled"); + checks.check(E2_ > ZERO, "E2 must be positive when saturation is enabled"); + checks.check(Se1_ >= ZERO, "Se1 must be non-negative when saturation is enabled"); + checks.check(Se2_ >= ZERO, "Se2 must be non-negative when saturation is enabled"); const bool sat_ordered = (E2_ > E1_ && Se2_ > Se1_) || (E2_ < E1_ && Se2_ < Se1_); - check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently"); + checks.check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently"); } if (!ports_.out.template port().connected()) { - Log::error() << "Esdc1a: required efd output signal is not assigned\n"; - ret += 1; + checks.fail("required efd output signal is not assigned"); } if (Spdmlt_ && !ports_.in.template port().connected()) { - Log::error() << "Esdc1a: speed signal is required when Spdmlt is enabled\n"; - ret += 1; + checks.fail("speed signal is required when Spdmlt is enabled"); } // An attached port must resolve to writable signal storage. The @@ -203,8 +193,7 @@ namespace GridKit if (ports_.in.template port().connected() && !ports_.in.template port().linked()) { - Log::error() << "Esdc1a: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -213,7 +202,7 @@ namespace GridKit check_attached_signal.template operator()("vs"); check_attached_signal.template operator()("vuel"); - return ret; + return checks; } /** @@ -250,7 +239,7 @@ namespace GridKit const auto VFE = static_cast(Esdc1aInternalVariables::VFE); const auto EFD = static_cast(Esdc1aInternalVariables::EFD); - bool ret = verify() == 0; + bool ret = this->verifyAndLog(); if (!ret) { Log::error() << "Esdc1a: cannot initialize with invalid configuration\n"; @@ -635,9 +624,9 @@ namespace GridKit * @brief Read the parameters out of the model data * * No parameter is required; every parameter keeps the default - * documented in the model README when omitted. A non-numeric value, a - * switch outside \f$\{0,1\}\f$, or a non-integer selector is counted and - * reported by verify() rather than throwing. Integer JSON values are + * documented in the model README when omitted. A non-numeric or + * non-finite real, a non-Boolean flag, a non-integer selector, or a + * negative lag throws std::invalid_argument. Integer JSON values are * accepted for real parameters. * * @param[in] data Parameters and monitored-variable selections. @@ -647,99 +636,26 @@ namespace GridKit { using Params = typename ModelDataT::Parameters; - parameter_error_count_ = 0; - - auto load_real = [&](auto key, RealT& target, const char* name) - { - if (!data.parameters.contains(key)) - { - return; - } - - const auto& value = data.parameters.at(key); - RealT parsed_value{}; - if (const auto* real_value = std::get_if(&value)) - { - parsed_value = *real_value; - } - else if (const auto* index_value = std::get_if(&value)) - { - parsed_value = static_cast(*index_value); - } - else - { - Log::error() << "Esdc1a: parameter '" << name << "' must be numeric\n"; - ++parameter_error_count_; - return; - } - - const bool ret = std::isfinite(parsed_value); - if (!ret) - { - Log::error() << "Esdc1a: parameter '" << name << "' must be finite\n"; - ++parameter_error_count_; - return; - } - - target = parsed_value; - }; - - auto load_switch = [&](auto key, bool& target, const char* name) - { - if (!data.parameters.contains(key)) - { - return; - } - - const auto& value = data.parameters.at(key); - if (const auto* bool_value = std::get_if(&value)) - { - target = *bool_value; - } - else - { - Log::error() << "Esdc1a: parameter '" << name << "' must be boolean\n"; - ++parameter_error_count_; - } - }; - - auto load_selector = [&](auto key, IdxT& target, const char* name) - { - if (!data.parameters.contains(key)) - { - return; - } - - const auto& value = data.parameters.at(key); - if (const auto* index_value = std::get_if(&value)) - { - target = *index_value; - } - else - { - Log::error() << "Esdc1a: parameter '" << name << "' must be an integer selector\n"; - ++parameter_error_count_; - } - }; - - load_real(Params::Tr, Tr_, "Tr"); - load_real(Params::Ka, Ka_, "Ka"); - load_real(Params::Ta, Ta_, "Ta"); - load_real(Params::Tb, Tb_, "Tb"); - load_real(Params::Tc, Tc_, "Tc"); - load_real(Params::Vrmax, Vrmax_, "Vrmax"); - load_real(Params::Vrmin, Vrmin_, "Vrmin"); - load_real(Params::Ke, Ke_, "Ke"); - load_real(Params::Te, Te_, "Te"); - load_real(Params::Kf, Kf_, "Kf"); - load_real(Params::Tf1, Tf1_, "Tf1"); - load_switch(Params::Spdmlt, Spdmlt_, "Spdmlt"); - load_real(Params::E1, E1_, "E1"); - load_real(Params::Se1, Se1_, "Se1"); - load_real(Params::E2, E2_, "E2"); - load_real(Params::Se2, Se2_, "Se2"); - load_selector(Params::UEL, UEL_, "UEL"); - load_switch(Params::exclim, exclim_, "exclim"); + const Model::ParameterReader reader(data, "Esdc1a"); + + Tr_ = reader.readNonNegative(Params::Tr).value_or(Tr_); + Ka_ = reader.readReal(Params::Ka).value_or(Ka_); + Ta_ = reader.readNonNegative(Params::Ta).value_or(Ta_); + Tb_ = reader.readNonNegative(Params::Tb).value_or(Tb_); + Tc_ = reader.readReal(Params::Tc).value_or(Tc_); + Vrmax_ = reader.readReal(Params::Vrmax).value_or(Vrmax_); + Vrmin_ = reader.readReal(Params::Vrmin).value_or(Vrmin_); + Ke_ = reader.readReal(Params::Ke).value_or(Ke_); + Te_ = reader.readNonNegative(Params::Te).value_or(Te_); + Kf_ = reader.readReal(Params::Kf).value_or(Kf_); + Tf1_ = reader.readNonNegative(Params::Tf1).value_or(Tf1_); + Spdmlt_ = reader.readBool(Params::Spdmlt).value_or(Spdmlt_); + E1_ = reader.readReal(Params::E1).value_or(E1_); + Se1_ = reader.readReal(Params::Se1).value_or(Se1_); + E2_ = reader.readReal(Params::E2).value_or(E2_); + Se2_ = reader.readReal(Params::Se2).value_or(Se2_); + UEL_ = reader.readInteger(Params::UEL).value_or(UEL_); + exclim_ = reader.readBool(Params::exclim).value_or(exclim_); setDerivedParameters(); } @@ -795,24 +711,6 @@ namespace GridKit template void Esdc1a::setDerivedParameters() { - // The lags are raised to the floor in place, so a negative value is - // rejected here while the value as read is still available. verify() - // reports the count. - auto check_non_negative = [&](RealT value, const char* name) - { - if (value < ZERO) - { - Log::error() << "Esdc1a: " << name << " must be non-negative\n"; - ++parameter_error_count_; - } - }; - - check_non_negative(Tr_, "Tr"); - check_non_negative(Ta_, "Ta"); - check_non_negative(Tb_, "Tb"); - check_non_negative(Te_, "Te"); - check_non_negative(Tf1_, "Tf1"); - if (Tr_ < TIME_CONSTANT_MINIMUM || Ta_ < TIME_CONSTANT_MINIMUM || Tb_ < TIME_CONSTANT_MINIMUM || Te_ < TIME_CONSTANT_MINIMUM || Tf1_ < TIME_CONSTANT_MINIMUM) diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp index f45eef4f00..66a4057626 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1.hpp @@ -87,13 +87,14 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp index 0074d0e33b..798aa18e35 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Impl.hpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -126,36 +127,27 @@ namespace GridKit * @brief Verify parameter values and attached signal links */ template - int Ieeet1::verify() const + Model::ConfigurationChecks Ieeet1::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("Ieeet1"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Ieeet1: " << message << '\n'; - ret += 1; - } - }; - - check(Ka_ > ZERO, "Ka must be positive"); - check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax"); - check(Ispdlim_ == ZERO || Ispdlim_ == ONE, - "Ispdlim must be 0 or 1"); + checks.check(Ka_ > ZERO, "Ka must be positive"); + checks.check(Vrmin_ <= Vrmax_, "Vrmin must be less than or equal to Vrmax"); + checks.check(Ispdlim_ == ZERO || Ispdlim_ == ONE, + "Ispdlim must be 0 or 1"); const bool saturation_disabled = Se1_ == ZERO && Se2_ == ZERO; if (!saturation_disabled) { - check(E1_ > ZERO, "E1 must be positive when saturation is enabled"); - check(E2_ > ZERO, "E2 must be positive when saturation is enabled"); - check(Se1_ >= ZERO, "Se1 must be non-negative when saturation is enabled"); - check(Se2_ >= ZERO, "Se2 must be non-negative when saturation is enabled"); + checks.check(E1_ > ZERO, "E1 must be positive when saturation is enabled"); + checks.check(E2_ > ZERO, "E2 must be positive when saturation is enabled"); + checks.check(Se1_ >= ZERO, "Se1 must be non-negative when saturation is enabled"); + checks.check(Se2_ >= ZERO, "Se2 must be non-negative when saturation is enabled"); const bool sat_ordered = (E2_ > E1_ && Se2_ > Se1_) || (E2_ < E1_ && Se2_ < Se1_); - check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently"); + checks.check(sat_ordered, "E1/E2 and Se1/Se2 must be ordered consistently"); } auto check_attached_signal = @@ -164,8 +156,7 @@ namespace GridKit auto port = ports_.in.template port(); if (port.connected() && !port.linked()) { - Log::error() << "Ieeet1: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -175,7 +166,7 @@ namespace GridKit check_attached_signal.template operator()("vuel"); check_attached_signal.template operator()("voel"); - return ret; + return checks; } /** @@ -205,7 +196,7 @@ namespace GridKit template int Ieeet1::initialize() { - if (verify() != 0) + if (!this->verifyAndLog()) { Log::error() << "Ieeet1: cannot initialize with invalid configuration\n"; return 1; diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp index fbc3687e1e..30801c1acd 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPti.hpp @@ -74,13 +74,14 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp index 5c9b035d21..579ec9f792 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/SEXS-PTI/SexsPtiImpl.hpp @@ -6,6 +6,7 @@ * @brief Definition of the SEXS-PTI exciter model. */ +#include #include #include @@ -91,46 +92,41 @@ namespace GridKit } template - int SexsPti::verify() const + Model::ConfigurationChecks SexsPti::verify() const { - int ret = missing_param_count_; + Model::ConfigurationChecks checks("SexsPti"); + + checks.check(missing_param_count_ == 0, "model data is missing required parameters"); if (bus_ == nullptr) { - Log::error() << "SexsPti: bus pointer is null\n"; - ret += 1; + checks.fail("bus pointer is null"); } if (Ta_ < 0.0) { - Log::error() << "SexsPti: Ta must be non-negative\n"; - ret += 1; + checks.fail("Ta must be non-negative"); } if (Tb_ <= 0.0) { - Log::error() << "SexsPti: Tb must be positive\n"; - ret += 1; + checks.fail("Tb must be positive"); } if (Te_ <= 0.0) { - Log::error() << "SexsPti: Te must be positive\n"; - ret += 1; + checks.fail("Te must be positive"); } if (K_ <= 0.0) { - Log::error() << "SexsPti: K must be positive\n"; - ret += 1; + checks.fail("K must be positive"); } if (Efdmin_ >= Efdmax_) { - Log::error() << "SexsPti: Efdmin must be less than Efdmax\n"; - ret += 1; + checks.fail("Efdmin must be less than Efdmax"); } auto efd_port = ports_.out.template port(); if (!efd_port.connected() || !efd_port.linked()) { - Log::error() << "SexsPti: required EFD signal is not assigned\n"; - ret += 1; + checks.fail("required EFD signal is not assigned"); } auto check_attached_signal = @@ -139,8 +135,7 @@ namespace GridKit auto port = ports_.in.template port(); if (port.connected() && !port.linked()) { - Log::error() << "SexsPti: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -149,7 +144,7 @@ namespace GridKit check_attached_signal.template operator()("vuel"); check_attached_signal.template operator()("voel"); - return ret; + return checks; } template diff --git a/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPti.hpp b/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPti.hpp index 300218d25c..074bb2caa1 100644 --- a/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPti.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPti.hpp @@ -88,13 +88,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPtiImpl.hpp b/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPtiImpl.hpp index 92aa4f1e0b..d236370374 100644 --- a/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPtiImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/GASTPTI/GastPtiImpl.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -128,59 +129,52 @@ namespace GridKit * mechanical-power output assignment, attached external signals, and * distinct indexed ports. * - * @return int Number of configuration errors; zero when valid. + * @return The configuration checks; passed() when valid. */ template - int GastPti::verify() const + Model::ConfigurationChecks GastPti::verify() const { const auto PMECH = static_cast(GastPtiInternalVariables::PMECH); - int ret = static_cast(parameter_error_count_); + Model::ConfigurationChecks checks("GastPti"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "GastPti: " << message << '\n'; - ret += 1; - } - }; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); - check(std::isfinite(R_) && R_ > ZERO, "R must be finite and positive"); - check(std::isfinite(At_) && At_ >= ZERO, - "At must be finite and non-negative"); - check(std::isfinite(Kt_) && Kt_ >= ZERO, - "Kt must be finite and non-negative"); + checks.check(std::isfinite(R_) && R_ > ZERO, "R must be finite and positive"); + checks.check(std::isfinite(At_) && At_ >= ZERO, + "At must be finite and non-negative"); + checks.check(std::isfinite(Kt_) && Kt_ >= ZERO, + "Kt must be finite and non-negative"); const bool finite_limits = std::isfinite(Vmin_) && std::isfinite(Vmax_); - check(finite_limits, "Vmin and Vmax must be finite"); + checks.check(finite_limits, "Vmin and Vmax must be finite"); if (finite_limits) { - check(Vmin_ <= Vmax_, "Vmin must be less than or equal to Vmax"); + checks.check(Vmin_ <= Vmax_, "Vmin must be less than or equal to Vmax"); } - check(std::isfinite(Dturb_) && Dturb_ >= ZERO, - "Dturb must be finite and non-negative"); + checks.check(std::isfinite(Dturb_) && Dturb_ >= ZERO, + "Dturb must be finite and non-negative"); const bool valid_component_base = std::isfinite(va_component_base_) && va_component_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - check(valid_component_base, "component power base must be finite and positive"); - check(valid_system_base, "system power base must be finite and positive"); + checks.check(valid_component_base, "component power base must be finite and positive"); + checks.check(valid_system_base, "system power base must be finite and positive"); if (valid_component_base && valid_system_base) { const RealT system_to_component = va_system_base_ / va_component_base_; const RealT component_to_system = va_component_base_ / va_system_base_; - check(std::isfinite(system_to_component) - && system_to_component > ZERO - && std::isfinite(component_to_system) - && component_to_system > ZERO, - "system/component power-base conversion ratios must be finite and positive"); + checks.check(std::isfinite(system_to_component) + && system_to_component > ZERO + && std::isfinite(component_to_system) + && component_to_system > ZERO, + "system/component power-base conversion ratios must be finite and positive"); } - check(ports_.out.template port().connected(), - "pmech output must be assigned"); + checks.check(ports_.out.template port().connected(), + "pmech output must be assigned"); // An attached port must resolve to writable signal storage. auto check_attached_signal = @@ -192,8 +186,7 @@ namespace GridKit { if (!port.linked()) { - Log::error() << "GastPti: " << name << " port attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} port attached with no linked source", name)); } else { @@ -217,14 +210,14 @@ namespace GridKit if (omega_linked) { auto omega = ports_.in.template port(); - check(omega.signalVariableIndex() != pmech_index, - "speed and pmech ports must use distinct signals"); + checks.check(omega.signalVariableIndex() != pmech_index, + "speed and pmech ports must use distinct signals"); } if (pref_linked) { auto pref = ports_.in.template port(); - check(pref.signalVariableIndex() != pmech_index, - "pref and pmech ports must use distinct signals"); + checks.check(pref.signalVariableIndex() != pmech_index, + "pref and pmech ports must use distinct signals"); } if (omega_linked && pref_linked) { @@ -236,13 +229,13 @@ namespace GridKit if (omega_index != INVALID_INDEX || pref_index != INVALID_INDEX) { - check(omega_index != pref_index, - "speed and pref ports must use distinct signals"); + checks.check(omega_index != pref_index, + "speed and pref ports must use distinct signals"); } } } - return ret; + return checks; } /** @@ -283,7 +276,7 @@ namespace GridKit return 1; } - if (verify() > 0) + if (!this->verifyAndLog()) { Log::error() << "GastPti: cannot initialize with invalid configuration\n"; return 1; diff --git a/GridKit/Model/PhasorDynamics/Governor/HYGOV/Hygov.hpp b/GridKit/Model/PhasorDynamics/Governor/HYGOV/Hygov.hpp index 10ed07c265..108f4972d8 100644 --- a/GridKit/Model/PhasorDynamics/Governor/HYGOV/Hygov.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/HYGOV/Hygov.hpp @@ -99,13 +99,14 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; diff --git a/GridKit/Model/PhasorDynamics/Governor/HYGOV/HygovImpl.hpp b/GridKit/Model/PhasorDynamics/Governor/HYGOV/HygovImpl.hpp index 1ad51b4240..a5bd471af0 100644 --- a/GridKit/Model/PhasorDynamics/Governor/HYGOV/HygovImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/HYGOV/HygovImpl.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -124,30 +125,23 @@ namespace GridKit * power feasibility is operating-point dependent and is checked by * initialize(). * - * @return int Number of configuration errors; zero when valid. + * @return The configuration checks; passed() when valid. */ template - int Hygov::verify() const + Model::ConfigurationChecks Hygov::verify() const { - int ret = static_cast(parameter_error_count_); + Model::ConfigurationChecks checks("Hygov"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Hygov: " << message << '\n'; - ret += 1; - } - }; + checks.check(parameter_error_count_ == 0, "model data has invalid parameters"); const bool valid_component_base = std::isfinite(va_component_base_) && va_component_base_ > ZERO; const bool valid_system_base = std::isfinite(va_system_base_) && va_system_base_ > ZERO; - check(valid_component_base, - "component power base must be finite and positive"); - check(valid_system_base, - "system power base must be finite and positive"); + checks.check(valid_component_base, + "component power base must be finite and positive"); + checks.check(valid_system_base, + "system power base must be finite and positive"); if (valid_component_base && valid_system_base) { const RealT system_to_component = va_system_base_ / va_component_base_; @@ -156,18 +150,18 @@ namespace GridKit && system_to_component > ZERO && std::isfinite(component_to_system) && component_to_system > ZERO; - check(valid_base_ratios, - "system/component power-base conversion ratios must be finite and positive"); + checks.check(valid_base_ratios, + "system/component power-base conversion ratios must be finite and positive"); } - check(Rtemp_ > ZERO, "Rtemp must be nonzero"); - check(Tn_ >= ZERO, "Tn must be non-negative"); - check(Velm_ >= ZERO, "Velm must be non-negative"); - check(Gmin_ < Gmax_, "Gmin must be less than Gmax"); - check(At_ > ZERO, "At must be positive"); - check(Dturb_ >= ZERO, "Dturb must be non-negative"); - check(db1_ >= ZERO, "db1 must be non-negative"); - check(Hdam_ > ZERO, "Hdam must be positive"); + checks.check(Rtemp_ > ZERO, "Rtemp must be nonzero"); + checks.check(Tn_ >= ZERO, "Tn must be non-negative"); + checks.check(Velm_ >= ZERO, "Velm must be non-negative"); + checks.check(Gmin_ < Gmax_, "Gmin must be less than Gmax"); + checks.check(At_ > ZERO, "At must be positive"); + checks.check(Dturb_ >= ZERO, "Dturb must be non-negative"); + checks.check(db1_ >= ZERO, "db1 must be non-negative"); + checks.check(Hdam_ > ZERO, "Hdam must be positive"); bool curve_shape_is_valid = true; for (size_t i = 1; i < Gv_.size(); ++i) @@ -175,8 +169,8 @@ namespace GridKit const bool gate_points_increase = Gv_[i - 1] < Gv_[i]; const bool power_points_increase = Pgv_[i - 1] <= Pgv_[i]; - check(gate_points_increase, "Gv points must be strictly increasing"); - check(power_points_increase, "Pgv points must be non-decreasing"); + checks.check(gate_points_increase, "Gv points must be strictly increasing"); + checks.check(power_points_increase, "Pgv points must be non-decreasing"); if (!gate_points_increase || !power_points_increase) { @@ -185,8 +179,8 @@ namespace GridKit } const bool minimum_gate_is_valid = Gv_[0] <= Gmin_; const bool maximum_gate_is_valid = Gmax_ <= Gv_[5]; - check(minimum_gate_is_valid, "Gmin must be at or above the first Gv point"); - check(maximum_gate_is_valid, "Gmax must be at or below the last Gv point"); + checks.check(minimum_gate_is_valid, "Gmin must be at or above the first Gv point"); + checks.check(maximum_gate_is_valid, "Gmax must be at or below the last Gv point"); const bool can_check_power_range = curve_shape_is_valid && Gmin_ < Gmax_ @@ -204,17 +198,17 @@ namespace GridKit const bool finite_power_range = std::isfinite(minimum_power) && std::isfinite(maximum_power) && std::isfinite(power_range); - check(finite_power_range, - "mechanical-power range must be finite"); + checks.check(finite_power_range, + "mechanical-power range must be finite"); if (finite_power_range) { - check(power_range > INITIALIZATION_TOLERANCE, - "mechanical power must rise across [Gv0, Gv5]"); + checks.check(power_range > INITIALIZATION_TOLERANCE, + "mechanical power must rise across [Gv0, Gv5]"); } } - check(ports_.out.template port().connected(), - "pmech output signal must be assigned"); + checks.check(ports_.out.template port().connected(), + "pmech output signal must be assigned"); // An attached port must resolve to readable signal storage. The // enumerator is a template argument, so each port names itself once. @@ -224,8 +218,7 @@ namespace GridKit if (ports_.in.template port().connected() && !ports_.in.template port().linked()) { - Log::error() << "Hygov: " << name << " signal attached with no linked source\n"; - ret += 1; + checks.fail(std::format("{} signal attached with no linked source", name)); } }; @@ -233,7 +226,7 @@ namespace GridKit check_attached_signal.template operator()("pref"); check_attached_signal.template operator()("paux"); - return ret; + return checks; } /** @@ -275,7 +268,7 @@ namespace GridKit const auto H = static_cast(HygovInternalVariables::H); const auto PMECH = static_cast(HygovInternalVariables::PMECH); - bool ret = verify() == 0; + bool ret = this->verifyAndLog(); if (!ret) { Log::error() << "Hygov: cannot initialize with invalid configuration\n"; diff --git a/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1.hpp b/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1.hpp index dcc5e147e2..84b7f260dc 100644 --- a/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1.hpp @@ -78,12 +78,13 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT) override final; int evaluateResidual() override final; + Model::ConfigurationChecks verify() const override final; + // Still to be implemented int evaluateJacobian() override final; diff --git a/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1Impl.hpp b/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1Impl.hpp index a50c3a2748..06b400682b 100644 --- a/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1Impl.hpp +++ b/GridKit/Model/PhasorDynamics/Governor/Tgov1/Tgov1Impl.hpp @@ -220,41 +220,30 @@ namespace GridKit * @brief verify method checks that attached signals are also linked */ template - int Tgov1::verify() const + Model::ConfigurationChecks Tgov1::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("Tgov1"); - auto check = [&](bool condition, const char* message) - { - if (!condition) - { - Log::error() << "Tgov1: " << message << '\n'; - ret += 1; - } - }; - - check(Trate_ > ZERO, "Trate must be positive"); - check(va_system_base_ > ZERO, "system power base must be positive"); - check(R_ != ZERO, "R must be nonzero"); - check(Pvmin_ <= Pvmax_, "Pvmin must be less than or equal to Pvmax"); - check(ports_.out.template port().connected(), - "pmech output signal must be assigned"); + checks.check(Trate_ > ZERO, "Trate must be positive"); + checks.check(va_system_base_ > ZERO, "system power base must be positive"); + checks.check(R_ != ZERO, "R must be nonzero"); + checks.check(Pvmin_ <= Pvmax_, "Pvmin must be less than or equal to Pvmax"); + checks.check(ports_.out.template port().connected(), + "pmech output signal must be assigned"); auto speed_port = ports_.in.template port(); if (speed_port.connected() && !speed_port.linked()) { - Log::error() << "Tgov1: speed signal attached with no linked generator\n"; - ret += 1; + checks.fail("speed signal attached with no linked generator"); } auto pref_port = ports_.in.template port(); if (pref_port.connected() && !pref_port.linked()) { - Log::error() << "Tgov1: pref signal attached with no linked source\n"; - ret += 1; + checks.fail("pref signal attached with no linked source"); } - return ret; + return checks; } /** @@ -264,7 +253,7 @@ namespace GridKit template int Tgov1::initialize() { - if (verify() != 0) + if (!this->verifyAndLog()) { Log::error() << "Tgov1: cannot initialize with invalid configuration\n"; return 1; diff --git a/GridKit/Model/PhasorDynamics/Load/LoadZ/LoadZ.hpp b/GridKit/Model/PhasorDynamics/Load/LoadZ/LoadZ.hpp index 008abceeb7..ebe30a1345 100644 --- a/GridKit/Model/PhasorDynamics/Load/LoadZ/LoadZ.hpp +++ b/GridKit/Model/PhasorDynamics/Load/LoadZ/LoadZ.hpp @@ -56,11 +56,6 @@ namespace GridKit virtual int evaluateResidual() override final; virtual int evaluateJacobian() override final; - virtual int verify() const override final - { - return 0; - } - public: void setR(RealT R) { diff --git a/GridKit/Model/PhasorDynamics/Load/LoadZIP/LoadZIP.hpp b/GridKit/Model/PhasorDynamics/Load/LoadZIP/LoadZIP.hpp index 737cba8c7b..1fdd146e2d 100644 --- a/GridKit/Model/PhasorDynamics/Load/LoadZIP/LoadZIP.hpp +++ b/GridKit/Model/PhasorDynamics/Load/LoadZIP/LoadZIP.hpp @@ -56,11 +56,6 @@ namespace GridKit int evaluateResidual() override final; int evaluateJacobian() override final; - int verify() const override final - { - return 0; - } - public: void setPnom(RealT Pnom) { diff --git a/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSource.hpp b/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSource.hpp index 6adf70824f..e7de26adba 100644 --- a/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSource.hpp +++ b/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSource.hpp @@ -45,7 +45,6 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT) override final; diff --git a/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSourceImpl.hpp b/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSourceImpl.hpp index 561fbe759b..b7163385c4 100644 --- a/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSourceImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SignalSource/ConstantSignalSourceImpl.hpp @@ -83,12 +83,6 @@ namespace GridKit return 0; } - template - int ConstantSignalSource::verify() const - { - return 0; - } - template int ConstantSignalSource::initialize() { diff --git a/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSource.hpp b/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSource.hpp index c172e1ce2b..16169e0a20 100644 --- a/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSource.hpp +++ b/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSource.hpp @@ -48,7 +48,6 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT) override final; diff --git a/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSourceImpl.hpp b/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSourceImpl.hpp index af2dfcf732..304810b4fd 100644 --- a/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSourceImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SignalSource/FunctionSignalSourceImpl.hpp @@ -109,12 +109,6 @@ namespace GridKit return 0; } - template - int FunctionSignalSource::verify() const - { - return 0; - } - template int FunctionSignalSource::initialize() { diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/CMakeLists.txt b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/CMakeLists.txt index abc5bf20cf..99a20aac23 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/CMakeLists.txt +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/CMakeLists.txt @@ -3,7 +3,7 @@ # - Luke Lowery # ]] -set(_install_headers Ieeest.hpp IeeestData.hpp) +set(_install_headers Ieeest.hpp IeeestData.hpp IeeestFactory.hpp) if(GRIDKIT_ENABLE_ENZYME) gridkit_add_library( diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp index 44eb4be902..c1cd1762d6 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.cpp @@ -17,16 +17,24 @@ namespace GridKit * * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { Log::misc() << "Ieeest: Jacobian evaluation is not implemented\n"; return 0; } // Available template instantiations - template class Ieeest; - template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp index 448e0144c9..1437e73931 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/Ieeest.hpp @@ -6,6 +6,9 @@ #pragma once +#include +#include + #include #include #include @@ -18,21 +21,18 @@ namespace GridKit { namespace Stabilizer { - /// Internal variables of `Ieeest` + /// Internal variables of an `Ieeest`. Notch states come last; a model + /// of notch order \f$n\f$ has the first \f$n\f$ of them. enum class IeeestInternalVariables : size_t { - X1, ///< \f$x_1\f$ Notch-filter signal state - X2, ///< \f$x_2\f$ First derivative of the filtered signal - X3, ///< \f$x_3\f$ Second derivative of the filtered signal - X4, ///< \f$x_4\f$ Third derivative of the filtered signal X5, ///< \f$x_5\f$ Lead-lag 1 state X6, ///< \f$x_6\f$ Lead-lag 2 state X7, ///< \f$x_7\f$ Washout state - V4, ///< \f$v_4\f$ Notch-filter output - V5, ///< \f$v_5\f$ Lead-lag 1 output - V6, ///< \f$v_6\f$ Lead-lag 2 output - V7, ///< \f$v_7\f$ Unlimited stabilizer signal VSS, ///< \f$V_{\mathrm{ss}}\f$ Limited stabilizer signal and model output + X1, ///< \f$x_1\f$ First notch factor output + X2, ///< \f$x_2\f$ Rate of \f$x_1\f$ + X3, ///< \f$x_3\f$ Second notch factor output + X4, ///< \f$x_4\f$ Rate of \f$x_3\f$ }; /// External variables of an `Ieeest`. @@ -41,38 +41,71 @@ namespace GridKit U, ///< \f$u\f$ Stabilizer input signal }; - template + /// Degree of \f$1 + p s + q s^2\f$. A zero coefficient marks an absent term. + template + constexpr size_t factorDegree(RealT p, RealT q) + { + if (q != RealT{0}) + { + return 2; + } + if (p != RealT{0}) + { + return 1; + } + return 0; + } + + /// Degree of the notch denominator + /// \f$(1 + A_1 s + A_2 s^2)(1 + A_3 s + A_4 s^2)\f$. + template + constexpr size_t notchOrder(RealT A1, RealT A2, RealT A3, RealT A4) + { + return factorDegree(A1, A2) + factorDegree(A3, A4); + } + + /** + * @brief IEEE power system stabilizer (IEEEST). + * + * IeeestFactory selects the notch order from the model data. + * + * @tparam scalar_type Plain real or differentiable scalar type. + * @tparam index_type Integer index type. + * @tparam order Degree of the notch denominator, at most four. + */ + template class Ieeest : public Component { - using Component::gridkit_component_id_; + static_assert(order <= 4, "IEEEST notch order is at most four"); + + using Component::abs_tol_; + using Component::allocated_; using Component::alpha_; using Component::f_; + using Component::gridkit_component_id_; + using Component::J_cols_buffer_; + using Component::J_rows_buffer_; + using Component::J_vals_buffer_; using Component::nnz_; + using Component::residual_indices_; using Component::size_; using Component::tag_; - using Component::abs_tol_; - using Component::time_; - using Component::y_; - using Component::yp_; - using Component::wb_; + using Component::variable_indices_; using Component::ws_; using Component::ws_indices_; - using Component::h_; - using Component::J_rows_buffer_; - using Component::J_cols_buffer_; - using Component::J_vals_buffer_; - using Component::variable_indices_; - using Component::residual_indices_; - using Component::allocated_; + using Component::y_; + using Component::yp_; public: - using ScalarT = scalar_type; - using IdxT = index_type; - using RealT = typename Component::RealT; - using ModelDataT = IeeestData; - using SignalNodeSetT = SignalNodeSet; - using SignalPortsT = SignalPorts; - using MonitorT = Model::VariableMonitor; + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename Component::RealT; + using ModelDataT = IeeestData; + using SignalNodeSetT = SignalNodeSet; + using SignalPortsT = SignalPorts; + using MonitorT = Model::VariableMonitor; + using InternalVariablesT = IeeestInternalVariables; + using ExternalVariablesT = IeeestExternalVariables; Ieeest(); Ieeest(const ModelDataT& data); @@ -80,13 +113,14 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; int evaluateJacobian() override final; + Model::ConfigurationChecks verify() const override final; + SignalPortsT& getPorts() { return ports_; @@ -94,7 +128,7 @@ namespace GridKit const Model::VariableMonitorBase* getMonitor() const override; - __attribute__((always_inline)) inline int evaluateInternalResidual( + [[gnu::always_inline]] inline int evaluateInternalResidual( const ScalarT*, const ScalarT*, const ScalarT*, @@ -102,6 +136,13 @@ namespace GridKit ScalarT*); private: + void initializeParameters(const ModelDataT& data); + void initializeMonitor(); + void setDerivedParameters(); + + static constexpr RealT TIME_CONSTANT_MINIMUM = static_cast(1.0e-3); + static void logTimeConstantWarning(); + RealT A1_{0}; RealT A2_{0}; RealT A3_{0}; @@ -121,34 +162,13 @@ namespace GridKit RealT Vcu_{0}; RealT Tdelay_{0}; - RealT a0_{1}; - RealT a1_{0}; - RealT a2_{0}; - RealT a3_{0}; - RealT a4_{0}; - - // Precomputed masks and safe inverse coefficients for branch-free degenerate paths. - RealT use_notch_{0}; - RealT bypass_notch_{1}; - RealT use_4th_order_{0}; - RealT use_3rd_order_{0}; - RealT use_2nd_order_{0}; - RealT safe_inv_a4_{0}; - RealT safe_inv_a3_{0}; - RealT safe_inv_a2_{0}; - RealT use_T2_block_{1}; - RealT bypass_T2_block_{0}; - RealT use_T4_block_{1}; - RealT bypass_T4_block_{0}; - RealT use_T6_block_{1}; - RealT bypass_T6_block_{0}; - - SignalPortsT ports_; + RealT b1_{0}; ///< First notch factor \f$1 + b_1 s + b_2 s^2\f$, which carries the numerator + RealT b2_{0}; + RealT c1_{0}; ///< Second notch factor \f$1 + c_1 s + c_2 s^2\f$ + RealT c2_{0}; + SignalPortsT ports_; std::unique_ptr monitor_; - - void initializeParameters(const ModelDataT& data); - void initializeMonitor(); }; } // namespace Stabilizer diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp index 376b1fe210..d694232f53 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestDependencyTracking.cpp @@ -22,8 +22,8 @@ namespace GridKit * * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { this->constructCsr(); @@ -31,9 +31,16 @@ namespace GridKit } // Available template instantiations - template class Ieeest; - template class Ieeest; - + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp index 4a421a8475..0889e17c29 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestEnzyme.cpp @@ -15,14 +15,15 @@ namespace GridKit namespace Stabilizer { /** - * @brief Jacobian evaluation experimental + * @brief Evaluate the sparse Jacobian. * - * @tparam ScalarT - Scalar data type - * @tparam IdxT - Index data type + * @tparam scalar_type Scalar data type. + * @tparam index_type Index data type. + * @tparam order Notch-denominator degree. * @return int - error code, 0 = success */ - template - int Ieeest::evaluateJacobian() + template + int Ieeest::evaluateJacobian() { if (J_rows_buffer_ == nullptr) { @@ -37,53 +38,53 @@ namespace GridKit J_vals_buffer_ = new RealT[buffer_size]; } + using ModelT = Ieeest; + using Fn = GridKit::Enzyme::Sparse::MemberFunctions; + nnz_ = 0; - GridKit::Enzyme::Sparse::DfDy, - GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, - static_cast(f_.getSize()), - static_cast(y_.getSize()), - (this->getResidualIndices()).data(), - (this->getVariableIndices()).data(), - y_.getData(), - yp_.getData(), - nullptr, - ws_.getData(), - J_rows_buffer_, - J_cols_buffer_, - J_vals_buffer_, - nnz_); + GridKit::Enzyme::Sparse::DfDy::eval(this, + static_cast(f_.getSize()), + static_cast(y_.getSize()), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.getData(), + yp_.getData(), + nullptr, + ws_.getData(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); - GridKit::Enzyme::Sparse::DfDyp, - GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, - static_cast(f_.getSize()), - static_cast(y_.getSize()), - (this->getResidualIndices()).data(), - (this->getVariableIndices()).data(), - y_.getData(), - yp_.getData(), - nullptr, - ws_.getData(), - alpha_, - J_rows_buffer_, - J_cols_buffer_, - J_vals_buffer_, - nnz_); + GridKit::Enzyme::Sparse::DfDyp::eval(this, + static_cast(f_.getSize()), + static_cast(y_.getSize()), + (this->getResidualIndices()).data(), + (this->getVariableIndices()).data(), + y_.getData(), + yp_.getData(), + nullptr, + ws_.getData(), + alpha_, + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); - GridKit::Enzyme::Sparse::DfDws, - GridKit::Enzyme::Sparse::MemberFunctions::InternalResidualWithSignal>::eval(this, - static_cast(f_.getSize()), - static_cast(ws_.getSize()), - (this->getResidualIndices()).data(), - ws_indices_.data(), - y_.getData(), - yp_.getData(), - nullptr, - ws_.getData(), - J_rows_buffer_, - J_cols_buffer_, - J_vals_buffer_, - nnz_); + GridKit::Enzyme::Sparse::DfDws::eval(this, + static_cast(f_.getSize()), + static_cast(ws_.getSize()), + (this->getResidualIndices()).data(), + ws_indices_.data(), + y_.getData(), + yp_.getData(), + nullptr, + ws_.getData(), + J_rows_buffer_, + J_cols_buffer_, + J_vals_buffer_, + nnz_); this->constructCoo(); @@ -91,8 +92,16 @@ namespace GridKit } // Available template instantiations - template class Ieeest; - template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; + template class Ieeest; } // namespace Stabilizer } // namespace PhasorDynamics diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestFactory.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestFactory.hpp new file mode 100644 index 0000000000..625bed3e24 --- /dev/null +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestFactory.hpp @@ -0,0 +1,76 @@ +/** + * @file IeeestFactory.hpp + * @author Luke Lowery (lukel@tamu.edu) + * @brief Construct the IEEEST specialization for the notch order of its data. + */ + +#pragma once + +#include +#include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + namespace Stabilizer + { + template + class IeeestFactory + { + public: + using ScalarT = scalar_type; + using IdxT = index_type; + using ComponentT = Component; + using RealT = typename ComponentT::RealT; + using ModelDataT = IeeestData; + using SignalNodeSetT = SignalNodeSet; + + IeeestFactory() = delete; + + /// Construct and connect the specialization for the notch order of + /// the data; the caller owns the result. + static ComponentT* create(const ModelDataT& data, SignalNodeSetT& signal_nodes) + { + switch (readNotchOrder(data)) + { + case 0: + return construct<0>(data, signal_nodes); + case 1: + return construct<1>(data, signal_nodes); + case 2: + return construct<2>(data, signal_nodes); + case 3: + return construct<3>(data, signal_nodes); + default: + return construct<4>(data, signal_nodes); + } + } + + /// Read the notch order from the denominator coefficients. + static size_t readNotchOrder(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; + + Model::ParameterReader reader(data, "Ieeest"); + + const RealT A1 = reader.readReal(Params::A1).value_or(0); + const RealT A2 = reader.readReal(Params::A2).value_or(0); + const RealT A3 = reader.readReal(Params::A3).value_or(0); + const RealT A4 = reader.readReal(Params::A4).value_or(0); + return notchOrder(A1, A2, A3, A4); + } + + private: + template + static ComponentT* construct(const ModelDataT& data, SignalNodeSetT& signal_nodes) + { + auto* stabilizer = new Ieeest(data); + stabilizer->getPorts().connect(data, signal_nodes); + return stabilizer; + } + }; + } // namespace Stabilizer + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp index 2bc836363e..223c0bffff 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/IeeestImpl.hpp @@ -1,18 +1,22 @@ -#pragma once - /** * @file IeeestImpl.hpp * @author Luke Lowery (lukel@tamu.edu) * @brief Definition of the IEEEST Power System Stabilizer. */ -#include +#pragma once + +#include +#include +#include +#include +#include #include -#include #include #include #include +#include #include namespace GridKit @@ -21,233 +25,198 @@ namespace GridKit { namespace Stabilizer { + /// Logger used for IEEEST diagnostics. using Log = ::GridKit::Utilities::Logger; - template - Ieeest::Ieeest() + /** + * @brief Construct an IEEEST stabilizer without parameters + * + * Every parameter keeps its documented default, and no monitor is + * created. verify() rejects the model until an input is attached. + */ + template + Ieeest::Ieeest() { - size_ = 12; + size_ = static_cast(static_cast(IeeestInternalVariables::X1) + order); + setDerivedParameters(); } - template - Ieeest::Ieeest(const ModelDataT& data) + /** + * @brief Construct an IEEEST stabilizer from model data + * + * @param[in] data Parameters and monitored-variable selections. + */ + template + Ieeest::Ieeest(const ModelDataT& data) : monitor_(std::make_unique(data)) { initializeParameters(data); initializeMonitor(); - size_ = 12; - } - - template - Ieeest::~Ieeest() - { + size_ = static_cast(static_cast(IeeestInternalVariables::X1) + order); } - template - void Ieeest::initializeParameters(const ModelDataT& data) + /** + * @brief Destroy the IEEEST stabilizer. + */ + template + Ieeest::~Ieeest() { - using Parameter = typename ModelDataT::Parameters; - if (data.parameters.contains(Parameter::A1)) - { - A1_ = std::get(data.parameters.at(Parameter::A1)); - } - if (data.parameters.contains(Parameter::A2)) - { - A2_ = std::get(data.parameters.at(Parameter::A2)); - } - if (data.parameters.contains(Parameter::A3)) - { - A3_ = std::get(data.parameters.at(Parameter::A3)); - } - if (data.parameters.contains(Parameter::A4)) - { - A4_ = std::get(data.parameters.at(Parameter::A4)); - } - if (data.parameters.contains(Parameter::A5)) - { - A5_ = std::get(data.parameters.at(Parameter::A5)); - } - if (data.parameters.contains(Parameter::A6)) - { - A6_ = std::get(data.parameters.at(Parameter::A6)); - } - if (data.parameters.contains(Parameter::T1)) - { - T1_ = std::get(data.parameters.at(Parameter::T1)); - } - if (data.parameters.contains(Parameter::T2)) - { - T2_ = std::get(data.parameters.at(Parameter::T2)); - } - if (data.parameters.contains(Parameter::T3)) - { - T3_ = std::get(data.parameters.at(Parameter::T3)); - } - if (data.parameters.contains(Parameter::T4)) - { - T4_ = std::get(data.parameters.at(Parameter::T4)); - } - if (data.parameters.contains(Parameter::T5)) - { - T5_ = std::get(data.parameters.at(Parameter::T5)); - } - if (data.parameters.contains(Parameter::T6)) - { - T6_ = std::get(data.parameters.at(Parameter::T6)); - } - if (data.parameters.contains(Parameter::Ks)) - { - Ks_ = std::get(data.parameters.at(Parameter::Ks)); - } - if (data.parameters.contains(Parameter::Lsmin)) - { - Lsmin_ = std::get(data.parameters.at(Parameter::Lsmin)); - } - if (data.parameters.contains(Parameter::Lsmax)) - { - Lsmax_ = std::get(data.parameters.at(Parameter::Lsmax)); - } - if (data.parameters.contains(Parameter::Vcl)) - { - Vcl_ = std::get(data.parameters.at(Parameter::Vcl)); - } - if (data.parameters.contains(Parameter::Vcu)) - { - Vcu_ = std::get(data.parameters.at(Parameter::Vcu)); - } - if (data.parameters.contains(Parameter::Tdelay)) - { - Tdelay_ = std::get(data.parameters.at(Parameter::Tdelay)); - } - - a0_ = 1; - a1_ = A1_ + A3_; - a2_ = A2_ + A4_ + A1_ * A3_; - a3_ = A1_ * A4_ + A2_ * A3_; - a4_ = A2_ * A4_; - - // Precompute masks and safe inverse coefficients so the residual stays branch-free. - use_notch_ = static_cast(a2_ != 0.0 || a3_ != 0.0 || a4_ != 0.0); - bypass_notch_ = 1.0 - use_notch_; - - use_4th_order_ = static_cast(a4_ != 0.0); - use_3rd_order_ = static_cast(a4_ == 0.0 && a3_ != 0.0); - use_2nd_order_ = static_cast(a4_ == 0.0 && a3_ == 0.0 && a2_ != 0.0); - safe_inv_a4_ = use_4th_order_ / (a4_ + (1.0 - use_4th_order_)); - safe_inv_a3_ = use_3rd_order_ / (a3_ + (1.0 - use_3rd_order_)); - safe_inv_a2_ = use_2nd_order_ / (a2_ + (1.0 - use_2nd_order_)); - - use_T2_block_ = static_cast(T2_ != 0.0); - bypass_T2_block_ = 1.0 - use_T2_block_; - - use_T4_block_ = static_cast(T4_ != 0.0); - bypass_T4_block_ = 1.0 - use_T4_block_; - - use_T6_block_ = static_cast(T6_ != 0.0); - bypass_T6_block_ = 1.0 - use_T6_block_; } - template - int Ieeest::setGridKitComponentID(IdxT component_id) + /** + * @brief Set the component ID + * + * @param[in] component_id Identifier assigned by the system model. + * @return Zero on success. + */ + template + int Ieeest::setGridKitComponentID(IdxT component_id) { gridkit_component_id_ = component_id; return 0; } - template - int Ieeest::allocate() + /** + * @brief Allocate the model vectors and wire the stabilizer output + * + * An assigned `output` node aliases the limited stabilizer signal. + * Repeated calls reuse the allocated vectors. + * + * @return Zero on success. + */ + template + int Ieeest::allocate() { + const auto VSS = static_cast(IeeestInternalVariables::VSS); + if (!allocated_) { this->allocateVectors(size_); } - auto size = static_cast(size_); - - tag_.resize(size); + const auto size = static_cast(size_); + tag_.assign(size, false); variable_indices_.resize(size); residual_indices_.resize(size); + + const auto signal_size = Utilities::enum_size(); + ws_.resize(static_cast(signal_size)); + ws_.setToZero(); + ws_indices_.assign(signal_size, INVALID_INDEX); + for (IdxT j = 0; j < size_; ++j) { this->setVariableIndex(j, j); this->setResidualIndex(j, j); } - ws_.resize(1); - ws_.setToZero(); - ws_indices_.resize(1); - ws_indices_[0] = INVALID_INDEX; - - if (auto output_port = ports_.out.template port()) + if (auto port = ports_.out.template port()) { - output_port.link(&y_.getData()[11], &(this->getVariableIndex(11))); + auto* y = y_.getData(); + port.link(&y[VSS], &(this->getVariableIndex(static_cast(VSS)))); } allocated_ = true; return 0; } - template - int Ieeest::verify() const + /** + * @brief Validate the IEEEST configuration + * + * Checks the input signal, the output limits, and that the notch + * coefficients define this specialization's order with a numerator of + * no higher degree. + * + * @return The configuration checks; passed() when valid. + */ + template + Model::ConfigurationChecks Ieeest::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("Ieeest"); - auto input_port = ports_.in.template port(); - if (!input_port.connected()) + const auto input = ports_.in.template port(); + if (!input.connected()) { - Log::error() << "Ieeest: required input signal U is not attached\n"; - ret += 1; + checks.fail("required input signal is not attached"); } - if (input_port.connected() && !input_port.linked()) + if (input.connected() && !input.linked()) { - Log::error() << "Ieeest: input signal U attached with no linked source\n"; - ret += 1; + checks.fail("input signal attached with no linked source"); } - if (a4_ == 0 && a3_ == 0 && a2_ == 0 && a1_ != 0) + checks.check(Lsmin_ < Lsmax_, "Lsmin must be less than Lsmax"); + + // Model data uses an exact zero to mark an absent notch term, so the + // degrees come from exact comparisons by intent rather than tolerance tests. + const size_t data_order = notchOrder(A1_, A2_, A3_, A4_); + if (data_order != order) { - Log::error() << "Ieeest: a2, a3, and a4 are all zero - no valid notch filter\n"; - ret += 1; + checks.fail(std::format("A1 to A4 define notch order {}, not {}", data_order, order)); } + checks.check(factorDegree(A5_, A6_) <= order, + "notch numerator degree must not exceed the notch order"); - return ret; + return checks; } - template - int Ieeest::initialize() + /** + * @brief Initialize IEEEST at the steady state of its input + * + * A constant input is an equilibrium: each notch factor and lead-lag + * passes it through, and the washout blocks it, so the output is zero. + * + * @return Zero on success; nonzero when the configuration is rejected. + */ + template + int Ieeest::initialize() { - if (verify() != 0) + const auto X5 = static_cast(IeeestInternalVariables::X5); + const auto X6 = static_cast(IeeestInternalVariables::X6); + const auto X7 = static_cast(IeeestInternalVariables::X7); + const auto VSS = static_cast(IeeestInternalVariables::VSS); + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + const auto X3 = static_cast(IeeestInternalVariables::X3); + const auto X4 = static_cast(IeeestInternalVariables::X4); + const auto U = static_cast(IeeestExternalVariables::U); + + if (!this->verifyAndLog()) { Log::error() << "Ieeest: cannot initialize with invalid configuration\n"; return 1; } - auto input_port = ports_.in.template port(); - const ScalarT u = input_port.readSignal(); + auto input = ports_.in.template port(); + const ScalarT u = input.readSignal(); - auto* y = y_.getData(); - auto* yp = yp_.getData(); + ws_.getData()[U] = u; + ws_indices_[U] = input.signalVariableIndex(); - for (IdxT i = 0; i < size_; ++i) + auto* y = y_.getData(); + + y[X5] = u; + y[X6] = u; + y[X7] = u; + y[VSS] = Math::clamp(ZERO, Lsmin_, Lsmax_); + if constexpr (order >= 1) { - y[static_cast(i)] = 0.0; - yp[static_cast(i)] = 0.0; + y[X1] = u; + } + if constexpr (order >= 2) + { + y[X2] = ZERO; + } + if constexpr (order >= 3) + { + y[X3] = u; + } + if constexpr (order == 4) + { + y[X4] = ZERO; } - ws_.getData()[0] = u; - ws_indices_[0] = input_port.signalVariableIndex(); - - y[0] = use_notch_ * u; - y[4] = u; - y[5] = u; - y[6] = u; - y[7] = u; - y[8] = u; - y[9] = u; - - // Preserve the current T6 = 0 bypass behavior. - y[10] = bypass_T6_block_ * Ks_ * u; - y[11] = Math::clamp(y[10], Lsmin_, Lsmax_); + y_.setDataUpdated(); + yp_.setToConst(static_cast(ZERO)); // For DependencyTracking::Variable, set variable numbers if constexpr (std::is_same_v) @@ -255,133 +224,337 @@ namespace GridKit this->initializeDependencyTrackingVariableNumbers(); } - y_.setDataUpdated(); - yp_.setDataUpdated(); - return 0; } - template - int Ieeest::tagDifferentiable() + /** + * @brief Identify the differential variables + * + * Every internal variable is differential except the limited output. + * + * @return Zero on success. + */ + template + int Ieeest::tagDifferentiable() { - tag_[0] = true; - tag_[1] = true; - tag_[2] = true; - tag_[3] = true; - tag_[4] = (T2_ != 0.0); - tag_[5] = (T4_ != 0.0); - tag_[6] = (T6_ != 0.0); - tag_[7] = false; - tag_[8] = false; - tag_[9] = false; - tag_[10] = false; - tag_[11] = false; - + const auto X5 = static_cast(IeeestInternalVariables::X5); + const auto X6 = static_cast(IeeestInternalVariables::X6); + const auto X7 = static_cast(IeeestInternalVariables::X7); + const auto VSS = static_cast(IeeestInternalVariables::VSS); + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + const auto X3 = static_cast(IeeestInternalVariables::X3); + const auto X4 = static_cast(IeeestInternalVariables::X4); + + tag_[X5] = true; + tag_[X6] = true; + tag_[X7] = true; + tag_[VSS] = false; + if constexpr (order >= 1) + { + tag_[X1] = true; + } + if constexpr (order >= 2) + { + tag_[X2] = true; + } + if constexpr (order >= 3) + { + tag_[X3] = true; + } + if constexpr (order == 4) + { + tag_[X4] = true; + } return 0; } /** * @brief Compute the absolute tolerance for each variable in the model * - * @param rel_tol The relative tolerance which can be used to pick the - * absolute tolerance. - * @tparam scalar_type Scalar data type - * @tparam index_type Index data type - * @return int 0 if successful, non-zero otherwise. + * Every internal variable receives @p rel_tol as its absolute + * tolerance. * - * This represents a "noise" level close to zero for which pure relative - * error cannot be used. + * @param[in] rel_tol Solver relative tolerance. + * @return Zero on success. */ - template - int Ieeest::setAbsoluteTolerance(RealT rel_tol) + template + int Ieeest::setAbsoluteTolerance(RealT rel_tol) { abs_tol_.setToConst(static_cast(rel_tol)); return 0; } - template - __attribute__((always_inline)) inline int Ieeest::evaluateInternalResidual( - const ScalarT* y, - const ScalarT* yp, - [[maybe_unused]] const ScalarT* wb, - const ScalarT* ws, - ScalarT* f) + /** + * @brief Residuals of system equations + * + * Reads the input signal and evaluates the internal residual. + * + * @return Zero on success. + */ + template + int Ieeest::evaluateResidual() { - ScalarT x1 = y[0]; - ScalarT x2 = y[1]; - ScalarT x3 = y[2]; - ScalarT x4 = y[3]; - ScalarT x5 = y[4]; - ScalarT x6 = y[5]; - ScalarT x7 = y[6]; - ScalarT v4 = y[7]; - ScalarT v5 = y[8]; - ScalarT v6 = y[9]; - ScalarT v7 = y[10]; - ScalarT vss = y[11]; - - ScalarT x1_dot = yp[0]; - ScalarT x2_dot = yp[1]; - ScalarT x3_dot = yp[2]; - ScalarT x4_dot = yp[3]; - ScalarT x5_dot = yp[4]; - ScalarT x6_dot = yp[5]; - ScalarT x7_dot = yp[6]; - - ScalarT u = ws[0]; - - const ScalarT x2_rhs = (use_4th_order_ + use_3rd_order_) * x3 - + use_2nd_order_ * (-a0_ * x1 - a1_ * x2 + u) * safe_inv_a2_; - - f[0] = -x1_dot + use_notch_ * x2; - f[1] = -x2_dot + x2_rhs; - f[2] = -x3_dot + use_4th_order_ * x4 - + use_3rd_order_ * (-a0_ * x1 - a1_ * x2 - a2_ * x3 + u) * safe_inv_a3_; - f[3] = -x4_dot + use_4th_order_ * (-a0_ * x1 - a1_ * x2 - a2_ * x3 - a3_ * x4 + u) * safe_inv_a4_; - f[4] = -T2_ * x5_dot - x5 + v4; - f[5] = -T4_ * x6_dot - x6 + v5; - f[6] = -T6_ * x7_dot - x7 + v6; - f[7] = -v4 + bypass_notch_ * u + use_notch_ * (x1 + A5_ * x2 + A6_ * x2_rhs); - f[8] = use_T2_block_ * (-T2_ * (v5 - x5) + T1_ * (v4 - x5)) + bypass_T2_block_ * (v4 - v5); - f[9] = use_T4_block_ * (-T4_ * (v6 - x6) + T3_ * (v5 - x6)) + bypass_T4_block_ * (v5 - v6); - f[10] = use_T6_block_ * (-T6_ * v7 + Ks_ * T5_ * (v6 - x7)) + bypass_T6_block_ * (Ks_ * v6 - v7); - f[11] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); - - return 0; - } + const auto U = static_cast(IeeestExternalVariables::U); - template - int Ieeest::evaluateResidual() - { auto* ws = ws_.getData(); - if (auto input_port = ports_.in.template port()) + if (auto input = ports_.in.template port()) { - ws[0] = input_port.readSignal(); - ws_indices_[0] = input_port.signalVariableIndex(); + ws[U] = input.readSignal(); + ws_indices_[U] = input.signalVariableIndex(); } const auto* y = y_.getData(); const auto* yp = yp_.getData(); auto* f = f_.getData(); + evaluateInternalResidual(y, yp, nullptr, ws, f); f_.setDataUpdated(); - return 0; } - template - const Model::VariableMonitorBase* Ieeest::getMonitor() const + /** + * @brief Access the monitor + * + * @return Monitor for this model, or nullptr when the model was + * constructed without data. + */ + template + const Model::VariableMonitorBase* Ieeest::getMonitor() const { return monitor_.get(); } - template - void Ieeest::initializeMonitor() + /** + * @brief Evaluate the IEEEST internal residual + * + * @param[in] y Internal variables in IeeestInternalVariables order. + * @param[in] yp Internal derivatives in the same enum order. + * @param[in] wb Unused; IEEEST has no bus interface. + * @param[in] ws Signal values in IeeestExternalVariables order. + * @param[out] f Residuals in IeeestInternalVariables order. + * @return Zero on success. + */ + template + [[gnu::always_inline]] inline int + Ieeest::evaluateInternalResidual( + const ScalarT* y, + const ScalarT* yp, + [[maybe_unused]] const ScalarT* wb, + const ScalarT* ws, + ScalarT* f) + { + const auto X5 = static_cast(IeeestInternalVariables::X5); + const auto X6 = static_cast(IeeestInternalVariables::X6); + const auto X7 = static_cast(IeeestInternalVariables::X7); + const auto VSS = static_cast(IeeestInternalVariables::VSS); + const auto X1 = static_cast(IeeestInternalVariables::X1); + const auto X2 = static_cast(IeeestInternalVariables::X2); + const auto X3 = static_cast(IeeestInternalVariables::X3); + const auto X4 = static_cast(IeeestInternalVariables::X4); + const auto U = static_cast(IeeestExternalVariables::U); + + const ScalarT x5 = y[X5]; + const ScalarT x6 = y[X6]; + const ScalarT x7 = y[X7]; + const ScalarT vss = y[VSS]; + + const ScalarT x5_dot = yp[X5]; + const ScalarT x6_dot = yp[X6]; + const ScalarT x7_dot = yp[X7]; + + const ScalarT u = ws[U]; + + // Notch output, built factor by factor + ScalarT v4 = u; + if constexpr (order == 1) + { + const ScalarT x1 = y[X1]; + const ScalarT x1_dot = yp[X1]; + const ScalarT x1_rate = (u - x1) / b1_; + + f[X1] = -x1_dot + x1_rate; + v4 = x1 + A5_ * x1_rate; + } + if constexpr (order >= 2) + { + const ScalarT x1 = y[X1]; + const ScalarT x2 = y[X2]; + const ScalarT x1_dot = yp[X1]; + const ScalarT x2_dot = yp[X2]; + const ScalarT x2_rate = (u - x1 - b1_ * x2) / b2_; + + f[X1] = -x1_dot + x2; + f[X2] = -x2_dot + x2_rate; + v4 = x1 + A5_ * x2 + A6_ * x2_rate; + } + if constexpr (order == 3) + { + const ScalarT x3 = y[X3]; + const ScalarT x3_dot = yp[X3]; + const ScalarT x3_rate = (v4 - x3) / c1_; + + f[X3] = -x3_dot + x3_rate; + v4 = x3; + } + if constexpr (order == 4) + { + const ScalarT x3 = y[X3]; + const ScalarT x4 = y[X4]; + const ScalarT x3_dot = yp[X3]; + const ScalarT x4_dot = yp[X4]; + const ScalarT x4_rate = (v4 - x3 - c1_ * x4) / c2_; + + f[X3] = -x3_dot + x4; + f[X4] = -x4_dot + x4_rate; + v4 = x3; + } + + // Lead-lags, washout, and output limiter + const ScalarT x5_rate = (v4 - x5) / T2_; + const ScalarT v5 = x5 + T1_ * x5_rate; + const ScalarT x6_rate = (v5 - x6) / T4_; + const ScalarT v6 = x6 + T3_ * x6_rate; + const ScalarT x7_rate = (v6 - x7) / T6_; + const ScalarT v7 = Ks_ * T5_ * x7_rate; + + f[X5] = -x5_dot + x5_rate; + f[X6] = -x6_dot + x6_rate; + f[X7] = -x7_dot + x7_rate; + f[VSS] = -vss + Math::clamp(v7, Lsmin_, Lsmax_); + + return 0; + } + + // + // Private methods + // + + /** + * @brief Read the parameters out of the model data + * + * @param[in] data Parameters and monitored-variable selections. + * + * @pre Every given parameter is a finite number, and T2, T4, and T6 + * are nonnegative. + */ + template + void Ieeest::initializeParameters(const ModelDataT& data) + { + using Params = typename ModelDataT::Parameters; + + Model::ParameterReader reader(data, "Ieeest"); + + A1_ = reader.readReal(Params::A1).value_or(A1_); + A2_ = reader.readReal(Params::A2).value_or(A2_); + A3_ = reader.readReal(Params::A3).value_or(A3_); + A4_ = reader.readReal(Params::A4).value_or(A4_); + A5_ = reader.readReal(Params::A5).value_or(A5_); + A6_ = reader.readReal(Params::A6).value_or(A6_); + T1_ = reader.readReal(Params::T1).value_or(T1_); + T2_ = reader.readNonNegative(Params::T2).value_or(T2_); + T3_ = reader.readReal(Params::T3).value_or(T3_); + T4_ = reader.readNonNegative(Params::T4).value_or(T4_); + T5_ = reader.readReal(Params::T5).value_or(T5_); + T6_ = reader.readNonNegative(Params::T6).value_or(T6_); + Ks_ = reader.readReal(Params::Ks).value_or(Ks_); + Lsmin_ = reader.readReal(Params::Lsmin).value_or(Lsmin_); + Lsmax_ = reader.readReal(Params::Lsmax).value_or(Lsmax_); + Vcl_ = reader.readReal(Params::Vcl).value_or(Vcl_); + Vcu_ = reader.readReal(Params::Vcu).value_or(Vcu_); + Tdelay_ = reader.readReal(Params::Tdelay).value_or(Tdelay_); + + // Model data uses an exact zero to disable these features. + if (Vcl_ != ZERO || Vcu_ != ZERO || Tdelay_ != ZERO) + { + Log::warning() << "Ieeest: Vcl, Vcu, and Tdelay are not modeled and are ignored\n"; + } + + setDerivedParameters(); + } + + /** + * @brief Bind the monitorable variables to their internal states + */ + template + void Ieeest::initializeMonitor() { using Variable = typename ModelDataT::MonitorableVariables; + monitor_->set(Variable::vss, [this] - { return y_.getData()[11]; }); + { return y_.getData()[static_cast(IeeestInternalVariables::VSS)]; }); + } + + /** + * @brief Log the time-constant floor warning once per notch order + */ + template + void Ieeest::logTimeConstantWarning() + { + static std::atomic time_constant_warning_flag_{false}; + if (time_constant_warning_flag_.exchange(true)) + { + return; + } + Log::warning() << "Ieeest: T2, T4, and T6 below " + << TIME_CONSTANT_MINIMUM + << " s are raised to that floor to keep the stabilizer lags well posed\n"; + } + + /** + * @brief Resolve the parameter-derived constants + * + * Raises the lead-lag and washout lags to the well-posedness floor and + * assigns the notch denominator factors in the order they are realized. + */ + template + void Ieeest::setDerivedParameters() + { + if (T2_ < TIME_CONSTANT_MINIMUM || T4_ < TIME_CONSTANT_MINIMUM + || T6_ < TIME_CONSTANT_MINIMUM) + { + logTimeConstantWarning(); + } + + T2_ = std::max(T2_, TIME_CONSTANT_MINIMUM); + T4_ = std::max(T4_, TIME_CONSTANT_MINIMUM); + T6_ = std::max(T6_, TIME_CONSTANT_MINIMUM); + + b1_ = b2_ = c1_ = c2_ = ZERO; + + if constexpr (order == 1) + { + b1_ = A1_ + A3_; + } + else if constexpr (order == 2) + { + b1_ = A1_ + A3_; + b2_ = A2_ + A4_ + A1_ * A3_; + } + else if constexpr (order == 3) + { + // The quadratic factor carries the numerator. + if (A2_ != ZERO) + { + b1_ = A1_; + b2_ = A2_; + c1_ = A3_; + } + else + { + b1_ = A3_; + b2_ = A4_; + c1_ = A1_; + } + } + else if constexpr (order == 4) + { + b1_ = A1_; + b2_ = A2_; + c1_ = A3_; + c2_ = A4_; + } } } // namespace Stabilizer diff --git a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md index c023a495df..d7a77944ef 100644 --- a/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md +++ b/GridKit/Model/PhasorDynamics/Stabilizer/IEEEST/README.md @@ -1,75 +1,115 @@ # IEEEST -Standard IEEE power system stabilizer: notch filter of order up to four, two lead–lag -blocks, washout, and output limiter. +IEEEST is an IEEE power system stabilizer with a notch filter, two lead–lag +blocks, a washout, and an output limiter. + +## Notes + +- The notch order $n$ is the degree of the notch denominator. It is derived + from $A_1$–$A_4$, not given as a parameter, and a model of order $n$ has $n$ + notch states. +- The block outputs $v_4$–$v_7$ are evaluated from the states rather than + solved for. +- $V_{\mathrm{cu}}$, $V_{\mathrm{cl}}$, and $T_{\mathrm{delay}}$ are read but + not modeled; a nonzero value logs a warning. ## Block Diagram -![](../../../../../docs/Figures/stabilizer_ieeest_diagram.png) +![IEEEST stabilizer block diagram](../../../../../docs/Figures/stabilizer_ieeest_diagram.png) -Figure 1: Stabilizer IEEEST model. Figure courtesy of [PowerWorld](https://www.powerworld.com/WebHelp/) +Figure 1: IEEEST stabilizer model. Figure courtesy of the +[PowerWorld IEEEST model reference](https://www.powerworld.com/WebHelp/Content/TransientModels_HTML/Stabilizer%20IEEEST.htm). ## Model Parameters -Symbol | Units | JSON | Description | Typical Value | Note --------------|--------|---------|--------------------------------------|---------------|----- -$A_1$ | [s] | `A1` | Notch denominator coefficient | 1.013 | -$A_2$ | [s²] | `A2` | Notch denominator coefficient | 0.013 | -$A_3$ | [s] | `A3` | Notch denominator coefficient | 0.0 | -$A_4$ | [s²] | `A4` | Notch denominator coefficient | 0.0 | -$A_5$ | [s] | `A5` | Notch numerator coefficient | 1.013 | -$A_6$ | [s²] | `A6` | Notch numerator coefficient | 0.113 | -$T_1$ | [s] | `T1` | Lead–lag 1 numerator time constant | 0.0 | -$T_2$ | [s] | `T2` | Lead–lag 1 denominator time constant | 0.02 | -$T_3$ | [s] | `T3` | Lead–lag 2 numerator time constant | 0.0 | -$T_4$ | [s] | `T4` | Lead–lag 2 denominator time constant | 0.0 | -$T_5$ | [s] | `T5` | Washout numerator time constant | 1.65 | -$T_6$ | [s] | `T6` | Washout denominator time constant | 1.65 | -$K_\mathrm{s}$ | [p.u.] | `Ks` | Stabilizer gain | 3.0 | -$L_\mathrm{s}^{\min}$ | [p.u.] | `Lsmin` | Minimum stabilizer output limit | -0.1 | -$L_\mathrm{s}^{\max}$ | [p.u.] | `Lsmax` | Maximum stabilizer output limit | 0.1 | - -The IEEE 421.5 IEEEST also defines a cutout window ($V_\mathrm{cl}$, $V_\mathrm{cu}$) and an -input delay ($T_\mathrm{delay}$). These parameters are accepted for input-format -compatibility but are not modeled here. +Symbol | Units | JSON | Description | Typical Value | Note +---------------------|------------------|----------|--------------------------------------|---------------|------------ +$A_1$ | [s] | `A1` | Notch denominator coefficient | 1.013 | +$A_2$ | [$\mathrm{s}^2$] | `A2` | Notch denominator coefficient | 0.013 | +$A_3$ | [s] | `A3` | Notch denominator coefficient | 0.0 | +$A_4$ | [$\mathrm{s}^2$] | `A4` | Notch denominator coefficient | 0.0 | +$A_5$ | [s] | `A5` | Notch numerator coefficient | 1.013 | +$A_6$ | [$\mathrm{s}^2$] | `A6` | Notch numerator coefficient | 0.113 | +$T_1$ | [s] | `T1` | Lead–lag 1 numerator time constant | 0.0 | +$T_2$ | [s] | `T2` | Lead–lag 1 denominator time constant | 0.02 | +$T_3$ | [s] | `T3` | Lead–lag 2 numerator time constant | 0.0 | +$T_4$ | [s] | `T4` | Lead–lag 2 denominator time constant | 0.0 | +$T_5$ | [s] | `T5` | Washout numerator time constant | 1.65 | +$T_6$ | [s] | `T6` | Washout denominator time constant | 1.65 | +$K_s$ | [p.u.] | `Ks` | Stabilizer gain | 3.0 | +$L_s^{\max}$ | [p.u.] | `Lsmax` | Maximum stabilizer output | 0.1 | +$L_s^{\min}$ | [p.u.] | `Lsmin` | Minimum stabilizer output | -0.1 | +$V_{\mathrm{cu}}$ | [p.u.] | `Vcu` | Upper cutout voltage | 0.0 | Not modeled +$V_{\mathrm{cl}}$ | [p.u.] | `Vcl` | Lower cutout voltage | 0.0 | Not modeled +$T_{\mathrm{delay}}$ | [s] | `Tdelay` | Input delay | 0.0 | Not modeled + +Every parameter is optional and must be finite. An omitted parameter is zero, +except $T_2 = T_4 = T_6 = K_s = 1$ and $L_s^{\max} = -L_s^{\min} = 0.1$. ### Parameter Validation -A valid IEEEST parameter set must satisfy the following conditions: - ```math -a_2=a_3=a_4=0 \quad\Longrightarrow\quad a_1=0 +\begin{aligned} + T_2, T_4, T_6 + &\ge 0 \\ + L_s^{\min} + &< L_s^{\max} \\ + m + &\le n +\end{aligned} ``` ### Model Derived Parameters +A zero coefficient marks an absent term, so the degree of $1 + ps + qs^2$ is + +```math +d(p,q) = + \begin{cases} + 2 & q \ne 0 \\ + 1 & q = 0,\ p \ne 0 \\ + 0 & p = q = 0 + \end{cases} +``` + +and the notch denominator and numerator degrees are + ```math \begin{aligned} -a_0 &= 1 \\ -a_1 &= A_1 + A_3 \\ -a_2 &= A_2 + A_4 + A_1 A_3 \\ -a_3 &= A_1 A_4 + A_2 A_3 \\ -a_4 &= A_2 A_4 + n &= d(A_1,A_2) + d(A_3,A_4) \\ + m &= d(A_5,A_6) \end{aligned} ``` -The notch order is +The notch is realized as two factors in series, +$(1 + A_5 s + A_6 s^2) / [(1 + b_1 s + b_2 s^2)(1 + c_1 s + c_2 s^2)]$. The +denominator factors commute; the second-order factor goes first so it can carry +the numerator: + +```math +(b_1, b_2, c_1, c_2) = + \begin{cases} + (A_1 + A_3,\ A_2 + A_4 + A_1 A_3,\ 0,\ 0) & n \le 2 \\ + (A_1, A_2, A_3, A_4) & n \ge 3,\ A_2 \ne 0 \\ + (A_3, A_4, A_1, A_2) & n \ge 3,\ A_2 = 0 + \end{cases} +``` + +Let $\epsilon_T = 10^{-3}\ \mathrm{s}$. A lag time constant below $\epsilon_T$ +is raised to that floor in place: ```math -n = \begin{cases} -4 & a_4\ne0 \\ -3 & a_4=0,\ a_3\ne0 \\ -2 & a_4=a_3=0,\ a_2\ne0 \\ -0 & a_4=a_3=a_2=a_1=0 -\end{cases} +T_x \leftarrow \max\!(T_x, \epsilon_T), \quad x \in \{2, 4, 6\} ``` ## Model Ports Name | Port | Init | Description ----------|--------|-------|--------------------------------- -`input` | Input | Known | Required stabilizer input signal -`output` | Output | Known | Limited stabilizer output signal +---------|--------|-------|------------------------ +`input` | Input | Known | Stabilizer input signal +`output` | Output | Known | Stabilizer output + +The input is required; the output is optional. ## Model Variables @@ -77,25 +117,21 @@ Name | Port | Init | Description #### Differential -Symbol | Units | Description | Note --------|-----------|--------------------|------------------------ -$x_1$ | [p.u.] | Notch filter state | Held at zero when $n=0$ -$x_2$ | [p.u./s] | Notch filter state | Held at zero when $n=0$ -$x_3$ | [p.u./s²] | Notch filter state | Held at zero when $n<3$ -$x_4$ | [p.u./s³] | Notch filter state | Held at zero when $n<4$ -$x_5$ | [p.u.] | Lead–lag 1 state | Algebraic when $T_2=0$ -$x_6$ | [p.u.] | Lead–lag 2 state | Algebraic when $T_4=0$ -$x_7$ | [p.u.] | Washout state | Algebraic when $T_6=0$ +Symbol | Units | Description | Note +-------|----------|----------------------------|------------------ +$x_1$ | [p.u.] | First notch factor output | $n \ge 1$ +$x_2$ | [p.u./s] | Rate of $x_1$ | $n \ge 2$ +$x_3$ | [p.u.] | Second notch factor output | $n \ge 3$ +$x_4$ | [p.u./s] | Rate of $x_3$ | $n = 4$ +$x_5$ | [p.u.] | Lead–lag 1 state | State 5 in Fig. 1 +$x_6$ | [p.u.] | Lead–lag 2 state | State 6 in Fig. 1 +$x_7$ | [p.u.] | Washout state | State 7 in Fig. 1 #### Algebraic -Symbol | Units | Description | Note -------------------|--------|------------------------------------------|----- -$v_4$ | [p.u.] | Notch filter output | -$v_5$ | [p.u.] | Lead–lag 1 output | -$v_6$ | [p.u.] | Lead–lag 2 output | -$v_7$ | [p.u.] | Unlimited stabilizer signal | -$V_{\mathrm{ss}}$ | [p.u.] | Limited stabilizer signal (model output) | +Symbol | Units | Description | Note +------------------|--------|---------------------------|-------------------------- +$V_{\mathrm{ss}}$ | [p.u.] | Limited stabilizer output | Published through `output` ### External Variables @@ -106,8 +142,8 @@ None. #### Algebraic Symbol | Units | Description | Note --------|--------|-------------------------|----- -$u$ | [p.u.] | Stabilizer input signal | +-------|--------|-------------------------|-------------------- +$u$ | [p.u.] | Stabilizer input signal | Signal port `input` ## Model Equations @@ -115,63 +151,84 @@ Smooth functions: [`clamp`](../../../../CommonMath.md#clamp). ### Internal Equations -#### Differential +Define the notch rates, the first-factor output $w$, and the block outputs: ```math \begin{aligned} -0 &= -\dot{x}_k+x_{k+1}, && 1\le k0 \\ -0 &= -\dot{x}_k, && n0 \\ -u & n=0 -\end{cases} \\ -0 &= \begin{cases} --T_2(v_5-x_5)+T_1(v_4-x_5) & T_2\ne0 \\ --v_5+v_4 & T_2=0 -\end{cases} \\ -0 &= \begin{cases} --T_4(v_6-x_6)+T_3(v_5-x_6) & T_4\ne0 \\ --v_6+v_5 & T_4=0 -\end{cases} \\ -0 &= \begin{cases} --T_6v_7+K_\mathrm{s}T_5(v_6-x_7) & T_6\ne0 \\ --v_7+K_\mathrm{s}v_6 & T_6=0 -\end{cases} \\ -0 &= -V_{\mathrm{ss}}+\text{clamp}(v_7;L_\mathrm{s}^{\min},L_\mathrm{s}^{\max}) + 0 &= -\dot{x}_1 + f_1 && n = 1 \\ + 0 &= -\dot{x}_1 + x_2 && n \ge 2 \\ + 0 &= -\dot{x}_2 + f_2 && n \ge 2 \\ + 0 &= -\dot{x}_3 + \dfrac{1}{c_1}(w - x_3) && n = 3 \\ + 0 &= -\dot{x}_3 + x_4 && n = 4 \\ + 0 &= -\dot{x}_4 + \dfrac{1}{c_2}(w - x_3 - c_1 x_4) && n = 4 \\ + 0 &= -\dot{x}_5 + \dfrac{1}{T_2}(v_4 - x_5) \\ + 0 &= -\dot{x}_6 + \dfrac{1}{T_4}(v_5 - x_6) \\ + 0 &= -\dot{x}_7 + \dfrac{1}{T_6}(v_6 - x_7) \end{aligned} ``` +#### Algebraic + +```math +0 = -V_{\mathrm{ss}} + \text{clamp}(v_7, L_s^{\min}, L_s^{\max}) +``` + ### External Equations None. ## Initialization -The initial input determines the steady state; all derivatives initialize to zero. +### Input Initialization + +```math +u \leftarrow \text{input signal} +``` + +### Internal Initialization + +All derivatives initialize to zero. A constant input is an equilibrium: each +notch factor and lead–lag passes it, and the washout blocks it. ```math \begin{aligned} -x_1 &\leftarrow \begin{cases}u & n>0 \\ 0 & n=0\end{cases} \\ -x_2,x_3,x_4 &\leftarrow 0 \\ -x_5,x_6,x_7,v_4,v_5,v_6 &\leftarrow u \\ -v_7 &\leftarrow \begin{cases}0 & T_6\ne0 \\ K_\mathrm{s}u & T_6=0\end{cases} \\ -V_{\mathrm{ss}} &\leftarrow \text{clamp}(v_7;L_\mathrm{s}^{\min},L_\mathrm{s}^{\max}) + x_1, x_3, x_5, x_6, x_7 &\leftarrow u \\ + x_2, x_4 &\leftarrow 0 \\ + V_{\mathrm{ss}} &\leftarrow \text{clamp}(0, L_s^{\min}, L_s^{\max}) \end{aligned} ``` +### Output Initialization + +None. + ## Monitors Monitor | Units | Description | Note ---------|--------|---------------------------|-------------------------------- -`vss` | [p.u.] | Limited stabilizer signal | $V_{\mathrm{ss}}$; model output +--------|--------|---------------------------|------------------ +`vss` | [p.u.] | Limited stabilizer output | $V_{\mathrm{ss}}$ diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/Genrou.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/Genrou.hpp index 8241aac801..f8fd7ec340 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/Genrou.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/Genrou.hpp @@ -121,12 +121,13 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT) override final; int evaluateResidual() override final; + Model::ConfigurationChecks verify() const override final; + // Still to be implemented int evaluateJacobian() override final; diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouImpl.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouImpl.hpp index c052fec4f2..cfad1a9ffc 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouImpl.hpp @@ -349,25 +349,23 @@ namespace GridKit * @brief verify method checks that attached signals are also linked */ template - int Genrou::verify() const + Model::ConfigurationChecks Genrou::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("Genrou"); auto pmech_port = ports_.in.template port(); if (pmech_port.connected() && !pmech_port.linked()) { - Log::error() << "Genrou: pmech signal attached with no linked governor\n"; - ret += 1; + checks.fail("pmech signal attached with no linked governor"); } auto efd_port = ports_.in.template port(); if (efd_port.connected() && !efd_port.linked()) { - Log::error() << "Genrou: efd signal attached with no linked exciter\n"; - ret += 1; + checks.fail("efd signal attached with no linked exciter"); } - return ret; + return checks; } /** diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/Gensal.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/Gensal.hpp index cf3d379c6d..9dff277405 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/Gensal.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/Gensal.hpp @@ -87,12 +87,13 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; + Model::ConfigurationChecks verify() const override final; + // Still to be implemented int evaluateJacobian() override final; diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/GensalImpl.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/GensalImpl.hpp index 8435bdb713..9bfa40b916 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/GensalImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENSAL/GensalImpl.hpp @@ -225,25 +225,23 @@ namespace GridKit * @brief verify method checks that attached signals are also linked */ template - int Gensal::verify() const + Model::ConfigurationChecks Gensal::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("Gensal"); auto pmech_port = ports_.in.template port(); if (pmech_port.connected() && !pmech_port.linked()) { - Log::error() << "Gensal: pmech signal attached with no linked governor\n"; - ret += 1; + checks.fail("pmech signal attached with no linked governor"); } auto efd_port = ports_.in.template port(); if (efd_port.connected() && !efd_port.linked()) { - Log::error() << "Gensal: efd signal attached with no linked exciter\n"; - ret += 1; + checks.fail("efd signal attached with no linked exciter"); } - return ret; + return checks; } /** diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassical.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassical.hpp index ced9a652e3..b5f5163f0d 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassical.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassical.hpp @@ -76,12 +76,13 @@ namespace GridKit int setGridKitComponentID(IdxT) override final; int allocate() override final; - int verify() const override final; int initialize() override final; int tagDifferentiable() override final; int setAbsoluteTolerance(RealT rel_tol) override final; int evaluateResidual() override final; + Model::ConfigurationChecks verify() const override final; + // Still to be implemented int evaluateJacobian() override final; diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassicalImpl.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassicalImpl.hpp index 0c3b12cf18..0d79c2dd46 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassicalImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GenClassical/GenClassicalImpl.hpp @@ -166,25 +166,23 @@ namespace GridKit * @brief verify method checks that attached signals are also linked */ template - int GenClassical::verify() const + Model::ConfigurationChecks GenClassical::verify() const { - int ret = 0; + Model::ConfigurationChecks checks("GenClassical"); auto pmech_port = ports_.in.template port(); if (pmech_port.connected() && !pmech_port.linked()) { - Log::error() << "GenClassical: pmech signal attached with no linked governor\n"; - ret += 1; + checks.fail("pmech signal attached with no linked governor"); } auto efd_port = ports_.in.template port(); if (efd_port.connected() && !efd_port.linked()) { - Log::error() << "GenClassical: efd signal attached with no linked exciter\n"; - ret += 1; + checks.fail("efd signal attached with no linked exciter"); } - return ret; + return checks; } /** diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index f55d8aa68e..f9fc722b5d 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -75,9 +75,10 @@ namespace GridKit int setGridKitComponentID(IdxT component_id) override; int allocate() override; - int verify() const override; int initialize() override; + Model::ConfigurationChecks verify() const override; + bool hasJacobian() override; void initializeMonitor(); diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 53acd0d42f..50ac062a09 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -1,11 +1,15 @@ +#include #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -211,12 +215,50 @@ namespace GridKit addComponent(hygov); } + using IeeestDataT = IeeestData; + using IeeestFactoryT = IeeestFactory; + + std::array, 5> stabilizers_by_order; + for (const auto& stabdata : data.stabilizer) + { + const auto order = IeeestFactoryT::readNotchOrder(stabdata); + stabilizers_by_order[order].push_back(&stabdata); + } + // Add IEEEST stabilizers before exciters that consume their output during // initialization. - for (const auto& stabdata : data.stabilizer) + for (const auto* stabdata : stabilizers_by_order[0]) + { + auto* stabilizer = new Ieeest(*stabdata); + stabilizer->getPorts().connect(*stabdata, signal_nodes_); + addComponent(stabilizer); + } + + for (const auto* stabdata : stabilizers_by_order[1]) + { + auto* stabilizer = new Ieeest(*stabdata); + stabilizer->getPorts().connect(*stabdata, signal_nodes_); + addComponent(stabilizer); + } + + for (const auto* stabdata : stabilizers_by_order[2]) + { + auto* stabilizer = new Ieeest(*stabdata); + stabilizer->getPorts().connect(*stabdata, signal_nodes_); + addComponent(stabilizer); + } + + for (const auto* stabdata : stabilizers_by_order[3]) { - auto* stabilizer = new Ieeest(stabdata); - stabilizer->getPorts().connect(stabdata, signal_nodes_); + auto* stabilizer = new Ieeest(*stabdata); + stabilizer->getPorts().connect(*stabdata, signal_nodes_); + addComponent(stabilizer); + } + + for (const auto* stabdata : stabilizers_by_order[4]) + { + auto* stabilizer = new Ieeest(*stabdata); + stabilizer->getPorts().connect(*stabdata, signal_nodes_); addComponent(stabilizer); } @@ -456,10 +498,8 @@ namespace GridKit } // Verify component configuration - int errorCount = this->verify(); - if (errorCount > 0) + if (!this->verifyAndLog()) { - Log::error() << "Component errors: " << errorCount << std::endl; throw std::runtime_error("SystemModel allocation failed"); } @@ -491,21 +531,25 @@ namespace GridKit /** * @brief Verify all components are configured correctly * - * This method accumulates and returns the number of errors given by - * components. It should return 0 when all is well. + * Collects every problem reported by the components, each prefixed with + * the component ID. passed() is true when all is well. */ template - int SystemModel::verify() const + Model::ConfigurationChecks SystemModel::verify() const { - int ret = 0; + Model::ConfigurationChecks checks; // Verify components for (const auto& component : components_) { - ret += component->verify(); + const auto component_checks = component->verify(); + for (const auto& error : component_checks.errors()) + { + checks.fail(std::format("component {}: {}", component->getGridKitComponentID(), error)); + } } - return ret; + return checks; } /** diff --git a/GridKit/Model/VariableMonitorImpl.hpp b/GridKit/Model/VariableMonitorImpl.hpp index 682692c3c5..1235295cd4 100644 --- a/GridKit/Model/VariableMonitorImpl.hpp +++ b/GridKit/Model/VariableMonitorImpl.hpp @@ -27,21 +27,23 @@ namespace GridKit */ template typename eval_type, - template typename model_data_type> - class VariableMonitor, model_data_type> + template typename eval_type, + template typename model_data_type, + auto... model_parameters> + class VariableMonitor, model_data_type> : public VariableMonitorBase { template friend class VariableMonitorController; - public: /// Underlying scalar value type - using ScalarT = scalar_type; + using ScalarT = scalar_type; /// Index type - using IdxT = index_type; + using IdxT = index_type; /// Underlying real value type - using RealT = typename GridKit::ScalarTraits::RealT; + using RealT = typename GridKit::ScalarTraits::RealT; + + public: /// Type of (EvalT)Data class expected to have MonitorableVariables enum using ObjData = model_data_type; /// Enum of valid monitorable variables diff --git a/GridKit/Testing/Testing.hpp b/GridKit/Testing/Testing.hpp index ba6839f4fd..2f676099b3 100644 --- a/GridKit/Testing/Testing.hpp +++ b/GridKit/Testing/Testing.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace GridKit @@ -234,5 +235,16 @@ namespace GridKit { func(std::forward(args)...); }); } + /// True when building a model throws @ref std::invalid_argument or the + /// configuration checks it returns report a problem. + template + inline bool configurationRejected(BuildT build) + { + bool passed = false; + const bool threw = throws([&]() + { passed = build().passed(); }); + return threw || !passed; + } + } // namespace Testing } // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/BranchTests.hpp b/tests/UnitTests/PhasorDynamics/BranchTests.hpp index ba09fd4e53..1de06155e1 100644 --- a/tests/UnitTests/PhasorDynamics/BranchTests.hpp +++ b/tests/UnitTests/PhasorDynamics/BranchTests.hpp @@ -307,7 +307,7 @@ namespace GridKit PhasorDynamics::Bus bus2(1.0, 0.0); PhasorDynamics::Branch valid_branch(&bus1, &bus2, 0.0, 0.1, 0.0, 0.0); - success *= (valid_branch.verify() == 0); + success *= (valid_branch.verify().passed()); const auto previous_verbosity = Log::verbosity(); // Suppress expected errors from the invalid branch configurations below. @@ -315,17 +315,17 @@ namespace GridKit Log::setVerbosity(Log::Verbosity::NONE); PhasorDynamics::Branch zero_impedance_branch(&bus1, &bus2, 0.0, 0.0, 0.0, 0.0); - success *= (zero_impedance_branch.verify() != 0); + success *= (!zero_impedance_branch.verify().passed()); PhasorDynamics::Branch zero_tap_branch(&bus1, &bus2, 0.0, 0.1, 0.0, 0.0, 0.0, 0.0); - success *= (zero_tap_branch.verify() != 0); + success *= (!zero_tap_branch.verify().passed()); PhasorDynamics::Branch negative_tap_branch(&bus1, &bus2, 0.0, 0.1, 0.0, 0.0, -1.0, 0.0); - success *= (negative_tap_branch.verify() != 0); + success *= (!negative_tap_branch.verify().passed()); const RealT nan = std::numeric_limits::quiet_NaN(); PhasorDynamics::Branch nonfinite_branch(&bus1, &bus2, nan, 0.1, 0.0, 0.0); - success *= (nonfinite_branch.verify() != 0); + success *= (!nonfinite_branch.verify().passed()); Log::setVerbosity(previous_verbosity); return success.report(__func__); diff --git a/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp b/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp index 2d7fa6a931..c1e869a052 100644 --- a/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp +++ b/tests/UnitTests/PhasorDynamics/BusSignalVoltageInTests.hpp @@ -32,21 +32,6 @@ namespace GridKit BusSignalVoltageInTests() = default; ~BusSignalVoltageInTests() = default; - /// True if verify() throws, as it must for a misconnected bus - template - static bool verifyThrows(const BusLike& bus) - { - try - { - bus.verify(); - } - catch (const std::runtime_error&) - { - return true; - } - return false; - } - /// Constructor, allocation, and initialization checks TestOutcome constructor() { @@ -69,14 +54,14 @@ namespace GridKit success *= isEqual(bus->Ir(), 0.0); success *= isEqual(bus->Ii(), 0.0); // Voltage inlets are mandatory: an unconnected bus fails verification - success *= verifyThrows(*bus); + success *= !bus->verify().passed(); delete bus; // Initial voltage arguments are accepted for interface uniformity but not used bus = new BusT(Vr, Vi); bus->allocate(); bus->initialize(); - success *= verifyThrows(*bus); + success *= !bus->verify().passed(); delete bus; bus = nullptr; @@ -110,7 +95,7 @@ namespace GridKit bus.getPorts().in.template port().connect(&vi_node); bus.allocate(); bus.initialize(); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); // Voltage is read straight from the signals, no evaluation needed success *= isEqual(bus.Vr(), Vr); @@ -131,7 +116,7 @@ namespace GridKit BusT plain; plain.allocate(); plain.initialize(); - success *= verifyThrows(plain); + success *= !plain.verify().passed(); bool threw = false; try { @@ -177,7 +162,7 @@ namespace GridKit bus.allocate(); bus.initialize(); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); success *= ir_node.linked(); success *= ii_node.linked(); success *= (ir_node.getVariableIndex() == INVALID_INDEX); @@ -203,7 +188,7 @@ namespace GridKit return success.report(__func__); } - /// verify() throws for voltage inlets that are unconnected or unlinked + /// verify() reports errors for voltage inlets that are unconnected or unlinked TestOutcome verifyUnlinked() { TestStatus success = true; @@ -221,20 +206,20 @@ namespace GridKit bus.initialize(); // vr connected but unlinked, vi not connected - success *= verifyThrows(bus); + success *= !bus.verify().passed(); ScalarT Vr{0.1}; IdxT vr_index{0}; vr_node.link(&Vr, &vr_index); - success *= verifyThrows(bus); + success *= !bus.verify().passed(); bus.getPorts().in.template port().connect(&vi_node); - success *= verifyThrows(bus); + success *= !bus.verify().passed(); ScalarT Vi{0.2}; IdxT vi_index{1}; vi_node.link(&Vi, &vi_index); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); Log::setVerbosity(previous_verbosity); diff --git a/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp b/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp index 4ca19516ff..5a1d52c60e 100644 --- a/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp +++ b/tests/UnitTests/PhasorDynamics/BusSignalVoltageOutTests.hpp @@ -33,21 +33,6 @@ namespace GridKit BusSignalVoltageOutTests() = default; ~BusSignalVoltageOutTests() = default; - /// True if verify() throws, as it must for a misconnected bus - template - static bool verifyThrows(const BusLike& bus) - { - try - { - bus.verify(); - } - catch (const std::runtime_error&) - { - return true; - } - return false; - } - /// Constructor, allocation, and initialization checks TestOutcome constructor() { @@ -117,7 +102,7 @@ namespace GridKit bus.setVariableIndex(1, vi_index); bus.initialize(); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); success *= vr_node.linked(); success *= vi_node.linked(); success *= isEqual(vr_node.read(), Vr); @@ -153,7 +138,7 @@ namespace GridKit bus.initialize(); const auto previous_verbosity = Log::verbosity(); Log::setVerbosity(Log::Verbosity::NONE); // expected errors - success *= verifyThrows(bus); + success *= !bus.verify().passed(); Log::setVerbosity(previous_verbosity); } @@ -167,7 +152,7 @@ namespace GridKit bus.getPorts().in.template port().connect(&ii_node); bus.allocate(); bus.initialize(); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); bus.evaluateResidual(); success *= isEqual(bus.Ir(), Ir); @@ -191,7 +176,7 @@ namespace GridKit return success.report(__func__); } - /// verify() throws for current inlets that are unconnected or unlinked + /// verify() reports errors for current inlets that are unconnected or unlinked TestOutcome verifyUnlinked() { TestStatus success = true; @@ -209,17 +194,17 @@ namespace GridKit bus.allocate(); bus.initialize(); - success *= verifyThrows(bus); + success *= !bus.verify().passed(); ScalarT Ir{0.1}; IdxT ir_index{0}; ir_node.link(&Ir, &ir_index); - success *= verifyThrows(bus); + success *= !bus.verify().passed(); ScalarT Ii{0.2}; IdxT ii_index{1}; ii_node.link(&Ii, &ii_index); - success *= (bus.verify() == 0); + success *= (bus.verify().passed()); Log::setVerbosity(previous_verbosity); diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index 646b18ec2b..b430e75988 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -3,6 +3,10 @@ # - Slaven Peles #]] +add_executable(test_phasor_model_configuration runModelConfigurationTests.cpp) +target_link_libraries( + test_phasor_model_configuration GridKit::phasor_dynamics_core GridKit::testing) + add_executable(test_phasor_bus runBusTests.cpp) target_link_libraries( test_phasor_bus @@ -204,6 +208,7 @@ target_link_libraries( GridKit::phasor_dynamics_systemmodel GridKit::testing) +add_test(NAME PhasorDynamicsModelConfigurationTest COMMAND test_phasor_model_configuration) add_test(NAME PhasorDynamicsBusTest COMMAND test_phasor_bus) add_test(NAME PhasorDynamicsBusSignalVoltageOutTest COMMAND test_phasor_bus_signal_voltage_out) add_test(NAME PhasorDynamicsBusSignalVoltageInTest COMMAND test_phasor_bus_signal_voltage_in) diff --git a/tests/UnitTests/PhasorDynamics/ControllerReecbTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerReecbTests.hpp index 65568cecb7..3c6e8ad660 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerReecbTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerReecbTests.hpp @@ -93,7 +93,7 @@ namespace GridKit Fixture configured(makeData()); success *= (configured.reecb.size() == static_cast(Utilities::enum_size())); success *= (configured.reecb.getMonitor() != nullptr); - success *= (configured.reecb.verify() == 0); + success *= (configured.reecb.verify().passed()); success *= (configured.reecb.initialize() != 0); success *= (configured.reecb.allocate() == 0); success *= (configured.reecb.tagDifferentiable() == 0); @@ -111,20 +111,20 @@ namespace GridKit } Fixture documented_defaults(makeMinimalData()); - success *= (documented_defaults.reecb.verify() == 0); + success *= (documented_defaults.reecb.verify().passed()); success *= defaultsMatchDocumentedValues(); auto missing_mva_data = makeMinimalData(); missing_mva_data.parameters.erase(Params::mva); Fixture missing_mva(missing_mva_data); - success *= (missing_mva.reecb.verify() > 0); + success *= (!missing_mva.reecb.verify().passed()); // Integer JSON values are accepted for real parameters; booleans are // not numeric. auto integer_numeric = makeData(); integer_numeric.parameters[Params::mva] = static_cast(100); Fixture integer_parameter(integer_numeric); - success *= (integer_parameter.reecb.verify() == 0); + success *= (integer_parameter.reecb.verify().passed()); success *= invalidParameterCase(Params::mva, true); const RealT nan = std::numeric_limits::quiet_NaN(); @@ -215,7 +215,7 @@ namespace GridKit auto data = makeData(); data.parameters[flag] = value; Fixture model(data); - success *= (model.reecb.verify() == 0); + success *= (model.reecb.verify().passed()); } for (const IdxT value : invalid_integral_flag_values) @@ -231,7 +231,7 @@ namespace GridKit PhasorDynamics::Controller::Reecb busless(nullptr, makeData()); busless.setSystemBase(kNominalFrequency, kSystemBaseVa); - success *= (busless.verify() > 0); + success *= (!busless.verify().passed()); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); @@ -1876,7 +1876,7 @@ namespace GridKit bool prepare(RealT iqcmd, RealT ipcmd) { const bool ready = (bus.allocate() == 0) && (reecb.allocate() == 0) - && (reecb.verify() == 0) && (bus.initialize() == 0); + && (reecb.verify().passed()) && (bus.initialize() == 0); if (!ready) { std::cout << "REECB fixture preparation failed\n"; @@ -2296,7 +2296,7 @@ namespace GridKit auto data = makeData(); data.parameters[parameter] = value; Fixture fixture(data); - return fixture.reecb.verify() > 0; + return !fixture.reecb.verify().passed(); } template @@ -2305,7 +2305,7 @@ namespace GridKit PhasorDynamics::SignalNode unlinked_node; Fixture fixture(makeData()); fixture.reecb.getPorts().in.template port().connect(&unlinked_node); - return fixture.reecb.verify() > 0; + return !fixture.reecb.verify().passed(); } /// Fill state and derivative with a recognizable ramp, restoring the diff --git a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp index e6f90ca2e7..04b63fadcb 100644 --- a/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ControllerRepcaTests.hpp @@ -57,27 +57,27 @@ namespace GridKit PhasorDynamics::Controller::Repca empty(&bus); success *= (empty.size() == static_cast(Utilities::enum_size())); success *= (empty.getMonitor() == nullptr); - success *= (empty.verify() > 0); + success *= (!empty.verify().passed()); Fixture configured(makeData()); configured.attachAllInputs(); success *= (configured.repca.size() == static_cast(Utilities::enum_size())); success *= (configured.repca.getMonitor() != nullptr); - success *= (configured.repca.verify() == 0); + success *= (configured.repca.verify().passed()); Fixture documented_defaults(makeMinimalData()); documented_defaults.attachAllInputs(); - success *= (documented_defaults.repca.verify() == 0); + success *= (documented_defaults.repca.verify().passed()); success *= defaultsMatchDocumentedValues(); auto integer_numeric = makeData(); integer_numeric.parameters[Params::mva] = static_cast(100); Fixture integer_parameter(integer_numeric); integer_parameter.attachAllInputs(); - success *= (integer_parameter.repca.verify() == 0); + success *= (integer_parameter.repca.verify().passed()); PhasorDynamics::Controller::Repca missing_signals(&bus, makeData()); - success *= (missing_signals.verify() > 0); + success *= (!missing_signals.verify().passed()); success *= invalidParameterCase(Params::mva, 0.0); success *= invalidParameterCase(Params::Tfv, -0.1); @@ -143,7 +143,7 @@ namespace GridKit { Fixture nonfinite_system_base(makeData(), 1.0, 0.0, infinity); nonfinite_system_base.attachAllInputs(); - success *= (nonfinite_system_base.repca.verify() > 0); + success *= (!nonfinite_system_base.repca.verify().passed()); } { auto tiny_base_data = makeData(); @@ -153,7 +153,7 @@ namespace GridKit 0.0, std::numeric_limits::max()); overflowing_base_ratio.attachAllInputs(); - success *= (overflowing_base_ratio.repca.verify() > 0); + success *= (!overflowing_base_ratio.repca.verify().passed()); } const std::array flag_parameters{{ @@ -182,7 +182,7 @@ namespace GridKit data.parameters[flag] = value; Fixture model(data); model.attachAllInputs(); - success *= (model.repca.verify() == 0); + success *= (model.repca.verify().passed()); } for (const IdxT value : invalid_integral_flag_values) @@ -197,7 +197,7 @@ namespace GridKit } PhasorDynamics::Controller::Repca busless(nullptr, makeData()); - success *= (busless.verify() > 0); + success *= (!busless.verify().passed()); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); @@ -1398,7 +1398,7 @@ namespace GridKit bool prepare(RealT qext, RealT pext) { const bool success = (bus.allocate() == 0) && (repca.allocate() == 0) - && (repca.verify() == 0) && (bus.initialize() == 0); + && (repca.verify().passed()) && (bus.initialize() == 0); if (!success) { std::cout << "REPCA fixture preparation failed\n"; @@ -1721,7 +1721,7 @@ namespace GridKit data.parameters[parameter] = value; Fixture fixture(data); fixture.attachAllInputs(); - return fixture.repca.verify() > 0; + return !fixture.repca.verify().passed(); } template @@ -1731,7 +1731,7 @@ namespace GridKit fixture.attachAllInputs(); PhasorDynamics::SignalNode unlinked_node; fixture.repca.getPorts().in.template port().connect(&unlinked_node); - return fixture.repca.verify() > 0; + return !fixture.repca.verify().passed(); } /// Fill state and derivative with a recognizable ramp, restoring the diff --git a/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp index 1288eee310..c00c3a3f2f 100644 --- a/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ConverterRegcaTests.hpp @@ -56,26 +56,26 @@ namespace GridKit PhasorDynamics::Converter::Regca configured(&bus, makeData()); success *= (configured.size() == static_cast(Utilities::enum_size())); success *= (configured.getMonitor() != nullptr); - success *= (configured.verify() == 0); + success *= (configured.verify().passed()); const auto previous_verbosity = Log::verbosity(); // Suppress expected errors and warnings from the invalid cases below. // Use EVERYTHING to inspect those diagnostics. Log::setVerbosity(Log::Verbosity::NONE); - success *= (minimal.verify() > 0); + success *= (!minimal.verify().passed()); for (const Params parameter : {Params::Tg, Params::p0, Params::q0}) { auto data = makeData(); data.parameters.erase(parameter); PhasorDynamics::Converter::Regca missing(&bus, data); - success *= (missing.verify() > 0); + success *= (!missing.verify().passed()); } auto bad_switch = makeData(); bad_switch.parameters[Params::sL] = static_cast(2); PhasorDynamics::Converter::Regca bad_switch_model(&bus, bad_switch); - success *= (bad_switch_model.verify() > 0); + success *= (!bad_switch_model.verify().passed()); success *= invalidParameterCase(bus, Params::mva, 0.0); success *= invalidParameterCase(bus, Params::IL1, -0.1); @@ -95,12 +95,12 @@ namespace GridKit // A null bus and an attached command with no linked source count as // configuration errors on the same footing as bad parameters. PhasorDynamics::Converter::Regca busless(nullptr, makeData()); - success *= (busless.verify() > 0); + success *= (!busless.verify().passed()); PhasorDynamics::SignalNode unlinked_node; PhasorDynamics::Converter::Regca unlinked(&bus, makeData()); unlinked.getPorts().in.template port().connect(&unlinked_node); - success *= (unlinked.verify() > 0); + success *= (!unlinked.verify().passed()); // Zero time constants are raised to the well-posedness floor with a // warning, and the raised model still initializes to zero residuals. @@ -758,7 +758,7 @@ namespace GridKit bool prepare() { const bool success = (bus.allocate() == 0) && (regca.allocate() == 0) - && (regca.verify() == 0) && (bus.initialize() == 0); + && (regca.verify().passed()) && (bus.initialize() == 0); if (!success) { std::cout << "REGCA fixture preparation failed\n"; @@ -954,7 +954,7 @@ namespace GridKit auto data = makeData(); data.parameters[param] = value; PhasorDynamics::Converter::Regca model(&bus, data); - return model.verify() > 0; + return !model.verify().passed(); } bool allResidualsZero(PhasorDynamics::Converter::Regca& regca) const diff --git a/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp b/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp index 05fc850b01..d16180f63a 100644 --- a/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ExciterEsdc1aTests.hpp @@ -59,7 +59,7 @@ namespace GridKit PhasorDynamics::Exciter::Esdc1a empty(&bus); success *= (empty.size() == static_cast(Utilities::enum_size())); success *= (empty.getMonitor() == nullptr); - success *= (empty.verify() > 0); + success *= (!empty.verify().passed()); Fixture configured(makeData()); success *= (configured.esdc1a.size() == static_cast(Utilities::enum_size())); @@ -74,7 +74,7 @@ namespace GridKit // A model without the required efd output assignment is rejected. PhasorDynamics::Exciter::Esdc1a unassigned(&bus, makeData()); - success *= (unassigned.verify() > 0); + success *= (!unassigned.verify().passed()); success *= invalidParameterCase(Params::Ka, 0.0); success *= invalidParameterCase(Params::Ta, -0.1); @@ -120,26 +120,26 @@ namespace GridKit reversed_saturation.parameters[Params::E2] = 2.8; reversed_saturation.parameters[Params::Se2] = 0.08; Fixture reversed_saturation_fixture(reversed_saturation); - success *= (reversed_saturation_fixture.esdc1a.verify() == 0); + success *= (reversed_saturation_fixture.esdc1a.verify().passed()); auto crossed_ascending = makeData(); crossed_ascending.parameters[Params::Se1] = 0.33; crossed_ascending.parameters[Params::Se2] = 0.08; Fixture crossed_ascending_fixture(crossed_ascending); - success *= (crossed_ascending_fixture.esdc1a.verify() > 0); + success *= (!crossed_ascending_fixture.esdc1a.verify().passed()); auto crossed_descending = makeData(); crossed_descending.parameters[Params::E1] = 3.7; crossed_descending.parameters[Params::E2] = 2.8; Fixture crossed_descending_fixture(crossed_descending); - success *= (crossed_descending_fixture.esdc1a.verify() > 0); + success *= (!crossed_descending_fixture.esdc1a.verify().passed()); // Integer JSON values are accepted for real parameters; booleans are // not numeric. auto integer_real = makeData(); integer_real.parameters[Params::Ka] = static_cast(40); Fixture integer_real_fixture(integer_real); - success *= (integer_real_fixture.esdc1a.verify() == 0); + success *= (integer_real_fixture.esdc1a.verify().passed()); success *= invalidParameterCase(Params::Ka, true); // Binary selectors accept JSON booleans only. @@ -148,7 +148,7 @@ namespace GridKit boolean_switches.parameters[Params::exclim] = false; Fixture boolean_switch_fixture(boolean_switches); boolean_switch_fixture.attachAllInputs(); - success *= (boolean_switch_fixture.esdc1a.verify() == 0); + success *= (boolean_switch_fixture.esdc1a.verify().passed()); for (const Params flag : {Params::Spdmlt, Params::exclim}) { @@ -164,12 +164,12 @@ namespace GridKit auto speed_required = makeData(); speed_required.parameters[Params::Spdmlt] = true; Fixture speed_required_fixture(speed_required); - success *= (speed_required_fixture.esdc1a.verify() > 0); + success *= (!speed_required_fixture.esdc1a.verify().passed()); PhasorDynamics::SignalNode busless_efd_node; PhasorDynamics::Exciter::Esdc1a busless(nullptr, makeData()); busless.getPorts().out.template port().connect(&busless_efd_node); - success *= (busless.verify() > 0); + success *= (!busless.verify().passed()); success *= unlinkedSignalRejected(); success *= unlinkedSignalRejected(); @@ -940,7 +940,7 @@ namespace GridKit bool prepare(RealT efd) { const bool success = (bus.allocate() == 0) && (esdc1a.allocate() == 0) - && (esdc1a.verify() == 0) && (bus.initialize() == 0); + && (esdc1a.verify().passed()) && (bus.initialize() == 0); if (!success) { std::cout << "ESDC1A fixture preparation failed\n"; @@ -1193,8 +1193,12 @@ namespace GridKit { auto data = makeData(); data.parameters[parameter] = value; - Fixture fixture(data); - return fixture.esdc1a.verify() > 0; + return configurationRejected( + [&]() + { + Fixture fixture(data); + return fixture.esdc1a.verify(); + }); } template @@ -1203,7 +1207,7 @@ namespace GridKit PhasorDynamics::SignalNode unlinked_node; Fixture fixture(makeData()); fixture.esdc1a.getPorts().in.template port().connect(&unlinked_node); - return fixture.esdc1a.verify() > 0; + return !fixture.esdc1a.verify().passed(); } template diff --git a/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp b/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp index 2fbb230660..757b2dcf92 100644 --- a/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp +++ b/tests/UnitTests/PhasorDynamics/ExciterIeeet1Tests.hpp @@ -200,7 +200,7 @@ namespace GridKit bus.allocate(); exciter.allocate(); - success *= (exciter.verify() != 0); + success *= (!exciter.verify().passed()); Log::setVerbosity(previous_verbosity); return success.report(__func__); diff --git a/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp b/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp index 19ad86a58c..9898a0f9c0 100644 --- a/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp +++ b/tests/UnitTests/PhasorDynamics/ExciterSexsPtiTests.hpp @@ -72,7 +72,7 @@ namespace GridKit exciter.allocate(); efd_node.init(1.2); - success *= (exciter.verify() == 0); + success *= (exciter.verify().passed()); exciter.initialize(); exciter.evaluateResidual(); @@ -124,7 +124,7 @@ namespace GridKit exciter.allocate(); efd_node.init(1.2); - success *= (exciter.verify() == 0); + success *= (exciter.verify().passed()); exciter.initialize(); exciter.evaluateResidual(); @@ -281,12 +281,12 @@ namespace GridKit auto missing = makeTestData(); missing.parameters.erase(Parameter::K); PhasorDynamics::Exciter::SexsPti missing_model(&bus, missing); - success *= (missing_model.verify() > 0); + success *= (!missing_model.verify().passed()); auto invalid = makeTestData(); invalid.parameters[Parameter::Tb] = 0.0; PhasorDynamics::Exciter::SexsPti invalid_model(&bus, invalid); - success *= (invalid_model.verify() > 0); + success *= (!invalid_model.verify().passed()); Log::setVerbosity(previous_verbosity); return success.report(__func__); @@ -331,7 +331,7 @@ namespace GridKit // Suppress the expected missing-EFD configuration error below. // Use EVERYTHING to inspect the diagnostic. Log::setVerbosity(Log::Verbosity::NONE); - success *= (missing_efd_system.verify() > 0); + success *= (!missing_efd_system.verify().passed()); Log::setVerbosity(previous_verbosity); return success.report(__func__); diff --git a/tests/UnitTests/PhasorDynamics/GenClassicalTests.hpp b/tests/UnitTests/PhasorDynamics/GenClassicalTests.hpp index e0d2ed5488..a089cf1ccb 100644 --- a/tests/UnitTests/PhasorDynamics/GenClassicalTests.hpp +++ b/tests/UnitTests/PhasorDynamics/GenClassicalTests.hpp @@ -328,7 +328,7 @@ namespace GridKit gen.setSystemBase(60.0, 100.0e6); gen.allocate(); - success *= gen.verify() == 0; + success *= gen.verify().passed(); success *= speed.linked(); gen.initialize(); diff --git a/tests/UnitTests/PhasorDynamics/GovernorGastPtiTests.hpp b/tests/UnitTests/PhasorDynamics/GovernorGastPtiTests.hpp index 3231b05611..f1443c9f2f 100644 --- a/tests/UnitTests/PhasorDynamics/GovernorGastPtiTests.hpp +++ b/tests/UnitTests/PhasorDynamics/GovernorGastPtiTests.hpp @@ -56,12 +56,12 @@ namespace GridKit PhasorDynamics::Governor::GastPti empty; success *= (empty.size() == static_cast(Utilities::enum_size())); success *= (empty.getMonitor() == nullptr); - success *= (empty.verify() > 0); // required pmech assignment is absent + success *= (!empty.verify().passed()); // required pmech assignment is absent PhasorDynamics::Governor::GastPti configured(makeData()); success *= (configured.size() == static_cast(Utilities::enum_size())); success *= (configured.getMonitor() != nullptr); - success *= (configured.verify() > 0); // required pmech assignment is absent + success *= (!configured.verify().passed()); // required pmech assignment is absent // Framework binding precedes model allocation; verification must not // inspect index maps until allocate() has sized them. @@ -78,16 +78,16 @@ namespace GridKit GastPtiT bound(makeData()); bound.getPorts().out.template port().connect(&bound_pmech); success *= (bound.bind(bound_y, bound_yp, bound_f, bound_abs_tol, 0) == 0); - success *= (bound.verify() == 0); + success *= (bound.verify().passed()); PhasorDynamics::Governor::GastPti minimal(makeMinimalData()); - success *= (minimal.verify() > 0); // required pmech assignment is absent - success *= (verifyData(makeData()) == 0); + success *= (!minimal.verify().passed()); // required pmech assignment is absent + success *= (verifyData(makeData()).passed()); success *= defaultsMatchDocumentedValues(); auto missing_trate = makeMinimalData(); missing_trate.parameters.erase(Params::Trate); - success *= (verifyData(missing_trate) > 0); + success *= (!verifyData(missing_trate).passed()); success *= invalidParameterCase(Params::R, 0.0); success *= invalidParameterCase(Params::R, -0.1); @@ -130,17 +130,17 @@ namespace GridKit auto equal = makeData(); equal.parameters[Params::Vmin] = 0.5; equal.parameters[Params::Vmax] = 0.5; - success *= (verifyData(equal) == 0); + success *= (verifyData(equal).passed()); auto reversed = makeData(); reversed.parameters[Params::Vmin] = 0.6; reversed.parameters[Params::Vmax] = 0.5; - success *= (verifyData(reversed) > 0); + success *= (!verifyData(reversed).passed()); // Narrow configured limits remain valid. auto narrow = makeData(); narrow.parameters[Params::Vmax] = 0.01; - success *= (verifyData(narrow) == 0); + success *= (verifyData(narrow).passed()); // Integer JSON values are accepted for real parameters; booleans are // not numeric. @@ -165,7 +165,7 @@ namespace GridKit { Fixture invalid_base(makeData(), system_base); success *= (invalid_base.gastpti.allocate() == 0); - success *= (invalid_base.gastpti.verify() > 0); + success *= (!invalid_base.gastpti.verify().passed()); } success *= unlinkedSignalRejected(); @@ -1014,7 +1014,7 @@ namespace GridKit /// verification, and a machine-seeded mechanical-power node. bool prepare(RealT pmech) { - const bool success = (gastpti.allocate() == 0) && (gastpti.verify() == 0); + const bool success = (gastpti.allocate() == 0) && (gastpti.verify().passed()); if (!success) { std::cout << "GASTPTI fixture preparation failed\n"; @@ -1231,10 +1231,10 @@ namespace GridKit { auto data = makeData(); data.parameters[parameter] = value; - return verifyData(data) > 0; + return !verifyData(data).passed(); } - int verifyData(const Data& data) const + Model::ConfigurationChecks verifyData(const Data& data) const { PhasorDynamics::SignalNode pmech; PhasorDynamics::Governor::GastPti model(data); @@ -1250,7 +1250,7 @@ namespace GridKit PhasorDynamics::Governor::GastPti model(makeData()); model.getPorts().out.template port().connect(&pmech_node); model.getPorts().in.template port().connect(&unlinked_node); - return model.verify() > 0; + return !model.verify().passed(); } bool aliasedSignalsRejected() const @@ -1268,7 +1268,7 @@ namespace GridKit { success = false; } - if (!(pref_alias.verify() > 0)) + if (pref_alias.verify().passed()) { success = false; } @@ -1281,7 +1281,7 @@ namespace GridKit { success = false; } - if (!(speed_alias.verify() > 0)) + if (speed_alias.verify().passed()) { success = false; } @@ -1300,7 +1300,7 @@ namespace GridKit { success = false; } - if (!(input_alias.verify() > 0)) + if (input_alias.verify().passed()) { success = false; } diff --git a/tests/UnitTests/PhasorDynamics/GovernorHygovTests.hpp b/tests/UnitTests/PhasorDynamics/GovernorHygovTests.hpp index f72a8ba1a8..221fbfb0c9 100644 --- a/tests/UnitTests/PhasorDynamics/GovernorHygovTests.hpp +++ b/tests/UnitTests/PhasorDynamics/GovernorHygovTests.hpp @@ -57,7 +57,7 @@ namespace GridKit Fixture configured(makeData()); success *= (configured.hygov.size() == static_cast(Utilities::enum_size())); success *= (configured.hygov.getMonitor() != nullptr); - success *= (configured.hygov.verify() == 0); + success *= (configured.hygov.verify().passed()); const auto previous_verbosity = Log::verbosity(); // Suppress expected errors and warnings from the invalid cases below. @@ -65,15 +65,15 @@ namespace GridKit Log::setVerbosity(Log::Verbosity::NONE); Fixture minimal(makeMinimalData()); - success *= (minimal.hygov.verify() == 0); + success *= (minimal.hygov.verify().passed()); success *= defaultsMatchDocumentedValues(); auto missing_trate_data = makeMinimalData(); missing_trate_data.parameters.erase(Params::Trate); Fixture missing_trate(missing_trate_data); - success *= (missing_trate.hygov.verify() > 0); + success *= (!missing_trate.hygov.verify().passed()); - success *= (empty.verify() > 0); + success *= (!empty.verify().passed()); const RealT nan = std::numeric_limits::quiet_NaN(); const RealT infinity = std::numeric_limits::infinity(); @@ -117,14 +117,14 @@ namespace GridKit for (const RealT value : nonfinite_values) { Fixture invalid_fixture(makeData(), {{parameter, value}}); - success *= (invalid_fixture.hygov.verify() > 0); + success *= (!invalid_fixture.hygov.verify().passed()); } } // The pmech output is required, so a model without an assigned node // is rejected even when every parameter is valid. PhasorDynamics::Governor::Hygov unassigned(makeData()); - success *= (unassigned.verify() > 0); + success *= (!unassigned.verify().passed()); const std::array, 19> invalid_parameter_values{{ {Params::Trate, 0.0}, @@ -151,7 +151,7 @@ namespace GridKit for (const auto& [parameter, value] : invalid_parameter_values) { Fixture invalid_fixture(makeData(), {{parameter, value}}); - success *= (invalid_fixture.hygov.verify() > 0); + success *= (!invalid_fixture.hygov.verify().passed()); } // A curve with no rise cannot yield a unique gate. @@ -161,7 +161,7 @@ namespace GridKit {Params::Pgv3, 0.0}, {Params::Pgv4, 0.0}, {Params::Pgv5, 0.0}}); - success *= (flat_curve.hygov.verify() > 0); + success *= (!flat_curve.hygov.verify().passed()); // A curve that rises only outside the configured response limits is // valid because initialization may expand those limits. @@ -175,33 +175,33 @@ namespace GridKit {Params::Pgv3, 0.5}, {Params::Pgv4, 0.5}, {Params::Pgv5, 1.0}}); - success *= (flat_configured_range.hygov.verify() == 0); + success *= (flat_configured_range.hygov.verify().passed()); // A requested backlash is accepted, warns, and remains inactive. Fixture backlash(makeData(), {{Params::db2, 0.5}}); - success *= (backlash.hygov.verify() == 0); + success *= (backlash.hygov.verify().passed()); // Integer JSON values are accepted for real parameters; booleans are // not numeric. auto integer_real = makeData(); integer_real.parameters[Params::Tw] = static_cast(2); Fixture integer_model(integer_real); - success *= (integer_model.hygov.verify() == 0); + success *= (integer_model.hygov.verify().passed()); auto bad_numeric_type = makeData(); bad_numeric_type.parameters[Params::Trate] = true; Fixture bad_numeric_model(bad_numeric_type); - success *= (bad_numeric_model.hygov.verify() > 0); + success *= (!bad_numeric_model.hygov.verify().passed()); Fixture overflowing_component_base( makeData(), {{Params::Trate, std::numeric_limits::max()}}); - success *= (overflowing_component_base.hygov.verify() > 0); + success *= (!overflowing_component_base.hygov.verify().passed()); Fixture overflowing_base_ratio( makeData(), {{Params::Trate, std::numeric_limits::min()}}); - success *= (overflowing_base_ratio.hygov.verify() > 0); + success *= (!overflowing_base_ratio.hygov.verify().passed()); const std::array invalid_system_bases{{ 0.0, @@ -215,7 +215,7 @@ namespace GridKit for (const RealT system_base : invalid_system_bases) { Fixture invalid_base(makeData(), {}, system_base); - success *= (invalid_base.hygov.verify() > 0); + success *= (!invalid_base.hygov.verify().passed()); } success *= unlinkedSignalRejected(); @@ -1008,7 +1008,7 @@ namespace GridKit /// verification, and a machine-provided mechanical-power value. bool prepare(RealT pmech) { - const bool success = (hygov.allocate() == 0) && (hygov.verify() == 0); + const bool success = (hygov.allocate() == 0) && (hygov.verify().passed()); if (!success) { std::cout << "HYGOV fixture preparation failed\n"; @@ -1275,7 +1275,7 @@ namespace GridKit PhasorDynamics::SignalNode unlinked_node; Fixture fixture(makeData()); fixture.hygov.getPorts().in.template port().connect(&unlinked_node); - return fixture.hygov.verify() > 0; + return !fixture.hygov.verify().passed(); } template diff --git a/tests/UnitTests/PhasorDynamics/ModelConfigurationTests.hpp b/tests/UnitTests/PhasorDynamics/ModelConfigurationTests.hpp new file mode 100644 index 0000000000..91e0f4c698 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ModelConfigurationTests.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + /// One parameter per ParameterReader case, named for its value. + enum class ReaderTestParameters : std::size_t + { + omitted, + integer, + real, + zero, + negative, + infinite, + not_a_number, + integer_flag, + false_flag, + true_flag, + text, + }; + + template + class ModelConfigurationTests + { + private: + using Params = ReaderTestParameters; + using DataT = PhasorDynamics::ComponentData; + + public: + /// Omitted parameters return no value, integers widen to reals, and + /// invalid values are rejected naming the model and parameter. + TestOutcome parameterReader() + { + TestStatus success = true; + + DataT data; + data.parameters = {{Params::integer, static_cast(2)}, + {Params::real, static_cast(0.5)}, + {Params::zero, ZERO}, + {Params::negative, -ONE}, + {Params::infinite, std::numeric_limits::infinity()}, + {Params::not_a_number, std::numeric_limits::quiet_NaN()}, + {Params::integer_flag, static_cast(1)}, + {Params::false_flag, false}, + {Params::true_flag, true}, + {Params::text, std::string("2")}}; + + const Model::ParameterReader reader(data, "Test"); + + success *= !reader.readReal(Params::omitted).has_value(); + success *= !reader.readNonNegative(Params::omitted).has_value(); + success *= !reader.readBool(Params::omitted).has_value(); + success *= !reader.readInteger(Params::omitted).has_value(); + success *= (reader.readReal(Params::omitted).value_or(ONE) == ONE); + success *= reader.readBool(Params::omitted).value_or(true); + + success *= (reader.readReal(Params::integer) == TWO); + success *= (reader.readReal(Params::real) == static_cast(0.5)); + success *= (reader.readReal(Params::negative) == -ONE); + success *= (reader.readNonNegative(Params::zero) == ZERO); + success *= (reader.readNonNegative(Params::integer) == TWO); + success *= (reader.readInteger(Params::integer) == static_cast(2)); + success *= (reader.readBool(Params::false_flag) == false); + success *= (reader.readBool(Params::true_flag) == true); + success *= !reader.readBool(Params::false_flag).value_or(true); + + success *= throws([&]() + { static_cast(reader.readReal(Params::infinite)); }); + success *= throws([&]() + { static_cast(reader.readReal(Params::not_a_number)); }); + success *= throws([&]() + { static_cast(reader.readReal(Params::text)); }); + success *= throws([&]() + { static_cast(reader.readReal(Params::true_flag)); }); + success *= throws([&]() + { static_cast(reader.readInteger(Params::real)); }); + success *= throws([&]() + { static_cast(reader.readInteger(Params::zero)); }); + success *= throws([&]() + { static_cast(reader.readInteger(Params::true_flag)); }); + success *= throws([&]() + { static_cast(reader.readNonNegative(Params::negative)); }); + + try + { + static_cast(reader.readBool(Params::integer_flag)); + success = false; + } + catch (const std::invalid_argument& error) + { + success *= (std::string_view(error.what()) == "Test: parameter 'integer_flag' must be boolean"); + } + + return success.report(__func__); + } + + /// Checks record only failed conditions, prefixed by the model name. + TestOutcome configurationChecks() + { + TestStatus success = true; + + Model::ConfigurationChecks checks("Test"); + checks.check(true, "never recorded"); + success *= checks.passed(); + + checks.check(false, "x must be positive"); + success *= !checks.passed(); + success *= (checks.errors() == std::vector{"Test: x must be positive"}); + + return success.report(__func__); + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp index 155a88a164..2b6fb56bed 100644 --- a/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp +++ b/tests/UnitTests/PhasorDynamics/StabilizerIeeestTests.hpp @@ -1,14 +1,21 @@ #pragma once -#include -#include +#include +#include #include +#include +#include +#include +#include +#include #include #include #include +#include #include -#include +#include +#include #include #include #include @@ -18,369 +25,628 @@ namespace GridKit { namespace Testing { - using Log = ::GridKit::Utilities::Logger; - - template + template class StabilizerIeeestTests { public: - using RealT = typename PhasorDynamics::Component::RealT; + using ScalarT = scalar_type; + using IdxT = index_type; + using RealT = typename PhasorDynamics::Component::RealT; - StabilizerIeeestTests() = default; - ~StabilizerIeeestTests() = default; + // Exactly representable parameters keep every check within one epsilon. + static constexpr RealT kTol = std::numeric_limits::epsilon(); - TestOutcome constructor() + /// Construction and every verify() rule, including parameter types, + /// output limits, signal linkage, and the notch order of the data. + TestOutcome validation() { TestStatus success = true; - auto data = makeTestData(); - auto* stab = new PhasorDynamics::Stabilizer::Ieeest(data); - - success *= (stab != nullptr); - success *= (stab->getMonitor() != nullptr); + const auto previous_verbosity = Log::verbosity(); + // Suppress expected errors and warnings from the invalid cases below. + // Use EVERYTHING to inspect those diagnostics. + Log::setVerbosity(Log::Verbosity::NONE); + + IeeestT empty; + success *= (empty.size() == kSize); + success *= (empty.getMonitor() == nullptr); + success *= (!empty.verify().passed()); + + Fixture configured(makeData()); + success *= (configured.ieeest.size() == kSize); + success *= (configured.ieeest.getMonitor() != nullptr); + success *= configured.initialize(0.0); + + IeeestT unattached(makeData()); + success *= (!unattached.verify().passed()); + + PhasorDynamics::SignalNode unlinked_node; + IeeestT unlinked(makeData()); + unlinked.getPorts().in.template port().connect(&unlinked_node); + success *= (!unlinked.verify().passed()); + + success *= invalidParameterCase(Params::Lsmin, 1.0); + success *= invalidParameterCase(Params::T2, -0.1); + success *= invalidParameterCase(Params::T4, -0.1); + success *= invalidParameterCase(Params::T6, -0.1); + success *= invalidParameterCase(Params::Ks, true); + success *= invalidParameterCase(Params::A1, std::numeric_limits::quiet_NaN()); + success *= invalidParameterCase(Params::T1, std::numeric_limits::infinity()); + + // Data of another notch order does not fit this specialization. + RealT other_order_A4 = 0.125; + if constexpr (order == 4) + { + other_order_A4 = 0.0; + } + success *= invalidParameterCase(Params::A4, other_order_A4); - delete stab; + // The numerator degree may not exceed the notch order. + if constexpr (order == 0) + { + success *= invalidParameterCase(Params::A5, 0.5); + } + if constexpr (order == 1) + { + success *= invalidParameterCase(Params::A6, 0.125); + } + Log::setVerbosity(previous_verbosity); return success.report(__func__); } - /** - * @brief All states initialize to zero (stabilizer at rest). - * With u = 0, all residuals should be zero. - */ - TestOutcome zeroInitialResidual() + /// The factory builds this specialization from data of this notch + /// order, whichever denominator factor comes first, and connects its + /// signals. + TestOutcome factory() { TestStatus success = true; - // Create signal nodes for input (u) and output (Vss) - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.0}; - IdxT u_index = 12; // beyond internal variables - ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + auto data = makeData(); + data.signal_inputs[Inputs::input] = 1; + data.signal_outputs[Outputs::output] = 2; - // Link signal nodes to backing storage - u_node.link(&u_value, &u_index); - vss_node.link(&vss_value, &vss_index); + typename Factory::SignalNodeSetT nodes; + nodes.add({"input", 1}); + nodes.add({"output", 2}); - using namespace GridKit::PhasorDynamics::Stabilizer; + std::unique_ptr> built(Factory::create(data, nodes)); - auto data = makeTestData(); - auto stab = Ieeest(data); + auto* ieeest = dynamic_cast(built.get()); + success *= (ieeest != nullptr); + if (ieeest != nullptr) + { + success *= ieeest->getPorts().in.template port().connected(); + success *= ieeest->getPorts().out.template port().connected(); + } + + auto swapped = makeData(); + swapFactors(swapped); + std::unique_ptr> swapped_built(Factory::create(swapped, nodes)); + success *= (dynamic_cast(swapped_built.get()) != nullptr); - // Wire: stabilizer reads u_node as input, writes vss_node as output - stab.getPorts().in.template port().connect(&u_node); - stab.getPorts().out.template port().connect(&vss_node); + return success.report(__func__); + } - stab.allocate(); - success *= (stab.verify() == 0); - stab.initialize(); - stab.evaluateResidual(); + /// Any constant input is an equilibrium with zero output, every + /// variable but the limited output is differential, and the output node + /// and the monitor both publish the limited output. + TestOutcome steadyState() + { + TestStatus success = true; - auto tol = 10 * std::numeric_limits::epsilon(); - const auto& f = stab.getResidual(); - const auto* f_data = f.getData(); - for (size_t i = 0; i < f.getSize(); ++i) + for (const RealT u : {0.0, 0.5, -2.0}) { - if (!isEqual(f_data[i], 0.0, tol)) + Fixture fixture(makeData()); + success *= fixture.initialize(u); + success *= (fixture.ieeest.evaluateResidual() == 0); + success *= allZero(fixture.ieeest.getResidual(), "residual", "at rest"); + success *= allZero(fixture.ieeest.yp(), "derivative", "at rest"); + success *= scalarMatches(fixture.output(), 0.0, "output at rest"); + } + + Fixture fixture(makeData()); + success *= fixture.initialize(0.5); + success *= (fixture.ieeest.tagDifferentiable() == 0); + for (size_t i = 0; i < kSize; ++i) + { + const bool expected = (i != static_cast(Internal::VSS)); + if (fixture.ieeest.tag()[i] != expected) { - std::cout << "Non-zero residual at index " << i << ": " << f_data[i] << "\n"; + Log::error() << std::format("IEEEST order {} differentiability tag {} mismatch\n", order, i); success = false; } } - // Verify output signal is linked and reads the correct value - success *= vss_node.linked(); - success *= (vss_node.getVariableIndex() == 11); - success *= isEqual(vss_node.read(), static_cast(0.0), tol); + setState(fixture.ieeest, {{Internal::VSS, 0.25}}); + success *= scalarMatches(fixture.output(), 0.25, "published output"); + + RealT time = 0.0; + Model::VariableMonitorController monitor(time); + monitor.addMonitor(fixture.ieeest.getMonitor()); + std::stringstream monitor_output; + monitor.addSink({Model::VariableMonitorFormat::CSV}, monitor_output); + monitor.start(); + monitor.print(); + monitor.stop(); + + std::string monitor_header; + std::string monitor_values; + std::getline(monitor_output, monitor_header); + std::getline(monitor_output, monitor_values); + success *= (monitor_header == "t,Ieeest_ieeest_test_vss"); + const auto monitored = Tokenizer(monitor_values, ',')(); + success *= (monitored.size() == 2); + if (monitored.size() == 2) + { + success *= scalarMatches(monitored[1], 0.25, "monitored vss"); + } return success.report(__func__); } - /** - * @brief Residual evaluation against hand-computed answer key. - * - * Sets specific y/yp values and verifies residuals match - * pre-computed expected values. See plan for derivation. - */ - TestOutcome residual() + /// An exponential input \f$u e^{st}\f$ drives every signal as its + /// transfer function times the input, so the residual vanishes at + /// every rate that is not a pole. + TestOutcome transferFunction() { TestStatus success = true; - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.5}; - IdxT u_index = 12; - ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; - - u_node.link(&u_value, &u_index); - vss_node.link(&vss_value, &vss_index); - - using namespace GridKit::PhasorDynamics::Stabilizer; - - auto data = makeTestData(); - auto stab = Ieeest(data); - - stab.getPorts().in.template port().connect(&u_node); - stab.getPorts().out.template port().connect(&vss_node); - - stab.allocate(); - stab.initialize(); - setStatePoint(stab); - stab.evaluateResidual(); - - // Hand-computed answer key (see plan for full derivation) - const std::vector res_answer = { - 0.19, // f[0]: -x1_dot + x2 - 0.28, // f[1]: -x2_dot + x3 - 0.37, // f[2]: -x3_dot + x4 - 1.0975, // f[3]: -x4_dot + (-a0*x1 - a1*x2 - a2*x3 - a3*x4 + u) / a4 - 0.25, // f[4]: -T2*x5_dot - x5 + v4 - 0.24, // f[5]: -T4*x6_dot - x6 + v5 - -0.05, // f[6]: -T6*x7_dot - x7 + v6 - -0.42, // f[7]: -v4 + x1 + A5*x2 + A6*x3 - -0.25, // f[8]: -T2*(v5 - x5) + T1*(v4 - x5) - -0.31, // f[9]: -T4*(v6 - x6) + T3*(v5 - x6) - 5.75, // f[10]: -T6*v7 + Ks*T5*(v6 - x7) - 0.0, // f[11]: limiter (v7=0.05 within [-0.1, 0.1]) + const auto data = makeData(); + const auto p = [&data](Params key) + { + return std::get(data.parameters.at(key)); }; - // Looser tolerance for f[11] — Math::clamp is a smooth ramp approximation. - const auto loose_tol = static_cast(1.0e-4); - auto& residual = stab.getResidual(); - const auto* residual_data = residual.getData(); + for (const RealT s : {-0.5, 0.25, 2.0}) + { + const RealT numerator = 1.0 + p(Params::A5) * s + p(Params::A6) * s * s; + const RealT first = 1.0 + p(Params::A1) * s + p(Params::A2) * s * s; + const RealT second = 1.0 + p(Params::A3) * s + p(Params::A4) * s * s; + + // Small enough that the limiter is transparent. + const RealT u = 0.01; + const RealT x1 = u / first; + const RealT v4 = numerator * x1 / second; + const RealT x5 = v4 / (1.0 + p(Params::T2) * s); + const RealT v5 = (1.0 + p(Params::T1) * s) * x5; + const RealT x6 = v5 / (1.0 + p(Params::T4) * s); + const RealT v6 = (1.0 + p(Params::T3) * s) * x6; + const RealT x7 = v6 / (1.0 + p(Params::T6) * s); + const RealT v7 = p(Params::Ks) * p(Params::T5) * s * x7; + + Fixture fixture(data); + success *= fixture.initialize(0.0); + fixture.input() = u; + setMode(fixture.ieeest, + s, + {{Internal::X1, x1}, + {Internal::X2, s * x1}, + {Internal::X3, v4}, + {Internal::X4, s * v4}, + {Internal::X5, x5}, + {Internal::X6, x6}, + {Internal::X7, x7}}); + setState(fixture.ieeest, {{Internal::VSS, v7}}); + success *= (fixture.ieeest.evaluateResidual() == 0); + success *= allZero(fixture.ieeest.getResidual(), "residual", "under exponential input"); + } + + return success.report(__func__); + } + + /// For notch orders one to three, equivalent factorizations of the + /// notch denominator give the same residual: the factors commute, and + /// two first-order factors combine into one second-order factor. + TestOutcome factorOrdering() + { + TestStatus success = true; + + auto swapped = makeData(); + swapFactors(swapped); + success *= sameResidual(makeData(), swapped, "with swapped factors"); - for (size_t i = 0; i < res_answer.size(); ++i) + if constexpr (order == 2) { - auto test_tol = (i == 11) ? loose_tol : static_cast(10 * std::numeric_limits::epsilon()); - if (!isEqual(residual_data[i], res_answer[i], test_tol)) - { - std::cout << "Incorrect result for residual " << i << ": " - << std::setprecision(15) << residual_data[i] - << " != " << res_answer[i] << "\n"; - success = false; - } + auto first_order_factors = makeData(); + first_order_factors.parameters[Params::A1] = 1.0; + first_order_factors.parameters[Params::A2] = 0.0; + first_order_factors.parameters[Params::A3] = 0.5; + + auto second_order_factor = makeData(); + second_order_factor.parameters[Params::A1] = 1.5; + second_order_factor.parameters[Params::A2] = 0.5; + + success *= sameResidual(first_order_factors, + second_order_factor, + "with combined first-order factors"); } - // Verify output signal reads the stabilizer output - success *= isEqual(vss_node.read(), static_cast(0.05), loose_tol); + return success.report(__func__); + } + + /// The limiter passes the washout output inside its band and holds the + /// nearest limit outside it. + TestOutcome limiter() + { + TestStatus success = true; + + // With zero input and lead-lag states and a unit washout gain, the + // unlimited signal is -x7. + success *= limitedOutput(-0.75, 0.75, "inside the band"); + success *= limitedOutput(0.75, -0.75, "inside the band"); + success *= limitedOutput(-2.0, 1.0, "above Lsmax"); + success *= limitedOutput(2.0, -1.0, "below Lsmin"); return success.report(__func__); } #ifdef GRIDKIT_ENABLE_ENZYME - /** - * @brief Compare DependencyTracking Jacobian against Enzyme Jacobian. - */ + /// Away from equilibrium, every Enzyme Jacobian row matches dependency + /// tracking. TestOutcome jacobian() { TestStatus success = true; - auto data = makeTestData(); - - std::vector - dependency_tracking_jacobian = DependencyTrackingJacobian(data); + const auto dependency_jacobian = dependencyTrackingJacobian(success); + const auto enzyme_jacobian = enzymeJacobian(success); - std::vector - enzyme_jacobian = EnzymeJacobian(data); - - // Compare DependencyTracking dependencies to Enzyme's - auto tol = 10 * std::numeric_limits::epsilon(); - for (size_t i = 0; i < dependency_tracking_jacobian.size(); ++i) + success *= (dependency_jacobian.size() == enzyme_jacobian.size()); + const auto rows = std::min(dependency_jacobian.size(), enzyme_jacobian.size()); + for (size_t row = 0; row < rows; ++row) { - success *= (GridKit::Testing::isEqual(dependency_tracking_jacobian[i], enzyme_jacobian[i], tol)); + if (!isEqual(dependency_jacobian[row], enzyme_jacobian[row], kTol)) + { + Log::error() << std::format("IEEEST order {} Jacobian row {} mismatch between dependency tracking and Enzyme\n", + order, + row); + success = false; + } } return success.report(__func__); } +#endif private: - std::vector DependencyTrackingJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) + using Log = ::GridKit::Utilities::Logger; + using IeeestT = PhasorDynamics::Stabilizer::Ieeest; + using Factory = PhasorDynamics::Stabilizer::IeeestFactory; + using Data = typename IeeestT::ModelDataT; + using Params = typename Data::Parameters; + using Inputs = typename Data::SignalInputs; + using Outputs = typename Data::SignalOutputs; + using Internal = typename IeeestT::InternalVariablesT; + using Rows = std::vector>; + + static constexpr size_t kSize = static_cast(Internal::X1) + order; + static constexpr IdxT kInputIndex = static_cast(kSize); + + /// Owns IEEEST and its input and output nodes. Signal storage is + /// declared before the model so every referenced node outlives it. + /// Copying would invalidate the model and signal-node pointers. + template + class Fixture { - using DepVar = DependencyTracking::Variable; + private: + T input_value_{}; + IdxT input_index_{kInputIndex}; + PhasorDynamics::SignalNode input_node_; + PhasorDynamics::SignalNode output_node_; + + public: + explicit Fixture(const Data& data) + : ieeest(data) + { + input_node_.link(&input_value_, &input_index_); + ieeest.getPorts().in.template port().connect(&input_node_); + ieeest.getPorts().out.template port().connect(&output_node_); + } - // Set up signal nodes with DependencyTracking scalar type - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - DepVar u_value{0.5}; - IdxT u_index = 12; - DepVar vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + Fixture(const Fixture&) = delete; + Fixture& operator=(const Fixture&) = delete; - u_node.link(&u_value, &u_index); - vss_node.link(&vss_value, &vss_index); + /// Allocation, verification, and initialization at a constant input. + bool initialize(RealT u) + { + input_value_ = static_cast(u); + const bool success = (ieeest.allocate() == 0) && ieeest.verify().passed() + && (ieeest.initialize() == 0); + if (!success) + { + Log::error() << std::format("IEEEST order {} fixture initialization failed\n", order); + } + return success; + } - using namespace GridKit::PhasorDynamics::Stabilizer; + T& input() + { + return input_value_; + } - auto stab = Ieeest(ieeestdata); - stab.getPorts().in.template port().connect(&u_node); - stab.getPorts().out.template port().connect(&vss_node); + IdxT inputIndex() const + { + return input_index_; + } - stab.allocate(); - stab.initialize(); + T output() + { + return output_node_.read(); + } - // Tag external signal u as an additional independent variable - u_value.setVariableNumber(2 * stab.size()); // @todo avoid requiring knowledge of the numbering - setStatePointDep(stab); + PhasorDynamics::Stabilizer::Ieeest ieeest; + }; - stab.updateTime(0.0, 1.0); // alpha = 1.0 to verify d/dy' term + /// Distinct, exactly representable parameters. Notch terms above this + /// order are zero, the lead-lag ratios differ from one so no Jacobian + /// entry cancels, and the washout gain Ks T5 / T6 is one. + static Data makeData() + { + Data data; + data.device_class = "Ieeest"; + data.disambiguation_string = "ieeest_test"; + data.monitored_variables.insert(Data::MonitorableVariables::vss); + data.parameters = {{Params::A1, 0.0}, + {Params::A2, 0.0}, + {Params::A3, 0.0}, + {Params::A4, 0.0}, + {Params::A5, 0.0}, + {Params::A6, 0.0}, + {Params::T1, 1.5}, + {Params::T2, 0.75}, + {Params::T3, 2.5}, + {Params::T4, 1.25}, + {Params::T5, 2.0}, + {Params::T6, 1.0}, + {Params::Ks, 0.5}, + {Params::Lsmin, -1.0}, + {Params::Lsmax, 1.0}}; + if constexpr (order >= 1) + { + data.parameters[Params::A1] = 1.0; + data.parameters[Params::A5] = 0.375; + } + if constexpr (order >= 2) + { + data.parameters[Params::A2] = 0.5; + data.parameters[Params::A6] = 0.125; + } + if constexpr (order >= 3) + { + data.parameters[Params::A3] = 0.75; + } + if constexpr (order == 4) + { + data.parameters[Params::A4] = 0.25; + } + return data; + } - stab.evaluateResidual(); - stab.evaluateJacobian(); - auto model_jacobian = stab.getCsrJacobian(); - auto& output_stream = Log::misc(); - output_stream << "Sparse Csr Matrix: Ieeest DependencyTracking Jacobian\n"; - model_jacobian->print(output_stream); + /// Exchange the two notch denominator factors. + static void swapFactors(Data& data) + { + std::swap(data.parameters[Params::A1], data.parameters[Params::A3]); + std::swap(data.parameters[Params::A2], data.parameters[Params::A4]); + } - return GridKit::Testing::MapFromCsr(model_jacobian); + template + bool invalidParameterCase(Params parameter, ValueT value) const + { + auto data = makeData(); + data.parameters[parameter] = value; + return configurationRejected( + [&]() + { + Fixture fixture(data); + return fixture.ieeest.verify(); + }); } - std::vector EnzymeJacobian( - PhasorDynamics::Stabilizer::IeeestData ieeestdata) + /// Drive the unlimited signal through the washout state and check the + /// limited-output row. + bool limitedOutput(RealT x7, RealT expected, const char* context) const { - PhasorDynamics::SignalNode u_node; - PhasorDynamics::SignalNode vss_node; - ScalarT u_value{0.5}; - IdxT u_index = 12; - ScalarT vss_value{0.0}; - IdxT vss_index = INVALID_INDEX; + Fixture fixture(makeData()); + if (!fixture.initialize(0.0)) + { + return false; + } + setState(fixture.ieeest, {{Internal::X7, x7}}); + if (fixture.ieeest.evaluateResidual() != 0) + { + return false; + } + return residualsMatch(fixture.ieeest, {{Internal::VSS, expected}}, context); + } - u_node.link(&u_value, &u_index); - vss_node.link(&vss_value, &vss_index); + /// Evaluate two models at the same state and compare their residuals. + bool sameResidual(const Data& first, const Data& second, const char* context) const + { + Fixture expected(first); + Fixture actual(second); + if (!expected.initialize(0.0) || !actual.initialize(0.0)) + { + return false; + } + setOffEquilibriumState(expected); + setOffEquilibriumState(actual); + if (expected.ieeest.evaluateResidual() != 0 || actual.ieeest.evaluateResidual() != 0) + { + return false; + } - using namespace GridKit::PhasorDynamics::Stabilizer; + bool success = true; + const auto* expected_values = expected.ieeest.getResidual().getData(); + const auto* actual_values = actual.ieeest.getResidual().getData(); + for (size_t row = 0; row < kSize; ++row) + { + if (!rowMatches(actual_values[row], expected_values[row], "residual", row, context)) + { + success = false; + } + } + return success; + } - auto stab = Ieeest(ieeestdata); - stab.getPorts().in.template port().connect(&u_node); - stab.getPorts().out.template port().connect(&vss_node); + /// A state away from equilibrium that keeps the unlimited signal at + /// least 0.5 inside the output limits for every notch order. + template + void setOffEquilibriumState(Fixture& fixture) const + { + fixture.input() = static_cast(0.5); + setState(fixture.ieeest, + {{Internal::X1, 0.25}, + {Internal::X2, 0.125}, + {Internal::X3, 0.5}, + {Internal::X4, -0.25}, + {Internal::X5, 0.375}, + {Internal::X6, 0.25}, + {Internal::X7, 0.625}}); + } - stab.allocate(); - stab.initialize(); - setStatePoint(stab); + /// Write state rows and publish the update. Rows of notch states this + /// order does not have are skipped. + template + static void setState(PhasorDynamics::Stabilizer::Ieeest& ieeest, const Rows& rows) + { + auto* y = ieeest.y().getData(); + for (const auto& [variable, value] : rows) + { + const auto row = static_cast(variable); + if (row < kSize) + { + y[row] = static_cast(value); + } + } + ieeest.y().setDataUpdated(); + } - stab.updateTime(0.0, 1.0); // alpha = 1.0 to verify d/dy' term + /// setState() with each derivative set to the rate times its state, + /// as an exponential trajectory requires. + template + static void setMode(PhasorDynamics::Stabilizer::Ieeest& ieeest, RealT rate, const Rows& rows) + { + setState(ieeest, rows); + auto* yp = ieeest.yp().getData(); + for (const auto& [variable, value] : rows) + { + const auto row = static_cast(variable); + if (row < kSize) + { + yp[row] = static_cast(rate * value); + } + } + ieeest.yp().setDataUpdated(); + } - stab.evaluateResidual(); - stab.evaluateJacobian(); - stab.constructCsr(); - auto model_jacobian = stab.getCsrJacobian(); - auto& output_stream = Log::misc(); - output_stream << "Sparse Csr Matrix: Ieeest Enzyme Jacobian\n"; - model_jacobian->print(output_stream); + /// Compare one vector row against its expected value. Every row check + /// in this suite reports through here, so failures share one format. + static bool rowMatches(RealT actual, + RealT expected, + const char* what, + size_t row, + const char* context) + { + if (isEqual(actual, expected, kTol)) + { + return true; + } + Log::error() << std::format("IEEEST order {} {} row {} {} mismatch: {} != {}\n", + order, + what, + row, + context, + actual, + expected); + return false; + } - return GridKit::Testing::MapFromCsr(model_jacobian); + bool residualsMatch(const IeeestT& ieeest, const Rows& rows, const char* context) const + { + bool success = true; + const auto* f = ieeest.getResidual().getData(); + for (const auto& [variable, expected] : rows) + { + const auto row = static_cast(variable); + if (!rowMatches(f[row], expected, "residual", row, context)) + { + success = false; + } + } + return success; } -#endif - private: - static constexpr ScalarT tol_ = 10 * std::numeric_limits::epsilon(); + template + static bool allZero(const VectorT& vector, const char* what, const char* context) + { + bool success = true; + const auto* values = vector.getData(); + for (size_t row = 0; row < kSize; ++row) + { + if (!rowMatches(values[row], 0.0, what, row, context)) + { + success = false; + } + } + return success; + } - /** - * @brief Standard IEEEST parameter set for all tests. - * Derived: a0=1, a1=0.4, a2=0.63, a3=0.1, a4=0.08 - */ - auto makeTestData() -> PhasorDynamics::Stabilizer::IeeestData + static bool scalarMatches(RealT actual, RealT expected, const char* label) { - using Params = PhasorDynamics::Stabilizer::IeeestParameters; + if (isEqual(actual, expected, kTol)) + { + return true; + } + Log::error() << std::format("IEEEST order {} {} mismatch: {} != {}\n", order, label, actual, expected); + return false; + } - PhasorDynamics::Stabilizer::IeeestData data; - data.device_class = "stabilizer"; - data.disambiguation_string = "ieeest_test"; - data.monitored_variables.insert(PhasorDynamics::Stabilizer::IeeestMonitorableVariables::vss); - - data.parameters[Params::A1] = 0.1; - data.parameters[Params::A2] = 0.2; - data.parameters[Params::A3] = 0.3; - data.parameters[Params::A4] = 0.4; - data.parameters[Params::A5] = 0.5; - data.parameters[Params::A6] = 0.6; - data.parameters[Params::T1] = 0.5; - data.parameters[Params::T2] = 1.0; - data.parameters[Params::T3] = 0.3; - data.parameters[Params::T4] = 1.0; - data.parameters[Params::T5] = 2.0; - data.parameters[Params::T6] = 5.0; - data.parameters[Params::Ks] = 10.0; - data.parameters[Params::Lsmin] = -0.1; - data.parameters[Params::Lsmax] = 0.1; - data.parameters[Params::Vcl] = 0.0; - data.parameters[Params::Vcu] = 0.0; - data.parameters[Params::Tdelay] = 0.0; +#ifdef GRIDKIT_ENABLE_ENZYME + /// Number y and yp as the even and odd columns that constructCsr() + /// merges, and the input as the column after the internal variables. + void numberVariables(Fixture& fixture) const + { + auto* y = fixture.ieeest.y().getData(); + auto* yp = fixture.ieeest.yp().getData(); + for (size_t i = 0; i < kSize; ++i) + { + y[i].setVariableNumber(2 * i); + yp[i].setVariableNumber(2 * i + 1); + } + fixture.input().setVariableNumber(2 * static_cast(fixture.inputIndex())); - return data; + fixture.ieeest.y().setDataUpdated(); + fixture.ieeest.yp().setDataUpdated(); } - /** - * @brief Set a non-trivial operating point for residual/Jacobian tests. - * Avoids zeros and ones to catch coefficient errors. - */ - void setStatePoint(PhasorDynamics::Stabilizer::Ieeest& stab) + std::vector dependencyTrackingJacobian( + TestStatus& success) const { - auto* y = stab.y().getData(); - auto* yp = stab.yp().getData(); - - y[0] = 0.1; // x1 - y[1] = 0.2; // x2 - y[2] = 0.3; // x3 - y[3] = 0.4; // x4 - y[4] = 0.5; // x5 - y[5] = 0.6; // x6 - y[6] = 0.7; // x7 - y[7] = 0.8; // v4 - y[8] = 0.9; // v5 - y[9] = 1.0; // v6 - y[10] = 0.05; // v7 (within limiter range) - y[11] = 0.05; // Vss (model output) - - yp[0] = 0.01; // x1_dot - yp[1] = 0.02; // x2_dot - yp[2] = 0.03; // x3_dot - yp[3] = 0.04; // x4_dot - yp[4] = 0.05; // x5_dot - yp[5] = 0.06; // x6_dot - yp[6] = 0.07; // x7_dot - - stab.y().setDataUpdated(); - stab.yp().setDataUpdated(); + Fixture fixture(makeData()); + success *= fixture.initialize(0.0); + setOffEquilibriumState(fixture); + numberVariables(fixture); + fixture.ieeest.updateTime(0.0, 1.0); + success *= (fixture.ieeest.evaluateResidual() == 0); + success *= (fixture.ieeest.evaluateJacobian() == 0); + + return MapFromCsr(fixture.ieeest.getCsrJacobian()); } - /** - * @brief Set the same operating point for DependencyTracking variables. - * Uses setValue() to set the numeric value while preserving dependency info. - */ - void setStatePointDep(PhasorDynamics::Stabilizer::Ieeest& stab) + std::vector enzymeJacobian( + TestStatus& success) const { - auto* y = stab.y().getData(); - auto* yp = stab.yp().getData(); - - y[0].setValue(0.1); - y[1].setValue(0.2); - y[2].setValue(0.3); - y[3].setValue(0.4); - y[4].setValue(0.5); - y[5].setValue(0.6); - y[6].setValue(0.7); - y[7].setValue(0.8); - y[8].setValue(0.9); - y[9].setValue(1.0); - y[10].setValue(0.05); - y[11].setValue(0.05); - - yp[0].setValue(0.01); - yp[1].setValue(0.02); - yp[2].setValue(0.03); - yp[3].setValue(0.04); - yp[4].setValue(0.05); - yp[5].setValue(0.06); - yp[6].setValue(0.07); - - stab.y().setDataUpdated(); - stab.yp().setDataUpdated(); + Fixture fixture(makeData()); + success *= fixture.initialize(0.0); + setOffEquilibriumState(fixture); + fixture.ieeest.updateTime(0.0, 1.0); + success *= (fixture.ieeest.evaluateResidual() == 0); + success *= (fixture.ieeest.evaluateJacobian() == 0); + success *= (fixture.ieeest.constructCsr() == 0); + + return MapFromCsr(fixture.ieeest.getCsrJacobian()); } - }; // class StabilizerIeeestTests - +#endif + }; } // namespace Testing } // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp index 1d78f3ce0e..aee34b8ffe 100644 --- a/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemSingleComponentTests.hpp @@ -160,7 +160,7 @@ namespace GridKit // Suppress the expected missing-bus configuration error below. // Use EVERYTHING to inspect the diagnostic. Log::setVerbosity(Log::Verbosity::NONE); - success *= missing_bus_system.verify() > 0; + success *= !missing_bus_system.verify().passed(); Log::setVerbosity(previous_verbosity); return success.report(__func__); @@ -478,7 +478,7 @@ namespace GridKit // Suppress the expected missing-output configuration error below. // Use EVERYTHING to inspect the diagnostic. Log::setVerbosity(Log::Verbosity::NONE); - success *= missing_output_system.verify() > 0; + success *= !missing_output_system.verify().passed(); Log::setVerbosity(previous_verbosity); return success.report(__func__); diff --git a/tests/UnitTests/PhasorDynamics/SystemTests.hpp b/tests/UnitTests/PhasorDynamics/SystemTests.hpp index 0d1e8bdcca..bc7effc43c 100644 --- a/tests/UnitTests/PhasorDynamics/SystemTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemTests.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -67,9 +68,9 @@ namespace GridKit return 0; } - int verify() const override final + Model::ConfigurationChecks verify() const override final { - return 0; + return {}; } int initialize() override final @@ -403,6 +404,41 @@ namespace GridKit return status.report(__func__); } + /// SystemModel selects the correct IEEEST specialization and connects its ports. + template + TestOutcome ieeestConstruction() + { + using IeeestT = PhasorDynamics::Stabilizer::Ieeest; + using Params = PhasorDynamics::Stabilizer::IeeestParameters; + using Inputs = PhasorDynamics::Stabilizer::IeeestSignalInputs; + using Outputs = PhasorDynamics::Stabilizer::IeeestSignalOutputs; + + TestStatus success = true; + + PhasorDynamics::SystemModelData data; + data.signal = {{"input", 1}, {"output", 2}}; + + auto& stabilizer = data.stabilizer.emplace_back(); + stabilizer.parameters[Params::A1] = static_cast(order >= 1 ? 1.0 : 0.0); + stabilizer.parameters[Params::A2] = static_cast(order >= 2 ? 0.5 : 0.0); + stabilizer.parameters[Params::A3] = static_cast(order >= 3 ? 0.75 : 0.0); + stabilizer.parameters[Params::A4] = static_cast(order == 4 ? 0.25 : 0.0); + stabilizer.signal_inputs[Inputs::input] = 1; + stabilizer.signal_outputs[Outputs::output] = 2; + + PhasorDynamics::SystemModel system(data); + + auto* ieeest = dynamic_cast(system.getComponent(0)); + success *= (ieeest != nullptr); + if (ieeest != nullptr) + { + success *= ieeest->getPorts().in.template port().connected(); + success *= ieeest->getPorts().out.template port().connected(); + } + + return success.report(__func__); + } + /// SystemModel propagates a statically valid component's initialization error. TestOutcome componentInitializationError() { @@ -412,7 +448,7 @@ namespace GridKit InitializationFailureComponent component; system.addComponent(&component); - success *= system.verify() == 0; + success *= system.verify().passed(); const auto previous_verbosity = Log::verbosity(); Log::setVerbosity(Log::Verbosity::NONE); diff --git a/tests/UnitTests/PhasorDynamics/runModelConfigurationTests.cpp b/tests/UnitTests/PhasorDynamics/runModelConfigurationTests.cpp new file mode 100644 index 0000000000..dce0b67b53 --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runModelConfigurationTests.cpp @@ -0,0 +1,12 @@ +#include "ModelConfigurationTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::Testing::ModelConfigurationTests test; + + result += test.parameterReader(); + result += test.configurationChecks(); + + return result.summary(); +} diff --git a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp index c1999c24aa..34ec2e85a8 100644 --- a/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runStabilizerIeeestTests.cpp @@ -1,17 +1,39 @@ +#include + #include "StabilizerIeeestTests.hpp" -int main() +using Log = GridKit::Utilities::Logger; + +template +void runNotchOrder(GridKit::Testing::TestingResults& result) { - GridKit::Testing::TestingResults result; + Log::misc() << "IEEEST notch order " << order << ":\n"; - GridKit::Testing::StabilizerIeeestTests test; + GridKit::Testing::StabilizerIeeestTests test; - result += test.constructor(); - result += test.zeroInitialResidual(); - result += test.residual(); + result += test.validation(); + result += test.factory(); + result += test.steadyState(); + result += test.transferFunction(); + if constexpr (order >= 1 && order <= 3) + { + result += test.factorOrdering(); + } + result += test.limiter(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); #endif +} + +int main() +{ + GridKit::Testing::TestingResults result; + + runNotchOrder<0>(result); + runNotchOrder<1>(result); + runNotchOrder<2>(result); + runNotchOrder<3>(result); + runNotchOrder<4>(result); return result.summary(); } diff --git a/tests/UnitTests/PhasorDynamics/runSystemTests.cpp b/tests/UnitTests/PhasorDynamics/runSystemTests.cpp index 71cbe85a36..ce6f12d6c6 100644 --- a/tests/UnitTests/PhasorDynamics/runSystemTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSystemTests.cpp @@ -17,6 +17,11 @@ int main() #endif result += test.allocationError(); + result += test.ieeestConstruction<0>(); + result += test.ieeestConstruction<1>(); + result += test.ieeestConstruction<2>(); + result += test.ieeestConstruction<3>(); + result += test.ieeestConstruction<4>(); result += test.componentInitializationError(); result += test.signalError();