Repository: PennyLaneAI/pennylane

Issue:
RandomCircuit template rebuilds circuit every time qnode is used
As of v0.4 of PennyLane, circuits are not cached any more, which means every time a qnode is called the quantum circuit is rebuilt from the quantum function. That means that random choices in the architecture get redone with every run.

* *Expected behavior:* A template circuit architecture is fixed for a node

* *Actual behavior:* See above

***********************
While caching could in principle done by building the qnode with cache=True argument, a template does not have access to the qnode building procedure, since it can be used in any qnode. 

Actually, I am not sure how to fix that. I could pick a seed, but where would I set the seed, if anything in the template must be fixed? In other words, how can I induce randomness in a function that in some calls must not be random?
RandomCircuit template rebuilds circuit every time qnode is used
As of v0.4 of PennyLane, circuits are not cached any more, which means every time a qnode is called the quantum circuit is rebuilt from the quantum function. That means that random choices in the architecture get redone with every run.

* *Expected behavior:* A template circuit architecture is fixed for a node

* *Actual behavior:* See above

***********************
While caching could in principle done by building the qnode with cache=True argument, a template does not have access to the qnode building procedure, since it can be used in any qnode. 

Actually, I am not sure how to fix that. I could pick a seed, but where would I set the seed, if anything in the template must be fixed? In other words, how can I induce randomness in a function that in some calls must not be random?


Discussion:
Off the top of my head, I can think of maybe a couple of options (@co9olguy may have more):

1. You could have a keyword argument in the function signature that sets the seed

2. You could have the docstring specify (maybe using `.. important::`) that `cache=True` if using `RandomCircuit`. I don't personally like this approach though.
I would also not do 2., and my concern with 1. is that usually random functions show random behaviour if a seed is not given, and deterministic one otherwise. But agreed, this would definitely be an option.
Alternatively, you could modify RandomCircuit to return a _function_, that is created with random parameters BUT identical each time it is called. To generate a new RandomCircuit, you must call the top level wrapper to generate a new function.

Similar to the idea of how decorators are coded/structured.
I'm not sure I understand..something like:

def _randomcirc(...):
     <<construct random circuit, using np.random>>
    return ???

def RandomCircuit(...):
   _randomcirc(...)


Something like this:

```python
def RandomLayersGenerator():
    random_value = np.random.random()

    def _random_layers(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=None):
        # put random layer code
        # here, using random_value instead
        # of calls to np.random.random()

    return _random_layers
```

Then, I suppose the user would have to 'initialize' a random circuit function before using it inside the QNode. Every time the 'initialized' function is called, it will use the same random value:

```python
random_circuit1 = RandomLayersGenerator()

@qml.qnode(dev)
def circuit(weights):
    random_circuit1(weights, wires=[0, 1])
    return expval(..)
```

Not sure if it is an ugly solution or not though.
Off the top of my head, I can think of maybe a couple of options (@co9olguy may have more):

1. You could have a keyword argument in the function signature that sets the seed

2. You could have the docstring specify (maybe using `.. important::`) that `cache=True` if using `RandomCircuit`. I don't personally like this approach though.
I would also not do 2., and my concern with 1. is that usually random functions show random behaviour if a seed is not given, and deterministic one otherwise. But agreed, this would definitely be an option.
Alternatively, you could modify RandomCircuit to return a _function_, that is created with random parameters BUT identical each time it is called. To generate a new RandomCircuit, you must call the top level wrapper to generate a new function.

Similar to the idea of how decorators are coded/structured.
I'm not sure I understand..something like:

def _randomcirc(...):
     <<construct random circuit, using np.random>>
    return ???

def RandomCircuit(...):
   _randomcirc(...)


Something like this:

```python
def RandomLayersGenerator():
    random_value = np.random.random()

    def _random_layers(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=None):
        # put random layer code
        # here, using random_value instead
        # of calls to np.random.random()

    return _random_layers
```

Then, I suppose the user would have to 'initialize' a random circuit function before using it inside the QNode. Every time the 'initialized' function is called, it will use the same random value:

```python
random_circuit1 = RandomLayersGenerator()

@qml.qnode(dev)
def circuit(weights):
    random_circuit1(weights, wires=[0, 1])
    return expval(..)
```

Not sure if it is an ugly solution or not though.

Resolving patch:
diff --git a/pennylane/templates/layers.py b/pennylane/templates/layers.py
--- a/pennylane/templates/layers.py
+++ b/pennylane/templates/layers.py
@@ -134,13 +134,15 @@ def StronglyEntanglingLayer(weights, wires, r=1, imprimitive=CNOT):
         imprimitive(wires=[wires[i], wires[(i + r) % num_wires]])
 
 
