Skip to content

Commit 87186f5

Browse files
committed
test: filling gaps in MAS and SW
1 parent 6be9134 commit 87186f5

3 files changed

Lines changed: 335 additions & 12 deletions

File tree

test/mas_sw_maxflow_oracle.hpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright Arnaud Becheler 2026.
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// (See accompanying file LICENSE_1_0.txt or the copy at
4+
// http://www.boost.org/LICENSE_1_0.txt)
5+
6+
// Shared max-flow oracle helpers for
7+
// maximum_adjacency_search and stoer_wagner_min_cut characterization tests.
8+
// push_relabel_max_flow serves as an independent reference
9+
// expected values are recomputed on each random graph rather than hardcoded.
10+
11+
#ifndef BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP
12+
#define BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP
13+
14+
#include <random>
15+
#include <vector>
16+
17+
#include <boost/graph/adjacency_list.hpp>
18+
#include <boost/graph/connected_components.hpp>
19+
#include <boost/graph/push_relabel_max_flow.hpp>
20+
#include <boost/property_map/property_map.hpp>
21+
22+
namespace mas_sw_oracle
23+
{
24+
25+
using undirected_graph = boost::adjacency_list<
26+
boost::vecS,
27+
boost::vecS,
28+
boost::undirectedS,
29+
boost::no_property,
30+
boost::property< boost::edge_weight_t, int >
31+
>;
32+
33+
using weight_map_type = boost::property_map< undirected_graph, boost::edge_weight_t >::type;
34+
using weight_type = boost::property_traits< weight_map_type >::value_type;
35+
using vertex_descriptor = boost::graph_traits< undirected_graph >::vertex_descriptor;
36+
using edge_descriptor = boost::graph_traits< undirected_graph >::edge_descriptor;
37+
38+
// Directed network used only by the max-flow oracle.
39+
using flow_traits = boost::adjacency_list_traits< boost::vecS, boost::vecS, boost::directedS >;
40+
using flow_graph = boost::adjacency_list< boost::vecS, boost::vecS, boost::directedS,
41+
boost::no_property,
42+
boost::property< boost::edge_capacity_t, weight_type,
43+
boost::property< boost::edge_residual_capacity_t, weight_type,
44+
boost::property< boost::edge_reverse_t, flow_traits::edge_descriptor > > > >;
45+
46+
// Minimum u-v cut of the undirected graph via max-flow.
47+
inline weight_type undirected_min_cut(const undirected_graph& g, vertex_descriptor u, vertex_descriptor v)
48+
{
49+
flow_graph fg(num_vertices(g));
50+
auto capacity_map = get(boost::edge_capacity, fg);
51+
auto reverse_edge_map = get(boost::edge_reverse, fg);
52+
auto weight_map = get(boost::edge_weight, g);
53+
54+
boost::graph_traits< undirected_graph >::edge_iterator ei, ei_end;
55+
for (boost::tie(ei, ei_end) = edges(g); ei != ei_end; ++ei)
56+
{
57+
// read the undirected edge and its weight
58+
const vertex_descriptor a = source(*ei, g);
59+
const vertex_descriptor b = target(*ei, g);
60+
const weight_type w = get(weight_map, *ei);
61+
// arc a to b at capacity w, paired with a zero capacity reverse
62+
const auto a_to_b = add_edge(a, b, fg).first;
63+
const auto a_to_b_reverse = add_edge(b, a, fg).first;
64+
put(capacity_map, a_to_b, w);
65+
put(capacity_map, a_to_b_reverse, 0);
66+
put(reverse_edge_map, a_to_b, a_to_b_reverse);
67+
put(reverse_edge_map, a_to_b_reverse, a_to_b);
68+
// opposite arc b to a, its own capacity w and zero capacity reverse
69+
const auto b_to_a = add_edge(b, a, fg).first;
70+
const auto b_to_a_reverse = add_edge(a, b, fg).first;
71+
put(capacity_map, b_to_a, w);
72+
put(capacity_map, b_to_a_reverse, 0);
73+
put(reverse_edge_map, b_to_a, b_to_a_reverse);
74+
put(reverse_edge_map, b_to_a_reverse, b_to_a);
75+
}
76+
return boost::push_relabel_max_flow(fg, u, v);
77+
}
78+
79+
inline weight_type weighted_degree(const undirected_graph& g, vertex_descriptor v)
80+
{
81+
auto weight_map = get(boost::edge_weight, g);
82+
weight_type sum = 0;
83+
boost::graph_traits< undirected_graph >::out_edge_iterator oi, oi_end;
84+
for (boost::tie(oi, oi_end) = out_edges(v, g); oi != oi_end; ++oi) sum += get(weight_map, *oi);
85+
return sum;
86+
}
87+
88+
// Random connected undirected weighted multigraph.
89+
inline undirected_graph make_random_connected_graph(std::size_t n, std::size_t extra_edges, std::mt19937& rng)
90+
{
91+
undirected_graph g(n);
92+
std::uniform_int_distribution< weight_type > weight_dist(1, 10);
93+
std::uniform_int_distribution< std::size_t > vertex_dist(0, n - 1);
94+
auto weight_map = get(boost::edge_weight, g);
95+
96+
// spanning path 0,1,...,n-1 makes the graph connected
97+
for (std::size_t i = 1; i < n; ++i)
98+
{
99+
const auto e = add_edge(i - 1, i, g).first;
100+
put(weight_map, e, weight_dist(rng));
101+
}
102+
103+
// random extra edges, skipping self loops
104+
for (std::size_t k = 0; k < extra_edges; ++k)
105+
{
106+
const std::size_t u = vertex_dist(rng);
107+
const std::size_t v = vertex_dist(rng);
108+
if (u == v) continue;
109+
const auto e = add_edge(u, v, g).first;
110+
put(weight_map, e, weight_dist(rng));
111+
}
112+
return g;
113+
}
114+
115+
inline bool is_connected(const undirected_graph& g)
116+
{
117+
std::vector< std::size_t > component(num_vertices(g));
118+
auto components_map = boost::make_iterator_property_map(component.begin(), get(boost::vertex_index, g));
119+
return boost::connected_components(g, components_map) == 1U;
120+
}
121+
122+
} // namespace mas_sw_oracle
123+
124+
#endif // BOOST_GRAPH_TEST_MAS_SW_MAXFLOW_ORACLE_HPP

