Probabilistic error cancellation with shaded lightcones
Tato stránka zatím nebyla přeložena do češtiny. Zobrazujete původní anglickou verzi.
Background
This tutorial demonstrates how to mitigate errors by using the Shaded lightcone (SLC) addon. This addon is an evolution of the probabilistic error cancellation (PEC) technique, wherein a user learns the noise of unique layers in a circuit and then cancels out the noise by applying single-qubit gates and post-processing techniques. Compared to other methods, PEC offers more robust bounds on the bias of the mitigated result, but tends to suffer from a higher overhead in terms of QPU time. During PEC, to compensate for attenuation of the expectation value by noise, the average result is rescaled by a factor of , where is the learned noise rate of error Pauli at layer in the circuit. This rescaling increases the variance by a factor of , and thus also multiplies the number of circuit executions needed on the QPU by , which we call the sampling cost or sampling overhead. Because grows exponentially, PEC is often limited to shallow or few-qubit circuits. Learn more about PEC in Probabilistic error cancellation with sparse Pauli-Lindblad models on noisy quantum processors.
If we can identify errors that do not need to be mitigated, we can decrease this sampling cost exponentially. A first step in this direction is implementing locally aware error mitigation, which uses a quickly computable conventional “lightcone” to reduce the PEC overhead by bounding an observable's sensitivity to errors throughout the circuit, extending the feasibility of PEC to larger scales for some problems. Errors outside of this lightcone cannot affect the measured outcome and can therefore be excluded from the error cancellation procedure. This exclusion decreases the sampling overhead, in some cases substantially, without introducing additional bias. In particular, for measuring a local observable of a fixed-depth circuit, the required sampling overhead eventually plateaus when scaling the number of qubits in the circuit (see Fig. 2b in Locality and Error Mitigation of Quantum Circuits.)
Shaded lightcones (SLC) go further, using classical simulations to more tightly bound the sensitivity to errors throughout the circuit. This trades some QPU time for CPU time and reduces the sampling overhead needed to renormalize the bias. Instead of a hard cutoff, each potential error in the circuit is assigned a graded “shade” that upper-bounds the susceptibility of the observable to that error. This refined characterization allows for more efficient, targeted applications of PEC with reduced variance, while giving the user the ability to controllably tune the bias in the observable estimation. See Lightcone shading for classically accelerated quantum error mitigation for more details.
Our workflow for the SLC addon leverages the new Samplomatic and Executor framework, allowing users to have more modular control of execution settings for error suppression and mitigation while retaining ease of use for advanced users. For a deeper understanding of the benefits of this framework and its general features, refer to the Hello samplomatic tutorial.
Workflow for lightcone shading, noise learning, and anti-noise injection
For modeling the QPU's noise, we chose to use a sparse Pauli-Lindblad noise model with 1- and 2-qubit Pauli error rates, locally generated on each qubit and edge of the device. With this choice, the SLC error-mitigation workflow presented in this tutorial is as follows:
a. CPU — Bound per-error impact of 1- and 2-qubit Pauli errors
- Forward propagation (bound effect on observable). Propagate each error to the end of the circuit and compute its commutator with the observable.
- Truncate operator terms during evolution to keep computation tractable.
- Further tighten these bounds by a loose back-propagation of the observable based on quantum speed limits.
- Backward propagation (bound effect on initial state). Propagate each error to the start of the circuit and compute its commutator with the initial state.
b. QPU — Learn noise rates. Use NoiseLearner to estimate rates of the Pauli-Lindblad noise model.
c. CPU — Prioritize mitigation
- Update merged bounds with learned noise rates. Combine forward and backward bounds that were previously computed and update them with learned noise rates.
- Rank noise components to mitigate by using the computed bounds and learned rates. Prioritize each possible noise error based on its estimated impact on bias and the associated expense to correct.
d. QPU — Insert antinoise and run. Execute the circuit of interest with antinoise (inverse noise) specified by using Box annotations.
e. CPU — Estimate observable. Compute the expectation value, applying measurement-based post-selection to reduce non-Markovian noise impact.
Noise learning overview
Noise learning is a common step in several error-mitigation methods, carried out by the NoiseLearner, and can be seen in our PEA error mitigation tutorial, as well as our Propagated noise absorption (PNA) tutorial. In NoiseLearnerV3, a user can specifically identify the to-be-learned noise layers as CircuitInstruction objects, which allows users to compute the desired SLC noise bounds for each layer in the manner described above. The learned Pauli-Lindblad model provides coefficients to be used in the PEC-SLC prioritization. The way in which the gates are collected into layers can be determined by using generate_boxing_pass_manager and unique_2q_instructions convenience functions, and then fed into the SLC utility function generate_noise_model_paulis, as described in Step 2 below.
| Part 1 | Part 2 | Part 3 |
|---|---|---|
| Pauli-twirl two-qubit gate layers | Repeat identity pairs of layers and learn noise | Derive a fidelity (error for each noise channel) |
![]() | ![]() |
Post-processing overview
After executing on quantum hardware by using the Samplomatic and Executor framework, we convert our bitstring measurements into the desired observable value. In the case of our mirrored Ising circuit, we will ideally get a measured observable of 1, as all qubits should ideally return to their starting point of . When computing the observable value with our expectation_values function, we will apply a few post-processing techniques that reduce noise impact. This includes removing shots affected by non-Markovian noise, readout-error mitigation, as well as accounting for details of our PEC implementation. Details are discussed in Step 4 below.
Requirements
Before starting this tutorial, ensure that you have the following packages installed:
- Qiskit IBM Runtime with the Executor primitive (
pip install "qiskit-ibm-runtime @ git+https://github.com/Qiskit/qiskit-ibm-runtime.git") - Qiskit addon Shaded lightcone 0.1 (
pip install "qiskit-addon-slc~=0.1.0") - Qiskit addon utils (
pip install "qiskit-addon-utils~=0.3.0") - Samplomatic v0.16 or more(
pip install samplomatic) - Qiskit Visualization support (
pip install "qiskit[visualization]")
Step 0. Setup
First, import the packages and functions needed to run this notebook successfully.
# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-addon-slc qiskit-addon-utils qiskit-ibm-runtime samplomatic
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(module)s %(message)s")
# Setting this value prevents itertools.starmap deadlock on UNIX systems
from multiprocessing import set_start_method
set_start_method("spawn")
# Needed to prevent PySCF from parallelizing internally (SLC only)
%set_env OMP_NUM_THREADS=1
env: OMP_NUM_THREADS=1
import pickle
import numpy as np
import samplomatic
from matplotlib import pyplot as plt
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import PassManager, generate_preset_pass_manager
from qiskit_addon_slc.bounds import (
compute_backward_bounds,
compute_forward_bounds,
compute_local_scales,
merge_bounds,
tighten_with_speed_limit,
)
from qiskit_addon_slc.utils import generate_noise_model_paulis, map_modifier_ref_to_ref
from qiskit_addon_slc.visualization import draw_shaded_lightcone
from qiskit_addon_utils.exp_vals.expectation_values import executor_expectation_values
from qiskit_addon_utils.exp_vals.measurement_bases import get_measurement_bases
from qiskit_addon_utils.noise_management import gamma_from_noisy_boxes, trex_factors
from qiskit_addon_utils.noise_management.post_selection import PostSelector
from qiskit_addon_utils.noise_management.post_selection.transpiler.passes import (
AddPostSelectionMeasures,
AddSpectatorMeasures,
)
from qiskit_ibm_runtime import Executor, QiskitRuntimeService, QuantumProgram
from qiskit_ibm_runtime.noise_learner_v3 import NoiseLearnerV3
from qiskit_ibm_runtime.options import NoiseLearnerV3Options
from samplomatic.transpiler import generate_boxing_pass_manager
from samplomatic.utils import find_unique_box_instructions
Step 1. Map the problem
For ease of demonstration, we select a 1D mirror Ising chain. The 1D Ising chain gives a nicely dense circuit structure, which is convenient for showcasing PEC implementations. A mirror circuit makes it straightforward to know the expected outcome (namely, we should measure an observable of 1).
Further, we want to run a mirror circuit, so for every gate in the second half of the circuit, there needs to be an inverse gate in the first half. As the measured observable has non-Z-basis measurements, and the executor accounts for the desired basis at the end of the circuit, we provide a prepare_basis function that inserts the appropriate gates at the start of the mirror circuit. This detail is specific to our mirror-circuit demonstration. The get_measurement_bases function allows us to easily identify which gates are needed and where to append them, as well as keeping track of qubit index subtleties arising from conventions in box annotation as discussed in the "Prepare canonical bases measurements" section.
num_qubits = 20
target_obs_sparse = [("XZ", [6, 13], 1.0)]
observable = SparsePauliOp.from_sparse_list(target_obs_sparse, num_qubits=num_qubits)
bases_virt, reverser_virt = get_measurement_bases(observable)
num_trotter_steps = 10
rx_angle = np.pi / 4
def construct_ising_circuit(
num_qubits: int, num_trotter_steps: int, rx_angle: float, barrier: bool = True
) -> QuantumCircuit:
circuit = QuantumCircuit(num_qubits)
for _step in range(num_trotter_steps):
circuit.rx(rx_angle, range(num_qubits))
if barrier:
circuit.barrier()
for first_qubit in (1, 2):
for idx in range(first_qubit, num_qubits, 2):
# equivalent to Rzz(-pi/2):
circuit.sdg([idx - 1, idx])
circuit.cz(idx - 1, idx)
if barrier:
circuit.barrier()
return circuit
def prepare_basis(circuit: QuantumCircuit, basis: list[int]) -> QuantumCircuit:
# basis is a list of integer values from 0 to 3. These map to the basis measurement as:
# 0 = I; 1 = Z; 2 = X; 3 = Y
assert len(basis) == circuit.num_qubits
out_circ = circuit.copy_empty_like()
for qb, bas in enumerate(basis):
if bas in {0, 1}:
continue
if bas == 2:
out_circ.h(qb)
elif bas == 3:
out_circ.rx(-np.pi / 2, qb)
out_circ.barrier()
out_circ.compose(circuit, inplace=True)
return out_circ
def mirror_circuit(circuit: QuantumCircuit, *, inverse_first: bool = False) -> QuantumCircuit:
mirror_circ = circuit.copy_empty_like()
mirror_circ.compose(circuit.inverse() if inverse_first else circuit, inplace=True)
mirror_circ.barrier()
mirror_circ.compose(circuit if inverse_first else circuit.inverse(), inplace=True)
mirror_circ.measure_active()
return mirror_circ
# Instantiate circuit
circuit = construct_ising_circuit(num_qubits, num_trotter_steps, rx_angle, barrier=False)
mirrored_circuit = mirror_circuit(circuit, inverse_first=True)
mirrored_circuit = prepare_basis(mirrored_circuit, bases_virt[0])
mirrored_circuit.draw("mpl", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False)

