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.

Build a learning experiment

This tutorial demonstrates how to build a standard learning experiment from scratch, and use it to learn a model from simulated noisy data.

  1. Define a gate set on a ring of qubits
  2. Choose a 2-local Pauli-Lindblad model
  3. Build the learning experiment
  4. Generate circuits
  5. Set up local simulation
  6. Run the program
  7. Analyze the data
Running on real hardware

The circuits below are simulated locally, so this walkthrough needs no IBM Quantum® credentials. Three changes take it to a real device, each flagged again where it applies:


1. Define a gate set on a ring of qubits

Build a QiskitGateSet from a backend Target and a subset of its qubits — here a twelve-qubit ring of FakeMarrakesh — then add one gate to it: a layer of six CZ gates covering that ring. By default the gate set is initialized with a preparation gate P and a measurement gate M.

from qiskit_ibm_runtime.fake_provider import FakeMarrakesh

from qiskit_noise_learning.gate_sets import QiskitGateSet

backend = FakeMarrakesh()

qubit_subset = [*range(25, 30), *range(37, 39), *range(45, 50)]
cz_pairs = [(25, 26), (27, 28), (29, 38), (37, 45), (46, 47), (48, 49)]

gate_set = QiskitGateSet(backend.num_qubits, target=backend.target, qubit_subset=qubit_subset)

with gate_set.build_new_gate("cz_gate", latex_str=r"\mathrm{CZ}") as builder:
    for pair in cz_pairs:
        builder.circuit.cz(*pair)

list(gate_set)
['M', 'P', 'cz_gate']
Running on real hardware
from qiskit_ibm_runtime import QiskitRuntimeService

backend = QiskitRuntimeService().backend("ibm_marrakesh")

Call draw() to see the gate on the device topology: the ring of qubits in the gate set, and which pairs of them the gate entangles.

gate_set.draw()

2. Choose a 2-local Pauli-Lindblad model

Decide which Pauli-Lindblad generators the noise is allowed to have with a PauliLindbladModel. Build that generator set from the gate set’s connectivity with k_local() — here every 2-local Pauli on connected qubit pairs of the unitary gate, and single-qubit Paulis for preparation and measurement.

from qiskit_noise_learning.models import PauliLindbladModel

model = PauliLindbladModel.k_local(gate_set, gate_k={"cz_gate": 2, "M": 1, "P": 1})

Display the unknown parameter count for the noise model of each gate.

{name: len(generators) for name, generators in model.generators.items()}
{'M': 12, 'P': 12, 'cz_gate': 144}

3. Build the learning experiment

Assemble an Experiment by composing builder stages. Each stage reads what earlier stages wrote, so order matters:

from qiskit_noise_learning.experiment_builder import (
    BindFragmentDepths,
    CompleteSequences,
    EvenDepthVanillaPaths,
    Experiment,
    GenerateInstructionSequences,
    IdentifyRelations,
    MergeInstructionSequences,
    SPAMPaths,
    VanillaInstructionSequences,
)

experiment_builder = (
    # Add standard vanilla learning paths and instruction sequences, and identify relations
    EvenDepthVanillaPaths()
    + VanillaInstructionSequences()
    + IdentifyRelations()
    # Add paths for learning SPAM, then generate and merge instruction sequences for measuring them
    + SPAMPaths()
    + GenerateInstructionSequences()
    + MergeInstructionSequences()
    # Finalize by completing the instruction sequences and setting experiment depths
    + CompleteSequences()
    + BindFragmentDepths([2, 16, 32, 64])
)

experiment = experiment_builder.run(
    Experiment(fidelity_model=model, shots=20, randomizations=50)
)

print(f"Number of paths: {len(experiment.paths)}")
print(f"Number of instruction sequences: {len(experiment.instruction_sequences)}")
Number of paths: 156
Number of instruction sequences: 37

Observe design matrix rank:

print(f"Design matrix rank: {experiment.design_matrix.rank}")
Design matrix rank: 120

4. Generate circuits

Compile the experiment with an ExecutorCircuitGenerator, generating a QuantumProgram — one parameterized template circuit per fragment depth.

