Repository: networkx/networkx

Issue:
directed_edge_swap: ZeroDivisionError when passing a digraph without edges
I was working on improving test coverage for `swap.py` and tested `directed_edge_swap` with a digraph without edges. That resulted in a ZeroDivisionError from `nx.utils.cumulative_distribution`. 

### Steps to Reproduce

```python
from networkx as nx

G = nx.DiGraph()
G.add_nodes_from([0, 2, 4, 3])

G = nx.directed_edge_swap(G, nswap=1, max_tries=500, seed=1)
```
### Current Behavior

ZeroDivisionError is raised because in `nx.utils.cumulative_distribution` there's a division by the sum of the input distribution that is zero. The distribution that `nx.utils.cumulative_distribution` is called with in this example is the degrees of a digraph without edges (all degrees are zero). This is the error log. 

```
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    G = nx.directed_edge_swap(G, nswap=1, max_tries=500, seed=1)
  File "/home/paula/Outreachy/networkX/networkx/networkx/utils/decorators.py", line 766, in func
    return argmap._lazy_compile(__wrapper)(*args, **kwargs)
  File "<class 'networkx.utils.decorators.argmap'> compilation 21", line 5, in argmap_directed_edge_swap_17
  File "/home/paula/Outreachy/networkX/networkx/networkx/algorithms/swap.py", line 81, in directed_edge_swap
    cdf = nx.utils.cumulative_distribution(degrees)  # cdf of degree
  File "/home/paula/Outreachy/networkX/networkx/networkx/utils/random_sequence.py", line 103, in cumulative_distribution
    cdf.append(cdf[i] + distribution[i] / psum)
ZeroDivisionError: division by zero
```
### Expected Behavior

I think the best is to raise an exception in `directed_edge_swap` with a message that exposes this situation.  Also, this should be added to the [doc entry](https://output.circle-artifacts.com/output/job/ab098af6-f76c-4bf9-a732-5c243aca96da/artifacts/0/doc/build/html/reference/algorithms/generated/networkx.algorithms.swap.directed_edge_swap.html#directed-edge-swap). 

### Environment

Python version: 3.8
NetworkX version: 2.8.7

### Further Discoveries

Something else that should be added is that if the digraph has less than 3 edges then is impossible to swap edges. In that case, the current behavior is that the max number of tries is reached. In my opinion, this case should raise an exception with an appropriate message. Also, this could be mentioned in the  [doc entry](https://output.circle-artifacts.com/output/job/ab098af6-f76c-4bf9-a732-5c243aca96da/artifacts/0/doc/build/html/reference/algorithms/generated/networkx.algorithms.swap.directed_edge_swap.html#directed-edge-swap). 

I will work on these changes and will look closely at the other functions in `swap.py`. 
Also, I think it will be good to review the functions that use `nx.utils.cumulative_distribution` to check for this error. I can do that too. 



Discussion:
MridulS: @paulitapb just a ping to review the comments :)

Patch:
diff --git a/networkx/algorithms/swap.py b/networkx/algorithms/swap.py
index 9b2f06db6..c02a4b3ca 100644
--- a/networkx/algorithms/swap.py
+++ b/networkx/algorithms/swap.py
@@ -46,7 +46,7 @@ def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None):
     NetworkXError
         If `G` is not directed, or
         If nswap > max_tries, or
-        If there are fewer than 4 nodes in `G`
+        If there are fewer than 4 nodes or 3 edges in `G`.
     NetworkXAlgorithmError
         If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
 
@@ -70,7 +70,9 @@ def directed_edge_swap(G, *, nswap=1, max_tries=100, seed=None):
     if nswap > max_tries:
         raise nx.NetworkXError("Number of swaps > number of tries allowed.")
     if len(G) < 4:
-        raise nx.NetworkXError("Graph has less than four nodes.")
+        raise nx.NetworkXError("DiGraph has fewer than four nodes.")
+    if len(G.edges) < 3:
+        raise nx.NetworkXError("DiGraph has fewer than 3 edges")
 
     # Instead of choosing uniformly at random from a generated edge list,
     # this algorithm chooses nonuniformly from the set of nodes with
@@ -161,6 +163,15 @@ def double_edge_swap(G, nswap=1, max_tries=100, seed=None):
     G : graph
        The graph after double edge swaps.
 
+    Raises
+    ------
+    NetworkXError
+        If `G` is directed, or
+        If `nswap` > `max_tries`, or
+        If there are fewer than 4 nodes or 2 edges in `G`.
+    NetworkXAlgorithmError
+        If the number of swap attempts exceeds `max_tries` before `nswap` swaps are made
+
     Notes
     -----
     Does not enforce any connectivity constraints.
@@ -174,7 +185,9 @@ def double_edge_swap(G, nswap=1, max_tries=100, seed=None):
     if nswap > max_tries:
         raise nx.NetworkXError("Number of swaps > number of tries allowed.")
     if len(G) < 4:
-        raise nx.NetworkXError("Graph has less than four nodes.")
+        raise nx.NetworkXError("Graph has fewer than four nodes.")
+    if len(G.edges) < 2:
+        raise nx.NetworkXError("Graph has fewer than 2 edges")
     # Instead of choosing uniformly at random from a generated edge list,
     # this algorithm chooses nonuniformly from the set of nodes with
     # probability weighted by degree.
@@ -285,7 +298,7 @@ def connected_double_edge_swap(G, nswap=1, _window_threshold=3, seed=None):
     if not nx.is_connected(G):
         raise nx.NetworkXError("Graph not connected")
     if len(G) < 4:
-        raise nx.NetworkXError("Graph has less than four nodes.")
+        raise nx.NetworkXError("Graph has fewer than four nodes.")
     n = 0
     swapcount = 0
     deg = G.degree()


Test patch:
diff --git a/networkx/algorithms/tests/test_swap.py b/networkx/algorithms/tests/test_swap.py
index 2bc25df23..49dd5f8e8 100644
--- a/networkx/algorithms/tests/test_swap.py
+++ b/networkx/algorithms/tests/test_swap.py
@@ -19,9 +19,6 @@ def test_edge_cases_directed_edge_swap():
         "Maximum number of swap attempts \\(11\\) exceeded "
         "before desired swaps achieved \\(\\d\\)."
     )
-    graph = nx.DiGraph([(0, 1), (2, 3)])
-    with pytest.raises(nx.NetworkXAlgorithmError, match=e):
-        nx.directed_edge_swap(graph, nswap=4, max_tries=10, seed=1)
     graph = nx.DiGraph([(0, 0), (0, 1), (1, 0), (2, 3), (3, 2)])
     with pytest.raises(nx.NetworkXAlgorithmError, match=e):
         nx.directed_edge_swap(graph, nswap=1, max_tries=10, seed=1)
@@ -138,3 +135,22 @@ def test_degree_seq_c4():
     degrees = sorted(d for n, d in G.degree())
     G = nx.double_edge_swap(G, 1, 100)
     assert degrees == sorted(d for n, d in G.degree())
+
+
+def test_fewer_than_4_nodes():
+    G = nx.DiGraph()
+    G.add_nodes_from([0, 1, 2])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than four nodes."):
+        nx.directed_edge_swap(G)
+
+
+def test_less_than_3_edges():
+    G = nx.DiGraph([(0, 1), (1, 2)])
+    G.add_nodes_from([3, 4])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than 3 edges"):
+        nx.directed_edge_swap(G)
+
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2, 3])
+    with pytest.raises(nx.NetworkXError, match=".*fewer than 2 edges"):
+        nx.double_edge_swap(G)