Repository: Qiskit/qiskit

Issue:
Side effects of setting numpy.random.seed
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information
Some qiskit functions still call `numpy.random.seed` to set a given (integer) seed. This sets the seed _globally_, influencing also user's code.

Example: https://github.com/Qiskit/qiskit-terra/blob/master/qiskit/quantum_info/random/utils.py#L54

- **Qiskit Terra version**: master
- **Python version**: 3.7.4
- **Operating system**: macOS 10.14

### What is the current behavior?
Using numpy.random.seed sets a seed globally which also impacts code on the user side. It makes it harder for the user to derandomize only specific sections of their code.


### Steps to reproduce the problem
One can run the following to seed that the seed is indeed global.
```
>>> random_unitary(4,seed=3)
Operator([[ 0.65770066-0.20053555j -0.26006836-0.3826572j   0.36309952+0.38527141j
  -0.08189584+0.16177457j]
 [-0.10199905-0.43575616j  0.09482339+0.23352842j -0.1152966 +0.43890564j
  -0.5449333 -0.48301344j]
 [-0.01611248-0.37645837j -0.07949969-0.06578792j -0.69457921+0.11270693j
  -0.02579361+0.5929273j ]
 [ 0.32407147-0.28271057j  0.79667728+0.27761121j  0.13524564-0.01893929j
   0.25499839+0.14005532j]], input_dims=(2, 2), output_dims=(2, 2))
>>> np.random.randint(10)
1
>>> np.random.randint(10)
1
>>> np.random.randint(10)
1
>>> np.random.randint(10)
5
>>> random_unitary(4,seed=3)
Operator([[ 0.65770066-0.20053555j -0.26006836-0.3826572j   0.36309952+0.38527141j
  -0.08189584+0.16177457j]
 [-0.10199905-0.43575616j  0.09482339+0.23352842j -0.1152966 +0.43890564j
  -0.5449333 -0.48301344j]
 [-0.01611248-0.37645837j -0.07949969-0.06578792j -0.69457921+0.11270693j
  -0.02579361+0.5929273j ]
 [ 0.32407147-0.28271057j  0.79667728+0.27761121j  0.13524564-0.01893929j
   0.25499839+0.14005532j]], input_dims=(2, 2), output_dims=(2, 2))
>>> np.random.randint(10)
1
>>> np.random.randint(10)
1
>>> np.random.randint(10)
1
>>> np.random.randint(10)
5
```

### What is the expected behavior?
The seed should be scoped to the function call.


### Suggested solutions
Either use a numpy.random.RandomState generated from the given (integer) seed internally or accept it as argument. I would suggest something along the lines of what SciPy does: https://github.com/scipy/scipy/blob/7651d17041dfa0bd7df045bd7b418d693178e0fe/scipy/_lib/_util.py#L174





Discussion:
This is already done in places like the `StochasticSwap` transpiler pass using:

```
rng = np.random.RandomState(seed)
```

and the like should be done elsewhere.

Resolving patch:
diff --git a/qiskit/quantum_info/operators/pauli.py b/qiskit/quantum_info/operators/pauli.py
--- a/qiskit/quantum_info/operators/pauli.py
+++ b/qiskit/quantum_info/operators/pauli.py
@@ -461,10 +461,9 @@ def random(cls, num_qubits, seed=None):
         Returns:
             Pauli: the random pauli
         """
-        if seed is not None:
-            np.random.seed(seed)
-        z = np.random.randint(2, size=num_qubits).astype(np.bool)
-        x = np.random.randint(2, size=num_qubits).astype(np.bool)
+        rng = np.random.RandomState(seed)
+        z = rng.randint(2, size=num_qubits).astype(np.bool)
+        x = rng.randint(2, size=num_qubits).astype(np.bool)
         return cls(z, x)
 
     @classmethod
diff --git a/qiskit/quantum_info/random/utils.py b/qiskit/quantum_info/random/utils.py
--- a/qiskit/quantum_info/random/utils.py
+++ b/qiskit/quantum_info/random/utils.py
@@ -63,11 +63,9 @@ def random_unitary(dim, seed=None):
     Raises:
         QiskitError: if dim is not a positive power of 2.
     """
-    if seed is not None:
-        np.random.seed(seed)
     if dim == 0 or not math.log2(dim).is_integer():
         raise QiskitError("Desired unitary dimension not a positive power of 2.")
-    return Operator(unitary_group.rvs(dim))
+    return Operator(unitary_group.rvs(dim, random_state=seed))
 
 
 # TODO: return a DensityMatrix object.
@@ -110,10 +108,9 @@ def __ginibre_matrix(nrow, ncol=None, seed=None):
     """
     if ncol is None:
         ncol = nrow
-    if seed is not None:
-        np.random.seed(seed)
+    rng = np.random.RandomState(seed)
 
-    ginibre = np.random.normal(size=(nrow, ncol)) + np.random.normal(size=(nrow, ncol)) * 1j
+    ginibre = rng.normal(size=(nrow, ncol)) + rng.normal(size=(nrow, ncol)) * 1j
     return ginibre
 
 


Test patch:
diff --git a/test/python/quantum_info/test_random.py b/test/python/quantum_info/test_random.py
new file mode 100644
--- /dev/null
+++ b/test/python/quantum_info/test_random.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Qiskit.
+#
+# (C) Copyright IBM 2019.
+#
+# This code is licensed under the Apache License, Version 2.0. You may
+# obtain a copy of this license in the LICENSE.txt file in the root directory
+# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
+#
+# Any modifications or derivative works of this code must retain this
+# copyright notice, and modified files need to carry a notice indicating
+# that they have been altered from the originals.
+
+"""Test random unitary generation utility."""
+
+import unittest
+
+import numpy as np
+
+from qiskit.quantum_info.random import random_unitary, random_density_matrix
+from qiskit.test import QiskitTestCase
+
+
+class TestRandomUtils(QiskitTestCase):
+    """Testing qiskit.quantum_info.random.utils"""
+
+    def test_unitary(self):
+        """ Test that a random unitary with set seed will not affect later
+        results
+        """
+        seed = 314159
+        test_cases = 100
+        random_unitary(4, seed=seed)
+        rng_before = [np.random.randint(1000) for _ in range(test_cases)]
+        random_unitary(4, seed=seed)
+        rng_after = [np.random.randint(1000) for _ in range(test_cases)]
+        array_equality = all([rng_before[i] == rng_after[i] for i in range(test_cases)])
+        self.assertFalse(array_equality)
+
+    def test_density_matrix(self):
+        """ Test that a random state with set seed will not affect later
+        results.
+        """
+        seed = 314159
+        test_cases = 100
+        random_density_matrix(4, seed=seed)
+        rng_before = [np.random.randint(1000) for _ in range(test_cases)]
+        random_density_matrix(4, seed=seed)
+        rng_after = [np.random.randint(1000) for _ in range(test_cases)]
+        array_equality = all([rng_before[i] == rng_after[i] for i in range(test_cases)])
+        self.assertFalse(array_equality)
+
+
+if __name__ == '__main__':
+    unittest.main()