qml.transform¶
- transform(tape_transform=None, pass_name=None, *, setup_inputs=None, expand_transform=None, classical_cotransform=None, is_informative=False, final_transform=False, use_argnum_in_expand=False, plxpr_transform=None)[source]¶
Generalizes a function that transforms tapes to work with additional circuit-like objects such as a
QNode.transformshould be applied to a function that transforms tapes. Once validated, the result will be an object that is able to transform PennyLane’s range of circuit-like objects:QuantumTape, quantum function andQNode. A circuit-like object can be transformed either via decoration or by passing it functionally through the created transform.- Parameters:
tape_transform (Callable | None) –
The input quantum transform must be a function that satisfies the following requirements:
Accepts a
QuantumScriptas its first input and returns a sequence ofQuantumScriptand a processing function.The transform must have the following structure (type hinting is optional):
my_tape_transform(tape: qml.tape.QuantumScript, ...) -> tuple[qml.tape.QuantumScriptBatch, qml.typing.PostprocessingFn]
pass_name (str | None) – the name of the associated MLIR pass to be applied when Catalyst is used. See Usage Details for more information.
- Keyword Arguments:
expand_transform=None (Optional[Callable]) – An optional transform that is applied directly before the input transform. It must be a function that satisfies the same requirements as
tape_transform.classical_cotransform=None (Optional[Callable]) – A classical co-transform is a function to post-process the classical jacobian and the quantum jacobian and has the signature:
my_cotransform(qjac, cjac, tape) -> tensor_likeis_informative=False (bool) – Whether or not a transform is informative. If true, the transform is queued at the end of the compile pipeline and the tapes or qnode aren’t executed.
final_transform=False (bool) – Whether or not the transform is terminal. If true, the transform is queued at the end of the compile pipeline.
is_informativesupersedesfinal_transform.use_argnum_in_expand=False (bool) – Whether to use
argnumof the tape to determine trainable parameters during the expansion transform process.plxpr_transform=None (Optional[Callable]) – Function for transforming plxpr. Experimental
Example
First define an input tape transform with the necessary structure defined above. In this example, we copy the tape and sum the results of the execution of the two tapes.
from pennylane.tape import QuantumScript, QuantumScriptBatch from pennylane.typing import PostprocessingFn def my_quantum_transform(tape: QuantumScript) -> tuple[QuantumScriptBatch, PostprocessingFn]: tape1 = tape tape2 = tape.copy() def post_processing_fn(results): return qml.math.sum(results) return [tape1, tape2], post_processing_fn
We want to be able to apply this transform on both a
qfuncand apennylane.QNodeand will usetransformto achieve this.transformvalidates the signature of your input quantum transform and makes it capable of transformingqfuncandpennylane.QNodein addition to quantum tapes. Let’s define a circuit as apennylane.QNode:dev = qml.device("default.qubit") @qml.qnode(device=dev) def qnode_circuit(a): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) qml.X(0) qml.RZ(a, wires=1) return qml.expval(qml.Z(0))
We first apply
transformtomy_quantum_transform:>>> dispatched_transform = qml.transform(my_quantum_transform)
Now you can use the dispatched transform directly on a
pennylane.QNode.For
pennylane.QNode, the dispatched transform populates theCompilePipelineof your QNode. The transform and its processing function are applied in the execution.>>> transformed_qnode = dispatched_transform(qnode_circuit) >>> transformed_qnode <QNode: device='<default.qubit device at ...>', interface='auto', diff_method='best', shots='Shots(total=None)'>
>>> print(transformed_qnode.compile_pipeline) CompilePipeline( [1] my_quantum_transform() )
If we apply
dispatched_transforma second time to thepennylane.QNode, we would add it to the compile pipeline again and therefore the transform would be applied twice before execution.>>> transformed_qnode = dispatched_transform(transformed_qnode) >>> print(transformed_qnode.compile_pipeline) CompilePipeline( [1] my_quantum_transform(), [2] my_quantum_transform() )
When a transformed QNode is executed, the QNode’s compile pipeline is applied to the generated tape and creates a sequence of tapes to be executed. The execution results are then post-processed in the reverse order of the compile pipeline to obtain the final results.
Setup inputs
The
setup_inputsfunction will independently applied prior to any application of the transform. This allows for validation of the inputs, separation into positional and keyword arguments, and specification of a call signature and docstring for transforms without a tape definition.def my_transform_setup(a, b=1, metadata : str = "my_value"): "Docstring for my_transform." return (a, b), {"metadata": metadata} my_transform = qml.transform(pass_name="my_pass", setup_inputs=my_transform_setup) @qml.qnode(qml.device('default.qubit', wires=4)) def circuit(): return qml.expval(qml.Z(0))
This allows us to perform eager input validation and set default values.
>>> my_transform(circuit) Traceback (most recent call last): ... TypeError: <transform: my_pass> missing 1 required positional argument: 'a' >>> new_circuit = my_transform(circuit, a=2) >>> new_circuit.compile_pipeline[0] <my_pass(2, 1, metadata=my_value)>
We will also have a docstring and signature. If a tape transform is present, the signature will be determined by that.
>>> my_transform.__doc__ 'Docstring for my_transform.' >>> import inspect >>> inspect.signature(my_transform) <Signature (a, b=1, metadata: str = 'my_value')>
Dispatch a transform onto a batch of tapes
We can compose multiple transforms when working in the tape paradigm and apply them to more than one tape. The following example demonstrates how to apply a transform to a batch of tapes.
Example
In this example, we apply sequentially a transform to a tape and another one to a batch of tapes. We then execute the transformed tapes on a device and post-process the results.
import pennylane as qml H = qml.PauliY(2) @ qml.PauliZ(1) + 0.5 * qml.PauliZ(2) + qml.PauliZ(1) measurement = [qml.expval(H)] operations = [qml.Hadamard(0), qml.RX(0.2, 0), qml.RX(0.6, 0), qml.CNOT((0, 1))] tape = qml.tape.QuantumTape(operations, measurement) batch1, function1 = qml.transforms.split_non_commuting(tape) batch2, function2 = qml.transforms.merge_rotations(batch1) dev = qml.device("default.qubit", wires=3) result = dev.execute(batch2)
The first
split_non_commutingtransform splits the original tape, returning a batch of tapesbatch1and a processing functionfunction1. The secondmerge_rotationstransform is applied to the batch of tapes returned by the first transform. It returns a new batch of tapesbatch2, each of which has been transformed by the second transform, and a processing functionfunction2.>>> batch2 (<QuantumTape: wires=[0, 1, 2], params=1>, <QuantumTape: wires=[0, 1, 2], params=1>)
>>> type(function2) <class 'function'>
We can combine the processing functions to post-process the results of the execution.
>>> function1(function2(result)) np.float64(0.499...)
Signature of a transform
A dispatched transform is able to handle several PennyLane circuit-like objects:
a quantum function (callable)
a batch of
pennylane.tape.QuantumScript
For each object, the transform will be applied in a different way, but it always preserves the underlying tape-based quantum transform behaviour.
The return of a dispatched transform depends upon which of the above objects is passed as an input:
For a
QNodeinput, the underlying transform is added to the QNode’sCompilePipelineand the return is the transformedQNode. For each execution of thepennylane.QNode, it first applies the compile pipeline on the original captured circuit. Then the transformed circuits are executed by a device and finally the post-processing function is applied on the results.When experimental program capture is enabled, transforming a
QNodereturns a new function to which the transform has been added as a higher-order primitive.For a quantum function (callable) input, the transform builds the tape when the quantum function is executed and then applies itself to the tape. The resulting tape is then converted back to a quantum function (callable). It therefore returns a transformed quantum function (Callable). The limitation is that the underlying transform can only return a sequence containing a single tape, because quantum functions only support a single circuit.
When experimental program capture is enabled, transforming a function (callable) returns a new function to which the transform has been added as a higher-order primitive.
For a
QuantumScript. It returns a sequence ofQuantumScriptand a processing function to be applied after execution.For a batch of
pennylane.tape.QuantumScript, the quantum transform is mapped across all the tapes. It returns a sequence ofQuantumScriptand a processing function to be applied after execution. Each tape in the sequence is transformed by the transform.For a
Device, the transform is added to the device’s compile pipeline and a transformedpennylane.devices.Deviceis returned. The transform is added to the end of the device program and will be last in the overall compile pipeline.
Transforms with Catalyst
If a compilation pass is written in MLIR, using it in a
qjit’d workflow requires that it have a transform with a matchingpass_name. This ensures that the transform is properly applied as part of the lower-level compilation.For example, we can create a transform that will apply the
cancel-inversespass, like the in-builtqml.transforms.cancel_inversestransform.my_transform = qml.transform(pass_name="cancel-inverses") @qml.qjit @my_transform @qml.qnode(qml.device('lightning.qubit', wires=4)) def circuit(): qml.X(0) qml.X(0) return qml.expval(qml.Z(0))
We can see that the instruction to apply
"cancel-inverses"is present in the initial MLIR.>>> circuit() Array(1., dtype=float64) >>> print(circuit.mlir[200:600]) tensor<f64> } module @module_circuit { module attributes {transform.with_named_sequence} { transform.named_sequence @__transform_main(%arg0: !transform.op<"builtin.module">) { %0 = transform.apply_registered_pass "cancel-inverses" to %arg0 : (!transform.op<"builtin.module">) -> !transform.op<"builtin.module"> transform.yield } } func.func public @circui
Transforms can have both tape-based and
pass_name-based definitions. For example, the transform below calledmy_transformhas both definitions. In this case, the MLIR pass will take precedence when beingqjit’d if only MLIR passes can occur after.from functools import partial @partial(qml.transform, pass_name="my-pass-name") def my_transform(tape): return (tape, ), lambda res: res[0]
Note that any transform with only a
pass_namedefinition must occur after any purely tape-based transform, as tape transforms occur prior to lowering to MLIR.>>> @qml.qjit ... @qml.defer_measurements ... @qml.transform(pass_name="cancel-inverses") ... @qml.qnode(qml.device('lightning.qubit', wires=4)) ... def c(): ... qml.X(0) ... qml.X(0) ... return qml.expval(qml.Z(0)) ... Traceback (most recent call last): ... ValueError: <cancel-inverses()> without a tape definition occurs before tape transform <defer_measurements()>.