Repository: networkx/networkx

Issue:
`generate_random_paths` does not have a `seed` argument
The `nx.generate_random_paths` iterator creates paths of specified length from `G`. This function is not seeded, so there is currently no way to reproduce results between runs.


Discussion:
@rossbar I had a quick go at implementing this - changing the signature to:
```python
@py_random_state(5)
@nx._dispatchable(edge_attrs="weight")
def generate_random_paths(
    G, sample_size, path_length=5, index_map=None, weight="weight", seed=None
):
```
The convention in the codebase seems to be to use the numpy-backed `random.Random` subclass `PythonRandomViaNumpyBits` for new code. This was simple enough to achieve, but I had to substitute `np.random.choice` with `seed.choices` to get the same weighted choice behaviour. However, this generates different choices than `np.random.choice`, I guess through a different (probably less efficient) implementation that calls `seed.getrandbits`.

This would mean that user code that uses a set random seed would break when upgrading. Would we be okay with that? Or should we try and guarantee the state is the same as the current implementation if the global numpy RNG is seeded (this might be very difficult)?

Resolving patch:
diff --git a/networkx/algorithms/similarity.py b/networkx/algorithms/similarity.py
--- a/networkx/algorithms/similarity.py
+++ b/networkx/algorithms/similarity.py
@@ -20,6 +20,7 @@
 from itertools import product
 
 import networkx as nx
+from networkx.utils import np_random_state
 
 __all__ = [
     "graph_edit_distance",
@@ -1662,9 +1663,10 @@ def panther_similarity(
     return top_k_with_val
 
 
+@np_random_state(5)
 @nx._dispatchable(edge_attrs="weight")
 def generate_random_paths(
-    G, sample_size, path_length=5, index_map=None, weight="weight"
+    G, sample_size, path_length=5, index_map=None, weight="weight", seed=None
 ):
     """Randomly generate `sample_size` paths of length `path_length`.
 
@@ -1685,6 +1687,9 @@ def generate_random_paths(
     weight : string or None, optional (default="weight")
         The name of an edge attribute that holds the numerical value
         used as a weight. If None then each edge has weight 1.
+    seed : integer, random_state, or None (default)
+        Indicator of random number generation state.
+        See :ref:`Randomness<randomness>`.
 
     Returns
     -------
@@ -1718,6 +1723,10 @@ def generate_random_paths(
     """
     import numpy as np
 
+    randint_fn = (
+        seed.integers if isinstance(seed, np.random.Generator) else seed.randint
+    )
+
     # Calculate transition probabilities between
     # every pair of vertices according to Eq. (3)
     adj_mat = nx.to_numpy_array(G, weight=weight)
@@ -1729,7 +1738,7 @@ def generate_random_paths(
 
     for path_index in range(sample_size):
         # Sample current vertex v = v_i uniformly at random
-        node_index = np.random.randint(0, high=num_nodes)
+        node_index = randint_fn(num_nodes)
         node = node_map[node_index]
 
         # Add v into p_r and add p_r into the path set
@@ -1747,7 +1756,7 @@ def generate_random_paths(
         for _ in range(path_length):
             # Randomly sample a neighbor (v_j) according
             # to transition probabilities from ``node`` (v) to its neighbors
-            nbr_index = np.random.choice(
+            nbr_index = seed.choice(
                 num_nodes, p=transition_probabilities[starting_index]
             )
 


Test patch:
diff --git a/networkx/algorithms/tests/test_similarity.py b/networkx/algorithms/tests/test_similarity.py
--- a/networkx/algorithms/tests/test_similarity.py
+++ b/networkx/algorithms/tests/test_similarity.py
@@ -845,8 +845,6 @@ def test_panther_similarity_isolated(self):
             nx.panther_similarity(G, source=1)
 
     def test_generate_random_paths_unweighted(self):
-        np.random.seed(42)
-
         index_map = {}
         num_paths = 10
         path_length = 2
@@ -857,7 +855,7 @@ def test_generate_random_paths_unweighted(self):
         G.add_edge(1, 2)
         G.add_edge(2, 4)
         paths = nx.generate_random_paths(
-            G, num_paths, path_length=path_length, index_map=index_map
+            G, num_paths, path_length=path_length, index_map=index_map, seed=42
         )
         expected_paths = [
             [3, 0, 3],