Digital dynamical decoupling (DDD) with Qiskit on GHZ Circuits#
In this notebook DDD is applied to improve the success rate of the computation on a real hardware backend.
A similar approach can be taken on a simulated backend, by setting the USE_REAL_HARDWARE
option to False
and specifying a simulated backend from qiskit.providers.fake_provider
, which includes a noise model that approximates the noise of the
real device.
In DDD, sequences of gates are applied to slack windows, i.e. single-qubit idle windows, in a quantum circuit. Applying such sequences can reduce the coupling between the qubits and the environment, mitigating the effects of noise. While the DDD module includes some built-in sequences, the user may choose to define others best suited to their application. For more information on DDD, see the section DDD section of the user guide.
Setup#
We begin by importing the relevant modules and libraries that we will require for the rest of this tutorial.
from typing import List, Callable
import numpy as np
from matplotlib import pyplot as plt
import qiskit
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService
from mitiq.interface.mitiq_qiskit import to_qiskit
from mitiq import ddd, QPROGRAM
from mitiq.ddd import insert_ddd_sequences
Define DDD rules#
We now use Mitiq’s DDD rule, i. e., a function that generates DDD sequences of different length.
In this example, we test the performance of repeated I (default built into get_circuit
below) and repeated IXIX, repeated XX, and XX sequences from Mitiq.
import cirq
def rep_ixix_rule(window_length: int) -> Callable[[int], QPROGRAM]:
return ddd.rules.repeated_rule(
window_length, [cirq.I, cirq.X, cirq.I, cirq.X]
)
def rep_xx_rule(window_length: int) -> Callable[[int], QPROGRAM]:
return ddd.rules.repeated_rule(window_length, [cirq.X, cirq.X])
# Set DDD sequences to test.
rules = [rep_ixix_rule, rep_xx_rule, ddd.rules.xx]
# Test the sequence insertion
for rule in rules:
print(rule(10))
0: ───I───I───X───I───X───I───X───I───X───I───
0: ───X───X───X───X───X───X───X───X───X───X───
0: ───I───I───I───X───I───I───X───I───I───I───
Set parameters for the experiment#
# Total number of shots to use.
shots = 10000
# Qubits to use on the experiment.
num_qubits = 2
# Test at multiple depths.
depths = [10, 30, 50, 100]
Define the circuit#
We use Greenberger-Horne-Zeilinger (GHZ) circuits to benchmark the performance of the device. GHZ circuits are designed such that only two bitstrings \(|00...0 \rangle\) and \(|11...1 \rangle\) should be sampled, with \(P_0 = P_1 = 0.5\). As noted in Mooney et al. (2021) [73], when GHZ circuits are run on a device, any other measured bitstrings are due to noise. In this example the GHZ sequence is applied first, followed by a long idle window of identity gates and finally the inverse of the GHZ sequence. Therefore \(P_0 = 1\), and the frequency of the \(|0 \rangle\) bitstring is our target metric (in this example we only measure the first qubit).
def get_circuit(depth: int):
"""Returns a circuit composed of a GHZ sequence, idle windows,
and finally an inverse GHZ sequence.
Args:
depth: The depth of the idle window in the circuit.
"""
circuit = qiskit.QuantumCircuit(num_qubits, num_qubits)
circuit.h(0)
circuit.cx(0, 1)
for _ in range(depth):
circuit.id(0)
circuit.id(1)
circuit.cx(0, 1)
circuit.h(0)
circuit.measure(0, 0)
return circuit
Test the circuit output for depth 4, unmitigated
ibm_circ = get_circuit(4)
print(ibm_circ)
┌───┐ ┌───┐┌───┐┌───┐┌───┐ ┌───┐┌─┐
q_0: ┤ H ├──■──┤ I ├┤ I ├┤ I ├┤ I ├──■──┤ H ├┤M├
└───┘┌─┴─┐├───┤├───┤├───┤├───┤┌─┴─┐└───┘└╥┘
q_1: ─────┤ X ├┤ I ├┤ I ├┤ I ├┤ I ├┤ X ├──────╫─
└───┘└───┘└───┘└───┘└───┘└───┘ ║
c: 2/═════════════════════════════════════════╩═
0
Test the circuit output for depth 4, with IX sequences inserted
ixix_circ = insert_ddd_sequences(ibm_circ, rep_ixix_rule)
print(ixix_circ)
┌───┐ ┌───┐┌───┐┌───┐┌───┐ ┌───┐┌─┐
q_0: ┤ H ├──■──┤ I ├┤ X ├┤ I ├┤ X ├──■──┤ H ├┤M├
└───┘┌─┴─┐├───┤├───┤├───┤├───┤┌─┴─┐└───┘└╥┘
q_1: ─────┤ X ├┤ I ├┤ X ├┤ I ├┤ X ├┤ X ├──────╫─
└───┘└───┘└───┘└───┘└───┘└───┘ ║
c: 2/═════════════════════════════════════════╩═
0
Define the executor#
Now that we have a circuit, we define the execute
function which inputs a circuit and returns an expectation value -
here, the frequency of sampling the correct bitstring.
USE_REAL_HARDWARE = True
correct_bitstring=[0]
if USE_REAL_HARDWARE:
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
else:
from qiskit_ibm_runtime.fake_provider import FakeLimaV2 as FakeLima
backend = FakeLima()
def ibm_executor(
circuit: qiskit.QuantumCircuit,
shots: int,
correct_bitstring: List[int],
noisy: bool = True,
) -> float:
"""Executes the input circuit(s) and returns ⟨A⟩, where
A = |correct_bitstring⟩⟨correct_bitstring| for each circuit.
Args:
circuit: Circuit to run.
shots: Number of times to execute the circuit to compute the
expectation value.
correct_bitstring: Bitstring the circuit is expected to return, in the
absence of noise.
"""
if noisy:
transpiled = qiskit.transpile(circuit, backend=backend, optimization_level=0)
job = backend.run(transpiled, shots=shots)
else:
ideal_backend = AerSimulator()
job = ideal_backend.run(circuit, optimization_level=0, shots=shots)
# Convert from raw measurement counts to the expectation value
all_counts = job.result().get_counts()
prob_zero = all_counts.get("".join(map(str, correct_bitstring)), 0.0) / shots
return prob_zero
Run circuits with and without DDD#
data = []
for depth in depths:
circuit = get_circuit(depth)
noisy_value = ibm_executor(
circuit, shots=shots, correct_bitstring=correct_bitstring
)
data.append((depth, "unmitigated", noisy_value))
for rule in rules:
ddd_circuit = insert_ddd_sequences(circuit, rule)
ddd_value = ibm_executor(
ddd_circuit, shots=shots, correct_bitstring=correct_bitstring
)
data.append((depth, rule.__name__, ddd_value))
Now we can visualize the results.
# Plot unmitigated
x, y = [], []
for res in data:
if res[1] == "unmitigated":
x.append(res[0])
y.append(res[2])
plt.plot(x, y, "--*", label="Unmitigated")
# Plot xx
x, y = [], []
for res in data:
if res[1] == "rep_xx_rule":
x.append(res[0])
y.append(res[2])
plt.plot(x, y, "--*", label="rep_xx_rule")
# Plot ixix
x, y = [], []
for res in data:
if res[1] == "rep_ixix_rule":
x.append(res[0])
y.append(res[2])
plt.plot(x, y, "--*", label="rep_ixix_rule")
# Plot xx
x, y = [], []
for res in data:
if res[1] == "xx":
x.append(res[0])
y.append(res[2])
plt.plot(x, y, "--*", label="xx")
plt.legend()
We can see that DDD improves the expectation value at each circuit depth, and the repeated XX sequence is the best at mitigating the errors occurring during idle windows.