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. For qjit-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, specs will always attempt to calculate circuit depth (behaves as True), except where not available, such as in pass-by-pass analysis with qjit() present.

Returns:

A function that has the same argument signature as qnode. This function returns a CircuitSpecs object containing the qnode specifications, including gate and measurement data, wire allocations, device information, shots, and more.

Warning

Computing circuit depth is computationally expensive and can lead to slower specs calculations. If circuit depth is not needed, set compute_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 levels are different for circuits which have been compiled using Catalyst. There are 2 broad ways to use specs on qjit compiled QNodes:

  • Runtime resource tracking via mock circuit execution

  • Pass-by-pass resource collection for user applied compilation passes

Here you can see how the number of gates and their types change as we apply different amounts of transforms through the level argument:

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 that level=top would 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() and undo_swaps()) by setting level=2. The result will show that SWAP and PauliX are 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 to level=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 SpecsResources for 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 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 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 for loop with a non-static range or a while loop will only be counted as if one iteration occurred. Additionally, resources contained in conditional branches from if or switch statements 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 level argument:

  • An int: the desired pass level of a user-applied pass, see the note below

  • A marker name (str): The name of an applied qml.marker pass

  • An iterable: A list, tuple, or similar containing ints and/or marker names. Should be sorted in ascending pass order with no duplicates

  • The 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-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 returned CircuitSpecs object’s resources field is a dictionary mapping transform names (or marker labels) to their associated SpecsResources object. The keys to this dictionary have human readable names. To use the int level name directly, use the level attribute of the returned CircuitSpecs object, 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-commuting transform, can result in multiple output tapes. In this case, the resources for that level will be returned as a list of SpecsResources objects. 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_commuting transform results in two tapes, which are labeled as 1-a and 1-b in 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.