Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantum® documentation. Refer to quantum.cloud.ibm.com/docs for the official documentation.

Noise learning helper

  • The code on this page was developed using the following requirements. We recommend using these versions or newer.

    qiskit[all]~=2.4.1
    qiskit-ibm-runtime~=0.47.0
    samplomatic~=0.18.0
    

The error mitigation techniques PEA and PEC both utilize a noise learning component based on a Pauli-Lindblad noise model, which is typically managed during execution after submitting one or more jobs through qiskit-ibm-runtime without any local access to the fitted noise model. However, as of qiskit-ibm-runtime v0.27.1, a NoiseLearner and associated NoiseLearnerOptions class have been created to obtain the results of these noise learning experiments. These results can then be stored locally as a NoiseLearnerResult and used as input in later experiments. This page provides an overview of its usage and the associated options available.

In addition, starting with qiskit-ibm-runtime v0.47.0, there is a new NoiseLearnerV3 class that is compatible with the Executor primitive. This new version, also part of the directed execution model, gives you the ability to explicitly specify the layers you want to learn.

Note

NoiseLearner only works with EstimatorV2 and NoiseLearnerV3 only works with Executor.


NoiseLearner

Overview

The NoiseLearner class performs experiments that characterize noise processes based on a Pauli-Lindblad noise model for one (or more) circuits. It possesses a run() method that executes the learning experiments and takes as input either a list of circuits or a PUB, and returns a NoiseLearnerResult containing the learned noise channels and metadata about the job(s) submitted. Below is a code snippet demonstrating the usage of the helper program.

from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap
from qiskit.transpiler import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService, EstimatorV2
from qiskit_ibm_runtime.noise_learner import NoiseLearner
from qiskit_ibm_runtime.options import (
    NoiseLearnerOptions,
    ResilienceOptionsV2,
    EstimatorOptions,
)

# Build a circuit with two entangling layers
num_qubits = 27
edges = list(CouplingMap.from_line(num_qubits, bidirectional=False))
even_edges = edges[::2]
odd_edges = edges[1::2]

circuit = QuantumCircuit(num_qubits)
for pair in even_edges:
    circuit.cx(pair[0], pair[1])
for pair in odd_edges:
    circuit.cx(pair[0], pair[1])

# Choose a backend to run on
service = QiskitRuntimeService()
backend = service.least_busy()

# Transpile the circuit for execution
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
circuit_to_learn = pm.run(circuit)

# Instantiate a NoiseLearner object and execute the noise learning program
learner = NoiseLearner(mode=backend)
job = learner.run([circuit_to_learn])
noise_model = job.result()

The resulting NoiseLearnerResult.data is a list of LayerError objects containing the noise model for each individual entangling layer that belongs to the target circuit(s). Each LayerError stores the layer information, in the form of a circuit and a set of qubit labels, alongside the PauliLindbladError for the noise model that was learned for the given layer.

import numpy

print(
    f"Noise learner result contains {len(noise_model.data)} entries"
    f" and has the following type:\n {type(noise_model)}\n"
)
print(
    f"Each element of `NoiseLearnerResult` then contains"
    f" an object of type:\n {type(noise_model.data[0])}\n"
)
# Results are truncated
with numpy.printoptions(threshold=200):
    print(
        f"And each of these `LayerError` objects possess"
        f" data on the generators for the error channel: \n{noise_model.data[0].error.generators}\n"
    )
# Results are truncated
with numpy.printoptions(threshold=200):
    print(
        f"Along with the error rates: \n{noise_model.data[0].error.rates}\n"
    )

Output:

Noise learner result contains 2 entries and has the following type:
 <class 'qiskit_ibm_runtime.utils.noise_learner_result.NoiseLearnerResult'>

Each element of `NoiseLearnerResult` then contains an object of type:
 <class 'qiskit_ibm_runtime.utils.noise_learner_result.LayerError'>

