Noise mitigation

Near-term quantum devices are inherently noisy: qubits can decohere and manipulation of qubits is imperfect. To combat the effects of noise a wide range of approaches are being developed, which include scalable error correction methods and various quantum error mitigation techniques. Many of these are general schemes and they are not specific to chemistry simulations, however there are also methods that are specifically designed for quantum chemistry algorithms (such as taking advantage of molecular symmetries) to calculate a more accurate final result.

General error mitigation methods can be applied to chemistry calculations in InQuanto by importing from the Qermit package [36]. This is a flexible open-source quantum error mitigation package developed by Quantinuum which can modify circuits and perform post-processing to improve results. Many schemes are available in Qermit, and they can be applied to InQuanto calculations through use of run_mitres() or run_mitex().

Alongside Qermit support, InQuanto offers additional error mitigation schemes. In particular we highlight Partition Measurement Symmetry Verification (PMSV), which uses symmetries of the Hamiltonian to validate measurements. Another approach involves mitigating state preparation and measurement (SPAM) noise, which enhances the precision of the energy derived from quantum hardware in comparison to the precise value obtained through state-vector simulations on a classical computer. These InQuanto error mitgation methods are contained in the protocols module, and are applied at the point of build(). These classes can seamlessly integrate with Qermit workflows.

Below, we showcase the application of both Qermit and InQuanto error mitigation techniques to InQuanto’s PauliAveraging protocol. To achieve this, we construct a simple 2-qubit ansatz and operator, and import the necessary dependencies:

from pytket import Circuit
from sympy import Symbol, pi

from inquanto.ansatzes import CircuitAnsatz
from inquanto.operators import QubitOperator

circ = Circuit(2)
circ.Ry(-2 * Symbol("a") / pi, 0)
circ.CX(0, 1)
circ.Rz(-2 * Symbol("b") / pi, 1)
circ.Rx(-2 * Symbol("c") / pi, 1)
circ.CX(1, 0)
circ.Ry(-2 * Symbol("d") / pi, 0)

ansatz = CircuitAnsatz(circ)
kernel = QubitOperator("X0 X1", 2) + QubitOperator("Y0 Y1", 2) + QubitOperator("Z0 Z1", 2)
parameters = ansatz.state_symbols.construct_from_array([0.1, 0.2, 0.3, 0.4])

We will calculate the expectation value of the operator kernel with the ansatz using PauliAveraging, and qiskit’s AerBackend with a basic noise model. The InQuanto express module offers a get_noisy_backend() utility function to quickly set up a simple noisy backend for demonstration purposes:

from inquanto.express import get_noisy_backend
from inquanto.protocols import PauliAveraging

noisy_backend = get_noisy_backend(n_qubits=2)
protocol = PauliAveraging(noisy_backend, shots_per_circuit=10000)

We are now ready to build the circuits and run them, applying the mitigation schemes.

Using Qermit

Qermit provides detailed API documentation and offers two types of error mitigation workflows: MitRes (mitigation of results) and MitEx (mitigation of expectation values). The PauliAveraging protocol supports both of these workflows. To begin, we will build the protocol without any InQuanto noise mitigation.

protocol.build(parameters, ansatz, kernel);

Typically, we can call the run() method of the protocol at this point. However, if we wish to use a MitRes or MitEx object, we need to call run_mitres() or run_mitex() respectively. For example, to use Qermit’s State preparation and measurement scheme (SPAM), follow these steps:

from qermit.spam import gen_UnCorrelated_SPAM_MitRes

uc_spam_mitres = gen_UnCorrelated_SPAM_MitRes(
    backend=noisy_backend, calibration_shots=50
)
protocol.run_mitres(uc_spam_mitres, {})

energy_value = protocol.evaluate_expectation_value(ansatz, kernel)
print("MitRes (SPAM): ", energy_value)
MitRes (SPAM):  1.4589720664284618

Alternatively, we could use Qermit’s zero-noise extrapolation (ZNE) method as such:

from qermit.zero_noise_extrapolation import gen_ZNE_MitEx

zne_mitex = gen_ZNE_MitEx(
    backend=noisy_backend, noise_scaling_list=[3]
)
protocol.run_mitex(zne_mitex, {})

