{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "d2c31ae8",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Retropropagazione dell'operatore (OBP) per la stima dei valori attesi\"\n",
        "description: \"Questo tutorial implementa un modello Qiskit per la simulazione delle dinamiche quantistiche di una catena di spin di Heisenberg utilizzando 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",
        "# Retropropagazione dell'operatore (OBP) per la stima dei valori attesi\n",
        "\n",
        "*Stima della durata: 4 minuti su un processore Heron r3 (NOTA: si tratta solo di una stima). (La durata potrebbe variare.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8bf80006",
      "metadata": {},
      "source": [
        "<span id=\"learning-outcomes\" />\n",
        "\n",
        "## Risultati di apprendimento\n",
        "\n",
        "Dopo aver seguito questo tutorial, gli utenti dovrebbero aver compreso:\n",
        "\n",
        "* Come utilizzare [`qiskit-addon-obp`](https://github.com/Qiskit/qiskit-addon-obp) per ridurre la profondità del circuito quantistico a costo di un maggior numero di esecuzioni del circuito\n",
        "* Come utilizzare [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) per costruire gli hamiltoniani XYZ e i relativi circuiti di evoluzione temporale\n",
        "\n",
        "<span id=\"prerequisites\" />\n",
        "\n",
        "## Prerequisiti\n",
        "\n",
        "Consigliamo agli utenti di acquisire familiarità con i seguenti argomenti prima di seguire questo tutorial:\n",
        "\n",
        "* Utilizzo della primitiva [Estimator](/docs/api/qiskit-ibm-runtime/estimator-v2) per calcolare i valori attesi di una grandezza osservabile\n",
        "\n",
        "<span id=\"background\" />\n",
        "\n",
        "## Sfondo\n",
        "\n",
        "La retropropagazione degli operatori è una tecnica che consiste nell'assorbire le operazioni dalla fine di un circuito quantistico nell'osservabile misurato, riducendo in genere la profondità del circuito a scapito dell'introduzione di termini aggiuntivi nell'osservabile. L'obiettivo è quello di applicare il backpropagation a una porzione del circuito il più ampia possibile, senza che la grandezza dell'osservabile diventi eccessiva. Nell'add-on OBP Qiskit è disponibile un'implementazione basata su Qiskit. Per ulteriori informazioni, consultare la [documentazione](https://qiskit.github.io/qiskit-addon-obp/) corrispondente.\n",
        "\n",
        "Si consideri un circuito di esempio in cui si deve misurare un osservabile $O = \\sum_P c_P P$, dove $P$ sono i vettori di Pauli e $c_P$ sono i coefficienti. Indichiamo il circuito come un unico \" $U$ \" unitario, che può essere suddiviso logicamente in \" $U = U_C U_Q$ \", come illustrato nella figura sottostante.\n",
        "\n",
        "![Schema del circuito che mostra Uq seguito da Uc](https://quantum.cloud.ibm.com/docs/images/tutorials/improving-estimation-of-expectation-values-with-operator-backpropagation/logical-partitioning.avif)\n",
        "\n",
        "La retropropagazione dell'operatore incorpora l' $U_C$ e unitaria nell'osservabile, facendola evolvere come $O' = U_C^{\\dagger}OU_C = \\sum_P c_P U_C^{\\dagger}PU_C$. In altre parole, parte del calcolo viene eseguita in modo classico attraverso l'evoluzione dell'osservabile da $O$ a $O'$. Il problema originale può ora essere riformulato come la misurazione dell'osservabile $O'$ per il nuovo circuito a profondità inferiore, il cui operatore unitario è $U_Q$.\n",
        "\n",
        "L' $U_C$ e unitario è rappresentato come un insieme di sezioni $U_C = U_S U_{S-1}...U_2U_1$. Esistono diversi modi per definire una sezione. Ad esempio, nel circuito sopra riportato, ogni strato di gate \" $R_{zz}$ \" e ogni strato di gate \" $R_x$ \" può essere considerato come una singola sezione. La retropropagazione comporta, in linea di massima, il calcolo dell' $O' = \\Pi_{s=1}^S \\sum_P c_P U_s^{\\dagger} P U_s$. Ogni sezione $U_s$ può essere rappresentata come $U_s = exp(\\frac{-i\\theta_s P_s}{2})$, dove $P_s$ è un operatore di Pauli a -qubit $n$ e $\\theta_s$ è uno scalare. È facile verificare che\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",
        "Nell'esempio sopra riportato, se $\\{P,P_s\\} = 0$, allora per calcolare il valore atteso è necessario eseguire due circuiti quantistici, anziché uno solo. Pertanto, la retropropagazione potrebbe aumentare il numero di termini nell'osservabile, determinando un maggior numero di esecuzioni del circuito. Un modo per consentire una retropropagazione più profonda nel circuito, evitando al contempo che l'operatore diventi troppo grande, consiste nel troncare i termini con coefficienti piccoli, anziché aggiungerli all'operatore. Ad esempio, nell'esempio precedente, si potrebbe scegliere di troncare il termine che coinvolge $P_sP$, a condizione che $\\theta_s$ sia sufficientemente piccolo. Il troncamento dei termini può comportare una riduzione del numero di circuiti quantistici da eseguire, ma ciò comporta un errore nel calcolo del valore atteso finale, proporzionale all'ampiezza dei coefficienti dei termini troncati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "55b94021",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisiti\n",
        "\n",
        "Prima di iniziare questa esercitazione, assicuratevi di aver installato quanto segue:\n",
        "\n",
        "* Qiskit SDK v2.0 o versioni successive, con supporto [alla visualizzazione](/docs/api/qiskit/visualization)\n",
        "* Qiskit Runtime v0.22 o versioni successive (`pip install qiskit-ibm-runtime`)\n",
        "* Componente aggiuntivo OBP Qiskit 0.3 o versioni successive (`pip install qiskit-addon-obp`)\n",
        "* Qiskit addon utils 0.3 o versioni successive (`pip install qiskit-addon-utils`)\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "7db2e559",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configura\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",
        "## Esempio di simulatore su piccola scala\n",
        "\n",
        "Questo tutorial implementa un [modello Qiskit](/docs/guides/intro-to-patterns) per la simulazione delle dinamiche quantistiche di una catena di spin di Heisenberg utilizzando [l](https://github.com/Qiskit/qiskit-addon-obp) 'add-on OBP per Qiskit. Si noti che in un simulatore privo di rumore, il valore atteso ottenuto con e senza retropropagazione sarà lo stesso.\n",
        "\n"
      ]
    },
    {
      "attachments": {},
      "cell_type": "markdown",
      "id": "988ee237",
      "metadata": {},
      "source": [
        "<span id=\"step-1-map-classical-inputs-to-a-quantum-problem\" />\n",
        "\n",
        "### Fase 1: mappare gli input classici su un problema quantistico\n",
        "\n",
        "<span id=\"map-the-time-evolution-of-a-quantum-heisenberg-model-to-a-quantum-experiment\" />\n",
        "\n",
        "#### Mappare l'evoluzione temporale di un modello quantistico di Heisenberg in un esperimento quantistico\n",
        "\n",
        "Per prima cosa, useremo la [`generate_xyz_hamiltonian`](/docs/api/qiskit-addon-utils/problem-generators#generate_xyz_hamiltonian) funzione di `qiskit-addon-utils` per generare un hamiltoniano di tipo Heisenberg su un dato grafo di connettività. Questo grafico può essere sia un grafico a barre ( [rustworkx.PyGraph](https://www.rustworkx.org/apiref/rustworkx.PyGraph.html) ) che un grafico a linee ( [CouplingMap](/docs/api/qiskit/qiskit.transpiler.CouplingMap) ). Di seguito utilizzeremo una catena `CouplingMap` lineare di 10 qubit.\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": [
        "Successivamente, generiamo un operatore di Pauli che modella un hamiltoniano di 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",
        "dove $G(V,E)$ è il grafico della mappa di accoppiamento. Per questo tutorial abbiamo impostato $J_x, J_y, J_z$ in modo che rimandi a $\\frac{\\pi}{8}, \\frac{\\pi}{4}, \\frac{\\pi}{2}$ e $h_x, h_y, h_z$ in modo che rimandi a $\\frac{\\pi}{3}, \\frac{\\pi}{6}, \\frac{\\pi}{9}$.\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 partire dall'operatore del qubit, possiamo generare un circuito quantistico che ne modella l'evoluzione temporale. Abbiamo utilizzato [`generate_time_evolution_circuit`](/docs/api/qiskit-addon-utils/problem-generators#generate_time_evolution_circuit) la scomposizione di Lie-Trotter per costruire il circuito di evoluzione temporale.\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",
        "### Fase 2: Ottimizzazione del problema per l'esecuzione su hardware quantistico\n",
        "\n",
        "<span id=\"create-circuit-slices-to-backpropagate\" />\n",
        "\n",
        "#### Creare sezioni di circuito per la retropropagazione\n",
        "\n",
        "La `backpropagate` funzione esegue la retropropagazione di intere sezioni del circuito alla volta. Pertanto, la scelta della suddivisione in strati può influire sulle prestazioni della retropropagazione per un determinato problema. In questa sede raggrupperemo i gate dello stesso tipo in sezioni utilizzando la [`slice_by_depth`](/docs/api/qiskit-addon-utils/slicing#slice_by_depth) funzione.\n",
        "\n",
        "Per un approfondimento sul circuit slicing, dai un'occhiata a questa [guida pratica](https://qiskit.github.io/qiskit-addon-utils/how_tos/create_circuit_slices.html) del [`qiskit-addon-utils`](https://github.com/Qiskit/qiskit-addon-utils) pacchetto.\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",
        "#### Limitare la crescita dell'operatore durante la retropropagazione\n",
        "\n",
        "Durante la retropropagazione, il numero di termini nell'operatore tenderà generalmente ad avvicinarsi rapidamente a $2^L$, dove $L$ è il numero di sezioni. Quando due termini dell'operatore non commutano a livello di qubit, occorrono circuiti distinti per ottenere i valori attesi a essi corrispondenti. Ad esempio, se abbiamo un osservabile a due qubit $O = 0.1 XX + 0.3 IZ - 0.5 IX$, allora, dato che $[XX,IX] = 0$, è sufficiente effettuare una misurazione su una singola base per calcolare i valori attesi di questi due termini. Tuttavia, l'operatore $IZ$ è anticommutativo rispetto agli altri due termini, quindi occorre una misurazione di base separata per calcolare il valore atteso dell'operatore $IZ$. In altre parole, occorrono due circuiti anziché uno solo per calcolare l'operatore $\\langle O \\rangle$. Man mano che il numero di termini nell'operatore aumenta, è possibile che aumenti anche il numero di esecuzioni del circuito richieste.\n",
        "\n",
        "La dimensione dell'operatore può essere delimitata specificando il kwarg `operator_budget` della funzione `backpropagate` , che accetta un'istanza [OperatorBudget](/docs/api/qiskit-addon-obp/utils-simplify#operatorbudget) istanza.\n",
        "\n",
        "Per controllare la quantità di risorse aggiuntive (numero di iterazioni del circuito e, di conseguenza, il tempo richiesto dalla QPU) assegnate, limitiamo il numero massimo di gruppi di Pauli commutativi a livello di qubit che l'osservabile sottoposto a retropropagazione può avere. Qui specifichiamo che la retropropagazione deve interrompersi quando il numero di gruppi di Pauli commutativi a livello di qubit presenti nell'operatore supera gli otto.\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",
        "#### Retropropagare le sezioni dal circuito\n",
        "\n",
        "Per prima cosa specifichiamo che l'osservabile è $M_Z = \\frac{1}{N} \\sum_{i=1}^N \\langle Z_i \\rangle$, essendo $N$ il numero di qubit. Si retropropagheranno le fette del circuito di evoluzione temporale fino a quando i termini dell'osservabile non potranno più essere combinati in gruppi di Pauli commutativi di otto o meno 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": [
        "Come potete vedere qui sotto, abbiamo eseguito la retropropagazione su sei sezioni e i termini sono stati raggruppati in sei gruppi anziché in otto. Ciò implica che la retropropagazione di un'ulteriore sezione farebbe sì che il numero dei gruppi di Pauli superasse gli otto. Possiamo verificare che sia così esaminando i metadati restituiti. Si noti inoltre che in questa parte la trasformazione del circuito è esatta.  In altre parole, nessun termine della nuova grandezza osservabile $O’$ è stato troncato. Il circuito con retropropagazione e l'operatore con retropropagazione forniscono lo stesso risultato esatto del circuito e dell'operatore originali.\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": [
        "Per l'esempio su piccola scala eseguito su un simulatore, non ricorreremo al troncamento. Questo perché, in assenza di rumore, il circuito con e senza retropropagazione porta allo stesso risultato, mentre il troncamento peggiora il risultato a causa dell'approssimazione aggiuntiva.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "43f58cfb",
      "metadata": {},
      "source": [
        "<span id=\"transpile-the-circuits-into-the-basis-gate-set\" />\n",
        "\n",
        "#### Trasformare i circuiti nel set di porte di base\n",
        "\n",
        "Ora trasponiamo sia il circuito originale che quello ottenuto tramite backpropagation nel gate di base del backend. Non è necessario eseguire la transpilazione sul backend vero e proprio, dato che per l'istanza di piccole dimensioni utilizzeremo un simulatore.\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",
        "### Passaggio 3: eseguire utilizzando Qiskit primitives\n",
        "\n",
        "Per prima cosa, creiamo due [blocchi unificati primitivi](/docs/api/qiskit/primitives) (PUB) corrispondenti al circuito originale e al circuito ottenuto tramite retropropagazione. Quindi eseguiamo le simulazioni su un Estimator ideale per ottenere i valori attesi.\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",
        "### Fase 4: Elaborazione finale e restituzione del risultato nel formato classico desiderato\n",
        "\n",
        "Ora calcoliamo i valori attesi dei circuiti originali e di quelli sottoposti a retropropagazione.\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": [
        "Come previsto, i due valori attesi coincidono. Poiché stiamo utilizzando un simulatore di vettori di stato privo di rumore, la retropropagazione costituisce una trasformazione esatta della coppia circuito-osservabile; pertanto, il flusso di lavoro originale e quello ottenuto tramite retropropagazione devono produrre lo stesso valore dell' $M_Z$ e. Il vantaggio della retropropagazione diventa evidente solo su hardware soggetto a rumore, dove il circuito retropropagato, essendo più breve, accumula meno errori, come illustrato nell'esempio di hardware su larga scala riportato di seguito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0d6db390-e7a8-4efe-902c-8d9a312170c6",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Esempio di hardware su larga scala\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ae69c5e0-32b1-4f03-ab13-7b95a9acfd25",
      "metadata": {},
      "source": [
        "Quando si progetta un esperimento, è utile partire da un circuito semplice per facilitare la creazione di rappresentazioni grafiche e simulazioni. Ora esaminiamo la retropropagazione degli operatori per un hamiltoniano di Heisenberg a 50 qubit con lo stesso insieme di valori per i parametri $J$ e $h$ e lo stesso osservabile $M_Z$, ma per quattro passi di Trotter. Il valore atteso ideale a questa scala non può essere calcolato con un metodo di forza bruta, pertanto utilizziamo una rete tensoriale e otteniamo che il valore atteso ideale è pari a $\\simeq 0.89$.\n",
        "\n",
        "Oltre alla retropropagazione, in questo esempio su larga scala introduciamo anche la retropropagazione con troncamento. L'ideale sarebbe eseguire la retropropagazione il più possibile per ridurre la profondità del circuito effettivo. Tuttavia, ciò comporta spesso la presenza di un gran numero di termini non commutativi nell'osservabile aggiornato, aumentando il sovraccarico quantistico. Pertanto, possiamo eliminare i termini osservabili con coefficienti piccoli ricorrendo a una tecnica chiamata troncamento. Sebbene il troncamento consenta una maggiore propagazione riducendo il numero di termini nell'osservabile aggiornato, introduce anche una certa approssimazione. È quindi necessario limitare il troncamento entro certi limiti, in modo che l'errore di approssimazione non annulli la riduzione del rumore ottenuta grazie a una retropropagazione più profonda.\n",
        "\n",
        "Per limitare l'entità del troncamento, assegniamo un margine di errore a ciascuna sezione, oltre al margine di errore complessivo per l'intero circuito sottoposto a retropropagazione, utilizzando la [`setup_budget`](/docs/api/qiskit-addon-obp/utils-truncating#setup_budget) funzione. Ciò garantisce che il troncamento sia controllato sia per ogni singola sezione che per l'intero circuito. Consulta anche questa [guida](https://qiskit.github.io/qiskit-addon-obp/how_tos/truncate_operator_terms.html) per scoprire altri modi di ripartire il 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",
        "## Passi successivi\n",
        "\n",
        "Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti contenuti:\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Suggerimenti\">\n",
        "  * [Compilazione quantistica approssimativa per circuiti di evoluzione temporale](/docs/tutorials/approximate-quantum-compilation-for-time-evolution)\n",
        "  * [Formule multiprodotto per ridurre l'errore di Trotter](/docs/tutorials/multi-product-formula)\n",
        "  * [`pauli-prop`](https://github.com/Qiskit/pauli-prop), un pacchetto ottimizzato con Rust per la propagazione di Pauli, con tutorial dedicati all'OBP, alla stima classica del valore atteso e alla simulazione con rumore\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
}