Step 2. Optimize
We will optimize details associated with the circuit to be run, the observable to be measured, and the noise-learning parameters. As a starting point, we ensure that we instantiate a backend with fractional gates turned on as an option. These fractional gates will allow for greater sensitivity in some of our post-selection filtering.
token = "<YOUR_TOKEN>"
instance = "<YOUR_INSTANCE>"
# This is used to retrieve shared results
shared_service = QiskitRuntimeService(
channel="ibm_quantum_platform",
token=token,
instance=instance,
)
# This is used to run on real hardware
service = service = QiskitRuntimeService()
qiskit_runtime_service._discover_account:WARNING:2025-11-10 11:19:40,108: Loading account with the given token. A saved account will not be used.
backend = service.backend("ibm_kingston", use_fractional_gates=True)
First, we will transpile our circuit to ISA instructions, as required for execution on our QPUs. For the data collected in this experiment, we hand select our qubits based on evaluation of the highest quality chain.
layout = [44, 45, 46, 47, 57, 67, 68, 69, 78, 89, 88, 87, 97, 107, 106, 105, 104, 103, 96, 83]
isa_pm = generate_preset_pass_manager(backend=backend, initial_layout=layout, optimization_level=0)
isa_circuit = isa_pm.run(mirrored_circuit)
assert isa_circuit.layout.final_index_layout() == layout
isa_observable = observable.apply_layout(layout, num_qubits=isa_circuit.num_qubits)
2025-11-10 11:19:57,810 INFO base_tasks Pass: ContainsInstruction - 0.00715 (ms)
2025-11-10 11:19:57,811 INFO base_tasks Pass: UnitarySynthesis - 0.00525 (ms)
2025-11-10 11:19:57,811 INFO base_tasks Pass: HighLevelSynthesis - 0.02599 (ms)
2025-11-10 11:19:57,811 INFO base_tasks Pass: BasisTranslator - 0.09131 (ms)
2025-11-10 11:19:57,811 INFO base_tasks Pass: SetLayout - 0.02623 (ms)
2025-11-10 11:19:57,812 INFO base_tasks Pass: FullAncillaAllocation - 0.14400 (ms)
2025-11-10 11:19:57,812 INFO base_tasks Pass: EnlargeWithAncilla - 0.06318 (ms)
2025-11-10 11:19:57,813 INFO base_tasks Pass: ApplyLayout - 0.29802 (ms)
2025-11-10 11:19:57,813 INFO base_tasks Pass: CheckMap - 0.07820 (ms)
2025-11-10 11:19:57,814 INFO base_tasks Pass: FilterOpNodes - 0.33283 (ms)
2025-11-10 11:19:57,814 INFO base_tasks Pass: UnitarySynthesis - 0.00691 (ms)
2025-11-10 11:19:57,814 INFO base_tasks Pass: HighLevelSynthesis - 0.13208 (ms)
2025-11-10 11:19:57,816 INFO base_tasks Pass: BasisTranslator - 1.00303 (ms)
2025-11-10 11:19:57,818 INFO base_tasks Pass: FoldRzzAngle - 1.78719 (ms)
2025-11-10 11:19:57,818 INFO base_tasks Pass: ContainsInstruction - 0.00691 (ms)
2025-11-10 11:19:57,818 INFO base_tasks Pass: InstructionDurationCheck - 0.00405 (ms)
wire_order = layout + [q for q in range(isa_circuit.num_qubits) if q not in layout]
isa_circuit.draw(
"mpl", fold=-1, scale=0.3, idle_wires=False, wire_order=wire_order, measure_arrows=False
)

