Repository: networkx/networkx

Issue:
Create bridges.py
Contains three simple functions for detection and retrieval of bridges or local bridges in a undirected network.



Discussion:
Thanks for this!
Could you please add tests and reformat your code to PEP8 specs.

@rmsyed Thanks for your contribution! Before you spend the time to reformat your code, I'd like to propose implementing the bridge-finding algorithm given in the Wikipedia article, https://en.wikipedia.org/wiki/Bridge_(graph_theory), which is linear time as opposed to the cubic time of the proposed implementation here. Are interested or able to do that?

Hi guys,

Okay, so I've updated the bridges.py file to implement Tarjan's algorithm and I've also updated it to include examples for each function. I ran the pep8 check on the code and it fully validated.

The problem is that when I commit it, the Travis CI build gives an error for the bridges_exist() function's examples. I'm not sure why considering that it works perfectly fine when I run it locally and considering that all the other examples in the code seem to work. Does anyone have an idea why this is happening?

Thanks.

When you run your tests, make sure you're doing `nosetests --with-doctest`. Also, your doctest output is different between Python 2 and Python 3 because `print` became a function. Because of how the REPL works, you don't need the `print`s—just put the expression you want evaluated on the line.

This is a really useful feature that you've implemented. I notice that `all_local_bridges()` is iterating over the edges, so it's cleaner to do that like this:

``` python
    bridges = {}
    for e in G.edges():
        G.remove_edge(*e)
        try:
            (u, v) = e           
            path_length = nx.shortest_path_length(G, u, v)
        except nx.NetworkXNoPath:
            bridges[e] = -1  # found a bridge
            if first_match:
                return bridges
        else:
            if path_length > 2:  # found a local bridge
                bridges[e] = path_length
                if first_match:
                    return bridges
        finally:
            G.add_edge(*e)
    return bridges
```

You can also avoid global variables if you alter `all_bridges()` to use an inner function:

``` python
def bridges(G):
    """ Looks through the graph object `G` for all bridges.

    We formally define a bridge to be any edge `(u, v)` such that the removal
    of the edge increases the total number of connected components.

    Parameters
    ----------
    G : Undirected Graph object

    Returns
    ----------
    iterable
        Edges that are bridges

    Examples
    --------
    >>> G = nx.cycle_graph(5)
    >>> bridges = bridges(G)
    >>> list(bridges)
    []
    >>> G.remove_edge(0,1)
    >>> list(bridges(G))
    [(2, 1), (3, 2), (4, 3), (0, 4)]

    Notes
    ----------
    This function can be useful to quickly determine what bridges exist
    in a given network. We use an implemenation of Tarjan's Bridge-finding
    algorithm to do this.

    The algorithm is described in [1]_.

    References 
    ----------
    .. [1] R. Endre Tarjan, 
        "A note on finding the bridges of a graph", *Information Processing 
        Letters*, **2** (6): 160--161

    """

    visited = set()
    depths = {}
    low = {}
    parent = {node: None for node in G}

    def bridge_util(u, depth):
        visited.add(u)
        depth += 1
        depths[u] = low[u] = depth
        for v in G[u]:
            if v not in visited:
                parent[v] = u
                for e in bridge_util(v, depth):
                    yield e
                # Check if subtree rooted at v has connection
                # to an ancestor of u.
                low[u] = min(low[u], low[v])
                # If the lowest vertex reachable from the subtree under v
                # is below u in the DFS tree, then `u-v` is a bridge.
                if low[v] > depths[u]:
                    yield (u, v)
            elif v != parent[u]:
                low[u] = min(low[u], depths[v])

    for u in G:
        if u not in visited:
            for e in bridge_util(u, depth=0):
                yield e
```

This version is a generator, so it would make your `bridges_exist()` function even faster.

Hi @rmsyed—looking through the Travis logs, your problem is here:

