From dc091235573be804ae52968d4da974759f4130e0 Mon Sep 17 00:00:00 2001 From: Arnaud Becheler <8360330+Becheler@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:53:03 +0200 Subject: [PATCH] test: filling gaps in MAS and SW --- test/mas_sw_maxflow_oracle.hpp | 146 +++++++++++++++++++++++++++ test/mas_test.cpp | 178 +++++++++++++++++++++++++++++++-- test/stoer_wagner_test.cpp | 24 +++-- 3 files changed, 333 insertions(+), 15 deletions(-) create mode 100644 test/mas_sw_maxflow_oracle.hpp diff --git a/test/mas_sw_maxflow_oracle.hpp b/test/mas_sw_maxflow_oracle.hpp new file mode 100644 index 000000000..18c3672a4 --- /dev/null +++ b/test/mas_sw_maxflow_oracle.hpp @@ -0,0 +1,146 @@ +// Copyright Arnaud Becheler 2026. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or the copy at +// http://www.boost.org/LICENSE_1_0.txt) + +// Shared helpers for the maximum_adjacency_search and stoer_wagner_min_cut +// characterization tests: a curated set of small graphs each with its known +// global min cut, plus a push_relabel_max_flow oracle used to cross-check the +// MAS legal ordering property. + +#ifndef BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP +#define BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP + +#include +#include + +#include +#include +#include +#include + +namespace mas_sw_oracle +{ + +using undirected_graph = boost::adjacency_list< + boost::vecS, + boost::vecS, + boost::undirectedS, + boost::no_property, + boost::property< boost::edge_weight_t, int > +>; + +using weight_map_type = boost::property_map< undirected_graph, boost::edge_weight_t >::type; +using weight_type = boost::property_traits< weight_map_type >::value_type; +using vertex_descriptor = boost::graph_traits< undirected_graph >::vertex_descriptor; +using edge_descriptor = boost::graph_traits< undirected_graph >::edge_descriptor; + +// Directed network used only by the max-flow oracle. +using flow_traits = boost::adjacency_list_traits< boost::vecS, boost::vecS, boost::directedS >; +using flow_graph = boost::adjacency_list< boost::vecS, boost::vecS, boost::directedS, + boost::no_property, + boost::property< boost::edge_capacity_t, weight_type, + boost::property< boost::edge_residual_capacity_t, weight_type, + boost::property< boost::edge_reverse_t, flow_traits::edge_descriptor > > > >; + +// Minimum u-v cut of the undirected graph via max-flow. +inline weight_type undirected_min_cut(const undirected_graph& g, vertex_descriptor u, vertex_descriptor v) +{ + flow_graph fg(num_vertices(g)); + auto capacity_map = get(boost::edge_capacity, fg); + auto reverse_edge_map = get(boost::edge_reverse, fg); + auto weight_map = get(boost::edge_weight, g); + + boost::graph_traits< undirected_graph >::edge_iterator ei, ei_end; + for (boost::tie(ei, ei_end) = edges(g); ei != ei_end; ++ei) + { + // read the undirected edge and its weight + const vertex_descriptor a = source(*ei, g); + const vertex_descriptor b = target(*ei, g); + const weight_type w = get(weight_map, *ei); + // arc a to b at capacity w, paired with a zero capacity reverse + const auto a_to_b = add_edge(a, b, fg).first; + const auto a_to_b_reverse = add_edge(b, a, fg).first; + put(capacity_map, a_to_b, w); + put(capacity_map, a_to_b_reverse, 0); + put(reverse_edge_map, a_to_b, a_to_b_reverse); + put(reverse_edge_map, a_to_b_reverse, a_to_b); + // opposite arc b to a, its own capacity w and zero capacity reverse + const auto b_to_a = add_edge(b, a, fg).first; + const auto b_to_a_reverse = add_edge(a, b, fg).first; + put(capacity_map, b_to_a, w); + put(capacity_map, b_to_a_reverse, 0); + put(reverse_edge_map, b_to_a, b_to_a_reverse); + put(reverse_edge_map, b_to_a_reverse, b_to_a); + } + return boost::push_relabel_max_flow(fg, u, v); +} + +inline weight_type weighted_degree(const undirected_graph& g, vertex_descriptor v) +{ + auto weight_map = get(boost::edge_weight, g); + weight_type sum = 0; + boost::graph_traits< undirected_graph >::out_edge_iterator oi, oi_end; + for (boost::tie(oi, oi_end) = out_edges(v, g); oi != oi_end; ++oi) sum += get(weight_map, *oi); + return sum; +} + +struct weighted_edge +{ + std::size_t u; + std::size_t v; + weight_type w; +}; + +// Build an undirected weighted graph from an explicit edge list. +inline undirected_graph make_weighted_graph(std::size_t n, std::initializer_list< weighted_edge > edges) +{ + undirected_graph g(n); + auto weight_map = get(boost::edge_weight, g); + for (const weighted_edge& e : edges) + { + const auto ed = add_edge(e.u, e.v, g).first; + put(weight_map, ed, e.w); + } + return g; +} + +inline bool is_connected(const undirected_graph& g) +{ + std::vector< std::size_t > component(num_vertices(g)); + auto components_map = boost::make_iterator_property_map(component.begin(), get(boost::vertex_index, g)); + return boost::connected_components(g, components_map) == 1U; +} + +// A curated graph together with its known global minimum cut. +struct curated_graph +{ + undirected_graph graph; + weight_type min_cut; +}; + +// Deterministic set of small graphs, each chosen to trigger a specific +// condition inside maximum_adjacency_search and stoer_wagner_min_cut. +inline std::vector< curated_graph > curated_graphs() +{ + std::vector< curated_graph > graphs; + // path: every cut is a single edge, min is the lightest + graphs.push_back({ make_weighted_graph(4, { { 0, 1, 3 }, { 1, 2, 1 }, { 2, 3, 2 } }), 1 }); + // triangle: min cut isolates the lowest weighted degree vertex + graphs.push_back({ make_weighted_graph(3, { { 0, 1, 2 }, { 0, 2, 6 }, { 1, 2, 7 } }), 8 }); + // two triangles joined by a light bridge (issue 286) + graphs.push_back({ make_weighted_graph(6, { { 0, 1, 10 }, { 0, 2, 10 }, { 1, 2, 10 }, { 3, 4, 10 }, { 3, 5, 10 }, { 4, 5, 10 }, { 0, 3, 1 } }), 1 }); + // multigraph: parallel edge weights must sum + graphs.push_back({ make_weighted_graph(3, { { 0, 1, 3 }, { 1, 2, 1 }, { 1, 2, 1 }, { 2, 0, 1 } }), 3 }); + // cycle with equal weights: ties in the reach counts + graphs.push_back({ make_weighted_graph(4, { { 0, 1, 1 }, { 1, 2, 1 }, { 2, 3, 1 }, { 3, 0, 1 } }), 2 }); + // star with one heavy spoke: min cut isolates the lightest leaf + graphs.push_back({ make_weighted_graph(5, { { 0, 1, 1 }, { 0, 2, 1 }, { 0, 3, 1 }, { 0, 4, 5 } }), 1 }); + // Stoer Wagner 1997 example + graphs.push_back({ make_weighted_graph(8, { { 0, 1, 2 }, { 1, 2, 3 }, { 2, 3, 4 }, { 0, 4, 3 }, { 1, 4, 2 }, { 1, 5, 2 }, { 2, 6, 2 }, { 3, 6, 2 }, { 3, 7, 2 }, { 4, 5, 3 }, { 5, 6, 1 }, { 6, 7, 3 } }), 4 }); + return graphs; +} + +} // namespace mas_sw_oracle + +#endif // BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP diff --git a/test/mas_test.cpp b/test/mas_test.cpp index 8aaf73294..36f18e10b 100644 --- a/test/mas_test.cpp +++ b/test/mas_test.cpp @@ -4,9 +4,11 @@ // (See accompanying file LICENSE_1_0.txt or the copy at // http://www.boost.org/LICENSE_1_0.txt) +#include #include #include #include +#include #include #include #include @@ -27,12 +29,13 @@ #include -typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS, - boost::no_property, boost::property< boost::edge_weight_t, int > > - undirected_graph; -typedef boost::property_map< undirected_graph, boost::edge_weight_t >::type - weight_map_type; -typedef boost::property_traits< weight_map_type >::value_type weight_type; +#include "mas_sw_maxflow_oracle.hpp" + +using mas_sw_oracle::undirected_graph; +using mas_sw_oracle::weight_map_type; +using mas_sw_oracle::weight_type; +using mas_sw_oracle::vertex_descriptor; +using mas_sw_oracle::edge_descriptor; typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS > undirected_unweighted_graph; @@ -65,7 +68,7 @@ class mas_test_visitor : public boost::default_mas_visitor vertex_weights_when_visited_->clear(); } - void start_vertex(vertex_descriptor u, const Graph& g) + void start_vertex(vertex_descriptor u, const Graph&) { vertex_visit_order_->push_back(u); @@ -316,7 +319,7 @@ void test1() boost::maximum_adjacency_search( g, boost::weight_map(ws_map).visitor(test_vis).max_priority_queue(pq)); - + const boost::array< vertex_descriptor, vertices_count > expected_vertex_order2 = { 0, 4, 1, 5, 2, 3, 6, 7 }; const boost::array< weight_type, vertices_count > expected_weights_when_visited2 = { 9, 3, 4, 5, 3, 4, 5, 5 }; @@ -416,7 +419,7 @@ void test_unweighted( for (std::size_t i = 0; i < edge_count; i++) { weights_list[i] = 1; } - + test_weighted( edge_list, weights_list, @@ -570,6 +573,160 @@ void test9_weights_start_vertex() { ); } +using cv_distances_map_type = boost::shared_array_property_map< weight_type, + boost::property_map< undirected_graph, boost::vertex_index_t >::const_type >; +using cv_index_in_heap_type = std::vector< vertex_descriptor >::size_type; +using cv_indices_map_type = boost::shared_array_property_map< cv_index_in_heap_type, + boost::property_map< undirected_graph, boost::vertex_index_t >::const_type >; +using cv_maxheap_type = boost::d_ary_heap_indirect< vertex_descriptor, 4, cv_indices_map_type, + cv_distances_map_type, std::greater< weight_type > >; + +// Build the keyed max priority queue MAS runs on: reach counts plus heap positions. +cv_maxheap_type make_weighted_maxheap(const undirected_graph& g) +{ + auto distances_map = boost::make_shared_array_property_map(num_vertices(g), weight_type(0), get(boost::vertex_index, g)); + auto indices_map = boost::make_shared_array_property_map(num_vertices(g), cv_index_in_heap_type(-1), get(boost::vertex_index, g)); + return cv_maxheap_type(distances_map, indices_map); +} + +// Check invariants on a MAS run +void check_visit_order_invariants( + const undirected_graph& g, + const std::vector< vertex_descriptor >& order, + const std::vector< weight_type >& reach) +{ + const std::size_t n = num_vertices(g); + + // 1) the order is a permutation of all vertices + std::vector< vertex_descriptor > sorted = order; + std::sort(sorted.begin(), sorted.end()); + for (std::size_t i = 0; i < sorted.size(); ++i) + BOOST_TEST_EQ(sorted[i], static_cast< vertex_descriptor >(i)); + + // 2) MAS adds n+1 to the start vertex key to force it first, so its recorded reach is n+1 + BOOST_TEST_EQ(reach[0], static_cast< weight_type >(n + 1)); + + // only the start vertex is visited before the loop + std::vector< bool > visited(n, false); + visited[order[0]] = true; + auto weight_map = get(boost::edge_weight, g); + + // 3) recompute each later vertex reach independently and check MAS agrees + for (std::size_t i = 1; i < order.size(); ++i) + { + const vertex_descriptor u = order[i]; + // sum the weights of u's edges that lead back into the visited set + weight_type expected = 0; + boost::graph_traits< undirected_graph >::out_edge_iterator oi, oi_end; + for (boost::tie(oi, oi_end) = out_edges(u, g); oi != oi_end; ++oi) + if (visited[target(*oi, g)]) + expected += get(weight_map, *oi); + + BOOST_TEST_EQ(reach[i], expected); + visited[u] = true; + } +} + +// Records every visitor event. +class recording_visitor : public boost::default_mas_visitor +{ +public: + recording_visitor( + std::size_t& initialize_count, + std::size_t& examine_count, + std::vector< vertex_descriptor >& start_order, + std::vector< vertex_descriptor >& finish_order + ) + : + initialize_count_(initialize_count), + examine_count_(examine_count), + start_order_(start_order), + finish_order_(finish_order) + {} + + void initialize_vertex(vertex_descriptor, const undirected_graph&) { ++initialize_count_; } + void start_vertex(vertex_descriptor u, const undirected_graph&) { start_order_.push_back(u); } + void examine_edge(edge_descriptor, const undirected_graph&) { ++examine_count_; } + void finish_vertex(vertex_descriptor u, const undirected_graph&) { finish_order_.push_back(u); } + +private: + std::size_t& initialize_count_; + std::size_t& examine_count_; + std::vector< vertex_descriptor >& start_order_; + std::vector< vertex_descriptor >& finish_order_; +}; + +// Cross-validation against the max-flow oracle +// for the last two visited vertices s (second-last) and t (last), +// the reach count of t equals its weighted degree and equals the min s-t cut. +void test_maxflow_crossvalidation() +{ + // check every curated graph + for (const mas_sw_oracle::curated_graph& cg : mas_sw_oracle::curated_graphs()) + { + const undirected_graph& g = cg.graph; + BOOST_TEST(mas_sw_oracle::is_connected(g)); + + // run MAS, recording visit order and reach counts + cv_maxheap_type pq = make_weighted_maxheap(g); + mas_test_visitor< undirected_graph, cv_maxheap_type > vis(pq); + boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).visitor(vis).max_priority_queue(pq)); + + const std::vector< vertex_descriptor >& order = vis.vertex_visit_order(); + const std::vector< weight_type >& reach = vis.vertex_weights_when_visited(); + BOOST_TEST_EQ(order.size(), num_vertices(g)); + + // take the last two visited vertices s and t + const std::size_t last = order.size() - 1; + const vertex_descriptor s = order[last - 1]; + const vertex_descriptor t = order[last]; + const weight_type reach_of_t = reach[last]; + + // t's reach must equal its weighted degree and the min s-t cut + BOOST_TEST_EQ(reach_of_t, mas_sw_oracle::weighted_degree(g, t)); + BOOST_TEST_EQ(reach_of_t, mas_sw_oracle::undirected_min_cut(g, s, t)); + + // and the whole order must be a valid maximum adjacency ordering + check_visit_order_invariants(g, order, reach); + } +} + +// Every visitor event fires the expected number of times +void test_visitor_events() +{ + const undirected_graph g = mas_sw_oracle::make_weighted_graph(6, { { 0, 1, 2 }, { 1, 2, 3 }, { 2, 0, 1 }, { 2, 3, 4 }, { 3, 4, 2 }, { 4, 5, 1 }, { 5, 3, 3 } }); + + cv_maxheap_type pq = make_weighted_maxheap(g); + std::size_t initialize_count = 0; + std::size_t examine_count = 0; + std::vector< vertex_descriptor > start_order; + std::vector< vertex_descriptor > finish_order; + recording_visitor vis(initialize_count, examine_count, start_order, finish_order); + + boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).visitor(vis).max_priority_queue(pq)); + + BOOST_TEST_EQ(initialize_count, static_cast< std::size_t >(num_vertices(g))); + BOOST_TEST_EQ(examine_count, static_cast< std::size_t >(2 * num_edges(g))); + BOOST_TEST_EQ(start_order.size(), static_cast< std::size_t >(num_vertices(g))); + BOOST_TEST_EQ(finish_order.size(), static_cast< std::size_t >(num_vertices(g))); + BOOST_TEST_ALL_EQ(start_order.begin(), start_order.end(), finish_order.begin(), finish_order.end()); +} + +// Precondition violations throw. +void test_exceptions() +{ + // a graph with fewer than two vertices is rejected + undirected_graph too_small; + add_vertex(too_small); + BOOST_TEST_THROWS(boost::maximum_adjacency_search(too_small, boost::weight_map(get(boost::edge_weight, too_small))), boost::bad_graph); + + // a non-empty priority queue is rejected + const undirected_graph g = mas_sw_oracle::make_weighted_graph(4, { { 0, 1, 1 }, { 1, 2, 1 }, { 2, 3, 1 } }); + cv_maxheap_type pq = make_weighted_maxheap(g); + pq.push(0); + BOOST_TEST_THROWS(boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).max_priority_queue(pq)), std::invalid_argument); +} + #include int main(int argc, char* argv[]) @@ -586,6 +743,9 @@ int main(int argc, char* argv[]) test7_weights(); test8_weights(); test9_weights_start_vertex(); + test_maxflow_crossvalidation(); + test_visitor_events(); + test_exceptions(); } return boost::report_errors(); } diff --git a/test/stoer_wagner_test.cpp b/test/stoer_wagner_test.cpp index 198121474..9fa7acde5 100644 --- a/test/stoer_wagner_test.cpp +++ b/test/stoer_wagner_test.cpp @@ -23,12 +23,11 @@ #include #include -typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS, - boost::no_property, boost::property< boost::edge_weight_t, int > > - undirected_graph; -typedef boost::property_map< undirected_graph, boost::edge_weight_t >::type - weight_map_type; -typedef boost::property_traits< weight_map_type >::value_type weight_type; +#include "mas_sw_maxflow_oracle.hpp" + +using mas_sw_oracle::undirected_graph; +using mas_sw_oracle::weight_map_type; +using mas_sw_oracle::weight_type; typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS > undirected_unweighted_graph; @@ -41,6 +40,17 @@ struct edge_t unsigned long second; }; +// Stoer Wagner global min cut on a curated set, checked against the known value. +void test_curated_min_cuts() +{ + for (const mas_sw_oracle::curated_graph& cg : mas_sw_oracle::curated_graphs()) + { + const undirected_graph& g = cg.graph; + BOOST_TEST(mas_sw_oracle::is_connected(g)); + BOOST_TEST_EQ(boost::stoer_wagner_min_cut(g, get(boost::edge_weight, g)), cg.min_cut); + } +} + // the example from Stoer & Wagner (1997) void test0() { @@ -321,7 +331,9 @@ int main(int argc, char* argv[]) test4(); test5(); test_prgen_20_70_2(); + test_prgen_50_40_2(); test_prgen_50_70_2(); + test_curated_min_cuts(); } return boost::report_errors(); }