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