diff --git a/CHANGELOG.md b/CHANGELOG.md index b908e7201..34920158d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added `enable` and `disable` solver events that put a branch in or out of service. - `verify()` returns `Model::ConfigurationChecks` listing every configuration problem instead of an error count. - Added `Model::ParameterReader` +- Added `ComponentManifest` with `MANIFEST` tables for IEEET1 and GENROU, and the `ComponentLibrary` model list. - Removed legacy PhasorDynamics component constructors in favor of constructors taking model data. ## v0.2 diff --git a/GridKit/Model/PhasorDynamics/ComponentData.hpp b/GridKit/Model/PhasorDynamics/ComponentData.hpp index be06bf828..e8484eff6 100644 --- a/GridKit/Model/PhasorDynamics/ComponentData.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentData.hpp @@ -1,16 +1,113 @@ #pragma once +#include +#include #include #include #include #include +#include #include #include +#include + namespace GridKit { namespace PhasorDynamics { + /// Manifest entry describing one parameter + template + struct ParameterEntry + { + EnumT key; + std::string_view units; + std::string_view symbol; + std::string_view description; + + constexpr std::string_view name() const + { + return Utilities::enum_name(key); + } + }; + + /// Manifest entry describing one bus or signal port + template + struct PortEntry + { + EnumT key; + std::string_view description; + + constexpr std::string_view name() const + { + return Utilities::enum_name(key); + } + }; + + /// Manifest entry describing one monitored variable + template + struct MonitorEntry + { + EnumT key; + std::string_view units; + std::string_view description; + + constexpr std::string_view name() const + { + return Utilities::enum_name(key); + } + }; + + /// True when every enumeration value has exactly one described entry + template + consteval bool entriesComplete(const std::array& entries) + { + for (std::size_t value = 0; value < N; ++value) + { + std::size_t described = 0; + for (const auto& entry : entries) + { + if (static_cast(entry.key) == value && !entry.description.empty()) + { + ++described; + } + } + if (described != 1) + { + return false; + } + } + return true; + } + + /// Units, symbols and descriptions of a component class, one entry per enumeration value + template + struct ComponentManifest + { + template + using Table = std::array()>; + + std::string_view class_name; + std::string_view description; + Table, ParametersT> parameters; + Table, BusesT> buses; + Table, InputsT> inputs; + Table, OutputsT> outputs; + Table, MonitorsT> monitors; + + /// True when the class and description are set and every table is complete + consteval bool complete() const + { + return !class_name.empty() && !description.empty() && entriesComplete(parameters) + && entriesComplete(buses) && entriesComplete(inputs) + && entriesComplete(outputs) && entriesComplete(monitors); + } + }; + /** * @brief Unified interface for `Component` data containers * @@ -52,6 +149,13 @@ namespace GridKit /// Enumeration over monitorable variables. using MonitorableVariables = monitorable_variables_enum; + /// Units, symbols and descriptions of this component class + using Manifest = ComponentManifest; + /// Class of device this is for std::string device_class; diff --git a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp index eda9dcd43..0714618b8 100644 --- a/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp +++ b/GridKit/Model/PhasorDynamics/ComponentLibrary.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include + #include #include #include @@ -23,3 +26,26 @@ #include #include #include +#include + +namespace GridKit +{ + namespace PhasorDynamics + { + /// Component models with a manifest, in `SystemModel` construction order + template + struct ComponentLibrary + { + /// Calls `visit` with each model type and its data type, in construction order + template + static constexpr void forEach(Visitor&& visit) + { + using RealT = typename ScalarTraits::RealT; + visit(std::type_identity>{}, + std::type_identity>{}); + visit(std::type_identity>{}, + std::type_identity>{}); + } + }; + } // namespace PhasorDynamics +} // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp index 6b53a34ac..8069d12d9 100644 --- a/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp +++ b/GridKit/Model/PhasorDynamics/Exciter/IEEET1/Ieeet1Data.hpp @@ -2,7 +2,7 @@ * @file Ieeet1Data.hpp * @author Luke Lowery (lukel@tamu.edu) * - * @brief Data structure for IEEET1 Data + * @brief Data structure and manifest for IEEET1 Data * */ #pragma once @@ -18,42 +18,42 @@ namespace GridKit /// Initial parameters for IEEET1 Exciter model enum class Ieeet1Parameters : size_t { - Tr, ///< Time constant for voltage sensing - Ka, ///< Coefficient for voltage regulation - Ta, ///< Time constant for voltage regulation - Ke, ///< Coefficient for excitation system - Te, ///< Time constant for excitation system - Kf, ///< Coefficient for feedback - Tf, ///< Time constant for feedback - Vrmin, ///< LL to voltage regulation - Vrmax, ///< HH to voltage regulation - E1, ///< Saturation parameter - E2, ///< Saturation parameter - Se1, ///< Saturation parameter - Se2, ///< Saturation parameter - Ispdlim, ///< Speed limit flag indicator + Tr, + Ka, + Ta, + Ke, + Te, + Kf, + Tf, + Vrmin, + Vrmax, + E1, + E2, + Se1, + Se2, + Ispdlim, }; /// Buses for a IEEET1 Exciter model enum class Ieeet1Buses : size_t { - bus, ///< Unique ID of the terminal bus + bus, }; /// Signal inputs for a IEEET1 Exciter model enum class Ieeet1SignalInputs : size_t { - speed, ///< Unique ID of the generator speed signal - vref, ///< Unique ID of the voltage reference signal (optional) - vs, ///< Unique ID of the stabilizer output signal (optional) - vuel, ///< Unique ID of the under-excitation limiter signal (optional) - voel, ///< Unique ID of the over-excitation limiter signal (optional) + speed, + vref, + vs, + vuel, + voel, }; /// Signal outputs for a IEEET1 Exciter model enum class Ieeet1SignalOutputs : size_t { - efd, ///< Unique ID of the output efd signal + efd, }; /// Variables able to be monitored for a IEEET1 Exciter model @@ -72,14 +72,55 @@ namespace GridKit * Integer parameters are of the same type as matrix and vector indices. */ template - using Ieeet1Data = - ComponentData; + struct Ieeet1Data : ComponentData + { + /// Units, symbols and descriptions of the IEEET1 data + static constexpr typename Ieeet1Data::Manifest MANIFEST{ + .class_name = "Ieeet1", + .description = "IEEE Type 1 excitation-system model", + .parameters = {{ + {Ieeet1Parameters::Tr, "s", "T_R", "Time constant for voltage sensing"}, + {Ieeet1Parameters::Ka, "p.u.", "K_A", "Coefficient for voltage regulation"}, + {Ieeet1Parameters::Ta, "s", "T_A", "Time constant for voltage regulation"}, + {Ieeet1Parameters::Ke, "p.u.", "K_E", "Exciter field resistance line slope margin, 0 requests automatic calculation"}, + {Ieeet1Parameters::Te, "s", "T_E", "Time constant for excitation system"}, + {Ieeet1Parameters::Kf, "p.u.", "K_F", "Coefficient for feedback"}, + {Ieeet1Parameters::Tf, "s", "T_F", "Time constant for feedback"}, + {Ieeet1Parameters::Vrmin, "p.u.", "V_R^{\\min}", "Lower limit to voltage regulation"}, + {Ieeet1Parameters::Vrmax, "p.u.", "V_R^{\\max}", "Upper limit to voltage regulation"}, + {Ieeet1Parameters::E1, "p.u.", "E_1", "Saturation parameter"}, + {Ieeet1Parameters::E2, "p.u.", "E_2", "Saturation parameter"}, + {Ieeet1Parameters::Se1, "p.u.", "S_1", "Saturation parameter"}, + {Ieeet1Parameters::Se2, "p.u.", "S_2", "Saturation parameter"}, + {Ieeet1Parameters::Ispdlim, "binary", "I_{\\mathrm{spdlim}}", "Speed limit flag indicator"}, + }}, + .buses = {{ + {Ieeet1Buses::bus, "Terminal bus voltage"}, + }}, + .inputs = {{ + {Ieeet1SignalInputs::speed, "Machine speed deviation"}, + {Ieeet1SignalInputs::vref, "Voltage-control reference"}, + {Ieeet1SignalInputs::vs, "Stabilizer input signal"}, + {Ieeet1SignalInputs::vuel, "Under-excitation limiter input"}, + {Ieeet1SignalInputs::voel, "Over-excitation limiter input"}, + }}, + .outputs = {{ + {Ieeet1SignalOutputs::efd, "Field-voltage output seeded by the machine"}, + }}, + .monitors = {{ + {Ieeet1MonitorableVariables::efd, "p.u.", "Field winding voltage"}, + {Ieeet1MonitorableVariables::ksat, "p.u.", "Scaled-quadratic saturation contribution"}, + }}, + }; + + static_assert(MANIFEST.complete()); + }; } // namespace Exciter } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Model/PhasorDynamics/README.md b/GridKit/Model/PhasorDynamics/README.md index b94075bae..415822e31 100644 --- a/GridKit/Model/PhasorDynamics/README.md +++ b/GridKit/Model/PhasorDynamics/README.md @@ -12,7 +12,8 @@ files: - `MyModel.hpp`: Component model declaration. - `MyModelData.hpp`: Definitions of model data including model parameters, model connection ports and model outputs (monitored - variables). + variables), and its `MANIFEST` holding their units, + symbols and descriptions. - `MyModelDependencyTracking.cpp`: Compilation unit for Jacobian evaluation using dependency tracking data type (a.k.a tapeless automatic differentiation). @@ -78,7 +79,8 @@ We recommend developers follow these steps when adding new component models: should ensure residual evaluates to zero within prescribed tolerance). 5. Once model is tested, add it to the system composer. This requires following steps: 1. Add header file `MyModel.hpp` to `ComponentLibrary.hpp`, so that - `MyModel` declaration is visible to the `SystemModel` class. + `MyModel` declaration is visible to the `SystemModel` class, and add + `MyModel` to `ComponentLibrary` in construction order. 2. Modify `SystemModelDataJSONParser.hpp` so that `MyModel` is recognized by the parser. 3. Modify `SystemModelData.hpp` so that `MyModelData` is visible to the system diff --git a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouData.hpp b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouData.hpp index 3af8dd628..c840ee359 100644 --- a/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouData.hpp +++ b/GridKit/Model/PhasorDynamics/SynchronousMachine/GENROU/GenrouData.hpp @@ -1,7 +1,7 @@ /** * @file GenrouData.hpp * @author Slaven Peles (peless@ornl.gov) - * @brief Modeling data for branches (transmission lines) + * @brief Modeling data and manifest for the GENROU synchronous machine * */ #pragma once @@ -15,44 +15,44 @@ namespace GridKit /// Initial parameters for a Genrou generator model enum class GenrouParameters : size_t { - p0, ///< Initial active power - q0, ///< Initial reactive power - H, ///< Rotor inertia - D, ///< Damping coefficient - Ra, ///< Winding resistance - Tdop, ///< Open circuit direct axis transient time - Tdopp, ///< Open circuit direct axis sub-transient time - Tqop, ///< Open circuit quadrature axis transient - Tqopp, ///< Open circuit quadrature axis sub-transient time - Xd, ///< Direct axis synchronous reactance - Xdp, ///< Direct axis transient reactance - Xdpp, ///< Direct axis sub-transient reactance - Xq, ///< Quadrature axis synchronous reactance - Xqp, ///< Quadrature axis transient reactance - Xqpp, ///< Quadrature axis sub-transient reactance - Xl, ///< Stator leakage reactance - S10, ///< Saturation factor at 1.0 pu flux - S12, ///< Saturation factor at 1.2 pu flux - mva, ///< MVA base of the genrou model + p0, + q0, + H, + D, + Ra, + Tdop, + Tdopp, + Tqop, + Tqopp, + Xd, + Xdp, + Xdpp, + Xq, + Xqp, + Xqpp, + Xl, + S10, + S12, + mva, }; /// Buses for a Genrou generator model enum class GenrouBuses : size_t { - bus, ///< Unique ID of the connecting bus + bus, }; /// Signal inputs for a Genrou generator model enum class GenrouSignalInputs : size_t { - pmech, ///< Unique ID of the signal providing mechanical power - efd, ///< Unique ID of the signal providing exciter field signal + pmech, + efd, }; /// Signal outputs for a Genrou generator model enum class GenrouSignalOutputs : size_t { - speed, ///< Unique ID of the signal receiving speed deviation + speed, }; /// Variables able to be monitored for a Genrou generator model @@ -76,13 +76,61 @@ namespace GridKit * Integer parameters are of the same type as matrix and vector indices. */ template - using GenrouData = - ComponentData; + struct GenrouData : ComponentData + { + /// Units, symbols and descriptions of the GENROU data + static constexpr typename GenrouData::Manifest MANIFEST{ + .class_name = "Genrou", + .description = "Round-rotor synchronous machine model", + .parameters = {{ + {GenrouParameters::p0, "p.u.", "P_0", "Initial active power injection"}, + {GenrouParameters::q0, "p.u.", "Q_0", "Initial reactive power injection"}, + {GenrouParameters::H, "s", "H", "Rotor inertia"}, + {GenrouParameters::D, "p.u.", "D", "Damping coefficient"}, + {GenrouParameters::Ra, "p.u.", "R_\\mathrm{a}", "Winding resistance"}, + {GenrouParameters::Tdop, "s", "T'_{d0}", "Open circuit direct axis transient time constant"}, + {GenrouParameters::Tdopp, "s", "T''_{d0}", "Open circuit direct axis sub-transient time constant"}, + {GenrouParameters::Tqop, "s", "T'_{q0}", "Open circuit quadrature axis transient time constant"}, + {GenrouParameters::Tqopp, "s", "T''_{q0}", "Open circuit quadrature axis sub-transient time constant"}, + {GenrouParameters::Xd, "p.u.", "X_d", "Direct axis synchronous reactance"}, + {GenrouParameters::Xdp, "p.u.", "X'_{d}", "Direct axis transient reactance"}, + {GenrouParameters::Xdpp, "p.u.", "X''_{d}", "Direct axis sub-transient reactance"}, + {GenrouParameters::Xq, "p.u.", "X_q", "Quadrature axis synchronous reactance"}, + {GenrouParameters::Xqp, "p.u.", "X'_{q}", "Quadrature axis transient reactance"}, + {GenrouParameters::Xqpp, "p.u.", "X''_{q}", "Quadrature axis sub-transient reactance"}, + {GenrouParameters::Xl, "p.u.", "X_{\\ell}", "Stator leakage reactance"}, + {GenrouParameters::S10, "p.u.", "S_{10}", "Saturation factor at 1.0 pu flux"}, + {GenrouParameters::S12, "p.u.", "S_{12}", "Saturation factor at 1.2 pu flux"}, + {GenrouParameters::mva, "MVA", "S^\\mathrm{base}", "Machine power base"}, + }}, + .buses = {{ + {GenrouBuses::bus, "Terminal bus"}, + }}, + .inputs = {{ + {GenrouSignalInputs::pmech, "Mechanical-power input"}, + {GenrouSignalInputs::efd, "Field-voltage input"}, + }}, + .outputs = {{ + {GenrouSignalOutputs::speed, "Machine speed-deviation output"}, + }}, + .monitors = {{ + {GenrouMonitorableVariables::ir, "p.u.", "Terminal current, real component, system base"}, + {GenrouMonitorableVariables::ii, "p.u.", "Terminal current, imaginary component, system base"}, + {GenrouMonitorableVariables::p, "p.u.", "Active power, system base"}, + {GenrouMonitorableVariables::q, "p.u.", "Reactive power, system base"}, + {GenrouMonitorableVariables::delta, "rad", "Machine internal rotor angle"}, + {GenrouMonitorableVariables::omega, "p.u.", "Machine speed deviation"}, + {GenrouMonitorableVariables::speed, "p.u.", "Per-unit machine speed"}, + }}, + }; + + static_assert(MANIFEST.complete()); + }; } // namespace PhasorDynamics } // namespace GridKit diff --git a/GridKit/Utilities/Enum.hpp b/GridKit/Utilities/Enum.hpp index e21f9908a..04db8ad49 100644 --- a/GridKit/Utilities/Enum.hpp +++ b/GridKit/Utilities/Enum.hpp @@ -80,6 +80,20 @@ namespace GridKit } } + /// Variant of `magic_enum::enum_name` that permits passing empty enums. + template + constexpr auto enum_name(T value) -> std::string_view + { + if constexpr (magic_enum::enum_count() == 0) + { + return {}; + } + else + { + return magic_enum::enum_name(value); + } + } + /// Upper bound on attainable values of the specified @ref SizedEnum. template consteval auto enum_size() -> std::size_t diff --git a/tests/UnitTests/PhasorDynamics/CMakeLists.txt b/tests/UnitTests/PhasorDynamics/CMakeLists.txt index b430e7598..845ac20a9 100644 --- a/tests/UnitTests/PhasorDynamics/CMakeLists.txt +++ b/tests/UnitTests/PhasorDynamics/CMakeLists.txt @@ -208,6 +208,10 @@ target_link_libraries( GridKit::phasor_dynamics_systemmodel GridKit::testing) +add_executable(test_phasor_component_manifest runComponentManifestTests.cpp) +target_link_libraries( + test_phasor_component_manifest GridKit::phasor_dynamics_components 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) @@ -234,6 +238,7 @@ add_test( WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) add_test(NAME PhasorDynamicsSystemSingleComponentTest COMMAND test_phasor_system_single_component) add_test(NAME PhasorDynamicsComponentConnectionTest COMMAND test_phasor_component_connection) +add_test(NAME PhasorDynamicsComponentManifestTest COMMAND test_phasor_component_manifest) install( TARGETS test_phasor_bus @@ -258,4 +263,5 @@ install( test_phasor_system test_phasor_system_single_component test_phasor_component_connection + test_phasor_component_manifest RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/PhasorDynamics/ComponentManifestTests.hpp b/tests/UnitTests/PhasorDynamics/ComponentManifestTests.hpp new file mode 100644 index 000000000..cf32b328f --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/ComponentManifestTests.hpp @@ -0,0 +1,131 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + enum class ExampleParameters : size_t + { + gain, + time_constant, + }; + + enum class ExampleBuses : size_t + { + bus, + }; + + enum class ExampleSignalInputs : size_t + { + }; + + enum class ExampleSignalOutputs : size_t + { + output, + }; + + enum class ExampleMonitorableVariables : size_t + { + state, + }; + + template + struct ExampleData : PhasorDynamics::ComponentData + { + static constexpr typename ExampleData::Manifest MANIFEST{ + .class_name = "Example", + .description = "Example component", + .parameters = {{ + {ExampleParameters::gain, "p.u.", "K", "Gain"}, + {ExampleParameters::time_constant, "s", "T", "Time constant"}, + }}, + .buses = {{ + {ExampleBuses::bus, "Terminal bus"}, + }}, + .inputs = {}, + .outputs = {{ + {ExampleSignalOutputs::output, "Output signal"}, + }}, + .monitors = {{ + {ExampleMonitorableVariables::state, "p.u.", "State"}, + }}, + }; + + static_assert(MANIFEST.complete()); + }; + + using ExampleDataT = ExampleData; + + /// Single-entry bus table left out, so its entry is value-initialized and undescribed + inline constexpr ExampleDataT::Manifest INCOMPLETE_MANIFEST{ + .class_name = "Example", + .description = "Example component", + .parameters = ExampleDataT::MANIFEST.parameters, + .outputs = ExampleDataT::MANIFEST.outputs, + .monitors = ExampleDataT::MANIFEST.monitors, + }; + static_assert(!INCOMPLETE_MANIFEST.complete()); + + /// Checks `ComponentManifest` entries and the `ComponentLibrary` model list. + class ComponentManifestTests + { + public: + TestOutcome manifest() + { + TestStatus success = true; + + const auto& manifest = ExampleDataT::MANIFEST; + + success *= (manifest.parameters.size() == Utilities::enum_size()); + success *= (manifest.parameters[1].name() == "time_constant"); + success *= (manifest.buses[0].name() == "bus"); + success *= (manifest.outputs[0].name() == "output"); + success *= (manifest.monitors[0].name() == "state"); + + // Compiles the empty-enumeration path of name(), the loop body never runs + success *= manifest.inputs.empty(); + for (const auto& entry : manifest.inputs) + { + success *= !entry.name().empty(); + } + + return success.report(__func__); + } + + TestOutcome library() + { + TestStatus success = true; + + std::size_t visited = 0; + std::set class_names; + PhasorDynamics::ComponentLibrary<>::forEach( + [&](std::type_identity, std::type_identity) + { + class_names.insert(DataT::MANIFEST.class_name); + ++visited; + }); + + // Every model has its own class, so the parser can dispatch on it + success *= (class_names.size() == visited); + + return success.report(__func__); + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PhasorDynamics/runComponentManifestTests.cpp b/tests/UnitTests/PhasorDynamics/runComponentManifestTests.cpp new file mode 100644 index 000000000..175e3cdbe --- /dev/null +++ b/tests/UnitTests/PhasorDynamics/runComponentManifestTests.cpp @@ -0,0 +1,13 @@ +#include "ComponentManifestTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + + GridKit::Testing::ComponentManifestTests test; + + result += test.manifest(); + result += test.library(); + + return result.summary(); +}