Getting Started

This guide shows short examples of two error mitigation techniques in Mitiq: zero-noise extrapolation and probabilistic error cancellation. First, we highlight the general workflow of both methods.

General workflow: Front-ends, backends, and executors

We refer to a library/language that constructs quantum programs as a “front-end,” and a quantum computer or quantum computer simulator as a “backend.” Mitiq currently supports Cirq, Qiskit, PyQuil and Braket front-ends, and is backend agnostic - as long as you can run a supported quantum program on a backend, you can use that backend with Mitiq.

We refer to a user-defined function that inputs a quantum program and executes it on a backend as an executor. Such functions always accept a quantum program, sometimes accept other arguments, and always return an expectation value as a float. As an example, below we define a simple executor function which inputs a Cirq circuit, executes it on a noisy simulator, and returns the probability of the ground state.

import numpy as np
import cirq

def executor(circ: cirq.Circuit) -> float:
    """Simulates a circuit with depolarizing noise.

    Args:
        circ: The quantum program as a Cirq object.

    Returns:
        The expectation value of the ground state projector.
    """
    circuit = circ.with_noise(cirq.depolarize(p=0.01))
    result = cirq.DensityMatrixSimulator().simulate(circuit)
    return np.real(result.final_density_matrix[0, 0])

Now we consider a simple example: a single-qubit circuit with an even number of Pauli-X gates. By construction, the ideal expectation value should be 1, but the noisy expectation value will be slightly different.

circuit = cirq.Circuit(
    cirq.X(cirq.LineQubit(0)) for _ in range(6)
)

exact_result = 1.0
noisy_result = executor(circuit)

print(f"Error in noisy simulation: {abs(exact_result - noisy_result):.{3}}")
Error in noisy simulation: 0.0387

This shows the impact of noise on the final expectation value (without error mitigation). Now let’s use Mitiq to improve this performance.

Zero-Noise Extrapolation

Zero-noise extrapolation can be easily implemented with the function mitiq.zne.zne.execute_with_zne().

from mitiq import zne

mitigated_result = zne.execute_with_zne(circuit, executor)

print(f"Error without mitigation: {abs(exact_result - noisy_result):.{3}}")
print(f"Error with mitigation (ZNE): {abs(exact_result - mitigated_result):.{3}}")
Error without mitigation: 0.0387
Error with mitigation (ZNE): 0.000232

You can also use Mitiq to wrap your backend execution function into an error-mitigated version.

from mitiq.zne import mitigate_executor

mitigated_executor = mitigate_executor(executor)
mitigated_result = mitigated_executor(circuit)
print(round(mitigated_result, 5))
0.99977

Note

As shown here, Mitiq wraps executor functions that have a specific type: they take quantum programs as input and return expectation values. However, one often has an execution function with other arguments such as the number of shots, the observable to measure, or the noise level of a noisy simulation. It is still easy to use these with Mitiq by using partial function application. Here’s a pseudo-code example:

from functools import partial

def shot_executor(qprogram, n_shots) -> float:
    ...
# we partially apply the n_shots argument to get a function that just
# takes a quantum program
mitigated = execute_with_zne(circ, partial(shot_executor, n_shots=100))

You can read more about functools partial application here.

By default, mitiq.zne.zne.execute_with_zne() uses Richardson extrapolation to extrapolate the expectation value to the zero noise limit [1]. Mitiq comes equipped with other extrapolation methods as well. Different methods of extrapolation are packaged into Factory objects. It is easy to try different ones.

fac = zne.inference.LinearFactory(scale_factors=[1.0, 2.0, 2.5])
linear_zne_result = zne.execute_with_zne(circuit, executor, factory=fac)
error = abs(exact_result - linear_zne_result)
print(f"Mitigated error with linear ZNE: {error:.{3}}")
Mitigated error with linear ZNE: 0.00769

You can use built-in methods from factories like plot_data() and plot_fit() to plot the noise scale factors v. the expectation value returned by the executor.

fac.plot_fit()
factory data from executor.

You can read more about the Factory objects that are built into Mitiq and how to create your own here.

Another key step in zero-noise extrapolation is to choose how your circuit is transformed to scale the noise. You can read more about the noise scaling methods built into Mitiq and how to create your own here.

Zero-Noise Extrapolation with Qiskit

