From cae8cafed8205556115ccaa428f5c3086dccb35f Mon Sep 17 00:00:00 2001 From: Christina Lee Date: Thu, 3 Aug 2023 09:42:34 -0400 Subject: [PATCH] Integrate `TransformProgram` with `QNode` (#4404) * Draft structure * draf exec * Simple execute * Update * More tests * Update * Update exec * Pylint and black * Update tests * Update more tests * More tests * changelog * Coverage * Cover fix * pylint * Pylint * Pylint tests * proposed changes to transform program integration * oops * add to legacy, remove cotransform support * just transform program call component * just transform program call component * no longer support cotransforms, fix _batch_postprocessing * some more testing * test null postprocessing function * docstring, rename batch_slices to slices, black * Apply suggestions from code review Co-authored-by: Matthew Silverman * integrate transform program with qnode * adding integration tests * test modifications * [skip ci] fiddling * more testing * changelog entry * add to execute, start on testing * add qml.execute tests * Update doc/releases/changelog-dev.md Co-authored-by: Matthew Silverman * fix test --------- Co-authored-by: rmoyard Co-authored-by: Matthew Silverman --- doc/releases/changelog-dev.md | 25 ++ pennylane/interfaces/execution.py | 31 ++- pennylane/qnode.py | 8 +- pennylane/tape/qscript.py | 1 + pennylane/transforms/core/transform.py | 4 +- .../transforms/core/transform_dispatcher.py | 4 +- .../test_transform_program_integration.py | 251 ++++++++++++++++++ tests/tape/test_qscript.py | 7 + tests/test_qnode.py | 206 ++++++++++---- 9 files changed, 475 insertions(+), 62 deletions(-) create mode 100644 tests/interfaces/test_transform_program_integration.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 2a9141cd3eb..e8bf19f5af6 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -11,6 +11,31 @@ issue, say using JAX, TensorFlow, Torch, try setting `max_workers` to `None`. [(#4319)](https://github.com/PennyLaneAI/pennylane/pull/4319) +* Transform Programs are now integrated with the `QNode`. + [(#4404)](https://github.com/PennyLaneAI/pennylane/pull/4404) + +``` +def null_postprocessing(results: qml.typing.ResultBatch) -> qml.typing.Result: + return results[0] + +@qml.transforms.core.transform +def scale_shots(tape: qml.tape.QuantumTape, shot_scaling) -> (Tuple[qml.tape.QuantumTape], Callable): + new_shots = tape.shots.total_shots * shot_scaling + new_tape = qml.tape.QuantumScript(tape.operations, tape.measurements, shots=new_shots) + return (new_tape, ), null_postprocessing + +dev = qml.devices.experimental.DefaultQubit2() + +@partial(scale_shots, shot_scaling=2) +@qml.qnode(dev, interface=None) +def circuit(): + return qml.sample(wires=0) + +``` + +>>> circuit(shots=1) +array([False, False]) +

Improvements 🛠

* Transform Programs, `qml.transforms.core.TransformProgram`, can now be called on a batch of circuits diff --git a/pennylane/interfaces/execution.py b/pennylane/interfaces/execution.py index 25ee2769ebd..e67ed1c0b56 100644 --- a/pennylane/interfaces/execution.py +++ b/pennylane/interfaces/execution.py @@ -296,6 +296,7 @@ def execute( device: device_type, gradient_fn: Optional[Union[Callable, str]] = None, interface="auto", + transform_program=None, grad_on_execution="best", gradient_kwargs=None, cache: Union[bool, dict, Cache] = True, @@ -430,6 +431,7 @@ def cost_fn(params, x): ) ### Specifying and preprocessing variables #### + transform_program = transform_program or qml.transforms.core.TransformProgram() if interface == "auto": params = [] @@ -465,6 +467,7 @@ def cost_fn(params, x): #### Executing the configured setup ##### + tapes, program_post_processing = transform_program(tapes) tapes, batch_fn, config = _batch_transform( tapes, device, config, override_shots, device_batch_transform ) @@ -491,7 +494,8 @@ def cost_fn(params, x): pass_kwargs=new_device_interface, ) results = cached_execute_fn(tapes, execution_config=config) - return batch_fn(results) + results = batch_fn(results) + return program_post_processing(results) # the default execution function is batch_execute # use qml.interfaces so that mocker can spy on it during testing @@ -621,7 +625,7 @@ def gradient_fn(internal_tapes): elif mapped_interface == "jax": _execute = _get_jax_execute_fn(interface, tapes) - res = _execute( + results = _execute( tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff ) @@ -631,7 +635,8 @@ def gradient_fn(internal_tapes): f"version of {mapped_interface} to enable the '{mapped_interface}' interface." ) from e - return batch_fn(res) + results = batch_fn(results) + return program_post_processing(results) def _execute_legacy( @@ -639,6 +644,7 @@ def _execute_legacy( device: device_type, gradient_fn: Callable = None, interface="auto", + transform_program=None, mode="best", gradient_kwargs=None, cache=True, @@ -754,6 +760,9 @@ def cost_fn(params, x): if isinstance(device, qml.devices.experimental.Device): raise ValueError("New device interface only works with return types enabled.") + transform_program = transform_program or qml.transforms.core.TransformProgram() + tapes, program_post_processing = transform_program(tapes) + if interface == "auto": params = [] for tape in tapes: @@ -782,24 +791,27 @@ def cost_fn(params, x): if gradient_fn is None: # don't unwrap if it's an interface device if "passthru_interface" in device.capabilities(): - return batch_fn( + results = batch_fn( qml.interfaces.cache_execute( batch_execute, cache, return_tuple=False, expand_fn=expand_fn )(tapes) ) + return program_post_processing(results) unwrapped_tapes = tuple(qml.transforms.convert_to_numpy_parameters(t) for t in tapes) res = qml.interfaces.cache_execute( batch_execute, cache, return_tuple=False, expand_fn=expand_fn )(unwrapped_tapes) - return batch_fn(res) + results = batch_fn(res) + return program_post_processing(results) if gradient_fn == "backprop" or interface is None: - return batch_fn( + results = batch_fn( qml.interfaces.cache_execute( batch_execute, cache, return_tuple=False, expand_fn=expand_fn )(tapes) ) + return program_post_processing(results) # the default execution function is batch_execute execute_fn = qml.interfaces.cache_execute(batch_execute, cache, expand_fn=expand_fn) @@ -873,9 +885,12 @@ def cost_fn(params, x): f"version of {mapped_interface} to enable the '{mapped_interface}' interface." ) from e - res = _execute(tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff) + results = _execute( + tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff + ) - return batch_fn(res) + results = batch_fn(results) + return program_post_processing(results) def _get_jax_execute_fn(interface: str, tapes: Sequence[QuantumTape]): diff --git a/pennylane/qnode.py b/pennylane/qnode.py index 4a02926e49b..39708d18c89 100644 --- a/pennylane/qnode.py +++ b/pennylane/qnode.py @@ -963,12 +963,14 @@ def __call__(self, *args, **kwargs) -> qml.typing.Result: if qml.active_return(): if "mode" in self.execute_kwargs: self.execute_kwargs.pop("mode") + # pylint: disable=unexpected-keyword-arg res = qml.execute( - [self.tape], + (self._tape,), device=self.device, gradient_fn=self.gradient_fn, interface=self.interface, + transform_program=self.transform_program, gradient_kwargs=self.gradient_kwargs, override_shots=override_shots, **self.execute_kwargs, @@ -1018,11 +1020,13 @@ def __call__(self, *args, **kwargs) -> qml.typing.Result: grad_on_execution = "best" self.execute_kwargs["grad_on_execution"] = grad_on_execution # pylint: disable=unexpected-keyword-arg + res = qml.execute( - [self.tape], + (self._tape,), device=self.device, gradient_fn=self.gradient_fn, interface=self.interface, + transform_program=self._transform_program, gradient_kwargs=self.gradient_kwargs, override_shots=override_shots, **self.execute_kwargs, diff --git a/pennylane/tape/qscript.py b/pennylane/tape/qscript.py index f5e520abd60..e034bc9524d 100644 --- a/pennylane/tape/qscript.py +++ b/pennylane/tape/qscript.py @@ -221,6 +221,7 @@ def hash(self): fingerprint.extend(op.hash for op in self.operations) fingerprint.extend(m.hash for m in self.measurements) fingerprint.extend(self.trainable_params) + fingerprint.extend(self.shots) return hash(tuple(fingerprint)) def __iter__(self): diff --git a/pennylane/transforms/core/transform.py b/pennylane/transforms/core/transform.py index c8484b53b5a..bf403bd929a 100644 --- a/pennylane/transforms/core/transform.py +++ b/pennylane/transforms/core/transform.py @@ -14,7 +14,7 @@ """ This module contains the transform function to make your custom transforms compatible with qfunc and QNodes. """ -from typing import get_type_hints, Sequence, Callable, List, Tuple +from typing import get_type_hints, Sequence, List, Tuple, Callable import pennylane as qml from .transform_dispatcher import TransformDispatcher, TransformError @@ -156,7 +156,7 @@ def _transform_signature_check(signature): "pennylane.tape.tape.QuantumTape], )" ) - if not ret[0] in ( + if ret[0] not in ( Sequence[qml.tape.QuantumTape], List[qml.tape.QuantumTape], Tuple[qml.tape.QuantumTape], diff --git a/pennylane/transforms/core/transform_dispatcher.py b/pennylane/transforms/core/transform_dispatcher.py index cb4b38e405e..867c728d6c7 100644 --- a/pennylane/transforms/core/transform_dispatcher.py +++ b/pennylane/transforms/core/transform_dispatcher.py @@ -144,8 +144,8 @@ def __init__( self, transform, args=None, kwargs=None, classical_cotransform=None, is_informative=False ): # pylint:disable=redefined-outer-name,too-many-arguments self._transform = transform - self._args = args if args else [] - self._kwargs = kwargs if kwargs else {} + self._args = args or [] + self._kwargs = kwargs or {} self._classical_cotransform = classical_cotransform self._is_informative = is_informative diff --git a/tests/interfaces/test_transform_program_integration.py b/tests/interfaces/test_transform_program_integration.py new file mode 100644 index 00000000000..d6eb31144b8 --- /dev/null +++ b/tests/interfaces/test_transform_program_integration.py @@ -0,0 +1,251 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Integration tests for the transform program and the execution pipeline. + +Differentiability tests are still in the ml-framework specific files. +""" +import copy +from typing import Tuple, Callable +import pytest + +import numpy as np + +import pennylane as qml + + +device_suite = ( + qml.device("default.qubit", wires=5), + qml.devices.experimental.DefaultQubit2(), + qml.device("lightning.qubit", wires=5), +) + + +class TestTransformProgram: + """Non differentiability tests for the transform program keyword argument.""" + + @pytest.mark.parametrize("interface", (None, "autograd", "jax", "tf", "torch")) + def test_transform_program_none(self, interface): + """Test that if no transform program is provided, null default behavior is used.""" + + dev = qml.devices.experimental.DefaultQubit2() + + tape0 = qml.tape.QuantumScript([qml.RX(1.0, 0)], [qml.expval(qml.PauliZ(0))]) + tape1 = qml.tape.QuantumScript([qml.RY(2.0, 0)], [qml.state()]) + + with dev.tracker as tracker: + results = qml.execute((tape0, tape1), dev, transform_program=None, interface=interface) + + assert qml.math.allclose(results[0], np.cos(1.0)) + assert qml.math.allclose(results[1], np.array([np.cos(1.0), np.sin(1.0)])) + + # checks on what is passed to the device. Should be exactly what we put in. + assert tracker.totals["executions"] == 2 + assert tracker.history["resources"][0].gate_types["RX"] == 1 + assert tracker.history["resources"][1].gate_types["RY"] == 1 + + @pytest.mark.parametrize("interface", (None, "autograd", "jax", "tf", "torch")) + def test_transform_program_modifies_circuit(self, interface): + """Integration tests for a transform program that modifies the input tapes.""" + + dev = qml.devices.experimental.DefaultQubit2() + + def null_postprocessing(results): + return results[0] + + def just_pauli_x_out( + tape: qml.tape.QuantumTape, + ) -> (Tuple[qml.tape.QuantumTape], Callable): + return ( + qml.tape.QuantumScript([qml.PauliX(0)], tape.measurements), + ), null_postprocessing + + pauli_x_out_container = qml.transforms.core.TransformContainer(just_pauli_x_out) + + transform_program = qml.transforms.core.TransformProgram([pauli_x_out_container]) + + tape0 = qml.tape.QuantumScript( + [qml.Rot(1.2, 2.3, 3.4, wires=0)], [qml.expval(qml.PauliZ(0))] + ) + tape1 = qml.tape.QuantumScript( + [qml.Hadamard(0), qml.IsingXX(1.2, wires=(0, 1))], [qml.expval(qml.PauliX(0))] + ) + + with dev.tracker as tracker: + results = qml.execute( + (tape0, tape1), dev, transform_program=transform_program, interface=interface + ) + + assert qml.math.allclose(results[0], -1.0) + assert qml.math.allclose(results[1], 0.0) + + assert tracker.totals["executions"] == 2 + assert tracker.history["resources"][0].gate_types["PauliX"] == 1 + assert tracker.history["resources"][0].num_gates == 1 + assert tracker.history["resources"][1].gate_types["PauliX"] == 1 + assert tracker.history["resources"][1].num_gates == 1 + + @pytest.mark.parametrize("interface", (None, "autograd", "jax", "tf", "torch")) + def test_shot_distributing_transform(self, interface): + """Test a transform that creates a batch of tapes with different shots. + + Note that this only works with the new device interface. + """ + dev = qml.devices.experimental.DefaultQubit2() + + def null_postprocessing(results): + return results + + def split_shots(tape): + tape1 = qml.tape.QuantumScript( + tape.operations, tape.measurements, shots=tape.shots.total_shots // 2 + ) + tape2 = qml.tape.QuantumScript( + tape.operations, tape.measurements, shots=tape.shots.total_shots * 2 + ) + return (tape1, tape2), null_postprocessing + + scale_shots = qml.transforms.core.TransformContainer(split_shots) + program = qml.transforms.core.TransformProgram([scale_shots]) + + tape = qml.tape.QuantumScript([], [qml.counts(wires=0)], shots=100) + results = qml.execute((tape,), dev, interface=interface, transform_program=program)[0] + + assert results[0] == {"0": 50} + assert results[1] == {"0": 200} + + @pytest.mark.parametrize("interface", (None, "autograd", "jax", "tf", "torch")) + @pytest.mark.parametrize("dev", device_suite) + def test_ragged_batch_sizes(self, dev, interface): + """Test a transform that splits input tapes up into different sizes.""" + + # note this does not work for partitioned shots + def sum_measurements(results): + return sum(results) + + def split_sum_terms(tape): + sum_obj = tape.measurements[0].obs + new_tapes = tuple( + qml.tape.QuantumScript(tape.operations, [qml.expval(o)], shots=tape.shots) + for o in sum_obj + ) + + return new_tapes, sum_measurements + + container = qml.transforms.core.TransformContainer(split_sum_terms) + prog = qml.transforms.core.TransformProgram((container,)) + + op = qml.RX(1.2, 0) + tape1 = qml.tape.QuantumScript([op], [qml.expval(qml.sum(qml.PauliX(0), qml.PauliZ(0)))]) + tape2 = qml.tape.QuantumScript( + [op], [qml.expval(qml.sum(qml.PauliX(0), qml.PauliY(0), qml.PauliZ(0)))] + ) + tape3 = qml.tape.QuantumScript( + [op], [qml.expval(qml.sum(*(qml.PauliZ(i) for i in range(5))))] + ) + with dev.tracker: + results = qml.execute( + (tape1, tape2, tape3), dev, interface=interface, transform_program=prog + ) + + assert qml.math.allclose(results[0], np.cos(1.2)) + assert qml.math.allclose(results[1], -np.sin(1.2) + np.cos(1.2)) + assert qml.math.allclose(results[2], 4 + np.cos(1.2)) + + assert dev.tracker.totals["executions"] == 7 + + def test_chained_preprocessing(self): + """Test a transform program with two transforms where their order affects the output.""" + + dev = qml.device("default.qubit", wires=2) + + def null_postprocessing(results): + return results[0] + + def just_pauli_x_out(tape: qml.tape.QuantumTape) -> (Tuple[qml.tape.QuantumTape], Callable): + return ( + qml.tape.QuantumScript([qml.PauliX(0)], tape.measurements), + ), null_postprocessing + + def repeat_operations( + tape: qml.tape.QuantumTape, + ) -> (Tuple[qml.tape.QuantumTape], Callable): + new_tape = qml.tape.QuantumScript( + tape.operations + copy.deepcopy(tape.operations), tape.measurements + ) + return (new_tape,), null_postprocessing + + just_pauli_x_container = qml.transforms.core.TransformContainer(just_pauli_x_out) + repeat_operations_container = qml.transforms.core.TransformContainer(repeat_operations) + + prog = qml.transforms.core.TransformProgram( + (just_pauli_x_container, repeat_operations_container) + ) + + tape1 = qml.tape.QuantumScript([qml.RX(1.2, 0)], [qml.expval(qml.PauliZ(0))]) + + with dev.tracker: + results = qml.execute((tape1,), dev, transform_program=prog) + + assert dev.tracker.history["resources"][0].gate_types["PauliX"] == 2 + assert qml.math.allclose(results, 1.0) + + prog_reverse = qml.transforms.core.TransformProgram( + (repeat_operations_container, just_pauli_x_container) + ) + + with dev.tracker: + results = qml.execute((tape1,), dev, transform_program=prog_reverse) + + assert dev.tracker.history["resources"][0].gate_types["PauliX"] == 1 + assert qml.math.allclose(results, -1.0) + + @pytest.mark.parametrize("interface", (None, "autograd", "jax", "tf", "torch")) + @pytest.mark.parametrize("dev", device_suite) + def test_chained_postprocessing(self, dev, interface): + def add_one(results): + return results[0] + 1.0 + + def scale_two(results): + return results[0] * 2.0 + + def transform_add(tape: qml.tape.QuantumTape): + """A valid transform.""" + return (tape,), add_one + + def transform_mul(tape: qml.tape.QuantumTape): + return (tape,), scale_two + + add_container = qml.transforms.core.TransformContainer(transform_add) + mul_container = qml.transforms.core.TransformContainer(transform_mul) + prog = qml.transforms.core.TransformProgram((add_container, mul_container)) + prog_reverse = qml.transforms.core.TransformProgram((mul_container, add_container)) + + tape0 = qml.tape.QuantumScript([], [qml.expval(qml.PauliZ(0))]) + tape1 = qml.tape.QuantumScript([qml.PauliX(0)], [qml.expval(qml.PauliZ(0))]) + + results = qml.execute((tape0, tape1), dev, interface=interface, transform_program=prog) + + # 1.0 * 2.0 + 1.0 + assert qml.math.allclose(results[0], 3.0) + # -1.0 * 2.0 + 1.0 = -1.0 + assert qml.math.allclose(results[1], -1.0) + + results_reverse = qml.execute( + (tape0, tape1), dev, interface=interface, transform_program=prog_reverse + ) + + # (1.0 + 1.0) * 2.0 = 4.0 + assert qml.math.allclose(results_reverse[0], 4.0) + # (-1.0 + 1.0) * 2.0 = 0.0 + assert qml.math.allclose(results_reverse[1], 0.0) diff --git a/tests/tape/test_qscript.py b/tests/tape/test_qscript.py index dc1e743ccff..c5576505c5d 100644 --- a/tests/tape/test_qscript.py +++ b/tests/tape/test_qscript.py @@ -714,6 +714,13 @@ def test_controlled_rotation_modulo_identical(self): assert qs.hash == qs_add_4pi.hash assert qs.hash != qs_add_2pi.hash + def test_hash_shots(self): + """Test tha circuits with different shots have different hashes.""" + qs1 = QuantumScript([qml.S(0)], [qml.sample(wires=0)], shots=10) + qs2 = QuantumScript([qml.T(0)], [qml.sample(wires=0)], shots=20) + + assert qs1.hash != qs2.hash + class TestQScriptDraw: """Test the script draw method.""" diff --git a/tests/test_qnode.py b/tests/test_qnode.py index 8489b48e29d..95067f2b2d0 100644 --- a/tests/test_qnode.py +++ b/tests/test_qnode.py @@ -14,12 +14,16 @@ """Unit tests for the QNode""" # pylint: disable=import-outside-toplevel, protected-access, no-member import warnings -from collections import defaultdict +import copy + +from functools import partial +from typing import Callable, Tuple import numpy as np import pytest from scipy.sparse import csr_matrix + import pennylane as qml from pennylane import QNode from pennylane.devices import experimental @@ -1268,7 +1272,7 @@ def circuit(): with qml.queuing.AnnotatedQueue() as q: circuit() - assert q.queue == [] + assert q.queue == [] # pylint: disable=use-implicit-booleaness-not-comparison assert len(circuit.tape.operations) == 1 @@ -1406,7 +1410,7 @@ def circuit(x): # pylint: disable=unexpected-keyword-arg def test_warning_finite_shots_override(self): """Tests that a warning is raised when caching is used with finite shots.""" - dev = qml.device("default.qubit", wires=1) + dev = qml.device("default.qubit", wires=1, shots=5) @qml.qnode(dev, cache={}) def circuit(x): @@ -1508,67 +1512,173 @@ def qn2(x, y): assert qn2.tape.shots.shot_vector == shot_vector -@pytest.mark.xfail -class TestSpecs: - """Tests for the qnode property specs""" +class TestTransformProgramIntegration: + def test_transform_program_modifies_circuit(self): + """Test qnode integration with a transform that turns the circuit into just a pauli x.""" + dev = qml.device("default.qubit", wires=1) - # pylint: disable=pointless-statement - def test_specs_error(self): - """Tests an error is raised if the tape is not constructed.""" + def null_postprocessing(results): + return results[0] - dev = qml.device("default.qubit", wires=4) + @qml.transforms.core.transform + def just_pauli_x_out( + tape: qml.tape.QuantumTape, + ) -> (Tuple[qml.tape.QuantumTape], Callable): + return ( + qml.tape.QuantumScript([qml.PauliX(0)], tape.measurements), + ), null_postprocessing - @qnode(dev) - def circuit(): + @just_pauli_x_out + @qml.qnode(dev, interface=None, diff_method=None) + def circuit(x): + qml.RX(x, 0) return qml.expval(qml.PauliZ(0)) - with pytest.raises(qml.QuantumFunctionError, match=r"The QNode specifications"): - circuit.specs # pylint: disable=pointless-statement + assert circuit.transform_program[0].transform == just_pauli_x_out.transform - @pytest.mark.parametrize( - "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)] - ) - def test_specs(self, diff_method, len_info): - """Tests the specs property with backprop, parameter-shift and adjoint diff_method""" + assert qml.math.allclose(circuit(0.1), -1) - dev = qml.device("default.qubit", wires=4) + with circuit.device.tracker as tracker: + circuit(0.1) - @qnode(dev, diff_method=diff_method) - def circuit(x, y): - qml.RX(x[0], wires=0) - qml.Toffoli(wires=(0, 1, 2)) - qml.CRY(x[1], wires=(0, 1)) - qml.Rot(x[2], x[3], y, wires=2) - return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1)) + assert tracker.totals["executions"] == 1 + assert tracker.history["resources"][0].gate_types["PauliX"] == 1 + assert tracker.history["resources"][0].gate_types["RX"] == 0 - x = pnp.array([0.05, 0.1, 0.2, 0.3], requires_grad=True) - y = pnp.array(0.1, requires_grad=False) + def tet_transform_program_modifies_results(self): + """Test integration with a transform that modifies the result output.""" - _ = circuit(x, y) + dev = qml.device("default.qubit", wires=2) - info = circuit.specs + @qml.transforms.core.transform + def pin_result( + tape: qml.tape.QuantumTape, requested_result + ) -> (Tuple[qml.tape.QuantumTape], Callable): + def postprocessing(_: qml.typing.ResultBatch) -> qml.typing.Result: + return requested_result - assert len(info) == len_info + return (tape,), postprocessing - assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1}) - assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1}) - assert info["num_operations"] == 4 - assert info["num_observables"] == 2 - assert info["num_diagonalizing_gates"] == 1 - assert info["num_used_wires"] == 3 - assert info["depth"] == 3 - assert info["num_device_wires"] == 4 + @partial(pin_result, requested_result=3.0) + @qml.qnode(dev, interface=None, diff_method=None) + def circuit(x): + qml.RX(x, 0) + return qml.expval(qml.PauliZ(0)) - assert info["diff_method"] == diff_method + assert circuit.transform_program[0].transform == pin_result.transform + assert circuit.transform_program[0].kwargs == {"requested_result": 3.0} - if diff_method == "parameter-shift": - assert info["num_parameter_shift_executions"] == 7 + assert qml.math.allclose(circuit(0.1), 3.0) - if diff_method != "backprop": - assert info["device_name"] == "default.qubit" - assert info["num_trainable_params"] == 4 - else: - assert info["device_name"] == "default.qubit.autograd" + def test_transform_order_circuit_processing(self): + """Test that transforms are applied in the correct order in integration.""" + + dev = qml.device("default.qubit", wires=2) + + def null_postprocessing(results): + return results[0] + + @qml.transforms.core.transform + def just_pauli_x_out(tape: qml.tape.QuantumTape) -> (Tuple[qml.tape.QuantumTape], Callable): + return ( + qml.tape.QuantumScript([qml.PauliX(0)], tape.measurements), + ), null_postprocessing + + @qml.transforms.core.transform + def repeat_operations( + tape: qml.tape.QuantumTape, + ) -> (Tuple[qml.tape.QuantumTape], Callable): + new_tape = qml.tape.QuantumScript( + tape.operations + copy.deepcopy(tape.operations), tape.measurements + ) + return (new_tape,), null_postprocessing + + @repeat_operations + @just_pauli_x_out + @qml.qnode(dev, interface=None, diff_method=None) + def circuit1(x): + qml.RX(x, 0) + return qml.expval(qml.PauliZ(0)) + + with circuit1.device.tracker as tracker: + assert qml.math.allclose(circuit1(0.1), 1.0) + + assert tracker.history["resources"][0].gate_types["PauliX"] == 2 + + @just_pauli_x_out + @repeat_operations + @qml.qnode(dev, interface=None, diff_method=None) + def circuit2(x): + qml.RX(x, 0) + return qml.expval(qml.PauliZ(0)) + + with circuit2.device.tracker as tracker: + assert qml.math.allclose(circuit2(0.1), -1.0) + + assert tracker.history["resources"][0].gate_types["PauliX"] == 1 + + def test_transform_order_postprocessing(self): + """Test that transform postprocessing is called in the right order.""" + + dev = qml.device("default.qubit", wires=2) + + def scale_by_factor(results, factor): + return results[0] * factor + + def add_shift(results, shift): + return results[0] + shift + + @qml.transforms.core.transform + def scale_output( + tape: qml.tape.QuantumTape, factor + ) -> (Tuple[qml.tape.QuantumTape], Callable): + return (tape,), partial(scale_by_factor, factor=factor) + + @qml.transforms.core.transform + def shift_output( + tape: qml.tape.QuantumTape, shift + ) -> (Tuple[qml.tape.QuantumTape], Callable): + return (tape,), partial(add_shift, shift=shift) + + @partial(shift_output, shift=1.0) + @partial(scale_output, factor=2.0) + @qml.qnode(dev, interface=None, diff_method=None) + def circuit1(): + return qml.expval(qml.PauliZ(0)) + + # first add one, then scale by 2.0. Outer postprocessing transforms are applied first + assert qml.math.allclose(circuit1(), 4.0) + + @partial(scale_output, factor=2.0) + @partial(shift_output, shift=1.0) + @qml.qnode(dev, interface=None, diff_method=None) + def circuit2(): + return qml.expval(qml.PauliZ(0)) + + # first scale by 2, then add one. Outer postprocessing transforms are applied first + assert qml.math.allclose(circuit2(), 3.0) + + def test_scaling_shots_transform(self): + """Test a transform that scales the number of shots used in an execution.""" + + # note that this won't work with the old device interface :( + dev = qml.devices.experimental.DefaultQubit2() + + def num_of_shots_from_sample(results): + return len(results[0]) + + @qml.transforms.core.transform + def use_n_shots(tape: qml.tape.QuantumTape, n) -> (Tuple[qml.tape.QuantumTape], Callable): + return ( + qml.tape.QuantumScript(tape.operations, tape.measurements, shots=n), + ), num_of_shots_from_sample + + @partial(use_n_shots, n=100) + @qml.qnode(dev, interface=None, diff_method=None) + def circuit(): + return qml.sample(wires=0) + + assert circuit() == 100 # pylint: disable=unused-argument