Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tensor unwrapping in BaseQNode for keyword arguments #903

Merged
merged 10 commits into from
Nov 16, 2020
7 changes: 6 additions & 1 deletion .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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)
Expand All @@ -229,7 +234,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)
Expand Down
21 changes: 21 additions & 0 deletions pennylane/qnodes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
antalszava marked this conversation as resolved.
Show resolved Hide resolved

def _construct(self, args, kwargs):
"""Construct the quantum circuit graph by calling the quantum function.

Expand Down
6 changes: 1 addition & 5 deletions pennylane/templates/layers/random.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
antalszava marked this conversation as resolved.
Show resolved Hide resolved

i += 1
else:
Expand Down
2 changes: 1 addition & 1 deletion tests/templates/test_subroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This had to be changed because pennylane.numpy.tensor objects are accepted types here (no check for the type of the elements contained):

[int, np.int64, np.ndarray],

With the unwrapping this changed as we'd have a real ndarray instead of a pennylane.numpy.tensor object:

from pennylane.templates.utils import check_type
from pennylane import numpy as np
import numpy as original_np

init_state = np.array([1.2, 1, 0, 0])

for i in init_state:
    check_type(
        i,
        [int, np.int64, np.ndarray],
        msg="(First check): Elements of 'init_state' must be integers; got {}".format(init_state),
    )

init_state = original_np.array([1.2, 1, 0, 0])

for i in init_state:
    check_type(
        i,
        [int, np.int64, np.ndarray],
        msg="(Second check): Elements of 'init_state' must be integers; got {}".format(init_state),
    )
~/xanadu/pennylane/pennylane/templates/utils.py in check_type(element, types, msg)
    182 
    183     if not any([isinstance(element, t) for t in types]):
--> 184         raise ValueError(msg)
    185 
    186 

ValueError: (Second check): Elements of 'init_state' must be integers; got [1.2 1.  0.  0. ]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@antalszava I have to admit, I don't follow the above comment 🤔

How come the first check pass, but the second one fails, in the example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case tests that the UCCSD template will raise an error when supplied np.array([1.2, 1, 0, 0]) as the initial state.

Previously:

This test case passed because the expected error was raised by the call on line 206 due to the call to qml.BasisState.

qml.BasisState(np.flip(init_state), wires=wires)

However, conceptually speaking the check on line 176 using check_type (linked in the previous comment) should have actually already raised an error. It is meant to check that elements of init_state are integer types. However, since it also allows np.ndarray as an accepted type, it passes for single element PennyLane tensor objects because if we received a np.ndarray we don't further check the type of the element contained. Therefore, when we passed np.array([1.2, 1, 0, 0]) as a PennyLane tensor and indexed into it using the for loop, this check passes.

The snippet in the previous comment proves that this check indeed passes for a PennyLane tensor, but fails for an original ndarray. The following is the output of the snippet by adding print('Type of element: ', type(i)) right before the check_type call:

Type of element:  <class 'pennylane.numpy.tensor.tensor'>
Type of element:  <class 'pennylane.numpy.tensor.tensor'>
Type of element:  <class 'pennylane.numpy.tensor.tensor'>
Type of element:  <class 'pennylane.numpy.tensor.tensor'>
Type of element:  <class 'numpy.float64'>
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-18-70b15445a0e0> in <module>
     20         i,
     21         [int, np.int64, np.ndarray],
---> 22         msg="(Second check): Elements of 'init_state' must be integers; got {}".format(init_state),
     23     )

~/xanadu/pennylane/pennylane/templates/utils.py in check_type(element, types, msg)
    182 
    183     if not any([isinstance(element, t) for t in types]):
--> 184         raise ValueError(msg)
    185 
    186 

ValueError: (Second check): Elements of 'init_state' must be integers; got [1.2 1.  0.  0. ]

It might be worth revisiting the edge cases for the check using check_type, although I would argue that that is out of scope for this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! thanks Antal

),
(
np.array([-2.8]),
Expand Down
59 changes: 59 additions & 0 deletions tests/test_numpy_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +525 to +526
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯


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)