From 935b71f6b2bf748c5cd40de8779be8a733294f56 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Wed, 29 Apr 2026 14:23:59 +0200 Subject: [PATCH 1/8] topology to separate namespace Signed-off-by: Martijn Govers --- .../include/power_grid_model/calculation_preparation.hpp | 2 ++ .../power_grid_model/include/power_grid_model/topology.hpp | 4 ++-- tests/cpp_unit_tests/test_topology.cpp | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_preparation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_preparation.hpp index 159490735e..ed6b6b7d01 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_preparation.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_preparation.hpp @@ -130,6 +130,8 @@ void reset_solvers(typename ModelType::MainModelState& state, SolverPreparationC template void rebuild_topology(typename ModelType::MainModelState& state, SolverPreparationContext& solver_context, SolversCacheStatus& solvers_cache_status) { + using topology::Topology; + // clear old solvers reset_solvers(state, solver_context, solvers_cache_status); ComponentConnections const comp_conn = main_core::construct_components_connections(state.components); diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp index 824f5a9643..28bcf743b5 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp @@ -41,7 +41,7 @@ // start search from a source // using DFS search -namespace power_grid_model { +namespace power_grid_model::topology { class Topology { using GraphIdx = size_t; @@ -699,4 +699,4 @@ class Topology { } }; -} // namespace power_grid_model +} // namespace power_grid_model::topology diff --git a/tests/cpp_unit_tests/test_topology.cpp b/tests/cpp_unit_tests/test_topology.cpp index 40059adad7..5223a06bd1 100644 --- a/tests/cpp_unit_tests/test_topology.cpp +++ b/tests/cpp_unit_tests/test_topology.cpp @@ -96,7 +96,7 @@ * (4, 6) by removing node 2 */ -namespace power_grid_model { +namespace power_grid_model::topology { namespace { @@ -424,4 +424,4 @@ TEST_CASE("Test cycle reorder") { } } -} // namespace power_grid_model +} // namespace power_grid_model::topology From cf807ef5d2fc336ad19314055efd0f9bffb354b9 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 30 Apr 2026 08:25:39 +0200 Subject: [PATCH 2/8] skeleton for supernodes Signed-off-by: Martijn Govers --- .../common/grouped_index_vector.hpp | 2 +- .../include/power_grid_model/supernodes.hpp | 98 +++++++++++++++++++ tests/cpp_unit_tests/CMakeLists.txt | 1 + tests/cpp_unit_tests/test_supernodes.cpp | 23 +++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp create mode 100644 tests/cpp_unit_tests/test_supernodes.cpp diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/grouped_index_vector.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/grouped_index_vector.hpp index 2fc27a4e08..52eb08a0f8 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/grouped_index_vector.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/grouped_index_vector.hpp @@ -266,7 +266,7 @@ class DenseGroupedIdxVector { constexpr auto begin() const { return group_iterator(Idx{}); } constexpr auto end() const { return group_iterator(size()); } - constexpr auto element_size() const { return static_cast(dense_vector_.size()); } + constexpr Idx element_size() const { return std::ssize(dense_vector_); } constexpr auto get_group(Idx element) const { return dense_vector_[element]; } constexpr auto get_element_range(Idx group) const { return *group_iterator(group); } diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp new file mode 100644 index 0000000000..43beb19b1b --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "calculation_parameters.hpp" +#include "common/common.hpp" +#include "common/counting_iterator.hpp" +#include "common/grouped_index_vector.hpp" + +#include +#include +#include +#include + +namespace power_grid_model::supernodes { +class TopologicalNode { + public: + TopologicalNode() = default; + TopologicalNode(IdxVector user_nodes, std::vector user_links) + : user_nodes_{std::move(user_nodes)}, user_links_{std::move(user_links)} { + assert(!std::ranges::empty(user_nodes_)); + assert(user_nodes_.size() > 1 || std::ranges::empty(user_links_)); + } + + constexpr bool is_supernode() const noexcept { return std::ranges::empty(user_links_); } + + private: + IdxVector user_nodes_; + std::vector user_links_; +}; + +struct TopologicalNodeMapping : DenseGroupedIdxVector { + using DenseGroupedIdxVector::DenseGroupedIdxVector; + + constexpr Idx n_topo_nodes() const { return size(); } +}; + +struct ReducedTopology { + ComponentTopology comp_topo; + std::vector topological_nodes; +}; + +namespace detail { +// Union-find algorithm: +// n nodes = 4 +// Start: [0, 1, 2, 3] +// Process links in comp topo and comp conn +// Result: [0, 1, 1, 2] => this is fed into the dense_group_elements of the DenseGroupedIdxVector +// (user nodes -> TN) +// Mapping: +// [N0, SN1, SN1, N2] +// [TN0, TN1, TN1, TN2] +inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, + ComponentConnections const& /*comp_conn*/) { + IdxVector node_mapping = + IdxRange{comp_topo.n_node} | std::ranges::to(); // TODO(mgovers): replace with real implementation + + // TODO(marcvanraalte): the implementation goes here + + return TopologicalNodeMapping{from_dense, std::move(node_mapping), comp_topo.n_node}; +}; + +inline std::vector create_topological_nodes(ComponentTopology const& /*comp_topo*/, + ComponentConnections const& /*comp_conn*/, + TopologicalNodeMapping const& topo_node_mapping) { + std::vector topo_nodes( + topo_node_mapping.n_topo_nodes()); // TODO(mgovers): replace with real implementation + + // TODO(marcvanraalte): the implementation goes here + + return topo_nodes; +}; + +// Use the topo_node_mapping to remap every part of comp_topo such that the output nodes are the topological nodes; not +// the user nodes. This effectively removes all link-related stuff from the math topology. +inline ComponentTopology construct_reduced_topology(ComponentTopology const& comp_topo, + TopologicalNodeMapping const& /*topo_node_mapping*/) { + // TODO(mgovers): do we really require a full deep-copy of comp_topo here? + ComponentTopology comp_topo_reduced{comp_topo}; // TODO(mgovers): replace with real implementation + + // TODO(marcvanraalte): the implementation goes here + + return comp_topo_reduced; +} +} // namespace detail + +inline ReducedTopology reduce_topology(ComponentTopology const& comp_topo, + ComponentConnections const& comp_conn) { + using namespace detail; + + auto topo_node_mapping = create_map(comp_topo, comp_conn); + return ReducedTopology{.comp_topo = construct_reduced_topology(comp_topo, topo_node_mapping), + .topological_nodes = create_topological_nodes(comp_topo, comp_conn, topo_node_mapping)}; +} + +} // namespace power_grid_model::supernodes diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 34cc608f49..4febe490af 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -131,6 +131,7 @@ add_executable( "test_job_dispatch.cpp" "test_calculation_preparation.cpp" "test_link_solver.cpp" + "test_supernodes.cpp" ) target_link_libraries( diff --git a/tests/cpp_unit_tests/test_supernodes.cpp b/tests/cpp_unit_tests/test_supernodes.cpp new file mode 100644 index 0000000000..64c75fba80 --- /dev/null +++ b/tests/cpp_unit_tests/test_supernodes.cpp @@ -0,0 +1,23 @@ +#include + +#include + +namespace { + +using namespace power_grid_model; + +TEST_CASE("Test Supernodes") { + SUBCASE("create_map") { + // TODO: Add test implementation + } + SUBCASE("create_topological_nodes") { + // TODO: Add test implementation + } + SUBCASE("construct_reduced_topology") { + // TODO: Add test implementation + } + SUBCASE("reduce_topology") { + // TODO: Add test implementation + } +} +} // namespace From 7e0d286c8d144db932ba4f390abc3b81178eda3f Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 7 May 2026 16:57:45 +0200 Subject: [PATCH 3/8] create map Signed-off-by: Martijn Govers --- .../calculation_parameters.hpp | 4 +- .../include/power_grid_model/supernodes.hpp | 112 ++++++++++++++---- tests/cpp_unit_tests/test_supernodes.cpp | 107 ++++++++++++++++- 3 files changed, 199 insertions(+), 24 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index bc26c488c5..4578e6a96f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -412,6 +412,7 @@ struct ComponentTopology { Idx n_node{}; std::vector branch_node_idx; std::vector branch3_node_idx; + std::vector link_node_idx; IdxVector shunt_node_idx; IdxVector source_node_idx; IdxVector load_gen_node_idx; @@ -425,7 +426,7 @@ struct ComponentTopology { IdxVector regulated_object_idx; // the index is relative to branch or branch3 std::vector regulated_object_type; - Idx n_node_total() const { return n_node + static_cast(branch3_node_idx.size()); } + constexpr Idx n_node_total() const { return n_node + std::ssize(branch3_node_idx); } }; // connection property @@ -436,6 +437,7 @@ using Branch3Connected = std::array; struct ComponentConnections { std::vector branch_connected; std::vector branch3_connected; + std::vector link_connected; DoubleVector branch_phase_shift; // 3-way branch, phase shift = phase_node_x - phase_internal_node std::vector> branch3_phase_shift; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index 43beb19b1b..fc214b3ade 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -8,8 +8,19 @@ #include "common/common.hpp" #include "common/counting_iterator.hpp" #include "common/grouped_index_vector.hpp" +#include "common/typing.hpp" +#include "index_mapping.hpp" +#include +#include +#include +#include +#include + +#include #include +#include +#include #include #include #include @@ -20,21 +31,33 @@ class TopologicalNode { TopologicalNode() = default; TopologicalNode(IdxVector user_nodes, std::vector user_links) : user_nodes_{std::move(user_nodes)}, user_links_{std::move(user_links)} { - assert(!std::ranges::empty(user_nodes_)); - assert(user_nodes_.size() > 1 || std::ranges::empty(user_links_)); + check_sanity(); } - constexpr bool is_supernode() const noexcept { return std::ranges::empty(user_links_); } + constexpr bool is_supernode() const noexcept { + check_sanity(); + return !std::ranges::empty(user_links_); + } private: IdxVector user_nodes_; std::vector user_links_; + + constexpr void check_sanity() const noexcept { + // a supernode has multiple user nodes and at least one user link; a non-supernode only contains one user node + assert(!std::ranges::empty(user_nodes_)); + assert(is_supernode() ? (user_nodes_.size() > 1 && !std::ranges::empty(user_links_)) + : (user_nodes_.size() == 1 && std::ranges::empty(user_links_))); + } }; -struct TopologicalNodeMapping : DenseGroupedIdxVector { - using DenseGroupedIdxVector::DenseGroupedIdxVector; +struct TopologicalNodeMapping { + public: + DenseGroupedIdxVector mapping; + IdxVector reorder; - constexpr Idx n_topo_nodes() const { return size(); } + constexpr Idx n_topo_nodes() const { return mapping.size(); } + constexpr Idx n_user_nodes() const { return mapping.element_size(); } }; struct ReducedTopology { @@ -43,23 +66,69 @@ struct ReducedTopology { }; namespace detail { + +inline IdxVector find_link_connected_components(ComponentTopology const& comp_topo, + ComponentConnections const& comp_conn) { + using GraphIdx = size_t; + + struct GlobalEdge {}; + struct GlobalVertex {}; + + // sparse directed graph + // edge i -> j + using GlobalGraph = boost::compressed_sparse_row_graph; + + IdxVector vertices = IdxRange{comp_topo.n_node_total()} | std::ranges::to(); + + std::vector> edges; + edges.reserve(2 * comp_topo.link_node_idx.size()); + std::ranges::for_each(std::views::zip(comp_topo.link_node_idx, comp_conn.link_connected), + [&edges](auto const& vertices_and_connectivity) { + if (BranchConnected const& connectivity = std::get<1>(vertices_and_connectivity); + connectivity[0] == 0 || connectivity[1] == 0) { + return; // only add edge if both sides are connected + } + BranchIdx const& vertices = std::get<0>(vertices_and_connectivity); + auto const from = narrow_cast(vertices[0]); + auto const to = narrow_cast(vertices[1]); + edges.emplace_back(from, to); + edges.emplace_back(to, from); + }); + + auto const global_graph_ = GlobalGraph{boost::edges_are_unsorted_multi_pass, edges.cbegin(), edges.cend(), + narrow_cast(comp_topo.n_node_total())}; + + boost::connected_components(global_graph_, vertices.data()); + return vertices; +} + // Union-find algorithm: -// n nodes = 4 -// Start: [0, 1, 2, 3] +// n nodes = 6 +// lines/trafos = [[0, 1], [3, 4], [5, 4]] +// links = [[1, 3], [5, 2], [2, 1]] +// +// Start: [0, 1, 2, 3, 4, 5] // Process links in comp topo and comp conn -// Result: [0, 1, 1, 2] => this is fed into the dense_group_elements of the DenseGroupedIdxVector +// NAIVE (WRONG): [0, 1, 1, 1, 4, 2] (2 should be super node) +// Fixing this retroactively is O(N_links^2) +// Unless we use counting sort, which is O(N_nodes + N_links) +// Result: +// indvector = [0, 1, 1, 1, 1, 4] +// reorder = [0, 1, 2, 3, 5, 4] // (user nodes -> TN) -// Mapping: -// [N0, SN1, SN1, N2] -// [TN0, TN1, TN1, TN2] -inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, - ComponentConnections const& /*comp_conn*/) { - IdxVector node_mapping = - IdxRange{comp_topo.n_node} | std::ranges::to(); // TODO(mgovers): replace with real implementation - - // TODO(marcvanraalte): the implementation goes here - - return TopologicalNodeMapping{from_dense, std::move(node_mapping), comp_topo.n_node}; +// Mapping (explanation): +// [N0, SN1, SN1, SN1, N4, SN1] +// [TN0, TN1, TN1, TN1, TN4, TN1] +inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, ComponentConnections const& comp_conn) { + std::vector node_groups; + std::unordered_map node_to_group; + + auto mapping = build_dense_mapping(find_link_connected_components(comp_topo, comp_conn), comp_topo.n_node); + auto const n_topo_nodes = comp_topo.n_node_total() > 0 ? mapping.indvector.back() + 1 : 0; + return TopologicalNodeMapping{.mapping = + DenseGroupedIdxVector{from_dense, std::move(mapping.indvector), n_topo_nodes}, + .reorder = std::move(mapping.reorder)}; }; inline std::vector create_topological_nodes(ComponentTopology const& /*comp_topo*/, @@ -86,8 +155,7 @@ inline ComponentTopology construct_reduced_topology(ComponentTopology const& com } } // namespace detail -inline ReducedTopology reduce_topology(ComponentTopology const& comp_topo, - ComponentConnections const& comp_conn) { +inline ReducedTopology reduce_topology(ComponentTopology const& comp_topo, ComponentConnections const& comp_conn) { using namespace detail; auto topo_node_mapping = create_map(comp_topo, comp_conn); diff --git a/tests/cpp_unit_tests/test_supernodes.cpp b/tests/cpp_unit_tests/test_supernodes.cpp index 64c75fba80..4a2f8d7e00 100644 --- a/tests/cpp_unit_tests/test_supernodes.cpp +++ b/tests/cpp_unit_tests/test_supernodes.cpp @@ -1,14 +1,119 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include "power_grid_model/common/common.hpp" +#include "power_grid_model/common/grouped_index_vector.hpp" +#include +#include #include #include +#include +#include + namespace { using namespace power_grid_model; +using namespace power_grid_model::supernodes; +namespace detail = power_grid_model::supernodes::detail; + +void check_equal(DenseGroupedIdxVector const& mapping, IdxVector const& expected_mapping) { + CHECK(mapping.size() == std::ranges::max(expected_mapping) + 1); + CHECK(mapping.element_size() == expected_mapping.size()); + CHECK(mapping.get_group(0) == expected_mapping[0]); + CHECK(mapping.get_group(expected_mapping.size() - 1) == expected_mapping.back()); + std::ranges::for_each(enumerate(expected_mapping), [&mapping](auto const& pair) { + auto const [idx, expected_group] = pair; + CHECK(mapping.get_group(idx) == expected_group); + }); +} TEST_CASE("Test Supernodes") { SUBCASE("create_map") { - // TODO: Add test implementation + SUBCASE("No links => no remapping") { + ComponentTopology const comp_topo{ + .n_node = 3, + .branch_node_idx = {{0, 1}, {1, 2}, {2, 0}}, + }; + ComponentConnections const comp_conn{ + .branch_connected = {{1, 1}, {1, 1}, {1, 1}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + CHECK(topo_node_mapping.n_topo_nodes() == 3); + REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); + check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 2}); + CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + } + SUBCASE("One link between node 0 and 1 => node 0 and 1 are remapped to the same topological node") { + ComponentTopology const comp_topo{ + .n_node = 3, + .branch_node_idx = {{0, 1}, {1, 2}, {2, 0}}, + .link_node_idx = {{0, 1}}, + }; + ComponentConnections const comp_conn{ + .branch_connected = {{1, 1}, {1, 1}, {1, 1}}, + .link_connected = {{1, 1}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + CHECK(topo_node_mapping.n_topo_nodes() == 2); + REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); + check_equal(topo_node_mapping.mapping, IdxVector{0, 0, 1}); + CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + } + SUBCASE("Disconnected link => not remapped") { + for (auto const disconnected : + std::array{BranchConnected{1, 0}, BranchConnected{0, 1}, BranchConnected{0, 0}}) { + ComponentTopology const comp_topo{ + .n_node = 3, + .branch_node_idx = {{0, 1}, {1, 2}, {2, 0}}, + .link_node_idx = {{0, 1}}, + }; + ComponentConnections const comp_conn{ + .branch_connected = {{1, 1}, {1, 1}, {1, 1}}, + .link_connected = {disconnected}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + CHECK(topo_node_mapping.n_topo_nodes() == 3); + REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); + check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 2}); + CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + } + } + SUBCASE("Multiple links => all connected nodes are remapped to the same topological node") { + ComponentTopology const comp_topo{ + .n_node = 6, + .link_node_idx = {{2, 1}, {5, 3}, {5, 2}}, + }; + ComponentConnections const comp_conn{ + .link_connected = {{1, 1}, {1, 1}, {1, 1}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + CHECK(topo_node_mapping.n_topo_nodes() == 3); + REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); + check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 1, 1, 1, 2}); + CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2, 3, 5, 4}); + } + SUBCASE("Multiple connected components map to different topological nodes") { + ComponentTopology const comp_topo{ + .n_node = 6, + .link_node_idx = {{0, 1}, {2, 4}, {3, 5}}, + }; + ComponentConnections const comp_conn{ + .link_connected = {{1, 1}, {1, 1}, {1, 1}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + CHECK(topo_node_mapping.n_topo_nodes() == 3); + REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); + check_equal(topo_node_mapping.mapping, IdxVector{0, 0, 1, 1, 2, 2}); + CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2, 4, 3, 5}); + } } SUBCASE("create_topological_nodes") { // TODO: Add test implementation From d668c39a064933a56c0272a4f848a9d1d379e914 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 7 May 2026 17:04:54 +0200 Subject: [PATCH 4/8] refactor Signed-off-by: Martijn Govers --- .../include/power_grid_model/supernodes.hpp | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index fc214b3ade..bd674e9e39 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -67,37 +67,35 @@ struct ReducedTopology { namespace detail { -inline IdxVector find_link_connected_components(ComponentTopology const& comp_topo, - ComponentConnections const& comp_conn) { - using GraphIdx = size_t; - +inline IdxVector find_link_connected_components(Idx n_nodes, std::vector const& edges, + std::vector const& edge_connections) { struct GlobalEdge {}; struct GlobalVertex {}; // sparse directed graph // edge i -> j using GlobalGraph = boost::compressed_sparse_row_graph; + boost::no_property, Idx, Idx>; - IdxVector vertices = IdxRange{comp_topo.n_node_total()} | std::ranges::to(); + IdxVector vertices = IdxRange{n_nodes} | std::ranges::to(); - std::vector> edges; - edges.reserve(2 * comp_topo.link_node_idx.size()); - std::ranges::for_each(std::views::zip(comp_topo.link_node_idx, comp_conn.link_connected), - [&edges](auto const& vertices_and_connectivity) { + std::vector> internal_edges; + internal_edges.reserve(2 * edges.size()); + std::ranges::for_each(std::views::zip(edges, edge_connections), + [&internal_edges](auto const& vertices_and_connectivity) { if (BranchConnected const& connectivity = std::get<1>(vertices_and_connectivity); connectivity[0] == 0 || connectivity[1] == 0) { return; // only add edge if both sides are connected } BranchIdx const& vertices = std::get<0>(vertices_and_connectivity); - auto const from = narrow_cast(vertices[0]); - auto const to = narrow_cast(vertices[1]); - edges.emplace_back(from, to); - edges.emplace_back(to, from); + auto const from = vertices[0]; + auto const to = vertices[1]; + internal_edges.emplace_back(from, to); + internal_edges.emplace_back(to, from); }); - auto const global_graph_ = GlobalGraph{boost::edges_are_unsorted_multi_pass, edges.cbegin(), edges.cend(), - narrow_cast(comp_topo.n_node_total())}; + auto const global_graph_ = + GlobalGraph{boost::edges_are_unsorted_multi_pass, internal_edges.cbegin(), internal_edges.cend(), n_nodes}; boost::connected_components(global_graph_, vertices.data()); return vertices; @@ -124,7 +122,9 @@ inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, Com std::vector node_groups; std::unordered_map node_to_group; - auto mapping = build_dense_mapping(find_link_connected_components(comp_topo, comp_conn), comp_topo.n_node); + auto mapping = build_dense_mapping( + find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected), + comp_topo.n_node); auto const n_topo_nodes = comp_topo.n_node_total() > 0 ? mapping.indvector.back() + 1 : 0; return TopologicalNodeMapping{.mapping = DenseGroupedIdxVector{from_dense, std::move(mapping.indvector), n_topo_nodes}, From 716e1dd19ecd39b15008ed2bb6965a1a1a45aff1 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 7 May 2026 17:05:20 +0200 Subject: [PATCH 5/8] refactor Signed-off-by: Martijn Govers --- .../power_grid_model/include/power_grid_model/supernodes.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index bd674e9e39..493c6e3206 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -119,9 +119,6 @@ inline IdxVector find_link_connected_components(Idx n_nodes, std::vector node_groups; - std::unordered_map node_to_group; - auto mapping = build_dense_mapping( find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected), comp_topo.n_node); From 60bc575d4a080f6291df3f0c9529fc007f24be14 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Thu, 7 May 2026 17:31:21 +0200 Subject: [PATCH 6/8] rewrite to not use grouped idx vector Signed-off-by: Martijn Govers --- .../include/power_grid_model/supernodes.hpp | 74 +++++++++++-------- tests/cpp_unit_tests/test_supernodes.cpp | 41 ++++------ 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index 493c6e3206..e43979825f 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -51,13 +51,28 @@ class TopologicalNode { } }; -struct TopologicalNodeMapping { +class TopologicalNodeMapping { public: - DenseGroupedIdxVector mapping; - IdxVector reorder; + TopologicalNodeMapping() = default; + explicit TopologicalNodeMapping(Idx n_topo_nodes_, IdxVector mapping) + : mapping_{std::move(mapping)}, n_topo_nodes_{n_topo_nodes_} { + check_sanity(); + } + + auto mapping() const& -> IdxVector const& { return mapping_; } + auto mapping() && -> IdxVector { return std::move(mapping_); } + + constexpr Idx n_topo_nodes() const { return n_topo_nodes_; } + constexpr Idx n_user_nodes() const { return mapping_.size(); } + + private: + IdxVector mapping_; + Idx n_topo_nodes_{}; - constexpr Idx n_topo_nodes() const { return mapping.size(); } - constexpr Idx n_user_nodes() const { return mapping.element_size(); } + constexpr void check_sanity() const noexcept { + // the mapping should be a valid partition of user nodes into topological nodes + assert(n_topo_nodes_ == 0 || (n_topo_nodes_ == std::ranges::max(mapping_) + 1)); + } }; struct ReducedTopology { @@ -67,8 +82,8 @@ struct ReducedTopology { namespace detail { -inline IdxVector find_link_connected_components(Idx n_nodes, std::vector const& edges, - std::vector const& edge_connections) { +inline TopologicalNodeMapping find_link_connected_components(Idx n_nodes, std::vector const& edges, + std::vector const& edge_connections) { struct GlobalEdge {}; struct GlobalVertex {}; @@ -97,8 +112,8 @@ inline IdxVector find_link_connected_components(Idx n_nodes, std::vector TN) +// [0, 1, 1, 1, 4, 1] // Mapping (explanation): // [N0, SN1, SN1, SN1, N4, SN1] -// [TN0, TN1, TN1, TN1, TN4, TN1] +// [TN0, TN1, TN1, TN1, TN2, TN1] inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, ComponentConnections const& comp_conn) { - auto mapping = build_dense_mapping( - find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected), - comp_topo.n_node); - auto const n_topo_nodes = comp_topo.n_node_total() > 0 ? mapping.indvector.back() + 1 : 0; - return TopologicalNodeMapping{.mapping = - DenseGroupedIdxVector{from_dense, std::move(mapping.indvector), n_topo_nodes}, - .reorder = std::move(mapping.reorder)}; + return find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected); }; -inline std::vector create_topological_nodes(ComponentTopology const& /*comp_topo*/, +inline std::vector create_topological_nodes(ComponentTopology const& comp_topo, ComponentConnections const& /*comp_conn*/, TopologicalNodeMapping const& topo_node_mapping) { - std::vector topo_nodes( - topo_node_mapping.n_topo_nodes()); // TODO(mgovers): replace with real implementation - - // TODO(marcvanraalte): the implementation goes here - - return topo_nodes; + // auto topo_nodes = std::ranges::transform(topo_node_mapping.mapping(), + // [](IdxRange const& user_nodes) { + // return TopologicalNode{user_nodes | std::ranges::to(), + // {}}; + // }) | + // std::ranges::to>(); + + // std::ranges::for_each(comp_topo.link_node_idx, [&topo_nodes](Idx2D const& link) { + // auto& [i, j] = link; + // topo_nodes[i].user_links_.push_back(link); + // topo_nodes[j].user_links_.push_back(link); + // }); + + // return topo_nodes; + return {}; }; -// Use the topo_node_mapping to remap every part of comp_topo such that the output nodes are the topological nodes; not -// the user nodes. This effectively removes all link-related stuff from the math topology. +// Use the topo_node_mapping to remap every part of comp_topo such that the output nodes are the topological nodes; +// not the user nodes. This effectively removes all link-related stuff from the math topology. inline ComponentTopology construct_reduced_topology(ComponentTopology const& comp_topo, TopologicalNodeMapping const& /*topo_node_mapping*/) { // TODO(mgovers): do we really require a full deep-copy of comp_topo here? diff --git a/tests/cpp_unit_tests/test_supernodes.cpp b/tests/cpp_unit_tests/test_supernodes.cpp index 4a2f8d7e00..b7a94fad27 100644 --- a/tests/cpp_unit_tests/test_supernodes.cpp +++ b/tests/cpp_unit_tests/test_supernodes.cpp @@ -19,17 +19,6 @@ using namespace power_grid_model; using namespace power_grid_model::supernodes; namespace detail = power_grid_model::supernodes::detail; -void check_equal(DenseGroupedIdxVector const& mapping, IdxVector const& expected_mapping) { - CHECK(mapping.size() == std::ranges::max(expected_mapping) + 1); - CHECK(mapping.element_size() == expected_mapping.size()); - CHECK(mapping.get_group(0) == expected_mapping[0]); - CHECK(mapping.get_group(expected_mapping.size() - 1) == expected_mapping.back()); - std::ranges::for_each(enumerate(expected_mapping), [&mapping](auto const& pair) { - auto const [idx, expected_group] = pair; - CHECK(mapping.get_group(idx) == expected_group); - }); -} - TEST_CASE("Test Supernodes") { SUBCASE("create_map") { SUBCASE("No links => no remapping") { @@ -42,10 +31,10 @@ TEST_CASE("Test Supernodes") { }; auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); CHECK(topo_node_mapping.n_topo_nodes() == 3); - REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(std::ranges::max(topo_node_mapping.mapping()) + 1 == topo_node_mapping.n_topo_nodes()); + REQUIRE(topo_node_mapping.n_user_nodes() == topo_node_mapping.mapping().size()); REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); - check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 2}); - CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + CHECK(topo_node_mapping.mapping() == IdxVector{0, 1, 2}); } SUBCASE("One link between node 0 and 1 => node 0 and 1 are remapped to the same topological node") { ComponentTopology const comp_topo{ @@ -59,10 +48,10 @@ TEST_CASE("Test Supernodes") { }; auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); CHECK(topo_node_mapping.n_topo_nodes() == 2); - REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(std::ranges::max(topo_node_mapping.mapping()) + 1 == topo_node_mapping.n_topo_nodes()); + REQUIRE(topo_node_mapping.n_user_nodes() == topo_node_mapping.mapping().size()); REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); - check_equal(topo_node_mapping.mapping, IdxVector{0, 0, 1}); - CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + CHECK(topo_node_mapping.mapping() == IdxVector{0, 0, 1}); } SUBCASE("Disconnected link => not remapped") { for (auto const disconnected : @@ -78,10 +67,10 @@ TEST_CASE("Test Supernodes") { }; auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); CHECK(topo_node_mapping.n_topo_nodes() == 3); - REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(std::ranges::max(topo_node_mapping.mapping()) + 1 == topo_node_mapping.n_topo_nodes()); + REQUIRE(topo_node_mapping.n_user_nodes() == topo_node_mapping.mapping().size()); REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); - check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 2}); - CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2}); + CHECK(topo_node_mapping.mapping() == IdxVector{0, 1, 2}); } } SUBCASE("Multiple links => all connected nodes are remapped to the same topological node") { @@ -94,10 +83,10 @@ TEST_CASE("Test Supernodes") { }; auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); CHECK(topo_node_mapping.n_topo_nodes() == 3); - REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(std::ranges::max(topo_node_mapping.mapping()) + 1 == topo_node_mapping.n_topo_nodes()); + REQUIRE(topo_node_mapping.n_user_nodes() == topo_node_mapping.mapping().size()); REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); - check_equal(topo_node_mapping.mapping, IdxVector{0, 1, 1, 1, 1, 2}); - CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2, 3, 5, 4}); + CHECK(topo_node_mapping.mapping() == IdxVector{0, 1, 1, 1, 2, 1}); } SUBCASE("Multiple connected components map to different topological nodes") { ComponentTopology const comp_topo{ @@ -109,10 +98,10 @@ TEST_CASE("Test Supernodes") { }; auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); CHECK(topo_node_mapping.n_topo_nodes() == 3); - REQUIRE(topo_node_mapping.n_topo_nodes() == topo_node_mapping.mapping.size()); + REQUIRE(std::ranges::max(topo_node_mapping.mapping()) + 1 == topo_node_mapping.n_topo_nodes()); + REQUIRE(topo_node_mapping.n_user_nodes() == topo_node_mapping.mapping().size()); REQUIRE(topo_node_mapping.n_user_nodes() == comp_topo.n_node); - check_equal(topo_node_mapping.mapping, IdxVector{0, 0, 1, 1, 2, 2}); - CHECK(topo_node_mapping.reorder == IdxVector{0, 1, 2, 4, 3, 5}); + CHECK(topo_node_mapping.mapping() == IdxVector{0, 0, 1, 2, 1, 2}); } } SUBCASE("create_topological_nodes") { From 4ed7f7a496a14ad9e89398674d3bfd6f77a07934 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Mon, 11 May 2026 18:49:33 +0200 Subject: [PATCH 7/8] create supernodes Signed-off-by: Martijn Govers --- .../common/counting_iterator.hpp | 13 ++ .../include/power_grid_model/supernodes.hpp | 112 ++++++++++-------- .../include/power_grid_model/topology.hpp | 5 +- .../cpp_unit_tests/test_counting_iterator.cpp | 15 +++ tests/cpp_unit_tests/test_supernodes.cpp | 47 +++++++- 5 files changed, 137 insertions(+), 55 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/common/counting_iterator.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/counting_iterator.hpp index ed6e8c6920..5db7cbbd3d 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/common/counting_iterator.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/common/counting_iterator.hpp @@ -29,4 +29,17 @@ constexpr auto enumerate(std::ranges::viewable_range auto&& range_to_enumerate) } #endif +#if __cpp_lib_ranges_pairwise >= 202110L +using std::views::pairwise; +#else +constexpr auto pairwise(std::ranges::viewable_range auto&& range_to_pair) { + auto const begin = std::ranges::begin(range_to_pair); + auto const end = std::ranges::end(range_to_pair); + if (begin == end) { + return std::views::zip(std::ranges::subrange(begin, begin), std::ranges::subrange(begin, begin)); + } + return std::views::zip(std::ranges::subrange(begin, end - 1), std::ranges::subrange(begin + 1, end)); +} +#endif + } // namespace power_grid_model diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index e43979825f..c3aa919913 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -7,9 +7,6 @@ #include "calculation_parameters.hpp" #include "common/common.hpp" #include "common/counting_iterator.hpp" -#include "common/grouped_index_vector.hpp" -#include "common/typing.hpp" -#include "index_mapping.hpp" #include #include @@ -19,38 +16,12 @@ #include #include -#include #include #include #include #include namespace power_grid_model::supernodes { -class TopologicalNode { - public: - TopologicalNode() = default; - TopologicalNode(IdxVector user_nodes, std::vector user_links) - : user_nodes_{std::move(user_nodes)}, user_links_{std::move(user_links)} { - check_sanity(); - } - - constexpr bool is_supernode() const noexcept { - check_sanity(); - return !std::ranges::empty(user_links_); - } - - private: - IdxVector user_nodes_; - std::vector user_links_; - - constexpr void check_sanity() const noexcept { - // a supernode has multiple user nodes and at least one user link; a non-supernode only contains one user node - assert(!std::ranges::empty(user_nodes_)); - assert(is_supernode() ? (user_nodes_.size() > 1 && !std::ranges::empty(user_links_)) - : (user_nodes_.size() == 1 && std::ranges::empty(user_links_))); - } -}; - class TopologicalNodeMapping { public: TopologicalNodeMapping() = default; @@ -63,7 +34,7 @@ class TopologicalNodeMapping { auto mapping() && -> IdxVector { return std::move(mapping_); } constexpr Idx n_topo_nodes() const { return n_topo_nodes_; } - constexpr Idx n_user_nodes() const { return mapping_.size(); } + constexpr Idx n_user_nodes() const { return std::ssize(mapping_); } private: IdxVector mapping_; @@ -75,9 +46,27 @@ class TopologicalNodeMapping { } }; +struct TopologicalNode { + IdxVector user_nodes; + std::vector user_links; + + constexpr auto is_supernode() const noexcept -> bool { return user_nodes.size() > 1 && !user_links.empty(); } +}; + +struct ComponentToTopoNodeCoupling { + Idx n_topo_nodes{}; + std::vector user_nodes_to_topo_nodes; + std::vector user_links_to_topo_nodes; +}; + +struct TopologicalNodesAndCoupling { + std::vector topo_nodes; + ComponentToTopoNodeCoupling coupling; +}; + struct ReducedTopology { - ComponentTopology comp_topo; - std::vector topological_nodes; + ComponentTopology reduced_comp_topo; + TopologicalNodesAndCoupling topo_node_coup; }; namespace detail { @@ -135,24 +124,43 @@ inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, Com return find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected); }; -inline std::vector create_topological_nodes(ComponentTopology const& comp_topo, - ComponentConnections const& /*comp_conn*/, - TopologicalNodeMapping const& topo_node_mapping) { - // auto topo_nodes = std::ranges::transform(topo_node_mapping.mapping(), - // [](IdxRange const& user_nodes) { - // return TopologicalNode{user_nodes | std::ranges::to(), - // {}}; - // }) | - // std::ranges::to>(); - - // std::ranges::for_each(comp_topo.link_node_idx, [&topo_nodes](Idx2D const& link) { - // auto& [i, j] = link; - // topo_nodes[i].user_links_.push_back(link); - // topo_nodes[j].user_links_.push_back(link); - // }); - - // return topo_nodes; - return {}; +inline TopologicalNodesAndCoupling create_topological_nodes(ComponentTopology const& comp_topo, + ComponentConnections const& comp_conn, + TopologicalNodeMapping const& topo_node_mapping) { + auto const& node_mapping = topo_node_mapping.mapping(); + + std::vector topo_nodes(topo_node_mapping.n_topo_nodes()); + std::vector user_node_topo_node_coup = enumerate(node_mapping) | + std::views::transform([&topo_nodes](auto const& idx_and_topo) { + auto const& [user_node, topo_node] = idx_and_topo; + topo_nodes[topo_node].user_nodes.push_back(user_node); + return Idx2D{.group = topo_node, .pos = user_node}; + }) | + std::ranges::to(); + + std::vector user_link_topo_node_coup = + enumerate(std::views::zip(comp_topo.link_node_idx, comp_conn.link_connected)) | + std::views::transform([&node_mapping, &topo_nodes](auto const& idx_link_and_connectivity) { + auto const& [link_idx, link_nodes_and_connectivity] = idx_link_and_connectivity; + auto const& [link_nodes, link_connected] = link_nodes_and_connectivity; + if (link_connected[0] == 0 || link_connected[1] == 0) { + return Idx2D{.group = disconnected, .pos = disconnected}; + } + auto const [from, to] = link_nodes; + auto const topo_from = node_mapping[from]; + assert(topo_from == node_mapping[to]); // sanity check: links should be merged at this point + + auto& user_links = topo_nodes[topo_from].user_links; + Idx const pos = std::ssize(user_links); + user_links.push_back(BranchIdx{from, to}); // can't emplace_back because it's std::array + return Idx2D{.group = topo_from, .pos = pos}; + }) | + std::ranges::to(); + + return TopologicalNodesAndCoupling{.topo_nodes = std::move(topo_nodes), + .coupling = {.n_topo_nodes = topo_node_mapping.n_topo_nodes(), + .user_nodes_to_topo_nodes = std::move(user_node_topo_node_coup), + .user_links_to_topo_nodes = std::move(user_link_topo_node_coup)}}; }; // Use the topo_node_mapping to remap every part of comp_topo such that the output nodes are the topological nodes; @@ -172,8 +180,8 @@ inline ReducedTopology reduce_topology(ComponentTopology const& comp_topo, Compo using namespace detail; auto topo_node_mapping = create_map(comp_topo, comp_conn); - return ReducedTopology{.comp_topo = construct_reduced_topology(comp_topo, topo_node_mapping), - .topological_nodes = create_topological_nodes(comp_topo, comp_conn, topo_node_mapping)}; + return ReducedTopology{.reduced_comp_topo = construct_reduced_topology(comp_topo, topo_node_mapping), + .topo_node_coup = create_topological_nodes(comp_topo, comp_conn, topo_node_mapping)}; } } // namespace power_grid_model::supernodes diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp index 28bcf743b5..24c44d7f78 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp @@ -173,8 +173,9 @@ class Topology { TopologicalComponentToMathCoupling comp_coup_; void reset_topology() { - constexpr auto unknown_idx2d = Idx2D{.group = -1, .pos = -1}; - constexpr auto unknown_idx2d_branch3 = Idx2DBranch3{.group = -1, .pos = {-1, -1, -1}}; + constexpr auto unknown_idx2d = Idx2D{.group = disconnected, .pos = disconnected}; + constexpr auto unknown_idx2d_branch3 = + Idx2DBranch3{.group = disconnected, .pos = {disconnected, disconnected, disconnected}}; comp_coup_.node.resize(comp_topo_.n_node_total(), unknown_idx2d); comp_coup_.branch.resize(comp_topo_.branch_node_idx.size(), unknown_idx2d); diff --git a/tests/cpp_unit_tests/test_counting_iterator.cpp b/tests/cpp_unit_tests/test_counting_iterator.cpp index 34ad794d92..380e05ad3c 100644 --- a/tests/cpp_unit_tests/test_counting_iterator.cpp +++ b/tests/cpp_unit_tests/test_counting_iterator.cpp @@ -7,6 +7,8 @@ #include +#include + namespace power_grid_model { TEST_CASE("Counting Iterator") { CHECK(IdxRange{}.size() == 0); @@ -45,4 +47,17 @@ TEST_CASE("Enumerate") { CHECK(std::get<0>(*it) == 2); CHECK(std::get<1>(*it) == 30); } + +TEST_CASE("Pairwise") { + IdxVector vec{10, 20, 30}; + auto paired = pairwise(vec); + auto it = paired.begin(); + CHECK(std::get<0>(*it) == 10); + CHECK(std::get<1>(*it) == 20); + ++it; + CHECK(std::get<0>(*it) == 20); + CHECK(std::get<1>(*it) == 30); + + CHECK(std::ranges::empty(pairwise(IdxVector{}))); +} } // namespace power_grid_model diff --git a/tests/cpp_unit_tests/test_supernodes.cpp b/tests/cpp_unit_tests/test_supernodes.cpp index b7a94fad27..fab234be37 100644 --- a/tests/cpp_unit_tests/test_supernodes.cpp +++ b/tests/cpp_unit_tests/test_supernodes.cpp @@ -12,6 +12,8 @@ #include #include +#include +#include namespace { @@ -105,7 +107,50 @@ TEST_CASE("Test Supernodes") { } } SUBCASE("create_topological_nodes") { - // TODO: Add test implementation + SUBCASE("Multiple connected components map to different topological nodes") { + ComponentTopology const comp_topo{ + .n_node = 6, + .link_node_idx = {{0, 1}, {2, 4}, {3, 5}}, + }; + ComponentConnections const comp_conn{ + .link_connected = {{1, 1}, {1, 1}, {1, 1}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + auto const topo_nodes = detail::create_topological_nodes(comp_topo, comp_conn, topo_node_mapping); + REQUIRE(std::ssize(topo_nodes.topo_nodes) == topo_node_mapping.n_topo_nodes()); + CHECK(std::ranges::all_of(topo_nodes.topo_nodes, + [](TopologicalNode const& node) { return node.is_supernode(); })); + CHECK(topo_nodes.topo_nodes[0].user_nodes == IdxVector{0, 1}); + CHECK(topo_nodes.topo_nodes[1].user_nodes == IdxVector{2, 4}); + CHECK(topo_nodes.topo_nodes[2].user_nodes == IdxVector{3, 5}); + CHECK(topo_nodes.topo_nodes[0].user_links == std::vector{{0, 1}}); + CHECK(topo_nodes.topo_nodes[1].user_links == std::vector{{2, 4}}); + CHECK(topo_nodes.topo_nodes[2].user_links == std::vector{{3, 5}}); + } + SUBCASE("Disconnected link => not included in user links") { + ComponentTopology const comp_topo{ + .n_node = 6, + .link_node_idx = {{0, 1}, {2, 4}, {3, 5}}, + }; + ComponentConnections const comp_conn{ + .link_connected = {{1, 0}, {1, 1}, {0, 0}}, + }; + auto const topo_node_mapping = detail::create_map(comp_topo, comp_conn); + auto const topo_nodes = detail::create_topological_nodes(comp_topo, comp_conn, topo_node_mapping); + REQUIRE(std::ssize(topo_nodes.topo_nodes) == topo_node_mapping.n_topo_nodes()); + CHECK(std::ranges::equal(topo_nodes.topo_nodes | std::views::transform([](TopologicalNode const& node) { + return node.is_supernode(); + }), + std::array{false, false, true, false, false})); + CHECK(std::ranges::equal( + topo_nodes.topo_nodes | + std::views::transform([](TopologicalNode const& node) -> auto& { return node.user_nodes; }), + std::vector{{0}, {1}, {2, 4}, {3}, {5}})); + CHECK(std::ranges::equal( + topo_nodes.topo_nodes | + std::views::transform([](TopologicalNode const& node) -> auto& { return node.user_links; }), + std::vector>{{}, {}, {{2, 4}}, {}, {}})); + } } SUBCASE("construct_reduced_topology") { // TODO: Add test implementation From ec4b96d0610aa4f7b021c5df6252a376a3b35f88 Mon Sep 17 00:00:00 2001 From: Martijn Govers Date: Wed, 13 May 2026 16:04:20 +0200 Subject: [PATCH 8/8] complete implementation of construct_reduced_topology Signed-off-by: Martijn Govers --- .../calculation_parameters.hpp | 19 ++++++++ .../include/power_grid_model/supernodes.hpp | 47 +++++++++++++++---- .../include/power_grid_model/topology.hpp | 39 ++++++++------- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp index 4578e6a96f..fbfb216c26 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_parameters.hpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace power_grid_model { @@ -429,6 +430,24 @@ struct ComponentTopology { constexpr Idx n_node_total() const { return n_node + std::ssize(branch3_node_idx); } }; +struct ReducedComponentTopology { + Idx n_node{}; // num of topological nodes (with reduced links), plus internal nodes for 3-way branches + std::vector branch_node_idx; + std::vector branch3_node_idx; + IdxVector shunt_node_idx; + IdxVector source_node_idx; + IdxVector load_gen_node_idx; + std::span load_gen_type; + IdxVector voltage_sensor_node_idx; + std::span power_sensor_object_idx; // the index is relative to branch, source, shunt or load_gen + std::span power_sensor_terminal_type; + std::span current_sensor_object_idx; // the index is relative to branch + std::span current_sensor_terminal_type; + std::span regulator_type; + std::span regulated_object_idx; // the index is relative to branch or branch3 + std::span regulated_object_type; +}; + // connection property using BranchConnected = std::array; using Branch3Connected = std::array; diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp index c3aa919913..1f614db5a9 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -15,9 +15,13 @@ #include #include +#include #include +#include +#include #include #include +#include #include #include @@ -65,7 +69,7 @@ struct TopologicalNodesAndCoupling { }; struct ReducedTopology { - ComponentTopology reduced_comp_topo; + ReducedComponentTopology reduced_comp_topo; TopologicalNodesAndCoupling topo_node_coup; }; @@ -165,14 +169,39 @@ inline TopologicalNodesAndCoupling create_topological_nodes(ComponentTopology co // Use the topo_node_mapping to remap every part of comp_topo such that the output nodes are the topological nodes; // not the user nodes. This effectively removes all link-related stuff from the math topology. -inline ComponentTopology construct_reduced_topology(ComponentTopology const& comp_topo, - TopologicalNodeMapping const& /*topo_node_mapping*/) { - // TODO(mgovers): do we really require a full deep-copy of comp_topo here? - ComponentTopology comp_topo_reduced{comp_topo}; // TODO(mgovers): replace with real implementation - - // TODO(marcvanraalte): the implementation goes here - - return comp_topo_reduced; +inline ReducedComponentTopology construct_reduced_topology(ComponentTopology const& comp_topo, + TopologicalNodeMapping const& topo_node_mapping) { + auto remap_nodes = [&topo_node_mapping](std::vector const& user_nodes) { + return user_nodes | std::views::transform([&mapping = topo_node_mapping.mapping()](IdxType const& user_node) { + if constexpr (std::same_as) { + return mapping[user_node]; + } else { + static_assert( + requires { std::tuple_size_v; }, + "IdxType must have a static size (e.g. std::array)"); + return [&](std::index_sequence) { + return IdxType{mapping[user_node[I]]...}; + }(std::make_index_sequence>{}); + } + }) | + std::ranges::to>(); + }; + + return ReducedComponentTopology{ + .n_node = topo_node_mapping.n_topo_nodes(), + .branch_node_idx = remap_nodes(comp_topo.branch_node_idx), + .branch3_node_idx = remap_nodes(comp_topo.branch3_node_idx), + .shunt_node_idx = remap_nodes(comp_topo.shunt_node_idx), + .source_node_idx = remap_nodes(comp_topo.source_node_idx), + .load_gen_node_idx = remap_nodes(comp_topo.load_gen_node_idx), + .load_gen_type = std::span{comp_topo.load_gen_type}, + .voltage_sensor_node_idx = remap_nodes(comp_topo.voltage_sensor_node_idx), + .power_sensor_object_idx = std::span{comp_topo.power_sensor_object_idx}, + .power_sensor_terminal_type = std::span{comp_topo.power_sensor_terminal_type}, + .current_sensor_object_idx = std::span{comp_topo.current_sensor_object_idx}, + .current_sensor_terminal_type = std::span{comp_topo.current_sensor_terminal_type}, + .regulated_object_idx = std::span{comp_topo.regulated_object_idx}, + }; } } // namespace detail diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp index 24c44d7f78..97d33ddc8b 100644 --- a/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/topology.hpp @@ -130,7 +130,7 @@ class Topology { predecessors_( boost::counting_iterator{0}, // Predecessors is initialized as 0, 1, 2, ..., n_node_total() - 1 boost::counting_iterator{(GraphIdx)comp_topo_.n_node_total()}), - node_status_(comp_topo_.n_node_total(), -1) {} + node_status_(comp_topo_.n_node_total(), not_processed) {} // build topology std::pair>, @@ -164,10 +164,9 @@ class Topology { DoubleVector phase_shift_; std::vector predecessors_; // node status - // -1, node not processed, assuming that node in the far end of a tree structure - // -2, node in cycles or between the source and cycles, reordering not yet happened - // >=0, temporary internal bus number for minimum degree reordering - std::vector node_status_; + static constexpr Idx not_processed{-1}; // assume node in the far end of a tree structure + static constexpr Idx in_cycle_or_between_source_and_cycle{-2}; // reordering not yet happened + std::vector node_status_; // >=0, temporary internal bus number for minimum degree reordering // output std::vector math_topology_; TopologicalComponentToMathCoupling comp_coup_; @@ -247,7 +246,7 @@ class Topology { continue; } // if the source node is already part of a graph - if (comp_coup_.node[source_node].group != -1) { + if (comp_coup_.node[source_node].group != disconnected) { // skip the source continue; } @@ -312,21 +311,21 @@ class Topology { // loop back from source in the predecessor tree // stop if it is already marked as in cycle - while (node_status_[node_in_cycle] != -2) { + while (node_status_[node_in_cycle] != in_cycle_or_between_source_and_cycle) { // assign cycle status and go to predecessor - node_status_[node_in_cycle] = -2; + node_status_[node_in_cycle] = in_cycle_or_between_source_and_cycle; node_in_cycle = predecessors_[node_in_cycle]; } } // copy all the far-end non-cyclic node, in reverse order std::ranges::copy_if(std::views::reverse(dfs_node_copy), std::back_inserter(dfs_node), - [this](Idx x) { return node_status_[x] == -1; }); + [this](Idx x) { return node_status_[x] == not_processed; }); // copy all cyclic node std::vector cyclic_node; std::ranges::copy_if(dfs_node_copy, std::back_inserter(cyclic_node), - [this](Idx x) { return node_status_[x] == -2; }); + [this](Idx x) { return node_status_[x] == in_cycle_or_between_source_and_cycle; }); // reorder does not make sense if number of cyclic nodes in a sub graph is smaller than 4 if (cyclic_node.size() < 4) { @@ -370,7 +369,7 @@ class Topology { void couple_branch() { auto const get_group_pos_if = []([[maybe_unused]] Idx math_group, IntS status, Idx2D const& math_idx) { if (status == 0) { - return Idx{-1}; + return disconnected; } assert(math_group == math_idx.group); return math_idx.pos; @@ -387,16 +386,16 @@ class Topology { Idx2D const i_math = comp_coup_.node[i]; Idx2D const j_math = comp_coup_.node[j]; Idx const math_group = [&]() { - if (i_status != 0 && i_math.group != -1) { + if (i_status != 0 && i_math.group != disconnected) { return i_math.group; } - if (j_status != 0 && j_math.group != -1) { + if (j_status != 0 && j_math.group != disconnected) { return j_math.group; } - return Idx{-1}; + return disconnected; }(); // skip if no math model connected - if (math_group == -1) { + if (math_group == disconnected) { continue; } assert(i_status || j_status); @@ -422,18 +421,18 @@ class Topology { }; // internal node number as j Idx const math_group = [&]() { - Idx group = -1; + Idx group = disconnected; // loop 3 way as indices n for (size_t n = 0; n != 3; ++n) { - if (i_status[n] != 0 && i_math[n].group != -1) { + if (i_status[n] != 0 && i_math[n].group != disconnected) { group = i_math[n].group; } } return group; }(); // skip if no math model connected - if (math_group == -1) { - assert(j_math.group == -1); + if (math_group == disconnected) { + assert(j_math.group == disconnected); continue; } assert(i_status[0] || i_status[1] || i_status[2]); @@ -581,7 +580,7 @@ class Topology { // assign load type for (auto const& [idx_math, load_gen_type] : std::views::zip(std::as_const(comp_coup_.load_gen), std::as_const(comp_topo_.load_gen_type))) { - if (idx_math.group == -1) { + if (idx_math.group == disconnected) { continue; } math_topology_[idx_math.group].load_gen_type[idx_math.pos] = load_gen_type;