Skip to content

Commit deff59d

Browse files
committed
Add tests for grapht::make_chordal and grapht::connected_subgraphs
1 parent d00d833 commit deff59d

File tree

3 files changed

+322
-0
lines changed

3 files changed

+322
-0
lines changed

src/util/graph.h

+9
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,11 @@ std::vector<typename N::node_indext> grapht<N>::depth_limited_search(
678678
return src;
679679
}
680680

681+
/// Find connected subgraphs in an undirected graph.
682+
/// \param [out] subgraph_nr: will be resized to graph.size() and populated
683+
/// to map node indices onto subgraph numbers. The subgraph numbers are dense,
684+
/// in the range 0 - (number of subgraphs - 1)
685+
/// \return Number of subgraphs found.
681686
template<class N>
682687
std::size_t grapht<N>::connected_subgraphs(
683688
std::vector<node_indext> &subgraph_nr)
@@ -790,6 +795,10 @@ std::size_t grapht<N>::SCCs(std::vector<node_indext> &subgraph_nr) const
790795
return t.scc_count;
791796
}
792797

798+
/// Ensure a graph is chordal (contains no 4+-cycles without an edge crossing
799+
/// the cycle) by adding extra edges. Note this adds more edges than are
800+
/// required, including to acyclic graphs or acyclic subgraphs of cyclic graphs,
801+
/// but does at least ensure the graph is not chordal.
793802
template<class N>
794803
void grapht<N>::make_chordal()
795804
{

unit/Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ SRC += unit_tests.cpp \
2424
solvers/refinement/string_refinement/sparse_array.cpp \
2525
solvers/refinement/string_refinement/union_find_replace.cpp \
2626
util/expr_cast/expr_cast.cpp \
27+
util/graph.cpp \
2728
util/irep.cpp \
2829
util/irep_sharing.cpp \
2930
util/message.cpp \

unit/util/graph.cpp

+312
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
/*******************************************************************\
2+
3+
Module: Unit test for graph.h
4+
5+
Author: Diffblue Ltd
6+
7+
\*******************************************************************/
8+
9+
#include <testing-utils/catch.hpp>
10+
11+
#include <util/graph.h>
12+
13+
template<class E>
14+
static inline bool operator==(
15+
const graph_nodet<E> &gn1, const graph_nodet<E> &gn2)
16+
{
17+
return gn1.in == gn2.in && gn1.out == gn2.out;
18+
}
19+
20+
template<class N>
21+
static inline bool operator==(const grapht<N> &g1, const grapht<N> &g2)
22+
{
23+
if(g1.size() != g2.size())
24+
return false;
25+
26+
for(typename grapht<N>::node_indext i = 0; i < g1.size(); ++i)
27+
{
28+
if(!(g1[i] == g2[i]))
29+
return false;
30+
}
31+
32+
return true;
33+
}
34+
35+
/// To verify make_chordal is working as intended: naively search for a
36+
/// hole (a chordless 4+-cycle)
37+
template<class N>
38+
static bool contains_hole(
39+
const grapht<N> &g,
40+
const std::vector<typename grapht<N>::node_indext> &cycle_so_far)
41+
{
42+
const auto &successors_map = g[cycle_so_far.back()].out;
43+
44+
// If this node has a triangular edge (one leading to cycle_so_far[-3]) then
45+
// this isn't a hole:
46+
if(cycle_so_far.size() >= 3 &&
47+
successors_map.count(cycle_so_far[cycle_so_far.size() - 3]) != 0)
48+
{
49+
return false;
50+
}
51+
52+
// If this node has an edge leading to any other cycle member (except our
53+
// immediate predecessor) then we've found a hole:
54+
if(cycle_so_far.size() >= 4)
55+
{
56+
for(std::size_t i = 0; i <= cycle_so_far.size() - 4; ++i)
57+
{
58+
if(successors_map.count(cycle_so_far[i]) != 0)
59+
return true;
60+
}
61+
}
62+
63+
// Otherwise try to extend the cycle:
64+
for(const auto &successor : successors_map)
65+
{
66+
// The input is undirected, so a 2-cycle is always present:
67+
if(cycle_so_far.size() >= 2 &&
68+
successor.first == cycle_so_far[cycle_so_far.size() - 2])
69+
{
70+
continue;
71+
}
72+
73+
std::vector<typename grapht<N>::node_indext> extended_cycle = cycle_so_far;
74+
extended_cycle.push_back(successor.first);
75+
if(contains_hole(g, extended_cycle))
76+
return true;
77+
}
78+
79+
return false;
80+
}
81+
82+
template<class N>
83+
static bool contains_hole(const grapht<N> &g)
84+
{
85+
// For each node in the graph, check for cycles starting at that node.
86+
// This is pretty dumb, but I figure this formulation is simple enough to
87+
// check by hand and the complexity isn't too bad for small examples.
88+
89+
for(typename grapht<N>::node_indext i = 0; i < g.size(); ++i)
90+
{
91+
std::vector<typename grapht<N>::node_indext> start_node {i};
92+
if(contains_hole(g, start_node))
93+
return true;
94+
}
95+
96+
return false;
97+
}
98+
99+
typedef grapht<graph_nodet<empty_edget>> simple_grapht;
100+
101+
SCENARIO("graph-make-chordal",
102+
"[core][util][graph]")
103+
{
104+
GIVEN("An acyclic graph")
105+
{
106+
simple_grapht graph;
107+
simple_grapht::node_indext indices[5];
108+
109+
for(int i = 0; i < 5; ++i)
110+
indices[i] = graph.add_node();
111+
112+
// Make a graph: 0 <-> 1 <-> 4
113+
// \-> 2 <-/
114+
// \-> 3
115+
graph.add_undirected_edge(indices[0], indices[1]);
116+
graph.add_undirected_edge(indices[0], indices[2]);
117+
graph.add_undirected_edge(indices[0], indices[3]);
118+
graph.add_undirected_edge(indices[1], indices[4]);
119+
graph.add_undirected_edge(indices[2], indices[4]);
120+
121+
WHEN("The graph is made chordal")
122+
{
123+
simple_grapht chordal_graph = graph;
124+
chordal_graph.make_chordal();
125+
126+
THEN("The graph should be unchanged")
127+
{
128+
// This doesn't pass, as make_chordal actually adds triangular edges to
129+
// *all* common neighbours, even nodes that aren't part of a cycle.
130+
// REQUIRE(graph == chordal_graph);
131+
132+
// At least it shouldn't be chordal!
133+
REQUIRE(!contains_hole(chordal_graph));
134+
}
135+
}
136+
}
137+
138+
GIVEN("A cyclic graph that is already chordal")
139+
{
140+
simple_grapht graph;
141+
simple_grapht::node_indext indices[5];
142+
143+
for(int i = 0; i < 5; ++i)
144+
indices[i] = graph.add_node();
145+
146+
// Make a graph: 0 <-> 1 <-> 2 <-> 0
147+
// 3 <-> 1 <-> 2 <-> 3
148+
// 4 <-> 1 <-> 2 <-> 4
149+
graph.add_undirected_edge(indices[0], indices[1]);
150+
graph.add_undirected_edge(indices[1], indices[2]);
151+
graph.add_undirected_edge(indices[2], indices[0]);
152+
graph.add_undirected_edge(indices[3], indices[1]);
153+
graph.add_undirected_edge(indices[2], indices[3]);
154+
graph.add_undirected_edge(indices[4], indices[1]);
155+
graph.add_undirected_edge(indices[2], indices[4]);
156+
157+
WHEN("The graph is made chordal")
158+
{
159+
simple_grapht chordal_graph = graph;
160+
chordal_graph.make_chordal();
161+
162+
THEN("The graph should be unchanged")
163+
{
164+
// This doesn't pass, as make_chordal actually adds triangular edges to
165+
// *all* common neighbours, even cycles that are already chordal.
166+
// REQUIRE(graph == chordal_graph);
167+
168+
// At least it shouldn't be chordal!
169+
REQUIRE(!contains_hole(chordal_graph));
170+
}
171+
}
172+
}
173+
174+
GIVEN("A simple 4-cycle")
175+
{
176+
simple_grapht graph;
177+
simple_grapht::node_indext indices[4];
178+
179+
for(int i = 0; i < 4; ++i)
180+
indices[i] = graph.add_node();
181+
182+
graph.add_undirected_edge(indices[0], indices[1]);
183+
graph.add_undirected_edge(indices[1], indices[2]);
184+
graph.add_undirected_edge(indices[2], indices[3]);
185+
graph.add_undirected_edge(indices[3], indices[0]);
186+
187+
// Check the contains_hole predicate is working as intended:
188+
REQUIRE(contains_hole(graph));
189+
190+
WHEN("The graph is made chordal")
191+
{
192+
simple_grapht chordal_graph = graph;
193+
chordal_graph.make_chordal();
194+
195+
THEN("The graph should gain a chord")
196+
{
197+
REQUIRE(!contains_hole(chordal_graph));
198+
}
199+
}
200+
}
201+
202+
GIVEN("A more complicated graph with a hole")
203+
{
204+
simple_grapht graph;
205+
simple_grapht::node_indext indices[8];
206+
207+
for(int i = 0; i < 8; ++i)
208+
indices[i] = graph.add_node();
209+
210+
// A 5-cycle:
211+
graph.add_undirected_edge(indices[0], indices[1]);
212+
graph.add_undirected_edge(indices[1], indices[2]);
213+
graph.add_undirected_edge(indices[2], indices[3]);
214+
graph.add_undirected_edge(indices[3], indices[4]);
215+
graph.add_undirected_edge(indices[4], indices[0]);
216+
217+
// A 3-cycle connected to the 5:
218+
graph.add_undirected_edge(indices[4], indices[5]);
219+
graph.add_undirected_edge(indices[5], indices[6]);
220+
graph.add_undirected_edge(indices[6], indices[4]);
221+
222+
// Another 3-cycle joined onto the 5:
223+
graph.add_undirected_edge(indices[1], indices[7]);
224+
graph.add_undirected_edge(indices[3], indices[7]);
225+
226+
// Check we've made the input correctly:
227+
REQUIRE(contains_hole(graph));
228+
229+
WHEN("The graph is made chordal")
230+
{
231+
simple_grapht chordal_graph = graph;
232+
chordal_graph.make_chordal();
233+
234+
THEN("The graph's 5-cycle should be completed with chords")
235+
{
236+
REQUIRE(!contains_hole(chordal_graph));
237+
}
238+
}
239+
}
240+
}
241+
242+
SCENARIO("graph-connected-subgraphs",
243+
"[core][util][graph]")
244+
{
245+
GIVEN("A connected graph")
246+
{
247+
simple_grapht graph;
248+
simple_grapht::node_indext indices[5];
249+
250+
for(int i = 0; i < 5; ++i)
251+
indices[i] = graph.add_node();
252+
253+
// Make a graph: 0 <-> 1 <-> 4
254+
// \-> 2 <-/
255+
// \-> 3
256+
graph.add_undirected_edge(indices[0], indices[1]);
257+
graph.add_undirected_edge(indices[0], indices[2]);
258+
graph.add_undirected_edge(indices[0], indices[3]);
259+
graph.add_undirected_edge(indices[1], indices[4]);
260+
graph.add_undirected_edge(indices[2], indices[4]);
261+
262+
WHEN("We take its connected subgraphs")
263+
{
264+
std::vector<simple_grapht::node_indext> subgraphs;
265+
graph.connected_subgraphs(subgraphs);
266+
267+
REQUIRE(subgraphs.size() == graph.size());
268+
simple_grapht::node_indext only_subgraph = subgraphs.at(0);
269+
270+
// Check everything is in one subgraph:
271+
REQUIRE(
272+
subgraphs ==
273+
std::vector<simple_grapht::node_indext>(graph.size(), only_subgraph));
274+
}
275+
}
276+
277+
GIVEN("A graph with three unconnected subgraphs")
278+
{
279+
simple_grapht graph;
280+
simple_grapht::node_indext indices[6];
281+
282+
for(int i = 0; i < 6; ++i)
283+
indices[i] = graph.add_node();
284+
285+
graph.add_undirected_edge(indices[0], indices[1]);
286+
graph.add_undirected_edge(indices[2], indices[3]);
287+
graph.add_undirected_edge(indices[4], indices[5]);
288+
289+
WHEN("We take its connected subgraphs")
290+
{
291+
std::vector<simple_grapht::node_indext> subgraphs;
292+
graph.connected_subgraphs(subgraphs);
293+
294+
REQUIRE(subgraphs.size() == graph.size());
295+
simple_grapht::node_indext first_subgraph = subgraphs.at(0);
296+
simple_grapht::node_indext second_subgraph = subgraphs.at(2);
297+
simple_grapht::node_indext third_subgraph = subgraphs.at(4);
298+
299+
std::vector<simple_grapht::node_indext> expected_subgraphs
300+
{
301+
first_subgraph,
302+
first_subgraph,
303+
second_subgraph,
304+
second_subgraph,
305+
third_subgraph,
306+
third_subgraph
307+
};
308+
309+
REQUIRE(subgraphs == expected_subgraphs);
310+
}
311+
}
312+
}

0 commit comments

Comments
 (0)