Dynamic quantum circuits

PennyLane allows using measurements in the middle of a quantum circuit. Such measurements are called mid-circuit measurements and can be used to shape the structure of the circuit dynamically, and to gather information about the quantum state during the circuit execution.

Available features

Mid-circuit measurements

The function to perform a mid-circuit measurement in PennyLane is measure(), and can be used as follows:

dev = qml.device("default.qubit")

@qml.qnode(dev)
def my_qnode(x, y):
    qml.RY(x, wires=0)
    qml.CNOT(wires=[0, 1])
    m_0 = qml.measure(1, reset=False, postselect=None)

    qml.cond(m_0, qml.RY)(y, wires=0)
    return qml.probs(wires=[0]), qml.expval(m_0)

See the following sections for details on measure(), cond(), and statistics of mid-circuit measurements, as well as information about simulation strategies and how to configure them further below. Additional information can be found in the documentation of the individual methods. Also consider our Introduction to mid-circuit measurements how-to on collecting statistics of mid-circuit measurements, and how-to on creating dynamic circuits with mid-circuit measurements.

Resetting qubits

Wires can be reused after making mid-circuit measurements. Moreover, a measured wire can be reset to the \(|0 \rangle\) state by setting reset=True in measure():

dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
def func():
    qml.PauliX(1)
    m_0 = qml.measure(1, reset=True)
    qml.PauliX(1)
    return qml.probs(wires=[1])

Executing this QNode:

>>> func()
tensor([0., 1.], requires_grad=True)

Postselecting mid-circuit measurements

PennyLane also supports postselecting on mid-circuit measurement outcomes by specifying the postselect keyword argument of measure(). By default, postselection discards outcomes that do not match the postselect argument. For example, specifying postselect=1 is equivalent to projecting the state vector onto the \(|1\rangle\) state, i.e., disregarding all outcomes where \(|0\rangle\) is measured:

dev = qml.device("default.qubit")

@qml.qnode(dev)
def func(x):
    qml.RX(x, wires=0)
    m_0 = qml.measure(0, postselect=1)
    return qml.sample(wires=0)

By postselecting on 1, we only consider results that measured the outcome 1. Executing this QNode with 10 shots yields

>>> func(np.pi / 2, shots=10)
array([1, 1, 1, 1, 1, 1, 1])

Note that only 7 samples are returned. This is because samples that do not meet the postselection criteria are discarded. This behaviour can be customized, see the section “Configuring mid-circuit measurements”.

Conditional operators

Users can create conditional operators controlled on mid-circuit measurements using cond(). The condition for a conditional operator may simply be the measured value returned by a measure() call, or we may construct a boolean condition based on such values and pass it to cond():

@qml.qnode(dev)
def qnode_conditional_op_on_zero(x, y):
    qml.RY(x, wires=0)
    qml.CNOT(wires=[0, 1])
    m_0 = qml.measure(1)

    qml.cond(m_0 == 0, qml.RY)(y, wires=0)
    return qml.probs(wires=[0])

pars = np.array([0.643, 0.246], requires_grad=True)
>>> qnode_conditional_op_on_zero(*pars)
tensor([0.88660045, 0.11339955], requires_grad=True)

For more examples, refer to the cond() documentation and the how-to on creating dynamic circuits with mid-circuit measurements.

Mid-circuit measurement statistics

Statistics of mid-circuit measurements can be collected along with terminal measurement statistics. Currently, counts(), expval(), probs(), sample(), and var() are supported.

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

@qml.qnode(dev)
def func(x, y):
    qml.RX(x, wires=0)
    m_0 = qml.measure(0)
    qml.cond(m_0, qml.RY)(y, wires=1)
    return qml.probs(wires=1), qml.probs(op=m_0)

Executing this QNode:

>>> func(np.pi / 2, np.pi / 4)
(tensor([0.9267767, 0.0732233], requires_grad=True),
 tensor([0.5, 0.5], requires_grad=True))

Users can also collect statistics on mid-circuit measurements manipulated using arithmetic/boolean operators. This works for both unary and binary operators. To see a full list of supported operators, refer to the measure() documentation. An example for collecting such statistics is shown below:

import pennylane as qml

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit(phi, theta):
    qml.RX(phi, wires=0)
    m_0 = qml.measure(wires=0)
    qml.RY(theta, wires=1)
    m_1 = qml.measure(wires=1)
    return qml.sample(~m_0 - 2 * m_1)

