Repository: PennyLaneAI/pennylane

Issue:
[BUG] qml.chem.molecular_hamiltonian sets random number generator seed
### Expected behavior

When using the argument `shots=100`  to a device one expects variation in the output. This variation depends on the random number generator that is used internally in the device.

Setting on controlling the seed of the RNG is not possible at this moment, see https://github.com/PennyLaneAI/pennylane/issues/1327

In an application with a for loop over some parameters of the system the results of a circuit turned out to be always the same (instead of a random variation due to the finite number of shots). The problem turns out to be in a call to `qml.qchem.molecular_hamiltonian` which seems to reset the RNG. A minimal example:

```
import pennylane
import pennylane as qml
from pennylane import numpy as np
symbols = ["H", "H"]
coordinates = np.array([0.0, 0.0, -0.6614, 0.0, 0.0, 0.6614])
H, qubits = qml.qchem.molecular_hamiltonian(symbols, coordinates)
dev = qml.device("default.qubit", wires=qubits, shots=100)

@qml.qnode(dev)
def F():
    qml.BasisState(np.array([0,0,0,0]), wires=[0,1,2,3])
    qml.Hadamard(0)
    qml.Hadamard(1)
    
    return qml.expval(H)

#%% Circuit has variation in the result, as expected for a finite number of shots

[F() for ii in range(10)]

#%% Redefining the device does not lead to reproducible results. How can we set the seed?

for ii in range(10):
    dev = qml.device("default.qubit", wires=qubits, shots=100)

    @qml.qnode(dev)
    def F():
        qml.BasisState(np.array([0,0]), wires=[0,1])
        qml.Hadamard(0)
        qml.Hadamard(1)    
        return qml.expval(H)
    print(F())
    
    
#%% Making a call to qml.qchem.molecular_hamiltonian seems to reset the seed
for ii in range(10):
    dev = qml.device("default.qubit", wires=qubits, shots=100)
    H, qubits = qml.qchem.molecular_hamiltonian(symbols, coordinates)

    @qml.qnode(dev)
    def F():
        qml.BasisState(np.array([0,0,0,0]), wires=[0,1,2,3])
        qml.Hadamard(0)
        qml.Hadamard(1)  
        return qml.expval(H)
    print(F())
   
#%% Check the call indeed modifies the seed:

qml.qchem.molecular_hamiltonian(symbols, coordinates)
s1=np.random.get_state()
np.random.rand()
qml.qchem.molecular_hamiltonian(symbols, coordinates)
s2=np.random.get_state()

print(s1)
print(s2)
```

Expected behaviour:

* `qml.qchem.molecular_hamiltonian` should not modify the RNG seed




### Actual behavior

`qml.qchem.molecular_hamiltonian` changes the RNG seed


### Additional information

_No response_

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.28.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: c:\develop\env310\lib\site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, retworkx, scipy, semantic-version, toml
Required-by: PennyLane-Lightning

