From 17a4c91265df80b85788405a9aac80e10e642944 Mon Sep 17 00:00:00 2001 From: antalszava Date: Mon, 16 Nov 2020 10:03:10 -0500 Subject: [PATCH 1/2] Add tensor unwrapping in BaseQNode for keyword arguments (#903) * Fix in BaseQNode * Add staticmethod for unwrapping (codefactor); remove import * Change to check that spots init_state difference earlier * CHANGELOG * Update * Use recorder to check gates used * Test docstrings --- .github/CHANGELOG.md | 7 +++- pennylane/qnodes/base.py | 21 ++++++++++ pennylane/templates/layers/random.py | 6 +-- tests/templates/test_subroutines.py | 2 +- tests/test_numpy_wrapper.py | 59 ++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 4f013c2ede9..74d18f14442 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -231,6 +231,11 @@

Bug fixes

+* 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 0d2861e3f31..68d6ab54f32 100644 --- 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 index 5013c3a1ccf..2560f3d50e9 100644 --- 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: diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py index 881d4d00075..abd12751203 100644 --- 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 index e77b57c03ea..c877c665fa5 100644 --- 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) From 47b1dfff654dfa514aa655d8bca3d8a190dcbaf2 Mon Sep 17 00:00:00 2001 From: Tom Bromley <49409390+trbromley@users.noreply.github.com> Date: Mon, 16 Nov 2020 11:52:09 -0500 Subject: [PATCH 2/2] Fix gradient in tape mode when measurements are independent of a parameter (#901) * Fix batch execution for parameters that are independent of observable * Add test * Remove extra indent --- pennylane/tape/tapes/jacobian_tape.py | 7 ++++++- tests/tape/tapes/test_jacobian_tape.py | 27 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pennylane/tape/tapes/jacobian_tape.py b/pennylane/tape/tapes/jacobian_tape.py index d4d9a08bc96..9939f5c9526 100644 --- a/pennylane/tape/tapes/jacobian_tape.py +++ b/pennylane/tape/tapes/jacobian_tape.py @@ -490,8 +490,13 @@ def jacobian(self, device, params=None, **options): all_tapes = [] reshape_info = [] processing_fns = [] + nonzero_grad_idx = [] for trainable_idx, param_method in enumerate(diff_methods): + if param_method == "0": + continue + + nonzero_grad_idx.append(trainable_idx) if (method == "best" and param_method[0] == "F") or (method == "numeric"): # numeric method @@ -516,7 +521,7 @@ def jacobian(self, device, params=None, **options): jac = None start = 0 - for i, (processing_fn, res_len) in enumerate(zip(processing_fns, reshape_info)): + for i, processing_fn, res_len in zip(nonzero_grad_idx, processing_fns, reshape_info): # extract the correct results from the flat list res = results[start : start + res_len] diff --git a/tests/tape/tapes/test_jacobian_tape.py b/tests/tape/tapes/test_jacobian_tape.py index 413aaa41a64..6a27083fe3c 100644 --- a/tests/tape/tapes/test_jacobian_tape.py +++ b/tests/tape/tapes/test_jacobian_tape.py @@ -384,6 +384,33 @@ def test_numeric_unknown_order(self): with pytest.raises(ValueError, match="Order must be 1 or 2"): tape.jacobian(dev, order=3) + def test_independent_parameters(self): + """Test the case where expectation values are independent of some parameters. For those + parameters, the gradient should be evaluated to zero without executing the device.""" + dev = qml.device("default.qubit", wires=2) + + with JacobianTape() as tape1: + qml.RX(1, wires=[0]) + qml.RX(1, wires=[1]) + qml.expval(qml.PauliZ(0)) + + with JacobianTape() as tape2: + qml.RX(1, wires=[0]) + qml.RX(1, wires=[1]) + qml.expval(qml.PauliZ(1)) + + j1 = tape1.jacobian(dev) + + # We should only be executing the device to differentiate 1 parameter (2 executions) + assert dev.num_executions == 2 + + j2 = tape2.jacobian(dev) + + exp = - np.sin(1) + + assert np.allclose(j1, [exp, 0]) + assert np.allclose(j2, [0, exp]) + class TestJacobianIntegration: """Integration tests for the Jacobian method"""