~tfardet/NNGT

d3979a1526793df2419368675f3f21a1e7556938 — Tanguy Fardet 5 months ago 7f5340b
Core - Connectedness with default backend

Make connected_components and is_connected work with default nngt backend.
3 files changed, 181 insertions(+), 6 deletions(-)

M nngt/analysis/nngt_functions.py
M nngt/core/nngt_graph.py
M testing/library_compatibility.py
M nngt/analysis/nngt_functions.py => nngt/analysis/nngt_functions.py +147 -0
@@ 23,6 23,8 @@

""" Tools to analyze graphs with the nngt backend """

from collections import deque

import numpy as np
import scipy.sparse as ssp



@@ 79,3 81,148 @@ def reciprocity(g):
    num_recip = sum((1 if e[::-1] in g._edges else 0 for e in g._edges))

    return num_recip / num_edges


def connected_components(g, ctype=None):
    '''
    Returns the connected component to which each node belongs.

    .. versionadded:: 2.0

    Parameters
    ----------
    g : :class:`~nngt.Graph`
        Graph to analyze.
    ctype : str, optional (default 'scc')
        Type of component that will be searched: either strongly connected
        ('scc', by default) or weakly connected ('wcc').

    Returns
    -------
    cc, hist : :class:`numpy.ndarray`
        The component associated to each node (`cc`) and the number of nodes in
        each of the component (`hist`).

    References
    ----------
    .. [gt-cc] :gtdoc:`topology.label_components`
    .. [ig-cc] :igdoc:`clusters`
    .. [nx-ucc] :nxdoc:`algorithms.components.connected_components`
    .. [nx-scc] :nxdoc:`algorithms.components.strongly_connected_components`
    .. [nx-wcc] :nxdoc:`algorithms.components.weakly_connected_components`
    '''
    ctype = "scc" if ctype is None else ctype

    num_nodes = g.node_nb()

    all_nodes = set(g.get_nodes())

    all_seen = set()

    components = []

    # get adjacency matrix
    A = g.adjacency_matrix()

    if ctype == "wcc" and g.is_directed():
        A = A + A.T

    start_nodes = []

    while len(all_seen) < num_nodes:
        start = next(iter(all_nodes.difference(all_seen)))

        start_nodes.append(start)

        visited = _dfs(A, start)

        all_seen.update(visited)

        components.append(visited)

    # do reverse search
    if ctype == "scc" and g.is_directed():
        A = A.T

        count = 0

        components2 = []

        all_seen = set()

        while len(all_seen) < num_nodes:
            start = start_nodes[count] if count < len(start_nodes) - 1 \
                    else next(iter(all_nodes.difference(all_seen)))

            visited = _dfs(A, start)

            all_seen.update(visited)

            components2.append(visited)

            count += 1

        # generate sccs
        components1 = components.copy()

        components = []

        # sort components by size
        order1 = np.argsort([len(s) for s in components1])
        order2 = np.argsort([len(s) for s in components2])

        for i in order1:
            s1 = components1[i]

            if len(s1) == 1:
                components.append(s1)
                for j in order1:
                    components1[j] = components1[j].difference(s1)

                for j in order2:
                    components2[j] = components2[j].difference(s1)
            else:
                for j in order2:
                    s2 = components2[j]

                    intsct = s1.intersection(s2)

                    if intsct:
                        components.append(intsct)

                        for k in order1:
                            components1[k] = components1[k].difference(intsct)

                        for k in order2:
                            components2[k] = components2[k].difference(intsct)

    # make labels and histogram
    labels = np.zeros(num_nodes, dtype=int)

    hist = np.array([len(s) for s in components], dtype=int)

    order = np.argsort(hist)[::-1]

    for i, s in zip(order, components):
        labels[list(s)] = i

    return labels, hist[order]


def _dfs(adjacency, start):
    '''
    Depth-first search returning all nodes that can be reached from a
    '''
    todo  = deque([start])

    visited = {start}

    while todo:
        n = todo.popleft()

        neighbours = np.where(adjacency.getrow(n).todense().A1)[0]
        todo.extend([v for v in neighbours if v not in visited])

        visited.update(neighbours)

    return visited

M nngt/core/nngt_graph.py => nngt/core/nngt_graph.py +30 -4
@@ 31,6 31,7 @@ import numpy as np
from scipy.sparse import coo_matrix, lil_matrix

import nngt
from nngt.analysis.nngt_functions import _dfs
from nngt.lib import InvalidArgument, nonstring_container, is_integer
from nngt.lib.connect_tools import (_cleanup_edges, _set_dist_new_edges,
                                    _set_default_edge_attributes)


@@ 480,10 481,35 @@ class _NNGTGraph(GraphInterface):
        return [e for e in g._unique
                if e[0] in target_node or e[1] in target_node]

    def is_connected(self):
        raise NotImplementedError("Not available with 'nngt' backend, please "
                                  "install a graph library (networkx, igraph, "
                                  "or graph-tool).")
    def is_connected(self, mode="strong"):
        '''
        Return whether the graph is connected.

        Parameters
        ----------
        mode : str, optional (default: "strong")
            Whether to test connectedness with directed ("strong") or
            undirected ("weak") connections.
        '''
        num_nodes = g.node_nb()

        # get adjacency matrix
        A = g.adjacency_matrix()

        if mode == "weak" and g.is_directed():
            A = A + A.T

        visited = _dfs(A, 0)

        if len(visited) != num_nodes:
            return False
        elif mode == "strong" and g.is_directed():
            visited = _dfs(A.T, 0)

            if len(visited) != num_nodes:
                return False

        return True

    def new_node(self, n=1, neuron_type=1, attributes=None, value_types=None,
                 positions=None, groups=None):

M testing/library_compatibility.py => testing/library_compatibility.py +4 -2
@@ 20,6 20,8 @@ import nngt.analysis as na

backends = ["networkx", "igraph", "graph-tool"]

all_backends = ["networkx", "igraph", "graph-tool", "nngt"]


def test_weighted_undirected_clustering():
    '''


@@ 63,7 65,7 @@ def test_weighted_undirected_clustering():
    gc_barrat = np.sum(np.multiply(barrat, triplets)) / np.sum(triplets)

    # check for all backends
    for bckd in ["networkx", "igraph", "graph-tool", "nngt"]:
    for bckd in all_backends:
        nngt.set_config("backend", bckd)

        g = nngt.Graph(nodes=num_nodes, directed=False)


@@ 352,7 354,7 @@ def test_components():

    edge_list.append((7, 3))

    for bckd in backends:
    for bckd in all_backends:
        nngt.set_config("backend", bckd)

        g = nngt.Graph(nodes=num_nodes, directed=True)