NoiseHandler
Running programs on NISQ devices often leads to partially useful results due to the presence of noise. In order to perform realistic simulations, a number of noise models (for digital operations, analog operations and simulated readout errors) are supported in Qadence through their implementation in backends and corresponding error mitigation techniques whenever possible.
Noise models can be defined via the NoiseHandler. It is a container of several noise instances which require to specify a protocols and
a dictionary of options (or lists). The protocol field is to be instantiated from NoiseProtocol.
from qadence import NoiseHandlerfrom qadence.types import NoiseProtocol
analog_noise = NoiseHandler(protocol=NoiseProtocol.ANALOG.DEPOLARIZING, options={"noise_probs": 0.1})digital_noise = NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": 0.1})readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, options={"error_probability": 0.1, "seed": 0})One can also define a NoiseHandler passing a list of protocols and a list of options (careful with the order):
from qadence import NoiseHandlerfrom qadence.types import NoiseProtocol
protocols = [NoiseProtocol.DIGITAL.DEPOLARIZING, NoiseProtocol.READOUT]options = [{"error_probability": 0.1}, {"error_probability": 0.1, "seed": 0}]
noise_combination = NoiseHandler(protocols, options)print(noise_combination)Noise(Depolarizing, {'error_probability': 0.1})Noise(<enum 'ReadoutNoise'>, {'error_probability': 0.1, 'seed': 0})One can also append to a NoiseHandler other NoiseHandler instances:
from qadence import NoiseHandlerfrom qadence.types import NoiseProtocol
depo_noise = NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": 0.1})readout_noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, options={"error_probability": 0.1, "seed": 0})
noise_combination = NoiseHandler(protocol=NoiseProtocol.DIGITAL.BITFLIP, options={"error_probability": 0.1})noise_combination.append([depo_noise, readout_noise])print(noise_combination)Noise(BitFlip, {'error_probability': 0.1})Noise(Depolarizing, {'error_probability': 0.1})Noise(Independent Readout, {'error_probability': 0.1, 'seed': 0})Finally, one can add directly a few pre-defined types using several NoiseHandler methods:
from qadence import NoiseHandlerfrom qadence.types import NoiseProtocolnoise_combination = NoiseHandler(protocol=NoiseProtocol.DIGITAL.BITFLIP, options={"error_probability": 0.1})noise_combination.digital_depolarizing({"error_probability": 0.1}).readout_independent({"error_probability": 0.1, "seed": 0})print(noise_combination)Noise(BitFlip, {'error_probability': 0.1})Noise(Depolarizing, {'error_probability': 0.1})Noise(Independent Readout, {'error_probability': 0.1, 'seed': 0})Readout errors
Section titled “Readout errors”State Preparation and Measurement (SPAM) in the hardware is a major source of noise in the execution of quantum programs. They are typically described using confusion matrices of the form:
Two types of readout protocols are available:
NoiseProtocol.READOUT.INDEPENDENTwhere each bit can be corrupted independently of each other.NoiseProtocol.READOUT.CORRELATEDwhere we can define of confusion matrix of corruption between each possible bitstrings.
Qadence offers to simulate readout errors with the NoiseHandler to corrupt the output
samples of a simulation, through execution via a QuantumModel:
from qadence import QuantumModel, QuantumCircuit, kron, H, Zfrom qadence import hamiltonian_factory
# Simple circuit and observable construction.block = kron(H(0), Z(1))circuit = QuantumCircuit(2, block)observable = hamiltonian_factory(circuit.n_qubits, detuning=Z)
# Construct a quantum model.model = QuantumModel(circuit=circuit, observable=observable)
# Define a noise model to use.noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT)
# Run noiseless and noisy simulations.noiseless_samples = model.sample(n_shots=100)noisy_samples = model.sample(noise=noise, n_shots=100)noiseless = [OrderedCounter({'10': 53, '00': 47})]noisy = [OrderedCounter({'10': 56, '00': 39, '11': 3, '01': 2})]It is possible to pass options to the noise model. In the previous example, a noise matrix is implicitly computed from a uniform distribution.
For NoiseProtocol.READOUT.INDEPENDENT, the option dictionary argument accepts the following options:
seed: defaulted toNone, for reproducibility purposeserror_probability: If float, the same probability is applied to every bit. By default, this is 0.1. If a 1D tensor with the number of elements equal to the number of qubits, a different probability can be set for each qubit. If a tensor of shape (n_qubits, 2, 2) is passed, that is a confusion matrix obtained from experiments, we extract the error_probability. and do not compute internally the confusion matrix as in the other cases.noise_distribution: defaulted toWhiteNoise.UNIFORM, for non-uniform noise distributions
For NoiseProtocol.READOUT.CORRELATED, the option dictionary argument accepts the following options:
confusion_matrix: The square matrix representing \(T(x|x')\) for each possible bitstring of lengthnqubits. Should be of size (\(2^n, 2^n\)).seed: defaulted toNone, for reproducibility purposes
Noisy simulations go hand-in-hand with measurement protocols discussed in the measurements section, to assess the impact of noise on expectation values. In this case, both measurement and noise protocols have to be defined appropriately. Please note that a noise protocol without a measurement protocol will be ignored for expectation values computations.
from qadence.measurements import Measurements
# Define a noise model with options.options = {"error_probability": 0.01}noise = NoiseHandler(protocol=NoiseProtocol.READOUT.INDEPENDENT, options=options)
# Define a tomographical measurement protocol with options.options = {"n_shots": 10000}measurement = Measurements(protocol=Measurements.TOMOGRAPHY, options=options)
# Run noiseless and noisy simulations.noiseless_exp = model.expectation(measurement=measurement)noisy_exp = model.expectation(measurement=measurement, noise=noise)noiseless = tensor([[1.0024]], grad_fn=<TransposeBackward0>)noisy = tensor([[0.9872]], grad_fn=<TransposeBackward0>)Analog noisy simulation
Section titled “Analog noisy simulation”At the moment, analog noisy simulations are only compatible with the Pulser backend.
from qadence import DiffMode, NoiseHandler, QuantumModelfrom qadence.blocks import chain, kronfrom qadence.circuit import QuantumCircuitfrom qadence.operations import AnalogRX, AnalogRZ, Zfrom qadence.types import PI, BackendName, NoiseProtocol
analog_block = chain(AnalogRX(PI / 2.0), AnalogRZ(PI))observable = Z(0) + Z(1)circuit = QuantumCircuit(2, analog_block)
options = {"noise_probs": 0.1}noise = NoiseHandler(protocol=NoiseProtocol.ANALOG.DEPOLARIZING, options=options)model_noisy = QuantumModel( circuit=circuit, observable=observable, backend=BackendName.PULSER, diff_mode=DiffMode.GPSR, noise=noise,)noisy_expectation = model_noisy.expectation()noisy = tensor([[0.3597]])Digital noisy simulation
Section titled “Digital noisy simulation”When dealing with programs involving only digital operations, several options are made available from PyQTorch (external) via the NoiseProtocol.DIGITAL. One can define noisy digital operations as follows:
from qadence import NoiseProtocol, RX, runimport torch
noise = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, {"error_probability": 0.2})op = RX(0, torch.pi, noise = noise)
print(run(op))DensityMatrix([[[0.2000+0.0000e+00j, 0.0000+3.6739e-17j], [0.0000-3.6739e-17j, 0.8000+0.0000e+00j]]])It is also possible to set a noise configuration to all gates within a block or circuit as follows:
from qadence import set_noise, chain
n_qubits = 2
block = chain(RX(i, f"theta_{i}") for i in range(n_qubits))
noise = NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, {"error_probability": 0.1})
# The function changes the block in place:set_noise(block, noise)print(run(block))DensityMatrix([[[ 0.6571+0.0000j, 0.0000+0.0096j, 0.0000+0.2943j, -0.0043+0.0000j], [ 0.0000-0.0096j, 0.0732+0.0000j, 0.0043+0.0000j, 0.0000+0.0328j], [ 0.0000-0.2943j, 0.0043+0.0000j, 0.2427+0.0000j, 0.0000+0.0035j], [-0.0043+0.0000j, 0.0000-0.0328j, 0.0000-0.0035j, 0.0270+0.0000j]]])There is an extra optional argument to specify the type of block we want to apply a noise configuration to. E.g., let's say we want to apply noise only to X gates, a target_class argument can be passed with the corresponding block:
from qadence import Xblock = chain(RX(0, "theta"), X(0))set_noise(block, noise, target_class = X)
for block in block.blocks: print(block.noise)NoneNoise(BitFlip, {'error_probability': 0.1})