From 74fc4fcdeaa35edb2228ffd63d78f7a5bb3cc2d2 Mon Sep 17 00:00:00 2001 From: Kakeru Date: Fri, 16 Jan 2026 23:20:50 -0500 Subject: [PATCH 1/4] Add CircuitNode class for PowerElectronics --- GridKit/Model/PowerElectronics/CMakeLists.txt | 9 + .../Model/PowerElectronics/CircuitNode.hpp | 355 ++++++++++++++++++ tests/UnitTests/CMakeLists.txt | 1 + .../UnitTests/PowerElectronics/CMakeLists.txt | 7 + .../PowerElectronics/CircuitNodeTests.hpp | 74 ++++ .../PowerElectronics/runCircuitNodeTests.cpp | 12 + 6 files changed, 458 insertions(+) create mode 100644 GridKit/Model/PowerElectronics/CircuitNode.hpp create mode 100644 tests/UnitTests/PowerElectronics/CMakeLists.txt create mode 100644 tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp create mode 100644 tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp diff --git a/GridKit/Model/PowerElectronics/CMakeLists.txt b/GridKit/Model/PowerElectronics/CMakeLists.txt index aa9ba7fad..395917b44 100644 --- a/GridKit/Model/PowerElectronics/CMakeLists.txt +++ b/GridKit/Model/PowerElectronics/CMakeLists.txt @@ -1,4 +1,12 @@ +add_library(power_electronics_circuit_node INTERFACE) +target_include_directories(power_electronics_circuit_node + INTERFACE + $ + $) + +add_library(GridKit::power_electronics_circuit_node + ALIAS power_electronics_circuit_node) add_subdirectory(Capacitor) add_subdirectory(Resistor) @@ -16,6 +24,7 @@ add_subdirectory(MicrogridBusDQ) install( FILES CircuitComponent.hpp + CircuitNode.hpp CircuitGraph.hpp SystemModelPowerElectronics.hpp DESTINATION diff --git a/GridKit/Model/PowerElectronics/CircuitNode.hpp b/GridKit/Model/PowerElectronics/CircuitNode.hpp new file mode 100644 index 000000000..72db7ae25 --- /dev/null +++ b/GridKit/Model/PowerElectronics/CircuitNode.hpp @@ -0,0 +1,355 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace GridKit +{ + /** + * @brief Circuit node representing a connection point. + */ + template + class CircuitNode : public Model::Evaluator + { + using RealT = typename Model::Evaluator::RealT; + using MatrixT = typename Model::Evaluator::MatrixT; + + public: + CircuitNode() + { + size_ = 1; + } + + CircuitNode(ScalarT v0) + : V0_(v0) + { + size_ = 1; + } + + ~CircuitNode() = default; + + int setNodeID(IdxT id) + { + id_ = id; + return 0; + } + + IdxT nodeID() const + { + return id_; + } + + // Voltage accessor + ScalarT& V() + { + return y_[0]; + } + + const ScalarT& V() const + { + return y_[0]; + } + + // KCL residual accessor + ScalarT& I() + { + return f_[0]; + } + + const ScalarT& I() const + { + return f_[0]; + } + + // Allocate storage for a single-node voltage and KCL residual + int allocate() + { + size_ = 1; + nnz_ = 0; + + y_.resize(1); + yp_.resize(1); + f_.resize(1); + tag_.resize(1); + + variable_indices_[0] = 0; + residual_indices_[0] = 0; + + return 0; + } + + /*! + * @brief Sets node variables + */ + int initialize() + { + y_[0] = V0_; + yp_[0] = 0.0; + f_[0] = 0.0; + tag_[0] = false; + + return 0; + } + + int tagDifferentiable() + { + tag_[0] = false; + + return 0; + } + + int evaluateResidual() + { + // Components add to this + f_[0] = 0.0; + + return 0; + } + + bool hasJacobian() final + { + return false; + } + + int evaluateJacobian() + { + jac_.zeroMatrix(); + + return 0; + } + + int evaluateIntegrand() + { + return 0; + } + + int initializeAdjoint() + { + return 0; + } + + int evaluateAdjointResidual() + { + return 0; + } + + int evaluateAdjointIntegrand() + { + return 0; + } + + static CircuitNode ground() + { + CircuitNode g(0.0); + g.setNodeID(static_cast(-1)); + return g; + } + + private: + IdxT id_{static_cast(-1)}; + IdxT size_{1}; + IdxT nnz_{0}; + IdxT size_quad_{0}; + IdxT size_opt_{0}; + ScalarT V0_{0.0}; + + std::map variable_indices_; + std::map residual_indices_; + + std::vector y_{0.0}; + std::vector yp_{0.0}; + std::vector tag_{false}; + std::vector f_{0.0}; + + std::vector g_{}; + std::vector param_{}; + std::vector param_up_{}; + std::vector param_lo_{}; + + std::vector yB_{}; + std::vector ypB_{}; + std::vector fB_{}; + std::vector gB_{}; + + MatrixT jac_; + + RealT time_{0}; + RealT alpha_{0}; + + RealT rel_tol_{0}; + RealT abs_tol_{0}; + + IdxT max_steps_{0}; + + public: + IdxT size() final + { + return size_; + } + + IdxT nnz() final + { + return nnz_; + } + + IdxT sizeQuadrature() final + { + return size_quad_; + } + + IdxT sizeParams() final + { + return size_opt_; + } + + void updateTime(RealT /* t */, RealT /* a */) final + { + // No time to update in node models + } + + void setTolerances(RealT& rel_tol, RealT& abs_tol) const final + { + rel_tol = rel_tol_; + abs_tol = abs_tol_; + } + + void setMaxSteps(IdxT& msa) const final + { + msa = max_steps_; + } + + std::vector& y() final + { + return y_; + } + + const std::vector& y() const final + { + return y_; + } + + std::vector& yp() final + { + return yp_; + } + + const std::vector& yp() const final + { + return yp_; + } + + std::vector& tag() final + { + return tag_; + } + + const std::vector& tag() const final + { + return tag_; + } + + std::vector& yB() final + { + return yB_; + } + + const std::vector& yB() const final + { + return yB_; + } + + std::vector& ypB() final + { + return ypB_; + } + + const std::vector& ypB() const final + { + return ypB_; + } + + std::vector& param() final + { + return param_; + } + + const std::vector& param() const final + { + return param_; + } + + std::vector& param_up() final + { + return param_up_; + } + + const std::vector& param_up() const final + { + return param_up_; + } + + std::vector& param_lo() final + { + return param_lo_; + } + + const std::vector& param_lo() const final + { + return param_lo_; + } + + std::vector& getResidual() final + { + return f_; + } + + const std::vector& getResidual() const final + { + return f_; + } + + MatrixT& getJacobian() final + { + return jac_; + } + + const MatrixT& getJacobian() const final + { + return jac_; + } + + std::vector& getIntegrand() final + { + return g_; + } + + const std::vector& getIntegrand() const final + { + return g_; + } + + std::vector& getAdjointResidual() final + { + return fB_; + } + + const std::vector& getAdjointResidual() const final + { + return fB_; + } + + std::vector& getAdjointIntegrand() final + { + return gB_; + } + + const std::vector& getAdjointIntegrand() const final + { + return gB_; + } + }; +} // namespace GridKit \ No newline at end of file diff --git a/tests/UnitTests/CMakeLists.txt b/tests/UnitTests/CMakeLists.txt index c002660bc..3c7e0892c 100644 --- a/tests/UnitTests/CMakeLists.txt +++ b/tests/UnitTests/CMakeLists.txt @@ -2,5 +2,6 @@ add_subdirectory(AutomaticDifferentiation) add_subdirectory(LinearAlgebra) add_subdirectory(PhasorDynamics) +add_subdirectory(PowerElectronics) add_subdirectory(Solver) add_subdirectory(Utilities) diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt new file mode 100644 index 000000000..75f5f8a2c --- /dev/null +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(test_power_electronics_node runCircuitNodeTests.cpp) +target_link_libraries(test_power_electronics_node PRIVATE GridKit::power_electronics_circuit_node) + +add_test(NAME PowerElectronicsNodeTest COMMAND $) + +install(TARGETS test_power_electronics_node + RUNTIME DESTINATION bin) \ No newline at end of file diff --git a/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp new file mode 100644 index 000000000..bc202e57f --- /dev/null +++ b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp @@ -0,0 +1,74 @@ + +#include +#include + +#include +#include +#include + +namespace GridKit +{ + namespace Testing + { + template + class NodeTests + { + public: + NodeTests() = default; + ~NodeTests() = default; + + /// Constructor, allocation, and initialization checks + TestOutcome constructor() + { + TestStatus success = true; + + ScalarT V{1.0}; + + CircuitNode* node = nullptr; + + // Default construct + node = new CircuitNode(); + node->allocate(); + node->initialize(); + success *= isEqual(node->V(), static_cast(0)); + success *= isEqual(node->I(), static_cast(0)); + delete node; + + // Construct with initial voltage + node = new CircuitNode(V); + node->allocate(); + node->initialize(); + success *= isEqual(node->V(), V); + success *= isEqual(node->I(), static_cast(0)); + delete node; + + node = nullptr; + + return success.report(__func__); + } + + /// Accessor method tests + TestOutcome residual() + { + TestStatus success = true; + + ScalarT V{1.0}; + ScalarT I{1.0}; + + CircuitNode node(V); + node.allocate(); + node.initialize(); + success *= isEqual(node.V(), V); + + node.I() = I; // inject current + success *= isEqual(node.I(), I); + + node.evaluateResidual(); // should reset to zero + success *= isEqual(node.I(), static_cast(0)); + + return success.report(__func__); + } + }; + + } // namespace Testing +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp b/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp new file mode 100644 index 000000000..babd766ad --- /dev/null +++ b/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp @@ -0,0 +1,12 @@ +#include "CircuitNodeTests.hpp" + +int main() +{ + GridKit::Testing::TestingResults result; + GridKit::Testing::NodeTests test; + + result += test.constructor(); + result += test.residual(); + + return result.summary(); +} \ No newline at end of file From 64da3953df3e17e7c0a436a24ee5fcaac9dc6e1f Mon Sep 17 00:00:00 2001 From: Kakeru Date: Tue, 20 Jan 2026 11:40:42 -0500 Subject: [PATCH 2/4] Small fix --- .../Model/PowerElectronics/CircuitNode.hpp | 62 +++++++++---------- .../PowerElectronics/CircuitNodeTests.hpp | 6 +- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/GridKit/Model/PowerElectronics/CircuitNode.hpp b/GridKit/Model/PowerElectronics/CircuitNode.hpp index 72db7ae25..3085a2567 100644 --- a/GridKit/Model/PowerElectronics/CircuitNode.hpp +++ b/GridKit/Model/PowerElectronics/CircuitNode.hpp @@ -68,13 +68,12 @@ namespace GridKit // Allocate storage for a single-node voltage and KCL residual int allocate() { - size_ = 1; - nnz_ = 0; + size_t size = static_cast(size_); - y_.resize(1); - yp_.resize(1); - f_.resize(1); - tag_.resize(1); + y_.resize(size); + yp_.resize(size); + f_.resize(size); + tag_.resize(size); variable_indices_[0] = 0; residual_indices_[0] = 0; @@ -82,19 +81,20 @@ namespace GridKit return 0; } - /*! - * @brief Sets node variables + /** + * @brief Initialize node variables */ int initialize() { - y_[0] = V0_; - yp_[0] = 0.0; - f_[0] = 0.0; - tag_[0] = false; + y_[0] = V0_; + yp_[0] = 0.0; return 0; } + /** + * @brief Node variables are algebraic. + */ int tagDifferentiable() { tag_[0] = false; @@ -102,9 +102,15 @@ namespace GridKit return 0; } + /** + * @brief Node does not compute residuals, so here we just reset residual values. + * + * @warning This implementation assumes node residuals are always evaluated + * _before_ component model residuals. + * + */ int evaluateResidual() { - // Components add to this f_[0] = 0.0; return 0; @@ -115,10 +121,11 @@ namespace GridKit return false; } + /** + * @brief There is no Jacobian for node variables + */ int evaluateJacobian() { - jac_.zeroMatrix(); - return 0; } @@ -142,16 +149,9 @@ namespace GridKit return 0; } - static CircuitNode ground() - { - CircuitNode g(0.0); - g.setNodeID(static_cast(-1)); - return g; - } - private: IdxT id_{static_cast(-1)}; - IdxT size_{1}; + IdxT size_{0}; IdxT nnz_{0}; IdxT size_quad_{0}; IdxT size_opt_{0}; @@ -160,10 +160,12 @@ namespace GridKit std::map variable_indices_; std::map residual_indices_; - std::vector y_{0.0}; - std::vector yp_{0.0}; - std::vector tag_{false}; - std::vector f_{0.0}; + std::vector y_; + std::vector yp_; + std::vector tag_; + std::vector f_; + + MatrixT J_; std::vector g_{}; std::vector param_{}; @@ -175,8 +177,6 @@ namespace GridKit std::vector fB_{}; std::vector gB_{}; - MatrixT jac_; - RealT time_{0}; RealT alpha_{0}; @@ -314,12 +314,12 @@ namespace GridKit MatrixT& getJacobian() final { - return jac_; + return J_; } const MatrixT& getJacobian() const final { - return jac_; + return J_; } std::vector& getIntegrand() final diff --git a/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp index bc202e57f..c7147fd3a 100644 --- a/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp +++ b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp @@ -60,11 +60,11 @@ namespace GridKit node.initialize(); success *= isEqual(node.V(), V); - node.I() = I; // inject current + node.I() = I; success *= isEqual(node.I(), I); - node.evaluateResidual(); // should reset to zero - success *= isEqual(node.I(), static_cast(0)); + node.evaluateResidual(); + success *= isEqual(node.I(), 0.0); return success.report(__func__); } From 4a773e9a9b6ca88d29eacf7f3824990b7d3169c4 Mon Sep 17 00:00:00 2001 From: Kakeru Date: Tue, 20 Jan 2026 12:27:11 -0500 Subject: [PATCH 3/4] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aec17250f..eb388b870 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ - Added IDA statistics object which can be accumulated over multiple simulations. - Minor performance improvements to residual evaluation in PowerElectronics module. - Added full support for sparse Jacobians obtained with Enzyme in PhasorDynamics. - +- Added `Node` class to the PowerElectronics module to separate nodes from circuit components. ## v0.1 - Refactored code to support adding different model families. From 4fbe204741a6111b326b3350ef3004ca8474db91 Mon Sep 17 00:00:00 2001 From: Kakeru Date: Tue, 20 Jan 2026 12:31:02 -0500 Subject: [PATCH 4/4] Apply formatting --- GridKit/Model/PowerElectronics/CircuitNode.hpp | 4 ++-- tests/UnitTests/PowerElectronics/CMakeLists.txt | 2 +- tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp | 10 +++++----- .../UnitTests/PowerElectronics/runCircuitNodeTests.cpp | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/GridKit/Model/PowerElectronics/CircuitNode.hpp b/GridKit/Model/PowerElectronics/CircuitNode.hpp index 3085a2567..5fd096f62 100644 --- a/GridKit/Model/PowerElectronics/CircuitNode.hpp +++ b/GridKit/Model/PowerElectronics/CircuitNode.hpp @@ -164,7 +164,7 @@ namespace GridKit std::vector yp_; std::vector tag_; std::vector f_; - + MatrixT J_; std::vector g_{}; @@ -352,4 +352,4 @@ namespace GridKit return gB_; } }; -} // namespace GridKit \ No newline at end of file +} // namespace GridKit diff --git a/tests/UnitTests/PowerElectronics/CMakeLists.txt b/tests/UnitTests/PowerElectronics/CMakeLists.txt index 75f5f8a2c..a9bcbcc93 100644 --- a/tests/UnitTests/PowerElectronics/CMakeLists.txt +++ b/tests/UnitTests/PowerElectronics/CMakeLists.txt @@ -4,4 +4,4 @@ target_link_libraries(test_power_electronics_node PRIVATE GridKit::power_electro add_test(NAME PowerElectronicsNodeTest COMMAND $) install(TARGETS test_power_electronics_node - RUNTIME DESTINATION bin) \ No newline at end of file + RUNTIME DESTINATION bin) diff --git a/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp index c7147fd3a..2d90f2ab4 100644 --- a/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp +++ b/tests/UnitTests/PowerElectronics/CircuitNodeTests.hpp @@ -27,7 +27,7 @@ namespace GridKit CircuitNode* node = nullptr; // Default construct - node = new CircuitNode(); + node = new CircuitNode(); node->allocate(); node->initialize(); success *= isEqual(node->V(), static_cast(0)); @@ -35,7 +35,7 @@ namespace GridKit delete node; // Construct with initial voltage - node = new CircuitNode(V); + node = new CircuitNode(V); node->allocate(); node->initialize(); success *= isEqual(node->V(), V); @@ -60,10 +60,10 @@ namespace GridKit node.initialize(); success *= isEqual(node.V(), V); - node.I() = I; - success *= isEqual(node.I(), I); + node.I() = I; + success *= isEqual(node.I(), I); - node.evaluateResidual(); + node.evaluateResidual(); success *= isEqual(node.I(), 0.0); return success.report(__func__); diff --git a/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp b/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp index babd766ad..600babd50 100644 --- a/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp +++ b/tests/UnitTests/PowerElectronics/runCircuitNodeTests.cpp @@ -9,4 +9,4 @@ int main() result += test.residual(); return result.summary(); -} \ No newline at end of file +}