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!
About the author
Anuj Apte
Anuj is a PhD student at the University of Chicago. His research interests include quantum field theory with applications to topological phases and quantum computing.