Error mitigation with Cirq on IBMQ backends#

This tutorial shows an example of how to mitigate noise on IBMQ backends with the Cirq frontend. It isn’t necessary to use Qiskit frontends (circuits) to run on IBM backends. We can use conversions in Mitiq to use supported frontends with supported backends. Below, we show how to run a Cirq circuit on an IBMQ backend.

Settings#

from mitiq import zne
from mitiq.interface.mitiq_qiskit.qiskit_utils import initialized_depolarizing_noise


USE_REAL_HARDWARE = False

Note: When USE_REAL_HARDWARE is set to False, a classically simulated noisy backend is used instead of a real quantum computer.

Setup: Defining a circuit in Cirq#

For simplicity, we’ll use a random single-qubit circuit with ten gates that compiles to the identity, defined below.

import cirq

qbit = cirq.LineQubit(0)
cirq_circuit = cirq.Circuit([cirq.X(qbit)] * 10, cirq.measure(qbit))
print(cirq_circuit)
0: ───X───X───X───X───X───X───X───X───X───X───M───

We will use the probability of the ground state as our observable to mitigate, the expectation value of which should evaluate to one in the noiseless setting.

High-level usage#

To use Mitiq with just a few lines of code, we need to define an executor, a function which inputs a circuit and outputs the expectation value to mitigate. This function will:

  1. [Optionally] Add measurement(s) to the circuit.

  2. Run the circuit.

  3. Convert from raw measurement statistics (or a different output format) to an expectation value.

For information on how to define more advanced executors, see the Executors section of the Mitiq user guide.

We define the executor function in the following code block. Because we are using IBMQ backends, we first load our account.

Note: Using an IBM quantum computer requires a valid IBMQ account. See https://quantum-computing.ibm.com/ for instructions to create an account, save credentials, and see online quantum computers.

import qiskit
from qiskit_aer import Aer
from qiskit_ibm_provider import IBMProvider
from mitiq.interface.mitiq_qiskit.conversions import to_qiskit

if IBMProvider.saved_accounts() and USE_REAL_HARDWARE:
    provider = IBMProvider()
    backend = provider.get_backend("ibmq_qasm_simulator")  # Set quantum computer here!
else:
    # Default to a simulator.
    backend = Aer.get_backend("qasm_simulator"),


def cirq_ibm_executor(cirq_circuit: cirq.Circuit, shots: int = 1024) -> float:
    """Returns the expectation value to be mitigated.

    Args:
        circuit: Circuit to run.
        shots: Number of times to execute the circuit to compute the expectation value.
    """
    # convert from Cirq circuit to Qiskit circuit.
    circuit = to_qiskit(cirq_circuit) 
    
    if USE_REAL_HARDWARE:
        # Run the circuit on hardware
        job = qiskit.execute(
            experiments=circuit,
            backend=backend,
            optimization_level=0,  # Important to preserve folded gates.
            shots=shots
        )
    else:
        # Simulate the circuit with noise
        noise_model = initialized_depolarizing_noise(noise_level=0.02)
        job = qiskit.execute(
            experiments=circuit,
            backend=qiskit.Aer.get_backend("qasm_simulator"),
            noise_model=noise_model,
            basis_gates=noise_model.basis_gates,
            optimization_level=0,  # Important to preserve folded gates.
            shots=shots,
        )

    # Convert from raw measurement counts to the expectation value
    counts = job.result().get_counts()
    if counts.get("0") is None:
        expectation_value = 0.
    else:
        expectation_value = counts.get("0") / shots
    return expectation_value

At this point, the circuit can be executed to return a mitigated expectation value by running Mitiq’s zne.execute_with_zne() function as follows.

unmitigated = cirq_ibm_executor(cirq_circuit)
mitigated = zne.execute_with_zne(cirq_circuit, cirq_ibm_executor)
print(f"Unmitigated result {unmitigated:.3f}")
print(f"Mitigated result {mitigated:.3f}")
Unmitigated result 0.916
Mitigated result 0.943

As long as a circuit and a function for executing the circuit are defined, the zne.execute_with_zne() function can be called as above to return zero-noise extrapolated expectation value(s).

Options#

Different options for noise scaling and extrapolation can be passed into the zne.execute_with_zne() function. By default, noise is scaled by locally folding gates at random, and the default extrapolation is Richardson.

