diff --git a/docs/tutorials/_toc.json b/docs/tutorials/_toc.json index c9694142e9d..5a1865c7743 100644 --- a/docs/tutorials/_toc.json +++ b/docs/tutorials/_toc.json @@ -107,7 +107,7 @@ "url": "/docs/tutorials/ai-transpiler-introduction" }, { - "title": "Transpilation optimizations with SABRE", + "title": "Transpilation optimization with SABRE", "url": "/docs/tutorials/transpilation-optimizations-with-sabre" }, { diff --git a/docs/tutorials/index.mdx b/docs/tutorials/index.mdx index 1f81f0a0aeb..3130c874123 100644 --- a/docs/tutorials/index.mdx +++ b/docs/tutorials/index.mdx @@ -85,7 +85,7 @@ Workload optimization focuses on either efficient orchestration of classical and * [Qiskit AI-powered transpiler service introduction](/docs/tutorials/ai-transpiler-introduction) -* [Transpilation optimizations with SABRE](/docs/tutorials/transpilation-optimizations-with-sabre) +* [Transpilation optimization with SABRE](/docs/tutorials/transpilation-optimizations-with-sabre) * [Compilation methods for Hamiltonian simulation circuits](/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits) diff --git a/docs/tutorials/transpilation-optimizations-with-sabre.ipynb b/docs/tutorials/transpilation-optimizations-with-sabre.ipynb index 25b9622640a..62ff351c888 100644 --- a/docs/tutorials/transpilation-optimizations-with-sabre.ipynb +++ b/docs/tutorials/transpilation-optimizations-with-sabre.ipynb @@ -2,63 +2,63 @@ "cells": [ { "cell_type": "markdown", - "id": "316f2558-4976-4bc2-b346-a66256f622a6", + "id": "1ed79d34-1831-47cb-9985-5bb7dacc70e0", "metadata": {}, "source": [ "---\n", - "title: Transpilation optimizations with SABRE\n", - "description: SABRE is an optimization tool for layout and routing. It is especially effective with large-scale circuits and complex coupling maps.\n", + "title: Transpilation optimization with SABRE\n", + "description: Optimize quantum circuits using SABRE layout and routing for large-scale hardware execution.\n", "---\n", "\n", "\n", - "{/* cspell:ignore ylabel xlabel fontsize sharex edgecolor */}\n", + "{/* cspell:ignore ylabel xlabel fontsize sharex edgecolor, fontweight, elinewidth, ecolor, prerouter, ylim */}\n", "\n", - "# Transpilation optimizations with SABRE\n", - "*Usage estimate: under one minute on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + "# Transpilation optimization with SABRE\n", + "*Usage estimate: 1 minute on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" ] }, { "cell_type": "markdown", - "id": "e10fe088-c914-4c6b-be7a-d83dc6166245", + "id": "d6834016-1525-42d7-aa21-0fef9d957ecd", "metadata": {}, "source": [ - "## Background\n", - "Transpilation is a critical step in Qiskit that converts quantum circuits into forms compatible with specific quantum hardware. It involves two key stages: **qubit layout** (mapping logical qubits to physical qubits on the device) and **gate routing** (ensuring multi-qubit gates respect device connectivity by inserting SWAP gates as needed).\n", - "\n", - "SABRE (*SWAP-Based Bidirectional heuristic search algorithm*) is a powerful optimization tool for both layout and routing. It is especially effective for **large-scale circuits** (100+ qubits) and devices with complex coupling maps, like the **IBM® Heron**, where the exponential growth in possible qubit mappings demands efficient solutions.\n", + "## Learning outcomes\n", + "After going through this tutorial, you should understand:\n", + "- How to configure SABRE parameters (`layout_trials`, `swap_trials`, `max_iterations`) to improve transpilation quality\n", + "- The trade-offs between transpilation runtime and circuit quality (depth and gate count)\n", + "- How to customize the SABRE routing heuristic (`basic`, `decay`, `lookahead`) and compare their performance on hardware\n", + "\n", + "## Prerequisites\n", + "We suggest that you are familiar with the following topics before going through this tutorial:\n", + "- [Transpile circuits](/docs/guides/transpile): overview of transpilation in Qiskit\n", + "- [Transpiler stages](/docs/guides/transpiler-stages): layout and routing stages\n", + "- [Configure preset pass managers](/docs/guides/transpile-with-pass-managers): customizing optimization levels\n", "\n", - "### Why use SABRE?\n", - "\n", - "SABRE minimizes the number of SWAP gates and reduces circuit depth, improving circuit performance on real hardware. Its heuristic-based approach makes it ideal for advanced hardware and large, complex circuits. Recent improvements introduced in the [LightSABRE](https://arxiv.org/abs/2409.08368) algorithm further optimize SABRE’s performance, offering faster runtimes and fewer SWAP gates. These enhancements make it even more effective for large-scale circuits.\n", + "## Background\n", "\n", - "### What you’ll learn\n", + "Transpilation converts quantum circuits into forms compatible with specific quantum hardware. Two key stages are choosing a **qubit layout** (mapping logical qubits to physical qubits) and **gate routing** (inserting SWAP gates so multi-qubit gates respect device connectivity).\n", "\n", - "This tutorial is divided into two parts:\n", - "1. Learn to use SABRE with **Qiskit patterns** for advanced optimization of large circuits.\n", - "2. Leverage **qiskit_serverless** to maximize SABRE’s potential for scalable and efficient transpilation.\n", + "**SABRE** (*SWAP-Based Bidirectional heuristic search algorithm*) optimizes both layout and routing. It is especially effective for large-scale circuits (100+ qubits) on devices with complex coupling maps, like IBM® Heron processors. SABRE minimizes SWAP gates and reduces circuit depth, improving execution fidelity. Recent improvements in the [LightSABRE](https://arxiv.org/abs/2409.08368) algorithm further reduce runtimes and gate counts.\n", "\n", - "You will:\n", - "- Optimize SABRE for circuits with 100+ qubits, surpassing default transpilation settings like `optimization_level=3`.\n", - "- Explore **LightSABRE enhancements** that improve runtime and reduce gate counts.\n", - "- Customize key SABRE parameters (`swap_trials`, `layout_trials`, `max_iterations`, `heuristic`) to balance **circuit quality** and **transpilation runtime**." + "In this tutorial, you will first configure `SabreLayout` with different parameters to optimize a small GHZ circuit and observe the impact on execution fidelity. Then, you will compare SABRE's routing heuristics at scale on real hardware." ] }, { "cell_type": "markdown", - "id": "b6099f09-341e-4895-9d38-48538836e155", + "id": "50cf9d9f-c875-49d5-83b0-c691363615ab", "metadata": {}, "source": [ "## Requirements\n", "\n", "Before starting this tutorial, be sure you have the following installed:\n", - "- Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.28 or later (`pip install qiskit-ibm-runtime`)\n", - "- Serverless (`pip install qiskit-ibm-catalog qiskit_serverless`)" + "- Qiskit SDK v2.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.22 or later (`pip install qiskit-ibm-runtime`)\n", + "- Qiskit Aer (`pip install qiskit-aer`)" ] }, { "cell_type": "markdown", - "id": "131313f1-cad4-4b80-b60d-981ece02295a", + "id": "6d9ad2c3-1b10-4a22-8b75-608869be0ac7", "metadata": {}, "source": [ "## Setup" @@ -66,221 +66,198 @@ }, { "cell_type": "code", - "execution_count": 1, - "id": "208d379b-10ee-4d7c-9062-35b15fd18d94", + "execution_count": null, + "id": "6d93c123-b92e-434d-8361-8115509e6d5f", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using backend: ibm_kingston\n" + ] + } + ], "source": [ "from qiskit import QuantumCircuit\n", "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit_ibm_catalog import QiskitServerless, QiskitFunction\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", "from qiskit_ibm_runtime import EstimatorOptions\n", "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "from qiskit.transpiler import CouplingMap\n", - "from qiskit.transpiler.passes import SabreLayout, SabreSwap\n", + "from qiskit_aer.primitives import EstimatorV2 as AerEstimator\n", + "from qiskit.transpiler.passes import (\n", + " SabreLayout,\n", + " SabreSwap,\n", + " BarrierBeforeFinalMeasurements,\n", + " StarPreRouting,\n", + ")\n", + "from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason\n", "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.passmanager.flow_controllers import ConditionalController\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", - "import time" + "import time\n", + "\n", + "seed = 42\n", + "\n", + "service = QiskitRuntimeService(\n", + " channel=\"ibm_cloud\",\n", + " token=\"\", # Replace with your actual API token\n", + " instance=\"\", # Replace with your instance name if needed\n", + ")\n", + "backend = service.least_busy(operational=True, simulator=False)\n", + "\n", + "\n", + "print(f\"Using backend: {backend.name}\")" ] }, { "cell_type": "markdown", - "id": "c2ac52a8-697e-49bb-9780-fa30213c44fe", + "id": "c34842c6-5e1b-4658-8e91-15149280c783", "metadata": {}, "source": [ - "## Part I. Using SABRE with Qiskit patterns\n", + "## Small-scale simulator example\n", "\n", - "SABRE can be used in Qiskit to optimize quantum circuits by handling both the qubit layout and gate routing stages. In this section, we’ll guide you through the **minimal example** of using SABRE with Qiskit patterns, with the primary focus on step 2 of optimization.\n", - "\n", - "To run SABRE, you need:\n", - "- A **DAG** (Directed Acyclic Graph) representation of your quantum circuit.\n", - "- The **coupling map** from the backend, which specifies how qubits are physically connected.\n", - "- The **SABRE pass**, which applies the algorithm to optimize the layout and routing.\n", - "\n", - "For this part, we’ll focus on the **SabreLayout** pass. It performs both layout and routing trials, working to find the most efficient initial layout while minimizing the number of SWAP gates needed. Importantly, `SabreLayout`, just by itself, internally optimizes both the layout and routing by storing the solution that adds the least number of SWAP gates. Note that when using just **SabreLayout**, we cannot change the heuristic of SABRE, but we are able to customize the number of `layout_trials`." + "In this section, a **noisy simulator** based on the real backend's noise model is used to demonstrate how different `SabreLayout` configurations affect both transpilation quality and execution fidelity. Using `qiskit_aer` with a noise model derived from actual hardware calibration data allows you to test the transpilation without consuming hardware credits." ] }, { - "attachments": {}, "cell_type": "markdown", - "id": "888f597b-cce4-48df-b75c-773170260f24", + "id": "a2bcc2b2-727b-447f-b680-a23e3b9e0c3c", "metadata": {}, "source": [ "### Step 1: Map classical inputs to a quantum problem\n", "\n", - "A **GHZ (Greenberger-Horne-Zeilinger)** circuit is a quantum circuit that prepares an entangled state where all qubits are either in the `|0...0⟩` or `|1...1⟩` state. The GHZ state for $n$ qubits is mathematically represented as:\n", - "$$ |\\text{GHZ}\\rangle = \\frac{1}{\\sqrt{2}} \\left( |0\\rangle^{\\otimes n} + |1\\rangle^{\\otimes n} \\right) $$\n", - "\n", - "It is constructed by applying:\n", - "1. A Hadamard gate to the first qubit to create superposition.\n", - "2. A series of CNOT gates to entangle the remaining qubits with the first.\n", - "\n", - "For this example, we intentionally construct a **star-topology GHZ circuit** instead of a linear-topology one. In the star topology, the first qubit acts as the \"hub,\" and all other qubits are entangled directly with it using CNOT gates. This choice is deliberate because, while the **linear topology GHZ state** can theoretically be implemented in $ O(N) $ depth on a linear coupling map without any SWAP gates, SABRE would trivially find an optimal solution by mapping a 100-qubit GHZ circuit to a subgraph of the backend's heavy-hex coupling map.\n", + "We construct a **star-topology GHZ circuit** with 15 qubits. The first qubit is the hub, with CNOT gates connecting it directly to every other qubit. This topology creates a challenging layout problem because it does not map trivially to the device's coupling map.\n", "\n", - "The **star topology GHZ circuit** poses a significantly more challenging problem. Although it can still theoretically be executed in $ O(N) $ depth without SWAP gates, finding this solution requires identifying an optimal initial layout, which is much harder due to the non-linear connectivity of the circuit. This topology serves as a better test case for evaluating SABRE, as it demonstrates how configuration parameters impact layout and routing performance under more complex conditions.\n", + "We also define `ZZ` operators to measure entanglement correlations $\\langle Z_0 Z_i \\rangle$ across qubit pairs.\n", "\n", "![ghz_star_topology.png](/docs/images/tutorials/transpilation-optimizations-with-sabre/ghz_star_topology.avif)\n", "\n", - "Notably:\n", - "- The **HighLevelSynthesis** tool can produce the optimal $ O(N) $ depth solution for the star topology GHZ circuit without introducing SWAP gates, like shown in the image above.\n", - "- Alternatively, the **StarPrerouting** pass can reduce the depth further by guiding SABRE's routing decisions, though it may still introduce some SWAP gates. However, StarPrerouting increases runtime and requires integration into the initial transpilation process.\n", - "\n", - "For the purposes of this tutorial, we exclude both HighLevelSynthesis and StarPrerouting to isolate and highlight the direct impact of SABRE configuration on runtime and circuit depth. By measuring the expectation value $ \\langle Z_0 Z_i \\rangle $ for each qubit pair, we analyze:\n", - "- How well SABRE reduces SWAP gates and circuit depth.\n", - "- The effect of these optimizations on the fidelity of the executed circuit, where deviations from $ \\langle Z_0 Z_i \\rangle = 1 $ indicate loss of entanglement.!" + "\n", + "SABRE is a general-purpose algorithm and makes no assumptions about circuit structure. For this star-topology GHZ circuit, an optimal routing is actually known: the [`StarPreRouting`](/docs/api/qiskit/qiskit.transpiler.passes.StarPreRouting) pass detects star sub-circuits and rewrites them into a linear chain that maps directly onto any backend with a long enough linear path. This tutorial focuses on SABRE because it works for arbitrary circuits, but if you know your circuit has a clear special structure, applying a specialized pass like `StarPreRouting` before routing can outperform any heuristic search.\n", + "" ] }, { "cell_type": "code", "execution_count": 2, - "id": "1d77f7e1-8815-4aec-ab54-93efd735ae38", + "id": "08a8c4df-b1a5-45b7-9808-ca4e5d7631d8", "metadata": {}, "outputs": [], "source": [ - "# set seed for reproducibility\n", - "seed = 42\n", - "num_qubits = 110\n", - "\n", - "# Create GHZ circuit\n", - "qc = QuantumCircuit(num_qubits)\n", - "qc.h(0)\n", - "for i in range(1, num_qubits):\n", - " qc.cx(0, i)\n", - "\n", - "qc.measure_all()" - ] - }, - { - "cell_type": "markdown", - "id": "24018506-2ab5-4ff3-94b8-f173fb8b2673", - "metadata": {}, - "source": [ - "Next, we will map the operators of interest to evaluate the behavior of the system. Specifically, we will use `ZZ` operators between qubits to examine how the entanglement degrades as the qubits become farther apart. This analysis is critical because inaccuracies in the expectation values $\\langle Z_0 Z_i \\rangle$ for distant qubits can reveal the impact of noise and errors in the circuit execution. By studying these deviations, we gain insight into how well the circuit preserves entanglement under different SABRE configurations and how effectively SABRE minimizes the impact of hardware constraints." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9df28ec1-e572-47d7-bb84-457394129659", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "['ZZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZIII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZII', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZI', 'ZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIZ']\n", - "109\n" - ] - } - ], - "source": [ - "# ZZII...II, ZIZI...II, ... , ZIII...IZ\n", - "operator_strings = [\n", - " \"Z\" + \"I\" * i + \"Z\" + \"I\" * (num_qubits - 2 - i)\n", - " for i in range(num_qubits - 1)\n", + "num_qubits_sim = 15\n", + "\n", + "# Create star-topology GHZ circuit\n", + "qc_sim = QuantumCircuit(num_qubits_sim)\n", + "qc_sim.h(0)\n", + "for i in range(1, num_qubits_sim):\n", + " qc_sim.cx(0, i)\n", + "qc_sim.measure_all()\n", + "\n", + "# ZZ operators: Z on qubit 0 and qubit i, identity elsewhere\n", + "operator_strings_sim = [\n", + " \"Z\" + \"I\" * i + \"Z\" + \"I\" * (num_qubits_sim - 2 - i)\n", + " for i in range(num_qubits_sim - 1)\n", "]\n", - "print(operator_strings)\n", - "print(len(operator_strings))\n", - "\n", - "operators = [SparsePauliOp(operator) for operator in operator_strings]" + "operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]" ] }, { "cell_type": "markdown", - "id": "15400bae-af90-40d8-87c1-1637b8f226e2", + "id": "25ac8d92-6362-4d4e-a133-2914703df658", "metadata": {}, "source": [ "### Step 2: Optimize problem for quantum hardware execution\n", "\n", - "In this step, we focus on optimizing the circuit layout for execution on a specific quantum hardware device with 127 qubits. This is the main focus of the tutorial, as we perform **SABRE optimizations and transpilation** to achieve the best circuit performance. Using the `SabreLayout` pass, we determine an initial qubit mapping that minimizes the need for SWAP gates during routing. By passing the `coupling_map` of the target backend, `SabreLayout` adapts the layout to the device's connectivity constraints.\n", + "The default `optimization_level=3` preset pass manager already uses `SabreLayout`, but with conservative defaults. To explore the impact of stronger settings, that pass is replaced with a custom `SabreLayout` configured for more aggressive search, while every other pass in the layout stage is left untouched. As a separate point of comparison, a fourth pass manager keeps the default `SabreLayout` but adds [`StarPreRouting`](/docs/api/qiskit/qiskit.transpiler.passes.StarPreRouting) to the init stage. `StarPreRouting` is a *structure-aware* pass that detects star sub-circuits and rewrites them into a linear chain before routing.\n", "\n", - "We will use `generate_preset_pass_manager` with `optimization_level=3` for the transpilation process and customize the `SabreLayout` pass with different configurations. The goal is to find a setup that produces a transpiled circuit with the **lowest size and/or depth**, demonstrating the impact of SABRE optimizations.\n", + "The workflow is:\n", + "1. **Inspect** the default pass manager to see where `SabreLayout` sits inside the `layout` stage.\n", + "2. **Replace** that pass with a custom `SabreLayout` instance using `PassManager.replace(index, passes=...)`, and build the `pm_star` variant with `pm.init += StarPreRouting()`.\n", + "3. **Run** all four pass managers and compare metrics.\n", "\n", - "#### Why Are Circuit Size and Depth Important?\n", + "The four configurations are:\n", "\n", - "- **Lower size (gate count):** Reduces the number of operations, minimizing opportunities for errors to accumulate.\n", - "- **Lower depth:** Shortens the overall execution time, which is critical for avoiding decoherence and maintaining quantum state fidelity.\n", + "| Config | Description |\n", + "|--------|-------------|\n", + "| `pm_1` (default) | Default level-3 preset (`SabreLayout` with `max_iterations=4`, `layout_trials=20`, `swap_trials=20`) |\n", + "| `pm_2` | Custom `SabreLayout` (`max_iterations=4`, `layout_trials=200`, `swap_trials=200`) |\n", + "| `pm_3` | Custom `SabreLayout` (`max_iterations=8`, `layout_trials=200`, `swap_trials=200`) |\n", + "| `pm_star` | Default preset with `StarPreRouting` added to the init stage |\n", "\n", - "By optimizing these metrics, we improve the circuit’s reliability and execution accuracy on noisy quantum hardware." - ] - }, - { - "cell_type": "markdown", - "id": "21bb1821-ebcd-43f2-b48b-a93db99d872b", - "metadata": {}, - "source": [ - "Select the backend." + "**Key SABRE parameters:**\n", + "- **`layout_trials` / `swap_trials`**: Control how many candidate layouts and routing solutions SABRE explores. Increasing the number of trials means SABRE samples a wider search space, increasing the chance of finding a better solution.\n", + "- **`max_iterations`**: Controls how many forward-backward routing refinement cycles SABRE performs on each candidate. SABRE iteratively improves the layout by learning from routing feedback, so the more iterations, the better the improvements.\n", + "\n", + "Both come at the cost of longer transpilation time, but the resulting circuits are shorter and use fewer gates, which directly reduces decoherence and gate errors on real hardware.\n", + "\n", + "**Step 2a: Inspect the default pass manager.** A `StagedPassManager` is composed of stages (`init`, `layout`, `routing`, `translation`, `optimization`, `scheduling`), each itself a `PassManager`. Calling `.draw()` on a stage renders its passes as a graph so we can see where `SabreLayout` lives." ] }, { "cell_type": "code", - "execution_count": 4, - "id": "6bb232e5-cb6c-45b3-9bfa-ec027046bfab", + "execution_count": 3, + "id": "b40fe1e0-41cd-4e8b-acb9-801872d35f1f", "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using backend: ibm_boston\n" - ] + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "service = QiskitRuntimeService()\n", - "# backend = service.least_busy(\n", - "# operational=True, simulator=False, min_num_qubits=127\n", - "# )\n", - "backend = service.backend(\"ibm_boston\")\n", - "print(f\"Using backend: {backend.name}\")" + "# Build the default pass manager (no modifications yet)\n", + "pm_1 = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=seed\n", + ")\n", + "\n", + "# Visualize the layout stage to see where SabreLayout sits\n", + "pm_1.layout.draw()" ] }, { "cell_type": "markdown", - "id": "e17c1fd9-7d62-4c33-b5ff-c9d4e0596651", + "id": "8f9e5393-af3b-4d3d-a0d8-d20585b02895", "metadata": {}, "source": [ - "To evaluate the impact of different configurations on circuit optimization, we will create three pass managers, each with unique settings for the `SabreLayout` pass. These configurations help analyze the trade-off between circuit quality and transpilation time.\n", - "\n", - "#### Key parameters\n", - "- **`max_iterations`**: The number of forward-backward routing iterations to refine the layout and reduce routing costs.\n", - "- **`layout_trials`**: The number of random initial layouts tested, selecting the one that minimizes SWAP gates.\n", - "- **`swap_trials`**: The number of routing trials for each layout, refining gate placement for better routing.\n", - "\n", - "Increase `layout_trials` and `swap_trials` to perform more thorough optimization, at the cost of increased transpilation time.\n", - "\n", - "#### Configurations in this tutorial\n", - "1. **`pm_1`**: Default settings with `optimization_level=3`.\n", - " - `max_iterations=4`\n", - " - `layout_trials=20`\n", - " - `swap_trials=20`\n", + "In the diagram above, the `SabreLayout` pass we want to customize lives inside the `ConditionalController` at position **[2]** of the layout stage. That controller does two things:\n", "\n", - "2. **`pm_2`**: Increases the number of trials for better exploration.\n", - " - `max_iterations=4`\n", - " - `layout_trials=200`\n", - " - `swap_trials=200`\n", + "- It gates `SabreLayout` so it only runs when `VF2Layout` at [1] failed to find a perfect mapping (otherwise the perfect VF2 layout is kept).\n", + "- It precedes `SabreLayout` with a `BarrierBeforeFinalMeasurements` pass that protects measurements from being reordered during SabreLayout's internal routing.\n", "\n", - "3. **`pm_3`**: Extends `pm_2` by increasing the number of iterations for further refinement.\n", - " - `max_iterations=8`\n", - " - `layout_trials=200`\n", - " - `swap_trials=200`\n", + "If we just `replace(index=2, passes=sl_2)`, both behaviors are dropped. To keep them, we re-wrap our custom `SabreLayout` in the same `ConditionalController` (with the same condition and the protective barrier) before swapping it in.\n", "\n", - "By comparing the results of these configurations, we aim to determine which achieves the best balance between circuit quality (for example, size and depth) and computational cost." + "**Step 2b: Build custom `SabreLayout` passes and replace the default.**" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "2c1535af-a7ea-4439-b7f4-fd0aa9285930", + "execution_count": 4, + "id": "79075a21-8f36-4fd9-9d0d-bd0e97395b60", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Get the coupling map from the backend\n", - "cmap = CouplingMap(backend().configuration().coupling_map)\n", + "cmap = backend.coupling_map\n", "\n", - "# Create the SabreLayout passes for the custom configurations\n", + "# Custom SabreLayout passes with more aggressive search\n", "sl_2 = SabreLayout(\n", " coupling_map=cmap,\n", " seed=seed,\n", @@ -296,137 +273,141 @@ " swap_trials=200,\n", ")\n", "\n", - "# Create the pass managers, need to first create then configure the SabreLayout passes\n", - "pm_1 = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")\n", + "\n", + "# Same condition the preset uses: only run SabreLayout when VF2Layout did not\n", + "# find a perfect mapping. This preserves any perfect layout VF2 produced at [1].\n", + "def _vf2_match_not_found(property_set):\n", + " if property_set[\"layout\"] is None:\n", + " return True\n", + " return (\n", + " property_set[\"VF2Layout_stop_reason\"] is not None\n", + " and property_set[\"VF2Layout_stop_reason\"]\n", + " is not VF2LayoutStopReason.SOLUTION_FOUND\n", + " )\n", + "\n", + "\n", + "def wrap_sabre(sabre_pass):\n", + " \"\"\"Re-wrap a SabreLayout in the original ConditionalController + barrier.\"\"\"\n", + " return ConditionalController(\n", + " [\n", + " BarrierBeforeFinalMeasurements(\n", + " \"qiskit.transpiler.internal.routing.protection.barrier\"\n", + " ),\n", + " sabre_pass,\n", + " ],\n", + " condition=_vf2_match_not_found,\n", + " )\n", + "\n", + "\n", + "# Build two fresh pass managers and swap in the wrapped custom SabreLayout at index 2\n", "pm_2 = generate_preset_pass_manager(\n", " optimization_level=3, backend=backend, seed_transpiler=seed\n", ")\n", "pm_3 = generate_preset_pass_manager(\n", " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d899d9fd-b72d-4930-b337-e67c98307b76", - "metadata": {}, - "source": [ - "Now we can configure the `SabreLayout` pass in the custom pass managers. To do this we know that for the default `generate_preset_pass_manager` on `optimization_level=3`, the `SabreLayout` pass is at index 2, as `SabreLayout` occurs after `SetLayout` and `VF2Laout` passes. We can access this pass and modify its parameters." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "98d7901c-bc20-434b-881d-ac0e841907f8", - "metadata": {}, - "outputs": [], - "source": [ - "pm_2.layout.replace(index=2, passes=sl_2)\n", - "pm_3.layout.replace(index=2, passes=sl_3)" + ")\n", + "pm_2.layout.replace(index=2, passes=wrap_sabre(sl_2))\n", + "pm_3.layout.replace(index=2, passes=wrap_sabre(sl_3))\n", + "\n", + "# Build pm_star: default preset with StarPreRouting added to the init stage\n", + "pm_star = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=seed\n", + ")\n", + "pm_star.init += StarPreRouting()\n", + "\n", + "# Visualize pm_3 after replacement (pm_2 has the same structure, only max_iterations differs)\n", + "pm_3.layout.draw()" ] }, { "cell_type": "markdown", - "id": "26e8bcee-a8ef-4603-a5ec-92ca7cea6799", + "id": "378a5829-b18b-4359-827f-b052c7cc1b89", "metadata": {}, "source": [ - "With each pass manager configured, we will now execute the transpilation process for each. To compare results, we will track key metrics, including the transpilation time, the depth of the circuit (measured as the two-qubit gate depth), and the total number of gates in the transpiled circuits" + "Position **[2]** is now a `ConditionalController` again — identical in shape to the default, but the inner `SabreLayout` is our custom one (with `layout_trials=200`, `swap_trials=200`, and `max_iterations=8` for `pm_3`; `pm_2` is identical apart from `max_iterations=4`). The protective barrier and the `_vf2_match_not_found` gating are preserved, so the only difference between `pm_2`/`pm_3` and `pm_1` is the SABRE configuration itself. `pm_star` keeps the default `SabreLayout` and only adds `StarPreRouting` at the end of the init stage.\n", + "\n", + "**Step 2c: Run each pass manager and compare.**" ] }, { "cell_type": "code", - "execution_count": 7, - "id": "06dda879-4eb8-4e78-a433-5aaefcd780b0", + "execution_count": 5, + "id": "fd52f8dd-862b-46e2-b93d-0b35f47a3d83", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Pass manager 1 (4,20,20) : Depth 439, Size 2346, Time 0.5775 s\n", - "Pass manager 2 (4,200,200): Depth 395, Size 2070, Time 3.9927 s\n", - " - Depth improvement: 10.02%\n", - " - Size improvement: 11.76%\n", - " - Time increase: 591.43%\n", - "Pass manager 3 (8,200,200): Depth 375, Size 1873, Time 2.3079 s\n", - " - Depth improvement: 14.58%\n", - " - Size improvement: 20.16%\n", - " - Time increase: 299.67%\n" + "pm_1 (4,20,20): 2Q Depth 38, Size 183, Time 0.01s\n", + "pm_2 (4,200,200): 2Q Depth 36, Size 183, Time 0.15s\n", + "pm_3 (8,200,200): 2Q Depth 30, Size 158, Time 0.16s\n", + "pm_star (default + StarPreRouting): 2Q Depth 26, Size 160, Time 0.01s\n", + "\n", + "Improvement vs. default (pm_1):\n", + " pm_2 (4,200,200): 2Q depth +5.3%, size +0.0%\n", + " pm_3 (8,200,200): 2Q depth +21.1%, size +13.7%\n", + " pm_star (default + StarPreRouting): 2Q depth +31.6%, size +12.6%\n" ] } ], "source": [ - "# Transpile the circuit with each pass manager and measure the time\n", - "t0 = time.time()\n", - "tqc_1 = pm_1.run(qc)\n", - "t1 = time.time() - t0\n", - "t0 = time.time()\n", - "tqc_2 = pm_2.run(qc)\n", - "t2 = time.time() - t0\n", - "t0 = time.time()\n", - "tqc_3 = pm_3.run(qc)\n", - "t3 = time.time() - t0\n", - "\n", - "# Obtain the depths and the total number of gates (circuit size)\n", - "depth_1 = tqc_1.depth(lambda x: x.operation.num_qubits == 2)\n", - "depth_2 = tqc_2.depth(lambda x: x.operation.num_qubits == 2)\n", - "depth_3 = tqc_3.depth(lambda x: x.operation.num_qubits == 2)\n", - "size_1 = tqc_1.size()\n", - "size_2 = tqc_2.size()\n", - "size_3 = tqc_3.size()\n", - "\n", - "# Transform the observables to match the backend's ISA\n", - "operators_list_1 = [op.apply_layout(tqc_1.layout) for op in operators]\n", - "operators_list_2 = [op.apply_layout(tqc_2.layout) for op in operators]\n", - "operators_list_3 = [op.apply_layout(tqc_3.layout) for op in operators]\n", - "\n", - "# Compute improvements compared to pass manager 1 (default)\n", - "depth_improvement_2 = ((depth_1 - depth_2) / depth_1) * 100\n", - "depth_improvement_3 = ((depth_1 - depth_3) / depth_1) * 100\n", - "size_improvement_2 = ((size_1 - size_2) / size_1) * 100\n", - "size_improvement_3 = ((size_1 - size_3) / size_1) * 100\n", - "time_increase_2 = ((t2 - t1) / t1) * 100\n", - "time_increase_3 = ((t3 - t1) / t1) * 100\n", - "\n", - "print(\n", - " f\"Pass manager 1 (4,20,20) : Depth {depth_1}, Size {size_1}, Time {t1:.4f} s\"\n", - ")\n", - "print(\n", - " f\"Pass manager 2 (4,200,200): Depth {depth_2}, Size {size_2}, Time {t2:.4f} s\"\n", - ")\n", - "print(f\" - Depth improvement: {depth_improvement_2:.2f}%\")\n", - "print(f\" - Size improvement: {size_improvement_2:.2f}%\")\n", - "print(f\" - Time increase: {time_increase_2:.2f}%\")\n", - "print(\n", - " f\"Pass manager 3 (8,200,200): Depth {depth_3}, Size {size_3}, Time {t3:.4f} s\"\n", - ")\n", - "print(f\" - Depth improvement: {depth_improvement_3:.2f}%\")\n", - "print(f\" - Size improvement: {size_improvement_3:.2f}%\")\n", - "print(f\" - Time increase: {time_increase_3:.2f}%\")" + "results_sim = {}\n", + "for name, pm in [\n", + " (\"pm_1 (4,20,20)\", pm_1),\n", + " (\"pm_2 (4,200,200)\", pm_2),\n", + " (\"pm_3 (8,200,200)\", pm_3),\n", + " (\"pm_star (default + StarPreRouting)\", pm_star),\n", + "]:\n", + " t0 = time.time()\n", + " tqc = pm.run(qc_sim)\n", + " elapsed = time.time() - t0\n", + " depth = tqc.depth(lambda x: x.operation.num_qubits == 2)\n", + " size = tqc.size()\n", + " ops_mapped = [op.apply_layout(tqc.layout) for op in operators_sim]\n", + " results_sim[name] = {\n", + " \"tqc\": tqc,\n", + " \"ops\": ops_mapped,\n", + " \"depth\": depth,\n", + " \"size\": size,\n", + " \"time\": elapsed,\n", + " }\n", + " print(f\"{name}: 2Q Depth {depth}, Size {size}, Time {elapsed:.2f}s\")\n", + "\n", + "# Print improvement relative to default (pm_1)\n", + "baseline = results_sim[\"pm_1 (4,20,20)\"]\n", + "print(\"\\nImprovement vs. default (pm_1):\")\n", + "for name in [\n", + " \"pm_2 (4,200,200)\",\n", + " \"pm_3 (8,200,200)\",\n", + " \"pm_star (default + StarPreRouting)\",\n", + "]:\n", + " r = results_sim[name]\n", + " depth_pct = (baseline[\"depth\"] - r[\"depth\"]) / baseline[\"depth\"] * 100\n", + " size_pct = (baseline[\"size\"] - r[\"size\"]) / baseline[\"size\"] * 100\n", + " print(f\" {name}: 2Q depth {depth_pct:+.1f}%, size {size_pct:+.1f}%\")" ] }, { "cell_type": "markdown", - "id": "f187b5db-24a0-4080-a6bb-f949f72354ba", + "id": "0c27cbdb-c065-4a36-a0ef-6c9572c7a4a5", "metadata": {}, "source": [ - "The results demonstrate that increasing the number of trials (`layout_trials` and `swap_trials`) can significantly improve circuit quality by reducing both depth and size. However, this improvement often comes at the cost of increased runtime due to the additional computation required to explore more potential layouts and routing paths.\n", + "All three modified pass managers produced circuits with lower 2Q depth than the default. The aggressive SABRE configurations (`pm_2` and `pm_3`) trade longer transpilation time for a wider search, while `pm_star` leverages the star structure of the circuit and produces an even shallower result without paying any extra transpilation cost. The exact gains will vary from run to run, but the general trend is consistent: more SABRE trials and iterations let the heuristic search a wider space, and structure-aware passes like `StarPreRouting` can sidestep that search entirely when the circuit shape matches.\n", "\n", - "Increasing the `max_iterations` can further enhance optimization by refining the layout through more forward-backward routing cycles. In this case, increasing `max_iterations` resulted in the most significant reduction in circuit depth and size, even reducing runtime compared to `pm_2`, likely by streamlining subsequent optimization stages. It’s important to note, however, that the effectiveness of increasing `max_iterations` can vary significantly depending on the circuit. While more iterations may yield better layout and routing choices, they provide no guarantees and depend heavily on the circuit’s structure and the complexity of the connectivity constraints" + "Even at this small scale (15 qubits), the room for improvement is enough that all three approaches beat the default. With larger circuits (100+ qubits), the search space grows dramatically and the benefits of both increased trials and structure-aware passes become much more pronounced, as the large-scale section will show." ] }, { "cell_type": "code", - "execution_count": 8, - "id": "818a8997-d2c7-4661-a6ea-f58eac376bf8", + "execution_count": 6, + "id": "bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, "metadata": {}, @@ -434,147 +415,201 @@ } ], "source": [ - "# Plot the results of the metrics\n", - "times = [t1, t2, t3]\n", - "depths = [depth_1, depth_2, depth_3]\n", - "sizes = [size_1, size_2, size_3]\n", - "pm_names = [\n", - " \"pm_1 (4 iter, 20 trials)\",\n", - " \"pm_2 (4 iter, 200 trials)\",\n", - " \"pm_3 (8 iter, 200 trials)\",\n", - "]\n", - "colors = plt.cm.viridis(np.linspace(0.2, 0.8, len(pm_names)))\n", - "\n", - "# Create a figure with three subplots\n", - "fig, axs = plt.subplots(3, 1, figsize=(6, 9), sharex=True)\n", - "axs[0].bar(pm_names, times, color=colors)\n", - "axs[0].set_ylabel(\"Time (s)\", fontsize=12)\n", - "axs[0].set_title(\"Transpilation Time\", fontsize=14)\n", - "axs[0].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - "axs[1].bar(pm_names, depths, color=colors)\n", - "axs[1].set_ylabel(\"Depth\", fontsize=12)\n", - "axs[1].set_title(\"Circuit Depth\", fontsize=14)\n", - "axs[1].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - "axs[2].bar(pm_names, sizes, color=colors)\n", - "axs[2].set_ylabel(\"Size\", fontsize=12)\n", - "axs[2].set_title(\"Circuit Size\", fontsize=14)\n", - "axs[2].set_xticks(range(len(pm_names)))\n", - "axs[2].set_xticklabels(pm_names, fontsize=10, rotation=15)\n", - "axs[2].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - "\n", - "# Add some spacing between subplots\n", + "pm_names = list(results_sim.keys())\n", + "depths = [results_sim[n][\"depth\"] for n in pm_names]\n", + "sizes = [results_sim[n][\"size\"] for n in pm_names]\n", + "times = [results_sim[n][\"time\"] for n in pm_names]\n", + "colors = [\"#404080\", \"#2a9d8f\", \"#a8d05e\", \"#e29bdd\"]\n", + "x = np.arange(len(pm_names))\n", + "\n", + "fig, axs = plt.subplots(1, 3, figsize=(14, 5))\n", + "\n", + "# 2Q Depth\n", + "bars = axs[0].bar(x, depths, color=colors)\n", + "axs[0].set_ylabel(\"2Q Depth\", fontsize=11)\n", + "axs[0].set_title(\"Two-Qubit Gate Depth\", fontsize=13)\n", + "axs[0].set_ylim(0, max(depths) * 1.2)\n", + "for bar, val in zip(bars, depths):\n", + " axs[0].text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + max(depths) * 0.02,\n", + " str(val),\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=11,\n", + " fontweight=\"bold\",\n", + " )\n", + "for i in range(1, len(depths)):\n", + " pct = (depths[0] - depths[i]) / depths[0] * 100\n", + " if pct != 0:\n", + " axs[0].text(\n", + " bars[i].get_x() + bars[i].get_width() / 2,\n", + " bars[i].get_height() / 2,\n", + " f\"{pct:+.0f}%\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=10,\n", + " color=\"white\",\n", + " fontweight=\"bold\",\n", + " )\n", + "\n", + "# Size\n", + "bars = axs[1].bar(x, sizes, color=colors)\n", + "axs[1].set_ylabel(\"Gate Count\", fontsize=11)\n", + "axs[1].set_title(\"Circuit Size\", fontsize=13)\n", + "axs[1].set_ylim(0, max(sizes) * 1.2)\n", + "for bar, val in zip(bars, sizes):\n", + " axs[1].text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + max(sizes) * 0.02,\n", + " str(val),\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=11,\n", + " fontweight=\"bold\",\n", + " )\n", + "for i in range(1, len(sizes)):\n", + " pct = (sizes[0] - sizes[i]) / sizes[0] * 100\n", + " if abs(pct) > 0.1:\n", + " axs[1].text(\n", + " bars[i].get_x() + bars[i].get_width() / 2,\n", + " bars[i].get_height() / 2,\n", + " f\"{pct:+.0f}%\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=10,\n", + " color=\"white\",\n", + " fontweight=\"bold\",\n", + " )\n", + "\n", + "# Time\n", + "bars = axs[2].bar(x, times, color=colors)\n", + "axs[2].set_ylabel(\"Time (s)\", fontsize=11)\n", + "axs[2].set_title(\"Transpilation Time\", fontsize=13)\n", + "axs[2].set_ylim(0, max(times) * 1.3)\n", + "for bar, val in zip(bars, times):\n", + " axs[2].text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + max(times) * 0.03,\n", + " f\"{val:.2f}s\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=11,\n", + " fontweight=\"bold\",\n", + " )\n", + "\n", + "for ax in axs:\n", + " ax.set_xticks(x)\n", + " ax.set_xticklabels(pm_names, fontsize=8, rotation=15)\n", + " ax.grid(axis=\"y\", linestyle=\"--\", alpha=0.5)\n", + "\n", + "plt.suptitle(\n", + " \"Transpilation quality vs. configuration\",\n", + " fontsize=14,\n", + " fontweight=\"bold\",\n", + " y=1.02,\n", + ")\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", - "id": "13a9127a-aee2-447c-8921-f6d26caac84c", + "id": "6a233965-1358-420b-8823-5f4e206090b4", "metadata": {}, "source": [ "### Step 3: Execute using Qiskit primitives\n", "\n", - "In this step, we use the `Estimator` primitive to calculate the expectation values $\\langle Z_0 Z_i \\rangle$ for the `ZZ` operators, evaluating the entanglement and execution quality of the transpiled circuits. To align with typical user workflows, we submit the job for execution and apply error suppression using **dynamical decoupling**, a technique that mitigates decoherence by inserting gate sequences to preserve qubit states. Additionally, we specify a resilience level to counteract noise, with higher levels providing more accurate results at the cost of increased processing time. This approach assesses the performance of each pass manager configuration under realistic execution conditions." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "4ca2c383-f58c-457e-9394-2e0e8ef38d5a", - "metadata": {}, - "outputs": [], - "source": [ - "options = EstimatorOptions()\n", - "options.resilience_level = 2\n", - "options.dynamical_decoupling.enable = True\n", - "options.dynamical_decoupling.sequence_type = \"XY4\"\n", - "\n", - "# Create an Estimator object\n", - "estimator = Estimator(backend, options=options)" + "We run each transpiled circuit **10 times** using the Aer `EstimatorV2` with a noise model derived from the real backend. Since noisy simulation results vary between runs, averaging over multiple runs gives more reliable fidelity estimates and lets us quantify the statistical uncertainty with error bars." ] }, { "cell_type": "code", - "execution_count": 10, - "id": "bde39e77-e4f2-43bf-a9c4-8b9d444a6e82", + "execution_count": 15, + "id": "a91b9887-c8cf-48fd-a6fb-a5506d201f8d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "d5k0qs7853es738dab6g\n", - "d5k0qsf853es738dab70\n", - "d5k0qsf853es738dab7g\n" + "Run 1/10 done\n", + "Run 2/10 done\n", + "Run 3/10 done\n", + "Run 4/10 done\n", + "Run 5/10 done\n", + "Run 6/10 done\n", + "Run 7/10 done\n", + "Run 8/10 done\n", + "Run 9/10 done\n", + "Run 10/10 done\n", + "pm_1 (4,20,20): mean fidelity = 0.9510 +/- 0.0094\n", + "pm_2 (4,200,200): mean fidelity = 0.9513 +/- 0.0043\n", + "pm_3 (8,200,200): mean fidelity = 0.9540 +/- 0.0065\n", + "pm_star (default + StarPreRouting): mean fidelity = 0.9547 +/- 0.0072\n" ] } ], "source": [ - "# Submit the circuit to Estimator\n", - "job_1 = estimator.run([(tqc_1, operators_list_1)])\n", - "job_1_id = job_1.job_id()\n", - "print(job_1_id)\n", - "\n", - "job_2 = estimator.run([(tqc_2, operators_list_2)])\n", - "job_2_id = job_2.job_id()\n", - "print(job_2_id)\n", - "\n", - "job_3 = estimator.run([(tqc_3, operators_list_3)])\n", - "job_3_id = job_3.job_id()\n", - "print(job_3_id)" + "# Create a noisy estimator from the real backend's noise model\n", + "noisy_estimator = AerEstimator.from_backend(backend)\n", + "\n", + "num_runs = 10\n", + "# sim_all_runs[name] = list of arrays, one per run\n", + "sim_all_runs = {name: [] for name in results_sim}\n", + "\n", + "for run in range(num_runs):\n", + " for name, r in results_sim.items():\n", + " job = noisy_estimator.run([(r[\"tqc\"], r[\"ops\"])])\n", + " evs = list(job.result()[0].data.evs)\n", + " sim_all_runs[name].append(evs)\n", + " print(f\"Run {run + 1}/{num_runs} done\")\n", + "\n", + "# Compute mean and std across runs for each config\n", + "sim_stats = {}\n", + "for name in results_sim:\n", + " all_evs = np.array(sim_all_runs[name]) # shape (num_runs, num_operators)\n", + " sim_stats[name] = {\n", + " \"mean\": np.mean(all_evs, axis=0),\n", + " \"std\": np.std(all_evs, axis=0),\n", + " \"overall_mean\": np.mean(all_evs),\n", + " \"overall_std\": np.std(\n", + " np.mean(all_evs, axis=1)\n", + " ), # std of per-run averages\n", + " }\n", + " print(\n", + " f\"{name}: mean fidelity = {sim_stats[name]['overall_mean']:.4f} +/- {sim_stats[name]['overall_std']:.4f}\"\n", + " )" ] }, { - "cell_type": "code", - "execution_count": 11, - "id": "a5d36e34-c725-414d-af94-ff48fc139b81", + "cell_type": "markdown", + "id": "89e6c70f-4da4-470f-b0fa-2e2521c82f6f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job 1 done\n", - "Job 2 done\n", - "Job 3 done\n" - ] - } - ], "source": [ - "# Run the jobs\n", - "result_1 = job_1.result()[0]\n", - "print(\"Job 1 done\")\n", - "result_2 = job_2.result()[0]\n", - "print(\"Job 2 done\")\n", - "result_3 = job_3.result()[0]\n", - "print(\"Job 3 done\")" + "Because this is a small circuit, the fidelity values land relatively close across all four configurations. The circuits are short enough that hardware noise does not heavily penalize even the least optimized version. Mean fidelity broadly tracks 2Q depth: `pm_3` and `pm_star`, the two shallowest circuits, achieve the highest fidelities and are essentially tied within their error bars. `pm_2` is a useful counter-example: although its 2Q depth is lower than `pm_1`'s, its mean fidelity ends up marginally lower as well, which is a reminder that the depth-to-fidelity link is statistical rather than deterministic. The specific qubits a layout selects and the calibration of those qubits at run time also matter." ] }, { "cell_type": "markdown", - "id": "30899adf-e2a1-4ca0-867c-1ac994bb2206", + "id": "e601648b-8f5f-4111-ba35-f64af63da909", "metadata": {}, "source": [ "### Step 4: Post-process and return result in desired classical format\n", "\n", - "Once the job completes, we analyze the results by plotting the expectation values $\\langle Z_0 Z_i \\rangle$ for each qubit. In an ideal simulation, all $\\langle Z_0 Z_i \\rangle$ values should be 1, reflecting perfect entanglement across the qubits. However, due to noise and hardware constraints, the expectation values typically decrease as `i` increases, revealing how entanglement degrades over distance.\n", - "\n", - "In this step, we compare the results from each pass manager configuration to the ideal simulation. By examining the deviation of $\\langle Z_0 Z_i \\rangle$ from 1 for each configuration, we can quantify how well each pass manager preserves entanglement and mitigates the effects of noise. This analysis directly assesses the impact of SABRE optimizations on execution fidelity and highlights which configuration best balances optimization quality and execution performance.\n", - "\n", - "\n", - "The results will be visualized to highlight differences across pass managers, showcasing how improvements in layout and routing influence the final circuit execution on noisy quantum hardware." + "Next, plot the entanglement correlations $\\langle Z_0 Z_i \\rangle$ as a function of qubit distance, along with the **mean correlation** as a single fidelity metric. In an ideal (noiseless) case, all correlations would be 1. With realistic noise, each additional gate introduces error and each additional time step allows decoherence, so a transpiled circuit with lower depth and fewer gates (especially two-qubit gates) should preserve entanglement better." ] }, { "cell_type": "code", - "execution_count": 12, - "id": "bc6cb36f-4bf2-4275-baf5-9557fcba520a", + "execution_count": 16, + "id": "a6dac5ed-a963-458a-ada1-89c915f036e0", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, "metadata": {}, @@ -582,765 +617,581 @@ } ], "source": [ - "data = list(range(1, len(operators) + 1)) # Distance between the Z operators\n", - "\n", - "values_1 = list(result_1.data.evs)\n", - "values_2 = list(result_2.data.evs)\n", - "values_3 = list(result_3.data.evs)\n", + "data_sim = list(range(1, len(operators_sim) + 1))\n", + "markers = [\"o\", \"s\", \"^\", \"*\"]\n", + "colors_line = [\"#404080\", \"#2a9d8f\", \"#a8d05e\", \"#e29bdd\"]\n", "\n", - "plt.plot(\n", - " data,\n", - " values_1,\n", - " marker=\"o\",\n", - " label=\"pm_1 (iters=4, swap_trials=20, layout_trials=20)\",\n", + "fig, (ax1, ax2) = plt.subplots(\n", + " 1, 2, figsize=(14, 5), gridspec_kw={\"width_ratios\": [2.5, 1]}\n", ")\n", - "plt.plot(\n", - " data,\n", - " values_2,\n", - " marker=\"s\",\n", - " label=\"pm_2 (iters=4, swap_trials=200, layout_trials=200)\",\n", + "\n", + "# Left: correlations vs distance with error bars (mean +/- 1 std)\n", + "for (name, stats), marker, color in zip(\n", + " sim_stats.items(), markers, colors_line\n", + "):\n", + " ax1.errorbar(\n", + " data_sim,\n", + " stats[\"mean\"],\n", + " yerr=stats[\"std\"],\n", + " marker=marker,\n", + " label=name,\n", + " color=color,\n", + " linewidth=2,\n", + " capsize=3,\n", + " capthick=1,\n", + " elinewidth=1,\n", + " )\n", + "\n", + "ax1.set_xlabel(\"Distance between qubits $i$\", fontsize=11)\n", + "ax1.set_ylabel(r\"$\\langle Z_0 Z_i \\rangle$\", fontsize=11)\n", + "ax1.set_title(\n", + " \"Entanglement correlations vs. qubit distance (avg. of 10 runs)\",\n", + " fontsize=12,\n", ")\n", - "plt.plot(\n", - " data,\n", - " values_3,\n", - " marker=\"^\",\n", - " label=\"pm_3 (iters=8, swap_trials=200, layout_trials=200)\",\n", + "ax1.legend(fontsize=9)\n", + "ax1.grid(alpha=0.3)\n", + "\n", + "# Right: mean correlation bar chart with error bars\n", + "names = list(sim_stats.keys())\n", + "means = [sim_stats[n][\"overall_mean\"] for n in names]\n", + "stds = [sim_stats[n][\"overall_std\"] for n in names]\n", + "x_bar = np.arange(len(names))\n", + "bars = ax2.bar(\n", + " x_bar, means, yerr=stds, color=colors_line, capsize=5, ecolor=\"gray\"\n", ")\n", - "plt.xlabel(\"Distance between qubits $i$\")\n", - "plt.ylabel(r\"$\\langle Z_i Z_0 \\rangle / \\langle Z_1 Z_0 \\rangle $\")\n", - "plt.legend()\n", + "ax2.set_ylabel(r\"Mean $\\langle Z_0 Z_i \\rangle$\", fontsize=11)\n", + "ax2.set_title(\"Average fidelity\", fontsize=13, pad=12)\n", + "y_range = max(means) - min(means) if max(means) != min(means) else 0.01\n", + "# Top of ylim accounts for the bar height + std error bar + headroom for the value label\n", + "y_top = max(m + s for m, s in zip(means, stds)) + y_range * 1.5\n", + "ax2.set_ylim(min(means) - y_range * 0.8, y_top)\n", + "for bar, val, std in zip(bars, means, stds):\n", + " ax2.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + std + y_range * 0.15,\n", + " f\"{val:.4f}\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=10,\n", + " fontweight=\"bold\",\n", + " )\n", + "# Annotate % change vs pm_1\n", + "baseline_mean = means[0]\n", + "for i in range(1, len(means)):\n", + " pct = (means[i] - baseline_mean) / baseline_mean * 100\n", + " if abs(pct) > 0.01:\n", + " mid_y = (means[i] + ax2.get_ylim()[0]) / 2\n", + " ax2.text(\n", + " bars[i].get_x() + bars[i].get_width() / 2,\n", + " mid_y,\n", + " f\"{pct:+.1f}%\",\n", + " ha=\"center\",\n", + " va=\"center\",\n", + " fontsize=10,\n", + " color=\"white\",\n", + " fontweight=\"bold\",\n", + " )\n", + "ax2.set_xticks(x_bar)\n", + "ax2.set_xticklabels(names, fontsize=8, rotation=15)\n", + "ax2.grid(axis=\"y\", linestyle=\"--\", alpha=0.5)\n", + "\n", + "fig.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", - "id": "38a1d768-aa3e-41dc-8af6-e6806f58a85c", + "id": "05235ab1-b473-4267-a34c-5f56e728550b", "metadata": {}, "source": [ - "### Analysis of Results\n", - "\n", - "The plot shows the expectation values $\\langle Z_0 Z_i \\rangle / \\langle Z_0 Z_0 \\rangle$ as a function of the distance between qubits for three pass manager configurations with increasing levels of optimization. In the ideal case, these values remain close to 1, indicating strong correlations across the circuit. As the distance increases, noise and accumulated errors lead to a decay in correlations, revealing how well each transpilation strategy preserves the underlying structure of the state.\n", + "The results show a clear connection between transpilation quality and execution fidelity, with a few useful caveats:\n", "\n", - "Among the three configurations, `pm_1` clearly performs the worst. Its correlation values decay rapidly as the distance increases and approach zero much earlier than the other two configurations. This behavior is consistent with its larger circuit depth and gate count, where accumulated noise quickly degrades long-range correlations.\n", + "- **`pm_1` (default)**: Baseline. With only 20 trials and four iterations, SABRE has limited room to optimize, resulting in the deepest of the SABRE-only circuits.\n", + "- **`pm_2` (more trials)**: Exploring ten times more candidates finds a slightly shallower layout, but mean fidelity is roughly flat (and can even dip below the baseline within noise) because the depth gain is small at this scale.\n", + "- **`pm_3` (more trials + more iterations)**: Doubling `max_iterations` to 8 gives SABRE more refinement cycles, producing the shallowest SABRE-only circuit and the highest mean fidelity in the comparison.\n", + "- **`pm_star` (default + StarPreRouting)**: Adds `StarPreRouting` to the init stage of an otherwise default preset. The structure-aware rewrite collapses the star into a linear chain that the rest of the transpiler maps onto the device's linear path, producing the shallowest circuit overall (slightly better than `pm_3`) and matching `pm_3` on fidelity within error bars. It does this with the same transpilation time as the default, since the rewrite is essentially free compared to SABRE's stochastic search.\n", "\n", - "Both `pm_2` and `pm_3` represent significant improvements over `pm_1` across essentially all distances. On average, `pm_3` exhibits the strongest overall performance, maintaining higher correlation values over longer distances and showing a more gradual decay. This aligns with its more aggressive optimization, which produces shallower circuits that are generally more robust to noise accumulation.\n", - "\n", - "That said, `pm_2` shows noticeably better accuracy at short distances compared to `pm_3`, despite having a slightly larger depth and gate count. This suggests that circuit depth alone does not fully determine performance; the specific structure produced by the transpilation, including how entangling gates are arranged and how errors propagate through the circuit, also plays an important role. In some cases, the transformations applied by `pm_2` appear to better preserve local correlations, even if they do not scale as well to longer distances.\n", - "\n", - "Taken together, these results highlight a trade-off between circuit compactness and circuit structure. While increased optimization generally improves long-range stability, the best performance for a given observable depends on both reducing circuit depth and producing a structure that is well matched to the noise characteristics of the hardware." + "Note that increasing `max_iterations` does not always have a positive impact. In this case it helped significantly, but for other circuits or backends the additional iterations may not yield further improvement, or may even slightly hurt performance due to over-optimization of a local minimum. In general, you should increase `layout_trials` and `swap_trials` as much as your time budget allows, since more trials always increases the chance of finding a better layout. Increasing `max_iterations` is worth testing but should be validated for your specific use case. Specialized passes like `StarPreRouting` are similar in spirit but more circuit-dependent: they only help when the circuit actually contains the structure they target. The gain is large when applicable and zero otherwise, but they cost essentially nothing to try." ] }, { "cell_type": "markdown", - "id": "33b1f4dd-5d8b-4282-9f69-ced86cc2530b", + "id": "c2b60e42-4aa2-4dc1-8a11-022072e79776", "metadata": {}, "source": [ - "## Part II. Configuring the heuristic in SABRE and using Serverless\n", + "## Large-scale hardware example\n", "\n", - "In addition to adjusting trial numbers, SABRE supports customization of the routing heuristic used during transpilation. By default, `SabreLayout` employs the decay heuristic, which dynamically weights qubits based on their likelihood of being swapped. To use a different heuristic (such as the `lookahead` heuristic), you can create a custom `SabreSwap` pass and connect it to `SabreLayout` by running a `PassManager` with `FullAncillaAllocation`, `EnlargeWithAncilla`, and `ApplyLayout`. When using `SabreSwap` as a parameter for `SabreLayout`, only one layout trial is performed by default. To efficiently run multiple layout trials, we leverage the serverless runtime for parallelization. For more about serverless, see the [Serverless documentation](/docs/guides/serverless).\n", + "In addition to adjusting trial numbers, SABRE supports customizing the **routing heuristic**. SABRE offers three heuristics:\n", + "- **`basic`**: A simple greedy approach that selects the swap minimizing the immediate distance to the next gate.\n", + "- **`decay`** (default): Dynamically weights qubits based on recent activity, discouraging repeated swaps on the same qubits.\n", + "- **`lookahead`**: Evaluates future routing costs by looking ahead at upcoming gates, potentially finding better swap sequences.\n", "\n", - "### How to Change the Routing Heuristic\n", - "1. Create a custom `SabreSwap` pass with the desired heuristic.\n", - "2. Use this custom `SabreSwap` as the routing method for the `SabreLayout` pass.\n", + "To use a custom heuristic, create a `SabreSwap` pass and connect it to `SabreLayout` via the `routing_pass` parameter.\n", "\n", - "While it is possible to run multiple layout trials using a loop, serverless runtime is the better choice for large-scale and more vigorous experiments. Serverless supports parallel execution of layout trials, significantly speeding up the optimization of larger circuits and large experimental sweeps. This makes it especially valuable when working with resource-intensive tasks or when time efficiency is critical.\n", + "A fourth pass manager is added to the comparison: `pm_star_hw`, which keeps the default `SabreLayout`/`SabreSwap` settings but adds `StarPreRouting` to the init stage. At this scale (100 qubits) the SABRE search is harder, and the rewrite from a star into a linear chain becomes a clear win because a Heron processor has linear paths long enough to host the resulting circuit.\n", "\n", - "This section focuses solely on step 2 of optimization: minimizing circuit size and depth to achieve the best possible transpiled circuit. Building on the earlier results, we now explore how heuristic customization and serverless parallelization can further enhance optimization performance, making it suitable for large-scale quantum circuit transpilation." + "Here we compare all three SABRE heuristics plus `StarPreRouting` at scale on a 100-qubit GHZ circuit. We run multiple layout trials with different seeds for the SABRE configurations, select the best transpiled circuit from each, and submit them all to real hardware alongside the `StarPreRouting` result." ] }, { "cell_type": "markdown", - "id": "38876eac-253f-4882-b468-7618ef0c6ba9", + "id": "d755fafa-bbbc-4191-9ad9-a77f2af1bedc", "metadata": {}, "source": [ - "### Results without serverless runtime (1 layout trial):" + "### Steps 1-4 compressed into a single code block\n", + "Here the full workflow is put together at a larger scale. When using `SabreSwap` as the `routing_pass` for `SabreLayout`, only one layout trial is performed per call, so the following code cell loops over seeds to explore the layout space.\n", + "\n", + "We use the same `wrap_sabre` helper defined in the small-scale Step 2 (above), and add an analogous `wrap_routing` helper because the `routing` stage at index [1] is also a `ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...)` — replacing it bare would similarly drop the protective barrier and the `_swap_condition` gating." ] }, { "cell_type": "code", - "execution_count": 17, - "id": "95a5fb2f-8bca-4418-ad1a-c8da533c111f", + "execution_count": 9, + "id": "4feb0fcf-d305-4741-82a7-b0fe0f0894fc", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Default (heuristic='decay') : Depth 443, Size 3115, Time 1.034372091293335\n", - "Custom (heuristic='lookahead'): Depth 432, Size 2856, Time 0.6669301986694336\n" - ] - } - ], + "outputs": [], "source": [ - "swap_trials = 1000\n", - "\n", - "# Default PassManager with `SabreLayout` and `SabreSwap`, using heuristic \"decay\"\n", - "sr_default = SabreSwap(\n", - " coupling_map=cmap, heuristic=\"decay\", trials=swap_trials, seed=seed\n", - ")\n", - "sl_default = SabreLayout(\n", - " coupling_map=cmap, routing_pass=sr_default, seed=seed\n", - ")\n", - "pm_default = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")\n", - "pm_default.layout.replace(index=2, passes=sl_default)\n", - "pm_default.routing.replace(index=1, passes=sr_default)\n", + "# -------------------------Step 1-------------------------\n", "\n", - "t0 = time.time()\n", - "tqc_default = pm_default.run(qc)\n", - "t_default = time.time() - t0\n", - "size_default = tqc_default.size()\n", - "depth_default = tqc_default.depth(lambda x: x.operation.num_qubits == 2)\n", + "num_qubits = 100\n", "\n", + "# Create star-topology GHZ circuit\n", + "qc = QuantumCircuit(num_qubits)\n", + "qc.h(0)\n", + "for i in range(1, num_qubits):\n", + " qc.cx(0, i)\n", + "qc.measure_all()\n", "\n", - "# Custom PassManager with `SabreLayout` and `SabreSwap`, using heuristic \"lookahead\"\n", - "sr_custom = SabreSwap(\n", - " coupling_map=cmap, heuristic=\"lookahead\", trials=swap_trials, seed=seed\n", - ")\n", - "sl_custom = SabreLayout(coupling_map=cmap, routing_pass=sr_custom, seed=seed)\n", - "pm_custom = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")\n", - "pm_custom.layout.replace(index=2, passes=sl_custom)\n", - "pm_custom.routing.replace(index=1, passes=sr_custom)\n", - "\n", - "t0 = time.time()\n", - "tqc_custom = pm_custom.run(qc)\n", - "t_custom = time.time() - t0\n", - "size_custom = tqc_custom.size()\n", - "depth_custom = tqc_custom.depth(lambda x: x.operation.num_qubits == 2)\n", - "\n", - "print(\n", - " f\"Default (heuristic='decay') : Depth {depth_default}, Size {size_default}, Time {t_default}\"\n", - ")\n", - "print(\n", - " f\"Custom (heuristic='lookahead'): Depth {depth_custom}, Size {size_custom}, Time {t_custom}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "25d4519b-e41b-4d9f-9d3c-54e8252409d9", - "metadata": {}, - "source": [ - "Here we see that the `lookahead` heuristic performs better than the `decay` heuristic in terms of circuit depth, size, and time. This improvements highlights how we can improve SABRE beyond just trials and iterations for your specific circuit and hardware constraints. Note that these results are based on a single layout trial. To achieve more accurate results, we recommend running multiple layout trials, which can be done efficiently using the serverless runtime." - ] - }, - { - "cell_type": "markdown", - "id": "eff72eda-87e7-48db-93d5-94d06fe57c80", - "metadata": {}, - "source": [ - "### Results with serverless runtime (multiple layout trials)" - ] - }, - { - "cell_type": "markdown", - "id": "0d6b162b-8a78-4b7b-a497-def00672a816", - "metadata": {}, - "source": [ - "Qiskit Serverless requires setting up your workload’s `.py` files into a dedicated directory. The following code cell is a Python file in the `source_files` directory named `transpile_remote.py`. This file contains the function that runs the transpilation process." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "0ba37113-8e9b-4797-8e13-915310933100", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it makes sure the `source_files` directory exists\n", - "from pathlib import Path\n", - "\n", - "Path(\"source_files\").mkdir(exist_ok=True)" + "# ZZ operators\n", + "operator_strings = [\n", + " \"Z\" + \"I\" * i + \"Z\" + \"I\" * (num_qubits - 2 - i)\n", + " for i in range(num_qubits - 1)\n", + "]\n", + "operators = [SparsePauliOp(op) for op in operator_strings]" ] }, { "cell_type": "code", - "execution_count": 26, - "id": "7fc69c04-42c4-48f0-9ad5-736343b9d316", + "execution_count": 10, + "id": "43ec98c6-f4c6-4584-8ba0-edfb4e99f04a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Overwriting source_files/transpile_remote.py\n" + "basic:\n", + " 2Q depth: min: 524, mean: 570.5, std: 39.9\n", + " size : min: 3819, mean: 4227.1, std: 360.6\n", + " best seed: 51 (2Q depth=524, size=3852)\n", + "decay:\n", + " 2Q depth: min: 387, mean: 436.4, std: 41.7\n", + " size : min: 2687, mean: 3183.1, std: 459.3\n", + " best seed: 45 (2Q depth=387, size=2786)\n", + "lookahead:\n", + " 2Q depth: min: 364, mean: 424.6, std: 36.5\n", + " size : min: 2335, mean: 3014.6, std: 388.1\n", + " best seed: 51 (2Q depth=364, size=2485)\n", + "StarPreRouting:\n", + " 2Q depth: min: 196, mean: 196.0, std: 0.0\n", + " size : min: 1151, mean: 1151.0, std: 0.0\n", + " best seed: 42 (2Q depth=196, size=1151)\n" ] } ], "source": [ - "%%writefile source_files/transpile_remote.py\n", - "import time\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.transpiler.passes import SabreLayout, SabreSwap\n", - "from qiskit.transpiler import CouplingMap\n", - "from qiskit_serverless import get_arguments, save_result, distribute_task, get\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "# -------------------------Step 2-------------------------\n", "\n", - "@distribute_task(target={\n", - " \"cpu\": 1,\n", - " \"mem\": 1024 * 1024 * 1024\n", - "})\n", - "def transpile_remote(qc, optimization_level, backend_name, seed, swap_trials, heuristic):\n", - " \"\"\"Transpiles an abstract circuit into an ISA circuit for a given backend.\"\"\"\n", + "num_seeds = 10\n", + "seed_list = [seed + i for i in range(num_seeds)]\n", + "swap_trials = 200\n", "\n", - " service = QiskitRuntimeService()\n", - " backend = service.backend(backend_name)\n", "\n", - " pm = generate_preset_pass_manager(\n", - " optimization_level=optimization_level,\n", - " backend=backend,\n", - " seed_transpiler=seed\n", - " )\n", - "\n", - " # Changing the `SabreLayout` and `SabreSwap` passes to use the custom configurations\n", - " cmap = CouplingMap(backend().configuration().coupling_map)\n", - " sr = SabreSwap(coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=seed)\n", - " sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=seed)\n", - " pm.layout.replace(index=2, passes=sl)\n", - " pm.routing.replace(index=1, passes=sr)\n", - "\n", - " # Measure the transpile time\n", - " start_time = time.time() # Start timer\n", - " tqc = pm.run(qc) # Transpile the circuit\n", - " end_time = time.time() # End timer\n", - "\n", - " transpile_time = end_time - start_time # Calculate the elapsed time\n", - " return tqc, transpile_time # Return both the transpiled circuit and the transpile time\n", - "\n", - "\n", - "# Get program arguments\n", - "arguments = get_arguments()\n", - "circuit = arguments.get(\"circuit\")\n", - "backend_name = arguments.get(\"backend_name\")\n", - "optimization_level = arguments.get(\"optimization_level\")\n", - "seed_list = arguments.get(\"seed_list\")\n", - "swap_trials = arguments.get(\"swap_trials\")\n", - "heuristic = arguments.get(\"heuristic\")\n", - "\n", - "# Transpile the circuits\n", - "transpile_worker_references = [\n", - " transpile_remote(circuit, optimization_level, backend_name, seed, swap_trials, heuristic)\n", - " for seed in seed_list\n", - "]\n", - "\n", - "results_with_times = get(transpile_worker_references)\n", + "# The default routing[1] is a ConditionalController([barrier, routing_pass],\n", + "# condition=_swap_condition); we re-wrap so the new routing pass keeps the\n", + "# protective barrier and is skipped when routing isn't needed (matches the preset).\n", + "def _swap_condition(property_set):\n", + " return not property_set[\"routing_not_needed\"]\n", "\n", - "# Separate the transpiled circuits and their transpile times\n", - "transpiled_circuits = [result[0] for result in results_with_times]\n", - "transpile_times = [result[1] for result in results_with_times]\n", "\n", - "# Save both results and transpile times\n", - "save_result({\"transpiled_circuits\": transpiled_circuits, \"transpile_times\": transpile_times})" - ] - }, - { - "cell_type": "markdown", - "id": "fefed67d-3499-4049-a3fa-0920a8fe796d", - "metadata": {}, - "source": [ - "The following cell uploads the `transpile_remote.py` file as a Qiskit Serverless program under the name `transpile_remote_serverless`." - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "c467b63b-364f-4bdd-8e65-7a0c013e5d2c", - "metadata": {}, - "outputs": [], - "source": [ - "serverless = QiskitServerless()\n", + "def wrap_routing(routing_pass):\n", + " return ConditionalController(\n", + " [\n", + " BarrierBeforeFinalMeasurements(\n", + " \"qiskit.transpiler.internal.routing.protection.barrier\"\n", + " ),\n", + " routing_pass,\n", + " ],\n", + " condition=_swap_condition,\n", + " )\n", "\n", - "transpile_remote_demo = QiskitFunction(\n", - " title=\"transpile_remote_serverless\",\n", - " entrypoint=\"transpile_remote.py\",\n", - " working_dir=\"./source_files/\",\n", - ")\n", - "serverless.upload(transpile_remote_demo)\n", - "transpile_remote_serverless = serverless.load(\"transpile_remote_serverless\")" - ] - }, - { - "cell_type": "markdown", - "id": "1571a9ab-aeff-4033-b33f-0c4c6e760808", - "metadata": {}, - "source": [ - "Generate 20 different seeds to represent 20 different layout trials." - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "6bc1561c-e057-4247-9425-c29dfe7cc555", - "metadata": {}, - "outputs": [], - "source": [ - "num_seeds = 20 # represents the different layout trials\n", - "seed_list = [seed + i for i in range(num_seeds)]" - ] - }, - { - "cell_type": "markdown", - "id": "d2a3f146-49e9-468a-9793-2559a9182e1b", - "metadata": {}, - "source": [ - "Run the uploaded program and pass inputs for lookahead heuristic." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "95bad790-86e8-4cca-bfd5-b57b1e6a26c1", - "metadata": {}, - "outputs": [], - "source": [ - "job_lookahead = transpile_remote_serverless.run(\n", - " circuit=qc,\n", - " backend_name=backend.name,\n", - " optimization_level=3,\n", - " seed_list=seed_list,\n", - " swap_trials=swap_trials,\n", - " heuristic=\"lookahead\",\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "51409259-e359-4b20-90f6-e5c432c3d01a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'15767dfc-e71d-4720-94d6-9212f72334c2'" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job_lookahead.job_id" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "61049219-0189-4d0c-b717-3e74a1dd7455", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'QUEUED'" - ] - }, - "execution_count": 31, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job_lookahead.status()" - ] - }, - { - "cell_type": "markdown", - "id": "1254e7f4-f2b7-47a1-8373-36dfc4ef8752", - "metadata": {}, - "source": [ - "Receive the logs and results from the serverless runtime." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "46504d93-a0c8-4077-8075-07271f6c93b0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "No logs yet.\n" - ] - } - ], - "source": [ - "logs_lookahead = job_lookahead.logs()\n", - "print(logs_lookahead)" - ] - }, - { - "cell_type": "markdown", - "id": "a1d4c23a-7f11-491b-b61c-1aadc7351df6", - "metadata": {}, - "source": [ - "Once a program is `DONE`, you can use `job.results()` to fetch the result stored in `save_result()`." - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "61ce55f7-abec-458d-a0b8-f34d1e79f838", - "metadata": {}, - "outputs": [], - "source": [ - "# Run the job with lookahead heuristic\n", - "start_time = time.time()\n", - "results_lookahead = job_lookahead.result()\n", - "end_time = time.time()\n", "\n", - "job_lookahead_time = end_time - start_time" - ] - }, - { - "cell_type": "markdown", - "id": "5c7d6447-2492-4b7d-83a7-62052c6c982a", - "metadata": {}, - "source": [ - "Now perform the same for decay heuristic." - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "cc400305-3f34-46fe-95bf-fb672ae00954", - "metadata": {}, - "outputs": [], - "source": [ - "job_decay = transpile_remote_serverless.run(\n", - " circuit=qc,\n", - " backend_name=backend.name,\n", - " optimization_level=3,\n", - " seed_list=seed_list,\n", - " swap_trials=swap_trials,\n", - " heuristic=\"decay\",\n", - ")" + "heuristic_results = {}\n", + "\n", + "# Three SABRE heuristics, swept over seeds\n", + "for heuristic in [\"basic\", \"decay\", \"lookahead\"]:\n", + " trials = []\n", + " for s in seed_list:\n", + " sr = SabreSwap(\n", + " coupling_map=cmap, heuristic=heuristic, trials=swap_trials, seed=s\n", + " )\n", + " sl = SabreLayout(coupling_map=cmap, routing_pass=sr, seed=s)\n", + " pm = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=s\n", + " )\n", + " # Re-wrap each custom pass in its original ConditionalController + barrier\n", + " # (wrap_sabre is defined in the small-scale Step 2 cell above).\n", + " pm.layout.replace(index=2, passes=wrap_sabre(sl))\n", + " pm.routing.replace(index=1, passes=wrap_routing(sr))\n", + "\n", + " t0 = time.time()\n", + " tqc = pm.run(qc)\n", + " elapsed = time.time() - t0\n", + " depth = tqc.depth(lambda x: x.operation.num_qubits == 2)\n", + " size = tqc.size()\n", + " trials.append(\n", + " {\n", + " \"tqc\": tqc,\n", + " \"depth\": depth,\n", + " \"size\": size,\n", + " \"time\": elapsed,\n", + " \"seed\": s,\n", + " }\n", + " )\n", + "\n", + " heuristic_results[heuristic] = trials\n", + "\n", + "# Default preset + StarPreRouting in init, also swept over seeds for a fair comparison\n", + "star_trials = []\n", + "for s in seed_list:\n", + " pm_star_hw = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=s\n", + " )\n", + " pm_star_hw.init += StarPreRouting()\n", + "\n", + " t0 = time.time()\n", + " tqc = pm_star_hw.run(qc)\n", + " elapsed = time.time() - t0\n", + " depth = tqc.depth(lambda x: x.operation.num_qubits == 2)\n", + " size = tqc.size()\n", + " star_trials.append(\n", + " {\n", + " \"tqc\": tqc,\n", + " \"depth\": depth,\n", + " \"size\": size,\n", + " \"time\": elapsed,\n", + " \"seed\": s,\n", + " }\n", + " )\n", + "heuristic_results[\"StarPreRouting\"] = star_trials\n", + "\n", + "# Print summary for each entry\n", + "for label in [\"basic\", \"decay\", \"lookahead\", \"StarPreRouting\"]:\n", + " trials = heuristic_results[label]\n", + " depths = [t[\"depth\"] for t in trials]\n", + " sizes = [t[\"size\"] for t in trials]\n", + " best = min(trials, key=lambda t: t[\"depth\"])\n", + " print(f\"{label}:\")\n", + " print(\n", + " f\" 2Q depth: min: {min(depths)}, mean: {np.mean(depths):.1f}, std: {np.std(depths):.1f}\"\n", + " )\n", + " print(\n", + " f\" size : min: {min(sizes)}, mean: {np.mean(sizes):.1f}, std: {np.std(sizes):.1f}\"\n", + " )\n", + " print(\n", + " f\" best seed: {best['seed']} (2Q depth={best['depth']}, size={best['size']})\"\n", + " )" ] }, { "cell_type": "code", - "execution_count": 34, - "id": "da60e3e8-a65e-456c-988b-65ba6d16f65a", + "execution_count": 11, + "id": "eead9bd2-17e0-4f5b-80bc-eb9b30af052e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'00418c76-d6ec-4bd8-9f70-05d0fa14d4eb'" + "\"Output" ] }, - "execution_count": 34, "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "job_decay.job_id" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "b32d4bd0-6ffa-42b2-a827-e606cc79791d", - "metadata": {}, - "outputs": [ + "output_type": "display_data" + }, { "name": "stdout", "output_type": "stream", "text": [ - "No logs yet.\n" + "basic: best 2Q depth=524, size=3852 (seed=51)\n", + "decay: best 2Q depth=387, size=2786 (seed=45)\n", + "lookahead: best 2Q depth=364, size=2485 (seed=51)\n", + "StarPreRouting: best 2Q depth=196, size=1151 (seed=42)\n" ] } ], "source": [ - "logs_decay = job_decay.logs()\n", - "print(logs_decay)" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "2552cd11-8283-47c5-86c9-58541ee31a12", - "metadata": {}, - "outputs": [], - "source": [ - "# Run the job with the decay heuristic\n", - "start_time = time.time()\n", - "results_decay = job_decay.result()\n", - "end_time = time.time()\n", - "\n", - "job_decay_time = end_time - start_time" + "hw_colors = {\n", + " \"basic\": \"#ff7f0e\",\n", + " \"decay\": \"#d62728\",\n", + " \"lookahead\": \"#1f77b4\",\n", + " \"StarPreRouting\": \"#2a9d8f\",\n", + "}\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))\n", + "\n", + "for label in [\"basic\", \"decay\", \"lookahead\", \"StarPreRouting\"]:\n", + " trials = heuristic_results[label]\n", + " depths = [t[\"depth\"] for t in trials]\n", + " sizes = [t[\"size\"] for t in trials]\n", + " seeds = [t[\"seed\"] for t in trials]\n", + " color = hw_colors[label]\n", + "\n", + " ax1.scatter(\n", + " seeds,\n", + " depths,\n", + " label=label,\n", + " color=color,\n", + " alpha=0.8,\n", + " edgecolor=\"k\",\n", + " s=60,\n", + " )\n", + " ax1.axhline(np.mean(depths), color=color, linestyle=\"--\", alpha=0.5)\n", + "\n", + " ax2.scatter(\n", + " seeds,\n", + " sizes,\n", + " label=label,\n", + " color=color,\n", + " alpha=0.8,\n", + " edgecolor=\"k\",\n", + " s=60,\n", + " )\n", + " ax2.axhline(np.mean(sizes), color=color, linestyle=\"--\", alpha=0.5)\n", + "\n", + "ax1.set_xlabel(\"Seed\", fontsize=11)\n", + "ax1.set_ylabel(\"2Q Depth\", fontsize=11)\n", + "ax1.set_title(\"Two-Qubit Gate Depth per Seed\", fontsize=13)\n", + "ax1.legend(fontsize=10)\n", + "ax1.grid(alpha=0.3)\n", + "\n", + "ax2.set_xlabel(\"Seed\", fontsize=11)\n", + "ax2.set_ylabel(\"Gate Count\", fontsize=11)\n", + "ax2.set_title(\"Circuit Size per Seed\", fontsize=13)\n", + "ax2.legend(fontsize=10)\n", + "ax2.grid(alpha=0.3)\n", + "\n", + "plt.suptitle(\n", + " \"Transpilation variability across seeds: SABRE heuristics vs. StarPreRouting\",\n", + " fontsize=14,\n", + " fontweight=\"bold\",\n", + " y=1.02,\n", + ")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Summary comparison\n", + "for label in [\"basic\", \"decay\", \"lookahead\", \"StarPreRouting\"]:\n", + " best = min(heuristic_results[label], key=lambda t: t[\"depth\"])\n", + " print(\n", + " f\"{label}: best 2Q depth={best['depth']}, size={best['size']} (seed={best['seed']})\"\n", + " )" ] }, { "cell_type": "code", - "execution_count": 37, - "id": "fa620c66-c6bd-4e8e-9fe1-46fd614b24fc", + "execution_count": 12, + "id": "5ad47245-41d0-4d90-ba94-dda4cd63705d", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "=== Total Transpilation Time (Serial Execution) ===\n", - "Decay Heuristic : 112.37 seconds\n", - "Lookahead Heuristic: 85.37 seconds\n", - "\n", - "=== Serverless Job Time (Parallel Execution) ===\n", - "Decay Heuristic : 5.72 seconds\n", - "Lookahead Heuristic: 5.85 seconds\n", - "\n", - "=== Average Time Per Transpilation ===\n", - "Decay Heuristic (Serial) : 5.62 seconds\n", - "Decay Heuristic (Serverless): 0.29 seconds\n", - "Lookahead Heuristic (Serial) : 4.27 seconds\n", - "Lookahead Heuristic (Serverless): 0.29 seconds\n", - "\n", - "=== Serverless Improvement ===\n", - "Decay Heuristic : 94.91%\n", - "Lookahead Heuristic: 93.14%\n" + "Best basic: 2Q depth=524, size=3852\n", + "Best decay: 2Q depth=387, size=2786\n", + "Best lookahead: 2Q depth=364, size=2485\n", + "Best StarPreRouting: 2Q depth=196, size=1151\n", + "basic job: d81q5tnoha1c73bknprg\n", + "decay job: d81q5tugbeec73aktopg\n", + "lookahead job: d81q5to0bvlc73d1epe0\n", + "StarPreRouting job: d81q5u7tjchs73bn82hg\n", + "basic job done\n", + "decay job done\n", + "lookahead job done\n", + "StarPreRouting job done\n" ] } ], "source": [ - "# Extract transpilation times\n", - "transpile_times_decay = results_decay[\"transpile_times\"]\n", - "transpile_times_lookahead = results_lookahead[\"transpile_times\"]\n", - "\n", - "# Calculate total transpilation time for serial execution\n", - "total_transpile_time_decay = sum(transpile_times_decay)\n", - "total_transpile_time_lookahead = sum(transpile_times_lookahead)\n", - "\n", - "# Print total transpilation time\n", - "print(\"=== Total Transpilation Time (Serial Execution) ===\")\n", - "print(f\"Decay Heuristic : {total_transpile_time_decay:.2f} seconds\")\n", - "print(f\"Lookahead Heuristic: {total_transpile_time_lookahead:.2f} seconds\")\n", - "\n", - "# Print serverless job time (parallel execution)\n", - "print(\"\\n=== Serverless Job Time (Parallel Execution) ===\")\n", - "print(f\"Decay Heuristic : {job_decay_time:.2f} seconds\")\n", - "print(f\"Lookahead Heuristic: {job_lookahead_time:.2f} seconds\")\n", - "\n", - "# Calculate and print average runtime per transpilation\n", - "avg_transpile_time_decay = total_transpile_time_decay / num_seeds\n", - "avg_transpile_time_lookahead = total_transpile_time_lookahead / num_seeds\n", - "avg_job_time_decay = job_decay_time / num_seeds\n", - "avg_job_time_lookahead = job_lookahead_time / num_seeds\n", - "\n", - "print(\"\\n=== Average Time Per Transpilation ===\")\n", - "print(f\"Decay Heuristic (Serial) : {avg_transpile_time_decay:.2f} seconds\")\n", - "print(f\"Decay Heuristic (Serverless): {avg_job_time_decay:.2f} seconds\")\n", - "print(\n", - " f\"Lookahead Heuristic (Serial) : {avg_transpile_time_lookahead:.2f} seconds\"\n", - ")\n", - "print(\n", - " f\"Lookahead Heuristic (Serverless): {avg_job_time_lookahead:.2f} seconds\"\n", - ")\n", + "# -------------------------Step 3: Execute on hardware-------------------------\n", "\n", - "# Calculate and print serverless improvement percentage\n", - "decay_improvement_percentage = (\n", - " (total_transpile_time_decay - job_decay_time) / total_transpile_time_decay\n", - ") * 100\n", - "lookahead_improvement_percentage = (\n", - " (total_transpile_time_lookahead - job_lookahead_time)\n", - " / total_transpile_time_lookahead\n", - ") * 100\n", - "\n", - "print(\"\\n=== Serverless Improvement ===\")\n", - "print(f\"Decay Heuristic : {decay_improvement_percentage:.2f}%\")\n", - "print(f\"Lookahead Heuristic: {lookahead_improvement_percentage:.2f}%\")" - ] - }, - { - "cell_type": "markdown", - "id": "5296f4b5-1ac7-497e-979f-ea5eaea7448a", - "metadata": {}, - "source": [ - "These results demonstrate the substantial efficiency gains from using serverless execution for quantum circuit transpilation. Compared to serial execution, serverless execution dramatically reduces overall runtime for both the `decay` and `lookahead` heuristics by parallelizing independent transpilation trials. While serial execution reflects the full cumulative cost of exploring multiple layout trials, the serverless job times highlight how parallel execution collapses this cost into a much shorter wall-clock time. As a result, the effective time per transpilation is reduced to a small fraction of that required in the serial setting, largely independent of the heuristic used. This capability is particularly important for optimizing SABRE to its fullest potential. Many of SABRE’s strongest performance gains come from increasing the number of layout and routing trials, which can be prohibitively expensive when executed sequentially. Serverless execution removes this bottleneck, enabling large-scale parameter sweeps and deeper exploration of heuristic configurations with minimal overhead.\n", + "best_circuits = {}\n", + "for label in [\"basic\", \"decay\", \"lookahead\", \"StarPreRouting\"]:\n", + " best_circuits[label] = min(\n", + " heuristic_results[label], key=lambda t: t[\"depth\"]\n", + " )\n", + " b = best_circuits[label]\n", + " print(f\"Best {label}: 2Q depth={b['depth']}, size={b['size']}\")\n", "\n", - "Overall, these findings show that serverless execution is key to scaling SABRE optimization, making aggressive experimentation and refinement practical compared to serial execution." - ] - }, - { - "cell_type": "markdown", - "id": "e4f41118-cfb9-4aa3-8aba-201ceadd2195", - "metadata": {}, - "source": [ - "Obtain the results from the serverless runtime and compare the results of the lookahead and decay heuristics. We will compare the sizes and depths." + "options = EstimatorOptions()\n", + "options.resilience_level = 2\n", + "options.dynamical_decoupling.enable = True\n", + "options.dynamical_decoupling.sequence_type = \"XY4\"\n", + "estimator = Estimator(backend, options=options)\n", + "\n", + "hw_jobs = {}\n", + "hw_ops = {}\n", + "for label, best in best_circuits.items():\n", + " hw_ops[label] = [op.apply_layout(best[\"tqc\"].layout) for op in operators]\n", + " hw_jobs[label] = estimator.run([(best[\"tqc\"], hw_ops[label])])\n", + " print(f\"{label} job: {hw_jobs[label].job_id()}\")\n", + "estimator.options.environment.job_tags = [\"TUT_TOWS\"]\n", + "\n", + "hw_results = {}\n", + "for label, job in hw_jobs.items():\n", + " hw_results[label] = job.result()[0]\n", + " print(f\"{label} job done\")" ] }, { "cell_type": "code", - "execution_count": 38, - "id": "4cf9588b-8ea6-4761-b544-14bef8f0be85", + "execution_count": 13, + "id": "0280b0b9-6320-43e5-8396-f82f9e718319", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, "metadata": {}, "output_type": "display_data" }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Extract sizes and depths\n", - "sizes_lookahead = [\n", - " circuit.size() for circuit in results_lookahead[\"transpiled_circuits\"]\n", - "]\n", - "depths_lookahead = [\n", - " circuit.depth(lambda x: x.operation.num_qubits == 2)\n", - " for circuit in results_lookahead[\"transpiled_circuits\"]\n", - "]\n", - "sizes_decay = [\n", - " circuit.size() for circuit in results_decay[\"transpiled_circuits\"]\n", - "]\n", - "depths_decay = [\n", - " circuit.depth(lambda x: x.operation.num_qubits == 2)\n", - " for circuit in results_decay[\"transpiled_circuits\"]\n", - "]\n", - "\n", - "\n", - "def create_scatterplot(x, y1, y2, xlabel, ylabel, title, labels, colors):\n", - " plt.figure(figsize=(8, 5))\n", - " plt.scatter(\n", - " x, y1, label=labels[0], color=colors[0], alpha=0.8, edgecolor=\"k\"\n", - " )\n", - " plt.scatter(\n", - " x, y2, label=labels[1], color=colors[1], alpha=0.8, edgecolor=\"k\"\n", - " )\n", - " plt.xlabel(xlabel, fontsize=12)\n", - " plt.ylabel(ylabel, fontsize=12)\n", - " plt.title(title, fontsize=14)\n", - " plt.legend(fontsize=10)\n", - " plt.grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - " plt.tight_layout()\n", - " plt.show()\n", - "\n", - "\n", - "create_scatterplot(\n", - " seed_list,\n", - " sizes_lookahead,\n", - " sizes_decay,\n", - " \"Seed\",\n", - " \"Size\",\n", - " \"Circuit Size\",\n", - " [\"lookahead\", \"Decay\"],\n", - " [\"blue\", \"red\"],\n", - ")\n", - "create_scatterplot(\n", - " seed_list,\n", - " depths_lookahead,\n", - " depths_decay,\n", - " \"Seed\",\n", - " \"Depth\",\n", - " \"Circuit Depth\",\n", - " [\"lookahead\", \"Decay\"],\n", - " [\"blue\", \"red\"],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "9f905f0d-f439-4a7c-9006-be7f292713fc", - "metadata": {}, - "source": [ - "Each point in the scatter plots above represents a layout trial, with the x-axis indicating the circuit depth and the y-axis indicating the circuit size. The results reveal that the lookahead heuristic generally outperforms the decay heuristic in minimizing circuit depth and circuit size. In practical applications, the goal is to identify the optimal layout trial for your chosen heuristic, whether prioritizing depth or size. This can be achieved by selecting the trial with the lowest value for the desired metric. Importantly, increasing the number of layout trials improves the chances of achieving a better result in terms of size or depth, but it comes at the cost of higher computational overhead." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "e0db89b9-171a-473b-bcb4-19a65b75155c", - "metadata": {}, - "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Lookahead: Min Depth 399 Min Size 2452\n", - "Decay: Min Depth 415 Min Size 2611\n" + "\n", + "Mean fidelity:\n", + " basic: 0.0344\n", + " decay: 0.1298\n", + " lookahead: 0.1857\n", + " StarPreRouting: 0.3295\n" ] } ], "source": [ - "min_depth_lookahead = min(depths_lookahead)\n", - "min_depth_decay = min(depths_decay)\n", - "min_size_lookahead = min(sizes_lookahead)\n", - "min_size_decay = min(sizes_decay)\n", - "print(\n", - " \"Lookahead: Min Depth\",\n", - " min_depth_lookahead,\n", - " \"Min Size\",\n", - " min_size_lookahead,\n", + "# -------------------------Step 4: Post-process-------------------------\n", + "\n", + "data = list(range(1, len(operators) + 1))\n", + "hw_markers = {\n", + " \"basic\": \"D\",\n", + " \"decay\": \"o\",\n", + " \"lookahead\": \"s\",\n", + " \"StarPreRouting\": \"*\",\n", + "}\n", + "hw_labels = [\"basic\", \"decay\", \"lookahead\", \"StarPreRouting\"]\n", + "\n", + "fig, (ax1, ax2) = plt.subplots(\n", + " 1, 2, figsize=(14, 5), gridspec_kw={\"width_ratios\": [2.5, 1]}\n", ")\n", - "print(\"Decay: Min Depth\", min_depth_decay, \"Min Size\", min_size_decay)" + "\n", + "# Left: correlations vs distance\n", + "for label in hw_labels:\n", + " evs = list(hw_results[label].data.evs)\n", + " b = best_circuits[label]\n", + " ax1.plot(\n", + " data,\n", + " evs,\n", + " marker=hw_markers[label],\n", + " color=hw_colors[label],\n", + " linewidth=2,\n", + " label=f\"{label} (2Q depth={b['depth']}, size={b['size']})\",\n", + " markersize=5 if label == \"StarPreRouting\" else 4,\n", + " )\n", + "\n", + "ax1.set_xlabel(\"Distance between qubits $i$\", fontsize=11)\n", + "ax1.set_ylabel(r\"$\\langle Z_0 Z_i \\rangle$\", fontsize=11)\n", + "ax1.set_title(\n", + " \"Entanglement correlations vs. qubit distance (hardware)\", fontsize=12\n", + ")\n", + "ax1.legend(fontsize=9)\n", + "ax1.grid(alpha=0.3)\n", + "\n", + "# Right: mean fidelity bar chart\n", + "hw_means = [np.mean(list(hw_results[label].data.evs)) for label in hw_labels]\n", + "hw_bar_colors = [hw_colors[label] for label in hw_labels]\n", + "x_bar = np.arange(len(hw_labels))\n", + "bars = ax2.bar(x_bar, hw_means, color=hw_bar_colors)\n", + "ax2.set_ylabel(r\"Mean $\\langle Z_0 Z_i \\rangle$\", fontsize=11)\n", + "ax2.set_title(\"Average fidelity\", fontsize=13)\n", + "y_range = (\n", + " max(hw_means) - min(hw_means) if max(hw_means) != min(hw_means) else 0.01\n", + ")\n", + "ax2.set_ylim(min(hw_means) - y_range * 0.2, max(hw_means) + y_range * 0.15)\n", + "for bar, val in zip(bars, hw_means):\n", + " ax2.text(\n", + " bar.get_x() + bar.get_width() / 2,\n", + " bar.get_height() + y_range * 0.05,\n", + " f\"{val:.4f}\",\n", + " ha=\"center\",\n", + " va=\"bottom\",\n", + " fontsize=11,\n", + " fontweight=\"bold\",\n", + " )\n", + "ax2.set_xticks(x_bar)\n", + "ax2.set_xticklabels(hw_labels, fontsize=9, rotation=15)\n", + "ax2.grid(axis=\"y\", linestyle=\"--\", alpha=0.5)\n", + "\n", + "fig.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"\\nMean fidelity:\")\n", + "for label, m in zip(hw_labels, hw_means):\n", + " print(f\" {label}: {m:.4f}\")" ] }, { "cell_type": "markdown", - "id": "762c6b28-a968-4d0d-8a06-7e5a19197d0d", + "id": "151e5fe9-872a-4b89-92f9-85bb883de17b", "metadata": {}, "source": [ - "In our initial comparison using a single layout trial, the lookahead heuristic showed slightly better performance in both circuit depth and size. By extending this study to multiple layout trials using `QiskitServerless`, we were able to explore a much broader space of SABRE initializations, enabling a more representative comparison between heuristics.\n", + "### Analysis\n", "\n", - "From the scatter plots and the best observed results, it is clear that performance varies significantly with the random seed used by SABRE. Both heuristics exhibit a wide spread in circuit depth and size across seeds, indicating that a single run is often insufficient to capture near-optimal results. This variability highlights the importance of running many trials with different seeds when aiming to minimize depth and/or gate count. Across the full set of trials, both the `lookahead` and `decay` heuristics were capable of producing competitive results. In some cases, the `decay` heuristic matched or even outperformed `lookahead` for specific seeds. However, for this particular circuit, the best overall results were obtained using the lookahead heuristic, albeit by a modest margin. This suggests that while lookahead provided the strongest outcome here, its advantage over decay is not absolute.\n", + "The scatter plots show significant variability across seeds for all three SABRE heuristics, which underscores the importance of running multiple layout trials rather than relying on a single transpilation. The `StarPreRouting` line is essentially flat across seeds because the rewrite from a star into a linear chain is deterministic given the structure; the downstream SABRE routing then has very little freedom on a linear chain, so the seed has almost no effect on the final depth or size.\n", "\n", - "Overall, these results reinforce two key points. First, leveraging many seeds is essential for extracting the best possible performance from SABRE, regardless of the heuristic used. Second, while heuristic choice matters, circuit structure plays a dominant role, and the relative performance of `lookahead` and `decay` may differ for other circuits. As such, large-scale, multi-seed experimentation is critical for robust and effective quantum circuit transpilation." - ] - }, - { - "cell_type": "code", - "execution_count": 40, - "id": "42343aca-1327-4ac8-bbd0-880aa916a091", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users, it cleans up the `source_files` directory\n", - "from pathlib import Path\n", + "From the transpilation results, both the `decay` and `lookahead` heuristics consistently outperform `basic` by a wide margin. The `basic` heuristic, while fast, uses a simple greedy strategy that often leads to substantially deeper circuits. For this star-topology GHZ circuit, `lookahead` tends to produce the lowest 2Q depth and gate count among the SABRE heuristics, since its forward-looking cost function is well suited to circuits with long-range connectivity patterns. `StarPreRouting`, however, dwarfs all three by a substantial margin: by rewriting the star into a linear chain before routing, it short-circuits the search problem entirely and delivers a circuit that the rest of the transpiler can map onto a linear path with minimal additional SWAPs.\n", + "\n", + "That advantage carries straight over to hardware fidelity. Lower 2Q depth and gate count do not always translate one-for-one to higher fidelity (the specific physical qubits a layout uses and their calibration at run time also matter), but when the depth gap is as large as the one between SABRE and `StarPreRouting` here, the structure-aware approach wins decisively because the circuit accumulates far less decoherence and far fewer two-qubit error events. The fidelity bar chart shows `StarPreRouting` substantially ahead of even the best SABRE heuristic, while `basic` sits well below the rest because its much deeper circuits accumulate the most error.\n", "\n", - "Path(\"source_files/transpile_remote.py\").unlink()\n", - "Path(\"source_files\").rmdir()" + "**Key takeaways:**\n", + "- Among SABRE heuristics, `decay` and `lookahead` are substantially better than `basic` for non-trivial circuits. Prefer one of the two for production workloads.\n", + "- The best SABRE heuristic depends on your circuit and hardware. Testing multiple heuristics with multiple seeds is the most reliable strategy.\n", + "- If you want to explore even more layouts, increase `swap_trials` (and `layout_trials` when you are not pinning a custom routing pass) rather than fanning the work out to remote nodes. The SABRE passes already parallelize trials across local threads, and the per-trial work is small enough that distribution overhead typically dominates any speedup.\n", + "- When the circuit has a known special structure, applying a structure-aware pass like `StarPreRouting` before SABRE can deliver an order-of-magnitude improvement that no amount of SABRE tuning will match. This is not a replacement for SABRE: `StarPreRouting` only helps when the circuit actually contains star sub-circuits and the backend has a long enough linear path. It is worth checking the pass library for matches whenever you know your circuit's shape." ] }, { "cell_type": "markdown", - "id": "b2c5e4ad-cc9f-4090-a513-164bc4a46b85", + "id": "65c2cebe-50b9-4304-9123-bf4cea7ecff6", "metadata": {}, "source": [ - "## Conclusion\n", - "\n", - "In this tutorial, we explored how to optimize large circuits using SABRE in Qiskit. We demonstrated how to configure the `SabreLayout` pass with different parameters to balance circuit quality and transpilation runtime. We also showed how to customize the routing heuristic in SABRE and use the `QiskitServerless`runtime to parallelize layout trials efficiently for when `SabreSwap` is involved. By adjusting these parameters and heuristics, you can optimize the layout and routing of large circuits, ensuring they are executed efficiently on quantum hardware." + "## Next steps\n", + "If you found this work interesting, you might be interested in the following material:\n", + "\n", + "- [`SabreLayout` API reference](/docs/api/qiskit/qiskit.transpiler.passes.SabreLayout): full parameter documentation\n", + "- [SABRE paper](https://arxiv.org/abs/1809.02573): the original SABRE algorithm for layout and routing\n", + "- [LightSABRE paper](https://arxiv.org/abs/2409.08368): the algorithmic improvements that power Qiskit's current SABRE implementation\n", + "- [Write a custom transpiler pass](/docs/guides/custom-transpiler-pass): build your own transpilation logic\n", + "- [Transpiler plugins](/docs/guides/transpiler-plugins): extend Qiskit's transpilation pipeline with third-party passes\n", + "- [DAG representation](/docs/guides/DAG-representation): understand the directed acyclic graph used internally by the transpiler\n", + "" ] }, { "cell_type": "markdown", - "id": "b21ac273-966f-41c2-a54e-1bb76072b2fe", + "id": "42d1c053-1683-4c32-be1b-a36602207f74", "metadata": {}, "source": [ "## Tutorial survey\n", diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/0280b0b9-6320-43e5-8396-f82f9e718319-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/0280b0b9-6320-43e5-8396-f82f9e718319-0.avif new file mode 100644 index 00000000000..7a5d845b496 Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/0280b0b9-6320-43e5-8396-f82f9e718319-0.avif differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-0.avif deleted file mode 100644 index 6c2bd6b4988..00000000000 Binary files a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-1.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-1.avif deleted file mode 100644 index be637d9785b..00000000000 Binary files a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/4cf9588b-8ea6-4761-b544-14bef8f0be85-1.avif and /dev/null differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/79075a21-8f36-4fd9-9d0d-bd0e97395b60-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/79075a21-8f36-4fd9-9d0d-bd0e97395b60-0.avif new file mode 100644 index 00000000000..42a4dd54068 Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/79075a21-8f36-4fd9-9d0d-bd0e97395b60-0.avif differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/818a8997-d2c7-4661-a6ea-f58eac376bf8-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/818a8997-d2c7-4661-a6ea-f58eac376bf8-0.avif deleted file mode 100644 index de3bd8c5500..00000000000 Binary files a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/818a8997-d2c7-4661-a6ea-f58eac376bf8-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/a6dac5ed-a963-458a-ada1-89c915f036e0-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/a6dac5ed-a963-458a-ada1-89c915f036e0-0.avif new file mode 100644 index 00000000000..91f458f5c1e Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/a6dac5ed-a963-458a-ada1-89c915f036e0-0.avif differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/b40fe1e0-41cd-4e8b-acb9-801872d35f1f-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/b40fe1e0-41cd-4e8b-acb9-801872d35f1f-0.avif new file mode 100644 index 00000000000..42a4dd54068 Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/b40fe1e0-41cd-4e8b-acb9-801872d35f1f-0.avif differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bc6cb36f-4bf2-4275-baf5-9557fcba520a-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bc6cb36f-4bf2-4275-baf5-9557fcba520a-0.avif deleted file mode 100644 index 24dcd16ac17..00000000000 Binary files a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bc6cb36f-4bf2-4275-baf5-9557fcba520a-0.avif and /dev/null differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb-0.avif new file mode 100644 index 00000000000..5edaf8e15ed Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb-0.avif differ diff --git a/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/eead9bd2-17e0-4f5b-80bc-eb9b30af052e-0.avif b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/eead9bd2-17e0-4f5b-80bc-eb9b30af052e-0.avif new file mode 100644 index 00000000000..9c20fb1bf89 Binary files /dev/null and b/public/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/eead9bd2-17e0-4f5b-80bc-eb9b30af052e-0.avif differ