diff --git a/CHANGELOG.md b/CHANGELOG.md index ece0a51bd..dbe1b7e57 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. +- 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/GridKit/Model/PhasorDynamics/Component.hpp b/GridKit/Model/PhasorDynamics/Component.hpp index d738aea77..caf606b25 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/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index 7833d8205..33eeba3a0 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 85224f88b..95302f14f 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -1068,6 +1068,27 @@ 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 + { + return static_cast(buses_.size()); + } + + /** + * @brief Return the bus_id of one of this system's own buses + */ + 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 bcf62271c..15f072f7d 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,10 +731,10 @@ "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": 0.0, + "Vrmax": 3.0, "E1": 0.0, "E2": 0.0, "Se1": 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/CMakeLists.txt b/tests/IntegrationTests/PhasorDynamics/CMakeLists.txt index 4da93fddf..38bbc9790 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 000000000..2f2b24169 --- /dev/null +++ b/tests/IntegrationTests/PhasorDynamics/TwoArea/CMakeLists.txt @@ -0,0 +1,14 @@ +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/README.md b/tests/IntegrationTests/PhasorDynamics/TwoArea/README.md new file mode 100644 index 000000000..9da646567 --- /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. diff --git a/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json b/tests/IntegrationTests/PhasorDynamics/TwoArea/TwoAreaHierarchical.case.json new file mode 100644 index 000000000..479e7ff2f --- /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.1, + "Tf": 1.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.1, + "Tf": 1.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.1, + "Tf": 1.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.1, + "Tf": 1.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 000000000..0f6a089f9 --- /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, 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); + + // 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 = "hierarchical"; + if (system == &flat) + { + name = "flat"; + } + 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 000000000..230ad5729 --- /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 9011724f0..ebf0895f4 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 4e5801dd3..f11baf6eb 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 4aee9499d..cf85fee2a 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 @@ -117,22 +123,36 @@ 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); 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); @@ -172,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 { @@ -237,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__); @@ -324,8 +357,17 @@ 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(); + + // 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}) { @@ -353,10 +395,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 +421,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 +517,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 +543,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 +584,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 199143cc4..430961fd2 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()