To specify a different extrapolation technique, we can pass a different Factory object to execute_with_zne(). The following code block shows an example of using linear extrapolation with five different (noise) scale factors.

linear_factory = zne.inference.LinearFactory(scale_factors=[1.0, 1.5, 2.0, 2.5, 3.0])
mitigated = zne.execute_with_zne(cirq_circuit, cirq_ibm_executor, factory=linear_factory)
print(f"Mitigated result {mitigated:.3f}")
Mitigated result 0.968

To specify a different noise scaling method, we can pass a different function for the argument scale_noise. This function should input a circuit and scale factor and return a circuit. The following code block shows an example of scaling noise by global folding (instead of local folding, the default behavior for zne.execute_with_zne()).

mitigated = zne.execute_with_zne(cirq_circuit, cirq_ibm_executor, scale_noise=zne.scaling.fold_global)
print(f"Mitigated result {mitigated:.3f}")
Mitigated result 1.001

Any different combination of noise scaling and extrapolation technique can be passed as arguments to zne.execute_with_zne().

Lower-level usage#

Here, we give more detailed usage of the Mitiq library which mimics what happens in the call to zne.execute_with_zne() in the previous example. In addition to showing more of the Mitiq library, this example explains the code in the previous section in more detail.

First, we define factors to scale the circuit length by and fold the circuit using the fold_gates_at_random local folding method.

scale_factors = [1., 1.5, 2., 2.5, 3.]
folded_circuits = [
        zne.scaling.fold_gates_at_random(to_qiskit(cirq_circuit), scale)
        for scale in scale_factors
]

# Check that the circuit depth is (approximately) scaled as expected
for j, c in enumerate(folded_circuits):
    print(f"Number of gates of folded circuit {j} scaled by: {len(c) / len(cirq_circuit):.3f}")
Number of gates of folded circuit 0 scaled by: 1.000
Number of gates of folded circuit 1 scaled by: 1.364
Number of gates of folded circuit 2 scaled by: 1.909
Number of gates of folded circuit 3 scaled by: 2.273
Number of gates of folded circuit 4 scaled by: 2.818

For a noiseless simulation, the expectation of this observable should be 1.0 because our circuit compiles to the identity. For a noisy simulation, the value will be smaller than one. Because folding introduces more gates and thus more noise, the expectation value will decrease as the length (scale factor) of the folded circuits increase. By fitting this to a curve, we can extrapolate to the zero-noise limit and obtain a better estimate.

Below we execute the folded circuits using the backend defined at the start of this example.

shots = 8192

if USE_REAL_HARDWARE:
    # Run the circuit on hardware
    job = qiskit.execute(
        experiments=folded_circuits,
        backend=backend,
        optimization_level=0,  # Important to preserve folded gates.
        shots=shots
    )
else:
    # Simulate the circuit with noise
    noise_model = initialized_depolarizing_noise(noise_level=0.05)
    job = qiskit.execute(
        experiments=folded_circuits,
        backend=qiskit.Aer.get_backend("qasm_simulator"),
        noise_model=noise_model,
        basis_gates=noise_model.basis_gates,
        optimization_level=0,  # Important to preserve folded gates.
        shots=shots,
    )

Note: We set the optimization_level=0 to prevent any compilation by Qiskit transpilers.

Once the job has finished executing, we can convert the raw measurement statistics to observable values by running the following code block.

all_counts = [job.result().get_counts(i) for i in range(len(folded_circuits))]
expectation_values = [counts.get("0") / shots for counts in all_counts]
print(f"Expectation values:\n{expectation_values}")
Expectation values:
[0.796142578125, 0.740478515625, 0.6898193359375, 0.6475830078125, 0.6048583984375]

We can now see the unmitigated observable value by printing the first element of expectation_values. (This value corresponds to a circuit with scale factor one, i.e., the original circuit.)

print("Unmitigated expectation value:", round(expectation_values[0], 3))
Unmitigated expectation value: 0.796

Now we can use the static extrapolate method of zne.inference.Factory objects to extrapolate to the zero-noise limit. Below we use an exponential fit and print out the extrapolated zero-noise value.

zero_noise_value = zne.ExpFactory.extrapolate(scale_factors, expectation_values, asymptote=0.5)
print(f"Extrapolated zero-noise value:", round(zero_noise_value, 3))
Extrapolated zero-noise value: 0.999