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.

Quickstart

This guide demonstrates a minimal working example of the qiskit-addon-pna package. We use propagated noise absorption (PNA) to build a noise-mitigating observable. Given a circuit and a Pauli-Lindblad noise model, PNA classically propagates the observable through the inverse noise channel. Measuring the resulting observable on the noisy QPU mitigates the learned gate noise.

To see how to build a realistic workflow and run on quantum hardware with the directed execution model, including learning the noise model with NoiseLearnerV3, check out the PNA tutorial on the IBM Quantum Platform.


1. Prepare the inputs for PNA

PNA takes as input a circuit, noise model, and observable. Here we build a 10-qubit Trotterized transverse-field Ising model on a 1D chain. We generate a random 2-local Pauli-Lindblad noise model for each entangling gate and embed it as a Qiskit Aer PauliLindbladError instruction just before that gate. We choose a weight-4 Pauli-Z observable to measure.

import numpy as np
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp, pauli_basis
from qiskit_aer.noise import PauliLindbladError


def random_pauli_lindblad_noise(generators, seed, noise_scale=2e-3):
    rates = np.random.default_rng(seed).random(len(generators)) * noise_scale
    return PauliLindbladError(generators, rates)


def ising_circuit(
    num_qubits,
    layers,
    edge_noise=None,
    *,
    num_steps=3,
    rx_angle=np.pi / 8,
    rzz_angle=-np.pi / 2,
):
    """Trotterized transverse-field Ising model; edge_noise=None gives the noiseless circuit."""
    qc = QuantumCircuit(num_qubits)
    for _ in range(num_steps):
        qc.rx(rx_angle, range(num_qubits))
        for layer in layers:
            for edge in layer:
                if edge_noise is not None:
                    qc.append(
                        edge_noise[edge], edge
                    )  # inject synthetic gate noise
                qc.rzz(rzz_angle, *edge)
    return qc


num_qubits = 10

# Two entangling layers per Trotter step: even and odd bonds of a 1D chain
layers = [
    [(i, i + 1) for i in range(0, num_qubits - 1, 2)],
    [(i, i + 1) for i in range(1, num_qubits - 1, 2)],
]
edges = [edge for layer in layers for edge in layer]

# Random 2-local Pauli-Lindblad noise, one instance per entangling gate
two_qubit_paulis = SparsePauliOp(
    [p for p in pauli_basis(2) if np.sum(p.x + p.z)]
).paulis
edge_noise = {
    edge: random_pauli_lindblad_noise(two_qubit_paulis, seed=1234 + j)
    for j, edge in enumerate(edges)
}

noisy_circuit = ising_circuit(num_qubits, layers, edge_noise)

# A single weight-4 observable: <Z3 Z4 Z5 Z6>
observable = SparsePauliOp.from_sparse_list(
    [("ZZZZ", [3, 4, 5, 6], 1.0)], num_qubits=num_qubits
)

noisy_circuit.draw("mpl", fold=-1, scale=0.6)

Output:

Output of the previous code cell

2. Generate the noise-mitigating observable

generate_noise_mitigating_observable propagates each Pauli generator of the inverse noise channel forward to the end of the circuit. The observable is then back-propagated through the inverse noise channel, returning a new observable O~\tilde{O}. Three key parameters affect the computation cost:

  • max_err_terms: the number of terms kept in each anti-noise generator as it is propagated forward.
  • max_obs_terms: the number of terms kept in O~\tilde{O}.
  • atol: terms with coefficient magnitude below this threshold are dropped.

For this small, near-Clifford circuit we set the term limits high and use a modest atol, so O~\tilde{O} stays small and we can measure all of its terms.

Note: This function uses Python multiprocessing. When running as a script, call it inside an if __name__ == "__main__": guard.

from qiskit_addon_pna import generate_noise_mitigating_observable

mitigating_observable = generate_noise_mitigating_observable(
    noisy_circuit,
    observable,
    max_err_terms=100_000,
    max_obs_terms=100_000,
    atol=1e-5,
    num_processes=4,
)

print(f"Original observable:         {len(observable)} term")
print(f"Noise-mitigating observable: {len(mitigating_observable)} terms")

Output:

Original observable:         1 term
Noise-mitigating observable: 207 terms

3. Mitigate gate errors by measuring the noise-mitigating observable

Here we see that the new observable effectively mitigates the gate noise affecting the experiment.

import matplotlib.pyplot as plt
from qiskit_aer.primitives import EstimatorV2

noiseless_circuit = ising_circuit(num_qubits, layers)

# density_matrix method at zero precision -> exact expectation values (no shot noise)
estimator = EstimatorV2(
    options={
        "backend_options": {"method": "density_matrix"},
        "default_precision": 0.0,
    }
)

ideal, noisy, mitigated = (
    result.data.evs
    for result in estimator.run(
        [
            (noiseless_circuit, observable),
            (noisy_circuit, observable),
            (noisy_circuit, mitigating_observable),
        ]
    ).result()
)

print(f"Ideal (noiseless):   {ideal:.4f}")
print(f"Noisy (unmitigated): {noisy:.4f}")
print(f"Mitigated (PNA):     {mitigated:.4f}")

fig, ax = plt.subplots()
ax.bar(
    ["Noisy", "Mitigated"],
    [noisy, mitigated],
    width=0.6,
    color=["#b0b0b0", "#4c4c4c"],
)
ax.axhline(ideal, color="green", linestyle="--", label="Ideal (noiseless)")
ax.set_ylabel(r"$\langle Z_3 Z_4 Z_5 Z_6 \rangle$")
ax.legend()
plt.show()

Output:

Ideal (noiseless):   0.8073
Noisy (unmitigated): 0.6431
Mitigated (PNA):     0.8071
Output of the previous code cell