And each of these `LayerError` objects possess data on the generators for the error channel: 
['IIIIIIIIIIIIIIIIIIIIIIIIIIX', 'IIIIIIIIIIIIIIIIIIIIIIIIIIY',
 'IIIIIIIIIIIIIIIIIIIIIIIIIIZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIXI',
 'IIIIIIIIIIIIIIIIIIIIIIIIIXX', 'IIIIIIIIIIIIIIIIIIIIIIIIIXY',
 'IIIIIIIIIIIIIIIIIIIIIIIIIXZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIYI',
 'IIIIIIIIIIIIIIIIIIIIIIIIIYX', 'IIIIIIIIIIIIIIIIIIIIIIIIIYY',
 'IIIIIIIIIIIIIIIIIIIIIIIIIYZ', 'IIIIIIIIIIIIIIIIIIIIIIIIIZI',
 'IIIIIIIIIIIIIIIIIIIIIIIIIZX', 'IIIIIIIIIIIIIIIIIIIIIIIIIZY',
 'IIIIIIIIIIIIIIIIIIIIIIIIIZZ', 'IIIIIIIIIIIIIIIIIIIIIIIIXII',
 'IIIIIIIIIIIIIIIIIIIIIIIIXIX', 'IIIIIIIIIIIIIIIIIIIIIIIIXIY',
 'IIIIIIIIIIIIIIIIIIIIIIIIXIZ', 'IIIIIIIIIIIIIIIIIIIIIIIIYII',
 'IIIIIIIIIIIIIIIIIIIIIIIIYIX', 'IIIIIIIIIIIIIIIIIIIIIIIIYIY',
 'IIIIIIIIIIIIIIIIIIIIIIIIYIZ', 'IIIIIIIIIIIIIIIIIIIIIIIIZII',
 'IIIIIIIIIIIIIIIIIIIIIIIIZIX', 'IIIIIIIIIIIIIIIIIIIIIIIIZIY',
 'IIIIIIIIIIIIIIIIIIIIIIIIZIZ', 'IIIIIIIIIIIIIIIIIIIIIIIXIII',
 'IIIIIIIIIIIIIIIIIIIIIIIYIII', 'IIIIIIIIIIIIIIIIIIIIIIIZIII',
 'IIIIIIIIIIIIIIIIIIIIIIXIIII', 'IIIIIIIIIIIIIIIIIIIIIIXXIII',
 'IIIIIIIIIIIIIIIIIIIIIIXYIII', 'IIIIIIIIIIIIIIIIIIIIIIXZIII',
 'IIIIIIIIIIIIIIIIIIIIIIYIIII', 'IIIIIIIIIIIIIIIIIIIIIIYXIII',
 'IIIIIIIIIIIIIIIIIIIIIIYYIII', 'IIIIIIIIIIIIIIIIIIIIIIYZIII',
 'IIIIIIIIIIIIIIIIIIIIIIZIIII', 'IIIIIIIIIIIIIIIIIIIIIIZXIII',
 'IIIIIIIIIIIIIIIIIIIIIIZYIII', 'IIIIIIIIIIIIIIIIIIIIIIZZIII',
 'IIIIIIIIIIIIIIIIIIIIIXIIIII', 'IIIIIIIIIIIIIIIIIIIIIXXIIII',
 'IIIIIIIIIIIIIIIIIIIIIXYIIII', 'IIIIIIIIIIIIIIIIIIIIIXZIIII',
 'IIIIIIIIIIIIIIIIIIIIIYIIIII', 'IIIIIIIIIIIIIIIIIIIIIYXIIII',
 'IIIIIIIIIIIIIIIIIIIIIYYIIII', 'IIIIIIIIIIIIIIIIIIIIIYZIIII',
 'IIIIIIIIIIIIIIIIIIIIIZIIIII', 'IIIIIIIIIIIIIIIIIIIIIZXIIII',
 'IIIIIIIIIIIIIIIIIIIIIZYIIII', 'IIIIIIIIIIIIIIIIIIIIIZZIIII',
 'IIIIIIIIIIIIIIIIIIIIXIIIIII', 'IIIIIIIIIIIIIIIIIIIIXXIIIII',
 'IIIIIIIIIIIIIIIIIIIIXYIIIII', 'IIIIIIIIIIIIIIIIIIIIXZIIIII',
 'IIIIIIIIIIIIIIIIIIIIYIIIIII', 'IIIIIIIIIIIIIIIIIIIIYXIIIII',
 'IIIIIIIIIIIIIIIIIIIIYYIIIII', 'IIIIIIIIIIIIIIIIIIIIYZIIIII',
 'IIIIIIIIIIIIIIIIIIIIZIIIIII', 'IIIIIIIIIIIIIIIIIIIIZXIIIII',
 'IIIIIIIIIIIIIIIIIIIIZYIIIII', 'IIIIIIIIIIIIIIIIIIIIZZIIIII',
 'IIIIIIIIIIIIIIIIIIIXIIIIIII', 'IIIIIIIIIIIIIIIIIIIXXIIIIII',
 'IIIIIIIIIIIIIIIIIIIXYIIIIII', 'IIIIIIIIIIIIIIIIIIIXZIIIIII',
 'IIIIIIIIIIIIIIIIIIIYIIIIIII', 'IIIIIIIIIIIIIIIIIIIYXIIIIII',
 'IIIIIIIIIIIIIIIIIIIYYIIIIII', 'IIIIIIIIIIIIIIIIIIIYZIIIIII', ...]

Along with the error rates: 
[5.9e-04 5.3e-04 5.7e-04 ... 0.0e+00 1.0e-05 0.0e+00]

The LayerError.error attribute of the noise learning result contains the generators and error rates of the fitted Pauli Lindblad model, which has the form

