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_transform_program()for more details on the available levels. 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 Resource specifications: Total wire allocations: 2 Total gates: 98 Circuit depth: 98 Gate types: RX: 1 CNOT: 1 Evolution: 96 Measurements: probs(all wires): 1
Usage Details
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) Total wire allocations: 2 Total gates: 6 Circuit depth: 6 Gate types: RandomLayers: 1 RX: 2 SWAP: 1 PauliX: 2 Measurements: expval(Sum(num_wires=2, num_terms=2)): 1
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) Total wire allocations: 2 Total gates: 3 Circuit depth: 3 Gate types: RandomLayers: 1 RX: 2 Measurements: expval(Sum(num_wires=2, num_terms=2)): 1
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) Total wire allocations: 2 Total gates: 2 Circuit depth: 1 Gate types: RY: 1 RX: 1 Measurements: expval(Sum(num_wires=2, num_terms=2)): 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)])
Using specs on workflows compiled with Catalyst
The available options for
levelsare different for circuits which have been compiled using Catalyst. There are 2 broad ways to usespecson compiled QNodes: runtime resource tracking, and pass-by-pass specs for user applied compilation passes.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.qml.capture.enable() # Enable program capture to allow these transforms to be applied only as MLIR passes 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 Resource specifications: Total wire allocations: 3 Total gates: 2 Circuit depth: 2 Gate types: CNOT: 1 RX: 1 Measurements: No measurements.
Warning
Measurement data is not currently supported with runtime resource tracking, so measurement data may show as missing.
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 passes
The 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 0 always corresponds to the original circuit before any user transforms have been applied, and incremental levels correspond to the aggregate of user transforms in the order in which they were applied.
In addition,
"all"may show 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 returned, 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) Level: ['Before transforms', 'Before MLIR Passes (MLIR-0)', 'cancel-inverses (MLIR-1)', 'merge-rotations (MLIR-2)'] Resource specifications: Level = Before transforms: Total wire allocations: 2 Total gates: 5 Circuit depth: Not computed Gate types: RX: 2 PauliX: 2 CNOT: 1 Measurements: probs(all wires): 1 ------------------------------------------------------------ Level = Before MLIR Passes (MLIR-0): Total wire allocations: 3 Total gates: 5 Circuit depth: Not computed Gate types: RX: 2 PauliX: 2 CNOT: 1 Measurements: probs(all wires): 1 ------------------------------------------------------------ Level = cancel-inverses (MLIR-1): Total wire allocations: 3 Total gates: 3 Circuit depth: Not computed Gate types: RX: 2 CNOT: 1 Measurements: probs(all wires): 1 ------------------------------------------------------------ Level = merge-rotations (MLIR-2): Total wire allocations: 3 Total gates: 2 Circuit depth: Not computed Gate types: RX: 1 CNOT: 1 Measurements: probs(all wires): 1
When invoked with
"all"as above, the returnedCircuitSpecsobject’sresourcesfield is a dictionary mapping level names to their associatedSpecsResourcesobject. The keys to this dictionary are returned as thelevelattribute of theCircuitSpecsobject.>>> print(all_specs.level) ['Before transforms', 'Before MLIR Passes (MLIR-0)', 'cancel-inverses (MLIR-1)', '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)']) Total wire allocations: 3 Total gates: 2 Circuit depth: Not computed Gate types: RX: 1 CNOT: 1 Measurements: probs(all wires): 1