Box the circuit
For ease of implementation, we will utilize the generate_boxing_pass_manager transpilation pass, which places the circuit instructions into annotated boxes. These boxes clearly indicate where, in the case of PEC, antinoise should be injected into the circuit. For details on settings, refer to the Samplomatic documentation.
Note that the SLC workflow the use of inject_noise_strategy="individual_modification" later in the process because this allows us to uniquely identify each BoxOp in the circuit.
The find_unique_box_instructions function iterates through the provided boxed circuit and identifies those that have unique 2Q layers or measurements, for the purpose of noise learning and noise injection.
# Box circuit with Twirl and InjectNoise annotations
boxes_pm = generate_boxing_pass_manager(
twirling_strategy="active",
inject_noise_strategy="individual_modification",
inject_noise_targets="gates",
measure_annotations="all",
)
boxed_circuit = boxes_pm.run(isa_circuit)
# Find the unique instructions (layers) from boxed circuit
unique_2q_instructions = find_unique_box_instructions(
boxed_circuit, normalize_annotations=None, undress_boxes=True
)
2025-11-10 11:20:01,088 INFO base_tasks Pass: RemoveBarriers - 0.02289 (ms)
2025-11-10 11:20:01,100 INFO base_tasks Pass: GroupGatesIntoBoxes - 12.38990 (ms)
2025-11-10 11:20:01,101 INFO base_tasks Pass: GroupMeasIntoBoxes - 0.47898 (ms)
2025-11-10 11:20:01,104 INFO base_tasks Pass: AddTerminalRightDressedBoxes - 2.88177 (ms)
2025-11-10 11:20:01,111 INFO base_tasks Pass: AddInjectNoise - 6.66904 (ms)
boxed_circuit.draw(
"mpl", fold=-1, scale=0.3, idle_wires=False, wire_order=wire_order, measure_arrows=False
)