Executing this QNode:

>>> circuit(1.23, 4.56, shots=5)
array([-1, -2,  1, -1,  1])

Collecting statistics for mid-circuit measurements manipulated using arithmetic/boolean operators is supported with counts(), expval(), sample(), and var().

Moreover, statistics for multiple mid-circuit measurements can be collected by passing lists of mid-circuit measurement values to the measurement process:

import pennylane as qml

dev = qml.device("default.qubit")

@qml.qnode(dev)
def circuit(phi, theta):
    qml.RX(phi, wires=0)
    m_0 = qml.measure(wires=0)
    qml.RY(theta, wires=1)
    m_1 = qml.measure(wires=1)
    return qml.sample([m_0, m_1])

Executing this QNode:

>>> circuit(1.23, 4.56, shots=5)
array([[0, 1],
       [1, 1],
       [0, 1],
       [0, 0],
       [1, 1]])

Collecting statistics for sequences of mid-circuit measurements is supported with counts(), probs(), and sample().

Warning

When collecting statistics for a sequence of mid-circuit measurements, the sequence must not contain arithmetic expressions.

Simulation techniques

PennyLane currently offers three methods to simulate mid-circuit measurements on classical computers: the deferred measurements principle, dynamic one-shot sampling, and a tree-traversal approach. These methods differ in their memory requirements and computational cost, as well as their compatibility with other features such as shots and differentiation methods. While the requirements depend on details of the simulation, the expected scalings with respect to the number of mid-circuit measurements (and shots) are

Simulation technique

Memory

Time

Differentiation

shots

analytic

Deferred measurements

\(\mathcal{O}(2^{n_{MCM}})\)

\(\mathcal{O}(2^{n_{MCM}})\)

yes \({}^1\)

yes

yes

Dynamic one-shot

\(\mathcal{O}(1)\)

\(\mathcal{O}(n_{shots})\)

finite differences\({}^2\)

yes

no

Tree-traversal

\(\mathcal{O}(n_{MCM}+1)\)

\(\mathcal{O}(min(n_{shots}, 2^{n_{MCM}}))\)

finite differences\({}^2\)

yes

no

\({}^1\) Backpropagation and finite differences are fully supported. The adjoint method and the parameter-shift rule are supported if no postselection is used.

\({}^2\) In principle, parameter-shift differentiation is supported as long as no postselection is used. Parameters within conditionally applied operations will fall back to finite differences, so a proper value for h should be provided (see finite_diff()).

The strengths and weaknesses of the simulation techniques differ strongly and the best technique will depend on details of the simulation workflow. As a rule of thumb:

  • dynamic one-shot sampling excels in the many-measurements-few-shots regime,

  • the tree-traversal technique can handle large-scale simulations with many shots and measurements, and

  • deferred measurements are the generalist solution that enables mid-circuit measurement support under (almost) all circumstances, but at large memory cost. It is the only method supporting analytic simulations.

By default, QNodes use deferred measurements and dynamic one-shot sampling (if supported) when executed without and with shots, respectively. The method can be configured with the keyword argument mcm_method at QNode creation (see “Configuring mid-circuit measurements”).

Deferred measurements

A quantum function with mid-circuit measurements can be executed via the deferred measurement principle. In PennyLane, this technique is available via mcm_method="deferred" or as the transform defer_measurements().

The deferred measurement principle provides a powerful method to simulate mid-circuit measurements, conditional operations and measurement statistics in a differentiable and device-independent way. It adds an auxiliary qubit to the circuit for each mid-circuit measurement, leading to overheads of both memory and simulation time that scale exponentially with the number of measurements.

>>> deferred_qnode = qml.defer_measurements(my_qnode)
>>> pars = np.array([0.643, 0.246])
>>> deferred_qnode(*pars)
(tensor([0.90165331, 0.09834669], requires_grad=True),
 tensor(0.09984972, requires_grad=True))

The effect of deferring measurements becomes clear if we draw the QNode before and after applying the transform:

>>> print(qml.draw(my_qnode)(*pars))
0: ──RY(0.64)─╭●───────RY(0.25)─┤  Probs
1: ───────────╰X──┤↗├──║────────┤
                   ╚═══╩════════╡  <MCM>
>>> print(qml.draw(deferred_qnode)(*pars))
0: ──RY(0.64)─╭●────╭RY(0.25)─┤  Probs
1: ───────────╰X─╭●─│─────────┤
2: ──────────────╰X─╰●────────┤  <None>

