{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d2c31ae8",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Rétropropagation de l'opérateur (OBP) pour l'estimation des valeurs attendues\"\n",
        "description: \"Ce tutoriel met en œuvre un modèle Qiskit permettant de simuler la dynamique quantique d'une chaîne de spins de Heisenberg à l'aide de qiskit-addon-obp\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore fontsize edgecolor circo simeq */}\n",
        "\n",
        "<span id=\"operator-backpropagation-obp-for-estimation-of-expectation-values\" />\n",
        "\n",
        "# Rétropropagation de l'opérateur (OBP) pour l'estimation des valeurs attendues\n",
        "\n",
        "*Durée estimée : 4 minutes sur un processeur Heron r3 (REMARQUE : il s'agit uniquement d'une estimation.) (Votre temps d'exécution peut varier.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Résultats d'apprentissage\n",
        "\n",
        "À l'issue de ce tutoriel, les utilisateurs devraient être en mesure de :\n",
        "\n",
        "* Comment utiliser [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) pour réduire la profondeur du circuit quantique au prix d'une augmentation du nombre d'exécutions du circuit\n",
        "* Comment utiliser [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) pour construire des hamiltoniens XYZ et leurs circuits d'évolution temporelle\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prérequis\n",
        "\n",
        "Nous recommandons aux utilisateurs de se familiariser avec les sujets suivants avant de suivre ce tutoriel :\n",
        "\n",
        "* Utilisation de la primitive «[ Estimator](/docs/api/qiskit-ibm-runtime/estimator-v2) » pour calculer les valeurs attendues d'une variable observable\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Arrière-plan\n",
        "\n",
        "La rétropropagation des opérateurs est une technique qui consiste à intégrer les opérations de la fin d'un circuit quantique dans l'observable mesurée, ce qui réduit généralement la profondeur du circuit au prix de l'ajout de termes supplémentaires dans l'observable. L'objectif est de faire remonter le plus de couches du circuit possible sans que la valeur observable ne devienne trop importante. Une implémentation basée sur Qiskit est disponible dans l'extension OBP Qiskit. Pour plus d'informations, consultez la [documentation](https://qiskit.github.io/qiskit-addon-obp/) correspondante.\n",
        "\n",
        "Prenons l'exemple d'un circuit dans lequel on souhaite mesurer une grandeur observable $O = \\sum_P c_P P$, où $P$ sont des quantités de Pauli et $c_P$ sont des coefficients. Considérons ce circuit comme une seule « $U$ » unitaire, qui peut être divisée logiquement en « $U = U_C U_Q$ », comme le montre la figure ci-dessous.\n",
        "\n",
        "![Schéma du circuit montrant Uq suivi de Uc](https://quantum.cloud.ibm.com/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n",
        "\n",
        "La rétropropagation des opérateurs intègre l' $U_C$ e unitaire dans l'observable en la faisant évoluer selon la relation $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. En d'autres termes, une partie du calcul est effectuée de manière classique par l'évolution de l'observable de $O$ à $O'$. Le problème initial peut désormais être reformulé comme la mesure de l'observable $O'$ pour le nouveau circuit de profondeur réduite dont l'opérateur unitaire est $U_Q$.\n",
        "\n",
        "L' $U_C$ e unitaire est représentée par un certain nombre de tranches $U_C = U_S U_{S-1}...U_2U_1$. Il existe plusieurs façons de définir une tranche. Par exemple, dans le circuit présenté ci-dessus, chaque couche de transistors de type « $R_{zz}$ » et chaque couche de transistors de type « $R_x$ » peuvent être considérées comme une tranche distincte. La rétropropagation implique généralement le calcul d' $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$. Chaque tranche $U_s$ peut être représentée sous la forme $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, où $P_s$ est un état de Pauli à $n$ qubits et $\\theta_s$ est un scalaire. Il est facile de vérifier que\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",
        "Dans l'exemple ci-dessus, si l'on a $\\{P,P_s\\} = 0$, il faut alors exécuter deux circuits quantiques, au lieu d'un seul, pour calculer la valeur d'espérance. Par conséquent, la rétropropagation pourrait augmenter le nombre de termes dans l'observable, ce qui entraînerait une augmentation du nombre d'exécutions du circuit. Une façon de permettre une rétropropagation plus profonde dans le circuit, tout en évitant que l'opérateur ne devienne trop volumineux, consiste à tronquer les termes dont les coefficients sont faibles, plutôt que de les ajouter à l'opérateur. Par exemple, dans l'exemple ci-dessus, on pourrait choisir de tronquer le terme contenant $P_sP$, à condition que $\\theta_s$ soit suffisamment petit. Le troncage des termes peut permettre de réduire le nombre de circuits quantiques à exécuter, mais cela entraîne une erreur dans le calcul de la valeur d'espérance finale, proportionnelle à l'amplitude des coefficients des termes tronqués.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Exigences\n",
        "\n",
        "Avant de commencer ce tutoriel, assurez-vous que les éléments suivants sont installés :\n",
        "\n",
        "* Qiskit SDK v2.0 ou version ultérieure, avec prise en charge [de la visualisation](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.22 ou version ultérieure (`pip install qiskit-ibm-runtime`)\n",
        "* Module complémentaire OBP Qiskit 0.3 ou version ultérieure (`pip install qiskit-addon-obp`)\n",
        "* Module complémentaire Qiskit utils 0.3 ou version ultérieure (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuration\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",
        "## Exemple de simulateur à petite échelle\n",
        "\n",
        "Ce tutoriel met en œuvre un [modèle Qiskit](/docs/guides/intro-to-patterns) permettant de simuler la dynamique quantique d'une chaîne de spins de Heisenberg à l'aide de [l](https://github.com/Qiskit/qiskit-addon-obp) 'extension OBP pour Qiskit. Il convient de noter que, dans un simulateur sans bruit, la valeur attendue obtenue avec et sans rétropropagation sera identique.\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Étape 1 : Mettre en correspondance les entrées classiques avec un problème quantique\n",
        "\n",
        "<span id=\"map-the-time-evolution-of-a-quantum-heisenberg-model-to-a-quantum-experiment\" />\n",
        "\n",
        "#### Mettre en correspondance l'évolution temporelle d'un modèle quantique de Heisenberg avec une expérience quantique\n",
        "\n",
        "Tout d'abord, nous utiliserons la [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) fonction de `qiskit-addon-utils` pour générer un hamiltonien de type Heisenberg sur un graphe de connectivité donné. Ce graphique peut être soit un graphique de type « [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) », soit un graphique de type « [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap) ». Dans ce qui suit, nous utiliserons une chaîne `CouplingMap` linéaire de 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": [
        "Ensuite, nous générons un opérateur de Pauli modélisant un hamiltonien de Heisenberg XYZ :\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",
        "où $G(V,E)$ est la courbe de la fonction de couplage. Pour ce tutoriel, nous avons utilisé $J_x, J_y, J_z$ et $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$, respectivement, ainsi que $h_x, h_y, h_z$ et $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$, respectivement.\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": [
        "À partir de l'opérateur de qubit, on peut générer un circuit quantique qui modélise son évolution temporelle. Nous avons utilisé [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) la décomposition de Lie-Trotter pour construire le circuit d'évolution temporelle.\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",
        "### Étape 2 : Optimiser le problème pour l'exécution sur du matériel quantique\n",
        "\n",
        "<span id=\"create-circuit-slices-to-backpropagate\" />\n",
        "\n",
        "#### Créer des tranches de circuit pour rétropropager\n",
        "\n",
        "La `backpropagate` fonction effectue une rétropropagation par tranches de circuit entières. Par conséquent, le choix du découpage peut avoir une incidence sur l'efficacité de la rétropropagation pour un problème donné. Ici, nous allons regrouper les portes de même type en tranches à l'aide de la [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) fonction.\n",
        "\n",
        "Pour en savoir plus sur le découpage de circuits, consultez le [guide pratique](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) fourni avec le [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) logiciel.\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",
        "#### Limiter la taille maximale que l'opérateur peut atteindre pendant la rétropropagation\n",
        "\n",
        "Au cours de la rétropropagation, le nombre de termes de l'opérateur tend généralement à atteindre rapidement la valeur d' $2^L$, où $L$ correspond au nombre de tranches. Lorsque deux termes de l'opérateur ne commutent pas au niveau des qubits, il faut recourir à des circuits distincts pour obtenir les valeurs d'espérance qui leur correspondent. Par exemple, si nous avons une observable à deux qubits $O = 0.1 XX + 0.3 IZ - 0.5 IX$, alors, puisque $[XX,IX] = 0$, une mesure dans une seule base suffit pour calculer les valeurs attendues de ces deux termes. Cependant, l' $IZ$ e est anticommutatif avec les deux autres termes; il nous faut donc une mesure de base distincte pour calculer la valeur attendue de l' $IZ$. En d'autres termes, il nous faut deux circuits au lieu d'un seul pour calculer l' $\\langle O \\rangle$. À mesure que le nombre de termes de l'opérateur augmente, il est possible que le nombre d'exécutions de circuits nécessaires augmente également.\n",
        "\n",
        "La taille de l'opérateur peut être limitée en spécifiant le kwarg `operator_budget` de la fonction `backpropagate` , qui accepte un [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instance.\n",
        "\n",
        "Afin de contrôler la quantité de ressources supplémentaires allouées (nombre d'itérations du circuit et, par conséquent, temps de calcul requis par le QPU), nous limitons le nombre maximal de groupes de Pauli commutatifs au niveau des qubits que l'observable rétropropagée est autorisée à posséder. Nous précisons ici que la rétropropagation doit s'arrêter lorsque le nombre de groupes de Pauli commutant au niveau des qubits dans l'opérateur dépasse huit.\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",
        "#### Rétropropagation des tranches à partir du circuit\n",
        "\n",
        "Tout d'abord, nous spécifions que l'observable doit être $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, $N$ étant le nombre de qubits. Nous rétropropagerons les tranches du circuit d'évolution temporelle jusqu'à ce que les termes de l'observable ne puissent plus être combinés en huit groupes de Pauli commutés dans le sens du qubit ou moins.\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": [
        "Vous verrez ci-dessous que nous avons effectué une rétropropagation sur six tranches, et que les termes ont été regroupés en six groupes et non en huit. Cela signifie que la rétropropagation d'une tranche supplémentaire ferait passer le nombre de groupes de Pauli au-delà de huit. Nous pouvons vérifier que c'est bien le cas en examinant les métadonnées renvoyées. Notez également que, dans cette partie, la transformation du circuit est exacte.  En d'autres termes, aucun terme de la nouvelle observable $O’$ n'a été tronqué. Le circuit à rétropropagation et l'opérateur à rétropropagation donnent exactement le même résultat que le circuit et l'opérateur d'origine.\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": [
        "Pour cet exemple à petite échelle sur un simulateur, nous n'utiliserons pas la troncature. En effet, en l'absence de bruit, le circuit avec ou sans rétropropagation donne le même résultat, et la troncature détériore ce résultat en raison de l'approximation supplémentaire.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43f58cfb",
      "metadata": {},
      "source": [
        "<span id=\"transpile-the-circuits-into-the-basis-gate-set\" />\n",
        "\n",
        "#### Transposer les circuits dans le jeu de portes de base\n",
        "\n",
        "Nous transpilons désormais à la fois le circuit d'origine et le circuit issu de la rétropropagation vers la porte de base du backend. Il n'est pas nécessaire de transcompiler sur le backend proprement dit, puisque nous allons utiliser un simulateur pour la petite instance.\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",
        "### Étape 3 : Exécutez à l'aide d' Qiskit primitives\n",
        "\n",
        "Tout d'abord, nous créons deux [blocs unifiés primitifs](/docs/api/qiskit/primitives) (PUB) correspondant au circuit d'origine et au circuit issu de la rétropropagation. Nous exécutons ensuite les publications sur un estimateur idéal afin d'obtenir les valeurs attendues.\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",
        "### Étape 4 : Traiter les données et restituer le résultat dans le format classique souhaité\n",
        "\n",
        "Nous obtenons maintenant les valeurs attendues des circuits d'origine et des circuits obtenus par rétropropagation.\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": [
        "Comme on pouvait s'y attendre, les deux valeurs attendues concordent. Comme nous utilisons un simulateur de vecteurs d'état sans bruit, la rétropropagation constitue une transformation exacte de la paire circuit-observable; par conséquent, les flux de travail d'origine et ceux issus de la rétropropagation doivent produire la même valeur d' $M_Z$. L'avantage de la rétropropagation ne se manifeste que sur du matériel bruyant, où le circuit rétropropagé, plus court, accumule moins d'erreurs, comme l'illustre l'exemple de matériel à grande échelle ci-dessous.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Exemple de matériel à grande échelle\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25",
      "metadata": {},
      "source": [
        "Lorsqu'on met au point une expérience, il est utile de commencer par un petit circuit afin de faciliter la visualisation et la simulation. Nous allons maintenant examiner la rétropropagation des opérateurs pour un hamiltonien de Heisenberg à 50 qubits, avec le même ensemble de valeurs pour les paramètres d' $J$ et d' $h$, ainsi que la même observable $M_Z$, mais en effectuant quatre étapes de Trotter. La valeur attendue idéale à cette échelle ne pouvant être calculée par une méthode de force brute, nous utilisons un réseau de tenseurs et obtenons une valeur attendue idéale de $\\simeq 0.89$.\n",
        "\n",
        "Outre la rétropropagation, nous présentons également, dans cet exemple à grande échelle, la rétropropagation avec troncature. Idéalement, nous souhaitons effectuer une rétropropagation aussi loin que possible afin de réduire la profondeur du circuit effectif. Cependant, cela entraîne souvent l'apparition d'un grand nombre de termes non commutatifs dans l'observable mise à jour, ce qui augmente la charge quantique. Nous pouvons donc éliminer les termes observables dont les coefficients sont faibles en recourant à une technique appelée « troncature ». Si la troncature permet une meilleure propagation en réduisant le nombre de termes dans l'observable mise à jour, elle introduit également une certaine approximation. Il est donc nécessaire de limiter la troncature afin que l'erreur d'approximation ne l'emporte pas sur la réduction du bruit obtenue grâce à une propagation en arrière plus profonde.\n",
        "\n",
        "Afin de limiter l'ampleur de la troncature, nous attribuons un budget d'erreur à chaque tranche ainsi qu'un budget d'erreur total pour l'ensemble du circuit soumis à la rétropropagation à l'aide de la [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) fonction. Cela permet de garantir que la coupure est contrôlée aussi bien pour chaque tranche que pour l'ensemble du circuit. Consultez également ce [guide](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) pour découvrir d'autres méthodes de répartition du budget.\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",
        "## Etapes suivantes\n",
        "\n",
        "Si ce travail vous a paru intéressant, les ressources suivantes pourraient vous intéresser :\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recommandations\">\n",
        "  * [Compilation quantique approximative pour les circuits d'évolution temporelle](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n",
        "  * [Formules multiproduits visant à réduire l'erreur de Trotter](/docs/tutorials/multi-product-formula)\n",
        "  * [`pauli-prop`](https://github.com/Qiskit/pauli-prop), un package optimisé en Rust pour la propagation de Pauli, proposant des tutoriels sur l'OBP, l'estimation classique de la valeur attendue et la simulation avec bruit\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
}