qml.batch_transform

class batch_transform(transform_fn, expand_fn=None, differentiable=True)[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.

  • 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. If None, 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.'''

    tape1 = qml.tape.JacobianTape()
    tape2 = qml.tape.JacobianTape()

    # loop through all operations on the input tape
    for op in tape.operations + tape.measurements:
        if op.name == "RX":
            wires = op.wires
            param = op.parameters[0]

            with tape1:
                qml.RY(a * qml.math.abs(param), wires=wires)

            with tape2:
                qml.RZ(b * qml.math.abs(param), wires=wires)
        else:
            for t in [tape1, tape2]:
                with t:
                    qml.apply(op)

    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:

>>> with qml.tape.JacobianTape() as tape:
...     qml.Hadamard(wires=0)
...     qml.RX(-0.5, wires=0)
...     qml.expval(qml.PauliX(0))
>>> tapes, fn = my_transform(tape, 0.65, 2.5)
>>> print(tapes[0].draw())
 0: ──H──RY(0.325)──┤ ⟨X⟩
>>> print(tapes[1].draw())
 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:

>>> gradient = qml.grad(circuit)(-0.5)
>>> print(gradient)
2.5800122591960153

construct(tape, *args, **kwargs)

Applies the batch tape transform to an input tape.

custom_qnode_wrapper(fn)

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, *args, **kwargs)[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 and tkwargs (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 the applying the tape transform to the QNode’s constructed tape.