How to get Device and Circuit Information

Anuj Apte (Xanadu resident)

The purpose of this article is to explain how to obtain information about devices and circuits using the qml.specs and qml.Tracker functions. Devices are key objects in PennyLane and necessary to execute quantum circuits and represent simulators or hardware devices.

The most basic device is the state-vector simulator default.qubit. In the following example we are going to import PennyLane and initialize a device with two qubits. Let us recall that we specify the number qubits (wires) in PennyLane via the wires argument passed to the qml.device function. The optional shots argument determines the number of times a circuit is run on a device. For a simulator, it determines the number of samples drawn from an analytic probability distribution.

import pennylane as qml
from pennylane import numpy as np

dev = qml.device("default.qubit", wires=2, shots=100)

The circuits that we run on a device are implemented in PennyLane as quantum functions. Devices and circuits can be combined to form quantum nodes represented by the QNode class. We do this by applying the qml.qnode decorator to the quantum function which takes a device argument. This device specifies the device on which the circuit will be executed. For illustration purposes, we will build a small circuit with parametrized RY rotation gates on each of the qubits, followed by a CNOT gate controlled by the first qubit.

@qml.qnode(dev)
def circuit(theta):
    qml.RY(theta[0], wires=0)
    qml.RY(theta[1], wires=1)
    qml.CNOT(wires=[0, 1])
    return qml.expval(qml.PauliZ(1))

theta = np.array([0.1, 0.3], requires_grad = True)
qml.draw_mpl(circuit)(theta)

The specifications of the device and the circuit can be obtained using the qml.specs function. The device specifications that you might find most useful are device_name and num_device_wires. Other important circuit specifications, shown below, are num_operations (unitary operators), num_observables (hermitian operators), num_trainable_params and depth. Note that, since there are two trainable parameters, it takes four evaluations to calculate the gradient using the parameter-shift rule.

qml.specs(circuit)(theta)
{'gate_sizes': defaultdict(int, {1: 2, 2: 1}),
 'gate_types': defaultdict(int, {'RY': 2, 'CNOT': 1}),
 'num_operations': 3,
 'num_observables': 1,
 'num_diagonalizing_gates': 0,
 'num_used_wires': 2,
 'depth': 2,
 'num_trainable_params': 2,
 'num_device_wires': 2,
 'device_name': 'default.qubit',
 'expansion_strategy': 'gradient',
 'gradient_options': {},
 'interface': 'autograd',
 'diff_method': 'best',
 'gradient_fn': 'pennylane.gradients.parameter_shift.param_shift',
 'num_gradient_executions': 4}

The information about device executions while running a circuit can be obtained using the qml.Tracker function. For standard devices, the function will track the number of executions, number of shots, number of batch executions, and batch execution length.

with qml.Tracker(dev) as tracker:
    qml.grad(circuit)(theta)

Querying totals outputs a running sum per keyword, while history stores a list of values passed for each keyword.

tracker.totals
{'executions': 5, 'shots': 500, 'batches': 2, 'batch_len': 5}

As expected, five hundred shots were needed in total: one hundred for the evaluation of the quantum function and four hundred for evaluating the circuits with the shifted values. Let’s now see the output of history.

tracker.history
{'executions': [1, 1, 1, 1, 1],
 'shots': [100, 100, 100, 100, 100],
 'batches': [1, 1],
 'batch_len': [1, 4]}

The tracker history makes it explicit that the gradient evaluation is batched (batch length = 4) separately from the function evaluation (batch length = 1). The last round of information from the tracker is stored in latest.

tracker.latest
{'batches': 1, 'batch_len': 4}

The callback function allows you to track long-running jobs. It requires totals, history, and latest as keyword arguments and is run each time a task, such as function evaluation, is executed on the device.

def shots_info(totals, history, latest):
    if 'shots' in latest:
        print("Total shots: ", totals['shots'])

with qml.Tracker(circuit.device, callback=shots_info) as tracker:
    qml.grad(circuit)(theta)
Total shots:  100
Total shots:  200
Total shots:  300
Total shots:  400
Total shots:  500

By default, information recorded by the tracker persists for multiple circuit executions. If we need to reuse the tracker across different circuit runs, we can set persistent=False. Notice below how the number of shots resets when we begin evaluating the circuit with different parameters.

with qml.Tracker(circuit.device, persistent=False, callback=shots_info) as tracker:
    qml.grad(circuit)(theta)

phi = np.array([0.2, 0.4], requires_grad = True)
with tracker:
    qml.grad(circuit)(phi)
Total shots:  100
Total shots:  200
Total shots:  300
Total shots:  400
Total shots:  500
Total shots:  100
Total shots:  200
Total shots:  300
Total shots:  400
Total shots:  500

Now that we know how to get this information out, we can better estimate the resources needed for our quantum and hybrid algorithms. For example, we might ask how many circuits we need to estimate the ground state of a molecule. I invite you to figure this out, you will be surprised by the answer!

Anuj Apte

Anuj Apte

I am a PhD student in Physics at University of Chicago with research interests in Quantum Computing and Quantum Field Theory. This summer, I am working with the architecture team on Mr. Mustard as a Xanadu resident.



Tags: device, circuit, qml