test/mas_test.cpp

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
// (See accompanying file LICENSE_1_0.txt or the copy at
55
// http://www.boost.org/LICENSE_1_0.txt)
66

7+
#include <algorithm>
78
#include <fstream>
89
#include <iostream>
910
#include <map>
11+
#include <random>
12+
#include <stdexcept>
1013
#include <vector>
1114
#include <string>
1215
#include <boost/array.hpp>
@@ -27,12 +30,13 @@
2730

2831
#include <boost/graph/iteration_macros.hpp>
2932

30-
typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS,
31-
boost::no_property, boost::property< boost::edge_weight_t, int > >
32-
undirected_graph;
33-
typedef boost::property_map< undirected_graph, boost::edge_weight_t >::type
34-
weight_map_type;
35-
typedef boost::property_traits< weight_map_type >::value_type weight_type;
33+
#include "mas_sw_maxflow_oracle.hpp"
34+
35+
using mas_sw_oracle::undirected_graph;
36+
using mas_sw_oracle::weight_map_type;
37+
using mas_sw_oracle::weight_type;
38+
using mas_sw_oracle::vertex_descriptor;
39+
using mas_sw_oracle::edge_descriptor;
3640

3741
typedef boost::adjacency_list< boost::vecS, boost::vecS, boost::undirectedS >
3842
undirected_unweighted_graph;
@@ -570,6 +574,169 @@ void test9_weights_start_vertex() {
570574
);
571575
}
572576

