From 2d42addf5e16881eae50aac2a2c1c2af96365db6 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 28 Oct 2026 12:09:11 -0400 Subject: [PATCH 1/9] Add TwoArea hierarchical test and bus ID accessors, and align the case's Vrmax. --- GridKit/Model/PhasorDynamics/SystemModel.hpp | 2 + .../Model/PhasorDynamics/SystemModelImpl.hpp | 17 + .../PhasorDynamics/TwoArea/TwoArea.case.json | 2 +- .../PhasorDynamics/CMakeLists.txt | 1 + .../PhasorDynamics/TwoArea/CMakeLists.txt | 15 + .../TwoArea/TwoAreaHierarchical.case.json | 790 ++++++++++++++++++ .../TwoArea/TwoAreaHierarchicalTests.hpp | 275 ++++++ .../TwoArea/runTwoAreaHierarchicalTests.cpp | 18 + .../PhasorDynamics/SubsystemFlat.json | 58 ++ .../PhasorDynamics/SubsystemHierarchical.json | 58 ++ .../PhasorDynamics/SubsystemModelTests.hpp | 50 +- .../PhasorDynamics/runSubsystemModelTests.cpp | 7 + 12 files changed, 1278 insertions(+), 15 deletions(-) create mode 100644 tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt create mode 100644 tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json create mode 100644 tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp create mode 100644 tests/IntegrationTests/PhasorDynamics/TwoArea/runTwoAreaHierarchicalTests.cpp diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index 7833d82055..33eeba3a07 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -125,6 +125,8 @@ namespace GridKit ComponentT* getComponent(const std::string& label); BranchT* getBranch(const std::string& id); + IdxT numBuses() const; + IdxT getBusId(IdxT gridkit_bus_id) const; IdxT numComponents() const; const std::string& getComponentLabel(IdxT gridkit_component_id) const; IdxT numSubsystems() const; diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 85224f88bd..2bc494be1e 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -1068,6 +1068,23 @@ namespace GridKit return bus; } + template + typename SystemModel::IdxT + SystemModel::numBuses() const + { + return static_cast(buses_.size()); + } + + /** + * @brief Return the bus_id of a bus of this system + */ + template + typename SystemModel::IdxT + SystemModel::getBusId(IdxT gridkit_bus_id) const + { + return buses_.at(gridkit_bus_id)->busID(); + } + template typename SystemModel::IdxT SystemModel::numComponents() const diff --git a/cases/PhasorDynamics/TwoArea/TwoArea.case.json b/cases/PhasorDynamics/TwoArea/TwoArea.case.json index bcf62271c8..80e553c3cc 100644 --- a/cases/PhasorDynamics/TwoArea/TwoArea.case.json +++ b/cases/PhasorDynamics/TwoArea/TwoArea.case.json @@ -734,7 +734,7 @@ "Kf": 0.0, "Tf": 0.0, "Vrmin": -3.0, - "Vrmax": 0.0, + "Vrmax": 3.0, "E1": 0.0, "E2": 0.0, "Se1": 0.0, diff --git a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt index 4da93fddf7..38bbc97900 100644 --- a/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt +++ b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt @@ -11,6 +11,7 @@ if(TARGET SUNDIALS::idas) install(TARGETS test_pd_integration) add_subdirectory(TenGen) + add_subdirectory(TwoArea) endif() if(GRIDKIT_ENABLE_ENZYME) diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt new file mode 100644 index 0000000000..67493c1aad --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt @@ -0,0 +1,15 @@ +add_executable(test_pd_two_area_hierarchical runTwoAreaHierarchicalTests.cpp) +target_link_libraries( + test_pd_two_area_hierarchical + PRIVATE GridKit::phasor_dynamics_systemmodel GridKit::solvers_dyn GridKit::testing) + +configure_file(${CMAKE_SOURCE_DIR}/cases/PhasorDynamics/TwoArea/TwoArea.case.json TwoArea.case.json + COPYONLY) +configure_file(TwoAreaHierarchical.case.json TwoAreaHierarchical.case.json COPYONLY) + +add_test(NAME PhasorDynamicsIntegrationTwoAreaHierarchical COMMAND test_pd_two_area_hierarchical) + +install(TARGETS test_pd_two_area_hierarchical) +install(FILES ${CMAKE_SOURCE_DIR}/cases/PhasorDynamics/TwoArea/TwoArea.case.json + TwoAreaHierarchical.case.json + DESTINATION share/gridkit/tests/PhasorDynamics/TwoArea) diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json new file mode 100644 index 0000000000..6896c7cbe5 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json @@ -0,0 +1,790 @@ +{ + "header": { + "format_version": 0.2, + "format_revision": 0, + "case_name": "TwoAreaHierarchical", + "case_description": "TwoArea case split into hierarchical subsystems: bus 8 at the top level (external to both areas), Area 1 (buses 1, 2, 5, 6, 7), and Area 2 (buses 3, 4, 9, 10) with the load on bus 8", + "case_comments": "" + }, + "params": { + "freq_base": 60.0, + "va_base": 100000000.0 + }, + "buses": [ + { + "number": 8, + "class": "Bus", + "name": "13", + "init": { + "Vr": 0.8761848072516336, + "Vi": -0.070766649365215 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + } + ], + "devices": [], + "subsystems": [ + { + "id": "Area1", + "level": 1, + "buses": [ + { + "number": 1, + "class": "Bus", + "name": "1", + "init": { + "Vr": 0.8417632907983953, + "Vi": 0.5398467951782767 + }, + "params": { + "kv": 20.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 2, + "class": "Bus", + "name": "2", + "init": { + "Vr": 0.9305542572926901, + "Vi": 0.3595857457779554 + }, + "params": { + "kv": 20.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 5, + "class": "Bus", + "name": "101", + "init": { + "Vr": 0.8685212337220097, + "Vi": 0.45238511469039194 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 6, + "class": "Bus", + "name": "102", + "init": { + "Vr": 0.919614611832545, + "Vi": 0.2683138874430543 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 7, + "class": "Bus", + "name": "3", + "init": { + "Vr": 0.928670340372285, + "Vi": 0.1184863350395493 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + } + ], + "signals": [ + { + "signal_id": 0, + "name": "1_1_speed" + }, + { + "signal_id": 1, + "name": "1_1_pmech" + }, + { + "signal_id": 2, + "name": "2_1_speed" + }, + { + "signal_id": 3, + "name": "2_1_pmech" + }, + { + "signal_id": 8, + "name": "1_1_efd" + }, + { + "signal_id": 9, + "name": "2_1_efd" + } + ], + "devices": [ + { + "class": "Branch", + "ports": { + "bus1": 1, + "bus2": 5 + }, + "id": "BR_1_5_1", + "params": { + "R": 0.001, + "X": 0.012, + "G": 0.0, + "B": 0.0 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 2, + "bus2": 6 + }, + "id": "BR_2_6_1", + "params": { + "R": 0.001, + "X": 0.012, + "G": 0.0, + "B": 0.0 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 5, + "bus2": 6 + }, + "id": "BR_5_6_1", + "params": { + "R": 0.005, + "X": 0.05, + "G": 0.0, + "B": 0.075 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 5, + "bus2": 6 + }, + "id": "BR_5_6_2", + "params": { + "R": 0.00501, + "X": 0.05001, + "G": 0.0, + "B": 0.075 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 6, + "bus2": 7 + }, + "id": "BR_6_7_1", + "params": { + "R": 0.002, + "X": 0.02, + "G": 0.0, + "B": 0.03 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 6, + "bus2": 7 + }, + "id": "BR_6_7_2", + "params": { + "R": 0.00201, + "X": 0.02001, + "G": 0.0, + "B": 0.03 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 7, + "bus2": 8 + }, + "id": "BR_7_8_1", + "params": { + "R": 0.02201, + "X": 0.22001, + "G": 0.0, + "B": 0.33000001 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 7, + "bus2": 8 + }, + "id": "BR_7_8_2", + "params": { + "R": 0.02202, + "X": 0.22002, + "G": 0.0, + "B": 0.33000001 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 7, + "bus2": 8 + }, + "id": "BR_7_8_3", + "params": { + "R": 0.022, + "X": 0.22, + "G": 0.0, + "B": 0.33000001 + } + }, + { + "class": "LoadZ", + "ports": { + "bus": 7 + }, + "id": "load_7_2", + "params": { + "R": 0.07531983137390114, + "X": -0.004776538085245143 + } + }, + { + "class": "Genrou", + "ports": { + "bus": 1, + "speed": 0, + "pmech": 1, + "efd": 8 + }, + "id": "1_1", + "params": { + "p0": 7.4586119652, + "q0": 1.4361202717000001, + "H": 6.5, + "D": 0.0, + "Ra": 0.0, + "Tdop": 8.0, + "Tdopp": 0.03, + "Tqopp": 0.05, + "Tqop": 0.40000001, + "Xd": 1.79999995, + "Xdp": 0.30000001, + "Xdpp": 0.25, + "Xq": 1.70000005, + "Xqp": 0.55000001, + "Xqpp": 0.25, + "Xl": 0.2, + "S10": 0.2227, + "S12": 0.39199999, + "mva": 900.0 + }, + "mon": [ + "delta", + "omega" + ] + }, + { + "class": "Genrou", + "ports": { + "bus": 2, + "speed": 2, + "pmech": 3, + "efd": 9 + }, + "id": "2_1", + "params": { + "p0": 7.0, + "q0": 3.0, + "H": 6.5, + "D": 0.0, + "Ra": 0.0, + "Tdop": 8.0, + "Tdopp": 0.03, + "Tqopp": 0.05, + "Tqop": 0.40000001, + "Xd": 1.79999995, + "Xdp": 0.30000001, + "Xdpp": 0.25, + "Xq": 1.70000005, + "Xqp": 0.55000001, + "Xqpp": 0.25, + "Xl": 0.2, + "S10": 0.0392, + "S12": 0.2227, + "mva": 900.0 + }, + "mon": [ + "delta", + "omega" + ] + }, + { + "class": "Tgov1", + "ports": { + "speed": 0, + "pmech": 1 + }, + "id": "1_1_tgov1", + "params": { + "Trate": 900.0, + "R": 0.22, + "T1": 0.1, + "T2": 0.0, + "T3": 0.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 10.0 + } + }, + { + "class": "Tgov1", + "ports": { + "speed": 2, + "pmech": 3 + }, + "id": "2_1_tgov1", + "params": { + "Trate": 900.0, + "R": 0.22, + "T1": 0.1, + "T2": 0.0, + "T3": 0.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 10.0 + } + }, + { + "class": "Ieeet1", + "ports": { + "bus": 1, + "speed": 0, + "efd": 8 + }, + "id": "1_1_ieeet1", + "params": { + "Tr": 0.0, + "Ka": 200.0, + "Ta": 1.0, + "Ke": 0.0, + "Te": 0.0, + "Kf": 0.0, + "Tf": 0.0, + "Vrmin": -3.0, + "Vrmax": 3.0, + "E1": 0.0, + "E2": 0.0, + "Se1": 0.0, + "Se2": 0.0, + "Ispdlim": 0.0 + } + }, + { + "class": "Ieeet1", + "ports": { + "bus": 2, + "speed": 2, + "efd": 9 + }, + "id": "2_1_ieeet1", + "params": { + "Tr": 0.0, + "Ka": 200.0, + "Ta": 1.0, + "Ke": 0.0, + "Te": 0.0, + "Kf": 0.0, + "Tf": 0.0, + "Vrmin": -3.0, + "Vrmax": 3.0, + "E1": 0.0, + "E2": 0.0, + "Se1": 0.0, + "Se2": 0.0, + "Ispdlim": 0.0 + } + } + ] + }, + { + "id": "Area2", + "level": 1, + "buses": [ + { + "number": 3, + "class": "Bus", + "name": "12", + "init": { + "Vr": 0.9461473466531829, + "Vi": 0.1773726063411831 + }, + "params": { + "kv": 20.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 4, + "class": "Bus", + "name": "11", + "init": { + "Vr": 0.7311328197493693, + "Vi": 0.3643895204006644 + }, + "params": { + "kv": 20.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 9, + "class": "Bus", + "name": "112", + "init": { + "Vr": 0.8866370706078832, + "Vi": 0.08324885886884298 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + }, + { + "number": 10, + "class": "Bus", + "name": "111", + "init": { + "Vr": 0.7830220631762669, + "Vi": 0.27399232263759193 + }, + "params": { + "kv": 230.0 + }, + "mon": [ + "Vr", + "Vi" + ] + } + ], + "signals": [ + { + "signal_id": 4, + "name": "3_1_speed" + }, + { + "signal_id": 5, + "name": "3_1_pmech" + }, + { + "signal_id": 6, + "name": "4_1_speed" + }, + { + "signal_id": 7, + "name": "4_1_pmech" + }, + { + "signal_id": 10, + "name": "3_1_efd" + }, + { + "signal_id": 11, + "name": "4_1_efd" + } + ], + "devices": [ + { + "class": "Branch", + "ports": { + "bus1": 3, + "bus2": 9 + }, + "id": "BR_3_9_1", + "params": { + "R": 0.001, + "X": 0.012, + "G": 0.0, + "B": 0.0 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 4, + "bus2": 10 + }, + "id": "BR_4_10_1", + "params": { + "R": 0.001, + "X": 0.012, + "G": 0.0, + "B": 0.0 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 8, + "bus2": 9 + }, + "id": "BR_8_9_1", + "params": { + "R": 0.002, + "X": 0.02, + "G": 0.0, + "B": 0.03 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 8, + "bus2": 9 + }, + "id": "BR_8_9_2", + "params": { + "R": 0.00201, + "X": 0.02001, + "G": 0.0, + "B": 0.03 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 9, + "bus2": 10 + }, + "id": "BR_9_10_1", + "params": { + "R": 0.005, + "X": 0.05, + "G": 0.0, + "B": 0.075 + } + }, + { + "class": "Branch", + "ports": { + "bus1": 9, + "bus2": 10 + }, + "id": "BR_9_10_2", + "params": { + "R": 0.00501, + "X": 0.05001, + "G": 0.0, + "B": 0.075 + } + }, + { + "class": "LoadZ", + "ports": { + "bus": 8 + }, + "id": "load_8_1", + "params": { + "R": 0.048901484908512066, + "X": -0.002791265675862604 + } + }, + { + "class": "Genrou", + "ports": { + "bus": 3, + "speed": 4, + "pmech": 5, + "efd": 10 + }, + "id": "3_1", + "params": { + "p0": 7.0, + "q0": 5.5, + "H": 6.17500019, + "D": 0.0, + "Ra": 0.0, + "Tdop": 8.0, + "Tdopp": 0.03, + "Tqopp": 0.05, + "Tqop": 0.40000001, + "Xd": 1.79999995, + "Xdp": 0.30000001, + "Xdpp": 0.25, + "Xq": 1.70000005, + "Xqp": 0.55000001, + "Xqpp": 0.25, + "Xl": 0.2, + "S10": 0.0392, + "S12": 0.2227, + "mva": 900.0 + }, + "mon": [ + "delta", + "omega" + ] + }, + { + "class": "Genrou", + "ports": { + "bus": 4, + "speed": 6, + "pmech": 7, + "efd": 11 + }, + "id": "4_1", + "params": { + "p0": 7.0, + "q0": -1.0, + "H": 6.17500019, + "D": 0.0, + "Ra": 0.0, + "Tdop": 8.0, + "Tdopp": 0.03, + "Tqopp": 0.05, + "Tqop": 0.40000001, + "Xd": 1.79999995, + "Xdp": 0.30000001, + "Xdpp": 0.25, + "Xq": 1.70000005, + "Xqp": 0.55000001, + "Xqpp": 0.25, + "Xl": 0.2, + "S10": 0.0392, + "S12": 0.2227, + "mva": 900.0 + }, + "mon": [ + "delta", + "omega" + ] + }, + { + "class": "Tgov1", + "ports": { + "speed": 4, + "pmech": 5 + }, + "id": "3_1_tgov1", + "params": { + "Trate": 900.0, + "R": 0.22, + "T1": 0.1, + "T2": 0.0, + "T3": 0.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 10.0 + } + }, + { + "class": "Tgov1", + "ports": { + "speed": 6, + "pmech": 7 + }, + "id": "4_1_tgov1", + "params": { + "Trate": 900.0, + "R": 0.22, + "T1": 0.1, + "T2": 0.0, + "T3": 0.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 10.0 + } + }, + { + "class": "Ieeet1", + "ports": { + "bus": 3, + "speed": 4, + "efd": 10 + }, + "id": "3_1_ieeet1", + "params": { + "Tr": 0.0, + "Ka": 200.0, + "Ta": 1.0, + "Ke": 0.0, + "Te": 0.0, + "Kf": 0.0, + "Tf": 0.0, + "Vrmin": -3.0, + "Vrmax": 3.0, + "E1": 0.0, + "E2": 0.0, + "Se1": 0.0, + "Se2": 0.0, + "Ispdlim": 0.0 + } + }, + { + "class": "Ieeet1", + "ports": { + "bus": 4, + "speed": 6, + "efd": 11 + }, + "id": "4_1_ieeet1", + "params": { + "Tr": 0.0, + "Ka": 200.0, + "Ta": 1.0, + "Ke": 0.0, + "Te": 0.0, + "Kf": 0.0, + "Tf": 0.0, + "Vrmin": -3.0, + "Vrmax": 3.0, + "E1": 0.0, + "E2": 0.0, + "Se1": 0.0, + "Se2": 0.0, + "Ispdlim": 0.0 + } + } + ] + } + ] +} diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp new file mode 100644 index 0000000000..4eafe7fbd1 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp @@ -0,0 +1,275 @@ +/** + * @file TwoAreaHierarchicalTests.hpp + * @author Nicholson Koukpaizan (koukpaizannk@ornl.gov) + * @brief Compare the TwoArea case with the same case split into hierarchical + * subsystems: bus 8 at the top level, Area 1 (buses 1, 2, 5, 6, 7), and + * Area 2 (buses 3, 4, 9, 10) with the load on bus 8. + * + * Variable ordering differs between the two systems, so values are compared + * through an index map built from bus IDs and component labels. + */ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class TwoAreaHierarchicalTests + { + private: + using RealT = ScalarT; + using SystemT = PhasorDynamics::SystemModel; + using BusT = PhasorDynamics::BusBase; + using DependencyMap = DependencyTracking::Variable::DependencyMap; + + public: + TwoAreaHierarchicalTests() = default; + ~TwoAreaHierarchicalTests() = default; + + /** + * @brief Compare the flat and hierarchical TwoArea systems at the + * initial point and after a short run with a fault. + */ + TestOutcome flatVsHierarchical() + { + using namespace GridKit::PhasorDynamics; + using namespace AnalysisManager::Sundials; + using Log = GridKit::Utilities::Logger; + + TestStatus success = true; + + const auto flat_data = parseSystemModelData(std::string("TwoArea.case.json")); + const auto hierarchical_data = parseSystemModelData(std::string("TwoAreaHierarchical.case.json")); + + SystemT flat(flat_data); + SystemT hierarchical(hierarchical_data); + + success *= hierarchical.numSubsystems() == 2; + if (hierarchical.numSubsystems() == 2) + { + auto* area1 = hierarchical.getSubsystem(0); + auto* area2 = hierarchical.getSubsystem(1); + + // Two levels: areas have no subsystems + success *= area1->numSubsystems() == 0; + success *= area2->numSubsystems() == 0; + + for (auto* system : {&flat, &hierarchical}) + { + success *= system->allocate() == 0; + success *= system->initialize() == 0; + success *= system->evaluateResidual() == 0; + success *= system->evaluateJacobian() == 0; + } + + const IdxT n = flat.size(); + success *= hierarchical.size() == n; + if (hierarchical.size() == n) + { + // Hierarchical variable and residual index of each flat index + std::vector variable_index(n, 0); + std::vector residual_index(n, 0); + for (IdxT bus_index = 0; bus_index < flat.numBuses(); ++bus_index) + { + const IdxT bus_id = flat.getBusId(bus_index); + + BusT* flat_bus = flat.getBus(bus_id); + BusT* bus = hierarchical.getBus(bus_id); + for (IdxT variable = 0; variable < flat_bus->size(); ++variable) + { + const IdxT flat_variable = flat_bus->getVariableIndex(variable); + const IdxT flat_residual = flat_bus->getResidualIndex(variable); + variable_index[flat_variable] = bus->getVariableIndex(variable); + residual_index[flat_residual] = bus->getResidualIndex(variable); + } + } + for (IdxT component_index = 0; component_index < flat.numComponents(); ++component_index) + { + auto* flat_component = flat.getComponent(component_index); + auto* component = hierarchical.getComponent(flat.getComponentLabel(component_index)); + for (IdxT variable = 0; variable < flat_component->size(); ++variable) + { + const IdxT flat_variable = flat_component->getVariableIndex(variable); + const IdxT flat_residual = flat_component->getResidualIndex(variable); + variable_index[flat_variable] = component->getVariableIndex(variable); + residual_index[flat_residual] = component->getResidualIndex(variable); + } + } + + // The map reaches every hierarchical index exactly once + std::vector variable_count(n, 0); + std::vector residual_count(n, 0); + for (IdxT position = 0; position < n; ++position) + { + const IdxT variable = variable_index[position]; + const IdxT residual = residual_index[position]; + ++variable_count[variable]; + ++residual_count[residual]; + } + for (IdxT position = 0; position < n; ++position) + { + success *= variable_count[position] == 1; + success *= residual_count[position] == 1; + } + + // External buses sum device contributions in a different order. + const RealT residual_tol = 100 * std::numeric_limits::epsilon(); + + const auto* y_flat = flat.y().getData(); + const auto* y = hierarchical.y().getData(); + const auto* f_flat = flat.getResidual().getData(); + const auto* f = hierarchical.getResidual().getData(); + for (IdxT position = 0; position < n; ++position) + { + const IdxT variable = variable_index[position]; + const IdxT residual = residual_index[position]; + success *= isEqual(y[variable], y_flat[position]); + success *= isEqual(f[residual], f_flat[position], residual_tol); + } + +#ifdef GRIDKIT_ENABLE_ENZYME + auto* jacobian_flat = flat.getCsrJacobian(); + auto* jacobian = hierarchical.getCsrJacobian(); + success *= jacobian_flat != nullptr && jacobian != nullptr; + if (jacobian_flat != nullptr && jacobian != nullptr) + { + success *= jacobian->getNnz() == jacobian_flat->getNnz(); + + const auto expected = MapFromCsr(jacobian); + + // Flat Jacobian entries at their hierarchical row and column + const auto flat_entries = MapFromCsr(jacobian_flat); + std::vector mapped_flat(n); + for (IdxT row = 0; row < n; ++row) + { + DependencyMap& mapped_row = mapped_flat[residual_index[row]]; + for (const auto& [column, value] : flat_entries[row]) + { + const IdxT mapped_column = variable_index[column]; + mapped_row[mapped_column] = value; + } + } + for (IdxT row = 0; row < n; ++row) + { + success *= isEqual(expected[row], mapped_flat[row]); + } + + // Bus 8 entries plus the Jacobians of both areas give the hierarchical + // system Jacobian. + BusT* bus8 = hierarchical.getBus(8); + std::vector sum(n); + auto* bus_jacobian = bus8->getCooJacobian(); + for (IdxT entry = 0; entry < bus_jacobian->getNnz(); ++entry) + { + const IdxT row = bus_jacobian->getRowData()[entry]; + const IdxT column = bus_jacobian->getColData()[entry]; + DependencyMap& sum_row = sum[row]; + sum_row[column] += bus_jacobian->getValues()[entry]; + } + + for (auto* area : {area1, area2}) + { + success *= area->evaluateJacobian() == 0; + success *= area->getExternalBuses() == std::vector{bus8}; + success *= area->getCsrJacobian() != nullptr; + if (area->getCsrJacobian() != nullptr) + { + const auto area_jacobian = MapFromCsr(area->getCsrJacobian()); + for (IdxT row = 0; row < area_jacobian.size(); ++row) + { + DependencyMap& sum_row = sum[row]; + for (const auto& [column, value] : area_jacobian[row]) + { + sum_row[column] += value; + } + } + } + } + + // Duplicate entries may be summed in a different order. + const RealT jacobian_tol = 10 * std::numeric_limits::epsilon(); + for (IdxT row = 0; row < n; ++row) + { + success *= isEqual(sum[row], expected[row], jacobian_tol); + } + } +#endif + + // Short run with a fault at bus 7 in Area 1, set up as in the + // DynamicSimulation application with the example studies' settings + const RealT dt_monitor = 1.0 / 240.0; + const RealT relative_tol = 1.0e-7; + const RealT absolute_tol = 1.0e-9; + const RealT fault_x = 0.01; + for (auto* system : {&flat, &hierarchical}) + { + const char* name = (system == &flat) ? "flat" : "hierarchical"; + BusT* bus7 = system->getBus(7); + + try + { + Ida ida(system); + ida.setTolerance(relative_tol, absolute_tol); + ida.setConsistentICType(IdaConsistentICType::YA_YDP); + ida.configureSimulation(); + ida.initializeSimulation(0.0); + ida.runSimulation(0.1, dt_monitor); + + success *= bus7->setFault(true, 0.0, fault_x) == 0; + ida.initializeSimulation(0.1); + ida.runSimulation(0.2, dt_monitor); + + success *= bus7->setFault(false, 0.0, fault_x) == 0; + ida.initializeSimulation(0.2); + ida.runSimulation(1.0, dt_monitor); + } + catch (const SundialsException&) + { + Log::error() << "IDA failed for the " << name << " system\n"; + success *= false; + } + } + + // The two systems order variables and sum residuals differently, so + // IDA takes different steps. Voltages and states agree to a multiple + // of the integrator tolerance, not to eps. + const RealT voltage_tol = 10 * relative_tol; + for (IdxT bus_index = 0; bus_index < flat.numBuses(); ++bus_index) + { + const IdxT bus_id = flat.getBusId(bus_index); + const BusT* flat_bus = flat.getBus(bus_id); + const BusT* bus = hierarchical.getBus(bus_id); + success *= isEqual(bus->Vr(), flat_bus->Vr(), voltage_tol); + success *= isEqual(bus->Vi(), flat_bus->Vi(), voltage_tol); + } + + const RealT state_tol = 100 * relative_tol; + for (IdxT position = 0; position < n; ++position) + { + const IdxT variable = variable_index[position]; + success *= isEqual(y[variable], y_flat[position], state_tol); + } + } + } + + return success.report(__func__); + } + }; + } // namespace Testing +} // namespace GridKit diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/runTwoAreaHierarchicalTests.cpp b/tests/IntegrationTests/PhasorDynamics/TwoArea/runTwoAreaHierarchicalTests.cpp new file mode 100644 index 0000000000..230ad57298 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/runTwoAreaHierarchicalTests.cpp @@ -0,0 +1,18 @@ +/** + * @file runTwoAreaHierarchicalTests.cpp + * @author Nicholson Koukpaizan (koukpaizannk@ornl.gov) + * @brief Driver for the TwoArea hierarchical system test. + * + */ + +#include "TwoAreaHierarchicalTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::Testing::TwoAreaHierarchicalTests test; + + result += test.flatVsHierarchical(); + + return result.summary(); +} diff --git a/tests/UnitTests/PhasorDynamics/SubsystemFlat.json b/tests/UnitTests/PhasorDynamics/SubsystemFlat.json index 9011724f0f..ebf0895f44 100644 --- a/tests/UnitTests/PhasorDynamics/SubsystemFlat.json +++ b/tests/UnitTests/PhasorDynamics/SubsystemFlat.json @@ -58,6 +58,16 @@ } } ], + "signals": [ + { + "signal_id": 0, + "name": "Machine Speed Deviation" + }, + { + "signal_id": 1, + "name": "Mechanical Power" + } + ], "devices": [ { "class": "Branch", @@ -111,6 +121,54 @@ "R": 5.0, "X": 0.8 } + }, + { + "class": "Genrou", + "ports": { + "bus": 4, + "speed": 0, + "pmech": 1 + }, + "id": "1", + "params": { + "p0": 1.0, + "q0": 0.05013, + "H": 3.0, + "D": 0.0, + "Ra": 0.0, + "Tdop": 7.0, + "Tdopp": 0.04, + "Tqopp": 0.05, + "Tqop": 0.75, + "Xd": 2.1, + "Xdp": 0.2, + "Xdpp": 0.18, + "Xq": 0.5, + "Xqp": 0.5, + "Xqpp": 0.18, + "Xl": 0.15, + "S10": 0.0, + "S12": 0.0, + "mva": 100.0 + } + }, + { + "class": "Tgov1", + "ports": { + "speed": 0, + "pmech": 1 + }, + "id": "1", + "params": { + "Trate": 100.0, + "R": 0.05, + "T1": 0.5, + "T2": 2.5, + "T3": 7.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 0.0 + } } ] } diff --git a/tests/UnitTests/PhasorDynamics/SubsystemHierarchical.json b/tests/UnitTests/PhasorDynamics/SubsystemHierarchical.json index 4e5801dd3c..f11baf6ebb 100644 --- a/tests/UnitTests/PhasorDynamics/SubsystemHierarchical.json +++ b/tests/UnitTests/PhasorDynamics/SubsystemHierarchical.json @@ -94,6 +94,16 @@ } } ], + "signals": [ + { + "signal_id": 0, + "name": "Machine Speed Deviation" + }, + { + "signal_id": 1, + "name": "Mechanical Power" + } + ], "devices": [ { "class": "Branch", @@ -133,6 +143,54 @@ "R": 5.0, "X": 0.8 } + }, + { + "class": "Genrou", + "ports": { + "bus": 4, + "speed": 0, + "pmech": 1 + }, + "id": "1", + "params": { + "p0": 1.0, + "q0": 0.05013, + "H": 3.0, + "D": 0.0, + "Ra": 0.0, + "Tdop": 7.0, + "Tdopp": 0.04, + "Tqopp": 0.05, + "Tqop": 0.75, + "Xd": 2.1, + "Xdp": 0.2, + "Xdpp": 0.18, + "Xq": 0.5, + "Xqp": 0.5, + "Xqpp": 0.18, + "Xl": 0.15, + "S10": 0.0, + "S12": 0.0, + "mva": 100.0 + } + }, + { + "class": "Tgov1", + "ports": { + "speed": 0, + "pmech": 1 + }, + "id": "1", + "params": { + "Trate": 100.0, + "R": 0.05, + "T1": 0.5, + "T2": 2.5, + "T3": 7.5, + "Pvmin": 0.0, + "Pvmax": 1.0, + "Dt": 0.0 + } } ] } diff --git a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp index 4aee9499de..811a907e90 100644 --- a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp @@ -1,3 +1,9 @@ +/** + * @file SubsystemModelTests.hpp + * @author Nicholson Koukpaizan (koukpaizannk@ornl.gov) + * @brief Tests for hierarchical PhasorDynamics system models. + * + */ #pragma once #include @@ -125,14 +131,26 @@ namespace GridKit auto* b = top.getSubsystem(1); auto* c = b->getSubsystem(0); + // Bus IDs of each system's own buses + success *= top.numBuses() == 1; + success *= top.getBusId(0) == 1; + success *= a->numBuses() == 1; + success *= a->getBusId(0) == 2; + success *= b->numBuses() == 1; + success *= b->getBusId(0) == 3; + success *= c->numBuses() == 1; + success *= c->getBusId(0) == 4; + // Labels in construction order success *= top.numComponents() == 0; success *= a->numComponents() == 1; success *= a->getComponentLabel(0) == "Branch_1"; - success *= c->numComponents() == 3; + success *= c->numComponents() == 5; success *= c->getComponentLabel(0) == "Branch_2"; success *= c->getComponentLabel(1) == "Branch_3"; success *= c->getComponentLabel(2) == "LoadZ_1"; + success *= c->getComponentLabel(3) == "Genrou_1"; + success *= c->getComponentLabel(4) == "Tgov1_1"; success *= top.getComponent("LoadZ_1") == c->getComponent(2); success *= b->getComponent("Branch_3") == c->getComponent(1); @@ -324,8 +342,10 @@ namespace GridKit const IdxT n = top.size(); - // Duplicate entries may be summed in a different order. - const RealT tol = 10 * std::numeric_limits::epsilon(); + // Duplicate entries may be summed in a different order, and Enzyme + // and DependencyTracking differentiate the governor's antiwindup + // sigmoid with different formulas. + const RealT tol = 100 * std::numeric_limits::epsilon(); for (const bool fault : {false, true}) { @@ -353,10 +373,10 @@ namespace GridKit const auto jacobian_flat = MapFromCsr(flat.getCsrJacobian()); const auto jacobian_top = MapFromCsr(top.getCsrJacobian()); const auto jacobian_dt = MapFromCsr(top_dt.getCsrJacobian()); - for (IdxT i = 0; i < n; ++i) + for (IdxT row = 0; row < n; ++row) { - success *= isEqual(jacobian_top[i], jacobian_flat[i]); - success *= isEqual(jacobian_top[i], jacobian_dt[i], tol); + success *= isEqual(jacobian_top[row], jacobian_flat[row]); + success *= isEqual(jacobian_top[row], jacobian_dt[row], tol); } for (auto* subsystem : {a, b, c}) @@ -379,18 +399,18 @@ namespace GridKit return success.report(__func__); } - for (IdxT i = begin; i < end; ++i) + for (IdxT row = begin; row < end; ++row) { DependencyMap expected_row; DependencyMap actual_row; - for (const auto& [column, value] : jacobian_top[i]) + for (const auto& [column, value] : jacobian_top[row]) { if (begin <= column && column < end) { expected_row[column] = value; } } - for (const auto& [column, value] : jacobian[i]) + for (const auto& [column, value] : jacobian[row]) { if (begin <= column && column < end) { @@ -475,7 +495,7 @@ namespace GridKit } /** - * @brief Compare top = [bus 1 | A[bus 2] | B[bus 3 | C[bus 4, load]]] + * @brief Compare top = [bus 1 | A[bus 2] | B[bus 3 | C[bus 4, load, machine, governor]]] * with the flat system, before and after a fault on bus 4. */ static void compareSystems(SystemT& flat, SystemT& top, TestStatus& success) @@ -501,13 +521,15 @@ namespace GridKit auto* c = b->getSubsystem(0); // Branches have no variables, so both systems order variables as - // buses 1-4, then the load. + // buses 1-4, then the load, machine and governor. success *= a->offset() == top.getBus(1)->size(); success *= a->size() == a->getBus(2)->size(); success *= b->offset() == a->offset() + a->size(); success *= b->size() == b->getBus(3)->size() + c->size(); success *= c->offset() == b->offset() + b->getBus(3)->size(); - success *= c->size() == c->getBus(4)->size() + c->getComponent(2)->size(); + success *= c->size() + == c->getBus(4)->size() + c->getComponent(2)->size() + + c->getComponent(3)->size() + c->getComponent(4)->size(); success *= top.size() == top.getBus(1)->size() + a->size() + b->size(); success *= top.size() == flat.size(); if (top.size() != flat.size()) @@ -540,9 +562,9 @@ namespace GridKit } const auto* f_flat = flat.getResidual().getData(); const auto* f_top = top.getResidual().getData(); - for (IdxT i = 0; i < flat.size(); ++i) + for (IdxT variable = 0; variable < flat.size(); ++variable) { - success *= isEqual(f_top[i], f_flat[i]); + success *= isEqual(f_top[variable], f_flat[variable]); } } } diff --git a/tests/UnitTests/PhasorDynamics/runSubsystemModelTests.cpp b/tests/UnitTests/PhasorDynamics/runSubsystemModelTests.cpp index 199143cc47..430961fd27 100644 --- a/tests/UnitTests/PhasorDynamics/runSubsystemModelTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSubsystemModelTests.cpp @@ -1,3 +1,10 @@ +/** + * @file runSubsystemModelTests.cpp + * @author Nicholson Koukpaizan (koukpaizannk@ornl.gov) + * @brief Driver for hierarchical system model tests. + * + */ + #include "SubsystemModelTests.hpp" int main() From dbaf82f4d28cf7270ff5ec81824abd2a06953f06 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Mon, 2 Nov 2026 16:50:23 -0500 Subject: [PATCH 2/9] Updated CHANGELOG. --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece0a51bdf..8e7560abb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Release Changelog -## v.03 +## v0.3 - Added `output_layout` solver option for long monitor output and a PostgreSQL example. - Restricted monitored bus names and device ids to letters, digits, spaces, `_`, `.` and `-`. @@ -25,6 +25,8 @@ - Fixed REGCA to hold the terminal voltage at `Vhvmax`, as PowerWorld does; `Khv` is accepted but unused. - Added the FourBusLimits validation case, which drives the REPCA, REECB, REGCA, and ESDC1A limits. - Added `SubsystemModel` and `BusLinePartitionInterface` to support partitioning PowerElectronics systems into independently evaluated and simulated subsystems. +- Added a `TwoArea` integration test comparing flat and hierarchical PhasorDynamics systems. +- Fixed the `Vrmax` of generator 3's exciter in the `TwoArea` case, which made the case unstable. ## v0.2 From 99cddcdbdf221d61fcbdad0451668feaf2a8f435 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Mon, 2 Nov 2026 17:41:53 -0500 Subject: [PATCH 3/9] Use plain ifs and named JSON references in hierarchical tests. --- .../TwoArea/TwoAreaHierarchicalTests.hpp | 16 ++--- .../PhasorDynamics/SubsystemModelTests.hpp | 59 ++++++++++++------- 2 files changed, 45 insertions(+), 30 deletions(-) diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp index 4eafe7fbd1..0f6a089f94 100644 --- a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchicalTests.hpp @@ -2,11 +2,7 @@ * @file TwoAreaHierarchicalTests.hpp * @author Nicholson Koukpaizan (koukpaizannk@ornl.gov) * @brief Compare the TwoArea case with the same case split into hierarchical - * subsystems: bus 8 at the top level, Area 1 (buses 1, 2, 5, 6, 7), and - * Area 2 (buses 3, 4, 9, 10) with the load on bus 8. - * - * Variable ordering differs between the two systems, so values are compared - * through an index map built from bus IDs and component labels. + * subsystems, through an index map built from bus IDs and component labels. */ #pragma once @@ -66,7 +62,7 @@ namespace GridKit auto* area1 = hierarchical.getSubsystem(0); auto* area2 = hierarchical.getSubsystem(1); - // Two levels: areas have no subsystems + // Areas have no subsystems success *= area1->numSubsystems() == 0; success *= area2->numSubsystems() == 0; @@ -219,8 +215,12 @@ namespace GridKit const RealT fault_x = 0.01; for (auto* system : {&flat, &hierarchical}) { - const char* name = (system == &flat) ? "flat" : "hierarchical"; - BusT* bus7 = system->getBus(7); + const char* name = "hierarchical"; + if (system == &flat) + { + name = "flat"; + } + BusT* bus7 = system->getBus(7); try { diff --git a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp index 811a907e90..a8c6a8034f 100644 --- a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp @@ -123,8 +123,10 @@ namespace GridKit TestStatus success = true; // Signal in subsystem C, added to any existing ones - json input = readJson("SubsystemHierarchical.json"); - input["subsystems"][1]["subsystems"][0]["signals"].push_back({{"signal_id", 5}, {"name", "s"}}); + json input = readJson("SubsystemHierarchical.json"); + json& input_subsystem_b = input["subsystems"][1]; + json& input_subsystem_c = input_subsystem_b["subsystems"][0]; + input_subsystem_c["signals"].push_back({{"signal_id", 5}, {"name", "s"}}); SystemT top(input.template get()); auto* a = top.getSubsystem(0); @@ -190,28 +192,37 @@ namespace GridKit json input; // Device uses a bus of a sibling subsystem - input = hierarchical; - input["subsystems"][0]["devices"][0]["ports"]["bus2"] = 3; - success *= constructionThrows(input); + { + input = hierarchical; + json& subsystem_a = input["subsystems"][0]; + json& branch = subsystem_a["devices"][0]; + branch["ports"]["bus2"] = 3; + success *= constructionThrows(input); + } // Device uses a signal of its parent - const json signal_r = {{"signal_id", 1}, {"name", "sr"}}; - const json signal_i = {{"signal_id", 2}, {"name", "si"}}; - const json source = {{"class", "ConstantSignalSource"}, - {"ports", {{"sr", 1}, {"si", 2}}}, - {"id", "1"}, - {"params", json::object()}}; - input = hierarchical; - input["signals"] = json::array({signal_r, signal_i}); - input["subsystems"][0]["devices"].push_back(source); + const json signal_r = {{"signal_id", 1}, {"name", "sr"}}; + const json signal_i = {{"signal_id", 2}, {"name", "si"}}; + const json source = {{"class", "ConstantSignalSource"}, + {"ports", {{"sr", 1}, {"si", 2}}}, + {"id", "1"}, + {"params", json::object()}}; + input = hierarchical; + input["signals"] = json::array({signal_r, signal_i}); + json& subsystem_with_source = input["subsystems"][0]; + subsystem_with_source["devices"].push_back(source); success *= constructionThrows(input); // Devices in a system with subsystems - input = hierarchical; - json load = input["subsystems"][1]["subsystems"][0]["devices"][2]; - load["ports"]["bus"] = 1; - input["devices"].push_back(load); - success *= constructionThrows(input); + { + input = hierarchical; + json& subsystem_b = input["subsystems"][1]; + json& subsystem_c = subsystem_b["subsystems"][0]; + json load = subsystem_c["devices"][2]; + load["ports"]["bus"] = 1; + input["devices"].push_back(load); + success *= constructionThrows(input); + } // Programmatic hierarchy: devices and subsystems in one system { @@ -255,9 +266,13 @@ namespace GridKit success *= constructionThrows(input); // Subsystem level not matching the hierarchy (grandchild at level 1) - input = hierarchical; - input["subsystems"][1]["subsystems"][0]["level"] = 1; - success *= constructionThrows(input); + { + input = hierarchical; + json& subsystem_b = input["subsystems"][1]; + json& subsystem_c = subsystem_b["subsystems"][0]; + subsystem_c["level"] = 1; + success *= constructionThrows(input); + } Log::setVerbosity(previous_verbosity); return success.report(__func__); From bc15f34818d6a9ed6a1363349a0f5014d675d236 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 4 Nov 2026 15:13:26 -0500 Subject: [PATCH 4/9] Document the hierarchical TwoArea case. --- .../PhasorDynamics/TwoArea/README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/IntegrationTests/PhasorDynamics/TwoArea/README.md diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/README.md b/tests/IntegrationTests/PhasorDynamics/TwoArea/README.md new file mode 100644 index 0000000000..9da646567d --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/README.md @@ -0,0 +1,13 @@ +# TwoArea Hierarchical Test + +`TwoAreaHierarchical.case.json` has the same system parameters, buses and devices as the [TwoArea case](../../../../cases/PhasorDynamics/TwoArea/README.md) ([`TwoArea.case.json`](../../../../cases/PhasorDynamics/TwoArea/TwoArea.case.json)), split into the [subsystems](../../../../GridKit/Model/PhasorDynamics/INPUT_FORMAT.md#subsystems) below. The test `PhasorDynamicsIntegrationTwoAreaHierarchical` compares the two files. + +System | Buses | Devices +-- | -- | -- +Top level | 8 | None +`Area1` | 1, 2, 5, 6, 7 | Generators on buses 1 and 2 with their exciters and governors, the load on bus 7, and the branches between these buses and to bus 8 +`Area2` | 3, 4, 9, 10 | Generators on buses 3 and 4 with their exciters and governors, the load on bus 8, and the branches between these buses and to bus 8 + +Bus 8 connects the two areas, so it stays at the top level, where both areas can use it. The load on bus 8 is in `Area2` because a system with subsystems cannot have devices of its own. + +The hierarchical file is a separate copy of the data, so a change to the case must be made in both files. From b5953118b698ce425a62ece12e527a1fd4f71e23 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 4 Nov 2026 15:28:41 -0500 Subject: [PATCH 5/9] Reword the Vrmax CHANGELOG entry and document the own-bus accessors. --- CHANGELOG.md | 2 +- GridKit/Model/PhasorDynamics/SystemModelImpl.hpp | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7560abb6..aa87534962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ - Added the FourBusLimits validation case, which drives the REPCA, REECB, REGCA, and ESDC1A limits. - Added `SubsystemModel` and `BusLinePartitionInterface` to support partitioning PowerElectronics systems into independently evaluated and simulated subsystems. - Added a `TwoArea` integration test comparing flat and hierarchical PhasorDynamics systems. -- Fixed the `Vrmax` of generator 3's exciter in the `TwoArea` case, which made the case unstable. +- Set the `Vrmax` of generator 3's exciter in the `TwoArea` case to match the other exciters, which delays the loss of synchronism without a fault. ## v0.2 diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 2bc494be1e..95302f14ff 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -1068,6 +1068,10 @@ namespace GridKit return bus; } + /** + * @brief Return the number of this system's own buses, without those of + * its subsystems + */ template typename SystemModel::IdxT SystemModel::numBuses() const @@ -1076,7 +1080,7 @@ namespace GridKit } /** - * @brief Return the bus_id of a bus of this system + * @brief Return the bus_id of one of this system's own buses */ template typename SystemModel::IdxT From 24c3e15a15561b0d880e4e819f0ecafc9db8d317 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 4 Nov 2026 16:01:25 -0500 Subject: [PATCH 6/9] Add rate feedback to the TwoArea exciters. --- CHANGELOG.md | 2 +- cases/PhasorDynamics/TwoArea/README.md | 2 ++ cases/PhasorDynamics/TwoArea/TwoArea.case.json | 16 ++++++++-------- .../TwoArea/TwoAreaHierarchical.case.json | 16 ++++++++-------- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa87534962..dbe1b7e576 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ - Added the FourBusLimits validation case, which drives the REPCA, REECB, REGCA, and ESDC1A limits. - Added `SubsystemModel` and `BusLinePartitionInterface` to support partitioning PowerElectronics systems into independently evaluated and simulated subsystems. - Added a `TwoArea` integration test comparing flat and hierarchical PhasorDynamics systems. -- Set the `Vrmax` of generator 3's exciter in the `TwoArea` case to match the other exciters, which delays the loss of synchronism without a fault. +- Added rate feedback (`Kf`, `Tf`) to the IEEET1 exciters in the `TwoArea` case, whose generators lost synchronism without a fault. ## v0.2 diff --git a/cases/PhasorDynamics/TwoArea/README.md b/cases/PhasorDynamics/TwoArea/README.md index 5701d02eec..9327da256b 100644 --- a/cases/PhasorDynamics/TwoArea/README.md +++ b/cases/PhasorDynamics/TwoArea/README.md @@ -10,6 +10,8 @@ Figure 1: One-line diagram of the TwoArea case.[^1] This case is derived from the Kundur two-area system and includes exciters for use in GridKit. +The exciters use the rate feedback `Kf = 0.1` and `Tf = 1.0` from the DC1C sample data in IEEE 421.5-2016, Annex H, because the generators lose synchronism without it. These values are not specific to this case. All four exciters use `Vrmax = 3.0` for consistency. + Simulation and validation studies have not yet been implemented for this case. [^1]: PowerWorld, [Simulator Help](https://www.powerworld.com/WebHelp/). diff --git a/cases/PhasorDynamics/TwoArea/TwoArea.case.json b/cases/PhasorDynamics/TwoArea/TwoArea.case.json index 80e553c3cc..15f072f7db 100644 --- a/cases/PhasorDynamics/TwoArea/TwoArea.case.json +++ b/cases/PhasorDynamics/TwoArea/TwoArea.case.json @@ -681,8 +681,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -706,8 +706,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -731,8 +731,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -756,8 +756,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json index 6896c7cbe5..479e7ff2fb 100644 --- a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json @@ -399,8 +399,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -424,8 +424,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -748,8 +748,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, @@ -773,8 +773,8 @@ "Ta": 1.0, "Ke": 0.0, "Te": 0.0, - "Kf": 0.0, - "Tf": 0.0, + "Kf": 0.1, + "Tf": 1.0, "Vrmin": -3.0, "Vrmax": 3.0, "E1": 0.0, From e5598ceeaee3e7dfd79ae3374fa7b14e7776417a Mon Sep 17 00:00:00 2001 From: nkoukpaizan Date: Wed, 4 Nov 2026 21:26:42 +0000 Subject: [PATCH 7/9] Apply pre-commit fixes --- tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt index 67493c1aad..2f2b241694 100644 --- a/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt @@ -11,5 +11,4 @@ add_test(NAME PhasorDynamicsIntegrationTwoAreaHierarchical COMMAND test_pd_two_a install(TARGETS test_pd_two_area_hierarchical) install(FILES ${CMAKE_SOURCE_DIR}/cases/PhasorDynamics/TwoArea/TwoArea.case.json - TwoAreaHierarchical.case.json - DESTINATION share/gridkit/tests/PhasorDynamics/TwoArea) + TwoAreaHierarchical.case.json DESTINATION share/gridkit/tests/PhasorDynamics/TwoArea) From 2d78e61bea75b8ef659657e88694ddc60968c566 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Wed, 4 Nov 2026 16:57:12 -0500 Subject: [PATCH 8/9] Initialize the Component time and alpha, and set alpha in the subsystem Jacobian test. --- GridKit/Model/PhasorDynamics/Component.hpp | 4 ++-- tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/GridKit/Model/PhasorDynamics/Component.hpp b/GridKit/Model/PhasorDynamics/Component.hpp index d738aea775..caf606b257 100644 --- a/GridKit/Model/PhasorDynamics/Component.hpp +++ b/GridKit/Model/PhasorDynamics/Component.hpp @@ -608,8 +608,8 @@ namespace GridKit /// Global indices of attached external signals std::vector ws_indices_; - RealT time_; - RealT alpha_; + RealT time_{0}; + RealT alpha_{0}; RealT freq_system_base_{60.0}; RealT va_system_base_{100.0e6}; diff --git a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp index a8c6a8034f..cf85fee2af 100644 --- a/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SubsystemModelTests.hpp @@ -362,6 +362,13 @@ namespace GridKit // sigmoid with different formulas. const RealT tol = 100 * std::numeric_limits::epsilon(); + // Nonzero alpha, so the Jacobians include the derivative terms. + for (auto* system : {&flat, &top}) + { + system->updateTime(0.0, 1.0); + } + top_dt.updateTime(0.0, 1.0); + for (const bool fault : {false, true}) { if (fault) From 898b76087050cfd00ae287b39eb8b0f379802979 Mon Sep 17 00:00:00 2001 From: Nicholson Koukpaizan Date: Thu, 5 Nov 2026 16:25:21 -0500 Subject: [PATCH 9/9] Remove extra explanation for TwoArea exciters. --- cases/PhasorDynamics/TwoArea/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/cases/PhasorDynamics/TwoArea/README.md b/cases/PhasorDynamics/TwoArea/README.md index 9327da256b..5701d02eec 100644 --- a/cases/PhasorDynamics/TwoArea/README.md +++ b/cases/PhasorDynamics/TwoArea/README.md @@ -10,8 +10,6 @@ Figure 1: One-line diagram of the TwoArea case.[^1] This case is derived from the Kundur two-area system and includes exciters for use in GridKit. -The exciters use the rate feedback `Kf = 0.1` and `Tf = 1.0` from the DC1C sample data in IEEE 421.5-2016, Annex H, because the generators lose synchronism without it. These values are not specific to this case. All four exciters use `Vrmax = 3.0` for consistency. - Simulation and validation studies have not yet been implemented for this case. [^1]: PowerWorld, [Simulator Help](https://www.powerworld.com/WebHelp/).