Mitiq is designed to be agnostic to the stack that you are using. Thus for Qiskit things work in the same manner as before. Since we are now using Qiskit, we want to run the error mitigated programs on a Qiskit backend. Let’s define the new backend that accepts Qiskit circuits. In this case it is a simulator, but you could also use a QPU.

import qiskit

# For noisy simulation.
from qiskit.providers.aer.noise import NoiseModel
from qiskit.providers.aer.noise.errors.standard_errors import depolarizing_error


backend = qiskit.Aer.get_backend("qasm_simulator")

def qiskit_executor(circuit: qiskit.QuantumCircuit, shots: int = 4096) -> float:
    """Runs the quantum circuit with a depolarizing channel noise model at
    level NOISE.

    Args:
        circuit: Single-qubit quantum circuit to execute.
        shots: Number of shots to run the circuit on the back-end.

    Returns:
        The ground state probability of the single-qubit circuit.
    """
    # Use a depolarizing noise model.
    noise_model = NoiseModel()
    noise_model.add_all_qubit_quantum_error(
        depolarizing_error(0.001, 1),
        ["u1", "u2", "u3"],
    )

    # execution of the experiment
    job = qiskit.execute(
        circuit,
        backend,
        basis_gates=["u1", "u2", "u3"],
        # we want all gates to be actually applied,
        # so we skip any circuit optimization
        optimization_level=0,
        noise_model=noise_model,
        shots=shots,
        seed_transpiler=1,
        seed_simulator=1
    )
    results = job.result()
    counts = results.get_counts()
    expval = counts["0"] / shots
    return expval

We can then use this backend for our mitigation.

from qiskit import QuantumCircuit
from mitiq.zne import execute_with_zne

circ = QuantumCircuit(1, 1)
for _ in range(100):
     _ = circ.x(0)
_ = circ.measure(0, 0)

exact = 1
unmitigated = qiskit_executor(circ)
mitigated = execute_with_zne(circ, qiskit_executor)

# The mitigation should improve the result.
assert abs(exact - mitigated) < abs(exact - unmitigated)

Note that Factory’s are only used for fitting classical data and are completely frontend/backend agnostic.

Probabilistic Error Cancellation

The workflow for probabilistic error cancellation is very similar to that of zero-noise extrapolation. In particular, we can use Probabilistic Error Cancellation (PEC) with the same execution function (executor) and the same Cirq circuit (circuit) that we have already defined in the General Workflow section.

PEC requires a good knowledge of the noise model and of the noise strength acting on the system. In particular for each operation of the circuit, we need to build a quasi-probability representation of the ideal unitary gate expanded in a basis of noisy implementable operations. For more details behind the theory of PEC see the Probabilistic Error Cancellation section.

In our simple case, circuit corresponds to the repetition of the same X gate, whose representation in the presence of depolarizing noise can be obtained as follows:

from mitiq.pec import represent_operation_with_local_depolarizing_noise

x_representation = represent_operation_with_local_depolarizing_noise(
    ideal_operation=cirq.Circuit(cirq.X(cirq.LineQubit(0))),
    noise_level=0.01,
)

print(x_representation)
0: ───X─── = 1.010*0: ───X───-0.003*0: ───X───X───-0.003*0: ───X───Y───-0.003*0: ───X───Z───

Note

Note that we use the same noise_level in the call to represent_operation_with_local_depolarizing_noise that we use for the noisy simulator (i.e., in the execute function). For this method of getting operation representations, it is important to provide a good estimate of the noise level so the representations are faithful.

The result above is an OperationRepresentation object which contains the information for representing the ideal operation X (left-hand-side of the printed output) as a linear combination of noisy operations (right-hand-side of the printed output).

We can now implement PEC by importing the function execute_with_pec() from the pec module.

from mitiq import pec

exact_result = 1
noisy_result = executor(circuit)
pec_result = pec.execute_with_pec(
    circuit,
    executor,
    representations=[x_representation],
    random_state=0,
)

print(f"Error without mitigation: {abs(exact_result - noisy_result):.{3}}")
print(f"Error with mitigation (PEC): {abs(exact_result - pec_result):.{3}}")
Error without mitigation: 0.0387
Error with mitigation (PEC): 0.00363

In addition to execute_with_pec(), you can also use Mitiq to wrap your backend execution function into an error-mitigated version like you can with zero-noise extrapolation.