577+
using cv_distances_map_type = boost::shared_array_property_map< weight_type,
578+
boost::property_map< undirected_graph, boost::vertex_index_t >::const_type >;
579+
using cv_index_in_heap_type = std::vector< vertex_descriptor >::size_type;
580+
using cv_indices_map_type = boost::shared_array_property_map< cv_index_in_heap_type,
581+
boost::property_map< undirected_graph, boost::vertex_index_t >::const_type >;
582+
using cv_maxheap_type = boost::d_ary_heap_indirect< vertex_descriptor, 4, cv_indices_map_type,
583+
cv_distances_map_type, std::greater< weight_type > >;
584+
585+
// Build the keyed max priority queue MAS runs on: reach counts plus heap positions.
586+
cv_maxheap_type make_weighted_maxheap(const undirected_graph& g)
587+
{
588+
auto distances_map = boost::make_shared_array_property_map(num_vertices(g), weight_type(0), get(boost::vertex_index, g));
589+
auto indices_map = boost::make_shared_array_property_map(num_vertices(g), cv_index_in_heap_type(-1), get(boost::vertex_index, g));
590+
return cv_maxheap_type(distances_map, indices_map);
591+
}
592+
593+
// Check invariants on a MAS run
594+
void check_visit_order_invariants(
595+
const undirected_graph& g,
596+
const std::vector< vertex_descriptor >& order,
597+
const std::vector< weight_type >& reach)
598+
{
599+
const std::size_t n = num_vertices(g);
600+
601+
// 1) the order is a permutation of all vertices
602+
std::vector< vertex_descriptor > sorted = order;
603+
std::sort(sorted.begin(), sorted.end());
604+
for (std::size_t i = 0; i < sorted.size(); ++i)
605+
BOOST_TEST_EQ(sorted[i], static_cast< vertex_descriptor >(i));
606+
607+
// 2) MAS adds n+1 to the start vertex key to force it first, so its recorded reach is n+1
608+
BOOST_TEST_EQ(reach[0], static_cast< weight_type >(n + 1));
609+
610+
// only the start vertex is visited before the loop
611+
std::vector< bool > visited(n, false);
612+
visited[order[0]] = true;
613+
auto weight_map = get(boost::edge_weight, g);
614+
615+
// 3) recompute each later vertex reach independently and check MAS agrees
616+
for (std::size_t i = 1; i < order.size(); ++i)
617+
{
618+
const vertex_descriptor u = order[i];
619+
// sum the weights of u's edges that lead back into the visited set
620+
weight_type expected = 0;
621+
boost::graph_traits< undirected_graph >::out_edge_iterator oi, oi_end;
622+
for (boost::tie(oi, oi_end) = out_edges(u, g); oi != oi_end; ++oi)
623+
if (visited[target(*oi, g)])
624+
expected += get(weight_map, *oi);
625+
626+
BOOST_TEST_EQ(reach[i], expected);
627+
visited[u] = true;
628+
}
629+
}
630+
631+
// Records every visitor event.
632+
class recording_visitor : public boost::default_mas_visitor
633+
{
634+
public:
635+
recording_visitor(
636+
std::size_t& initialize_count,
637+
std::size_t& examine_count,
638+
std::vector< vertex_descriptor >& start_order,
639+
std::vector< vertex_descriptor >& finish_order
640+
)
641+
:
642+
initialize_count_(initialize_count),
643+
examine_count_(examine_count),
644+
start_order_(start_order),
645+
finish_order_(finish_order)
646+
{}
647+
648+
void initialize_vertex(vertex_descriptor, const undirected_graph&) { ++initialize_count_; }
649+
void start_vertex(vertex_descriptor u, const undirected_graph&) { start_order_.push_back(u); }
650+
void examine_edge(edge_descriptor, const undirected_graph&) { ++examine_count_; }
651+
void finish_vertex(vertex_descriptor u, const undirected_graph&) { finish_order_.push_back(u); }
652+
653+
private:
654+
std::size_t& initialize_count_;
655+
std::size_t& examine_count_;
656+
std::vector< vertex_descriptor >& start_order_;
657+
std::vector< vertex_descriptor >& finish_order_;
658+
};
659+
660+
// Cross-validation against the max-flow oracle
661+
// for the last two visited vertices s (second-last) and t (last),
662+
// the reach count of t equals its weighted degree and equals the min s-t cut.
663+
void test_maxflow_crossvalidation()
664+
{
665+
// sweep a few graph sizes and several seeds
666+
const std::size_t sizes[] = { 6, 8, 10, 12, 50 };
667+
for (std::size_t si = 0; si < sizeof(sizes) / sizeof(sizes[0]); ++si)
668+
{
669+
const std::size_t n = sizes[si];
670+
for (std::size_t seed = 1; seed <= 25; ++seed)
671+
{
672+
// build a deterministic random connected graph
673+
std::mt19937 rng(static_cast< std::mt19937::result_type >(seed));
674+
const undirected_graph g = mas_sw_oracle::make_random_connected_graph(n, n, rng);
675+
BOOST_TEST(mas_sw_oracle::is_connected(g));
676+
677+
// run MAS, recording visit order and reach counts
678+
cv_maxheap_type pq = make_weighted_maxheap(g);
679+
mas_test_visitor< undirected_graph, cv_maxheap_type > vis(pq);
680+
boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).visitor(vis).max_priority_queue(pq));
681+
682+
const std::vector< vertex_descriptor >& order = vis.vertex_visit_order();
683+
const std::vector< weight_type >& reach = vis.vertex_weights_when_visited();
684+
BOOST_TEST_EQ(order.size(), n);
685+
686+
// take the last two visited vertices s and t
687+
const std::size_t last = order.size() - 1;
688+
const vertex_descriptor s = order[last - 1];
689+
const vertex_descriptor t = order[last];
690+
const weight_type reach_of_t = reach[last];
691+
692+
// t's reach must equal its weighted degree and the min s-t cut
693+
BOOST_TEST_EQ(reach_of_t, mas_sw_oracle::weighted_degree(g, t));
694+
BOOST_TEST_EQ(reach_of_t, mas_sw_oracle::undirected_min_cut(g, s, t));
695+
696+
// and the whole order must be a valid maximum adjacency ordering
697+
check_visit_order_invariants(g, order, reach);
698+
}
699+
}
700+
}
701+
702+
// Every visitor event fires the expected number of times
703+
void test_visitor_events()
704+
{
705+
std::mt19937 rng(7);
706+
const undirected_graph g = mas_sw_oracle::make_random_connected_graph(8, 8, rng);
707+
708+
cv_maxheap_type pq = make_weighted_maxheap(g);
709+
std::size_t initialize_count = 0;
710+
std::size_t examine_count = 0;
711+
std::vector< vertex_descriptor > start_order;
712+
std::vector< vertex_descriptor > finish_order;
713+
recording_visitor vis(initialize_count, examine_count, start_order, finish_order);
714+
715+
boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).visitor(vis).max_priority_queue(pq));
716+
717+
BOOST_TEST_EQ(initialize_count, static_cast< std::size_t >(num_vertices(g)));
718+
BOOST_TEST_EQ(examine_count, static_cast< std::size_t >(2 * num_edges(g)));
719+
BOOST_TEST_EQ(start_order.size(), static_cast< std::size_t >(num_vertices(g)));
720+
BOOST_TEST_EQ(finish_order.size(), static_cast< std::size_t >(num_vertices(g)));
721+
BOOST_TEST_ALL_EQ(start_order.begin(), start_order.end(), finish_order.begin(), finish_order.end());
722+
}
723+
724+
// Precondition violations throw.
725+
void test_exceptions()
726+
{
727+
// a graph with fewer than two vertices is rejected
728+
undirected_graph too_small;
729+
add_vertex(too_small);
730+
BOOST_TEST_THROWS(boost::maximum_adjacency_search(too_small, boost::weight_map(get(boost::edge_weight, too_small))), boost::bad_graph);
731+
732+
// a non-empty priority queue is rejected
733+
std::mt19937 rng(3);
734+
const undirected_graph g = mas_sw_oracle::make_random_connected_graph(4, 4, rng);
735+
cv_maxheap_type pq = make_weighted_maxheap(g);
736+
pq.push(0);
737+
BOOST_TEST_THROWS(boost::maximum_adjacency_search(g, boost::weight_map(get(boost::edge_weight, g)).max_priority_queue(pq)), std::invalid_argument);
738+
}
739+
573740
#include <boost/graph/iteration_macros_undef.hpp>
574741

575742
int main(int argc, char* argv[])
@@ -586,6 +753,9 @@ int main(int argc, char* argv[])
586753
test7_weights();
587754
test8_weights();
588755
test9_weights_start_vertex();
756+
test_maxflow_crossvalidation();
757+
test_visitor_events();
758+
test_exceptions();
589759
}
590760
return boost::report_errors();
591761
}

0 commit comments

Comments
 (0)