-def RandomLayers(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=None):
+def RandomLayers(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=42):
     r"""A sequence of layers of type :func:`RandomLayer()`.
 
     The number of layers :math:`L` and the number :math:`k` of rotations per layer is inferred from the first
     and second dimension of ``weights``. The type of imprimitive (two-qubit) gate and rotations distributed
     randomly in the circuit can be chosen explicitly.
 
+    See :func:`RandomLayer` for details on the randomised behaviour.
+
     Args:
         weights (array[float]): array of weights of shape ``(L, k)``,
         wires (Sequence[int]): sequence of qubit indices that the template acts on
@@ -162,7 +164,7 @@ def RandomLayers(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=N
                     seed=seed)
 
 
-def RandomLayer(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=None):
+def RandomLayer(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=None, seed=42):
     r"""A layer of randomly chosen single qubit rotations and 2-qubit entangling gates, acting
     on randomly chosen qubits.
 
@@ -176,6 +178,15 @@ def RandomLayer(weights, wires, ratio_imprim=0.3, imprimitive=CNOT, rotations=No
         :width: 60%
         :target: javascript:void(0);
 
+    .. note::
+        Using the default seed (or any other fixed integer seed) generates one and the same circuit in every
+        quantum node. To generate different circuit architectures, either use a different random seed, or use ``seed=None``
+        together with the ``cache=False`` option when creating a quantum node.
+
+    .. warning::
+        If you use a random number generator anywhere inside the quantum function without the ``cache=False`` option,
+        a new random circuit architecture will be created every time the quantum node is evaluated.
+
     Args:
         weights (array[float]): array of weights of shape ``(k,)``
         wires (Sequence[int]): sequence of qubit indices that the template acts on


Test patch:
diff --git a/tests/test_templates_layers.py b/tests/test_templates_layers.py
--- a/tests/test_templates_layers.py
+++ b/tests/test_templates_layers.py
@@ -548,11 +548,10 @@ def impr(self, request):
     def rots(self, request):
         return request.param
 
-    def test_random_layers_seed(self, n_layers, tol, seed):
-        """Test that pennylane.templates.layers.RandomLayers() gets deterministic when using seed."""
+    def test_random_layers_seed_deterministic(self, n_layers, tol, seed):
+        """Test that pennylane.templates.layers.RandomLayers() acts deterministically when using fixed seed."""
         n_rots = 1
         n_wires = 2
-        impr = CNOT
         dev = qml.device('default.qubit', wires=n_wires)
         weights = np.random.randn(n_layers, n_rots)
 
@@ -569,6 +568,46 @@ def circuit2(weights):
 
         assert np.allclose(qnode1(weights), qnode2(weights), atol=tol)
 
+    def test_random_layers_no_seed_deterministic(self, n_layers, tol):
+        """Test that pennylane.templates.layers.RandomLayers() acts deterministically when using no seed."""
+        n_rots = 1
+        n_wires = 2
+        dev = qml.device('default.qubit', wires=n_wires)
+        weights = np.random.randn(n_layers, n_rots)
+
+        def circuit1(weights):
+            RandomLayers(weights=weights, wires=range(n_wires))
+            return qml.expval(qml.PauliZ(0))
+
+        def circuit2(weights):
+            RandomLayers(weights=weights, wires=range(n_wires))
+            return qml.expval(qml.PauliZ(0))
+
+        qnode1 = qml.QNode(circuit1, dev)
+        qnode2 = qml.QNode(circuit2, dev)
+
+        assert np.allclose(qnode1(weights), qnode2(weights), atol=tol)
+
+    def test_random_layers_two_seeds_different(self, n_layers, tol):
+        """Test that pennylane.templates.layers.RandomLayers() does not have the same output for two different seeds."""
+        n_rots = 10
+        n_wires = 2
+        dev = qml.device('default.qubit', wires=n_wires)
+        weights = np.random.randn(n_layers, n_rots)
+
+        def circuit1(weights):
+            RandomLayers(weights=weights, wires=range(n_wires), seed=0)
+            return qml.expval(qml.PauliZ(0))
+
+        def circuit2(weights):
+            RandomLayers(weights=weights, wires=range(n_wires), seed=1)
+            return qml.expval(qml.PauliZ(0))
+
+        qnode1 = qml.QNode(circuit1, dev)
+        qnode2 = qml.QNode(circuit2, dev)
+
+        assert not np.allclose(qnode1(weights), qnode2(weights), atol=tol)
+
     def test_random_layers_nlayers(self, n_layers):
         """Test that  pennylane.templates.layers.RandomLayers() picks the correct number of gates."""
         np.random.seed(12)