{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d2c31ae8",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Retropropagación del operador (OBP) para la estimación de valores esperados\"\n",
        "description: \"Este tutorial implementa un patrón de Qiskit para simular la dinámica cuántica de una cadena de espines de Heisenberg utilizando 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",
        "# Retropropagación del operador (OBP) para la estimación de valores esperados\n",
        "\n",
        "*Tiempo estimado de ejecución: 4 minutos en un procesador Heron r3 (NOTA: Se trata únicamente de una estimación). (El tiempo de ejecución puede variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados del aprendizaje\n",
        "\n",
        "Una vez completado este tutorial, los usuarios deberían comprender:\n",
        "\n",
        "* Cómo utilizar [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) para reducir la profundidad del circuito cuántico a costa de aumentar el número de ejecuciones del circuito\n",
        "* Cómo utilizar [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) para construir hamiltonianos XYZ y sus circuitos de evolución temporal\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Requisitos previos\n",
        "\n",
        "Recomendamos a los usuarios que se familiaricen con los siguientes temas antes de seguir este tutorial:\n",
        "\n",
        "* Uso de la primitiva [«Estimator»](/docs/api/qiskit-ibm-runtime/estimator-v2) para calcular los valores esperados de una variable observable\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## En segundo plano\n",
        "\n",
        "La retropropagación de operadores es una técnica que consiste en incorporar operaciones del final de un circuito cuántico en el observable medido, lo que suele reducir la profundidad del circuito a costa de introducir términos adicionales en el observable. El objetivo es realizar la retropropagación en la mayor parte posible del circuito sin que la magnitud observable aumente demasiado. En el complemento OBP Qiskit hay disponible una implementación basada en Qiskit. Consulte la [documentación](https://qiskit.github.io/qiskit-addon-obp/) correspondiente para obtener más información.\n",
        "\n",
        "Consideremos un circuito de ejemplo en el que se va a medir una observable $O = \\sum_P c_P P$, donde $P$ son los estados de Pauli y $c_P$ son los coeficientes. Denotemos el circuito como un único « $U$ » unitario, que puede dividirse lógicamente en un « $U = U_C U_Q$ », tal y como se muestra en la siguiente figura.\n",
        "\n",
        "![Diagrama del circuito que muestra Uq seguido de Uc](https://quantum.cloud.ibm.com/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n",
        "\n",
        "La retropropagación de operadores absorbe el operador unitario $U_C$ en el observable, haciéndolo evolucionar como $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. En otras palabras, parte del cálculo se realiza de forma clásica mediante la evolución del observable desde $O$ hasta $O'$. El problema original puede reformularse ahora como la medición del observable $O'$ para el nuevo circuito de menor profundidad cuyo operador unitario es $U_Q$.\n",
        "\n",
        "El espacio unitario $U_C$ se representa como un conjunto de subespacios $U_C = U_S U_{S-1}...U_2U_1$. Existen varias formas de definir un subespacio. Por ejemplo, en el circuito del ejemplo anterior, cada capa de puertas « $R_{zz}$ » y cada capa de puertas « $R_x$ » pueden considerarse como una sección individual. La retropropagación implica, tradicionalmente, el cálculo de un $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$. Cada corte $U_s$ puede representarse como $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, donde $P_s$ es un operador de Pauli de -qubits $n$ y $\\theta_s$ es un escalar. Es fácil comprobar 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",
        "En el ejemplo anterior, si se cumple que « $\\{P,P_s\\} = 0$ », entonces tendremos que ejecutar dos circuitos cuánticos, en lugar de uno, para calcular el valor esperado. Por lo tanto, la retropropagación podría aumentar el número de términos en el observable, lo que daría lugar a un mayor número de ejecuciones del circuito. Una forma de permitir una retropropagación más profunda en el circuito, evitando al mismo tiempo que el operador se haga demasiado grande, consiste en truncar los términos con coeficientes pequeños, en lugar de añadirlos al operador. Por ejemplo, en el ejemplo anterior, se podría optar por truncar el término que incluye « $P_sP$ », siempre que « $\\theta_s$ » sea lo suficientemente pequeño. El truncamiento de términos puede reducir el número de circuitos cuánticos que hay que ejecutar, pero ello da lugar a un error en el cálculo del valor esperado final que es proporcional a la magnitud de los coeficientes de los términos truncados.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de empezar este tutorial, asegúrate de tener instalado lo siguiente:\n",
        "\n",
        "* Qiskit SDK v2.0 o posterior, con soporte [para visualización](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.22 o posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Complemento OBP Qiskit 0.3 o posterior (`pip install qiskit-addon-obp`)\n",
        "* Utilidades del complemento Qiskit: 0.3 o posterior (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configuración\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",
        "## Ejemplo de simulador a pequeña escala\n",
        "\n",
        "Este tutorial implementa un [patrón de Qiskit](/docs/guides/intro-to-patterns) para simular la dinámica cuántica de una cadena de espines de Heisenberg utilizando el [complemento OBP de Qiskit](https://github.com/Qiskit/qiskit-addon-obp). Ten en cuenta que, en un simulador sin ruido, el valor esperado obtenido con y sin retropropagación será el mismo.\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Paso 1: Asignar entradas clásicas a un problema cuántico\n",
        "\n",
        "<span id=\"map-the-time-evolution-of-a-quantum-heisenberg-model-to-a-quantum-experiment\" />\n",
        "\n",
        "#### Representar la evolución temporal de un modelo cuántico de Heisenberg en un experimento cuántico\n",
        "\n",
        "En primer lugar, utilizaremos la [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) función de `qiskit-addon-utils` para generar un hamiltoniano de tipo Heisenberg en un grafo de conectividad dado. Este gráfico puede ser tanto un gráfico de « [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) » como uno de « [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap) ». A continuación, utilizaremos una cadena `CouplingMap` lineal 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": [
        "A continuación, generamos un operador de Pauli que modela un hamiltoniano XYZ de Heisenberg:\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",
        "donde $G(V,E)$ es la gráfica de la aplicación de acoplamiento. En este tutorial, hemos utilizado $J_x, J_y, J_z$ y $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$, respectivamente, y $h_x, h_y, h_z$ y $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$, respectivamente.\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": [
        "A partir del operador del qubit, podemos generar un circuito cuántico que modele su evolución temporal. Hemos utilizado [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) la descomposición de Lie-Trotter para construir el circuito de evolución temporal.\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",
        "### Paso 2: Optimizar el problema para la ejecución en hardware cuántico\n",
        "\n",
        "<span id=\"create-circuit-slices-to-backpropagate\" />\n",
        "\n",
        "#### Crear segmentos de circuito para retropropagación\n",
        "\n",
        "La `backpropagate` función realiza la retropropagación de secciones completas del circuito de una vez. Por lo tanto, la elección del método de segmentación puede influir en el rendimiento de la retropropagación para un problema concreto. Aquí agruparemos las puertas del mismo tipo en secciones utilizando la [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) función.\n",
        "\n",
        "Si quieres obtener información más detallada sobre el «circuit slicing», consulta esta [guía práctica](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) del paquete [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) .\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",
        "#### Limitar el tamaño que puede alcanzar el operador durante la retropropagación\n",
        "\n",
        "Durante la retropropagación, el número de términos del operador tenderá, por lo general, a acercarse rápidamente a $2^L$, donde $L$ es el número de cortes. Cuando dos términos del operador no conmutan a nivel de qubit, necesitamos circuitos independientes para obtener los valores esperados que les corresponden. Por ejemplo, si tenemos un observable de dos qubits $O = 0.1 XX + 0.3 IZ - 0.5 IX$, dado que $[XX,IX] = 0$, basta con realizar una medición en una sola base para calcular los valores esperados de estos dos términos. Sin embargo, $IZ$ es anticomutativo con los otros dos términos, por lo que necesitamos una medición de base independiente para calcular el valor esperado de $IZ$. En otras palabras, necesitamos dos circuitos en lugar de uno para calcular $\\langle O \\rangle$. A medida que aumenta el número de términos del operador, existe la posibilidad de que también aumente el número de ejecuciones de circuitos necesarias.\n",
        "\n",
        "El tamaño del operador puede limitarse especificando el kwarg `operator_budget` de la función `backpropagate` , que acepta una [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instancia.\n",
        "\n",
        "Para controlar la cantidad de recursos adicionales (número de ejecuciones de circuitos y, por lo tanto, el tiempo de QPU necesario) que se asignan, limitamos el número máximo de grupos de Pauli conmutativos a nivel de qubit que puede tener el observable retropropagado. Aquí especificamos que la retropropagación debe detenerse cuando el número de grupos de Pauli conmutativos por qubit en el operador supere los ocho.\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",
        "#### Retropropagación de segmentos del circuito\n",
        "\n",
        "Primero especificamos que el observable es $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, siendo $N$ el número de qubits. Retropropagaremos cortes del circuito de evolución temporal hasta que los términos del observable ya no puedan combinarse en ocho o menos grupos de Pauli conmutables en qubit.\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": [
        "A continuación verás que hemos aplicado la retropropagación a seis cortes, y que los términos se han agrupado en seis grupos, en lugar de ocho. Esto implica que, si se aplicara la retropropagación a una capa más, el número de grupos de Pauli superaría los ocho. Podemos comprobar que así es examinando los metadatos devueltos. Ten en cuenta también que, en esta parte, la transformación del circuito es exacta.  Es decir, no se ha truncado ningún término de la nueva observable $O’$. El circuito de retropropagación y el operador de retropropagación dan el mismo resultado que el circuito y el operador originales.\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": [
        "Para el ejemplo a pequeña escala en un simulador, no utilizaremos el truncamiento. Esto se debe a que, en ausencia de ruido, el circuito con y sin retropropagación da el mismo resultado, y el truncamiento empeora el resultado debido a la aproximación añadida.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43f58cfb",
      "metadata": {},
      "source": [
        "<span id=\"transpile-the-circuits-into-the-basis-gate-set\" />\n",
        "\n",
        "#### Transpilar los circuitos al conjunto de puertas básico\n",
        "\n",
        "Ahora compilamos tanto el circuito original como el resultante de la retropropagación en la puerta base del backend. No es necesario realizar la transpilación en el backend real, ya que vamos a ejecutar el código en un simulador para la instancia pequeña.\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",
        "### Paso 3: Ejecutar utilizando Qiskit primitives\n",
        "\n",
        "En primer lugar, creamos dos [bloques primitivos unificados](/docs/api/qiskit/primitives) (PUB) que corresponden al circuito original y al circuito retropropagado. A continuación, aplicamos los intervalos de confianza a un estimador ideal para obtener los valores esperados.\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",
        "### Paso 4: Realizar el posprocesamiento y obtener el resultado en el formato clásico deseado\n",
        "\n",
        "Ahora obtenemos los valores esperados de los circuitos originales y de los circuitos con retropropagación.\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": [
        "Como era de esperar, los dos valores esperados coinciden. Dado que estamos trabajando con un simulador de vectores de estado sin ruido, la retropropagación es una transformación exacta del par circuito-observable, por lo que los flujos de trabajo original y el de retropropagación deben producir el mismo valor de « $M_Z$ ». La ventaja de la retropropagación solo se hace evidente en hardware con ruido, donde el circuito de retropropagación, al ser más corto, acumula menos error, tal y como se ilustra en el ejemplo de hardware a gran escala que se muestra a continuación.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Ejemplo de hardware a gran escala\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25",
      "metadata": {},
      "source": [
        "A la hora de diseñar un experimento, resulta útil empezar con un circuito sencillo para facilitar las visualizaciones y las simulaciones. Ahora analizamos la retropropagación de operadores para un hamiltoniano de Heisenberg de 50 qubits con el mismo conjunto de valores para los parámetros $J$ y $h$ y el mismo observable $M_Z$, pero para cuatro pasos de Trotter. El valor esperado ideal a esta escala no se puede calcular mediante un método de fuerza bruta, por lo que utilizamos una red tensorial y obtenemos que el valor esperado ideal es $\\simeq 0.89$\n",
        "\n",
        "Además de la retropropagación, en este ejemplo a gran escala también presentamos la retropropagación con truncamiento. Lo ideal es realizar la retropropagación lo máximo posible para reducir la profundidad del circuito efectivo. Sin embargo, esto suele dar lugar a un gran número de términos no conmutativos en el observable actualizado, lo que aumenta la sobrecarga cuántica. Por lo tanto, podemos eliminar los términos observables con coeficientes pequeños mediante una técnica denominada truncamiento. Si bien el truncamiento permite una mayor propagación al reducir el número de términos en el observable actualizado, también introduce cierta aproximación. Por lo tanto, es necesario limitar el truncamiento dentro de ciertos límites para que el error de aproximación no supere la reducción del ruido obtenida gracias a una retropropagación más profunda.\n",
        "\n",
        "Para limitar el grado de truncamiento, asignamos un margen de error a cada segmento, así como un margen de error total para todo el circuito de retropropagación, utilizando la [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) función. Esto garantiza que el truncamiento se controle tanto para cada sección como para el circuito completo. Consulte también esta [guía](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) para conocer otras formas de distribuir el presupuesto.\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",
        "## Próximos pasos\n",
        "\n",
        "Si te ha parecido interesante este trabajo, quizá te interese el siguiente material:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendaciones\">\n",
        "  * [Compilación cuántica aproximada para circuitos de evolución temporal](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n",
        "  * [Fórmulas multiproducto para reducir el error de Trotter](/docs/tutorials/multi-product-formula)\n",
        "  * [`pauli-prop`](https://github.com/Qiskit/pauli-prop), un paquete acelerado con Rust para la propagación de Pauli, con tutoriales sobre el método OBP, la estimación clásica del valor esperado y la simulación con ruido\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
}