Prepare canonical bases measurements
Due to how the qubits are labeled when identifying unique 2Q layers, one must take special care to keep track of qubit ordering. Below, we introduce the notion of canonical_qubits as a means to appropriately update the qubit ordering when providing it to the executor, as a result of how qubit order is captured when boxing circuits and finding unique instructions. See the Qubit ordering convention documentation for details.
# Determine the canonical qubits order
meas_box = boxed_circuit.data[-1]
canonical_qubits = [
idx for idx, qubit in enumerate(boxed_circuit.qubits) if qubit in meas_box.qubits
]
# map canonical qubit to physical (isa) qubit
c_2_p = {c: p for c, p in enumerate(canonical_qubits)}
# map physical (isa) qubit to virtual qubit (index in original circuit)
p_2_v = {p: v for v, p in enumerate(layout)}
# compute map between virtual and canonical qubit indices.
c_2_v = {c: p_2_v[p] for c, p in c_2_p.items()}
assert len(c_2_v) == num_qubits
bases_canon = [
np.array([base_i[c_2_v[c]] for c in range(num_qubits)], dtype=np.uint8) for base_i in bases_virt
]
Workflow for lightcone shading, noise learning, and anti-noise injection
Note: For the implementation of SLC-PEC in this tutorial, we run SLC bound computations before the noise learning has been completed, so the to-be-mitigated circuit is run as close in time as possible to the learned noise model. In principle, this workflow can be further enhanced to execute simultaneously. Namely, a noise-learning job is run while, in parallel, noise bounds are estimated. For an arbitrary quantum circuit, the noise-bound computation can scale with a weakly exponential dependence. As such, it might be prudent to use parallelized execution when trying to maximize the workflow's efficiency. To this end, we briefly demonstrate this by including cluster-based (128-thread) resources and showing how you can achieve a more refined set of bounds for a provided circuit when constrained to equal computation-time limits, compared to our laptop (8 threads). Furthermore, although not implemented in this workflow, you can parallelize the QPU executions for noise learning and noise-bound computations to achieve the most efficient workflow.
Predict to-be-learned noise-model Paulis
The generate_noise_model_paulis function goes through each boxed layer of the provided circuit and generates all relevant weight-one and weight-two Pauli terms, taking into account the circuit's qubit connectivity, and selecting terms that are relevant to the active nodes and edges. These terms are then used to compute forward and backward noise bounds.
noise_model_paulis = generate_noise_model_paulis(
unique_2q_instructions, backend.coupling_map, boxed_circuit
)
noise_model_rates = {ref: None for ref in noise_model_paulis}

