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 bc26c488c..fbfb216c2 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 { @@ -412,6 +413,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 +427,25 @@ 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); } +}; + +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 @@ -436,6 +456,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/calculation_preparation.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/calculation_preparation.hpp index 159490735..ed6b6b7d0 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/common/counting_iterator.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/counting_iterator.hpp index ed6e8c692..5db7cbbd3 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/common/grouped_index_vector.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/common/grouped_index_vector.hpp index 2fc27a4e0..52eb08a0f 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 000000000..1f614db5a --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/supernodes.hpp @@ -0,0 +1,216 @@ +// 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace power_grid_model::supernodes { +class TopologicalNodeMapping { + public: + 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 std::ssize(mapping_); } + + private: + IdxVector mapping_; + Idx n_topo_nodes_{}; + + 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 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 { + ReducedComponentTopology reduced_comp_topo; + TopologicalNodesAndCoupling topo_node_coup; +}; + +namespace detail { + +inline TopologicalNodeMapping 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; + + IdxVector vertices = IdxRange{n_nodes} | std::ranges::to(); + + 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 = 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, internal_edges.cbegin(), internal_edges.cend(), n_nodes}; + + Idx const num_connected_components = boost::connected_components(global_graph_, vertices.data()); + return TopologicalNodeMapping{num_connected_components, std::move(vertices)}; +} + +// Union-find algorithm: +// 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 +// 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: +// [0, 1, 1, 1, 4, 1] +// Mapping (explanation): +// [N0, SN1, SN1, SN1, N4, SN1] +// [TN0, TN1, TN1, TN1, TN2, TN1] +inline TopologicalNodeMapping create_map(ComponentTopology const& comp_topo, ComponentConnections const& comp_conn) { + return find_link_connected_components(comp_topo.n_node, comp_topo.link_node_idx, comp_conn.link_connected); +}; + +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; +// not the user nodes. This effectively removes all link-related stuff from the math topology. +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 + +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{.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 824f5a964..97d33ddc8 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; @@ -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,17 +164,17 @@ 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_; 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); @@ -246,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; } @@ -311,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) { @@ -369,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; @@ -386,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); @@ -421,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]); @@ -580,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; @@ -699,4 +699,4 @@ class Topology { } }; -} // namespace power_grid_model +} // namespace power_grid_model::topology diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 34cc608f4..4febe490a 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_counting_iterator.cpp b/tests/cpp_unit_tests/test_counting_iterator.cpp index 34ad794d9..380e05ad3 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 new file mode 100644 index 000000000..fab234be3 --- /dev/null +++ b/tests/cpp_unit_tests/test_supernodes.cpp @@ -0,0 +1,162 @@ +// 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 +#include +#include + +namespace { + +using namespace power_grid_model; +using namespace power_grid_model::supernodes; +namespace detail = power_grid_model::supernodes::detail; + +TEST_CASE("Test Supernodes") { + SUBCASE("create_map") { + 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(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(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{ + .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(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(topo_node_mapping.mapping() == IdxVector{0, 0, 1}); + } + 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(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(topo_node_mapping.mapping() == 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(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(topo_node_mapping.mapping() == IdxVector{0, 1, 1, 1, 2, 1}); + } + 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(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(topo_node_mapping.mapping() == IdxVector{0, 0, 1, 2, 1, 2}); + } + } + SUBCASE("create_topological_nodes") { + 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 + } + SUBCASE("reduce_topology") { + // TODO: Add test implementation + } +} +} // namespace diff --git a/tests/cpp_unit_tests/test_topology.cpp b/tests/cpp_unit_tests/test_topology.cpp index 40059adad..5223a06bd 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