Platform info:           Windows-10-10.0.19045-SP0
Python version:          3.10.8
Numpy version:           1.24.1
Scipy version:           1.9.2
Installed devices:
- default.gaussian (PennyLane-0.28.0.dev0)
- default.mixed (PennyLane-0.28.0.dev0)
- default.qubit (PennyLane-0.28.0.dev0)
- default.qubit.autograd (PennyLane-0.28.0.dev0)
- default.qubit.jax (PennyLane-0.28.0.dev0)
- default.qubit.tf (PennyLane-0.28.0.dev0)
- default.qubit.torch (PennyLane-0.28.0.dev0)
- default.qutrit (PennyLane-0.28.0.dev0)
- null.qubit (PennyLane-0.28.0.dev0)
- lightning.qubit (PennyLane-Lightning-0.26.1)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Comments:

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index fc59a61f5..6adf859bb 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -160,13 +160,22 @@
   from the queue instead of updating their metadata to have an `"owner"`.
   [(#3282)](https://github.com/PennyLaneAI/pennylane/pull/3282)
 
+* `qchem.scf`, `RandomLayers.compute_decomposition`, and `Wires.select_random` all use
+  local random number generators now instead of global random number generators. This may lead to slighlty
+  different random numbers, and an independence of the results from the global random number generation state.
+  Please provide a seed to each individual function instead if you want controllable results.
+  [(#3624)](https://github.com/PennyLaneAI/pennylane/pull/3624)
+
 <h3>Deprecations</h3>
 
 <h3>Documentation</h3>
 
 <h3>Bug fixes</h3>
 
-* Handles breaking networkx version change by selectively skipping a qcut tensorflow-jit test,
+* Uses a local random number generator where possible to avoid mutating the global random state.
+  [(#3624)](https://github.com/PennyLaneAI/pennylane/pull/3624)
+
+* Handles breaking networkx version change by selectively skipping a qcut tensorflow-jit test.
   [(#3609)](https://github.com/PennyLaneAI/pennylane/pull/3609)
   [(#3619)](https://github.com/PennyLaneAI/pennylane/pull/3619)
 
diff --git a/pennylane/math/is_independent.py b/pennylane/math/is_independent.py
index 78fcbbc01..d90b32784 100644
--- a/pennylane/math/is_independent.py
+++ b/pennylane/math/is_independent.py
@@ -196,10 +196,9 @@ def _get_random_args(args, interface, num, seed, bounds):
             tuple(torch.rand(np.shape(arg)) * width + bounds[0] for arg in args) for _ in range(num)
         ]
     else:
-        np.random.seed(seed)
+        rng = np.random.default_rng(seed)
         rnd_args = [
-            tuple(np.random.random(np.shape(arg)) * width + bounds[0] for arg in args)
-            for _ in range(num)
+            tuple(rng.random(np.shape(arg)) * width + bounds[0] for arg in args) for _ in range(num)
         ]
         if interface == "autograd":
 
diff --git a/pennylane/qchem/hartree_fock.py b/pennylane/qchem/hartree_fock.py
index 8634e2dcf..fb3762b5f 100644
--- a/pennylane/qchem/hartree_fock.py
+++ b/pennylane/qchem/hartree_fock.py
@@ -132,8 +132,8 @@ def scf(mol, n_steps=50, tol=1e-8):
             s = overlap_matrix(basis_functions)(*args)
             h_core = core_matrix(basis_functions, charges, r)(*args)
 
-        qml.math.random.seed(2030)
-        s = s + qml.math.diag(qml.math.random.rand(len(s)) * 1.0e-12)
+        rng = qml.math.random.default_rng(2030)
+        s = s + qml.math.diag(rng.random(len(s)) * 1.0e-12)
 
         w, v = qml.math.linalg.eigh(s)
         x = v @ qml.math.diag(1.0 / qml.math.sqrt(w)) @ v.T
diff --git a/pennylane/templates/layers/random.py b/pennylane/templates/layers/random.py
index 8e07418dc..20a3d90dd 100644
--- a/pennylane/templates/layers/random.py
+++ b/pennylane/templates/layers/random.py
@@ -233,8 +233,7 @@ class RandomLayers(Operation):
          RX(tensor(1.4000), wires=['a'])]
         """
         wires = qml.wires.Wires(wires)
-        if seed is not None:
-            np.random.seed(seed)
+        rng = np.random.default_rng(seed)
 
         shape = qml.math.shape(weights)
         n_layers = qml.math.shape(weights)[0]
@@ -244,17 +243,17 @@ class RandomLayers(Operation):
 
             i = 0
             while i < shape[1]:
-                if np.random.random() > ratio_imprimitive:
+                if rng.random() > ratio_imprimitive:
                     # apply a random rotation gate to a random wire
-                    gate = np.random.choice(rotations)
-                    rnd_wire = wires.select_random(1)
+                    gate = rng.choice(rotations)
+                    rnd_wire = wires.select_random(1, seed=rng)
                     op_list.append(gate(weights[l][i], wires=rnd_wire))
                     i += 1
 
                 else:
                     # apply the entangler to two random wires
                     if len(wires) > 1:
-                        rnd_wires = wires.select_random(2)
+                        rnd_wires = wires.select_random(2, seed=rng)
                         op_list.append(imprimitive(wires=rnd_wires))
         return op_list
 
diff --git a/pennylane/wires.py b/pennylane/wires.py
index 61a54129f..21d556e51 100644
--- a/pennylane/wires.py
+++ b/pennylane/wires.py
@@ -348,10 +348,9 @@ class Wires(Sequence):
         if n_samples > len(self._labels):
             raise WireError(f"Cannot sample {n_samples} wires from {len(self._labels)} wires.")
 
-        if seed is not None:
-            np.random.seed(seed)
+        rng = np.random.default_rng(seed)
 
-        indices = np.random.choice(len(self._labels), size=n_samples, replace=False)
+        indices = rng.choice(len(self._labels), size=n_samples, replace=False)
         subset = tuple(self[i] for i in indices)
         return Wires(subset, _override=True)
 


Test patch:
diff --git a/tests/math/test_is_independent.py b/tests/math/test_is_independent.py
index cdeaf3062..5e933eaa9 100644
--- a/tests/math/test_is_independent.py
+++ b/tests/math/test_is_independent.py
@@ -115,11 +115,10 @@ class TestIsIndependentAutograd:
         seed = 921
         rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
         assert len(rnd_args) == num
-        np.random.seed(seed)
+        rng = np.random.default_rng(seed)
         for _rnd_args in rnd_args:
             expected = tuple(
-                np.random.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
-                for arg in args
+                rng.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0] for arg in args
             )
             assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
 
@@ -244,11 +243,10 @@ class TestIsIndependentJax:
         seed = 921
         rnd_args = _get_random_args(args, self.interface, num, seed, bounds)
         assert len(rnd_args) == num
-        np.random.seed(seed)
+        rng = np.random.default_rng(seed)
         for _rnd_args in rnd_args:
             expected = tuple(
-                np.random.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0]
-                for arg in args
+                rng.random(np.shape(arg)) * (bounds[1] - bounds[0]) + bounds[0] for arg in args
             )
             assert all(np.allclose(_exp, _rnd) for _exp, _rnd in zip(expected, _rnd_args))
 
diff --git a/tests/qchem/test_hartree_fock.py b/tests/qchem/test_hartree_fock.py
index 5ce5809ac..3db579f62 100644
--- a/tests/qchem/test_hartree_fock.py
+++ b/tests/qchem/test_hartree_fock.py
@@ -21,6 +21,26 @@ from pennylane import numpy as np
 from pennylane import qchem
 
 
+def test_scf_leaves_random_seed_unchanged():
+    """Tests that the scf function leaves the global numpy sampling state unchanged."""
+
+    symbols = ["H", "H"]
+    geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.0]], requires_grad=False)
+    alpha = np.array(
+        [[3.42525091, 0.62391373, 0.1688554], [3.42525091, 0.62391373, 0.1688554]],
+        requires_grad=True,
+    )
+    mol = qchem.Molecule(symbols, geometry, alpha=alpha)
+    args = [alpha]
+
+    initial_numpy_state = np.random.get_state()
+    v_fock, coeffs, fock_matrix, h_core, rep_tensor = qchem.scf(mol)(*args)
+    final_numpy_state = np.random.get_state()
+
+    assert initial_numpy_state[0] == final_numpy_state[0]
+    assert np.all(initial_numpy_state[1] == final_numpy_state[1])
+
+
 @pytest.mark.parametrize(
     ("symbols", "geometry", "v_fock", "coeffs", "fock_matrix", "h_core", "repulsion_tensor"),
     [
diff --git a/tests/templates/test_layers/test_random.py b/tests/templates/test_layers/test_random.py
index 94537749a..50aeea78b 100644
--- a/tests/templates/test_layers/test_random.py
+++ b/tests/templates/test_layers/test_random.py
@@ -150,10 +150,10 @@ def circuit_template(weights):
 
 def circuit_decomposed(weights):
     # this structure is only true for a seed of 42 and 3 wires
-    qml.RX(weights[0, 0], wires=1)
-    qml.RX(weights[0][1], wires=0)
-    qml.CNOT(wires=[1, 0])
-    qml.RZ(weights[0, 2], wires=2)
+    qml.RY(weights[0, 0], wires=1)
+    qml.RX(weights[0][1], wires=2)
+    qml.CNOT(wires=[1, 2])
+    qml.RZ(weights[0, 2], wires=1)
     return qml.expval(qml.PauliZ(0))
 
 
@@ -169,16 +169,14 @@ class TestInterfaces:
 
         decomp = op.decomposition()
         expected = [
-            qml.RX(weights[0][0], wires=1),
-            qml.RX(weights[0][1], wires=0),
-            qml.CNOT(wires=[1, 0]),
-            qml.RZ(weights[0][2], wires=2),
+            qml.RY(weights[0][0], wires=1),
+            qml.RX(weights[0][1], wires=2),
+            qml.CNOT(wires=[1, 2]),
+            qml.RZ(weights[0][2], wires=1),
         ]
 
         for op1, op2 in zip(decomp, expected):
-            assert op1.name == op2.name
-            assert op1.data == op2.data
-            assert op1.wires == op2.wires
+            assert qml.equal(op1, op2)
 
     def test_autograd(self, tol):
         """Tests the autograd interface."""