Λ(ρ)=expjrj(PjρPjρ),\Lambda(\rho) = \exp{\sum_j r_j \left(P_j \rho P_j^\dagger - \rho\right)},

where the rjr_j are the LayerError.rates and PjP_j are the Pauli operators specified in LayerError.generators.

Noise learning options

You can choose among several options to input when you instantiate a NoiseLearner object. These options are encapsulated by the qiskit_ibm_runtime.options.NoiseLearnerOptions class and include the ability to specify the maximum layers to learn, number of randomizations, and the twirling strategy, among others. Refer to the NoiseLearnerOptions API documentation for detailed information.

Following is a simple example that shows how to use the NoiseLearnerOptions in a NoiseLearner experiment:

# Build a GHZ circuit
circuit = QuantumCircuit(10)
circuit.h(0)
circuit.cx(range(0, 9), range(1, 10))
# Choose a backend to run on
service = QiskitRuntimeService()
backend = service.least_busy()

# Transpile the circuit for execution
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
circuit_to_run = pm.run(circuit_to_learn)

# Instantiate a NoiseLearnerOptions object
learner_options = NoiseLearnerOptions(
    max_layers_to_learn=3, num_randomizations=32, twirling_strategy="all"
)

# Instantiate a NoiseLearner object and execute the noise learning program
learner = NoiseLearner(mode=backend, options=learner_options)
job = learner.run([circuit_to_run])
noise_model = job.result()

Input noise model to a primitive

The noise model learned on the circuit can also be used as an input to the EstimatorV2 primitive implemented in Qiskit Runtime. This can be passed into the primitive a few different ways. The next three examples show how you can pass the noise model to the estimator.options attribute directly, by using a ResilienceOptionsV2 object before instantiating an Estimator primitive, and by passing in an appropriately formatted dictionary.

# Pass the noise model to the `estimator.options` attribute directly
estimator = EstimatorV2(mode=backend)
estimator.options.resilience.layer_noise_model = noise_model
# Specify options through a ResilienceOptionsV2 object
resilience_options = ResilienceOptionsV2(layer_noise_model=noise_model)
estimator_options = EstimatorOptions(resilience=resilience_options)
estimator = EstimatorV2(mode=backend, options=estimator_options)
# Specify options by using a dictionary
options_dict = {
    "resilience_level": 2,
    "resilience": {"layer_noise_model": noise_model},
}

estimator = EstimatorV2(mode=backend, options=options_dict)

After the noise model is passed into the EstimatorV2 object, it can be used to run workloads and perform error mitigation as normal.


NoiseLearnerV3

Overview

Similar to NoiseLearner, the NoiseLearnerV3 class performs experiments that characterize noise processes based on a Pauli-Lindblad noise model for one or more circuits. Its run() method takes a list of instructions, each of which must be a twirled-annotated BoxOp containing ISA operations.

The result of a NoiseLearnerV3 job contains a list of NoiseLearnerV3Result objects, one for each input instruction. The following code shows how to use the helper program.

from qiskit import QuantumCircuit
from qiskit.transpiler import CouplingMap
from qiskit.transpiler import generate_preset_pass_manager

from qiskit_ibm_runtime import QiskitRuntimeService, Executor
from qiskit_ibm_runtime.noise_learner_v3 import NoiseLearnerV3
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions


# Build a circuit with two entangling layers
num_qubits = 27
edges = list(CouplingMap.from_line(num_qubits, bidirectional=False))
even_edges = edges[::2]
odd_edges = edges[1::2]

circuit = QuantumCircuit(num_qubits)
for pair in even_edges:
    circuit.cx(pair[0], pair[1])
for pair in odd_edges:
    circuit.cx(pair[0], pair[1])

# Choose a backend to run on
service = QiskitRuntimeService()
backend = service.least_busy()

# Transpile the circuit for execution
pm = generate_preset_pass_manager(backend=backend, optimization_level=3)
isa_circuit = pm.run(circuit)

# Run the boxing pass manager to group instructions into annotated boxes
boxing_pm = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=False,
    inject_noise_targets="gates",  # no measurement mitigation
    inject_noise_strategy="uniform_modification",
)
boxed_circuit = boxing_pm.run(isa_circuit)

# Find unique boxed instructions
unique_box_instructions = find_unique_box_instructions(boxed_circuit.data)
print(f"Found {len(unique_box_instructions)} unique layers")
print(
    f"Each instruction is of type {type(unique_box_instructions[0].operation)}"
)
print(
    f"And has annotations: {unique_box_instructions[0].operation.annotations}"
)

# Instantiate a NoiseLearnerV3 object and execute the noise learning program
learner = NoiseLearnerV3(backend)
learner.options.shots_per_randomization = 128
learner.options.num_randomizations = 32
learner_job = learner.run(unique_box_instructions)
learner_result = learner_job.result()