Mid-circuit measurements are deferred to the end of the circuit, and conditionally applied operations become (quantumly) controlled operations.

Note

This method requires an additional qubit for each mid-circuit measurement, which limits the number of measurements that can be used both on classical simulators and quantum hardware.

Postselection with deferred measurements is only supported on DefaultQubit.

Dynamic one-shot sampling

Devices that natively support mid-circuit measurements can evaluate dynamic circuits by executing them one shot at a time, sampling a dynamic execution path for each shot.

In PennyLane, this technique is available via the QNode argument mcm_method="one-shot" or as the transform dynamic_one_shot(). As the name suggests, this transform only works for a QNode executing with finite shots and it requires the device to support mid-circuit measurements natively.

The dynamic_one_shot() transform is usually advantageous compared with the defer_measurements() transform in the many-mid-circuit-measurements and few-shots limit. This is because, unlike the deferred measurement principle, the method does not need an additional wire for every mid-circuit measurement in the circuit.

Warning

Dynamic circuits executed with shots should be differentiated with the finite difference method.

Tree-traversal algorithm

Dynamic circuit execution is akin to traversing a binary tree where each mid-circuit measurement corresponds to a node and gates between them correspond to edges. The tree-traversal algorithm explores this tree depth-first. It improves upon the dynamic one-shot approach above, which simulates a randomly chosen branch from beginning to end for each shot, by collecting all samples at a node or leaf at once.

In PennyLane, this technique is available via the QNode argument mcm_method="tree-traversal"; it is not a transform.

The tree-traversal algorithm combines the exponential savings of memory of the one-shot approach with sampling efficiency of deferred measurements. Neglecting overheads, simulating all branches requires the same amount of computations as defer_measurements(), but without the \(O(2^{n_{MCM}})\) memory cost. To save time, a copy of the state vector is made at every mid-circuit measurement, requiring \(n_{MCM}+1\) state vectors, an exponential improvement over defer_measurements(). Since the counts of many nodes come out to be zero for shot-based simulations, it is often possible to ignore entire sub-trees, thereby reducing the computational cost.

Warning

The tree-traversal algorithm is only supported by the DefaultQubit device, and currently does not support just-in-time (JIT) compilation.

Configuring mid-circuit measurements

As described above, there are multiple simulation techniques for circuits with mid-circuit measurements in PennyLane. They can be configured when initializing a QNode, using the following keywords:

  • mcm_method: Sets the method used for applying mid-circuit measurements. The options are "deferred", "one-shot", and "tree-traversal" for the three techniques described above. The default is mcm_method="one-shot" when executing with shots, and "deferred" otherwise. When using qjit(), there is the additional (default) option mcm_method="single-branch-statistics", which explores a single branch of the execution tree at random.

    Warning

    If the mcm_method argument is provided, the transforms for deferred measurements or dynamic one-shot sampling must not be applied manually to the QNode.

  • postselect_mode: Configures how invalid shots are handled when postselecting mid-circuit measurements with finite-shot circuits. Use "hw-like" to discard invalid samples. In this case, fewer than the total number of shots may be used to process results. Use "fill-shots" to sample the postselected value unconditionally, creating valid samples only. This is equivalent to sampling until the number of valid samples matches the total number of shots. The default is "hw-like".

    dev = qml.device("default.qubit", wires=3, shots=10)
    
    def circ():
        qml.Hadamard(0)
        m_0 = qml.measure(0, postselect=1)
        return qml.sample(qml.PauliZ(0))
    
    fill_shots = qml.QNode(circ, dev, mcm_method="one-shot", postselect_mode="fill-shots")
    hw_like = qml.QNode(circ, dev, mcm_method="one-shot", postselect_mode="hw-like")
    
    >>> fill_shots()
    array([-1., -1., -1., -1., -1., -1., -1., -1., -1., -1.])
    >>> hw_like()
    array([-1., -1., -1., -1., -1., -1., -1.])
    

    Note

    When using the jax interface, the postselection mode "hw-like" will change behaviour with the simulation technique.

    • For dynamic one-shot, invalid shots will not be discarded, but will be replaced by np.iinfo(np.int32).min. They will not be used for processing final results (like expectation values), but they will appear in the output of QNodes that return samples directly.

    • When using jax.jit, the combination "deferred" and "hw-like" is not supported, due to limitations of the defer_measurements() transform. This behaviour will change in the future.