diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d190ac74..633a5ef464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ - Added `BusSignalVoltageOut` bus model with voltage signal outlets and current signal inlets. - Added `BusSignalVoltageIn` bus model with voltage signal inlets and current signal outlets. - Added `Bus::setFault` to apply or clear a fault to ground directly at a bus. +- Added `enable` and `disable` solver events that put a branch in or out of service. ## v0.2 diff --git a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp index 82a5144a21..8588f7869f 100644 --- a/GridKit/Model/PhasorDynamics/Branch/Branch.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/Branch.hpp @@ -74,6 +74,8 @@ namespace GridKit virtual int evaluateJacobian() override final; virtual int verify() const override final; + int setInService(bool in_service); + void setR(RealT R) { R_ = R; @@ -195,6 +197,7 @@ namespace GridKit RealT Bmag_{0.0}; RealT tap_{1.0}; RealT phase_{0.0}; + RealT in_service_{1.0}; ///< 1 in service, 0 out of service IdxT bus1_id_{0}; IdxT bus2_id_{0}; diff --git a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp index f7b1391f17..90d9ea5e28 100644 --- a/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp +++ b/GridKit/Model/PhasorDynamics/Branch/BranchImpl.hpp @@ -184,6 +184,19 @@ namespace GridKit return ret; } + /// Put the branch in or out of service; out of service, it injects no current. + template + int Branch::setInService(bool in_service) + { + in_service_ = ZERO; + if (in_service) + { + in_service_ = ONE; + } + setDerivedParams(); + return 0; + } + template __attribute__((always_inline)) inline void Branch::addAdmittanceContribution( const RealT G, @@ -296,6 +309,9 @@ namespace GridKit /** * @brief Residual contribution of the branch is computed and pushed to the terminal buses. * + * Out-of-service branches retain bus voltages but contribute no current. + * Empty isolated buses and floating series-only islands can make the + * system Jacobian singular; generator-supported islands need not do so. */ template int Branch::evaluateResidual() @@ -514,6 +530,16 @@ namespace GridKit g22_ = g_diag - RealT{0.5} * G_; b22_ = b_diag - RealT{0.5} * B_; + + // An out-of-service branch has zero admittance + g11_ *= in_service_; + b11_ *= in_service_; + g12_ *= in_service_; + b12_ *= in_service_; + g21_ *= in_service_; + b21_ *= in_service_; + g22_ *= in_service_; + b22_ *= in_service_; } } // namespace PhasorDynamics diff --git a/GridKit/Model/PhasorDynamics/Branch/README.md b/GridKit/Model/PhasorDynamics/Branch/README.md index adebd94fdf..a2fb1a3fee 100644 --- a/GridKit/Model/PhasorDynamics/Branch/README.md +++ b/GridKit/Model/PhasorDynamics/Branch/README.md @@ -13,6 +13,11 @@ contributions are oriented entering the adjacent buses. bus 1; both shunts are added outside the $\mathbf{M}$ transformation. - The branch has no solver-owned variables; it contributes current residuals directly to the connected buses. +- Taking a branch out of service can leave bus voltages unconstrained and make + the system Jacobian singular. This can happen when an outage leaves a bus + with no other current contributions, or a floating island of series branches + without shunts. An island with generators may still be solvable. The model + does not detect or handle these singular cases. ## Model Parameters @@ -94,12 +99,15 @@ The off-nominal transformer transformation uses bus 1 as the tap side: \end{aligned} ``` -The magnetizing and line shunts are added outside the transformation: +The magnetizing and line shunts are added outside the transformation, and the +service status $u$ scales the whole branch: ```math \begin{aligned} \mathbf{Y} &= + u + \left( \mathbf{M}^{\dagger} \mathbf{Y}_0 \mathbf{M} @@ -107,6 +115,7 @@ The magnetizing and line shunts are added outside the transformation: \mathbf{Y}_\mathrm{mag} + \mathbf{Y}_\mathrm{sh} + \right) \end{aligned} ``` diff --git a/GridKit/Model/PhasorDynamics/SystemModel.hpp b/GridKit/Model/PhasorDynamics/SystemModel.hpp index dc0ed0b7b3..f93930db9a 100644 --- a/GridKit/Model/PhasorDynamics/SystemModel.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModel.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -25,6 +26,9 @@ namespace GridKit template class BusFault; + template + class Branch; + template class SignalNode; @@ -105,6 +109,7 @@ namespace GridKit BusT* getBus(IdxT bus_id); SignalNodeT* getSignalNode(IdxT signal_id); ComponentT* getComponent(IdxT gridkit_component_id); + Branch* getBranch(const std::string& id); BusFault* getBusFault(IdxT fault_id); private: @@ -115,6 +120,8 @@ namespace GridKit std::map gridkit_bus_indices_; ///< Map between gridkit_bus_id and bus_id std::map gridkit_fault_indices_; ///< Map between fault_id and component_id + std::map gridkit_branch_indices_; ///< Map between branch id and gridkit_component_id + bool owns_components_{false}; /// Variable monitor diff --git a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp index 1cba9db950..5d83f057f2 100644 --- a/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp +++ b/GridKit/Model/PhasorDynamics/SystemModelImpl.hpp @@ -110,6 +110,8 @@ namespace GridKit auto* branch = new Branch(getBus(bus1_index), getBus(bus2_index), branchdata); + + gridkit_branch_indices_[branchdata.disambiguation_string] = static_cast(components_.size()); addComponent(branch); } @@ -830,6 +832,17 @@ namespace GridKit return components_[gridkit_component_id]; } + /** + * @brief Return pointer to a branch by case-file `id` + */ + template + Branch* + SystemModel::getBranch(const std::string& id) + { + // Should fail if user-provided id is incorrect + return dynamic_cast*>(components_[gridkit_branch_indices_.at(id)]); + } + /** * @brief Return pointer to a bus fault model * diff --git a/application/PhasorDynamics/AnalysisUtilities.hpp b/application/PhasorDynamics/AnalysisUtilities.hpp index 67db15240c..129b2469ac 100644 --- a/application/PhasorDynamics/AnalysisUtilities.hpp +++ b/application/PhasorDynamics/AnalysisUtilities.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -34,7 +35,9 @@ namespace GridKit enum class Type { FAULT_ON, - FAULT_OFF + FAULT_OFF, + IN_SERVICE, + OUT_OF_SERVICE }; /// Time event takes place @@ -43,6 +46,8 @@ namespace GridKit Type type; /// ID of element used in event (e.g., bus fault id) std::size_t element_id; + /// Case-file `id` of the branch to put in or out of service + std::string device; }; /** @@ -130,7 +135,8 @@ namespace GridKit { auto& event = c.events.emplace_back(); raw_event.at("time").get_to(event.time); - raw_event.at("element_id").get_to(event.element_id); + event.element_id = raw_event.value("element_id", INVALID_INDEX); + event.device = raw_event.value("device", std::string{}); auto type_str = raw_event.at("type").get(); using EventType = SystemEvent::Type; diff --git a/application/PhasorDynamics/ContingencyAnalysis.cpp b/application/PhasorDynamics/ContingencyAnalysis.cpp index 7fc1990aa3..bd2dea9fbe 100644 --- a/application/PhasorDynamics/ContingencyAnalysis.cpp +++ b/application/PhasorDynamics/ContingencyAnalysis.cpp @@ -7,6 +7,7 @@ #include #endif +#include #include #include #include @@ -64,6 +65,12 @@ TestStatus runStudy(StudyData study_data) case EventType::FAULT_OFF: sys.getBusFault(event.element_id)->setStatus(false); break; + case EventType::IN_SERVICE: + sys.getBranch(event.device)->setInService(true); + break; + case EventType::OUT_OF_SERVICE: + sys.getBranch(event.device)->setInService(false); + break; } // Re-initialize simulation at event time diff --git a/application/PhasorDynamics/DynamicSimulation.cpp b/application/PhasorDynamics/DynamicSimulation.cpp index c607a0f1e2..93b087ba8a 100644 --- a/application/PhasorDynamics/DynamicSimulation.cpp +++ b/application/PhasorDynamics/DynamicSimulation.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,12 @@ int runApplication(int argc, const char* argv[]) case EventType::FAULT_OFF: sys.getBusFault(event.element_id)->setStatus(false); break; + case EventType::IN_SERVICE: + sys.getBranch(event.device)->setInService(true); + break; + case EventType::OUT_OF_SERVICE: + sys.getBranch(event.device)->setInService(false); + break; } // Re-initialize simulation at event time diff --git a/application/PhasorDynamics/README.md b/application/PhasorDynamics/README.md index 9ffaf4361b..92eab86013 100644 --- a/application/PhasorDynamics/README.md +++ b/application/PhasorDynamics/README.md @@ -29,5 +29,6 @@ Each event group describes a system event that occurs at a given time point Name | Value --------------------|------------------------------------------------------- `time` | A floating point value for time event occurs - `type` | Event type (one of { "fault_on", "fault_off" }) - `element_id` | An integer value referencing the element associated with the event (e.g., bus fault id) + `type` | Event type (one of { "fault_on", "fault_off", "in_service", "out_of_service" }) +`element_id` | An integer value referencing the element associated with the event (e.g., bus fault id) + `device` | `id` of the branch to put in service ("enable") or take out of service ("disable") diff --git a/examples/PhasorDynamics/DynamicSimulation/README.md b/examples/PhasorDynamics/DynamicSimulation/README.md index ef309ebf15..1fc1f2d986 100644 --- a/examples/PhasorDynamics/DynamicSimulation/README.md +++ b/examples/PhasorDynamics/DynamicSimulation/README.md @@ -5,5 +5,6 @@ inputs. | Example | Description | | --- | --- | +| [ThreeBusBasic](Toy/ThreeBusBasic/README.md) | A three-bus line opened and reclosed. | | [ThreeBusConstantSource](Toy/ThreeBusConstantSource/README.md) | A three-bus constant signal source example. | | [ACTIVSg10k](ACTIVSg10k/README.md) | A short simulation without disturbances using the reusable ACTIVSg10k case. | diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/CMakeLists.txt b/examples/PhasorDynamics/DynamicSimulation/Toy/CMakeLists.txt index b42f4e33a8..781579c495 100644 --- a/examples/PhasorDynamics/DynamicSimulation/Toy/CMakeLists.txt +++ b/examples/PhasorDynamics/DynamicSimulation/Toy/CMakeLists.txt @@ -1 +1,2 @@ +add_subdirectory(ThreeBusBasic) add_subdirectory(ThreeBusConstantSource) diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/CMakeLists.txt b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/CMakeLists.txt new file mode 100644 index 0000000000..f92d9f3b47 --- /dev/null +++ b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/CMakeLists.txt @@ -0,0 +1,7 @@ +gridkit_example_add_file(${CMAKE_SOURCE_DIR}/cases/PhasorDynamics/Toy/ThreeBusBasic.case.json) +gridkit_example_add_file(ThreeBusBasic.solver.json) + +add_test( + NAME ThreeBusBasic_line_switching + COMMAND DynamicSimulation ThreeBusBasic.solver.json + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/README.md b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/README.md new file mode 100644 index 0000000000..9c6a3ed708 --- /dev/null +++ b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/README.md @@ -0,0 +1,8 @@ +# ThreeBusBasic Line Switching + +This study uses the reusable +[ThreeBusBasic case](../../../../../cases/PhasorDynamics/Toy/README.md). +Branch `BR_1_2` is taken out of service at 1 s and put back in service at 5 s. + +![Bus voltage magnitude](figures/ThreeBusBasic.Vm.png) +![Generator speed](figures/ThreeBusBasic.speed.png) diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/ThreeBusBasic.solver.json b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/ThreeBusBasic.solver.json new file mode 100644 index 0000000000..9bfa9704c5 --- /dev/null +++ b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/ThreeBusBasic.solver.json @@ -0,0 +1,10 @@ +{ + "system_model_file": "ThreeBusBasic.case.json", + "output_file": "ThreeBusBasic.csv", + "dt_monitor": 0.004166666666666667, + "tmax": 10.0, + "events": [ + { "time": 1.0, "type": "out_of_service", "device": "BR_1_2" }, + { "time": 5.0, "type": "in_service", "device": "BR_1_2" } + ] +} diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.Vm.png b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.Vm.png new file mode 100644 index 0000000000..554697d13f Binary files /dev/null and b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.Vm.png differ diff --git a/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.speed.png b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.speed.png new file mode 100644 index 0000000000..8a4946aa09 Binary files /dev/null and b/examples/PhasorDynamics/DynamicSimulation/Toy/ThreeBusBasic/figures/ThreeBusBasic.speed.png differ diff --git a/tests/IntegrationTests/PhasorDynamics/TenGen/Classical/TenGenClassical.cpp b/tests/IntegrationTests/PhasorDynamics/TenGen/Classical/TenGenClassical.cpp index 5d8570bf13..91bf841883 100644 --- a/tests/IntegrationTests/PhasorDynamics/TenGen/Classical/TenGenClassical.cpp +++ b/tests/IntegrationTests/PhasorDynamics/TenGen/Classical/TenGenClassical.cpp @@ -234,5 +234,15 @@ int main() error_set->display(); success *= error_set->total_error.max_value < 1e-4; + // Check that the generator-supported island remains solvable. + success *= branch56.setInService(false) == 0; + success *= ida.initializeSimulation(10.0) == 0; + success *= ida.runSimulation(10.1) == 0; + success *= sys.evaluateResidual() == 0; + for (index_type i = 0; i < sys.size(); ++i) + { + success *= isEqual(sys.getResidual().getData()[i], real_type{0.0}, 1e-5); + } + return success.report("TenGenClassical"); } diff --git a/tests/UnitTests/PhasorDynamics/BranchTests.hpp b/tests/UnitTests/PhasorDynamics/BranchTests.hpp index ba09fd4e53..5dea2270af 100644 --- a/tests/UnitTests/PhasorDynamics/BranchTests.hpp +++ b/tests/UnitTests/PhasorDynamics/BranchTests.hpp @@ -1,8 +1,10 @@ #include #include #include +#include #include +#include #include #include #include @@ -243,6 +245,70 @@ namespace GridKit return success.report(__func__); } +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome outOfServiceJacobian() + { + // Verify zero currents, fixed sparsity, and restoration after switching. + TestStatus success = true; + + PhasorDynamics::Bus bus1(10.0, 20.0); + PhasorDynamics::Bus bus2(30.0, 40.0); + bus1.allocate(); + bus2.allocate(); + bus1.initialize(); + bus2.initialize(); + for (IdxT i = 0; i < 2; ++i) + { + bus1.setVariableIndex(i, i); + bus1.setResidualIndex(i, i); + bus2.setVariableIndex(i, i + 2); + bus2.setResidualIndex(i, i + 2); + } + + PhasorDynamics::Branch branch(&bus1, &bus2, 2.0, 4.0, 0.2, 1.2); + branch.allocate(); + branch.evaluateJacobian(); + const IdxT nnz = branch.nnz(); + success *= nnz > 0; + auto* jacobian = branch.getCooJacobian(); + const std::vector rows(jacobian->getRowData(), jacobian->getRowData() + nnz); + const std::vector cols(jacobian->getColData(), jacobian->getColData() + nnz); + const std::vector values(jacobian->getValues(), jacobian->getValues() + nnz); + + success *= branch.setInService(false) == 0; + bus1.evaluateResidual(); + bus2.evaluateResidual(); + branch.evaluateResidual(); + success *= isEqual(bus1.Ir(), ScalarT{0.0}); + success *= isEqual(bus1.Ii(), ScalarT{0.0}); + success *= isEqual(bus2.Ir(), ScalarT{0.0}); + success *= isEqual(bus2.Ii(), ScalarT{0.0}); + branch.evaluateJacobian(); + success *= branch.nnz() == nnz; + + jacobian = branch.getCooJacobian(); + for (IdxT i = 0; i < nnz; ++i) + { + success *= jacobian->getRowData()[i] == rows[i]; + success *= jacobian->getColData()[i] == cols[i]; + success *= isEqual(jacobian->getValues()[i], RealT{0.0}); + } + + success *= branch.setInService(true) == 0; + branch.evaluateJacobian(); + success *= branch.nnz() == nnz; + jacobian = branch.getCooJacobian(); + for (IdxT i = 0; i < nnz; ++i) + { + success *= jacobian->getRowData()[i] == rows[i]; + success *= jacobian->getColData()[i] == cols[i]; + success *= isEqual(jacobian->getValues()[i], values[i]); + } + + return success.report(__func__); + } +#endif + TestOutcome parameterSetters() { // Verifies parameter setters refresh derived admittance values. diff --git a/tests/UnitTests/PhasorDynamics/SystemTests.hpp b/tests/UnitTests/PhasorDynamics/SystemTests.hpp index 3aeb9a7823..16b2a77167 100644 --- a/tests/UnitTests/PhasorDynamics/SystemTests.hpp +++ b/tests/UnitTests/PhasorDynamics/SystemTests.hpp @@ -222,6 +222,44 @@ namespace GridKit return success.report(__func__); } +#ifdef GRIDKIT_ENABLE_ENZYME + TestOutcome isolatedBusJacobian() + { + TestStatus success = true; + + PhasorDynamics::BusInfinite source(1.0, 0.0); + PhasorDynamics::Bus bus(1.0, 0.0); + PhasorDynamics::Branch branch(&source, &bus, 0.1, 0.2, 0.0, 0.0); + PhasorDynamics::SystemModel system; + system.addBus(&source); + system.addBus(&bus); + system.addComponent(&branch); + success *= system.allocate() == 0; + success *= system.initialize() == 0; + success *= system.evaluateJacobian() == 0; + + const IdxT nnz = system.getCsrJacobian()->getNnz(); + success *= system.size() == 2; + success *= nnz > 0; + + success *= branch.setInService(false) == 0; + success *= system.evaluateResidual() == 0; + success *= system.evaluateJacobian() == 0; + success *= isEqual(bus.Ir(), ScalarT{0.0}); + success *= isEqual(bus.Ii(), ScalarT{0.0}); + + // Both voltage variables remain, but their KCL rows are identically zero. + auto* jacobian = system.getCsrJacobian(); + success *= jacobian->getNnz() == nnz; + for (IdxT i = 0; i < nnz; ++i) + { + success *= isEqual(jacobian->getValues()[i], RealT{0.0}); + } + + return success.report(__func__); + } +#endif + TestOutcome reallocateAfterTopologyChange() { TestStatus success = true; diff --git a/tests/UnitTests/PhasorDynamics/runBranchTests.cpp b/tests/UnitTests/PhasorDynamics/runBranchTests.cpp index c0fe130c87..81d5aaf916 100644 --- a/tests/UnitTests/PhasorDynamics/runBranchTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runBranchTests.cpp @@ -16,6 +16,9 @@ int main() result += test.offNominalResidual(); result += test.jacobian(); result += test.offNominalJacobian(); +#ifdef GRIDKIT_ENABLE_ENZYME + result += test.outOfServiceJacobian(); +#endif return result.summary(); } diff --git a/tests/UnitTests/PhasorDynamics/runSystemTests.cpp b/tests/UnitTests/PhasorDynamics/runSystemTests.cpp index 71cbe85a36..6a52b1b279 100644 --- a/tests/UnitTests/PhasorDynamics/runSystemTests.cpp +++ b/tests/UnitTests/PhasorDynamics/runSystemTests.cpp @@ -14,6 +14,7 @@ int main() result += test.modelVectorsAliasSystemStorage(); #ifdef GRIDKIT_ENABLE_ENZYME result += test.jacobian(); + result += test.isolatedBusJacobian(); #endif result += test.allocationError();