Performing parity checks with mid-circuit measurements

Using mid-circuit measurements to perform parity checks

This tutorial explains how to use mid-circuit measurements to perform parity checks throughout a circuit's execution. A parity check, or parity measurement, is a way to check the equality of two qubits to determine whether an error has been introduced without destroying the quantum state of the qubit of interest.

To execute a simple three-qubit code parity check, an ancillary qubit is used as the subject of measurement. By performing controlled-NOT gates on the ancillary qubit dependent on the other two qubits and then performing a measurment, we can determine if the qubits are in the same state or not. This type of parity check is an essential concept in the implementation of Quantum Error Correction (QEC).

As you implement more complex algorithms, such as QEC, Fire Opal is an essential tool to reduce hardware error that arises during circuit execution and worsens as circuits get deeper.

In this tutorial you will:

  • Create a circuit with mid-circuit measurements
  • Run the circuit on a simulator and evaluate mid-circuit and final measurements
  • Run the circuit on a real device with Fire Opal
  • Run the circuit on a real device without Fire Opal
  • Compare final measurements across each of the executions

The goal is to validate that the mid-circuit measurements performed correctly across all three executions, according to the expected outcomes of the parity checks, and to compare the benefits of executing circuits with mid-circuit measurements to suppress noise.

1. Import packages and set up your IBM account credentials

You'll first need to install and import the packages used in this tutorial. To do so, run this script in your command line or terminal:

pip install fire-opal, qiskit, qctrl-visualizer, matplotlib, pylatexenc, qiskit-ibm-provider
import fireopal
import qiskit
from qiskit_ibm_runtime import QiskitRuntimeService
import qctrlvisualizer
import numpy as np
import matplotlib.pyplot as plt
# These are the properties for the publicly available provider for IBM backends.
# If you have access to a private provider and wish to use it, replace these values.
hub = "ibm-q"
group = "open"
project = "main"
token = "YOUR_IBM_TOKEN"
credentials = fireopal.credentials.make_credentials_for_ibmq(
    token=token, hub=hub, group=group, project=project
)

service = QiskitRuntimeService(
    token=token, instance=hub + "/" + group + "/" + project, channel="ibm_quantum"
)

2. Choose a backend and validate that it supports mid-circuit measurements

Be sure to choose a backend to which you have access through the specified provider, and then check the boolean flag multi_meas_enabled to determine if mid-circuit measurements are supported.

# Enter your desired IBM backend here.
backend_name = "desired_backend"
backend = provider.get_backend(backend_name)
assert backend.configuration().multi_meas_enabled

3. Define helper functions

These helper functions will be used to adjust the results for plotting and output purposes.

def get_prob_from_qubit_trace(counts, bit_index, shot_count, expected_measurement):
    """Return the probability between 0 and 100."""
    correct_outcome = 0
    for key in counts:
        if key[bit_index] == expected_measurement:
            correct_outcome += counts[key]
    return 100 * correct_outcome / shot_count


def print_parity_check_results(counts, shot_count):
    """
    Check the values of the mid-circuit measurements to compare to the expected values: 0, 1, 1.
    Since the mid-circuit measurements are listed to the left and in reverse order (qiskit ordering)
    we expect the 0th bitstring position to be 0, the 1st to be 1, and 2nd to be 1, each with 100% probability.
    """
    qiskit_ordering_map = {0: "2", 1: "1", 2: "0"}
    expected_measurement_map = {"0": "1", "1": "1", "2": "0"}
    for check_index in range(3):
        print(
            f"Parity check #{check_index+1}\n"
            f"We expect to see mid-circuit measurement {qiskit_ordering_map[check_index]} = '{expected_measurement_map[str(check_index)]}' with 100% probability\n"
            f"It was measured to be '{expected_measurement_map[str(check_index)]}' with probability "
            f"{np.round(get_prob_from_qubit_trace(counts, check_index, shot_count, expected_measurement_map[str(check_index)]), 1)}%"
        )

4. Define the circuit

For this parity check, three qubits will be used, and the mid-circuit and final measurements will be stored in different classical registers. Note that if different memory registers are not defined for mid- and end-circuit measurements, the mid-circuit measurements will be overwritten.

This circuit, will involve creating a Bell pair entangled state $\frac{1}{\sqrt{2}}|00\rangle + |11\rangle$ on the first two qubits, while parity checking these two qubits (q0,q1) through two controlled-NOT operations on a third ancilla qubit (q2=q0$\oplus$q1). In the first case, measuring q2 will yield the state '0' if the Bell state is prepared perfectly. The circuit will also perform parity checks on two other consecutive states synthetically corrupted, first corrupting q0 with an X gate and parity checking, and lastly corrupting q1 with an X gate and parity checking. The second parity check measurement on q2 should yield the value '1' after the first corruption, and then the final parity check on q2 should again be unchanged and remain as '1'.

qubit_count = 3
shot_count = 1024

quantum_register = qiskit.QuantumRegister(qubit_count)
final_measurement_reg = qiskit.ClassicalRegister(2, "final")
mid_circuit_measurement_reg = qiskit.ClassicalRegister(3, "mcm")

qc = qiskit.QuantumCircuit(
    quantum_register, final_measurement_reg, mid_circuit_measurement_reg
)

# Create a Bell pair entangled state between qubits 0 and 1.
qc.h(0)
qc.cx(0, 1)

# Perform a parity check.
qc.cx(0, 2)
qc.cx(1, 2)
qc.measure(2, mid_circuit_measurement_reg[0])

# Deliberately add error to change the state of qubit 0.
qc.x(0)