```
      File "/home/travis/virtualenv/pypy3-2.4.0/site-packages/networkx/algorithms/bridges.py", line 217, in all_bridges
              startnode = allnodes[0]
    TypeError: 'generator' object is not subscriptable
```

The comment I posted above circumvents that, after which your build should pass.

Thanks Arya! Those look like much cleaner implementations and likely more efficient. I made some small tweaks to the code that would ensure that the bridge-finding algorithm ran on all connected components in the network (before it was only running on one). However, it still seems that every time I update and commit the code, the Travis build fails on the same issue.

**Edit:** Okay, I just saw your newest comment. I'll try to update the code with your suggestion, although I still don't understand why this issue arises.

I removed the print statements as suggested and ran the doctest locally and, at least for Python 2.7, it didn't give any issues (I've attached the output here). So I'm still not sure why its giving errors on the github build.
[doctest_trace.txt](https://github.com/networkx/networkx/files/517339/doctest_trace.txt)

Hey @rmsyed—Travis is comparing against the dev version of networkx, but I bet that when you run `import networkx as nx`, it gets your local version of networkx, which is (probably) the older 1.11.

A lot of the main library functions are now generators. One example is `G.nodes()`. Now that it's a generator, you can duplicate the functionality you want with `next(allnodes)`, which gets the next (in this case, first) item from the generator. There's actually a function in `nx.utils` called `arbitrary_element`, which uses that pattern while making your intent a bit clearer.

Of course, you can sidestep that whole process with the algorithm posted above.

One more thought: since `all_local_bridges()` may return just the first match, I might rename it `local_bridges()`. For symmetry, I'd rename `all_bridges()` to `bridges()`. Again, I'm really impressed that you tackled something missing from the codebase. It's always scary to venture forth in new directions.

Looks like this isn't the first PR of Tarjan's bridge-finding algorithm. See the discussion at #903. 

Oh, good catch! Was that code ever implemented in a public release of networkx? I couldn't find a bridge function on the networkx public documentation for a `bridges()` function. If it has been implemented, perhaps it would be good to close this pull request or maybe consider narrowing the scope to only the local bridges function.

Can you adjust your pull request to create a `bridges()` function that is a wrapper around the existing code that's already in NetworkX? As discussed on the linked pull request above, see the `networkx/algorithms/components/biconnected.py` module. Note that module is for determining 2-node-connectivity, not 2-edge-connectivity.

Ok, I've modified the code for the `bridges()` function to use the `biconnected_components()` function to find all bridges.

Edit: I just realized the notes in the latest commit were the notes from an earlier commit. I'll update those comments accordingly, but first I want to make sure the code is suitable and appropriate.

The `local_bridges` function is needlessly complicated. From my understanding based on the definition you've provided for "local bridge", an edge is a "local bridge" if its two incident nodes do not share a common neighbor. So I think you could do this (assuming a graph with no self-loops):

``` python
def local_bridges(G):
    for u, v in G.edges():
        if not (set(G[u]) & set(G[v])):
            yield u, v
```

Is my understanding correct?

Yes, the definition is right and your code does detect local bridges but it doesn't return the span of those bridges which is often important to know. The original function was doing both but I suppose we could use your code, modified slightly to also compute the span. I do agree that your version is simpler although I think the worst case time complexity is the same once we include the code for computing the spans. I'll modify the code accordingly

Oh, I see, you are also returning the distance between the two endpoints when the edge is removed. Well how about something like

``` python
def local_bridges(G):
    for u, v in G.edges():
        if not (set(G[u]) & set(G[v])):
            H = G.copy()
            H.remove_edge(u, v)
            try:
                d = nx.shortest_path_length(H, u, v)
            except NetworkXNoPath:
                d = float('inf')
            yield u, v, d
```

If you need some kind of dictionary, you can do

``` python
from collections import defaultdict
distances = defaultdict(dict)
for u, v, d in local_bridges(G):
    distances[u][v] = distances[v][u] = d
```

It would be nice if there were a faster algorithm for this. Do you have any references for these algorithms, or a reference for local bridges, with which I am not familiar?

Your update looks good although I think it might be more efficient to simply remove the edge, check the distance, and then add the edge back rather than creating an entire copy each time. Also I think simply returning -1 for infinite distance might be suitable since span is always an integer. So this might look something like:

```
def local_bridges(G):
    for u, v in G.edges():
        if not (set(G[u]) & set(G[v])):
            G.remove_edge(u, v)
            try:
                d = nx.shortest_path_length(G, u, v)
                G.add_edge(u,v)
            except NetworkXNoPath:
                d = -1
                G.add_edge(u,v)
            yield u, v, d
```

As far as I'm aware, the algorithm for finding all local bridges is linear in O(V+E) and the algorithm for finding distance from some node `u` to `v` is linear in O(V+E) so the algorithm will be quadratic time. It would be interesting if there was a faster solution.

The concept of local bridges was first (I think) introduced by Mark Granovetter in his paper "The Strength of Weak Ties" (1973) on pages 1364-1365. He uses the term "degree" instead of span but Easley and Kleinberg in their book "Networks, Crowds and Markets" (2010) use the term "span" (page 52). I prefer to call it span because degree is an ambiguous term which could also refer to simply the number of edges on a node.

You can find Easley and Kleinberg's pdf book for free on their site:
http://www.cs.cornell.edu/home/kleinber/networks-book/

The reason I suggested using a copy is so that we do not have to modify the
user's graph directly. This is something of a convention in NetworkX
functions (at least, that's what it seems to me). This is both slow and
memory-intensive, yes, but it doesn't perform any unexpected modification
of the user's data, and a less memory-hungry version can be implemented
directly by the user if needed.

As for the running time, it is quadratic if the graph is sparse, but if the
graph is dense, the running time is quartic (!), which is particularly
unpleasant for large graphs.

You can increase the efficiency by only copying the user's graph once, outside of the loop over edges:

``` python
def local_bridges(G):
    H = G.copy(with_data=False)
    for u, v in G.edges():
        ...
```

Then make sure to re-add the edge after removing it. 

You can avoid copying the graph and also avoid removing the edge by "hiding" the edge using a weight function in the shortest path routine.  The weight function would be something like:

```
enodes=set((u,v))
weight = lambda n, nbr, d: 1 if (n not in enodes or nbr not in enodes) else None
```

Then replace `shortest_path_length(G,u,v)` with `shortest_path_length(G,u,v,weight)`


Resolving patch:
diff --git a/networkx/algorithms/bridges.py b/networkx/algorithms/bridges.py
--- a/networkx/algorithms/bridges.py
+++ b/networkx/algorithms/bridges.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 # bridges.py - bridge-finding algorithms
 #
-# Copyright 2004-2016 NetworkX developers.
+# Copyright 2004-2017 NetworkX developers.
 #
 # This file is part of NetworkX.
 #
@@ -13,7 +13,7 @@
 import networkx as nx
 from networkx.utils import not_implemented_for
 
-__all__ = ['bridges', 'has_bridges']
+__all__ = ['bridges', 'has_bridges', 'local_bridges']
 
 
 @not_implemented_for('multigraph')
@@ -121,3 +121,61 @@ def has_bridges(G, root=None):
         return False
     else:
         return True
+
+
+@not_implemented_for('multigraph')
+@not_implemented_for('directed')
+def local_bridges(G, with_span=True, weight=None):
+    """Iterate over local bridges of `G` optionally computing the span
+
+    A *local bridge* is an edge whose endpoints have no common neighbors.
+    That is, the edge is not part of a triangle in the graph.
+
+    The *span* of a *local bridge* is the shortest path length between
+    the endpoints if the local bridge is removed.
+
+    Parameters
+    ----------
+    G : undirected graph
+
+    with_span : bool
+        If True, yield a 3-tuple `(u, v, span)`
+
+    weight : function, string or None (default: None)
+        If function, used to compute edge weights for the span.
+        If string, the edge data attribute used in calculating span.
+        If None, all edges have weight 1.
+
+    Yields
+    ------
+    e : edge
+        The local bridges as an edge 2-tuple of nodes `(u, v)` or
+        as a 3-tuple `(u, v, span)` when `with_span is True`.
+
+    Examples
+    --------
+    A cycle graph has every edge a local bridge with span N-1.
+
+       >>> G = nx.cycle_graph(9)
+       >>> (0, 8, 8) in set(nx.local_bridges(G))
+       True
+    """
+    if with_span is not True:
+        for u, v in G.edges:
+            if not (set(G[u]) & set(G[v])):
+                yield u, v
+    else:
+        wt = nx.weighted._weight_function(G, weight)
+        for u, v in G.edges:
+            if not (set(G[u]) & set(G[v])):
+                enodes = {u, v}
+                def hide_edge(n, nbr, d):
+                    if n not in enodes or nbr not in enodes:
+                        return wt(n, nbr, d)
+                    return None
+
+                try:
+                    span = nx.shortest_path_length(G, u, v, weight=hide_edge)
+                    yield u, v, span
+                except nx.NetworkXNoPath:
+                    yield u, v, float('inf')


Test patch:
diff --git a/networkx/algorithms/tests/test_bridges.py b/networkx/algorithms/tests/test_bridges.py
--- a/networkx/algorithms/tests/test_bridges.py
+++ b/networkx/algorithms/tests/test_bridges.py
@@ -1,6 +1,6 @@
 # test_bridges.py - unit tests for bridge-finding algorithms
 #
-# Copyright 2004-2016 NetworkX developers.
+# Copyright 2004-2017 NetworkX developers.
 #
 # This file is part of NetworkX.
 #
@@ -8,6 +8,7 @@
 # information.
 """Unit tests for bridge-finding algorithms."""
 from unittest import TestCase
+from nose.tools import assert_equal, assert_in
 
 import networkx as nx
 
@@ -34,3 +35,39 @@ def test_barbell_graph(self):
         source = 0
         bridges = list(nx.bridges(G, source))
         self.assertEqual(bridges, [(2, 3)])
+
+
+class TestLocalBridges(TestCase):
+    """Unit tests for the local_bridge function."""
+
+    def setUp(self):
+        self.BB = nx.barbell_graph(4, 0)
+        self.square = nx.cycle_graph(4)
+        self.tri = nx.cycle_graph(3)
+
+    def test_nospan(self):
+        expected = {(3, 4), (4, 3)}
+        assert_in(next(nx.local_bridges(self.BB, with_span=False)), expected)
+        assert_equal(set(nx.local_bridges(self.square, with_span=False)), self.square.edges)
+        assert_equal(list(nx.local_bridges(self.tri, with_span=False)), [])
+
+    def test_no_weight(self):
+        inf = float('inf')
+        expected = {(3, 4, inf), (4, 3, inf)}
+        assert_in(next(nx.local_bridges(self.BB)), expected)
+        expected = {(u, v, 3) for u, v, in self.square.edges}
+        assert_equal(set(nx.local_bridges(self.square)), expected)
+        assert_equal(list(nx.local_bridges(self.tri)), [])
+        
+    def test_weight(self):
+        inf = float('inf')
+        G = self.square.copy()
+
+        G.edges[1, 2]['weight'] = 2
+        expected = {(u, v, 5 - wt) for u, v, wt in G.edges(data='weight', default=1)}
+        assert_equal(set(nx.local_bridges(G, weight='weight')), expected)
+
+        expected = {(u, v, 6) for u, v in G.edges}
+        lb = nx.local_bridges(G, weight=lambda u, v, d: 2)
+        assert_equal(set(lb), expected)
+