PennyLane
Install
Install

Related materials

  • Related contentQuantum Teleportation
  • Related contentMeasurement-based quantum computation
  • Related contentHow to collect statistics of mid-circuit measurements

Contents

  1. Dynamic circuit with a single MCM and conditional
  2. How to dynamically prepare half-filled basis states
  3. Postselecting dynamically prepared states
  4. Dynamically correct quantum states
  5. About the author

Downloads

  • Download Python script
  • Download Notebook
  • View on GitHub
  1. Demos/
  2. Getting Started/
  3. How to create dynamic circuits with mid-circuit measurements

How to create dynamic circuits with mid-circuit measurements

David Wierichs

David Wierichs

Published: May 02, 2024. Last updated: November 05, 2024.

Measuring qubits in the middle of a quantum circuit execution can be useful in many ways. From understanding the inner workings of a circuit, hardware characterization, modeling and error mitigation, to error correction, quantum teleportation, algorithmic improvements and even up to full computations encoded as measurements in measurement-based quantum computation (MBQC).

Before turning to any of these advanced topics, it is worthwhile to familiarize ourselves with the syntax and features around mid-circuit measurements (MCMs). In this how-to, we will focus on dynamic quantum circuits that use control flow based on MCMs. Most of the advanced concepts mentioned above incorporate MCMs in this way, making it a key ingredient to scalable quantum computing.

If you want to learn more or are looking for how to collect statistics of mid-circuit measurements, have a read of the how-to on MCM statistics.

demos/_static/demonstration_assets/how_to_create_dynamic_mcm_circuits/socialthumbnail_how_to_create_dynamic_mcm_circuits.png

Dynamic circuit with a single MCM and conditional

We start with a minimal dynamic circuit on two qubits.

  • It rotates one qubit about the X-axis and applies a phase to the other qubit using a T gate.

  • Both qubits are entangled with a CNOT gate.

  • The second qubit is measured with measure().

  • If we measured a 1 in the previous step, an S gate is applied to the first qubit. This conditioned operation is realized with cond().

  • Finally, the expectation value of the Pauli Y operator on the first qubit is returned.

import pennylane as qml
import numpy as np

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


@qml.qnode(dev, interface="numpy")
def circuit(x):
    qml.RX(x, 0)
    qml.T(1)
    qml.CNOT(wires=[0, 1])
    mcm = qml.measure(1)
    qml.cond(mcm, qml.S)(wires=0)

    return qml.expval(qml.Y(0))


x = 1.361
print(circuit(x))
0.0

After this minimal working example, we now construct a more complex circuit showcasing more features of MCMs and dynamic circuits in PennyLane. We start with some short preparatory definitions.

How to dynamically prepare half-filled basis states

We now turn to a more complex example of a dynamic circuit. We will build a circuit that non-deterministically initializes half-filled computational basis states, i.e., basis states with as many \(1\)s as \(0\)s.

The procedure is as follows:

  • Single-qubit rotations and a layer of CNOT gates create an entangled state on the first three qubits.

  • The first three qubits are measured and for each measured \(0,\) another qubit is excited from the \(|0\rangle\) state to the \(|1\rangle\) state.

First, we define a quantum subprogram that creates the initial state:

def init_state(x):
    # Rotate the first three qubits
    for w in range(3):
        qml.RX(x[w], w)
    # Entangle the first three qubits
    qml.CNOT([0, 1])
    qml.CNOT([1, 2])
    qml.CNOT([2, 0])

With this subroutine in our hands, let’s define the full QNode. For this, we also create a shot-based device.

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


@qml.qnode(dev)
def create_half_filled_state(x):
    init_state(x)
    for w in range(3):
        # Measure one qubit at a time and flip another, fresh qubit if measured 0
        mcm = qml.measure(w)
        qml.cond(~mcm, qml.X)(w + 3)

    return qml.counts(wires=range(6))

Before running this QNode, let’s sample some random input parameters and draw the circuit:

np.random.seed(652)
x = np.random.random(3) * np.pi

print(qml.draw(create_half_filled_state)(x))
0: ──RX(1.33)─╭●────╭X──┤↗├────────────────────┤ ╭Counts
1: ──RX(1.63)─╰X─╭●─│────║──────┤↗├────────────┤ ├Counts
2: ──RX(0.92)────╰X─╰●───║───────║──────┤↗├────┤ ├Counts
3: ──────────────────────║───X───║───────║─────┤ ├Counts
4: ──────────────────────║───║───║───X───║─────┤ ├Counts
5: ──────────────────────║───║───║───║───║───X─┤ ╰Counts
                         ╚═══╝   ╚═══╝   ╚═══╝

We see an initial block of gates that prepares the starting state on the first three qubits, followed by pairs of measurements and conditional bit flips, applied to pairs of qubits.

Great, now let’s finally see if it works:

counts = create_half_filled_state(x)
print(f"Sampled bit strings:\n{list(counts.keys())}")
Sampled bit strings:
['000111', '010101', '011100', '100011', '101010', '110001', '111000']

Indeed, we created half-filled computational basis states, each with its own probability:

print("The probabilities for the bit strings are:")
for key, val in counts.items():
    print(f"    {key}: {val/shots*100:4.1f} %")
The probabilities for the bit strings are:
    000111: 19.0 %
    010101:  6.0 %
    011100: 21.0 %
    100011: 19.0 %
    101010:  3.0 %
    110001:  3.0 %
    111000: 29.0 %

Postselecting dynamically prepared states

We can select only some of these half-filled states by postselecting on measurement outcomes:

@qml.qnode(dev)
def postselect_half_filled_state(x, selection):
    init_state(x)
    for w in range(3):
        # Postselect the measured qubit to match the selection criterion
        mcm = qml.measure(w, postselect=selection[w])
        qml.cond(~mcm, qml.X)(w + 3)

    return qml.counts(wires=range(6))

As an example, suppose we wanted half-filled states that have a \(0\) in the first and a \(1\) in the third position. We do not postselect on the second qubit, which we can indicate by passing None to the postselect argument of measure(). Again, before running the circuit, let’s draw it first:

selection = [0, None, 1]
print(qml.draw(postselect_half_filled_state)(x, selection))
0: ──RX(1.33)─╭●────╭X──┤↗₀├─────────────────────┤ ╭Counts
1: ──RX(1.63)─╰X─╭●─│────║───────┤↗├─────────────┤ ├Counts
2: ──RX(0.92)────╰X─╰●───║────────║──────┤↗₁├────┤ ├Counts
3: ──────────────────────║────X───║───────║──────┤ ├Counts
4: ──────────────────────║────║───║───X───║──────┤ ├Counts
5: ──────────────────────║────║───║───║───║────X─┤ ╰Counts
                         ╚════╝   ╚═══╝   ╚════╝

Note the indicated postselection values next to the drawn MCMs.

Time to run the postselecting circuit:

counts = postselect_half_filled_state(x, selection)
postselected_shots = sum(counts.values())

print(f"Obtained {postselected_shots} out of {shots} samples after postselection.")
print("The probabilities for the postselected bit strings are:")
for key, val in counts.items():
    print(f"    {key}: {val/postselected_shots*100:4.1f} %")
Obtained 17 out of 100 samples after postselection.
The probabilities for the postselected bit strings are:
    001110: 11.8 %
    011100: 88.2 %

We successfully postselected on the desired properties of the computational basis state. Note that the number of returned samples is reduced, because those samples that do not meet the postselection criteria are discarded entirely.

Dynamically correct quantum states

If we do not want to postselect the prepared states but still would like to guarantee some of the qubits to be in a selected state, we can instead flip the corresponding pairs of bits if we measured the undesired state:

