| # -*- coding: utf-8 -*- |
| """ |
| Algorithms for chordal graphs. |
| |
| A graph is chordal if every cycle of length at least 4 has a chord |
| (an edge joining two nodes not adjacent in the cycle). |
| http://en.wikipedia.org/wiki/Chordal_graph |
| """ |
| import networkx as nx |
| import random |
| import sys |
| |
| __authors__ = "\n".join(['Jesus Cerquides <cerquide@iiia.csic.es>']) |
| # Copyright (C) 2010 by |
| # Jesus Cerquides <cerquide@iiia.csic.es> |
| # All rights reserved. |
| # BSD license. |
| |
| __all__ = ['is_chordal', |
| 'find_induced_nodes', |
| 'chordal_graph_cliques', |
| 'chordal_graph_treewidth', |
| 'NetworkXTreewidthBoundExceeded'] |
| |
| class NetworkXTreewidthBoundExceeded(nx.NetworkXException): |
| """Exception raised when a treewidth bound has been provided and it has |
| been exceeded""" |
| |
| |
| def is_chordal(G): |
| """Checks whether G is a chordal graph. |
| |
| A graph is chordal if every cycle of length at least 4 has a chord |
| (an edge joining two nodes not adjacent in the cycle). |
| |
| Parameters |
| ---------- |
| G : graph |
| A NetworkX graph. |
| |
| Returns |
| ------- |
| chordal : bool |
| True if G is a chordal graph and False otherwise. |
| |
| Raises |
| ------ |
| NetworkXError |
| The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. |
| If the input graph is an instance of one of these classes, a |
| NetworkXError is raised. |
| |
| Examples |
| -------- |
| >>> import networkx as nx |
| >>> e=[(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(3,6),(4,5),(4,6),(5,6)] |
| >>> G=nx.Graph(e) |
| >>> nx.is_chordal(G) |
| True |
| |
| Notes |
| ----- |
| The routine tries to go through every node following maximum cardinality |
| search. It returns False when it finds that the separator for any node |
| is not a clique. Based on the algorithms in [1]_. |
| |
| References |
| ---------- |
| .. [1] R. E. Tarjan and M. Yannakakis, Simple linear-time algorithms |
| to test chordality of graphs, test acyclicity of hypergraphs, and |
| selectively reduce acyclic hypergraphs, SIAM J. Comput., 13 (1984), |
| pp. 566–579. |
| """ |
| if G.is_directed(): |
| raise nx.NetworkXError('Directed graphs not supported') |
| if G.is_multigraph(): |
| raise nx.NetworkXError('Multiply connected graphs not supported.') |
| if len(_find_chordality_breaker(G))==0: |
| return True |
| else: |
| return False |
| |
| def find_induced_nodes(G,s,t,treewidth_bound=sys.maxsize): |
| """Returns the set of induced nodes in the path from s to t. |
| |
| Parameters |
| ---------- |
| G : graph |
| A chordal NetworkX graph |
| s : node |
| Source node to look for induced nodes |
| t : node |
| Destination node to look for induced nodes |
| treewith_bound: float |
| Maximum treewidth acceptable for the graph H. The search |
| for induced nodes will end as soon as the treewidth_bound is exceeded. |
| |
| Returns |
| ------- |
| I : Set of nodes |
| The set of induced nodes in the path from s to t in G |
| |
| Raises |
| ------ |
| NetworkXError |
| The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. |
| If the input graph is an instance of one of these classes, a |
| NetworkXError is raised. |
| The algorithm can only be applied to chordal graphs. If |
| the input graph is found to be non-chordal, a NetworkXError is raised. |
| |
| Examples |
| -------- |
| >>> import networkx as nx |
| >>> G=nx.Graph() |
| >>> G = nx.generators.classic.path_graph(10) |
| >>> I = nx.find_induced_nodes(G,1,9,2) |
| >>> list(I) |
| [1, 2, 3, 4, 5, 6, 7, 8, 9] |
| |
| Notes |
| ----- |
| G must be a chordal graph and (s,t) an edge that is not in G. |
| |
| If a treewidth_bound is provided, the search for induced nodes will end |
| as soon as the treewidth_bound is exceeded. |
| |
| The algorithm is inspired by Algorithm 4 in [1]_. |
| A formal definition of induced node can also be found on that reference. |
| |
| References |
| ---------- |
| .. [1] Learning Bounded Treewidth Bayesian Networks. |
| Gal Elidan, Stephen Gould; JMLR, 9(Dec):2699--2731, 2008. |
| http://jmlr.csail.mit.edu/papers/volume9/elidan08a/elidan08a.pdf |
| """ |
| if not is_chordal(G): |
| raise nx.NetworkXError("Input graph is not chordal.") |
| |
| H = nx.Graph(G) |
| H.add_edge(s,t) |
| I = set() |
| triplet = _find_chordality_breaker(H,s,treewidth_bound) |
| while triplet: |
| (u,v,w) = triplet |
| I.update(triplet) |
| for n in triplet: |
| if n!=s: |
| H.add_edge(s,n) |
| triplet = _find_chordality_breaker(H,s,treewidth_bound) |
| if I: |
| # Add t and the second node in the induced path from s to t. |
| I.add(t) |
| for u in G[s]: |
| if len(I & set(G[u]))==2: |
| I.add(u) |
| break |
| return I |
| |
| def chordal_graph_cliques(G): |
| """Returns the set of maximal cliques of a chordal graph. |
| |
| The algorithm breaks the graph in connected components and performs a |
| maximum cardinality search in each component to get the cliques. |
| |
| Parameters |
| ---------- |
| G : graph |
| A NetworkX graph |
| |
| Returns |
| ------- |
| cliques : A set containing the maximal cliques in G. |
| |
| Raises |
| ------ |
| NetworkXError |
| The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. |
| If the input graph is an instance of one of these classes, a |
| NetworkXError is raised. |
| The algorithm can only be applied to chordal graphs. If the |
| input graph is found to be non-chordal, a NetworkXError is raised. |
| |
| Examples |
| -------- |
| >>> import networkx as nx |
| >>> e= [(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(3,6),(4,5),(4,6),(5,6),(7,8)] |
| >>> G = nx.Graph(e) |
| >>> G.add_node(9) |
| >>> setlist = nx.chordal_graph_cliques(G) |
| """ |
| if not is_chordal(G): |
| raise nx.NetworkXError("Input graph is not chordal.") |
| |
| cliques = set() |
| for C in nx.connected.connected_component_subgraphs(G): |
| cliques |= _connected_chordal_graph_cliques(C) |
| |
| return cliques |
| |
| |
| def chordal_graph_treewidth(G): |
| """Returns the treewidth of the chordal graph G. |
| |
| Parameters |
| ---------- |
| G : graph |
| A NetworkX graph |
| |
| Returns |
| ------- |
| treewidth : int |
| The size of the largest clique in the graph minus one. |
| |
| Raises |
| ------ |
| NetworkXError |
| The algorithm does not support DiGraph, MultiGraph and MultiDiGraph. |
| If the input graph is an instance of one of these classes, a |
| NetworkXError is raised. |
| The algorithm can only be applied to chordal graphs. If |
| the input graph is found to be non-chordal, a NetworkXError is raised. |
| |
| Examples |
| -------- |
| >>> import networkx as nx |
| >>> e = [(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(3,6),(4,5),(4,6),(5,6),(7,8)] |
| >>> G = nx.Graph(e) |
| >>> G.add_node(9) |
| >>> nx.chordal_graph_treewidth(G) |
| 3 |
| |
| References |
| ---------- |
| .. [1] http://en.wikipedia.org/wiki/Tree_decomposition#Treewidth |
| """ |
| if not is_chordal(G): |
| raise nx.NetworkXError("Input graph is not chordal.") |
| |
| max_clique = -1 |
| for clique in nx.chordal_graph_cliques(G): |
| max_clique = max(max_clique,len(clique)) |
| return max_clique - 1 |
| |
| def _is_complete_graph(G): |
| """Returns True if G is a complete graph.""" |
| if G.number_of_selfloops()>0: |
| raise nx.NetworkXError("Self loop found in _is_complete_graph()") |
| n = G.number_of_nodes() |
| if n < 2: |
| return True |
| e = G.number_of_edges() |
| max_edges = ((n * (n-1))/2) |
| return e == max_edges |
| |
| |
| def _find_missing_edge(G): |
| """ Given a non-complete graph G, returns a missing edge.""" |
| nodes=set(G) |
| for u in G: |
| missing=nodes-set(list(G[u].keys())+[u]) |
| if missing: |
| return (u,missing.pop()) |
| |
| |
| def _max_cardinality_node(G,choices,wanna_connect): |
| """Returns a the node in choices that has more connections in G |
| to nodes in wanna_connect. |
| """ |
| # max_number = None |
| max_number = -1 |
| for x in choices: |
| number=len([y for y in G[x] if y in wanna_connect]) |
| if number > max_number: |
| max_number = number |
| max_cardinality_node = x |
| return max_cardinality_node |
| |
| |
| def _find_chordality_breaker(G,s=None,treewidth_bound=sys.maxsize): |
| """ Given a graph G, starts a max cardinality search |
| (starting from s if s is given and from a random node otherwise) |
| trying to find a non-chordal cycle. |
| |
| If it does find one, it returns (u,v,w) where u,v,w are the three |
| nodes that together with s are involved in the cycle. |
| """ |
| |
| unnumbered = set(G) |
| if s is None: |
| s = random.choice(list(unnumbered)) |
| unnumbered.remove(s) |
| numbered = set([s]) |
| # current_treewidth = None |
| current_treewidth = -1 |
| while unnumbered:# and current_treewidth <= treewidth_bound: |
| v = _max_cardinality_node(G,unnumbered,numbered) |
| unnumbered.remove(v) |
| numbered.add(v) |
| clique_wanna_be = set(G[v]) & numbered |
| sg = G.subgraph(clique_wanna_be) |
| if _is_complete_graph(sg): |
| # The graph seems to be chordal by now. We update the treewidth |
| current_treewidth = max(current_treewidth,len(clique_wanna_be)) |
| if current_treewidth > treewidth_bound: |
| raise nx.NetworkXTreewidthBoundExceeded(\ |
| "treewidth_bound exceeded: %s"%current_treewidth) |
| else: |
| # sg is not a clique, |
| # look for an edge that is not included in sg |
| (u,w) = _find_missing_edge(sg) |
| return (u,v,w) |
| return () |
| |
| |
| |
| def _connected_chordal_graph_cliques(G): |
| """Return the set of maximal cliques of a connected chordal graph.""" |
| if G.number_of_nodes() == 1: |
| x = frozenset(G.nodes()) |
| return set([x]) |
| else: |
| cliques = set() |
| unnumbered = set(G.nodes()) |
| v = random.choice(list(unnumbered)) |
| unnumbered.remove(v) |
| numbered = set([v]) |
| clique_wanna_be = set([v]) |
| while unnumbered: |
| v = _max_cardinality_node(G,unnumbered,numbered) |
| unnumbered.remove(v) |
| numbered.add(v) |
| new_clique_wanna_be = set(G.neighbors(v)) & numbered |
| sg = G.subgraph(clique_wanna_be) |
| if _is_complete_graph(sg): |
| new_clique_wanna_be.add(v) |
| if not new_clique_wanna_be >= clique_wanna_be: |
| cliques.add(frozenset(clique_wanna_be)) |
| clique_wanna_be = new_clique_wanna_be |
| else: |
| raise nx.NetworkXError("Input graph is not chordal.") |
| cliques.add(frozenset(clique_wanna_be)) |
| return cliques |
| |
| |
| |
| |