Repository: PennyLaneAI/pennylane

Issue:
Error for pennylane.numpy.tensor with plugin devices using non-differentiable syntax
#### Issue description
When a `pennylane.numpy.tensor` is passed to a QNode as a keyword argument, it is considered as a non-differentiable parameter. Unlike in the case when it is handled as a differentiable parameter, it is not cast into a `ndarray`.

When indexing into a `pennylane.numpy.tensor`, one can end up having a single element sequence. Certain devices (e.g., `qiskit.aer`, `forest.wavefunction`), are not capable of supporting the application of a gate taking this type of argument and raise an error. Gates can take such a `pennylane.numpy.tensor` as gate parameter, when passed as a non-differentiable parameter.

```python
import pennylane as qml
from pennylane.numpy import tensor

dev = qml.device("qiskit.aer", wires=4)

@qml.qnode(dev)
def circuit(phi=None):
    for y in phi:
        for idx, x in enumerate(y):
            qml.RX(x, wires=idx)
    return qml.expval(qml.PauliZ(0))

phi = tensor([[0.04439891, 0.14490549, 3.29725643, 2.51240058]])

circuit(phi=phi)
```

A special case using `RandomLayers` was tackled in PR #893. This is an example that is used in the  [Quanvolutional Neural Networks](https://pennylane.ai/qml/demos/tutorial_quanvolution.html) demonstration.

* *Expected behavior:*
The above snippet executes without errors.

