qml.templates.core.Subroutine

class Subroutine(definition, *, setup_inputs=<function _default_setup_inputs>, static_argnames=(), wire_argnames=('wires', ), compute_resources=None)[source]

Bases: object

The definition of a Subroutine, compatible both with program capture and backwards compatible with operators.

Parameters:
  • definition (Callable) – a quantum function that can contain both quantum and classical processing. The definition can return purely classical values or the outputs from mid circuit measurements, but it cannot return terminal statistics.

  • setup_inputs (Callable) – An function that can run preprocessing on the inputs before hitting definition. This can be used to make static arguments hashable for compatibility with program capture.

  • static_argnames (str | tuple[str]) – The name of arguments that are treated as static (trace- and compile-time constant).

  • wire_argnames (str | tuple[str]) – The name of arguments that represent wire registers. While the users can be more permissive in what they provide to wire arguments, the definition should treat all wire arguments as 1D arrays.

  • compute_resources (None | Callable) – A function for computing resources used by the function. It should only calculate the resources from the static arguments, the length of the wire registers, and the shape and dtype of the dynamic arguments. In the case of the specific resources depending on the specifics of a dynamic argument, a worse case scenario can be used.

For simple cases, a Subroutine can simply be created from a single quantum function, like:

from functools import partial
from pennylane.templates import Subroutine

def resources(x, y, wires):
    return {qml.RX: 1, qml.RY: 1}

@partial(Subroutine, compute_resources=resources)
def MyTemplate(x, y, wires):
    qml.RX(x, wires[0])
    qml.RY(y, wires[0])

@qml.qnode(qml.device('default.qubit'))
def c():
    MyTemplate(0.1, 0.2, 0)
    return qml.state()

c()
>>> print(qml.draw(c)())
0: ──MyTemplate(0.10,0.20)─┤  State
>>> print(qml.draw(c, level="device")())
0: ──RX(0.10)──RY(0.20)─┤  State
>>> print(qml.specs(c)().resources)
Total wire allocations: 1
Total gates: 1
Circuit depth: 1

Gate types:
  MyTemplate: 1

Measurements:
  state(all wires): 1

For multiple wire register inputs or use of a different name than "wires", the wire_argnames can be provided:

from functools import partial

@partial(Subroutine, wire_argnames=("register1", "register2"))
def MultiRegisterTemplate(register1, register2):
    for wire in register1:
        qml.X(wire)
    for wire in register2:
        qml.Z(wire)
>>> print(qml.draw(MultiRegisterTemplate)(0, [1,2]))
0: ─╭MultiRegisterTemplate─┤
1: ─├MultiRegisterTemplate─┤
2: ─╰MultiRegisterTemplate─┤

Static arguments are treated as compile-time constant with qml.qjit, and must be hashable. These are any inputs that are not numerical data or Operators. In the below example, the pauli_word argument is a string that is a static argument.

@partial(Subroutine, static_argnames="pauli_word")
def WithStaticArg(x, wires, pauli_word: str):
    qml.PauliRot(x, pauli_word, wires)

Setup Inputs:

Sometimes we want to allow the user to be able to provide a static input in a non-hashable format. For example, the user might provide an input as a list instead of a tuple. This can be done by providing the setup_inputs function. This function should have the same call signature as the template and return a tuple of position arguments and a dictionary of keyword arguments.

def setup_inputs(x, wires, pauli_words):
    return (x, wires, tuple(pauli_words)), {}

@partial(Subroutine, static_argnames="pauli_words", setup_inputs=setup_inputs)
def WithSetup(x, wires, pauli_words: list[str] | tuple[str,...]):
    for word in pauli_words:
        qml.PauliRot(x, word, wires)
>>> print(qml.draw(WithSetup)(0.5, [0, 1], ["XX", "XY", "XZ"]))
0: ─╭WithSetup(0.50)─┤
1: ─╰WithSetup(0.50)─┤

setup_inputs can also help us set default values for dynamic inputs. If an input is numerical (not static), but needs to default to a value contingent on the other inputs, that is allowed to occur in setup_inputs. This has to happen in setup_inputs because a dynamic, numerical input like y cannot be None when it hits the quantum function definition.

def setup_default_value(y : int | None = None, wires=()):
    if y is None:
        y = len(wires)
    return (y, wires), {}

setup_inputs should only interact with with compile-time information like static arguments, pytree structures, shapes, and dtypes, and not interact with any numerical values. Any manipulation or checks on values should occur inside the quantum function definition itself.

def BAD(x, wires, metadata):
    if x < 0:
        # do something
    ...

def GOOD(x, wires, metadata):
    if x.shape == ():
        # do something
    if metadata:
        # do something else
    ...

Integration with Graph decompositions:

Warning

Program capture Catalyst only supports graph decompositions for fundamental Gates with Ahead-Of-Time compiled decomposition rules and simple call signatures. Graph decompositions are not available for higher order algorithmic abstractions like Subroutine, or operators that decompose to Subroutine, in Catalyst.

