{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d2c31ae8",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Retropropagação do operador (OBP) para estimativa de valores esperados\"\n",
        "description: \"Este tutorial implementa um padrão do Qiskit para simular a dinâmica quântica de uma cadeia de spins de Heisenberg utilizando o 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",
        "# Retropropagação do operador (OBP) para estimativa de valores esperados\n",
        "\n",
        "*Estimativa de tempo de execução: 4 minutos em um processador Heron r3 (NOTA: Trata-se apenas de uma estimativa. (O tempo de execução pode variar.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Resultados do aprendizado\n",
        "\n",
        "Após concluir este tutorial, os usuários deverão compreender:\n",
        "\n",
        "* Como usar [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) para reduzir a profundidade do circuito quântico em troca de um maior número de execuções do circuito\n",
        "* Como usar [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) para construir hamiltonianos XYZ e seus circuitos de evolução temporal\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Pré-requisitos\n",
        "\n",
        "Recomendamos que os usuários estejam familiarizados com os seguintes tópicos antes de seguir com este tutorial:\n",
        "\n",
        "* Utilização da primitiva [Estimator](/docs/api/qiskit-ibm-runtime/estimator-v2) para calcular os valores esperados de uma variável observável\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Segundo plano\n",
        "\n",
        "A retropropagação de operadores é uma técnica que consiste em incorporar operações do final de um circuito quântico no observável medido, reduzindo geralmente a profundidade do circuito à custa de termos adicionais no observável. O objetivo é realizar a retropropagação em o máximo possível do circuito, sem permitir que a observável se torne excessivamente grande. Uma implementação baseada no Qiskit está disponível no complemento OBP Qiskit. Leia a [documentação](https://qiskit.github.io/qiskit-addon-obp/) correspondente para obter mais informações.\n",
        "\n",
        "Considere um circuito de exemplo no qual se pretende medir uma observável $O = \\sum_P c_P P$, sendo que $P$ são os estados de Pauli e $c_P$ são os coeficientes. Denotemos o circuito como um único \" $U$ \" unitário, que pode ser dividido logicamente em \" $U = U_C U_Q$ \", conforme mostrado na figura abaixo.\n",
        "\n",
        "![Diagrama do circuito mostrando 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",
        "A retropropagação do operador incorpora o operador unitário $U_C$ no observável, evoluindo-o como $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. Em outras palavras, parte do cálculo é realizada de forma clássica por meio da evolução do observável de $O$ para $O'$. O problema original pode agora ser reformulado como a medição do observável $O'$ para o novo circuito de menor profundidade, cujo operador unitário é $U_Q$.\n",
        "\n",
        "O espaço unitário $U_C$ é representado por um conjunto de fatias $U_C = U_S U_{S-1}...U_2U_1$. Existem várias maneiras de definir uma fatia. Por exemplo, no circuito do exemplo acima, cada camada de portas do tipo “ $R_{zz}$ ” e cada camada de portas do tipo “ $R_x$ ” pode ser considerada como uma fatia individual. A retropropagação envolve, tradicionalmente, o cálculo de um $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$. Cada fatia $U_s$ pode ser representada como $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, onde $P_s$ é um $n$ -qubit Pauli e $\\theta_s$ é um escalar. É fácil verificar 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",
        "No exemplo acima, se $\\{P,P_s\\} = 0$, precisamos executar dois circuitos quânticos, em vez de um, para calcular o valor esperado. Portanto, a retropropagação pode aumentar o número de termos no observável, levando a um maior número de execuções do circuito. Uma maneira de permitir uma retropropagação mais profunda no circuito, evitando ao mesmo tempo que o operador se torne excessivamente grande, é truncar os termos com coeficientes pequenos, em vez de adicioná-los ao operador. Por exemplo, no exemplo acima, seria possível optar por truncar o termo que envolve $P_sP$, desde que $\\theta_s$ seja suficientemente pequeno. O truncamento de termos pode reduzir o número de circuitos quânticos a serem executados, mas isso acarreta um erro no cálculo do valor esperado final, proporcional à magnitude dos coeficientes dos termos truncados.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisitos\n",
        "\n",
        "Antes de iniciar este tutorial, verifique se você tem os seguintes itens instalados:\n",
        "\n",
        "* Qiskit SDK v2.0 ou posterior, com suporte [à visualização](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.22 ou posterior (`pip install qiskit-ibm-runtime`)\n",
        "* Complemento OBP Qiskit 0.3 ou versão posterior (`pip install qiskit-addon-obp`)\n",
        "* Complemento Qiskit utils 0.3 ou posterior (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Instalação\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",
        "## Exemplo de simulador em pequena escala\n",
        "\n",
        "Este tutorial implementa um [padrão do Qiskit](/docs/guides/intro-to-patterns) para simular a dinâmica quântica de uma cadeia de spins de Heisenberg utilizando o [complemento OBP do Qiskit](https://github.com/Qiskit/qiskit-addon-obp). Observe que, em um simulador sem ruído, o valor esperado obtido com e sem retropropagação será o mesmo.\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Passo 1: Mapear entradas clássicas para um problema quântico\n",
        "\n",
        "<span id=\"map-the-time-evolution-of-a-quantum-heisenberg-model-to-a-quantum-experiment\" />\n",
        "\n",
        "#### Relacionar a evolução temporal de um modelo quântico de Heisenberg a um experimento quântico\n",
        "\n",
        "Primeiro, utilizaremos a [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) função de `qiskit-addon-utils` para gerar um hamiltoniano do tipo de Heisenberg em um determinado gráfico de conectividade. Este gráfico pode ser um gráfico de barras ( [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) ) ou um gráfico de colunas ( [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap) ). A seguir, utilizaremos uma cadeia `CouplingMap` linear 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": [
        "Em seguida, geramos um operador de Pauli que modela um hamiltoniano 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",
        "onde $G(V,E)$ é a representação gráfica do mapa de acoplamento. Para este tutorial, definimos $J_x, J_y, J_z$ como $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$ e $h_x, h_y, h_z$ como $\\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 do operador do qubit, podemos gerar um circuito quântico que modela sua evolução temporal. Utilizamos [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) a decomposição de Lie-Trotter para construir o circuito de evolução 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",
        "### Etapa 2: Otimizar o problema para execução em hardware quântico\n",
        "\n",
        "<span id=\"create-circuit-slices-to-backpropagate\" />\n",
        "\n",
        "#### Criar fatias de circuito para retropropagação\n",
        "\n",
        "A `backpropagate` função realiza a retropropagação de fatias inteiras do circuito de cada vez. Portanto, a escolha da divisão em fatias pode influenciar o desempenho da retropropagação em um determinado problema. Aqui, vamos agrupar portas do mesmo tipo em faixas usando a [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) função.\n",
        "\n",
        "Para uma explicação mais detalhada sobre o corte de circuitos, consulte este [guia prático](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) do pacote [`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 o tamanho que o operador pode atingir durante a retropropagação\n",
        "\n",
        "Durante a retropropagação, o número de termos no operador geralmente se aproxima rapidamente de $2^L$, onde $L$ é o número de fatias. Quando dois termos do operador não comutam em nível de qubit, precisamos de circuitos separados para obter os valores esperados correspondentes a eles. Por exemplo, se tivermos um observável de dois qubits $O = 0.1 XX + 0.3 IZ - 0.5 IX$, então, como $[XX,IX] = 0$, a medição em uma única base é suficiente para calcular os valores esperados desses dois termos. No entanto, o operador $IZ$ é anticomutativo em relação aos outros dois termos; portanto, precisamos de uma medição de base separada para calcular o valor esperado de $IZ$. Em outras palavras, precisamos de dois circuitos em vez de um para calcular $\\langle O \\rangle$. À medida que o número de termos no operador aumenta, existe a possibilidade de que o número necessário de execuções do circuito também aumente.\n",
        "\n",
        "O tamanho do operador pode ser limitado especificando o kwarg `operator_budget` da função `backpropagate` , que aceita uma [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) instância.\n",
        "\n",
        "Para controlar a quantidade de recursos adicionais (número de execuções do circuito e, consequentemente, o tempo de QPU necessário) alocados, restringimos o número máximo de grupos de Pauli comutativos por qubit que o observável retropropagado pode ter. Especificamos aqui que a retropropagação deve ser interrompida quando o número de grupos de Pauli comutativos por qubit no operador ultrapassar oito.\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",
        "#### Retropropagação de fatias do circuito\n",
        "\n",
        "Primeiro, especificamos o observável a ser $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, sendo $N$ o número de qubits. Faremos a retropropagação das fatias do circuito de evolução temporal até que os termos no observável não possam mais ser combinados em oito ou menos grupos Pauli comutáveis de qubit-wise.\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": [
        "Abaixo, você verá que realizamos a retropropagação em seis fatias, e os termos foram agrupados em seis, e não em oito grupos. Isso significa que a retropropagação de mais uma fatia faria com que o número de grupos de Pauli ultrapassasse oito. Podemos verificar se é esse o caso analisando os metadados retornados. Observe também que, nesta parte, a transformação do circuito é exata.  Ou seja, nenhum termo do novo observável $O’$ foi truncado. O circuito com retropropagação e o operador com retropropagação fornecem o mesmo resultado exato que o circuito e o operador originais.\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 o exemplo em pequena escala no simulador, não utilizaremos o truncamento. Isso ocorre porque, na ausência de ruído, o circuito com e sem retropropagação leva ao mesmo resultado, e o truncamento piora o resultado devido à aproximação adicional.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43f58cfb",
      "metadata": {},
      "source": [
        "<span id=\"transpile-the-circuits-into-the-basis-gate-set\" />\n",
        "\n",
        "#### Transpilar os circuitos para o conjunto de portas de base\n",
        "\n",
        "Agora, compilamos tanto o circuito original quanto o circuito resultante da retropropagação para a porta de base do backend. Não precisamos fazer a transpilagem no backend propriamente dito, já que vamos rodar em um simulador para a instância pequena.\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",
        "### Passo 3: Execute usando Qiskit primitives\n",
        "\n",
        "Primeiro, criamos dois [Blocos Primitivos Unificados](/docs/api/qiskit/primitives) (PUBs) correspondentes ao circuito original e ao circuito resultante da retropropagação. Em seguida, aplicamos os intervalos de confiança a um estimador ideal para obter os 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",
        "### Etapa 4: Realizar o pós-processamento e apresentar o resultado no formato clássico desejado\n",
        "\n",
        "Agora, calculamos os valores esperados dos circuitos original e retropropagado.\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 se esperar, os dois valores esperados coincidem. Como estamos utilizando um simulador de vetor de estado sem ruído, a retropropagação é uma transformação exata do par circuito-observável; portanto, os fluxos de trabalho original e retropropagado devem produzir o mesmo valor de $M_Z$. A vantagem da retropropagação só se torna evidente em hardware com ruído, onde o circuito retropropagado mais curto acumula menos erro, conforme ilustrado no exemplo de hardware em grande escala abaixo.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Exemplo de hardware em grande escala\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25",
      "metadata": {},
      "source": [
        "Ao desenvolver um experimento, é útil começar com um circuito simples para facilitar as visualizações e simulações. Agora, analisamos a retropropagação de operadores para um hamiltoniano de Heisenberg de 50 qubits com o mesmo conjunto de valores para os parâmetros $J$ e $h$ e o mesmo observável $M_Z$, mas para quatro passos de Trotter. O valor esperado ideal nessa escala não pode ser calculado por um método de força bruta; por isso, utilizamos uma rede tensorial e obtemos o valor esperado ideal como sendo $\\simeq 0.89$.\n",
        "\n",
        "Além da retropropagação, neste exemplo em grande escala, também apresentamos a retropropagação com truncamento. O ideal é realizar a retropropagação o máximo possível para reduzir a profundidade do circuito efetivo. No entanto, isso muitas vezes resulta em um grande número de termos não comutativos no observável atualizado, aumentando a sobrecarga quântica. Portanto, podemos eliminar os termos observáveis com coeficientes pequenos por meio de uma técnica chamada truncamento. Embora o truncamento permita uma maior propagação ao reduzir o número de termos no observável atualizado, ele também introduz alguma aproximação. Portanto, é necessário restringir o truncamento dentro de certos limites, para que o erro de aproximação não supere a redução de ruído obtida com uma retropropagação mais profunda.\n",
        "\n",
        "Para limitar o grau de truncamento, atribuímos um limite de erro para cada fatia, bem como um limite de erro total para todo o circuito submetido à retropropagação, utilizando a [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) função. Isso garante que o truncamento seja controlado tanto para cada fatia quanto para todo o circuito. Consulte também este [guia](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) para conhecer outras formas de distribuir o orçamento.\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óximas etapas\n",
        "\n",
        "Se você achou este trabalho interessante, talvez se interesse pelo seguinte material:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Recomendações\">\n",
        "  * [Compilação quântica aproximada para circuitos de evolução temporal](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n",
        "  * [Fórmulas multiprodutos para reduzir o erro de Trotter](/docs/tutorials/multi-product-formula)\n",
        "  * [`pauli-prop`](https://github.com/Qiskit/pauli-prop), um pacote acelerado por Rust para propagação de Pauli, com tutoriais que abordam OBP, estimativa clássica do valor esperado e simulação com ruído\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
}