{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d2c31ae8",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Operator-Backpropagation (OBP) zur Schätzung von Erwartungswerten\"\n",
        "description: \"In diesem Tutorial wird ein Qiskit-Muster zur Simulation der Quantendynamik einer Heisenberg-Spin-Kette mithilfe von qiskit-addon-obp implementiert\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore fontsize edgecolor circo simeq */}\n",
        "\n",
        "<span id=\"operator-backpropagation-obp-for-estimation-of-expectation-values\" />\n",
        "\n",
        "# Operator-Backpropagation (OBP) zur Schätzung von Erwartungswerten\n",
        "\n",
        "*Geschätzte Laufzeit: 4 Minuten auf einem Heron- r3 -Prozessor (HINWEIS: Dies ist nur eine Schätzung. (Die Laufzeit kann variieren.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Lernziele\n",
        "\n",
        "* Wie man nutzt [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) , um die Tiefe des Quantenschaltkreises auf Kosten einer erhöhten Anzahl von Schaltkreisausführungen zu verringern\n",
        "* Wie man zur Konstruktion von XYZ-Hamiltonianen und deren Zeitentwicklungsschaltungen verwendet [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils)\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Voraussetzungen\n",
        "\n",
        "* Berechnung der Erwartungswerte einer beobachtbaren Größe mithilfe der [Estimator](/docs/api/qiskit-ibm-runtime/executor-estimator-estimator) -Primitive\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Hintergrund\n",
        "\n",
        "Die Operator-Backpropagation ist eine Technik, bei der Operationen vom Ende eines Quantenschaltkreises in die gemessene Observable integriert werden, wodurch die Tiefe des Schaltkreises im Allgemeinen verringert wird – allerdings auf Kosten zusätzlicher Terme in der Observable. Das Ziel besteht darin, so viel wie möglich des Netzwerks rückwärts zu propagieren, ohne dass die Observable zu groß wird. Eine auf Qiskit basierende Implementierung ist im OBP-Qiskit-Add-on verfügbar. Weitere Informationen finden Sie in der entsprechenden [Dokumentation](https://qiskit.github.io/qiskit-addon-obp/).\n",
        "\n",
        "Betrachten wir eine Beispielschaltung, bei der ein Observabel $O = \\sum_P c_P P$ gemessen werden soll, wobei $P$ Pauli-Operatoren und $c_P$ Koeffizienten sind. Bezeichnen wir die Schaltung als eine einzige einheitliche Schaltung ( $U$ ), die logisch in die in der folgenden Abbildung dargestellten Schaltungen ( $U = U_C U_Q$ ) unterteilt werden kann.\n",
        "\n",
        "![Schaltplan mit Uq gefolgt von Uc](https://quantum.cloud.ibm.com/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n",
        "\n",
        "Die Operator-Backpropagation integriert die Unitar- $U_C$ in die Observable, indem sie diese wie folgt entwickelt: $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. Mit anderen Worten: Ein Teil der Berechnung erfolgt klassisch durch die Entwicklung der Observable von $O$ zu $O'$. Das ursprüngliche Problem lässt sich nun so umformulieren, dass die Observable $O'$ für den neuen Schaltkreis mit geringerer Tiefe gemessen wird, dessen Unitar $U_Q$ lautet.\n",
        "\n",
        "Das Einheits $U_C$ wird als eine Reihe von Schnitten $U_C = U_S U_{S-1}...U_2U_1$ dargestellt. Es gibt mehrere Möglichkeiten, einen Schnitt zu definieren. So können beispielsweise in der oben dargestellten Beispielschaltung jede Schicht der „ $R_{zz}$ “-Gates und jede Schicht der „ $R_x$ “-Gates als eigenständiger „Slice“ betrachtet werden. Bei der Backpropagation werden klassischerweise die Gewichte ( $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$ ) berechnet. Jede Schichtung $U_s$ lässt sich als $U_s = exp(\\frac{-i\\theta_s P_s}{2})$ darstellen, wobei $P_s$ ein $n$ -Qubit-Pauli-Operator und $\\theta_s$ ein Skalar ist. Es lässt sich leicht nachweisen, dass\n",
        "\n",
        "$$\n",
        "U_s^{\\dagger} P U_s = P \\qquad \\text{if} ~[P,P_s] = 0,\n",
        "$$\n",
        "\n",
        "$$\n",
        "U_s^{\\dagger} P U_s = \\qquad cos(\\theta_s)P + i sin(\\theta_s)P_sP \\qquad \\text{if} ~\\{P,P_s\\} = 0\n",
        "$$\n",
        "\n",
        "Wenn im obigen Beispiel $\\{P,P_s\\} = 0$ gilt, müssen wir zur Berechnung des Erwartungswerts zwei Quantenkreise statt nur eines ausführen. Daher kann die Rückpropagation die Anzahl der Terme im Observablen erhöhen, was zu einer höheren Anzahl von Schaltungsausführungen führt. Eine Möglichkeit, eine tiefere Rückpropagierung in die Schaltung zu ermöglichen und gleichzeitig zu verhindern, dass der Operator zu groß wird, besteht darin, Terme mit kleinen Koeffizienten zu kürzen, anstatt sie zum Operator hinzuzufügen. Im obigen Beispiel könnte man beispielsweise den Term mit „ $P_sP$ “ kürzen, vorausgesetzt, dass „ $\\theta_s$ “ ausreichend klein ist. Das Kürzen von Termen kann zwar zu einer geringeren Anzahl auszuführender Quantenschaltungen führen, hat jedoch zur Folge, dass bei der Berechnung des endgültigen Erwartungswerts ein Fehler auftritt, der proportional zur Größe der Koeffizienten der gekürzten Terme ist.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "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",
        "* OBP Qiskit-Add-on 0.3 oder höher (`pip install qiskit-addon-obp`)\n",
        "* Qiskit-Add-on „utils“ 0.3 oder höher (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Einrichtung\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "bc380c46",
      "metadata": {},
      "outputs": [],
      "source": [
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "from qiskit.primitives import StatevectorEstimator\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit.quantum_info import SparsePauliOp\n",
        "from qiskit.transpiler import CouplingMap\n",
        "from qiskit.synthesis import LieTrotter\n",
        "\n",
        "from qiskit_addon_utils.problem_generators import generate_xyz_hamiltonian\n",
        "from qiskit_addon_utils.problem_generators import (\n",
        "    generate_time_evolution_circuit,\n",
        ")\n",
        "from qiskit_addon_utils.slicing import slice_by_depth, combine_slices\n",
        "from qiskit_addon_obp.utils.simplify import OperatorBudget\n",
        "from qiskit_addon_obp import backpropagate\n",
        "from qiskit_addon_obp.utils.truncating import setup_budget\n",
        "\n",
        "from rustworkx.visualization import graphviz_draw\n",
        "\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "from qiskit_ibm_runtime import EstimatorV2, EstimatorOptions"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "431a5bd2-e6ed-471b-ad9e-c4edd27784a8",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Beispiel für einen Simulator im kleinen Maßstab\n",
        "\n",
        "In diesem Tutorial wird ein [Qiskit-Muster](/docs/guides/intro-to-patterns) zur Simulation der Quantendynamik einer Heisenberg-Spin-Kette mithilfe des [OBP-Qiskit-Add-ons](https://github.com/Qiskit/qiskit-addon-obp) implementiert. Beachten Sie, dass in einem rauschfreien Simulator der Erwartungswert, der mit und ohne Backpropagation ermittelt wird, identisch ist.\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen\n",
        "\n",
        "<span id=\"map-the-time-evolution-of-a-quantum-heisenberg-model-to-a-quantum-experiment\" />\n",
        "\n",
        "#### Ordne die zeitliche Entwicklung eines Quanten-Heisenberg-Modells einem Quantenexperiment zu\n",
        "\n",
        "Zunächst werden wir die [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) Funktion aus `qiskit-addon-utils` verwenden, um einen Heisenberg-ähnlichen Hamilton-Operator auf einem gegebenen Verbindungsgraphen zu generieren. Diese Grafik kann entweder ein „ [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) “ oder ein „ [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap) “ sein. Im Folgenden verwenden wir eine lineare Kette `CouplingMap` aus 10 Qubits.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "a3debf65-06df-4277-933e-14b6f6170756",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/operator-back-propagation/extracted-outputs/a3debf65-06df-4277-933e-14b6f6170756-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 2,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "num_qubits = 10\n",
        "layout = [(i - 1, i) for i in range(1, num_qubits)]\n",
        "\n",
        "# Instantiate a CouplingMap object\n",
        "coupling_map = CouplingMap(layout)\n",
        "graphviz_draw(coupling_map.graph, method=\"circo\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ef13f367",
      "metadata": {},
      "source": [
        "Als Nächstes generieren wir einen Pauli-Operator, der einen Heisenberg-XYZ-Hamiltonian modelliert:\n",
        "\n",
        "$$\n",
        "{\\hat{\\mathcal{H}}_{XYZ} = \\sum_{(j,k)\\in E} (J_{x} \\sigma_j^{x} \\sigma_{k}^{x} + J_{y} \\sigma_j^{y} \\sigma_{k}^{y} + J_{z} \\sigma_j^{z} \\sigma_{k}^{z}) + \\sum_{j\\in V} (h_{x} \\sigma_j^{x} + h_{y} \\sigma_j^{y} + h_{z} \\sigma_j^{z}),}\n",
        "$$\n",
        "\n",
        "wobei $G(V,E)$ die Kurve der Kopplungsabbildung ist. Für dieses Tutorial haben wir $J_x, J_y, J_z$ bzw. $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$ und $h_x, h_y, h_z$ bzw. $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$ verwendet.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "b0f1e5fd",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "SparsePauliOp(['IIIIIIIXXI', 'IIIIIIIYYI', 'IIIIIIIZZI', 'IIIIIXXIII', 'IIIIIYYIII', 'IIIIIZZIII', 'IIIXXIIIII', 'IIIYYIIIII', 'IIIZZIIIII', 'IXXIIIIIII', 'IYYIIIIIII', 'IZZIIIIIII', 'IIIIIIIIXX', 'IIIIIIIIYY', 'IIIIIIIIZZ', 'IIIIIIXXII', 'IIIIIIYYII', 'IIIIIIZZII', 'IIIIXXIIII', 'IIIIYYIIII', 'IIIIZZIIII', 'IIXXIIIIII', 'IIYYIIIIII', 'IIZZIIIIII', 'XXIIIIIIII', 'YYIIIIIIII', 'ZZIIIIIIII', 'IIIIIIIIIX', 'IIIIIIIIIY', 'IIIIIIIIIZ', 'IIIIIIIIXI', 'IIIIIIIIYI', 'IIIIIIIIZI', 'IIIIIIIXII', 'IIIIIIIYII', 'IIIIIIIZII', 'IIIIIIXIII', 'IIIIIIYIII', 'IIIIIIZIII', 'IIIIIXIIII', 'IIIIIYIIII', 'IIIIIZIIII', 'IIIIXIIIII', 'IIIIYIIIII', 'IIIIZIIIII', 'IIIXIIIIII', 'IIIYIIIIII', 'IIIZIIIIII', 'IIXIIIIIII', 'IIYIIIIIII', 'IIZIIIIIII', 'IXIIIIIIII', 'IYIIIIIIII', 'IZIIIIIIII', 'XIIIIIIIII', 'YIIIIIIIII', 'ZIIIIIIIII'],\n",
            "              coeffs=[0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n",
            " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n",
            " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n",
            " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j,\n",
            " 0.78539816+0.j, 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j,\n",
            " 1.57079633+0.j, 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j,\n",
            " 0.39269908+0.j, 0.78539816+0.j, 1.57079633+0.j, 1.04719755+0.j,\n",
            " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n",
            " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n",
            " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n",
            " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n",
            " 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j,\n",
            " 1.04719755+0.j, 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j,\n",
            " 0.52359878+0.j, 0.34906585+0.j, 1.04719755+0.j, 0.52359878+0.j,\n",
            " 0.34906585+0.j])\n"
          ]
        }
      ],
      "source": [
        "# Get a qubit operator describing the Heisenberg XYZ model\n",
        "hamiltonian = generate_xyz_hamiltonian(\n",
        "    coupling_map,\n",
        "    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n",
        "    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n",
        ")\n",
        "print(hamiltonian)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a81f0346",
      "metadata": {},
      "source": [
        "Ausgehend vom Qubit-Operator können wir eine Quantenschaltung erstellen, die dessen zeitliche Entwicklung abbildet. Wir haben die Lie-Trotter-Zerlegung verwendet [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) , um die Zeitentwicklungsschaltung zu konstruieren.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "5208e0a8",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/operator-back-propagation/extracted-outputs/5208e0a8-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 4,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "circuit = generate_time_evolution_circuit(\n",
        "    hamiltonian,\n",
        "    time=0.2,\n",
        "    synthesis=LieTrotter(reps=2),\n",
        ")\n",
        "circuit.draw(\"mpl\", style=\"iqp\", fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ac6f36e3",
      "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",
        "<span id=\"create-circuit-slices-to-backpropagate\" />\n",
        "\n",
        "#### Erstellen Sie Schaltungsschnitte, um rückwärts zu propagieren\n",
        "\n",
        "Die `backpropagate` Funktion führt die Rückwärtsausbreitung jeweils für ganze Schaltungsabschnitte durch. Daher kann die Wahl der Unterteilung Einfluss darauf haben, wie gut die Backpropagation bei einem bestimmten Problem funktioniert. Hier werden wir Gatter desselben Typs mithilfe der [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) Funktion zu Schnitten zusammenfassen.\n",
        "\n",
        "Eine ausführlichere Erläuterung zum Thema „Circuit Slicing“ finden Sie in dieser [Anleitung](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) zum [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) Paket.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "1834cb22",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Separated the circuit into 18 slices.\n"
          ]
        }
      ],
      "source": [
        "slices = slice_by_depth(circuit, max_slice_depth=1)\n",
        "print(f\"Separated the circuit into {len(slices)} slices.\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1df16e6b",
      "metadata": {},
      "source": [
        "<span id=\"constrain-how-large-the-operator-can-grow-during-backpropagation\" />\n",
        "\n",
        "#### Begrenze, wie groß der Operator während der Rückwärtsausbreitung werden darf\n",
        "\n",
        "Während der Rückpropagation nähert sich die Anzahl der Terme im Operator in der Regel schnell dem Wert „ $2^L$ “ an, wobei „ $L$ “ die Anzahl der Schichten ist. Wenn zwei Terme im Operator auf Qubit-Ebene nicht kommutieren, benötigen wir separate Schaltkreise, um die ihnen entsprechenden Erwartungswerte zu erhalten. Wenn wir beispielsweise eine Observable mit zwei Qubits $O = 0.1 XX + 0.3 IZ - 0.5 IX$ haben, dann reicht – da $[XX,IX] = 0$ – eine Messung in einer einzigen Basis aus, um die Erwartungswerte für diese beiden Terme zu berechnen. $IZ$ ist jedoch mit den beiden anderen Termen antikommutativ, sodass wir eine separate Basismessung benötigen, um den Erwartungswert von $IZ$ zu berechnen. Mit anderen Worten: Wir benötigen zwei Schaltkreise statt nur einem, um $\\langle O \\rangle$ zu berechnen. Mit zunehmender Anzahl der Terme im Operator steigt möglicherweise auch die erforderliche Anzahl der Schaltkreisausführungen.\n",
        "\n",
        "Die Größe des Operators kann durch Angabe des `operator_budget` kwarg der Funktion `backpropagate` begrenzt werden, die eine [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instanz akzeptiert.\n",
        "\n",
        "Um den Umfang der zugewiesenen zusätzlichen Ressourcen (Anzahl der Schaltkreisläufe und damit die erforderliche QPU-Zeit) zu steuern, begrenzen wir die maximale Anzahl der qubitweisen kommutierenden Pauli-Gruppen, die die rückpropagierte Observable aufweisen darf. Hier legen wir fest, dass die Rückpropagation beendet werden soll, sobald die Anzahl der qubitweise kommutierenden Pauli-Gruppen im Operator acht übersteigt.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "3074b173",
      "metadata": {},
      "outputs": [],
      "source": [
        "op_budget = OperatorBudget(max_qwc_groups=8)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bd6ac5f",
      "metadata": {},
      "source": [
        "<span id=\"backpropagate-slices-from-the-circuit\" />\n",
        "\n",
        "#### Slices aus dem Schaltkreis rückwärts propagieren\n",
        "\n",
        "Zunächst legen wir die Beobachtungsgröße $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$ fest, wobei $N$ die Anzahl der Qubits ist. Wir werden die Scheiben des Zeitentwicklungskreises so lange zurückverfolgen, bis die Terme in der Beobachtungsgröße nicht mehr in acht oder weniger Qubit-weise kommutierende Pauli-Gruppen zusammengefasst werden können.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "a1300365",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "SparsePauliOp(['IIIIIIIIIZ', 'IIIIIIIIZI', 'IIIIIIIZII', 'IIIIIIZIII', 'IIIIIZIIII', 'IIIIZIIIII', 'IIIZIIIIII', 'IIZIIIIIII', 'IZIIIIIIII', 'ZIIIIIIIII'],\n",
              "              coeffs=[0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j, 0.1+0.j,\n",
              " 0.1+0.j, 0.1+0.j])"
            ]
          },
          "execution_count": 7,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n",
        "    num_qubits=num_qubits,\n",
        ")\n",
        "observable"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "820236bf",
      "metadata": {},
      "source": [
        "Unten sehen Sie, dass wir sechs Schichten rückwärtsverbreitet haben und die Terme zu sechs statt zu acht Gruppen zusammengefasst wurden. Das bedeutet, dass bei einer weiteren Rückpropagierung die Anzahl der Pauli-Gruppen acht überschreiten würde. Wir können dies überprüfen, indem wir die zurückgegebenen Metadaten untersuchen. Beachten Sie außerdem, dass die Schaltungsumformung in diesem Abschnitt exakt ist.  Das heißt, es wurden keine Terme der neuen Observablen $O’$ abgeschnitten. Die Schaltung mit Rückwärtsausbreitung und der Operator mit Rückwärtsausbreitung liefern genau dasselbe Ergebnis wie die ursprüngliche Schaltung und der ursprüngliche Operator.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "ee8fd385",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Backpropagated 6 slices.\n",
            "New observable has 60 terms, which can be combined into 6 groups.\n",
            "Note that backpropagating one more slice would result in 114 terms across 12 groups.\n",
            "The remaining circuit after backpropagation looks as follows:\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/operator-back-propagation/extracted-outputs/ee8fd385-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 8,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "# Backpropagate slices onto the observable\n",
        "bp_obs, remaining_slices, metadata = backpropagate(\n",
        "    observable, slices, operator_budget=op_budget\n",
        ")\n",
        "# Recombine the slices remaining after backpropagation\n",
        "bp_circuit = combine_slices(remaining_slices)\n",
        "\n",
        "print(f\"Backpropagated {metadata.num_backpropagated_slices} slices.\")\n",
        "print(\n",
        "    f\"New observable has {len(bp_obs.paulis)} terms, which can be combined into \"\n",
        "    f\"{len(bp_obs.group_commuting(qubit_wise=True))} groups.\"\n",
        ")\n",
        "print(\n",
        "    f\"Note that backpropagating one more slice would result in \"\n",
        "    f\"{metadata.backpropagation_history[-1].num_paulis[0]} terms \"\n",
        "    f\"across {metadata.backpropagation_history[-1].num_qwc_groups} groups.\"\n",
        ")\n",
        "print(\"The remaining circuit after backpropagation looks as follows:\")\n",
        "bp_circuit.draw(\"mpl\", fold=-1, scale=0.6)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "833d9c36",
      "metadata": {},
      "source": [
        "Für das kleine Beispiel auf einem Simulator werden wir keine Rundung vornehmen. Das liegt daran, dass bei Abwesenheit von Rauschen die Schaltung mit und ohne Rückpropagation zum gleichen Ergebnis führt und die Trunkierung das Ergebnis aufgrund der zusätzlichen Approximation verschlechtert.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43f58cfb",
      "metadata": {},
      "source": [
        "<span id=\"transpile-the-circuits-into-the-basis-gate-set\" />\n",
        "\n",
        "#### Die Schaltungen in den Basis-Gatter-Satz transpilieren\n",
        "\n",
        "Nun transpilieren wir sowohl die ursprüngliche als auch die rückwärtspropagierte Schaltung in das Basisgatter des Backends. Wir müssen auf dem eigentlichen Backend nicht transpilieren, da wir für die kleine Instanz einen Simulator verwenden werden.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "29d71cd3",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<IBMBackend('ibm_kingston')>\n"
          ]
        }
      ],
      "source": [
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=133\n",
        ")\n",
        "print(backend)"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "18707b33",
      "metadata": {},
      "outputs": [],
      "source": [
        "pm_basis = generate_preset_pass_manager(\n",
        "    optimization_level=3, basis_gates=backend.configuration().basis_gates\n",
        ")\n",
        "isa_circuit = pm_basis.run(circuit)\n",
        "isa_bp_circuit = pm_basis.run(bp_circuit)"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "b4d480b3",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Schritt 3: Mit „ Qiskit primitives “ ausführen\n",
        "\n",
        "Zunächst erstellen wir zwei [„Primitive Unified Blocs“](/docs/api/qiskit/primitives) (PUBs), die der ursprünglichen Schaltung bzw. der durch Rückpropagierung erzeugten Schaltung entsprechen. Anschließend führen wir die Pubs auf einem idealen Schätzer aus, um die Erwartungswerte zu erhalten.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "b22a1b00",
      "metadata": {},
      "outputs": [],
      "source": [
        "pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "eb174b15",
      "metadata": {},
      "outputs": [],
      "source": [
        "rng = np.random.default_rng()\n",
        "estimator = StatevectorEstimator(seed=rng)\n",
        "job = estimator.run(pubs)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "50b94af2",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-desired-classical-format\" />\n",
        "\n",
        "### Schritt 4: Nachbearbeitung und Ausgabe des Ergebnisses im gewünschten klassischen Format\n",
        "\n",
        "Nun berechnen wir die Erwartungswerte der ursprünglichen und der durch Rückpropagation optimierten Schaltungen.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "31dc35ea-6554-4ca7-9c3b-0b5394c46e4e",
      "metadata": {},
      "outputs": [],
      "source": [
        "primitive_result = job.result()\n",
        "circuit_expval = primitive_result[0].data.evs.item()\n",
        "bp_circuit_expval = primitive_result[1].data.evs.item()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "fb5f955a",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "Text(0, 0.5, '$M_Z$')"
            ]
          },
          "execution_count": 14,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/operator-back-propagation/extracted-outputs/fb5f955a-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "methods = [\n",
        "    \"No backpropagation\",\n",
        "    \"Backpropagation\",\n",
        "]\n",
        "values = [circuit_expval, bp_circuit_expval]\n",
        "\n",
        "ax = plt.gca()\n",
        "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n",
        "ax.set_ylim([0.6, 0.92])\n",
        "ax.set_ylabel(r\"$M_Z$\", fontsize=12)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c0be738f",
      "metadata": {},
      "source": [
        "Wie erwartet stimmen die beiden Erwartungswerte überein. Da wir mit einem rauschfreien Statevector-Simulator arbeiten, stellt die Rückpropagierung eine exakte Transformation des Schaltkreis-Beobachtungs-Paares dar, sodass der ursprüngliche und der rückpropagierte Arbeitsablauf denselben Wert für „ $M_Z$ “ ergeben müssen. Der Vorteil der Rückpropagierung zeigt sich erst auf rauschbehafteter Hardware, wo der kürzere rückpropagierte Schaltkreis weniger Fehler akkumuliert, wie das nachstehende Beispiel für groß angelegte Hardware veranschaulicht.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Beispiel für groß angelegte Hardware\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25",
      "metadata": {},
      "source": [
        "Bei der Entwicklung eines Experiments ist es sinnvoll, mit einer kleinen Schaltung zu beginnen, um Visualisierungen und Simulationen zu vereinfachen. Nun betrachten wir die Operator-Backpropagation für einen 50-Qubit-Heisenberg-Hamiltonian mit denselben Werten für die Parameter „ $J$ “ und „ $h$ “ sowie demselben Observablen „ $M_Z$ “, jedoch für vier Trotter-Schritte. Der ideale Erwartungswert lässt sich in diesem Maßstab nicht mit einer Brute-Force-Methode berechnen, daher verwenden wir ein Tensornetzwerk und erhalten als idealen Erwartungswert den Wert $\\simeq 0.89$.\n",
        "\n",
        "Neben der Rückwärtsausbreitung stellen wir in diesem groß angelegten Beispiel auch die Rückwärtsausbreitung mit Trunkierung vor. Idealerweise möchten wir die Rückpropagierung so weit wie möglich vornehmen, um die Tiefe des effektiven Schaltkreises zu verringern. Dies führt jedoch häufig zu einer großen Anzahl von nicht-kommutierenden Termen in der aktualisierten Observable, was den Quanten-Overhead erhöht. Daher können wir beobachtbare Terme mit kleinen Koeffizienten mithilfe einer Methode namens Trunkierung eliminieren. Zwar ermöglicht die Trunkierung eine stärkere Ausbreitung, indem sie die Anzahl der Terme in der aktualisierten Observablen verringert, doch führt sie auch zu einer gewissen Annäherung. Daher ist es notwendig, die Kürzung auf ein gewisses Maß zu beschränken, damit der Approximationsfehler die durch eine tiefere Backpropagation erzielte Rauschreduzierung nicht zunichte macht.\n",
        "\n",
        "Um den Umfang der Trunkierung zu begrenzen, weisen wir jedem Segment ein Fehlerbudget zu und legen mithilfe der [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) Funktion ein Gesamtfehlerbudget für die gesamte Schaltung fest, die der Rückwärtsausbreitung unterliegt. Dadurch wird sichergestellt, dass die Abschaltung sowohl für jeden einzelnen Abschnitt als auch für die gesamte Schaltung gesteuert wird. Weitere Möglichkeiten zur Aufteilung des Budgets finden Sie in diesem [Leitfaden](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "28ac4dbf",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "2-qubit depth without backpropagation: 24\n",
            "2-qubit depth with backpropagation: 20\n",
            "2-qubit depth with backpropagation and truncation: 18\n"
          ]
        }
      ],
      "source": [
        "num_qubits = 50\n",
        "layout = [(i - 1, i) for i in range(1, num_qubits)]\n",
        "\n",
        "# Instantiate a CouplingMap object\n",
        "coupling_map = CouplingMap(layout)\n",
        "\n",
        "hamiltonian = generate_xyz_hamiltonian(\n",
        "    coupling_map,\n",
        "    coupling_constants=(np.pi / 8, np.pi / 4, np.pi / 2),\n",
        "    ext_magnetic_field=(np.pi / 3, np.pi / 6, np.pi / 9),\n",
        ")\n",
        "\n",
        "# Generate a time evolution circuit for the Hamiltonian\n",
        "circuit = generate_time_evolution_circuit(\n",
        "    hamiltonian,\n",
        "    time=0.2,\n",
        "    synthesis=LieTrotter(reps=4),\n",
        ")\n",
        "\n",
        "# Define the observable to measure\n",
        "observable = SparsePauliOp.from_sparse_list(\n",
        "    [(\"Z\", [i], 1 / num_qubits) for i in range(num_qubits)],\n",
        "    num_qubits,\n",
        ")\n",
        "\n",
        "slices = slice_by_depth(circuit, max_slice_depth=1)\n",
        "\n",
        "# Define the maximum number of qwc groups allowed in the\n",
        "# backpropagated observable,\n",
        "# and the truncation error budget\n",
        "op_budget = OperatorBudget(max_qwc_groups=15)\n",
        "truncation_error_budget = setup_budget(\n",
        "    max_error_total=0.03, max_error_per_slice=0.005\n",
        ")\n",
        "\n",
        "# First backpropagation without truncation\n",
        "bp_obs, remaining_slices, metadata = backpropagate(\n",
        "    observable, slices, operator_budget=op_budget\n",
        ")\n",
        "bp_circuit = combine_slices(remaining_slices)\n",
        "\n",
        "# Now backpropagate with truncation, using the same operator budget and\n",
        "# the defined truncation error budget\n",
        "bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(\n",
        "    observable,\n",
        "    slices,\n",
        "    operator_budget=op_budget,\n",
        "    truncation_error_budget=truncation_error_budget,\n",
        ")\n",
        "bp_circuit_trunc = combine_slices(\n",
        "    remaining_slices_trunc, include_barriers=False\n",
        ")\n",
        "\n",
        "# Now we transpile the original circuit and the two backpropagated circuits,\n",
        "# and apply the layout to the corresponding observables\n",
        "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n",
        "\n",
        "isa_circuit = pm.run(circuit)\n",
        "isa_bp_circuit = pm.run(bp_circuit)\n",
        "isa_bp_circuit_trunc = pm.run(bp_circuit_trunc)\n",
        "\n",
        "isa_observable = observable.apply_layout(isa_circuit.layout)\n",
        "isa_bp_observable = bp_obs.apply_layout(isa_bp_circuit.layout)\n",
        "isa_bp_observable_trunc = bp_obs_trunc.apply_layout(\n",
        "    isa_bp_circuit_trunc.layout\n",
        ")\n",
        "\n",
        "# Compare the 2-qubit depth of each transpiled circuit to see how much\n",
        "# depth backpropagation saved\n",
        "print(\n",
        "    f\"2-qubit depth without backpropagation: \"\n",
        "    f\"{isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n",
        ")\n",
        "print(\n",
        "    f\"2-qubit depth with backpropagation: \"\n",
        "    f\"{isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}\"\n",
        ")\n",
        "print(\n",
        "    f\"2-qubit depth with backpropagation and truncation: \"\n",
        "    f\"{isa_bp_circuit_trunc.depth(lambda x: x.operation.num_qubits == 2)}\"\n",
        ")\n",
        "\n",
        "pubs = [\n",
        "    (isa_circuit, isa_observable),\n",
        "    (isa_bp_circuit, isa_bp_observable),\n",
        "    (isa_bp_circuit_trunc, isa_bp_observable_trunc),\n",
        "]\n",
        "\n",
        "# Now we instantiate the Estimator primitive for the hardware with\n",
        "# ZNE and measurement error\n",
        "# mitigation and compute the three circuits and observables\n",
        "options = EstimatorOptions()\n",
        "options.default_precision = 0.01\n",
        "options.resilience_level = 2\n",
        "options.resilience.zne.noise_factors = [1, 1.2, 1.4]\n",
        "options.resilience.zne.extrapolator = [\"linear\"]\n",
        "estimator = EstimatorV2(mode=backend, options=options)\n",
        "\n",
        "estimator.options.environment.job_tags = [\"TUT_OBP\"]\n",
        "job = estimator.run(pubs)\n",
        "\n",
        "# Retrieve the results and the standard deviations\n",
        "result_no_bp = job.result()[0].data.evs.item()\n",
        "result_bp = job.result()[1].data.evs.item()\n",
        "result_bp_trunc = job.result()[2].data.evs.item()\n",
        "\n",
        "std_no_bp = job.result()[0].data.stds.item()\n",
        "std_bp = job.result()[1].data.stds.item()\n",
        "std_bp_trunc = job.result()[2].data.stds.item()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "4a0155bf",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Expectation value without backpropagation: 0.9543907942381811\n",
            "Backpropagated expectation value: 0.9445337385406468\n",
            "Backpropagated expectation value with truncation: 0.934050286970965\n"
          ]
        }
      ],
      "source": [
        "print(f\"Expectation value without backpropagation: {result_no_bp}\")\n",
        "print(f\"Backpropagated expectation value: {result_bp}\")\n",
        "print(f\"Backpropagated expectation value with truncation: {result_bp_trunc}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "37834c72",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "Text(0, 0.5, '$M_Z$')"
            ]
          },
          "execution_count": 17,
          "metadata": {},
          "output_type": "execute_result"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/operator-back-propagation/extracted-outputs/37834c72-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# Plot the results\n",
        "methods = [\n",
        "    \"No backpropagation\",\n",
        "    \"Backpropagation\",\n",
        "    \"Backpropagation w/ truncation\",\n",
        "]\n",
        "values = [result_no_bp, result_bp, result_bp_trunc]\n",
        "error_bars = [std_no_bp, std_bp, std_bp_trunc]\n",
        "\n",
        "ax = plt.gca()\n",
        "plt.bar(methods, values, color=\"#a56eff\", width=0.4, edgecolor=\"#8a3ffc\")\n",
        "plt.errorbar(methods, values, yerr=error_bars, fmt=\"o\", color=\"r\", capsize=5)\n",
        "plt.axhline(0.89)\n",
        "ax.set_ylim([0.8, 0.98])\n",
        "plt.text(0.25, 0.895, \"Exact result\")\n",
        "ax.set_ylabel(r\"$M_Z$\", fontsize=12)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "75f48e6a-c7e4-46f3-9d39-a7a877427a04",
      "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",
        "  * [Ungefähre Quantenkompilierung für Zeitentwicklungsschaltungen](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n",
        "  * [Mehrproduktformeln zur Verringerung des Trotter-Fehlers](/docs/tutorials/multi-product-formula)\n",
        "  * [`pauli-prop`](https://github.com/Qiskit/pauli-prop), ein mit Rust beschleunigtes Paket für die Pauli-Propagation, mit Tutorials zu OBP, klassischer Erwartungswertschätzung und Simulation unter Rauschen\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    },
    "hours": 1.5,
    "qpuSeconds": 240
  },
  "nbformat": 4,
  "nbformat_minor": 5
}