{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "9857bace",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Circuiti dinamici di riferimento con coppie Bell tagliate\"\n",
        "description: \"Confronta le capacità dei circuiti dinamici con il protocollo Cut Bell Pair\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore LOCC Ristè bitstr textcoords xytext fontsize Carrera */}\n",
        "\n",
        "<span id=\"benchmark-dynamic-circuits-with-cut-bell-pairs\" />\n",
        "\n",
        "# Circuiti dinamici di riferimento con coppie Bell tagliate\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d08a5e51",
      "metadata": {},
      "source": [
        "*Stima di utilizzo: 22 secondi su un processore r2 Heron (NOTA: si tratta solo di una stima). Il tempo di esecuzione può variare.)*\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0b9b92c8",
      "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 realizzare circuiti dinamici con misurazioni a metà percorso e feedforward classico per teletrasportare l'entanglement tra qubit distanti;\n",
        "* Come calcolare e interpretare la metrica di errore per quantificare la fedeltà della coppia di Bell su un dispositivo;\n",
        "* Come individuare, sulla base dei risultati dei test di benchmark, quali coppie di qubit sono più adatte alle operazioni basate sul LOCC.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "bff9db42",
      "metadata": {},
      "source": [
        "<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",
        "* [Concetti fondamentali dell'informatica quantistica](/learning/courses/basics-of-quantum-information), tra cui gli stati di Bell, l'entanglement e i gate quantistici;\n",
        "* Conoscenza dei [circuiti dinamici](/docs/guides/classical-feedforward-and-control-flow) ( [misure a metà circuito](/docs/guides/execute-dynamic-circuits#mid-circuit-measurements) e feedforward classico);\n",
        "* Conoscenze di base di [Qiskit SDK](/docs/guides) e [del servizio IBM Quantum® Compute Service](/docs/guides/compute-services#qiskit-runtime), nonché accesso a un [account IBM Quantum](/docs/guides/cloud-setup).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "77dca78b",
      "metadata": {},
      "source": [
        "<span id=\"background\" />\n",
        "\n",
        "## Sfondo\n",
        "\n",
        "L'hardware quantistico è tipicamente limitato alle interazioni locali, ma molti algoritmi richiedono l'entanglement di qubit distanti o addirittura [di qubit su processori separati](#references). I circuiti dinamici, ovvero circuiti con misurazione a metà circuito e feedforward, consentono di superare tali limitazioni utilizzando la comunicazione classica in tempo reale per implementare in modo efficace operazioni quantistiche non locali. In questo approccio, i risultati delle misurazioni effettuate su una parte di un circuito (o su una QPU) possono attivare in modo condizionale i gate su un'altra parte, consentendoci di teletrasportare l'entanglement su lunghe distanze. Questo costituisce la base dei sistemi **LOCC (Local Operations and Classical Communication)**, in cui utilizziamo stati di risorse intrecciate (coppie di Bell) e comunichiamo i risultati delle misurazioni in modo classico per collegare qubit distanti.\n",
        "\n",
        "Un utilizzo promettente del LOCC è quello di realizzare porte CNOT virtuali a lungo raggio tramite teletrasporto, come mostrato nel [tutorial sull'entanglement a lungo raggio](/docs/tutorials/long-range-entanglement). Invece di un CNOT diretto a lungo raggio (che la connettività hardware potrebbe non consentire), creiamo coppie di Bell ed eseguiamo un'implementazione del gate basata sul teletrasporto. Tuttavia, la fedeltà di tali operazioni dipende dalle caratteristiche dell'hardware. La decoerenza dei qubit durante il ritardo necessario (in attesa dei risultati delle misurazioni) e la latenza della comunicazione classica possono degradare lo stato di entanglement. Inoltre, gli errori nelle misurazioni intermedie del circuito sono più difficili da correggere rispetto agli errori nelle misurazioni finali, poiché si propagano al resto del circuito attraverso i gate condizionali.\n",
        "\n",
        "[Nell'esperimento](#references) di riferimento, gli autori introducono un parametro di riferimento della fedeltà della coppia di Bell per individuare quali parti di un dispositivo siano più adatte all'entanglement basato sulla tecnologia LOCC. L'idea è quella di far funzionare un piccolo circuito dinamico su ogni gruppo di quattro qubit collegati nel processore. Questo circuito a quattro qubit crea innanzitutto una coppia di Bell sui due qubit centrali, quindi li utilizza come risorsa per intrecciare i due qubit periferici tramite LOCC. In pratica, i qubit 1 e 2 vengono preparati localmente in una coppia di Bell *non tagliata* (una coppia di Bell creata direttamente con un Hadamard e un CNOT, senza teletrasporto), dopodiché una procedura di teletrasporto utilizza quella coppia di Bell per intrecciare i qubit 0 e 3. I qubit 1 e 2 vengono misurati durante l'esecuzione del circuito e, in base a tali risultati, vengono applicate le correzioni di Pauli (una X sul qubit 3 e una Z sul qubit 0). I qubit 0 e 3 rimangono quindi in uno stato di Bell alla fine del circuito.\n",
        "\n",
        "Per quantificare la qualità di questa coppia intrecciata finale, misuriamo i suoi stabilizzatori: in particolare, la parità nella $Z$ base ( $Z_0Z_3$ ) e nella $X$ base ( $X_0X_3$ ). Per una coppia di Bell perfetta, entrambe queste aspettative sono pari a +1. In pratica, il rumore dell'hardware ridurrà questi valori. Ripetiamo quindi il circuito due volte per ogni coppia di qubit: un circuito misura i qubit 0 e 3 nella $Z$ base, mentre un altro li misura nella $X$ base. Dai risultati otteniamo una stima di $\\langle Z_0Z_3\\rangle$ e $\\langle X_0X_3\\rangle$ per quella coppia di qubit. Utilizziamo l'errore quadratico medio (MSE) di questi stabilizzatori rispetto al valore ideale (1) come semplice metrica della fedeltà dell'entanglement. Un MSE più basso significa che i due qubit hanno raggiunto uno stato di Bell più vicino all'ideale (maggiore fedeltà), mentre un MSE più alto indica un errore maggiore. Eseguendo la scansione di questo esperimento sul dispositivo, possiamo valutare le capacità di misurazione e feedforward di diversi gruppi di qubit e identificare le migliori coppie di qubit per le operazioni LOCC.\n",
        "\n",
        "Questo tutorial illustra l'esperimento condotto su un dispositivo IBM Quantum® per mostrare come i circuiti dinamici possano essere utilizzati per generare e valutare l'entanglement tra qubit distanti. Mapperemo tutte le catene lineari a quattro qubit presenti sul dispositivo, eseguiremo il circuito di teletrasporto su ciascuna di esse e infine visualizzeremo la distribuzione dei valori di MSE. Questa procedura completa illustra come sfruttare le funzionalità del servizio di calcolo quantistico \" IBM Quantum \" e dei circuiti dinamici per orientare le scelte relative all'hardware, al fine di ridurre i circuiti o distribuire algoritmi quantistici all'interno di un sistema modulare.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "152c479f",
      "metadata": {},
      "source": [
        "<span id=\"requirements\" />\n",
        "\n",
        "## Requisiti\n",
        "\n",
        "Prima di iniziare questo tutorial, assicurati di avere installato quanto segue:\n",
        "\n",
        "* Qiskit SDK v2.0 o versioni successive, con supporto [alla visualizzazione](/docs/api/qiskit/visualization)\n",
        "* v0.40Qiskit Runtime o successivamente (`pip install qiskit-ibm-runtime`)\n",
        "* Qiskit Aer v0.17 o versioni successive (`pip install qiskit-aer`)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e67f9466",
      "metadata": {},
      "source": [
        "<span id=\"setup\" />\n",
        "\n",
        "## Configura\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "b59e4534",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit import QuantumCircuit\n",
        "\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler\n",
        "from qiskit.transpiler import generate_preset_pass_manager\n",
        "\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "\n",
        "def create_bell_stab(initial_layouts):\n",
        "    \"\"\"\n",
        "    Create a circuit for a 1D chain of qubits (number\n",
        "    of qubits must be a multiple of 4), where a middle\n",
        "    Bell pair is consumed to create a Bell at the edge.\n",
        "    Takes as input a list of lists, where each element\n",
        "    of the list is a 1D chain of physical qubits that is\n",
        "    used as the initial_layout for the transpiled circuit.\n",
        "    Returns a list of length-2 tuples, each tuple\n",
        "    contains a circuit to measure the ZZ stabilizer and\n",
        "    a circuit to measure the XX stabilizer of the edge\n",
        "    Bell state.\n",
        "    \"\"\"\n",
        "    bell_circuits = []\n",
        "    for (\n",
        "        initial_layout\n",
        "    ) in initial_layouts:  # Iterate over chains of physical qubits\n",
        "        assert (\n",
        "            len(initial_layout) % 4 == 0\n",
        "        ), \"The length of the chain must be a multiple of 4, \"\n",
        "        f\"len(inital_layout)={len(initial_layout)}\"\n",
        "        num_pairs = len(initial_layout) // 4\n",
        "\n",
        "        bell_parallel = QuantumCircuit(4 * num_pairs, 4 * num_pairs)\n",
        "\n",
        "        for pair_idx in range(num_pairs):\n",
        "            (q0, q1, q2, q3) = (\n",
        "                pair_idx * 4,\n",
        "                pair_idx * 4 + 1,\n",
        "                pair_idx * 4 + 2,\n",
        "                pair_idx * 4 + 3,\n",
        "            )\n",
        "            (c0, c1) = pair_idx * 4, pair_idx * 4 + 3  # edge qubits\n",
        "            (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2  # middle qubits\n",
        "\n",
        "            bell_parallel.h(q0)\n",
        "            bell_parallel.h(q1)\n",
        "            bell_parallel.cx(q1, q2)\n",
        "            bell_parallel.cx(q0, q1)\n",
        "            bell_parallel.cx(q2, q3)\n",
        "            bell_parallel.h(q2)\n",
        "\n",
        "        # add barrier BEFORE measurements and add id in conditional\n",
        "        bell_parallel.barrier()\n",
        "        for pair_idx in range(num_pairs):\n",
        "            (q0, q1, q2, q3) = (\n",
        "                pair_idx * 4,\n",
        "                pair_idx * 4 + 1,\n",
        "                pair_idx * 4 + 2,\n",
        "                pair_idx * 4 + 3,\n",
        "            )\n",
        "            (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2  # middle qubits\n",
        "\n",
        "            bell_parallel.measure(q1, ca0)\n",
        "            bell_parallel.measure(q2, ca1)\n",
        "        # bell_parallel.barrier() #remove barrier after measurement\n",
        "\n",
        "        for pair_idx in range(num_pairs):\n",
        "            (q0, q1, q2, q3) = (\n",
        "                pair_idx * 4,\n",
        "                pair_idx * 4 + 1,\n",
        "                pair_idx * 4 + 2,\n",
        "                pair_idx * 4 + 3,\n",
        "            )\n",
        "            (ca0, ca1) = pair_idx * 4 + 1, pair_idx * 4 + 2  # middle qubits\n",
        "            with bell_parallel.if_test((ca0, 1)):\n",
        "                bell_parallel.x(q3)\n",
        "            with bell_parallel.if_test((ca1, 1)):\n",
        "                bell_parallel.z(q0)\n",
        "                bell_parallel.id(q0)  # add id here for correct alignment\n",
        "\n",
        "        bell_zz = bell_parallel.copy()\n",
        "        bell_zz.barrier()\n",
        "        bell_xx = bell_parallel.copy()\n",
        "        bell_xx.barrier()\n",
        "        for pair_idx in range(num_pairs):\n",
        "            (q0, q1, q2, q3) = (\n",
        "                pair_idx * 4,\n",
        "                pair_idx * 4 + 1,\n",
        "                pair_idx * 4 + 2,\n",
        "                pair_idx * 4 + 3,\n",
        "            )\n",
        "            bell_xx.h(q0)\n",
        "            bell_xx.h(q3)\n",
        "        bell_xx.barrier()\n",
        "        for pair_idx in range(num_pairs):\n",
        "            (q0, q1, q2, q3) = (\n",
        "                pair_idx * 4,\n",
        "                pair_idx * 4 + 1,\n",
        "                pair_idx * 4 + 2,\n",
        "                pair_idx * 4 + 3,\n",
        "            )\n",
        "            (c0, c1) = pair_idx * 4, pair_idx * 4 + 3  # edge qubits\n",
        "\n",
        "            bell_zz.measure(q0, c0)\n",
        "            bell_zz.measure(q3, c1)\n",
        "\n",
        "            bell_xx.measure(q0, c0)\n",
        "            bell_xx.measure(q3, c1)\n",
        "\n",
        "        bell_circuits.append(bell_zz)\n",
        "        bell_circuits.append(bell_xx)\n",
        "\n",
        "    return bell_circuits\n",
        "\n",
        "\n",
        "def get_mse(result, initial_layouts):\n",
        "    \"\"\"\n",
        "    given a result object and the initial layouts,\n",
        "    returns a dict of layouts and their mse\n",
        "    \"\"\"\n",
        "    layout_mse = {}\n",
        "    for layout_idx, initial_layout in enumerate(initial_layouts):\n",
        "        layout_mse[tuple(initial_layout)] = {}\n",
        "\n",
        "        num_pairs = len(initial_layout) // 4\n",
        "\n",
        "        counts_zz = result[2 * layout_idx].data.c.get_counts()\n",
        "        total_shots = sum(counts_zz.values())\n",
        "\n",
        "        # Get ZZ expectation value\n",
        "        exp_zz_list = []\n",
        "        for pair_idx in range(num_pairs):\n",
        "            exp_zz = 0\n",
        "            for bitstr, shots in counts_zz.items():\n",
        "                bitstr = bitstr[::-1]  # reverse order to big endian\n",
        "                b1, b0 = (\n",
        "                    bitstr[pair_idx * 4],\n",
        "                    bitstr[pair_idx * 4 + 3],\n",
        "                )  # parse bitstring to get edge measurements for each 4-q chain\n",
        "                z_val0 = 1 if b0 == \"0\" else -1\n",
        "                z_val1 = 1 if b1 == \"0\" else -1\n",
        "                exp_zz += z_val0 * z_val1 * shots\n",
        "            exp_zz /= total_shots\n",
        "            exp_zz_list.append(exp_zz)\n",
        "\n",
        "        counts_xx = result[2 * layout_idx + 1].data.c.get_counts()\n",
        "        total_shots = sum(counts_xx.values())\n",
        "\n",
        "        # Get XX expectation value\n",
        "        exp_xx_list = []\n",
        "        for pair_idx in range(num_pairs):\n",
        "            exp_xx = 0\n",
        "            for bitstr, shots in counts_xx.items():\n",
        "                bitstr = bitstr[::-1]  # reverse order to big endian\n",
        "                b1, b0 = (\n",
        "                    bitstr[pair_idx * 4],\n",
        "                    bitstr[pair_idx * 4 + 3],\n",
        "                )  # parse bitstring to get edge measurements for each 4-q chain\n",
        "                x_val0 = 1 if b0 == \"0\" else -1\n",
        "                x_val1 = 1 if b1 == \"0\" else -1\n",
        "                exp_xx += x_val0 * x_val1 * shots\n",
        "            exp_xx /= total_shots\n",
        "            exp_xx_list.append(exp_xx)\n",
        "\n",
        "        mse_list = [\n",
        "            ((exp_zz - 1) ** 2 + (exp_xx - 1) ** 2) / 2\n",
        "            for exp_zz, exp_xx in zip(exp_zz_list, exp_xx_list)\n",
        "        ]\n",
        "\n",
        "        print(f\"layout {initial_layout}\")\n",
        "        for idx in range(num_pairs):\n",
        "            layout_mse[tuple(initial_layout)][\n",
        "                tuple(initial_layout[4 * idx : 4 * idx + 4])\n",
        "            ] = mse_list[idx]\n",
        "            print(\n",
        "                f\"qubits: {initial_layout[4*idx:4*idx+4]}, mse:, \"\n",
        "                f\"{round(mse_list[idx],4)}\"\n",
        "            )\n",
        "            # print(f'exp_zz: {round(exp_zz_list[idx],4)},\n",
        "            # exp_xx: {round(exp_xx_list[idx],4)}')\n",
        "        print(\" \")\n",
        "    return layout_mse\n",
        "\n",
        "\n",
        "def plot_mse_ecdfs(layouts_mse, combine_layouts=False):\n",
        "    \"\"\"\n",
        "    Plot CDF of MSE data for multiple layouts.\n",
        "    Optionally combine all data in a single CDF\n",
        "    \"\"\"\n",
        "\n",
        "    if not combine_layouts:\n",
        "        for initial_layout, layouts in layouts_mse.items():\n",
        "            sorted_layouts = dict(\n",
        "                sorted(layouts.items(), key=lambda item: item[1])\n",
        "            )  # sort layouts by mse\n",
        "\n",
        "            # get layouts and mses\n",
        "            layout_list = list(sorted_layouts.keys())\n",
        "            mse_list = np.asarray(list(sorted_layouts.values()))\n",
        "\n",
        "            # convert to numpy\n",
        "            x = np.array(mse_list)\n",
        "            y = np.arange(1, len(x) + 1) / len(x)\n",
        "\n",
        "            # Prepend (x[0], 0) to start CDF at zero\n",
        "            x = np.insert(x, 0, x[0])\n",
        "            y = np.insert(y, 0, 0)\n",
        "\n",
        "            # Create the plot\n",
        "            plt.plot(\n",
        "                x,\n",
        "                y,\n",
        "                marker=\"x\",\n",
        "                linestyle=\"-\",\n",
        "                label=f\"qubits: {initial_layout}\",\n",
        "            )\n",
        "\n",
        "            # add qubits labels for the edge pairs\n",
        "            for xi, yi, q in zip(x[1:], y[1:], layout_list):\n",
        "                plt.annotate(\n",
        "                    [q[0], q[3]],\n",
        "                    (xi, yi),\n",
        "                    textcoords=\"offset points\",\n",
        "                    xytext=(5, -10),\n",
        "                    ha=\"left\",\n",
        "                    fontsize=8,\n",
        "                )\n",
        "\n",
        "    elif combine_layouts:\n",
        "        all_layouts = {}\n",
        "        all_initial_layout = []\n",
        "        for (\n",
        "            initial_layout,\n",
        "            layouts,\n",
        "        ) in layouts_mse.items():  # puts together all layout information\n",
        "            all_layouts.update(layouts)\n",
        "            all_initial_layout += initial_layout\n",
        "\n",
        "        sorted_layouts = dict(\n",
        "            sorted(all_layouts.items(), key=lambda item: item[1])\n",
        "        )  # sort layouts by mse\n",
        "\n",
        "        # get layouts and mses\n",
        "        layout_list = list(sorted_layouts.keys())\n",
        "        mse_list = np.asarray(list(sorted_layouts.values()))\n",
        "\n",
        "        # convert to numpy\n",
        "        x = np.array(mse_list)\n",
        "        y = np.arange(1, len(x) + 1) / len(x)\n",
        "\n",
        "        # Prepend (x[0], 0) to start CDF at zero\n",
        "        x = np.insert(x, 0, x[0])\n",
        "        y = np.insert(y, 0, 0)\n",
        "\n",
        "        # Create the plot\n",
        "        plt.plot(\n",
        "            x,\n",
        "            y,\n",
        "            marker=\"x\",\n",
        "            linestyle=\"-\",\n",
        "            label=f\"qubits: {sorted(list(set(all_initial_layout)))}\",\n",
        "        )\n",
        "\n",
        "        # add qubit labels for the edge pairs\n",
        "        for xi, yi, q in zip(x[1:], y[1:], layout_list):\n",
        "            plt.annotate(\n",
        "                [q[0], q[3]],\n",
        "                (xi, yi),\n",
        "                textcoords=\"offset points\",\n",
        "                xytext=(5, -10),\n",
        "                ha=\"left\",\n",
        "                fontsize=8,\n",
        "            )\n",
        "\n",
        "    plt.xscale(\"log\")\n",
        "    plt.xlabel(\"Mean squared error of ⟨ZZ⟩ and ⟨XX⟩\")\n",
        "    plt.ylabel(\"Cumulative distribution function\")\n",
        "    plt.title(\"CDF for different initial layouts\")\n",
        "    plt.grid(alpha=0.3)\n",
        "    plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "30e65223",
      "metadata": {},
      "source": [
        "<span id=\"small-scale-simulator-example\" />\n",
        "\n",
        "## Esempio di simulatore su piccola scala\n",
        "\n",
        "Prima di eseguire il codice sulla QPU reale, verifichiamo che il circuito produca una coppia di Bell testandolo su un simulatore privo di rumore con una catena di quattro qubit `[0, 1, 2, 3]`. Utilizziamo l' IBM Quantum `Sampler` con come `AerSimulator` modalità di backend per l'esecuzione dei circuiti.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2a16f346",
      "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",
        "Il primo passo consiste nel creare una serie di circuiti quantistici per valutare tutte le possibili connessioni di coppie di Bell, adattate alla topologia del dispositivo. `[0, 1, 2, 3]`Iniziamo realizzando tali circuiti con una catena di quattro qubit.\n",
        "\n",
        "La `create_bell_stab()` routine esegue le seguenti operazioni per ogni catena:\n",
        "\n",
        "* Prepara una coppia Bell centrale: applica un Hadamard sul qubit 1 e un CNOT dal qubit 1 al qubit 2. Questo intreccia i qubit 1 e 2 (creando uno stato $|\\Phi^+\\rangle = (|00\\rangle + |11\\rangle)/\\sqrt{2}$ di Bell).\n",
        "* Intreccia i qubit periferici: applica un CNOT dal qubit 0 al qubit 1 e un CNOT dal qubit 2 al qubit 3. Questo collega le coppie inizialmente separate in modo che i qubit 0 e 3 diventino intrecciati dopo i passaggi successivi. Viene applicato anche un Hadamard sul qubit 2 (questo, combinato con i precedenti CNOT, costituisce parte di una misurazione di Bell sui qubit 1 e 2). A questo punto, i qubit 0 e 3 non sono ancora intrecciati, ma i qubit 1 e 2 sono intrecciati con essi in uno stato più ampio a quattro qubit.\n",
        "* Misurazioni a metà circuito e feedforward: i qubit 1 e 2 (i qubit centrali) vengono misurati nella base computazionale, producendo due bit classici. Sulla base dei risultati di tali misurazioni, applichiamo operazioni condizionali: se la misurazione del qubit 1 (chiamiamola bit $m_{12}$ ) è 1, applichiamo un $X$ gate sul qubit 3; se la misurazione del qubit 2 ( $m_{21}$ ) è 1, applichiamo un $Z$ gate sul qubit 0. Questi gate condizionali (realizzati utilizzando il Qiskit `if_test`/`if_else` construct) implementano le correzioni standard del teletrasporto. Essi \"annullano\" i ribaltamenti casuali di Pauli che si verificano a causa della proiezione dei qubit 1 e 2, assicurando che i qubit 0 e 3 finiscano in uno stato di Bell noto, indipendentemente dai risultati della misurazione. Dopo questo passaggio, i qubit 0 e 3 dovrebbero idealmente essere intrecciati nello stato di Bell $|\\Phi^+\\rangle$.\n",
        "* Misurare gli stabilizzatori della coppia di campane: Abbiamo quindi suddiviso il circuito in due versioni. Nella prima versione, misuriamo lo $ZZ$ stabilizzatore sui qubit 0 e 3. Nella seconda versione, misuriamo lo $XX$ stabilizzatore su questi qubit.\n",
        "\n",
        "Per ogni configurazione iniziale a quattro qubit, la funzione sopra riportata restituisce due circuiti (uno per la misurazion $ZZ$ e e uno per la misurazione stabilizzatrice $XX$ ). Questi circuiti includono misurazioni a metà circuito e operazioni condizionali (if/else), che costituiscono le istruzioni fondamentali del circuito dinamico.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "160e25c5",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/160e25c5-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "execution_count": 19,
          "metadata": {},
          "output_type": "execute_result"
        }
      ],
      "source": [
        "from qiskit_aer import AerSimulator\n",
        "\n",
        "# 4-qubit chain for simulation\n",
        "sim_layout = [[0, 1, 2, 3]]\n",
        "\n",
        "aer_backend = AerSimulator()\n",
        "sim_circuits = create_bell_stab(sim_layout)\n",
        "sim_circuits[1].draw(\"mpl\", fold=-1, idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "17953786",
      "metadata": {},
      "source": [
        "<span id=\"step-2-optimize-the-problem-for-quantum-hardware-execution\" />\n",
        "\n",
        "### Fase 2: Ottimizzare il problema per l'esecuzione su hardware quantistico\n",
        "\n",
        "Prima di eseguire i nostri circuiti, dobbiamo traspilarli in operazioni a livello di porta supportate dal backend specificato. La transpilazione mapperà il circuito astratto sui qubit fisici e sull'insieme di porte del backend selezionato. Poiché abbiamo già scelto qubit fisici specifici per ciascuna catena (specificando un `initial_layout` nel generatore di circuiti), utilizziamo il transpiler `optimization_level=0` con quella configurazione fissa. Questo indica a Qiskit di non riassegnare i qubit né di eseguire ottimizzazioni complesse che potrebbero alterare la struttura del circuito. Vogliamo mantenere la sequenza delle operazioni (in particolare i gate condizionali) esattamente come specificato.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "61dd47b7",
      "metadata": {},
      "outputs": [],
      "source": [
        "pm_sim = generate_preset_pass_manager(\n",
        "    optimization_level=0, backend=aer_backend, initial_layout=sim_layout[0]\n",
        ")\n",
        "isa_sim_circuits = pm_sim.run(sim_circuits)\n",
        "isa_sim_circuits[1].draw(\"mpl\", fold=-1, idle_wires=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cd161710",
      "metadata": {},
      "source": [
        "<span id=\"step-3-execute-using-qiskit-primitives\" />\n",
        "\n",
        "### Passaggio 3: eseguire utilizzando Qiskit primitives\n",
        "\n",
        "Ora possiamo eseguire l'esperimento sul backend del simulatore senza rumore.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "71750073",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "layout [0, 1, 2, 3]\n",
            "qubits: [0, 1, 2, 3], mse:, 0.0\n",
            " \n"
          ]
        }
      ],
      "source": [
        "# Run on noiseless simulator\n",
        "sampler_sim = Sampler(mode=aer_backend)\n",
        "sim_job = sampler_sim.run(isa_sim_circuits)\n",
        "sim_mse = get_mse(sim_job.result(), sim_layout)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4b0b451f",
      "metadata": {},
      "source": [
        "<span id=\"step-4-post-process-and-return-result-in-the-desired-classical-format\" />\n",
        "\n",
        "### Fase 4: Post-elaborazione e restituzione del risultato nel formato classico desiderato\n",
        "\n",
        "Il passo finale consiste nel calcolare la metrica dell'errore quadratico medio (MSE) per ciascun gruppo di qubit testato e riassumere i risultati. Per ogni catena, ora abbiamo i valori misurati $\\langle Z_0Z_3\\rangle$ e $\\langle X_0X_3\\rangle$. Se i qubit 0 e 3 fossero perfettamente intrecciati in uno stato $|\\Phi^+\\rangle$ di Bell, ci aspetteremmo che entrambi fossero +1. Quantifichiamo la deviazione utilizzando l'MSE:\n",
        "\n",
        "$\\text{MSE} = \\frac{( \\langle Z_0Z_3\\rangle - 1)^2 + (\\langle X_0X_3\\rangle - 1)^2}{2}.$\n",
        "\n",
        "Questo valore è pari a 0 per una coppia di Bell perfetta e aumenta man mano che lo stato intrecciato diventa più rumoroso (con risultati casuali che danno un valore atteso intorno a 0, l'errore quadratico medio si avvicinerebbe a 1). Il codice calcola questo MSE per ciascun gruppo di quattro qubit. In assenza di rumore, in questo esempio di simulatore su piccola scala osserviamo che MSE = 0, come previsto.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c8e42569",
      "metadata": {},
      "source": [
        "<span id=\"large-scale-hardware-example\" />\n",
        "\n",
        "## Esempio di hardware su larga scala\n",
        "\n",
        "A questo punto riuniamo tutti questi dettagli in un unico flusso di lavoro su scala più ampia, che viene poi eseguito su hardware quantistico reale.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "55f49481",
      "metadata": {},
      "outputs": [],
      "source": [
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(operational=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "27ee6db2",
      "metadata": {},
      "source": [
        "Effettuiamo una ricerca automatica nella mappa di accoppiamento del dispositivo per individuare tutte le catene di quattro qubit collegate linearmente. Ciascuna di queste catene (contrassegnata dagli indici dei qubit $[q0-q1-q2-q3]$ ) funge da caso di prova per il circuito di scambio di entanglement. Identificando tutti i possibili percorsi di length-4, garantiamo la massima copertura per i possibili raggruppamenti di qubit in grado di attuare il protocollo.\n",
        "\n",
        "Generiamo queste catene utilizzando una funzione di supporto che esegue una ricerca avida sul grafico dei dispositivi. Restituisce \"strisce\" di quattro catene da quattro qubit raggruppate in gruppi da 16 qubit. Il bundling ci permette di eseguire più esperimenti a quattro qubit in parallelo su parti distinte del chip e di sfruttare in modo efficiente l'intero dispositivo. Ogni striscia da 16 qubit contiene quattro catene disgiunte, il che significa che nessun qubit viene riutilizzato all'interno di quel gruppo. Ad esempio, una striscia potrebbe essere costituita dalle catene $[0-1-2-3]$, $[4-5-6-7]$, $[8-9-10-11]$ e $[12-13-14-15]$, tutte raggruppate insieme. Qualsiasi qubit che non sia stato incluso in una striscia viene restituito nella `leftover` variabile.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "0cae5453",
      "metadata": {},
      "outputs": [],
      "source": [
        "from itertools import chain\n",
        "from collections import defaultdict\n",
        "\n",
        "\n",
        "def stripes16_from_backend(backend):\n",
        "    \"\"\"\n",
        "    Creates stripes of 16 qubits, four non-overlapping\n",
        "    four-qubit chains, that cover as much of the coupling\n",
        "    map as possible. Returns any unused qubits as leftovers.\n",
        "    \"\"\"\n",
        "    # get the undirected adjacency list\n",
        "    edges = backend.coupling_map.get_edges()\n",
        "    graph = defaultdict(set)\n",
        "    for u, v in edges:\n",
        "        graph[u].add(v)\n",
        "        graph[v].add(u)\n",
        "\n",
        "    qubits = sorted(graph)  # all qubit indices that appear\n",
        "\n",
        "    # greedy search for 4-long linear chains (blocks) ────────────\n",
        "    used = set()  # qubits already placed in a block\n",
        "    blocks = []  # each block is a four-qubit list\n",
        "\n",
        "    for q in qubits:  # deterministic order for reproducibility\n",
        "        if q in used:\n",
        "            continue  # already consumed by earlier block\n",
        "\n",
        "        # depth-first \"straight\" walk of length 3 without revisiting nodes\n",
        "        def extend(path):\n",
        "            if len(path) == 4:\n",
        "                return path\n",
        "            tip = path[-1]\n",
        "            for nbr in sorted(graph[tip]):  # deterministic\n",
        "                if nbr not in path and nbr not in used:\n",
        "                    maybe = extend(path + [nbr])\n",
        "                    if maybe:\n",
        "                        return maybe\n",
        "            return None\n",
        "\n",
        "        block = extend([q])\n",
        "        if block:  # found a 4-node path\n",
        "            blocks.append(block)\n",
        "            used.update(block)\n",
        "\n",
        "    # bundle four four-qubit blocks into one 16-qubit\n",
        "    # stripe (max number of measurement compatible with if-else)\n",
        "    stripes = [\n",
        "        list(chain.from_iterable(blocks[i : i + 4]))\n",
        "        for i in range(0, len(blocks) // 4 * 4, 4)  # full groups of four\n",
        "    ]\n",
        "\n",
        "    leftovers = set(qubits) - set(chain.from_iterable(stripes))\n",
        "    return stripes, leftovers"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "3cf9db43",
      "metadata": {},
      "outputs": [],
      "source": [
        "initial_layouts, leftover = stripes16_from_backend(backend)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8121aa97",
      "metadata": {},
      "source": [
        "Successivamente, costruiamo il circuito per ciascuna striscia da 16 qubit utilizzando la funzione `create_bell_stab()`. Al termine di questa fase, disponiamo di un elenco di circuiti che coprono tutte le catene a quattro qubit presenti sul dispositivo. Successivamente, trasponiamo ed eseguiamo i circuiti sul backend reale, quindi elaboriamo i risultati.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "bd04755f",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "layout [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]\n",
            "qubits: [0, 1, 2, 3], mse:, 0.6302\n",
            "qubits: [4, 5, 6, 7], mse:, 0.0949\n",
            "qubits: [8, 9, 10, 11], mse:, 0.1729\n",
            "qubits: [12, 13, 14, 15], mse:, 0.0473\n",
            " \n",
            "layout [16, 23, 22, 21, 17, 27, 26, 25, 18, 31, 30, 29, 19, 35, 34, 33]\n",
            "qubits: [16, 23, 22, 21], mse:, 0.0533\n",
            "qubits: [17, 27, 26, 25], mse:, 0.2966\n",
            "qubits: [18, 31, 30, 29], mse:, 0.0447\n",
            "qubits: [19, 35, 34, 33], mse:, 0.0392\n",
            " \n",
            "layout [36, 41, 42, 43, 37, 45, 46, 47, 38, 49, 50, 51, 39, 53, 54, 55]\n",
            "qubits: [36, 41, 42, 43], mse:, 0.1577\n",
            "qubits: [37, 45, 46, 47], mse:, 0.0705\n",
            "qubits: [38, 49, 50, 51], mse:, 0.2914\n",
            "qubits: [39, 53, 54, 55], mse:, 0.1711\n",
            " \n",
            "layout [56, 63, 62, 61, 57, 67, 66, 65, 58, 71, 70, 69, 59, 75, 74, 73]\n",
            "qubits: [56, 63, 62, 61], mse:, 0.1236\n",
            "qubits: [57, 67, 66, 65], mse:, 0.9969\n",
            "qubits: [58, 71, 70, 69], mse:, 0.0631\n",
            "qubits: [59, 75, 74, 73], mse:, 0.0301\n",
            " \n",
            "layout [76, 81, 82, 83, 77, 85, 86, 87, 78, 89, 90, 91, 79, 93, 94, 95]\n",
            "qubits: [76, 81, 82, 83], mse:, 0.2787\n",
            "qubits: [77, 85, 86, 87], mse:, 0.0497\n",
            "qubits: [78, 89, 90, 91], mse:, 0.1271\n",
            "qubits: [79, 93, 94, 95], mse:, 0.0468\n",
            " \n",
            "layout [96, 103, 102, 101, 97, 107, 106, 105, 98, 111, 110, 109, 99, 115, 114, 113]\n",
            "qubits: [96, 103, 102, 101], mse:, 0.8657\n",
            "qubits: [97, 107, 106, 105], mse:, 0.0399\n",
            "qubits: [98, 111, 110, 109], mse:, 0.0667\n",
            "qubits: [99, 115, 114, 113], mse:, 0.2444\n",
            " \n",
            "layout [116, 121, 122, 123, 117, 125, 126, 127, 118, 129, 130, 131, 119, 133, 134, 135]\n",
            "qubits: [116, 121, 122, 123], mse:, 0.0429\n",
            "qubits: [117, 125, 126, 127], mse:, 0.0487\n",
            "qubits: [118, 129, 130, 131], mse:, 0.0823\n",
            "qubits: [119, 133, 134, 135], mse:, 0.0583\n",
            " \n",
            "layout [136, 143, 142, 141, 137, 147, 146, 145, 138, 151, 150, 149, 139, 155, 154, 153]\n",
            "qubits: [136, 143, 142, 141], mse:, 0.0209\n",
            "qubits: [137, 147, 146, 145], mse:, 0.0384\n",
            "qubits: [138, 151, 150, 149], mse:, 0.4941\n",
            "qubits: [139, 155, 154, 153], mse:, 0.1062\n",
            " \n"
          ]
        }
      ],
      "source": [
        "# -------------------------Step 1-------------------------\n",
        "circuits = create_bell_stab(initial_layouts)\n",
        "\n",
        "# -------------------------Step 2-------------------------\n",
        "isa_circuits = []\n",
        "for ind, init_layout in enumerate(initial_layouts):\n",
        "    pm = generate_preset_pass_manager(\n",
        "        optimization_level=0, backend=backend, initial_layout=init_layout\n",
        "    )\n",
        "    isa_circ = pm.run(circuits[ind * 2 : ind * 2 + 2])\n",
        "    isa_circuits.extend(isa_circ)\n",
        "isa_circuits[1].draw(\"mpl\", fold=-1, idle_wires=False)\n",
        "\n",
        "# -------------------------Step 3-------------------------\n",
        "sampler = Sampler(mode=backend)\n",
        "sampler.options.environment.job_tags = [\"TUT_BDC\"]\n",
        "job = sampler.run(isa_circuits)\n",
        "\n",
        "# -------------------------Step 4-------------------------\n",
        "layouts_mse = get_mse(job.result(), initial_layouts)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "87484abf",
      "metadata": {},
      "source": [
        "I risultati rivelano un'ampia gamma di qualità di entanglement in tutto il dispositivo. Ciò conferma la conclusione dell'articolo secondo cui può esserci una variazione di oltre un ordine di grandezza nella fedeltà dello stato di Bell a seconda dei qubit fisici utilizzati. In termini pratici, ciò significa che alcune regioni o collegamenti del chip sono molto più efficaci di altri nell'eseguire misurazioni a metà circuito e operazioni di feedforward. Fattori quali errori di lettura dei qubit, durata dei qubit e crosstalk contribuiscono probabilmente a queste differenze. Ad esempio, se una catena include un qubit di lettura particolarmente rumoroso, la misurazione a metà circuito potrebbe essere inaffidabile, portando a una scarsa fedeltà per quella coppia intrecciata (MSE elevato).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5af0facd",
      "metadata": {},
      "source": [
        "Infine, visualizziamo le prestazioni complessive tracciando la funzione di distribuzione cumulativa (CDF) dei valori MSE per tutte le catene. Il grafico CDF mostra la soglia MSE sull'asse x e la frazione di coppie di qubit che hanno al massimo quell'MSE sull'asse y. Questa curva parte da zero e si avvicina a uno man mano che la soglia cresce fino a comprendere tutti i punti dati. Un aumento ripido vicino a un MSE basso indicherebbe che molte coppie sono ad alta fedeltà; un aumento lento significa che molte coppie presentano errori più grandi. Annotiamo il CDF con le identità delle coppie migliori. Nel grafico, ogni punto nella CDF corrisponde all'MSE di una catena di quattro qubit e contrassegniamo il punto con la coppia di indici di $[q0, q3]$ qubit che erano intrecciati in quell'esperimento. Questo rende facile individuare quali coppie di qubit fisici sono quelle con le prestazioni migliori (i punti all'estrema sinistra della CDF).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "678ddac9",
      "metadata": {},
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/edc-cut-bell-pair-benchmarking/extracted-outputs/678ddac9-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "plot_mse_ecdfs(layouts_mse, combine_layouts=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ba738d2b",
      "metadata": {},
      "source": [
        "<span id=\"next-steps\" />\n",
        "\n",
        "## Passi successivi\n",
        "\n",
        "<Admonition type=\"tip\" title=\"Suggerimenti\">\n",
        "  Se questo lavoro ti è sembrato interessante, potrebbero interessarti anche i seguenti materiali:\n",
        "\n",
        "  * Scopri come implementare [l'entanglement a lunga distanza con circuiti dinamici](/docs/tutorials/long-range-entanglement)\n",
        "  * Scopri come simulare [l](/docs/tutorials/dc-hex-ising) 'Hamiltoniano di Ising con kickback utilizzando circuiti dinamici\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "109cd9c3",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Riferimenti\n",
        "\n",
        "[\\[1\\] Carrera Vázquez, A., Tornow, C., Ristè, D. et al. Combinazione di processori quantistici con comunicazioni classiche in tempo reale. Nature 636, 75-79 (2024)](https://www.nature.com/articles/s41586-024-08178-2).\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"
    },
    "widgets": {
      "application/vnd.jupyter.widget-state+json": {
        "state": {},
        "version_major": 2,
        "version_minor": 0
      }
    },
    "hours": 1,
    "qpuSeconds": 22
  },
  "nbformat": 4,
  "nbformat_minor": 5
}