* *Actual behavior:*
Error is raised (Qiskit case):
```
~/anaconda3/lib/python3.7/site-packages/qiskit/providers/aer/backends/backend_utils.py in cpp_execute(controller, qobj)
     49     qobj_dict['config']['library_dir'] = LIBRARY_DIR
     50 
---> 51     return controller(qobj_dict)
     52 
     53 

RuntimeError: Invalid number of dimensions!
```
Further errors may arise for other devices (see https://github.com/PennyLaneAI/pennylane/pull/893#issuecomment-726153779 for error messages that were thrown when using `RandomLayers`).

* *Reproduces how often:* (What percentage of the time does it reproduce?)
For certain plugin devices, each time a `pennylane.numpy.tensor` type sequence is passed as a gate parameter during execution.

* *System information:*

```
Name: PennyLane
Version: 0.13.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location:
Requires: numpy, scipy, networkx, autograd, toml, appdirs, semantic-version
Required-by: PennyLane-SF, PennyLane-Qchem, PennyLane-Forest, PennyLane-qsharp, pennylane-qulacs, PennyLane-lightning, PennyLane-qiskit, PennyLane-AQT, PennyLane-Cirq
Platform info:           Linux-4.19.11-041911-generic-x86_64-with-debian-buster-sid
Python version:          3.7.6
Numpy version:           1.18.5
Scipy version:           1.4.1
Installed devices:
- strawberryfields.fock (PennyLane-SF-0.12.0)
- strawberryfields.gaussian (PennyLane-SF-0.12.0)
- strawberryfields.gbs (PennyLane-SF-0.12.0)
- strawberryfields.remote (PennyLane-SF-0.12.0)
- strawberryfields.tf (PennyLane-SF-0.12.0)
- forest.numpy_wavefunction (PennyLane-Forest-0.13.0.dev0)
- forest.qpu (PennyLane-Forest-0.13.0.dev0)
- forest.qvm (PennyLane-Forest-0.13.0.dev0)
- forest.wavefunction (PennyLane-Forest-0.13.0.dev0)
- microsoft.QuantumSimulator (PennyLane-qsharp-0.9.0.dev0)
- qulacs.simulator (pennylane-qulacs-0.12.0.dev0)
- lightning.qubit (PennyLane-lightning-0.10.0.dev0)
- qiskit.aer (PennyLane-qiskit-0.13.0.dev0)
- qiskit.basicaer (PennyLane-qiskit-0.13.0.dev0)
- qiskit.ibmq (PennyLane-qiskit-0.13.0.dev0)
- default.gaussian (PennyLane-0.13.0.dev0)
- default.mixed (PennyLane-0.13.0.dev0)
- default.qubit (PennyLane-0.13.0.dev0)
- default.qubit.autograd (PennyLane-0.13.0.dev0)
- default.qubit.tf (PennyLane-0.13.0.dev0)
- default.tensor (PennyLane-0.13.0.dev0)
- default.tensor.tf (PennyLane-0.13.0.dev0)
- aqt.noisy_sim (PennyLane-AQT-0.10.0.dev0)
- aqt.sim (PennyLane-AQT-0.10.0.dev0)
- cirq.mixedsimulator (PennyLane-Cirq-0.12.0.dev0)
- cirq.simulator (PennyLane-Cirq-0.12.0.dev0)
```

#### Source code and tracebacks


#### Additional information

A solution could be to create a case for `pennylane.numpy.tensor` type sequence parameters in each plugin device implementation (see https://github.com/PennyLaneAI/pennylane/pull/893#issuecomment-725935113 containing a suggestion).

Seems that this problem does not arise in tape mode.


Discussion:
Thanks for catching this @antalszava @mariaschuld.

I think this is a result of the old + new QNode variable marking system clashing.

* Keyword arguments being non-differentiable is only assumed in the old core; PennyLane NumPy tensors are passed as-is to the device if passed as a keyword argument.

* In tape mode, however, positional vs. keyword arg makes no difference. Any argument can be differentiable, if marked as so.

As a result, when tape-mode is default this issue should disappear. However, in the meantime, I suggest we add

```python
if isinstance(x, qml.numpy.tensor) and x.ndim == 0:
    x = x.unwrap()
```

either to the (old) autograd interface, the (old) `BaseQNode`, or `QubitDevice`, depending on which one makes more sense; this way we don't have to update all plugins.

My gut feeling is that it makes more sense to fix this in `BaseQNode`, since it will be deleted anyway when tape mode is default.

Resolving patch:
diff --git a/pennylane/qnodes/base.py b/pennylane/qnodes/base.py
--- a/pennylane/qnodes/base.py
+++ b/pennylane/qnodes/base.py
@@ -453,6 +453,8 @@ def qfunc(a, w):
                 Each positional argument is replaced with a :class:`~.variable.Variable` instance.
             kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function.
         """
+        kwargs = self.unwrap_tensor_kwargs(kwargs)
+
         # Get the name of the qfunc's arguments
         full_argspec = inspect.getfullargspec(self.func)
 
@@ -525,6 +527,25 @@ def qfunc(a, w):
 
         return arg_vars, kwarg_vars
 
+    @staticmethod
+    def unwrap_tensor_kwargs(kwargs):
+        """Unwraps the pennylane.numpy.tensor objects that were passed as
+        keyword arguments so that they can be handled as gate parameters by
+        arbitrary devices.
+
+        Args:
+            kwargs (dict[str, Any]): Auxiliary arguments passed to the quantum function.
+
+        Returns:
+            dict[str, Any]: Auxiliary arguments passed to the quantum function
+            in an unwrapped form (if applicable).
+        """
+        for k, v in kwargs.items():
+            if isinstance(v, qml.numpy.tensor):
+                kwargs[k] = v.unwrap()
+
+        return kwargs
+
     def _construct(self, args, kwargs):
         """Construct the quantum circuit graph by calling the quantum function.
 
diff --git a/pennylane/templates/layers/random.py b/pennylane/templates/layers/random.py
--- a/pennylane/templates/layers/random.py
+++ b/pennylane/templates/layers/random.py
@@ -25,7 +25,6 @@
     get_shape,
 )
 from pennylane.wires import Wires
-from pennylane.numpy import tensor
 
 
 def random_layer(weights, wires, ratio_imprim, imprimitive, rotations, seed):
@@ -51,10 +50,7 @@ def random_layer(weights, wires, ratio_imprim, imprimitive, rotations, seed):
             gate = np.random.choice(rotations)
             rnd_wire = wires.select_random(1)
 
-            if isinstance(weights[i], tensor):
-                gate(weights[i].unwrap(), wires=rnd_wire)
-            else:
-                gate(weights[i], wires=rnd_wire)
+            gate(weights[i], wires=rnd_wire)
 
             i += 1
         else:


Test patch:
diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py
--- a/tests/templates/test_subroutines.py
+++ b/tests/templates/test_subroutines.py
@@ -913,7 +913,7 @@ def test_uccsd_operations(self, s_wires, d_wires, weights, ref_gates):
                 [[0, 1, 2]],
                 [],
                 np.array([1.2, 1, 0, 0]),
-                "BasisState parameter must consist of 0 or 1 integers",
+                "Elements of 'init_state' must be integers",
             ),
             (
                 np.array([-2.8]),
diff --git a/tests/test_numpy_wrapper.py b/tests/test_numpy_wrapper.py
--- a/tests/test_numpy_wrapper.py
+++ b/tests/test_numpy_wrapper.py
@@ -497,3 +497,62 @@ def test_convert_array(self):
         assert np.all(res == data)
         assert isinstance(res, np.ndarray)
         assert not isinstance(res, np.tensor)
+
+    def test_single_gate_parameter(self, monkeypatch):
+        """Test that when supplied a PennyLane tensor, a QNode passes an
+        unwrapped tensor as the argument to a gate taking a single parameter"""
+        dev = qml.device("default.qubit", wires=4)
+
+        @qml.qnode(dev)
+        def circuit(phi=None):
+            for y in phi:
+                for idx, x in enumerate(y):
+                    qml.RX(x, wires=idx)
+            return qml.expval(qml.PauliZ(0))
+
+        phi = np.tensor([[0.04439891, 0.14490549, 3.29725643, 2.51240058]])
+
+        with qml._queuing.OperationRecorder() as rec:
+            circuit(phi=phi)
+
+        for i in range(phi.shape[1]):
+            # Test each rotation applied
+            assert rec.queue[0].name == "RX"
+            assert len(rec.queue[0].parameters) == 1
+
+            # Test that the gate parameter is not a PennyLane tensor, but a
+            # float
+            assert not isinstance(rec.queue[0].parameters[0], np.tensor)
+            assert isinstance(rec.queue[0].parameters[0], float)
+
+    def test_multiple_gate_parameter(self):
+        """Test that when supplied a PennyLane tensor, a QNode passes arguments
+        as unwrapped tensors to a gate taking multiple parameters"""
+        dev = qml.device("default.qubit", wires=1)
+
+        @qml.qnode(dev)
+        def circuit(phi=None):
+            for idx, x in enumerate(phi):
+                qml.Rot(*x, wires=idx)
+            return qml.expval(qml.PauliZ(0))
+
+        phi = np.tensor([[0.04439891, 0.14490549, 3.29725643]])
+
+
+        with qml._queuing.OperationRecorder() as rec:
+            circuit(phi=phi)
+
+        # Test the rotation applied
+        assert rec.queue[0].name == "Rot"
+        assert len(rec.queue[0].parameters) == 3
+
+        # Test that the gate parameters are not PennyLane tensors, but a
+        # floats
+        assert not isinstance(rec.queue[0].parameters[0], np.tensor)
+        assert isinstance(rec.queue[0].parameters[0], float)
+
+        assert not isinstance(rec.queue[0].parameters[1], np.tensor)
+        assert isinstance(rec.queue[0].parameters[1], float)
+
+        assert not isinstance(rec.queue[0].parameters[2], np.tensor)
+        assert isinstance(rec.queue[0].parameters[2], float)