{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "3ba61eec",
      "metadata": {},
      "source": [
        "---\n",
        "title: Tools to profile, customize, and inspect experiments\n",
        "description: Use Qiskit and IBM Quantum Compute Service developer tools to profile, customize, and inspect your experiment settings and runtime environment.\n",
        "---\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fdbb2ca4",
      "metadata": {},
      "source": [
        "# Tools to profile, customize, and inspect experiments\n",
        "\n",
        "*Usage estimate: 10 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*\n",
        "\n",
        "To get good results from a quantum computer, you need more than a well-designed circuit. The device's current condition, how your circuit is scheduled, the noise that the circuit experiences, and when and how your job runs on the quantum processing unit (QPU) all affect the quality of your results. Qiskit and IBM Quantum® Compute Service offer a growing set of developer tools that expose this information. You can use these tools to act on that information before you commit QPU time to a full experiment, and to debug a finished experiment.\n",
        "\n",
        "This tutorial uses a single end-to-end experiment to show how to combine these tools at each stage of a typical development workflow. Not every tool is useful for every experiment, so choose the tools that address the questions that you have about your own workload.\n",
        "\n",
        "The workflow is as follows:\n",
        "\n",
        "1. Prepare a mirror circuit of a Trotterized time evolution of a linear transverse-field Ising model.\n",
        "2. Select a backend with an alternative calibration data set that is designed for error mitigation methods based on a noise model.\n",
        "3. Run device benchmarking experiments so that the transpiler uses up-to-date device properties when it selects a layout.\n",
        "4. Inspect the timing information of the scheduled circuit.\n",
        "5. Learn Pauli-Lindblad and readout noise models for the circuit.\n",
        "6. Run noiseless, unmitigated, and mitigated local simulations. The noisy simulations use the learned noise model to show how much improvement you might gain from the selected error mitigation methods. This tutorial uses probabilistic error cancellation (PEC) and Twirled Readout Error eXtinction (TREX).\n",
        "7. Get a usage estimate without running on a QPU, to check whether you need to adjust the experiment to fit your budget.\n",
        "8. Start a session and trigger calibration so that the experiment uses the most recent calibration data.\n",
        "9. Run the experiment on the QPU by using Estimator, and compare the hardware results with the simulated preview.\n",
        "10. Inspect the execution spans to see whether there are significant gaps between groups of executions (chunks).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "480d8e52",
      "metadata": {},
      "source": [
        "## Requirements\n",
        "\n",
        "Before starting this tutorial, ensure that you have the following installed:\n",
        "\n",
        "* Qiskit SDK v2.3 or later, with [visualization](/docs/api/qiskit/visualization) support\n",
        "* `qiskit-ibm-runtime` v0.52 or later (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer (`pip install qiskit-aer`)\n",
        "* Plotly (`pip install plotly`)\n",
        "* [Qiskit Device Benchmarking](https://github.com/qiskit-community/qiskit-device-benchmarking) (`pip install git+https://github.com/qiskit-community/qiskit-device-benchmarking.git`)\n",
        "\n",
        "<Admonition type=\"note\" title=\"Plan requirements\">\n",
        "  Some steps in this tutorial are not available on every plan:\n",
        "\n",
        "  * Dry-run jobs (Steps 2.4 and 3.5) are available only on the Flex, Premium, and On-Prem plans.\n",
        "  * Sessions (Step 4.1) are not available on the Open Plan. To run in batch mode instead, set `run_calibration = False` in Step 4.1.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4c19bf19",
      "metadata": {},
      "source": [
        "## Summary of tools used\n",
        "\n",
        "| Tool                                     | What it tells you or does                                                                                                      | Step | Docs                                                                                 |\n",
        "| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ---- | ------------------------------------------------------------------------------------ |\n",
        "| Specially calibrated instructions        | Non-standard calibrations that can benefit some experiments.                                                                   | 2.1  | [Documentation](/docs/ibm-quantum-compute/custom-calibration)                        |\n",
        "| Qubit quality benchmarking               | Refreshes device properties data.                                                                                              | 2.2  | [Tutorial](/docs/tutorials/refresh-backend-properties-with-real-time-benchmarking)   |\n",
        "| Timing for scheduled circuits            | Visualizes when each instruction runs, to help spot idle time and inefficient instruction alignment.                           | 2.4  | [Documentation](/docs/ibm-quantum-compute/qiskit-runtime-circuit-timing)             |\n",
        "| `select-error-mitigation` AI agent skill | Compares error mitigation methods for your workload and reports which ones to use and why.                                     | 3.2  | [GitHub repository](https://github.com/Qiskit/skills)                                |\n",
        "| Client-side primitives                   | New implementations of the primitives that show more detail about pre- and post-processing, and give you more control over it. | 3.2  | [Documentation](/docs/guides/migrate-to-client-side-primitives)                      |\n",
        "| `NoiseLearnerV3`                         | Performs noise learning by using both the Pauli-Lindblad and TREX protocols.                                                   | 3.3  | [Documentation](/docs/ibm-quantum-compute/noise-learning)                            |\n",
        "| Local mode                               | Verifies syntactic correctness and compares the effects of different error mitigation methods.                                 | 3.4  | [Documentation](/docs/ibm-quantum-compute/local-testing-mode)                        |\n",
        "| Usage estimation                         | Provides an accurate usage estimate before hardware execution.                                                                 | 3.5  | [Documentation](/docs/ibm-quantum-compute/dry-run-jobs)                              |\n",
        "| Calibration in sessions                  | Triggers calibration to ensure that the experiment uses optimal readout parameters.                                            | 4.1  | [Documentation](/docs/ibm-quantum-compute/calibration-jobs)                          |\n",
        "| Execution spans                          | Shows when each circuit configuration ran on the QPU, and in which order.                                                      | 4.4  | [Documentation](/docs/ibm-quantum-compute/sampler-input-output#view-execution-spans) |\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e5d33241",
      "metadata": {},
      "source": [
        "## Step 1: Specify a circuit and observable\n",
        "\n",
        "Prepare a mirror circuit of a Trotterized time evolution of a linear transverse-field Ising model. Mirror the circuit to cancel the circuit unitary and form an identity circuit. Because the ideal output is the all-zero state, the ideal expectation value of each single-qubit $Z$ observable is 1. Mirroring also doubles the circuit depth, which amplifies the effect of noise.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "7a11c71f",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "from qiskit.circuit import QuantumCircuit\n",
        "from qiskit.quantum_info import SparsePauliOp\n",
        "\n",
        "\n",
        "def construct_ising_circuit(\n",
        "    num_qubits: int,\n",
        "    num_trotter_steps: int,\n",
        "    rx_angle: float,\n",
        "    barrier: bool = True,\n",
        ") -> QuantumCircuit:\n",
        "    circuit = QuantumCircuit(num_qubits)\n",
        "\n",
        "    for _step in range(num_trotter_steps):\n",
        "        circuit.rx(rx_angle, range(num_qubits))\n",
        "        if barrier:\n",
        "            circuit.barrier()\n",
        "        for first_qubit in (1, 2):\n",
        "            for idx in range(first_qubit, num_qubits, 2):\n",
        "                # equivalent to Rzz(-pi/2):\n",
        "                circuit.sdg([idx - 1, idx])\n",
        "                circuit.cz(idx - 1, idx)\n",
        "        if barrier:\n",
        "            circuit.barrier()\n",
        "\n",
        "    return circuit\n",
        "\n",
        "\n",
        "def mirror_circuit(circuit: QuantumCircuit) -> QuantumCircuit:\n",
        "    mirror_circ = circuit.copy_empty_like()\n",
        "    mirror_circ.compose(circuit, inplace=True)\n",
        "    mirror_circ.barrier()\n",
        "    mirror_circ.compose(circuit.inverse(), inplace=True)\n",
        "    mirror_circ.measure_active()\n",
        "    return mirror_circ"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "fa06845c",
      "metadata": {},
      "outputs": [],
      "source": [
        "num_qubits = 20\n",
        "observables = [\n",
        "    SparsePauliOp(\"I\" * ((num_qubits - 1) - q) + \"Z\" + \"I\" * q)\n",
        "    for q in range(num_qubits)\n",
        "]\n",
        "\n",
        "# Free parameters of the demonstration circuit: the number of Trotter steps\n",
        "# sets the circuit depth, and rx_angle is the transverse-field rotation angle\n",
        "# applied in each step (pi/4 makes the dynamics non-Clifford)\n",
        "num_trotter_steps = 5\n",
        "rx_angle = np.pi / 4\n",
        "\n",
        "circuit = construct_ising_circuit(\n",
        "    num_qubits, num_trotter_steps, rx_angle, barrier=False\n",
        ")\n",
        "mirrored_circuit = mirror_circuit(circuit)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "caa280da",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/profiling-tools/extracted-outputs/caa280da-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "mirrored_circuit.draw(\n",
        "    \"mpl\", fold=-1, scale=0.3, idle_wires=False, measure_arrows=False\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "933629c0",
      "metadata": {},
      "source": [
        "## Step 2: Optimize for quantum hardware execution\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "fe9679d3",
      "metadata": {},
      "source": [
        "### 2.1 Use alternative calibration if applicable\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6a3a3387",
      "metadata": {},
      "source": [
        "IBM® calibrates its QPUs periodically for good performance across general use cases. Some workloads, however, benefit from instructions whose parameters are tuned differently. For more information, refer to [Specially calibrated instructions](/docs/ibm-quantum-compute/custom-calibration).\n",
        "\n",
        "This tutorial uses an alternative calibration data set called `slow_gates_ac`. This calibration contains a slower `cz` gate and modulates the two-level system (TLS) bias to stabilize device errors over time. These changes benefit error mitigation methods that rely on a learned noise model. The `slow_gates_ac` calibration is available only on Heron QPUs. If you run on a Nighthawk QPU, use `slow_gates` instead.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "6fb01c25",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "service = QiskitRuntimeService()\n",
        "# Use slow_gates_ac because this tutorial uses a noise-learning-based mitigation method\n",
        "backend = service.backend(\"ibm_kingston\", calibration_id=\"slow_gates_ac\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "200d8983",
      "metadata": {},
      "source": [
        "### 2.2 Run real-time device benchmarking\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "98166362",
      "metadata": {},
      "source": [
        "One of the most critical stages of transpilation is layout selection, which maps virtual qubits in a circuit to physical qubits on the target device. A good layout minimizes the number of SWAP operations needed and places the circuit on the best-performing qubits, so the quality of the initial layout can significantly affect the quality of your results.\n",
        "\n",
        "Periodic benchmarking routines run on every IBM QPU to report the current performance of its qubits: relaxation times ($T_1$), coherence times ($T_2$), readout errors, and one- and two-qubit gate errors. The Qiskit transpiler uses these metrics during layout selection.\n",
        "\n",
        "The properties of superconducting qubits, however, can drift on shorter timescales, and transient defects, known as TLSs, can temporarily degrade a qubit that performed well at calibration time. If the transpiler uses outdated information, it cannot select the best layout. Therefore, it can be worthwhile to spend some QPU time to characterize the device before you select a layout.\n",
        "\n",
        "Refer to the [Refresh backend properties with real-time benchmarking](/docs/tutorials/refresh-backend-properties-with-real-time-benchmarking) tutorial for a detailed walk-through.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3c968b2b",
      "metadata": {},
      "source": [
        "The characterization suite builds several experiments, submits multiple jobs, and fits the results. This process can take a few minutes in total. Enable INFO-level logging so that each stage prints its progress as it runs.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "c1c8e265",
      "metadata": {},
      "outputs": [],
      "source": [
        "import sys\n",
        "import logging\n",
        "\n",
        "logger = logging.getLogger(\"qiskit_device_benchmarking\")\n",
        "logger.setLevel(logging.INFO)\n",
        "logger.addHandler(logging.StreamHandler(stream=sys.stdout))"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "226f7593",
      "metadata": {},
      "source": [
        "`BackendCharacterization` is a convenience class that runs the characterization experiments of your choice, analyzes the results, and returns an updated `Backend` object that you can pass to the transpiler.\n",
        "\n",
        "You might see a warning that a backend was passed in as the mode while a batch is open. This warning is expected, and you can safely ignore it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "1475a744",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "base_primitive.get_mode_service_backend:WARNING:2026-11-06 17:02:58,380: A backend was passed in as the mode but a session context manager is open so this job will run inside this session/batch instead of in job mode.\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Building readout experiments\n",
            "Building T1 experiments\n",
            "Building T2 experiments\n",
            "Readout job submitted: dbn4t55t7fqc73aiugag\n",
            "T1 experiment submitted: ['dbn4t5gqtr0c73accb9g']\n",
            "T2 (Hahn) experiment submitted: ['dbn4t6c4148s7390ardg']\n"
          ]
        }
      ],
      "source": [
        "from qiskit_ibm_runtime import Batch\n",
        "from qiskit_device_benchmarking.utilities.characterization_utils import (\n",
        "    BackendCharacterization,\n",
        ")\n",
        "\n",
        "# The full suite is [\"readout\", \"rb_1q\", \"rb_2q\", \"t1\", \"t2\"].\n",
        "# Run a subset of the experiments to save time.\n",
        "experiments = [\"readout\", \"t1\", \"t2\"]\n",
        "characterizer = BackendCharacterization(backend)\n",
        "with Batch(backend=backend):\n",
        "    jobs = characterizer.run_experiments(experiments=experiments)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "37549bfc",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Updating readout error\n",
            "Updating T1\n",
            "Updating T2\n"
          ]
        }
      ],
      "source": [
        "# Retrieve all job results and update the backend data\n",
        "characterizer.analyze_results()\n",
        "backend_updated = characterizer.update_backend()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b7458e6b",
      "metadata": {},
      "source": [
        "### 2.3 Transpile to ISA\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f0b46d6e",
      "metadata": {},
      "source": [
        "Transform the circuit and observables to match the backend's instruction set architecture (ISA) by using the refreshed device properties.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "51ef38ea",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "\n",
        "pm = generate_preset_pass_manager(\n",
        "    backend=backend_updated, optimization_level=3\n",
        ")\n",
        "isa_circuit = pm.run(mirrored_circuit)\n",
        "isa_observables = [\n",
        "    observable.apply_layout(isa_circuit.layout) for observable in observables\n",
        "]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d194d938",
      "metadata": {},
      "source": [
        "### 2.4 Visualize circuit timing\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "73407c24",
      "metadata": {},
      "source": [
        "In addition to the instructions in a circuit, you might want to visualize the circuit's schedule, to find idle time on qubits or inefficient instruction alignment. You can get the most detailed and accurate timing information from IBM Quantum Compute Service by using dry-run mode.\n",
        "\n",
        "The circuit in this tutorial is static, so its schedule is not very informative. You can get similar timing information locally from the circuit and the backend properties, for example by using the [timeline drawer](/docs/qiskit/visualize-circuit-timing) built into Qiskit. This tutorial includes this step to show how to request and visualize the timing information. Timing information from the service is most useful for [dynamic circuits](/docs/qiskit/classical-feedforward-and-control-flow). Dynamic circuits contain mid-circuit measurements, conditional operations, and feedforward, which add implicit operations such as broadcasting measurement results and determining branches. The timing of these operations cannot be determined from the circuit and backend properties alone.\n",
        "\n",
        "Refer to [Visualize circuit timing](/docs/ibm-quantum-compute/qiskit-runtime-circuit-timing) for more information.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cf627964",
      "metadata": {},
      "source": [
        "Scheduled circuit timing is available only for Sampler and Executor jobs, so the following cell submits the circuit in dry-run mode by using Sampler. Jobs that you submit in dry-run mode do not run on real QPUs and are not charged.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "338a9a89",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime.executor_sampler import Sampler\n",
        "\n",
        "sampler = Sampler(backend)\n",
        "# Request timing information in the result metadata\n",
        "sampler.options.experimental = {\n",
        "    \"execution\": {\n",
        "        \"scheduler_timing\": True,\n",
        "    },\n",
        "}\n",
        "\n",
        "# Specify dry_run=True to submit in dry-run mode\n",
        "sampler_job = sampler.run([isa_circuit], dry_run=True)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "362a5423",
      "metadata": {},
      "outputs": [],
      "source": [
        "job_result = sampler_job.result()\n",
        "circuit_schedule = job_result[0].metadata[\"compilation\"][\"scheduler_timing\"]\n",
        "circuit_schedule_timing = circuit_schedule[\"timing\"]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "7985f79b",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime.visualization import draw_circuit_schedule_timing\n",
        "\n",
        "# Create a figure from the metadata\n",
        "fig = draw_circuit_schedule_timing(\n",
        "    circuit_schedule=circuit_schedule_timing,\n",
        "    included_channels=None,\n",
        "    filter_readout_channels=False,\n",
        "    filter_barriers=False,\n",
        "    width=1000,\n",
        ")\n",
        "\n",
        "# For large circuits, displaying the figure inline can make your Jupyter session stop responding.\n",
        "# Uncomment the following to save it to an HTML file instead.\n",
        "# fig.write_html(\"scheduler_timing.html\")\n",
        "\n",
        "# For small circuits, uncomment the following line to display the figure inline.\n",
        "# fig.show(renderer=\"notebook\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "841efe57",
      "metadata": {},
      "source": [
        "This figure is too large to display in a notebook. The following screenshot shows a summarized view.\n",
        "\n",
        "<Accordion>\n",
        "  <AccordionItem title=\"View a screenshot of the scheduled circuit timing\">\n",
        "    ![Scheduled circuit timing for the mirror circuit, showing each qubit channel over time](https://eu-de.quantum.cloud.ibm.com/docs/images/tutorials/profiling-tools/scheduler-timing.avif)\n",
        "  </AccordionItem>\n",
        "</Accordion>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6c6a0f00",
      "metadata": {},
      "source": [
        "## Step 3: Apply error mitigation and test locally\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d123b7b0",
      "metadata": {},
      "source": [
        "### 3.1 Cliffordize the circuit\n",
        "\n",
        "The cost of general noisy simulation grows exponentially with the number of qubits, and most laptops cannot simulate more than about 32 qubits. Therefore, first convert the circuit to a Clifford circuit, which a stabilizer simulator can handle far more efficiently. The conversion rounds each rotation angle to a Clifford angle, so the simulated circuit approximates the original circuit rather than reproducing it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "e09f10b7",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit.transpiler import PassManager\n",
        "from qiskit_ibm_runtime.transpiler.passes import ConvertISAToClifford\n",
        "\n",
        "clifford_circuit = PassManager([ConvertISAToClifford()]).run(isa_circuit)\n",
        "clifford_pubs = [(clifford_circuit, isa_observables)]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1a3903c7",
      "metadata": {},
      "source": [
        "### 3.2 Apply PEC with TREX\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b0da0375",
      "metadata": {},
      "source": [
        "It can be difficult to select error mitigation methods that balance cost and accuracy for your specific experiment. You can use the `select-error-mitigation` AI agent skill to analyze the available methods for your workload. The skill presents a summary report that explains whether each method was selected and why.\n",
        "\n",
        "Refer to the [Qiskit AI skills](https://github.com/Qiskit/skills) GitHub repository for more information.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2995baca",
      "metadata": {},
      "source": [
        "This tutorial uses PEC with TREX directly, so that you can complete it without an AI agent.\n",
        "\n",
        "PEC is a technique that mitigates quantum noise when you calculate the expectation values of observables. Under ideal conditions, PEC can produce an unbiased estimate of the expectation values. It learns the noise of each layer of two-qubit gates, then runs an ensemble of circuits that, on average, apply the inverse of that noise. PEC uses Pauli twirling to tailor the noise into a Pauli channel, which can be learned and inverted efficiently.\n",
        "\n",
        "In practice, PEC is only as accurate as the learned noise model. The sparse Pauli-Lindblad model includes only one- and two-qubit error terms on neighboring qubits, and it cannot fully capture effects such as crosstalk between distant qubits or time-correlated errors. The device noise can also drift between noise learning and execution. PEC does not cancel noise that the model misses, and it overcompensates for noise that the model overestimates. Therefore, mitigated results can still deviate from the ideal values in either direction.\n",
        "\n",
        "TREX is a technique that mitigates measurement errors, and is useful when you estimate the expectation values of Pauli observables. It relies on measurement twirling, which diagonalizes the readout-error transfer matrix and makes it easy to invert. TREX learns a rescaling term from the diagonal noise channel by measuring twirled calibration circuits that are initialized in the zero state.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0ee74f9f",
      "metadata": {},
      "source": [
        "You can enable both PEC and TREX by setting Estimator options. This tutorial uses the client-side implementations of the Qiskit SamplerV2 and EstimatorV2 interfaces, which were introduced in `qiskit-ibm-runtime` v0.50.0. These client-side primitives perform most of the pre- and post-processing locally and delegate execution to the Executor primitive. When the primitives run on the client side, you get greater transparency, faster feedback when you debug, and finer control.\n",
        "\n",
        "The client-side primitives implement the same API as the legacy server-side primitives. Therefore, you can usually switch to the client-side primitives by changing only the import statement.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "44bcd862",
      "metadata": {},
      "source": [
        "Enable INFO-level logging to see the progress of client-side pre- and post-processing.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "3c2091e3",
      "metadata": {},
      "outputs": [],
      "source": [
        "import logging\n",
        "\n",
        "logger = logging.getLogger(\"qiskit_ibm_runtime\")\n",
        "logger.setLevel(logging.INFO)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "35375639",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime.executor_estimator import Estimator\n",
        "\n",
        "estimator = Estimator(mode=backend)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "e3dcf355",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Enable TREX\n",
        "estimator.options.resilience.measure_mitigation = True\n",
        "# Enable PEC\n",
        "estimator.options.resilience.pec_mitigation = True"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bdf54725",
      "metadata": {},
      "source": [
        "### 3.3 Learn noise models and run the Cliffordized circuit\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6022f106",
      "metadata": {},
      "source": [
        "Both PEC and TREX require noise learning, which you can do with `NoiseLearnerV3`. The steps are as follows:\n",
        "\n",
        "1. Find the unique two-qubit gate layers and measurement layers in the circuit, for the Pauli-Lindblad and TREX protocols, respectively.\n",
        "2. Pass the layers to `NoiseLearnerV3` to learn the noise models.\n",
        "3. Convert the learned noise models to Pauli-Lindblad maps.\n",
        "\n",
        "Estimator does not use a pre-learned TREX noise model. Instead, it performs its own TREX learning. The following code learns both models anyway, because the noisy simulation uses the learned readout model.\n",
        "\n",
        "Refer to [Noise learning helper](/docs/ibm-quantum-compute/noise-learning) for more information about `NoiseLearnerV3`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f9c6502a",
      "metadata": {},
      "source": [
        "In addition to the noise-learning experiment, run the Cliffordized circuit on the QPU. This step is not always necessary, but it provides a comparison for the simulated results in Step 3.4.4. Run both in a batch to minimize how much the noise profile drifts between the two.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "e7e0fe67",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Number of unique layers found: 3\n"
          ]
        }
      ],
      "source": [
        "pubs = [(isa_circuit, isa_observables)]\n",
        "# Specify types=\"all\" to find layers for both Pauli-Lindblad and TREX learning\n",
        "layers = estimator.find_unique_layers(pubs, types=\"all\")\n",
        "print(f\"Number of unique layers found: {len(layers)}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "a50ebec0",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "runtime.program_run:INFO:2026-11-06 17:04:28,694: Posting the API request.\n",
            "runtime.program_run:INFO:2026-11-06 17:04:29,028: Payload size: 0 MB.\n",
            "estimator.run:INFO:2026-11-06 17:04:29,029: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:04:29,032: Processing pub 1/1 with MitigationTask.\n",
            "estimator.run:INFO:2026-11-06 17:04:29,124: Submitting 1 pub to executor with 4096 total shots\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "NoiseLearner job id: dbn4tr44148s7390as4g\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "runtime.program_run:INFO:2026-11-06 17:04:29,377: Posting the API request.\n",
            "runtime.program_run:INFO:2026-11-06 17:04:29,897: Payload size: 0 MB.\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Clifford job id: dbn4tr8ou4as73b3shng\n"
          ]
        }
      ],
      "source": [
        "from qiskit_ibm_runtime import NoiseLearnerV3\n",
        "\n",
        "with Batch(backend) as batch:\n",
        "    learner = NoiseLearnerV3(batch)\n",
        "    learner.options.num_randomizations = 64\n",
        "    learner.options.shots_per_randomization = 128\n",
        "    learner.options.environment.job_tags = [\n",
        "        \"noise-learning\",\n",
        "        f\"{len(layers)} layers\",\n",
        "    ]\n",
        "    learner_job = learner.run(layers)\n",
        "    print(f\"NoiseLearner job id: {learner_job.job_id()}\")\n",
        "\n",
        "    clifford_estimator = Estimator(batch)\n",
        "    clifford_estimator.options.environment.job_tags = [\n",
        "        \"cliffordized\",\n",
        "        \"unmitigated\",\n",
        "    ]\n",
        "    clifford_estimator.options.resilience.measure_mitigation = False\n",
        "    clifford_estimator.options.twirling.enable_measure = True\n",
        "    clifford_estimator.options.twirling.enable_gates = True\n",
        "\n",
        "    clifford_job = clifford_estimator.run(clifford_pubs)\n",
        "    print(f\"Clifford job id: {clifford_job.job_id()}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "9be504cc",
      "metadata": {},
      "outputs": [],
      "source": [
        "learner_result = learner_job.result()\n",
        "pauli_lindblad_maps = learner_result.to_pauli_lindblad_maps()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "a4d65b92",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:09:34,573: Post-processing pub 1/1 (MitigationTask).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Expectation values from unmitigated Clifford circuit:\n",
            "[0.74121094 0.70117188 0.68945312 0.64648438 0.62646484 0.60498047\n",
            " 0.63818359 0.61230469 0.65478516 0.65283203 0.65087891 0.62841797\n",
            " 0.64404297 0.62109375 0.64550781 0.63720703 0.68212891 0.70654297\n",
            " 0.75097656 0.77392578]\n"
          ]
        }
      ],
      "source": [
        "clifford_result = clifford_job.result()\n",
        "print(\">>> Expectation values from unmitigated Clifford circuit:\")\n",
        "print(clifford_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2c768a8e",
      "metadata": {},
      "source": [
        "### 3.4 Perform local simulation\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9c3cf257",
      "metadata": {},
      "source": [
        "With `qiskit-ibm-runtime` local mode, you can perform both noiseless and noisy simulations locally with minimal code changes. Use local mode to verify syntactic correctness, compare the effects of different mitigation methods, and check how well the learned noise model agrees with hardware results.\n",
        "\n",
        "Local mode uses largely the same interface as QPU execution. In most cases, you need to change only the backend that you pass when you initialize the primitive, and then rerun the same code. Each of the following subsections creates its own `Estimator` instance, so you can run them without changing a previous cell.\n",
        "\n",
        "Refer to [Local mode](/docs/ibm-quantum-compute/local-testing-mode) for more information.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3f44d16d",
      "metadata": {},
      "source": [
        "#### 3.4.1 Run a noiseless simulation\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 20,
      "id": "ce9484e9",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "estimator.run:INFO:2026-11-06 17:09:34,595: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:09:34,600: Processing pub 1/1 with MitigationTask.\n",
            "estimator.run:INFO:2026-11-06 17:09:34,670: Submitting 1 pub to executor with 4096 total shots\n",
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:09:36,491: Post-processing pub 1/1 (MitigationTask).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Ideal expectation values from simulation:\n",
            "[1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1. 1.]\n"
          ]
        }
      ],
      "source": [
        "from qiskit_aer import AerSimulator\n",
        "\n",
        "sim_estimator = Estimator(AerSimulator(method=\"stabilizer\"))\n",
        "sim_estimator.options.resilience_level = 0\n",
        "sim_estimator_job = sim_estimator.run(clifford_pubs)\n",
        "ideal_result = sim_estimator_job.result()\n",
        "print(\">>> Ideal expectation values from simulation:\")\n",
        "print(ideal_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6c15778d",
      "metadata": {},
      "source": [
        "#### 3.4.2 Run a noisy simulation with the learned Pauli-Lindblad model\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "80caf3ff",
      "metadata": {},
      "source": [
        "<Admonition type=\"caution\">\n",
        "  Do not use a fake backend or a Qiskit Aer noise model if you plan to use the learned Pauli-Lindblad noise model in your local simulation. If you do, the simulation applies the noise twice.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "2c51d5fa",
      "metadata": {},
      "outputs": [],
      "source": [
        "sim_estimator_noisy = Estimator(AerSimulator(method=\"stabilizer\"))\n",
        "sim_estimator_noisy.options.resilience_level = 0\n",
        "# Twirling is required for find_unique_layers to work\n",
        "sim_estimator_noisy.options.twirling.enable_gates = True\n",
        "sim_estimator_noisy.options.twirling.enable_measure = True\n",
        "clifford_layers = sim_estimator_noisy.find_unique_layers(\n",
        "    clifford_pubs, types=\"all\"\n",
        ")\n",
        "# Pass noise model for both 2Q and measurement layers for noisy simulation.\n",
        "sim_estimator_noisy.options.simulator.layer_noise_model = zip(\n",
        "    clifford_layers, pauli_lindblad_maps\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "ee74d5ed",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "estimator.run:INFO:2026-11-06 17:09:36,541: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:09:36,545: Processing pub 1/1 with MitigationTask.\n",
            "estimator.run:INFO:2026-11-06 17:09:36,640: Submitting 1 pub to executor with 4096 total shots\n",
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:09:38,405: Post-processing pub 1/1 (MitigationTask).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Unmitigated expectation values from simulation:\n",
            "[0.72607422 0.71826172 0.68457031 0.70019531 0.703125   0.73242188\n",
            " 0.69287109 0.66357422 0.64648438 0.65478516 0.63867188 0.69726562\n",
            " 0.68994141 0.72802734 0.72314453 0.74609375 0.73974609 0.76123047\n",
            " 0.77148438 0.78173828]\n"
          ]
        }
      ],
      "source": [
        "sim_estimator_noisy_job = sim_estimator_noisy.run(clifford_pubs)\n",
        "sim_noisy_result = sim_estimator_noisy_job.result()\n",
        "print(\">>> Unmitigated expectation values from simulation:\")\n",
        "print(sim_noisy_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bd2a24b7",
      "metadata": {},
      "source": [
        "#### 3.4.3 Run a noisy simulation with PEC and TREX\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "id": "b227725b",
      "metadata": {},
      "outputs": [],
      "source": [
        "sim_estimator_mitigated = Estimator(AerSimulator(method=\"stabilizer\"))\n",
        "\n",
        "# Twirling is required for find_unique_layers to work\n",
        "sim_estimator_mitigated.options.twirling.enable_gates = True\n",
        "sim_estimator_mitigated.options.twirling.enable_measure = True\n",
        "\n",
        "# Enable PEC & TREX\n",
        "sim_estimator_mitigated.options.resilience.pec_mitigation = True\n",
        "sim_estimator_mitigated.options.resilience.measure_mitigation = True\n",
        "\n",
        "clifford_layers = sim_estimator_mitigated.find_unique_layers(\n",
        "    clifford_pubs, types=\"all\"\n",
        ")\n",
        "\n",
        "# This sets the noise model\n",
        "sim_estimator_mitigated.options.simulator.layer_noise_model = zip(\n",
        "    clifford_layers, pauli_lindblad_maps\n",
        ")\n",
        "# This tells PEC which noise to cancel\n",
        "sim_estimator_mitigated.options.resilience.layer_noise_model = zip(\n",
        "    clifford_layers, pauli_lindblad_maps\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "id": "181bcadd",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "estimator.run:INFO:2026-11-06 17:09:38,460: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:09:38,463: Processing pub 1/1 with PEC.\n",
            "trex.apply_trex:INFO:2026-11-06 17:09:38,586: TREX calibration circuit added (64 randomizations).\n",
            "estimator.run:INFO:2026-11-06 17:09:38,586: Submitting 2 pubs to executor with 138176 total shots\n",
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:10:44,581: Post-processing pub 1/1 (PEC).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Mitigated expectation values from simulation:\n",
            "[1.01157385 1.01426832 1.08479389 1.08083535 0.96896198 0.96465857\n",
            " 0.97870353 0.95113655 1.03992974 1.01440225 0.94385996 1.01909196\n",
            " 0.99009273 1.09171259 1.00659746 1.04646524 1.02475636 1.15051973\n",
            " 1.03481948 1.0941715 ]\n"
          ]
        }
      ],
      "source": [
        "sim_estimator_mitigated_job = sim_estimator_mitigated.run(clifford_pubs)\n",
        "sim_mitigated_result = sim_estimator_mitigated_job.result()\n",
        "print(\">>> Mitigated expectation values from simulation:\")\n",
        "print(sim_mitigated_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9fbe3cdb",
      "metadata": {},
      "source": [
        "#### 3.4.4 Compare the simulation results\n",
        "\n",
        "{/* cspell:ignore frameon sharey */}\n",
        "\n",
        "Plot the expectation value of each single-qubit $Z$ observable from the three simulations, together with the unmitigated QPU result for the same Clifford circuit from Step 3.3. Because the circuit is a mirror circuit, every ideal value is 1. The error bars show the standard error that Estimator reports for each value.\n",
        "\n",
        "The closer the unmitigated simulation is to the unmitigated QPU result, the better the learned noise model describes the hardware. Use this comparison as a check before you spend QPU time on the mitigated experiment. If the two results disagree significantly, PEC is also unlikely to get close to the ideal values on hardware. In that case, consider relearning the noise model or selecting a different layout. You can also relearn the noise model with different `NoiseLearnerV3` options, such as more randomizations or additional layer-pair depths, and check whether the agreement improves. For example, in Step 3.3 this tutorial sets `num_randomizations` to 64 instead of the default value of 32, because the additional randomizations improved the agreement for this circuit.\n",
        "\n",
        "`NoiseLearnerV3` runs a standard learning protocol. For full control over the learning experiment, you can build your own with [Qiskit noise learning](/docs/addons/qiskit-noise-learning/guides/workflow).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "id": "2e4cb660",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/profiling-tools/extracted-outputs/2e4cb660-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "qubit_indices = np.arange(num_qubits)\n",
        "results_to_plot = {\n",
        "    \"Unmitigated QPU (Clifford circuit)\": (clifford_result, \"#4a3aa7\", \"^\"),\n",
        "    \"Unmitigated simulation (learned noise model)\": (\n",
        "        sim_noisy_result,\n",
        "        \"#2a78d6\",\n",
        "        \"o\",\n",
        "    ),\n",
        "    \"Mitigated simulation (PEC + TREX)\": (\n",
        "        sim_mitigated_result,\n",
        "        \"#eb6834\",\n",
        "        \"s\",\n",
        "    ),\n",
        "}\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(9, 4.5))\n",
        "ax.plot(\n",
        "    qubit_indices,\n",
        "    ideal_result[0].data.evs,\n",
        "    color=\"#52514e\",\n",
        "    linestyle=\"--\",\n",
        "    linewidth=1.5,\n",
        "    label=\"Ideal (noiseless simulation)\",\n",
        ")\n",
        "for label, (result, color, marker) in results_to_plot.items():\n",
        "    ax.errorbar(\n",
        "        qubit_indices,\n",
        "        result[0].data.evs,\n",
        "        yerr=result[0].data.stds,\n",
        "        color=color,\n",
        "        marker=marker,\n",
        "        markersize=6,\n",
        "        linewidth=2,\n",
        "        capsize=3,\n",
        "        label=label,\n",
        "    )\n",
        "\n",
        "ax.set_xlabel(\"Qubit index of the $Z$ observable\")\n",
        "ax.set_ylabel(\"Expectation value\")\n",
        "ax.set_title(\"Local simulation compared with the QPU\")\n",
        "ax.set_xticks(qubit_indices)\n",
        "ax.grid(axis=\"y\", alpha=0.3)\n",
        "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
        "# Place the legend below the plot so it doesn't cover any data\n",
        "ax.legend(\n",
        "    frameon=False, loc=\"upper center\", bbox_to_anchor=(0.5, -0.15), ncol=2\n",
        ")\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "21469120",
      "metadata": {},
      "source": [
        "### 3.5 Estimate usage\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "22d48971",
      "metadata": {},
      "source": [
        "The main purpose of dry-run mode is to estimate usage. Unlike local mode, dry-run mode submits the job to IBM Quantum Compute Service, which returns an accurate usage estimate but **random result data**. Dry-run jobs do not run on real QPUs and do not consume any QPU time.\n",
        "\n",
        "Dry-run jobs are not available on the Open or Pay-As-You-Go plans.\n",
        "\n",
        "Refer to [Dry-run jobs](/docs/ibm-quantum-compute/dry-run-jobs) for more information.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "id": "03ac0928",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Usage is 42 seconds\n"
          ]
        }
      ],
      "source": [
        "import time\n",
        "\n",
        "dry_run_estimator = Estimator(mode=backend)\n",
        "dry_run_estimator.options.resilience.measure_mitigation = True\n",
        "dry_run_estimator.options.resilience.pec_mitigation = True\n",
        "dry_run_estimator.options.resilience.layer_noise_model = zip(\n",
        "    layers, pauli_lindblad_maps\n",
        ")\n",
        "\n",
        "dry_run_job = dry_run_estimator.run(\n",
        "    [(isa_circuit, isa_observables)], dry_run=True\n",
        ")\n",
        "# Wait for the job to finish to get an accurate estimate\n",
        "dry_run_job.result()\n",
        "# Sometimes it takes a second for usage to get updated\n",
        "time.sleep(1)\n",
        "print(f\">>> Usage is {dry_run_job.usage()} seconds\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b1bcdca3",
      "metadata": {},
      "source": [
        "## Step 4: Run on hardware\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "15967604",
      "metadata": {},
      "source": [
        "### 4.1 Initialize a session and run calibration\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "14aa2089",
      "metadata": {},
      "source": [
        "QPU calibration optimizes many parameters that define the electrical signals that drive the quantum gates and operations. Calibration jobs run throughout the day between user jobs, but not during an active session. These parameters can drift within a short period of time. For the best possible results, consider triggering calibration at the start of a session, or periodically during a long-running session.\n",
        "\n",
        "Currently, only readout calibration is supported, and it takes approximately five minutes of wall-clock time.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "bbfd1bc7",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime import Session, Batch, Calibrator\n",
        "\n",
        "# Set to False to skip the session and calibration and run in batch mode instead\n",
        "run_calibration = False\n",
        "mode = Session(backend) if run_calibration else Batch(backend)\n",
        "\n",
        "if run_calibration:\n",
        "    calibrator = Calibrator(mode)\n",
        "    # Wait for calibration to finish before submitting the experiment.\n",
        "    calibrator.run().result()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7c2c8029",
      "metadata": {},
      "source": [
        "### 4.2 Run on hardware with Estimator\n",
        "\n",
        "The following cells run three jobs in the batch or session:\n",
        "\n",
        "* A noise-learning experiment to refresh the noise model. This experiment is useful if you expect the noise model from Step 3.3 to be outdated when the Estimator job runs. In a batch, the Estimator jobs can run much later than the noise-learning job, because jobs from other users can run on the QPU between them. A session gives your workload exclusive access to the QPU, so the noise model is more likely to still be accurate when the experiment runs.\n",
        "* The experiment with mitigation.\n",
        "* The experiment without mitigation, for comparison.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 28,
      "id": "3ed602f7",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "NoiseLearner job id: dbn50pgqtr0c73accf70\n"
          ]
        }
      ],
      "source": [
        "learner = NoiseLearnerV3(mode)\n",
        "learner.options.num_randomizations = 64\n",
        "learner.options.shots_per_randomization = 128\n",
        "learner_job = learner.run(layers)\n",
        "print(f\"NoiseLearner job id: {learner_job.job_id()}\")\n",
        "learner_result = learner_job.result()\n",
        "pauli_lindblad_maps = learner_result.to_pauli_lindblad_maps()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 29,
      "id": "a671794e",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "estimator.run:INFO:2026-11-06 17:26:17,386: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:26:17,398: Processing pub 1/1 with PEC.\n",
            "trex.apply_trex:INFO:2026-11-06 17:26:17,534: TREX calibration circuit added (64 randomizations).\n",
            "estimator.run:INFO:2026-11-06 17:26:17,534: Submitting 2 pubs to executor with 133696 total shots\n",
            "runtime.program_run:INFO:2026-11-06 17:26:17,765: Posting the API request.\n",
            "runtime.program_run:INFO:2026-11-06 17:26:18,214: Payload size: 0 MB.\n",
            "estimator.run:INFO:2026-11-06 17:26:18,215: Starting pre-processing\n",
            "prepare._build_quantum_program:INFO:2026-11-06 17:26:18,219: Processing pub 1/1 with MitigationTask.\n",
            "estimator.run:INFO:2026-11-06 17:26:18,320: Submitting 1 pub to executor with 4096 total shots\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Mitigated job id: dbn582lt7fqc73aiv1t0\n"
          ]
        },
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "runtime.program_run:INFO:2026-11-06 17:26:18,549: Posting the API request.\n",
            "runtime.program_run:INFO:2026-11-06 17:26:18,882: Payload size: 0 MB.\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Unmitigated job id: dbn582gou4as73b3t2i0\n"
          ]
        }
      ],
      "source": [
        "mitigated_estimator = Estimator(mode)\n",
        "mitigated_estimator.options.environment.job_tags = [\"pec\", \"trex\"]\n",
        "mitigated_estimator.options.resilience.measure_mitigation = True\n",
        "mitigated_estimator.options.resilience.pec_mitigation = True\n",
        "# Pass the learned noise model for PEC.\n",
        "# PEC does not apply mitigation to the measurement layer, so it ignores that layer.\n",
        "mitigated_estimator.options.resilience.layer_noise_model = zip(\n",
        "    layers, pauli_lindblad_maps\n",
        ")\n",
        "\n",
        "mitigated_job = mitigated_estimator.run([(isa_circuit, isa_observables)])\n",
        "print(f\">>> Mitigated job id: {mitigated_job.job_id()}\")\n",
        "\n",
        "unmitigated_estimator = Estimator(mode)\n",
        "unmitigated_estimator.options.resilience_level = 0\n",
        "unmitigated_estimator.options.twirling.enable_gates = True\n",
        "unmitigated_estimator.options.twirling.enable_measure = True\n",
        "unmitigated_estimator.options.environment.job_tags = [\"unmitigated\"]\n",
        "unmitigated_job = unmitigated_estimator.run([(isa_circuit, isa_observables)])\n",
        "print(f\">>> Unmitigated job id: {unmitigated_job.job_id()}\")\n",
        "\n",
        "# Close the session or batch if you are using one\n",
        "mode.close()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 30,
      "id": "2366b9ad",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:27:05,288: Post-processing pub 1/1 (PEC).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Expectation values from mitigated QPU run:\n",
            "[1.12575542 1.10585523 1.04760069 1.01521185 0.96232448 1.02410608\n",
            " 1.05707854 0.95455801 0.92802096 0.95614542 0.99906656 1.0678834\n",
            " 1.03981228 1.00530725 1.04935027 1.03028844 1.04582987 1.07583472\n",
            " 1.03438868 1.07511027]\n"
          ]
        }
      ],
      "source": [
        "mitigated_result = mitigated_job.result()\n",
        "print(\">>> Expectation values from mitigated QPU run:\")\n",
        "print(mitigated_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 31,
      "id": "042c5d9c",
      "metadata": {},
      "outputs": [
        {
          "name": "stderr",
          "output_type": "stream",
          "text": [
            "post_processor_v0_1.estimator_v2_post_processor_v0_1:INFO:2026-11-06 17:27:05,567: Post-processing pub 1/1 (MitigationTask).\n"
          ]
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            ">>> Expectation values from unmitigated QPU run:\n",
            "[0.88183594 0.80810547 0.77197266 0.74560547 0.74316406 0.76074219\n",
            " 0.76123047 0.77685547 0.79736328 0.8046875  0.78564453 0.80761719\n",
            " 0.83056641 0.77099609 0.79492188 0.77880859 0.80126953 0.81445312\n",
            " 0.82226562 0.90576172]\n"
          ]
        }
      ],
      "source": [
        "unmitigated_result = unmitigated_job.result()\n",
        "print(\">>> Expectation values from unmitigated QPU run:\")\n",
        "print(unmitigated_result[0].data.evs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "074e0a5c",
      "metadata": {},
      "source": [
        "Plot the mitigated and unmitigated hardware results together with the ideal values. Because the circuit is a mirror circuit, every ideal value is 1, so the distance between each point and the dashed line shows the remaining error. Any remaining deviation from the ideal value, whether above or below it, reflects the statistical uncertainty of PEC and the limits of the learned noise model described in Step 3.2.\n",
        "\n",
        "To further improve the results, you can try the following:\n",
        "\n",
        "* Increase the number of shots or randomizations to reduce the statistical uncertainty of the mitigated estimates. The cost grows with the PEC sampling overhead.\n",
        "* Relearn the noise model immediately before the experiment, in the same session, to reduce the effect of noise drift.\n",
        "* Learn the noise model more precisely by increasing the `num_randomizations`, `shots_per_randomization`, or `layer_pair_depths` options of `NoiseLearnerV3`. Errors in the learned rates bias the mitigated results, and additional PEC shots do not remove this bias.\n",
        "* Add [non-Markovian error checks](/docs/addons/qiskit-mitigation/guides/postselection-with-non-markovian-error-checks) to detect shots that are likely corrupted by non-Markovian errors, which the learned noise model does not describe. Then discard those shots by postselection. Apply the same checks when you learn the noise model, by using the `bit_flip_checks` options of `NoiseLearnerV3`. Otherwise, the learned model includes errors that postselection removes from the experiment, and PEC overcompensates for them. Stricter postselection discards more shots, so balance the accuracy gain against the number of shots that you keep.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 32,
      "id": "3cd8d65c",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/profiling-tools/extracted-outputs/3cd8d65c-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "qubit_indices = np.arange(num_qubits)\n",
        "# The mirror circuit is an identity circuit, so every ideal value is 1\n",
        "ideal_evs = np.ones(num_qubits)\n",
        "results_to_plot = {\n",
        "    \"Unmitigated QPU\": (unmitigated_result, \"#4a3aa7\", \"^\"),\n",
        "    \"Mitigated QPU (PEC + TREX)\": (mitigated_result, \"#eb6834\", \"s\"),\n",
        "}\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(9, 4.5))\n",
        "ax.plot(\n",
        "    qubit_indices,\n",
        "    ideal_evs,\n",
        "    color=\"#52514e\",\n",
        "    linestyle=\"--\",\n",
        "    linewidth=1.5,\n",
        "    label=\"Ideal\",\n",
        ")\n",
        "for label, (result, color, marker) in results_to_plot.items():\n",
        "    ax.errorbar(\n",
        "        qubit_indices,\n",
        "        result[0].data.evs,\n",
        "        yerr=result[0].data.stds,\n",
        "        color=color,\n",
        "        marker=marker,\n",
        "        markersize=6,\n",
        "        linewidth=2,\n",
        "        capsize=3,\n",
        "        label=label,\n",
        "    )\n",
        "\n",
        "ax.set_xlabel(\"Qubit index of the $Z$ observable\")\n",
        "ax.set_ylabel(\"Expectation value\")\n",
        "ax.set_title(\"Hardware results with and without error mitigation\")\n",
        "ax.set_xticks(qubit_indices)\n",
        "ax.grid(axis=\"y\", alpha=0.3)\n",
        "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
        "# Place the legend below the plot so it doesn't cover any data\n",
        "ax.legend(\n",
        "    frameon=False, loc=\"upper center\", bbox_to_anchor=(0.5, -0.15), ncol=3\n",
        ")\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ce8e68ba",
      "metadata": {},
      "source": [
        "### 4.3 Compare hardware and simulation results\n",
        "\n",
        "In Step 3.4, you used local simulation to preview how much PEC and TREX might improve the results. Compare that preview with the QPU results. If the simulated improvement and the hardware improvement are similar, local simulation is a useful way to evaluate mitigation settings for this workload before you spend QPU time on them.\n",
        "\n",
        "The comparison is approximate, for two reasons:\n",
        "\n",
        "* The simulations run the Cliffordized circuit from Step 3.1, while the hardware jobs run the original circuit.\n",
        "* The simulations use the noise model learned in Step 3.3, while the mitigated hardware job uses the noise model that was refreshed in Step 4.2.\n",
        "\n",
        "For a direct check of the noise model itself, use the comparison of the Clifford circuit results in Step 3.4.4.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 33,
      "id": "3b67439e",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/profiling-tools/extracted-outputs/3b67439e-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Local simulation (Clifford circuit)  Unmitigated              mean absolute deviation: 0.290\n",
            "Local simulation (Clifford circuit)  Mitigated (PEC + TREX)   mean absolute deviation: 0.046\n",
            "QPU (original circuit)               Unmitigated              mean absolute deviation: 0.202\n",
            "QPU (original circuit)               Mitigated (PEC + TREX)   mean absolute deviation: 0.050\n"
          ]
        }
      ],
      "source": [
        "fig, axes = plt.subplots(1, 2, figsize=(12, 4.5), sharey=True)\n",
        "panels = {\n",
        "    \"Local simulation (Clifford circuit)\": {\n",
        "        \"Unmitigated\": (sim_noisy_result, \"#2a78d6\", \"o\"),\n",
        "        \"Mitigated (PEC + TREX)\": (sim_mitigated_result, \"#eb6834\", \"s\"),\n",
        "    },\n",
        "    \"QPU (original circuit)\": {\n",
        "        \"Unmitigated\": (unmitigated_result, \"#4a3aa7\", \"^\"),\n",
        "        \"Mitigated (PEC + TREX)\": (mitigated_result, \"#eb6834\", \"s\"),\n",
        "    },\n",
        "}\n",
        "\n",
        "for ax, (title, results_to_plot) in zip(axes, panels.items()):\n",
        "    ax.axhline(\n",
        "        1, color=\"#52514e\", linestyle=\"--\", linewidth=1.5, label=\"Ideal\"\n",
        "    )\n",
        "    for label, (result, color, marker) in results_to_plot.items():\n",
        "        ax.errorbar(\n",
        "            qubit_indices,\n",
        "            result[0].data.evs,\n",
        "            yerr=result[0].data.stds,\n",
        "            color=color,\n",
        "            marker=marker,\n",
        "            markersize=5,\n",
        "            linewidth=2,\n",
        "            capsize=3,\n",
        "            label=label,\n",
        "        )\n",
        "    ax.set_title(title)\n",
        "    ax.set_xlabel(\"Qubit index of the $Z$ observable\")\n",
        "    ax.set_xticks(qubit_indices[::2])\n",
        "    ax.grid(axis=\"y\", alpha=0.3)\n",
        "    ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
        "    # Place the legend below the plot so it doesn't cover any data\n",
        "    ax.legend(\n",
        "        frameon=False, loc=\"upper center\", bbox_to_anchor=(0.5, -0.15), ncol=3\n",
        "    )\n",
        "\n",
        "axes[0].set_ylabel(\"Expectation value\")\n",
        "plt.tight_layout()\n",
        "plt.show()\n",
        "\n",
        "# Summarize each result by its mean absolute deviation from the ideal value of 1\n",
        "for title, results_to_plot in panels.items():\n",
        "    for label, (result, _, _) in results_to_plot.items():\n",
        "        error = np.mean(np.abs(result[0].data.evs - 1))\n",
        "        print(f\"{title:<36} {label:<24} mean absolute deviation: {error:.3f}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e19a6621",
      "metadata": {},
      "source": [
        "In this run, mitigation reduced the mean absolute deviation from 0.290 to 0.046 in the simulation (about a factor of six) and from 0.202 to 0.050 on the QPU (about a factor of four). In both cases, the mitigated values scatter around 1, and most of them agree with 1 within their error bars.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c9cfe125",
      "metadata": {},
      "source": [
        "### 4.4 Inspect execution spans\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "07074a46",
      "metadata": {},
      "source": [
        "When you submit circuits with multiple configurations, such as with gate twirling, IBM Quantum Compute Service does not necessarily run all circuit configurations at the same time. Instead, it groups them into chunks and runs one chunk per shot loop. A shot loop is a single round of execution on the quantum hardware, in which every circuit in the chunk is run for the requested number of shots.\n",
        "\n",
        "All jobs that are submitted to a real QPU require some classical pre-processing, such as translation into pulses that can be sent to the hardware. Because this pre-processing can be done in parallel, the service dequeues and starts multiple jobs at a time. The QPU, however, can process only one unit of work at a time. Therefore, if your job generates multiple chunks, they might be interleaved with the chunks of other users' jobs, and each chunk might have to wait for its turn on the QPU.\n",
        "\n",
        "The job result metadata reports the QPU execution timing for each chunk. The following visualization shows whether there are significant gaps between the executions of the chunks in your job. If the gaps are wider than your experiment can tolerate, you might want to rerun the job, or use [session execution mode](/docs/ibm-quantum-compute/execution-modes#session-mode), which gives your workload exclusive access to the QPU.\n",
        "\n",
        "Refer to [View execution spans](/docs/ibm-quantum-compute/sampler-input-output#view-execution-spans) for more information.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 34,
      "id": "84cd3a8b",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "2 chunks were executed.\n",
            "The first chunk contains 2 parts.\n",
            "    Part 0 contains 881 configurations from circuit 0\n",
            "    Part 1 contains 30 configurations from circuit 1\n"
          ]
        }
      ],
      "source": [
        "chunk_timing = mitigated_result.metadata[\"executor\"].chunk_timing\n",
        "\n",
        "print(f\"{len(chunk_timing)} chunks were executed.\")\n",
        "chunk_1 = chunk_timing[0]\n",
        "print(f\"The first chunk contains {len(chunk_1.parts)} parts.\")\n",
        "for idx, part in enumerate(chunk_1.parts):\n",
        "    print(\n",
        "        f\"    Part {idx} contains {part.size} configurations from circuit {part.idx_item}\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 35,
      "id": "e328d0ac",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime.visualization import draw_chunk_timings\n",
        "\n",
        "draw_chunk_timings(chunk_timing)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3910b0f7",
      "metadata": {},
      "source": [
        "![Chunk execution timeline for the mitigated job, showing two chunks and the gap between them](https://eu-de.quantum.cloud.ibm.com/docs/images/tutorials/profiling-tools/chunk-timing.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f8469e9c",
      "metadata": {},
      "source": [
        "## Next steps\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommendations\">\n",
        "  * Rerun the experiment with and without the `slow_gates_ac` calibration (Step 2.1), the refreshed device properties (Step 2.2), or session calibration (Step 4.1) to measure how much each tool improves the results for your workload.\n",
        "  * Try a cheaper error mitigation method such as zero-noise extrapolation (ZNE), and use local simulation and dry-run usage estimates to compare its cost and accuracy with those of PEC. The [`select-error-mitigation`](https://github.com/Qiskit/skills) AI agent skill can help you make this choice.\n",
        "  * Explore the full characterization suite, including randomized benchmarking, in the [Refresh backend properties with real-time benchmarking](/docs/tutorials/refresh-backend-properties-with-real-time-benchmarking) tutorial.\n",
        "  * Learn more about the client-side primitives and the Executor in [Migrate from server-side to client-side Sampler and Estimator](/docs/guides/migrate-to-client-side-primitives).\n",
        "  * Apply these tools to your own experiments. Start with the tools that address your largest source of uncertainty, such as device quality, noise, cost, or execution timing.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}