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, 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

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

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)
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() 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)
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 to level=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 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)])

The available options for levels are different for circuits which have been compiled using Catalyst. There are 2 broad ways to use specs on 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 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 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 returned CircuitSpecs object’s resources field is a dictionary mapping level names to their associated SpecsResources object. The keys to this dictionary are returned as the level attribute of the CircuitSpecs object.

>>> 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