|
|
""" |
|
|
Moody and White algorithm for k-components |
|
|
""" |
|
|
from collections import defaultdict |
|
|
from itertools import combinations |
|
|
from operator import itemgetter |
|
|
|
|
|
import networkx as nx |
|
|
|
|
|
|
|
|
from networkx.algorithms.flow import edmonds_karp |
|
|
from networkx.utils import not_implemented_for |
|
|
|
|
|
default_flow_func = edmonds_karp |
|
|
|
|
|
__all__ = ["k_components"] |
|
|
|
|
|
|
|
|
@not_implemented_for("directed") |
|
|
@nx._dispatch |
|
|
def k_components(G, flow_func=None): |
|
|
r"""Returns the k-component structure of a graph G. |
|
|
|
|
|
A `k`-component is a maximal subgraph of a graph G that has, at least, |
|
|
node connectivity `k`: we need to remove at least `k` nodes to break it |
|
|
into more components. `k`-components have an inherent hierarchical |
|
|
structure because they are nested in terms of connectivity: a connected |
|
|
graph can contain several 2-components, each of which can contain |
|
|
one or more 3-components, and so forth. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : NetworkX graph |
|
|
|
|
|
flow_func : function |
|
|
Function to perform the underlying flow computations. Default value |
|
|
:meth:`edmonds_karp`. This function performs better in sparse graphs with |
|
|
right tailed degree distributions. :meth:`shortest_augmenting_path` will |
|
|
perform better in denser graphs. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
k_components : dict |
|
|
Dictionary with all connectivity levels `k` in the input Graph as keys |
|
|
and a list of sets of nodes that form a k-component of level `k` as |
|
|
values. |
|
|
|
|
|
Raises |
|
|
------ |
|
|
NetworkXNotImplemented |
|
|
If the input graph is directed. |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> # Petersen graph has 10 nodes and it is triconnected, thus all |
|
|
>>> # nodes are in a single component on all three connectivity levels |
|
|
>>> G = nx.petersen_graph() |
|
|
>>> k_components = nx.k_components(G) |
|
|
|
|
|
Notes |
|
|
----- |
|
|
Moody and White [1]_ (appendix A) provide an algorithm for identifying |
|
|
k-components in a graph, which is based on Kanevsky's algorithm [2]_ |
|
|
for finding all minimum-size node cut-sets of a graph (implemented in |
|
|
:meth:`all_node_cuts` function): |
|
|
|
|
|
1. Compute node connectivity, k, of the input graph G. |
|
|
|
|
|
2. Identify all k-cutsets at the current level of connectivity using |
|
|
Kanevsky's algorithm. |
|
|
|
|
|
3. Generate new graph components based on the removal of |
|
|
these cutsets. Nodes in a cutset belong to both sides |
|
|
of the induced cut. |
|
|
|
|
|
4. If the graph is neither complete nor trivial, return to 1; |
|
|
else end. |
|
|
|
|
|
This implementation also uses some heuristics (see [3]_ for details) |
|
|
to speed up the computation. |
|
|
|
|
|
See also |
|
|
-------- |
|
|
node_connectivity |
|
|
all_node_cuts |
|
|
biconnected_components : special case of this function when k=2 |
|
|
k_edge_components : similar to this function, but uses edge-connectivity |
|
|
instead of node-connectivity |
|
|
|
|
|
References |
|
|
---------- |
|
|
.. [1] Moody, J. and D. White (2003). Social cohesion and embeddedness: |
|
|
A hierarchical conception of social groups. |
|
|
American Sociological Review 68(1), 103--28. |
|
|
http://www2.asanet.org/journals/ASRFeb03MoodyWhite.pdf |
|
|
|
|
|
.. [2] Kanevsky, A. (1993). Finding all minimum-size separating vertex |
|
|
sets in a graph. Networks 23(6), 533--541. |
|
|
http://onlinelibrary.wiley.com/doi/10.1002/net.3230230604/abstract |
|
|
|
|
|
.. [3] Torrents, J. and F. Ferraro (2015). Structural Cohesion: |
|
|
Visualization and Heuristics for Fast Computation. |
|
|
https://arxiv.org/pdf/1503.04476v1 |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
k_components = defaultdict(list) |
|
|
|
|
|
if flow_func is None: |
|
|
flow_func = default_flow_func |
|
|
|
|
|
for component in nx.connected_components(G): |
|
|
|
|
|
comp = set(component) |
|
|
if len(comp) > 1: |
|
|
k_components[1].append(comp) |
|
|
bicomponents = [G.subgraph(c) for c in nx.biconnected_components(G)] |
|
|
for bicomponent in bicomponents: |
|
|
bicomp = set(bicomponent) |
|
|
|
|
|
if len(bicomp) > 2: |
|
|
k_components[2].append(bicomp) |
|
|
for B in bicomponents: |
|
|
if len(B) <= 2: |
|
|
continue |
|
|
k = nx.node_connectivity(B, flow_func=flow_func) |
|
|
if k > 2: |
|
|
k_components[k].append(set(B)) |
|
|
|
|
|
cuts = list(nx.all_node_cuts(B, k=k, flow_func=flow_func)) |
|
|
stack = [(k, _generate_partition(B, cuts, k))] |
|
|
while stack: |
|
|
(parent_k, partition) = stack[-1] |
|
|
try: |
|
|
nodes = next(partition) |
|
|
C = B.subgraph(nodes) |
|
|
this_k = nx.node_connectivity(C, flow_func=flow_func) |
|
|
if this_k > parent_k and this_k > 2: |
|
|
k_components[this_k].append(set(C)) |
|
|
cuts = list(nx.all_node_cuts(C, k=this_k, flow_func=flow_func)) |
|
|
if cuts: |
|
|
stack.append((this_k, _generate_partition(C, cuts, this_k))) |
|
|
except StopIteration: |
|
|
stack.pop() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return _reconstruct_k_components(k_components) |
|
|
|
|
|
|
|
|
def _consolidate(sets, k): |
|
|
"""Merge sets that share k or more elements. |
|
|
|
|
|
See: http://rosettacode.org/wiki/Set_consolidation |
|
|
|
|
|
The iterative python implementation posted there is |
|
|
faster than this because of the overhead of building a |
|
|
Graph and calling nx.connected_components, but it's not |
|
|
clear for us if we can use it in NetworkX because there |
|
|
is no licence for the code. |
|
|
|
|
|
""" |
|
|
G = nx.Graph() |
|
|
nodes = dict(enumerate(sets)) |
|
|
G.add_nodes_from(nodes) |
|
|
G.add_edges_from( |
|
|
(u, v) for u, v in combinations(nodes, 2) if len(nodes[u] & nodes[v]) >= k |
|
|
) |
|
|
for component in nx.connected_components(G): |
|
|
yield set.union(*[nodes[n] for n in component]) |
|
|
|
|
|
|
|
|
def _generate_partition(G, cuts, k): |
|
|
def has_nbrs_in_partition(G, node, partition): |
|
|
return any(n in partition for n in G[node]) |
|
|
|
|
|
components = [] |
|
|
nodes = {n for n, d in G.degree() if d > k} - {n for cut in cuts for n in cut} |
|
|
H = G.subgraph(nodes) |
|
|
for cc in nx.connected_components(H): |
|
|
component = set(cc) |
|
|
for cut in cuts: |
|
|
for node in cut: |
|
|
if has_nbrs_in_partition(G, node, cc): |
|
|
component.add(node) |
|
|
if len(component) < G.order(): |
|
|
components.append(component) |
|
|
yield from _consolidate(components, k + 1) |
|
|
|
|
|
|
|
|
def _reconstruct_k_components(k_comps): |
|
|
result = {} |
|
|
max_k = max(k_comps) |
|
|
for k in reversed(range(1, max_k + 1)): |
|
|
if k == max_k: |
|
|
result[k] = list(_consolidate(k_comps[k], k)) |
|
|
elif k not in k_comps: |
|
|
result[k] = list(_consolidate(result[k + 1], k)) |
|
|
else: |
|
|
nodes_at_k = set.union(*k_comps[k]) |
|
|
to_add = [c for c in result[k + 1] if any(n not in nodes_at_k for n in c)] |
|
|
if to_add: |
|
|
result[k] = list(_consolidate(k_comps[k] + to_add, k)) |
|
|
else: |
|
|
result[k] = list(_consolidate(k_comps[k], k)) |
|
|
return result |
|
|
|
|
|
|
|
|
def build_k_number_dict(kcomps): |
|
|
result = {} |
|
|
for k, comps in sorted(kcomps.items(), key=itemgetter(0)): |
|
|
for comp in comps: |
|
|
for node in comp: |
|
|
result[node] = k |
|
|
return result |
|
|
|