Repository: networkx/networkx

Issue:
Approximated Metrics - Diameter
Hi all,
In some cases, especially for large graphs, an exact result for a metric might not be necessary and a bound could be good enough.  
This might be the case for the *diameter*, which currently on networkx is present only as an [exact metric](https://networkx.org/documentation/stable//reference/algorithms/generated/networkx.algorithms.distance_measures.diameter.html).  

I think it might be worth to add functionalities to computed an approximated diameter (i.e., a lower bound).  
Good candidates might be the *2-sweep* [[1]](#1) and *2d-sweep* [[2]](#2)  for the undirected and directed case, respectively. 

In addition to the results on the paper, here some comparisons made on my laptop (2.9 GHz Dual-Core Intel Core i5) with a draft implementation of the *2-sweep* on undirected graphs considering the [Twitch Snap Dataset](http://snap.stanford.edu/data/twitch-social-networks.html).  
Time is expressed in seconds. The code used can be found [here](https://gist.github.com/atomassi/3ee4d827afc6f7f20e3a70bba97ed697).


| network    	| \|V\| 	|  \|E\|  	| Time (exact) 	| Value (exact) 	| Time (LB) 	| Value (LB) 	|
|------------	|:-----:	|:-------:	|:------------:	|:-------------:	|:---------:	|:----------:	|
| musae_ENGB 	| 7,126 	|  35,324 	|     166.7    	|       10      	|    0.06   	|     10     	|
| musae_RU   	| 4,385 	|  37,304 	|     63.2     	|       9       	|    0.03   	|      9     	|
| musae_FR   	| 6,549 	| 112,666 	|     255.5    	|       7       	|    0.11   	|      7     	|
| musae_DE   	| 9,498 	| 153,138 	|     602.7    	|       7       	|    0.18   	|      7     	|
| musae_ES   	| 4,648 	|  59,382 	|     101.9    	|       9       	|    0.07   	|      8     	|
| musae_PTBR 	| 1,912 	|  31,299 	|     17.5     	|       7       	|    0.02   	|      7     	|


If you think it might make sense to add this metric for the 2 cases, I can add a module (e.g., `networkx/algorithms/approximation/distance_measures.py`) with a proposed implementation and the tests for it.

## References
<a id="1">[1]</a> 
Magnien, C., Latapy, M. and Habib, M., 2009. *Fast computation of empirically tight bounds for the diameter of massive graphs*. Journal of Experimental Algorithmics (JEA), 13, pp.1-10.
<a id="1">[2]</a>
Crescenzi, P., Grossi, R., Lanzi, L. and Marino, A., 2012, June. *On computing the diameter of real-world directed (weighted) graphs*. In International Symposium on Experimental Algorithms (pp. 99-110). Springer, Berlin, Heidelberg. 


Discussion:
This looks interesting, and you have chosen the correct module place and name. It would be great if you could put together a PR. Hook into the documentation with an entry at doc/reference/algorithms/approximation.rst. Hook into the namespace with an entry in `__init__.py`. If you have questions, let us know.

Resolving patch:
diff --git a/networkx/algorithms/approximation/__init__.py b/networkx/algorithms/approximation/__init__.py
--- a/networkx/algorithms/approximation/__init__.py
+++ b/networkx/algorithms/approximation/__init__.py
@@ -12,6 +12,7 @@
 from networkx.algorithms.approximation.clustering_coefficient import *
 from networkx.algorithms.approximation.clique import *
 from networkx.algorithms.approximation.connectivity import *
+from networkx.algorithms.approximation.distance_measures import *
 from networkx.algorithms.approximation.dominating_set import *
 from networkx.algorithms.approximation.kcomponents import *
 from networkx.algorithms.approximation.independent_set import *
diff --git a/networkx/algorithms/approximation/distance_measures.py b/networkx/algorithms/approximation/distance_measures.py
new file mode 100644
--- /dev/null
+++ b/networkx/algorithms/approximation/distance_measures.py
@@ -0,0 +1,140 @@
+"""Distance measures approximated metrics."""
+
+import networkx as nx
+from networkx.utils.decorators import py_random_state
+
+__all__ = ["diameter"]
+
+
+@py_random_state(1)
+def diameter(G, seed=None):
+    """Returns a lower bound on the diameter of the graph G.
+
+    The function computes a lower bound on the diameter (i.e., the maximum eccentricity)
+    of a directed or undirected graph G. The procedure used varies depending on the graph
+    being directed or not.
+
+    If G is an `undirected` graph, then the function uses the `2-sweep` algorithm [1]_.
+    The main idea is to pick the farthest node from a random node and return its eccentricity.
+
+    Otherwise, if G is a `directed` graph, the function uses the `2-dSweep` algorithm [2]_,
+    The procedure starts by selecting a random source node $s$ from which it performs a
+    forward and a backward BFS. Let $a_1$ and $a_2$ be the farthest nodes in the forward and
+    backward cases, respectively. Then, it computes the backward eccentricity of $a_1$ using
+    a backward BFS and the forward eccentricity of $a_2$ using a forward BFS.
+    Finally, it returns the best lower bound between the two.
+
+    In both cases, the time complexity is linear with respect to the size of G.
+
+    Parameters
+    ----------
+    G : NetworkX graph
+
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
+
+    Returns
+    -------
+    d : integer
+       Lower Bound on the Diameter of G
+
+    Raises
+    ------
+    NetworkXError
+        If the graph is empty or
+        If the graph is undirected and not connected or
+        If the graph is directed and not strongly connected.
+
+    See Also
+    --------
+    networkx.algorithms.distance_measures.diameter
+
+    References
+    ----------
+    .. [1] Magnien, Clémence, Matthieu Latapy, and Michel Habib.
+       *Fast computation of empirically tight bounds for the diameter of massive graphs.*
+       Journal of Experimental Algorithmics (JEA), 2009.
+       https://arxiv.org/pdf/0904.2728.pdf
+    .. [2] Crescenzi, Pierluigi, Roberto Grossi, Leonardo Lanzi, and Andrea Marino.
+       *On computing the diameter of real-world directed (weighted) graphs.*
+       International Symposium on Experimental Algorithms. Springer, Berlin, Heidelberg, 2012.
+       https://courses.cs.ut.ee/MTAT.03.238/2014_fall/uploads/Main/diameter.pdf
+    """
+    # if G is empty
+    if not G:
+        raise nx.NetworkXError("Expected non-empty NetworkX graph!")
+    # if there's only a node
+    if G.number_of_nodes() == 1:
+        return 0
+    # if G is directed
+    if G.is_directed():
+        return _two_sweep_directed(G, seed)
+    # else if G is undirected
+    return _two_sweep_undirected(G, seed)
+
+
+def _two_sweep_undirected(G, seed):
+    """Helper function for finding a lower bound on the diameter
+        for undirected Graphs.
+
+        The idea is to pick the farthest node from a random node
+        and return its eccentricity.
+
+        ``G`` is a NetworkX undirected graph.
+
+    .. note::
+
+        ``seed`` is a random.Random or numpy.random.RandomState instance
+    """
+    # select a random source node
+    source = seed.choice(list(G))
+    # get the distances to the other nodes
+    distances = nx.shortest_path_length(G, source)
+    # if some nodes have not been visited, then the graph is not connected
+    if len(distances) != len(G):
+        raise nx.NetworkXError("Graph not connected.")
+    # take a node that is (one of) the farthest nodes from the source
+    *_, node = distances
+    # return the eccentricity of the node
+    return nx.eccentricity(G, node)
+
+
+def _two_sweep_directed(G, seed):
+    """Helper function for finding a lower bound on the diameter
+        for directed Graphs.
+
+        It implements 2-dSweep, the directed version of the 2-sweep algorithm.
+        The algorithm follows the following steps.
+        1. Select a source node $s$ at random.
+        2. Perform a forward BFS from $s$ to select a node $a_1$ at the maximum
+        distance from the source, and compute $LB_1$, the backward eccentricity of $a_1$.
+        3. Perform a backward BFS from $s$ to select a node $a_2$ at the maximum
+        distance from the source, and compute $LB_2$, the forward eccentricity of $a_2$.
+        4. Return the maximum between $LB_1$ and $LB_2$.
+
+        ``G`` is a NetworkX directed graph.
+
+    .. note::
+
+        ``seed`` is a random.Random or numpy.random.RandomState instance
+    """
+    # get a new digraph G' with the edges reversed in the opposite direction
+    G_reversed = G.reverse()
+    # select a random source node
+    source = seed.choice(list(G))
+    # compute forward distances from source
+    forward_distances = nx.shortest_path_length(G, source)
+    # compute backward distances  from source
+    backward_distances = nx.shortest_path_length(G_reversed, source)
+    # if either the source can't reach every node or not every node
+    # can reach the source, then the graph is not strongly connected
+    n = len(G)
+    if len(forward_distances) != n or len(backward_distances) != n:
+        raise nx.NetworkXError("DiGraph not strongly connected.")
+    # take a node a_1 at the maximum distance from the source in G
+    *_, a_1 = forward_distances
+    # take a node a_2 at the maximum distance from the source in G_reversed
+    *_, a_2 = backward_distances
+    # return the max between the backward eccentricity of a_1 and the forward eccentricity of a_2
+    return max(nx.eccentricity(G_reversed, a_1), nx.eccentricity(G, a_2))


Test patch:
diff --git a/networkx/algorithms/approximation/tests/test_distance_measures.py b/networkx/algorithms/approximation/tests/test_distance_measures.py
new file mode 100644
--- /dev/null
+++ b/networkx/algorithms/approximation/tests/test_distance_measures.py
@@ -0,0 +1,59 @@
+"""Unit tests for the :mod:`networkx.algorithms.approximation.distance_measures` module.
+"""
+
+import pytest
+import networkx as nx
+from networkx.algorithms.approximation import diameter
+
+
+class TestDiameter:
+    """Unit tests for the approximate diameter function
+    :func:`~networkx.algorithms.approximation.distance_measures.diameter`.
+    """
+
+    def test_null_graph(self):
+        """Test empty graph."""
+        G = nx.null_graph()
+        with pytest.raises(
+            nx.NetworkXError, match="Expected non-empty NetworkX graph!"
+        ):
+            diameter(G)
+
+    def test_undirected_non_connected(self):
+        """Test an undirected disconnected graph."""
+        graph = nx.path_graph(10)
+        graph.remove_edge(3, 4)
+        with pytest.raises(nx.NetworkXError, match="Graph not connected."):
+            diameter(graph)
+
+    def test_directed_non_strongly_connected(self):
+        """Test a directed non strongly connected graph."""
+        graph = nx.path_graph(10, create_using=nx.DiGraph())
+        with pytest.raises(nx.NetworkXError, match="DiGraph not strongly connected."):
+            diameter(graph)
+
+    def test_complete_undirected_graph(self):
+        """Test a complete undirected graph."""
+        graph = nx.complete_graph(10)
+        assert diameter(graph) == 1
+
+    def test_complete_directed_graph(self):
+        """Test a complete directed graph."""
+        graph = nx.complete_graph(10, create_using=nx.DiGraph())
+        assert diameter(graph) == 1
+
+    def test_undirected_path_graph(self):
+        """Test an undirected path graph with 10 nodes."""
+        graph = nx.path_graph(10)
+        assert diameter(graph) == 9
+
+    def test_directed_path_graph(self):
+        """Test a directed path graph with 10 nodes."""
+        graph = nx.path_graph(10).to_directed()
+        assert diameter(graph) == 9
+
+    def test_single_node(self):
+        """Test a graph which contains just a node."""
+        graph = nx.Graph()
+        graph.add_node(1)
+        assert diameter(graph) == 0