qml.specs¶
- specs(qnode, level=None, compute_depth=None)[source]¶
Provides the specifications of a quantum circuit.
This transform converts a QNode into a callable that provides resource information about the circuit after applying the specified transforms, expansions, and/or compilation passes.
- Parameters:
qnode (
QNode|QJIT) – the QNode to calculate the specifications for.- Keyword Arguments:
level (str | int | slice | iter[int]) – An indication of which transforms, expansions, and passes to apply before computing the resource information. See
get_compile_pipeline()for more details on the available levels. Forqjit-compiled workflows, see the sections below for more information. Default is"device"for qjit-compiled workflows or"gradient"otherwise.compute_depth (bool) – Whether to compute the depth of the circuit. If
False, circuit depth will not be included in the output. By default,specswill always attempt to calculate circuit depth (behaves asTrue), except where not available, such as in pass-by-pass analysis withqjit()present.
- Returns:
A function that has the same argument signature as
qnode. This function returns aCircuitSpecsobject containing theqnodespecifications, including gate and measurement data, wire allocations, device information, shots, and more.
Warning
Computing circuit depth is computationally expensive and can lead to slower
specscalculations. If circuit depth is not needed, setcompute_depth=False.Example
from pennylane import numpy as pnp dev = qml.device("default.qubit", wires=2) x = pnp.array([0.1, 0.2]) Hamiltonian = qml.dot([1.0, 0.5], [qml.X(0), qml.Y(0)]) gradient_kwargs = {"shifts": pnp.pi / 4} @qml.qnode(dev, diff_method="parameter-shift", gradient_kwargs=gradient_kwargs) def circuit(x, add_ry=True): qml.RX(x[0], wires=0) qml.CNOT(wires=(0,1)) qml.TrotterProduct(Hamiltonian, time=1.0, n=4, order=2) if add_ry: qml.RY(x[1], wires=1) qml.TrotterProduct(Hamiltonian, time=1.0, n=4, order=4) return qml.probs(wires=(0,1))
>>> print(qml.specs(circuit)(x, add_ry=False)) Device: default.qubit Device wires: 2 Shots: Shots(total=None) Level: gradient Wire allocations: 2 Total gates: 98 Gate counts: - RX: 1 - CNOT: 1 - Evolution: 96 Measurements: - probs(all wires): 1 Depth: 98
Note
The available options for
levelsare different for circuits which have been compiled using Catalyst. There are 2 broad ways to usespecsonqjitcompiled QNodes:Runtime resource tracking via mock circuit execution
Pass-by-pass resource collection for user applied compilation passes
Specs with Tape Transforms
Here you can see how the number of gates and their types change as we apply different amounts of transforms through the
levelargument:dev = qml.device("default.qubit") gradient_kwargs = {"shifts": pnp.pi / 4} @qml.transforms.merge_rotations @qml.transforms.undo_swaps @qml.transforms.cancel_inverses @qml.qnode(dev, diff_method="parameter-shift", gradient_kwargs=gradient_kwargs) def circuit(x): qml.RandomLayers(pnp.array([[1.0, 2.0]]), wires=(0, 1)) qml.RX(x, wires=0) qml.RX(-x, wires=0) qml.SWAP((0, 1)) qml.X(0) qml.X(0) return qml.expval(qml.X(0) + qml.Y(1))
First, we can check the resource information of the QNode without any modifications by specifying
level=0. Note thatlevel=topwould return the same results:>>> print(qml.specs(circuit, level=0)(0.1).resources) Wire allocations: 2 Total gates: 6 Gate counts: - RandomLayers: 1 - RX: 2 - SWAP: 1 - PauliX: 2 Measurements: - expval(Sum(num_wires=2, num_terms=2)): 1 Depth: 6
We can analyze the effects of, for example, applying the first two transforms (
cancel_inverses()andundo_swaps()) by settinglevel=2. The result will show thatSWAPandPauliXare not present in the circuit:>>> print(qml.specs(circuit, level=2)(0.1).resources) Wire allocations: 2 Total gates: 3 Gate counts: - RandomLayers: 1 - RX: 2 Measurements: - expval(Sum(num_wires=2, num_terms=2)): 1 Depth: 3
We can then check the resources after applying all transforms with
level="device"(which, in this particular example, would be equivalent tolevel=3):>>> print(qml.specs(circuit, level="device")(0.1).resources) Wire allocations: 2 Total gates: 2 Gate counts: - RY: 1 - RX: 1 Measurements: - expval(Sum(num_wires=2, num_terms=2)): 1 Depth: 1
If a QNode with a tape-splitting transform is supplied to the function, with the transform included in the desired transforms, the specs output’s resources field is instead returned as a list with a
SpecsResourcesfor each resulting tape:dev = qml.device("default.qubit") H = qml.Hamiltonian([0.2, -0.543], [qml.X(0) @ qml.Z(1), qml.Z(0) @ qml.Y(2)]) gradient_kwargs = {"shifts": pnp.pi / 4} @qml.transforms.split_non_commuting @qml.qnode(dev, diff_method="parameter-shift", gradient_kwargs=gradient_kwargs) def circuit(): qml.RandomLayers(qml.numpy.array([[1.0, 2.0]]), wires=(0, 1)) return qml.expval(H)
>>> from pprint import pprint >>> pprint(qml.specs(circuit, level="user")()) CircuitSpecs(device_name='default.qubit', num_device_wires=None, shots=Shots(total_shots=None, shot_vector=()), level='user', resources=[SpecsResources(gate_types={'RandomLayers': 1}, gate_sizes={2: 1}, measurements={'expval(Prod(num_wires=2, num_terms=2))': 1}, num_allocs=2, depth=1), SpecsResources(gate_types={'RandomLayers': 1}, gate_sizes={2: 1}, measurements={'expval(Prod(num_wires=2, num_terms=2))': 1}, num_allocs=3, depth=1)])
Runtime Specs with Catalyst
Runtime resource tracking (specified by
level="device") works by mock-executing the desired workflow and tracking the number of times a given gate has been applied. This mock-execution happens after all compilation steps, and should be highly accurate to the final gatecounts of running on a real device.dev = qml.device("lightning.qubit", wires=3) @qml.qjit @qml.transforms.merge_rotations @qml.transforms.cancel_inverses @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) qml.RX(x, wires=0) qml.X(0) qml.X(0) qml.CNOT([0, 1]) return qml.probs()
>>> print(qml.specs(circuit, level="device")(1.23)) Device: lightning.qubit Device wires: 3 Shots: Shots(total=None) Level: device Wire allocations: 3 Total gates: 2 Gate counts: - CNOT: 1 - RX: 1 Measurements: - probs(all wires): 1 Depth: 2
Pass-by-pass Specs with Catalyst
Pass-by-pass specs analyze the intermediate representations of compiled circuits. This can be helpful for determining how circuit resources change after a given transform or compilation pass.
Warning
Some resource information from pass-by-pass specs may be estimated, since it is not always possible to determine exact resource usage from intermediate representations. For example, resources contained in a
forloop with a non-static range or awhileloop will only be counted as if one iteration occurred. Additionally, resources contained in conditional branches fromiforswitchstatements will take a union of resources over all branches, providing a tight upper-bound.Due to similar technical limitations, depth computation is not available for pass-by-pass specs.
Pass-by-pass specs can be obtained by providing one of the following values for the
levelargument:An
int: the desired pass level of a user-applied pass, see the note belowA marker name (str): The name of an applied
qml.markerpassAn iterable: A
list,tuple, or similar containing ints and/or marker names. Should be sorted in ascending pass order with no duplicatesThe string
"all": To output information about all user-applied transforms and compilation passesThe string
"all-mlir": To output information about all compilation passes at the MLIR level only
Note
The level arguments only take into account user-applied transforms and compilation passes. Level
0always corresponds to the original circuit before any user-specified tape transforms or compilation passes have been applied, and incremental levels correspond to the aggregate of user-specified transforms and passes in the order in which they are applied.This may include an MLIR “lowering” pass that indicates that the program had to be lowered into MLIR for further compilation with Catalyst. If such a pass is included, it will be placed after all tape transforms but before all other MLIR passes.
Here is an example using
level="all"on the circuit from the previous code example:>>> all_specs = qml.specs(circuit, level="all")(1.23) >>> print(all_specs) Device: lightning.qubit Device wires: 3 Shots: Shots(total=None) Levels: - 0: Before MLIR Passes (MLIR-0) - 1: cancel-inverses (MLIR-1) - 2: merge-rotations (MLIR-2) ↓Metric Level→ | 0 | 1 | 2 --------------------------------- Wire allocations | 3 | 3 | 3 Total gates | 5 | 3 | 2 Gate counts: | - RX | 2 | 2 | 1 - PauliX | 2 | 0 | 0 - CNOT | 1 | 1 | 1 Measurements: | - probs(all wires) | 1 | 1 | 1
When invoked with an iterable of levels, or
"all"as above, the returnedCircuitSpecsobject’sresourcesfield is a dictionary mapping transform names (or marker labels) to their associatedSpecsResourcesobject. The keys to this dictionary have human readable names. To use the int level name directly, use thelevelattribute of the returnedCircuitSpecsobject, which maps int levels to their associated transform or pass name. For example, the level names for the above example>>> print(all_specs.level) {0: 'Before MLIR Passes (MLIR-0)', 1: 'cancel-inverses (MLIR-1)', 2: 'merge-rotations (MLIR-2)'}
The resources associated with a particular level can be accessed using the returned level name as follows:
>>> print(all_specs.resources['merge-rotations (MLIR-2)']) Wire allocations: 3 Total gates: 2 Gate counts: - RX: 1 - CNOT: 1 Measurements: - probs(all wires): 1 Depth: Not computed
Or, equivalently, by using the int level directly:
>>> print(all_specs.resources[all_specs.level[2]]) Wire allocations: 3 Total gates: 2 Gate counts: - RX: 1 - CNOT: 1 Measurements: - probs(all wires): 1 Depth: Not computed
Warning
Certain transforms, like the
split-non-commutingtransform, can result in multiple output tapes. In this case, the resources for that level will be returned as a list ofSpecsResourcesobjects. When printed, these split tapes will be shown as individual columns.dev = qml.device("lightning.qubit", wires=3) @qml.qjit @qml.transforms.cancel_inverses @qml.transforms.split_non_commuting @qml.qnode(dev) def circuit(): qml.X(0) qml.X(0) return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))
>>> print(qml.specs(circuit, level="all")()) Device: lightning.qubit Device wires: 3 Shots: Shots(total=None) Levels: - 0: Before Tape Transforms - 1: split_non_commuting - 2: Before MLIR Passes (MLIR-0) - 3: cancel-inverses (MLIR-1) ↓Metric Level→ | 0 | 1-a | 1-b | 2 | 3 --------------------------------------------------- Wire allocations | 1 | 1 | 1 | 6 | 6 Total gates | 2 | 2 | 2 | 4 | 0 Gate counts: | - PauliX | 2 | 2 | 2 | 4 | 0 Measurements: | - expval(PauliZ) | 1 | 1 | 0 | 1 | 1 - expval(PauliX) | 1 | 0 | 1 | 1 | 1
Note that in the above example, the
split_non_commutingtransform results in two tapes, which are labeled as1-aand1-bin the output. The resources for these tapes are shown separately, and the level name for both tapes is the same since they come from the same transform. Multiple tapes may not display as separate columns for MLIR passes since MLIR passes do not operate on tapes directly.