Output:

Found 3 unique layers
Each instruction is of type <class 'qiskit.circuit.controlflow.box.BoxOp'>
And has annotations: [Twirl(group='pauli', dressing='left', decomposition='rzsx'), InjectNoise(ref='r789B', modifier_ref='', site='before')]

The job result is a list of NoiseLearnerV3Result objects, one for each input-boxed set of instructions. NoiseLearnerV3Result has a to_pauli_lindblad_map() method that returns a PauliLindbladMap object, which has methods to extract the generators, error rates, and more.

print(
    f"The Noise learner V3 result contains {len(learner_result)} entries"
    f" and each has the following type:\n {type(learner_result[0])}\n"
)
noise_map = learner_result[0].to_pauli_lindblad_map()
print(
    f"After converting to PauliLindbladMap, you can extract data "
    f" on the generators for the error channel (truncated to 3): \n{noise_map.generators()[:3]}\n"
)
with numpy.printoptions(threshold=20):
    print(
        f"Along with the error rates (truncated to 3): \n{noise_map.rates[:3]}\n"
    )

Output:

The Noise learner V3 result contains 3 entries and each has the following type:
 <class 'qiskit_ibm_runtime.results.noise_learner_v3.NoiseLearnerV3Result'>

After converting to PauliLindbladMap, you can extract data  on the generators for the error channel (truncated to 3): 
<QubitSparsePauliList with 3 elements on 27 qubits: [X_0, Y_0, Z_0]>

Along with the error rates (truncated to 3): 
[0.00026 0.00032 0.00023]

Noise learning options

NoiseLearnerV3 supports several options, including the number of randomizations and layer pair depth, among others. Similar to the primitives, you can specify the options during or after instantiating the NoiseLearnerV3 object. The previous code example demonstrated how to set the shots_per_randomization and num_randomizations options. Refer to the NoiseLearnerV3Options API documentation for detailed information.

Input a noise model to Executor

Executor follows the design intents specified in circuit annotations (in the form of a samplex) and options. InjectNoise is the annotation for specifying where to inject noise, and the pauli_lindblad_maps samplex argument specifies which noise map to use.

The circuit in the previous example runs through the boxing pass manager, which groups instructions into annotated boxes. The relevant code is added here for ease of understanding.

  • inject_noise_targets=”gates” specifies to add the InjectNoise annotations to boxes that contain entanglers.
  • inject_noise_strategy="uniform_modification" specifies to assign the same ref and modifier_ref to all equivalent boxes with InjectNoise annotations.
    • InjectNoise.ref is a unique identifier used to assign a noise model to that box.
    • InjectNoise.modifier_ref allows scaling the noise model assigned to a box by multiplicative factors.
boxing_pm = generate_boxing_pass_manager(
    enable_gates=True,
    enable_measures=False,
    inject_noise_targets="gates",  # no measurement mitigation
    inject_noise_strategy="uniform_modification",
)

The circuit from the previous example contains three boxes, two of which contain InjectNoise annotations with different ref attributes (since they are not equivalent).

# box_circuit comes from the example above
for idx, instruction in enumerate(boxed_circuit):
    # The `InjectNoise` annotation defines which boxes to inject noise.
    print(f"Annotations of box #{idx}: {instruction.operation.annotations}\n")

Output:

Annotations of box #0: [Twirl(group='pauli', dressing='left', decomposition='rzsx'), InjectNoise(ref='r789B', modifier_ref='r789B', site='before')]

Annotations of box #1: [Twirl(group='pauli', dressing='left', decomposition='rzsx'), InjectNoise(ref='r054B', modifier_ref='r054B', site='before')]

Annotations of box #2: [Twirl(group='pauli', dressing='right', decomposition='rzsx')]

The result of the NoiseLearnerV3 job must be converted to a dictionary before being passed to Executor. This dictionary's keys are the InjectNoise.ref attributes and the values are the corresponding noise maps. This mapping tells Executor which noise models to inject where.

The following code shows how to take the circuit and the NoiseLearnerV3 result from the previous example and pass them to Executor, which will generate the circuit variants with the injected noise models and execute them on hardware.

from qiskit_ibm_runtime.quantum_program import QuantumProgram
from samplomatic import build

# Generate a quantum program
program = QuantumProgram(shots=1000)

# Build the template circuit and samplex pair
template_circuit, samplex = build(boxed_circuit)

# Convert the NoiseLearnerV3 result to a dictionary
noise_maps = learner_result.to_dict(
    instructions=unique_box_instructions, require_refs=False
)

# Append the samplex item and execute
program.append_samplex_item(
    template_circuit,
    samplex=samplex,
    samplex_arguments={
        "pauli_lindblad_maps": noise_maps,
    },
)

executor = Executor(backend)
executor_job = executor.run(program)

Next steps

Recommendations