Skip to content

Commit e768769

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

File tree

3 files changed

+327
-0
lines changed

3 files changed

+327
-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

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

0 commit comments

Comments
 (0)