@qml.qnode(dev)
def create_selected_half_filled_state(x, selection):
    init_state(x)
    all_mcms = []
    for w in range(3):
        # Don't postselect on the selection criterion, but store the MCM for later
        mcm = qml.measure(w)
        qml.cond(~mcm, qml.X)(w + 3)
        all_mcms.append(mcm)

    for w, sel, mcm in zip(range(3), selection, all_mcms):
        # If the postselection criterion is not None, flip the corresponding pair
        # of qubits conditioned on the mcm not satisfying the selection criterion
        if sel is not None:
            qml.cond(mcm != sel, qml.X)(w)
            qml.cond(mcm != sel, qml.X)(w + 3)

    return qml.counts(wires=range(6))


print(qml.draw(create_selected_half_filled_state)(x, selection))
0: ──RX(1.33)─╭●────╭X──┤↗├─────────────────────X──────────┤ ╭Counts
1: ──RX(1.63)─╰X─╭●─│────║──────┤↗├─────────────║──────────┤ ├Counts
2: ──RX(0.92)────╰X─╰●───║───────║──────┤↗├─────║─────X────┤ ├Counts
3: ──────────────────────║───X───║───────║──────║──X──║────┤ ├Counts
4: ──────────────────────║───║───║───X───║──────║──║──║────┤ ├Counts
5: ──────────────────────║───║───║───║───║───X──║──║──║──X─┤ ╰Counts
                         ╚═══╩═══║═══║═══║═══║══╩══╝  ║  ║
                                 ╚═══╝   ╚═══╩════════╩══╝

We can see how the measured values are fed not only into the original conditioned operation, but also into the two additional bit flips for our “correction” procedure, as long as the selection criterion is not None. Let’s execute the circuit:

counts = create_selected_half_filled_state(x, selection)
selected_shots = sum(counts.values())

print(f"Obtained all {selected_shots} of {shots} samples because we did not postselect")
print("The probabilities for the selected bit strings are:")
for key, val in counts.items():
    print(f"    {key}: {val/selected_shots*100:4.1f} %")
Obtained all 100 of 100 samples because we did not postselect
The probabilities for the selected bit strings are:
    001110: 47.0 %
    011100: 53.0 %

Note that we kept all samples because we did not postselect, and that (in this particular case) this evened out the probabilities of measuring either of the two possible bit strings. Also, note that we conditionally applied the bit flip operators qml.X by comparing an MCM result to the corresponding selection criterion (mcm!=sel). More generally, MCM results in PennyLane can be processed with standard arithmetic operations. For details, see the introduction to MCMs and the documentation of measure().

And this is how to create dynamic circuits in PennyLane with mid-circuit measurements!

About the author

David Wierichs
David Wierichs

David Wierichs

I like to think about differentiation and representations of quantum programs, and I enjoy coding up research ideas and useful features for anyone to use in PennyLane.

Total running time of the script: (0 minutes 0.664 seconds)

Share demo

Ask a question on the forum

Related Demos

Quantum Teleportation

Measurement-based quantum computation

How to collect statistics of mid-circuit measurements

Introduction to mid-circuit measurements

Building a quantum lock using phase kickback

Constant-depth preparation of matrix product states with dynamic circuits

Pauli Propagation: Classically estimating expectation values from parametrized quantum circuits

Turning quantum nodes into Torch Layers

Basic arithmetic with the quantum Fourier transform (QFT)

Turning quantum nodes into Keras Layers

PennyLane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Built by researchers, for research. Created with ❤️ by Xanadu.

Research

  • Research
  • Performance
  • Hardware & Simulators
  • Demos
  • Quantum Compilation
  • Quantum Datasets

Education

  • Teach
  • Learn
  • Codebook
  • Coding Challenges
  • Videos
  • Glossary

Software

  • Install PennyLane
  • Features
  • Documentation
  • Catalyst Compilation Docs
  • Development Guide
  • API
  • GitHub
Stay updated with our newsletter

© Copyright 2025 | Xanadu | All rights reserved

TensorFlow, the TensorFlow logo and any related marks are trademarks of Google Inc.

Privacy Policy|Terms of Service|Cookie Policy|Code of Conduct