diff --git a/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp b/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp new file mode 100644 index 000000000..41c9622fd --- /dev/null +++ b/power_grid_model_c/power_grid_model/include/power_grid_model/sparse_ordering.hpp @@ -0,0 +1,221 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#pragma once + +#include "common/common.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace power_grid_model { + +namespace detail { +class DegreeLookup { + public: + void set(Idx u, Idx degree) { + if (auto degree_it = vertex_to_degree.find(u); degree_it != vertex_to_degree.end()) { + remove_degree(u, degree_it->second); + degree_it->second = degree; + } else { + vertex_to_degree.try_emplace(u, degree); + } + degrees_to_vertex[degree].insert(u); + } + + void erase(Idx u) { + auto vertex_it = vertex_to_degree.find(u); + if (vertex_it == vertex_to_degree.end()) { + return; + } + Idx const degree = vertex_it->second; + vertex_to_degree.erase(vertex_it); + remove_degree(u, degree); + } + + friend auto min_element(DegreeLookup const& dgd); + + private: + void remove_degree(Idx u, Idx degree) { + if (auto degree_it = degrees_to_vertex.find(degree); degree_it != degrees_to_vertex.end()) { + degree_it->second.erase(u); + if (degree_it->second.empty()) { + degrees_to_vertex.erase(degree_it); + } + } + } + + std::map vertex_to_degree; + std::map> degrees_to_vertex; +}; + +inline auto min_element(DegreeLookup const& dgd) { + Idx const vertex = *dgd.degrees_to_vertex.begin()->second.begin(); + return dgd.vertex_to_degree.find(vertex); +} + +inline void remove_element_degree(Idx u, DegreeLookup& dgd) { dgd.erase(u); } + +inline void set_element_degree(Idx u, Idx degree, DegreeLookup& dgd) { dgd.set(u, degree); } + +inline Idx num_adjacent(Idx const u, std::map const& d) { + if (auto it = d.find(u); it != d.end()) { + return static_cast(it->second.size()); + } + return 0; +} + +inline IdxVector const& adj(Idx const u, std::map const& d) { return d.at(u); } + +inline std::vector> comp_size_degrees_graph(std::map const& d) { + DegreeLookup dd; + IdxVector v; + + for (auto const& [k, adjacent] : d) { + v.push_back(k); + set_element_degree(k, static_cast(adjacent.size()), dd); + } + + return {{d.size(), dd}}; +} + +inline std::map make_clique(IdxVector& l) { + std::map d; + + for (Idx i = 0; i < static_cast(l.size()); i++) { + IdxVector sl(l.size() - 1); + std::copy(l.begin(), l.begin() + i, sl.begin()); + std::copy(l.begin() + i + 1, l.end(), sl.begin() + i); + d[l[i]] = std::move(sl); + } + + return d; +} + +inline std::vector> check_indistguishable(Idx const u, + std::map const& d) { + IdxVector rl; + + auto l = adj(u, d); + auto lu = l; + lu.push_back(u); + std::ranges::sort(lu); + + for (auto const& v : l) { + auto lv = adj(v, d); + lv.push_back(v); + std::ranges::sort(lv); + if (lu == lv) { + rl.push_back(v); + } + } + + return {{l, rl}}; +} + +inline bool in_graph(std::pair const& e, std::map const& d) { + if (auto edges_it = d.find(e.first); + edges_it != d.cend() && std::ranges::find(edges_it->second, e.second) != edges_it->second.cend()) { + return true; + } + return false; +} + +inline IdxVector remove_vertices_update_degrees(Idx const u, std::map& d, DegreeLookup& dgd, + std::vector>& fills) { + std::vector> nbsrl = check_indistguishable(u, d); + auto& [nbs, rl] = nbsrl[0]; + IdxVector alpha = rl; + std::map dd; + + rl.push_back(u); + + for (auto uu : rl) { + if (uu != u) { + std::erase(nbs, uu); + } + + remove_element_degree(uu, dgd); + IdxVector el; + for (auto e : d[uu]) { + auto& adjacents = d[e]; + std::erase(adjacents, uu); + if (adjacents.empty()) { + el.push_back(e); + } + } + + el.push_back(uu); + + for (auto const& it : el) { + d.erase(it); + } + } + + dd = make_clique(nbs); + + for (auto const& [k, adjacent] : dd) { + auto it = d.find(k); + for (Idx const e : adjacent) { + if (!in_graph(std::make_pair(k, e), d)) { + if (it == d.end()) { + std::tie(it, std::ignore) = d.try_emplace(k); + } + it->second.push_back(e); + d[e].push_back(k); + fills.emplace_back(k, e); + } + } + } + + for (auto const& e : nbs) { + set_element_degree(e, num_adjacent(e, d), dgd); + } + + return alpha; +} +} // namespace detail + +inline std::pair>> minimum_degree_ordering(std::map d) { + // make symmetric + for (auto const& [k, adjacent] : d) { + for (auto e : adjacent) { + d[e].push_back(k); + } + } + for (auto& [k, adjacent] : d) { + std::set const unique_sorted_adjacent{adjacent.begin(), adjacent.end()}; + adjacent = IdxVector{unique_sorted_adjacent.begin(), unique_sorted_adjacent.end()}; + } + + auto data = detail::comp_size_degrees_graph(d); + auto& [n, dgd] = data[0]; + + IdxVector alpha; + std::vector> fills; + + for (Idx k = 0; k < n; ++k) { + Idx const u = get<0>(*detail::min_element(dgd)); + alpha.push_back(u); + if (d.size() == 2) { + assert(d.begin()->second.size() == 1); + + Idx const from = d.begin()->first; + Idx const to = d.begin()->second[0]; + alpha.push_back(alpha.back() == from ? to : from); + return {alpha, fills}; + } + std::ranges::copy(detail::remove_vertices_update_degrees(u, d, dgd, fills), std::back_inserter(alpha)); + if (d.empty()) { + return {alpha, fills}; + } + } + return {alpha, fills}; +} +} // namespace power_grid_model 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 6bcb7ea5a..f5d019796 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 @@ -9,12 +9,12 @@ #include "common/enum.hpp" #include "common/exception.hpp" #include "index_mapping.hpp" +#include "sparse_ordering.hpp" #include #include #include #include -#include // build topology of the grid // divide grid into several math models @@ -284,6 +284,7 @@ class Topology { // loop all back edges assign all nodes before the back edges as inside cycle for (auto const& back_edge : back_edges) { GraphIdx node_in_cycle = back_edge.first; + // loop back from source in the predecessor tree // stop if it is already marked as in cycle while (node_status_[node_in_cycle] != -2) { @@ -300,105 +301,43 @@ class Topology { std::vector cyclic_node; std::copy_if(dfs_node_copy.cbegin(), dfs_node_copy.cend(), std::back_inserter(cyclic_node), [this](Idx x) { return node_status_[x] == -2; }); - GraphIdx const n_cycle_node = cyclic_node.size(); + // reorder does not make sense if number of cyclic nodes in a sub graph is smaller than 4 - if (n_cycle_node < 4) { + if (cyclic_node.size() < 4) { std::copy(cyclic_node.crbegin(), cyclic_node.crend(), std::back_inserter(dfs_node)); return fill_in; } - // assign temporary bus number as increasing from 0, 1, 2, ..., n_cycle_node - 1 - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - node_status_[cyclic_node[i]] = static_cast(i); + std::map> unique_nearest_neighbours; + for (Idx const node_idx : cyclic_node) { + auto predecessor = static_cast(predecessors_[node_idx]); + if (predecessor != node_idx) { + unique_nearest_neighbours[node_idx] = {predecessor}; + } } - // build graph lambda - auto const build_graph = [&](ReorderGraph& g) { - // add edges - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - // loop all edges of vertex i - auto const global_i = static_cast(cyclic_node[i]); - BGL_FORALL_ADJ(global_i, global_j, global_graph_, GlobalGraph) { - // skip if j is not part of cyclic sub graph - if (node_status_[global_j] == -1) { - continue; - } - auto const j = static_cast(node_status_[global_j]); - if (!boost::edge(i, j, g).second) { - boost::add_edge(i, j, g); - } - } + for (auto const& [from_node, to_node] : back_edges) { + auto const from{static_cast(from_node)}; + auto const to{static_cast(to_node)}; + if (!detail::in_graph(std::pair{from, to}, unique_nearest_neighbours)) { + unique_nearest_neighbours[from].push_back(to); } - }; - ReorderGraph meshed_graph{n_cycle_node}; - build_graph(meshed_graph); - // start minimum degree ordering - std::vector> perm(n_cycle_node); - std::vector> inverse_perm(n_cycle_node); - std::vector> degree(n_cycle_node); - std::vector> supernode_sizes(n_cycle_node, 1); - boost::vec_adj_list_vertex_id_map> const id{}; - int const delta = 0; - boost::minimum_degree_ordering(meshed_graph, boost::make_iterator_property_map(degree.begin(), id), - boost::make_iterator_property_map(inverse_perm.begin(), id), - boost::make_iterator_property_map(perm.begin(), id), - boost::make_iterator_property_map(supernode_sizes.begin(), id), delta, id); - // re-order cyclic node - std::vector const cyclic_node_copy{cyclic_node}; - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - cyclic_node[i] = cyclic_node_copy[perm[i]]; } - // copy back to dfs node - std::copy(cyclic_node.cbegin(), cyclic_node.cend(), std::back_inserter(dfs_node)); - // analyze and record fill-ins - // re-assign temporary bus number as increasing from 0, 1, 2, ..., n_cycle_node - 1 - for (GraphIdx i = 0; i != n_cycle_node; ++i) { - node_status_[cyclic_node[i]] = static_cast(i); + auto [reordered, fills] = minimum_degree_ordering(std::move(unique_nearest_neighbours)); + + const auto n_non_cyclic_nodes = static_cast(dfs_node.size()); + std::map permuted_node_indices; + for (Idx idx = 0; idx < static_cast(reordered.size()); ++idx) { + permuted_node_indices[reordered[idx]] = n_non_cyclic_nodes + idx; } - // re-build graph with reordered cyclic node - meshed_graph.clear(); - meshed_graph = ReorderGraph{n_cycle_node}; - build_graph(meshed_graph); - // begin to remove vertices from graph, create fill-ins - BGL_FORALL_VERTICES(i, meshed_graph, ReorderGraph) { - // double loop to loop all pairs of adjacent vertices - BGL_FORALL_ADJ(i, j1, meshed_graph, ReorderGraph) { - // skip for already removed vertices - if (j1 < i) { - continue; - } - BGL_FORALL_ADJ(i, j2, meshed_graph, ReorderGraph) { - // no self edges - assert(i != j1); - assert(i != j2); - // skip for already removed vertices - if (j2 < i) { - continue; - } - // only keep pair with j1 < j2 - if (j1 >= j2) { - continue; - } - // if edge j1 -> j2 does not already exists - // it is a fill-in - if (!boost::edge(j1, j2, meshed_graph).second) { - // anti edge should also not exist - assert(!boost::edge(j2, j1, meshed_graph).second); - // add both edges to the graph - boost::add_edge(j1, j2, meshed_graph); - boost::add_edge(j2, j1, meshed_graph); - // add to fill-in - fill_in.push_back({static_cast(j1), static_cast(j2)}); - } - } - } + + std::ranges::copy(reordered, std::back_inserter(dfs_node)); + for (auto [from, to] : fills) { + auto from_reordered = permuted_node_indices[from]; + auto to_reordered = permuted_node_indices[to]; + fill_in.push_back({from_reordered, to_reordered}); } - // offset fill-in indices by n_node - n_cycle_node - auto const offset = static_cast(dfs_node.size() - n_cycle_node); - std::for_each(fill_in.begin(), fill_in.end(), [offset](BranchIdx& b) { - b[0] += offset; - b[1] += offset; - }); + return fill_in; } @@ -418,14 +357,13 @@ class Topology { Idx2D const i_math = comp_coup_.node[i]; Idx2D const j_math = comp_coup_.node[j]; Idx const math_group = [&]() { - Idx group = -1; if (i_status != 0 && i_math.group != -1) { - group = i_math.group; + return i_math.group; } if (j_status != 0 && j_math.group != -1) { - group = j_math.group; + return j_math.group; } - return group; + return Idx{-1}; }(); // skip if no math model connected if (math_group == -1) { @@ -687,6 +625,6 @@ class Topology { {comp_topo_.power_sensor_object_idx, comp_coup_.node}, comp_coup_.power_sensor, [this](Idx i) { return comp_topo_.power_sensor_terminal_type[i] == MeasuredTerminalType::node; }); } -}; // namespace power_grid_model +}; } // namespace power_grid_model diff --git a/tests/benchmark_cpp/fictional_grid_generator.hpp b/tests/benchmark_cpp/fictional_grid_generator.hpp index 25da2beb7..60393a010 100644 --- a/tests/benchmark_cpp/fictional_grid_generator.hpp +++ b/tests/benchmark_cpp/fictional_grid_generator.hpp @@ -116,7 +116,9 @@ class FictionalGridGenerator { option_.n_mv_feeder = option_.n_lv_grid / option_.n_node_per_mv_feeder + 1; } total_mv_connection = option_.n_mv_feeder * option_.n_node_per_mv_feeder; - option_.ratio_lv_grid = static_cast(option_.n_lv_grid) / static_cast(total_mv_connection); + option_.ratio_lv_grid = total_mv_connection > 0 + ? static_cast(option_.n_lv_grid) / static_cast(total_mv_connection) + : 1.0; // each mv feeder 10 MVA, each transformer 60 MVA, scaled up by 10% option_.n_parallel_hv_mv_transformer = static_cast(static_cast(option.n_mv_feeder) * 10.0 * 1.1 / 60.0) + 1; diff --git a/tests/cpp_unit_tests/CMakeLists.txt b/tests/cpp_unit_tests/CMakeLists.txt index 836fe4f5b..f75cad3c4 100644 --- a/tests/cpp_unit_tests/CMakeLists.txt +++ b/tests/cpp_unit_tests/CMakeLists.txt @@ -22,6 +22,7 @@ set(PROJECT_SOURCES "test_measured_values.cpp" "test_observability.cpp" "test_topology.cpp" + "test_sparse_ordering.cpp" "test_grouped_index_vector.cpp" "test_container.cpp" "test_index_mapping.cpp" diff --git a/tests/cpp_unit_tests/test_sparse_ordering.cpp b/tests/cpp_unit_tests/test_sparse_ordering.cpp new file mode 100644 index 000000000..99c15fd91 --- /dev/null +++ b/tests/cpp_unit_tests/test_sparse_ordering.cpp @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Contributors to the Power Grid Model project +// +// SPDX-License-Identifier: MPL-2.0 + +#include + +#include + +#include +#include +#include + +namespace { +using power_grid_model::Idx; +} // namespace + +TEST_CASE("Test sparse ordering") { + SUBCASE("minimum_degree_ordering") { + std::map> graph{{0, {3, 5}}, {1, {4, 5, 8}}, {2, {4, 5, 6}}, {3, {6, 7}}, + {4, {6, 8}}, {6, {7, 8, 9}}, {7, {8, 9}}, {8, {9}}}; + + auto const start = std::chrono::high_resolution_clock::now(); + auto const [alpha, fills] = power_grid_model::minimum_degree_ordering(std::move(graph)); + auto const stop = std::chrono::high_resolution_clock::now(); + + auto const duration = duration_cast(stop - start); + std::cout << "Time taken by function: " << duration.count() << " microseconds\n"; + + CHECK(alpha == std::vector{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + CHECK(fills == std::vector>{{3, 5}, {4, 5}, {5, 8}, {5, 6}, {5, 7}}); + } +} diff --git a/tests/cpp_unit_tests/test_topology.cpp b/tests/cpp_unit_tests/test_topology.cpp index 892cf4f32..6a5e8e3a6 100644 --- a/tests/cpp_unit_tests/test_topology.cpp +++ b/tests/cpp_unit_tests/test_topology.cpp @@ -6,8 +6,6 @@ #include -#include - /* * [0] = Node / Bus * --0--> = Branch (from --id--> to) @@ -178,10 +176,10 @@ TEST_CASE("Test topology") { // result TopologicalComponentToMathCoupling comp_coup_ref{}; comp_coup_ref.node = { // 0 1 2 3 - {0, 4}, // Topological node 0 has become node 4 in mathematical model (group) 0 + {0, 1}, // Topological node 0 has become node 4 in mathematical model (group) 0 {0, 2}, {0, 0}, - {0, 1}, + {0, 4}, // 4 5 6 {1, 2}, // Topological node 4 has become node 2 in mathematical model (group) 1 {1, 3}, @@ -218,9 +216,9 @@ TEST_CASE("Test topology") { {-1, {-1, -1, -1}}, // b1 {1, {2, 3, 4}}, // b2 }; - comp_coup_ref.load_gen = {{0, 1}, {-1, -1}, {1, 0}, {0, 0}}; + comp_coup_ref.load_gen = {{0, 0}, {-1, -1}, {1, 0}, {0, 1}}; comp_coup_ref.shunt = {{0, 0}, {1, 0}, {-1, -1}}; - comp_coup_ref.voltage_sensor = {{0, 0}, {0, 3}, {0, 1}, {0, 2}, {1, 0}, {-1, -1}}; + comp_coup_ref.voltage_sensor = {{0, 0}, {0, 2}, {0, 1}, {0, 3}, {1, 0}, {-1, -1}}; comp_coup_ref.power_sensor = { {0, 0}, // 0 branch_from {1, 0}, // 1 source @@ -244,51 +242,50 @@ TEST_CASE("Test topology") { // Sub graph / math model 0 MathModelTopology math0; - math0.slack_bus = 4; - math0.sources_per_bus = {from_sparse, {0, 0, 0, 0, 0, 1}}; - math0.branch_bus_idx = {{4, 2}, {4, 1}, {1, -1}, {-1, 0}, {2, 3}, {1, 3}, {0, 3}}; - math0.phase_shift = {0.0, -1.0, 0.0, 0.0, 0.0}; - math0.load_gens_per_bus = {from_sparse, {0, 0, 0, 1, 1, 2}}; - math0.load_gen_type = {LoadGenType::const_y, LoadGenType::const_pq}; - math0.shunts_per_bus = {from_sparse, {0, 0, 1, 1, 1, 1}}; - math0.voltage_sensors_per_bus = {from_sparse, {0, 2, 3, 4, 4, 4}}; - math0.power_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 0, 0}}; - math0.power_sensors_per_source = {from_sparse, {0, 0}}; - math0.power_sensors_per_shunt = {from_sparse, {0, 0}}; - math0.power_sensors_per_load_gen = {from_sparse, {0, 1, 1}}; - math0.power_sensors_per_branch_from = {from_sparse, {0, 0, 2, 2, 2, 3, 4, 5}}; + math0.slack_bus = 1; + math0.sources_per_bus = {from_dense, {1}, 5}; + math0.branch_bus_idx = {{1, 2}, {1, 4}, {4, -1}, {-1, 0}, {2, 3}, {4, 3}, {0, 3}}; + math0.phase_shift = {0.0, 0.0, 0.0, 0.0, -1.0}; + math0.load_gens_per_bus = {from_dense, {1, 2}, 5}; + math0.load_gen_type = {LoadGenType::const_pq, LoadGenType::const_y}; + math0.shunts_per_bus = {from_dense, {4}, 5}; + math0.voltage_sensors_per_bus = {from_dense, {0, 0, 2, 4}, 5}; + math0.power_sensors_per_bus = {from_dense, {}, 5}; + math0.power_sensors_per_source = {from_dense, {}, 1}; + math0.power_sensors_per_shunt = {from_dense, {}, 1}; + math0.power_sensors_per_load_gen = {from_dense, {1}, 2}; + math0.power_sensors_per_branch_from = {from_dense, {1, 1, 4, 5, 6}, 7}; // 7 branches, 3 branch-to power sensors // sensor 0 is connected to branch 0 // sensor 1 and 2 are connected to branch 1 - math0.power_sensors_per_branch_to = {from_sparse, {0, 1, 3, 3, 3, 3, 3, 3}}; - math0.fill_in = {{3, 4}}; + math0.power_sensors_per_branch_to = {from_dense, {0, 1, 1}, 7}; + math0.fill_in = {{2, 4}}; // Sub graph / math model 1 MathModelTopology math1; math1.slack_bus = 3; - math1.sources_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.branch_bus_idx = { - {3, 2}, {2, 3}, {-1, 1}, {0, 1}, {3, 1}, - }; + math1.sources_per_bus = {from_dense, {3}, 4}; + math1.branch_bus_idx = {{3, 2}, {2, 3}, {-1, 1}, {0, 1}, {3, 1}}; math1.phase_shift = {0, 0, 0, 0}; - math1.load_gens_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; + math1.load_gens_per_bus = {from_dense, {3}, 4}; math1.load_gen_type = {LoadGenType::const_i}; - math1.shunts_per_bus = {from_sparse, {0, 1, 1, 1, 1}}; - math1.voltage_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.power_sensors_per_bus = {from_sparse, {0, 0, 0, 0, 1}}; - math1.power_sensors_per_source = {from_sparse, {0, 2}}; - math1.power_sensors_per_shunt = {from_sparse, {0, 2}}; - math1.power_sensors_per_load_gen = {from_sparse, {0, 2}}; - math1.power_sensors_per_branch_from = {from_sparse, {0, 0, 0, 0, 0, 0}}; - math1.power_sensors_per_branch_to = {from_sparse, {0, 0, 0, 0, 0, 0}}; + math1.shunts_per_bus = {from_dense, {0}, 4}; + math1.voltage_sensors_per_bus = {from_dense, {3}, 4}; + math1.power_sensors_per_bus = {from_dense, {3}, 4}; + math1.power_sensors_per_source = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_shunt = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_load_gen = {from_dense, {0, 0}, 1}; + math1.power_sensors_per_branch_from = {from_dense, {}, 5}; + math1.power_sensors_per_branch_to = {from_dense, {}, 5}; std::vector math_topology_ref = {math0, math1}; SUBCASE("Test topology result") { Topology topo{comp_topo, comp_conn}; - auto pair = topo.build_topology(); - auto const& math_topology = pair.first; - auto const& topo_comp_coup = *pair.second; + auto const [math_topology, topo_comp_coup_ptr] = topo.build_topology(); + + REQUIRE(topo_comp_coup_ptr != nullptr); + auto const& topo_comp_coup = *topo_comp_coup_ptr; CHECK(math_topology.size() == 2); // test component coupling @@ -324,41 +321,89 @@ TEST_CASE("Test topology") { } TEST_CASE("Test cycle reorder") { - // component topology - ComponentTopology comp_topo{}; - comp_topo.n_node = 7; - comp_topo.branch_node_idx = { - {0, 1}, // 0 - {1, 2}, // 1 - {2, 3}, // 2 - {3, 4}, // 3 - {4, 5}, // 4 - {0, 5}, // 5 - {1, 4}, // 6 - {6, 0}, // 7 - {6, 2}, // 8 - {5, 1}, // 9 - {3, 1}, // 10 - {6, 1}, // 11 - {2, 1}, // 12 - }; - comp_topo.source_node_idx = {0}; - // component connection - ComponentConnections comp_conn{}; - comp_conn.branch_connected = std::vector(13, {1, 1}); - comp_conn.branch_phase_shift = std::vector(13, 0.0); - comp_conn.source_connected = {1}; - // result - TopologicalComponentToMathCoupling comp_coup_ref{}; - comp_coup_ref.node = {{0, 3}, {0, 5}, {0, 4}, {0, 2}, {0, 6}, {0, 1}, {0, 0}}; - std::vector const fill_in_ref{{3, 4}, {3, 6}, {4, 6}}; + SUBCASE("9 nodes") { + // { + // 0: [3, 5], + // 1: [4, 5, 8], + // 2: [4, 5, 6], + // 3: [6, 7], + // 4: [6, 8], + // 6: [7, 8, 9], + // 7: [8, 9], + // 8: [9] + // } - Topology topo{comp_topo, comp_conn}; - auto pair = topo.build_topology(); - auto const& topo_comp_coup = *pair.second; - auto const& math_topo = *pair.first[0]; - CHECK(topo_comp_coup.node == comp_coup_ref.node); - CHECK(math_topo.fill_in == fill_in_ref); + // component topology + ComponentTopology comp_topo{}; + comp_topo.n_node = 10; + comp_topo.branch_node_idx = { + {0, 3}, {0, 5}, {1, 4}, {1, 5}, {1, 8}, {2, 4}, {2, 5}, {2, 6}, {3, 6}, + {3, 7}, {4, 6}, {4, 8}, {6, 7}, {6, 8}, {6, 9}, {7, 8}, {7, 9}, {8, 9}, + }; + comp_topo.source_node_idx = {0}; + // component connection + ComponentConnections comp_conn{}; + comp_conn.branch_connected = std::vector(18, {1, 1}); + comp_conn.branch_phase_shift = std::vector(18, 0.0); + comp_conn.source_connected = {1}; + // result + TopologicalComponentToMathCoupling comp_coup_ref{}; + comp_coup_ref.node = {{0, 0}, {0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}, {0, 7}, {0, 8}, {0, 9}}; + std::vector const fill_in_ref{{3, 5}, {4, 5}, {5, 8}, {5, 6}, {5, 7}}; + + Topology topo{comp_topo, comp_conn}; + auto pair = topo.build_topology(); + auto const& topo_comp_coup = *pair.second; + auto const& math_topo = *pair.first[0]; + CHECK(topo_comp_coup.node == comp_coup_ref.node); + CHECK(math_topo.fill_in == fill_in_ref); + } + + SUBCASE("7 nodes") { + // { + // 0: [1, 5, 6], + // 1: [2, 4, 5, 3, 6], + // 2: [3, 6] + // 3: [4], + // 4: [5], + // } + + // component topology + ComponentTopology comp_topo{}; + comp_topo.n_node = 7; + comp_topo.branch_node_idx = { + {0, 1}, // 0 + {1, 2}, // 1 + {2, 3}, // 2 + {3, 4}, // 3 + {4, 5}, // 4 + {0, 5}, // 5 + {1, 4}, // 6 + {6, 0}, // 7 + {6, 2}, // 8 + {5, 1}, // 9 + {3, 1}, // 10 + {6, 1}, // 11 + {2, 1}, // 12 + }; + comp_topo.source_node_idx = {0}; + // component connection + ComponentConnections comp_conn{}; + comp_conn.branch_connected = std::vector(13, {1, 1}); + comp_conn.branch_phase_shift = std::vector(13, 0.0); + comp_conn.source_connected = {1}; + // result + TopologicalComponentToMathCoupling comp_coup_ref{}; + comp_coup_ref.node = {{0, 0}, {0, 3}, {0, 1}, {0, 2}, {0, 4}, {0, 5}, {0, 6}}; + std::vector const fill_in_ref{{5, 6}, {2, 6}, {4, 6}}; + + Topology topo{comp_topo, comp_conn}; + auto pair = topo.build_topology(); + auto const& topo_comp_coup = *pair.second; + auto const& math_topo = *pair.first[0]; + CHECK(topo_comp_coup.node == comp_coup_ref.node); + CHECK(math_topo.fill_in == fill_in_ref); + } } } // namespace power_grid_model \ No newline at end of file diff --git a/tests/cpp_validation_tests/test_validation.cpp b/tests/cpp_validation_tests/test_validation.cpp index a45acbf48..98afca0a0 100644 --- a/tests/cpp_validation_tests/test_validation.cpp +++ b/tests/cpp_validation_tests/test_validation.cpp @@ -617,7 +617,9 @@ TEST_CASE("Validation test single") { try { validate_single_case(param); } catch (std::exception& e) { - auto const msg = std::string("Unexpected exception with message: ") + e.what(); + using namespace std::string_literals; + + auto const msg = "Unexpected exception with message: "s + e.what(); FAIL_CHECK(msg); } } @@ -631,7 +633,9 @@ TEST_CASE("Validation test batch") { try { validate_batch_case(param); } catch (std::exception& e) { - auto const msg = std::string("Unexpected exception with message: ") + e.what(); + using namespace std::string_literals; + + auto const msg = "Unexpected exception with message: "s + e.what(); FAIL_CHECK(msg); } }