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.4469767423257673
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.49740000000000
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