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:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=h1) Report
> Merging [#903](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=desc) (dba1b7f) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/cf8fe6400efe75a2b75a0313c38bccdd28cb7071?el=desc) (cf8fe64) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master     #903   +/-   ##
=======================================
  Coverage   97.80%   97.80%           
=======================================
  Files         138      138           
  Lines        9553     9557    +4     
=======================================
+ Hits         9343     9347    +4     
  Misses        210      210           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/qnodes/base.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3Fub2Rlcy9iYXNlLnB5) | `98.64% <100.00%> (+0.03%)` | :arrow_up: |
| [pennylane/templates/layers/random.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9sYXllcnMvcmFuZG9tLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=footer). Last update [cf8fe64...dba1b7f](https://codecov.io/gh/PennyLaneAI/pennylane/pull/903?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

antalszava: Hi @josh146, thanks for the comments! I've updated the tests and left more details on why the message was changed in the test case.

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 4f013c2ed..74d18f144 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -231,6 +231,11 @@
 
 <h3>Bug fixes</h3>
 
+* PennyLane tensor objects are now unwrapped in BaseQNode when passed as a
+  keyword argument to the quantum function.
+  [(#903)](https://github.com/PennyLaneAI/pennylane/pull/903)
+  [(#893)](https://github.com/PennyLaneAI/pennylane/pull/893)
+
 * The new tape mode now prevents multiple observables from being evaluated on the same wire
   if the observables are not qubit-wise commuting Pauli words.
   [(#882)](https://github.com/PennyLaneAI/pennylane/pull/882)
@@ -252,7 +257,7 @@
 This release contains contributions from (in alphabetical order):
 
 Thomas Bromley, Christina Lee, Olivia Di Matteo, Anthony Hayes, Josh Izaac, Nathan Killoran,
-Romain Moyard, Maria Schuld
+Romain Moyard, Maria Schuld, Antal Száva
 
 
 # Release 0.12.0 (current release)
diff --git a/pennylane/qnodes/base.py b/pennylane/qnodes/base.py
index 0d2861e3f..68d6ab54f 100644
--- a/pennylane/qnodes/base.py
+++ b/pennylane/qnodes/base.py
@@ -453,6 +453,8 @@ class BaseQNode(qml.QueuingContext):
                 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 @@ class BaseQNode(qml.QueuingContext):
 
         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
index 5013c3a1c..2560f3d50 100644
--- a/pennylane/templates/layers/random.py
+++ b/pennylane/templates/layers/random.py
@@ -25,7 +25,6 @@ from pennylane.templates.utils import (
     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
index 881d4d000..abd127512 100644
--- a/tests/templates/test_subroutines.py
+++ b/tests/templates/test_subroutines.py
@@ -913,7 +913,7 @@ class TestUCCSDUnitary:
                 [[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
index e77b57c03..c877c665f 100644
--- a/tests/test_numpy_wrapper.py
+++ b/tests/test_numpy_wrapper.py
@@ -497,3 +497,62 @@ class TestNumpyConversion:
         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)