qml.batch_transform¶
-
class
batch_transform
(*args, **kwargs)[source]¶ Bases:
object
Class for registering a tape transform that takes a tape, and outputs a batch of tapes to be independently executed on a quantum device.
Examples of such transforms include quantum gradient shift rules (such as finite-differences and the parameter-shift rule) and metrics such as the quantum Fisher information matrix.
- Parameters
transform_fn (function) – The function to register as the batch tape transform. It can have an arbitrary number of arguments, but the first argument must be the input tape.
expand_fn (function) – An expansion function (if required) to be applied to the input tape before the transformation takes place. It must take the same input arguments as
transform_fn
.differentiable (bool) –
Specifies whether the transform is differentiable or not. A transform may be non-differentiable for several reasons:
It does not use an autodiff framework for its tensor manipulations;
It returns a non-differentiable or non-numeric quantity, such as a boolean, string, or integer.
In such a case, setting
differentiable=False
instructs the decorator to mark the output as ‘constant’, reducing potential overhead.
Example
A valid batch tape transform is a function that satisfies the following:
The first argument must be a tape.
Depending on the structure of this input tape, various quantum operations, functions, and templates may be called.
Any internal classical processing should use the
qml.math
module to ensure the transform is differentiable.The transform should return a tuple containing:
Multiple transformed tapes to be executed on a device.
A classical processing function for post-processing the executed tape results. This processing function should have the signature
f(list[tensor_like]) → Any
. IfNone
, no classical processing is applied to the results.
For example:
@qml.batch_transform def my_transform(tape, a, b): '''Generates two tapes, one with all RX replaced with RY, and the other with all RX replaced with RZ.''' ops1 = [] ops2 = [] # loop through all operations on the input tape for op in tape.operations: if op.name == "RX": wires = op.wires param = op.parameters[0] ops1.append(qml.RY(a * qml.math.abs(param), wires=wires)) ops2.append(qml.RZ(b * qml.math.abs(param), wires=wires)) else: ops1.append(op) ops2.append(op) tape1 = qml.tape.QuantumTape(ops1, tape.measurements) tape2 = qml.tape.QuantumTape(ops2, tape.measurements) def processing_fn(results): return qml.math.sum(qml.math.stack(results)) return [tape1, tape2], processing_fn
We can apply this transform to a quantum tape:
>>> ops = [qml.Hadamard(wires=0), qml.RX(-0.5, wires=0)] >>> tape = qml.tape.QuantumTape(ops, [qml.expval(qml.PauliX(0))]) >>> tapes, fn = my_transform(tape, 0.65, 2.5) >>> print(qml.drawer.tape_text(tapes[0], decimals=2)) 0: ──H──RY(0.33)─┤ <X> >>> print(qml.drawer.tape_text(tapes[1], decimals=2)) 0: ──H──RZ(1.25)─┤ <X>
We can execute these tapes manually:
>>> dev = qml.device("default.qubit", wires=1) >>> res = qml.execute(tapes, dev, interface="autograd", gradient_fn=qml.gradients.param_shift) >>> print(res) [tensor([0.94765073], requires_grad=True), tensor([0.31532236], requires_grad=True)]
Applying the processing function, we retrieve the end result of the transform:
>>> print(fn(res)) 1.2629730888100839
Alternatively, we may also transform a QNode directly, using either decorator syntax:
>>> @my_transform(0.65, 2.5) ... @qml.qnode(dev) ... def circuit(x): ... qml.Hadamard(wires=0) ... qml.RX(x, wires=0) ... return qml.expval(qml.PauliX(0)) >>> print(circuit(-0.5)) 1.2629730888100839
or by transforming an existing QNode:
>>> @qml.qnode(dev) ... def circuit(x): ... qml.Hadamard(wires=0) ... qml.RX(x, wires=0) ... return qml.expval(qml.PauliX(0)) >>> circuit = my_transform(circuit, 0.65, 2.5) >>> print(circuit(-0.5)) 1.2629730888100839
Batch tape transforms are fully differentiable:
>>> x = np.array(-0.5, requires_grad=True) >>> gradient = qml.grad(circuit)(x) >>> print(gradient) 2.5800122591960153
Usage Details
Expansion functions
Tape expansion, decomposition, or manipulation may always be performed within the custom batch transform. However, by specifying a separate expansion function, PennyLane will be possible to access this separate expansion function where needed via
>>> my_transform.expand_fn
The provided
expand_fn
must have the same input arguments astransform_fn
and return atape
. Following the example above:def expand_fn(tape, a, b): stopping_crit = lambda obj: obj.name!="PhaseShift" return tape.expand(depth=10, stop_at=stopping_crit) my_transform = batch_transform(my_transform, expand_fn)
Note that:
the transform arguments
a
andb
must be passed to the expansion function, andthe expansion function must return a single tape.
Methods
construct
(tape, *targs, **tkwargs)Applies the batch tape transform to an input tape.
Register a custom QNode execution wrapper function for the batch transform.
default_qnode_wrapper
(qnode, targs, tkwargs)A wrapper method that takes a QNode and transform arguments, and returns a function that ‘wraps’ the QNode execution.
-
construct
(tape, *targs, **tkwargs)[source]¶ Applies the batch tape transform to an input tape.
- Parameters
tape (QuantumTape) – the tape to be transformed
*args – positional arguments to pass to the tape transform
**kwargs – keyword arguments to pass to the tape transform
- Returns
list of transformed tapes to execute and a post-processing function.
- Return type
tuple[list[tapes], callable]
-
custom_qnode_wrapper
(fn)[source]¶ Register a custom QNode execution wrapper function for the batch transform.
Example
def my_transform(tape, *targs, **tkwargs): ... return tapes, processing_fn @my_transform.custom_qnode_wrapper def my_custom_qnode_wrapper(self, qnode, targs, tkwargs): def wrapper_fn(*args, **kwargs): # construct QNode qnode.construct(args, kwargs) # apply transform to QNode's tapes tapes, processing_fn = self.construct(qnode.qtape, *targs, **tkwargs) # execute tapes and return processed result ... return processing_fn(results) return wrapper_fn
The custom QNode execution wrapper must have arguments
self
(the batch transform object),qnode
(the input QNode to transform and execute),targs
andtkwargs
(the transform arguments and keyword arguments respectively).It should return a callable object that accepts the same arguments as the QNode, and returns the transformed numerical result.
The default
default_qnode_wrapper()
method may be called if only pre- or post-processing dependent on QNode arguments is required:@my_transform.custom_qnode_wrapper def my_custom_qnode_wrapper(self, qnode, targs, tkwargs): transformed_qnode = self.default_qnode_wrapper(qnode) def wrapper_fn(*args, **kwargs): args, kwargs = pre_process(args, kwargs) res = transformed_qnode(*args, **kwargs) ... return ... return wrapper_fn
-
default_qnode_wrapper
(qnode, targs, tkwargs)[source]¶ A wrapper method that takes a QNode and transform arguments, and returns a function that ‘wraps’ the QNode execution.
The returned function should accept the same keyword arguments as the QNode, and return the output of applying the tape transform to the QNode’s constructed tape.