summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>2015-04-09 13:55:40 +1200
committerAndrew Bartlett <abartlet@samba.org>2015-05-29 11:08:22 +0200
commitcb8b99e335101c5454a1e448275993aa18f77e10 (patch)
tree2d298c935ca40a69e9e19fcc045053987a674866 /python
parent75eedf85b1516f0b2370d23d6ff1058a70b093b0 (diff)
downloadsamba-cb8b99e335101c5454a1e448275993aa18f77e10.tar.gz
KCC: improve directed_double_ring graph check
The previous test assumed there would be only a double directed ring but in fact there could be other edges. In large graphs there are certain to be more edges. Now we want to be sure there is a complete ring apart from any other connections. This is called the Hamiltonian path problem and takes exponential time in general, so now our test is that it looks *quite* a lot like a complete ring. Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz> Reviewed-by: Garming Sam <garming@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Diffstat (limited to 'python')
-rw-r--r--python/samba/graph_utils.py119
1 files changed, 73 insertions, 46 deletions
diff --git a/python/samba/graph_utils.py b/python/samba/graph_utils.py
index 9e97c62092b..2f50fdd9d10 100644
--- a/python/samba/graph_utils.py
+++ b/python/samba/graph_utils.py
@@ -197,59 +197,86 @@ def verify_graph_directed_double_ring(edges, vertices, edge_vertices):
touches every vertex and form a loop.
There might be other connections that *aren't* part of the ring.
+
+ Deciding this for sure is NP-complete (the Hamiltonian path
+ problem), but there are some easy failures that can be detected.
+ So far we check for:
+ - leaf nodes
+ - disjoint subgraphs
+
+ But, for example, a figure-8 topology will not be found.
"""
- #XXX 1 and 2 vertex cases are special cases.
- if not edges:
+ # a zero or one node graph is OK with no edges.
+ # The two vertex case is special. Use
+ # verify_graph_directed_double_ring_or_small() to allow that.
+ if not edges and len(vertices) <= 1:
return
if len(edges) < 2 * len(vertices):
- raise GraphError("directed double ring requires at least twice as many edges as vertices")
-
- exits = {}
- for start, end in edges:
- s = exits.setdefault(start, [])
- s.append(end)
-
- try:
- #follow both paths at once -- they should be the same length
- #XXX there is probably a simpler way.
- forwards, backwards = exits[start]
- fprev, bprev = (start, start)
- f_path = [start]
- b_path = [start]
- for i in range(len(vertices)):
- a, b = exits[forwards]
- if a == fprev:
- fnext = b
- else:
- fnext = a
- f_path.append(forwards)
- fprev = forwards
- forwards = fnext
-
- a, b = exits[backwards]
- if a == bprev:
- bnext = b
- else:
- bnext = a
- b_path.append(backwards)
- bprev = backwards
- backwards = bnext
-
- except ValueError, e:
- raise GraphError("wrong number of exits '%s'" % e)
-
- f_set = set(f_path)
- b_set = set(b_path)
-
- if (f_path != list(reversed(b_path)) or
- len(f_path) != len(f_set) + 1 or
- len(f_set) != len(vertices)):
- raise GraphError("doesn't seem like a double ring to me!")
+ raise GraphError("directed double ring requires at least twice "
+ "as many edges as vertices")
+
+ # Reduce the problem space by looking only at bi-directional links.
+ half_duplex = set(edges)
+ duplex_links = set()
+ for edge in edges:
+ rev_edge = (edge[1], edge[0])
+ if edge in half_duplex and rev_edge in half_duplex:
+ duplex_links.add(edge)
+ half_duplex.remove(edge)
+ half_duplex.remove(rev_edge)
+
+ # the Hamiltonian cycle problem is NP-complete in general, but we
+ # can cheat a bit and prove a less strong result.
+ #
+ # We declutter the graph by replacing nodes with edges connecting
+ # their neighbours.
+ #
+ # A-B-C --> A-C
+ #
+ # -A-B-C- --> -A--C-
+ # `D_ `D'_
+ #
+ # In the end there should be a single 2 vertex graph.
+
+ edge_map = {}
+ for a, b in duplex_links:
+ edge_map.setdefault(a, set()).add(b)
+ edge_map.setdefault(b, set()).add(a)
+
+ # an easy to detect failure is a lonely leaf node
+ for vertex, neighbours in edge_map.items():
+ if len(neighbours) == 1:
+ raise GraphError("wanted double directed ring, found a leaf node"
+ "(%s)" % vertex)
+
+ for vertex in edge_map.keys():
+ nset = edge_map[vertex]
+ if not nset:
+ continue
+ for n in nset:
+ n_neighbours = edge_map[n]
+ n_neighbours.remove(vertex)
+ n_neighbours.update(x for x in nset if x != n)
+ del edge_map[vertex]
+
+ if len(edge_map) > 1:
+ raise GraphError("wanted double directed ring, but "
+ "this looks like a split graph\n"
+ "(%s can't reach each other)" %
+ ', '.join(edge_map.keys()))
def verify_graph_directed_double_ring_or_small(edges, vertices, edge_vertices):
- if len(vertices) < 3:
+ if len(vertices) < 2:
return
+ if len(vertices) == 2:
+ """With 2 nodes there should be a single link in each directions."""
+ if (len(edges) == 2 and
+ edges[0][0] == edges[1][1] and
+ edges[0][1] == edges[1][0]):
+ return
+ raise GraphError("A two vertex graph should have an edge each way.")
+
return verify_graph_directed_double_ring(edges, vertices, edge_vertices)