energy_value = protocol.evaluate_expectation_value(ansatz, kernel)
print("MitEx (ZNE3): ", energy_value)
print("Exact: ", 1.5196420749021882)
MitEx (ZNE3):  1.38000000000000
Exact:  1.5196420749021882

In both cases, after running, we can call evaluate_expectation_value() to evaluate the final result.

Note that running a Qermit graph will often perform a significant amount of circuit preparation, evaluation, and post-processing, all of which must be completed synchronously.

Using InQuanto’s PMSV and SPAM

An alternative to Qermit is using InQuanto’s noise mitigation classes. These can be particularly useful for those who want to perform asynchronous jobs (i.e. launch/retrieve logic), enabling the separation of pre-measurement from post-measurement workflows.

A simple SPAM error mitigation can be implemented as follows:

from inquanto.protocols import SPAM

protocol = PauliAveraging(noisy_backend, shots_per_circuit=10000)

spam = SPAM(backend=noisy_backend).calibrate(
    calibration_shots=50, seed=0
)

protocol.build(parameters, ansatz, kernel).run(seed=0)
energy_value = protocol.evaluate_expectation_value(ansatz, kernel)
print("Raw: ", energy_value)

protocol.clear()
protocol.build(
    parameters, ansatz, kernel, noise_mitigation=spam
).run(seed=0)
energy_value = protocol.evaluate_expectation_value(ansatz, kernel)
print("NoiseMitigation (SPAM): ", energy_value)
Raw:  1.4407999999999999
NoiseMitigation (SPAM):  1.5000999800039994

In the next example, we use PMSV for a chemistry system (H2). We begin by preparing the system:

from inquanto.express import get_system
from inquanto.ansatzes import FermionSpaceStateExpChemicallyAware

hamiltonian, space, state = get_system("h2_sto3g_symmetry.h5")
kernel = hamiltonian.qubit_encode()
exponents = space.construct_single_ucc_operators(state)
exponents += space.construct_double_ucc_operators(state)

ansatz = FermionSpaceStateExpChemicallyAware(exponents, state)
p = ansatz.state_symbols.construct_random(seed=1)

# We need a 4 qubit noisy backend
noisy_backend = get_noisy_backend(4)

PMSV prepares a set of expected Pauli string results and adds measurement of those Pauli strings to the circuit. If a shot does not have the expected symmetry for the set of stabilizers, it is discarded. Further details are given in the Appendix of the paper by Yamamoto et. al. [37].

from inquanto.protocols import PMSV

stabilizers = [
    -1 * QubitOperator("Z0 Z2"),
    -1 * QubitOperator("Z1 Z3"),
    +1 * QubitOperator("Z2 Z3"),
]

protocol = PauliAveraging(noisy_backend, shots_per_circuit=10000)
pmsv = PMSV(stabilizers)
protocol.build(p, ansatz, kernel, noise_mitigation = pmsv).run(seed=0)
energy_value = protocol.evaluate_expectation_value(ansatz, kernel)
print("PMSV: ", energy_value)
PMSV:  0.4928363367103187

Lastly, the shot table for the Pauli strings can be examined.

print(protocol.dataframe_measurements())
   pauli_string      mean    stderr           umean  sample_size
0            Z0  0.838858  0.005620   0.839+/-0.006         9383
1            Z3 -0.838858  0.005620  -0.839+/-0.006         9383
2            Z2 -0.838858  0.005620  -0.839+/-0.006         9383
3   Y0 Y1 X2 X3 -0.528168  0.008730  -0.528+/-0.009         9461
4   X0 Y1 Y2 X3  0.507124  0.008855   0.507+/-0.009         9475
5         Z1 Z3 -1.000000  0.000000        -1.0+/-0        47232
6   X0 X1 Y2 Y3 -0.526716  0.008736  -0.527+/-0.009         9470
7         Z2 Z3  1.000000  0.000000         1.0+/-0        47232
8         Z0 Z2 -1.000000  0.000000        -1.0+/-0        47232
9         Z1 Z2 -1.000000  0.000000        -1.0+/-0         9383
10           Z1  0.838858  0.005620   0.839+/-0.006         9383
11        Z0 Z1  1.000000  0.000000         1.0+/-0         9383
12        Z0 Z3 -1.000000  0.000000        -1.0+/-0         9383
13  Y0 X1 X2 Y3  0.520491  0.008787   0.520+/-0.009         9443