qml.templates.core.Subroutine¶
- class Subroutine(definition, *, setup_inputs=<function _default_setup_inputs>, static_argnames=(), wire_argnames=('wires', ), compute_resources=None, exact_resources=True)[source]¶
Bases:
objectThe 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.
exact_resources (bool) – whether or not
compute_resourcesis exact. Similar toregister_resources, this option is used purely for testing purposes.
For simple cases, a
Subroutinecan 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) Wire allocations: 1 Total gates: 1 Gate counts: - MyTemplate: 1 Measurements: - state(all wires): 1 Depth: 1
For multiple wire register inputs or use of a different name than
"wires", thewire_argnamescan 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, thepauli_wordargument 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
listinstead of atuple. This can be done by providing thesetup_inputsfunction. 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_inputscan 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 insetup_inputs. This has to happen insetup_inputsbecause a dynamic, numerical input likeycannot beNonewhen 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_inputsshould 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 toSubroutine, in Catalyst.To use
Subroutinewith 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 anddtypeof 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
AbstractArrayclass.>>> 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
Operatorthat can decompose to aSubroutineusingAbstractArrayandsubroutine_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 inqjit()does not effect the capture of aSubroutine. Autograph should instead be applied manually withrun_autograph()to the quantum function as needed.For example, is we have the template and
qjitworkflow:@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_autographto the quantum function definition itself or useqml.condmanually:@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)
Attributes
The names of the function arguments that are pytrees of numerical data.
Whether or not the
compute_resourcesfunction provides the exact resources.A string representation to label the Subroutine.
"The signature for the definition.
The names of arguments that are compile time constant.
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.
- exact_resources¶
Whether or not the
compute_resourcesfunction provides the exact resources. Used for testing.
- 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.
Methods
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
SubroutineOpfrom the template.setup_inputs(*args, **kwargs)Perform and initial setup of the arguments.