# Perform a parity check.
qc.cx(0, 2)
qc.cx(1, 2)
qc.measure(2, mid_circuit_measurement_reg[1])

# Deliberately add error to change the state of qubit 1.
qc.x(1)

# Perform a parity check.
qc.cx(0, 2)
qc.cx(1, 2)
qc.measure(2, mid_circuit_measurement_reg[2])

qc.measure(0, final_measurement_reg[0])
qc.measure(1, final_measurement_reg[1])

qc.draw("mpl", fold=-1)

png-1

5. Run the circuit on a simulator

First, the circuit will be ran on a simulator to verify the expected outcomes of the three mid-circuit measurements and the results at final measurement.

# Verify expected output via Aer qasm simulator.
simulation_jobs = qiskit.execute(
    qc, backend=qiskit.BasicAer.get_backend("qasm_simulator"), shots=shot_count
)
simulation_counts = qiskit.result.marginal_counts(simulation_jobs.result()).get_counts()
print("Simulated counts:", simulation_counts)
Simulated counts: {'110 00': 545, '110 11': 479}
print_parity_check_results(simulation_counts, shot_count)
Parity check #1
We expect to see mid-circuit measurement 2 = '1' with 100% probability
It was measured to be '1' with probability 100.0%
Parity check #2
We expect to see mid-circuit measurement 1 = '1' with 100% probability
It was measured to be '1' with probability 100.0%
Parity check #3
We expect to see mid-circuit measurement 0 = '0' with 100% probability
It was measured to be '0' with probability 100.0%

6. Run the circuit using Fire Opal

Next, the circuit will be run with Fire Opal to suppress hardware errors during execution.

Note: Fire Opal mitigates both the mid-circuit as well as the final measurements in post-processing to provide you with the optimal end results. This mitigation does not impact the values of the mid-circuit measurements.

fire_opal_results = fireopal.execute(
    circuits=[qc.qasm()],
    shot_count=shot_count,
    credentials=credentials,
    backend_name=backend_name,
)["results"][0]

fire_opal_counts = {
    bitstring: int(probability * shot_count)
    for bitstring, probability in fire_opal_results.items()
}

print("Fire Opal counts:", fire_opal_counts)
Fire Opal counts: [{'000 00': 16, '000 01': 1, '000 10': 3, '000 11': 23, '001 00': 6, '001 10': 1, '001 11': 10, '010 00': 32, '010 01': 31, '010 10': 24, '010 11': 33, '011 00': 2, '011 01': 11, '011 10': 7, '100 00': 2, '100 01': 47, '100 10': 43, '100 11': 2, '101 01': 2, '101 11': 1, '110 00': 343, '110 01': 11, '110 10': 14, '110 11': 345, '111 00': 4, '111 01': 2, '111 10': 3, '111 11': 5}]
print_parity_check_results(fire_opal_counts, shot_count)
Parity check #1
We expect to see mid-circuit measurement 2 = '1' with 100% probability
It was measured to be '1' with probability 80.5%
Parity check #2
We expect to see mid-circuit measurement 1 = '1' with 100% probability
It was measured to be '1' with probability 84.7%
Parity check #3
We expect to see mid-circuit measurement 0 = '0' with 100% probability
It was measured to be '0' with probability 94.7%

7. Run the circuit without Fire Opal

Finally, the circuit will be executed on the same bare metal IBM device, but without the use of Fire Opal to suppress hardware error.

ibm_counts = qiskit.execute(qc, backend=backend, shots=shot_count).result().get_counts()
print("Without Fire Opal counts:", ibm_counts)
Without Fire Opal counts: {'000 00': 25, '000 01': 2, '100 00': 7, '100 01': 62, '100 10': 25, '100 11': 3, '101 00': 1, '101 01': 1, '101 10': 4, '110 00': 231, '110 01': 20, '110 10': 32, '110 11': 336, '111 00': 4, '111 01': 2, '111 10': 1, '111 11': 5, '000 10': 8, '000 11': 13, '001 00': 12, '001 11': 17, '010 00': 33, '010 01': 10, '010 10': 102, '010 11': 24, '011 00': 1, '011 01': 14, '011 10': 28, '011 11': 1}
print_parity_check_results(ibm_counts, shot_count)
Parity check #1
We expect to see mid-circuit measurement 2 = '1' with 100% probability
It was measured to be '1' with probability 71.7%
Parity check #2
We expect to see mid-circuit measurement 1 = '1' with 100% probability
It was measured to be '1' with probability 82.4%
Parity check #3
We expect to see mid-circuit measurement 0 = '0' with 100% probability
It was measured to be '0' with probability 91.1%

As you can see, all three executions yielded results similar to our expected values, but the final probabilities obtained from the run with Fire Opal more closely match the expected outcomes of the parity checks than the run without Fire Opal.

When running complex algorithms that use advanced features like mid-circuit measurement, Fire Opal can crucially suppress noise that is correlated with algorithm complexity.

The package versions below were used to produce this notebook.

from fireopal import print_package_versions

print_package_versions()
| Package          | Version |
| ---------------- | ------- |
| Python           | 3.11.3  |
| matplotlib       | 3.7.1   |
| networkx         | 2.8.8   |
| numpy            | 1.23.5  |
| qiskit-terra     | 0.24.1  |
| sympy            | 1.12    |
| fire-opal        | 5.3.1   |
| qctrl-visualizer | 6.0.0   |

Was this useful?

cta background

New to Fire Opal?

Get access to everything you need to automate and optimize quantum hardware performance at scale.