from qiskit_noise_learning.circuit_generator import ExecutorCircuitGenerator

circuit_generator = ExecutorCircuitGenerator(gate_set)
quantum_program = circuit_generator.generate(experiment)

print(f"Number of template circuits: {len(quantum_program.items)}")
Number of template circuits: 5

Draw a template circuit to inspect.

quantum_program.items[0].circuit.draw("mpl", idle_wires=False, fold=False)
../_images/dcdaa347d38e1035a0396479bb23edae0b60dfe36ef934e052f8f8b1377174a3.png

5. Set up local simulation

An AerExecutor runs a program on a local Aer simulator, injecting Pauli-Lindblad noise at the barriers Samplomatic places around each twirled gate.

Unlike the hand-picked noise of the protocol guide, give every one of the model’s 168 generators an independent random rate. The model’s generators are already exactly the Paulis to put in a PauliLindbladMap, so pair each one with a rate and build the map per gate directly:

import numpy as np
from qiskit.quantum_info import PauliLindbladMap

rng = np.random.default_rng(1234)

true_maps = {
    name: PauliLindbladMap.from_terms(
        [
            PauliLindbladMap.GeneratorTerm(rate, generator)
            for generator, rate in zip(generators, rng.uniform(2e-4, 1.5e-3, len(generators)))
        ]
    )
    for name, generators in model.generators.items()
}
{name: noise_map.num_qubits for name, noise_map in true_maps.items()}
{'M': 156, 'P': 156, 'cz_gate': 156}

Those maps are as wide as the device, since the model is expressed in backend’s qubit indexing, but AerExecutor wants each map to be as wide as the gate it applies to, with Pauli indices running over the gate’s qubits in ascending physical order. Narrow them with keep_qubits(), which traces out everything off the ring:

noise_dict = {
    name: noise_map.keep_qubits(qubit_subset) for name, noise_map in true_maps.items()
}
{name: noise_map.num_qubits for name, noise_map in noise_dict.items()}
{'M': 12, 'P': 12, 'cz_gate': 12}

Instantiate AerExecutor with the stabilizer method. Set root_seed to make the simulated data reproducible.

from qiskit_aer import AerSimulator

from qiskit_noise_learning.aer_executor import AerExecutor

executor = AerExecutor(
    AerSimulator(method="stabilizer"), noise_dict=noise_dict, root_seed=42
)
Running on real hardware

Skip this step entirely.


6. Run the program

Run the program, then format the results with collect(), to produce a Fit — the container the analysis stages read from and write to.

job = executor.run(quantum_program)
fit = circuit_generator.collect(job.result())
Running on real hardware
from qiskit_ibm_runtime import Executor

executor = Executor(mode=backend)

7. Analyze the data

Build an analysis pipeline around nonnegative least squares fitting of the model.

from qiskit_noise_learning.analysis import (
    ComputeObservables,  # computes observables from raw data
    CurveFitObservables,  # performs exponential fitting
    LeastSquaresSolve,  # solves model with (nonnegative) least squares
)

analyzer = ComputeObservables() + CurveFitObservables() + LeastSquaresSolve()

fit = analyzer.run(fit)
/home/kaelyn/projects/open-source/notes/qiskit-noise-learning/qiskit_noise_learning/analysis/model_solve.py:250: UserWarning: 12 of 156 row(s) of the linear system have a non-positive or non-finite uncertainty, so they carry no usable statistical weight. Positions in LinearSystemData.row_labels: 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, and 2 more.
  system = LinearSystemData.from_fit(fit)

Plot the measured observable means against their fitted exponentials, one subplot per CZ pair, to see whether the decays being fitted are exponential at all.

fit.plot_qubit_pair_decays(
    pairs=cz_pairs,
    observable_type="means",
    exponential_fit=True,
)

Swap exponential_fit for model_prediction to compare the same data against the decays implied by the rates that came out of the non-negative least squares solve.

fit.plot_qubit_pair_decays(
    pairs=cz_pairs,
    observable_type="means",
    model_prediction=True,
)