{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "1ed79d34-1831-47cb-9985-5bb7dacc70e0",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Optimierung der Transpilation mit SABRE\"\n",
        "description: \"Optimierung von Quantenschaltungen mithilfe des SABRE-Layouts und -Routings für die Ausführung auf groß angelegter Hardware.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore ylabel xlabel fontsize sharex edgecolor, fontweight, elinewidth, ecolor, prerouter, ylim */}\n",
        "\n",
        "<span id=\"transpilation-optimization-with-sabre\" />\n",
        "\n",
        "# Optimierung der Transpilation mit SABRE\n",
        "\n",
        "*Geschätzte Laufzeit: 1 Minute auf einem Heron- r2 -Prozessor (HINWEIS: Dies ist lediglich eine Schätzung. (Die Laufzeit kann variieren.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d6834016-1525-42d7-aa21-0fef9d957ecd",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Lernziele\n",
        "\n",
        "* So konfigurieren Sie die SABRE-Parameter (`layout_trials`, `swap_trials`, `max_iterations`), um die Transpilationsqualität zu verbessern\n",
        "* Die Abwägung zwischen Transpilationslaufzeit und Schaltungsqualität (Tiefe und Gate-Anzahl)\n",
        "* So passen Sie die SABRE-Routing-Heuristik (`basic`, `decay`, `lookahead`) an und vergleichen deren Leistung auf der Hardware\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Voraussetzungen\n",
        "\n",
        "* [Transpile-Schaltkreise](/docs/guides/transpile) : Überblick über die Transpilierung in Qiskit\n",
        "* [Transpiler-Phasen](/docs/guides/transpiler-stages) : Layout- und Routing-Phasen\n",
        "* [Voreingestellte Pass-Manager konfigurieren](/docs/guides/transpile-with-pass-managers) : Optimierungsstufen anpassen\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Hintergrund\n",
        "\n",
        "Bei der Transpilation werden Quantenschaltungen in Formen umgewandelt, die mit bestimmter Quantenhardware kompatibel sind. Zwei wichtige Schritte sind die Wahl eines **Qubit-Layouts** (die Zuordnung logischer Qubits zu physikalischen Qubits) und **das Gate-Routing** (das Einfügen von SWAP-Gates, damit Multi-Qubit-Gates die Konnektivität des Bauelements berücksichtigen).\n",
        "\n",
        "**SABRE** ( *SWAP-basierter bidirektionaler heuristischer Suchalgorithmus* ) optimiert sowohl das Layout als auch die Verdrahtung. Es ist besonders effektiv für groß angelegte Schaltkreise (100+ Qubits) auf Geräten mit komplexen Kopplungsabbildungen, wie beispielsweise den Heron-Prozessoren von IBM®. SABRE minimiert die Anzahl der SWAP-Gatter und verringert die Schaltungstiefe, wodurch die Ausführungsgenauigkeit verbessert wird. Die jüngsten Verbesserungen am Algorithmus „ [LightSABRE](https://arxiv.org/abs/2409.08368) “ führen zu einer weiteren Verkürzung der Laufzeiten und einer weiteren Verringerung der Gatteranzahl.\n",
        "\n",
        "In diesem Tutorial werden Sie zunächst verschiedene Parameter konfigurieren `SabreLayout` , um eine kleine GHZ-Schaltung zu optimieren, und die Auswirkungen auf die Ausführungsgenauigkeit beobachten. Anschließend werden Sie die Routing-Heuristiken von SABRE in großem Maßstab auf echter Hardware vergleichen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "50cf9d9f-c875-49d5-83b0-c691363615ab",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Anforderungen\n",
        "\n",
        "Bevor Sie mit diesem Tutorial beginnen, sollten Sie sicherstellen, dass Sie Folgendes installiert haben:\n",
        "\n",
        "* Qiskit SDK v2.0 oder höher, mit [Visual](/docs/api/qiskit/visualization) isierungsunterstützung\n",
        "* Qiskit Runtime v0.22 oder später (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer (`pip install qiskit-aer`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6d9ad2c3-1b10-4a22-8b75-608869be0ac7",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Einrichtung\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "6d93c123-b92e-434d-8361-8115509e6d5f",
      "metadata": {},
      "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_runtime import QiskitRuntimeService\n",
        "from qiskit_ibm_runtime import EstimatorOptions\n",
        "from qiskit_ibm_runtime import EstimatorV2 as Estimator\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\n",
        "\n",
        "seed = 42\n",
        "\n",
        "service = QiskitRuntimeService(\n",
        "    channel=\"ibm_cloud\",\n",
        "    token=\"<YOUR_API_TOKEN>\",  # Replace with your actual API token\n",
        "    instance=\"<YOUR_INSTANCE_NAME>\",  # 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": "c34842c6-5e1b-4658-8e91-15149280c783",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Beispiel für einen Simulator im kleinen Maßstab\n",
        "\n",
        "In diesem Abschnitt wird ein auf dem Rauschmodell des realen Backends basierender **Rauschsimulator** verwendet, um zu veranschaulichen, wie sich verschiedene `SabreLayout` Konfigurationen sowohl auf die Transpilationsqualität als auch auf die Ausführungsgenauigkeit auswirken. Durch `qiskit_aer` die Verwendung eines Rauschmodells, das auf tatsächlichen Hardware-Kalibrierungsdaten basiert, können Sie die Transpilierung testen, ohne Hardware-Credits zu verbrauchen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a2bcc2b2-727b-447f-b680-a23e3b9e0c3c",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "Wir konstruieren einen **GHZ-Schaltkreis** mit Sterntopologie und 15 Qubits. Das erste Qubit bildet den Knotenpunkt, wobei es über CNOT-Gatter direkt mit jedem anderen Qubit verbunden ist. Diese Topologie stellt ein anspruchsvolles Layout-Problem dar, da sie sich nicht ohne Weiteres auf die Kopplungskarte des Bauteils abbilden lässt.\n",
        "\n",
        "Außerdem definieren `ZZ` wir Operatoren zur Messung von Verschränkungs-Korrelationen $\\langle Z_0 Z_i \\rangle$ zwischen Qubit-Paaren.\n",
        "\n",
        "![ghz\\_star\\_topology.png](https://quantum.cloud.ibm.com/docs/images/tutorials/transpilation-optimizations-with-sabre/ghz_star_topology.avif)\n",
        "\n",
        "<Admonition type=\"note\" title=\"Wenn man den Schaltplan kennt\">\n",
        "  SABRE ist ein universeller Algorithmus und stellt keine Annahmen hinsichtlich der Schaltungsstruktur auf. Für diese GHZ-Schaltung mit Sterntopologie ist tatsächlich eine optimale Verlegung bekannt: Der [`StarPreRouting`](/docs/api/qiskit/qiskit.transpiler.passes.StarPreRouting) „Pass“ erkennt Stern-Teilstromkreise und wandelt sie in eine lineare Kette um, die sich direkt auf jedes Backend mit einem ausreichend langen linearen Pfad abbilden lässt. Dieses Tutorial konzentriert sich auf SABRE, da es für beliebige Schaltungen funktioniert. Wenn Sie jedoch wissen, dass Ihre Schaltung eine eindeutige spezielle Struktur aufweist, kann die Anwendung eines spezialisierten Durchlaufs wie `StarPreRouting` vor dem Routing eine bessere Leistung erzielen als jede heuristische Suche.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "08a8c4df-b1a5-45b7-9808-ca4e5d7631d8",
      "metadata": {},
      "outputs": [],
      "source": [
        "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",
        "operators_sim = [SparsePauliOp(op) for op in operator_strings_sim]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "25ac8d92-6362-4d4e-a133-2914703df658",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "### Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware\n",
        "\n",
        "`SabreLayout`Der standardmäßige `optimization_level=3` Pass-Manager verwendet bereits diese Einstellung, allerdings mit konservativen Standardwerten. Um die Auswirkungen strengerer Einstellungen zu untersuchen, wird dieser Durchlauf durch einen benutzerdefinierten `SabreLayout` ersetzt, der für eine aggressivere Suche konfiguriert ist, während alle anderen Durchläufe in der Layout-Phase unverändert bleiben. Als separaten Vergleichspunkt behält ein „Fourth-Pass-Manager“ die Standardeinstellung `SabreLayout` bei, fügt [`StarPreRouting`](/docs/api/qiskit/qiskit.transpiler.passes.StarPreRouting) jedoch der Initialisierungsphase etwas hinzu. `StarPreRouting` ist ein *strukturorientierter* Durchlauf, der Stern-Teilschaltungen erkennt und diese vor dem Routing in eine lineare Kette umschreibt.\n",
        "\n",
        "Der Arbeitsablauf sieht wie folgt aus:\n",
        "\n",
        "1. **Überprüfen Sie** den Standard-Pass-Manager, um festzustellen, wo `SabreLayout` er sich innerhalb der `layout` Phase befindet.\n",
        "2. **Ersetzen Sie** diesen Durchlauf durch eine benutzerdefinierte `SabreLayout` Instanz mithilfe von `PassManager.replace(index, passes=...)`und erstellen Sie die `pm_star` Variante mit `pm.init += StarPreRouting()`.\n",
        "3. **Führen Sie** alle vier Pass-Manager aus und vergleichen Sie die Kennzahlen.\n",
        "\n",
        "Die vier Konfigurationen sind:\n",
        "\n",
        "| Konfiguration    | Beschreibung                                                                                                         |\n",
        "| ---------------- | -------------------------------------------------------------------------------------------------------------------- |\n",
        "| `pm_1`(Standard) | Standard-Voreinstellung für „ level-3 “ (`SabreLayout` mit `max_iterations=4`, `layout_trials=20`, `swap_trials=20`) |\n",
        "| `pm_2`           | Benutzerdefiniert `SabreLayout` (`max_iterations=4`, `layout_trials=200`, `swap_trials=200`)                         |\n",
        "| `pm_3`           | Benutzerdefiniert `SabreLayout` (`max_iterations=8`, `layout_trials=200`, `swap_trials=200`)                         |\n",
        "| `pm_star`        | Standardvoreinstellung mit `StarPreRouting` in der Initialisierungsphase hinzugefügt                                 |\n",
        "\n",
        "**Wichtige SABRE-Parameter:**\n",
        "\n",
        "* **`layout_trials` / `swap_trials`**: Legt fest, wie viele mögliche Layouts und Routing-Lösungen SABRE prüft. Durch die Erhöhung der Anzahl der Versuche deckt SABRE einen größeren Suchraum ab, wodurch sich die Chance erhöht, eine bessere Lösung zu finden.\n",
        "* **`max_iterations`**: Legt fest, wie viele Vorwärts-Rückwärts-Routing-Verfeinerungszyklen SABRE für jeden Kandidaten durchführt. SABRE optimiert das Layout schrittweise, indem es aus dem Routing-Feedback lernt. Je mehr Iterationen durchgeführt werden, desto besser fallen die Verbesserungen aus.\n",
        "\n",
        "Beides geht zwar mit einer längeren Transpilationszeit einher, doch die daraus resultierenden Schaltungen sind kürzer und verwenden weniger Gatter, was die Dekohärenz und die Gatterfehler auf der tatsächlichen Hardware direkt verringert.\n",
        "\n",
        "**Schritt 2a: Überprüfen Sie den Standard-Passwortmanager.** `PassManager`A `StagedPassManager` besteht aus den Teilmengen (`init`, `layout`, `routing`, `translation`, `optimization`, `scheduling`), von denen jede selbst ein ist. Wenn man eine Funktion `.draw()` auf einer Bühne aufruft, werden ihre Durchläufe als Graph dargestellt, sodass wir sehen können, wo `SabreLayout` sie sich befindet.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "b40fe1e0-41cd-4e8b-acb9-801872d35f1f",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/b40fe1e0-41cd-4e8b-acb9-801872d35f1f-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 3,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# 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": "8f9e5393-af3b-4d3d-a0d8-d20585b02895",
      "metadata": {},
      "source": [
        "In der obigen Abbildung befindet sich der `SabreLayout` Pass, den `ConditionalController` wir anpassen möchten, an Position **\\[2]** der Layout-Phase. Dieser Controller erfüllt zwei Aufgaben:\n",
        "\n",
        "* Es wird ein Gate `SabreLayout` eingesetzt, sodass es nur dann ausgeführt wird, wenn `VF2Layout` bei \\[1] keine perfekte Zuordnung gefunden werden konnte (ansonsten wird das perfekte Layout „ VF2 “ beibehalten).\n",
        "* Dem geht `SabreLayout` ein `BarrierBeforeFinalMeasurements` Durchlauf voraus, der verhindert, dass Messungen während des internen Routings von „ SabreLayout's “ neu angeordnet werden.\n",
        "\n",
        "Wenn wir einfach `replace(index=2, passes=sl_2)`, werden beide Verhaltensweisen verworfen. Um sie zu schützen, verpacken wir unser Sonderanfertigung `SabreLayout` vor dem Einbau erneut in dieselbe `ConditionalController` Verpackung (unter denselben Bedingungen und mit derselben Schutzschicht).\n",
        "\n",
        "**Schritt 2b: : Erstellen Sie benutzerdefinierte `SabreLayout` Pässe und ersetzen Sie die Standardpasses.**\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "79075a21-8f36-4fd9-9d0d-bd0e97395b60",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/79075a21-8f36-4fd9-9d0d-bd0e97395b60-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "cmap = backend.coupling_map\n",
        "\n",
        "# Custom SabreLayout passes with more aggressive search\n",
        "sl_2 = SabreLayout(\n",
        "    coupling_map=cmap,\n",
        "    seed=seed,\n",
        "    max_iterations=4,\n",
        "    layout_trials=200,\n",
        "    swap_trials=200,\n",
        ")\n",
        "sl_3 = SabreLayout(\n",
        "    coupling_map=cmap,\n",
        "    seed=seed,\n",
        "    max_iterations=8,\n",
        "    layout_trials=200,\n",
        "    swap_trials=200,\n",
        ")\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",
        ")\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": "378a5829-b18b-4359-827f-b052c7cc1b89",
      "metadata": {},
      "source": [
        "`max_iterations=4`Position **\\[2]** ist nun wieder ein `ConditionalController` — in der Form identisch mit der Standardvariante, aber das innere `SabreLayout` ist unsere benutzerdefinierte Variante (mit `layout_trials=200`, `swap_trials=200`, und `max_iterations=8` für `pm_3`; `pm_2` ist bis auf identisch). Die Schutzbarriere und die `_vf2_match_not_found` Gating-Funktion bleiben erhalten, sodass der einzige Unterschied zwischen `pm_2`/`pm_3` und `pm_1` in der SABRE-Konfiguration selbst besteht. `pm_star` behält die Standardeinstellung `SabreLayout` bei und fügt `StarPreRouting` lediglich am Ende der Initialisierungsphase hinzu.\n",
        "\n",
        "**Schritt 2c: Führen Sie jeden Pass-Manager aus und vergleichen Sie die Ergebnisse.**\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "fd52f8dd-862b-46e2-b93d-0b35f47a3d83",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "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": [
        "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": "0c27cbdb-c065-4a36-a0ef-6c9572c7a4a5",
      "metadata": {},
      "source": [
        "Alle drei modifizierten Pass-Manager erzeugten Schaltkreise mit einer geringeren „ 2Q “-Tiefe als die Standardeinstellung. Die aggressiven SABRE-Konfigurationen (`pm_2` und `pm_3`) tauschen eine längere Transpilationszeit gegen eine breitere Suche ein, während `pm_star` die Sternstruktur der Schaltung nutzt und ein noch flacheres Ergebnis liefert, ohne dass zusätzliche Transpilationskosten anfallen. Die genauen Verbesserungen variieren von Durchlauf zu Durchlauf, doch der allgemeine Trend ist konsistent: Je mehr SABRE-Versuche und Iterationen durchgeführt werden, desto größer ist der Suchraum, den die Heuristik abtasten kann, und strukturbewusste Durchläufe wie `StarPreRouting` können diese Suche gänzlich umgehen, wenn die Schaltungsform übereinstimmt.\n",
        "\n",
        "Selbst bei diesem kleinen Maßstab (15 Qubits) ist der Spielraum für Verbesserungen so groß, dass alle drei Ansätze die Standardlösung übertreffen. Bei größeren Schaltkreisen (100+ Qubits) nimmt der Suchraum dramatisch zu, und die Vorteile sowohl einer erhöhten Anzahl von Versuchen als auch strukturorientierter Durchläufe treten deutlich stärker zutage, wie im Abschnitt über groß angelegte Schaltkreise gezeigt wird.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/bf75c45a-2c3e-4ef6-8336-0b3f69e6e8fb-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "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": "6a233965-1358-420b-8823-5f4e206090b4",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Schritt 3: Mit „ Qiskit primitives “ ausführen\n",
        "\n",
        "Wir führen jede transpilierte Schaltung **10 Mal** mit dem Aer `EstimatorV2` aus, wobei wir ein Rauschmodell verwenden, das aus dem tatsächlichen Backend abgeleitet wurde. Da die Simulationsergebnisse von Lauf zu Lauf Schwankungen unterliegen, liefert die Mittelwertbildung über mehrere Läufe zuverlässigere Genauigkeitsschätzungen und ermöglicht es uns, die statistische Unsicherheit mithilfe von Fehlerbalken zu quantifizieren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "a91b9887-c8cf-48fd-a6fb-a5506d201f8d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "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": [
        "# 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": "markdown",
      "id": "89e6c70f-4da4-470f-b0fa-2e2521c82f6f",
      "metadata": {},
      "source": [
        "Da es sich um eine kleine Schaltung handelt, liegen die Wiedergabetreuewerte bei allen vier Konfigurationen relativ nahe beieinander. Die Schaltkreise sind so kurz, dass Hardware-Rauschen selbst bei der am wenigsten optimierten Version keine nennenswerten Einbußen verursacht. Die mittlere Genauigkeit entspricht im Großen und Ganzen der Tiefe der „ 2Q “: `pm_3` und `pm_star`, die beiden flachsten Schaltkreise, erzielen die höchsten Genauigkeitswerte und liegen innerhalb ihrer Fehlerbänder praktisch gleichauf. `pm_2` ist ein nützliches Gegenbeispiel: Obwohl seine „ 2Q “-Tiefe geringer ist als `pm_1`die von, fällt auch seine mittlere Fidelity geringfügig niedriger aus, was daran erinnert, dass der Zusammenhang zwischen Tiefe und Fidelity eher statistischer als deterministischer Natur ist. Auch die Auswahl der einzelnen Qubits durch ein Layout und deren Kalibrierung zur Laufzeit spielen eine Rolle.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e601648b-8f5f-4111-ba35-f64af63da909",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format\n",
        "\n",
        "Stellen Sie als Nächstes die Verschränkungskorrelationen $\\langle Z_0 Z_i \\rangle$ als Funktion des Qubit-Abstands dar, zusammen mit der **mittleren Korrelation** als einheitlicher Maßstab für die Fidelity. Im Idealfall (ohne Rauschen) würden alle Korrelationen den Wert 1 annehmen. Bei realistischen Rauschbedingungen führt jedes zusätzliche Gatter zu Fehlern und jeder zusätzliche Zeitschritt ermöglicht Dekohärenz; daher sollte eine transpilierte Schaltung mit geringerer Tiefe und weniger Gattern (insbesondere Zwei-Qubit-Gattern) die Verschränkung besser bewahren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "a6dac5ed-a963-458a-ada1-89c915f036e0",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/a6dac5ed-a963-458a-ada1-89c915f036e0-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "data_sim = list(range(1, len(operators_sim) + 1))\n",
        "markers = [\"o\", \"s\", \"^\", \"*\"]\n",
        "colors_line = [\"#404080\", \"#2a9d8f\", \"#a8d05e\", \"#e29bdd\"]\n",
        "\n",
        "fig, (ax1, ax2) = plt.subplots(\n",
        "    1, 2, figsize=(14, 5), gridspec_kw={\"width_ratios\": [2.5, 1]}\n",
        ")\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",
        "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",
        "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": "05235ab1-b473-4267-a34c-5f56e728550b",
      "metadata": {},
      "source": [
        "Die Ergebnisse zeigen einen eindeutigen Zusammenhang zwischen der Qualität der Transpilierung und der Ausführungsgenauigkeit, wobei einige nützliche Einschränkungen zu beachten sind:\n",
        "\n",
        "* **`pm_1` (Standard)** : Basislinie. Mit nur 20 Versuchen und vier Iterationen verfügt SABRE nur über begrenzten Optimierungsspielraum, was zu den tiefsten Schaltungen führt, die ausschließlich mit SABRE erzielt werden können.\n",
        "* **`pm_2` (weitere Versuche)** : Bei der Untersuchung von zehnmal so vielen Kandidaten ergibt sich eine etwas flachere Struktur, doch die mittlere Genauigkeit bleibt in etwa gleich (und kann im Rauschbereich sogar unter den Ausgangswert fallen), da der Tiefengewinn in diesem Maßstab gering ist.\n",
        "* **`pm_3` (mehr Versuche + mehr Iterationen)** : Durch die Verdopplung `max_iterations` auf 8 erhält SABRE mehr Verfeinerungszyklen, wodurch die flachste reine SABRE-Schaltung und die höchste mittlere Genauigkeit im Vergleich erzielt werden.\n",
        "* **`pm_star` (Standard + StarPreRouting )** : Fügt `StarPreRouting` zur Initialisierungsphase einer ansonsten standardmäßigen Voreinstellung hinzu. Die strukturorientierte Umschreibung reduziert das Stern-Symbol auf eine lineare Kette, die der Transpiler im weiteren Verlauf auf den linearen Pfad des Bausteins abbildet, wodurch insgesamt die flachste Schaltung entsteht (etwas besser als `pm_3`) und die Treue innerhalb der Fehlergrenzen mit übereinstimmt `pm_3` . Dies geschieht mit derselben Transpilierungszeit wie bei der Standardeinstellung, da die Umschreibung im Vergleich zur stochastischen Suche von SABRE im Wesentlichen keinen zusätzlichen Aufwand verursacht.\n",
        "\n",
        "Beachten Sie, dass eine Erhöhung `max_iterations` von nicht immer positive Auswirkungen hat. In diesem Fall hat es erheblich geholfen, aber bei anderen Schaltkreisen oder Backends führen die zusätzlichen Iterationen möglicherweise nicht zu weiteren Verbesserungen oder beeinträchtigen die Leistung aufgrund einer Überoptimierung eines lokalen Minimums sogar leicht. Generell solltest du und `swap_trials` so weit wie es dein Zeitbudget zulässt erhöhen `layout_trials` , da mehr Versuche immer die Chance erhöhen, ein besseres Layout zu finden. Eine Erhöhung `max_iterations` ist einen Versuch wert, sollte jedoch für Ihren konkreten Anwendungsfall validiert werden. Spezialisierte Pässe wie `StarPreRouting` verfolgen einen ähnlichen Ansatz, sind jedoch stärker von der Schaltung abhängig: Sie sind nur dann hilfreich, wenn die Schaltung tatsächlich die Struktur enthält, auf die sie abzielen. Der Nutzen ist groß, sofern dies zutrifft, und andernfalls gleich Null, aber ein Versuch kostet im Grunde nichts.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c2b60e42-4aa2-4dc1-8a11-022072e79776",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Beispiel für groß angelegte Hardware\n",
        "\n",
        "Neben der Anpassung der Versuchsanzahl unterstützt SABRE auch die individuelle Anpassung der **Routing-Heuristik**. SABRE bietet drei Heuristiken:\n",
        "\n",
        "* **`basic`**: Ein einfacher „Greedy“-Ansatz, bei dem der Tausch ausgewählt wird, der die unmittelbare Entfernung zum nächsten Tor minimiert.\n",
        "* **`decay`** (Standard): Die Gewichtung der Qubits erfolgt dynamisch auf der Grundlage der jüngsten Aktivitäten, wodurch wiederholte Austausche bei denselben Qubits vermieden werden.\n",
        "* **`lookahead`**: Schätzt die zukünftigen Routing-Kosten, indem es die bevorstehenden Gates vorausschauend berücksichtigt und so möglicherweise bessere Swap-Sequenzen ermittelt.\n",
        "\n",
        "Um eine benutzerdefinierte Heuristik zu verwenden, erstellen Sie einen `SabreSwap` Durchlauf und verbinden Sie ihn über den `routing_pass` Parameter mit `SabreLayout` .\n",
        "\n",
        "`SabreLayout`Dem Vergleich wird ein vierter Pass-Manager hinzugefügt: `pm_star_hw`, der die Standard-/Einstellungen`SabreSwap` beibehält, aber zur Initialisierungsphase hinzufügt `StarPreRouting` . Bei dieser Größenordnung (100 Qubits) ist die SABRE-Suche schwieriger, und die Umwandlung von einer Sternstruktur in eine lineare Kette erweist sich als klarer Vorteil, da ein Heron-Prozessor über ausreichend lange lineare Pfade verfügt, um die resultierende Schaltung aufzunehmen.\n",
        "\n",
        "Hier vergleichen wir alle drei SABRE-Heuristiken sowie `StarPreRouting` deren Skalierbarkeit anhand eines 100-Qubit-GHZ-Schaltkreises. Wir führen mehrere Layout-Versuche mit unterschiedlichen Startwerten für die SABRE-Konfigurationen durch, wählen aus jedem Versuch die beste transpilierte Schaltung aus und übermitteln diese zusammen mit dem `StarPreRouting` Ergebnis an die reale Hardware.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d755fafa-bbbc-4191-9ad9-a77f2af1bedc",
      "metadata": {},
      "source": [
        "<span id=\"steps-1-4-compressed-into-a-single-code-block\" />\n",
        "\n",
        "### Schritte 1–4 in einem einzigen Codeblock zusammengefasst\n",
        "\n",
        "Hier wird der gesamte Arbeitsablauf in größerem Maßstab dargestellt. `SabreLayout`Wenn als für `routing_pass` verwendet `SabreSwap` wird, wird pro Aufruf nur ein Layout-Versuch durchgeführt; daher durchläuft die folgende Codezelle die Startwerte in einer Schleife, um den Layout-Raum zu erkunden.\n",
        "\n",
        "Wir verwenden denselben `wrap_sabre` Helfer, der im oben beschriebenen Schritt 2 (im kleinen Maßstab) definiert wurde, und fügen einen analogen `wrap_routing` Helfer hinzu, da die `routing` Stufe am Index \\[1] ebenfalls ein `ConditionalController([BarrierBeforeFinalMeasurements, routing_pass], ...)` ist – würde man sie einfach so ersetzen, würden ebenfalls die Schutzbarriere und das `_swap_condition` Gating wegfallen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "4feb0fcf-d305-4741-82a7-b0fe0f0894fc",
      "metadata": {},
      "outputs": [],
      "source": [
        "# -------------------------Step 1-------------------------\n",
        "\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",
        "# 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": 10,
      "id": "43ec98c6-f4c6-4584-8ba0-edfb4e99f04a",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "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": [
        "# -------------------------Step 2-------------------------\n",
        "\n",
        "num_seeds = 10\n",
        "seed_list = [seed + i for i in range(num_seeds)]\n",
        "swap_trials = 200\n",
        "\n",
        "\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",
        "\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",
        "\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": 11,
      "id": "eead9bd2-17e0-4f5b-80bc-eb9b30af052e",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/eead9bd2-17e0-4f5b-80bc-eb9b30af052e-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "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": [
        "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": 12,
      "id": "5ad47245-41d0-4d90-ba94-dda4cd63705d",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "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": [
        "# -------------------------Step 3: Execute on hardware-------------------------\n",
        "\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",
        "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": 13,
      "id": "0280b0b9-6320-43e5-8396-f82f9e718319",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/transpilation-optimizations-with-sabre/extracted-outputs/0280b0b9-6320-43e5-8396-f82f9e718319-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "\n",
            "Mean fidelity:\n",
            "  basic: 0.0344\n",
            "  decay: 0.1298\n",
            "  lookahead: 0.1857\n",
            "  StarPreRouting: 0.3295\n"
          ]
        }
      ],
      "source": [
        "# -------------------------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",
        "\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": "151e5fe9-872a-4b89-92f9-85bb883de17b",
      "metadata": {},
      "source": [
        "<span id=\"analysis\" />\n",
        "\n",
        "### Analyse\n",
        "\n",
        "Die Streudiagramme zeigen bei allen drei SABRE-Heuristiken erhebliche Schwankungen zwischen den einzelnen Samen, was verdeutlicht, wie wichtig es ist, mehrere Layout-Versuche durchzuführen, anstatt sich auf eine einzige Transpilierung zu verlassen. Die `StarPreRouting` Kurve verläuft über alle Startwerte hinweg im Wesentlichen flach, da die Umwandlung von einer Sternstruktur in eine lineare Kette angesichts der Struktur deterministisch ist; das nachgelagerte SABRE-Routing hat bei einer linearen Kette nur sehr wenig Spielraum, sodass der Startwert nahezu keinen Einfluss auf die endgültige Tiefe oder Größe hat.\n",
        "\n",
        "Den Transpilierungsergebnissen zufolge schneiden sowohl die `decay` als auch `lookahead` die Heuristik durchweg deutlich besser ab `basic` als. Die `basic` Heuristik ist zwar schnell, verwendet jedoch eine einfache „Greedy“-Strategie, die oft zu wesentlich tieferen Schaltkreisen führt. Bei dieser GHZ-Schaltung mit Sterntopologie führt `lookahead` in der Regel zu der geringsten „ 2Q “-Tiefe und der geringsten Anzahl an Gatter unter den SABRE-Heuristiken, da sich ihre vorausschauende Kostenfunktion gut für Schaltungen mit weitreichenden Verbindungsmustern eignet. `StarPreRouting`, übertrifft jedoch alle drei bei weitem: Indem der Stern vor der Routierung in eine lineare Kette umgeschrieben wird, umgeht es das Suchproblem vollständig und liefert eine Schaltung, die der Rest des Transpilers mit minimalem zusätzlichem SWAP-Aufwand auf einen linearen Pfad abbilden kann.\n",
        "\n",
        "Dieser Vorteil schlägt sich direkt in der Hardware-Genauigkeit nieder. Eine geringere Tiefe des „ 2Q “ und eine geringere Gate-Anzahl führen nicht immer eins zu eins zu einer höheren Genauigkeit (die spezifischen physikalischen Qubits, die ein Layout verwendet, und deren Kalibrierung zur Laufzeit spielen ebenfalls eine Rolle), aber wenn der Unterschied in der Tiefe so groß ist wie der zwischen SABRE und `StarPreRouting` hier, setzt sich der strukturbewusste Ansatz eindeutig durch, da die Schaltung weitaus weniger Dekohärenz und weitaus weniger Zwei-Qubit-Fehlerereignisse aufweist. Das Treue-Balkendiagramm zeigt `StarPreRouting` , dass sogar deutlich vor der besten SABRE-Heuristik liegt, während `basic` deutlich hinter den anderen zurückbleibt, da seine wesentlich tieferen Schaltkreise die meisten Fehler akkumulieren.\n",
        "\n",
        "**Das Wichtigste auf einen Blick:**\n",
        "\n",
        "* Unter den SABRE-Heuristiken sind und `lookahead` bei nicht-trivialen Schaltkreisen deutlich besser als `basic` . `decay` Wählen Sie für Produktions-Workloads eine der beiden Optionen.\n",
        "* Die beste SABRE-Heuristik hängt von Ihrer Schaltung und Ihrer Hardware ab. Das Testen mehrerer Heuristiken mit mehreren Startwerten ist die zuverlässigste Strategie.\n",
        "* Wenn Sie noch mehr Layouts untersuchen möchten, erhöhen Sie `swap_trials` (und `layout_trials` wenn Sie keinen benutzerdefinierten Routing-Durchlauf festlegen), anstatt die Arbeit auf entfernte Knoten zu verteilen. Die SABRE-Passes parallelisieren die Versuche bereits über lokale Threads hinweg, und der Aufwand pro Versuch ist so gering, dass der Overhead durch die Verteilung in der Regel die Geschwindigkeitssteigerung übertrifft.\n",
        "* Wenn die Schaltung eine bekannte spezielle Struktur aufweist, kann die Anwendung eines strukturorientierten Durchlaufs – beispielsweise `StarPreRouting` vor SABRE – eine Verbesserung um eine Größenordnung bewirken, die durch keine noch so intensive SABRE-Optimierung erreicht werden kann. Dies ist kein Ersatz für SABRE: `StarPreRouting` Es hilft nur, wenn die Schaltung tatsächlich Stern-Teilstromkreise enthält und das Backend über einen ausreichend langen linearen Pfad verfügt. Es lohnt sich, die Pass-Bibliothek nach Übereinstimmungen zu durchsuchen, sobald Sie die Form Ihrer Schaltung kennen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "65c2cebe-50b9-4304-9123-bf4cea7ecff6",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Nächste Schritte\n",
        "\n",
        "Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Empfehlungen\">\n",
        "  * [`SabreLayout` API-Referenz](/docs/api/qiskit/qiskit.transpiler.passes.SabreLayout) : Vollständige Parameterdokumentation\n",
        "  * [SABRE-Artikel](https://arxiv.org/abs/1809.02573) : Der ursprüngliche SABRE-Algorithmus für Layout und Verlegung\n",
        "  * [LightSABRE Artikel](https://arxiv.org/abs/2409.08368) : Die algorithmischen Verbesserungen, die der aktuellen SABRE-Implementierung von Qiskit zugrunde liegen\n",
        "  * [Schreiben Sie einen benutzerdefinierten Transpiler-Durchlauf](/docs/guides/custom-transpiler-pass) : Erstellen Sie Ihre eigene Transpiler-Logik\n",
        "  * [Transpiler-Plugins](/docs/guides/transpiler-plugins) : Erweiterung der Transpiler-Pipeline von Qiskit um Pässe von Drittanbietern\n",
        "  * [DAG-Darstellung](/docs/guides/DAG-representation) : Verständnis des intern vom Transpiler verwendeten gerichteten azyklischen Graphen\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "42d1c053-1683-4c32-be1b-a36602207f74",
      "metadata": {},
      "source": [
        "<span id=\"tutorial-survey\" />\n",
        "\n",
        "## Tutorial-Umfrage\n",
        "\n",
        "Bitte nehmen Sie an dieser kurzen Umfrage teil, um uns Ihr Feedback zu diesem Lernprogramm zu geben. Ihre Erkenntnisse helfen uns, unser Inhaltsangebot und unsere Nutzererfahrung zu verbessern.\n",
        "\n",
        "[Link zur Umfrage](https://your.feedback.ibm.com/jfe/form/SV_d9YWUSQIAvU9HXE)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 1,
    "qpuSeconds": 60
  },
  "nbformat": 4,
  "nbformat_minor": 4
}