To use Subroutine with graph-based decompositions, a function to compute the resources must be provided. The calculation of resources should only depend on the static arguments, the number of wires in each register, and the shape and dtype of the dynamic arguments. This will allow the calculation of the resources to performed in an abstract way.

def RXLayerResources(params, wires):
    return {qml.RX: qml.math.shape(params)[0]}

@partial(qml.templates.Subroutine, compute_resources=RXLayerResources)
def RXLayer(params, wires):
    for i in range(params.shape[0]):
        qml.RX(params[i], wires[i])

For example, we should be able to calculate the resources using the AbstractArray class.

>>> from pennylane.templates import AbstractArray
>>> abstract_params = AbstractArray((10,), float)
>>> abstract_wires = AbstractArray((10,))
>>> RXLayer.compute_resources(abstract_params, abstract_wires)
{<class 'pennylane.ops.qubit.parametric_ops_single_qubit.RX'>: 10}

We can create an Operator that can decompose to a Subroutine using AbstractArray and subroutine_resource_rep().

from pennylane.templates import AbstractArray, subroutine_resource_rep

class MyOp(qml.operation.Operation):
    pass

abstract_params = AbstractArray((3, ), float)
abstract_wires = AbstractArray((3, ))
rxlayer_rep = subroutine_resource_rep(RXLayer, abstract_params, abstract_wires)

@qml.decomposition.register_resources({rxlayer_rep: 1})
def MyOpDecomposition(wires):
    params = np.arange(3, dtype=float)
    RXLayer(params, wires)

qml.add_decomps(MyOp, MyOpDecomposition)
@qml.qnode(qml.device('default.qubit'))
def c():
    MyOp((0,1,2))
    return qml.expval(qml.Z(0))

qml.decomposition.enable_graph()
>>> print(qml.draw(c)())
0: ─╭MyOp─┤  <Z>
1: ─├MyOp─┤
2: ─╰MyOp─┤
>>> print(qml.draw(qml.decompose(c, max_expansion=1))())
0: ─╭RXLayer(M0)─┤  <Z>
1: ─├RXLayer(M0)─┤
2: ─╰RXLayer(M0)─┤

M0 =
[0. 1. 2.]
>>> print(qml.draw(qml.decompose(c, max_expansion=2))())
0: ──RX(0.00)─┤  <Z>
1: ──RX(1.00)─┤
2: ──RX(2.00)─┤

Use of Autograph:

Autograph converts Python control flow (if, for, while, etc.) into PennyLane’s control flow (for_loop(), cond(), while_loop()) that is compatible with traced arguments. The user’s choice of applying autograph on their workflow in qjit() does not effect the capture of a Subroutine. Autograph should instead be applied manually with run_autograph() to the quantum function as needed.

For example, is we have the template and qjit workflow:

@qml.templates.Subroutine
def f(x, wires):
    if x < 0:
        qml.X(wires)
    else:
        qml.Y(wires)

@qml.qjit(autograph=True)
@qml.qnode(qml.device('lightning.qubit', wires=1))
def c(x):
    f(x, 0)
    return qml.expval(qml.Z(0))
>>> c(0.5) 
Traceback (most recent call last):
    ...
CaptureError: Autograph must be used when Python control flow is dependent on a dynamic variable
(a function input). Please ensure that autograph is being correctly enabled with
`qml.capture.run_autograph` or disabled with `qml.capture.disable_autograph` or
consider using PennyLane native control flow functions like `qml.for_loop`, `qml.while_loop`,
or `qml.cond`.

In order to support a conditional on a dynamic value, we should either run_autograph to the quantum function definition itself or use qml.cond manually:

@qml.templates.Subroutine
@qml.capture.run_autograph
def UsingAutograph(x, wires):
    if x < 0:
        qml.X(wires)
    else:
        qml.Y(wires)

@qml.templates.Subroutine
def UsingCond(x, wires):
    qml.cond(x  > 0, qml.X, qml.Y)(wires)

dynamic_argnames

The names of the function arguments that are pytrees of numerical data.

name

A string representation to label the Subroutine.

signature

"The signature for the definition.

static_argnames

The names of arguments that are compile time constant.

wire_argnames

The names for the arguments that represent a register of wires.

dynamic_argnames

The names of the function arguments that are pytrees of numerical data. These are the arguments that are not static or wires.

name

A string representation to label the Subroutine.

signature

“The signature for the definition. Used to preprocess the user inputs.

static_argnames

The names of arguments that are compile time constant.

wire_argnames

The names for the arguments that represent a register of wires.

compute_resources(*args, **kwargs)

Calculate a condensed representation for the resources required for the Subroutine.

definition(*args, **kwargs)

The quantum function definition of the subroutine.

operator(*args[, id])

Create a SubroutineOp from the template.

setup_inputs(*args, **kwargs)

Perform and initial setup of the arguments.

compute_resources(*args, **kwargs)[source]

Calculate a condensed representation for the resources required for the Subroutine.

definition(*args, **kwargs)[source]

The quantum function definition of the subroutine.

operator(*args, id=None, **kwargs)[source]

Create a SubroutineOp from the template.

setup_inputs(*args, **kwargs)[source]

Perform and initial setup of the arguments.